@dxos/app-graph 0.6.3-main.9e4e207 → 0.6.3-main.a95c491

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { batch, effect, untracked } from '@preact/signals-core';
6
6
 
7
- import { Trigger } from '@dxos/async';
7
+ import { asyncTimeout, Trigger } from '@dxos/async';
8
8
  import { type ReactiveObject, create } from '@dxos/echo-schema';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { nonNullable } from '@dxos/util';
@@ -26,12 +26,10 @@ export const ROOT_TYPE = 'dxos.org/type/GraphRoot';
26
26
  export const ACTION_TYPE = 'dxos.org/type/GraphAction';
27
27
  export const ACTION_GROUP_TYPE = 'dxos.org/type/GraphActionGroup';
28
28
 
29
- const NODE_TIMEOUT = 5_000;
30
-
31
29
  export type NodesOptions<T = any, U extends Record<string, any> = Record<string, any>> = {
32
30
  relation?: Relation;
33
31
  filter?: NodeFilter<T, U>;
34
- onlyLoaded?: boolean;
32
+ expansion?: boolean;
35
33
  type?: string;
36
34
  };
37
35
 
@@ -58,18 +56,18 @@ export type GraphTraversalOptions = {
58
56
  relation?: Relation;
59
57
 
60
58
  /**
61
- * Only traverse nodes that are already loaded.
59
+ * Allow traversal to trigger expansion of the graph via `onInitialNodes`.
62
60
  */
63
- onlyLoaded?: boolean;
61
+ expansion?: boolean;
64
62
  };
65
63
 
66
64
  /**
67
65
  * The Graph represents the structure of the application constructed via plugins.
68
66
  */
