@dxos/app-graph 0.8.4-main.c85a9c8dae → 0.8.4-main.d05539e30a
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} +304 -349
- 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 +3 -3
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +2 -2
- 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 +44 -18
- package/dist/types/src/node-matcher.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +11 -3
- 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 +26 -10
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +38 -27
- package/src/graph-builder.test.ts +536 -34
- package/src/graph-builder.ts +87 -24
- package/src/graph.test.ts +21 -1
- package/src/graph.ts +32 -21
- package/src/index.ts +1 -0
- package/src/node-matcher.ts +59 -27
- package/src/node.ts +32 -3
- package/src/scheduler.browser.ts +5 -0
- package/src/scheduler.ts +17 -0
- package/src/stories/EchoGraph.stories.tsx +13 -15
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +59 -13
- 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 -1523
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
|
@@ -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', () => {
|
|
@@ -78,6 +79,187 @@ describe('GraphBuilder', () => {
|
|
|
78
79
|
expect(node?.data).to.equal('updated');
|
|
79
80
|
}
|
|
80
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
|
+
});
|
|
81
263
|
});
|
|
82
264
|
|
|
83
265
|
describe('connector', () => {
|
|
@@ -109,10 +291,10 @@ describe('GraphBuilder', () => {
|
|
|
109
291
|
const inbound = registry.get(graph.connections(Node.RootId, Node.childRelation('inbound')));
|
|
110
292
|
|
|
111
293
|
expect(outbound).has.length(1);
|
|
112
|
-
expect(outbound[0].id).to.equal('child');
|
|
294
|
+
expect(outbound[0].id).to.equal('root/child');
|
|
113
295
|
expect(outbound[0].data).to.equal(2);
|
|
114
296
|
expect(inbound).has.length(1);
|
|
115
|
-
expect(inbound[0].id).to.equal('parent');
|
|
297
|
+
expect(inbound[0].id).to.equal('root/parent');
|
|
116
298
|
expect(inbound[0].data).to.equal(0);
|
|
117
299
|
});
|
|
118
300
|
|
|
@@ -237,8 +419,8 @@ describe('GraphBuilder', () => {
|
|
|
237
419
|
{
|
|
238
420
|
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
239
421
|
expect(nodes).has.length(2);
|
|
240
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
241
|
-
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)));
|
|
242
424
|
}
|
|
243
425
|
|
|
244
426
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
@@ -247,7 +429,7 @@ describe('GraphBuilder', () => {
|
|
|
247
429
|
{
|
|
248
430
|
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
249
431
|
expect(nodes).has.length(1);
|
|
250
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
432
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
251
433
|
}
|
|
252
434
|
});
|
|
253
435
|
|
|
@@ -276,7 +458,7 @@ describe('GraphBuilder', () => {
|
|
|
276
458
|
|
|
277
459
|
let count = 0;
|
|
278
460
|
let exists = false;
|
|
279
|
-
const cancel = registry.subscribe(graph.node(EXAMPLE_ID), (node) => {
|
|
461
|
+
const cancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (node) => {
|
|
280
462
|
count++;
|
|
281
463
|
exists = Option.isSome(node);
|
|
282
464
|
});
|
|
@@ -303,6 +485,132 @@ describe('GraphBuilder', () => {
|
|
|
303
485
|
expect(exists).to.be.true;
|
|
304
486
|
});
|
|
305
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
|
+
|
|
306
614
|
test('sort edges', async () => {
|
|
307
615
|
const registry = Registry.make();
|
|
308
616
|
const builder = GraphBuilder.make({ registry });
|
|
@@ -325,9 +633,9 @@ describe('GraphBuilder', () => {
|
|
|
325
633
|
{
|
|
326
634
|
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
327
635
|
expect(nodes).has.length(3);
|
|
328
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
329
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
330
|
-
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)));
|
|
331
639
|
}
|
|
332
640
|
|
|
333
641
|
registry.set(nodes, [
|
|
@@ -340,9 +648,9 @@ describe('GraphBuilder', () => {
|
|
|
340
648
|
{
|
|
341
649
|
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
342
650
|
expect(nodes).has.length(3);
|
|
343
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
344
|
-
expect(nodes[1].id).to.equal(exampleId(1));
|
|
345
|
-
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)));
|
|
346
654
|
}
|
|
347
655
|
});
|
|
348
656
|
|
|
@@ -372,7 +680,9 @@ describe('GraphBuilder', () => {
|
|
|
372
680
|
Atom.make((get) =>
|
|
373
681
|
Function.pipe(
|
|
374
682
|
get(node),
|
|
375
|
-
Option.flatMap((node) =>
|
|
683
|
+
Option.flatMap((node) =>
|
|
684
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
685
|
+
),
|
|
376
686
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
377
687
|
Option.getOrElse(() => []),
|
|
378
688
|
),
|
|
@@ -384,7 +694,9 @@ describe('GraphBuilder', () => {
|
|
|
384
694
|
Atom.make((get) =>
|
|
385
695
|
Function.pipe(
|
|
386
696
|
get(node),
|
|
387
|
-
Option.flatMap((node) =>
|
|
697
|
+
Option.flatMap((node) =>
|
|
698
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
699
|
+
),
|
|
388
700
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
389
701
|
Option.getOrElse(() => []),
|
|
390
702
|
),
|
|
@@ -395,19 +707,19 @@ describe('GraphBuilder', () => {
|
|
|
395
707
|
const graph = builder.graph;
|
|
396
708
|
|
|
397
709
|
let parentCount = 0;
|
|
398
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
710
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
399
711
|
parentCount++;
|
|
400
712
|
});
|
|
401
713
|
onTestFinished(() => parentCancel());
|
|
402
714
|
|
|
403
715
|
let independentCount = 0;
|
|
404
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
716
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
405
717
|
independentCount++;
|
|
406
718
|
});
|
|
407
719
|
onTestFinished(() => independentCancel());
|
|
408
720
|
|
|
409
721
|
let dependentCount = 0;
|
|
410
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
722
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
411
723
|
dependentCount++;
|
|
412
724
|
});
|
|
413
725
|
onTestFinished(() => dependentCancel());
|
|
@@ -420,7 +732,7 @@ describe('GraphBuilder', () => {
|
|
|
420
732
|
expect(dependentCount).to.equal(0);
|
|
421
733
|
|
|
422
734
|
// Counts should increment when the node is expanded.
|
|
423
|
-
Graph.expand(graph, EXAMPLE_ID, 'child');
|
|
735
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
424
736
|
await GraphBuilder.flush(builder);
|
|
425
737
|
expect(parentCount).to.equal(1);
|
|
426
738
|
expect(independentCount).to.equal(1);
|
|
@@ -458,7 +770,7 @@ describe('GraphBuilder', () => {
|
|
|
458
770
|
expect(dependentCount).to.equal(3);
|
|
459
771
|
|
|
460
772
|
// Counts should not increment when the node is expanded again.
|
|
461
|
-
Graph.expand(graph, EXAMPLE_ID, 'child');
|
|
773
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
462
774
|
await GraphBuilder.flush(builder);
|
|
463
775
|
expect(parentCount).to.equal(3);
|
|
464
776
|
expect(independentCount).to.equal(3);
|
|
@@ -559,7 +871,7 @@ describe('GraphBuilder', () => {
|
|
|
559
871
|
|
|
560
872
|
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
561
873
|
expect(connections).has.length(1);
|
|
562
|
-
expect(connections[0].id).to.equal('child');
|
|
874
|
+
expect(connections[0].id).to.equal('root/child');
|
|
563
875
|
});
|
|
564
876
|
});
|
|
565
877
|
|
|
@@ -586,7 +898,7 @@ describe('GraphBuilder', () => {
|
|
|
586
898
|
|
|
587
899
|
const connections = registry.get(graph.connections('parent', 'child'));
|
|
588
900
|
expect(connections).has.length(1);
|
|
589
|
-
expect(connections[0].id).to.equal('child');
|
|
901
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
590
902
|
expect(connections[0].data).to.equal('test');
|
|
591
903
|
});
|
|
592
904
|
|
|
@@ -619,11 +931,11 @@ describe('GraphBuilder', () => {
|
|
|
619
931
|
|
|
620
932
|
const edges = registry.get(graph.edges('parent'));
|
|
621
933
|
expect(edges[Graph.relationKey('action')] ?? []).to.have.length(1);
|
|
622
|
-
expect(edges[Graph.relationKey('action')] ?? []).to.include('test-action');
|
|
934
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/test-action');
|
|
623
935
|
expect(edges[Graph.relationKey('child')] ?? []).to.have.length(0);
|
|
624
936
|
const actions = registry.get(graph.actions('parent'));
|
|
625
937
|
expect(actions).has.length(1);
|
|
626
|
-
expect(actions[0].id).to.equal('test-action');
|
|
938
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
627
939
|
});
|
|
628
940
|
|
|
629
941
|
test('actions expand automatically with child relation', async ({ expect }) => {
|
|
@@ -649,14 +961,14 @@ describe('GraphBuilder', () => {
|
|
|
649
961
|
await GraphBuilder.flush(builder);
|
|
650
962
|
|
|
651
963
|
const edges = registry.get(graph.edges('parent'));
|
|
652
|
-
expect(edges[Graph.relationKey('child')] ?? []).to.include('child');
|
|
653
|
-
expect(edges[Graph.relationKey('action')] ?? []).to.include('act1');
|
|
964
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.include('parent/child');
|
|
965
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/act1');
|
|
654
966
|
const actions = registry.get(graph.actions('parent'));
|
|
655
967
|
expect(actions).has.length(1);
|
|
656
|
-
expect(actions[0].id).to.equal('act1');
|
|
968
|
+
expect(actions[0].id).to.equal('parent/act1');
|
|
657
969
|
const connections = registry.get(graph.connections('parent', 'child'));
|
|
658
970
|
expect(connections).has.length(1);
|
|
659
|
-
expect(connections[0].id).to.equal('child');
|
|
971
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
660
972
|
});
|
|
661
973
|
|
|
662
974
|
test('actions appear when extension registered after expand', async ({ expect }) => {
|
|
@@ -684,10 +996,10 @@ describe('GraphBuilder', () => {
|
|
|
684
996
|
await GraphBuilder.flush(builder);
|
|
685
997
|
|
|
686
998
|
const edges = registry.get(graph.edges('parent'));
|
|
687
|
-
expect(edges[Graph.relationKey('action')] ?? []).to.include('late-act');
|
|
999
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/late-act');
|
|
688
1000
|
const actions = registry.get(graph.actions('parent'));
|
|
689
1001
|
expect(actions).has.length(1);
|
|
690
|
-
expect(actions[0].id).to.equal('late-act');
|
|
1002
|
+
expect(actions[0].id).to.equal('parent/late-act');
|
|
691
1003
|
});
|
|
692
1004
|
|
|
693
1005
|
test('_actionContext captures and provides services to action execution', async () => {
|
|
@@ -809,13 +1121,13 @@ describe('GraphBuilder', () => {
|
|
|
809
1121
|
const connections = registry.get(graph.connections('parent', 'child'));
|
|
810
1122
|
// Should have both the child node and the action node.
|
|
811
1123
|
expect(connections.length).to.be.greaterThanOrEqual(1);
|
|
812
|
-
const childNode = connections.find((n) => n.id === 'child');
|
|
1124
|
+
const childNode = connections.find((n) => n.id === 'parent/child');
|
|
813
1125
|
expect(childNode).to.not.be.undefined;
|
|
814
1126
|
expect(childNode?.data).to.equal('test');
|
|
815
1127
|
|
|
816
1128
|
const actions = registry.get(graph.actions('parent'));
|
|
817
1129
|
expect(actions).has.length(1);
|
|
818
|
-
expect(actions[0].id).to.equal('test-action');
|
|
1130
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
819
1131
|
});
|
|
820
1132
|
|
|
821
1133
|
test('works with reactive connector using get context', async () => {
|
|
@@ -970,7 +1282,7 @@ describe('GraphBuilder', () => {
|
|
|
970
1282
|
// The working extension should still produce its node.
|
|
971
1283
|
const connections = registry.get(graph.connections('parent', 'child'));
|
|
972
1284
|
expect(connections).has.length(1);
|
|
973
|
-
expect(connections[0].id).to.equal('child-from-working');
|
|
1285
|
+
expect(connections[0].id).to.equal('parent/child-from-working');
|
|
974
1286
|
expect(connections[0].data).to.equal('success');
|
|
975
1287
|
});
|
|
976
1288
|
});
|
|
@@ -999,9 +1311,199 @@ describe('GraphBuilder', () => {
|
|
|
999
1311
|
|
|
1000
1312
|
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1001
1313
|
expect(connections).has.length(1);
|
|
1002
|
-
expect(connections[0].id).to.equal('child');
|
|
1314
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
1003
1315
|
expect(connections[0].data).to.equal(testObject);
|
|
1004
1316
|
});
|
|
1005
1317
|
});
|
|
1006
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
|
+
});
|
|
1007
1509
|
});
|