@playfast/reform 0.0.4 → 0.0.7

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 (209) hide show
  1. package/dist/boundary/boundary.d.ts +72 -0
  2. package/dist/boundary/boundary.d.ts.map +1 -0
  3. package/dist/boundary/boundary.js +83 -0
  4. package/dist/boundary/boundary.js.map +1 -0
  5. package/dist/calc/asyncCalc.d.ts +91 -0
  6. package/dist/calc/asyncCalc.d.ts.map +1 -0
  7. package/dist/calc/asyncCalc.js +95 -0
  8. package/dist/calc/asyncCalc.js.map +1 -0
  9. package/{src/calc/asyncData.ts → dist/calc/asyncData.d.ts} +22 -53
  10. package/dist/calc/asyncData.d.ts.map +1 -0
  11. package/dist/calc/asyncData.js +34 -0
  12. package/dist/calc/asyncData.js.map +1 -0
  13. package/dist/calc/calc.d.ts +57 -0
  14. package/dist/calc/calc.d.ts.map +1 -0
  15. package/dist/calc/calc.js +58 -0
  16. package/dist/calc/calc.js.map +1 -0
  17. package/dist/calc/calcFamily.d.ts +57 -0
  18. package/dist/calc/calcFamily.d.ts.map +1 -0
  19. package/dist/calc/calcFamily.js +124 -0
  20. package/dist/calc/calcFamily.js.map +1 -0
  21. package/dist/calc/compose.d.ts +16 -0
  22. package/dist/calc/compose.d.ts.map +1 -0
  23. package/dist/calc/compose.js +7 -0
  24. package/dist/calc/compose.js.map +1 -0
  25. package/dist/channel/channel.d.ts +115 -0
  26. package/dist/channel/channel.d.ts.map +1 -0
  27. package/dist/channel/channel.js +138 -0
  28. package/dist/channel/channel.js.map +1 -0
  29. package/dist/compose/composition.d.ts +72 -0
  30. package/dist/compose/composition.d.ts.map +1 -0
  31. package/dist/compose/composition.js +46 -0
  32. package/dist/compose/composition.js.map +1 -0
  33. package/{src/compose/host.ts → dist/compose/host.d.ts} +8 -6
  34. package/dist/compose/host.d.ts.map +1 -0
  35. package/dist/compose/host.js +6 -0
  36. package/dist/compose/host.js.map +1 -0
  37. package/dist/compose/props.d.ts +13 -0
  38. package/dist/compose/props.d.ts.map +1 -0
  39. package/{src/compose/props.ts → dist/compose/props.js} +5 -3
  40. package/dist/compose/props.js.map +1 -0
  41. package/dist/compose/provide.d.ts +22 -0
  42. package/dist/compose/provide.d.ts.map +1 -0
  43. package/dist/compose/provide.js +28 -0
  44. package/dist/compose/provide.js.map +1 -0
  45. package/{src/compose/slot.ts → dist/compose/slot.d.ts} +19 -36
  46. package/dist/compose/slot.d.ts.map +1 -0
  47. package/dist/compose/slot.js +23 -0
  48. package/dist/compose/slot.js.map +1 -0
  49. package/dist/compose/ui.d.ts +135 -0
  50. package/dist/compose/ui.d.ts.map +1 -0
  51. package/dist/compose/ui.js +63 -0
  52. package/dist/compose/ui.js.map +1 -0
  53. package/dist/definition/definition.d.ts +33 -0
  54. package/dist/definition/definition.d.ts.map +1 -0
  55. package/dist/definition/definition.js +42 -0
  56. package/dist/definition/definition.js.map +1 -0
  57. package/dist/event/event.d.ts +33 -0
  58. package/dist/event/event.d.ts.map +1 -0
  59. package/dist/event/event.js +30 -0
  60. package/dist/event/event.js.map +1 -0
  61. package/dist/event/eventGroup.d.ts +9 -0
  62. package/dist/event/eventGroup.d.ts.map +1 -0
  63. package/dist/event/eventGroup.js +4 -0
  64. package/dist/event/eventGroup.js.map +1 -0
  65. package/{src/feature/feature.ts → dist/feature/feature.d.ts} +92 -178
  66. package/dist/feature/feature.d.ts.map +1 -0
  67. package/dist/feature/feature.js +98 -0
  68. package/dist/feature/feature.js.map +1 -0
  69. package/dist/index.d.ts +53 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +62 -0
  72. package/dist/index.js.map +1 -0
  73. package/{src/internal/capture.ts → dist/internal/capture.d.ts} +11 -13
  74. package/dist/internal/capture.d.ts.map +1 -0
  75. package/dist/internal/capture.js +12 -0
  76. package/dist/internal/capture.js.map +1 -0
  77. package/{src/internal/ctx.ts → dist/internal/ctx.d.ts} +4 -8
  78. package/dist/internal/ctx.d.ts.map +1 -0
  79. package/dist/internal/ctx.js +2 -0
  80. package/dist/internal/ctx.js.map +1 -0
  81. package/dist/internal/errors.d.ts +89 -0
  82. package/dist/internal/errors.d.ts.map +1 -0
  83. package/dist/internal/errors.js +102 -0
  84. package/dist/internal/errors.js.map +1 -0
  85. package/dist/internal/inspect.d.ts +17 -0
  86. package/dist/internal/inspect.d.ts.map +1 -0
  87. package/dist/internal/inspect.js +32 -0
  88. package/dist/internal/inspect.js.map +1 -0
  89. package/dist/internal/queryDriver.d.ts +65 -0
  90. package/dist/internal/queryDriver.d.ts.map +1 -0
  91. package/dist/internal/queryDriver.js +134 -0
  92. package/dist/internal/queryDriver.js.map +1 -0
  93. package/dist/internal/reuse.d.ts +10 -0
  94. package/dist/internal/reuse.d.ts.map +1 -0
  95. package/dist/internal/reuse.js +68 -0
  96. package/dist/internal/reuse.js.map +1 -0
  97. package/dist/internal/scheduler.d.ts +47 -0
  98. package/dist/internal/scheduler.d.ts.map +1 -0
  99. package/dist/internal/scheduler.js +70 -0
  100. package/dist/internal/scheduler.js.map +1 -0
  101. package/{src/internal/seeds.ts → dist/internal/seeds.d.ts} +3 -6
  102. package/dist/internal/seeds.d.ts.map +1 -0
  103. package/dist/internal/seeds.js +17 -0
  104. package/dist/internal/seeds.js.map +1 -0
  105. package/dist/internal/sources.d.ts +39 -0
  106. package/dist/internal/sources.d.ts.map +1 -0
  107. package/dist/internal/sources.js +59 -0
  108. package/dist/internal/sources.js.map +1 -0
  109. package/dist/internal/stateRegistry.d.ts +2 -0
  110. package/dist/internal/stateRegistry.d.ts.map +1 -0
  111. package/dist/internal/stateRegistry.js +28 -0
  112. package/dist/internal/stateRegistry.js.map +1 -0
  113. package/dist/internal/store.d.ts +47 -0
  114. package/dist/internal/store.d.ts.map +1 -0
  115. package/dist/internal/store.js +73 -0
  116. package/dist/internal/store.js.map +1 -0
  117. package/{src/internal/track.ts → dist/internal/track.d.ts} +11 -15
  118. package/dist/internal/track.d.ts.map +1 -0
  119. package/dist/internal/track.js +19 -0
  120. package/dist/internal/track.js.map +1 -0
  121. package/dist/procedure/procedure.d.ts +40 -0
  122. package/dist/procedure/procedure.d.ts.map +1 -0
  123. package/dist/procedure/procedure.js +50 -0
  124. package/dist/procedure/procedure.js.map +1 -0
  125. package/dist/reducer/reducer.d.ts +44 -0
  126. package/dist/reducer/reducer.d.ts.map +1 -0
  127. package/dist/reducer/reducer.js +63 -0
  128. package/dist/reducer/reducer.js.map +1 -0
  129. package/dist/remote/remoteState.d.ts +119 -0
  130. package/dist/remote/remoteState.d.ts.map +1 -0
  131. package/dist/remote/remoteState.js +270 -0
  132. package/dist/remote/remoteState.js.map +1 -0
  133. package/dist/runtime/bus.d.ts +27 -0
  134. package/dist/runtime/bus.d.ts.map +1 -0
  135. package/{src/runtime/bus.ts → dist/runtime/bus.js} +8 -23
  136. package/dist/runtime/bus.js.map +1 -0
  137. package/dist/runtime/loop.d.ts +45 -0
  138. package/dist/runtime/loop.d.ts.map +1 -0
  139. package/dist/runtime/loop.js +117 -0
  140. package/dist/runtime/loop.js.map +1 -0
  141. package/dist/scene/scene.d.ts +44 -0
  142. package/dist/scene/scene.d.ts.map +1 -0
  143. package/dist/scene/scene.js +31 -0
  144. package/dist/scene/scene.js.map +1 -0
  145. package/dist/state/state.d.ts +37 -0
  146. package/dist/state/state.d.ts.map +1 -0
  147. package/dist/state/state.js +46 -0
  148. package/dist/state/state.js.map +1 -0
  149. package/dist/state/stateFamily.d.ts +79 -0
  150. package/dist/state/stateFamily.d.ts.map +1 -0
  151. package/dist/state/stateFamily.js +99 -0
  152. package/dist/state/stateFamily.js.map +1 -0
  153. package/dist/state/stateGroup.d.ts +50 -0
  154. package/dist/state/stateGroup.d.ts.map +1 -0
  155. package/dist/state/stateGroup.js +54 -0
  156. package/dist/state/stateGroup.js.map +1 -0
  157. package/{src/state/token.ts → dist/state/token.d.ts} +13 -21
  158. package/dist/state/token.d.ts.map +1 -0
  159. package/dist/state/token.js +20 -0
  160. package/dist/state/token.js.map +1 -0
  161. package/dist/synced/syncedStore.d.ts +51 -0
  162. package/dist/synced/syncedStore.d.ts.map +1 -0
  163. package/dist/synced/syncedStore.js +43 -0
  164. package/dist/synced/syncedStore.js.map +1 -0
  165. package/{src/ui/node.ts → dist/ui/node.d.ts} +3 -3
  166. package/dist/ui/node.d.ts.map +1 -0
  167. package/dist/ui/node.js +2 -0
  168. package/dist/ui/node.js.map +1 -0
  169. package/{src/ui/trigger.ts → dist/ui/trigger.d.ts} +2 -1
  170. package/dist/ui/trigger.d.ts.map +1 -0
  171. package/dist/ui/trigger.js +2 -0
  172. package/dist/ui/trigger.js.map +1 -0
  173. package/dist/wire/tree.d.ts +66 -0
  174. package/dist/wire/tree.d.ts.map +1 -0
  175. package/dist/wire/tree.js +60 -0
  176. package/dist/wire/tree.js.map +1 -0
  177. package/dist/wire/triggers.d.ts +44 -0
  178. package/dist/wire/triggers.d.ts.map +1 -0
  179. package/dist/wire/triggers.js +31 -0
  180. package/dist/wire/triggers.js.map +1 -0
  181. package/package.json +32 -11
  182. package/src/boundary/boundary.ts +0 -192
  183. package/src/calc/asyncCalc.ts +0 -267
  184. package/src/calc/calc.ts +0 -120
  185. package/src/calc/calcFamily.ts +0 -194
  186. package/src/calc/compose.ts +0 -69
  187. package/src/channel/channel.ts +0 -265
  188. package/src/compose/composition.ts +0 -112
  189. package/src/compose/provide.ts +0 -70
  190. package/src/compose/ui.ts +0 -97
  191. package/src/definition/definition.ts +0 -74
  192. package/src/event/event.ts +0 -65
  193. package/src/event/eventGroup.ts +0 -14
  194. package/src/index.ts +0 -140
  195. package/src/internal/errors.ts +0 -69
  196. package/src/internal/inspect.ts +0 -34
  197. package/src/internal/queryDriver.ts +0 -247
  198. package/src/internal/reuse.ts +0 -73
  199. package/src/internal/scheduler.ts +0 -91
  200. package/src/internal/sources.ts +0 -104
  201. package/src/internal/store.ts +0 -105
  202. package/src/procedure/procedure.ts +0 -89
  203. package/src/reducer/reducer.ts +0 -137
  204. package/src/remote/remoteState.ts +0 -571
  205. package/src/runtime/loop.ts +0 -169
  206. package/src/scene/scene.ts +0 -76
  207. package/src/state/state.ts +0 -91
  208. package/src/state/stateFamily.ts +0 -171
  209. package/src/state/stateGroup.ts +0 -78