69
67
  export class Graph {
70
- private readonly _onInitialNode?: (id: string, type?: string) => NodeArg<any> | undefined;
71
- private readonly _onInitialNodes?: (node: Node, relation: Relation, type?: string) => NodeArg<any>[] | undefined;
72
- private readonly _onRemoveNode?: (id: string) => void;
68
+ private readonly _onInitialNode?: (id: string) => Promise<void>;
69
+ private readonly _onInitialNodes?: (node: Node, relation: Relation, type?: string) => Promise<void>;
70
+ private readonly _onRemoveNode?: (id: string) => Promise<void>;
73
71
 
74
72
  private readonly _waitingForNodes: Record<string, Trigger<Node>> = {};
75
73
  private readonly _initialized: Record<string, boolean> = {};
@@ -110,13 +108,9 @@ export class Graph {
110
108
  /**
111
109
  * Convert the graph to a JSON object.
112
110
  */
113
- toJSON({
114
- id = ROOT_ID,
115
- maxLength = 32,
116
- onlyLoaded = true,
117
- }: { id?: string; maxLength?: number; onlyLoaded?: boolean } = {}) {
111
+ toJSON({ id = ROOT_ID, maxLength = 32 }: { id?: string; maxLength?: number } = {}) {
118
112
  const toJSON = (node: Node, seen: string[] = []): any => {
119
- const nodes = this.nodes(node, { onlyLoaded });
113
+ const nodes = this.nodes(node);
120
114
  const obj: Record<string, any> = {
121
115
  id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id,
122
116
  type: node.type,
@@ -147,10 +141,13 @@ export class Graph {
147
141
  * If a node is not found within the graph and an `onInitialNode` callback is provided,
148
142
  * it is called with the id and type of the node, potentially initializing the node.
149
143
  */
150
- findNode(id: string, type?: string): Node | undefined {
144
+ findNode(id: string): Node | undefined {
151
145
  const existingNode = this._nodes[id];
152
- const nodeArg = !existingNode && this._onInitialNode?.(id, type);
153
- return existingNode ?? (nodeArg ? this._addNode(nodeArg) : undefined);
146
+ if (!existingNode) {
147
+ void this._onInitialNode?.(id);
148
+ }
149
+
150
+ return existingNode;
154
151
  }
155
152
 
156
153
  /**
@@ -161,21 +158,27 @@ export class Graph {
161
158
  * @param id The id of the node to wait for.
162
159
  * @param timeout The time in milliseconds to wait for the node to be added.
163
160
  */
164
- waitForNode(id: string, timeout = NODE_TIMEOUT): Promise<Node> {
165
- if (this._nodes[id]) {
166
- return Promise.resolve(this._nodes[id]);
161
+ async waitForNode(id: string, timeout?: number): Promise<Node> {
162
+ const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger<Node>());
163
+ const node = this.findNode(id);
164
+ if (node) {
165
+ delete this._waitingForNodes[id];
166
+ return node;
167
167
  }
168
168
 
169
- const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger<Node>());
170
- return trigger.wait({ timeout });
169
+ if (timeout === undefined) {
170
+ return trigger.wait();
171
+ } else {
172
+ return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
173
+ }
171
174
  }
172
175
 
173
176
  /**
174
177
  * Nodes that this node is connected to in default order.
175
178
  */
176
179
  nodes<T = any, U extends Record<string, any> = Record<string, any>>(node: Node, options: NodesOptions<T, U> = {}) {
177
- const { onlyLoaded, relation, filter, type } = options;
178
- const nodes = this._getNodes({ node, relation, type, onlyLoaded });
180
+ const { relation, expansion, filter, type } = options;
181
+ const nodes = this._getNodes({ node, relation, expansion, type });
179
182
  return nodes.filter((n) => untracked(() => !isActionLike(n))).filter((n) => filter?.(n, node) ?? true);
180
183
  }
181
184
 
@@ -189,13 +192,23 @@ export class Graph {
189
192
  /**
190
193
  * Actions or action groups that this node is connected to in default order.
191
194
  */
192
- actions(node: Node, { onlyLoaded }: { onlyLoaded?: boolean } = {}) {
195
+ actions(node: Node, { expansion }: { expansion?: boolean } = {}) {
193
196
  return [
194
- ...this._getNodes({ node, type: ACTION_GROUP_TYPE, onlyLoaded }),
195
- ...this._getNodes({ node, type: ACTION_TYPE, onlyLoaded }),
197
+ ...this._getNodes({ node, expansion, type: ACTION_GROUP_TYPE }),
198
+ ...this._getNodes({ node, expansion, type: ACTION_TYPE }),
196
199
  ];
197
200
  }
198
201
 
202
+ async expand(node: Node, relation: Relation = 'outbound', type?: string) {
203
+ // TODO(wittjosiah): Factor out helper.
204
+ const key = `${node.id}-${relation}-${type}`;
205
+ const initialized = this._initialized[key];
206
+ if (!initialized && this._onInitialNodes) {
207
+ await this._onInitialNodes(node, relation, type);
208
+ this._initialized[key] = true;
209
+ }
210
+ }
211
+
199
212
  /**
200
213
  * Recursive depth-first traversal of the graph.
201
214
  *
@@ -204,7 +217,7 @@ export class Graph {
204
217
  * @param options.visitor A callback which is called for each node visited during traversal.
205
218
  */
206
219
  traverse(
207
- { visitor, node = this.root, relation = 'outbound', onlyLoaded }: GraphTraversalOptions,
220
+ { visitor, node = this.root, relation = 'outbound', expansion }: GraphTraversalOptions,
208
221
  path: string[] = [],
209
222
  ): void {
210
223
  // Break cycles.
@@ -217,8 +230,8 @@ export class Graph {
217
230
  return;
218
231
  }
219
232
 
220
- Object.values(this._getNodes({ node, relation, onlyLoaded })).forEach((child) =>
221
- this.traverse({ node: child, relation, visitor, onlyLoaded }, [...path, node.id]),
233
+ Object.values(this._getNodes({ node, relation, expansion })).forEach((child) =>
234
+ this.traverse({ node: child, relation, visitor, expansion }, [...path, node.id]),
222
235
  );
223
236
  }
224
237
 
@@ -230,7 +243,7 @@ export class Graph {
230
243
  * @param options.visitor A callback which is called for each node visited during traversal.
231
244
  */
232
245
  subscribeTraverse(
233
- { visitor, node = this.root, relation = 'outbound', onlyLoaded }: GraphTraversalOptions,
246
+ { visitor, node = this.root, relation = 'outbound', expansion }: GraphTraversalOptions,
234
247
  currentPath: string[] = [],
235
248
  ) {
236
249
  return effect(() => {
@@ -240,8 +253,8 @@ export class Graph {
240
253
  return;
241
254
  }
242
255
 
243
- const nodes = this._getNodes({ node, relation, onlyLoaded });
244
- const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({ node: n, visitor, onlyLoaded }, path));
256
+ const nodes = this._getNodes({ node, relation, expansion });
257
+ const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({ node: n, visitor, expansion }, path));
245
258
 
246
259
  return () => {
247
260
  nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
@@ -260,7 +273,6 @@ export class Graph {
260
273
 
261
274
  let found: string[] | undefined;
262
275
  this.traverse({
263
- onlyLoaded: true,
264
276
  node: start,
265
277
  visitor: (node, path) => {
266
278
  if (found) {
@@ -360,10 +372,10 @@ export class Graph {
360
372
 
361
373
  if (edges) {
362
374
  // Remove edges from connected nodes.
363
- this._getNodes({ node, onlyLoaded: true }).forEach((node) => {
375
+ this._getNodes({ node }).forEach((node) => {
364
376
  this._removeEdge({ source: id, target: node.id });
365
377
  });
366
- this._getNodes({ node, relation: 'inbound', onlyLoaded: true }).forEach((node) => {
378
+ this._getNodes({ node, relation: 'inbound' }).forEach((node) => {
367
379
  this._removeEdge({ source: node.id, target: id });
368
380
  });
369
381
 
@@ -373,7 +385,7 @@ export class Graph {
373
385
 
374
386
  // Remove node.
375
387
  delete this._nodes[id];
376
- this._onRemoveNode?.(id);
388
+ void this._onRemoveNode?.(id);
377
389
  });
378
390
  }
379
391
 
@@ -462,27 +474,15 @@ export class Graph {
462
474
  node,
463
475
  relation = 'outbound',
464
476
  type,
465
- onlyLoaded,
477
+ expansion,
466
478
  }: {
467
479
  node: Node;
468
480
  relation?: Relation;
469
481
  type?: string;
470
- onlyLoaded?: boolean;
482
+ expansion?: boolean;
471
483
  }): Node[] {
472
- // TODO(wittjosiah): Factor out helper.
473
- const key = `${node.id}-${relation}-${type}`;
474
- const initialized = this._initialized[key];
475
- if (!initialized && !onlyLoaded && this._onInitialNodes) {
476
- const args = this._onInitialNodes(node, relation, type)?.filter((n) => !type || n.type === type);
477
- this._initialized[key] = true;
478
- if (args && args.length > 0) {
479
- const nodes = this._addNodes(args);
480
- this._addEdges(
481
- nodes.map(({ id }) =>
482
- relation === 'outbound' ? { source: node.id, target: id } : { source: id, target: node.id },
483
- ),
484
- );
485
- }
484
+ if (expansion) {
485
+ void this.expand(node, relation, type);
486
486
  }
487
487
 
488
488
  const edges = this._edges[node.id];
@@ -80,7 +80,7 @@ const spaceBuilderExtension = createExtension({
80
80
  }
81
81
 
82
82
  return spaces
83
- .filter((space) => space.state.get() === SpaceState.READY)
83
+ .filter((space) => space.state.get() === SpaceState.SPACE_READY)
84
84
  .map((space) => ({
85
85
  id: space.id,
86
86
  type: 'dxos.org/type/Space',
@@ -106,6 +106,12 @@ const objectBuilderExtension = createExtension({
106
106
 
107
107
  const graph = new GraphBuilder().addExtension(spaceBuilderExtension).addExtension(objectBuilderExtension).graph;
108
108
 
109
+ graph.subscribeTraverse({
110
+ visitor: (node) => {
111
+ void graph.expand(node);
112
+ },
113
+ });
114
+
109
115
  enum Action {
110
116
  CREATE_SPACE = 'CREATE_SPACE',
111
117
  CLOSE_SPACE = 'CLOSE_SPACE',
@@ -133,13 +139,13 @@ const randomAction = () => {
133
139
  };
134
140
 
135
141
  const getRandomSpace = (): Space | undefined => {
136
- const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.READY);
142
+ const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
137
143
  const space = spaces[Math.floor(Math.random() * spaces.length)];
138
144
  return space;
139
145
  };
140
146
 
141
147
  const getSpaceWithObjects = async (): Promise<Space | undefined> => {
142
- const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.READY);
148
+ const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
143
149
  const spaceQueries = await Promise.all(readySpaces.map((space) => space.db.query({ type: 'test' }).run()));
144
150
  const spaces = readySpaces.filter((space, index) => spaceQueries[index].objects.length > 0);
145
151
  return spaces[Math.floor(Math.random() * spaces.length)];
@@ -243,7 +249,7 @@ const EchoGraphStory = () => {
243
249
  </Select.Root>
244
250
  </DensityProvider>
245
251
  </div>
246
- <Tree data={graph.toJSON({ onlyLoaded: false })} />
252
+ <Tree data={graph.toJSON()} />
247
253
  </>
248
254
  );
249
255
  };