@solidjs/router 0.17.0-next.5 → 0.17.0-next.6
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.d.ts +13 -1
- package/dist/data/action.js +58 -32
- package/dist/data/flash.d.ts +25 -0
- package/dist/data/flash.js +86 -0
- package/dist/data/query.js +11 -4
- package/dist/index.js +472 -347
- package/dist/routers/components.jsx +9 -2
- package/dist/routing.d.ts +9 -0
- package/dist/routing.js +31 -4
- package/dist/server.d.ts +65 -0
- package/dist/server.js +228 -0
- package/dist/types.d.ts +1 -1
- package/package.json +10 -8
package/dist/data/action.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JSX } from "@solidjs/web";
|
|
2
|
-
import type { Submission, NarrowResponse } from "../types.js";
|
|
2
|
+
import type { RouterContext, Submission, 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>;
|
|
@@ -16,4 +16,16 @@ export declare const actions: Map<string, Action<any, any, any>>;
|
|
|
16
16
|
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<V, NarrowResponse<U>>[];
|
|
17
17
|
export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>;
|
|
18
18
|
export declare const action: ActionFactory;
|
|
19
|
+
/**
|
|
20
|
+
* Registers the router as the single-flight consumer of the server function
|
|
21
|
+
* transport. Subscribing is the opt-in: while registered, the transport
|
|
22
|
+
* sends the `X-Single-Flight` request header on mutations and delivers the
|
|
23
|
+
* folded payload here — fresh route data is seeded into the `query` cache
|
|
24
|
+
* and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
|
|
25
|
+
* applied, all before the action sees its plain return value. Called by the
|
|
26
|
+
* Router component on the client unless `singleFlight={false}`, which now
|
|
27
|
+
* simply means "never subscribe" — no consumer, no request header, no
|
|
28
|
+
* collection work on the server. Returns the unsubscribe function.
|
|
29
|
+
*/
|
|
30
|
+
export declare function setupFlightDataConsumer(router: RouterContext): () => void;
|
|
19
31
|
export {};
|
package/dist/data/action.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
|
|
2
2
|
import { isResponseEnvelope, isServer } from "@solidjs/web";
|
|
3
|
-
import { decodeResponse } from "@solidjs/web/server-functions";
|
|
3
|
+
import { decodeResponse, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
4
|
import { useRouter } from "../routing.js";
|
|
5
5
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
6
6
|
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
@@ -32,9 +32,10 @@ function actionImpl(fn, options = {}) {
|
|
|
32
32
|
const form = this.f;
|
|
33
33
|
const submitHooks = current[submitHooksSymbol];
|
|
34
34
|
const settledHooks = current[settledHooksSymbol];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// Single-flight opt-in is no longer per call: the router's registered
|
|
36
|
+
// flight-data consumer (see setupFlightDataConsumer) makes the transport
|
|
37
|
+
// send the request header itself, so the mutation is just called.
|
|
38
|
+
const runMutation = () => fn(...variables);
|
|
38
39
|
const run = createSolidAction(async function* (context) {
|
|
39
40
|
context.optimistic?.();
|
|
40
41
|
try {
|
|
@@ -147,11 +148,52 @@ async function settleActionResult(result) {
|
|
|
147
148
|
}
|
|
148
149
|
return result;
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Registers the router as the single-flight consumer of the server function
|
|
153
|
+
* transport. Subscribing is the opt-in: while registered, the transport
|
|
154
|
+
* sends the `X-Single-Flight` request header on mutations and delivers the
|
|
155
|
+
* folded payload here — fresh route data is seeded into the `query` cache
|
|
156
|
+
* and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
|
|
157
|
+
* applied, all before the action sees its plain return value. Called by the
|
|
158
|
+
* Router component on the client unless `singleFlight={false}`, which now
|
|
159
|
+
* simply means "never subscribe" — no consumer, no request header, no
|
|
160
|
+
* collection work on the server. Returns the unsubscribe function.
|
|
161
|
+
*/
|
|
162
|
+
export function setupFlightDataConsumer(router) {
|
|
163
|
+
return subscribeFlightData((data, { response }) => applyResponseMetadata(response, router.navigatorFactory(), data));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Applies a server function response's integration metadata: `X-Revalidate`
|
|
167
|
+
* keys invalidate, `Location` navigates (hard for absolute urls), flight
|
|
168
|
+
* data seeds the query cache, and matching entries revalidate. Shared by
|
|
169
|
+
* the flight-data consumer and the action response path (which still sees
|
|
170
|
+
* metadata-bearing responses when no flight data was collected).
|
|
171
|
+
*/
|
|
172
|
+
async function applyResponseMetadata(metadata, navigate, flightData) {
|
|
173
|
+
let keys;
|
|
174
|
+
if (metadata) {
|
|
175
|
+
if (metadata.headers.has("X-Revalidate"))
|
|
176
|
+
keys = metadata.headers.get("X-Revalidate").split(",");
|
|
177
|
+
if (metadata.headers.has("Location")) {
|
|
178
|
+
const locationUrl = metadata.headers.get("Location") || "/";
|
|
179
|
+
if (locationUrl.startsWith("http")) {
|
|
180
|
+
window.location.href = locationUrl;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
navigate(locationUrl);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// invalidate
|
|
188
|
+
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
189
|
+
// set cache
|
|
190
|
+
flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
191
|
+
// trigger revalidation
|
|
192
|
+
await revalidate(keys, false);
|
|
193
|
+
}
|
|
150
194
|
async function handleResponse(response, error, navigate) {
|
|
151
195
|
let data;
|
|
152
|
-
let
|
|
153
|
-
let keys;
|
|
154
|
-
let flightKeys;
|
|
196
|
+
let flightData;
|
|
155
197
|
let metadata;
|
|
156
198
|
if (isResponseEnvelope(response)) {
|
|
157
199
|
// client-only respond(): the value rides in memory beside the metadata
|
|
@@ -160,15 +202,17 @@ async function handleResponse(response, error, navigate) {
|
|
|
160
202
|
}
|
|
161
203
|
else if (response instanceof Response) {
|
|
162
204
|
metadata = response;
|
|
163
|
-
// responses the transport hands over whole (redirects, revalidation
|
|
164
|
-
//
|
|
205
|
+
// responses the transport hands over whole (redirects, revalidation)
|
|
206
|
+
// carry a codec-encoded body the router decodes itself. With the
|
|
207
|
+
// flight-data consumer registered single-flight payloads never reach
|
|
208
|
+
// this path, but a manually opted-in call (no consumer) still can —
|
|
209
|
+
// unwrap the standardized { value, data } shape for it too.
|
|
165
210
|
if (response.body) {
|
|
166
211
|
data = await decodeResponse(response);
|
|
167
212
|
if (response.headers.has("X-Single-Flight")) {
|
|
168
|
-
|
|
169
|
-
data =
|
|
170
|
-
|
|
171
|
-
flightKeys = Object.keys(custom);
|
|
213
|
+
const payload = data;
|
|
214
|
+
data = payload.value;
|
|
215
|
+
flightData = payload.data;
|
|
172
216
|
}
|
|
173
217
|
}
|
|
174
218
|
}
|
|
@@ -176,24 +220,6 @@ async function handleResponse(response, error, navigate) {
|
|
|
176
220
|
return { error: response };
|
|
177
221
|
else
|
|
178
222
|
data = response;
|
|
179
|
-
|
|
180
|
-
if (metadata.headers.has("X-Revalidate"))
|
|
181
|
-
keys = metadata.headers.get("X-Revalidate").split(",");
|
|
182
|
-
if (metadata.headers.has("Location")) {
|
|
183
|
-
const locationUrl = metadata.headers.get("Location") || "/";
|
|
184
|
-
if (locationUrl.startsWith("http")) {
|
|
185
|
-
window.location.href = locationUrl;
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
navigate(locationUrl);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// invalidate
|
|
193
|
-
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
194
|
-
// set cache
|
|
195
|
-
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
196
|
-
// trigger revalidation
|
|
197
|
-
await revalidate(keys, false);
|
|
223
|
+
await applyResponseMetadata(metadata, navigate, flightData);
|
|
198
224
|
return data != null ? { data } : undefined;
|
|
199
225
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const FLASH_COOKIE = "flash";
|
|
2
|
+
/** What rides the cookie: a Submission minus its lifecycle methods. */
|
|
3
|
+
export interface FlashSubmission {
|
|
4
|
+
input: any[];
|
|
5
|
+
url: string;
|
|
6
|
+
result?: any;
|
|
7
|
+
error?: any;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Encodes the outcome of a no-JS submission as a Set-Cookie value. `url` is
|
|
11
|
+
* the action url (pathname + search of the server function call) so the
|
|
12
|
+
* seeded submission matches `useSubmission` filters; `thrown` errors land on
|
|
13
|
+
* `error`, returned values on `result` (mirroring the scripted split).
|
|
14
|
+
*/
|
|
15
|
+
export declare function encodeFlashCookie(url: string, result: any, input: any[], thrown?: boolean): string;
|
|
16
|
+
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
17
|
+
export declare function clearFlashCookie(): string;
|
|
18
|
+
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
19
|
+
export declare function hasFlashCookie(cookieHeader: string | null): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Decodes the flash cookie out of a request's Cookie header. Returns
|
|
22
|
+
* undefined when absent or unreadable (a malformed cookie must never take
|
|
23
|
+
* down SSR — it is cleared either way).
|
|
24
|
+
*/
|
|
25
|
+
export declare function decodeFlashCookie(cookieHeader: string | null): FlashSubmission | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The no-JS form convention's cookie codec. When a form posts to a server
|
|
2
|
+
// function without the client runtime (no instance header), the server
|
|
3
|
+
// handler redirects back carrying the outcome in a one-shot "flash" cookie;
|
|
4
|
+
// the next SSR pass reads it here and seeds the router's submission state so
|
|
5
|
+
// useSubmission() renders the result exactly as a scripted submission would.
|
|
6
|
+
// Both halves live in this module so the write (src/server.ts, the handler's
|
|
7
|
+
// handleNoJS) and the read (src/routing.ts, SSR initialization) can never
|
|
8
|
+
// drift apart.
|
|
9
|
+
export const FLASH_COOKIE = "flash";
|
|
10
|
+
// Form payloads have no JSON encoding, so entries are captured as pair
|
|
11
|
+
// arrays under a marker key ($f / $u) and revived to real FormData /
|
|
12
|
+
// URLSearchParams on the way out. File entries cannot ride a cookie and are
|
|
13
|
+
// dropped.
|
|
14
|
+
function encodeInputValue(value) {
|
|
15
|
+
if (value instanceof FormData)
|
|
16
|
+
return { $f: [...value.entries()].filter(([, v]) => typeof v === "string") };
|
|
17
|
+
if (value instanceof URLSearchParams)
|
|
18
|
+
return { $u: [...value.entries()] };
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function decodeInputValue(value) {
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
if (Array.isArray(value.$f)) {
|
|
24
|
+
const form = new FormData();
|
|
25
|
+
for (const [k, v] of value.$f)
|
|
26
|
+
form.append(k, v);
|
|
27
|
+
return form;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value.$u))
|
|
30
|
+
return new URLSearchParams(value.$u);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Encodes the outcome of a no-JS submission as a Set-Cookie value. `url` is
|
|
36
|
+
* the action url (pathname + search of the server function call) so the
|
|
37
|
+
* seeded submission matches `useSubmission` filters; `thrown` errors land on
|
|
38
|
+
* `error`, returned values on `result` (mirroring the scripted split).
|
|
39
|
+
*/
|
|
40
|
+
export function encodeFlashCookie(url, result, input, thrown) {
|
|
41
|
+
const isError = result instanceof Error;
|
|
42
|
+
const payload = {
|
|
43
|
+
url,
|
|
44
|
+
result: isError ? result.message : result,
|
|
45
|
+
error: isError,
|
|
46
|
+
thrown: !!thrown,
|
|
47
|
+
input: input.map(encodeInputValue)
|
|
48
|
+
};
|
|
49
|
+
return `${FLASH_COOKIE}=${encodeURIComponent(JSON.stringify(payload))}; Secure; HttpOnly; Path=/`;
|
|
50
|
+
}
|
|
51
|
+
/** The Set-Cookie value clearing the flash cookie after it has been read. */
|
|
52
|
+
export function clearFlashCookie() {
|
|
53
|
+
return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
|
|
54
|
+
}
|
|
55
|
+
const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
|
|
56
|
+
/** Whether a Cookie header carries a flash cookie (readable or not). */
|
|
57
|
+
export function hasFlashCookie(cookieHeader) {
|
|
58
|
+
return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Decodes the flash cookie out of a request's Cookie header. Returns
|
|
62
|
+
* undefined when absent or unreadable (a malformed cookie must never take
|
|
63
|
+
* down SSR — it is cleared either way).
|
|
64
|
+
*/
|
|
65
|
+
export function decodeFlashCookie(cookieHeader) {
|
|
66
|
+
if (!cookieHeader)
|
|
67
|
+
return;
|
|
68
|
+
const match = cookieHeader.match(FLASH_MATCHER);
|
|
69
|
+
if (!match)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
const payload = JSON.parse(decodeURIComponent(match[1]));
|
|
73
|
+
if (!payload || !payload.result)
|
|
74
|
+
return;
|
|
75
|
+
const result = payload.error ? new Error(payload.result) : payload.result;
|
|
76
|
+
return {
|
|
77
|
+
input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
|
|
78
|
+
url: payload.url,
|
|
79
|
+
result: payload.thrown ? undefined : result,
|
|
80
|
+
error: payload.thrown ? result : undefined
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/data/query.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
|
|
2
2
|
import { getRequestEvent, isResponseEnvelope, isServer } from "@solidjs/web";
|
|
3
|
-
import { decodeResponse } from "@solidjs/web/server-functions";
|
|
3
|
+
import { GET, decodeResponse, getServerFunctionMetadata, isServerFunction } from "@solidjs/web/server-functions";
|
|
4
4
|
import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
|
|
5
5
|
const LocationHeader = "Location";
|
|
6
6
|
const PRELOAD_TIMEOUT = 5000;
|
|
@@ -43,9 +43,16 @@ export function cacheKeyOp(key, fn) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
export function query(fn, name) {
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// query implies GET: the router primitive is the declaration site, so a
|
|
47
|
+
// server function handed to query() is wrapped with core `GET(fn)` here,
|
|
48
|
+
// at query-creation (module scope) — the server half records the method
|
|
49
|
+
// declaration for dispatch, the client half swaps in the GET transport.
|
|
50
|
+
// An explicit `GET(fn)` already carries the declaration on the metadata
|
|
51
|
+
// channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
|
|
52
|
+
// through; non-server functions are untouched.
|
|
53
|
+
if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
|
|
54
|
+
fn = GET(fn);
|
|
55
|
+
}
|
|
49
56
|
const cachedFn = ((...args) => {
|
|
50
57
|
const cache = getCache();
|
|
51
58
|
const intent = getIntent();
|