@playfast/reform 0.0.2 → 0.0.3

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 (230) hide show
  1. package/package.json +15 -13
  2. package/src/boundary/boundary.ts +192 -0
  3. package/src/calc/asyncCalc.ts +267 -0
  4. package/{dist/dts/calc/asyncData.d.ts → src/calc/asyncData.ts} +53 -20
  5. package/src/calc/calc.ts +120 -0
  6. package/src/calc/calcFamily.ts +194 -0
  7. package/src/calc/compose.ts +69 -0
  8. package/src/channel/channel.ts +265 -0
  9. package/src/compose/composition.ts +112 -0
  10. package/{dist/dts/compose/host.d.ts → src/compose/host.ts} +6 -8
  11. package/{dist/esm/compose/props.js → src/compose/props.ts} +3 -4
  12. package/src/compose/provide.ts +70 -0
  13. package/{dist/dts/compose/slot.d.ts → src/compose/slot.ts} +36 -19
  14. package/src/compose/ui.ts +97 -0
  15. package/src/definition/definition.ts +74 -0
  16. package/src/event/event.ts +65 -0
  17. package/src/event/eventGroup.ts +14 -0
  18. package/{dist/dts/feature/feature.d.ts → src/feature/feature.ts} +178 -92
  19. package/src/index.ts +140 -0
  20. package/{dist/dts/internal/capture.d.ts → src/internal/capture.ts} +13 -11
  21. package/{dist/dts/internal/ctx.d.ts → src/internal/ctx.ts} +8 -4
  22. package/{dist/esm/internal/errors.js → src/internal/errors.ts} +41 -26
  23. package/src/internal/inspect.ts +34 -0
  24. package/src/internal/queryDriver.ts +247 -0
  25. package/src/internal/reuse.ts +73 -0
  26. package/src/internal/scheduler.ts +91 -0
  27. package/{dist/dts/internal/seeds.d.ts → src/internal/seeds.ts} +6 -3
  28. package/src/internal/sources.ts +104 -0
  29. package/src/internal/store.ts +105 -0
  30. package/{dist/dts/internal/track.d.ts → src/internal/track.ts} +15 -11
  31. package/src/procedure/procedure.ts +89 -0
  32. package/src/reducer/reducer.ts +137 -0
  33. package/src/remote/remoteState.ts +571 -0
  34. package/{dist/cjs/runtime/bus.js → src/runtime/bus.ts} +23 -12
  35. package/src/runtime/loop.ts +169 -0
  36. package/src/scene/scene.ts +76 -0
  37. package/src/state/state.ts +91 -0
  38. package/src/state/stateFamily.ts +171 -0
  39. package/src/state/stateGroup.ts +78 -0
  40. package/{dist/dts/state/token.d.ts → src/state/token.ts} +21 -13
  41. package/{dist/dts/ui/node.d.ts → src/ui/node.ts} +3 -3
  42. package/{dist/dts/ui/trigger.d.ts → src/ui/trigger.ts} +1 -2
  43. package/dist/cjs/boundary/boundary.js +0 -86
  44. package/dist/cjs/calc/asyncCalc.js +0 -128
  45. package/dist/cjs/calc/asyncData.js +0 -37
  46. package/dist/cjs/calc/calc.js +0 -58
  47. package/dist/cjs/calc/calcFamily.js +0 -127
  48. package/dist/cjs/channel/channel.js +0 -142
  49. package/dist/cjs/compose/composition.js +0 -50
  50. package/dist/cjs/compose/host.js +0 -8
  51. package/dist/cjs/compose/props.js +0 -14
  52. package/dist/cjs/compose/provide.js +0 -30
  53. package/dist/cjs/compose/slot.js +0 -27
  54. package/dist/cjs/compose/ui.js +0 -61
  55. package/dist/cjs/definition/definition.js +0 -46
  56. package/dist/cjs/event/event.js +0 -36
  57. package/dist/cjs/event/eventGroup.js +0 -7
  58. package/dist/cjs/feature/feature.js +0 -102
  59. package/dist/cjs/index.js +0 -116
  60. package/dist/cjs/internal/capture.js +0 -14
  61. package/dist/cjs/internal/ctx.js +0 -2
  62. package/dist/cjs/internal/errors.js +0 -62
  63. package/dist/cjs/internal/inspect.js +0 -36
  64. package/dist/cjs/internal/queryDriver.js +0 -138
  65. package/dist/cjs/internal/reuse.js +0 -71
  66. package/dist/cjs/internal/scheduler.js +0 -73
  67. package/dist/cjs/internal/seeds.js +0 -19
  68. package/dist/cjs/internal/sources.js +0 -61
  69. package/dist/cjs/internal/store.js +0 -77
  70. package/dist/cjs/internal/track.js +0 -22
  71. package/dist/cjs/package.json +0 -4
  72. package/dist/cjs/procedure/procedure.js +0 -52
  73. package/dist/cjs/reducer/reducer.js +0 -64
  74. package/dist/cjs/remote/remoteState.js +0 -307
  75. package/dist/cjs/runtime/loop.js +0 -119
  76. package/dist/cjs/scene/scene.js +0 -36
  77. package/dist/cjs/state/state.js +0 -47
  78. package/dist/cjs/state/stateFamily.js +0 -101
  79. package/dist/cjs/state/stateGroup.js +0 -47
  80. package/dist/cjs/state/token.js +0 -23
  81. package/dist/cjs/ui/node.js +0 -2
  82. package/dist/cjs/ui/trigger.js +0 -2
  83. package/dist/dts/boundary/boundary.d.ts +0 -72
  84. package/dist/dts/boundary/boundary.d.ts.map +0 -1
  85. package/dist/dts/calc/asyncCalc.d.ts +0 -91
  86. package/dist/dts/calc/asyncCalc.d.ts.map +0 -1
  87. package/dist/dts/calc/asyncData.d.ts.map +0 -1
  88. package/dist/dts/calc/calc.d.ts +0 -57
  89. package/dist/dts/calc/calc.d.ts.map +0 -1
  90. package/dist/dts/calc/calcFamily.d.ts +0 -57
  91. package/dist/dts/calc/calcFamily.d.ts.map +0 -1
  92. package/dist/dts/channel/channel.d.ts +0 -115
  93. package/dist/dts/channel/channel.d.ts.map +0 -1
  94. package/dist/dts/compose/composition.d.ts +0 -72
  95. package/dist/dts/compose/composition.d.ts.map +0 -1
  96. package/dist/dts/compose/host.d.ts.map +0 -1
  97. package/dist/dts/compose/props.d.ts +0 -13
  98. package/dist/dts/compose/props.d.ts.map +0 -1
  99. package/dist/dts/compose/provide.d.ts +0 -22
  100. package/dist/dts/compose/provide.d.ts.map +0 -1
  101. package/dist/dts/compose/slot.d.ts.map +0 -1
  102. package/dist/dts/compose/ui.d.ts +0 -50
  103. package/dist/dts/compose/ui.d.ts.map +0 -1
  104. package/dist/dts/definition/definition.d.ts +0 -33
  105. package/dist/dts/definition/definition.d.ts.map +0 -1
  106. package/dist/dts/event/event.d.ts +0 -33
  107. package/dist/dts/event/event.d.ts.map +0 -1
  108. package/dist/dts/event/eventGroup.d.ts +0 -9
  109. package/dist/dts/event/eventGroup.d.ts.map +0 -1
  110. package/dist/dts/feature/feature.d.ts.map +0 -1
  111. package/dist/dts/index.d.ts +0 -43
  112. package/dist/dts/index.d.ts.map +0 -1
  113. package/dist/dts/internal/capture.d.ts.map +0 -1
  114. package/dist/dts/internal/ctx.d.ts.map +0 -1
  115. package/dist/dts/internal/errors.d.ts +0 -69
  116. package/dist/dts/internal/errors.d.ts.map +0 -1
  117. package/dist/dts/internal/inspect.d.ts +0 -17
  118. package/dist/dts/internal/inspect.d.ts.map +0 -1
  119. package/dist/dts/internal/queryDriver.d.ts +0 -65
  120. package/dist/dts/internal/queryDriver.d.ts.map +0 -1
  121. package/dist/dts/internal/reuse.d.ts +0 -10
  122. package/dist/dts/internal/reuse.d.ts.map +0 -1
  123. package/dist/dts/internal/scheduler.d.ts +0 -47
  124. package/dist/dts/internal/scheduler.d.ts.map +0 -1
  125. package/dist/dts/internal/seeds.d.ts.map +0 -1
  126. package/dist/dts/internal/sources.d.ts +0 -39
  127. package/dist/dts/internal/sources.d.ts.map +0 -1
  128. package/dist/dts/internal/store.d.ts +0 -47
  129. package/dist/dts/internal/store.d.ts.map +0 -1
  130. package/dist/dts/internal/track.d.ts.map +0 -1
  131. package/dist/dts/procedure/procedure.d.ts +0 -40
  132. package/dist/dts/procedure/procedure.d.ts.map +0 -1
  133. package/dist/dts/reducer/reducer.d.ts +0 -44
  134. package/dist/dts/reducer/reducer.d.ts.map +0 -1
  135. package/dist/dts/remote/remoteState.d.ts +0 -119
  136. package/dist/dts/remote/remoteState.d.ts.map +0 -1
  137. package/dist/dts/runtime/bus.d.ts +0 -27
  138. package/dist/dts/runtime/bus.d.ts.map +0 -1
  139. package/dist/dts/runtime/loop.d.ts +0 -45
  140. package/dist/dts/runtime/loop.d.ts.map +0 -1
  141. package/dist/dts/scene/scene.d.ts +0 -44
  142. package/dist/dts/scene/scene.d.ts.map +0 -1
  143. package/dist/dts/state/state.d.ts +0 -37
  144. package/dist/dts/state/state.d.ts.map +0 -1
  145. package/dist/dts/state/stateFamily.d.ts +0 -79
  146. package/dist/dts/state/stateFamily.d.ts.map +0 -1
  147. package/dist/dts/state/stateGroup.d.ts +0 -36
  148. package/dist/dts/state/stateGroup.d.ts.map +0 -1
  149. package/dist/dts/state/token.d.ts.map +0 -1
  150. package/dist/dts/ui/node.d.ts.map +0 -1
  151. package/dist/dts/ui/trigger.d.ts.map +0 -1
  152. package/dist/esm/boundary/boundary.js +0 -83
  153. package/dist/esm/boundary/boundary.js.map +0 -1
  154. package/dist/esm/calc/asyncCalc.js +0 -95
  155. package/dist/esm/calc/asyncCalc.js.map +0 -1
  156. package/dist/esm/calc/asyncData.js +0 -34
  157. package/dist/esm/calc/asyncData.js.map +0 -1
  158. package/dist/esm/calc/calc.js +0 -58
  159. package/dist/esm/calc/calc.js.map +0 -1
  160. package/dist/esm/calc/calcFamily.js +0 -124
  161. package/dist/esm/calc/calcFamily.js.map +0 -1
  162. package/dist/esm/channel/channel.js +0 -136
  163. package/dist/esm/channel/channel.js.map +0 -1
  164. package/dist/esm/compose/composition.js +0 -46
  165. package/dist/esm/compose/composition.js.map +0 -1
  166. package/dist/esm/compose/host.js +0 -5
  167. package/dist/esm/compose/host.js.map +0 -1
  168. package/dist/esm/compose/props.js.map +0 -1
  169. package/dist/esm/compose/provide.js +0 -28
  170. package/dist/esm/compose/provide.js.map +0 -1
  171. package/dist/esm/compose/slot.js +0 -23
  172. package/dist/esm/compose/slot.js.map +0 -1
  173. package/dist/esm/compose/ui.js +0 -57
  174. package/dist/esm/compose/ui.js.map +0 -1
  175. package/dist/esm/definition/definition.js +0 -42
  176. package/dist/esm/definition/definition.js.map +0 -1
  177. package/dist/esm/event/event.js +0 -30
  178. package/dist/esm/event/event.js.map +0 -1
  179. package/dist/esm/event/eventGroup.js +0 -4
  180. package/dist/esm/event/eventGroup.js.map +0 -1
  181. package/dist/esm/feature/feature.js +0 -98
  182. package/dist/esm/feature/feature.js.map +0 -1
  183. package/dist/esm/index.js +0 -45
  184. package/dist/esm/index.js.map +0 -1
  185. package/dist/esm/internal/capture.js +0 -11
  186. package/dist/esm/internal/capture.js.map +0 -1
  187. package/dist/esm/internal/ctx.js +0 -2
  188. package/dist/esm/internal/ctx.js.map +0 -1
  189. package/dist/esm/internal/errors.js.map +0 -1
  190. package/dist/esm/internal/inspect.js +0 -32
  191. package/dist/esm/internal/inspect.js.map +0 -1
  192. package/dist/esm/internal/queryDriver.js +0 -134
  193. package/dist/esm/internal/queryDriver.js.map +0 -1
  194. package/dist/esm/internal/reuse.js +0 -68
  195. package/dist/esm/internal/reuse.js.map +0 -1
  196. package/dist/esm/internal/scheduler.js +0 -69
  197. package/dist/esm/internal/scheduler.js.map +0 -1
  198. package/dist/esm/internal/seeds.js +0 -17
  199. package/dist/esm/internal/seeds.js.map +0 -1
  200. package/dist/esm/internal/sources.js +0 -59
  201. package/dist/esm/internal/sources.js.map +0 -1
  202. package/dist/esm/internal/store.js +0 -73
  203. package/dist/esm/internal/store.js.map +0 -1
  204. package/dist/esm/internal/track.js +0 -18
  205. package/dist/esm/internal/track.js.map +0 -1
  206. package/dist/esm/package.json +0 -4
  207. package/dist/esm/procedure/procedure.js +0 -50
  208. package/dist/esm/procedure/procedure.js.map +0 -1
  209. package/dist/esm/reducer/reducer.js +0 -63
  210. package/dist/esm/reducer/reducer.js.map +0 -1
  211. package/dist/esm/remote/remoteState.js +0 -270
  212. package/dist/esm/remote/remoteState.js.map +0 -1
  213. package/dist/esm/runtime/bus.js +0 -20
  214. package/dist/esm/runtime/bus.js.map +0 -1
  215. package/dist/esm/runtime/loop.js +0 -116
  216. package/dist/esm/runtime/loop.js.map +0 -1
  217. package/dist/esm/scene/scene.js +0 -31
  218. package/dist/esm/scene/scene.js.map +0 -1
  219. package/dist/esm/state/state.js +0 -43
  220. package/dist/esm/state/state.js.map +0 -1
  221. package/dist/esm/state/stateFamily.js +0 -96
  222. package/dist/esm/state/stateFamily.js.map +0 -1
  223. package/dist/esm/state/stateGroup.js +0 -46
  224. package/dist/esm/state/stateGroup.js.map +0 -1
  225. package/dist/esm/state/token.js +0 -20
  226. package/dist/esm/state/token.js.map +0 -1
  227. package/dist/esm/ui/node.js +0 -2
  228. package/dist/esm/ui/node.js.map +0 -1
  229. package/dist/esm/ui/trigger.js +0 -2
  230. package/dist/esm/ui/trigger.js.map +0 -1
