@dxos/app-graph 0.6.3-main.40d1cec → 0.6.3-main.9e4e207

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/node.ts CHANGED
@@ -8,16 +8,21 @@ import { type MaybePromise, type MakeOptional } from '@dxos/util';
8
8
  * Represents a node in the graph.
9
9
  */
10
10
  // TODO(wittjosiah): Use Effect Schema.
11
- export type NodeBase<TData = any, TProperties extends Record<string, any> = Record<string, any>> = {
11
+ export type Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = Readonly<{
12
12
  /**
13
13
  * Globally unique ID.
14
14
  */
15
15
  id: string;
16
16
 
17
+ /**
18
+ * Typename of the data the node represents.
19
+ */
20
+ type: string;
21
+
17
22
  /**
18
23
  * Properties of the node relevant to displaying the node.
19
24
  */
20
- properties: TProperties;
25
+ properties: Readonly<TProperties>;
21
26
 
22
27
  /**
23
28
  * Data the node represents.
@@ -25,46 +30,14 @@ export type NodeBase<TData = any, TProperties extends Record<string, any> = Reco
25
30
  // TODO(burdon): Type system (e.g., minimally provide identifier string vs. TypedObject vs. Graph mixin type system)?
26
31
  // type field would prevent convoluted sniffing of object properties. And allow direct pass-through for ECHO TypedObjects.
27
32
  data: TData;
28
- };
33
+ }>;
29
34
 
30
35
  export type NodeFilter<T = any, U extends Record<string, any> = Record<string, any>> = (
31
36
  node: Node<unknown, Record<string, any>>,
32
37
  connectedNode: Node,
33
38
  ) => node is Node<T, U>;
34
39
 
35
- export type EdgeDirection = 'outbound' | 'inbound';
36
-
37
- export type ConnectedNodes = {
38
- /**
39
- * Edges that this node is connected to in default order.
40
- */
41
- edges(params?: { direction?: EdgeDirection }): Readonly<string[]>;
42
-
43
- /**
44
- * Nodes that this node is connected to in default order.
45
- */
46
- nodes<T = any, U extends Record<string, any> = Record<string, any>>(params?: {
47
- direction?: EdgeDirection;
48
- filter?: NodeFilter<T, U>;
49
- }): Node<T>[];
50
-
51
- /**
52
- * Get a specific connected node by id.
53
- */
54
- node(id: string): Node | undefined;
55
- };
56
-
57
- export type ConnectedActions = {
58
- /**
59
- * Actions or action groups that this node is connected to in default order.
60
- */
61
- actions(): ActionLike[];
62
- };
63
-
64
- export type Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = Readonly<
65
- Omit<NodeBase<TData, TProperties>, 'properties'> & { properties: Readonly<TProperties> } & ConnectedNodes &
66
- ConnectedActions
67
- >;
40
+ export type Relation = 'outbound' | 'inbound';
68
41
 
69
42
  export const isGraphNode = (data: unknown): data is Node =>
70
43
  data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
@@ -72,14 +45,14 @@ export const isGraphNode = (data: unknown): data is Node =>
72
45
  : false;
73
46
 
74
47
  export type NodeArg<TData, TProperties extends Record<string, any> = Record<string, any>> = MakeOptional<
75
- NodeBase<TData, TProperties>,
48
+ Node<TData, TProperties>,
76
49
  'data' | 'properties'
77
50
  > & {
78
51
  /** Will automatically add nodes with an edge from this node to each. */
79
52
  nodes?: NodeArg<unknown>[];
80
53
 
81
54
  /** Will automatically add specified edges. */
82
- edges?: [string, EdgeDirection][];
55
+ edges?: [string, Relation][];
83
56
  };
84
57
 
85
58
  //
@@ -96,9 +69,9 @@ export type InvokeParams = {
96
69
  export type ActionData = (params: InvokeParams) => MaybePromise<void>;
97
70
 
98
71
  export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
99
- Omit<NodeBase<ActionData, TProperties>, 'properties'> & {
72
+ Omit<Node<ActionData, TProperties>, 'properties'> & {
100
73
  properties: Readonly<TProperties>;
101
- } & ConnectedNodes
74
+ }
102
75
  >;
103
76
 
104
77
  export const isAction = (data: unknown): data is Action =>
@@ -107,9 +80,9 @@ export const isAction = (data: unknown): data is Action =>
107
80
  export const actionGroupSymbol = Symbol('ActionGroup');
108
81
 
109
82
  export type ActionGroup = Readonly<
110
- Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
83
+ Omit<Node<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
111
84
  properties: Readonly<Record<string, any>>;
112
- } & ConnectedActions
85
+ }
113
86
  >;
