@playfast/reform 1.1.1 → 1.2.1

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 (106) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +82 -33
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +733 -32
  6. package/src/calc/asyncCalc.test.ts +397 -225
  7. package/src/calc/asyncCalc.ts +115 -100
  8. package/src/calc/asyncCalcTypes.ts +147 -0
  9. package/src/calc/calc.test.ts +13 -16
  10. package/src/calc/calc.ts +44 -18
  11. package/src/calc/calcFamily.test.ts +90 -32
  12. package/src/calc/calcFamily.ts +56 -25
  13. package/src/calc/compose.ts +1 -14
  14. package/src/channel/channel.ts +25 -6
  15. package/src/channel/{procedureRegistry.ts → procedures.ts} +41 -23
  16. package/src/compose/composition.test.ts +19 -0
  17. package/src/compose/composition.ts +168 -81
  18. package/src/compose/compositionTypes.ts +249 -0
  19. package/src/compose/props.ts +13 -6
  20. package/src/compose/provide.ts +187 -46
  21. package/src/compose/slot.ts +156 -19
  22. package/src/compose/structure.test.ts +6 -3
  23. package/src/compose/structure.ts +106 -13
  24. package/src/compose/ui.test.ts +19 -3
  25. package/src/compose/ui.ts +147 -54
  26. package/src/compose/ui.typecheck.ts +40 -4
  27. package/src/definition/definition.ts +22 -9
  28. package/src/event/event.fromSource.test.ts +172 -0
  29. package/src/event/event.test.ts +10 -0
  30. package/src/event/event.ts +102 -18
  31. package/src/feature/feature.mount.test.ts +123 -56
  32. package/src/feature/feature.test.ts +67 -63
  33. package/src/feature/feature.ts +222 -262
  34. package/src/feature/feature.typecheck.ts +224 -91
  35. package/src/feature/featureBinding.ts +130 -20
  36. package/src/feature/featureClass.ts +162 -0
  37. package/src/feature/featureConfig.ts +192 -0
  38. package/src/feature/featureFactory.ts +163 -0
  39. package/src/feature/featureModule.ts +136 -0
  40. package/src/feature/featureModule.typecheck.ts +63 -0
  41. package/src/feature/featureNativeTree.ts +152 -0
  42. package/src/feature/featureRequirement.ts +82 -0
  43. package/src/feature/featureVariants.ts +234 -0
  44. package/src/feature/mountFeature.ts +54 -0
  45. package/src/graph/closure.ts +222 -0
  46. package/src/graph/services.ts +95 -0
  47. package/src/graph/summary.ts +134 -0
  48. package/src/index.ts +171 -245
  49. package/src/internal/bucketCache.ts +39 -0
  50. package/src/internal/capture.ts +9 -4
  51. package/src/internal/env.ts +1 -3
  52. package/src/internal/errors.ts +21 -10
  53. package/src/internal/queryDriver.ts +61 -25
  54. package/src/internal/queryDriverHydrate.ts +45 -0
  55. package/src/internal/queryDriverStore.ts +8 -5
  56. package/src/internal/queryDriverTypes.ts +11 -11
  57. package/src/internal/reuse.test.ts +10 -3
  58. package/src/internal/reuse.ts +5 -2
  59. package/src/internal/scheduler.ts +4 -1
  60. package/src/internal/sources.ts +25 -11
  61. package/src/internal/store.test.ts +27 -6
  62. package/src/internal/track.ts +3 -2
  63. package/src/internal.ts +222 -0
  64. package/src/namespace/namespace.ts +46 -25
  65. package/src/procedure/procedure.ts +38 -11
  66. package/src/reducer/reducer.ts +78 -34
  67. package/src/remote/pendingQueue.ts +145 -0
  68. package/src/remote/remoteState.test.ts +536 -354
  69. package/src/remote/remoteState.ts +145 -106
  70. package/src/remote/remoteState.typecheck.ts +47 -23
  71. package/src/remote/remoteStateMake.ts +97 -0
  72. package/src/remote/remoteStateSend.ts +95 -37
  73. package/src/remote/remoteStateTypes.ts +155 -0
  74. package/src/runtime/appRuntime.activation.test.ts +104 -0
  75. package/src/runtime/appRuntime.test.ts +187 -111
  76. package/src/runtime/appRuntime.ts +36 -130
  77. package/src/runtime/capturedAppRuntime.ts +274 -0
  78. package/src/runtime/eventBudget.test.ts +39 -12
  79. package/src/runtime/eventBudget.ts +9 -8
  80. package/src/runtime/featureMount.ts +94 -0
  81. package/src/runtime/hardening.test.ts +22 -13
  82. package/src/runtime/instrumentation.test.ts +213 -194
  83. package/src/runtime/instrumentation.ts +6 -11
  84. package/src/runtime/loop.test.ts +84 -7
  85. package/src/runtime/loop.ts +69 -40
  86. package/src/runtime/runtimeHandle.ts +124 -0
  87. package/src/scene/featureScene.test.ts +4 -2
  88. package/src/scene/scene.ts +234 -64
  89. package/src/scene/seedScene.test.ts +69 -86
  90. package/src/state/state.nominal.typecheck.ts +68 -0
  91. package/src/state/state.test.ts +17 -0
  92. package/src/state/state.ts +79 -26
  93. package/src/state/stateFamily.test.ts +39 -8
  94. package/src/state/stateFamily.ts +55 -22
  95. package/src/state/stateGroup.ts +53 -17
  96. package/src/state/token.ts +40 -10
  97. package/src/synced/syncedStore.ts +46 -23
  98. package/src/testkit/flight.testkit.ts +84 -0
  99. package/src/wire/tree.test.ts +8 -4
  100. package/src/wire/triggers.test.ts +6 -2
  101. package/src/wire/triggers.ts +14 -10
  102. package/src/calc/asyncCalcDefinitions.ts +0 -48
  103. package/src/internal/ctx.ts +0 -9
  104. package/src/remote/remoteStateDefinition.ts +0 -135
  105. package/src/remote/remoteStateLayers.ts +0 -118
  106. package/src/remote/remoteStateLiveTypes.ts +0 -59