@@ -0,0 +1,112 @@
1
+ import { Context, Effect, Layer, type Schema } from 'effect'
2
+ import type { YieldWrap } from 'effect/Utils'
3
+ import { type Manifest, definitionClass } from '../definition/definition'
4
+ import { type Ctx } from '../internal/ctx'
5
+ import type { Tracker } from '../internal/track'
6
+ import { CurrentTracker } from '../internal/track'
7
+ import { CurrentSlots, type SlotHost } from './host'
8
+ import type { Node, SlotClass } from './slot'
9
+ import type { UiClass, UiContract } from './ui'
10
+ import { Props } from './props'
11
+
12
+ /** Everything the host injects per render: the instance props, the dependency tracker, and slot bindings. */
13
+ export interface RenderEnv {
14
+ readonly props: unknown
15
+ readonly tracker: Tracker
16
+ readonly slots: SlotHost
17
+ }
18
+
19
+ /** A mounted composition: given a render environment, produces the current frame's node. */
20
+ export interface CompositionService {
21
+ readonly render: (env: RenderEnv) => Effect.Effect<Node, never, never>
22
+ }
23
+
24
+ export interface CompositionConfig {
25
+ readonly title: string
26
+ readonly description?: string
27
+ // `any` in the encoded/context slots is required by Schema's variance for
28
+ // branded/Class schemas (e.g. `Todo`); only the decoded type `P` is used.
29
+ readonly props?: Schema.Schema<any, any>
30
+ readonly states?: ReadonlyArray<unknown>
31
+ readonly calcs?: ReadonlyArray<unknown>
32
+ readonly events?: ReadonlyArray<unknown>
33
+ readonly slots?: Record<string, SlotClass>
34
+ // The UI contract this composition resolves. Typed as a manifest carrier (not
35
+ // the full `UiClass`, whose invariant `impl` tag would force the contract type
36
+ // to leak here) so reflection — the dev tool, proofs — can read `ui.manifest`.
37
+ readonly ui: { readonly manifest: Manifest & { readonly kind: 'Ui' } }
38
+ }
39
+
40
+ type PropsOf<Config> = Config extends { props: Schema.Schema<infer P, any> } ? P : unknown
41
+
42
+ export interface CompositionClass<out P, out C extends UiContract = UiContract> {
43
+ new (): {}
44
+ readonly manifest: Manifest & { readonly kind: 'Composition' } & CompositionConfig
45
+ /** Phantom carrying the props type for `provide(slot, composition)` checks. */
46
+ readonly Props: P
47
+ /**
48
+ * Phantom carrying the resolved UI contract (props + events + slots). It lets
49
+ * reflection consumers — proofs, the dev tool — recover the full contract from
50
+ * a composition value and stay fully typed. Covariant (output only); the
51
+ * invariant `impl` tag is never stored here, so no variance regression.
52
+ */
53
+ readonly Contract: C
54
+ readonly tag: Context.Tag<CompositionService, CompositionService>
55
+ }
56
+
57
+ /**
58
+ * A component's logic. The *definition* (`Composition.make`) is a reflectable
59
+ * manifest that doubles as a requirement/tag; the *implementation*
60
+ * (`Composition.live`) is the synchronous body, provided separately and
61
+ * checked against it.
62
+ */
63
+ export const make = <const Config extends CompositionConfig, C extends UiContract>(
64
+ name: string,
65
+ // Constrain `ui` to the full `UiClass<C>` so `C` is inferred here; the stored
66
+ // manifest still sees only the erased `{ manifest }` carrier (CompositionConfig),
67
+ // so the manifest type — and its variance — is unchanged.
68
+ config: Config & { readonly ui: UiClass<C> },
69
+ ): CompositionClass<PropsOf<Config>, C> => {
70
+ const tag = Context.GenericTag<CompositionService, CompositionService>(
71
+ `reform/composition/${name}`,
72
+ )
73
+ const manifest = { kind: 'Composition' as const, name, ...config }
74
+ return definitionClass<CompositionClass<PropsOf<Config>, C>>({ manifest, tag })
75
+ }
76
+
77
+ /**
78
+ * Wire a composition's synchronous logic (D2): read state, acquire triggers,
79
+ * resolve the UI, return the view. Its requirements (minus `Props`, which the
80
+ * host injects per render) surface as the Layer's `RIn`; the runtime runs the
81
+ * body per render within the captured context.
82
+ */
83
+ export const live = <P, Eff extends YieldWrap<Effect.Effect<unknown, never, unknown>>>(
84
+ // `live` doesn't use the contract type; accept any composition.
85
+ composition: CompositionClass<P, any>,
86
+ body: () => Generator<Eff, Node, never>,
87
+ ): Layer.Layer<CompositionService, never, Exclude<Ctx<Eff>, Props>> => {
88
+ const logic = Effect.gen(body)
89
+ return Layer.effect(
90
+ composition.tag,
91
+ Effect.gen(function* () {
92
+ // Capture the build-time context (state stores, calc, ui, bus). `Props`,
93
+ // the tracker, and slots are injected per render below.
94
+ const context = yield* Effect.context<Exclude<Ctx<Eff>, Props>>()
95
+ const render = (env: RenderEnv): Effect.Effect<Node, never, never> =>
96
+ // Total once fully provided: the body is synchronous, reads can't fail,
97
+ // and every requirement is now satisfied (D2). `Effect.gen` widens the
98
+ // error channel to `unknown` for a generic body, so we restate it.
99
+ logic.pipe(
100
+ Effect.provideService(Props, env.props),
101
+ Effect.provideService(CurrentTracker, env.tracker),
102
+ Effect.provideService(CurrentSlots, env.slots),
103
+ Effect.provide(context),
104
+ ) as Effect.Effect<Node, never, never>
105
+ return { render }
106
+ }),
107
+ )
108
+ }
109
+
110
+ /** Run a mounted composition for one frame, producing its node. */
111
+ export const render = (service: CompositionService, env: RenderEnv): Effect.Effect<Node, never, never> =>
112
+ service.render(env)
@@ -1,5 +1,6 @@
1
- import { Context } from 'effect';
2
- import type { Node } from './slot.js';
1
+ import { Context } from 'effect'
2
+ import type { Node } from './slot'
3
+
3
4
  /**
4
5
  * Binds a parent's slots to concrete child renderers. The host package
5
6
  * (`@reform/react`) builds one per composition instance from the manifest's
@@ -7,11 +8,8 @@ import type { Node } from './slot.js';
7
8
  * child. Core stays renderer-neutral — it only defines this hole.
8
9
  */
