import type { packed } from "atom.io/foundations/canonical" import { packCanonical, unpackCanonical } from "atom.io/foundations/canonical" import type { Enumeration } from "atom.io/foundations/enumeration" import { enumeration } from "atom.io/foundations/enumeration" import type { primitive } from "atom.io/foundations/json" import { Subject } from "atom.io/foundations/subject" import type { Fn, Transceiver, TransceiverMode } from "atom.io/internal" export type SetMutations = Exclude< keyof Set, symbol | keyof ReadonlySet > export type SetUpdate

= | { type: `add` | `delete` value: P } | { type: `clear` values: P[] } export type UListUpdateType = SetUpdate[`type`] true satisfies SetMutations extends UListUpdateType ? true : Exclude export type PackedSetUpdate

= string & { update?: SetUpdate

} export const SET_UPDATE_ENUM: Enumeration<[`add`, `delete`, `clear`]> = enumeration([`add`, `delete`, `clear`] as const) export type SetMutationHandler = { [K in UListUpdateType]: Fn } export type UListView

= ReadonlySet

& { subscribe: ( key: string, fn: (update: PackedSetUpdate

) => void, ) => () => void } export class UList

extends Set

implements Transceiver, PackedSetUpdate

, ReadonlyArray

>, SetMutationHandler { public mode: TransceiverMode = `record` public readonly subject: Subject> = new Subject() public constructor(values?: Iterable

) { super(values) if (values instanceof UList) { } } public readonly READONLY_VIEW: UListView

= this public toJSON(): ReadonlyArray

{ return [...this] } public static fromJSON

(json: ReadonlyArray

): UList

{ return new UList

(json) } public add(value: P): this { const result = super.add(value) if (this.mode === `record`) { this.emit({ type: `add`, value }) } return result } public clear(): void { const capturedContents = this.mode === `record` ? [...this] : null super.clear() if (capturedContents) { this.emit({ type: `clear`, values: capturedContents }) } } public delete(value: P): boolean { const result = super.delete(value) if (this.mode === `record`) { this.emit({ type: `delete`, value }) } return result } public subscribe( key: string, fn: (update: PackedSetUpdate

) => void, ): () => void { return this.subject.subscribe(key, fn) } public emit(update: SetUpdate

): void { this.subject.next(UList.packUpdate(update)) } public do(packed: PackedSetUpdate

): null { this.mode = `playback` const update = UList.unpackUpdate(packed) switch (update.type) { case `add`: this.add(update.value) break case `delete`: this.delete(update.value) break case `clear`: this.clear() } this.mode = `record` return null } public undo(packed: PackedSetUpdate

): number | null { const update = UList.unpackUpdate(packed) this.mode = `playback` switch (update.type) { case `add`: this.delete(update.value) break case `delete`: this.add(update.value) break case `clear`: { const values = update.values for (const v of values) this.add(v) } } this.mode = `record` return null } public static packUpdate

( update: SetUpdate

, ): PackedSetUpdate

{ const head = SET_UPDATE_ENUM[update.type] + `\u001F` if (update.type === `clear`) { return head + update.values.map(packCanonical).join(`\u001E`) } return head + packCanonical(update.value) } public static unpackUpdate

( packed: PackedSetUpdate

, ): SetUpdate

{ const [type, tail] = packed.split(`\u001F`) as [0 | 1 | 2, packed

] const head = SET_UPDATE_ENUM[type] if (head === `clear`) { const values = tail.split(`\u001E`).map

(unpackCanonical) return { type: `clear`, values } } return { type: head, value: unpackCanonical(tail) } } }