@dxos/app-graph 0.6.3-main.9e4e207 → 0.6.3-next.2f65b78

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,21 +8,16 @@ 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 Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = Readonly<{
11
+ export type NodeBase<TData = any, TProperties extends Record<string, any> = Record<string, any>> = {
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
-
22
17
  /**
23
18
  * Properties of the node relevant to displaying the node.
24
19
  */
25
- properties: Readonly<TProperties>;
20
+ properties: TProperties;
26
21
 
27
22
  /**
28
23
  * Data the node represents.
@@ -30,14 +25,46 @@ export type Node<TData = any, TProperties extends Record<string, any> = Record<s
30
25
  // TODO(burdon): Type system (e.g., minimally provide identifier string vs. TypedObject vs. Graph mixin type system)?
31
26
  // type field would prevent convoluted sniffing of object properties. And allow direct pass-through for ECHO TypedObjects.
32
27
  data: TData;
33
- }>;
28
+ };
34
29
 
35
30
  export type NodeFilter<T = any, U extends Record<string, any> = Record<string, any>> = (
36
31
  node: Node<unknown, Record<string, any>>,
37
32
  connectedNode: Node,
38
33
  ) => node is Node<T, U>;
39
34
 
40
- export type Relation = 'outbound' | 'inbound';
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
+ >;
41
68
 
42
69
  export const isGraphNode = (data: unknown): data is Node =>
43
70
  data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
@@ -45,14 +72,14 @@ export const isGraphNode = (data: unknown): data is Node =>
45
72
  : false;
46
73
 
47
74
  export type NodeArg<TData, TProperties extends Record<string, any> = Record<string, any>> = MakeOptional<
48
- Node<TData, TProperties>,
75
+ NodeBase<TData, TProperties>,
49
76
  'data' | 'properties'
50
77
  > & {
51
78
  /** Will automatically add nodes with an edge from this node to each. */
52
79
  nodes?: NodeArg<unknown>[];
53
80
 
54
81
  /** Will automatically add specified edges. */
55
- edges?: [string, Relation][];
82
+ edges?: [string, EdgeDirection][];
56
83
  };
57
84
 
58
85
  //
@@ -69,9 +96,9 @@ export type InvokeParams = {
69
96
  export type ActionData = (params: InvokeParams) => MaybePromise<void>;
70
97
 
71
98
  export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
72
- Omit<Node<ActionData, TProperties>, 'properties'> & {
99
+ Omit<NodeBase<ActionData, TProperties>, 'properties'> & {
73
100
  properties: Readonly<TProperties>;
74
- }
101
+ } & ConnectedNodes
75
102
  >;
76
103
 
77
104
  export const isAction = (data: unknown): data is Action =>
@@ -80,9 +107,9 @@ export const isAction = (data: unknown): data is Action =>
80
107
  export const actionGroupSymbol = Symbol('ActionGroup');
81
108
 
82
109
  export type ActionGroup = Readonly<
83
- Omit<Node<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
110
+ Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
84
111
  properties: Readonly<Record<string, any>>;
85
- }
112
+ } & ConnectedActions
86
113
  >;
87
114
 
88
115
  export const isActionGroup = (data: unknown): data is ActionGroup =>
@@ -5,21 +5,15 @@
5
5
  import '@dxosTheme';
6
6
 
7
7
  import { Pause, Play, Plus, Timer } from '@phosphor-icons/react';
8
+ import { effect } from '@preact/signals-core';
8
9
  import React, { useEffect, useState } from 'react';
9
10
 
10
- import { type EchoReactiveObject, create } from '@dxos/echo-schema';
11
+ import { EventSubscriptions } from '@dxos/async';
12
+ import { create, type EchoReactiveObject } from '@dxos/echo-schema';
11
13
  import { registerSignalRuntime } from '@dxos/echo-signals';
12
14
  import { faker } from '@dxos/random';
13
15
  import { Client } from '@dxos/react-client';
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';
16
+ import { type Space, SpaceState } from '@dxos/react-client/echo';
23
17
  import { ClientRepeater, TestBuilder } from '@dxos/react-client/testing';
24
18
  import { Button, DensityProvider, Input, Select } from '@dxos/react-ui';
25
19
  import { getSize, mx } from '@dxos/react-ui-theme';
