@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
@@ -1,13 +1,17 @@
1
- // Type-level proofs enforced by `tsc --noEmit` over `src` (not a vitest file).
2
- // These guard the cast-free seam: if a guarantee regresses, a `@ts-expect-error`
3
- // goes unused or an assignment fails, and `tsc` breaks.
4
-
5
- import { Layer, Schema as S } from 'effect'
6
- import type { Store } from '../internal/store'
1
+ import { Context, Layer, Schema as S } from 'effect'
7
2
  import * as Composition from '../compose/composition'
8
3
  import { provide } from '../compose/provide'
9
- import { type SlotChild, type SlotClass, slot } from '../compose/slot'
4
+ import {
5
+ type ContractOfSlotTarget,
6
+ type FeatureSlotTarget,
7
+ type PropsOfSlotTarget,
8
+ type SlotChild,
9
+ type SlotService,
10
+ slot,
11
+ } from '../compose/slot'
12
+ import { mount } from '../compose/structure'
10
13
  import { type Contract, type UiClass, ui } from '../compose/ui'
14
+ import { scene } from '../scene/scene'
11
15
  import * as State from '../state/state'
12
16
  import * as StateGroup from '../state/stateGroup'
13
17
  import { type EngineServices, type FeatureModule, featureModule, lazyImport } from './feature'
