diff --git a/.changeset/funny-dolphins-joke.md b/.changeset/funny-dolphins-joke.md new file mode 100644 index 00000000000..8ff32327908 --- /dev/null +++ b/.changeset/funny-dolphins-joke.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add Context.Reference - a Tag with a default value diff --git a/packages/effect/src/Context.ts b/packages/effect/src/Context.ts index b91ef354607..964e1b7cd0a 100644 --- a/packages/effect/src/Context.ts +++ b/packages/effect/src/Context.ts @@ -45,6 +45,38 @@ export interface Tag extends Pipeable, Inspectable { [Unify.ignoreSymbol]?: TagUnifyIgnore } +const ReferenceTypeId: unique symbol = internal.ReferenceTypeId + +/** + * @since 3.11.0 + * @category symbol + */ +export type ReferenceTypeId = typeof ReferenceTypeId + +/** + * @since 3.11.0 + * @category models + */ +export interface Reference extends Pipeable, Inspectable { + readonly [ReferenceTypeId]: ReferenceTypeId + readonly defaultValue: () => Value + + readonly _op: "Tag" + readonly Service: Value + readonly Identifier: Id + readonly [TagTypeId]: { + readonly _Service: Types.Invariant + readonly _Identifier: Types.Invariant + } + of(self: Value): Value + context(self: Value): Context + readonly stack?: string | undefined + readonly key: string + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: TagUnify + [Unify.ignoreSymbol]?: TagUnifyIgnore +} + /** * @since 2.0.0 * @category models @@ -63,6 +95,14 @@ export interface TagClass extends Tag { new(_: never): TagClassShape } +/** + * @since 3.11.0 + * @category models + */ +export interface ReferenceClass extends Reference { + new(_: never): TagClassShape +} + /** * @category models * @since 2.0.0 @@ -172,6 +212,16 @@ export const isContext: (input: unknown) => input is Context = internal.i */ export const isTag: (input: unknown) => input is Tag = internal.isTag +/** + * Checks if the provided argument is a `Reference`. + * + * @param input - The value to be checked if it is a `Reference`. + * @since 3.11.0 + * @category guards + * @experimental + */ +export const isReference: (u: unknown) => u is Reference = internal.isReference + /** * Returns an empty `Context`. * @@ -259,7 +309,9 @@ export const add: { * @category getters */ export const get: { + (tag: Reference): (self: Context) => S >(tag: T): (self: Context) => Tag.Service + (self: Context, tag: Reference): S >(self: Context, tag: T): Tag.Service } = internal.get @@ -407,3 +459,22 @@ export const omit: >>( * @category constructors */ export const Tag: (id: Id) => () => TagClass = internal.Tag + +/** + * @example + * import { Context, Layer } from "effect" + * + * class MyTag extends Context.Reference()("MyTag", { + * defaultValue: () => ({ myNum: 108 }) + * }) { + * static Live = Layer.succeed(this, { myNum: 108 }) + * } + * + * @since 3.11.0 + * @category constructors + * @experimental + */ +export const Reference: () => ( + id: Id, + options: { readonly defaultValue: () => Service } +) => ReferenceClass = internal.Reference diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index 8f477fdc059..1a6c781bf34 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -154,6 +154,9 @@ declare module "./Context.js" { interface Tag extends Effect { [Symbol.iterator](): EffectGenerator> } + interface Reference extends Effect { + [Symbol.iterator](): EffectGenerator> + } interface TagUnifyIgnore { Effect?: true Either?: true diff --git a/packages/effect/src/STM.ts b/packages/effect/src/STM.ts index 4499c335f55..eb355ba201f 100644 --- a/packages/effect/src/STM.ts +++ b/packages/effect/src/STM.ts @@ -106,6 +106,8 @@ export interface STMTypeLambda extends TypeLambda { */ declare module "./Context.js" { interface Tag extends STM {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Reference extends STM {} } /** diff --git a/packages/effect/src/internal/context.ts b/packages/effect/src/internal/context.ts index e85765c9ba4..26f10d23263 100644 --- a/packages/effect/src/internal/context.ts +++ b/packages/effect/src/internal/context.ts @@ -2,6 +2,7 @@ import type * as C from "../Context.js" import * as Equal from "../Equal.js" import type { LazyArg } from "../Function.js" import { dual } from "../Function.js" +import { globalValue } from "../GlobalValue.js" import * as Hash from "../Hash.js" import { format, NodeInspectSymbol, toJSON } from "../Inspectable.js" import type * as O from "../Option.js" @@ -14,6 +15,9 @@ import * as option from "./option.js" /** @internal */ export const TagTypeId: C.TagTypeId = Symbol.for("effect/Context/Tag") as C.TagTypeId +/** @internal */ +export const ReferenceTypeId: C.ReferenceTypeId = Symbol.for("effect/Context/Reference") as C.ReferenceTypeId + /** @internal */ const STMSymbolKey = "effect/STM" @@ -55,6 +59,11 @@ export const TagProto: any = { } } +export const ReferenceProto: any = { + ...TagProto, + [ReferenceTypeId]: ReferenceTypeId +} + /** @internal */ export const makeGenericTag = (key: string): C.Tag => { const limit = Error.stackTraceLimit @@ -89,6 +98,28 @@ export const Tag = (id: Id) => (): C.TagCl return TagClass as any } +/** @internal */ +export const Reference = () => +(id: Id, options: { + readonly defaultValue: () => Service +}): C.ReferenceClass => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + + function ReferenceClass() {} + Object.setPrototypeOf(ReferenceClass, ReferenceProto) + ReferenceClass.key = id + ReferenceClass.defaultValue = options.defaultValue + Object.defineProperty(ReferenceClass, "stack", { + get() { + return creationError.stack + } + }) + return ReferenceClass as any +} + /** @internal */ export const TypeId: C.TypeId = Symbol.for("effect/Context") as C.TypeId @@ -162,6 +193,9 @@ export const isContext = (u: unknown): u is C.Context => hasProperty(u, T /** @internal */ export const isTag = (u: unknown): u is C.Tag => hasProperty(u, TagTypeId) +/** @internal */ +export const isReference = (u: unknown): u is C.Reference => hasProperty(u, ReferenceTypeId) + const _empty = makeContext(new Map()) /** @internal */ @@ -192,22 +226,40 @@ export const add = dual< return makeContext(map) }) +const defaultValueCache = globalValue("effect/Context/defaultValueCache", () => new Map()) +const getDefaultValue = (tag: C.Reference) => { + if (defaultValueCache.has(tag.key)) { + return defaultValueCache.get(tag.key) + } + const value = tag.defaultValue() + defaultValueCache.set(tag.key, value) + return value +} + +/** @internal */ +export const unsafeGetReference = (self: C.Context, tag: C.Reference): S => { + return self.unsafeMap.has(tag.key) ? self.unsafeMap.get(tag.key) : getDefaultValue(tag) +} + /** @internal */ export const unsafeGet = dual< (tag: C.Tag) => (self: C.Context) => S, (self: C.Context, tag: C.Tag) => S >(2, (self, tag) => { if (!self.unsafeMap.has(tag.key)) { - throw serviceNotFoundError(tag as any) + if (ReferenceTypeId in tag) return getDefaultValue(tag as any) + throw serviceNotFoundError(tag) } return self.unsafeMap.get(tag.key)! as any }) /** @internal */ export const get: { + (tag: C.Reference): (self: C.Context) => S >(tag: T): (self: C.Context) => C.Tag.Service + (self: C.Context, tag: C.Reference): S >(self: C.Context, tag: T): C.Tag.Service -} = unsafeGet +} = unsafeGet as any /** @internal */ export const getOrElse = dual< @@ -215,7 +267,7 @@ export const getOrElse = dual< (self: C.Context, tag: C.Tag, orElse: LazyArg) => S | B >(3, (self, tag, orElse) => { if (!self.unsafeMap.has(tag.key)) { - return orElse() + return isReference(tag) ? getDefaultValue(tag) : orElse() } return self.unsafeMap.get(tag.key)! as any }) @@ -226,7 +278,7 @@ export const getOption = dual< (self: C.Context, tag: C.Tag) => O.Option >(2, (self, tag) => { if (!self.unsafeMap.has(tag.key)) { - return option.none + return isReference(tag) ? option.some(getDefaultValue(tag)) : option.none } return option.some(self.unsafeMap.get(tag.key)! as any) }) diff --git a/packages/effect/test/Context.test.ts b/packages/effect/test/Context.test.ts index 3a50e86c4be..9475881cd35 100644 --- a/packages/effect/test/Context.test.ts +++ b/packages/effect/test/Context.test.ts @@ -21,6 +21,10 @@ const C = Context.GenericTag("C") class D extends Context.Tag("D")() {} +class E extends Context.Reference()("E", { + defaultValue: () => ({ e: 0 }) +}) {} + describe("Context", () => { it("Tag.toJson()", () => { const json: any = A.toJSON() @@ -81,6 +85,11 @@ describe("Context", () => { Context.getOption(C) )).toEqual(O.none()) + expect(pipe( + Services, + Context.get(E) + )).toEqual({ e: 0 }) + assert.throw(() => { pipe( Services, @@ -256,4 +265,9 @@ describe("Context", () => { expect(Context.isTag(Context.GenericTag("Demo"))).toEqual(true) expect(Context.isContext(null)).toEqual(false) }) + + it("isReference", () => { + expect(Context.isTag(E)).toEqual(true) + expect(Context.isReference(E)).toEqual(true) + }) })