@playfast/reform 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +103 -56
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +63 -49
  4. package/src/boundary/boundary.ts +144 -113
  5. package/src/calc/asyncCalc.invalidate.test.ts +518 -32
  6. package/src/calc/asyncCalc.test.ts +207 -213
  7. package/src/calc/asyncCalc.ts +131 -154
  8. package/src/calc/asyncData.ts +0 -37
  9. package/src/calc/calc.test.ts +7 -33
  10. package/src/calc/calc.ts +44 -58
  11. package/src/calc/calcFamily.test.ts +80 -34
  12. package/src/calc/calcFamily.ts +54 -65
  13. package/src/calc/compose.test.ts +1 -12
  14. package/src/calc/compose.ts +1 -36
  15. package/src/calc/queryState.ts +0 -23
  16. package/src/channel/channel.ts +126 -111
  17. package/src/compose/composition.test.ts +19 -0
  18. package/src/compose/composition.ts +351 -127
  19. package/src/compose/host.ts +0 -6
  20. package/src/compose/props.ts +13 -12
  21. package/src/compose/provide.ts +189 -62
  22. package/src/compose/slot.ts +158 -49
  23. package/src/compose/structure.test.ts +6 -9
  24. package/src/compose/structure.ts +108 -79
  25. package/src/compose/ui.test.ts +19 -7
  26. package/src/compose/ui.ts +155 -156
  27. package/src/compose/ui.typecheck.ts +40 -18
  28. package/src/definition/definition.ts +22 -41
  29. package/src/event/event.fromSource.test.ts +172 -0
  30. package/src/event/event.test.ts +10 -3
  31. package/src/event/event.ts +102 -27
  32. package/src/event/eventGroup.ts +0 -1
  33. package/src/feature/feature.mount.test.ts +122 -43
  34. package/src/feature/feature.test.ts +46 -21
  35. package/src/feature/feature.ts +1347 -256
  36. package/src/feature/feature.typecheck.ts +273 -68
  37. package/src/graph/closure.ts +403 -0
  38. package/src/index.ts +68 -126
  39. package/src/internal/bucketCache.ts +39 -0
  40. package/src/internal/capture.ts +9 -24
  41. package/src/internal/env.ts +7 -0
  42. package/src/internal/errors.test.ts +0 -6
  43. package/src/internal/errors.ts +37 -60
  44. package/src/internal/inspect.test.ts +0 -6
  45. package/src/internal/inspect.ts +0 -12
  46. package/src/internal/queryDriver.ts +105 -201
  47. package/src/internal/queryDriverStore.ts +156 -0
  48. package/src/internal/queryDriverTypes.ts +59 -0
  49. package/src/internal/queryEvents.ts +0 -12
  50. package/src/internal/queryStore.ts +0 -14
  51. package/src/internal/reuse.test.ts +10 -14
  52. package/src/internal/reuse.ts +5 -30
  53. package/src/internal/scheduler.ts +4 -44
  54. package/src/internal/seeds.ts +0 -14
  55. package/src/internal/sources.ts +26 -49
  56. package/src/internal/stateRegistry.ts +0 -14
  57. package/src/internal/store.test.ts +1 -3
  58. package/src/internal/store.ts +0 -37
  59. package/src/internal/track.ts +3 -20
  60. package/src/internal/variance.ts +5 -0
  61. package/src/internal.ts +222 -0
  62. package/src/namespace/namespace.test.ts +46 -0
  63. package/src/namespace/namespace.ts +106 -0
  64. package/src/procedure/procedure.ts +40 -35
  65. package/src/reducer/reducer.ts +78 -52
  66. package/src/remote/remoteState.test.ts +590 -397
  67. package/src/remote/remoteState.ts +425 -347
  68. package/src/remote/remoteState.typecheck.ts +47 -56
  69. package/src/runtime/appRuntime.activation.test.ts +100 -0
  70. package/src/runtime/appRuntime.test.ts +249 -0
  71. package/src/runtime/appRuntime.ts +415 -97
  72. package/src/runtime/bus.ts +2 -15
  73. package/src/runtime/eventBudget.test.ts +152 -0
  74. package/src/runtime/eventBudget.ts +120 -0
  75. package/src/runtime/hardening.test.ts +73 -13
  76. package/src/runtime/instrumentation.test.ts +55 -52
  77. package/src/runtime/instrumentation.ts +6 -60
  78. package/src/runtime/loop.test.ts +36 -17
  79. package/src/runtime/loop.ts +87 -88
  80. package/src/runtime/queries.ts +0 -17
  81. package/src/scene/featureScene.test.ts +61 -0
  82. package/src/scene/scene.ts +247 -104
  83. package/src/scene/seedScene.test.ts +42 -79
  84. package/src/state/state.nominal.typecheck.ts +68 -0
  85. package/src/state/state.test.ts +17 -0
  86. package/src/state/state.ts +80 -46
  87. package/src/state/stateFamily.test.ts +16 -11
  88. package/src/state/stateFamily.ts +55 -65
  89. package/src/state/stateGroup.ts +53 -64
  90. package/src/state/token.ts +40 -23
  91. package/src/synced/syncedStore.ts +46 -58
  92. package/src/testkit/flight.testkit.ts +75 -0
  93. package/src/ui/node.ts +0 -6
  94. package/src/ui/trigger.ts +0 -5
  95. package/src/wire/tree.test.ts +8 -7
  96. package/src/wire/tree.ts +0 -39
  97. package/src/wire/triggers.test.ts +6 -6
  98. package/src/wire/triggers.ts +14 -32
  99. package/src/internal/ctx.ts +0 -16
