@dxos/app-graph 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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/chunk-AKBGYELG.mjs +1603 -0
- package/dist/lib/browser/chunk-AKBGYELG.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +17 -1276
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs +1604 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +17 -1276
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/graph-builder.d.ts +11 -7
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +13 -17
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +43 -17
- package/dist/types/src/node-matcher.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +21 -5
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +39 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -26
- package/src/graph-builder.test.ts +569 -102
- package/src/graph-builder.ts +202 -74
- package/src/graph.test.ts +187 -52
- package/src/graph.ts +174 -98
- package/src/index.ts +1 -0
- package/src/node-matcher.ts +58 -28
- package/src/node.ts +46 -5
- package/src/stories/EchoGraph.stories.tsx +90 -61
- package/src/stories/Tree.tsx +1 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +95 -0
package/src/graph.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { log } from '@dxos/log';
|
|
|
15
15
|
import { type MakeOptional, isNonNullable } from '@dxos/util';
|
|
16
16
|
|
|
17
17
|
import * as Node from './node';
|
|
18
|
+
import { normalizeRelation, primaryKey, primaryParts, secondaryKey, secondaryParts, shallowEqual } from './util';
|
|
18
19
|
|
|
19
20
|
const graphSymbol = Symbol('graph');
|
|
20
21
|
|
|
@@ -48,12 +49,8 @@ export type GraphTraversalOptions = {
|
|
|
48
49
|
*/
|
|
49
50
|
source?: string;
|
|
50
51
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
*
|
|
54
|
-
* @default 'outbound'
|
|
55
|
-
*/
|
|
56
|
-
relation?: Node.Relation;
|
|
52
|
+
/** The relation(s) to traverse graph edges. */
|
|
53
|
+
relation: Node.RelationInput | Node.RelationInput[];
|
|
57
54
|
};
|
|
58
55
|
|
|
59
56
|
export type GraphProps = {
|
|
@@ -65,8 +62,8 @@ export type GraphProps = {
|
|
|
65
62
|
onRemoveNode?: (id: string) => void;
|
|
66
63
|
};
|
|
67
64
|
|
|
68
|
-
export type Edge = { source: string; target: string };
|
|
69
|
-
export type Edges =
|
|
65
|
+
export type Edge = { source: string; target: string; relation: Node.RelationInput };
|
|
66
|
+
export type Edges = Record<string, string[]>;
|
|
70
67
|
|
|
71
68
|
/**
|
|
72
69
|
* Identifier denoting a Graph.
|
|
@@ -104,7 +101,7 @@ export interface BaseGraph extends Pipeable.Pipeable {
|
|
|
104
101
|
/**
|
|
105
102
|
* Get the atom key for the connections of the node with the given id.
|
|
106
103
|
*/
|
|
107
|
-
connections(id: string, relation
|
|
104
|
+
connections(id: string, relation: Node.RelationInput): Atom.Atom<Node.Node[]>;
|
|
108
105
|
/**
|
|
109
106
|
* Get the atom key for the actions of the node with the given id.
|
|
110
107
|
*/
|
|
@@ -148,6 +145,7 @@ class GraphImpl implements WritableGraph {
|
|
|
148
145
|
|
|
149
146
|
readonly _registry: Registry.Registry;
|
|
150
147
|
readonly _expanded = Record.empty<string, boolean>();
|
|
148
|
+
readonly _pendingExpands = new Set<string>();
|
|
151
149
|
readonly _initialized = Record.empty<string, boolean>();
|
|
152
150
|
readonly _initialEdges = Record.empty<string, Edges>();
|
|
153
151
|
readonly _initialNodes = Record.fromEntries([
|
|
@@ -177,7 +175,7 @@ class GraphImpl implements WritableGraph {
|
|
|
177
175
|
});
|
|
178
176
|
|
|
179
177
|
readonly _edges = Atom.family<string, Atom.Writable<Edges>>((id) => {
|
|
180
|
-
const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({
|
|
178
|
+
const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({}) as Edges));
|
|
181
179
|
return Atom.make<Edges>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
|
|
182
180
|
});
|
|
183
181
|
|
|
@@ -185,9 +183,12 @@ class GraphImpl implements WritableGraph {
|
|
|
185
183
|
// TODO(wittjosiah): Atom feature request, support for something akin to `ComplexMap` to allow for complex arguments.
|
|
186
184
|
readonly _connections = Atom.family<string, Atom.Atom<Node.Node[]>>((key) => {
|
|
187
185
|
return Atom.make((get) => {
|
|
188
|
-
|
|
186
|
+
if (!key || primaryParts(key).length < 2) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const { id, relation } = relationFromConnectionKey(key);
|
|
189
190
|
const edges = get(this._edges(id));
|
|
190
|
-
return edges[relation
|
|
191
|
+
return (edges[relationKey(relation)] ?? [])
|
|
191
192
|
.map((id) => get(this._node(id)))
|
|
192
193
|
.filter(Option.isSome)
|
|
193
194
|
.map((o) => o.value);
|
|
@@ -196,16 +197,17 @@ class GraphImpl implements WritableGraph {
|
|
|
196
197
|
|
|
197
198
|
readonly _actions = Atom.family<string, Atom.Atom<(Node.Action | Node.ActionGroup)[]>>((id) => {
|
|
198
199
|
return Atom.make((get) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
if (!id) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
return get(this._connections(connectionKey(id, Node.actionRelation()))) as (Node.Action | Node.ActionGroup)[];
|
|
202
204
|
}).pipe(Atom.withLabel(`graph:actions:${id}`));
|
|
203
205
|
});
|
|
204
206
|
|
|
205
207
|
readonly _json = Atom.family<string, Atom.Atom<any>>((id) => {
|
|
206
208
|
return Atom.make((get) => {
|
|
207
209
|
const toJSON = (node: Node.Node, seen: string[] = []): any => {
|
|
208
|
-
const nodes = get(this._connections(
|
|
210
|
+
const nodes = get(this._connections(connectionKey(node.id, 'child')));
|
|
209
211
|
const obj: Record<string, any> = {
|
|
210
212
|
id: node.id,
|
|
211
213
|
type: node.type,
|
|
@@ -261,7 +263,7 @@ class GraphImpl implements WritableGraph {
|
|
|
261
263
|
return nodeOrThrowImpl(this, id);
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
connections(id: string, relation: Node.
|
|
266
|
+
connections(id: string, relation: Node.RelationInput): Atom.Atom<Node.Node[]> {
|
|
265
267
|
return connectionsImpl(this, id, relation);
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -327,13 +329,9 @@ const nodeOrThrowImpl = (graph: BaseGraph, id: string): Atom.Atom<Node.Node> =>
|
|
|
327
329
|
/**
|
|
328
330
|
* Implementation helper for connections.
|
|
329
331
|
*/
|
|
330
|
-
const connectionsImpl = (
|
|
331
|
-
graph: BaseGraph,
|
|
332
|
-
id: string,
|
|
333
|
-
relation: Node.Relation = 'outbound',
|
|
334
|
-
): Atom.Atom<Node.Node[]> => {
|
|
332
|
+
const connectionsImpl = (graph: BaseGraph, id: string, relation: Node.RelationInput): Atom.Atom<Node.Node[]> => {
|
|
335
333
|
const internal = getInternal(graph);
|
|
336
|
-
return internal._connections(
|
|
334
|
+
return internal._connections(connectionKey(id, relation));
|
|
337
335
|
};
|
|
338
336
|
|
|
339
337
|
/**
|
|
@@ -421,7 +419,7 @@ export function getRoot(graph: BaseGraph): Node.Node {
|
|
|
421
419
|
/**
|
|
422
420
|
* Implementation helper for getConnections.
|
|
423
421
|
*/
|
|
424
|
-
const getConnectionsImpl = (graph: BaseGraph, id: string, relation: Node.
|
|
422
|
+
const getConnectionsImpl = (graph: BaseGraph, id: string, relation: Node.RelationInput): Node.Node[] => {
|
|
425
423
|
const internal = getInternal(graph);
|
|
426
424
|
return internal._registry.get(connectionsImpl(graph, id, relation));
|
|
427
425
|
};
|
|
@@ -429,23 +427,24 @@ const getConnectionsImpl = (graph: BaseGraph, id: string, relation: Node.Relatio
|
|
|
429
427
|
/**
|
|
430
428
|
* Get all nodes connected to the node with the given id by the given relation from the graph's registry.
|
|
431
429
|
*/
|
|
432
|
-
export function getConnections(graph: BaseGraph, id: string, relation
|
|
433
|
-
export function getConnections(id: string, relation
|
|
430
|
+
export function getConnections(graph: BaseGraph, id: string, relation: Node.RelationInput): Node.Node[];
|
|
431
|
+
export function getConnections(id: string, relation: Node.RelationInput): (graph: BaseGraph) => Node.Node[];
|
|
434
432
|
export function getConnections(
|
|
435
433
|
graphOrId: BaseGraph | string,
|
|
436
|
-
idOrRelation
|
|
437
|
-
relation?: Node.
|
|
434
|
+
idOrRelation: string | Node.RelationInput,
|
|
435
|
+
relation?: Node.RelationInput,
|
|
438
436
|
): Node.Node[] | ((graph: BaseGraph) => Node.Node[]) {
|
|
439
437
|
if (typeof graphOrId === 'string') {
|
|
440
|
-
// Curried: getConnections(id, relation
|
|
438
|
+
// Curried: getConnections(id, relation)
|
|
441
439
|
const id = graphOrId;
|
|
442
|
-
const rel =
|
|
440
|
+
const rel = idOrRelation as Node.RelationInput;
|
|
443
441
|
return (graph: BaseGraph) => getConnectionsImpl(graph, id, rel);
|
|
444
442
|
} else {
|
|
445
|
-
// Direct: getConnections(graph, id, relation
|
|
443
|
+
// Direct: getConnections(graph, id, relation)
|
|
446
444
|
const graph = graphOrId;
|
|
447
445
|
const id = idOrRelation as string;
|
|
448
|
-
|
|
446
|
+
invariant(relation !== undefined, 'Relation is required.');
|
|
447
|
+
const rel = relation;
|
|
449
448
|
return getConnectionsImpl(graph, id, rel);
|
|
450
449
|
}
|
|
451
450
|
}
|
|
@@ -510,7 +509,7 @@ export function getEdges(graphOrId: BaseGraph | string, id?: string): Edges | ((
|
|
|
510
509
|
* Implementation helper for traverse.
|
|
511
510
|
*/
|
|
512
511
|
const traverseImpl = (graph: BaseGraph, options: GraphTraversalOptions, path: string[] = []): void => {
|
|
513
|
-
const { visitor, source = Node.RootId, relation
|
|
512
|
+
const { visitor, source = Node.RootId, relation } = options;
|
|
514
513
|
// Break cycles.
|
|
515
514
|
if (path.includes(source)) {
|
|
516
515
|
return;
|
|
@@ -522,9 +521,16 @@ const traverseImpl = (graph: BaseGraph, options: GraphTraversalOptions, path: st
|
|
|
522
521
|
return;
|
|
523
522
|
}
|
|
524
523
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
)
|
|
524
|
+
const relations = Array.isArray(relation) ? relation : [relation];
|
|
525
|
+
const seen = new Set<string>();
|
|
526
|
+
for (const rel of relations) {
|
|
527
|
+
for (const connected of getConnections(graph, source, rel)) {
|
|
528
|
+
if (!seen.has(connected.id)) {
|
|
529
|
+
seen.add(connected.id);
|
|
530
|
+
traverseImpl(graph, { source: connected.id, relation, visitor }, [...path, source]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
528
534
|
};
|
|
529
535
|
|
|
530
536
|
/**
|
|
@@ -561,6 +567,7 @@ const getPathImpl = (graph: BaseGraph, params: { source?: string; target: string
|
|
|
561
567
|
let found: Option.Option<string[]> = Option.none();
|
|
562
568
|
traverseImpl(graph, {
|
|
563
569
|
source: node.id,
|
|
570
|
+
relation: 'child',
|
|
564
571
|
visitor: (node, path) => {
|
|
565
572
|
if (Option.isSome(found)) {
|
|
566
573
|
return false;
|
|
@@ -660,8 +667,8 @@ const initializeImpl = async <T extends ExpandableGraph | WritableGraph>(graph:
|
|
|
660
667
|
const initialized = Record.get(internal._initialized, id).pipe(Option.getOrElse(() => false));
|
|
661
668
|
log('initialize', { id, initialized });
|
|
662
669
|
if (!initialized) {
|
|
663
|
-
await internal._onInitialize?.(id);
|
|
664
670
|
Record.set(internal._initialized, id, true);
|
|
671
|
+
await internal._onInitialize?.(id);
|
|
665
672
|
}
|
|
666
673
|
return graph;
|
|
667
674
|
};
|
|
@@ -690,19 +697,29 @@ export function initialize<T extends ExpandableGraph | WritableGraph>(
|
|
|
690
697
|
|
|
691
698
|
/**
|
|
692
699
|
* Implementation helper for expand.
|
|
700
|
+
* If the node does not exist yet, the expand is recorded as pending and applied when the node is added.
|
|
693
701
|
*/
|
|
694
702
|
const expandImpl = <T extends ExpandableGraph | WritableGraph>(
|
|
695
703
|
graph: T,
|
|
696
704
|
id: string,
|
|
697
|
-
relation: Node.
|
|
705
|
+
relation: Node.RelationInput,
|
|
698
706
|
): T => {
|
|
699
707
|
const internal = getInternal(graph);
|
|
700
|
-
const
|
|
708
|
+
const normalizedRelation = normalizeRelation(relation);
|
|
709
|
+
const key = primaryKey(id, relationKey(normalizedRelation));
|
|
710
|
+
const nodeOpt = internal._registry.get(internal._node(id));
|
|
711
|
+
if (Option.isNone(nodeOpt)) {
|
|
712
|
+
// Node not yet in graph: record expand to run when the node is added.
|
|
713
|
+
internal._pendingExpands.add(key);
|
|
714
|
+
log('expand', { key, deferred: true });
|
|
715
|
+
return graph;
|
|
716
|
+
}
|
|
717
|
+
|
|
701
718
|
const expanded = Record.get(internal._expanded, key).pipe(Option.getOrElse(() => false));
|
|
702
719
|
log('expand', { key, expanded });
|
|
703
720
|
if (!expanded) {
|
|
704
|
-
internal._onExpand?.(id, relation);
|
|
705
721
|
Record.set(internal._expanded, key, true);
|
|
722
|
+
internal._onExpand?.(id, normalizedRelation);
|
|
706
723
|
}
|
|
707
724
|
return graph;
|
|
708
725
|
};
|
|
@@ -712,26 +729,31 @@ const expandImpl = <T extends ExpandableGraph | WritableGraph>(
|
|
|
712
729
|
*
|
|
713
730
|
* Fires the `onExpand` callback to add connections to the node.
|
|
714
731
|
*/
|
|
715
|
-
export function expand<T extends ExpandableGraph | WritableGraph>(
|
|
732
|
+
export function expand<T extends ExpandableGraph | WritableGraph>(
|
|
733
|
+
graph: T,
|
|
734
|
+
id: string,
|
|
735
|
+
relation: Node.RelationInput,
|
|
736
|
+
): T;
|
|
716
737
|
export function expand(
|
|
717
738
|
id: string,
|
|
718
|
-
relation
|
|
739
|
+
relation: Node.RelationInput,
|
|
719
740
|
): <T extends ExpandableGraph | WritableGraph>(graph: T) => T;
|
|
720
741
|
export function expand<T extends ExpandableGraph | WritableGraph>(
|
|
721
742
|
graphOrId: T | string,
|
|
722
|
-
idOrRelation
|
|
723
|
-
relation?: Node.
|
|
743
|
+
idOrRelation: string | Node.RelationInput,
|
|
744
|
+
relation?: Node.RelationInput,
|
|
724
745
|
): T | (<T extends ExpandableGraph | WritableGraph>(graph: T) => T) {
|
|
725
746
|
if (typeof graphOrId === 'string') {
|
|
726
|
-
// Curried: expand(id, relation
|
|
747
|
+
// Curried: expand(id, relation)
|
|
727
748
|
const id = graphOrId;
|
|
728
|
-
const rel =
|
|
749
|
+
const rel = idOrRelation as Node.RelationInput;
|
|
729
750
|
return <T extends ExpandableGraph | WritableGraph>(graph: T) => expandImpl(graph, id, rel);
|
|
730
751
|
} else {
|
|
731
|
-
// Direct: expand(graph, id, relation
|
|
752
|
+
// Direct: expand(graph, id, relation)
|
|
732
753
|
const graph = graphOrId;
|
|
733
754
|
const id = idOrRelation as string;
|
|
734
|
-
|
|
755
|
+
invariant(relation !== undefined, 'Relation is required.');
|
|
756
|
+
const rel = relation;
|
|
735
757
|
return expandImpl(graph, id, rel);
|
|
736
758
|
}
|
|
737
759
|
}
|
|
@@ -742,16 +764,24 @@ export function expand<T extends ExpandableGraph | WritableGraph>(
|
|
|
742
764
|
const sortEdgesImpl = <T extends ExpandableGraph | WritableGraph>(
|
|
743
765
|
graph: T,
|
|
744
766
|
id: string,
|
|
745
|
-
relation: Node.
|
|
767
|
+
relation: Node.RelationInput,
|
|
746
768
|
order: string[],
|
|
747
769
|
): T => {
|
|
748
770
|
const internal = getInternal(graph);
|
|
749
771
|
const edgesAtom = internal._edges(id);
|
|
750
772
|
const edges = internal._registry.get(edgesAtom);
|
|
751
|
-
const
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
773
|
+
const relationId = relationKey(relation);
|
|
774
|
+
const current = edges[relationId] ?? [];
|
|
775
|
+
const unsorted = current.filter((id) => !order.includes(id));
|
|
776
|
+
const sorted = order.filter((id) => current.includes(id));
|
|
777
|
+
const newOrder = [...sorted, ...unsorted];
|
|
778
|
+
if (newOrder.length === current.length && newOrder.every((id, i) => id === current[i])) {
|
|
779
|
+
return graph;
|
|
780
|
+
}
|
|
781
|
+
internal._registry.set(edgesAtom, {
|
|
782
|
+
...edges,
|
|
783
|
+
[relationId]: newOrder,
|
|
784
|
+
});
|
|
755
785
|
return graph;
|
|
756
786
|
};
|
|
757
787
|
|
|
@@ -761,31 +791,31 @@ const sortEdgesImpl = <T extends ExpandableGraph | WritableGraph>(
|
|
|
761
791
|
export function sortEdges<T extends ExpandableGraph | WritableGraph>(
|
|
762
792
|
graph: T,
|
|
763
793
|
id: string,
|
|
764
|
-
relation: Node.
|
|
794
|
+
relation: Node.RelationInput,
|
|
765
795
|
order: string[],
|
|
766
796
|
): T;
|
|
767
797
|
export function sortEdges(
|
|
768
798
|
id: string,
|
|
769
|
-
relation: Node.
|
|
799
|
+
relation: Node.RelationInput,
|
|
770
800
|
order: string[],
|
|
771
801
|
): <T extends ExpandableGraph | WritableGraph>(graph: T) => T;
|
|
772
802
|
export function sortEdges<T extends ExpandableGraph | WritableGraph>(
|
|
773
803
|
graphOrId: T | string,
|
|
774
|
-
idOrRelation?: string | Node.
|
|
775
|
-
relationOrOrder?: Node.
|
|
804
|
+
idOrRelation?: string | Node.RelationInput,
|
|
805
|
+
relationOrOrder?: Node.RelationInput | string[],
|
|
776
806
|
order?: string[],
|
|
777
807
|
): T | (<T extends ExpandableGraph | WritableGraph>(graph: T) => T) {
|
|
778
808
|
if (typeof graphOrId === 'string') {
|
|
779
809
|
// Curried: sortEdges(id, relation, order)
|
|
780
810
|
const id = graphOrId;
|
|
781
|
-
const relation = idOrRelation as Node.
|
|
811
|
+
const relation = idOrRelation as Node.RelationInput;
|
|
782
812
|
const order = relationOrOrder as string[];
|
|
783
813
|
return <T extends ExpandableGraph | WritableGraph>(graph: T) => sortEdgesImpl(graph, id, relation, order);
|
|
784
814
|
} else {
|
|
785
815
|
// Direct: sortEdges(graph, id, relation, order)
|
|
786
816
|
const graph = graphOrId;
|
|
787
817
|
const id = idOrRelation as string;
|
|
788
|
-
const relation = relationOrOrder as Node.
|
|
818
|
+
const relation = relationOrOrder as Node.RelationInput;
|
|
789
819
|
return sortEdgesImpl(graph, id, relation, order!);
|
|
790
820
|
}
|
|
791
821
|
}
|
|
@@ -842,7 +872,7 @@ const addNodeImpl = <T extends WritableGraph>(graph: T, nodeArg: Node.NodeArg<an
|
|
|
842
872
|
Option.match(existingNode, {
|
|
843
873
|
onSome: (existing) => {
|
|
844
874
|
const typeChanged = existing.type !== type;
|
|
845
|
-
const dataChanged = existing.data
|
|
875
|
+
const dataChanged = !shallowEqual(existing.data, data);
|
|
846
876
|
const propertiesChanged = Object.keys(properties).some((key) => existing.properties[key] !== properties[key]);
|
|
847
877
|
log('existing node', {
|
|
848
878
|
id,
|
|
@@ -868,12 +898,21 @@ const addNodeImpl = <T extends WritableGraph>(graph: T, nodeArg: Node.NodeArg<an
|
|
|
868
898
|
const newNode = internal._constructNode({ id, type, data, properties, ...rest });
|
|
869
899
|
internal._registry.set(nodeAtom, newNode);
|
|
870
900
|
graph.onNodeChanged.emit({ id, node: newNode });
|
|
901
|
+
|
|
902
|
+
// Apply any expands that were deferred because this node did not exist yet.
|
|
903
|
+
const toApply = [...internal._pendingExpands].filter((k) => primaryParts(k)[0] === id);
|
|
904
|
+
for (const pendingKey of toApply) {
|
|
905
|
+
internal._pendingExpands.delete(pendingKey);
|
|
906
|
+
const relation = relationFromKey(primaryParts(pendingKey)[1]);
|
|
907
|
+
Record.set(internal._expanded, pendingKey, true);
|
|
908
|
+
internal._onExpand?.(id, relation);
|
|
909
|
+
}
|
|
871
910
|
},
|
|
872
911
|
});
|
|
873
912
|
|
|
874
913
|
if (nodes) {
|
|
875
914
|
addNodesImpl(graph, nodes);
|
|
876
|
-
const _edges = nodes.map((node) => ({ source: id, target: node.id }));
|
|
915
|
+
const _edges = nodes.map((node) => ({ source: id, target: node.id, relation: 'child' as const }));
|
|
877
916
|
addEdgesImpl(graph, _edges);
|
|
878
917
|
}
|
|
879
918
|
|
|
@@ -949,11 +988,20 @@ const removeNodeImpl = <T extends WritableGraph>(graph: T, id: string, edges = f
|
|
|
949
988
|
// TODO(wittjosiah): Reset expanded and initialized flags?
|
|
950
989
|
|
|
951
990
|
if (edges) {
|
|
952
|
-
const
|
|
953
|
-
const edgesToRemove = [
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
991
|
+
const nodeEdges = internal._registry.get(internal._edges(id));
|
|
992
|
+
const edgesToRemove: Edge[] = [];
|
|
993
|
+
for (const [relationKeyValue, relatedIds] of Object.entries(nodeEdges)) {
|
|
994
|
+
const relation = relationFromKey(relationKeyValue);
|
|
995
|
+
const isInboundRelation = relation.direction === 'inbound';
|
|
996
|
+
for (const relatedId of relatedIds) {
|
|
997
|
+
if (isInboundRelation) {
|
|
998
|
+
// Inbound edge lists store source node IDs; reconstruct the canonical outbound edge.
|
|
999
|
+
edgesToRemove.push({ source: relatedId, target: id, relation: inverseRelation(relation) });
|
|
1000
|
+
} else {
|
|
1001
|
+
edgesToRemove.push({ source: id, target: relatedId, relation });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
957
1005
|
removeEdgesImpl(graph, edgesToRemove);
|
|
958
1006
|
}
|
|
959
1007
|
|
|
@@ -1019,32 +1067,28 @@ export function addEdges<T extends WritableGraph>(
|
|
|
1019
1067
|
* Implementation helper for addEdge.
|
|
1020
1068
|
*/
|
|
1021
1069
|
const addEdgeImpl = <T extends WritableGraph>(graph: T, edgeArg: Edge): T => {
|
|
1070
|
+
const relation = normalizeRelation(edgeArg.relation);
|
|
1071
|
+
const relationId = relationKey(relation);
|
|
1072
|
+
const inverse = inverseRelation(relation);
|
|
1073
|
+
const inverseId = relationKey(inverse);
|
|
1022
1074
|
const internal = getInternal(graph);
|
|
1075
|
+
|
|
1023
1076
|
const sourceAtom = internal._edges(edgeArg.source);
|
|
1024
1077
|
const source = internal._registry.get(sourceAtom);
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
});
|
|
1030
|
-
internal._registry.set(sourceAtom, {
|
|
1031
|
-
inbound: source.inbound,
|
|
1032
|
-
outbound: [...source.outbound, edgeArg.target],
|
|
1033
|
-
});
|
|
1078
|
+
const sourceList = source[relationId] ?? [];
|
|
1079
|
+
if (!sourceList.includes(edgeArg.target)) {
|
|
1080
|
+
log('add edge', { source: edgeArg.source, target: edgeArg.target, relation: relationId });
|
|
1081
|
+
internal._registry.set(sourceAtom, { ...source, [relationId]: [...sourceList, edgeArg.target] });
|
|
1034
1082
|
}
|
|
1035
1083
|
|
|
1036
1084
|
const targetAtom = internal._edges(edgeArg.target);
|
|
1037
1085
|
const target = internal._registry.get(targetAtom);
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
});
|
|
1043
|
-
internal._registry.set(targetAtom, {
|
|
1044
|
-
inbound: [...target.inbound, edgeArg.source],
|
|
1045
|
-
outbound: target.outbound,
|
|
1046
|
-
});
|
|
1086
|
+
const targetList = target[inverseId] ?? [];
|
|
1087
|
+
if (!targetList.includes(edgeArg.source)) {
|
|
1088
|
+
log('add inverse edge', { source: edgeArg.source, target: edgeArg.target, relation: inverseId });
|
|
1089
|
+
internal._registry.set(targetAtom, { ...target, [inverseId]: [...targetList, edgeArg.source] });
|
|
1047
1090
|
}
|
|
1091
|
+
|
|
1048
1092
|
return graph;
|
|
1049
1093
|
};
|
|
1050
1094
|
|
|
@@ -1106,32 +1150,34 @@ export function removeEdges<T extends WritableGraph>(
|
|
|
1106
1150
|
* Implementation helper for removeEdge.
|
|
1107
1151
|
*/
|
|
1108
1152
|
const removeEdgeImpl = <T extends WritableGraph>(graph: T, edgeArg: Edge, removeOrphans = false): T => {
|
|
1153
|
+
const relation = normalizeRelation(edgeArg.relation);
|
|
1154
|
+
const relationId = relationKey(relation);
|
|
1155
|
+
const inverse = inverseRelation(relation);
|
|
1156
|
+
const inverseId = relationKey(inverse);
|
|
1109
1157
|
const internal = getInternal(graph);
|
|
1158
|
+
|
|
1110
1159
|
const sourceAtom = internal._edges(edgeArg.source);
|
|
1111
1160
|
const source = internal._registry.get(sourceAtom);
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
outbound: source.outbound.filter((id) => id !== edgeArg.target),
|
|
1116
|
-
});
|
|
1161
|
+
const sourceList = source[relationId] ?? [];
|
|
1162
|
+
if (sourceList.includes(edgeArg.target)) {
|
|
1163
|
+
internal._registry.set(sourceAtom, { ...source, [relationId]: sourceList.filter((id) => id !== edgeArg.target) });
|
|
1117
1164
|
}
|
|
1118
1165
|
|
|
1119
1166
|
const targetAtom = internal._edges(edgeArg.target);
|
|
1120
1167
|
const target = internal._registry.get(targetAtom);
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
outbound: target.outbound,
|
|
1125
|
-
});
|
|
1168
|
+
const targetList = target[inverseId] ?? [];
|
|
1169
|
+
if (targetList.includes(edgeArg.source)) {
|
|
1170
|
+
internal._registry.set(targetAtom, { ...target, [inverseId]: targetList.filter((id) => id !== edgeArg.source) });
|
|
1126
1171
|
}
|
|
1127
1172
|
|
|
1128
1173
|
if (removeOrphans) {
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1174
|
+
const sourceAfter = internal._registry.get(sourceAtom);
|
|
1175
|
+
const targetAfter = internal._registry.get(targetAtom);
|
|
1176
|
+
const isEmpty = (edges: Edges) => Object.values(edges).every((ids) => ids.length === 0);
|
|
1177
|
+
if (isEmpty(sourceAfter) && edgeArg.source !== Node.RootId) {
|
|
1132
1178
|
removeNodesImpl(graph, [edgeArg.source]);
|
|
1133
1179
|
}
|
|
1134
|
-
if (
|
|
1180
|
+
if (isEmpty(targetAfter) && edgeArg.target !== Node.RootId) {
|
|
1135
1181
|
removeNodesImpl(graph, [edgeArg.target]);
|
|
1136
1182
|
}
|
|
1137
1183
|
}
|
|
@@ -1172,3 +1218,33 @@ export function removeEdge<T extends WritableGraph>(
|
|
|
1172
1218
|
export const make = (params?: GraphProps): Graph => {
|
|
1173
1219
|
return new GraphImpl(params);
|
|
1174
1220
|
};
|
|
1221
|
+
|
|
1222
|
+
//
|
|
1223
|
+
// Utilities
|
|
1224
|
+
//
|
|
1225
|
+
|
|
1226
|
+
export const relationKey = (relation: Node.RelationInput): string => {
|
|
1227
|
+
const normalized = normalizeRelation(relation);
|
|
1228
|
+
return secondaryKey(normalized.kind, normalized.direction);
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
export const relationFromKey = (encoded: string): Node.Relation => {
|
|
1232
|
+
const parts = secondaryParts(encoded);
|
|
1233
|
+
invariant(parts.length === 2 && parts[0].length > 0 && parts[1].length > 0, `Invalid relation key: ${encoded}`);
|
|
1234
|
+
const [kind, directionRaw] = parts;
|
|
1235
|
+
invariant(directionRaw === 'outbound' || directionRaw === 'inbound', `Invalid relation direction: ${directionRaw}`);
|
|
1236
|
+
return Node.relation(kind, directionRaw);
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
const connectionKey = (id: string, relation: Node.RelationInput): string => primaryKey(id, relationKey(relation));
|
|
1240
|
+
|
|
1241
|
+
const relationFromConnectionKey = (key: string): { id: string; relation: Node.Relation } => {
|
|
1242
|
+
const [id, encodedRelation] = primaryParts(key);
|
|
1243
|
+
invariant(id && encodedRelation, `Invalid connection key: ${key}`);
|
|
1244
|
+
return { id, relation: relationFromKey(encodedRelation) };
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
const inverseRelation = (relation: Node.RelationInput): Node.Relation => {
|
|
1248
|
+
const normalized = normalizeRelation(relation);
|
|
1249
|
+
return Node.relation(normalized.kind, normalized.direction === 'outbound' ? 'inbound' : 'outbound');
|
|
1250
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * as Graph from './graph';
|
|
|
8
8
|
export * as GraphBuilder from './graph-builder';
|
|
9
9
|
export * as Node from './node';
|
|
10
10
|
export * as NodeMatcher from './node-matcher';
|
|
11
|
+
export { qualifyId, getParentId, getSegmentId } from './util';
|
|
11
12
|
|
|
12
13
|
// TODO(wittjosiah): Direct re-export needed for portable type references.
|
|
13
14
|
export type { BuilderExtensions } from './graph-builder';
|