9
10
  export interface SlotHost {
10
- slot(name: string): (props: unknown) => Node;
11
+ slot(name: string): (props: unknown) => Node
11
12
  }
12
- declare const CurrentSlots_base: Context.TagClass<CurrentSlots, "reform/Slots", SlotHost>;
13
+
13
14
  /** The active composition's slot bindings, provided per render by the host. */
14
- export declare class CurrentSlots extends CurrentSlots_base {
15
- }
16
- export {};
17
- //# sourceMappingURL=host.d.ts.map
15
+ export class CurrentSlots extends Context.Tag('reform/Slots')<CurrentSlots, SlotHost>() {}
@@ -1,4 +1,5 @@
1
- import { Context } from 'effect';
1
+ import { Context } from 'effect'
2
+
2
3
  /**
3
4
  * The current composition instance's props. Bound per render by the runtime; a
4
5
  * composition body reads them with `const { todo } = yield* Props`.
@@ -6,6 +7,4 @@ import { Context } from 'effect';
6
7
  * (Per-instance typing of Props is DESIGN #26 — for now the value is opaque and
7
8
  * narrowed at the use site against the composition's declared props schema.)
8
9
  */
9
- export class Props extends Context.Tag('reform/Props')() {
10
- }
11
- //# sourceMappingURL=props.js.map
10
+ export class Props extends Context.Tag('reform/Props')<Props, any>() {}
@@ -0,0 +1,70 @@
1
+ import { type Context, Layer } from 'effect'
2
+ import { InvalidProvideTarget } from '../internal/errors'
3
+ import type { CompositionClass } from './composition'
4
+ import {
5
+ type EngineServices,
6
+ type FeatureClass,
7
+ isFeature,
8
+ type ProvidedBy,
9
+ type RequireCarrier,
10
+ } from '../feature/feature'
11
+ import { isSlot, type SlotChild, type SlotClass } from './slot'
12
+ import { isUi, type UiClass, type UiContract, type ViewImpl } from './ui'
13
+
14
+ /**
15
+ * Wire an implementation to a contract. Provides a `.make` presentation to a UI
16
+ * contract, a child composition into a parent's slot, or a `Feature` into a slot.
17
+ * The compose code never imports its implementations — they meet here.
18
+ */
19
+ export function provide<C extends UiContract>(
20
+ contract: UiClass<C>,
21
+ view: ViewImpl<C>,
22
+ ): Layer.Layer<ViewImpl<C>>
23
+ export function provide<Comp extends CompositionClass<any, any>>(
24
+ hole: SlotClass<Comp>,
25
+ composition: Comp,
26
+ ): Layer.Layer<CompositionClass<unknown>>
27
+ /**
28
+ * Fill a slot with a feature. The slot's child composition type
29
+ * (`CompositionClass<P, C>`) must match the feature's mounted composition, so the
30
+ * facade/contract stays typed. The returned layer surfaces the feature's shared
31
+ * requirement (`EngineServices | ProvidedBy<Requires>`) as an open `RIn`, so the
32
+ * scene that wires it won't compile unless it provides the matching `Core` — even
33
+ * for a lazy feature whose own `.live` layer isn't built until mount.
34
+ */
35
+ export function provide<
36
+ Comp extends CompositionClass<any, any>,
37
+ ROut,
38
+ Requires extends ReadonlyArray<RequireCarrier>,
39
+ >(
40
+ hole: SlotClass<Comp>,
41
+ feature: FeatureClass<
42
+ Comp extends CompositionClass<infer P, any> ? P : never,
43
+ Comp extends CompositionClass<any, infer C> ? C : never,
44
+ ROut,
45
+ Requires
46
+ >,
47
+ ): Layer.Layer<SlotChild, never, EngineServices | ProvidedBy<Requires>>
48
+ export function provide(target: unknown, impl: unknown): Layer.Layer<never, never, unknown> {
49
+ // Discriminate the target by its nominal brand, not by which DI-hole property
50
+ // happens to exist: a UI contract provides under its `impl` tag, a slot under
51
+ // its `tag`. Anything else is a wiring mistake. The overloads above pair
52
+ // `impl` with the chosen tag; this implementation sees both erased, so view
53
+ // the tag loosely (the one documented cast) to provide the service under it.
54
+ const tag = (isUi(target) ? target.impl : isSlot(target) ? target.tag : undefined) as
55
+ | Context.Tag<unknown, unknown>
56
+ | undefined
57
+ if (tag === undefined) throw new InvalidProvideTarget()
58
+ if (isFeature(impl)) {
59
+ const eager = impl.eagerModule
60
+ // Lazy: bind the `FeatureBinding`; the host drives load → mount, painting the
61
+ // placeholders. Default: bind the composition (so the existing `Compose` path
62
+ // renders it, no host, no flash) and merge the eager `.live` layer, whose `RIn`
63
+ // surfaces to the scene. Either way the overload's return type carries the
64
+ // shared `RIn`, so the scene still type-demands `Core`.
65
+ return eager === undefined
66
+ ? Layer.succeed(tag, impl.binding)
67
+ : Layer.merge(Layer.succeed(tag, impl.binding.composition), eager.layer)
68
+ }
69
+ return Layer.succeed(tag, impl)
70
+ }
@@ -1,17 +1,22 @@
1
- import { Context } from 'effect';
2
- import type { CompositionClass } from './composition.js';
3
- import type { FeatureBinding } from '../feature/feature.js';
4
- import type { Node } from '../ui/node.js';
5
- export type { Node };
1
+ import { Context } from 'effect'
2
+ import { definitionClass } from '../definition/definition'
3
+ import type { CompositionClass } from './composition'
4
+ import type { FeatureBinding } from '../feature/feature'
5
+ import type { Node } from '../ui/node'
6
+
7
+ export type { Node }
8
+
6
9
  /** Any composition, used where only the slot's structural role matters. */
