@playfast/reform 0.0.7 → 0.0.8

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 (237) hide show
  1. package/package.json +2 -2
  2. package/src/boundary/boundary.test.ts +301 -0
  3. package/src/boundary/boundary.ts +192 -0
  4. package/src/calc/asyncCalc.test.ts +556 -0
  5. package/src/calc/asyncCalc.ts +267 -0
  6. package/src/calc/asyncData.ts +96 -0
  7. package/src/calc/calc.test.ts +287 -0
  8. package/src/calc/calc.ts +120 -0
  9. package/src/calc/calcFamily.test.ts +206 -0
  10. package/src/calc/calcFamily.ts +194 -0
  11. package/src/calc/compose.test.ts +68 -0
  12. package/src/calc/compose.ts +69 -0
  13. package/src/channel/channel.ts +269 -0
  14. package/src/compose/composition.ts +112 -0
  15. package/{dist/compose/host.d.ts → src/compose/host.ts} +9 -8
  16. package/{dist/compose/props.js → src/compose/props.ts} +7 -5
  17. package/src/compose/provide.ts +70 -0
  18. package/{dist/compose/slot.d.ts → src/compose/slot.ts} +36 -19
  19. package/src/compose/ui.test.ts +62 -0
  20. package/src/compose/ui.ts +226 -0
  21. package/src/compose/ui.typecheck.ts +56 -0
  22. package/src/definition/definition.ts +76 -0
  23. package/src/event/event.test.ts +23 -0
  24. package/src/event/event.ts +65 -0
  25. package/src/event/eventGroup.ts +14 -0
  26. package/src/feature/feature.mount.test.ts +82 -0
  27. package/src/feature/feature.test.ts +60 -0
  28. package/{dist/feature/feature.d.ts → src/feature/feature.ts} +178 -92
  29. package/src/feature/feature.typecheck.ts +108 -0
  30. package/src/index.ts +176 -0
  31. package/{dist/internal/capture.d.ts → src/internal/capture.ts} +13 -11
  32. package/{dist/internal/ctx.d.ts → src/internal/ctx.ts} +8 -4
  33. package/src/internal/errors.test.ts +33 -0
  34. package/src/internal/errors.ts +145 -0
  35. package/src/internal/inspect.test.ts +28 -0
  36. package/src/internal/inspect.ts +34 -0
  37. package/src/internal/queryDriver.ts +247 -0
  38. package/src/internal/reuse.test.ts +116 -0
  39. package/src/internal/reuse.ts +73 -0
  40. package/src/internal/scheduler.ts +93 -0
  41. package/{dist/internal/seeds.d.ts → src/internal/seeds.ts} +6 -3
  42. package/src/internal/sources.ts +104 -0
  43. package/{dist/internal/stateRegistry.js → src/internal/stateRegistry.ts} +18 -14
  44. package/src/internal/store.test.ts +80 -0
  45. package/src/internal/store.ts +105 -0
  46. package/{dist/internal/track.d.ts → src/internal/track.ts} +17 -11
  47. package/src/procedure/procedure.ts +89 -0
  48. package/src/reducer/reducer.ts +137 -0
  49. package/src/remote/remoteState.test.ts +695 -0
  50. package/src/remote/remoteState.ts +571 -0
  51. package/src/remote/remoteState.typecheck.ts +195 -0
  52. package/src/runtime/bus.ts +40 -0
  53. package/src/runtime/hardening.test.ts +69 -0
  54. package/src/runtime/loop.test.ts +178 -0
  55. package/src/runtime/loop.ts +171 -0
  56. package/src/scene/scene.ts +76 -0
  57. package/src/scene/seedScene.test.ts +169 -0
  58. package/src/state/state.ts +94 -0
  59. package/src/state/stateFamily.test.ts +138 -0
  60. package/src/state/stateFamily.ts +174 -0
  61. package/src/state/stateGroup.ts +105 -0
  62. package/{dist/state/token.d.ts → src/state/token.ts} +22 -13
  63. package/src/synced/syncedStore.ts +99 -0
  64. package/{dist/ui/node.d.ts → src/ui/node.ts} +3 -3
  65. package/{dist/ui/trigger.d.ts → src/ui/trigger.ts} +1 -2
  66. package/src/wire/tree.test.ts +81 -0
  67. package/src/wire/tree.ts +129 -0
  68. package/src/wire/triggers.test.ts +76 -0
  69. package/src/wire/triggers.ts +98 -0
  70. package/dist/boundary/boundary.d.ts +0 -72
  71. package/dist/boundary/boundary.d.ts.map +0 -1
  72. package/dist/boundary/boundary.js +0 -83
  73. package/dist/boundary/boundary.js.map +0 -1
  74. package/dist/calc/asyncCalc.d.ts +0 -91
  75. package/dist/calc/asyncCalc.d.ts.map +0 -1
  76. package/dist/calc/asyncCalc.js +0 -95
  77. package/dist/calc/asyncCalc.js.map +0 -1
  78. package/dist/calc/asyncData.d.ts +0 -57
  79. package/dist/calc/asyncData.d.ts.map +0 -1
  80. package/dist/calc/asyncData.js +0 -34
  81. package/dist/calc/asyncData.js.map +0 -1
  82. package/dist/calc/calc.d.ts +0 -57
  83. package/dist/calc/calc.d.ts.map +0 -1
  84. package/dist/calc/calc.js +0 -58
  85. package/dist/calc/calc.js.map +0 -1
  86. package/dist/calc/calcFamily.d.ts +0 -57
  87. package/dist/calc/calcFamily.d.ts.map +0 -1
  88. package/dist/calc/calcFamily.js +0 -124
  89. package/dist/calc/calcFamily.js.map +0 -1
  90. package/dist/calc/compose.d.ts +0 -16
  91. package/dist/calc/compose.d.ts.map +0 -1
  92. package/dist/calc/compose.js +0 -7
  93. package/dist/calc/compose.js.map +0 -1
  94. package/dist/channel/channel.d.ts +0 -115
  95. package/dist/channel/channel.d.ts.map +0 -1
  96. package/dist/channel/channel.js +0 -138
  97. package/dist/channel/channel.js.map +0 -1
  98. package/dist/compose/composition.d.ts +0 -72
  99. package/dist/compose/composition.d.ts.map +0 -1
  100. package/dist/compose/composition.js +0 -46
  101. package/dist/compose/composition.js.map +0 -1
  102. package/dist/compose/host.d.ts.map +0 -1
  103. package/dist/compose/host.js +0 -6
  104. package/dist/compose/host.js.map +0 -1
  105. package/dist/compose/props.d.ts +0 -13
  106. package/dist/compose/props.d.ts.map +0 -1
  107. package/dist/compose/props.js.map +0 -1
  108. package/dist/compose/provide.d.ts +0 -22
  109. package/dist/compose/provide.d.ts.map +0 -1
  110. package/dist/compose/provide.js +0 -28
  111. package/dist/compose/provide.js.map +0 -1
  112. package/dist/compose/slot.d.ts.map +0 -1
  113. package/dist/compose/slot.js +0 -23
  114. package/dist/compose/slot.js.map +0 -1
  115. package/dist/compose/ui.d.ts +0 -135
  116. package/dist/compose/ui.d.ts.map +0 -1
  117. package/dist/compose/ui.js +0 -63
  118. package/dist/compose/ui.js.map +0 -1
  119. package/dist/definition/definition.d.ts +0 -33
  120. package/dist/definition/definition.d.ts.map +0 -1
  121. package/dist/definition/definition.js +0 -42
  122. package/dist/definition/definition.js.map +0 -1
  123. package/dist/event/event.d.ts +0 -33
  124. package/dist/event/event.d.ts.map +0 -1
  125. package/dist/event/event.js +0 -30
  126. package/dist/event/event.js.map +0 -1
  127. package/dist/event/eventGroup.d.ts +0 -9
  128. package/dist/event/eventGroup.d.ts.map +0 -1
  129. package/dist/event/eventGroup.js +0 -4
  130. package/dist/event/eventGroup.js.map +0 -1
  131. package/dist/feature/feature.d.ts.map +0 -1
  132. package/dist/feature/feature.js +0 -98
  133. package/dist/feature/feature.js.map +0 -1
  134. package/dist/index.d.ts +0 -53
  135. package/dist/index.d.ts.map +0 -1
  136. package/dist/index.js +0 -62
  137. package/dist/index.js.map +0 -1
  138. package/dist/internal/capture.d.ts.map +0 -1
  139. package/dist/internal/capture.js +0 -12
  140. package/dist/internal/capture.js.map +0 -1
  141. package/dist/internal/ctx.d.ts.map +0 -1
  142. package/dist/internal/ctx.js +0 -2
  143. package/dist/internal/ctx.js.map +0 -1
  144. package/dist/internal/errors.d.ts +0 -89
  145. package/dist/internal/errors.d.ts.map +0 -1
  146. package/dist/internal/errors.js +0 -102
  147. package/dist/internal/errors.js.map +0 -1
  148. package/dist/internal/inspect.d.ts +0 -17
  149. package/dist/internal/inspect.d.ts.map +0 -1
  150. package/dist/internal/inspect.js +0 -32
  151. package/dist/internal/inspect.js.map +0 -1
  152. package/dist/internal/queryDriver.d.ts +0 -65
  153. package/dist/internal/queryDriver.d.ts.map +0 -1
  154. package/dist/internal/queryDriver.js +0 -134
  155. package/dist/internal/queryDriver.js.map +0 -1
  156. package/dist/internal/reuse.d.ts +0 -10
  157. package/dist/internal/reuse.d.ts.map +0 -1
  158. package/dist/internal/reuse.js +0 -68
  159. package/dist/internal/reuse.js.map +0 -1
  160. package/dist/internal/scheduler.d.ts +0 -47
  161. package/dist/internal/scheduler.d.ts.map +0 -1
  162. package/dist/internal/scheduler.js +0 -70
  163. package/dist/internal/scheduler.js.map +0 -1
  164. package/dist/internal/seeds.d.ts.map +0 -1
  165. package/dist/internal/seeds.js +0 -17
  166. package/dist/internal/seeds.js.map +0 -1
  167. package/dist/internal/sources.d.ts +0 -39
  168. package/dist/internal/sources.d.ts.map +0 -1
  169. package/dist/internal/sources.js +0 -59
  170. package/dist/internal/sources.js.map +0 -1
  171. package/dist/internal/stateRegistry.d.ts +0 -2
  172. package/dist/internal/stateRegistry.d.ts.map +0 -1
  173. package/dist/internal/stateRegistry.js.map +0 -1
  174. package/dist/internal/store.d.ts +0 -47
  175. package/dist/internal/store.d.ts.map +0 -1
  176. package/dist/internal/store.js +0 -73
  177. package/dist/internal/store.js.map +0 -1
  178. package/dist/internal/track.d.ts.map +0 -1
  179. package/dist/internal/track.js +0 -19
  180. package/dist/internal/track.js.map +0 -1
  181. package/dist/procedure/procedure.d.ts +0 -40
  182. package/dist/procedure/procedure.d.ts.map +0 -1
  183. package/dist/procedure/procedure.js +0 -50
  184. package/dist/procedure/procedure.js.map +0 -1
  185. package/dist/reducer/reducer.d.ts +0 -44
  186. package/dist/reducer/reducer.d.ts.map +0 -1
  187. package/dist/reducer/reducer.js +0 -63
  188. package/dist/reducer/reducer.js.map +0 -1
  189. package/dist/remote/remoteState.d.ts +0 -119
  190. package/dist/remote/remoteState.d.ts.map +0 -1
  191. package/dist/remote/remoteState.js +0 -270
  192. package/dist/remote/remoteState.js.map +0 -1
  193. package/dist/runtime/bus.d.ts +0 -27
  194. package/dist/runtime/bus.d.ts.map +0 -1
  195. package/dist/runtime/bus.js +0 -21
  196. package/dist/runtime/bus.js.map +0 -1
  197. package/dist/runtime/loop.d.ts +0 -45
  198. package/dist/runtime/loop.d.ts.map +0 -1
  199. package/dist/runtime/loop.js +0 -117
  200. package/dist/runtime/loop.js.map +0 -1
  201. package/dist/scene/scene.d.ts +0 -44
  202. package/dist/scene/scene.d.ts.map +0 -1
  203. package/dist/scene/scene.js +0 -31
  204. package/dist/scene/scene.js.map +0 -1
  205. package/dist/state/state.d.ts +0 -37
  206. package/dist/state/state.d.ts.map +0 -1
  207. package/dist/state/state.js +0 -46
  208. package/dist/state/state.js.map +0 -1
  209. package/dist/state/stateFamily.d.ts +0 -79
  210. package/dist/state/stateFamily.d.ts.map +0 -1
  211. package/dist/state/stateFamily.js +0 -99
  212. package/dist/state/stateFamily.js.map +0 -1
  213. package/dist/state/stateGroup.d.ts +0 -50
  214. package/dist/state/stateGroup.d.ts.map +0 -1
  215. package/dist/state/stateGroup.js +0 -54
  216. package/dist/state/stateGroup.js.map +0 -1
  217. package/dist/state/token.d.ts.map +0 -1
  218. package/dist/state/token.js +0 -20
  219. package/dist/state/token.js.map +0 -1
  220. package/dist/synced/syncedStore.d.ts +0 -51
  221. package/dist/synced/syncedStore.d.ts.map +0 -1
  222. package/dist/synced/syncedStore.js +0 -43
  223. package/dist/synced/syncedStore.js.map +0 -1
  224. package/dist/ui/node.d.ts.map +0 -1
  225. package/dist/ui/node.js +0 -2
  226. package/dist/ui/node.js.map +0 -1
  227. package/dist/ui/trigger.d.ts.map +0 -1
  228. package/dist/ui/trigger.js +0 -2
  229. package/dist/ui/trigger.js.map +0 -1
  230. package/dist/wire/tree.d.ts +0 -66
  231. package/dist/wire/tree.d.ts.map +0 -1
  232. package/dist/wire/tree.js +0 -60
  233. package/dist/wire/tree.js.map +0 -1
  234. package/dist/wire/triggers.d.ts +0 -44
  235. package/dist/wire/triggers.d.ts.map +0 -1
  236. package/dist/wire/triggers.js +0 -31
  237. package/dist/wire/triggers.js.map +0 -1
