@solidjs/router 0.14.10 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -19
- package/dist/data/action.d.ts +5 -1
- package/dist/data/action.js +20 -7
- package/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +1 -1
- package/dist/data/query.d.ts +20 -0
- package/dist/data/query.js +218 -0
- package/dist/index.js +50 -26
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -427,30 +427,30 @@ The return value of the `preload` function is passed to the page component when
|
|
|
427
427
|
|
|
428
428
|
Keep in mind these are completely optional. To use but showcase the power of our preload mechanism.
|
|
429
429
|
|
|
430
|
-
### `
|
|
430
|
+
### `query`
|
|
431
431
|
|
|
432
|
-
To prevent duplicate fetching and to trigger handle refetching we provide a
|
|
432
|
+
To prevent duplicate fetching and to trigger handle refetching we provide a query api. That takes a function and returns the same function.
|
|
433
433
|
|
|
434
434
|
```jsx
|
|
435
|
-
const getUser =
|
|
435
|
+
const getUser = query(async (id) => {
|
|
436
436
|
return (await fetch(`/api/users${id}`)).json()
|
|
437
|
-
}, "users") // used as
|
|
437
|
+
}, "users") // used as the query key + serialized arguments
|
|
438
438
|
```
|
|
439
|
-
It is expected that the arguments to the
|
|
439
|
+
It is expected that the arguments to the query function are serializable.
|
|
440
440
|
|
|
441
|
-
This
|
|
441
|
+
This query accomplishes the following:
|
|
442
442
|
|
|
443
|
-
1. It does
|
|
444
|
-
2. It
|
|
443
|
+
1. It does deduping on the server for the lifetime of the request.
|
|
444
|
+
2. It fills a preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
|
|
445
445
|
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
|
|
446
|
-
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses
|
|
446
|
+
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses this cache. Revalidation or new fetch updates the cache.
|
|
447
447
|
|
|
448
448
|
Using it with preload function might look like:
|
|
449
449
|
|
|
450
450
|
```js
|
|
451
451
|
import { lazy } from "solid-js";
|
|
452
452
|
import { Route } from "@solidjs/router";
|
|
453
|
-
import { getUser } from ... // the
|
|
453
|
+
import { getUser } from ... // the query function
|
|
454
454
|
|
|
455
455
|
const User = lazy(() => import("./pages/users/[id].js"));
|
|
456
456
|
|
|
@@ -467,7 +467,7 @@ Inside your page component you:
|
|
|
467
467
|
|
|
468
468
|
```jsx
|
|
469
469
|
// pages/users/[id].js
|
|
470
|
-
import { getUser } from ... // the
|
|
470
|
+
import { getUser } from ... // the query function
|
|
471
471
|
|
|
472
472
|
export default function User(props) {
|
|
473
473
|
const user = createAsync(() => getUser(props.params.id));
|
|
@@ -483,9 +483,9 @@ getUser.key // returns "users"
|
|
|
483
483
|
getUser.keyFor(id) // returns "users[5]"
|
|
484
484
|
```
|
|
485
485
|
|
|
486
|
-
You can revalidate the
|
|
486
|
+
You can revalidate the query using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the query (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
|
|
487
487
|
|
|
488
|
-
`
|
|
488
|
+
`query` can be defined anywhere and then used inside your components with:
|
|
489
489
|
|
|
490
490
|
### `createAsync`
|
|
491
491
|
|
|
@@ -502,7 +502,7 @@ const user = createAsync((currentValue) => getUser(params.id))
|
|
|
502
502
|
return <h1>{user.latest.name}</h1>;
|
|
503
503
|
```
|
|
504
504
|
|
|
505
|
-
Using `
|
|
505
|
+
Using `query` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
|
|
506
506
|
|
|
507
507
|
### `createAsyncStore`
|
|
508
508
|
|
|
@@ -558,7 +558,9 @@ const deleteUser = action(api.deleteTodo)
|
|
|
558
558
|
</form>
|
|
559
559
|
```
|
|
560
560
|
|
|
561
|
-
|
|
561
|
+
Actions also a second argument which can be the name or an option object with `name` and `onComplete`. `name` is used to identify SSR actions that aren't server functions (see note below). `onComplete` allows you to configure behavior when `action`s complete. Keep in mind `onComplete` does not work when JavaScript is disabled.
|
|
562
|
+
|
|
563
|
+
#### Notes on `<form>` implementation and SSR
|
|
562
564
|
This requires stable references as you can only serialize a string as an attribute, and across SSR they'd need to match. The solution is providing a unique name.
|
|
563
565
|
|
|
564
566
|
```jsx
|
|
@@ -597,13 +599,13 @@ const submission = useSubmission(action, (input) => filter(input));
|
|
|
597
599
|
|
|
598
600
|
### Response Helpers
|
|
599
601
|
|
|
600
|
-
These are used to communicate router navigations from
|
|
602
|
+
These are used to communicate router navigations from query/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
|
|
601
603
|
|
|
602
604
|
#### `redirect(path, options)`
|
|
603
605
|
|
|
604
606
|
Redirects to the next route
|
|
605
607
|
```js
|
|
606
|
-
const getUser =
|
|
608
|
+
const getUser = query(() => {
|
|
607
609
|
const user = await api.getCurrentUser()
|
|
608
610
|
if (!user) throw redirect("/login");
|
|
609
611
|
return user;
|
|
@@ -614,7 +616,7 @@ const getUser = cache(() => {
|
|
|
614
616
|
|
|
615
617
|
Reloads the data on the current page
|
|
616
618
|
```js
|
|
617
|
-
const getTodo =
|
|
619
|
+
const getTodo = query(async (id: number) => {
|
|
618
620
|
const todo = await fetchTodo(id);
|
|
619
621
|
return todo;
|
|
620
622
|
}, "todo")
|
|
@@ -937,7 +939,7 @@ Related without Outlet component it has to be passed in manually. At which point
|
|
|
937
939
|
|
|
938
940
|
### `data` functions & `useRouteData`
|
|
939
941
|
|
|
940
|
-
These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/
|
|
942
|
+
These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/query APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
|
|
941
943
|
|
|
942
944
|
That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:
|
|
943
945
|
|
package/dist/data/action.d.ts
CHANGED
|
@@ -10,4 +10,8 @@ export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T,
|
|
|
10
10
|
};
|
|
11
11
|
export declare function useSubmission<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>> | SubmissionStub;
|
|
12
12
|
export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>;
|
|
13
|
-
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U
|
|
13
|
+
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
|
|
14
|
+
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
|
|
15
|
+
name?: string;
|
|
16
|
+
onComplete?: (s: Submission<T, U>) => boolean;
|
|
17
|
+
}): Action<T, U>;
|
package/dist/data/action.js
CHANGED
|
@@ -2,7 +2,7 @@ import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js"
|
|
|
2
2
|
import { isServer } from "solid-js/web";
|
|
3
3
|
import { useRouter } from "../routing.js";
|
|
4
4
|
import { mockBase } from "../utils.js";
|
|
5
|
-
import { cacheKeyOp, hashKey, revalidate,
|
|
5
|
+
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
6
6
|
export const actions = /* #__PURE__ */ new Map();
|
|
7
7
|
export function useSubmissions(fn, filter) {
|
|
8
8
|
const router = useRouter();
|
|
@@ -24,8 +24,8 @@ export function useSubmission(fn, filter) {
|
|
|
24
24
|
const submissions = useSubmissions(fn, filter);
|
|
25
25
|
return new Proxy({}, {
|
|
26
26
|
get(_, property) {
|
|
27
|
-
if (submissions.length === 0 && property === "clear" || property === "retry")
|
|
28
|
-
return (
|
|
27
|
+
if ((submissions.length === 0 && property === "clear") || property === "retry")
|
|
28
|
+
return () => { };
|
|
29
29
|
return submissions[submissions.length - 1]?.[property];
|
|
30
30
|
}
|
|
31
31
|
});
|
|
@@ -34,7 +34,7 @@ export function useAction(action) {
|
|
|
34
34
|
const r = useRouter();
|
|
35
35
|
return (...args) => action.apply({ r }, args);
|
|
36
36
|
}
|
|
37
|
-
export function action(fn,
|
|
37
|
+
export function action(fn, options = {}) {
|
|
38
38
|
function mutate(...variables) {
|
|
39
39
|
const router = this.r;
|
|
40
40
|
const form = this.f;
|
|
@@ -46,6 +46,18 @@ export function action(fn, name) {
|
|
|
46
46
|
function handler(error) {
|
|
47
47
|
return async (res) => {
|
|
48
48
|
const result = await handleResponse(res, error, router.navigatorFactory());
|
|
49
|
+
let retry = null;
|
|
50
|
+
!o.onComplete?.({
|
|
51
|
+
...submission,
|
|
52
|
+
result: result?.data,
|
|
53
|
+
error: result?.error,
|
|
54
|
+
pending: false,
|
|
55
|
+
retry() {
|
|
56
|
+
return retry = submission.retry();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (retry)
|
|
60
|
+
return retry;
|
|
49
61
|
if (!result)
|
|
50
62
|
return submission.clear();
|
|
51
63
|
setResult(result);
|
|
@@ -69,7 +81,7 @@ export function action(fn, name) {
|
|
|
69
81
|
return !result();
|
|
70
82
|
},
|
|
71
83
|
clear() {
|
|
72
|
-
router.submissions[1](v => v.filter(i => i
|
|
84
|
+
router.submissions[1](v => v.filter(i => i !== submission));
|
|
73
85
|
},
|
|
74
86
|
retry() {
|
|
75
87
|
setResult(undefined);
|
|
@@ -80,8 +92,9 @@ export function action(fn, name) {
|
|
|
80
92
|
]);
|
|
81
93
|
return p.then(handler(), handler(true));
|
|
82
94
|
}
|
|
95
|
+
const o = typeof options === "string" ? { name: options } : options;
|
|
83
96
|
const url = fn.url ||
|
|
84
|
-
(name && `https://action/${name}`) ||
|
|
97
|
+
(o.name && `https://action/${o.name}`) ||
|
|
85
98
|
(!isServer ? `https://action/${hashString(fn.toString())}` : "");
|
|
86
99
|
mutate.base = url;
|
|
87
100
|
return toAction(mutate, url);
|
|
@@ -142,7 +155,7 @@ async function handleResponse(response, error, navigate) {
|
|
|
142
155
|
// invalidate
|
|
143
156
|
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
144
157
|
// set cache
|
|
145
|
-
flightKeys && flightKeys.forEach(k =>
|
|
158
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
146
159
|
// trigger revalidation
|
|
147
160
|
await revalidate(keys, false);
|
|
148
161
|
return data != null ? { data } : undefined;
|
package/dist/data/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js";
|
|
2
2
|
export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
|
|
3
|
-
export {
|
|
3
|
+
export { query, revalidate, cache, type CachedFunction } from "./query.js";
|
|
4
4
|
export { redirect, reload, json } from "./response.js";
|
package/dist/data/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createAsync, createAsyncStore } from "./createAsync.js";
|
|
2
2
|
export { action, useSubmission, useSubmissions, useAction } from "./action.js";
|
|
3
|
-
export {
|
|
3
|
+
export { query, revalidate, cache } from "./query.js";
|
|
4
4
|
export { redirect, reload, json } from "./response.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CacheEntry, NarrowResponse } from "../types.js";
|
|
2
|
+
export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
|
|
3
|
+
export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
|
|
4
|
+
export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
|
|
5
|
+
[K in keyof A]-?: A[K];
|
|
6
|
+
} ? (...args: never[]) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R> : (...args: A) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R>) & {
|
|
7
|
+
keyFor: (...args: A) => string;
|
|
8
|
+
key: string;
|
|
9
|
+
} : never;
|
|
10
|
+
export declare function query<T extends (...args: any) => any>(fn: T, name: string): CachedFunction<T>;
|
|
11
|
+
export declare namespace query {
|
|
12
|
+
export var get: (key: string) => any;
|
|
13
|
+
export var set: <T>(key: string, value: T extends Promise<any> ? never : T) => void;
|
|
14
|
+
var _a: (key: string) => boolean;
|
|
15
|
+
export var clear: () => void;
|
|
16
|
+
export { _a as delete };
|
|
17
|
+
}
|
|
18
|
+
/** @deprecated use query instead */
|
|
19
|
+
export declare const cache: typeof query;
|
|
20
|
+
export declare function hashKey<T extends Array<any>>(args: T): string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
|
|
2
|
+
import { getRequestEvent, isServer } from "solid-js/web";
|
|
3
|
+
import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
|
|
4
|
+
const LocationHeader = "Location";
|
|
5
|
+
const PRELOAD_TIMEOUT = 5000;
|
|
6
|
+
const CACHE_TIMEOUT = 180000;
|
|
7
|
+
let cacheMap = new Map();
|
|
8
|
+
// cleanup forward/back cache
|
|
9
|
+
if (!isServer) {
|
|
10
|
+
setInterval(() => {
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
for (let [k, v] of cacheMap.entries()) {
|
|
13
|
+
if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
|
|
14
|
+
cacheMap.delete(k);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}, 300000);
|
|
18
|
+
}
|
|
19
|
+
function getCache() {
|
|
20
|
+
if (!isServer)
|
|
21
|
+
return cacheMap;
|
|
22
|
+
const req = getRequestEvent();
|
|
23
|
+
if (!req)
|
|
24
|
+
throw new Error("Cannot find cache context");
|
|
25
|
+
return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
|
|
26
|
+
}
|
|
27
|
+
export function revalidate(key, force = true) {
|
|
28
|
+
return startTransition(() => {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
cacheKeyOp(key, entry => {
|
|
31
|
+
force && (entry[0] = 0); //force cache miss
|
|
32
|
+
entry[4][1](now); // retrigger live signals
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function cacheKeyOp(key, fn) {
|
|
37
|
+
key && !Array.isArray(key) && (key = [key]);
|
|
38
|
+
for (let k of cacheMap.keys()) {
|
|
39
|
+
if (key === undefined || matchKey(k, key))
|
|
40
|
+
fn(cacheMap.get(k));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function query(fn, name) {
|
|
44
|
+
// prioritize GET for server functions
|
|
45
|
+
if (fn.GET)
|
|
46
|
+
fn = fn.GET;
|
|
47
|
+
const cachedFn = ((...args) => {
|
|
48
|
+
const cache = getCache();
|
|
49
|
+
const intent = getIntent();
|
|
50
|
+
const inPreloadFn = getInPreloadFn();
|
|
51
|
+
const owner = getOwner();
|
|
52
|
+
const navigate = owner ? useNavigate() : undefined;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const key = name + hashKey(args);
|
|
55
|
+
let cached = cache.get(key);
|
|
56
|
+
let tracking;
|
|
57
|
+
if (isServer) {
|
|
58
|
+
const e = getRequestEvent();
|
|
59
|
+
if (e) {
|
|
60
|
+
const dataOnly = (e.router || (e.router = {})).dataOnly;
|
|
61
|
+
if (dataOnly) {
|
|
62
|
+
const data = e && (e.router.data || (e.router.data = {}));
|
|
63
|
+
if (data && key in data)
|
|
64
|
+
return data[key];
|
|
65
|
+
if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
|
|
66
|
+
data[key] = undefined;
|
|
67
|
+
return Promise.resolve();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (getListener() && !isServer) {
|
|
73
|
+
tracking = true;
|
|
74
|
+
onCleanup(() => cached[4].count--);
|
|
75
|
+
}
|
|
76
|
+
if (cached &&
|
|
77
|
+
cached[0] &&
|
|
78
|
+
(isServer ||
|
|
79
|
+
intent === "native" ||
|
|
80
|
+
cached[4].count ||
|
|
81
|
+
Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
|
|
82
|
+
if (tracking) {
|
|
83
|
+
cached[4].count++;
|
|
84
|
+
cached[4][0](); // track
|
|
85
|
+
}
|
|
86
|
+
if (cached[3] === "preload" && intent !== "preload") {
|
|
87
|
+
cached[0] = now;
|
|
88
|
+
}
|
|
89
|
+
let res = cached[1];
|
|
90
|
+
if (intent !== "preload") {
|
|
91
|
+
res =
|
|
92
|
+
"then" in cached[1]
|
|
93
|
+
? cached[1].then(handleResponse(false), handleResponse(true))
|
|
94
|
+
: handleResponse(false)(cached[1]);
|
|
95
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
96
|
+
}
|
|
97
|
+
inPreloadFn && "then" in res && res.catch(() => { });
|
|
98
|
+
return res;
|
|
99
|
+
}
|
|
100
|
+
let res = !isServer && sharedConfig.context && sharedConfig.has(key)
|
|
101
|
+
? sharedConfig.load(key) // hydrating
|
|
102
|
+
: fn(...args);
|
|
103
|
+
if (cached) {
|
|
104
|
+
cached[0] = now;
|
|
105
|
+
cached[1] = res;
|
|
106
|
+
cached[3] = intent;
|
|
107
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
cache.set(key, (cached = [now, res, , intent, createSignal(now)]));
|
|
111
|
+
cached[4].count = 0;
|
|
112
|
+
}
|
|
113
|
+
if (tracking) {
|
|
114
|
+
cached[4].count++;
|
|
115
|
+
cached[4][0](); // track
|
|
116
|
+
}
|
|
117
|
+
if (isServer) {
|
|
118
|
+
const e = getRequestEvent();
|
|
119
|
+
if (e && e.router.dataOnly)
|
|
120
|
+
return (e.router.data[key] = res);
|
|
121
|
+
}
|
|
122
|
+
if (intent !== "preload") {
|
|
123
|
+
res =
|
|
124
|
+
"then" in res
|
|
125
|
+
? res.then(handleResponse(false), handleResponse(true))
|
|
126
|
+
: handleResponse(false)(res);
|
|
127
|
+
}
|
|
128
|
+
inPreloadFn && "then" in res && res.catch(() => { });
|
|
129
|
+
// serialize on server
|
|
130
|
+
if (isServer &&
|
|
131
|
+
sharedConfig.context &&
|
|
132
|
+
sharedConfig.context.async &&
|
|
133
|
+
!sharedConfig.context.noHydrate) {
|
|
134
|
+
const e = getRequestEvent();
|
|
135
|
+
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
136
|
+
}
|
|
137
|
+
return res;
|
|
138
|
+
function handleResponse(error) {
|
|
139
|
+
return async (v) => {
|
|
140
|
+
if (v instanceof Response) {
|
|
141
|
+
const url = v.headers.get(LocationHeader);
|
|
142
|
+
if (url !== null) {
|
|
143
|
+
// client + server relative redirect
|
|
144
|
+
if (navigate && url.startsWith("/"))
|
|
145
|
+
startTransition(() => {
|
|
146
|
+
navigate(url, { replace: true });
|
|
147
|
+
});
|
|
148
|
+
else if (!isServer)
|
|
149
|
+
window.location.href = url;
|
|
150
|
+
else if (isServer) {
|
|
151
|
+
const e = getRequestEvent();
|
|
152
|
+
if (e)
|
|
153
|
+
e.response = { status: 302, headers: new Headers({ Location: url }) };
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (v.customBody)
|
|
158
|
+
v = await v.customBody();
|
|
159
|
+
}
|
|
160
|
+
if (error)
|
|
161
|
+
throw v;
|
|
162
|
+
cached[2] = v;
|
|
163
|
+
return v;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
cachedFn.keyFor = (...args) => name + hashKey(args);
|
|
168
|
+
cachedFn.key = name;
|
|
169
|
+
return cachedFn;
|
|
170
|
+
}
|
|
171
|
+
query.get = (key) => {
|
|
172
|
+
const cached = getCache().get(key);
|
|
173
|
+
return cached[2];
|
|
174
|
+
};
|
|
175
|
+
query.set = (key, value) => {
|
|
176
|
+
const cache = getCache();
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
let cached = cache.get(key);
|
|
179
|
+
if (cached) {
|
|
180
|
+
cached[0] = now;
|
|
181
|
+
cached[1] = Promise.resolve(value);
|
|
182
|
+
cached[2] = value;
|
|
183
|
+
cached[3] = "preload";
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]));
|
|
187
|
+
cached[4].count = 0;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
query.delete = (key) => getCache().delete(key);
|
|
191
|
+
query.clear = () => getCache().clear();
|
|
192
|
+
/** @deprecated use query instead */
|
|
193
|
+
export const cache = query;
|
|
194
|
+
function matchKey(key, keys) {
|
|
195
|
+
for (let k of keys) {
|
|
196
|
+
if (k && key.startsWith(k))
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
// Modified from the amazing Tanstack Query library (MIT)
|
|
202
|
+
// https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L168
|
|
203
|
+
export function hashKey(args) {
|
|
204
|
+
return JSON.stringify(args, (_, val) => isPlainObject(val)
|
|
205
|
+
? Object.keys(val)
|
|
206
|
+
.sort()
|
|
207
|
+
.reduce((result, key) => {
|
|
208
|
+
result[key] = val[key];
|
|
209
|
+
return result;
|
|
210
|
+
}, {})
|
|
211
|
+
: val);
|
|
212
|
+
}
|
|
213
|
+
function isPlainObject(obj) {
|
|
214
|
+
let proto;
|
|
215
|
+
return (obj != null &&
|
|
216
|
+
typeof obj === "object" &&
|
|
217
|
+
(!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype));
|
|
218
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -894,7 +894,7 @@ if (!isServer) {
|
|
|
894
894
|
setInterval(() => {
|
|
895
895
|
const now = Date.now();
|
|
896
896
|
for (let [k, v] of cacheMap.entries()) {
|
|
897
|
-
if (!v[
|
|
897
|
+
if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
|
|
898
898
|
cacheMap.delete(k);
|
|
899
899
|
}
|
|
900
900
|
}
|
|
@@ -911,7 +911,7 @@ function revalidate(key, force = true) {
|
|
|
911
911
|
const now = Date.now();
|
|
912
912
|
cacheKeyOp(key, entry => {
|
|
913
913
|
force && (entry[0] = 0); //force cache miss
|
|
914
|
-
entry[
|
|
914
|
+
entry[4][1](now); // retrigger live signals
|
|
915
915
|
});
|
|
916
916
|
});
|
|
917
917
|
}
|
|
@@ -921,7 +921,7 @@ function cacheKeyOp(key, fn) {
|
|
|
921
921
|
if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k));
|
|
922
922
|
}
|
|
923
923
|
}
|
|
924
|
-
function
|
|
924
|
+
function query(fn, name) {
|
|
925
925
|
// prioritize GET for server functions
|
|
926
926
|
if (fn.GET) fn = fn.GET;
|
|
927
927
|
const cachedFn = (...args) => {
|
|
@@ -950,20 +950,20 @@ function cache(fn, name) {
|
|
|
950
950
|
}
|
|
951
951
|
if (getListener() && !isServer) {
|
|
952
952
|
tracking = true;
|
|
953
|
-
onCleanup(() => cached[
|
|
953
|
+
onCleanup(() => cached[4].count--);
|
|
954
954
|
}
|
|
955
|
-
if (cached && cached[0] && (isServer || intent === "native" || cached[
|
|
955
|
+
if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
|
|
956
956
|
if (tracking) {
|
|
957
|
-
cached[
|
|
958
|
-
cached[
|
|
957
|
+
cached[4].count++;
|
|
958
|
+
cached[4][0](); // track
|
|
959
959
|
}
|
|
960
|
-
if (cached[
|
|
960
|
+
if (cached[3] === "preload" && intent !== "preload") {
|
|
961
961
|
cached[0] = now;
|
|
962
962
|
}
|
|
963
963
|
let res = cached[1];
|
|
964
964
|
if (intent !== "preload") {
|
|
965
965
|
res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
|
|
966
|
-
!isServer && intent === "navigate" && startTransition(() => cached[
|
|
966
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
967
967
|
}
|
|
968
968
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
969
969
|
return res;
|
|
@@ -973,15 +973,15 @@ function cache(fn, name) {
|
|
|
973
973
|
if (cached) {
|
|
974
974
|
cached[0] = now;
|
|
975
975
|
cached[1] = res;
|
|
976
|
-
cached[
|
|
977
|
-
!isServer && intent === "navigate" && startTransition(() => cached[
|
|
976
|
+
cached[3] = intent;
|
|
977
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
978
978
|
} else {
|
|
979
|
-
cache.set(key, cached = [now, res
|
|
980
|
-
cached[
|
|
979
|
+
cache.set(key, cached = [now, res,, intent, createSignal(now)]);
|
|
980
|
+
cached[4].count = 0;
|
|
981
981
|
}
|
|
982
982
|
if (tracking) {
|
|
983
|
-
cached[
|
|
984
|
-
cached[
|
|
983
|
+
cached[4].count++;
|
|
984
|
+
cached[4][0](); // track
|
|
985
985
|
}
|
|
986
986
|
if (isServer) {
|
|
987
987
|
const e = getRequestEvent();
|
|
@@ -1021,6 +1021,7 @@ function cache(fn, name) {
|
|
|
1021
1021
|
if (v.customBody) v = await v.customBody();
|
|
1022
1022
|
}
|
|
1023
1023
|
if (error) throw v;
|
|
1024
|
+
cached[2] = v;
|
|
1024
1025
|
return v;
|
|
1025
1026
|
};
|
|
1026
1027
|
}
|
|
@@ -1029,20 +1030,29 @@ function cache(fn, name) {
|
|
|
1029
1030
|
cachedFn.key = name;
|
|
1030
1031
|
return cachedFn;
|
|
1031
1032
|
}
|
|
1032
|
-
|
|
1033
|
+
query.get = key => {
|
|
1034
|
+
const cached = getCache().get(key);
|
|
1035
|
+
return cached[2];
|
|
1036
|
+
};
|
|
1037
|
+
query.set = (key, value) => {
|
|
1033
1038
|
const cache = getCache();
|
|
1034
1039
|
const now = Date.now();
|
|
1035
1040
|
let cached = cache.get(key);
|
|
1036
1041
|
if (cached) {
|
|
1037
1042
|
cached[0] = now;
|
|
1038
|
-
cached[1] = value;
|
|
1039
|
-
cached[2] =
|
|
1043
|
+
cached[1] = Promise.resolve(value);
|
|
1044
|
+
cached[2] = value;
|
|
1045
|
+
cached[3] = "preload";
|
|
1040
1046
|
} else {
|
|
1041
|
-
cache.set(key, cached = [now, value
|
|
1042
|
-
cached[
|
|
1047
|
+
cache.set(key, cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]);
|
|
1048
|
+
cached[4].count = 0;
|
|
1043
1049
|
}
|
|
1044
1050
|
};
|
|
1045
|
-
|
|
1051
|
+
query.delete = key => getCache().delete(key);
|
|
1052
|
+
query.clear = () => getCache().clear();
|
|
1053
|
+
|
|
1054
|
+
/** @deprecated use query instead */
|
|
1055
|
+
const cache = query;
|
|
1046
1056
|
function matchKey(key, keys) {
|
|
1047
1057
|
for (let k of keys) {
|
|
1048
1058
|
if (k && key.startsWith(k)) return true;
|
|
@@ -1093,7 +1103,7 @@ function useAction(action) {
|
|
|
1093
1103
|
r
|
|
1094
1104
|
}, args);
|
|
1095
1105
|
}
|
|
1096
|
-
function action(fn,
|
|
1106
|
+
function action(fn, options = {}) {
|
|
1097
1107
|
function mutate(...variables) {
|
|
1098
1108
|
const router = this.r;
|
|
1099
1109
|
const form = this.f;
|
|
@@ -1107,6 +1117,17 @@ function action(fn, name) {
|
|
|
1107
1117
|
function handler(error) {
|
|
1108
1118
|
return async res => {
|
|
1109
1119
|
const result = await handleResponse(res, error, router.navigatorFactory());
|
|
1120
|
+
let retry = null;
|
|
1121
|
+
!o.onComplete?.({
|
|
1122
|
+
...submission,
|
|
1123
|
+
result: result?.data,
|
|
1124
|
+
error: result?.error,
|
|
1125
|
+
pending: false,
|
|
1126
|
+
retry() {
|
|
1127
|
+
return retry = submission.retry();
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
if (retry) return retry;
|
|
1110
1131
|
if (!result) return submission.clear();
|
|
1111
1132
|
setResult(result);
|
|
1112
1133
|
if (result.error && !form) throw result.error;
|
|
@@ -1126,7 +1147,7 @@ function action(fn, name) {
|
|
|
1126
1147
|
return !result();
|
|
1127
1148
|
},
|
|
1128
1149
|
clear() {
|
|
1129
|
-
router.submissions[1](v => v.filter(i => i
|
|
1150
|
+
router.submissions[1](v => v.filter(i => i !== submission));
|
|
1130
1151
|
},
|
|
1131
1152
|
retry() {
|
|
1132
1153
|
setResult(undefined);
|
|
@@ -1136,7 +1157,10 @@ function action(fn, name) {
|
|
|
1136
1157
|
}]);
|
|
1137
1158
|
return p.then(handler(), handler(true));
|
|
1138
1159
|
}
|
|
1139
|
-
const
|
|
1160
|
+
const o = typeof options === "string" ? {
|
|
1161
|
+
name: options
|
|
1162
|
+
} : options;
|
|
1163
|
+
const url = fn.url || o.name && `https://action/${o.name}` || (!isServer ? `https://action/${hashString(fn.toString())}` : "");
|
|
1140
1164
|
mutate.base = url;
|
|
1141
1165
|
return toAction(mutate, url);
|
|
1142
1166
|
}
|
|
@@ -1191,7 +1215,7 @@ async function handleResponse(response, error, navigate) {
|
|
|
1191
1215
|
// invalidate
|
|
1192
1216
|
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
1193
1217
|
// set cache
|
|
1194
|
-
flightKeys && flightKeys.forEach(k =>
|
|
1218
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
1195
1219
|
// trigger revalidation
|
|
1196
1220
|
await revalidate(keys, false);
|
|
1197
1221
|
return data != null ? {
|
|
@@ -1658,4 +1682,4 @@ function json(data, init = {}) {
|
|
|
1658
1682
|
return response;
|
|
1659
1683
|
}
|
|
1660
1684
|
|
|
1661
|
-
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|
|
1685
|
+
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|
package/dist/types.d.ts
CHANGED
|
@@ -183,7 +183,7 @@ export type SubmissionStub = {
|
|
|
183
183
|
export interface MaybePreloadableComponent extends Component {
|
|
184
184
|
preload?: () => void;
|
|
185
185
|
}
|
|
186
|
-
export type CacheEntry = [number, any, Intent | undefined, Signal<number> & {
|
|
186
|
+
export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & {
|
|
187
187
|
count: number;
|
|
188
188
|
}];
|
|
189
189
|
export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
|