@@ -2,19 +2,18 @@ import { Effect, Layer, Predicate } from 'effect'
2
2
  import { type Manifest, definitionClass } from '../definition/definition'
3
3
  import { AsyncReducer } from '../internal/errors'
4
4
  import { type AnyEvent, type EventType } from '../event/event'
5
- import type { Store } from '../internal/store'
6
- import { type AnyState, type StateValue } from '../state/state'
5
+ import { type AnyState, type StateName, type StateStore, type StateValue } from '../state/state'
7
6
  import {
8
7
  type AnyFamily,
9
8
  type FamilyKey,
10
- type FamilyStore,
9
+ type FamilyName,
11
10
  type FamilyValue,
11
+ type StateFamilyStore,
12
12
  Tombstone,
13
13
  } from '../state/stateFamily'
14
14
  import { narrowHandled, type Tagged } from '../runtime/bus'
15
15
  import { type ReducerEntry, Reducers } from '../runtime/loop'
16
16
 
17
- /** The reflectable descriptor a reducer carries, like every other primitive's `*Manifest`. */
18
17
  export interface ReducerManifest extends Manifest {
19
18
  readonly kind: 'Reducer'
20
19
  }
@@ -33,7 +32,6 @@ export interface FamilyReducerConfig<F extends AnyFamily, Events extends Readonl
33
32
  export interface StateReducerClass<S extends AnyState, Events extends ReadonlyArray<AnyEvent>> {
34
33
  new (): {}
35
34
  readonly manifest: ReducerManifest
36
- /** Marker data read by `Reducer.live`. */
37
35
  readonly config: StateReducerConfig<S, Events>
38
36
  readonly handles: ReadonlySet<string>
39
37
  }
@@ -45,12 +43,26 @@ export interface FamilyReducerClass<F extends AnyFamily, Events extends Readonly
45
43
  readonly handles: ReadonlySet<string>
46
44
  }
47
45
 
