@dxos/app-graph 0.8.4-main.e8ec1fe → 0.8.4-main.ef1bc66f44

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 (43) hide show
  1. package/dist/lib/browser/index.mjs +1012 -551
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +1011 -551
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/atoms.d.ts +8 -0
  8. package/dist/types/src/atoms.d.ts.map +1 -0
  9. package/dist/types/src/graph-builder.d.ts +107 -65
  10. package/dist/types/src/graph-builder.d.ts.map +1 -1
  11. package/dist/types/src/graph.d.ts +174 -204
  12. package/dist/types/src/graph.d.ts.map +1 -1
  13. package/dist/types/src/index.d.ts +6 -3
  14. package/dist/types/src/index.d.ts.map +1 -1
  15. package/dist/types/src/node-matcher.d.ts +218 -0
  16. package/dist/types/src/node-matcher.d.ts.map +1 -0
  17. package/dist/types/src/node-matcher.test.d.ts +2 -0
  18. package/dist/types/src/node-matcher.test.d.ts.map +1 -0
  19. package/dist/types/src/node.d.ts +32 -3
  20. package/dist/types/src/node.d.ts.map +1 -1
  21. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  22. package/dist/types/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +35 -33
  24. package/src/atoms.ts +25 -0
  25. package/src/graph-builder.test.ts +494 -78
  26. package/src/graph-builder.ts +543 -250
  27. package/src/graph.test.ts +297 -104
  28. package/src/graph.ts +934 -404
  29. package/src/index.ts +9 -3
  30. package/src/node-matcher.test.ts +301 -0
  31. package/src/node-matcher.ts +284 -0
  32. package/src/node.ts +39 -6
  33. package/src/stories/EchoGraph.stories.tsx +94 -92
  34. package/src/stories/Tree.tsx +1 -1
  35. package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
  36. package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
  37. package/dist/types/src/signals-integration.test.d.ts +0 -2
  38. package/dist/types/src/signals-integration.test.d.ts.map +0 -1
  39. package/dist/types/src/testing.d.ts +0 -5
  40. package/dist/types/src/testing.d.ts.map +0 -1
  41. package/src/experimental/graph-projections.test.ts +0 -56
  42. package/src/signals-integration.test.ts +0 -219
  43. package/src/testing.ts +0 -20
@@ -6,33 +6,25 @@ import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom
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';
9
- import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
10
-
11
- import {
12
- Expando,
13
- Filter,
14
- type Live,
15
- Query,
16
- type QueryResult,
17
- type Space,
18
- SpaceState,
19
- isSpace,
20
- live,
21
- } from '@dxos/client/echo';
22
- import { Obj, Type } from '@dxos/echo';
9
+ import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
10
+
11
+ import { Filter, type Space, SpaceState, isSpace } from '@dxos/client/echo';
12
+ import { Obj, Query } from '@dxos/echo';
13
+ import { TestSchema } from '@dxos/echo/testing';
14
+ import { AtomObj, AtomQuery } from '@dxos/echo-atom';
23
15
  import { faker } from '@dxos/random';
24
16
  import { type Client, useClient } from '@dxos/react-client';
25
17
  import { withClientProvider } from '@dxos/react-client/testing';
26
18
  import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
27
19
  import { withTheme } from '@dxos/react-ui/testing';
28
20
  import { Path, Tree } from '@dxos/react-ui-list';
29
- import { getSize, mx } from '@dxos/react-ui-theme';
21
+ import { getSize, mx } from '@dxos/ui-theme';
30
22
  import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
31
23
 
32
- import { type ExpandableGraph, ROOT_ID } from '../graph';
33
- import { GraphBuilder, atomFromObservable, createExtension } from '../graph-builder';
34
- import { type Node } from '../node';
35
- import { atomFromQuery } from '../testing';
24
+ import * as CreateAtom from '../atoms';
25
+ import * as Graph from '../graph';
26
+ import * as GraphBuilder from '../graph-builder';
27
+ import * as Node from '../node';
36
28
 
