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