@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.
- package/README.md +64 -0
- package/dist/cjs/boundary/boundary.js +86 -0
- package/dist/cjs/calc/asyncCalc.js +128 -0
- package/dist/cjs/calc/asyncData.js +37 -0
- package/dist/cjs/calc/calc.js +58 -0
- package/dist/cjs/calc/calcFamily.js +127 -0
- package/dist/cjs/channel/channel.js +142 -0
- package/dist/cjs/compose/composition.js +50 -0
- package/dist/cjs/compose/host.js +8 -0
- package/dist/cjs/compose/props.js +14 -0
- package/dist/cjs/compose/provide.js +30 -0
- package/dist/cjs/compose/slot.js +27 -0
- package/dist/cjs/compose/ui.js +61 -0
- package/dist/cjs/definition/definition.js +46 -0
- package/dist/cjs/event/event.js +36 -0
- package/dist/cjs/event/eventGroup.js +7 -0
- package/dist/cjs/feature/feature.js +102 -0
- package/dist/cjs/index.js +116 -0
- package/dist/cjs/internal/capture.js +14 -0
- package/dist/cjs/internal/ctx.js +2 -0
- package/dist/cjs/internal/errors.js +62 -0
- package/dist/cjs/internal/inspect.js +36 -0
- package/dist/cjs/internal/queryDriver.js +138 -0
- package/dist/cjs/internal/reuse.js +71 -0
- package/dist/cjs/internal/scheduler.js +73 -0
- package/dist/cjs/internal/seeds.js +19 -0
- package/dist/cjs/internal/sources.js +61 -0
- package/dist/cjs/internal/store.js +77 -0
- package/dist/cjs/internal/track.js +22 -0
- package/dist/cjs/package.json +4 -0
- package/dist/cjs/procedure/procedure.js +52 -0
- package/dist/cjs/reducer/reducer.js +64 -0
- package/dist/cjs/remote/remoteState.js +307 -0
- package/dist/cjs/runtime/bus.js +25 -0
- package/dist/cjs/runtime/loop.js +119 -0
- package/dist/cjs/scene/scene.js +36 -0
- package/dist/cjs/state/state.js +47 -0
- package/dist/cjs/state/stateFamily.js +101 -0
- package/dist/cjs/state/stateGroup.js +47 -0
- package/dist/cjs/state/token.js +23 -0
- package/dist/cjs/ui/node.js +2 -0
- package/dist/cjs/ui/trigger.js +2 -0
- package/dist/dts/boundary/boundary.d.ts +72 -0
- package/dist/dts/boundary/boundary.d.ts.map +1 -0
- package/dist/dts/calc/asyncCalc.d.ts +91 -0
- package/dist/dts/calc/asyncCalc.d.ts.map +1 -0
- package/dist/dts/calc/asyncData.d.ts +55 -0
- package/dist/dts/calc/asyncData.d.ts.map +1 -0
- package/dist/dts/calc/calc.d.ts +57 -0
- package/dist/dts/calc/calc.d.ts.map +1 -0
- package/dist/dts/calc/calcFamily.d.ts +57 -0
- package/dist/dts/calc/calcFamily.d.ts.map +1 -0
- package/dist/dts/channel/channel.d.ts +115 -0
- package/dist/dts/channel/channel.d.ts.map +1 -0
- package/dist/dts/compose/composition.d.ts +72 -0
- package/dist/dts/compose/composition.d.ts.map +1 -0
- package/dist/dts/compose/host.d.ts +17 -0
- package/dist/dts/compose/host.d.ts.map +1 -0
- package/dist/dts/compose/props.d.ts +13 -0
- package/dist/dts/compose/props.d.ts.map +1 -0
- package/dist/dts/compose/provide.d.ts +22 -0
- package/dist/dts/compose/provide.d.ts.map +1 -0
- package/dist/dts/compose/slot.d.ts +49 -0
- package/dist/dts/compose/slot.d.ts.map +1 -0
- package/dist/dts/compose/ui.d.ts +50 -0
- package/dist/dts/compose/ui.d.ts.map +1 -0
- package/dist/dts/definition/definition.d.ts +33 -0
- package/dist/dts/definition/definition.d.ts.map +1 -0
- package/dist/dts/event/event.d.ts +33 -0
- package/dist/dts/event/event.d.ts.map +1 -0
- package/dist/dts/event/eventGroup.d.ts +9 -0
- package/dist/dts/event/eventGroup.d.ts.map +1 -0
- package/dist/dts/feature/feature.d.ts +220 -0
- package/dist/dts/feature/feature.d.ts.map +1 -0
- package/dist/dts/index.d.ts +43 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/capture.d.ts +28 -0
- package/dist/dts/internal/capture.d.ts.map +1 -0
- package/dist/dts/internal/ctx.d.ts +12 -0
- package/dist/dts/internal/ctx.d.ts.map +1 -0
- package/dist/dts/internal/errors.d.ts +69 -0
- package/dist/dts/internal/errors.d.ts.map +1 -0
- package/dist/dts/internal/inspect.d.ts +17 -0
- package/dist/dts/internal/inspect.d.ts.map +1 -0
- package/dist/dts/internal/queryDriver.d.ts +65 -0
- package/dist/dts/internal/queryDriver.d.ts.map +1 -0
- package/dist/dts/internal/reuse.d.ts +10 -0
- package/dist/dts/internal/reuse.d.ts.map +1 -0
- package/dist/dts/internal/scheduler.d.ts +47 -0
- package/dist/dts/internal/scheduler.d.ts.map +1 -0
- package/dist/dts/internal/seeds.d.ts +17 -0
- package/dist/dts/internal/seeds.d.ts.map +1 -0
- package/dist/dts/internal/sources.d.ts +39 -0
- package/dist/dts/internal/sources.d.ts.map +1 -0
- package/dist/dts/internal/store.d.ts +47 -0
- package/dist/dts/internal/store.d.ts.map +1 -0
- package/dist/dts/internal/track.d.ts +33 -0
- package/dist/dts/internal/track.d.ts.map +1 -0
- package/dist/dts/procedure/procedure.d.ts +40 -0
- package/dist/dts/procedure/procedure.d.ts.map +1 -0
- package/dist/dts/reducer/reducer.d.ts +44 -0
- package/dist/dts/reducer/reducer.d.ts.map +1 -0
- package/dist/dts/remote/remoteState.d.ts +119 -0
- package/dist/dts/remote/remoteState.d.ts.map +1 -0
- package/dist/dts/runtime/bus.d.ts +27 -0
- package/dist/dts/runtime/bus.d.ts.map +1 -0
- package/dist/dts/runtime/loop.d.ts +45 -0
- package/dist/dts/runtime/loop.d.ts.map +1 -0
- package/dist/dts/scene/scene.d.ts +44 -0
- package/dist/dts/scene/scene.d.ts.map +1 -0
- package/dist/dts/state/state.d.ts +37 -0
- package/dist/dts/state/state.d.ts.map +1 -0
- package/dist/dts/state/stateFamily.d.ts +79 -0
- package/dist/dts/state/stateFamily.d.ts.map +1 -0
- package/dist/dts/state/stateGroup.d.ts +36 -0
- package/dist/dts/state/stateGroup.d.ts.map +1 -0
- package/dist/dts/state/token.d.ts +30 -0
- package/dist/dts/state/token.d.ts.map +1 -0
- package/dist/dts/ui/node.d.ts +9 -0
- package/dist/dts/ui/node.d.ts.map +1 -0
- package/dist/dts/ui/trigger.d.ts +7 -0
- package/dist/dts/ui/trigger.d.ts.map +1 -0
- package/dist/esm/boundary/boundary.js +83 -0
- package/dist/esm/boundary/boundary.js.map +1 -0
- package/dist/esm/calc/asyncCalc.js +95 -0
- package/dist/esm/calc/asyncCalc.js.map +1 -0
- package/dist/esm/calc/asyncData.js +34 -0
- package/dist/esm/calc/asyncData.js.map +1 -0
- package/dist/esm/calc/calc.js +58 -0
- package/dist/esm/calc/calc.js.map +1 -0
- package/dist/esm/calc/calcFamily.js +124 -0
- package/dist/esm/calc/calcFamily.js.map +1 -0
- package/dist/esm/channel/channel.js +136 -0
- package/dist/esm/channel/channel.js.map +1 -0
- package/dist/esm/compose/composition.js +46 -0
- package/dist/esm/compose/composition.js.map +1 -0
- package/dist/esm/compose/host.js +5 -0
- package/dist/esm/compose/host.js.map +1 -0
- package/dist/esm/compose/props.js +11 -0
- package/dist/esm/compose/props.js.map +1 -0
- package/dist/esm/compose/provide.js +28 -0
- package/dist/esm/compose/provide.js.map +1 -0
- package/dist/esm/compose/slot.js +23 -0
- package/dist/esm/compose/slot.js.map +1 -0
- package/dist/esm/compose/ui.js +57 -0
- package/dist/esm/compose/ui.js.map +1 -0
- package/dist/esm/definition/definition.js +42 -0
- package/dist/esm/definition/definition.js.map +1 -0
- package/dist/esm/event/event.js +30 -0
- package/dist/esm/event/event.js.map +1 -0
- package/dist/esm/event/eventGroup.js +4 -0
- package/dist/esm/event/eventGroup.js.map +1 -0
- package/dist/esm/feature/feature.js +98 -0
- package/dist/esm/feature/feature.js.map +1 -0
- package/dist/esm/index.js +45 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/capture.js +11 -0
- package/dist/esm/internal/capture.js.map +1 -0
- package/dist/esm/internal/ctx.js +2 -0
- package/dist/esm/internal/ctx.js.map +1 -0
- package/dist/esm/internal/errors.js +54 -0
- package/dist/esm/internal/errors.js.map +1 -0
- package/dist/esm/internal/inspect.js +32 -0
- package/dist/esm/internal/inspect.js.map +1 -0
- package/dist/esm/internal/queryDriver.js +134 -0
- package/dist/esm/internal/queryDriver.js.map +1 -0
- package/dist/esm/internal/reuse.js +68 -0
- package/dist/esm/internal/reuse.js.map +1 -0
- package/dist/esm/internal/scheduler.js +69 -0
- package/dist/esm/internal/scheduler.js.map +1 -0
- package/dist/esm/internal/seeds.js +17 -0
- package/dist/esm/internal/seeds.js.map +1 -0
- package/dist/esm/internal/sources.js +59 -0
- package/dist/esm/internal/sources.js.map +1 -0
- package/dist/esm/internal/store.js +73 -0
- package/dist/esm/internal/store.js.map +1 -0
- package/dist/esm/internal/track.js +18 -0
- package/dist/esm/internal/track.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/procedure/procedure.js +50 -0
- package/dist/esm/procedure/procedure.js.map +1 -0
- package/dist/esm/reducer/reducer.js +63 -0
- package/dist/esm/reducer/reducer.js.map +1 -0
- package/dist/esm/remote/remoteState.js +270 -0
- package/dist/esm/remote/remoteState.js.map +1 -0
- package/dist/esm/runtime/bus.js +20 -0
- package/dist/esm/runtime/bus.js.map +1 -0
- package/dist/esm/runtime/loop.js +116 -0
- package/dist/esm/runtime/loop.js.map +1 -0
- package/dist/esm/scene/scene.js +31 -0
- package/dist/esm/scene/scene.js.map +1 -0
- package/dist/esm/state/state.js +43 -0
- package/dist/esm/state/state.js.map +1 -0
- package/dist/esm/state/stateFamily.js +96 -0
- package/dist/esm/state/stateFamily.js.map +1 -0
- package/dist/esm/state/stateGroup.js +46 -0
- package/dist/esm/state/stateGroup.js.map +1 -0
- package/dist/esm/state/token.js +20 -0
- package/dist/esm/state/token.js.map +1 -0
- package/dist/esm/ui/node.js +2 -0
- package/dist/esm/ui/node.js.map +1 -0
- package/dist/esm/ui/trigger.js +2 -0
- package/dist/esm/ui/trigger.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,307 @@
|
|
|
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.live = exports.make = void 0;
|
|
37
|
+
const effect_1 = require("effect");
|
|
38
|
+
const asyncData_js_1 = require("../calc/asyncData.js");
|
|
39
|
+
const Channel = __importStar(require("../channel/channel.js"));
|
|
40
|
+
const definition_js_1 = require("../definition/definition.js");
|
|
41
|
+
const Event = __importStar(require("../event/event.js"));
|
|
42
|
+
const queryDriver_js_1 = require("../internal/queryDriver.js");
|
|
43
|
+
const reuse_js_1 = require("../internal/reuse.js");
|
|
44
|
+
const scheduler_js_1 = require("../internal/scheduler.js");
|
|
45
|
+
const sources_js_1 = require("../internal/sources.js");
|
|
46
|
+
const store_js_1 = require("../internal/store.js");
|
|
47
|
+
const track_js_1 = require("../internal/track.js");
|
|
48
|
+
const Reducer = __importStar(require("../reducer/reducer.js"));
|
|
49
|
+
const bus_js_1 = require("../runtime/bus.js");
|
|
50
|
+
const loop_js_1 = require("../runtime/loop.js");
|
|
51
|
+
const State = __importStar(require("../state/state.js"));
|
|
52
|
+
const token_js_1 = require("../state/token.js");
|
|
53
|
+
// Intent payloads are typed by the definition's `Intents`, but `Intents` is an
|
|
54
|
+
// open type parameter inside `make`/`live`, so the runtime schemas can only be
|
|
55
|
+
// structural (`Schema.Unknown` in the intent slot). Event schemas are
|
|
56
|
+
// reflective metadata — never decoded on the dispatch path — so the typed view
|
|
57
|
+
// is granted here once (the `narrowStore`/`narrowHandled` style; `any`
|
|
58
|
+
// short-circuits the schema's invariance, no value is cast).
|
|
59
|
+
const looseSchema = (schema) => schema;
|
|
60
|
+
/**
|
|
61
|
+
* Define a remote state: a query (like `AsyncCalc.make`) plus the intent
|
|
62
|
+
* events that mutate it. `yield* TheClass` reads the overlaid view;
|
|
63
|
+
* `TheClass.truth` / `TheClass.pending` / `TheClass.Failed` expose the rest of
|
|
64
|
+
* the surface. The query/send/apply logic is supplied by `RemoteState.live`.
|
|
65
|
+
*/
|
|
66
|
+
const make = (name, config) => {
|
|
67
|
+
const store = effect_1.Context.GenericTag(`reform/remoteState/${name}`);
|
|
68
|
+
const truthTag = effect_1.Context.GenericTag(`reform/remoteState/${name}/truth`);
|
|
69
|
+
const pendingTag = effect_1.Context.GenericTag(`reform/remoteState/${name}/pending`);
|
|
70
|
+
const gated = (0, queryDriver_js_1.gatedFlag)(config.alwaysOn);
|
|
71
|
+
const manifest = {
|
|
72
|
+
kind: 'RemoteState',
|
|
73
|
+
name,
|
|
74
|
+
output: config.output,
|
|
75
|
+
gated,
|
|
76
|
+
intents: config.intents.map((event) => event.tag),
|
|
77
|
+
...(config.error !== undefined ? { error: config.error } : {}),
|
|
78
|
+
};
|
|
79
|
+
// Annotated consts so the template literal expressions keep their literal
|
|
80
|
+
// types (contextual typing) — the statics must match the interface exactly.
|
|
81
|
+
const truthName = `${name}/truth`;
|
|
82
|
+
const pendingName = `${name}/pending`;
|
|
83
|
+
const failedName = `${name}/Failed`;
|
|
84
|
+
const read = effect_1.Effect.flatMap(store, track_js_1.readTracked);
|
|
85
|
+
return (0, definition_js_1.yieldableClass)(read, {
|
|
86
|
+
manifest,
|
|
87
|
+
store,
|
|
88
|
+
name,
|
|
89
|
+
inputs: config.inputs,
|
|
90
|
+
intents: config.intents,
|
|
91
|
+
gated,
|
|
92
|
+
truth: new token_js_1.StateToken(truthName, truthTag),
|
|
93
|
+
pending: new token_js_1.StateToken(pendingName, pendingTag),
|
|
94
|
+
Failed: Event.make(failedName, looseSchema(effect_1.Schema.Struct({ intent: effect_1.Schema.Unknown, error: effect_1.Schema.Unknown }))),
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
exports.make = make;
|
|
98
|
+
/**
|
|
99
|
+
* Wire a remote state: the query driver (truth), the overlay (visible view),
|
|
100
|
+
* the hidden queue reducer, the send procedure, and the settle link between
|
|
101
|
+
* them — one layer. Hidden names minted per call, namespaced by the remote
|
|
102
|
+
* state's name: events `…/Queued`/`…/Acked`/`…/Settled`/`…/Invalidated`, state
|
|
103
|
+
* `…/revision`, reducers `…/pending`/`…/revision`, procedure `…/send`, default
|
|
104
|
+
* channel `…/sends`.
|
|
105
|
+
*/
|
|
106
|
+
const live = (remote, config) => {
|
|
107
|
+
const name = remote.manifest.name;
|
|
108
|
+
const truthTag = remote.truth.store;
|
|
109
|
+
const pendingTag = remote.pending.store;
|
|
110
|
+
// The hidden event vocabulary. `Queued` carries the intent (the queue
|
|
111
|
+
// reducer's only source of it); `Acked`/`Settled` name the op; `Invalidated`
|
|
112
|
+
// bumps the hidden revision exactly like an `invalidateOn` event.
|
|
113
|
+
const Queued = Event.make(`${name}/Queued`, looseSchema(effect_1.Schema.Struct({ opId: effect_1.Schema.String, intent: effect_1.Schema.Unknown })));
|
|
114
|
+
const Acked = Event.make(`${name}/Acked`, effect_1.Schema.Struct({ opId: effect_1.Schema.String }));
|
|
115
|
+
const Settled = Event.make(`${name}/Settled`, effect_1.Schema.Struct({ opId: effect_1.Schema.String }));
|
|
116
|
+
const Invalidated = Event.make(`${name}/Invalidated`, effect_1.Schema.Struct({}));
|
|
117
|
+
// The hidden revision pair (the `invalidateOn` machinery, unconditional here:
|
|
118
|
+
// `Invalidated` always drives it; user `invalidateOn` events join the same fold).
|
|
119
|
+
const revisionState = State.make(`${name}/revision`, queryDriver_js_1.RevisionSchema);
|
|
120
|
+
const revisionReducer = Reducer.make(`${name}/revision`, {
|
|
121
|
+
states: [revisionState],
|
|
122
|
+
events: [Invalidated, ...(config.invalidateOn ?? [])],
|
|
123
|
+
});
|
|
124
|
+
// The settle link, keyed per live so two remote states never cross-resolve.
|
|
125
|
+
const linkTag = effect_1.Context.GenericTag(`reform/remoteState/${name}/link`);
|
|
126
|
+
// ── 1. Driver + link: the truth store and the generation bookkeeping. ──────
|
|
127
|
+
const driverLayer = effect_1.Layer.scopedContext(effect_1.Effect.gen(function* () {
|
|
128
|
+
// `disabled` is rejected at the type level for non-gated definitions
|
|
129
|
+
// (erased to `never` there); read it through a loose view for the runtime.
|
|
130
|
+
const cfg = config;
|
|
131
|
+
// Requirement-free read (`serviceOption`): the assembly below always
|
|
132
|
+
// provides the revision store alongside this driver.
|
|
133
|
+
const revision = effect_1.Option.getOrUndefined(yield* effect_1.Effect.serviceOption(revisionState.store));
|
|
134
|
+
const runtime = yield* effect_1.Effect.runtime();
|
|
135
|
+
// Settle waiters: a plain registry, not fibers parked per intent — so the
|
|
136
|
+
// settle path is independent of the user channel's policy (`latest`/
|
|
137
|
+
// `exclusive` lanes can't orphan a pending entry by cancelling its fiber).
|
|
138
|
+
const waiters = effect_1.MutableRef.make([]);
|
|
139
|
+
const onSettled = (generation) => {
|
|
140
|
+
const due = effect_1.MutableRef.get(waiters).filter((waiter) => waiter.waitFor <= generation);
|
|
141
|
+
if (due.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
effect_1.MutableRef.update(waiters, (all) => all.filter((waiter) => waiter.waitFor > generation));
|
|
144
|
+
// Dispatch synchronously (`publish` only enqueues on the unbounded bus):
|
|
145
|
+
// the settle is enqueued in the same flush as the converged value write.
|
|
146
|
+
for (const waiter of due) {
|
|
147
|
+
effect_1.Runtime.runSync(runtime)((0, bus_js_1.publish)('Normal', Event.construct(Settled, { opId: waiter.opId })));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const driver = yield* (0, queryDriver_js_1.makeQueryDriver)({
|
|
151
|
+
name,
|
|
152
|
+
label: 'RemoteState',
|
|
153
|
+
gated: remote.gated,
|
|
154
|
+
inputs: remote.inputs,
|
|
155
|
+
query: cfg.query,
|
|
156
|
+
invalidateBy: cfg.invalidateBy,
|
|
157
|
+
disabled: cfg.disabled,
|
|
158
|
+
coalesce: cfg.coalesce,
|
|
159
|
+
reuse: cfg.reuse,
|
|
160
|
+
extraKey: revision === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: {
|
|
163
|
+
read: () => revision.getSnapshot(),
|
|
164
|
+
subscribe: (listener) => revision.subscribe(listener),
|
|
165
|
+
},
|
|
166
|
+
onSettled,
|
|
167
|
+
});
|
|
168
|
+
const link = {
|
|
169
|
+
requested: driver.requested,
|
|
170
|
+
register: (opId, waitFor) => effect_1.MutableRef.update(waiters, (all) => [...all, { opId, waitFor }]),
|
|
171
|
+
};
|
|
172
|
+
return effect_1.Context.make(truthTag, (0, asyncData_js_1.narrowStore)(driver.store)).pipe(effect_1.Context.add(linkTag, link));
|
|
173
|
+
}));
|
|
174
|
+
// ── 2. Overlay: visible = pending.reduce(apply, truth). ────────────────────
|
|
175
|
+
const overlayLayer = effect_1.Layer.scoped(remote.store, effect_1.Effect.gen(function* () {
|
|
176
|
+
const scheduler = yield* scheduler_js_1.resolveScheduler;
|
|
177
|
+
// Widened so the arm dispatch below is a plain tag union (read-only view).
|
|
178
|
+
const feedStore = (0, asyncData_js_1.widenStore)(yield* truthTag);
|
|
179
|
+
const queueStore = yield* pendingTag;
|
|
180
|
+
const memo = effect_1.MutableRef.make(undefined);
|
|
181
|
+
const recompute = () => {
|
|
182
|
+
const feed = feedStore.getSnapshot();
|
|
183
|
+
const pending = queueStore.getSnapshot();
|
|
184
|
+
const key = [feed, pending];
|
|
185
|
+
const prev = effect_1.MutableRef.get(memo);
|
|
186
|
+
if (prev !== undefined && (0, sources_js_1.sameKey)(key, prev.key))
|
|
187
|
+
return prev.output;
|
|
188
|
+
// `apply` runs over every `Success` — including `refetching: true`, so
|
|
189
|
+
// queued intents stay visible while the server converges; the other
|
|
190
|
+
// arms pass through by reference.
|
|
191
|
+
const overlaid = feed._tag === 'Success'
|
|
192
|
+
? asyncData_js_1.AsyncData.success(pending.reduce((value, entry) => config.apply(value, entry.intent), feed.value), feed.refetching)
|
|
193
|
+
: feed;
|
|
194
|
+
// With `reuse`, also reconcile consecutive overlay outputs: the fold
|
|
195
|
+
// rebuilds the value per recompute, so untouched subtrees would
|
|
196
|
+
// otherwise lose identity every time the queue moves.
|
|
197
|
+
const output = config.reuse === true &&
|
|
198
|
+
prev !== undefined &&
|
|
199
|
+
prev.output._tag === 'Success' &&
|
|
200
|
+
overlaid._tag === 'Success'
|
|
201
|
+
? asyncData_js_1.AsyncData.success((0, reuse_js_1.reuse)(prev.output.value, overlaid.value), overlaid.refetching)
|
|
202
|
+
: overlaid;
|
|
203
|
+
effect_1.MutableRef.set(memo, { key, output });
|
|
204
|
+
return output;
|
|
205
|
+
};
|
|
206
|
+
const subscribe = (listener) => {
|
|
207
|
+
const offFeed = feedStore.subscribe(listener);
|
|
208
|
+
const offQueue = queueStore.subscribe(listener);
|
|
209
|
+
return () => {
|
|
210
|
+
offFeed();
|
|
211
|
+
offQueue();
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const derived = (0, store_js_1.makeDerivedStore)(recompute, subscribe, scheduler);
|
|
215
|
+
yield* effect_1.Effect.addFinalizer(() => effect_1.Effect.sync(derived.unsubscribe));
|
|
216
|
+
return (0, asyncData_js_1.narrowStore)(derived.store);
|
|
217
|
+
}));
|
|
218
|
+
// ── 3. Queue reducer: the pending queue's SOLE writer. ─────────────────────
|
|
219
|
+
// Assembled directly against the registry rather than through `Reducer.live`,
|
|
220
|
+
// whose parameter types are conditionals TS keeps deferred while `Intents` is
|
|
221
|
+
// an open type parameter (user code instantiates the public surface
|
|
222
|
+
// concretely, so this is the one generic caller).
|
|
223
|
+
const queueReducerName = `${name}/pending`;
|
|
224
|
+
const queueHandles = new Set([Queued.tag, Acked.tag, Settled.tag]);
|
|
225
|
+
const foldQueue = (queue, event) =>
|
|
226
|
+
// `Queued` is the arm carrying the intent (the `undefined` guard is for the
|
|
227
|
+
// type only — every `Queued` carries one). An ack/settle for an id that is
|
|
228
|
+
// absent (or already confirmed) keeps the same reference — a true no-op.
|
|
229
|
+
event._tag === Queued.tag
|
|
230
|
+
? event.intent === undefined
|
|
231
|
+
? queue
|
|
232
|
+
: [...queue, { opId: event.opId, intent: event.intent, status: 'sending' }]
|
|
233
|
+
: event._tag === Acked.tag
|
|
234
|
+
? queue.some((entry) => entry.opId === event.opId && entry.status === 'sending')
|
|
235
|
+
? queue.map((entry) => entry.opId === event.opId ? { ...entry, status: 'confirmed' } : entry)
|
|
236
|
+
: queue
|
|
237
|
+
: queue.some((entry) => entry.opId === event.opId)
|
|
238
|
+
? queue.filter((entry) => entry.opId !== event.opId)
|
|
239
|
+
: queue;
|
|
240
|
+
const queueReducerLayer = effect_1.Layer.scopedDiscard(effect_1.Effect.gen(function* () {
|
|
241
|
+
const reducers = yield* loop_js_1.Reducers;
|
|
242
|
+
if (reducers.entries.some((entry) => entry.name === queueReducerName)) {
|
|
243
|
+
yield* effect_1.Effect.logWarning(`reform: duplicate reducer name '${queueReducerName}' registered`);
|
|
244
|
+
}
|
|
245
|
+
const store = yield* pendingTag;
|
|
246
|
+
const entry = {
|
|
247
|
+
name: queueReducerName,
|
|
248
|
+
handles: queueHandles,
|
|
249
|
+
// The loop only invokes `apply` for events in `handles`; `narrowHandled`
|
|
250
|
+
// restores that typed view from the erased envelope.
|
|
251
|
+
apply: (event) => store.set(foldQueue(store.get(), (0, bus_js_1.narrowHandled)(event))),
|
|
252
|
+
};
|
|
253
|
+
yield* effect_1.Effect.acquireRelease(effect_1.Effect.sync(() => reducers.register(entry)), () => effect_1.Effect.sync(() => reducers.unregister(entry)));
|
|
254
|
+
}));
|
|
255
|
+
// ── 4. Send procedure: intent dispatch → deliver → ack/settle bookkeeping. ─
|
|
256
|
+
// Hand-assembled `ProcedureEntry` for the same deferred-conditional reason as
|
|
257
|
+
// the queue reducer (the `Procedure.live` internals, verbatim).
|
|
258
|
+
const channel = config.channel ?? Channel.make(`${name}/sends`, { policy: { _tag: 'merge' } });
|
|
259
|
+
const procedureName = `${name}/send`;
|
|
260
|
+
const sendHandles = new Set(remote.intents.map((event) => event.tag));
|
|
261
|
+
const sendProcedureLayer = effect_1.Layer.scopedDiscard(effect_1.Effect.gen(function* () {
|
|
262
|
+
const procedures = yield* Channel.Procedures;
|
|
263
|
+
if (procedures.entries.some((entry) => entry.name === procedureName)) {
|
|
264
|
+
yield* effect_1.Effect.logWarning(`reform: duplicate procedure name '${procedureName}' registered`);
|
|
265
|
+
}
|
|
266
|
+
const link = yield* linkTag;
|
|
267
|
+
// Snapshot the body's full context (Bus + the send's R2) so `run` is total.
|
|
268
|
+
const runtime = yield* effect_1.Effect.runtime();
|
|
269
|
+
const deliver = (intent) => effect_1.Effect.gen(function* () {
|
|
270
|
+
const opId = yield* effect_1.Effect.sync(() => crypto.randomUUID());
|
|
271
|
+
yield* Event.dispatch(Queued, { opId, intent });
|
|
272
|
+
const outcome = yield* config.send(intent).pipe(
|
|
273
|
+
// A defect in `send` is a bug, but it must not strand the entry:
|
|
274
|
+
// fold it into the failure arm so the intent settles and `Failed`
|
|
275
|
+
// fires (the channel would otherwise swallow it after logging).
|
|
276
|
+
effect_1.Effect.catchAllDefect((defect) => effect_1.Effect.fail(defect)), effect_1.Effect.either,
|
|
277
|
+
// A cancelled send (a `latest`/`exclusive` user channel tearing the
|
|
278
|
+
// run down) settles too: the overlay reverts, and if the request
|
|
279
|
+
// did reach the server the next refetch shows it as truth.
|
|
280
|
+
effect_1.Effect.onInterrupt(() => Event.dispatch(Settled, { opId })));
|
|
281
|
+
yield* effect_1.Either.match(outcome, {
|
|
282
|
+
onLeft: (error) => effect_1.Effect.zipRight(Event.dispatch(remote.Failed, { intent, error }), Event.dispatch(Settled, { opId })),
|
|
283
|
+
onRight: () =>
|
|
284
|
+
// Register the waiter BEFORE the invalidation bump: `Invalidated`
|
|
285
|
+
// routes through the drain loop later, so the run it triggers is
|
|
286
|
+
// necessarily requested after `requested()` here — the waiter can
|
|
287
|
+
// never be satisfied by a refetch that predates the ack.
|
|
288
|
+
effect_1.Effect.sync(() => link.register(opId, link.requested() + 1)).pipe(effect_1.Effect.zipRight(Event.dispatch(Acked, { opId })), effect_1.Effect.zipRight(Event.dispatch(Invalidated, {}))),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
const entry = {
|
|
292
|
+
name: procedureName,
|
|
293
|
+
channelName: channel.manifest.name,
|
|
294
|
+
handles: sendHandles,
|
|
295
|
+
run: (event) => effect_1.Effect.provide(deliver((0, bus_js_1.narrowHandled)(event)), runtime),
|
|
296
|
+
};
|
|
297
|
+
yield* effect_1.Effect.acquireRelease(effect_1.Effect.sync(() => procedures.register(entry)), () => effect_1.Effect.sync(() => procedures.unregister(entry)));
|
|
298
|
+
}));
|
|
299
|
+
// ── Assembly. ───────────────────────────────────────────────────────────────
|
|
300
|
+
// The pending store backs both the overlay and the queue reducer; the hidden
|
|
301
|
+
// revision store feeds both the driver (`extraKey`) and its reducer; the
|
|
302
|
+
// channel `.live` is idempotent by name+policy, so a user channel shared with
|
|
303
|
+
// other procedures wires up exactly once.
|
|
304
|
+
const pendingStoreLayer = effect_1.Layer.effect(pendingTag, effect_1.Effect.map(scheduler_js_1.resolveScheduler, (scheduler) => (0, store_js_1.makeStore)([], scheduler)));
|
|
305
|
+
return effect_1.Layer.mergeAll(overlayLayer, queueReducerLayer, Reducer.live(revisionReducer, queryDriver_js_1.bumpRevision), sendProcedureLayer, Channel.live(channel)).pipe(effect_1.Layer.provideMerge(driverLayer), effect_1.Layer.provide(State.live(revisionState, queryDriver_js_1.revisionZero)), effect_1.Layer.provideMerge(pendingStoreLayer));
|
|
306
|
+
};
|
|
307
|
+
exports.live = live;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.publish = exports.busLayer = exports.Bus = exports.narrowHandled = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
/**
|
|
6
|
+
* Re-narrow an erased `Tagged` envelope to a handler's declared event union. The
|
|
7
|
+
* loop and channels only route events whose tag is in a handler's `handles`, so
|
|
8
|
+
* at runtime the envelope IS one of those events; the registries store it as the
|
|
9
|
+
* generic `Tagged`, and this restores the typed view. The one documented home
|
|
10
|
+
* for that boundary cast, shared by `Reducer.live` and `Procedure.live`.
|
|
11
|
+
*/
|
|
12
|
+
const narrowHandled = (event) => event;
|
|
13
|
+
exports.narrowHandled = narrowHandled;
|
|
14
|
+
/** The single dispatch bus: an unbounded PubSub fanned out to the loop and procedures. */
|
|
15
|
+
class Bus extends effect_1.Context.Tag('reform/Bus')() {
|
|
16
|
+
}
|
|
17
|
+
exports.Bus = Bus;
|
|
18
|
+
// Unbounded so `publish` never suspends: UI triggers dispatch synchronously
|
|
19
|
+
// (`Runtime.runSync`, no fiber fork per event), and an event storm can't apply
|
|
20
|
+
// backpressure to the UI thread. The single drain loop empties the bus every
|
|
21
|
+
// microtask, so it does not accumulate. Swap to `PubSub.dropping(N)` if bounded
|
|
22
|
+
// memory is preferred over never dropping (publish stays synchronous either way).
|
|
23
|
+
exports.busLayer = effect_1.Layer.scoped(Bus, effect_1.PubSub.unbounded());
|
|
24
|
+
const publish = (priority, event) => effect_1.Effect.flatMap(Bus, (bus) => effect_1.PubSub.publish(bus, { priority, event }));
|
|
25
|
+
exports.publish = publish;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Engine = exports.reducersLayer = exports.Reducers = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const channel_js_1 = require("../channel/channel.js");
|
|
6
|
+
const scheduler_js_1 = require("../internal/scheduler.js");
|
|
7
|
+
const bus_js_1 = require("./bus.js");
|
|
8
|
+
class Reducers extends effect_1.Context.Tag('reform/Reducers')() {
|
|
9
|
+
}
|
|
10
|
+
exports.Reducers = Reducers;
|
|
11
|
+
/** Drop an item from a `Map<string, Array>` bucket, pruning the key when it empties. */
|
|
12
|
+
const dropFromBuckets = (map, key, item) => {
|
|
13
|
+
const bucket = map.get(key);
|
|
14
|
+
if (bucket === undefined)
|
|
15
|
+
return;
|
|
16
|
+
const next = bucket.filter((entry) => entry !== item);
|
|
17
|
+
if (next.length === 0)
|
|
18
|
+
map.delete(key);
|
|
19
|
+
else
|
|
20
|
+
map.set(key, next);
|
|
21
|
+
};
|
|
22
|
+
const makeReducerRegistry = () => {
|
|
23
|
+
const entries = [];
|
|
24
|
+
const byTag = new Map();
|
|
25
|
+
return {
|
|
26
|
+
entries,
|
|
27
|
+
byTag,
|
|
28
|
+
register: (entry) => {
|
|
29
|
+
entries.push(entry);
|
|
30
|
+
for (const tag of entry.handles) {
|
|
31
|
+
const bucket = byTag.get(tag);
|
|
32
|
+
if (bucket === undefined)
|
|
33
|
+
byTag.set(tag, [entry]);
|
|
34
|
+
else
|
|
35
|
+
bucket.push(entry);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
unregister: (entry) => {
|
|
39
|
+
const index = entries.indexOf(entry);
|
|
40
|
+
if (index >= 0)
|
|
41
|
+
entries.splice(index, 1);
|
|
42
|
+
for (const tag of entry.handles)
|
|
43
|
+
dropFromBuckets(byTag, tag, entry);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
exports.reducersLayer = effect_1.Layer.sync(Reducers, makeReducerRegistry);
|
|
48
|
+
/** High before Normal; within a priority class, dispatch order is preserved. */
|
|
49
|
+
const rank = (priority) => (priority === 'High' ? 0 : 1);
|
|
50
|
+
// The single drain loop: one fiber that frame-batches the bus. Each tick it
|
|
51
|
+
// harvests every event dispatched in that microtask, runs all matching reducers
|
|
52
|
+
// in ONE synchronous pass (so the store notifier flushes once for the batch),
|
|
53
|
+
// then routes each event to the channels its procedures run on.
|
|
54
|
+
const drain = effect_1.Effect.gen(function* () {
|
|
55
|
+
const bus = yield* bus_js_1.Bus;
|
|
56
|
+
const reducers = yield* Reducers;
|
|
57
|
+
const { byName: channels } = yield* channel_js_1.Channels;
|
|
58
|
+
const procedures = yield* channel_js_1.Procedures;
|
|
59
|
+
const subscription = yield* effect_1.PubSub.subscribe(bus);
|
|
60
|
+
yield* effect_1.Effect.forkScoped(effect_1.Effect.forever(effect_1.Effect.gen(function* () {
|
|
61
|
+
// Block until the first event, then drain the rest of this tick.
|
|
62
|
+
const first = yield* effect_1.Queue.take(subscription);
|
|
63
|
+
const rest = yield* effect_1.Queue.takeAll(subscription);
|
|
64
|
+
const frame = [first, ...effect_1.Chunk.toArray(rest)];
|
|
65
|
+
// Stable priority order: High-dispatched (UI) events fold before
|
|
66
|
+
// Normal-dispatched (procedure follow-up) events in the same frame. A
|
|
67
|
+
// two-bucket partition does this in O(m) with no comparator — push order
|
|
68
|
+
// preserves intra-priority dispatch order — instead of sorting the frame.
|
|
69
|
+
const high = [];
|
|
70
|
+
const normal = [];
|
|
71
|
+
for (const envelope of frame) {
|
|
72
|
+
if (rank(envelope.priority) === 0)
|
|
73
|
+
high.push(envelope);
|
|
74
|
+
else
|
|
75
|
+
normal.push(envelope);
|
|
76
|
+
}
|
|
77
|
+
const ordered = high.concat(normal);
|
|
78
|
+
// One synchronous block: every reducer write for the frame coalesces into
|
|
79
|
+
// a single notification flush. Routing to channels happens here too, so
|
|
80
|
+
// procedure bodies (run later, off their channel fibers) see post-batch
|
|
81
|
+
// state. No `yield*` in between, or the flush could fire early.
|
|
82
|
+
const failures = [];
|
|
83
|
+
yield* effect_1.Effect.sync(() => {
|
|
84
|
+
for (const envelope of ordered) {
|
|
85
|
+
for (const reducer of reducers.byTag.get(envelope.event._tag) ?? []) {
|
|
86
|
+
// Isolate each fold: a synchronous throw in one reducer must not
|
|
87
|
+
// abandon the rest of the frame nor (via the `forever` below) kill
|
|
88
|
+
// the drain fiber and freeze the whole app. Collect and log after.
|
|
89
|
+
try {
|
|
90
|
+
reducer.apply(envelope.event);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
failures.push({ event: envelope.event, error });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const envelope of ordered) {
|
|
98
|
+
// Offer to each DISTINCT channel once. Routing per-procedure would
|
|
99
|
+
// offer a shared channel multiple times for one event, and each offer
|
|
100
|
+
// re-runs every procedure on it — duplicate execution.
|
|
101
|
+
for (const channelName of procedures.channelsByTag.get(envelope.event._tag) ?? []) {
|
|
102
|
+
channels.get(channelName)?.offer(envelope.event);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
yield* effect_1.Effect.forEach(failures, (failure) => effect_1.Effect.logError(`reform: reducer threw handling '${failure.event._tag}'`, failure.error), { discard: true });
|
|
107
|
+
}).pipe(
|
|
108
|
+
// Belt-and-suspenders: even a defect outside the isolated fold (a bug in
|
|
109
|
+
// the loop itself) is logged and the tick restarts, so the bus never goes
|
|
110
|
+
// permanently deaf.
|
|
111
|
+
effect_1.Effect.catchAllCause((cause) => effect_1.Effect.logError('reform: drain tick crashed', cause)))));
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* The reform engine: provides the bus + reducer/channel/procedure registries +
|
|
115
|
+
* the per-runtime notification scheduler to the application and forks the single
|
|
116
|
+
* drain loop. Registration mutates the live collections, so reducers/procedures/
|
|
117
|
+
* channels merged alongside are picked up before the first dispatch (boot).
|
|
118
|
+
*/
|
|
119
|
+
exports.Engine = effect_1.Layer.scopedDiscard(drain).pipe(effect_1.Layer.provideMerge(effect_1.Layer.mergeAll(bus_js_1.busLayer, exports.reducersLayer, channel_js_1.channelsLayer, channel_js_1.proceduresLayer, scheduler_js_1.notificationsLayer)));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isScene = exports.seedScene = exports.scene = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const seeds_js_1 = require("../internal/seeds.js");
|
|
6
|
+
/**
|
|
7
|
+
* Define a scene — `scene(TodoApp, { provide: [makeTestApp(client, seeds)] })`.
|
|
8
|
+
* The composition's contract flows through `C`, so consumers stay typed.
|
|
9
|
+
*/
|
|
10
|
+
const scene = (composition, config) => ({
|
|
11
|
+
kind: 'Scene',
|
|
12
|
+
composition,
|
|
13
|
+
provide: config.provide,
|
|
14
|
+
...(config.boot !== undefined ? { boot: config.boot } : {}),
|
|
15
|
+
});
|
|
16
|
+
exports.scene = scene;
|
|
17
|
+
/**
|
|
18
|
+
* Overlay seed values onto a CLOSED scene — the tooling/test seam (dev-tool
|
|
19
|
+
* inspector overrides, proofs). Scenes stay closed for authors: this does not
|
|
20
|
+
* reopen `provide`, it wraps each layer with `Layer.locally(CurrentSeedOverrides,
|
|
21
|
+
* seeds)` so `State.live` boots matching stores from the override instead of
|
|
22
|
+
* the authored seed.
|
|
23
|
+
*
|
|
24
|
+
* Keys are state MEMBER NAMES as reflected in `manifest.states` (e.g. `count`),
|
|
25
|
+
* not group names. If two groups in one scene share a member name, both receive
|
|
26
|
+
* the override (documented limitation). Values are schema-validated at build:
|
|
27
|
+
* an invalid value silently falls back to the authored seed. `StateFamily`
|
|
28
|
+
* entries are not covered.
|
|
29
|
+
*/
|
|
30
|
+
const seedScene = (base, seeds) => Object.keys(seeds).length === 0
|
|
31
|
+
? base
|
|
32
|
+
: { ...base, provide: base.provide.map(effect_1.Layer.locally(seeds_js_1.CurrentSeedOverrides, seeds)) };
|
|
33
|
+
exports.seedScene = seedScene;
|
|
34
|
+
/** Reflection guard: is this exported value a scene? */
|
|
35
|
+
const isScene = (v) => typeof v === 'object' && v !== null && v.kind === 'Scene';
|
|
36
|
+
exports.isScene = isScene;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 seeds_js_1 = require("../internal/seeds.js");
|
|
8
|
+
const store_js_1 = require("../internal/store.js");
|
|
9
|
+
const track_js_1 = require("../internal/track.js");
|
|
10
|
+
/**
|
|
11
|
+
* Atomic reactive state. `State.make` is the *definition* (a reflectable
|
|
12
|
+
* manifest + an internal store tag); it carries no starting value — the seed is
|
|
13
|
+
* an implementation detail supplied at create-time by `State.live(state, seed)`.
|
|
14
|
+
* Composed into a `StateGroup` exactly as `@effect/rpc` composes rpcs.
|
|
15
|
+
*/
|
|
16
|
+
const make = (name, schema, options = {}) => {
|
|
17
|
+
const store = effect_1.Context.GenericTag(`reform/state/${name}`);
|
|
18
|
+
const manifest = {
|
|
19
|
+
kind: 'State',
|
|
20
|
+
name,
|
|
21
|
+
schema,
|
|
22
|
+
...(options.title !== undefined ? { title: options.title } : {}),
|
|
23
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
24
|
+
};
|
|
25
|
+
const read = effect_1.Effect.flatMap(store, track_js_1.readTracked);
|
|
26
|
+
return (0, definition_js_1.yieldableClass)(read, { manifest, store });
|
|
27
|
+
};
|
|
28
|
+
exports.make = make;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the value a store should boot from: the ambient seed override when
|
|
31
|
+
* one is present (and valid) for this state's member name, else the authored
|
|
32
|
+
* `initial`. Reads `CurrentSeedOverrides` — empty outside the tooling seam
|
|
33
|
+
* (`Scene.seedScene`), so production wiring always boots from `initial`. An
|
|
34
|
+
* override is decoded against the state's schema; an invalid value silently
|
|
35
|
+
* falls back to `initial` — a half-typed override from the dev tool must never
|
|
36
|
+
* crash a preview.
|
|
37
|
+
*/
|
|
38
|
+
const resolveSeed = (state, initial) => effect_1.Effect.map(effect_1.FiberRef.get(seeds_js_1.CurrentSeedOverrides), (overrides) => state.manifest.name in overrides
|
|
39
|
+
? effect_1.Option.getOrElse(effect_1.Schema.decodeUnknownOption(state.manifest.schema)(overrides[state.manifest.name]), () => initial)
|
|
40
|
+
: initial);
|
|
41
|
+
/**
|
|
42
|
+
* Allocate a state's store, seeded with `initial` — `State.live(Feed, seed)`.
|
|
43
|
+
* The seed lives here (implementation), not on the definition, so the same
|
|
44
|
+
* definition can boot from different values in app vs. test wiring.
|
|
45
|
+
*/
|
|
46
|
+
const live = (state, initial) => effect_1.Layer.effect(state.store, effect_1.Effect.zipWith(scheduler_js_1.resolveScheduler, resolveSeed(state, initial), (scheduler, seed) => (0, store_js_1.makeStore)(seed, scheduler)));
|
|
47
|
+
exports.live = live;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.live = exports.read = exports.make = exports.Tombstone = 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 store_js_1 = require("../internal/store.js");
|
|
8
|
+
const track_js_1 = require("../internal/track.js");
|
|
9
|
+
/**
|
|
10
|
+
* A reducer fold returns this to evict the key it folded over, instead of a new
|
|
11
|
+
* value. The loop drops the entry's store from the family, so a normalized
|
|
12
|
+
* collection (chat messages, search hits, transient rows) does not grow without
|
|
13
|
+
* bound — the one fix for the otherwise-unbounded per-key `Map`.
|
|
14
|
+
*/
|
|
15
|
+
exports.Tombstone = Symbol.for('reform/StateFamily/Tombstone');
|
|
16
|
+
const makeFamilyStore = (seed, scheduler, options = {}) => {
|
|
17
|
+
const entries = new Map();
|
|
18
|
+
const evictWhenUnused = options.evictWhenUnused === true;
|
|
19
|
+
// Live subscriber count per key — maintained only when eviction is on.
|
|
20
|
+
const subscribers = new Map();
|
|
21
|
+
// Wrap a store's `subscribe` to ref-count, evicting the key when it falls idle.
|
|
22
|
+
// `get`/`set`/`getVersion` are delegated unchanged, so the loop writes and the
|
|
23
|
+
// host reads see the same cell; only subscribe/unsubscribe are instrumented.
|
|
24
|
+
const refCounted = (key, store) => ({
|
|
25
|
+
...store,
|
|
26
|
+
subscribe: (listener) => {
|
|
27
|
+
subscribers.set(key, (subscribers.get(key) ?? 0) + 1);
|
|
28
|
+
const off = store.subscribe(listener);
|
|
29
|
+
const released = { done: false };
|
|
30
|
+
return () => {
|
|
31
|
+
if (released.done)
|
|
32
|
+
return;
|
|
33
|
+
released.done = true;
|
|
34
|
+
off();
|
|
35
|
+
const remaining = (subscribers.get(key) ?? 1) - 1;
|
|
36
|
+
if (remaining > 0) {
|
|
37
|
+
subscribers.set(key, remaining);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
subscribers.delete(key);
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
if ((subscribers.get(key) ?? 0) === 0)
|
|
43
|
+
entries.delete(key);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
at: (key) => {
|
|
50
|
+
const existing = entries.get(key);
|
|
51
|
+
if (existing !== undefined)
|
|
52
|
+
return existing;
|
|
53
|
+
const base = (0, store_js_1.makeStore)(seed(key), scheduler);
|
|
54
|
+
const created = evictWhenUnused ? refCounted(key, base) : base;
|
|
55
|
+
entries.set(key, created);
|
|
56
|
+
return created;
|
|
57
|
+
},
|
|
58
|
+
forget: (key) => {
|
|
59
|
+
entries.delete(key);
|
|
60
|
+
subscribers.delete(key);
|
|
61
|
+
},
|
|
62
|
+
clear: () => {
|
|
63
|
+
entries.clear();
|
|
64
|
+
subscribers.clear();
|
|
65
|
+
},
|
|
66
|
+
size: () => entries.size,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Normalized keyed state: one logical State per key, backed by a single family
|
|
71
|
+
* store. Scales with the collection without re-rendering unrelated entries.
|
|
72
|
+
*/
|
|
73
|
+
const make = (name, key, value, options = {}) => {
|
|
74
|
+
const store = effect_1.Context.GenericTag(`reform/family/${name}`);
|
|
75
|
+
const manifest = {
|
|
76
|
+
kind: 'StateFamily',
|
|
77
|
+
name,
|
|
78
|
+
key,
|
|
79
|
+
value,
|
|
80
|
+
...(options.title !== undefined ? { title: options.title } : {}),
|
|
81
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
82
|
+
};
|
|
83
|
+
return (0, definition_js_1.definitionClass)({ manifest, store });
|
|
84
|
+
};
|
|
85
|
+
exports.make = make;
|
|
86
|
+
/**
|
|
87
|
+
* Read one entry by key — `StateFamily.read(ItemUi, id)`. Inside a render it
|
|
88
|
+
* subscribes that key's slice; elsewhere it just snapshots (D5).
|
|
89
|
+
*/
|
|
90
|
+
const read = (family, key) => effect_1.Effect.flatMap(family.store, (fs) => (0, track_js_1.readTracked)(fs.at(key)));
|
|
91
|
+
exports.read = read;
|
|
92
|
+
/**
|
|
93
|
+
* Allocate a family's keyed store — `StateFamily.live(ItemUi, seed)`. The seed
|
|
94
|
+
* is a plain value or a factory; a factory receives the key, so a new entry can
|
|
95
|
+
* derive its starting value from its own id (`(id) => ({ ...blank, id }))`).
|
|
96
|
+
*/
|
|
97
|
+
const live = (family, initial, options = {}) => {
|
|
98
|
+
const seed = typeof initial === 'function' ? initial : () => initial;
|
|
99
|
+
return effect_1.Layer.effect(family.store, effect_1.Effect.map(scheduler_js_1.resolveScheduler, (scheduler) => makeFamilyStore(seed, scheduler, options)));
|
|
100
|
+
};
|
|
101
|
+
exports.live = live;
|