@playfast/reform 0.0.9 → 0.0.11

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 (49) hide show
  1. package/package.json +1 -1
  2. package/src/boundary/boundary.ts +16 -7
  3. package/src/calc/asyncCalc.invalidate.test.ts +166 -0
  4. package/src/calc/asyncCalc.ts +116 -25
  5. package/src/calc/asyncData.ts +8 -4
  6. package/src/calc/calc.ts +7 -3
  7. package/src/calc/calcFamily.ts +24 -10
  8. package/src/calc/compose.ts +1 -1
  9. package/src/calc/queryState.ts +52 -0
  10. package/src/channel/channel.ts +63 -37
  11. package/src/compose/composition.ts +20 -1
  12. package/src/compose/provide.ts +5 -1
  13. package/src/compose/slot.ts +4 -2
  14. package/src/compose/structure.ts +43 -19
  15. package/src/compose/ui.ts +21 -2
  16. package/src/compose/ui.typecheck.ts +4 -4
  17. package/src/definition/definition.ts +20 -7
  18. package/src/feature/feature.test.ts +4 -4
  19. package/src/feature/feature.ts +30 -16
  20. package/src/feature/feature.typecheck.ts +2 -2
  21. package/src/index.ts +21 -0
  22. package/src/internal/capture.ts +1 -0
  23. package/src/internal/errors.ts +9 -4
  24. package/src/internal/inspect.ts +4 -4
  25. package/src/internal/queryDriver.ts +273 -86
  26. package/src/internal/queryEvents.ts +34 -0
  27. package/src/internal/queryStore.ts +36 -0
  28. package/src/internal/reuse.ts +67 -30
  29. package/src/internal/scheduler.ts +35 -23
  30. package/src/internal/sources.ts +14 -8
  31. package/src/internal/stateRegistry.ts +3 -1
  32. package/src/internal/store.ts +10 -8
  33. package/src/internal/track.ts +3 -1
  34. package/src/procedure/procedure.ts +2 -2
  35. package/src/reducer/reducer.ts +17 -9
  36. package/src/remote/remoteState.test.ts +188 -1
  37. package/src/remote/remoteState.ts +112 -51
  38. package/src/remote/remoteState.typecheck.ts +4 -1
  39. package/src/runtime/bus.ts +3 -1
  40. package/src/runtime/hardening.test.ts +1 -1
  41. package/src/runtime/loop.ts +81 -53
  42. package/src/runtime/queries.ts +55 -0
  43. package/src/scene/scene.ts +16 -8
  44. package/src/state/state.ts +11 -10
  45. package/src/state/stateFamily.ts +27 -11
  46. package/src/state/stateGroup.ts +22 -9
  47. package/src/synced/syncedStore.ts +15 -9
  48. package/src/wire/tree.ts +66 -30
  49. package/src/wire/triggers.ts +9 -8
@@ -1,4 +1,4 @@
1
- import { type Context, Effect, Layer, type Scope } from 'effect'
1
+ import { type Context, Effect, Layer, Option, type Scope } from 'effect'
2
2
  import { type Engine } from '../runtime/loop'
3
3
  import { type StoresOf } from '../internal/sources'
4
4
  import { FeatureLoadFailed } from '../internal/errors'
@@ -114,10 +114,10 @@ export interface FeatureManifest extends Manifest {
114
114
  readonly name: string
115
115
  readonly strategy: 'lazy' | 'default'
116
116
  readonly composition: Manifest & { readonly kind: 'Composition' }
117
- readonly placeholder?: {
117
+ readonly placeholder: Option.Option<{
118
118
  readonly loading: Manifest & { readonly kind: 'Composition' }
119
119
  readonly failed: Manifest & { readonly kind: 'Composition' }
120
- }
120
+ }>
121
121
  }
122
122
 
