@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.
- package/dist/boundary/boundary.d.ts +72 -0
- package/dist/boundary/boundary.d.ts.map +1 -0
- package/dist/boundary/boundary.js +83 -0
- package/dist/boundary/boundary.js.map +1 -0
- package/dist/calc/asyncCalc.d.ts +91 -0
- package/dist/calc/asyncCalc.d.ts.map +1 -0
- package/dist/calc/asyncCalc.js +95 -0
- package/dist/calc/asyncCalc.js.map +1 -0
- package/{src/calc/asyncData.ts → dist/calc/asyncData.d.ts} +22 -53
- package/dist/calc/asyncData.d.ts.map +1 -0
- package/dist/calc/asyncData.js +34 -0
- package/dist/calc/asyncData.js.map +1 -0
- package/dist/calc/calc.d.ts +57 -0
- package/dist/calc/calc.d.ts.map +1 -0
- package/dist/calc/calc.js +58 -0
- package/dist/calc/calc.js.map +1 -0
- package/dist/calc/calcFamily.d.ts +57 -0
- package/dist/calc/calcFamily.d.ts.map +1 -0
- package/dist/calc/calcFamily.js +124 -0
- package/dist/calc/calcFamily.js.map +1 -0
- package/dist/calc/compose.d.ts +16 -0
- package/dist/calc/compose.d.ts.map +1 -0
- package/dist/calc/compose.js +7 -0
- package/dist/calc/compose.js.map +1 -0
- package/dist/channel/channel.d.ts +115 -0
- package/dist/channel/channel.d.ts.map +1 -0
- package/dist/channel/channel.js +138 -0
- package/dist/channel/channel.js.map +1 -0
- package/dist/compose/composition.d.ts +72 -0
- package/dist/compose/composition.d.ts.map +1 -0
- package/dist/compose/composition.js +46 -0
- package/dist/compose/composition.js.map +1 -0
- package/{src/compose/host.ts → dist/compose/host.d.ts} +8 -6
- package/dist/compose/host.d.ts.map +1 -0
- package/dist/compose/host.js +6 -0
- package/dist/compose/host.js.map +1 -0
- package/dist/compose/props.d.ts +13 -0
- package/dist/compose/props.d.ts.map +1 -0
- package/{src/compose/props.ts → dist/compose/props.js} +5 -3
- package/dist/compose/props.js.map +1 -0
- package/dist/compose/provide.d.ts +22 -0
- package/dist/compose/provide.d.ts.map +1 -0
- package/dist/compose/provide.js +28 -0
- package/dist/compose/provide.js.map +1 -0
- package/{src/compose/slot.ts → dist/compose/slot.d.ts} +19 -36
- package/dist/compose/slot.d.ts.map +1 -0
- package/dist/compose/slot.js +23 -0
- package/dist/compose/slot.js.map +1 -0
- package/dist/compose/ui.d.ts +135 -0
- package/dist/compose/ui.d.ts.map +1 -0
- package/dist/compose/ui.js +63 -0
- package/dist/compose/ui.js.map +1 -0
- package/dist/definition/definition.d.ts +33 -0
- package/dist/definition/definition.d.ts.map +1 -0
- package/dist/definition/definition.js +42 -0
- package/dist/definition/definition.js.map +1 -0
- package/dist/event/event.d.ts +33 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +30 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/eventGroup.d.ts +9 -0
- package/dist/event/eventGroup.d.ts.map +1 -0
- package/dist/event/eventGroup.js +4 -0
- package/dist/event/eventGroup.js.map +1 -0
- package/{src/feature/feature.ts → dist/feature/feature.d.ts} +92 -178
- package/dist/feature/feature.d.ts.map +1 -0
- package/dist/feature/feature.js +98 -0
- package/dist/feature/feature.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/{src/internal/capture.ts → dist/internal/capture.d.ts} +11 -13
- package/dist/internal/capture.d.ts.map +1 -0
- package/dist/internal/capture.js +12 -0
- package/dist/internal/capture.js.map +1 -0
- package/{src/internal/ctx.ts → dist/internal/ctx.d.ts} +4 -8
- package/dist/internal/ctx.d.ts.map +1 -0
- package/dist/internal/ctx.js +2 -0
- package/dist/internal/ctx.js.map +1 -0
- package/dist/internal/errors.d.ts +89 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +102 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/inspect.d.ts +17 -0
- package/dist/internal/inspect.d.ts.map +1 -0
- package/dist/internal/inspect.js +32 -0
- package/dist/internal/inspect.js.map +1 -0
- package/dist/internal/queryDriver.d.ts +65 -0
- package/dist/internal/queryDriver.d.ts.map +1 -0
- package/dist/internal/queryDriver.js +134 -0
- package/dist/internal/queryDriver.js.map +1 -0
- package/dist/internal/reuse.d.ts +10 -0
- package/dist/internal/reuse.d.ts.map +1 -0
- package/dist/internal/reuse.js +68 -0
- package/dist/internal/reuse.js.map +1 -0
- package/dist/internal/scheduler.d.ts +47 -0
- package/dist/internal/scheduler.d.ts.map +1 -0
- package/dist/internal/scheduler.js +70 -0
- package/dist/internal/scheduler.js.map +1 -0
- package/{src/internal/seeds.ts → dist/internal/seeds.d.ts} +3 -6
- package/dist/internal/seeds.d.ts.map +1 -0
- package/dist/internal/seeds.js +17 -0
- package/dist/internal/seeds.js.map +1 -0
- package/dist/internal/sources.d.ts +39 -0
- package/dist/internal/sources.d.ts.map +1 -0
- package/dist/internal/sources.js +59 -0
- package/dist/internal/sources.js.map +1 -0
- package/dist/internal/stateRegistry.d.ts +2 -0
- package/dist/internal/stateRegistry.d.ts.map +1 -0
- package/dist/internal/stateRegistry.js +28 -0
- package/dist/internal/stateRegistry.js.map +1 -0
- package/dist/internal/store.d.ts +47 -0
- package/dist/internal/store.d.ts.map +1 -0
- package/dist/internal/store.js +73 -0
- package/dist/internal/store.js.map +1 -0
- package/{src/internal/track.ts → dist/internal/track.d.ts} +11 -15
- package/dist/internal/track.d.ts.map +1 -0
- package/dist/internal/track.js +19 -0
- package/dist/internal/track.js.map +1 -0
- package/dist/procedure/procedure.d.ts +40 -0
- package/dist/procedure/procedure.d.ts.map +1 -0
- package/dist/procedure/procedure.js +50 -0
- package/dist/procedure/procedure.js.map +1 -0
- package/dist/reducer/reducer.d.ts +44 -0
- package/dist/reducer/reducer.d.ts.map +1 -0
- package/dist/reducer/reducer.js +63 -0
- package/dist/reducer/reducer.js.map +1 -0
- package/dist/remote/remoteState.d.ts +119 -0
- package/dist/remote/remoteState.d.ts.map +1 -0
- package/dist/remote/remoteState.js +270 -0
- package/dist/remote/remoteState.js.map +1 -0
- package/dist/runtime/bus.d.ts +27 -0
- package/dist/runtime/bus.d.ts.map +1 -0
- package/{src/runtime/bus.ts → dist/runtime/bus.js} +8 -23
- package/dist/runtime/bus.js.map +1 -0
- package/dist/runtime/loop.d.ts +45 -0
- package/dist/runtime/loop.d.ts.map +1 -0
- package/dist/runtime/loop.js +117 -0
- package/dist/runtime/loop.js.map +1 -0
- package/dist/scene/scene.d.ts +44 -0
- package/dist/scene/scene.d.ts.map +1 -0
- package/dist/scene/scene.js +31 -0
- package/dist/scene/scene.js.map +1 -0
- package/dist/state/state.d.ts +37 -0
- package/dist/state/state.d.ts.map +1 -0
- package/dist/state/state.js +46 -0
- package/dist/state/state.js.map +1 -0
- package/dist/state/stateFamily.d.ts +79 -0
- package/dist/state/stateFamily.d.ts.map +1 -0
- package/dist/state/stateFamily.js +99 -0
- package/dist/state/stateFamily.js.map +1 -0
- package/dist/state/stateGroup.d.ts +50 -0
- package/dist/state/stateGroup.d.ts.map +1 -0
- package/dist/state/stateGroup.js +54 -0
- package/dist/state/stateGroup.js.map +1 -0
- package/{src/state/token.ts → dist/state/token.d.ts} +13 -21
- package/dist/state/token.d.ts.map +1 -0
- package/dist/state/token.js +20 -0
- package/dist/state/token.js.map +1 -0
- package/dist/synced/syncedStore.d.ts +51 -0
- package/dist/synced/syncedStore.d.ts.map +1 -0
- package/dist/synced/syncedStore.js +43 -0
- package/dist/synced/syncedStore.js.map +1 -0
- package/{src/ui/node.ts → dist/ui/node.d.ts} +3 -3
- package/dist/ui/node.d.ts.map +1 -0
- package/dist/ui/node.js +2 -0
- package/dist/ui/node.js.map +1 -0
- package/{src/ui/trigger.ts → dist/ui/trigger.d.ts} +2 -1
- package/dist/ui/trigger.d.ts.map +1 -0
- package/dist/ui/trigger.js +2 -0
- package/dist/ui/trigger.js.map +1 -0
- package/dist/wire/tree.d.ts +66 -0
- package/dist/wire/tree.d.ts.map +1 -0
- package/dist/wire/tree.js +60 -0
- package/dist/wire/tree.js.map +1 -0
- package/dist/wire/triggers.d.ts +44 -0
- package/dist/wire/triggers.d.ts.map +1 -0
- package/dist/wire/triggers.js +31 -0
- package/dist/wire/triggers.js.map +1 -0
- package/package.json +32 -11
- package/src/boundary/boundary.ts +0 -192
- package/src/calc/asyncCalc.ts +0 -267
- package/src/calc/calc.ts +0 -120
- package/src/calc/calcFamily.ts +0 -194
- package/src/calc/compose.ts +0 -69
- package/src/channel/channel.ts +0 -265
- package/src/compose/composition.ts +0 -112
- package/src/compose/provide.ts +0 -70
- package/src/compose/ui.ts +0 -97
- package/src/definition/definition.ts +0 -74
- package/src/event/event.ts +0 -65
- package/src/event/eventGroup.ts +0 -14
- package/src/index.ts +0 -140
- package/src/internal/errors.ts +0 -69
- package/src/internal/inspect.ts +0 -34
- package/src/internal/queryDriver.ts +0 -247
- package/src/internal/reuse.ts +0 -73
- package/src/internal/scheduler.ts +0 -91
- package/src/internal/sources.ts +0 -104
- package/src/internal/store.ts +0 -105
- package/src/procedure/procedure.ts +0 -89
- package/src/reducer/reducer.ts +0 -137
- package/src/remote/remoteState.ts +0 -571
- package/src/runtime/loop.ts +0 -169
- package/src/scene/scene.ts +0 -76
- package/src/state/state.ts +0 -91
- package/src/state/stateFamily.ts +0 -171
- package/src/state/stateGroup.ts +0 -78
|
@@ -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 @@
|
|
|
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
|
-
"
|
|
3
|
+
"playbook": "./playbook",
|
|
4
|
+
"version": "0.0.7",
|
|
4
5
|
"type": "module",
|
|
5
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
21
|
-
"
|
|
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": "*",
|
package/src/boundary/boundary.ts
DELETED
|
@@ -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
|
-
)
|