@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
@@ -3,4 +3,5 @@
3
3
  * (High priority) through the runtime. The UI never sees Effect — it just calls
4
4
  * `events.submit({ text })`.
5
5
  */
6
- export type Trigger<P> = (payload: P) => void
6
+ export type Trigger<P> = (payload: P) => void;
7
+ //# sourceMappingURL=trigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trigger.d.ts","sourceRoot":"","sources":["../../src/ui/trigger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=trigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../../src/ui/trigger.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The serializable model of a rendered UI tree, and the pure diff/apply over it.
3
+ *
4
+ * This is the renderer-neutral heart of the remote transport (REMOTE_UI.md §3):
5
+ * the server renders a scene to a `WireTree`, sends `diff(prev, next)` as
6
+ * `WirePatch`es, and the client folds them back with `apply`. No React, no
7
+ * Effect — pure data, so both ends and the proofs share one source of truth.
8
+ */
9
+ /**
10
+ * A prop on a wire node: either an already-encoded data value, or a handle the
11
+ * client invokes to fire the server-side trigger (resolved by the trigger
12
+ * registry, REMOTE_UI.md §4). Streams, when added, become a third arm.
13
+ */
14
+ export type WireProp = {
15
+ readonly _tag: 'Data';
16
+ readonly name: string;
17
+ readonly value: unknown;
18
+ } | {
19
+ readonly _tag: 'Event';
20
+ readonly name: string;
21
+ readonly handle: string;
22
+ };
23
+ /** One rendered UI contract instance, identified stably across frames. */
24
+ export interface WireNode {
25
+ /** Stable identity across renders — the unit of diffing. */
26
+ readonly id: string;
27
+ /** The UI contract name (`UiCapture.name`) the client looks up a presentation by. */
28
+ readonly name: string;
29
+ /** Parent node id, or `null` for a root. */
30
+ readonly parentId: string | null;
31
+ /** Order among siblings under the same parent. */
32
+ readonly childIndex: number;
33
+ /** The slot name this node fills in its parent, or `null` for a root / direct child. */
34
+ readonly slot: string | null;
35
+ /**
36
+ * The React `key` the parent gave this slot child (`createElement(slots.Row, { key })`),
37
+ * or `null` when none was set. It is the per-child identity a KEYED slot selects on: the
38
+ * client renders `<slots.Row slotKey={id} />` and the slot thunk picks the one wire child
39
+ * whose `key` matches, instead of rendering every child of that slot at every call site.
40
+ * Without it the client slot is render-all-children (correct for singleton slots, wrong for
41
+ * a list slot invoked once per item — it duplicates the whole list under each call).
42
+ */
43
+ readonly key: string | null;
44
+ readonly props: ReadonlyArray<WireProp>;
45
+ }
46
+ export type WireTree = ReadonlyArray<WireNode>;
47
+ /** A change to apply to a client's tree: upsert a node, or drop one by id. */
48
+ export type WirePatch = {
49
+ readonly _tag: 'Upsert';
50
+ readonly node: WireNode;
51
+ } | {
52
+ readonly _tag: 'Delete';
53
+ readonly id: string;
54
+ };
55
+ /**
56
+ * The patches that turn `previous` into `next`. Deletes precede upserts so a
57
+ * client never holds a child whose reparented slot was freed in the same frame.
58
+ */
59
+ export declare const diff: (previous: WireTree, next: WireTree) => ReadonlyArray<WirePatch>;
60
+ /** Fold patches into a client's tree (the receiving side's reducer). */
61
+ export declare const apply: (state: WireTree, patches: ReadonlyArray<WirePatch>) => WireTree;
62
+ /** The root nodes of a tree, in sibling order. */
63
+ export declare const roots: (tree: WireTree) => WireTree;
64
+ /** The children of a node, in sibling order. */
65
+ export declare const childrenOf: (tree: WireTree, parentId: string) => WireTree;
66
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;CACxC;AAED,MAAM,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;AAE9C,8EAA8E;AAC9E,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AAoCpD;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,UAAU,QAAQ,EAAE,MAAM,QAAQ,KAAG,aAAa,CAAC,SAAS,CAahF,CAAA;AAQD,wEAAwE;AACxE,eAAO,MAAM,KAAK,GAAI,OAAO,QAAQ,EAAE,SAAS,aAAa,CAAC,SAAS,CAAC,KAAG,QASxE,CAAA;AAEH,kDAAkD;AAClD,eAAO,MAAM,KAAK,GAAI,MAAM,QAAQ,KAAG,QACoD,CAAA;AAE3F,gDAAgD;AAChD,eAAO,MAAM,UAAU,GAAI,MAAM,QAAQ,EAAE,UAAU,MAAM,KAAG,QACiC,CAAA"}
@@ -0,0 +1,60 @@
1
+ import { Match } from 'effect';
2
+ const arraysEqual = (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
3
+ const recordsEqual = (a, b) => {
4
+ const aKeys = Object.keys(a);
5
+ const bKeys = Object.keys(b);
6
+ return aKeys.length === bKeys.length && aKeys.every((key) => deepEqual(a[key], b[key]));
7
+ };
8
+ /** Structural equality over serializable wire values (and the tagged props that carry them). */
9
+ const deepEqual = (a, b) => {
10
+ if (a === b)
11
+ return true;
12
+ if (a === null || b === null)
13
+ return false;
14
+ if (Array.isArray(a))
15
+ return Array.isArray(b) && arraysEqual(a, b);
16
+ if (Array.isArray(b))
17
+ return false;
18
+ if (typeof a === 'object' && typeof b === 'object') {
19
+ return recordsEqual(a, b);
20
+ }
21
+ return false;
22
+ };
23
+ const nodesEqual = (a, b) => a.name === b.name &&
24
+ a.parentId === b.parentId &&
25
+ a.childIndex === b.childIndex &&
26
+ a.slot === b.slot &&
27
+ a.key === b.key &&
28
+ deepEqual(a.props, b.props);
29
+ const isUnchanged = (node, previousById) => {
30
+ const previous = previousById.get(node.id);
31
+ return previous !== undefined && nodesEqual(previous, node);
32
+ };
33
+ /**
34
+ * The patches that turn `previous` into `next`. Deletes precede upserts so a
35
+ * client never holds a child whose reparented slot was freed in the same frame.
36
+ */
37
+ export const diff = (previous, next) => {
38
+ const previousById = new Map(previous.map((node) => [node.id, node]));
39
+ const nextById = new Map(next.map((node) => [node.id, node]));
40
+ const deletes = previous
41
+ .filter((node) => !nextById.has(node.id))
42
+ .map((node) => ({ _tag: 'Delete', id: node.id }));
43
+ const upserts = next
44
+ .filter((node) => !isUnchanged(node, previousById))
45
+ .map((node) => ({ _tag: 'Upsert', node }));
46
+ return [...deletes, ...upserts];
47
+ };
48
+ const upsertNode = (state, node) => {
49
+ const index = state.findIndex((existing) => existing.id === node.id);
50
+ if (index < 0)
51
+ return [...state, node];
52
+ return [...state.slice(0, index), node, ...state.slice(index + 1)];
53
+ };
54
+ /** Fold patches into a client's tree (the receiving side's reducer). */
55
+ export const apply = (state, patches) => patches.reduce((current, patch) => Match.value(patch).pipe(Match.tag('Delete', ({ id }) => current.filter((node) => node.id !== id)), Match.tag('Upsert', ({ node }) => upsertNode(current, node)), Match.exhaustive), state);
56
+ /** The root nodes of a tree, in sibling order. */
57
+ export const roots = (tree) => tree.filter((node) => node.parentId === null).sort((a, b) => a.childIndex - b.childIndex);
58
+ /** The children of a node, in sibling order. */
59
+ export const childrenOf = (tree, parentId) => tree.filter((node) => node.parentId === parentId).sort((a, b) => a.childIndex - b.childIndex);
60
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { type Cause, Context, Effect, Layer, type ParseResult, Schema } from 'effect';
2
+ import type { Trigger } from '../ui/trigger';
3
+ /**
4
+ * The server-side bridge for trigger handles (REMOTE_UI.md §4). A rendered
5
+ * `WireProp` of `_tag: 'Event'` carries a `handle`; the client invokes it by
6
+ * sending `(handle, encodedPayload)`. Here we hold the live `Trigger` and its
7
+ * payload schema, decode the wire payload, and fire — so the UI never ships a
8
+ * function, only a handle, and the payload is validated at the seam.
9
+ *
10
+ * Handles are caller-provided keys, by convention `${nodeId}:${eventName}`: a
11
+ * re-render re-`register`s the same handle with that frame's fresh trigger, so the
12
+ * handle is stable across frames (the client's in-flight handle stays valid) and
13
+ * the handle space never grows. The schema is the contract's own per-event wire
14
+ * schema (`WiredUiManifest.events`, Phase 1) — the single source of truth for the
15
+ * UI boundary, decoupled from which `Event` the composition binds behind it.
16
+ */
17
+ export type TriggerHandle = string;
18
+ declare const UnknownTriggerBase: new (args: {
19
+ readonly handle: TriggerHandle;
20
+ }) => Cause.YieldableError & {
21
+ readonly _tag: 'reform/UnknownTrigger';
22
+ } & Readonly<{
23
+ readonly handle: TriggerHandle;
24
+ }>;
25
+ /** A wire message named a trigger handle the server no longer holds (a stale client). */
26
+ export declare class UnknownTrigger extends UnknownTriggerBase {
27
+ get message(): string;
28
+ }
29
+ export interface TriggerRegistryApi {
30
+ /** Register (or overwrite) a trigger + its payload schema under a handle key. */
31
+ readonly register: <P>(handle: TriggerHandle, trigger: Trigger<P>, schema: Schema.Schema<P, any>) => Effect.Effect<void>;
32
+ /** Decode an encoded payload via the handle's schema and fire the trigger. */
33
+ readonly invoke: (handle: TriggerHandle, encodedPayload: unknown) => Effect.Effect<void, UnknownTrigger | ParseResult.ParseError>;
34
+ /** Drop a handle once its node unmounts (the Phase 2 tree diff signals it). */
35
+ readonly revoke: (handle: TriggerHandle) => Effect.Effect<void>;
36
+ }
37
+ declare const TriggerRegistryBase: Context.TagClass<TriggerRegistry, 'reform/wire/TriggerRegistry', TriggerRegistryApi>;
38
+ export declare class TriggerRegistry extends TriggerRegistryBase {
39
+ }
40
+ export declare const make: Effect.Effect<TriggerRegistryApi>;
41
+ /** Provide a fresh registry — one per remote connection (each client gets its own handle space). */
42
+ export declare const layer: Layer.Layer<TriggerRegistry>;
43
+ export {};
44
+ //# sourceMappingURL=triggers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triggers.d.ts","sourceRoot":"","sources":["../../src/wire/triggers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,OAAO,EAAQ,MAAM,EAAE,KAAK,EAAE,KAAK,WAAW,EAAO,MAAM,EAAE,MAAM,QAAQ,CAAA;AAChG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAE5C;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAA;AAUlC,QAAA,MAAM,kBAAkB,EAAE,KAAK,IAAI,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,KAAK,KAAK,CAAC,cAAc,GAAG;IACjG,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAA;CACvC,GAAG,QAAQ,CAAC;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAE7C,CAAA;AAEF,yFAAyF;AACzF,qBAAa,cAAe,SAAQ,kBAAkB;IACpD,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;AAED,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,EACnB,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,OAAO,KACpB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;IACjE,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAChE;AAED,QAAA,MAAM,mBAAmB,EAAE,OAAO,CAAC,QAAQ,CACzC,eAAe,EACf,6BAA6B,EAC7B,kBAAkB,CACiE,CAAA;AAErF,qBAAa,eAAgB,SAAQ,mBAAmB;CAAG;AAE3D,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,kBAAkB,CA6BjD,CAAA;AAEF,oGAAoG;AACpG,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,eAAe,CAAuC,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { Context, Data, Effect, Layer, Ref, Schema } from 'effect';
2
+ const UnknownTriggerBase = (Data.TaggedError('reform/UnknownTrigger'));
3
+ /** A wire message named a trigger handle the server no longer holds (a stale client). */
4
+ export class UnknownTrigger extends UnknownTriggerBase {
5
+ get message() {
6
+ return `reform: no trigger registered under handle '${this.handle}'`;
7
+ }
8
+ }
9
+ const TriggerRegistryBase = Context.Tag('reform/wire/TriggerRegistry')();
10
+ export class TriggerRegistry extends TriggerRegistryBase {
11
+ }
12
+ export const make = Effect.gen(function* () {
13
+ const entries = yield* Ref.make(new Map());
14
+ const register = (handle, trigger, schema) => Ref.update(entries, (map) => new Map(map).set(handle, { trigger, schema }));
15
+ const invoke = (handle, encodedPayload) => Effect.gen(function* () {
16
+ const entry = (yield* Ref.get(entries)).get(handle);
17
+ if (entry === undefined)
18
+ return yield* Effect.fail(new UnknownTrigger({ handle }));
19
+ const payload = yield* Schema.decodeUnknown(entry.schema)(encodedPayload);
20
+ entry.trigger(payload);
21
+ });
22
+ const revoke = (handle) => Ref.update(entries, (map) => {
23
+ const next = new Map(map);
24
+ next.delete(handle);
25
+ return next;
26
+ });
27
+ return { register, invoke, revoke };
28
+ });
29
+ /** Provide a fresh registry — one per remote connection (each client gets its own handle space). */
30
+ export const layer = Layer.effect(TriggerRegistry, make);
31
+ //# sourceMappingURL=triggers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triggers.js","sourceRoot":"","sources":["../../src/wire/triggers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAoB,GAAG,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AA4BhG,MAAM,kBAAkB,GAE2B,CAAA,IAAI,CAAC,WAAW,CAAC,uBAAuB,CAEzF,CAAA,CAAA;AAEF,yFAAyF;AACzF,MAAM,OAAO,cAAe,SAAQ,kBAAkB;IACpD,IAAa,OAAO;QAClB,OAAO,+CAA+C,IAAI,CAAC,MAAM,GAAG,CAAA;IACtE,CAAC;CACF;AAkBD,MAAM,mBAAmB,GAIrB,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,EAAuC,CAAA;AAErF,MAAM,OAAO,eAAgB,SAAQ,mBAAmB;CAAG;AAE3D,MAAM,CAAC,MAAM,IAAI,GAAsC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACzE,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,EAA+B,CAAC,CAAA;IAEvE,MAAM,QAAQ,GAAG,CACf,MAAqB,EACrB,OAAmB,EACnB,MAA6B,EACR,EAAE,CACvB,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IAE7E,MAAM,MAAM,GAAG,CACb,MAAqB,EACrB,cAAuB,EACuC,EAAE,CAChE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACnD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;QAClF,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAA;QACzE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACxB,CAAC,CAAC,CAAA;IAEJ,MAAM,MAAM,GAAG,CAAC,MAAqB,EAAuB,EAAE,CAC5D,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QACzB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACnB,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,CAAA;IAEJ,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;AACrC,CAAC,CAAC,CAAA;AAEF,oGAAoG;AACpG,MAAM,CAAC,MAAM,KAAK,GAAiC,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,24 +1,45 @@
1
1
  {
2
2
  "name": "@playfast/reform",
3
- "version": "0.0.4",
3
+ "playbook": "./playbook",
4
+ "version": "0.0.7",
4
5
  "type": "module",
5
- "sideEffects": [],
6
+ "description": "The renderer-neutral core of the reform framework — typed, headless state, events, reducers, derived values, async/remote data, and compositions built on Effect.",
7
+ "keywords": [
8
+ "reform",
9
+ "effect",
10
+ "state-management",
11
+ "headless",
12
+ "framework",
13
+ "reactive"
14
+ ],
6
15
  "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/playfast/reform.git",
19
+ "directory": "packages/reform"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/playfast/reform/issues"
23
+ },
24
+ "sideEffects": false,
7
25
  "exports": {
8
26
  "./package.json": "./package.json",
9
- ".": "./src/index.ts"
27
+ ".": "./src/index.ts",
28
+ "./*": "./src/*.ts"
10
29
  },
11
30
  "files": [
12
- "src",
13
- "README.md",
14
- "!src/**/*.test.ts",
15
- "!src/**/*.test.tsx",
16
- "!src/**/*.typecheck.ts",
17
- "!src/**/*.typecheck.tsx"
31
+ "dist",
32
+ "README.md"
18
33
  ],
19
34
  "scripts": {
20
- "typecheck": "tsc --noEmit",
21
- "test": "vitest run"
35
+ "clean": "rm -rf dist .tsbuildinfo",
36
+ "check": "tsc --noEmit",
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "coverage": "vitest run --coverage",
41
+ "lint": "oxlint src",
42
+ "lint:fix": "oxlint --fix src"
22
43
  },
23
44
  "peerDependencies": {
24
45
  "effect": "*",
@@ -1,192 +0,0 @@
1
- import { Context, Data, Effect, Layer, MutableRef, Predicate } from 'effect'
2
- import { type Manifest, yieldableClass } from '../definition/definition'
3
- import { resolveScheduler } from '../internal/scheduler'
4
- import { type InputStores, sameKey, wireSources } from '../internal/sources'
5
- import { makeDerivedStore, type Store } from '../internal/store'
6
- import { readTracked } from '../internal/track'
7
- import { type Source } from '../state/token'
8
-
9
- // Boundary: the Suspense-boundary analog, value-level. One derived store that
10
- // merges the lifecycles of several async sources into a single
11
- // Pending/Errored/Ready value, so ONE surface (a splash, a skeleton) covers a
12
- // whole subtree's first load instead of every query consumer painting its own.
13
- //
14
- // Reform can do this without React's throw-a-promise machinery because
15
- // fetching is layer-driven, not render-driven: an `AsyncCalc` runs because its
16
- // layer is built, never because something rendered. A boundary therefore only
17
- // *aggregates* lifecycles — it cannot create render-waterfalls, and content it
18
- // reveals already has its data.
19
- //
20
- // The arm mapping IS the semantics, and most of it falls out of `AsyncData`:
21
- // Loading → Pending (the arm only exists before the first value,
22
- // so a boundary is first-load-only by construction)
23
- // Success → Ready (even `refetching: true` — SWR refetches never
24
- // re-show the fallback; no transition machinery)
25
- // Idle → Ready (a gated query that is off is DELIBERATELY off —
26
- // a signed-out viewer must not wait on a query
27
- // that only runs signed-in)
28
- // Error → Errored (the error-boundary half, also value-level)
29
- //
30
- // Chained gates (B enables when A succeeds) cannot leak a Ready between hops:
31
- // the boundary's layer requires every covered store, so each driver subscribes
32
- // upstream BEFORE the boundary does, the scheduler flush runs listeners in
33
- // subscription order to a fixpoint within one microtask, and reads are
34
- // pull-fresh — by the time any subscriber reads, the next hop is already
35
- // Loading. `boundary.test.ts` pins this with a recorded-notification proof.
36
-
37
- /** Some covered source is still on its FIRST load — show the fallback. */
38
- export interface BoundaryPending {
39
- readonly _tag: 'Pending'
40
- }
41
-
42
- /** Some covered source failed before its first value. */
43
- export interface BoundaryErrored {
44
- readonly _tag: 'Errored'
45
- /** The failures, in `over` order (heterogeneous — display via `String`). */
46
- readonly errors: ReadonlyArray<unknown>
47
- }
48
-
49
- /** Every covered source has settled (or is deliberately gated off). */
50
- export interface BoundaryReady {
51
- readonly _tag: 'Ready'
52
- }
53
-
54
- export type BoundaryState = BoundaryPending | BoundaryErrored | BoundaryReady
55
-
56
- // Stable arm references: repeated recomputes return the same object, so the
57
- // derived store's Equal gate (and `useSyncExternalStore`'s stable-snapshot
58
- // contract) hold without a wrapper. `Errored` carries data, so it is a `Data`
59
- // struct — value-equal failures coalesce the same way.
60
- const pendingArm: BoundaryState = { _tag: 'Pending' }
61
- const readyArm: BoundaryState = { _tag: 'Ready' }
62
- const erroredArm = (errors: ReadonlyArray<unknown>): BoundaryState =>
63
- Data.struct({ _tag: 'Errored' as const, errors: Data.array(errors) })
64
-
65
- /**
66
- * Anything a boundary can cover: a `Source` whose value is an async lifecycle —
67
- * an `AsyncCalc`, or anything lifecycle-valued built over one (e.g. a
68
- * `RemoteState` overlay). The constraint is the same erased `Source` every calc
69
- * input uses (store invariance rules out a fixed lifecycle value type — each
70
- * definition's union carries its own `Gated`/`E` arms); the captured `const`
71
- * tuple keeps each member's precise store requirement, and the runtime merge
72
- * reads arms through structural guards, so a non-lifecycle source simply
73
- * counts as settled.
74
- */
75
- export type AnyLifecycleSource = Source<string, any>
76
-
77
- export interface BoundaryManifest<N extends string> extends Manifest {
78
- readonly kind: 'Boundary'
79
- readonly name: N
80
- }
81
-
82
- export interface BoundaryClass<out N extends string, out Over extends ReadonlyArray<AnyLifecycleSource>>
83
- extends Effect.Effect<BoundaryState, never, Store<BoundaryState>> {
84
- new (): {}
85
- readonly manifest: BoundaryManifest<N>
86
- readonly store: Context.Tag<Store<BoundaryState>, Store<BoundaryState>>
87
- /** The boundary's name, so it doubles as a `Source` input to a calc. */
88
- readonly name: N
89
- /** The covered lifecycle sources — read by `live` and the editor's graph. */
90
- readonly over: Over
91
- }
92
-
93
- export interface BoundaryConfig<Over extends ReadonlyArray<AnyLifecycleSource>> {
94
- readonly over: Over
95
- }
96
-
97
- export interface BoundaryOptions {
98
- /**
99
- * Latch: once Ready, stay Ready. A BOOT boundary wants this — a covered
100
- * route-gated query that first enables on a later navigation (Idle →
101
- * Loading) must not re-splash the whole shell; its own screen owns that
102
- * loading state. Without the latch the boundary re-pends on any covered
103
- * first load, which is what a per-screen boundary wants.
104
- */
105
- readonly once?: boolean
106
- }
107
-
108
- /**
109
- * Define a boundary over async sources. `yield* MyBoundary` reads the merged
110
- * `Pending | Errored | Ready` lifecycle; the consuming view renders ONE
111
- * fallback on Pending and its content slot on Ready — content reveals
112
- * together, with data already present (fetching never waited on rendering).
113
- */
114
- export const make = <const N extends string, const Over extends ReadonlyArray<AnyLifecycleSource>>(
115
- name: N,
116
- config: BoundaryConfig<Over>,
117
- ): BoundaryClass<N, Over> => {
118
- const store = Context.GenericTag<Store<BoundaryState>, Store<BoundaryState>>(
119
- `reform/boundary/${name}`,
120
- )
121
- const manifest: BoundaryManifest<N> = { kind: 'Boundary', name }
122
- const read = Effect.flatMap(store, readTracked)
123
- return yieldableClass(read, { manifest, store, name, over: config.over })
124
- }
125
-
126
- // The snapshot values arrive through `wireSources`' erased key projection
127
- // (`ReadonlyArray<unknown>`), so read the two facts the merge needs through
128
- // honest structural guards — no cast back to `AnyAsyncData`.
129
- const tagOf = (arm: unknown): string =>
130
- Predicate.hasProperty(arm, '_tag') && Predicate.isString(arm._tag) ? arm._tag : ''
131
- const errorOf = (arm: unknown): ReadonlyArray<unknown> =>
132
- Predicate.hasProperty(arm, 'error') && tagOf(arm) === 'Error' ? [arm.error] : []
133
-
134
- /**
135
- * Wire the merge. A derived store over the covered stores (sensed through the
136
- * same `wireSources` plumbing every calc uses): recomputes are memoized on the
137
- * input snapshots and the arms are stable references, so input churn that does
138
- * not move the merged arm wakes nobody.
139
- */
140
- export const live = <N extends string, Over extends ReadonlyArray<AnyLifecycleSource>>(
141
- boundary: BoundaryClass<N, Over>,
142
- options: BoundaryOptions = {},
143
- ): Layer.Layer<Store<BoundaryState>, never, InputStores<Over>> =>
144
- // Scoped so the source subscriptions are released with the layer's scope.
145
- Layer.scoped(
146
- boundary.store,
147
- Effect.gen(function* () {
148
- const scheduler = yield* resolveScheduler
149
- const sources = yield* wireSources(boundary.over)
150
-
151
- const latched = MutableRef.make(false)
152
- const memo = MutableRef.make<
153
- { readonly key: ReadonlyArray<unknown>; readonly value: BoundaryState } | undefined
154
- >(undefined)
155
- const recompute = (): BoundaryState => {
156
- if (options.once === true && MutableRef.get(latched)) return readyArm
157
- const arms = sources.keyOf(sources.snapshot())
158
- const prev = MutableRef.get(memo)
159
- if (prev !== undefined && sameKey(arms, prev.key)) return prev.value
160
- const errors = arms.flatMap(errorOf)
161
- const value =
162
- errors.length > 0
163
- ? erroredArm(errors)
164
- : arms.some((arm) => tagOf(arm) === 'Loading')
165
- ? pendingArm
166
- : readyArm
167
- MutableRef.set(memo, { key: arms, value })
168
- return value
169
- }
170
-
171
- const derived = makeDerivedStore(recompute, sources.subscribe, scheduler)
172
- yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
173
-
174
- if (options.once === true) {
175
- // Latch on CONVERGED values only — at build and on (coalesced,
176
- // post-fixpoint) notifications — never inside `recompute`. A chained
177
- // gate with a plain Calc hop between the async stores (bootstrap →
178
- // current-workspace → boards) propagates over TWO flush rounds, so a
179
- // mid-flush recompute can observe the frame where the downstream
180
- // driver has not yet flipped its query to Loading. That intermediate
181
- // Ready self-corrects within the flush for readers, but a latch taken
182
- // there would freeze it; a subscriber runs only after the fixpoint,
183
- // where the value is converged.
184
- if (recompute()._tag === 'Ready') MutableRef.set(latched, true)
185
- const offSelf = derived.store.subscribe(() => {
186
- if (derived.store.get()._tag === 'Ready') MutableRef.set(latched, true)
187
- })
188
- yield* Effect.addFinalizer(() => Effect.sync(offSelf))
189
- }
190
- return derived.store
191
- }),
192
- )