@dxos/echo-react 0.8.4-main.fcfe5033a5 → 0.8.4-staging.60fe92afc8
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 +74 -33
- 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/useType.d.ts +14 -0
- package/dist/types/src/useType.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -9
- package/src/index.ts +1 -1
- package/src/useObject.test.tsx +7 -19
- package/src/useObject.ts +38 -11
- package/src/useQuery.__test.tsx +1 -2
- package/src/useType.ts +74 -0
- package/dist/types/src/useSchema.d.ts +0 -6
- package/dist/types/src/useSchema.d.ts.map +0 -1
- package/src/useSchema.ts +0 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-react",
|
|
3
|
-
"version": "0.8.4-
|
|
3
|
+
"version": "0.8.4-staging.60fe92afc8",
|
|
4
4
|
"description": "React 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,18 +20,14 @@
|
|
|
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
|
"@effect-atom/atom-react": "^0.5.0",
|
|
33
|
-
"@dxos/echo": "0.8.4-
|
|
34
|
-
"@dxos/echo-atom": "0.8.4-main.fcfe5033a5"
|
|
30
|
+
"@dxos/echo": "0.8.4-staging.60fe92afc8"
|
|
35
31
|
},
|
|
36
32
|
"devDependencies": {
|
|
37
33
|
"@testing-library/react": "^16.3.0",
|
|
@@ -39,7 +35,7 @@
|
|
|
39
35
|
"@types/react-dom": "~19.2.3",
|
|
40
36
|
"react": "~19.2.3",
|
|
41
37
|
"react-dom": "~19.2.3",
|
|
42
|
-
"@dxos/echo-
|
|
38
|
+
"@dxos/echo-client": "0.8.4-staging.60fe92afc8"
|
|
43
39
|
},
|
|
44
40
|
"peerDependencies": {
|
|
45
41
|
"react": "~19.2.3"
|
package/src/index.ts
CHANGED
package/src/useObject.test.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import React, { type PropsWithChildren } from 'react';
|
|
|
9
9
|
import { describe, expect, test } from 'vitest';
|
|
10
10
|
|
|
11
11
|
import { Obj, Ref } from '@dxos/echo';
|
|
12
|
-
import { createObject } from '@dxos/echo-
|
|
12
|
+
import { createObject } from '@dxos/echo-client';
|
|
13
13
|
import { TestSchema } from '@dxos/echo/testing';
|
|
14
14
|
|
|
15
15
|
import { useObject } from './useObject';
|
|
@@ -46,7 +46,6 @@ describe('useObject', () => {
|
|
|
46
46
|
const wrapper = createWrapper(registry);
|
|
47
47
|
|
|
48
48
|
const { result } = renderHook(() => useObject(obj, 'name'), { wrapper });
|
|
49
|
-
|
|
50
49
|
const [value] = result.current;
|
|
51
50
|
expect(value).toBe('Test');
|
|
52
51
|
});
|
|
@@ -59,11 +58,10 @@ describe('useObject', () => {
|
|
|
59
58
|
const wrapper = createWrapper(registry);
|
|
60
59
|
|
|
61
60
|
const { result } = renderHook(() => useObject(obj, 'name'), { wrapper });
|
|
62
|
-
|
|
63
61
|
expect(result.current[0]).toBe('Test');
|
|
64
62
|
|
|
65
|
-
// Update the property via Obj.
|
|
66
|
-
Obj.
|
|
63
|
+
// Update the property via Obj.update
|
|
64
|
+
Obj.update(obj, (obj) => {
|
|
67
65
|
obj.name = 'Updated';
|
|
68
66
|
});
|
|
69
67
|
|
|
@@ -81,11 +79,10 @@ describe('useObject', () => {
|
|
|
81
79
|
const wrapper = createWrapper(registry);
|
|
82
80
|
|
|
83
81
|
const { result } = renderHook(() => useObject(obj), { wrapper });
|
|
84
|
-
|
|
85
82
|
expect(result.current[0].name).toBe('Test');
|
|
86
83
|
|
|
87
|
-
// Update a property via Obj.
|
|
88
|
-
Obj.
|
|
84
|
+
// Update a property via Obj.update
|
|
85
|
+
Obj.update(obj, (obj) => {
|
|
89
86
|
obj.name = 'Updated';
|
|
90
87
|
});
|
|
91
88
|
|
|
@@ -103,11 +100,10 @@ describe('useObject', () => {
|
|
|
103
100
|
const wrapper = createWrapper(registry);
|
|
104
101
|
|
|
105
102
|
const { result } = renderHook(() => useObject(obj, 'name'), { wrapper });
|
|
106
|
-
|
|
107
103
|
expect(result.current[0]).toBe('Test');
|
|
108
104
|
|
|
109
|
-
// Update a different property via Obj.
|
|
110
|
-
Obj.
|
|
105
|
+
// Update a different property via Obj.update
|
|
106
|
+
Obj.update(obj, (obj) => {
|
|
111
107
|
obj.email = 'newemail@example.com';
|
|
112
108
|
});
|
|
113
109
|
|
|
@@ -142,7 +138,6 @@ describe('useObject', () => {
|
|
|
142
138
|
const wrapper = createWrapper(registry);
|
|
143
139
|
|
|
144
140
|
const { result } = renderHook(() => useObject(obj), { wrapper });
|
|
145
|
-
|
|
146
141
|
const [value, updateCallback] = result.current;
|
|
147
142
|
expect(value.name).toBe('Test');
|
|
148
143
|
|
|
@@ -165,7 +160,6 @@ describe('useObject', () => {
|
|
|
165
160
|
const wrapper = createWrapper(registry);
|
|
166
161
|
|
|
167
162
|
const { result } = renderHook(() => useObject(obj, 'name'), { wrapper });
|
|
168
|
-
|
|
169
163
|
const [value, updateCallback] = result.current;
|
|
170
164
|
expect(value).toBe('Test');
|
|
171
165
|
|
|
@@ -186,7 +180,6 @@ describe('useObject', () => {
|
|
|
186
180
|
const wrapper = createWrapper(registry);
|
|
187
181
|
|
|
188
182
|
const { result } = renderHook(() => useObject(obj, 'name'), { wrapper });
|
|
189
|
-
|
|
190
183
|
const [value, updateCallback] = result.current;
|
|
191
184
|
expect(value).toBe('Test');
|
|
192
185
|
|
|
@@ -207,7 +200,6 @@ describe('useObject', () => {
|
|
|
207
200
|
const wrapper = createWrapper(registry);
|
|
208
201
|
|
|
209
202
|
const { result, rerender } = renderHook(() => useObject(obj, 'name'), { wrapper });
|
|
210
|
-
|
|
211
203
|
const [, firstUpdateCallback] = result.current;
|
|
212
204
|
|
|
213
205
|
rerender();
|
|
@@ -233,7 +225,6 @@ describe('useObject', () => {
|
|
|
233
225
|
const wrapper = createWrapper(registry);
|
|
234
226
|
|
|
235
227
|
const { result } = renderHook(() => useObject(undefined as TestSchema.Person | undefined, 'name'), { wrapper });
|
|
236
|
-
|
|
237
228
|
const [value] = result.current;
|
|
238
229
|
expect(value).toBeUndefined();
|
|
239
230
|
});
|
|
@@ -243,7 +234,6 @@ describe('useObject', () => {
|
|
|
243
234
|
const wrapper = createWrapper(registry);
|
|
244
235
|
|
|
245
236
|
const { result } = renderHook(() => useObject(undefined as TestSchema.Person | undefined), { wrapper });
|
|
246
|
-
|
|
247
237
|
const [, updateCallback] = result.current;
|
|
248
238
|
|
|
249
239
|
// Should not throw when calling update on undefined object.
|
|
@@ -262,7 +252,6 @@ describe('useObject', () => {
|
|
|
262
252
|
const wrapper = createWrapper(registry);
|
|
263
253
|
|
|
264
254
|
const { result, rerender } = renderHook(() => useObject(obj), { wrapper });
|
|
265
|
-
|
|
266
255
|
const [firstSnapshot] = result.current;
|
|
267
256
|
expect(firstSnapshot).toBeDefined();
|
|
268
257
|
|
|
@@ -286,7 +275,6 @@ describe('useObject', () => {
|
|
|
286
275
|
const wrapper = createWrapper(registry);
|
|
287
276
|
|
|
288
277
|
const { result, rerender } = renderHook(() => useObject(person.employer!), { wrapper });
|
|
289
|
-
|
|
290
278
|
const [firstSnapshot] = result.current;
|
|
291
279
|
expect(firstSnapshot).toBeDefined();
|
|
292
280
|
|
package/src/useObject.ts
CHANGED
|
@@ -7,7 +7,6 @@ import * as Atom from '@effect-atom/atom/Atom';
|
|
|
7
7
|
import { useCallback, useMemo } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Obj, Ref } from '@dxos/echo';
|
|
10
|
-
import { AtomObj } from '@dxos/echo-atom';
|
|
11
10
|
|
|
12
11
|
export interface ObjectUpdateCallback<T> {
|
|
13
12
|
(update: (obj: Obj.Mutable<T>) => void): void;
|
|
@@ -123,7 +122,7 @@ export const useObject: {
|
|
|
123
122
|
if (obj === undefined) {
|
|
124
123
|
return;
|
|
125
124
|
}
|
|
126
|
-
Obj.
|
|
125
|
+
Obj.update(obj, (obj: any) => {
|
|
127
126
|
if (typeof updateOrValue === 'function') {
|
|
128
127
|
const returnValue = updateOrValue(property !== undefined ? obj[property] : obj);
|
|
129
128
|
if (returnValue !== undefined) {
|
|
@@ -144,32 +143,60 @@ export const useObject: {
|
|
|
144
143
|
);
|
|
145
144
|
|
|
146
145
|
if (property !== undefined) {
|
|
147
|
-
// For
|
|
148
|
-
//
|
|
149
|
-
|
|
146
|
+
// For refs, subscribe to load event only (not full mutation tracking).
|
|
147
|
+
// Property-level updates are handled by useObjectProperty once the ref loads.
|
|
148
|
+
useRefLoad(objOrRef);
|
|
150
149
|
return [useObjectProperty(liveObj, property as any), callback];
|
|
150
|
+
} else {
|
|
151
|
+
return [useObjectValue(objOrRef), callback];
|
|
151
152
|
}
|
|
152
|
-
return [useObjectValue(objOrRef), callback];
|
|
153
153
|
}) as any;
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
156
|
* Internal hook for subscribing to an Echo object or Ref.
|
|
157
|
-
* AtomObj.make handles both objects and refs, returning snapshots.
|
|
158
157
|
*/
|
|
159
158
|
const useObjectValue = <T extends Obj.Unknown>(objOrRef: T | Ref.Ref<T> | undefined): Obj.Snapshot<T> | undefined => {
|
|
160
|
-
const atom = useMemo(() =>
|
|
159
|
+
const atom = useMemo(() => {
|
|
160
|
+
if (objOrRef == null) {
|
|
161
|
+
return Atom.make<Obj.Snapshot<T> | undefined>(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
if (Ref.isRef(objOrRef)) {
|
|
164
|
+
return Obj.atom(objOrRef);
|
|
165
|
+
}
|
|
166
|
+
return Obj.atom(objOrRef);
|
|
167
|
+
}, [objOrRef]);
|
|
161
168
|
return useAtomValue(atom) as Obj.Snapshot<T> | undefined;
|
|
162
169
|
};
|
|
163
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Internal hook for subscribing to ref resolution only (load-once).
|
|
173
|
+
* Triggers a re-render when the ref target first becomes available,
|
|
174
|
+
* without subscribing to subsequent target mutations.
|
|
175
|
+
* For non-refs, this is a no-op.
|
|
176
|
+
*/
|
|
177
|
+
const useRefLoad = <T extends Obj.Unknown>(objOrRef: T | Ref.Ref<T> | undefined): void => {
|
|
178
|
+
const atom = useMemo(() => {
|
|
179
|
+
if (objOrRef == null || !Ref.isRef(objOrRef)) {
|
|
180
|
+
return Atom.make<T | undefined>(() => undefined);
|
|
181
|
+
}
|
|
182
|
+
return objOrRef.atom;
|
|
183
|
+
}, [objOrRef]);
|
|
184
|
+
useAtomValue(atom);
|
|
185
|
+
};
|
|
186
|
+
|
|
164
187
|
/**
|
|
165
188
|
* Internal hook for subscribing to a specific property of an Echo object.
|
|
166
|
-
* Uses useAtomValue directly since makeProperty returns the value directly.
|
|
167
189
|
*/
|
|
168
190
|
const useObjectProperty = <T extends Obj.Unknown, K extends keyof T>(
|
|
169
191
|
obj: T | undefined,
|
|
170
192
|
property: K,
|
|
171
193
|
): T[K] | undefined => {
|
|
172
|
-
const atom = useMemo(() =>
|
|
194
|
+
const atom = useMemo(() => {
|
|
195
|
+
if (obj == null) {
|
|
196
|
+
return Atom.make<T[K] | undefined>(() => undefined);
|
|
197
|
+
}
|
|
198
|
+
return Obj.atomProperty(obj, property);
|
|
199
|
+
}, [obj, property]);
|
|
173
200
|
return useAtomValue(atom);
|
|
174
201
|
};
|
|
175
202
|
|
|
@@ -190,7 +217,7 @@ export const useObjects = <T extends Obj.Unknown>(refs: readonly Ref.Ref<T>[]):
|
|
|
190
217
|
Atom.make((get) => {
|
|
191
218
|
const results: Obj.Snapshot<T>[] = [];
|
|
192
219
|
for (const ref of refs) {
|
|
193
|
-
const value = get(
|
|
220
|
+
const value = get(Obj.atom(ref));
|
|
194
221
|
if (value !== undefined) {
|
|
195
222
|
results.push(value as Obj.Snapshot<T>);
|
|
196
223
|
}
|
package/src/useQuery.__test.tsx
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
import { renderHook } from '@testing-library/react';
|
|
9
9
|
import { describe, expect, test } from 'vitest';
|
|
10
10
|
|
|
11
|
-
import { Filter } from '@dxos/
|
|
12
|
-
import { Obj, Type } from '@dxos/echo';
|
|
11
|
+
import { Filter, Obj, Type } from '@dxos/echo';
|
|
13
12
|
import { TestSchema } from '@dxos/echo/testing';
|
|
14
13
|
|
|
15
14
|
import { createClient, createClientContextProvider } from '../testing/util';
|
package/src/useType.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
6
|
+
|
|
7
|
+
import { DXN, EID, URI, type Database, Filter, Query, Scope, Type } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to and retrieve a type by its URI from a space: a static schema's typename DXN, or a
|
|
11
|
+
* persisted (database) schema's `echo:` EID (what `Type.getURI` / `getTypeURIFromQuery` produce).
|
|
12
|
+
*
|
|
13
|
+
* Fans across the owning space db (persisted custom types) and the shared
|
|
14
|
+
* registry (static/runtime plugin types). Persisted types live only in the db,
|
|
15
|
+
* so a registry-only lookup misses them.
|
|
16
|
+
*
|
|
17
|
+
* DXN matching is version-agnostic: `dxn:com.example/Foo` matches `dxn:com.example/Foo:0.1.0`.
|
|
18
|
+
* This lets callers pass a bare typename DXN (no version) from e.g. a `ReferenceAnnotation`.
|
|
19
|
+
*/
|
|
20
|
+
export const useType = <T extends Type.AnyEntity = Type.AnyEntity>(
|
|
21
|
+
db?: Database.Database,
|
|
22
|
+
typeUri?: URI.URI,
|
|
23
|
+
): T | undefined => {
|
|
24
|
+
const { subscribe, getType } = useMemo(() => {
|
|
25
|
+
if (!typeUri || !db) {
|
|
26
|
+
return {
|
|
27
|
+
subscribe: () => () => {},
|
|
28
|
+
getType: (): T | undefined => undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const searchEid = EID.isEID(typeUri) ? EID.tryParse(typeUri) : undefined;
|
|
33
|
+
const searchDxn = DXN.isDXN(typeUri) ? DXN.tryMake(typeUri) : undefined;
|
|
34
|
+
|
|
35
|
+
const queryResult = db.query(Query.select(Filter.type(Type.Type)).from(Scope.space(), Scope.registry()));
|
|
36
|
+
let subscribed = false;
|
|
37
|
+
const find = (): T | undefined => {
|
|
38
|
+
if (!subscribed) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return queryResult.results.find((type) => {
|
|
42
|
+
const uri = Type.getURI(type);
|
|
43
|
+
if (uri === typeUri) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// EID matching is space-agnostic: echo:/<id> matches echo://<space>/<id>.
|
|
47
|
+
if (searchEid && EID.isEID(uri)) {
|
|
48
|
+
const typeEid = EID.tryParse(uri);
|
|
49
|
+
return typeEid != null && EID.getEntityId(typeEid) === EID.getEntityId(searchEid);
|
|
50
|
+
}
|
|
51
|
+
// DXN matching is version-agnostic so callers may pass an unversioned DXN.
|
|
52
|
+
if (searchDxn && DXN.isDXN(uri)) {
|
|
53
|
+
const typeDxn = DXN.tryMake(uri);
|
|
54
|
+
return typeDxn != null && DXN.getName(typeDxn) === DXN.getName(searchDxn);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}) as T | undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
subscribe: (onStoreChange: () => void) => {
|
|
62
|
+
subscribed = true;
|
|
63
|
+
const unsubscribe = queryResult.subscribe(onStoreChange);
|
|
64
|
+
return () => {
|
|
65
|
+
unsubscribe();
|
|
66
|
+
subscribed = false;
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
getType: find,
|
|
70
|
+
};
|
|
71
|
+
}, [typeUri, db]);
|
|
72
|
+
|
|
73
|
+
return useSyncExternalStore(subscribe, getType);
|
|
74
|
+
};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type Database, type Type } from '@dxos/echo';
|
|
2
|
-
/**
|
|
3
|
-
* Subscribe to and retrieve schema changes from a space's schema registry.
|
|
4
|
-
*/
|
|
5
|
-
export declare const useSchema: <T extends Type.AnyEntity = Type.AnyEntity>(db?: Database.Database, typename?: string) => T | undefined;
|
|
6
|
-
//# sourceMappingURL=useSchema.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useSchema.d.ts","sourceRoot":"","sources":["../../../src/useSchema.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAC;AAEtD;;GAEG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,EACjE,KAAK,QAAQ,CAAC,QAAQ,EACtB,WAAW,MAAM,KAChB,CAAC,GAAG,SA2BN,CAAC"}
|
package/src/useSchema.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { useMemo, useSyncExternalStore } from 'react';
|
|
6
|
-
|
|
7
|
-
import { type Database, type Type } from '@dxos/echo';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Subscribe to and retrieve schema changes from a space's schema registry.
|
|
11
|
-
*/
|
|
12
|
-
export const useSchema = <T extends Type.AnyEntity = Type.AnyEntity>(
|
|
13
|
-
db?: Database.Database,
|
|
14
|
-
typename?: string,
|
|
15
|
-
): T | undefined => {
|
|
16
|
-
const { subscribe, getSchema } = useMemo(() => {
|
|
17
|
-
if (!typename || !db) {
|
|
18
|
-
return {
|
|
19
|
-
subscribe: () => () => {},
|
|
20
|
-
getSchema: () => undefined,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const query = db.schemaRegistry.query({ typename, location: ['database', 'runtime'] });
|
|
25
|
-
const initialResult = query.runSync()[0];
|
|
26
|
-
let currentSchema = initialResult;
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
subscribe: (onStoreChange: () => void) => {
|
|
30
|
-
const unsubscribe = query.subscribe(() => {
|
|
31
|
-
currentSchema = query.results[0];
|
|
32
|
-
onStoreChange();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
return unsubscribe;
|
|
36
|
-
},
|
|
37
|
-
getSchema: () => currentSchema,
|
|
38
|
-
};
|
|
39
|
-
}, [typename, db]);
|
|
40
|
-
|
|
41
|
-
return useSyncExternalStore(subscribe, getSchema) as T;
|
|
42
|
-
};
|