@dxos/app-graph 0.8.3 → 0.8.4-main.03d5cd7b56

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 (73) hide show
  1. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  2. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  3. package/dist/lib/neutral/chunk-WJJ5KEOH.mjs +1477 -0
  4. package/dist/lib/neutral/chunk-WJJ5KEOH.mjs.map +7 -0
  5. package/dist/lib/neutral/index.mjs +40 -0
  6. package/dist/lib/neutral/index.mjs.map +7 -0
  7. package/dist/lib/neutral/meta.json +1 -0
  8. package/dist/lib/neutral/scheduler.mjs +15 -0
  9. package/dist/lib/neutral/scheduler.mjs.map +7 -0
  10. package/dist/lib/neutral/testing/index.mjs +40 -0
  11. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  12. package/dist/types/src/atoms.d.ts +8 -0
  13. package/dist/types/src/atoms.d.ts.map +1 -0
  14. package/dist/types/src/graph-builder.d.ts +117 -60
  15. package/dist/types/src/graph-builder.d.ts.map +1 -1
  16. package/dist/types/src/graph.d.ts +188 -218
  17. package/dist/types/src/graph.d.ts.map +1 -1
  18. package/dist/types/src/index.d.ts +7 -3
  19. package/dist/types/src/index.d.ts.map +1 -1
  20. package/dist/types/src/node-matcher.d.ts +244 -0
  21. package/dist/types/src/node-matcher.d.ts.map +1 -0
  22. package/dist/types/src/node-matcher.test.d.ts +2 -0
  23. package/dist/types/src/node-matcher.test.d.ts.map +1 -0
  24. package/dist/types/src/node.d.ts +50 -5
  25. package/dist/types/src/node.d.ts.map +1 -1
  26. package/dist/types/src/scheduler.browser.d.ts +2 -0
  27. package/dist/types/src/scheduler.browser.d.ts.map +1 -0
  28. package/dist/types/src/scheduler.d.ts +8 -0
  29. package/dist/types/src/scheduler.d.ts.map +1 -0
  30. package/dist/types/src/stories/EchoGraph.stories.d.ts +6 -13
  31. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  32. package/dist/types/src/testing/index.d.ts +2 -0
  33. package/dist/types/src/testing/index.d.ts.map +1 -0
  34. package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
  35. package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
  36. package/dist/types/src/util.d.ts +40 -0
  37. package/dist/types/src/util.d.ts.map +1 -0
  38. package/dist/types/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +53 -42
  40. package/src/atoms.ts +25 -0
  41. package/src/graph-builder.test.ts +1193 -126
  42. package/src/graph-builder.ts +753 -264
  43. package/src/graph.test.ts +451 -123
  44. package/src/graph.ts +1057 -407
  45. package/src/index.ts +10 -3
  46. package/src/node-matcher.test.ts +301 -0
  47. package/src/node-matcher.ts +314 -0
  48. package/src/node.ts +83 -7
  49. package/src/scheduler.browser.ts +5 -0
  50. package/src/scheduler.ts +17 -0
  51. package/src/stories/EchoGraph.stories.tsx +178 -255
  52. package/src/stories/Tree.tsx +1 -1
  53. package/src/testing/index.ts +5 -0
  54. package/src/testing/setup-graph-builder.ts +41 -0
  55. package/src/util.ts +101 -0
  56. package/dist/lib/browser/index.mjs +0 -778
  57. package/dist/lib/browser/index.mjs.map +0 -7
  58. package/dist/lib/browser/meta.json +0 -1
  59. package/dist/lib/node/index.cjs +0 -816
  60. package/dist/lib/node/index.cjs.map +0 -7
  61. package/dist/lib/node/meta.json +0 -1
  62. package/dist/lib/node-esm/index.mjs +0 -780
  63. package/dist/lib/node-esm/index.mjs.map +0 -7
  64. package/dist/lib/node-esm/meta.json +0 -1
  65. package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
  66. package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
  67. package/dist/types/src/signals-integration.test.d.ts +0 -2
  68. package/dist/types/src/signals-integration.test.d.ts.map +0 -1
  69. package/dist/types/src/testing.d.ts +0 -5
  70. package/dist/types/src/testing.d.ts.map +0 -1
  71. package/src/experimental/graph-projections.test.ts +0 -56
  72. package/src/signals-integration.test.ts +0 -218
  73. package/src/testing.ts +0 -20
package/src/graph.test.ts CHANGED
@@ -2,32 +2,37 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Registry, Rx } from '@effect-rx/rx-react';
6
- import { Option } from 'effect';
5
+ import { Atom, Registry } from '@effect-atom/atom-react';
6
+ import * as Option from 'effect/Option';
7
7
  import { assert, describe, expect, onTestFinished, test } from 'vitest';
8
8
 
9
- import { getGraph, Graph, ROOT_ID, ROOT_TYPE } from './graph';
10
- import { type Node } from './node';
9
+ import * as Graph from './graph';
10
+ import * as GraphBuilder from './graph-builder';
11
+ import * as Node from './node';
11
12
 
12
13
  const exampleId = (id: number) => `dx:test:${id}`;