@@ -1,11 +1,19 @@
1
- import { type Context, Effect, Layer, type Scope } from 'effect';
2
- import { type Engine } from '../runtime/loop';
3
- import { type StoresOf } from '../internal/sources';
4
- import { FeatureLoadFailed } from '../internal/errors';
5
- import { type Manifest } from '../definition/definition';
6
- import { type CompositionClass } from '../compose/composition';
7
- import { type UiContract } from '../compose/ui';
8
- import { type Tagged } from '../runtime/bus';
1
+ import { type Context, Effect, Layer, type Scope } from 'effect'
2
+ import { type Engine } from '../runtime/loop'
3
+ import { type StoresOf } from '../internal/sources'
4
+ import { FeatureLoadFailed } from '../internal/errors'
5
+ import { type Manifest, definitionClass } from '../definition/definition'
6
+ import { type CompositionClass } from '../compose/composition'
7
+ import { type UiContract } from '../compose/ui'
8
+ import { publish, type Tagged } from '../runtime/bus'
9
+
10
+ // A `Feature` is the unit of lazy code-split, ownership, reflection, and proof.
11
+ // It splits on the seam Reform already has: definitions (manifests, schemas, the
12
+ // composition class, placeholders) stay eager and reflectable; the heavy `.live`
13
+ // layer loads on demand. The whole-app *shape* stays inspectable with no chunk
14
+ // loaded. Everything here is additive — `provide(slot, Composition)` and eager
15
+ // scenes are unchanged. See `LAZY.md` for the design rationale and variance proof.
16
+
9
17
  /**
10
18
  * The always-on engine services a feature builds against: the shared event `Bus`
11
19
  * plus the reducer/channel/procedure registries. Derived from the `Engine`
@@ -13,20 +21,18 @@ import { type Tagged } from '../runtime/bus';
13
21
  * these only as OPEN requirements (its `RIn`) and must never *provide* them — that
14
22
  * is what keeps the engine single and shared across every lazily-mounted feature.
15
23
  */
