@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.
- package/README.md +103 -56
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +63 -49
- package/src/boundary/boundary.ts +144 -113
- package/src/calc/asyncCalc.invalidate.test.ts +518 -32
- package/src/calc/asyncCalc.test.ts +207 -213
- package/src/calc/asyncCalc.ts +131 -154
- package/src/calc/asyncData.ts +0 -37
- package/src/calc/calc.test.ts +7 -33
- package/src/calc/calc.ts +44 -58
- package/src/calc/calcFamily.test.ts +80 -34
- package/src/calc/calcFamily.ts +54 -65
- package/src/calc/compose.test.ts +1 -12
- package/src/calc/compose.ts +1 -36
- package/src/calc/queryState.ts +0 -23
- package/src/channel/channel.ts +126 -111
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +351 -127
- package/src/compose/host.ts +0 -6
- package/src/compose/props.ts +13 -12
- package/src/compose/provide.ts +189 -62
- package/src/compose/slot.ts +158 -49
- package/src/compose/structure.test.ts +6 -9
- package/src/compose/structure.ts +108 -79
- package/src/compose/ui.test.ts +19 -7
- package/src/compose/ui.ts +155 -156
- package/src/compose/ui.typecheck.ts +40 -18
- package/src/definition/definition.ts +22 -41
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -3
- package/src/event/event.ts +102 -27
- package/src/event/eventGroup.ts +0 -1
- package/src/feature/feature.mount.test.ts +122 -43
- package/src/feature/feature.test.ts +46 -21
- package/src/feature/feature.ts +1347 -256
- package/src/feature/feature.typecheck.ts +273 -68
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +68 -126
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -24
- package/src/internal/env.ts +7 -0
- package/src/internal/errors.test.ts +0 -6
- package/src/internal/errors.ts +37 -60
- package/src/internal/inspect.test.ts +0 -6
- package/src/internal/inspect.ts +0 -12
- package/src/internal/queryDriver.ts +105 -201
- package/src/internal/queryDriverStore.ts +156 -0
- package/src/internal/queryDriverTypes.ts +59 -0
- package/src/internal/queryEvents.ts +0 -12
- package/src/internal/queryStore.ts +0 -14
- package/src/internal/reuse.test.ts +10 -14
- package/src/internal/reuse.ts +5 -30
- package/src/internal/scheduler.ts +4 -44
- package/src/internal/seeds.ts +0 -14
- package/src/internal/sources.ts +26 -49
- package/src/internal/stateRegistry.ts +0 -14
- package/src/internal/store.test.ts +1 -3
- package/src/internal/store.ts +0 -37
- package/src/internal/track.ts +3 -20
- package/src/internal/variance.ts +5 -0
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.test.ts +46 -0
- package/src/namespace/namespace.ts +106 -0
- package/src/procedure/procedure.ts +40 -35
- package/src/reducer/reducer.ts +78 -52
- package/src/remote/remoteState.test.ts +590 -397
- package/src/remote/remoteState.ts +425 -347
- package/src/remote/remoteState.typecheck.ts +47 -56
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +249 -0
- package/src/runtime/appRuntime.ts +415 -97
- package/src/runtime/bus.ts +2 -15
- package/src/runtime/eventBudget.test.ts +152 -0
- package/src/runtime/eventBudget.ts +120 -0
- package/src/runtime/hardening.test.ts +73 -13
- package/src/runtime/instrumentation.test.ts +55 -52
- package/src/runtime/instrumentation.ts +6 -60
- package/src/runtime/loop.test.ts +36 -17
- package/src/runtime/loop.ts +87 -88
- package/src/runtime/queries.ts +0 -17
- package/src/scene/featureScene.test.ts +61 -0
- package/src/scene/scene.ts +247 -104
- package/src/scene/seedScene.test.ts +42 -79
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +80 -46
- package/src/state/stateFamily.test.ts +16 -11
- package/src/state/stateFamily.ts +55 -65
- package/src/state/stateGroup.ts +53 -64
- package/src/state/token.ts +40 -23
- package/src/synced/syncedStore.ts +46 -58
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/ui/node.ts +0 -6
- package/src/ui/trigger.ts +0 -5
- package/src/wire/tree.test.ts +8 -7
- package/src/wire/tree.ts +0 -39
- package/src/wire/triggers.test.ts +6 -6
- package/src/wire/triggers.ts +14 -32
- package/src/internal/ctx.ts +0 -16
package/src/index.ts
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
// Reform core — the renderer-neutral primitives and runtime.
|
|
2
|
-
// Definitions (`X.make`) are separate from implementations (`.live`)
|
|
3
|
-
// throughout, and nothing self-registers on import (principle #1, #6).
|
|
4
|
-
|
|
5
1
|
export * as State from './state/state'
|
|
2
|
+
export type { StateClass, StateStore, StateValue } from './state/state'
|
|
6
3
|
export * as StateGroup from './state/stateGroup'
|
|
4
|
+
export type { SeedsOf } from './state/stateGroup'
|
|
7
5
|
export * as StateFamily from './state/stateFamily'
|
|
8
|
-
export {
|
|
9
|
-
|
|
6
|
+
export type {
|
|
7
|
+
FamilyKey,
|
|
8
|
+
FamilyValue,
|
|
9
|
+
StateFamilyClass,
|
|
10
|
+
StateFamilyStore,
|
|
11
|
+
} from './state/stateFamily'
|
|
10
12
|
|
|
11
13
|
export * as Event from './event/event'
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// TS2742: direct type exports allow declaration emit to name event-derived types.
|
|
15
|
+
export type {
|
|
16
|
+
EventClass,
|
|
17
|
+
EventOf,
|
|
18
|
+
EventPayload,
|
|
19
|
+
SourceChange,
|
|
20
|
+
SourceEventConfig,
|
|
21
|
+
SourceEventConfigExternalApi,
|
|
22
|
+
} from './event/event'
|
|
16
23
|
export * as EventGroup from './event/eventGroup'
|
|
17
24
|
|
|
18
25
|
export * as Reducer from './reducer/reducer'
|
|
19
26
|
export * as Calc from './calc/calc'
|
|
27
|
+
export type { CalcClass, CalcStore } from './calc/calc'
|
|
20
28
|
export * as CalcFamily from './calc/calcFamily'
|
|
29
|
+
export type { CalcFamilyClass, CalcFamilyStore } from './calc/calcFamily'
|
|
21
30
|
export * as AsyncCalc from './calc/asyncCalc'
|
|
31
|
+
export type { AsyncCalcClass, AsyncCalcStore } from './calc/asyncCalc'
|
|
22
32
|
export { composeCalcs } from './calc/compose'
|
|
23
|
-
// The inputs-object/key/store types appearing in `AsyncCalc.live` /
|
|
24
|
-
// `RemoteState.live` signatures, exported so app code can annotate
|
|
25
|
-
// query-swapping helpers and layer types without re-spelling them.
|
|
26
33
|
export type { InputsObject, InputStores, InvalidateBy } from './internal/sources'
|
|
27
34
|
export {
|
|
28
35
|
AsyncData,
|
|
@@ -31,138 +38,118 @@ export {
|
|
|
31
38
|
type AsyncLoading,
|
|
32
39
|
type AsyncSuccess,
|
|
33
40
|
} 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
41
|
export { isLoading, type QueryState, toAsyncData } from './calc/queryState'
|
|
37
|
-
|
|
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'
|
|
42
|
+
|
|
51
43
|
export * as RemoteState from './remote/remoteState'
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
// TS2742: direct exports keep pending and failed state types nameable.
|
|
45
|
+
export type {
|
|
46
|
+
FailedIntent,
|
|
47
|
+
PendingIntent,
|
|
48
|
+
RemoteStateClass,
|
|
49
|
+
RemoteStatePendingStore,
|
|
50
|
+
RemoteStateSettled,
|
|
51
|
+
RemoteStateTruthStore,
|
|
52
|
+
RemoteStateVisibleStore,
|
|
53
|
+
} from './remote/remoteState'
|
|
56
54
|
export * as SyncedStore from './synced/syncedStore'
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
export type { SyncedSource, SyncedStoreClass } from './synced/syncedStore'
|
|
55
|
+
// TS2742: adapters need a directly nameable synchronized source type.
|
|
56
|
+
export type { SyncedSource, SyncedStoreClass, SyncedStoreIdentifier } from './synced/syncedStore'
|
|
60
57
|
export * as Boundary from './boundary/boundary'
|
|
61
|
-
//
|
|
62
|
-
// reason as the RemoteState types above): app code annotates props with these.
|
|
58
|
+
// TS2742: direct exports keep boundary state types nameable.
|
|
63
59
|
export type {
|
|
64
60
|
BoundaryErrored,
|
|
65
61
|
BoundaryPending,
|
|
66
62
|
BoundaryReady,
|
|
67
63
|
BoundaryState,
|
|
64
|
+
BoundaryStore,
|
|
68
65
|
} from './boundary/boundary'
|
|
66
|
+
|
|
69
67
|
export * as Procedure from './procedure/procedure'
|
|
68
|
+
export type { ProcedureClass, ProcedureConfig } from './procedure/procedure'
|
|
70
69
|
export * as Channel from './channel/channel'
|
|
70
|
+
export type { ChannelClass, ChannelPolicy } from './channel/channel'
|
|
71
|
+
export * as Namespace from './namespace/namespace'
|
|
72
|
+
export type { FeatureNamespace } from './namespace/namespace'
|
|
71
73
|
|
|
72
74
|
export * as Composition from './compose/composition'
|
|
75
|
+
export { liveScoped } from './compose/composition'
|
|
73
76
|
export type {
|
|
77
|
+
CompositionActivation,
|
|
74
78
|
CompositionClass,
|
|
79
|
+
CompositionId,
|
|
75
80
|
CompositionService,
|
|
76
81
|
Frame,
|
|
77
82
|
RenderEnv,
|
|
83
|
+
ScopedCompositionLiveConfig,
|
|
78
84
|
} from './compose/composition'
|
|
79
85
|
export {
|
|
80
86
|
absent,
|
|
87
|
+
type AbsentFill,
|
|
81
88
|
each,
|
|
82
89
|
type EachFill,
|
|
83
90
|
isStructure,
|
|
84
91
|
mount,
|
|
85
92
|
one,
|
|
86
93
|
type OneFill,
|
|
87
|
-
type AbsentFill,
|
|
88
94
|
type SlotFill,
|
|
89
95
|
type Structure,
|
|
90
96
|
structureEquals,
|
|
91
97
|
type StructureSlots,
|
|
92
|
-
StructureTypeId,
|
|
93
98
|
when,
|
|
94
99
|
} from './compose/structure'
|
|
95
100
|
export { Props } from './compose/props'
|
|
96
101
|
export {
|
|
102
|
+
type CompositionOfSlotTarget,
|
|
103
|
+
type ContractOfSlotTarget,
|
|
104
|
+
type FeatureSlotTarget,
|
|
97
105
|
isSlot,
|
|
98
106
|
type Node,
|
|
107
|
+
type PropsOfSlotTarget,
|
|
99
108
|
slot,
|
|
100
109
|
type SlotClass,
|
|
110
|
+
type SlotComposition,
|
|
111
|
+
type SlotContract,
|
|
101
112
|
type SlotInstance,
|
|
102
113
|
type SlotProps,
|
|
103
|
-
|
|
114
|
+
type SlotService,
|
|
115
|
+
type SlotTarget,
|
|
116
|
+
type TargetOfSlot,
|
|
104
117
|
} from './compose/slot'
|
|
105
|
-
export { type SlotRenderer } from './compose/host'
|
|
106
118
|
export * as Ui from './compose/ui'
|
|
107
119
|
export {
|
|
120
|
+
type CallableView,
|
|
108
121
|
type DerivedContract,
|
|
109
122
|
isUi,
|
|
110
123
|
type MadeView,
|
|
111
124
|
ui,
|
|
112
125
|
type UiClass,
|
|
113
126
|
type UiContract,
|
|
114
|
-
type
|
|
115
|
-
UiTypeId,
|
|
127
|
+
type UiService,
|
|
116
128
|
UiViewContract,
|
|
117
129
|
type ViewImpl,
|
|
118
|
-
type WireSchemas,
|
|
119
|
-
type WiredUi,
|
|
120
|
-
type WiredUiManifest,
|
|
121
130
|
} from './compose/ui'
|
|
122
|
-
export { provide } from './compose/provide'
|
|
131
|
+
export { provide, provideExisting, provideMergeExact } from './compose/provide'
|
|
123
132
|
export type { Trigger } from './ui/trigger'
|
|
124
133
|
|
|
125
|
-
// Wire: the serializable rendered-tree model + pure diff/apply the remote
|
|
126
|
-
// transport sends over the wire and the client folds back. See REMOTE_UI.md §3.
|
|
127
|
-
export * as Wire from './wire/tree'
|
|
128
|
-
export type { WireNode, WirePatch, WireProp, WireTree } from './wire/tree'
|
|
129
|
-
|
|
130
|
-
// Triggers: the server-side handle registry bridging client trigger invocations
|
|
131
|
-
// back to live triggers, decoding payloads via the event schema. See REMOTE_UI.md §4.
|
|
132
|
-
export * as Triggers from './wire/triggers'
|
|
133
|
-
export {
|
|
134
|
-
TriggerRegistry,
|
|
135
|
-
type TriggerHandle,
|
|
136
|
-
type TriggerRegistryApi,
|
|
137
|
-
UnknownTrigger,
|
|
138
|
-
} from './wire/triggers'
|
|
139
|
-
|
|
140
|
-
// Feature: the lazy/eager code-split unit. Definitions stay eager and reflectable;
|
|
141
|
-
// the heavy `.live` layer loads on demand (an Effect, never a Promise). See LAZY.md.
|
|
142
134
|
export * as Feature from './feature/feature'
|
|
143
135
|
export {
|
|
144
|
-
type
|
|
136
|
+
type EagerFeatureClass,
|
|
145
137
|
type FailedProps,
|
|
146
|
-
type FeatureBinding,
|
|
147
|
-
FeatureBindingTypeId,
|
|
148
138
|
type FeatureClass,
|
|
149
|
-
type FeatureManifest,
|
|
150
139
|
featureModule,
|
|
151
140
|
type FeatureModule,
|
|
141
|
+
type FeatureModuleInput,
|
|
152
142
|
isFeature,
|
|
153
143
|
isFeatureBinding,
|
|
154
144
|
lazyImport,
|
|
155
|
-
type
|
|
156
|
-
mountFeature,
|
|
145
|
+
type LazyFeatureClass,
|
|
157
146
|
type Placeholders,
|
|
158
|
-
type ProvidedBy,
|
|
159
|
-
type RequireCarrier,
|
|
160
147
|
} from './feature/feature'
|
|
161
|
-
export { type SlotChild } from './compose/slot'
|
|
162
148
|
|
|
163
|
-
// A scene bundles a composition with the closed wiring that runs it — the single
|
|
164
|
-
// handle the react host, proofs, and the dev tool all consume.
|
|
165
149
|
export {
|
|
150
|
+
type CapturedScene,
|
|
151
|
+
featureScene,
|
|
152
|
+
type FeatureSceneConfig,
|
|
166
153
|
isScene,
|
|
167
154
|
type MountedServices,
|
|
168
155
|
profiledScene,
|
|
@@ -171,69 +158,24 @@ export {
|
|
|
171
158
|
sceneInstrumentation,
|
|
172
159
|
seedScene,
|
|
173
160
|
} from './scene/scene'
|
|
174
|
-
export type { SeedsOf } from './state/stateGroup'
|
|
175
161
|
|
|
176
|
-
|
|
177
|
-
export { Bus, busLayer, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
|
|
178
|
-
// The renderer-neutral scene runtime: hosts layer their lifecycle on top;
|
|
179
|
-
// headless tests/proofs/drive scripts consume it directly.
|
|
162
|
+
export { type Priority, publish } from './runtime/bus'
|
|
180
163
|
export {
|
|
181
164
|
type AppRuntime,
|
|
182
165
|
type FeatureMountHandlers,
|
|
183
166
|
makeAppRuntime,
|
|
167
|
+
type RuntimeFeatureAvailability,
|
|
184
168
|
type RuntimeHandle,
|
|
185
169
|
} from './runtime/appRuntime'
|
|
186
|
-
export { Engine
|
|
187
|
-
|
|
188
|
-
// the FiberRef seam `@playfast/reform-profiler` writes through (`profiledScene`).
|
|
189
|
-
export {
|
|
190
|
-
CurrentInstrumentation,
|
|
191
|
-
type Instrumentation,
|
|
192
|
-
noopInstrumentation,
|
|
193
|
-
resolveInstrumentation,
|
|
194
|
-
type SpanEnd,
|
|
195
|
-
} from './runtime/instrumentation'
|
|
196
|
-
// The runtime-wide registry of live query handles. `AsyncCalc.invalidate` /
|
|
197
|
-
// `AsyncCalc.refetch` and provider layers act on a query by name through it.
|
|
198
|
-
export { Queries, queriesLayer, type QueryHandle, type QueryRegistry } from './runtime/queries'
|
|
199
|
-
export {
|
|
200
|
-
type ChannelClass,
|
|
201
|
-
type ChannelPolicy,
|
|
202
|
-
type ChannelRuntime,
|
|
203
|
-
Channels,
|
|
204
|
-
channelsLayer,
|
|
205
|
-
type ProcedureEntry,
|
|
206
|
-
Procedures,
|
|
207
|
-
proceduresLayer,
|
|
208
|
-
} from './channel/channel'
|
|
209
|
-
export { CurrentTracker, type Subscribable, type Tracker } from './internal/track'
|
|
210
|
-
export { CaptureSink, type CaptureSinkApi, type UiCapture } from './internal/capture'
|
|
170
|
+
export { Engine } from './runtime/loop'
|
|
171
|
+
export type { EventBudget, ReformOptions } from './runtime/eventBudget'
|
|
211
172
|
export {
|
|
212
173
|
AsyncReducer,
|
|
213
174
|
AsyncSceneLayer,
|
|
214
175
|
DuplicateRegistration,
|
|
176
|
+
EventLoopOverflow,
|
|
215
177
|
FeatureLoadFailed,
|
|
216
178
|
forceSync,
|
|
217
179
|
InvalidProvideTarget,
|
|
218
180
|
UnknownGroupState,
|
|
219
181
|
} from './internal/errors'
|
|
220
|
-
// Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
|
|
221
|
-
// multiple mounted roots) provide `Notifications` upstream of their state layers;
|
|
222
|
-
// everything else coalesces on the process-wide default.
|
|
223
|
-
export {
|
|
224
|
-
defaultScheduler,
|
|
225
|
-
makeScheduler,
|
|
226
|
-
Notifications,
|
|
227
|
-
notificationsLayer,
|
|
228
|
-
resolveScheduler,
|
|
229
|
-
type Scheduler,
|
|
230
|
-
} from './internal/scheduler'
|
|
231
|
-
export { makeStore, type Store } from './internal/store'
|
|
232
|
-
export type { FamilyStore } from './state/stateFamily'
|
|
233
|
-
// Primitive-authoring toolkit — exported for first-party companion packages
|
|
234
|
-
// (e.g. `@playfast/reform-db`, `@playfast/reform-resource`) that build their own
|
|
235
|
-
// first-class `Source`s which must participate in dependency tracking and
|
|
236
|
-
// reflection exactly like a `Calc`.
|
|
237
|
-
export { type Kind, type Manifest, yieldableClass } from './definition/definition'
|
|
238
|
-
export { readTracked } from './internal/track'
|
|
239
|
-
export { sameKey, wireSources, type WiredSources } from './internal/sources'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Option } from 'effect'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A read-through snapshot cache over a tag-indexed `Map<string, Set<T>>`.
|
|
5
|
+
*
|
|
6
|
+
* The registries keep mutable `Set` buckets so register/unregister stay O(1),
|
|
7
|
+
* but the dispatch loop must iterate a *snapshot*: an entry registered while a
|
|
8
|
+
* frame is folding must not join the fold already in flight (`Set` iteration is
|
|
9
|
+
* live, an array copy is not). Copying per event would pay that snapshot on
|
|
10
|
+
* every dispatch, so the copy is cached and dropped only when the bucket
|
|
11
|
+
* actually changes — registration is rare, dispatch is not.
|
|
12
|
+
*
|
|
13
|
+
* A tag with no bucket returns a shared empty array and is never cached, so
|
|
14
|
+
* events nothing handles allocate nothing and cannot grow the cache.
|
|
15
|
+
*/
|
|
16
|
+
export interface BucketCache<T> {
|
|
17
|
+
readonly read: (tag: string) => ReadonlyArray<T>
|
|
18
|
+
readonly invalidate: (tag: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const makeBucketCache = <T>(source: Map<string, Set<T>>): BucketCache<T> => {
|
|
22
|
+
const empty: ReadonlyArray<T> = []
|
|
23
|
+
const cache = new Map<string, ReadonlyArray<T>>()
|
|
24
|
+
const build = (tag: string): ReadonlyArray<T> =>
|
|
25
|
+
Option.match(Option.fromNullable(source.get(tag)), {
|
|
26
|
+
onNone: (): ReadonlyArray<T> => empty,
|
|
27
|
+
onSome: (bucket) => {
|
|
28
|
+
const current: ReadonlyArray<T> = Array.from(bucket)
|
|
29
|
+
cache.set(tag, current)
|
|
30
|
+
return current
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
return {
|
|
34
|
+
read: (tag) => Option.getOrElse(Option.fromNullable(cache.get(tag)), () => build(tag)),
|
|
35
|
+
invalidate: (tag) => {
|
|
36
|
+
cache.delete(tag)
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/internal/capture.ts
CHANGED
|
@@ -1,39 +1,24 @@
|
|
|
1
1
|
import { Context } from 'effect'
|
|
2
2
|
import type { Trigger } from '../ui/trigger'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* A single rendered UI node's observable surface: which contract rendered, the
|
|
6
|
-
* props it received, and the event triggers it was handed. This is exactly what
|
|
7
|
-
* a headless inspector (the proof facade, the dev tool) needs — no DOM, no JSX.
|
|
8
|
-
*/
|
|
9
|
-
export interface UiCapture {
|
|
4
|
+
export interface CapturedRender {
|
|
10
5
|
readonly name: string
|
|
11
6
|
readonly props: unknown
|
|
12
7
|
readonly events: Record<string, Trigger<unknown>>
|
|
13
|
-
/**
|
|
14
|
-
* The stable slot-fill key this render was mounted under (an `each` item's
|
|
15
|
-
* `key`, or a synthesized singleton key for a `one`). Present only on the
|
|
16
|
-
* structure path — the legacy Node path defers slot expansion to host
|
|
17
|
-
* components and has no fill key, so it is omitted there. Lets the proof
|
|
18
|
-
* facade select a child by key (`byKey`) instead of by render index.
|
|
19
|
-
*/
|
|
20
8
|
// 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
|
|
21
9
|
readonly key?: string
|
|
22
10
|
}
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
export type UiCapture = CapturedRender
|
|
13
|
+
|
|
25
14
|
export interface CaptureSinkApi {
|
|
26
|
-
record(capture:
|
|
15
|
+
record(capture: CapturedRender): void
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* recorded and there is no cost beyond one `serviceOption` lookup per render.
|
|
35
|
-
*/
|
|
36
|
-
const CaptureSinkBase: Context.TagClass<CaptureSink, 'reform/internal/CaptureSink', CaptureSinkApi> =
|
|
37
|
-
Context.Tag('reform/internal/CaptureSink')<CaptureSink, CaptureSinkApi>()
|
|
18
|
+
const CaptureSinkBase: Context.TagClass<
|
|
19
|
+
CaptureSink,
|
|
20
|
+
'reform/internal/CaptureSink',
|
|
21
|
+
CaptureSinkApi
|
|
22
|
+
> = Context.Tag('reform/internal/CaptureSink')<CaptureSink, CaptureSinkApi>()
|
|
38
23
|
|
|
39
24
|
export class CaptureSink extends CaptureSinkBase {}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare const process: { readonly env: { readonly NODE_ENV: string | undefined } } | undefined
|
|
2
|
+
|
|
3
|
+
export const isDev: boolean =
|
|
4
|
+
typeof process === 'undefined' ||
|
|
5
|
+
typeof process.env === 'undefined' ||
|
|
6
|
+
// oxlint-disable-next-line reform-rules/no-process-env -- must stay a static member expression so bundlers substitute it and DCE dev-only branches; Config resolves at runtime and cannot
|
|
7
|
+
process.env.NODE_ENV !== 'production'
|
|
@@ -2,15 +2,9 @@ import { expect, it } from '@effect/vitest'
|
|
|
2
2
|
import { Context, Effect, Layer, ManagedRuntime } from 'effect'
|
|
3
3
|
import { AsyncSceneLayer, forceSync } from '../index'
|
|
4
4
|
|
|
5
|
-
// `forceSync` is the sync-build boundary: reform materialises a scene's layer graph
|
|
6
|
-
// with `runSync` so the first frame renders synchronously (D2). A layer whose acquire
|
|
7
|
-
// suspends makes that `runSync` throw Effect's opaque `AsyncFiberException`; the
|
|
8
|
-
// boundary translates it into `AsyncSceneLayer`, which names the cause and the fix.
|
|
9
|
-
|
|
10
5
|
class Thing extends Context.Tag('test/Thing')<Thing, number>() {}
|
|
11
6
|
|
|
12
7
|
it('translates an async layer build into AsyncSceneLayer', () => {
|
|
13
|
-
// A layer whose acquire suspends — exactly what a consumer trips on.
|
|
14
8
|
const asyncLayer = Layer.effect(Thing, Effect.as(Effect.sleep('1 millis'), 1))
|
|
15
9
|
const runtime = ManagedRuntime.make(asyncLayer)
|
|
16
10
|
expect(() => forceSync(() => runtime.runSync(Effect.context<Thing>()))).toThrow(AsyncSceneLayer)
|
package/src/internal/errors.ts
CHANGED
|
@@ -1,80 +1,58 @@
|
|
|
1
1
|
import { Cause, Data, Option, Runtime } from 'effect'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Tagged errors for the framework's synchronous boundaries. Even when thrown
|
|
5
|
-
* (React render, layer build, reflection), an error carries a `_tag` and its
|
|
6
|
-
* structured fields — never a bare `Error` — so callers and tooling can match on
|
|
7
|
-
* it. `Data.TaggedError` subclasses are real `Error`s, so `throw` still works.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The constructor shape `Data.TaggedError(tag)<A>` produces, named so the
|
|
12
|
-
* generated `.d.ts` can describe the `extends` base under `isolatedDeclarations`
|
|
13
|
-
* (which forbids an inferred expression in an extends clause).
|
|
14
|
-
*/
|
|
15
3
|
type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = new (
|
|
16
4
|
args: A,
|
|
17
5
|
) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A>
|
|
18
6
|
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
type EmptyTaggedErrorClass<Tag extends string> = new () => Cause.YieldableError & {
|
|
8
|
+
readonly _tag: Tag
|
|
9
|
+
}
|
|
21
10
|
|
|
22
11
|
const InvalidProvideTargetBase: EmptyTaggedErrorClass<'reform/InvalidProvideTarget'> =
|
|
23
12
|
Data.TaggedError('reform/InvalidProvideTarget')<{}>
|
|
24
13
|
|
|
25
|
-
/** `provide(target, …)` was handed something that is neither a ui contract nor a slot. */
|
|
26
14
|
export class InvalidProvideTarget extends InvalidProvideTargetBase {
|
|
27
15
|
override get message(): string {
|
|
28
16
|
return 'reform: provide target is neither a ui contract nor a slot'
|
|
29
17
|
}
|
|
30
18
|
}
|
|
31
19
|
|
|
32
|
-
const UnknownGroupStateBase: TaggedErrorClass<
|
|
33
|
-
|
|
20
|
+
const UnknownGroupStateBase: TaggedErrorClass<
|
|
21
|
+
'reform/UnknownGroupState',
|
|
22
|
+
{ readonly name: string }
|
|
23
|
+
> = Data.TaggedError('reform/UnknownGroupState')<{ readonly name: string }>
|
|
34
24
|
|
|
35
|
-
/** `StateGroup.select(group, name)` named a state the group does not contain. */
|
|
36
25
|
export class UnknownGroupState extends UnknownGroupStateBase {
|
|
37
26
|
override get message(): string {
|
|
38
27
|
return `reform: unknown state '${this.name}' in group`
|
|
39
28
|
}
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
const FeatureLoadFailedBase: TaggedErrorClass<
|
|
43
|
-
|
|
31
|
+
const FeatureLoadFailedBase: TaggedErrorClass<
|
|
32
|
+
'reform/FeatureLoadFailed',
|
|
33
|
+
{ readonly cause: unknown }
|
|
34
|
+
> = Data.TaggedError('reform/FeatureLoadFailed')<{ readonly cause: unknown }>
|
|
44
35
|
|
|
45
|
-
/**
|
|
46
|
-
* A lazy `Feature`'s `load` Effect failed — the dynamic `import()` rejected (a
|
|
47
|
-
* missing chunk, a network failure, a module-eval throw). Surfaced as a typed
|
|
48
|
-
* failure the host hands to the feature's `placeholder.failed`, never a bare
|
|
49
|
-
* rejection. `cause` is the original rejection value.
|
|
50
|
-
*/
|
|
51
36
|
export class FeatureLoadFailed extends FeatureLoadFailedBase {
|
|
52
37
|
override get message(): string {
|
|
53
38
|
return `reform: a feature failed to load (${String(this.cause)})`
|
|
54
39
|
}
|
|
55
40
|
}
|
|
56
41
|
|
|
57
|
-
const AsyncReducerBase: EmptyTaggedErrorClass<'reform/AsyncReducer'> =
|
|
58
|
-
|
|
42
|
+
const AsyncReducerBase: EmptyTaggedErrorClass<'reform/AsyncReducer'> = Data.TaggedError(
|
|
43
|
+
'reform/AsyncReducer',
|
|
44
|
+
)<{}>
|
|
59
45
|
|
|
60
|
-
/** A reducer fold returned a thenable — folds must be pure synchronous writes. */
|
|
61
46
|
export class AsyncReducer extends AsyncReducerBase {
|
|
62
47
|
override get message(): string {
|
|
63
48
|
return 'reform: a reducer fold must be synchronous (it returned a Promise)'
|
|
64
49
|
}
|
|
65
50
|
}
|
|
66
51
|
|
|
67
|
-
const AsyncSceneLayerBase: EmptyTaggedErrorClass<'reform/AsyncSceneLayer'> =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* A scene layer performed async work during reform's synchronous layer build.
|
|
72
|
-
* Reform materialises a scene's layer graph with `runSync` so the first frame
|
|
73
|
-
* renders synchronously (D2); a `Layer.effect`/`scoped` whose acquire suspends
|
|
74
|
-
* (HTTP, opening a DB, `Effect.sleep`) makes that `runSync` throw Effect's opaque
|
|
75
|
-
* `AsyncFiberException`. `forceSync` catches it and rethrows this, which names the
|
|
76
|
-
* cause and the sanctioned fix instead of pointing at Effect internals.
|
|
77
|
-
*/
|
|
52
|
+
const AsyncSceneLayerBase: EmptyTaggedErrorClass<'reform/AsyncSceneLayer'> = Data.TaggedError(
|
|
53
|
+
'reform/AsyncSceneLayer',
|
|
54
|
+
)<{}>
|
|
55
|
+
|
|
78
56
|
export class AsyncSceneLayer extends AsyncSceneLayerBase {
|
|
79
57
|
override get message(): string {
|
|
80
58
|
return (
|
|
@@ -87,12 +65,6 @@ export class AsyncSceneLayer extends AsyncSceneLayerBase {
|
|
|
87
65
|
}
|
|
88
66
|
}
|
|
89
67
|
|
|
90
|
-
/**
|
|
91
|
-
* Whether a thrown value is Effect's "ran async under runSync" defect. `runSync`
|
|
92
|
-
* throws the bare `AsyncFiberException`; a `ManagedRuntime.runSync` whose layer
|
|
93
|
-
* build suspends instead throws a `FiberFailure` carrying it as a `Die`, so both
|
|
94
|
-
* shapes are checked.
|
|
95
|
-
*/
|
|
96
68
|
const isAsyncBuildDefect = (error: unknown): boolean => {
|
|
97
69
|
if (Runtime.isAsyncFiberException(error)) {
|
|
98
70
|
return true
|
|
@@ -104,12 +76,6 @@ const isAsyncBuildDefect = (error: unknown): boolean => {
|
|
|
104
76
|
return Option.isSome(die) && Runtime.isAsyncFiberException(die.value)
|
|
105
77
|
}
|
|
106
78
|
|
|
107
|
-
/**
|
|
108
|
-
* Run a synchronous runtime read (the scene layer build / context capture) and
|
|
109
|
-
* translate Effect's async-fiber defect into `AsyncSceneLayer`; any other throw
|
|
110
|
-
* passes through unchanged. The single home for the sync-build boundary, shared by
|
|
111
|
-
* the React host and the remote server.
|
|
112
|
-
*/
|
|
113
79
|
export const forceSync = <A>(thunk: () => A): A => {
|
|
114
80
|
// 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
|
|
115
81
|
try {
|
|
@@ -123,16 +89,27 @@ export const forceSync = <A>(thunk: () => A): A => {
|
|
|
123
89
|
const DuplicateRegistrationBase: TaggedErrorClass<
|
|
124
90
|
'reform/DuplicateRegistration',
|
|
125
91
|
{ readonly kind: string; readonly name: string }
|
|
126
|
-
> = Data.TaggedError('reform/DuplicateRegistration')<{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
* channel name reused with a different policy (the second would otherwise be
|
|
132
|
-
* dropped). Reducer/procedure name clashes are warned (logged), not thrown.
|
|
133
|
-
*/
|
|
92
|
+
> = Data.TaggedError('reform/DuplicateRegistration')<{
|
|
93
|
+
readonly kind: string
|
|
94
|
+
readonly name: string
|
|
95
|
+
}>
|
|
96
|
+
|
|
134
97
|
export class DuplicateRegistration extends DuplicateRegistrationBase {
|
|
135
98
|
override get message(): string {
|
|
136
99
|
return `reform: duplicate ${this.kind} registered under name '${this.name}'`
|
|
137
100
|
}
|
|
138
101
|
}
|
|
102
|
+
|
|
103
|
+
const EventLoopOverflowBase: TaggedErrorClass<
|
|
104
|
+
'reform/EventLoopOverflow',
|
|
105
|
+
{ readonly count: number; readonly limit: number }
|
|
106
|
+
> = Data.TaggedError('reform/EventLoopOverflow')<{ readonly count: number; readonly limit: number }>
|
|
107
|
+
|
|
108
|
+
export class EventLoopOverflow extends EventLoopOverflowBase {
|
|
109
|
+
override get message(): string {
|
|
110
|
+
return (
|
|
111
|
+
`reform: ${this.count} events processed without yielding to the event loop ` +
|
|
112
|
+
`(limit ${this.limit}) — likely an infinite event feedback loop; the runtime has been halted`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -3,24 +3,18 @@ import { Schema as S } from 'effect'
|
|
|
3
3
|
import { State } from '../index'
|
|
4
4
|
import { makeStore } from './store'
|
|
5
5
|
|
|
6
|
-
// Every reform value should print like an Effect value (an `_id`-tagged JSON),
|
|
7
|
-
// not `[object Object]` — via `toJSON`/`toString`/the Node inspect symbol.
|
|
8
|
-
|
|
9
6
|
describe('Inspectable', () => {
|
|
10
7
|
it('a Store reports its current value as tagged JSON', () => {
|
|
11
8
|
const store = makeStore(41)
|
|
12
9
|
expect(store.toJSON()).toEqual({ _id: 'reform/Store', value: 41 })
|
|
13
10
|
store.set(42)
|
|
14
11
|
expect(store.toJSON()).toEqual({ _id: 'reform/Store', value: 42 })
|
|
15
|
-
// toString and JSON.stringify route through the same toJSON.
|
|
16
12
|
expect(JSON.parse(JSON.stringify(store))).toEqual({ _id: 'reform/Store', value: 42 })
|
|
17
13
|
expect(store.toString()).toContain('reform/Store')
|
|
18
14
|
})
|
|
19
15
|
|
|
20
16
|
it('a definition reports its manifest', () => {
|
|
21
17
|
class Count extends State.make('count', S.Number) {}
|
|
22
|
-
// Inspectable is attached at runtime (definitions are reflected via their
|
|
23
|
-
// manifest); reach the method through a typed view.
|
|
24
18
|
const inspectable = Count as unknown as { toJSON(): unknown }
|
|
25
19
|
expect(inspectable.toJSON()).toBe(Count.manifest)
|
|
26
20
|
expect(String(Count)).toContain('"kind": "State"')
|
package/src/internal/inspect.ts
CHANGED
|
@@ -2,24 +2,12 @@ import { format, type Inspectable, NodeInspectSymbol } from 'effect/Inspectable'
|
|
|
2
2
|
|
|
3
3
|
export { type Inspectable, NodeInspectSymbol }
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* The `Inspectable` trio built around a `toJSON`, so every reform value prints
|
|
7
|
-
* like an Effect value — `_id`-tagged JSON under `console.log`, Node's
|
|
8
|
-
* inspector, `JSON.stringify`, and `String(...)` — instead of `[object Object]`.
|
|
9
|
-
* Mirrors Effect's `BaseProto` / `Inspectable.Class` (`Inspectable.ts:170`).
|
|
10
|
-
*/
|
|
11
5
|
export const inspectable = (toJSON: () => unknown): Inspectable => ({
|
|
12
6
|
toJSON,
|
|
13
7
|
toString: () => format(toJSON()),
|
|
14
8
|
[NodeInspectSymbol]: () => toJSON(),
|
|
15
9
|
})
|
|
16
10
|
|
|
17
|
-
/**
|
|
18
|
-
* Attach the `Inspectable` trio to an existing object as *non-enumerable* own
|
|
19
|
-
* properties (so they don't show up in `Object.keys`/`for…in` over a definition
|
|
20
|
-
* class). Used where the value is built by other means — the definition classes,
|
|
21
|
-
* whose statics are merged in separately.
|
|
22
|
-
*/
|
|
23
11
|
export const attachInspectable = <T extends object>(target: T, toJSON: () => unknown): T => {
|
|
24
12
|
const trio = inspectable(toJSON)
|
|
25
13
|
Reflect.ownKeys(trio).forEach((key) => {
|