@dxos/app-graph 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/chunk-AKBGYELG.mjs +1603 -0
- package/dist/lib/browser/chunk-AKBGYELG.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +17 -1276
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs +1604 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +17 -1276
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/graph-builder.d.ts +11 -7
- 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 +43 -17
- 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/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +39 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -26
- package/src/graph-builder.test.ts +569 -102
- package/src/graph-builder.ts +202 -74
- package/src/graph.test.ts +187 -52
- package/src/graph.ts +174 -98
- package/src/index.ts +1 -0
- package/src/node-matcher.ts +58 -28
- package/src/node.ts +46 -5
- package/src/stories/EchoGraph.stories.tsx +90 -61
- 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 +95 -0
|
@@ -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,25 +458,29 @@ 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
|
});
|
|
@@ -308,14 +501,15 @@ describe('GraphBuilder', () => {
|
|
|
308
501
|
}),
|
|
309
502
|
);
|
|
310
503
|
const graph = builder.graph;
|
|
311
|
-
Graph.expand(graph, Node.RootId);
|
|
504
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
505
|
+
await GraphBuilder.flush(builder);
|
|
312
506
|
|
|
313
507
|
{
|
|
314
|
-
const nodes = registry.get(graph.connections(Node.RootId));
|
|
508
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
315
509
|
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));
|
|
510
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(1)));
|
|
511
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(2)));
|
|
512
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(3)));
|
|
319
513
|
}
|
|
320
514
|
|
|
321
515
|
registry.set(nodes, [
|
|
@@ -323,20 +517,18 @@ describe('GraphBuilder', () => {
|
|
|
323
517
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
324
518
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
325
519
|
]);
|
|
326
|
-
|
|
327
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
328
|
-
await sleep(0);
|
|
520
|
+
await GraphBuilder.flush(builder);
|
|
329
521
|
|
|
330
522
|
{
|
|
331
|
-
const nodes = registry.get(graph.connections(Node.RootId));
|
|
523
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
332
524
|
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));
|
|
525
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
526
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(1)));
|
|
527
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(2)));
|
|
336
528
|
}
|
|
337
529
|
});
|
|
338
530
|
|
|
339
|
-
test('updates are constrained', () => {
|
|
531
|
+
test('updates are constrained', async () => {
|
|
340
532
|
const registry = Registry.make();
|
|
341
533
|
const builder = GraphBuilder.make({ registry });
|
|
342
534
|
const name = Atom.make('default');
|
|
@@ -362,7 +554,9 @@ describe('GraphBuilder', () => {
|
|
|
362
554
|
Atom.make((get) =>
|
|
363
555
|
Function.pipe(
|
|
364
556
|
get(node),
|
|
365
|
-
Option.flatMap((node) =>
|
|
557
|
+
Option.flatMap((node) =>
|
|
558
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
559
|
+
),
|
|
366
560
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
367
561
|
Option.getOrElse(() => []),
|
|
368
562
|
),
|
|
@@ -374,7 +568,9 @@ describe('GraphBuilder', () => {
|
|
|
374
568
|
Atom.make((get) =>
|
|
375
569
|
Function.pipe(
|
|
376
570
|
get(node),
|
|
377
|
-
Option.flatMap((node) =>
|
|
571
|
+
Option.flatMap((node) =>
|
|
572
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
573
|
+
),
|
|
378
574
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
379
575
|
Option.getOrElse(() => []),
|
|
380
576
|
),
|
|
@@ -385,43 +581,47 @@ describe('GraphBuilder', () => {
|
|
|
385
581
|
const graph = builder.graph;
|
|
386
582
|
|
|
387
583
|
let parentCount = 0;
|
|
388
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
584
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
389
585
|
parentCount++;
|
|
390
586
|
});
|
|
391
587
|
onTestFinished(() => parentCancel());
|
|
392
588
|
|
|
393
589
|
let independentCount = 0;
|
|
394
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
590
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
395
591
|
independentCount++;
|
|
396
592
|
});
|
|
397
593
|
onTestFinished(() => independentCancel());
|
|
398
594
|
|
|
399
595
|
let dependentCount = 0;
|
|
400
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
596
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
401
597
|
dependentCount++;
|
|
402
598
|
});
|
|
403
599
|
onTestFinished(() => dependentCancel());
|
|
404
600
|
|
|
405
601
|
// Counts should not increment until the node is expanded.
|
|
406
|
-
Graph.expand(graph, Node.RootId);
|
|
602
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
603
|
+
await GraphBuilder.flush(builder);
|
|
407
604
|
expect(parentCount).to.equal(1);
|
|
408
605
|
expect(independentCount).to.equal(0);
|
|
409
606
|
expect(dependentCount).to.equal(0);
|
|
410
607
|
|
|
411
608
|
// Counts should increment when the node is expanded.
|
|
412
|
-
Graph.expand(graph, EXAMPLE_ID);
|
|
609
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
610
|
+
await GraphBuilder.flush(builder);
|
|
413
611
|
expect(parentCount).to.equal(1);
|
|
414
612
|
expect(independentCount).to.equal(1);
|
|
415
613
|
expect(dependentCount).to.equal(1);
|
|
416
614
|
|
|
417
615
|
// Only dependent count should increment when the parent changes.
|
|
418
616
|
registry.set(name, 'updated');
|
|
617
|
+
await GraphBuilder.flush(builder);
|
|
419
618
|
expect(parentCount).to.equal(2);
|
|
420
619
|
expect(independentCount).to.equal(1);
|
|
421
620
|
expect(dependentCount).to.equal(2);
|
|
422
621
|
|
|
423
622
|
// Only independent count should increment when its state changes.
|
|
424
623
|
registry.set(sub, 'updated');
|
|
624
|
+
await GraphBuilder.flush(builder);
|
|
425
625
|
expect(parentCount).to.equal(2);
|
|
426
626
|
expect(independentCount).to.equal(2);
|
|
427
627
|
expect(dependentCount).to.equal(2);
|
|
@@ -431,18 +631,21 @@ describe('GraphBuilder', () => {
|
|
|
431
631
|
registry.set(name, 'removed');
|
|
432
632
|
registry.set(sub, 'batch');
|
|
433
633
|
});
|
|
634
|
+
await GraphBuilder.flush(builder);
|
|
434
635
|
expect(parentCount).to.equal(2);
|
|
435
636
|
expect(independentCount).to.equal(3);
|
|
436
637
|
expect(dependentCount).to.equal(2);
|
|
437
638
|
|
|
438
639
|
// Dependent count should increment when the node is added back.
|
|
439
640
|
registry.set(name, 'added');
|
|
641
|
+
await GraphBuilder.flush(builder);
|
|
440
642
|
expect(parentCount).to.equal(3);
|
|
441
643
|
expect(independentCount).to.equal(3);
|
|
442
644
|
expect(dependentCount).to.equal(3);
|
|
443
645
|
|
|
444
646
|
// Counts should not increment when the node is expanded again.
|
|
445
|
-
Graph.expand(graph, EXAMPLE_ID);
|
|
647
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
648
|
+
await GraphBuilder.flush(builder);
|
|
446
649
|
expect(parentCount).to.equal(3);
|
|
447
650
|
expect(independentCount).to.equal(3);
|
|
448
651
|
expect(dependentCount).to.equal(3);
|
|
@@ -472,14 +675,14 @@ describe('GraphBuilder', () => {
|
|
|
472
675
|
let count = 0;
|
|
473
676
|
const trigger = new Trigger();
|
|
474
677
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
475
|
-
Graph.expand(builder.graph, id);
|
|
678
|
+
Graph.expand(builder.graph, id, 'child');
|
|
476
679
|
count++;
|
|
477
680
|
if (count === 5) {
|
|
478
681
|
trigger.wake();
|
|
479
682
|
}
|
|
480
683
|
});
|
|
481
684
|
|
|
482
|
-
Graph.expand(builder.graph, Node.RootId);
|
|
685
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
483
686
|
await trigger.wait();
|
|
484
687
|
expect(count).to.equal(5);
|
|
485
688
|
});
|
|
@@ -507,6 +710,7 @@ describe('GraphBuilder', () => {
|
|
|
507
710
|
|
|
508
711
|
let count = 0;
|
|
509
712
|
await GraphBuilder.explore(builder, {
|
|
713
|
+
relation: 'child',
|
|
510
714
|
visitor: () => {
|
|
511
715
|
count++;
|
|
512
716
|
},
|
|
@@ -518,7 +722,7 @@ describe('GraphBuilder', () => {
|
|
|
518
722
|
|
|
519
723
|
describe('helpers', () => {
|
|
520
724
|
describe('createConnector', () => {
|
|
521
|
-
test('creates connector with type inference', () => {
|
|
725
|
+
test('creates connector with type inference', async () => {
|
|
522
726
|
const registry = Registry.make();
|
|
523
727
|
const builder = GraphBuilder.make({ registry });
|
|
524
728
|
const graph = builder.graph;
|
|
@@ -536,16 +740,17 @@ describe('GraphBuilder', () => {
|
|
|
536
740
|
}),
|
|
537
741
|
);
|
|
538
742
|
|
|
539
|
-
Graph.expand(graph, Node.RootId);
|
|
743
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
744
|
+
await GraphBuilder.flush(builder);
|
|
540
745
|
|
|
541
|
-
const connections = registry.get(graph.connections(Node.RootId));
|
|
746
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
542
747
|
expect(connections).has.length(1);
|
|
543
|
-
expect(connections[0].id).to.equal('child');
|
|
748
|
+
expect(connections[0].id).to.equal('root/child');
|
|
544
749
|
});
|
|
545
750
|
});
|
|
546
751
|
|
|
547
752
|
describe('createExtension', () => {
|
|
548
|
-
test('works with Effect connector', () => {
|
|
753
|
+
test('works with Effect connector', async () => {
|
|
549
754
|
const registry = Registry.make();
|
|
550
755
|
const builder = GraphBuilder.make({ registry });
|
|
551
756
|
const graph = builder.graph;
|
|
@@ -562,15 +767,16 @@ describe('GraphBuilder', () => {
|
|
|
562
767
|
|
|
563
768
|
const writableGraph = graph as Graph.WritableGraph;
|
|
564
769
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
565
|
-
Graph.expand(graph, 'parent');
|
|
770
|
+
Graph.expand(graph, 'parent', 'child');
|
|
771
|
+
await GraphBuilder.flush(builder);
|
|
566
772
|
|
|
567
|
-
const connections = registry.get(graph.connections('parent'));
|
|
773
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
568
774
|
expect(connections).has.length(1);
|
|
569
|
-
expect(connections[0].id).to.equal('child');
|
|
775
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
570
776
|
expect(connections[0].data).to.equal('test');
|
|
571
777
|
});
|
|
572
778
|
|
|
573
|
-
test('works with Effect actions', () => {
|
|
779
|
+
test('works with Effect actions', async () => {
|
|
574
780
|
const registry = Registry.make();
|
|
575
781
|
const builder = GraphBuilder.make({ registry });
|
|
576
782
|
const graph = builder.graph;
|
|
@@ -583,10 +789,7 @@ describe('GraphBuilder', () => {
|
|
|
583
789
|
Effect.succeed([
|
|
584
790
|
{
|
|
585
791
|
id: 'test-action',
|
|
586
|
-
data: () =>
|
|
587
|
-
Effect.sync(() => {
|
|
588
|
-
console.log('TestAction');
|
|
589
|
-
}),
|
|
792
|
+
data: () => Effect.void,
|
|
590
793
|
properties: { label: 'Test' },
|
|
591
794
|
},
|
|
592
795
|
]),
|
|
@@ -597,14 +800,83 @@ describe('GraphBuilder', () => {
|
|
|
597
800
|
|
|
598
801
|
const writableGraph = graph as Graph.WritableGraph;
|
|
599
802
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
600
|
-
Graph.expand(graph, 'parent');
|
|
803
|
+
Graph.expand(graph, 'parent', 'child');
|
|
804
|
+
await GraphBuilder.flush(builder);
|
|
601
805
|
|
|
806
|
+
const edges = registry.get(graph.edges('parent'));
|
|
807
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.have.length(1);
|
|
808
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/test-action');
|
|
809
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.have.length(0);
|
|
602
810
|
const actions = registry.get(graph.actions('parent'));
|
|
603
811
|
expect(actions).has.length(1);
|
|
604
|
-
expect(actions[0].id).to.equal('test-action');
|
|
812
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
605
813
|
});
|
|
606
814
|
|
|
607
|
-
test('
|
|
815
|
+
test('actions expand automatically with child relation', async ({ expect }) => {
|
|
816
|
+
const registry = Registry.make();
|
|
817
|
+
const builder = GraphBuilder.make({ registry });
|
|
818
|
+
const graph = builder.graph;
|
|
819
|
+
|
|
820
|
+
const extensions = Effect.runSync(
|
|
821
|
+
GraphBuilder.createExtension({
|
|
822
|
+
id: 'test-extension',
|
|
823
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
824
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: 'c' }]),
|
|
825
|
+
actions: (node, get) =>
|
|
826
|
+
Effect.succeed([{ id: 'act1', data: () => Effect.void, properties: { label: 'A' } }]),
|
|
827
|
+
}),
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
831
|
+
|
|
832
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
833
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
834
|
+
Graph.expand(graph, 'parent', 'child');
|
|
835
|
+
await GraphBuilder.flush(builder);
|
|
836
|
+
|
|
837
|
+
const edges = registry.get(graph.edges('parent'));
|
|
838
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.include('parent/child');
|
|
839
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/act1');
|
|
840
|
+
const actions = registry.get(graph.actions('parent'));
|
|
841
|
+
expect(actions).has.length(1);
|
|
842
|
+
expect(actions[0].id).to.equal('parent/act1');
|
|
843
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
844
|
+
expect(connections).has.length(1);
|
|
845
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test('actions appear when extension registered after expand', async ({ expect }) => {
|
|
849
|
+
const registry = Registry.make();
|
|
850
|
+
const builder = GraphBuilder.make({ registry });
|
|
851
|
+
const graph = builder.graph;
|
|
852
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
853
|
+
|
|
854
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
855
|
+
Graph.expand(graph, 'parent', 'child');
|
|
856
|
+
await GraphBuilder.flush(builder);
|
|
857
|
+
|
|
858
|
+
expect(registry.get(graph.actions('parent'))).to.have.length(0);
|
|
859
|
+
|
|
860
|
+
const extensions = Effect.runSync(
|
|
861
|
+
GraphBuilder.createExtension({
|
|
862
|
+
id: 'late-extension',
|
|
863
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
864
|
+
actions: (node, get) =>
|
|
865
|
+
Effect.succeed([{ id: 'late-act', data: () => Effect.void, properties: { label: 'Late' } }]),
|
|
866
|
+
}),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
870
|
+
await GraphBuilder.flush(builder);
|
|
871
|
+
|
|
872
|
+
const edges = registry.get(graph.edges('parent'));
|
|
873
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/late-act');
|
|
874
|
+
const actions = registry.get(graph.actions('parent'));
|
|
875
|
+
expect(actions).has.length(1);
|
|
876
|
+
expect(actions[0].id).to.equal('parent/late-act');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test('_actionContext captures and provides services to action execution', async () => {
|
|
608
880
|
const registry = Registry.make();
|
|
609
881
|
const builder = GraphBuilder.make({ registry });
|
|
610
882
|
const graph = builder.graph;
|
|
@@ -648,7 +920,8 @@ describe('GraphBuilder', () => {
|
|
|
648
920
|
|
|
649
921
|
const writableGraph = graph as Graph.WritableGraph;
|
|
650
922
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
651
|
-
Graph.expand(graph, 'parent');
|
|
923
|
+
Graph.expand(graph, 'parent', 'child');
|
|
924
|
+
await GraphBuilder.flush(builder);
|
|
652
925
|
|
|
653
926
|
const actions = registry.get(graph.actions('parent'));
|
|
654
927
|
expect(actions).has.length(1);
|
|
@@ -691,7 +964,7 @@ describe('GraphBuilder', () => {
|
|
|
691
964
|
expect(node?.data).to.equal('resolved');
|
|
692
965
|
});
|
|
693
966
|
|
|
694
|
-
test('works with connector and actions together', () => {
|
|
967
|
+
test('works with connector and actions together', async () => {
|
|
695
968
|
const registry = Registry.make();
|
|
696
969
|
const builder = GraphBuilder.make({ registry });
|
|
697
970
|
const graph = builder.graph;
|
|
@@ -705,10 +978,7 @@ describe('GraphBuilder', () => {
|
|
|
705
978
|
Effect.succeed([
|
|
706
979
|
{
|
|
707
980
|
id: 'test-action',
|
|
708
|
-
data: () =>
|
|
709
|
-
Effect.sync(() => {
|
|
710
|
-
console.log('TestAction');
|
|
711
|
-
}),
|
|
981
|
+
data: () => Effect.void,
|
|
712
982
|
properties: { label: 'Test' },
|
|
713
983
|
},
|
|
714
984
|
]),
|
|
@@ -719,21 +989,22 @@ describe('GraphBuilder', () => {
|
|
|
719
989
|
|
|
720
990
|
const writableGraph = graph as Graph.WritableGraph;
|
|
721
991
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
722
|
-
Graph.expand(graph, 'parent');
|
|
992
|
+
Graph.expand(graph, 'parent', 'child');
|
|
993
|
+
await GraphBuilder.flush(builder);
|
|
723
994
|
|
|
724
|
-
const connections = registry.get(graph.connections('parent'));
|
|
995
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
725
996
|
// Should have both the child node and the action node.
|
|
726
997
|
expect(connections.length).to.be.greaterThanOrEqual(1);
|
|
727
|
-
const childNode = connections.find((n) => n.id === 'child');
|
|
998
|
+
const childNode = connections.find((n) => n.id === 'parent/child');
|
|
728
999
|
expect(childNode).to.not.be.undefined;
|
|
729
1000
|
expect(childNode?.data).to.equal('test');
|
|
730
1001
|
|
|
731
1002
|
const actions = registry.get(graph.actions('parent'));
|
|
732
1003
|
expect(actions).has.length(1);
|
|
733
|
-
expect(actions[0].id).to.equal('test-action');
|
|
1004
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
734
1005
|
});
|
|
735
1006
|
|
|
736
|
-
test('works with reactive connector using get context', () => {
|
|
1007
|
+
test('works with reactive connector using get context', async () => {
|
|
737
1008
|
const registry = Registry.make();
|
|
738
1009
|
const builder = GraphBuilder.make({ registry });
|
|
739
1010
|
const graph = builder.graph;
|
|
@@ -752,18 +1023,20 @@ describe('GraphBuilder', () => {
|
|
|
752
1023
|
|
|
753
1024
|
const writableGraph = graph as Graph.WritableGraph;
|
|
754
1025
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
755
|
-
Graph.expand(graph, 'parent');
|
|
1026
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1027
|
+
await GraphBuilder.flush(builder);
|
|
756
1028
|
|
|
757
1029
|
{
|
|
758
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1030
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
759
1031
|
expect(connections).has.length(1);
|
|
760
1032
|
expect(connections[0].data).to.equal('initial');
|
|
761
1033
|
}
|
|
762
1034
|
|
|
763
1035
|
registry.set(state, 'updated');
|
|
1036
|
+
await GraphBuilder.flush(builder);
|
|
764
1037
|
|
|
765
1038
|
{
|
|
766
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1039
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
767
1040
|
expect(connections).has.length(1);
|
|
768
1041
|
expect(connections[0].data).to.equal('updated');
|
|
769
1042
|
}
|
|
@@ -771,7 +1044,7 @@ describe('GraphBuilder', () => {
|
|
|
771
1044
|
});
|
|
772
1045
|
|
|
773
1046
|
describe('extension error handling', () => {
|
|
774
|
-
test('connector failure is caught and logged, returns empty array', () => {
|
|
1047
|
+
test('connector failure is caught and logged, returns empty array', async () => {
|
|
775
1048
|
const registry = Registry.make();
|
|
776
1049
|
const builder = GraphBuilder.make({ registry });
|
|
777
1050
|
const graph = builder.graph;
|
|
@@ -790,14 +1063,15 @@ describe('GraphBuilder', () => {
|
|
|
790
1063
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
791
1064
|
|
|
792
1065
|
// Should not throw, error is caught internally.
|
|
793
|
-
Graph.expand(graph, 'parent');
|
|
1066
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1067
|
+
await GraphBuilder.flush(builder);
|
|
794
1068
|
|
|
795
1069
|
// Should return empty connections since the connector failed.
|
|
796
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1070
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
797
1071
|
expect(connections).has.length(0);
|
|
798
1072
|
});
|
|
799
1073
|
|
|
800
|
-
test('actions failure is caught and logged, returns empty array', () => {
|
|
1074
|
+
test('actions failure is caught and logged, returns empty array', async () => {
|
|
801
1075
|
const registry = Registry.make();
|
|
802
1076
|
const builder = GraphBuilder.make({ registry });
|
|
803
1077
|
const graph = builder.graph;
|
|
@@ -816,7 +1090,8 @@ describe('GraphBuilder', () => {
|
|
|
816
1090
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
817
1091
|
|
|
818
1092
|
// Should not throw, error is caught internally.
|
|
819
|
-
Graph.expand(graph, 'parent');
|
|
1093
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1094
|
+
await GraphBuilder.flush(builder);
|
|
820
1095
|
|
|
821
1096
|
// Should return empty actions since the actions callback failed.
|
|
822
1097
|
const actions = registry.get(graph.actions('parent'));
|
|
@@ -846,7 +1121,7 @@ describe('GraphBuilder', () => {
|
|
|
846
1121
|
expect(node).to.be.null;
|
|
847
1122
|
});
|
|
848
1123
|
|
|
849
|
-
test('failing extension does not affect other extensions', () => {
|
|
1124
|
+
test('failing extension does not affect other extensions', async () => {
|
|
850
1125
|
const registry = Registry.make();
|
|
851
1126
|
const builder = GraphBuilder.make({ registry });
|
|
852
1127
|
const graph = builder.graph;
|
|
@@ -875,18 +1150,19 @@ describe('GraphBuilder', () => {
|
|
|
875
1150
|
|
|
876
1151
|
const writableGraph = graph as Graph.WritableGraph;
|
|
877
1152
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
878
|
-
Graph.expand(graph, 'parent');
|
|
1153
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1154
|
+
await GraphBuilder.flush(builder);
|
|
879
1155
|
|
|
880
1156
|
// The working extension should still produce its node.
|
|
881
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1157
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
882
1158
|
expect(connections).has.length(1);
|
|
883
|
-
expect(connections[0].id).to.equal('child-from-working');
|
|
1159
|
+
expect(connections[0].id).to.equal('parent/child-from-working');
|
|
884
1160
|
expect(connections[0].data).to.equal('success');
|
|
885
1161
|
});
|
|
886
1162
|
});
|
|
887
1163
|
|
|
888
1164
|
describe('createTypeExtension', () => {
|
|
889
|
-
test('creates extension matching by schema type with inferred object type', () => {
|
|
1165
|
+
test('creates extension matching by schema type with inferred object type', async () => {
|
|
890
1166
|
const registry = Registry.make();
|
|
891
1167
|
const builder = GraphBuilder.make({ registry });
|
|
892
1168
|
const graph = builder.graph;
|
|
@@ -904,13 +1180,204 @@ describe('GraphBuilder', () => {
|
|
|
904
1180
|
const writableGraph = graph as Graph.WritableGraph;
|
|
905
1181
|
const testObject = Obj.make(TestSchema.Person, { name: 'Test' });
|
|
906
1182
|
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: testObject });
|
|
907
|
-
Graph.expand(graph, 'parent');
|
|
1183
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1184
|
+
await GraphBuilder.flush(builder);
|
|
908
1185
|
|
|
909
|
-
const connections = registry.get(graph.connections('parent'));
|
|
1186
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
910
1187
|
expect(connections).has.length(1);
|
|
911
|
-
expect(connections[0].id).to.equal('child');
|
|
1188
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
912
1189
|
expect(connections[0].data).to.equal(testObject);
|
|
913
1190
|
});
|
|
914
1191
|
});
|
|
915
1192
|
});
|
|
1193
|
+
describe('path-based ID qualification', () => {
|
|
1194
|
+
test('rejects segment IDs containing slash', async () => {
|
|
1195
|
+
const registry = Registry.make();
|
|
1196
|
+
const builder = GraphBuilder.make({ registry });
|
|
1197
|
+
GraphBuilder.addExtension(
|
|
1198
|
+
builder,
|
|
1199
|
+
GraphBuilder.createExtensionRaw({
|
|
1200
|
+
id: 'bad-connector',
|
|
1201
|
+
connector: () => Atom.make([{ id: 'foo/bar', type: EXAMPLE_TYPE, data: null }]),
|
|
1202
|
+
}),
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
expect(() => Graph.expand(builder.graph, Node.RootId, 'child')).toThrow(/must not contain/);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test('multi-level path qualification', async () => {
|
|
1209
|
+
const registry = Registry.make();
|
|
1210
|
+
const builder = GraphBuilder.make({ registry });
|
|
1211
|
+
GraphBuilder.addExtension(builder, [
|
|
1212
|
+
GraphBuilder.createExtensionRaw({
|
|
1213
|
+
id: 'level1',
|
|
1214
|
+
connector: (node) =>
|
|
1215
|
+
Atom.make((get) =>
|
|
1216
|
+
Function.pipe(
|
|
1217
|
+
get(node),
|
|
1218
|
+
Option.filter((n) => n.id === 'root'),
|
|
1219
|
+
Option.map(() => [{ id: 'A', type: EXAMPLE_TYPE, data: 'a' }]),
|
|
1220
|
+
Option.getOrElse(() => []),
|
|
1221
|
+
),
|
|
1222
|
+
),
|
|
1223
|
+
}),
|
|
1224
|
+
GraphBuilder.createExtensionRaw({
|
|
1225
|
+
id: 'level2',
|
|
1226
|
+
connector: (node) =>
|
|
1227
|
+
Atom.make((get) =>
|
|
1228
|
+
Function.pipe(
|
|
1229
|
+
get(node),
|
|
1230
|
+
Option.filter((n) => n.id === 'root/A'),
|
|
1231
|
+
Option.map(() => [{ id: 'B', type: EXAMPLE_TYPE, data: 'b' }]),
|
|
1232
|
+
Option.getOrElse(() => []),
|
|
1233
|
+
),
|
|
1234
|
+
),
|
|
1235
|
+
}),
|
|
1236
|
+
]);
|
|
1237
|
+
|
|
1238
|
+
const graph = builder.graph;
|
|
1239
|
+
|
|
1240
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1241
|
+
await GraphBuilder.flush(builder);
|
|
1242
|
+
|
|
1243
|
+
const level1 = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1244
|
+
expect(level1).has.length(1);
|
|
1245
|
+
expect(level1[0].id).to.equal('root/A');
|
|
1246
|
+
|
|
1247
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1248
|
+
await GraphBuilder.flush(builder);
|
|
1249
|
+
|
|
1250
|
+
const level2 = registry.get(graph.connections('root/A', 'child'));
|
|
1251
|
+
expect(level2).has.length(1);
|
|
1252
|
+
expect(level2[0].id).to.equal('root/A/B');
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test('inline nodes are recursively qualified', async () => {
|
|
1256
|
+
const registry = Registry.make();
|
|
1257
|
+
const builder = GraphBuilder.make({ registry });
|
|
1258
|
+
GraphBuilder.addExtension(
|
|
1259
|
+
builder,
|
|
1260
|
+
GraphBuilder.createExtensionRaw({
|
|
1261
|
+
id: 'inline-connector',
|
|
1262
|
+
connector: () =>
|
|
1263
|
+
Atom.make([
|
|
1264
|
+
{
|
|
1265
|
+
id: 'parent-node',
|
|
1266
|
+
type: EXAMPLE_TYPE,
|
|
1267
|
+
data: null,
|
|
1268
|
+
nodes: [
|
|
1269
|
+
{
|
|
1270
|
+
id: 'inline-child',
|
|
1271
|
+
type: EXAMPLE_TYPE,
|
|
1272
|
+
data: null,
|
|
1273
|
+
nodes: [{ id: 'deep-child', type: EXAMPLE_TYPE, data: null }],
|
|
1274
|
+
},
|
|
1275
|
+
],
|
|
1276
|
+
},
|
|
1277
|
+
]),
|
|
1278
|
+
}),
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
const graph = builder.graph;
|
|
1282
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1283
|
+
await GraphBuilder.flush(builder);
|
|
1284
|
+
|
|
1285
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1286
|
+
expect(connections).has.length(1);
|
|
1287
|
+
expect(connections[0].id).to.equal('root/parent-node');
|
|
1288
|
+
|
|
1289
|
+
const inlineNode = Graph.getNode(graph, 'root/parent-node/inline-child').pipe(Option.getOrNull);
|
|
1290
|
+
expect(inlineNode).to.not.be.null;
|
|
1291
|
+
expect(inlineNode?.id).to.equal('root/parent-node/inline-child');
|
|
1292
|
+
|
|
1293
|
+
const deepNode = Graph.getNode(graph, 'root/parent-node/inline-child/deep-child').pipe(Option.getOrNull);
|
|
1294
|
+
expect(deepNode).to.not.be.null;
|
|
1295
|
+
expect(deepNode?.id).to.equal('root/parent-node/inline-child/deep-child');
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test('constant connector produces distinct nodes under different parents', async () => {
|
|
1299
|
+
const registry = Registry.make();
|
|
1300
|
+
const builder = GraphBuilder.make({ registry });
|
|
1301
|
+
|
|
1302
|
+
GraphBuilder.addExtension(builder, [
|
|
1303
|
+
GraphBuilder.createExtensionRaw({
|
|
1304
|
+
id: 'parents',
|
|
1305
|
+
connector: (node) =>
|
|
1306
|
+
Atom.make((get) =>
|
|
1307
|
+
Function.pipe(
|
|
1308
|
+
get(node),
|
|
1309
|
+
Option.filter((n) => n.id === 'root'),
|
|
1310
|
+
Option.map(() => [
|
|
1311
|
+
{ id: 'A', type: EXAMPLE_TYPE, data: 'a' },
|
|
1312
|
+
{ id: 'B', type: EXAMPLE_TYPE, data: 'b' },
|
|
1313
|
+
]),
|
|
1314
|
+
Option.getOrElse(() => []),
|
|
1315
|
+
),
|
|
1316
|
+
),
|
|
1317
|
+
}),
|
|
1318
|
+
GraphBuilder.createExtensionRaw({
|
|
1319
|
+
id: 'constant-child',
|
|
1320
|
+
connector: () => Atom.make([{ id: 'shared', type: EXAMPLE_TYPE, data: 'constant' }]),
|
|
1321
|
+
}),
|
|
1322
|
+
]);
|
|
1323
|
+
|
|
1324
|
+
const graph = builder.graph;
|
|
1325
|
+
|
|
1326
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1327
|
+
await GraphBuilder.flush(builder);
|
|
1328
|
+
|
|
1329
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1330
|
+
Graph.expand(graph, 'root/B', 'child');
|
|
1331
|
+
await GraphBuilder.flush(builder);
|
|
1332
|
+
|
|
1333
|
+
const childrenOfA = registry.get(graph.connections('root/A', 'child'));
|
|
1334
|
+
const childrenOfB = registry.get(graph.connections('root/B', 'child'));
|
|
1335
|
+
|
|
1336
|
+
expect(childrenOfA).has.length(1);
|
|
1337
|
+
expect(childrenOfB).has.length(1);
|
|
1338
|
+
expect(childrenOfA[0].id).to.equal('root/A/shared');
|
|
1339
|
+
expect(childrenOfB[0].id).to.equal('root/B/shared');
|
|
1340
|
+
|
|
1341
|
+
const nodeA = Graph.getNode(graph, 'root/A/shared').pipe(Option.getOrNull);
|
|
1342
|
+
const nodeB = Graph.getNode(graph, 'root/B/shared').pipe(Option.getOrNull);
|
|
1343
|
+
expect(nodeA).to.not.be.null;
|
|
1344
|
+
expect(nodeB).to.not.be.null;
|
|
1345
|
+
expect(nodeA?.id).to.not.equal(nodeB?.id);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test('explore qualifies node IDs', async () => {
|
|
1349
|
+
const builder = GraphBuilder.make();
|
|
1350
|
+
GraphBuilder.addExtension(
|
|
1351
|
+
builder,
|
|
1352
|
+
GraphBuilder.createExtensionRaw({
|
|
1353
|
+
id: 'connector',
|
|
1354
|
+
connector: (node) =>
|
|
1355
|
+
Atom.make((get) =>
|
|
1356
|
+
Function.pipe(
|
|
1357
|
+
get(node),
|
|
1358
|
+
Option.filter((n) => n.id === 'root'),
|
|
1359
|
+
Option.map(() => [
|
|
1360
|
+
{ id: 'first', type: EXAMPLE_TYPE, data: 1 },
|
|
1361
|
+
{ id: 'second', type: EXAMPLE_TYPE, data: 2 },
|
|
1362
|
+
]),
|
|
1363
|
+
Option.getOrElse(() => []),
|
|
1364
|
+
),
|
|
1365
|
+
),
|
|
1366
|
+
}),
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
const visited: Array<{ id: string; path: string[] }> = [];
|
|
1370
|
+
await GraphBuilder.explore(builder, {
|
|
1371
|
+
relation: 'child',
|
|
1372
|
+
visitor: (node, path) => {
|
|
1373
|
+
visited.push({ id: node.id, path });
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
expect(visited).has.length(3);
|
|
1378
|
+
expect(visited[0].id).to.equal('root');
|
|
1379
|
+
expect(visited[1].id).to.equal('root/first');
|
|
1380
|
+
expect(visited[2].id).to.equal('root/second');
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
916
1383
|
});
|