@dxos/app-graph 0.8.4-main.b97322e → 0.8.4-main.bc674ce

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 (44) hide show
  1. package/dist/lib/browser/index.mjs +1131 -612
  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 +1130 -612
  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 +113 -60
  10. package/dist/types/src/graph-builder.d.ts.map +1 -1
  11. package/dist/types/src/graph.d.ts +183 -209
  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 +8 -10
  22. package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +37 -35
  25. package/src/atoms.ts +25 -0
  26. package/src/graph-builder.test.ts +571 -97
  27. package/src/graph-builder.ts +600 -258
  28. package/src/graph.test.ts +300 -107
  29. package/src/graph.ts +971 -400
  30. package/src/index.ts +9 -3
  31. package/src/node-matcher.test.ts +301 -0
  32. package/src/node-matcher.ts +284 -0
  33. package/src/node.ts +39 -6
  34. package/src/stories/EchoGraph.stories.tsx +119 -108
  35. package/src/stories/Tree.tsx +2 -2
  36. package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
  37. package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
  38. package/dist/types/src/signals-integration.test.d.ts +0 -2
  39. package/dist/types/src/signals-integration.test.d.ts.map +0 -1
  40. package/dist/types/src/testing.d.ts +0 -5
  41. package/dist/types/src/testing.d.ts.map +0 -1
  42. package/src/experimental/graph-projections.test.ts +0 -56
  43. package/src/signals-integration.test.ts +0 -218
  44. package/src/testing.ts +0 -20
@@ -2,39 +2,31 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type Registry, RegistryContext, Rx, useRxValue } from '@effect-rx/rx-react';
8
- import { type Meta } from '@storybook/react-vite';
9
- import { Option, pipe } from 'effect';
10
- import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
11
-
12
- import {
13
- live,
14
- isSpace,
15
- Query,
16
- type QueryResult,
17
- type Space,
18
- SpaceState,
19
- Expando,
20
- type Live,
21
- Filter,
22
- } from '@dxos/client/echo';
23
- import { Obj, Type } from '@dxos/echo';
5
+ import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import * as Function from 'effect/Function';
8
+ import * as Option from 'effect/Option';
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';
24
15
  import { faker } from '@dxos/random';
25
16
  import { type Client, useClient } from '@dxos/react-client';
26
17
  import { withClientProvider } from '@dxos/react-client/testing';
27
- import { Input, Select, Icon, IconButton } from '@dxos/react-ui';
18
+ import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
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';
30
- import { withTheme } from '@dxos/storybook-utils';
21
+ import { getSize, mx } from '@dxos/ui-theme';
31
22
  import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
32
23
 
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';
28
+
33
29
  import { JsonTree } from './Tree';
34
- import { type ExpandableGraph, ROOT_ID } from '../graph';
35
- import { GraphBuilder, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
36
- import { type Node } from '../node';
37
- import { rxFromQuery } from '../testing';
38
30
 
39
31
  const DEFAULT_PERIOD = 500;
40
32
 
@@ -56,43 +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
- Rx.make((get) =>
64
- pipe(
55
+ Atom.make((get) =>
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(rxFromObservable(client.spaces)) ?? [];
60
+ const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
69
61
  return spaces
70
- .filter((space) => get(rxFromObservable(space.state)) === SpaceState.SPACE_READY)
71
- .map((space) => ({
72
- id: space.id,
73
- type: 'dxos.org/type/Space',
74
- properties: { label: get(rxFromSignal(() => space.properties.name)) },
75
- data: space,
76
- }));
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
+ });
77
74
  }),
78
75
  Option.getOrElse(() => []),
79
76
  ),
80
77
  ),
81
78
  });
82
79
 