13
14
  const EXAMPLE_ID = exampleId(1);
14
- const EXAMPLE_TYPE = 'dxos.org/type/example';
15
+ const EXAMPLE_TYPE = 'org.dxos.type.example';
16
+ const CHILD_RELATION_KEY = Graph.relationKey('child');
17
+ const CHILD_INBOUND_RELATION_KEY = Graph.relationKey(Node.childRelation('inbound'));
18
+ const ACTIONS_RELATION_KEY = Graph.relationKey('action');
19
+ const ACTIONS_INBOUND_RELATION_KEY = Graph.relationKey(Node.actionRelation('inbound'));
15
20
 
16
21
  describe('Graph', () => {
17
22
  test('getGraph', () => {
18
23
  const registry = Registry.make();
19
- const graph = new Graph({ registry });
20
- const root = registry.get(graph.node(ROOT_ID));
24
+ const graph = Graph.make({ registry });
25
+ const root = registry.get(graph.node(Node.RootId));
21
26
  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);
27
+ expect(root.value.id).toEqual(Node.RootId);
28
+ expect(root.value.type).toEqual(Node.RootType);
29
+ expect(Graph.getGraph(root.value)).toEqual(graph);
25
30
  });
26
31
 
27
32
  test('add node', () => {
28
33
  const registry = Registry.make();
29
- const graph = new Graph({ registry });
30
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
34
+ const graph = Graph.make({ registry });
35
+ Graph.addNode(graph, { id: EXAMPLE_ID, type: EXAMPLE_TYPE });
31
36
  const node = registry.get(graph.node(EXAMPLE_ID));
32
37
  assert.ok(Option.isSome(node));
33
38
  expect(node.value.id).toEqual(EXAMPLE_ID);
@@ -36,9 +41,20 @@ describe('Graph', () => {
36
41
  expect(node.value.properties).toEqual({});
37
42
  });
38
43
 
44
+ test('add node curried', () => {
45
+ const registry = Registry.make();
46
+ const graph = Graph.make({ registry });
47
+ const result = graph.pipe(Graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE }));
48
+ expect(result).toEqual(graph);
49
+ const node = registry.get(graph.node(EXAMPLE_ID));
50
+ assert.ok(Option.isSome(node));
51
+ expect(node.value.id).toEqual(EXAMPLE_ID);
52
+ expect(node.value.type).toEqual(EXAMPLE_TYPE);
53
+ });
54
+
39
55
  test('add nodes updates existing nodes', () => {
40
56
  const registry = Registry.make();
41
- const graph = new Graph({ registry });
57
+ const graph = Graph.make({ registry });
42
58
  const nodeKey = graph.node(EXAMPLE_ID);
43
59
 
44
60
  let count = 0;
@@ -53,7 +69,7 @@ describe('Graph', () => {
53
69
  expect(registry.get(nodeKey)).toEqual(Option.none());
54
70
  expect(count).toEqual(1);
55
71
 
56
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
72
+ Graph.addNode(graph, { id: EXAMPLE_ID, type: EXAMPLE_TYPE });
57
73
  const node = registry.get(nodeKey);
58
74
  assert.ok(Option.isSome(node));
59
75
  expect(node.value.id).toEqual(EXAMPLE_ID);
@@ -62,13 +78,13 @@ describe('Graph', () => {
62
78
  expect(node.value.properties).toEqual({});
63
79
  expect(count).toEqual(2);
64
80
 
65
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
81
+ Graph.addNode(graph, { id: EXAMPLE_ID, type: EXAMPLE_TYPE });
66
82
  expect(count).toEqual(2);
67
83
  });
68
84
 
69
85
  test('remove node', () => {
70
86
  const registry = Registry.make();
71
- const graph = new Graph({ registry });
87
+ const graph = Graph.make({ registry });
72
88
 
73
89
  {
74
90
  const node = registry.get(graph.node(EXAMPLE_ID));
@@ -76,125 +92,220 @@ describe('Graph', () => {
76
92
  }
77
93
 
78
94
  {
79
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
95
+ Graph.addNode(graph, { id: EXAMPLE_ID, type: EXAMPLE_TYPE });
80
96
  const node = registry.get(graph.node(EXAMPLE_ID));
81
97
  expect(Option.isSome(node)).toEqual(true);
82
98
  }
83
99
 
84
100
  {
85
- graph.removeNode(EXAMPLE_ID);
101
+ Graph.removeNode(graph, EXAMPLE_ID);
86
102
  const node = registry.get(graph.node(EXAMPLE_ID));
87
103
  expect(Option.isNone(node)).toEqual(true);
88
104
  }
89
105
  });
90
106
 
107
+ test('remove node with edges=true removes default inbound counterparts', () => {
108
+ const registry = Registry.make();
109
+ const graph = Graph.make({ registry });
110
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
111
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
112
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
113
+
114
+ Graph.removeNode(graph, exampleId(2), true);
115
+
116
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
117
+ expect(sourceEdges[CHILD_RELATION_KEY]).toEqual([]);
118
+ expect(sourceEdges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
119
+ });
120
+
121
+ test('remove node with edges=true removes typed inbound and outbound counterparts', () => {
122
+ const registry = Registry.make();
123
+ const graph = Graph.make({ registry });
124
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
125
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
126
+ Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
127
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
128
+ Graph.addEdge(graph, { source: exampleId(2), target: exampleId(3), relation: 'action' });
129
+
130
+ Graph.removeNode(graph, exampleId(2), true);
131
+
132
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
133
+ const targetEdges = registry.get(graph.edges(exampleId(3)));
134
+ expect(sourceEdges[ACTIONS_RELATION_KEY]).toEqual([]);
135
+ expect(sourceEdges[ACTIONS_INBOUND_RELATION_KEY]).toBeUndefined();
136
+ expect(targetEdges[ACTIONS_RELATION_KEY]).toBeUndefined();
137
+ expect(targetEdges[ACTIONS_INBOUND_RELATION_KEY]).toEqual([]);
138
+ });
139
+
140
+ test('remove node curried', () => {
141
+ const registry = Registry.make();
142
+ const graph = Graph.make({ registry });
143
+ Graph.addNode(graph, { id: EXAMPLE_ID, type: EXAMPLE_TYPE });
144
+ const result = graph.pipe(Graph.removeNode(EXAMPLE_ID));
145
+ expect(result).toEqual(graph);
146
+ const node = registry.get(graph.node(EXAMPLE_ID));
147
+ assert.ok(Option.isNone(node));
148
+ });
149
+
91
150
  test('onNodeChanged', () => {
92
- const graph = new Graph();
151
+ const graph = Graph.make();
93
152
 
94
- let node: Option.Option<Node> = Option.none();
153
+ let node: Option.Option<Node.Node> = Option.none();
95
154
  graph.onNodeChanged.on(({ node: newNode }) => {
96
155
  node = newNode;
97
156
  });
98
157
 
99
- graph.addNode({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
158
+ Graph.addNode(graph, { id: EXAMPLE_ID, type: EXAMPLE_TYPE });
100
159
  assert.ok(Option.isSome(node));
101
160
  expect(node.value.id).toEqual(EXAMPLE_ID);
102
161
  expect(node.value.type).toEqual(EXAMPLE_TYPE);
103
162
 
104
- graph.removeNode(EXAMPLE_ID);
163
+ Graph.removeNode(graph, EXAMPLE_ID);
105
164
  expect(node.pipe(Option.getOrNull)).toEqual(null);
106
165
  });
107
166
 
108
167
  test('add edge', () => {
109
168
  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)]);
169
+ const graph = Graph.make({ registry });
170
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
171
+
172
+ {
173
+ const edges = registry.get(graph.edges(exampleId(1)));
174
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
175
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2)]);
176
+ }
177
+
178
+ {
179
+ const edges = registry.get(graph.edges(exampleId(2)));
180
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toEqual([exampleId(1)]);
181
+ expect(edges[CHILD_RELATION_KEY]).toBeUndefined();
182
+ }
115
183
  });
116
184
 
117
185
  test('add edges is idempotent', () => {
118
186
  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) });
187
+ const graph = Graph.make({ registry });
188
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
189
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
122
190
  const edges = registry.get(graph.edges(exampleId(1)));
123
- expect(edges.inbound).toEqual([]);
124
- expect(edges.outbound).toEqual([exampleId(2)]);
191
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
192
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2)]);
125
193
  });
