@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.
- package/package.json +1 -1
- package/src/boundary/boundary.ts +16 -7
- package/src/calc/asyncCalc.invalidate.test.ts +166 -0
- package/src/calc/asyncCalc.ts +116 -25
- package/src/calc/asyncData.ts +8 -4
- package/src/calc/calc.ts +7 -3
- package/src/calc/calcFamily.ts +24 -10
- package/src/calc/compose.ts +1 -1
- package/src/calc/queryState.ts +52 -0
- package/src/channel/channel.ts +63 -37
- package/src/compose/composition.ts +20 -1
- package/src/compose/provide.ts +5 -1
- package/src/compose/slot.ts +4 -2
- package/src/compose/structure.ts +43 -19
- package/src/compose/ui.ts +21 -2
- package/src/compose/ui.typecheck.ts +4 -4
- package/src/definition/definition.ts +20 -7
- package/src/feature/feature.test.ts +4 -4
- package/src/feature/feature.ts +30 -16
- package/src/feature/feature.typecheck.ts +2 -2
- package/src/index.ts +21 -0
- package/src/internal/capture.ts +1 -0
- package/src/internal/errors.ts +9 -4
- package/src/internal/inspect.ts +4 -4
- package/src/internal/queryDriver.ts +273 -86
- package/src/internal/queryEvents.ts +34 -0
- package/src/internal/queryStore.ts +36 -0
- package/src/internal/reuse.ts +67 -30
- package/src/internal/scheduler.ts +35 -23
- package/src/internal/sources.ts +14 -8
- package/src/internal/stateRegistry.ts +3 -1
- package/src/internal/store.ts +10 -8
- package/src/internal/track.ts +3 -1
- package/src/procedure/procedure.ts +2 -2
- package/src/reducer/reducer.ts +17 -9
- package/src/remote/remoteState.test.ts +188 -1
- package/src/remote/remoteState.ts +112 -51
- package/src/remote/remoteState.typecheck.ts +4 -1
- package/src/runtime/bus.ts +3 -1
- package/src/runtime/hardening.test.ts +1 -1
- package/src/runtime/loop.ts +81 -53
- package/src/runtime/queries.ts +55 -0
- package/src/scene/scene.ts +16 -8
- package/src/state/state.ts +11 -10
- package/src/state/stateFamily.ts +27 -11
- package/src/state/stateGroup.ts +22 -9
- package/src/synced/syncedStore.ts +15 -9
- package/src/wire/tree.ts +66 -30
- package/src/wire/triggers.ts +9 -8
package/src/feature/feature.ts
CHANGED
|
@@ -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 = (
|
|
164
|
-
typeof
|
|
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 = (
|
|
199
|
-
(typeof
|
|
200
|
-
|
|
201
|
-
'manifest' in
|
|
202
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
257
|
-
|
|
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(() =>
|
|
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(() =>
|
|
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,
|
package/src/internal/capture.ts
CHANGED
|
@@ -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
|
|
package/src/internal/errors.ts
CHANGED
|
@@ -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))
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
package/src/internal/inspect.ts
CHANGED
|
@@ -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)
|
|
25
|
-
|
|
24
|
+
const trio = inspectable(toJSON)
|
|
25
|
+
Reflect.ownKeys(trio).forEach((key) => {
|
|
26
26
|
Object.defineProperty(target, key, {
|
|
27
|
-
value: trio
|
|
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
|
}
|