16
- export type EngineServices = Layer.Layer.Success<typeof Engine>;
24
+ export type EngineServices = Layer.Layer.Success<typeof Engine>
25
+
17
26
  /**
18
27
  * A shared-state carrier a feature may declare in `requires`: a `State`,
19
28
  * `StateFamily`, `Calc`, or `AsyncCalc` (each carrying a `store` tag), or a
20
29
  * `StateGroup` (carrying `members`, each with its own `store`). Per the design,
21
30
  * a feature depends on shared state/calc only — never on another feature.
22
31
  */
23
- export type RequireCarrier = {
24
- readonly store: Context.Tag<any, any>;
25
- } | {
26
- readonly members: ReadonlyArray<{
27
- readonly store: Context.Tag<any, any>;
28
- }>;
29
- };
32
+ export type RequireCarrier =
33
+ | { readonly store: Context.Tag<any, any> }
34
+ | { readonly members: ReadonlyArray<{ readonly store: Context.Tag<any, any> }> }
35
+
30
36
  /**
31
37
  * The Context services a `requires` tuple makes available to a feature layer: a
32
38
  * `StateGroup` contributes its members' stores (the shared `StoresOf`); every
@@ -37,12 +43,15 @@ export type RequireCarrier = {
37
43
  * is ordinary assignability — no `as`, no `infer` gymnastics in user code.
38
44
  */
