@dxos/echo-solid 0.8.4-main.fcfe5033a5 → 0.9.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 +102 -5
- package/dist/lib/neutral/index.mjs +68 -36
- package/dist/lib/neutral/index.mjs.map +4 -4
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/useObject.d.ts.map +1 -1
- package/dist/types/src/useQuery.d.ts.map +1 -1
- package/dist/types/src/useType.d.ts +19 -0
- package/dist/types/src/useType.d.ts.map +1 -0
- package/dist/types/src/useType.test.d.ts +2 -0
- package/dist/types/src/useType.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -12
- package/src/index.ts +1 -1
- package/src/useObject.test.tsx +7 -7
- package/src/useObject.ts +38 -12
- package/src/useQuery.test.tsx +3 -3
- package/src/{useSchema.test.tsx → useType.test.tsx} +31 -33
- package/src/useType.ts +83 -0
- package/dist/types/src/useSchema.d.ts +0 -14
- package/dist/types/src/useSchema.d.ts.map +0 -1
- package/dist/types/src/useSchema.test.d.ts +0 -2
- package/dist/types/src/useSchema.test.d.ts.map +0 -1
- package/src/useSchema.ts +0 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Solid.js integration for ECHO.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"type": "module",
|
|
@@ -20,26 +20,22 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"types": "dist/types/src/index.d.ts",
|
|
23
|
-
"typesVersions": {
|
|
24
|
-
"*": {}
|
|
25
|
-
},
|
|
26
23
|
"files": [
|
|
27
24
|
"dist",
|
|
28
25
|
"src"
|
|
29
26
|
],
|
|
30
27
|
"dependencies": {
|
|
31
|
-
"@effect-atom/atom": "^0.5.
|
|
28
|
+
"@effect-atom/atom": "^0.5.3",
|
|
32
29
|
"@solid-primitives/utils": "^6.3.2",
|
|
33
|
-
"@dxos/echo": "0.
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/effect-atom-solid": "0.8.4-main.fcfe5033a5"
|
|
30
|
+
"@dxos/echo": "0.9.0",
|
|
31
|
+
"@dxos/effect-atom-solid": "0.9.0"
|
|
36
32
|
},
|
|
37
33
|
"devDependencies": {
|
|
38
34
|
"@solidjs/testing-library": "^0.8.10",
|
|
39
35
|
"solid-js": "^1.9.11",
|
|
40
|
-
"vite-plugin-solid": "^2.11.
|
|
41
|
-
"vitest": "
|
|
42
|
-
"@dxos/echo-
|
|
36
|
+
"vite-plugin-solid": "^2.11.12",
|
|
37
|
+
"vitest": "4.1.7",
|
|
38
|
+
"@dxos/echo-client": "0.9.0"
|
|
43
39
|
},
|
|
44
40
|
"peerDependencies": {
|
|
45
41
|
"solid-js": "^1.9.11"
|
package/src/index.ts
CHANGED
package/src/useObject.test.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { describe, expect, test } from 'vitest';
|
|
|
8
8
|
|
|
9
9
|
import type { Entity } from '@dxos/echo';
|
|
10
10
|
import { Obj } from '@dxos/echo';
|
|
11
|
-
import { createObject } from '@dxos/echo-
|
|
11
|
+
import { createObject } from '@dxos/echo-client';
|
|
12
12
|
import { TestSchema } from '@dxos/echo/testing';
|
|
13
13
|
import { Registry } from '@dxos/effect-atom-solid';
|
|
14
14
|
import { RegistryProvider } from '@dxos/effect-atom-solid';
|
|
@@ -86,8 +86,8 @@ describe('useObject', () => {
|
|
|
86
86
|
expect(result).toBe('Test');
|
|
87
87
|
expect(getByTestId('value').textContent).toBe('Test');
|
|
88
88
|
|
|
89
|
-
// Update the property via Obj.
|
|
90
|
-
Obj.
|
|
89
|
+
// Update the property via Obj.update.
|
|
90
|
+
Obj.update(obj, (obj) => {
|
|
91
91
|
obj.name = 'Updated';
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -118,8 +118,8 @@ describe('useObject', () => {
|
|
|
118
118
|
expect(valueAccessor?.()?.name).toBe('Test');
|
|
119
119
|
expect(getByTestId('name').textContent).toBe('Test');
|
|
120
120
|
|
|
121
|
-
// Update a property via Obj.
|
|
122
|
-
Obj.
|
|
121
|
+
// Update a property via Obj.update.
|
|
122
|
+
Obj.update(obj, (obj) => {
|
|
123
123
|
obj.name = 'Updated';
|
|
124
124
|
});
|
|
125
125
|
|
|
@@ -149,8 +149,8 @@ describe('useObject', () => {
|
|
|
149
149
|
|
|
150
150
|
expect(result).toBe('Test');
|
|
151
151
|
|
|
152
|
-
// Update a different property via Obj.
|
|
153
|
-
Obj.
|
|
152
|
+
// Update a different property via Obj.update.
|
|
153
|
+
Obj.update(obj, (obj) => {
|
|
154
154
|
obj.email = 'newemail@example.com';
|
|
155
155
|
});
|
|
156
156
|
|
package/src/useObject.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { type MaybeAccessor, access } from '@solid-primitives/utils';
|
|
|
6
6
|
import { type Accessor, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
7
7
|
|
|
8
8
|
import { Obj, Ref } from '@dxos/echo';
|
|
9
|
-
import { AtomObj } from '@dxos/echo-atom';
|
|
10
9
|
import { type Registry, useRegistry } from '@dxos/effect-atom-solid';
|
|
11
10
|
|
|
12
11
|
export interface ObjectUpdateCallback<T> {
|
|
@@ -146,10 +145,36 @@ export function useObject<T extends Obj.Unknown, K extends keyof T>(
|
|
|
146
145
|
// Determine if input is a ref.
|
|
147
146
|
const isRef = createMemo(() => Ref.isRef(resolvedInput()));
|
|
148
147
|
|
|
148
|
+
// Signal to notify when a ref's target loads.
|
|
149
|
+
// Incremented by the ref.atom subscription below so that liveObj re-evaluates.
|
|
150
|
+
const [refLoadVersion, setRefLoadVersion] = createSignal(0, { equals: false });
|
|
151
|
+
|
|
152
|
+
// Subscribe to ref.atom (load-only) to detect when the ref target becomes available.
|
|
153
|
+
// This drives liveObj reactivity for the property-subscription case.
|
|
154
|
+
createEffect(() => {
|
|
155
|
+
const input = resolvedInput();
|
|
156
|
+
if (!input || !Ref.isRef(input)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const unsubscribe = registry.subscribe(
|
|
160
|
+
input.atom,
|
|
161
|
+
() => {
|
|
162
|
+
setRefLoadVersion((v) => v + 1);
|
|
163
|
+
},
|
|
164
|
+
{ immediate: false },
|
|
165
|
+
);
|
|
166
|
+
onCleanup(unsubscribe);
|
|
167
|
+
});
|
|
168
|
+
|
|
149
169
|
// Get the live object for the callback (refs need to dereference).
|
|
170
|
+
// For refs, depends on refLoadVersion so it re-evaluates when the target loads.
|
|
150
171
|
const liveObj = createMemo(() => {
|
|
151
172
|
const input = resolvedInput();
|
|
152
|
-
|
|
173
|
+
if (!isRef()) {
|
|
174
|
+
return input as T | undefined;
|
|
175
|
+
}
|
|
176
|
+
refLoadVersion();
|
|
177
|
+
return (input as Ref.Ref<T>)?.target;
|
|
153
178
|
});
|
|
154
179
|
|
|
155
180
|
// Create a stable callback that handles both object and property updates.
|
|
@@ -160,7 +185,7 @@ export function useObject<T extends Obj.Unknown, K extends keyof T>(
|
|
|
160
185
|
return;
|
|
161
186
|
}
|
|
162
187
|
|
|
163
|
-
Obj.
|
|
188
|
+
Obj.update(obj, (obj: any) => {
|
|
164
189
|
if (typeof updateOrValue === 'function') {
|
|
165
190
|
const returnValue = (updateOrValue as (obj: unknown) => unknown)(property !== undefined ? obj[property] : obj);
|
|
166
191
|
if (returnValue !== undefined) {
|
|
@@ -179,16 +204,15 @@ export function useObject<T extends Obj.Unknown, K extends keyof T>(
|
|
|
179
204
|
};
|
|
180
205
|
|
|
181
206
|
if (property !== undefined) {
|
|
182
|
-
// For property subscriptions on refs, we subscribe to trigger re-render on load.
|
|
183
|
-
useObjectValue(registry, objOrRef);
|
|
184
207
|
return [useObjectProperty(registry, liveObj, property), callback as ObjectPropUpdateCallback<T[K]>];
|
|
208
|
+
} else {
|
|
209
|
+
return [useObjectValue(registry, objOrRef), callback as ObjectUpdateCallback<T>];
|
|
185
210
|
}
|
|
186
|
-
return [useObjectValue(registry, objOrRef), callback as ObjectUpdateCallback<T>];
|
|
187
211
|
}
|
|
188
212
|
|
|
189
213
|
/**
|
|
190
214
|
* Internal function for subscribing to an Echo object or Ref.
|
|
191
|
-
*
|
|
215
|
+
* Obj.atom handles both objects and refs, returning snapshots.
|
|
192
216
|
*/
|
|
193
217
|
function useObjectValue<T extends Obj.Unknown>(
|
|
194
218
|
registry: Registry.Registry,
|
|
@@ -199,7 +223,9 @@ function useObjectValue<T extends Obj.Unknown>(
|
|
|
199
223
|
|
|
200
224
|
// Initialize with the current value (if available).
|
|
201
225
|
const initialInput = resolvedInput();
|
|
202
|
-
const initialValue = initialInput
|
|
226
|
+
const initialValue = initialInput
|
|
227
|
+
? registry.get(Ref.isRef(initialInput) ? Obj.atom(initialInput) : Obj.atom(initialInput as T))
|
|
228
|
+
: undefined;
|
|
203
229
|
const [value, setValue] = createSignal<T | undefined>(initialValue as T | undefined);
|
|
204
230
|
|
|
205
231
|
// Subscribe to atom updates.
|
|
@@ -211,7 +237,7 @@ function useObjectValue<T extends Obj.Unknown>(
|
|
|
211
237
|
return;
|
|
212
238
|
}
|
|
213
239
|
|
|
214
|
-
const atom =
|
|
240
|
+
const atom = Ref.isRef(input) ? Obj.atom(input) : Obj.atom(input as T);
|
|
215
241
|
const currentValue = registry.get(atom);
|
|
216
242
|
setValue(() => currentValue as unknown as T);
|
|
217
243
|
|
|
@@ -239,7 +265,7 @@ function useObjectProperty<T extends Obj.Unknown, K extends keyof T>(
|
|
|
239
265
|
): Accessor<T[K] | undefined> {
|
|
240
266
|
// Initialize with the current value (if available).
|
|
241
267
|
const initialObj = obj();
|
|
242
|
-
const initialValue = initialObj ? registry.get(
|
|
268
|
+
const initialValue = initialObj ? registry.get(Obj.atomProperty(initialObj, property)) : undefined;
|
|
243
269
|
const [value, setValue] = createSignal<T[K] | undefined>(initialValue);
|
|
244
270
|
|
|
245
271
|
// Subscribe to atom updates.
|
|
@@ -251,7 +277,7 @@ function useObjectProperty<T extends Obj.Unknown, K extends keyof T>(
|
|
|
251
277
|
return;
|
|
252
278
|
}
|
|
253
279
|
|
|
254
|
-
const atom =
|
|
280
|
+
const atom = Obj.atomProperty(currentObj, property);
|
|
255
281
|
const currentValue = registry.get(atom);
|
|
256
282
|
setValue(() => currentValue);
|
|
257
283
|
|
|
@@ -301,7 +327,7 @@ export const useObjects = <T extends Obj.Unknown>(refs: MaybeAccessor<readonly R
|
|
|
301
327
|
const subscribeToTarget = (ref: Ref.Ref<T>) => {
|
|
302
328
|
const target = ref.target;
|
|
303
329
|
if (target) {
|
|
304
|
-
const key = ref.
|
|
330
|
+
const key = ref.uri;
|
|
305
331
|
if (!targetUnsubscribes.has(key)) {
|
|
306
332
|
targetUnsubscribes.set(key, Obj.subscribe(target, triggerUpdate));
|
|
307
333
|
}
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { type JSX, createSignal } from 'solid-js';
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { Filter, Obj, Query } from '@dxos/echo';
|
|
10
|
-
import { EchoTestBuilder } from '@dxos/echo-
|
|
10
|
+
import { EchoTestBuilder } from '@dxos/echo-client/testing';
|
|
11
11
|
import { TestSchema } from '@dxos/echo/testing';
|
|
12
12
|
|
|
13
13
|
import { useQuery } from './useQuery';
|
|
@@ -148,7 +148,7 @@ describe('useQuery', () => {
|
|
|
148
148
|
|
|
149
149
|
test('accepts Filter directly', async () => {
|
|
150
150
|
// Register schema first
|
|
151
|
-
|
|
151
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
152
152
|
|
|
153
153
|
const obj = Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' });
|
|
154
154
|
db.add(obj);
|
|
@@ -233,7 +233,7 @@ describe('useQuery', () => {
|
|
|
233
233
|
|
|
234
234
|
test('accepts reactive query accessor', async () => {
|
|
235
235
|
// Register schema first
|
|
236
|
-
|
|
236
|
+
db.graph.registry.add([TestSchema.Person]);
|
|
237
237
|
|
|
238
238
|
const obj1 = Obj.make(TestSchema.Expando, { name: 'Test1' });
|
|
239
239
|
const obj2 = Obj.make(TestSchema.Person, { name: 'Test2', username: 'test', email: 'test@example.com' });
|
|
@@ -6,13 +6,13 @@ import { render, waitFor } from '@solidjs/testing-library';
|
|
|
6
6
|
import { type JSX, createMemo } from 'solid-js';
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import { Type } from '@dxos/echo';
|
|
10
|
-
import { EchoTestBuilder } from '@dxos/echo-
|
|
9
|
+
import { DXN, Type, URI } from '@dxos/echo';
|
|
10
|
+
import { EchoTestBuilder } from '@dxos/echo-client/testing';
|
|
11
11
|
import { TestSchema } from '@dxos/echo/testing';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { useType } from './useType';
|
|
14
14
|
|
|
15
|
-
describe('
|
|
15
|
+
describe('useType', () => {
|
|
16
16
|
let testBuilder: EchoTestBuilder;
|
|
17
17
|
let db: any;
|
|
18
18
|
|
|
@@ -30,7 +30,7 @@ describe('useSchema', () => {
|
|
|
30
30
|
let result: any;
|
|
31
31
|
|
|
32
32
|
render(() => {
|
|
33
|
-
const schema =
|
|
33
|
+
const schema = useType(undefined, DXN.make('dxos.test.Person'));
|
|
34
34
|
result = schema();
|
|
35
35
|
return (<div>test</div>) as JSX.Element;
|
|
36
36
|
});
|
|
@@ -42,7 +42,7 @@ describe('useSchema', () => {
|
|
|
42
42
|
let result: any;
|
|
43
43
|
|
|
44
44
|
render(() => {
|
|
45
|
-
const schema =
|
|
45
|
+
const schema = useType(db, undefined);
|
|
46
46
|
result = schema();
|
|
47
47
|
return (<div>test</div>) as JSX.Element;
|
|
48
48
|
});
|
|
@@ -54,7 +54,7 @@ describe('useSchema', () => {
|
|
|
54
54
|
let result: any;
|
|
55
55
|
|
|
56
56
|
render(() => {
|
|
57
|
-
const schema =
|
|
57
|
+
const schema = useType(db, DXN.make('dxos.test.NonExistent'));
|
|
58
58
|
result = schema();
|
|
59
59
|
return (<div>test</div>) as JSX.Element;
|
|
60
60
|
});
|
|
@@ -66,13 +66,13 @@ describe('useSchema', () => {
|
|
|
66
66
|
|
|
67
67
|
test('returns schema when it exists', async () => {
|
|
68
68
|
// Register a schema to the database
|
|
69
|
-
const
|
|
69
|
+
const registeredSchema = await db.addType(TestSchema.Person);
|
|
70
70
|
await db.flush({ indexes: true });
|
|
71
71
|
|
|
72
72
|
let schemaAccessor: (() => any) | undefined;
|
|
73
73
|
|
|
74
74
|
function TestComponent() {
|
|
75
|
-
const schema =
|
|
75
|
+
const schema = useType(db, Type.getURI(registeredSchema));
|
|
76
76
|
schemaAccessor = schema;
|
|
77
77
|
const t = createMemo(() => {
|
|
78
78
|
const s = schema();
|
|
@@ -84,26 +84,24 @@ describe('useSchema', () => {
|
|
|
84
84
|
const { getByTestId } = render(() => <TestComponent />);
|
|
85
85
|
|
|
86
86
|
await waitFor(() => {
|
|
87
|
-
expect(getByTestId('typename').textContent).toBe(registeredSchema
|
|
87
|
+
expect(getByTestId('typename').textContent).toBe(Type.getTypename(registeredSchema)!);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
// Get the actual result from the accessor
|
|
91
91
|
const result = schemaAccessor?.();
|
|
92
92
|
expect(result).toBeDefined();
|
|
93
|
-
expect(result
|
|
93
|
+
expect(Type.getTypename(result!)).toBe(Type.getTypename(registeredSchema)!);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test.skip('updates when schema is added', async () => {
|
|
97
97
|
// TODO: This test is skipped because runtime schema registry is not reactive to newly registered schemas.
|
|
98
|
-
// The query reads from db.graph.
|
|
99
|
-
// via db.
|
|
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
|
|
98
|
+
// The query reads from db.graph.registry.types (shared registry), but when a schema is added
|
|
99
|
+
// via db.add(), the runtime registry query subscription doesn't fire reactively.
|
|
102
100
|
let schemaAccessor: (() => any) | undefined;
|
|
103
|
-
const typename = 'com.example.type.person';
|
|
101
|
+
const typename = DXN.make('com.example.type.person');
|
|
104
102
|
|
|
105
103
|
function TestComponent() {
|
|
106
|
-
const schema =
|
|
104
|
+
const schema = useType(db, typename);
|
|
107
105
|
schemaAccessor = schema;
|
|
108
106
|
const t = createMemo(() => {
|
|
109
107
|
const s = schema();
|
|
@@ -119,29 +117,29 @@ describe('useSchema', () => {
|
|
|
119
117
|
});
|
|
120
118
|
|
|
121
119
|
// Register the schema
|
|
122
|
-
const
|
|
120
|
+
const registeredSchema = await db.addType(TestSchema.Person);
|
|
123
121
|
await db.flush({ indexes: true });
|
|
124
122
|
|
|
125
123
|
// The schema registry query should pick up the new schema
|
|
126
|
-
//
|
|
124
|
+
// db.graph.registry.types should include the newly registered schema
|
|
127
125
|
await waitFor(() => {
|
|
128
|
-
expect(getByTestId('typename').textContent).toBe(registeredSchema
|
|
126
|
+
expect(getByTestId('typename').textContent).toBe(Type.getTypename(registeredSchema)!);
|
|
129
127
|
});
|
|
130
128
|
|
|
131
129
|
// Get the actual result from the accessor
|
|
132
130
|
const result = schemaAccessor?.();
|
|
133
131
|
expect(result).toBeDefined();
|
|
134
|
-
expect(result
|
|
132
|
+
expect(Type.getTypename(result!)).toBe(Type.getTypename(registeredSchema)!);
|
|
135
133
|
});
|
|
136
134
|
|
|
137
135
|
test('accepts reactive database accessor', async () => {
|
|
138
|
-
const
|
|
136
|
+
const registeredSchema = await db.addType(TestSchema.Person);
|
|
139
137
|
|
|
140
138
|
let schemaAccessor: (() => any) | undefined;
|
|
141
139
|
let dbAccessor: any = db;
|
|
142
140
|
|
|
143
141
|
function TestComponent() {
|
|
144
|
-
const schema =
|
|
142
|
+
const schema = useType(() => dbAccessor, Type.getURI(registeredSchema));
|
|
145
143
|
schemaAccessor = schema;
|
|
146
144
|
const t = createMemo(() => {
|
|
147
145
|
const s = schema();
|
|
@@ -153,7 +151,7 @@ describe('useSchema', () => {
|
|
|
153
151
|
const { getByTestId } = render(() => <TestComponent />);
|
|
154
152
|
|
|
155
153
|
await waitFor(() => {
|
|
156
|
-
expect(getByTestId('typename').textContent).toBe(registeredSchema
|
|
154
|
+
expect(getByTestId('typename').textContent).toBe(Type.getTypename(registeredSchema)!);
|
|
157
155
|
});
|
|
158
156
|
|
|
159
157
|
// Get the actual result from the accessor
|
|
@@ -169,13 +167,13 @@ describe('useSchema', () => {
|
|
|
169
167
|
});
|
|
170
168
|
|
|
171
169
|
test('accepts reactive typename accessor', async () => {
|
|
172
|
-
const
|
|
170
|
+
const registeredSchema = await db.addType(TestSchema.Person);
|
|
173
171
|
|
|
174
172
|
let schemaAccessor: (() => any) | undefined;
|
|
175
|
-
let
|
|
173
|
+
let typeUri: URI.URI | undefined = Type.getURI(registeredSchema);
|
|
176
174
|
|
|
177
175
|
function TestComponent() {
|
|
178
|
-
const schema =
|
|
176
|
+
const schema = useType(db, () => typeUri);
|
|
179
177
|
schemaAccessor = schema;
|
|
180
178
|
const t = createMemo(() => {
|
|
181
179
|
const s = schema();
|
|
@@ -187,20 +185,20 @@ describe('useSchema', () => {
|
|
|
187
185
|
const { getByTestId } = render(() => <TestComponent />);
|
|
188
186
|
|
|
189
187
|
await waitFor(() => {
|
|
190
|
-
expect(getByTestId('typename').textContent).toBe(registeredSchema
|
|
188
|
+
expect(getByTestId('typename').textContent).toBe(Type.getTypename(registeredSchema)!);
|
|
191
189
|
});
|
|
192
190
|
|
|
193
191
|
// Get the actual result from the accessor
|
|
194
192
|
let result = schemaAccessor?.();
|
|
195
193
|
expect(result).toBeDefined();
|
|
196
|
-
expect(result
|
|
194
|
+
expect(Type.getTypename(result!)).toBe(Type.getTypename(registeredSchema)!);
|
|
197
195
|
|
|
198
|
-
// Change
|
|
199
|
-
|
|
196
|
+
// Change typeUri
|
|
197
|
+
typeUri = undefined;
|
|
200
198
|
|
|
201
199
|
await waitFor(() => {
|
|
202
|
-
// Should keep previous value when
|
|
203
|
-
expect(getByTestId('typename').textContent).toBe(registeredSchema
|
|
200
|
+
// Should keep previous value when typeUri becomes undefined
|
|
201
|
+
expect(getByTestId('typename').textContent).toBe(Type.getTypename(registeredSchema)!);
|
|
204
202
|
});
|
|
205
203
|
result = schemaAccessor?.();
|
|
206
204
|
expect(result).toBeDefined();
|
package/src/useType.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Accessor, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
6
|
+
|
|
7
|
+
import { DXN, EID, URI, type Database, Filter, Query, Scope, Type } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
type MaybeAccessor<T> = T | Accessor<T>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to and retrieve a type by its URI from a space: a static schema's typename DXN, or a
|
|
13
|
+
* persisted (database) schema's `echo:` EID (what `Type.getURI` / `getTypeURIFromQuery` produce).
|
|
14
|
+
* Accepts either values or accessors for db and typeUri.
|
|
15
|
+
*
|
|
16
|
+
* Fans across the owning space db (persisted custom types) and the shared
|
|
17
|
+
* registry (static/runtime plugin types). Persisted types live only in the db,
|
|
18
|
+
* so a registry-only lookup misses them.
|
|
19
|
+
*
|
|
20
|
+
* @param db - The database instance (can be reactive)
|
|
21
|
+
* @param typeUri - The type URI to query (can be reactive)
|
|
22
|
+
* @returns An accessor that returns the current type or undefined
|
|
23
|
+
*/
|
|
24
|
+
export const useType = (
|
|
25
|
+
db?: MaybeAccessor<Database.Database | undefined>,
|
|
26
|
+
typeUri?: MaybeAccessor<URI.URI | undefined>,
|
|
27
|
+
): Accessor<Type.AnyEntity | undefined> => {
|
|
28
|
+
// Derive resolved values reactively.
|
|
29
|
+
const resolved = createMemo(() => {
|
|
30
|
+
const resolvedDb = typeof db === 'function' ? db() : db;
|
|
31
|
+
const resolvedTypeUri = typeof typeUri === 'function' ? typeUri() : typeUri;
|
|
32
|
+
if (!resolvedTypeUri || !resolvedDb) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return { db: resolvedDb, typeUri: resolvedTypeUri };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Store the current type in a signal.
|
|
39
|
+
const [type, setType] = createSignal<Type.AnyEntity | undefined>(undefined);
|
|
40
|
+
|
|
41
|
+
// Subscribe to registry changes.
|
|
42
|
+
createEffect(() => {
|
|
43
|
+
const r = resolved();
|
|
44
|
+
if (!r) {
|
|
45
|
+
setType(() => undefined);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { db: resolvedDb, typeUri: resolvedTypeUri } = r;
|
|
50
|
+
|
|
51
|
+
const searchEid = EID.isEID(resolvedTypeUri) ? EID.tryParse(resolvedTypeUri) : undefined;
|
|
52
|
+
const searchDxn = DXN.isDXN(resolvedTypeUri) ? DXN.tryMake(resolvedTypeUri) : undefined;
|
|
53
|
+
const queryResult = resolvedDb.query(Query.select(Filter.type(Type.Type)).from(Scope.space(), Scope.registry()));
|
|
54
|
+
const update = () =>
|
|
55
|
+
setType(() =>
|
|
56
|
+
queryResult.results.find((type) => {
|
|
57
|
+
const uri = Type.getURI(type);
|
|
58
|
+
if (uri === resolvedTypeUri) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// EID matching is space-agnostic: echo:/<id> matches echo://<space>/<id>.
|
|
62
|
+
if (searchEid && EID.isEID(uri)) {
|
|
63
|
+
const typeEid = EID.tryParse(uri);
|
|
64
|
+
return typeEid != null && EID.getEntityId(typeEid) === EID.getEntityId(searchEid);
|
|
65
|
+
}
|
|
66
|
+
// DXN matching is version-agnostic so callers may pass an unversioned DXN.
|
|
67
|
+
if (searchDxn && DXN.isDXN(uri)) {
|
|
68
|
+
const typeDxn = DXN.tryMake(uri);
|
|
69
|
+
return typeDxn != null && DXN.getName(typeDxn) === DXN.getName(searchDxn);
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Subscribe before reading `.results` — the query requires at least one subscriber.
|
|
76
|
+
const unsubscribe = queryResult.subscribe(update);
|
|
77
|
+
update();
|
|
78
|
+
|
|
79
|
+
onCleanup(unsubscribe);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return type;
|
|
83
|
+
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type Accessor } from 'solid-js';
|
|
2
|
-
import { type Database, type Type } from '@dxos/echo';
|
|
3
|
-
type MaybeAccessor<T> = T | Accessor<T>;
|
|
4
|
-
/**
|
|
5
|
-
* Subscribe to and retrieve schema changes from a database's schema registry.
|
|
6
|
-
* Accepts either values or accessors for db and typename.
|
|
7
|
-
*
|
|
8
|
-
* @param db - The database instance (can be reactive)
|
|
9
|
-
* @param typename - The schema typename to query (can be reactive)
|
|
10
|
-
* @returns An accessor that returns the current schema or undefined
|
|
11
|
-
*/
|
|
12
|
-
export declare const useSchema: <T extends Type.AnyEntity = Type.AnyEntity>(db?: MaybeAccessor<Database.Database | undefined>, typename?: MaybeAccessor<string | undefined>) => Accessor<T | undefined>;
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=useSchema.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useSchema.d.ts","sourceRoot":"","sources":["../../../src/useSchema.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,QAAQ,EAAqD,MAAM,UAAU,CAAC;AAE5F,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAC;AAEtD,KAAK,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AAExC;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,EACjE,KAAK,aAAa,CAAC,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,EACjD,WAAW,aAAa,CAAC,MAAM,GAAG,SAAS,CAAC,KAC3C,QAAQ,CAAC,CAAC,GAAG,SAAS,CAqCxB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useSchema.test.d.ts","sourceRoot":"","sources":["../../../src/useSchema.test.tsx"],"names":[],"mappings":""}
|
package/src/useSchema.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
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.AnyEntity = Type.AnyEntity>(
|
|
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
|
-
};
|