@dxos/app-graph 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446

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