@dxos/app-graph 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446
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 +789 -541
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +780 -533
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +789 -541
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/graph-builder.d.ts +91 -48
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +98 -191
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +3 -3
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -23
- package/src/graph-builder.test.ts +310 -293
- package/src/graph-builder.ts +317 -209
- package/src/graph.test.ts +463 -314
- package/src/graph.ts +455 -452
- package/src/node.ts +4 -4
- package/src/stories/EchoGraph.stories.tsx +78 -57
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -218
- package/src/testing.ts +0 -20
package/src/node.ts
CHANGED
|
@@ -41,10 +41,10 @@ export type Node<TData = any, TProperties extends Record<string, any> = Record<s
|
|
|
41
41
|
data: TData;
|
|
42
42
|
}>;
|
|
43
43
|
|
|
44
|
-
export type NodeFilter<
|
|
44
|
+
export type NodeFilter<T = any, U extends Record<string, any> = Record<string, any>> = (
|
|
45
45
|
node: Node<unknown, Record<string, any>>,
|
|
46
46
|
connectedNode: Node,
|
|
47
|
-
) => node is Node<
|
|
47
|
+
) => node is Node<T, U>;
|
|
48
48
|
|
|
49
49
|
export type Relation = 'outbound' | 'inbound';
|
|
50
50
|
|
|
@@ -70,12 +70,12 @@ export type NodeArg<TData, TProperties extends Record<string, any> = Record<stri
|
|
|
70
70
|
|
|
71
71
|
export type InvokeParams = {
|
|
72
72
|
/** Node the invoked action is connected to. */
|
|
73
|
-
|
|
73
|
+
node: Node;
|
|
74
74
|
|
|
75
75
|
caller?: string;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
export type ActionData = (params
|
|
78
|
+
export type ActionData = (params: InvokeParams) => MaybePromise<void>;
|
|
79
79
|
|
|
80
80
|
export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
|
|
81
81
|
Omit<Node<ActionData, TProperties>, 'properties'> & {
|
|
@@ -4,27 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
|
-
import { Rx, useRxValue } from '@effect-rx/rx-react';
|
|
8
7
|
import { Pause, Play, Plus, Timer } from '@phosphor-icons/react';
|
|
9
|
-
import {
|
|
10
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import React, { useEffect, useState } from 'react';
|
|
11
9
|
|
|
12
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
create,
|
|
12
|
+
isSpace,
|
|
13
|
+
type Echo,
|
|
14
|
+
type FilterSource,
|
|
15
|
+
type ReactiveEchoObject,
|
|
16
|
+
type Space,
|
|
17
|
+
SpaceState,
|
|
18
|
+
type QueryOptions,
|
|
19
|
+
type Query,
|
|
20
|
+
} from '@dxos/client/echo';
|
|
13
21
|
import { faker } from '@dxos/random';
|
|
14
22
|
import { type Client, useClient } from '@dxos/react-client';
|
|
15
23
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
16
|
-
import { Button, Input, Select } from '@dxos/react-ui';
|
|
24
|
+
import { Button, Input, Select, useAsyncEffect } from '@dxos/react-ui';
|
|
17
25
|
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
18
26
|
import { withTheme } from '@dxos/storybook-utils';
|
|
19
27
|
import { safeParseInt } from '@dxos/util';
|
|
20
28
|
|
|
21
29
|
import { Tree } from './Tree';
|
|
22
|
-
import { type
|
|
23
|
-
import { GraphBuilder, createExtension,
|
|
24
|
-
import {
|
|
30
|
+
import { type Graph } from '../graph';
|
|
31
|
+
import { GraphBuilder, cleanup, createExtension, memoize, toSignal } from '../graph-builder';
|
|
32
|
+
import { type Node } from '../node';
|
|
25
33
|
|
|
26
34
|
const DEFAULT_PERIOD = 500;
|
|
27
35
|
|
|
36
|
+
const EMPTY_ARRAY: never[] = [];
|
|
37
|
+
|
|
28
38
|
enum Action {
|
|
29
39
|
CREATE_SPACE = 'CREATE_SPACE',
|
|
30
40
|
CLOSE_SPACE = 'CLOSE_SPACE',
|
|
@@ -43,61 +53,70 @@ const actionWeights = {
|
|
|
43
53
|
[Action.RENAME_OBJECT]: 4,
|
|
44
54
|
};
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
// TODO(wittjosiah): Factor out.
|
|
57
|
+
const memoizeQuery = <T extends ReactiveEchoObject<any>>(
|
|
58
|
+
spaceOrEcho?: Space | Echo,
|
|
59
|
+
filter?: FilterSource<T>,
|
|
60
|
+
options?: QueryOptions,
|
|
61
|
+
): T[] => {
|
|
62
|
+
const key = isSpace(spaceOrEcho) ? spaceOrEcho.id : undefined;
|
|
63
|
+
const query = memoize(
|
|
64
|
+
() =>
|
|
65
|
+
isSpace(spaceOrEcho)
|
|
66
|
+
? spaceOrEcho.db.query(filter, options)
|
|
67
|
+
: (spaceOrEcho?.query(filter, options) as Query<T> | undefined),
|
|
68
|
+
key,
|
|
69
|
+
);
|
|
70
|
+
const unsubscribe = memoize(() => query?.subscribe(), key);
|
|
71
|
+
cleanup(() => unsubscribe?.());
|
|
72
|
+
|
|
73
|
+
return query?.objects ?? EMPTY_ARRAY;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const createGraph = async (client: Client): Promise<Graph> => {
|
|
47
77
|
const spaceBuilderExtension = createExtension({
|
|
48
78
|
id: 'space',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
79
|
+
filter: (node): node is Node<null> => node.id === 'root',
|
|
80
|
+
connector: ({ node }) => {
|
|
81
|
+
const spaces = toSignal(
|
|
82
|
+
(onChange) => client.spaces.subscribe(() => onChange()).unsubscribe,
|
|
83
|
+
() => client.spaces.get(),
|
|
84
|
+
);
|
|
85
|
+
if (!spaces) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return spaces
|
|
90
|
+
.filter((space) => space.state.get() === SpaceState.SPACE_READY)
|
|
91
|
+
.map((space) => ({
|
|
92
|
+
id: space.id,
|
|
93
|
+
type: 'dxos.org/type/Space',
|
|
94
|
+
properties: { label: space.properties.name },
|
|
95
|
+
data: space,
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
68
98
|
});
|
|
69
99
|
|
|
70
100
|
const objectBuilderExtension = createExtension({
|
|
71
101
|
id: 'object',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
return get(rxFromQuery(query)).map((object) => ({
|
|
83
|
-
id: object.id,
|
|
84
|
-
type: 'dxos.org/type/test',
|
|
85
|
-
properties: { label: object.name },
|
|
86
|
-
data: object,
|
|
87
|
-
}));
|
|
88
|
-
}),
|
|
89
|
-
Option.getOrElse(() => []),
|
|
90
|
-
),
|
|
91
|
-
);
|
|
102
|
+
filter: (node): node is Node<Space> => isSpace(node.data),
|
|
103
|
+
connector: ({ node }) => {
|
|
104
|
+
const objects = memoizeQuery(node.data, { type: 'test' });
|
|
105
|
+
return objects.map((object) => ({
|
|
106
|
+
id: object.id,
|
|
107
|
+
type: 'dxos.org/type/test',
|
|
108
|
+
properties: { label: object.name },
|
|
109
|
+
data: object,
|
|
110
|
+
}));
|
|
92
111
|
},
|
|
93
112
|
});
|
|
94
113
|
|
|
95
114
|
const graph = new GraphBuilder().addExtension(spaceBuilderExtension).addExtension(objectBuilderExtension).graph;
|
|
96
|
-
graph.
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
graph.subscribeTraverse({
|
|
116
|
+
visitor: (node) => {
|
|
117
|
+
void graph.expand(node);
|
|
118
|
+
},
|
|
99
119
|
});
|
|
100
|
-
graph.expand(ROOT_ID);
|
|
101
120
|
|
|
102
121
|
return graph;
|
|
103
122
|
};
|
|
@@ -141,7 +160,7 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
case Action.ADD_OBJECT:
|
|
144
|
-
getRandomSpace(client)?.db.add(
|
|
163
|
+
getRandomSpace(client)?.db.add(create({ type: 'test', name: faker.commerce.productName() }));
|
|
145
164
|
break;
|
|
146
165
|
|
|
147
166
|
case Action.REMOVE_OBJECT: {
|
|
@@ -170,8 +189,10 @@ const DefaultStory = () => {
|
|
|
170
189
|
const [action, setAction] = useState<Action>();
|
|
171
190
|
|
|
172
191
|
const client = useClient();
|
|
173
|
-
const graph =
|
|
174
|
-
|
|
192
|
+
const [graph, setGraph] = useState<Graph>();
|
|
193
|
+
useAsyncEffect(async () => {
|
|
194
|
+
setGraph(await createGraph(client));
|
|
195
|
+
}, [client]);
|
|
175
196
|
|
|
176
197
|
useEffect(() => {
|
|
177
198
|
if (!generating) {
|
|
@@ -223,7 +244,7 @@ const DefaultStory = () => {
|
|
|
223
244
|
</Select.Portal>
|
|
224
245
|
</Select.Root>
|
|
225
246
|
</div>
|
|
226
|
-
{
|
|
247
|
+
{graph && <Tree data={graph.toJSON()} />}
|
|
227
248
|
</>
|
|
228
249
|
);
|
|
229
250
|
};
|
|
@@ -235,7 +256,7 @@ export default {
|
|
|
235
256
|
withTheme,
|
|
236
257
|
withClientProvider({
|
|
237
258
|
createIdentity: true,
|
|
238
|
-
|
|
259
|
+
onInitialized: async (client: Client) => {
|
|
239
260
|
await client.spaces.create();
|
|
240
261
|
await client.spaces.create();
|
|
241
262
|
},
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
type Cb = () => void;
|
|
2
|
-
interface Query {
|
|
3
|
-
id?: string[];
|
|
4
|
-
typename?: string[];
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Lazy query result that can be executed.
|
|
8
|
-
* Does not run without the run function being called.
|
|
9
|
-
*/
|
|
10
|
-
interface QueryResult<T> {
|
|
11
|
-
/**
|
|
12
|
-
* Execute the query and subscribe to the result.
|
|
13
|
-
* @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
|
|
14
|
-
* @param error Called on error. `next` is never called after that.
|
|
15
|
-
* @returns Function to dispose the query and unsubscribe.
|
|
16
|
-
*/
|
|
17
|
-
run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
|
|
18
|
-
}
|
|
19
|
-
declare const QueryResult: Readonly<{
|
|
20
|
-
fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>) => QueryResult<T>;
|
|
21
|
-
}>;
|
|
22
|
-
interface _Resolver<T> {
|
|
23
|
-
query(query: Query): QueryResult<T>;
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=graph-projections.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-projections.test.d.ts","sourceRoot":"","sources":["../../../../src/experimental/graph-projections.test.ts"],"names":[],"mappings":"AAIA,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC;AAErB,UAAU,KAAK;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;GAGG;AACH,UAAU,WAAW,CAAC,CAAC;IACrB;;;;;OAKG;IACH,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;CACvE;AAED,QAAA,MAAM,WAAW;kBACD,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,KAAG,WAAW,CAAC,CAAC,CAAC;EAyBpF,CAAC;AAEH,UAAU,SAAS,CAAC,CAAC;IACnB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;CACrC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"signals-integration.test.d.ts","sourceRoot":"","sources":["../../../src/signals-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Rx } from '@effect-rx/rx-react';
|
|
2
|
-
import { type QueryResult } from '@dxos/echo-db';
|
|
3
|
-
import { type BaseObject } from '@dxos/echo-schema';
|
|
4
|
-
export declare const rxFromQuery: <T extends BaseObject>(query: QueryResult<T>) => Rx.Rx<T[]>;
|
|
5
|
-
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../src/testing.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,EAAE,EAAE,MAAM,qBAAqB,CAAC;AAEzC,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,UAAU,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAUlF,CAAC"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
type Cb = () => void;
|
|
6
|
-
|
|
7
|
-
interface Query {
|
|
8
|
-
id?: string[];
|
|
9
|
-
typename?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Lazy query result that can be executed.
|
|
14
|
-
* Does not run without the run function being called.
|
|
15
|
-
*/
|
|
16
|
-
interface QueryResult<T> {
|
|
17
|
-
/**
|
|
18
|
-
* Execute the query and subscribe to the result.
|
|
19
|
-
* @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
|
|
20
|
-
* @param error Called on error. `next` is never called after that.
|
|
21
|
-
* @returns Function to dispose the query and unsubscribe.
|
|
22
|
-
*/
|
|
23
|
-
run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const QueryResult = Object.freeze({
|
|
27
|
-
fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>): QueryResult<T> => {
|
|
28
|
-
return {
|
|
29
|
-
run: (onData, onError) => {
|
|
30
|
-
const cbs: Cb[] = [];
|
|
31
|
-
let disposed = false;
|
|
32
|
-
const dispose = () => {
|
|
33
|
-
cbs.forEach((cb) => cb());
|
|
34
|
-
disposed = true;
|
|
35
|
-
};
|
|
36
|
-
run((cb) => (disposed ? cb() : cbs.push(cb))).then(
|
|
37
|
-
(data) => {
|
|
38
|
-
if (disposed) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
onData(data);
|
|
42
|
-
},
|
|
43
|
-
(err) => {
|
|
44
|
-
dispose();
|
|
45
|
-
onError(err);
|
|
46
|
-
},
|
|
47
|
-
);
|
|
48
|
-
return dispose;
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
interface _Resolver<T> {
|
|
55
|
-
query(query: Query): QueryResult<T>;
|
|
56
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Registry, Rx } from '@effect-rx/rx-react';
|
|
6
|
-
import { signal } from '@preact/signals-core';
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
|
|
8
|
-
|
|
9
|
-
import { Trigger } from '@dxos/async';
|
|
10
|
-
import { Filter } from '@dxos/echo-db';
|
|
11
|
-
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
|
-
import { Expando, Ref } from '@dxos/echo-schema';
|
|
13
|
-
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
14
|
-
import { live } from '@dxos/live-object';
|
|
15
|
-
|
|
16
|
-
import { ROOT_ID } from './graph';
|
|
17
|
-
import { createExtension, GraphBuilder, rxFromSignal } from './graph-builder';
|
|
18
|
-
import { rxFromQuery } from './testing';
|
|
19
|
-
|
|
20
|
-
registerSignalsRuntime();
|
|
21
|
-
|
|
22
|
-
const EXAMPLE_TYPE = 'dxos.org/type/example';
|
|
23
|
-
|
|
24
|
-
describe('signals integration', () => {
|
|
25
|
-
test('creating rx from signal', () => {
|
|
26
|
-
const registry = Registry.make();
|
|
27
|
-
const state = signal<number>(0);
|
|
28
|
-
const value = rxFromSignal(() => state.value);
|
|
29
|
-
const inline = Rx.make((get) => {
|
|
30
|
-
// NOTE: This will create a new rx instance each time.
|
|
31
|
-
// This test is verifying that this behaves the same as using a stable rx instance.
|
|
32
|
-
// The parent will remain subscribed to one instance until the new one is created.
|
|
33
|
-
// The old one will then be garbage collected because it is no longer referenced.
|
|
34
|
-
const rx = rxFromSignal(() => get(value));
|
|
35
|
-
return get(rx);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
let count = 0;
|
|
39
|
-
const cancel = registry.subscribe(value, (value) => {
|
|
40
|
-
count = value;
|
|
41
|
-
});
|
|
42
|
-
onTestFinished(() => cancel());
|
|
43
|
-
|
|
44
|
-
let inlineCount = 0;
|
|
45
|
-
const inlineCancel = registry.subscribe(inline, (value) => {
|
|
46
|
-
inlineCount = value;
|
|
47
|
-
});
|
|
48
|
-
onTestFinished(() => inlineCancel());
|
|
49
|
-
|
|
50
|
-
registry.get(value);
|
|
51
|
-
registry.get(inline);
|
|
52
|
-
expect(count).to.eq(0);
|
|
53
|
-
expect(inlineCount).to.eq(0);
|
|
54
|
-
|
|
55
|
-
state.value = 1;
|
|
56
|
-
expect(count).to.eq(1);
|
|
57
|
-
expect(inlineCount).to.eq(1);
|
|
58
|
-
|
|
59
|
-
state.value = 2;
|
|
60
|
-
expect(count).to.eq(2);
|
|
61
|
-
expect(inlineCount).to.eq(2);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('echo', () => {
|
|
65
|
-
let dbBuilder: EchoTestBuilder;
|
|
66
|
-
|
|
67
|
-
beforeEach(async () => {
|
|
68
|
-
dbBuilder = await new EchoTestBuilder().open();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
afterEach(async () => {
|
|
72
|
-
await dbBuilder.close();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('rx references are loaded lazily and receive signal notifications', async () => {
|
|
76
|
-
const registry = Registry.make();
|
|
77
|
-
await using peer = await dbBuilder.createPeer();
|
|
78
|
-
|
|
79
|
-
let outerId: string;
|
|
80
|
-
{
|
|
81
|
-
await using db = await peer.createDatabase();
|
|
82
|
-
const inner = db.add({ name: 'inner' });
|
|
83
|
-
const outer = db.add({ inner: Ref.make(inner) });
|
|
84
|
-
outerId = outer.id;
|
|
85
|
-
await db.flush();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
await peer.reload();
|
|
89
|
-
{
|
|
90
|
-
await using db = await peer.openLastDatabase();
|
|
91
|
-
const outer = (await db.query({ id: outerId }).first()) as any;
|
|
92
|
-
const innerRx = rxFromSignal(() => outer.inner.target);
|
|
93
|
-
|
|
94
|
-
const loaded = new Trigger();
|
|
95
|
-
let count = 0;
|
|
96
|
-
const cancel = registry.subscribe(innerRx, (inner) => {
|
|
97
|
-
count++;
|
|
98
|
-
if (inner) {
|
|
99
|
-
loaded.wake();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
onTestFinished(() => cancel());
|
|
103
|
-
|
|
104
|
-
expect(registry.get(innerRx)).to.eq(undefined);
|
|
105
|
-
expect(count).to.eq(1);
|
|
106
|
-
|
|
107
|
-
await loaded.wait();
|
|
108
|
-
expect(registry.get(innerRx)).to.include({ name: 'inner' });
|
|
109
|
-
expect(count).to.eq(2);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('references graph builder', async () => {
|
|
114
|
-
const registry = Registry.make();
|
|
115
|
-
await using peer = await dbBuilder.createPeer();
|
|
116
|
-
|
|
117
|
-
let outerId, innerId: string;
|
|
118
|
-
{
|
|
119
|
-
await using db = await peer.createDatabase();
|
|
120
|
-
const inner = db.add({ name: 'inner' });
|
|
121
|
-
const outer = db.add({ inner: Ref.make(inner) });
|
|
122
|
-
innerId = inner.id;
|
|
123
|
-
outerId = outer.id;
|
|
124
|
-
await db.flush();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
await peer.reload();
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
await using db = await peer.openLastDatabase();
|
|
131
|
-
const outer = (await db.query({ id: outerId }).first()) as any;
|
|
132
|
-
const innerRx = rxFromSignal(() => outer.inner.target);
|
|
133
|
-
const inner = registry.get(innerRx);
|
|
134
|
-
expect(inner).to.eq(undefined);
|
|
135
|
-
|
|
136
|
-
const builder = new GraphBuilder({ registry });
|
|
137
|
-
builder.addExtension(
|
|
138
|
-
createExtension({
|
|
139
|
-
id: 'outbound-connector',
|
|
140
|
-
connector: () =>
|
|
141
|
-
Rx.make((get) => {
|
|
142
|
-
const inner = get(innerRx) as any;
|
|
143
|
-
return inner ? [{ id: inner.id, type: EXAMPLE_TYPE, data: inner.name }] : [];
|
|
144
|
-
}),
|
|
145
|
-
}),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
const graph = builder.graph;
|
|
149
|
-
|
|
150
|
-
const loaded = new Trigger();
|
|
151
|
-
let count = 0;
|
|
152
|
-
const cancel = registry.subscribe(graph.connections(ROOT_ID), (nodes) => {
|
|
153
|
-
count++;
|
|
154
|
-
if (nodes.length > 0) {
|
|
155
|
-
loaded.wake();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
onTestFinished(() => cancel());
|
|
159
|
-
registry.get(graph.connections(ROOT_ID));
|
|
160
|
-
expect(count).to.eq(1);
|
|
161
|
-
|
|
162
|
-
graph.expand(ROOT_ID);
|
|
163
|
-
await loaded.wait();
|
|
164
|
-
expect(count).to.eq(2);
|
|
165
|
-
|
|
166
|
-
const nodes = registry.get(graph.connections(ROOT_ID));
|
|
167
|
-
expect(nodes).has.length(1);
|
|
168
|
-
expect(nodes[0].id).to.eq(innerId);
|
|
169
|
-
expect(nodes[0].data).to.eq('inner');
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('query graph builder', async () => {
|
|
174
|
-
const registry = Registry.make();
|
|
175
|
-
await using peer = await dbBuilder.createPeer();
|
|
176
|
-
await using db = await peer.createDatabase();
|
|
177
|
-
db.add(live(Expando, { name: 'a' }));
|
|
178
|
-
db.add(live(Expando, { name: 'b' }));
|
|
179
|
-
|
|
180
|
-
const builder = new GraphBuilder({ registry });
|
|
181
|
-
builder.addExtension(
|
|
182
|
-
createExtension({
|
|
183
|
-
id: 'expando',
|
|
184
|
-
connector: () => {
|
|
185
|
-
const query = db.query(Filter.type(Expando));
|
|
186
|
-
|
|
187
|
-
return Rx.make((get) => {
|
|
188
|
-
const objects = get(rxFromQuery(query));
|
|
189
|
-
return objects.map((object) => ({ id: object.id, type: EXAMPLE_TYPE, data: object.name }));
|
|
190
|
-
});
|
|
191
|
-
},
|
|
192
|
-
}),
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const graph = builder.graph;
|
|
196
|
-
let count = 0;
|
|
197
|
-
const cancel = registry.subscribe(graph.connections(ROOT_ID), (nodes) => {
|
|
198
|
-
count = nodes.length;
|
|
199
|
-
});
|
|
200
|
-
onTestFinished(() => cancel());
|
|
201
|
-
|
|
202
|
-
registry.get(graph.connections(ROOT_ID));
|
|
203
|
-
expect(count).to.eq(0);
|
|
204
|
-
|
|
205
|
-
graph.expand(ROOT_ID);
|
|
206
|
-
expect(count).to.eq(2);
|
|
207
|
-
|
|
208
|
-
const object = db.add(live(Expando, { name: 'c' }));
|
|
209
|
-
await db.flush();
|
|
210
|
-
expect(count).to.eq(3);
|
|
211
|
-
|
|
212
|
-
// NOTE: This graph builder is not reactive to the object update.
|
|
213
|
-
object.name = 'updated';
|
|
214
|
-
await db.flush();
|
|
215
|
-
expect(count).to.eq(3);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
});
|
package/src/testing.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Rx } from '@effect-rx/rx-react';
|
|
6
|
-
|
|
7
|
-
import { type QueryResult } from '@dxos/echo-db';
|
|
8
|
-
import { type BaseObject } from '@dxos/echo-schema';
|
|
9
|
-
|
|
10
|
-
export const rxFromQuery = <T extends BaseObject>(query: QueryResult<T>): Rx.Rx<T[]> => {
|
|
11
|
-
return Rx.make((get) => {
|
|
12
|
-
const unsubscribe = query.subscribe((result) => {
|
|
13
|
-
get.setSelf(result.objects);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
get.addFinalizer(() => unsubscribe());
|
|
17
|
-
|
|
18
|
-
return query.objects;
|
|
19
|
-
});
|
|
20
|
-
};
|