83
- const objectBuilderExtension = createExtension({
80
+ const objectBuilderExtension = GraphBuilder.createExtensionRaw({
84
81
  id: 'object',
85
82
  connector: (node) => {
86
- let query: QueryResult<Live<Expando>> | undefined;
87
- return Rx.make((get) =>
88
- pipe(
83
+ return Atom.make((get) =>
84
+ Function.pipe(
89
85
  get(node),
90
86
  Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
91
87
  Option.map((space) => {
92
- if (!query) {
93
- query = space.db.query(Query.type(Expando, { type: 'test' }));
94
- }
95
- const objects = get(rxFromQuery(query));
88
+ const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
96
89
  return objects.map((object) => ({
97
90
  id: object.id,
98
91
  type: 'dxos.org/type/test',
@@ -106,15 +99,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
106
99
  },
107
100
  });
108
101
 
109
- const graph = new GraphBuilder({ registry })
110
- .addExtension(spaceBuilderExtension)
111
- .addExtension(objectBuilderExtension).graph;
102
+ const builder = GraphBuilder.make({ registry });
103
+ GraphBuilder.addExtension(builder, spaceBuilderExtension);
104
+ GraphBuilder.addExtension(builder, objectBuilderExtension);
105
+ const graph = builder.graph;
112
106
  graph.onNodeChanged.on(({ id }) => {
113
- graph.expand(id);
107
+ Graph.expand(graph, id);
114
108
  });
115
- graph.expand(ROOT_ID);
109
+ Graph.expand(graph, Node.RootId);
116
110
  (window as any).graph = graph;
117
-
118
111
  return graph;
119
112
  };
120
113
 
@@ -134,9 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
134
127
  const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
135
128
  const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
136
129
  const spaceQueries = await Promise.all(
137
- 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()),
138
131
  );
139
- const spaces = readySpaces.filter((space, index) => spaceQueries[index].objects.length > 0);
132
+ const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
140
133
  return spaces[Math.floor(Math.random() * spaces.length)];
141
134
  };
142
135
 
@@ -153,19 +146,26 @@ const runAction = async (client: Client, action: Action) => {
153
146
  case Action.RENAME_SPACE: {
154
147
  const space = getRandomSpace(client);
155
148
  if (space) {
156
- space.properties.name = faker.commerce.productName();
149
+ Obj.change(space.properties, (p) => {
150
+ p.name = faker.commerce.productName();
151
+ });
157
152
  }
158
153
  break;
159
154
  }
160
155
 
161
156
  case Action.ADD_OBJECT:
162
- getRandomSpace(client)?.db.add(Obj.make(Type.Expando, { type: 'test', name: faker.commerce.productName() }));
157
+ getRandomSpace(client)?.db.add(
158
+ Obj.make(TestSchema.Expando, {
159
+ type: 'test',
160
+ name: faker.commerce.productName(),
161
+ }),
162
+ );
163
163
  break;
164
164
 
165
165
  case Action.REMOVE_OBJECT: {
166
166
  const space = await getSpaceWithObjects(client);
167
167
  if (space) {
168
- 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();
169
169
  space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
170
170
  }
171
171
  break;
@@ -174,8 +174,11 @@ const runAction = async (client: Client, action: Action) => {
174
174
  case Action.RENAME_OBJECT: {
175
175
  const space = await getSpaceWithObjects(client);
176
176
  if (space) {
177
- const { objects } = await space.db.query(Filter.type(Expando, { type: 'test' })).run();
178
- 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
+ });
179
182
  }
180
183
  break;
181
184
  }
@@ -214,7 +217,7 @@ const Controls = ({ children }: PropsWithChildren) => {
214
217
  <Input.TextInput
215
218
  autoComplete='off'
216
219
  size={5}
217
- classNames='w-[100px] text-right pie-[22px]'
220
+ classNames='is-[100px] text-right pie-[22px]'
218
221
  placeholder='Interval'
219
222
  value={actionInterval}
220
223
  onChange={({ target: { value } }) => setActionInterval(value)}
@@ -246,28 +249,30 @@ const Controls = ({ children }: PropsWithChildren) => {
246
249
  );
247
250
  };
248
251
 
249
- const meta: Meta = {
252
+ const meta = {
250
253
  title: 'sdk/app-graph/EchoGraph',
251
254
  decorators: [
255
+ withTheme,
252
256
  withClientProvider({
253
257
  createIdentity: true,
254
- onIdentityCreated: async ({ client }) => {
258
+ onCreateIdentity: async ({ client }) => {
255
259
  await client.spaces.create();
256
260
  await client.spaces.create();
257
261
  },
258
262
  }),
259
- withTheme,
260
263
  ],
261
- };
264
+ } satisfies Meta;
262
265
 
263
266
  export default meta;
264
267
 
265
- export const JsonView = {
268
+ type Story = StoryObj<typeof meta>;
269
+
270
+ export const JsonView: Story = {
266
271
  render: () => {
267
272
  const client = useClient();
268
273
  const registry = useContext(RegistryContext);
269
274
  const graph = useMemo(() => createGraph(client, registry), [client, registry]);
270
- const data = useRxValue(graph.json());
275
+ const data = useAtomValue(graph.json());
271
276
 
272
277
  return (
273
278
  <>
@@ -278,31 +283,42 @@ export const JsonView = {
278
283
  },
279
284
  };
280
285
 
281
- export const TreeView = {
286
+ export const TreeView: Story = {
282
287
  render: () => {
283
288
  const client = useClient();
284
289
  const registry = useContext(RegistryContext);
285
290
  const graph = useMemo(() => createGraph(client, registry), [client, registry]);
286
- 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
+ );
287
304
 
288
305
  const useItems = useCallback(
289
- (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
290
- const connections = useRxValue(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));
291
308
  return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
292
309
  },
293
310
  [graph],
294
311
  );
295
312
 
296
313
  const getProps = useCallback(
297
- (node: Node, path: string[]) => {
298
- const children = graph
299
- .getConnections(node.id, 'outbound')
314
+ (node: Node.Node, path: string[]) => {
315
+ const children = Graph.getConnections(graph, node.id, 'outbound')
300
316
  .map((n) => {
301
317
  // Break cycles.
302
318
  const nextPath = [...path, node.id];
303
- return nextPath.includes(n.id) ? undefined : (n as Node);
319
+ return nextPath.includes(n.id) ? undefined : (n as Node.Node);
304
320
  })
305
- .filter(isNonNullable) as Node[];
321
+ .filter(isNonNullable) as Node.Node[];
306
322
  const parentOf =
307
323
  children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
308
324
  return {
@@ -315,59 +331,54 @@ export const TreeView = {
315
331
  [graph],
316
332
  );
317
333
 
318
- const isOpen = useCallback(
319
- (_path: string[]) => {
320
- const path = Path.create(..._path);
321
- const object = state.get(path) ?? live({ open: true, current: false });
322
- if (!state.has(path)) {
323
- state.set(path, object);
324
- }
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
+ };
325
340
 
326
- return object.open;
327
- },
328
- [state],
329
- );
330
-
331
- const isCurrent = useCallback(
332
- (_path: string[]) => {
333
- const path = Path.create(..._path);
334
- const object = state.get(path) ?? live({ open: false, current: false });
335
- if (!state.has(path)) {
336
- state.set(path, object);
337
- }
341
+ const useIsOpen = (_path: string[]) => {
342
+ return useItemState(_path).open;
343
+ };
338
344
 
339
- return object.current;
340
- },
341
- [state],
342
- );
345
+ const useIsCurrent = (_path: string[]) => {
346
+ return useItemState(_path).current;
347
+ };
343
348
 
344
349
  const onOpenChange = useCallback(
345
350
  ({ path: _path, open }: { path: string[]; open: boolean }) => {
346
351
  const path = Path.create(..._path);
347
- const object = state.get(path);
348
- 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
+ }
349
357
  },
350
- [state],
358
+ [registry],
351
359
  );
352
360
 
353
361
  const onSelect = useCallback(
354
362
  ({ path: _path, current }: { path: string[]; current: boolean }) => {
355
363
  const path = Path.create(..._path);
356
- const object = state.get(path);
357
- 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
+ }
358
369
  },
359
- [state],
370
+ [registry],
360
371
  );
361
372
 
362
373
  return (
363
374
  <>
364
375
  <Controls />
365
376
  <Tree
366
- id={ROOT_ID}
377
+ id={Node.RootId}
367
378
  useItems={useItems}
368
379
  getProps={getProps}
369
- isOpen={isOpen}
370
- isCurrent={isCurrent}
380
+ useIsOpen={useIsOpen}
381
+ useIsCurrent={useIsCurrent}
371
382
  onOpenChange={onOpenChange}
372
383
  onSelect={onSelect}
373
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
 
@@ -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
  );
@@ -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 { Rx } from '@effect-rx/rx-react';
2
- import { type QueryResult } from '@dxos/echo-db';
3
- import { type AnyEchoObject } from '@dxos/echo-schema';
4
- export declare const rxFromQuery: <T extends AnyEchoObject>(query: QueryResult<T>) => Rx.Rx<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,EAAE,EAAE,MAAM,qBAAqB,CAAC;AAEzC,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,aAAa,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAUrF,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
- }