convex-helpers 0.1.41 → 0.1.42
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/README.md +69 -4
- package/dist/react/cache/hooks.d.ts +73 -0
- package/dist/react/cache/hooks.d.ts.map +1 -0
- package/dist/react/cache/hooks.js +140 -0
- package/dist/react/cache/provider.d.ts +69 -0
- package/dist/react/cache/provider.d.ts.map +1 -0
- package/dist/react/cache/provider.js +142 -0
- package/dist/react/index.d.ts +70 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +95 -46
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -3
- package/react/cache/hooks.ts +171 -0
- package/react/cache/provider.tsx +212 -0
- package/react/index.ts +122 -72
- package/dist/server/_generated/_ignore.d.ts +0 -2
- package/dist/server/_generated/_ignore.d.ts.map +0 -1
- package/dist/server/_generated/_ignore.js +0 -2
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Table of contents:
|
|
|
17
17
|
- [CRUD](#crud-utilities)
|
|
18
18
|
- [Validator utilities](#validator-utilities)
|
|
19
19
|
- [Filter db queries with JS](#filter)
|
|
20
|
+
- [Query caching with ConvexQueryCacheProvider](#query-caching)
|
|
20
21
|
|
|
21
22
|
## Custom Functions
|
|
22
23
|
|
|
@@ -373,10 +374,13 @@ By default, `useQuery` will throw an error when the server throws. It also
|
|
|
373
374
|
returns `undefined` to indicate a "loading" state. This helper returns:
|
|
374
375
|
|
|
375
376
|
```ts
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
377
|
+
import { makeUseQueryWithStatus } from "convex-helpers/react";
|
|
378
|
+
import { useQueries } from "convex/react";
|
|
379
|
+
// Do this once somewhere, name it whatever you want.
|
|
380
|
+
export const useQueryWithStatus = makeUseQueryWithStatus(useQueries);
|
|
381
|
+
|
|
382
|
+
const { status, data, error, isSuccess, isPending, isError } =
|
|
383
|
+
useQueryWithStatus(api.foo.bar, { myArg: 123 });
|
|
380
384
|
```
|
|
381
385
|
|
|
382
386
|
The types of the return is:
|
|
@@ -630,3 +634,64 @@ export const lastCountLongerThanName = query({
|
|
|
630
634
|
},
|
|
631
635
|
});
|
|
632
636
|
```
|
|
637
|
+
|
|
638
|
+
## Query Caching
|
|
639
|
+
|
|
640
|
+
Utilize a query cache implementation which persists subscriptions to the
|
|
641
|
+
server for some expiration period even after app `useQuery` hooks have all
|
|
642
|
+
unmounted. This allows very fast reloading of unevicted values during
|
|
643
|
+
navigation changes, view changes, etc.
|
|
644
|
+
|
|
645
|
+
Related files:
|
|
646
|
+
|
|
647
|
+
- [provider.tsx](./react/cache/provider.tsx) contains `ConvexQueryCacheProvider`,
|
|
648
|
+
a configurable cache provider you put in your react app's root.
|
|
649
|
+
- [hooks.ts](./react/cache/hooks.ts) contains cache-enabled drop-in
|
|
650
|
+
replacements for both `useQuery` and `useQueries` from `convex/react`.
|
|
651
|
+
|
|
652
|
+
To use the cache, first make sure to put a `<ConvexQueryCacheProvider>`
|
|
653
|
+
inside `<ConvexProvider>` in your react component tree:
|
|
654
|
+
|
|
655
|
+
```jsx
|
|
656
|
+
|
|
657
|
+
import { ConvexQueryCacheProvider } from "convex-helpers/react/cache/provider";
|
|
658
|
+
|
|
659
|
+
//...
|
|
660
|
+
|
|
661
|
+
export default function RootLayout({
|
|
662
|
+
children,
|
|
663
|
+
}: Readonly<{
|
|
664
|
+
children: React.ReactNode;
|
|
665
|
+
}>) {
|
|
666
|
+
return (
|
|
667
|
+
<html lang="en">
|
|
668
|
+
<body className={inter.className}>
|
|
669
|
+
<ConvexClientProvider>
|
|
670
|
+
<ConvexQueryCacheProvider>{children}</ConvexQueryCacheProvider>
|
|
671
|
+
</ConvexClientProvider>
|
|
672
|
+
</body>
|
|
673
|
+
</html>
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
This provider takes three optional props:
|
|
679
|
+
|
|
680
|
+
- **expiration** (number) -- Milliseconds to preserve unmounted subscriptions
|
|
681
|
+
in the cache. After this, the subscriptions will be dropped, and the value
|
|
682
|
+
will have to be re-fetched from the server. (Default: 300000, aka 5 minutes)
|
|
683
|
+
- **maxIdleEntires** (number) -- Maximum number of unused subscriptions
|
|
684
|
+
kept in the cache. (Default: 250).
|
|
685
|
+
- **debug** (boolean) -- Dump console logs every 3s to debug the state of
|
|
686
|
+
the cache (Default: false).
|
|
687
|
+
|
|
688
|
+
Finally, you can utilize `useQuery` (and `useQueries`) just the same as
|
|
689
|
+
their `convex/react` equivalents.
|
|
690
|
+
|
|
691
|
+
```jsx
|
|
692
|
+
import { useQuery } from "convex-helpers/react/cache/hooks";
|
|
693
|
+
|
|
694
|
+
// ...
|
|
695
|
+
|
|
696
|
+
const users = useQuery(api.users.getAll);
|
|
697
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { OptionalRestArgsOrSkip, RequestForQueries } from "convex/react";
|
|
2
|
+
import { FunctionReference, FunctionReturnType } from "convex/server";
|
|
3
|
+
/**
|
|
4
|
+
* Load a variable number of reactive Convex queries, utilizing
|
|
5
|
+
* the query cache.
|
|
6
|
+
*
|
|
7
|
+
* `useQueries` is similar to {@link useQuery} but it allows
|
|
8
|
+
* loading multiple queries which can be useful for loading a dynamic number
|
|
9
|
+
* of queries without violating the rules of React hooks.
|
|
10
|
+
*
|
|
11
|
+
* This hook accepts an object whose keys are identifiers for each query and the
|
|
12
|
+
* values are objects of `{ query: FunctionReference, args: Record<string, Value> }`. The
|
|
13
|
+
* `query` is a FunctionReference for the Convex query function to load, and the `args` are
|
|
14
|
+
* the arguments to that function.
|
|
15
|
+
*
|
|
16
|
+
* The hook returns an object that maps each identifier to the result of the query,
|
|
17
|
+
* `undefined` if the query is still loading, or an instance of `Error` if the query
|
|
18
|
+
* threw an exception.
|
|
19
|
+
*
|
|
20
|
+
* For example if you loaded a query like:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const results = useQueries({
|
|
23
|
+
* messagesInGeneral: {
|
|
24
|
+
* query: "listMessages",
|
|
25
|
+
* args: { channel: "#general" }
|
|
26
|
+
* }
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
* then the result would look like:
|
|
30
|
+
* ```typescript
|
|
31
|
+
* {
|
|
32
|
+
* messagesInGeneral: [{
|
|
33
|
+
* channel: "#general",
|
|
34
|
+
* body: "hello"
|
|
35
|
+
* _id: ...,
|
|
36
|
+
* _creationTime: ...
|
|
37
|
+
* }]
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* This React hook contains internal state that will cause a rerender
|
|
42
|
+
* whenever any of the query results change.
|
|
43
|
+
*
|
|
44
|
+
* Throws an error if not used under {@link ConvexProvider}.
|
|
45
|
+
*
|
|
46
|
+
* @param queries - An object mapping identifiers to objects of
|
|
47
|
+
* `{query: string, args: Record<string, Value> }` describing which query
|
|
48
|
+
* functions to fetch.
|
|
49
|
+
* @returns An object with the same keys as the input. The values are the result
|
|
50
|
+
* of the query function, `undefined` if it's still loading, or an `Error` if
|
|
51
|
+
* it threw an exception.
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export declare function useQueries(queries: RequestForQueries): Record<string, any | undefined | Error>;
|
|
56
|
+
/**
|
|
57
|
+
* Load a reactive query within a React component.
|
|
58
|
+
*
|
|
59
|
+
* This React hook contains internal state that will cause a rerender
|
|
60
|
+
* whenever the query result changes.
|
|
61
|
+
*
|
|
62
|
+
* Throws an error if not used under {@link ConvexProvider} and {@link ConvexQueryCacheProvider}.
|
|
63
|
+
*
|
|
64
|
+
* @param query - a {@link FunctionReference} for the public query to run
|
|
65
|
+
* like `api.dir1.dir2.filename.func`.
|
|
66
|
+
* @param args - The arguments to the query function or the string "skip" if the
|
|
67
|
+
* query should not be loaded.
|
|
68
|
+
* @returns the result of the query. If the query is loading returns `undefined`.
|
|
69
|
+
*
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
export declare function useQuery<Query extends FunctionReference<"query">>(query: Query, ...queryArgs: OptionalRestArgsOrSkip<Query>): FunctionReturnType<Query>;
|
|
73
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../react/cache/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAEL,iBAAiB,EACjB,kBAAkB,EAEnB,MAAM,eAAe,CAAC;AAKvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,SAAS,GAAG,KAAK,CAAC,CA4CzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC/D,KAAK,EAAE,KAAK,EACZ,GAAG,SAAS,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAC1C,kBAAkB,CAAC,KAAK,CAAC,CAoB3B"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { getFunctionName, } from "convex/server";
|
|
2
|
+
import { useContext, useEffect, useState } from "react";
|
|
3
|
+
import { ConvexQueryCacheContext } from "./provider.js";
|
|
4
|
+
import { convexToJson } from "convex/values";
|
|
5
|
+
/**
|
|
6
|
+
* Load a variable number of reactive Convex queries, utilizing
|
|
7
|
+
* the query cache.
|
|
8
|
+
*
|
|
9
|
+
* `useQueries` is similar to {@link useQuery} but it allows
|
|
10
|
+
* loading multiple queries which can be useful for loading a dynamic number
|
|
11
|
+
* of queries without violating the rules of React hooks.
|
|
12
|
+
*
|
|
13
|
+
* This hook accepts an object whose keys are identifiers for each query and the
|
|
14
|
+
* values are objects of `{ query: FunctionReference, args: Record<string, Value> }`. The
|
|
15
|
+
* `query` is a FunctionReference for the Convex query function to load, and the `args` are
|
|
16
|
+
* the arguments to that function.
|
|
17
|
+
*
|
|
18
|
+
* The hook returns an object that maps each identifier to the result of the query,
|
|
19
|
+
* `undefined` if the query is still loading, or an instance of `Error` if the query
|
|
20
|
+
* threw an exception.
|
|
21
|
+
*
|
|
22
|
+
* For example if you loaded a query like:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const results = useQueries({
|
|
25
|
+
* messagesInGeneral: {
|
|
26
|
+
* query: "listMessages",
|
|
27
|
+
* args: { channel: "#general" }
|
|
28
|
+
* }
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
* then the result would look like:
|
|
32
|
+
* ```typescript
|
|
33
|
+
* {
|
|
34
|
+
* messagesInGeneral: [{
|
|
35
|
+
* channel: "#general",
|
|
36
|
+
* body: "hello"
|
|
37
|
+
* _id: ...,
|
|
38
|
+
* _creationTime: ...
|
|
39
|
+
* }]
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* This React hook contains internal state that will cause a rerender
|
|
44
|
+
* whenever any of the query results change.
|
|
45
|
+
*
|
|
46
|
+
* Throws an error if not used under {@link ConvexProvider}.
|
|
47
|
+
*
|
|
48
|
+
* @param queries - An object mapping identifiers to objects of
|
|
49
|
+
* `{query: string, args: Record<string, Value> }` describing which query
|
|
50
|
+
* functions to fetch.
|
|
51
|
+
* @returns An object with the same keys as the input. The values are the result
|
|
52
|
+
* of the query function, `undefined` if it's still loading, or an `Error` if
|
|
53
|
+
* it threw an exception.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
export function useQueries(queries) {
|
|
58
|
+
const { registry } = useContext(ConvexQueryCacheContext);
|
|
59
|
+
if (registry === null) {
|
|
60
|
+
throw new Error("Could not find `ConvexQueryCacheContext`! This `useQuery` implementation must be used in the React component " +
|
|
61
|
+
"tree under `ConvexQueryCacheProvider`. Did you forget it? ");
|
|
62
|
+
}
|
|
63
|
+
const queryKeys = {};
|
|
64
|
+
const results = {};
|
|
65
|
+
for (const [key, { query, args }] of Object.entries(queries)) {
|
|
66
|
+
const queryKey = createQueryKey(query, args);
|
|
67
|
+
results[key] = registry.probe(queryKey);
|
|
68
|
+
queryKeys[key] = queryKey;
|
|
69
|
+
}
|
|
70
|
+
const [_, setValues] = useState(results);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const ids = [];
|
|
73
|
+
for (const [key, { query, args }] of Object.entries(queries)) {
|
|
74
|
+
const id = crypto.randomUUID();
|
|
75
|
+
registry.start(id, queryKeys[key], query, args, (v) => setValues((old) => {
|
|
76
|
+
if (!(key in old))
|
|
77
|
+
return old; // We're no longer tracking this query
|
|
78
|
+
if (old[key] === v)
|
|
79
|
+
return old; // No change
|
|
80
|
+
return { ...old, [key]: v };
|
|
81
|
+
}));
|
|
82
|
+
ids.push(id);
|
|
83
|
+
}
|
|
84
|
+
return () => {
|
|
85
|
+
for (const id of ids) {
|
|
86
|
+
registry.end(id);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
// Safe to ignore query and args since queryKey is derived from them
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
[registry, JSON.stringify(queryKeys)]);
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Load a reactive query within a React component.
|
|
97
|
+
*
|
|
98
|
+
* This React hook contains internal state that will cause a rerender
|
|
99
|
+
* whenever the query result changes.
|
|
100
|
+
*
|
|
101
|
+
* Throws an error if not used under {@link ConvexProvider} and {@link ConvexQueryCacheProvider}.
|
|
102
|
+
*
|
|
103
|
+
* @param query - a {@link FunctionReference} for the public query to run
|
|
104
|
+
* like `api.dir1.dir2.filename.func`.
|
|
105
|
+
* @param args - The arguments to the query function or the string "skip" if the
|
|
106
|
+
* query should not be loaded.
|
|
107
|
+
* @returns the result of the query. If the query is loading returns `undefined`.
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export function useQuery(query, ...queryArgs) {
|
|
112
|
+
const args = queryArgs[0] ?? {};
|
|
113
|
+
// Unlike the regular useQuery implementation, we don't need to memoize
|
|
114
|
+
// the params here, since the cached useQueries will handle that.
|
|
115
|
+
const results = useQueries(args === "skip"
|
|
116
|
+
? {} // Use queries doesn't support skip.
|
|
117
|
+
: {
|
|
118
|
+
_default: { query, args },
|
|
119
|
+
});
|
|
120
|
+
// This may be undefined either because the upstream
|
|
121
|
+
// value is actually undefined, or because the value
|
|
122
|
+
// was not sent to `useQueries` due to "skip".
|
|
123
|
+
const result = results._default;
|
|
124
|
+
if (result instanceof Error) {
|
|
125
|
+
throw result;
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Generate a query key from a query function and its arguments.
|
|
131
|
+
* @param query Query function reference like api.foo.bar
|
|
132
|
+
* @param args Arguments to the function, like { foo: "bar" }
|
|
133
|
+
* @returns A string key that uniquely identifies the query and its arguments.
|
|
134
|
+
*/
|
|
135
|
+
function createQueryKey(query, args) {
|
|
136
|
+
const queryString = getFunctionName(query);
|
|
137
|
+
const key = [queryString, convexToJson(args)];
|
|
138
|
+
const queryKey = JSON.stringify(key);
|
|
139
|
+
return queryKey;
|
|
140
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ConvexReactClient } from "convex/react";
|
|
2
|
+
import { FunctionArgs, FunctionReference, FunctionReturnType } from "convex/server";
|
|
3
|
+
import { FC, PropsWithChildren } from "react";
|
|
4
|
+
export declare const ConvexQueryCacheContext: import("react").Context<{
|
|
5
|
+
registry: CacheRegistry | null;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* A provider that establishes a query cache context in the React render
|
|
9
|
+
* tree so that cached `useQuery` calls can be used.
|
|
10
|
+
*
|
|
11
|
+
* @component
|
|
12
|
+
* @param {ConvexQueryCacheOptions} props.options - Options for the query cache
|
|
13
|
+
* @returns {Element}
|
|
14
|
+
*/
|
|
15
|
+
export declare const ConvexQueryCacheProvider: FC<PropsWithChildren<ConvexQueryCacheOptions>>;
|
|
16
|
+
export type ConvexQueryCacheOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* How long, in milliseconds, to keep the subscription to the convex
|
|
19
|
+
* query alive even after all references in the app have been dropped.
|
|
20
|
+
*
|
|
21
|
+
* @default 300000
|
|
22
|
+
*/
|
|
23
|
+
expiration?: number;
|
|
24
|
+
/**
|
|
25
|
+
* How many "extra" idle query subscriptions are allowed to remain
|
|
26
|
+
* connected to your convex backend.
|
|
27
|
+
*
|
|
28
|
+
* @default Infinity
|
|
29
|
+
*/
|
|
30
|
+
maxIdleEntries?: number;
|
|
31
|
+
/**
|
|
32
|
+
* A debug flag that will cause information about the query cache
|
|
33
|
+
* to be logged to the console every 3 seconds.
|
|
34
|
+
*
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
debug?: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Implementation of the query cache.
|
|
41
|
+
*/
|
|
42
|
+
type SubKey = string;
|
|
43
|
+
type QueryKey = string;
|
|
44
|
+
type CachedQuery<Query extends FunctionReference<"query">> = {
|
|
45
|
+
refs: Set<string>;
|
|
46
|
+
evictTimer: number | null;
|
|
47
|
+
unsub?: () => void;
|
|
48
|
+
value?: FunctionReturnType<Query> | Error;
|
|
49
|
+
};
|
|
50
|
+
type SubEntry<Query extends FunctionReference<"query">> = {
|
|
51
|
+
queryKey: QueryKey;
|
|
52
|
+
setter: (v?: FunctionReturnType<Query>) => void;
|
|
53
|
+
};
|
|
54
|
+
declare class CacheRegistry {
|
|
55
|
+
#private;
|
|
56
|
+
queries: Map<QueryKey, CachedQuery<FunctionReference<"query">>>;
|
|
57
|
+
subs: Map<SubKey, SubEntry<FunctionReference<"query">>>;
|
|
58
|
+
convex: ConvexReactClient;
|
|
59
|
+
timeout: number;
|
|
60
|
+
maxIdleEntries: number;
|
|
61
|
+
idle: number;
|
|
62
|
+
constructor(convex: ConvexReactClient, options: ConvexQueryCacheOptions);
|
|
63
|
+
probe<Query extends FunctionReference<"query">>(queryKey: QueryKey): FunctionReturnType<Query> | undefined;
|
|
64
|
+
start<Query extends FunctionReference<"query">>(id: string, queryKey: string, query: Query, args: FunctionArgs<Query>, setter: (v: FunctionReturnType<Query>) => void): void;
|
|
65
|
+
end(id: string): void;
|
|
66
|
+
debug(): void;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../react/cache/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAiB,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE7D,eAAO,MAAM,uBAAuB;;EAElC,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,EAAE,CACvC,iBAAiB,CAAC,uBAAuB,CAAC,CAgB3C,CAAC;AAKF,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AAEH,KAAK,MAAM,GAAG,MAAM,CAAC;AACrB,KAAK,QAAQ,GAAG,MAAM,CAAC;AACvB,KAAK,WAAW,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,IAAI;IAC3D,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;CAC3C,CAAC;AACF,KAAK,QAAQ,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,IAAI;IACxD,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;CACjD,CAAC;AAGF,cAAM,aAAa;;IACjB,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAChE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;gBAED,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,uBAAuB;IA0BvE,KAAK,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC5C,QAAQ,EAAE,QAAQ,GACjB,kBAAkB,CAAC,KAAK,CAAC,GAAG,SAAS;IAMxC,KAAK,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC5C,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,EACzB,MAAM,EAAE,CAAC,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,KAAK,IAAI,GAC7C,IAAI;IAoCP,GAAG,CAAC,EAAE,EAAE,MAAM;IA0Bd,KAAK;CAeN"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useConvex } from "convex/react";
|
|
3
|
+
import { createContext } from "react";
|
|
4
|
+
export const ConvexQueryCacheContext = createContext({
|
|
5
|
+
registry: null,
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* A provider that establishes a query cache context in the React render
|
|
9
|
+
* tree so that cached `useQuery` calls can be used.
|
|
10
|
+
*
|
|
11
|
+
* @component
|
|
12
|
+
* @param {ConvexQueryCacheOptions} props.options - Options for the query cache
|
|
13
|
+
* @returns {Element}
|
|
14
|
+
*/
|
|
15
|
+
export const ConvexQueryCacheProvider = ({ children, ...options }) => {
|
|
16
|
+
const convex = useConvex();
|
|
17
|
+
if (convex === undefined) {
|
|
18
|
+
throw new Error("Could not find Convex client! `ConvexQueryCacheProvider` must be used in the React component " +
|
|
19
|
+
"tree under `ConvexProvider`. Did you forget it? " +
|
|
20
|
+
"See https://docs.convex.dev/quick-start#set-up-convex-in-your-react-app");
|
|
21
|
+
}
|
|
22
|
+
const registry = new CacheRegistry(convex, options);
|
|
23
|
+
return (_jsx(ConvexQueryCacheContext.Provider, { value: { registry }, children: children }));
|
|
24
|
+
};
|
|
25
|
+
const DEFAULT_EXPIRATION_MS = 300_000; // 5 minutes
|
|
26
|
+
const DEFAULT_MAX_ENTRIES = 250;
|
|
27
|
+
// Core caching structure.
|
|
28
|
+
class CacheRegistry {
|
|
29
|
+
queries;
|
|
30
|
+
subs;
|
|
31
|
+
convex;
|
|
32
|
+
timeout;
|
|
33
|
+
maxIdleEntries;
|
|
34
|
+
idle;
|
|
35
|
+
constructor(convex, options) {
|
|
36
|
+
this.queries = new Map();
|
|
37
|
+
this.subs = new Map();
|
|
38
|
+
this.convex = convex;
|
|
39
|
+
this.idle = 0;
|
|
40
|
+
this.timeout = options.expiration ?? DEFAULT_EXPIRATION_MS;
|
|
41
|
+
this.maxIdleEntries = options.maxIdleEntries ?? DEFAULT_MAX_ENTRIES;
|
|
42
|
+
if (options.debug ?? false) {
|
|
43
|
+
const weakThis = new WeakRef(this);
|
|
44
|
+
const debugInterval = setInterval(() => {
|
|
45
|
+
const r = weakThis.deref();
|
|
46
|
+
if (r === undefined) {
|
|
47
|
+
clearInterval(debugInterval);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
r.debug();
|
|
51
|
+
}
|
|
52
|
+
}, 3000);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
#getQueryEntry(queryKey) {
|
|
56
|
+
const entry = this.queries.get(queryKey);
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
|
59
|
+
probe(queryKey) {
|
|
60
|
+
const entry = this.#getQueryEntry(queryKey);
|
|
61
|
+
return entry === undefined ? undefined : entry.value;
|
|
62
|
+
}
|
|
63
|
+
// Enable a new subscription.
|
|
64
|
+
start(id, queryKey, query, args, setter) {
|
|
65
|
+
let entry = this.#getQueryEntry(queryKey);
|
|
66
|
+
this.subs.set(id, {
|
|
67
|
+
queryKey,
|
|
68
|
+
setter,
|
|
69
|
+
});
|
|
70
|
+
if (entry === undefined) {
|
|
71
|
+
entry = {
|
|
72
|
+
refs: new Set(),
|
|
73
|
+
evictTimer: null,
|
|
74
|
+
};
|
|
75
|
+
const w = this.convex.watchQuery(query, args);
|
|
76
|
+
const unsub = w.onUpdate(() => {
|
|
77
|
+
const e = entry;
|
|
78
|
+
try {
|
|
79
|
+
e.value = w.localQueryResult();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
e.value = err;
|
|
83
|
+
}
|
|
84
|
+
for (const ref of e.refs.values()) {
|
|
85
|
+
this.subs.get(ref)?.setter(e.value);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
entry.unsub = unsub;
|
|
89
|
+
this.queries.set(queryKey, entry);
|
|
90
|
+
}
|
|
91
|
+
else if (entry.evictTimer !== null) {
|
|
92
|
+
this.idle -= 1;
|
|
93
|
+
clearTimeout(entry.evictTimer);
|
|
94
|
+
entry.evictTimer = null;
|
|
95
|
+
}
|
|
96
|
+
entry.refs.add(id);
|
|
97
|
+
if (entry.value !== undefined) {
|
|
98
|
+
setter(entry.value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// End a previous subscription.
|
|
102
|
+
end(id) {
|
|
103
|
+
const sub = this.subs.get(id);
|
|
104
|
+
if (sub) {
|
|
105
|
+
this.subs.delete(id);
|
|
106
|
+
const cq = this.queries.get(sub.queryKey);
|
|
107
|
+
const qk = sub.queryKey;
|
|
108
|
+
cq?.refs.delete(id);
|
|
109
|
+
// None left?
|
|
110
|
+
if (cq?.refs.size === 0) {
|
|
111
|
+
const remove = () => {
|
|
112
|
+
cq.unsub();
|
|
113
|
+
this.queries.delete(qk);
|
|
114
|
+
};
|
|
115
|
+
if (this.idle == this.maxIdleEntries) {
|
|
116
|
+
remove();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.idle += 1;
|
|
120
|
+
const evictTimer = window.setTimeout(() => {
|
|
121
|
+
this.idle -= 1;
|
|
122
|
+
remove();
|
|
123
|
+
}, this.timeout);
|
|
124
|
+
cq.evictTimer = evictTimer;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
debug() {
|
|
130
|
+
console.log("DEBUG CACHE");
|
|
131
|
+
console.log(`IDLE = ${this.idle}`);
|
|
132
|
+
console.log(" SUBS");
|
|
133
|
+
for (const [k, v] of this.subs.entries()) {
|
|
134
|
+
console.log(` ${k} => ${v.queryKey}`);
|
|
135
|
+
}
|
|
136
|
+
console.log(" QUERIES");
|
|
137
|
+
for (const [k, v] of this.queries.entries()) {
|
|
138
|
+
console.log(` ${k} => ${v.refs.size} refs, evict = ${v.evictTimer}, value = ${v.value}`);
|
|
139
|
+
}
|
|
140
|
+
console.log("~~~~~~~~~~~~~~~~~~~~~~");
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,9 +1,73 @@
|
|
|
1
|
-
import { OptionalRestArgsOrSkip } from "convex/react";
|
|
1
|
+
import { OptionalRestArgsOrSkip, useQueries } from "convex/react";
|
|
2
2
|
import { FunctionReference, FunctionReturnType } from "convex/server";
|
|
3
3
|
/**
|
|
4
4
|
* Use in place of `useQuery` from "convex/react" to fetch data from a query
|
|
5
5
|
* function but instead returns `{ status, data, error, isSuccess, isPending, isError}`.
|
|
6
6
|
*
|
|
7
|
+
* Want a different name? Use `makeUseQueryWithStatus` to create a custom hook:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { useQueries } from "convex/react";
|
|
10
|
+
* import { makeUseQueryWithStatus } from "convex-helpers/react";
|
|
11
|
+
* export const useQuery = makeUseQueryWithStatus(useQueries);
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Status is one of "success", "pending", or "error".
|
|
15
|
+
* Docs copied from {@link useQueryOriginal} until `returns` block:
|
|
16
|
+
*
|
|
17
|
+
* Load a reactive query within a React component.
|
|
18
|
+
*
|
|
19
|
+
* This React hook contains internal state that will cause a rerender
|
|
20
|
+
* whenever the query result changes.
|
|
21
|
+
*
|
|
22
|
+
* Throws an error if not used under {@link ConvexProvider}.
|
|
23
|
+
*
|
|
24
|
+
* @param query - a {@link server.FunctionReference} for the public query to run
|
|
25
|
+
* like `api.dir1.dir2.filename.func`.
|
|
26
|
+
* @param args - The arguments to the query function or the string "skip" if the
|
|
27
|
+
* query should not be loaded.
|
|
28
|
+
* @returns {status, data, error, isSuccess, isPending, isError} where:
|
|
29
|
+
* - `status` is one of "success", "pending", or "error"
|
|
30
|
+
* - `data` is the result of the query function, if it loaded successfully,
|
|
31
|
+
* - `error` is an `Error` if the query threw an exception.
|
|
32
|
+
* - `isSuccess` is `true` if the query loaded successfully.
|
|
33
|
+
* - `isPending` is `true` if the query is still loading or "skip" was passed.
|
|
34
|
+
* - `isError` is `true` if the query threw an exception.
|
|
35
|
+
*/
|
|
36
|
+
export declare const useQuery: <Query extends FunctionReference<"query">>(query: Query, ...queryArgs: OptionalRestArgsOrSkip<Query>) => {
|
|
37
|
+
status: "success";
|
|
38
|
+
data: FunctionReturnType<Query>;
|
|
39
|
+
error: undefined;
|
|
40
|
+
isSuccess: true;
|
|
41
|
+
isPending: false;
|
|
42
|
+
isError: false;
|
|
43
|
+
} | {
|
|
44
|
+
status: "pending";
|
|
45
|
+
data: undefined;
|
|
46
|
+
error: undefined;
|
|
47
|
+
isSuccess: false;
|
|
48
|
+
isPending: true;
|
|
49
|
+
isError: false;
|
|
50
|
+
} | {
|
|
51
|
+
status: "error";
|
|
52
|
+
data: undefined;
|
|
53
|
+
error: Error;
|
|
54
|
+
isSuccess: false;
|
|
55
|
+
isPending: false;
|
|
56
|
+
isError: true;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Makes a hook to use in place of `useQuery` from "convex/react" to fetch data from a query
|
|
60
|
+
* function but instead returns `{ status, data, error, isSuccess, isPending, isError}`.
|
|
61
|
+
*
|
|
62
|
+
* You can pass in any hook that matches the signature of {@link useQueries} from "convex/react".
|
|
63
|
+
* For instance:
|
|
64
|
+
*
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { useQueries } from "convex-helpers/react/cache/hooks";
|
|
67
|
+
* import { makeUseQueryWithStatus } from "convex-helpers/react";
|
|
68
|
+
* const useQuery = makeUseQueryWithStatus(useQueries);
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
7
71
|
* Status is one of "success", "pending", or "error".
|
|
8
72
|
* Docs copied from {@link useQueryOriginal} until `returns` block:
|
|
9
73
|
*
|
|
@@ -25,8 +89,12 @@ import { FunctionReference, FunctionReturnType } from "convex/server";
|
|
|
25
89
|
* - `isSuccess` is `true` if the query loaded successfully.
|
|
26
90
|
* - `isPending` is `true` if the query is still loading or "skip" was passed.
|
|
27
91
|
* - `isError` is `true` if the query threw an exception.
|
|
92
|
+
*
|
|
93
|
+
* @param useQueries Something matching the signature of {@link useQueries} from "convex/react".
|
|
94
|
+
* @returns
|
|
95
|
+
* @returns A useQuery function that returns an object with status, data, error, isSuccess, isPending, isError.
|
|
28
96
|
*/
|
|
29
|
-
export declare function
|
|
97
|
+
export declare function makeUseQueryWithStatus(useQueriesHook: typeof useQueries): <Query extends FunctionReference<"query">>(query: Query, ...queryArgs: OptionalRestArgsOrSkip<Query>) => {
|
|
30
98
|
status: "success";
|
|
31
99
|
data: FunctionReturnType<Query>;
|
|
32
100
|
error: undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,UAAU,EAEX,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAEnB,MAAM,eAAe,CAAC;AAIvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,QAAQ,oDA2CV,KAAK,gBACE,uBAAuB,KAAK,CAAC,KAEzC;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,mBAAmB,KAAK,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,AArEmD,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,OAAO,UAAU,qDAE7D,KAAK,gBACE,uBAAuB,KAAK,CAAC,KAEzC;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,mBAAmB,KAAK,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,CAuDN"}
|