@dxos/app-graph 0.8.4-main.dedc0f3 → 0.8.4-main.e00bdcdb52
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-ZPU7IO6U.mjs +1469 -0
- package/dist/lib/browser/chunk-ZPU7IO6U.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +27 -842
- 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-2WLCW7IW.mjs +1470 -0
- package/dist/lib/node-esm/chunk-2WLCW7IW.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +27 -843
- 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 +113 -67
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +188 -222
- 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 +0 -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 +42 -38
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1028 -144
- package/src/graph-builder.ts +719 -293
- package/src/graph.test.ts +451 -123
- package/src/graph.ts +1054 -403
- package/src/index.ts +10 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +314 -0
- package/src/node.ts +82 -8
- package/src/stories/EchoGraph.stories.tsx +167 -131
- 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 +99 -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,45 +2,51 @@
|
|
|
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 { Trigger
|
|
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', () => {
|
|
20
27
|
describe('resolver', () => {
|
|
21
28
|
test('works', async () => {
|
|
22
29
|
const registry = Registry.make();
|
|
23
|
-
const builder =
|
|
30
|
+
const builder = GraphBuilder.make({ registry });
|
|
24
31
|
const graph = builder.graph;
|
|
25
32
|
|
|
26
33
|
{
|
|
27
|
-
const node =
|
|
34
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
28
35
|
expect(node).to.be.null;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
// Test direct API
|
|
39
|
+
GraphBuilder.addExtension(
|
|
40
|
+
builder,
|
|
41
|
+
GraphBuilder.createExtensionRaw({
|
|
33
42
|
id: 'resolver',
|
|
34
|
-
resolver: () => {
|
|
35
|
-
console.log('resolver');
|
|
36
|
-
return Rx.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
37
|
-
},
|
|
43
|
+
resolver: () => Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }),
|
|
38
44
|
}),
|
|
39
45
|
);
|
|
40
|
-
await
|
|
46
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
41
47
|
|
|
42
48
|
{
|
|
43
|
-
const node =
|
|
49
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
44
50
|
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
45
51
|
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
46
52
|
expect(node?.data).to.equal(1);
|
|
@@ -49,132 +55,326 @@ describe('GraphBuilder', () => {
|
|
|
49
55
|
|
|
50
56
|
test('updates', async () => {
|
|
51
57
|
const registry = Registry.make();
|
|
52
|
-
const builder =
|
|
53
|
-
const name =
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const builder = GraphBuilder.make({ registry });
|
|
59
|
+
const name = Atom.make('default');
|
|
60
|
+
GraphBuilder.addExtension(
|
|
61
|
+
builder,
|
|
62
|
+
GraphBuilder.createExtensionRaw({
|
|
56
63
|
id: 'resolver',
|
|
57
|
-
resolver: () =>
|
|
64
|
+
resolver: () => Atom.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
58
65
|
}),
|
|
59
66
|
);
|
|
60
67
|
const graph = builder.graph;
|
|
61
|
-
await
|
|
68
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
62
69
|
|
|
63
70
|
{
|
|
64
|
-
const node =
|
|
71
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
65
72
|
expect(node?.data).to.equal('default');
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
registry.set(name, 'updated');
|
|
69
76
|
|
|
70
77
|
{
|
|
71
|
-
const node =
|
|
78
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
72
79
|
expect(node?.data).to.equal('updated');
|
|
73
80
|
}
|
|
74
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
|
+
});
|
|
75
263
|
});
|
|
76
264
|
|
|
77
265
|
describe('connector', () => {
|
|
78
|
-
test('works', () => {
|
|
266
|
+
test('works', async () => {
|
|
79
267
|
const registry = Registry.make();
|
|
80
|
-
const builder =
|
|
81
|
-
|
|
82
|
-
|
|
268
|
+
const builder = GraphBuilder.make({ registry });
|
|
269
|
+
GraphBuilder.addExtension(
|
|
270
|
+
builder,
|
|
271
|
+
GraphBuilder.createExtensionRaw({
|
|
83
272
|
id: 'outbound-connector',
|
|
84
|
-
connector: () =>
|
|
273
|
+
connector: () => Atom.make([{ id: 'child', type: EXAMPLE_TYPE, data: 2 }]),
|
|
85
274
|
}),
|
|
86
275
|
);
|
|
87
|
-
|
|
88
|
-
|
|
276
|
+
GraphBuilder.addExtension(
|
|
277
|
+
builder,
|
|
278
|
+
GraphBuilder.createExtensionRaw({
|
|
89
279
|
id: 'inbound-connector',
|
|
90
|
-
relation: 'inbound',
|
|
91
|
-
connector: () =>
|
|
280
|
+
relation: Node.childRelation('inbound'),
|
|
281
|
+
connector: () => Atom.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
92
282
|
}),
|
|
93
283
|
);
|
|
94
284
|
|
|
95
285
|
const graph = builder.graph;
|
|
96
|
-
|
|
97
|
-
|
|
286
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
287
|
+
Graph.expand(graph, Node.RootId, Node.childRelation('inbound'));
|
|
288
|
+
await GraphBuilder.flush(builder);
|
|
98
289
|
|
|
99
|
-
const outbound = registry.get(graph.connections(
|
|
100
|
-
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')));
|
|
101
292
|
|
|
102
293
|
expect(outbound).has.length(1);
|
|
103
|
-
expect(outbound[0].id).to.equal('child');
|
|
294
|
+
expect(outbound[0].id).to.equal('root/child');
|
|
104
295
|
expect(outbound[0].data).to.equal(2);
|
|
105
296
|
expect(inbound).has.length(1);
|
|
106
|
-
expect(inbound[0].id).to.equal('parent');
|
|
297
|
+
expect(inbound[0].id).to.equal('root/parent');
|
|
107
298
|
expect(inbound[0].data).to.equal(0);
|
|
108
299
|
});
|
|
109
300
|
|
|
110
|
-
test('updates', () => {
|
|
301
|
+
test('updates', async () => {
|
|
111
302
|
const registry = Registry.make();
|
|
112
|
-
const builder =
|
|
113
|
-
const state =
|
|
114
|
-
|
|
115
|
-
|
|
303
|
+
const builder = GraphBuilder.make({ registry });
|
|
304
|
+
const state = Atom.make(0);
|
|
305
|
+
GraphBuilder.addExtension(
|
|
306
|
+
builder,
|
|
307
|
+
GraphBuilder.createExtensionRaw({
|
|
116
308
|
id: 'connector',
|
|
117
|
-
connector: () =>
|
|
309
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
118
310
|
}),
|
|
119
311
|
);
|
|
120
312
|
const graph = builder.graph;
|
|
121
|
-
|
|
313
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
314
|
+
await GraphBuilder.flush(builder);
|
|
122
315
|
|
|
123
316
|
{
|
|
124
|
-
const [node] = registry.get(graph.connections(
|
|
317
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
125
318
|
expect(node.data).to.equal(0);
|
|
126
319
|
}
|
|
127
320
|
|
|
128
321
|
{
|
|
129
322
|
registry.set(state, 1);
|
|
130
|
-
|
|
323
|
+
await GraphBuilder.flush(builder);
|
|
324
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
131
325
|
expect(node.data).to.equal(1);
|
|
132
326
|
}
|
|
133
327
|
});
|
|
134
328
|
|
|
135
|
-
test('subscribes to updates', () => {
|
|
329
|
+
test('subscribes to updates', async () => {
|
|
136
330
|
const registry = Registry.make();
|
|
137
|
-
const builder =
|
|
138
|
-
const state =
|
|
139
|
-
|
|
140
|
-
|
|
331
|
+
const builder = GraphBuilder.make({ registry });
|
|
332
|
+
const state = Atom.make(0);
|
|
333
|
+
GraphBuilder.addExtension(
|
|
334
|
+
builder,
|
|
335
|
+
GraphBuilder.createExtensionRaw({
|
|
141
336
|
id: 'connector',
|
|
142
|
-
connector: () =>
|
|
337
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
143
338
|
}),
|
|
144
339
|
);
|
|
145
340
|
const graph = builder.graph;
|
|
146
341
|
|
|
147
342
|
let count = 0;
|
|
148
|
-
const cancel = registry.subscribe(graph.connections(
|
|
343
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_) => {
|
|
149
344
|
count++;
|
|
150
345
|
});
|
|
151
346
|
onTestFinished(() => cancel());
|
|
152
347
|
|
|
153
348
|
expect(count).to.equal(0);
|
|
154
|
-
expect(registry.get(graph.connections(
|
|
349
|
+
expect(registry.get(graph.connections(Node.RootId, 'child'))).to.have.length(0);
|
|
155
350
|
expect(count).to.equal(1);
|
|
156
351
|
|
|
157
|
-
|
|
352
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
353
|
+
await GraphBuilder.flush(builder);
|
|
158
354
|
expect(count).to.equal(2);
|
|
355
|
+
|
|
159
356
|
registry.set(state, 1);
|
|
357
|
+
await GraphBuilder.flush(builder);
|
|
160
358
|
expect(count).to.equal(3);
|
|
161
359
|
});
|
|
162
360
|
|
|
163
|
-
test('updates with new extensions', () => {
|
|
361
|
+
test('updates with new extensions', async () => {
|
|
164
362
|
const registry = Registry.make();
|
|
165
|
-
const builder =
|
|
166
|
-
|
|
167
|
-
|
|
363
|
+
const builder = GraphBuilder.make({ registry });
|
|
364
|
+
GraphBuilder.addExtension(
|
|
365
|
+
builder,
|
|
366
|
+
GraphBuilder.createExtensionRaw({
|
|
168
367
|
id: 'connector',
|
|
169
|
-
connector: () =>
|
|
368
|
+
connector: () => Atom.make([{ id: EXAMPLE_ID, type: EXAMPLE_TYPE }]),
|
|
170
369
|
}),
|
|
171
370
|
);
|
|
172
371
|
const graph = builder.graph;
|
|
173
|
-
|
|
372
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
373
|
+
await GraphBuilder.flush(builder);
|
|
174
374
|
|
|
175
|
-
let nodes: Node[] = [];
|
|
375
|
+
let nodes: Node.Node[] = [];
|
|
176
376
|
let count = 0;
|
|
177
|
-
const cancel = registry.subscribe(graph.connections(
|
|
377
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_nodes) => {
|
|
178
378
|
count++;
|
|
179
379
|
nodes = _nodes;
|
|
180
380
|
});
|
|
@@ -182,63 +382,68 @@ describe('GraphBuilder', () => {
|
|
|
182
382
|
|
|
183
383
|
expect(nodes).has.length(0);
|
|
184
384
|
expect(count).to.equal(0);
|
|
185
|
-
registry.get(graph.connections(
|
|
385
|
+
registry.get(graph.connections(Node.RootId, 'child'));
|
|
186
386
|
expect(nodes).has.length(1);
|
|
187
387
|
expect(count).to.equal(1);
|
|
188
388
|
|
|
189
|
-
|
|
190
|
-
|
|
389
|
+
GraphBuilder.addExtension(
|
|
390
|
+
builder,
|
|
391
|
+
GraphBuilder.createExtensionRaw({
|
|
191
392
|
id: 'connector-2',
|
|
192
|
-
connector: () =>
|
|
393
|
+
connector: () => Atom.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
193
394
|
}),
|
|
194
395
|
);
|
|
396
|
+
await GraphBuilder.flush(builder);
|
|
195
397
|
expect(nodes).has.length(2);
|
|
196
398
|
expect(count).to.equal(2);
|
|
197
399
|
});
|
|
198
400
|
|
|
199
|
-
test('removes', () => {
|
|
401
|
+
test('removes', async () => {
|
|
200
402
|
const registry = Registry.make();
|
|
201
|
-
const builder =
|
|
202
|
-
const nodes =
|
|
403
|
+
const builder = GraphBuilder.make({ registry });
|
|
404
|
+
const nodes = Atom.make([
|
|
203
405
|
{ id: exampleId(1), type: EXAMPLE_TYPE },
|
|
204
406
|
{ id: exampleId(2), type: EXAMPLE_TYPE },
|
|
205
407
|
]);
|
|
206
|
-
|
|
207
|
-
|
|
408
|
+
GraphBuilder.addExtension(
|
|
409
|
+
builder,
|
|
410
|
+
GraphBuilder.createExtensionRaw({
|
|
208
411
|
id: 'connector',
|
|
209
|
-
connector: () =>
|
|
412
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
210
413
|
}),
|
|
211
414
|
);
|
|
212
415
|
const graph = builder.graph;
|
|
213
|
-
|
|
416
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
417
|
+
await GraphBuilder.flush(builder);
|
|
214
418
|
|
|
215
419
|
{
|
|
216
|
-
const nodes = registry.get(graph.connections(
|
|
420
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
217
421
|
expect(nodes).has.length(2);
|
|
218
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
219
|
-
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)));
|
|
220
424
|
}
|
|
221
425
|
|
|
222
426
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
427
|
+
await GraphBuilder.flush(builder);
|
|
223
428
|
|
|
224
429
|
{
|
|
225
|
-
const nodes = registry.get(graph.connections(
|
|
430
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
226
431
|
expect(nodes).has.length(1);
|
|
227
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
432
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
228
433
|
}
|
|
229
434
|
});
|
|
230
435
|
|
|
231
|
-
test('nodes are updated when removed', () => {
|
|
436
|
+
test('nodes are updated when removed', async () => {
|
|
232
437
|
const registry = Registry.make();
|
|
233
|
-
const builder =
|
|
234
|
-
const name =
|
|
438
|
+
const builder = GraphBuilder.make({ registry });
|
|
439
|
+
const name = Atom.make('removed');
|
|
235
440
|
|
|
236
|
-
|
|
237
|
-
|
|
441
|
+
GraphBuilder.addExtension(builder, [
|
|
442
|
+
GraphBuilder.createExtensionRaw({
|
|
238
443
|
id: 'root',
|
|
239
444
|
connector: (node) =>
|
|
240
|
-
|
|
241
|
-
pipe(
|
|
445
|
+
Atom.make((get) =>
|
|
446
|
+
Function.pipe(
|
|
242
447
|
get(node),
|
|
243
448
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
244
449
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -253,52 +458,58 @@ describe('GraphBuilder', () => {
|
|
|
253
458
|
|
|
254
459
|
let count = 0;
|
|
255
460
|
let exists = false;
|
|
256
|
-
const cancel = registry.subscribe(graph.node(EXAMPLE_ID), (node) => {
|
|
461
|
+
const cancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (node) => {
|
|
257
462
|
count++;
|
|
258
463
|
exists = Option.isSome(node);
|
|
259
464
|
});
|
|
260
465
|
onTestFinished(() => cancel());
|
|
261
466
|
|
|
262
|
-
|
|
467
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
468
|
+
await GraphBuilder.flush(builder);
|
|
263
469
|
expect(count).to.equal(0);
|
|
264
470
|
expect(exists).to.be.false;
|
|
265
471
|
|
|
266
472
|
registry.set(name, 'default');
|
|
473
|
+
await GraphBuilder.flush(builder);
|
|
267
474
|
expect(count).to.equal(1);
|
|
268
475
|
expect(exists).to.be.true;
|
|
269
476
|
|
|
270
477
|
registry.set(name, 'removed');
|
|
478
|
+
await GraphBuilder.flush(builder);
|
|
271
479
|
expect(count).to.equal(2);
|
|
272
480
|
expect(exists).to.be.false;
|
|
273
481
|
|
|
274
482
|
registry.set(name, 'added');
|
|
483
|
+
await GraphBuilder.flush(builder);
|
|
275
484
|
expect(count).to.equal(3);
|
|
276
485
|
expect(exists).to.be.true;
|
|
277
486
|
});
|
|
278
487
|
|
|
279
488
|
test('sort edges', async () => {
|
|
280
489
|
const registry = Registry.make();
|
|
281
|
-
const builder =
|
|
282
|
-
const nodes =
|
|
490
|
+
const builder = GraphBuilder.make({ registry });
|
|
491
|
+
const nodes = Atom.make([
|
|
283
492
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
284
493
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
285
494
|
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
286
495
|
]);
|
|
287
|
-
|
|
288
|
-
|
|
496
|
+
GraphBuilder.addExtension(
|
|
497
|
+
builder,
|
|
498
|
+
GraphBuilder.createExtensionRaw({
|
|
289
499
|
id: 'connector',
|
|
290
|
-
connector: () =>
|
|
500
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
291
501
|
}),
|
|
292
502
|
);
|
|
293
503
|
const graph = builder.graph;
|
|
294
|
-
|
|
504
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
505
|
+
await GraphBuilder.flush(builder);
|
|
295
506
|
|
|
296
507
|
{
|
|
297
|
-
const nodes = registry.get(graph.connections(
|
|
508
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
298
509
|
expect(nodes).has.length(3);
|
|
299
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
300
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
301
|
-
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)));
|
|
302
513
|
}
|
|
303
514
|
|
|
304
515
|
registry.set(nodes, [
|
|
@@ -306,31 +517,29 @@ describe('GraphBuilder', () => {
|
|
|
306
517
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
307
518
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
308
519
|
]);
|
|
309
|
-
|
|
310
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
311
|
-
await sleep(0);
|
|
520
|
+
await GraphBuilder.flush(builder);
|
|
312
521
|
|
|
313
522
|
{
|
|
314
|
-
const nodes = registry.get(graph.connections(
|
|
523
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
315
524
|
expect(nodes).has.length(3);
|
|
316
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
317
|
-
expect(nodes[1].id).to.equal(exampleId(1));
|
|
318
|
-
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)));
|
|
319
528
|
}
|
|
320
529
|
});
|
|
321
530
|
|
|
322
|
-
test('updates are constrained', () => {
|
|
531
|
+
test('updates are constrained', async () => {
|
|
323
532
|
const registry = Registry.make();
|
|
324
|
-
const builder =
|
|
325
|
-
const name =
|
|
326
|
-
const sub =
|
|
533
|
+
const builder = GraphBuilder.make({ registry });
|
|
534
|
+
const name = Atom.make('default');
|
|
535
|
+
const sub = Atom.make('default');
|
|
327
536
|
|
|
328
|
-
|
|
329
|
-
|
|
537
|
+
GraphBuilder.addExtension(builder, [
|
|
538
|
+
GraphBuilder.createExtensionRaw({
|
|
330
539
|
id: 'root',
|
|
331
540
|
connector: (node) =>
|
|
332
|
-
|
|
333
|
-
pipe(
|
|
541
|
+
Atom.make((get) =>
|
|
542
|
+
Function.pipe(
|
|
334
543
|
get(node),
|
|
335
544
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
336
545
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -339,25 +548,29 @@ describe('GraphBuilder', () => {
|
|
|
339
548
|
),
|
|
340
549
|
),
|
|
341
550
|
}),
|
|
342
|
-
|
|
551
|
+
GraphBuilder.createExtensionRaw({
|
|
343
552
|
id: 'connector1',
|
|
344
553
|
connector: (node) =>
|
|
345
|
-
|
|
346
|
-
pipe(
|
|
554
|
+
Atom.make((get) =>
|
|
555
|
+
Function.pipe(
|
|
347
556
|
get(node),
|
|
348
|
-
Option.flatMap((node) =>
|
|
557
|
+
Option.flatMap((node) =>
|
|
558
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
559
|
+
),
|
|
349
560
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
350
561
|
Option.getOrElse(() => []),
|
|
351
562
|
),
|
|
352
563
|
),
|
|
353
564
|
}),
|
|
354
|
-
|
|
565
|
+
GraphBuilder.createExtensionRaw({
|
|
355
566
|
id: 'connector2',
|
|
356
567
|
connector: (node) =>
|
|
357
|
-
|
|
358
|
-
pipe(
|
|
568
|
+
Atom.make((get) =>
|
|
569
|
+
Function.pipe(
|
|
359
570
|
get(node),
|
|
360
|
-
Option.flatMap((node) =>
|
|
571
|
+
Option.flatMap((node) =>
|
|
572
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
573
|
+
),
|
|
361
574
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
362
575
|
Option.getOrElse(() => []),
|
|
363
576
|
),
|
|
@@ -368,64 +581,71 @@ describe('GraphBuilder', () => {
|
|
|
368
581
|
const graph = builder.graph;
|
|
369
582
|
|
|
370
583
|
let parentCount = 0;
|
|
371
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
584
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
372
585
|
parentCount++;
|
|
373
586
|
});
|
|
374
587
|
onTestFinished(() => parentCancel());
|
|
375
588
|
|
|
376
589
|
let independentCount = 0;
|
|
377
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
590
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
378
591
|
independentCount++;
|
|
379
592
|
});
|
|
380
593
|
onTestFinished(() => independentCancel());
|
|
381
594
|
|
|
382
595
|
let dependentCount = 0;
|
|
383
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
596
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
384
597
|
dependentCount++;
|
|
385
598
|
});
|
|
386
599
|
onTestFinished(() => dependentCancel());
|
|
387
600
|
|
|
388
601
|
// Counts should not increment until the node is expanded.
|
|
389
|
-
|
|
602
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
603
|
+
await GraphBuilder.flush(builder);
|
|
390
604
|
expect(parentCount).to.equal(1);
|
|
391
605
|
expect(independentCount).to.equal(0);
|
|
392
606
|
expect(dependentCount).to.equal(0);
|
|
393
607
|
|
|
394
608
|
// Counts should increment when the node is expanded.
|
|
395
|
-
|
|
609
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
610
|
+
await GraphBuilder.flush(builder);
|
|
396
611
|
expect(parentCount).to.equal(1);
|
|
397
612
|
expect(independentCount).to.equal(1);
|
|
398
613
|
expect(dependentCount).to.equal(1);
|
|
399
614
|
|
|
400
615
|
// Only dependent count should increment when the parent changes.
|
|
401
616
|
registry.set(name, 'updated');
|
|
617
|
+
await GraphBuilder.flush(builder);
|
|
402
618
|
expect(parentCount).to.equal(2);
|
|
403
619
|
expect(independentCount).to.equal(1);
|
|
404
620
|
expect(dependentCount).to.equal(2);
|
|
405
621
|
|
|
406
622
|
// Only independent count should increment when its state changes.
|
|
407
623
|
registry.set(sub, 'updated');
|
|
624
|
+
await GraphBuilder.flush(builder);
|
|
408
625
|
expect(parentCount).to.equal(2);
|
|
409
626
|
expect(independentCount).to.equal(2);
|
|
410
627
|
expect(dependentCount).to.equal(2);
|
|
411
628
|
|
|
412
629
|
// Independent count should update if its state changes even if the parent is removed.
|
|
413
|
-
|
|
630
|
+
Atom.batch(() => {
|
|
414
631
|
registry.set(name, 'removed');
|
|
415
632
|
registry.set(sub, 'batch');
|
|
416
633
|
});
|
|
634
|
+
await GraphBuilder.flush(builder);
|
|
417
635
|
expect(parentCount).to.equal(2);
|
|
418
636
|
expect(independentCount).to.equal(3);
|
|
419
637
|
expect(dependentCount).to.equal(2);
|
|
420
638
|
|
|
421
639
|
// Dependent count should increment when the node is added back.
|
|
422
640
|
registry.set(name, 'added');
|
|
641
|
+
await GraphBuilder.flush(builder);
|
|
423
642
|
expect(parentCount).to.equal(3);
|
|
424
643
|
expect(independentCount).to.equal(3);
|
|
425
644
|
expect(dependentCount).to.equal(3);
|
|
426
645
|
|
|
427
646
|
// Counts should not increment when the node is expanded again.
|
|
428
|
-
|
|
647
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
648
|
+
await GraphBuilder.flush(builder);
|
|
429
649
|
expect(parentCount).to.equal(3);
|
|
430
650
|
expect(independentCount).to.equal(3);
|
|
431
651
|
expect(dependentCount).to.equal(3);
|
|
@@ -433,13 +653,14 @@ describe('GraphBuilder', () => {
|
|
|
433
653
|
|
|
434
654
|
test('eager graph expansion', async () => {
|
|
435
655
|
const registry = Registry.make();
|
|
436
|
-
const builder =
|
|
437
|
-
|
|
438
|
-
|
|
656
|
+
const builder = GraphBuilder.make({ registry });
|
|
657
|
+
GraphBuilder.addExtension(
|
|
658
|
+
builder,
|
|
659
|
+
GraphBuilder.createExtensionRaw({
|
|
439
660
|
id: 'connector',
|
|
440
661
|
connector: (node) => {
|
|
441
|
-
return
|
|
442
|
-
pipe(
|
|
662
|
+
return Atom.make((get) =>
|
|
663
|
+
Function.pipe(
|
|
443
664
|
get(node),
|
|
444
665
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
445
666
|
Option.filter((data) => data <= 5),
|
|
@@ -454,14 +675,14 @@ describe('GraphBuilder', () => {
|
|
|
454
675
|
let count = 0;
|
|
455
676
|
const trigger = new Trigger();
|
|
456
677
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
457
|
-
builder.graph
|
|
678
|
+
Graph.expand(builder.graph, id, 'child');
|
|
458
679
|
count++;
|
|
459
680
|
if (count === 5) {
|
|
460
681
|
trigger.wake();
|
|
461
682
|
}
|
|
462
683
|
});
|
|
463
684
|
|
|
464
|
-
builder.graph.
|
|
685
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
465
686
|
await trigger.wait();
|
|
466
687
|
expect(count).to.equal(5);
|
|
467
688
|
});
|
|
@@ -469,13 +690,14 @@ describe('GraphBuilder', () => {
|
|
|
469
690
|
|
|
470
691
|
describe('explore', () => {
|
|
471
692
|
test('works', async () => {
|
|
472
|
-
const builder =
|
|
473
|
-
|
|
474
|
-
|
|
693
|
+
const builder = GraphBuilder.make();
|
|
694
|
+
GraphBuilder.addExtension(
|
|
695
|
+
builder,
|
|
696
|
+
GraphBuilder.createExtensionRaw({
|
|
475
697
|
id: 'connector',
|
|
476
698
|
connector: (node) =>
|
|
477
|
-
|
|
478
|
-
pipe(
|
|
699
|
+
Atom.make((get) =>
|
|
700
|
+
Function.pipe(
|
|
479
701
|
get(node),
|
|
480
702
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
481
703
|
Option.filter((data) => data <= 5),
|
|
@@ -487,7 +709,8 @@ describe('GraphBuilder', () => {
|
|
|
487
709
|
);
|
|
488
710
|
|
|
489
711
|
let count = 0;
|
|
490
|
-
await
|
|
712
|
+
await GraphBuilder.explore(builder, {
|
|
713
|
+
relation: 'child',
|
|
491
714
|
visitor: () => {
|
|
492
715
|
count++;
|
|
493
716
|
},
|
|
@@ -496,4 +719,665 @@ describe('GraphBuilder', () => {
|
|
|
496
719
|
expect(count).to.equal(6);
|
|
497
720
|
});
|
|
498
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
|
+
});
|
|
499
1383
|
});
|