@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
@@ -4,20 +4,20 @@ import {
4
4
  Effect,
5
5
  MutableRef,
6
6
  Option,
7
+ type ParseResult,
7
8
  Queue,
9
+ Runtime,
8
10
  type Scope,
9
11
  Stream,
10
12
  } from 'effect'
13
+ import { type AnyAsyncData } from '../calc/asyncData'
11
14
  import { empty, type QueryState } from '../calc/queryState'
12
15
  import { Queries, type QueryHandle } from '../runtime/queries'
13
16
  import { reuse } from './reuse'
14
17
  import { resolveInstrumentation } from '../runtime/instrumentation'
15
18
  import { resolveQueryStore } from './queryStore'
16
- import type { QueryDriver, QueryDriverOptions } from './queryDriverTypes'
17
19
  import {
18
- hydrateQuery,
19
20
  incrementGeneration,
20
- makePersistWriter,
21
21
  makeQueryProjection,
22
22
  subscribeQuerySources,
23
23
  settleQueryError,
@@ -27,23 +27,58 @@ import { resolveScheduler } from './scheduler'
27
27
  import {
28
28
  type InputsObject,
29
29
  type InputStores,
30
+ type InvalidateBy,
30
31
  sameKey,
31
32
  wireSources,
32
33
  } from './sources'
33
- import { makeStore } from './store'
34
+ import { makeStore, type Store } from './store'
34
35
  import { type AnySource } from '../state/token'
35
36
 
36
37
  export {
37
38
  bumpRevision,
38
39
  gatedFlag,
39
40
  type GatedOf,
40
- type QueryDriver,
41
- type QueryDriverOptions,
42
41
  Revision,
43
42
  RevisionSchema,
44
43
  revisionZero,
45
44
  } from './queryDriverTypes'
46
45
 
46
+ // Keep schema invariance behind a covariant persistence boundary.
47
+ export interface QueryCodec<A> {
48
+ readonly encode: (value: A) => Effect.Effect<unknown, ParseResult.ParseError, never>
49
+ readonly decode: (input: unknown) => Effect.Effect<A, ParseResult.ParseError, never>
50
+ }
51
+
52
+ export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
53
+ readonly name: string
54
+ readonly label: string
55
+ readonly gated: boolean
56
+ readonly inputs: Inputs
57
+ readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
58
+ readonly invalidateBy?: InvalidateBy<Inputs> | undefined
59
+ readonly disabled?: ((inputs: InputsObject<Inputs>) => boolean) | undefined
60
+ readonly coalesce?: 'switch' | 'trailing' | undefined
61
+ readonly reuse?: boolean | undefined
62
+ readonly extraKey?:
63
+ | {
64
+ readonly read: () => unknown
65
+ readonly subscribe: (listener: () => void) => () => void
66
+ }
67
+ | undefined
68
+ readonly persist?:
69
+ | {
70
+ readonly key: string | ((inputs: InputsObject<Inputs>) => string)
71
+ readonly codec: QueryCodec<A>
72
+ }
73
+ | undefined
74
+ readonly onSettled?: ((generation: number) => void) | undefined
75
+ }
76
+
77
+ export interface QueryDriver<A, E> {
78
+ readonly store: Store<AnyAsyncData<A, E>>
79
+ readonly requested: () => number
80
+ }
81
+
47
82
  // oxlint-disable-next-line reform-rules/prefer-effect-fn -- generic export: Effect.fn's inferred type isn't portable under isolatedDeclarations