37
29
  import { JsonTree } from './Tree';
38
30
 
@@ -56,45 +48,44 @@ const actionWeights = {
56
48
  [Action.RENAME_OBJECT]: 4,
57
49
  };
58
50
 
59
- const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
60
- const spaceBuilderExtension = createExtension({
51
+ const createGraph = (client: Client, registry: Registry.Registry): Graph.ExpandableGraph => {
52
+ const spaceBuilderExtension = GraphBuilder.createExtensionRaw({
61
53
  id: 'space',
62
54
  connector: (node) =>
63
55
  Atom.make((get) =>
64
56
  Function.pipe(
65
57
  get(node),
66
- Option.flatMap((node) => (node.id === ROOT_ID ? Option.some(node) : Option.none())),
58
+ Option.flatMap((node) => (node.id === Node.RootId ? Option.some(node) : Option.none())),
67
59
  Option.map(() => {
68
- const spaces = get(atomFromObservable(client.spaces)) ?? [];
60
+ const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
69
61
  return spaces
70
- .filter((space) => get(atomFromObservable(space.state)) === SpaceState.SPACE_READY)
71
- .map((space) => ({
72
- id: space.id,
73
- type: 'dxos.org/type/Space',
74
- properties: {
75
- label: get(atomFromObservable(space.properties.name)),
76
- },
77
- data: space,
78
- }));
62
+ .filter((space: any) => get(CreateAtom.fromObservable(space.state)) === SpaceState.SPACE_READY)
63
+ .map((space) => {
64
+ const propertiesSnapshot = get(AtomObj.make(space.properties));
65
+ return {
66
+ id: space.id,
67
+ type: 'dxos.org/type/Space',
68
+ properties: {
69
+ label: propertiesSnapshot.name,
70
+ },
71
+ data: space,
72
+ };
73
+ });
79
74
  }),
80
75
  Option.getOrElse(() => []),
81
76
  ),
82
77
  ),
83
78
  });
84
79
 
85
- const objectBuilderExtension = createExtension({
80
+ const objectBuilderExtension = GraphBuilder.createExtensionRaw({
86
81
  id: 'object',
87
82
  connector: (node) => {
88
- let query: QueryResult<Live<Expando>> | undefined;
89
83
  return Atom.make((get) =>
90
84
  Function.pipe(
91
85
  get(node),
92
86
  Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
93
87
  Option.map((space) => {
94
- if (!query) {
95
- query = space.db.query(Query.type(Expando, { type: 'test' }));
96
- }
97
- const objects = get(atomFromQuery(query));
88
+ const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
98
89
  return objects.map((object) => ({
99
90
  id: object.id,
100
91
  type: 'dxos.org/type/test',
@@ -108,15 +99,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
108
99
  },
109
100
  });
110
101
 
111
- const graph = new GraphBuilder({ registry })
112
- .addExtension(spaceBuilderExtension)
113
- .addExtension(objectBuilderExtension).graph;
102
+ const builder = GraphBuilder.make({ registry });
103
+ GraphBuilder.addExtension(builder, spaceBuilderExtension);
104
+ GraphBuilder.addExtension(builder, objectBuilderExtension);
105
+ const graph = builder.graph;
114
106
  graph.onNodeChanged.on(({ id }) => {
115
- graph.expand(id);
107
+ Graph.expand(graph, id);
116
108
  });
117
- graph.expand(ROOT_ID);
109
+ Graph.expand(graph, Node.RootId);
118
110
  (window as any).graph = graph;
119
-
120
111
  return graph;
121
112
  };
122
113
 
@@ -136,9 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
136
127
  const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
137
128
  const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
138
129
  const spaceQueries = await Promise.all(
139
- readySpaces.map((space) => space.db.query(Filter.type(Expando, { type: 'test' })).run()),
130
+ readySpaces.map((space) => space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run()),
140
131
  );
141
- const spaces = readySpaces.filter((space, index) => spaceQueries[index].objects.length > 0);
132
+ const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
142
133
  return spaces[Math.floor(Math.random() * spaces.length)];
143
134
  };
144
135
 
@@ -155,14 +146,16 @@ const runAction = async (client: Client, action: Action) => {
155
146
  case Action.RENAME_SPACE: {
156
147
  const space = getRandomSpace(client);
157
148
  if (space) {
158
- space.properties.name = faker.commerce.productName();
149
+ Obj.change(space.properties, (p) => {
150
+ p.name = faker.commerce.productName();
151
+ });
159
152
  }
160
153
  break;
161
154
  }
162
155
 
163
156
  case Action.ADD_OBJECT:
164
157
  getRandomSpace(client)?.db.add(
165
- Obj.make(Type.Expando, {
158
+ Obj.make(TestSchema.Expando, {
166
159
  type: 'test',
167
160
  name: faker.commerce.productName(),
168
161
  }),
@@ -172,7 +165,7 @@ const runAction = async (client: Client, action: Action) => {
172
165
  case Action.REMOVE_OBJECT: {
173
166
  const space = await getSpaceWithObjects(client);
174
167
  if (space) {
175
- const { objects } = await space.db.query(Filter.type(Expando, { type: 'test' })).run();
168
+ const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
176
169
  space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
177
170
  }
178
171
  break;
@@ -181,8 +174,11 @@ const runAction = async (client: Client, action: Action) => {
181
174
  case Action.RENAME_OBJECT: {
182
175
  const space = await getSpaceWithObjects(client);
183
176
  if (space) {
184
- const { objects } = await space.db.query(Filter.type(Expando, { type: 'test' })).run();
185
- objects[Math.floor(Math.random() * objects.length)].name = faker.commerce.productName();
177
+ const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
178
+ const object = objects[Math.floor(Math.random() * objects.length)];
179
+ Obj.change(object, (o) => {
180
+ o.name = faker.commerce.productName();
181
+ });
186
182
  }
187
183
  break;
188
184
  }
@@ -256,7 +252,7 @@ const Controls = ({ children }: PropsWithChildren) => {
256
252
  const meta = {
257
253
  title: 'sdk/app-graph/EchoGraph',
258
254
  decorators: [
259
- withTheme,
255
+ withTheme(),
260
256
  withClientProvider({
261
257
  createIdentity: true,
262
258
  onCreateIdentity: async ({ client }) => {
@@ -292,26 +288,37 @@ export const TreeView: Story = {
292
288
  const client = useClient();
293
289
  const registry = useContext(RegistryContext);
294
290
  const graph = useMemo(() => createGraph(client, registry), [client, registry]);
295
- const state = useMemo(() => new Map<string, Live<{ open: boolean; current: boolean }>>(), []);
291
+ const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
292
+
293
+ const getOrCreateState = useMemo(
294
+ () => (path: string) => {
295
+ let atom = stateRef.current.get(path);
296
+ if (!atom) {
297
+ atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
298
+ stateRef.current.set(path, atom);
299
+ }
300
+ return atom;
301
+ },
302
+ [],
303
+ );
296
304
 
297
305
  const useItems = useCallback(
298
- (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
299
- const connections = useAtomValue(graph.connections(node?.id ?? ROOT_ID));
306
+ (node?: Node.Node, options?: { disposition?: string; sort?: boolean }) => {
307
+ const connections = useAtomValue(graph.connections(node?.id ?? Node.RootId));
300
308
  return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
301
309
  },
302
310
  [graph],
303
311
  );
304
312
 
305
313
  const getProps = useCallback(
306
- (node: Node, path: string[]) => {
307
- const children = graph
308
- .getConnections(node.id, 'outbound')
314
+ (node: Node.Node, path: string[]) => {
315
+ const children = Graph.getConnections(graph, node.id, 'outbound')
309
316
  .map((n) => {
310
317
  // Break cycles.
311
318
  const nextPath = [...path, node.id];
312
- return nextPath.includes(n.id) ? undefined : (n as Node);
319
+ return nextPath.includes(n.id) ? undefined : (n as Node.Node);
313
320
  })
314
- .filter(isNonNullable) as Node[];
321
+ .filter(isNonNullable) as Node.Node[];
315
322
  const parentOf =
316
323
  children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
317
324
  return {
@@ -324,59 +331,54 @@ export const TreeView: Story = {
324
331
  [graph],
325
332
  );
326
333
 
327
- const isOpen = useCallback(
328
- (_path: string[]) => {
329
- const path = Path.create(..._path);
330
- const object = state.get(path) ?? live({ open: true, current: false });
331
- if (!state.has(path)) {
332
- state.set(path, object);
333
- }
334
-
335
- return object.open;
336
- },
337
- [state],
338
- );
334
+ // Hook that subscribes to item state via Atom.
335
+ const useItemState = (_path: string[]) => {
336
+ const path = useMemo(() => Path.create(..._path), [_path.join('~')]);
337
+ const atom = getOrCreateState(path);
338
+ return useAtomValue(atom);
339
+ };
339
340
 
340
- const isCurrent = useCallback(
341
- (_path: string[]) => {
342
- const path = Path.create(..._path);
343
- const object = state.get(path) ?? live({ open: false, current: false });
344
- if (!state.has(path)) {
345
- state.set(path, object);
346
- }
341
+ const useIsOpen = (_path: string[]) => {
342
+ return useItemState(_path).open;
343
+ };
347
344
 
348
- return object.current;
349
- },
350
- [state],
351
- );
345
+ const useIsCurrent = (_path: string[]) => {
346
+ return useItemState(_path).current;
347
+ };
352
348
 
353
349
  const onOpenChange = useCallback(
354
350
  ({ path: _path, open }: { path: string[]; open: boolean }) => {
355
351
  const path = Path.create(..._path);
356
- const object = state.get(path);
357
- object!.open = open;
352
+ const atom = stateRef.current.get(path);
353
+ if (atom) {
354
+ const prev = registry.get(atom);
355
+ registry.set(atom, { ...prev, open });
356
+ }
358
357
  },
359
- [state],
358
+ [registry],
360
359
  );
361
360
 
362
361
  const onSelect = useCallback(
363
362
  ({ path: _path, current }: { path: string[]; current: boolean }) => {
364
363
  const path = Path.create(..._path);
365
- const object = state.get(path);
366
- object!.current = current;
364
+ const atom = stateRef.current.get(path);
365
+ if (atom) {
366
+ const prev = registry.get(atom);
367
+ registry.set(atom, { ...prev, current });
368
+ }
367
369
  },
368
- [state],
370
+ [registry],
369
371
  );
370
372
 
371
373
  return (
372
374
  <>
373
375
  <Controls />
374
376
  <Tree
375
- id={ROOT_ID}
377
+ id={Node.RootId}
376
378
  useItems={useItems}
377
379
  getProps={getProps}
378
- isOpen={isOpen}
379
- isCurrent={isCurrent}
380
+ useIsOpen={useIsOpen}
381
+ useIsCurrent={useIsCurrent}
380
382
  onOpenChange={onOpenChange}
381
383
  onSelect={onSelect}
382
384
  />
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { type FC, type HTMLAttributes, useState } from 'react';
6
6
 
7
- import { mx } from '@dxos/react-ui-theme';
7
+ import { mx } from '@dxos/ui-theme';
8
8
 
9
9
  // TODO(burdon): Copied form devtools.
10
10
 
@@ -1,25 +0,0 @@
1
- type Cb = () => void;
2
- interface Query {
3
- id?: string[];
4
- typename?: string[];
5
- }
6
- /**
7
- * Lazy query result that can be executed.
8
- * Does not run without the run function being called.
9
- */
10
- interface QueryResult<T> {
11
- /**
12
- * Execute the query and subscribe to the result.
13
- * @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
14
- * @param error Called on error. `next` is never called after that.
15
- * @returns Function to dispose the query and unsubscribe.
16
- */
17
- run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
18
- }
19
- declare const QueryResult: Readonly<{
20
- fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>) => QueryResult<T>;
21
- }>;
22
- interface _Resolver<T> {
23
- query(query: Query): QueryResult<T>;
24
- }
25
- //# sourceMappingURL=graph-projections.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph-projections.test.d.ts","sourceRoot":"","sources":["../../../../src/experimental/graph-projections.test.ts"],"names":[],"mappings":"AAIA,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC;AAErB,UAAU,KAAK;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;GAGG;AACH,UAAU,WAAW,CAAC,CAAC;IACrB;;;;;OAKG;IACH,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;CACvE;AAED,QAAA,MAAM,WAAW;kBACD,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,KAAG,WAAW,CAAC,CAAC,CAAC;EAyBpF,CAAC;AAEH,UAAU,SAAS,CAAC,CAAC;IACnB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;CACrC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=signals-integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"signals-integration.test.d.ts","sourceRoot":"","sources":["../../../src/signals-integration.test.ts"],"names":[],"mappings":""}
@@ -1,5 +0,0 @@
1
- import { Atom } from '@effect-atom/atom-react';
2
- import { type AnyEchoObject } from '@dxos/echo/internal';
3
- import { type QueryResult } from '@dxos/echo-db';
4
- export declare const atomFromQuery: <T extends AnyEchoObject>(query: QueryResult<T>) => Atom.Atom<T[]>;
5
- //# sourceMappingURL=testing.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../src/testing.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE/C,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,aAAa,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAU3F,CAAC"}
@@ -1,56 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- type Cb = () => void;
6
-
7
- interface Query {
8
- id?: string[];
9
- typename?: string[];
10
- }
11
-
12
- /**
13
- * Lazy query result that can be executed.
14
- * Does not run without the run function being called.
15
- */
16
- interface QueryResult<T> {
17
- /**
18
- * Execute the query and subscribe to the result.
19
- * @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
20
- * @param error Called on error. `next` is never called after that.
21
- * @returns Function to dispose the query and unsubscribe.
22
- */
23
- run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
24
- }
25
-
26
- const QueryResult = Object.freeze({
27
- fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>): QueryResult<T> => {
28
- return {
29
- run: (onData, onError) => {
30
- const cbs: Cb[] = [];
31
- let disposed = false;
32
- const dispose = () => {
33
- cbs.forEach((cb) => cb());
34
- disposed = true;
35
- };
36
- run((cb) => (disposed ? cb() : cbs.push(cb))).then(
37
- (data) => {
38
- if (disposed) {
39
- return;
40
- }
41
- onData(data);
42
- },
43
- (err) => {
44
- dispose();
45
- onError(err);
46
- },
47
- );
48
- return dispose;
49
- },
50
- };
51
- },
52
- });
53
-
54
- interface _Resolver<T> {
55
- query(query: Query): QueryResult<T>;
56
- }
@@ -1,219 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { Atom, Registry } from '@effect-atom/atom-react';
6
- import { signal } from '@preact/signals-core';
7
- import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
8
-
9
- import { Trigger } from '@dxos/async';
10
- import { Obj, Type } from '@dxos/echo';
11
- import { Ref } from '@dxos/echo/internal';
12
- import { Filter } from '@dxos/echo-db';
13
- import { EchoTestBuilder } from '@dxos/echo-db/testing';
14
- import { registerSignalsRuntime } from '@dxos/echo-signals';
15
-
16
- import { ROOT_ID } from './graph';
17
- import { GraphBuilder, atomFromSignal, createExtension } from './graph-builder';
18
- import { atomFromQuery } from './testing';
19
-
20
- registerSignalsRuntime();
21
-
22
- const EXAMPLE_TYPE = 'dxos.org/type/example';
23
-
24
- describe('signals integration', () => {
25
- test('creating atom from signal', () => {
26
- const registry = Registry.make();
27
- const state = signal<number>(0);
28
- const value = atomFromSignal(() => state.value);
29
- const inline = Atom.make((get) => {
30
- // NOTE: This will create a new atom instance each time.
31
- // This test is verifying that this behaves the same as using a stable atom instance.
32
- // The parent will remain subscribed to one instance until the new one is created.
33
- // The old one will then be garbage collected because it is no longer referenced.
34
- const atom = atomFromSignal(() => get(value));
35
- return get(atom);
36
- });
37
-
38
- let count = 0;
39
- const cancel = registry.subscribe(value, (value) => {
40
- count = value;
41
- });
42
- onTestFinished(() => cancel());
43
-
44
- let inlineCount = 0;
45
- const inlineCancel = registry.subscribe(inline, (value) => {
46
- inlineCount = value;
47
- });
48
- onTestFinished(() => inlineCancel());
49
-
50
- registry.get(value);
51
- registry.get(inline);
52
- expect(count).to.eq(0);
53
- expect(inlineCount).to.eq(0);
54
-
55
- state.value = 1;
56
- expect(count).to.eq(1);
57
- expect(inlineCount).to.eq(1);
58
-
59
- state.value = 2;
60
- expect(count).to.eq(2);
61
- expect(inlineCount).to.eq(2);
62
- });
63
-
64
- describe('echo', () => {
65
- let dbBuilder: EchoTestBuilder;
66
-
67
- beforeEach(async () => {
68
- dbBuilder = await new EchoTestBuilder().open();
69
- });
70
-
71
- afterEach(async () => {
72
- await dbBuilder.close();
73
- });
74
-
75
- test('atom references are loaded lazily and receive signal notifications', async () => {
76
- const registry = Registry.make();
77
- await using peer = await dbBuilder.createPeer();
78
-
79
- let outerId: string;
80
- {
81
- await using db = await peer.createDatabase();
82
- const inner = db.add({ name: 'inner' });
83
- const outer = db.add({ inner: Ref.make(inner) });
84
- outerId = outer.id;
85
- await db.flush();
86
- }
87
-
88
- await peer.reload();
89
- {
90
- await using db = await peer.openLastDatabase();
91
- const outer = (await db.query(Filter.ids(outerId)).first()) as any;
92
- const innerAtom = atomFromSignal(() => outer.inner.target);
93
- const loaded = new Trigger();
94
-
95
- let count = 0;
96
- const cancel = registry.subscribe(innerAtom, (inner) => {
97
- count++;
98
- if (inner) {
99
- loaded.wake();
100
- }
101
- });
102
-
103
- onTestFinished(() => cancel());
104
-
105
- expect(registry.get(innerAtom)).to.eq(undefined);
106
- expect(count).to.eq(1);
107
-
108
- await loaded.wait();
109
- expect(registry.get(innerAtom)).to.include({ name: 'inner' });
110
- expect(count).to.eq(2);
111
- }
112
- });
113
-
114
- test('references graph builder', async () => {
115
- const registry = Registry.make();
116
- await using peer = await dbBuilder.createPeer();
117
-
118
- let outerId, innerId: string;
119
- {
120
- await using db = await peer.createDatabase();
121
- const inner = db.add({ name: 'inner' });
122
- const outer = db.add({ inner: Ref.make(inner) });
123
- innerId = inner.id;
124
- outerId = outer.id;
125
- await db.flush();
126
- }
127
-
128
- await peer.reload();
129
-
130
- {
131
- await using db = await peer.openLastDatabase();
132
- const outer = (await db.query(Filter.ids(outerId)).first()) as any;
133
- const innerAtom = atomFromSignal(() => outer.inner.target);
134
- const inner = registry.get(innerAtom);
135
- expect(inner).to.eq(undefined);
136
-
137
- const builder = new GraphBuilder({ registry });
138
- builder.addExtension(
139
- createExtension({
140
- id: 'outbound-connector',
141
- connector: () =>
142
- Atom.make((get) => {
143
- const inner = get(innerAtom) as any;
144
- return inner ? [{ id: inner.id, type: EXAMPLE_TYPE, data: inner.name }] : [];
145
- }),
146
- }),
147
- );
148
-
149
- const graph = builder.graph;
150
-
151
- const loaded = new Trigger();
152
- let count = 0;
153
- const cancel = registry.subscribe(graph.connections(ROOT_ID), (nodes) => {
154
- count++;
155
- if (nodes.length > 0) {
156
- loaded.wake();
157
- }
158
- });
159
- onTestFinished(() => cancel());
160
- registry.get(graph.connections(ROOT_ID));
161
- expect(count).to.eq(1);
162
-
163
- graph.expand(ROOT_ID);
164
- await loaded.wait();
165
- expect(count).to.eq(2);
166
-
167
- const nodes = registry.get(graph.connections(ROOT_ID));
168
- expect(nodes).has.length(1);
169
- expect(nodes[0].id).to.eq(innerId);
170
- expect(nodes[0].data).to.eq('inner');
171
- }
172
- });
173
-
174
- test('query graph builder', async () => {
175
- const registry = Registry.make();
176
- await using peer = await dbBuilder.createPeer();
177
- await using db = await peer.createDatabase();
178
- db.add(Obj.make(Type.Expando, { name: 'a' }));
179
- db.add(Obj.make(Type.Expando, { name: 'b' }));
180
-
181
- const builder = new GraphBuilder({ registry });
182
- builder.addExtension(
183
- createExtension({
184
- id: 'expando',
185
- connector: () => {
186
- const query = db.query(Filter.type(Type.Expando));
187
-
188
- return Atom.make((get) => {
189
- const objects = get(atomFromQuery(query));
190
- return objects.map((object) => ({ id: object.id, type: EXAMPLE_TYPE, data: object.name }));
191
- });
192
- },
193
- }),
194
- );
195
-
196
- const graph = builder.graph;
197
- let count = 0;
198
- const cancel = registry.subscribe(graph.connections(ROOT_ID), (nodes) => {
199
- count = nodes.length;
200
- });
201
- onTestFinished(() => cancel());
202
-
203
- registry.get(graph.connections(ROOT_ID));
204
- expect(count).to.eq(0);
205
-
206
- graph.expand(ROOT_ID);
207
- expect(count).to.eq(2);
208
-
209
- const object = db.add(Obj.make(Type.Expando, { name: 'c' }));
210
- await db.flush();
211
- expect(count).to.eq(3);
212
-
213
- // NOTE: This graph builder is not reactive to the object update.
214
- object.name = 'updated';
215
- await db.flush();
216
- expect(count).to.eq(3);
217
- });
218
- });
219
- });
package/src/testing.ts DELETED
@@ -1,20 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { Atom } from '@effect-atom/atom-react';
6
-
7
- import { type AnyEchoObject } from '@dxos/echo/internal';
8
- import { type QueryResult } from '@dxos/echo-db';
9
-
10
- export const atomFromQuery = <T extends AnyEchoObject>(query: QueryResult<T>): Atom.Atom<T[]> => {
11
- return Atom.make((get) => {
12
- const unsubscribe = query.subscribe((result) => {
13
- get.setSelf(result.objects);
14
- });
15
-
16
- get.addFinalizer(() => unsubscribe());
17
-
18
- return query.objects;
19
- });
20
- };