@dxos/echo-solid 0.0.0
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/LICENSE +8 -0
- package/README.md +1 -0
- package/package.json +46 -0
- package/src/index.ts +8 -0
- package/src/useObject.test.tsx +362 -0
- package/src/useObject.ts +205 -0
- package/src/useQuery.test.tsx +277 -0
- package/src/useQuery.ts +61 -0
- package/src/useRef.test.tsx +247 -0
- package/src/useRef.ts +48 -0
- package/src/useSchema.test.tsx +208 -0
- package/src/useSchema.ts +59 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { render, waitFor } from '@solidjs/testing-library';
|
|
6
|
+
import { type JSX, createMemo } from 'solid-js';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { Type } from '@dxos/echo';
|
|
10
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
11
|
+
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
|
+
|
|
13
|
+
import { useSchema } from './useSchema';
|
|
14
|
+
|
|
15
|
+
describe('useSchema', () => {
|
|
16
|
+
let testBuilder: EchoTestBuilder;
|
|
17
|
+
let db: any;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
testBuilder = new EchoTestBuilder();
|
|
21
|
+
const result = await testBuilder.createDatabase();
|
|
22
|
+
db = result.db;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await testBuilder.close();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('returns undefined when database is undefined', () => {
|
|
30
|
+
let result: any;
|
|
31
|
+
|
|
32
|
+
render(() => {
|
|
33
|
+
const schema = useSchema(undefined, 'dxos.test.Person');
|
|
34
|
+
result = schema();
|
|
35
|
+
return (<div>test</div>) as JSX.Element;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns undefined when typename is undefined', () => {
|
|
42
|
+
let result: any;
|
|
43
|
+
|
|
44
|
+
render(() => {
|
|
45
|
+
const schema = useSchema(db, undefined);
|
|
46
|
+
result = schema();
|
|
47
|
+
return (<div>test</div>) as JSX.Element;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('returns undefined when schema does not exist', async () => {
|
|
54
|
+
let result: any;
|
|
55
|
+
|
|
56
|
+
render(() => {
|
|
57
|
+
const schema = useSchema(db, 'dxos.test.NonExistent');
|
|
58
|
+
result = schema();
|
|
59
|
+
return (<div>test</div>) as JSX.Element;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(result).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('returns schema when it exists', async () => {
|
|
68
|
+
// Register a schema to the database
|
|
69
|
+
const [registeredSchema] = await db.schemaRegistry.register([TestSchema.Person]);
|
|
70
|
+
await db.flush({ indexes: true });
|
|
71
|
+
|
|
72
|
+
let schemaAccessor: (() => any) | undefined;
|
|
73
|
+
|
|
74
|
+
function TestComponent() {
|
|
75
|
+
const schema = useSchema(db, registeredSchema.typename);
|
|
76
|
+
schemaAccessor = schema;
|
|
77
|
+
const t = createMemo(() => {
|
|
78
|
+
const s = schema();
|
|
79
|
+
return s ? Type.getTypename(s) : undefined;
|
|
80
|
+
});
|
|
81
|
+
return (<div data-testid='typename'>{t() || 'undefined'}</div>) as JSX.Element;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(getByTestId('typename').textContent).toBe(registeredSchema.typename);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Get the actual result from the accessor
|
|
91
|
+
const result = schemaAccessor?.();
|
|
92
|
+
expect(result).toBeDefined();
|
|
93
|
+
expect(result?.typename).toBe(registeredSchema.typename);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test.skip('updates when schema is added', async () => {
|
|
97
|
+
// TODO: This test is skipped because runtime schema registry is not reactive to newly registered schemas.
|
|
98
|
+
// The query reads from db.graph.schemaRegistry.schemas (runtime registry), but when a schema is registered
|
|
99
|
+
// via db.schemaRegistry.register(), it's added to the database registry, not the runtime registry.
|
|
100
|
+
// The runtime registry query subscription doesn't fire when schemas are registered.
|
|
101
|
+
// See: packages/core/echo/echo-db/src/proxy-db/runtime-schema-registry.ts:57
|
|
102
|
+
let schemaAccessor: (() => any) | undefined;
|
|
103
|
+
const typename = 'example.com/type/Person';
|
|
104
|
+
|
|
105
|
+
function TestComponent() {
|
|
106
|
+
const schema = useSchema(db, typename);
|
|
107
|
+
schemaAccessor = schema;
|
|
108
|
+
const t = createMemo(() => {
|
|
109
|
+
const s = schema();
|
|
110
|
+
return s ? Type.getTypename(s) : undefined;
|
|
111
|
+
});
|
|
112
|
+
return (<div data-testid='typename'>{t() || 'undefined'}</div>) as JSX.Element;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(getByTestId('typename').textContent).toBe('undefined');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Register the schema
|
|
122
|
+
const [registeredSchema] = await db.schemaRegistry.register([TestSchema.Person]);
|
|
123
|
+
await db.flush({ indexes: true });
|
|
124
|
+
|
|
125
|
+
// The schema registry query should pick up the new schema
|
|
126
|
+
// It reads from db.graph.schemaRegistry.schemas which should include the newly registered schema
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(getByTestId('typename').textContent).toBe(registeredSchema.typename);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Get the actual result from the accessor
|
|
132
|
+
const result = schemaAccessor?.();
|
|
133
|
+
expect(result).toBeDefined();
|
|
134
|
+
expect(result?.typename).toBe(registeredSchema.typename);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('accepts reactive database accessor', async () => {
|
|
138
|
+
const [registeredSchema] = await db.schemaRegistry.register([TestSchema.Person]);
|
|
139
|
+
|
|
140
|
+
let schemaAccessor: (() => any) | undefined;
|
|
141
|
+
let dbAccessor: any = db;
|
|
142
|
+
|
|
143
|
+
function TestComponent() {
|
|
144
|
+
const schema = useSchema(() => dbAccessor, registeredSchema.typename);
|
|
145
|
+
schemaAccessor = schema;
|
|
146
|
+
const t = createMemo(() => {
|
|
147
|
+
const s = schema();
|
|
148
|
+
return s ? Type.getTypename(s) : undefined;
|
|
149
|
+
});
|
|
150
|
+
return (<div data-testid='typename'>{t() || 'undefined'}</div>) as JSX.Element;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(getByTestId('typename').textContent).toBe(registeredSchema.typename);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Get the actual result from the accessor
|
|
160
|
+
let result = schemaAccessor?.();
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
|
|
163
|
+
// Change database to undefined
|
|
164
|
+
dbAccessor = undefined;
|
|
165
|
+
|
|
166
|
+
// Should keep previous value (no flickering)
|
|
167
|
+
result = schemaAccessor?.();
|
|
168
|
+
expect(result).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('accepts reactive typename accessor', async () => {
|
|
172
|
+
const [registeredSchema] = await db.schemaRegistry.register([TestSchema.Person]);
|
|
173
|
+
|
|
174
|
+
let schemaAccessor: (() => any) | undefined;
|
|
175
|
+
let typename: string | undefined = registeredSchema.typename;
|
|
176
|
+
|
|
177
|
+
function TestComponent() {
|
|
178
|
+
const schema = useSchema(db, () => typename);
|
|
179
|
+
schemaAccessor = schema;
|
|
180
|
+
const t = createMemo(() => {
|
|
181
|
+
const s = schema();
|
|
182
|
+
return s ? Type.getTypename(s) : undefined;
|
|
183
|
+
});
|
|
184
|
+
return (<div data-testid='typename'>{t() || 'undefined'}</div>) as JSX.Element;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
188
|
+
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(getByTestId('typename').textContent).toBe(registeredSchema.typename);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Get the actual result from the accessor
|
|
194
|
+
let result = schemaAccessor?.();
|
|
195
|
+
expect(result).toBeDefined();
|
|
196
|
+
expect(result?.typename).toBe(registeredSchema.typename);
|
|
197
|
+
|
|
198
|
+
// Change typename
|
|
199
|
+
typename = undefined;
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
// Should keep previous value when typename becomes undefined
|
|
203
|
+
expect(getByTestId('typename').textContent).toBe(registeredSchema.typename);
|
|
204
|
+
});
|
|
205
|
+
result = schemaAccessor?.();
|
|
206
|
+
expect(result).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
});
|
package/src/useSchema.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Accessor, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
6
|
+
|
|
7
|
+
import { type Database, type Type } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
type MaybeAccessor<T> = T | Accessor<T>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to and retrieve schema changes from a database's schema registry.
|
|
13
|
+
* Accepts either values or accessors for db and typename.
|
|
14
|
+
*
|
|
15
|
+
* @param db - The database instance (can be reactive)
|
|
16
|
+
* @param typename - The schema typename to query (can be reactive)
|
|
17
|
+
* @returns An accessor that returns the current schema or undefined
|
|
18
|
+
*/
|
|
19
|
+
export const useSchema = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
20
|
+
db?: MaybeAccessor<Database.Database | undefined>,
|
|
21
|
+
typename?: MaybeAccessor<string | undefined>,
|
|
22
|
+
): Accessor<T | undefined> => {
|
|
23
|
+
// Derive the schema query reactively
|
|
24
|
+
const query = createMemo(() => {
|
|
25
|
+
const resolvedDb = typeof db === 'function' ? db() : db;
|
|
26
|
+
const resolvedTypename = typeof typename === 'function' ? typename() : typename;
|
|
27
|
+
if (!resolvedTypename || !resolvedDb) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return resolvedDb.schemaRegistry.query({ typename: resolvedTypename, location: ['database', 'runtime'] });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Store the current schema in a signal
|
|
34
|
+
const [schema, setSchema] = createSignal<T | undefined>(undefined);
|
|
35
|
+
|
|
36
|
+
// Subscribe to query changes
|
|
37
|
+
createEffect(() => {
|
|
38
|
+
const q = query();
|
|
39
|
+
if (!q) {
|
|
40
|
+
// Keep previous value during transitions to prevent flickering
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Subscribe to updates with immediate fire to get initial result and track changes
|
|
45
|
+
// The subscription will automatically start the reactive query
|
|
46
|
+
const unsubscribe = q.subscribe(
|
|
47
|
+
() => {
|
|
48
|
+
// Access results inside the callback to ensure query is running
|
|
49
|
+
const results = q.results;
|
|
50
|
+
setSchema(() => results[0] as T | undefined);
|
|
51
|
+
},
|
|
52
|
+
{ fire: true },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
onCleanup(unsubscribe);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return schema;
|
|
59
|
+
};
|