39
45
  export type ProvidedBy<Requires extends ReadonlyArray<RequireCarrier>> = {
40
- [I in keyof Requires]: Requires[I] extends {
41
- readonly members: infer M extends ReadonlyArray<unknown>;
42
- } ? StoresOf<M> : Requires[I] extends {
43
- readonly store: Context.Tag<infer Svc, any>;
44
- } ? Svc : never;
45
- }[number];
46
+ [I in keyof Requires]: Requires[I] extends {
47
+ readonly members: infer M extends ReadonlyArray<unknown>
48
+ }
49
+ ? StoresOf<M>
50
+ : Requires[I] extends { readonly store: Context.Tag<infer Svc, any> }
51
+ ? Svc
52
+ : never
53
+ }[number]
54
+
46
55
  /**
47
56
  * A loaded feature's two artifacts. `requires` is the runtime-enumerable list the
48
57
  * editor reads (types are erased, so the carriers are kept as values); `layer` is
@@ -53,9 +62,10 @@ export type ProvidedBy<Requires extends ReadonlyArray<RequireCarrier>> = {
53
62
  * a feature's lazy half.
54
63
  */
55
64
  export interface FeatureModule<ROut, Requires extends ReadonlyArray<RequireCarrier>> {
56
- readonly requires: Requires;
57
- readonly layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>;
65
+ readonly requires: Requires
66
+ readonly layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>
58
67
  }
68
+
59
69
  /**
60
70
  * Publish a feature's lazy half: pair its `requires` (a runtime tuple of shared
61
71
  * carriers) with its `layer`. The covariant `RIn` bound does the checking — a
@@ -67,7 +77,11 @@ export interface FeatureModule<ROut, Requires extends ReadonlyArray<RequireCarri
67
77
  * be open `RIn`), structurally enforcing the liveness rule: a feature can never
68
78
  * bundle its own engine — it always shares the root's bus and registries.
69
79
  */
70
- export declare const featureModule: <const Requires extends ReadonlyArray<RequireCarrier>, ROut>(requires: Requires, layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>) => FeatureModule<ROut, Requires>;
80
+ export const featureModule = <const Requires extends ReadonlyArray<RequireCarrier>, ROut>(
81
+ requires: Requires,
82
+ layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>,
83
+ ): FeatureModule<ROut, Requires> => ({ requires, layer })
84
+
71
85
  /**
72
86
  * Wrap a dynamic `import()` of a feature's `.live` module as an `Effect` — Reform
73
87
  * never exposes a Promise/`await` on the feature surface. The unavoidable thenable
@@ -75,14 +89,20 @@ export declare const featureModule: <const Requires extends ReadonlyArray<Requir
75
89
  * throw) surfaces as a typed `FeatureLoadFailed` the host hands to
76
90
  * `placeholder.failed`. The module's default export is its `FeatureModule`.
77
91
  */
78
- export declare const lazyImport: <M>(thunk: () => Promise<{
79
- readonly default: M;
80
- }>) => Effect.Effect<M, FeatureLoadFailed>;
92
+ export const lazyImport = <M>(
93
+ thunk: () => Promise<{ readonly default: M }>,
94
+ ): Effect.Effect<M, FeatureLoadFailed> =>
95
+ Effect.tryPromise({
96
+ try: thunk,
97
+ catch: (cause) => new FeatureLoadFailed({ cause }),
98
+ }).pipe(Effect.map((module) => module.default))
99
+
81
100
  /** The props the host injects into a lazy feature's `failed` placeholder. */
82
101
  export interface FailedProps {
83
- readonly error: FeatureLoadFailed;
84
- readonly retry: () => void;
102
+ readonly error: FeatureLoadFailed
103
+ readonly retry: () => void
85
104
  }
105
+
86
106
  /**
87
107
  * The reflectable shape of a feature: its strategy, the composition it mounts,
88
108
  * and (when lazy) its placeholder compositions — all as manifests, so the editor
@@ -90,21 +110,16 @@ export interface FailedProps {
90
110
  * runtime data and lives on the `FeatureBinding`, not here.
91
111
  */
92
112
  export interface FeatureManifest extends Manifest {
93
- readonly kind: 'Feature';
94
- readonly name: string;
95
- readonly strategy: 'lazy' | 'default';
96
- readonly composition: Manifest & {
97
- readonly kind: 'Composition';
98
- };
99
- readonly placeholder?: {
100
- readonly loading: Manifest & {
101
- readonly kind: 'Composition';
102
- };
103
- readonly failed: Manifest & {
104
- readonly kind: 'Composition';
105
- };
106
- };
113
+ readonly kind: 'Feature'
114
+ readonly name: string
115
+ readonly strategy: 'lazy' | 'default'
116
+ readonly composition: Manifest & { readonly kind: 'Composition' }
117
+ readonly placeholder?: {
118
+ readonly loading: Manifest & { readonly kind: 'Composition' }
119
+ readonly failed: Manifest & { readonly kind: 'Composition' }
120
+ }
107
121
  }
122
+
108
123
  /**
109
124
  * A feature's loaded artifacts with type params erased, for the renderer-neutral,
110
125
  * strategy-uniform host. The erasure is cast-free *into*: because `Layer`'s `ROut`
@@ -114,12 +129,14 @@ export interface FeatureManifest extends Manifest {
114
129
  * documented boundary cast there, mirroring `buildRuntime`).
115
130
  */
116
131
  export interface LoadedModule {
117
- readonly requires: ReadonlyArray<RequireCarrier>;
118
- readonly layer: Layer.Layer<never, never, unknown>;
132
+ readonly requires: ReadonlyArray<RequireCarrier>
133
+ readonly layer: Layer.Layer<never, never, unknown>
119
134
  }
135
+
120
136
  /** Nominal brand identifying the value a `provide(slot, Feature)` puts under a slot tag. */
121
- export declare const FeatureBindingTypeId: unique symbol;
122
- export type FeatureBindingTypeId = typeof FeatureBindingTypeId;
137
+ export const FeatureBindingTypeId: unique symbol = Symbol.for('reform/FeatureBinding')
138
+ export type FeatureBindingTypeId = typeof FeatureBindingTypeId
139
+
123
140
  /**
124
141
  * What `provide(slot, Feature)` binds under the slot tag — everything the host
125
142
  * needs to mount the feature, with types erased (the host is renderer-neutral and
@@ -130,19 +147,22 @@ export type FeatureBindingTypeId = typeof FeatureBindingTypeId;
130
147
  * each.
131
148
  */
