@mark1russell7/render-node 0.0.0-bootstrap.0 → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,40 @@
1
1
  # @mark1russell7/render-node
2
2
 
3
- This version is a placeholder. The release workflow of https://github.com/mark1russell7/render publishes the real versions. Use a later version.
3
+ [![npm](https://img.shields.io/npm/v/@mark1russell7/render-node?color=2346c4)](https://www.npmjs.com/package/@mark1russell7/render-node) [![CI](https://github.com/mark1russell7/render/actions/workflows/ci.yml/badge.svg)](https://github.com/mark1russell7/render/actions/workflows/ci.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-2346c4)](https://github.com/mark1russell7/render/blob/main/LICENSE)
4
+
5
+ The reactive store of render. Each value is a node with an expression. One write starts one epoch. The store evaluates each reader of the node in topological order, and it stops at each value that does not change.
6
+
7
+ ```sh
8
+ npm i @mark1russell7/render-node @mark1russell7/render-dsl @mark1russell7/render-splay
9
+ ```
10
+
11
+ ```ts
12
+ import { app, lit, ref } from "@mark1russell7/render-dsl";
13
+ import { addNode, batch, nodeStore, readValue, setValue } from "@mark1russell7/render-node";
14
+ import { standardOps } from "@mark1russell7/render-splay";
15
+
16
+ const store = nodeStore({ ops: standardOps });
17
+ addNode(store, lit(2), "w");
18
+ addNode(store, lit(4), "h");
19
+ addNode(store, app("*", ref("w"), ref("h")), "area");
20
+
21
+ setValue(store, "w", 3);
22
+ readValue(store, "area"); // { tag: "some", value: 12 }
23
+ store.epochStats; // the nodes that the write evaluated and changed
24
+
25
+ // A batch makes many writes one epoch
26
+ batch(store, () => {
27
+ setValue(store, "w", 5);
28
+ setValue(store, "h", 5);
29
+ });
30
+ ```
31
+
32
+ A node can be a container of named slots, and a reference reads through the slots. The `NodeOps` of a store (`splash`, `flow`, `deref` and `targets`) are its extension points.
33
+
34
+ ## More
35
+
36
+ - [The site](https://mark1russell7.github.io/render/): the guide, live demos and the viewer.
37
+ - [The API reference](https://mark1russell7.github.io/render/reference/api/): the exports of each package.
38
+ - [The repository](https://github.com/mark1russell7/render): the source, the tests and the review of each known defect.
39
+
40
+ Each package of render is ESM, has TypeScript declarations, and works in Node.js 22.12 or later and in browsers. The license is MIT.
@@ -0,0 +1,238 @@
1
+ import { Optional } from "@mark1russell7/render-optional";
2
+ import { DepPath, Expr, Ops, Resolver } from "@mark1russell7/render-dsl";
3
+ //#region src/types.d.ts
4
+ /** The identity of a node in its store. */
5
+ type NodeId = string;
6
+ /**
7
+ * A node is the reactive primitive. It is a reactive variable (an expression and its value),
8
+ * a seat (it knows its readers) and a container of named slots.
9
+ *
10
+ * Only the engine changes a node. The `splash` op writes `value` through its target (`SplashTarget`).
11
+ */
12
+ type Node = {
13
+ readonly id: NodeId;
14
+ /**
15
+ * The expression is the source of truth. The value is a cache of its evaluation.
16
+ * A write changes the expression, thus a later evaluation cannot revert the write.
17
+ */
18
+ readonly expr: Expr;
19
+ /** The value of the expression after the last epoch. `none` is a value: the expression has no result. */
20
+ readonly value: Optional<unknown>;
21
+ /** The free reference paths of `expr`. */
22
+ readonly reads: readonly DepPath[];
23
+ /** The value readers: the nodes whose read path ends on this node. */
24
+ readonly seats: ReadonlySet<NodeId>;
25
+ /** The structural readers: the nodes whose read path goes through a slot of this node. */
26
+ readonly seatsStructural: ReadonlySet<NodeId>;
27
+ /** The nodes that have this node as a reader. Unwiring uses this index. */
28
+ readonly seatedOn: ReadonlySet<NodeId>;
29
+ /** The container that owns this node as a slot. The removal of the owner also removes this node. */
30
+ readonly parent: NodeId | undefined;
31
+ /** The containers that have a slot to this node: the owner and each container that shares the node. */
32
+ readonly heldBy: ReadonlySet<NodeId>;
33
+ /**
34
+ * The named slots of a container. A node with slots is a container: its expression is a `record`
35
+ * form over its slots, thus its value is the record of their values.
36
+ */
37
+ readonly slots: ReadonlyMap<string, NodeId>;
38
+ };
39
+ /**
40
+ * The reactive semantics of a store. The engine uses these three ops for each write, each epoch and
41
+ * each reference. Biblo layers class methods on top of them (`classNodeOps`).
42
+ */
43
+ type NodeOps = {
44
+ /** This op writes a result to a node. It gives `true` when the value changed. */
45
+ readonly splash: SplashFn;
46
+ /** This op gives the nodes that a change of the target makes dirty. */
47
+ readonly flow: FlowFn;
48
+ /** This op resolves a path from a root node. It goes through slots first, then through the fields of the value. */
49
+ readonly deref: DerefFn;
50
+ /**
51
+ * This op gives the nodes that a path from a root node depends on. The engine seats the reader on them.
52
+ * A custom `deref` gives a matching `targets`, thus the wiring always agrees with the resolution.
53
+ */
54
+ readonly targets: TargetsFn;
55
+ };
56
+ /** The target of the write op: a node whose `value` the op can write. */
57
+ type SplashTarget = Omit<Node, "value"> & {
58
+ value: Optional<unknown>;
59
+ };
60
+ /** The write op. It gives `true` when the value of the target changed. */
61
+ type SplashFn = (value: Optional<unknown>, target: SplashTarget, store: NodeStore) => boolean;
62
+ /** The propagation op. It gives the nodes that a change of the target makes dirty. */
63
+ type FlowFn = (target: Node, store: NodeStore) => ReadonlySet<NodeId>;
64
+ /** The resolution op. It gives the value at a path from a root node. */
65
+ type DerefFn = (root: Node, path: readonly string[], store: NodeStore) => Optional<unknown>;
66
+ /**
67
+ * The nodes that a read path depends on. A change of the value of `terminal` makes the reader dirty.
68
+ * A change of the slots of a node in `through` makes the reader resolve its path again.
69
+ * A path with no terminal reads no value, for example a missing slot of a container.
70
+ */
71
+ type ReadTargets = {
72
+ readonly through: readonly Node[];
73
+ readonly terminal: Node | undefined;
74
+ };
75
+ /** The dependency op: the nodes that a path from a root node depends on. It agrees with `deref`. */
76
+ type TargetsFn = (root: Node, path: readonly string[], store: NodeStore) => ReadTargets;
77
+ /** The record of one epoch. */
78
+ type EpochStats = {
79
+ /** The nodes that the epoch evaluated. */
80
+ readonly evaluated: ReadonlySet<NodeId>;
81
+ /** The nodes whose value changed in the epoch. */
82
+ readonly changed: ReadonlySet<NodeId>;
83
+ /** The nodes on a dependency cycle. A cycle evaluates again until its values are stable, up to `MAX_CYCLE_ROUNDS` rounds. */
84
+ readonly cyclic: ReadonlySet<NodeId>;
85
+ /** The number of nodes in the store after the epoch. */
86
+ readonly total: number;
87
+ };
88
+ /** A store of nodes, with the semantics of its reactivity and the op registry of its expressions. */
89
+ type NodeStore = {
90
+ readonly nodes: ReadonlyMap<NodeId, Node>;
91
+ readonly nodeOps: NodeOps;
92
+ readonly ops: Ops;
93
+ /** The record of the last epoch, or `null` before the first epoch. */
94
+ readonly epochStats: EpochStats | null;
95
+ };
96
+ /** The options of `nodeStore`. */
97
+ type StoreOptions = {
98
+ /** The reactive semantics. The default is `defaultOps`. */
99
+ readonly nodeOps?: NodeOps | undefined;
100
+ /** The op registry of the expressions. The default is an empty registry. */
101
+ readonly ops?: Ops | undefined;
102
+ };
103
+ //#endregion
104
+ //#region src/ops.d.ts
105
+ /**
106
+ * The default write. It keeps the old value when the new result is structurally equal, thus an equal
107
+ * object keeps its old reference. A change from a value to `none`, or from `none` to a value, is a change.
108
+ */
109
+ export declare const defaultSplash: SplashFn;
110
+ /** The default propagation. A change of a node makes its value readers dirty. */
111
+ export declare const defaultFlow: FlowFn;
112
+ /**
113
+ * The default resolution. It goes through slots as far as they exist, then through the own fields of the value.
114
+ * A path that ends on a node gives the value of that node. For a container, this value is the record of its slots.
115
+ * A path that stops at a container before its end reads a missing slot, thus it gives `none`.
116
+ */
117
+ export declare const defaultDeref: DerefFn;
118
+ /**
119
+ * The default dependencies of a path, which agree with `defaultDeref`. The nodes that the path goes through are
120
+ * structural targets, and the node where it ends is the terminal. A path that stops at a container before its end
121
+ * reads a missing slot: it has no terminal, and the container is a structural target.
122
+ */
123
+ export declare const defaultTargets: TargetsFn;
124
+ /** The default semantics of a store. */
125
+ export declare const defaultOps: NodeOps;
126
+ //#endregion
127
+ //#region src/writes.d.ts
128
+ /** This function makes an empty store. The options give its reactive semantics and its op registry. */
129
+ export declare const nodeStore: (options?: StoreOptions) => NodeStore;
130
+ /**
131
+ * This function gives an independent copy of a store: the same nodes, values, slots and seats, and the same
132
+ * counters. A write to one store does not change the other. The expressions and the values are immutable,
133
+ * thus the copy shares them. The options replace the semantics or the op registry of the copy, for example the
134
+ * class semantics of a copied biblo. The function throws inside a batch, because a batch has pending work.
135
+ */
136
+ export declare const forkStore: (store: NodeStore, options?: StoreOptions) => NodeStore;
137
+ /** This function gives a node of the store, or `undefined`. */
138
+ export declare const getNode: (store: NodeStore, id: NodeId) => Node | undefined;
139
+ /** This function gives the value of a node, or `none` when the store has no such node. */
140
+ export declare const readValue: (store: NodeStore, id: NodeId) => Optional<unknown>;
141
+ /**
142
+ * This function makes `fn` one transaction. The writes in `fn` change the structure at once, and one epoch
143
+ * at the end evaluates all their effects. A batch inside a batch joins the outer batch.
144
+ */
145
+ export declare const batch: <T>(store: NodeStore, fn: () => T) => T;
146
+ /**
147
+ * This function adds a node with an expression and evaluates it. It gives the ID of the node.
148
+ * The ID is the hint when no node has it, otherwise a fresh ID. A reader that waited for this ID gets rewired.
149
+ */
150
+ export declare const addNode: (store: NodeStore, expr: Expr, idHint?: string) => NodeId;
151
+ /**
152
+ * This function sets the expression of a node. This is the fundamental write.
153
+ * The engine rewires the reads of the node, evaluates it and propagates the change.
154
+ * A write to a container collapses it first: its own slots are removed, and the expression replaces the record.
155
+ */
156
+ export declare const setExpr: (store: NodeStore, id: NodeId, expr: Expr) => void;
157
+ /**
158
+ * This function writes a literal value: it is `setExpr` with `lit(value)`.
159
+ * A write to a derived node makes it an input node, like a value that a person types over a formula.
160
+ */
161
+ export declare const setValue: (store: NodeStore, id: NodeId, value: unknown) => void;
162
+ /** This function writes many literal values in one batch, thus in one epoch. */
163
+ export declare const fillMany: (store: NodeStore, writes: Iterable<readonly [NodeId, unknown]>) => void;
164
+ /** The options of `setSlot`. */
165
+ type SlotOptions = {
166
+ /**
167
+ * With `own`, the container becomes the owner of the child when the child has no owner. The removal of the
168
+ * owner removes the child, and so does a slot change that drops the child. Without `own`, the child is shared.
169
+ */
170
+ readonly own?: boolean;
171
+ };
172
+ /**
173
+ * This function points a slot of a container at a node, or removes the slot when `childId` is `undefined`.
174
+ * This is the structural write: the container gets a new record expression, and each reader whose path goes
175
+ * through the container resolves its path again. A node that the container owns and drops is removed.
176
+ * The function does nothing when a node is missing or when the child is the container itself.
177
+ */
178
+ export declare const setSlot: (store: NodeStore, parentId: NodeId, name: string, childId: NodeId | undefined, options?: SlotOptions) => void;
179
+ /**
180
+ * This function expands a node whose value is a plain object. Each field becomes a slot, and
181
+ * a field that is a plain object becomes a container in turn. The node becomes a container, and its slots
182
+ * are from then on the source of truth of its fields. A reader of a field gets a seat on the field node,
183
+ * thus an edit of one field makes only its own readers dirty. An expansion of a derived node keeps the current fields.
184
+ *
185
+ * Inside a batch, the expansion waits for the epoch of the batch, thus it uses the settled value.
186
+ * The function does nothing for a container or for a value that is not a plain object.
187
+ */
188
+ export declare const expandNode: (store: NodeStore, id: NodeId) => void;
189
+ /**
190
+ * This function removes a node and the slots that it owns. The containers that hold it lose the slot.
191
+ * Its readers resolve their paths again: a path to a removed node gives `none`. If a node with the same ID
192
+ * comes back later, the readers find it.
193
+ */
194
+ export declare const removeNode: (store: NodeStore, id: NodeId) => void;
195
+ /**
196
+ * This function evaluates the full store again. It rewires each node, makes each record expression again,
197
+ * and starts one epoch in which each node evaluates one time in topological order. The operations keep the
198
+ * store consistent, thus this function is a repair tool. It does not revert a write.
199
+ */
200
+ export declare const resolveAll: (store: NodeStore) => void;
201
+ //#endregion
202
+ //#region src/engine.d.ts
203
+ /**
204
+ * This function gives the chain of nodes that a read path goes through. The chain starts at the root
205
+ * (`path[0]`, a node ID) and follows slots as far as they exist. The last node is the terminal: the path
206
+ * reads its value. The function gives an empty chain when the root is not in the store.
207
+ */
208
+ export declare const readTargets: (nodes: ReadonlyMap<NodeId, Node>, path: DepPath) => Node[];
209
+ /** This function makes the resolver that the expressions of a store use: `path[0]` is a node ID. */
210
+ declare const resolverOf: (store: NodeStore) => Resolver;
211
+ /**
212
+ * The limit of the rounds of a cycle in one epoch. Like the iterative calculation of a spreadsheet, the members
213
+ * of a cycle evaluate again until their values are stable, or until this limit.
214
+ */
215
+ export declare const MAX_CYCLE_ROUNDS = 100;
216
+ //#endregion
217
+ //#region src/toposort.d.ts
218
+ /**
219
+ * This function sorts the nodes of a store by their static dependencies (Kahn's algorithm).
220
+ *
221
+ * The edges come from the `targets` op of the store, like the seat wiring.
222
+ * A reader depends on the terminal of each path and on each node that the path goes through.
223
+ * A path to a node that is not in the store adds no edge.
224
+ *
225
+ * The result has the dependencies before their readers. A node on a cycle of two or more nodes is not in the
226
+ * result. A reference of a node to itself adds no edge. The epochs of the engine use the edges of `flow`
227
+ * instead, thus this function is a tool for analysis.
228
+ */
229
+ export declare const toposort: (store: NodeStore) => NodeId[];
230
+ //#endregion
231
+ //#region src/equality.d.ts
232
+ /** This function tells if a value is a plain object: its prototype is `Object.prototype` or `null`. */
233
+ export declare const isPlainObject: (v: unknown) => v is Record<string, unknown>;
234
+ /** This function compares two node values structurally. The module comment gives the rules. */
235
+ export declare const valueEquals: (a: unknown, b: unknown) => boolean;
236
+ //#endregion
237
+ export { type DerefFn, type EpochStats, type FlowFn, type Node, type NodeId, type NodeOps, type NodeStore, type ReadTargets, type SlotOptions, type SplashFn, type SplashTarget, type StoreOptions, type TargetsFn, resolverOf as storeResolver };
238
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/ops.ts","../src/writes.ts","../src/engine.ts","../src/toposort.ts","../src/equality.ts"],"mappings":";;;;KAIY;;;;;;;KAQA;WACD,IAAI;;;;;WAKJ,MAAM;;WAEN,OAAO;;WAEP,gBAAgB;;WAEhB,OAAO,YAAY;;WAEnB,iBAAiB,YAAY;;WAE7B,UAAU,YAAY;;WAEtB,QAAQ;;WAER,QAAQ,YAAY;;;;;WAKpB,OAAO,oBAAoB;;;;;;KAO1B;;WAED,QAAQ;;WAER,MAAM;;WAEN,OAAO;;;;;WAKP,SAAS;;;KAIR,eAAe,KAAK;EAAmB,OAAO;;;KAG9C,YAAY,OAAO,mBAAmB,QAAQ,cAAc,OAAO;;KAGnE,UAAU,QAAQ,MAAM,OAAO,cAAc,YAAY;;KAGzD,WAAW,MAAM,MAAM,yBAAyB,OAAO,cAAc;;;;;;KAOrE;WAAyB,kBAAkB;WAAiB,UAAU;;;KAGtE,aAAa,MAAM,MAAM,yBAAyB,OAAO,cAAc;;KAGvE;;WAED,WAAW,YAAY;;WAEvB,SAAS,YAAY;;WAErB,QAAQ,YAAY;;WAEpB;;;KAIC;WACD,OAAO,YAAY,QAAQ;WAC3B,SAAS;WACT,KAAK;;WAEL,YAAY;;;KAIX;;WAED,UAAU;;WAEV,MAAM;;;;;;;;qBChGJ,eAAe;;qBAQf,aAAa;;;;;;qBAOb,cAAc;;;;;;qBA2Bd,gBAAgB;;qBAgBhB,YAAY;;;;qBClDZ,YAAa,UAAS,iBAAoB;;;;;;;qBAS1C,YAAa,OAAO,WAAW,UAAS,iBAAoB;;qBAoC5D,UAAW,OAAO,WAAW,IAAI,WAAS;;qBAG1C,YAAa,OAAO,WAAW,IAAI,WAAS;;;;;qBAM5C,QAAS,GAAG,OAAO,WAAW,UAAU,MAAI;;;;;qBAM5C,UAAW,OAAO,WAAW,MAAM,MAAM,oBAAkB;;;;;;qBAa3D,UAAW,OAAO,WAAW,IAAI,QAAQ,MAAM;;;;;qBAe/C,WAAY,OAAO,WAAW,IAAI,QAAQ;;qBAK1C,WAAY,OAAO,WAAW,QAAQ,mBAAmB;;KAO1D;;;;;WAKD;;;;;;;;qBASE,UACX,OAAO,WACP,UAAU,QACV,cACA,SAAS,oBACT,UAAS;;;;;;;;;;qBA6BE,aAAc,OAAO,WAAW,IAAI;;;;;;qBAYpC,aAAc,OAAO,WAAW,IAAI;;;;;;qBAYpC,aAAc,OAAO;;;;;;;;qBC/BrB,cAAe,OAAO,YAAY,QAAQ,OAAO,MAAM,YAAU;;cAwQjE,aAAc,OAAO,cAAY;;;;;qBAUjC;;;;;;;;;;;;;;qBCnaA,WAAY,OAAO,cAAY;;;;qBCG/B,gBAAiB,eAAa,KAAK;;qBA6BnC,cAAe,YAAY"}