@solidjs/router 1.0.0-next.8 → 1.0.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 +715 -531
- package/dist/components.d.ts +31 -0
- package/dist/components.jsx +40 -0
- package/dist/data/action.d.ts +11 -40
- package/dist/data/action.js +100 -274
- package/dist/data/createAsync.d.ts +32 -0
- package/dist/data/createAsync.js +96 -0
- package/dist/data/events.d.ts +0 -8
- package/dist/data/events.js +23 -24
- package/dist/data/index.d.ts +4 -2
- package/dist/data/index.js +4 -2
- package/dist/data/query.d.ts +3 -1
- package/dist/data/query.js +22 -39
- package/dist/data/response.d.ts +4 -0
- package/dist/data/response.js +42 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1024 -1578
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +4 -29
- package/dist/lifecycle.js +37 -40
- package/dist/routers/HashRouter.d.ts +9 -0
- package/dist/routers/HashRouter.js +41 -0
- package/dist/routers/MemoryRouter.d.ts +24 -0
- package/dist/routers/MemoryRouter.js +57 -0
- package/dist/routers/Router.d.ts +17 -0
- package/dist/routers/Router.js +59 -0
- package/dist/routers/StaticRouter.d.ts +6 -0
- package/dist/routers/StaticRouter.js +15 -0
- package/dist/routers/components.d.ts +26 -12
- package/dist/routers/components.jsx +54 -68
- package/dist/routers/createRouter.d.ts +10 -0
- package/dist/routers/createRouter.js +41 -0
- package/dist/routers/index.d.ts +11 -4
- package/dist/routers/index.js +6 -2
- package/dist/routers/scrollRestoration.d.ts +32 -0
- package/dist/routers/scrollRestoration.js +96 -0
- package/dist/routing.d.ts +52 -88
- package/dist/routing.js +166 -381
- package/dist/types.d.ts +33 -83
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -2
- package/package.json +7 -11
- package/dist/claims.d.ts +0 -21
- package/dist/claims.js +0 -115
- package/dist/data/flash.d.ts +0 -21
- package/dist/data/flash.js +0 -78
- package/dist/data/flashCookie.d.ts +0 -7
- package/dist/data/flashCookie.js +0 -20
- package/dist/data/serverForms.d.ts +0 -1
- package/dist/data/serverForms.js +0 -5
- package/dist/paths.d.ts +0 -117
- package/dist/paths.js +0 -41
- package/dist/routers/factory.d.ts +0 -45
- package/dist/routers/factory.jsx +0 -143
- package/dist/routers/history.d.ts +0 -24
- package/dist/routers/history.js +0 -180
- package/dist/server.d.ts +0 -75
- package/dist/server.js +0 -264
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { Location, Navigator } from "./types.js";
|
|
3
|
+
declare module "solid-js" {
|
|
4
|
+
namespace JSX {
|
|
5
|
+
interface AnchorHTMLAttributes<T> {
|
|
6
|
+
state?: string;
|
|
7
|
+
noScroll?: boolean;
|
|
8
|
+
replace?: boolean;
|
|
9
|
+
preload?: boolean;
|
|
10
|
+
link?: boolean;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> {
|
|
15
|
+
href: string;
|
|
16
|
+
replace?: boolean | undefined;
|
|
17
|
+
noScroll?: boolean | undefined;
|
|
18
|
+
state?: unknown | undefined;
|
|
19
|
+
inactiveClass?: string | undefined;
|
|
20
|
+
activeClass?: string | undefined;
|
|
21
|
+
end?: boolean | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare function A(props: AnchorProps): JSX.Element;
|
|
24
|
+
export interface NavigateProps {
|
|
25
|
+
href: ((args: {
|
|
26
|
+
navigate: Navigator;
|
|
27
|
+
location: Location;
|
|
28
|
+
}) => string) | string;
|
|
29
|
+
state?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export declare function Navigate(props: NavigateProps): null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createMemo, mergeProps, splitProps } from "solid-js";
|
|
2
|
+
import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
|
|
3
|
+
import { normalizePath } from "./utils.js";
|
|
4
|
+
export function A(props) {
|
|
5
|
+
props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
|
|
6
|
+
const [, rest] = splitProps(props, [
|
|
7
|
+
"href",
|
|
8
|
+
"state",
|
|
9
|
+
"class",
|
|
10
|
+
"activeClass",
|
|
11
|
+
"inactiveClass",
|
|
12
|
+
"end"
|
|
13
|
+
]);
|
|
14
|
+
const to = useResolvedPath(() => props.href);
|
|
15
|
+
const href = useHref(to);
|
|
16
|
+
const location = useLocation();
|
|
17
|
+
const isActive = createMemo(() => {
|
|
18
|
+
const to_ = to();
|
|
19
|
+
if (to_ === undefined)
|
|
20
|
+
return [false, false];
|
|
21
|
+
// trailing slashes are ignored so `/route` and `/route/` share active state
|
|
22
|
+
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
|
|
23
|
+
const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
|
|
24
|
+
return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
|
|
25
|
+
});
|
|
26
|
+
return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
|
|
27
|
+
...(props.class && { [props.class]: true }),
|
|
28
|
+
[props.inactiveClass]: !isActive()[0],
|
|
29
|
+
[props.activeClass]: isActive()[0],
|
|
30
|
+
...rest.classList
|
|
31
|
+
}} link aria-current={isActive()[1] ? "page" : undefined}/>);
|
|
32
|
+
}
|
|
33
|
+
export function Navigate(props) {
|
|
34
|
+
const navigate = useNavigate();
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
const { href, state } = props;
|
|
37
|
+
const path = typeof href === "function" ? href({ navigate, location }) : href;
|
|
38
|
+
navigate(path, { replace: true, state });
|
|
39
|
+
return null;
|
|
40
|
+
}
|
package/dist/data/action.d.ts
CHANGED
|
@@ -1,46 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import { JSX } from "solid-js";
|
|
2
|
+
import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
|
|
3
3
|
export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
|
|
4
4
|
url: string;
|
|
5
5
|
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U, V>;
|
|
6
|
-
onSubmit(hook: (...args: V extends Array<any> ? V : T) => void): Action<T, U, V>;
|
|
7
|
-
onSettled(hook: (submission: Submission<V extends Array<any> ? V : T, NarrowResponse<U>>) => void): Action<T, U, V>;
|
|
8
|
-
};
|
|
9
|
-
type ActionFactory = {
|
|
10
|
-
<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
|
|
11
|
-
<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
|
|
12
|
-
name?: string;
|
|
13
|
-
}): Action<T, U>;
|
|
14
6
|
};
|
|
15
7
|
export declare const actions: Map<string, Action<any, any, any>>;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* first action is created on the client.
|
|
21
|
-
*/
|
|
22
|
-
export declare function handleFormAction(evt: SubmitEvent, router: RouterContext, actionBase: string): void;
|
|
23
|
-
/**
|
|
24
|
-
* Entry point for delegation's lazy fallback (data/events.ts): when no form
|
|
25
|
-
* handler was ever installed — no action module in the client graph at all —
|
|
26
|
-
* the router intercepts posts to server-action urls synchronously and loads
|
|
27
|
-
* this module to run them. The FormData was captured at submit time; only
|
|
28
|
-
* the enctype conversion and the generic invocation happen here.
|
|
29
|
-
*/
|
|
30
|
-
export declare function submitServerForm(router: RouterContext, url: string, form: HTMLFormElement, data: FormData): void;
|
|
31
|
-
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<V, NarrowResponse<U>>[];
|
|
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
|
+
pending: boolean;
|
|
10
|
+
};
|
|
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;
|
|
32
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>>;
|
|
33
|
-
export declare
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* folded payload here — fresh route data is seeded into the `query` cache
|
|
39
|
-
* and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
|
|
40
|
-
* applied, all before the action sees its plain return value. Called by the
|
|
41
|
-
* Router component on the client unless `singleFlight={false}`, which now
|
|
42
|
-
* simply means "never subscribe" — no consumer, no request header, no
|
|
43
|
-
* collection work on the server. Returns the unsubscribe function.
|
|
44
|
-
*/
|
|
45
|
-
export declare function setupFlightDataConsumer(router: RouterContext): () => void;
|
|
46
|
-
export {};
|
|
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>) => void;
|
|
17
|
+
}): Action<T, U>;
|
package/dist/data/action.js
CHANGED
|
@@ -1,119 +1,9 @@
|
|
|
1
|
-
import { $TRACK,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
|
|
5
|
-
import { setRouterFormHandler } from "./events.js";
|
|
6
|
-
import { decodeFlashCookie } from "./flash.js";
|
|
1
|
+
import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
|
|
2
|
+
import { isServer } from "solid-js/web";
|
|
3
|
+
import { useRouter } from "../routing.js";
|
|
7
4
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
8
5
|
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
9
|
-
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
10
|
-
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
11
|
-
const invokeSymbol = Symbol("routerActionInvoke");
|
|
12
|
-
// Forms submitted through delegation are marked `aria-busy` while their
|
|
13
|
-
// action is in flight — the form half of the attribute vocabulary links get
|
|
14
|
-
// (`data-active`/`data-pending`). Style with `form[aria-busy] button { ... }`.
|
|
15
|
-
// A counter (not a boolean) keeps the attribute through overlapping
|
|
16
|
-
// submissions from the same form.
|
|
17
|
-
const busyForms = /* #__PURE__ */ new WeakMap();
|
|
18
|
-
function setFormBusy(form, delta) {
|
|
19
|
-
const count = (busyForms.get(form) || 0) + delta;
|
|
20
|
-
busyForms.set(form, count);
|
|
21
|
-
count > 0 ? form.setAttribute("aria-busy", "true") : form.removeAttribute("aria-busy");
|
|
22
|
-
}
|
|
23
6
|
export const actions = /* #__PURE__ */ new Map();
|
|
24
|
-
/**
|
|
25
|
-
* The document-delegation submit handler for router actions. Lives here —
|
|
26
|
-
* not in events.ts — so the router's event wiring holds no static reference
|
|
27
|
-
* to the action module; `installRouterIntegrations` slots it in when the
|
|
28
|
-
* first action is created on the client.
|
|
29
|
-
*/
|
|
30
|
-
export function handleFormAction(evt, router, actionBase) {
|
|
31
|
-
if (evt.defaultPrevented)
|
|
32
|
-
return;
|
|
33
|
-
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
34
|
-
? evt.submitter.getAttribute("formaction")
|
|
35
|
-
: evt.target.getAttribute("action");
|
|
36
|
-
if (!actionRef)
|
|
37
|
-
return;
|
|
38
|
-
const serverAction = !actionRef.startsWith("https://action/");
|
|
39
|
-
if (serverAction) {
|
|
40
|
-
// normalize server actions
|
|
41
|
-
const url = new URL(actionRef, mockBase);
|
|
42
|
-
actionRef = router.parsePath(url.pathname + url.search);
|
|
43
|
-
if (!actionRef.startsWith(actionBase))
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (evt.target.method.toUpperCase() !== "POST")
|
|
47
|
-
throw new Error("Only POST forms are supported for Actions");
|
|
48
|
-
// A registry miss on a server-action url is a direct bind whose module
|
|
49
|
-
// never loaded client-side (server components): the url is self-describing
|
|
50
|
-
// (`?id`, bound `?args`), so a generic invocation is synthesized from it —
|
|
51
|
-
// delegation alone is sufficient, the no-JS path stays a no-JS fallback.
|
|
52
|
-
// Client-only actions (`https://action/`) are their module's JS by
|
|
53
|
-
// definition, so a miss there falls through to native submission.
|
|
54
|
-
const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
|
|
55
|
-
if (handler) {
|
|
56
|
-
evt.preventDefault();
|
|
57
|
-
const data = new FormData(evt.target, evt.submitter);
|
|
58
|
-
handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
|
|
59
|
-
? data
|
|
60
|
-
: new URLSearchParams(data));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Synthesizes a router action for a server-rendered action url. The url
|
|
65
|
-
* carries everything an invocation needs — the function id and any bound
|
|
66
|
-
* `.with()` arguments (plain JSON in `?args`, which the server prepends for
|
|
67
|
-
* natural-encoding bodies exactly as it does for no-JS posts) — so the
|
|
68
|
-
* FormData is posted to it verbatim through the server-function transport:
|
|
69
|
-
* submissions, `aria-busy`, redirects, revalidation, and single-flight all
|
|
70
|
-
* flow through the normal action machinery. Registered under the url, so
|
|
71
|
-
* repeat submits reuse it (and a later real registration overrides it).
|
|
72
|
-
*/
|
|
73
|
-
function createServerFormAction(url) {
|
|
74
|
-
const id = new URL(url, mockBase).searchParams.get("id");
|
|
75
|
-
if (!id)
|
|
76
|
-
return undefined;
|
|
77
|
-
// typecheck resolves the server half of the dual module; this path only
|
|
78
|
-
// runs in the browser, where the client transport's signature applies
|
|
79
|
-
const stub = createServerReference(id, undefined, url);
|
|
80
|
-
const caller = Object.assign((form) => stub(form), { url });
|
|
81
|
-
return actionImpl(caller);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Entry point for delegation's lazy fallback (data/events.ts): when no form
|
|
85
|
-
* handler was ever installed — no action module in the client graph at all —
|
|
86
|
-
* the router intercepts posts to server-action urls synchronously and loads
|
|
87
|
-
* this module to run them. The FormData was captured at submit time; only
|
|
88
|
-
* the enctype conversion and the generic invocation happen here.
|
|
89
|
-
*/
|
|
90
|
-
export function submitServerForm(router, url, form, data) {
|
|
91
|
-
const handler = actions.get(url) || createServerFormAction(url);
|
|
92
|
-
// no `?id` — not the server function convention; nothing can run it,
|
|
93
|
-
// resubmit natively (submit() bypasses the delegated handler)
|
|
94
|
-
if (!handler)
|
|
95
|
-
return form.submit();
|
|
96
|
-
handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
97
|
-
}
|
|
98
|
-
// Wires the action layer into the router's slots exactly once, triggered by
|
|
99
|
-
// the first action creation. Not an import side effect — with
|
|
100
|
-
// `sideEffects: false`, module evaluation only happens when action() is
|
|
101
|
-
// actually used, which is precisely when the wiring is wanted: no action in
|
|
102
|
-
// the graph means no form interception, no single-flight subscription (the
|
|
103
|
-
// server is never asked to collect), and no flash cookies to decode. On the
|
|
104
|
-
// server, actions are created at module scope, so the flash decoder is
|
|
105
|
-
// always installed before useSubmission can read the submissions signal.
|
|
106
|
-
let integrationsInstalled = false;
|
|
107
|
-
function installRouterIntegrations() {
|
|
108
|
-
if (integrationsInstalled)
|
|
109
|
-
return;
|
|
110
|
-
integrationsInstalled = true;
|
|
111
|
-
provideFlashDecoder(decodeFlashCookie);
|
|
112
|
-
if (!isServer) {
|
|
113
|
-
setRouterFormHandler(handleFormAction);
|
|
114
|
-
provideFlightConsumer(setupFlightDataConsumer);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
7
|
export function useSubmissions(fn, filter) {
|
|
118
8
|
const router = useRouter();
|
|
119
9
|
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
@@ -121,6 +11,8 @@ export function useSubmissions(fn, filter) {
|
|
|
121
11
|
get(_, property) {
|
|
122
12
|
if (property === $TRACK)
|
|
123
13
|
return subs();
|
|
14
|
+
if (property === "pending")
|
|
15
|
+
return subs().some(sub => !sub.result);
|
|
124
16
|
return subs()[property];
|
|
125
17
|
},
|
|
126
18
|
has(_, property) {
|
|
@@ -128,169 +20,130 @@ export function useSubmissions(fn, filter) {
|
|
|
128
20
|
}
|
|
129
21
|
});
|
|
130
22
|
}
|
|
23
|
+
export function useSubmission(fn, filter) {
|
|
24
|
+
const submissions = useSubmissions(fn, filter);
|
|
25
|
+
return new Proxy({}, {
|
|
26
|
+
get(_, property) {
|
|
27
|
+
if (submissions.length === 0 && (property === "clear" || property === "retry"))
|
|
28
|
+
return () => { };
|
|
29
|
+
return submissions[submissions.length - 1]?.[property];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
131
33
|
export function useAction(action) {
|
|
132
34
|
const r = useRouter();
|
|
133
35
|
return (...args) => action.apply({ r }, args);
|
|
134
36
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
37
|
+
export function action(fn, options = {}) {
|
|
38
|
+
function mutate(...variables) {
|
|
137
39
|
const router = this.r;
|
|
138
40
|
const form = this.f;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
form && setFormBusy(form, 1);
|
|
158
|
-
let settled;
|
|
159
|
-
let response;
|
|
160
|
-
try {
|
|
161
|
-
settled = await settleActionResult(run({
|
|
162
|
-
call: runMutation,
|
|
163
|
-
optimistic: submitHooks.size
|
|
164
|
-
? () => {
|
|
165
|
-
for (const hook of submitHooks.values())
|
|
166
|
-
hook(...variables);
|
|
41
|
+
const p = (router.singleFlight && fn.withOptions
|
|
42
|
+
? fn.withOptions({ headers: { "X-Single-Flight": "true" } })
|
|
43
|
+
: fn)(...variables);
|
|
44
|
+
const [result, setResult] = createSignal();
|
|
45
|
+
let submission;
|
|
46
|
+
function handler(error) {
|
|
47
|
+
return async (res) => {
|
|
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());
|
|
167
57
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
58
|
+
});
|
|
59
|
+
if (retry)
|
|
60
|
+
return retry;
|
|
61
|
+
if (!result)
|
|
62
|
+
return submission.clear();
|
|
63
|
+
setResult(result);
|
|
64
|
+
if (result.error && !form)
|
|
65
|
+
throw result.error;
|
|
66
|
+
return result.data;
|
|
67
|
+
};
|
|
171
68
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return
|
|
69
|
+
router.submissions[1](s => [
|
|
70
|
+
...s,
|
|
71
|
+
(submission = {
|
|
72
|
+
input: variables,
|
|
73
|
+
url,
|
|
74
|
+
get result() {
|
|
75
|
+
return result()?.data;
|
|
76
|
+
},
|
|
77
|
+
get error() {
|
|
78
|
+
return result()?.error;
|
|
79
|
+
},
|
|
80
|
+
get pending() {
|
|
81
|
+
return !result();
|
|
82
|
+
},
|
|
83
|
+
clear() {
|
|
84
|
+
router.submissions[1](v => v.filter(i => i !== submission));
|
|
85
|
+
},
|
|
86
|
+
retry() {
|
|
87
|
+
setResult(undefined);
|
|
88
|
+
const p = fn(...variables);
|
|
89
|
+
return p.then(handler(), handler(true));
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
]);
|
|
93
|
+
return p.then(handler(), handler(true));
|
|
197
94
|
}
|
|
198
95
|
const o = typeof options === "string" ? { name: options } : options;
|
|
199
96
|
const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
|
|
200
97
|
const url = fn.url || (name && `https://action/${name}`) || "";
|
|
201
|
-
|
|
98
|
+
mutate.base = url;
|
|
202
99
|
if (name)
|
|
203
|
-
setFunctionName(
|
|
204
|
-
return
|
|
100
|
+
setFunctionName(mutate, name);
|
|
101
|
+
return toAction(mutate, url);
|
|
205
102
|
}
|
|
206
|
-
|
|
207
|
-
function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
|
|
208
|
-
const fn = function (...args) {
|
|
209
|
-
return invoke.call(this, [...boundArgs, ...args], fn);
|
|
210
|
-
};
|
|
103
|
+
function toAction(fn, url) {
|
|
211
104
|
fn.toString = () => {
|
|
212
105
|
if (!url)
|
|
213
106
|
throw new Error("Client Actions need explicit names if server rendered");
|
|
214
107
|
return url;
|
|
215
108
|
};
|
|
216
109
|
fn.with = function (...args) {
|
|
110
|
+
const newFn = function (...passedArgs) {
|
|
111
|
+
return fn.call(this, ...args, ...passedArgs);
|
|
112
|
+
};
|
|
113
|
+
newFn.base = fn.base;
|
|
217
114
|
const uri = new URL(url, mockBase);
|
|
218
115
|
uri.searchParams.set("args", hashKey(args));
|
|
219
|
-
|
|
220
|
-
return next;
|
|
221
|
-
};
|
|
222
|
-
fn.onSubmit = function (hook) {
|
|
223
|
-
const id = Symbol("actionOnSubmitHook");
|
|
224
|
-
submitHooks.set(id, hook);
|
|
225
|
-
getOwner() && onCleanup(() => submitHooks.delete(id));
|
|
226
|
-
return this;
|
|
227
|
-
};
|
|
228
|
-
fn.onSettled = function (hook) {
|
|
229
|
-
const id = Symbol("actionOnSettledHook");
|
|
230
|
-
settledHooks.set(id, hook);
|
|
231
|
-
getOwner() && onCleanup(() => settledHooks.delete(id));
|
|
232
|
-
return this;
|
|
116
|
+
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
|
|
233
117
|
};
|
|
234
118
|
fn.url = url;
|
|
235
|
-
fn.base = base;
|
|
236
|
-
fn[submitHooksSymbol] = submitHooks;
|
|
237
|
-
fn[settledHooksSymbol] = settledHooks;
|
|
238
|
-
fn[invokeSymbol] = invoke;
|
|
239
|
-
installRouterIntegrations();
|
|
240
119
|
if (!isServer) {
|
|
241
120
|
actions.set(url, fn);
|
|
242
121
|
// Only remove the registration if it still belongs to this instance —
|
|
243
122
|
// a re-created action (e.g. a new `.with()` binding after revalidation)
|
|
244
123
|
// may have registered itself under the same URL since.
|
|
245
|
-
getOwner() &&
|
|
246
|
-
onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
124
|
+
getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
247
125
|
}
|
|
248
126
|
return fn;
|
|
249
127
|
}
|
|
250
128
|
const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
|
|
251
|
-
async function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return result.then(value => value);
|
|
255
|
-
}
|
|
256
|
-
if (value && typeof value.next === "function") {
|
|
257
|
-
const iterator = value;
|
|
258
|
-
let next = await iterator.next();
|
|
259
|
-
while (!next.done) {
|
|
260
|
-
next = await iterator.next();
|
|
261
|
-
}
|
|
262
|
-
return next.value;
|
|
263
|
-
}
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Registers the router as the single-flight consumer of the server function
|
|
268
|
-
* transport. Subscribing is the opt-in: while registered, the transport
|
|
269
|
-
* sends the `X-Single-Flight` request header on mutations and delivers the
|
|
270
|
-
* folded payload here — fresh route data is seeded into the `query` cache
|
|
271
|
-
* and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
|
|
272
|
-
* applied, all before the action sees its plain return value. Called by the
|
|
273
|
-
* Router component on the client unless `singleFlight={false}`, which now
|
|
274
|
-
* simply means "never subscribe" — no consumer, no request header, no
|
|
275
|
-
* collection work on the server. Returns the unsubscribe function.
|
|
276
|
-
*/
|
|
277
|
-
export function setupFlightDataConsumer(router) {
|
|
278
|
-
return subscribeFlightData((data, { response }) => applyResponseMetadata(response, router.navigatorFactory(), data));
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Applies a server function response's integration metadata: `X-Revalidate`
|
|
282
|
-
* keys invalidate, `Location` navigates (hard for absolute urls), flight
|
|
283
|
-
* data seeds the query cache, and matching entries revalidate. Shared by
|
|
284
|
-
* the flight-data consumer and the action response path (which still sees
|
|
285
|
-
* metadata-bearing responses when no flight data was collected).
|
|
286
|
-
*/
|
|
287
|
-
async function applyResponseMetadata(metadata, navigate, flightData) {
|
|
129
|
+
async function handleResponse(response, error, navigate) {
|
|
130
|
+
let data;
|
|
131
|
+
let custom;
|
|
288
132
|
let keys;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
133
|
+
let flightKeys;
|
|
134
|
+
if (response instanceof Response) {
|
|
135
|
+
if (response.headers.has("X-Revalidate"))
|
|
136
|
+
keys = response.headers.get("X-Revalidate").split(",");
|
|
137
|
+
if (response.customBody) {
|
|
138
|
+
data = custom = await response.customBody();
|
|
139
|
+
if (response.headers.has("X-Single-Flight")) {
|
|
140
|
+
data = data._$value;
|
|
141
|
+
delete custom._$value;
|
|
142
|
+
flightKeys = Object.keys(custom);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (response.headers.has("Location")) {
|
|
146
|
+
const locationUrl = response.headers.get("Location") || "/";
|
|
294
147
|
if (locationUrl.startsWith("http")) {
|
|
295
148
|
window.location.href = locationUrl;
|
|
296
149
|
}
|
|
@@ -299,42 +152,15 @@ async function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
299
152
|
}
|
|
300
153
|
}
|
|
301
154
|
}
|
|
155
|
+
else if (error)
|
|
156
|
+
return { error: response };
|
|
157
|
+
else
|
|
158
|
+
data = response;
|
|
302
159
|
// invalidate
|
|
303
160
|
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
304
161
|
// set cache
|
|
305
|
-
|
|
162
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
306
163
|
// trigger revalidation
|
|
307
164
|
await revalidate(keys, false);
|
|
308
|
-
}
|
|
309
|
-
async function handleResponse(response, error, navigate) {
|
|
310
|
-
let data;
|
|
311
|
-
let flightData;
|
|
312
|
-
let metadata;
|
|
313
|
-
if (isResponseEnvelope(response)) {
|
|
314
|
-
// client-only respond(): the value rides in memory beside the metadata
|
|
315
|
-
data = response.value;
|
|
316
|
-
metadata = response.response;
|
|
317
|
-
}
|
|
318
|
-
else if (response instanceof Response) {
|
|
319
|
-
metadata = response;
|
|
320
|
-
// responses the transport hands over whole (redirects, revalidation)
|
|
321
|
-
// carry a codec-encoded body the router decodes itself. With the
|
|
322
|
-
// flight-data consumer registered single-flight payloads never reach
|
|
323
|
-
// this path, but a manually opted-in call (no consumer) still can —
|
|
324
|
-
// unwrap the standardized { value, data } shape for it too.
|
|
325
|
-
if (response.body) {
|
|
326
|
-
data = await decodeResponse(response);
|
|
327
|
-
if (response.headers.has("X-Single-Flight")) {
|
|
328
|
-
const payload = data;
|
|
329
|
-
data = payload.value;
|
|
330
|
-
flightData = payload.data;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else if (error)
|
|
335
|
-
return { error: response };
|
|
336
|
-
else
|
|
337
|
-
data = response;
|
|
338
|
-
await applyResponseMetadata(metadata, navigate, flightData);
|
|
339
165
|
return data != null ? { data } : undefined;
|
|
340
166
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReconcileOptions } from "solid-js/store";
|
|
2
|
+
/**
|
|
3
|
+
* As `createAsync` and `createAsyncStore` are wrappers for `createResource`,
|
|
4
|
+
* this type allows to support `latest` field for these primitives.
|
|
5
|
+
* It will be removed in the future.
|
|
6
|
+
*/
|
|
7
|
+
export type AccessorWithLatest<T> = {
|
|
8
|
+
(): T;
|
|
9
|
+
latest: T;
|
|
10
|
+
};
|
|
11
|
+
export declare function createAsync<T>(fn: (prev: T) => Promise<T>, options: {
|
|
12
|
+
name?: string;
|
|
13
|
+
initialValue: T;
|
|
14
|
+
deferStream?: boolean;
|
|
15
|
+
}): AccessorWithLatest<T>;
|
|
16
|
+
export declare function createAsync<T>(fn: (prev: T | undefined) => Promise<T>, options?: {
|
|
17
|
+
name?: string;
|
|
18
|
+
initialValue?: T;
|
|
19
|
+
deferStream?: boolean;
|
|
20
|
+
}): AccessorWithLatest<T | undefined>;
|
|
21
|
+
export declare function createAsyncStore<T>(fn: (prev: T) => Promise<T>, options: {
|
|
22
|
+
name?: string;
|
|
23
|
+
initialValue: T;
|
|
24
|
+
deferStream?: boolean;
|
|
25
|
+
reconcile?: ReconcileOptions;
|
|
26
|
+
}): AccessorWithLatest<T>;
|
|
27
|
+
export declare function createAsyncStore<T>(fn: (prev: T | undefined) => Promise<T>, options?: {
|
|
28
|
+
name?: string;
|
|
29
|
+
initialValue?: T;
|
|
30
|
+
deferStream?: boolean;
|
|
31
|
+
reconcile?: ReconcileOptions;
|
|
32
|
+
}): AccessorWithLatest<T | undefined>;
|