@dxos/app-graph 0.8.4-main.406dc2a → 0.8.4-main.548089c

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/src/graph.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Registry, Rx } from '@effect-rx/rx-react';
5
+ import { Atom, Registry } from '@effect-atom/atom-react';
6
6
  import * as Function from 'effect/Function';
7
7
  import * as Option from 'effect/Option';
8
8
  import * as Record from 'effect/Record';
@@ -16,7 +16,9 @@ import { type MakeOptional, isNonNullable } from '@dxos/util';
16
16
  import { type Action, type ActionGroup, type Node, type NodeArg, type Relation } from './node';
17
17
 
18
18
  const graphSymbol = Symbol('graph');
19
- type DeepWriteable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K] };
19
+ type DeepWriteable<T> = {
20
+ -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K];
21
+ };
20
22
  type NodeInternal = DeepWriteable<Node> & { [graphSymbol]: Graph };
21
23
 
22
24
  /**
@@ -79,32 +81,32 @@ export interface ReadableGraph {
79
81
  */
80
82
  toJSON(id?: string): object;
81
83
 
82
- json(id?: string): Rx.Rx<any>;
84
+ json(id?: string): Atom.Atom<any>;
83
85
 
84
86
  /**
85
- * Get the rx key for the node with the given id.
87
+ * Get the atom key for the node with the given id.
86
88
  */
87
- node(id: string): Rx.Rx<Option.Option<Node>>;
89
+ node(id: string): Atom.Atom<Option.Option<Node>>;
88
90
 
89
91
  /**
90
- * Get the rx key for the node with the given id.
92
+ * Get the atom key for the node with the given id.
91
93
  */
92
- nodeOrThrow(id: string): Rx.Rx<Node>;
94
+ nodeOrThrow(id: string): Atom.Atom<Node>;
93
95
 
94
96
  /**
95
- * Get the rx key for the connections of the node with the given id.
97
+ * Get the atom key for the connections of the node with the given id.
96
98
  */
97
- connections(id: string, relation?: Relation): Rx.Rx<Node[]>;
99
+ connections(id: string, relation?: Relation): Atom.Atom<Node[]>;
98
100
 
99
101
  /**
100
- * Get the rx key for the actions of the node with the given id.
102
+ * Get the atom key for the actions of the node with the given id.
101
103
  */
102
- actions(id: string): Rx.Rx<(Action | ActionGroup)[]>;
104
+ actions(id: string): Atom.Atom<(Action | ActionGroup)[]>;
103
105
 
104
106
  /**
105
- * Get the rx key for the edges of the node with the given id.
107
+ * Get the atom key for the edges of the node with the given id.
106
108
  */
107
- edges(id: string): Rx.Rx<Edges>;
109
+ edges(id: string): Atom.Atom<Edges>;
108
110
 
109
111
  /**
110
112
  * Alias for `getNodeOrThrow(ROOT_ID)`.
@@ -228,7 +230,10 @@ export interface WritableGraph extends ExpandableGraph {
228
230
  * The Graph represents the user interface information architecture of the application constructed via plugins.
229
231
  */
