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
}