114
87
 
115
88
  export const isActionGroup = (data: unknown): data is ActionGroup =>
@@ -5,15 +5,21 @@
5
5
  import '@dxosTheme';
6
6
 
7
7
  import { Pause, Play, Plus, Timer } from '@phosphor-icons/react';
8
- import { effect } from '@preact/signals-core';
9
8
  import React, { useEffect, useState } from 'react';
10
9
 
11
- import { EventSubscriptions } from '@dxos/async';
12
- import { create, type EchoReactiveObject } from '@dxos/echo-schema';
10
+ import { type EchoReactiveObject, create } from '@dxos/echo-schema';
13
11
  import { registerSignalRuntime } from '@dxos/echo-signals';
14
12
  import { faker } from '@dxos/random';
15
13
  import { Client } from '@dxos/react-client';
16
- import { type Space, SpaceState } from '@dxos/react-client/echo';
14
+ import {
15
+ type Space,
16
+ SpaceState,
17
+ isSpace,
18
+ type Echo,
19
+ type FilterSource,
20
+ type QueryOptions,
21
+ type Query,
22
+ } from '@dxos/react-client/echo';
17
23
  import { ClientRepeater, TestBuilder } from '@dxos/react-client/testing';
18
24
  import { Button, DensityProvider, Input, Select } from '@dxos/react-ui';
19
25
  import { getSize, mx } from '@dxos/react-ui-theme';
@@ -21,8 +27,8 @@ import { withTheme } from '@dxos/storybook-utils';
21
27
  import { safeParseInt } from '@dxos/util';
22
28
 
23
29
  import { Tree } from './Tree';
24
- import { type Graph } from '../graph';
25
- import { GraphBuilder } from '../graph-builder';
30
+ import { GraphBuilder, cleanup, createExtension, memoize, toSignal } from '../graph-builder';
31
+ import { type Node } from '../node';
26
32
 
