@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/dist/lib/browser/index.mjs +76 -85
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +75 -84
- 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 +54 -54
- package/src/stories/EchoGraph.stories.tsx +10 -4
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
|
-
|
|
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,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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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 {
|
|
178
|
-
const nodes = this._getNodes({ node, relation,
|
|
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, {
|
|
195
|
+
actions(node: Node, { expansion }: { expansion?: boolean } = {}) {
|
|
193
196
|
return [
|
|
194
|
-
...this._getNodes({ node, type: ACTION_GROUP_TYPE
|
|
195
|
-
...this._getNodes({ node, type: ACTION_TYPE
|
|
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',
|
|
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,
|
|
221
|
-
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]),
|
|
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',
|
|
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,
|
|
244
|
-
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));
|
|
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
|
|
375
|
+
this._getNodes({ node }).forEach((node) => {
|
|
364
376
|
this._removeEdge({ source: id, target: node.id });
|
|
365
377
|
});
|
|
366
|
-
this._getNodes({ node, relation: 'inbound'
|
|
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
|
-
|
|
477
|
+
expansion,
|
|
466
478
|
}: {
|
|
467
479
|
node: Node;
|
|
468
480
|
relation?: Relation;
|
|
469
481
|
type?: string;
|
|
470
|
-
|
|
482
|
+
expansion?: boolean;
|
|
471
483
|
}): Node[] {
|
|
472
|
-
|
|
473
|
-
|
|
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.
|
|
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
|
};
|