@dxos/app-graph 0.6.2 → 0.6.3-main.038c693
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/dist/lib/browser/index.mjs +581 -191
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +586 -185
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +99 -7
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph-builder.test.d.ts +2 -0
- package/dist/types/src/graph-builder.test.d.ts.map +1 -0
- package/dist/types/src/graph.d.ts +96 -47
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +99 -40
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/package.json +14 -12
- package/src/graph-builder.test.ts +310 -0
- package/src/graph-builder.ts +332 -19
- package/src/graph.test.ts +431 -179
- package/src/graph.ts +337 -149
- package/src/index.ts +0 -1
- package/src/node.ts +15 -42
- package/src/stories/EchoGraph.stories.tsx +85 -103
- package/dist/types/src/helpers.d.ts +0 -12
- package/dist/types/src/helpers.d.ts.map +0 -1
- package/src/helpers.ts +0 -27
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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<
|
|
72
|
+
Omit<Node<ActionData, TProperties>, 'properties'> & {
|
|
100
73
|
properties: Readonly<TProperties>;
|
|
101
|
-
}
|
|
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<
|
|
83
|
+
Omit<Node<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
|
|
111
84
|
properties: Readonly<Record<string, any>>;
|
|
112
|
-
}
|
|
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 {
|
|
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 {
|
|
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 {
|
|
25
|
-
import {
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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;
|
|
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
|
|
153
|
-
const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.
|
|
154
|
-
|
|
135
|
+
const getRandomSpace = (): Space | undefined => {
|
|
136
|
+
const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
137
|
+
const space = spaces[Math.floor(Math.random() * spaces.length)];
|
|
138
|
+
return space;
|
|
155
139
|
};
|
|
156
140
|
|
|
157
|
-
const getSpaceWithObjects = (): Space | undefined => {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
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.SPACE_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
|
|
155
|
+
void getRandomSpace()?.close();
|
|
174
156
|
break;
|
|
175
157
|
|
|
176
158
|
case Action.RENAME_SPACE: {
|
|
177
|
-
const space =
|
|
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
|
-
|
|
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' }).
|
|
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' }).
|
|
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
|
-
};
|