@dxos/app-graph 0.10.0 → 0.11.1

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.
Files changed (57) hide show
  1. package/dist/lib/chunk-graph-builder.mjs +1901 -0
  2. package/dist/lib/chunk-graph-builder.mjs.map +1 -0
  3. package/dist/lib/index.mjs +281 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/lib/scheduler.browser.mjs +2 -0
  6. package/dist/lib/scheduler.mjs +12 -0
  7. package/dist/lib/scheduler.mjs.map +1 -0
  8. package/dist/lib/testing.mjs +28 -0
  9. package/dist/lib/testing.mjs.map +1 -0
  10. package/dist/types/src/atoms.d.ts +1 -1
  11. package/dist/types/src/atoms.d.ts.map +1 -1
  12. package/dist/types/src/graph-builder.d.ts +128 -9
  13. package/dist/types/src/graph-builder.d.ts.map +1 -1
  14. package/dist/types/src/graph.d.ts +4 -1
  15. package/dist/types/src/graph.d.ts.map +1 -1
  16. package/dist/types/src/index.d.ts +1 -0
  17. package/dist/types/src/index.d.ts.map +1 -1
  18. package/dist/types/src/node-matcher.d.ts +1 -1
  19. package/dist/types/src/node-matcher.d.ts.map +1 -1
  20. package/dist/types/src/node.d.ts +14 -2
  21. package/dist/types/src/node.d.ts.map +1 -1
  22. package/dist/types/src/path-resolution.d.ts +69 -0
  23. package/dist/types/src/path-resolution.d.ts.map +1 -0
  24. package/dist/types/src/path-resolution.test.d.ts +2 -0
  25. package/dist/types/src/path-resolution.test.d.ts.map +1 -0
  26. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  27. package/dist/types/src/testing/setup-graph-builder.d.ts +1 -1
  28. package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -1
  29. package/dist/types/src/util.test.d.ts +2 -0
  30. package/dist/types/src/util.test.d.ts.map +1 -0
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +22 -21
  33. package/src/atoms.ts +1 -1
  34. package/src/graph-builder.test.ts +21 -1
  35. package/src/graph-builder.ts +327 -58
  36. package/src/graph.test.ts +1 -1
  37. package/src/graph.ts +22 -16
  38. package/src/index.ts +1 -0
  39. package/src/node-matcher.test.ts +1 -1
  40. package/src/node-matcher.ts +1 -1
  41. package/src/node.ts +19 -2
  42. package/src/path-resolution.test.ts +520 -0
  43. package/src/path-resolution.ts +355 -0
  44. package/src/stories/EchoGraph.stories.tsx +97 -121
  45. package/src/testing/setup-graph-builder.ts +1 -1
  46. package/src/util.test.ts +85 -0
  47. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  48. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  49. package/dist/lib/neutral/chunk-YMTZ7MPW.mjs +0 -1509
  50. package/dist/lib/neutral/chunk-YMTZ7MPW.mjs.map +0 -7
  51. package/dist/lib/neutral/index.mjs +0 -40
  52. package/dist/lib/neutral/index.mjs.map +0 -7
  53. package/dist/lib/neutral/meta.json +0 -1
  54. package/dist/lib/neutral/scheduler.mjs +0 -15
  55. package/dist/lib/neutral/scheduler.mjs.map +0 -7
  56. package/dist/lib/neutral/testing/index.mjs +0 -40
  57. package/dist/lib/neutral/testing/index.mjs.map +0 -7
package/src/graph.ts CHANGED
@@ -2,11 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Atom, Registry } from '@effect-atom/atom-react';
5
+ import { Atom, Registry } from '@effect-atom/atom';
6
6
  import * as Function from 'effect/Function';
7
7
  import * as Option from 'effect/Option';
8
8
  import * as Pipeable from 'effect/Pipeable';
9
- import * as Record from 'effect/Record';
10
9
 
11
10
  import { Event, Trigger } from '@dxos/async';
12
11
  import { todo } from '@dxos/debug';
