@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.
@@ -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
+ });
@@ -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
+ };