123
123
  /**
@@ -152,6 +152,7 @@ export interface FeatureBinding {
152
152
  readonly composition: CompositionClass<unknown>
153
153
  readonly strategy: 'lazy' | 'default'
154
154
  readonly load: Effect.Effect<LoadedModule, FeatureLoadFailed>
155
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- read at the react host seam via `binding.placeholder === undefined`; an Option here would force a cross-package edit (react host) owned by another agent
155
156
  readonly placeholder?: {
156
157
  readonly loading: CompositionClass<unknown>
157
158
  readonly failed: CompositionClass<unknown>
@@ -160,8 +161,8 @@ export interface FeatureBinding {
160
161
  }
161
162
 
162
163
  /** Whether a slot's bound child is a lazy/eager feature (vs a plain composition). */
163
- export const isFeatureBinding = (u: unknown): u is FeatureBinding =>
164
- typeof u === 'object' && u !== null && FeatureBindingTypeId in u
164
+ export const isFeatureBinding = (candidate: unknown): candidate is FeatureBinding =>
165
+ typeof candidate === 'object' && candidate !== null && FeatureBindingTypeId in candidate
165
166
 
166
167
  /**
167
168
  * A feature *definition*: a reflectable manifest (`kind: 'Feature'`) that carries
@@ -195,11 +196,14 @@ export interface FeatureClass<
195
196
  export type AnyFeature = FeatureClass<any, any, any, ReadonlyArray<RequireCarrier>>
196
197
 
197
198
  /** Whether a value is a feature definition (its target shape in `provide(slot, Feature)`). */
198
- export const isFeature = (u: unknown): u is AnyFeature =>
199
- (typeof u === 'function' || typeof u === 'object') &&
200
- u !== null &&
201
- 'manifest' in u &&
202
- (u as { readonly manifest: { readonly kind?: unknown } }).manifest?.kind === 'Feature'
199
+ export const isFeature = (candidate: unknown): candidate is AnyFeature =>
200
+ (typeof candidate === 'function' || typeof candidate === 'object') &&
201
+ candidate !== null &&
202
+ 'manifest' in candidate &&
203
+ typeof candidate.manifest === 'object' &&
204
+ candidate.manifest !== null &&
205
+ 'kind' in candidate.manifest &&
206
+ candidate.manifest.kind === 'Feature'
203
207
 
204
208
  /**
205
209
  * The placeholders a lazy feature must ship — both eager compositions (their
@@ -213,7 +217,12 @@ export interface Placeholders {
213
217
  readonly failed: CompositionClass<any, UiContract>
214
218
  }
215
219
 
216
- type FeatureConfig<P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>> = {
220
+ type FeatureConfigExternalApi<
221
+ P,
222
+ C extends UiContract,
223
+ ROut,
224
+ Requires extends ReadonlyArray<RequireCarrier>,
225
+ > = {
217
226
  readonly composition: CompositionClass<P, C>
218
227
  readonly boot?: ReadonlyArray<Tagged>
219
228
  } & (
@@ -240,7 +249,7 @@ type FeatureConfig<P, C extends UiContract, ROut, Requires extends ReadonlyArray
240
249
  */
241
250
  export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>>(
242
251
  name: string,
243
- config: FeatureConfig<P, C, ROut, Requires>,
252
+ config: FeatureConfigExternalApi<P, C, ROut, Requires>,
244
253
  ): FeatureClass<P, C, ROut, Requires> => {
245
254
  const strategy: 'lazy' | 'default' = config.loadingStrategy ?? 'default'
246
255
  const load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed> =
@@ -253,9 +262,12 @@ export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArr
253
262
  name,
254
263
  strategy,
255
264
  composition: config.composition.manifest,
256
- ...(placeholder
257
- ? { placeholder: { loading: placeholder.loading.manifest, failed: placeholder.failed.manifest } }
258
- : {}),
265
+ placeholder: Option.fromNullable(placeholder).pipe(
266
+ Option.map((placeholders) => ({
267
+ loading: placeholders.loading.manifest,
268
+ failed: placeholders.failed.manifest,
269
+ })),
270
+ ),
259
271
  }
260
272
 
