@dxos/app-graph 0.8.4-main.ae835ea → 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 (43) hide show
  1. package/dist/lib/browser/index.mjs +1014 -553
  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 +1013 -553
  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 +108 -66
  10. package/dist/types/src/graph-builder.d.ts.map +1 -1
  11. package/dist/types/src/graph.d.ts +182 -212
  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 +520 -104
  26. package/src/graph-builder.ts +550 -255
  27. package/src/graph.test.ts +299 -106
  28. package/src/graph.ts +964 -394
  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 +104 -95
  34. package/src/stories/Tree.tsx +2 -2
  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 -218
  43. package/src/testing.ts +0 -20
@@ -2,37 +2,29 @@
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';
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, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
34
- import { type Node } from '../node';
35
- import { rxFromQuery } 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,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) =>
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(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) =>
83
+ return Atom.make((get) =>
88
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)}
@@ -258,7 +261,7 @@ const meta = {
258
261
  },
259
262
  }),
260
263
  ],
261
- } satisfies Meta<typeof Registry>;
264
+ } satisfies Meta;
262
265
 
263
266
  export default meta;
264
267
 
@@ -269,7 +272,7 @@ export const JsonView: Story = {
269
272
  const client = useClient();
270
273
  const registry = useContext(RegistryContext);
271
274
  const graph = useMemo(() => createGraph(client, registry), [client, registry]);
272
- const data = useRxValue(graph.json());
275
+ const data = useAtomValue(graph.json());
273
276
 
274
277
  return (
275
278
  <>
@@ -285,26 +288,37 @@ export const TreeView: Story = {
285
288
  const client = useClient();
286
289
  const registry = useContext(RegistryContext);
287
290
  const graph = useMemo(() => createGraph(client, registry), [client, registry]);
288
- 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
+ );
289
304
 
290
305
  const useItems = useCallback(
291
- (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
292
- 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));
293
308
  return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
294
309
  },
295
310
  [graph],
296
311
  );
297
312
 
298
313
  const getProps = useCallback(
299
- (node: Node, path: string[]) => {
300
- const children = graph
301
- .getConnections(node.id, 'outbound')
314
+ (node: Node.Node, path: string[]) => {
315
+ const children = Graph.getConnections(graph, node.id, 'outbound')
302
316
  .map((n) => {
303
317
  // Break cycles.
304
318
  const nextPath = [...path, node.id];
305
- return nextPath.includes(n.id) ? undefined : (n as Node);
319
+ return nextPath.includes(n.id) ? undefined : (n as Node.Node);
306
320
  })
307
- .filter(isNonNullable) as Node[];
321
+ .filter(isNonNullable) as Node.Node[];
308
322
  const parentOf =
309
323
  children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
310
324
  return {
@@ -317,59 +331,54 @@ export const TreeView: Story = {
317
331
  [graph],
318
332
  );
319
333
 
320
- const isOpen = useCallback(
321
- (_path: string[]) => {
322
- const path = Path.create(..._path);
323
- const object = state.get(path) ?? live({ open: true, current: false });
324
- if (!state.has(path)) {
325
- state.set(path, object);
326
- }
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
+ };
327
340
 
328
- return object.open;
329
- },
330
- [state],
331
- );
332
-
333
- const isCurrent = useCallback(
334
- (_path: string[]) => {
335
- const path = Path.create(..._path);
336
- const object = state.get(path) ?? live({ open: false, current: false });
337
- if (!state.has(path)) {
338
- state.set(path, object);
339
- }
341
+ const useIsOpen = (_path: string[]) => {
342
+ return useItemState(_path).open;
343
+ };
340
344
 
341
- return object.current;
342
- },
343
- [state],
344
- );
345
+ const useIsCurrent = (_path: string[]) => {
346
+ return useItemState(_path).current;
347
+ };
345
348
 
346
349
  const onOpenChange = useCallback(
347
350
  ({ path: _path, open }: { path: string[]; open: boolean }) => {
348
351
  const path = Path.create(..._path);
349
- const object = state.get(path);
350
- 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
+ }
351
357
  },
352
- [state],
358
+ [registry],
353
359
  );
354
360
 
355
361
  const onSelect = useCallback(
356
362
  ({ path: _path, current }: { path: string[]; current: boolean }) => {
357
363
  const path = Path.create(..._path);
358
- const object = state.get(path);
359
- 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
+ }
360
369
  },
361
- [state],
370
+ [registry],
362
371
  );
363
372
 
364
373
  return (
365
374
  <>
366
375
  <Controls />
367
376
  <Tree
368
- id={ROOT_ID}
377
+ id={Node.RootId}
369
378
  useItems={useItems}
370
379
  getProps={getProps}
371
- isOpen={isOpen}
372
- isCurrent={isCurrent}
380
+ useIsOpen={useIsOpen}
381
+ useIsCurrent={useIsCurrent}
373
382
  onOpenChange={onOpenChange}
374
383
  onSelect={onSelect}
375
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 AnyEchoObject } from '@dxos/echo/internal';
3
- import { type QueryResult } from '@dxos/echo-db';
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,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,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
- }