@@ -144,11 +143,11 @@ class GraphImpl implements WritableGraph {
144
143
  readonly _onRemoveNode?: GraphProps['onRemoveNode'];
145
144
 
146
145
  readonly _registry: Registry.Registry;
147
- readonly _expanded = Record.empty<string, boolean>();
146
+ readonly _expanded = new Set<string>();
148
147
  readonly _pendingExpands = new Set<string>();
149
- readonly _initialized = Record.empty<string, boolean>();
150
- readonly _initialEdges = Record.empty<string, Edges>();
151
- readonly _initialNodes = Record.fromEntries([
148
+ readonly _initialized = new Set<string>();
149
+ readonly _initialEdges = new Map<string, Edges>();
150
+ readonly _initialNodes = new Map<string, Option.Option<Node.Node>>([
152
151
  [
153
152
  Node.RootId,
154
153
  this._constructNode({
@@ -162,7 +161,7 @@ class GraphImpl implements WritableGraph {
162
161
 
163
162
  /** @internal */
164
163
  readonly _node = Atom.family<string, Atom.Writable<Option.Option<Node.Node>>>((id) => {
165
- const initial = Option.flatten(Record.get(this._initialNodes, id));
164
+ const initial = this._initialNodes.get(id) ?? Option.none();
166
165
  return Atom.make<Option.Option<Node.Node>>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
167
166
  });
168
167
 
@@ -175,7 +174,7 @@ class GraphImpl implements WritableGraph {
175
174
  });
176
175
 
177
176
  readonly _edges = Atom.family<string, Atom.Writable<Edges>>((id) => {
178
- const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({}) as Edges));
177
+ const initial = this._initialEdges.get(id) ?? ({} as Edges);
179
178
  return Atom.make<Edges>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
180
179
  });
181
180
 
@@ -243,13 +242,13 @@ class GraphImpl implements WritableGraph {
243
242
 
244
243
  if (nodes) {
245
244
  nodes.forEach((node) => {
246
- Record.set(this._initialNodes, node.id, this._constructNode(node));
245
+ this._initialNodes.set(node.id, this._constructNode(node));
247
246
  });
248
247
  }
249
248
 
250
249
  if (edges) {
251
250
  Object.entries(edges).forEach(([source, edges]) => {
252
- Record.set(this._initialEdges, source, edges);
251
+ this._initialEdges.set(source, edges);
253
252
  });
254
253
  }
255
254
  }
@@ -667,10 +666,10 @@ export function waitForPath(
667
666
  */
668
667
  const initializeImpl = async <T extends ExpandableGraph | WritableGraph>(graph: T, id: string): Promise<T> => {
669
668
  const internal = getInternal(graph);
670
- const initialized = Record.get(internal._initialized, id).pipe(Option.getOrElse(() => false));
669
+ const initialized = internal._initialized.has(id);
671
670
  log('initialize', { id, initialized });
672
671
  if (!initialized) {
673
- Record.set(internal._initialized, id, true);
672
+ internal._initialized.add(id);
674
673
  await internal._onInitialize?.(id);
675
674
  }
676
675
  return graph;
@@ -680,6 +679,9 @@ const initializeImpl = async <T extends ExpandableGraph | WritableGraph>(graph:
680
679
  * Initialize a node in the graph.
681
680
  *
682
681
  * Fires the `onInitialize` callback to provide initial data for a node.
682
+ *
683
+ * TODO(wittjosiah): Remove? No graph-builder extension declares a `resolver`, so `onInitialize` has
684
+ * nothing to run; callers expand the nodes they need explicitly.
683
685
  */
684
686
  export function initialize<T extends ExpandableGraph | WritableGraph>(graph: T, id: string): Promise<T>;
685
687
  export function initialize(id: string): <T extends ExpandableGraph | WritableGraph>(graph: T) => Promise<T>;
@@ -718,10 +720,10 @@ const expandImpl = <T extends ExpandableGraph | WritableGraph>(
718
720
  return graph;
719
721
  }
720
722
 
721
- const expanded = Record.get(internal._expanded, key).pipe(Option.getOrElse(() => false));
723
+ const expanded = internal._expanded.has(key);
722
724
  log('expand', { key, expanded });
723
725
  if (!expanded) {
724
- Record.set(internal._expanded, key, true);
726
+ internal._expanded.add(key);
725
727
  internal._onExpand?.(id, normalizedRelation);
726
728
  }
727
729
  return graph;
@@ -878,13 +880,17 @@ const addNodeImpl = <T extends WritableGraph>(graph: T, nodeArg: Node.NodeArg<an
878
880
  const typeChanged = existing.type !== type;
879
881
  const dataChanged = !shallowEqual(existing.data, data);
880
882
  const propertiesChanged = Object.keys(properties).some((key) => existing.properties[key] !== properties[key]);
883
+ // `changed` is on the visit log because counting `existing node` lines alone measures how often a
884
+ // node was re-offered, not how often it actually changed — two very different costs.
885
+ const changed = typeChanged || dataChanged || propertiesChanged;
881
886
  log('existing node', {
882
887
  id,
888
+ changed,
883
889
  typeChanged,
884
890
  dataChanged,
885
891
  propertiesChanged,
886
892
  });
887
- if (typeChanged || dataChanged || propertiesChanged) {
893
+ if (changed) {
888
894
  log('updating node', { id, type, data, properties });
889
895
  const newNode = Option.some({
890
896
  ...existing,
@@ -908,7 +914,7 @@ const addNodeImpl = <T extends WritableGraph>(graph: T, nodeArg: Node.NodeArg<an
908
914
  for (const pendingKey of toApply) {
909
915
  internal._pendingExpands.delete(pendingKey);
910
916
  const relation = relationFromKey(primaryParts(pendingKey)[1]);
911
- Record.set(internal._expanded, pendingKey, true);
917
+ internal._expanded.add(pendingKey);
912
918
  internal._onExpand?.(id, relation);
913
919
  }
914
920
  },
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * as Graph from './graph';
8
8
  export * as GraphBuilder from './graph-builder';
9
9
  export * as Node from './node';
10
10
  export * as NodeMatcher from './node-matcher';
11
+ export * as PathResolution from './path-resolution';
11
12
  export { getParentId, getSegmentId, qualifyId } from './util';
12
13
 
13
14
  // TODO(wittjosiah): Direct re-export needed for portable type references.
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Atom, Registry } from '@effect-atom/atom-react';
5
+ import { Atom, Registry } from '@effect-atom/atom';
6
6
  import * as Option from 'effect/Option';
7
7
  import { describe, expect, test } from 'vitest';
8
8
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Atom } from '@effect-atom/atom-react';
5
+ import { Atom } from '@effect-atom/atom';
6
6
  import * as Option from 'effect/Option';
7
7
 
8
8
  import { Entity, Obj, type Type } from '@dxos/echo';
package/src/node.ts CHANGED
@@ -95,8 +95,9 @@ export type NodeArg<TData, TProperties extends Record<string, any> = Record<stri
95
95
  /** Will automatically add nodes with an edge from this node to each. */
96
96
  nodes?: NodeArg<unknown>[];
97
97
 
98
- /** Will automatically add actions with an edge from this node to each. */
99
- actions?: NodeArg<ActionData<any>>[];
98
+ /** Will automatically add actions with an edge from this node to each. An action child may itself
99
+ * be an action group (e.g. a toolbar dropdown group), so groups are accepted alongside actions. */
100
+ actions?: NodeArg<ActionData<any> | typeof actionGroupSymbol>[];
100
101
 
101
102
  /** Will automatically add specified edges. */
102
103
  edges?: [string, RelationInput][];
@@ -114,6 +115,9 @@ export type InvokeProps = {
114
115
  path?: string[];
115
116
 
116
117
  caller?: string;
118
+
119
+ /** Input modifiers held during the gesture that triggered the action (e.g. shift-clicking a menu item). */
120
+ modifiers?: { shift?: boolean };
117
121
  };
118
122
 
119
123
  /**
@@ -154,6 +158,19 @@ export type ActionLike = Action | ActionGroup;
154
158
 
155
159
  export const isActionLike = (data: unknown): data is Action | ActionGroup => isAction(data) || isActionGroup(data);
156
160
 
161
+ /**
162
+ * Tests whether a node's `disposition` property (a single string or an array, letting one node opt
163
+ * into multiple surfaces at once) includes any of `key`. Every surface that routes nodes/actions by
164
+ * disposition (toolbar, nav-tree list-item, sigil menu, …) should filter through this rather than
165
+ * comparing `properties.disposition` directly, so a node can multi-target surfaces.
166
+ */
167
+ export const hasDisposition = (node: Pick<Node, 'properties'>, key: string | string[]): boolean => {
168
+ const disposition = node.properties.disposition;
169
+ const dispositions = Array.isArray(disposition) ? disposition : disposition !== undefined ? [disposition] : [];
170
+ const keys = Array.isArray(key) ? key : [key];
171
+ return dispositions.some((candidate) => keys.includes(candidate));
172
+ };
173
+
157
174
  //
158
175
  // Node Factories
159
176
  //