@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/internal.ts
ADDED
|
@@ -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'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { Match, Schema as S } from 'effect'
|
|
3
|
+
import * as Event from '../event/event'
|
|
4
|
+
import * as Namespace from './namespace'
|
|
5
|
+
|
|
6
|
+
describe('Namespace.make', () => {
|
|
7
|
+
const ns = Namespace.make('demoFeature')
|
|
8
|
+
|
|
9
|
+
it('prefixes every definition kind', () => {
|
|
10
|
+
class Saved extends ns.Event('Saved', S.Struct({})) {}
|
|
11
|
+
class Lane extends ns.Channel('Lane', { policy: { _tag: 'latest' } }) {}
|
|
12
|
+
class Count extends ns.State('count', S.Number) {}
|
|
13
|
+
class Doubled extends ns.Calc('Doubled', { inputs: [Count], output: S.Number }) {}
|
|
14
|
+
class Feed extends ns.AsyncCalc('Feed', { inputs: [Count], output: S.Number }) {}
|
|
15
|
+
class Rows extends ns.RemoteState('Rows', {
|
|
16
|
+
inputs: [Count],
|
|
17
|
+
output: S.Number,
|
|
18
|
+
intents: [],
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
expect(Saved.tag).toBe('demoFeature.Saved')
|
|
22
|
+
expect(Lane.manifest.name).toBe('demoFeature.Lane')
|
|
23
|
+
expect(Count.manifest.name).toBe('demoFeature.count')
|
|
24
|
+
expect(Doubled.manifest.name).toBe('demoFeature.Doubled')
|
|
25
|
+
expect(Feed.manifest.name).toBe('demoFeature.Feed')
|
|
26
|
+
expect(Rows.manifest.name).toBe('demoFeature.Rows')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('keeps the tag literal type for Match.tags folds', () => {
|
|
30
|
+
class Opened extends ns.Event('Opened', S.Struct({})) {}
|
|
31
|
+
class Closed extends ns.Event('Closed', S.Struct({ reason: S.String })) {}
|
|
32
|
+
type Folded = Event.EventType<typeof Opened> | Event.EventType<typeof Closed>
|
|
33
|
+
|
|
34
|
+
const fold = (event: Folded): string =>
|
|
35
|
+
Match.value(event).pipe(
|
|
36
|
+
Match.tags({
|
|
37
|
+
'demoFeature.Opened': () => 'open',
|
|
38
|
+
'demoFeature.Closed': ({ reason }) => reason,
|
|
39
|
+
}),
|
|
40
|
+
Match.exhaustive,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(fold(Event.construct(Opened, {}))).toBe('open')
|
|
44
|
+
expect(fold(Event.construct(Closed, { reason: 'done' }))).toBe('done')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Schema } from 'effect'
|
|
2
|
+
import * as AsyncCalc from '../calc/asyncCalc'
|
|
3
|
+
import * as Calc from '../calc/calc'
|
|
4
|
+
import * as CalcFamily from '../calc/calcFamily'
|
|
5
|
+
import * as Channel from '../channel/channel'
|
|
6
|
+
import * as Event from '../event/event'
|
|
7
|
+
import type { GatedOf } from '../internal/queryDriver'
|
|
8
|
+
import * as RemoteState from '../remote/remoteState'
|
|
9
|
+
import * as State from '../state/state'
|
|
10
|
+
import * as StateFamily from '../state/stateFamily'
|
|
11
|
+
import type { AnySource } from '../state/token'
|
|
12
|
+
|
|
13
|
+
export interface FeatureNamespace<Prefix extends string> {
|
|
14
|
+
readonly prefix: Prefix
|
|
15
|
+
readonly Event: <const N extends string, P, Encoded>(
|
|
16
|
+
name: N,
|
|
17
|
+
schema: Schema.Schema<P, Encoded, never>,
|
|
18
|
+
) => Event.EventClass<`${Prefix}.${N}`, P>
|
|
19
|
+
readonly Channel: (name: string, config: Channel.ChannelConfig) => Channel.ChannelClass
|
|
20
|
+
readonly State: <const N extends string, StateSchema extends Schema.Schema.AnyNoContext>(
|
|
21
|
+
name: N,
|
|
22
|
+
schema: StateSchema,
|
|
23
|
+
options?: State.StateOptions,
|
|
24
|
+
) => State.StateDefinition<`${Prefix}.${N}`, Schema.Schema.Type<StateSchema>, StateSchema>
|
|
25
|
+
readonly StateFamily: <const N extends string, K, KeyEncoded, V, ValueEncoded>(
|
|
26
|
+
name: N,
|
|
27
|
+
key: Schema.Schema<K, KeyEncoded, never>,
|
|
28
|
+
valueSchema: Schema.Schema<V, ValueEncoded, never>,
|
|
29
|
+
options?: StateFamily.StateFamilyMakeOptionsExternalApi,
|
|
30
|
+
) => StateFamily.StateFamilyClass<`${Prefix}.${N}`, K, V>
|
|
31
|
+
readonly Calc: <
|
|
32
|
+
const N extends string,
|
|
33
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
34
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
35
|
+
>(
|
|
36
|
+
name: N,
|
|
37
|
+
config: Calc.CalcConfig<Inputs, OutputSchema>,
|
|
38
|
+
) => Calc.CalcClass<`${Prefix}.${N}`, Inputs, Schema.Schema.Type<OutputSchema>>
|
|
39
|
+
readonly CalcFamily: <
|
|
40
|
+
const N extends string,
|
|
41
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
42
|
+
KeySchema extends Schema.Schema.AnyNoContext,
|
|
43
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
44
|
+
>(
|
|
45
|
+
name: N,
|
|
46
|
+
config: CalcFamily.CalcFamilyConfig<Inputs, KeySchema, OutputSchema>,
|
|
47
|
+
) => CalcFamily.CalcFamilyClass<
|
|
48
|
+
`${Prefix}.${N}`,
|
|
49
|
+
Inputs,
|
|
50
|
+
Schema.Schema.Type<KeySchema>,
|
|
51
|
+
Schema.Schema.Type<OutputSchema>
|
|
52
|
+
>
|
|
53
|
+
readonly AsyncCalc: <
|
|
54
|
+
const N extends string,
|
|
55
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
56
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
57
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
58
|
+
const AlwaysOn extends boolean = false,
|
|
59
|
+
>(
|
|
60
|
+
name: N,
|
|
61
|
+
config: AsyncCalc.AsyncCalcConfig<Inputs, OutputSchema, ErrorSchema, AlwaysOn>,
|
|
62
|
+
) => AsyncCalc.AsyncCalcClass<
|
|
63
|
+
`${Prefix}.${N}`,
|
|
64
|
+
Inputs,
|
|
65
|
+
Schema.Schema.Type<OutputSchema>,
|
|
66
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
67
|
+
GatedOf<AlwaysOn>
|
|
68
|
+
>
|
|
69
|
+
readonly RemoteState: <
|
|
70
|
+
const N extends string,
|
|
71
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
72
|
+
const Intents extends ReadonlyArray<Event.AnyEvent>,
|
|
73
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
74
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
75
|
+
const AlwaysOn extends boolean = false,
|
|
76
|
+
>(
|
|
77
|
+
name: N,
|
|
78
|
+
config: RemoteState.RemoteStateConfigExternalApi<
|
|
79
|
+
Inputs,
|
|
80
|
+
Intents,
|
|
81
|
+
OutputSchema,
|
|
82
|
+
ErrorSchema,
|
|
83
|
+
AlwaysOn
|
|
84
|
+
>,
|
|
85
|
+
) => RemoteState.RemoteStateClass<
|
|
86
|
+
`${Prefix}.${N}`,
|
|
87
|
+
Inputs,
|
|
88
|
+
Intents,
|
|
89
|
+
Schema.Schema.Type<OutputSchema>,
|
|
90
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
91
|
+
GatedOf<AlwaysOn>
|
|
92
|
+
>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const make = <const Prefix extends string>(prefix: Prefix): FeatureNamespace<Prefix> => ({
|
|
96
|
+
prefix,
|
|
97
|
+
Event: (name, schema) => Event.make(`${prefix}.${name}`, schema),
|
|
98
|
+
Channel: (name, config) => Channel.make(`${prefix}.${name}`, config),
|
|
99
|
+
State: (name, schema, options) => State.make(`${prefix}.${name}`, schema, options),
|
|
100
|
+
StateFamily: (name, key, valueSchema, options) =>
|
|
101
|
+
StateFamily.make(`${prefix}.${name}`, key, valueSchema, options),
|
|
102
|
+
Calc: (name, config) => Calc.make(`${prefix}.${name}`, config),
|
|
103
|
+
CalcFamily: (name, config) => CalcFamily.make(`${prefix}.${name}`, config),
|
|
104
|
+
AsyncCalc: (name, config) => AsyncCalc.make(`${prefix}.${name}`, config),
|
|
105
|
+
RemoteState: (name, config) => RemoteState.make(`${prefix}.${name}`, config),
|
|
106
|
+
})
|
|
@@ -3,86 +3,91 @@ import type { YieldWrap } from 'effect/Utils'
|
|
|
3
3
|
import { type ChannelClass, type ProcedureEntry, Procedures } from '../channel/channel'
|
|
4
4
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
5
5
|
import { type AnyEvent, type EventType } from '../event/event'
|
|
6
|
-
import { type Ctx } from '../internal/ctx'
|
|
7
6
|
import { Bus, narrowHandled } from '../runtime/bus'
|
|
7
|
+
import type { AnyValue } from '../internal/variance'
|
|
8
8
|
|
|
9
|
-
/** The reflectable descriptor a procedure carries. */
|
|
10
9
|
export interface ProcedureManifest extends Manifest {
|
|
11
10
|
readonly kind: 'Procedure'
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
export interface
|
|
13
|
+
export interface ProcedureReflection<
|
|
14
|
+
out Events extends ReadonlyArray<AnyEvent> = ReadonlyArray<AnyEvent>,
|
|
15
|
+
> {
|
|
15
16
|
new (): {}
|
|
16
17
|
readonly manifest: ProcedureManifest
|
|
17
|
-
/** The trigger events (marker data; read by `Procedure.live`). */
|
|
18
18
|
readonly events: Events
|
|
19
|
-
/** The channel this procedure's runs are scheduled on (marker data). */
|
|
20
19
|
readonly channel: ChannelClass
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
export type ProcedureCapture<Result> = <Events extends ReadonlyArray<AnyEvent>>(
|
|
23
|
+
procedure: ProcedureClass<Events>,
|
|
24
|
+
) => Result
|
|
25
|
+
|
|
26
|
+
export interface AnyProcedure extends ProcedureReflection {
|
|
27
|
+
readonly capture: <Result>(visit: ProcedureCapture<Result>) => Result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProcedureClass<
|
|
31
|
+
Events extends ReadonlyArray<AnyEvent>,
|
|
32
|
+
> extends ProcedureReflection<Events> {
|
|
33
|
+
readonly capture: <Result>(visit: ProcedureCapture<Result>) => Result
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
export interface ProcedureConfig<Events extends ReadonlyArray<AnyEvent>> {
|
|
24
37
|
readonly events: Events
|
|
25
|
-
/** The scheduling lane. Replaces the old `concurrency` option. */
|
|
26
38
|
readonly channel: ChannelClass
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
export type AnyProcedure = ProcedureClass<ReadonlyArray<AnyEvent>>
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A cross-event-loop flow. The *definition* declares its trigger events and the
|
|
33
|
-
* channel it runs on; the *implementation* is the effectful body. Procedures no
|
|
34
|
-
* longer subscribe to the bus themselves — the single drain loop routes events
|
|
35
|
-
* to the channel, which runs the registered bodies under its policy.
|
|
36
|
-
*/
|
|
37
41
|
export const make = <const Events extends ReadonlyArray<AnyEvent>>(
|
|
38
42
|
name: string,
|
|
39
43
|
config: ProcedureConfig<Events>,
|
|
40
|
-
): ProcedureClass<Events> =>
|
|
41
|
-
definitionClass<ProcedureClass<Events>>({
|
|
44
|
+
): ProcedureClass<Events> => {
|
|
45
|
+
const procedure: ProcedureClass<Events> = definitionClass<ProcedureClass<Events>>({
|
|
42
46
|
manifest: { kind: 'Procedure' as const, name } satisfies ProcedureManifest,
|
|
43
47
|
events: config.events,
|
|
44
48
|
channel: config.channel,
|
|
49
|
+
capture: <Result>(visit: ProcedureCapture<Result>): Result => visit(procedure),
|
|
45
50
|
})
|
|
51
|
+
return procedure
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type GeneratorContext<Eff> = [Eff] extends [never]
|
|
55
|
+
? never
|
|
56
|
+
: [Eff] extends [YieldWrap<Effect.Effect<AnyValue, AnyValue, infer Context>>]
|
|
57
|
+
? Context
|
|
58
|
+
: never
|
|
46
59
|
|
|
47
|
-
/**
|
|
48
|
-
* Register the flow body into its channel. The body may read state and dispatch
|
|
49
|
-
* events, but never writes state (the core invariant); its context requirements
|
|
50
|
-
* (RPC clients, etc.) are captured here so the channel fiber can run it with no
|
|
51
|
-
* outstanding R.
|
|
52
|
-
*/
|
|
53
60
|
export const live = <
|
|
54
61
|
Events extends ReadonlyArray<AnyEvent>,
|
|
55
|
-
Eff extends YieldWrap<Effect.Effect<
|
|
62
|
+
Eff extends YieldWrap<Effect.Effect<AnyValue, AnyValue, AnyValue>>,
|
|
56
63
|
Done,
|
|
57
64
|
>(
|
|
58
65
|
procedure: ProcedureClass<Events>,
|
|
59
66
|
body: (event: EventType<Events[number]>) => Generator<Eff, Done, never>,
|
|
60
|
-
): Layer.Layer<never, never, Procedures | Bus |
|
|
67
|
+
): Layer.Layer<never, never, Procedures | Bus | GeneratorContext<Eff>> => {
|
|
61
68
|
const name = procedure.manifest.name
|
|
62
69
|
const channelName = procedure.channel.manifest.name
|
|
63
70
|
const handles = new Set(procedure.events.map((event) => event.tag))
|
|
64
|
-
// Scoped registration (see `Reducer.live`): an eager procedure's scope is the
|
|
65
|
-
// root runtime's, so behavior is unchanged; a lazy feature's procedure registers
|
|
66
|
-
// on mount and unregisters on unmount, so it stops running and is reclaimed.
|
|
67
71
|
return Layer.scopedDiscard(
|
|
68
72
|
Effect.gen(function* () {
|
|
69
73
|
const procedures = yield* Procedures
|
|
70
74
|
if (procedures.entries.some((entry) => entry.name === name)) {
|
|
71
75
|
yield* Effect.logWarning(`reform: duplicate procedure name '${name}' registered`)
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
const runtime = yield* Effect.runtime<Bus | Ctx<Eff>>()
|
|
77
|
+
const runtime = yield* Effect.runtime<Bus | GeneratorContext<Eff>>()
|
|
75
78
|
const entry: ProcedureEntry = {
|
|
76
79
|
name,
|
|
77
80
|
channelName,
|
|
78
|
-
// The channel only routes events whose tag is in `handles`, so at runtime
|
|
79
|
-
// `event` is always one of `Events`; `narrowHandled` restores the body's
|
|
80
|
-
// declared event union from the erased `Tagged` envelope.
|
|
81
81
|
handles,
|
|
82
|
-
run: (event) =>
|
|
82
|
+
run: (event) =>
|
|
83
|
+
Effect.provide(
|
|
84
|
+
Effect.gen(() => body(narrowHandled(event))),
|
|
85
|
+
runtime,
|
|
86
|
+
),
|
|
83
87
|
}
|
|
84
|
-
yield* Effect.acquireRelease(
|
|
85
|
-
Effect.sync(() => procedures.
|
|
88
|
+
yield* Effect.acquireRelease(
|
|
89
|
+
Effect.sync(() => procedures.register(entry)),
|
|
90
|
+
() => Effect.sync(() => procedures.unregister(entry)),
|
|
86
91
|
)
|
|
87
92
|
}),
|
|
88
93
|
)
|