@playfast/reform 0.0.1

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 (204) hide show
  1. package/README.md +64 -0
  2. package/dist/cjs/boundary/boundary.js +86 -0
  3. package/dist/cjs/calc/asyncCalc.js +128 -0
  4. package/dist/cjs/calc/asyncData.js +37 -0
  5. package/dist/cjs/calc/calc.js +58 -0
  6. package/dist/cjs/calc/calcFamily.js +127 -0
  7. package/dist/cjs/channel/channel.js +142 -0
  8. package/dist/cjs/compose/composition.js +50 -0
  9. package/dist/cjs/compose/host.js +8 -0
  10. package/dist/cjs/compose/props.js +14 -0
  11. package/dist/cjs/compose/provide.js +30 -0
  12. package/dist/cjs/compose/slot.js +27 -0
  13. package/dist/cjs/compose/ui.js +61 -0
  14. package/dist/cjs/definition/definition.js +46 -0
  15. package/dist/cjs/event/event.js +36 -0
  16. package/dist/cjs/event/eventGroup.js +7 -0
  17. package/dist/cjs/feature/feature.js +102 -0
  18. package/dist/cjs/index.js +116 -0
  19. package/dist/cjs/internal/capture.js +14 -0
  20. package/dist/cjs/internal/ctx.js +2 -0
  21. package/dist/cjs/internal/errors.js +62 -0
  22. package/dist/cjs/internal/inspect.js +36 -0
  23. package/dist/cjs/internal/queryDriver.js +138 -0
  24. package/dist/cjs/internal/reuse.js +71 -0
  25. package/dist/cjs/internal/scheduler.js +73 -0
  26. package/dist/cjs/internal/seeds.js +19 -0
  27. package/dist/cjs/internal/sources.js +61 -0
  28. package/dist/cjs/internal/store.js +77 -0
  29. package/dist/cjs/internal/track.js +22 -0
  30. package/dist/cjs/package.json +4 -0
  31. package/dist/cjs/procedure/procedure.js +52 -0
  32. package/dist/cjs/reducer/reducer.js +64 -0
  33. package/dist/cjs/remote/remoteState.js +307 -0
  34. package/dist/cjs/runtime/bus.js +25 -0
  35. package/dist/cjs/runtime/loop.js +119 -0
  36. package/dist/cjs/scene/scene.js +36 -0
  37. package/dist/cjs/state/state.js +47 -0
  38. package/dist/cjs/state/stateFamily.js +101 -0
  39. package/dist/cjs/state/stateGroup.js +47 -0
  40. package/dist/cjs/state/token.js +23 -0
  41. package/dist/cjs/ui/node.js +2 -0
  42. package/dist/cjs/ui/trigger.js +2 -0
  43. package/dist/dts/boundary/boundary.d.ts +72 -0
  44. package/dist/dts/boundary/boundary.d.ts.map +1 -0
  45. package/dist/dts/calc/asyncCalc.d.ts +91 -0
  46. package/dist/dts/calc/asyncCalc.d.ts.map +1 -0
  47. package/dist/dts/calc/asyncData.d.ts +55 -0
  48. package/dist/dts/calc/asyncData.d.ts.map +1 -0
  49. package/dist/dts/calc/calc.d.ts +57 -0
  50. package/dist/dts/calc/calc.d.ts.map +1 -0
  51. package/dist/dts/calc/calcFamily.d.ts +57 -0
  52. package/dist/dts/calc/calcFamily.d.ts.map +1 -0
  53. package/dist/dts/channel/channel.d.ts +115 -0
  54. package/dist/dts/channel/channel.d.ts.map +1 -0
  55. package/dist/dts/compose/composition.d.ts +72 -0
  56. package/dist/dts/compose/composition.d.ts.map +1 -0
  57. package/dist/dts/compose/host.d.ts +17 -0
  58. package/dist/dts/compose/host.d.ts.map +1 -0
  59. package/dist/dts/compose/props.d.ts +13 -0
  60. package/dist/dts/compose/props.d.ts.map +1 -0
  61. package/dist/dts/compose/provide.d.ts +22 -0
  62. package/dist/dts/compose/provide.d.ts.map +1 -0
  63. package/dist/dts/compose/slot.d.ts +49 -0
  64. package/dist/dts/compose/slot.d.ts.map +1 -0
  65. package/dist/dts/compose/ui.d.ts +50 -0
  66. package/dist/dts/compose/ui.d.ts.map +1 -0
  67. package/dist/dts/definition/definition.d.ts +33 -0
  68. package/dist/dts/definition/definition.d.ts.map +1 -0
  69. package/dist/dts/event/event.d.ts +33 -0
  70. package/dist/dts/event/event.d.ts.map +1 -0
  71. package/dist/dts/event/eventGroup.d.ts +9 -0
  72. package/dist/dts/event/eventGroup.d.ts.map +1 -0
  73. package/dist/dts/feature/feature.d.ts +220 -0
  74. package/dist/dts/feature/feature.d.ts.map +1 -0
  75. package/dist/dts/index.d.ts +43 -0
  76. package/dist/dts/index.d.ts.map +1 -0
  77. package/dist/dts/internal/capture.d.ts +28 -0
  78. package/dist/dts/internal/capture.d.ts.map +1 -0
  79. package/dist/dts/internal/ctx.d.ts +12 -0
  80. package/dist/dts/internal/ctx.d.ts.map +1 -0
  81. package/dist/dts/internal/errors.d.ts +69 -0
  82. package/dist/dts/internal/errors.d.ts.map +1 -0
  83. package/dist/dts/internal/inspect.d.ts +17 -0
  84. package/dist/dts/internal/inspect.d.ts.map +1 -0
  85. package/dist/dts/internal/queryDriver.d.ts +65 -0
  86. package/dist/dts/internal/queryDriver.d.ts.map +1 -0
  87. package/dist/dts/internal/reuse.d.ts +10 -0
  88. package/dist/dts/internal/reuse.d.ts.map +1 -0
  89. package/dist/dts/internal/scheduler.d.ts +47 -0
  90. package/dist/dts/internal/scheduler.d.ts.map +1 -0
  91. package/dist/dts/internal/seeds.d.ts +17 -0
  92. package/dist/dts/internal/seeds.d.ts.map +1 -0
  93. package/dist/dts/internal/sources.d.ts +39 -0
  94. package/dist/dts/internal/sources.d.ts.map +1 -0
  95. package/dist/dts/internal/store.d.ts +47 -0
  96. package/dist/dts/internal/store.d.ts.map +1 -0
  97. package/dist/dts/internal/track.d.ts +33 -0
  98. package/dist/dts/internal/track.d.ts.map +1 -0
  99. package/dist/dts/procedure/procedure.d.ts +40 -0
  100. package/dist/dts/procedure/procedure.d.ts.map +1 -0
  101. package/dist/dts/reducer/reducer.d.ts +44 -0
  102. package/dist/dts/reducer/reducer.d.ts.map +1 -0
  103. package/dist/dts/remote/remoteState.d.ts +119 -0
  104. package/dist/dts/remote/remoteState.d.ts.map +1 -0
  105. package/dist/dts/runtime/bus.d.ts +27 -0
  106. package/dist/dts/runtime/bus.d.ts.map +1 -0
  107. package/dist/dts/runtime/loop.d.ts +45 -0
  108. package/dist/dts/runtime/loop.d.ts.map +1 -0
  109. package/dist/dts/scene/scene.d.ts +44 -0
  110. package/dist/dts/scene/scene.d.ts.map +1 -0
  111. package/dist/dts/state/state.d.ts +37 -0
  112. package/dist/dts/state/state.d.ts.map +1 -0
  113. package/dist/dts/state/stateFamily.d.ts +79 -0
  114. package/dist/dts/state/stateFamily.d.ts.map +1 -0
  115. package/dist/dts/state/stateGroup.d.ts +36 -0
  116. package/dist/dts/state/stateGroup.d.ts.map +1 -0
  117. package/dist/dts/state/token.d.ts +30 -0
  118. package/dist/dts/state/token.d.ts.map +1 -0
  119. package/dist/dts/ui/node.d.ts +9 -0
  120. package/dist/dts/ui/node.d.ts.map +1 -0
  121. package/dist/dts/ui/trigger.d.ts +7 -0
  122. package/dist/dts/ui/trigger.d.ts.map +1 -0
  123. package/dist/esm/boundary/boundary.js +83 -0
  124. package/dist/esm/boundary/boundary.js.map +1 -0
  125. package/dist/esm/calc/asyncCalc.js +95 -0
  126. package/dist/esm/calc/asyncCalc.js.map +1 -0
  127. package/dist/esm/calc/asyncData.js +34 -0
  128. package/dist/esm/calc/asyncData.js.map +1 -0
  129. package/dist/esm/calc/calc.js +58 -0
  130. package/dist/esm/calc/calc.js.map +1 -0
  131. package/dist/esm/calc/calcFamily.js +124 -0
  132. package/dist/esm/calc/calcFamily.js.map +1 -0
  133. package/dist/esm/channel/channel.js +136 -0
  134. package/dist/esm/channel/channel.js.map +1 -0
  135. package/dist/esm/compose/composition.js +46 -0
  136. package/dist/esm/compose/composition.js.map +1 -0
  137. package/dist/esm/compose/host.js +5 -0
  138. package/dist/esm/compose/host.js.map +1 -0
  139. package/dist/esm/compose/props.js +11 -0
  140. package/dist/esm/compose/props.js.map +1 -0
  141. package/dist/esm/compose/provide.js +28 -0
  142. package/dist/esm/compose/provide.js.map +1 -0
  143. package/dist/esm/compose/slot.js +23 -0
  144. package/dist/esm/compose/slot.js.map +1 -0
  145. package/dist/esm/compose/ui.js +57 -0
  146. package/dist/esm/compose/ui.js.map +1 -0
  147. package/dist/esm/definition/definition.js +42 -0
  148. package/dist/esm/definition/definition.js.map +1 -0
  149. package/dist/esm/event/event.js +30 -0
  150. package/dist/esm/event/event.js.map +1 -0
  151. package/dist/esm/event/eventGroup.js +4 -0
  152. package/dist/esm/event/eventGroup.js.map +1 -0
  153. package/dist/esm/feature/feature.js +98 -0
  154. package/dist/esm/feature/feature.js.map +1 -0
  155. package/dist/esm/index.js +45 -0
  156. package/dist/esm/index.js.map +1 -0
  157. package/dist/esm/internal/capture.js +11 -0
  158. package/dist/esm/internal/capture.js.map +1 -0
  159. package/dist/esm/internal/ctx.js +2 -0
  160. package/dist/esm/internal/ctx.js.map +1 -0
  161. package/dist/esm/internal/errors.js +54 -0
  162. package/dist/esm/internal/errors.js.map +1 -0
  163. package/dist/esm/internal/inspect.js +32 -0
  164. package/dist/esm/internal/inspect.js.map +1 -0
  165. package/dist/esm/internal/queryDriver.js +134 -0
  166. package/dist/esm/internal/queryDriver.js.map +1 -0
  167. package/dist/esm/internal/reuse.js +68 -0
  168. package/dist/esm/internal/reuse.js.map +1 -0
  169. package/dist/esm/internal/scheduler.js +69 -0
  170. package/dist/esm/internal/scheduler.js.map +1 -0
  171. package/dist/esm/internal/seeds.js +17 -0
  172. package/dist/esm/internal/seeds.js.map +1 -0
  173. package/dist/esm/internal/sources.js +59 -0
  174. package/dist/esm/internal/sources.js.map +1 -0
  175. package/dist/esm/internal/store.js +73 -0
  176. package/dist/esm/internal/store.js.map +1 -0
  177. package/dist/esm/internal/track.js +18 -0
  178. package/dist/esm/internal/track.js.map +1 -0
  179. package/dist/esm/package.json +4 -0
  180. package/dist/esm/procedure/procedure.js +50 -0
  181. package/dist/esm/procedure/procedure.js.map +1 -0
  182. package/dist/esm/reducer/reducer.js +63 -0
  183. package/dist/esm/reducer/reducer.js.map +1 -0
  184. package/dist/esm/remote/remoteState.js +270 -0
  185. package/dist/esm/remote/remoteState.js.map +1 -0
  186. package/dist/esm/runtime/bus.js +20 -0
  187. package/dist/esm/runtime/bus.js.map +1 -0
  188. package/dist/esm/runtime/loop.js +116 -0
  189. package/dist/esm/runtime/loop.js.map +1 -0
  190. package/dist/esm/scene/scene.js +31 -0
  191. package/dist/esm/scene/scene.js.map +1 -0
  192. package/dist/esm/state/state.js +43 -0
  193. package/dist/esm/state/state.js.map +1 -0
  194. package/dist/esm/state/stateFamily.js +96 -0
  195. package/dist/esm/state/stateFamily.js.map +1 -0
  196. package/dist/esm/state/stateGroup.js +46 -0
  197. package/dist/esm/state/stateGroup.js.map +1 -0
  198. package/dist/esm/state/token.js +20 -0
  199. package/dist/esm/state/token.js.map +1 -0
  200. package/dist/esm/ui/node.js +2 -0
  201. package/dist/esm/ui/node.js.map +1 -0
  202. package/dist/esm/ui/trigger.js +2 -0
  203. package/dist/esm/ui/trigger.js.map +1 -0
  204. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # `reform` — core