132
149
  export interface FeatureBinding {
133
- readonly [FeatureBindingTypeId]: FeatureBindingTypeId;
134
- readonly manifest: FeatureManifest;
135
- readonly composition: CompositionClass<unknown>;
136
- readonly strategy: 'lazy' | 'default';
137
- readonly load: Effect.Effect<LoadedModule, FeatureLoadFailed>;
138
- readonly placeholder?: {
139
- readonly loading: CompositionClass<unknown>;
140
- readonly failed: CompositionClass<unknown>;
141
- };
142
- readonly boot: ReadonlyArray<Tagged>;
150
+ readonly [FeatureBindingTypeId]: FeatureBindingTypeId
151
+ readonly manifest: FeatureManifest
152
+ readonly composition: CompositionClass<unknown>
153
+ readonly strategy: 'lazy' | 'default'
154
+ readonly load: Effect.Effect<LoadedModule, FeatureLoadFailed>
155
+ readonly placeholder?: {
156
+ readonly loading: CompositionClass<unknown>
157
+ readonly failed: CompositionClass<unknown>
158
+ }
159
+ readonly boot: ReadonlyArray<Tagged>
143
160
  }
161
+
144
162
  /** Whether a slot's bound child is a lazy/eager feature (vs a plain composition). */
145
- export declare const isFeatureBinding: (u: unknown) => u is FeatureBinding;
163
+ export const isFeatureBinding = (u: unknown): u is FeatureBinding =>
164
+ typeof u === 'object' && u !== null && FeatureBindingTypeId in u
165
+
146
166
  /**
147
167
  * A feature *definition*: a reflectable manifest (`kind: 'Feature'`) that carries
148
168
  * the eager half (composition + placeholders) and an Effect `load` for the heavy
@@ -150,25 +170,37 @@ export declare const isFeatureBinding: (u: unknown) => u is FeatureBinding;
150
170
  * shared-carrier tuple the loaded module declares. `load` is an `Effect`, never a
151
171
  * Promise — consumers `yield* Feature.load`.
152
172
  */
153
- export interface FeatureClass<P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>> {
154
- new (): {};
155
- readonly manifest: FeatureManifest;
156
- /** Phantom carrying the mounted composition's props type (for `provide` checks). */
157
- readonly Props: P;
158
- /** Phantom carrying the composition's UI contract (for typed reflection consumers). */
159
- readonly Contract: C;
160
- /** Phantom carrying the declared shared-carrier tuple. */
161
- readonly Requires: Requires;
162
- /** Load the feature's module — `yield* Feature.load` → its `FeatureModule`, or `FeatureLoadFailed`. */
163
- readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>;
164
- /** The value `provide(slot, Feature)` binds under the slot tag. */
165
- readonly binding: FeatureBinding;
166
- /** The eagerly-available module — present only for `loadingStrategy: 'default'`. */
167
- readonly eagerModule: FeatureModule<ROut, Requires> | undefined;
173
+ export interface FeatureClass<
174
+ P,
175
+ C extends UiContract,
176
+ ROut,
177
+ Requires extends ReadonlyArray<RequireCarrier>,
178
+ > {
179
+ new (): {}
180
+ readonly manifest: FeatureManifest
181
+ /** Phantom carrying the mounted composition's props type (for `provide` checks). */
182
+ readonly Props: P
183
+ /** Phantom carrying the composition's UI contract (for typed reflection consumers). */
184
+ readonly Contract: C
185
+ /** Phantom carrying the declared shared-carrier tuple. */
186
+ readonly Requires: Requires
187
+ /** Load the feature's module — `yield* Feature.load` → its `FeatureModule`, or `FeatureLoadFailed`. */
188
+ readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>
189
+ /** The value `provide(slot, Feature)` binds under the slot tag. */
190
+ readonly binding: FeatureBinding
191
+ /** The eagerly-available module — present only for `loadingStrategy: 'default'`. */
192
+ readonly eagerModule: FeatureModule<ROut, Requires> | undefined
168
193
  }
169
- export type AnyFeature = FeatureClass<any, any, any, ReadonlyArray<RequireCarrier>>;
194
+
195
+ export type AnyFeature = FeatureClass<any, any, any, ReadonlyArray<RequireCarrier>>
196
+
170
197
  /** Whether a value is a feature definition (its target shape in `provide(slot, Feature)`). */
171
- export declare const isFeature: (u: unknown) => u is AnyFeature;
198
+ export const isFeature = (u: unknown): u is AnyFeature =>
199
+ (typeof u === 'function' || typeof u === 'object') &&
200
+ u !== null &&
201
+ 'manifest' in u &&
202
+ (u as { readonly manifest: { readonly kind?: unknown } }).manifest?.kind === 'Feature'
203
+
172
204
  /**
173
205
  * The placeholders a lazy feature must ship — both eager compositions (their
174
206
  * `.live` is in the eager bundle, so they paint the load gap). `loading` receives
@@ -177,20 +209,28 @@ export declare const isFeature: (u: unknown) => u is AnyFeature;
177
209
  * are not schema-decoded inputs.
178
210
  */
179
211
  export interface Placeholders {
180
- readonly loading: CompositionClass<any, UiContract>;
181
- readonly failed: CompositionClass<any, UiContract>;
212
+ readonly loading: CompositionClass<any, UiContract>
213
+ readonly failed: CompositionClass<any, UiContract>
182
214
  }
215
+
183
216
  type FeatureConfig<P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>> = {
184
- readonly composition: CompositionClass<P, C>;
185
- readonly boot?: ReadonlyArray<Tagged>;
186
- } & ({
187
- readonly loadingStrategy?: 'default';
188
- readonly module: FeatureModule<ROut, Requires>;
189
- } | {
190
- readonly loadingStrategy: 'lazy';
191
- readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>;
192
- readonly placeholder: Placeholders;
193
- });
217
+ readonly composition: CompositionClass<P, C>
218
+ readonly boot?: ReadonlyArray<Tagged>
219
+ } & (
220
+ | {
221
+ // EAGER: the module is statically present and merged into the runtime at
222
+ // boot. No load gap, so no placeholder.
223
+ readonly loadingStrategy?: 'default'
224
+ readonly module: FeatureModule<ROut, Requires>
225
+ }
226
+ | {
227
+ // LAZY: the module loads on mount via an Effect. Placeholders required.
228
+ readonly loadingStrategy: 'lazy'
229
+ readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>
230
+ readonly placeholder: Placeholders
231
+ }
232
+ )
233
+
194
234
  /**
195
235
  * Define a feature. The whole-app shape (this manifest, the composition, the
196
236
  * placeholders) stays eager and reflectable; the `.live` layer in `module`/`load`
@@ -198,7 +238,41 @@ type FeatureConfig<P, C extends UiContract, ROut, Requires extends ReadonlyArray
198
238
  * the discriminated config *requires* `placeholder` — the compiler walks you to a
199
239
  * complete code-split feature.
200
240
  */
