@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.
- package/dist/lib/browser/index.mjs +789 -541
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +780 -533
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +789 -541
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +91 -48
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +98 -191
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +3 -3
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -23
- package/src/graph-builder.test.ts +310 -293
- package/src/graph-builder.ts +317 -209
- package/src/graph.test.ts +463 -314
- package/src/graph.ts +455 -452
- package/src/node.ts +4 -4
- package/src/stories/EchoGraph.stories.tsx +78 -57
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -218
- 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 {
|
|
6
|
-
import {
|
|
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 {
|
|
10
|
-
import {
|
|
8
|
+
import { updateCounter } from '@dxos/echo-schema/testing';
|
|
9
|
+
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
19
|
-
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
graph.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
41
|
-
const graph = new Graph({ registry });
|
|
42
|
-
const nodeKey = graph.node(EXAMPLE_ID);
|
|
62
|
+
const graph = new Graph();
|
|
43
63
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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(
|
|
51
|
-
expect(
|
|
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
|
-
|
|
54
|
-
|
|
90
|
+
test('remove node', () => {
|
|
91
|
+
const graph = new Graph();
|
|
55
92
|
|
|
56
|
-
graph.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
expect(
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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('
|
|
114
|
+
test('re-add node', () => {
|
|
92
115
|
const graph = new Graph();
|
|
93
116
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
100
|
-
|
|
101
|
-
expect(
|
|
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.
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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('
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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('
|
|
177
|
-
const
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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('
|
|
194
|
-
const
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
expect(
|
|
212
|
-
expect(
|
|
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.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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('
|
|
273
|
-
const
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
303
|
-
graph
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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('
|
|
442
|
+
test('can filter to longest paths', () => {
|
|
431
443
|
const graph = new Graph();
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
});
|