@dxos/app-graph 0.6.3-next.2f65b78 → 0.6.3-next.4424131

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/src/graph.test.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  import { effect } from '@preact/signals-core';
6
6
  import { expect } from 'chai';
7
7
 
8
+ import { updateCounter } from '@dxos/echo-schema/testing';
8
9
  import { registerSignalRuntime } from '@dxos/echo-signals';
9
10
  import { describe, test } from '@dxos/test';
10
11
 
11
- import { Graph } from './graph';
12
+ import { Graph, ROOT_ID, ROOT_TYPE, getGraph } from './graph';
12
13
  import { type Node, type NodeFilter } from './node';
13
14
 
14
15
  const longestPaths = new Map<string, string[]>();
@@ -26,253 +27,398 @@ const filterLongestPath: NodeFilter = (node, connectedNode): node is Node => {
26
27
  return true;
27
28
  };
28
29
 
29
- // TODO(wittjosiah): Add tests for granularity of reactivity.
30
30
  describe('Graph', () => {
31
+ test('getGraph', () => {
32
+ const graph = new Graph();
33
+ expect(getGraph(graph.root)).to.equal(graph);
34
+ });
35
+
31
36
  test('add nodes', () => {
32
37
  const graph = new Graph();
33
38
 
34
- const [root] = graph.addNodes({
35
- id: 'root',
36
- nodes: [{ id: 'test1' }, { id: 'test2' }],
37
- });
39
+ const [root] = graph._addNodes([
40
+ {
41
+ id: ROOT_ID,
42
+ type: ROOT_TYPE,
43
+ nodes: [
44
+ { id: 'test1', type: 'test' },
45
+ { id: 'test2', type: 'test' },
46
+ ],
47
+ },
48
+ ]);
38
49
 
39
50
  expect(root.id).to.equal('root');
40
- expect(root.nodes()).to.have.length(2);
51
+ expect(graph.nodes(root)).to.have.length(2);
41
52
  expect(graph.findNode('test1')?.id).to.equal('test1');
42
53
  expect(graph.findNode('test2')?.id).to.equal('test2');
43
- expect(graph.findNode('test1')?.nodes()).to.be.empty;
44
- expect(graph.findNode('test2')?.nodes()).to.be.empty;
45
- expect(graph.findNode('test1')?.nodes({ direction: 'inbound' })).to.have.length(1);
46
- expect(graph.findNode('test2')?.nodes({ direction: 'inbound' })).to.have.length(1);
54
+ expect(graph.nodes(graph.findNode('test1')!)).to.be.empty;
55
+ expect(graph.nodes(graph.findNode('test2')!)).to.be.empty;
56
+ expect(graph.nodes(graph.findNode('test1')!, { relation: 'inbound' })).to.have.length(1);
57
+ expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(1);
47
58
  });
48
59
 
49
60
  test('add nodes updates existing nodes', () => {
50
61
  const graph = new Graph();
51
62
 
52
- graph.addNodes({
53
- id: 'root',
54
- nodes: [{ id: 'test1' }, { id: 'test2' }],
55
- });
56
- graph.addNodes({
57
- id: 'root',
58
- nodes: [{ id: 'test1' }, { id: 'test2' }],
59
- });
63
+ graph._addNodes([
64
+ {
65
+ id: ROOT_ID,
66
+ type: ROOT_TYPE,
67
+ nodes: [
68
+ { id: 'test1', type: 'test' },
69
+ { id: 'test2', type: 'test' },
70
+ ],
71
+ },
72
+ ]);
73
+ graph._addNodes([
74
+ {
75
+ id: ROOT_ID,
76
+ type: ROOT_TYPE,
77
+ nodes: [
78
+ { id: 'test1', type: 'test' },
79
+ { id: 'test2', type: 'test' },
80
+ ],
81
+ },
82
+ ]);
60
83
 
61
84
  expect(Object.keys(graph._nodes)).to.have.length(3);
62
85
  expect(Object.keys(graph._edges)).to.have.length(3);
63
- expect(graph.root.nodes()).to.have.length(2);
86
+ expect(graph.nodes(graph.root)).to.have.length(2);
64
87
  });
65
88
 
66
89
  test('remove node', () => {
67
90
  const graph = new Graph();
68
91
 
69
- const [root] = graph.addNodes({
70
- id: 'root',
71
- nodes: [{ id: 'test1' }, { id: 'test2' }],
72
- });
92
+ const [root] = graph._addNodes([
93
+ {
94
+ id: ROOT_ID,
95
+ type: ROOT_TYPE,
96
+ nodes: [
97
+ { id: 'test1', type: 'test' },
98
+ { id: 'test2', type: 'test' },
99
+ ],
100
+ },
101
+ ]);
73
102
 
74
103
  expect(root.id).to.equal('root');
75
- expect(root.nodes()).to.have.length(2);
104
+ expect(graph.nodes(root)).to.have.length(2);
76
105
  expect(graph.findNode('test1')?.id).to.equal('test1');
77
106
  expect(graph.findNode('test2')?.id).to.equal('test2');
78
107
 
79
- graph.removeNode('test1');
108
+ graph._removeNodes(['test1']);
80
109
  expect(graph.findNode('test1')).to.be.undefined;
81
- expect(root.nodes()).to.have.length(1);
110
+ expect(graph.nodes(root)).to.have.length(1);
82
111
  });
83
112
 
84
113
  test('add edge', () => {
85
114
  const graph = new Graph();
86
115
 
87
- graph.addNodes({
88
- id: 'root',
89
- nodes: [{ id: 'test1' }, { id: 'test2' }],
90
- });
91
- graph.addEdge({ source: 'test1', target: 'test2' });
116
+ graph._addNodes([
117
+ {
118
+ id: ROOT_ID,
119
+ type: ROOT_TYPE,
120
+ nodes: [
121
+ { id: 'test1', type: 'test' },
122
+ { id: 'test2', type: 'test' },
123
+ ],
124
+ },
125
+ ]);
126
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
92
127
 
93
- expect(graph.findNode('test1')?.nodes()).to.have.length(1);
94
- expect(graph.findNode('test2')?.nodes({ direction: 'inbound' })).to.have.length(2);
128
+ expect(graph.nodes(graph.findNode('test1')!)).to.have.length(1);
129
+ expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(2);
95
130
  });
96
131
 
97
132
  test('add edges is idempontent', () => {
98
133
  const graph = new Graph();
99
134
 
100
- graph.addNodes({
101
- id: 'root',
102
- nodes: [{ id: 'test1' }, { id: 'test2' }],
103
- });
104
- graph.addEdge({ source: 'test1', target: 'test2' });
105
- graph.addEdge({ source: 'test1', target: 'test2' });
135
+ graph._addNodes([
136
+ {
137
+ id: ROOT_ID,
138
+ type: ROOT_TYPE,
139
+ nodes: [
140
+ { id: 'test1', type: 'test' },
141
+ { id: 'test2', type: 'test' },
142
+ ],
143
+ },
144
+ ]);
145
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
146
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
106
147
 
107
- expect(graph.findNode('test1')?.nodes()).to.have.length(1);
108
- expect(graph.findNode('test2')?.nodes({ direction: 'inbound' })).to.have.length(2);
148
+ expect(graph.nodes(graph.findNode('test1')!)).to.have.length(1);
149
+ expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(2);
109
150
  });
110
151
 
111
152
  test('sort edges', () => {
112
153
  const graph = new Graph();
113
154
 
114
- const [root] = graph.addNodes({
115
- id: 'root',
116
- nodes: [{ id: 'test1' }, { id: 'test3' }, { id: 'test2' }, { id: 'test4' }],
117
- });
155
+ const [root] = graph._addNodes([
156
+ {
157
+ id: ROOT_ID,
158
+ type: ROOT_TYPE,
159
+ nodes: [
160
+ { id: 'test1', type: 'test' },
161
+ { id: 'test3', type: 'test' },
162
+ { id: 'test2', type: 'test' },
163
+ { id: 'test4', type: 'test' },
164
+ ],
165
+ },
166
+ ]);
118
167
 
119
- expect(root.nodes().map((node) => node.id)).to.deep.equal(['test1', 'test3', 'test2', 'test4']);
168
+ expect(graph.nodes(root).map((node) => node.id)).to.deep.equal(['test1', 'test3', 'test2', 'test4']);
120
169
 
121
- graph.sortEdges('root', 'outbound', ['test4', 'test3']);
170
+ graph._sortEdges('root', 'outbound', ['test4', 'test3']);
122
171
 
123
- expect(root.nodes().map((node) => node.id)).to.deep.equal(['test4', 'test3', 'test1', 'test2']);
172
+ expect(graph.nodes(root).map((node) => node.id)).to.deep.equal(['test4', 'test3', 'test1', 'test2']);
124
173
  });
125
174
 
126
175
  test('remove edge', () => {
127
176
  const graph = new Graph();
128
177
 
129
- graph.addNodes({
130
- id: 'root',
131
- nodes: [{ id: 'test1' }, { id: 'test2' }],
132
- });
133
- graph.removeEdge({ source: 'root', target: 'test1' });
178
+ graph._addNodes([
179
+ {
180
+ id: ROOT_ID,
181
+ type: ROOT_TYPE,
182
+ nodes: [
183
+ { id: 'test1', type: 'test' },
184
+ { id: 'test2', type: 'test' },
185
+ ],
186
+ },
187
+ ]);
188
+ graph._removeEdges([{ source: 'root', target: 'test1' }]);
134
189
 
135
- expect(graph.root.nodes()).to.have.length(1);
136
- expect(graph.findNode('test1')?.nodes({ direction: 'inbound' })).to.be.empty;
190
+ expect(graph.nodes(graph.root)).to.have.length(1);
191
+ expect(graph.nodes(graph.findNode('test1')!, { relation: 'inbound' })).to.be.empty;
137
192
  });
138
193
 
139
194
  test('toJSON', () => {
140
195
  const graph = new Graph();
141
196
 
142
- graph.addNodes({
143
- id: 'root',
144
- nodes: [{ id: 'test1' }, { id: 'test2' }],
145
- });
146
- graph.addEdge({ source: 'test1', target: 'test2' });
197
+ graph._addNodes([
198
+ {
199
+ id: ROOT_ID,
200
+ type: ROOT_TYPE,
201
+ nodes: [
202
+ { id: 'test1', type: 'test' },
203
+ { id: 'test2', type: 'test' },
204
+ ],
205
+ },
206
+ ]);
207
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
147
208
 
148
209
  const json = graph.toJSON();
149
210
  expect(json).to.deep.equal({
150
- id: 'root',
151
- nodes: [{ id: 'test1', nodes: [{ id: 'test2' }] }, { id: 'test2' }],
211
+ id: ROOT_ID,
212
+ type: ROOT_TYPE,
213
+ nodes: [
214
+ { id: 'test1', type: 'test', nodes: [{ id: 'test2', type: 'test' }] },
215
+ { id: 'test2', type: 'test' },
216
+ ],
152
217
  });
153
218
  });
154
219
 
155
- test('can be traversed', () => {
220
+ test('waitForNode', async () => {
221
+ registerSignalRuntime();
156
222
  const graph = new Graph();
157
-
158
- const [root] = graph.addNodes({
159
- id: 'root',
160
- nodes: [{ id: 'test1' }, { id: 'test2' }],
161
- });
162
-
163
- const nodes: string[] = [];
164
- graph.traverse({ node: root, visitor: (node) => nodes.push(node.id) });
165
- expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
223
+ const promise = graph.waitForNode('test1');
224
+ graph._addNodes([{ id: 'test1', type: 'test', data: 1 }]);
225
+ const node = await promise;
226
+ expect(node.id).to.equal('test1');
227
+ expect(node.data).to.equal(1);
166
228
  });
167
229
 
168
- test('traversal breaks cycles', () => {
230
+ test('updates are constrained on data', () => {
231
+ registerSignalRuntime();
169
232
  const graph = new Graph();
170
-
171
- const [root] = graph.addNodes({
172
- id: 'root',
173
- nodes: [{ id: 'test1' }, { id: 'test2' }],
233
+ const [node1] = graph._addNodes([{ id: 'test1', type: 'test', data: 1 }]);
234
+ using updates = updateCounter(() => {
235
+ node1.data;
174
236
  });
175
- graph.addEdge({ source: 'test1', target: 'root' });
176
-
177
- const nodes: string[] = [];
178
- graph.traverse({ node: root, visitor: (node) => nodes.push(node.id) });
179
- expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
237
+ graph._addNodes([{ id: 'test2', type: 'test', data: 2 }]);
238
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
239
+ expect(updates.count, 'update count').to.eq(0);
240
+ graph._addNodes([{ id: 'test1', type: 'test', data: -1 }]);
241
+ expect(updates.count, 'update count').to.eq(1);
242
+ graph._addNodes([{ id: 'test1', type: 'test', data: -1, properties: { label: 'test' } }]);
243
+ expect(updates.count, 'update count').to.eq(1);
180
244
  });
181
245
 
182
- test('traversal can be limited by predicate', () => {
246
+ test('updates are constrained on properties', () => {
247
+ registerSignalRuntime();
183
248
  const graph = new Graph();
184
-
185
- const [root] = graph.addNodes({
186
- id: 'root',
187
- nodes: [{ id: 'test1' }, { id: 'test2' }, { id: 'test3' }, { id: 'test4' }],
188
- });
189
-
190
- const nodes: string[] = [];
191
- graph.traverse({
192
- node: root,
193
- visitor: (node) => nodes.push(node.id),
194
- filter: (node) => {
195
- try {
196
- const id = parseInt(node.id.replace('test', ''), 10);
197
- return id % 2 === 0;
198
- } catch (e) {
199
- return false;
200
- }
201
- },
249
+ const [node1] = graph._addNodes([{ id: 'test1', type: 'test', properties: { value: 1 } }]);
250
+ using updates = updateCounter(() => {
251
+ node1.properties.value;
202
252
  });
203
- expect(nodes).to.deep.equal(['test2', 'test4']);
253
+ graph._addNodes([{ id: 'test2', type: 'test', properties: { value: 2 } }]);
254
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
255
+ expect(updates.count, 'update count').to.eq(0);
256
+ graph._addNodes([{ id: 'test1', type: 'test', properties: { value: -1 } }]);
257
+ expect(updates.count, 'update count').to.eq(1);
204
258
  });
205
259
 
206
- test('traversal can be started from any node', () => {
260
+ test('updates are constrained on connected nodes', () => {
261
+ registerSignalRuntime();
207
262
  const graph = new Graph();
208
-
209
- graph.addNodes({
210
- id: 'root',
211
- nodes: [{ id: 'test1', nodes: [{ id: 'test2', nodes: [{ id: 'test3' }] }] }],
263
+ const [node1] = graph._addNodes([{ id: 'test1', type: 'test', properties: { value: 1 } }]);
264
+ using updates = updateCounter(() => {
265
+ graph.nodes(node1);
212
266
  });
213
-
214
- const nodes: string[] = [];
215
- graph.traverse({
216
- node: graph.findNode('test2')!,
217
- visitor: (node) => nodes.push(node.id),
218
- });
219
- expect(nodes).to.deep.equal(['test2', 'test3']);
267
+ expect(updates.count, 'update count').to.eq(0);
268
+ graph._addNodes([{ id: 'test2', type: 'test', properties: { value: 2 } }]);
269
+ expect(updates.count, 'update count').to.eq(0);
270
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
271
+ expect(updates.count, 'update count').to.eq(1);
272
+ graph._addNodes([{ id: 'test2', type: 'test', properties: { value: -2 } }]);
273
+ expect(updates.count, 'update count').to.eq(1);
274
+ graph._addNodes([{ id: 'test3', type: 'test', properties: { value: 3 } }]);
275
+ expect(updates.count, 'update count').to.eq(1);
276
+ graph._addEdges([{ source: 'test2', target: 'test3' }]);
277
+ expect(updates.count, 'update count').to.eq(1);
278
+ graph._addEdges([{ source: 'test1', target: 'test3' }]);
279
+ expect(updates.count, 'update count').to.eq(2);
220
280
  });
221
281
 
222
- test('traversal can follow inbound edges', () => {
282
+ test('get path', () => {
223
283
  const graph = new Graph();
224
284
 
225
- graph.addNodes({
226
- id: 'root',
227
- nodes: [{ id: 'test1', nodes: [{ id: 'test2', nodes: [{ id: 'test3' }] }] }],
228
- });
285
+ graph._addNodes([
286
+ {
287
+ id: ROOT_ID,
288
+ type: ROOT_TYPE,
289
+ nodes: [
290
+ { id: 'test1', type: 'test' },
291
+ { id: 'test2', type: 'test' },
292
+ ],
293
+ },
294
+ ]);
295
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
229
296
 
230
- const nodes: string[] = [];
231
- graph.traverse({
232
- node: graph.findNode('test2')!,
233
- direction: 'inbound',
234
- visitor: (node) => nodes.push(node.id),
235
- });
236
- expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
297
+ expect(graph.getPath({ target: 'test2' })).to.deep.equal(['root', 'test1', 'test2']);
298
+ expect(graph.getPath({ source: 'test1', target: 'test2' })).to.deep.equal(['test1', 'test2']);
299
+ expect(graph.getPath({ source: 'test2', target: 'test1' })).to.be.undefined;
237
300
  });
238
301
 
239
- test('can filter to longest pathes', () => {
240
- const graph = new Graph();
302
+ describe('traverse', () => {
303
+ test('can be traversed', () => {
304
+ const graph = new Graph();
305
+
306
+ const [root] = graph._addNodes([
307
+ {
308
+ id: ROOT_ID,
309
+ type: ROOT_TYPE,
310
+ nodes: [
311
+ { id: 'test1', type: 'test' },
312
+ { id: 'test2', type: 'test' },
313
+ ],
314
+ },
315
+ ]);
241
316
 
242
- graph.addNodes({
243
- id: 'root',
244
- nodes: [{ id: 'test1' }, { id: 'test2' }],
317
+ const nodes: string[] = [];
318
+ graph.traverse({
319
+ node: root,
320
+ visitor: (node) => {
321
+ nodes.push(node.id);
322
+ },
323
+ });
324
+ expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
245
325
  });
246
- graph.addEdge({ source: 'test1', target: 'test2' });
247
326
 
248
- graph.traverse({
249
- visitor: (node, path) => {
250
- if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
251
- longestPaths.set(node.id, path);
252
- }
253
- },
327
+ test('traversal breaks cycles', () => {
328
+ const graph = new Graph();
329
+
330
+ const [root] = graph._addNodes([
331
+ {
332
+ id: ROOT_ID,
333
+ type: ROOT_TYPE,
334
+ nodes: [
335
+ { id: 'test1', type: 'test' },
336
+ { id: 'test2', type: 'test' },
337
+ ],
338
+ },
339
+ ]);
340
+ graph._addEdges([{ source: 'test1', target: 'root' }]);
341
+
342
+ const nodes: string[] = [];
343
+ graph.traverse({
344
+ node: root,
345
+ visitor: (node) => {
346
+ nodes.push(node.id);
347
+ },
348
+ });
349
+ expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
254
350
  });
255
351
 
256
- expect(longestPaths.get('root')).to.deep.equal(['root']);
257
- expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
258
- expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
259
- expect(graph.root.nodes({ filter: filterLongestPath })).to.have.length(1);
260
- expect(graph.findNode('test1')?.nodes({ filter: filterLongestPath })).to.have.length(1);
261
- expect(graph.findNode('test2')?.nodes({ filter: filterLongestPath })).to.be.empty;
352
+ test('traversal can be started from any node', () => {
353
+ const graph = new Graph();
354
+
355
+ graph._addNodes([
356
+ {
357
+ id: ROOT_ID,
358
+ type: ROOT_TYPE,
359
+ nodes: [
360
+ {
361
+ id: 'test1',
362
+ type: 'test',
363
+ nodes: [{ id: 'test2', type: 'test', nodes: [{ id: 'test3', type: 'test' }] }],
364
+ },
365
+ ],
366
+ },
367
+ ]);
262
368
 
263
- longestPaths.clear();
264
- });
369
+ const nodes: string[] = [];
370
+ graph.traverse({
371
+ node: graph.findNode('test2')!,
372
+ visitor: (node) => {
373
+ nodes.push(node.id);
374
+ },
375
+ });
376
+ expect(nodes).to.deep.equal(['test2', 'test3']);
377
+ });
265
378
 
266
- test('traversing the graph subscribes to changes', () => {
267
- registerSignalRuntime();
268
- const graph = new Graph();
379
+ test('traversal can follow inbound edges', () => {
380
+ const graph = new Graph();
381
+
382
+ graph._addNodes([
383
+ {
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
+ },
394
+ ]);
269
395
 
270
- graph.addNodes({
271
- id: 'root',
272
- nodes: [{ id: 'test1' }, { id: 'test2' }],
396
+ const nodes: string[] = [];
397
+ graph.traverse({
398
+ node: graph.findNode('test2')!,
399
+ relation: 'inbound',
400
+ visitor: (node) => {
401
+ nodes.push(node.id);
402
+ },
403
+ });
404
+ expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
273
405
  });
274
406
 
275
- const dispose = effect(() => {
407
+ test('can filter to longest paths', () => {
408
+ const graph = new Graph();
409
+
410
+ graph._addNodes([
411
+ {
412
+ id: ROOT_ID,
413
+ type: ROOT_TYPE,
414
+ nodes: [
415
+ { id: 'test1', type: 'test' },
416
+ { id: 'test2', type: 'test' },
417
+ ],
418
+ },
419
+ ]);
420
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
421
+
276
422
  graph.traverse({
277
423
  visitor: (node, path) => {
278
424
  if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
@@ -280,39 +426,145 @@ describe('Graph', () => {
280
426
  }
281
427
  },
282
428
  });
429
+
430
+ expect(longestPaths.get('root')).to.deep.equal(['root']);
431
+ expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
432
+ expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
433
+ expect(graph.nodes(graph.root, { filter: filterLongestPath })).to.have.length(1);
434
+ expect(graph.nodes(graph.findNode('test1')!, { filter: filterLongestPath })).to.have.length(1);
435
+ expect(graph.nodes(graph.findNode('test2')!, { filter: filterLongestPath })).to.be.empty;
436
+
437
+ longestPaths.clear();
283
438
  });
284
439
 
285
- expect(longestPaths.get('root')).to.deep.equal(['root']);
286
- expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
287
- expect(longestPaths.get('test2')).to.deep.equal(['root', 'test2']);
288
- expect(graph.root.nodes({ filter: filterLongestPath })).to.have.length(2);
289
- expect(graph.findNode('test1')?.nodes({ filter: filterLongestPath })).to.be.empty;
290
- expect(graph.findNode('test2')?.nodes({ filter: filterLongestPath })).to.be.empty;
440
+ test('traversing the graph subscribes to changes', () => {
441
+ registerSignalRuntime();
442
+ const graph = new Graph();
443
+
444
+ graph._addNodes([
445
+ {
446
+ id: ROOT_ID,
447
+ type: ROOT_TYPE,
448
+ nodes: [
449
+ { id: 'test1', type: 'test' },
450
+ { id: 'test2', type: 'test' },
451
+ ],
452
+ },
453
+ ]);
454
+
455
+ const dispose = effect(() => {
456
+ graph.traverse({
457
+ visitor: (node, path) => {
458
+ if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
459
+ longestPaths.set(node.id, path);
460
+ }
461
+ },
462
+ });
463
+ });
291
464
 
292
- graph.addEdge({ source: 'test1', target: 'test2' });
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', 'test2']);
468
+ expect(graph.nodes(graph.root, { filter: filterLongestPath })).to.have.length(2);
469
+ expect(graph.nodes(graph.findNode('test1')!, { filter: filterLongestPath })).to.be.empty;
470
+ expect(graph.nodes(graph.findNode('test2')!, { filter: filterLongestPath })).to.be.empty;
293
471
 
294
- expect(longestPaths.get('root')).to.deep.equal(['root']);
295
- expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
296
- expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
297
- expect(graph.root.nodes({ filter: filterLongestPath })).to.have.length(1);
298
- expect(graph.findNode('test1')?.nodes({ filter: filterLongestPath })).to.have.length(1);
299
- expect(graph.findNode('test2')?.nodes({ filter: filterLongestPath })).to.be.empty;
472
+ graph._addEdges([{ source: 'test1', target: 'test2' }]);
300
473
 
301
- dispose();
302
- longestPaths.clear();
303
- });
474
+ expect(longestPaths.get('root')).to.deep.equal(['root']);
475
+ expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
476
+ expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
477
+ expect(graph.nodes(graph.root, { filter: filterLongestPath })).to.have.length(1);
478
+ expect(graph.nodes(graph.findNode('test1')!, { filter: filterLongestPath })).to.have.length(1);
479
+ expect(graph.nodes(graph.findNode('test2')!, { filter: filterLongestPath })).to.be.empty;
304
480
 
305
- test('get path', () => {
306
- const graph = new Graph();
481
+ dispose();
482
+ longestPaths.clear();
483
+ });
484
+
485
+ test('traversal can be terminated early', () => {
486
+ const graph = new Graph();
487
+
488
+ const [root] = graph._addNodes([
489
+ {
490
+ id: ROOT_ID,
491
+ type: ROOT_TYPE,
492
+ nodes: [
493
+ { id: 'test1', type: 'test' },
494
+ { id: 'test2', type: 'test' },
495
+ ],
496
+ },
497
+ ]);
307
498
 
308
- graph.addNodes({
309
- id: 'root',
310
- nodes: [{ id: 'test1' }, { id: 'test2' }],
499
+ const nodes: string[] = [];
500
+ graph.traverse({
501
+ node: root,
502
+ visitor: (node) => {
503
+ if (nodes.length === 2) {
504
+ return false;
505
+ }
506
+
507
+ nodes.push(node.id);
508
+ },
509
+ });
510
+ expect(nodes).to.deep.equal(['root', 'test1']);
311
511
  });
312
- graph.addEdge({ source: 'test1', target: 'test2' });
313
512
 
314
- expect(graph.getPath({ target: 'test2' })).to.deep.equal(['root', 'test1', 'test2']);
315
- expect(graph.getPath({ source: 'test1', target: 'test2' })).to.deep.equal(['test1', 'test2']);
316
- expect(graph.getPath({ source: 'test2', target: 'test1' })).to.be.undefined;
513
+ test('traversal can be reactive', async () => {
514
+ registerSignalRuntime();
515
+ const graph = new Graph();
516
+ const latest: Record<string, any> = {};
517
+ const updates: Record<string, number> = {};
518
+ graph.subscribeTraverse({
519
+ node: graph.root,
520
+ visitor: (node) => {
521
+ latest[node.id] = node.data;
522
+ updates[node.id] = (updates[node.id] ?? 0) + 1;
523
+ },
524
+ });
525
+
526
+ expect(latest.root).to.equal(null);
527
+ expect(updates.root).to.equal(1);
528
+
529
+ graph._addNodes([
530
+ {
531
+ id: ROOT_ID,
532
+ type: ROOT_TYPE,
533
+ nodes: [
534
+ {
535
+ id: 'test1',
536
+ type: 'test',
537
+ data: 1,
538
+ nodes: [{ id: 'test2', type: 'test', data: 2 }],
539
+ },
540
+ ],
541
+ },
542
+ ]);
543
+
544
+ expect(latest.root).to.equal(null);
545
+ expect(latest.test1).to.equal(1);
546
+ expect(latest.test2).to.equal(2);
547
+ expect(updates.root).to.equal(2);
548
+ expect(updates.test1).to.equal(1);
549
+ expect(updates.test2).to.equal(1);
550
+
551
+ graph._addNodes([{ id: 'test2', type: 'test', data: -2 }]);
552
+
553
+ expect(latest.root).to.equal(null);
554
+ expect(latest.test1).to.equal(1);
555
+ expect(latest.test2).to.equal(-2);
556
+ expect(updates.root).to.equal(2);
557
+ expect(updates.test1).to.equal(1);
558
+ expect(updates.test2).to.equal(2);
559
+
560
+ graph._addNodes([{ id: 'test1', type: 'test', data: -1 }]);
561
+
562
+ expect(latest.root).to.equal(null);
563
+ expect(latest.test1).to.equal(-1);
564
+ expect(latest.test2).to.equal(-2);
565
+ expect(updates.root).to.equal(2);
566
+ expect(updates.test1).to.equal(2);
567
+ expect(updates.test2).to.equal(3);
568
+ });
317
569
  });
318
570
  });