@dxos/app-graph 0.8.3 → 0.8.4-main.1c7ec43d41
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/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-WJJ5KEOH.mjs +1477 -0
- package/dist/lib/neutral/chunk-WJJ5KEOH.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +40 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/scheduler.mjs +15 -0
- package/dist/lib/neutral/scheduler.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +40 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/atoms.d.ts +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/scheduler.browser.d.ts +2 -0
- package/dist/types/src/scheduler.browser.d.ts.map +1 -0
- package/dist/types/src/scheduler.d.ts +8 -0
- package/dist/types/src/scheduler.d.ts.map +1 -0
- package/dist/types/src/stories/EchoGraph.stories.d.ts +6 -13
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +40 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +53 -42
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1193 -126
- package/src/graph-builder.ts +753 -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/scheduler.browser.ts +5 -0
- package/src/scheduler.ts +17 -0
- package/src/stories/EchoGraph.stories.tsx +178 -255
- package/src/stories/Tree.tsx +1 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +101 -0
- package/dist/lib/browser/index.mjs +0 -778
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node/index.cjs +0 -816
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -780
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- 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,184 @@ 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
|
|
|
488
|
+
describe('inline nodes', () => {
|
|
489
|
+
const parent = (child?: Node.NodeArg<any>): Node.NodeArg<any> => ({
|
|
490
|
+
id: 'parent-node',
|
|
491
|
+
type: EXAMPLE_TYPE,
|
|
492
|
+
data: null,
|
|
493
|
+
nodes: child ? [child] : [],
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const inlineChild = (overrides?: Partial<Node.NodeArg<any>>): Node.NodeArg<any> => ({
|
|
497
|
+
id: 'inline-child',
|
|
498
|
+
type: EXAMPLE_TYPE,
|
|
499
|
+
data: null,
|
|
500
|
+
...overrides,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const makeGraph = () => {
|
|
504
|
+
const registry = Registry.make();
|
|
505
|
+
const builder = GraphBuilder.make({ registry });
|
|
506
|
+
const nodesAtom = Atom.make<Node.NodeArg<any>[]>([]);
|
|
507
|
+
GraphBuilder.addExtension(
|
|
508
|
+
builder,
|
|
509
|
+
GraphBuilder.createExtensionRaw({
|
|
510
|
+
id: 'inline-connector',
|
|
511
|
+
connector: () => Atom.make((get) => get(nodesAtom)),
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
const graph = builder.graph;
|
|
515
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
516
|
+
return { registry, builder, graph, nodesAtom };
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const getInlineChild = (graph: Graph.ExpandableGraph) =>
|
|
520
|
+
Graph.getNode(graph, 'root/parent-node/inline-child').pipe(Option.getOrNull);
|
|
521
|
+
|
|
522
|
+
test('are removed when connector re-runs with data change', async () => {
|
|
523
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
524
|
+
registry.set(nodesAtom, [parent(inlineChild())]);
|
|
525
|
+
await GraphBuilder.flush(builder);
|
|
526
|
+
|
|
527
|
+
expect(getInlineChild(graph)).to.not.be.null;
|
|
528
|
+
|
|
529
|
+
// Remove inline child while also changing parent data to trigger the update.
|
|
530
|
+
registry.set(nodesAtom, [{ ...parent(), data: 'v2' }]);
|
|
531
|
+
await GraphBuilder.flush(builder);
|
|
532
|
+
|
|
533
|
+
expect(getInlineChild(graph)).to.be.null;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('are removed when only inline children change', async () => {
|
|
537
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
538
|
+
registry.set(nodesAtom, [parent(inlineChild())]);
|
|
539
|
+
await GraphBuilder.flush(builder);
|
|
540
|
+
|
|
541
|
+
expect(getInlineChild(graph)).to.not.be.null;
|
|
542
|
+
|
|
543
|
+
// Remove inline child without touching parent — tests change detection covers inline children.
|
|
544
|
+
registry.set(nodesAtom, [parent()]);
|
|
545
|
+
await GraphBuilder.flush(builder);
|
|
546
|
+
|
|
547
|
+
expect(getInlineChild(graph)).to.be.null;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('are added when connector re-runs', async () => {
|
|
551
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
552
|
+
registry.set(nodesAtom, [parent()]);
|
|
553
|
+
await GraphBuilder.flush(builder);
|
|
554
|
+
|
|
555
|
+
expect(getInlineChild(graph)).to.be.null;
|
|
556
|
+
|
|
557
|
+
registry.set(nodesAtom, [parent(inlineChild())]);
|
|
558
|
+
await GraphBuilder.flush(builder);
|
|
559
|
+
|
|
560
|
+
expect(getInlineChild(graph)).to.not.be.null;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('reactively update data', async ({ expect }) => {
|
|
564
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
565
|
+
registry.set(nodesAtom, [parent(inlineChild({ data: 'v1' }))]);
|
|
566
|
+
await GraphBuilder.flush(builder);
|
|
567
|
+
|
|
568
|
+
expect(getInlineChild(graph)?.data).to.equal('v1');
|
|
569
|
+
|
|
570
|
+
// Change only the inline child's data — parent is unchanged.
|
|
571
|
+
registry.set(nodesAtom, [parent(inlineChild({ data: 'v2' }))]);
|
|
572
|
+
await GraphBuilder.flush(builder);
|
|
573
|
+
|
|
574
|
+
expect(getInlineChild(graph)?.data).to.equal('v2');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('reactively update properties', async ({ expect }) => {
|
|
578
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
579
|
+
registry.set(nodesAtom, [parent(inlineChild({ properties: { label: 'before' } }))]);
|
|
580
|
+
await GraphBuilder.flush(builder);
|
|
581
|
+
|
|
582
|
+
expect(getInlineChild(graph)?.properties.label).to.equal('before');
|
|
583
|
+
|
|
584
|
+
registry.set(nodesAtom, [parent(inlineChild({ properties: { label: 'after' } }))]);
|
|
585
|
+
await GraphBuilder.flush(builder);
|
|
586
|
+
|
|
587
|
+
expect(getInlineChild(graph)?.properties.label).to.equal('after');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('deeply nested inline nodes reactively update', async ({ expect }) => {
|
|
591
|
+
const { registry, builder, graph, nodesAtom } = makeGraph();
|
|
592
|
+
|
|
593
|
+
const withGrandchild = (data: string) =>
|
|
594
|
+
parent({
|
|
595
|
+
id: 'child',
|
|
596
|
+
type: EXAMPLE_TYPE,
|
|
597
|
+
data: null,
|
|
598
|
+
nodes: [{ id: 'grandchild', type: EXAMPLE_TYPE, data }],
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
registry.set(nodesAtom, [withGrandchild('v1')]);
|
|
602
|
+
await GraphBuilder.flush(builder);
|
|
603
|
+
|
|
604
|
+
expect(Graph.getNode(graph, 'root/parent-node/child/grandchild').pipe(Option.getOrNull)?.data).to.equal('v1');
|
|
605
|
+
|
|
606
|
+
// Change only the grandchild's data — all ancestors unchanged.
|
|
607
|
+
registry.set(nodesAtom, [withGrandchild('v2')]);
|
|
608
|
+
await GraphBuilder.flush(builder);
|
|
609
|
+
|
|
610
|
+
expect(Graph.getNode(graph, 'root/parent-node/child/grandchild').pipe(Option.getOrNull)?.data).to.equal('v2');
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
222
614
|
test('sort edges', async () => {
|
|
223
615
|
const registry = Registry.make();
|
|
224
|
-
const builder =
|
|
225
|
-
const nodes =
|
|
616
|
+
const builder = GraphBuilder.make({ registry });
|
|
617
|
+
const nodes = Atom.make([
|
|
226
618
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
227
619
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
228
620
|
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
229
621
|
]);
|
|
230
|
-
|
|
231
|
-
|
|
622
|
+
GraphBuilder.addExtension(
|
|
623
|
+
builder,
|
|
624
|
+
GraphBuilder.createExtensionRaw({
|
|
232
625
|
id: 'connector',
|
|
233
|
-
connector: () =>
|
|
626
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
234
627
|
}),
|
|
235
628
|
);
|
|
236
629
|
const graph = builder.graph;
|
|
237
|
-
|
|
630
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
631
|
+
await GraphBuilder.flush(builder);
|
|
238
632
|
|
|
239
633
|
{
|
|
240
|
-
const nodes = registry.get(graph.connections(
|
|
634
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
241
635
|
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));
|
|
636
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(1)));
|
|
637
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(2)));
|
|
638
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(3)));
|
|
245
639
|
}
|
|
246
640
|
|
|
247
641
|
registry.set(nodes, [
|
|
@@ -249,31 +643,29 @@ describe('GraphBuilder', () => {
|
|
|
249
643
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
250
644
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
251
645
|
]);
|
|
252
|
-
|
|
253
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
254
|
-
await sleep(0);
|
|
646
|
+
await GraphBuilder.flush(builder);
|
|
255
647
|
|
|
256
648
|
{
|
|
257
|
-
const nodes = registry.get(graph.connections(
|
|
649
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
258
650
|
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));
|
|
651
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
652
|
+
expect(nodes[1].id).to.equal(qualifyId('root', exampleId(1)));
|
|
653
|
+
expect(nodes[2].id).to.equal(qualifyId('root', exampleId(2)));
|
|
262
654
|
}
|
|
263
655
|
});
|
|
264
656
|
|
|
265
|
-
test('updates are constrained', () => {
|
|
657
|
+
test('updates are constrained', async () => {
|
|
266
658
|
const registry = Registry.make();
|
|
267
|
-
const builder =
|
|
268
|
-
const name =
|
|
269
|
-
const sub =
|
|
659
|
+
const builder = GraphBuilder.make({ registry });
|
|
660
|
+
const name = Atom.make('default');
|
|
661
|
+
const sub = Atom.make('default');
|
|
270
662
|
|
|
271
|
-
|
|
272
|
-
|
|
663
|
+
GraphBuilder.addExtension(builder, [
|
|
664
|
+
GraphBuilder.createExtensionRaw({
|
|
273
665
|
id: 'root',
|
|
274
666
|
connector: (node) =>
|
|
275
|
-
|
|
276
|
-
pipe(
|
|
667
|
+
Atom.make((get) =>
|
|
668
|
+
Function.pipe(
|
|
277
669
|
get(node),
|
|
278
670
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
279
671
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -282,25 +674,29 @@ describe('GraphBuilder', () => {
|
|
|
282
674
|
),
|
|
283
675
|
),
|
|
284
676
|
}),
|
|
285
|
-
|
|
677
|
+
GraphBuilder.createExtensionRaw({
|
|
286
678
|
id: 'connector1',
|
|
287
679
|
connector: (node) =>
|
|
288
|
-
|
|
289
|
-
pipe(
|
|
680
|
+
Atom.make((get) =>
|
|
681
|
+
Function.pipe(
|
|
290
682
|
get(node),
|
|
291
|
-
Option.flatMap((node) =>
|
|
683
|
+
Option.flatMap((node) =>
|
|
684
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
685
|
+
),
|
|
292
686
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
293
687
|
Option.getOrElse(() => []),
|
|
294
688
|
),
|
|
295
689
|
),
|
|
296
690
|
}),
|
|
297
|
-
|
|
691
|
+
GraphBuilder.createExtensionRaw({
|
|
298
692
|
id: 'connector2',
|
|
299
693
|
connector: (node) =>
|
|
300
|
-
|
|
301
|
-
pipe(
|
|
694
|
+
Atom.make((get) =>
|
|
695
|
+
Function.pipe(
|
|
302
696
|
get(node),
|
|
303
|
-
Option.flatMap((node) =>
|
|
697
|
+
Option.flatMap((node) =>
|
|
698
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
699
|
+
),
|
|
304
700
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
305
701
|
Option.getOrElse(() => []),
|
|
306
702
|
),
|
|
@@ -311,64 +707,71 @@ describe('GraphBuilder', () => {
|
|
|
311
707
|
const graph = builder.graph;
|
|
312
708
|
|
|
313
709
|
let parentCount = 0;
|
|
314
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
710
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
315
711
|
parentCount++;
|
|
316
712
|
});
|
|
317
713
|
onTestFinished(() => parentCancel());
|
|
318
714
|
|
|
319
715
|
let independentCount = 0;
|
|
320
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
716
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
321
717
|
independentCount++;
|
|
322
718
|
});
|
|
323
719
|
onTestFinished(() => independentCancel());
|
|
324
720
|
|
|
325
721
|
let dependentCount = 0;
|
|
326
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
722
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
327
723
|
dependentCount++;
|
|
328
724
|
});
|
|
329
725
|
onTestFinished(() => dependentCancel());
|
|
330
726
|
|
|
331
727
|
// Counts should not increment until the node is expanded.
|
|
332
|
-
|
|
728
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
729
|
+
await GraphBuilder.flush(builder);
|
|
333
730
|
expect(parentCount).to.equal(1);
|
|
334
731
|
expect(independentCount).to.equal(0);
|
|
335
732
|
expect(dependentCount).to.equal(0);
|
|
336
733
|
|
|
337
734
|
// Counts should increment when the node is expanded.
|
|
338
|
-
|
|
735
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
736
|
+
await GraphBuilder.flush(builder);
|
|
339
737
|
expect(parentCount).to.equal(1);
|
|
340
738
|
expect(independentCount).to.equal(1);
|
|
341
739
|
expect(dependentCount).to.equal(1);
|
|
342
740
|
|
|
343
741
|
// Only dependent count should increment when the parent changes.
|
|
344
742
|
registry.set(name, 'updated');
|
|
743
|
+
await GraphBuilder.flush(builder);
|
|
345
744
|
expect(parentCount).to.equal(2);
|
|
346
745
|
expect(independentCount).to.equal(1);
|
|
347
746
|
expect(dependentCount).to.equal(2);
|
|
348
747
|
|
|
349
748
|
// Only independent count should increment when its state changes.
|
|
350
749
|
registry.set(sub, 'updated');
|
|
750
|
+
await GraphBuilder.flush(builder);
|
|
351
751
|
expect(parentCount).to.equal(2);
|
|
352
752
|
expect(independentCount).to.equal(2);
|
|
353
753
|
expect(dependentCount).to.equal(2);
|
|
354
754
|
|
|
355
755
|
// Independent count should update if its state changes even if the parent is removed.
|
|
356
|
-
|
|
756
|
+
Atom.batch(() => {
|
|
357
757
|
registry.set(name, 'removed');
|
|
358
758
|
registry.set(sub, 'batch');
|
|
359
759
|
});
|
|
760
|
+
await GraphBuilder.flush(builder);
|
|
360
761
|
expect(parentCount).to.equal(2);
|
|
361
762
|
expect(independentCount).to.equal(3);
|
|
362
763
|
expect(dependentCount).to.equal(2);
|
|
363
764
|
|
|
364
765
|
// Dependent count should increment when the node is added back.
|
|
365
766
|
registry.set(name, 'added');
|
|
767
|
+
await GraphBuilder.flush(builder);
|
|
366
768
|
expect(parentCount).to.equal(3);
|
|
367
769
|
expect(independentCount).to.equal(3);
|
|
368
770
|
expect(dependentCount).to.equal(3);
|
|
369
771
|
|
|
370
772
|
// Counts should not increment when the node is expanded again.
|
|
371
|
-
|
|
773
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
774
|
+
await GraphBuilder.flush(builder);
|
|
372
775
|
expect(parentCount).to.equal(3);
|
|
373
776
|
expect(independentCount).to.equal(3);
|
|
374
777
|
expect(dependentCount).to.equal(3);
|
|
@@ -376,13 +779,14 @@ describe('GraphBuilder', () => {
|
|
|
376
779
|
|
|
377
780
|
test('eager graph expansion', async () => {
|
|
378
781
|
const registry = Registry.make();
|
|
379
|
-
const builder =
|
|
380
|
-
|
|
381
|
-
|
|
782
|
+
const builder = GraphBuilder.make({ registry });
|
|
783
|
+
GraphBuilder.addExtension(
|
|
784
|
+
builder,
|
|
785
|
+
GraphBuilder.createExtensionRaw({
|
|
382
786
|
id: 'connector',
|
|
383
787
|
connector: (node) => {
|
|
384
|
-
return
|
|
385
|
-
pipe(
|
|
788
|
+
return Atom.make((get) =>
|
|
789
|
+
Function.pipe(
|
|
386
790
|
get(node),
|
|
387
791
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
388
792
|
Option.filter((data) => data <= 5),
|
|
@@ -397,14 +801,14 @@ describe('GraphBuilder', () => {
|
|
|
397
801
|
let count = 0;
|
|
398
802
|
const trigger = new Trigger();
|
|
399
803
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
400
|
-
builder.graph
|
|
804
|
+
Graph.expand(builder.graph, id, 'child');
|
|
401
805
|
count++;
|
|
402
806
|
if (count === 5) {
|
|
403
807
|
trigger.wake();
|
|
404
808
|
}
|
|
405
809
|
});
|
|
406
810
|
|
|
407
|
-
builder.graph.
|
|
811
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
408
812
|
await trigger.wait();
|
|
409
813
|
expect(count).to.equal(5);
|
|
410
814
|
});
|
|
@@ -412,13 +816,14 @@ describe('GraphBuilder', () => {
|
|
|
412
816
|
|
|
413
817
|
describe('explore', () => {
|
|
414
818
|
test('works', async () => {
|
|
415
|
-
const builder =
|
|
416
|
-
|
|
417
|
-
|
|
819
|
+
const builder = GraphBuilder.make();
|
|
820
|
+
GraphBuilder.addExtension(
|
|
821
|
+
builder,
|
|
822
|
+
GraphBuilder.createExtensionRaw({
|
|
418
823
|
id: 'connector',
|
|
419
824
|
connector: (node) =>
|
|
420
|
-
|
|
421
|
-
pipe(
|
|
825
|
+
Atom.make((get) =>
|
|
826
|
+
Function.pipe(
|
|
422
827
|
get(node),
|
|
423
828
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
424
829
|
Option.filter((data) => data <= 5),
|
|
@@ -430,7 +835,8 @@ describe('GraphBuilder', () => {
|
|
|
430
835
|
);
|
|
431
836
|
|
|
432
837
|
let count = 0;
|
|
433
|
-
await
|
|
838
|
+
await GraphBuilder.explore(builder, {
|
|
839
|
+
relation: 'child',
|
|
434
840
|
visitor: () => {
|
|
435
841
|
count++;
|
|
436
842
|
},
|
|
@@ -439,4 +845,665 @@ describe('GraphBuilder', () => {
|
|
|
439
845
|
expect(count).to.equal(6);
|
|
440
846
|
});
|
|
441
847
|
});
|
|
848
|
+
|
|
849
|
+
describe('helpers', () => {
|
|
850
|
+
describe('createConnector', () => {
|
|
851
|
+
test('creates connector with type inference', async () => {
|
|
852
|
+
const registry = Registry.make();
|
|
853
|
+
const builder = GraphBuilder.make({ registry });
|
|
854
|
+
const graph = builder.graph;
|
|
855
|
+
|
|
856
|
+
const matcher = (node: Node.Node) => NodeMatcher.whenId('root')(node);
|
|
857
|
+
const factory = (node: Node.Node) => [{ id: 'child', type: EXAMPLE_TYPE, data: node.id }];
|
|
858
|
+
|
|
859
|
+
const connector = GraphBuilder.createConnector(matcher, factory);
|
|
860
|
+
|
|
861
|
+
GraphBuilder.addExtension(
|
|
862
|
+
builder,
|
|
863
|
+
GraphBuilder.createExtensionRaw({
|
|
864
|
+
id: 'test-connector',
|
|
865
|
+
connector,
|
|
866
|
+
}),
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
870
|
+
await GraphBuilder.flush(builder);
|
|
871
|
+
|
|
872
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
873
|
+
expect(connections).has.length(1);
|
|
874
|
+
expect(connections[0].id).to.equal('root/child');
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
describe('createExtension', () => {
|
|
879
|
+
test('works with Effect connector', async () => {
|
|
880
|
+
const registry = Registry.make();
|
|
881
|
+
const builder = GraphBuilder.make({ registry });
|
|
882
|
+
const graph = builder.graph;
|
|
883
|
+
|
|
884
|
+
const extensions = Effect.runSync(
|
|
885
|
+
GraphBuilder.createExtension({
|
|
886
|
+
id: 'test-extension',
|
|
887
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
888
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: node.data }]),
|
|
889
|
+
}),
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
893
|
+
|
|
894
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
895
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
896
|
+
Graph.expand(graph, 'parent', 'child');
|
|
897
|
+
await GraphBuilder.flush(builder);
|
|
898
|
+
|
|
899
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
900
|
+
expect(connections).has.length(1);
|
|
901
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
902
|
+
expect(connections[0].data).to.equal('test');
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test('works with Effect actions', async () => {
|
|
906
|
+
const registry = Registry.make();
|
|
907
|
+
const builder = GraphBuilder.make({ registry });
|
|
908
|
+
const graph = builder.graph;
|
|
909
|
+
|
|
910
|
+
const extensions = Effect.runSync(
|
|
911
|
+
GraphBuilder.createExtension({
|
|
912
|
+
id: 'test-extension',
|
|
913
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
914
|
+
actions: (node, get) =>
|
|
915
|
+
Effect.succeed([
|
|
916
|
+
{
|
|
917
|
+
id: 'test-action',
|
|
918
|
+
data: () => Effect.void,
|
|
919
|
+
properties: { label: 'Test' },
|
|
920
|
+
},
|
|
921
|
+
]),
|
|
922
|
+
}),
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
926
|
+
|
|
927
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
928
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
929
|
+
Graph.expand(graph, 'parent', 'child');
|
|
930
|
+
await GraphBuilder.flush(builder);
|
|
931
|
+
|
|
932
|
+
const edges = registry.get(graph.edges('parent'));
|
|
933
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.have.length(1);
|
|
934
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/test-action');
|
|
935
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.have.length(0);
|
|
936
|
+
const actions = registry.get(graph.actions('parent'));
|
|
937
|
+
expect(actions).has.length(1);
|
|
938
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test('actions expand automatically with child relation', async ({ expect }) => {
|
|
942
|
+
const registry = Registry.make();
|
|
943
|
+
const builder = GraphBuilder.make({ registry });
|
|
944
|
+
const graph = builder.graph;
|
|
945
|
+
|
|
946
|
+
const extensions = Effect.runSync(
|
|
947
|
+
GraphBuilder.createExtension({
|
|
948
|
+
id: 'test-extension',
|
|
949
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
950
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: 'c' }]),
|
|
951
|
+
actions: (node, get) =>
|
|
952
|
+
Effect.succeed([{ id: 'act1', data: () => Effect.void, properties: { label: 'A' } }]),
|
|
953
|
+
}),
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
957
|
+
|
|
958
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
959
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
960
|
+
Graph.expand(graph, 'parent', 'child');
|
|
961
|
+
await GraphBuilder.flush(builder);
|
|
962
|
+
|
|
963
|
+
const edges = registry.get(graph.edges('parent'));
|
|
964
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.include('parent/child');
|
|
965
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/act1');
|
|
966
|
+
const actions = registry.get(graph.actions('parent'));
|
|
967
|
+
expect(actions).has.length(1);
|
|
968
|
+
expect(actions[0].id).to.equal('parent/act1');
|
|
969
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
970
|
+
expect(connections).has.length(1);
|
|
971
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test('actions appear when extension registered after expand', async ({ expect }) => {
|
|
975
|
+
const registry = Registry.make();
|
|
976
|
+
const builder = GraphBuilder.make({ registry });
|
|
977
|
+
const graph = builder.graph;
|
|
978
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
979
|
+
|
|
980
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
981
|
+
Graph.expand(graph, 'parent', 'child');
|
|
982
|
+
await GraphBuilder.flush(builder);
|
|
983
|
+
|
|
984
|
+
expect(registry.get(graph.actions('parent'))).to.have.length(0);
|
|
985
|
+
|
|
986
|
+
const extensions = Effect.runSync(
|
|
987
|
+
GraphBuilder.createExtension({
|
|
988
|
+
id: 'late-extension',
|
|
989
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
990
|
+
actions: (node, get) =>
|
|
991
|
+
Effect.succeed([{ id: 'late-act', data: () => Effect.void, properties: { label: 'Late' } }]),
|
|
992
|
+
}),
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
996
|
+
await GraphBuilder.flush(builder);
|
|
997
|
+
|
|
998
|
+
const edges = registry.get(graph.edges('parent'));
|
|
999
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('parent/late-act');
|
|
1000
|
+
const actions = registry.get(graph.actions('parent'));
|
|
1001
|
+
expect(actions).has.length(1);
|
|
1002
|
+
expect(actions[0].id).to.equal('parent/late-act');
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
test('_actionContext captures and provides services to action execution', async () => {
|
|
1006
|
+
const registry = Registry.make();
|
|
1007
|
+
const builder = GraphBuilder.make({ registry });
|
|
1008
|
+
const graph = builder.graph;
|
|
1009
|
+
|
|
1010
|
+
// Define a test service using Context.GenericTag pattern.
|
|
1011
|
+
interface TestServiceInterface {
|
|
1012
|
+
getValue(): number;
|
|
1013
|
+
}
|
|
1014
|
+
const TestService = Context.GenericTag<TestServiceInterface>('TestService');
|
|
1015
|
+
|
|
1016
|
+
// Track whether the action was executed with the correct context.
|
|
1017
|
+
let executionResult: number | null = null;
|
|
1018
|
+
|
|
1019
|
+
// Create extension with service requirement.
|
|
1020
|
+
// Note: The actions callback must USE the service for R to be inferred correctly.
|
|
1021
|
+
const extensions = Effect.runSync(
|
|
1022
|
+
GraphBuilder.createExtension({
|
|
1023
|
+
id: 'test-extension',
|
|
1024
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1025
|
+
actions: (node, get) =>
|
|
1026
|
+
// Use TestService in the callback to include it in R.
|
|
1027
|
+
Effect.gen(function* () {
|
|
1028
|
+
const service = yield* TestService;
|
|
1029
|
+
return [
|
|
1030
|
+
{
|
|
1031
|
+
id: 'test-action',
|
|
1032
|
+
data: () =>
|
|
1033
|
+
Effect.gen(function* () {
|
|
1034
|
+
// Action can use the same service from captured context.
|
|
1035
|
+
const svc = yield* TestService;
|
|
1036
|
+
executionResult = svc.getValue();
|
|
1037
|
+
}).pipe(Effect.asVoid),
|
|
1038
|
+
properties: { label: `Test ${service.getValue()}` },
|
|
1039
|
+
},
|
|
1040
|
+
];
|
|
1041
|
+
}),
|
|
1042
|
+
}).pipe(Effect.provideService(TestService, { getValue: () => 42 })),
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1046
|
+
|
|
1047
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1048
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1049
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1050
|
+
await GraphBuilder.flush(builder);
|
|
1051
|
+
|
|
1052
|
+
const actions = registry.get(graph.actions('parent'));
|
|
1053
|
+
expect(actions).has.length(1);
|
|
1054
|
+
|
|
1055
|
+
// Verify _actionContext is captured.
|
|
1056
|
+
const action = actions[0] as Node.Action;
|
|
1057
|
+
expect(action._actionContext).to.not.be.undefined;
|
|
1058
|
+
|
|
1059
|
+
// Execute the action with the captured context.
|
|
1060
|
+
const actionEffect = action.data();
|
|
1061
|
+
const effectWithContext = action._actionContext
|
|
1062
|
+
? actionEffect.pipe(Effect.provide(action._actionContext))
|
|
1063
|
+
: actionEffect;
|
|
1064
|
+
|
|
1065
|
+
Effect.runSync(effectWithContext);
|
|
1066
|
+
|
|
1067
|
+
// Verify the service was accessible during execution.
|
|
1068
|
+
expect(executionResult).to.equal(42);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
test('works with resolver', async () => {
|
|
1072
|
+
const registry = Registry.make();
|
|
1073
|
+
const builder = GraphBuilder.make({ registry });
|
|
1074
|
+
const graph = builder.graph;
|
|
1075
|
+
|
|
1076
|
+
const extensions = Effect.runSync(
|
|
1077
|
+
GraphBuilder.createExtension({
|
|
1078
|
+
id: 'test-extension',
|
|
1079
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1080
|
+
resolver: (id, get) => Effect.succeed({ id, type: EXAMPLE_TYPE, properties: {}, data: 'resolved' }),
|
|
1081
|
+
}),
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1085
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
1086
|
+
|
|
1087
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
1088
|
+
expect(node).to.not.be.null;
|
|
1089
|
+
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
1090
|
+
expect(node?.data).to.equal('resolved');
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
test('works with connector and actions together', async () => {
|
|
1094
|
+
const registry = Registry.make();
|
|
1095
|
+
const builder = GraphBuilder.make({ registry });
|
|
1096
|
+
const graph = builder.graph;
|
|
1097
|
+
|
|
1098
|
+
const extensions = Effect.runSync(
|
|
1099
|
+
GraphBuilder.createExtension({
|
|
1100
|
+
id: 'test-extension',
|
|
1101
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1102
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: node.data }]),
|
|
1103
|
+
actions: (node, get) =>
|
|
1104
|
+
Effect.succeed([
|
|
1105
|
+
{
|
|
1106
|
+
id: 'test-action',
|
|
1107
|
+
data: () => Effect.void,
|
|
1108
|
+
properties: { label: 'Test' },
|
|
1109
|
+
},
|
|
1110
|
+
]),
|
|
1111
|
+
}),
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1115
|
+
|
|
1116
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1117
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1118
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1119
|
+
await GraphBuilder.flush(builder);
|
|
1120
|
+
|
|
1121
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1122
|
+
// Should have both the child node and the action node.
|
|
1123
|
+
expect(connections.length).to.be.greaterThanOrEqual(1);
|
|
1124
|
+
const childNode = connections.find((n) => n.id === 'parent/child');
|
|
1125
|
+
expect(childNode).to.not.be.undefined;
|
|
1126
|
+
expect(childNode?.data).to.equal('test');
|
|
1127
|
+
|
|
1128
|
+
const actions = registry.get(graph.actions('parent'));
|
|
1129
|
+
expect(actions).has.length(1);
|
|
1130
|
+
expect(actions[0].id).to.equal('parent/test-action');
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test('works with reactive connector using get context', async () => {
|
|
1134
|
+
const registry = Registry.make();
|
|
1135
|
+
const builder = GraphBuilder.make({ registry });
|
|
1136
|
+
const graph = builder.graph;
|
|
1137
|
+
|
|
1138
|
+
const state = Atom.make('initial');
|
|
1139
|
+
|
|
1140
|
+
const extensions = Effect.runSync(
|
|
1141
|
+
GraphBuilder.createExtension({
|
|
1142
|
+
id: 'test-extension',
|
|
1143
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1144
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: get(state) }]),
|
|
1145
|
+
}),
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1149
|
+
|
|
1150
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1151
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1152
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1153
|
+
await GraphBuilder.flush(builder);
|
|
1154
|
+
|
|
1155
|
+
{
|
|
1156
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1157
|
+
expect(connections).has.length(1);
|
|
1158
|
+
expect(connections[0].data).to.equal('initial');
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
registry.set(state, 'updated');
|
|
1162
|
+
await GraphBuilder.flush(builder);
|
|
1163
|
+
|
|
1164
|
+
{
|
|
1165
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1166
|
+
expect(connections).has.length(1);
|
|
1167
|
+
expect(connections[0].data).to.equal('updated');
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
describe('extension error handling', () => {
|
|
1173
|
+
test('connector failure is caught and logged, returns empty array', async () => {
|
|
1174
|
+
const registry = Registry.make();
|
|
1175
|
+
const builder = GraphBuilder.make({ registry });
|
|
1176
|
+
const graph = builder.graph;
|
|
1177
|
+
|
|
1178
|
+
const extensions = Effect.runSync(
|
|
1179
|
+
GraphBuilder.createExtension({
|
|
1180
|
+
id: 'failing-extension',
|
|
1181
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1182
|
+
connector: (node, get) => Effect.fail(new Error('Connector failed intentionally')),
|
|
1183
|
+
}),
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1187
|
+
|
|
1188
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1189
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1190
|
+
|
|
1191
|
+
// Should not throw, error is caught internally.
|
|
1192
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1193
|
+
await GraphBuilder.flush(builder);
|
|
1194
|
+
|
|
1195
|
+
// Should return empty connections since the connector failed.
|
|
1196
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1197
|
+
expect(connections).has.length(0);
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
test('actions failure is caught and logged, returns empty array', async () => {
|
|
1201
|
+
const registry = Registry.make();
|
|
1202
|
+
const builder = GraphBuilder.make({ registry });
|
|
1203
|
+
const graph = builder.graph;
|
|
1204
|
+
|
|
1205
|
+
const extensions = Effect.runSync(
|
|
1206
|
+
GraphBuilder.createExtension({
|
|
1207
|
+
id: 'failing-actions-extension',
|
|
1208
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1209
|
+
actions: (node, get) => Effect.fail(new Error('Actions failed intentionally')),
|
|
1210
|
+
}),
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1214
|
+
|
|
1215
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1216
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1217
|
+
|
|
1218
|
+
// Should not throw, error is caught internally.
|
|
1219
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1220
|
+
await GraphBuilder.flush(builder);
|
|
1221
|
+
|
|
1222
|
+
// Should return empty actions since the actions callback failed.
|
|
1223
|
+
const actions = registry.get(graph.actions('parent'));
|
|
1224
|
+
expect(actions).has.length(0);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
test('resolver failure is caught and logged, returns null', async () => {
|
|
1228
|
+
const registry = Registry.make();
|
|
1229
|
+
const builder = GraphBuilder.make({ registry });
|
|
1230
|
+
const graph = builder.graph;
|
|
1231
|
+
|
|
1232
|
+
const extensions = Effect.runSync(
|
|
1233
|
+
GraphBuilder.createExtension({
|
|
1234
|
+
id: 'failing-resolver-extension',
|
|
1235
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1236
|
+
resolver: (id, get) => Effect.fail(new Error('Resolver failed intentionally')),
|
|
1237
|
+
}),
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1241
|
+
|
|
1242
|
+
// Should not throw, error is caught internally.
|
|
1243
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
1244
|
+
|
|
1245
|
+
// Should return null/none since the resolver failed.
|
|
1246
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
1247
|
+
expect(node).to.be.null;
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
test('failing extension does not affect other extensions', async () => {
|
|
1251
|
+
const registry = Registry.make();
|
|
1252
|
+
const builder = GraphBuilder.make({ registry });
|
|
1253
|
+
const graph = builder.graph;
|
|
1254
|
+
|
|
1255
|
+
// Add a failing extension.
|
|
1256
|
+
const failingExtensions = Effect.runSync(
|
|
1257
|
+
GraphBuilder.createExtension({
|
|
1258
|
+
id: 'failing-extension',
|
|
1259
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1260
|
+
connector: (node, get) => Effect.fail(new Error('This one fails')),
|
|
1261
|
+
}),
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
// Add a working extension.
|
|
1265
|
+
const workingExtensions = Effect.runSync(
|
|
1266
|
+
GraphBuilder.createExtension({
|
|
1267
|
+
id: 'working-extension',
|
|
1268
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
1269
|
+
connector: (node, get) =>
|
|
1270
|
+
Effect.succeed([{ id: 'child-from-working', type: EXAMPLE_TYPE, data: 'success' }]),
|
|
1271
|
+
}),
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
GraphBuilder.addExtension(builder, failingExtensions);
|
|
1275
|
+
GraphBuilder.addExtension(builder, workingExtensions);
|
|
1276
|
+
|
|
1277
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1278
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
1279
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1280
|
+
await GraphBuilder.flush(builder);
|
|
1281
|
+
|
|
1282
|
+
// The working extension should still produce its node.
|
|
1283
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1284
|
+
expect(connections).has.length(1);
|
|
1285
|
+
expect(connections[0].id).to.equal('parent/child-from-working');
|
|
1286
|
+
expect(connections[0].data).to.equal('success');
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
describe('createTypeExtension', () => {
|
|
1291
|
+
test('creates extension matching by schema type with inferred object type', async () => {
|
|
1292
|
+
const registry = Registry.make();
|
|
1293
|
+
const builder = GraphBuilder.make({ registry });
|
|
1294
|
+
const graph = builder.graph;
|
|
1295
|
+
|
|
1296
|
+
const extensions = Effect.runSync(
|
|
1297
|
+
GraphBuilder.createTypeExtension({
|
|
1298
|
+
id: 'type-extension',
|
|
1299
|
+
type: TestSchema.Person,
|
|
1300
|
+
connector: (object) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: object }]),
|
|
1301
|
+
}),
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
1305
|
+
|
|
1306
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
1307
|
+
const testObject = Obj.make(TestSchema.Person, { name: 'Test' });
|
|
1308
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: testObject });
|
|
1309
|
+
Graph.expand(graph, 'parent', 'child');
|
|
1310
|
+
await GraphBuilder.flush(builder);
|
|
1311
|
+
|
|
1312
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1313
|
+
expect(connections).has.length(1);
|
|
1314
|
+
expect(connections[0].id).to.equal('parent/child');
|
|
1315
|
+
expect(connections[0].data).to.equal(testObject);
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
describe('path-based ID qualification', () => {
|
|
1320
|
+
test('rejects segment IDs containing slash', async () => {
|
|
1321
|
+
const registry = Registry.make();
|
|
1322
|
+
const builder = GraphBuilder.make({ registry });
|
|
1323
|
+
GraphBuilder.addExtension(
|
|
1324
|
+
builder,
|
|
1325
|
+
GraphBuilder.createExtensionRaw({
|
|
1326
|
+
id: 'bad-connector',
|
|
1327
|
+
connector: () => Atom.make([{ id: 'foo/bar', type: EXAMPLE_TYPE, data: null }]),
|
|
1328
|
+
}),
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
expect(() => Graph.expand(builder.graph, Node.RootId, 'child')).toThrow(/must not contain/);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
test('multi-level path qualification', async () => {
|
|
1335
|
+
const registry = Registry.make();
|
|
1336
|
+
const builder = GraphBuilder.make({ registry });
|
|
1337
|
+
GraphBuilder.addExtension(builder, [
|
|
1338
|
+
GraphBuilder.createExtensionRaw({
|
|
1339
|
+
id: 'level1',
|
|
1340
|
+
connector: (node) =>
|
|
1341
|
+
Atom.make((get) =>
|
|
1342
|
+
Function.pipe(
|
|
1343
|
+
get(node),
|
|
1344
|
+
Option.filter((n) => n.id === 'root'),
|
|
1345
|
+
Option.map(() => [{ id: 'A', type: EXAMPLE_TYPE, data: 'a' }]),
|
|
1346
|
+
Option.getOrElse(() => []),
|
|
1347
|
+
),
|
|
1348
|
+
),
|
|
1349
|
+
}),
|
|
1350
|
+
GraphBuilder.createExtensionRaw({
|
|
1351
|
+
id: 'level2',
|
|
1352
|
+
connector: (node) =>
|
|
1353
|
+
Atom.make((get) =>
|
|
1354
|
+
Function.pipe(
|
|
1355
|
+
get(node),
|
|
1356
|
+
Option.filter((n) => n.id === 'root/A'),
|
|
1357
|
+
Option.map(() => [{ id: 'B', type: EXAMPLE_TYPE, data: 'b' }]),
|
|
1358
|
+
Option.getOrElse(() => []),
|
|
1359
|
+
),
|
|
1360
|
+
),
|
|
1361
|
+
}),
|
|
1362
|
+
]);
|
|
1363
|
+
|
|
1364
|
+
const graph = builder.graph;
|
|
1365
|
+
|
|
1366
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1367
|
+
await GraphBuilder.flush(builder);
|
|
1368
|
+
|
|
1369
|
+
const level1 = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1370
|
+
expect(level1).has.length(1);
|
|
1371
|
+
expect(level1[0].id).to.equal('root/A');
|
|
1372
|
+
|
|
1373
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1374
|
+
await GraphBuilder.flush(builder);
|
|
1375
|
+
|
|
1376
|
+
const level2 = registry.get(graph.connections('root/A', 'child'));
|
|
1377
|
+
expect(level2).has.length(1);
|
|
1378
|
+
expect(level2[0].id).to.equal('root/A/B');
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
test('inline nodes are recursively qualified', async () => {
|
|
1382
|
+
const registry = Registry.make();
|
|
1383
|
+
const builder = GraphBuilder.make({ registry });
|
|
1384
|
+
GraphBuilder.addExtension(
|
|
1385
|
+
builder,
|
|
1386
|
+
GraphBuilder.createExtensionRaw({
|
|
1387
|
+
id: 'inline-connector',
|
|
1388
|
+
connector: () =>
|
|
1389
|
+
Atom.make([
|
|
1390
|
+
{
|
|
1391
|
+
id: 'parent-node',
|
|
1392
|
+
type: EXAMPLE_TYPE,
|
|
1393
|
+
data: null,
|
|
1394
|
+
nodes: [
|
|
1395
|
+
{
|
|
1396
|
+
id: 'inline-child',
|
|
1397
|
+
type: EXAMPLE_TYPE,
|
|
1398
|
+
data: null,
|
|
1399
|
+
nodes: [{ id: 'deep-child', type: EXAMPLE_TYPE, data: null }],
|
|
1400
|
+
},
|
|
1401
|
+
],
|
|
1402
|
+
},
|
|
1403
|
+
]),
|
|
1404
|
+
}),
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
const graph = builder.graph;
|
|
1408
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1409
|
+
await GraphBuilder.flush(builder);
|
|
1410
|
+
|
|
1411
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
1412
|
+
expect(connections).has.length(1);
|
|
1413
|
+
expect(connections[0].id).to.equal('root/parent-node');
|
|
1414
|
+
|
|
1415
|
+
const inlineNode = Graph.getNode(graph, 'root/parent-node/inline-child').pipe(Option.getOrNull);
|
|
1416
|
+
expect(inlineNode).to.not.be.null;
|
|
1417
|
+
expect(inlineNode?.id).to.equal('root/parent-node/inline-child');
|
|
1418
|
+
|
|
1419
|
+
const deepNode = Graph.getNode(graph, 'root/parent-node/inline-child/deep-child').pipe(Option.getOrNull);
|
|
1420
|
+
expect(deepNode).to.not.be.null;
|
|
1421
|
+
expect(deepNode?.id).to.equal('root/parent-node/inline-child/deep-child');
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test('constant connector produces distinct nodes under different parents', async () => {
|
|
1425
|
+
const registry = Registry.make();
|
|
1426
|
+
const builder = GraphBuilder.make({ registry });
|
|
1427
|
+
|
|
1428
|
+
GraphBuilder.addExtension(builder, [
|
|
1429
|
+
GraphBuilder.createExtensionRaw({
|
|
1430
|
+
id: 'parents',
|
|
1431
|
+
connector: (node) =>
|
|
1432
|
+
Atom.make((get) =>
|
|
1433
|
+
Function.pipe(
|
|
1434
|
+
get(node),
|
|
1435
|
+
Option.filter((n) => n.id === 'root'),
|
|
1436
|
+
Option.map(() => [
|
|
1437
|
+
{ id: 'A', type: EXAMPLE_TYPE, data: 'a' },
|
|
1438
|
+
{ id: 'B', type: EXAMPLE_TYPE, data: 'b' },
|
|
1439
|
+
]),
|
|
1440
|
+
Option.getOrElse(() => []),
|
|
1441
|
+
),
|
|
1442
|
+
),
|
|
1443
|
+
}),
|
|
1444
|
+
GraphBuilder.createExtensionRaw({
|
|
1445
|
+
id: 'constant-child',
|
|
1446
|
+
connector: () => Atom.make([{ id: 'shared', type: EXAMPLE_TYPE, data: 'constant' }]),
|
|
1447
|
+
}),
|
|
1448
|
+
]);
|
|
1449
|
+
|
|
1450
|
+
const graph = builder.graph;
|
|
1451
|
+
|
|
1452
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
1453
|
+
await GraphBuilder.flush(builder);
|
|
1454
|
+
|
|
1455
|
+
Graph.expand(graph, 'root/A', 'child');
|
|
1456
|
+
Graph.expand(graph, 'root/B', 'child');
|
|
1457
|
+
await GraphBuilder.flush(builder);
|
|
1458
|
+
|
|
1459
|
+
const childrenOfA = registry.get(graph.connections('root/A', 'child'));
|
|
1460
|
+
const childrenOfB = registry.get(graph.connections('root/B', 'child'));
|
|
1461
|
+
|
|
1462
|
+
expect(childrenOfA).has.length(1);
|
|
1463
|
+
expect(childrenOfB).has.length(1);
|
|
1464
|
+
expect(childrenOfA[0].id).to.equal('root/A/shared');
|
|
1465
|
+
expect(childrenOfB[0].id).to.equal('root/B/shared');
|
|
1466
|
+
|
|
1467
|
+
const nodeA = Graph.getNode(graph, 'root/A/shared').pipe(Option.getOrNull);
|
|
1468
|
+
const nodeB = Graph.getNode(graph, 'root/B/shared').pipe(Option.getOrNull);
|
|
1469
|
+
expect(nodeA).to.not.be.null;
|
|
1470
|
+
expect(nodeB).to.not.be.null;
|
|
1471
|
+
expect(nodeA?.id).to.not.equal(nodeB?.id);
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
test('explore qualifies node IDs', async () => {
|
|
1475
|
+
const builder = GraphBuilder.make();
|
|
1476
|
+
GraphBuilder.addExtension(
|
|
1477
|
+
builder,
|
|
1478
|
+
GraphBuilder.createExtensionRaw({
|
|
1479
|
+
id: 'connector',
|
|
1480
|
+
connector: (node) =>
|
|
1481
|
+
Atom.make((get) =>
|
|
1482
|
+
Function.pipe(
|
|
1483
|
+
get(node),
|
|
1484
|
+
Option.filter((n) => n.id === 'root'),
|
|
1485
|
+
Option.map(() => [
|
|
1486
|
+
{ id: 'first', type: EXAMPLE_TYPE, data: 1 },
|
|
1487
|
+
{ id: 'second', type: EXAMPLE_TYPE, data: 2 },
|
|
1488
|
+
]),
|
|
1489
|
+
Option.getOrElse(() => []),
|
|
1490
|
+
),
|
|
1491
|
+
),
|
|
1492
|
+
}),
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
const visited: Array<{ id: string; path: string[] }> = [];
|
|
1496
|
+
await GraphBuilder.explore(builder, {
|
|
1497
|
+
relation: 'child',
|
|
1498
|
+
visitor: (node, path) => {
|
|
1499
|
+
visited.push({ id: node.id, path });
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
expect(visited).has.length(3);
|
|
1504
|
+
expect(visited[0].id).to.equal('root');
|
|
1505
|
+
expect(visited[1].id).to.equal('root/first');
|
|
1506
|
+
expect(visited[2].id).to.equal('root/second');
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
442
1509
|
});
|