@playfast/reform 0.0.7 → 0.0.9

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 (241) 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 +146 -0
  15. package/src/compose/host.ts +12 -0
  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/structure.test.ts +90 -0
  20. package/src/compose/structure.ts +145 -0
  21. package/src/compose/ui.test.ts +34 -0
  22. package/src/compose/ui.ts +204 -0
  23. package/src/compose/ui.typecheck.ts +56 -0
  24. package/src/definition/definition.ts +76 -0
  25. package/src/event/event.test.ts +23 -0
  26. package/src/event/event.ts +65 -0
  27. package/src/event/eventGroup.ts +14 -0
  28. package/src/feature/feature.mount.test.ts +82 -0
  29. package/src/feature/feature.test.ts +60 -0
  30. package/{dist/feature/feature.d.ts → src/feature/feature.ts} +178 -92
  31. package/src/feature/feature.typecheck.ts +108 -0
  32. package/src/index.ts +191 -0
  33. package/src/internal/capture.ts +38 -0
  34. package/{dist/internal/ctx.d.ts → src/internal/ctx.ts} +8 -4
  35. package/src/internal/errors.test.ts +33 -0
  36. package/src/internal/errors.ts +133 -0
  37. package/src/internal/inspect.test.ts +28 -0
  38. package/src/internal/inspect.ts +34 -0
  39. package/src/internal/queryDriver.ts +247 -0
  40. package/src/internal/reuse.test.ts +116 -0
  41. package/src/internal/reuse.ts +73 -0
  42. package/src/internal/scheduler.ts +93 -0
  43. package/{dist/internal/seeds.d.ts → src/internal/seeds.ts} +6 -3
  44. package/src/internal/sources.ts +104 -0
  45. package/{dist/internal/stateRegistry.js → src/internal/stateRegistry.ts} +18 -14
  46. package/src/internal/store.test.ts +80 -0
  47. package/src/internal/store.ts +105 -0
  48. package/{dist/internal/track.d.ts → src/internal/track.ts} +17 -11
  49. package/src/procedure/procedure.ts +89 -0
  50. package/src/reducer/reducer.ts +137 -0
  51. package/src/remote/remoteState.test.ts +695 -0
  52. package/src/remote/remoteState.ts +571 -0
  53. package/src/remote/remoteState.typecheck.ts +195 -0
  54. package/src/runtime/bus.ts +40 -0
  55. package/src/runtime/hardening.test.ts +69 -0
  56. package/src/runtime/loop.test.ts +178 -0
  57. package/src/runtime/loop.ts +171 -0
  58. package/src/scene/scene.ts +88 -0
  59. package/src/scene/seedScene.test.ts +176 -0
  60. package/src/state/state.ts +94 -0
  61. package/src/state/stateFamily.test.ts +138 -0
  62. package/src/state/stateFamily.ts +174 -0
  63. package/src/state/stateGroup.ts +125 -0
  64. package/{dist/state/token.d.ts → src/state/token.ts} +22 -13
  65. package/src/synced/syncedStore.ts +99 -0
  66. package/{dist/ui/node.d.ts → src/ui/node.ts} +3 -3
  67. package/{dist/ui/trigger.d.ts → src/ui/trigger.ts} +1 -2
  68. package/src/wire/tree.test.ts +81 -0
  69. package/src/wire/tree.ts +129 -0
  70. package/src/wire/triggers.test.ts +76 -0
  71. package/src/wire/triggers.ts +98 -0
  72. package/dist/boundary/boundary.d.ts +0 -72
  73. package/dist/boundary/boundary.d.ts.map +0 -1
  74. package/dist/boundary/boundary.js +0 -83
  75. package/dist/boundary/boundary.js.map +0 -1
  76. package/dist/calc/asyncCalc.d.ts +0 -91
  77. package/dist/calc/asyncCalc.d.ts.map +0 -1
  78. package/dist/calc/asyncCalc.js +0 -95
  79. package/dist/calc/asyncCalc.js.map +0 -1
  80. package/dist/calc/asyncData.d.ts +0 -57
  81. package/dist/calc/asyncData.d.ts.map +0 -1
  82. package/dist/calc/asyncData.js +0 -34
  83. package/dist/calc/asyncData.js.map +0 -1
  84. package/dist/calc/calc.d.ts +0 -57
  85. package/dist/calc/calc.d.ts.map +0 -1
  86. package/dist/calc/calc.js +0 -58
  87. package/dist/calc/calc.js.map +0 -1
  88. package/dist/calc/calcFamily.d.ts +0 -57
  89. package/dist/calc/calcFamily.d.ts.map +0 -1
  90. package/dist/calc/calcFamily.js +0 -124
  91. package/dist/calc/calcFamily.js.map +0 -1
  92. package/dist/calc/compose.d.ts +0 -16
  93. package/dist/calc/compose.d.ts.map +0 -1
  94. package/dist/calc/compose.js +0 -7
  95. package/dist/calc/compose.js.map +0 -1
  96. package/dist/channel/channel.d.ts +0 -115
  97. package/dist/channel/channel.d.ts.map +0 -1
  98. package/dist/channel/channel.js +0 -138
  99. package/dist/channel/channel.js.map +0 -1
  100. package/dist/compose/composition.d.ts +0 -72
  101. package/dist/compose/composition.d.ts.map +0 -1
  102. package/dist/compose/composition.js +0 -46
  103. package/dist/compose/composition.js.map +0 -1
  104. package/dist/compose/host.d.ts +0 -17
  105. package/dist/compose/host.d.ts.map +0 -1
  106. package/dist/compose/host.js +0 -6
  107. package/dist/compose/host.js.map +0 -1
  108. package/dist/compose/props.d.ts +0 -13
  109. package/dist/compose/props.d.ts.map +0 -1
  110. package/dist/compose/props.js.map +0 -1
  111. package/dist/compose/provide.d.ts +0 -22
  112. package/dist/compose/provide.d.ts.map +0 -1
  113. package/dist/compose/provide.js +0 -28
  114. package/dist/compose/provide.js.map +0 -1
  115. package/dist/compose/slot.d.ts.map +0 -1
  116. package/dist/compose/slot.js +0 -23
  117. package/dist/compose/slot.js.map +0 -1
  118. package/dist/compose/ui.d.ts +0 -135
  119. package/dist/compose/ui.d.ts.map +0 -1
  120. package/dist/compose/ui.js +0 -63
  121. package/dist/compose/ui.js.map +0 -1
  122. package/dist/definition/definition.d.ts +0 -33
  123. package/dist/definition/definition.d.ts.map +0 -1
  124. package/dist/definition/definition.js +0 -42
  125. package/dist/definition/definition.js.map +0 -1
  126. package/dist/event/event.d.ts +0 -33
  127. package/dist/event/event.d.ts.map +0 -1
  128. package/dist/event/event.js +0 -30
  129. package/dist/event/event.js.map +0 -1
  130. package/dist/event/eventGroup.d.ts +0 -9
  131. package/dist/event/eventGroup.d.ts.map +0 -1
  132. package/dist/event/eventGroup.js +0 -4
  133. package/dist/event/eventGroup.js.map +0 -1
  134. package/dist/feature/feature.d.ts.map +0 -1
  135. package/dist/feature/feature.js +0 -98
  136. package/dist/feature/feature.js.map +0 -1
  137. package/dist/index.d.ts +0 -53
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js +0 -62
  140. package/dist/index.js.map +0 -1
  141. package/dist/internal/capture.d.ts +0 -28
  142. package/dist/internal/capture.d.ts.map +0 -1
  143. package/dist/internal/capture.js +0 -12
  144. package/dist/internal/capture.js.map +0 -1
  145. package/dist/internal/ctx.d.ts.map +0 -1
  146. package/dist/internal/ctx.js +0 -2
  147. package/dist/internal/ctx.js.map +0 -1
  148. package/dist/internal/errors.d.ts +0 -89
  149. package/dist/internal/errors.d.ts.map +0 -1
  150. package/dist/internal/errors.js +0 -102
  151. package/dist/internal/errors.js.map +0 -1
  152. package/dist/internal/inspect.d.ts +0 -17
  153. package/dist/internal/inspect.d.ts.map +0 -1
  154. package/dist/internal/inspect.js +0 -32
  155. package/dist/internal/inspect.js.map +0 -1
  156. package/dist/internal/queryDriver.d.ts +0 -65
  157. package/dist/internal/queryDriver.d.ts.map +0 -1
  158. package/dist/internal/queryDriver.js +0 -134
  159. package/dist/internal/queryDriver.js.map +0 -1
  160. package/dist/internal/reuse.d.ts +0 -10
  161. package/dist/internal/reuse.d.ts.map +0 -1
  162. package/dist/internal/reuse.js +0 -68
  163. package/dist/internal/reuse.js.map +0 -1
  164. package/dist/internal/scheduler.d.ts +0 -47
  165. package/dist/internal/scheduler.d.ts.map +0 -1
  166. package/dist/internal/scheduler.js +0 -70
  167. package/dist/internal/scheduler.js.map +0 -1
  168. package/dist/internal/seeds.d.ts.map +0 -1
  169. package/dist/internal/seeds.js +0 -17
  170. package/dist/internal/seeds.js.map +0 -1
  171. package/dist/internal/sources.d.ts +0 -39
  172. package/dist/internal/sources.d.ts.map +0 -1
  173. package/dist/internal/sources.js +0 -59
  174. package/dist/internal/sources.js.map +0 -1
  175. package/dist/internal/stateRegistry.d.ts +0 -2
  176. package/dist/internal/stateRegistry.d.ts.map +0 -1
  177. package/dist/internal/stateRegistry.js.map +0 -1
  178. package/dist/internal/store.d.ts +0 -47
  179. package/dist/internal/store.d.ts.map +0 -1
  180. package/dist/internal/store.js +0 -73
  181. package/dist/internal/store.js.map +0 -1
  182. package/dist/internal/track.d.ts.map +0 -1
  183. package/dist/internal/track.js +0 -19
  184. package/dist/internal/track.js.map +0 -1
  185. package/dist/procedure/procedure.d.ts +0 -40
  186. package/dist/procedure/procedure.d.ts.map +0 -1
  187. package/dist/procedure/procedure.js +0 -50
  188. package/dist/procedure/procedure.js.map +0 -1
  189. package/dist/reducer/reducer.d.ts +0 -44
  190. package/dist/reducer/reducer.d.ts.map +0 -1
  191. package/dist/reducer/reducer.js +0 -63
  192. package/dist/reducer/reducer.js.map +0 -1
  193. package/dist/remote/remoteState.d.ts +0 -119
  194. package/dist/remote/remoteState.d.ts.map +0 -1
  195. package/dist/remote/remoteState.js +0 -270
  196. package/dist/remote/remoteState.js.map +0 -1
  197. package/dist/runtime/bus.d.ts +0 -27
  198. package/dist/runtime/bus.d.ts.map +0 -1
  199. package/dist/runtime/bus.js +0 -21
  200. package/dist/runtime/bus.js.map +0 -1
  201. package/dist/runtime/loop.d.ts +0 -45
  202. package/dist/runtime/loop.d.ts.map +0 -1
  203. package/dist/runtime/loop.js +0 -117
  204. package/dist/runtime/loop.js.map +0 -1
  205. package/dist/scene/scene.d.ts +0 -44
  206. package/dist/scene/scene.d.ts.map +0 -1
  207. package/dist/scene/scene.js +0 -31
  208. package/dist/scene/scene.js.map +0 -1
  209. package/dist/state/state.d.ts +0 -37
  210. package/dist/state/state.d.ts.map +0 -1
  211. package/dist/state/state.js +0 -46
  212. package/dist/state/state.js.map +0 -1
  213. package/dist/state/stateFamily.d.ts +0 -79
  214. package/dist/state/stateFamily.d.ts.map +0 -1
  215. package/dist/state/stateFamily.js +0 -99
  216. package/dist/state/stateFamily.js.map +0 -1
  217. package/dist/state/stateGroup.d.ts +0 -50
  218. package/dist/state/stateGroup.d.ts.map +0 -1
  219. package/dist/state/stateGroup.js +0 -54
  220. package/dist/state/stateGroup.js.map +0 -1
  221. package/dist/state/token.d.ts.map +0 -1
  222. package/dist/state/token.js +0 -20
  223. package/dist/state/token.js.map +0 -1
  224. package/dist/synced/syncedStore.d.ts +0 -51
  225. package/dist/synced/syncedStore.d.ts.map +0 -1
  226. package/dist/synced/syncedStore.js +0 -43
  227. package/dist/synced/syncedStore.js.map +0 -1
  228. package/dist/ui/node.d.ts.map +0 -1
  229. package/dist/ui/node.js +0 -2
  230. package/dist/ui/node.js.map +0 -1
  231. package/dist/ui/trigger.d.ts.map +0 -1
  232. package/dist/ui/trigger.js +0 -2
  233. package/dist/ui/trigger.js.map +0 -1
  234. package/dist/wire/tree.d.ts +0 -66
  235. package/dist/wire/tree.d.ts.map +0 -1
  236. package/dist/wire/tree.js +0 -60
  237. package/dist/wire/tree.js.map +0 -1
  238. package/dist/wire/triggers.d.ts +0 -44
  239. package/dist/wire/triggers.d.ts.map +0 -1
  240. package/dist/wire/triggers.js +0 -31
  241. package/dist/wire/triggers.js.map +0 -1
