@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.
- package/dist/lib/browser/index.mjs +541 -789
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +533 -780
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +541 -789
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/experimental/graph-projections.test.d.ts +25 -0
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +48 -91
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +191 -98
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +2 -2
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/signals-integration.test.d.ts +2 -0
- package/dist/types/src/signals-integration.test.d.ts.map +1 -0
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +5 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -16
- package/src/experimental/graph-projections.test.ts +56 -0
- package/src/graph-builder.test.ts +293 -310
- package/src/graph-builder.ts +209 -317
- package/src/graph.test.ts +314 -463
- package/src/graph.ts +452 -458
- package/src/node.ts +2 -2
- package/src/signals-integration.test.ts +218 -0
- package/src/stories/EchoGraph.stories.tsx +56 -77
- 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 {
|
|
6
|
-
import {
|
|
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 {
|
|
9
|
-
import {
|
|
9
|
+
import { getGraph, Graph, ROOT_ID, ROOT_TYPE } from './graph';
|
|
10
|
+
import { type Node } from './node';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
34
|
-
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
40
|
+
const registry = Registry.make();
|
|
41
|
+
const graph = new Graph({ registry });
|
|
42
|
+
const nodeKey = graph.node(EXAMPLE_ID);
|
|
63
43
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
86
|
-
expect(
|
|
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
|
-
|
|
91
|
-
|
|
53
|
+
expect(registry.get(nodeKey)).toEqual(Option.none());
|
|
54
|
+
expect(count).toEqual(1);
|
|
92
55
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
expect(
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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('
|
|
91
|
+
test('onNodeChanged', () => {
|
|
115
92
|
const graph = new Graph();
|
|
116
93
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
130
|
-
|
|
131
|
-
expect(
|
|
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.
|
|
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
|
|
147
|
-
|
|
148
|
-
graph.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
graph.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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('
|
|
253
|
-
const
|
|
254
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
using updates = updateCounter(() => {
|
|
272
|
-
node1.data;
|
|
286
|
+
let json: any;
|
|
287
|
+
const cancel = registry.subscribe(graph.json(), (_) => {
|
|
288
|
+
json = _;
|
|
273
289
|
});
|
|
274
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
expect(graph.getPath({ source:
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
type:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
type:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
type:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
});
|