state.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. export * as State from "./state"
  2. import { Context, Effect, Scope, Semaphore } from "effect"
  3. /**
  4. * A replayable transform applied to a draft during reload.
  5. *
  6. * Domain drafts expose readable and writable state while preserving concise
  7. * plugin/config code. Transforms may perform Effects before returning.
  8. */
  9. type TransformCallback<DraftApi> = (draft: DraftApi) => Effect.Effect<void> | void
  10. export type MakeDraft<State, DraftApi> = (state: State) => DraftApi
  11. export interface Registration {
  12. readonly dispose: Effect.Effect<void>
  13. }
  14. export type Transform<DraftApi> = (
  15. transform: TransformCallback<DraftApi>,
  16. ) => Effect.Effect<Registration, never, Scope.Scope>
  17. export type Reload = () => Effect.Effect<void>
  18. export interface Transformable<DraftApi> {
  19. readonly transform: Transform<DraftApi>
  20. readonly reload: Reload
  21. }
  22. const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@kirincode/State/CurrentBatch", {
  23. defaultValue: () => undefined,
  24. })
  25. export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
  26. return Effect.gen(function* () {
  27. const current = yield* CurrentBatch
  28. if (current) return yield* effect
  29. const reloads = new Set<Reload>()
  30. const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
  31. yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
  32. return result
  33. })
  34. }
  35. export interface Options<State, DraftApi> {
  36. /** Creates the base value for initial state and every scoped-transform reload. */
  37. readonly initial: () => State
  38. /** Wraps mutable state in a domain-specific draft API. */
  39. readonly draft: MakeDraft<State, DraftApi>
  40. /** Runs after all active transforms and before the rebuilt state becomes visible. */
  41. readonly finalize?: (draft: DraftApi) => Effect.Effect<void>
  42. }
  43. export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
  44. readonly get: () => State
  45. /**
  46. * Registers and applies a scoped transform. Closing the owning Scope removes
  47. * the transform and reloads the materialized state.
  48. */
  49. }
  50. export function create<State, DraftApi>(options: Options<State, DraftApi>): Interface<State, DraftApi> {
  51. let state = options.initial()
  52. let transforms: { run: TransformCallback<DraftApi> }[] = []
  53. const semaphore = Semaphore.makeUnsafe(1)
  54. const commit = Effect.fn("State.commit")(function* (next: State) {
  55. const api = options.draft(next)
  56. if (options.finalize) yield* options.finalize(api)
  57. state = next
  58. })
  59. const apply = (transform: TransformCallback<DraftApi>, draft: DraftApi) =>
  60. Effect.suspend(() => {
  61. const result = transform(draft)
  62. return Effect.isEffect(result) ? Effect.asVoid(result).pipe(Effect.orDie) : Effect.void
  63. })
  64. const materialize = Effect.fnUntraced(function* () {
  65. const next = options.initial()
  66. const api = options.draft(next)
  67. for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.reload.update"))
  68. yield* commit(next)
  69. })
  70. const reload = () => semaphore.withPermit(materialize())
  71. const result: Interface<State, DraftApi> = {
  72. get: () => state,
  73. transform: Effect.fn("State.transform")(function* (update) {
  74. const scope = yield* Scope.Scope
  75. return yield* Effect.uninterruptible(
  76. Effect.gen(function* () {
  77. const transform = { run: update }
  78. let active = true
  79. const dispose = Effect.uninterruptible(
  80. semaphore.withPermit(
  81. Effect.suspend(() => {
  82. if (!active) return Effect.void
  83. active = false
  84. transforms = transforms.filter((item) => item !== transform)
  85. return Effect.gen(function* () {
  86. const batch = yield* CurrentBatch
  87. if (batch) {
  88. batch.add(reload)
  89. return
  90. }
  91. yield* materialize()
  92. })
  93. }),
  94. ),
  95. )
  96. yield* semaphore.withPermit(
  97. Effect.sync(() => {
  98. transforms = [...transforms, transform]
  99. }),
  100. )
  101. yield* Scope.addFinalizer(scope, dispose)
  102. const batch = yield* CurrentBatch
  103. if (batch) batch.add(reload)
  104. else yield* reload()
  105. return { dispose }
  106. }),
  107. )
  108. }),
  109. reload,
  110. }
  111. return result
  112. }