@dxos/app-graph 0.8.4-main.72ec0f3 → 0.8.4-main.74a063c4e0
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/chunk-AKBGYELG.mjs +1603 -0
- package/dist/lib/browser/chunk-AKBGYELG.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +29 -827
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs +1604 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +29 -828
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/atoms.d.ts +8 -0
- package/dist/types/src/atoms.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +111 -65
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +179 -213
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +7 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +244 -0
- package/dist/types/src/node-matcher.d.ts.map +1 -0
- package/dist/types/src/node-matcher.test.d.ts +2 -0
- package/dist/types/src/node-matcher.test.d.ts.map +1 -0
- package/dist/types/src/node.d.ts +50 -5
- 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/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +39 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +47 -35
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +995 -112
- package/src/graph-builder.ts +705 -284
- package/src/graph.test.ts +448 -120
- package/src/graph.ts +1019 -413
- package/src/index.ts +10 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +314 -0
- package/src/node.ts +82 -8
- package/src/stories/EchoGraph.stories.tsx +151 -121
- package/src/stories/Tree.tsx +2 -2
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +95 -0
- 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 -219
- package/src/testing.ts +0 -20
|
@@ -3,45 +3,50 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Atom, Registry } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Context from 'effect/Context';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
6
8
|
import * as Function from 'effect/Function';
|
|
7
9
|
import * as Option from 'effect/Option';
|
|
8
10
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
9
11
|
|
|
10
|
-
import { Trigger
|
|
12
|
+
import { Trigger } from '@dxos/async';
|
|
13
|
+
import { Obj } from '@dxos/echo';
|
|
14
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
11
15
|
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
16
|
+
import * as Graph from './graph';
|
|
17
|
+
import * as GraphBuilder from './graph-builder';
|
|
18
|
+
import * as Node from './node';
|
|
19
|
+
import * as NodeMatcher from './node-matcher';
|
|
20
|
+
import { qualifyId } from './util';
|
|
15
21
|
|
|
16
22
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
17
23
|
const EXAMPLE_ID = exampleId(1);
|
|
18
|
-
const EXAMPLE_TYPE = 'dxos.
|
|
24
|
+
const EXAMPLE_TYPE = 'org.dxos.type.example';
|
|
19
25
|
|
|
20
26
|
describe('GraphBuilder', () => {
|
|
21
27
|
describe('resolver', () => {
|
|
22
28
|
test('works', async () => {
|
|
23
29
|
const registry = Registry.make();
|
|
24
|
-
const builder =
|
|
30
|
+
const builder = GraphBuilder.make({ registry });
|
|
25
31
|
const graph = builder.graph;
|
|
26
32
|
|
|
27
33
|
{
|
|
28
|
-
const node =
|
|
34
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
29
35
|
expect(node).to.be.null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
// Test direct API
|
|
39
|
+
GraphBuilder.addExtension(
|
|
40
|
+
builder,
|
|
41
|
+
GraphBuilder.createExtensionRaw({
|
|
34
42
|
id: 'resolver',
|
|
35
|
-
resolver: () => {
|
|
36
|
-
console.log('resolver');
|
|
37
|
-
return Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
38
|
-
},
|
|
43
|
+
resolver: () => Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }),
|
|
39
44
|
}),
|
|
40
45
|
);
|
|
41
|
-
await
|
|
46
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
42
47
|
|
|
43
48
|
{
|
|
44
|
-
const node =
|
|
49
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
45
50
|
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
46
51
|
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
47
52
|
expect(node?.data).to.equal(1);
|
|
@@ -50,95 +55,284 @@ describe('GraphBuilder', () => {
|
|
|
50
55
|
|
|
51
56
|
test('updates', async () => {
|
|
52
57
|
const registry = Registry.make();
|
|
53
|
-
const builder =
|
|
58
|
+
const builder = GraphBuilder.make({ registry });
|
|
54
59
|
const name = Atom.make('default');
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
GraphBuilder.addExtension(
|
|
61
|
+
builder,
|
|
62
|
+
GraphBuilder.createExtensionRaw({
|
|
57
63
|
id: 'resolver',
|
|
58
64
|
resolver: () => Atom.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
59
65
|
}),
|
|
60
66
|
);
|
|
61
67
|
const graph = builder.graph;
|
|
62
|
-
await
|
|
68
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
63
69
|
|
|
64
70
|
{
|
|
65
|
-
const node =
|
|
71
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
66
72
|
expect(node?.data).to.equal('default');
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
registry.set(name, 'updated');
|
|
70
76
|
|
|
71
77
|
{
|
|
72
|
-
const node =
|
|
78
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
73
79
|
expect(node?.data).to.equal('updated');
|
|
74
80
|
}
|
|
75
81
|
});
|
|
82
|
+
|
|
83
|
+
test('connects resolved node to parent via child edge', async ({ expect }) => {
|
|
84
|
+
const registry = Registry.make();
|
|
85
|
+
const builder = GraphBuilder.make({ registry });
|
|
86
|
+
const childId = qualifyId('root', '~child');
|
|
87
|
+
|
|
88
|
+
GraphBuilder.addExtension(
|
|
89
|
+
builder,
|
|
90
|
+
GraphBuilder.createExtensionRaw({
|
|
91
|
+
id: 'resolver',
|
|
92
|
+
resolver: (id) =>
|
|
93
|
+
id === childId ? Atom.make({ id: childId, type: EXAMPLE_TYPE, data: 'resolved' }) : Atom.make(null),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const graph = builder.graph;
|
|
98
|
+
await Graph.initialize(graph, childId);
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
const node = Graph.getNode(graph, childId).pipe(Option.getOrNull);
|
|
102
|
+
expect(node?.id).to.equal(childId);
|
|
103
|
+
expect(node?.data).to.equal('resolved');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Verify the resolved node is a child of root.
|
|
107
|
+
{
|
|
108
|
+
const children = registry.get(graph.connections('root', 'child'));
|
|
109
|
+
expect(children.some((n) => n.id === childId)).to.be.true;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('out-of-order: resolver fires before parent exists', async ({ expect }) => {
|
|
114
|
+
const registry = Registry.make();
|
|
115
|
+
const builder = GraphBuilder.make({ registry });
|
|
116
|
+
const parentId = qualifyId('root', 'parent');
|
|
117
|
+
const childId = qualifyId('root', 'parent', '~child');
|
|
118
|
+
|
|
119
|
+
GraphBuilder.addExtension(builder, [
|
|
120
|
+
GraphBuilder.createExtensionRaw({
|
|
121
|
+
id: 'resolver',
|
|
122
|
+
resolver: (id) =>
|
|
123
|
+
id === childId ? Atom.make({ id: childId, type: EXAMPLE_TYPE, data: 'resolved-child' }) : Atom.make(null),
|
|
124
|
+
}),
|
|
125
|
+
GraphBuilder.createExtensionRaw({
|
|
126
|
+
id: 'connector',
|
|
127
|
+
connector: (node) =>
|
|
128
|
+
Atom.make((get) =>
|
|
129
|
+
Function.pipe(
|
|
130
|
+
get(node),
|
|
131
|
+
Option.filter((n) => n.id === 'root'),
|
|
132
|
+
Option.map(() => [{ id: 'parent', type: EXAMPLE_TYPE, data: 'parent-data' }]),
|
|
133
|
+
Option.getOrElse(() => []),
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const graph = builder.graph;
|
|
140
|
+
|
|
141
|
+
// Resolve child BEFORE parent exists in the graph.
|
|
142
|
+
await Graph.initialize(graph, childId);
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
const node = Graph.getNode(graph, childId).pipe(Option.getOrNull);
|
|
146
|
+
expect(node?.id).to.equal(childId);
|
|
147
|
+
expect(node?.data).to.equal('resolved-child');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Now expand root to create parent via connector.
|
|
151
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
152
|
+
await GraphBuilder.flush(builder);
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
const parent = Graph.getNode(graph, parentId).pipe(Option.getOrNull);
|
|
156
|
+
expect(parent?.data).to.equal('parent-data');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The resolved child should be connected to the parent.
|
|
160
|
+
{
|
|
161
|
+
const children = registry.get(graph.connections(parentId, 'child'));
|
|
162
|
+
expect(children.some((n) => n.id === childId)).to.be.true;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('onNone does not remove connector-owned node', async ({ expect }) => {
|
|
167
|
+
const registry = Registry.make();
|
|
168
|
+
const builder = GraphBuilder.make({ registry });
|
|
169
|
+
const nodeId = qualifyId('root', 'shared');
|
|
170
|
+
|
|
171
|
+
// Connector that produces root/shared. No resolver matches root/shared.
|
|
172
|
+
GraphBuilder.addExtension(
|
|
173
|
+
builder,
|
|
174
|
+
GraphBuilder.createExtensionRaw({
|
|
175
|
+
id: 'connector',
|
|
176
|
+
connector: (node) =>
|
|
177
|
+
Atom.make((get) =>
|
|
178
|
+
Function.pipe(
|
|
179
|
+
get(node),
|
|
180
|
+
Option.filter((n) => n.id === 'root'),
|
|
181
|
+
Option.map(() => [{ id: 'shared', type: EXAMPLE_TYPE, data: 'from-connector' }]),
|
|
182
|
+
Option.getOrElse(() => []),
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const graph = builder.graph;
|
|
189
|
+
|
|
190
|
+
// Connector produces root/shared.
|
|
191
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
192
|
+
await GraphBuilder.flush(builder);
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
const node = Graph.getNode(graph, nodeId).pipe(Option.getOrNull);
|
|
196
|
+
expect(node?.data).to.equal('from-connector');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Initialize fires for the same ID. No resolver matches, so onNone fires.
|
|
200
|
+
// The connector-owned node should NOT be removed.
|
|
201
|
+
await Graph.initialize(graph, nodeId);
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
const node = Graph.getNode(graph, nodeId).pipe(Option.getOrNull);
|
|
205
|
+
expect(node?.data).to.equal('from-connector');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('does not overwrite connector-produced node', async () => {
|
|
210
|
+
const registry = Registry.make();
|
|
211
|
+
const builder = GraphBuilder.make({ registry });
|
|
212
|
+
const resolverData = Atom.make('from-resolver');
|
|
213
|
+
|
|
214
|
+
GraphBuilder.addExtension(builder, [
|
|
215
|
+
GraphBuilder.createExtensionRaw({
|
|
216
|
+
id: 'resolver',
|
|
217
|
+
resolver: (id) =>
|
|
218
|
+
id === qualifyId('root', 'shared')
|
|
219
|
+
? Atom.make((get) => ({ id: qualifyId('root', 'shared'), type: EXAMPLE_TYPE, data: get(resolverData) }))
|
|
220
|
+
: Atom.make(null),
|
|
221
|
+
}),
|
|
222
|
+
GraphBuilder.createExtensionRaw({
|
|
223
|
+
id: 'connector',
|
|
224
|
+
connector: (node) =>
|
|
225
|
+
Atom.make((get) =>
|
|
226
|
+
Function.pipe(
|
|
227
|
+
get(node),
|
|
228
|
+
Option.filter((n) => n.id === 'root'),
|
|
229
|
+
Option.map(() => [{ id: 'shared', type: EXAMPLE_TYPE, data: 'from-connector' }]),
|
|
230
|
+
Option.getOrElse(() => []),
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const graph = builder.graph;
|
|
237
|
+
|
|
238
|
+
// Connector produces root/shared.
|
|
239
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
240
|
+
await GraphBuilder.flush(builder);
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
const node = Graph.getNode(graph, qualifyId('root', 'shared')).pipe(Option.getOrNull);
|
|
244
|
+
expect(node?.data).to.equal('from-connector');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Resolver fires for the same ID but should not overwrite.
|
|
248
|
+
await Graph.initialize(graph, qualifyId('root', 'shared'));
|
|
249
|
+
|
|
250
|
+
{
|
|
251
|
+
const node = Graph.getNode(graph, qualifyId('root', 'shared')).pipe(Option.getOrNull);
|
|
252
|
+
expect(node?.data).to.equal('from-connector');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Updating the resolver's atom should still not overwrite.
|
|
256
|
+
registry.set(resolverData, 'updated-resolver');
|
|
257
|
+
|
|
258
|
+
{
|
|
259
|
+
const node = Graph.getNode(graph, qualifyId('root', 'shared')).pipe(Option.getOrNull);
|
|
260
|
+
expect(node?.data).to.equal('from-connector');
|
|
261
|
+
}
|
|
262
|
+
});
|
|
76
263
|
});
|
|
77
264
|
|
|
78
265
|
describe('connector', () => {
|
|
79
|
-
test('works', () => {
|
|
266
|
+
test('works', async () => {
|
|
80
267
|
const registry = Registry.make();
|
|
81
|
-
const builder =
|
|
82
|
-
|
|
83
|
-
|
|
268
|
+
const builder = GraphBuilder.make({ registry });
|
|
269
|
+
GraphBuilder.addExtension(
|
|
270
|
+
builder,
|
|
271
|
+
GraphBuilder.createExtensionRaw({
|
|
84
272
|
id: 'outbound-connector',
|
|
85
273
|
connector: () => Atom.make([{ id: 'child', type: EXAMPLE_TYPE, data: 2 }]),
|
|
86
274
|
}),
|
|
87
275
|
);
|
|
88
|
-
|
|
89
|
-
|
|
276
|
+
GraphBuilder.addExtension(
|
|
277
|
+
builder,
|
|
278
|
+
GraphBuilder.createExtensionRaw({
|
|
90
279
|
id: 'inbound-connector',
|
|
91
|
-
relation: 'inbound',
|
|
280
|
+
relation: Node.childRelation('inbound'),
|
|
92
281
|
connector: () => Atom.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
93
282
|
}),
|
|
94
283
|
);
|
|
95
284
|
|
|
96
285
|
const graph = builder.graph;
|
|
97
|
-
|
|
98
|
-
|
|
286
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
287
|
+
Graph.expand(graph, Node.RootId, Node.childRelation('inbound'));
|
|
288
|
+
await GraphBuilder.flush(builder);
|
|
99
289
|
|
|
100
|
-
const outbound = registry.get(graph.connections(
|
|
101
|
-
const inbound = registry.get(graph.connections(
|
|
290
|
+
const outbound = registry.get(graph.connections(Node.RootId, 'child'));
|
|
291
|
+
const inbound = registry.get(graph.connections(Node.RootId, Node.childRelation('inbound')));
|
|
102
292
|
|
|
103
293
|
expect(outbound).has.length(1);
|
|
104
|
-
expect(outbound[0].id).to.equal('child');
|
|
294
|
+
expect(outbound[0].id).to.equal('root/child');
|
|
105
295
|
expect(outbound[0].data).to.equal(2);
|
|
106
296
|
expect(inbound).has.length(1);
|
|
107
|
-
expect(inbound[0].id).to.equal('parent');
|
|
297
|
+
expect(inbound[0].id).to.equal('root/parent');
|
|
108
298
|
expect(inbound[0].data).to.equal(0);
|
|
109
299
|
});
|
|
110
300
|
|
|
111
|
-
test('updates', () => {
|
|
301
|
+
test('updates', async () => {
|
|
112
302
|
const registry = Registry.make();
|
|
113
|
-
const builder =
|
|
303
|
+
const builder = GraphBuilder.make({ registry });
|
|
114
304
|
const state = Atom.make(0);
|
|
115
|
-
|
|
116
|
-
|
|
305
|
+
GraphBuilder.addExtension(
|
|
306
|
+
builder,
|
|
307
|
+
GraphBuilder.createExtensionRaw({
|
|
117
308
|
id: 'connector',
|
|
118
309
|
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
119
310
|
}),
|
|
120
311
|
);
|
|
121
312
|
const graph = builder.graph;
|
|
122
|
-
|
|
313
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
314
|
+
await GraphBuilder.flush(builder);
|
|
123
315
|
|
|
124
316
|
{
|
|
125
|
-
const [node] = registry.get(graph.connections(
|
|
317
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
126
318
|
expect(node.data).to.equal(0);
|
|
127
319
|
}
|
|
128
320
|
|
|
129
321
|
{
|
|
130
322
|
registry.set(state, 1);
|
|
131
|
-
|
|
323
|
+
await GraphBuilder.flush(builder);
|
|
324
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
132
325
|
expect(node.data).to.equal(1);
|
|
133
326
|
}
|
|
134
327
|
});
|
|
135
328
|
|
|
136
|
-
test('subscribes to updates', () => {
|
|
329
|
+
test('subscribes to updates', async () => {
|
|
137
330
|
const registry = Registry.make();
|
|
138
|
-
const builder =
|
|
331
|
+
const builder = GraphBuilder.make({ registry });
|
|
139
332
|
const state = Atom.make(0);
|
|
140
|
-
|
|
141
|
-
|
|
333
|
+
GraphBuilder.addExtension(
|
|
334
|
+
builder,
|
|
335
|
+
GraphBuilder.createExtensionRaw({
|
|
142
336
|
id: 'connector',
|
|
143
337
|
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
144
338
|
}),
|
|
@@ -146,36 +340,41 @@ describe('GraphBuilder', () => {
|
|
|
146
340
|
const graph = builder.graph;
|
|
147
341
|
|
|
148
342
|
let count = 0;
|
|
149
|
-
const cancel = registry.subscribe(graph.connections(
|
|
343
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_) => {
|
|
150
344
|
count++;
|
|
151
345
|
});
|
|
152
346
|
onTestFinished(() => cancel());
|
|
153
347
|
|
|
154
348
|
expect(count).to.equal(0);
|
|
155
|
-
expect(registry.get(graph.connections(
|
|
349
|
+
expect(registry.get(graph.connections(Node.RootId, 'child'))).to.have.length(0);
|
|
156
350
|
expect(count).to.equal(1);
|
|
157
351
|
|
|
158
|
-
|
|
352
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
353
|
+
await GraphBuilder.flush(builder);
|
|
159
354
|
expect(count).to.equal(2);
|
|
355
|
+
|
|
160
356
|
registry.set(state, 1);
|
|
357
|
+
await GraphBuilder.flush(builder);
|
|
161
358
|
expect(count).to.equal(3);
|
|
162
359
|
});
|
|
163
360
|
|
|
164
|
-
test('updates with new extensions', () => {
|
|
361
|
+
test('updates with new extensions', async () => {
|
|
165
362
|
const registry = Registry.make();
|
|
166
|
-
const builder =
|
|
167
|
-
|
|
168
|
-
|
|
363
|
+
const builder = GraphBuilder.make({ registry });
|
|
364
|
+
GraphBuilder.addExtension(
|
|
365
|
+
builder,
|
|
366
|
+
GraphBuilder.createExtensionRaw({
|
|
169
367
|
id: 'connector',
|
|
170
368
|
connector: () => Atom.make([{ id: EXAMPLE_ID, type: EXAMPLE_TYPE }]),
|
|
171
369
|
}),
|
|
172
370
|
);
|
|
173
371
|
const graph = builder.graph;
|
|
174
|
-
|
|
372
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
373
|
+
await GraphBuilder.flush(builder);
|
|
175
374
|
|
|
176
|
-
let nodes: Node[] = [];
|
|
375
|
+
let nodes: Node.Node[] = [];
|
|
177
376
|
let count = 0;
|
|
178
|
-
const cancel = registry.subscribe(graph.connections(
|
|
377
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_nodes) => {
|
|
179
378
|
count++;
|
|
180
379
|
nodes = _nodes;
|
|
181
380
|
});
|
|
@@ -183,59 +382,64 @@ describe('GraphBuilder', () => {
|
|
|
183
382
|
|
|
184
383
|
expect(nodes).has.length(0);
|
|
185
384
|
expect(count).to.equal(0);
|
|
186
|
-
registry.get(graph.connections(
|
|
385
|
+
registry.get(graph.connections(Node.RootId, 'child'));
|
|
187
386
|
expect(nodes).has.length(1);
|
|
188
387
|
expect(count).to.equal(1);
|
|
189
388
|
|
|
190
|
-
|
|
191
|
-
|
|
389
|
+
GraphBuilder.addExtension(
|
|
390
|
+
builder,
|
|
391
|
+
GraphBuilder.createExtensionRaw({
|
|
192
392
|
id: 'connector-2',
|
|
193
393
|
connector: () => Atom.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
194
394
|
}),
|
|
195
395
|
);
|
|
396
|
+
await GraphBuilder.flush(builder);
|
|
196
397
|
expect(nodes).has.length(2);
|
|
197
398
|
expect(count).to.equal(2);
|
|
198
399
|
});
|
|
199
400
|
|
|
200
|
-
test('removes', () => {
|
|
401
|
+
test('removes', async () => {
|
|
201
402
|
const registry = Registry.make();
|
|
202
|
-
const builder =
|
|
403
|
+
const builder = GraphBuilder.make({ registry });
|
|
203
404
|
const nodes = Atom.make([
|
|
204
405
|
{ id: exampleId(1), type: EXAMPLE_TYPE },
|
|
205
406
|
{ id: exampleId(2), type: EXAMPLE_TYPE },
|
|
206
407
|
]);
|
|
207
|
-
|
|
208
|
-
|
|
408
|
+
GraphBuilder.addExtension(
|
|
409
|
+
builder,
|
|
410
|
+
GraphBuilder.createExtensionRaw({
|
|
209
411
|
id: 'connector',
|
|
210
412
|
connector: () => Atom.make((get) => get(nodes)),
|
|
211
413
|
}),
|
|
212
414
|
);
|
|
213
415
|
const graph = builder.graph;
|
|
214
|
-
|
|
416
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
417
|
+
await GraphBuilder.flush(builder);
|
|
215
418
|
|
|
216
419
|
{
|
|
217
|
-
const nodes = registry.get(graph.connections(
|
|
420
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
218
421
|
expect(nodes).has.length(2);
|
|
219
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
220
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
422
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(1)));
|
|
423
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(2)));
|
|
221
424
|
}
|
|
222
425
|
|
|
223
426
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
427
|
+
await GraphBuilder.flush(builder);
|
|
224
428
|
|
|
225
429
|
{
|
|
226
|
-
const nodes = registry.get(graph.connections(
|
|
430
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
227
431
|
expect(nodes).has.length(1);
|
|
228
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
432
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
229
433
|
}
|
|
230
434
|
});
|
|
231
435
|
|
|
232
|
-
test('nodes are updated when removed', () => {
|
|
436
|
+
test('nodes are updated when removed', async () => {
|
|
233
437
|
const registry = Registry.make();
|
|
234
|
-
const builder =
|
|
438
|
+
const builder = GraphBuilder.make({ registry });
|
|
235
439
|
const name = Atom.make('removed');
|
|
236
440
|
|
|
237
|
-
|
|
238
|
-
|
|
441
|
+
GraphBuilder.addExtension(builder, [
|
|
442
|
+
GraphBuilder.createExtensionRaw({
|
|
239
443
|
id: 'root',
|
|
240
444
|
connector: (node) =>
|
|
241
445
|
Atom.make((get) =>
|
|
@@ -254,52 +458,58 @@ describe('GraphBuilder', () => {
|
|
|
254
458
|
|
|
255
459
|
let count = 0;
|
|
256
460
|
let exists = false;
|
|
257
|
-
const cancel = registry.subscribe(graph.node(EXAMPLE_ID), (node) => {
|
|
461
|
+
const cancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (node) => {
|
|
258
462
|
count++;
|
|
259
463
|
exists = Option.isSome(node);
|
|
260
464
|
});
|
|
261
465
|
onTestFinished(() => cancel());
|
|
262
466
|
|
|
263
|
-
|
|
467
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
468
|
+
await GraphBuilder.flush(builder);
|
|
264
469
|
expect(count).to.equal(0);
|
|
265
470
|
expect(exists).to.be.false;
|
|
266
471
|
|
|
267
472
|
registry.set(name, 'default');
|
|
473
|
+
await GraphBuilder.flush(builder);
|
|
268
474
|
expect(count).to.equal(1);
|
|
269
475
|
expect(exists).to.be.true;
|
|
270
476
|
|
|
271
477
|
registry.set(name, 'removed');
|
|
478
|
+
await GraphBuilder.flush(builder);
|
|
272
479
|
expect(count).to.equal(2);
|
|
273
480
|
expect(exists).to.be.false;
|
|
274
481
|
|
|
275
482
|
registry.set(name, 'added');
|
|
483
|
+
await GraphBuilder.flush(builder);
|
|
276
484
|
expect(count).to.equal(3);
|
|
277
485
|
expect(exists).to.be.true;
|
|
278
486
|
});
|
|
279
487
|
|
|
280
488
|
test('sort edges', async () => {
|
|
281
489
|
const registry = Registry.make();
|
|
282
|
-
const builder =
|
|
490
|
+
const builder = GraphBuilder.make({ registry });
|
|
283
491
|
const nodes = Atom.make([
|
|
284
492
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
285
493
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
286
494
|
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
287
495
|
]);
|
|
288
|
-
|
|
289
|
-
|
|
496
|
+
GraphBuilder.addExtension(
|
|
497
|
+
builder,
|
|
498
|
+
GraphBuilder.createExtensionRaw({
|
|
290
499
|
id: 'connector',
|
|
291
500
|
connector: () => Atom.make((get) => get(nodes)),
|
|
292
501
|
}),
|
|
293
502
|
);
|
|
294
503
|
const graph = builder.graph;
|
|
295
|
-
|
|
504
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
505
|
+
await GraphBuilder.flush(builder);
|
|
296
506
|
|
|
297
507
|
{
|
|
298
|
-
const nodes = registry.get(graph.connections(
|
|
508
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
299
509
|
expect(nodes).has.length(3);
|
|
300
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
301
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
302
|
-
expect(nodes[2].id).to.equal(exampleId(3));
|
|
510
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(1)));
|
|
511
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(2)));
|
|
512
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(3)));
|
|
303
513
|
}
|
|
304
514
|
|
|
305
515
|
registry.set(nodes, [
|
|
@@ -307,27 +517,25 @@ describe('GraphBuilder', () => {
|
|
|
307
517
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
308
518
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
309
519
|
]);
|
|
310
|
-
|
|
311
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
312
|
-
await sleep(0);
|
|
520
|
+
await GraphBuilder.flush(builder);
|
|
313
521
|
|
|
314
522
|
{
|
|
315
|
-
const nodes = registry.get(graph.connections(
|
|
523
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
316
524
|
expect(nodes).has.length(3);
|
|
317
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
318
|
-
expect(nodes[1].id).to.equal(exampleId(1));
|
|
319
|
-
expect(nodes[2].id).to.equal(exampleId(2));
|
|
525
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
526
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(1)));
|
|
527
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(2)));
|
|
320
528
|
}
|
|
321
529
|
});
|
|
322
530
|
|
|
323
|
-
test('updates are constrained', () => {
|
|
531
|
+
test('updates are constrained', async () => {
|
|
324
532
|
const registry = Registry.make();
|
|
325
|
-
const builder =
|
|
533
|
+
const builder = GraphBuilder.make({ registry });
|
|
326
534
|
const name = Atom.make('default');
|
|
327
535
|
const sub = Atom.make('default');
|
|
328
536
|
|
|
329
|
-
|
|
330
|
-
|
|
537
|
+
GraphBuilder.addExtension(builder, [
|
|
538
|
+
GraphBuilder.createExtensionRaw({
|
|
331
539
|
id: 'root',
|
|
332
540
|
connector: (node) =>
|
|
333
541
|
Atom.make((get) =>
|
|
@@ -340,25 +548,29 @@ describe('GraphBuilder', () => {
|
|
|
340
548
|
),
|
|
341
549
|
),
|
|
342
550
|
}),
|
|
343
|
-
|
|
551
|
+
GraphBuilder.createExtensionRaw({
|
|
344
552
|
id: 'connector1',
|
|
345
553
|
connector: (node) =>
|
|
346
554
|
Atom.make((get) =>
|
|
347
555
|
Function.pipe(
|
|
348
556
|
get(node),
|
|
349
|
-
Option.flatMap((node) =>
|
|
557
|
+
Option.flatMap((node) =>
|
|
558
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
559
|
+
),
|
|
350
560
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
351
561
|
Option.getOrElse(() => []),
|
|
352
562
|
),
|
|
353
563
|
),
|
|
354
564
|
}),
|
|
355
|
-
|
|
565
|
+
GraphBuilder.createExtensionRaw({
|
|
356
566
|
id: 'connector2',
|
|
357
567
|
connector: (node) =>
|
|
358
568
|
Atom.make((get) =>
|
|
359
569
|
Function.pipe(
|
|
360
570
|
get(node),
|
|
361
|
-
Option.flatMap((node) =>
|
|
571
|
+
Option.flatMap((node) =>
|
|
572
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
573
|
+
),
|
|
362
574
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
363
575
|
Option.getOrElse(() => []),
|
|
364
576
|
),
|
|
@@ -369,43 +581,47 @@ describe('GraphBuilder', () => {
|
|
|
369
581
|
const graph = builder.graph;
|
|
370
582
|
|
|
371
583
|
let parentCount = 0;
|
|
372
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
584
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
373
585
|
parentCount++;
|
|
374
586
|
});
|
|
375
587
|
onTestFinished(() => parentCancel());
|
|
376
588
|
|
|
377
589
|
let independentCount = 0;
|
|
378
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
590
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
379
591
|
independentCount++;
|
|
380
592
|
});
|
|
381
593
|
onTestFinished(() => independentCancel());
|
|
382
594
|
|
|
383
595
|
let dependentCount = 0;
|
|
384
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
596
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
385
597
|
dependentCount++;
|
|
386
598
|
});
|
|
387
599
|
onTestFinished(() => dependentCancel());
|
|
388
600
|
|
|
389
601
|
// Counts should not increment until the node is expanded.
|
|
390
|
-
|
|
602
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
603
|
+
await GraphBuilder.flush(builder);
|
|
391
604
|
expect(parentCount).to.equal(1);
|
|
392
605
|
expect(independentCount).to.equal(0);
|
|
393
606
|
expect(dependentCount).to.equal(0);
|
|
394
607
|
|
|
395
608
|
// Counts should increment when the node is expanded.
|
|
396
|
-
|
|
609
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
610
|
+
await GraphBuilder.flush(builder);
|
|
397
611
|
expect(parentCount).to.equal(1);
|
|
398
612
|
expect(independentCount).to.equal(1);
|
|
399
613
|
expect(dependentCount).to.equal(1);
|
|
400
614
|
|
|
401
615
|
// Only dependent count should increment when the parent changes.
|
|
402
616
|
registry.set(name, 'updated');
|
|
617
|
+
await GraphBuilder.flush(builder);
|
|
403
618
|
expect(parentCount).to.equal(2);
|
|
404
619
|
expect(independentCount).to.equal(1);
|
|
405
620
|
expect(dependentCount).to.equal(2);
|
|
406
621
|
|
|
407
622
|
// Only independent count should increment when its state changes.
|
|
408
623
|
registry.set(sub, 'updated');
|
|
624
|
+
await GraphBuilder.flush(builder);
|
|
409
625
|
expect(parentCount).to.equal(2);
|
|
410
626
|
expect(independentCount).to.equal(2);
|
|
411
627
|
expect(dependentCount).to.equal(2);
|
|
@@ -415,18 +631,21 @@ describe('GraphBuilder', () => {
|
|
|
415
631
|
registry.set(name, 'removed');
|
|
416
632
|
registry.set(sub, 'batch');
|
|
417
633
|
});
|
|
634
|
+
await GraphBuilder.flush(builder);
|
|
418
635
|
expect(parentCount).to.equal(2);
|
|
419
636
|
expect(independentCount).to.equal(3);
|
|
420
637
|
expect(dependentCount).to.equal(2);
|
|
421
638
|
|
|
422
639
|
// Dependent count should increment when the node is added back.
|
|
423
640
|
registry.set(name, 'added');
|
|
641
|
+
await GraphBuilder.flush(builder);
|
|
424
642
|
expect(parentCount).to.equal(3);
|
|
425
643
|
expect(independentCount).to.equal(3);
|
|
426
644
|
expect(dependentCount).to.equal(3);
|
|
427
645
|
|
|
428
646
|
// Counts should not increment when the node is expanded again.
|
|
429
|
-
|
|
647
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
648
|
+
await GraphBuilder.flush(builder);
|
|
430
649
|
expect(parentCount).to.equal(3);
|
|
431
650
|
expect(independentCount).to.equal(3);
|
|
432
651
|
expect(dependentCount).to.equal(3);
|
|
@@ -434,9 +653,10 @@ describe('GraphBuilder', () => {
|
|
|
434
653
|
|
|
435
654
|
test('eager graph expansion', async () => {
|
|
436
655
|
const registry = Registry.make();
|
|
437
|
-
const builder =
|
|
438
|
-
|
|
439
|
-
|
|
656
|
+
const builder = GraphBuilder.make({ registry });
|
|
657
|
+
GraphBuilder.addExtension(
|
|
658
|
+
builder,
|
|
659
|
+
GraphBuilder.createExtensionRaw({
|
|
440
660
|
id: 'connector',
|
|
441
661
|
connector: (node) => {
|
|
442
662
|
return Atom.make((get) =>
|
|
@@ -455,14 +675,14 @@ describe('GraphBuilder', () => {
|
|
|
455
675
|
let count = 0;
|
|
456
676
|
const trigger = new Trigger();
|
|
457
677
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
458
|
-
builder.graph
|
|
678
|
+
Graph.expand(builder.graph, id, 'child');
|
|
459
679
|
count++;
|
|
460
680
|
if (count === 5) {
|
|
461
681
|
trigger.wake();
|
|
462
682
|
}
|
|
463
683
|
});
|
|
464
684
|
|
|
465
|
-
builder.graph.
|
|
685
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
466
686
|
await trigger.wait();
|
|
467
687
|
expect(count).to.equal(5);
|
|
468
688
|
});
|
|
@@ -470,9 +690,10 @@ describe('GraphBuilder', () => {
|
|
|
470
690
|
|
|
471
691
|
describe('explore', () => {
|
|
472
692
|
test('works', async () => {
|
|
473
|
-
const builder =
|
|
474
|
-
|
|
475
|
-
|
|
693
|
+
const builder = GraphBuilder.make();
|
|
694
|
+
GraphBuilder.addExtension(
|
|
695
|
+
builder,
|
|
696
|
+
GraphBuilder.createExtensionRaw({
|
|
476
697
|
id: 'connector',
|
|
477
698
|
connector: (node) =>
|
|
478
699
|
Atom.make((get) =>
|
|
@@ -488,7 +709,8 @@ describe('GraphBuilder', () => {
|
|
|
488
709
|
);
|
|
489
710
|
|
|
490
711
|
let count = 0;
|
|
491
|
-
await
|
|
712
|
+
await GraphBuilder.explore(builder, {
|
|
713
|
+
relation: 'child',
|
|
492
714
|
visitor: () => {
|
|
493
715
|
count++;
|
|
494
716
|
},
|
|
@@ -497,4 +719,665 @@ describe('GraphBuilder', () => {
|
|
|
497
719
|
expect(count).to.equal(6);
|
|
498
720
|
});
|
|
499
721
|
});
|
|
722
|
+
|
|
723
|
+
describe('helpers', () => {
|
|
724
|
+
describe('createConnector', () => {
|
|
725
|
+
test('creates connector with type inference', async () => {
|
|
726
|
+
const registry = Registry.make();
|
|
727
|
+
const builder = GraphBuilder.make({ registry });
|
|
728
|
+
const graph = builder.graph;
|
|
729
|
+
|
|
730
|
+
const matcher = (node: Node.Node) => NodeMatcher.whenId('root')(node);
|
|
731
|
+
const factory = (node: Node.Node) => [{ id: 'child', type: EXAMPLE_TYPE, data: node.id }];
|
|
732
|
+
|
|
733
|
+
const connector = GraphBuilder.createConnector(matcher, factory);
|
|
734
|
+
|
|
735
|
+
GraphBuilder.addExtension(
|
|
736
|
+
builder,
|
|
737
|
+
GraphBuilder.createExtensionRaw({
|
|
738
|
+
id: 'test-connector',
|
|
739
|
+
connector,
|
|
740
|
+
}),
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
744
|
+
await GraphBuilder.flush(builder);
|
|
745
|
+
|
|
746
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
747
|
+
expect(connections).has.length(1);
|
|
748
|
+
expect(connections[0].id).to.equal('root/child');
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe('createExtension', () => {
|
|
753
|
+
test('works with Effect connector', async () => {
|
|
754
|
+
const registry = Registry.make();
|
|
755
|
+
const builder = GraphBuilder.make({ registry });
|
|
756
|
+
const graph = builder.graph;
|
|
757
|
+
|
|
758
|
+
const extensions = Effect.runSync(
|
|
759
|
+
GraphBuilder.createExtension({
|
|
760
|
+
id: 'test-extension',
|
|
761
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
762
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: node.data }]),
|
|
763
|
+
}),
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
767
|
+
|
|
768
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
769
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
770
|
+
Graph.expand(graph, 'parent', 'child');
|
|
771
|
+
await GraphBuilder.flush(builder);
|
|
772
|
+
|
|
773
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
774
|
+
expect(connections).has.length(1);
|
|
775
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
776
|
+
expect(connections[0].data).to.equal('test');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('works with Effect actions', async () => {
|
|
780
|
+
const registry = Registry.make();
|
|
781
|
+
const builder = GraphBuilder.make({ registry });
|
|
782
|
+
const graph = builder.graph;
|
|
783
|
+
|
|
784
|
+
const extensions = Effect.runSync(
|
|
785
|
+
GraphBuilder.createExtension({
|
|
786
|
+
id: 'test-extension',
|
|
787
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
788
|
+
actions: (node, get) =>
|
|
789
|
+
Effect.succeed([
|
|
790
|
+
{
|
|
791
|
+
id: 'test-action',
|
|
792
|
+
data: () => Effect.void,
|
|
793
|
+
properties: { label: 'Test' },
|
|
794
|
+
},
|
|
795
|
+
]),
|
|
796
|
+
}),
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
800
|
+
|
|
801
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
802
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
803
|
+
Graph.expand(graph, 'parent', 'child');
|
|
804
|
+
await GraphBuilder.flush(builder);
|
|
805
|
+
|
|
806
|
+
const edges = registry.get(graph.edges('parent'));
|
|
807
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.have.length(1);
|
|
808
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/test-action');
|
|
809
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.have.length(0);
|
|
810
|
+
const actions = registry.get(graph.actions('parent'));
|
|
811
|
+
expect(actions).has.length(1);
|
|
812
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test('actions expand automatically with child relation', async ({ expect }) => {
|
|
816
|
+
const registry = Registry.make();
|
|
817
|
+
const builder = GraphBuilder.make({ registry });
|
|
818
|
+
const graph = builder.graph;
|
|
819
|
+
|
|
820
|
+
const extensions = Effect.runSync(
|
|
821
|
+
GraphBuilder.createExtension({
|
|
822
|
+
id: 'test-extension',
|
|
823
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
824
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: 'c' }]),
|
|
825
|
+
actions: (node, get) =>
|
|
826
|
+
Effect.succeed([{ id: 'act1', data: () => Effect.void, properties: { label: 'A' } }]),
|
|
827
|
+
}),
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
831
|
+
|
|
832
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
833
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
834
|
+
Graph.expand(graph, 'parent', 'child');
|
|
835
|
+
await GraphBuilder.flush(builder);
|
|
836
|
+
|
|
837
|
+
const edges = registry.get(graph.edges('parent'));
|
|
838
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.include('parent/child');
|
|
839
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/act1');
|
|
840
|
+
const actions = registry.get(graph.actions('parent'));
|
|
841
|
+
expect(actions).has.length(1);
|
|
842
|
+
expect(actions[0].id).to.equal('parent/act1');
|
|
843
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
844
|
+
expect(connections).has.length(1);
|
|
845
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test('actions appear when extension registered after expand', async ({ expect }) => {
|
|
849
|
+
const registry = Registry.make();
|
|
850
|
+
const builder = GraphBuilder.make({ registry });
|
|
851
|
+
const graph = builder.graph;
|
|
852
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
853
|
+
|
|
854
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
855
|
+
Graph.expand(graph, 'parent', 'child');
|
|
856
|
+
await GraphBuilder.flush(builder);
|
|
857
|
+
|
|
858
|
+
expect(registry.get(graph.actions('parent'))).to.have.length(0);
|
|
859
|
+
|
|
860
|
+
const extensions = Effect.runSync(
|
|
861
|
+
GraphBuilder.createExtension({
|
|
862
|
+
id: 'late-extension',
|
|
863
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
864
|
+
actions: (node, get) =>
|
|
865
|
+
Effect.succeed([{ id: 'late-act', data: () => Effect.void, properties: { label: 'Late' } }]),
|
|
866
|
+
}),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
870
|
+
await GraphBuilder.flush(builder);
|
|
871
|
+
|
|
872
|
+
const edges = registry.get(graph.edges('parent'));
|
|
873
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/late-act');
|
|
874
|
+
const actions = registry.get(graph.actions('parent'));
|
|
875
|
+
expect(actions).has.length(1);
|
|
876
|
+
expect(actions[0].id).to.equal('parent/late-act');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test('_actionContext captures and provides services to action execution', async () => {
|
|
880
|
+
const registry = Registry.make();
|
|
881
|
+
const builder = GraphBuilder.make({ registry });
|
|
882
|
+
const graph = builder.graph;
|
|
883
|
+
|
|
884
|
+
// Define a test service using Context.GenericTag pattern.
|
|
885
|
+
interface TestServiceInterface {
|
|
886
|
+
getValue(): number;
|
|
887
|
+
}
|
|
888
|
+
const TestService = Context.GenericTag<TestServiceInterface>('TestService');
|
|
889
|
+
|
|
890
|
+
// Track whether the action was executed with the correct context.
|
|
891
|
+
let executionResult: number | null = null;
|
|
892
|
+
|
|
893
|
+
// Create extension with service requirement.
|
|
894
|
+
// Note: The actions callback must USE the service for R to be inferred correctly.
|
|
895
|
+
const extensions = Effect.runSync(
|
|
896
|
+
GraphBuilder.createExtension({
|
|
897
|
+
id: 'test-extension',
|
|
898
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
899
|
+
actions: (node, get) =>
|
|
900
|
+
// Use TestService in the callback to include it in R.
|
|
901
|
+
Effect.gen(function* () {
|
|
902
|
+
const service = yield* TestService;
|
|
903
|
+
return [
|
|
904
|
+
{
|
|
905
|
+
id: 'test-action',
|
|
906
|
+
data: () =>
|
|
907
|
+
Effect.gen(function* () {
|
|
908
|
+
// Action can use the same service from captured context.
|
|
909
|
+
const svc = yield* TestService;
|
|
910
|
+
executionResult = svc.getValue();
|
|
911
|
+
}).pipe(Effect.asVoid),
|
|
912
|
+
properties: { label: `Test ${service.getValue()}` },
|
|
913
|
+
},
|
|
914
|
+
];
|
|
915
|
+
}),
|
|
916
|
+
}).pipe(Effect.provideService(TestService, { getValue: () => 42 })),
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
920
|
+
|
|
921
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
922
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
923
|
+
Graph.expand(graph, 'parent', 'child');
|
|
924
|
+
await GraphBuilder.flush(builder);
|
|
925
|
+
|
|
926
|
+
const actions = registry.get(graph.actions('parent'));
|
|
927
|
+
expect(actions).has.length(1);
|
|
928
|
+
|
|
929
|
+
// Verify _actionContext is captured.
|
|
930
|
+
const action = actions[0] as Node.Action;
|
|
931
|
+
expect(action._actionContext).to.not.be.undefined;
|
|
932
|
+
|
|
933
|
+
// Execute the action with the captured context.
|
|
934
|
+
const actionEffect = action.data();
|
|
935
|
+
const effectWithContext = action._actionContext
|
|
936
|
+
? actionEffect.pipe(Effect.provide(action._actionContext))
|
|
937
|
+
: actionEffect;
|
|
938
|
+
|
|
939
|
+
Effect.runSync(effectWithContext);
|
|
940
|
+
|
|
941
|
+
// Verify the service was accessible during execution.
|
|
942
|
+
expect(executionResult).to.equal(42);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test('works with resolver', async () => {
|
|
946
|
+
const registry = Registry.make();
|
|
947
|
+
const builder = GraphBuilder.make({ registry });
|
|
948
|
+
const graph = builder.graph;
|
|
949
|
+
|
|
950
|
+
const extensions = Effect.runSync(
|
|
951
|
+
GraphBuilder.createExtension({
|
|
952
|
+
id: 'test-extension',
|
|
953
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
954
|
+
resolver: (id, get) => Effect.succeed({ id, type: EXAMPLE_TYPE, properties: {}, data: 'resolved' }),
|
|
955
|
+
}),
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
959
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
960
|
+
|
|
961
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
962
|
+
expect(node).to.not.be.null;
|
|
963
|
+
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
964
|
+
expect(node?.data).to.equal('resolved');
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test('works with connector and actions together', async () => {
|
|
968
|
+
const registry = Registry.make();
|
|
969
|
+
const builder = GraphBuilder.make({ registry });
|
|
970
|
+
const graph = builder.graph;
|
|
971
|
+
|
|
972
|
+
const extensions = Effect.runSync(
|
|
973
|
+
GraphBuilder.createExtension({
|
|
974
|
+
id: 'test-extension',
|
|
975
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
976
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: node.data }]),
|
|
977
|
+
actions: (node, get) =>
|
|
978
|
+
Effect.succeed([
|
|
979
|
+
{
|
|
980
|
+
id: 'test-action',
|
|
981
|
+
data: () => Effect.void,
|
|
982
|
+
properties: { label: 'Test' },
|
|
983
|
+
},
|
|
984
|
+
]),
|
|
985
|
+
}),
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
989
|
+
|
|
990
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
991
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
992
|
+
Graph.expand(graph, 'parent', 'child');
|
|
993
|
+
await GraphBuilder.flush(builder);
|
|
994
|
+
|
|
995
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
996
|
+
// Should have both the child node and the action node.
|
|
997
|
+
expect(connections.length).to.be.greaterThanOrEqual(1);
|
|
998
|
+
const childNode = connections.find((n) => n.id === 'parent/child');
|
|
999
|
+
expect(childNode).to.not.be.undefined;
|
|
1000
|
+
expect(childNode?.data).to.equal('test');
|
|
1001
|
+
|
|
1002
|
+
const actions = registry.get(graph.actions('parent'));
|
|
1003
|
+
expect(actions).has.length(1);
|
|
1004
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test('works with reactive connector using get context', async () => {
|
|
1008
|
+
const registry = Registry.make();
|
|
1009
|
+
const builder = GraphBuilder.make({ registry });
|
|
1010
|
+
const graph = builder.graph;
|
|
1011
|
+
|
|
1012
|
+
const state = Atom.make('initial');
|
|
1013
|
+
|
|
1014
|
+
const extensions = Effect.runSync(
|
|
1015
|
+
GraphBuilder.createExtension({
|
|
1016
|
+
id: 'test-extension',
|
|
1017
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1018
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: get(state) }]),
|
|
1019
|
+
}),
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1023
|
+
|
|
1024
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1025
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1026
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1027
|
+
await GraphBuilder.flush(builder);
|
|
1028
|
+
|
|
1029
|
+
{
|
|
1030
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1031
|
+
expect(connections).has.length(1);
|
|
1032
|
+
expect(connections[0].data).to.equal('initial');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
registry.set(state, 'updated');
|
|
1036
|
+
await GraphBuilder.flush(builder);
|
|
1037
|
+
|
|
1038
|
+
{
|
|
1039
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1040
|
+
expect(connections).has.length(1);
|
|
1041
|
+
expect(connections[0].data).to.equal('updated');
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
describe('extension error handling', () => {
|
|
1047
|
+
test('connector failure is caught and logged, returns empty array', async () => {
|
|
1048
|
+
const registry = Registry.make();
|
|
1049
|
+
const builder = GraphBuilder.make({ registry });
|
|
1050
|
+
const graph = builder.graph;
|
|
1051
|
+
|
|
1052
|
+
const extensions = Effect.runSync(
|
|
1053
|
+
GraphBuilder.createExtension({
|
|
1054
|
+
id: 'failing-extension',
|
|
1055
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1056
|
+
connector: (node, get) => Effect.fail(new Error('Connector failed intentionally')),
|
|
1057
|
+
}),
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1061
|
+
|
|
1062
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1063
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1064
|
+
|
|
1065
|
+
// Should not throw, error is caught internally.
|
|
1066
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1067
|
+
await GraphBuilder.flush(builder);
|
|
1068
|
+
|
|
1069
|
+
// Should return empty connections since the connector failed.
|
|
1070
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1071
|
+
expect(connections).has.length(0);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
test('actions failure is caught and logged, returns empty array', async () => {
|
|
1075
|
+
const registry = Registry.make();
|
|
1076
|
+
const builder = GraphBuilder.make({ registry });
|
|
1077
|
+
const graph = builder.graph;
|
|
1078
|
+
|
|
1079
|
+
const extensions = Effect.runSync(
|
|
1080
|
+
GraphBuilder.createExtension({
|
|
1081
|
+
id: 'failing-actions-extension',
|
|
1082
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1083
|
+
actions: (node, get) => Effect.fail(new Error('Actions failed intentionally')),
|
|
1084
|
+
}),
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1088
|
+
|
|
1089
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1090
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1091
|
+
|
|
1092
|
+
// Should not throw, error is caught internally.
|
|
1093
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1094
|
+
await GraphBuilder.flush(builder);
|
|
1095
|
+
|
|
1096
|
+
// Should return empty actions since the actions callback failed.
|
|
1097
|
+
const actions = registry.get(graph.actions('parent'));
|
|
1098
|
+
expect(actions).has.length(0);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test('resolver failure is caught and logged, returns null', async () => {
|
|
1102
|
+
const registry = Registry.make();
|
|
1103
|
+
const builder = GraphBuilder.make({ registry });
|
|
1104
|
+
const graph = builder.graph;
|
|
1105
|
+
|
|
1106
|
+
const extensions = Effect.runSync(
|
|
1107
|
+
GraphBuilder.createExtension({
|
|
1108
|
+
id: 'failing-resolver-extension',
|
|
1109
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1110
|
+
resolver: (id, get) => Effect.fail(new Error('Resolver failed intentionally')),
|
|
1111
|
+
}),
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1115
|
+
|
|
1116
|
+
// Should not throw, error is caught internally.
|
|
1117
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
1118
|
+
|
|
1119
|
+
// Should return null/none since the resolver failed.
|
|
1120
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
1121
|
+
expect(node).to.be.null;
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test('failing extension does not affect other extensions', async () => {
|
|
1125
|
+
const registry = Registry.make();
|
|
1126
|
+
const builder = GraphBuilder.make({ registry });
|
|
1127
|
+
const graph = builder.graph;
|
|
1128
|
+
|
|
1129
|
+
// Add a failing extension.
|
|
1130
|
+
const failingExtensions = Effect.runSync(
|
|
1131
|
+
GraphBuilder.createExtension({
|
|
1132
|
+
id: 'failing-extension',
|
|
1133
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1134
|
+
connector: (node, get) => Effect.fail(new Error('This one fails')),
|
|
1135
|
+
}),
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
// Add a working extension.
|
|
1139
|
+
const workingExtensions = Effect.runSync(
|
|
1140
|
+
GraphBuilder.createExtension({
|
|
1141
|
+
id: 'working-extension',
|
|
1142
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1143
|
+
connector: (node, get) =>
|
|
1144
|
+
Effect.succeed([{ id: 'child-from-working', type: EXAMPLE_TYPE, data: 'success' }]),
|
|
1145
|
+
}),
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
GraphBuilder.addExtension(builder, failingExtensions);
|
|
1149
|
+
GraphBuilder.addExtension(builder, workingExtensions);
|
|
1150
|
+
|
|
1151
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1152
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1153
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1154
|
+
await GraphBuilder.flush(builder);
|
|
1155
|
+
|
|
1156
|
+
// The working extension should still produce its node.
|
|
1157
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1158
|
+
expect(connections).has.length(1);
|
|
1159
|
+
expect(connections[0].id).to.equal('parent/child-from-working');
|
|
1160
|
+
expect(connections[0].data).to.equal('success');
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
describe('createTypeExtension', () => {
|
|
1165
|
+
test('creates extension matching by schema type with inferred object type', async () => {
|
|
1166
|
+
const registry = Registry.make();
|
|
1167
|
+
const builder = GraphBuilder.make({ registry });
|
|
1168
|
+
const graph = builder.graph;
|
|
1169
|
+
|
|
1170
|
+
const extensions = Effect.runSync(
|
|
1171
|
+
GraphBuilder.createTypeExtension({
|
|
1172
|
+
id: 'type-extension',
|
|
1173
|
+
type: TestSchema.Person,
|
|
1174
|
+
connector: (object) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: object }]),
|
|
1175
|
+
}),
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1179
|
+
|
|
1180
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1181
|
+
const testObject = Obj.make(TestSchema.Person, { name: 'Test' });
|
|
1182
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: testObject });
|
|
1183
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1184
|
+
await GraphBuilder.flush(builder);
|
|
1185
|
+
|
|
1186
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1187
|
+
expect(connections).has.length(1);
|
|
1188
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
1189
|
+
expect(connections[0].data).to.equal(testObject);
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
describe('path-based ID qualification', () => {
|
|
1194
|
+
test('rejects segment IDs containing slash', async () => {
|
|
1195
|
+
const registry = Registry.make();
|
|
1196
|
+
const builder = GraphBuilder.make({ registry });
|
|
1197
|
+
GraphBuilder.addExtension(
|
|
1198
|
+
builder,
|
|
1199
|
+
GraphBuilder.createExtensionRaw({
|
|
1200
|
+
id: 'bad-connector',
|
|
1201
|
+
connector: () => Atom.make([{ id: 'foo/bar', type: EXAMPLE_TYPE, data: null }]),
|
|
1202
|
+
}),
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
expect(() => Graph.expand(builder.graph, Node.RootId, 'child')).toThrow(/must not contain/);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test('multi-level path qualification', async () => {
|
|
1209
|
+
const registry = Registry.make();
|
|
1210
|
+
const builder = GraphBuilder.make({ registry });
|
|
1211
|
+
GraphBuilder.addExtension(builder, [
|
|
1212
|
+
GraphBuilder.createExtensionRaw({
|
|
1213
|
+
id: 'level1',
|
|
1214
|
+
connector: (node) =>
|
|
1215
|
+
Atom.make((get) =>
|
|
1216
|
+
Function.pipe(
|
|
1217
|
+
get(node),
|
|
1218
|
+
Option.filter((n) => n.id === 'root'),
|
|
1219
|
+
Option.map(() => [{ id: 'A', type: EXAMPLE_TYPE, data: 'a' }]),
|
|
1220
|
+
Option.getOrElse(() => []),
|
|
1221
|
+
),
|
|
1222
|
+
),
|
|
1223
|
+
}),
|
|
1224
|
+
GraphBuilder.createExtensionRaw({
|
|
1225
|
+
id: 'level2',
|
|
1226
|
+
connector: (node) =>
|
|
1227
|
+
Atom.make((get) =>
|
|
1228
|
+
Function.pipe(
|
|
1229
|
+
get(node),
|
|
1230
|
+
Option.filter((n) => n.id === 'root/A'),
|
|
1231
|
+
Option.map(() => [{ id: 'B', type: EXAMPLE_TYPE, data: 'b' }]),
|
|
1232
|
+
Option.getOrElse(() => []),
|
|
1233
|
+
),
|
|
1234
|
+
),
|
|
1235
|
+
}),
|
|
1236
|
+
]);
|
|
1237
|
+
|
|
1238
|
+
const graph = builder.graph;
|
|
1239
|
+
|
|
1240
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1241
|
+
await GraphBuilder.flush(builder);
|
|
1242
|
+
|
|
1243
|
+
const level1 = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1244
|
+
expect(level1).has.length(1);
|
|
1245
|
+
expect(level1[0].id).to.equal('root/A');
|
|
1246
|
+
|
|
1247
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1248
|
+
await GraphBuilder.flush(builder);
|
|
1249
|
+
|
|
1250
|
+
const level2 = registry.get(graph.connections('root/A', 'child'));
|
|
1251
|
+
expect(level2).has.length(1);
|
|
1252
|
+
expect(level2[0].id).to.equal('root/A/B');
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test('inline nodes are recursively qualified', async () => {
|
|
1256
|
+
const registry = Registry.make();
|
|
1257
|
+
const builder = GraphBuilder.make({ registry });
|
|
1258
|
+
GraphBuilder.addExtension(
|
|
1259
|
+
builder,
|
|
1260
|
+
GraphBuilder.createExtensionRaw({
|
|
1261
|
+
id: 'inline-connector',
|
|
1262
|
+
connector: () =>
|
|
1263
|
+
Atom.make([
|
|
1264
|
+
{
|
|
1265
|
+
id: 'parent-node',
|
|
1266
|
+
type: EXAMPLE_TYPE,
|
|
1267
|
+
data: null,
|
|
1268
|
+
nodes: [
|
|
1269
|
+
{
|
|
1270
|
+
id: 'inline-child',
|
|
1271
|
+
type: EXAMPLE_TYPE,
|
|
1272
|
+
data: null,
|
|
1273
|
+
nodes: [{ id: 'deep-child', type: EXAMPLE_TYPE, data: null }],
|
|
1274
|
+
},
|
|
1275
|
+
],
|
|
1276
|
+
},
|
|
1277
|
+
]),
|
|
1278
|
+
}),
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
const graph = builder.graph;
|
|
1282
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1283
|
+
await GraphBuilder.flush(builder);
|
|
1284
|
+
|
|
1285
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1286
|
+
expect(connections).has.length(1);
|
|
1287
|
+
expect(connections[0].id).to.equal('root/parent-node');
|
|
1288
|
+
|
|
1289
|
+
const inlineNode = Graph.getNode(graph, 'root/parent-node/inline-child').pipe(Option.getOrNull);
|
|
1290
|
+
expect(inlineNode).to.not.be.null;
|
|
1291
|
+
expect(inlineNode?.id).to.equal('root/parent-node/inline-child');
|
|
1292
|
+
|
|
1293
|
+
const deepNode = Graph.getNode(graph, 'root/parent-node/inline-child/deep-child').pipe(Option.getOrNull);
|
|
1294
|
+
expect(deepNode).to.not.be.null;
|
|
1295
|
+
expect(deepNode?.id).to.equal('root/parent-node/inline-child/deep-child');
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test('constant connector produces distinct nodes under different parents', async () => {
|
|
1299
|
+
const registry = Registry.make();
|
|
1300
|
+
const builder = GraphBuilder.make({ registry });
|
|
1301
|
+
|
|
1302
|
+
GraphBuilder.addExtension(builder, [
|
|
1303
|
+
GraphBuilder.createExtensionRaw({
|
|
1304
|
+
id: 'parents',
|
|
1305
|
+
connector: (node) =>
|
|
1306
|
+
Atom.make((get) =>
|
|
1307
|
+
Function.pipe(
|
|
1308
|
+
get(node),
|
|
1309
|
+
Option.filter((n) => n.id === 'root'),
|
|
1310
|
+
Option.map(() => [
|
|
1311
|
+
{ id: 'A', type: EXAMPLE_TYPE, data: 'a' },
|
|
1312
|
+
{ id: 'B', type: EXAMPLE_TYPE, data: 'b' },
|
|
1313
|
+
]),
|
|
1314
|
+
Option.getOrElse(() => []),
|
|
1315
|
+
),
|
|
1316
|
+
),
|
|
1317
|
+
}),
|
|
1318
|
+
GraphBuilder.createExtensionRaw({
|
|
1319
|
+
id: 'constant-child',
|
|
1320
|
+
connector: () => Atom.make([{ id: 'shared', type: EXAMPLE_TYPE, data: 'constant' }]),
|
|
1321
|
+
}),
|
|
1322
|
+
]);
|
|
1323
|
+
|
|
1324
|
+
const graph = builder.graph;
|
|
1325
|
+
|
|
1326
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1327
|
+
await GraphBuilder.flush(builder);
|
|
1328
|
+
|
|
1329
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1330
|
+
Graph.expand(graph, 'root/B', 'child');
|
|
1331
|
+
await GraphBuilder.flush(builder);
|
|
1332
|
+
|
|
1333
|
+
const childrenOfA = registry.get(graph.connections('root/A', 'child'));
|
|
1334
|
+
const childrenOfB = registry.get(graph.connections('root/B', 'child'));
|
|
1335
|
+
|
|
1336
|
+
expect(childrenOfA).has.length(1);
|
|
1337
|
+
expect(childrenOfB).has.length(1);
|
|
1338
|
+
expect(childrenOfA[0].id).to.equal('root/A/shared');
|
|
1339
|
+
expect(childrenOfB[0].id).to.equal('root/B/shared');
|
|
1340
|
+
|
|
1341
|
+
const nodeA = Graph.getNode(graph, 'root/A/shared').pipe(Option.getOrNull);
|
|
1342
|
+
const nodeB = Graph.getNode(graph, 'root/B/shared').pipe(Option.getOrNull);
|
|
1343
|
+
expect(nodeA).to.not.be.null;
|
|
1344
|
+
expect(nodeB).to.not.be.null;
|
|
1345
|
+
expect(nodeA?.id).to.not.equal(nodeB?.id);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test('explore qualifies node IDs', async () => {
|
|
1349
|
+
const builder = GraphBuilder.make();
|
|
1350
|
+
GraphBuilder.addExtension(
|
|
1351
|
+
builder,
|
|
1352
|
+
GraphBuilder.createExtensionRaw({
|
|
1353
|
+
id: 'connector',
|
|
1354
|
+
connector: (node) =>
|
|
1355
|
+
Atom.make((get) =>
|
|
1356
|
+
Function.pipe(
|
|
1357
|
+
get(node),
|
|
1358
|
+
Option.filter((n) => n.id === 'root'),
|
|
1359
|
+
Option.map(() => [
|
|
1360
|
+
{ id: 'first', type: EXAMPLE_TYPE, data: 1 },
|
|
1361
|
+
{ id: 'second', type: EXAMPLE_TYPE, data: 2 },
|
|
1362
|
+
]),
|
|
1363
|
+
Option.getOrElse(() => []),
|
|
1364
|
+
),
|
|
1365
|
+
),
|
|
1366
|
+
}),
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
const visited: Array<{ id: string; path: string[] }> = [];
|
|
1370
|
+
await GraphBuilder.explore(builder, {
|
|
1371
|
+
relation: 'child',
|
|
1372
|
+
visitor: (node, path) => {
|
|
1373
|
+
visited.push({ id: node.id, path });
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
expect(visited).has.length(3);
|
|
1378
|
+
expect(visited[0].id).to.equal('root');
|
|
1379
|
+
expect(visited[1].id).to.equal('root/first');
|
|
1380
|
+
expect(visited[2].id).to.equal('root/second');
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
500
1383
|
});
|