126
194
 
127
195
  test('sort edges', () => {
128
196
  const registry = Registry.make();
129
- const graph = new Graph({ registry });
197
+ const graph = Graph.make({ registry });
130
198
 
131
199
  {
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) });
200
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
201
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3), relation: 'child' });
202
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(4), relation: 'child' });
135
203
  const edges = registry.get(graph.edges(exampleId(1)));
136
- expect(edges.outbound).toEqual([exampleId(2), exampleId(3), exampleId(4)]);
204
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2), exampleId(3), exampleId(4)]);
137
205
  }
138
206
 
139
207
  {
140
- graph.sortEdges(exampleId(1), 'outbound', [exampleId(3), exampleId(2)]);
208
+ Graph.sortEdges(graph, exampleId(1), 'child', [exampleId(3), exampleId(2)]);
141
209
  const edges = registry.get(graph.edges(exampleId(1)));
142
- expect(edges.outbound).toEqual([exampleId(3), exampleId(2), exampleId(4)]);
210
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(3), exampleId(2), exampleId(4)]);
143
211
  }
144
212
  });
145
213
 
146
214
  test('remove edge', () => {
147
215
  const registry = Registry.make();
148
- const graph = new Graph({ registry });
216
+ const graph = Graph.make({ registry });
149
217
 
150
218
  {
151
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
219
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
152
220
  const edges = registry.get(graph.edges(exampleId(1)));
153
- expect(edges.inbound).toEqual([]);
154
- expect(edges.outbound).toEqual([exampleId(2)]);
221
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
222
+ expect(edges[CHILD_RELATION_KEY]).toEqual([exampleId(2)]);
155
223
  }
156
224
 
157
225
  {
158
- graph.removeEdge({ source: exampleId(1), target: exampleId(2) });
226
+ Graph.removeEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
159
227
  const edges = registry.get(graph.edges(exampleId(1)));
160
- expect(edges.inbound).toEqual([]);
161
- expect(edges.outbound).toEqual([]);
228
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
229
+ expect(edges[CHILD_RELATION_KEY]).toEqual([]);
162
230
  }
163
231
  });
