@dxos/app-graph 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef
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/chunk-3T75MQOS.mjs +1480 -0
- package/dist/lib/browser/chunk-3T75MQOS.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +27 -842
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-UEXRLXMS.mjs +1481 -0
- package/dist/lib/node-esm/chunk-UEXRLXMS.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +27 -843
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/atoms.d.ts +8 -0
- package/dist/types/src/atoms.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +113 -67
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +188 -222
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +7 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +244 -0
- package/dist/types/src/node-matcher.d.ts.map +1 -0
- package/dist/types/src/node-matcher.test.d.ts +2 -0
- package/dist/types/src/node-matcher.test.d.ts.map +1 -0
- package/dist/types/src/node.d.ts +50 -5
- 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/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +40 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +42 -38
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1154 -144
- package/src/graph-builder.ts +737 -293
- package/src/graph.test.ts +451 -123
- package/src/graph.ts +1054 -403
- package/src/index.ts +10 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +314 -0
- package/src/node.ts +82 -8
- package/src/stories/EchoGraph.stories.tsx +164 -126
- package/src/stories/Tree.tsx +1 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +101 -0
- 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/util.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { invariant } from '@dxos/invariant';
|
|
6
|
+
|
|
7
|
+
import * as Node from './node';
|
|
8
|
+
|
|
9
|
+
// PRIMARY separates top-level components (e.g., node ID from relation) in compound string keys used within the app-graph package.
|
|
10
|
+
const PRIMARY = '\u0001';
|
|
11
|
+
|
|
12
|
+
// SECONDARY separates sub-components within an encoded value (e.g., relation kind from direction) in the same context.
|
|
13
|
+
const SECONDARY = '\u0002';
|
|
14
|
+
|
|
15
|
+
// PATH separates segments in qualified node IDs (e.g., parent path from local segment).
|
|
16
|
+
const PATH = '/';
|
|
17
|
+
|
|
18
|
+
/** Join parts with the primary separator. */
|
|
19
|
+
export const primaryKey = (...parts: string[]): string => parts.join(PRIMARY);
|
|
20
|
+
|
|
21
|
+
/** Split a key on the primary separator. */
|
|
22
|
+
export const primaryParts = (key: string): string[] => key.split(PRIMARY);
|
|
23
|
+
|
|
24
|
+
/** Join parts with the secondary separator. */
|
|
25
|
+
export const secondaryKey = (...parts: string[]): string => parts.join(SECONDARY);
|
|
26
|
+
|
|
27
|
+
/** Split a key on the secondary separator. */
|
|
28
|
+
export const secondaryParts = (key: string): string[] => key.split(SECONDARY);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize a relation input to a full Relation object.
|
|
32
|
+
*/
|
|
33
|
+
export const normalizeRelation = (relation?: Node.RelationInput): Node.Relation =>
|
|
34
|
+
relation == null ? Node.childRelation() : typeof relation === 'string' ? Node.relation(relation) : relation;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shallow-compare two values: same reference, or same own-keys with === values.
|
|
38
|
+
*/
|
|
39
|
+
export const shallowEqual = (a: unknown, b: unknown): boolean => {
|
|
40
|
+
if (a === b) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const keysA = Object.keys(a as Record<string, unknown>);
|
|
47
|
+
const keysB = Object.keys(b as Record<string, unknown>);
|
|
48
|
+
if (keysA.length !== keysB.length) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return keysA.every((k) => (a as Record<string, unknown>)[k] === (b as Record<string, unknown>)[k]);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if two NodeArg arrays are semantically identical (same id, type, data, properties per index).
|
|
56
|
+
* Inline child nodes (the `nodes` field) are compared recursively.
|
|
57
|
+
*/
|
|
58
|
+
export const nodeArgsUnchanged = (prev: Node.NodeArg<any>[], next: Node.NodeArg<any>[]): boolean => {
|
|
59
|
+
if (prev.length !== next.length) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return prev.every((prevNode, idx) => {
|
|
64
|
+
const nextNode = next[idx];
|
|
65
|
+
return (
|
|
66
|
+
prevNode.id === nextNode.id &&
|
|
67
|
+
prevNode.type === nextNode.type &&
|
|
68
|
+
shallowEqual(prevNode.data, nextNode.data) &&
|
|
69
|
+
shallowEqual(prevNode.properties, nextNode.properties) &&
|
|
70
|
+
nodeArgsUnchanged(prevNode.nodes ?? [], nextNode.nodes ?? [])
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a qualified node ID by joining path segments.
|
|
77
|
+
*/
|
|
78
|
+
export const qualifyId = (parentId: string, ...segmentIds: string[]): string => [parentId, ...segmentIds].join(PATH);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate that a segment ID does not contain the path separator.
|
|
82
|
+
*/
|
|
83
|
+
export const validateSegmentId = (id: string): void => {
|
|
84
|
+
invariant(!id.includes(PATH), `Node segment ID must not contain '${PATH}': ${id}`);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract the parent qualified ID (everything before the last path separator).
|
|
89
|
+
* Returns undefined for IDs with no parent (single segment).
|
|
90
|
+
*/
|
|
91
|
+
export const getParentId = (qualifiedId: string): string | undefined => {
|
|
92
|
+
const lastSlash = qualifiedId.lastIndexOf(PATH);
|
|
93
|
+
return lastSlash > 0 ? qualifiedId.slice(0, lastSlash) : undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract the last segment of a qualified ID.
|
|
98
|
+
*/
|
|
99
|
+
export const getSegmentId = (qualifiedId: string): string => {
|
|
100
|
+
return qualifiedId.split(PATH).pop() ?? qualifiedId;
|
|
101
|
+
};
|
|
@@ -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 AnyEchoObject } from '@dxos/echo-schema';
|
|
4
|
-
export declare const rxFromQuery: <T extends AnyEchoObject>(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,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,aAAa,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAUrF,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 { GraphBuilder, createExtension, 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
|
-
});
|
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 AnyEchoObject } from '@dxos/echo-schema';
|
|
9
|
-
|
|
10
|
-
export const rxFromQuery = <T extends AnyEchoObject>(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
|
-
};
|