@dxos/app-graph 0.7.4 → 0.7.5-feature-compute.4d9d99a

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.
@@ -8,11 +8,13 @@ import { Trigger, type UnsubscribeCallback } from '@dxos/async';
8
8
  import { invariant } from '@dxos/invariant';
9
9
  import { create } from '@dxos/live-object';
10
10
  import { log } from '@dxos/log';
11
- import { isNode, type MaybePromise, nonNullable } from '@dxos/util';
11
+ import { byDisposition, type Disposition, isNode, type MaybePromise, nonNullable } from '@dxos/util';
12
12
 
13
13
  import { ACTION_GROUP_TYPE, ACTION_TYPE, Graph, ROOT_ID, type GraphParams } from './graph';
14
14
  import { type ActionData, actionGroupSymbol, type Node, type NodeArg, type Relation } from './node';
15
15
 
16
+ const NODE_RESOLVER_TIMEOUT = 1_000;
17
+
16
18
  /**
17
19
  * Graph builder extension for adding nodes to the graph based on just the node id.
18
20
  * This is useful for creating the first node in a graph or for hydrating cached nodes with data.
@@ -50,6 +52,7 @@ type GuardedNodeType<T> = T extends (value: any) => value is infer N ? (N extend
50
52
  * @param params.id The unique id of the extension.
51
53
  * @param params.relation The relation the graph is being expanded from the existing node.
52
54
  * @param params.type If provided, all nodes returned are expected to have this type.
55
+ * @param params.disposition Affects the order the extensions are processed in.
53
56
  * @param params.filter A filter function to determine if an extension should act on a node.
54
57
  * @param params.resolver A function to add nodes to the graph based on just the node id.
55
58
  * @param params.connector A function to add nodes to the graph based on a connection to an existing node.
@@ -60,6 +63,7 @@ export type CreateExtensionOptions<T = any> = {
60
63
  id: string;
61
64
  relation?: Relation;
62
65
  type?: string;
66
+ disposition?: Disposition;
63
67
  filter?: (node: Node) => node is Node<T>;
64
68
  resolver?: ResolverExtension;
65
69
  connector?: ConnectorExtension<GuardedNodeType<CreateExtensionOptions<T>['filter']>>;
@@ -71,15 +75,16 @@ export type CreateExtensionOptions<T = any> = {
71
75
  * Create a graph builder extension.
72
76
  */
