@solidjs/router 0.14.9 → 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 +10 -6
- package/dist/data/action.js +23 -8
- 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 +66 -30
- package/dist/routing.d.ts +2 -2
- package/dist/types.d.ts +5 -3
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +19 -3
- 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
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { JSX } from "solid-js";
|
|
2
2
|
import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
|
|
3
|
-
export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
|
|
3
|
+
export type Action<T extends Array<any>, U, V = T> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
|
|
4
4
|
url: string;
|
|
5
|
-
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U>;
|
|
5
|
+
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U, V>;
|
|
6
6
|
};
|
|
7
|
-
export declare const actions: Map<string, Action<any, any>>;
|
|
8
|
-
export declare function useSubmissions<T extends Array<any>, U>(fn: Action<T, U>, filter?: (
|
|
7
|
+
export declare const actions: Map<string, Action<any, any, any>>;
|
|
8
|
+
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>>[] & {
|
|
9
9
|
pending: boolean;
|
|
10
10
|
};
|
|
11
|
-
export declare function useSubmission<T extends Array<any>, U>(fn: Action<T, U>, filter?: (
|
|
12
|
-
export declare function useAction<T extends Array<any>, U>(action: Action<T, U>): (...args: Parameters<Action<T, U>>) => Promise<NarrowResponse<U>>;
|
|
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
|
+
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
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,11 +2,11 @@ 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();
|
|
9
|
-
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.
|
|
9
|
+
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
10
10
|
return new Proxy([], {
|
|
11
11
|
get(_, property) {
|
|
12
12
|
if (property === $TRACK)
|
|
@@ -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,9 +92,11 @@ 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())}` : "");
|
|
99
|
+
mutate.base = url;
|
|
86
100
|
return toAction(mutate, url);
|
|
87
101
|
}
|
|
88
102
|
function toAction(fn, url) {
|
|
@@ -95,6 +109,7 @@ function toAction(fn, url) {
|
|
|
95
109
|
const newFn = function (...passedArgs) {
|
|
96
110
|
return fn.call(this, ...args, ...passedArgs);
|
|
97
111
|
};
|
|
112
|
+
newFn.base = fn.base;
|
|
98
113
|
const uri = new URL(url, mockBase);
|
|
99
114
|
uri.searchParams.set("args", hashKey(args));
|
|
100
115
|
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
|
|
@@ -140,7 +155,7 @@ async function handleResponse(response, error, navigate) {
|
|
|
140
155
|
// invalidate
|
|
141
156
|
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
142
157
|
// set cache
|
|
143
|
-
flightKeys && flightKeys.forEach(k =>
|
|
158
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
144
159
|
// trigger revalidation
|
|
145
160
|
await revalidate(keys, false);
|
|
146
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
|
@@ -111,7 +111,9 @@ function joinPaths(from, to) {
|
|
|
111
111
|
function extractSearchParams(url) {
|
|
112
112
|
const params = {};
|
|
113
113
|
url.searchParams.forEach((value, key) => {
|
|
114
|
-
|
|
114
|
+
if (key in params) {
|
|
115
|
+
if (Array.isArray(params[key])) params[key].push(value);else params[key] = [params[key], value];
|
|
116
|
+
} else params[key] = value;
|
|
115
117
|
});
|
|
116
118
|
return params;
|
|
117
119
|
}
|
|
@@ -197,10 +199,18 @@ function createMemoObject(fn) {
|
|
|
197
199
|
function mergeSearchString(search, params) {
|
|
198
200
|
const merged = new URLSearchParams(search);
|
|
199
201
|
Object.entries(params).forEach(([key, value]) => {
|
|
200
|
-
if (value == null || value === "") {
|
|
202
|
+
if (value == null || value === "" || value instanceof Array && !value.length) {
|
|
201
203
|
merged.delete(key);
|
|
202
204
|
} else {
|
|
203
|
-
|
|
205
|
+
if (value instanceof Array) {
|
|
206
|
+
// Delete all instances of the key before appending
|
|
207
|
+
merged.delete(key);
|
|
208
|
+
value.forEach(v => {
|
|
209
|
+
merged.append(key, String(v));
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
merged.set(key, String(value));
|
|
213
|
+
}
|
|
204
214
|
}
|
|
205
215
|
});
|
|
206
216
|
const s = merged.toString();
|
|
@@ -884,7 +894,7 @@ if (!isServer) {
|
|
|
884
894
|
setInterval(() => {
|
|
885
895
|
const now = Date.now();
|
|
886
896
|
for (let [k, v] of cacheMap.entries()) {
|
|
887
|
-
if (!v[
|
|
897
|
+
if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
|
|
888
898
|
cacheMap.delete(k);
|
|
889
899
|
}
|
|
890
900
|
}
|
|
@@ -901,7 +911,7 @@ function revalidate(key, force = true) {
|
|
|
901
911
|
const now = Date.now();
|
|
902
912
|
cacheKeyOp(key, entry => {
|
|
903
913
|
force && (entry[0] = 0); //force cache miss
|
|
904
|
-
entry[
|
|
914
|
+
entry[4][1](now); // retrigger live signals
|
|
905
915
|
});
|
|
906
916
|
});
|
|
907
917
|
}
|
|
@@ -911,7 +921,7 @@ function cacheKeyOp(key, fn) {
|
|
|
911
921
|
if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k));
|
|
912
922
|
}
|
|
913
923
|
}
|
|
914
|
-
function
|
|
924
|
+
function query(fn, name) {
|
|
915
925
|
// prioritize GET for server functions
|
|
916
926
|
if (fn.GET) fn = fn.GET;
|
|
917
927
|
const cachedFn = (...args) => {
|
|
@@ -940,20 +950,20 @@ function cache(fn, name) {
|
|
|
940
950
|
}
|
|
941
951
|
if (getListener() && !isServer) {
|
|
942
952
|
tracking = true;
|
|
943
|
-
onCleanup(() => cached[
|
|
953
|
+
onCleanup(() => cached[4].count--);
|
|
944
954
|
}
|
|
945
|
-
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)) {
|
|
946
956
|
if (tracking) {
|
|
947
|
-
cached[
|
|
948
|
-
cached[
|
|
957
|
+
cached[4].count++;
|
|
958
|
+
cached[4][0](); // track
|
|
949
959
|
}
|
|
950
|
-
if (cached[
|
|
960
|
+
if (cached[3] === "preload" && intent !== "preload") {
|
|
951
961
|
cached[0] = now;
|
|
952
962
|
}
|
|
953
963
|
let res = cached[1];
|
|
954
964
|
if (intent !== "preload") {
|
|
955
965
|
res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
|
|
956
|
-
!isServer && intent === "navigate" && startTransition(() => cached[
|
|
966
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
957
967
|
}
|
|
958
968
|
inPreloadFn && "then" in res && res.catch(() => {});
|
|
959
969
|
return res;
|
|
@@ -963,15 +973,15 @@ function cache(fn, name) {
|
|
|
963
973
|
if (cached) {
|
|
964
974
|
cached[0] = now;
|
|
965
975
|
cached[1] = res;
|
|
966
|
-
cached[
|
|
967
|
-
!isServer && intent === "navigate" && startTransition(() => cached[
|
|
976
|
+
cached[3] = intent;
|
|
977
|
+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
|
|
968
978
|
} else {
|
|
969
|
-
cache.set(key, cached = [now, res
|
|
970
|
-
cached[
|
|
979
|
+
cache.set(key, cached = [now, res,, intent, createSignal(now)]);
|
|
980
|
+
cached[4].count = 0;
|
|
971
981
|
}
|
|
972
982
|
if (tracking) {
|
|
973
|
-
cached[
|
|
974
|
-
cached[
|
|
983
|
+
cached[4].count++;
|
|
984
|
+
cached[4][0](); // track
|
|
975
985
|
}
|
|
976
986
|
if (isServer) {
|
|
977
987
|
const e = getRequestEvent();
|
|
@@ -1011,6 +1021,7 @@ function cache(fn, name) {
|
|
|
1011
1021
|
if (v.customBody) v = await v.customBody();
|
|
1012
1022
|
}
|
|
1013
1023
|
if (error) throw v;
|
|
1024
|
+
cached[2] = v;
|
|
1014
1025
|
return v;
|
|
1015
1026
|
};
|
|
1016
1027
|
}
|
|
@@ -1019,20 +1030,29 @@ function cache(fn, name) {
|
|
|
1019
1030
|
cachedFn.key = name;
|
|
1020
1031
|
return cachedFn;
|
|
1021
1032
|
}
|
|
1022
|
-
|
|
1033
|
+
query.get = key => {
|
|
1034
|
+
const cached = getCache().get(key);
|
|
1035
|
+
return cached[2];
|
|
1036
|
+
};
|
|
1037
|
+
query.set = (key, value) => {
|
|
1023
1038
|
const cache = getCache();
|
|
1024
1039
|
const now = Date.now();
|
|
1025
1040
|
let cached = cache.get(key);
|
|
1026
1041
|
if (cached) {
|
|
1027
1042
|
cached[0] = now;
|
|
1028
|
-
cached[1] = value;
|
|
1029
|
-
cached[2] =
|
|
1043
|
+
cached[1] = Promise.resolve(value);
|
|
1044
|
+
cached[2] = value;
|
|
1045
|
+
cached[3] = "preload";
|
|
1030
1046
|
} else {
|
|
1031
|
-
cache.set(key, cached = [now, value
|
|
1032
|
-
cached[
|
|
1047
|
+
cache.set(key, cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]);
|
|
1048
|
+
cached[4].count = 0;
|
|
1033
1049
|
}
|
|
1034
1050
|
};
|
|
1035
|
-
|
|
1051
|
+
query.delete = key => getCache().delete(key);
|
|
1052
|
+
query.clear = () => getCache().clear();
|
|
1053
|
+
|
|
1054
|
+
/** @deprecated use query instead */
|
|
1055
|
+
const cache = query;
|
|
1036
1056
|
function matchKey(key, keys) {
|
|
1037
1057
|
for (let k of keys) {
|
|
1038
1058
|
if (k && key.startsWith(k)) return true;
|
|
@@ -1056,7 +1076,7 @@ function isPlainObject(obj) {
|
|
|
1056
1076
|
const actions = /* #__PURE__ */new Map();
|
|
1057
1077
|
function useSubmissions(fn, filter) {
|
|
1058
1078
|
const router = useRouter();
|
|
1059
|
-
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.
|
|
1079
|
+
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
1060
1080
|
return new Proxy([], {
|
|
1061
1081
|
get(_, property) {
|
|
1062
1082
|
if (property === $TRACK) return subs();
|
|
@@ -1083,7 +1103,7 @@ function useAction(action) {
|
|
|
1083
1103
|
r
|
|
1084
1104
|
}, args);
|
|
1085
1105
|
}
|
|
1086
|
-
function action(fn,
|
|
1106
|
+
function action(fn, options = {}) {
|
|
1087
1107
|
function mutate(...variables) {
|
|
1088
1108
|
const router = this.r;
|
|
1089
1109
|
const form = this.f;
|
|
@@ -1097,6 +1117,17 @@ function action(fn, name) {
|
|
|
1097
1117
|
function handler(error) {
|
|
1098
1118
|
return async res => {
|
|
1099
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;
|
|
1100
1131
|
if (!result) return submission.clear();
|
|
1101
1132
|
setResult(result);
|
|
1102
1133
|
if (result.error && !form) throw result.error;
|
|
@@ -1116,7 +1147,7 @@ function action(fn, name) {
|
|
|
1116
1147
|
return !result();
|
|
1117
1148
|
},
|
|
1118
1149
|
clear() {
|
|
1119
|
-
router.submissions[1](v => v.filter(i => i
|
|
1150
|
+
router.submissions[1](v => v.filter(i => i !== submission));
|
|
1120
1151
|
},
|
|
1121
1152
|
retry() {
|
|
1122
1153
|
setResult(undefined);
|
|
@@ -1126,7 +1157,11 @@ function action(fn, name) {
|
|
|
1126
1157
|
}]);
|
|
1127
1158
|
return p.then(handler(), handler(true));
|
|
1128
1159
|
}
|
|
1129
|
-
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())}` : "");
|
|
1164
|
+
mutate.base = url;
|
|
1130
1165
|
return toAction(mutate, url);
|
|
1131
1166
|
}
|
|
1132
1167
|
function toAction(fn, url) {
|
|
@@ -1138,6 +1173,7 @@ function toAction(fn, url) {
|
|
|
1138
1173
|
const newFn = function (...passedArgs) {
|
|
1139
1174
|
return fn.call(this, ...args, ...passedArgs);
|
|
1140
1175
|
};
|
|
1176
|
+
newFn.base = fn.base;
|
|
1141
1177
|
const uri = new URL(url, mockBase);
|
|
1142
1178
|
uri.searchParams.set("args", hashKey(args));
|
|
1143
1179
|
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
|
|
@@ -1179,7 +1215,7 @@ async function handleResponse(response, error, navigate) {
|
|
|
1179
1215
|
// invalidate
|
|
1180
1216
|
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
1181
1217
|
// set cache
|
|
1182
|
-
flightKeys && flightKeys.forEach(k =>
|
|
1218
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
1183
1219
|
// trigger revalidation
|
|
1184
1220
|
await revalidate(keys, false);
|
|
1185
1221
|
return data != null ? {
|
|
@@ -1646,4 +1682,4 @@ function json(data, init = {}) {
|
|
|
1646
1682
|
return response;
|
|
1647
1683
|
}
|
|
1648
1684
|
|
|
1649
|
-
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/routing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JSX, Accessor } from "solid-js";
|
|
2
|
-
import type { BeforeLeaveEventArgs, Branch, Intent, Location, MatchFilters, NavigateOptions, Navigator, Params, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouterContext, RouterIntegration,
|
|
2
|
+
import type { BeforeLeaveEventArgs, Branch, Intent, Location, MatchFilters, NavigateOptions, Navigator, Params, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SearchParams, SetSearchParams } from "./types.js";
|
|
3
3
|
export declare const RouterContextObj: import("solid-js").Context<RouterContext | undefined>;
|
|
4
4
|
export declare const RouteContextObj: import("solid-js").Context<RouteContext | undefined>;
|
|
5
5
|
export declare const useRouter: () => RouterContext;
|
|
@@ -15,7 +15,7 @@ export declare const usePreloadRoute: () => (url: string | URL, options?: {
|
|
|
15
15
|
export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S> | undefined) => Accessor<import("./types.js").PathMatch | undefined>;
|
|
16
16
|
export declare const useCurrentMatches: () => () => RouteMatch[];
|
|
17
17
|
export declare const useParams: <T extends Params>() => T;
|
|
18
|
-
export declare const useSearchParams: <T extends
|
|
18
|
+
export declare const useSearchParams: <T extends SearchParams>() => [Partial<T>, (params: SetSearchParams, options?: Partial<NavigateOptions>) => void];
|
|
19
19
|
export declare const useBeforeLeave: (listener: (e: BeforeLeaveEventArgs) => void) => void;
|
|
20
20
|
export declare function createRoutes(routeDef: RouteDefinition, base?: string): RouteDescription[];
|
|
21
21
|
export declare function createBranch(routes: RouteDescription[], index?: number): Branch;
|
package/dist/types.d.ts
CHANGED
|
@@ -22,14 +22,16 @@ declare module "solid-js/web" {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
export type Params = Record<string, string>;
|
|
25
|
+
export type SearchParams = Record<string, string | string[]>;
|
|
25
26
|
export type SetParams = Record<string, string | number | boolean | null | undefined>;
|
|
27
|
+
export type SetSearchParams = Record<string, string | string[] | number | number[] | boolean | boolean[] | null | undefined>;
|
|
26
28
|
export interface Path {
|
|
27
29
|
pathname: string;
|
|
28
30
|
search: string;
|
|
29
31
|
hash: string;
|
|
30
32
|
}
|
|
31
33
|
export interface Location<S = unknown> extends Path {
|
|
32
|
-
query:
|
|
34
|
+
query: SearchParams;
|
|
33
35
|
state: Readonly<Partial<S>> | null;
|
|
34
36
|
key: string;
|
|
35
37
|
}
|
|
@@ -127,7 +129,7 @@ export interface RouterUtils {
|
|
|
127
129
|
go(delta: number): void;
|
|
128
130
|
beforeLeave: BeforeLeaveLifecycle;
|
|
129
131
|
paramsWrapper: (getParams: () => Params, branches: () => Branch[]) => Params;
|
|
130
|
-
queryWrapper: (getQuery: () =>
|
|
132
|
+
queryWrapper: (getQuery: () => SearchParams) => SearchParams;
|
|
131
133
|
}
|
|
132
134
|
export interface RouterContext {
|
|
133
135
|
base: RouteContext;
|
|
@@ -181,7 +183,7 @@ export type SubmissionStub = {
|
|
|
181
183
|
export interface MaybePreloadableComponent extends Component {
|
|
182
184
|
preload?: () => void;
|
|
183
185
|
}
|
|
184
|
-
export type CacheEntry = [number, any, Intent | undefined, Signal<number> & {
|
|
186
|
+
export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & {
|
|
185
187
|
count: number;
|
|
186
188
|
}];
|
|
187
189
|
export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { MatchFilters,
|
|
1
|
+
import type { MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.ts";
|
|
2
2
|
export declare const mockBase = "http://sr";
|
|
3
3
|
export declare function normalizePath(path: string, omitSlash?: boolean): string;
|
|
4
4
|
export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
|
|
5
5
|
export declare function invariant<T>(value: T | null | undefined, message: string): T;
|
|
6
6
|
export declare function joinPaths(from: string, to: string): string;
|
|
7
|
-
export declare function extractSearchParams(url: URL):
|
|
7
|
+
export declare function extractSearchParams(url: URL): SearchParams;
|
|
8
8
|
export declare function createMatcher<S extends string>(path: S, partial?: boolean, matchFilters?: MatchFilters<S>): (location: string) => PathMatch | null;
|
|
9
9
|
export declare function scoreRoute(route: RouteDescription): number;
|
|
10
10
|
export declare function createMemoObject<T extends Record<string | symbol, unknown>>(fn: () => T): T;
|
|
11
|
-
export declare function mergeSearchString(search: string, params:
|
|
11
|
+
export declare function mergeSearchString(search: string, params: SetSearchParams): string;
|
|
12
12
|
export declare function expandOptionals(pattern: string): string[];
|
package/dist/utils.js
CHANGED
|
@@ -36,7 +36,14 @@ export function joinPaths(from, to) {
|
|
|
36
36
|
export function extractSearchParams(url) {
|
|
37
37
|
const params = {};
|
|
38
38
|
url.searchParams.forEach((value, key) => {
|
|
39
|
-
|
|
39
|
+
if (key in params) {
|
|
40
|
+
if (Array.isArray(params[key]))
|
|
41
|
+
params[key].push(value);
|
|
42
|
+
else
|
|
43
|
+
params[key] = [params[key], value];
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
params[key] = value;
|
|
40
47
|
});
|
|
41
48
|
return params;
|
|
42
49
|
}
|
|
@@ -128,11 +135,20 @@ export function createMemoObject(fn) {
|
|
|
128
135
|
export function mergeSearchString(search, params) {
|
|
129
136
|
const merged = new URLSearchParams(search);
|
|
130
137
|
Object.entries(params).forEach(([key, value]) => {
|
|
131
|
-
if (value == null || value === "") {
|
|
138
|
+
if (value == null || value === "" || (value instanceof Array && !value.length)) {
|
|
132
139
|
merged.delete(key);
|
|
133
140
|
}
|
|
134
141
|
else {
|
|
135
|
-
|
|
142
|
+
if (value instanceof Array) {
|
|
143
|
+
// Delete all instances of the key before appending
|
|
144
|
+
merged.delete(key);
|
|
145
|
+
value.forEach(v => {
|
|
146
|
+
merged.append(key, String(v));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
merged.set(key, String(value));
|
|
151
|
+
}
|
|
136
152
|
}
|
|
137
153
|
});
|
|
138
154
|
const s = merged.toString();
|