@dxos/app-graph 0.6.2-next.5b5129c → 0.6.3-main.0308ae2

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,72 @@ 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
- });
69
-
70
- return () => {
71
- unsubscribe();
72
- subscriptions.clear();
73
- };
74
- };
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?.());
75
66
 
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
- };
67
+ return query?.objects ?? EMPTY_ARRAY;
119
68
  };
120
69
 
121
- const graph = new GraphBuilder()
122
- .addExtension('space', spaceBuilderExtension)
123
- .addExtension('object', objectBuilderExtension)
124
- .build();
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
+ }
81
+
82
+ return spaces
83
+ .filter((space) => space.state.get() === SpaceState.SPACE_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;
108
+
109
+ graph.subscribeTraverse({
110
+ visitor: (node) => {
111
+ void graph.expand(node);
112
+ },
113
+ });
125
114
 
126
115
  enum Action {
127
116
  CREATE_SPACE = 'CREATE_SPACE',
@@ -149,32 +138,31 @@ const randomAction = () => {
149
138
  return actionDistribution[Math.floor(Math.random() * actionDistribution.length)];
150
139
  };
151
140
 
152
- const getSpace = (): Space | undefined => {
153
- const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.READY);
154
- return spaces[Math.floor(Math.random() * spaces.length)];
141
+ const getRandomSpace = (): Space | undefined => {
142
+ const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
143
+ const space = spaces[Math.floor(Math.random() * spaces.length)];
144
+ return space;
155
145
  };
156
146
 
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
-
147
+ const getSpaceWithObjects = async (): Promise<Space | undefined> => {
148
+ const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
149
+ const spaceQueries = await Promise.all(readySpaces.map((space) => space.db.query({ type: 'test' }).run()));
150
+ const spaces = readySpaces.filter((space, index) => spaceQueries[index].objects.length > 0);
163
151
  return spaces[Math.floor(Math.random() * spaces.length)];
164
152
  };
165
153
 
166
- const runAction = (action: Action) => {
154
+ const runAction = async (action: Action) => {
167
155
  switch (action) {
168
156
  case Action.CREATE_SPACE:
169
157
  void client.spaces.create();
170
158
  break;
171
159
 
172
160
  case Action.CLOSE_SPACE:
173
- void getSpace()?.close();
161
+ void getRandomSpace()?.close();
174
162
  break;
175
163
 
176
164
  case Action.RENAME_SPACE: {
177
- const space = getSpace();
165
+ const space = getRandomSpace();
178
166
  if (space) {
179
167
  space.properties.name = faker.commerce.productName();
180
168
  }
@@ -182,22 +170,22 @@ const runAction = (action: Action) => {
182
170
  }
183
171
 
184
172
  case Action.ADD_OBJECT:
185
- getSpace()?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
173
+ getRandomSpace()?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
186
174
  break;
187
175
 
188
176
  case Action.REMOVE_OBJECT: {
189
- const space = getSpaceWithObjects();
177
+ const space = await getSpaceWithObjects();
190
178
  if (space) {
191
- const objects = space.db.query({ type: 'test' }).objects;
179
+ const { objects } = await space.db.query({ type: 'test' }).run();
192
180
  space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
193
181
  }
194
182
  break;
195
183
  }
196
184
 
197
185
  case Action.RENAME_OBJECT: {
198
- const space = getSpaceWithObjects();
186
+ const space = await getSpaceWithObjects();
199
187
  if (space) {
200
- const objects = space.db.query({ type: 'test' }).objects;
188
+ const { objects } = await space.db.query({ type: 'test' }).run();
201
189
  objects[Math.floor(Math.random() * objects.length)].name = faker.commerce.productName();
202
190
  }
203
191
  break;
@@ -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
- };