@dxos/app-graph 0.8.4-main.dedc0f3 → 0.8.4-main.e8ec1fe
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 +216 -233
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +216 -233
- 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 +16 -16
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +19 -19
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts +0 -1
- 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 +31 -32
- package/src/graph-builder.test.ts +34 -33
- package/src/graph-builder.ts +50 -45
- package/src/graph.test.ts +3 -3
- package/src/graph.ts +115 -73
- package/src/signals-integration.test.ts +29 -28
- package/src/stories/EchoGraph.stories.tsx +28 -22
- package/src/stories/Tree.tsx +1 -1
- package/src/testing.ts +4 -4
package/src/graph.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
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';
|
|
@@ -14,7 +16,9 @@ import { type MakeOptional, isNonNullable } from '@dxos/util';
|
|
|
14
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
|
/**
|
|
@@ -77,32 +81,32 @@ export interface ReadableGraph {
|
|
|
77
81
|
*/
|
|
78
82
|
toJSON(id?: string): object;
|
|
79
83
|
|
|
80
|
-
json(id?: string):
|
|
84
|
+
json(id?: string): Atom.Atom<any>;
|
|
81
85
|
|
|
82
86
|
/**
|
|
83
|
-
* Get the
|
|
87
|
+
* Get the atom key for the node with the given id.
|
|
84
88
|
*/
|
|
85
|
-
node(id: string):
|
|
89
|
+
node(id: string): Atom.Atom<Option.Option<Node>>;
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
|
-
* Get the
|
|
92
|
+
* Get the atom key for the node with the given id.
|
|
89
93
|
*/
|
|
90
|
-
nodeOrThrow(id: string):
|
|
94
|
+
nodeOrThrow(id: string): Atom.Atom<Node>;
|
|
91
95
|
|
|
92
96
|
/**
|
|
93
|
-
* Get the
|
|
97
|
+
* Get the atom key for the connections of the node with the given id.
|
|
94
98
|
*/
|
|
95
|
-
connections(id: string, relation?: Relation):
|
|
99
|
+
connections(id: string, relation?: Relation): Atom.Atom<Node[]>;
|
|
96
100
|
|
|
97
101
|
/**
|
|
98
|
-
* Get the
|
|
102
|
+
* Get the atom key for the actions of the node with the given id.
|
|
99
103
|
*/
|
|
100
|
-
actions(id: string):
|
|
104
|
+
actions(id: string): Atom.Atom<(Action | ActionGroup)[]>;
|
|
101
105
|
|
|
102
106
|
/**
|
|
103
|
-
* Get the
|
|
107
|
+
* Get the atom key for the edges of the node with the given id.
|
|
104
108
|
*/
|
|
105
|
-
edges(id: string):
|
|
109
|
+
edges(id: string): Atom.Atom<Edges>;
|
|
106
110
|
|
|
107
111
|
/**
|
|
108
112
|
* Alias for `getNodeOrThrow(ROOT_ID)`.
|
|
@@ -226,7 +230,10 @@ export interface WritableGraph extends ExpandableGraph {
|
|
|
226
230
|
* The Graph represents the user interface information architecture of the application constructed via plugins.
|
|
227
231
|
*/
|
|
228
232
|
export class Graph implements WritableGraph {
|
|
229
|
-
readonly onNodeChanged = new Event<{
|
|
233
|
+
readonly onNodeChanged = new Event<{
|
|
234
|
+
id: string;
|
|
235
|
+
node: Option.Option<Node>;
|
|
236
|
+
}>();
|
|
230
237
|
|
|
231
238
|
private readonly _onExpand?: (id: string, relation: Relation) => void;
|
|
232
239
|
private readonly _onInitialize?: (id: string) => Promise<void>;
|
|
@@ -237,55 +244,63 @@ export class Graph implements WritableGraph {
|
|
|
237
244
|
private readonly _initialized = Record.empty<string, boolean>();
|
|
238
245
|
private readonly _initialEdges = Record.empty<string, Edges>();
|
|
239
246
|
private readonly _initialNodes = Record.fromEntries([
|
|
240
|
-
[
|
|
247
|
+
[
|
|
248
|
+
ROOT_ID,
|
|
249
|
+
this._constructNode({
|
|
250
|
+
id: ROOT_ID,
|
|
251
|
+
type: ROOT_TYPE,
|
|
252
|
+
data: null,
|
|
253
|
+
properties: {},
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
241
256
|
]);
|
|
242
257
|
|
|
243
258
|
/** @internal */
|
|
244
|
-
readonly _node =
|
|
259
|
+
readonly _node = Atom.family<string, Atom.Writable<Option.Option<Node>>>((id) => {
|
|
245
260
|
const initial = Option.flatten(Record.get(this._initialNodes, id));
|
|
246
|
-
return
|
|
261
|
+
return Atom.make<Option.Option<Node>>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
|
|
247
262
|
});
|
|
248
263
|
|
|
249
|
-
private readonly _nodeOrThrow =
|
|
250
|
-
return
|
|
264
|
+
private readonly _nodeOrThrow = Atom.family<string, Atom.Atom<Node>>((id) => {
|
|
265
|
+
return Atom.make((get) => {
|
|
251
266
|
const node = get(this._node(id));
|
|
252
267
|
invariant(Option.isSome(node), `Node not available: ${id}`);
|
|
253
268
|
return node.value;
|
|
254
269
|
});
|
|
255
270
|
});
|
|
256
271
|
|
|
257
|
-
private readonly _edges =
|
|
272
|
+
private readonly _edges = Atom.family<string, Atom.Writable<Edges>>((id) => {
|
|
258
273
|
const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({ inbound: [], outbound: [] })));
|
|
259
|
-
return
|
|
274
|
+
return Atom.make<Edges>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
|
|
260
275
|
});
|
|
261
276
|
|
|
262
|
-
// NOTE: Currently the argument to the family needs to be referentially stable for the
|
|
263
|
-
// TODO(wittjosiah):
|
|
264
|
-
private readonly _connections =
|
|
265
|
-
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) => {
|
|
266
281
|
const [id, relation] = key.split('$');
|
|
267
282
|
const edges = get(this._edges(id));
|
|
268
283
|
return edges[relation as Relation]
|
|
269
284
|
.map((id) => get(this._node(id)))
|
|
270
285
|
.filter(Option.isSome)
|
|
271
286
|
.map((o) => o.value);
|
|
272
|
-
}).pipe(
|
|
287
|
+
}).pipe(Atom.withLabel(`graph:connections:${key}`));
|
|
273
288
|
});
|
|
274
289
|
|
|
275
|
-
private readonly _actions =
|
|
276
|
-
return
|
|
290
|
+
private readonly _actions = Atom.family<string, Atom.Atom<(Action | ActionGroup)[]>>((id) => {
|
|
291
|
+
return Atom.make((get) => {
|
|
277
292
|
return get(this._connections(`${id}$outbound`)).filter(
|
|
278
293
|
(node) => node.type === ACTION_TYPE || node.type === ACTION_GROUP_TYPE,
|
|
279
294
|
);
|
|
280
|
-
}).pipe(
|
|
295
|
+
}).pipe(Atom.withLabel(`graph:actions:${id}`));
|
|
281
296
|
});
|
|
282
297
|
|
|
283
|
-
private readonly _json =
|
|
284
|
-
return
|
|
298
|
+
private readonly _json = Atom.family<string, Atom.Atom<any>>((id) => {
|
|
299
|
+
return Atom.make((get) => {
|
|
285
300
|
const toJSON = (node: Node, seen: string[] = []): any => {
|
|
286
301
|
const nodes = get(this.connections(node.id));
|
|
287
302
|
const obj: Record<string, any> = {
|
|
288
|
-
id: node.id
|
|
303
|
+
id: node.id,
|
|
289
304
|
type: node.type,
|
|
290
305
|
};
|
|
291
306
|
if (node.properties.label) {
|
|
@@ -305,7 +320,7 @@ export class Graph implements WritableGraph {
|
|
|
305
320
|
|
|
306
321
|
const root = get(this.nodeOrThrow(id));
|
|
307
322
|
return toJSON(root);
|
|
308
|
-
}).pipe(
|
|
323
|
+
}).pipe(Atom.withLabel(`graph:json:${id}`));
|
|
309
324
|
});
|
|
310
325
|
|
|
311
326
|
constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphParams = {}) {
|
|
@@ -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
|
|
|
@@ -399,38 +414,48 @@ export class Graph implements WritableGraph {
|
|
|
399
414
|
}
|
|
400
415
|
|
|
401
416
|
addNodes(nodes: NodeArg<any, Record<string, any>>[]): void {
|
|
402
|
-
|
|
417
|
+
Atom.batch(() => {
|
|
403
418
|
nodes.map((node) => this.addNode(node));
|
|
404
419
|
});
|
|
405
420
|
}
|
|
406
421
|
|
|
407
422
|
addNode({ nodes, edges, ...nodeArg }: NodeArg<any, Record<string, any>>): void {
|
|
408
423
|
const { id, type, data = null, properties = {} } = nodeArg;
|
|
409
|
-
const
|
|
410
|
-
const node = this._registry.get(
|
|
424
|
+
const nodeAtom = this._node(id);
|
|
425
|
+
const node = this._registry.get(nodeAtom);
|
|
411
426
|
Option.match(node, {
|
|
412
427
|
onSome: (node) => {
|
|
413
428
|
const typeChanged = node.type !== type;
|
|
414
429
|
const dataChanged = node.data !== data;
|
|
415
430
|
const propertiesChanged = Object.keys(properties).some((key) => node.properties[key] !== properties[key]);
|
|
416
|
-
log('existing node', {
|
|
431
|
+
log('existing node', {
|
|
432
|
+
id,
|
|
433
|
+
typeChanged,
|
|
434
|
+
dataChanged,
|
|
435
|
+
propertiesChanged,
|
|
436
|
+
});
|
|
417
437
|
if (typeChanged || dataChanged || propertiesChanged) {
|
|
418
438
|
log('updating node', { id, type, data, properties });
|
|
419
|
-
const newNode = Option.some({
|
|
420
|
-
|
|
439
|
+
const newNode = Option.some({
|
|
440
|
+
...node,
|
|
441
|
+
type,
|
|
442
|
+
data,
|
|
443
|
+
properties: { ...node.properties, ...properties },
|
|
444
|
+
});
|
|
445
|
+
this._registry.set(nodeAtom, newNode);
|
|
421
446
|
this.onNodeChanged.emit({ id, node: newNode });
|
|
422
447
|
}
|
|
423
448
|
},
|
|
424
449
|
onNone: () => {
|
|
425
450
|
log('new node', { id, type, data, properties });
|
|
426
451
|
const newNode = this._constructNode({ id, type, data, properties });
|
|
427
|
-
this._registry.set(
|
|
452
|
+
this._registry.set(nodeAtom, newNode);
|
|
428
453
|
this.onNodeChanged.emit({ id, node: newNode });
|
|
429
454
|
},
|
|
430
455
|
});
|
|
431
456
|
|
|
432
457
|
if (nodes) {
|
|
433
|
-
//
|
|
458
|
+
// Atom.batch(() => {
|
|
434
459
|
this.addNodes(nodes);
|
|
435
460
|
const _edges = nodes.map((node) => ({ source: id, target: node.id }));
|
|
436
461
|
this.addEdges(_edges);
|
|
@@ -443,15 +468,15 @@ export class Graph implements WritableGraph {
|
|
|
443
468
|
}
|
|
444
469
|
|
|
445
470
|
removeNodes(ids: string[], edges = false): void {
|
|
446
|
-
|
|
471
|
+
Atom.batch(() => {
|
|
447
472
|
ids.map((id) => this.removeNode(id, edges));
|
|
448
473
|
});
|
|
449
474
|
}
|
|
450
475
|
|
|
451
476
|
removeNode(id: string, edges = false): void {
|
|
452
|
-
const
|
|
453
|
-
// TODO(wittjosiah): Is there a way to mark these
|
|
454
|
-
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());
|
|
455
480
|
this.onNodeChanged.emit({ id, node: Option.none() });
|
|
456
481
|
// TODO(wittjosiah): Reset expanded and initialized flags?
|
|
457
482
|
|
|
@@ -468,55 +493,67 @@ export class Graph implements WritableGraph {
|
|
|
468
493
|
}
|
|
469
494
|
|
|
470
495
|
addEdges(edges: Edge[]): void {
|
|
471
|
-
|
|
496
|
+
Atom.batch(() => {
|
|
472
497
|
edges.map((edge) => this.addEdge(edge));
|
|
473
498
|
});
|
|
474
499
|
}
|
|
475
500
|
|
|
476
501
|
addEdge(edgeArg: Edge): void {
|
|
477
|
-
const
|
|
478
|
-
const source = this._registry.get(
|
|
502
|
+
const sourceAtom = this._edges(edgeArg.source);
|
|
503
|
+
const source = this._registry.get(sourceAtom);
|
|
479
504
|
if (!source.outbound.includes(edgeArg.target)) {
|
|
480
|
-
log('add outbound edge', {
|
|
481
|
-
|
|
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
|
+
});
|
|
482
513
|
}
|
|
483
514
|
|
|
484
|
-
const
|
|
485
|
-
const target = this._registry.get(
|
|
515
|
+
const targetAtom = this._edges(edgeArg.target);
|
|
516
|
+
const target = this._registry.get(targetAtom);
|
|
486
517
|
if (!target.inbound.includes(edgeArg.source)) {
|
|
487
|
-
log('add inbound edge', {
|
|
488
|
-
|
|
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
|
+
});
|
|
489
526
|
}
|
|
490
527
|
}
|
|
491
528
|
|
|
492
529
|
removeEdges(edges: Edge[], removeOrphans = false): void {
|
|
493
|
-
|
|
530
|
+
Atom.batch(() => {
|
|
494
531
|
edges.map((edge) => this.removeEdge(edge, removeOrphans));
|
|
495
532
|
});
|
|
496
533
|
}
|
|
497
534
|
|
|
498
535
|
removeEdge(edgeArg: Edge, removeOrphans = false): void {
|
|
499
|
-
const
|
|
500
|
-
const source = this._registry.get(
|
|
536
|
+
const sourceAtom = this._edges(edgeArg.source);
|
|
537
|
+
const source = this._registry.get(sourceAtom);
|
|
501
538
|
if (source.outbound.includes(edgeArg.target)) {
|
|
502
|
-
this._registry.set(
|
|
539
|
+
this._registry.set(sourceAtom, {
|
|
503
540
|
inbound: source.inbound,
|
|
504
541
|
outbound: source.outbound.filter((id) => id !== edgeArg.target),
|
|
505
542
|
});
|
|
506
543
|
}
|
|
507
544
|
|
|
508
|
-
const
|
|
509
|
-
const target = this._registry.get(
|
|
545
|
+
const targetAtom = this._edges(edgeArg.target);
|
|
546
|
+
const target = this._registry.get(targetAtom);
|
|
510
547
|
if (target.inbound.includes(edgeArg.source)) {
|
|
511
|
-
this._registry.set(
|
|
548
|
+
this._registry.set(targetAtom, {
|
|
512
549
|
inbound: target.inbound.filter((id) => id !== edgeArg.source),
|
|
513
550
|
outbound: target.outbound,
|
|
514
551
|
});
|
|
515
552
|
}
|
|
516
553
|
|
|
517
554
|
if (removeOrphans) {
|
|
518
|
-
const source = this._registry.get(
|
|
519
|
-
const target = this._registry.get(
|
|
555
|
+
const source = this._registry.get(sourceAtom);
|
|
556
|
+
const target = this._registry.get(targetAtom);
|
|
520
557
|
if (source.outbound.length === 0 && source.inbound.length === 0 && edgeArg.source !== ROOT_ID) {
|
|
521
558
|
this.removeNodes([edgeArg.source]);
|
|
522
559
|
}
|
|
@@ -527,12 +564,12 @@ export class Graph implements WritableGraph {
|
|
|
527
564
|
}
|
|
528
565
|
|
|
529
566
|
sortEdges(id: string, relation: Relation, order: string[]): void {
|
|
530
|
-
const
|
|
531
|
-
const edges = this._registry.get(
|
|
567
|
+
const edgesAtom = this._edges(id);
|
|
568
|
+
const edges = this._registry.get(edgesAtom);
|
|
532
569
|
const unsorted = edges[relation].filter((id) => !order.includes(id)) ?? [];
|
|
533
570
|
const sorted = order.filter((id) => edges[relation].includes(id)) ?? [];
|
|
534
571
|
edges[relation].splice(0, edges[relation].length, ...[...sorted, ...unsorted]);
|
|
535
|
-
this._registry.set(
|
|
572
|
+
this._registry.set(edgesAtom, edges);
|
|
536
573
|
}
|
|
537
574
|
|
|
538
575
|
traverse({ visitor, source = ROOT_ID, relation = 'outbound' }: GraphTraversalOptions, path: string[] = []): void {
|
|
@@ -553,7 +590,7 @@ export class Graph implements WritableGraph {
|
|
|
553
590
|
}
|
|
554
591
|
|
|
555
592
|
getPath({ source = 'root', target }: { source?: string; target: string }): Option.Option<string[]> {
|
|
556
|
-
return pipe(
|
|
593
|
+
return Function.pipe(
|
|
557
594
|
this.getNode(source),
|
|
558
595
|
Option.flatMap((node) => {
|
|
559
596
|
let found: Option.Option<string[]> = Option.none();
|
|
@@ -597,6 +634,11 @@ export class Graph implements WritableGraph {
|
|
|
597
634
|
|
|
598
635
|
/** @internal */
|
|
599
636
|
_constructNode(node: NodeArg<any>): Option.Option<Node> {
|
|
600
|
-
return Option.some({
|
|
637
|
+
return Option.some({
|
|
638
|
+
[graphSymbol]: this,
|
|
639
|
+
data: null,
|
|
640
|
+
properties: {},
|
|
641
|
+
...node,
|
|
642
|
+
});
|
|
601
643
|
}
|
|
602
644
|
}
|
|
@@ -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 { GraphBuilder,
|
|
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
|
|