164
232
 
233
+ test('remove edge curried', () => {
234
+ const registry = Registry.make();
235
+ const graph = Graph.make({ registry });
236
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
237
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
238
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
239
+ const result = graph.pipe(Graph.removeEdge({ source: exampleId(1), target: exampleId(2), relation: 'child' }));
240
+ expect(result).toEqual(graph);
241
+ const edges = registry.get(graph.edges(exampleId(1)));
242
+ expect(edges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
243
+ expect(edges[CHILD_RELATION_KEY]).toEqual([]);
244
+ });
245
+
246
+ test('add edge with custom relation creates typed inbound inverse', () => {
247
+ const registry = Registry.make();
248
+ const graph = Graph.make({ registry });
249
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
250
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
251
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
252
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
253
+ expect(sourceEdges[ACTIONS_RELATION_KEY]).toEqual([exampleId(2)]);
254
+ expect(sourceEdges[CHILD_RELATION_KEY]).toBeUndefined();
255
+ const targetEdges = registry.get(graph.edges(exampleId(2)));
256
+ expect(targetEdges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
257
+ expect(targetEdges[ACTIONS_INBOUND_RELATION_KEY]).toEqual([exampleId(1)]);
258
+ const reverseConnections = registry.get(graph.connections(exampleId(2), Node.actionRelation('inbound')));
259
+ expect(reverseConnections.map(({ id }) => id)).toEqual([exampleId(1)]);
260
+ });
261
+
262
+ test('remove edge with custom relation removes typed inbound inverse', () => {
263
+ const registry = Registry.make();
264
+ const graph = Graph.make({ registry });
265
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
266
+ Graph.removeEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'action' });
267
+ const sourceEdges = registry.get(graph.edges(exampleId(1)));
268
+ expect(sourceEdges[ACTIONS_RELATION_KEY]).toEqual([]);
269
+ const targetEdges = registry.get(graph.edges(exampleId(2)));
270
+ expect(targetEdges[CHILD_INBOUND_RELATION_KEY]).toBeUndefined();
271
+ expect(targetEdges[CHILD_RELATION_KEY]).toBeUndefined();
272
+ expect(targetEdges[ACTIONS_RELATION_KEY]).toBeUndefined();
273
+ expect(targetEdges[ACTIONS_INBOUND_RELATION_KEY]).toEqual([]);
274
+ });
275
+
165
276
  test('get connections', () => {
166
277
  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)));
278
+ const graph = Graph.make({ registry });
279
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
280
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
281
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
282
+ const nodes = registry.get(graph.connections(exampleId(1), 'child'));
172
283
  expect(nodes).has.length(1);
173
284
  expect(nodes[0].id).toEqual(exampleId(2));
174
285
  });
175
286
 
176
287
  test('can subscribe to a node before it exists', async () => {
177
288
  const registry = Registry.make();
178
- const graph = new Graph({ registry });
289
+ const graph = Graph.make({ registry });
179
290
  const nodeKey = graph.node(exampleId(1));
180
291
 
181
- let node: Option.Option<Node> = Option.none();
292
+ let node: Option.Option<Node.Node> = Option.none();
182
293
  const cancel = registry.subscribe(nodeKey, (n) => {
183
294
  node = n;
184
295
  });
185
296
  onTestFinished(() => cancel());
186
297
 
187
298
  expect(node).toEqual(Option.none());
188
- graph.addNode({ id: exampleId(1), type: EXAMPLE_TYPE });
299
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
189
300
  assert.ok(Option.isSome(node));
190
301
  expect(node.value.id).toEqual(exampleId(1));
191
302
  });
192
303
 
193
304
  test('connections updates', () => {
194
305
  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));
306
+ const graph = Graph.make({ registry });
307
+ assert.strictEqual(graph.connections(exampleId(1), 'child'), graph.connections(exampleId(1), 'child'));
308
+ const childrenKey = graph.connections(exampleId(1), 'child');
198
309
 
199
310
  let count = 0;
200
311
  const cancel = registry.subscribe(childrenKey, (_) => {
@@ -202,9 +313,11 @@ describe('Graph', () => {
202
313
  });
203
314
  onTestFinished(() => cancel());
204
315
 
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) });
316
+ graph.pipe(
317
+ Graph.addNode({ id: exampleId(1), type: EXAMPLE_TYPE }),
318
+ Graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE }),
319
+ Graph.addEdge({ source: exampleId(1), target: exampleId(2), relation: 'child' }),
320
+ );
208
321
 
209
322
  expect(count).toEqual(0);
210
323
  const children = registry.get(childrenKey);
@@ -213,55 +326,57 @@ describe('Graph', () => {
213
326
  expect(count).toEqual(1);
214
327
 
215
328
  // Updating an existing node fires an update.
216
- graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE, data: 'updated' });
329
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE, data: 'updated' });
217
330
  expect(count).toEqual(2);
