@dxos/app-graph 0.8.4-main.ef1bc66f44 → 0.8.4-main.f466a3d56e
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/{browser/index.mjs → neutral/chunk-32XXJE6M.mjs} +564 -384
- package/dist/lib/neutral/chunk-32XXJE6M.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +40 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/scheduler.mjs +15 -0
- package/dist/lib/neutral/scheduler.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +40 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/atoms.d.ts.map +1 -1
- package/dist/types/src/graph-builder.d.ts +13 -9
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +13 -17
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +46 -20
- package/dist/types/src/node-matcher.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +21 -5
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/scheduler.browser.d.ts +2 -0
- package/dist/types/src/scheduler.browser.d.ts.map +1 -0
- package/dist/types/src/scheduler.d.ts +8 -0
- package/dist/types/src/scheduler.d.ts.map +1 -0
- 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 +40 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +38 -27
- package/src/graph-builder.test.ts +695 -102
- package/src/graph-builder.ts +223 -76
- package/src/graph.test.ts +187 -52
- package/src/graph.ts +177 -98
- package/src/index.ts +1 -0
- package/src/node-matcher.ts +61 -31
- package/src/node.ts +46 -5
- package/src/scheduler.browser.ts +5 -0
- package/src/scheduler.ts +17 -0
- package/src/stories/EchoGraph.stories.tsx +94 -66
- package/src/stories/Tree.tsx +1 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +101 -0
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -1298
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
|
@@ -9,7 +9,7 @@ import * as Function from 'effect/Function';
|
|
|
9
9
|
import * as Option from 'effect/Option';
|
|
10
10
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
11
11
|
|
|
12
|
-
import { Trigger
|
|
12
|
+
import { Trigger } from '@dxos/async';
|
|
13
13
|
import { Obj } from '@dxos/echo';
|
|
14
14
|
import { TestSchema } from '@dxos/echo/testing';
|
|
15
15
|
|
|
@@ -17,10 +17,11 @@ import * as Graph from './graph';
|
|
|
17
17
|
import * as GraphBuilder from './graph-builder';
|
|
18
18
|
import * as Node from './node';
|
|
19
19
|
import * as NodeMatcher from './node-matcher';
|
|
20
|
+
import { qualifyId } from './util';
|
|
20
21
|
|
|
21
22
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
22
23
|
const EXAMPLE_ID = exampleId(1);
|
|
23
|
-
const EXAMPLE_TYPE = 'dxos.
|
|
24
|
+
const EXAMPLE_TYPE = 'org.dxos.type.example';
|
|
24
25
|
|
|
25
26
|
describe('GraphBuilder', () => {
|
|
26
27
|
describe('resolver', () => {
|
|
@@ -39,10 +40,7 @@ describe('GraphBuilder', () => {
|
|
|
39
40
|
builder,
|
|
40
41
|
GraphBuilder.createExtensionRaw({
|
|
41
42
|
id: 'resolver',
|
|
42
|
-
resolver: () => {
|
|
43
|
-
console.log('resolver');
|
|
44
|
-
return Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
45
|
-
},
|
|
43
|
+
resolver: () => Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }),
|
|
46
44
|
}),
|
|
47
45
|
);
|
|
48
46
|
await Graph.initialize(graph, EXAMPLE_ID);
|
|
@@ -81,10 +79,191 @@ describe('GraphBuilder', () => {
|
|
|
81
79
|
expect(node?.data).to.equal('updated');
|
|
82
80
|
}
|
|
83
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
|
+
});
|
|
84
263
|
});
|
|
85
264
|
|
|
86
265
|
describe('connector', () => {
|
|
87
|
-
test('works', () => {
|
|
266
|
+
test('works', async () => {
|
|
88
267
|
const registry = Registry.make();
|
|
89
268
|
const builder = GraphBuilder.make({ registry });
|
|
90
269
|
GraphBuilder.addExtension(
|
|
@@ -98,27 +277,28 @@ describe('GraphBuilder', () => {
|
|
|
98
277
|
builder,
|
|
99
278
|
GraphBuilder.createExtensionRaw({
|
|
100
279
|
id: 'inbound-connector',
|
|
101
|
-
relation: 'inbound',
|
|
280
|
+
relation: Node.childRelation('inbound'),
|
|
102
281
|
connector: () => Atom.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
103
282
|
}),
|
|
104
283
|
);
|
|
105
284
|
|
|
106
285
|
const graph = builder.graph;
|
|
107
|
-
Graph.expand(graph, Node.RootId);
|
|
108
|
-
Graph.expand(graph, Node.RootId, 'inbound');
|
|
286
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
287
|
+
Graph.expand(graph, Node.RootId, Node.childRelation('inbound'));
|
|
288
|
+
await GraphBuilder.flush(builder);
|
|
109
289
|
|
|
110
|
-
const outbound = registry.get(graph.connections(Node.RootId));
|
|
111
|
-
const inbound = registry.get(graph.connections(Node.RootId, 'inbound'));
|
|
290
|
+
const outbound = registry.get(graph.connections(Node.RootId, 'child'));
|
|
291
|
+
const inbound = registry.get(graph.connections(Node.RootId, Node.childRelation('inbound')));
|
|
112
292
|
|
|
113
293
|
expect(outbound).has.length(1);
|
|
114
|
-
expect(outbound[0].id).to.equal('child');
|
|
294
|
+
expect(outbound[0].id).to.equal('root/child');
|
|
115
295
|
expect(outbound[0].data).to.equal(2);
|
|
116
296
|
expect(inbound).has.length(1);
|
|
117
|
-
expect(inbound[0].id).to.equal('parent');
|
|
297
|
+
expect(inbound[0].id).to.equal('root/parent');
|
|
118
298
|
expect(inbound[0].data).to.equal(0);
|
|
119
299
|
});
|
|
120
300
|
|
|
121
|
-
test('updates', () => {
|
|
301
|
+
test('updates', async () => {
|
|
122
302
|
const registry = Registry.make();
|
|
123
303
|
const builder = GraphBuilder.make({ registry });
|
|
124
304
|
const state = Atom.make(0);
|
|
@@ -130,21 +310,23 @@ describe('GraphBuilder', () => {
|
|
|
130
310
|
}),
|
|
131
311
|
);
|
|
132
312
|
const graph = builder.graph;
|
|
133
|
-
Graph.expand(graph, Node.RootId);
|
|
313
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
314
|
+
await GraphBuilder.flush(builder);
|
|
134
315
|
|
|
135
316
|
{
|
|
136
|
-
const [node] = registry.get(graph.connections(Node.RootId));
|
|
317
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
137
318
|
expect(node.data).to.equal(0);
|
|
138
319
|
}
|
|
139
320
|
|
|
140
321
|
{
|
|
141
322
|
registry.set(state, 1);
|
|
142
|
-
|
|
323
|
+
await GraphBuilder.flush(builder);
|
|
324
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
143
325
|
expect(node.data).to.equal(1);
|
|
144
326
|
}
|
|
145
327
|
});
|
|
146
328
|
|
|
147
|
-
test('subscribes to updates', () => {
|
|
329
|
+
test('subscribes to updates', async () => {
|
|
148
330
|
const registry = Registry.make();
|
|
149
331
|
const builder = GraphBuilder.make({ registry });
|
|
150
332
|
const state = Atom.make(0);
|
|
@@ -158,22 +340,25 @@ describe('GraphBuilder', () => {
|
|
|
158
340
|
const graph = builder.graph;
|
|
159
341
|
|
|
160
342
|
let count = 0;
|
|
161
|
-
const cancel = registry.subscribe(graph.connections(Node.RootId), (_) => {
|
|
343
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_) => {
|
|
162
344
|
count++;
|
|
163
345
|
});
|
|
164
346
|
onTestFinished(() => cancel());
|
|
165
347
|
|
|
166
348
|
expect(count).to.equal(0);
|
|
167
|
-
expect(registry.get(graph.connections(Node.RootId))).to.have.length(0);
|
|
349
|
+
expect(registry.get(graph.connections(Node.RootId, 'child'))).to.have.length(0);
|
|
168
350
|
expect(count).to.equal(1);
|
|
169
351
|
|
|
170
|
-
Graph.expand(graph, Node.RootId);
|
|
352
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
353
|
+
await GraphBuilder.flush(builder);
|
|
171
354
|
expect(count).to.equal(2);
|
|
355
|
+
|
|
172
356
|
registry.set(state, 1);
|
|
357
|
+
await GraphBuilder.flush(builder);
|
|
173
358
|
expect(count).to.equal(3);
|
|
174
359
|
});
|
|
175
360
|
|
|
176
|
-
test('updates with new extensions', () => {
|
|
361
|
+
test('updates with new extensions', async () => {
|
|
177
362
|
const registry = Registry.make();
|
|
178
363
|
const builder = GraphBuilder.make({ registry });
|
|
179
364
|
GraphBuilder.addExtension(
|
|
@@ -184,11 +369,12 @@ describe('GraphBuilder', () => {
|
|
|
184
369
|
}),
|
|
185
370
|
);
|
|
186
371
|
const graph = builder.graph;
|
|
187
|
-
Graph.expand(graph, Node.RootId);
|
|
372
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
373
|
+
await GraphBuilder.flush(builder);
|
|
188
374
|
|
|
189
375
|
let nodes: Node.Node[] = [];
|
|
190
376
|
let count = 0;
|
|
191
|
-
const cancel = registry.subscribe(graph.connections(Node.RootId), (_nodes) => {
|
|
377
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_nodes) => {
|
|
192
378
|
count++;
|
|
193
379
|
nodes = _nodes;
|
|
194
380
|
});
|
|
@@ -196,7 +382,7 @@ describe('GraphBuilder', () => {
|
|
|
196
382
|
|
|
197
383
|
expect(nodes).has.length(0);
|
|
198
384
|
expect(count).to.equal(0);
|
|
199
|
-
registry.get(graph.connections(Node.RootId));
|
|
385
|
+
registry.get(graph.connections(Node.RootId, 'child'));
|
|
200
386
|
expect(nodes).has.length(1);
|
|
201
387
|
expect(count).to.equal(1);
|
|
202
388
|
|
|
@@ -207,11 +393,12 @@ describe('GraphBuilder', () => {
|
|
|
207
393
|
connector: () => Atom.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
208
394
|
}),
|
|
209
395
|
);
|
|
396
|
+
await GraphBuilder.flush(builder);
|
|
210
397
|
expect(nodes).has.length(2);
|
|
211
398
|
expect(count).to.equal(2);
|
|
212
399
|
});
|
|
213
400
|
|
|
214
|
-
test('removes', () => {
|
|
401
|
+
test('removes', async () => {
|
|
215
402
|
const registry = Registry.make();
|
|
216
403
|
const builder = GraphBuilder.make({ registry });
|
|
217
404
|
const nodes = Atom.make([
|
|
@@ -226,25 +413,27 @@ describe('GraphBuilder', () => {
|
|
|
226
413
|
}),
|
|
227
414
|
);
|
|
228
415
|
const graph = builder.graph;
|
|
229
|
-
Graph.expand(graph, Node.RootId);
|
|
416
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
417
|
+
await GraphBuilder.flush(builder);
|
|
230
418
|
|
|
231
419
|
{
|
|
232
|
-
const nodes = registry.get(graph.connections(Node.RootId));
|
|
420
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
233
421
|
expect(nodes).has.length(2);
|
|
234
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
235
|
-
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)));
|
|
236
424
|
}
|
|
237
425
|
|
|
238
426
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
427
|
+
await GraphBuilder.flush(builder);
|
|
239
428
|
|
|
240
429
|
{
|
|
241
|
-
const nodes = registry.get(graph.connections(Node.RootId));
|
|
430
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
242
431
|
expect(nodes).has.length(1);
|
|
243
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
432
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
244
433
|
}
|
|
245
434
|
});
|
|
246
435
|
|
|
247
|
-
test('nodes are updated when removed', () => {
|
|
436
|
+
test('nodes are updated when removed', async () => {
|
|
248
437
|
const registry = Registry.make();
|
|
249
438
|
const builder = GraphBuilder.make({ registry });
|
|
250
439
|
const name = Atom.make('removed');
|
|
@@ -269,29 +458,159 @@ describe('GraphBuilder', () => {
|
|
|
269
458
|
|
|
270
459
|
let count = 0;
|
|
271
460
|
let exists = false;
|
|
272
|
-
const cancel = registry.subscribe(graph.node(EXAMPLE_ID), (node) => {
|
|
461
|
+
const cancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (node) => {
|
|
273
462
|
count++;
|
|
274
463
|
exists = Option.isSome(node);
|
|
275
464
|
});
|
|
276
465
|
onTestFinished(() => cancel());
|
|
277
466
|
|
|
278
|
-
Graph.expand(graph, Node.RootId);
|
|
467
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
468
|
+
await GraphBuilder.flush(builder);
|
|
279
469
|
expect(count).to.equal(0);
|
|
280
470
|
expect(exists).to.be.false;
|
|
281
471
|
|
|
282
472
|
registry.set(name, 'default');
|
|
473
|
+
await GraphBuilder.flush(builder);
|
|
283
474
|
expect(count).to.equal(1);
|
|
284
475
|
expect(exists).to.be.true;
|
|
285
476
|
|
|
286
477
|
registry.set(name, 'removed');
|
|
478
|
+
await GraphBuilder.flush(builder);
|
|
287
479
|
expect(count).to.equal(2);
|
|
288
480
|
expect(exists).to.be.false;
|
|
289
481
|
|
|
290
482
|
registry.set(name, 'added');
|
|
483
|
+
await GraphBuilder.flush(builder);
|
|
291
484
|
expect(count).to.equal(3);
|
|
292
485
|
expect(exists).to.be.true;
|
|
293
486
|
});
|
|
294
487
|
|
|
488
|
+
describe('inline nodes', () => {
|
|
489
|
+
const parent = (child?: Node.NodeArg<any>): Node.NodeArg<any> => ({
|
|
490
|
+
id: 'parent-node',
|
|
491
|
+
type: EXAMPLE_TYPE,
|
|
492
|
+
data: null,
|
|
493
|
+
nodes: child ? [child] : [],
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const inlineChild = (overrides?: Partial<Node.NodeArg<any>>): Node.NodeArg<any> => ({
|
|
497
|
+
id: 'inline-child',
|
|
498
|
+
type: EXAMPLE_TYPE,
|
|
499
|
+
data: null,
|
|
500
|
+
...overrides,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const makeGraph = () => {
|
|
504
|
+
const registry = Registry.make();
|
|
505
|
+
const builder = GraphBuilder.make({ registry });
|
|
506
|
+
const nodesAtom = Atom.make<Node.NodeArg<any>[]>([]);
|
|
507
|
+
GraphBuilder.addExtension(
|
|
508
|
+
builder,
|
|
509
|
+
GraphBuilder.createExtensionRaw({
|
|
510
|
+
id: 'inline-connector',
|
|
511
|
+
connector: () => Atom.make((get) => get(nodesAtom)),
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
const graph = builder.graph;
|
|
515
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
516
|
+
return { registry, builder, graph, nodesAtom };
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const getInlineChild = (graph: Graph.ExpandableGraph) =>
|
|
520
|
+
Graph.getNode(graph, 'root/parent-node/inline-child').pipe(Option.getOrNull);
|
|
521
|
+
|
|
522
|
+
test('are removed when connector re-runs with data change', async () => {
|
|
523
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
524
|
+
registry.set(nodesAtom, [parent(inlineChild())]);
|
|
525
|
+
await GraphBuilder.flush(builder);
|
|
526
|
+
|
|
527
|
+
expect(getInlineChild(graph)).to.not.be.null;
|
|
528
|
+
|
|
529
|
+
// Remove inline child while also changing parent data to trigger the update.
|
|
530
|
+
registry.set(nodesAtom, [{ ...parent(), data: 'v2' }]);
|
|
531
|
+
await GraphBuilder.flush(builder);
|
|
532
|
+
|
|
533
|
+
expect(getInlineChild(graph)).to.be.null;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('are removed when only inline children change', async () => {
|
|
537
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
538
|
+
registry.set(nodesAtom, [parent(inlineChild())]);
|
|
539
|
+
await GraphBuilder.flush(builder);
|
|
540
|
+
|
|
541
|
+
expect(getInlineChild(graph)).to.not.be.null;
|
|
542
|
+
|
|
543
|
+
// Remove inline child without touching parent — tests change detection covers inline children.
|
|
544
|
+
registry.set(nodesAtom, [parent()]);
|
|
545
|
+
await GraphBuilder.flush(builder);
|
|
546
|
+
|
|
547
|
+
expect(getInlineChild(graph)).to.be.null;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('are added when connector re-runs', async () => {
|
|
551
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
552
|
+
registry.set(nodesAtom, [parent()]);
|
|
553
|
+
await GraphBuilder.flush(builder);
|
|
554
|
+
|
|
555
|
+
expect(getInlineChild(graph)).to.be.null;
|
|
556
|
+
|
|
557
|
+
registry.set(nodesAtom, [parent(inlineChild())]);
|
|
558
|
+
await GraphBuilder.flush(builder);
|
|
559
|
+
|
|
560
|
+
expect(getInlineChild(graph)).to.not.be.null;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('reactively update data', async ({ expect }) => {
|
|
564
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
565
|
+
registry.set(nodesAtom, [parent(inlineChild({ data: 'v1' }))]);
|
|
566
|
+
await GraphBuilder.flush(builder);
|
|
567
|
+
|
|
568
|
+
expect(getInlineChild(graph)?.data).to.equal('v1');
|
|
569
|
+
|
|
570
|
+
// Change only the inline child's data — parent is unchanged.
|
|
571
|
+
registry.set(nodesAtom, [parent(inlineChild({ data: 'v2' }))]);
|
|
572
|
+
await GraphBuilder.flush(builder);
|
|
573
|
+
|
|
574
|
+
expect(getInlineChild(graph)?.data).to.equal('v2');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('reactively update properties', async ({ expect }) => {
|
|
578
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
579
|
+
registry.set(nodesAtom, [parent(inlineChild({ properties: { label: 'before' } }))]);
|
|
580
|
+
await GraphBuilder.flush(builder);
|
|
581
|
+
|
|
582
|
+
expect(getInlineChild(graph)?.properties.label).to.equal('before');
|
|
583
|
+
|
|
584
|
+
registry.set(nodesAtom, [parent(inlineChild({ properties: { label: 'after' } }))]);
|
|
585
|
+
await GraphBuilder.flush(builder);
|
|
586
|
+
|
|
587
|
+
expect(getInlineChild(graph)?.properties.label).to.equal('after');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('deeply nested inline nodes reactively update', async ({ expect }) => {
|
|
591
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
592
|
+
|
|
593
|
+
const withGrandchild = (data: string) =>
|
|
594
|
+
parent({
|
|
595
|
+
id: 'child',
|
|
596
|
+
type: EXAMPLE_TYPE,
|
|
597
|
+
data: null,
|
|
598
|
+
nodes: [{ id: 'grandchild', type: EXAMPLE_TYPE, data }],
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
registry.set(nodesAtom, [withGrandchild('v1')]);
|
|
602
|
+
await GraphBuilder.flush(builder);
|
|
603
|
+
|
|
604
|
+
expect(Graph.getNode(graph, 'root/parent-node/child/grandchild').pipe(Option.getOrNull)?.data).to.equal('v1');
|
|
605
|
+
|
|
606
|
+
// Change only the grandchild's data — all ancestors unchanged.
|
|
607
|
+
registry.set(nodesAtom, [withGrandchild('v2')]);
|
|
608
|
+
await GraphBuilder.flush(builder);
|
|
609
|
+
|
|
610
|
+
expect(Graph.getNode(graph, 'root/parent-node/child/grandchild').pipe(Option.getOrNull)?.data).to.equal('v2');
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
295
614
|
test('sort edges', async () => {
|
|
296
615
|
const registry = Registry.make();
|
|
297
616
|
const builder = GraphBuilder.make({ registry });
|
|
@@ -308,14 +627,15 @@ describe('GraphBuilder', () => {
|
|
|
308
627
|
}),
|
|
309
628
|
);
|
|
310
629
|
const graph = builder.graph;
|
|
311
|
-
Graph.expand(graph, Node.RootId);
|
|
630
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
631
|
+
await GraphBuilder.flush(builder);
|
|
312
632
|
|
|
313
633
|
{
|
|
314
|
-
const nodes = registry.get(graph.connections(Node.RootId));
|
|
634
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
315
635
|
expect(nodes).has.length(3);
|
|
316
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
317
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
318
|
-
expect(nodes[2].id).to.equal(exampleId(3));
|
|
636
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(1)));
|
|
637
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(2)));
|
|
638
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(3)));
|
|
319
639
|
}
|
|
320
640
|
|
|
321
641
|
registry.set(nodes, [
|
|
@@ -323,20 +643,18 @@ describe('GraphBuilder', () => {
|
|
|
323
643
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
324
644
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
325
645
|
]);
|
|
326
|
-
|
|
327
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
328
|
-
await sleep(0);
|
|
646
|
+
await GraphBuilder.flush(builder);
|
|
329
647
|
|
|
330
648
|
{
|
|
331
|
-
const nodes = registry.get(graph.connections(Node.RootId));
|
|
649
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
332
650
|
expect(nodes).has.length(3);
|
|
333
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
334
|
-
expect(nodes[1].id).to.equal(exampleId(1));
|
|
335
|
-
expect(nodes[2].id).to.equal(exampleId(2));
|
|
651
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
652
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(1)));
|
|
653
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(2)));
|
|
336
654
|
}
|
|
337
655
|
});
|
|
338
656
|
|
|
339
|
-
test('updates are constrained', () => {
|
|
657
|
+
test('updates are constrained', async () => {
|
|
340
658
|
const registry = Registry.make();
|
|
341
659
|
const builder = GraphBuilder.make({ registry });
|
|
342
660
|
const name = Atom.make('default');
|
|
@@ -362,7 +680,9 @@ describe('GraphBuilder', () => {
|
|
|
362
680
|
Atom.make((get) =>
|
|
363
681
|
Function.pipe(
|
|
364
682
|
get(node),
|
|
365
|
-
Option.flatMap((node) =>
|
|
683
|
+
Option.flatMap((node) =>
|
|
684
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
685
|
+
),
|
|
366
686
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
367
687
|
Option.getOrElse(() => []),
|
|
368
688
|
),
|
|
@@ -374,7 +694,9 @@ describe('GraphBuilder', () => {
|
|
|
374
694
|
Atom.make((get) =>
|
|
375
695
|
Function.pipe(
|
|
376
696
|
get(node),
|
|
377
|
-
Option.flatMap((node) =>
|
|
697
|
+
Option.flatMap((node) =>
|
|
698
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
699
|
+
),
|
|
378
700
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
379
701
|
Option.getOrElse(() => []),
|
|
380
702
|
),
|
|
@@ -385,43 +707,47 @@ describe('GraphBuilder', () => {
|
|
|
385
707
|
const graph = builder.graph;
|
|
386
708
|
|
|
387
709
|
let parentCount = 0;
|
|
388
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
710
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
389
711
|
parentCount++;
|
|
390
712
|
});
|
|
391
713
|
onTestFinished(() => parentCancel());
|
|
392
714
|
|
|
393
715
|
let independentCount = 0;
|
|
394
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
716
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
395
717
|
independentCount++;
|
|
396
718
|
});
|
|
397
719
|
onTestFinished(() => independentCancel());
|
|
398
720
|
|
|
399
721
|
let dependentCount = 0;
|
|
400
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
722
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
401
723
|
dependentCount++;
|
|
402
724
|
});
|
|
403
725
|
onTestFinished(() => dependentCancel());
|
|
404
726
|
|
|
405
727
|
// Counts should not increment until the node is expanded.
|
|
406
|
-
Graph.expand(graph, Node.RootId);
|
|
728
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
729
|
+
await GraphBuilder.flush(builder);
|
|
407
730
|
expect(parentCount).to.equal(1);
|
|
408
731
|
expect(independentCount).to.equal(0);
|
|
409
732
|
expect(dependentCount).to.equal(0);
|
|
410
733
|
|
|
411
734
|
// Counts should increment when the node is expanded.
|
|
412
|
-
Graph.expand(graph, EXAMPLE_ID);
|
|
735
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
736
|
+
await GraphBuilder.flush(builder);
|
|
413
737
|
expect(parentCount).to.equal(1);
|
|
414
738
|
expect(independentCount).to.equal(1);
|
|
415
739
|
expect(dependentCount).to.equal(1);
|
|
416
740
|
|
|
417
741
|
// Only dependent count should increment when the parent changes.
|
|
418
742
|
registry.set(name, 'updated');
|
|
743
|
+
await GraphBuilder.flush(builder);
|
|
419
744
|
expect(parentCount).to.equal(2);
|
|
420
745
|
expect(independentCount).to.equal(1);
|
|
421
746
|
expect(dependentCount).to.equal(2);
|
|
422
747
|
|
|
423
748
|
// Only independent count should increment when its state changes.
|
|
424
749
|
registry.set(sub, 'updated');
|
|
750
|
+
await GraphBuilder.flush(builder);
|
|
425
751
|
expect(parentCount).to.equal(2);
|
|
426
752
|
expect(independentCount).to.equal(2);
|
|
427
753
|
expect(dependentCount).to.equal(2);
|
|
@@ -431,18 +757,21 @@ describe('GraphBuilder', () => {
|
|
|
431
757
|
registry.set(name, 'removed');
|
|
432
758
|
registry.set(sub, 'batch');
|
|
433
759
|
});
|
|
760
|
+
await GraphBuilder.flush(builder);
|
|
434
761
|
expect(parentCount).to.equal(2);
|
|
435
762
|
expect(independentCount).to.equal(3);
|
|
436
763
|
expect(dependentCount).to.equal(2);
|
|
437
764
|
|
|
438
765
|
// Dependent count should increment when the node is added back.
|
|
439
766
|
registry.set(name, 'added');
|
|
767
|
+
await GraphBuilder.flush(builder);
|
|
440
768
|
expect(parentCount).to.equal(3);
|
|
441
769
|
expect(independentCount).to.equal(3);
|
|
442
770
|
expect(dependentCount).to.equal(3);
|
|
443
771
|
|
|
444
772
|
// Counts should not increment when the node is expanded again.
|
|
445
|
-
Graph.expand(graph, EXAMPLE_ID);
|
|
773
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
774
|
+
await GraphBuilder.flush(builder);
|
|
446
775
|
expect(parentCount).to.equal(3);
|
|
447
776
|
expect(independentCount).to.equal(3);
|
|
448
777
|
expect(dependentCount).to.equal(3);
|
|
@@ -472,14 +801,14 @@ describe('GraphBuilder', () => {
|
|
|
472
801
|
let count = 0;
|
|
473
802
|
const trigger = new Trigger();
|
|
474
803
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
475
|
-
Graph.expand(builder.graph, id);
|
|
804
|
+
Graph.expand(builder.graph, id, 'child');
|
|
476
805
|
count++;
|
|
477
806
|
if (count === 5) {
|
|
478
807
|
trigger.wake();
|
|
479
808
|
}
|
|
480
809
|
});
|
|
481
810
|
|
|
482
|
-
Graph.expand(builder.graph, Node.RootId);
|
|
811
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
483
812
|
await trigger.wait();
|
|
484
813
|
expect(count).to.equal(5);
|
|
485
814
|
});
|
|
@@ -507,6 +836,7 @@ describe('GraphBuilder', () => {
|
|
|
507
836
|
|
|
508
837
|
let count = 0;
|
|
509
838
|
await GraphBuilder.explore(builder, {
|
|
839
|
+
relation: 'child',
|
|
510
840
|
visitor: () => {
|
|
511
841
|
count++;
|
|
512
842
|
},
|
|
@@ -518,7 +848,7 @@ describe('GraphBuilder', () => {
|
|
|
518
848
|
|
|
519
849
|
describe('helpers', () => {
|
|
520
850
|
describe('createConnector', () => {
|
|
521
|
-
test('creates connector with type inference', () => {
|
|
851
|
+
test('creates connector with type inference', async () => {
|
|
522
852
|
const registry = Registry.make();
|
|
523
853
|
const builder = GraphBuilder.make({ registry });
|
|
524
854
|
const graph = builder.graph;
|
|
@@ -536,16 +866,17 @@ describe('GraphBuilder', () => {
|
|
|
536
866
|
}),
|
|
537
867
|
);
|
|
538
868
|
|
|
539
|
-
Graph.expand(graph, Node.RootId);
|
|
869
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
870
|
+
await GraphBuilder.flush(builder);
|
|
540
871
|
|
|
541
|
-
const connections = registry.get(graph.connections(Node.RootId));
|
|
872
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
542
873
|
expect(connections).has.length(1);
|
|
543
|
-
expect(connections[0].id).to.equal('child');
|
|
874
|
+
expect(connections[0].id).to.equal('root/child');
|
|
544
875
|
});
|
|
545
876
|
});
|
|
546
877
|
|
|
547
878
|
describe('createExtension', () => {
|
|
548
|
-
test('works with Effect connector', () => {
|
|
879
|
+
test('works with Effect connector', async () => {
|
|
549
880
|
const registry = Registry.make();
|
|
550
881
|
const builder = GraphBuilder.make({ registry });
|
|
551
882
|
const graph = builder.graph;
|
|
@@ -562,15 +893,16 @@ describe('GraphBuilder', () => {
|
|
|
562
893
|
|
|
563
894
|
const writableGraph = graph as Graph.WritableGraph;
|
|
564
895
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
565
|
-
Graph.expand(graph, 'parent');
|
|
896
|
+
Graph.expand(graph, 'parent', 'child');
|
|
897
|
+
await GraphBuilder.flush(builder);
|
|
566
898
|
|
|
567
|
-
const connections = registry.get(graph.connections('parent'));
|
|
899
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
568
900
|
expect(connections).has.length(1);
|
|
569
|
-
expect(connections[0].id).to.equal('child');
|
|
901
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
570
902
|
expect(connections[0].data).to.equal('test');
|
|
571
903
|
});
|
|
572
904
|
|
|
573
|
-
test('works with Effect actions', () => {
|
|
905
|
+
test('works with Effect actions', async () => {
|
|
574
906
|
const registry = Registry.make();
|
|
575
907
|
const builder = GraphBuilder.make({ registry });
|
|
576
908
|
const graph = builder.graph;
|
|
@@ -583,10 +915,7 @@ describe('GraphBuilder', () => {
|
|
|
583
915
|
Effect.succeed([
|
|
584
916
|
{
|
|
585
917
|
id: 'test-action',
|
|
586
|
-
data: () =>
|
|
587
|
-
Effect.sync(() => {
|
|
588
|
-
console.log('TestAction');
|
|
589
|
-
}),
|
|
918
|
+
data: () => Effect.void,
|
|
590
919
|
properties: { label: 'Test' },
|
|
591
920
|
},
|
|
592
921
|
]),
|
|
@@ -597,14 +926,83 @@ describe('GraphBuilder', () => {
|
|
|
597
926
|
|
|
598
927
|
const writableGraph = graph as Graph.WritableGraph;
|
|
599
928
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
600
|
-
Graph.expand(graph, 'parent');
|
|
929
|
+
Graph.expand(graph, 'parent', 'child');
|
|
930
|
+
await GraphBuilder.flush(builder);
|
|
931
|
+
|
|
932
|
+
const edges = registry.get(graph.edges('parent'));
|
|
933
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.have.length(1);
|
|
934
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/test-action');
|
|
935
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.have.length(0);
|
|
936
|
+
const actions = registry.get(graph.actions('parent'));
|
|
937
|
+
expect(actions).has.length(1);
|
|
938
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test('actions expand automatically with child relation', async ({ expect }) => {
|
|
942
|
+
const registry = Registry.make();
|
|
943
|
+
const builder = GraphBuilder.make({ registry });
|
|
944
|
+
const graph = builder.graph;
|
|
945
|
+
|
|
946
|
+
const extensions = Effect.runSync(
|
|
947
|
+
GraphBuilder.createExtension({
|
|
948
|
+
id: 'test-extension',
|
|
949
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
950
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: 'c' }]),
|
|
951
|
+
actions: (node, get) =>
|
|
952
|
+
Effect.succeed([{ id: 'act1', data: () => Effect.void, properties: { label: 'A' } }]),
|
|
953
|
+
}),
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
957
|
+
|
|
958
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
959
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
960
|
+
Graph.expand(graph, 'parent', 'child');
|
|
961
|
+
await GraphBuilder.flush(builder);
|
|
962
|
+
|
|
963
|
+
const edges = registry.get(graph.edges('parent'));
|
|
964
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.include('parent/child');
|
|
965
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/act1');
|
|
966
|
+
const actions = registry.get(graph.actions('parent'));
|
|
967
|
+
expect(actions).has.length(1);
|
|
968
|
+
expect(actions[0].id).to.equal('parent/act1');
|
|
969
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
970
|
+
expect(connections).has.length(1);
|
|
971
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test('actions appear when extension registered after expand', async ({ expect }) => {
|
|
975
|
+
const registry = Registry.make();
|
|
976
|
+
const builder = GraphBuilder.make({ registry });
|
|
977
|
+
const graph = builder.graph;
|
|
978
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
979
|
+
|
|
980
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
981
|
+
Graph.expand(graph, 'parent', 'child');
|
|
982
|
+
await GraphBuilder.flush(builder);
|
|
983
|
+
|
|
984
|
+
expect(registry.get(graph.actions('parent'))).to.have.length(0);
|
|
985
|
+
|
|
986
|
+
const extensions = Effect.runSync(
|
|
987
|
+
GraphBuilder.createExtension({
|
|
988
|
+
id: 'late-extension',
|
|
989
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
990
|
+
actions: (node, get) =>
|
|
991
|
+
Effect.succeed([{ id: 'late-act', data: () => Effect.void, properties: { label: 'Late' } }]),
|
|
992
|
+
}),
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
996
|
+
await GraphBuilder.flush(builder);
|
|
601
997
|
|
|
998
|
+
const edges = registry.get(graph.edges('parent'));
|
|
999
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/late-act');
|
|
602
1000
|
const actions = registry.get(graph.actions('parent'));
|
|
603
1001
|
expect(actions).has.length(1);
|
|
604
|
-
expect(actions[0].id).to.equal('
|
|
1002
|
+
expect(actions[0].id).to.equal('parent/late-act');
|
|
605
1003
|
});
|
|
606
1004
|
|
|
607
|
-
test('_actionContext captures and provides services to action execution', () => {
|
|
1005
|
+
test('_actionContext captures and provides services to action execution', async () => {
|
|
608
1006
|
const registry = Registry.make();
|
|
609
1007
|
const builder = GraphBuilder.make({ registry });
|
|
610
1008
|
const graph = builder.graph;
|
|
@@ -648,7 +1046,8 @@ describe('GraphBuilder', () => {
|
|
|
648
1046
|
|
|
649
1047
|
const writableGraph = graph as Graph.WritableGraph;
|
|
650
1048
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
651
|
-
Graph.expand(graph, 'parent');
|
|
1049
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1050
|
+
await GraphBuilder.flush(builder);
|
|
652
1051
|
|
|
653
1052
|
const actions = registry.get(graph.actions('parent'));
|
|
654
1053
|
expect(actions).has.length(1);
|
|
@@ -691,7 +1090,7 @@ describe('GraphBuilder', () => {
|
|
|
691
1090
|
expect(node?.data).to.equal('resolved');
|
|
692
1091
|
});
|
|
693
1092
|
|
|
694
|
-
test('works with connector and actions together', () => {
|
|
1093
|
+
test('works with connector and actions together', async () => {
|
|
695
1094
|
const registry = Registry.make();
|
|
696
1095
|
const builder = GraphBuilder.make({ registry });
|
|
697
1096
|
const graph = builder.graph;
|
|
@@ -705,10 +1104,7 @@ describe('GraphBuilder', () => {
|
|
|
705
1104
|
Effect.succeed([
|
|
706
1105
|
{
|
|
707
1106
|
id: 'test-action',
|
|
708
|
-
data: () =>
|
|
709
|
-
Effect.sync(() => {
|
|
710
|
-
console.log('TestAction');
|
|
711
|
-
}),
|
|
1107
|
+
data: () => Effect.void,
|
|
712
1108
|
properties: { label: 'Test' },
|
|
713
1109
|
},
|
|
714
1110
|
]),
|
|
@@ -719,21 +1115,22 @@ describe('GraphBuilder', () => {
|
|
|
719
1115
|
|
|
720
1116
|
const writableGraph = graph as Graph.WritableGraph;
|
|
721
1117
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
722
|
-
Graph.expand(graph, 'parent');
|
|
1118
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1119
|
+
await GraphBuilder.flush(builder);
|
|
723
1120
|
|
|
724
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1121
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
725
1122
|
// Should have both the child node and the action node.
|
|
726
1123
|
expect(connections.length).to.be.greaterThanOrEqual(1);
|
|
727
|
-
const childNode = connections.find((n) => n.id === 'child');
|
|
1124
|
+
const childNode = connections.find((n) => n.id === 'parent/child');
|
|
728
1125
|
expect(childNode).to.not.be.undefined;
|
|
729
1126
|
expect(childNode?.data).to.equal('test');
|
|
730
1127
|
|
|
731
1128
|
const actions = registry.get(graph.actions('parent'));
|
|
732
1129
|
expect(actions).has.length(1);
|
|
733
|
-
expect(actions[0].id).to.equal('test-action');
|
|
1130
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
734
1131
|
});
|
|
735
1132
|
|
|
736
|
-
test('works with reactive connector using get context', () => {
|
|
1133
|
+
test('works with reactive connector using get context', async () => {
|
|
737
1134
|
const registry = Registry.make();
|
|
738
1135
|
const builder = GraphBuilder.make({ registry });
|
|
739
1136
|
const graph = builder.graph;
|
|
@@ -752,18 +1149,20 @@ describe('GraphBuilder', () => {
|
|
|
752
1149
|
|
|
753
1150
|
const writableGraph = graph as Graph.WritableGraph;
|
|
754
1151
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
755
|
-
Graph.expand(graph, 'parent');
|
|
1152
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1153
|
+
await GraphBuilder.flush(builder);
|
|
756
1154
|
|
|
757
1155
|
{
|
|
758
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1156
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
759
1157
|
expect(connections).has.length(1);
|
|
760
1158
|
expect(connections[0].data).to.equal('initial');
|
|
761
1159
|
}
|
|
762
1160
|
|
|
763
1161
|
registry.set(state, 'updated');
|
|
1162
|
+
await GraphBuilder.flush(builder);
|
|
764
1163
|
|
|
765
1164
|
{
|
|
766
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1165
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
767
1166
|
expect(connections).has.length(1);
|
|
768
1167
|
expect(connections[0].data).to.equal('updated');
|
|
769
1168
|
}
|
|
@@ -771,7 +1170,7 @@ describe('GraphBuilder', () => {
|
|
|
771
1170
|
});
|
|
772
1171
|
|
|
773
1172
|
describe('extension error handling', () => {
|
|
774
|
-
test('connector failure is caught and logged, returns empty array', () => {
|
|
1173
|
+
test('connector failure is caught and logged, returns empty array', async () => {
|
|
775
1174
|
const registry = Registry.make();
|
|
776
1175
|
const builder = GraphBuilder.make({ registry });
|
|
777
1176
|
const graph = builder.graph;
|
|
@@ -790,14 +1189,15 @@ describe('GraphBuilder', () => {
|
|
|
790
1189
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
791
1190
|
|
|
792
1191
|
// Should not throw, error is caught internally.
|
|
793
|
-
Graph.expand(graph, 'parent');
|
|
1192
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1193
|
+
await GraphBuilder.flush(builder);
|
|
794
1194
|
|
|
795
1195
|
// Should return empty connections since the connector failed.
|
|
796
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1196
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
797
1197
|
expect(connections).has.length(0);
|
|
798
1198
|
});
|
|
799
1199
|
|
|
800
|
-
test('actions failure is caught and logged, returns empty array', () => {
|
|
1200
|
+
test('actions failure is caught and logged, returns empty array', async () => {
|
|
801
1201
|
const registry = Registry.make();
|
|
802
1202
|
const builder = GraphBuilder.make({ registry });
|
|
803
1203
|
const graph = builder.graph;
|
|
@@ -816,7 +1216,8 @@ describe('GraphBuilder', () => {
|
|
|
816
1216
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
817
1217
|
|
|
818
1218
|
// Should not throw, error is caught internally.
|
|
819
|
-
Graph.expand(graph, 'parent');
|
|
1219
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1220
|
+
await GraphBuilder.flush(builder);
|
|
820
1221
|
|
|
821
1222
|
// Should return empty actions since the actions callback failed.
|
|
822
1223
|
const actions = registry.get(graph.actions('parent'));
|
|
@@ -846,7 +1247,7 @@ describe('GraphBuilder', () => {
|
|
|
846
1247
|
expect(node).to.be.null;
|
|
847
1248
|
});
|
|
848
1249
|
|
|
849
|
-
test('failing extension does not affect other extensions', () => {
|
|
1250
|
+
test('failing extension does not affect other extensions', async () => {
|
|
850
1251
|
const registry = Registry.make();
|
|
851
1252
|
const builder = GraphBuilder.make({ registry });
|
|
852
1253
|
const graph = builder.graph;
|
|
@@ -875,18 +1276,19 @@ describe('GraphBuilder', () => {
|
|
|
875
1276
|
|
|
876
1277
|
const writableGraph = graph as Graph.WritableGraph;
|
|
877
1278
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
878
|
-
Graph.expand(graph, 'parent');
|
|
1279
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1280
|
+
await GraphBuilder.flush(builder);
|
|
879
1281
|
|
|
880
1282
|
// The working extension should still produce its node.
|
|
881
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1283
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
882
1284
|
expect(connections).has.length(1);
|
|
883
|
-
expect(connections[0].id).to.equal('child-from-working');
|
|
1285
|
+
expect(connections[0].id).to.equal('parent/child-from-working');
|
|
884
1286
|
expect(connections[0].data).to.equal('success');
|
|
885
1287
|
});
|
|
886
1288
|
});
|
|
887
1289
|
|
|
888
1290
|
describe('createTypeExtension', () => {
|
|
889
|
-
test('creates extension matching by schema type with inferred object type', () => {
|
|
1291
|
+
test('creates extension matching by schema type with inferred object type', async () => {
|
|
890
1292
|
const registry = Registry.make();
|
|
891
1293
|
const builder = GraphBuilder.make({ registry });
|
|
892
1294
|
const graph = builder.graph;
|
|
@@ -904,13 +1306,204 @@ describe('GraphBuilder', () => {
|
|
|
904
1306
|
const writableGraph = graph as Graph.WritableGraph;
|
|
905
1307
|
const testObject = Obj.make(TestSchema.Person, { name: 'Test' });
|
|
906
1308
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: testObject });
|
|
907
|
-
Graph.expand(graph, 'parent');
|
|
1309
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1310
|
+
await GraphBuilder.flush(builder);
|
|
908
1311
|
|
|
909
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1312
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
910
1313
|
expect(connections).has.length(1);
|
|
911
|
-
expect(connections[0].id).to.equal('child');
|
|
1314
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
912
1315
|
expect(connections[0].data).to.equal(testObject);
|
|
913
1316
|
});
|
|
914
1317
|
});
|
|
915
1318
|
});
|
|
1319
|
+
describe('path-based ID qualification', () => {
|
|
1320
|
+
test('rejects segment IDs containing slash', async () => {
|
|
1321
|
+
const registry = Registry.make();
|
|
1322
|
+
const builder = GraphBuilder.make({ registry });
|
|
1323
|
+
GraphBuilder.addExtension(
|
|
1324
|
+
builder,
|
|
1325
|
+
GraphBuilder.createExtensionRaw({
|
|
1326
|
+
id: 'bad-connector',
|
|
1327
|
+
connector: () => Atom.make([{ id: 'foo/bar', type: EXAMPLE_TYPE, data: null }]),
|
|
1328
|
+
}),
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
expect(() => Graph.expand(builder.graph, Node.RootId, 'child')).toThrow(/must not contain/);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
test('multi-level path qualification', async () => {
|
|
1335
|
+
const registry = Registry.make();
|
|
1336
|
+
const builder = GraphBuilder.make({ registry });
|
|
1337
|
+
GraphBuilder.addExtension(builder, [
|
|
1338
|
+
GraphBuilder.createExtensionRaw({
|
|
1339
|
+
id: 'level1',
|
|
1340
|
+
connector: (node) =>
|
|
1341
|
+
Atom.make((get) =>
|
|
1342
|
+
Function.pipe(
|
|
1343
|
+
get(node),
|
|
1344
|
+
Option.filter((n) => n.id === 'root'),
|
|
1345
|
+
Option.map(() => [{ id: 'A', type: EXAMPLE_TYPE, data: 'a' }]),
|
|
1346
|
+
Option.getOrElse(() => []),
|
|
1347
|
+
),
|
|
1348
|
+
),
|
|
1349
|
+
}),
|
|
1350
|
+
GraphBuilder.createExtensionRaw({
|
|
1351
|
+
id: 'level2',
|
|
1352
|
+
connector: (node) =>
|
|
1353
|
+
Atom.make((get) =>
|
|
1354
|
+
Function.pipe(
|
|
1355
|
+
get(node),
|
|
1356
|
+
Option.filter((n) => n.id === 'root/A'),
|
|
1357
|
+
Option.map(() => [{ id: 'B', type: EXAMPLE_TYPE, data: 'b' }]),
|
|
1358
|
+
Option.getOrElse(() => []),
|
|
1359
|
+
),
|
|
1360
|
+
),
|
|
1361
|
+
}),
|
|
1362
|
+
]);
|
|
1363
|
+
|
|
1364
|
+
const graph = builder.graph;
|
|
1365
|
+
|
|
1366
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1367
|
+
await GraphBuilder.flush(builder);
|
|
1368
|
+
|
|
1369
|
+
const level1 = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1370
|
+
expect(level1).has.length(1);
|
|
1371
|
+
expect(level1[0].id).to.equal('root/A');
|
|
1372
|
+
|
|
1373
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1374
|
+
await GraphBuilder.flush(builder);
|
|
1375
|
+
|
|
1376
|
+
const level2 = registry.get(graph.connections('root/A', 'child'));
|
|
1377
|
+
expect(level2).has.length(1);
|
|
1378
|
+
expect(level2[0].id).to.equal('root/A/B');
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
test('inline nodes are recursively qualified', async () => {
|
|
1382
|
+
const registry = Registry.make();
|
|
1383
|
+
const builder = GraphBuilder.make({ registry });
|
|
1384
|
+
GraphBuilder.addExtension(
|
|
1385
|
+
builder,
|
|
1386
|
+
GraphBuilder.createExtensionRaw({
|
|
1387
|
+
id: 'inline-connector',
|
|
1388
|
+
connector: () =>
|
|
1389
|
+
Atom.make([
|
|
1390
|
+
{
|
|
1391
|
+
id: 'parent-node',
|
|
1392
|
+
type: EXAMPLE_TYPE,
|
|
1393
|
+
data: null,
|
|
1394
|
+
nodes: [
|
|
1395
|
+
{
|
|
1396
|
+
id: 'inline-child',
|
|
1397
|
+
type: EXAMPLE_TYPE,
|
|
1398
|
+
data: null,
|
|
1399
|
+
nodes: [{ id: 'deep-child', type: EXAMPLE_TYPE, data: null }],
|
|
1400
|
+
},
|
|
1401
|
+
],
|
|
1402
|
+
},
|
|
1403
|
+
]),
|
|
1404
|
+
}),
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
const graph = builder.graph;
|
|
1408
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1409
|
+
await GraphBuilder.flush(builder);
|
|
1410
|
+
|
|
1411
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1412
|
+
expect(connections).has.length(1);
|
|
1413
|
+
expect(connections[0].id).to.equal('root/parent-node');
|
|
1414
|
+
|
|
1415
|
+
const inlineNode = Graph.getNode(graph, 'root/parent-node/inline-child').pipe(Option.getOrNull);
|
|
1416
|
+
expect(inlineNode).to.not.be.null;
|
|
1417
|
+
expect(inlineNode?.id).to.equal('root/parent-node/inline-child');
|
|
1418
|
+
|
|
1419
|
+
const deepNode = Graph.getNode(graph, 'root/parent-node/inline-child/deep-child').pipe(Option.getOrNull);
|
|
1420
|
+
expect(deepNode).to.not.be.null;
|
|
1421
|
+
expect(deepNode?.id).to.equal('root/parent-node/inline-child/deep-child');
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test('constant connector produces distinct nodes under different parents', async () => {
|
|
1425
|
+
const registry = Registry.make();
|
|
1426
|
+
const builder = GraphBuilder.make({ registry });
|
|
1427
|
+
|
|
1428
|
+
GraphBuilder.addExtension(builder, [
|
|
1429
|
+
GraphBuilder.createExtensionRaw({
|
|
1430
|
+
id: 'parents',
|
|
1431
|
+
connector: (node) =>
|
|
1432
|
+
Atom.make((get) =>
|
|
1433
|
+
Function.pipe(
|
|
1434
|
+
get(node),
|
|
1435
|
+
Option.filter((n) => n.id === 'root'),
|
|
1436
|
+
Option.map(() => [
|
|
1437
|
+
{ id: 'A', type: EXAMPLE_TYPE, data: 'a' },
|
|
1438
|
+
{ id: 'B', type: EXAMPLE_TYPE, data: 'b' },
|
|
1439
|
+
]),
|
|
1440
|
+
Option.getOrElse(() => []),
|
|
1441
|
+
),
|
|
1442
|
+
),
|
|
1443
|
+
}),
|
|
1444
|
+
GraphBuilder.createExtensionRaw({
|
|
1445
|
+
id: 'constant-child',
|
|
1446
|
+
connector: () => Atom.make([{ id: 'shared', type: EXAMPLE_TYPE, data: 'constant' }]),
|
|
1447
|
+
}),
|
|
1448
|
+
]);
|
|
1449
|
+
|
|
1450
|
+
const graph = builder.graph;
|
|
1451
|
+
|
|
1452
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1453
|
+
await GraphBuilder.flush(builder);
|
|
1454
|
+
|
|
1455
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1456
|
+
Graph.expand(graph, 'root/B', 'child');
|
|
1457
|
+
await GraphBuilder.flush(builder);
|
|
1458
|
+
|
|
1459
|
+
const childrenOfA = registry.get(graph.connections('root/A', 'child'));
|
|
1460
|
+
const childrenOfB = registry.get(graph.connections('root/B', 'child'));
|
|
1461
|
+
|
|
1462
|
+
expect(childrenOfA).has.length(1);
|
|
1463
|
+
expect(childrenOfB).has.length(1);
|
|
1464
|
+
expect(childrenOfA[0].id).to.equal('root/A/shared');
|
|
1465
|
+
expect(childrenOfB[0].id).to.equal('root/B/shared');
|
|
1466
|
+
|
|
1467
|
+
const nodeA = Graph.getNode(graph, 'root/A/shared').pipe(Option.getOrNull);
|
|
1468
|
+
const nodeB = Graph.getNode(graph, 'root/B/shared').pipe(Option.getOrNull);
|
|
1469
|
+
expect(nodeA).to.not.be.null;
|
|
1470
|
+
expect(nodeB).to.not.be.null;
|
|
1471
|
+
expect(nodeA?.id).to.not.equal(nodeB?.id);
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
test('explore qualifies node IDs', async () => {
|
|
1475
|
+
const builder = GraphBuilder.make();
|
|
1476
|
+
GraphBuilder.addExtension(
|
|
1477
|
+
builder,
|
|
1478
|
+
GraphBuilder.createExtensionRaw({
|
|
1479
|
+
id: 'connector',
|
|
1480
|
+
connector: (node) =>
|
|
1481
|
+
Atom.make((get) =>
|
|
1482
|
+
Function.pipe(
|
|
1483
|
+
get(node),
|
|
1484
|
+
Option.filter((n) => n.id === 'root'),
|
|
1485
|
+
Option.map(() => [
|
|
1486
|
+
{ id: 'first', type: EXAMPLE_TYPE, data: 1 },
|
|
1487
|
+
{ id: 'second', type: EXAMPLE_TYPE, data: 2 },
|
|
1488
|
+
]),
|
|
1489
|
+
Option.getOrElse(() => []),
|
|
1490
|
+
),
|
|
1491
|
+
),
|
|
1492
|
+
}),
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
const visited: Array<{ id: string; path: string[] }> = [];
|
|
1496
|
+
await GraphBuilder.explore(builder, {
|
|
1497
|
+
relation: 'child',
|
|
1498
|
+
visitor: (node, path) => {
|
|
1499
|
+
visited.push({ id: node.id, path });
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
expect(visited).has.length(3);
|
|
1504
|
+
expect(visited[0].id).to.equal('root');
|
|
1505
|
+
expect(visited[1].id).to.equal('root/first');
|
|
1506
|
+
expect(visited[2].id).to.equal('root/second');
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
916
1509
|
});
|