73
77
  export const createExtension = <T = any>(extension: CreateExtensionOptions<T>): BuilderExtension[] => {
74
- const { id, resolver, connector, actions, actionGroups, ...rest } = extension;
78
+ const { id, disposition = 'static', resolver, connector, actions, actionGroups, ...rest } = extension;
75
79
  const getId = (key: string) => `${id}/${key}`;
76
80
  return [
77
- resolver ? { id: getId('resolver'), resolver } : undefined,
78
- connector ? { ...rest, id: getId('connector'), connector } : undefined,
81
+ resolver ? { id: getId('resolver'), disposition, resolver } : undefined,
82
+ connector ? { ...rest, id: getId('connector'), disposition, connector } : undefined,
79
83
  actionGroups
80
84
  ? ({
81
85
  ...rest,
82
86
  id: getId('actionGroups'),
87
+ disposition,
83
88
  type: ACTION_GROUP_TYPE,
84
89
  relation: 'outbound',
85
90
  connector: ({ node }) =>
@@ -90,6 +95,7 @@ export const createExtension = <T = any>(extension: CreateExtensionOptions<T>):
90
95
  ? ({
91
96
  ...rest,
92
97
  id: getId('actions'),
98
+ disposition,
93
99
  type: ACTION_TYPE,
94
100
  relation: 'outbound',
95
101
  connector: ({ node }) => actions({ node })?.map((arg) => ({ ...arg, type: ACTION_TYPE })),
@@ -166,15 +172,16 @@ export const toSignal = <T>(
166
172
  return thisSignal.value;
167
173
  };
168
174
 
169
- export type BuilderExtension = {
175
+ export type BuilderExtension = Readonly<{
170
176
  id: string;
177
+ disposition: Disposition;
171
178
  resolver?: ResolverExtension;
172
179
  connector?: ConnectorExtension;
173
180
  // Only for connector.
174
181
  relation?: Relation;
175
182
  type?: string;
176
183
  filter?: (node: Node) => boolean;
177
- };
184
+ }>;
178
185
 
179
186
  type ExtensionArg = BuilderExtension | BuilderExtension[] | ExtensionArg[];
180
187
 
@@ -221,7 +228,16 @@ export class GraphBuilder {
221
228
  .filter((id) => id !== ROOT_ID)
222
229
  .forEach((id) => (this._initialized[id] = new Trigger()));
223
230
  Object.keys(this._graph._nodes).forEach((id) => this._onInitialNode(id));
224
- await Promise.all(Object.values(this._initialized).map((trigger) => trigger.wait()));
231
+ await Promise.all(
232
+ Object.entries(this._initialized).map(async ([id, trigger]) => {
233
+ try {
234
+ await trigger.wait({ timeout: NODE_RESOLVER_TIMEOUT });
235
+ } catch {
236
+ log.error('node resolver timeout', { id });
237
+ this.graph._removeNodes([id]);
238
+ }
239
+ }),
240
+ );
225
241
  }
226
242
 
227
243
  get graph() {
@@ -310,7 +326,8 @@ export class GraphBuilder {
310
326
  this._resolverSubscriptions.set(
311
327
  nodeId,
312
328
  effect(() => {
313
- for (const { id, resolver } of Object.values(this._extensions)) {
329
+ const extensions = Object.values(this._extensions).toSorted(byDisposition);
330
+ for (const { id, resolver } of extensions) {
314
331
  if (!resolver) {
315
332
  continue;
316
333
  }
@@ -367,7 +384,8 @@ export class GraphBuilder {
367
384
 
368
385
  // TODO(wittjosiah): Consider allowing extensions to collaborate on the same node by merging their results.
369
386
  const nodes: NodeArg<any>[] = [];
370
- for (const { id, connector, filter, type, relation = 'outbound' } of Object.values(this._extensions)) {
387
+ const extensions = Object.values(this._extensions).toSorted(byDisposition);
388
+ for (const { id, connector, filter, type, relation = 'outbound' } of extensions) {
371
389
  if (
372
390
  !connector ||
373
391
  relation !== nodesRelation ||
package/src/graph.ts CHANGED
@@ -13,7 +13,7 @@ import { type MakeOptional, nonNullable, pick } from '@dxos/util';
13
13
  import { type Node, type NodeArg, type NodeFilter, type Relation, actionGroupSymbol, isActionLike } from './node';
14
14
 
15
15
  const graphSymbol = Symbol('graph');
16
- type DeepWriteable<T> = { -readonly [K in keyof T]: DeepWriteable<T[K]> };
16
+ type DeepWriteable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K] };
17
17
  type NodeInternal = DeepWriteable<Node> & { [graphSymbol]: Graph };
18
18
 
19
19
  export const getGraph = (node: Node): Graph => {
@@ -319,7 +319,6 @@ export class Graph {
319
319
 
320
320
  const nodes = this._getNodes({ node, relation, expansion });
321
321
  const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({ node: n, visitor, expansion }, path));
322
-
323
322
  return () => {
324
323
  nodeSubscriptions.forEach((unsubscribe) => unsubscribe());
325
324
  };
package/src/node.ts CHANGED
@@ -87,9 +87,9 @@ export const isAction = (data: unknown): data is Action =>
87
87
 
88
88
  export const actionGroupSymbol = Symbol('ActionGroup');
89
89
 
90
- export type ActionGroup = Readonly<
91
- Omit<Node<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
92
- properties: Readonly<Record<string, any>>;
90
+ export type ActionGroup<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
91
+ Omit<Node<typeof actionGroupSymbol, TProperties>, 'properties'> & {
92
+ properties: Readonly<TProperties>;
93
93
  }
94
94
  >;
95
95