@dxos/app-graph 0.4.7-main.d28a540 → 0.4.7-main.e015b9e

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/src/graph.ts CHANGED
@@ -2,30 +2,25 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { untracked } from '@preact/signals-core';
6
- import { type DeepSignal, deepSignal } from 'deepsignal/react';
5
+ import { deepSignal } from 'deepsignal/react';
6
+ // TODO(wittjosiah): Remove lodash dependency.
7
+ import get from 'lodash.get';
7
8
 
8
9
  import { invariant } from '@dxos/invariant';
9
- import { nonNullable } from '@dxos/util';
10
10
 
11
- import { isActionLike, type EdgeDirection, type Node, type NodeArg, type NodeBase } from './node';
12
-
13
- export const ROOT_ID = 'root';
11
+ import { type Label } from './action';
12
+ import { type Node } from './node';
14
13
 
15
14
  export type TraversalOptions = {
16
15
  /**
17
- * The node to start traversing from.
18
- *
19
- * @default root
16
+ * The node to start traversing from. Defaults to the root node.
20
17
  */
21
18
  node?: Node;
22
19
 
23
20
  /**
24
- * The direction to traverse graph edges.
25
- *
26
- * @default 'outbound'
21
+ * The direction to traverse the graph. Defaults to 'down'.
27
22
  */
28
- direction?: EdgeDirection;
23
+ direction?: 'up' | 'down';
29
24
 
30
25
  /**
31
26
  * A predicate to filter nodes which are passed to the `visitor` callback.
@@ -42,264 +37,76 @@ export type TraversalOptions = {
42
37
  * The Graph represents the structure of the application constructed via plugins.
43
38
  */
44
39
  export class Graph {
45
- /**
46
- * @internal
47
- */
48
- readonly _nodes = deepSignal<Record<string, NodeBase>>({
49
- [ROOT_ID]: { id: ROOT_ID, properties: {}, data: null },
50
- });
40
+ // TODO(wittjosiah): Should this support multiple paths to the same node?
41
+ private readonly _index = deepSignal<Record<string, string[]>>({});
51
42
 
52
- /**
53
- * @internal
54
- */
55
- // Key is the `${node.id}-${direction}` and value is an ordered list of node ids.
56
- // Explicit type required because TS says this is not portable.
57
- readonly _edges: DeepSignal<Record<string, string[]>> = deepSignal({});
43
+ constructor(private readonly _root: Node) {}
58
44
 
59
- /**
60
- * Alias for `findNode('root')`.
61
- */
62
- get root() {
63
- return this.findNode(ROOT_ID)!;
64
- }
65
-
66
- /**
67
- * Convert the graph to a JSON object.
68
- */
69
- toJSON({ id = ROOT_ID, maxLength = 32 }: { id?: string; maxLength?: number } = {}) {
45
+ toJSON() {
46
+ const toLabel = (label: Label) => (Array.isArray(label) ? `${label[1].ns}[${label[0]}]` : label);
70
47
  const toJSON = (node: Node): any => {
71
- const nodes = node.nodes();
72
- const obj: Record<string, any> = {
73
- id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
48
+ return {
49
+ id: node.id.slice(0, 16),
50
+ label: toLabel(node.label),
51
+ children: node.children.length ? node.children.map((node) => toJSON(node)) : undefined,
52
+ actions: node.actions.length
53
+ ? node.actions.map(({ id, label }) => ({
54
+ id,
55
+ label: toLabel(label),
56
+ }))
57
+ : undefined,
74
58
  };
75
- if (node.properties.label) {
76
- obj.label = node.properties.label;
77
- }
78
- if (nodes.length) {
79
- obj.nodes = nodes.map((node) => toJSON(node));
80
- }
81
- return obj;
82
59
  };
83
60
 
84
- const root = this.findNode(id);
85
- invariant(root, `Node not found: ${id}`);
86
- return toJSON(root);
87
- }
88
-
89
- /**
90
- * Find the node with the given id in the graph.
91
- */
92
- findNode(id: string): Node | undefined {
93
- const nodeBase = this._nodes[id];
94
- if (!nodeBase) {
95
- return undefined;
96
- }
97
-
98
- return this._constructNode(nodeBase);
99
- }
100
-
101
- private _constructNode = (nodeBase: NodeBase): Node => {
102
- const node: Node = {
103
- ...nodeBase,
104
- edges: ({ direction = 'outbound' } = {}) => {
105
- return this._edges[this.getEdgeKey(node.id, direction)];
106
- },
107
- nodes: ({ direction, filter } = {}) => {
108
- const nodes = this._getNodes({ id: node.id, direction }).filter((n) => !isActionLike(n));
109
- return filter ? nodes.filter((n) => filter(n, node)) : nodes;
110
- },
111
- node: (id: string) => {
112
- return this._getNodes({ id }).find((node) => node.id === id);
113
- },
114
- actions: () => {
115
- return this._getNodes({ id: node.id }).filter(isActionLike);
116
- },
117
- };
118
-
119
- return node;
120
- };
121
-
122
- private _getNodes({ id, direction = 'outbound' }: { id: string; direction?: EdgeDirection }): Node[] {
123
- const edges = this._edges[this.getEdgeKey(id, direction)];
124
- if (!edges) {
125
- return [];
126
- }
127
-
128
- return edges.map((id) => this.findNode(id)).filter(nonNullable);
129
- }
130
-
131
- private getEdgeKey(id: string, direction: EdgeDirection) {
132
- return `${id}-${direction}`;
61
+ return toJSON(this._root);
133
62
  }
134
63
 
135
64
  /**
136
- * Add nodes to the graph.
65
+ * The root node of the graph which is the entry point for all knowledge.
137
66
  */
138
- addNodes<TData = null, TProperties extends Record<string, any> = Record<string, any>>(
139
- ...nodes: NodeArg<TData, TProperties>[]
140
- ): Node<TData, TProperties>[] {
141
- return nodes.map((node) => this._addNode(node));
142
- }
143
-
144
- private _addNode<TData, TProperties extends Record<string, any> = Record<string, any>>({
145
- nodes,
146
- edges,
147
- ..._node
148
- }: NodeArg<TData, TProperties>): Node<TData, TProperties> {
149
- return untracked(() => {
150
- const node = { data: null, properties: {}, ..._node };
151
- this._nodes[node.id] = node;
152
-
153
- if (nodes) {
154
- nodes.forEach((subNode) => {
155
- this._addNode(subNode);
156
- this.addEdge({ source: node.id, target: subNode.id });
157
- });
158
- }
159
-
160
- if (edges) {
161
- edges.forEach(([id, direction]) =>
162
- direction === 'outbound'
163
- ? this.addEdge({ source: node.id, target: id })
164
- : this.addEdge({ source: id, target: node.id }),
165
- );
166
- }
167
-
168
- return this._constructNode(node) as Node<TData, TProperties>;
169
- });
67
+ get root(): Node {
68
+ return this._root;
170
69
  }
171
70
 
172
71
  /**
173
- * Remove nodes from the graph.
174
- *
175
- * @param id The id of the node to remove.
176
- * @param edges Whether to remove edges connected to the node from the graph as well.
72
+ * Get the path through the graph from the root to the node with the given id.
177
73
  */
178
- removeNode(id: string, edges = false) {
179
- untracked(() => {
180
- const node = this.findNode(id);
181
- if (!node) {
182
- return;
183
- }
184
-
185
- if (edges) {
186
- // Remove edges from node.
187
- delete this._edges[this.getEdgeKey(id, 'outbound')];
188
- delete this._edges[this.getEdgeKey(id, 'inbound')];
189
-
190
- // Remove edges from connected nodes.
191
- this._getNodes({ id }).forEach((node) => this.removeEdge({ source: id, target: node.id }));
192
- this._getNodes({ id, direction: 'inbound' }).forEach((node) =>
193
- this.removeEdge({ source: node.id, target: id }),
194
- );
195
- }
196
-
197
- // Remove node.
198
- delete this._nodes[id];
199
- });
200
- }
201
-
202
- /**
203
- * Add an edge to the graph.
204
- */
205
- addEdge({ source, target }: { source: string; target: string }) {
206
- untracked(() => {
207
- const outbound = this._edges[this.getEdgeKey(source, 'outbound')];
208
- if (!outbound) {
209
- this._edges[this.getEdgeKey(source, 'outbound')] = [target];
210
- } else if (!outbound.includes(target)) {
211
- outbound.push(target);
212
- }
213
-
214
- const inbound = this._edges[this.getEdgeKey(target, 'inbound')];
215
- if (!inbound) {
216
- this._edges[this.getEdgeKey(target, 'inbound')] = [source];
217
- } else if (!inbound.includes(source)) {
218
- inbound.push(source);
219
- }
220
- });
74
+ getPath(id: string): string[] | undefined {
75
+ return this._index[id];
221
76
  }
222
77
 
223
78
  /**
224
- * Sort edges for a node.
225
- *
226
- * Edges not included in the sorted list are appended to the end of the list.
227
- *
228
- * @param nodeId The id of the node to sort edges for.
229
- * @param direction The direction of the edges from the node to sort.
230
- * @param edges The ordered list of edges.
79
+ * @internal
231
80
  */
232
- sortEdges(nodeId: string, direction: EdgeDirection, edges: string[]) {
233
- untracked(() => {
234
- const current = this._edges[this.getEdgeKey(nodeId, direction)];
235
- if (current) {
236
- const unsorted = current.filter((id) => !edges.includes(id)) ?? [];
237
- const sorted = edges.filter((id) => current.includes(id)) ?? [];
238
- current.splice(0, current.length, ...[...sorted, ...unsorted]);
239
- }
240
- });
81
+ _setPath(id: string, path: string[]) {
82
+ invariant(id && path, 'Invalid path.');
83
+ this._index[id] = path;
241
84
  }
242
85
 
243
86
  /**
244
- * Remove an edge from the graph.
87
+ * Find the node with the given id in the graph.
245
88
  */
246
- removeEdge({ source, target }: { source: string; target: string }) {
247
- untracked(() => {
248
- const outboundIndex = this._edges[this.getEdgeKey(source, 'outbound')]?.findIndex((id) => id === target);
249
- if (outboundIndex !== -1) {
250
- this._edges[this.getEdgeKey(source, 'outbound')].splice(outboundIndex, 1);
251
- }
89
+ findNode(id: string): Node | undefined {
90
+ const path = this.getPath(id);
91
+ if (!path) {
92
+ return undefined;
93
+ }
252
94
 
253
- const inboundIndex = this._edges[this.getEdgeKey(target, 'inbound')]?.findIndex((id) => id === source);
254
- if (inboundIndex !== -1) {
255
- this._edges[this.getEdgeKey(target, 'inbound')].splice(inboundIndex, 1);
256
- }
257
- });
95
+ return path.length > 0 ? get(this._root, path) : this._root;
258
96
  }
259
97
 
260
98
  /**
261
- * Recursive depth-first traversal.
262
- *
263
- * @param options.node The node to start traversing from.
264
- * @param options.direction The direction to traverse graph edges.
265
- * @param options.filter A predicate to filter nodes which are passed to the `visitor` callback.
266
- * @param options.visitor A callback which is called for each node visited during traversal.
99
+ * Recursive breadth-first traversal.
267
100
  */
268
- traverse({ node = this.root, direction = 'outbound', filter, visitor }: TraversalOptions, path: string[] = []): void {
269
- // Break cycles.
270
- if (path.includes(node.id)) {
271
- return;
272
- }
273
-
101
+ traverse({ node = this._root, direction = 'down', filter, visitor }: TraversalOptions, depth = 0): void {
274
102
  if (!filter || filter(node)) {
275
- visitor?.(node, [...path, node.id]);
103
+ visitor?.(node, this.getPath(node.id)!);
276
104
  }
277
105
 
278
- Object.values(this._getNodes({ id: node.id, direction })).forEach((child) =>
279
- this.traverse({ node: child, direction, filter, visitor }, [...path, node.id]),
280
- );
281
- }
282
-
283
- /**
284
- * Get the path between two nodes in the graph.
285
- */
286
- getPath({ source = 'root', target }: { source?: string; target: string }): string[] | undefined {
287
- const start = this.findNode(source);
288
- if (!start) {
289
- return undefined;
106
+ if (direction === 'down') {
107
+ Object.values(node.children).forEach((child) => this.traverse({ node: child, filter, visitor }));
108
+ } else if (direction === 'up' && node.parent) {
109
+ this.traverse({ node: node.parent, direction, filter, visitor }, depth + 1);
290
110
  }
291
-
292
- let found: string[] | undefined;
293
- this.traverse({
294
- node: start,
295
- filter: () => !found,
296
- visitor: (node, path) => {
297
- if (node.id === target) {
298
- found = path;
299
- }
300
- },
301
- });
302
-
303
- return found;
304
111
  }
305
112
  }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './action';
5
6
  export * from './graph';
6
7
  export * from './graph-builder';
7
- export * from './helpers';
8
8
  export * from './node';
package/src/node.ts CHANGED
@@ -2,21 +2,56 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type MaybePromise, type MakeOptional } from '@dxos/util';
5
+ import type { IconProps } from '@phosphor-icons/react';
6
+ import { type FC } from 'react';
7
+
8
+ import type { UnsubscribeCallback } from '@dxos/async';
9
+
10
+ import { type ActionArg, type Action, type Label } from './action';
11
+
12
+ /**
13
+ * Called when a node is added to the graph, allowing other node builders to add children, actions or properties.
14
+ */
15
+ export type NodeBuilder = (parent: Node) => UnsubscribeCallback | void;
6
16
 
7
17
  /**
8
18
  * Represents a node in the graph.
9
19
  */
10
- // TODO(wittjosiah): Use Effect Schema.
11
- export type NodeBase<TData = any, TProperties extends Record<string, any> = Record<string, any>> = {
20
+ export type Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = {
12
21
  /**
13
22
  * Globally unique ID.
14
23
  */
15
24
  id: string;
16
25
 
26
+ /**
27
+ * Parent node in the graph.
28
+ */
29
+ parent: Node | null;
30
+
31
+ /**
32
+ * Label to be used when displaying the node.
33
+ * For default labels, use a translated string.
34
+ *
35
+ * @example 'My Node'
36
+ * @example ['unknown node label, { ns: 'example-plugin' }]
37
+ */
38
+ label: Label;
39
+
40
+ /**
41
+ * Description to be used when displaying a detailed view of the node.
42
+ * For default descriptions, use a translated string.
43
+ */
44
+ description?: Label;
45
+
46
+ /**
47
+ * Icon to be used when displaying the node.
48
+ */
49
+ icon?: FC<IconProps>;
50
+
17
51
  /**
18
52
  * Properties of the node relevant to displaying the node.
19
53
  */
54
+ // TODO(burdon): Make this extensible and move label, description, and icon into here?
20
55
  properties: TProperties;
21
56
 
22
57
  /**
@@ -24,97 +59,50 @@ export type NodeBase<TData = any, TProperties extends Record<string, any> = Reco
24
59
  */
25
60
  // TODO(burdon): Type system (e.g., minimally provide identifier string vs. TypedObject vs. Graph mixin type system)?
26
61
  // type field would prevent convoluted sniffing of object properties. And allow direct pass-through for ECHO TypedObjects.
62
+ // TODO(burdon): In some places `null` is cast to TData so make optional?
27
63
  data: TData;
28
- };
29
64
 
30
- export type NodeFilter<T = any, U extends Record<string, any> = Record<string, any>> = (
31
- node: Node<unknown, Record<string, any>>,
32
- connectedNode: Node,
33
- ) => node is Node<T, U>;
34
-
35
- export type EdgeDirection = 'outbound' | 'inbound';
36
-
37
- export type ConnectedNodes = {
38
65
  /**
39
- * Edges that this node is connected to in default order.
66
+ * Children of the node stored by their id.
40
67
  */
41
- edges(params?: { direction?: EdgeDirection }): Readonly<string[]>;
68
+ // TODO(burdon): Rename nodes/nodeMap?
69
+ childrenMap: Record<string, Node>;
42
70
 
43
71
  /**
44
- * Nodes that this node is connected to in default order.
72
+ * Actions of the node stored by their id.
45
73
  */
46
- nodes<T = any, U extends Record<string, any> = Record<string, any>>(params?: {
47
- direction?: EdgeDirection;
48
- filter?: NodeFilter<T, U>;
49
- }): Node<T>[];
74
+ actionsMap: Record<string, Action>;
50
75
 
51
76
  /**
52
- * Get a specific connected node by id.
77
+ * Children of the node in default order.
53
78
  */
54
- node(id: string): Node | undefined;
55
- };
79
+ get children(): Node[];
56
80
 
57
- export type ConnectedActions = {
58
81
  /**
59
- * Actions or action groups that this node is connected to in default order.
82
+ * Actions of the node in default order.
60
83
  */
61
- actions(): ActionLike[];
62
- };
84
+ get actions(): Action[];
63
85
 
64
- export type Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = Readonly<
65
- Omit<NodeBase<TData, TProperties>, 'properties'> & { properties: Readonly<TProperties> } & ConnectedNodes &
66
- ConnectedActions
67
- >;
86
+ addProperty(key: string, value: any): void;
87
+ removeProperty(key: string): void;
68
88
 
69
- export const isGraphNode = (data: unknown): data is Node =>
70
- data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
71
- ? typeof data.properties === 'object' && 'data' in data
72
- : false;
73
-
74
- export type NodeArg<TData, TProperties extends Record<string, any> = Record<string, any>> = MakeOptional<
75
- NodeBase<TData, TProperties>,
76
- 'data' | 'properties'
77
- > & {
78
- /** Will automatically add nodes with an edge from this node to each. */
79
- nodes?: NodeArg<unknown>[];
80
-
81
- /** Will automatically add specified edges. */
82
- edges?: [string, EdgeDirection][];
83
- };
84
-
85
- //
86
- // Actions
87
- //
89
+ addNode<TChildData = null, TChildProperties extends Record<string, any> = Record<string, any>>(
90
+ id: string,
91
+ ...node: NodeArg<TChildData, TChildProperties>[]
92
+ ): Node<TChildData, TChildProperties>[];
93
+ removeNode(id: string): Node;
88
94
 
89
- export type InvokeParams = {
90
- /** Node the invoked action is connected to. */
91
- node: Node;
92
-
93
- caller?: string;
95
+ addAction<TActionProperties extends Record<string, any> = Record<string, any>>(
96
+ ...action: ActionArg<TActionProperties>[]
97
+ ): Action<TActionProperties>[];
98
+ removeAction(id: string): Action;
94
99
  };
95
100
 
96
- export type ActionData = (params: InvokeParams) => MaybePromise<void>;
97
-
98
- export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
99
- Omit<NodeBase<ActionData, TProperties>, 'properties'> & {
100
- properties: Readonly<TProperties>;
101
- } & ConnectedNodes
102
- >;
103
-
104
- export const isAction = (data: unknown): data is Action =>
105
- isGraphNode(data) ? typeof data.data === 'function' : false;
106
-
107
- export const actionGroupSymbol = Symbol('ActionGroup');
101
+ export type NodeArg<TData = null, TProperties extends Record<string, any> = Record<string, any>> = Pick<
102
+ Node,
103
+ 'id' | 'label'
104
+ > &
105
+ Partial<Omit<Node<TData, TProperties>, 'id' | 'label' | 'actions'>> & { actions?: ActionArg[] };
108
106
 
109
- export type ActionGroup = Readonly<
110
- Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
111
- properties: Readonly<Record<string, any>>;
112
- } & ConnectedActions
113
- >;
114
-
115
- export const isActionGroup = (data: unknown): data is ActionGroup =>
116
- isGraphNode(data) ? data.data === actionGroupSymbol : false;
117
-
118
- export type ActionLike = Action | ActionGroup;
119
-
120
- export const isActionLike = (data: unknown): data is Action | ActionGroup => isAction(data) || isActionGroup(data);
107
+ export const isGraphNode = (data: unknown): data is Node =>
108
+ data && typeof data === 'object' ? 'id' in data && 'label' in data : false;
@@ -11,7 +11,7 @@ import React, { useEffect, useState } from 'react';
11
11
  import { EventSubscriptions } from '@dxos/async';
12
12
  import { faker } from '@dxos/random';
13
13
  import { Client } from '@dxos/react-client';
14
- import { Expando, type Space, SpaceState } from '@dxos/react-client/echo';
14
+ import { Expando, type Space, SpaceProxy, SpaceState } from '@dxos/react-client/echo';
15
15
  import { ClientRepeater, TestBuilder } from '@dxos/react-client/testing';
16
16
  import { Button, DensityProvider, Input, Select } from '@dxos/react-ui';
17
17
  import { getSize, mx } from '@dxos/react-ui-theme';
@@ -19,8 +19,8 @@ import { withTheme } from '@dxos/storybook-utils';
19
19
  import { safeParseInt } from '@dxos/util';
20
20
 
21
21
  import { Tree } from './Tree';
22
- import { type Graph } from '../graph';
23
22
  import { GraphBuilder } from '../graph-builder';
23
+ import { type Node } from '../node';
24
24
 
25
25
  export default {
26
26
  title: 'app-graph/EchoGraph',
@@ -36,7 +36,11 @@ await client.halo.createIdentity();
36
36
  await client.spaces.create();
37
37
  await client.spaces.create();
38
38
 
39
- const spaceBuilderExtension = (graph: Graph) => {
39
+ const spaceNodeBuilder = (parent: Node) => {
40
+ if (parent.id !== 'root') {
41
+ return;
42
+ }
43
+
40
44
  const subscriptions = new EventSubscriptions();
41
45
  const { unsubscribe } = client.spaces.subscribe((spaces) => {
42
46
  subscriptions.clear();
@@ -44,23 +48,12 @@ const spaceBuilderExtension = (graph: Graph) => {
44
48
  subscriptions.add(
45
49
  effect(() => {
46
50
  if (space.state.get() === SpaceState.READY) {
47
- console.log('add space');
48
- graph.addNodes({ id: space.key.toHex(), properties: { label: space.properties.name }, data: space });
49
- graph.addEdge({ source: 'root', target: space.key.toHex() });
51
+ parent.addNode('space', { id: space.key.toHex(), label: space.properties.name, data: space });
50
52
  } else {
51
- graph.removeNode(space.key.toHex());
53
+ parent.removeNode(space.key.toHex());
52
54
  }
53
55
  }),
54
56
  );
55
-
56
- const query = space.db.query();
57
- subscriptions.add(
58
- effect(() => {
59
- query.objects.forEach((object) => {
60
- graph.addEdge({ source: space.key.toHex(), target: object.id });
61
- });
62
- }),
63
- );
64
57
  });
65
58
  });
66
59
 
@@ -70,53 +63,28 @@ const spaceBuilderExtension = (graph: Graph) => {
70
63
  };
71
64
  };
72
65
 
73
- // TODO(wittjosiah): Hypergraph query isn't working.
74
- // const objectBuilderExtension = (graph: Graph) => {
75
- // const query = client.spaces.query({ type: 'test' });
76
- // let previousObjects: Expando[] = [];
77
- // return effect(() => {
78
- // const removedObjects = previousObjects.filter((object) => !query.objects.includes(object));
79
- // previousObjects = query.objects;
80
-
81
- // removedObjects.forEach((object) => graph.removeNode(object.id));
82
- // query.objects.forEach((object) => {
83
- // console.log('add object');
84
- // graph.addNodes({ id: object.id, properties: { label: object.name }, data: object });
85
- // });
86
- // });
87
- // };
88
-
89
- const objectBuilderExtension = (graph: Graph) => {
90
- const subscriptions = new EventSubscriptions();
91
- const { unsubscribe } = client.spaces.subscribe((spaces) => {
92
- subscriptions.clear();
93
- spaces.forEach((space) => {
94
- const query = space.db.query({ type: 'test' });
95
- let previousObjects: Expando[] = [];
96
- subscriptions.add(
97
- effect(() => {
98
- const removedObjects = previousObjects.filter((object) => !query.objects.includes(object));
99
- previousObjects = query.objects;
100
-
101
- removedObjects.forEach((object) => graph.removeNode(object.id));
102
- query.objects.forEach((object) => {
103
- console.log('add object');
104
- graph.addNodes({ id: object.id, properties: { label: object.name }, data: object });
105
- });
106
- }),
107
- );
108
- });
109
- });
66
+ const objectNodeBuilder = (parent: Node) => {
67
+ if (!(parent.data instanceof SpaceProxy)) {
68
+ return;
69
+ }
110
70
 
111
- return () => {
112
- unsubscribe();
113
- subscriptions.clear();
114
- };
71
+ const space = parent.data;
72
+ const query = space.db.query({ type: 'test' });
73
+ let previousObjects: Expando[] = [];
74
+ return effect(() => {
75
+ const removedObjects = previousObjects.filter((object) => !query.objects.includes(object));
76
+ previousObjects = query.objects;
77
+
78
+ removedObjects.forEach((object) => parent.removeNode(object.id));
79
+ query.objects.forEach((expando) =>
80
+ parent.addNode('object', { id: expando.id, label: expando.name, data: expando }),
81
+ );
82
+ });
115
83
  };
116
84
 
117
85
  const graph = new GraphBuilder()
118
- .addExtension('space', spaceBuilderExtension)
119
- .addExtension('object', objectBuilderExtension)
86
+ .addNodeBuilder('space', spaceNodeBuilder)
87
+ .addNodeBuilder('object', objectNodeBuilder)
120
88
  .build();
121
89
 
122
90
  enum Action {