48
- /**
49
- * Reducers are the only writers of state. The *definition* (`Reducer.make`)
50
- * declares which state and events it touches; the *implementation*
51
- * (`Reducer.live`) is the pure fold. An unhandled event is a no-op because
52
- * it is never in `handles`.
53
- */
46
+ type ErasedFold = (...args: ReadonlyArray<never>) => unknown
47
+
48
+ interface RuntimeStateReducerConfig {
49
+ readonly states: readonly [AnyState]
50
+ readonly events: ReadonlyArray<AnyEvent>
51
+ }
52
+
53
+ interface RuntimeFamilyReducerConfig {
54
+ readonly family: AnyFamily
55
+ readonly keyOf: ErasedFold
56
+ readonly events: ReadonlyArray<AnyEvent>
57
+ }
58
+
59
+ interface RuntimeReducerClass {
60
+ new (): {}
61
+ readonly manifest: ReducerManifest
62
+ readonly config: RuntimeStateReducerConfig | RuntimeFamilyReducerConfig
63
+ readonly handles: ReadonlySet<string>
64
+ }
65
+
54
66
  export function make<S extends AnyState, const Events extends ReadonlyArray<AnyEvent>>(
55
67
  name: string,
56
68
  config: StateReducerConfig<S, Events>,
@@ -61,9 +73,7 @@ export function make<F extends AnyFamily, const Events extends ReadonlyArray<Any
61
73
  ): FamilyReducerClass<F, Events>