7
- type AnyComposition = CompositionClass<any, any>;
10
+ type AnyComposition = CompositionClass<any, any>
11
+
8
12
  /**
9
13
  * What a slot can be filled with: a plain child composition (the host renders it
10
14
  * directly), or a `FeatureBinding` from `provide(slot, Feature)` (the host drives
11
15
  * its lazy/eager mount lifecycle). The slot's DI tag holds one of these; the host
12
16
  * discriminates by the `FeatureBinding` brand.
13
17
  */
14
- export type SlotChild = CompositionClass<unknown> | FeatureBinding;
18
+ export type SlotChild = CompositionClass<unknown> | FeatureBinding
19
+
15
20
  /**
16
21
  * Phantom instance shape: `ItemSlot` used as a *type* denotes this. It carries
17
22
  * the child *composition* type `Comp`, so the slot knows both the external props
@@ -20,8 +25,9 @@ export type SlotChild = CompositionClass<unknown> | FeatureBinding;
20
25
  * proof facade, drive.
21
26
  */
22
27
  export interface SlotInstance<Comp extends AnyComposition> {
23
- readonly _slot: Comp;
28
+ readonly _slot: Comp
24
29
  }
30
+
25
31
  /**
26
32
  * A DI hole for a child composition. The parent declares
27
33
  * `slot('Item')<typeof TodoItem>()` and renders through it; the concrete child
@@ -32,18 +38,29 @@ export interface SlotInstance<Comp extends AnyComposition> {
32
38
  * by `provide`'s signature, not the (invariant) tag.
33
39
  */