201
- export declare const make: <P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>>(name: string, config: FeatureConfig<P, C, ROut, Requires>) => FeatureClass<P, C, ROut, Requires>;
241
+ export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>>(
242
+ name: string,
243
+ config: FeatureConfig<P, C, ROut, Requires>,
244
+ ): FeatureClass<P, C, ROut, Requires> => {
245
+ const strategy: 'lazy' | 'default' = config.loadingStrategy ?? 'default'
246
+ const load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed> =
247
+ 'load' in config ? config.load : Effect.succeed(config.module)
248
+ const eagerModule = 'module' in config ? config.module : undefined
249
+ const placeholder = 'placeholder' in config ? config.placeholder : undefined
250
+
251
+ const manifest: FeatureManifest = {
252
+ kind: 'Feature',
253
+ name,
254
+ strategy,
255
+ composition: config.composition.manifest,
256
+ ...(placeholder
257
+ ? { placeholder: { loading: placeholder.loading.manifest, failed: placeholder.failed.manifest } }
258
+ : {}),
259
+ }
260
+
261
+ const binding: FeatureBinding = {
262
+ [FeatureBindingTypeId]: FeatureBindingTypeId,
263
+ manifest,
264
+ composition: config.composition,
265
+ strategy,
266
+ // The host is strategy-uniform: `default`'s `load` resolves immediately. The
267
+ // assignment to the erased `LoadedModule` needs no cast — see `LoadedModule`.
268
+ load,
269
+ ...(placeholder ? { placeholder: { loading: placeholder.loading, failed: placeholder.failed } } : {}),
270
+ boot: config.boot ?? [],
271
+ }
272
+
273
+ return definitionClass<FeatureClass<P, C, ROut, Requires>>({ manifest, load, binding, eagerModule })
274
+ }
275
+
202
276
  /**
203
277
  * Mount a feature against a live engine. The renderer-neutral lifecycle, run on a
204
278
  * per-feature `Scope`: load the module (an Effect — lazy = dynamic import; default
@@ -215,6 +289,18 @@ export declare const make: <P, C extends UiContract, ROut, Requires extends Read
215
289
  * we re-attach the concrete `engineContext` it builds against. `R` is the engine
216
290
  * context's service set, supplied by the host.
217
291
  */
