import * as Chunk from "@effect/data/Chunk" import { Structural } from "@effect/data/Data" import type * as Differ from "@effect/data/Differ" import * as Equal from "@effect/data/Equal" import * as Dual from "@effect/data/Function" import * as HashMap from "@effect/data/HashMap" /** @internal */ export const HashMapPatchTypeId: Differ.Differ.HashMap.TypeId = Symbol.for( "@effect/data/DifferHashMapPatch" ) as Differ.Differ.HashMap.TypeId function variance(a: A): B { return a as unknown as B } /** @internal */ const PatchProto = Object.setPrototypeOf({ [HashMapPatchTypeId]: { _Value: variance, _Key: variance, _Patch: variance } }, Structural.prototype) interface Empty extends Differ.Differ.HashMap.Patch { readonly _tag: "Empty" } const EmptyProto = Object.setPrototypeOf({ _tag: "Empty" }, PatchProto) const _empty = Object.create(EmptyProto) /** @internal */ export const empty = (): Differ.Differ.HashMap.Patch => _empty interface AndThen extends Differ.Differ.HashMap.Patch { readonly _tag: "AndThen" readonly first: Differ.Differ.HashMap.Patch readonly second: Differ.Differ.HashMap.Patch } const AndThenProto = Object.setPrototypeOf({ _tag: "AndThen" }, PatchProto) const makeAndThen = ( first: Differ.Differ.HashMap.Patch, second: Differ.Differ.HashMap.Patch ): Differ.Differ.HashMap.Patch => { const o = Object.create(AndThenProto) o.first = first o.second = second return o } interface Add extends Differ.Differ.HashMap.Patch { readonly _tag: "Add" readonly key: Key readonly value: Value } const AddProto = Object.setPrototypeOf({ _tag: "Add" }, PatchProto) const makeAdd = (key: Key, value: Value): Differ.Differ.HashMap.Patch => { const o = Object.create(AddProto) o.key = key o.value = value return o } interface Remove extends Differ.Differ.HashMap.Patch { readonly _tag: "Remove" readonly key: Key } const RemoveProto = Object.setPrototypeOf({ _tag: "Remove" }, PatchProto) const makeRemove = (key: Key): Differ.Differ.HashMap.Patch => { const o = Object.create(RemoveProto) o.key = key return o } interface Update extends Differ.Differ.HashMap.Patch { readonly _tag: "Update" readonly key: Key readonly patch: Patch } const UpdateProto = Object.setPrototypeOf({ _tag: "Update" }, PatchProto) const makeUpdate = (key: Key, patch: Patch): Differ.Differ.HashMap.Patch => { const o = Object.create(UpdateProto) o.key = key o.patch = patch return o } type Instruction = | Add | Remove | Update | Empty | AndThen /** @internal */ export const diff = ( options: { readonly oldValue: HashMap.HashMap readonly newValue: HashMap.HashMap readonly differ: Differ.Differ } ): Differ.Differ.HashMap.Patch => { const [removed, patch] = HashMap.reduce( [options.oldValue, empty()] as const, ([map, patch], newValue: Value, key: Key) => { const option = HashMap.get(key)(map) switch (option._tag) { case "Some": { const valuePatch = options.differ.diff(option.value, newValue) if (Equal.equals(valuePatch, options.differ.empty)) { return [HashMap.remove(key)(map), patch] as const } return [ HashMap.remove(key)(map), combine(makeUpdate(key, valuePatch))(patch) ] as const } case "None": { return [map, combine(makeAdd(key, newValue))(patch)] as const } } } )(options.newValue) return HashMap.reduce( patch, (patch, _, key: Key) => combine(makeRemove(key))(patch) )(removed) } /** @internal */ export const combine = Dual.dual< ( that: Differ.Differ.HashMap.Patch ) => ( self: Differ.Differ.HashMap.Patch ) => Differ.Differ.HashMap.Patch, ( self: Differ.Differ.HashMap.Patch, that: Differ.Differ.HashMap.Patch ) => Differ.Differ.HashMap.Patch >(2, (self, that) => makeAndThen(self, that)) /** @internal */ export const patch = Dual.dual< ( oldValue: HashMap.HashMap, differ: Differ.Differ ) => ( self: Differ.Differ.HashMap.Patch ) => HashMap.HashMap, ( self: Differ.Differ.HashMap.Patch, oldValue: HashMap.HashMap, differ: Differ.Differ ) => HashMap.HashMap >(3, ( self: Differ.Differ.HashMap.Patch, oldValue: HashMap.HashMap, differ: Differ.Differ ) => { let map = oldValue let patches: Chunk.Chunk> = Chunk.of(self) 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 "AndThen": { patches = Chunk.prepend(head.first)(Chunk.prepend(head.second)(tail)) break } case "Add": { map = HashMap.set(head.key, head.value)(map) patches = tail break } case "Remove": { map = HashMap.remove(head.key)(map) patches = tail break } case "Update": { const option = HashMap.get(head.key)(map) if (option._tag === "Some") { map = HashMap.set(head.key, differ.patch(head.patch, option.value))(map) } patches = tail break } } } return map })