261
273
  const binding: FeatureBinding = {
@@ -267,7 +279,7 @@ export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArr
267
279
  // assignment to the erased `LoadedModule` needs no cast — see `LoadedModule`.
268
280
  load,
269
281
  ...(placeholder ? { placeholder: { loading: placeholder.loading, failed: placeholder.failed } } : {}),
270
- boot: config.boot ?? [],
282
+ boot: Option.getOrElse(Option.fromNullable(config.boot), () => []),
271
283
  }
272
284
 
273
285
  return definitionClass<FeatureClass<P, C, ROut, Requires>>({ manifest, load, binding, eagerModule })
@@ -289,12 +301,14 @@ export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArr
289
301
  * we re-attach the concrete `engineContext` it builds against. `R` is the engine
290
302
  * context's service set, supplied by the host.
291
303
  */
304
+ // oxlint-disable-next-line reform-rules/prefer-effect-fn -- generic export: Effect.fn's inferred type isn't portable under isolatedDeclarations
292
305
  export const mountFeature = <R>(
293
306
  binding: FeatureBinding,
294
307
  engineContext: Context.Context<R>,
295
308
  ): Effect.Effect<Context.Context<unknown>, FeatureLoadFailed, Scope.Scope> =>
296
309
  Effect.gen(function* () {
297
310
  const { layer } = yield* binding.load
311
+ // oxlint-disable-next-line reform-rules/no-type-assertion -- documented erasure boundary: re-attach the concrete engine context to the type-erased `LoadedModule` layer (mirrors `buildRuntime`); RIn was verified cast-free at `provide`/scene wiring
298
312
  const featureLayer = layer as Layer.Layer<unknown, never, R>
299
313
  const context = yield* Layer.build(
300
314
  Layer.provideMerge(featureLayer, Layer.succeedContext(engineContext)),
@@ -65,7 +65,7 @@ const AppFeatureBase: Feature.FeatureClass<
65
65
  > = Feature.make('appFeature', {
66
66
  loadingStrategy: 'lazy',
67
67
  composition: AppComp,
68
- load: lazyImport(() => Promise.resolve({ default: featureModule([], Layer.empty) })),
68
+ load: lazyImport(async () => ({ default: featureModule([], Layer.empty) })),
69
69
  placeholder: { loading: AppComp, failed: AppComp },
70
70
  })
71
71
  class AppFeature extends AppFeatureBase {}
@@ -95,7 +95,7 @@ const OtherFeatureBase: Feature.FeatureClass<
95
95
  > = Feature.make('otherFeature', {
96
96
  loadingStrategy: 'lazy',
97
97
  composition: OtherComp,
98
- load: lazyImport(() => Promise.resolve({ default: featureModule([], Layer.empty) })),
98
+ load: lazyImport(async () => ({ default: featureModule([], Layer.empty) })),
99
99
  placeholder: { loading: OtherComp, failed: OtherComp },
100
100
  })
101
101
  class OtherFeature extends OtherFeatureBase {}
package/src/index.ts CHANGED
@@ -31,6 +31,23 @@ export {
31
31
  type AsyncLoading,
32
32
  type AsyncSuccess,
33
33
  } from './calc/asyncData'
34
+ // The flat, data-oriented query value (`data`/`error`/`isFetching`/`isStale`) an
35
+ // `AsyncCalc` is driven by; `toAsyncData` projects it onto the matchable arms.
36
+ export { isLoading, type QueryState, toAsyncData } from './calc/queryState'
37
+ // Optional host seams an `AsyncCalc` consults — persistence and lifecycle signals.
38
+ // Concrete implementations live in edge packages (e.g. `@playfast/reform-query-browser`).
39
+ export {
40
+ noopQueryStore,
41
+ QueryStore,
42
+ type QueryStoreApi,
43
+ resolveQueryStore,
44
+ } from './internal/queryStore'
45
+ export {
46
+ noopQueryEvents,
47
+ QueryEvents,
48
+ type QueryEventsApi,
49
+ resolveQueryEvents,
50
+ } from './internal/queryEvents'
34
51
  export * as RemoteState from './remote/remoteState'
35
52
  // Direct type re-exports alongside the namespace (TS2742 nameability): an app
36
53
  // layer's inferred type references these (`Store<ReadonlyArray<PendingIntent<…>>>`),
@@ -146,10 +163,14 @@ export { type SlotChild } from './compose/slot'
146
163
  // A scene bundles a composition with the closed wiring that runs it — the single
147
164
  // handle the react host, proofs, and the dev tool all consume.
148
165
  export { isScene, type MountedServices, type Scene, scene, seedScene } from './scene/scene'
166
+ export type { SeedsOf } from './state/stateGroup'
149
167
 
150
168
  // Runtime surface for hosts (`@reform/react`) and headless tests.
151
169
  export { Bus, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
152
170
  export { Engine, type ReducerEntry, Reducers } from './runtime/loop'
171
+ // The runtime-wide registry of live query handles. `AsyncCalc.invalidate` /
172
+ // `AsyncCalc.refetch` and provider layers act on a query by name through it.
173
+ export { Queries, type QueryHandle, type QueryRegistry } from './runtime/queries'
153
174
  export {
154
175
  type ChannelClass,
155
176
  type ChannelPolicy,
@@ -17,6 +17,7 @@ export interface UiCapture {
17
17
  * components and has no fill key, so it is omitted there. Lets the proof
18
18
  * facade select a child by key (`byKey`) instead of by render index.
19
19
  */
20
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- omitted (not Option-none) on the legacy Node path; built in compose/structure.ts and read by the proof package, both outside this batch
20
21
  readonly key?: string
21
22
  }
22
23
 
@@ -94,8 +94,12 @@ export class AsyncSceneLayer extends AsyncSceneLayerBase {
94
94
  * shapes are checked.
95
95
  */
96
96
  const isAsyncBuildDefect = (error: unknown): boolean => {
97
- if (Runtime.isAsyncFiberException(error)) return true
98
- if (!Runtime.isFiberFailure(error)) return false
97
+ if (Runtime.isAsyncFiberException(error)) {
98
+ return true
99
+ }
100
+ if (!Runtime.isFiberFailure(error)) {
101
+ return false
102
+ }
99
103
  const die = Cause.dieOption(error[Runtime.FiberFailureCauseId])
100
104
  return Option.isSome(die) && Runtime.isAsyncFiberException(die.value)
101
105
  }
@@ -107,11 +111,12 @@ const isAsyncBuildDefect = (error: unknown): boolean => {
107
111
  * the React host and the remote server.
108
112
  */
109
113
  export const forceSync = <A>(thunk: () => A): A => {
114
+ // oxlint-disable-next-line reform-rules/no-try-catch -- sync-build boundary must throw the exact value, which Effect.runSync would re-wrap in a FiberFailure
110
115
  try {
111
116
  return thunk()
112
117
  } catch (error) {
113
- if (isAsyncBuildDefect(error)) throw new AsyncSceneLayer()
114
- throw error
118
+ // oxlint-disable-next-line reform-rules/no-throw -- rethrow the exact value (translated for the async-build defect) so callers can match on it
119
+ throw isAsyncBuildDefect(error) ? new AsyncSceneLayer() : error
115
120
  }
116
121
  }
117
122
 
@@ -21,14 +21,14 @@ export const inspectable = (toJSON: () => unknown): Inspectable => ({
21
21
  * whose statics are merged in separately.
22
22
  */
23
23
  export const attachInspectable = <T extends object>(target: T, toJSON: () => unknown): T => {
24
- const trio = inspectable(toJSON) as unknown as Record<PropertyKey, unknown>
25
- for (const key of Reflect.ownKeys(trio)) {
24
+ const trio = inspectable(toJSON)
25
+ Reflect.ownKeys(trio).forEach((key) => {
26
26
  Object.defineProperty(target, key, {
27
- value: trio[key],
27
+ value: Reflect.get(trio, key),
28
28
  enumerable: false,
29
29
  writable: true,
30
30
  configurable: true,
31
31
  })
32
- }
32
+ })
33
33
  return target
34
34
  }