48
83
  export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R>(
49
84
  options: QueryDriverOptions<Inputs, A, E, R>,
@@ -58,6 +93,10 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
58
93
 
59
94
  const keyOf = (args: InputsObject<Inputs>): ReadonlyArray<unknown> =>
60
95
  extraKey === undefined ? sources.keyOf(args) : [...sources.keyOf(args), extraKey.read()]
96
+ const persistKey = (
97
+ key: string | ((inputs: InputsObject<Inputs>) => string),
98
+ args: InputsObject<Inputs>,
99
+ ): string => (typeof key === 'function' ? key(args) : key)
61
100
  const disabledNow = (args: InputsObject<Inputs> = sources.snapshot()): boolean =>
62
101
  options.gated && options.disabled !== undefined ? options.disabled(args) : false
63
102
 
@@ -67,8 +106,17 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
67
106
  const readers = MutableRef.make(0)
68
107
 
69
108
  const lastKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
109
+ // Monotonic source epoch for the one startup hydration attempt. Comparing
110
+ // only the current key is insufficient: A → B → A would otherwise let the
111
+ // original A cache land over the later A request. Same-key refetches do not
112
+ // move this epoch, so cache may still paint during an in-flight revalidation.
113
+ const sourceEpoch = MutableRef.make(0)
70
114
  const generation = MutableRef.make(0)
71
115
  const nextGeneration = (): number => incrementGeneration(generation)
116
+ // A stale cause needs a strictly newer requested generation. Hydration
117
+ // captures this before the startup kick, while invalidation captures it when
118
+ // the value is marked stale.
119
+ const staleAtGeneration = MutableRef.make(0)
72
120
  const markFetching = () => {
73
121
  const state = stateStore.get()
74
122
  if (!state.isFetching) {
@@ -89,7 +137,24 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
89
137
  })
90
138
  return shared
91
139
  }
92
- const persistWrite = makePersistWriter<Inputs, A>({ persist, queryStore })
140
+ const settleError = (error: E, failedGeneration: number) => {
141
+ // Preserve the stale cause after failure without duplicating a newer request.
142
+ MutableRef.update(staleAtGeneration, (current) => Math.max(current, failedGeneration))
143
+ settleQueryError(stateStore, error)
144
+ }
145
+
146
+ const persistWriteTo = (
147
+ target: NonNullable<typeof persist>,
148
+ produced: A,
149
+ args: InputsObject<Inputs>,
150
+ ): Effect.Effect<void> =>
151
+ target.codec.encode(produced).pipe(
152
+ Effect.flatMap((encoded) => queryStore.set(persistKey(target.key, args), encoded)),
153
+ // Persistence is best-effort and must not block the visible write.
154
+ Effect.ignore,
155
+ )
156
+ const persistWrite = (produced: A, args: InputsObject<Inputs>): Effect.Effect<void> =>
157
+ persist === undefined ? Effect.void : persistWriteTo(persist, produced, args)
93
158
 
94
159
  const derived = makeQueryProjection({ gated: options.gated, scheduler, stateStore })
95
160
  yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
@@ -123,7 +188,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
123
188
  onFailure: (cause): Option.Option<A> => {
124
189
  const failure = Cause.failureOption(cause)
125
190
  if (Option.isSome(failure) && !disabledNow()) {
126
- settleQueryError(stateStore, failure.value)
191
+ settleError(failure.value, request.generation)
127
192
  }
128
193
  return Option.none()
129
194
  },