34
40
  /** Nominal brand identifying a slot at runtime (Effect's `TypeId` idiom). */
35
- export declare const SlotTypeId: unique symbol;
36
- export type SlotTypeId = typeof SlotTypeId;
41
+ export const SlotTypeId: unique symbol = Symbol.for('reform/Slot')
42
+ export type SlotTypeId = typeof SlotTypeId
43
+
37
44
  export interface SlotClass<Comp extends AnyComposition = AnyComposition> {
38
- new (): SlotInstance<Comp>;
39
- readonly [SlotTypeId]: SlotTypeId;
40
- readonly kind: 'Slot';
41
- /** Internal DI tag the filling child (composition or feature binding) is provided under. */
42
- readonly tag: Context.Tag<SlotChild, SlotChild>;
45
+ new (): SlotInstance<Comp>
46
+ readonly [SlotTypeId]: SlotTypeId
47
+ readonly kind: 'Slot'
48
+ /** Internal DI tag the filling child (composition or feature binding) is provided under. */
49
+ readonly tag: Context.Tag<SlotChild, SlotChild>
43
50
  }
51
+
44
52
  /** Whether a value is a slot — discriminates a `provide` target by brand. */
45
- export declare const isSlot: (u: unknown) => u is SlotClass;
53
+ export const isSlot = (u: unknown): u is SlotClass =>
54
+ (typeof u === 'function' || typeof u === 'object') && u !== null && SlotTypeId in u
55
+
46
56
  /** The props a slot renders with — the child composition's external props. */