@@ -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'
@@ -1,3 +1,4 @@
1
+ import type { Schema } from 'effect'
1
2
  import * as AsyncCalc from '../calc/asyncCalc'
2
3
  import * as Calc from '../calc/calc'
3
4
  import * as CalcFamily from '../calc/calcFamily'
@@ -8,65 +9,85 @@ import * as RemoteState from '../remote/remoteState'
8
9
  import * as State from '../state/state'
9
10
  import * as StateFamily from '../state/stateFamily'
10
11
  import type { AnySource } from '../state/token'
11
- import type { AnySchema } from '../internal/variance'
12
12
 
13
13
  export interface FeatureNamespace<Prefix extends string> {
14
14
  readonly prefix: Prefix
15
- readonly Event: <const N extends string, P>(
15
+ readonly Event: <const N extends string, P, Encoded>(
16
16
  name: N,
17
- schema: AnySchema<P>,
17
+ schema: Schema.Schema<P, Encoded, never>,
18
18
  ) => Event.EventClass<`${Prefix}.${N}`, P>
19
19
  readonly Channel: (name: string, config: Channel.ChannelConfig) => Channel.ChannelClass
20
- readonly State: <const N extends string, A>(
20
+ readonly State: <const N extends string, StateSchema extends Schema.Schema.AnyNoContext>(
21
21
  name: N,
22
- schema: AnySchema<A>,
22
+ schema: StateSchema,
23
23
  options?: State.StateOptions,
24
- ) => State.StateClass<`${Prefix}.${N}`, A>
25
- readonly StateFamily: <const N extends string, K, V>(
24
+ ) => State.StateDefinition<`${Prefix}.${N}`, Schema.Schema.Type<StateSchema>, StateSchema>
25
+ readonly StateFamily: <const N extends string, K, KeyEncoded, V, ValueEncoded>(
26
26
  name: N,
27
- key: AnySchema<K>,
28
- valueSchema: AnySchema<V>,
27
+ key: Schema.Schema<K, KeyEncoded, never>,
28
+ valueSchema: Schema.Schema<V, ValueEncoded, never>,
29
29
  options?: StateFamily.StateFamilyMakeOptionsExternalApi,
30
30
  ) => StateFamily.StateFamilyClass<`${Prefix}.${N}`, K, V>
31
- readonly Calc: <const N extends string, const Inputs extends ReadonlyArray<AnySource>, Out>(
31
+ readonly Calc: <
32
+ const N extends string,
33
+ const Inputs extends ReadonlyArray<AnySource>,
34
+ OutputSchema extends Schema.Schema.AnyNoContext,
35
+ >(
32
36
  name: N,
33
- config: Calc.CalcConfig<Inputs, Out>,
34
- ) => Calc.CalcClass<`${Prefix}.${N}`, Inputs, Out>
37
+ config: Calc.CalcConfig<Inputs, OutputSchema>,
38
+ ) => Calc.CalcClass<`${Prefix}.${N}`, Inputs, Schema.Schema.Type<OutputSchema>>
35
39
  readonly CalcFamily: <
36
40
  const N extends string,
37
41
  const Inputs extends ReadonlyArray<AnySource>,
38
- K,
39
- Out,
42
+ KeySchema extends Schema.Schema.AnyNoContext,
43
+ OutputSchema extends Schema.Schema.AnyNoContext,
40
44
  >(
41
45
  name: N,
42
- config: CalcFamily.CalcFamilyConfig<Inputs, K, Out>,
43
- ) => CalcFamily.CalcFamilyClass<`${Prefix}.${N}`, Inputs, K, Out>
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
+ >
44
53
  readonly AsyncCalc: <
45
54
  const N extends string,
46
55
  const Inputs extends ReadonlyArray<AnySource>,
47
- A,
48
- E = never,
56
+ OutputSchema extends Schema.Schema.AnyNoContext,
57
+ ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
49
58
  const AlwaysOn extends boolean = false,
50
59
  >(
51
60
  name: N,
52
- config: AsyncCalc.AsyncCalcConfig<Inputs, A, E, AlwaysOn>,
53
- ) => AsyncCalc.AsyncCalcClass<`${Prefix}.${N}`, Inputs, A, E, GatedOf<AlwaysOn>>
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
+ >
54
69
  readonly RemoteState: <
55
70
  const N extends string,
56
71
  const Inputs extends ReadonlyArray<AnySource>,
57
72
  const Intents extends ReadonlyArray<Event.AnyEvent>,
58
- A,
59
- E = never,
73
+ OutputSchema extends Schema.Schema.AnyNoContext,
74
+ ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
60
75
  const AlwaysOn extends boolean = false,
61
76
  >(
62
77
  name: N,
63
- config: RemoteState.RemoteStateConfigExternalApi<Inputs, Intents, A, E, AlwaysOn>,
78
+ config: RemoteState.RemoteStateConfigExternalApi<
79
+ Inputs,
80
+ Intents,
81
+ OutputSchema,
82
+ ErrorSchema,
83
+ AlwaysOn
84
+ >,
64
85
  ) => RemoteState.RemoteStateClass<
65
86
  `${Prefix}.${N}`,
66
87
  Inputs,
67
88
  Intents,
68
- A,
69
- E,
89
+ Schema.Schema.Type<OutputSchema>,
90
+ ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
70
91
  GatedOf<AlwaysOn>
71
92
  >
72
93
  }
@@ -3,7 +3,6 @@ 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'
8
7
  import type { AnyValue } from '../internal/variance'
9
8
 
@@ -11,29 +10,52 @@ 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
18
  readonly events: Events
18
19
  readonly channel: ChannelClass
19
20
  }
20
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
+
21
36
  export interface ProcedureConfig<Events extends ReadonlyArray<AnyEvent>> {
22
37
  readonly events: Events
23
38
  readonly channel: ChannelClass
24
39
  }
25
40
 
26
- export type AnyProcedure = ProcedureClass<ReadonlyArray<AnyEvent>>
27
-
28
41
  export const make = <const Events extends ReadonlyArray<AnyEvent>>(
29
42
  name: string,
30
43
  config: ProcedureConfig<Events>,
31
- ): ProcedureClass<Events> =>
32
- definitionClass<ProcedureClass<Events>>({
44
+ ): ProcedureClass<Events> => {
45
+ const procedure: ProcedureClass<Events> = definitionClass<ProcedureClass<Events>>({
33
46
  manifest: { kind: 'Procedure' as const, name } satisfies ProcedureManifest,
34
47
  events: config.events,
35
48
  channel: config.channel,
49
+ capture: <Result>(visit: ProcedureCapture<Result>): Result => visit(procedure),
36
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
37
59
 
38
60
  export const live = <
39
61
  Events extends ReadonlyArray<AnyEvent>,
@@ -42,7 +64,7 @@ export const live = <
42
64
  >(
43
65
  procedure: ProcedureClass<Events>,
44
66
  body: (event: EventType<Events[number]>) => Generator<Eff, Done, never>,
45
- ): Layer.Layer<never, never, Procedures | Bus | Ctx<Eff>> => {
67
+ ): Layer.Layer<never, never, Procedures | Bus | GeneratorContext<Eff>> => {
46
68
  const name = procedure.manifest.name
47
69
  const channelName = procedure.channel.manifest.name
48
70
  const handles = new Set(procedure.events.map((event) => event.tag))
@@ -52,15 +74,20 @@ export const live = <
52
74
  if (procedures.entries.some((entry) => entry.name === name)) {
53
75
  yield* Effect.logWarning(`reform: duplicate procedure name '${name}' registered`)
54
76
  }
55
- const runtime = yield* Effect.runtime<Bus | Ctx<Eff>>()
77
+ const runtime = yield* Effect.runtime<Bus | GeneratorContext<Eff>>()
56
78
  const entry: ProcedureEntry = {
57
79
  name,
58
80
  channelName,
59
81
  handles,
60
- 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
+ ),
61
87
  }
62
- yield* Effect.acquireRelease(Effect.sync(() => procedures.register(entry)), () =>
63
- Effect.sync(() => procedures.unregister(entry)),
88
+ yield* Effect.acquireRelease(
89
+ Effect.sync(() => procedures.register(entry)),
90
+ () => Effect.sync(() => procedures.unregister(entry)),
64
91
  )
65
92
  }),
66
93
  )
@@ -2,18 +2,17 @@ import { Effect, Layer, Predicate } from 'effect'
2
2
  import { type Manifest, definitionClass } from '../definition/definition'
3
3
  import { AsyncReducer } from '../internal/errors'
4
4
  import { type AnyEvent, type EventType } from '../event/event'
5
- import type { Store } from '../internal/store'
6
- import { type AnyState, type StateValue } from '../state/state'
5
+ import { type AnyState, type StateName, type StateStore, type StateValue } from '../state/state'
7
6
  import {
8
7
  type AnyFamily,
9
8
  type FamilyKey,
10
- type FamilyStore,
9
+ type FamilyName,
11
10
  type FamilyValue,
11
+ type StateFamilyStore,
12
12
  Tombstone,
13
13
  } from '../state/stateFamily'
14
14
  import { narrowHandled, type Tagged } from '../runtime/bus'
15
15
  import { type ReducerEntry, Reducers } from '../runtime/loop'
16
- import type { AnyValue } from '../internal/variance'
17
16
 
18
17
  export interface ReducerManifest extends Manifest {
19
18
  readonly kind: 'Reducer'
@@ -44,6 +43,26 @@ export interface FamilyReducerClass<F extends AnyFamily, Events extends Readonly
44
43
  readonly handles: ReadonlySet<string>
45
44
  }
46
45
 
46
+ type ErasedFold = (...args: ReadonlyArray<never>) => unknown
47
+
48
+ interface RuntimeStateReducerConfig {
49
+ readonly states: readonly [AnyState]
50
+ readonly events: ReadonlyArray<AnyEvent>
51
+ }
52
+
53
+ interface RuntimeFamilyReducerConfig {
54
+ readonly family: AnyFamily
55
+ readonly keyOf: ErasedFold
56
+ readonly events: ReadonlyArray<AnyEvent>
57
+ }
58
+
59
+ interface RuntimeReducerClass {
60
+ new (): {}
61
+ readonly manifest: ReducerManifest
62
+ readonly config: RuntimeStateReducerConfig | RuntimeFamilyReducerConfig
63
+ readonly handles: ReadonlySet<string>
64
+ }
65
+
47
66
  export function make<S extends AnyState, const Events extends ReadonlyArray<AnyEvent>>(
48
67
  name: string,
49
68
  config: StateReducerConfig<S, Events>,
@@ -54,9 +73,7 @@ export function make<F extends AnyFamily, const Events extends ReadonlyArray<Any
54
73
  ): FamilyReducerClass<F, Events>
55
74
  export function make(
56
75
  name: string,
57
- config:
58
- | StateReducerConfig<AnyState, ReadonlyArray<AnyEvent>>
59
- | FamilyReducerConfig<AnyFamily, ReadonlyArray<AnyEvent>>,
76
+ config: RuntimeStateReducerConfig | RuntimeFamilyReducerConfig,
60
77
  ): unknown {
61
78
  return definitionClass<unknown>({
62
79
  manifest: { kind: 'Reducer' as const, name } satisfies ReducerManifest,
@@ -79,48 +96,75 @@ const sync = <A>(candidate: A): A => {
79
96
  export function live<S extends AnyState, Events extends ReadonlyArray<AnyEvent>>(
80
97
  reducer: StateReducerClass<S, Events>,
81
98
  fold: (value: StateValue<S>, event: EventType<Events[number]>) => StateValue<S>,
82
- ): Layer.Layer<never, never, Reducers | Store<StateValue<S>>>
99
+ ): Layer.Layer<never, never, Reducers | StateStore<StateName<S>, StateValue<S>>>
83
100
  export function live<F extends AnyFamily, Events extends ReadonlyArray<AnyEvent>>(
84
101
  reducer: FamilyReducerClass<F, Events>,
85
102
  fold: (entry: FamilyValue<F>, event: EventType<Events[number]>) => FamilyValue<F> | Tombstone,
86
- ): Layer.Layer<never, never, Reducers | FamilyStore<FamilyKey<F>, FamilyValue<F>>>
103
+ ): Layer.Layer<
104
+ never,
105
+ never,
106
+ Reducers | StateFamilyStore<FamilyName<F>, FamilyKey<F>, FamilyValue<F>>
107
+ >
87
108
  export function live(
88
- reducer:
89
- | StateReducerClass<AnyState, ReadonlyArray<AnyEvent>>
90
- | FamilyReducerClass<AnyFamily, ReadonlyArray<AnyEvent>>,
91
- // oxlint-disable-next-line reform-rules/no-explicit-any-value -- overload-impl signature: `unknown` params fail contravariance against the State/Family folds (TS2394); only `any` subsumes both
92
- fold: (acc: AnyValue, event: AnyValue) => AnyValue,
93
- ): Layer.Layer<never, never, AnyValue> {
109
+ reducer: RuntimeReducerClass,
110
+ fold: ErasedFold,
111
+ ): Layer.Layer<never, never, unknown> {
94
112
  const { config, handles } = reducer
95
113
  const name = reducer.manifest.name
96
114
  return Layer.scopedDiscard(
97
115
  Effect.gen(function* () {
98
116
  const reducers = yield* Reducers
99
- if (reducers.entries.some((existing) => existing.name === name)) {
117
+ if (reducers.byName.has(name)) {
100
118
  yield* Effect.logWarning(`reform: duplicate reducer name '${name}' registered`)
101
119
  }
102
120
  const entry: ReducerEntry = yield* Effect.gen(function* () {
103
121
  if ('family' in config) {
104
- const family = yield* config.family.store
105
- return {
106
- name,
107
- handles,
108
- apply: (event: Tagged) => {
109
- const key = config.keyOf(narrowHandled(event))
110
- const next = fold(family.at(key).get(), event)
111
- if (next === Tombstone) {
112
- family.forget(key)
113
- } else {
114
- family.at(key).set(sync(next))
115
- }
116
- },
117
- }
122
+ return yield* config.family.capture<Effect.Effect<ReducerEntry, never, unknown>>(
123
+ (familyDefinition) =>
124
+ Effect.map(familyDefinition.store, (family) => ({
125
+ name,
126
+ handles,
127
+ apply: (event: Tagged) => {
128
+ // The loop calls entries only for handled events. At this
129
+ // existential boundary the family's decoders restore the
130
+ // exact key/value types before either touches its store.
131
+ const key = familyDefinition.decodeKeySync(
132
+ sync(Reflect.apply(config.keyOf, undefined, [narrowHandled(event)])),
133
+ )
134
+ const previous = family.at(key).get()
135
+ const next = sync(Reflect.apply(fold, undefined, [previous, event]))
136
+ if (next === Tombstone) {
137
+ family.forget(key)
138
+ } else if (next !== previous) {
139
+ // Same-reference return means "no change": skip the nominal re-decode,
140
+ // which would mint a fresh value and repaint every tracker of this slice.
141
+ family.at(key).set(familyDefinition.decodeValueSync(next))
142
+ }
143
+ },
144
+ })),
145
+ )
118
146
  }
119
- const store = yield* config.states[0].store
120
- return { name, handles, apply: (event: Tagged) => store.set(sync(fold(store.get(), event))) }
147
+ return yield* config.states[0].capture<Effect.Effect<ReducerEntry, never, unknown>>(
148
+ (state) =>
149
+ Effect.map(state.store, (store) => ({
150
+ name,
151
+ handles,
152
+ apply: (event: Tagged) => {
153
+ const previous = store.get()
154
+ const next = sync(Reflect.apply(fold, undefined, [previous, event]))
155
+ // Same-reference return means "no change": skip the nominal re-decode,
156
+ // which would mint a fresh value and repaint every tracker of this slice.
157
+ if (next === previous) {
158
+ return
159
+ }
160
+ store.set(state.decodeUnknownSync(next))
161
+ },
162
+ })),
163
+ )
121
164
  })
122
- yield* Effect.acquireRelease(Effect.sync(() => reducers.register(entry)), () =>
123
- Effect.sync(() => reducers.unregister(entry)),
165
+ yield* Effect.acquireRelease(
166
+ Effect.sync(() => reducers.register(entry)),
167
+ () => Effect.sync(() => reducers.unregister(entry)),
124
168
  )
125
169
  }),
126
170
  )