218
- export declare const mountFeature: <R>(binding: FeatureBinding, engineContext: Context.Context<R>) => Effect.Effect<Context.Context<unknown>, FeatureLoadFailed, Scope.Scope>;
219
- export {};
220
- //# sourceMappingURL=feature.d.ts.map
292
+ export const mountFeature = <R>(
293
+ binding: FeatureBinding,
294
+ engineContext: Context.Context<R>,
295
+ ): Effect.Effect<Context.Context<unknown>, FeatureLoadFailed, Scope.Scope> =>
296
+ Effect.gen(function* () {
297
+ const { layer } = yield* binding.load
298
+ const featureLayer = layer as Layer.Layer<unknown, never, R>
299
+ const context = yield* Layer.build(
300
+ Layer.provideMerge(featureLayer, Layer.succeedContext(engineContext)),
301
+ )
302
+ yield* Effect.forEach(binding.boot, (event) => publish('High', event), { discard: true }).pipe(
303
+ Effect.provide(context),
304
+ )
305
+ return context
306
+ })
@@ -0,0 +1,108 @@
1
+ // Type-level proofs enforced by `tsc --noEmit` over `src` (not a vitest file).
2
+ // These guard the cast-free seam: if a guarantee regresses, a `@ts-expect-error`
3
+ // goes unused or an assignment fails, and `tsc` breaks.
4
+
5
+ import { Layer, Schema as S } from 'effect'
6
+ import type { Store } from '../internal/store'
7
+ import * as Composition from '../compose/composition'
8
+ import { provide } from '../compose/provide'
9
+ import { type SlotChild, type SlotClass, slot } from '../compose/slot'
10
+ import { type Contract, type UiClass, ui } from '../compose/ui'
11
+ import * as State from '../state/state'
12
+ import * as StateGroup from '../state/stateGroup'
13
+ import { type EngineServices, type FeatureModule, featureModule, lazyImport } from './feature'
14
+ import * as Feature from './feature'
15
+
16
+ const FeedStateBase: State.StateClass<'feed', string> = State.make('feed', S.String)
17
+ class FeedState extends FeedStateBase {}
18
+ const FilterStateBase: State.StateClass<'filter', string> = State.make('filter', S.String)
19
+ class FilterState extends FilterStateBase {}
20
+ const TodosStatesBase: StateGroup.StateGroupClass<readonly [typeof FeedState, typeof FilterState]> =
21
+ StateGroup.make(FeedState, FilterState)
22
+ class TodosStates extends TodosStatesBase {}
23
+
24
+ // --- featureModule: the covariant-RIn bound (cast-free requirement honesty) ---
25
+
26
+ // A feature layer that reads FeedState's store and the shared engine — nothing else.
27
+ declare const readsFeed: Layer.Layer<{ readonly _out: true }, never, Store<string> | EngineServices>
28
+
29
+ // POSITIVE: declaring the State it reads in `requires` covers the read — compiles.
30
+ export const okState: FeatureModule<{ readonly _out: true }, readonly [typeof FeedState]> =
31
+ featureModule([FeedState], readsFeed)
32
+
33
+ // POSITIVE: a StateGroup contributes its members' stores via `ProvidedBy` — compiles.
34
+ export const okGroup: FeatureModule<{ readonly _out: true }, readonly [typeof TodosStates]> =
35
+ featureModule([TodosStates], readsFeed)
36
+
37
+ // NEGATIVE: empty `requires` provides only the engine, not Store<string>; covariant
38
+ // `RIn` makes the layer unassignable to the declared bound.
39
+ export const badEmpty: FeatureModule<{ readonly _out: true }, readonly []> =
40
+ // @ts-expect-error feature layer reads a store it does not declare in `requires`
41
+ featureModule([], readsFeed)
42
+
43
+ // --- provide(slot, Feature): RIn surfacing + composition match ---
44
+
45
+ const AppUiBase: UiClass<{ props: { readonly a: string }; events: {} }> = ui('AppUi')<{
46
+ props: { readonly a: string }
47
+ events: {}
48
+ }>()
49
+ class AppUi extends AppUiBase {}
50
+ const AppCompBase: Composition.CompositionClass<{ readonly a: string }, Contract<typeof AppUi>> =
51
+ Composition.make('AppComp', {
52
+ title: 'App',
53
+ props: S.Struct({ a: S.String }),
54
+ ui: AppUi,
55
+ })
56
+ class AppComp extends AppCompBase {}
57
+ const AppSlotBase: SlotClass<typeof AppComp> = slot('App')<typeof AppComp>()
58
+ class AppSlot extends AppSlotBase {}
59
+
60
+ const AppFeatureBase: Feature.FeatureClass<
61
+ { readonly a: string },
62
+ Contract<typeof AppUi>,
63
+ never,
64
+ readonly []
65
+ > = Feature.make('appFeature', {
66
+ loadingStrategy: 'lazy',
67
+ composition: AppComp,
68
+ load: lazyImport(() => Promise.resolve({ default: featureModule([], Layer.empty) })),
69
+ placeholder: { loading: AppComp, failed: AppComp },
70
+ })
71
+ class AppFeature extends AppFeatureBase {}
72
+
73
+ // POSITIVE: the wired layer's open `RIn` is exactly `EngineServices` (requires=[]),
74
+ // so a scene must provide `Core` — surfaced cast-free by the overload.
75
+ export const wired: Layer.Layer<SlotChild, never, EngineServices> = provide(AppSlot, AppFeature)
76
+
77
+ // A feature over a DIFFERENT contract must not fill `AppSlot`.
78
+ const OtherUiBase: UiClass<{ props: { readonly n: number }; events: {} }> = ui('OtherUi')<{
79
+ props: { readonly n: number }
80
+ events: {}
81
+ }>()
82
+ class OtherUi extends OtherUiBase {}
83
+ const OtherCompBase: Composition.CompositionClass<{ readonly n: number }, Contract<typeof OtherUi>> =
84
+ Composition.make('OtherComp', {
85
+ title: 'Other',
86
+ props: S.Struct({ n: S.Number }),
87
+ ui: OtherUi,
88
+ })
89
+ class OtherComp extends OtherCompBase {}
90
+ const OtherFeatureBase: Feature.FeatureClass<
91
+ { readonly n: number },
92
+ Contract<typeof OtherUi>,
93
+ never,
94
+ readonly []
95
+ > = Feature.make('otherFeature', {
96
+ loadingStrategy: 'lazy',
97
+ composition: OtherComp,
98
+ load: lazyImport(() => Promise.resolve({ default: featureModule([], Layer.empty) })),
99
+ placeholder: { loading: OtherComp, failed: OtherComp },
100
+ })
101
+ class OtherFeature extends OtherFeatureBase {}
102
+
103
+ // NEGATIVE: OtherFeature's composition props don't match AppSlot's child composition.
104
+ // @ts-expect-error feature composition does not match the slot's child type
105
+ export const mismatched: Layer.Layer<SlotChild, never, EngineServices> = provide(
106
+ AppSlot,
107
+ OtherFeature,
108
+ )
package/src/index.ts ADDED
@@ -0,0 +1,176 @@
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
+ export * as State from './state/state'
6
+ export * as StateGroup from './state/stateGroup'
7
+ export * as StateFamily from './state/stateFamily'
8
+ export { StateToken } from './state/token'
9
+ export type { AnySource, Source } from './state/token'
10
+
11
+ export * as Event from './event/event'
12
+ // Direct type re-export alongside the namespace (TS2742 nameability): inferred
13
+ // types of layers built over intent/event unions reference `EventOf`, and the
14
+ // declaration emitter can only name it through a plain export.
15
+ export type { EventOf } from './event/event'
16
+ export * as EventGroup from './event/eventGroup'
17
+
18
+ export * as Reducer from './reducer/reducer'
19
+ export * as Calc from './calc/calc'
20
+ export * as CalcFamily from './calc/calcFamily'
21
+ export * as AsyncCalc from './calc/asyncCalc'
22
+ 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
+ export type { InputsObject, InputStores, InvalidateBy } from './internal/sources'
27
+ export {
28
+ AsyncData,
29
+ type AsyncError,
30
+ type AsyncIdle,
31
+ type AsyncLoading,
32
+ type AsyncSuccess,
33
+ } from './calc/asyncData'
34
+ export * as RemoteState from './remote/remoteState'
35
+ // Direct type re-exports alongside the namespace (TS2742 nameability): an app
36
+ // layer's inferred type references these (`Store<ReadonlyArray<PendingIntent<…>>>`),
37
+ // and the declaration emitter can only name them through a plain export.
38
+ export type { FailedIntent, PendingIntent, RemoteStateClass } from './remote/remoteState'
39
+ export * as SyncedStore from './synced/syncedStore'
40
+ // Direct type re-export alongside the namespace (TS2742 nameability): adapters
41
+ // (e.g. `@playfast/reform-db`) annotate the source handed to `SyncedStore.live`.
42
+ export type { SyncedSource, SyncedStoreClass } from './synced/syncedStore'
43
+ export * as Boundary from './boundary/boundary'
44
+ // Direct type re-exports alongside the namespace (same TS2742 nameability
45
+ // reason as the RemoteState types above): app code annotates props with these.
46
+ export type {
47
+ BoundaryErrored,
48
+ BoundaryPending,
49
+ BoundaryReady,
50
+ BoundaryState,
51
+ } from './boundary/boundary'
52
+ export * as Procedure from './procedure/procedure'
53
+ export * as Channel from './channel/channel'
54
+
55
+ export * as Composition from './compose/composition'
56
+ export type {
57
+ CompositionClass,
58
+ CompositionService,
59
+ RenderEnv,
60
+ } from './compose/composition'
61
+ export { Props } from './compose/props'
62
+ export {
63
+ isSlot,
64
+ type Node,
65
+ slot,
66
+ type SlotClass,
67
+ type SlotInstance,
68
+ type SlotProps,
69
+ SlotTypeId,
70
+ } from './compose/slot'
71
+ export { CurrentSlots, type SlotHost } from './compose/host'
72
+ export * as Ui from './compose/ui'
73
+ export {
74
+ type DerivedContract,
75
+ isUi,
76
+ type LogicView,
77
+ type MadeView,
78
+ ui,
79
+ type UiClass,
80
+ type UiContract,
81
+ type UiManifest,
82
+ UiTypeId,
83
+ UiViewContract,
84
+ type ViewImpl,
85
+ type WireSchemas,
86
+ type WiredUi,
87
+ type WiredUiManifest,
88
+ } from './compose/ui'
89
+ export { provide } from './compose/provide'
90
+ export type { Trigger } from './ui/trigger'
91
+
92
+ // Wire: the serializable rendered-tree model + pure diff/apply the remote
93
+ // transport sends over the wire and the client folds back. See REMOTE_UI.md §3.
94
+ export * as Wire from './wire/tree'
95
+ export type { WireNode, WirePatch, WireProp, WireTree } from './wire/tree'
96
+
97
+ // Triggers: the server-side handle registry bridging client trigger invocations
98
+ // back to live triggers, decoding payloads via the event schema. See REMOTE_UI.md §4.
99
+ export * as Triggers from './wire/triggers'
100
+ export {
101
+ TriggerRegistry,
102
+ type TriggerHandle,
103
+ type TriggerRegistryApi,
104
+ UnknownTrigger,
105
+ } from './wire/triggers'
106
+
107
+ // Feature: the lazy/eager code-split unit. Definitions stay eager and reflectable;
108
+ // the heavy `.live` layer loads on demand (an Effect, never a Promise). See LAZY.md.
109
+ export * as Feature from './feature/feature'
110
+ export {
111
+ type EngineServices,
112
+ type FailedProps,
113
+ type FeatureBinding,
114
+ FeatureBindingTypeId,
115
+ type FeatureClass,
116
+ type FeatureManifest,
117
+ featureModule,
118
+ type FeatureModule,
119
+ isFeature,
120
+ isFeatureBinding,
121
+ lazyImport,
122
+ type LoadedModule,
123
+ mountFeature,
124
+ type Placeholders,
125
+ type ProvidedBy,
126
+ type RequireCarrier,
127
+ } from './feature/feature'
128
+ export { type SlotChild } from './compose/slot'
129
+
130
+ // A scene bundles a composition with the closed wiring that runs it — the single
131
+ // handle the react host, proofs, and the dev tool all consume.
132
+ export { isScene, type MountedServices, type Scene, scene, seedScene } from './scene/scene'
133
+
134
+ // Runtime surface for hosts (`@reform/react`) and headless tests.
135
+ export { Bus, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
136
+ export { Engine, type ReducerEntry, Reducers } from './runtime/loop'
137
+ export {
138
+ type ChannelClass,
139
+ type ChannelPolicy,
140
+ type ChannelRuntime,
141
+ Channels,
142
+ type ProcedureEntry,
143
+ Procedures,
144
+ } from './channel/channel'
145
+ export { CurrentTracker, type Subscribable, type Tracker } from './internal/track'
146
+ export { CaptureSink, type CaptureSinkApi, type UiCapture } from './internal/capture'
147
+ export {
148
+ AsyncReducer,
149
+ AsyncSceneLayer,
150
+ DuplicateRegistration,
151
+ FeatureLoadFailed,
152
+ forceSync,
153
+ InvalidProvideTarget,
154
+ SlotRenderingUnavailable,
155
+ UnknownGroupState,
156
+ } from './internal/errors'
157
+ // Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
158
+ // multiple mounted roots) provide `Notifications` upstream of their state layers;
159
+ // everything else coalesces on the process-wide default.
160
+ export {
161
+ defaultScheduler,
162
+ makeScheduler,
163
+ Notifications,
164
+ notificationsLayer,
165
+ resolveScheduler,
166
+ type Scheduler,
167
+ } from './internal/scheduler'
168
+ export { makeStore, type Store } from './internal/store'
169
+ export type { FamilyStore } from './state/stateFamily'
170
+ // Primitive-authoring toolkit — exported for first-party companion packages
171
+ // (e.g. `@playfast/reform-db`, `@playfast/reform-resource`) that build their own
172
+ // first-class `Source`s which must participate in dependency tracking and
173
+ // reflection exactly like a `Calc`.
174
+ export { type Kind, type Manifest, yieldableClass } from './definition/definition'
175
+ export { readTracked } from './internal/track'
176
+ export { sameKey, wireSources, type WiredSources } from './internal/sources'