2
+
3
+ The renderer-neutral primitives and runtime. Renders nothing itself (that is
4
+ `@reform/react`); the whole core is headless and testable without a DOM.
5
+
6
+ ## What's here (v0)
7
+
8
+ Every primitive follows the **definition / implementation split**: `X.make(…)`
9
+ is a reflectable definition (a manifest + DI tag), provided separately by
10
+ `.live`. Nothing self-registers on
11
+ import.
12
+
13
+ | primitive | definition | implementation |
14
+ | ---------------------------------------- | ----------------------------------- | --------------------- |
15
+ | `State` / `StateGroup` / `StateFamily` | `State.make(name, schema, opts)` | `.live` |
16
+ | `Event` / `EventGroup` | `Event.make(name, schema)` | — (pure data) |
17
+ | `Reducer` | `Reducer.make(name, { states/family, events })` | `.live(fold)` |
18
+ | `Calc` | `Calc.make(name, { inputs, output })` | `.live(fn)` |
19
+ | `AsyncCalc` | `AsyncCalc.make(name, { inputs, output, error?, alwaysOn? })` | `.live({ query })` |
20
+ | `RemoteState` | `RemoteState.make(name, { inputs, output, error?, alwaysOn?, intents })` | `.live({ query, send, apply, … })` |
21
+ | `Procedure` | `Procedure.make(name, { events, concurrency })` | `.live(fn*)` |
22
+ | `Composition` | `Composition.make(name, manifest)` | `.live(fn*)` |
23
+ | `ui` / `slot` | `ui(name)<C>()` / `slot(name)<P>()` | `.make` / `provide` |
24
+
25
+ ## The model
26
+
27
+ ```
28
+ UI trigger ──► Bus (High) ─┐
29
+ ├─► reduce loop ──► reducers (the only writers) ──► stores
30
+ procedure ──► Bus (Normal)┘ │
31
+ ▲ ▼
32
+ └────────────── reads state, dispatches ◄──── calc (derived, memoized)
33
+ ```
34
+
35
+ - **State** lives in plain reactive stores outside Effect; `set` is internal, so
36
+ reducers (which only *return* values) are the sole writers by construction.
37
+ - **The bus** is one bounded `PubSub`. A central loop drains it and runs matching
38
+ reducers per event; **procedures** consume it on their own forked fibers with a
39
+ concurrency policy (`latest` = switch, `merge` = unbounded).
40
+ - **Reads** (`yield* group.state('x')`, `yield* Calc`, `family.at(k).read`) return
41
+ the current snapshot. The yieldable class trick (a class whose static prototype
42
+ is an Effect) is what lets `yield* VisibleTodos` return the derived value.
43
+ - **Derived reads** take any `Source` as input (a state member, a `Calc`, or an
44
+ `AsyncCalc`) and accept an optional `invalidateBy` queryKey projection that
45
+ bounds recompute/re-fetch to a value-equal key change. `AsyncCalc` yields an
46
+ `AsyncData` query lifecycle (`Idle`/`Loading`/`Success`/`Error`), matched with
47
+ `effect`'s `Match`.
48
+ - **Remote state is derived-only.** `RemoteState` is server-owned state with
49
+ optimistic mutations: the only writer is the server, the only write surface is
50
+ dispatching one of the declared intent events, and the visible value is
51
+ `pending.reduce(apply, serverTruth)`. A failed or settled intent leaves the
52
+ queue and the derivation converges — no rollback machinery. Settles obey the
53
+ generation rule (an intent acked at query-run generation g settles only on a
54
+ later-generation `Success` run), so an optimistic change never flickers out
55
+ between the RPC confirm and the refetch landing.
56
+ - **Notifications** coalesce into one microtask flush (the React bridge will bind
57
+ `getSnapshot`/`subscribe`).
58
+
59
+ ## Status
60
+
61
+ Engine complete and proven headless (see `examples/todo/src/app/engine.test.ts`):
62
+ boot → load → add → toggle → filter flows through events, procedures, reducers
63
+ and the calc, and a composition renders against a capturing UI. React rendering,
64
+ the editor and proofs are the next phases.
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.live = exports.make = void 0;
4
+ const effect_1 = require("effect");
5
+ const definition_js_1 = require("../definition/definition.js");
6
+ const scheduler_js_1 = require("../internal/scheduler.js");
7
+ const sources_js_1 = require("../internal/sources.js");
8
+ const store_js_1 = require("../internal/store.js");
9
+ const track_js_1 = require("../internal/track.js");
10
+ // Stable arm references: repeated recomputes return the same object, so the
11
+ // derived store's Equal gate (and `useSyncExternalStore`'s stable-snapshot
12
+ // contract) hold without a wrapper. `Errored` carries data, so it is a `Data`
13
+ // struct — value-equal failures coalesce the same way.
14
+ const pendingArm = { _tag: 'Pending' };
15
+ const readyArm = { _tag: 'Ready' };
16
+ const erroredArm = (errors) => effect_1.Data.struct({ _tag: 'Errored', errors: effect_1.Data.array(errors) });
17
+ /**
18
+ * Define a boundary over async sources. `yield* MyBoundary` reads the merged
19
+ * `Pending | Errored | Ready` lifecycle; the consuming view renders ONE
20
+ * fallback on Pending and its content slot on Ready — content reveals
21
+ * together, with data already present (fetching never waited on rendering).
22
+ */
23
+ const make = (name, config) => {
24
+ const store = effect_1.Context.GenericTag(`reform/boundary/${name}`);
25
+ const manifest = { kind: 'Boundary', name };
26
+ const read = effect_1.Effect.flatMap(store, track_js_1.readTracked);
27
+ return (0, definition_js_1.yieldableClass)(read, { manifest, store, name, over: config.over });
28
+ };
29
+ exports.make = make;
30
+ // The snapshot values arrive through `wireSources`' erased key projection
31
+ // (`ReadonlyArray<unknown>`), so read the two facts the merge needs through
32
+ // honest structural guards — no cast back to `AnyAsyncData`.
33
+ const tagOf = (arm) => effect_1.Predicate.hasProperty(arm, '_tag') && effect_1.Predicate.isString(arm._tag) ? arm._tag : '';
34
+ const errorOf = (arm) => effect_1.Predicate.hasProperty(arm, 'error') && tagOf(arm) === 'Error' ? [arm.error] : [];
35
+ /**
36
+ * Wire the merge. A derived store over the covered stores (sensed through the
37
+ * same `wireSources` plumbing every calc uses): recomputes are memoized on the
38
+ * input snapshots and the arms are stable references, so input churn that does
39
+ * not move the merged arm wakes nobody.
40
+ */
41
+ const live = (boundary, options = {}) =>
42
+ // Scoped so the source subscriptions are released with the layer's scope.
43
+ effect_1.Layer.scoped(boundary.store, effect_1.Effect.gen(function* () {
44
+ const scheduler = yield* scheduler_js_1.resolveScheduler;
45
+ const sources = yield* (0, sources_js_1.wireSources)(boundary.over);
46
+ const latched = effect_1.MutableRef.make(false);
47
+ const memo = effect_1.MutableRef.make(undefined);
48
+ const recompute = () => {
49
+ if (options.once === true && effect_1.MutableRef.get(latched))
50
+ return readyArm;
51
+ const arms = sources.keyOf(sources.snapshot());
52
+ const prev = effect_1.MutableRef.get(memo);
53
+ if (prev !== undefined && (0, sources_js_1.sameKey)(arms, prev.key))
54
+ return prev.value;
55
+ const errors = arms.flatMap(errorOf);
56
+ const value = errors.length > 0
57
+ ? erroredArm(errors)
58
+ : arms.some((arm) => tagOf(arm) === 'Loading')
59
+ ? pendingArm
60
+ : readyArm;
61
+ effect_1.MutableRef.set(memo, { key: arms, value });
62
+ return value;
63
+ };
64
+ const derived = (0, store_js_1.makeDerivedStore)(recompute, sources.subscribe, scheduler);
65
+ yield* effect_1.Effect.addFinalizer(() => effect_1.Effect.sync(derived.unsubscribe));
66
+ if (options.once === true) {
67
+ // Latch on CONVERGED values only — at build and on (coalesced,
68
+ // post-fixpoint) notifications — never inside `recompute`. A chained
69
+ // gate with a plain Calc hop between the async stores (bootstrap →
70
+ // current-workspace → boards) propagates over TWO flush rounds, so a
71
+ // mid-flush recompute can observe the frame where the downstream
72
+ // driver has not yet flipped its query to Loading. That intermediate
73
+ // Ready self-corrects within the flush for readers, but a latch taken
74
+ // there would freeze it; a subscriber runs only after the fixpoint,
75
+ // where the value is converged.
76
+ if (recompute()._tag === 'Ready')
77
+ effect_1.MutableRef.set(latched, true);
78
+ const offSelf = derived.store.subscribe(() => {
79
+ if (derived.store.get()._tag === 'Ready')
80
+ effect_1.MutableRef.set(latched, true);
81
+ });
82
+ yield* effect_1.Effect.addFinalizer(() => effect_1.Effect.sync(offSelf));
83
+ }
84
+ return derived.store;
85
+ }));
86
+ exports.live = live;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.make = void 0;
37
+ exports.live = live;
38
+ const effect_1 = require("effect");
39
+ const definition_js_1 = require("../definition/definition.js");
40
+ const queryDriver_js_1 = require("../internal/queryDriver.js");
41
+ const track_js_1 = require("../internal/track.js");
42
+ const Reducer = __importStar(require("../reducer/reducer.js"));
43
+ const loop_js_1 = require("../runtime/loop.js");
44
+ const State = __importStar(require("../state/state.js"));
45
+ const asyncData_js_1 = require("./asyncData.js");
46
+ /**
47
+ * Define an async derived value. `output`/`error` schemas and `alwaysOn` shape
48
+ * the value type, so `yield* MyQuery` is typed to exactly the arms that can
49
+ * occur. The query effect itself is supplied by `AsyncCalc.live`.
50
+ */
51
+ const make = (name, config) => {
52
+ const store = effect_1.Context.GenericTag(`reform/asyncCalc/${name}`);
53
+ // Runtime mirror of the type-level `Gated`, typed as its literal in one place.
54
+ const gated = (0, queryDriver_js_1.gatedFlag)(config.alwaysOn);
55
+ const manifest = {
56
+ kind: 'AsyncCalc',
57
+ name,
58
+ output: config.output,
59
+ gated,
60
+ ...(config.error !== undefined ? { error: config.error } : {}),
61
+ };
62
+ const read = effect_1.Effect.flatMap(store, track_js_1.readTracked);
63
+ return (0, definition_js_1.yieldableClass)(read, {
64
+ manifest,
65
+ store,
66
+ name,
67
+ inputs: config.inputs,
68
+ gated,
69
+ });
70
+ };
71
+ exports.make = make;
72
+ function live(calc, config) {
73
+ // The hidden revision pair, built once per `live` call. Namespacing the state
74
+ // by the calc's name (`reform/state/${calcName}/invalidateOn`) keeps the tag
75
+ // out of any plausible user namespace; the duplicate-reducer check is
76
+ // warning-only and calc names are unique by convention.
77
+ const revisionState = config.invalidateOn === undefined
78
+ ? undefined
79
+ : State.make(`${calc.manifest.name}/invalidateOn`, queryDriver_js_1.RevisionSchema);
80
+ // Scoped so the source subscriptions and the driver fiber are released when
81
+ // the layer's scope closes (each proof/test builds and disposes its runtime).
82
+ const driver = effect_1.Layer.scoped(calc.store, effect_1.Effect.gen(function* () {
83
+ // `disabled` is rejected at the type level for non-gated calcs (erased to
84
+ // `never` there); read it through a loose view for the runtime.
85
+ const cfg = config;
86
+ // The hidden revision store, read requirement-free (`serviceOption`): the
87
+ // assembly below always provides it alongside this driver, and feeding it
88
+ // through `extraKey` (not `wireSources`) keeps it out of the snapshot —
89
+ // the user's `query` and `invalidateBy` receive exactly
90
+ // `InputsObject<Inputs>`, with no hidden property to leak into
91
+ // spread-into-RPC payloads.
92
+ const revision = revisionState === undefined
93
+ ? undefined
94
+ : effect_1.Option.getOrUndefined(yield* effect_1.Effect.serviceOption(revisionState.store));
95
+ const driver = yield* (0, queryDriver_js_1.makeQueryDriver)({
96
+ name: calc.manifest.name,
97
+ label: 'AsyncCalc',
98
+ gated: calc.gated,
99
+ inputs: calc.inputs,
100
+ query: cfg.query,
101
+ invalidateBy: cfg.invalidateBy,
102
+ disabled: cfg.disabled,
103
+ coalesce: cfg.coalesce,
104
+ reuse: cfg.reuse,
105
+ extraKey: revision === undefined
106
+ ? undefined
107
+ : {
108
+ read: () => revision.getSnapshot(),
109
+ subscribe: (listener) => revision.subscribe(listener),
110
+ },
111
+ });
112
+ // The internal store is the full union; narrow to the definition's arms.
113
+ return (0, asyncData_js_1.narrowStore)(driver.store);
114
+ }));
115
+ if (config.invalidateOn === undefined || revisionState === undefined)
116
+ return driver;
117
+ // The hidden reducer: an ordinary `Reducer` folding every listed event to
118
+ // n + 1 — the revision's sole writer, registered/unregistered with this
119
+ // layer's scope like any user reducer. `Layer.provide` builds the hidden
120
+ // state once and feeds the same store to both the driver and the reducer,
121
+ // while the outer context (user inputs, the `Reducers` registry) passes
122
+ // through untouched.
123
+ const revisionReducer = Reducer.make(`${calc.manifest.name}/invalidateOn`, {
124
+ states: [revisionState],
125
+ events: config.invalidateOn,
126
+ });
127
+ return effect_1.Layer.merge(driver, Reducer.live(revisionReducer, queryDriver_js_1.bumpRevision)).pipe(effect_1.Layer.provide(State.live(revisionState, queryDriver_js_1.revisionZero)));
128
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.widenStore = exports.narrowStore = exports.AsyncData = void 0;
4
+ const idle = { _tag: 'Idle' };
5
+ const loading = { _tag: 'Loading' };
6
+ const success = (value, refetching = false) => ({
7
+ _tag: 'Success',
8
+ value,
9
+ refetching,
10
+ });
11
+ const error = (err, refetching = false) => ({
12
+ _tag: 'Error',
13
+ error: err,
14
+ refetching,
15
+ });
16
+ /**
17
+ * Constructors for the arms, namespaced under the same name as the type so call
18
+ * sites read `AsyncData.success(v)` / `AsyncData.error(e)` — no `error` import
19
+ * shadowing the keyword and one obvious home for every arm.
20
+ */
21
+ exports.AsyncData = { idle, loading, success, error };
22
+ /**
23
+ * Narrow the live store (which holds the full `AnyAsyncData` union) to the arms
24
+ * the definition actually permits (`Gated`/`E`). The single documented home for
25
+ * that narrowing, so `AsyncCalc.live` returns it without an inline cast.
26
+ */
27
+ const narrowStore = (store) => store;
28
+ exports.narrowStore = narrowStore;
29
+ /**
30
+ * The read-only inverse of `narrowStore`: widen a definition-narrowed store back
31
+ * to the full union, so a consumer (the `RemoteState` overlay) can dispatch on the
32
+ * lifecycle arms without carrying the definition's `Gated`/`E` conditionals.
33
+ * Sound for reads only — every narrowed value IS an `AnyAsyncData` — so callers
34
+ * must never `set` through the widened view (derived stores ignore `set` anyway).
35
+ */
36
+ const widenStore = (store) => store;
37
+ exports.widenStore = widenStore;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.live = exports.make = void 0;
4
+ const effect_1 = require("effect");
5
+ const definition_js_1 = require("../definition/definition.js");
6
+ const sources_js_1 = require("../internal/sources.js");
7
+ const scheduler_js_1 = require("../internal/scheduler.js");
8
+ const reuse_js_1 = require("../internal/reuse.js");
9
+ const store_js_1 = require("../internal/store.js");
10
+ const track_js_1 = require("../internal/track.js");
11
+ /**
12
+ * A derived, memoized value over explicit inputs (state members, other calcs, or
13
+ * async calcs). The *definition* fixes the inputs and the output schema; the
14
+ * *implementation* is the pure projection. Recomputes only when an input changes;
15
+ * `yield* Calc` reads the memoized value.
16
+ */
17
+ const make = (name, config) => {
18
+ const store = effect_1.Context.GenericTag(`reform/calc/${name}`);
19
+ const manifest = { kind: 'Calc', name, output: config.output };
20
+ const read = effect_1.Effect.flatMap(store, track_js_1.readTracked);
21
+ return (0, definition_js_1.yieldableClass)(read, { manifest, store, name, inputs: config.inputs });
22
+ };
23
+ exports.make = make;
24
+ /**
25
+ * Wire the pure projection for a calc. Recomputes only when its invalidation key
26
+ * changes (by default every input value; override with `invalidateBy`), memoized
27
+ * on that key; `yield* Calc` reads the memoized value. The derived store reuses
28
+ * the shared coalescing scheduler, so a recompute that yields an equal value
29
+ * wakes no subscriber.
30
+ */
31
+ const live = (calc, compute, options = {}) =>
32
+ // Scoped so the source subscriptions are released when the layer's scope
33
+ // closes (each proof/test builds and disposes its own runtime).
34
+ effect_1.Layer.scoped(calc.store, effect_1.Effect.gen(function* () {
35
+ const scheduler = yield* scheduler_js_1.resolveScheduler;
36
+ const sources = yield* (0, sources_js_1.wireSources)(calc.inputs, options.invalidateBy);
37
+ // Memo cell: key and output captured together so reads are always
38
+ // consistent (no separate output that could lag the key it was computed for).
39
+ const memo = effect_1.MutableRef.make(undefined);
40
+ const recompute = () => {
41
+ const args = sources.snapshot();
42
+ const key = sources.keyOf(args);
43
+ const prev = effect_1.MutableRef.get(memo);
44
+ if (prev !== undefined && (0, sources_js_1.sameKey)(key, prev.key))
45
+ return prev.output;
46
+ const fresh = compute(args);
47
+ // With `reuse`, a recompute that lands value-equal to the previous
48
+ // output returns the previous reference — the derived store's Equal
49
+ // gate then wakes no subscriber at all.
50
+ const output = options.reuse === true && prev !== undefined ? (0, reuse_js_1.reuse)(prev.output, fresh) : fresh;
51
+ effect_1.MutableRef.set(memo, { key, output });
52
+ return output;
53
+ };
54
+ const derived = (0, store_js_1.makeDerivedStore)(recompute, sources.subscribe, scheduler);
55
+ yield* effect_1.Effect.addFinalizer(() => effect_1.Effect.sync(derived.unsubscribe));
56
+ return derived.store;
57
+ }));
58
+ exports.live = live;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.live = exports.read = exports.make = void 0;
4
+ const effect_1 = require("effect");
5
+ const definition_js_1 = require("../definition/definition.js");
6
+ const sources_js_1 = require("../internal/sources.js");
7
+ const scheduler_js_1 = require("../internal/scheduler.js");
8
+ const store_js_1 = require("../internal/store.js");
9
+ const track_js_1 = require("../internal/track.js");
10
+ /**
11
+ * Define a keyed derived value: per-key memoized projections over shared
12
+ * inputs. The *definition* fixes the key/output schemas and the inputs; the
13
+ * *implementation* (the per-key projection) is supplied by `CalcFamily.live`.
14
+ */
15
+ const make = (name, config) => {
16
+ const store = effect_1.Context.GenericTag(`reform/calcFamily/${name}`);
17
+ const manifest = {
18
+ kind: 'CalcFamily',
19
+ name,
20
+ key: config.key,
21
+ output: config.output,
22
+ };
23
+ return (0, definition_js_1.definitionClass)({
24
+ manifest,
25
+ store,
26
+ inputs: config.inputs,
27
+ });
28
+ };
29
+ exports.make = make;
30
+ /**
31
+ * Read one member by key — `CalcFamily.read(GroupProjection, id)`. Inside a
32
+ * render it subscribes exactly that member's store (unrelated members never
33
+ * wake the subtree); elsewhere it just snapshots — the same one-read-API
34
+ * contract as `StateFamily.read`.
35
+ */
36
+ const read = (family, key) => effect_1.Effect.flatMap(family.store, (fs) => (0, track_js_1.readTracked)(fs.at(key)));
37
+ exports.read = read;
38
+ /**
39
+ * Wire the per-key projection. `compute` is curried — `(key) => (inputs) =>
40
+ * Out` — so per-key construction (closing over key-derived constants) runs once
41
+ * per member while the inner projection runs per recompute. Each member is a
42
+ * lazy derived store over the shared wired sources: memoized on the family's
43
+ * invalidation key, notifying only when its own output moves. Unlike a
44
+ * `StateFamily` entry, a member holds an upstream subscription, so every
45
+ * removal path (forget / clear / eviction / the layer finalizer) releases it.
46
+ */
47
+ const live = (family, compute, options = {}) => effect_1.Layer.scoped(family.store, effect_1.Effect.gen(function* () {
48
+ const scheduler = yield* scheduler_js_1.resolveScheduler;
49
+ // Wired ONCE for the whole family — members share the sensing surface.
50
+ const sources = yield* (0, sources_js_1.wireSources)(family.inputs, options.invalidateBy);
51
+ const entries = new Map();
52
+ const evictWhenUnused = options.evictWhenUnused === true;
53
+ // Live subscriber count per key — maintained only when eviction is on.
54
+ const subscribers = new Map();
55
+ const dropEntry = (key) => {
56
+ const entry = entries.get(key);
57
+ if (entry !== undefined)
58
+ entry.unsubscribe();
59
+ entries.delete(key);
60
+ subscribers.delete(key);
61
+ };
62
+ // One member: the same memo block as `Calc.live`, with the key closed over.
63
+ const createMember = (key) => {
64
+ const body = compute(key);
65
+ const memo = effect_1.MutableRef.make(undefined);
66
+ const recompute = () => {
67
+ const args = sources.snapshot();
68
+ const memoKey = sources.keyOf(args);
69
+ const prev = effect_1.MutableRef.get(memo);
70
+ if (prev !== undefined && (0, sources_js_1.sameKey)(memoKey, prev.key))
71
+ return prev.output;
72
+ const output = body(args);
73
+ effect_1.MutableRef.set(memo, { key: memoKey, output });
74
+ return output;
75
+ };
76
+ return (0, store_js_1.makeDerivedStore)(recompute, sources.subscribe, scheduler);
77
+ };
78
+ // The `StateFamily` ref-count idiom: evict on the next microtask once a
79
+ // key falls idle (a same-commit re-subscribe cancels it) — here eviction
80
+ // also releases the member's upstream subscription via `dropEntry`.
81
+ const refCounted = (key, store) => ({
82
+ ...store,
83
+ subscribe: (listener) => {
84
+ subscribers.set(key, (subscribers.get(key) ?? 0) + 1);
85
+ const off = store.subscribe(listener);
86
+ const released = { done: false };
87
+ return () => {
88
+ if (released.done)
89
+ return;
90
+ released.done = true;
91
+ off();
92
+ const remaining = (subscribers.get(key) ?? 1) - 1;
93
+ if (remaining > 0) {
94
+ subscribers.set(key, remaining);
95
+ return;
96
+ }
97
+ subscribers.delete(key);
98
+ queueMicrotask(() => {
99
+ if ((subscribers.get(key) ?? 0) === 0)
100
+ dropEntry(key);
101
+ });
102
+ };
103
+ },
104
+ });
105
+ const familyStore = {
106
+ at: (key) => {
107
+ const existing = entries.get(key);
108
+ if (existing !== undefined)
109
+ return existing.store;
110
+ const member = createMember(key);
111
+ const created = evictWhenUnused
112
+ ? { store: refCounted(key, member.store), unsubscribe: member.unsubscribe }
113
+ : member;
114
+ entries.set(key, created);
115
+ return created.store;
116
+ },
117
+ forget: dropEntry,
118
+ clear: () => {
119
+ for (const key of [...entries.keys()])
120
+ dropEntry(key);
121
+ },
122
+ size: () => entries.size,
123
+ };
124
+ yield* effect_1.Effect.addFinalizer(() => effect_1.Effect.sync(() => familyStore.clear()));
125
+ return familyStore;
126
+ }));
127
+ exports.live = live;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.live = exports.make = exports.channelsLayer = exports.Channels = exports.proceduresLayer = exports.Procedures = void 0;
4
+ const effect_1 = require("effect");
5
+ const definition_js_1 = require("../definition/definition.js");
6
+ const errors_js_1 = require("../internal/errors.js");
7
+ class Procedures extends effect_1.Context.Tag('reform/Procedures')() {
8
+ }
9
+ exports.Procedures = Procedures;
10
+ /** Get a map entry, creating and inserting it on first access (avoids `let`). */
11
+ const getOrCreate = (map, key, make) => {
12
+ const existing = map.get(key);
13
+ if (existing !== undefined)
14
+ return existing;
15
+ const created = make();
16
+ map.set(key, created);
17
+ return created;
18
+ };
19
+ /** Drop an item from a `Map<K, Array>` bucket, pruning the key when it empties. */
20
+ const dropFrom = (map, key, item) => {
21
+ const bucket = map.get(key);
22
+ if (bucket === undefined)
23
+ return;
24
+ const next = bucket.filter((value) => value !== item);
25
+ if (next.length === 0)
26
+ map.delete(key);
27
+ else
28
+ map.set(key, next);
29
+ };
30
+ const makeProcedureRegistry = () => {
31
+ const entries = [];
32
+ const byTag = new Map();
33
+ const channelsByTag = new Map();
34
+ const byChannelTag = new Map();
35
+ return {
36
+ entries,
37
+ byTag,
38
+ channelsByTag,
39
+ byChannelTag,
40
+ register: (entry) => {
41
+ entries.push(entry);
42
+ for (const tag of entry.handles) {
43
+ getOrCreate(byTag, tag, () => []).push(entry);
44
+ const channels = getOrCreate(channelsByTag, tag, () => []);
45
+ if (!channels.includes(entry.channelName))
46
+ channels.push(entry.channelName);
47
+ getOrCreate(getOrCreate(byChannelTag, entry.channelName, () => new Map()), tag, () => []).push(entry);
48
+ }
49
+ },
50
+ unregister: (entry) => {
51
+ const index = entries.indexOf(entry);
52
+ if (index >= 0)
53
+ entries.splice(index, 1);
54
+ for (const tag of entry.handles) {
55
+ dropFrom(byTag, tag, entry);
56
+ const channelMap = byChannelTag.get(entry.channelName);
57
+ if (channelMap !== undefined) {
58
+ dropFrom(channelMap, tag, entry);
59
+ // If no procedure on this channel still handles the tag, drop the
60
+ // channel from `channelsByTag[tag]` so the loop stops offering to it.
61
+ if (channelMap.get(tag) === undefined)
62
+ dropFrom(channelsByTag, tag, entry.channelName);
63
+ if (channelMap.size === 0)
64
+ byChannelTag.delete(entry.channelName);
65
+ }
66
+ }
67
+ },
68
+ };
69
+ };
70
+ exports.proceduresLayer = effect_1.Layer.sync(Procedures, makeProcedureRegistry);
71
+ /** Registry of live channels, keyed by name. The single loop routes through it. */
72
+ class Channels extends effect_1.Context.Tag('reform/Channels')() {
73
+ }
74
+ exports.Channels = Channels;
75
+ exports.channelsLayer = effect_1.Layer.sync(Channels, () => ({ byName: new Map() }));
76
+ /**
77
+ * A scheduling lane for procedures. The *definition* (`Channel.make`) is a
78
+ * reflectable name + policy; the *implementation* (`Channel.live`) builds the
79
+ * queue + fiber that applies the policy. Procedures reference a channel by value
80
+ * and register into it; `Channel.live` is merged into the app once per channel.
81
+ */
82
+ const make = (name, config) => (0, definition_js_1.definitionClass)({
83
+ manifest: { kind: 'Channel', name },
84
+ policy: config.policy,
85
+ });
86
+ exports.make = make;
87
+ /**
88
+ * Build and register the live machinery for a channel. Idempotent by name, so
89
+ * procedures sharing an `exclusive`/`latest` channel share one queue + fiber +
90
+ * semaphore (and therefore truly serialize / cancel across each other).
91
+ */
92
+ const live = (channel) => effect_1.Layer.scopedDiscard(effect_1.Effect.gen(function* () {
93
+ const name = channel.manifest.name;
94
+ const channels = yield* Channels;
95
+ const existing = channels.byName.get(name);
96
+ if (existing !== undefined) {
97
+ // Idempotent for the *same* channel `.live`'d through several sub-layers
98
+ // (shared `exclusive`/`latest` lane). A *different* channel reusing the
99
+ // name is a silent footgun — the second policy would be dropped — so
100
+ // reject it loudly instead.
101
+ if (existing.policy !== channel.policy) {
102
+ throw new errors_js_1.DuplicateRegistration({ kind: 'Channel', name });
103
+ }
104
+ return;
105
+ }
106
+ const procedures = yield* Procedures;
107
+ const queue = yield* effect_1.Queue.unbounded();
108
+ // Run every procedure on THIS channel that handles the event. Read the live
109
+ // index each time so procedures registered later are still seen.
110
+ // A procedure is expected to model expected failures as events; a leaked
111
+ // defect (a bug in the body) is logged before being isolated, so one
112
+ // procedure's crash never tears down the channel fiber — but is observable.
113
+ const runMatching = (event) => effect_1.Effect.forEach(procedures.byChannelTag.get(name)?.get(event._tag) ?? [], (e) => e.run(event).pipe(effect_1.Effect.tapErrorCause((cause) => effect_1.Effect.logError(`reform: procedure on channel '${name}' failed`, cause)), effect_1.Effect.catchAllCause(() => effect_1.Effect.void)), { discard: true, concurrency: 'unbounded' });
114
+ // Map the policy to its scheduling stream. `Match.exhaustive` makes a new
115
+ // policy variant a compile error, and each arm is a self-contained const.
116
+ const events = effect_1.Stream.fromQueue(queue);
117
+ const driven = effect_1.Match.value(channel.policy).pipe(effect_1.Match.tag('merge', () => effect_1.Stream.mapEffect(events, runMatching, { concurrency: 'unbounded' })), effect_1.Match.tag('latest', () => effect_1.Stream.flatMap(events, (e) => effect_1.Stream.fromEffect(runMatching(e)), { switch: true })), effect_1.Match.tag('debounce', (policy) => events.pipe(effect_1.Stream.debounce(policy.duration), effect_1.Stream.mapEffect(runMatching, { concurrency: 'unbounded' }))), effect_1.Match.tag('throttle', (policy) => events.pipe(effect_1.Stream.throttle({
118
+ cost: (chunk) => effect_1.Chunk.size(chunk) * (policy.cost ?? 1),
119
+ units: policy.units,
120
+ duration: policy.duration,
121
+ ...(policy.burst !== undefined ? { burst: policy.burst } : {}),
122
+ strategy: policy.strategy ?? 'shape',
123
+ }), effect_1.Stream.mapEffect(runMatching, { concurrency: 'unbounded' }))),
124
+ // `concurrency: 1` runs one event at a time AND in offer order — a true
125
+ // serialized lane. (An unbounded fork racing for a single semaphore permit
126
+ // would serialize but could reorder under contention.)
127
+ effect_1.Match.tag('exclusive', () => effect_1.Stream.mapEffect(events, runMatching, { concurrency: 1 })), effect_1.Match.exhaustive);
128
+ yield* effect_1.Effect.forkScoped(effect_1.Stream.runDrain(driven));
129
+ channels.byName.set(name, {
130
+ name,
131
+ policy: channel.policy,
132
+ offer: (event) => {
133
+ effect_1.Queue.unsafeOffer(queue, event);
134
+ },
135
+ });
136
+ // On scope close (a lazy feature unmounting) remove the live channel so the
137
+ // loop stops routing to it; the drain fiber is already torn down by
138
+ // `forkScoped`. Eager channels live on the root scope, so this is a no-op
139
+ // until app dispose.
140
+ yield* effect_1.Effect.addFinalizer(() => effect_1.Effect.sync(() => channels.byName.delete(name)));
141
+ }));
142
+ exports.live = live;