@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 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
- ### `cache`
430
+ ### `query`
431
431
 
432
- To prevent duplicate fetching and to trigger handle refetching we provide a cache api. That takes a function and returns the same function.
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 = cache(async (id) => {
435
+ const getUser = query(async (id) => {
436
436
  return (await fetch(`/api/users${id}`)).json()
437
- }, "users") // used as cache key + serialized arguments
437
+ }, "users") // used as the query key + serialized arguments
438
438
  ```
439
- It is expected that the arguments to the cache function are serializable.
439
+ It is expected that the arguments to the query function are serializable.
440
440
 
441
- This cache accomplishes the following:
441
+ This query accomplishes the following:
442
442
 
443
- 1. It does just deduping on the server for the lifetime of the request.
444
- 2. It does 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.
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 it. Revalidation or new fetch updates the cache.
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 cache function
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 cache function
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 cache 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 cache (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
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
- `cache` can be defined anywhere and then used inside your components with:
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 `cache` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
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
- #### Notes of `<form>` implementation and SSR
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 cache/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.
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 = cache(() => {
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 = cache(async (id: number) => {
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/cache 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.
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
 
@@ -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?: (arg: T) => boolean): Submission<T, NarrowResponse<U>>[] & {
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?: (arg: T) => boolean): Submission<T, NarrowResponse<U>> | SubmissionStub;
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>;
@@ -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, cache } from "./cache.js";
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.toString() && (!filter || filter(s.input))));
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, name) {
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.input !== variables));
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 => cache.set(k, custom[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;
@@ -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 { cache, revalidate, type CachedFunction } from "./cache.js";
3
+ export { query, revalidate, cache, type CachedFunction } from "./query.js";
4
4
  export { redirect, reload, json } from "./response.js";
@@ -1,4 +1,4 @@
1
1
  export { createAsync, createAsyncStore } from "./createAsync.js";
2
2
  export { action, useSubmission, useSubmissions, useAction } from "./action.js";
3
- export { cache, revalidate } from "./cache.js";
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
- params[key] = value;
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
- merged.set(key, String(value));
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[3].count && now - v[0] > CACHE_TIMEOUT) {
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[3][1](now); // retrigger live signals
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 cache(fn, name) {
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[3].count--);
953
+ onCleanup(() => cached[4].count--);
944
954
  }
945
- if (cached && cached[0] && (isServer || intent === "native" || cached[3].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
955
+ if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
946
956
  if (tracking) {
947
- cached[3].count++;
948
- cached[3][0](); // track
957
+ cached[4].count++;
958
+ cached[4][0](); // track
949
959
  }
950
- if (cached[2] === "preload" && intent !== "preload") {
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[3][1](cached[0])); // update version
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[2] = intent;
967
- !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
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, intent, createSignal(now)]);
970
- cached[3].count = 0;
979
+ cache.set(key, cached = [now, res,, intent, createSignal(now)]);
980
+ cached[4].count = 0;
971
981
  }
972
982
  if (tracking) {
973
- cached[3].count++;
974
- cached[3][0](); // track
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
- cache.set = (key, value) => {
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] = "preload";
1043
+ cached[1] = Promise.resolve(value);
1044
+ cached[2] = value;
1045
+ cached[3] = "preload";
1030
1046
  } else {
1031
- cache.set(key, cached = [now, value,, createSignal(now)]);
1032
- cached[3].count = 0;
1047
+ cache.set(key, cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]);
1048
+ cached[4].count = 0;
1033
1049
  }
1034
1050
  };
1035
- cache.clear = () => getCache().clear();
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.toString() && (!filter || filter(s.input))));
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, name) {
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.input !== variables));
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 url = fn.url || name && `https://action/${name}` || (!isServer ? `https://action/${hashString(fn.toString())}` : "");
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 => cache.set(k, custom[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, SetParams } from "./types.js";
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 Params>() => [Partial<T>, (params: SetParams, options?: Partial<NavigateOptions>) => void];
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: Params;
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: () => Params) => Params;
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, Params, PathMatch, RouteDescription, SetParams } from "./types.ts";
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): Params;
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: SetParams): string;
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
- params[key] = value;
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
- merged.set(key, String(value));
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();
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.14.9",
9
+ "version": "0.15.0",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",