@dxos/echo-solid 0.0.0 → 0.8.4-main.69d29f4
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 +209 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node-esm/index.mjs +211 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/useObject.d.ts +101 -0
- package/dist/types/src/useObject.d.ts.map +1 -0
- package/dist/types/src/useObject.test.d.ts +2 -0
- package/dist/types/src/useObject.test.d.ts.map +1 -0
- package/dist/types/src/useQuery.d.ts +14 -0
- package/dist/types/src/useQuery.d.ts.map +1 -0
- package/dist/types/src/useQuery.test.d.ts +2 -0
- package/dist/types/src/useQuery.test.d.ts.map +1 -0
- package/dist/types/src/useSchema.d.ts +14 -0
- package/dist/types/src/useSchema.d.ts.map +1 -0
- package/dist/types/src/useSchema.test.d.ts +2 -0
- package/dist/types/src/useSchema.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +10 -5
- package/src/index.ts +0 -1
- package/src/useObject.test.tsx +2 -2
- package/src/useObject.ts +218 -73
- package/src/useQuery.test.tsx +17 -17
- package/src/useRef.test.tsx +0 -247
- package/src/useRef.ts +0 -48
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.4-main.69d29f4",
|
|
4
4
|
"description": "Solid.js integration for ECHO.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"sideEffects": false,
|
|
@@ -25,17 +29,18 @@
|
|
|
25
29
|
"src"
|
|
26
30
|
],
|
|
27
31
|
"dependencies": {
|
|
32
|
+
"@effect-atom/atom": "^0.4.13",
|
|
28
33
|
"@solid-primitives/utils": "^6.3.2",
|
|
29
|
-
"@dxos/
|
|
30
|
-
"@dxos/echo-atom": "0.
|
|
31
|
-
"@dxos/
|
|
34
|
+
"@dxos/echo": "0.8.4-main.69d29f4",
|
|
35
|
+
"@dxos/echo-atom": "0.8.4-main.69d29f4",
|
|
36
|
+
"@dxos/effect-atom-solid": "0.8.4-main.69d29f4"
|
|
32
37
|
},
|
|
33
38
|
"devDependencies": {
|
|
34
39
|
"@solidjs/testing-library": "^0.8.10",
|
|
35
40
|
"solid-js": "^1.9.9",
|
|
36
41
|
"vite-plugin-solid": "^2.11.10",
|
|
37
42
|
"vitest": "3.2.4",
|
|
38
|
-
"@dxos/echo-db": "0.8.
|
|
43
|
+
"@dxos/echo-db": "0.8.4-main.69d29f4"
|
|
39
44
|
},
|
|
40
45
|
"peerDependencies": {
|
|
41
46
|
"solid-js": "^1.9.9"
|
package/src/index.ts
CHANGED
package/src/useObject.test.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { createObject } from '@dxos/echo-db';
|
|
|
13
13
|
import { Registry } from '@dxos/effect-atom-solid';
|
|
14
14
|
import { RegistryProvider } from '@dxos/effect-atom-solid';
|
|
15
15
|
|
|
16
|
-
import { useObject } from './useObject';
|
|
16
|
+
import { type ObjectUpdateCallback, useObject } from './useObject';
|
|
17
17
|
|
|
18
18
|
const createWrapper = (registry: Registry.Registry) => {
|
|
19
19
|
return (props: { children: JSX.Element }) => (
|
|
@@ -334,7 +334,7 @@ describe('useObject', () => {
|
|
|
334
334
|
const registry = Registry.make();
|
|
335
335
|
const Wrapper = createWrapper(registry);
|
|
336
336
|
|
|
337
|
-
let updatePerson:
|
|
337
|
+
let updatePerson: ObjectUpdateCallback<TestSchema.Person> | undefined;
|
|
338
338
|
const { getByTestId } = render(
|
|
339
339
|
() => {
|
|
340
340
|
const [value, update] = useObject(obj);
|
package/src/useObject.ts
CHANGED
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
import { type MaybeAccessor, access } from '@solid-primitives/utils';
|
|
6
6
|
import { type Accessor, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
9
9
|
import { AtomObj } from '@dxos/echo-atom';
|
|
10
10
|
import { type Registry, useRegistry } from '@dxos/effect-atom-solid';
|
|
11
11
|
|
|
12
12
|
export interface ObjectUpdateCallback<T> {
|
|
13
|
-
(update: (obj: T) => void): void;
|
|
14
|
-
(update: (obj: T) => T): void;
|
|
13
|
+
(update: (obj: Obj.Mutable<T>) => void): void;
|
|
14
|
+
(update: (obj: Obj.Mutable<T>) => Obj.Mutable<T>): void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export interface ObjectPropUpdateCallback<T>
|
|
17
|
+
export interface ObjectPropUpdateCallback<T> {
|
|
18
|
+
(update: (value: Obj.Mutable<T>) => void): void;
|
|
19
|
+
(update: (value: Obj.Mutable<T>) => Obj.Mutable<T>): void;
|
|
18
20
|
(newValue: T): void;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -29,17 +31,25 @@ type ConditionalUndefined<T, R> = [T] extends [Exclude<T, undefined>]
|
|
|
29
31
|
: R; // Both T and R include undefined, return R as-is (no double undefined)
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* Subscribe to a
|
|
33
|
-
*
|
|
34
|
+
* Subscribe to a Ref's target object.
|
|
35
|
+
* Automatically dereferences the ref and handles async loading.
|
|
36
|
+
* Returns undefined if the ref hasn't loaded yet.
|
|
34
37
|
*
|
|
35
|
-
* @param
|
|
36
|
-
* @param property - Property key to subscribe to
|
|
38
|
+
* @param ref - The Ref to dereference and subscribe to (can be reactive)
|
|
37
39
|
* @returns A tuple of [accessor, updateCallback]
|
|
38
40
|
*/
|
|
39
|
-
export function useObject<T
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
export function useObject<T>(ref: MaybeAccessor<Ref.Ref<T>>): [Accessor<T | undefined>, ObjectUpdateCallback<T>];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Subscribe to a Ref's target object that may be undefined.
|
|
45
|
+
* Returns undefined if the ref is undefined or hasn't loaded yet.
|
|
46
|
+
*
|
|
47
|
+
* @param ref - The Ref to dereference and subscribe to (can be undefined/reactive)
|
|
48
|
+
* @returns A tuple of [accessor, updateCallback]
|
|
49
|
+
*/
|
|
50
|
+
export function useObject<T>(
|
|
51
|
+
ref: MaybeAccessor<Ref.Ref<T> | undefined>,
|
|
52
|
+
): [Accessor<T | undefined>, ObjectUpdateCallback<T>];
|
|
43
53
|
|
|
44
54
|
/**
|
|
45
55
|
* Subscribe to an entire Echo object.
|
|
@@ -48,52 +58,109 @@ export function useObject<T extends Entity.Unknown | undefined, K extends keyof
|
|
|
48
58
|
* @param obj - The Echo object to subscribe to (can be reactive)
|
|
49
59
|
* @returns A tuple of [accessor, updateCallback]
|
|
50
60
|
*/
|
|
51
|
-
export function useObject<T extends
|
|
61
|
+
export function useObject<T extends Obj.Unknown>(
|
|
52
62
|
obj: MaybeAccessor<T>,
|
|
53
63
|
): [Accessor<ConditionalUndefined<T, T>>, ObjectUpdateCallback<T>];
|
|
54
|
-
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Subscribe to an entire Echo object that may be undefined.
|
|
67
|
+
* Returns undefined if the object is undefined.
|
|
68
|
+
*
|
|
69
|
+
* @param obj - The Echo object to subscribe to (can be undefined/reactive)
|
|
70
|
+
* @returns A tuple of [accessor, updateCallback]
|
|
71
|
+
*/
|
|
72
|
+
export function useObject<T extends Obj.Unknown>(
|
|
73
|
+
obj: MaybeAccessor<T | undefined>,
|
|
74
|
+
): [Accessor<ConditionalUndefined<T, T>>, ObjectUpdateCallback<T>];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Subscribe to a specific property of an Echo object.
|
|
78
|
+
* Returns the current property value accessor and an update callback.
|
|
79
|
+
*
|
|
80
|
+
* @param obj - The Echo object to subscribe to (can be reactive)
|
|
81
|
+
* @param property - Property key to subscribe to
|
|
82
|
+
* @returns A tuple of [accessor, updateCallback]
|
|
83
|
+
*/
|
|
84
|
+
export function useObject<T extends Obj.Unknown, K extends keyof T>(
|
|
55
85
|
obj: MaybeAccessor<T>,
|
|
56
|
-
|
|
86
|
+
property: K,
|
|
87
|
+
): [Accessor<T[K]>, ObjectPropUpdateCallback<T[K]>];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Subscribe to a specific property of an Echo object that may be undefined.
|
|
91
|
+
* Returns undefined if the object is undefined.
|
|
92
|
+
*
|
|
93
|
+
* @param obj - The Echo object to subscribe to (can be undefined/reactive)
|
|
94
|
+
* @param property - Property key to subscribe to
|
|
95
|
+
* @returns A tuple of [accessor, updateCallback]
|
|
96
|
+
*/
|
|
97
|
+
export function useObject<T extends Obj.Unknown, K extends keyof T>(
|
|
98
|
+
obj: MaybeAccessor<T | undefined>,
|
|
99
|
+
property: K,
|
|
100
|
+
): [Accessor<T[K] | undefined>, ObjectPropUpdateCallback<T[K]>];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Subscribe to a specific property of a Ref's target object.
|
|
104
|
+
* Automatically dereferences the ref and handles async loading.
|
|
105
|
+
* Returns undefined if the ref hasn't loaded yet.
|
|
106
|
+
*
|
|
107
|
+
* @param ref - The Ref to dereference and subscribe to (can be reactive)
|
|
108
|
+
* @param property - Property key to subscribe to
|
|
109
|
+
* @returns A tuple of [accessor, updateCallback]
|
|
110
|
+
*/
|
|
111
|
+
export function useObject<T, K extends keyof T>(
|
|
112
|
+
ref: MaybeAccessor<Ref.Ref<T>>,
|
|
113
|
+
property: K,
|
|
114
|
+
): [Accessor<T[K] | undefined>, ObjectPropUpdateCallback<T[K]>];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Subscribe to a specific property of a Ref's target object that may be undefined.
|
|
118
|
+
* Returns undefined if the ref is undefined or hasn't loaded yet.
|
|
119
|
+
*
|
|
120
|
+
* @param ref - The Ref to dereference and subscribe to (can be undefined/reactive)
|
|
121
|
+
* @param property - Property key to subscribe to
|
|
122
|
+
* @returns A tuple of [accessor, updateCallback]
|
|
123
|
+
*/
|
|
124
|
+
export function useObject<T, K extends keyof T>(
|
|
125
|
+
ref: MaybeAccessor<Ref.Ref<T> | undefined>,
|
|
126
|
+
property: K,
|
|
127
|
+
): [Accessor<T[K] | undefined>, ObjectPropUpdateCallback<T[K]>];
|
|
57
128
|
|
|
58
129
|
/**
|
|
59
|
-
* Subscribe to an Echo object (entire object or specific property).
|
|
130
|
+
* Subscribe to an Echo object or Ref (entire object or specific property).
|
|
60
131
|
* Returns the current value accessor and an update callback.
|
|
61
132
|
*
|
|
62
|
-
* @param
|
|
133
|
+
* @param objOrRef - The Echo object or Ref to subscribe to (can be reactive)
|
|
63
134
|
* @param property - Optional property key to subscribe to a specific property
|
|
64
135
|
* @returns A tuple of [accessor, updateCallback]
|
|
65
136
|
*/
|
|
66
|
-
export function useObject<T extends
|
|
67
|
-
|
|
68
|
-
property?: K,
|
|
69
|
-
):
|
|
70
|
-
| [Accessor<ConditionalUndefined<T, Exclude<T, undefined>>>, ObjectUpdateCallback<Exclude<T, undefined>>]
|
|
71
|
-
| [Accessor<ConditionalUndefined<T, Exclude<T, undefined>[K]>>, ObjectPropUpdateCallback<Exclude<T, undefined>[K]>];
|
|
72
|
-
export function useObject<T extends Entity.Unknown, K extends keyof T>(
|
|
73
|
-
obj: MaybeAccessor<T>,
|
|
74
|
-
property?: K,
|
|
75
|
-
):
|
|
76
|
-
| [Accessor<ConditionalUndefined<T, T>>, ObjectUpdateCallback<T>]
|
|
77
|
-
| [Accessor<ConditionalUndefined<T, T[K]>>, ObjectPropUpdateCallback<T[K]>];
|
|
78
|
-
export function useObject<T extends Entity.Unknown | undefined, K extends keyof Exclude<T, undefined>>(
|
|
79
|
-
obj: MaybeAccessor<T>,
|
|
137
|
+
export function useObject<T extends Obj.Unknown, K extends keyof T>(
|
|
138
|
+
objOrRef: MaybeAccessor<T | Ref.Ref<T> | undefined>,
|
|
80
139
|
property?: K,
|
|
81
|
-
):
|
|
82
|
-
| [Accessor<ConditionalUndefined<T, Exclude<T, undefined>>>, ObjectUpdateCallback<Exclude<T, undefined>>]
|
|
83
|
-
| [Accessor<ConditionalUndefined<T, Exclude<T, undefined>[K]>>, ObjectPropUpdateCallback<Exclude<T, undefined>[K]>] {
|
|
140
|
+
): [Accessor<any>, ObjectUpdateCallback<T> | ObjectPropUpdateCallback<T[K]>] {
|
|
84
141
|
const registry = useRegistry();
|
|
85
142
|
|
|
86
|
-
// Memoize the resolved
|
|
87
|
-
const
|
|
143
|
+
// Memoize the resolved input to track changes.
|
|
144
|
+
const resolvedInput = createMemo(() => access(objOrRef));
|
|
145
|
+
|
|
146
|
+
// Determine if input is a ref.
|
|
147
|
+
const isRef = createMemo(() => Ref.isRef(resolvedInput()));
|
|
148
|
+
|
|
149
|
+
// Get the live object for the callback (refs need to dereference).
|
|
150
|
+
const liveObj = createMemo(() => {
|
|
151
|
+
const input = resolvedInput();
|
|
152
|
+
return isRef() ? (input as Ref.Ref<T>)?.target : (input as T | undefined);
|
|
153
|
+
});
|
|
88
154
|
|
|
89
155
|
// Create a stable callback that handles both object and property updates.
|
|
90
156
|
const callback = (updateOrValue: unknown | ((obj: unknown) => unknown)) => {
|
|
91
|
-
|
|
92
|
-
|
|
157
|
+
// Get current target for refs (may have loaded since render).
|
|
158
|
+
const obj = isRef() ? (resolvedInput() as Ref.Ref<T>)?.target : liveObj();
|
|
159
|
+
if (!obj) {
|
|
93
160
|
return;
|
|
94
161
|
}
|
|
95
162
|
|
|
96
|
-
Obj.change(
|
|
163
|
+
Obj.change(obj, (o: any) => {
|
|
97
164
|
if (typeof updateOrValue === 'function') {
|
|
98
165
|
const returnValue = (updateOrValue as (obj: unknown) => unknown)(property !== undefined ? o[property] : o);
|
|
99
166
|
if (returnValue !== undefined) {
|
|
@@ -112,44 +179,46 @@ export function useObject<T extends Entity.Unknown | undefined, K extends keyof
|
|
|
112
179
|
};
|
|
113
180
|
|
|
114
181
|
if (property !== undefined) {
|
|
115
|
-
|
|
182
|
+
// For property subscriptions on refs, we subscribe to trigger re-render on load.
|
|
183
|
+
useObjectValue(registry, objOrRef);
|
|
184
|
+
return [useObjectProperty(registry, liveObj, property), callback as ObjectPropUpdateCallback<T[K]>];
|
|
116
185
|
}
|
|
117
|
-
return [useObjectValue(registry,
|
|
186
|
+
return [useObjectValue(registry, objOrRef), callback as ObjectUpdateCallback<T>];
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
/**
|
|
121
|
-
* Internal function for subscribing to an
|
|
122
|
-
*
|
|
190
|
+
* Internal function for subscribing to an Echo object or Ref.
|
|
191
|
+
* AtomObj.make handles both objects and refs, returning snapshots.
|
|
123
192
|
*/
|
|
124
|
-
function useObjectValue<T extends
|
|
193
|
+
function useObjectValue<T extends Obj.Unknown>(
|
|
125
194
|
registry: Registry.Registry,
|
|
126
|
-
|
|
127
|
-
): Accessor<
|
|
128
|
-
// Memoize the resolved
|
|
129
|
-
const
|
|
195
|
+
objOrRef: MaybeAccessor<T | Ref.Ref<T> | undefined>,
|
|
196
|
+
): Accessor<T | undefined> {
|
|
197
|
+
// Memoize the resolved input to track changes.
|
|
198
|
+
const resolvedInput = createMemo(() => access(objOrRef));
|
|
130
199
|
|
|
131
|
-
// Initialize with
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const [value, setValue] = createSignal<T | undefined>(
|
|
200
|
+
// Initialize with the current value (if available).
|
|
201
|
+
const initialInput = resolvedInput();
|
|
202
|
+
const initialValue = initialInput ? registry.get(AtomObj.make(initialInput)) : undefined;
|
|
203
|
+
const [value, setValue] = createSignal<T | undefined>(initialValue as T | undefined);
|
|
135
204
|
|
|
136
205
|
// Subscribe to atom updates.
|
|
137
206
|
createEffect(() => {
|
|
138
|
-
const
|
|
207
|
+
const input = resolvedInput();
|
|
139
208
|
|
|
140
|
-
if (!
|
|
209
|
+
if (!input) {
|
|
141
210
|
setValue(() => undefined);
|
|
142
211
|
return;
|
|
143
212
|
}
|
|
144
213
|
|
|
145
|
-
const atom = AtomObj.make(
|
|
214
|
+
const atom = AtomObj.make(input);
|
|
146
215
|
const currentValue = registry.get(atom);
|
|
147
|
-
setValue(() => currentValue as T);
|
|
216
|
+
setValue(() => currentValue as unknown as T);
|
|
148
217
|
|
|
149
218
|
const unsubscribe = registry.subscribe(
|
|
150
219
|
atom,
|
|
151
220
|
() => {
|
|
152
|
-
setValue(() => registry.get(atom) as T);
|
|
221
|
+
setValue(() => registry.get(atom) as unknown as T);
|
|
153
222
|
},
|
|
154
223
|
{ immediate: true },
|
|
155
224
|
);
|
|
@@ -157,43 +226,39 @@ function useObjectValue<T extends Entity.Unknown | undefined>(
|
|
|
157
226
|
onCleanup(unsubscribe);
|
|
158
227
|
});
|
|
159
228
|
|
|
160
|
-
return value
|
|
229
|
+
return value;
|
|
161
230
|
}
|
|
162
231
|
|
|
163
232
|
/**
|
|
164
233
|
* Internal function for subscribing to a specific property of an Echo object.
|
|
165
234
|
*/
|
|
166
|
-
function useObjectProperty<T extends
|
|
235
|
+
function useObjectProperty<T extends Obj.Unknown, K extends keyof T>(
|
|
167
236
|
registry: Registry.Registry,
|
|
168
|
-
obj:
|
|
237
|
+
obj: Accessor<T | undefined>,
|
|
169
238
|
property: K,
|
|
170
|
-
): Accessor<
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const initialValue = resolvedObj() ? (resolvedObj() as NonUndefinedT)[property] : undefined;
|
|
176
|
-
const [value, setValue] = createSignal<NonUndefinedT[K] | undefined>(initialValue);
|
|
239
|
+
): Accessor<T[K] | undefined> {
|
|
240
|
+
// Initialize with the current value (if available).
|
|
241
|
+
const initialObj = obj();
|
|
242
|
+
const initialValue = initialObj ? registry.get(AtomObj.makeProperty(initialObj, property)) : undefined;
|
|
243
|
+
const [value, setValue] = createSignal<T[K] | undefined>(initialValue);
|
|
177
244
|
|
|
178
245
|
// Subscribe to atom updates.
|
|
179
246
|
createEffect(() => {
|
|
180
|
-
const currentObj =
|
|
247
|
+
const currentObj = obj();
|
|
181
248
|
|
|
182
249
|
if (!currentObj) {
|
|
183
250
|
setValue(() => undefined);
|
|
184
251
|
return;
|
|
185
252
|
}
|
|
186
253
|
|
|
187
|
-
|
|
188
|
-
const echoObj = currentObj as NonUndefinedT;
|
|
189
|
-
const atom = AtomObj.makeProperty(echoObj, property);
|
|
254
|
+
const atom = AtomObj.makeProperty(currentObj, property);
|
|
190
255
|
const currentValue = registry.get(atom);
|
|
191
256
|
setValue(() => currentValue);
|
|
192
257
|
|
|
193
258
|
const unsubscribe = registry.subscribe(
|
|
194
259
|
atom,
|
|
195
260
|
() => {
|
|
196
|
-
setValue(() => registry.get(atom) as
|
|
261
|
+
setValue(() => registry.get(atom) as T[K]);
|
|
197
262
|
},
|
|
198
263
|
{ immediate: true },
|
|
199
264
|
);
|
|
@@ -201,5 +266,85 @@ function useObjectProperty<T extends Entity.Unknown | undefined, K extends keyof
|
|
|
201
266
|
onCleanup(unsubscribe);
|
|
202
267
|
});
|
|
203
268
|
|
|
204
|
-
return value
|
|
269
|
+
return value;
|
|
205
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Subscribe to multiple Refs' target objects.
|
|
274
|
+
* Automatically dereferences each ref and handles async loading.
|
|
275
|
+
* Returns an accessor to an array of loaded snapshots (filtering out undefined values).
|
|
276
|
+
*
|
|
277
|
+
* This hook is useful for aggregate computations like counts or filtering
|
|
278
|
+
* across multiple refs without using .target directly.
|
|
279
|
+
*
|
|
280
|
+
* @param refs - Array of Refs to dereference and subscribe to (can be reactive)
|
|
281
|
+
* @returns Accessor to array of loaded target snapshots (excludes unloaded refs)
|
|
282
|
+
*/
|
|
283
|
+
export const useObjects = <T extends Obj.Unknown>(refs: MaybeAccessor<readonly Ref.Ref<T>[]>): Accessor<T[]> => {
|
|
284
|
+
// Track version to trigger re-renders when any ref or target changes.
|
|
285
|
+
const [version, setVersion] = createSignal(0);
|
|
286
|
+
|
|
287
|
+
// Memoize the refs array to track changes.
|
|
288
|
+
const resolvedRefs = createMemo(() => access(refs));
|
|
289
|
+
|
|
290
|
+
// Subscribe to all refs and their targets.
|
|
291
|
+
createEffect(() => {
|
|
292
|
+
const currentRefs = resolvedRefs();
|
|
293
|
+
const targetUnsubscribes = new Map<string, () => void>();
|
|
294
|
+
|
|
295
|
+
// Function to trigger re-render.
|
|
296
|
+
const triggerUpdate = () => {
|
|
297
|
+
setVersion((v) => v + 1);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Function to set up subscription for a target.
|
|
301
|
+
const subscribeToTarget = (ref: Ref.Ref<T>) => {
|
|
302
|
+
const target = ref.target;
|
|
303
|
+
if (target) {
|
|
304
|
+
const key = ref.dxn.toString();
|
|
305
|
+
if (!targetUnsubscribes.has(key)) {
|
|
306
|
+
targetUnsubscribes.set(key, Obj.subscribe(target, triggerUpdate));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Try to load all refs and subscribe to targets.
|
|
312
|
+
for (const ref of currentRefs) {
|
|
313
|
+
// Subscribe to existing target if available.
|
|
314
|
+
subscribeToTarget(ref);
|
|
315
|
+
|
|
316
|
+
// Trigger async load if not already loaded.
|
|
317
|
+
if (!ref.target) {
|
|
318
|
+
void ref
|
|
319
|
+
.load()
|
|
320
|
+
.then(() => {
|
|
321
|
+
subscribeToTarget(ref);
|
|
322
|
+
triggerUpdate();
|
|
323
|
+
})
|
|
324
|
+
.catch(() => {
|
|
325
|
+
// Ignore load errors.
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
onCleanup(() => {
|
|
331
|
+
targetUnsubscribes.forEach((u) => u());
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Compute current snapshots by reading each ref's target.
|
|
336
|
+
return createMemo(() => {
|
|
337
|
+
// Depend on version to re-compute when targets change.
|
|
338
|
+
version();
|
|
339
|
+
|
|
340
|
+
const currentRefs = resolvedRefs();
|
|
341
|
+
const snapshots: T[] = [];
|
|
342
|
+
for (const ref of currentRefs) {
|
|
343
|
+
const target = ref.target;
|
|
344
|
+
if (target !== undefined) {
|
|
345
|
+
snapshots.push(target as T);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return snapshots;
|
|
349
|
+
});
|
|
350
|
+
};
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { render, waitFor } from '@solidjs/testing-library';
|
|
|
6
6
|
import { type JSX, createSignal } from 'solid-js';
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import { Filter, Obj, Query
|
|
9
|
+
import { Filter, Obj, Query } from '@dxos/echo';
|
|
10
10
|
import { TestSchema } from '@dxos/echo/testing';
|
|
11
11
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
12
12
|
|
|
@@ -30,7 +30,7 @@ describe('useQuery', () => {
|
|
|
30
30
|
let result: any[] | undefined;
|
|
31
31
|
|
|
32
32
|
render(() => {
|
|
33
|
-
const objects = useQuery(undefined, Filter.type(
|
|
33
|
+
const objects = useQuery(undefined, Filter.type(TestSchema.Expando));
|
|
34
34
|
result = objects();
|
|
35
35
|
return (<div>test</div>) as JSX.Element;
|
|
36
36
|
});
|
|
@@ -42,7 +42,7 @@ describe('useQuery', () => {
|
|
|
42
42
|
let result: any[] | undefined;
|
|
43
43
|
|
|
44
44
|
render(() => {
|
|
45
|
-
const objects = useQuery(db, Filter.type(
|
|
45
|
+
const objects = useQuery(db, Filter.type(TestSchema.Expando));
|
|
46
46
|
result = objects();
|
|
47
47
|
return (<div>test</div>) as JSX.Element;
|
|
48
48
|
});
|
|
@@ -54,8 +54,8 @@ describe('useQuery', () => {
|
|
|
54
54
|
|
|
55
55
|
test('returns matching objects', async () => {
|
|
56
56
|
// Add some objects to the database
|
|
57
|
-
const obj1 = Obj.make(
|
|
58
|
-
const obj2 = Obj.make(
|
|
57
|
+
const obj1 = Obj.make(TestSchema.Expando, { name: 'Alice' });
|
|
58
|
+
const obj2 = Obj.make(TestSchema.Expando, { name: 'Bob' });
|
|
59
59
|
db.add(obj1);
|
|
60
60
|
db.add(obj2);
|
|
61
61
|
await db.flush({ indexes: true });
|
|
@@ -63,7 +63,7 @@ describe('useQuery', () => {
|
|
|
63
63
|
let objectsAccessor: (() => any[]) | undefined;
|
|
64
64
|
|
|
65
65
|
function TestComponent() {
|
|
66
|
-
const objects = useQuery(db, Filter.type(
|
|
66
|
+
const objects = useQuery(db, Filter.type(TestSchema.Expando));
|
|
67
67
|
objectsAccessor = objects;
|
|
68
68
|
return (<div data-testid='count'>{objects().length}</div>) as JSX.Element;
|
|
69
69
|
}
|
|
@@ -84,7 +84,7 @@ describe('useQuery', () => {
|
|
|
84
84
|
let objectsAccessor: (() => any[]) | undefined;
|
|
85
85
|
|
|
86
86
|
function TestComponent() {
|
|
87
|
-
const objects = useQuery(db, Filter.type(
|
|
87
|
+
const objects = useQuery(db, Filter.type(TestSchema.Expando));
|
|
88
88
|
objectsAccessor = objects;
|
|
89
89
|
return (<div data-testid='count'>{objects().length}</div>) as JSX.Element;
|
|
90
90
|
}
|
|
@@ -96,7 +96,7 @@ describe('useQuery', () => {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
// Add an object
|
|
99
|
-
const obj = Obj.make(
|
|
99
|
+
const obj = Obj.make(TestSchema.Expando, { name: 'Charlie' });
|
|
100
100
|
db.add(obj);
|
|
101
101
|
await db.flush({ indexes: true });
|
|
102
102
|
|
|
@@ -112,8 +112,8 @@ describe('useQuery', () => {
|
|
|
112
112
|
|
|
113
113
|
test('updates when objects are removed', async () => {
|
|
114
114
|
// Add objects first
|
|
115
|
-
const obj1 = Obj.make(
|
|
116
|
-
const obj2 = Obj.make(
|
|
115
|
+
const obj1 = Obj.make(TestSchema.Expando, { name: 'Alice' });
|
|
116
|
+
const obj2 = Obj.make(TestSchema.Expando, { name: 'Bob' });
|
|
117
117
|
db.add(obj1);
|
|
118
118
|
db.add(obj2);
|
|
119
119
|
await db.flush({ indexes: true });
|
|
@@ -121,7 +121,7 @@ describe('useQuery', () => {
|
|
|
121
121
|
let objectsAccessor: (() => any[]) | undefined;
|
|
122
122
|
|
|
123
123
|
function TestComponent() {
|
|
124
|
-
const objects = useQuery(db, Filter.type(
|
|
124
|
+
const objects = useQuery(db, Filter.type(TestSchema.Expando));
|
|
125
125
|
objectsAccessor = objects;
|
|
126
126
|
return (<div data-testid='count'>{objects().length}</div>) as JSX.Element;
|
|
127
127
|
}
|
|
@@ -175,14 +175,14 @@ describe('useQuery', () => {
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
test('accepts Query directly', async () => {
|
|
178
|
-
const obj = Obj.make(
|
|
178
|
+
const obj = Obj.make(TestSchema.Expando, { name: 'Test' });
|
|
179
179
|
db.add(obj);
|
|
180
180
|
await db.flush({ indexes: true });
|
|
181
181
|
|
|
182
182
|
let objectsAccessor: (() => any[]) | undefined;
|
|
183
183
|
|
|
184
184
|
function TestComponent() {
|
|
185
|
-
const objects = useQuery(db, Query.select(Filter.type(
|
|
185
|
+
const objects = useQuery(db, Query.select(Filter.type(TestSchema.Expando)));
|
|
186
186
|
objectsAccessor = objects;
|
|
187
187
|
return (<div data-testid='count'>{objects().length}</div>) as JSX.Element;
|
|
188
188
|
}
|
|
@@ -200,7 +200,7 @@ describe('useQuery', () => {
|
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
test('accepts reactive database accessor', async () => {
|
|
203
|
-
const obj = Obj.make(
|
|
203
|
+
const obj = Obj.make(TestSchema.Expando, { name: 'Test' });
|
|
204
204
|
db.add(obj);
|
|
205
205
|
await db.flush({ indexes: true });
|
|
206
206
|
|
|
@@ -208,7 +208,7 @@ describe('useQuery', () => {
|
|
|
208
208
|
let dbAccessor: any = db;
|
|
209
209
|
|
|
210
210
|
function TestComponent() {
|
|
211
|
-
const objects = useQuery(() => dbAccessor, Filter.type(
|
|
211
|
+
const objects = useQuery(() => dbAccessor, Filter.type(TestSchema.Expando));
|
|
212
212
|
objectsAccessor = objects;
|
|
213
213
|
return (<div data-testid='count'>{objects().length}</div>) as JSX.Element;
|
|
214
214
|
}
|
|
@@ -235,7 +235,7 @@ describe('useQuery', () => {
|
|
|
235
235
|
// Register schema first
|
|
236
236
|
await db.graph.schemaRegistry.register([TestSchema.Person]);
|
|
237
237
|
|
|
238
|
-
const obj1 = Obj.make(
|
|
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' });
|
|
240
240
|
db.add(obj1);
|
|
241
241
|
db.add(obj2);
|
|
@@ -246,7 +246,7 @@ describe('useQuery', () => {
|
|
|
246
246
|
|
|
247
247
|
function TestComponent() {
|
|
248
248
|
const objects = useQuery(db, () =>
|
|
249
|
-
usePersonFilter() ? Filter.type(TestSchema.Person) : Filter.type(
|
|
249
|
+
usePersonFilter() ? Filter.type(TestSchema.Person) : Filter.type(TestSchema.Expando),
|
|
250
250
|
);
|
|
251
251
|
objectsAccessor = objects;
|
|
252
252
|
return (<div data-testid='count'>{objects().length}</div>) as JSX.Element;
|