@@ -161,6 +226,9 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
161
226
  const enqueue = (force: boolean) => {
162
227
  const args = sources.snapshot()
163
228
  if (disabledNow(args)) {
229
+ if (MutableRef.get(lastKey) !== undefined) {
230
+ MutableRef.update(sourceEpoch, (current) => current + 1)
231
+ }
164
232
  MutableRef.set(lastKey, undefined)
165
233
  stateStore.set(idle)
166
234
  return
@@ -170,6 +238,9 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
170
238
  if (!force && previous !== undefined && sameKey(key, previous)) {
171
239
  return
172
240
  }
241
+ if (previous !== undefined && !sameKey(key, previous)) {
242
+ MutableRef.update(sourceEpoch, (current) => current + 1)
243
+ }
173
244
  MutableRef.set(lastKey, key)
174
245
  markFetching()
175
246
  Queue.unsafeOffer(requests, { args, generation: nextGeneration() })
@@ -178,7 +249,8 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
178
249
  const refetch = () => enqueue(true)
179
250
 
180
251
  const maybeAutoRefetch = () => {
181
- if (stateStore.get().isStale && MutableRef.get(readers) > 0) {
252
+ const hasCoveringRequest = MutableRef.get(generation) > MutableRef.get(staleAtGeneration)
253
+ if (stateStore.get().isStale && MutableRef.get(readers) > 0 && !hasCoveringRequest) {
182
254
  refetch()
183
255
  }
184
256
  }
@@ -187,6 +259,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
187
259
  if (state.isStale) {
188
260
  return
189
261
  }
262
+ MutableRef.set(staleAtGeneration, MutableRef.get(generation))
190
263
  stateStore.set({ ...state, isStale: true })
191
264
  maybeAutoRefetch()
192
265
  }
@@ -194,14 +267,46 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
194
267
  const unsubscribe = subscribeQuerySources(sources, extraKey, trigger)
195
268
  yield* Effect.addFinalizer(() => Effect.sync(unsubscribe))
196
269
 
270
+ // Fork hydration so an asynchronous store never blocks layer acquisition.
271
+ // Epoch, key, and settled-state guards prevent a late cache from replacing newer truth.
197
272
  if (persist !== undefined && !disabledNow()) {
198
273
  const hydrateArgs = sources.snapshot()
199
- yield* hydrateQuery({
200
- cacheKey: typeof persist.key === 'function' ? persist.key(hydrateArgs) : persist.key,
201
- queryStore,
202
- schema: persist.schema,
203
- stateStore,
204
- })
274
+ const hydrateSourceKey = keyOf(hydrateArgs)
275
+ const hydrateSourceEpoch = MutableRef.get(sourceEpoch)
276
+ const hydrateStaleAtGeneration = MutableRef.get(generation)
277
+ const hydrate = queryStore.get(persistKey(persist.key, hydrateArgs)).pipe(
278
+ Effect.flatMap((cached) =>
279
+ Option.match(cached, {
280
+ onNone: () => Effect.void,
281
+ onSome: (raw) =>
282
+ persist.codec.decode(raw).pipe(
283
+ Effect.tap((decoded) =>
284
+ Effect.sync(() => {
285
+ const state = stateStore.get()
286
+ if (
287
+ disabledNow() ||
288
+ hydrateSourceEpoch !== MutableRef.get(sourceEpoch) ||
289
+ !sameKey(hydrateSourceKey, keyOf(sources.snapshot())) ||
290
+ Option.isSome(state.data) ||
291
+ Option.isSome(state.error)
292
+ ) {
293
+ return
294
+ }
295
+ MutableRef.update(staleAtGeneration, (current) =>
296
+ Math.max(current, hydrateStaleAtGeneration),
297
+ )
298
+ stateStore.set({ ...state, data: Option.some(decoded), isStale: true })
299
+ }),
300
+ ),
301
+ ),
302
+ }),
303
+ ),
304
+ // Corrupt/incompatible cache: ignore and fetch fresh.
305
+ Effect.ignore,
306
+ )
307
+ const runtime = yield* Effect.runtime<never>()
308
+ const scope = yield* Effect.scope
309
+ Runtime.runFork(runtime)(hydrate, { immediate: true, scope })
205
310
  }
206
311
 
207
312
  const initial = sources.snapshot()
@@ -15,9 +15,7 @@ interface HydrateQueryOptions<A, E> {
15
15
  readonly stateStore: Store<QueryState<A, E>>
16
16
  }
17
17
 
18
- export const hydrateQuery = <A, E>(
19
- options: HydrateQueryOptions<A, E>,
20
- ): Effect.Effect<void> =>
18
+ export const hydrateQuery = <A, E>(options: HydrateQueryOptions<A, E>): Effect.Effect<void> =>
21
19
  Effect.flatMap(options.queryStore.get(options.cacheKey), (cached) =>
22
20
  Option.match(cached, {
23
21
  onNone: () => Effect.void,
@@ -52,7 +50,10 @@ export const persistQueryValue = <A>(options: PersistQueryValueOptions<A>): Effe
52
50
 
53
51
  interface PersistWriterOptions<Inputs extends ReadonlyArray<AnySource>, A> {
54
52
  readonly persist:
55
- | { readonly key: string | ((inputs: InputsObject<Inputs>) => string); readonly schema: AnySchema<A> }
53
+ | {
54
+ readonly key: string | ((inputs: InputsObject<Inputs>) => string)
55
+ readonly schema: AnySchema<A>
56
+ }
56
57
  | undefined
57
58
  readonly queryStore: QueryStoreApi
58
59
  }
@@ -136,7 +137,9 @@ interface ReaderStoreOptions<A, E> {
136
137
  readonly onFirstReader: () => void
137
138
  }
138
139
 
139
- export const withReaderTracking = <A, E>(options: ReaderStoreOptions<A, E>): Store<AnyAsyncData<A, E>> => ({
140
+ export const withReaderTracking = <A, E>(
141
+ options: ReaderStoreOptions<A, E>,
142
+ ): Store<AnyAsyncData<A, E>> => ({
140
143
  ...options.store,
141
144
  subscribe: (listener) => {
142
145
  const before = MutableRef.get(options.readers)
@@ -7,7 +7,9 @@ import type { AnySchema } from './variance'
7
7
 
8
8
  export type GatedOf<AlwaysOn extends boolean> = AlwaysOn extends true ? false : true
9
9
 
10
- export const gatedFlag = <AlwaysOn extends boolean>(alwaysOn: AlwaysOn | undefined): GatedOf<AlwaysOn> =>
10
+ export const gatedFlag = <AlwaysOn extends boolean>(
11
+ alwaysOn: AlwaysOn | undefined,
12
+ ): GatedOf<AlwaysOn> =>
11
13
  // oxlint-disable-next-line reform-rules/no-type-assertion -- restate the runtime boolean as its conditional `GatedOf` literal; no guard maps a boolean to a conditional type
12
14
  (alwaysOn !== true) as GatedOf<AlwaysOn>
13
15
 
@@ -13,7 +13,10 @@ describe('reuse', () => {
13
13
  const e0 = { id: 1, name: 'a' }
14
14
  const e1 = { id: 2, name: 'b' }
15
15
  const prev = [e0, e1]
16
- const next = [{ id: 1, name: 'a' }, { id: 2, name: 'B' }]
16
+ const next = [
17
+ { id: 1, name: 'a' },
18
+ { id: 2, name: 'B' },
19
+ ]
17
20
  const out = reuse(prev, next)
18
21
  expect(out).not.toBe(prev)
19
22
  expect(out[0]).toBe(e0)
@@ -49,7 +52,10 @@ describe('reuse', () => {
49
52
  class View extends S.Class<View>('View')({ title: S.String, rows: S.Array(Row) }) {}
50
53
  const keep = new Row({ id: 1, name: 'a' })
51
54
  const prev = new View({ title: 't', rows: [keep, new Row({ id: 2, name: 'b' })] })
52
- const next = new View({ title: 't', rows: [new Row({ id: 1, name: 'a' }), new Row({ id: 2, name: 'B' })] })
55
+ const next = new View({
56
+ title: 't',
57
+ rows: [new Row({ id: 1, name: 'a' }), new Row({ id: 2, name: 'B' })],
58
+ })
53
59
  const out = reuse(prev, next)
54
60
  expect(out).not.toBe(prev)
55
61
  expect(out).toBeInstanceOf(View)
@@ -77,7 +83,8 @@ describe('reuse', () => {
77
83
 
78
84
  it('returns next below the depth bound', () => {
79
85
  type Nest = { readonly n?: Nest; readonly v: number }
80
- const deep = (depth: number): Nest => (depth === 0 ? { v: 0 } : { v: depth, n: deep(depth - 1) })
86
+ const deep = (depth: number): Nest =>
87
+ depth === 0 ? { v: 0 } : { v: depth, n: deep(depth - 1) }
81
88
  const prev = deep(20)
82
89
  const next = deep(20)
83
90
  const out = reuse(prev, next)
@@ -54,10 +54,13 @@ const reconcile = ({ prev, next, depth }: ReconcileArgs): unknown => {
54
54
  }
55
55
  if (Array.isArray(prev) && Array.isArray(next)) {
56
56
  const reconciled = next.map((element, index) =>
57
- index < prev.length ? reconcile({ prev: prev[index], next: element, depth: depth - 1 }) : element,
57
+ index < prev.length
58
+ ? reconcile({ prev: prev[index], next: element, depth: depth - 1 })
59
+ : element,
58
60
  )
59
61
  const unchanged =
60
- prev.length === next.length && reconciled.every((entry, index) => Object.is(entry, prev[index]))
62
+ prev.length === next.length &&
63
+ reconciled.every((entry, index) => Object.is(entry, prev[index]))
61
64
  return unchanged ? prev : reconciled
62
65
  }
63
66
  if (isPlainRecord(prev) && isPlainRecord(next)) {
@@ -54,7 +54,10 @@ const NotificationsBase: Context.TagClass<Notifications, 'reform/Notifications',
54
54
  Context.Tag('reform/Notifications')<Notifications, Scheduler>()
55
55
  export class Notifications extends NotificationsBase {}
56
56
 
57
- export const notificationsLayer: Layer.Layer<Notifications> = Layer.sync(Notifications, makeScheduler)
57
+ export const notificationsLayer: Layer.Layer<Notifications> = Layer.sync(
58
+ Notifications,
59
+ makeScheduler,
60
+ )
58
61
 
59
62
  export const resolveScheduler: Effect.Effect<Scheduler> = Effect.map(
60
63
  Effect.serviceOption(Notifications),
@@ -1,15 +1,20 @@
1
- import { type Context, Effect, Equal, Predicate } from 'effect'
2
- import { type AnySource, type SourceName, type SourceValue } from '../state/token'
3
- import { type Store } from './store'
4
- import type { AnyValue } from './variance'
1
+ import { Context, Effect, Equal, Predicate } from 'effect'
2
+ import {
3
+ type AnySource,
4
+ type SourceIdentifier,
5
+ type SourceName,
6
+ type SourceValue,
7
+ } from '../state/token'
5
8
 
6
9
  export type InputsObject<Inputs extends ReadonlyArray<AnySource>> = {
7
10
  readonly [S in Inputs[number] as SourceName<S>]: SourceValue<S>
8
11
  }
9
12
 
10
13
  export type StoresOf<Items extends ReadonlyArray<unknown>> = {
11
- [I in keyof Items]: Items[I] extends { readonly store: Context.Tag<infer Svc, AnyValue> }
12
- ? Svc
14
+ [I in keyof Items]: Items[I] extends {
15
+ readonly store: Context.Tag<infer Service, infer _Store>
16
+ }
17
+ ? Service
13
18
  : never
14
19
  }[number]
15
20
 
@@ -26,8 +31,20 @@ const inputKey = (input: AnySource): string =>
26
31
  ? input.manifest['name']
27
32
  : input.name
28
33
 
34
+ // Capture an invariant source tag through a covariant read-only surface.
35
+ interface ReadableStore<out A> {
36
+ readonly getSnapshot: () => A
37
+ readonly subscribe: (listener: () => void) => () => void
38
+ }
39
+
40
+ const readStore = <Source extends AnySource>(
41
+ source: Source,
42
+ ): Effect.Effect<ReadableStore<SourceValue<Source>>, never, SourceIdentifier<Source>> =>
43
+ Context.GenericTag<SourceIdentifier<Source>, ReadableStore<SourceValue<Source>>>(source.store.key)
44
+
29
45
  export const sameKey = (left: ReadonlyArray<unknown>, right: ReadonlyArray<unknown>): boolean =>
30
- left.length === right.length && left.every((element, index) => Equal.equals(element, right[index]))
46
+ left.length === right.length &&
47
+ left.every((element, index) => Equal.equals(element, right[index]))
31
48
 
32
49
  export interface WiredSources<Inputs extends ReadonlyArray<AnySource>> {
33
50
  readonly snapshot: () => InputsObject<Inputs>
@@ -41,10 +58,7 @@ export const wireSources = <Inputs extends ReadonlyArray<AnySource>>(
41
58
  ): Effect.Effect<WiredSources<Inputs>, never, InputStores<Inputs>> =>
42
59
  // oxlint-disable-next-line reform-rules/no-type-assertion -- restate the precise InputStores<Inputs> union the signature promises; Effect.forEach over a heterogeneous tuple widens R (the single seam this cast lives at)
43
60
  Effect.gen(function* () {
44
- const sources: ReadonlyArray<Store<unknown>> = yield* Effect.forEach(
45
- inputs,
46
- (input) => input.store,
47
- )
61
+ const sources = yield* Effect.forEach(inputs, readStore)
48
62
 
49
63
  const snapshot = (): InputsObject<Inputs> => {
50
64
  const args: Record<string, unknown> = {}
@@ -10,8 +10,9 @@ export interface Tracker {
10
10
  add(source: Subscribable): void
11
11
  }
12
12
 
13
- const CurrentTrackerBase: Context.TagClass<CurrentTracker, 'reform/Tracker', Tracker> =
14
- Context.Tag('reform/Tracker')<CurrentTracker, Tracker>()
13
+ const CurrentTrackerBase: Context.TagClass<CurrentTracker, 'reform/Tracker', Tracker> = Context.Tag(
14
+ 'reform/Tracker',
15
+ )<CurrentTracker, Tracker>()
15
16
  export class CurrentTracker extends CurrentTrackerBase {}
16
17
 
17
18
  export const readTracked = <A>(store: Store<A>): Effect.Effect<A> =>
@@ -0,0 +1,222 @@
1
+ // Quarantined surface for first-party hosts and companion packages
2
+ // (`@playfast/reform-*`): reflection/manifest types, TypeIds, erased `Any*`
3
+ // forms, capture callbacks, and runtime seams. Importable as
4
+ // `@playfast/reform/internal`; not part of the app-facing API and not covered
5
+ // by its stability expectations.
6
+
7
+ export {
8
+ type AnyState,
9
+ type StateCapture,
10
+ type StateDefinition,
11
+ type StateManifest,
12
+ type StateName,
13
+ type StateSchemaReflection,
14
+ StateStoreTypeId,
15
+ } from './state/state'
16
+ export {
17
+ type AnyFamily,
18
+ type FamilyName,
19
+ type FamilySchemaReflection,
20
+ type FamilyStore,
21
+ type StateFamilyCapture,
22
+ type StateFamilyManifest,
23
+ StateFamilyStoreTypeId,
24
+ } from './state/stateFamily'
25
+ export { StateToken } from './state/token'
26
+ export type {
27
+ AnySource,
28
+ Source,
29
+ SourceCapture,
30
+ SourceIdentifier,
31
+ SourceName,
32
+ SourceValue,
33
+ } from './state/token'
34
+ export type {
35
+ AnyEvent,
36
+ EventCapture,
37
+ EventManifest,
38
+ EventName,
39
+ EventReflection,
40
+ EventSchemaReflection,
41
+ EventType,
42
+ } from './event/event'
43
+ export { CalcStoreTypeId } from './calc/calc'
44
+ export { CalcFamilyStoreTypeId } from './calc/calcFamily'
45
+ export { AsyncCalcStoreTypeId } from './calc/asyncCalc'
46
+ export { sameKey, type WiredSources, wireSources } from './internal/sources'
47
+ // Optional host seams an `AsyncCalc` consults — persistence and lifecycle signals.
48
+ // Concrete implementations live in edge packages (e.g. `@playfast/reform-query-browser`).
49
+ export {
50
+ noopQueryStore,
51
+ QueryStore,
52
+ type QueryStoreApi,
53
+ resolveQueryStore,
54
+ } from './internal/queryStore'
55
+ export {
56
+ noopQueryEvents,
57
+ QueryEvents,
58
+ type QueryEventsApi,
59
+ resolveQueryEvents,
60
+ } from './internal/queryEvents'
61
+ export {
62
+ RemoteStatePendingStoreTypeId,
63
+ RemoteStateTruthStoreTypeId,
64
+ RemoteStateVisibleStoreTypeId,
65
+ } from './remote/remoteState'
66
+ export { SyncedStoreIdentifierTypeId } from './synced/syncedStore'
67
+ export { BoundaryStoreTypeId } from './boundary/boundary'
68
+ export type {
69
+ AnyProcedure,
70
+ ProcedureCapture,
71
+ ProcedureManifest,
72
+ ProcedureReflection,
73
+ } from './procedure/procedure'
74
+ export {
75
+ type ChannelRuntime,
76
+ Channels,
77
+ channelsLayer,
78
+ type ProcedureEntry,
79
+ Procedures,
80
+ proceduresLayer,
81
+ } from './channel/channel'
82
+ export { CompositionIdTypeId } from './compose/composition'
83
+ export type {
84
+ AnyComposition,
85
+ CompositionCapture,
86
+ CompositionManifest,
87
+ CompositionPropsReceiver,
88
+ CompositionReflection,
89
+ CompositionTag,
90
+ HasExactCompositionId,
91
+ RequiredSlotIdentities,
92
+ SlotClassesOf,
93
+ } from './compose/composition'
94
+ export { StructureTypeId } from './compose/structure'
95
+ export { type PropsId, PropsIdTypeId, type PropsTag, propsTag } from './compose/props'
96
+ export {
97
+ type AnySlot,
98
+ type AnySlotChild,
99
+ type AnySlotInstance,
100
+ type SlotCapture,
101
+ type SlotChild,
102
+ type SlotManifest,
103
+ SlotServiceTypeId,
104
+ SlotTypeId,
105
+ } from './compose/slot'
106
+ export type { SlotRenderer } from './compose/host'
107
+ export {
108
+ type AnyMadeView,
109
+ type AnyUi,
110
+ type MadeViewCapture,
111
+ type UiCapture,
112
+ type UiManifest,
113
+ type UiName,
114
+ type UiReflection,
115
+ type UiSchemaReflection,
116
+ UiServiceTypeId,
117
+ UiTypeId,
118
+ type WiredUi,
119
+ type WiredUiManifest,
120
+ type WireSchemas,
121
+ } from './compose/ui'
122
+
123
+ // Wire: the serializable rendered-tree model + pure diff/apply the remote
124
+ // transport sends over the wire and the client folds back. See REMOTE_UI.md §3.
125
+ export * as Wire from './wire/tree'
126
+ export type { WireNode, WirePatch, WireProp, WireTree } from './wire/tree'
127
+ // Triggers: the server-side handle registry bridging client trigger invocations
128
+ // back to live triggers, decoding payloads via the event schema. See REMOTE_UI.md §4.
129
+ export * as Triggers from './wire/triggers'
130
+ export {
131
+ type TriggerHandle,
132
+ TriggerRegistry,
133
+ type TriggerRegistryApi,
134
+ UnknownTrigger,
135
+ } from './wire/triggers'
136
+
137
+ export {
138
+ type AnyFeatureBinding,
139
+ type AnyLoadedModule,
140
+ type CapturedFeatureBinding,
141
+ type FeatureBinding,
142
+ type FeatureBindingCapture,
143
+ FeatureBindingTypeId,
144
+ type FeatureDefinition,
145
+ FeatureGraphValidTypeId,
146
+ type FeatureIdentityToken,
147
+ FeatureInput,
148
+ type FeatureManifest,
149
+ featureModuleFromInput,
150
+ type FeatureMountProps,
151
+ type FeatureOpenServices,
152
+ type FeatureRequirement,
153
+ type FeatureSupportOpenServices,
154
+ type FeatureToken,
155
+ type FeatureTreeCarrier,
156
+ type LoadedModule,
157
+ type LoadedModuleCapture,
158
+ mountFeature,
159
+ type ProvideableFeature,
160
+ type ProvidedBy,
161
+ type RequireCarrier,
162
+ } from './feature/feature'
163
+ export {
164
+ type CompositionChildSummary,
165
+ CompositionChildSummaryTypeId,
166
+ type DescendantOpenServices,
167
+ type FeatureChildSummary,
168
+ FeatureChildSummaryTypeId,
169
+ type ModuleOpenServices,
170
+ type OpenServicesFromFeatureSlotWitnesses,
171
+ type SlotBindingSummary,
172
+ SlotBindingSummaryTypeId,
173
+ type ValidateFeatureGraphLayer,
174
+ } from './graph/closure'
175
+ export { type AnyScene, type AnySceneCapture, type SceneCapture, seedAnyScene } from './scene/scene'
176
+ export { Bus, busLayer, type Envelope, type Tagged } from './runtime/bus'
177
+ export {
178
+ type AnyFeatureMountHandlers,
179
+ type CapturedFeatureMountHandlers,
180
+ type FeatureMountHandlersExternalApi,
181
+ type UnspecifiedRuntimeServices,
182
+ UnspecifiedRuntimeServicesTypeId,
183
+ } from './runtime/appRuntime'
184
+ export {
185
+ EngineRequirements,
186
+ type EngineServices,
187
+ type ReducerEntry,
188
+ Reducers,
189
+ reducersLayer,
190
+ } from './runtime/loop'
191
+ export { CurrentEventBudget, defaultEventBudget, resolveEventBudget } from './runtime/eventBudget'
192
+ // The profiler SPI: synchronous hooks the engine calls on its hot paths, plus
193
+ // the FiberRef seam `@playfast/reform-profiler` writes through (`profiledScene`).
194
+ export {
195
+ CurrentInstrumentation,
196
+ type Instrumentation,
197
+ noopInstrumentation,
198
+ resolveInstrumentation,
199
+ type SpanEnd,
200
+ } from './runtime/instrumentation'
201
+ // The runtime-wide registry of live query handles. `AsyncCalc.invalidate` /
202
+ // `AsyncCalc.refetch` and provider layers act on a query by name through it.
203
+ export { Queries, queriesLayer, type QueryHandle, type QueryRegistry } from './runtime/queries'
204
+ export { CurrentTracker, readTracked, type Subscribable, type Tracker } from './internal/track'
205
+ export { type CapturedRender, CaptureSink, type CaptureSinkApi } from './internal/capture'
206
+ // Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
207
+ // multiple mounted roots) provide `Notifications` upstream of their state layers;
208
+ // everything else coalesces on the process-wide default.
209
+ export {
210
+ defaultScheduler,
211
+ makeScheduler,
212
+ Notifications,
213
+ notificationsLayer,
214
+ resolveScheduler,
215
+ type Scheduler,
216
+ } from './internal/scheduler'
217
+ export { makeStore, type Store } from './internal/store'
218
+ // Primitive-authoring toolkit — for first-party companion packages
219
+ // (e.g. `@playfast/reform-db`, `@playfast/reform-resource`) that build their own
220
+ // first-class `Source`s which must participate in dependency tracking and
221
+ // reflection exactly like a `Calc`.
222
+ export { type Kind, type Manifest, yieldableClass } from './definition/definition'