@dxos/app-graph 0.6.3-main.d007b87 → 0.6.3-main.d76104f
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 +572 -190
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +577 -184
- 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 +94 -45
- 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 +322 -0
- package/src/graph-builder.ts +327 -19
- package/src/graph.test.ts +431 -179
- package/src/graph.ts +335 -148
- package/src/index.ts +0 -1
- package/src/node.ts +15 -42
- package/src/stories/EchoGraph.stories.tsx +91 -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,72 @@ 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
|
-
});
|
|
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
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
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
|
|
153
|
-
const spaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.
|
|
154
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
161
|
+
void getRandomSpace()?.close();
|
|
174
162
|
break;
|
|
175
163
|
|
|
176
164
|
case Action.RENAME_SPACE: {
|
|
177
|
-
const space =
|
|
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
|
-
|
|
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' }).
|
|
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' }).
|
|
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
|
-
};
|