@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-react",
3
- "version": "0.8.4-main.fcfe5033a5",
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": "MIT",
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.1",
28
+ "@effect-atom/atom": "^0.5.3",
32
29
  "@effect-atom/atom-react": "^0.5.0",
33
- "@dxos/echo": "0.8.4-main.fcfe5033a5",
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-db": "0.8.4-main.fcfe5033a5"
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
@@ -3,5 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './useQuery';
6
- export * from './useSchema';
6
+ export * from './useType';
7
7
  export * from './useObject';
@@ -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-db';
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.change
66
- Obj.change(obj, (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.change
88
- Obj.change(obj, (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.change
110
- Obj.change(obj, (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.change(obj, (obj: any) => {
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 property subscriptions on refs, we subscribe to trigger re-render on load.
148
- // TODO(dxos): Property subscriptions on refs may not update correctly until the ref loads.
149
- useObjectValue(objOrRef);
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(() => AtomObj.make(objOrRef), [objOrRef]);
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(() => AtomObj.makeProperty(obj, property), [obj, property]);
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(AtomObj.make(ref));
220
+ const value = get(Obj.atom(ref));
194
221
  if (value !== undefined) {
195
222
  results.push(value as Obj.Snapshot<T>);
196
223
  }
@@ -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/client/echo';
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
- };