@dxos/app-graph 0.6.3-main.cc41ccb → 0.6.3-main.d76104f
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/index.mjs +72 -80
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +72 -80
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +10 -10
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/package.json +13 -13
- package/src/graph-builder.test.ts +30 -18
- package/src/graph-builder.ts +30 -35
- package/src/graph.ts +50 -51
- package/src/stories/EchoGraph.stories.tsx +10 -4
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
|
-
|
|
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
|
-
*
|
|
59
|
+
* Allow traversal to trigger expansion of the graph via `onInitialNodes`.
|
|
62
60
|
*/
|
|
63
|
-
|
|
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
|
|
71
|
-
private readonly _onInitialNodes?: (node: Node, relation: Relation, type?: string) =>
|
|
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
|
|
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
|
|
144
|
+
findNode(id: string): Node | undefined {
|
|
151
145
|
const existingNode = this._nodes[id];
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
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 {
|
|
179
|
-
const nodes = this._getNodes({ node, relation,
|
|
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, {
|
|
195
|
+
actions(node: Node, { expansion }: { expansion?: boolean } = {}) {
|
|
194
196
|
return [
|
|
195
|
-
...this._getNodes({ node, type: ACTION_GROUP_TYPE
|
|
196
|
-
...this._getNodes({ node, type: ACTION_TYPE
|
|
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',
|
|
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,
|
|
222
|
-
this.traverse({ node: child, relation, visitor,
|
|
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',
|
|
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,
|
|
245
|
-
const nodeSubscriptions = nodes.map((n) => this.subscribeTraverse({ node: n, visitor,
|
|
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
|
|
375
|
+
this._getNodes({ node }).forEach((node) => {
|
|
365
376
|
this._removeEdge({ source: id, target: node.id });
|
|
366
377
|
});
|
|
367
|
-
this._getNodes({ node, relation: 'inbound'
|
|
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
|
-
|
|
477
|
+
expansion,
|
|
467
478
|
}: {
|
|
468
479
|
node: Node;
|
|
469
480
|
relation?: Relation;
|
|
470
481
|
type?: string;
|
|
471
|
-
|
|
482
|
+
expansion?: boolean;
|
|
472
483
|
}): Node[] {
|
|
473
|
-
|
|
474
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
252
|
+
<Tree data={graph.toJSON()} />
|
|
247
253
|
</>
|
|
248
254
|
);
|
|
249
255
|
};
|