@dxos/app-graph 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +1355 -611
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1354 -611
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- 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 +6 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +218 -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 +42 -5
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts +8 -10
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +24 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -35
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +673 -108
- package/src/graph-builder.ts +692 -266
- package/src/graph.test.ts +430 -122
- package/src/graph.ts +1047 -408
- package/src/index.ts +9 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +282 -0
- package/src/node.ts +53 -8
- package/src/stories/EchoGraph.stories.tsx +172 -131
- package/src/stories/Tree.tsx +1 -1
- package/src/util.ts +55 -0
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -218
- package/src/testing.ts +0 -20
|
@@ -2,45 +2,111 @@
|
|
|
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';
|
|
14
20
|
|
|
15
21
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
16
22
|
const EXAMPLE_ID = exampleId(1);
|
|
17
23
|
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
18
24
|
|
|
19
25
|
describe('GraphBuilder', () => {
|
|
26
|
+
describe('resolver', () => {
|
|
27
|
+
test('works', async () => {
|
|
28
|
+
const registry = Registry.make();
|
|
29
|
+
const builder = GraphBuilder.make({ registry });
|
|
30
|
+
const graph = builder.graph;
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
34
|
+
expect(node).to.be.null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Test direct API
|
|
38
|
+
GraphBuilder.addExtension(
|
|
39
|
+
builder,
|
|
40
|
+
GraphBuilder.createExtensionRaw({
|
|
41
|
+
id: 'resolver',
|
|
42
|
+
resolver: () => Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
49
|
+
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
50
|
+
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
51
|
+
expect(node?.data).to.equal(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('updates', async () => {
|
|
56
|
+
const registry = Registry.make();
|
|
57
|
+
const builder = GraphBuilder.make({ registry });
|
|
58
|
+
const name = Atom.make('default');
|
|
59
|
+
GraphBuilder.addExtension(
|
|
60
|
+
builder,
|
|
61
|
+
GraphBuilder.createExtensionRaw({
|
|
62
|
+
id: 'resolver',
|
|
63
|
+
resolver: () => Atom.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
const graph = builder.graph;
|
|
67
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
71
|
+
expect(node?.data).to.equal('default');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
registry.set(name, 'updated');
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
78
|
+
expect(node?.data).to.equal('updated');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
20
83
|
describe('connector', () => {
|
|
21
|
-
test('works', () => {
|
|
84
|
+
test('works', async () => {
|
|
22
85
|
const registry = Registry.make();
|
|
23
|
-
const builder =
|
|
24
|
-
|
|
25
|
-
|
|
86
|
+
const builder = GraphBuilder.make({ registry });
|
|
87
|
+
GraphBuilder.addExtension(
|
|
88
|
+
builder,
|
|
89
|
+
GraphBuilder.createExtensionRaw({
|
|
26
90
|
id: 'outbound-connector',
|
|
27
|
-
connector: () =>
|
|
91
|
+
connector: () => Atom.make([{ id: 'child', type: EXAMPLE_TYPE, data: 2 }]),
|
|
28
92
|
}),
|
|
29
93
|
);
|
|
30
|
-
|
|
31
|
-
|
|
94
|
+
GraphBuilder.addExtension(
|
|
95
|
+
builder,
|
|
96
|
+
GraphBuilder.createExtensionRaw({
|
|
32
97
|
id: 'inbound-connector',
|
|
33
|
-
relation: 'inbound',
|
|
34
|
-
connector: () =>
|
|
98
|
+
relation: Node.childRelation('inbound'),
|
|
99
|
+
connector: () => Atom.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
35
100
|
}),
|
|
36
101
|
);
|
|
37
102
|
|
|
38
103
|
const graph = builder.graph;
|
|
39
|
-
|
|
40
|
-
|
|
104
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
105
|
+
Graph.expand(graph, Node.RootId, Node.childRelation('inbound'));
|
|
106
|
+
await GraphBuilder.flush(builder);
|
|
41
107
|
|
|
42
|
-
const outbound = registry.get(graph.connections(
|
|
43
|
-
const inbound = registry.get(graph.connections(
|
|
108
|
+
const outbound = registry.get(graph.connections(Node.RootId, 'child'));
|
|
109
|
+
const inbound = registry.get(graph.connections(Node.RootId, Node.childRelation('inbound')));
|
|
44
110
|
|
|
45
111
|
expect(outbound).has.length(1);
|
|
46
112
|
expect(outbound[0].id).to.equal('child');
|
|
@@ -50,74 +116,83 @@ describe('GraphBuilder', () => {
|
|
|
50
116
|
expect(inbound[0].data).to.equal(0);
|
|
51
117
|
});
|
|
52
118
|
|
|
53
|
-
test('updates', () => {
|
|
119
|
+
test('updates', async () => {
|
|
54
120
|
const registry = Registry.make();
|
|
55
|
-
const builder =
|
|
56
|
-
const state =
|
|
57
|
-
|
|
58
|
-
|
|
121
|
+
const builder = GraphBuilder.make({ registry });
|
|
122
|
+
const state = Atom.make(0);
|
|
123
|
+
GraphBuilder.addExtension(
|
|
124
|
+
builder,
|
|
125
|
+
GraphBuilder.createExtensionRaw({
|
|
59
126
|
id: 'connector',
|
|
60
|
-
connector: () =>
|
|
127
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
61
128
|
}),
|
|
62
129
|
);
|
|
63
130
|
const graph = builder.graph;
|
|
64
|
-
|
|
131
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
132
|
+
await GraphBuilder.flush(builder);
|
|
65
133
|
|
|
66
134
|
{
|
|
67
|
-
const [node] = registry.get(graph.connections(
|
|
135
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
68
136
|
expect(node.data).to.equal(0);
|
|
69
137
|
}
|
|
70
138
|
|
|
71
139
|
{
|
|
72
140
|
registry.set(state, 1);
|
|
73
|
-
|
|
141
|
+
await GraphBuilder.flush(builder);
|
|
142
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
74
143
|
expect(node.data).to.equal(1);
|
|
75
144
|
}
|
|
76
145
|
});
|
|
77
146
|
|
|
78
|
-
test('subscribes to updates', () => {
|
|
147
|
+
test('subscribes to updates', async () => {
|
|
79
148
|
const registry = Registry.make();
|
|
80
|
-
const builder =
|
|
81
|
-
const state =
|
|
82
|
-
|
|
83
|
-
|
|
149
|
+
const builder = GraphBuilder.make({ registry });
|
|
150
|
+
const state = Atom.make(0);
|
|
151
|
+
GraphBuilder.addExtension(
|
|
152
|
+
builder,
|
|
153
|
+
GraphBuilder.createExtensionRaw({
|
|
84
154
|
id: 'connector',
|
|
85
|
-
connector: () =>
|
|
155
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
86
156
|
}),
|
|
87
157
|
);
|
|
88
158
|
const graph = builder.graph;
|
|
89
159
|
|
|
90
160
|
let count = 0;
|
|
91
|
-
const cancel = registry.subscribe(graph.connections(
|
|
161
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_) => {
|
|
92
162
|
count++;
|
|
93
163
|
});
|
|
94
164
|
onTestFinished(() => cancel());
|
|
95
165
|
|
|
96
166
|
expect(count).to.equal(0);
|
|
97
|
-
expect(registry.get(graph.connections(
|
|
167
|
+
expect(registry.get(graph.connections(Node.RootId, 'child'))).to.have.length(0);
|
|
98
168
|
expect(count).to.equal(1);
|
|
99
169
|
|
|
100
|
-
|
|
170
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
171
|
+
await GraphBuilder.flush(builder);
|
|
101
172
|
expect(count).to.equal(2);
|
|
173
|
+
|
|
102
174
|
registry.set(state, 1);
|
|
175
|
+
await GraphBuilder.flush(builder);
|
|
103
176
|
expect(count).to.equal(3);
|
|
104
177
|
});
|
|
105
178
|
|
|
106
|
-
test('updates with new extensions', () => {
|
|
179
|
+
test('updates with new extensions', async () => {
|
|
107
180
|
const registry = Registry.make();
|
|
108
|
-
const builder =
|
|
109
|
-
|
|
110
|
-
|
|
181
|
+
const builder = GraphBuilder.make({ registry });
|
|
182
|
+
GraphBuilder.addExtension(
|
|
183
|
+
builder,
|
|
184
|
+
GraphBuilder.createExtensionRaw({
|
|
111
185
|
id: 'connector',
|
|
112
|
-
connector: () =>
|
|
186
|
+
connector: () => Atom.make([{ id: EXAMPLE_ID, type: EXAMPLE_TYPE }]),
|
|
113
187
|
}),
|
|
114
188
|
);
|
|
115
189
|
const graph = builder.graph;
|
|
116
|
-
|
|
190
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
191
|
+
await GraphBuilder.flush(builder);
|
|
117
192
|
|
|
118
|
-
let nodes: Node[] = [];
|
|
193
|
+
let nodes: Node.Node[] = [];
|
|
119
194
|
let count = 0;
|
|
120
|
-
const cancel = registry.subscribe(graph.connections(
|
|
195
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_nodes) => {
|
|
121
196
|
count++;
|
|
122
197
|
nodes = _nodes;
|
|
123
198
|
});
|
|
@@ -125,63 +200,68 @@ describe('GraphBuilder', () => {
|
|
|
125
200
|
|
|
126
201
|
expect(nodes).has.length(0);
|
|
127
202
|
expect(count).to.equal(0);
|
|
128
|
-
registry.get(graph.connections(
|
|
203
|
+
registry.get(graph.connections(Node.RootId, 'child'));
|
|
129
204
|
expect(nodes).has.length(1);
|
|
130
205
|
expect(count).to.equal(1);
|
|
131
206
|
|
|
132
|
-
|
|
133
|
-
|
|
207
|
+
GraphBuilder.addExtension(
|
|
208
|
+
builder,
|
|
209
|
+
GraphBuilder.createExtensionRaw({
|
|
134
210
|
id: 'connector-2',
|
|
135
|
-
connector: () =>
|
|
211
|
+
connector: () => Atom.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
136
212
|
}),
|
|
137
213
|
);
|
|
214
|
+
await GraphBuilder.flush(builder);
|
|
138
215
|
expect(nodes).has.length(2);
|
|
139
216
|
expect(count).to.equal(2);
|
|
140
217
|
});
|
|
141
218
|
|
|
142
|
-
test('removes', () => {
|
|
219
|
+
test('removes', async () => {
|
|
143
220
|
const registry = Registry.make();
|
|
144
|
-
const builder =
|
|
145
|
-
const nodes =
|
|
221
|
+
const builder = GraphBuilder.make({ registry });
|
|
222
|
+
const nodes = Atom.make([
|
|
146
223
|
{ id: exampleId(1), type: EXAMPLE_TYPE },
|
|
147
224
|
{ id: exampleId(2), type: EXAMPLE_TYPE },
|
|
148
225
|
]);
|
|
149
|
-
|
|
150
|
-
|
|
226
|
+
GraphBuilder.addExtension(
|
|
227
|
+
builder,
|
|
228
|
+
GraphBuilder.createExtensionRaw({
|
|
151
229
|
id: 'connector',
|
|
152
|
-
connector: () =>
|
|
230
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
153
231
|
}),
|
|
154
232
|
);
|
|
155
233
|
const graph = builder.graph;
|
|
156
|
-
|
|
234
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
235
|
+
await GraphBuilder.flush(builder);
|
|
157
236
|
|
|
158
237
|
{
|
|
159
|
-
const nodes = registry.get(graph.connections(
|
|
238
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
160
239
|
expect(nodes).has.length(2);
|
|
161
240
|
expect(nodes[0].id).to.equal(exampleId(1));
|
|
162
241
|
expect(nodes[1].id).to.equal(exampleId(2));
|
|
163
242
|
}
|
|
164
243
|
|
|
165
244
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
245
|
+
await GraphBuilder.flush(builder);
|
|
166
246
|
|
|
167
247
|
{
|
|
168
|
-
const nodes = registry.get(graph.connections(
|
|
248
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
169
249
|
expect(nodes).has.length(1);
|
|
170
250
|
expect(nodes[0].id).to.equal(exampleId(3));
|
|
171
251
|
}
|
|
172
252
|
});
|
|
173
253
|
|
|
174
|
-
test('nodes are updated when removed', () => {
|
|
254
|
+
test('nodes are updated when removed', async () => {
|
|
175
255
|
const registry = Registry.make();
|
|
176
|
-
const builder =
|
|
177
|
-
const name =
|
|
256
|
+
const builder = GraphBuilder.make({ registry });
|
|
257
|
+
const name = Atom.make('removed');
|
|
178
258
|
|
|
179
|
-
|
|
180
|
-
|
|
259
|
+
GraphBuilder.addExtension(builder, [
|
|
260
|
+
GraphBuilder.createExtensionRaw({
|
|
181
261
|
id: 'root',
|
|
182
262
|
connector: (node) =>
|
|
183
|
-
|
|
184
|
-
pipe(
|
|
263
|
+
Atom.make((get) =>
|
|
264
|
+
Function.pipe(
|
|
185
265
|
get(node),
|
|
186
266
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
187
267
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -202,42 +282,48 @@ describe('GraphBuilder', () => {
|
|
|
202
282
|
});
|
|
203
283
|
onTestFinished(() => cancel());
|
|
204
284
|
|
|
205
|
-
|
|
285
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
286
|
+
await GraphBuilder.flush(builder);
|
|
206
287
|
expect(count).to.equal(0);
|
|
207
288
|
expect(exists).to.be.false;
|
|
208
289
|
|
|
209
290
|
registry.set(name, 'default');
|
|
291
|
+
await GraphBuilder.flush(builder);
|
|
210
292
|
expect(count).to.equal(1);
|
|
211
293
|
expect(exists).to.be.true;
|
|
212
294
|
|
|
213
295
|
registry.set(name, 'removed');
|
|
296
|
+
await GraphBuilder.flush(builder);
|
|
214
297
|
expect(count).to.equal(2);
|
|
215
298
|
expect(exists).to.be.false;
|
|
216
299
|
|
|
217
300
|
registry.set(name, 'added');
|
|
301
|
+
await GraphBuilder.flush(builder);
|
|
218
302
|
expect(count).to.equal(3);
|
|
219
303
|
expect(exists).to.be.true;
|
|
220
304
|
});
|
|
221
305
|
|
|
222
306
|
test('sort edges', async () => {
|
|
223
307
|
const registry = Registry.make();
|
|
224
|
-
const builder =
|
|
225
|
-
const nodes =
|
|
308
|
+
const builder = GraphBuilder.make({ registry });
|
|
309
|
+
const nodes = Atom.make([
|
|
226
310
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
227
311
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
228
312
|
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
229
313
|
]);
|
|
230
|
-
|
|
231
|
-
|
|
314
|
+
GraphBuilder.addExtension(
|
|
315
|
+
builder,
|
|
316
|
+
GraphBuilder.createExtensionRaw({
|
|
232
317
|
id: 'connector',
|
|
233
|
-
connector: () =>
|
|
318
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
234
319
|
}),
|
|
235
320
|
);
|
|
236
321
|
const graph = builder.graph;
|
|
237
|
-
|
|
322
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
323
|
+
await GraphBuilder.flush(builder);
|
|
238
324
|
|
|
239
325
|
{
|
|
240
|
-
const nodes = registry.get(graph.connections(
|
|
326
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
241
327
|
expect(nodes).has.length(3);
|
|
242
328
|
expect(nodes[0].id).to.equal(exampleId(1));
|
|
243
329
|
expect(nodes[1].id).to.equal(exampleId(2));
|
|
@@ -249,12 +335,10 @@ describe('GraphBuilder', () => {
|
|
|
249
335
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
250
336
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
251
337
|
]);
|
|
252
|
-
|
|
253
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
254
|
-
await sleep(0);
|
|
338
|
+
await GraphBuilder.flush(builder);
|
|
255
339
|
|
|
256
340
|
{
|
|
257
|
-
const nodes = registry.get(graph.connections(
|
|
341
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
258
342
|
expect(nodes).has.length(3);
|
|
259
343
|
expect(nodes[0].id).to.equal(exampleId(3));
|
|
260
344
|
expect(nodes[1].id).to.equal(exampleId(1));
|
|
@@ -262,18 +346,18 @@ describe('GraphBuilder', () => {
|
|
|
262
346
|
}
|
|
263
347
|
});
|
|
264
348
|
|
|
265
|
-
test('updates are constrained', () => {
|
|
349
|
+
test('updates are constrained', async () => {
|
|
266
350
|
const registry = Registry.make();
|
|
267
|
-
const builder =
|
|
268
|
-
const name =
|
|
269
|
-
const sub =
|
|
351
|
+
const builder = GraphBuilder.make({ registry });
|
|
352
|
+
const name = Atom.make('default');
|
|
353
|
+
const sub = Atom.make('default');
|
|
270
354
|
|
|
271
|
-
|
|
272
|
-
|
|
355
|
+
GraphBuilder.addExtension(builder, [
|
|
356
|
+
GraphBuilder.createExtensionRaw({
|
|
273
357
|
id: 'root',
|
|
274
358
|
connector: (node) =>
|
|
275
|
-
|
|
276
|
-
pipe(
|
|
359
|
+
Atom.make((get) =>
|
|
360
|
+
Function.pipe(
|
|
277
361
|
get(node),
|
|
278
362
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
279
363
|
Option.filter((name) => name !== 'removed'),
|
|
@@ -282,11 +366,11 @@ describe('GraphBuilder', () => {
|
|
|
282
366
|
),
|
|
283
367
|
),
|
|
284
368
|
}),
|
|
285
|
-
|
|
369
|
+
GraphBuilder.createExtensionRaw({
|
|
286
370
|
id: 'connector1',
|
|
287
371
|
connector: (node) =>
|
|
288
|
-
|
|
289
|
-
pipe(
|
|
372
|
+
Atom.make((get) =>
|
|
373
|
+
Function.pipe(
|
|
290
374
|
get(node),
|
|
291
375
|
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(get(sub)) : Option.none())),
|
|
292
376
|
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
@@ -294,11 +378,11 @@ describe('GraphBuilder', () => {
|
|
|
294
378
|
),
|
|
295
379
|
),
|
|
296
380
|
}),
|
|
297
|
-
|
|
381
|
+
GraphBuilder.createExtensionRaw({
|
|
298
382
|
id: 'connector2',
|
|
299
383
|
connector: (node) =>
|
|
300
|
-
|
|
301
|
-
pipe(
|
|
384
|
+
Atom.make((get) =>
|
|
385
|
+
Function.pipe(
|
|
302
386
|
get(node),
|
|
303
387
|
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(node.data) : Option.none())),
|
|
304
388
|
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
@@ -329,46 +413,53 @@ describe('GraphBuilder', () => {
|
|
|
329
413
|
onTestFinished(() => dependentCancel());
|
|
330
414
|
|
|
331
415
|
// Counts should not increment until the node is expanded.
|
|
332
|
-
|
|
416
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
417
|
+
await GraphBuilder.flush(builder);
|
|
333
418
|
expect(parentCount).to.equal(1);
|
|
334
419
|
expect(independentCount).to.equal(0);
|
|
335
420
|
expect(dependentCount).to.equal(0);
|
|
336
421
|
|
|
337
422
|
// Counts should increment when the node is expanded.
|
|
338
|
-
|
|
423
|
+
Graph.expand(graph, EXAMPLE_ID, 'child');
|
|
424
|
+
await GraphBuilder.flush(builder);
|
|
339
425
|
expect(parentCount).to.equal(1);
|
|
340
426
|
expect(independentCount).to.equal(1);
|
|
341
427
|
expect(dependentCount).to.equal(1);
|
|
342
428
|
|
|
343
429
|
// Only dependent count should increment when the parent changes.
|
|
344
430
|
registry.set(name, 'updated');
|
|
431
|
+
await GraphBuilder.flush(builder);
|
|
345
432
|
expect(parentCount).to.equal(2);
|
|
346
433
|
expect(independentCount).to.equal(1);
|
|
347
434
|
expect(dependentCount).to.equal(2);
|
|
348
435
|
|
|
349
436
|
// Only independent count should increment when its state changes.
|
|
350
437
|
registry.set(sub, 'updated');
|
|
438
|
+
await GraphBuilder.flush(builder);
|
|
351
439
|
expect(parentCount).to.equal(2);
|
|
352
440
|
expect(independentCount).to.equal(2);
|
|
353
441
|
expect(dependentCount).to.equal(2);
|
|
354
442
|
|
|
355
443
|
// Independent count should update if its state changes even if the parent is removed.
|
|
356
|
-
|
|
444
|
+
Atom.batch(() => {
|
|
357
445
|
registry.set(name, 'removed');
|
|
358
446
|
registry.set(sub, 'batch');
|
|
359
447
|
});
|
|
448
|
+
await GraphBuilder.flush(builder);
|
|
360
449
|
expect(parentCount).to.equal(2);
|
|
361
450
|
expect(independentCount).to.equal(3);
|
|
362
451
|
expect(dependentCount).to.equal(2);
|
|
363
452
|
|
|
364
453
|
// Dependent count should increment when the node is added back.
|
|
365
454
|
registry.set(name, 'added');
|
|
455
|
+
await GraphBuilder.flush(builder);
|
|
366
456
|
expect(parentCount).to.equal(3);
|
|
367
457
|
expect(independentCount).to.equal(3);
|
|
368
458
|
expect(dependentCount).to.equal(3);
|
|
369
459
|
|
|
370
460
|
// Counts should not increment when the node is expanded again.
|
|
371
|
-
|
|
461
|
+
Graph.expand(graph, EXAMPLE_ID, 'child');
|
|
462
|
+
await GraphBuilder.flush(builder);
|
|
372
463
|
expect(parentCount).to.equal(3);
|
|
373
464
|
expect(independentCount).to.equal(3);
|
|
374
465
|
expect(dependentCount).to.equal(3);
|
|
@@ -376,13 +467,14 @@ describe('GraphBuilder', () => {
|
|
|
376
467
|
|
|
377
468
|
test('eager graph expansion', async () => {
|
|
378
469
|
const registry = Registry.make();
|
|
379
|
-
const builder =
|
|
380
|
-
|
|
381
|
-
|
|
470
|
+
const builder = GraphBuilder.make({ registry });
|
|
471
|
+
GraphBuilder.addExtension(
|
|
472
|
+
builder,
|
|
473
|
+
GraphBuilder.createExtensionRaw({
|
|
382
474
|
id: 'connector',
|
|
383
475
|
connector: (node) => {
|
|
384
|
-
return
|
|
385
|
-
pipe(
|
|
476
|
+
return Atom.make((get) =>
|
|
477
|
+
Function.pipe(
|
|
386
478
|
get(node),
|
|
387
479
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
388
480
|
Option.filter((data) => data <= 5),
|
|
@@ -397,14 +489,14 @@ describe('GraphBuilder', () => {
|
|
|
397
489
|
let count = 0;
|
|
398
490
|
const trigger = new Trigger();
|
|
399
491
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
400
|
-
builder.graph
|
|
492
|
+
Graph.expand(builder.graph, id, 'child');
|
|
401
493
|
count++;
|
|
402
494
|
if (count === 5) {
|
|
403
495
|
trigger.wake();
|
|
404
496
|
}
|
|
405
497
|
});
|
|
406
498
|
|
|
407
|
-
builder.graph.
|
|
499
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
408
500
|
await trigger.wait();
|
|
409
501
|
expect(count).to.equal(5);
|
|
410
502
|
});
|
|
@@ -412,13 +504,14 @@ describe('GraphBuilder', () => {
|
|
|
412
504
|
|
|
413
505
|
describe('explore', () => {
|
|
414
506
|
test('works', async () => {
|
|
415
|
-
const builder =
|
|
416
|
-
|
|
417
|
-
|
|
507
|
+
const builder = GraphBuilder.make();
|
|
508
|
+
GraphBuilder.addExtension(
|
|
509
|
+
builder,
|
|
510
|
+
GraphBuilder.createExtensionRaw({
|
|
418
511
|
id: 'connector',
|
|
419
512
|
connector: (node) =>
|
|
420
|
-
|
|
421
|
-
pipe(
|
|
513
|
+
Atom.make((get) =>
|
|
514
|
+
Function.pipe(
|
|
422
515
|
get(node),
|
|
423
516
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
424
517
|
Option.filter((data) => data <= 5),
|
|
@@ -430,7 +523,8 @@ describe('GraphBuilder', () => {
|
|
|
430
523
|
);
|
|
431
524
|
|
|
432
525
|
let count = 0;
|
|
433
|
-
await
|
|
526
|
+
await GraphBuilder.explore(builder, {
|
|
527
|
+
relation: 'child',
|
|
434
528
|
visitor: () => {
|
|
435
529
|
count++;
|
|
436
530
|
},
|
|
@@ -439,4 +533,475 @@ describe('GraphBuilder', () => {
|
|
|
439
533
|
expect(count).to.equal(6);
|
|
440
534
|
});
|
|
441
535
|
});
|
|
536
|
+
|
|
537
|
+
describe('helpers', () => {
|
|
538
|
+
describe('createConnector', () => {
|
|
539
|
+
test('creates connector with type inference', async () => {
|
|
540
|
+
const registry = Registry.make();
|
|
541
|
+
const builder = GraphBuilder.make({ registry });
|
|
542
|
+
const graph = builder.graph;
|
|
543
|
+
|
|
544
|
+
const matcher = (node: Node.Node) => NodeMatcher.whenId('root')(node);
|
|
545
|
+
const factory = (node: Node.Node) => [{ id: 'child', type: EXAMPLE_TYPE, data: node.id }];
|
|
546
|
+
|
|
547
|
+
const connector = GraphBuilder.createConnector(matcher, factory);
|
|
548
|
+
|
|
549
|
+
GraphBuilder.addExtension(
|
|
550
|
+
builder,
|
|
551
|
+
GraphBuilder.createExtensionRaw({
|
|
552
|
+
id: 'test-connector',
|
|
553
|
+
connector,
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
558
|
+
await GraphBuilder.flush(builder);
|
|
559
|
+
|
|
560
|
+
const connections = registry.get(graph.connections(Node.RootId, 'child'));
|
|
561
|
+
expect(connections).has.length(1);
|
|
562
|
+
expect(connections[0].id).to.equal('child');
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('createExtension', () => {
|
|
567
|
+
test('works with Effect connector', async () => {
|
|
568
|
+
const registry = Registry.make();
|
|
569
|
+
const builder = GraphBuilder.make({ registry });
|
|
570
|
+
const graph = builder.graph;
|
|
571
|
+
|
|
572
|
+
const extensions = Effect.runSync(
|
|
573
|
+
GraphBuilder.createExtension({
|
|
574
|
+
id: 'test-extension',
|
|
575
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
576
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: node.data }]),
|
|
577
|
+
}),
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
581
|
+
|
|
582
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
583
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
584
|
+
Graph.expand(graph, 'parent', 'child');
|
|
585
|
+
await GraphBuilder.flush(builder);
|
|
586
|
+
|
|
587
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
588
|
+
expect(connections).has.length(1);
|
|
589
|
+
expect(connections[0].id).to.equal('child');
|
|
590
|
+
expect(connections[0].data).to.equal('test');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test('works with Effect actions', async () => {
|
|
594
|
+
const registry = Registry.make();
|
|
595
|
+
const builder = GraphBuilder.make({ registry });
|
|
596
|
+
const graph = builder.graph;
|
|
597
|
+
|
|
598
|
+
const extensions = Effect.runSync(
|
|
599
|
+
GraphBuilder.createExtension({
|
|
600
|
+
id: 'test-extension',
|
|
601
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
602
|
+
actions: (node, get) =>
|
|
603
|
+
Effect.succeed([
|
|
604
|
+
{
|
|
605
|
+
id: 'test-action',
|
|
606
|
+
data: () => Effect.void,
|
|
607
|
+
properties: { label: 'Test' },
|
|
608
|
+
},
|
|
609
|
+
]),
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
614
|
+
|
|
615
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
616
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
617
|
+
Graph.expand(graph, 'parent', 'child');
|
|
618
|
+
await GraphBuilder.flush(builder);
|
|
619
|
+
|
|
620
|
+
const edges = registry.get(graph.edges('parent'));
|
|
621
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.have.length(1);
|
|
622
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('test-action');
|
|
623
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.have.length(0);
|
|
624
|
+
const actions = registry.get(graph.actions('parent'));
|
|
625
|
+
expect(actions).has.length(1);
|
|
626
|
+
expect(actions[0].id).to.equal('test-action');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test('actions expand automatically with child relation', async ({ expect }) => {
|
|
630
|
+
const registry = Registry.make();
|
|
631
|
+
const builder = GraphBuilder.make({ registry });
|
|
632
|
+
const graph = builder.graph;
|
|
633
|
+
|
|
634
|
+
const extensions = Effect.runSync(
|
|
635
|
+
GraphBuilder.createExtension({
|
|
636
|
+
id: 'test-extension',
|
|
637
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
638
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: 'c' }]),
|
|
639
|
+
actions: (node, get) =>
|
|
640
|
+
Effect.succeed([{ id: 'act1', data: () => Effect.void, properties: { label: 'A' } }]),
|
|
641
|
+
}),
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
645
|
+
|
|
646
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
647
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
648
|
+
Graph.expand(graph, 'parent', 'child');
|
|
649
|
+
await GraphBuilder.flush(builder);
|
|
650
|
+
|
|
651
|
+
const edges = registry.get(graph.edges('parent'));
|
|
652
|
+
expect(edges[Graph.relationKey('child')] ?? []).to.include('child');
|
|
653
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('act1');
|
|
654
|
+
const actions = registry.get(graph.actions('parent'));
|
|
655
|
+
expect(actions).has.length(1);
|
|
656
|
+
expect(actions[0].id).to.equal('act1');
|
|
657
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
658
|
+
expect(connections).has.length(1);
|
|
659
|
+
expect(connections[0].id).to.equal('child');
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('actions appear when extension registered after expand', async ({ expect }) => {
|
|
663
|
+
const registry = Registry.make();
|
|
664
|
+
const builder = GraphBuilder.make({ registry });
|
|
665
|
+
const graph = builder.graph;
|
|
666
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
667
|
+
|
|
668
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
669
|
+
Graph.expand(graph, 'parent', 'child');
|
|
670
|
+
await GraphBuilder.flush(builder);
|
|
671
|
+
|
|
672
|
+
expect(registry.get(graph.actions('parent'))).to.have.length(0);
|
|
673
|
+
|
|
674
|
+
const extensions = Effect.runSync(
|
|
675
|
+
GraphBuilder.createExtension({
|
|
676
|
+
id: 'late-extension',
|
|
677
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
678
|
+
actions: (node, get) =>
|
|
679
|
+
Effect.succeed([{ id: 'late-act', data: () => Effect.void, properties: { label: 'Late' } }]),
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
684
|
+
await GraphBuilder.flush(builder);
|
|
685
|
+
|
|
686
|
+
const edges = registry.get(graph.edges('parent'));
|
|
687
|
+
expect(edges[Graph.relationKey('action')] ?? []).to.include('late-act');
|
|
688
|
+
const actions = registry.get(graph.actions('parent'));
|
|
689
|
+
expect(actions).has.length(1);
|
|
690
|
+
expect(actions[0].id).to.equal('late-act');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test('_actionContext captures and provides services to action execution', async () => {
|
|
694
|
+
const registry = Registry.make();
|
|
695
|
+
const builder = GraphBuilder.make({ registry });
|
|
696
|
+
const graph = builder.graph;
|
|
697
|
+
|
|
698
|
+
// Define a test service using Context.GenericTag pattern.
|
|
699
|
+
interface TestServiceInterface {
|
|
700
|
+
getValue(): number;
|
|
701
|
+
}
|
|
702
|
+
const TestService = Context.GenericTag<TestServiceInterface>('TestService');
|
|
703
|
+
|
|
704
|
+
// Track whether the action was executed with the correct context.
|
|
705
|
+
let executionResult: number | null = null;
|
|
706
|
+
|
|
707
|
+
// Create extension with service requirement.
|
|
708
|
+
// Note: The actions callback must USE the service for R to be inferred correctly.
|
|
709
|
+
const extensions = Effect.runSync(
|
|
710
|
+
GraphBuilder.createExtension({
|
|
711
|
+
id: 'test-extension',
|
|
712
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
713
|
+
actions: (node, get) =>
|
|
714
|
+
// Use TestService in the callback to include it in R.
|
|
715
|
+
Effect.gen(function* () {
|
|
716
|
+
const service = yield* TestService;
|
|
717
|
+
return [
|
|
718
|
+
{
|
|
719
|
+
id: 'test-action',
|
|
720
|
+
data: () =>
|
|
721
|
+
Effect.gen(function* () {
|
|
722
|
+
// Action can use the same service from captured context.
|
|
723
|
+
const svc = yield* TestService;
|
|
724
|
+
executionResult = svc.getValue();
|
|
725
|
+
}).pipe(Effect.asVoid),
|
|
726
|
+
properties: { label: `Test ${service.getValue()}` },
|
|
727
|
+
},
|
|
728
|
+
];
|
|
729
|
+
}),
|
|
730
|
+
}).pipe(Effect.provideService(TestService, { getValue: () => 42 })),
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
734
|
+
|
|
735
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
736
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
737
|
+
Graph.expand(graph, 'parent', 'child');
|
|
738
|
+
await GraphBuilder.flush(builder);
|
|
739
|
+
|
|
740
|
+
const actions = registry.get(graph.actions('parent'));
|
|
741
|
+
expect(actions).has.length(1);
|
|
742
|
+
|
|
743
|
+
// Verify _actionContext is captured.
|
|
744
|
+
const action = actions[0] as Node.Action;
|
|
745
|
+
expect(action._actionContext).to.not.be.undefined;
|
|
746
|
+
|
|
747
|
+
// Execute the action with the captured context.
|
|
748
|
+
const actionEffect = action.data();
|
|
749
|
+
const effectWithContext = action._actionContext
|
|
750
|
+
? actionEffect.pipe(Effect.provide(action._actionContext))
|
|
751
|
+
: actionEffect;
|
|
752
|
+
|
|
753
|
+
Effect.runSync(effectWithContext);
|
|
754
|
+
|
|
755
|
+
// Verify the service was accessible during execution.
|
|
756
|
+
expect(executionResult).to.equal(42);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('works with resolver', async () => {
|
|
760
|
+
const registry = Registry.make();
|
|
761
|
+
const builder = GraphBuilder.make({ registry });
|
|
762
|
+
const graph = builder.graph;
|
|
763
|
+
|
|
764
|
+
const extensions = Effect.runSync(
|
|
765
|
+
GraphBuilder.createExtension({
|
|
766
|
+
id: 'test-extension',
|
|
767
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
768
|
+
resolver: (id, get) => Effect.succeed({ id, type: EXAMPLE_TYPE, properties: {}, data: 'resolved' }),
|
|
769
|
+
}),
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
773
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
774
|
+
|
|
775
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
776
|
+
expect(node).to.not.be.null;
|
|
777
|
+
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
778
|
+
expect(node?.data).to.equal('resolved');
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('works with connector and actions together', async () => {
|
|
782
|
+
const registry = Registry.make();
|
|
783
|
+
const builder = GraphBuilder.make({ registry });
|
|
784
|
+
const graph = builder.graph;
|
|
785
|
+
|
|
786
|
+
const extensions = Effect.runSync(
|
|
787
|
+
GraphBuilder.createExtension({
|
|
788
|
+
id: 'test-extension',
|
|
789
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
790
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: node.data }]),
|
|
791
|
+
actions: (node, get) =>
|
|
792
|
+
Effect.succeed([
|
|
793
|
+
{
|
|
794
|
+
id: 'test-action',
|
|
795
|
+
data: () => Effect.void,
|
|
796
|
+
properties: { label: 'Test' },
|
|
797
|
+
},
|
|
798
|
+
]),
|
|
799
|
+
}),
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
803
|
+
|
|
804
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
805
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
806
|
+
Graph.expand(graph, 'parent', 'child');
|
|
807
|
+
await GraphBuilder.flush(builder);
|
|
808
|
+
|
|
809
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
810
|
+
// Should have both the child node and the action node.
|
|
811
|
+
expect(connections.length).to.be.greaterThanOrEqual(1);
|
|
812
|
+
const childNode = connections.find((n) => n.id === 'child');
|
|
813
|
+
expect(childNode).to.not.be.undefined;
|
|
814
|
+
expect(childNode?.data).to.equal('test');
|
|
815
|
+
|
|
816
|
+
const actions = registry.get(graph.actions('parent'));
|
|
817
|
+
expect(actions).has.length(1);
|
|
818
|
+
expect(actions[0].id).to.equal('test-action');
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test('works with reactive connector using get context', async () => {
|
|
822
|
+
const registry = Registry.make();
|
|
823
|
+
const builder = GraphBuilder.make({ registry });
|
|
824
|
+
const graph = builder.graph;
|
|
825
|
+
|
|
826
|
+
const state = Atom.make('initial');
|
|
827
|
+
|
|
828
|
+
const extensions = Effect.runSync(
|
|
829
|
+
GraphBuilder.createExtension({
|
|
830
|
+
id: 'test-extension',
|
|
831
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
832
|
+
connector: (node, get) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: get(state) }]),
|
|
833
|
+
}),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
837
|
+
|
|
838
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
839
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
840
|
+
Graph.expand(graph, 'parent', 'child');
|
|
841
|
+
await GraphBuilder.flush(builder);
|
|
842
|
+
|
|
843
|
+
{
|
|
844
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
845
|
+
expect(connections).has.length(1);
|
|
846
|
+
expect(connections[0].data).to.equal('initial');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
registry.set(state, 'updated');
|
|
850
|
+
await GraphBuilder.flush(builder);
|
|
851
|
+
|
|
852
|
+
{
|
|
853
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
854
|
+
expect(connections).has.length(1);
|
|
855
|
+
expect(connections[0].data).to.equal('updated');
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe('extension error handling', () => {
|
|
861
|
+
test('connector failure is caught and logged, returns empty array', async () => {
|
|
862
|
+
const registry = Registry.make();
|
|
863
|
+
const builder = GraphBuilder.make({ registry });
|
|
864
|
+
const graph = builder.graph;
|
|
865
|
+
|
|
866
|
+
const extensions = Effect.runSync(
|
|
867
|
+
GraphBuilder.createExtension({
|
|
868
|
+
id: 'failing-extension',
|
|
869
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
870
|
+
connector: (node, get) => Effect.fail(new Error('Connector failed intentionally')),
|
|
871
|
+
}),
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
875
|
+
|
|
876
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
877
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
878
|
+
|
|
879
|
+
// Should not throw, error is caught internally.
|
|
880
|
+
Graph.expand(graph, 'parent', 'child');
|
|
881
|
+
await GraphBuilder.flush(builder);
|
|
882
|
+
|
|
883
|
+
// Should return empty connections since the connector failed.
|
|
884
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
885
|
+
expect(connections).has.length(0);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test('actions failure is caught and logged, returns empty array', async () => {
|
|
889
|
+
const registry = Registry.make();
|
|
890
|
+
const builder = GraphBuilder.make({ registry });
|
|
891
|
+
const graph = builder.graph;
|
|
892
|
+
|
|
893
|
+
const extensions = Effect.runSync(
|
|
894
|
+
GraphBuilder.createExtension({
|
|
895
|
+
id: 'failing-actions-extension',
|
|
896
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
897
|
+
actions: (node, get) => Effect.fail(new Error('Actions failed intentionally')),
|
|
898
|
+
}),
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
902
|
+
|
|
903
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
904
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
905
|
+
|
|
906
|
+
// Should not throw, error is caught internally.
|
|
907
|
+
Graph.expand(graph, 'parent', 'child');
|
|
908
|
+
await GraphBuilder.flush(builder);
|
|
909
|
+
|
|
910
|
+
// Should return empty actions since the actions callback failed.
|
|
911
|
+
const actions = registry.get(graph.actions('parent'));
|
|
912
|
+
expect(actions).has.length(0);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
test('resolver failure is caught and logged, returns null', async () => {
|
|
916
|
+
const registry = Registry.make();
|
|
917
|
+
const builder = GraphBuilder.make({ registry });
|
|
918
|
+
const graph = builder.graph;
|
|
919
|
+
|
|
920
|
+
const extensions = Effect.runSync(
|
|
921
|
+
GraphBuilder.createExtension({
|
|
922
|
+
id: 'failing-resolver-extension',
|
|
923
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
924
|
+
resolver: (id, get) => Effect.fail(new Error('Resolver failed intentionally')),
|
|
925
|
+
}),
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
929
|
+
|
|
930
|
+
// Should not throw, error is caught internally.
|
|
931
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
932
|
+
|
|
933
|
+
// Should return null/none since the resolver failed.
|
|
934
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
935
|
+
expect(node).to.be.null;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test('failing extension does not affect other extensions', async () => {
|
|
939
|
+
const registry = Registry.make();
|
|
940
|
+
const builder = GraphBuilder.make({ registry });
|
|
941
|
+
const graph = builder.graph;
|
|
942
|
+
|
|
943
|
+
// Add a failing extension.
|
|
944
|
+
const failingExtensions = Effect.runSync(
|
|
945
|
+
GraphBuilder.createExtension({
|
|
946
|
+
id: 'failing-extension',
|
|
947
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
948
|
+
connector: (node, get) => Effect.fail(new Error('This one fails')),
|
|
949
|
+
}),
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
// Add a working extension.
|
|
953
|
+
const workingExtensions = Effect.runSync(
|
|
954
|
+
GraphBuilder.createExtension({
|
|
955
|
+
id: 'working-extension',
|
|
956
|
+
match: NodeMatcher.whenNodeType(EXAMPLE_TYPE),
|
|
957
|
+
connector: (node, get) =>
|
|
958
|
+
Effect.succeed([{ id: 'child-from-working', type: EXAMPLE_TYPE, data: 'success' }]),
|
|
959
|
+
}),
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
GraphBuilder.addExtension(builder, failingExtensions);
|
|
963
|
+
GraphBuilder.addExtension(builder, workingExtensions);
|
|
964
|
+
|
|
965
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
966
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: 'test' });
|
|
967
|
+
Graph.expand(graph, 'parent', 'child');
|
|
968
|
+
await GraphBuilder.flush(builder);
|
|
969
|
+
|
|
970
|
+
// The working extension should still produce its node.
|
|
971
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
972
|
+
expect(connections).has.length(1);
|
|
973
|
+
expect(connections[0].id).to.equal('child-from-working');
|
|
974
|
+
expect(connections[0].data).to.equal('success');
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
describe('createTypeExtension', () => {
|
|
979
|
+
test('creates extension matching by schema type with inferred object type', async () => {
|
|
980
|
+
const registry = Registry.make();
|
|
981
|
+
const builder = GraphBuilder.make({ registry });
|
|
982
|
+
const graph = builder.graph;
|
|
983
|
+
|
|
984
|
+
const extensions = Effect.runSync(
|
|
985
|
+
GraphBuilder.createTypeExtension({
|
|
986
|
+
id: 'type-extension',
|
|
987
|
+
type: TestSchema.Person,
|
|
988
|
+
connector: (object) => Effect.succeed([{ id: 'child', type: EXAMPLE_TYPE, data: object }]),
|
|
989
|
+
}),
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
993
|
+
|
|
994
|
+
const writableGraph = graph as Graph.WritableGraph;
|
|
995
|
+
const testObject = Obj.make(TestSchema.Person, { name: 'Test' });
|
|
996
|
+
Graph.addNode(writableGraph, { id: 'parent', type: EXAMPLE_TYPE, properties: {}, data: testObject });
|
|
997
|
+
Graph.expand(graph, 'parent', 'child');
|
|
998
|
+
await GraphBuilder.flush(builder);
|
|
999
|
+
|
|
1000
|
+
const connections = registry.get(graph.connections('parent', 'child'));
|
|
1001
|
+
expect(connections).has.length(1);
|
|
1002
|
+
expect(connections[0].id).to.equal('child');
|
|
1003
|
+
expect(connections[0].data).to.equal(testObject);
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
442
1007
|
});
|