@dxos/graph 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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/dist/lib/GraphBuilder.mjs +4 -515
- package/dist/lib/GraphModel.mjs +26 -38
- package/dist/lib/GraphModel.mjs.map +1 -1
- package/dist/lib/GraphNode.mjs +12 -2
- package/dist/lib/GraphNode.mjs.map +1 -1
- package/dist/lib/GraphNodeMatcher.mjs.map +1 -1
- package/dist/lib/chunk-GraphBuilder.mjs +902 -0
- package/dist/lib/chunk-GraphBuilder.mjs.map +1 -0
- package/dist/lib/index.mjs +3 -100
- package/dist/lib/ns/SelectionModel.mjs +103 -0
- package/dist/lib/ns/SelectionModel.mjs.map +1 -0
- package/dist/types/src/GraphBuilder.d.ts +121 -89
- package/dist/types/src/GraphBuilder.d.ts.map +1 -1
- package/dist/types/src/GraphModel.d.ts +15 -15
- package/dist/types/src/GraphModel.d.ts.map +1 -1
- package/dist/types/src/GraphNode.d.ts +34 -0
- package/dist/types/src/GraphNode.d.ts.map +1 -1
- package/dist/types/src/GraphNodeMatcher.d.ts +2 -2
- package/dist/types/src/GraphNodeMatcher.d.ts.map +1 -1
- package/dist/types/src/Retention.d.ts +36 -0
- package/dist/types/src/Retention.d.ts.map +1 -0
- package/dist/types/src/SelectionModel.d.ts +2 -0
- package/dist/types/src/SelectionModel.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +7 -6
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/internal/ConnectorTracker.d.ts +51 -0
- package/dist/types/src/internal/ConnectorTracker.d.ts.map +1 -0
- package/dist/types/src/internal/ModelStore.d.ts +4 -0
- package/dist/types/src/internal/ModelStore.d.ts.map +1 -0
- package/dist/types/src/selection.d.ts +1 -1
- package/dist/types/src/selection.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -9
- package/src/GraphBuilder.test.ts +1167 -30
- package/src/GraphBuilder.ts +399 -296
- package/src/GraphModel.test.ts +49 -16
- package/src/GraphModel.ts +30 -50
- package/src/GraphNode.ts +10 -0
- package/src/GraphNodeMatcher.test.ts +4 -4
- package/src/GraphNodeMatcher.ts +2 -2
- package/src/Retention.ts +101 -0
- package/src/SelectionModel.ts +7 -0
- package/src/index.ts +7 -7
- package/src/internal/ConnectorTracker.ts +236 -0
- package/src/internal/ModelStore.ts +92 -0
- package/src/selection.ts +2 -2
- package/dist/lib/GraphBuilder.mjs.map +0 -1
- package/dist/lib/index.mjs.map +0 -1
|
@@ -1,515 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
6
|
-
import * as Registry from "effect/unstable/reactivity/AtomRegistry";
|
|
7
|
-
import * as Array from "effect/Array";
|
|
8
|
-
import * as Function from "effect/Function";
|
|
9
|
-
import * as Option from "effect/Option";
|
|
10
|
-
import * as Pipeable from "effect/Pipeable";
|
|
11
|
-
import * as Record from "effect/Record";
|
|
12
|
-
import { log } from "@dxos/log";
|
|
13
|
-
import { Position, getDebugName, isNonNullable } from "@dxos/util";
|
|
14
|
-
//#region src/GraphBuilder.ts
|
|
15
|
-
var GraphBuilder_exports = /* @__PURE__ */ __exportAll({
|
|
16
|
-
GraphBuilder: () => GraphBuilder,
|
|
17
|
-
ModelGraphBuilder: () => ModelGraphBuilder,
|
|
18
|
-
TypeId: () => TypeId,
|
|
19
|
-
addExtension: () => addExtension,
|
|
20
|
-
createConnector: () => createConnector,
|
|
21
|
-
destroy: () => destroy,
|
|
22
|
-
explore: () => explore,
|
|
23
|
-
flattenExtensions: () => flattenExtensions,
|
|
24
|
-
flush: () => flush,
|
|
25
|
-
make: () => make,
|
|
26
|
-
makeModel: () => makeModel,
|
|
27
|
-
release: () => release,
|
|
28
|
-
removeExtension: () => removeExtension
|
|
29
|
-
});
|
|
30
|
-
var __dxlog_file = "/__w/dxos/dxos/packages/common/graph/src/GraphBuilder.ts";
|
|
31
|
-
var PRIMARY = "";
|
|
32
|
-
var primaryKey = (...parts) => parts.join(PRIMARY);
|
|
33
|
-
var primaryParts = (key) => key.split(PRIMARY);
|
|
34
|
-
/** {@link Atom.withLabel}, reduced to a pass-through wherever labels are not collected. */
|
|
35
|
-
var withLabel = () => (self) => self;
|
|
36
|
-
var defaultInline = {
|
|
37
|
-
children: () => [],
|
|
38
|
-
map: (node) => node
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Identifier denoting a GraphBuilder.
|
|
42
|
-
*/
|
|
43
|
-
var TypeId = Symbol.for("@dxos/graph/GraphBuilder");
|
|
44
|
-
/**
|
|
45
|
-
* Composes the construction of a graph out of independently registered extensions: each expansion of a
|
|
46
|
-
* node's relation subscribes to every connector declared for it, and the difference between successive
|
|
47
|
-
* reads is flushed into the store as node and edge changes.
|
|
48
|
-
*
|
|
49
|
-
* Subclass to layer a vocabulary on top (see `@dxos/app-graph`'s `AppGraphBuilder`); the generic engine
|
|
50
|
-
* is unaware of what the nodes mean.
|
|
51
|
-
*/
|
|
52
|
-
var GraphBuilder = class {
|
|
53
|
-
[TypeId] = TypeId;
|
|
54
|
-
pipe() {
|
|
55
|
-
return Pipeable.pipeArguments(this, arguments);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Expansion subscriptions, keyed by the node they belong to and then by relation key. Nested
|
|
59
|
-
* rather than flat: removal cancels a node's subscriptions, and a flat map makes that a scan of
|
|
60
|
-
* every subscription in the graph per removed node.
|
|
61
|
-
*/
|
|
62
|
-
_subscriptions = /* @__PURE__ */ new Map();
|
|
63
|
-
/** Connector updates pending flush, keyed by connector key. */
|
|
64
|
-
_dirtyConnectors = /* @__PURE__ */ new Map();
|
|
65
|
-
/** Last-flushed node IDs per connector key, used for edge removal on update. */
|
|
66
|
-
_connectorPrevious = /* @__PURE__ */ new Map();
|
|
67
|
-
/** All inline-descendant IDs per connector key, used to remove stale inline nodes on update. */
|
|
68
|
-
_connectorPreviousInlineIds = /* @__PURE__ */ new Map();
|
|
69
|
-
/** Last-flushed node args per connector key, used for change detection. */
|
|
70
|
-
_connectorPreviousArgs = /* @__PURE__ */ new Map();
|
|
71
|
-
/** Whether a dirty-flush task is already scheduled. */
|
|
72
|
-
_flushScheduled = false;
|
|
73
|
-
/** Resolves when the current flush completes. */
|
|
74
|
-
_flushPromise = Promise.resolve();
|
|
75
|
-
/** Registered extensions keyed by extension ID. */
|
|
76
|
-
_extensions = Atom.make(Record.empty()).pipe(Atom.keepAlive, withLabel("graph-builder:extensions"));
|
|
77
|
-
/**
|
|
78
|
-
* Node id -> id of the extension whose connector produced it. Non-reactive: updated directly as
|
|
79
|
-
* connectors materialize/remove nodes, so a reverse mapping from a node back to its producer does not
|
|
80
|
-
* need a reactive read.
|
|
81
|
-
*/
|
|
82
|
-
_nodeExtensions = /* @__PURE__ */ new Map();
|
|
83
|
-
_registry;
|
|
84
|
-
_store;
|
|
85
|
-
_inline;
|
|
86
|
-
_relationKey;
|
|
87
|
-
_decorateNode;
|
|
88
|
-
_unchanged;
|
|
89
|
-
constructor({ registry, store, relationKey, inline, decorateNode, unchanged }) {
|
|
90
|
-
this._registry = registry ?? Registry.make();
|
|
91
|
-
this._relationKey = relationKey;
|
|
92
|
-
this._inline = inline ?? defaultInline;
|
|
93
|
-
this._decorateNode = decorateNode ?? ((node) => node);
|
|
94
|
-
this._unchanged = unchanged ?? (() => false);
|
|
95
|
-
this._store = store({
|
|
96
|
-
onExpand: (id, relation) => this._onExpand(id, relation),
|
|
97
|
-
onRemoveNode: (id) => this._onRemoveNode(id)
|
|
98
|
-
}, this._registry);
|
|
99
|
-
}
|
|
100
|
-
get graph() {
|
|
101
|
-
return this._store.graph;
|
|
102
|
-
}
|
|
103
|
-
get extensions() {
|
|
104
|
-
return this._extensions;
|
|
105
|
-
}
|
|
106
|
-
/** Read the currently registered extensions synchronously. */
|
|
107
|
-
getExtensions() {
|
|
108
|
-
return this._registry.get(this._extensions);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* The id of the extension whose connector produced the given node, if known. Populated as connectors
|
|
112
|
-
* materialize nodes and cleared on removal.
|
|
113
|
-
*/
|
|
114
|
-
getNodeExtensionId(nodeId) {
|
|
115
|
-
return this._nodeExtensions.get(nodeId);
|
|
116
|
-
}
|
|
117
|
-
/** Every inline descendant of `node`, at every depth. */
|
|
118
|
-
_allInline(node) {
|
|
119
|
-
return this._inline.children(node).flatMap((child) => [child, ...this._allInline(child)]);
|
|
120
|
-
}
|
|
121
|
-
/** Qualify a node argument's id against its parent, recursively through its inline descendants. */
|
|
122
|
-
_qualify(parentId, node) {
|
|
123
|
-
validateSegmentId(node.id);
|
|
124
|
-
const id = qualifyId(parentId, node.id);
|
|
125
|
-
return this._inline.map({
|
|
126
|
-
...node,
|
|
127
|
-
id
|
|
128
|
-
}, (child) => this._qualify(id, child));
|
|
129
|
-
}
|
|
130
|
-
/** Record `extensionId` as the producer of a node and of the inline descendants that inherit it. */
|
|
131
|
-
_recordProvenance(node, extensionId) {
|
|
132
|
-
this._nodeExtensions.set(node.id, extensionId);
|
|
133
|
-
const owned = this._inline.owned ?? this._inline.children;
|
|
134
|
-
for (const child of owned(node)) this._recordProvenance(child, extensionId);
|
|
135
|
-
}
|
|
136
|
-
/** Apply a set of node changes for a single connector key. */
|
|
137
|
-
_applyConnectorUpdate(key, nodes, previous) {
|
|
138
|
-
const { id, relation } = relationFromConnectorKey(key);
|
|
139
|
-
const ids = nodes.map((node) => node.id);
|
|
140
|
-
const current = new Set(ids);
|
|
141
|
-
const removed = previous.filter((previousId) => !current.has(previousId));
|
|
142
|
-
this._connectorPrevious.set(key, ids);
|
|
143
|
-
this._connectorPreviousArgs.set(key, nodes);
|
|
144
|
-
const currentInlineIds = nodes.flatMap((node) => this._allInline(node).map((child) => child.id));
|
|
145
|
-
const currentInline = new Set(currentInlineIds);
|
|
146
|
-
const staleInlineIds = (this._connectorPreviousInlineIds.get(key) ?? []).filter((previousId) => !currentInline.has(previousId));
|
|
147
|
-
this._connectorPreviousInlineIds.set(key, currentInlineIds);
|
|
148
|
-
this._store.removeNodes(staleInlineIds, true);
|
|
149
|
-
this._store.removeEdges(removed.map((target) => ({
|
|
150
|
-
source: id,
|
|
151
|
-
target,
|
|
152
|
-
relation
|
|
153
|
-
})), true);
|
|
154
|
-
this._store.addNodes(nodes);
|
|
155
|
-
this._store.addEdges(nodes.map((node) => ({
|
|
156
|
-
source: id,
|
|
157
|
-
target: node.id,
|
|
158
|
-
relation
|
|
159
|
-
})));
|
|
160
|
-
if (ids.length > 0) {
|
|
161
|
-
const sortedIds = [...nodes].sort((a, b) => Position.compare({ position: a.properties?.position }, { position: b.properties?.position })).map((node) => node.id);
|
|
162
|
-
this._store.sortEdges(id, relation, sortedIds);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
_scheduleDirtyFlush() {
|
|
166
|
-
if (!this._flushScheduled) {
|
|
167
|
-
this._flushScheduled = true;
|
|
168
|
-
this._flushPromise = this._schedule(() => {
|
|
169
|
-
this._flushScheduled = false;
|
|
170
|
-
while (this._dirtyConnectors.size > 0) {
|
|
171
|
-
const entries = [...this._dirtyConnectors.entries()];
|
|
172
|
-
this._dirtyConnectors.clear();
|
|
173
|
-
const apply = () => {
|
|
174
|
-
for (const [key, { nodes, previous }] of entries) this._applyConnectorUpdate(key, nodes, previous);
|
|
175
|
-
};
|
|
176
|
-
this._store.batch ? this._store.batch(apply) : apply();
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* When a flush runs. Defaults to the next microtask; override to hand the work to a scheduler that can
|
|
183
|
-
* yield to the main thread.
|
|
184
|
-
*/
|
|
185
|
-
_schedule(callback) {
|
|
186
|
-
return Promise.resolve().then(callback);
|
|
187
|
-
}
|
|
188
|
-
/** Where a traversal yields between nodes; overridden alongside {@link GraphBuilder._schedule}. */
|
|
189
|
-
_yield() {
|
|
190
|
-
return Promise.resolve();
|
|
191
|
-
}
|
|
192
|
-
/** A connector-produced node, tagged with the id of the extension that produced it (provenance). */
|
|
193
|
-
_connectors = Atom.family((key) => {
|
|
194
|
-
return Atom.make((get) => {
|
|
195
|
-
const { id, relation } = relationFromConnectorKey(key);
|
|
196
|
-
const node = this._store.node(id);
|
|
197
|
-
if (Option.isNone(get(node))) return [];
|
|
198
|
-
const extensions = Function.pipe(get(this._extensions), Record.values, Array.sortBy(Position.compare), Array.filter((extension) => this._relationKey(extension.relation) === relation && extension.connector != null));
|
|
199
|
-
const entries = [];
|
|
200
|
-
for (const extension of extensions) for (const node of get(extension.connector(this._store.node(id)))) entries.push({
|
|
201
|
-
extensionId: extension.id,
|
|
202
|
-
node
|
|
203
|
-
});
|
|
204
|
-
return entries;
|
|
205
|
-
}).pipe(withLabel(`graph-builder:connectors:${key}`));
|
|
206
|
-
});
|
|
207
|
-
/**
|
|
208
|
-
* A relation of a node was read for the first time; subscribe every connector declared for it.
|
|
209
|
-
* Override to expand additional relations alongside the requested one.
|
|
210
|
-
*/
|
|
211
|
-
_onExpand(id, relation) {
|
|
212
|
-
log("onExpand", {
|
|
213
|
-
id,
|
|
214
|
-
relation,
|
|
215
|
-
registry: getDebugName(this._registry)
|
|
216
|
-
}, {
|
|
217
|
-
"~LogMeta": "~LogMeta",
|
|
218
|
-
F: __dxlog_file,
|
|
219
|
-
L: 435,
|
|
220
|
-
S: this
|
|
221
|
-
});
|
|
222
|
-
this._expandRelation(id, relation);
|
|
223
|
-
}
|
|
224
|
-
_expandRelation(id, relation) {
|
|
225
|
-
const key = connectorKey(id, relation);
|
|
226
|
-
const cancel = this._registry.subscribe(this._connectors(key), (entries) => {
|
|
227
|
-
const extensions = this.getExtensions();
|
|
228
|
-
const nodes = entries.map((entry) => this._decorateNode(this._qualify(id, entry.node), extensions[entry.extensionId]));
|
|
229
|
-
entries.forEach((entry, index) => this._recordProvenance(nodes[index], entry.extensionId));
|
|
230
|
-
const previous = this._connectorPrevious.get(key) ?? [];
|
|
231
|
-
const ids = nodes.map((node) => node.id);
|
|
232
|
-
if (ids.length === previous.length && ids.every((nodeId, index) => nodeId === previous[index])) {
|
|
233
|
-
const previousArgs = this._connectorPreviousArgs.get(key);
|
|
234
|
-
if (previousArgs && this._unchanged(previousArgs, nodes)) return;
|
|
235
|
-
}
|
|
236
|
-
log("update", {
|
|
237
|
-
id,
|
|
238
|
-
relation,
|
|
239
|
-
ids
|
|
240
|
-
}, {
|
|
241
|
-
"~LogMeta": "~LogMeta",
|
|
242
|
-
F: __dxlog_file,
|
|
243
|
-
L: 463,
|
|
244
|
-
S: this
|
|
245
|
-
});
|
|
246
|
-
this._dirtyConnectors.set(key, {
|
|
247
|
-
nodes,
|
|
248
|
-
previous
|
|
249
|
-
});
|
|
250
|
-
this._scheduleDirtyFlush();
|
|
251
|
-
}, { immediate: true });
|
|
252
|
-
const forNode = this._subscriptions.get(id) ?? /* @__PURE__ */ new Map();
|
|
253
|
-
forNode.set(key, cancel);
|
|
254
|
-
this._subscriptions.set(id, forNode);
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* A relation is being torn down by {@link release}: drop its expansion subscription so the next
|
|
258
|
-
* read expands it again. Override to unwind whatever expansion bookkeeping the layer keeps.
|
|
259
|
-
*/
|
|
260
|
-
_onReleaseRelation({ id, relation }) {
|
|
261
|
-
const forNode = this._subscriptions.get(id);
|
|
262
|
-
const cancel = forNode?.get(connectorKey(id, relation));
|
|
263
|
-
if (cancel) {
|
|
264
|
-
cancel();
|
|
265
|
-
forNode.delete(connectorKey(id, relation));
|
|
266
|
-
if (forNode.size === 0) this._subscriptions.delete(id);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
_onRemoveNode(id) {
|
|
270
|
-
this._nodeExtensions.delete(id);
|
|
271
|
-
const forNode = this._subscriptions.get(id);
|
|
272
|
-
if (forNode) {
|
|
273
|
-
this._subscriptions.delete(id);
|
|
274
|
-
forNode.forEach((cleanup) => cleanup());
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
/** A graph shaped for {@link ModelGraphBuilder} to build into. */
|
|
279
|
-
var makeModel = (options) => make$1(options);
|
|
280
|
-
/**
|
|
281
|
-
* The builder over `@dxos/graph`'s own {@link GraphModel} — the default specialization, and the one to
|
|
282
|
-
* reach for unless a layer needs its own node vocabulary (as `@dxos/app-graph` does).
|
|
283
|
-
*
|
|
284
|
-
* Relations are plain strings carried in the edge `type`, and sibling order in the edge `data.order`;
|
|
285
|
-
* {@link ModelGraphBuilder.children} reads them back in that order.
|
|
286
|
-
*/
|
|
287
|
-
var ModelGraphBuilder = class extends GraphBuilder {
|
|
288
|
-
/** Relations already expanded, so a repeated read does not re-subscribe the same connectors. */
|
|
289
|
-
#expanded = /* @__PURE__ */ new Set();
|
|
290
|
-
/** Memoized {@link ModelGraphBuilder.children} views, so subscribers of a relation share one atom. */
|
|
291
|
-
#children = Atom.family((key) => {
|
|
292
|
-
const [id, relation] = primaryParts(key);
|
|
293
|
-
const model = this.graph;
|
|
294
|
-
return Atom.make((get) => {
|
|
295
|
-
get(model.version);
|
|
296
|
-
return model.outgoing(id, relation).toSorted((a, b) => a.data.order - b.data.order).map((edge) => model.findNode(edge.target)).filter(isNonNullable);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
constructor({ model, rootId = RootId, ...props } = {}) {
|
|
300
|
-
super({
|
|
301
|
-
...props,
|
|
302
|
-
relationKey: (relation) => relation ?? "child",
|
|
303
|
-
inline: {
|
|
304
|
-
children: (node) => node.nodes ?? [],
|
|
305
|
-
map: (node, fn) => ({
|
|
306
|
-
...node,
|
|
307
|
-
nodes: node.nodes?.map(fn)
|
|
308
|
-
})
|
|
309
|
-
},
|
|
310
|
-
store: (hooks, registry) => modelStore(model ?? make$1({
|
|
311
|
-
registry,
|
|
312
|
-
retainAtoms: true
|
|
313
|
-
}), hooks)
|
|
314
|
-
});
|
|
315
|
-
if (!this.graph.findNode(rootId)) this.graph.addNode({ id: rootId });
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* The nodes a relation of `node` resolves to, in sibling order, expanding it on first read — the
|
|
319
|
-
* entry point that makes the graph grow.
|
|
320
|
-
*/
|
|
321
|
-
children(id, relation = "child") {
|
|
322
|
-
const key = primaryKey(id, relation);
|
|
323
|
-
if (!this.#expanded.has(key)) {
|
|
324
|
-
this.#expanded.add(key);
|
|
325
|
-
this._onExpand(id, relation);
|
|
326
|
-
}
|
|
327
|
-
return this.#children(key);
|
|
328
|
-
}
|
|
329
|
-
_onReleaseRelation(target) {
|
|
330
|
-
super._onReleaseRelation(target);
|
|
331
|
-
this.#expanded.delete(primaryKey(target.id, target.relation));
|
|
332
|
-
}
|
|
333
|
-
_onRemoveNode(id) {
|
|
334
|
-
super._onRemoveNode(id);
|
|
335
|
-
for (const key of this.#expanded) if (primaryParts(key)[0] === id) this.#expanded.delete(key);
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
/** Adapt a {@link GraphModel} to the store port. */
|
|
339
|
-
var modelStore = (model, hooks) => {
|
|
340
|
-
const nodes = Atom.family((id) => Atom.make((get) => {
|
|
341
|
-
const node = get(model.nodeAtom(id));
|
|
342
|
-
return node ? Option.some(node) : Option.none();
|
|
343
|
-
}));
|
|
344
|
-
const edgeId = ({ source, target, relation }) => createId({
|
|
345
|
-
source,
|
|
346
|
-
target,
|
|
347
|
-
relation
|
|
348
|
-
});
|
|
349
|
-
const addEdge = (edge) => {
|
|
350
|
-
const id = edgeId(edge);
|
|
351
|
-
if (!model.findEdge(id)) model.addEdge({
|
|
352
|
-
id,
|
|
353
|
-
type: edge.relation,
|
|
354
|
-
source: edge.source,
|
|
355
|
-
target: edge.target,
|
|
356
|
-
data: { order: 0 }
|
|
357
|
-
});
|
|
358
|
-
};
|
|
359
|
-
const addNode = (node) => {
|
|
360
|
-
const { nodes: children, ...rest } = node;
|
|
361
|
-
model.setNode(rest);
|
|
362
|
-
children?.forEach((child) => {
|
|
363
|
-
addNode(child);
|
|
364
|
-
addEdge({
|
|
365
|
-
source: node.id,
|
|
366
|
-
target: child.id,
|
|
367
|
-
relation: "child"
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
};
|
|
371
|
-
return {
|
|
372
|
-
graph: model,
|
|
373
|
-
node: (id) => nodes(id),
|
|
374
|
-
nodeOrThrow: (id) => Atom.make((get) => Option.getOrThrowWith(get(nodes(id)), () => /* @__PURE__ */ new Error(`No node: ${id}`))),
|
|
375
|
-
addNodes: (args) => model.batch(() => args.forEach(addNode)),
|
|
376
|
-
removeNodes: (ids, edges) => model.batch(() => {
|
|
377
|
-
model.removeNodes([...ids], { detachEdges: edges });
|
|
378
|
-
ids.forEach((id) => hooks.onRemoveNode(id));
|
|
379
|
-
}),
|
|
380
|
-
addEdges: (edges) => model.batch(() => edges.forEach(addEdge)),
|
|
381
|
-
removeEdges: (edges, removeOrphans) => model.batch(() => {
|
|
382
|
-
const present = edges.filter((edge) => model.findEdge(edgeId(edge)) !== void 0);
|
|
383
|
-
model.removeEdges(present.map(edgeId));
|
|
384
|
-
if (removeOrphans) {
|
|
385
|
-
const orphans = [...new Set(present.flatMap(({ source, target }) => [source, target]))].filter((id) => id !== "root" && model.findNode(id) !== void 0 && !model.hasEdges(id));
|
|
386
|
-
if (orphans.length > 0) {
|
|
387
|
-
model.removeNodes(orphans);
|
|
388
|
-
orphans.forEach((id) => hooks.onRemoveNode(id));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}),
|
|
392
|
-
sortEdges: (id, relation, order) => model.batch(() => {
|
|
393
|
-
for (const edge of model.outgoing(id, relation)) {
|
|
394
|
-
const index = order.indexOf(edge.target);
|
|
395
|
-
if (index >= 0) edge.data.order = index;
|
|
396
|
-
}
|
|
397
|
-
model.touch();
|
|
398
|
-
}),
|
|
399
|
-
setNode: (id, node) => Option.match(node, {
|
|
400
|
-
onNone: () => model.removeNode(id),
|
|
401
|
-
onSome: (value) => model.setNode(value)
|
|
402
|
-
}),
|
|
403
|
-
constructNode: ({ nodes: _, ...node }) => Option.some(node),
|
|
404
|
-
batch: (fn) => model.batch(fn),
|
|
405
|
-
release: (ids) => model.release(ids)
|
|
406
|
-
};
|
|
407
|
-
};
|
|
408
|
-
/**
|
|
409
|
-
* Creates a new GraphBuilder.
|
|
410
|
-
*/
|
|
411
|
-
var make = (props) => new GraphBuilder(props);
|
|
412
|
-
/**
|
|
413
|
-
* Add extensions to the builder.
|
|
414
|
-
*/
|
|
415
|
-
var addExtension = Function.dual(2, (builder, extensions) => {
|
|
416
|
-
flattenExtensions(extensions).forEach((extension) => {
|
|
417
|
-
builder._registry.set(builder._extensions, Record.set(builder.getExtensions(), extension.id, extension));
|
|
418
|
-
});
|
|
419
|
-
return builder;
|
|
420
|
-
});
|
|
421
|
-
/**
|
|
422
|
-
* Remove an extension from the builder.
|
|
423
|
-
*/
|
|
424
|
-
var removeExtension = Function.dual(2, (builder, id) => {
|
|
425
|
-
builder._registry.set(builder._extensions, Record.remove(builder.getExtensions(), id));
|
|
426
|
-
return builder;
|
|
427
|
-
});
|
|
428
|
-
/**
|
|
429
|
-
* Wait for all pending connector updates to be flushed.
|
|
430
|
-
*/
|
|
431
|
-
var flush = (builder) => builder._flushPromise;
|
|
432
|
-
/**
|
|
433
|
-
* Unloads the nodes and everything the builder remembers about them: expansion subscriptions, the
|
|
434
|
-
* per-connector diff state, and provenance. The nodes leave the store outright rather than being
|
|
435
|
-
* tombstoned, so reading a released relation again re-expands it from its connectors.
|
|
436
|
-
*
|
|
437
|
-
* Releasing a node does NOT release its descendants — the caller chooses the set, since what counts
|
|
438
|
-
* as a releasable unit (a workspace, a collection, one node) is a policy the builder has no view of.
|
|
439
|
-
* {@link explore} and the store's own traversal are how that set is collected.
|
|
440
|
-
*/
|
|
441
|
-
var release = (builder, ids) => {
|
|
442
|
-
const released = new Set(ids);
|
|
443
|
-
for (const [key, previous] of [...builder._connectorPrevious]) if (released.has(relationFromConnectorKey(key).id) || previous.some((id) => released.has(id))) {
|
|
444
|
-
builder._connectorPrevious.delete(key);
|
|
445
|
-
builder._connectorPreviousArgs.delete(key);
|
|
446
|
-
builder._connectorPreviousInlineIds.delete(key);
|
|
447
|
-
builder._dirtyConnectors.delete(key);
|
|
448
|
-
builder._onReleaseRelation(relationFromConnectorKey(key));
|
|
449
|
-
}
|
|
450
|
-
for (const [key, { nodes }] of [...builder._dirtyConnectors]) if (released.has(relationFromConnectorKey(key).id) || nodes.some((node) => released.has(node.id))) {
|
|
451
|
-
builder._dirtyConnectors.delete(key);
|
|
452
|
-
builder._onReleaseRelation(relationFromConnectorKey(key));
|
|
453
|
-
}
|
|
454
|
-
ids.forEach((id) => builder._onRemoveNode(id));
|
|
455
|
-
builder._store.release?.(ids);
|
|
456
|
-
};
|
|
457
|
-
/**
|
|
458
|
-
* Release every expansion subscription the builder holds.
|
|
459
|
-
*/
|
|
460
|
-
var destroy = (builder) => {
|
|
461
|
-
builder._subscriptions.forEach((forNode) => forNode.forEach((unsubscribe) => unsubscribe()));
|
|
462
|
-
builder._subscriptions.clear();
|
|
463
|
-
};
|
|
464
|
-
/**
|
|
465
|
-
* Traverse the graph, materializing the nodes reached along the way.
|
|
466
|
-
*
|
|
467
|
-
* The `registry` option scopes the connector reads, not the nodes themselves, which the store owns.
|
|
468
|
-
*/
|
|
469
|
-
var explore = async (builder, options, path = []) => {
|
|
470
|
-
const { registry = Registry.make(), source = RootId, relation, visitor } = options;
|
|
471
|
-
if (path.includes(source)) return;
|
|
472
|
-
await builder._yield();
|
|
473
|
-
const node = registry.get(builder._store.nodeOrThrow(source));
|
|
474
|
-
if (await visitor(node, [...path, node.id]) === false) return;
|
|
475
|
-
const nodes = Function.pipe(builder.getExtensions(), Record.values, Array.map((extension) => extension.connector), Array.filter(isNonNullable), Array.flatMap((connector) => registry.get(connector(builder._store.node(source)))), Array.map((node) => builder._qualify(source, node)));
|
|
476
|
-
await Promise.all(nodes.map((node) => {
|
|
477
|
-
builder._store.setNode(node.id, builder._store.constructNode(node));
|
|
478
|
-
return explore(builder, {
|
|
479
|
-
registry,
|
|
480
|
-
source: node.id,
|
|
481
|
-
relation,
|
|
482
|
-
visitor
|
|
483
|
-
}, [...path, source]);
|
|
484
|
-
}));
|
|
485
|
-
if (registry !== builder._registry) {
|
|
486
|
-
registry.reset();
|
|
487
|
-
registry.dispose();
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
/**
|
|
491
|
-
* Create a connector from a matcher and a factory: the factory runs only for nodes the matcher accepts,
|
|
492
|
-
* and its data type is inferred from the matcher's.
|
|
493
|
-
*/
|
|
494
|
-
var createConnector = (matcher, factory) => {
|
|
495
|
-
return (node) => Atom.make((get) => Function.pipe(get(node), Option.flatMap((matched) => matcher(matched, get)), Option.map((data) => factory(data, get)), Option.getOrElse(() => [])));
|
|
496
|
-
};
|
|
497
|
-
/**
|
|
498
|
-
* Flatten arbitrarily nested extension groups into a single list.
|
|
499
|
-
*/
|
|
500
|
-
var flattenExtensions = (extensions, acc = []) => {
|
|
501
|
-
if (Array.isArray(extensions)) return [...acc, ...extensions.flatMap((extension) => flattenExtensions(extension, acc))];
|
|
502
|
-
else return [...acc, extensions];
|
|
503
|
-
};
|
|
504
|
-
var connectorKey = (id, relation) => primaryKey(id, relation);
|
|
505
|
-
var relationFromConnectorKey = (key) => {
|
|
506
|
-
const [id, relation] = primaryParts(key);
|
|
507
|
-
return {
|
|
508
|
-
id,
|
|
509
|
-
relation
|
|
510
|
-
};
|
|
511
|
-
};
|
|
512
|
-
//#endregion
|
|
513
|
-
export { GraphBuilder, ModelGraphBuilder, TypeId, addExtension, createConnector, destroy, explore, flattenExtensions, flush, make, makeModel, release, removeExtension, GraphBuilder_exports as t };
|
|
514
|
-
|
|
515
|
-
//# sourceMappingURL=GraphBuilder.mjs.map
|
|
1
|
+
import "./GraphNode.mjs";
|
|
2
|
+
import "./GraphModel.mjs";
|
|
3
|
+
import { a as addExtension, c as explore, d as make, f as makeModel, h as setRetention, i as TypeId, l as flattenExtensions, m as removeExtension, o as createConnector, p as release, r as ModelGraphBuilder, s as destroy, t as GraphBuilder, u as flush } from "./chunk-GraphBuilder.mjs";
|
|
4
|
+
export { GraphBuilder, ModelGraphBuilder, TypeId, addExtension, createConnector, destroy, explore, flattenExtensions, flush, make, makeModel, release, removeExtension, setRetention };
|
package/dist/lib/GraphModel.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
2
|
import { GraphEdge, createId } from "./GraphEdge.mjs";
|
|
3
3
|
import { GraphNode } from "./GraphNode.mjs";
|
|
4
|
-
import * as Atom from "effect/
|
|
5
|
-
import * as Registry from "effect/
|
|
4
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
import * as Registry from "effect/reactivity/AtomRegistry";
|
|
6
6
|
import { failedInvariant, invariant } from "@dxos/invariant";
|
|
7
7
|
import * as Function from "effect/Function";
|
|
8
8
|
import * as Option from "effect/Option";
|
|
9
|
-
import * as
|
|
9
|
+
import * as AtomEx from "@dxos/effect/AtomEx";
|
|
10
10
|
import * as EffectGraph from "effect/Graph";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
11
12
|
import { inspectCustom } from "@dxos/debug";
|
|
12
13
|
//#region src/GraphModel.ts
|
|
13
14
|
var GraphModel_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -37,7 +38,9 @@ var Data = Schema.Struct({
|
|
|
37
38
|
* views read the working graph through that atom, so immutable snapshots are produced on demand
|
|
38
39
|
* rather than on every write.
|
|
39
40
|
*/
|
|
40
|
-
var AbstractGraphModel = class {
|
|
41
|
+
var AbstractGraphModel = class AbstractGraphModel {
|
|
42
|
+
static #finalizer = new FinalizationRegistry((unmount) => unmount());
|
|
43
|
+
[AtomEx.OwnerId];
|
|
41
44
|
#registry;
|
|
42
45
|
#version;
|
|
43
46
|
#nodeIndex = /* @__PURE__ */ new Map();
|
|
@@ -55,13 +58,13 @@ var AbstractGraphModel = class {
|
|
|
55
58
|
#nodeAtoms;
|
|
56
59
|
#edgeAtoms;
|
|
57
60
|
#neighborAtoms;
|
|
58
|
-
|
|
59
|
-
#pins;
|
|
60
|
-
constructor({ registry, graph, change, retainAtoms } = {}) {
|
|
61
|
+
constructor({ registry, graph, change } = {}) {
|
|
61
62
|
this.#registry = registry ?? Registry.make();
|
|
62
|
-
this
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
this[AtomEx.OwnerId] = {
|
|
64
|
+
registry: this.#registry,
|
|
65
|
+
finalizer: AbstractGraphModel.#finalizer
|
|
66
|
+
};
|
|
67
|
+
this.#version = AtomEx.makeOwned(this, Atom.make(0));
|
|
65
68
|
this.#graph = EffectGraph.beginMutation(EffectGraph.directed());
|
|
66
69
|
this.#change = change;
|
|
67
70
|
this.#mirror = change ? graph : void 0;
|
|
@@ -107,15 +110,11 @@ var AbstractGraphModel = class {
|
|
|
107
110
|
return this.#version;
|
|
108
111
|
}
|
|
109
112
|
/**
|
|
110
|
-
* Immutable snapshot in the schema shape, recomputed
|
|
113
|
+
* Immutable snapshot in the schema shape, recomputed on the first read after a change.
|
|
111
114
|
*/
|
|
112
115
|
get graph() {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
version,
|
|
116
|
-
graph: this.#encode()
|
|
117
|
-
};
|
|
118
|
-
return this.#snapshot.graph;
|
|
116
|
+
this.#snapshot ??= this.#encode();
|
|
117
|
+
return this.#snapshot;
|
|
119
118
|
}
|
|
120
119
|
get graphAtom() {
|
|
121
120
|
return this.#graphAtom;
|
|
@@ -137,7 +136,8 @@ var AbstractGraphModel = class {
|
|
|
137
136
|
this.#depth--;
|
|
138
137
|
if (this.#depth === 0 && this.#dirty) {
|
|
139
138
|
this.#dirty = false;
|
|
140
|
-
this.#
|
|
139
|
+
this.#snapshot = void 0;
|
|
140
|
+
this.#registry.update(this.#version, (version) => version + 1);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
}
|
|
@@ -201,14 +201,14 @@ var AbstractGraphModel = class {
|
|
|
201
201
|
invariant(node.id, "ID is required", {
|
|
202
202
|
"~LogMeta": "~LogMeta",
|
|
203
203
|
F: __dxlog_file,
|
|
204
|
-
L:
|
|
204
|
+
L: 493,
|
|
205
205
|
S: this,
|
|
206
206
|
A: ["node.id", "'ID is required'"]
|
|
207
207
|
});
|
|
208
208
|
invariant(!this.findNode(node.id), `node already exists: ${node.id}`, {
|
|
209
209
|
"~LogMeta": "~LogMeta",
|
|
210
210
|
F: __dxlog_file,
|
|
211
|
-
L:
|
|
211
|
+
L: 494,
|
|
212
212
|
S: this,
|
|
213
213
|
A: ["!this.findNode(node.id)", "`node already exists: ${node.id}`"]
|
|
214
214
|
});
|
|
@@ -232,7 +232,7 @@ var AbstractGraphModel = class {
|
|
|
232
232
|
invariant(node.id, "ID is required", {
|
|
233
233
|
"~LogMeta": "~LogMeta",
|
|
234
234
|
F: __dxlog_file,
|
|
235
|
-
L:
|
|
235
|
+
L: 512,
|
|
236
236
|
S: this,
|
|
237
237
|
A: ["node.id", "'ID is required'"]
|
|
238
238
|
});
|
|
@@ -287,7 +287,6 @@ var AbstractGraphModel = class {
|
|
|
287
287
|
this.#nodeIndex.delete(id);
|
|
288
288
|
this.#outgoing.delete(id);
|
|
289
289
|
this.#incoming.delete(id);
|
|
290
|
-
this.#unpin(id);
|
|
291
290
|
this.#mirrorMutate((mirror) => removeInPlace(mirror.nodes, (candidate) => candidate.id === id));
|
|
292
291
|
}
|
|
293
292
|
this.#touch();
|
|
@@ -379,14 +378,14 @@ var AbstractGraphModel = class {
|
|
|
379
378
|
invariant(edge.source, void 0, {
|
|
380
379
|
"~LogMeta": "~LogMeta",
|
|
381
380
|
F: __dxlog_file,
|
|
382
|
-
L:
|
|
381
|
+
L: 682,
|
|
383
382
|
S: this,
|
|
384
383
|
A: ["edge.source", ""]
|
|
385
384
|
});
|
|
386
385
|
invariant(edge.target, void 0, {
|
|
387
386
|
"~LogMeta": "~LogMeta",
|
|
388
387
|
F: __dxlog_file,
|
|
389
|
-
L:
|
|
388
|
+
L: 683,
|
|
390
389
|
S: this,
|
|
391
390
|
A: ["edge.target", ""]
|
|
392
391
|
});
|
|
@@ -397,7 +396,7 @@ var AbstractGraphModel = class {
|
|
|
397
396
|
invariant(!this.findEdge(resolved.id), `edge already exists: ${resolved.id}`, {
|
|
398
397
|
"~LogMeta": "~LogMeta",
|
|
399
398
|
F: __dxlog_file,
|
|
400
|
-
L:
|
|
399
|
+
L: 686,
|
|
401
400
|
S: this,
|
|
402
401
|
A: ["!this.findEdge(resolved.id)", "`edge already exists: ${resolved.id}`"]
|
|
403
402
|
});
|
|
@@ -641,17 +640,9 @@ var AbstractGraphModel = class {
|
|
|
641
640
|
value: Option.none()
|
|
642
641
|
});
|
|
643
642
|
this.#nodeIndex.set(id, index);
|
|
644
|
-
if (this.#pins && !this.#pins.has(id)) this.#pins.set(id, this.#registry.mount(this.#nodeAtoms(id)));
|
|
645
643
|
}
|
|
646
644
|
return index;
|
|
647
645
|
}
|
|
648
|
-
#unpin(id) {
|
|
649
|
-
const cancel = this.#pins?.get(id);
|
|
650
|
-
if (cancel) {
|
|
651
|
-
this.#pins.delete(id);
|
|
652
|
-
cancel();
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
646
|
#unlinkEdge(edge) {
|
|
656
647
|
const index = this.#edgeIndex.get(edge.id);
|
|
657
648
|
if (index === void 0) return;
|
|
@@ -665,8 +656,6 @@ var AbstractGraphModel = class {
|
|
|
665
656
|
* Discards the working graph wholesale, which also drops placeholders left by removals.
|
|
666
657
|
*/
|
|
667
658
|
#resetWorking() {
|
|
668
|
-
this.#pins?.forEach((cancel) => cancel());
|
|
669
|
-
this.#pins?.clear();
|
|
670
659
|
this.#graph = EffectGraph.beginMutation(EffectGraph.directed());
|
|
671
660
|
this.#nodeIndex.clear();
|
|
672
661
|
this.#edgeIndex.clear();
|
|
@@ -678,9 +667,8 @@ var AbstractGraphModel = class {
|
|
|
678
667
|
this.#dirty = true;
|
|
679
668
|
}
|
|
680
669
|
#mirrorMutate(fn) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
this.#change(() => fn(mirror));
|
|
670
|
+
if (!this.#mirror || !this.#change) return;
|
|
671
|
+
this.#change((mirror) => fn(mirror));
|
|
684
672
|
}
|
|
685
673
|
};
|
|
686
674
|
var NEIGHBOR_SEPARATOR = "";
|