export * as State from "./state" import { Effect, Scope, Semaphore } from "effect" import type { Draft, Objectish } from "immer" export type Transform = (editor: Editor) => void export type MakeEditor = (draft: Draft) => Editor export interface Options { readonly initial: () => State readonly editor: MakeEditor /** Completes every committed edit; reason identifies exceptional update origins. */ readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect } export interface Interface { readonly get: () => State readonly transform: () => Effect.Effect<(transform: Transform) => Effect.Effect, never, Scope.Scope> readonly update: (update: (editor: Editor) => Effect.Effect, reason?: string) => Effect.Effect } export function create(options: Options): Interface { let state = options.initial() let transforms: { update: Transform }[] = [] const semaphore = Semaphore.makeUnsafe(1) const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) { const api = options.editor(next as Draft) if (options.finalize) yield* options.finalize(api, reason) state = next }) const rebuild = Effect.fn("State.rebuild")(function* () { const next = options.initial() const api = options.editor(next as Draft) for (const transform of transforms) yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {})) yield* commit(next) }, semaphore.withPermit) return { get: () => state, transform: Effect.fn("State.transform")(function* () { const transform = { update: (_editor: Editor) => {} } transforms = [...transforms, transform] const scope = yield* Scope.Scope yield* Scope.addFinalizer( scope, Effect.sync(() => { transforms = transforms.filter((item) => item !== transform) }).pipe(Effect.andThen(rebuild())), ) return Effect.fnUntraced(function* (update: Transform) { transform.update = update yield* rebuild() }) }), update: Effect.fn("State.update")(function* (update, reason) { const api = options.editor(state as Draft) yield* update(api) if (options.finalize) yield* options.finalize(api, reason) }, semaphore.withPermit), } }