@@ -27,8 +21,8 @@ import { withTheme } from '@dxos/storybook-utils';
27
21
  import { safeParseInt } from '@dxos/util';
28
22
 
29
23
  import { Tree } from './Tree';
30
- import { GraphBuilder, cleanup, createExtension, memoize, toSignal } from '../graph-builder';
31
- import { type Node } from '../node';
24
+ import { type Graph } from '../graph';
25
+ import { GraphBuilder } from '../graph-builder';
32
26
 
33
27
  export default {
34
28
  title: 'app-graph/EchoGraph',
@@ -45,66 +39,89 @@ await client.halo.createIdentity();
45
39
  await client.spaces.create();
46
40
  await client.spaces.create();
47
41
 
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?.());
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
+ });
66
69
 
67
- return query?.objects ?? EMPTY_ARRAY;
70
+ return () => {
71
+ unsubscribe();
72
+ subscriptions.clear();
73
+ };
68
74
  };
69
75
 
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
- }
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;
81
104
 
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;
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
+ };
120
+
121
+ const graph = new GraphBuilder()
122
+ .addExtension('space', spaceBuilderExtension)
123
+ .addExtension('object', objectBuilderExtension)
124
+ .build();
108
125
 
109
126
  enum Action {
110
127
  CREATE_SPACE = 'CREATE_SPACE',
@@ -132,31 +149,32 @@ const randomAction = () => {
132
149
  return actionDistribution[Math.floor(Math.random() * actionDistribution.length)];
133
150
  };
134
151
 
135
- const getRandomSpace = (): Space | undefined => {
152
+ const getSpace = (): Space | undefined => {
136
153
  const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.READY);
137
- const space = spaces[Math.floor(Math.random() * spaces.length)];
138
- return space;
154
+ return spaces[Math.floor(Math.random() * spaces.length)];
139
155
  };
140
156
 
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);
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
+
145
163
  return spaces[Math.floor(Math.random() * spaces.length)];
146
164
  };
147
165
 
148
- const runAction = async (action: Action) => {
166
+ const runAction = (action: Action) => {
149
167
  switch (action) {
150
168
  case Action.CREATE_SPACE:
151
169
  void client.spaces.create();
152
170
  break;
153
171
 
154
172
  case Action.CLOSE_SPACE:
155
- void getRandomSpace()?.close();
173
+ void getSpace()?.close();
156
174
  break;
157
175
 
158
176
  case Action.RENAME_SPACE: {
159
- const space = getRandomSpace();
177
+ const space = getSpace();
160
178
  if (space) {
161
179
  space.properties.name = faker.commerce.productName();
162
180
  }
@@ -164,22 +182,22 @@ const runAction = async (action: Action) => {
164
182
  }
165
183
 
166
184
  case Action.ADD_OBJECT:
167
- getRandomSpace()?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
185
+ getSpace()?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
168
186
  break;
169
187
 
170
188
  case Action.REMOVE_OBJECT: {
171
- const space = await getSpaceWithObjects();
189
+ const space = getSpaceWithObjects();
172
190
  if (space) {
173
- const { objects } = await space.db.query({ type: 'test' }).run();
191
+ const objects = space.db.query({ type: 'test' }).objects;
174
192
  space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
175
193
  }
176
194
  break;
177
195
  }
178
196
 
179
197
  case Action.RENAME_OBJECT: {
180
- const space = await getSpaceWithObjects();
198
+ const space = getSpaceWithObjects();
181
199
  if (space) {
182
- const { objects } = await space.db.query({ type: 'test' }).run();
200
+ const objects = space.db.query({ type: 'test' }).objects;
183
201
  objects[Math.floor(Math.random() * objects.length)].name = faker.commerce.productName();
184
202
  }
185
203
  break;
@@ -243,7 +261,7 @@ const EchoGraphStory = () => {
243
261
  </Select.Root>
244
262
  </DensityProvider>
245
263
  </div>
246
- <Tree data={graph.toJSON({ onlyLoaded: false })} />
264
+ <Tree data={graph.toJSON()} />
247
265
  </>
248
266
  );
249
267
  };
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=graph-builder.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph-builder.test.d.ts","sourceRoot":"","sources":["../../../src/graph-builder.test.ts"],"names":[],"mappings":""}