state.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. export * as State from "./state"
  2. import { Effect, Scope, Semaphore } from "effect"
  3. import type { Draft, Objectish } from "immer"
  4. /**
  5. * A replayable contribution applied to an editor during rebuild.
  6. *
  7. * Transforms are intentionally synchronous and mutation-shaped: domain editors
  8. * hide the draft representation while preserving concise plugin/config code.
  9. */
  10. export type Transform<Editor> = (editor: Editor) => void
  11. export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
  12. export interface Options<State extends Objectish, Editor> {
  13. /** Creates the base value for initial state and every scoped-transform rebuild. */
  14. readonly initial: () => State
  15. /** Wraps the mutable draft in a domain-specific editor. */
  16. readonly editor: MakeEditor<State, Editor>
  17. /**
  18. * Completes every committed edit.
  19. *
  20. * For rebuilds, this runs after all active transforms have been replayed and
  21. * before the rebuilt state becomes visible. For direct updates, this runs
  22. * after the current state has already been edited. The optional reason is
  23. * caller-defined metadata for exceptional update origins.
  24. */
  25. readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
  26. }
  27. export interface Interface<State extends Objectish, Editor> {
  28. readonly get: () => State
  29. /**
  30. * Registers a scoped transform slot and returns the slot updater.
  31. *
  32. * Acquiring the slot has no visible effect until the returned updater is
  33. * called. Each updater call replaces that slot's transform, then rebuilds the
  34. * materialized state from `initial()` by replaying all active transforms in
  35. * registration order. Closing the owning Scope removes the slot and rebuilds.
  36. */
  37. readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
  38. /**
  39. * Mutates the current materialized state directly.
  40. *
  41. * This is not replayable contribution state: a later rebuild starts again
  42. * from `initial()` plus active transforms, so direct edits must be reserved
  43. * for current-state adjustments that are intentionally outside the transform
  44. * fold.
  45. */
  46. readonly update: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
  47. }
  48. export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
  49. let state = options.initial()
  50. let transforms: { update: Transform<Editor> }[] = []
  51. const semaphore = Semaphore.makeUnsafe(1)
  52. const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) {
  53. const api = options.editor(next as Draft<State>)
  54. if (options.finalize) yield* options.finalize(api, reason)
  55. state = next
  56. })
  57. const rebuild = Effect.fnUntraced(function* () {
  58. const next = options.initial()
  59. const api = options.editor(next as Draft<State>)
  60. for (const transform of transforms)
  61. yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
  62. yield* commit(next)
  63. })
  64. return {
  65. get: () => state,
  66. transform: Effect.fn("State.transform")(function* () {
  67. const scope = yield* Scope.Scope
  68. return yield* Effect.uninterruptible(
  69. Effect.gen(function* () {
  70. const transform = { update: (_editor: Editor) => {} }
  71. transforms = [...transforms, transform]
  72. yield* Scope.addFinalizer(
  73. scope,
  74. semaphore.withPermit(
  75. Effect.sync(() => {
  76. transforms = transforms.filter((item) => item !== transform)
  77. }).pipe(Effect.andThen(rebuild())),
  78. ),
  79. )
  80. return (update: Transform<Editor>) =>
  81. Effect.uninterruptible(
  82. semaphore.withPermit(
  83. Effect.sync(() => {
  84. transform.update = update
  85. }).pipe(Effect.andThen(rebuild())),
  86. ),
  87. )
  88. }),
  89. )
  90. }),
  91. update: Effect.fn("State.update")(function* (update, reason) {
  92. const api = options.editor(state as Draft<State>)
  93. yield* update(api)
  94. if (options.finalize) yield* options.finalize(api, reason)
  95. }, semaphore.withPermit),
  96. }
  97. }