@tanstack/preact-db 0.0.0-stage → 1.0.0-rc.1
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 +21 -0
- package/README.md +58 -2
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/useLiveInfiniteQuery.d.ts +42 -0
- package/dist/esm/useLiveInfiniteQuery.js +76 -0
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -0
- package/dist/esm/useLiveQuery.d.ts +93 -0
- package/dist/esm/useLiveQuery.js +44 -0
- package/dist/esm/useLiveQuery.js.map +1 -0
- package/package.json +51 -3
- package/skills/preact-db/SKILL.md +153 -0
- package/src/index.ts +22 -0
- package/src/useLiveInfiniteQuery.ts +183 -0
- package/src/useLiveQuery.ts +287 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kyle Mathews
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tanstack/preact-db
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Preact hooks for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @tanstack/preact-db
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@tanstack/preact-db` supports Preact 10 and later and ships as ES modules.
|
|
12
|
+
|
|
13
|
+
Queries read concrete source Collections. Preact has no `DbProvider` yet, so it does not bind collection descriptors or read from a `DbClient`; a query over a descriptor throws when it is consumed. Use `@tanstack/react-db` or `@tanstack/svelte-db` for `DbClient`-based SSR and hydration.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { eq, useLiveQuery } from '@tanstack/preact-db'
|
|
19
|
+
|
|
20
|
+
function TodoList({ userId }: { userId?: string }) {
|
|
21
|
+
const { data, isLoading } = useLiveQuery((q) =>
|
|
22
|
+
userId
|
|
23
|
+
? q
|
|
24
|
+
.from({ todos: todosCollection })
|
|
25
|
+
.where(({ todos }) => eq(todos.userId, userId))
|
|
26
|
+
: undefined,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if (isLoading) return <div>Loading...</div>
|
|
30
|
+
return (
|
|
31
|
+
<ul>
|
|
32
|
+
{data?.map((todo) => (
|
|
33
|
+
<li key={todo.id}>{todo.text}</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`useLiveQuery` accepts a query callback, a `{ query }` config object, or an
|
|
41
|
+
existing live-query Collection. Return `undefined` or `null` from the callback
|
|
42
|
+
to disable the query. `useLiveInfiniteQuery` pages an ordered live query.
|
|
43
|
+
|
|
44
|
+
The hooks derive each query's identity from its structured IR, so values the
|
|
45
|
+
callback captures recompile the query when they change. A query that uses
|
|
46
|
+
functional variants such as `fn.where` has no stable IR hash and throws
|
|
47
|
+
`UnhashableQueryIRError` unless a config object names it with `queryKey`:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const { data } = useLiveQuery({
|
|
51
|
+
queryKey: [`todos-by-text`, search],
|
|
52
|
+
query: (q) =>
|
|
53
|
+
q
|
|
54
|
+
.from({ todos: todosCollection })
|
|
55
|
+
.fn.where(({ todos }) => todos.text.includes(search)),
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See the [Preact adapter documentation](https://tanstack.com/db/latest/docs/framework/preact/overview) for more.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useLiveQuery } from './useLiveQuery.js';
|
|
2
|
+
export type { ConditionalUseLiveQueryConfig, UseConditionalLiveQueryResult, UseLiveQueryConfig, UseLiveQueryResult, UseLiveQueryStatus, } from './useLiveQuery.js';
|
|
3
|
+
export { useLiveInfiniteQuery } from './useLiveInfiniteQuery.js';
|
|
4
|
+
export type { UseLiveInfiniteQueryConfig, UseLiveInfiniteQueryReturn, UseLiveInfiniteQueryReturnWithCollection, } from './useLiveInfiniteQuery.js';
|
|
5
|
+
export * from '@tanstack/db';
|
|
6
|
+
export type { Collection } from '@tanstack/db';
|
|
7
|
+
export { createTransaction } from '@tanstack/db';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useLiveQuery } from "./useLiveQuery.js";
|
|
2
|
+
import { useLiveInfiniteQuery } from "./useLiveInfiniteQuery.js";
|
|
3
|
+
export * from "@tanstack/db";
|
|
4
|
+
import { createTransaction } from "@tanstack/db";
|
|
5
|
+
export {
|
|
6
|
+
createTransaction,
|
|
7
|
+
useLiveInfiniteQuery,
|
|
8
|
+
useLiveQuery
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Collection, Context, InferResultType, InitialQueryBuilder, LiveQueryKey, NonSingleResult, QueryBuilder } from '@tanstack/db';
|
|
2
|
+
import { UseLiveQueryResult } from './useLiveQuery.js';
|
|
3
|
+
export type UseLiveInfiniteQueryConfig<_TContext extends Context> = {
|
|
4
|
+
/**
|
|
5
|
+
* Explicit identity for a query whose IR cannot derive one, such as a query
|
|
6
|
+
* with functional variants. Structured queries should omit it.
|
|
7
|
+
*/
|
|
8
|
+
queryKey?: LiveQueryKey;
|
|
9
|
+
pageSize?: number;
|
|
10
|
+
/** First result-page label, not a server cursor or remote offset. */
|
|
11
|
+
initialPageParam?: number;
|
|
12
|
+
};
|
|
13
|
+
interface InfiniteQueryPagination<TRow> {
|
|
14
|
+
pages: Array<Array<TRow>>;
|
|
15
|
+
pageParams: Array<number>;
|
|
16
|
+
fetchNextPage: () => Promise<void>;
|
|
17
|
+
hasNextPage: boolean;
|
|
18
|
+
isFetchingNextPage: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type UseLiveInfiniteQueryReturn<TContext extends Context> = UseLiveQueryResult<InferResultType<TContext>[number], string | number, InferResultType<TContext>, Collection<InferResultType<TContext>[number], string | number, {}>> & InfiniteQueryPagination<InferResultType<TContext>[number]>;
|
|
21
|
+
export type UseLiveInfiniteQueryReturnWithCollection<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>> = UseLiveQueryResult<TResult, TKey, Array<TResult>, Collection<TResult, TKey, TUtils> & NonSingleResult> & InfiniteQueryPagination<TResult>;
|
|
22
|
+
/** What one hook instance renders until its input or page shape changes. */
|
|
23
|
+
/**
|
|
24
|
+
* Page through an ordered live query, growing one live-query Collection's
|
|
25
|
+
* window with `utils.setWindow()` instead of recreating it per page.
|
|
26
|
+
*
|
|
27
|
+
* The query must include `.orderBy()`. Its identity follows `useLiveQuery`:
|
|
28
|
+
* derived from the query IR, or named by `queryKey`. A query whose IR cannot be
|
|
29
|
+
* hashed throws `UnhashableQueryIRError` unless `queryKey` is supplied.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const { pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
|
|
33
|
+
* (q) =>
|
|
34
|
+
* q
|
|
35
|
+
* .from({ posts: postsCollection })
|
|
36
|
+
* .orderBy(({ posts }) => posts.createdAt, `desc`),
|
|
37
|
+
* { pageSize: 20 },
|
|
38
|
+
* )
|
|
39
|
+
*/
|
|
40
|
+
export declare function useLiveInfiniteQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult, config: UseLiveInfiniteQueryConfig<any>): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>;
|
|
41
|
+
export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>): UseLiveInfiniteQueryReturn<TContext>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useRef, useState, useLayoutEffect, useCallback } from "preact/hooks";
|
|
2
|
+
import { readLiveQueryWindowPageShape, getLiveQueryKey, getLiveQueryHash, prepareLiveQueryValue, identifyLiveQuery, createLiveQuerySourceIdClaims, bindLiveQueryWindow, fetchNextLiveQueryWindowPage } from "@tanstack/db";
|
|
3
|
+
function useLiveInfiniteQuery(queryFnOrCollection, config) {
|
|
4
|
+
const pageShape = readLiveQueryWindowPageShape(config);
|
|
5
|
+
const claimsRef = useRef(null);
|
|
6
|
+
const bindingRef = useRef(null);
|
|
7
|
+
let preparedValue;
|
|
8
|
+
let identity;
|
|
9
|
+
if (typeof queryFnOrCollection === `function`) {
|
|
10
|
+
const queryKey = getLiveQueryKey(config);
|
|
11
|
+
if (queryKey) {
|
|
12
|
+
identity = getLiveQueryHash(void 0, queryKey);
|
|
13
|
+
} else {
|
|
14
|
+
preparedValue = prepareLiveQueryValue(queryFnOrCollection, void 0);
|
|
15
|
+
identity = identifyLiveQuery(
|
|
16
|
+
preparedValue,
|
|
17
|
+
{
|
|
18
|
+
claims: claimsRef.current ??= createLiveQuerySourceIdClaims(),
|
|
19
|
+
client: void 0,
|
|
20
|
+
bindingName: `useLiveInfiniteQuery`
|
|
21
|
+
},
|
|
22
|
+
getLiveQueryHash
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const binding = bindingRef.current = bindLiveQueryWindow(
|
|
27
|
+
bindingRef.current,
|
|
28
|
+
{
|
|
29
|
+
...pageShape,
|
|
30
|
+
// Compile what this render prepared, so the callback runs once.
|
|
31
|
+
input: preparedValue === void 0 ? queryFnOrCollection : () => preparedValue,
|
|
32
|
+
dependencies: identity === void 0 ? void 0 : [identity],
|
|
33
|
+
recompile: `identity`
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
const { controller } = binding;
|
|
37
|
+
const snapshot = controller.getSnapshot();
|
|
38
|
+
const [, setRevision] = useState(0);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const rerender = () => setRevision((revision) => revision + 1);
|
|
41
|
+
controller.subscribe(rerender);
|
|
42
|
+
if (controller.getSnapshot() !== snapshot) rerender();
|
|
43
|
+
if (binding.warning) console.warn(binding.warning);
|
|
44
|
+
return () => controller.dispose();
|
|
45
|
+
}, [controller]);
|
|
46
|
+
const fetchNextPage = useCallback(
|
|
47
|
+
() => fetchNextLiveQueryWindowPage(controller),
|
|
48
|
+
[controller]
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
data: snapshot.data,
|
|
52
|
+
get state() {
|
|
53
|
+
return snapshot.state;
|
|
54
|
+
},
|
|
55
|
+
status: snapshot.status,
|
|
56
|
+
isLoading: snapshot.isLoading,
|
|
57
|
+
isReady: snapshot.isReady,
|
|
58
|
+
isIdle: snapshot.isIdle,
|
|
59
|
+
isLoadingError: snapshot.isLoadingError,
|
|
60
|
+
isCrashed: snapshot.isCrashed,
|
|
61
|
+
isCleanedUp: snapshot.isCleanedUp,
|
|
62
|
+
isSyncError: snapshot.isSyncError,
|
|
63
|
+
collection: snapshot.collection,
|
|
64
|
+
isEnabled: true,
|
|
65
|
+
pages: snapshot.pages,
|
|
66
|
+
pageParams: snapshot.pageParams,
|
|
67
|
+
fetchNextPage,
|
|
68
|
+
hasNextPage: snapshot.hasNextPage,
|
|
69
|
+
isFetchingNextPage: snapshot.isFetchingNextPage,
|
|
70
|
+
health: snapshot.health
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
useLiveInfiniteQuery
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=useLiveInfiniteQuery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from 'preact/hooks'\nimport {\n bindLiveQueryWindow,\n createLiveQuerySourceIdClaims,\n fetchNextLiveQueryWindowPage,\n getLiveQueryHash,\n getLiveQueryKey,\n identifyLiveQuery,\n prepareLiveQueryValue,\n readLiveQueryWindowPageShape,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryKey,\n LiveQuerySourceIdClaims,\n LiveQueryWindowBinding,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\nimport type { UseLiveQueryResult } from './useLiveQuery'\n\n// Keep the generic parameter for typed config wrappers.\nexport type UseLiveInfiniteQueryConfig<_TContext extends Context> = {\n /**\n * Explicit identity for a query whose IR cannot derive one, such as a query\n * with functional variants. Structured queries should omit it.\n */\n queryKey?: LiveQueryKey\n pageSize?: number\n /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\n}\n\ninterface InfiniteQueryPagination<TRow> {\n pages: Array<Array<TRow>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> =\n UseLiveQueryResult<\n InferResultType<TContext>[number],\n string | number,\n InferResultType<TContext>,\n Collection<InferResultType<TContext>[number], string | number, {}>\n > &\n InfiniteQueryPagination<InferResultType<TContext>[number]>\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = UseLiveQueryResult<\n TResult,\n TKey,\n Array<TResult>,\n Collection<TResult, TKey, TUtils> & NonSingleResult\n> &\n InfiniteQueryPagination<TResult>\n\n/** What one hook instance renders until its input or page shape changes. */\n/**\n * Page through an ordered live query, growing one live-query Collection's\n * window with `utils.setWindow()` instead of recreating it per page.\n *\n * The query must include `.orderBy()`. Its identity follows `useLiveQuery`:\n * derived from the query IR, or named by `queryKey`. A query whose IR cannot be\n * hashed throws `UnhashableQueryIRError` unless `queryKey` is supplied.\n *\n * @example\n * const { pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) =>\n * q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, `desc`),\n * { pageSize: 20 },\n * )\n */\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\n\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n): UseLiveInfiniteQueryReturn<TContext>\n\nexport function useLiveInfiniteQuery(\n queryFnOrCollection: unknown,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any> {\n const pageShape = readLiveQueryWindowPageShape(config)\n const claimsRef = useRef<LiveQuerySourceIdClaims | null>(null)\n const bindingRef = useRef<LiveQueryWindowBinding | null>(null)\n\n // A query input's identity is a hash, so a structurally equal query keeps\n // its loaded pages and a changed one compiles again.\n let preparedValue: unknown\n let identity: string | undefined\n if (typeof queryFnOrCollection === `function`) {\n const queryKey = getLiveQueryKey(config)\n if (queryKey) {\n identity = getLiveQueryHash(undefined, queryKey)\n } else {\n preparedValue = prepareLiveQueryValue(queryFnOrCollection, undefined)\n identity = identifyLiveQuery(\n preparedValue,\n {\n claims: (claimsRef.current ??= createLiveQuerySourceIdClaims()),\n client: undefined,\n bindingName: `useLiveInfiniteQuery`,\n },\n getLiveQueryHash,\n )\n }\n }\n\n const binding = (bindingRef.current = bindLiveQueryWindow(\n bindingRef.current,\n {\n ...pageShape,\n // Compile what this render prepared, so the callback runs once.\n input:\n preparedValue === undefined ? queryFnOrCollection : () => preparedValue,\n dependencies: identity === undefined ? undefined : [identity],\n recompile: `identity`,\n },\n ))\n\n const { controller } = binding\n const snapshot = controller.getSnapshot()\n const [, setRevision] = useState(0)\n\n useLayoutEffect(() => {\n const rerender = () => setRevision((revision) => revision + 1)\n controller.subscribe(rerender)\n // The query can publish between this render and its subscription.\n if (controller.getSnapshot() !== snapshot) rerender()\n if (binding.warning) console.warn(binding.warning)\n // Each binding owns its controller, so disposal also detaches this\n // subscription and releases its window lease.\n return () => controller.dispose()\n }, [controller])\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data,\n get state() {\n return snapshot.state\n },\n status: snapshot.status as CollectionStatus,\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n isIdle: snapshot.isIdle,\n isLoadingError: snapshot.isLoadingError,\n isCrashed: snapshot.isCrashed,\n isCleanedUp: snapshot.isCleanedUp,\n isSyncError: snapshot.isSyncError,\n collection: snapshot.collection,\n isEnabled: true,\n pages: snapshot.pages,\n pageParams: snapshot.pageParams,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n health: snapshot.health,\n } as UseLiveInfiniteQueryReturn<any>\n}\n"],"names":[],"mappings":";;AAkGO,SAAS,qBACd,qBACA,QACiC;AACjC,QAAM,YAAY,6BAA6B,MAAM;AACrD,QAAM,YAAY,OAAuC,IAAI;AAC7D,QAAM,aAAa,OAAsC,IAAI;AAI7D,MAAI;AACJ,MAAI;AACJ,MAAI,OAAO,wBAAwB,YAAY;AAC7C,UAAM,WAAW,gBAAgB,MAAM;AACvC,QAAI,UAAU;AACZ,iBAAW,iBAAiB,QAAW,QAAQ;AAAA,IACjD,OAAO;AACL,sBAAgB,sBAAsB,qBAAqB,MAAS;AACpE,iBAAW;AAAA,QACT;AAAA,QACA;AAAA,UACE,QAAS,UAAU,YAAY,8BAAA;AAAA,UAC/B,QAAQ;AAAA,UACR,aAAa;AAAA,QAAA;AAAA,QAEf;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AAEA,QAAM,UAAW,WAAW,UAAU;AAAA,IACpC,WAAW;AAAA,IACX;AAAA,MACE,GAAG;AAAA;AAAA,MAEH,OACE,kBAAkB,SAAY,sBAAsB,MAAM;AAAA,MAC5D,cAAc,aAAa,SAAY,SAAY,CAAC,QAAQ;AAAA,MAC5D,WAAW;AAAA,IAAA;AAAA,EACb;AAGF,QAAM,EAAE,eAAe;AACvB,QAAM,WAAW,WAAW,YAAA;AAC5B,QAAM,GAAG,WAAW,IAAI,SAAS,CAAC;AAElC,kBAAgB,MAAM;AACpB,UAAM,WAAW,MAAM,YAAY,CAAC,aAAa,WAAW,CAAC;AAC7D,eAAW,UAAU,QAAQ;AAE7B,QAAI,WAAW,kBAAkB,SAAU,UAAA;AAC3C,QAAI,QAAQ,QAAS,SAAQ,KAAK,QAAQ,OAAO;AAGjD,WAAO,MAAM,WAAW,QAAA;AAAA,EAC1B,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,gBAAgB;AAAA,IACpB,MAAM,6BAA6B,UAAU;AAAA,IAC7C,CAAC,UAAU;AAAA,EAAA;AAGb,SAAO;AAAA,IACL,MAAM,SAAS;AAAA,IACf,IAAI,QAAQ;AACV,aAAO,SAAS;AAAA,IAClB;AAAA,IACA,QAAQ,SAAS;AAAA,IACjB,WAAW,SAAS;AAAA,IACpB,SAAS,SAAS;AAAA,IAClB,QAAQ,SAAS;AAAA,IACjB,gBAAgB,SAAS;AAAA,IACzB,WAAW,SAAS;AAAA,IACpB,aAAa,SAAS;AAAA,IACtB,aAAa,SAAS;AAAA,IACtB,YAAY,SAAS;AAAA,IACrB,WAAW;AAAA,IACX,OAAO,SAAS;AAAA,IAChB,YAAY,SAAS;AAAA,IACrB;AAAA,IACA,aAAa,SAAS;AAAA,IACtB,oBAAoB,SAAS;AAAA,IAC7B,QAAQ,SAAS;AAAA,EAAA;AAErB;"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryKey, LiveQueryStatusFlags, NonSingleResult, PooledLiveQueries, QueryBuilder, SingleResult } from '@tanstack/db';
|
|
2
|
+
export type UseLiveQueryStatus = CollectionStatus | `disabled`;
|
|
3
|
+
type ConfiguredQueryBuilder<TContext extends Context> = Extract<LiveQueryCollectionConfig<TContext>[`query`], QueryBuilder<TContext>>;
|
|
4
|
+
type UseLiveQueryConfigOptions<TContext extends Context> = Omit<LiveQueryCollectionConfig<TContext>, `query`> & {
|
|
5
|
+
/**
|
|
6
|
+
* Explicit identity for a query whose IR cannot derive one, such as a query
|
|
7
|
+
* with functional variants. Structured queries should omit it.
|
|
8
|
+
*/
|
|
9
|
+
queryKey?: LiveQueryKey;
|
|
10
|
+
/**
|
|
11
|
+
* Pass `pooledLiveQueries` to share one partition among equality-filtered
|
|
12
|
+
* queries of one shape.
|
|
13
|
+
*/
|
|
14
|
+
pool?: PooledLiveQueries;
|
|
15
|
+
};
|
|
16
|
+
export type UseLiveQueryConfig<TContext extends Context> = UseLiveQueryConfigOptions<TContext> & Pick<LiveQueryCollectionConfig<TContext>, `query`>;
|
|
17
|
+
export type ConditionalUseLiveQueryConfig<TContext extends Context> = UseLiveQueryConfigOptions<TContext> & {
|
|
18
|
+
query: ConfiguredQueryBuilder<TContext> | ((q: InitialQueryBuilder) => ConfiguredQueryBuilder<TContext> | undefined | null);
|
|
19
|
+
};
|
|
20
|
+
/** The result of a live query that is always enabled. */
|
|
21
|
+
export interface UseLiveQueryResult<TResult extends object, TKey extends string | number, TData, TCollection> extends LiveQueryStatusFlags {
|
|
22
|
+
state: ReadonlyMap<TKey, TResult>;
|
|
23
|
+
data: TData;
|
|
24
|
+
collection: TCollection;
|
|
25
|
+
status: CollectionStatus;
|
|
26
|
+
isEnabled: true;
|
|
27
|
+
}
|
|
28
|
+
/** The result of a live query whose callback can disable it. */
|
|
29
|
+
export interface UseConditionalLiveQueryResult<TResult extends object, TKey extends string | number, TData, TCollection> extends LiveQueryStatusFlags {
|
|
30
|
+
state: ReadonlyMap<TKey, TResult> | undefined;
|
|
31
|
+
data: TData | undefined;
|
|
32
|
+
collection: TCollection | undefined;
|
|
33
|
+
status: UseLiveQueryStatus;
|
|
34
|
+
isEnabled: boolean;
|
|
35
|
+
}
|
|
36
|
+
type QueryResult<TContext extends Context> = UseLiveQueryResult<GetResult<TContext>, string | number, InferResultType<TContext>, Collection<GetResult<TContext>, string | number, {}>>;
|
|
37
|
+
type ConditionalQueryResult<TContext extends Context> = UseConditionalLiveQueryResult<GetResult<TContext>, string | number, InferResultType<TContext>, Collection<GetResult<TContext>, string | number, {}>>;
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe a Preact component to a live query.
|
|
40
|
+
*
|
|
41
|
+
* The hook derives the query's identity from its structured IR on each render,
|
|
42
|
+
* so values captured by the callback recompile the query when they change. A
|
|
43
|
+
* query whose IR cannot be hashed, such as one using `fn.where`, throws
|
|
44
|
+
* `UnhashableQueryIRError` unless its config supplies `queryKey`. While the
|
|
45
|
+
* hook is mounted, each Collection ID its queries read must keep naming one
|
|
46
|
+
* source object, or it throws `SourceCollectionIdReuseError`.
|
|
47
|
+
* Return `undefined` or `null` from the callback to disable the query.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const { data, isLoading } = useLiveQuery((q) =>
|
|
51
|
+
* q
|
|
52
|
+
* .from({ todos: todosCollection })
|
|
53
|
+
* .where(({ todos }) => eq(todos.completed, false)),
|
|
54
|
+
* )
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Disable the query until its input exists
|
|
58
|
+
* const { data, isEnabled } = useLiveQuery((q) =>
|
|
59
|
+
* userId
|
|
60
|
+
* ? q
|
|
61
|
+
* .from({ todos: todosCollection })
|
|
62
|
+
* .where(({ todos }) => eq(todos.userId, userId))
|
|
63
|
+
* : undefined,
|
|
64
|
+
* )
|
|
65
|
+
*/
|
|
66
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>): QueryResult<TContext>;
|
|
67
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): ConditionalQueryResult<TContext>;
|
|
68
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null): ConditionalQueryResult<TContext>;
|
|
69
|
+
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null): UseConditionalLiveQueryResult<TResult, TKey, Array<TResult>, Collection<TResult, TKey, TUtils>>;
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to a live query described by a config object.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const { data } = useLiveQuery({
|
|
75
|
+
* query: (q) => q.from({ todos: todosCollection }),
|
|
76
|
+
* gcTime: 60_000,
|
|
77
|
+
* })
|
|
78
|
+
*/
|
|
79
|
+
export declare function useLiveQuery<TContext extends Context>(config: UseLiveQueryConfig<TContext>): QueryResult<TContext>;
|
|
80
|
+
export declare function useLiveQuery<TContext extends Context>(config: ConditionalUseLiveQueryConfig<TContext>): ConditionalQueryResult<TContext>;
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to an existing live-query Collection. The hook starts its sync and
|
|
83
|
+
* leaves its retention policy to the caller.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* const activeTodos = createLiveQueryCollection((q) =>
|
|
87
|
+
* q.from({ todos: todosCollection }).where(({ todos }) => todos.active),
|
|
88
|
+
* )
|
|
89
|
+
* const { data, collection } = useLiveQuery(activeTodos)
|
|
90
|
+
*/
|
|
91
|
+
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult): UseLiveQueryResult<TResult, TKey, Array<TResult>, Collection<TResult, TKey, TUtils>>;
|
|
92
|
+
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult): UseLiveQueryResult<TResult, TKey, TResult | undefined, Collection<TResult, TKey, TUtils> & SingleResult>;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useRef, useState, useLayoutEffect } from "preact/hooks";
|
|
2
|
+
import { prepareLiveQueryValue, getLiveQueryKey, isCollection, getLiveQueryHash, identifyLiveQuery, createLiveQuerySourceIdClaims, createLiveQueryObserver, resolveLiveQueryValue } from "@tanstack/db";
|
|
3
|
+
const DEFAULT_GC_TIME_MS = 1;
|
|
4
|
+
function useLiveQuery(input) {
|
|
5
|
+
const claimsRef = useRef(null);
|
|
6
|
+
const preparedValue = prepareLiveQueryValue(input, void 0);
|
|
7
|
+
const queryKey = getLiveQueryKey(input);
|
|
8
|
+
const identity = isCollection(input) ? input : queryKey ? getLiveQueryHash(preparedValue, queryKey) : identifyLiveQuery(
|
|
9
|
+
preparedValue,
|
|
10
|
+
{
|
|
11
|
+
claims: claimsRef.current ??= createLiveQuerySourceIdClaims(),
|
|
12
|
+
client: void 0,
|
|
13
|
+
bindingName: `useLiveQuery`
|
|
14
|
+
},
|
|
15
|
+
getLiveQueryHash
|
|
16
|
+
);
|
|
17
|
+
const bindingRef = useRef(null);
|
|
18
|
+
let binding = bindingRef.current;
|
|
19
|
+
if (binding?.identity !== identity) {
|
|
20
|
+
binding = bindingRef.current = {
|
|
21
|
+
identity,
|
|
22
|
+
// Wholesale mode publishes nothing during subscribe; the hook re-reads
|
|
23
|
+
// the observer's snapshot on each notification.
|
|
24
|
+
observer: createLiveQueryObserver(
|
|
25
|
+
resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS }),
|
|
26
|
+
{ mode: `wholesale` }
|
|
27
|
+
)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const { observer } = binding;
|
|
31
|
+
const snapshot = observer.getSnapshot();
|
|
32
|
+
const [, setRevision] = useState(0);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
const rerender = () => setRevision((revision) => revision + 1);
|
|
35
|
+
observer.subscribe(rerender);
|
|
36
|
+
if (observer.getSnapshot() !== snapshot) rerender();
|
|
37
|
+
return () => observer.dispose();
|
|
38
|
+
}, [observer]);
|
|
39
|
+
return snapshot;
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
useLiveQuery
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=useLiveQuery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from 'preact/hooks'\nimport {\n createLiveQueryObserver,\n createLiveQuerySourceIdClaims,\n getLiveQueryHash,\n getLiveQueryKey,\n identifyLiveQuery,\n isCollection,\n prepareLiveQueryValue,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryKey,\n LiveQueryObserver,\n LiveQuerySourceIdClaims,\n LiveQueryStatusFlags,\n NonSingleResult,\n PooledLiveQueries,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\n// A live query this hook creates from a callback or config retires 1 ms after\n// its last subscriber leaves. A supplied Collection, or a config with an\n// explicit `gcTime`, keeps the caller's retention policy.\nconst DEFAULT_GC_TIME_MS = 1\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for a query whose IR cannot derive one, such as a query\n * with functional variants. Structured queries should omit it.\n */\n queryKey?: LiveQueryKey\n /**\n * Pass `pooledLiveQueries` to share one partition among equality-filtered\n * queries of one shape.\n */\n pool?: PooledLiveQueries\n}\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\n/** The result of a live query that is always enabled. */\nexport interface UseLiveQueryResult<\n TResult extends object,\n TKey extends string | number,\n TData,\n TCollection,\n> extends LiveQueryStatusFlags {\n state: ReadonlyMap<TKey, TResult>\n data: TData\n collection: TCollection\n status: CollectionStatus\n isEnabled: true\n}\n\n/** The result of a live query whose callback can disable it. */\nexport interface UseConditionalLiveQueryResult<\n TResult extends object,\n TKey extends string | number,\n TData,\n TCollection,\n> extends LiveQueryStatusFlags {\n state: ReadonlyMap<TKey, TResult> | undefined\n data: TData | undefined\n collection: TCollection | undefined\n status: UseLiveQueryStatus\n isEnabled: boolean\n}\n\ntype QueryResult<TContext extends Context> = UseLiveQueryResult<\n GetResult<TContext>,\n string | number,\n InferResultType<TContext>,\n Collection<GetResult<TContext>, string | number, {}>\n>\n\ntype ConditionalQueryResult<TContext extends Context> =\n UseConditionalLiveQueryResult<\n GetResult<TContext>,\n string | number,\n InferResultType<TContext>,\n Collection<GetResult<TContext>, string | number, {}>\n >\n\n/**\n * Subscribe a Preact component to a live query.\n *\n * The hook derives the query's identity from its structured IR on each render,\n * so values captured by the callback recompile the query when they change. A\n * query whose IR cannot be hashed, such as one using `fn.where`, throws\n * `UnhashableQueryIRError` unless its config supplies `queryKey`. While the\n * hook is mounted, each Collection ID its queries read must keep naming one\n * source object, or it throws `SourceCollectionIdReuseError`.\n * Return `undefined` or `null` from the callback to disable the query.\n *\n * @example\n * const { data, isLoading } = useLiveQuery((q) =>\n * q\n * .from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false)),\n * )\n *\n * @example\n * // Disable the query until its input exists\n * const { data, isEnabled } = useLiveQuery((q) =>\n * userId\n * ? q\n * .from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * : undefined,\n * )\n */\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): QueryResult<TContext>\n\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): ConditionalQueryResult<TContext>\n\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n): ConditionalQueryResult<TContext>\n\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n): UseConditionalLiveQueryResult<\n TResult,\n TKey,\n Array<TResult>,\n Collection<TResult, TKey, TUtils>\n>\n\n/**\n * Subscribe to a live query described by a config object.\n *\n * @example\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60_000,\n * })\n */\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): QueryResult<TContext>\n\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): ConditionalQueryResult<TContext>\n\n/**\n * Subscribe to an existing live-query Collection. The hook starts its sync and\n * leaves its retention policy to the caller.\n *\n * @example\n * const activeTodos = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => todos.active),\n * )\n * const { data, collection } = useLiveQuery(activeTodos)\n */\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): UseLiveQueryResult<\n TResult,\n TKey,\n Array<TResult>,\n Collection<TResult, TKey, TUtils>\n>\n\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): UseLiveQueryResult<\n TResult,\n TKey,\n TResult | undefined,\n Collection<TResult, TKey, TUtils> & SingleResult\n>\n\nexport function useLiveQuery(\n input: unknown,\n): UseConditionalLiveQueryResult<\n object,\n string | number,\n unknown,\n Collection<object, string | number, any>\n> {\n const claimsRef = useRef<LiveQuerySourceIdClaims | null>(null)\n const preparedValue = prepareLiveQueryValue(input, undefined)\n const queryKey = getLiveQueryKey(input)\n // A supplied Collection is not compiled, so its object is its identity.\n const identity = isCollection(input)\n ? input\n : queryKey\n ? getLiveQueryHash(preparedValue, queryKey)\n : identifyLiveQuery(\n preparedValue,\n {\n claims: (claimsRef.current ??= createLiveQuerySourceIdClaims()),\n client: undefined,\n bindingName: `useLiveQuery`,\n },\n getLiveQueryHash,\n )\n\n const bindingRef = useRef<LiveQueryBinding | null>(null)\n let binding = bindingRef.current\n if (binding?.identity !== identity) {\n binding = bindingRef.current = {\n identity,\n // Wholesale mode publishes nothing during subscribe; the hook re-reads\n // the observer's snapshot on each notification.\n observer: createLiveQueryObserver(\n resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS }),\n { mode: `wholesale` },\n ),\n }\n }\n\n const { observer } = binding\n const snapshot = observer.getSnapshot()\n const [, setRevision] = useState(0)\n\n useLayoutEffect(() => {\n const rerender = () => setRevision((revision) => revision + 1)\n observer.subscribe(rerender)\n // The query can publish between this render and its subscription.\n if (observer.getSnapshot() !== snapshot) rerender()\n // Each binding owns its observer, so disposal also detaches this\n // subscription and releases the live-query Collection.\n return () => observer.dispose()\n }, [observer])\n\n return snapshot\n}\n\n/** The live query one hook instance reads until its identity changes. */\ninterface LiveQueryBinding {\n identity: unknown\n observer: LiveQueryObserver<object, string | number>\n}\n"],"names":[],"mappings":";;AAgCA,MAAM,qBAAqB;AAiMpB,SAAS,aACd,OAMA;AACA,QAAM,YAAY,OAAuC,IAAI;AAC7D,QAAM,gBAAgB,sBAAsB,OAAO,MAAS;AAC5D,QAAM,WAAW,gBAAgB,KAAK;AAEtC,QAAM,WAAW,aAAa,KAAK,IAC/B,QACA,WACE,iBAAiB,eAAe,QAAQ,IACxC;AAAA,IACE;AAAA,IACA;AAAA,MACE,QAAS,UAAU,YAAY,8BAAA;AAAA,MAC/B,QAAQ;AAAA,MACR,aAAa;AAAA,IAAA;AAAA,IAEf;AAAA,EAAA;AAGR,QAAM,aAAa,OAAgC,IAAI;AACvD,MAAI,UAAU,WAAW;AACzB,MAAI,SAAS,aAAa,UAAU;AAClC,cAAU,WAAW,UAAU;AAAA,MAC7B;AAAA;AAAA;AAAA,MAGA,UAAU;AAAA,QACR,sBAAsB,eAAe,EAAE,QAAQ,oBAAoB;AAAA,QACnE,EAAE,MAAM,YAAA;AAAA,MAAY;AAAA,IACtB;AAAA,EAEJ;AAEA,QAAM,EAAE,aAAa;AACrB,QAAM,WAAW,SAAS,YAAA;AAC1B,QAAM,GAAG,WAAW,IAAI,SAAS,CAAC;AAElC,kBAAgB,MAAM;AACpB,UAAM,WAAW,MAAM,YAAY,CAAC,aAAa,WAAW,CAAC;AAC7D,aAAS,UAAU,QAAQ;AAE3B,QAAI,SAAS,kBAAkB,SAAU,UAAA;AAGzC,WAAO,MAAM,SAAS,QAAA;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/preact-db",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"description": "Preact integration for @tanstack/db",
|
|
5
|
+
"author": "Kyle Mathews",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/db.git",
|
|
10
|
+
"directory": "packages/preact-db"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/db",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"optimistic",
|
|
15
|
+
"preact",
|
|
16
|
+
"typescript",
|
|
17
|
+
"tanstack-intent"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "dist/esm/index.js",
|
|
21
|
+
"module": "dist/esm/index.js",
|
|
22
|
+
"types": "dist/esm/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/esm/index.d.ts",
|
|
27
|
+
"default": "./dist/esm/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"src",
|
|
36
|
+
"skills"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@tanstack/db": "1.0.0-rc.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"preact": ">=10.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@vitest/coverage-istanbul": "^4.1.12",
|
|
46
|
+
"preact": "^11.0.1"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "vite build",
|
|
50
|
+
"dev": "vite build --watch",
|
|
51
|
+
"test": "vitest --run",
|
|
52
|
+
"lint": "eslint . --fix"
|
|
53
|
+
}
|
|
6
54
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: preact-db
|
|
3
|
+
description: >
|
|
4
|
+
Preact bindings for TanStack DB. useLiveQuery hook with query callbacks,
|
|
5
|
+
{ query, queryKey } config objects, or pre-created collections; identity is
|
|
6
|
+
derived from query IR, with no dependency arrays. Conditional queries via
|
|
7
|
+
returning undefined/null. useLiveInfiniteQuery for paging an ordered live
|
|
8
|
+
query. Functional variants (fn.where) require queryKey. Import from
|
|
9
|
+
@tanstack/preact-db (re-exports all of @tanstack/db).
|
|
10
|
+
type: framework
|
|
11
|
+
library: db
|
|
12
|
+
framework: preact
|
|
13
|
+
library_version: '0.6.17'
|
|
14
|
+
requires:
|
|
15
|
+
- db-core
|
|
16
|
+
sources:
|
|
17
|
+
- 'TanStack/db:docs/framework/preact/overview.md'
|
|
18
|
+
- 'TanStack/db:packages/preact-db/src/useLiveQuery.ts'
|
|
19
|
+
- 'TanStack/db:packages/preact-db/src/useLiveInfiniteQuery.ts'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.
|
|
23
|
+
|
|
24
|
+
Queries read concrete source Collections. Preact has no `DbProvider` yet, so it does not bind collection descriptors or read from a `DbClient`; a query over a descriptor throws when it is consumed. Use `@tanstack/react-db` or `@tanstack/svelte-db` for `DbClient`-based SSR and hydration.
|
|
25
|
+
|
|
26
|
+
# TanStack DB — Preact
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { eq, not, useLiveQuery } from '@tanstack/preact-db'
|
|
32
|
+
|
|
33
|
+
function TodoList() {
|
|
34
|
+
const { data, isLoading } = useLiveQuery((q) =>
|
|
35
|
+
q
|
|
36
|
+
.from({ todo: todoCollection })
|
|
37
|
+
.where(({ todo }) => not(todo.completed))
|
|
38
|
+
.orderBy(({ todo }) => todo.created_at, 'asc'),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if (isLoading) return <div>Loading...</div>
|
|
42
|
+
return (
|
|
43
|
+
<ul>
|
|
44
|
+
{data.map((todo) => (
|
|
45
|
+
<li key={todo.id}>{todo.text}</li>
|
|
46
|
+
))}
|
|
47
|
+
</ul>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`@tanstack/preact-db` re-exports everything from `@tanstack/db`. It supports Preact 10+ and ships ES modules only.
|
|
53
|
+
|
|
54
|
+
## Hooks
|
|
55
|
+
|
|
56
|
+
### useLiveQuery
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Query callback
|
|
60
|
+
const { data, state, status, isReady, isEnabled } = useLiveQuery((q) =>
|
|
61
|
+
q.from({ todo: todoCollection }),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Config object (required for queryKey)
|
|
65
|
+
const { data } = useLiveQuery({
|
|
66
|
+
query: (q) => q.from({ todo: todoCollection }),
|
|
67
|
+
gcTime: 60_000,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Pre-created live query collection
|
|
71
|
+
const { data } = useLiveQuery(activeTodosCollection)
|
|
72
|
+
|
|
73
|
+
// Disabled until an input exists: data and state are undefined
|
|
74
|
+
const { data, isEnabled } = useLiveQuery((q) =>
|
|
75
|
+
userId
|
|
76
|
+
? q
|
|
77
|
+
.from({ todo: todoCollection })
|
|
78
|
+
.where(({ todo }) => eq(todo.userId, userId))
|
|
79
|
+
: undefined,
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`isLoadingError` means the first load failed and there is nothing to show; offer `collection.reload()`. `isCrashed` means local storage broke; only `cleanup({ dependents: true })` and a fresh start recover. After the query is ready, sync problems never change `status`: read `isSyncError` or the `health` tree to show stale-data warnings. There is no `isError`.
|
|
84
|
+
|
|
85
|
+
### useLiveInfiniteQuery
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const { data, pages, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
|
89
|
+
useLiveInfiniteQuery(
|
|
90
|
+
(q) =>
|
|
91
|
+
q
|
|
92
|
+
.from({ post: postsCollection })
|
|
93
|
+
.orderBy(({ post }) => post.createdAt, 'desc'),
|
|
94
|
+
{ pageSize: 20 },
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The query must include `.orderBy()`. `fetchNextPage()` resolves when the page request settles; a failed page load does not reject or change `status`; it appears as a request error in the returned `health` tree. `getNextPageParam` is rejected.
|
|
99
|
+
|
|
100
|
+
## Preact-Specific Patterns
|
|
101
|
+
|
|
102
|
+
### Query identity
|
|
103
|
+
|
|
104
|
+
The hook derives identity from the query IR on every render. Captured values in structured expressions recompile the query when they change, so there is no dependency array:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
function Filtered({ minPriority }: { minPriority: number }) {
|
|
108
|
+
const { data } = useLiveQuery((q) =>
|
|
109
|
+
q
|
|
110
|
+
.from({ todo: todoCollection })
|
|
111
|
+
.where(({ todo }) => gt(todo.priority, minPriority)),
|
|
112
|
+
)
|
|
113
|
+
return <div>{data.length}</div>
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
While a hook is mounted, a source Collection ID must keep naming the same Collection object. A different object under a previously read ID throws `SourceCollectionIdReuseError`.
|
|
118
|
+
|
|
119
|
+
## Common Mistakes
|
|
120
|
+
|
|
121
|
+
### CRITICAL Using opaque query logic without queryKey
|
|
122
|
+
|
|
123
|
+
Wrong:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const { data } = useLiveQuery((q) =>
|
|
127
|
+
q
|
|
128
|
+
.from({ todo: todoCollection })
|
|
129
|
+
.fn.where(({ todo }) => todo.text.includes(search)),
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Correct:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const { data } = useLiveQuery({
|
|
137
|
+
queryKey: [todoCollection.id, 'search', search],
|
|
138
|
+
query: (q) =>
|
|
139
|
+
q
|
|
140
|
+
.from({ todo: todoCollection })
|
|
141
|
+
.fn.where(({ todo }) => todo.text.includes(search)),
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Functional variants (`fn.where`, `fn.select`, `fn.having`) have no stable IR identity, so without `queryKey` the hook throws `UnhashableQueryIRError`. Put every value the function captures into the key.
|
|
146
|
+
|
|
147
|
+
### HIGH Using === instead of eq() in .where()
|
|
148
|
+
|
|
149
|
+
`.where(({ todo }) => todo.completed === false)` compares a query reference, not a value. Use `eq(todo.completed, false)` or `not(todo.completed)`.
|
|
150
|
+
|
|
151
|
+
### HIGH Reading data of a disabled query without narrowing
|
|
152
|
+
|
|
153
|
+
A callback that can return `undefined` makes `data` possibly `undefined`. Check `isEnabled` or use `data?.`.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Re-export all public APIs
|
|
2
|
+
export { useLiveQuery } from './useLiveQuery'
|
|
3
|
+
export type {
|
|
4
|
+
ConditionalUseLiveQueryConfig,
|
|
5
|
+
UseConditionalLiveQueryResult,
|
|
6
|
+
UseLiveQueryConfig,
|
|
7
|
+
UseLiveQueryResult,
|
|
8
|
+
UseLiveQueryStatus,
|
|
9
|
+
} from './useLiveQuery'
|
|
10
|
+
export { useLiveInfiniteQuery } from './useLiveInfiniteQuery'
|
|
11
|
+
export type {
|
|
12
|
+
UseLiveInfiniteQueryConfig,
|
|
13
|
+
UseLiveInfiniteQueryReturn,
|
|
14
|
+
UseLiveInfiniteQueryReturnWithCollection,
|
|
15
|
+
} from './useLiveInfiniteQuery'
|
|
16
|
+
|
|
17
|
+
// Re-export everything from @tanstack/db
|
|
18
|
+
export * from '@tanstack/db'
|
|
19
|
+
|
|
20
|
+
// Re-export some stuff explicitly to ensure the type & value is exported
|
|
21
|
+
export type { Collection } from '@tanstack/db'
|
|
22
|
+
export { createTransaction } from '@tanstack/db'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import {
|
|
3
|
+
bindLiveQueryWindow,
|
|
4
|
+
createLiveQuerySourceIdClaims,
|
|
5
|
+
fetchNextLiveQueryWindowPage,
|
|
6
|
+
getLiveQueryHash,
|
|
7
|
+
getLiveQueryKey,
|
|
8
|
+
identifyLiveQuery,
|
|
9
|
+
prepareLiveQueryValue,
|
|
10
|
+
readLiveQueryWindowPageShape,
|
|
11
|
+
} from '@tanstack/db'
|
|
12
|
+
import type {
|
|
13
|
+
Collection,
|
|
14
|
+
CollectionStatus,
|
|
15
|
+
Context,
|
|
16
|
+
InferResultType,
|
|
17
|
+
InitialQueryBuilder,
|
|
18
|
+
LiveQueryKey,
|
|
19
|
+
LiveQuerySourceIdClaims,
|
|
20
|
+
LiveQueryWindowBinding,
|
|
21
|
+
NonSingleResult,
|
|
22
|
+
QueryBuilder,
|
|
23
|
+
} from '@tanstack/db'
|
|
24
|
+
import type { UseLiveQueryResult } from './useLiveQuery'
|
|
25
|
+
|
|
26
|
+
// Keep the generic parameter for typed config wrappers.
|
|
27
|
+
export type UseLiveInfiniteQueryConfig<_TContext extends Context> = {
|
|
28
|
+
/**
|
|
29
|
+
* Explicit identity for a query whose IR cannot derive one, such as a query
|
|
30
|
+
* with functional variants. Structured queries should omit it.
|
|
31
|
+
*/
|
|
32
|
+
queryKey?: LiveQueryKey
|
|
33
|
+
pageSize?: number
|
|
34
|
+
/** First result-page label, not a server cursor or remote offset. */
|
|
35
|
+
initialPageParam?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface InfiniteQueryPagination<TRow> {
|
|
39
|
+
pages: Array<Array<TRow>>
|
|
40
|
+
pageParams: Array<number>
|
|
41
|
+
fetchNextPage: () => Promise<void>
|
|
42
|
+
hasNextPage: boolean
|
|
43
|
+
isFetchingNextPage: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type UseLiveInfiniteQueryReturn<TContext extends Context> =
|
|
47
|
+
UseLiveQueryResult<
|
|
48
|
+
InferResultType<TContext>[number],
|
|
49
|
+
string | number,
|
|
50
|
+
InferResultType<TContext>,
|
|
51
|
+
Collection<InferResultType<TContext>[number], string | number, {}>
|
|
52
|
+
> &
|
|
53
|
+
InfiniteQueryPagination<InferResultType<TContext>[number]>
|
|
54
|
+
|
|
55
|
+
export type UseLiveInfiniteQueryReturnWithCollection<
|
|
56
|
+
TResult extends object,
|
|
57
|
+
TKey extends string | number,
|
|
58
|
+
TUtils extends Record<string, any>,
|
|
59
|
+
> = UseLiveQueryResult<
|
|
60
|
+
TResult,
|
|
61
|
+
TKey,
|
|
62
|
+
Array<TResult>,
|
|
63
|
+
Collection<TResult, TKey, TUtils> & NonSingleResult
|
|
64
|
+
> &
|
|
65
|
+
InfiniteQueryPagination<TResult>
|
|
66
|
+
|
|
67
|
+
/** What one hook instance renders until its input or page shape changes. */
|
|
68
|
+
/**
|
|
69
|
+
* Page through an ordered live query, growing one live-query Collection's
|
|
70
|
+
* window with `utils.setWindow()` instead of recreating it per page.
|
|
71
|
+
*
|
|
72
|
+
* The query must include `.orderBy()`. Its identity follows `useLiveQuery`:
|
|
73
|
+
* derived from the query IR, or named by `queryKey`. A query whose IR cannot be
|
|
74
|
+
* hashed throws `UnhashableQueryIRError` unless `queryKey` is supplied.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const { pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
|
|
78
|
+
* (q) =>
|
|
79
|
+
* q
|
|
80
|
+
* .from({ posts: postsCollection })
|
|
81
|
+
* .orderBy(({ posts }) => posts.createdAt, `desc`),
|
|
82
|
+
* { pageSize: 20 },
|
|
83
|
+
* )
|
|
84
|
+
*/
|
|
85
|
+
export function useLiveInfiniteQuery<
|
|
86
|
+
TResult extends object,
|
|
87
|
+
TKey extends string | number,
|
|
88
|
+
TUtils extends Record<string, any>,
|
|
89
|
+
>(
|
|
90
|
+
liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
|
|
91
|
+
config: UseLiveInfiniteQueryConfig<any>,
|
|
92
|
+
): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>
|
|
93
|
+
|
|
94
|
+
export function useLiveInfiniteQuery<TContext extends Context>(
|
|
95
|
+
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
|
|
96
|
+
config: UseLiveInfiniteQueryConfig<TContext>,
|
|
97
|
+
): UseLiveInfiniteQueryReturn<TContext>
|
|
98
|
+
|
|
99
|
+
export function useLiveInfiniteQuery(
|
|
100
|
+
queryFnOrCollection: unknown,
|
|
101
|
+
config: UseLiveInfiniteQueryConfig<any>,
|
|
102
|
+
): UseLiveInfiniteQueryReturn<any> {
|
|
103
|
+
const pageShape = readLiveQueryWindowPageShape(config)
|
|
104
|
+
const claimsRef = useRef<LiveQuerySourceIdClaims | null>(null)
|
|
105
|
+
const bindingRef = useRef<LiveQueryWindowBinding | null>(null)
|
|
106
|
+
|
|
107
|
+
// A query input's identity is a hash, so a structurally equal query keeps
|
|
108
|
+
// its loaded pages and a changed one compiles again.
|
|
109
|
+
let preparedValue: unknown
|
|
110
|
+
let identity: string | undefined
|
|
111
|
+
if (typeof queryFnOrCollection === `function`) {
|
|
112
|
+
const queryKey = getLiveQueryKey(config)
|
|
113
|
+
if (queryKey) {
|
|
114
|
+
identity = getLiveQueryHash(undefined, queryKey)
|
|
115
|
+
} else {
|
|
116
|
+
preparedValue = prepareLiveQueryValue(queryFnOrCollection, undefined)
|
|
117
|
+
identity = identifyLiveQuery(
|
|
118
|
+
preparedValue,
|
|
119
|
+
{
|
|
120
|
+
claims: (claimsRef.current ??= createLiveQuerySourceIdClaims()),
|
|
121
|
+
client: undefined,
|
|
122
|
+
bindingName: `useLiveInfiniteQuery`,
|
|
123
|
+
},
|
|
124
|
+
getLiveQueryHash,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const binding = (bindingRef.current = bindLiveQueryWindow(
|
|
130
|
+
bindingRef.current,
|
|
131
|
+
{
|
|
132
|
+
...pageShape,
|
|
133
|
+
// Compile what this render prepared, so the callback runs once.
|
|
134
|
+
input:
|
|
135
|
+
preparedValue === undefined ? queryFnOrCollection : () => preparedValue,
|
|
136
|
+
dependencies: identity === undefined ? undefined : [identity],
|
|
137
|
+
recompile: `identity`,
|
|
138
|
+
},
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
const { controller } = binding
|
|
142
|
+
const snapshot = controller.getSnapshot()
|
|
143
|
+
const [, setRevision] = useState(0)
|
|
144
|
+
|
|
145
|
+
useLayoutEffect(() => {
|
|
146
|
+
const rerender = () => setRevision((revision) => revision + 1)
|
|
147
|
+
controller.subscribe(rerender)
|
|
148
|
+
// The query can publish between this render and its subscription.
|
|
149
|
+
if (controller.getSnapshot() !== snapshot) rerender()
|
|
150
|
+
if (binding.warning) console.warn(binding.warning)
|
|
151
|
+
// Each binding owns its controller, so disposal also detaches this
|
|
152
|
+
// subscription and releases its window lease.
|
|
153
|
+
return () => controller.dispose()
|
|
154
|
+
}, [controller])
|
|
155
|
+
|
|
156
|
+
const fetchNextPage = useCallback(
|
|
157
|
+
() => fetchNextLiveQueryWindowPage(controller),
|
|
158
|
+
[controller],
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
data: snapshot.data,
|
|
163
|
+
get state() {
|
|
164
|
+
return snapshot.state
|
|
165
|
+
},
|
|
166
|
+
status: snapshot.status as CollectionStatus,
|
|
167
|
+
isLoading: snapshot.isLoading,
|
|
168
|
+
isReady: snapshot.isReady,
|
|
169
|
+
isIdle: snapshot.isIdle,
|
|
170
|
+
isLoadingError: snapshot.isLoadingError,
|
|
171
|
+
isCrashed: snapshot.isCrashed,
|
|
172
|
+
isCleanedUp: snapshot.isCleanedUp,
|
|
173
|
+
isSyncError: snapshot.isSyncError,
|
|
174
|
+
collection: snapshot.collection,
|
|
175
|
+
isEnabled: true,
|
|
176
|
+
pages: snapshot.pages,
|
|
177
|
+
pageParams: snapshot.pageParams,
|
|
178
|
+
fetchNextPage,
|
|
179
|
+
hasNextPage: snapshot.hasNextPage,
|
|
180
|
+
isFetchingNextPage: snapshot.isFetchingNextPage,
|
|
181
|
+
health: snapshot.health,
|
|
182
|
+
} as UseLiveInfiniteQueryReturn<any>
|
|
183
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import {
|
|
3
|
+
createLiveQueryObserver,
|
|
4
|
+
createLiveQuerySourceIdClaims,
|
|
5
|
+
getLiveQueryHash,
|
|
6
|
+
getLiveQueryKey,
|
|
7
|
+
identifyLiveQuery,
|
|
8
|
+
isCollection,
|
|
9
|
+
prepareLiveQueryValue,
|
|
10
|
+
resolveLiveQueryValue,
|
|
11
|
+
} from '@tanstack/db'
|
|
12
|
+
import type {
|
|
13
|
+
Collection,
|
|
14
|
+
CollectionStatus,
|
|
15
|
+
Context,
|
|
16
|
+
GetResult,
|
|
17
|
+
InferResultType,
|
|
18
|
+
InitialQueryBuilder,
|
|
19
|
+
LiveQueryCollectionConfig,
|
|
20
|
+
LiveQueryKey,
|
|
21
|
+
LiveQueryObserver,
|
|
22
|
+
LiveQuerySourceIdClaims,
|
|
23
|
+
LiveQueryStatusFlags,
|
|
24
|
+
NonSingleResult,
|
|
25
|
+
PooledLiveQueries,
|
|
26
|
+
QueryBuilder,
|
|
27
|
+
SingleResult,
|
|
28
|
+
} from '@tanstack/db'
|
|
29
|
+
|
|
30
|
+
// A live query this hook creates from a callback or config retires 1 ms after
|
|
31
|
+
// its last subscriber leaves. A supplied Collection, or a config with an
|
|
32
|
+
// explicit `gcTime`, keeps the caller's retention policy.
|
|
33
|
+
const DEFAULT_GC_TIME_MS = 1
|
|
34
|
+
|
|
35
|
+
export type UseLiveQueryStatus = CollectionStatus | `disabled`
|
|
36
|
+
|
|
37
|
+
type ConfiguredQueryBuilder<TContext extends Context> = Extract<
|
|
38
|
+
LiveQueryCollectionConfig<TContext>[`query`],
|
|
39
|
+
QueryBuilder<TContext>
|
|
40
|
+
>
|
|
41
|
+
|
|
42
|
+
type UseLiveQueryConfigOptions<TContext extends Context> = Omit<
|
|
43
|
+
LiveQueryCollectionConfig<TContext>,
|
|
44
|
+
`query`
|
|
45
|
+
> & {
|
|
46
|
+
/**
|
|
47
|
+
* Explicit identity for a query whose IR cannot derive one, such as a query
|
|
48
|
+
* with functional variants. Structured queries should omit it.
|
|
49
|
+
*/
|
|
50
|
+
queryKey?: LiveQueryKey
|
|
51
|
+
/**
|
|
52
|
+
* Pass `pooledLiveQueries` to share one partition among equality-filtered
|
|
53
|
+
* queries of one shape.
|
|
54
|
+
*/
|
|
55
|
+
pool?: PooledLiveQueries
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type UseLiveQueryConfig<TContext extends Context> =
|
|
59
|
+
UseLiveQueryConfigOptions<TContext> &
|
|
60
|
+
Pick<LiveQueryCollectionConfig<TContext>, `query`>
|
|
61
|
+
|
|
62
|
+
export type ConditionalUseLiveQueryConfig<TContext extends Context> =
|
|
63
|
+
UseLiveQueryConfigOptions<TContext> & {
|
|
64
|
+
query:
|
|
65
|
+
| ConfiguredQueryBuilder<TContext>
|
|
66
|
+
| ((
|
|
67
|
+
q: InitialQueryBuilder,
|
|
68
|
+
) => ConfiguredQueryBuilder<TContext> | undefined | null)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The result of a live query that is always enabled. */
|
|
72
|
+
export interface UseLiveQueryResult<
|
|
73
|
+
TResult extends object,
|
|
74
|
+
TKey extends string | number,
|
|
75
|
+
TData,
|
|
76
|
+
TCollection,
|
|
77
|
+
> extends LiveQueryStatusFlags {
|
|
78
|
+
state: ReadonlyMap<TKey, TResult>
|
|
79
|
+
data: TData
|
|
80
|
+
collection: TCollection
|
|
81
|
+
status: CollectionStatus
|
|
82
|
+
isEnabled: true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The result of a live query whose callback can disable it. */
|
|
86
|
+
export interface UseConditionalLiveQueryResult<
|
|
87
|
+
TResult extends object,
|
|
88
|
+
TKey extends string | number,
|
|
89
|
+
TData,
|
|
90
|
+
TCollection,
|
|
91
|
+
> extends LiveQueryStatusFlags {
|
|
92
|
+
state: ReadonlyMap<TKey, TResult> | undefined
|
|
93
|
+
data: TData | undefined
|
|
94
|
+
collection: TCollection | undefined
|
|
95
|
+
status: UseLiveQueryStatus
|
|
96
|
+
isEnabled: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type QueryResult<TContext extends Context> = UseLiveQueryResult<
|
|
100
|
+
GetResult<TContext>,
|
|
101
|
+
string | number,
|
|
102
|
+
InferResultType<TContext>,
|
|
103
|
+
Collection<GetResult<TContext>, string | number, {}>
|
|
104
|
+
>
|
|
105
|
+
|
|
106
|
+
type ConditionalQueryResult<TContext extends Context> =
|
|
107
|
+
UseConditionalLiveQueryResult<
|
|
108
|
+
GetResult<TContext>,
|
|
109
|
+
string | number,
|
|
110
|
+
InferResultType<TContext>,
|
|
111
|
+
Collection<GetResult<TContext>, string | number, {}>
|
|
112
|
+
>
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Subscribe a Preact component to a live query.
|
|
116
|
+
*
|
|
117
|
+
* The hook derives the query's identity from its structured IR on each render,
|
|
118
|
+
* so values captured by the callback recompile the query when they change. A
|
|
119
|
+
* query whose IR cannot be hashed, such as one using `fn.where`, throws
|
|
120
|
+
* `UnhashableQueryIRError` unless its config supplies `queryKey`. While the
|
|
121
|
+
* hook is mounted, each Collection ID its queries read must keep naming one
|
|
122
|
+
* source object, or it throws `SourceCollectionIdReuseError`.
|
|
123
|
+
* Return `undefined` or `null` from the callback to disable the query.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* const { data, isLoading } = useLiveQuery((q) =>
|
|
127
|
+
* q
|
|
128
|
+
* .from({ todos: todosCollection })
|
|
129
|
+
* .where(({ todos }) => eq(todos.completed, false)),
|
|
130
|
+
* )
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* // Disable the query until its input exists
|
|
134
|
+
* const { data, isEnabled } = useLiveQuery((q) =>
|
|
135
|
+
* userId
|
|
136
|
+
* ? q
|
|
137
|
+
* .from({ todos: todosCollection })
|
|
138
|
+
* .where(({ todos }) => eq(todos.userId, userId))
|
|
139
|
+
* : undefined,
|
|
140
|
+
* )
|
|
141
|
+
*/
|
|
142
|
+
export function useLiveQuery<TContext extends Context>(
|
|
143
|
+
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
|
|
144
|
+
): QueryResult<TContext>
|
|
145
|
+
|
|
146
|
+
export function useLiveQuery<TContext extends Context>(
|
|
147
|
+
queryFn: (
|
|
148
|
+
q: InitialQueryBuilder,
|
|
149
|
+
) => QueryBuilder<TContext> | undefined | null,
|
|
150
|
+
): ConditionalQueryResult<TContext>
|
|
151
|
+
|
|
152
|
+
export function useLiveQuery<TContext extends Context>(
|
|
153
|
+
queryFn: (
|
|
154
|
+
q: InitialQueryBuilder,
|
|
155
|
+
) => LiveQueryCollectionConfig<TContext> | undefined | null,
|
|
156
|
+
): ConditionalQueryResult<TContext>
|
|
157
|
+
|
|
158
|
+
export function useLiveQuery<
|
|
159
|
+
TResult extends object,
|
|
160
|
+
TKey extends string | number,
|
|
161
|
+
TUtils extends Record<string, any>,
|
|
162
|
+
>(
|
|
163
|
+
queryFn: (
|
|
164
|
+
q: InitialQueryBuilder,
|
|
165
|
+
) => Collection<TResult, TKey, TUtils> | undefined | null,
|
|
166
|
+
): UseConditionalLiveQueryResult<
|
|
167
|
+
TResult,
|
|
168
|
+
TKey,
|
|
169
|
+
Array<TResult>,
|
|
170
|
+
Collection<TResult, TKey, TUtils>
|
|
171
|
+
>
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Subscribe to a live query described by a config object.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const { data } = useLiveQuery({
|
|
178
|
+
* query: (q) => q.from({ todos: todosCollection }),
|
|
179
|
+
* gcTime: 60_000,
|
|
180
|
+
* })
|
|
181
|
+
*/
|
|
182
|
+
export function useLiveQuery<TContext extends Context>(
|
|
183
|
+
config: UseLiveQueryConfig<TContext>,
|
|
184
|
+
): QueryResult<TContext>
|
|
185
|
+
|
|
186
|
+
export function useLiveQuery<TContext extends Context>(
|
|
187
|
+
config: ConditionalUseLiveQueryConfig<TContext>,
|
|
188
|
+
): ConditionalQueryResult<TContext>
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Subscribe to an existing live-query Collection. The hook starts its sync and
|
|
192
|
+
* leaves its retention policy to the caller.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* const activeTodos = createLiveQueryCollection((q) =>
|
|
196
|
+
* q.from({ todos: todosCollection }).where(({ todos }) => todos.active),
|
|
197
|
+
* )
|
|
198
|
+
* const { data, collection } = useLiveQuery(activeTodos)
|
|
199
|
+
*/
|
|
200
|
+
export function useLiveQuery<
|
|
201
|
+
TResult extends object,
|
|
202
|
+
TKey extends string | number,
|
|
203
|
+
TUtils extends Record<string, any>,
|
|
204
|
+
>(
|
|
205
|
+
liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
|
|
206
|
+
): UseLiveQueryResult<
|
|
207
|
+
TResult,
|
|
208
|
+
TKey,
|
|
209
|
+
Array<TResult>,
|
|
210
|
+
Collection<TResult, TKey, TUtils>
|
|
211
|
+
>
|
|
212
|
+
|
|
213
|
+
export function useLiveQuery<
|
|
214
|
+
TResult extends object,
|
|
215
|
+
TKey extends string | number,
|
|
216
|
+
TUtils extends Record<string, any>,
|
|
217
|
+
>(
|
|
218
|
+
liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,
|
|
219
|
+
): UseLiveQueryResult<
|
|
220
|
+
TResult,
|
|
221
|
+
TKey,
|
|
222
|
+
TResult | undefined,
|
|
223
|
+
Collection<TResult, TKey, TUtils> & SingleResult
|
|
224
|
+
>
|
|
225
|
+
|
|
226
|
+
export function useLiveQuery(
|
|
227
|
+
input: unknown,
|
|
228
|
+
): UseConditionalLiveQueryResult<
|
|
229
|
+
object,
|
|
230
|
+
string | number,
|
|
231
|
+
unknown,
|
|
232
|
+
Collection<object, string | number, any>
|
|
233
|
+
> {
|
|
234
|
+
const claimsRef = useRef<LiveQuerySourceIdClaims | null>(null)
|
|
235
|
+
const preparedValue = prepareLiveQueryValue(input, undefined)
|
|
236
|
+
const queryKey = getLiveQueryKey(input)
|
|
237
|
+
// A supplied Collection is not compiled, so its object is its identity.
|
|
238
|
+
const identity = isCollection(input)
|
|
239
|
+
? input
|
|
240
|
+
: queryKey
|
|
241
|
+
? getLiveQueryHash(preparedValue, queryKey)
|
|
242
|
+
: identifyLiveQuery(
|
|
243
|
+
preparedValue,
|
|
244
|
+
{
|
|
245
|
+
claims: (claimsRef.current ??= createLiveQuerySourceIdClaims()),
|
|
246
|
+
client: undefined,
|
|
247
|
+
bindingName: `useLiveQuery`,
|
|
248
|
+
},
|
|
249
|
+
getLiveQueryHash,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const bindingRef = useRef<LiveQueryBinding | null>(null)
|
|
253
|
+
let binding = bindingRef.current
|
|
254
|
+
if (binding?.identity !== identity) {
|
|
255
|
+
binding = bindingRef.current = {
|
|
256
|
+
identity,
|
|
257
|
+
// Wholesale mode publishes nothing during subscribe; the hook re-reads
|
|
258
|
+
// the observer's snapshot on each notification.
|
|
259
|
+
observer: createLiveQueryObserver(
|
|
260
|
+
resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS }),
|
|
261
|
+
{ mode: `wholesale` },
|
|
262
|
+
),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const { observer } = binding
|
|
267
|
+
const snapshot = observer.getSnapshot()
|
|
268
|
+
const [, setRevision] = useState(0)
|
|
269
|
+
|
|
270
|
+
useLayoutEffect(() => {
|
|
271
|
+
const rerender = () => setRevision((revision) => revision + 1)
|
|
272
|
+
observer.subscribe(rerender)
|
|
273
|
+
// The query can publish between this render and its subscription.
|
|
274
|
+
if (observer.getSnapshot() !== snapshot) rerender()
|
|
275
|
+
// Each binding owns its observer, so disposal also detaches this
|
|
276
|
+
// subscription and releases the live-query Collection.
|
|
277
|
+
return () => observer.dispose()
|
|
278
|
+
}, [observer])
|
|
279
|
+
|
|
280
|
+
return snapshot
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The live query one hook instance reads until its identity changes. */
|
|
284
|
+
interface LiveQueryBinding {
|
|
285
|
+
identity: unknown
|
|
286
|
+
observer: LiveQueryObserver<object, string | number>
|
|
287
|
+
}
|