import * as Chunk from "@effect/data/Chunk" import type { Context, Tag } from "@effect/data/Context" import { Structural } from "@effect/data/Data" import type { Differ } from "@effect/data/Differ" import * as Equal from "@effect/data/Equal" import * as Dual from "@effect/data/Function" import { makeContext } from "@effect/data/internal/Context" /** @internal */ export const ContextPatchTypeId: Differ.Context.TypeId = Symbol.for( "@effect/data/DifferContextPatch" ) as Differ.Context.TypeId function variance(a: A): B { return a as unknown as B } /** @internal */ const PatchProto = Object.setPrototypeOf({ [ContextPatchTypeId]: { _Value: variance, _Patch: variance } }, Structural.prototype) interface Empty extends Differ.Context.Patch { readonly _tag: "Empty" } const EmptyProto = Object.setPrototypeOf({ _tag: "Empty" }, PatchProto) const _empty = Object.create(EmptyProto) /** * @internal */ export const empty = (): Differ.Context.Patch => _empty /** @internal */ export interface AndThen extends Differ.Context.Patch { readonly _tag: "AndThen" readonly first: Differ.Context.Patch readonly second: Differ.Context.Patch } const AndThenProto = Object.setPrototypeOf({ _tag: "AndThen" }, PatchProto) const makeAndThen = ( first: Differ.Context.Patch, second: Differ.Context.Patch ): Differ.Context.Patch => { const o = Object.create(AndThenProto) o.first = first o.second = second return o } /** @internal */ export interface AddService extends Differ.Context.Patch { readonly _tag: "AddService" readonly tag: Tag readonly service: T } const AddServiceProto = Object.setPrototypeOf({ _tag: "AddService" }, PatchProto) const makeAddService = ( tag: Tag, service: T ): Differ.Context.Patch => { const o = Object.create(AddServiceProto) o.tag = tag o.service = service return o } /** @internal */ export interface RemoveService extends Differ.Context.Patch> { readonly _tag: "RemoveService" readonly tag: Tag } const RemoveServiceProto = Object.setPrototypeOf({ _tag: "RemoveService" }, PatchProto) const makeRemoveService = ( tag: Tag ): Differ.Context.Patch> => { const o = Object.create(RemoveServiceProto) o.tag = tag return o } /** @internal */ export interface UpdateService extends Differ.Context.Patch { readonly _tag: "UpdateService" readonly tag: Tag readonly update: (service: T) => T } const UpdateServiceProto = Object.setPrototypeOf({ _tag: "UpdateService" }, PatchProto) const makeUpdateService = ( tag: Tag, update: (service: T) => T ): Differ.Context.Patch => { const o = Object.create(UpdateServiceProto) o.tag = tag o.update = update return o } type Instruction = | Empty | AndThen | AddService | RemoveService | UpdateService /** @internal */ export const diff = ( oldValue: Context, newValue: Context ): Differ.Context.Patch => { const missingServices = new Map(oldValue.unsafeMap) let patch = empty() for (const [tag, newService] of newValue.unsafeMap.entries()) { if (missingServices.has(tag)) { const old = missingServices.get(tag)! missingServices.delete(tag) if (!Equal.equals(old, newService)) { patch = combine(makeUpdateService(tag, () => newService))(patch) } } else { missingServices.delete(tag) patch = combine(makeAddService(tag, newService))(patch) } } for (const [tag] of missingServices.entries()) { patch = combine(makeRemoveService(tag))(patch) } return patch } /** @internal */ export const combine = Dual.dual< ( that: Differ.Context.Patch ) => ( self: Differ.Context.Patch ) => Differ.Context.Patch, ( self: Differ.Context.Patch, that: Differ.Context.Patch ) => Differ.Context.Patch >(2, (self, that) => makeAndThen(self, that)) /** @internal */ export const patch = Dual.dual< ( context: Context ) => ( self: Differ.Context.Patch ) => Context, ( self: Differ.Context.Patch, context: Context ) => Context >(2, (self: Differ.Context.Patch, context: Context) => { let wasServiceUpdated = false let patches: Chunk.Chunk> = Chunk.of( self as Differ.Context.Patch ) const updatedContext: Map, unknown> = new Map(context.unsafeMap) while (Chunk.isNonEmpty(patches)) { const head: Instruction = Chunk.headNonEmpty(patches) as Instruction const tail = Chunk.tailNonEmpty(patches) switch (head._tag) { case "Empty": { patches = tail break } case "AddService": { updatedContext.set(head.tag, head.service) patches = tail break } case "AndThen": { patches = Chunk.prepend(Chunk.prepend(tail, head.second), head.first) break } case "RemoveService": { updatedContext.delete(head.tag) patches = tail break } case "UpdateService": { updatedContext.set(head.tag, head.update(updatedContext.get(head.tag))) wasServiceUpdated = true patches = tail break } } } if (!wasServiceUpdated) { return makeContext(updatedContext) as Context } const map = new Map() for (const [tag] of context.unsafeMap) { if (updatedContext.has(tag)) { map.set(tag, updatedContext.get(tag)) updatedContext.delete(tag) } } for (const [tag, s] of updatedContext) { map.set(tag, s) } return makeContext(map) as Context })