27
33
  export default {
28
34
  title: 'app-graph/EchoGraph',
@@ -39,89 +45,66 @@ await client.halo.createIdentity();
39
45
  await client.spaces.create();
40
46
  await client.spaces.create();
41
47
 
42
- const spaceBuilderExtension = (graph: Graph) => {
43
- const subscriptions = new EventSubscriptions();
44
- const { unsubscribe } = client.spaces.subscribe((spaces) => {
45
- subscriptions.clear();
46
- spaces.forEach((space) => {
47
- subscriptions.add(
48
- effect(() => {
49
- if (space.state.get() === SpaceState.READY) {
50
- graph.addNodes({ id: space.key.toHex(), properties: { label: space.properties.name }, data: space });
51
- graph.addEdge({ source: 'root', target: space.key.toHex() });
52
- } else {
53
- graph.removeNode(space.key.toHex());
54
- }
55
- }),
56
- );
57
-
58
- const query = space.db.query();
59
- subscriptions.add(query.subscribe());
60
- subscriptions.add(
61
- effect(() => {
62
- query.objects.forEach((object) => {
63
- graph.addEdge({ source: space.key.toHex(), target: object.id });
64
- });
65
- }),
66
- );
67
- });
68
- });
48
+ const EMPTY_ARRAY: never[] = [];
49
+
50
+ // TODO(wittjosiah): Factor out.
51
+ const memoizeQuery = <T extends EchoReactiveObject<any>>(
52
+ spaceOrEcho?: Space | Echo,
53
+ filter?: FilterSource<T>,
54
+ options?: QueryOptions,
55
+ ): T[] => {
56
+ const key = isSpace(spaceOrEcho) ? spaceOrEcho.id : undefined;
57
+ const query = memoize(
58
+ () =>
59
+ isSpace(spaceOrEcho)
60
+ ? spaceOrEcho.db.query(filter, options)
61
+ : (spaceOrEcho?.query(filter, options) as Query<T> | undefined),
62
+ key,
63
+ );
64
+ const unsubscribe = memoize(() => query?.subscribe(), key);
65
+ cleanup(() => unsubscribe?.());
69
66
 
70
- return () => {
71
- unsubscribe();
72
- subscriptions.clear();
73
- };
67
+ return query?.objects ?? EMPTY_ARRAY;
74
68
  };
75
69
 
76
- // TODO(wittjosiah): Hypergraph query isn't working.
77
- // const objectBuilderExtension = (graph: Graph) => {
78
- // const query = client.spaces.query({ type: 'test' });
79
- // let previousObjects: Expando[] = [];
80
- // return effect(() => {
81
- // const removedObjects = previousObjects.filter((object) => !query.objects.includes(object));
82
- // previousObjects = query.objects;
83
-
84
- // removedObjects.forEach((object) => graph.removeNode(object.id));
85
- // query.objects.forEach((object) => {
86
- // console.log('add object');
87
- // graph.addNodes({ id: object.id, properties: { label: object.name }, data: object });
88
- // });
89
- // });
90
- // };
91
-
92
- const objectBuilderExtension = (graph: Graph) => {
93
- const subscriptions = new EventSubscriptions();
94
- const { unsubscribe } = client.spaces.subscribe((spaces) => {
95
- subscriptions.clear();
96
- spaces.forEach((space) => {
97
- const query = space.db.query({ type: 'test' });
98
- subscriptions.add(query.subscribe());
99
- let previousObjects: EchoReactiveObject<any>[] = [];
100
- subscriptions.add(
101
- effect(() => {
102
- const removedObjects = previousObjects.filter((object) => !query.objects.includes(object));
103
- previousObjects = query.objects;
104
-
105
- removedObjects.forEach((object) => graph.removeNode(object.id));
106
- query.objects.forEach((object) => {
107
- console.log('add object');
108
- graph.addNodes({ id: object.id, properties: { label: object.name }, data: object });
109
- });
110
- }),
111
- );
112
- });
113
- });
114
-
115
- return () => {
116
- unsubscribe();
117
- subscriptions.clear();
118
- };
119
- };
70
+ const spaceBuilderExtension = createExtension({
71
+ id: 'space',
72
+ filter: (node): node is Node<null> => node.id === 'root',
73
+ connector: ({ node }) => {
74
+ const spaces = toSignal(
75
+ (onChange) => client.spaces.subscribe(() => onChange()).unsubscribe,
76
+ () => client.spaces.get(),
77
+ );
78
+ if (!spaces) {
79
+ return;
80
+ }
120
81
 
121
- const graph = new GraphBuilder()
122
- .addExtension('space', spaceBuilderExtension)
123
- .addExtension('object', objectBuilderExtension)
124
- .build();
82
+ return spaces
83
+ .filter((space) => space.state.get() === SpaceState.READY)
84
+ .map((space) => ({
85
+ id: space.id,
86
+ type: 'dxos.org/type/Space',
87
+ properties: { label: space.properties.name },
88
+ data: space,
89
+ }));
90
+ },
91
+ });
92
+
93
+ const objectBuilderExtension = createExtension({
94
+ id: 'object',
95
+ filter: (node): node is Node<Space> => isSpace(node.data),
96
+ connector: ({ node }) => {
97
+ const objects = memoizeQuery(node.data, { type: 'test' });
98
+ return objects.map((object) => ({
99
+ id: object.id,
100
+ type: 'dxos.org/type/test',
101
+ properties: { label: object.name },
102
+ data: object,
103
+ }));
104
+ },
105
+ });
106
+
107
+ const graph = new GraphBuilder().addExtension(spaceBuilderExtension).addExtension(objectBuilderExtension).graph;
125
108
 
126
109
  enum Action {
127
110
  CREATE_SPACE = 'CREATE_SPACE',
@@ -149,32 +132,31 @@ const randomAction = () => {
149
132
  return actionDistribution[Math.floor(Math.random() * actionDistribution.length)];
150
133
  };
151
134
 
152
- const getSpace = (): Space | undefined => {
135
+ const getRandomSpace = (): Space | undefined => {
153
136
  const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.READY);
154
- return spaces[Math.floor(Math.random() * spaces.length)];
137
+ const space = spaces[Math.floor(Math.random() * spaces.length)];
138
+ return space;
155
139
  };
156
140
 
157
- const getSpaceWithObjects = (): Space | undefined => {
158
- const spaces = client.spaces
159
- .get()
160
- .filter((space) => space.state.get() === SpaceState.READY)
161
- .filter((space) => space.db.query({ type: 'test' }).objects.length > 0);
162
-
141
+ const getSpaceWithObjects = async (): Promise<Space | undefined> => {
142
+ const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.READY);
143
+ const spaceQueries = await Promise.all(readySpaces.map((space) => space.db.query({ type: 'test' }).run()));
144
+ const spaces = readySpaces.filter((space, index) => spaceQueries[index].objects.length > 0);
163
145
  return spaces[Math.floor(Math.random() * spaces.length)];
164
146
  };
165
147
 
166
- const runAction = (action: Action) => {
148
+ const runAction = async (action: Action) => {
167
149
  switch (action) {
168
150
  case Action.CREATE_SPACE:
169
151
  void client.spaces.create();
170
152
  break;
171
153
 
172
154
  case Action.CLOSE_SPACE:
173
- void getSpace()?.close();
155
+ void getRandomSpace()?.close();
174
156
  break;
175
157
 
176
158
  case Action.RENAME_SPACE: {
177
- const space = getSpace();
159
+ const space = getRandomSpace();
178
160
  if (space) {
179
161
  space.properties.name = faker.commerce.productName();
180
162
  }
@@ -182,22 +164,22 @@ const runAction = (action: Action) => {
182
164
  }
183
165
 
184
166
  case Action.ADD_OBJECT:
185
- getSpace()?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
167
+ getRandomSpace()?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
186
168
  break;
187
169
 
188
170
  case Action.REMOVE_OBJECT: {
189
- const space = getSpaceWithObjects();
171
+ const space = await getSpaceWithObjects();
190
172
  if (space) {
191
- const objects = space.db.query({ type: 'test' }).objects;
173
+ const { objects } = await space.db.query({ type: 'test' }).run();
192
174
  space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
193
175
  }
194
176
  break;
195
177
  }
196
178
 
197
179
  case Action.RENAME_OBJECT: {
198
- const space = getSpaceWithObjects();
180
+ const space = await getSpaceWithObjects();
199
181
  if (space) {
200
- const objects = space.db.query({ type: 'test' }).objects;
182
+ const { objects } = await space.db.query({ type: 'test' }).run();
201
183
  objects[Math.floor(Math.random() * objects.length)].name = faker.commerce.productName();
202
184
  }
203
185
  break;
@@ -261,7 +243,7 @@ const EchoGraphStory = () => {
261
243
  </Select.Root>
262
244
  </DensityProvider>
263
245
  </div>
264
- <Tree data={graph.toJSON()} />
246
+ <Tree data={graph.toJSON({ onlyLoaded: false })} />
265
247
  </>
266
248
  );
267
249
  };
@@ -1,12 +0,0 @@
1
- import { type Graph } from './graph';
2
- import { type Node, type NodeArg } from './node';
3
- /**
4
- * If the condition is true, adds the nodes to the graph, otherwise removes the nodes from the graph.
5
- */
6
- export declare const manageNodes: <TData = null, TProperties extends Record<string, any> = Record<string, any>>({ graph, condition, nodes, removeEdges, }: {
7
- graph: Graph;
8
- condition: boolean;
9
- nodes: NodeArg<TData, TProperties>[];
10
- removeEdges?: boolean;
11
- }) => Node<TData, TProperties>[] | void;
12
- //# sourceMappingURL=helpers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/helpers.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,WAAW,2HAKrB;IACD,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,QAAQ,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC;IACrC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,KAAG,KAAK,KAAK,EAAE,WAAW,CAAC,EAAE,GAAG,IAMhC,CAAC"}
package/src/helpers.ts DELETED
@@ -1,27 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { type Graph } from './graph';
6
- import { type Node, type NodeArg } from './node';
7
-
8
- /**
9
- * If the condition is true, adds the nodes to the graph, otherwise removes the nodes from the graph.
10
- */
11
- export const manageNodes = <TData = null, TProperties extends Record<string, any> = Record<string, any>>({
12
- graph,
13
- condition,
14
- nodes,
15
- removeEdges,
16
- }: {
17
- graph: Graph;
18
- condition: boolean;
19
- nodes: NodeArg<TData, TProperties>[];
20
- removeEdges?: boolean;
21
- }): Node<TData, TProperties>[] | void => {
22
- if (condition) {
23
- return graph.addNodes(...nodes);
24
- } else {
25
- nodes.forEach(({ id }) => graph.removeNode(id, removeEdges));
26
- }
27
- };