@@ -1,265 +0,0 @@
1
- import { Chunk, Context, type Duration, Effect, Layer, Match, Queue, Stream } from 'effect'
2
- import { type Manifest, definitionClass } from '../definition/definition'
3
- import { DuplicateRegistration } from '../internal/errors'
4
- import type { Tagged } from '../runtime/bus'
5
-
6
- /**
7
- * How a channel schedules the procedure work routed to it. Channels generalize
8
- * the old per-procedure `concurrency`: a named lane with one policy, shared by
9
- * every procedure that declares it.
10
- *
11
- * - `merge` run every routed event concurrently (the default).
12
- * - `latest` a new event cancels the in-flight run (switch).
13
- * - `debounce` collapse a burst, run only after `duration` of quiet.
14
- * - `throttle` rate-limit to `units` per `duration` (shape or enforce).
15
- * - `exclusive` one run at a time across the whole channel (mutual exclusion).
16
- */
17
- export type ChannelPolicy =
18
- | { readonly _tag: 'merge' }
19
- | { readonly _tag: 'latest' }
20
- | { readonly _tag: 'debounce'; readonly duration: Duration.DurationInput }
21
- | {
22
- readonly _tag: 'throttle'
23
- readonly units: number
24
- readonly duration: Duration.DurationInput
25
- readonly cost?: number
26
- readonly burst?: number
27
- readonly strategy?: 'enforce' | 'shape'
28
- }
29
- | { readonly _tag: 'exclusive' }
30
-
31
- /** The reflectable descriptor a channel carries. */
32
- export interface ChannelManifest extends Manifest {
33
- readonly kind: 'Channel'
34
- }
35
-
36
- export interface ChannelClass {
37
- new (): {}
38
- readonly manifest: ChannelManifest
39
- readonly policy: ChannelPolicy
40
- }
41
-
42
- export interface ChannelConfig {
43
- readonly policy: ChannelPolicy
44
- }
45
-
46
- /**
47
- * A registered procedure body, with its trigger events and the channel it runs
48
- * on. `run`'s context (RPC clients, etc.) is already provided at registration
49
- * (see `Procedure.live`), so the channel fiber can run it with no R.
50
- */
51
- export interface ProcedureEntry {
52
- /** The procedure's name — used only to warn on duplicate registration. */
53
- readonly name: string
54
- readonly channelName: string
55
- readonly handles: ReadonlySet<string>
56
- readonly run: (event: Tagged) => Effect.Effect<unknown, unknown, never>
57
- }
58
-
59
- /**
60
- * Mutable collector procedures register into; channels read it to find their
61
- * bodies. `byTag` indexes entries by trigger tag, so a channel routes an event
62
- * to O(procedures handling it) rather than scanning every registered procedure.
63
- */
64
- export interface ProcedureRegistry {
65
- readonly entries: Array<ProcedureEntry>
66
- readonly byTag: Map<string, Array<ProcedureEntry>>
67
- /**
68
- * Distinct channel names per event tag. The drain loop routes through this so
69
- * it offers each event to each channel exactly once — not once per matching
70
- * procedure, which would re-run every procedure on a shared channel per offer.
71
- */
72
- readonly channelsByTag: Map<string, Array<string>>
73
- /**
74
- * Procedures keyed by (channel, tag), so a channel's `runMatching` is a direct
75
- * lookup instead of scanning `byTag` and re-filtering by channel per event.
76
- */
77
- readonly byChannelTag: Map<string, Map<string, Array<ProcedureEntry>>>
78
- readonly register: (entry: ProcedureEntry) => void
79
- /**
80
- * Remove a previously-registered entry — the inverse of `register`, used when a
81
- * lazy feature unmounts (scope close) so its procedures stop running and their
82
- * index entries are reclaimed. Eager procedures (root scope) never call it.
83
- */
84
- readonly unregister: (entry: ProcedureEntry) => void
85
- }
86
-
87
- export class Procedures extends Context.Tag('reform/Procedures')<Procedures, ProcedureRegistry>() {}
88
-
89
- /** Get a map entry, creating and inserting it on first access (avoids `let`). */
90
- const getOrCreate = <K, V>(map: Map<K, V>, key: K, make: () => V): V => {
91
- const existing = map.get(key)
92
- if (existing !== undefined) return existing
93
- const created = make()
94
- map.set(key, created)
95
- return created
96
- }
97
-
98
- /** Drop an item from a `Map<K, Array>` bucket, pruning the key when it empties. */
99
- const dropFrom = <K, T>(map: Map<K, Array<T>>, key: K, item: T): void => {
100
- const bucket = map.get(key)
101
- if (bucket === undefined) return
102
- const next = bucket.filter((value) => value !== item)
103
- if (next.length === 0) map.delete(key)
104
- else map.set(key, next)
105
- }
106
-
107
- const makeProcedureRegistry = (): ProcedureRegistry => {
108
- const entries: Array<ProcedureEntry> = []
109
- const byTag = new Map<string, Array<ProcedureEntry>>()
110
- const channelsByTag = new Map<string, Array<string>>()
111
- const byChannelTag = new Map<string, Map<string, Array<ProcedureEntry>>>()
112
- return {
113
- entries,
114
- byTag,
115
- channelsByTag,
116
- byChannelTag,
117
- register: (entry) => {
118
- entries.push(entry)
119
- for (const tag of entry.handles) {
120
- getOrCreate(byTag, tag, () => []).push(entry)
121
- const channels = getOrCreate(channelsByTag, tag, () => [])
122
- if (!channels.includes(entry.channelName)) channels.push(entry.channelName)
123
- getOrCreate(getOrCreate(byChannelTag, entry.channelName, () => new Map()), tag, () => []).push(
124
- entry,
125
- )
126
- }
127
- },
128
- unregister: (entry) => {
129
- const index = entries.indexOf(entry)
130
- if (index >= 0) entries.splice(index, 1)
131
- for (const tag of entry.handles) {
132
- dropFrom(byTag, tag, entry)
133
- const channelMap = byChannelTag.get(entry.channelName)
134
- if (channelMap !== undefined) {
135
- dropFrom(channelMap, tag, entry)
136
- // If no procedure on this channel still handles the tag, drop the
137
- // channel from `channelsByTag[tag]` so the loop stops offering to it.
138
- if (channelMap.get(tag) === undefined) dropFrom(channelsByTag, tag, entry.channelName)
139
- if (channelMap.size === 0) byChannelTag.delete(entry.channelName)
140
- }
141
- }
142
- },
143
- }
144
- }
145
-
146
- export const proceduresLayer = Layer.sync(Procedures, makeProcedureRegistry)
147
-
148
- /** A live channel: the single loop offers events here; the channel drives them. */
149
- export interface ChannelRuntime {
150
- readonly name: string
151
- /** The policy this channel was built with — used to reject a name clash. */
152
- readonly policy: ChannelPolicy
153
- readonly offer: (event: Tagged) => void
154
- }
155
-
156
- /** Registry of live channels, keyed by name. The single loop routes through it. */
157
- export class Channels extends Context.Tag('reform/Channels')<
158
- Channels,
159
- { readonly byName: Map<string, ChannelRuntime> }
160
- >() {}
161
-
162
- export const channelsLayer = Layer.sync(Channels, () => ({ byName: new Map() }))
163
-
164
- /**
165
- * A scheduling lane for procedures. The *definition* (`Channel.make`) is a
166
- * reflectable name + policy; the *implementation* (`Channel.live`) builds the
167
- * queue + fiber that applies the policy. Procedures reference a channel by value
168
- * and register into it; `Channel.live` is merged into the app once per channel.
169
- */
170
- export const make = (name: string, config: ChannelConfig): ChannelClass =>
171
- definitionClass<ChannelClass>({
172
- manifest: { kind: 'Channel' as const, name } satisfies ChannelManifest,
173
- policy: config.policy,
174
- })
175
-
176
- /**
177
- * Build and register the live machinery for a channel. Idempotent by name, so
178
- * procedures sharing an `exclusive`/`latest` channel share one queue + fiber +
179
- * semaphore (and therefore truly serialize / cancel across each other).
180
- */
181
- export const live = (channel: ChannelClass): Layer.Layer<never, never, Channels | Procedures> =>
182
- Layer.scopedDiscard(
183
- Effect.gen(function* () {
184
- const name = channel.manifest.name
185
- const channels = yield* Channels
186
- const existing = channels.byName.get(name)
187
- if (existing !== undefined) {
188
- // Idempotent for the *same* channel `.live`'d through several sub-layers
189
- // (shared `exclusive`/`latest` lane). A *different* channel reusing the
190
- // name is a silent footgun — the second policy would be dropped — so
191
- // reject it loudly instead.
192
- if (existing.policy !== channel.policy) {
193
- throw new DuplicateRegistration({ kind: 'Channel', name })
194
- }
195
- return
196
- }
197
- const procedures = yield* Procedures
198
- const queue = yield* Queue.unbounded<Tagged>()
199
-
200
- // Run every procedure on THIS channel that handles the event. Read the live
201
- // index each time so procedures registered later are still seen.
202
- // A procedure is expected to model expected failures as events; a leaked
203
- // defect (a bug in the body) is logged before being isolated, so one
204
- // procedure's crash never tears down the channel fiber — but is observable.
205
- const runMatching = (event: Tagged): Effect.Effect<void> =>
206
- Effect.forEach(
207
- procedures.byChannelTag.get(name)?.get(event._tag) ?? [],
208
- (e) =>
209
- e.run(event).pipe(
210
- Effect.tapErrorCause((cause) =>
211
- Effect.logError(`reform: procedure on channel '${name}' failed`, cause),
212
- ),
213
- Effect.catchAllCause(() => Effect.void),
214
- ),
215
- { discard: true, concurrency: 'unbounded' },
216
- )
217
-
218
- // Map the policy to its scheduling stream. `Match.exhaustive` makes a new
219
- // policy variant a compile error, and each arm is a self-contained const.
220
- const events = Stream.fromQueue(queue)
221
- const driven: Stream.Stream<unknown> = Match.value(channel.policy).pipe(
222
- Match.tag('merge', () => Stream.mapEffect(events, runMatching, { concurrency: 'unbounded' })),
223
- Match.tag('latest', () =>
224
- Stream.flatMap(events, (e) => Stream.fromEffect(runMatching(e)), { switch: true }),
225
- ),
226
- Match.tag('debounce', (policy) =>
227
- events.pipe(
228
- Stream.debounce(policy.duration),
229
- Stream.mapEffect(runMatching, { concurrency: 'unbounded' }),
230
- ),
231
- ),
232
- Match.tag('throttle', (policy) =>
233
- events.pipe(
234
- Stream.throttle({
235
- cost: (chunk) => Chunk.size(chunk) * (policy.cost ?? 1),
236
- units: policy.units,
237
- duration: policy.duration,
238
- ...(policy.burst !== undefined ? { burst: policy.burst } : {}),
239
- strategy: policy.strategy ?? 'shape',
240
- }),
241
- Stream.mapEffect(runMatching, { concurrency: 'unbounded' }),
242
- ),
243
- ),
244
- // `concurrency: 1` runs one event at a time AND in offer order — a true
245
- // serialized lane. (An unbounded fork racing for a single semaphore permit
246
- // would serialize but could reorder under contention.)
247
- Match.tag('exclusive', () => Stream.mapEffect(events, runMatching, { concurrency: 1 })),
248
- Match.exhaustive,
249
- )
250
-
251
- yield* Effect.forkScoped(Stream.runDrain(driven))
252
- channels.byName.set(name, {
253
- name,
254
- policy: channel.policy,
255
- offer: (event) => {
256
- Queue.unsafeOffer(queue, event)
257
- },
258
- })
259
- // On scope close (a lazy feature unmounting) remove the live channel so the
260
- // loop stops routing to it; the drain fiber is already torn down by
261
- // `forkScoped`. Eager channels live on the root scope, so this is a no-op
262
- // until app dispose.
263
- yield* Effect.addFinalizer(() => Effect.sync(() => channels.byName.delete(name)))
264
- }),
265
- )
@@ -1,112 +0,0 @@
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,70 +0,0 @@
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
- }
package/src/compose/ui.ts DELETED
@@ -1,97 +0,0 @@
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
@@ -1,74 +0,0 @@
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