@dxos/app-graph 0.4.6 → 0.4.7-main.06facb4

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