| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- export * as State from "./state"
- import { Effect, Scope, Semaphore } from "effect"
- import type { Draft, Objectish } from "immer"
- /**
- * A replayable contribution applied to an editor during rebuild.
- *
- * Transforms are intentionally synchronous and mutation-shaped: domain editors
- * hide the draft representation while preserving concise plugin/config code.
- */
- export type Transform<Editor> = (editor: Editor) => void
- export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
- export interface Options<State extends Objectish, Editor> {
- /** Creates the base value for initial state and every scoped-transform rebuild. */
- readonly initial: () => State
- /** Wraps the mutable draft in a domain-specific editor. */
- readonly editor: MakeEditor<State, Editor>
- /**
- * Completes every committed edit.
- *
- * For rebuilds, this runs after all active transforms have been replayed and
- * before the rebuilt state becomes visible. For direct updates, this runs
- * after the current state has already been edited. The optional reason is
- * caller-defined metadata for exceptional update origins.
- */
- readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
- }
- export interface Interface<State extends Objectish, Editor> {
- readonly get: () => State
- /**
- * Registers a scoped transform slot and returns the slot updater.
- *
- * Acquiring the slot has no visible effect until the returned updater is
- * called. Each updater call replaces that slot's transform, then rebuilds the
- * materialized state from `initial()` by replaying all active transforms in
- * registration order. Closing the owning Scope removes the slot and rebuilds.
- */
- readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
- /**
- * Mutates the current materialized state directly.
- *
- * This is not replayable contribution state: a later rebuild starts again
- * from `initial()` plus active transforms, so direct edits must be reserved
- * for current-state adjustments that are intentionally outside the transform
- * fold.
- */
- readonly update: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
- }
- export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
- let state = options.initial()
- let transforms: { update: Transform<Editor> }[] = []
- const semaphore = Semaphore.makeUnsafe(1)
- const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) {
- const api = options.editor(next as Draft<State>)
- if (options.finalize) yield* options.finalize(api, reason)
- state = next
- })
- const rebuild = Effect.fnUntraced(function* () {
- const next = options.initial()
- const api = options.editor(next as Draft<State>)
- for (const transform of transforms)
- yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
- yield* commit(next)
- })
- return {
- get: () => state,
- transform: Effect.fn("State.transform")(function* () {
- const scope = yield* Scope.Scope
- return yield* Effect.uninterruptible(
- Effect.gen(function* () {
- const transform = { update: (_editor: Editor) => {} }
- transforms = [...transforms, transform]
- yield* Scope.addFinalizer(
- scope,
- semaphore.withPermit(
- Effect.sync(() => {
- transforms = transforms.filter((item) => item !== transform)
- }).pipe(Effect.andThen(rebuild())),
- ),
- )
- return (update: Transform<Editor>) =>
- Effect.uninterruptible(
- semaphore.withPermit(
- Effect.sync(() => {
- transform.update = update
- }).pipe(Effect.andThen(rebuild())),
- ),
- )
- }),
- )
- }),
- update: Effect.fn("State.update")(function* (update, reason) {
- const api = options.editor(state as Draft<State>)
- yield* update(api)
- if (options.finalize) yield* options.finalize(api, reason)
- }, semaphore.withPermit),
- }
- }
|