@@ -21,88 +25,289 @@ const TodosStatesBase: StateGroup.StateGroupClass<readonly [typeof FeedState, ty
21
25
  StateGroup.make(FeedState, FilterState)
22
26
  class TodosStates extends TodosStatesBase {}
23
27
 
24
- // --- featureModule: the covariant-RIn bound (cast-free requirement honesty) ---
28
+ interface ClockService {
29
+ readonly now: () => number
30
+ }
31
+ const ClockBase: Context.TagClass<Clock, 'feature.typecheck.Clock', ClockService> = Context.Tag(
32
+ 'feature.typecheck.Clock',
33
+ )<Clock, ClockService>()
34
+ class Clock extends ClockBase {}
35
+
36
+ declare const readsFeed: Layer.Layer<
37
+ { readonly _out: true },
38
+ never,
39
+ State.StateStore<'feed', string> | EngineServices
40
+ >
25
41
 
26
- // A feature layer that reads FeedState's store and the shared engine — nothing else.
27
- declare const readsFeed: Layer.Layer<{ readonly _out: true }, never, Store<string> | EngineServices>
42
+ declare const readsTodos: Layer.Layer<
43
+ { readonly _out: true },
44
+ never,
45
+ State.StateStore<'feed', string> | State.StateStore<'filter', string> | EngineServices
46
+ >
28
47
 
29
- // POSITIVE: declaring the State it reads in `requires` covers the read — compiles.
30
- export const okState: FeatureModule<{ readonly _out: true }, readonly [typeof FeedState]> =
31
- featureModule([FeedState], readsFeed)
48
+ const okState: FeatureModule<{ readonly _out: true }, readonly [typeof FeedState]> = featureModule(
49
+ [FeedState],
50
+ readsFeed,
51
+ )
32
52
 
33
- // POSITIVE: a StateGroup contributes its members' stores via `ProvidedBy` — compiles.
34
- export const okGroup: FeatureModule<{ readonly _out: true }, readonly [typeof TodosStates]> =
35
- featureModule([TodosStates], readsFeed)
53
+ const okGroup: FeatureModule<{ readonly _out: true }, readonly [typeof TodosStates]> =
54
+ featureModule([TodosStates], readsTodos)
36
55
 
37
- // NEGATIVE: empty `requires` provides only the engine, not Store<string>; covariant
38
- // `RIn` makes the layer unassignable to the declared bound.
39
- export const badEmpty: FeatureModule<{ readonly _out: true }, readonly []> =
56
+ const badEmpty =
40
57
  // @ts-expect-error feature layer reads a store it does not declare in `requires`
41
58
  featureModule([], readsFeed)
42
59
 
43
- // --- provide(slot, Feature): RIn surfacing + composition match ---
60
+ declare const readsClock: Layer.Layer<{ readonly _clock: true }, never, Clock | EngineServices>
61
+
62
+ const okService: FeatureModule<{ readonly _clock: true }, readonly [typeof Clock]> = featureModule(
63
+ [Clock],
64
+ readsClock,
65
+ )
66
+
67
+ const badService =
68
+ // @ts-expect-error feature layer reads an ordinary Context service it did not declare
69
+ featureModule([], readsClock)
44
70
 
45
- const AppUiBase: UiClass<{ props: { readonly a: string }; events: {} }> = ui('AppUi')<{
71
+ const AppUiBase: UiClass<{ props: { readonly a: string }; events: {} }, 'AppUi'> = ui('AppUi')<{
46
72
  props: { readonly a: string }
47
73
  events: {}
48
74
  }>()
49
75
  class AppUi extends AppUiBase {}
50
- const AppCompBase: Composition.CompositionClass<{ readonly a: string }, Contract<typeof AppUi>> =
51
- Composition.make('AppComp', {
52
- title: 'App',
53
- props: S.Struct({ a: S.String }),
54
- ui: AppUi,
55
- })
56
- class AppComp extends AppCompBase {}
57
- const AppSlotBase: SlotClass<typeof AppComp> = slot('App')<typeof AppComp>()
58
- class AppSlot extends AppSlotBase {}
59
-
60
- const AppFeatureBase: Feature.FeatureClass<
61
- { readonly a: string },
62
- Contract<typeof AppUi>,
63
- never,
76
+ class AppComp extends Composition.make('AppComp', {
77
+ title: 'App',
78
+ props: S.Struct({ a: S.String }),
79
+ ui: AppUi,
80
+ })<AppComp>() {}
81
+ const AppLive = Composition.live(AppComp, function* () {
82
+ const props = yield* AppComp.props
83
+ return mount({ props: { a: props.a }, slots: {} })
84
+ })
85
+ declare const FailedPlaceholder: Composition.AnyComposition &
86
+ Composition.CompositionPropsReceiver<Feature.FailedProps>
87
+
88
+ class AppFeature extends Feature.make('appFeature', {
89
+ loadingStrategy: 'lazy',
90
+ composition: AppComp,
91
+ load: lazyImport(async () => ({ default: featureModule([], AppLive) })),
92
+ placeholder: { loading: AppComp, failed: FailedPlaceholder },
93
+ }) {}
94
+ type AppFeatureTarget = FeatureSlotTarget<typeof AppFeature>
95
+ class AppFeatureSlot extends slot('AppFeature')<AppFeatureSlot, AppFeatureTarget>() {}
96
+
97
+ class EmptyAppFeature extends Feature.make('emptyAppFeature', {
98
+ composition: AppComp,
99
+ // @ts-expect-error Layer.empty does not provide CompositionId<'AppComp'>
100
+ module: featureModule([], Layer.empty),
101
+ }) {}
102
+
103
+ declare const exactUnionModule: FeatureModule<
104
+ Composition.CompositionId<'AppComp', AppComp> | Clock,
105
+ readonly []
106
+ >
107
+
108
+ class ExactUnionAppFeature extends Feature.make('exactUnionAppFeature', {
109
+ composition: AppComp,
110
+ module: exactUnionModule,
111
+ }) {}
112
+
113
+ declare const unknownRootModule: FeatureModule<unknown, readonly []>
114
+ class UnknownRootAppFeature extends Feature.make('unknownRootAppFeature', {
115
+ composition: AppComp,
116
+ // @ts-expect-error unknown output does not prove the exact root composition service
117
+ module: unknownRootModule,
118
+ }) {}
119
+
120
+ declare const broadRootModule: FeatureModule<
121
+ Composition.CompositionId<string, unknown>,
64
122
  readonly []
65
- > = Feature.make('appFeature', {
123
+ >
124
+ class BroadRootAppFeature extends Feature.make('broadRootAppFeature', {
125
+ composition: AppComp,
126
+ // @ts-expect-error a broad identifier does not prove the exact root identity
127
+ module: broadRootModule,
128
+ }) {}
129
+
130
+ declare const exactUnionLayer: Layer.Layer<
131
+ Composition.CompositionId<'AppComp', AppComp> | Clock,
132
+ never,
133
+ never
134
+ >
135
+
136
+ const exactUnionScene = scene(AppComp, { provide: [exactUnionLayer] })
137
+
138
+ declare const unknownRootLayer: Layer.Layer<unknown, never, never>
139
+ // @ts-expect-error unknown services do not prove the exact scene root
140
+ const unknownRootScene = scene(AppComp, { provide: [unknownRootLayer] })
141
+
142
+ declare const broadRootLayer: Layer.Layer<Composition.CompositionId<string, unknown>, never, never>
143
+ // @ts-expect-error a broad composition identifier does not prove the exact scene root
144
+ const broadRootScene = scene(AppComp, { provide: [broadRootLayer] })
145
+
146
+ declare const privateEngine: Layer.Layer<EngineServices, never, EngineServices>
147
+ // @ts-expect-error a Feature module cannot provide its own Engine services
148
+ const invalidPrivateEngine = featureModule([], privateEngine)
149
+
150
+ const wired = provide(AppFeatureSlot, AppFeature)
151
+
152
+ type Equal<Left, Right> =
153
+ (<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2
154
+ ? true
155
+ : false
156
+ type Assert<Condition extends true> = Condition
157
+ type WiredRequirementsAreExact = Assert<Equal<Layer.Layer.Context<typeof wired>, EngineServices>>
158
+ type WiredOutputIsOnlySummary = Assert<
159
+ Equal<
160
+ Layer.Layer.Success<typeof wired>,
161
+ SlotService<'AppFeature', AppFeatureSlot, typeof AppFeature.summary>
162
+ >
163
+ >
164
+ type FeatureTargetSummaryIsExact = Assert<
165
+ Equal<AppFeatureTarget['summary'], typeof AppFeature.summary>
166
+ >
167
+ type FeatureTargetPropsAreExact = Assert<
168
+ Equal<PropsOfSlotTarget<AppFeatureTarget>, { readonly a: string }>
169
+ >
170
+ type FeatureTargetContractIsExact = Assert<
171
+ Equal<ContractOfSlotTarget<AppFeatureTarget>, Contract<typeof AppUi>>
172
+ >
173
+ type FeatureTargetCannotOpenTree = Assert<
174
+ Equal<AppFeatureTarget extends Feature.FeatureTreeCarrier ? true : false, false>
175
+ >
176
+ const appNative = Feature.tree<typeof AppFeature>(AppFeature.token)
177
+ type AppNative = typeof appNative
178
+ type ExactTreeComposition = Assert<Equal<AppNative['composition'], typeof AppComp>>
179
+ type NoDirectNativeFields = Assert<
180
+ Equal<
181
+ Extract<
182
+ 'composition' | 'binding' | 'load' | 'eagerModule' | 'supportModule' | 'tree',
183
+ keyof typeof AppFeature
184
+ >,
185
+ never
186
+ >
187
+ >
188
+
189
+ class AppFeatureTwin extends Feature.make('appFeature', {
66
190
  loadingStrategy: 'lazy',
67
191
  composition: AppComp,
68
- load: lazyImport(async () => ({ default: featureModule([], Layer.empty) })),
69
- placeholder: { loading: AppComp, failed: AppComp },
70
- })
71
- class AppFeature extends AppFeatureBase {}
192
+ load: lazyImport(async () => ({ default: featureModule([], AppLive) })),
193
+ placeholder: { loading: AppComp, failed: FailedPlaceholder },
194
+ }) {}
195
+ type SameCompositionTokenIsInterchangeable = Assert<
196
+ Equal<typeof AppFeatureTwin.token extends typeof AppFeature.token ? true : false, true>
197
+ >
198
+ type BroadFeatureIdentityIsRejected = Assert<
199
+ Equal<
200
+ Feature.FeatureIdentityToken<unknown> extends Feature.FeatureIdentityToken<AppComp>
201
+ ? true
202
+ : false,
203
+ false
204
+ >
205
+ >
206
+
207
+ class AppCompositionSlot extends slot('AppComposition')<AppCompositionSlot, typeof AppComp>() {}
208
+
209
+ // @ts-expect-error composition slots accept the Composition value, not a Feature
210
+ provide(AppCompositionSlot, AppFeature)
211
+
212
+ class SupportedAppFeature extends Feature.make('supportedAppFeature', {
213
+ loadingStrategy: 'lazy',
214
+ composition: AppComp,
215
+ load: lazyImport(async () => ({ default: featureModule([], AppLive) })),
216
+ eager: okService,
217
+ placeholder: { loading: AppComp, failed: FailedPlaceholder },
218
+ }) {}
219
+ type SupportedAppFeatureTarget = FeatureSlotTarget<typeof SupportedAppFeature>
220
+ class SupportedAppFeatureSlot extends slot('SupportedAppFeature')<
221
+ SupportedAppFeatureSlot,
222
+ SupportedAppFeatureTarget
223
+ >() {}
72
224
 
73
- // POSITIVE: the wired layer's open `RIn` is exactly `EngineServices` (requires=[]),
74
- // so a scene must provide `Core` — surfaced cast-free by the overload.
75
- export const wired: Layer.Layer<SlotChild, never, EngineServices> = provide(AppSlot, AppFeature)
225
+ const wiredWithSupport = provide(SupportedAppFeatureSlot, SupportedAppFeature)
226
+ type SupportedRequirementsAreExact = Assert<
227
+ Equal<Layer.Layer.Context<typeof wiredWithSupport>, EngineServices | Clock>
228
+ >
229
+ type SupportedOutputIsOnlySummary = Assert<
230
+ Equal<
231
+ Layer.Layer.Success<typeof wiredWithSupport>,
232
+ SlotService<'SupportedAppFeature', SupportedAppFeatureSlot, typeof SupportedAppFeature.summary>
233
+ >
234
+ >
76
235
 
77
- // A feature over a DIFFERENT contract must not fill `AppSlot`.
78
- const OtherUiBase: UiClass<{ props: { readonly n: number }; events: {} }> = ui('OtherUi')<{
236
+ const OtherUiBase: UiClass<{ props: { readonly n: number }; events: {} }, 'OtherUi'> = ui(
237
+ 'OtherUi',
238
+ )<{
79
239
  props: { readonly n: number }
80
240
  events: {}
81
241
  }>()
82
242
  class OtherUi extends OtherUiBase {}
83
- const OtherCompBase: Composition.CompositionClass<{ readonly n: number }, Contract<typeof OtherUi>> =
84
- Composition.make('OtherComp', {
85
- title: 'Other',
86
- props: S.Struct({ n: S.Number }),
87
- ui: OtherUi,
88
- })
89
- class OtherComp extends OtherCompBase {}
90
- const OtherFeatureBase: Feature.FeatureClass<
91
- { readonly n: number },
92
- Contract<typeof OtherUi>,
93
- never,
94
- readonly []
95
- > = Feature.make('otherFeature', {
243
+ class OtherComp extends Composition.make('OtherComp', {
244
+ title: 'Other',
245
+ props: S.Struct({ n: S.Number }),
246
+ ui: OtherUi,
247
+ })<OtherComp>() {}
248
+ const OtherLive = Composition.live(OtherComp, function* () {
249
+ const props = yield* OtherComp.props
250
+ return mount({ props, slots: {} })
251
+ })
252
+ class OtherFeature extends Feature.make('otherFeature', {
96
253
  loadingStrategy: 'lazy',
97
254
  composition: OtherComp,
98
- load: lazyImport(async () => ({ default: featureModule([], Layer.empty) })),
99
- placeholder: { loading: OtherComp, failed: OtherComp },
100
- })
101
- class OtherFeature extends OtherFeatureBase {}
255
+ load: lazyImport(async () => ({ default: featureModule([], OtherLive) })),
256
+ placeholder: { loading: OtherComp, failed: FailedPlaceholder },
257
+ }) {}
102
258
 
103
- // NEGATIVE: OtherFeature's composition props don't match AppSlot's child composition.
104
- // @ts-expect-error feature composition does not match the slot's child type
105
- export const mismatched: Layer.Layer<SlotChild, never, EngineServices> = provide(
106
- AppSlot,
107
- OtherFeature,
108
- )
259
+ type OtherFeatureIsRejectedByAppTarget = Assert<
260
+ Equal<typeof OtherFeature extends AppFeatureTarget ? true : false, false>
261
+ >
262
+ type DifferentCompositionTokenIsRejected = Assert<
263
+ Equal<typeof OtherFeature.token extends typeof AppFeature.token ? true : false, false>
264
+ >
265
+ const otherNative = Feature.tree<typeof OtherFeature>(OtherFeature.token)
266
+ type OtherNative = typeof otherNative
267
+ type DifferentCompositionBindingIsRejected = Assert<
268
+ Equal<OtherNative['binding'] extends SlotChild<AppFeatureTarget> ? true : false, false>
269
+ >
270
+
271
+ // @ts-expect-error OtherFeature.token does not belong to AppFeature
272
+ Feature.tree<typeof AppFeature>(OtherFeature.token)
273
+
274
+ // @ts-expect-error AppFeatureSlot accepts only features over AppComp
275
+ provide(AppFeatureSlot, OtherFeature)
276
+
277
+ type FeatureTypeProofs = [
278
+ WiredRequirementsAreExact,
279
+ WiredOutputIsOnlySummary,
280
+ FeatureTargetSummaryIsExact,
281
+ FeatureTargetPropsAreExact,
282
+ FeatureTargetContractIsExact,
283
+ FeatureTargetCannotOpenTree,
284
+ ExactTreeComposition,
285
+ NoDirectNativeFields,
286
+ SameCompositionTokenIsInterchangeable,
287
+ BroadFeatureIdentityIsRejected,
288
+ DifferentCompositionTokenIsRejected,
289
+ DifferentCompositionBindingIsRejected,
290
+ SupportedRequirementsAreExact,
291
+ SupportedOutputIsOnlySummary,
292
+ OtherFeatureIsRejectedByAppTarget,
293
+ ]
294
+ declare const featureTypeProofs: FeatureTypeProofs
295
+
296
+ void featureTypeProofs
297
+ void appNative
298
+ void otherNative
299
+ void AppFeatureTwin
300
+ void okState
301
+ void okGroup
302
+ void badEmpty
303
+ void badService
304
+ void EmptyAppFeature
305
+ void ExactUnionAppFeature
306
+ void UnknownRootAppFeature
307
+ void BroadRootAppFeature
308
+ void exactUnionScene
309
+ void unknownRootScene
310
+ void broadRootScene
311
+ void invalidPrivateEngine
312
+
313
+ export {}