@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.
package/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ MIT License
2
+ Copyright (c) 2025 DXOS
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @dxos/echo-solid
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@dxos/echo-solid",
3
+ "version": "0.0.0",
4
+ "description": "Solid.js integration for ECHO.",
5
+ "homepage": "https://dxos.org",
6
+ "bugs": "https://github.com/dxos/dxos/issues",
7
+ "license": "MIT",
8
+ "author": "DXOS.org",
9
+ "sideEffects": false,
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "source": "./src/index.ts",
14
+ "types": "./dist/types/src/index.d.ts",
15
+ "browser": "./dist/lib/browser/index.mjs",
16
+ "node": "./dist/lib/node-esm/index.mjs"
17
+ }
18
+ },
19
+ "types": "dist/types/src/index.d.ts",
20
+ "typesVersions": {
21
+ "*": {}
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "dependencies": {
28
+ "@solid-primitives/utils": "^6.3.2",
29
+ "@dxos/effect-atom-solid": "0.0.0",
30
+ "@dxos/echo-atom": "0.0.0",
31
+ "@dxos/echo": "0.8.3"
32
+ },
33
+ "devDependencies": {
34
+ "@solidjs/testing-library": "^0.8.10",
35
+ "solid-js": "^1.9.9",
36
+ "vite-plugin-solid": "^2.11.10",
37
+ "vitest": "3.2.4",
38
+ "@dxos/echo-db": "0.8.3"
39
+ },
40
+ "peerDependencies": {
41
+ "solid-js": "^1.9.9"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useObject';
6
+ export * from './useQuery';
7
+ export * from './useRef';
8
+ export * from './useSchema';
@@ -0,0 +1,362 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { render, waitFor } from '@solidjs/testing-library';
6
+ import { type JSX, createSignal } from 'solid-js';
7
+ import { describe, expect, test } from 'vitest';
8
+
9
+ import type { Entity } from '@dxos/echo';
10
+ import { Obj } from '@dxos/echo';
11
+ import { TestSchema } from '@dxos/echo/testing';
12
+ import { createObject } from '@dxos/echo-db';
13
+ import { Registry } from '@dxos/effect-atom-solid';
14
+ import { RegistryProvider } from '@dxos/effect-atom-solid';
15
+
16
+ import { useObject } from './useObject';
17
+
18
+ const createWrapper = (registry: Registry.Registry) => {
19
+ return (props: { children: JSX.Element }) => (
20
+ <RegistryProvider registry={registry}>{props.children}</RegistryProvider>
21
+ );
22
+ };
23
+
24
+ describe('useObject', () => {
25
+ test('returns entire object when property is not provided', () => {
26
+ const obj = createObject(
27
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
28
+ );
29
+ const registry = Registry.make();
30
+ const Wrapper = createWrapper(registry);
31
+
32
+ let result: Entity.Entity<TestSchema.Person> | undefined;
33
+ render(
34
+ () => {
35
+ const [value] = useObject(obj);
36
+ result = value();
37
+ return (<div>test</div>) as JSX.Element;
38
+ },
39
+ { wrapper: Wrapper },
40
+ );
41
+
42
+ // Returns a snapshot (plain object), not the Echo object itself.
43
+ expect(result).not.toBe(obj);
44
+ expect(result?.name).toBe('Test');
45
+ expect(result?.username).toBe('test');
46
+ expect(result?.email).toBe('test@example.com');
47
+ });
48
+
49
+ test('returns property value when property is provided', () => {
50
+ const obj = createObject(
51
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
52
+ );
53
+ const registry = Registry.make();
54
+ const Wrapper = createWrapper(registry);
55
+
56
+ let result: string | undefined;
57
+ render(
58
+ () => {
59
+ const [value] = useObject(obj, 'name');
60
+ result = value();
61
+ return (<div>test</div>) as JSX.Element;
62
+ },
63
+ { wrapper: Wrapper },
64
+ );
65
+
66
+ expect(result).toBe('Test');
67
+ });
68
+
69
+ test('updates when object property changes', async () => {
70
+ const obj = createObject(
71
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
72
+ );
73
+ const registry = Registry.make();
74
+ const Wrapper = createWrapper(registry);
75
+
76
+ let result: string | undefined;
77
+ const { getByTestId } = render(
78
+ () => {
79
+ const [value] = useObject(obj, 'name');
80
+ result = value();
81
+ return (<div data-testid='value'>{value()}</div>) as JSX.Element;
82
+ },
83
+ { wrapper: Wrapper },
84
+ );
85
+
86
+ expect(result).toBe('Test');
87
+ expect(getByTestId('value').textContent).toBe('Test');
88
+
89
+ // Update the property via Obj.change.
90
+ Obj.change(obj, (o) => {
91
+ o.name = 'Updated';
92
+ });
93
+
94
+ // Wait for reactivity to update.
95
+ await waitFor(() => {
96
+ expect(getByTestId('value').textContent).toBe('Updated');
97
+ });
98
+ });
99
+
100
+ test('updates when entire object changes', async () => {
101
+ const obj = createObject(
102
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
103
+ );
104
+ const registry = Registry.make();
105
+ const Wrapper = createWrapper(registry);
106
+
107
+ // Capture the accessor to get the latest snapshot value.
108
+ let valueAccessor: (() => Entity.Entity<TestSchema.Person> | undefined) | undefined;
109
+ const { getByTestId } = render(
110
+ () => {
111
+ const [value] = useObject(obj);
112
+ valueAccessor = value;
113
+ return (<div data-testid='name'>{value()?.name}</div>) as JSX.Element;
114
+ },
115
+ { wrapper: Wrapper },
116
+ );
117
+
118
+ expect(valueAccessor?.()?.name).toBe('Test');
119
+ expect(getByTestId('name').textContent).toBe('Test');
120
+
121
+ // Update a property via Obj.change.
122
+ Obj.change(obj, (o) => {
123
+ o.name = 'Updated';
124
+ });
125
+
126
+ // Wait for reactivity to update.
127
+ await waitFor(() => {
128
+ expect(getByTestId('name').textContent).toBe('Updated');
129
+ });
130
+ expect(valueAccessor?.()?.name).toBe('Updated');
131
+ });
132
+
133
+ test('property atom does not update when other properties change', async () => {
134
+ const obj = createObject(
135
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
136
+ );
137
+ const registry = Registry.make();
138
+ const Wrapper = createWrapper(registry);
139
+
140
+ let result: string | undefined;
141
+ render(
142
+ () => {
143
+ const [value] = useObject(obj, 'name');
144
+ result = value();
145
+ return (<div>test</div>) as JSX.Element;
146
+ },
147
+ { wrapper: Wrapper },
148
+ );
149
+
150
+ expect(result).toBe('Test');
151
+
152
+ // Update a different property via Obj.change.
153
+ Obj.change(obj, (o) => {
154
+ o.email = 'newemail@example.com';
155
+ });
156
+
157
+ // Name should still be 'Test'.
158
+ expect(result).toBe('Test');
159
+ });
160
+
161
+ test('works with accessor function for entire object', () => {
162
+ const obj = createObject(
163
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
164
+ );
165
+ const registry = Registry.make();
166
+ const Wrapper = createWrapper(registry);
167
+
168
+ let result: Entity.Entity<TestSchema.Person> | undefined;
169
+ render(
170
+ () => {
171
+ const [value] = useObject(() => obj);
172
+ result = value();
173
+ return (<div>test</div>) as JSX.Element;
174
+ },
175
+ { wrapper: Wrapper },
176
+ );
177
+
178
+ // Returns a snapshot (plain object), not the Echo object itself.
179
+ expect(result).not.toBe(obj);
180
+ expect(result?.name).toBe('Test');
181
+ });
182
+
183
+ test('works with accessor function for property', () => {
184
+ const obj = createObject(
185
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
186
+ );
187
+ const registry = Registry.make();
188
+ const Wrapper = createWrapper(registry);
189
+
190
+ let result: string | undefined;
191
+
192
+ render(
193
+ () => {
194
+ const [value] = useObject(() => obj, 'name');
195
+ result = value();
196
+ return (<div>test</div>) as JSX.Element;
197
+ },
198
+ { wrapper: Wrapper },
199
+ );
200
+
201
+ expect(result).toBe('Test');
202
+ });
203
+
204
+ test('reactively tracks changes when accessor returns different object', async () => {
205
+ const obj1 = createObject(
206
+ Obj.make(TestSchema.Person, { name: 'Test1', username: 'test1', email: 'test1@example.com' }),
207
+ );
208
+ const obj2 = createObject(
209
+ Obj.make(TestSchema.Person, { name: 'Test2', username: 'test2', email: 'test2@example.com' }),
210
+ );
211
+ const registry = Registry.make();
212
+ const Wrapper = createWrapper(registry);
213
+
214
+ const [objSignal, setObjSignal] = createSignal(obj1);
215
+ let valueAccessor: (() => string | undefined) | undefined;
216
+
217
+ const { getByTestId } = render(
218
+ () => {
219
+ const [value] = useObject(objSignal, 'name');
220
+ valueAccessor = value;
221
+ return (<div data-testid='value'>{value()}</div>) as JSX.Element;
222
+ },
223
+ { wrapper: Wrapper },
224
+ );
225
+
226
+ expect(valueAccessor?.()).toBe('Test1');
227
+ expect(getByTestId('value').textContent).toBe('Test1');
228
+
229
+ // Change the object via signal.
230
+ setObjSignal(() => obj2);
231
+
232
+ // Wait for reactivity to update.
233
+ await waitFor(() => {
234
+ expect(getByTestId('value').textContent).toBe('Test2');
235
+ });
236
+ expect(valueAccessor?.()).toBe('Test2');
237
+ });
238
+
239
+ test('reactively tracks entire object when accessor returns different object', async () => {
240
+ const obj1 = createObject(
241
+ Obj.make(TestSchema.Person, { name: 'Test1', username: 'test1', email: 'test1@example.com' }),
242
+ );
243
+ const obj2 = createObject(
244
+ Obj.make(TestSchema.Person, { name: 'Test2', username: 'test2', email: 'test2@example.com' }),
245
+ );
246
+ const registry = Registry.make();
247
+ const Wrapper = createWrapper(registry);
248
+
249
+ const [objSignal, setObjSignal] = createSignal(obj1);
250
+ let valueAccessor: (() => Entity.Entity<TestSchema.Person> | undefined) | undefined;
251
+ render(
252
+ () => {
253
+ const [value] = useObject(objSignal);
254
+ valueAccessor = value;
255
+ return (<div>test</div>) as JSX.Element;
256
+ },
257
+ { wrapper: Wrapper },
258
+ );
259
+
260
+ expect(valueAccessor?.()?.name).toBe('Test1');
261
+
262
+ // Change the object via signal.
263
+ setObjSignal(() => obj2);
264
+
265
+ // Wait for reactivity to update.
266
+ await waitFor(() => {
267
+ expect(valueAccessor?.()?.name).toBe('Test2');
268
+ });
269
+ expect(valueAccessor?.()?.username).toBe('test2');
270
+ });
271
+
272
+ test('update callback can update property value directly', async () => {
273
+ const obj = createObject(
274
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
275
+ );
276
+ const registry = Registry.make();
277
+ const Wrapper = createWrapper(registry);
278
+
279
+ let updateName: ((value: string | undefined) => void) | undefined;
280
+ const { getByTestId } = render(
281
+ () => {
282
+ const [value, update] = useObject(obj, 'name');
283
+ updateName = update;
284
+ return (<div data-testid='value'>{value()}</div>) as JSX.Element;
285
+ },
286
+ { wrapper: Wrapper },
287
+ );
288
+
289
+ expect(getByTestId('value').textContent).toBe('Test');
290
+
291
+ // Update using the callback.
292
+ updateName?.('Updated');
293
+
294
+ // Wait for reactivity to update.
295
+ await waitFor(() => {
296
+ expect(getByTestId('value').textContent).toBe('Updated');
297
+ });
298
+ expect(obj.name).toBe('Updated');
299
+ });
300
+
301
+ test('update callback can update property with updater function', async () => {
302
+ const obj = createObject(
303
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
304
+ );
305
+ const registry = Registry.make();
306
+ const Wrapper = createWrapper(registry);
307
+
308
+ let updateName: ((updater: (current: string | undefined) => string | undefined) => void) | undefined;
309
+ const { getByTestId } = render(
310
+ () => {
311
+ const [value, update] = useObject(obj, 'name');
312
+ updateName = update;
313
+ return (<div data-testid='value'>{value()}</div>) as JSX.Element;
314
+ },
315
+ { wrapper: Wrapper },
316
+ );
317
+
318
+ expect(getByTestId('value').textContent).toBe('Test');
319
+
320
+ // Update using an updater function.
321
+ updateName?.((current) => (current ?? '') + ' Updated');
322
+
323
+ // Wait for reactivity to update.
324
+ await waitFor(() => {
325
+ expect(getByTestId('value').textContent).toBe('Test Updated');
326
+ });
327
+ expect(obj.name).toBe('Test Updated');
328
+ });
329
+
330
+ test('update callback can mutate entire object', async () => {
331
+ const obj = createObject(
332
+ Obj.make(TestSchema.Person, { name: 'Test', username: 'test', email: 'test@example.com' }),
333
+ );
334
+ const registry = Registry.make();
335
+ const Wrapper = createWrapper(registry);
336
+
337
+ let updatePerson: ((updater: (p: Entity.Entity<TestSchema.Person>) => void) => void) | undefined;
338
+ const { getByTestId } = render(
339
+ () => {
340
+ const [value, update] = useObject(obj);
341
+ updatePerson = update;
342
+ return (<div data-testid='value'>{value()?.name}</div>) as JSX.Element;
343
+ },
344
+ { wrapper: Wrapper },
345
+ );
346
+
347
+ expect(getByTestId('value').textContent).toBe('Test');
348
+
349
+ // Update using the callback to mutate the object.
350
+ updatePerson?.((p) => {
351
+ p.name = 'Updated';
352
+ p.email = 'updated@example.com';
353
+ });
354
+
355
+ // Wait for reactivity to update.
356
+ await waitFor(() => {
357
+ expect(getByTestId('value').textContent).toBe('Updated');
358
+ });
359
+ expect(obj.name).toBe('Updated');
360
+ expect(obj.email).toBe('updated@example.com');
361
+ });
362
+ });
@@ -0,0 +1,205 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type MaybeAccessor, access } from '@solid-primitives/utils';
6
+ import { type Accessor, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
7
+
8
+ import { type Entity, Obj } from '@dxos/echo';
9
+ import { AtomObj } from '@dxos/echo-atom';
10
+ import { type Registry, useRegistry } from '@dxos/effect-atom-solid';
11
+
12
+ export interface ObjectUpdateCallback<T> {
13
+ (update: (obj: T) => void): void;
14
+ (update: (obj: T) => T): void;
15
+ }
16
+
17
+ export interface ObjectPropUpdateCallback<T> extends ObjectUpdateCallback<T> {
18
+ (newValue: T): void;
19
+ }
20
+
21
+ /**
22
+ * Helper type to conditionally include undefined in return type only if input includes undefined.
23
+ * Only adds undefined if T includes undefined AND R doesn't already include undefined.
24
+ */
25
+ type ConditionalUndefined<T, R> = [T] extends [Exclude<T, undefined>]
26
+ ? R // T doesn't include undefined, return R as-is
27
+ : [R] extends [Exclude<R, undefined>]
28
+ ? R | undefined // T includes undefined but R doesn't, add undefined
29
+ : R; // Both T and R include undefined, return R as-is (no double undefined)
30
+
31
+ /**
32
+ * Subscribe to a specific property of an Echo object.
33
+ * Returns the current property value accessor and an update callback.
34
+ *
35
+ * @param obj - The Echo object to subscribe to (can be reactive)
36
+ * @param property - Property key to subscribe to
37
+ * @returns A tuple of [accessor, updateCallback]
38
+ */
39
+ export function useObject<T extends Entity.Unknown | undefined, K extends keyof Exclude<T, undefined>>(
40
+ obj: MaybeAccessor<T>,
41
+ property: K,
42
+ ): [Accessor<ConditionalUndefined<T, Exclude<T, undefined>[K]>>, ObjectPropUpdateCallback<Exclude<T, undefined>[K]>];
43
+
44
+ /**
45
+ * Subscribe to an entire Echo object.
46
+ * Returns the current object value accessor and an update callback.
47
+ *
48
+ * @param obj - The Echo object to subscribe to (can be reactive)
49
+ * @returns A tuple of [accessor, updateCallback]
50
+ */
51
+ export function useObject<T extends Entity.Unknown>(
52
+ obj: MaybeAccessor<T>,
53
+ ): [Accessor<ConditionalUndefined<T, T>>, ObjectUpdateCallback<T>];
54
+ export function useObject<T extends Entity.Unknown | undefined>(
55
+ obj: MaybeAccessor<T>,
56
+ ): [Accessor<ConditionalUndefined<T, Exclude<T, undefined>>>, ObjectUpdateCallback<Exclude<T, undefined>>];
57
+
58
+ /**
59
+ * Subscribe to an Echo object (entire object or specific property).
60
+ * Returns the current value accessor and an update callback.
61
+ *
62
+ * @param obj - The Echo object to subscribe to (can be reactive)
63
+ * @param property - Optional property key to subscribe to a specific property
64
+ * @returns A tuple of [accessor, updateCallback]
65
+ */
66
+ export function useObject<T extends Entity.Unknown | undefined, K extends keyof Exclude<T, undefined>>(
67
+ obj: MaybeAccessor<T>,
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>,
80
+ 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]>] {
84
+ const registry = useRegistry();
85
+
86
+ // Memoize the resolved object to track changes.
87
+ const resolvedObj = createMemo(() => access(obj));
88
+
89
+ // Create a stable callback that handles both object and property updates.
90
+ const callback = (updateOrValue: unknown | ((obj: unknown) => unknown)) => {
91
+ const currentObj = resolvedObj();
92
+ if (!currentObj) {
93
+ return;
94
+ }
95
+
96
+ Obj.change(currentObj, (o: any) => {
97
+ if (typeof updateOrValue === 'function') {
98
+ const returnValue = (updateOrValue as (obj: unknown) => unknown)(property !== undefined ? o[property] : o);
99
+ if (returnValue !== undefined) {
100
+ if (property === undefined) {
101
+ throw new Error('Cannot re-assign the entire object');
102
+ }
103
+ o[property] = returnValue;
104
+ }
105
+ } else {
106
+ if (property === undefined) {
107
+ throw new Error('Cannot re-assign the entire object');
108
+ }
109
+ o[property] = updateOrValue;
110
+ }
111
+ });
112
+ };
113
+
114
+ if (property !== undefined) {
115
+ return [useObjectProperty(registry, obj, property), callback as ObjectPropUpdateCallback<Exclude<T, undefined>[K]>];
116
+ }
117
+ return [useObjectValue(registry, obj), callback as ObjectUpdateCallback<Exclude<T, undefined>>];
118
+ }
119
+
120
+ /**
121
+ * Internal function for subscribing to an entire Echo object.
122
+ * Uses snapshots from AtomObj.make() which return new object references on each change.
123
+ */
124
+ function useObjectValue<T extends Entity.Unknown | undefined>(
125
+ registry: Registry.Registry,
126
+ obj: MaybeAccessor<T>,
127
+ ): Accessor<ConditionalUndefined<T, Exclude<T, undefined>>> {
128
+ // Memoize the resolved object to track changes.
129
+ const resolvedObj = createMemo(() => access(obj));
130
+
131
+ // Initialize with snapshot of the current object (if available).
132
+ const initialObj = resolvedObj();
133
+ const initialSnapshot = initialObj ? (Obj.getSnapshot(initialObj as Obj.Any) as T) : undefined;
134
+ const [value, setValue] = createSignal<T | undefined>(initialSnapshot);
135
+
136
+ // Subscribe to atom updates.
137
+ createEffect(() => {
138
+ const currentObj = resolvedObj();
139
+
140
+ if (!currentObj) {
141
+ setValue(() => undefined);
142
+ return;
143
+ }
144
+
145
+ const atom = AtomObj.make(currentObj);
146
+ const currentValue = registry.get(atom);
147
+ setValue(() => currentValue as T);
148
+
149
+ const unsubscribe = registry.subscribe(
150
+ atom,
151
+ () => {
152
+ setValue(() => registry.get(atom) as T);
153
+ },
154
+ { immediate: true },
155
+ );
156
+
157
+ onCleanup(unsubscribe);
158
+ });
159
+
160
+ return value as Accessor<ConditionalUndefined<T, Exclude<T, undefined>>>;
161
+ }
162
+
163
+ /**
164
+ * Internal function for subscribing to a specific property of an Echo object.
165
+ */
166
+ function useObjectProperty<T extends Entity.Unknown | undefined, K extends keyof Exclude<T, undefined>>(
167
+ registry: Registry.Registry,
168
+ obj: MaybeAccessor<T>,
169
+ property: K,
170
+ ): Accessor<ConditionalUndefined<T, Exclude<T, undefined>[K]>> {
171
+ // Memoize the resolved object to track changes.
172
+ const resolvedObj = createMemo(() => access(obj));
173
+
174
+ type NonUndefinedT = Exclude<T, undefined>;
175
+ const initialValue = resolvedObj() ? (resolvedObj() as NonUndefinedT)[property] : undefined;
176
+ const [value, setValue] = createSignal<NonUndefinedT[K] | undefined>(initialValue);
177
+
178
+ // Subscribe to atom updates.
179
+ createEffect(() => {
180
+ const currentObj = resolvedObj();
181
+
182
+ if (!currentObj) {
183
+ setValue(() => undefined);
184
+ return;
185
+ }
186
+
187
+ type NonUndefinedT = Exclude<T, undefined>;
188
+ const echoObj = currentObj as NonUndefinedT;
189
+ const atom = AtomObj.makeProperty(echoObj, property);
190
+ const currentValue = registry.get(atom);
191
+ setValue(() => currentValue);
192
+
193
+ const unsubscribe = registry.subscribe(
194
+ atom,
195
+ () => {
196
+ setValue(() => registry.get(atom) as NonUndefinedT[K]);
197
+ },
198
+ { immediate: true },
199
+ );
200
+
201
+ onCleanup(unsubscribe);
202
+ });
203
+
204
+ return value as Accessor<ConditionalUndefined<T, Exclude<T, undefined>[K]>>;
205
+ }