@dxos/app-graph 0.6.3-main.cc41ccb → 0.6.3-main.d16c079

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
@@ -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,22 +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
- async waitForNode(id: string, timeout = NODE_TIMEOUT): Promise<Node> {
161
+ async waitForNode(id: string, timeout?: number): Promise<Node> {
162
+ const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger<Node>());
165
163
  const node = this.findNode(id);
166
164
  if (node) {
165
+ delete this._waitingForNodes[id];
167
166
  return node;
168
167
  }
169
168
 
170
- const trigger = this._waitingForNodes[id] ?? (this._waitingForNodes[id] = new Trigger<Node>());
171
- return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
169
+ if (timeout === undefined) {
170
+ return trigger.wait();
171
+ } else {
172
+ return asyncTimeout(trigger.wait(), timeout, `Node not found: ${id}`);
173
+ }
172
174
  }
173
175
 
174
176
  /**
175
177
  * Nodes that this node is connected to in default order.
176
178
  */
177
179
  nodes<T = any, U extends Record<string, any> = Record<string, any>>(node: Node, options: NodesOptions<T, U> = {}) {
178
- const { onlyLoaded, relation, filter, type } = options;
179
- const nodes = this._getNodes({ node, relation, type, onlyLoaded });
180
+ const { relation, expansion, filter, type } = options;
181
+ const nodes = this._getNodes({ node, relation, expansion, type });
180
182
  return nodes.filter((n) => untracked(() => !isActionLike(n))).filter((n) => filter?.(n, node) ?? true);
181
183
  }
182
184
 
@@ -190,13 +192,23 @@ export class Graph {
190
192
  /**
191
193
  * Actions or action groups that this node is connected to in default order.
192
194
  */
193
- actions(node: Node, { onlyLoaded }: { onlyLoaded?: boolean } = {}) {
195
+ actions(node: Node, { expansion }: { expansion?: boolean } = {}) {
194
196
  return [
195
- ...this._getNodes({ node, type: ACTION_GROUP_TYPE, onlyLoaded }),
196
- ...this._getNodes({ node, type: ACTION_TYPE, onlyLoaded }),
197
+ ...this._getNodes({ node, expansion, type: ACTION_GROUP_TYPE }),
198
+ ...this._getNodes({ node, expansion, type: ACTION_TYPE }),
197
199
  ];
198
200
  }
199
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
+
200
212
  /**
201
213
  * Recursive depth-first traversal of the graph.
202
214
  *
@@ -205,7 +217,7 @@ export class Graph {
205
217
  * @param options.visitor A callback which is called for each node visited during traversal.
206
218
  */
207
219
  traverse(
208
- { visitor, node = this.root, relation = 'outbound', onlyLoaded }: GraphTraversalOptions,
220
+ { visitor, node = this.root, relation = 'outbound', expansion }: GraphTraversalOptions,
209
221
  path: string[] = [],
210
222
  ): void {
211
223
  // Break cycles.
@@ -218,8 +230,8 @@ export class Graph {
218
230
  return;
219
231
  }
220
232
 
221
- Object.values(this._getNodes({ node, relation, onlyLoaded })).forEach((child) =>
222
- 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]),
223
235
  );
224
236
  }
225
237
 
@@ -231,7 +243,7 @@ export class Graph {
231
243
  * @param options.visitor A callback which is called for each node visited during traversal.
232
244
  */
233
245
  subscribeTraverse(
234
- { visitor, node = this.root, relation = 'outbound', onlyLoaded }: GraphTraversalOptions,
246
+ { visitor, node = this.root, relation = 'outbound', expansion }: GraphTraversalOptions,
235
247
  currentPath: string[] = [],
236
248
  ) {
237
249
  return effect(() => {
@@ -241,8 +253,8 @@ export class Graph {
241
253
  return;
242
254
  }
243
255
 
244
- const nodes = this._getNodes({ node, relation, onlyLoaded });
245
- 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));
246
258
 
247
259
  return () => {
248
260
  nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
@@ -261,7 +273,6 @@ export class Graph {
261
273
 
262
274
  let found: string[] | undefined;
263
275
  this.traverse({
264
- onlyLoaded: true,
265
276
  node: start,
266
277
  visitor: (node, path) => {
267
278
  if (found) {
@@ -361,10 +372,10 @@ export class Graph {
361
372
 
362
373
  if (edges) {
363
374
  // Remove edges from connected nodes.
364
- this._getNodes({ node, onlyLoaded: true }).forEach((node) => {
375
+ this._getNodes({ node }).forEach((node) => {
365
376
  this._removeEdge({ source: id, target: node.id });
366
377
  });
367
- this._getNodes({ node, relation: 'inbound', onlyLoaded: true }).forEach((node) => {
378
+ this._getNodes({ node, relation: 'inbound' }).forEach((node) => {
368
379
  this._removeEdge({ source: node.id, target: id });
369
380
  });
370
381
 
@@ -374,7 +385,7 @@ export class Graph {
374
385
 
375
386
  // Remove node.
376
387
  delete this._nodes[id];
377
- this._onRemoveNode?.(id);
388
+ void this._onRemoveNode?.(id);
378
389
  });
379
390
  }
380
391
 
@@ -463,27 +474,15 @@ export class Graph {
463
474
  node,
464
475
  relation = 'outbound',
465
476
  type,
466
- onlyLoaded,
477
+ expansion,
467
478
  }: {
468
479
  node: Node;
469
480
  relation?: Relation;
470
481
  type?: string;
471
- onlyLoaded?: boolean;
482
+ expansion?: boolean;
472
483
  }): Node[] {
473
- // TODO(wittjosiah): Factor out helper.
474
- const key = `${node.id}-${relation}-${type}`;
475
- const initialized = this._initialized[key];
476
- if (!initialized && !onlyLoaded && this._onInitialNodes) {
477
- const args = this._onInitialNodes(node, relation, type)?.filter((n) => !type || n.type === type);
478
- this._initialized[key] = true;
479
- if (args && args.length > 0) {
480
- const nodes = this._addNodes(args);
481
- this._addEdges(
482
- nodes.map(({ id }) =>
483
- relation === 'outbound' ? { source: node.id, target: id } : { source: id, target: node.id },
484
- ),
485
- );
486
- }
484
+ if (expansion) {
485
+ void this.expand(node, relation, type);
487
486
  }
488
487
 
489
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
  };