@@ -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,191 @@
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
+ Frame,
60
+ RenderEnv,
61
+ } from './compose/composition'
62
+ export {
63
+ absent,
64
+ each,
65
+ type EachFill,
66
+ isStructure,
67
+ mount,
68
+ one,
69
+ type OneFill,
70
+ type AbsentFill,
71
+ type SlotFill,
72
+ type Structure,
73
+ structureEquals,
74
+ type StructureSlots,
75
+ StructureTypeId,
76
+ when,
77
+ } from './compose/structure'
78
+ export { Props } from './compose/props'
79
+ export {
80
+ isSlot,
81
+ type Node,
82
+ slot,
83
+ type SlotClass,
84
+ type SlotInstance,
85
+ type SlotProps,
86
+ SlotTypeId,
87
+ } from './compose/slot'
88
+ export { type SlotRenderer } from './compose/host'
89
+ export * as Ui from './compose/ui'
90
+ export {
91
+ type DerivedContract,
92
+ isUi,
93
+ type MadeView,
94
+ ui,
95
+ type UiClass,
96
+ type UiContract,
97
+ type UiManifest,
98
+ UiTypeId,
99
+ UiViewContract,
100
+ type ViewImpl,
101
+ type WireSchemas,
102
+ type WiredUi,
103
+ type WiredUiManifest,
104
+ } from './compose/ui'
105
+ export { provide } from './compose/provide'
106
+ export type { Trigger } from './ui/trigger'
107
+
108
+ // Wire: the serializable rendered-tree model + pure diff/apply the remote
109
+ // transport sends over the wire and the client folds back. See REMOTE_UI.md §3.
110
+ export * as Wire from './wire/tree'
111
+ export type { WireNode, WirePatch, WireProp, WireTree } from './wire/tree'
112
+
113
+ // Triggers: the server-side handle registry bridging client trigger invocations
114
+ // back to live triggers, decoding payloads via the event schema. See REMOTE_UI.md §4.
115
+ export * as Triggers from './wire/triggers'
116
+ export {
117
+ TriggerRegistry,
118
+ type TriggerHandle,
119
+ type TriggerRegistryApi,
120
+ UnknownTrigger,
121
+ } from './wire/triggers'
122
+
123
+ // Feature: the lazy/eager code-split unit. Definitions stay eager and reflectable;
124
+ // the heavy `.live` layer loads on demand (an Effect, never a Promise). See LAZY.md.
125
+ export * as Feature from './feature/feature'
126
+ export {
127
+ type EngineServices,
128
+ type FailedProps,
129
+ type FeatureBinding,
130
+ FeatureBindingTypeId,
131
+ type FeatureClass,
132
+ type FeatureManifest,
133
+ featureModule,
134
+ type FeatureModule,
135
+ isFeature,
136
+ isFeatureBinding,
137
+ lazyImport,
138
+ type LoadedModule,
139
+ mountFeature,
140
+ type Placeholders,
141
+ type ProvidedBy,
142
+ type RequireCarrier,
143
+ } from './feature/feature'
144
+ export { type SlotChild } from './compose/slot'
145
+
146
+ // A scene bundles a composition with the closed wiring that runs it — the single
147
+ // handle the react host, proofs, and the dev tool all consume.
148
+ export { isScene, type MountedServices, type Scene, scene, seedScene } from './scene/scene'
149
+
150
+ // Runtime surface for hosts (`@reform/react`) and headless tests.
151
+ export { Bus, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
152
+ export { Engine, type ReducerEntry, Reducers } from './runtime/loop'
153
+ export {
154
+ type ChannelClass,
155
+ type ChannelPolicy,
156
+ type ChannelRuntime,
157
+ Channels,
158
+ type ProcedureEntry,
159
+ Procedures,
160
+ } from './channel/channel'
161
+ export { CurrentTracker, type Subscribable, type Tracker } from './internal/track'
162
+ export { CaptureSink, type CaptureSinkApi, type UiCapture } from './internal/capture'
163
+ export {
164
+ AsyncReducer,
165
+ AsyncSceneLayer,
166
+ DuplicateRegistration,
167
+ FeatureLoadFailed,
168
+ forceSync,
169
+ InvalidProvideTarget,
170
+ UnknownGroupState,
171
+ } from './internal/errors'
172
+ // Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
173
+ // multiple mounted roots) provide `Notifications` upstream of their state layers;
174
+ // everything else coalesces on the process-wide default.
175
+ export {
176
+ defaultScheduler,
177
+ makeScheduler,
178
+ Notifications,
179
+ notificationsLayer,
180
+ resolveScheduler,
181
+ type Scheduler,
182
+ } from './internal/scheduler'
183
+ export { makeStore, type Store } from './internal/store'
184
+ export type { FamilyStore } from './state/stateFamily'
185
+ // Primitive-authoring toolkit — exported for first-party companion packages
186
+ // (e.g. `@playfast/reform-db`, `@playfast/reform-resource`) that build their own
187
+ // first-class `Source`s which must participate in dependency tracking and
188
+ // reflection exactly like a `Calc`.
189
+ export { type Kind, type Manifest, yieldableClass } from './definition/definition'
190
+ export { readTracked } from './internal/track'
191
+ export { sameKey, wireSources, type WiredSources } from './internal/sources'
@@ -0,0 +1,38 @@
1
+ import { Context } from 'effect'
2
+ import type { Trigger } from '../ui/trigger'
3
+
4
+ /**
5
+ * A single rendered UI node's observable surface: which contract rendered, the
6
+ * props it received, and the event triggers it was handed. This is exactly what
7
+ * a headless inspector (the proof facade, the dev tool) needs — no DOM, no JSX.
8
+ */
9
+ export interface UiCapture {
10
+ readonly name: string
11
+ readonly props: unknown
12
+ readonly events: Record<string, Trigger<unknown>>
13
+ /**
14
+ * The stable slot-fill key this render was mounted under (an `each` item's
15
+ * `key`, or a synthesized singleton key for a `one`). Present only on the
16
+ * structure path — the legacy Node path defers slot expansion to host
17
+ * components and has no fill key, so it is omitted there. Lets the proof
18
+ * facade select a child by key (`byKey`) instead of by render index.
19
+ */
20
+ readonly key?: string
21
+ }
22
+
23
+ /** Where captured renders are reported, when a capturing host is present. */
24
+ export interface CaptureSinkApi {
25
+ record(capture: UiCapture): void
26
+ }
27
+
28
+ /**
29
+ * An OPTIONAL service. Core's `ui` read consults it via `serviceOption`: when a
30
+ * capturing host provides it (proofs, the dev tool), every resolved view reports
31
+ * its `(props, events)` before rendering, giving a faithful tree without
32
+ * replacing any presentation. Production never provides it, so nothing is
33
+ * recorded and there is no cost beyond one `serviceOption` lookup per render.
34
+ */
35
+ const CaptureSinkBase: Context.TagClass<CaptureSink, 'reform/internal/CaptureSink', CaptureSinkApi> =
36
+ Context.Tag('reform/internal/CaptureSink')<CaptureSink, CaptureSinkApi>()
37
+
38
+ export class CaptureSink extends CaptureSinkBase {}
@@ -1,5 +1,6 @@
1
- import type { Effect } from 'effect';
2
- import type { YieldWrap } from 'effect/Utils';
1
+ import type { Effect } from 'effect'
2
+ import type { YieldWrap } from 'effect/Utils'
3
+
3
4
  /**
4
5
  * The context (`R`) a generator body requires, recovered from the effects it
5
6
  * yields. Shared by `Composition.live` and `Procedure.live`, whose bodies are
@@ -8,5 +9,8 @@ import type { YieldWrap } from 'effect/Utils';
8
9
  * A body that never yields (`Eff = never`) requires nothing — guard that case
9
10
  * first, or `infer R` resolves to `unknown` and leaks into the layer's `RIn`.
10
11
  */
11
- export type Ctx<Eff> = [Eff] extends [never] ? never : [Eff] extends [YieldWrap<Effect.Effect<any, any, infer R>>] ? R : never;
12
- //# sourceMappingURL=ctx.d.ts.map
12
+ export type Ctx<Eff> = [Eff] extends [never]
13
+ ? never
14
+ : [Eff] extends [YieldWrap<Effect.Effect<any, any, infer R>>]
15
+ ? R
16
+ : never
@@ -0,0 +1,33 @@
1
+ import { expect, it } from '@effect/vitest'
2
+ import { Context, Effect, Layer, ManagedRuntime } from 'effect'
3
+ import { AsyncSceneLayer, forceSync } from '../index'
4
+
5
+ // `forceSync` is the sync-build boundary: reform materialises a scene's layer graph
6
+ // with `runSync` so the first frame renders synchronously (D2). A layer whose acquire
7
+ // suspends makes that `runSync` throw Effect's opaque `AsyncFiberException`; the
8
+ // boundary translates it into `AsyncSceneLayer`, which names the cause and the fix.
9
+
10
+ class Thing extends Context.Tag('test/Thing')<Thing, number>() {}
11
+
12
+ it('translates an async layer build into AsyncSceneLayer', () => {
13
+ // A layer whose acquire suspends — exactly what a consumer trips on.
14
+ const asyncLayer = Layer.effect(Thing, Effect.as(Effect.sleep('1 millis'), 1))
15
+ const runtime = ManagedRuntime.make(asyncLayer)
16
+ expect(() => forceSync(() => runtime.runSync(Effect.context<Thing>()))).toThrow(AsyncSceneLayer)
17
+ })
18
+
19
+ it('passes a synchronous layer build through unchanged', () => {
20
+ const syncLayer = Layer.succeed(Thing, 1)
21
+ const runtime = ManagedRuntime.make(syncLayer)
22
+ const context = forceSync(() => runtime.runSync(Effect.context<Thing>()))
23
+ expect(Context.get(context, Thing)).toBe(1)
24
+ })
25
+
26
+ it('re-throws a non-async error untouched', () => {
27
+ class Boom extends Error {}
28
+ expect(() =>
29
+ forceSync(() => {
30
+ throw new Boom('nope')
31
+ }),
32
+ ).toThrow(Boom)
33
+ })
@@ -0,0 +1,133 @@
1
+ import { Cause, Data, Option, Runtime } from 'effect'
2
+
3
+ /**
4
+ * Tagged errors for the framework's synchronous boundaries. Even when thrown
5
+ * (React render, layer build, reflection), an error carries a `_tag` and its
6
+ * structured fields — never a bare `Error` — so callers and tooling can match on
7
+ * it. `Data.TaggedError` subclasses are real `Error`s, so `throw` still works.
8
+ */
9
+
10
+ /**
11
+ * The constructor shape `Data.TaggedError(tag)<A>` produces, named so the
12
+ * generated `.d.ts` can describe the `extends` base under `isolatedDeclarations`
13
+ * (which forbids an inferred expression in an extends clause).
14
+ */
15
+ type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = new (
16
+ args: A,
17
+ ) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A>
18
+
19
+ /** The fieldless variant — a `Data.TaggedError(tag)<{}>` constructor. */
20
+ type EmptyTaggedErrorClass<Tag extends string> = new () => Cause.YieldableError & { readonly _tag: Tag }
21
+
22
+ const InvalidProvideTargetBase: EmptyTaggedErrorClass<'reform/InvalidProvideTarget'> =
23
+ Data.TaggedError('reform/InvalidProvideTarget')<{}>
24
+
25
+ /** `provide(target, …)` was handed something that is neither a ui contract nor a slot. */
26
+ export class InvalidProvideTarget extends InvalidProvideTargetBase {
27
+ override get message(): string {
28
+ return 'reform: provide target is neither a ui contract nor a slot'
29
+ }
30
+ }
31
+
32
+ const UnknownGroupStateBase: TaggedErrorClass<'reform/UnknownGroupState', { readonly name: string }> =
33
+ Data.TaggedError('reform/UnknownGroupState')<{ readonly name: string }>
34
+
35
+ /** `StateGroup.select(group, name)` named a state the group does not contain. */
36
+ export class UnknownGroupState extends UnknownGroupStateBase {
37
+ override get message(): string {
38
+ return `reform: unknown state '${this.name}' in group`
39
+ }
40
+ }
41
+
42
+ const FeatureLoadFailedBase: TaggedErrorClass<'reform/FeatureLoadFailed', { readonly cause: unknown }> =
43
+ Data.TaggedError('reform/FeatureLoadFailed')<{ readonly cause: unknown }>
44
+
45
+ /**
46
+ * A lazy `Feature`'s `load` Effect failed — the dynamic `import()` rejected (a
47
+ * missing chunk, a network failure, a module-eval throw). Surfaced as a typed
48
+ * failure the host hands to the feature's `placeholder.failed`, never a bare
49
+ * rejection. `cause` is the original rejection value.
50
+ */
51
+ export class FeatureLoadFailed extends FeatureLoadFailedBase {
52
+ override get message(): string {
53
+ return `reform: a feature failed to load (${String(this.cause)})`
54
+ }
55
+ }
56
+
57
+ const AsyncReducerBase: EmptyTaggedErrorClass<'reform/AsyncReducer'> =
58
+ Data.TaggedError('reform/AsyncReducer')<{}>
59
+
60
+ /** A reducer fold returned a thenable — folds must be pure synchronous writes. */
61
+ export class AsyncReducer extends AsyncReducerBase {
62
+ override get message(): string {
63
+ return 'reform: a reducer fold must be synchronous (it returned a Promise)'
64
+ }
65
+ }
66
+
67
+ const AsyncSceneLayerBase: EmptyTaggedErrorClass<'reform/AsyncSceneLayer'> =
68
+ Data.TaggedError('reform/AsyncSceneLayer')<{}>
69
+
70
+ /**
71
+ * A scene layer performed async work during reform's synchronous layer build.
72
+ * Reform materialises a scene's layer graph with `runSync` so the first frame
73
+ * renders synchronously (D2); a `Layer.effect`/`scoped` whose acquire suspends
74
+ * (HTTP, opening a DB, `Effect.sleep`) makes that `runSync` throw Effect's opaque
75
+ * `AsyncFiberException`. `forceSync` catches it and rethrows this, which names the
76
+ * cause and the sanctioned fix instead of pointing at Effect internals.
77
+ */
78
+ export class AsyncSceneLayer extends AsyncSceneLayerBase {
79
+ override get message(): string {
80
+ return (
81
+ 'reform: a scene layer performed async work while building. Reform builds the ' +
82
+ 'scene synchronously so the first frame can render — a layer acquire must be ' +
83
+ 'synchronous (Layer.sync/Layer.succeed). For async setup (open a connection, ' +
84
+ 'fetch config), use Resource.live from @playfast/reform-resource, which builds ' +
85
+ 'synchronously and resolves in the background.'
86
+ )
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Whether a thrown value is Effect's "ran async under runSync" defect. `runSync`
92
+ * throws the bare `AsyncFiberException`; a `ManagedRuntime.runSync` whose layer
93
+ * build suspends instead throws a `FiberFailure` carrying it as a `Die`, so both
94
+ * shapes are checked.
95
+ */
96
+ const isAsyncBuildDefect = (error: unknown): boolean => {
97
+ if (Runtime.isAsyncFiberException(error)) return true
98
+ if (!Runtime.isFiberFailure(error)) return false
99
+ const die = Cause.dieOption(error[Runtime.FiberFailureCauseId])
100
+ return Option.isSome(die) && Runtime.isAsyncFiberException(die.value)
101
+ }
102
+
103
+ /**
104
+ * Run a synchronous runtime read (the scene layer build / context capture) and
105
+ * translate Effect's async-fiber defect into `AsyncSceneLayer`; any other throw
106
+ * passes through unchanged. The single home for the sync-build boundary, shared by
107
+ * the React host and the remote server.
108
+ */
109
+ export const forceSync = <A>(thunk: () => A): A => {
110
+ try {
111
+ return thunk()
112
+ } catch (error) {
113
+ if (isAsyncBuildDefect(error)) throw new AsyncSceneLayer()
114
+ throw error
115
+ }
116
+ }
117
+
118
+ const DuplicateRegistrationBase: TaggedErrorClass<
119
+ 'reform/DuplicateRegistration',
120
+ { readonly kind: string; readonly name: string }
121
+ > = Data.TaggedError('reform/DuplicateRegistration')<{ readonly kind: string; readonly name: string }>
122
+
123
+ /**
124
+ * Two distinct primitives were registered under one name into the same runtime —
125
+ * a silent footgun, since names key DI tags and channel lanes. Thrown for a
126
+ * channel name reused with a different policy (the second would otherwise be
127
+ * dropped). Reducer/procedure name clashes are warned (logged), not thrown.
128
+ */
129
+ export class DuplicateRegistration extends DuplicateRegistrationBase {
130
+ override get message(): string {
131
+ return `reform: duplicate ${this.kind} registered under name '${this.name}'`
132
+ }
133
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from '@effect/vitest'
2
+ import { Schema as S } from 'effect'
3
+ import { State } from '../index'
4
+ import { makeStore } from './store'
5
+
6
+ // Every reform value should print like an Effect value (an `_id`-tagged JSON),
7
+ // not `[object Object]` — via `toJSON`/`toString`/the Node inspect symbol.
8
+
9
+ describe('Inspectable', () => {
10
+ it('a Store reports its current value as tagged JSON', () => {
11
+ const store = makeStore(41)
12
+ expect(store.toJSON()).toEqual({ _id: 'reform/Store', value: 41 })
13
+ store.set(42)
14
+ expect(store.toJSON()).toEqual({ _id: 'reform/Store', value: 42 })
15
+ // toString and JSON.stringify route through the same toJSON.
16
+ expect(JSON.parse(JSON.stringify(store))).toEqual({ _id: 'reform/Store', value: 42 })
17
+ expect(store.toString()).toContain('reform/Store')
18
+ })
19
+
20
+ it('a definition reports its manifest', () => {
21
+ class Count extends State.make('count', S.Number) {}
22
+ // Inspectable is attached at runtime (definitions are reflected via their
23
+ // manifest); reach the method through a typed view.
24
+ const inspectable = Count as unknown as { toJSON(): unknown }
25
+ expect(inspectable.toJSON()).toBe(Count.manifest)
26
+ expect(String(Count)).toContain('"kind": "State"')
27
+ })
28
+ })
@@ -0,0 +1,34 @@
1
+ import { format, type Inspectable, NodeInspectSymbol } from 'effect/Inspectable'
2
+
3
+ export { type Inspectable, NodeInspectSymbol }
4
+
5
+ /**
6
+ * The `Inspectable` trio built around a `toJSON`, so every reform value prints
7
+ * like an Effect value — `_id`-tagged JSON under `console.log`, Node's
8
+ * inspector, `JSON.stringify`, and `String(...)` — instead of `[object Object]`.
9
+ * Mirrors Effect's `BaseProto` / `Inspectable.Class` (`Inspectable.ts:170`).
10
+ */
11
+ export const inspectable = (toJSON: () => unknown): Inspectable => ({
12
+ toJSON,
13
+ toString: () => format(toJSON()),
14
+ [NodeInspectSymbol]: () => toJSON(),
15
+ })
16
+
17
+ /**
18
+ * Attach the `Inspectable` trio to an existing object as *non-enumerable* own
19
+ * properties (so they don't show up in `Object.keys`/`for…in` over a definition
20
+ * class). Used where the value is built by other means — the definition classes,
21
+ * whose statics are merged in separately.
22
+ */
23
+ export const attachInspectable = <T extends object>(target: T, toJSON: () => unknown): T => {
24
+ const trio = inspectable(toJSON) as unknown as Record<PropertyKey, unknown>
25
+ for (const key of Reflect.ownKeys(trio)) {
26
+ Object.defineProperty(target, key, {
27
+ value: trio[key],
28
+ enumerable: false,
29
+ writable: true,
30
+ configurable: true,
31
+ })
32
+ }
33
+ return target
34
+ }