@dxos/app-graph 0.6.13 → 0.6.14-main.1366248

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/graph.ts CHANGED
@@ -7,9 +7,10 @@ import { batch, effect, untracked } from '@preact/signals-core';
7
7
  import { asyncTimeout, Trigger } from '@dxos/async';
8
8
  import { type ReactiveObject, create } from '@dxos/echo-schema';
9
9
  import { invariant } from '@dxos/invariant';
10
+ import { log } from '@dxos/log';
10
11
  import { nonNullable } from '@dxos/util';
11
12
 
12
- import { type Relation, type Node, type NodeArg, type NodeFilter, isActionLike } from './node';
13
+ import { type Relation, type Node, type NodeArg, type NodeFilter, isActionLike, actionGroupSymbol } from './node';
13
14
 
14
15
  const graphSymbol = Symbol('graph');
15
16
  type DeepWriteable<T> = { -readonly [K in keyof T]: DeepWriteable<T[K]> };
@@ -64,6 +65,15 @@ export type GraphTraversalOptions = {
64
65
  expansion?: boolean;
65
66
  };
66
67
 
68
+ export type GraphParams = {
69
+ // TODO(wittjosiah): Make data optional instead of omitting.
70
+ nodes?: Omit<Node, 'data'>[];
71
+ edges?: Record<string, string[]>;
72
+ onInitialNode?: Graph['_onInitialNode'];
73
+ onInitialNodes?: Graph['_onInitialNodes'];
74
+ onRemoveNode?: Graph['_onRemoveNode'];
75
+ };
76
+
67
77
  /**
68
78
  * The Graph represents the structure of the application constructed via plugins.
69
79
  */
