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<I, A>(this: C.Tag<I, A>) {
return {
_id: "Tag",
key: this.key,
stack: this.stack
}
},
[NodeInspectSymbol]() {
return this.toJSON()
},
of<Service>(self: Service): Service {
return self
},
context<Identifier, Service>(
this: C.Tag<Identifier, Service>,
self: Service
): C.Context<Identifier> {
return make(this, self)
}
}
export const ReferenceProto: any = {
...TagProto,
[ReferenceTypeId]: ReferenceTypeId
}
/** @internal */
export const makeGenericTag = <Identifier, Service = Identifier>(key: string): C.Tag<Identifier, Service> => {
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 = <const Id extends string>(id: Id) => <Self, Shape>(): C.TagClass<Self, Id, Shape> => {
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 = <Self>() =>
<const Id extends string, Service>(id: Id, options: {
readonly defaultValue: () => Service
}): C.ReferenceClass<Self, Id, Service> => {
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<C.Context<unknown>, "unsafeMap"> = {
[TypeId]: {
_Services: (_: unknown) => _
},
[Equal.symbol]<A>(this: C.Context<A>, 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]<A>(this: C.Context<A>): number {
return Hash.cached(this, Hash.number(this.unsafeMap.size))
},
pipe<A>(this: C.Context<A>) {
return pipeArguments(this, arguments)
},
toString() {
return format(this.toJSON())
},
toJSON<A>(this: C.Context<A>) {
return {
_id: "Context",
services: Array.from(this.unsafeMap).map(toJSON)
}
},
[NodeInspectSymbol]() {
return (this as any).toJSON()
}
}
/** @internal */
export const makeContext = <Services>(unsafeMap: Map<string, any>): C.Context<Services> => {
const context = Object.create(ContextProto)
context.unsafeMap = unsafeMap
return context
}
const serviceNotFoundError = (tag: C.Tag<any, any>) => {
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<never> => hasProperty(u, TypeId)
/** @internal */
export const isTag = (u: unknown): u is C.Tag<any, any> => hasProperty(u, TagTypeId)
/** @internal */
export const isReference = (u: unknown): u is C.Reference<any, any> => hasProperty(u, ReferenceTypeId)
const _empty = makeContext(new Map())
/** @internal */
export const empty = (): C.Context<never> => _empty
/** @internal */
export const make = <I, S>(tag: C.Tag<I, S>, service: NoInfer<S>): C.Context<I> =>
makeContext(new Map([[tag.key, service]]))
/** @internal */
export const add = dual<
<I, S>(
tag: C.Tag<I, S>,
service: NoInfer<S>
) => <Services>(
self: C.Context<Services>
) => C.Context<Services | I>,
<Services, I, S>(
self: C.Context<Services>,
tag: C.Tag<I, S>,
service: NoInfer<S>
) => C.Context<Services | I>
>(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<string, any>())
const getDefaultValue = (tag: C.Reference<any, any>) => {
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 = <Services, I, S>(self: C.Context<Services>, tag: C.Reference<I, S>): S => {
return self.unsafeMap.has(tag.key) ? self.unsafeMap.get(tag.key) : getDefaultValue(tag)
}
/** @internal */
export const unsafeGet = dual<
<S, I>(tag: C.Tag<I, S>) => <Services>(self: C.Context<Services>) => S,
<Services, S, I>(self: C.Context<Services>, tag: C.Tag<I, S>) => 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: {
<I, S>(tag: C.Reference<I, S>): <Services>(self: C.Context<Services>) => S
<Services, I extends Services, S>(tag: C.Tag<I, S>): (self: C.Context<Services>) => S
<Services, I, S>(self: C.Context<Services>, tag: C.Reference<I, S>): S
<Services, I extends Services, S>(self: C.Context<Services>, tag: C.Tag<I, S>): S
} = unsafeGet as any
/** @internal */
export const getOrElse = dual<
<S, I, B>(tag: C.Tag<I, S>, orElse: LazyArg<B>) => <Services>(self: C.Context<Services>) => S | B,
<Services, S, I, B>(self: C.Context<Services>, tag: C.Tag<I, S>, orElse: LazyArg<B>) => 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<
<S, I>(tag: C.Tag<I, S>) => <Services>(self: C.Context<Services>) => O.Option<S>,
<Services, S, I>(self: C.Context<Services>, tag: C.Tag<I, S>) => O.Option<S>
>(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<
<R1>(that: C.Context<R1>) => <Services>(self: C.Context<Services>) => C.Context<Services | R1>,
<Services, R1>(self: C.Context<Services>, that: C.Context<R1>) => C.Context<Services | R1>
>(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 = <T extends Array<unknown>>(
...ctxs: [...{ [K in keyof T]: C.Context<T[K]> }]
): C.Context<T[number]> => {
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 extends ReadonlyArray<C.Tag<any, any>>>(...tags: Tags) =>
<Services>(self: C.Context<Services>): C.Context<
Services & C.Tag.Identifier<Tags[number]>
> => {
const tagSet = new Set<string>(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 extends ReadonlyArray<C.Tag<any, any>>>(...tags: Tags) =>
<Services>(self: C.Context<Services>): C.Context<
Exclude<Services, C.Tag.Identifier<Tags[number]>>
> => {
const newEnv = new Map(self.unsafeMap)
for (const tag of tags) {
newEnv.delete(tag.key)
}
return makeContext(newEnv)
}