@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.
Files changed (99) hide show
  1. package/README.md +103 -56
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +63 -49
  4. package/src/boundary/boundary.ts +144 -113
  5. package/src/calc/asyncCalc.invalidate.test.ts +518 -32
  6. package/src/calc/asyncCalc.test.ts +207 -213
  7. package/src/calc/asyncCalc.ts +131 -154
  8. package/src/calc/asyncData.ts +0 -37
  9. package/src/calc/calc.test.ts +7 -33
  10. package/src/calc/calc.ts +44 -58
  11. package/src/calc/calcFamily.test.ts +80 -34
  12. package/src/calc/calcFamily.ts +54 -65
  13. package/src/calc/compose.test.ts +1 -12
  14. package/src/calc/compose.ts +1 -36
  15. package/src/calc/queryState.ts +0 -23
  16. package/src/channel/channel.ts +126 -111
  17. package/src/compose/composition.test.ts +19 -0
  18. package/src/compose/composition.ts +351 -127
  19. package/src/compose/host.ts +0 -6
  20. package/src/compose/props.ts +13 -12
  21. package/src/compose/provide.ts +189 -62
  22. package/src/compose/slot.ts +158 -49
  23. package/src/compose/structure.test.ts +6 -9
  24. package/src/compose/structure.ts +108 -79
  25. package/src/compose/ui.test.ts +19 -7
  26. package/src/compose/ui.ts +155 -156
  27. package/src/compose/ui.typecheck.ts +40 -18
  28. package/src/definition/definition.ts +22 -41
  29. package/src/event/event.fromSource.test.ts +172 -0
  30. package/src/event/event.test.ts +10 -3
  31. package/src/event/event.ts +102 -27
  32. package/src/event/eventGroup.ts +0 -1
  33. package/src/feature/feature.mount.test.ts +122 -43
  34. package/src/feature/feature.test.ts +46 -21
  35. package/src/feature/feature.ts +1347 -256
  36. package/src/feature/feature.typecheck.ts +273 -68
  37. package/src/graph/closure.ts +403 -0
  38. package/src/index.ts +68 -126
  39. package/src/internal/bucketCache.ts +39 -0
  40. package/src/internal/capture.ts +9 -24
  41. package/src/internal/env.ts +7 -0
  42. package/src/internal/errors.test.ts +0 -6
  43. package/src/internal/errors.ts +37 -60
  44. package/src/internal/inspect.test.ts +0 -6
  45. package/src/internal/inspect.ts +0 -12
  46. package/src/internal/queryDriver.ts +105 -201
  47. package/src/internal/queryDriverStore.ts +156 -0
  48. package/src/internal/queryDriverTypes.ts +59 -0
  49. package/src/internal/queryEvents.ts +0 -12
  50. package/src/internal/queryStore.ts +0 -14
  51. package/src/internal/reuse.test.ts +10 -14
  52. package/src/internal/reuse.ts +5 -30
  53. package/src/internal/scheduler.ts +4 -44
  54. package/src/internal/seeds.ts +0 -14
  55. package/src/internal/sources.ts +26 -49
  56. package/src/internal/stateRegistry.ts +0 -14
  57. package/src/internal/store.test.ts +1 -3
  58. package/src/internal/store.ts +0 -37
  59. package/src/internal/track.ts +3 -20
  60. package/src/internal/variance.ts +5 -0
  61. package/src/internal.ts +222 -0
  62. package/src/namespace/namespace.test.ts +46 -0
  63. package/src/namespace/namespace.ts +106 -0
  64. package/src/procedure/procedure.ts +40 -35
  65. package/src/reducer/reducer.ts +78 -52
  66. package/src/remote/remoteState.test.ts +590 -397
  67. package/src/remote/remoteState.ts +425 -347
  68. package/src/remote/remoteState.typecheck.ts +47 -56
  69. package/src/runtime/appRuntime.activation.test.ts +100 -0
  70. package/src/runtime/appRuntime.test.ts +249 -0
  71. package/src/runtime/appRuntime.ts +415 -97
  72. package/src/runtime/bus.ts +2 -15
  73. package/src/runtime/eventBudget.test.ts +152 -0
  74. package/src/runtime/eventBudget.ts +120 -0
  75. package/src/runtime/hardening.test.ts +73 -13
  76. package/src/runtime/instrumentation.test.ts +55 -52
  77. package/src/runtime/instrumentation.ts +6 -60
  78. package/src/runtime/loop.test.ts +36 -17
  79. package/src/runtime/loop.ts +87 -88
  80. package/src/runtime/queries.ts +0 -17
  81. package/src/scene/featureScene.test.ts +61 -0
  82. package/src/scene/scene.ts +247 -104
  83. package/src/scene/seedScene.test.ts +42 -79
  84. package/src/state/state.nominal.typecheck.ts +68 -0
  85. package/src/state/state.test.ts +17 -0
  86. package/src/state/state.ts +80 -46
  87. package/src/state/stateFamily.test.ts +16 -11
  88. package/src/state/stateFamily.ts +55 -65
  89. package/src/state/stateGroup.ts +53 -64
  90. package/src/state/token.ts +40 -23
  91. package/src/synced/syncedStore.ts +46 -58
  92. package/src/testkit/flight.testkit.ts +75 -0
  93. package/src/ui/node.ts +0 -6
  94. package/src/ui/trigger.ts +0 -5
  95. package/src/wire/tree.test.ts +8 -7
  96. package/src/wire/tree.ts +0 -39
  97. package/src/wire/triggers.test.ts +6 -6
  98. package/src/wire/triggers.ts +14 -32
  99. package/src/internal/ctx.ts +0 -16
@@ -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 ProcedureClass<Events extends ReadonlyArray<AnyEvent>> {
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<any, any, any>>,
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 | Ctx<Eff>> => {
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
- // Snapshot the body's full context (Bus + RPC clients) so `run` is total.
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) => Effect.provide(Effect.gen(() => body(narrowHandled(event))), runtime),
82
+ run: (event) =>
83
+ Effect.provide(
84
+ Effect.gen(() => body(narrowHandled(event))),
85
+ runtime,
86
+ ),
83
87
  }
84
- yield* Effect.acquireRelease(Effect.sync(() => procedures.register(entry)), () =>
85
- Effect.sync(() => procedures.unregister(entry)),
88
+ yield* Effect.acquireRelease(
89
+ Effect.sync(() => procedures.register(entry)),
90
+ () => Effect.sync(() => procedures.unregister(entry)),
86
91
  )
87
92
  }),
88
93
  )