@dxos/app-graph 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/lib/browser/index.mjs +789 -541
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +780 -533
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +789 -541
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/graph-builder.d.ts +91 -48
  11. package/dist/types/src/graph-builder.d.ts.map +1 -1
  12. package/dist/types/src/graph.d.ts +98 -191
  13. package/dist/types/src/graph.d.ts.map +1 -1
  14. package/dist/types/src/node.d.ts +3 -3
  15. package/dist/types/src/node.d.ts.map +1 -1
  16. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  17. package/dist/types/tsconfig.tsbuildinfo +1 -1
  18. package/package.json +16 -23
  19. package/src/graph-builder.test.ts +310 -293
  20. package/src/graph-builder.ts +317 -209
  21. package/src/graph.test.ts +463 -314
  22. package/src/graph.ts +455 -452
  23. package/src/node.ts +4 -4
  24. package/src/stories/EchoGraph.stories.tsx +78 -57
  25. package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
  26. package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
  27. package/dist/types/src/signals-integration.test.d.ts +0 -2
  28. package/dist/types/src/signals-integration.test.d.ts.map +0 -1
  29. package/dist/types/src/testing.d.ts +0 -5
  30. package/dist/types/src/testing.d.ts.map +0 -1
  31. package/src/experimental/graph-projections.test.ts +0 -56
  32. package/src/signals-integration.test.ts +0 -218
  33. package/src/testing.ts +0 -20
package/src/graph.test.ts CHANGED
@@ -2,261 +2,241 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Registry, Rx } from '@effect-rx/rx-react';
6
- import { Option } from 'effect';
7
- import { assert, describe, expect, onTestFinished, test } from 'vitest';
5
+ import { effect } from '@preact/signals-core';
6
+ import { describe, expect, test } from 'vitest';
8
7
 
9
- import { getGraph, Graph, ROOT_ID, ROOT_TYPE } from './graph';
10
- import { type Node } from './node';
8
+ import { updateCounter } from '@dxos/echo-schema/testing';
9
+ import { registerSignalsRuntime } from '@dxos/echo-signals';
11
10
 
12
- const exampleId = (id: number) => `dx:test:${id}`;
13
- const EXAMPLE_ID = exampleId(1);
14
- const EXAMPLE_TYPE = 'dxos.org/type/example';
11
+ import { Graph, ROOT_ID, ROOT_TYPE, getGraph } from './graph';
12
+ import { type Node, type NodeFilter } from './node';
13
+
14
+ registerSignalsRuntime();
15
+
16
+ const longestPaths = new Map<string, string[]>();
17
+
18
+ const filterLongestPath: NodeFilter = (node, connectedNode): node is Node => {
19
+ const longestPath = longestPaths.get(node.id);
20
+ if (!longestPath) {
21
+ return false;
22
+ }
23
+
24
+ if (longestPath[longestPath.length - 2] !== connectedNode.id) {
25
+ return false;
26
+ }
27
+
28
+ return true;
29
+ };
15
30
 
