@dxos/app-graph 0.8.4-main.c4373fc → 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 +1350 -686
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1349 -686
- 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 +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 +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.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 +36 -34
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +626 -119
- package/src/graph-builder.ts +667 -288
- package/src/graph.test.ts +429 -121
- package/src/graph.ts +1041 -403
- 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 +158 -119
- 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,16 +2,21 @@
|
|
|
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';
|
|
15
20
|
|
|
16
21
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
17
22
|
const EXAMPLE_ID = exampleId(1);
|
|
@@ -21,27 +26,26 @@ describe('GraphBuilder', () => {
|
|
|
21
26
|
describe('resolver', () => {
|
|
22
27
|
test('works', async () => {
|
|
23
28
|
const registry = Registry.make();
|
|
24
|
-
const builder =
|
|
29
|
+
const builder = GraphBuilder.make({ registry });
|
|
25
30
|
const graph = builder.graph;
|
|
26
31
|
|
|
27
32
|
{
|
|
28
|
-
const node =
|
|
33
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
29
34
|
expect(node).to.be.null;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
// Test direct API
|
|
38
|
+
GraphBuilder.addExtension(
|
|
39
|
+
builder,
|
|
40
|
+
GraphBuilder.createExtensionRaw({
|
|
34
41
|
id: 'resolver',
|
|
35
|
-
resolver: () => {
|
|
36
|
-
console.log('resolver');
|
|
37
|
-
return Rx.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
|
|
38
|
-
},
|
|
42
|
+
resolver: () => Atom.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }),
|
|
39
43
|
}),
|
|
40
44
|
);
|
|
41
|
-
await
|
|
45
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
42
46
|
|
|
43
47
|
{
|
|
44
|
-
const node =
|
|
48
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
45
49
|
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
46
50
|
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
47
51
|
expect(node?.data).to.equal(1);
|
|
@@ -50,55 +54,59 @@ describe('GraphBuilder', () => {
|
|
|
50
54
|
|
|
51
55
|
test('updates', async () => {
|
|
52
56
|
const registry = Registry.make();
|
|
53
|
-
const builder =
|
|
54
|
-
const name =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const builder = GraphBuilder.make({ registry });
|
|
58
|
+
const name = Atom.make('default');
|
|
59
|
+
GraphBuilder.addExtension(
|
|
60
|
+
builder,
|
|
61
|
+
GraphBuilder.createExtensionRaw({
|
|
57
62
|
id: 'resolver',
|
|
58
|
-
resolver: () =>
|
|
63
|
+
resolver: () => Atom.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
|
|
59
64
|
}),
|
|
60
65
|
);
|
|
61
66
|
const graph = builder.graph;
|
|
62
|
-
await
|
|
67
|
+
await Graph.initialize(graph, EXAMPLE_ID);
|
|
63
68
|
|
|
64
69
|
{
|
|
65
|
-
const node =
|
|
70
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
66
71
|
expect(node?.data).to.equal('default');
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
registry.set(name, 'updated');
|
|
70
75
|
|
|
71
76
|
{
|
|
72
|
-
const node =
|
|
77
|
+
const node = Graph.getNode(graph, EXAMPLE_ID).pipe(Option.getOrNull);
|
|
73
78
|
expect(node?.data).to.equal('updated');
|
|
74
79
|
}
|
|
75
80
|
});
|
|
76
81
|
});
|
|
77
82
|
|
|
78
83
|
describe('connector', () => {
|
|
79
|
-
test('works', () => {
|
|
84
|
+
test('works', async () => {
|
|
80
85
|
const registry = Registry.make();
|
|
81
|
-
const builder =
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
const builder = GraphBuilder.make({ registry });
|
|
87
|
+
GraphBuilder.addExtension(
|
|
88
|
+
builder,
|
|
89
|
+
GraphBuilder.createExtensionRaw({
|
|
84
90
|
id: 'outbound-connector',
|
|
85
|
-
connector: () =>
|
|
91
|
+
connector: () => Atom.make([{ id: 'child', type: EXAMPLE_TYPE, data: 2 }]),
|
|
86
92
|
}),
|
|
87
93
|
);
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
GraphBuilder.addExtension(
|
|
95
|
+
builder,
|
|
96
|
+
GraphBuilder.createExtensionRaw({
|
|
90
97
|
id: 'inbound-connector',
|
|
91
|
-
relation: 'inbound',
|
|
92
|
-
connector: () =>
|
|
98
|
+
relation: Node.childRelation('inbound'),
|
|
99
|
+
connector: () => Atom.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
93
100
|
}),
|
|
94
101
|
);
|
|
95
102
|
|
|
96
103
|
const graph = builder.graph;
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
105
|
+
Graph.expand(graph, Node.RootId, Node.childRelation('inbound'));
|
|
106
|
+
await GraphBuilder.flush(builder);
|
|
99
107
|
|
|
100
|
-
const outbound = registry.get(graph.connections(
|
|
101
|
-
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')));
|
|
102
110
|
|
|
103
111
|
expect(outbound).has.length(1);
|
|
104
112
|
expect(outbound[0].id).to.equal('child');
|
|
@@ -108,74 +116,83 @@ describe('GraphBuilder', () => {
|
|
|
108
116
|
expect(inbound[0].data).to.equal(0);
|
|
109
117
|
});
|
|
110
118
|
|
|
111
|
-
test('updates', () => {
|
|
119
|
+
test('updates', async () => {
|
|
112
120
|
const registry = Registry.make();
|
|
113
|
-
const builder =
|
|
114
|
-
const state =
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
const builder = GraphBuilder.make({ registry });
|
|
122
|
+
const state = Atom.make(0);
|
|
123
|
+
GraphBuilder.addExtension(
|
|
124
|
+
builder,
|
|
125
|
+
GraphBuilder.createExtensionRaw({
|
|
117
126
|
id: 'connector',
|
|
118
|
-
connector: () =>
|
|
127
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
119
128
|
}),
|
|
120
129
|
);
|
|
121
130
|
const graph = builder.graph;
|
|
122
|
-
|
|
131
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
132
|
+
await GraphBuilder.flush(builder);
|
|
123
133
|
|
|
124
134
|
{
|
|
125
|
-
const [node] = registry.get(graph.connections(
|
|
135
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
126
136
|
expect(node.data).to.equal(0);
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
{
|
|
130
140
|
registry.set(state, 1);
|
|
131
|
-
|
|
141
|
+
await GraphBuilder.flush(builder);
|
|
142
|
+
const [node] = registry.get(graph.connections(Node.RootId, 'child'));
|
|
132
143
|
expect(node.data).to.equal(1);
|
|
133
144
|
}
|
|
134
145
|
});
|
|
135
146
|
|
|
136
|
-
test('subscribes to updates', () => {
|
|
147
|
+
test('subscribes to updates', async () => {
|
|
137
148
|
const registry = Registry.make();
|
|
138
|
-
const builder =
|
|
139
|
-
const state =
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
const builder = GraphBuilder.make({ registry });
|
|
150
|
+
const state = Atom.make(0);
|
|
151
|
+
GraphBuilder.addExtension(
|
|
152
|
+
builder,
|
|
153
|
+
GraphBuilder.createExtensionRaw({
|
|
142
154
|
id: 'connector',
|
|
143
|
-
connector: () =>
|
|
155
|
+
connector: () => Atom.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
144
156
|
}),
|
|
145
157
|
);
|
|
146
158
|
const graph = builder.graph;
|
|
147
159
|
|
|
148
160
|
let count = 0;
|
|
149
|
-
const cancel = registry.subscribe(graph.connections(
|
|
161
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_) => {
|
|
150
162
|
count++;
|
|
151
163
|
});
|
|
152
164
|
onTestFinished(() => cancel());
|
|
153
165
|
|
|
154
166
|
expect(count).to.equal(0);
|
|
155
|
-
expect(registry.get(graph.connections(
|
|
167
|
+
expect(registry.get(graph.connections(Node.RootId, 'child'))).to.have.length(0);
|
|
156
168
|
expect(count).to.equal(1);
|
|
157
169
|
|
|
158
|
-
|
|
170
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
171
|
+
await GraphBuilder.flush(builder);
|
|
159
172
|
expect(count).to.equal(2);
|
|
173
|
+
|
|
160
174
|
registry.set(state, 1);
|
|
175
|
+
await GraphBuilder.flush(builder);
|
|
161
176
|
expect(count).to.equal(3);
|
|
162
177
|
});
|
|
163
178
|
|
|
164
|
-
test('updates with new extensions', () => {
|
|
179
|
+
test('updates with new extensions', async () => {
|
|
165
180
|
const registry = Registry.make();
|
|
166
|
-
const builder =
|
|
167
|
-
|
|
168
|
-
|
|
181
|
+
const builder = GraphBuilder.make({ registry });
|
|
182
|
+
GraphBuilder.addExtension(
|
|
183
|
+
builder,
|
|
184
|
+
GraphBuilder.createExtensionRaw({
|
|
169
185
|
id: 'connector',
|
|
170
|
-
connector: () =>
|
|
186
|
+
connector: () => Atom.make([{ id: EXAMPLE_ID, type: EXAMPLE_TYPE }]),
|
|
171
187
|
}),
|
|
172
188
|
);
|
|
173
189
|
const graph = builder.graph;
|
|
174
|
-
|
|
190
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
191
|
+
await GraphBuilder.flush(builder);
|
|
175
192
|
|
|
176
|
-
let nodes: Node[] = [];
|
|
193
|
+
let nodes: Node.Node[] = [];
|
|
177
194
|
let count = 0;
|
|
178
|
-
const cancel = registry.subscribe(graph.connections(
|
|
195
|
+
const cancel = registry.subscribe(graph.connections(Node.RootId, 'child'), (_nodes) => {
|
|
179
196
|
count++;
|
|
180
197
|
nodes = _nodes;
|
|
181
198
|
});
|
|
@@ -183,62 +200,67 @@ describe('GraphBuilder', () => {
|
|
|
183
200
|
|
|
184
201
|
expect(nodes).has.length(0);
|
|
185
202
|
expect(count).to.equal(0);
|
|
186
|
-
registry.get(graph.connections(
|
|
203
|
+
registry.get(graph.connections(Node.RootId, 'child'));
|
|
187
204
|
expect(nodes).has.length(1);
|
|
188
205
|
expect(count).to.equal(1);
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
GraphBuilder.addExtension(
|
|
208
|
+
builder,
|
|
209
|
+
GraphBuilder.createExtensionRaw({
|
|
192
210
|
id: 'connector-2',
|
|
193
|
-
connector: () =>
|
|
211
|
+
connector: () => Atom.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
194
212
|
}),
|
|
195
213
|
);
|
|
214
|
+
await GraphBuilder.flush(builder);
|
|
196
215
|
expect(nodes).has.length(2);
|
|
197
216
|
expect(count).to.equal(2);
|
|
198
217
|
});
|
|
199
218
|
|
|
200
|
-
test('removes', () => {
|
|
219
|
+
test('removes', async () => {
|
|
201
220
|
const registry = Registry.make();
|
|
202
|
-
const builder =
|
|
203
|
-
const nodes =
|
|
221
|
+
const builder = GraphBuilder.make({ registry });
|
|
222
|
+
const nodes = Atom.make([
|
|
204
223
|
{ id: exampleId(1), type: EXAMPLE_TYPE },
|
|
205
224
|
{ id: exampleId(2), type: EXAMPLE_TYPE },
|
|
206
225
|
]);
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
GraphBuilder.addExtension(
|
|
227
|
+
builder,
|
|
228
|
+
GraphBuilder.createExtensionRaw({
|
|
209
229
|
id: 'connector',
|
|
210
|
-
connector: () =>
|
|
230
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
211
231
|
}),
|
|
212
232
|
);
|
|
213
233
|
const graph = builder.graph;
|
|
214
|
-
|
|
234
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
235
|
+
await GraphBuilder.flush(builder);
|
|
215
236
|
|
|
216
237
|
{
|
|
217
|
-
const nodes = registry.get(graph.connections(
|
|
238
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
218
239
|
expect(nodes).has.length(2);
|
|
219
240
|
expect(nodes[0].id).to.equal(exampleId(1));
|
|
220
241
|
expect(nodes[1].id).to.equal(exampleId(2));
|
|
221
242
|
}
|
|
222
243
|
|
|
223
244
|
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
245
|
+
await GraphBuilder.flush(builder);
|
|
224
246
|
|
|
225
247
|
{
|
|
226
|
-
const nodes = registry.get(graph.connections(
|
|
248
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
227
249
|
expect(nodes).has.length(1);
|
|
228
250
|
expect(nodes[0].id).to.equal(exampleId(3));
|
|
229
251
|
}
|
|
230
252
|
});
|
|
231
253
|
|
|
232
|
-
test('nodes are updated when removed', () => {
|
|
254
|
+
test('nodes are updated when removed', async () => {
|
|
233
255
|
const registry = Registry.make();
|
|
234
|
-
const builder =
|
|
235
|
-
const name =
|
|
256
|
+
const builder = GraphBuilder.make({ registry });
|
|
257
|
+
const name = Atom.make('removed');
|
|
236
258
|
|
|
237
|
-
|
|
238
|
-
|
|
259
|
+
GraphBuilder.addExtension(builder, [
|
|
260
|
+
GraphBuilder.createExtensionRaw({
|
|
239
261
|
id: 'root',
|
|
240
262
|
connector: (node) =>
|
|
241
|
-
|
|
263
|
+
Atom.make((get) =>
|
|
242
264
|
Function.pipe(
|
|
243
265
|
get(node),
|
|
244
266
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
@@ -260,42 +282,48 @@ describe('GraphBuilder', () => {
|
|
|
260
282
|
});
|
|
261
283
|
onTestFinished(() => cancel());
|
|
262
284
|
|
|
263
|
-
|
|
285
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
286
|
+
await GraphBuilder.flush(builder);
|
|
264
287
|
expect(count).to.equal(0);
|
|
265
288
|
expect(exists).to.be.false;
|
|
266
289
|
|
|
267
290
|
registry.set(name, 'default');
|
|
291
|
+
await GraphBuilder.flush(builder);
|
|
268
292
|
expect(count).to.equal(1);
|
|
269
293
|
expect(exists).to.be.true;
|
|
270
294
|
|
|
271
295
|
registry.set(name, 'removed');
|
|
296
|
+
await GraphBuilder.flush(builder);
|
|
272
297
|
expect(count).to.equal(2);
|
|
273
298
|
expect(exists).to.be.false;
|
|
274
299
|
|
|
275
300
|
registry.set(name, 'added');
|
|
301
|
+
await GraphBuilder.flush(builder);
|
|
276
302
|
expect(count).to.equal(3);
|
|
277
303
|
expect(exists).to.be.true;
|
|
278
304
|
});
|
|
279
305
|
|
|
280
306
|
test('sort edges', async () => {
|
|
281
307
|
const registry = Registry.make();
|
|
282
|
-
const builder =
|
|
283
|
-
const nodes =
|
|
308
|
+
const builder = GraphBuilder.make({ registry });
|
|
309
|
+
const nodes = Atom.make([
|
|
284
310
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
285
311
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
286
312
|
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
287
313
|
]);
|
|
288
|
-
|
|
289
|
-
|
|
314
|
+
GraphBuilder.addExtension(
|
|
315
|
+
builder,
|
|
316
|
+
GraphBuilder.createExtensionRaw({
|
|
290
317
|
id: 'connector',
|
|
291
|
-
connector: () =>
|
|
318
|
+
connector: () => Atom.make((get) => get(nodes)),
|
|
292
319
|
}),
|
|
293
320
|
);
|
|
294
321
|
const graph = builder.graph;
|
|
295
|
-
|
|
322
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
323
|
+
await GraphBuilder.flush(builder);
|
|
296
324
|
|
|
297
325
|
{
|
|
298
|
-
const nodes = registry.get(graph.connections(
|
|
326
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
299
327
|
expect(nodes).has.length(3);
|
|
300
328
|
expect(nodes[0].id).to.equal(exampleId(1));
|
|
301
329
|
expect(nodes[1].id).to.equal(exampleId(2));
|
|
@@ -307,12 +335,10 @@ describe('GraphBuilder', () => {
|
|
|
307
335
|
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
308
336
|
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
309
337
|
]);
|
|
310
|
-
|
|
311
|
-
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
312
|
-
await sleep(0);
|
|
338
|
+
await GraphBuilder.flush(builder);
|
|
313
339
|
|
|
314
340
|
{
|
|
315
|
-
const nodes = registry.get(graph.connections(
|
|
341
|
+
const nodes = registry.get(graph.connections(Node.RootId, 'child'));
|
|
316
342
|
expect(nodes).has.length(3);
|
|
317
343
|
expect(nodes[0].id).to.equal(exampleId(3));
|
|
318
344
|
expect(nodes[1].id).to.equal(exampleId(1));
|
|
@@ -320,17 +346,17 @@ describe('GraphBuilder', () => {
|
|
|
320
346
|
}
|
|
321
347
|
});
|
|
322
348
|
|
|
323
|
-
test('updates are constrained', () => {
|
|
349
|
+
test('updates are constrained', async () => {
|
|
324
350
|
const registry = Registry.make();
|
|
325
|
-
const builder =
|
|
326
|
-
const name =
|
|
327
|
-
const sub =
|
|
351
|
+
const builder = GraphBuilder.make({ registry });
|
|
352
|
+
const name = Atom.make('default');
|
|
353
|
+
const sub = Atom.make('default');
|
|
328
354
|
|
|
329
|
-
|
|
330
|
-
|
|
355
|
+
GraphBuilder.addExtension(builder, [
|
|
356
|
+
GraphBuilder.createExtensionRaw({
|
|
331
357
|
id: 'root',
|
|
332
358
|
connector: (node) =>
|
|
333
|
-
|
|
359
|
+
Atom.make((get) =>
|
|
334
360
|
Function.pipe(
|
|
335
361
|
get(node),
|
|
336
362
|
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
@@ -340,10 +366,10 @@ describe('GraphBuilder', () => {
|
|
|
340
366
|
),
|
|
341
367
|
),
|
|
342
368
|
}),
|
|
343
|
-
|
|
369
|
+
GraphBuilder.createExtensionRaw({
|
|
344
370
|
id: 'connector1',
|
|
345
371
|
connector: (node) =>
|
|
346
|
-
|
|
372
|
+
Atom.make((get) =>
|
|
347
373
|
Function.pipe(
|
|
348
374
|
get(node),
|
|
349
375
|
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(get(sub)) : Option.none())),
|
|
@@ -352,10 +378,10 @@ describe('GraphBuilder', () => {
|
|
|
352
378
|
),
|
|
353
379
|
),
|
|
354
380
|
}),
|
|
355
|
-
|
|
381
|
+
GraphBuilder.createExtensionRaw({
|
|
356
382
|
id: 'connector2',
|
|
357
383
|
connector: (node) =>
|
|
358
|
-
|
|
384
|
+
Atom.make((get) =>
|
|
359
385
|
Function.pipe(
|
|
360
386
|
get(node),
|
|
361
387
|
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(node.data) : Option.none())),
|
|
@@ -387,46 +413,53 @@ describe('GraphBuilder', () => {
|
|
|
387
413
|
onTestFinished(() => dependentCancel());
|
|
388
414
|
|
|
389
415
|
// Counts should not increment until the node is expanded.
|
|
390
|
-
|
|
416
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
417
|
+
await GraphBuilder.flush(builder);
|
|
391
418
|
expect(parentCount).to.equal(1);
|
|
392
419
|
expect(independentCount).to.equal(0);
|
|
393
420
|
expect(dependentCount).to.equal(0);
|
|
394
421
|
|
|
395
422
|
// Counts should increment when the node is expanded.
|
|
396
|
-
|
|
423
|
+
Graph.expand(graph, EXAMPLE_ID, 'child');
|
|
424
|
+
await GraphBuilder.flush(builder);
|
|
397
425
|
expect(parentCount).to.equal(1);
|
|
398
426
|
expect(independentCount).to.equal(1);
|
|
399
427
|
expect(dependentCount).to.equal(1);
|
|
400
428
|
|
|
401
429
|
// Only dependent count should increment when the parent changes.
|
|
402
430
|
registry.set(name, 'updated');
|
|
431
|
+
await GraphBuilder.flush(builder);
|
|
403
432
|
expect(parentCount).to.equal(2);
|
|
404
433
|
expect(independentCount).to.equal(1);
|
|
405
434
|
expect(dependentCount).to.equal(2);
|
|
406
435
|
|
|
407
436
|
// Only independent count should increment when its state changes.
|
|
408
437
|
registry.set(sub, 'updated');
|
|
438
|
+
await GraphBuilder.flush(builder);
|
|
409
439
|
expect(parentCount).to.equal(2);
|
|
410
440
|
expect(independentCount).to.equal(2);
|
|
411
441
|
expect(dependentCount).to.equal(2);
|
|
412
442
|
|
|
413
443
|
// Independent count should update if its state changes even if the parent is removed.
|
|
414
|
-
|
|
444
|
+
Atom.batch(() => {
|
|
415
445
|
registry.set(name, 'removed');
|
|
416
446
|
registry.set(sub, 'batch');
|
|
417
447
|
});
|
|
448
|
+
await GraphBuilder.flush(builder);
|
|
418
449
|
expect(parentCount).to.equal(2);
|
|
419
450
|
expect(independentCount).to.equal(3);
|
|
420
451
|
expect(dependentCount).to.equal(2);
|
|
421
452
|
|
|
422
453
|
// Dependent count should increment when the node is added back.
|
|
423
454
|
registry.set(name, 'added');
|
|
455
|
+
await GraphBuilder.flush(builder);
|
|
424
456
|
expect(parentCount).to.equal(3);
|
|
425
457
|
expect(independentCount).to.equal(3);
|
|
426
458
|
expect(dependentCount).to.equal(3);
|
|
427
459
|
|
|
428
460
|
// Counts should not increment when the node is expanded again.
|
|
429
|
-
|
|
461
|
+
Graph.expand(graph, EXAMPLE_ID, 'child');
|
|
462
|
+
await GraphBuilder.flush(builder);
|
|
430
463
|
expect(parentCount).to.equal(3);
|
|
431
464
|
expect(independentCount).to.equal(3);
|
|
432
465
|
expect(dependentCount).to.equal(3);
|
|
@@ -434,12 +467,13 @@ describe('GraphBuilder', () => {
|
|
|
434
467
|
|
|
435
468
|
test('eager graph expansion', async () => {
|
|
436
469
|
const registry = Registry.make();
|
|
437
|
-
const builder =
|
|
438
|
-
|
|
439
|
-
|
|
470
|
+
const builder = GraphBuilder.make({ registry });
|
|
471
|
+
GraphBuilder.addExtension(
|
|
472
|
+
builder,
|
|
473
|
+
GraphBuilder.createExtensionRaw({
|
|
440
474
|
id: 'connector',
|
|
441
475
|
connector: (node) => {
|
|
442
|
-
return
|
|
476
|
+
return Atom.make((get) =>
|
|
443
477
|
Function.pipe(
|
|
444
478
|
get(node),
|
|
445
479
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
@@ -455,14 +489,14 @@ describe('GraphBuilder', () => {
|
|
|
455
489
|
let count = 0;
|
|
456
490
|
const trigger = new Trigger();
|
|
457
491
|
builder.graph.onNodeChanged.on(({ id }) => {
|
|
458
|
-
builder.graph
|
|
492
|
+
Graph.expand(builder.graph, id, 'child');
|
|
459
493
|
count++;
|
|
460
494
|
if (count === 5) {
|
|
461
495
|
trigger.wake();
|
|
462
496
|
}
|
|
463
497
|
});
|
|
464
498
|
|
|
465
|
-
builder.graph.
|
|
499
|
+
Graph.expand(builder.graph, Node.RootId, 'child');
|
|
466
500
|
await trigger.wait();
|
|
467
501
|
expect(count).to.equal(5);
|
|
468
502
|
});
|
|
@@ -470,12 +504,13 @@ describe('GraphBuilder', () => {
|
|
|
470
504
|
|
|
471
505
|
describe('explore', () => {
|
|
472
506
|
test('works', async () => {
|
|
473
|
-
const builder =
|
|
474
|
-
|
|
475
|
-
|
|
507
|
+
const builder = GraphBuilder.make();
|
|
508
|
+
GraphBuilder.addExtension(
|
|
509
|
+
builder,
|
|
510
|
+
GraphBuilder.createExtensionRaw({
|
|
476
511
|
id: 'connector',
|
|
477
512
|
connector: (node) =>
|
|
478
|
-
|
|
513
|
+
Atom.make((get) =>
|
|
479
514
|
Function.pipe(
|
|
480
515
|
get(node),
|
|
481
516
|
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
@@ -488,7 +523,8 @@ describe('GraphBuilder', () => {
|
|
|
488
523
|
);
|
|
489
524
|
|
|
490
525
|
let count = 0;
|
|
491
|
-
await
|
|
526
|
+
await GraphBuilder.explore(builder, {
|
|
527
|
+
relation: 'child',
|
|
492
528
|
visitor: () => {
|
|
493
529
|
count++;
|
|
494
530
|
},
|
|
@@ -497,4 +533,475 @@ describe('GraphBuilder', () => {
|
|
|
497
533
|
expect(count).to.equal(6);
|
|
498
534
|
});
|
|
499
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
|
+
});
|
|
500
1007
|
});
|