import { Env } from "@tsplus/stdlib/service/Env" import type { Tag } from "@tsplus/stdlib/service/Tag" export const PatchSym = Symbol.for("@tsplus/stdlib/service/Patch") export type PatchSym = typeof PatchSym export const _Input = Symbol.for("@tsplus/stdlib/service/Patch/Input") export type _Input = typeof _Input export const _Output = Symbol.for("@tsplus/stdlib/service/Patch/Output") export type _Output = typeof _Output /** * A `Patch` describes an update that transforms a `Env` * to a `Env` as a data structure. This allows combining updates to * different services in the environment in a compositional way. * * @tsplus type Patch */ export interface Patch { readonly [PatchSym]: PatchSym readonly [_Input]: (input: Input) => void readonly [_Output]: () => Output } /** * @tsplus type Patch.Ops */ export interface PatchOps { $: PatchAspects } export const Patch: PatchOps = { $: {} } /** * @tsplus type Patch.Aspects */ export interface PatchAspects {} export abstract class BasePatch implements Patch { readonly [PatchSym]: PatchSym = PatchSym readonly [_Input]!: (input: Input) => void readonly [_Output]!: () => Output } export class Empty extends BasePatch { readonly _tag = "Empty" constructor() { super() } } export class AddService extends BasePatch { readonly _tag = "AddService" constructor(readonly tag: Tag, readonly service: T) { super() } } export class AndThen extends BasePatch { readonly _tag = "AndThen" constructor(readonly first: Patch, readonly second: Patch) { super() } } export class RemoveService extends BasePatch { readonly _tag = "RemoveService" constructor(readonly tag: Tag) { super() } } export class UpdateService extends BasePatch { readonly _tag = "UpdateService" constructor(readonly tag: Tag, readonly update: (service: T) => T) { super() } } /** * @tsplus macro remove */ export function concretePatch( _: Patch ): asserts _ is | Empty | AddService | AndThen | RemoveService | UpdateService { // } /** * Applies a `Patch` to the specified `Env` to produce a new patched `Env`. * * @tsplus static Patch.Aspects patch * @tsplus pipeable Patch patch */ export function patch(env: Env) { return (self: Patch): Env => { const updatedRef = { ref: false } const updated = patchLoop( new Map(env.unsafeMap), List(self as Patch), updatedRef ) if (!updatedRef.ref) { return new Env(updated) as Env } const map = new Map() for (const [tag] of env.unsafeMap) { if (updated.has(tag)) { map.set(tag, updated.get(tag)) updated.delete(tag) } } for (const [tag, s] of updated) { map.set(tag, s) } return new Env(map) as Env } } /** * @tsplus tailRec */ function patchLoop( env: Map, unknown>, patches: List>, updatedRef: { ref: boolean } ): Map, unknown> { if (patches.isNil()) { return env } const head = patches.head concretePatch(head) const tail = patches.tail switch (head._tag) { case "Empty": { return patchLoop(env, tail, updatedRef) } case "AddService": { return patchLoop(env.set(head.tag, head.service), tail, updatedRef) } case "AndThen": { return patchLoop(env, tail.prependAll(List(head.first, head.second)), updatedRef) } case "RemoveService": { return patchLoop((env.delete(head.tag), env), tail, updatedRef) } case "UpdateService": { return patchLoop( env.set(head.tag, head.update(env.get(head.tag))), tail, (updatedRef.ref = true, updatedRef) ) } } } /** * An empty patch which returns the environment unchanged. * * @tsplus static Patch.Ops empty */ export function empty(): Patch { return new Empty() } /** * Combines two patches to produce a new patch that describes applying the * updates from this patch and then the updates from the specified patch. * * @tsplus static Patch.Aspects combine * @tsplus pipeable Patch combine */ export function combine(that: Patch) { return (self: Patch): Patch => new AndThen(self, that) } /** * @tsplus static Patch.Ops diff */ export function diff( oldValue: Env, newValue: Env ): Patch { const missingServices = new Map(oldValue.unsafeMap) let patch = Patch.empty() for (const [tag, newService] of newValue.unsafeMap.entries()) { if (missingServices.has(tag)) { const old = missingServices.get(tag)! missingServices.delete(tag) if (old !== newService) { patch = patch.combine(new UpdateService(tag, () => newService)) } } else { missingServices.delete(tag) patch = patch.combine(new AddService(tag, newService)) } } for (const [tag] of missingServices.entries()) { patch = patch.combine(new RemoveService(tag)) } return patch }