@solidjs/router 1.0.0 → 2.0.0-next.13
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 +606 -697
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +115 -0
- package/dist/data/action.d.ts +40 -11
- package/dist/data/action.js +307 -101
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +24 -23
- package/dist/data/index.d.ts +2 -4
- package/dist/data/index.js +2 -4
- package/dist/data/query.d.ts +1 -3
- package/dist/data/query.js +75 -32
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/fs.d.ts +94 -0
- package/dist/fs.js +51 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +1949 -1204
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +12 -26
- package/dist/routers/components.jsx +68 -54
- package/dist/routers/factory.d.ts +121 -0
- package/dist/routers/factory.jsx +154 -0
- package/dist/routers/history.d.ts +26 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +5 -11
- package/dist/routers/index.js +2 -6
- package/dist/routers/scrollRestoration.d.ts +10 -1
- package/dist/routers/scrollRestoration.js +49 -11
- package/dist/routing.d.ts +96 -52
- package/dist/routing.js +429 -172
- package/dist/server.d.ts +47 -0
- package/dist/server.js +156 -0
- package/dist/types.d.ts +158 -44
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +12 -7
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -40
- package/dist/data/createAsync.d.ts +0 -32
- package/dist/data/createAsync.js +0 -96
- package/dist/data/response.d.ts +0 -4
- package/dist/data/response.js +0 -42
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -17
- package/dist/routers/Router.js +0 -59
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -41
package/dist/data/action.js
CHANGED
|
@@ -1,9 +1,125 @@
|
|
|
1
|
-
import { $TRACK,
|
|
2
|
-
import { isServer } from "
|
|
3
|
-
import {
|
|
1
|
+
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
|
|
2
|
+
import { isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
|
|
3
|
+
import { createServerReference, decodeResponsePayload, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
|
+
// The explicit /server specifier is safe here: the only call site is
|
|
5
|
+
// server-guarded, so client builds tree-shake the codec away.
|
|
6
|
+
import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
|
|
7
|
+
import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
|
|
8
|
+
import { setRouterFormHandler } from "./events.js";
|
|
4
9
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
5
10
|
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
11
|
+
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
12
|
+
const settledHooksSymbol = Symbol("routerActionSettledHooks");
|
|
13
|
+
const invokeSymbol = Symbol("routerActionInvoke");
|
|
14
|
+
// Forms submitted through delegation are marked `aria-busy` while their
|
|
15
|
+
// action is in flight — the form half of the attribute vocabulary links get
|
|
16
|
+
// (`data-active`/`data-pending`). Style with `form[aria-busy] button { ... }`.
|
|
17
|
+
// A counter (not a boolean) keeps the attribute through overlapping
|
|
18
|
+
// submissions from the same form.
|
|
19
|
+
const busyForms = /* #__PURE__ */ new WeakMap();
|
|
20
|
+
function setFormBusy(form, delta) {
|
|
21
|
+
const count = (busyForms.get(form) || 0) + delta;
|
|
22
|
+
busyForms.set(form, count);
|
|
23
|
+
count > 0 ? form.setAttribute("aria-busy", "true") : form.removeAttribute("aria-busy");
|
|
24
|
+
}
|
|
6
25
|
export const actions = /* #__PURE__ */ new Map();
|
|
26
|
+
/**
|
|
27
|
+
* The document-delegation submit handler for router actions. Lives here —
|
|
28
|
+
* not in events.ts — so the router's event wiring holds no static reference
|
|
29
|
+
* to the action module; `installRouterIntegrations` slots it in when the
|
|
30
|
+
* first action is created on the client.
|
|
31
|
+
*/
|
|
32
|
+
export function handleFormAction(evt, router, actionBase) {
|
|
33
|
+
if (evt.defaultPrevented)
|
|
34
|
+
return;
|
|
35
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
36
|
+
? evt.submitter.getAttribute("formaction")
|
|
37
|
+
: evt.target.getAttribute("action");
|
|
38
|
+
if (!actionRef)
|
|
39
|
+
return;
|
|
40
|
+
const serverAction = !actionRef.startsWith("https://action/");
|
|
41
|
+
if (serverAction) {
|
|
42
|
+
// normalize server actions
|
|
43
|
+
const url = new URL(actionRef, mockBase);
|
|
44
|
+
actionRef = router.parsePath(url.pathname + url.search);
|
|
45
|
+
if (!actionRef.startsWith(actionBase))
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (evt.target.method.toUpperCase() !== "POST")
|
|
49
|
+
throw new Error("Only POST forms are supported for Actions");
|
|
50
|
+
// A registry miss on a server-action url is a direct bind whose module
|
|
51
|
+
// never loaded client-side (server components): the url is self-describing
|
|
52
|
+
// (`?id`, bound `?args`), so a generic invocation is synthesized from it —
|
|
53
|
+
// delegation alone is sufficient, the no-JS path stays a no-JS fallback.
|
|
54
|
+
// Client-only actions (`https://action/`) are their module's JS by
|
|
55
|
+
// definition, so a miss there falls through to native submission.
|
|
56
|
+
const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
|
|
57
|
+
if (handler) {
|
|
58
|
+
evt.preventDefault();
|
|
59
|
+
const data = new FormData(evt.target, evt.submitter);
|
|
60
|
+
handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
|
|
61
|
+
? data
|
|
62
|
+
: new URLSearchParams(data));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Synthesizes a router action for a server-rendered action url. The url
|
|
67
|
+
* carries everything an invocation needs — the function id and any bound
|
|
68
|
+
* `.with()` arguments (plain JSON in `?args`, which the server prepends for
|
|
69
|
+
* natural-encoding bodies exactly as it does for no-JS posts) — so the
|
|
70
|
+
* FormData is posted to it verbatim through the server-function transport:
|
|
71
|
+
* submissions, `aria-busy`, redirects, revalidation, and single-flight all
|
|
72
|
+
* flow through the normal action machinery. Registered under the url, so
|
|
73
|
+
* repeat submits reuse it (and a later real registration overrides it).
|
|
74
|
+
*/
|
|
75
|
+
function createServerFormAction(url) {
|
|
76
|
+
const id = new URL(url, mockBase).searchParams.get("id");
|
|
77
|
+
if (!id)
|
|
78
|
+
return undefined;
|
|
79
|
+
// typecheck resolves the server half of the dual module; this path only
|
|
80
|
+
// runs in the browser, where the client transport's signature applies
|
|
81
|
+
const stub = createServerReference(id, undefined, url);
|
|
82
|
+
const caller = Object.assign((form) => stub(form), { url });
|
|
83
|
+
return actionImpl(caller);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Entry point for delegation's lazy fallback (data/events.ts): when no form
|
|
87
|
+
* handler was ever installed — no action module in the client graph at all —
|
|
88
|
+
* the router intercepts posts to server-action urls synchronously and loads
|
|
89
|
+
* this module to run them. The FormData was captured at submit time; only
|
|
90
|
+
* the enctype conversion and the generic invocation happen here.
|
|
91
|
+
*/
|
|
92
|
+
export function submitServerForm(router, url, form, data) {
|
|
93
|
+
const handler = actions.get(url) || createServerFormAction(url);
|
|
94
|
+
// no `?id` — not the server function convention; nothing can run it,
|
|
95
|
+
// resubmit natively (submit() bypasses the delegated handler)
|
|
96
|
+
if (!handler)
|
|
97
|
+
return form.submit();
|
|
98
|
+
handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
99
|
+
}
|
|
100
|
+
// Wires the action layer into the router's slots exactly once, triggered by
|
|
101
|
+
// the first action creation. Not an import side effect — with
|
|
102
|
+
// `sideEffects: false`, module evaluation only happens when action() is
|
|
103
|
+
// actually used, which is precisely when the wiring is wanted: no action in
|
|
104
|
+
// the graph means no form interception, no single-flight subscription (the
|
|
105
|
+
// server is never asked to collect), and no flash cookies to decode. On the
|
|
106
|
+
// server, actions are created at module scope, so the flash decoder is
|
|
107
|
+
// always installed before useSubmission can read the submissions signal.
|
|
108
|
+
let integrationsInstalled = false;
|
|
109
|
+
function installRouterIntegrations() {
|
|
110
|
+
if (integrationsInstalled)
|
|
111
|
+
return;
|
|
112
|
+
integrationsInstalled = true;
|
|
113
|
+
if (isServer) {
|
|
114
|
+
// Server-only: initSubmissions only decodes during SSR, so client builds
|
|
115
|
+
// tree-shake the codec (which now lives behind the runtime's server entry).
|
|
116
|
+
provideFlashDecoder(decodeFlashCookie);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
setRouterFormHandler(handleFormAction);
|
|
120
|
+
provideFlightConsumer(setupFlightDataConsumer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
7
123
|
export function useSubmissions(fn, filter) {
|
|
8
124
|
const router = useRouter();
|
|
9
125
|
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
@@ -11,8 +127,6 @@ export function useSubmissions(fn, filter) {
|
|
|
11
127
|
get(_, property) {
|
|
12
128
|
if (property === $TRACK)
|
|
13
129
|
return subs();
|
|
14
|
-
if (property === "pending")
|
|
15
|
-
return subs().some(sub => !sub.result);
|
|
16
130
|
return subs()[property];
|
|
17
131
|
},
|
|
18
132
|
has(_, property) {
|
|
@@ -20,130 +134,190 @@ export function useSubmissions(fn, filter) {
|
|
|
20
134
|
}
|
|
21
135
|
});
|
|
22
136
|
}
|
|
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
|
-
}
|
|
33
137
|
export function useAction(action) {
|
|
34
138
|
const r = useRouter();
|
|
35
139
|
return (...args) => action.apply({ r }, args);
|
|
36
140
|
}
|
|
37
|
-
|
|
38
|
-
function
|
|
141
|
+
function actionImpl(fn, options = {}) {
|
|
142
|
+
async function invoke(variables, current) {
|
|
39
143
|
const router = this.r;
|
|
40
144
|
const form = this.f;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
145
|
+
const submitHooks = current[submitHooksSymbol];
|
|
146
|
+
const settledHooks = current[settledHooksSymbol];
|
|
147
|
+
// Single-flight opt-in is no longer per call: the router's registered
|
|
148
|
+
// flight-data consumer (see setupFlightDataConsumer) makes the transport
|
|
149
|
+
// send the request header itself, so the mutation is just called.
|
|
150
|
+
const runMutation = () => fn(...variables);
|
|
151
|
+
const run = createSolidAction(async function* (context) {
|
|
152
|
+
context.optimistic?.();
|
|
153
|
+
try {
|
|
154
|
+
const value = await context.call();
|
|
155
|
+
yield;
|
|
156
|
+
return { error: false, value };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
yield;
|
|
160
|
+
return { error: true, value: error };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
form && setFormBusy(form, 1);
|
|
164
|
+
let settled;
|
|
165
|
+
let response;
|
|
166
|
+
// The transport consumer is awaited before a single-flight mutation
|
|
167
|
+
// resolves, so a counter delta over the call tells whether this action's
|
|
168
|
+
// metadata was already applied. Overlapping mutations can cross-attribute
|
|
169
|
+
// a run (skipping one default revalidation another pass just covered) —
|
|
170
|
+
// a far smaller window than predicting from the function's identity,
|
|
171
|
+
// which misses every response the server returned without flight data.
|
|
172
|
+
const flightApplicationsBefore = flightApplications;
|
|
173
|
+
try {
|
|
174
|
+
settled = await settleActionResult(run({
|
|
175
|
+
call: runMutation,
|
|
176
|
+
optimistic: submitHooks.size
|
|
177
|
+
? () => {
|
|
178
|
+
for (const hook of submitHooks.values())
|
|
179
|
+
hook(...variables);
|
|
57
180
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (result.error && !form)
|
|
65
|
-
throw result.error;
|
|
66
|
-
return result.data;
|
|
67
|
-
};
|
|
181
|
+
: undefined
|
|
182
|
+
}));
|
|
183
|
+
response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
form && setFormBusy(form, -1);
|
|
68
187
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
188
|
+
let submission;
|
|
189
|
+
submission = {
|
|
190
|
+
input: variables,
|
|
191
|
+
url,
|
|
192
|
+
result: response && response.data,
|
|
193
|
+
error: response && response.error,
|
|
194
|
+
clear() {
|
|
195
|
+
router.submissions[1](entries => entries.filter(entry => entry !== submission));
|
|
196
|
+
},
|
|
197
|
+
retry() {
|
|
198
|
+
submission.clear();
|
|
199
|
+
return current[invokeSymbol].call({ r: router, f: form }, variables, current);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
// Book-keeping is intentional: only outcomes worth showing or retrying
|
|
203
|
+
// (a result or an error) enter the submissions list, so the typical void
|
|
204
|
+
// mutation leaves nothing behind. Settled hooks still see every
|
|
205
|
+
// completion — void, metadata-only, and redirects included — one
|
|
206
|
+
// `onSettled` per invocation (#580).
|
|
207
|
+
response && router.submissions[1](entries => [...entries, submission]);
|
|
208
|
+
for (const hook of settledHooks.values())
|
|
209
|
+
hook(submission);
|
|
210
|
+
if (response) {
|
|
211
|
+
if (response.error && !form)
|
|
212
|
+
throw response.error;
|
|
213
|
+
return response.data;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
94
216
|
}
|
|
95
217
|
const o = typeof options === "string" ? { name: options } : options;
|
|
96
218
|
const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
|
|
97
219
|
const url = fn.url || (name && `https://action/${name}`) || "";
|
|
98
|
-
|
|
220
|
+
const wrapped = toAction(invoke, url);
|
|
99
221
|
if (name)
|
|
100
|
-
setFunctionName(
|
|
101
|
-
return
|
|
222
|
+
setFunctionName(wrapped, name);
|
|
223
|
+
return wrapped;
|
|
102
224
|
}
|
|
103
|
-
|
|
225
|
+
export const action = actionImpl;
|
|
226
|
+
function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
|
|
227
|
+
const fn = function (...args) {
|
|
228
|
+
return invoke.call(this, [...boundArgs, ...args], fn);
|
|
229
|
+
};
|
|
104
230
|
fn.toString = () => {
|
|
105
231
|
if (!url)
|
|
106
232
|
throw new Error("Client Actions need explicit names if server rendered");
|
|
107
233
|
return url;
|
|
108
234
|
};
|
|
109
235
|
fn.with = function (...args) {
|
|
110
|
-
const newFn = function (...passedArgs) {
|
|
111
|
-
return fn.call(this, ...args, ...passedArgs);
|
|
112
|
-
};
|
|
113
|
-
newFn.base = fn.base;
|
|
114
236
|
const uri = new URL(url, mockBase);
|
|
115
237
|
uri.searchParams.set("args", hashKey(args));
|
|
116
|
-
|
|
238
|
+
const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
|
|
239
|
+
return next;
|
|
240
|
+
};
|
|
241
|
+
fn.onSubmit = function (hook) {
|
|
242
|
+
const id = Symbol("actionOnSubmitHook");
|
|
243
|
+
submitHooks.set(id, hook);
|
|
244
|
+
getOwner() && onCleanup(() => submitHooks.delete(id));
|
|
245
|
+
return this;
|
|
246
|
+
};
|
|
247
|
+
fn.onSettled = function (hook) {
|
|
248
|
+
const id = Symbol("actionOnSettledHook");
|
|
249
|
+
settledHooks.set(id, hook);
|
|
250
|
+
getOwner() && onCleanup(() => settledHooks.delete(id));
|
|
251
|
+
return this;
|
|
117
252
|
};
|
|
118
253
|
fn.url = url;
|
|
254
|
+
fn.base = base;
|
|
255
|
+
fn[submitHooksSymbol] = submitHooks;
|
|
256
|
+
fn[settledHooksSymbol] = settledHooks;
|
|
257
|
+
fn[invokeSymbol] = invoke;
|
|
258
|
+
installRouterIntegrations();
|
|
119
259
|
if (!isServer) {
|
|
120
260
|
actions.set(url, fn);
|
|
121
261
|
// Only remove the registration if it still belongs to this instance —
|
|
122
262
|
// a re-created action (e.g. a new `.with()` binding after revalidation)
|
|
123
263
|
// may have registered itself under the same URL since.
|
|
124
|
-
getOwner() &&
|
|
264
|
+
getOwner() &&
|
|
265
|
+
onCleanup(() => actions.get(url) === fn && actions.delete(url));
|
|
125
266
|
}
|
|
126
267
|
return fn;
|
|
127
268
|
}
|
|
128
269
|
const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
|
|
129
|
-
async function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (response.headers.has("X-Single-Flight")) {
|
|
140
|
-
data = data._$value;
|
|
141
|
-
delete custom._$value;
|
|
142
|
-
flightKeys = Object.keys(custom);
|
|
143
|
-
}
|
|
270
|
+
async function settleActionResult(result) {
|
|
271
|
+
const value = result;
|
|
272
|
+
if (value && typeof value.then === "function") {
|
|
273
|
+
return result.then(value => value);
|
|
274
|
+
}
|
|
275
|
+
if (value && typeof value.next === "function") {
|
|
276
|
+
const iterator = value;
|
|
277
|
+
let next = await iterator.next();
|
|
278
|
+
while (!next.done) {
|
|
279
|
+
next = await iterator.next();
|
|
144
280
|
}
|
|
145
|
-
|
|
146
|
-
|
|
281
|
+
return next.value;
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
// Invocation count of the flight-data consumer. An action compares it across
|
|
286
|
+
// its mutation call to learn whether the transport already applied this
|
|
287
|
+
// response's metadata (and so the default revalidation pass must not run
|
|
288
|
+
// again and wipe the freshly seeded cache).
|
|
289
|
+
let flightApplications = 0;
|
|
290
|
+
/**
|
|
291
|
+
* Registers the router as the single-flight consumer of the server function
|
|
292
|
+
* transport. Subscribing is the opt-in: while registered, the transport
|
|
293
|
+
* sends the `X-Single-Flight` request header on mutations and delivers the
|
|
294
|
+
* folded payload here — fresh route data is seeded into the `query` cache
|
|
295
|
+
* and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
|
|
296
|
+
* applied, all before the action sees its plain return value. Called by the
|
|
297
|
+
* Router component on the client unless `singleFlight={false}`, which now
|
|
298
|
+
* simply means "never subscribe" — no consumer, no request header, no
|
|
299
|
+
* collection work on the server. Returns the unsubscribe function.
|
|
300
|
+
*/
|
|
301
|
+
export function setupFlightDataConsumer(router) {
|
|
302
|
+
return subscribeFlightData((data, { response }) => {
|
|
303
|
+
flightApplications++;
|
|
304
|
+
return applyResponseMetadata(response, router.navigatorFactory(), data);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Applies a server function response's integration metadata: `X-Revalidate`
|
|
309
|
+
* keys invalidate, `Location` navigates (hard for absolute urls), flight
|
|
310
|
+
* data seeds the query cache, and matching entries revalidate. Shared by
|
|
311
|
+
* the flight-data consumer and the action response path (which still sees
|
|
312
|
+
* metadata-bearing responses when no flight data was collected).
|
|
313
|
+
*/
|
|
314
|
+
function applyResponseMetadata(metadata, navigate, flightData) {
|
|
315
|
+
let keys;
|
|
316
|
+
if (metadata) {
|
|
317
|
+
if (metadata.headers.has(REVALIDATE_HEADER))
|
|
318
|
+
keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
|
|
319
|
+
if (metadata.headers.has("Location")) {
|
|
320
|
+
const locationUrl = metadata.headers.get("Location") || "/";
|
|
147
321
|
if (locationUrl.startsWith("http")) {
|
|
148
322
|
window.location.href = locationUrl;
|
|
149
323
|
}
|
|
@@ -152,15 +326,47 @@ async function handleResponse(response, error, navigate) {
|
|
|
152
326
|
}
|
|
153
327
|
}
|
|
154
328
|
}
|
|
329
|
+
// invalidate
|
|
330
|
+
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
331
|
+
// set cache
|
|
332
|
+
flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
333
|
+
// trigger revalidation inside the same transition as the navigation, so
|
|
334
|
+
// the redirect commits atomically with fresh data: surviving consumers
|
|
335
|
+
// (shared layouts) the flight payload didn't seed refetch and hold the
|
|
336
|
+
// commit rather than painting stale and updating after. Seeded entries
|
|
337
|
+
// are fresh again by now, so the sweep re-reads them from cache.
|
|
338
|
+
revalidate(keys, false);
|
|
339
|
+
}
|
|
340
|
+
async function handleResponse(response, error, navigate, metadataHandled) {
|
|
341
|
+
let data;
|
|
342
|
+
let flightData;
|
|
343
|
+
let metadata;
|
|
344
|
+
if (isResponseEnvelope(response)) {
|
|
345
|
+
// client-only respond(): the value rides in memory beside the metadata
|
|
346
|
+
data = response.value;
|
|
347
|
+
metadata = response.response;
|
|
348
|
+
}
|
|
349
|
+
else if (response instanceof Response) {
|
|
350
|
+
metadata = response;
|
|
351
|
+
// responses the transport hands over whole (redirects, revalidation)
|
|
352
|
+
// carry a codec-encoded body the router decodes itself. With the
|
|
353
|
+
// flight-data consumer registered single-flight payloads never reach
|
|
354
|
+
// this path, but a manually opted-in call (no consumer) still can —
|
|
355
|
+
// the runtime splits its own envelope shape.
|
|
356
|
+
if (response.body) {
|
|
357
|
+
const payload = await decodeResponsePayload(response);
|
|
358
|
+
data = payload.value;
|
|
359
|
+
flightData = payload.flightData;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
155
362
|
else if (error)
|
|
156
363
|
return { error: response };
|
|
157
364
|
else
|
|
158
365
|
data = response;
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
await revalidate(keys, false);
|
|
366
|
+
// The transport consumer applies metadata before returning a server
|
|
367
|
+
// function's unwrapped value. Do not treat that value as a second plain
|
|
368
|
+
// action response and invalidate the freshly seeded query cache again.
|
|
369
|
+
if (!metadataHandled || metadata || flightData)
|
|
370
|
+
applyResponseMetadata(metadata, navigate, flightData);
|
|
165
371
|
return data != null ? { data } : undefined;
|
|
166
372
|
}
|
package/dist/data/events.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { RouterContext } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The submit delegation consults this slot instead of importing the action
|
|
4
|
+
* module: the action side installs its handler on first action creation
|
|
5
|
+
* (see data/action.ts), so an app that never creates an action never pulls
|
|
6
|
+
* the data layer into its bundle through the router's event wiring.
|
|
7
|
+
*/
|
|
8
|
+
export type RouterFormHandler = (evt: SubmitEvent, router: RouterContext, actionBase: string) => void;
|
|
9
|
+
export declare function setRouterFormHandler(handler: RouterFormHandler | undefined): void;
|
|
2
10
|
type NativeEventConfig = {
|
|
3
11
|
preload?: boolean;
|
|
4
12
|
explicitLinks?: boolean;
|
package/dist/data/events.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { delegateEvents } from "
|
|
1
|
+
import { delegateEvents } from "@solidjs/web";
|
|
2
2
|
import { onCleanup } from "solid-js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
let formHandler;
|
|
4
|
+
export function setRouterFormHandler(handler) {
|
|
5
|
+
formHandler = handler;
|
|
6
|
+
}
|
|
5
7
|
export function setupNativeEvents({ preload = true, explicitLinks = false, actionBase = "/_server", transformUrl } = {}) {
|
|
6
8
|
return (router) => {
|
|
7
9
|
const basePath = router.base.path();
|
|
@@ -76,30 +78,29 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
76
78
|
}, 20);
|
|
77
79
|
}
|
|
78
80
|
function handleFormSubmit(evt) {
|
|
81
|
+
if (formHandler)
|
|
82
|
+
return formHandler(evt, router, actionBase);
|
|
83
|
+
// No form handler means no action module in the client graph at all
|
|
84
|
+
// (e.g. server components binding forms straight to server functions).
|
|
85
|
+
// A POST to a url under actionBase is self-describing, so delegation
|
|
86
|
+
// is still sufficient: intercept synchronously — the no-JS treatment
|
|
87
|
+
// is reserved for clients with no JS — capture the FormData, and load
|
|
88
|
+
// the handler lazily. Apps that never submit one never load it.
|
|
79
89
|
if (evt.defaultPrevented)
|
|
80
90
|
return;
|
|
81
|
-
|
|
91
|
+
const form = evt.target;
|
|
92
|
+
const ref = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
82
93
|
? evt.submitter.getAttribute("formaction")
|
|
83
|
-
:
|
|
84
|
-
if (!
|
|
94
|
+
: form.getAttribute("action");
|
|
95
|
+
if (!ref || ref.startsWith("https://action/"))
|
|
85
96
|
return;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (evt.target.method.toUpperCase() !== "POST")
|
|
94
|
-
throw new Error("Only POST forms are supported for Actions");
|
|
95
|
-
const handler = actions.get(actionRef);
|
|
96
|
-
if (handler) {
|
|
97
|
-
evt.preventDefault();
|
|
98
|
-
const data = new FormData(evt.target, evt.submitter);
|
|
99
|
-
handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
|
|
100
|
-
? data
|
|
101
|
-
: new URLSearchParams(data));
|
|
102
|
-
}
|
|
97
|
+
const url = new URL(ref, document.baseURI);
|
|
98
|
+
const path = router.parsePath(url.pathname + url.search);
|
|
99
|
+
if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST")
|
|
100
|
+
return;
|
|
101
|
+
evt.preventDefault();
|
|
102
|
+
const data = new FormData(form, evt.submitter);
|
|
103
|
+
import("./serverForms.js").then(m => m.submitServerForm(router, path, form, data));
|
|
103
104
|
}
|
|
104
105
|
// ensure delegated event run first
|
|
105
106
|
delegateEvents(["click", "submit"]);
|
package/dist/data/index.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export { query, revalidate, cache, type CachedFunction } from "./query.js";
|
|
4
|
-
export { redirect, reload, json } from "./response.js";
|
|
1
|
+
export { action, useSubmissions, useAction, type Action } from "./action.js";
|
|
2
|
+
export { query, revalidate, type CachedFunction } from "./query.js";
|
package/dist/data/index.js
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export { query, revalidate, cache } from "./query.js";
|
|
4
|
-
export { redirect, reload, json } from "./response.js";
|
|
1
|
+
export { action, useSubmissions, useAction } from "./action.js";
|
|
2
|
+
export { query, revalidate } from "./query.js";
|
package/dist/data/query.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { CacheEntry, NarrowResponse } from "../types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Revalidates the given cache entry/entries.
|
|
4
4
|
*/
|
|
5
|
-
export declare function revalidate(key?: string | string[] | void, force?: boolean):
|
|
5
|
+
export declare function revalidate(key?: string | string[] | void, force?: boolean): void;
|
|
6
6
|
export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
|
|
7
7
|
export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
|
|
8
8
|
[K in keyof A]-?: A[K];
|
|
@@ -18,6 +18,4 @@ export declare namespace query {
|
|
|
18
18
|
export var clear: () => void;
|
|
19
19
|
export { _a as delete };
|
|
20
20
|
}
|
|
21
|
-
/** @deprecated use query instead */
|
|
22
|
-
export declare const cache: typeof query;
|
|
23
21
|
export declare function hashKey<T extends Array<any>>(args: T): string;
|