62
74
  export function make(
63
75
  name: string,
64
- config:
65
- | StateReducerConfig<AnyState, ReadonlyArray<AnyEvent>>
66
- | FamilyReducerConfig<AnyFamily, ReadonlyArray<AnyEvent>>,
76
+ config: RuntimeStateReducerConfig | RuntimeFamilyReducerConfig,
67
77
  ): unknown {
68
78
  return definitionClass<unknown>({
69
79
  manifest: { kind: 'Reducer' as const, name } satisfies ReducerManifest,
@@ -72,11 +82,9 @@ export function make(
72
82
  })
73
83
  }
74
84
 
75
- /** A thenable check that excludes null/undefined without an assertion. */
76
85
  const isThenable = (candidate: unknown): candidate is PromiseLike<unknown> =>
77
86
  Predicate.isRecord(candidate) && typeof candidate['then'] === 'function'
78
87
 
79
- /** Reject an accidentally-async fold up front: a reducer must be a pure sync write. */
80
88
  const sync = <A>(candidate: A): A => {
81
89
  if (isThenable(candidate)) {
82
90
  // oxlint-disable-next-line reform-rules/no-throw -- sync reducer-write boundary can't yield an Effect; an async fold is a programmer defect
@@ -88,57 +96,75 @@ const sync = <A>(candidate: A): A => {
88
96
  export function live<S extends AnyState, Events extends ReadonlyArray<AnyEvent>>(
89
97
  reducer: StateReducerClass<S, Events>,
90
98
  fold: (value: StateValue<S>, event: EventType<Events[number]>) => StateValue<S>,
91
- ): Layer.Layer<never, never, Reducers | Store<StateValue<S>>>
99
+ ): Layer.Layer<never, never, Reducers | StateStore<StateName<S>, StateValue<S>>>
92
100
  export function live<F extends AnyFamily, Events extends ReadonlyArray<AnyEvent>>(
93
101
  reducer: FamilyReducerClass<F, Events>,
94
102
  fold: (entry: FamilyValue<F>, event: EventType<Events[number]>) => FamilyValue<F> | Tombstone,
95
- ): Layer.Layer<never, never, Reducers | FamilyStore<FamilyKey<F>, FamilyValue<F>>>
103
+ ): Layer.Layer<
104
+ never,
105
+ never,
106
+ Reducers | StateFamilyStore<FamilyName<F>, FamilyKey<F>, FamilyValue<F>>
107
+ >
96
108
  export function live(
97
- reducer:
98
- | StateReducerClass<AnyState, ReadonlyArray<AnyEvent>>
99
- | FamilyReducerClass<AnyFamily, ReadonlyArray<AnyEvent>>,
100
- // oxlint-disable-next-line reform-rules/no-explicit-any-value -- overload-impl signature: `unknown` params fail contravariance against the State/Family folds (TS2394); only `any` subsumes both
101
- fold: (acc: any, event: any) => any,
102
- ): Layer.Layer<never, never, any> {
109
+ reducer: RuntimeReducerClass,
110
+ fold: ErasedFold,
111
+ ): Layer.Layer<never, never, unknown> {
103
112
  const { config, handles } = reducer
104
113
  const name = reducer.manifest.name
105
- // Scoped registration: register on build, unregister on scope close. For an eager
106
- // app the scope is the root runtime's (closes only at dispose), so behavior is
107
- // unchanged; for a lazy feature the scope is the feature's, so unmount removes the
108
- // entry — it stops folding and is reclaimed instead of leaking onto a dead store.
109
114
  return Layer.scopedDiscard(
110
115
  Effect.gen(function* () {
111
116
  const reducers = yield* Reducers
112
- if (reducers.entries.some((existing) => existing.name === name)) {
117
+ if (reducers.byName.has(name)) {
113
118
  yield* Effect.logWarning(`reform: duplicate reducer name '${name}' registered`)
114
119
  }
115
120
  const entry: ReducerEntry = yield* Effect.gen(function* () {
116
121
  if ('family' in config) {
117
- const family = yield* config.family.store
118
- return {
119
- name,
120
- handles,
121
- apply: (event: Tagged) => {
122
- // The loop only invokes `apply` for events in `handles`, so `event`
123
- // is one of this reducer's declared events; `narrowHandled` restores
124
- // that typed view from the erased `Tagged` envelope `keyOf` is fed.
125
- const key = config.keyOf(narrowHandled(event))
126
- const next = fold(family.at(key).get(), event)
127
- // A fold may return the eviction sentinel to drop the key's store
128
- // (bounding an otherwise-unbounded family) instead of a new value.
129
- if (next === Tombstone) {
130
- family.forget(key)
131
- } else {
132
- family.at(key).set(sync(next))
133
- }
134
- },
135
- }
122
+ return yield* config.family.capture<Effect.Effect<ReducerEntry, never, unknown>>(
123
+ (familyDefinition) =>
124
+ Effect.map(familyDefinition.store, (family) => ({
125
+ name,
126
+ handles,
127
+ apply: (event: Tagged) => {
128
+ // The loop calls entries only for handled events. At this
129
+ // existential boundary the family's decoders restore the
130
+ // exact key/value types before either touches its store.
131
+ const key = familyDefinition.decodeKeySync(
132
+ sync(Reflect.apply(config.keyOf, undefined, [narrowHandled(event)])),
133
+ )
134
+ const previous = family.at(key).get()
135
+ const next = sync(Reflect.apply(fold, undefined, [previous, event]))
136
+ if (next === Tombstone) {
137
+ family.forget(key)
138
+ } else if (next !== previous) {
139
+ // Same-reference return means "no change": skip the nominal re-decode,
140
+ // which would mint a fresh value and repaint every tracker of this slice.
141
+ family.at(key).set(familyDefinition.decodeValueSync(next))
142
+ }
143
+ },
144
+ })),
145
+ )
136
146
  }
137
- const store = yield* config.states[0].store
138
- return { name, handles, apply: (event: Tagged) => store.set(sync(fold(store.get(), event))) }
147
+ return yield* config.states[0].capture<Effect.Effect<ReducerEntry, never, unknown>>(
148
+ (state) =>
149
+ Effect.map(state.store, (store) => ({
150
+ name,
151
+ handles,
152
+ apply: (event: Tagged) => {
153
+ const previous = store.get()
154
+ const next = sync(Reflect.apply(fold, undefined, [previous, event]))
155
+ // Same-reference return means "no change": skip the nominal re-decode,
156
+ // which would mint a fresh value and repaint every tracker of this slice.
157
+ if (next === previous) {
158
+ return
159
+ }
160
+ store.set(state.decodeUnknownSync(next))
161
+ },
162
+ })),
163
+ )
139
164
  })
140
- yield* Effect.acquireRelease(Effect.sync(() => reducers.register(entry)), () =>
141
- Effect.sync(() => reducers.unregister(entry)),
165
+ yield* Effect.acquireRelease(
166
+ Effect.sync(() => reducers.register(entry)),
167
+ () => Effect.sync(() => reducers.unregister(entry)),
142
168
  )
143
169
  }),
144
170
  )