218
331
 
219
332
  // Adding a node with no changes does not fire an update.
220
- graph.addNode({ id: exampleId(2), type: EXAMPLE_TYPE, data: 'updated' });
333
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE, data: 'updated' });
221
334
  expect(count).toEqual(2);
222
335
 
223
336
  // Adding an unconnected node does not fire an update.
224
- graph.addNode({ id: exampleId(3), type: EXAMPLE_TYPE });
337
+ Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
225
338
  expect(count).toEqual(2);
226
339
 
227
340
  // Connecting a node fires an update.
228
- graph.addEdge({ source: exampleId(1), target: exampleId(3) });
341
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3), relation: 'child' });
229
342
  expect(count).toEqual(3);
230
343
 
231
344
  // Adding an edge connected to nothing fires an update.
232
345
  // TODO(wittjosiah): Is there a way to avoid this?
233
- graph.addEdge({ source: exampleId(1), target: exampleId(4) });
346
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(4), relation: 'child' });
234
347
  expect(count).toEqual(4);
235
348
 
236
349
  // Adding a node to an existing edge fires an update.
237
- graph.addNode({ id: exampleId(4), type: EXAMPLE_TYPE });
350
+ Graph.addNode(graph, { id: exampleId(4), type: EXAMPLE_TYPE });
238
351
  expect(count).toEqual(5);
239
352
 
240
353
  // 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 });
354
+ Atom.batch(() => {
355
+ graph.pipe(
356
+ Graph.addEdge({ source: exampleId(1), target: exampleId(6), relation: 'child' }),
357
+ Graph.addNode({ id: exampleId(6), type: EXAMPLE_TYPE }),
358
+ );
244
359
  });
245
360
  expect(count).toEqual(6);
246
361
  });
247
362
 