@@ -85,20 +95,38 @@ export class Graph {
85
95
  */
86
96
  readonly _edges: Record<string, ReactiveObject<{ inbound: string[]; outbound: string[] }>> = {};
87
97
 
88
- constructor({
89
- onInitialNode,
90
- onInitialNodes,
91
- onRemoveNode,
92
- }: {
93
- onInitialNode?: Graph['_onInitialNode'];
94
- onInitialNodes?: Graph['_onInitialNodes'];
95
- onRemoveNode?: Graph['_onRemoveNode'];
96
- } = {}) {
98
+ constructor({ nodes, edges, onInitialNode, onInitialNodes, onRemoveNode }: GraphParams = {}) {
99
+ this._nodes[ROOT_ID] = this._constructNode({ id: ROOT_ID, type: ROOT_TYPE, properties: {}, data: null });
100
+ if (nodes) {
101
+ nodes.forEach((node) => {
102
+ if (node.type === ACTION_TYPE) {
103
+ this._addNode({ ...node, data: () => log.warn('Pickled action invocation') });
104
+ } else if (node.type === ACTION_GROUP_TYPE) {
105
+ this._addNode({ ...node, data: actionGroupSymbol });
106
+ } else {
107
+ this._addNode(node);
108
+ }
109
+ });
110
+ }
111
+
112
+ this._edges[ROOT_ID] = create({ inbound: [], outbound: [] });
113
+ if (edges) {
114
+ Object.entries(edges).forEach(([source, edges]) => {
115
+ edges.forEach((target) => {
116
+ this._addEdge({ source, target });
117
+ });
118
+ this._sortEdges(source, 'outbound', edges);
119
+ });
120
+ }
121
+
97
122
  this._onInitialNode = onInitialNode;
98
123
  this._onInitialNodes = onInitialNodes;
99
124
  this._onRemoveNode = onRemoveNode;
100
- this._nodes[ROOT_ID] = this._constructNode({ id: ROOT_ID, type: ROOT_TYPE, properties: {}, data: null });
101
- this._edges[ROOT_ID] = create({ inbound: [], outbound: [] });
125
+ }
126
+
127
+ static from(pickle: string, options: Omit<GraphParams, 'nodes' | 'edges'> = {}) {
128
+ const { nodes, edges } = JSON.parse(pickle);
129
+ return new Graph({ nodes, edges, ...options });
102
130
  }
103
131
 
104
132
  /**
@@ -138,15 +166,33 @@ export class Graph {
138
166
  return toJSON(root);
139
167
  }
140
168
 
169
+ pickle() {
170
+ const nodes = Object.values(this._nodes).map((node) => {
171
+ return {
172
+ id: node.id,
173
+ type: node.type,
174
+ properties: node.properties,
175
+ };
176
+ });
177
+
178
+ const edges = Object.fromEntries(
179
+ Object.entries(this._edges)
180
+ .map(([id, { outbound }]): [string, string[]] => [id, outbound])
181
+ .toSorted(([a], [b]) => a.localeCompare(b)),
182
+ );
183
+
184
+ return JSON.stringify({ nodes, edges });
185
+ }
186
+
141
187
  /**
142
188
  * Find the node with the given id in the graph.
143
189
  *
144
190
  * If a node is not found within the graph and an `onInitialNode` callback is provided,
145
191
  * it is called with the id and type of the node, potentially initializing the node.
146
192
  */
147
- findNode(id: string): Node | undefined {
193
+ findNode(id: string, expansion = true): Node | undefined {
148
194
  const existingNode = this._nodes[id];
149
- if (!existingNode) {
195
+ if (!existingNode && expansion) {
150
196
  void this._onInitialNode?.(id);
151
197
  }
152
198
 
@@ -7,21 +7,21 @@ import '@dxos-theme';
7
7
  import { Pause, Play, Plus, Timer } from '@phosphor-icons/react';
8
8
  import React, { useEffect, useState } from 'react';
9
9
 
10
- import { type EchoReactiveObject, create } from '@dxos/echo-schema';
11
- import { registerSignalRuntime } from '@dxos/echo-signals';
12
- import { faker } from '@dxos/random';
13
- import { type Client, useClient } from '@dxos/react-client';
14
10
  import {
11
+ create,
12
+ type Echo,
13
+ type EchoReactiveObject,
14
+ type FilterSource,
15
15
  type Space,
16
16
  SpaceState,
17
17
  isSpace,
18
- type Echo,
19
- type FilterSource,
20
18
  type QueryOptions,
21
19
  type Query,
22
- } from '@dxos/react-client/echo';
20
+ } from '@dxos/client/echo';
21
+ import { faker } from '@dxos/random';
22
+ import { type Client, useClient } from '@dxos/react-client';
23
23
  import { withClientProvider } from '@dxos/react-client/testing';
24
- import { Button, DensityProvider, Input, Select, useAsyncEffect } from '@dxos/react-ui';
24
+ import { Button, Input, Select, useAsyncEffect } from '@dxos/react-ui';
25
25
  import { getSize, mx } from '@dxos/react-ui-theme';
26
26
  import { withTheme } from '@dxos/storybook-utils';
27
27
  import { safeParseInt } from '@dxos/util';
@@ -35,8 +35,6 @@ const DEFAULT_PERIOD = 500;
35
35
 
36
36
  const EMPTY_ARRAY: never[] = [];
37
37
 
38
- registerSignalRuntime();
39
-
40
38
  enum Action {
41
39
  CREATE_SPACE = 'CREATE_SPACE',
42
40
  CLOSE_SPACE = 'CLOSE_SPACE',
@@ -185,7 +183,7 @@ const runAction = async (client: Client, action: Action) => {
185
183
  }
186
184
  };
187
185
 
188
- const Story = () => {
186
+ const DefaultStory = () => {
189
187
  const [generating, setGenerating] = useState(false);
190
188
  const [actionInterval, setActionInterval] = useState(String(DEFAULT_PERIOD));
191
189
  const [action, setAction] = useState<Action>();
@@ -211,44 +209,40 @@ const Story = () => {
211
209
  return (
212
210
  <>
213
211
  <div className='flex shrink-0 p-2 space-x-2'>
214
- <DensityProvider density='fine'>
215
- <Button onClick={() => setGenerating((generating) => !generating)}>
216
- {generating ? <Pause /> : <Play />}
217
- </Button>
218
- <div className='relative' title='mutation period'>
219
- <Input.Root>
220
- <Input.TextInput
221
- autoComplete='off'
222
- size={5}
223
- classNames='w-[100px] text-right pie-[22px]'
224
- placeholder='Interval'
225
- value={actionInterval}
226
- onChange={({ target: { value } }) => setActionInterval(value)}
227
- />
228
- </Input.Root>
229
- <Timer className={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
230
- </div>
231
- <Button onClick={() => action && runAction(client, action)}>
232
- <Plus />
233
- </Button>
234
- <Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
235
- <Select.TriggerButton placeholder='Select value' />
236
- <Select.Portal>
237
- <Select.Content>
238
- <Select.ScrollUpButton />
239
- <Select.Viewport>
240
- {Object.keys(actionWeights).map((action) => (
241
- <Select.Option key={action} value={action}>
242
- {action}
243
- </Select.Option>
244
- ))}
245
- </Select.Viewport>
246
- <Select.ScrollDownButton />
247
- <Select.Arrow />
248
- </Select.Content>
249
- </Select.Portal>
250
- </Select.Root>
251
- </DensityProvider>
212
+ <Button onClick={() => setGenerating((generating) => !generating)}>{generating ? <Pause /> : <Play />}</Button>
213
+ <div className='relative' title='mutation period'>
214
+ <Input.Root>
215
+ <Input.TextInput
216
+ autoComplete='off'
217
+ size={5}
218
+ classNames='w-[100px] text-right pie-[22px]'
219
+ placeholder='Interval'
220
+ value={actionInterval}
221
+ onChange={({ target: { value } }) => setActionInterval(value)}
222
+ />
223
+ </Input.Root>
224
+ <Timer className={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
225
+ </div>
226
+ <Button onClick={() => action && runAction(client, action)}>
227
+ <Plus />
228
+ </Button>
229
+ <Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
230
+ <Select.TriggerButton placeholder='Select value' />
231
+ <Select.Portal>
232
+ <Select.Content>
233
+ <Select.ScrollUpButton />
234
+ <Select.Viewport>
235
+ {Object.keys(actionWeights).map((action) => (
236
+ <Select.Option key={action} value={action}>
237
+ {action}
238
+ </Select.Option>
239
+ ))}
240
+ </Select.Viewport>
241
+ <Select.ScrollDownButton />
242
+ <Select.Arrow />
243
+ </Select.Content>
244
+ </Select.Portal>
245
+ </Select.Root>
252
246
  </div>
253
247
  {graph && <Tree data={graph.toJSON()} />}
254
248
  </>
@@ -256,7 +250,8 @@ const Story = () => {
256
250
  };
257
251
 
258
252
  export default {
259
- title: 'app-graph/EchoGraph',
253
+ title: 'sdk/app-graph/EchoGraph',
254
+ render: DefaultStory,
260
255
  decorators: [
261
256
  withTheme,
262
257
  withClientProvider({
@@ -267,7 +262,6 @@ export default {
267
262
  },
268
263
  }),
269
264
  ],
270
- render: Story,
271
265
  };
272
266
 
273
267
  export const Default = {};
@@ -10,7 +10,7 @@ import { mx } from '@dxos/react-ui-theme';
10
10
 
11
11
  export const Tree: FC<{ data?: object }> = ({ data }) => {
12
12
  return (
13
- <div className='flex overflow-auto ml-2 border-l-2 border-blue-500'>
13
+ <div className='flex overflow-auto ml-2 border-l-4 border-blue-500'>
14
14
  <Node data={data} root />
15
15
  </div>
16
16
  );
@@ -23,18 +23,18 @@ export const Node: FC<{ data?: any; root?: boolean }> = ({ data, root }) => {
23
23
 
24
24
  if (Array.isArray(data)) {
25
25
  return (
26
- <div className='flex flex-col space-y-2'>
26
+ <div className='flex flex-col space-y-1'>
27
27
  {data.map((value, index) => (
28
- <KeyValue key={index} label={String(index)} data={value} className='bg-teal-50' />
28
+ <KeyValue key={index} label={String(index)} data={value} className='' />
29
29
  ))}
30
30
  </div>
31
31
  );
32
32
  }
33
33
 
34
34
  return (
35
- <div className='flex flex-col space-y-2'>
35
+ <div className='flex flex-col space-y-1'>
36
36
  {Object.entries(data).map(([key, value]) => (
37
- <KeyValue key={key} label={key} data={value} className='bg-blue-50' />
37
+ <KeyValue key={key} label={key} data={value} className='' />
38
38
  ))}
39
39
  </div>
40
40
  );
@@ -49,7 +49,7 @@ export const KeyValue: FC<{ label: string; data?: any; className?: string }> = (
49
49
  return (
50
50
  <div className='flex'>
51
51
  <Box
52
- className={mx('border-blue-200 text-sm select-none cursor-pointer', className)}
52
+ className={mx('border-blue-500 text-sm select-none cursor-pointer min-w-[6rem]', className)}
53
53
  onClick={() => setOpen((open) => !open)}
54
54
  >
55
55
  {label}
@@ -61,7 +61,7 @@ export const KeyValue: FC<{ label: string; data?: any; className?: string }> = (
61
61
 
62
62
  const Scalar: FC<{ value: any }> = ({ value }) => {
63
63
  return (
64
- <Box className='bg-green-50 border-green-200 rounded-r text-sm font-thin'>
64
+ <Box className='border-green-500 text-sm font-thin'>
65
65
  {(value === undefined && 'undefined') ||
66
66
  (value === null && 'null') ||
67
67
  (typeof value === 'string' && value) ||