16
31
  describe('Graph', () => {
17
32
  test('getGraph', () => {
18
- const registry = Registry.make();
19
- const graph = new Graph({ registry });
20
- const root = registry.get(graph.node(ROOT_ID));
21
- assert.ok(Option.isSome(root));
22
- expect(root.value.id).toEqual(ROOT_ID);
23
- expect(root.value.type).toEqual(ROOT_TYPE);
24
- expect(getGraph(root.value)).toEqual(graph);
33
+ const graph = new Graph();
34
+ expect(getGraph(graph.root)).to.equal(graph);
25
35
  });
26
36
 
27
- test('add node', () => {
28
- const registry = Registry.make();
29
- const graph = new Graph({ registry });
30
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
31
- const node = registry.get(graph.node(EXAMPLE_ID));
32
- assert.ok(Option.isSome(node));
33
- expect(node.value.id).toEqual(EXAMPLE_ID);
34
- expect(node.value.type).toEqual(EXAMPLE_TYPE);
35
- expect(node.value.data).toEqual(null);
36
- expect(node.value.properties).toEqual({});
37
+ test('add nodes', () => {
38
+ const graph = new Graph();
39
+
40
+ const [root] = graph._addNodes([
41
+ {
42
+ id: ROOT_ID,
43
+ type: ROOT_TYPE,
44
+ nodes: [
45
+ { id: 'test1', type: 'test' },
46
+ { id: 'test2', type: 'test' },
47
+ ],
48
+ },
49
+ ]);
50
+
51
+ expect(root.id).to.equal('root');
52
+ expect(graph.nodes(root)).to.have.length(2);
53
+ expect(graph.findNode('test1')?.id).to.equal('test1');
54
+ expect(graph.findNode('test2')?.id).to.equal('test2');
55
+ expect(graph.nodes(graph.findNode('test1')!)).to.be.empty;
56
+ expect(graph.nodes(graph.findNode('test2')!)).to.be.empty;
57
+ expect(graph.nodes(graph.findNode('test1')!, { relation: 'inbound' })).to.have.length(1);
58
+ expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(1);
37
59
  });
38
60
 
39
61
  test('add nodes updates existing nodes', () => {
40
- const registry = Registry.make();
41
- const graph = new Graph({ registry });
42
- const nodeKey = graph.node(EXAMPLE_ID);
62
+ const graph = new Graph();
43
63
 
44
- let count = 0;
45
- const cancel = registry.subscribe(nodeKey, (_) => {
46
- count++;
47
- });
48
- onTestFinished(() => cancel());
64
+ graph._addNodes([
65
+ {
66
+ id: ROOT_ID,
67
+ type: ROOT_TYPE,
68
+ nodes: [
69
+ { id: 'test1', type: 'test' },
70
+ { id: 'test2', type: 'test' },
71
+ ],
72
+ },
73
+ ]);
74
+ graph._addNodes([
75
+ {
76
+ id: ROOT_ID,
77
+ type: ROOT_TYPE,
78
+ nodes: [
79
+ { id: 'test1', type: 'test' },
80
+ { id: 'test2', type: 'test' },
81
+ ],
82
+ },
83
+ ]);
49
84
 
50
- expect(registry.get(nodeKey)).toEqual(Option.none());
51
- expect(count).toEqual(1);
85
+ expect(Object.keys(graph._nodes)).to.have.length(3);
86
+ expect(Object.keys(graph._edges)).to.have.length(3);
87
+ expect(graph.nodes(graph.root)).to.have.length(2);
88
+ });
52
89
 
53
- expect(registry.get(nodeKey)).toEqual(Option.none());
54
- expect(count).toEqual(1);
90
+ test('remove node', () => {
91
+ const graph = new Graph();
55
92
 
56
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
57
- const node = registry.get(nodeKey);
58
- assert.ok(Option.isSome(node));
59
- expect(node.value.id).toEqual(EXAMPLE_ID);
60
- expect(node.value.type).toEqual(EXAMPLE_TYPE);
61
- expect(node.value.data).toEqual(null);
62
- expect(node.value.properties).toEqual({});
63
- expect(count).toEqual(2);
93
+ const [root] = graph._addNodes([
94
+ {
95
+ id: ROOT_ID,
96
+ type: ROOT_TYPE,
97
+ nodes: [
98
+ { id: 'test1', type: 'test' },
99
+ { id: 'test2', type: 'test' },
100
+ ],
101
+ },
102
+ ]);
64
103
 
65
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
66
- expect(count).toEqual(2);
67
- });
104
+ expect(root.id).to.equal('root');
105
+ expect(graph.nodes(root)).to.have.length(2);
106
+ expect(graph.findNode('test1')?.id).to.equal('test1');
107
+ expect(graph.findNode('test2')?.id).to.equal('test2');
68
108
 
69
- test('remove node', () => {
70
- const registry = Registry.make();
71
- const graph = new Graph({ registry });
72
-
73
- {
74
- const node = registry.get(graph.node(EXAMPLE_ID));
75
- expect(Option.isNone(node)).toEqual(true);
76
- }
77
-
78
- {
79
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
80
- const node = registry.get(graph.node(EXAMPLE_ID));
81
- expect(Option.isSome(node)).toEqual(true);
82
- }
83
-
84
- {
85
- graph.removeNode(EXAMPLE_ID);
86
- const node = registry.get(graph.node(EXAMPLE_ID));
87
- expect(Option.isNone(node)).toEqual(true);
88
- }
109
+ graph._removeNodes(['test1']);
110
+ expect(graph.findNode('test1')).to.be.undefined;
111
+ expect(graph.nodes(root)).to.have.length(1);
89
112
  });
90
113
 
91
- test('onNodeChanged', () => {
114
+ test('re-add node', () => {
92
115
  const graph = new Graph();
93
116
 
94
- let node: Option.Option<Node> = Option.none();
95
- graph.onNodeChanged.on(({ node: newNode }) => {
96
- node = newNode;
97
- });
117
+ graph._addNodes([
118
+ {
119
+ id: ROOT_ID,
120
+ type: ROOT_TYPE,
121
+ nodes: [{ id: 'test1', type: 'test' }],
122
+ },
123
+ ]);
124
+
125
+ expect(graph.root.id).to.equal('root');
126
+ expect(graph.nodes(graph.root)).to.have.length(1);
127
+ expect(graph.findNode('test1')?.id).to.equal('test1');
98
128
 
99
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
100
- assert.ok(Option.isSome(node));
101
- expect(node.value.id).toEqual(EXAMPLE_ID);
102
- expect(node.value.type).toEqual(EXAMPLE_TYPE);
129
+ graph._removeNodes(['test1']);
130
+ expect(graph.findNode('test1')).to.be.undefined;
131
+ expect(graph.nodes(graph.root)).to.be.empty;
103
132
 
104
- graph.removeNode(EXAMPLE_ID);
105
- expect(node.pipe(Option.getOrNull)).toEqual(null);
133
+ graph._addNodes([
134
+ {
135
+ id: ROOT_ID,
136
+ type: ROOT_TYPE,
137
+ nodes: [{ id: 'test1', type: 'test' }],
138
+ },
139
+ ]);
140
+ expect(graph.root.id).to.equal('root');
141
+ expect(graph.nodes(graph.root)).to.have.length(1);
142
+ expect(graph.findNode('test1')?.id).to.equal('test1');
106
143
  });
107
144
 
108
145
  test('add edge', () => {
109
- const registry = Registry.make();
110
- const graph = new Graph({ registry });
111
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
112
- const edges = registry.get(graph.edges(exampleId(1)));
113
- expect(edges.inbound).toEqual([]);
114
- expect(edges.outbound).toEqual([exampleId(2)]);
115
- });
146
+ const graph = new Graph();
116
147
 
117
- test('add edges is idempotent', () => {
118
- const registry = Registry.make();
119
- const graph = new Graph({ registry });
120
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
121
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
122
- const edges = registry.get(graph.edges(exampleId(1)));
123
- expect(edges.inbound).toEqual([]);
124
- expect(edges.outbound).toEqual([exampleId(2)]);
125
- });
148
+ graph._addNodes([
149
+ {
150
+ id: ROOT_ID,
151
+ type: ROOT_TYPE,
152
+ nodes: [
153
+ { id: 'test1', type: 'test' },
154
+ { id: 'test2', type: 'test' },
155
+ ],
156
+ },
157
+ ]);
158
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
126
159
 
127
- test('sort edges', () => {
128
- const registry = Registry.make();
129
- const graph = new Graph({ registry });
130
-
131
- {
132
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
133
- graph.addEdge({ source: exampleId(1), target: exampleId(3) });
134
- graph.addEdge({ source: exampleId(1), target: exampleId(4) });
135
- const edges = registry.get(graph.edges(exampleId(1)));
136
- expect(edges.outbound).toEqual([exampleId(2), exampleId(3), exampleId(4)]);
137
- }
138
-
139
- {
140
- graph.sortEdges(exampleId(1), 'outbound', [exampleId(3), exampleId(2)]);
141
- const edges = registry.get(graph.edges(exampleId(1)));
142
- expect(edges.outbound).toEqual([exampleId(3), exampleId(2), exampleId(4)]);
143
- }
160
+ expect(graph.nodes(graph.findNode('test1')!)).to.have.length(1);
161
+ expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(2);
144
162
  });
145
163
 
146
- test('remove edge', () => {
147
- const registry = Registry.make();
148
- const graph = new Graph({ registry });
149
-
150
- {
151
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
152
- const edges = registry.get(graph.edges(exampleId(1)));
153
- expect(edges.inbound).toEqual([]);
154
- expect(edges.outbound).toEqual([exampleId(2)]);
155
- }
156
-
157
- {
158
- graph.removeEdge({ source: exampleId(1), target: exampleId(2) });
159
- const edges = registry.get(graph.edges(exampleId(1)));
160
- expect(edges.inbound).toEqual([]);
161
- expect(edges.outbound).toEqual([]);
162
- }
163
- });
164
+ test('add edges is idempontent', () => {
165
+ const graph = new Graph();
166
+
167
+ graph._addNodes([
168
+ {
169
+ id: ROOT_ID,
170
+ type: ROOT_TYPE,
171
+ nodes: [
172
+ { id: 'test1', type: 'test' },
173
+ { id: 'test2', type: 'test' },
174
+ ],
175
+ },
176
+ ]);
177
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
178
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
164
179
 
165
- test('get connections', () => {
166
- const registry = Registry.make();
167
- const graph = new Graph({ registry });
168
- graph.addNode({ id: exampleId(1), type: EXAMPLE_TYPE });
169
- graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE });
170
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
171
- const nodes = registry.get(graph.connections(exampleId(1)));
172
- expect(nodes).has.length(1);
173
- expect(nodes[0].id).toEqual(exampleId(2));
180
+ expect(graph.nodes(graph.findNode('test1')!)).to.have.length(1);
181
+ expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(2);
174
182
  });
175
183
 
176
- test('can subscribe to a node before it exists', async () => {
177
- const registry = Registry.make();
178
- const graph = new Graph({ registry });
179
- const nodeKey = graph.node(exampleId(1));
184
+ test('sort edges', () => {
185
+ const graph = new Graph();
180
186
 
181
- let node: Option.Option<Node> = Option.none();
182
- const cancel = registry.subscribe(nodeKey, (n) => {
183
- node = n;
184
- });
185
- onTestFinished(() => cancel());
187
+ const [root] = graph._addNodes([
188
+ {
189
+ id: ROOT_ID,
190
+ type: ROOT_TYPE,
191
+ nodes: [
192
+ { id: 'test1', type: 'test' },
193
+ { id: 'test3', type: 'test' },
194
+ { id: 'test2', type: 'test' },
195
+ { id: 'test4', type: 'test' },
196
+ ],
197
+ },
198
+ ]);
199
+
200
+ expect(graph.nodes(root).map((node) => node.id)).to.deep.equal(['test1', 'test3', 'test2', 'test4']);
186
201
 
187
- expect(node).toEqual(Option.none());
188
- graph.addNode({ id: exampleId(1), type: EXAMPLE_TYPE });
189
- assert.ok(Option.isSome(node));
190
- expect(node.value.id).toEqual(exampleId(1));
202
+ graph._sortEdges('root', 'outbound', ['test4', 'test3']);
203
+
204
+ expect(graph.nodes(root).map((node) => node.id)).to.deep.equal(['test4', 'test3', 'test1', 'test2']);
191
205
  });
192
206
 
193
- test('connections updates', () => {
194
- const registry = Registry.make();
195
- const graph = new Graph({ registry });
196
- assert.strictEqual(graph.connections(exampleId(1)), graph.connections(exampleId(1)));
197
- const childrenKey = graph.connections(exampleId(1));
207
+ test('remove edge', () => {
208
+ const graph = new Graph();
198
209
 
199
- let count = 0;
200
- const cancel = registry.subscribe(childrenKey, (_) => {
201
- count++;
202
- });
203
- onTestFinished(() => cancel());
204
-
205
- graph.addNode({ id: exampleId(1), type: EXAMPLE_TYPE });
206
- graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE });
207
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
208
-
209
- expect(count).toEqual(0);
210
- const children = registry.get(childrenKey);
211
- expect(children).has.length(1);
212
- expect(children[0].id).toEqual(exampleId(2));
213
- expect(count).toEqual(1);
214
-
215
- // Updating an existing node fires an update.
216
- graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE, data: 'updated' });
217
- expect(count).toEqual(2);
218
-
219
- // Adding a node with no changes does not fire an update.
220
- graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE, data: 'updated' });
221
- expect(count).toEqual(2);
222
-
223
- // Adding an unconnected node does not fire an update.
224
- graph.addNode({ id: exampleId(3), type: EXAMPLE_TYPE });
225
- expect(count).toEqual(2);
226
-
227
- // Connecting a node fires an update.
228
- graph.addEdge({ source: exampleId(1), target: exampleId(3) });
229
- expect(count).toEqual(3);
230
-
231
- // Adding an edge connected to nothing fires an update.
232
- // TODO(wittjosiah): Is there a way to avoid this?
233
- graph.addEdge({ source: exampleId(1), target: exampleId(4) });
234
- expect(count).toEqual(4);
235
-
236
- // Adding a node to an existing edge fires an update.
237
- graph.addNode({ id: exampleId(4), type: EXAMPLE_TYPE });
238
- expect(count).toEqual(5);
239
-
240
- // Batching the edge and node updates fires a single update.
241
- Rx.batch(() => {
242
- graph.addEdge({ source: exampleId(1), target: exampleId(6) });
243
- graph.addNode({ id: exampleId(6), type: EXAMPLE_TYPE });
244
- });
245
- expect(count).toEqual(6);
210
+ graph._addNodes([
211
+ {
212
+ id: ROOT_ID,
213
+ type: ROOT_TYPE,
214
+ nodes: [
215
+ { id: 'test1', type: 'test' },
216
+ { id: 'test2', type: 'test' },
217
+ ],
218
+ },
219
+ ]);
220
+ graph._removeEdges([{ source: 'root', target: 'test1' }]);
221
+
222
+ expect(graph.nodes(graph.root)).to.have.length(1);
223
+ expect(graph.nodes(graph.findNode('test1')!, { relation: 'inbound' })).to.be.empty;
246
224
  });
247
225
 
248
226
  test('toJSON', () => {
249
227
  const graph = new Graph();
250
228
 
251
- graph.addNode({
252
- id: ROOT_ID,
253
- type: ROOT_TYPE,
254
- nodes: [
255
- { id: 'test1', type: 'test' },
256
- { id: 'test2', type: 'test' },
257
- ],
258
- });
259
- graph.addEdge({ source: 'test1', target: 'test2' });
229
+ graph._addNodes([
230
+ {
231
+ id: ROOT_ID,
232
+ type: ROOT_TYPE,
233
+ nodes: [
234
+ { id: 'test1', type: 'test' },
235
+ { id: 'test2', type: 'test' },
236
+ ],
237
+ },
238
+ ]);
239
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
260
240
 
261
241
  const json = graph.toJSON();
262
242
  expect(json).to.deep.equal({
@@ -269,87 +249,109 @@ describe('Graph', () => {
269
249
  });
270
250
  });
271
251
 
272
- test('subscribe to json', () => {
273
- const registry = Registry.make();
274
- const graph = new Graph({ registry });
252
+ test('pickle', () => {
253
+ const pickle =
254
+ '{"nodes":[{"id":"root","type":"dxos.org/type/GraphRoot","properties":{}},{"id":"test1","type":"test","properties":{"value":1}},{"id":"test2","type":"test","properties":{"value":2}}],"edges":{"root":["test1","test2"],"test1":["test2"],"test2":[]}}';
255
+ const graph = Graph.from(pickle);
256
+ expect(graph.pickle()).to.equal(pickle);
257
+ });
275
258
 
276
- graph.addNode({
277
- id: ROOT_ID,
278
- type: ROOT_TYPE,
279
- nodes: [
280
- { id: 'test1', type: 'test' },
281
- { id: 'test2', type: 'test' },
282
- ],
283
- });
284
- graph.addEdge({ source: 'test1', target: 'test2' });
259
+ test('waitForNode', async () => {
260
+ const graph = new Graph();
261
+ const promise = graph.waitForNode('test1');
262
+ graph._addNodes([{ id: 'test1', type: 'test', data: 1 }]);
263
+ const node = await promise;
264
+ expect(node.id).to.equal('test1');
265
+ expect(node.data).to.equal(1);
266
+ });
285
267
 
286
- let json: any;
287
- const cancel = registry.subscribe(graph.json(), (_) => {
288
- json = _;
268
+ test('updates are constrained on data', () => {
269
+ const graph = new Graph();
270
+ const [node1] = graph._addNodes([{ id: 'test1', type: 'test', data: 1 }]);
271
+ using updates = updateCounter(() => {
272
+ node1.data;
289
273
  });
290
- onTestFinished(() => cancel());
274
+ graph._addNodes([{ id: 'test2', type: 'test', data: 2 }]);
275
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
276
+ expect(updates.count, 'update count').to.eq(0);
277
+ graph._addNodes([{ id: 'test1', type: 'test', data: -1 }]);
278
+ expect(updates.count, 'update count').to.eq(1);
279
+ graph._addNodes([{ id: 'test1', type: 'test', data: -1, properties: { label: 'test' } }]);
280
+ expect(updates.count, 'update count').to.eq(1);
281
+ });
291
282
 
292
- registry.get(graph.json());
293
- expect(json).to.deep.equal({
294
- id: ROOT_ID,
295
- type: ROOT_TYPE,
296
- nodes: [
297
- { id: 'test1', type: 'test', nodes: [{ id: 'test2', type: 'test' }] },
298
- { id: 'test2', type: 'test' },
299
- ],
283
+ test('updates are constrained on properties', () => {
284
+ const graph = new Graph();
285
+ const [node1] = graph._addNodes([{ id: 'test1', type: 'test', properties: { value: 1 } }]);
286
+ using updates = updateCounter(() => {
287
+ node1.properties.value;
300
288
  });
289
+ graph._addNodes([{ id: 'test2', type: 'test', properties: { value: 2 } }]);
290
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
291
+ expect(updates.count, 'update count').to.eq(0);
292
+ graph._addNodes([{ id: 'test1', type: 'test', properties: { value: -1 } }]);
293
+ expect(updates.count, 'update count').to.eq(1);
294
+ });
301
295
 
302
- graph.addNode({ id: 'test3', type: 'test' });
303
- graph.addEdge({ source: 'root', target: 'test3' });
304
- expect(json).to.deep.equal({
305
- id: ROOT_ID,
306
- type: ROOT_TYPE,
307
- nodes: [
308
- { id: 'test1', type: 'test', nodes: [{ id: 'test2', type: 'test' }] },
309
- { id: 'test2', type: 'test' },
310
- { id: 'test3', type: 'test' },
311
- ],
296
+ test('updates are constrained on connected nodes', () => {
297
+ const graph = new Graph();
298
+ const [node1] = graph._addNodes([{ id: 'test1', type: 'test', properties: { value: 1 } }]);
299
+ using updates = updateCounter(() => {
300
+ graph.nodes(node1);
312
301
  });
302
+ expect(updates.count, 'update count').to.eq(0);
303
+ graph._addNodes([{ id: 'test2', type: 'test', properties: { value: 2 } }]);
304
+ expect(updates.count, 'update count').to.eq(0);
305
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
306
+ expect(updates.count, 'update count').to.eq(1);
307
+ graph._addNodes([{ id: 'test2', type: 'test', properties: { value: -2 } }]);
308
+ expect(updates.count, 'update count').to.eq(1);
309
+ graph._addNodes([{ id: 'test3', type: 'test', properties: { value: 3 } }]);
310
+ expect(updates.count, 'update count').to.eq(1);
311
+ graph._addEdges([{ source: 'test2', target: 'test3' }]);
312
+ expect(updates.count, 'update count').to.eq(1);
313
+ graph._addEdges([{ source: 'test1', target: 'test3' }]);
314
+ expect(updates.count, 'update count').to.eq(2);
313
315
  });
314
316
 
315
317
  test('get path', () => {
316
318
  const graph = new Graph();
317
- graph.addNode({
318
- id: ROOT_ID,
319
- type: ROOT_TYPE,
320
- nodes: [
321
- { id: exampleId(1), type: EXAMPLE_TYPE },
322
- { id: exampleId(2), type: EXAMPLE_TYPE },
323
- ],
324
- });
325
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
326
319
 
327
- expect(graph.getPath({ target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
328
- 'root',
329
- exampleId(1),
330
- exampleId(2),
331
- ]);
332
- expect(graph.getPath({ source: exampleId(1), target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
333
- exampleId(1),
334
- exampleId(2),
335
- ]);
336
- expect(graph.getPath({ source: exampleId(2), target: exampleId(1) }).pipe(Option.getOrNull)).to.be.null;
337
- });
338
-
339
- describe('traverse', () => {
340
- test('can be traversed', () => {
341
- const graph = new Graph();
342
- graph.addNode({
320
+ graph._addNodes([
321
+ {
343
322
  id: ROOT_ID,
344
323
  type: ROOT_TYPE,
345
324
  nodes: [
346
325
  { id: 'test1', type: 'test' },
347
326
  { id: 'test2', type: 'test' },
348
327
  ],
349
- });
328
+ },
329
+ ]);
330
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
331
+
332
+ expect(graph.getPath({ target: 'test2' })).to.deep.equal(['root', 'test1', 'test2']);
333
+ expect(graph.getPath({ source: 'test1', target: 'test2' })).to.deep.equal(['test1', 'test2']);
334
+ expect(graph.getPath({ source: 'test2', target: 'test1' })).to.be.undefined;
335
+ });
336
+
337
+ describe('traverse', () => {
338
+ test('can be traversed', () => {
339
+ const graph = new Graph();
340
+
341
+ const [root] = graph._addNodes([
342
+ {
343
+ id: ROOT_ID,
344
+ type: ROOT_TYPE,
345
+ nodes: [
346
+ { id: 'test1', type: 'test' },
347
+ { id: 'test2', type: 'test' },
348
+ ],
349
+ },
350
+ ]);
350
351
 
351
352
  const nodes: string[] = [];
352
353
  graph.traverse({
354
+ node: root,
353
355
  visitor: (node) => {
354
356
  nodes.push(node.id);
355
357
  },
@@ -359,18 +361,22 @@ describe('Graph', () => {
359
361
 
360
362
  test('traversal breaks cycles', () => {
361
363
  const graph = new Graph();
362
- graph.addNode({
363
- id: ROOT_ID,
364
- type: ROOT_TYPE,
365
- nodes: [
366
- { id: 'test1', type: 'test' },
367
- { id: 'test2', type: 'test' },
368
- ],
369
- });
370
- graph.addEdge({ source: 'test1', target: 'root' });
364
+
365
+ const [root] = graph._addNodes([
366
+ {
367
+ id: ROOT_ID,
368
+ type: ROOT_TYPE,
369
+ nodes: [
370
+ { id: 'test1', type: 'test' },
371
+ { id: 'test2', type: 'test' },
372
+ ],
373
+ },
374
+ ]);
375
+ graph._addEdges([{ source: 'test1', target: 'root' }]);
371
376
 
372
377
  const nodes: string[] = [];
373
378
  graph.traverse({
379
+ node: root,
374
380
  visitor: (node) => {
375
381
  nodes.push(node.id);
376
382
  },
@@ -380,21 +386,24 @@ describe('Graph', () => {
380
386
 
381
387
  test('traversal can be started from any node', () => {
382
388
  const graph = new Graph();
383
- graph.addNode({
384
- id: ROOT_ID,
385
- type: ROOT_TYPE,
386
- nodes: [
387
- {
388
- id: 'test1',
389
- type: 'test',
390
- nodes: [{ id: 'test2', type: 'test', nodes: [{ id: 'test3', type: 'test' }] }],
391
- },
392
- ],
393
- });
389
+
390
+ graph._addNodes([
391
+ {
392
+ id: ROOT_ID,
393
+ type: ROOT_TYPE,
394
+ nodes: [
395
+ {
396
+ id: 'test1',
397
+ type: 'test',
398
+ nodes: [{ id: 'test2', type: 'test', nodes: [{ id: 'test3', type: 'test' }] }],
399
+ },
400
+ ],
401
+ },
402
+ ]);
394
403
 
395
404
  const nodes: string[] = [];
396
405
  graph.traverse({
397
- source: 'test2',
406
+ node: graph.findNode('test2')!,
398
407
  visitor: (node) => {
399
408
  nodes.push(node.id);
400
409
  },
@@ -404,21 +413,24 @@ describe('Graph', () => {
404
413
 
405
414
  test('traversal can follow inbound edges', () => {
406
415
  const graph = new Graph();
407
- graph.addNode({
408
- id: ROOT_ID,
409
- type: ROOT_TYPE,
410
- nodes: [
411
- {
412
- id: 'test1',
413
- type: 'test',
414
- nodes: [{ id: 'test2', type: 'test', nodes: [{ id: 'test3', type: 'test' }] }],
415
- },
416
- ],
417
- });
416
+
417
+ graph._addNodes([
418
+ {
419
+ id: ROOT_ID,
420
+ type: ROOT_TYPE,
421
+ nodes: [
422
+ {
423
+ id: 'test1',
424
+ type: 'test',
425
+ nodes: [{ id: 'test2', type: 'test', nodes: [{ id: 'test3', type: 'test' }] }],
426
+ },
427
+ ],
428
+ },
429
+ ]);
418
430
 
419
431
  const nodes: string[] = [];
420
432
  graph.traverse({
421
- source: 'test2',
433
+ node: graph.findNode('test2')!,
422
434
  relation: 'inbound',
423
435
  visitor: (node) => {
424
436
  nodes.push(node.id);
@@ -427,19 +439,100 @@ describe('Graph', () => {
427
439
  expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
428
440
  });
429
441
 
430
- test('traversal can be terminated early', () => {
442
+ test('can filter to longest paths', () => {
431
443
  const graph = new Graph();
432
- graph.addNode({
433
- id: ROOT_ID,
434
- type: ROOT_TYPE,
435
- nodes: [
436
- { id: 'test1', type: 'test' },
437
- { id: 'test2', type: 'test' },
438
- ],
444
+
445
+ graph._addNodes([
446
+ {
447
+ id: ROOT_ID,
448
+ type: ROOT_TYPE,
449
+ nodes: [
450
+ { id: 'test1', type: 'test' },
451
+ { id: 'test2', type: 'test' },
452
+ ],
453
+ },
454
+ ]);
455
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
456
+
457
+ graph.traverse({
458
+ visitor: (node, path) => {
459
+ if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
460
+ longestPaths.set(node.id, path);
461
+ }
462
+ },
463
+ });
464
+
465
+ expect(longestPaths.get('root')).to.deep.equal(['root']);
466
+ expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
467
+ expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
468
+ expect(graph.nodes(graph.root, { filter: filterLongestPath })).to.have.length(1);
469
+ expect(graph.nodes(graph.findNode('test1')!, { filter: filterLongestPath })).to.have.length(1);
470
+ expect(graph.nodes(graph.findNode('test2')!, { filter: filterLongestPath })).to.be.empty;
471
+
472
+ longestPaths.clear();
473
+ });
474
+
475
+ test('traversing the graph subscribes to changes', () => {
476
+ const graph = new Graph();
477
+
478
+ graph._addNodes([
479
+ {
480
+ id: ROOT_ID,
481
+ type: ROOT_TYPE,
482
+ nodes: [
483
+ { id: 'test1', type: 'test' },
484
+ { id: 'test2', type: 'test' },
485
+ ],
486
+ },
487
+ ]);
488
+
489
+ const dispose = effect(() => {
490
+ graph.traverse({
491
+ visitor: (node, path) => {
492
+ if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
493
+ longestPaths.set(node.id, path);
494
+ }
495
+ },
496
+ });
439
497
  });
440
498
 
499
+ expect(longestPaths.get('root')).to.deep.equal(['root']);
500
+ expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
501
+ expect(longestPaths.get('test2')).to.deep.equal(['root', 'test2']);
502
+ expect(graph.nodes(graph.root, { filter: filterLongestPath })).to.have.length(2);
503
+ expect(graph.nodes(graph.findNode('test1')!, { filter: filterLongestPath })).to.be.empty;
504
+ expect(graph.nodes(graph.findNode('test2')!, { filter: filterLongestPath })).to.be.empty;
505
+
506
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
507
+
508
+ expect(longestPaths.get('root')).to.deep.equal(['root']);
509
+ expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
510
+ expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
511
+ expect(graph.nodes(graph.root, { filter: filterLongestPath })).to.have.length(1);
512
+ expect(graph.nodes(graph.findNode('test1')!, { filter: filterLongestPath })).to.have.length(1);
513
+ expect(graph.nodes(graph.findNode('test2')!, { filter: filterLongestPath })).to.be.empty;
514
+
515
+ dispose();
516
+ longestPaths.clear();
517
+ });
518
+
519
+ test('traversal can be terminated early', () => {
520
+ const graph = new Graph();
521
+
522
+ const [root] = graph._addNodes([
523
+ {
524
+ id: ROOT_ID,
525
+ type: ROOT_TYPE,
526
+ nodes: [
527
+ { id: 'test1', type: 'test' },
528
+ { id: 'test2', type: 'test' },
529
+ ],
530
+ },
531
+ ]);
532
+
441
533
  const nodes: string[] = [];
442
534
  graph.traverse({
535
+ node: root,
443
536
  visitor: (node) => {
444
537
  if (nodes.length === 2) {
445
538
  return false;
@@ -450,5 +543,61 @@ describe('Graph', () => {
450
543
  });
451
544
  expect(nodes).to.deep.equal(['root', 'test1']);
452
545
  });
546
+
547
+ test('traversal can be reactive', async () => {
548
+ const graph = new Graph();
549
+ const latest: Record<string, any> = {};
550
+ const updates: Record<string, number> = {};
551
+ graph.subscribeTraverse({
552
+ node: graph.root,
553
+ visitor: (node) => {
554
+ latest[node.id] = node.data;
555
+ updates[node.id] = (updates[node.id] ?? 0) + 1;
556
+ },
557
+ });
558
+
559
+ expect(latest.root).to.equal(null);
560
+ expect(updates.root).to.equal(1);
561
+
562
+ graph._addNodes([
563
+ {
564
+ id: ROOT_ID,
565
+ type: ROOT_TYPE,
566
+ nodes: [
567
+ {
568
+ id: 'test1',
569
+ type: 'test',
570
+ data: 1,
571
+ nodes: [{ id: 'test2', type: 'test', data: 2 }],
572
+ },
573
+ ],
574
+ },
575
+ ]);
576
+
577
+ expect(latest.root).to.equal(null);
578
+ expect(latest.test1).to.equal(1);
579
+ expect(latest.test2).to.equal(2);
580
+ expect(updates.root).to.equal(2);
581
+ expect(updates.test1).to.equal(1);
582
+ expect(updates.test2).to.equal(1);
583
+
584
+ graph._addNodes([{ id: 'test2', type: 'test', data: -2 }]);
585
+
586
+ expect(latest.root).to.equal(null);
587
+ expect(latest.test1).to.equal(1);
588
+ expect(latest.test2).to.equal(-2);
589
+ expect(updates.root).to.equal(2);
590
+ expect(updates.test1).to.equal(1);
591
+ expect(updates.test2).to.equal(2);
592
+
593
+ graph._addNodes([{ id: 'test1', type: 'test', data: -1 }]);
594
+
595
+ expect(latest.root).to.equal(null);
596
+ expect(latest.test1).to.equal(-1);
597
+ expect(latest.test2).to.equal(-2);
598
+ expect(updates.root).to.equal(2);
599
+ expect(updates.test1).to.equal(2);
600
+ expect(updates.test2).to.equal(3);
601
+ });
453
602
  });
454
603
  });