248
363
  test('toJSON', () => {
249
- const graph = new Graph();
364
+ const graph = Graph.make();
250
365
 
251
- graph.addNode({
252
- id: ROOT_ID,
253
- type: ROOT_TYPE,
366
+ Graph.addNode(graph, {
367
+ id: Node.RootId,
368
+ type: Node.RootType,
254
369
  nodes: [
255
370
  { id: 'test1', type: 'test' },
256
371
  { id: 'test2', type: 'test' },
257
372
  ],
258
373
  });
259
- graph.addEdge({ source: 'test1', target: 'test2' });
374
+ Graph.addEdge(graph, { source: 'test1', target: 'test2', relation: 'child' });
260
375
 
261
- const json = graph.toJSON();
376
+ const json = Graph.toJSON(graph);
262
377
  expect(json).to.deep.equal({
263
- id: ROOT_ID,
264
- type: ROOT_TYPE,
378
+ id: Node.RootId,
379
+ type: Node.RootType,
265
380
  nodes: [
266
381
  { id: 'test1', type: 'test', nodes: [{ id: 'test2', type: 'test' }] },
267
382
  { id: 'test2', type: 'test' },
@@ -271,17 +386,17 @@ describe('Graph', () => {
271
386
 
272
387
  test('subscribe to json', () => {
273
388
  const registry = Registry.make();
274
- const graph = new Graph({ registry });
389
+ const graph = Graph.make({ registry });
275
390
 
276
- graph.addNode({
277
- id: ROOT_ID,
278
- type: ROOT_TYPE,
391
+ Graph.addNode(graph, {
392
+ id: Node.RootId,
393
+ type: Node.RootType,
279
394
  nodes: [
280
395
  { id: 'test1', type: 'test' },
281
396
  { id: 'test2', type: 'test' },
282
397
  ],
283
398
  });
284
- graph.addEdge({ source: 'test1', target: 'test2' });
399
+ Graph.addEdge(graph, { source: 'test1', target: 'test2', relation: 'child' });
285
400
 
286
401
  let json: any;
287
402
  const cancel = registry.subscribe(graph.json(), (_) => {
@@ -291,19 +406,19 @@ describe('Graph', () => {
291
406
 
292
407
  registry.get(graph.json());
293
408
  expect(json).to.deep.equal({
294
- id: ROOT_ID,
295
- type: ROOT_TYPE,
409
+ id: Node.RootId,
410
+ type: Node.RootType,
296
411
  nodes: [
297
412
  { id: 'test1', type: 'test', nodes: [{ id: 'test2', type: 'test' }] },
298
413
  { id: 'test2', type: 'test' },
299
414
  ],
300
415
  });
301
416
 
302
- graph.addNode({ id: 'test3', type: 'test' });
303
- graph.addEdge({ source: 'root', target: 'test3' });
417
+ Graph.addNode(graph, { id: 'test3', type: 'test' });
418
+ Graph.addEdge(graph, { source: 'root', target: 'test3', relation: 'child' });
304
419
  expect(json).to.deep.equal({
305
- id: ROOT_ID,
306
- type: ROOT_TYPE,
420
+ id: Node.RootId,
421
+ type: Node.RootType,
307
422
  nodes: [
308
423
  { id: 'test1', type: 'test', nodes: [{ id: 'test2', type: 'test' }] },
309
424
  { id: 'test2', type: 'test' },
@@ -313,35 +428,50 @@ describe('Graph', () => {
313
428
  });
314
429
 
315
430
  test('get path', () => {
316
- const graph = new Graph();
317
- graph.addNode({
318
- id: ROOT_ID,
319
- type: ROOT_TYPE,
431
+ const graph = Graph.make();
432
+ Graph.addNode(graph, {
433
+ id: Node.RootId,
434
+ type: Node.RootType,
320
435
  nodes: [
321
436
  { id: exampleId(1), type: EXAMPLE_TYPE },
322
437
  { id: exampleId(2), type: EXAMPLE_TYPE },
323
438
  ],
324
439
  });
325
- graph.addEdge({ source: exampleId(1), target: exampleId(2) });
440
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
326
441
 
327
- expect(graph.getPath({ target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
442
+ expect(Graph.getPath(graph, { target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
328
443
  'root',
329
444
  exampleId(1),
330
445
  exampleId(2),
331
446
  ]);
332
- expect(graph.getPath({ source: exampleId(1), target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
447
+ expect(Graph.getPath(graph, { source: exampleId(1), target: exampleId(2) }).pipe(Option.getOrNull)).to.deep.equal([
333
448
  exampleId(1),
334
449
  exampleId(2),
335
450
  ]);
336
- expect(graph.getPath({ source: exampleId(2), target: exampleId(1) }).pipe(Option.getOrNull)).to.be.null;
451
+ expect(Graph.getPath(graph, { source: exampleId(2), target: exampleId(1) }).pipe(Option.getOrNull)).to.be.null;
452
+ });
453
+
454
+ test('get path curried', () => {
455
+ const graph = Graph.make();
456
+ Graph.addNode(graph, {
457
+ id: Node.RootId,
458
+ type: Node.RootType,
459
+ nodes: [
460
+ { id: exampleId(1), type: EXAMPLE_TYPE },
461
+ { id: exampleId(2), type: EXAMPLE_TYPE },
462
+ ],
463
+ });
464
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
465
+ const path = graph.pipe(Graph.getPath({ target: exampleId(2) }));
466
+ expect(path.pipe(Option.getOrNull)).to.deep.equal(['root', exampleId(1), exampleId(2)]);
337
467
  });
338
468
 
339
469
  describe('traverse', () => {
340
470
  test('can be traversed', () => {
341
- const graph = new Graph();
342
- graph.addNode({
343
- id: ROOT_ID,
344
- type: ROOT_TYPE,
471
+ const graph = Graph.make();
472
+ Graph.addNode(graph, {
473
+ id: Node.RootId,
474
+ type: Node.RootType,
345
475
  nodes: [
346
476
  { id: 'test1', type: 'test' },
347
477
  { id: 'test2', type: 'test' },
@@ -349,7 +479,8 @@ describe('Graph', () => {
349
479
  });
350
480
 
351
481
  const nodes: string[] = [];
352
- graph.traverse({
482
+ Graph.traverse(graph, {
483
+ relation: 'child',
353
484
  visitor: (node) => {
354
485
  nodes.push(node.id);
355
486
  },
@@ -358,19 +489,20 @@ describe('Graph', () => {
358
489
  });
359
490
 
360
491
  test('traversal breaks cycles', () => {
361
- const graph = new Graph();
362
- graph.addNode({
363
- id: ROOT_ID,
364
- type: ROOT_TYPE,
492
+ const graph = Graph.make();
493
+ Graph.addNode(graph, {
494
+ id: Node.RootId,
495
+ type: Node.RootType,
365
496
  nodes: [
366
497
  { id: 'test1', type: 'test' },
367
498
  { id: 'test2', type: 'test' },
368
499
  ],
369
500
  });
370
- graph.addEdge({ source: 'test1', target: 'root' });
501
+ Graph.addEdge(graph, { source: 'test1', target: 'root', relation: 'child' });
371
502
 
372
503
  const nodes: string[] = [];
373
- graph.traverse({
504
+ Graph.traverse(graph, {
505
+ relation: 'child',
374
506
  visitor: (node) => {
375
507
  nodes.push(node.id);
376
508
  },
@@ -379,10 +511,10 @@ describe('Graph', () => {
379
511
  });
380
512
 
381
513
  test('traversal can be started from any node', () => {
382
- const graph = new Graph();
383
- graph.addNode({
384
- id: ROOT_ID,
385
- type: ROOT_TYPE,
514
+ const graph = Graph.make();
515
+ Graph.addNode(graph, {
516
+ id: Node.RootId,
517
+ type: Node.RootType,
386
518
  nodes: [
387
519
  {
388
520
  id: 'test1',
@@ -393,8 +525,9 @@ describe('Graph', () => {
393
525
  });
394
526
 
395
527
  const nodes: string[] = [];
396
- graph.traverse({
528
+ Graph.traverse(graph, {
397
529
  source: 'test2',
530
+ relation: 'child',
398
531
  visitor: (node) => {
399
532
  nodes.push(node.id);
400
533
  },
@@ -403,10 +536,10 @@ describe('Graph', () => {
403
536
  });
404
537
 
405
538
  test('traversal can follow inbound edges', () => {
406
- const graph = new Graph();
407
- graph.addNode({
408
- id: ROOT_ID,
409
- type: ROOT_TYPE,
539
+ const graph = Graph.make();
540
+ Graph.addNode(graph, {
541
+ id: Node.RootId,
542
+ type: Node.RootType,
410
543
  nodes: [
411
544
  {
412
545
  id: 'test1',
@@ -417,9 +550,9 @@ describe('Graph', () => {
417
550
  });
418
551
 
419
552
  const nodes: string[] = [];
420
- graph.traverse({
553
+ Graph.traverse(graph, {
421
554
  source: 'test2',
422
- relation: 'inbound',
555
+ relation: Node.childRelation('inbound'),
423
556
  visitor: (node) => {
424
557
  nodes.push(node.id);
425
558
  },
@@ -427,11 +560,28 @@ describe('Graph', () => {
427
560
  expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
428
561
  });
429
562
 
563
+ test('traversal can follow typed inbound edges', () => {
564
+ const graph = Graph.make();
565
+ Graph.addNode(graph, { id: 'host', type: 'test' });
566
+ Graph.addNode(graph, { id: 'action', type: 'test' });
567
+ Graph.addEdge(graph, { source: 'host', target: 'action', relation: 'action' });
568
+
569
+ const nodes: string[] = [];
570
+ Graph.traverse(graph, {
571
+ source: 'action',
572
+ relation: Node.actionRelation('inbound'),
573
+ visitor: (node) => {
574
+ nodes.push(node.id);
575
+ },
576
+ });
577
+ expect(nodes).to.deep.equal(['action', 'host']);
578
+ });
579
+
430
580
  test('traversal can be terminated early', () => {
431
- const graph = new Graph();
432
- graph.addNode({
433
- id: ROOT_ID,
434
- type: ROOT_TYPE,
581
+ const graph = Graph.make();
582
+ Graph.addNode(graph, {
583
+ id: Node.RootId,
584
+ type: Node.RootType,
435
585
  nodes: [
436
586
  { id: 'test1', type: 'test' },
437
587
  { id: 'test2', type: 'test' },
@@ -439,7 +589,8 @@ describe('Graph', () => {
439
589
  });
440
590
 
441
591
  const nodes: string[] = [];
442
- graph.traverse({
592
+ Graph.traverse(graph, {
593
+ relation: 'child',
443
594
  visitor: (node) => {
444
595
  if (nodes.length === 2) {
445
596
  return false;
@@ -450,5 +601,182 @@ describe('Graph', () => {
450
601
  });
451
602
  expect(nodes).to.deep.equal(['root', 'test1']);
452
603
  });
604
+
605
+ test('traversal with multiple relations follows all edge types', () => {
606
+ const graph = Graph.make();
607
+ Graph.addNode(graph, {
608
+ id: Node.RootId,
609
+ type: Node.RootType,
610
+ nodes: [{ id: 'child1', type: 'test' }],
611
+ });
612
+ Graph.addNode(graph, { id: 'action1', type: Node.ActionType });
613
+ Graph.addEdge(graph, { source: Node.RootId, target: 'action1', relation: 'action' });
614
+
615
+ const nodes: string[] = [];
616
+ Graph.traverse(graph, {
617
+ relation: ['child', 'action'],
618
+ visitor: (node) => {
619
+ nodes.push(node.id);
620
+ },
621
+ });
622
+ expect(nodes).to.deep.equal(['root', 'child1', 'action1']);
623
+ });
624
+
625
+ test('traverse curried', () => {
626
+ const graph = Graph.make();
627
+ Graph.addNode(graph, {
628
+ id: Node.RootId,
629
+ type: Node.RootType,
630
+ nodes: [{ id: 'test1', type: 'test' }],
631
+ });
632
+ Graph.addNode(graph, { id: 'test2', type: 'test' });
633
+ Graph.addEdge(graph, { source: 'test1', target: 'test2', relation: 'child' });
634
+ const nodes: string[] = [];
635
+ graph.pipe(
636
+ Graph.traverse({
637
+ source: Node.RootId,
638
+ relation: 'child',
639
+ visitor: (node, _path) => {
640
+ nodes.push(node.id);
641
+ },
642
+ }),
643
+ );
644
+ expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
645
+ });
646
+ });
647
+
648
+ test('add nodes curried', () => {
649
+ const registry = Registry.make();
650
+ const graph = Graph.make({ registry });
651
+ const result = graph.pipe(
652
+ Graph.addNodes([
653
+ { id: exampleId(1), type: EXAMPLE_TYPE },
654
+ { id: exampleId(2), type: EXAMPLE_TYPE },
655
+ ]),
656
+ );
657
+ expect(result).toEqual(graph);
658
+ const node1 = registry.get(graph.node(exampleId(1)));
659
+ const node2 = registry.get(graph.node(exampleId(2)));
660
+ assert.ok(Option.isSome(node1));
661
+ assert.ok(Option.isSome(node2));
662
+ expect(node1.value.id).toEqual(exampleId(1));
663
+ expect(node2.value.id).toEqual(exampleId(2));
664
+ });
665
+
666
+ test('add edges curried', () => {
667
+ const registry = Registry.make();
668
+ const graph = Graph.make({ registry });
669
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
670
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
671
+ Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
672
+ const result = graph.pipe(
673
+ Graph.addEdges([
674
+ { source: exampleId(1), target: exampleId(2), relation: 'child' },
675
+ { source: exampleId(1), target: exampleId(3), relation: 'child' },
676
+ ]),
677
+ );
678
+ expect(result).toEqual(graph);
679
+ const edges = registry.get(graph.edges(exampleId(1)));
680
+ expect(edges[CHILD_RELATION_KEY]).to.have.length(2);
681
+ expect(edges[CHILD_RELATION_KEY]).to.include(exampleId(2));
682
+ expect(edges[CHILD_RELATION_KEY]).to.include(exampleId(3));
683
+ });
684
+
685
+ test('remove nodes curried', () => {
686
+ const registry = Registry.make();
687
+ const graph = Graph.make({ registry });
688
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
689
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
690
+ const result = graph.pipe(Graph.removeNodes([exampleId(1), exampleId(2)]));
691
+ expect(result).toEqual(graph);
692
+ const node1 = registry.get(graph.node(exampleId(1)));
693
+ const node2 = registry.get(graph.node(exampleId(2)));
694
+ assert.ok(Option.isNone(node1));
695
+ assert.ok(Option.isNone(node2));
696
+ });
697
+
698
+ test('remove edges curried', () => {
699
+ const registry = Registry.make();
700
+ const graph = Graph.make({ registry });
701
+ Graph.addNode(graph, { id: exampleId(1), type: EXAMPLE_TYPE });
702
+ Graph.addNode(graph, { id: exampleId(2), type: EXAMPLE_TYPE });
703
+ Graph.addNode(graph, { id: exampleId(3), type: EXAMPLE_TYPE });
704
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(2), relation: 'child' });
705
+ Graph.addEdge(graph, { source: exampleId(1), target: exampleId(3), relation: 'child' });
706
+ const result = graph.pipe(
707
+ Graph.removeEdges([
708
+ { source: exampleId(1), target: exampleId(2), relation: 'child' },
709
+ { source: exampleId(1), target: exampleId(3), relation: 'child' },
710
+ ]),
711
+ );
712
+ expect(result).toEqual(graph);
713
+ const edges = registry.get(graph.edges(exampleId(1)));
714
+ expect(edges[CHILD_RELATION_KEY]).to.have.length(0);
715
+ });
716
+
717
+ test('expand curried', async () => {
718
+ const registry = Registry.make();
719
+ const builder = GraphBuilder.make({ registry });
720
+ const graph = builder.graph;
721
+ let expandCalled = false;
722
+ GraphBuilder.addExtension(
723
+ builder,
724
+ GraphBuilder.createExtensionRaw({
725
+ id: 'test',
726
+ connector: () => {
727
+ expandCalled = true;
728
+ return Atom.make([]);
729
+ },
730
+ }),
731
+ );
732
+ await graph.pipe(Graph.expand(Node.RootId, 'child'));
733
+ expect(expandCalled).to.be.true;
734
+ });
735
+
736
+ test('expand defers for non-existent node and applies when node is added', () => {
737
+ const registry = Registry.make();
738
+ const expandCalls: [string, Node.Relation][] = [];
739
+ const graph = Graph.make({
740
+ registry,
741
+ onExpand: (id, relation) => expandCalls.push([id, relation]),
742
+ });
743
+ const childId = 'child';
744
+ expect(Option.isNone(registry.get(graph.node(childId)))).to.be.true;
745
+
746
+ Graph.expand(graph, childId, 'child');
747
+ expect(expandCalls).to.deep.equal([]);
748
+
749
+ Graph.addNode(graph, { id: childId, type: EXAMPLE_TYPE });
750
+ expect(expandCalls).to.deep.equal([[childId, Node.childRelation()]]);
751
+ });
752
+
753
+ test('initialize curried', async () => {
754
+ const registry = Registry.make();
755
+ const builder = GraphBuilder.make({ registry });
756
+ const graph = builder.graph;
757
+ let initializeCalled = false;
758
+ GraphBuilder.addExtension(
759
+ builder,
760
+ GraphBuilder.createExtensionRaw({
761
+ id: 'test',
762
+ resolver: () => {
763
+ initializeCalled = true;
764
+ return Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE });
765
+ },
766
+ }),
767
+ );
768
+ await graph.pipe(Graph.initialize(EXAMPLE_ID));
769
+ expect(initializeCalled).to.be.true;
770
+ });
771
+
772
+ test('waitForPath curried', async () => {
773
+ const graph = Graph.make();
774
+ Graph.addNode(graph, {
775
+ id: Node.RootId,
776
+ type: Node.RootType,
777
+ nodes: [{ id: exampleId(1), type: EXAMPLE_TYPE }],
778
+ });
779
+ const path = await graph.pipe(Graph.waitForPath({ target: exampleId(1) }, { timeout: 1000 }));
780
+ expect(path).to.deep.equal(['root', exampleId(1)]);
453
781
  });
454
782
  });