@dxos/app-graph 0.8.2-main.f11618f → 0.8.2-main.fbd8ed0

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 +541 -789
  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 +533 -780
  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 +541 -789
  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/experimental/graph-projections.test.d.ts +25 -0
  11. package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
  12. package/dist/types/src/graph-builder.d.ts +48 -91
  13. package/dist/types/src/graph-builder.d.ts.map +1 -1
  14. package/dist/types/src/graph.d.ts +191 -98
  15. package/dist/types/src/graph.d.ts.map +1 -1
  16. package/dist/types/src/node.d.ts +2 -2
  17. package/dist/types/src/node.d.ts.map +1 -1
  18. package/dist/types/src/signals-integration.test.d.ts +2 -0
  19. package/dist/types/src/signals-integration.test.d.ts.map +1 -0
  20. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  21. package/dist/types/src/testing.d.ts +5 -0
  22. package/dist/types/src/testing.d.ts.map +1 -0
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +23 -16
  25. package/src/experimental/graph-projections.test.ts +56 -0
  26. package/src/graph-builder.test.ts +293 -310
  27. package/src/graph-builder.ts +209 -317
  28. package/src/graph.test.ts +314 -463
  29. package/src/graph.ts +452 -458
  30. package/src/node.ts +2 -2
  31. package/src/signals-integration.test.ts +218 -0
  32. package/src/stories/EchoGraph.stories.tsx +56 -77
  33. package/src/testing.ts +20 -0
package/src/graph.test.ts CHANGED
@@ -2,241 +2,261 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { effect } from '@preact/signals-core';
6
- import { describe, expect, test } from 'vitest';
5
+ import { Registry, Rx } from '@effect-rx/rx-react';
6
+ import { Option } from 'effect';
7
+ import { assert, describe, expect, onTestFinished, test } from 'vitest';
7
8
 
8
- import { updateCounter } from '@dxos/echo-schema/testing';
9
- import { registerSignalsRuntime } from '@dxos/echo-signals';
9
+ import { getGraph, Graph, ROOT_ID, ROOT_TYPE } from './graph';
10
+ import { type Node } from './node';
10
11
 
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
- };
12
+ const exampleId = (id: number) => `dx:test:${id}`;
13
+ const EXAMPLE_ID = exampleId(1);
14
+ const EXAMPLE_TYPE = 'dxos.org/type/example';
30
15
 
31
16
  describe('Graph', () => {
32
17
  test('getGraph', () => {
33
- const graph = new Graph();
34
- expect(getGraph(graph.root)).to.equal(graph);
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);
35
25
  });
36
26
 
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);
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({});
59
37
  });
60
38
 
61
39
  test('add nodes updates existing nodes', () => {
62
- const graph = new Graph();
40
+ const registry = Registry.make();
41
+ const graph = new Graph({ registry });
42
+ const nodeKey = graph.node(EXAMPLE_ID);
63
43
 
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
- ]);
44
+ let count = 0;
45
+ const cancel = registry.subscribe(nodeKey, (_) => {
46
+ count++;
47
+ });
48
+ onTestFinished(() => cancel());
84
49
 
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
- });
50
+ expect(registry.get(nodeKey)).toEqual(Option.none());
51
+ expect(count).toEqual(1);
89
52
 
90
- test('remove node', () => {
91
- const graph = new Graph();
53
+ expect(registry.get(nodeKey)).toEqual(Option.none());
54
+ expect(count).toEqual(1);
92
55
 
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
- ]);
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);
103
64
 
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');
65
+ graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
66
+ expect(count).toEqual(2);
67
+ });
108
68
 
109
- graph._removeNodes(['test1']);
110
- expect(graph.findNode('test1')).to.be.undefined;
111
- expect(graph.nodes(root)).to.have.length(1);
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
+ }
112
89
  });
113
90
 
114
- test('re-add node', () => {
91
+ test('onNodeChanged', () => {
115
92
  const graph = new Graph();
116
93
 
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');
94
+ let node: Option.Option<Node> = Option.none();
95
+ graph.onNodeChanged.on(({ node: newNode }) => {
96
+ node = newNode;
97
+ });
128
98
 
129
- graph._removeNodes(['test1']);
130
- expect(graph.findNode('test1')).to.be.undefined;
131
- expect(graph.nodes(graph.root)).to.be.empty;
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);
132
103
 
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');
104
+ graph.removeNode(EXAMPLE_ID);
105
+ expect(node.pipe(Option.getOrNull)).toEqual(null);
143
106
  });
144
107
 
145
108
  test('add edge', () => {
146
- const graph = new Graph();
147
-
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' }]);
159
-
160
- expect(graph.nodes(graph.findNode('test1')!)).to.have.length(1);
161
- expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(2);
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)]);
162
115
  });
163
116
 
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' }]);
179
-
180
- expect(graph.nodes(graph.findNode('test1')!)).to.have.length(1);
181
- expect(graph.nodes(graph.findNode('test2')!, { relation: 'inbound' })).to.have.length(2);
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)]);
182
125
  });
183
126
 
184
127
  test('sort edges', () => {
185
- const graph = new Graph();
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
+ }
144
+ });
186
145
 
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
- ]);
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
+ });
199
164
 
200
- expect(graph.nodes(root).map((node) => node.id)).to.deep.equal(['test1', 'test3', 'test2', 'test4']);
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));
174
+ });
201
175
 
