@playfast/reform 1.1.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 (85) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +62 -16
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +516 -18
  6. package/src/calc/asyncCalc.test.ts +207 -175
  7. package/src/calc/asyncCalc.ts +168 -39
  8. package/src/calc/calc.test.ts +3 -5
  9. package/src/calc/calc.ts +44 -18
  10. package/src/calc/calcFamily.test.ts +80 -22
  11. package/src/calc/calcFamily.ts +56 -25
  12. package/src/calc/compose.ts +1 -14
  13. package/src/channel/channel.ts +128 -11
  14. package/src/compose/composition.test.ts +19 -0
  15. package/src/compose/composition.ts +346 -62
  16. package/src/compose/props.ts +13 -6
  17. package/src/compose/provide.ts +187 -46
  18. package/src/compose/slot.ts +156 -19
  19. package/src/compose/structure.test.ts +6 -3
  20. package/src/compose/structure.ts +106 -13
  21. package/src/compose/ui.test.ts +19 -3
  22. package/src/compose/ui.ts +147 -54
  23. package/src/compose/ui.typecheck.ts +40 -4
  24. package/src/definition/definition.ts +22 -9
  25. package/src/event/event.fromSource.test.ts +172 -0
  26. package/src/event/event.test.ts +10 -0
  27. package/src/event/event.ts +102 -18
  28. package/src/feature/feature.mount.test.ts +123 -56
  29. package/src/feature/feature.test.ts +67 -63
  30. package/src/feature/feature.ts +1317 -197
  31. package/src/feature/feature.typecheck.ts +253 -61
  32. package/src/graph/closure.ts +403 -0
  33. package/src/index.ts +171 -245
  34. package/src/internal/bucketCache.ts +39 -0
  35. package/src/internal/capture.ts +9 -4
  36. package/src/internal/env.ts +1 -3
  37. package/src/internal/errors.ts +21 -10
  38. package/src/internal/queryDriver.ts +120 -15
  39. package/src/internal/queryDriverStore.ts +8 -5
  40. package/src/internal/queryDriverTypes.ts +3 -1
  41. package/src/internal/reuse.test.ts +10 -3
  42. package/src/internal/reuse.ts +5 -2
  43. package/src/internal/scheduler.ts +4 -1
  44. package/src/internal/sources.ts +25 -11
  45. package/src/internal/track.ts +3 -2
  46. package/src/internal.ts +222 -0
  47. package/src/namespace/namespace.ts +46 -25
  48. package/src/procedure/procedure.ts +38 -11
  49. package/src/reducer/reducer.ts +78 -34
  50. package/src/remote/remoteState.test.ts +515 -339
  51. package/src/remote/remoteState.ts +572 -107
  52. package/src/remote/remoteState.typecheck.ts +47 -23
  53. package/src/runtime/appRuntime.activation.test.ts +100 -0
  54. package/src/runtime/appRuntime.test.ts +157 -105
  55. package/src/runtime/appRuntime.ts +394 -33
  56. package/src/runtime/eventBudget.test.ts +1 -4
  57. package/src/runtime/eventBudget.ts +9 -8
  58. package/src/runtime/hardening.test.ts +4 -9
  59. package/src/runtime/instrumentation.test.ts +174 -192
  60. package/src/runtime/instrumentation.ts +6 -11
  61. package/src/runtime/loop.test.ts +36 -0
  62. package/src/runtime/loop.ts +69 -40
  63. package/src/scene/featureScene.test.ts +4 -2
  64. package/src/scene/scene.ts +234 -64
  65. package/src/scene/seedScene.test.ts +69 -86
  66. package/src/state/state.nominal.typecheck.ts +68 -0
  67. package/src/state/state.test.ts +17 -0
  68. package/src/state/state.ts +79 -26
  69. package/src/state/stateFamily.test.ts +14 -0
  70. package/src/state/stateFamily.ts +55 -22
  71. package/src/state/stateGroup.ts +53 -17
  72. package/src/state/token.ts +40 -10
  73. package/src/synced/syncedStore.ts +46 -23
  74. package/src/testkit/flight.testkit.ts +75 -0
  75. package/src/wire/tree.test.ts +8 -4
  76. package/src/wire/triggers.test.ts +6 -2
  77. package/src/wire/triggers.ts +14 -10
  78. package/src/calc/asyncCalcDefinitions.ts +0 -48
  79. package/src/channel/procedureRegistry.ts +0 -90
  80. package/src/feature/featureBinding.ts +0 -48
  81. package/src/internal/ctx.ts +0 -9
  82. package/src/remote/remoteStateDefinition.ts +0 -135
  83. package/src/remote/remoteStateLayers.ts +0 -118
  84. package/src/remote/remoteStateLiveTypes.ts +0 -59
  85. package/src/remote/remoteStateSend.ts +0 -64
