@dxos/app-graph 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/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 +112 -66
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +187 -221
- 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.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 -40
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1146 -137
- package/src/graph-builder.ts +732 -290
- package/src/graph.test.ts +450 -122
- package/src/graph.ts +1050 -401
- 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/scheduler.browser.ts +5 -0
- package/src/scheduler.ts +17 -0
- package/src/stories/EchoGraph.stories.tsx +160 -123
- 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 -836
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -838
- 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,46 +2,51 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom, Registry } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Context from 'effect/Context';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
6
8
|
import * as Function from 'effect/Function';
|
|
7
9
|
import * as Option from 'effect/Option';
|
|
8
10
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
9
11
|
|
|
10
|
-
import { Trigger
|
|
12
|
+
import { Trigger } from '@dxos/async';
|
|
13
|
+
import { Obj } from '@dxos/echo';
|
|
14
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
11
15
|
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
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';
|
|
15
21
|
|
|
16
22
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
17
23
|
const EXAMPLE_ID = exampleId(1);
|
|
18
|
-
const EXAMPLE_TYPE = 'dxos.
|
|
24
|
+
const EXAMPLE_TYPE = 'org.dxos.type.example';
|
|
19
25
|
|
|
20
26
|
describe('GraphBuilder', () => {
|
|
21
27
|
describe('resolver', () => {
|
|
22
28
|
test('works', async () => {
|
|
23
29
|
const registry = Registry.make();
|
|
24
|
-
const builder =
|
|
30
|
+
const builder = GraphBuilder.make({ registry });
|
|
25
31
|
const graph = builder.graph;
|
|
26
32
|
|
|
27
33
|
{
|
|
28
|
-
const node =
|
|
34
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
29
35
|
expect(node).to.be.null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
// Test direct API
|
|
39
|
+
GraphBuilder.addExtension(
|
|
40
|
+
builder,
|
|
41
|
+
GraphBuilder.createExtensionRaw({
|
|
34
42
|
id: 'resolver',
|
|
35
|
-
resolver: () => {
|
|
36
|
-
console.log('resolver');
|
|
37
|
-
return Rx.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
38
|
-
},
|
|
43
|
+
resolver: () => Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }),
|
|
39
44
|
}),
|
|
40
45
|
);
|
|
41
|
-
await
|
|
46
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
42
47
|
|
|
43
48
|
{
|
|
44
|
-
const node =
|
|
49
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
45
50
|
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
46
51
|
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
47
52
|
expect(node?.data).to.equal(1);
|
|
@@ -50,132 +55,326 @@ describe('GraphBuilder', () => {
|
|
|
50
55
|
|
|
51
56
|
test('updates', async () => {
|
|
52
57
|
const registry = Registry.make();
|
|
53
|
-
const builder =
|
|
54
|
-
const name =
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const builder = GraphBuilder.make({ registry });
|
|
59
|
+
const name = Atom.make('default');
|
|
60
|
+
GraphBuilder.addExtension(
|
|
61
|
+
builder,
|
|
62
|
+
GraphBuilder.createExtensionRaw({
|
|
57
63
|
id: 'resolver',
|
|
58
|
-
resolver: () =>
|
|
64
|
+
resolver: () => Atom.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
59
65
|
}),
|
|
60
66
|
);
|
|
61
67
|
const graph = builder.graph;
|
|
62
|
-
await
|
|
68
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
63
69
|
|
|
64
70
|
{
|
|
65
|
-
const node =
|
|
71
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
66
72
|
expect(node?.data).to.equal('default');
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
registry.set(name, 'updated');
|
|
70
76
|
|
|
71
77
|
{
|
|
72
|
-
const node =
|
|
78
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
73
79
|
expect(node?.data).to.equal('updated');
|
|
74
80
|
}
|
|
75
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
|
+
});
|
|
76
263
|
});
|
|
77
264
|
|
|
78
265
|
describe('connector', () => {
|
|
79
|
-
test('works', () => {
|
|
266
|
+
test('works', async () => {
|
|
80
267
|
const registry = Registry.make();
|
|
81
|
-
const builder =
|
|
82
|
-
|
|
83
|
-
|
|
268
|
+
const builder = GraphBuilder.make({ registry });
|
|
269
|
+
GraphBuilder.addExtension(
|
|
270
|
+
builder,
|
|
271
|
+
GraphBuilder.createExtensionRaw({
|
|
84
272
|
id: 'outbound-connector',
|
|
85
|
-
connector: () =>
|
|
273
|
+
connector: () => Atom.make([{ id: 'child', type: EXAMPLE_TYPE, data: 2 }]),
|
|
86
274
|
}),
|
|
87
275
|
);
|
|
88
|
-
|
|
89
|
-
|
|
276
|
+
GraphBuilder.addExtension(
|
|
277
|
+
builder,
|
|
278
|
+
GraphBuilder.createExtensionRaw({
|
|
90
279
|
id: 'inbound-connector',
|
|
91
|
-
relation: 'inbound',
|
|
92
|
-
connector: () =>
|
|
280
|
+
relation: Node.childRelation('inbound'),
|
|
281
|
+
connector: () => Atom.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
93
282
|
}),
|
|
94
283
|
);
|
|
95
284
|
|
|
96
285
|
const graph = builder.graph;
|
|
97
|
-
|
|
98
|
-
|
|
286
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
287
|
+
Graph.expand(graph, Node.RootId, Node.childRelation('inbound'));
|
|
288
|
+
await GraphBuilder.flush(builder);
|
|
99
289
|
|
|
100
|
-
const outbound = registry.get(graph.connections(
|
|
101
|
-
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')));
|
|
102
292
|
|
|
103
293
|
expect(outbound).has.length(1);
|
|
104
|
-
expect(outbound[0].id).to.equal('child');
|
|
294
|
+
expect(outbound[0].id).to.equal('root/child');
|
|
105
295
|
expect(outbound[0].data).to.equal(2);
|
|
106
296
|
expect(inbound).has.length(1);
|
|
107
|
-
expect(inbound[0].id).to.equal('parent');
|
|
297
|
+
expect(inbound[0].id).to.equal('root/parent');
|
|
108
298
|
expect(inbound[0].data).to.equal(0);
|
|
109
299
|
});
|
|
110
300
|
|
|
111
|
-
test('updates', () => {
|
|
301
|
+
test('updates', async () => {
|
|
112
302
|
const registry = Registry.make();
|
|
113
|
-
const builder =
|
|
114
|
-
const state =
|
|
115
|
-
|
|
116
|
-
|
|
303
|
+
const builder = GraphBuilder.make({ registry });
|
|
304
|
+
const state = Atom.make(0);
|
|
305
|
+
GraphBuilder.addExtension(
|
|
306
|
+
builder,
|
|
307
|
+
GraphBuilder.createExtensionRaw({
|
|
117
308
|
id: 'connector',
|
|
118
|
-
connector: () =>
|
|
309
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
119
310
|
}),
|
|
120
311
|
);
|
|
121
312
|
const graph = builder.graph;
|
|
122
|
-
|
|
313
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
314
|
+
await GraphBuilder.flush(builder);
|
|
123
315
|
|
|
124
316
|
{
|
|
125
|
-
const [node] = registry.get(graph.connections(
|
|
317
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
126
318
|
expect(node.data).to.equal(0);
|
|
127
319
|
}
|
|
128
320
|
|
|
129
321
|
{
|
|
130
322
|
registry.set(state, 1);
|
|
131
|
-
|
|
323
|
+
await GraphBuilder.flush(builder);
|
|
324
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
132
325
|
expect(node.data).to.equal(1);
|
|
133
326
|
}
|
|
134
327
|
});
|
|
135
328
|
|
|
136
|
-
test('subscribes to updates', () => {
|
|
329
|
+
test('subscribes to updates', async () => {
|
|
137
330
|
const registry = Registry.make();
|
|
138
|
-
const builder =
|
|
139
|
-
const state =
|
|
140
|
-
|
|
141
|
-
|
|
331
|
+
const builder = GraphBuilder.make({ registry });
|
|
332
|
+
const state = Atom.make(0);
|
|
333
|
+
GraphBuilder.addExtension(
|
|
334
|
+
builder,
|
|
335
|
+
GraphBuilder.createExtensionRaw({
|
|
142
336
|
id: 'connector',
|
|
143
|
-
connector: () =>
|
|
337
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
144
338
|
}),
|
|
145
339
|
);
|
|
146
340
|
const graph = builder.graph;
|
|
147
341
|
|
|
148
342
|
let count = 0;
|
|
149
|
-
const cancel = registry.subscribe(graph.connections(
|
|
343
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_) => {
|
|
150
344
|
count++;
|
|
151
345
|
});
|
|
152
346
|
onTestFinished(() => cancel());
|
|
153
347
|
|
|
154
348
|
expect(count).to.equal(0);
|
|
155
|
-
expect(registry.get(graph.connections(
|
|
349
|
+
expect(registry.get(graph.connections(Node.RootId, 'child'))).to.have.length(0);
|
|
156
350
|
expect(count).to.equal(1);
|
|
157
351
|
|
|
158
|
-
|
|
352
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
353
|
+
await GraphBuilder.flush(builder);
|
|
159
354
|
expect(count).to.equal(2);
|
|
355
|
+
|
|
160
356
|
registry.set(state, 1);
|
|
357
|
+
await GraphBuilder.flush(builder);
|
|
161
358
|
expect(count).to.equal(3);
|
|
162
359
|
});
|
|
163
360
|
|
|
164
|
-
test('updates with new extensions', () => {
|
|
361
|
+
test('updates with new extensions', async () => {
|
|
165
362
|
const registry = Registry.make();
|
|
166
|
-
const builder =
|
|
167
|
-
|
|
168
|
-
|
|
363
|
+
const builder = GraphBuilder.make({ registry });
|
|
364
|
+
GraphBuilder.addExtension(
|
|
365
|
+
builder,
|
|
366
|
+
GraphBuilder.createExtensionRaw({
|
|
169
367
|
id: 'connector',
|
|
170
|
-
connector: () =>
|
|
368
|
+
connector: () => Atom.make([{ id: EXAMPLE_ID, type: EXAMPLE_TYPE }]),
|
|
171
369
|
}),
|
|
172
370
|
);
|
|
173
371
|
const graph = builder.graph;
|
|
174
|
-
|
|
372
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
373
|
+
await GraphBuilder.flush(builder);
|
|
175
374
|
|
|
176
|
-
let nodes: Node[] = [];
|
|
375
|
+
let nodes: Node.Node[] = [];
|
|
177
376
|
let count = 0;
|
|
178
|
-
const cancel = registry.subscribe(graph.connections(
|
|
377
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_nodes) => {
|
|
179
378
|
count++;
|
|
180
379
|
nodes = _nodes;
|
|
181
380
|
});
|
|
@@ -183,62 +382,67 @@ describe('GraphBuilder', () => {
|
|
|
183
382
|
|
|
184
383
|
expect(nodes).has.length(0);
|
|
185
384
|
expect(count).to.equal(0);
|
|
186
|
-
registry.get(graph.connections(
|
|
385
|
+
registry.get(graph.connections(Node.RootId, 'child'));
|
|
187
386
|
expect(nodes).has.length(1);
|
|
188
387
|
expect(count).to.equal(1);
|
|
189
388
|
|
|
190
|
-
|
|
191
|
-
|
|
389
|
+
GraphBuilder.addExtension(
|
|
390
|
+
builder,
|
|
391
|
+
GraphBuilder.createExtensionRaw({
|
|
192
392
|
id: 'connector-2',
|
|
193
|
-
connector: () =>
|
|
393
|
+
connector: () => Atom.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
194
394
|
}),
|
|
195
395
|
);
|
|
396
|
+
await GraphBuilder.flush(builder);
|
|
196
397
|
expect(nodes).has.length(2);
|
|
197
398
|
expect(count).to.equal(2);
|
|
198
399
|
});
|
|
199
400
|
|
|
200
|
-
test('removes', () => {
|
|
401
|
+
test('removes', async () => {
|
|
201
402
|
const registry = Registry.make();
|
|
202
|
-
const builder =
|
|
203
|
-
const nodes =
|
|
403
|
+
const builder = GraphBuilder.make({ registry });
|
|
404
|
+
const nodes = Atom.make([
|
|
204
405
|
{ id: exampleId(1), type: EXAMPLE_TYPE },
|
|
205
406
|
{ id: exampleId(2), type: EXAMPLE_TYPE },
|
|
206
407
|
]);
|
|
207
|
-
|
|
208
|
-
|
|
408
|
+
GraphBuilder.addExtension(
|
|
409
|
+
builder,
|
|
410
|
+
GraphBuilder.createExtensionRaw({
|
|
209
411
|
id: 'connector',
|
|
210
|
-
connector: () =>
|
|
412
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
211
413
|
}),
|
|
212
414
|
);
|
|
213
415
|
const graph = builder.graph;
|
|
214
|
-
|
|
416
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
417
|
+
await GraphBuilder.flush(builder);
|
|
215
418
|
|
|
216
419
|
{
|
|
217
|
-
const nodes = registry.get(graph.connections(
|
|
420
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
218
421
|
expect(nodes).has.length(2);
|
|
219
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
220
|
-
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)));
|
|
221
424
|
}
|
|
222
425
|
|
|
223
426
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
427
|
+
await GraphBuilder.flush(builder);
|
|
224
428
|
|
|
225
429
|
{
|
|
226
|
-
const nodes = registry.get(graph.connections(
|
|
430
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
227
431
|
expect(nodes).has.length(1);
|
|
228
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
432
|
+
expect(nodes[0].id).to.equal(qualifyId('root', exampleId(3)));
|
|
229
433
|
}
|
|
230
434
|
});
|
|
231
435
|
|
|
232
|
-
test('nodes are updated when removed', () => {
|
|
436
|
+
test('nodes are updated when removed', async () => {
|
|
233
437
|
const registry = Registry.make();
|
|
234
|
-
const builder =
|
|
235
|
-
const name =
|
|
438
|
+
const builder = GraphBuilder.make({ registry });
|
|
439
|
+
const name = Atom.make('removed');
|
|
236
440
|
|
|
237
|
-
|
|
238
|
-
|
|
441
|
+
GraphBuilder.addExtension(builder, [
|
|
442
|
+
GraphBuilder.createExtensionRaw({
|
|
239
443
|
id: 'root',
|
|
240
444
|
connector: (node) =>
|
|
241
|
-
|
|
445
|
+
Atom.make((get) =>
|
|
242
446
|
Function.pipe(
|
|
243
447
|
get(node),
|
|
244
448
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
@@ -254,52 +458,184 @@ describe('GraphBuilder', () => {
|
|
|
254
458
|
|
|
255
459
|
let count = 0;
|
|
256
460
|
let exists = false;
|
|
257
|
-
const cancel = registry.subscribe(graph.node(EXAMPLE_ID), (node) => {
|
|
461
|
+
const cancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (node) => {
|
|
258
462
|
count++;
|
|
259
463
|
exists = Option.isSome(node);
|
|
260
464
|
});
|
|
261
465
|
onTestFinished(() => cancel());
|
|
262
466
|
|
|
263
|
-
|
|
467
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
468
|
+
await GraphBuilder.flush(builder);
|
|
264
469
|
expect(count).to.equal(0);
|
|
265
470
|
expect(exists).to.be.false;
|
|
266
471
|
|
|
267
472
|
registry.set(name, 'default');
|
|
473
|
+
await GraphBuilder.flush(builder);
|
|
268
474
|
expect(count).to.equal(1);
|
|
269
475
|
expect(exists).to.be.true;
|
|
270
476
|
|
|
271
477
|
registry.set(name, 'removed');
|
|
478
|
+
await GraphBuilder.flush(builder);
|
|
272
479
|
expect(count).to.equal(2);
|
|
273
480
|
expect(exists).to.be.false;
|
|
274
481
|
|
|
275
482
|
registry.set(name, 'added');
|
|
483
|
+
await GraphBuilder.flush(builder);
|
|
276
484
|
expect(count).to.equal(3);
|
|
277
485
|
expect(exists).to.be.true;
|
|
278
486
|
});
|
|
279
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
|
+
|
|
280
614
|
test('sort edges', async () => {
|
|
281
615
|
const registry = Registry.make();
|
|
282
|
-
const builder =
|
|
283
|
-
const nodes =
|
|
616
|
+
const builder = GraphBuilder.make({ registry });
|
|
617
|
+
const nodes = Atom.make([
|
|
284
618
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
285
619
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
286
620
|
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
287
621
|
]);
|
|
288
|
-
|
|
289
|
-
|
|
622
|
+
GraphBuilder.addExtension(
|
|
623
|
+
builder,
|
|
624
|
+
GraphBuilder.createExtensionRaw({
|
|
290
625
|
id: 'connector',
|
|
291
|
-
connector: () =>
|
|
626
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
292
627
|
}),
|
|
293
628
|
);
|
|
294
629
|
const graph = builder.graph;
|
|
295
|
-
|
|
630
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
631
|
+
await GraphBuilder.flush(builder);
|
|
296
632
|
|
|
297
633
|
{
|
|
298
|
-
const nodes = registry.get(graph.connections(
|
|
634
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
299
635
|
expect(nodes).has.length(3);
|
|
300
|
-
expect(nodes[0].id).to.equal(exampleId(1));
|
|
301
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
302
|
-
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)));
|
|
303
639
|
}
|
|
304
640
|
|
|
305
641
|
registry.set(nodes, [
|
|
@@ -307,30 +643,28 @@ describe('GraphBuilder', () => {
|
|
|
307
643
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
308
644
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
309
645
|
]);
|
|
310
|
-
|
|
311
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
312
|
-
await sleep(0);
|
|
646
|
+
await GraphBuilder.flush(builder);
|
|
313
647
|
|
|
314
648
|
{
|
|
315
|
-
const nodes = registry.get(graph.connections(
|
|
649
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
316
650
|
expect(nodes).has.length(3);
|
|
317
|
-
expect(nodes[0].id).to.equal(exampleId(3));
|
|
318
|
-
expect(nodes[1].id).to.equal(exampleId(1));
|
|
319
|
-
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)));
|
|
320
654
|
}
|
|
321
655
|
});
|
|
322
656
|
|
|
323
|
-
test('updates are constrained', () => {
|
|
657
|
+
test('updates are constrained', async () => {
|
|
324
658
|
const registry = Registry.make();
|
|
325
|
-
const builder =
|
|
326
|
-
const name =
|
|
327
|
-
const sub =
|
|
659
|
+
const builder = GraphBuilder.make({ registry });
|
|
660
|
+
const name = Atom.make('default');
|
|
661
|
+
const sub = Atom.make('default');
|
|
328
662
|
|
|
329
|
-
|
|
330
|
-
|
|
663
|
+
GraphBuilder.addExtension(builder, [
|
|
664
|
+
GraphBuilder.createExtensionRaw({
|
|
331
665
|
id: 'root',
|
|
332
666
|
connector: (node) =>
|
|
333
|
-
|
|
667
|
+
Atom.make((get) =>
|
|
334
668
|
Function.pipe(
|
|
335
669
|
get(node),
|
|
336
670
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
@@ -340,25 +674,29 @@ describe('GraphBuilder', () => {
|
|
|
340
674
|
),
|
|
341
675
|
),
|
|
342
676
|
}),
|
|
343
|
-
|
|
677
|
+
GraphBuilder.createExtensionRaw({
|
|
344
678
|
id: 'connector1',
|
|
345
679
|
connector: (node) =>
|
|
346
|
-
|
|
680
|
+
Atom.make((get) =>
|
|
347
681
|
Function.pipe(
|
|
348
682
|
get(node),
|
|
349
|
-
Option.flatMap((node) =>
|
|
683
|
+
Option.flatMap((node) =>
|
|
684
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(get(sub)) : Option.none(),
|
|
685
|
+
),
|
|
350
686
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
351
687
|
Option.getOrElse(() => []),
|
|
352
688
|
),
|
|
353
689
|
),
|
|
354
690
|
}),
|
|
355
|
-
|
|
691
|
+
GraphBuilder.createExtensionRaw({
|
|
356
692
|
id: 'connector2',
|
|
357
693
|
connector: (node) =>
|
|
358
|
-
|
|
694
|
+
Atom.make((get) =>
|
|
359
695
|
Function.pipe(
|
|
360
696
|
get(node),
|
|
361
|
-
Option.flatMap((node) =>
|
|
697
|
+
Option.flatMap((node) =>
|
|
698
|
+
node.id === qualifyId('root', EXAMPLE_ID) ? Option.some(node.data) : Option.none(),
|
|
699
|
+
),
|
|
362
700
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
363
701
|
Option.getOrElse(() => []),
|
|
364
702
|
),
|
|
@@ -369,64 +707,71 @@ describe('GraphBuilder', () => {
|
|
|
369
707
|
const graph = builder.graph;
|
|
370
708
|
|
|
371
709
|
let parentCount = 0;
|
|
372
|
-
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
710
|
+
const parentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID)), (_) => {
|
|
373
711
|
parentCount++;
|
|
374
712
|
});
|
|
375
713
|
onTestFinished(() => parentCancel());
|
|
376
714
|
|
|
377
715
|
let independentCount = 0;
|
|
378
|
-
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
716
|
+
const independentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(2))), (_) => {
|
|
379
717
|
independentCount++;
|
|
380
718
|
});
|
|
381
719
|
onTestFinished(() => independentCancel());
|
|
382
720
|
|
|
383
721
|
let dependentCount = 0;
|
|
384
|
-
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
722
|
+
const dependentCancel = registry.subscribe(graph.node(qualifyId('root', EXAMPLE_ID, exampleId(3))), (_) => {
|
|
385
723
|
dependentCount++;
|
|
386
724
|
});
|
|
387
725
|
onTestFinished(() => dependentCancel());
|
|
388
726
|
|
|
389
727
|
// Counts should not increment until the node is expanded.
|
|
390
|
-
|
|
728
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
729
|
+
await GraphBuilder.flush(builder);
|
|
391
730
|
expect(parentCount).to.equal(1);
|
|
392
731
|
expect(independentCount).to.equal(0);
|
|
393
732
|
expect(dependentCount).to.equal(0);
|
|
394
733
|
|
|
395
734
|
// Counts should increment when the node is expanded.
|
|
396
|
-
|
|
735
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
736
|
+
await GraphBuilder.flush(builder);
|
|
397
737
|
expect(parentCount).to.equal(1);
|
|
398
738
|
expect(independentCount).to.equal(1);
|
|
399
739
|
expect(dependentCount).to.equal(1);
|
|
400
740
|
|
|
401
741
|
// Only dependent count should increment when the parent changes.
|
|
402
742
|
registry.set(name, 'updated');
|
|
743
|
+
await GraphBuilder.flush(builder);
|
|
403
744
|
expect(parentCount).to.equal(2);
|
|
404
745
|
expect(independentCount).to.equal(1);
|
|
405
746
|
expect(dependentCount).to.equal(2);
|
|
406
747
|
|
|
407
748
|
// Only independent count should increment when its state changes.
|
|
408
749
|
registry.set(sub, 'updated');
|
|
750
|
+
await GraphBuilder.flush(builder);
|
|
409
751
|
expect(parentCount).to.equal(2);
|
|
410
752
|
expect(independentCount).to.equal(2);
|
|
411
753
|
expect(dependentCount).to.equal(2);
|
|
412
754
|
|
|
413
755
|
// Independent count should update if its state changes even if the parent is removed.
|
|
414
|
-
|
|
756
|
+
Atom.batch(() => {
|
|
415
757
|
registry.set(name, 'removed');
|
|
416
758
|
registry.set(sub, 'batch');
|
|
417
759
|
});
|
|
760
|
+
await GraphBuilder.flush(builder);
|
|
418
761
|
expect(parentCount).to.equal(2);
|
|
419
762
|
expect(independentCount).to.equal(3);
|
|
420
763
|
expect(dependentCount).to.equal(2);
|
|
421
764
|
|
|
422
765
|
// Dependent count should increment when the node is added back.
|
|
423
766
|
registry.set(name, 'added');
|
|
767
|
+
await GraphBuilder.flush(builder);
|
|
424
768
|
expect(parentCount).to.equal(3);
|
|
425
769
|
expect(independentCount).to.equal(3);
|
|
426
770
|
expect(dependentCount).to.equal(3);
|
|
427
771
|
|
|
428
772
|
// Counts should not increment when the node is expanded again.
|
|
429
|
-
|
|
773
|
+
Graph.expand(graph, qualifyId('root', EXAMPLE_ID), 'child');
|
|
774
|
+
await GraphBuilder.flush(builder);
|
|
430
775
|
expect(parentCount).to.equal(3);
|
|
431
776
|
expect(independentCount).to.equal(3);
|
|
432
777
|
expect(dependentCount).to.equal(3);
|
|
@@ -434,12 +779,13 @@ describe('GraphBuilder', () => {
|
|
|
434
779
|
|
|
435
780
|
test('eager graph expansion', async () => {
|
|
436
781
|
const registry = Registry.make();
|
|
437
|
-
const builder =
|
|
438
|
-
|
|
439
|
-
|
|
782
|
+
const builder = GraphBuilder.make({ registry });
|
|
783
|
+
GraphBuilder.addExtension(
|
|
784
|
+
builder,
|
|
785
|
+
GraphBuilder.createExtensionRaw({
|
|
440
786
|
id: 'connector',
|
|
441
787
|
connector: (node) => {
|
|
442
|
-
return
|
|
788
|
+
return Atom.make((get) =>
|
|
443
789
|
Function.pipe(
|
|
444
790
|
get(node),
|
|
445
791
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
@@ -455,14 +801,14 @@ describe('GraphBuilder', () => {
|
|
|
455
801
|
let count = 0;
|
|
456
802
|
const trigger = new Trigger();
|
|
457
803
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
458
|
-
builder.graph
|
|
804
|
+
Graph.expand(builder.graph, id, 'child');
|
|
459
805
|
count++;
|
|
460
806
|
if (count === 5) {
|
|
461
807
|
trigger.wake();
|
|
462
808
|
}
|
|
463
809
|
});
|
|
464
810
|
|
|
465
|
-
builder.graph.
|
|
811
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
466
812
|
await trigger.wait();
|
|
467
813
|
expect(count).to.equal(5);
|
|
468
814
|
});
|
|
@@ -470,12 +816,13 @@ describe('GraphBuilder', () => {
|
|
|
470
816
|
|
|
471
817
|
describe('explore', () => {
|
|
472
818
|
test('works', async () => {
|
|
473
|
-
const builder =
|
|
474
|
-
|
|
475
|
-
|
|
819
|
+
const builder = GraphBuilder.make();
|
|
820
|
+
GraphBuilder.addExtension(
|
|
821
|
+
builder,
|
|
822
|
+
GraphBuilder.createExtensionRaw({
|
|
476
823
|
id: 'connector',
|
|
477
824
|
connector: (node) =>
|
|
478
|
-
|
|
825
|
+
Atom.make((get) =>
|
|
479
826
|
Function.pipe(
|
|
480
827
|
get(node),
|
|
481
828
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
@@ -488,7 +835,8 @@ describe('GraphBuilder', () => {
|
|
|
488
835
|
);
|
|
489
836
|
|
|
490
837
|
let count = 0;
|
|
491
|
-
await
|
|
838
|
+
await GraphBuilder.explore(builder, {
|
|
839
|
+
relation: 'child',
|
|
492
840
|
visitor: () => {
|
|
493
841
|
count++;
|
|
494
842
|
},
|
|
@@ -497,4 +845,665 @@ describe('GraphBuilder', () => {
|
|
|
497
845
|
expect(count).to.equal(6);
|
|
498
846
|
});
|
|
499
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
|
+
});
|
|
500
1509
|
});
|