@dxos/app-graph 0.8.2-staging.7ac8446 → 0.8.2
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 +593 -794
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +585 -785
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +593 -794
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/experimental/graph-projections.test.d.ts +25 -0
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +48 -91
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +191 -98
- 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/signals-integration.test.d.ts +2 -0
- package/dist/types/src/signals-integration.test.d.ts.map +1 -0
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +5 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +24 -17
- package/src/experimental/graph-projections.test.ts +56 -0
- package/src/graph-builder.test.ts +293 -310
- package/src/graph-builder.ts +235 -318
- package/src/graph.test.ts +314 -463
- package/src/graph.ts +452 -455
- package/src/node.ts +4 -4
- package/src/signals-integration.test.ts +218 -0
- package/src/stories/EchoGraph.stories.tsx +67 -76
- package/src/testing.ts +20 -0
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<TData = any, TProperties 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<TData, TProperties>;
|
|
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
|
+
parent?: 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'> & {
|
|
@@ -0,0 +1,218 @@
|
|
|
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(Filter.ids(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(Filter.ids(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
|
+
});
|
|
@@ -4,37 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
|
+
import { Rx, useRxValue } from '@effect-rx/rx-react';
|
|
7
8
|
import { Pause, Play, Plus, Timer } from '@phosphor-icons/react';
|
|
8
|
-
import
|
|
9
|
+
import { Option, pipe } from 'effect';
|
|
10
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
9
11
|
|
|
10
12
|
import {
|
|
11
|
-
|
|
13
|
+
live,
|
|
12
14
|
isSpace,
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
type ReactiveEchoObject,
|
|
15
|
+
Query,
|
|
16
|
+
type QueryResult,
|
|
16
17
|
type Space,
|
|
17
18
|
SpaceState,
|
|
18
|
-
|
|
19
|
-
type
|
|
19
|
+
Expando,
|
|
20
|
+
type Live,
|
|
21
|
+
Filter,
|
|
20
22
|
} from '@dxos/client/echo';
|
|
21
23
|
import { faker } from '@dxos/random';
|
|
22
24
|
import { type Client, useClient } from '@dxos/react-client';
|
|
23
25
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
24
|
-
import { Button, Input, Select
|
|
26
|
+
import { Button, Input, Select } from '@dxos/react-ui';
|
|
25
27
|
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
26
28
|
import { withTheme } from '@dxos/storybook-utils';
|
|
27
29
|
import { safeParseInt } from '@dxos/util';
|
|
28
30
|
|
|
29
31
|
import { Tree } from './Tree';
|
|
30
|
-
import { type
|
|
31
|
-
import { GraphBuilder,
|
|
32
|
-
import {
|
|
32
|
+
import { type ExpandableGraph, ROOT_ID } from '../graph';
|
|
33
|
+
import { GraphBuilder, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
|
|
34
|
+
import { rxFromQuery } from '../testing';
|
|
33
35
|
|
|
34
36
|
const DEFAULT_PERIOD = 500;
|
|
35
37
|
|
|
36
|
-
const EMPTY_ARRAY: never[] = [];
|
|
37
|
-
|
|
38
38
|
enum Action {
|
|
39
39
|
CREATE_SPACE = 'CREATE_SPACE',
|
|
40
40
|
CLOSE_SPACE = 'CLOSE_SPACE',
|
|
@@ -53,70 +53,61 @@ const actionWeights = {
|
|
|
53
53
|
[Action.RENAME_OBJECT]: 4,
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
|
|
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> => {
|
|
56
|
+
const createGraph = (client: Client): ExpandableGraph => {
|
|
77
57
|
const spaceBuilderExtension = createExtension({
|
|
78
58
|
id: 'space',
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
59
|
+
connector: (node) =>
|
|
60
|
+
Rx.make((get) =>
|
|
61
|
+
pipe(
|
|
62
|
+
get(node),
|
|
63
|
+
Option.flatMap((node) => (node.id === ROOT_ID ? Option.some(node) : Option.none())),
|
|
64
|
+
Option.map(() => {
|
|
65
|
+
const spaces = get(rxFromObservable(client.spaces)) ?? [];
|
|
66
|
+
return spaces
|
|
67
|
+
.filter((space) => get(rxFromObservable(space.state)) === SpaceState.SPACE_READY)
|
|
68
|
+
.map((space) => ({
|
|
69
|
+
id: space.id,
|
|
70
|
+
type: 'dxos.org/type/Space',
|
|
71
|
+
properties: { label: get(rxFromSignal(() => space.properties.name)) },
|
|
72
|
+
data: space,
|
|
73
|
+
}));
|
|
74
|
+
}),
|
|
75
|
+
Option.getOrElse(() => []),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
98
78
|
});
|
|
99
79
|
|
|
100
80
|
const objectBuilderExtension = createExtension({
|
|
101
81
|
id: 'object',
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
connector: (node) => {
|
|
83
|
+
let query: QueryResult<Live<Expando>> | undefined;
|
|
84
|
+
return Rx.make((get) =>
|
|
85
|
+
pipe(
|
|
86
|
+
get(node),
|
|
87
|
+
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
88
|
+
Option.map((space) => {
|
|
89
|
+
if (!query) {
|
|
90
|
+
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
91
|
+
}
|
|
92
|
+
return get(rxFromQuery(query)).map((object) => ({
|
|
93
|
+
id: object.id,
|
|
94
|
+
type: 'dxos.org/type/test',
|
|
95
|
+
properties: { label: object.name },
|
|
96
|
+
data: object,
|
|
97
|
+
}));
|
|
98
|
+
}),
|
|
99
|
+
Option.getOrElse(() => []),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
111
102
|
},
|
|
112
103
|
});
|
|
113
104
|
|
|
114
105
|
const graph = new GraphBuilder().addExtension(spaceBuilderExtension).addExtension(objectBuilderExtension).graph;
|
|
115
|
-
graph.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
},
|
|
106
|
+
graph.onNodeChanged.on(({ id }) => {
|
|
107
|
+
console.log('onNodeChanged', { id });
|
|
108
|
+
graph.expand(id);
|
|
119
109
|
});
|
|
110
|
+
graph.expand(ROOT_ID);
|
|
120
111
|
|
|
121
112
|
return graph;
|
|
122
113
|
};
|
|
@@ -136,7 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
136
127
|
|
|
137
128
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
138
129
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
139
|
-
const spaceQueries = await Promise.all(
|
|
130
|
+
const spaceQueries = await Promise.all(
|
|
131
|
+
readySpaces.map((space) => space.db.query(Filter.type(Expando, { type: 'test' })).run()),
|
|
132
|
+
);
|
|
140
133
|
const spaces = readySpaces.filter((space, index) => spaceQueries[index].objects.length > 0);
|
|
141
134
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
142
135
|
};
|
|
@@ -160,13 +153,13 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
case Action.ADD_OBJECT:
|
|
163
|
-
getRandomSpace(client)?.db.add(
|
|
156
|
+
getRandomSpace(client)?.db.add(live({ type: 'test', name: faker.commerce.productName() }));
|
|
164
157
|
break;
|
|
165
158
|
|
|
166
159
|
case Action.REMOVE_OBJECT: {
|
|
167
160
|
const space = await getSpaceWithObjects(client);
|
|
168
161
|
if (space) {
|
|
169
|
-
const { objects } = await space.db.query({ type: 'test' }).run();
|
|
162
|
+
const { objects } = await space.db.query(Filter.type(Expando, { type: 'test' })).run();
|
|
170
163
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
171
164
|
}
|
|
172
165
|
break;
|
|
@@ -175,7 +168,7 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
175
168
|
case Action.RENAME_OBJECT: {
|
|
176
169
|
const space = await getSpaceWithObjects(client);
|
|
177
170
|
if (space) {
|
|
178
|
-
const { objects } = await space.db.query({ type: 'test' }).run();
|
|
171
|
+
const { objects } = await space.db.query(Filter.type(Expando, { type: 'test' })).run();
|
|
179
172
|
objects[Math.floor(Math.random() * objects.length)].name = faker.commerce.productName();
|
|
180
173
|
}
|
|
181
174
|
break;
|
|
@@ -189,10 +182,8 @@ const DefaultStory = () => {
|
|
|
189
182
|
const [action, setAction] = useState<Action>();
|
|
190
183
|
|
|
191
184
|
const client = useClient();
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
setGraph(await createGraph(client));
|
|
195
|
-
}, [client]);
|
|
185
|
+
const graph = useMemo(() => createGraph(client), [client]);
|
|
186
|
+
const data = useRxValue(graph.json());
|
|
196
187
|
|
|
197
188
|
useEffect(() => {
|
|
198
189
|
if (!generating) {
|
|
@@ -244,7 +235,7 @@ const DefaultStory = () => {
|
|
|
244
235
|
</Select.Portal>
|
|
245
236
|
</Select.Root>
|
|
246
237
|
</div>
|
|
247
|
-
{
|
|
238
|
+
{data && <Tree data={data} />}
|
|
248
239
|
</>
|
|
249
240
|
);
|
|
250
241
|
};
|
|
@@ -256,7 +247,7 @@ export default {
|
|
|
256
247
|
withTheme,
|
|
257
248
|
withClientProvider({
|
|
258
249
|
createIdentity: true,
|
|
259
|
-
|
|
250
|
+
onIdentityCreated: async ({ client }) => {
|
|
260
251
|
await client.spaces.create();
|
|
261
252
|
await client.spaces.create();
|
|
262
253
|
},
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
};
|