230
232
  export class Graph implements WritableGraph {
231
- readonly onNodeChanged = new Event<{ id: string; node: Option.Option<Node> }>();
233
+ readonly onNodeChanged = new Event<{
234
+ id: string;
235
+ node: Option.Option<Node>;
236
+ }>();
232
237
 
233
238
  private readonly _onExpand?: (id: string, relation: Relation) => void;
234
239
  private readonly _onInitialize?: (id: string) => Promise<void>;
@@ -239,51 +244,59 @@ export class Graph implements WritableGraph {
239
244
  private readonly _initialized = Record.empty<string, boolean>();
240
245
  private readonly _initialEdges = Record.empty<string, Edges>();
241
246
  private readonly _initialNodes = Record.fromEntries([
242
- [ROOT_ID, this._constructNode({ id: ROOT_ID, type: ROOT_TYPE, data: null, properties: {} })],
247
+ [
248
+ ROOT_ID,
249
+ this._constructNode({
250
+ id: ROOT_ID,
251
+ type: ROOT_TYPE,
252
+ data: null,
253
+ properties: {},
254
+ }),
255
+ ],
243
256
  ]);
244
257
 
245
258
  /** @internal */
246
- readonly _node = Rx.family<string, Rx.Writable<Option.Option<Node>>>((id) => {
259
+ readonly _node = Atom.family<string, Atom.Writable<Option.Option<Node>>>((id) => {
247
260
  const initial = Option.flatten(Record.get(this._initialNodes, id));
248
- return Rx.make<Option.Option<Node>>(initial).pipe(Rx.keepAlive, Rx.withLabel(`graph:node:${id}`));
261
+ return Atom.make<Option.Option<Node>>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:node:${id}`));
249
262
  });
250
263
 
251
- private readonly _nodeOrThrow = Rx.family<string, Rx.Rx<Node>>((id) => {
252
- return Rx.make((get) => {
264
+ private readonly _nodeOrThrow = Atom.family<string, Atom.Atom<Node>>((id) => {
265
+ return Atom.make((get) => {
253
266
  const node = get(this._node(id));
254
267
  invariant(Option.isSome(node), `Node not available: ${id}`);
255
268
  return node.value;
256
269
  });
257
270
  });
258
271
 
259
- private readonly _edges = Rx.family<string, Rx.Writable<Edges>>((id) => {
272
+ private readonly _edges = Atom.family<string, Atom.Writable<Edges>>((id) => {
260
273
  const initial = Record.get(this._initialEdges, id).pipe(Option.getOrElse(() => ({ inbound: [], outbound: [] })));
261
- return Rx.make<Edges>(initial).pipe(Rx.keepAlive, Rx.withLabel(`graph:edges:${id}`));
274
+ return Atom.make<Edges>(initial).pipe(Atom.keepAlive, Atom.withLabel(`graph:edges:${id}`));
262
275
  });
263
276
 
264
- // NOTE: Currently the argument to the family needs to be referentially stable for the rx to be referentially stable.
265
- // TODO(wittjosiah): Rx feature request, support for something akin to `ComplexMap` to allow for complex arguments.
266
- private readonly _connections = Rx.family<string, Rx.Rx<Node[]>>((key) => {
267
- return Rx.make((get) => {
277
+ // NOTE: Currently the argument to the family needs to be referentially stable for the atom to be referentially stable.
278
+ // TODO(wittjosiah): Atom feature request, support for something akin to `ComplexMap` to allow for complex arguments.
279
+ private readonly _connections = Atom.family<string, Atom.Atom<Node[]>>((key) => {
280
+ return Atom.make((get) => {
268
281
  const [id, relation] = key.split('$');
269
282
  const edges = get(this._edges(id));
270
283
  return edges[relation as Relation]
271
284
  .map((id) => get(this._node(id)))
272
285
  .filter(Option.isSome)
273
286
  .map((o) => o.value);
274
- }).pipe(Rx.withLabel(`graph:connections:${key}`));
287
+ }).pipe(Atom.withLabel(`graph:connections:${key}`));
275
288
  });
276
289
 
277
- private readonly _actions = Rx.family<string, Rx.Rx<(Action | ActionGroup)[]>>((id) => {
278
- return Rx.make((get) => {
290
+ private readonly _actions = Atom.family<string, Atom.Atom<(Action | ActionGroup)[]>>((id) => {
291
+ return Atom.make((get) => {
279
292
  return get(this._connections(`${id}$outbound`)).filter(
280
293
  (node) => node.type === ACTION_TYPE || node.type === ACTION_GROUP_TYPE,
281
294
  );
282
- }).pipe(Rx.withLabel(`graph:actions:${id}`));
295
+ }).pipe(Atom.withLabel(`graph:actions:${id}`));
283
296
  });
284
297
 
285
- private readonly _json = Rx.family<string, Rx.Rx<any>>((id) => {
286
- return Rx.make((get) => {
298
+ private readonly _json = Atom.family<string, Atom.Atom<any>>((id) => {
299
+ return Atom.make((get) => {
287
300
  const toJSON = (node: Node, seen: string[] = []): any => {
288
301
  const nodes = get(this.connections(node.id));
289
302
  const obj: Record<string, any> = {
@@ -307,7 +320,7 @@ export class Graph implements WritableGraph {
307
320
 
308
321
  const root = get(this.nodeOrThrow(id));
309
322
  return toJSON(root);
310
- }).pipe(Rx.withLabel(`graph:json:${id}`));
323
+ }).pipe(Atom.withLabel(`graph:json:${id}`));
311
324
  });
312
325
 
313
326
  constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphParams = {}) {
@@ -337,15 +350,15 @@ export class Graph implements WritableGraph {
337
350
  return this._json(id);
338
351
  }
339
352
 
340
- node(id: string): Rx.Rx<Option.Option<Node>> {
353
+ node(id: string): Atom.Atom<Option.Option<Node>> {
341
354
  return this._node(id);
342
355
  }
343
356
 
344
- nodeOrThrow(id: string): Rx.Rx<Node> {
357
+ nodeOrThrow(id: string): Atom.Atom<Node> {
345
358
  return this._nodeOrThrow(id);
346
359
  }
347
360
 
348
- connections(id: string, relation: Relation = 'outbound'): Rx.Rx<Node[]> {
361
+ connections(id: string, relation: Relation = 'outbound'): Atom.Atom<Node[]> {
349
362
  return this._connections(`${id}$${relation}`);
350
363
  }
351
364
 
@@ -353,7 +366,7 @@ export class Graph implements WritableGraph {
353
366
  return this._actions(id);
354
367
  }
355
368
 
356
- edges(id: string): Rx.Rx<Edges> {
369
+ edges(id: string): Atom.Atom<Edges> {
357
370
  return this._edges(id);
358
371
  }
359
372
 
@@ -401,7 +414,7 @@ export class Graph implements WritableGraph {
401
414
  }
402
415
 
403
416
  addNodes(nodes: NodeArg<any, Record<string, any>>[]): void {
404
- Rx.batch(() => {
417
+ Atom.batch(() => {
405
418
  nodes.map((node) => this.addNode(node));
406
419
  });
407
420
  }
@@ -415,10 +428,20 @@ export class Graph implements WritableGraph {
415
428
  const typeChanged = node.type !== type;
416
429
  const dataChanged = node.data !== data;
417
430
  const propertiesChanged = Object.keys(properties).some((key) => node.properties[key] !== properties[key]);
418
- log('existing node', { id, typeChanged, dataChanged, propertiesChanged });
431
+ log('existing node', {
432
+ id,
433
+ typeChanged,
434
+ dataChanged,
435
+ propertiesChanged,
436
+ });
419
437
  if (typeChanged || dataChanged || propertiesChanged) {
420
438
  log('updating node', { id, type, data, properties });
421
- const newNode = Option.some({ ...node, type, data, properties: { ...node.properties, ...properties } });
439
+ const newNode = Option.some({
440
+ ...node,
441
+ type,
442
+ data,
443
+ properties: { ...node.properties, ...properties },
444
+ });
422
445
  this._registry.set(nodeRx, newNode);
423
446
  this.onNodeChanged.emit({ id, node: newNode });
424
447
  }
@@ -432,7 +455,7 @@ export class Graph implements WritableGraph {
432
455
  });
433
456
 
434
457
  if (nodes) {
435
- // Rx.batch(() => {
458
+ // Atom.batch(() => {
436
459
  this.addNodes(nodes);
437
460
  const _edges = nodes.map((node) => ({ source: id, target: node.id }));
438
461
  this.addEdges(_edges);
@@ -445,14 +468,14 @@ export class Graph implements WritableGraph {
445
468
  }
446
469
 
447
470
  removeNodes(ids: string[], edges = false): void {
448
- Rx.batch(() => {
471
+ Atom.batch(() => {
449
472
  ids.map((id) => this.removeNode(id, edges));
450
473
  });
451
474
  }
452
475
 
453
476
  removeNode(id: string, edges = false): void {
454
477
  const nodeRx = this._node(id);
455
- // TODO(wittjosiah): Is there a way to mark these rx values for garbage collection?
478
+ // TODO(wittjosiah): Is there a way to mark these atom values for garbage collection?
456
479
  this._registry.set(nodeRx, Option.none());
457
480
  this.onNodeChanged.emit({ id, node: Option.none() });
458
481
  // TODO(wittjosiah): Reset expanded and initialized flags?
@@ -470,7 +493,7 @@ export class Graph implements WritableGraph {
470
493
  }
471
494
 
472
495
  addEdges(edges: Edge[]): void {
473
- Rx.batch(() => {
496
+ Atom.batch(() => {
474
497
  edges.map((edge) => this.addEdge(edge));
475
498
  });
476
499
  }
@@ -479,20 +502,32 @@ export class Graph implements WritableGraph {
479
502
  const sourceRx = this._edges(edgeArg.source);
480
503
  const source = this._registry.get(sourceRx);
481
504
  if (!source.outbound.includes(edgeArg.target)) {
482
- log('add outbound edge', { source: edgeArg.source, target: edgeArg.target });
483
- this._registry.set(sourceRx, { inbound: source.inbound, outbound: [...source.outbound, edgeArg.target] });
505
+ log('add outbound edge', {
506
+ source: edgeArg.source,
507
+ target: edgeArg.target,
508
+ });
509
+ this._registry.set(sourceRx, {
510
+ inbound: source.inbound,
511
+ outbound: [...source.outbound, edgeArg.target],
512
+ });
484
513
  }
485
514
 
486
515
  const targetRx = this._edges(edgeArg.target);
487
516
  const target = this._registry.get(targetRx);
488
517
  if (!target.inbound.includes(edgeArg.source)) {
489
- log('add inbound edge', { source: edgeArg.source, target: edgeArg.target });
490
- this._registry.set(targetRx, { inbound: [...target.inbound, edgeArg.source], outbound: target.outbound });
518
+ log('add inbound edge', {
519
+ source: edgeArg.source,
520
+ target: edgeArg.target,
521
+ });
522
+ this._registry.set(targetRx, {
523
+ inbound: [...target.inbound, edgeArg.source],
524
+ outbound: target.outbound,
525
+ });
491
526
  }
492
527
  }
493
528
 
494
529
  removeEdges(edges: Edge[], removeOrphans = false): void {
495
- Rx.batch(() => {
530
+ Atom.batch(() => {
496
531
  edges.map((edge) => this.removeEdge(edge, removeOrphans));
497
532
  });
498
533
  }
@@ -599,6 +634,11 @@ export class Graph implements WritableGraph {
599
634
 
600
635
  /** @internal */
601
636
  _constructNode(node: NodeArg<any>): Option.Option<Node> {
602
- return Option.some({ [graphSymbol]: this, data: null, properties: {}, ...node });
637
+ return Option.some({
638
+ [graphSymbol]: this,
639
+ data: null,
640
+ properties: {},
641
+ ...node,
642
+ });
603
643
  }
604
644
  }
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Registry, Rx } from '@effect-rx/rx-react';
5
+ import { Atom, Registry } from '@effect-atom/atom-react';
6
6
  import { signal } from '@preact/signals-core';
7
7
  import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
8
8
 
@@ -26,7 +26,7 @@ describe('signals integration', () => {
26
26
  const registry = Registry.make();
27
27
  const state = signal<number>(0);
28
28
  const value = rxFromSignal(() => state.value);
29
- const inline = Rx.make((get) => {
29
+ const inline = Atom.make((get) => {
30
30
  // NOTE: This will create a new rx instance each time.
31
31
  // This test is verifying that this behaves the same as using a stable rx instance.
32
32
  // The parent will remain subscribed to one instance until the new one is created.
@@ -138,7 +138,7 @@ describe('signals integration', () => {
138
138
  createExtension({
139
139
  id: 'outbound-connector',
140
140
  connector: () =>
141
- Rx.make((get) => {
141
+ Atom.make((get) => {
142
142
  const inner = get(innerRx) as any;
143
143
  return inner ? [{ id: inner.id, type: EXAMPLE_TYPE, data: inner.name }] : [];
144
144
  }),
@@ -184,7 +184,7 @@ describe('signals integration', () => {
184
184
  connector: () => {
185
185
  const query = db.query(Filter.type(Type.Expando));
186
186
 
187
- return Rx.make((get) => {
187
+ return Atom.make((get) => {
188
188
  const objects = get(rxFromQuery(query));
189
189
  return objects.map((object) => ({ id: object.id, type: EXAMPLE_TYPE, data: object.name }));
190
190
  });
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Registry, RegistryContext, Rx, useRxValue } from '@effect-rx/rx-react';
5
+ import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
7
7
  import * as Function from 'effect/Function';
8
8
  import * as Option from 'effect/Option';
@@ -60,7 +60,7 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
60
60
  const spaceBuilderExtension = createExtension({
61
61
  id: 'space',
62
62
  connector: (node) =>
63
- Rx.make((get) =>
63
+ Atom.make((get) =>
64
64
  Function.pipe(
65
65
  get(node),
66
66
  Option.flatMap((node) => (node.id === ROOT_ID ? Option.some(node) : Option.none())),
@@ -71,7 +71,9 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
71
71
  .map((space) => ({
72
72
  id: space.id,
73
73
  type: 'dxos.org/type/Space',
74
- properties: { label: get(rxFromSignal(() => space.properties.name)) },
74
+ properties: {
75
+ label: get(rxFromSignal(() => space.properties.name)),
76
+ },
75
77
  data: space,
76
78
  }));
77
79
  }),
@@ -84,7 +86,7 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
84
86
  id: 'object',
85
87
  connector: (node) => {
86
88
  let query: QueryResult<Live<Expando>> | undefined;
87
- return Rx.make((get) =>
89
+ return Atom.make((get) =>
88
90
  Function.pipe(
89
91
  get(node),
90
92
  Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
@@ -159,7 +161,12 @@ const runAction = async (client: Client, action: Action) => {
159
161
  }
160
162
 
161
163
  case Action.ADD_OBJECT:
162
- getRandomSpace(client)?.db.add(Obj.make(Type.Expando, { type: 'test', name: faker.commerce.productName() }));
164
+ getRandomSpace(client)?.db.add(
165
+ Obj.make(Type.Expando, {
166
+ type: 'test',
167
+ name: faker.commerce.productName(),
168
+ }),
169
+ );
163
170
  break;
164
171
 
165
172
  case Action.REMOVE_OBJECT: {
@@ -258,7 +265,7 @@ const meta = {
258
265
  },
259
266
  }),
260
267
  ],
261
- } satisfies Meta<typeof Registry>;
268
+ } satisfies Meta;
262
269
 
263
270
  export default meta;
264
271
 
@@ -269,7 +276,7 @@ export const JsonView: Story = {
269
276
  const client = useClient();
270
277
  const registry = useContext(RegistryContext);
271
278
  const graph = useMemo(() => createGraph(client, registry), [client, registry]);
272
- const data = useRxValue(graph.json());
279
+ const data = useAtomValue(graph.json());
273
280
 
274
281
  return (
275
282
  <>
@@ -289,7 +296,7 @@ export const TreeView: Story = {
289
296
 
290
297
  const useItems = useCallback(
291
298
  (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
292
- const connections = useRxValue(graph.connections(node?.id ?? ROOT_ID));
299
+ const connections = useAtomValue(graph.connections(node?.id ?? ROOT_ID));
293
300
  return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
294
301
  },
295
302
  [graph],
@@ -72,7 +72,7 @@ const Scalar: FC<{ value: any }> = ({ value }) => {
72
72
 
73
73
  const Box: FC<HTMLAttributes<HTMLDivElement>> = ({ children, className, ...props }) => {
74
74
  return (
75
- <div className={mx('flex px-2 border border-l-0 font-mono truncate', className)} {...props}>
75
+ <div className={mx('flex pli-2 border border-l-0 font-mono truncate', className)} {...props}>
76
76
  {children}
77
77
  </div>
78
78
  );
package/src/testing.ts CHANGED
@@ -2,13 +2,13 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Rx } from '@effect-rx/rx-react';
5
+ import { Atom } from '@effect-atom/atom-react';
6
6
 
7
7
  import { type AnyEchoObject } from '@dxos/echo/internal';
8
8
  import { type QueryResult } from '@dxos/echo-db';
9
9
 
10
- export const rxFromQuery = <T extends AnyEchoObject>(query: QueryResult<T>): Rx.Rx<T[]> => {
11
- return Rx.make((get) => {
10
+ export const rxFromQuery = <T extends AnyEchoObject>(query: QueryResult<T>): Atom.Atom<T[]> => {
11
+ return Atom.make((get) => {
12
12
  const unsubscribe = query.subscribe((result) => {
13
13
  get.setSelf(result.objects);
14
14
  });