@@ -1,12 +1,11 @@
1
- import { Context, Data, Effect, Layer, MutableRef, Predicate } from 'effect'
1
+ import { Context, Data, Effect, Equal, Layer, MutableRef, Option, Predicate } from 'effect'
2
2
  import type { Effect as EffectType } from 'effect/Effect'
3
3
  import { type Manifest, yieldableClass } from '../definition/definition'
4
4
  import { resolveScheduler } from '../internal/scheduler'
5
- import { type InputStores, sameKey, wireSources } from '../internal/sources'
5
+ import { type InputStores, wireSources } from '../internal/sources'
6
6
  import { makeDerivedStore, type Store } from '../internal/store'
7
7
  import { readTracked } from '../internal/track'
8
- import { type Source } from '../state/token'
9
- import type { AnyValue } from '../internal/variance'
8
+ import { type AnySource, type SourceCapture, type SourceValue } from '../state/token'
10
9
 
11
10
  export interface BoundaryPending {
12
11
  readonly _tag: 'Pending'
@@ -23,29 +22,73 @@ export interface BoundaryReady {
23
22
 
24
23
  export type BoundaryState = BoundaryPending | BoundaryErrored | BoundaryReady
25
24
 
25
+ export const BoundaryStoreTypeId: unique symbol = Symbol.for('reform/BoundaryStore')
26
+ export type BoundaryStoreTypeId = typeof BoundaryStoreTypeId
27
+
28
+ export interface BoundaryStore<N extends string> {
29
+ readonly [BoundaryStoreTypeId]: N
30
+ }
31
+
26
32
  const pendingArm: BoundaryState = { _tag: 'Pending' }
27
33
  const readyArm: BoundaryState = { _tag: 'Ready' }
28
34
  const erroredArm = (errors: ReadonlyArray<unknown>): BoundaryState =>
29
35
  Data.struct({ _tag: 'Errored' as const, errors: Data.array(errors) })
30
36
 
31
- export type AnyLifecycleSource = Source<string, AnyValue>
37
+ export type AnyLifecycleSource = AnySource
38
+
39
+ declare const BoundaryLifecycleMismatchTypeId: unique symbol
40
+
41
+ interface BoundaryLifecycleMismatch<Value> {
42
+ readonly [BoundaryLifecycleMismatchTypeId]: Value
43
+ }
44
+
45
+ type CheckedLifecycleValue<Value> = Value extends { readonly _tag: 'Idle' | 'Loading' }
46
+ ? Value
47
+ : Value extends {
48
+ readonly _tag: 'Success'
49
+ readonly value: infer _Success
50
+ readonly refetching: boolean
51
+ }
52
+ ? Value
53
+ : Value extends {
54
+ readonly _tag: 'Error'
55
+ readonly error: infer _Failure
56
+ readonly refetching: boolean
57
+ }
58
+ ? Value
59
+ : BoundaryLifecycleMismatch<Value>
60
+
61
+ type CheckedLifecycleSource<Source extends AnySource> = [SourceValue<Source>] extends [never]
62
+ ? BoundaryLifecycleMismatch<Source>
63
+ : SourceValue<Source> extends CheckedLifecycleValue<SourceValue<Source>>
64
+ ? Source
65
+ : BoundaryLifecycleMismatch<SourceValue<Source>>
66
+
67
+ type CheckedLifecycleSources<Over extends ReadonlyArray<AnyLifecycleSource>> = {
68
+ readonly [Index in keyof Over]: Over[Index] extends AnySource
69
+ ? CheckedLifecycleSource<Over[Index]>
70
+ : BoundaryLifecycleMismatch<Over[Index]>
71
+ }
32
72
 
33
73
  export interface BoundaryManifest<N extends string> extends Manifest {
34
74
  readonly kind: 'Boundary'
35
75
  readonly name: N
36
76
  }
37
77
 
38
- export interface BoundaryClass<out N extends string, out Over extends ReadonlyArray<AnyLifecycleSource>>
39
- extends EffectType<BoundaryState, never, Store<BoundaryState>> {
78
+ export interface BoundaryClass<
79
+ N extends string,
80
+ out Over extends ReadonlyArray<AnyLifecycleSource>,
81
+ > extends EffectType<BoundaryState, never, BoundaryStore<N>> {
40
82
  new (): {}
41
83
  readonly manifest: BoundaryManifest<N>
42
- readonly store: Context.Tag<Store<BoundaryState>, Store<BoundaryState>>
84
+ readonly store: Context.Tag<BoundaryStore<N>, Store<BoundaryState>>
43
85
  readonly name: N
44
86
  readonly over: Over
87
+ readonly capture: <Result>(visit: SourceCapture<Result>) => Result
45
88
  }
46
89
 
47
90
  export interface BoundaryConfig<Over extends ReadonlyArray<AnyLifecycleSource>> {
48
- readonly over: Over
91
+ readonly over: Over & CheckedLifecycleSources<Over>
49
92
  }
50
93
 
51
94
  export interface BoundaryOptions {
@@ -57,54 +100,120 @@ export const make = <const N extends string, const Over extends ReadonlyArray<An
57
100
  name: N,
58
101
  config: BoundaryConfig<Over>,
59
102
  ): BoundaryClass<N, Over> => {
60
- const store = Context.GenericTag<Store<BoundaryState>, Store<BoundaryState>>(
103
+ const store = Context.GenericTag<BoundaryStore<N>, Store<BoundaryState>>(
61
104
  `reform/boundary/${name}`,
62
105
  )
63
106
  const manifest: BoundaryManifest<N> = { kind: 'Boundary', name }
64
107
  const read = Effect.flatMap(store, readTracked)
65
- return yieldableClass(read, { manifest, store, name, over: config.over })
108
+ const boundary: BoundaryClass<N, Over> = yieldableClass(read, {
109
+ manifest,
110
+ store,
111
+ name,
112
+ over: config.over,
113
+ capture: <Result>(visit: SourceCapture<Result>): Result => visit(boundary),
114
+ })
115
+ return boundary
66
116
  }
67
117
 
68
118
  const tagOf = (arm: unknown): string =>
69
119
  Predicate.hasProperty(arm, '_tag') && Predicate.isString(arm._tag) ? arm._tag : ''
70
- const errorOf = (arm: unknown): ReadonlyArray<unknown> =>
71
- Predicate.hasProperty(arm, 'error') && tagOf(arm) === 'Error' ? [arm.error] : []
120
+ const errorOf = (arm: unknown): Option.Option<unknown> =>
121
+ Predicate.hasProperty(arm, 'error') && tagOf(arm) === 'Error'
122
+ ? Option.some(arm.error)
123
+ : Option.none()
124
+
125
+ interface SlotUpdate {
126
+ readonly slot: number
127
+ readonly arm: unknown
128
+ }
72
129
 
73
130
  export const live = <N extends string, Over extends ReadonlyArray<AnyLifecycleSource>>(
74
131
  boundary: BoundaryClass<N, Over>,
75
132
  options: BoundaryOptions = {},
76
- ): Layer.Layer<Store<BoundaryState>, never, InputStores<Over>> =>
133
+ ): Layer.Layer<BoundaryStore<N>, never, InputStores<Over>> =>
77
134
  Layer.scoped(
78
135
  boundary.store,
79
136
  Effect.gen(function* () {
80
137
  const scheduler = yield* resolveScheduler
81
- const sources = yield* wireSources(boundary.over)
138
+ const sourceWires = yield* Effect.forEach(boundary.over, (source) => wireSources([source]))
139
+ const readSource = (source: (typeof sourceWires)[number]): unknown =>
140
+ source.keyOf(source.snapshot())[0]
141
+ const arms = sourceWires.map((source) => MutableRef.make(readSource(source)))
142
+ const loadingCount = MutableRef.make(
143
+ arms.reduce((count, arm) => count + (tagOf(MutableRef.get(arm)) === 'Loading' ? 1 : 0), 0),
144
+ )
145
+ const errorCount = MutableRef.make(
146
+ arms.reduce(
147
+ (count, arm) => count + (Option.isSome(errorOf(MutableRef.get(arm))) ? 1 : 0),
148
+ 0,
149
+ ),
150
+ )
151
+ const collectErrors = (): ReadonlyArray<unknown> =>
152
+ arms.flatMap((arm) => Option.toArray(errorOf(MutableRef.get(arm))))
153
+ const errorArm = MutableRef.make(erroredArm(collectErrors()))
154
+ const updateSlot = (update: SlotUpdate): void => {
155
+ const armRef = arms[update.slot]
156
+ if (armRef === undefined) {
157
+ return
158
+ }
159
+ const previous = MutableRef.get(armRef)
160
+ if (Equal.equals(previous, update.arm)) {
161
+ return
162
+ }
163
+ MutableRef.set(armRef, update.arm)
164
+ MutableRef.update(
165
+ loadingCount,
166
+ (count) =>
167
+ count +
168
+ (tagOf(update.arm) === 'Loading' ? 1 : 0) -
169
+ (tagOf(previous) === 'Loading' ? 1 : 0),
170
+ )
171
+ const previousError = errorOf(previous)
172
+ const nextError = errorOf(update.arm)
173
+ if (!Equal.equals(previousError, nextError)) {
174
+ MutableRef.update(
175
+ errorCount,
176
+ (count) =>
177
+ count + (Option.isSome(nextError) ? 1 : 0) - (Option.isSome(previousError) ? 1 : 0),
178
+ )
179
+ MutableRef.set(errorArm, erroredArm(collectErrors()))
180
+ }
181
+ }
182
+ const synchronize = (): void =>
183
+ sourceWires.forEach((source, slot) => updateSlot({ slot, arm: readSource(source) }))
82
184
 
83
185
  const latched = MutableRef.make(false)
84
- const memo = MutableRef.make<
85
- { readonly key: ReadonlyArray<unknown>; readonly value: BoundaryState } | undefined
86
- >(undefined)
186
+ const incremental = MutableRef.make(false)
87
187
  const recompute = (): BoundaryState => {
88
188
  if (options.once === true && MutableRef.get(latched)) {
89
189
  return readyArm
90
190
  }
91
- const arms = sources.keyOf(sources.snapshot())
92
- const prev = MutableRef.get(memo)
93
- if (prev !== undefined && sameKey(arms, prev.key)) {
94
- return prev.value
191
+ if (!MutableRef.get(incremental)) {
192
+ synchronize()
95
193
  }
96
- const errors = arms.flatMap(errorOf)
97
- const mergedArm =
98
- errors.length > 0
99
- ? erroredArm(errors)
100
- : arms.some((arm) => tagOf(arm) === 'Loading')
101
- ? pendingArm
102
- : readyArm
103
- MutableRef.set(memo, { key: arms, value: mergedArm })
104
- return mergedArm
194
+ return MutableRef.get(errorCount) > 0
195
+ ? MutableRef.get(errorArm)
196
+ : MutableRef.get(loadingCount) > 0
197
+ ? pendingArm
198
+ : readyArm
105
199
  }
106
200
 
107
- const derived = makeDerivedStore(recompute, sources.subscribe, scheduler)
201
+ const subscribe = (onChange: () => void): (() => void) => {
202
+ const publish = (): void => {
203
+ MutableRef.set(incremental, true)
204
+ onChange()
205
+ MutableRef.set(incremental, false)
206
+ }
207
+ const settle = (): void => scheduler.schedule([publish])
208
+ const unsubscribes = sourceWires.map((source, sourceIndex) =>
209
+ source.subscribe(() => {
210
+ updateSlot({ slot: sourceIndex, arm: readSource(source) })
211
+ scheduler.schedule([settle])
212
+ }),
213
+ )
214
+ return () => unsubscribes.forEach((unsubscribe) => unsubscribe())
215
+ }
216
+ const derived = makeDerivedStore(recompute, subscribe, scheduler)
108
217
  yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
109
218
 
110
219
  if (options.once === true) {