@dxos/app-graph 0.8.4-main.f9ba587 → 0.8.4-main.fffef41
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 +249 -191
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +249 -191
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +29 -18
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +25 -21
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +1 -1
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts +8 -10
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +3 -3
- package/dist/types/src/testing.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -34
- package/src/graph-builder.test.ts +90 -32
- package/src/graph-builder.ts +109 -60
- package/src/graph.test.ts +4 -4
- package/src/graph.ts +130 -89
- package/src/node.ts +5 -3
- package/src/signals-integration.test.ts +29 -28
- package/src/stories/EchoGraph.stories.tsx +49 -39
- package/src/stories/Tree.tsx +1 -1
- package/src/testing.ts +4 -4
package/src/graph.ts
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { Atom, Registry } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Function from 'effect/Function';
|
|
7
|
+
import * as Option from 'effect/Option';
|
|
8
|
+
import * as Record from 'effect/Record';
|
|
7
9
|
|
|
8
10
|
import { Event, Trigger } from '@dxos/async';
|
|
9
11
|
import { todo } from '@dxos/debug';
|
|
10
12
|
import { invariant } from '@dxos/invariant';
|
|
11
13
|
import { log } from '@dxos/log';
|
|
12
|
-
import {
|
|
14
|
+
import { type MakeOptional, isNonNullable } from '@dxos/util';
|
|
13
15
|
|
|
14
|
-
import { type
|
|
16
|
+
import { type Action, type ActionGroup, type Node, type NodeArg, type Relation } from './node';
|
|
15
17
|
|
|
16
18
|
const graphSymbol = Symbol('graph');
|
|
17
|
-
type DeepWriteable<T> = {
|
|
19
|
+
type DeepWriteable<T> = {
|
|
20
|
+
-readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K];
|
|
21
|
+
};
|
|
18
22
|
type NodeInternal = DeepWriteable<Node> & { [graphSymbol]: Graph };
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -59,8 +63,7 @@ export type GraphParams = {
|
|
|
59
63
|
nodes?: MakeOptional<Node, 'data' | 'cacheable'>[];
|
|
60
64
|
edges?: Record<string, Edges>;
|
|
61
65
|
onExpand?: Graph['_onExpand'];
|
|
62
|
-
|
|
63
|
-
// onInitialize?: Graph['_onInitialize'];
|
|
66
|
+
onInitialize?: Graph['_onInitialize'];
|
|
64
67
|
onRemoveNode?: Graph['_onRemoveNode'];
|
|
65
68
|
};
|
|
66
69
|
|
|
@@ -78,32 +81,32 @@ export interface ReadableGraph {
|
|
|
78
81
|
*/
|
|
79
82
|
toJSON(id?: string): object;
|
|
80
83
|
|
|
81
|
-
json(id?: string):
|
|
84
|
+
json(id?: string): Atom.Atom<any>;
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Get the
|
|
87
|
+
* Get the atom key for the node with the given id.
|
|
85
88
|
*/
|
|
86
|
-
node(id: string):
|
|
89
|
+
node(id: string): Atom.Atom<Option.Option<Node>>;
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
|
-
* Get the
|
|
92
|
+
* Get the atom key for the node with the given id.
|
|
90
93
|
*/
|
|
91
|
-
nodeOrThrow(id: string):
|
|
94
|
+
nodeOrThrow(id: string): Atom.Atom<Node>;
|
|
92
95
|
|
|
93
96
|
/**
|
|
94
|
-
* Get the
|
|
97
|
+
* Get the atom key for the connections of the node with the given id.
|
|
95
98
|
*/
|
|
96
|
-
connections(id: string, relation?: Relation):
|
|
99
|
+
connections(id: string, relation?: Relation): Atom.Atom<Node[]>;
|
|
97
100
|
|
|
98
101
|
/**
|
|
99
|
-
* Get the
|
|
102
|
+
* Get the atom key for the actions of the node with the given id.
|
|
100
103
|
*/
|
|
101
|
-
actions(id: string):
|
|
104
|
+
actions(id: string): Atom.Atom<(Action | ActionGroup)[]>;
|
|
102
105
|
|
|
103
106
|
/**
|
|
104
|
-
* Get the
|
|
107
|
+
* Get the atom key for the edges of the node with the given id.
|
|
105
108
|
*/
|
|
106
|
-
edges(id: string):
|
|
109
|
+
edges(id: string): Atom.Atom<Edges>;
|
|
107
110
|
|
|
108
111
|
/**
|
|
109
112
|
* Alias for `getNodeOrThrow(ROOT_ID)`.
|
|
@@ -166,7 +169,7 @@ export interface ExpandableGraph extends ReadableGraph {
|
|
|
166
169
|
*
|
|
167
170
|
* Fires the `onInitialize` callback to provide initial data for a node.
|
|
168
171
|
*/
|
|
169
|
-
|
|
172
|
+
initialize(id: string): Promise<void>;
|
|
170
173
|
|
|
171
174
|
/**
|
|
172
175
|
* Expand a node in the graph.
|
|
@@ -227,10 +230,13 @@ export interface WritableGraph extends ExpandableGraph {
|
|
|
227
230
|
* The Graph represents the user interface information architecture of the application constructed via plugins.
|
|
228
231
|
*/
|
|
229
232
|
export class Graph implements WritableGraph {
|
|
230
|
-
readonly onNodeChanged = new Event<{
|
|
233
|
+
readonly onNodeChanged = new Event<{
|
|
234
|
+
id: string;
|
|
235
|
+
node: Option.Option<Node>;
|
|
236
|
+
}>();
|
|
231
237
|
|
|
232
238
|
private readonly _onExpand?: (id: string, relation: Relation) => void;
|
|
233
|
-
|
|
239
|
+
private readonly _onInitialize?: (id: string) => Promise<void>;
|
|
234
240
|
private readonly _onRemoveNode?: (id: string) => void;
|
|
235
241
|
|
|
236
242
|
private readonly _registry: Registry.Registry;
|
|
@@ -238,55 +244,63 @@ export class Graph implements WritableGraph {
|
|
|
238
244
|
private readonly _initialized = Record.empty<string, boolean>();
|
|
239
245
|
private readonly _initialEdges = Record.empty<string, Edges>();
|
|
240
246
|
private readonly _initialNodes = Record.fromEntries([
|
|
241
|
-
[
|
|
247
|
+
[
|
|
248
|
+
ROOT_ID,
|
|
249
|
+
this._constructNode({
|
|
250
|
+
id: ROOT_ID,
|
|
251
|
+
type: ROOT_TYPE,
|
|
252
|
+
data: null,
|
|
253
|
+
properties: {},
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
242
256
|
]);
|
|
243
257
|
|
|
244
258
|
/** @internal */
|
|
245
|
-
readonly _node =
|
|
259
|
+
readonly _node = Atom.family<string, Atom.Writable<Option.Option<Node>>>((id) => {
|
|
246
260
|
const initial = Option.flatten(Record.get(this._initialNodes, id));
|
|
247
|
-
return
|
|
261
|
+
return Atom.make<Option.Option<Node>>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
|
|
248
262
|
});
|
|
249
263
|
|
|
250
|
-
private readonly _nodeOrThrow =
|
|
251
|
-
return
|
|
264
|
+
private readonly _nodeOrThrow = Atom.family<string, Atom.Atom<Node>>((id) => {
|
|
265
|
+
return Atom.make((get) => {
|
|
252
266
|
const node = get(this._node(id));
|
|
253
267
|
invariant(Option.isSome(node), `Node not available: ${id}`);
|
|
254
268
|
return node.value;
|
|
255
269
|
});
|
|
256
270
|
});
|
|
257
271
|
|
|
258
|
-
private readonly _edges =
|
|
272
|
+
private readonly _edges = Atom.family<string, Atom.Writable<Edges>>((id) => {
|
|
259
273
|
const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({ inbound: [], outbound: [] })));
|
|
260
|
-
return
|
|
274
|
+
return Atom.make<Edges>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
|
|
261
275
|
});
|
|
262
276
|
|
|
263
|
-
// NOTE: Currently the argument to the family needs to be referentially stable for the
|
|
264
|
-
// TODO(wittjosiah):
|
|
265
|
-
private readonly _connections =
|
|
266
|
-
return
|
|
277
|
+
// NOTE: Currently the argument to the family needs to be referentially stable for the atom to be referentially stable.
|
|
278
|
+
// TODO(wittjosiah): Atom feature request, support for something akin to `ComplexMap` to allow for complex arguments.
|
|
279
|
+
private readonly _connections = Atom.family<string, Atom.Atom<Node[]>>((key) => {
|
|
280
|
+
return Atom.make((get) => {
|
|
267
281
|
const [id, relation] = key.split('$');
|
|
268
282
|
const edges = get(this._edges(id));
|
|
269
283
|
return edges[relation as Relation]
|
|
270
284
|
.map((id) => get(this._node(id)))
|
|
271
285
|
.filter(Option.isSome)
|
|
272
286
|
.map((o) => o.value);
|
|
273
|
-
}).pipe(
|
|
287
|
+
}).pipe(Atom.withLabel(`graph:connections:${key}`));
|
|
274
288
|
});
|
|
275
289
|
|
|
276
|
-
private readonly _actions =
|
|
277
|
-
return
|
|
290
|
+
private readonly _actions = Atom.family<string, Atom.Atom<(Action | ActionGroup)[]>>((id) => {
|
|
291
|
+
return Atom.make((get) => {
|
|
278
292
|
return get(this._connections(`${id}$outbound`)).filter(
|
|
279
293
|
(node) => node.type === ACTION_TYPE || node.type === ACTION_GROUP_TYPE,
|
|
280
294
|
);
|
|
281
|
-
}).pipe(
|
|
295
|
+
}).pipe(Atom.withLabel(`graph:actions:${id}`));
|
|
282
296
|
});
|
|
283
297
|
|
|
284
|
-
private readonly _json =
|
|
285
|
-
return
|
|
298
|
+
private readonly _json = Atom.family<string, Atom.Atom<any>>((id) => {
|
|
299
|
+
return Atom.make((get) => {
|
|
286
300
|
const toJSON = (node: Node, seen: string[] = []): any => {
|
|
287
301
|
const nodes = get(this.connections(node.id));
|
|
288
302
|
const obj: Record<string, any> = {
|
|
289
|
-
id: node.id
|
|
303
|
+
id: node.id,
|
|
290
304
|
type: node.type,
|
|
291
305
|
};
|
|
292
306
|
if (node.properties.label) {
|
|
@@ -306,11 +320,12 @@ export class Graph implements WritableGraph {
|
|
|
306
320
|
|
|
307
321
|
const root = get(this.nodeOrThrow(id));
|
|
308
322
|
return toJSON(root);
|
|
309
|
-
}).pipe(
|
|
323
|
+
}).pipe(Atom.withLabel(`graph:json:${id}`));
|
|
310
324
|
});
|
|
311
325
|
|
|
312
|
-
constructor({ registry, nodes, edges, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
326
|
+
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
313
327
|
this._registry = registry ?? Registry.make();
|
|
328
|
+
this._onInitialize = onInitialize;
|
|
314
329
|
this._onExpand = onExpand;
|
|
315
330
|
this._onRemoveNode = onRemoveNode;
|
|
316
331
|
|
|
@@ -335,15 +350,15 @@ export class Graph implements WritableGraph {
|
|
|
335
350
|
return this._json(id);
|
|
336
351
|
}
|
|
337
352
|
|
|
338
|
-
node(id: string):
|
|
353
|
+
node(id: string): Atom.Atom<Option.Option<Node>> {
|
|
339
354
|
return this._node(id);
|
|
340
355
|
}
|
|
341
356
|
|
|
342
|
-
nodeOrThrow(id: string):
|
|
357
|
+
nodeOrThrow(id: string): Atom.Atom<Node> {
|
|
343
358
|
return this._nodeOrThrow(id);
|
|
344
359
|
}
|
|
345
360
|
|
|
346
|
-
connections(id: string, relation: Relation = 'outbound'):
|
|
361
|
+
connections(id: string, relation: Relation = 'outbound'): Atom.Atom<Node[]> {
|
|
347
362
|
return this._connections(`${id}$${relation}`);
|
|
348
363
|
}
|
|
349
364
|
|
|
@@ -351,7 +366,7 @@ export class Graph implements WritableGraph {
|
|
|
351
366
|
return this._actions(id);
|
|
352
367
|
}
|
|
353
368
|
|
|
354
|
-
edges(id: string):
|
|
369
|
+
edges(id: string): Atom.Atom<Edges> {
|
|
355
370
|
return this._edges(id);
|
|
356
371
|
}
|
|
357
372
|
|
|
@@ -379,15 +394,14 @@ export class Graph implements WritableGraph {
|
|
|
379
394
|
return this._registry.get(this.edges(id));
|
|
380
395
|
}
|
|
381
396
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// }
|
|
397
|
+
async initialize(id: string) {
|
|
398
|
+
const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
|
|
399
|
+
log('initialize', { id, initialized });
|
|
400
|
+
if (!initialized) {
|
|
401
|
+
await this._onInitialize?.(id);
|
|
402
|
+
Record.set(this._initialized, id, true);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
391
405
|
|
|
392
406
|
expand(id: string, relation: Relation = 'outbound'): void {
|
|
393
407
|
const key = `${id}$${relation}`;
|
|
@@ -400,38 +414,48 @@ export class Graph implements WritableGraph {
|
|
|
400
414
|
}
|
|
401
415
|
|
|
402
416
|
addNodes(nodes: NodeArg<any, Record<string, any>>[]): void {
|
|
403
|
-
|
|
417
|
+
Atom.batch(() => {
|
|
404
418
|
nodes.map((node) => this.addNode(node));
|
|
405
419
|
});
|
|
406
420
|
}
|
|
407
421
|
|
|
408
422
|
addNode({ nodes, edges, ...nodeArg }: NodeArg<any, Record<string, any>>): void {
|
|
409
423
|
const { id, type, data = null, properties = {} } = nodeArg;
|
|
410
|
-
const
|
|
411
|
-
const node = this._registry.get(
|
|
424
|
+
const nodeAtom = this._node(id);
|
|
425
|
+
const node = this._registry.get(nodeAtom);
|
|
412
426
|
Option.match(node, {
|
|
413
427
|
onSome: (node) => {
|
|
414
428
|
const typeChanged = node.type !== type;
|
|
415
429
|
const dataChanged = node.data !== data;
|
|
416
430
|
const propertiesChanged = Object.keys(properties).some((key) => node.properties[key] !== properties[key]);
|
|
417
|
-
log('existing node', {
|
|
431
|
+
log('existing node', {
|
|
432
|
+
id,
|
|
433
|
+
typeChanged,
|
|
434
|
+
dataChanged,
|
|
435
|
+
propertiesChanged,
|
|
436
|
+
});
|
|
418
437
|
if (typeChanged || dataChanged || propertiesChanged) {
|
|
419
438
|
log('updating node', { id, type, data, properties });
|
|
420
|
-
const newNode = Option.some({
|
|
421
|
-
|
|
439
|
+
const newNode = Option.some({
|
|
440
|
+
...node,
|
|
441
|
+
type,
|
|
442
|
+
data,
|
|
443
|
+
properties: { ...node.properties, ...properties },
|
|
444
|
+
});
|
|
445
|
+
this._registry.set(nodeAtom, newNode);
|
|
422
446
|
this.onNodeChanged.emit({ id, node: newNode });
|
|
423
447
|
}
|
|
424
448
|
},
|
|
425
449
|
onNone: () => {
|
|
426
450
|
log('new node', { id, type, data, properties });
|
|
427
451
|
const newNode = this._constructNode({ id, type, data, properties });
|
|
428
|
-
this._registry.set(
|
|
452
|
+
this._registry.set(nodeAtom, newNode);
|
|
429
453
|
this.onNodeChanged.emit({ id, node: newNode });
|
|
430
454
|
},
|
|
431
455
|
});
|
|
432
456
|
|
|
433
457
|
if (nodes) {
|
|
434
|
-
//
|
|
458
|
+
// Atom.batch(() => {
|
|
435
459
|
this.addNodes(nodes);
|
|
436
460
|
const _edges = nodes.map((node) => ({ source: id, target: node.id }));
|
|
437
461
|
this.addEdges(_edges);
|
|
@@ -444,15 +468,15 @@ export class Graph implements WritableGraph {
|
|
|
444
468
|
}
|
|
445
469
|
|
|
446
470
|
removeNodes(ids: string[], edges = false): void {
|
|
447
|
-
|
|
471
|
+
Atom.batch(() => {
|
|
448
472
|
ids.map((id) => this.removeNode(id, edges));
|
|
449
473
|
});
|
|
450
474
|
}
|
|
451
475
|
|
|
452
476
|
removeNode(id: string, edges = false): void {
|
|
453
|
-
const
|
|
454
|
-
// TODO(wittjosiah): Is there a way to mark these
|
|
455
|
-
this._registry.set(
|
|
477
|
+
const nodeAtom = this._node(id);
|
|
478
|
+
// TODO(wittjosiah): Is there a way to mark these atom values for garbage collection?
|
|
479
|
+
this._registry.set(nodeAtom, Option.none());
|
|
456
480
|
this.onNodeChanged.emit({ id, node: Option.none() });
|
|
457
481
|
// TODO(wittjosiah): Reset expanded and initialized flags?
|
|
458
482
|
|
|
@@ -469,55 +493,67 @@ export class Graph implements WritableGraph {
|
|
|
469
493
|
}
|
|
470
494
|
|
|
471
495
|
addEdges(edges: Edge[]): void {
|
|
472
|
-
|
|
496
|
+
Atom.batch(() => {
|
|
473
497
|
edges.map((edge) => this.addEdge(edge));
|
|
474
498
|
});
|
|
475
499
|
}
|
|
476
500
|
|
|
477
501
|
addEdge(edgeArg: Edge): void {
|
|
478
|
-
const
|
|
479
|
-
const source = this._registry.get(
|
|
502
|
+
const sourceAtom = this._edges(edgeArg.source);
|
|
503
|
+
const source = this._registry.get(sourceAtom);
|
|
480
504
|
if (!source.outbound.includes(edgeArg.target)) {
|
|
481
|
-
log('add outbound edge', {
|
|
482
|
-
|
|
505
|
+
log('add outbound edge', {
|
|
506
|
+
source: edgeArg.source,
|
|
507
|
+
target: edgeArg.target,
|
|
508
|
+
});
|
|
509
|
+
this._registry.set(sourceAtom, {
|
|
510
|
+
inbound: source.inbound,
|
|
511
|
+
outbound: [...source.outbound, edgeArg.target],
|
|
512
|
+
});
|
|
483
513
|
}
|
|
484
514
|
|
|
485
|
-
const
|
|
486
|
-
const target = this._registry.get(
|
|
515
|
+
const targetAtom = this._edges(edgeArg.target);
|
|
516
|
+
const target = this._registry.get(targetAtom);
|
|
487
517
|
if (!target.inbound.includes(edgeArg.source)) {
|
|
488
|
-
log('add inbound edge', {
|
|
489
|
-
|
|
518
|
+
log('add inbound edge', {
|
|
519
|
+
source: edgeArg.source,
|
|
520
|
+
target: edgeArg.target,
|
|
521
|
+
});
|
|
522
|
+
this._registry.set(targetAtom, {
|
|
523
|
+
inbound: [...target.inbound, edgeArg.source],
|
|
524
|
+
outbound: target.outbound,
|
|
525
|
+
});
|
|
490
526
|
}
|
|
491
527
|
}
|
|
492
528
|
|
|
493
529
|
removeEdges(edges: Edge[], removeOrphans = false): void {
|
|
494
|
-
|
|
530
|
+
Atom.batch(() => {
|
|
495
531
|
edges.map((edge) => this.removeEdge(edge, removeOrphans));
|
|
496
532
|
});
|
|
497
533
|
}
|
|
498
534
|
|
|
499
535
|
removeEdge(edgeArg: Edge, removeOrphans = false): void {
|
|
500
|
-
const
|
|
501
|
-
const source = this._registry.get(
|
|
536
|
+
const sourceAtom = this._edges(edgeArg.source);
|
|
537
|
+
const source = this._registry.get(sourceAtom);
|
|
502
538
|
if (source.outbound.includes(edgeArg.target)) {
|
|
503
|
-
this._registry.set(
|
|
539
|
+
this._registry.set(sourceAtom, {
|
|
504
540
|
inbound: source.inbound,
|
|
505
541
|
outbound: source.outbound.filter((id) => id !== edgeArg.target),
|
|
506
542
|
});
|
|
507
543
|
}
|
|
508
544
|
|
|
509
|
-
const
|
|
510
|
-
const target = this._registry.get(
|
|
545
|
+
const targetAtom = this._edges(edgeArg.target);
|
|
546
|
+
const target = this._registry.get(targetAtom);
|
|
511
547
|
if (target.inbound.includes(edgeArg.source)) {
|
|
512
|
-
this._registry.set(
|
|
548
|
+
this._registry.set(targetAtom, {
|
|
513
549
|
inbound: target.inbound.filter((id) => id !== edgeArg.source),
|
|
514
550
|
outbound: target.outbound,
|
|
515
551
|
});
|
|
516
552
|
}
|
|
517
553
|
|
|
518
554
|
if (removeOrphans) {
|
|
519
|
-
const source = this._registry.get(
|
|
520
|
-
const target = this._registry.get(
|
|
555
|
+
const source = this._registry.get(sourceAtom);
|
|
556
|
+
const target = this._registry.get(targetAtom);
|
|
521
557
|
if (source.outbound.length === 0 && source.inbound.length === 0 && edgeArg.source !== ROOT_ID) {
|
|
522
558
|
this.removeNodes([edgeArg.source]);
|
|
523
559
|
}
|
|
@@ -528,12 +564,12 @@ export class Graph implements WritableGraph {
|
|
|
528
564
|
}
|
|
529
565
|
|
|
530
566
|
sortEdges(id: string, relation: Relation, order: string[]): void {
|
|
531
|
-
const
|
|
532
|
-
const edges = this._registry.get(
|
|
567
|
+
const edgesAtom = this._edges(id);
|
|
568
|
+
const edges = this._registry.get(edgesAtom);
|
|
533
569
|
const unsorted = edges[relation].filter((id) => !order.includes(id)) ?? [];
|
|
534
570
|
const sorted = order.filter((id) => edges[relation].includes(id)) ?? [];
|
|
535
571
|
edges[relation].splice(0, edges[relation].length, ...[...sorted, ...unsorted]);
|
|
536
|
-
this._registry.set(
|
|
572
|
+
this._registry.set(edgesAtom, edges);
|
|
537
573
|
}
|
|
538
574
|
|
|
539
575
|
traverse({ visitor, source = ROOT_ID, relation = 'outbound' }: GraphTraversalOptions, path: string[] = []): void {
|
|
@@ -554,7 +590,7 @@ export class Graph implements WritableGraph {
|
|
|
554
590
|
}
|
|
555
591
|
|
|
556
592
|
getPath({ source = 'root', target }: { source?: string; target: string }): Option.Option<string[]> {
|
|
557
|
-
return pipe(
|
|
593
|
+
return Function.pipe(
|
|
558
594
|
this.getNode(source),
|
|
559
595
|
Option.flatMap((node) => {
|
|
560
596
|
let found: Option.Option<string[]> = Option.none();
|
|
@@ -598,6 +634,11 @@ export class Graph implements WritableGraph {
|
|
|
598
634
|
|
|
599
635
|
/** @internal */
|
|
600
636
|
_constructNode(node: NodeArg<any>): Option.Option<Node> {
|
|
601
|
-
return Option.some({
|
|
637
|
+
return Option.some({
|
|
638
|
+
[graphSymbol]: this,
|
|
639
|
+
data: null,
|
|
640
|
+
properties: {},
|
|
641
|
+
...node,
|
|
642
|
+
});
|
|
602
643
|
}
|
|
603
644
|
}
|
package/src/node.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type MakeOptional, type MaybePromise } from '@dxos/util';
|
|
6
|
+
|
|
7
|
+
import { ACTION_GROUP_TYPE, ACTION_TYPE } from './graph';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Represents a node in the graph.
|
|
@@ -84,7 +86,7 @@ export type Action<TProperties extends Record<string, any> = Record<string, any>
|
|
|
84
86
|
>;
|
|
85
87
|
|
|
86
88
|
export const isAction = (data: unknown): data is Action =>
|
|
87
|
-
isGraphNode(data) ? typeof data.data === 'function' : false;
|
|
89
|
+
isGraphNode(data) ? typeof data.data === 'function' && data.type === ACTION_TYPE : false;
|
|
88
90
|
|
|
89
91
|
export const actionGroupSymbol = Symbol('ActionGroup');
|
|
90
92
|
|
|
@@ -95,7 +97,7 @@ export type ActionGroup<TProperties extends Record<string, any> = Record<string,
|
|
|
95
97
|
>;
|
|
96
98
|
|
|
97
99
|
export const isActionGroup = (data: unknown): data is ActionGroup =>
|
|
98
|
-
isGraphNode(data) ? data.data === actionGroupSymbol : false;
|
|
100
|
+
isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ACTION_GROUP_TYPE : false;
|
|
99
101
|
|
|
100
102
|
export type ActionLike = Action | ActionGroup;
|
|
101
103
|
|
|
@@ -2,37 +2,37 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom, Registry } from '@effect-atom/atom-react';
|
|
6
6
|
import { signal } from '@preact/signals-core';
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { Trigger } from '@dxos/async';
|
|
10
|
+
import { Obj, Type } from '@dxos/echo';
|
|
11
|
+
import { Ref } from '@dxos/echo/internal';
|
|
10
12
|
import { Filter } from '@dxos/echo-db';
|
|
11
13
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
|
-
import { Expando, Ref } from '@dxos/echo-schema';
|
|
13
14
|
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
14
|
-
import { live } from '@dxos/live-object';
|
|
15
15
|
|
|
16
16
|
import { ROOT_ID } from './graph';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { GraphBuilder, atomFromSignal, createExtension } from './graph-builder';
|
|
18
|
+
import { atomFromQuery } from './testing';
|
|
19
19
|
|
|
20
20
|
registerSignalsRuntime();
|
|
21
21
|
|
|
22
22
|
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
23
23
|
|
|
24
24
|
describe('signals integration', () => {
|
|
25
|
-
test('creating
|
|
25
|
+
test('creating atom from signal', () => {
|
|
26
26
|
const registry = Registry.make();
|
|
27
27
|
const state = signal<number>(0);
|
|
28
|
-
const value =
|
|
29
|
-
const inline =
|
|
30
|
-
// NOTE: This will create a new
|
|
31
|
-
// This test is verifying that this behaves the same as using a stable
|
|
28
|
+
const value = atomFromSignal(() => state.value);
|
|
29
|
+
const inline = Atom.make((get) => {
|
|
30
|
+
// NOTE: This will create a new atom instance each time.
|
|
31
|
+
// This test is verifying that this behaves the same as using a stable atom instance.
|
|
32
32
|
// The parent will remain subscribed to one instance until the new one is created.
|
|
33
33
|
// The old one will then be garbage collected because it is no longer referenced.
|
|
34
|
-
const
|
|
35
|
-
return get(
|
|
34
|
+
const atom = atomFromSignal(() => get(value));
|
|
35
|
+
return get(atom);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
let count = 0;
|
|
@@ -72,7 +72,7 @@ describe('signals integration', () => {
|
|
|
72
72
|
await dbBuilder.close();
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('
|
|
75
|
+
test('atom references are loaded lazily and receive signal notifications', async () => {
|
|
76
76
|
const registry = Registry.make();
|
|
77
77
|
await using peer = await dbBuilder.createPeer();
|
|
78
78
|
|
|
@@ -89,23 +89,24 @@ describe('signals integration', () => {
|
|
|
89
89
|
{
|
|
90
90
|
await using db = await peer.openLastDatabase();
|
|
91
91
|
const outer = (await db.query(Filter.ids(outerId)).first()) as any;
|
|
92
|
-
const
|
|
93
|
-
|
|
92
|
+
const innerAtom = atomFromSignal(() => outer.inner.target);
|
|
94
93
|
const loaded = new Trigger();
|
|
94
|
+
|
|
95
95
|
let count = 0;
|
|
96
|
-
const cancel = registry.subscribe(
|
|
96
|
+
const cancel = registry.subscribe(innerAtom, (inner) => {
|
|
97
97
|
count++;
|
|
98
98
|
if (inner) {
|
|
99
99
|
loaded.wake();
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
|
+
|
|
102
103
|
onTestFinished(() => cancel());
|
|
103
104
|
|
|
104
|
-
expect(registry.get(
|
|
105
|
+
expect(registry.get(innerAtom)).to.eq(undefined);
|
|
105
106
|
expect(count).to.eq(1);
|
|
106
107
|
|
|
107
108
|
await loaded.wait();
|
|
108
|
-
expect(registry.get(
|
|
109
|
+
expect(registry.get(innerAtom)).to.include({ name: 'inner' });
|
|
109
110
|
expect(count).to.eq(2);
|
|
110
111
|
}
|
|
111
112
|
});
|
|
@@ -129,8 +130,8 @@ describe('signals integration', () => {
|
|
|
129
130
|
{
|
|
130
131
|
await using db = await peer.openLastDatabase();
|
|
131
132
|
const outer = (await db.query(Filter.ids(outerId)).first()) as any;
|
|
132
|
-
const
|
|
133
|
-
const inner = registry.get(
|
|
133
|
+
const innerAtom = atomFromSignal(() => outer.inner.target);
|
|
134
|
+
const inner = registry.get(innerAtom);
|
|
134
135
|
expect(inner).to.eq(undefined);
|
|
135
136
|
|
|
136
137
|
const builder = new GraphBuilder({ registry });
|
|
@@ -138,8 +139,8 @@ describe('signals integration', () => {
|
|
|
138
139
|
createExtension({
|
|
139
140
|
id: 'outbound-connector',
|
|
140
141
|
connector: () =>
|
|
141
|
-
|
|
142
|
-
const inner = get(
|
|
142
|
+
Atom.make((get) => {
|
|
143
|
+
const inner = get(innerAtom) as any;
|
|
143
144
|
return inner ? [{ id: inner.id, type: EXAMPLE_TYPE, data: inner.name }] : [];
|
|
144
145
|
}),
|
|
145
146
|
}),
|
|
@@ -174,18 +175,18 @@ describe('signals integration', () => {
|
|
|
174
175
|
const registry = Registry.make();
|
|
175
176
|
await using peer = await dbBuilder.createPeer();
|
|
176
177
|
await using db = await peer.createDatabase();
|
|
177
|
-
db.add(
|
|
178
|
-
db.add(
|
|
178
|
+
db.add(Obj.make(Type.Expando, { name: 'a' }));
|
|
179
|
+
db.add(Obj.make(Type.Expando, { name: 'b' }));
|
|
179
180
|
|
|
180
181
|
const builder = new GraphBuilder({ registry });
|
|
181
182
|
builder.addExtension(
|
|
182
183
|
createExtension({
|
|
183
184
|
id: 'expando',
|
|
184
185
|
connector: () => {
|
|
185
|
-
const query = db.query(Filter.type(Expando));
|
|
186
|
+
const query = db.query(Filter.type(Type.Expando));
|
|
186
187
|
|
|
187
|
-
return
|
|
188
|
-
const objects = get(
|
|
188
|
+
return Atom.make((get) => {
|
|
189
|
+
const objects = get(atomFromQuery(query));
|
|
189
190
|
return objects.map((object) => ({ id: object.id, type: EXAMPLE_TYPE, data: object.name }));
|
|
190
191
|
});
|
|
191
192
|
},
|
|
@@ -205,7 +206,7 @@ describe('signals integration', () => {
|
|
|
205
206
|
graph.expand(ROOT_ID);
|
|
206
207
|
expect(count).to.eq(2);
|
|
207
208
|
|
|
208
|
-
const object = db.add(
|
|
209
|
+
const object = db.add(Obj.make(Type.Expando, { name: 'c' }));
|
|
209
210
|
await db.flush();
|
|
210
211
|
expect(count).to.eq(3);
|
|
211
212
|
|