202
- graph._sortEdges('root', 'outbound', ['test4', 'test3']);
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));
203
180
 
204
- expect(graph.nodes(root).map((node) => node.id)).to.deep.equal(['test4', 'test3', 'test1', 'test2']);
205
- });
181
+ let node: Option.Option<Node> = Option.none();
182
+ const cancel = registry.subscribe(nodeKey, (n) => {
183
+ node = n;
184
+ });
185
+ onTestFinished(() => cancel());
206
186
 
207
- test('remove edge', () => {
208
- const graph = new Graph();
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));
191
+ });
209
192
 
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' }]);
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));
221
198
 
222
- expect(graph.nodes(graph.root)).to.have.length(1);
223
- expect(graph.nodes(graph.findNode('test1')!, { relation: 'inbound' })).to.be.empty;
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);
224
246
  });
225
247
 
226
248
  test('toJSON', () => {
227
249
  const graph = new Graph();
228
250
 
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' }]);
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' });
240
260
 
241
261
  const json = graph.toJSON();
242
262
  expect(json).to.deep.equal({
@@ -249,109 +269,87 @@ describe('Graph', () => {
249
269
  });
250
270
  });
251
271
 
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
- });
272
+ test('subscribe to json', () => {
273
+ const registry = Registry.make();
274
+ const graph = new Graph({ registry });
258
275
 
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
- });
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' });
267
285
 
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;
286
+ let json: any;
287
+ const cancel = registry.subscribe(graph.json(), (_) => {
288
+ json = _;
273
289
  });
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
- });
290
+ onTestFinished(() => cancel());
282
291
 
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;
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
+ ],
288
300
  });
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
- });
295
301
 
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);
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
+ ],
301
312
  });
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);
315
313
  });
316
314
 
317
315
  test('get path', () => {
318
316
  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) });
319
326
 
320
- graph._addNodes([
321
- {
322
- id: ROOT_ID,
323
- type: ROOT_TYPE,
324
- nodes: [
325
- { id: 'test1', type: 'test' },
326
- { id: 'test2', type: 'test' },
327
- ],
328
- },
327
+ expect(graph.getPath({ target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
328
+ 'root',
329
+ exampleId(1),
330
+ exampleId(2),
329
331
  ]);
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;
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;
335
337
  });
336
338
 
337
339
  describe('traverse', () => {
338
340
  test('can be traversed', () => {
339
341
  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
- ]);
342
+ graph.addNode({
343
+ id: ROOT_ID,
344
+ type: ROOT_TYPE,
345
+ nodes: [
346
+ { id: 'test1', type: 'test' },
347
+ { id: 'test2', type: 'test' },
348
+ ],
349
+ });
351
350
 
352
351
  const nodes: string[] = [];
353
352
  graph.traverse({
354
- node: root,
355
353
  visitor: (node) => {
356
354
  nodes.push(node.id);
357
355
  },
@@ -361,22 +359,18 @@ describe('Graph', () => {
361
359
 
362
360
  test('traversal breaks cycles', () => {
363
361
  const graph = new Graph();
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' }]);
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' });
376
371
 
377
372
  const nodes: string[] = [];
378
373
  graph.traverse({
379
- node: root,
380
374
  visitor: (node) => {
381
375
  nodes.push(node.id);
382
376
  },
@@ -386,24 +380,21 @@ describe('Graph', () => {
386
380
 
387
381
  test('traversal can be started from any node', () => {
388
382
  const graph = new Graph();
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
- ]);
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
+ });
403
394
 
404
395
  const nodes: string[] = [];
405
396
  graph.traverse({
406
- node: graph.findNode('test2')!,
397
+ source: 'test2',
407
398
  visitor: (node) => {
408
399
  nodes.push(node.id);
409
400
  },
@@ -413,24 +404,21 @@ describe('Graph', () => {
413
404
 
414
405
  test('traversal can follow inbound edges', () => {
415
406
  const graph = new Graph();
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
- ]);
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
+ });
430
418
 
431
419
  const nodes: string[] = [];
432
420
  graph.traverse({
433
- node: graph.findNode('test2')!,
421
+ source: 'test2',
434
422
  relation: 'inbound',
435
423
  visitor: (node) => {
436
424
  nodes.push(node.id);
@@ -439,100 +427,19 @@ describe('Graph', () => {
439
427
  expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
440
428
  });
441
429
 
442
- test('can filter to longest paths', () => {
443
- const graph = new Graph();
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
- });
497
- });
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
430
  test('traversal can be terminated early', () => {
520
431
  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
- ]);
432
+ graph.addNode({
433
+ id: ROOT_ID,
434
+ type: ROOT_TYPE,
435
+ nodes: [
436
+ { id: 'test1', type: 'test' },
437
+ { id: 'test2', type: 'test' },
438
+ ],
439
+ });
532
440
 
533
441
  const nodes: string[] = [];
534
442
  graph.traverse({
535
- node: root,
536
443
  visitor: (node) => {
537
444
  if (nodes.length === 2) {
538
445
  return false;
@@ -543,61 +450,5 @@ describe('Graph', () => {
543
450
  });
544
451
  expect(nodes).to.deep.equal(['root', 'test1']);
545
452
  });
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
- });
602
453
  });
603
454
  });