47
- export type SlotProps<S> = S extends SlotInstance<infer Comp> ? Comp['Props'] : never;
48
- export declare const slot: (name: string) => <Comp extends AnyComposition = AnyComposition>() => SlotClass<Comp>;
49
- //# sourceMappingURL=slot.d.ts.map
57
+ export type SlotProps<S> = S extends SlotInstance<infer Comp> ? Comp['Props'] : never
58
+
59
+ export const slot =
60
+ (name: string) =>
61
+ // Defaulted so a slot whose child carries no meaningful props/contract can be
62
+ // declared bare (`slot('Tree')()`); pass `typeof Child` to type the facade.
63
+ <Comp extends AnyComposition = AnyComposition>(): SlotClass<Comp> => {
64
+ const tag = Context.GenericTag<SlotChild, SlotChild>(`reform/slot/${name}`)
65
+ return definitionClass<SlotClass<Comp>>({ [SlotTypeId]: SlotTypeId, kind: 'Slot' as const, tag })
66
+ }
@@ -0,0 +1,97 @@
1
+ import { Context, Effect, Option } from 'effect'
2
+ import { type Manifest, yieldableClass } from '../definition/definition'
3
+ import { CaptureSink } from '../internal/capture'
4
+ import { SlotRenderingUnavailable } from '../internal/errors'
5
+ import type { Trigger } from '../ui/trigger'
6
+ import { CurrentSlots } from './host'
7
+ import { type Node, type SlotInstance, type SlotProps } from './slot'
8
+
9
+ export interface UiContract {
10
+ props: unknown
11
+ slots?: Record<string, SlotInstance<any>>
12
+ events?: Record<string, Trigger<any>>
13
+ }
14
+
15
+ type PropsOf<C extends UiContract> = C['props']
16
+ type EventsOf<C extends UiContract> = C extends { events: infer E } ? E : Record<never, never>
17
+ type SlotsOf<C extends UiContract> = C extends { slots: infer S }
18
+ ? { readonly [K in keyof S]: (props: SlotProps<S[K]>) => Node }
19
+ : Record<never, never>
20
+
21
+ /** What the composition logic calls: props + handlers in, a node out. Slots are bound by the renderer. */
22
+ export type LogicView<C extends UiContract> = (props: PropsOf<C>, events?: EventsOf<C>) => Node
23
+
24
+ /** What `.make` authors: the pure presentation. Slots are injected by the renderer. */
25
+ export type ViewImpl<C extends UiContract> = (
26
+ props: PropsOf<C>,
27
+ slots: SlotsOf<C>,
28
+ events: EventsOf<C>,
29
+ ) => Node
30
+
31
+ /** Nominal brand identifying a UI contract at runtime (Effect's `TypeId` idiom). */
32
+ export const UiTypeId: unique symbol = Symbol.for('reform/Ui')
33
+ export type UiTypeId = typeof UiTypeId
34
+
35
+ export interface UiClass<C extends UiContract>
36
+ extends Effect.Effect<LogicView<C>, never, ViewImpl<C>> {
37
+ new (): {}
38
+ readonly [UiTypeId]: UiTypeId
39
+ readonly manifest: Manifest & { readonly kind: 'Ui' }
40
+ /** Internal DI tag the `Ui.make` presentation is provided under. */
41
+ readonly impl: Context.Tag<ViewImpl<C>, ViewImpl<C>>
42
+ }
43
+
44
+ /** Whether a value is a UI contract — discriminates a `provide` target by brand. */
45
+ export const isUi = (u: unknown): u is UiClass<UiContract> =>
46
+ (typeof u === 'function' || typeof u === 'object') && u !== null && UiTypeId in u
47
+
48
+ /**
49
+ * A renderer-neutral UI contract. The logic resolves it (`yield* TodoAppUi`) to
50
+ * a `(props, events) => node` view with slots already bound; `.make` authors the
51
+ * presentation. Core defines the contract; `@reform/react` does the rendering.
52
+ */
53
+ export const ui =
54
+ (name: string) =>
55
+ <C extends UiContract>(): UiClass<C> => {
56
+ const impl = Context.GenericTag<ViewImpl<C>, ViewImpl<C>>(`reform/ui/${name}`)
57
+ const read = Effect.gen(function* () {
58
+ const render = yield* impl
59
+ // Slots are bound by the host (`@reform/react`) per composition instance.
60
+ // Core has no renderer, so without a host every slot access throws.
61
+ const host = yield* Effect.serviceOption(CurrentSlots)
62
+ const slots = new Proxy({} as SlotsOf<C>, {
63
+ get(_t, key) {
64
+ if (Option.isNone(host)) {
65
+ throw new SlotRenderingUnavailable({ slot: String(key) })
66
+ }
67
+ return host.value.slot(String(key))
68
+ },
69
+ })
70
+ // A capturing host (proofs, the dev tool) reports each render's observable
71
+ // surface without replacing the presentation. Absent in production.
72
+ const sink = yield* Effect.serviceOption(CaptureSink)
73
+ return (props: PropsOf<C>, events?: EventsOf<C>) => {
74
+ if (Option.isSome(sink)) {
75
+ sink.value.record({
76
+ name,
77
+ props,
78
+ events: (events ?? {}) as Record<string, Trigger<unknown>>,
79
+ })
80
+ }
81
+ return render(props, slots, (events ?? {}) as EventsOf<C>)
82
+ }
83
+ })
84
+ return yieldableClass(read, {
85
+ [UiTypeId]: UiTypeId,
86
+ manifest: { kind: 'Ui' as const, name },
87
+ impl,
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Author the pure presentation for a contract — `Ui.make(TodoAppUi, (props,
93
+ * slots, events) => node)`. The contract argument fixes the view's types; wire
94
+ * the result with `provide(contract, view)`.
95
+ */
96
+ export const make = <C extends UiContract>(_contract: UiClass<C>, view: ViewImpl<C>): ViewImpl<C> =>
97
+ view
@@ -0,0 +1,74 @@
1
+ import { Effect } from 'effect'
2
+ import { attachInspectable } from '../internal/inspect'
3
+
4
+ /** A definition's `toJSON`: its reflectable manifest, else its raw statics. */
5
+ const describe = (statics: object): unknown => (statics as { manifest?: unknown }).manifest ?? statics
6
+
7
+ /**
8
+ * The reflectable descriptor every primitive carries (principle #6). The editor
9
+ * and proof packages read these without running any logic.
10
+ */
11
+ export type Kind =
12
+ | 'State'
13
+ | 'StateGroup'
14
+ | 'StateFamily'
15
+ | 'Event'
16
+ | 'EventGroup'
17
+ | 'Reducer'
18
+ | 'Calc'
19
+ | 'CalcFamily'
20
+ | 'AsyncCalc'
21
+ | 'RemoteState'
22
+ | 'Boundary'
23
+ | 'Procedure'
24
+ | 'Channel'
25
+ | 'Composition'
26
+ | 'Ui'
27
+ | 'Slot'
28
+ | 'Feature'
29
+
30
+ export interface Manifest {
31
+ readonly kind: Kind
32
+ readonly name: string
33
+ }
34
+
35
+ /**
36
+ * Build a class whose *static* side is itself an Effect, so `yield* TheClass`
37
+ * runs `read` and returns its value (the same mechanism `Context.Tag` uses).
38
+ * Subclasses inherit the protocol through the prototype chain, so the user's
39
+ * `class FeedState extends State.make(...) {}` is yieldable too.
40
+ *
41
+ * `statics` (manifest, `.live`, tag, …) are attached as own properties
42
+ * and likewise inherited by the subclass.
43
+ */
44
+ export const yieldableClass = <A, E, R, Statics extends object>(
45
+ read: Effect.Effect<A, E, R>,
46
+ statics: Statics,
47
+ ): { new (): {} } & Effect.Effect<A, E, R> & Statics => {
48
+ class Base {}
49
+ Object.setPrototypeOf(Base, read)
50
+ // `defineProperty` over `Reflect.ownKeys`, not `Object.assign` over
51
+ // `Object.entries`: a static can shadow a non-writable own property of the
52
+ // class function (notably `name`, which a `Source`-shaped definition like a
53
+ // Calc carries) and `assign` would throw on those; and `ownKeys` (unlike
54
+ // `entries`) carries symbol-keyed statics — the `TypeId` brands.
55
+ for (const key of Reflect.ownKeys(statics)) {
56
+ const value = (statics as Record<PropertyKey, unknown>)[key]
57
+ Object.defineProperty(Base, key, { value, writable: true, enumerable: true, configurable: true })
58
+ }
59
+ // Print like an Effect value (the manifest), not `[object Object]`.
60
+ attachInspectable(Base, () => describe(statics))
61
+ return Base as never
62
+ }
63
+
64
+ /**
65
+ * Build a *definition class*: an empty constructible carrying static marker data
66
+ * (manifest, tag, policy, members, …) that the editor and proof packages reflect
67
+ * on. Definition interfaces declare phantom, type-only fields — a composition's
68
+ * `Props`, a family instance's `Key` — that exist purely for inference and have
69
+ * no runtime value, so the assembled object cannot *structurally* satisfy the
70
+ * interface. The one assertion to `Class` lives here, documented, instead of a
71
+ * scattered `as never` / `as unknown as` at every `*.make`.
72
+ */
73
+ export const definitionClass = <Class>(statics: object): Class =>
74
+ attachInspectable(Object.assign(class {}, statics), () => describe(statics)) as unknown as Class
@@ -0,0 +1,65 @@
1
+ import { Effect, PubSub, Runtime, type Schema } from 'effect'
2
+ import { type Manifest, definitionClass } from '../definition/definition'
3
+ import { Bus, publish, type Tagged } from '../runtime/bus'
4
+ import type { Trigger } from '../ui/trigger'
5
+
6
+ /** The dispatched value of an event: `{ _tag, ...payload }`, matched with `Match.tags`. */
7
+ export type EventOf<N extends string, P> = Tagged & { readonly _tag: N } & P
8
+
9
+ export interface EventManifest<N extends string, P> extends Manifest {
10
+ readonly kind: 'Event'
11
+ readonly name: N
12
+ readonly schema: Schema.Schema<P, any>
13
+ }
14
+
15
+ export interface EventClass<out N extends string, in out P> {
16
+ new (): {}
17
+ readonly manifest: EventManifest<N, P>
18
+ readonly tag: N
19
+ /** Carrier for `Event.construct` — builds the tagged value `{ _tag, ...payload }`. */
20
+ readonly build: (payload: P) => EventOf<N, P>
21
+ /** Carrier for `Event.trigger` — the UI callback effect (High priority). */
22
+ readonly trigger: Effect.Effect<Trigger<P>, never, Bus>
23
+ }
24
+
25
+ export type AnyEvent = EventClass<string, any>
26
+ export type EventType<E> = E extends EventClass<infer N, infer P> ? EventOf<N, P> : never
27
+
28
+ /** A pure data fact. Events depend on nothing — only reducers tie them to state. */
29
+ export const make = <const N extends string, P>(
30
+ name: N,
31
+ schema: Schema.Schema<P, any>,
32
+ ): EventClass<N, P> => {
33
+ const build = (payload: P): EventOf<N, P> => ({ _tag: name, ...payload })
34
+ const trigger = Effect.gen(function* () {
35
+ const bus = yield* Bus
36
+ const runtime = yield* Effect.runtime<never>()
37
+ // Dispatch synchronously: on the unbounded bus `publish` only enqueues and
38
+ // returns, so `runSync` never suspends and no fiber is forked per UI event.
39
+ // The drain loop still folds reducers on its own fiber — publish is pure offer.
40
+ return (payload: P) => {
41
+ Runtime.runSync(runtime)(PubSub.publish(bus, { priority: 'High', event: build(payload) }))
42
+ }
43
+ })
44
+ return definitionClass<EventClass<N, P>>({
45
+ manifest: { kind: 'Event' as const, name, schema },
46
+ tag: name,
47
+ build,
48
+ trigger,
49
+ })
50
+ }
51
+
52
+ /** Construct the tagged value for an event (used for `boot` events). */
53
+ export const construct = <N extends string, P>(event: EventClass<N, P>, payload: P): EventOf<N, P> =>
54
+ event.build(payload)
55
+
56
+ /** Dispatch as a follow-up (Normal priority) from a procedure/effect. */
57
+ export const dispatch = <N extends string, P>(
58
+ event: EventClass<N, P>,
59
+ payload: P,
60
+ ): Effect.Effect<void, never, Bus> => publish('Normal', event.build(payload))
61
+
62
+ /** A callback for the UI that dispatches at High priority via the runtime. */
63
+ export const trigger = <N extends string, P>(
64
+ event: EventClass<N, P>,
65
+ ): Effect.Effect<Trigger<P>, never, Bus> => event.trigger
@@ -0,0 +1,14 @@
1
+ import { definitionClass } from '../definition/definition'
2
+ import type { AnyEvent } from './event'
3
+
4
+ export interface EventGroupClass<Members extends ReadonlyArray<AnyEvent>> {
5
+ new (): {}
6
+ readonly kind: 'EventGroup'
7
+ readonly members: Members
8
+ }
9
+
10
+ /** Compose events into a group, the same variadic shape as `StateGroup`/`RpcGroup`. */
11
+ export const make = <const Members extends ReadonlyArray<AnyEvent>>(
12
+ ...members: Members
13
+ ): EventGroupClass<Members> =>
14
+ definitionClass<EventGroupClass<Members>>({ kind: 'EventGroup' as const, members })