@dxos/app-graph 0.8.2-staging.7ac8446 → 0.8.2
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 +593 -794
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +585 -785
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +593 -794
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/experimental/graph-projections.test.d.ts +25 -0
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +48 -91
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +191 -98
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +3 -3
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/signals-integration.test.d.ts +2 -0
- package/dist/types/src/signals-integration.test.d.ts.map +1 -0
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +5 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +24 -17
- package/src/experimental/graph-projections.test.ts +56 -0
- package/src/graph-builder.test.ts +293 -310
- package/src/graph-builder.ts +235 -318
- package/src/graph.test.ts +314 -463
- package/src/graph.ts +452 -455
- package/src/node.ts +4 -4
- package/src/signals-integration.test.ts +218 -0
- package/src/stories/EchoGraph.stories.tsx +67 -76
- package/src/testing.ts +20 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
|
+
import { Option, pipe } from 'effect';
|
|
7
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { sleep, Trigger } from '@dxos/async';
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { ROOT_ID } from './graph';
|
|
12
|
+
import { createExtension, GraphBuilder } from './graph-builder';
|
|
12
13
|
import { type Node } from './node';
|
|
13
14
|
|
|
14
15
|
const exampleId = (id: number) => `dx:test:${id}`;
|
|
@@ -16,379 +17,396 @@ const EXAMPLE_ID = exampleId(1);
|
|
|
16
17
|
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
17
18
|
|
|
18
19
|
describe('GraphBuilder', () => {
|
|
19
|
-
describe('
|
|
20
|
-
test('works',
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
const node = graph.findNode(EXAMPLE_ID);
|
|
26
|
-
expect(node).to.be.undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
builder.addExtension(
|
|
30
|
-
createExtension({ id: 'resolver', resolver: () => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 }) }),
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
const node = await graph.waitForNode(EXAMPLE_ID);
|
|
35
|
-
expect(node?.id).to.equal(EXAMPLE_ID);
|
|
36
|
-
expect(node?.type).to.equal(EXAMPLE_TYPE);
|
|
37
|
-
expect(node?.data).to.equal(1);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('updates', async () => {
|
|
42
|
-
const builder = new GraphBuilder();
|
|
43
|
-
const name = signal('default');
|
|
20
|
+
describe('connector', () => {
|
|
21
|
+
test('works', () => {
|
|
22
|
+
const registry = Registry.make();
|
|
23
|
+
const builder = new GraphBuilder({ registry });
|
|
44
24
|
builder.addExtension(
|
|
45
|
-
createExtension({
|
|
25
|
+
createExtension({
|
|
26
|
+
id: 'outbound-connector',
|
|
27
|
+
connector: () => Rx.make([{ id: 'child', type: EXAMPLE_TYPE, data: 2 }]),
|
|
28
|
+
}),
|
|
46
29
|
);
|
|
47
|
-
const graph = builder.graph;
|
|
48
|
-
|
|
49
|
-
const node = await graph.waitForNode(EXAMPLE_ID);
|
|
50
|
-
expect(node?.data).to.equal('default');
|
|
51
|
-
|
|
52
|
-
name.value = 'updated';
|
|
53
|
-
expect(node?.data).to.equal('updated');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('memoize', async () => {
|
|
57
|
-
const builder = new GraphBuilder();
|
|
58
|
-
const name = signal('default');
|
|
59
|
-
let count = 0;
|
|
60
|
-
let memoizedCount = 0;
|
|
61
30
|
builder.addExtension(
|
|
62
31
|
createExtension({
|
|
63
|
-
id: '
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
memoize(() => {
|
|
67
|
-
memoizedCount++;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return { id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: name.value };
|
|
71
|
-
},
|
|
32
|
+
id: 'inbound-connector',
|
|
33
|
+
relation: 'inbound',
|
|
34
|
+
connector: () => Rx.make([{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }]),
|
|
72
35
|
}),
|
|
73
36
|
);
|
|
74
|
-
const graph = builder.graph;
|
|
75
37
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(memoizedCount).to.equal(1);
|
|
38
|
+
const graph = builder.graph;
|
|
39
|
+
graph.expand(ROOT_ID);
|
|
40
|
+
graph.expand(ROOT_ID, 'inbound');
|
|
80
41
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
name!.value = 'three';
|
|
42
|
+
const outbound = registry.get(graph.connections(ROOT_ID));
|
|
43
|
+
const inbound = registry.get(graph.connections(ROOT_ID, 'inbound'));
|
|
84
44
|
|
|
85
|
-
expect(
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
45
|
+
expect(outbound).has.length(1);
|
|
46
|
+
expect(outbound[0].id).to.equal('child');
|
|
47
|
+
expect(outbound[0].data).to.equal(2);
|
|
48
|
+
expect(inbound).has.length(1);
|
|
49
|
+
expect(inbound[0].id).to.equal('parent');
|
|
50
|
+
expect(inbound[0].data).to.equal(0);
|
|
88
51
|
});
|
|
89
52
|
|
|
90
|
-
test('
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const graph = builder.graph;
|
|
95
|
-
|
|
53
|
+
test('updates', () => {
|
|
54
|
+
const registry = Registry.make();
|
|
55
|
+
const builder = new GraphBuilder({ registry });
|
|
56
|
+
const state = Rx.make(0);
|
|
96
57
|
builder.addExtension(
|
|
97
58
|
createExtension({
|
|
98
|
-
id: '
|
|
99
|
-
|
|
100
|
-
if (id === ROOT_ID) {
|
|
101
|
-
return { id: ROOT_ID, type: ROOT_TYPE };
|
|
102
|
-
} else {
|
|
103
|
-
return { id, type: EXAMPLE_TYPE, data: id, properties: { value: parseInt(id.replace('test', '')) } };
|
|
104
|
-
}
|
|
105
|
-
},
|
|
59
|
+
id: 'connector',
|
|
60
|
+
connector: () => Rx.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
106
61
|
}),
|
|
107
62
|
);
|
|
63
|
+
const graph = builder.graph;
|
|
64
|
+
graph.expand(ROOT_ID);
|
|
108
65
|
|
|
109
66
|
{
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
expect(graph.findNode('test1', false)?.properties.value).to.equal(1);
|
|
113
|
-
expect(graph.findNode('test2', false)).toBeDefined();
|
|
114
|
-
expect(graph.findNode('test2', false)?.data).to.equal(null);
|
|
115
|
-
expect(graph.findNode('test2', false)?.properties.value).to.equal(2);
|
|
67
|
+
const [node] = registry.get(graph.connections(ROOT_ID));
|
|
68
|
+
expect(node.data).to.equal(0);
|
|
116
69
|
}
|
|
117
70
|
|
|
118
|
-
await builder.initialize();
|
|
119
|
-
|
|
120
71
|
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
expect(
|
|
124
|
-
expect(graph.findNode('test2', false)?.properties.value).to.equal(2);
|
|
72
|
+
registry.set(state, 1);
|
|
73
|
+
const [node] = registry.get(graph.connections(ROOT_ID));
|
|
74
|
+
expect(node.data).to.equal(1);
|
|
125
75
|
}
|
|
126
76
|
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('connector', () => {
|
|
130
|
-
test('works', async () => {
|
|
131
|
-
const builder = new GraphBuilder();
|
|
132
|
-
builder.addExtension(
|
|
133
|
-
createExtension({
|
|
134
|
-
id: 'outbound-connector',
|
|
135
|
-
connector: () => [{ id: 'child', type: EXAMPLE_TYPE, data: 2 }],
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
138
|
-
builder.addExtension(
|
|
139
|
-
createExtension({
|
|
140
|
-
id: 'inbound-connector',
|
|
141
|
-
relation: 'inbound',
|
|
142
|
-
connector: () => [{ id: 'parent', type: EXAMPLE_TYPE, data: 0 }],
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const graph = builder.graph;
|
|
147
|
-
await graph.expand(graph.root);
|
|
148
|
-
await graph.expand(graph.root, 'inbound');
|
|
149
|
-
|
|
150
|
-
const outbound = graph.nodes(graph.root);
|
|
151
|
-
const inbound = graph.nodes(graph.root, { relation: 'inbound' });
|
|
152
|
-
|
|
153
|
-
expect(outbound).has.length(1);
|
|
154
|
-
expect(outbound?.[0].id).to.equal('child');
|
|
155
|
-
expect(outbound?.[0].data).to.equal(2);
|
|
156
|
-
expect(inbound).has.length(1);
|
|
157
|
-
expect(inbound?.[0].id).to.equal('parent');
|
|
158
|
-
expect(inbound?.[0].data).to.equal(0);
|
|
159
|
-
});
|
|
160
77
|
|
|
161
|
-
test('updates',
|
|
162
|
-
const
|
|
163
|
-
const builder = new GraphBuilder();
|
|
78
|
+
test('subscribes to updates', () => {
|
|
79
|
+
const registry = Registry.make();
|
|
80
|
+
const builder = new GraphBuilder({ registry });
|
|
81
|
+
const state = Rx.make(0);
|
|
164
82
|
builder.addExtension(
|
|
165
83
|
createExtension({
|
|
166
84
|
id: 'connector',
|
|
167
|
-
connector: () => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data:
|
|
85
|
+
connector: () => Rx.make((get) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(state) }]),
|
|
168
86
|
}),
|
|
169
87
|
);
|
|
170
88
|
const graph = builder.graph;
|
|
171
|
-
await graph.expand(graph.root);
|
|
172
89
|
|
|
173
|
-
|
|
174
|
-
|
|
90
|
+
let count = 0;
|
|
91
|
+
const cancel = registry.subscribe(graph.connections(ROOT_ID), (_) => {
|
|
92
|
+
count++;
|
|
93
|
+
});
|
|
94
|
+
onTestFinished(() => cancel());
|
|
95
|
+
|
|
96
|
+
expect(count).to.equal(0);
|
|
97
|
+
expect(registry.get(graph.connections(ROOT_ID))).to.have.length(0);
|
|
98
|
+
expect(count).to.equal(1);
|
|
175
99
|
|
|
176
|
-
|
|
177
|
-
expect(
|
|
100
|
+
graph.expand(ROOT_ID);
|
|
101
|
+
expect(count).to.equal(2);
|
|
102
|
+
registry.set(state, 1);
|
|
103
|
+
expect(count).to.equal(3);
|
|
178
104
|
});
|
|
179
105
|
|
|
180
|
-
test('updates with new extensions',
|
|
181
|
-
const
|
|
182
|
-
const builder = new GraphBuilder();
|
|
106
|
+
test('updates with new extensions', () => {
|
|
107
|
+
const registry = Registry.make();
|
|
108
|
+
const builder = new GraphBuilder({ registry });
|
|
183
109
|
builder.addExtension(
|
|
184
110
|
createExtension({
|
|
185
111
|
id: 'connector',
|
|
186
|
-
connector: () => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE
|
|
112
|
+
connector: () => Rx.make([{ id: EXAMPLE_ID, type: EXAMPLE_TYPE }]),
|
|
187
113
|
}),
|
|
188
114
|
);
|
|
189
115
|
const graph = builder.graph;
|
|
190
|
-
|
|
116
|
+
graph.expand(ROOT_ID);
|
|
191
117
|
|
|
192
118
|
let nodes: Node[] = [];
|
|
193
|
-
|
|
194
|
-
|
|
119
|
+
let count = 0;
|
|
120
|
+
const cancel = registry.subscribe(graph.connections(ROOT_ID), (_nodes) => {
|
|
121
|
+
count++;
|
|
122
|
+
nodes = _nodes;
|
|
195
123
|
});
|
|
124
|
+
onTestFinished(() => cancel());
|
|
196
125
|
|
|
197
|
-
expect(
|
|
126
|
+
expect(nodes).has.length(0);
|
|
127
|
+
expect(count).to.equal(0);
|
|
128
|
+
registry.get(graph.connections(ROOT_ID));
|
|
198
129
|
expect(nodes).has.length(1);
|
|
199
|
-
expect(
|
|
130
|
+
expect(count).to.equal(1);
|
|
200
131
|
|
|
201
132
|
builder.addExtension(
|
|
202
133
|
createExtension({
|
|
203
134
|
id: 'connector-2',
|
|
204
|
-
connector: () => [{ id: exampleId(2), type: EXAMPLE_TYPE
|
|
135
|
+
connector: () => Rx.make([{ id: exampleId(2), type: EXAMPLE_TYPE }]),
|
|
205
136
|
}),
|
|
206
137
|
);
|
|
207
|
-
|
|
208
|
-
expect(updates.count).to.equal(1);
|
|
209
138
|
expect(nodes).has.length(2);
|
|
210
|
-
expect(
|
|
211
|
-
expect(nodes[1].id).to.equal(exampleId(2));
|
|
139
|
+
expect(count).to.equal(2);
|
|
212
140
|
});
|
|
213
141
|
|
|
214
|
-
test('removes',
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
142
|
+
test('removes', () => {
|
|
143
|
+
const registry = Registry.make();
|
|
144
|
+
const builder = new GraphBuilder({ registry });
|
|
145
|
+
const nodes = Rx.make([
|
|
146
|
+
{ id: exampleId(1), type: EXAMPLE_TYPE },
|
|
147
|
+
{ id: exampleId(2), type: EXAMPLE_TYPE },
|
|
218
148
|
]);
|
|
219
|
-
|
|
220
|
-
const builder = new GraphBuilder();
|
|
221
149
|
builder.addExtension(
|
|
222
150
|
createExtension({
|
|
223
151
|
id: 'connector',
|
|
224
|
-
connector: () => nodes
|
|
152
|
+
connector: () => Rx.make((get) => get(nodes)),
|
|
225
153
|
}),
|
|
226
154
|
);
|
|
227
155
|
const graph = builder.graph;
|
|
228
|
-
|
|
156
|
+
graph.expand(ROOT_ID);
|
|
229
157
|
|
|
230
158
|
{
|
|
231
|
-
const nodes =
|
|
159
|
+
const nodes = registry.get(graph.connections(ROOT_ID));
|
|
232
160
|
expect(nodes).has.length(2);
|
|
233
161
|
expect(nodes[0].id).to.equal(exampleId(1));
|
|
162
|
+
expect(nodes[1].id).to.equal(exampleId(2));
|
|
234
163
|
}
|
|
235
164
|
|
|
236
|
-
nodes
|
|
165
|
+
registry.set(nodes, [{ id: exampleId(3), type: EXAMPLE_TYPE }]);
|
|
237
166
|
|
|
238
167
|
{
|
|
239
|
-
const nodes =
|
|
168
|
+
const nodes = registry.get(graph.connections(ROOT_ID));
|
|
240
169
|
expect(nodes).has.length(1);
|
|
241
170
|
expect(nodes[0].id).to.equal(exampleId(3));
|
|
242
|
-
expect(graph.findNode(exampleId(1))).to.be.undefined;
|
|
243
171
|
}
|
|
244
172
|
});
|
|
245
173
|
|
|
246
|
-
test('
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
174
|
+
test('nodes are updated when removed', () => {
|
|
175
|
+
const registry = Registry.make();
|
|
176
|
+
const builder = new GraphBuilder({ registry });
|
|
177
|
+
const name = Rx.make('removed');
|
|
178
|
+
|
|
251
179
|
builder.addExtension([
|
|
252
180
|
createExtension({
|
|
253
181
|
id: 'root',
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
count++;
|
|
265
|
-
sub.value;
|
|
266
|
-
|
|
267
|
-
return [];
|
|
268
|
-
},
|
|
182
|
+
connector: (node) =>
|
|
183
|
+
Rx.make((get) =>
|
|
184
|
+
pipe(
|
|
185
|
+
get(node),
|
|
186
|
+
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
187
|
+
Option.filter((name) => name !== 'removed'),
|
|
188
|
+
Option.map((name) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: name }]),
|
|
189
|
+
Option.getOrElse(() => []),
|
|
190
|
+
),
|
|
191
|
+
),
|
|
269
192
|
}),
|
|
270
193
|
]);
|
|
271
194
|
|
|
272
|
-
// Count should not increment until the node is expanded.
|
|
273
195
|
const graph = builder.graph;
|
|
274
|
-
|
|
196
|
+
|
|
197
|
+
let count = 0;
|
|
198
|
+
let exists = false;
|
|
199
|
+
const cancel = registry.subscribe(graph.node(EXAMPLE_ID), (node) => {
|
|
200
|
+
count++;
|
|
201
|
+
exists = Option.isSome(node);
|
|
202
|
+
});
|
|
203
|
+
onTestFinished(() => cancel());
|
|
204
|
+
|
|
205
|
+
graph.expand(ROOT_ID);
|
|
275
206
|
expect(count).to.equal(0);
|
|
207
|
+
expect(exists).to.be.false;
|
|
276
208
|
|
|
277
|
-
|
|
278
|
-
const [node] = graph.nodes(graph.root);
|
|
279
|
-
await graph.expand(node!);
|
|
209
|
+
registry.set(name, 'default');
|
|
280
210
|
expect(count).to.equal(1);
|
|
211
|
+
expect(exists).to.be.true;
|
|
281
212
|
|
|
282
|
-
|
|
283
|
-
name.value = 'updated';
|
|
213
|
+
registry.set(name, 'removed');
|
|
284
214
|
expect(count).to.equal(2);
|
|
215
|
+
expect(exists).to.be.false;
|
|
285
216
|
|
|
286
|
-
|
|
287
|
-
sub.value = 'updated';
|
|
217
|
+
registry.set(name, 'added');
|
|
288
218
|
expect(count).to.equal(3);
|
|
289
|
-
|
|
290
|
-
// Count will still increment if the node is removed in a batch.
|
|
291
|
-
batch(() => {
|
|
292
|
-
name.value = 'removed';
|
|
293
|
-
sub.value = 'batch';
|
|
294
|
-
});
|
|
295
|
-
expect(count).to.equal(4);
|
|
296
|
-
|
|
297
|
-
// Count should not increment after the node is removed.
|
|
298
|
-
sub.value = 'removed';
|
|
299
|
-
expect(count).to.equal(4);
|
|
300
|
-
|
|
301
|
-
// Count will not increment when node is added back.
|
|
302
|
-
name.value = 'added';
|
|
303
|
-
expect(count).to.equal(4);
|
|
304
|
-
|
|
305
|
-
// Count should increment when the node is expanded again.
|
|
306
|
-
await graph.expand(node!);
|
|
307
|
-
expect(count).to.equal(5);
|
|
308
|
-
|
|
309
|
-
// Count should increment when signal changes again.
|
|
310
|
-
sub.value = 'added';
|
|
311
|
-
expect(count).to.equal(6);
|
|
219
|
+
expect(exists).to.be.true;
|
|
312
220
|
});
|
|
313
221
|
|
|
314
|
-
test('
|
|
315
|
-
const
|
|
222
|
+
test('sort edges', async () => {
|
|
223
|
+
const registry = Registry.make();
|
|
224
|
+
const builder = new GraphBuilder({ registry });
|
|
225
|
+
const nodes = Rx.make([
|
|
226
|
+
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
227
|
+
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
228
|
+
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
229
|
+
]);
|
|
316
230
|
builder.addExtension(
|
|
317
231
|
createExtension({
|
|
318
|
-
id: '
|
|
319
|
-
connector: () =>
|
|
320
|
-
actions: () => [{ id: 'action', data: () => {} }],
|
|
232
|
+
id: 'connector',
|
|
233
|
+
connector: () => Rx.make((get) => get(nodes)),
|
|
321
234
|
}),
|
|
322
235
|
);
|
|
323
236
|
const graph = builder.graph;
|
|
237
|
+
graph.expand(ROOT_ID);
|
|
238
|
+
|
|
239
|
+
{
|
|
240
|
+
const nodes = registry.get(graph.connections(ROOT_ID));
|
|
241
|
+
expect(nodes).has.length(3);
|
|
242
|
+
expect(nodes[0].id).to.equal(exampleId(1));
|
|
243
|
+
expect(nodes[1].id).to.equal(exampleId(2));
|
|
244
|
+
expect(nodes[2].id).to.equal(exampleId(3));
|
|
245
|
+
}
|
|
324
246
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
247
|
+
registry.set(nodes, [
|
|
248
|
+
{ id: exampleId(3), type: EXAMPLE_TYPE, data: 3 },
|
|
249
|
+
{ id: exampleId(1), type: EXAMPLE_TYPE, data: 1 },
|
|
250
|
+
{ id: exampleId(2), type: EXAMPLE_TYPE, data: 2 },
|
|
251
|
+
]);
|
|
330
252
|
|
|
331
|
-
|
|
253
|
+
// TODO(wittjosiah): Why is this needed for the following conditions to pass?
|
|
254
|
+
await sleep(0);
|
|
332
255
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
256
|
+
{
|
|
257
|
+
const nodes = registry.get(graph.connections(ROOT_ID));
|
|
258
|
+
expect(nodes).has.length(3);
|
|
259
|
+
expect(nodes[0].id).to.equal(exampleId(3));
|
|
260
|
+
expect(nodes[1].id).to.equal(exampleId(1));
|
|
261
|
+
expect(nodes[2].id).to.equal(exampleId(2));
|
|
262
|
+
}
|
|
338
263
|
});
|
|
339
264
|
|
|
340
|
-
test('
|
|
341
|
-
const
|
|
342
|
-
builder
|
|
265
|
+
test('updates are constrained', () => {
|
|
266
|
+
const registry = Registry.make();
|
|
267
|
+
const builder = new GraphBuilder({ registry });
|
|
268
|
+
const name = Rx.make('default');
|
|
269
|
+
const sub = Rx.make('default');
|
|
270
|
+
|
|
271
|
+
builder.addExtension([
|
|
343
272
|
createExtension({
|
|
344
|
-
id: '
|
|
345
|
-
|
|
346
|
-
|
|
273
|
+
id: 'root',
|
|
274
|
+
connector: (node) =>
|
|
275
|
+
Rx.make((get) =>
|
|
276
|
+
pipe(
|
|
277
|
+
get(node),
|
|
278
|
+
Option.flatMap((node) => (node.id === 'root' ? Option.some(get(name)) : Option.none())),
|
|
279
|
+
Option.filter((name) => name !== 'removed'),
|
|
280
|
+
Option.map((name) => [{ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: name }]),
|
|
281
|
+
Option.getOrElse(() => []),
|
|
282
|
+
),
|
|
283
|
+
),
|
|
347
284
|
}),
|
|
348
|
-
|
|
285
|
+
createExtension({
|
|
286
|
+
id: 'connector1',
|
|
287
|
+
connector: (node) =>
|
|
288
|
+
Rx.make((get) =>
|
|
289
|
+
pipe(
|
|
290
|
+
get(node),
|
|
291
|
+
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(get(sub)) : Option.none())),
|
|
292
|
+
Option.map((sub) => [{ id: exampleId(2), type: EXAMPLE_TYPE, data: sub }]),
|
|
293
|
+
Option.getOrElse(() => []),
|
|
294
|
+
),
|
|
295
|
+
),
|
|
296
|
+
}),
|
|
297
|
+
createExtension({
|
|
298
|
+
id: 'connector2',
|
|
299
|
+
connector: (node) =>
|
|
300
|
+
Rx.make((get) =>
|
|
301
|
+
pipe(
|
|
302
|
+
get(node),
|
|
303
|
+
Option.flatMap((node) => (node.id === EXAMPLE_ID ? Option.some(node.data) : Option.none())),
|
|
304
|
+
Option.map((data) => [{ id: exampleId(3), type: EXAMPLE_TYPE, data }]),
|
|
305
|
+
Option.getOrElse(() => []),
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
}),
|
|
309
|
+
]);
|
|
310
|
+
|
|
349
311
|
const graph = builder.graph;
|
|
350
|
-
await graph.expand(graph.root);
|
|
351
312
|
|
|
352
|
-
|
|
353
|
-
|
|
313
|
+
let parentCount = 0;
|
|
314
|
+
const parentCancel = registry.subscribe(graph.node(EXAMPLE_ID), (_) => {
|
|
315
|
+
parentCount++;
|
|
316
|
+
});
|
|
317
|
+
onTestFinished(() => parentCancel());
|
|
354
318
|
|
|
355
|
-
|
|
356
|
-
|
|
319
|
+
let independentCount = 0;
|
|
320
|
+
const independentCancel = registry.subscribe(graph.node(exampleId(2)), (_) => {
|
|
321
|
+
independentCount++;
|
|
322
|
+
});
|
|
323
|
+
onTestFinished(() => independentCancel());
|
|
324
|
+
|
|
325
|
+
let dependentCount = 0;
|
|
326
|
+
const dependentCancel = registry.subscribe(graph.node(exampleId(3)), (_) => {
|
|
327
|
+
dependentCount++;
|
|
328
|
+
});
|
|
329
|
+
onTestFinished(() => dependentCancel());
|
|
330
|
+
|
|
331
|
+
// Counts should not increment until the node is expanded.
|
|
332
|
+
graph.expand(ROOT_ID);
|
|
333
|
+
expect(parentCount).to.equal(1);
|
|
334
|
+
expect(independentCount).to.equal(0);
|
|
335
|
+
expect(dependentCount).to.equal(0);
|
|
336
|
+
|
|
337
|
+
// Counts should increment when the node is expanded.
|
|
338
|
+
graph.expand(EXAMPLE_ID);
|
|
339
|
+
expect(parentCount).to.equal(1);
|
|
340
|
+
expect(independentCount).to.equal(1);
|
|
341
|
+
expect(dependentCount).to.equal(1);
|
|
342
|
+
|
|
343
|
+
// Only dependent count should increment when the parent changes.
|
|
344
|
+
registry.set(name, 'updated');
|
|
345
|
+
expect(parentCount).to.equal(2);
|
|
346
|
+
expect(independentCount).to.equal(1);
|
|
347
|
+
expect(dependentCount).to.equal(2);
|
|
348
|
+
|
|
349
|
+
// Only independent count should increment when its state changes.
|
|
350
|
+
registry.set(sub, 'updated');
|
|
351
|
+
expect(parentCount).to.equal(2);
|
|
352
|
+
expect(independentCount).to.equal(2);
|
|
353
|
+
expect(dependentCount).to.equal(2);
|
|
354
|
+
|
|
355
|
+
// Independent count should update if its state changes even if the parent is removed.
|
|
356
|
+
Rx.batch(() => {
|
|
357
|
+
registry.set(name, 'removed');
|
|
358
|
+
registry.set(sub, 'batch');
|
|
359
|
+
});
|
|
360
|
+
expect(parentCount).to.equal(2);
|
|
361
|
+
expect(independentCount).to.equal(3);
|
|
362
|
+
expect(dependentCount).to.equal(2);
|
|
363
|
+
|
|
364
|
+
// Dependent count should increment when the node is added back.
|
|
365
|
+
registry.set(name, 'added');
|
|
366
|
+
expect(parentCount).to.equal(3);
|
|
367
|
+
expect(independentCount).to.equal(3);
|
|
368
|
+
expect(dependentCount).to.equal(3);
|
|
369
|
+
|
|
370
|
+
// Counts should not increment when the node is expanded again.
|
|
371
|
+
graph.expand(EXAMPLE_ID);
|
|
372
|
+
expect(parentCount).to.equal(3);
|
|
373
|
+
expect(independentCount).to.equal(3);
|
|
374
|
+
expect(dependentCount).to.equal(3);
|
|
357
375
|
});
|
|
358
376
|
|
|
359
|
-
test('
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
let count = 0;
|
|
363
|
-
let memoizedCount = 0;
|
|
377
|
+
test('eager graph expansion', async () => {
|
|
378
|
+
const registry = Registry.make();
|
|
379
|
+
const builder = new GraphBuilder({ registry });
|
|
364
380
|
builder.addExtension(
|
|
365
381
|
createExtension({
|
|
366
382
|
id: 'connector',
|
|
367
|
-
connector: () => {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
383
|
+
connector: (node) => {
|
|
384
|
+
return Rx.make((get) =>
|
|
385
|
+
pipe(
|
|
386
|
+
get(node),
|
|
387
|
+
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
388
|
+
Option.filter((data) => data <= 5),
|
|
389
|
+
Option.map((data) => [{ id: `node-${data}`, type: EXAMPLE_TYPE, data }]),
|
|
390
|
+
Option.getOrElse(() => []),
|
|
391
|
+
),
|
|
392
|
+
);
|
|
374
393
|
},
|
|
375
394
|
}),
|
|
376
395
|
);
|
|
377
|
-
const graph = builder.graph;
|
|
378
|
-
await graph.expand(graph.root);
|
|
379
396
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
397
|
+
let count = 0;
|
|
398
|
+
const trigger = new Trigger();
|
|
399
|
+
builder.graph.onNodeChanged.on(({ id }) => {
|
|
400
|
+
builder.graph.expand(id);
|
|
401
|
+
count++;
|
|
402
|
+
if (count === 5) {
|
|
403
|
+
trigger.wake();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
388
406
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
expect(
|
|
407
|
+
builder.graph.expand(ROOT_ID);
|
|
408
|
+
await trigger.wait();
|
|
409
|
+
expect(count).to.equal(5);
|
|
392
410
|
});
|
|
393
411
|
});
|
|
394
412
|
|
|
@@ -398,17 +416,21 @@ describe('GraphBuilder', () => {
|
|
|
398
416
|
builder.addExtension(
|
|
399
417
|
createExtension({
|
|
400
418
|
id: 'connector',
|
|
401
|
-
connector: (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
419
|
+
connector: (node) =>
|
|
420
|
+
Rx.make((get) =>
|
|
421
|
+
pipe(
|
|
422
|
+
get(node),
|
|
423
|
+
Option.map((node) => (node.data ? node.data + 1 : 1)),
|
|
424
|
+
Option.filter((data) => data <= 5),
|
|
425
|
+
Option.map((data) => [{ id: `node-${data}`, type: EXAMPLE_TYPE, data }]),
|
|
426
|
+
Option.getOrElse(() => []),
|
|
427
|
+
),
|
|
428
|
+
),
|
|
405
429
|
}),
|
|
406
430
|
);
|
|
407
|
-
const graph = builder.graph;
|
|
408
431
|
|
|
409
432
|
let count = 0;
|
|
410
433
|
await builder.explore({
|
|
411
|
-
node: graph.root,
|
|
412
434
|
visitor: () => {
|
|
413
435
|
count++;
|
|
414
436
|
},
|
|
@@ -417,43 +439,4 @@ describe('GraphBuilder', () => {
|
|
|
417
439
|
expect(count).to.equal(6);
|
|
418
440
|
});
|
|
419
441
|
});
|
|
420
|
-
|
|
421
|
-
describe('multiples', () => {
|
|
422
|
-
test('one of each with multiple memos', async () => {
|
|
423
|
-
const name = signal('default');
|
|
424
|
-
const builder = new GraphBuilder();
|
|
425
|
-
builder.addExtension(
|
|
426
|
-
createExtension({
|
|
427
|
-
id: 'extension',
|
|
428
|
-
resolver: () => {
|
|
429
|
-
const data = memoize(() => Math.random());
|
|
430
|
-
return { id: EXAMPLE_ID, type: EXAMPLE_TYPE, data, properties: { name: name.value } };
|
|
431
|
-
},
|
|
432
|
-
connector: () => {
|
|
433
|
-
const a = memoize(() => Math.random());
|
|
434
|
-
const b = memoize(() => Math.random());
|
|
435
|
-
const c = Math.random();
|
|
436
|
-
return [{ id: `${EXAMPLE_ID}-child`, type: EXAMPLE_TYPE, data: { a, b, c } }];
|
|
437
|
-
},
|
|
438
|
-
}),
|
|
439
|
-
);
|
|
440
|
-
const graph = builder.graph;
|
|
441
|
-
|
|
442
|
-
const one = await graph.waitForNode(EXAMPLE_ID);
|
|
443
|
-
const initialData = one!.data;
|
|
444
|
-
await graph.expand(one!);
|
|
445
|
-
const two = graph.nodes(one!)[0];
|
|
446
|
-
const initialA = two?.data.a;
|
|
447
|
-
const initialB = two?.data.b;
|
|
448
|
-
const initialC = two?.data.c;
|
|
449
|
-
|
|
450
|
-
name.value = 'updated';
|
|
451
|
-
|
|
452
|
-
expect(one?.properties.name).to.equal('updated');
|
|
453
|
-
expect(one?.data).to.equal(initialData);
|
|
454
|
-
expect(two?.data.a).to.equal(initialA);
|
|
455
|
-
expect(two?.data.b).to.equal(initialB);
|
|
456
|
-
expect(two?.data.c).not.to.equal(initialC);
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
442
|
});
|