@dxos/app-graph 0.4.7-main.d28a540 → 0.4.7-main.e015b9e

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.
@@ -1,352 +1,292 @@
1
1
  // packages/sdk/app-graph/src/graph.ts
2
- import { untracked } from "@preact/signals-core";
3
2
  import { deepSignal } from "deepsignal/react";
3
+ import get from "lodash.get";
4
4
  import { invariant } from "@dxos/invariant";
5
- import { nonNullable } from "@dxos/util";
6
-
7
- // packages/sdk/app-graph/src/node.ts
8
- var isGraphNode = (data) => data && typeof data === "object" && "id" in data && "properties" in data && data.properties ? typeof data.properties === "object" && "data" in data : false;
9
- var isAction = (data) => isGraphNode(data) ? typeof data.data === "function" : false;
10
- var actionGroupSymbol = Symbol("ActionGroup");
11
- var isActionGroup = (data) => isGraphNode(data) ? data.data === actionGroupSymbol : false;
12
- var isActionLike = (data) => isAction(data) || isActionGroup(data);
13
-
14
- // packages/sdk/app-graph/src/graph.ts
15
5
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/sdk/app-graph/src/graph.ts";
16
- var ROOT_ID = "root";
17
6
  var Graph = class {
18
- constructor() {
19
- /**
20
- * @internal
21
- */
22
- this._nodes = deepSignal({
23
- [ROOT_ID]: {
24
- id: ROOT_ID,
25
- properties: {},
26
- data: null
27
- }
28
- });
29
- /**
30
- * @internal
31
- */
32
- // Key is the `${node.id}-${direction}` and value is an ordered list of node ids.
33
- // Explicit type required because TS says this is not portable.
34
- this._edges = deepSignal({});
35
- this._constructNode = (nodeBase) => {
36
- const node = {
37
- ...nodeBase,
38
- edges: ({ direction = "outbound" } = {}) => {
39
- return this._edges[this.getEdgeKey(node.id, direction)];
40
- },
41
- nodes: ({ direction, filter } = {}) => {
42
- const nodes = this._getNodes({
43
- id: node.id,
44
- direction
45
- }).filter((n) => !isActionLike(n));
46
- return filter ? nodes.filter((n) => filter(n, node)) : nodes;
47
- },
48
- node: (id) => {
49
- return this._getNodes({
50
- id
51
- }).find((node2) => node2.id === id);
52
- },
53
- actions: () => {
54
- return this._getNodes({
55
- id: node.id
56
- }).filter(isActionLike);
57
- }
7
+ constructor(_root) {
8
+ this._root = _root;
9
+ this._index = deepSignal({});
10
+ }
11
+ toJSON() {
12
+ const toLabel = (label) => Array.isArray(label) ? `${label[1].ns}[${label[0]}]` : label;
13
+ const toJSON = (node) => {
14
+ return {
15
+ id: node.id.slice(0, 16),
16
+ label: toLabel(node.label),
17
+ children: node.children.length ? node.children.map((node2) => toJSON(node2)) : void 0,
18
+ actions: node.actions.length ? node.actions.map(({ id, label }) => ({
19
+ id,
20
+ label: toLabel(label)
21
+ })) : void 0
58
22
  };
59
- return node;
60
23
  };
24
+ return toJSON(this._root);
61
25
  }
62
26
  /**
63
- * Alias for `findNode('root')`.
27
+ * The root node of the graph which is the entry point for all knowledge.
64
28
  */
65
29
  get root() {
66
- return this.findNode(ROOT_ID);
30
+ return this._root;
67
31
  }
68
32
  /**
69
- * Convert the graph to a JSON object.
33
+ * Get the path through the graph from the root to the node with the given id.
70
34
  */
71
- toJSON({ id = ROOT_ID, maxLength = 32 } = {}) {
72
- const toJSON = (node) => {
73
- const nodes = node.nodes();
74
- const obj = {
75
- id: node.id.length > maxLength ? `${node.id.slice(0, maxLength - 3)}...` : node.id
76
- };
77
- if (node.properties.label) {
78
- obj.label = node.properties.label;
79
- }
80
- if (nodes.length) {
81
- obj.nodes = nodes.map((node2) => toJSON(node2));
82
- }
83
- return obj;
84
- };
85
- const root = this.findNode(id);
86
- invariant(root, `Node not found: ${id}`, {
35
+ getPath(id) {
36
+ return this._index[id];
37
+ }
38
+ /**
39
+ * @internal
40
+ */
41
+ _setPath(id, path) {
42
+ invariant(id && path, "Invalid path.", {
87
43
  F: __dxlog_file,
88
- L: 85,
44
+ L: 82,
89
45
  S: this,
90
46
  A: [
91
- "root",
92
- "`Node not found: ${id}`"
47
+ "id && path",
48
+ "'Invalid path.'"
93
49
  ]
94
50
  });
95
- return toJSON(root);
51
+ this._index[id] = path;
96
52
  }
97
53
  /**
98
54
  * Find the node with the given id in the graph.
99
55
  */
100
56
  findNode(id) {
101
- const nodeBase = this._nodes[id];
102
- if (!nodeBase) {
57
+ const path = this.getPath(id);
58
+ if (!path) {
103
59
  return void 0;
104
60
  }
105
- return this._constructNode(nodeBase);
106
- }
107
- _getNodes({ id, direction = "outbound" }) {
108
- const edges = this._edges[this.getEdgeKey(id, direction)];
109
- if (!edges) {
110
- return [];
111
- }
112
- return edges.map((id2) => this.findNode(id2)).filter(nonNullable);
113
- }
114
- getEdgeKey(id, direction) {
115
- return `${id}-${direction}`;
116
- }
117
- /**
118
- * Add nodes to the graph.
119
- */
120
- addNodes(...nodes) {
121
- return nodes.map((node) => this._addNode(node));
122
- }
123
- _addNode({ nodes, edges, ..._node }) {
124
- return untracked(() => {
125
- const node = {
126
- data: null,
127
- properties: {},
128
- ..._node
129
- };
130
- this._nodes[node.id] = node;
131
- if (nodes) {
132
- nodes.forEach((subNode) => {
133
- this._addNode(subNode);
134
- this.addEdge({
135
- source: node.id,
136
- target: subNode.id
137
- });
138
- });
139
- }
140
- if (edges) {
141
- edges.forEach(([id, direction]) => direction === "outbound" ? this.addEdge({
142
- source: node.id,
143
- target: id
144
- }) : this.addEdge({
145
- source: id,
146
- target: node.id
147
- }));
148
- }
149
- return this._constructNode(node);
150
- });
151
- }
152
- /**
153
- * Remove nodes from the graph.
154
- *
155
- * @param id The id of the node to remove.
156
- * @param edges Whether to remove edges connected to the node from the graph as well.
157
- */
158
- removeNode(id, edges = false) {
159
- untracked(() => {
160
- const node = this.findNode(id);
161
- if (!node) {
162
- return;
163
- }
164
- if (edges) {
165
- delete this._edges[this.getEdgeKey(id, "outbound")];
166
- delete this._edges[this.getEdgeKey(id, "inbound")];
167
- this._getNodes({
168
- id
169
- }).forEach((node2) => this.removeEdge({
170
- source: id,
171
- target: node2.id
172
- }));
173
- this._getNodes({
174
- id,
175
- direction: "inbound"
176
- }).forEach((node2) => this.removeEdge({
177
- source: node2.id,
178
- target: id
179
- }));
180
- }
181
- delete this._nodes[id];
182
- });
183
- }
184
- /**
185
- * Add an edge to the graph.
186
- */
187
- addEdge({ source, target }) {
188
- untracked(() => {
189
- const outbound = this._edges[this.getEdgeKey(source, "outbound")];
190
- if (!outbound) {
191
- this._edges[this.getEdgeKey(source, "outbound")] = [
192
- target
193
- ];
194
- } else if (!outbound.includes(target)) {
195
- outbound.push(target);
196
- }
197
- const inbound = this._edges[this.getEdgeKey(target, "inbound")];
198
- if (!inbound) {
199
- this._edges[this.getEdgeKey(target, "inbound")] = [
200
- source
201
- ];
202
- } else if (!inbound.includes(source)) {
203
- inbound.push(source);
204
- }
205
- });
61
+ return path.length > 0 ? get(this._root, path) : this._root;
206
62
  }
207
63
  /**
208
- * Sort edges for a node.
209
- *
210
- * Edges not included in the sorted list are appended to the end of the list.
211
- *
212
- * @param nodeId The id of the node to sort edges for.
213
- * @param direction The direction of the edges from the node to sort.
214
- * @param edges The ordered list of edges.
64
+ * Recursive breadth-first traversal.
215
65
  */
216
- sortEdges(nodeId, direction, edges) {
217
- untracked(() => {
218
- const current = this._edges[this.getEdgeKey(nodeId, direction)];
219
- if (current) {
220
- const unsorted = current.filter((id) => !edges.includes(id)) ?? [];
221
- const sorted = edges.filter((id) => current.includes(id)) ?? [];
222
- current.splice(0, current.length, ...[
223
- ...sorted,
224
- ...unsorted
225
- ]);
226
- }
227
- });
228
- }
229
- /**
230
- * Remove an edge from the graph.
231
- */
232
- removeEdge({ source, target }) {
233
- untracked(() => {
234
- const outboundIndex = this._edges[this.getEdgeKey(source, "outbound")]?.findIndex((id) => id === target);
235
- if (outboundIndex !== -1) {
236
- this._edges[this.getEdgeKey(source, "outbound")].splice(outboundIndex, 1);
237
- }
238
- const inboundIndex = this._edges[this.getEdgeKey(target, "inbound")]?.findIndex((id) => id === source);
239
- if (inboundIndex !== -1) {
240
- this._edges[this.getEdgeKey(target, "inbound")].splice(inboundIndex, 1);
241
- }
242
- });
243
- }
244
- /**
245
- * Recursive depth-first traversal.
246
- *
247
- * @param options.node The node to start traversing from.
248
- * @param options.direction The direction to traverse graph edges.
249
- * @param options.filter A predicate to filter nodes which are passed to the `visitor` callback.
250
- * @param options.visitor A callback which is called for each node visited during traversal.
251
- */
252
- traverse({ node = this.root, direction = "outbound", filter, visitor }, path = []) {
253
- if (path.includes(node.id)) {
254
- return;
255
- }
66
+ traverse({ node = this._root, direction = "down", filter, visitor }, depth = 0) {
256
67
  if (!filter || filter(node)) {
257
- visitor?.(node, [
258
- ...path,
259
- node.id
260
- ]);
68
+ visitor?.(node, this.getPath(node.id));
261
69
  }
262
- Object.values(this._getNodes({
263
- id: node.id,
264
- direction
265
- })).forEach((child) => this.traverse({
266
- node: child,
267
- direction,
268
- filter,
269
- visitor
270
- }, [
271
- ...path,
272
- node.id
273
- ]));
274
- }
275
- /**
276
- * Get the path between two nodes in the graph.
277
- */
278
- getPath({ source = "root", target }) {
279
- const start = this.findNode(source);
280
- if (!start) {
281
- return void 0;
70
+ if (direction === "down") {
71
+ Object.values(node.children).forEach((child) => this.traverse({
72
+ node: child,
73
+ filter,
74
+ visitor
75
+ }));
76
+ } else if (direction === "up" && node.parent) {
77
+ this.traverse({
78
+ node: node.parent,
79
+ direction,
80
+ filter,
81
+ visitor
82
+ }, depth + 1);
282
83
  }
283
- let found;
284
- this.traverse({
285
- node: start,
286
- filter: () => !found,
287
- visitor: (node, path) => {
288
- if (node.id === target) {
289
- found = path;
290
- }
291
- }
292
- });
293
- return found;
294
84
  }
295
85
  };
296
86
 
297
87
  // packages/sdk/app-graph/src/graph-builder.ts
88
+ import { untracked } from "@preact/signals-core";
89
+ import { deepSignal as deepSignal2 } from "deepsignal/react";
298
90
  import { EventSubscriptions } from "@dxos/async";
91
+ import { Keyboard } from "@dxos/keyboard";
92
+ import { getHostPlatform } from "@dxos/util";
93
+ var KEY_BINDING = "KeyBinding";
299
94
  var GraphBuilder = class {
300
95
  constructor() {
301
- this._extensions = /* @__PURE__ */ new Map();
302
- this._unsubscribe = new EventSubscriptions();
96
+ this._nodeBuilders = /* @__PURE__ */ new Map();
97
+ this._unsubscribe = /* @__PURE__ */ new Map();
303
98
  }
304
99
  /**
305
100
  * Register a node builder which will be called in order to construct the graph.
306
101
  */
307
- addExtension(id, extension) {
308
- this._extensions.set(id, extension);
102
+ addNodeBuilder(id, builder) {
103
+ this._nodeBuilders.set(id, builder);
309
104
  return this;
310
105
  }
311
106
  /**
312
107
  * Remove a node builder from the graph builder.
313
108
  */
314
- removeExtension(id) {
315
- this._extensions.delete(id);
109
+ removeNodeBuilder(id) {
110
+ this._nodeBuilders.delete(id);
316
111
  return this;
317
112
  }
318
113
  /**
319
- * Construct the graph, starting by calling all registered extensions.
114
+ * Construct the graph, starting by calling all registered node builders on the root node.
115
+ * Node builders will be filtered out as they are used such that they are only used once on any given path.
320
116
  * @param previousGraph If provided, the graph will be updated in place.
117
+ * @param startingPath If provided, the graph will be updated starting at the given path.
118
+ */
119
+ build(previousGraph, startingPath = []) {
120
+ const graph = previousGraph ?? new Graph(this._createNode(() => graph, {
121
+ id: "root",
122
+ label: "Root"
123
+ }));
124
+ return this._build(graph, graph.root, startingPath);
125
+ }
126
+ /**
127
+ * Called recursively.
321
128
  */
322
- build(previousGraph) {
323
- this._unsubscribe.clear();
324
- const graph = previousGraph ?? new Graph();
325
- Array.from(this._extensions.values()).forEach((builder) => {
326
- const unsubscribe = builder(graph);
327
- unsubscribe && this._unsubscribe.add(unsubscribe);
129
+ _build(graph, node, path = [], ignoreBuilders = []) {
130
+ graph._setPath(node.id, path);
131
+ const subscriptions = this._unsubscribe.get(node.id) ?? new EventSubscriptions();
132
+ subscriptions.clear();
133
+ Array.from(this._nodeBuilders.entries()).filter(([id]) => ignoreBuilders.findIndex((ignore) => ignore === id) === -1).forEach(([_, builder]) => {
134
+ const unsubscribe = builder(node);
135
+ unsubscribe && subscriptions.add(unsubscribe);
328
136
  });
137
+ this._unsubscribe.set(node.id, subscriptions);
329
138
  return graph;
330
139
  }
331
- };
332
-
333
- // packages/sdk/app-graph/src/helpers.ts
334
- var manageNodes = ({ graph, condition, nodes, removeEdges }) => {
335
- if (condition) {
336
- return graph.addNodes(...nodes);
337
- } else {
338
- nodes.forEach(({ id }) => graph.removeNode(id, removeEdges));
140
+ _createNode(getGraph, partial, path = [], ignoreBuilders = []) {
141
+ const node = deepSignal2({
142
+ parent: null,
143
+ data: null,
144
+ properties: {},
145
+ childrenMap: {},
146
+ actionsMap: {},
147
+ // TODO(burdon): Document.
148
+ ...partial,
149
+ get children() {
150
+ return Object.values(node.childrenMap);
151
+ },
152
+ get actions() {
153
+ return Object.values(node.actionsMap);
154
+ },
155
+ //
156
+ // Properties
157
+ //
158
+ addProperty: (key, value) => {
159
+ untracked(() => {
160
+ node.properties[key] = value;
161
+ });
162
+ },
163
+ removeProperty: (key) => {
164
+ untracked(() => {
165
+ delete node.properties[key];
166
+ });
167
+ },
168
+ //
169
+ // Nodes
170
+ //
171
+ addNode: (builder, ...partials) => {
172
+ return untracked(() => {
173
+ return partials.map((partial2) => {
174
+ const builders = [
175
+ ...ignoreBuilders,
176
+ builder
177
+ ];
178
+ const childPath = [
179
+ ...path,
180
+ "childrenMap",
181
+ partial2.id
182
+ ];
183
+ const child = this._createNode(getGraph, {
184
+ ...partial2,
185
+ parent: node
186
+ }, childPath, builders);
187
+ node.childrenMap[child.id] = child;
188
+ this._build(getGraph(), child, childPath, builders);
189
+ return child;
190
+ });
191
+ });
192
+ },
193
+ removeNode: (id) => {
194
+ return untracked(() => {
195
+ const child = node.childrenMap[id];
196
+ delete node.childrenMap[id];
197
+ return child;
198
+ });
199
+ },
200
+ //
201
+ // Actions
202
+ //
203
+ addAction: (...partials) => {
204
+ return untracked(() => {
205
+ return partials.map((partial2) => {
206
+ const action = this._createAction(partial2);
207
+ let shortcut;
208
+ if (typeof action.keyBinding === "object") {
209
+ const availablePlatforms = Object.keys(action.keyBinding);
210
+ const platform = getHostPlatform();
211
+ shortcut = availablePlatforms.includes(platform) ? action.keyBinding[platform] : platform === "ios" ? action.keyBinding.macos : platform === "linux" || platform === "unknown" ? action.keyBinding.windows : void 0;
212
+ } else {
213
+ shortcut = action.keyBinding;
214
+ }
215
+ if (shortcut) {
216
+ Keyboard.singleton.getContext(path.join("/")).bind({
217
+ shortcut,
218
+ handler: () => {
219
+ action.invoke({
220
+ caller: KEY_BINDING
221
+ });
222
+ },
223
+ data: action.label
224
+ });
225
+ }
226
+ node.actionsMap[action.id] = action;
227
+ return action;
228
+ });
229
+ });
230
+ },
231
+ removeAction: (id) => {
232
+ return untracked(() => {
233
+ const action = node.actionsMap[id];
234
+ if (action.keyBinding) {
235
+ }
236
+ delete node.actionsMap[id];
237
+ return action;
238
+ });
239
+ }
240
+ });
241
+ partial.actions && partial.actions.forEach((action) => node.addAction(action));
242
+ return node;
243
+ }
244
+ _createAction(partial) {
245
+ const action = deepSignal2({
246
+ properties: {},
247
+ ...partial,
248
+ actionsMap: {},
249
+ get actions() {
250
+ return Object.values(action.actionsMap);
251
+ },
252
+ addAction: (...partials) => {
253
+ return untracked(() => {
254
+ return partials.map((partial2) => {
255
+ const subAction = this._createAction(partial2);
256
+ action.actionsMap[subAction.id] = subAction;
257
+ return subAction;
258
+ });
259
+ });
260
+ },
261
+ removeAction: (id) => {
262
+ return untracked(() => {
263
+ const subAction = action.actionsMap[id];
264
+ delete action.actionsMap[id];
265
+ return subAction;
266
+ });
267
+ },
268
+ addProperty: (key, value) => {
269
+ return untracked(() => {
270
+ action.properties[key] = value;
271
+ });
272
+ },
273
+ removeProperty: (key) => {
274
+ return untracked(() => {
275
+ delete action.properties[key];
276
+ });
277
+ }
278
+ });
279
+ partial.actions && partial.actions.forEach((subAction) => action.addAction(subAction));
280
+ return action;
339
281
  }
340
282
  };
283
+
284
+ // packages/sdk/app-graph/src/node.ts
285
+ var isGraphNode = (data) => data && typeof data === "object" ? "id" in data && "label" in data : false;
341
286
  export {
342
287
  Graph,
343
288
  GraphBuilder,
344
- ROOT_ID,
345
- actionGroupSymbol,
346
- isAction,
347
- isActionGroup,
348
- isActionLike,
349
- isGraphNode,
350
- manageNodes
289
+ KEY_BINDING,
290
+ isGraphNode
351
291
  };
352
292
  //# sourceMappingURL=index.mjs.map