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" import { pipeArguments } from "../Pipeable.js" import { hasProperty } from "../Predicate.js" import type * as STM from "../STM.js" import type { NoInfer } from "../Types.js" import { EffectPrototype, effectVariance } from "./effectable.js" 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" /** @internal */ export const STMTypeId: STM.STMTypeId = Symbol.for( STMSymbolKey ) as STM.STMTypeId /** @internal */ export const TagProto: any = { ...EffectPrototype, _op: "Tag", [STMTypeId]: effectVariance, [TagTypeId]: { _Service: (_: unknown) => _, _Identifier: (_: unknown) => _ }, toString() { return format(this.toJSON()) }, toJSON(this: C.Tag) { return { _id: "Tag", key: this.key, stack: this.stack } }, [NodeInspectSymbol]() { return this.toJSON() }, of(self: Service): Service { return self }, context( this: C.Tag, self: Service ): C.Context { return make(this, self) } } export const ReferenceProto: any = { ...TagProto, [ReferenceTypeId]: ReferenceTypeId } /** @internal */ export const makeGenericTag = (key: string): C.Tag => { const limit = Error.stackTraceLimit Error.stackTraceLimit = 2 const creationError = new Error() Error.stackTraceLimit = limit const tag = Object.create(TagProto) Object.defineProperty(tag, "stack", { get() { return creationError.stack } }) tag.key = key return tag } /** @internal */ export const Tag = (id: Id) => (): C.TagClass => { const limit = Error.stackTraceLimit Error.stackTraceLimit = 2 const creationError = new Error() Error.stackTraceLimit = limit function TagClass() {} Object.setPrototypeOf(TagClass, TagProto) TagClass.key = id Object.defineProperty(TagClass, "stack", { get() { return creationError.stack } }) 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 /** @internal */ export const ContextProto: Omit, "unsafeMap"> = { [TypeId]: { _Services: (_: unknown) => _ }, [Equal.symbol](this: C.Context, that: unknown): boolean { if (isContext(that)) { if (this.unsafeMap.size === that.unsafeMap.size) { for (const k of this.unsafeMap.keys()) { if (!that.unsafeMap.has(k) || !Equal.equals(this.unsafeMap.get(k), that.unsafeMap.get(k))) { return false } } return true } } return false }, [Hash.symbol](this: C.Context): number { return Hash.cached(this, Hash.number(this.unsafeMap.size)) }, pipe(this: C.Context) { return pipeArguments(this, arguments) }, toString() { return format(this.toJSON()) }, toJSON(this: C.Context) { return { _id: "Context", services: Array.from(this.unsafeMap).map(toJSON) } }, [NodeInspectSymbol]() { return (this as any).toJSON() } } /** @internal */ export const makeContext = (unsafeMap: Map): C.Context => { const context = Object.create(ContextProto) context.unsafeMap = unsafeMap return context } const serviceNotFoundError = (tag: C.Tag) => { const error = new Error(`Service not found${tag.key ? `: ${String(tag.key)}` : ""}`) if (tag.stack) { const lines = tag.stack.split("\n") if (lines.length > 2) { const afterAt = lines[2].match(/at (.*)/) if (afterAt) { error.message = error.message + ` (defined at ${afterAt[1]})` } } } if (error.stack) { const lines = error.stack.split("\n") lines.splice(1, 3) error.stack = lines.join("\n") } return error } /** @internal */ export const isContext = (u: unknown): u is C.Context => hasProperty(u, TypeId) /** @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 */ export const empty = (): C.Context => _empty /** @internal */ export const make = (tag: C.Tag, service: NoInfer): C.Context => makeContext(new Map([[tag.key, service]])) /** @internal */ export const add = dual< ( tag: C.Tag, service: NoInfer ) => ( self: C.Context ) => C.Context, ( self: C.Context, tag: C.Tag, service: NoInfer ) => C.Context >(3, (self, tag, service) => { const map = new Map(self.unsafeMap) map.set(tag.key, service) 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)) { 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: C.Tag): (self: C.Context) => S (self: C.Context, tag: C.Reference): S (self: C.Context, tag: C.Tag): S } = unsafeGet as any /** @internal */ export const getOrElse = dual< (tag: C.Tag, orElse: LazyArg) => (self: C.Context) => S | B, (self: C.Context, tag: C.Tag, orElse: LazyArg) => S | B >(3, (self, tag, orElse) => { if (!self.unsafeMap.has(tag.key)) { return isReference(tag) ? getDefaultValue(tag) : orElse() } return self.unsafeMap.get(tag.key)! as any }) /** @internal */ export const getOption = dual< (tag: C.Tag) => (self: C.Context) => O.Option, (self: C.Context, tag: C.Tag) => O.Option >(2, (self, tag) => { if (!self.unsafeMap.has(tag.key)) { return isReference(tag) ? option.some(getDefaultValue(tag)) : option.none } return option.some(self.unsafeMap.get(tag.key)! as any) }) /** @internal */ export const merge = dual< (that: C.Context) => (self: C.Context) => C.Context, (self: C.Context, that: C.Context) => C.Context >(2, (self, that) => { const map = new Map(self.unsafeMap) for (const [tag, s] of that.unsafeMap) { map.set(tag, s) } return makeContext(map) }) /** @internal */ export const mergeAll = >( ...ctxs: [...{ [K in keyof T]: C.Context }] ): C.Context => { const map = new Map() for (const ctx of ctxs) { for (const [tag, s] of ctx.unsafeMap) { map.set(tag, s) } } return makeContext(map) } /** @internal */ export const pick = >>(...tags: Tags) => (self: C.Context): C.Context< Services & C.Tag.Identifier > => { const tagSet = new Set(tags.map((_) => _.key)) const newEnv = new Map() for (const [tag, s] of self.unsafeMap.entries()) { if (tagSet.has(tag)) { newEnv.set(tag, s) } } return makeContext(newEnv) } /** @internal */ export const omit = >>(...tags: Tags) => (self: C.Context): C.Context< Exclude> > => { const newEnv = new Map(self.unsafeMap) for (const tag of tags) { newEnv.delete(tag.key) } return makeContext(newEnv) }