@solidjs/router 2.0.0-next.32 → 2.0.0-next.34
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/dist/data/action.js +34 -4
- package/dist/data/query.js +21 -8
- package/dist/index.js +95 -39
- package/dist/routing.js +33 -28
- package/package.json +1 -1
package/dist/data/action.js
CHANGED
|
@@ -52,13 +52,40 @@ export function handleFormAction(evt, router, actionBase) {
|
|
|
52
52
|
// stays a no-JS fallback.
|
|
53
53
|
// Client-only actions (`https://action/`) are their module's JS by
|
|
54
54
|
// definition, so a miss there falls through to native submission.
|
|
55
|
-
const handler =
|
|
55
|
+
const handler = findAction(actionRef) || (serverAction && createServerFormAction(actionRef));
|
|
56
56
|
if (handler) {
|
|
57
57
|
evt.preventDefault();
|
|
58
58
|
const data = new FormData(form, evt.submitter);
|
|
59
59
|
handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Looks a rendered action url up in the registry. A server-rendered
|
|
64
|
+
* `.with()` url is only registered if this client made the same binding
|
|
65
|
+
* itself; otherwise its base action (the url without `?args`) is rebound to
|
|
66
|
+
* the rendered arguments, so the submission runs through that action's
|
|
67
|
+
* submit and settled hooks and is recorded under its base.
|
|
68
|
+
*/
|
|
69
|
+
function findAction(url) {
|
|
70
|
+
const handler = actions.get(url);
|
|
71
|
+
if (handler)
|
|
72
|
+
return handler;
|
|
73
|
+
const query = url.indexOf("?");
|
|
74
|
+
if (query < 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
const base = actions.get(url.slice(0, query));
|
|
77
|
+
const args = new URLSearchParams(url.slice(query)).get("args");
|
|
78
|
+
if (!base || args === null)
|
|
79
|
+
return undefined;
|
|
80
|
+
let bound;
|
|
81
|
+
try {
|
|
82
|
+
bound = JSON.parse(args);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return Array.isArray(bound) ? base.with(...bound) : undefined;
|
|
88
|
+
}
|
|
62
89
|
/**
|
|
63
90
|
* Synthesizes a router action for a server-rendered action url. The url
|
|
64
91
|
* carries everything an invocation needs — the function id in the path
|
|
@@ -90,7 +117,7 @@ function createServerFormAction(url) {
|
|
|
90
117
|
* the enctype conversion and the generic invocation happen here.
|
|
91
118
|
*/
|
|
92
119
|
export function submitServerForm(router, url, form, data) {
|
|
93
|
-
const handler =
|
|
120
|
+
const handler = findAction(url) || createServerFormAction(url);
|
|
94
121
|
// not an address (`<endpoint>/<id>`) — not the server function convention;
|
|
95
122
|
// nothing can run it, resubmit natively (submit() bypasses the delegated
|
|
96
123
|
// handler)
|
|
@@ -244,9 +271,12 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
244
271
|
return url;
|
|
245
272
|
};
|
|
246
273
|
fn.with = function (...args) {
|
|
274
|
+
const bound = [...boundArgs, ...args];
|
|
247
275
|
const uri = new URL(url, mockBase);
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
// the server prepends `args` to the submitted arguments, so it must carry
|
|
277
|
+
// the whole binding, not just this call's part of a chain
|
|
278
|
+
uri.searchParams.set("args", hashKey(bound));
|
|
279
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, bound, base, submitHooks, settledHooks);
|
|
250
280
|
};
|
|
251
281
|
fn.onSubmit = function (hook) {
|
|
252
282
|
const id = Symbol("actionOnSubmitHook");
|
package/dist/data/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, getObserver, getOwner, isHydratable, onCleanup, untrack } from "solid-js";
|
|
1
|
+
import { createSignal, getObserver, getOwner, isHydratable, isHydrating, onCleanup, untrack } from "solid-js";
|
|
2
2
|
// Everything server-function-shaped comes off the CORE entry: detection
|
|
3
3
|
// (isServerFunction/getServerFunctionMetadata, registered-symbol reads) and
|
|
4
4
|
// the late-bound RPC seam (getServerFunctionRPC). The transport itself —
|
|
@@ -37,6 +37,13 @@ const forwardedCookies = /* #__PURE__ */ new WeakMap();
|
|
|
37
37
|
// hold values the server computed while rendering THIS page, so their age is
|
|
38
38
|
// anchored here — not at whenever a late query() call happens to consume one.
|
|
39
39
|
const bootTime = Date.now();
|
|
40
|
+
// Captured at module load: a hydrating client runs a serialized node's
|
|
41
|
+
// compute once as a dependency trace with the global `Promise` swapped for a
|
|
42
|
+
// never-settling mock, and a query() read inside that trace still consumes
|
|
43
|
+
// and caches its flight entry. Wrapped with the global there, a settled
|
|
44
|
+
// entry would cache a promise that never resolves, hanging every navigation
|
|
45
|
+
// that reuses it.
|
|
46
|
+
const NativePromise = Promise;
|
|
40
47
|
// Every hydratable read ships its entry: the writer keeps the first write
|
|
41
48
|
// per key, so a read skipped in a NoHydration zone never hides the entry
|
|
42
49
|
// from an island that reads the same key later.
|
|
@@ -165,11 +172,18 @@ export function query(fn, name) {
|
|
|
165
172
|
tracking = true;
|
|
166
173
|
onCleanup(() => cached[4].count--);
|
|
167
174
|
}
|
|
175
|
+
// A read with no navigation in flight while hydrating is claiming server
|
|
176
|
+
// DOM, so it takes the cached entry at any age — the rule adoption below
|
|
177
|
+
// applies to flight entries. It can come from a boundary resuming long
|
|
178
|
+
// after boot, once the entry was adopted and its first reader disposed;
|
|
179
|
+
// refetching there would run inside Solid's hydration trace, where fetch
|
|
180
|
+
// and Promise never settle, and cache that.
|
|
168
181
|
if (cached &&
|
|
169
182
|
cached[0] &&
|
|
170
183
|
(isServer ||
|
|
171
184
|
intent === "native" ||
|
|
172
185
|
cached[4].count ||
|
|
186
|
+
(!intent && isHydrating()) ||
|
|
173
187
|
Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
|
|
174
188
|
if (tracking) {
|
|
175
189
|
cached[4].count++;
|
|
@@ -210,8 +224,8 @@ export function query(fn, name) {
|
|
|
210
224
|
entry.status === "pending"
|
|
211
225
|
? entry.promise
|
|
212
226
|
: entry.status === "resolved"
|
|
213
|
-
?
|
|
214
|
-
:
|
|
227
|
+
? NativePromise.resolve(entry.value)
|
|
228
|
+
: NativePromise.reject(entry.error);
|
|
215
229
|
}
|
|
216
230
|
// A payload too old for this intent stays in the registry: it is
|
|
217
231
|
// inert while the refetched entry below lives, but a later claim
|
|
@@ -220,11 +234,10 @@ export function query(fn, name) {
|
|
|
220
234
|
if (!adopted)
|
|
221
235
|
res = fn(...args);
|
|
222
236
|
// Adopted entries are stamped at boot — when their data was actually
|
|
223
|
-
// fetched — not at consumption.
|
|
224
|
-
// through the
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// it would for a preload performed when the server fetched the data.
|
|
237
|
+
// fetched — not at consumption. Later hydration reads still reuse them
|
|
238
|
+
// through the claim rule above, while a navigation that comes along later
|
|
239
|
+
// sees the payload's true age and refetches past PRELOAD_TIMEOUT, exactly
|
|
240
|
+
// as it would for a preload performed when the server fetched the data.
|
|
228
241
|
const stamp = adopted ? bootTime : now;
|
|
229
242
|
if (cached) {
|
|
230
243
|
isServer && forwardedCookies.delete(cached);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, OBSERVE, onSettled, getObserver, isHydratable, $TRACK, action as action$1 } from 'solid-js';
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, OBSERVE, onSettled, getObserver, isHydrating, isHydratable, $TRACK, action as action$1 } from 'solid-js';
|
|
2
2
|
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
|
|
3
3
|
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
|
|
@@ -1190,6 +1190,17 @@ let flashDecoder;
|
|
|
1190
1190
|
function provideFlashDecoder(decoder) {
|
|
1191
1191
|
flashDecoder || (flashDecoder = decoder);
|
|
1192
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* A request's flash cookie and its decode, shared by every router created
|
|
1196
|
+
* for that request. A server render can re-create the router — a suspension
|
|
1197
|
+
* under `<Errored>` retries from the nearest hole above it, which may sit
|
|
1198
|
+
* above the router — so state kept on one router instance would restart the
|
|
1199
|
+
* decode with every re-creation and the seeding read would never settle.
|
|
1200
|
+
* Keyed by the request event, so it lives exactly as long as the request.
|
|
1201
|
+
*/
|
|
1202
|
+
|
|
1203
|
+
const flashStates = new WeakMap();
|
|
1193
1204
|
let preloadIntent;
|
|
1194
1205
|
function getIntent() {
|
|
1195
1206
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
@@ -1228,63 +1239,68 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1228
1239
|
// unread outcome must not haunt a later request's render. Only detection
|
|
1229
1240
|
// and clearing happen here (the runtime's isomorphic half); the raw header
|
|
1230
1241
|
// is stashed and decoding waits for the action-provided codec, read when
|
|
1231
|
-
// the lazily allocated submissions signal below first initializes.
|
|
1232
|
-
|
|
1242
|
+
// the lazily allocated submissions signal below first initializes. Both
|
|
1243
|
+
// happen once per request: a router re-created for the same request finds
|
|
1244
|
+
// the request's flash state (see `flashStates`) and neither clears again
|
|
1245
|
+
// nor decodes again.
|
|
1246
|
+
let flash;
|
|
1233
1247
|
if (isServer) {
|
|
1234
1248
|
const e = getRequestEvent();
|
|
1235
1249
|
if (e && !(e.router && e.router.submission)) {
|
|
1236
|
-
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1250
|
+
flash = flashStates.get(e);
|
|
1251
|
+
if (!flash) {
|
|
1252
|
+
const cookieHeader = e.request.headers.get("cookie");
|
|
1253
|
+
if (hasFlashCookie(cookieHeader)) {
|
|
1254
|
+
flashStates.set(e, flash = {
|
|
1255
|
+
cookieHeader
|
|
1256
|
+
});
|
|
1257
|
+
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
1258
|
+
if (e.response && e.response.headers) e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
1259
|
+
}
|
|
1241
1260
|
}
|
|
1242
1261
|
}
|
|
1243
1262
|
}
|
|
1244
1263
|
|
|
1245
|
-
// The decode, at most once per request: the decoder may answer with a
|
|
1246
|
-
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
1247
|
-
// async), and this cache is what keeps the parked read's rerun from
|
|
1248
|
-
// restarting it — resumption finds the settled outcome and just reads it.
|
|
1249
|
-
// A decoder that rejects reads as "no flash", matching the runtime's own
|
|
1250
|
-
// malformed-cookie semantics.
|
|
1251
|
-
let flashDecode;
|
|
1252
|
-
|
|
1253
1264
|
// The seeding read, as a memo: NotReadyError must surface from a reactive
|
|
1254
1265
|
// node the graph can park and retry — never from router setup, which no
|
|
1255
|
-
// boundary guards — and the memo bounds the recompute to this function
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
//
|
|
1266
|
+
// boundary guards — and the memo bounds the recompute to this function.
|
|
1267
|
+
// The decode runs at most once per request: the decoder may answer with a
|
|
1268
|
+
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
1269
|
+
// async), and the request's cached state is what keeps a parked read's
|
|
1270
|
+
// rerun — or a re-created router's first read — from restarting it. A
|
|
1271
|
+
// decoder that rejects reads as "no flash", matching the runtime's own
|
|
1272
|
+
// malformed-cookie semantics. Created only when a flash cookie actually
|
|
1273
|
+
// arrived (server-only by construction: `flash` is only ever set there), and
|
|
1259
1274
|
// - `lazy`: server memos compute eagerly by default — deferred to first
|
|
1260
1275
|
// read, a request whose submissions are never read never decodes;
|
|
1261
1276
|
// - `transparent`: the memo exists on the server only, so its owner
|
|
1262
1277
|
// must not consume a hydration-id slot — the client, which seeds
|
|
1263
1278
|
// submissions as [] without ever creating this memo, would miss it
|
|
1264
1279
|
// and every sibling id would shift.
|
|
1265
|
-
const flashSubmission =
|
|
1280
|
+
const flashSubmission = flash !== undefined ? createMemo(() => {
|
|
1281
|
+
const state = flash;
|
|
1266
1282
|
if (!flashDecoder) return undefined;
|
|
1267
|
-
if (!
|
|
1268
|
-
const promise = flashDecoder(
|
|
1269
|
-
|
|
1283
|
+
if (!state.decode) {
|
|
1284
|
+
const promise = flashDecoder(state.cookieHeader).then(value => {
|
|
1285
|
+
state.decode = {
|
|
1270
1286
|
done: true,
|
|
1271
1287
|
value
|
|
1272
1288
|
};
|
|
1273
1289
|
}, () => {
|
|
1274
|
-
|
|
1290
|
+
state.decode = {
|
|
1275
1291
|
done: true,
|
|
1276
1292
|
value: undefined
|
|
1277
1293
|
};
|
|
1278
1294
|
});
|
|
1279
|
-
|
|
1295
|
+
state.decode = {
|
|
1280
1296
|
done: false,
|
|
1281
1297
|
promise
|
|
1282
1298
|
};
|
|
1283
1299
|
}
|
|
1284
1300
|
// SSR carries the Promise through NotReadyError so the parked
|
|
1285
1301
|
// reader can resume, exactly like the lazy matches above.
|
|
1286
|
-
if (!
|
|
1287
|
-
return
|
|
1302
|
+
if (!state.decode.done) throw new NotReadyError(state.decode.promise);
|
|
1303
|
+
return state.decode.value;
|
|
1288
1304
|
}, {
|
|
1289
1305
|
lazy: true,
|
|
1290
1306
|
transparent: true
|
|
@@ -2663,6 +2679,13 @@ const forwardedCookies = /* #__PURE__ */new WeakMap();
|
|
|
2663
2679
|
// hold values the server computed while rendering THIS page, so their age is
|
|
2664
2680
|
// anchored here — not at whenever a late query() call happens to consume one.
|
|
2665
2681
|
const bootTime = Date.now();
|
|
2682
|
+
// Captured at module load: a hydrating client runs a serialized node's
|
|
2683
|
+
// compute once as a dependency trace with the global `Promise` swapped for a
|
|
2684
|
+
// never-settling mock, and a query() read inside that trace still consumes
|
|
2685
|
+
// and caches its flight entry. Wrapped with the global there, a settled
|
|
2686
|
+
// entry would cache a promise that never resolves, hanging every navigation
|
|
2687
|
+
// that reuses it.
|
|
2688
|
+
const NativePromise = Promise;
|
|
2666
2689
|
|
|
2667
2690
|
// Every hydratable read ships its entry: the writer keeps the first write
|
|
2668
2691
|
// per key, so a read skipped in a NoHydration zone never hides the entry
|
|
@@ -2787,7 +2810,14 @@ function query(fn, name) {
|
|
|
2787
2810
|
tracking = true;
|
|
2788
2811
|
onCleanup(() => cached[4].count--);
|
|
2789
2812
|
}
|
|
2790
|
-
|
|
2813
|
+
|
|
2814
|
+
// A read with no navigation in flight while hydrating is claiming server
|
|
2815
|
+
// DOM, so it takes the cached entry at any age — the rule adoption below
|
|
2816
|
+
// applies to flight entries. It can come from a boundary resuming long
|
|
2817
|
+
// after boot, once the entry was adopted and its first reader disposed;
|
|
2818
|
+
// refetching there would run inside Solid's hydration trace, where fetch
|
|
2819
|
+
// and Promise never settle, and cache that.
|
|
2820
|
+
if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || !intent && isHydrating() || Date.now() - cached[0] < PRELOAD_TIMEOUT$1)) {
|
|
2791
2821
|
if (tracking) {
|
|
2792
2822
|
cached[4].count++;
|
|
2793
2823
|
cached[4][0](); // track
|
|
@@ -2822,7 +2852,7 @@ function query(fn, name) {
|
|
|
2822
2852
|
const entry = takeHydrationValue(key); // hydrating
|
|
2823
2853
|
if (entry) {
|
|
2824
2854
|
adopted = true;
|
|
2825
|
-
res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ?
|
|
2855
|
+
res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ? NativePromise.resolve(entry.value) : NativePromise.reject(entry.error);
|
|
2826
2856
|
}
|
|
2827
2857
|
// A payload too old for this intent stays in the registry: it is
|
|
2828
2858
|
// inert while the refetched entry below lives, but a later claim
|
|
@@ -2831,11 +2861,10 @@ function query(fn, name) {
|
|
|
2831
2861
|
if (!adopted) res = fn(...args);
|
|
2832
2862
|
|
|
2833
2863
|
// Adopted entries are stamped at boot — when their data was actually
|
|
2834
|
-
// fetched — not at consumption.
|
|
2835
|
-
// through the
|
|
2836
|
-
//
|
|
2837
|
-
//
|
|
2838
|
-
// it would for a preload performed when the server fetched the data.
|
|
2864
|
+
// fetched — not at consumption. Later hydration reads still reuse them
|
|
2865
|
+
// through the claim rule above, while a navigation that comes along later
|
|
2866
|
+
// sees the payload's true age and refetches past PRELOAD_TIMEOUT, exactly
|
|
2867
|
+
// as it would for a preload performed when the server fetched the data.
|
|
2839
2868
|
const stamp = adopted ? bootTime : now;
|
|
2840
2869
|
if (cached) {
|
|
2841
2870
|
isServer && forwardedCookies.delete(cached);
|
|
@@ -3073,7 +3102,7 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3073
3102
|
// stays a no-JS fallback.
|
|
3074
3103
|
// Client-only actions (`https://action/`) are their module's JS by
|
|
3075
3104
|
// definition, so a miss there falls through to native submission.
|
|
3076
|
-
const handler =
|
|
3105
|
+
const handler = findAction(actionRef) || serverAction && createServerFormAction(actionRef);
|
|
3077
3106
|
if (handler) {
|
|
3078
3107
|
evt.preventDefault();
|
|
3079
3108
|
const data = new FormData(form, evt.submitter);
|
|
@@ -3084,6 +3113,30 @@ function handleFormAction(evt, router, actionBase) {
|
|
|
3084
3113
|
}
|
|
3085
3114
|
}
|
|
3086
3115
|
|
|
3116
|
+
/**
|
|
3117
|
+
* Looks a rendered action url up in the registry. A server-rendered
|
|
3118
|
+
* `.with()` url is only registered if this client made the same binding
|
|
3119
|
+
* itself; otherwise its base action (the url without `?args`) is rebound to
|
|
3120
|
+
* the rendered arguments, so the submission runs through that action's
|
|
3121
|
+
* submit and settled hooks and is recorded under its base.
|
|
3122
|
+
*/
|
|
3123
|
+
function findAction(url) {
|
|
3124
|
+
const handler = actions.get(url);
|
|
3125
|
+
if (handler) return handler;
|
|
3126
|
+
const query = url.indexOf("?");
|
|
3127
|
+
if (query < 0) return undefined;
|
|
3128
|
+
const base = actions.get(url.slice(0, query));
|
|
3129
|
+
const args = new URLSearchParams(url.slice(query)).get("args");
|
|
3130
|
+
if (!base || args === null) return undefined;
|
|
3131
|
+
let bound;
|
|
3132
|
+
try {
|
|
3133
|
+
bound = JSON.parse(args);
|
|
3134
|
+
} catch {
|
|
3135
|
+
return undefined;
|
|
3136
|
+
}
|
|
3137
|
+
return Array.isArray(bound) ? base.with(...bound) : undefined;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3087
3140
|
/**
|
|
3088
3141
|
* Synthesizes a router action for a server-rendered action url. The url
|
|
3089
3142
|
* carries everything an invocation needs — the function id in the path
|
|
@@ -3117,7 +3170,7 @@ function createServerFormAction(url) {
|
|
|
3117
3170
|
* the enctype conversion and the generic invocation happen here.
|
|
3118
3171
|
*/
|
|
3119
3172
|
function submitServerForm(router, url, form, data) {
|
|
3120
|
-
const handler =
|
|
3173
|
+
const handler = findAction(url) || createServerFormAction(url);
|
|
3121
3174
|
// not an address (`<endpoint>/<id>`) — not the server function convention;
|
|
3122
3175
|
// nothing can run it, resubmit natively (submit() bypasses the delegated
|
|
3123
3176
|
// handler)
|
|
@@ -3269,9 +3322,12 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
|
|
|
3269
3322
|
return url;
|
|
3270
3323
|
};
|
|
3271
3324
|
fn.with = function (...args) {
|
|
3325
|
+
const bound = [...boundArgs, ...args];
|
|
3272
3326
|
const uri = new URL(url, mockBase);
|
|
3273
|
-
|
|
3274
|
-
|
|
3327
|
+
// the server prepends `args` to the submitted arguments, so it must carry
|
|
3328
|
+
// the whole binding, not just this call's part of a chain
|
|
3329
|
+
uri.searchParams.set("args", hashKey(bound));
|
|
3330
|
+
return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, bound, base, submitHooks, settledHooks);
|
|
3275
3331
|
};
|
|
3276
3332
|
fn.onSubmit = function (hook) {
|
|
3277
3333
|
const id = Symbol("actionOnSubmitHook");
|
package/dist/routing.js
CHANGED
|
@@ -556,6 +556,7 @@ let flashDecoder;
|
|
|
556
556
|
export function provideFlashDecoder(decoder) {
|
|
557
557
|
flashDecoder || (flashDecoder = decoder);
|
|
558
558
|
}
|
|
559
|
+
const flashStates = new WeakMap();
|
|
559
560
|
let preloadIntent;
|
|
560
561
|
export function getIntent() {
|
|
561
562
|
return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
|
|
@@ -592,56 +593,60 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
592
593
|
// unread outcome must not haunt a later request's render. Only detection
|
|
593
594
|
// and clearing happen here (the runtime's isomorphic half); the raw header
|
|
594
595
|
// is stashed and decoding waits for the action-provided codec, read when
|
|
595
|
-
// the lazily allocated submissions signal below first initializes.
|
|
596
|
-
|
|
596
|
+
// the lazily allocated submissions signal below first initializes. Both
|
|
597
|
+
// happen once per request: a router re-created for the same request finds
|
|
598
|
+
// the request's flash state (see `flashStates`) and neither clears again
|
|
599
|
+
// nor decodes again.
|
|
600
|
+
let flash;
|
|
597
601
|
if (isServer) {
|
|
598
602
|
const e = getRequestEvent();
|
|
599
603
|
if (e && !(e.router && e.router.submission)) {
|
|
600
|
-
|
|
601
|
-
if (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
flash = flashStates.get(e);
|
|
605
|
+
if (!flash) {
|
|
606
|
+
const cookieHeader = e.request.headers.get("cookie");
|
|
607
|
+
if (hasFlashCookie(cookieHeader)) {
|
|
608
|
+
flashStates.set(e, (flash = { cookieHeader }));
|
|
609
|
+
// one-shot: clear it even when unreadable so it can't haunt later renders
|
|
610
|
+
if (e.response && e.response.headers)
|
|
611
|
+
e.response.headers.append("Set-Cookie", clearFlashCookie());
|
|
612
|
+
}
|
|
606
613
|
}
|
|
607
614
|
}
|
|
608
615
|
}
|
|
609
|
-
// The decode, at most once per request: the decoder may answer with a
|
|
610
|
-
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
611
|
-
// async), and this cache is what keeps the parked read's rerun from
|
|
612
|
-
// restarting it — resumption finds the settled outcome and just reads it.
|
|
613
|
-
// A decoder that rejects reads as "no flash", matching the runtime's own
|
|
614
|
-
// malformed-cookie semantics.
|
|
615
|
-
let flashDecode;
|
|
616
616
|
// The seeding read, as a memo: NotReadyError must surface from a reactive
|
|
617
617
|
// node the graph can park and retry — never from router setup, which no
|
|
618
|
-
// boundary guards — and the memo bounds the recompute to this function
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
618
|
+
// boundary guards — and the memo bounds the recompute to this function.
|
|
619
|
+
// The decode runs at most once per request: the decoder may answer with a
|
|
620
|
+
// Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
|
|
621
|
+
// async), and the request's cached state is what keeps a parked read's
|
|
622
|
+
// rerun — or a re-created router's first read — from restarting it. A
|
|
623
|
+
// decoder that rejects reads as "no flash", matching the runtime's own
|
|
624
|
+
// malformed-cookie semantics. Created only when a flash cookie actually
|
|
625
|
+
// arrived (server-only by construction: `flash` is only ever set there), and
|
|
622
626
|
// - `lazy`: server memos compute eagerly by default — deferred to first
|
|
623
627
|
// read, a request whose submissions are never read never decodes;
|
|
624
628
|
// - `transparent`: the memo exists on the server only, so its owner
|
|
625
629
|
// must not consume a hydration-id slot — the client, which seeds
|
|
626
630
|
// submissions as [] without ever creating this memo, would miss it
|
|
627
631
|
// and every sibling id would shift.
|
|
628
|
-
const flashSubmission =
|
|
632
|
+
const flashSubmission = flash !== undefined
|
|
629
633
|
? createMemo(() => {
|
|
634
|
+
const state = flash;
|
|
630
635
|
if (!flashDecoder)
|
|
631
636
|
return undefined;
|
|
632
|
-
if (!
|
|
633
|
-
const promise = flashDecoder(
|
|
634
|
-
|
|
637
|
+
if (!state.decode) {
|
|
638
|
+
const promise = flashDecoder(state.cookieHeader).then(value => {
|
|
639
|
+
state.decode = { done: true, value };
|
|
635
640
|
}, () => {
|
|
636
|
-
|
|
641
|
+
state.decode = { done: true, value: undefined };
|
|
637
642
|
});
|
|
638
|
-
|
|
643
|
+
state.decode = { done: false, promise };
|
|
639
644
|
}
|
|
640
645
|
// SSR carries the Promise through NotReadyError so the parked
|
|
641
646
|
// reader can resume, exactly like the lazy matches above.
|
|
642
|
-
if (!
|
|
643
|
-
throw new NotReadyError(
|
|
644
|
-
return
|
|
647
|
+
if (!state.decode.done)
|
|
648
|
+
throw new NotReadyError(state.decode.promise);
|
|
649
|
+
return state.decode.value;
|
|
645
650
|
}, { lazy: true, transparent: true })
|
|
646
651
|
: undefined;
|
|
647
652
|
let submissions;
|