@dxos/echo-react 0.10.0 → 0.11.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/dist/lib/index.mjs +329 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/useObject.d.ts +9 -0
- package/dist/types/src/useObject.d.ts.map +1 -1
- package/dist/types/src/usePagination.d.ts +56 -0
- package/dist/types/src/usePagination.d.ts.map +1 -0
- package/dist/types/src/usePagination.test.d.ts +2 -0
- package/dist/types/src/usePagination.test.d.ts.map +1 -0
- package/dist/types/src/useQuery.d.ts +1 -0
- package/dist/types/src/useQuery.d.ts.map +1 -1
- package/dist/types/src/useQuery.test.d.ts +2 -0
- package/dist/types/src/useQuery.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/index.ts +1 -0
- package/src/useObject.ts +25 -20
- package/src/usePagination.test.tsx +572 -0
- package/src/usePagination.ts +276 -0
- package/src/useQuery.test.tsx +106 -0
- package/src/useQuery.ts +10 -2
- package/dist/lib/neutral/index.mjs +0 -194
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo, useRef, useSyncExternalStore } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type Database, type Entity, Query } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
const EMPTY_ARRAY: never[] = [];
|
|
10
|
+
|
|
11
|
+
export type UsePaginationOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Max loaded items before the newest end is evicted (the window's start advances instead of
|
|
14
|
+
* growing further). Defaults to 10x the query's page size.
|
|
15
|
+
*/
|
|
16
|
+
maxWindowSize?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type PaginationResult<T> = {
|
|
20
|
+
/** Loaded window, in query order (newest first for `Order.natural('desc')`). */
|
|
21
|
+
items: T[];
|
|
22
|
+
/** Extends toward older items by one page. Single-flight; no-op while loading or exhausted. */
|
|
23
|
+
getNext: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Extends back toward the head by one page (the inverse of `getNext`). Single-flight; no-op
|
|
26
|
+
* while loading or already at the head.
|
|
27
|
+
*/
|
|
28
|
+
getPrevious: () => void;
|
|
29
|
+
/** Whether older items remain beyond what is currently loaded. Inferred from the last page's size. */
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
/** True until the current range delivers its first result, even an empty one. */
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
/** False once eviction has detached the window from the live head (i.e. skip > 0). */
|
|
34
|
+
atHead: boolean;
|
|
35
|
+
/** Resets to the newest page and resumes live updates. */
|
|
36
|
+
jumpToHead: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Reactive snapshot handed to React by `useSyncExternalStore`; a fresh object only on real change. */
|
|
40
|
+
type Snapshot<O> = { items: O[]; skip: number; limit: number; isLoading: boolean };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The element type a paginated query yields: the flat record for an aggregate query (branded with
|
|
44
|
+
* {@link Query.AggregateResult}), otherwise the query's entity type. Mirrors `useQuery`'s overloads.
|
|
45
|
+
*/
|
|
46
|
+
type PaginationElement<Q extends Query.Any> =
|
|
47
|
+
Query.Type<Q> extends Query.AggregateResult ? Query.Type<Q> : Entity.Entity<Query.Type<Q>>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds the external store backing `usePagination` for one query identity (filter/order/page
|
|
51
|
+
* size/resource). Mirrors `useQuery`'s own `useMemo`-built `{ getObjects, subscribe }` pair as
|
|
52
|
+
* closely as the added pagination state allows: nothing is queried and `.results` is never read
|
|
53
|
+
* until `subscribe()` is called (i.e. from `useSyncExternalStore`'s commit-safe effect, never
|
|
54
|
+
* during render). `getSnapshot` returns the last *delivered* results (`displayItems`), which are
|
|
55
|
+
* only replaced once a newly-pointed range actually produces a result -- so the previous page stays
|
|
56
|
+
* visible (rather than flashing empty) while an async range loads. `QueryResultImpl` still owns
|
|
57
|
+
* recompute/dedup; we only retain the most recent delivered array.
|
|
58
|
+
*
|
|
59
|
+
* `skip`/`limit` live as plain closure state rather than React state: pagination is internal
|
|
60
|
+
* bookkeeping for *which range this same identity is currently showing*, not a prop the rest of
|
|
61
|
+
* the component tree needs to read independently. `getNext`/`getPrevious` re-point the query at
|
|
62
|
+
* a new range by opening a fresh `QueryResult` for it, without discarding the previous one's
|
|
63
|
+
* results until the new one delivers its own -- that's what keeps the previous page visible while
|
|
64
|
+
* the new one loads (see the hook's own doc comment for why that matters).
|
|
65
|
+
*/
|
|
66
|
+
const createPaginationStore = <Q extends Query.Any, O>(
|
|
67
|
+
resource: Database.Queryable | undefined,
|
|
68
|
+
innerAst: Parameters<typeof Query.fromAst>[0],
|
|
69
|
+
pageSize: number,
|
|
70
|
+
initialMaxWindowSize: number,
|
|
71
|
+
// Seed for the visible page, handed over when a query-identity change rebuilds the store, so the
|
|
72
|
+
// view refreshes in place instead of flashing empty + loading (see the hook's `previousItemsRef`).
|
|
73
|
+
initialItems: O[],
|
|
74
|
+
) => {
|
|
75
|
+
let maxWindowSize = initialMaxWindowSize;
|
|
76
|
+
let skip = 0;
|
|
77
|
+
let limit = pageSize;
|
|
78
|
+
// Single-flight latch coalescing a burst of synchronous `getNext`/`getPrevious` calls (a
|
|
79
|
+
// virtualizer's `onChange` fires once per rendered row) into one range change.
|
|
80
|
+
let rangeChangePending = false;
|
|
81
|
+
let innerUnsubscribe: (() => void) | undefined;
|
|
82
|
+
// Held across a range change (and seeded across an identity change) until the new range delivers,
|
|
83
|
+
// so the list never flickers to empty while a new (possibly async) range loads.
|
|
84
|
+
let displayItems: O[] = initialItems.length > 0 ? initialItems : EMPTY_ARRAY;
|
|
85
|
+
// Cleared on the range's first delivery, never on a timer, so it can't stick true on an empty feed.
|
|
86
|
+
// False when seeded: a page is already shown, so this is a background refresh, not a first load.
|
|
87
|
+
let isLoading = resource !== undefined && displayItems.length === 0;
|
|
88
|
+
let snapshot: Snapshot<O> = { items: displayItems, skip, limit, isLoading };
|
|
89
|
+
const listeners = new Set<() => void>();
|
|
90
|
+
|
|
91
|
+
const notify = () => {
|
|
92
|
+
snapshot = { items: displayItems, skip, limit, isLoading };
|
|
93
|
+
for (const listener of listeners) {
|
|
94
|
+
listener();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// (Re)points the query at the current skip/limit. `markLoading` is true for a range the user
|
|
99
|
+
// navigated to (initial load, `getNext`/`getPrevious`, `jumpToHead`); a seeded background refresh
|
|
100
|
+
// passes false so the previous page stays visible and consumers don't blank on it.
|
|
101
|
+
//
|
|
102
|
+
// TODO(wittjosiah): For feeds, this re-fetches/decodes the whole feed on every range change --
|
|
103
|
+
// only what's rendered is bounded, not what's fetched. Needs index-backed keyset pagination to
|
|
104
|
+
// fix properly.
|
|
105
|
+
const pointAtRange = (markLoading: boolean) => {
|
|
106
|
+
innerUnsubscribe?.();
|
|
107
|
+
innerUnsubscribe = undefined;
|
|
108
|
+
|
|
109
|
+
if (!resource) {
|
|
110
|
+
displayItems = EMPTY_ARRAY;
|
|
111
|
+
isLoading = false;
|
|
112
|
+
notify();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `Query.fromAst` necessarily erases the entity type to `Query.Any`; the cast restores it,
|
|
117
|
+
// which is sound because only `skip`/`limit` were layered on top of the caller's own query's
|
|
118
|
+
// selection/filter/type -- the entity type this query selects is unchanged from `Q`.
|
|
119
|
+
const effectiveQuery = Query.fromAst(innerAst).skip(skip).limit(limit) as Q;
|
|
120
|
+
const nextQueryResult = resource.query(effectiveQuery);
|
|
121
|
+
if (markLoading) {
|
|
122
|
+
isLoading = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Publish results and clear `isLoading` only on delivery -- deferred for an async source (a feed
|
|
126
|
+
// served by the index) until its first response, which arrives even when that response is empty.
|
|
127
|
+
innerUnsubscribe = nextQueryResult.subscribe(
|
|
128
|
+
() => {
|
|
129
|
+
displayItems = nextQueryResult.results as O[];
|
|
130
|
+
isLoading = false;
|
|
131
|
+
notify();
|
|
132
|
+
},
|
|
133
|
+
{ fire: true },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
notify();
|
|
137
|
+
// The re-entrancy latch only needs to survive one tick, not the query's actual delivery.
|
|
138
|
+
queueMicrotask(() => {
|
|
139
|
+
rangeChangePending = false;
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
subscribe: (onStoreChange: () => void) => {
|
|
145
|
+
listeners.add(onStoreChange);
|
|
146
|
+
if (listeners.size === 1) {
|
|
147
|
+
// Seeded (background identity refresh) → don't surface loading; otherwise it's a first load.
|
|
148
|
+
pointAtRange(displayItems.length === 0);
|
|
149
|
+
}
|
|
150
|
+
return () => {
|
|
151
|
+
listeners.delete(onStoreChange);
|
|
152
|
+
if (listeners.size === 0) {
|
|
153
|
+
innerUnsubscribe?.();
|
|
154
|
+
innerUnsubscribe = undefined;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
getSnapshot: (): Snapshot<O> => snapshot,
|
|
159
|
+
setMaxWindowSize: (value: number) => {
|
|
160
|
+
maxWindowSize = value;
|
|
161
|
+
},
|
|
162
|
+
getNext: () => {
|
|
163
|
+
if (isLoading || rangeChangePending || snapshot.items.length < limit) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
rangeChangePending = true;
|
|
167
|
+
const nextLimit = limit + pageSize;
|
|
168
|
+
if (nextLimit <= maxWindowSize) {
|
|
169
|
+
limit = nextLimit;
|
|
170
|
+
} else {
|
|
171
|
+
limit = maxWindowSize;
|
|
172
|
+
skip += pageSize;
|
|
173
|
+
}
|
|
174
|
+
pointAtRange(true);
|
|
175
|
+
},
|
|
176
|
+
getPrevious: () => {
|
|
177
|
+
if (isLoading || rangeChangePending || skip === 0) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
rangeChangePending = true;
|
|
181
|
+
// Slides the window toward the head at constant size (limit unchanged) -- decreasing skip
|
|
182
|
+
// while holding limit fixed both reveals newer items at the front and drops an equal count
|
|
183
|
+
// of the oldest loaded items, mirroring getNext's own "slide" branch (taken once it hits
|
|
184
|
+
// maxWindowSize) in the opposite direction.
|
|
185
|
+
skip = Math.max(0, skip - pageSize);
|
|
186
|
+
pointAtRange(true);
|
|
187
|
+
},
|
|
188
|
+
jumpToHead: () => {
|
|
189
|
+
rangeChangePending = false;
|
|
190
|
+
skip = 0;
|
|
191
|
+
limit = pageSize;
|
|
192
|
+
pointAtRange(true);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Wraps a query with page management for paginated reads. The query's own `.limit(n)` clause is
|
|
199
|
+
* the page size -- the hook manages which page is loaded by rewriting `skip`/`limit` on top of
|
|
200
|
+
* it; the query must not carry its own `.skip()`.
|
|
201
|
+
*
|
|
202
|
+
* The query's `.orderBy(...)` is otherwise untouched, and applies to any query the underlying
|
|
203
|
+
* `Database.Queryable` supports -- e.g. `Order.natural('desc')` for feed insertion order.
|
|
204
|
+
*
|
|
205
|
+
* Keeps showing the previous page while a new one loads -- across both a `skip`/`limit` move and a
|
|
206
|
+
* query-identity change (filter/order/page-size/resource). The latter rebuilds the internal store;
|
|
207
|
+
* the last page is handed to the new store as its seed so the view refreshes in place instead of
|
|
208
|
+
* flashing empty, and `isLoading` stays put (a background refresh isn't a load the user asked for).
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```tsx
|
|
212
|
+
* const { items, getNext, hasMore, isLoading, atHead, jumpToHead } = usePagination(
|
|
213
|
+
* db,
|
|
214
|
+
* Query.select(Filter.type(Message.Message)).from(feed).orderBy(Order.natural('desc')).limit(50),
|
|
215
|
+
* );
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
export const usePagination = <Q extends Query.Any>(
|
|
219
|
+
resource: Database.Queryable | undefined,
|
|
220
|
+
query: Q,
|
|
221
|
+
options?: UsePaginationOptions,
|
|
222
|
+
): PaginationResult<PaginationElement<Q>> => {
|
|
223
|
+
if (query.ast.type !== 'limit') {
|
|
224
|
+
throw new TypeError('usePagination requires the query to carry .limit(pageSize).');
|
|
225
|
+
}
|
|
226
|
+
if (query.ast.query.type === 'skip') {
|
|
227
|
+
throw new TypeError('usePagination manages .skip() internally -- do not include it in the query.');
|
|
228
|
+
}
|
|
229
|
+
const pageSize = query.ast.limit;
|
|
230
|
+
const innerAst = query.ast.query;
|
|
231
|
+
const innerAstKey = JSON.stringify(innerAst);
|
|
232
|
+
const maxWindowSize = options?.maxWindowSize ?? pageSize * 10;
|
|
233
|
+
|
|
234
|
+
// The page shown right now, carried across a store rebuild to seed the new store. Written just
|
|
235
|
+
// after the snapshot below, so the next render's `useMemo` sees the previously-displayed page.
|
|
236
|
+
const previousItemsRef = useRef<PaginationElement<Q>[]>(EMPTY_ARRAY);
|
|
237
|
+
|
|
238
|
+
// A new store is built when "what" is being shown changes: filter/order (`innerAstKey`), page
|
|
239
|
+
// size, or `resource` (matching `useQuery`, which likewise keys its memo on `resource` directly
|
|
240
|
+
// rather than just its truthiness). It is seeded from the previous store's page (see above).
|
|
241
|
+
const store = useMemo(
|
|
242
|
+
() =>
|
|
243
|
+
createPaginationStore<Q, PaginationElement<Q>>(
|
|
244
|
+
resource,
|
|
245
|
+
innerAst,
|
|
246
|
+
pageSize,
|
|
247
|
+
maxWindowSize,
|
|
248
|
+
previousItemsRef.current,
|
|
249
|
+
),
|
|
250
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
251
|
+
[resource, innerAstKey, pageSize],
|
|
252
|
+
);
|
|
253
|
+
// `maxWindowSize` deliberately isn't part of the store's `useMemo` deps above (a caller passing
|
|
254
|
+
// a literal default each render shouldn't reset the window), but a genuine change should still
|
|
255
|
+
// take effect immediately rather than only on the next unrelated identity change.
|
|
256
|
+
store.setMaxWindowSize(maxWindowSize);
|
|
257
|
+
|
|
258
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
259
|
+
previousItemsRef.current = snapshot.items;
|
|
260
|
+
|
|
261
|
+
// Stable across renders that don't change `snapshot` (`store`'s own methods are already stable
|
|
262
|
+
// per store identity) -- callers can pass this straight through to a memoized child (e.g. a
|
|
263
|
+
// virtualizer's pagination callbacks) without re-destructuring and re-bundling it themselves.
|
|
264
|
+
return useMemo(
|
|
265
|
+
() => ({
|
|
266
|
+
items: snapshot.items,
|
|
267
|
+
getNext: store.getNext,
|
|
268
|
+
getPrevious: store.getPrevious,
|
|
269
|
+
hasMore: snapshot.items.length >= snapshot.limit,
|
|
270
|
+
isLoading: snapshot.isLoading,
|
|
271
|
+
atHead: snapshot.skip === 0,
|
|
272
|
+
jumpToHead: store.jumpToHead,
|
|
273
|
+
}),
|
|
274
|
+
[snapshot, store],
|
|
275
|
+
);
|
|
276
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { Aggregate, Filter, Obj, Query } from '@dxos/echo';
|
|
9
|
+
import { EchoTestBuilder } from '@dxos/echo-client/testing';
|
|
10
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
11
|
+
|
|
12
|
+
import { useQuery } from './useQuery';
|
|
13
|
+
|
|
14
|
+
describe('useQuery', () => {
|
|
15
|
+
let builder: EchoTestBuilder;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
builder = await new EchoTestBuilder().open();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await builder.close();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('returns entities for a plain query', async () => {
|
|
26
|
+
await using peer = await builder.createPeer({ types: [TestSchema.Person] });
|
|
27
|
+
const db = await peer.createDatabase();
|
|
28
|
+
|
|
29
|
+
db.add(Obj.make(TestSchema.Person, { name: 'Alice', username: 'alice', email: 'alice@example.com', tasks: [] }));
|
|
30
|
+
await db.flush();
|
|
31
|
+
|
|
32
|
+
const { result } = renderHook(() => useQuery(db, Query.select(Filter.type(TestSchema.Person))));
|
|
33
|
+
|
|
34
|
+
await waitFor(() => {
|
|
35
|
+
expect(result.current).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
expect(result.current[0].name).toBe('Alice');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('returns grouped results for an aggregate query', async () => {
|
|
41
|
+
await using peer = await builder.createPeer({ types: [TestSchema.Expando] });
|
|
42
|
+
const db = await peer.createDatabase();
|
|
43
|
+
|
|
44
|
+
db.add(Obj.make(TestSchema.Expando, { category: 'a' }));
|
|
45
|
+
db.add(Obj.make(TestSchema.Expando, { category: 'a' }));
|
|
46
|
+
db.add(Obj.make(TestSchema.Expando, { category: 'b' }));
|
|
47
|
+
await db.flush();
|
|
48
|
+
|
|
49
|
+
const query = Query.select(Filter.everything()).aggregate({
|
|
50
|
+
category: Aggregate.group('category'),
|
|
51
|
+
count: Aggregate.count(),
|
|
52
|
+
items: Aggregate.items(),
|
|
53
|
+
});
|
|
54
|
+
const { result } = renderHook(() => useQuery(db, query));
|
|
55
|
+
|
|
56
|
+
await waitFor(() => {
|
|
57
|
+
expect(result.current).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const byKey = new Map(result.current.map((group) => [group.category, group]));
|
|
61
|
+
expect(byKey.get('a')?.count).toBe(2);
|
|
62
|
+
expect(byKey.get('a')?.items).toHaveLength(2);
|
|
63
|
+
expect(byKey.get('b')?.count).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('groups update reactively as objects are added', async () => {
|
|
67
|
+
await using peer = await builder.createPeer({ types: [TestSchema.Expando] });
|
|
68
|
+
const db = await peer.createDatabase();
|
|
69
|
+
|
|
70
|
+
db.add(Obj.make(TestSchema.Expando, { category: 'a' }));
|
|
71
|
+
await db.flush();
|
|
72
|
+
|
|
73
|
+
const query = Query.select(Filter.everything()).aggregate({ category: Aggregate.group('category') });
|
|
74
|
+
const { result } = renderHook(() => useQuery(db, query));
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(result.current).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
db.add(Obj.make(TestSchema.Expando, { category: 'b' }));
|
|
81
|
+
await db.flush({ updates: true });
|
|
82
|
+
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
expect(result.current).toHaveLength(2);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('stable snapshot identity when nothing changes', async () => {
|
|
89
|
+
await using peer = await builder.createPeer({ types: [TestSchema.Expando] });
|
|
90
|
+
const db = await peer.createDatabase();
|
|
91
|
+
|
|
92
|
+
db.add(Obj.make(TestSchema.Expando, { category: 'a' }));
|
|
93
|
+
await db.flush();
|
|
94
|
+
|
|
95
|
+
const query = Query.select(Filter.everything()).aggregate({ category: Aggregate.group('category') });
|
|
96
|
+
const { result, rerender } = renderHook(() => useQuery(db, query));
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(result.current).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const first = result.current;
|
|
103
|
+
rerender();
|
|
104
|
+
expect(result.current).toBe(first);
|
|
105
|
+
});
|
|
106
|
+
});
|
package/src/useQuery.ts
CHANGED
|
@@ -11,6 +11,10 @@ const EMPTY_ARRAY: never[] = [];
|
|
|
11
11
|
const noop = () => {};
|
|
12
12
|
|
|
13
13
|
interface UseQueryFn {
|
|
14
|
+
// Aggregate queries produce flat records branded with `Query.AggregateResult`; return them as-is,
|
|
15
|
+
// never wrapped in `Entity.Entity` (they are not entities).
|
|
16
|
+
<R extends Query.AggregateResult>(resource: Database.Queryable | undefined, query: Query.Query<R>): R[];
|
|
17
|
+
|
|
14
18
|
<Q extends Query.Any, O extends Entity.Entity<Query.Type<Q>> = Entity.Entity<Query.Type<Q>>>(
|
|
15
19
|
resource: Database.Queryable | undefined,
|
|
16
20
|
query: Q,
|
|
@@ -27,10 +31,14 @@ interface UseQueryFn {
|
|
|
27
31
|
*
|
|
28
32
|
* @param queryOrFilter - The query or filter to apply. Query is memoized based on the AST. No need to call useMemo.
|
|
29
33
|
*/
|
|
34
|
+
// The overloaded public signature returns `O[]` (entities) or the flat record `R[]` (aggregate
|
|
35
|
+
// queries) depending on the query shape; the implementation forwards whatever the query result
|
|
36
|
+
// produces, so — as with any implementation of a multi-signature overloaded function type — it is
|
|
37
|
+
// typed as `any[]` here rather than picking one of the mutually-incompatible overload return shapes.
|
|
30
38
|
export const useQuery: UseQueryFn = (
|
|
31
39
|
resource: Database.Queryable | undefined,
|
|
32
40
|
queryOrFilter: Query.Any | Filter.Any,
|
|
33
|
-
):
|
|
41
|
+
): any[] => {
|
|
34
42
|
const query = Filter.is(queryOrFilter) ? Query.select(queryOrFilter) : queryOrFilter;
|
|
35
43
|
|
|
36
44
|
const { getObjects, subscribe } = useMemo(() => {
|
|
@@ -55,6 +63,6 @@ export const useQuery: UseQueryFn = (
|
|
|
55
63
|
|
|
56
64
|
// https://beta.reactjs.org/reference/react/useSyncExternalStore
|
|
57
65
|
// NOTE: This hook will resubscribe whenever the callback passed to the first argument changes; make sure it is stable.
|
|
58
|
-
const objects = useSyncExternalStore<
|
|
66
|
+
const objects = useSyncExternalStore<any[]>(subscribe, getObjects);
|
|
59
67
|
return objects;
|
|
60
68
|
};
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// src/useQuery.ts
|
|
2
|
-
import { useMemo, useSyncExternalStore } from "react";
|
|
3
|
-
import { Filter, Query } from "@dxos/echo";
|
|
4
|
-
var EMPTY_ARRAY = [];
|
|
5
|
-
var noop = () => {
|
|
6
|
-
};
|
|
7
|
-
var useQuery = (resource, queryOrFilter) => {
|
|
8
|
-
const query = Filter.is(queryOrFilter) ? Query.select(queryOrFilter) : queryOrFilter;
|
|
9
|
-
const { getObjects, subscribe } = useMemo(() => {
|
|
10
|
-
let queryResult = void 0;
|
|
11
|
-
if (resource) {
|
|
12
|
-
queryResult = resource.query(query);
|
|
13
|
-
}
|
|
14
|
-
let subscribed = false;
|
|
15
|
-
return {
|
|
16
|
-
getObjects: () => subscribed && queryResult ? queryResult.results : EMPTY_ARRAY,
|
|
17
|
-
subscribe: (cb) => {
|
|
18
|
-
subscribed = true;
|
|
19
|
-
const unsubscribe = queryResult?.subscribe(cb) ?? noop;
|
|
20
|
-
return () => {
|
|
21
|
-
unsubscribe?.();
|
|
22
|
-
subscribed = false;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
}, [
|
|
27
|
-
resource,
|
|
28
|
-
JSON.stringify(query.ast)
|
|
29
|
-
]);
|
|
30
|
-
const objects = useSyncExternalStore(subscribe, getObjects);
|
|
31
|
-
return objects;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// src/useType.ts
|
|
35
|
-
import { useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
36
|
-
import { DXN, EID, Filter as Filter2, Query as Query2, Scope, Type } from "@dxos/echo";
|
|
37
|
-
var useType = (db, typeUri) => {
|
|
38
|
-
const { subscribe, getType } = useMemo2(() => {
|
|
39
|
-
if (!typeUri || !db) {
|
|
40
|
-
return {
|
|
41
|
-
subscribe: () => () => {
|
|
42
|
-
},
|
|
43
|
-
getType: () => void 0
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const searchEid = EID.isEID(typeUri) ? EID.tryParse(typeUri) : void 0;
|
|
47
|
-
const searchDxn = DXN.isDXN(typeUri) ? DXN.tryMake(typeUri) : void 0;
|
|
48
|
-
const queryResult = db.query(Query2.select(Filter2.type(Type.Type)).from(Scope.space(), Scope.registry()));
|
|
49
|
-
let subscribed = false;
|
|
50
|
-
const find = () => {
|
|
51
|
-
if (!subscribed) {
|
|
52
|
-
return void 0;
|
|
53
|
-
}
|
|
54
|
-
return queryResult.results.find((type) => {
|
|
55
|
-
const uri = Type.getURI(type);
|
|
56
|
-
if (uri === typeUri) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
if (searchEid && EID.isEID(uri)) {
|
|
60
|
-
const typeEid = EID.tryParse(uri);
|
|
61
|
-
return typeEid != null && EID.getEntityId(typeEid) === EID.getEntityId(searchEid);
|
|
62
|
-
}
|
|
63
|
-
if (searchDxn && DXN.isDXN(uri)) {
|
|
64
|
-
const typeDxn = DXN.tryMake(uri);
|
|
65
|
-
return typeDxn != null && DXN.getName(typeDxn) === DXN.getName(searchDxn);
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
return {
|
|
71
|
-
subscribe: (onStoreChange) => {
|
|
72
|
-
subscribed = true;
|
|
73
|
-
const unsubscribe = queryResult.subscribe(onStoreChange);
|
|
74
|
-
return () => {
|
|
75
|
-
unsubscribe();
|
|
76
|
-
subscribed = false;
|
|
77
|
-
};
|
|
78
|
-
},
|
|
79
|
-
getType: find
|
|
80
|
-
};
|
|
81
|
-
}, [
|
|
82
|
-
typeUri,
|
|
83
|
-
db
|
|
84
|
-
]);
|
|
85
|
-
return useSyncExternalStore2(subscribe, getType);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// src/useObject.ts
|
|
89
|
-
import { useAtomValue } from "@effect-atom/atom-react";
|
|
90
|
-
import * as Atom from "@effect-atom/atom/Atom";
|
|
91
|
-
import { useCallback, useMemo as useMemo3 } from "react";
|
|
92
|
-
import { Obj, Ref } from "@dxos/echo";
|
|
93
|
-
var useObject = (objOrRef, property) => {
|
|
94
|
-
const isRef = Ref.isRef(objOrRef);
|
|
95
|
-
const liveObj = isRef ? objOrRef?.target : objOrRef;
|
|
96
|
-
const callback = useCallback((updateOrValue) => {
|
|
97
|
-
const obj = isRef ? objOrRef?.target : liveObj;
|
|
98
|
-
if (obj === void 0) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
Obj.update(obj, (obj2) => {
|
|
102
|
-
if (typeof updateOrValue === "function") {
|
|
103
|
-
const returnValue = updateOrValue(property !== void 0 ? obj2[property] : obj2);
|
|
104
|
-
if (returnValue !== void 0) {
|
|
105
|
-
if (property === void 0) {
|
|
106
|
-
throw new Error("Cannot re-assign the entire object");
|
|
107
|
-
}
|
|
108
|
-
obj2[property] = returnValue;
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
if (property === void 0) {
|
|
112
|
-
throw new Error("Cannot re-assign the entire object");
|
|
113
|
-
}
|
|
114
|
-
obj2[property] = updateOrValue;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}, [
|
|
118
|
-
objOrRef,
|
|
119
|
-
property,
|
|
120
|
-
isRef,
|
|
121
|
-
liveObj
|
|
122
|
-
]);
|
|
123
|
-
if (property !== void 0) {
|
|
124
|
-
useRefLoad(objOrRef);
|
|
125
|
-
return [
|
|
126
|
-
useObjectProperty(liveObj, property),
|
|
127
|
-
callback
|
|
128
|
-
];
|
|
129
|
-
} else {
|
|
130
|
-
return [
|
|
131
|
-
useObjectValue(objOrRef),
|
|
132
|
-
callback
|
|
133
|
-
];
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
var useObjectValue = (objOrRef) => {
|
|
137
|
-
const atom = useMemo3(() => {
|
|
138
|
-
if (objOrRef == null) {
|
|
139
|
-
return Atom.make(() => void 0);
|
|
140
|
-
}
|
|
141
|
-
if (Ref.isRef(objOrRef)) {
|
|
142
|
-
return Obj.atom(objOrRef);
|
|
143
|
-
}
|
|
144
|
-
return Obj.atom(objOrRef);
|
|
145
|
-
}, [
|
|
146
|
-
objOrRef
|
|
147
|
-
]);
|
|
148
|
-
return useAtomValue(atom);
|
|
149
|
-
};
|
|
150
|
-
var useRefLoad = (objOrRef) => {
|
|
151
|
-
const atom = useMemo3(() => {
|
|
152
|
-
if (objOrRef == null || !Ref.isRef(objOrRef)) {
|
|
153
|
-
return Atom.make(() => void 0);
|
|
154
|
-
}
|
|
155
|
-
return objOrRef.atom;
|
|
156
|
-
}, [
|
|
157
|
-
objOrRef
|
|
158
|
-
]);
|
|
159
|
-
useAtomValue(atom);
|
|
160
|
-
};
|
|
161
|
-
var useObjectProperty = (obj, property) => {
|
|
162
|
-
const atom = useMemo3(() => {
|
|
163
|
-
if (obj == null) {
|
|
164
|
-
return Atom.make(() => void 0);
|
|
165
|
-
}
|
|
166
|
-
return Obj.atomProperty(obj, property);
|
|
167
|
-
}, [
|
|
168
|
-
obj,
|
|
169
|
-
property
|
|
170
|
-
]);
|
|
171
|
-
return useAtomValue(atom);
|
|
172
|
-
};
|
|
173
|
-
var useObjects = (refs) => {
|
|
174
|
-
const atom = useMemo3(() => Atom.make((get) => {
|
|
175
|
-
const results = [];
|
|
176
|
-
for (const ref of refs) {
|
|
177
|
-
const value = get(Obj.atom(ref));
|
|
178
|
-
if (value !== void 0) {
|
|
179
|
-
results.push(value);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return results;
|
|
183
|
-
}), [
|
|
184
|
-
refs
|
|
185
|
-
]);
|
|
186
|
-
return useAtomValue(atom);
|
|
187
|
-
};
|
|
188
|
-
export {
|
|
189
|
-
useObject,
|
|
190
|
-
useObjects,
|
|
191
|
-
useQuery,
|
|
192
|
-
useType
|
|
193
|
-
};
|
|
194
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/useQuery.ts", "../../../src/useType.ts", "../../../src/useObject.ts"],
|
|
4
|
-
"sourcesContent": ["//\n// Copyright 2022 DXOS.org\n//\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { type Database, type Entity, Filter, Query } from '@dxos/echo';\n\nconst EMPTY_ARRAY: never[] = [];\n\nconst noop = () => {};\n\ninterface UseQueryFn {\n <Q extends Query.Any, O extends Entity.Entity<Query.Type<Q>> = Entity.Entity<Query.Type<Q>>>(\n resource: Database.Queryable | undefined,\n query: Q,\n ): O[];\n\n <F extends Filter.Any, O extends Entity.Entity<Filter.Type<F>> = Entity.Entity<Filter.Type<F>>>(\n resource: Database.Queryable | undefined,\n filter: F,\n ): O[];\n}\n\n/**\n * Create subscription.\n *\n * @param queryOrFilter - The query or filter to apply. Query is memoized based on the AST. No need to call useMemo.\n */\nexport const useQuery: UseQueryFn = (\n resource: Database.Queryable | undefined,\n queryOrFilter: Query.Any | Filter.Any,\n): Entity.Any[] => {\n const query = Filter.is(queryOrFilter) ? Query.select(queryOrFilter) : queryOrFilter;\n\n const { getObjects, subscribe } = useMemo(() => {\n let queryResult = undefined;\n if (resource) {\n queryResult = resource.query(query);\n }\n\n let subscribed = false;\n return {\n getObjects: () => (subscribed && queryResult ? queryResult.results : EMPTY_ARRAY),\n subscribe: (cb: () => void) => {\n subscribed = true;\n const unsubscribe = queryResult?.subscribe(cb) ?? noop;\n return () => {\n unsubscribe?.();\n subscribed = false;\n };\n },\n };\n }, [resource, JSON.stringify(query.ast)]);\n\n // https://beta.reactjs.org/reference/react/useSyncExternalStore\n // NOTE: This hook will resubscribe whenever the callback passed to the first argument changes; make sure it is stable.\n const objects = useSyncExternalStore<Entity.Any[]>(subscribe, getObjects);\n return objects;\n};\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { type Database, DXN, EID, Filter, Query, Scope, Type, URI } from '@dxos/echo';\n\n/**\n * Subscribe to and retrieve a type by its URI from a space: a static schema's typename DXN, or a\n * persisted (database) schema's `echo:` EID (what `Type.getURI` / `getTypeURIFromQuery` produce).\n *\n * Fans across the owning space db (persisted custom types) and the shared\n * registry (static/runtime plugin types). Persisted types live only in the db,\n * so a registry-only lookup misses them.\n *\n * DXN matching is version-agnostic: `dxn:com.example/Foo` matches `dxn:com.example/Foo:0.1.0`.\n * This lets callers pass a bare typename DXN (no version) from e.g. a `ReferenceAnnotation`.\n */\nexport const useType = <T extends Type.AnyEntity = Type.AnyEntity>(\n db?: Database.Database,\n typeUri?: URI.URI,\n): T | undefined => {\n const { subscribe, getType } = useMemo(() => {\n if (!typeUri || !db) {\n return {\n subscribe: () => () => {},\n getType: (): T | undefined => undefined,\n };\n }\n\n const searchEid = EID.isEID(typeUri) ? EID.tryParse(typeUri) : undefined;\n const searchDxn = DXN.isDXN(typeUri) ? DXN.tryMake(typeUri) : undefined;\n\n const queryResult = db.query(Query.select(Filter.type(Type.Type)).from(Scope.space(), Scope.registry()));\n let subscribed = false;\n const find = (): T | undefined => {\n if (!subscribed) {\n return undefined;\n }\n return queryResult.results.find((type) => {\n const uri = Type.getURI(type);\n if (uri === typeUri) {\n return true;\n }\n // EID matching is space-agnostic: echo:/<id> matches echo://<space>/<id>.\n if (searchEid && EID.isEID(uri)) {\n const typeEid = EID.tryParse(uri);\n return typeEid != null && EID.getEntityId(typeEid) === EID.getEntityId(searchEid);\n }\n // DXN matching is version-agnostic so callers may pass an unversioned DXN.\n if (searchDxn && DXN.isDXN(uri)) {\n const typeDxn = DXN.tryMake(uri);\n return typeDxn != null && DXN.getName(typeDxn) === DXN.getName(searchDxn);\n }\n return false;\n }) as T | undefined;\n };\n\n return {\n subscribe: (onStoreChange: () => void) => {\n subscribed = true;\n const unsubscribe = queryResult.subscribe(onStoreChange);\n return () => {\n unsubscribe();\n subscribed = false;\n };\n },\n getType: find,\n };\n }, [typeUri, db]);\n\n return useSyncExternalStore(subscribe, getType);\n};\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport { useAtomValue } from '@effect-atom/atom-react';\nimport * as Atom from '@effect-atom/atom/Atom';\nimport { useCallback, useMemo } from 'react';\n\nimport { Obj, Ref } from '@dxos/echo';\n\nexport interface ObjectUpdateCallback<T> {\n (update: (obj: Obj.Mutable<T>) => void): void;\n (update: (obj: Obj.Mutable<T>) => Obj.Mutable<T>): void;\n}\n\nexport interface ObjectPropUpdateCallback<T> {\n (update: (value: Obj.Mutable<T>) => void): void;\n (update: (value: Obj.Mutable<T>) => Obj.Mutable<T>): void;\n (newValue: T): void;\n}\n\nexport const useObject: {\n /**\n * Hook to subscribe to a Ref's target object.\n * Automatically dereferences the ref and handles async loading.\n * Returns a snapshot (undefined if the ref hasn't loaded yet).\n * Re-renders the component when the ref resolves or the target object changes.\n *\n * @param ref - The Ref to dereference and subscribe to\n * @returns The current target snapshot (or undefined if not loaded) and update callback\n *\n * @idiom org.dxos.echo-react.useObjectReactive\n * applies: Reading a ref's target (or a specific property) inside a React component — establishes a reactive subscription so the component re-renders when the ref resolves or the value changes\n * instead-of: `ref.target` — synchronous and not reactive; returns `undefined` when the target isn't loaded yet and never triggers a re-render when it becomes available\n * uses: {@link useObject}\n * related: org.dxos.echo.objAtomReactive, org.dxos.echo.refLoad\n */\n <T extends Obj.Unknown>(ref: Ref.Ref<T>): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];\n\n /**\n * Hook to subscribe to a Ref's target object that may be undefined.\n * Returns a snapshot (undefined if the ref is undefined or hasn't loaded yet).\n *\n * @param ref - The Ref to dereference and subscribe to (can be undefined)\n * @returns The current target snapshot (or undefined) and update callback\n */\n <T extends Obj.Unknown>(ref: Ref.Ref<T> | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];\n\n /**\n * Hook to subscribe to an entire Echo object.\n * Returns a snapshot of the current object value and automatically re-renders when the object changes.\n *\n * @param obj - The Echo object to subscribe to (objects only, not relations)\n * @returns The current object snapshot and update callback\n */\n <T extends Obj.Unknown>(obj: T): [Obj.Snapshot<T>, ObjectUpdateCallback<T>];\n\n /**\n * Hook to subscribe to an entire Echo object that may be undefined.\n * Returns a snapshot (undefined if the object is undefined).\n *\n * @param obj - The Echo object to subscribe to (can be undefined, objects only)\n * @returns The current object snapshot (or undefined) and update callback\n */\n <T extends Obj.Unknown>(obj: T | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];\n\n /**\n * Hook to subscribe to an Echo object or Ref.\n * Handles both cases - if passed a Ref, dereferences it and subscribes to the target.\n * Returns a snapshot (undefined if ref hasn't loaded).\n *\n * @param objOrRef - The Echo object or Ref to subscribe to\n * @returns The current object snapshot (or undefined) and update callback\n */\n <T extends Obj.Unknown>(objOrRef: T | Ref.Ref<T> | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];\n\n /**\n * Hook to subscribe to a specific property of an Echo object.\n * Returns the current property value and automatically re-renders when the property changes.\n *\n * @param obj - The Echo object to subscribe to (objects only, not relations)\n * @param property - Property key to subscribe to\n * @returns The current property value and update callback\n */\n <T extends Obj.Unknown, K extends keyof T>(obj: T, property: K): [T[K], ObjectPropUpdateCallback<T[K]>];\n\n /**\n * Hook to subscribe to a specific property of an Echo object that may be undefined.\n * Returns undefined if the object is undefined.\n *\n * @param obj - The Echo object to subscribe to (can be undefined, objects only)\n * @param property - Property key to subscribe to\n * @returns The current property value (or undefined) and update callback\n */\n <T extends Obj.Unknown, K extends keyof T>(\n obj: T | undefined,\n property: K,\n ): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];\n\n /**\n * Hook to subscribe to a specific property of a Ref's target object.\n * Automatically dereferences the ref and handles async loading.\n * Returns undefined if the ref hasn't loaded yet.\n *\n * @param ref - The Ref to dereference and subscribe to\n * @param property - Property key to subscribe to\n * @returns The current property value (or undefined if not loaded) and update callback\n */\n <T, K extends keyof T>(ref: Ref.Ref<T>, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];\n\n /**\n * Hook to subscribe to a specific property of a Ref's target object that may be undefined.\n * Returns undefined if the ref is undefined or hasn't loaded yet.\n *\n * @param ref - The Ref to dereference and subscribe to (can be undefined)\n * @param property - Property key to subscribe to\n * @returns The current property value (or undefined) and update callback\n */\n <T, K extends keyof T>(ref: Ref.Ref<T> | undefined, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];\n} = (<T extends Obj.Unknown, K extends keyof T>(objOrRef: T | Ref.Ref<T> | undefined, property?: K): any => {\n // Get the live object for the callback (refs need to dereference).\n const isRef = Ref.isRef(objOrRef);\n const liveObj = isRef ? (objOrRef as Ref.Ref<T>)?.target : (objOrRef as T | undefined);\n\n const callback: ObjectPropUpdateCallback<unknown> = useCallback(\n (updateOrValue: unknown | ((obj: unknown) => unknown)) => {\n // Get current target for refs (may have loaded since render).\n const obj = isRef ? (objOrRef as Ref.Ref<T>)?.target : liveObj;\n if (obj === undefined) {\n return;\n }\n Obj.update(obj, (obj: any) => {\n if (typeof updateOrValue === 'function') {\n const returnValue = updateOrValue(property !== undefined ? obj[property] : obj);\n if (returnValue !== undefined) {\n if (property === undefined) {\n throw new Error('Cannot re-assign the entire object');\n }\n obj[property] = returnValue;\n }\n } else {\n if (property === undefined) {\n throw new Error('Cannot re-assign the entire object');\n }\n obj[property] = updateOrValue;\n }\n });\n },\n [objOrRef, property, isRef, liveObj],\n );\n\n if (property !== undefined) {\n // For refs, subscribe to load event only (not full mutation tracking).\n // Property-level updates are handled by useObjectProperty once the ref loads.\n useRefLoad(objOrRef);\n return [useObjectProperty(liveObj, property as any), callback];\n } else {\n return [useObjectValue(objOrRef), callback];\n }\n}) as any;\n\n/**\n * Internal hook for subscribing to an Echo object or Ref.\n */\nconst useObjectValue = <T extends Obj.Unknown>(objOrRef: T | Ref.Ref<T> | undefined): Obj.Snapshot<T> | undefined => {\n const atom = useMemo(() => {\n if (objOrRef == null) {\n return Atom.make<Obj.Snapshot<T> | undefined>(() => undefined);\n }\n if (Ref.isRef(objOrRef)) {\n return Obj.atom(objOrRef);\n }\n return Obj.atom(objOrRef);\n }, [objOrRef]);\n return useAtomValue(atom) as Obj.Snapshot<T> | undefined;\n};\n\n/**\n * Internal hook for subscribing to ref resolution only (load-once).\n * Triggers a re-render when the ref target first becomes available,\n * without subscribing to subsequent target mutations.\n * For non-refs, this is a no-op.\n */\nconst useRefLoad = <T extends Obj.Unknown>(objOrRef: T | Ref.Ref<T> | undefined): void => {\n const atom = useMemo(() => {\n if (objOrRef == null || !Ref.isRef(objOrRef)) {\n return Atom.make<T | undefined>(() => undefined);\n }\n return objOrRef.atom;\n }, [objOrRef]);\n useAtomValue(atom);\n};\n\n/**\n * Internal hook for subscribing to a specific property of an Echo object.\n */\nconst useObjectProperty = <T extends Obj.Unknown, K extends keyof T>(\n obj: T | undefined,\n property: K,\n): T[K] | undefined => {\n const atom = useMemo(() => {\n if (obj == null) {\n return Atom.make<T[K] | undefined>(() => undefined);\n }\n return Obj.atomProperty(obj, property);\n }, [obj, property]);\n return useAtomValue(atom);\n};\n\n/**\n * Hook to subscribe to multiple Refs' target objects.\n * Automatically dereferences each ref and handles async loading.\n * Returns an array of loaded snapshots (filtering out undefined values).\n *\n * This hook is useful for aggregate computations like counts or filtering\n * across multiple refs without using .target directly.\n *\n * @deprecated Subscribes the component to the whole list, re-rendering on any element's change. Prefer pushing\n * the subscription down — derive the aggregate in an atom (reading each `Obj.atom(ref)`) and read it where it is\n * needed, or subscribe to the individual ref/property closest to the consumer — to keep subscriptions granular.\n *\n * @param refs - Array of Refs to dereference and subscribe to\n * @returns Array of loaded target snapshots (excludes unloaded refs)\n */\nexport const useObjects = <T extends Obj.Unknown>(refs: readonly Ref.Ref<T>[]): Obj.Snapshot<T>[] => {\n const atom = useMemo(\n () =>\n Atom.make((get) => {\n const results: Obj.Snapshot<T>[] = [];\n for (const ref of refs) {\n const value = get(Obj.atom(ref));\n if (value !== undefined) {\n results.push(value as Obj.Snapshot<T>);\n }\n }\n return results;\n }),\n [refs],\n );\n return useAtomValue(atom);\n};\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["useMemo", "useSyncExternalStore", "Filter", "Query", "EMPTY_ARRAY", "noop", "useQuery", "resource", "queryOrFilter", "query", "is", "select", "getObjects", "subscribe", "queryResult", "undefined", "subscribed", "results", "cb", "unsubscribe", "JSON", "stringify", "ast", "objects", "useMemo", "useSyncExternalStore", "DXN", "EID", "Filter", "Query", "Scope", "Type", "useType", "db", "typeUri", "subscribe", "getType", "undefined", "searchEid", "isEID", "tryParse", "searchDxn", "isDXN", "tryMake", "queryResult", "query", "select", "type", "from", "space", "registry", "subscribed", "find", "results", "uri", "getURI", "typeEid", "getEntityId", "typeDxn", "getName", "onStoreChange", "unsubscribe", "useAtomValue", "Atom", "useCallback", "useMemo", "Obj", "Ref", "useObject", "objOrRef", "property", "isRef", "liveObj", "target", "callback", "updateOrValue", "obj", "undefined", "update", "returnValue", "Error", "useRefLoad", "useObjectProperty", "useObjectValue", "atom", "make", "atomProperty", "useObjects", "refs", "get", "results", "ref", "value", "push"]
|
|
7
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"inputs":{"src/useQuery.ts":{"bytes":5393,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@dxos/echo","kind":"import-statement","external":true}],"format":"esm"},"src/useType.ts":{"bytes":8807,"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@dxos/echo","kind":"import-statement","external":true}],"format":"esm"},"src/useObject.ts":{"bytes":21031,"imports":[{"path":"@effect-atom/atom-react","kind":"import-statement","external":true},{"path":"@effect-atom/atom/Atom","kind":"import-statement","external":true},{"path":"react","kind":"import-statement","external":true},{"path":"@dxos/echo","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":552,"imports":[{"path":"src/useQuery.ts","kind":"import-statement","original":"./useQuery"},{"path":"src/useType.ts","kind":"import-statement","original":"./useType"},{"path":"src/useObject.ts","kind":"import-statement","original":"./useObject"}],"format":"esm"}},"outputs":{"dist/lib/neutral/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":19633},"dist/lib/neutral/index.mjs":{"imports":[{"path":"react","kind":"import-statement","external":true},{"path":"@dxos/echo","kind":"import-statement","external":true},{"path":"react","kind":"import-statement","external":true},{"path":"@dxos/echo","kind":"import-statement","external":true},{"path":"@effect-atom/atom-react","kind":"import-statement","external":true},{"path":"@effect-atom/atom/Atom","kind":"import-statement","external":true},{"path":"react","kind":"import-statement","external":true},{"path":"@dxos/echo","kind":"import-statement","external":true}],"exports":["useObject","useObjects","useQuery","useType"],"entryPoint":"src/index.ts","inputs":{"src/useQuery.ts":{"bytesInOutput":921},"src/index.ts":{"bytesInOutput":0},"src/useType.ts":{"bytesInOutput":1686},"src/useObject.ts":{"bytesInOutput":2437}},"bytes":5199}}}
|