@rangojs/router 0.23.0 → 0.24.1
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/bin/rango.js +4 -3
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/partial-update.d.ts +6 -4
- package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/types.d.ts +5 -1
- package/dist/types/build/route-types/scan-filter.d.ts +7 -1
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/types.d.ts +8 -0
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/handles/pushed-by.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/request-context.d.ts +39 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/serve-shell-request.d.ts +2 -1
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/vite/discovery/state.d.ts +12 -9
- package/dist/vite/index.js +164 -103
- package/package.json +1 -1
- package/skills/caching/SKILL.md +12 -0
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +86 -1
- package/skills/hooks/SKILL.md +11 -6
- package/skills/hooks/data.md +3 -1
- package/skills/loader/SKILL.md +7 -1
- package/skills/middleware/SKILL.md +183 -2
- package/skills/ppr/SKILL.md +9 -0
- package/skills/rango/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/client-components.md +1 -0
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +19 -1
- package/skills/use-cache/SKILL.md +4 -2
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/event-controller.ts +3 -0
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +19 -3
- package/src/browser/partial-update.ts +27 -12
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +46 -26
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/server-action-bridge.ts +6 -0
- package/src/browser/types.ts +5 -1
- package/src/build/route-types/scan-filter.ts +11 -4
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +12 -119
- package/src/client-urls/client-root.tsx +3 -3
- package/src/client-urls/client-urls.ts +8 -1
- package/src/client-urls/types.ts +8 -0
- package/src/context-var.ts +32 -2
- package/src/errors.ts +19 -0
- package/src/handles/pushed-by.ts +26 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +29 -12
- package/src/router/internal-params.ts +16 -0
- package/src/router/loader-resolution.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +11 -9
- package/src/router/match-middleware/cache-lookup.ts +20 -15
- package/src/router/match-middleware/cache-store.ts +9 -11
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
- package/src/router/segment-resolution/revalidation.ts +57 -33
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +4 -2
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +48 -17
- package/src/server/request-context.ts +154 -63
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/index.ts +1 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +2 -0
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/serve-shell-request.ts +15 -2
- package/src/thenable-status.ts +11 -0
- package/src/types/request-scope.ts +8 -0
- package/src/vite/discovery/state.ts +21 -13
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/router-discovery.ts +29 -27
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pass loop that honors `throw rewrite()` (route-definition/rewrite.ts):
|
|
3
|
+
* one catch around the global middleware chain moves the same request
|
|
4
|
+
* context to the target (server/request-context.ts rebaseRequestContext) and
|
|
5
|
+
* runs the request again from the top, as a direct visit to the target
|
|
6
|
+
* would run it. Shared by the request handler (rsc/handler.ts) and the
|
|
7
|
+
* dispatch testing primitive, so the two cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RewriteLoopError } from "../errors.js";
|
|
11
|
+
import {
|
|
12
|
+
describeRewriteTarget,
|
|
13
|
+
isRewrite,
|
|
14
|
+
rewriteNotSupported,
|
|
15
|
+
type Rewrite,
|
|
16
|
+
} from "../route-definition/rewrite.js";
|
|
17
|
+
import { prefixBasename } from "../router/basename.js";
|
|
18
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
19
|
+
import { stripInternalParams } from "../router/internal-params.js";
|
|
20
|
+
import {
|
|
21
|
+
rebaseRequestContext,
|
|
22
|
+
type RequestContext,
|
|
23
|
+
} from "../server/request-context.js";
|
|
24
|
+
import { recordRewriteHop } from "../server/rewrite-trail.js";
|
|
25
|
+
|
|
26
|
+
/** Response header naming the route path a rewritten request rendered. */
|
|
27
|
+
export const REWRITE_HEADER = "x-rango-rewrite";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a rewrite's target against the router's route map (global names,
|
|
31
|
+
* no param auto-fill, as middleware `ctx.reverse`) and the request origin.
|
|
32
|
+
* A path is basename-prefixed like `redirect()` and must stay under the
|
|
33
|
+
* basename once normalized (`/../admin` cannot step out of it). The
|
|
34
|
+
* target's own `_rsc*` params are dropped: only the visitor's transport
|
|
35
|
+
* params carry over. Throws for an unknown name, a missing param, another
|
|
36
|
+
* origin or a path outside the basename.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveRewriteTarget(
|
|
39
|
+
rewrite: Rewrite,
|
|
40
|
+
routeMap: Record<string, string>,
|
|
41
|
+
base: URL,
|
|
42
|
+
basename?: string,
|
|
43
|
+
): URL {
|
|
44
|
+
const { target } = rewrite;
|
|
45
|
+
const path =
|
|
46
|
+
"path" in target
|
|
47
|
+
? prefixBasename(target.path, basename)
|
|
48
|
+
: // `{}`, not undefined: a route with params then throws for the
|
|
49
|
+
// missing ones instead of resolving to its raw pattern.
|
|
50
|
+
createReverseFunction(routeMap)(
|
|
51
|
+
target.name,
|
|
52
|
+
target.params ?? {},
|
|
53
|
+
target.search,
|
|
54
|
+
);
|
|
55
|
+
const resolved = stripInternalParams(new URL(path, base.origin));
|
|
56
|
+
if (resolved.origin !== base.origin) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
`${describeRewriteTarget(target)} must name a path in this app, not another origin.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
basename &&
|
|
63
|
+
resolved.pathname !== basename &&
|
|
64
|
+
!resolved.pathname.startsWith(basename + "/")
|
|
65
|
+
) {
|
|
66
|
+
throw new TypeError(
|
|
67
|
+
`${describeRewriteTarget(target)} resolves to ${resolved.pathname}, outside the router's basename ${basename}.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
resolved.hash = "";
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The raw URL of a rewritten pass: the target's path and search plus the
|
|
76
|
+
* visitor's internal `_rsc*` transport params, so a navigation stays a
|
|
77
|
+
* navigation (same segments, version, partial flag).
|
|
78
|
+
*/
|
|
79
|
+
function rewriteTransportUrl(target: URL, visitorRaw: URL): URL {
|
|
80
|
+
const raw = new URL(target);
|
|
81
|
+
visitorRaw.searchParams.forEach((value, key) => {
|
|
82
|
+
if (key.startsWith("_rsc")) raw.searchParams.append(key, value);
|
|
83
|
+
});
|
|
84
|
+
return raw;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The next pass of a rewritten request. */
|
|
88
|
+
export interface RewritePass {
|
|
89
|
+
request: Request;
|
|
90
|
+
/** The pass's raw URL, `_rsc*` params included. */
|
|
91
|
+
url: URL;
|
|
92
|
+
/** The target's clean URL. */
|
|
93
|
+
target: URL;
|
|
94
|
+
hops: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Move the request context to `rewrite`'s target for the next pass: check
|
|
99
|
+
* that this request can be rewritten, resolve the target, count the hop,
|
|
100
|
+
* then rebase the context. Throws the site error for a mutation or a loader
|
|
101
|
+
* fetch, a resolution error, and RewriteLoopError.
|
|
102
|
+
*/
|
|
103
|
+
export function applyRewrite(
|
|
104
|
+
rewrite: Rewrite,
|
|
105
|
+
passRequest: Request,
|
|
106
|
+
reqCtx: RequestContext<any>,
|
|
107
|
+
routeMap: Record<string, string>,
|
|
108
|
+
basename: string | undefined,
|
|
109
|
+
): RewritePass {
|
|
110
|
+
// A loader fetch first: `load()` with a body is a POST, and its refusal
|
|
111
|
+
// is the loader fetch's.
|
|
112
|
+
if (reqCtx.originalUrl.searchParams.has("_rsc_loader")) {
|
|
113
|
+
throw rewriteNotSupported(rewrite, "loader-fetch");
|
|
114
|
+
}
|
|
115
|
+
if (passRequest.method !== "GET" && passRequest.method !== "HEAD") {
|
|
116
|
+
throw rewriteNotSupported(rewrite, "mutation");
|
|
117
|
+
}
|
|
118
|
+
const target = resolveRewriteTarget(rewrite, routeMap, reqCtx.url, basename);
|
|
119
|
+
const hops = recordRewriteHop(reqCtx, reqCtx.url, target);
|
|
120
|
+
const url = rewriteTransportUrl(target, reqCtx.originalUrl);
|
|
121
|
+
const request = new Request(url, passRequest);
|
|
122
|
+
rebaseRequestContext(reqCtx, request, url);
|
|
123
|
+
return { request, url, target, hops };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The response to a rewrite loop. */
|
|
127
|
+
export function loopDetectedResponse(): Response {
|
|
128
|
+
return new Response("Loop Detected", {
|
|
129
|
+
status: 508,
|
|
130
|
+
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface RewritePassesOptions {
|
|
135
|
+
reqCtx: RequestContext<any>;
|
|
136
|
+
/** The visitor's request and raw URL: the first pass. */
|
|
137
|
+
request: Request;
|
|
138
|
+
url: URL;
|
|
139
|
+
routeMap: () => Record<string, string>;
|
|
140
|
+
basename: string | undefined;
|
|
141
|
+
/** One pass: the global chain matched on `url`'s path around the core handler. */
|
|
142
|
+
runPass: (request: Request, url: URL) => Promise<Response>;
|
|
143
|
+
/** After a hop moved the context to its target. */
|
|
144
|
+
onHop?: (pass: RewritePass) => void;
|
|
145
|
+
/** Report a rewrite the request cannot take (onError, phase "routing"). */
|
|
146
|
+
report: (error: unknown, at: { request: Request; url: URL }) => void;
|
|
147
|
+
/**
|
|
148
|
+
* The response for a refused rewrite other than a loop (a loader fetch's
|
|
149
|
+
* error response); undefined rethrows the error.
|
|
150
|
+
*/
|
|
151
|
+
refuse?: (error: unknown, rewrite: Rewrite) => Response | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RewritePassesResult {
|
|
155
|
+
response: Response;
|
|
156
|
+
/** The pathname the final pass rendered, when a rewrite took the request there. */
|
|
157
|
+
rewrittenTo?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run the request pass by pass until one produces a response. A `Rewrite`
|
|
162
|
+
* that unwinds a pass moves the context to its target and runs the next;
|
|
163
|
+
* a loop answers 508; a rewrite the request cannot take is reported and
|
|
164
|
+
* becomes `refuse`'s response, or is thrown.
|
|
165
|
+
*/
|
|
166
|
+
export async function runRewritePasses(
|
|
167
|
+
opts: RewritePassesOptions,
|
|
168
|
+
): Promise<RewritePassesResult> {
|
|
169
|
+
let request = opts.request;
|
|
170
|
+
let url = opts.url;
|
|
171
|
+
let rewrittenTo: string | undefined;
|
|
172
|
+
for (;;) {
|
|
173
|
+
let rewrite: Rewrite;
|
|
174
|
+
try {
|
|
175
|
+
return { response: await opts.runPass(request, url), rewrittenTo };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (!isRewrite(error)) throw error;
|
|
178
|
+
rewrite = error;
|
|
179
|
+
}
|
|
180
|
+
let pass: RewritePass;
|
|
181
|
+
try {
|
|
182
|
+
pass = applyRewrite(
|
|
183
|
+
rewrite,
|
|
184
|
+
request,
|
|
185
|
+
opts.reqCtx,
|
|
186
|
+
opts.routeMap(),
|
|
187
|
+
opts.basename,
|
|
188
|
+
);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
opts.report(error, { request, url });
|
|
191
|
+
if (error instanceof RewriteLoopError) {
|
|
192
|
+
return { response: loopDetectedResponse() };
|
|
193
|
+
}
|
|
194
|
+
const refused = opts.refuse?.(error, rewrite);
|
|
195
|
+
if (refused) return { response: refused };
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
request = pass.request;
|
|
199
|
+
url = pass.url;
|
|
200
|
+
rewrittenTo = pass.target.pathname;
|
|
201
|
+
opts.onHop?.(pass);
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
import type { HandlerContext } from "./handler-context.js";
|
|
52
52
|
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
53
53
|
import { publicRouteName } from "../route-name.js";
|
|
54
|
-
import { buildFullPayload } from "./full-payload.js";
|
|
54
|
+
import { buildFullPayload, renderedLocation } from "./full-payload.js";
|
|
55
55
|
import {
|
|
56
56
|
capShellWindow,
|
|
57
57
|
lastStoredCaptureSeq,
|
|
@@ -845,7 +845,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
845
845
|
kind: "payload",
|
|
846
846
|
payload: {
|
|
847
847
|
metadata: {
|
|
848
|
-
pathname: url.pathname,
|
|
848
|
+
pathname: renderedLocation(reqCtx, url).pathname,
|
|
849
849
|
// routerId is serialized on every payload (including within-session
|
|
850
850
|
// ones) so the frontend can read the current app/router identity. It
|
|
851
851
|
// always equals the current app's id: a cross-app navigation is
|
|
@@ -1867,6 +1867,8 @@ function serveShellHit(
|
|
|
1867
1867
|
payload.metadata.initialTheme = entry.initialTheme as
|
|
1868
1868
|
| import("../theme/types.js").Theme
|
|
1869
1869
|
| undefined;
|
|
1870
|
+
// Same for the location: the capture rendered the target's.
|
|
1871
|
+
payload.metadata.pathname = url.pathname;
|
|
1870
1872
|
}
|
|
1871
1873
|
// Full Flight render per request: hydration needs the whole payload (there
|
|
1872
1874
|
// is no Flight-side resume — a React limitation, not ours).
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
RSC_WIRE_MIME,
|
|
23
23
|
} from "../router/content-negotiation.js";
|
|
24
24
|
import { _getRequestContext } from "../server/request-context.js";
|
|
25
|
+
import { renderedLocation } from "./full-payload.js";
|
|
25
26
|
|
|
26
27
|
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
27
28
|
|
|
@@ -38,6 +39,8 @@ export const SSR_SETUP_VAR = "__ssrSetup";
|
|
|
38
39
|
* lazily so stores created after kickoff are still captured). Without a
|
|
39
40
|
* getter the promises run bare — no `.then()` microtasks, no
|
|
40
41
|
* `performance.now()` calls — keeping the non-debug hot path lean.
|
|
42
|
+
* `ssrModule` reuses a load already started (a rewritten request's earlier
|
|
43
|
+
* pass).
|
|
41
44
|
*/
|
|
42
45
|
export function startSSRSetup<TEnv>(
|
|
43
46
|
ctx: HandlerContext<TEnv>,
|
|
@@ -45,16 +48,17 @@ export function startSSRSetup<TEnv>(
|
|
|
45
48
|
env: TEnv,
|
|
46
49
|
url: URL,
|
|
47
50
|
getMetricsStore?: () => MetricsStore | undefined,
|
|
51
|
+
ssrModule?: Promise<SSRModule>,
|
|
48
52
|
): Promise<SSRSetup> {
|
|
49
53
|
if (!getMetricsStore) {
|
|
50
54
|
return Promise.all([
|
|
51
|
-
ctx.loadSSRModule(),
|
|
55
|
+
ssrModule ?? ctx.loadSSRModule(),
|
|
52
56
|
ctx.resolveStreamMode(request, env, url),
|
|
53
57
|
]);
|
|
54
58
|
}
|
|
55
59
|
const start = performance.now();
|
|
56
60
|
return Promise.all([
|
|
57
|
-
ctx.loadSSRModule().then((mod) => {
|
|
61
|
+
(ssrModule ?? ctx.loadSSRModule()).then((mod) => {
|
|
58
62
|
appendMetric(
|
|
59
63
|
getMetricsStore(),
|
|
60
64
|
"ssr:module-load",
|
|
@@ -110,7 +114,9 @@ export function createSsrHtmlStage<TEnv>(
|
|
|
110
114
|
options.url,
|
|
111
115
|
options.metricsStore,
|
|
112
116
|
);
|
|
113
|
-
const
|
|
117
|
+
const reqCtx = _getRequestContext();
|
|
118
|
+
const renderErrors = reqCtx?._renderErrors;
|
|
119
|
+
const location = renderedLocation(reqCtx, options.url);
|
|
114
120
|
return {
|
|
115
121
|
render: () =>
|
|
116
122
|
// search: the LIVE request's query string, threaded out-of-band into
|
|
@@ -121,11 +127,11 @@ export function createSsrHtmlStage<TEnv>(
|
|
|
121
127
|
ssrModule.renderHTML(flight.stream, {
|
|
122
128
|
...options.render,
|
|
123
129
|
streamMode,
|
|
124
|
-
search:
|
|
130
|
+
search: location.search,
|
|
125
131
|
// origin: same out-of-band channel — seeds the SSR store location
|
|
126
132
|
// so origin-dependent markup (Link's data-external) agrees with the
|
|
127
133
|
// browser's window.location across hydration.
|
|
128
|
-
origin:
|
|
134
|
+
origin: location.origin,
|
|
129
135
|
// A component that throws inside a Suspense boundary leaves an
|
|
130
136
|
// errored boundary in a completed document; the document cache
|
|
131
137
|
// reads the list before storing it.
|
|
@@ -10,23 +10,47 @@
|
|
|
10
10
|
export function parseCookiesFromHeader(
|
|
11
11
|
cookieHeader: string | null,
|
|
12
12
|
): Record<string, string> {
|
|
13
|
-
if (!cookieHeader) return {};
|
|
14
|
-
|
|
15
13
|
const cookies: Record<string, string> = {};
|
|
16
|
-
const pairs = cookieHeader.split(";");
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
if (cookieHeader) {
|
|
16
|
+
for (const pair of cookieHeader.split(";")) {
|
|
17
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
18
|
+
if (name) {
|
|
19
|
+
const raw = rest.join("=");
|
|
20
|
+
let value: string;
|
|
21
|
+
try {
|
|
22
|
+
value = decodeURIComponent(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
// Malformed percent-encoding: fall back to raw value
|
|
25
|
+
value = raw;
|
|
26
|
+
}
|
|
27
|
+
setCookie(cookies, name, value);
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Null prototype (#1109): cookie names come from the request, so a lookup
|
|
33
|
+
// must never answer `constructor` or `toString` with an inherited member.
|
|
34
|
+
// Set once at the end: Object.create(null) starts in dictionary mode, which
|
|
35
|
+
// makes a spread of the jar several times slower.
|
|
36
|
+
Object.setPrototypeOf(cookies, null);
|
|
31
37
|
return cookies;
|
|
32
38
|
}
|
|
39
|
+
|
|
40
|
+
/** `cookies[name] = value`, a cookie named `__proto__` as an own key too. */
|
|
41
|
+
function setCookie(
|
|
42
|
+
cookies: Record<string, string>,
|
|
43
|
+
name: string,
|
|
44
|
+
value: string,
|
|
45
|
+
): void {
|
|
46
|
+
if (name === "__proto__") {
|
|
47
|
+
Object.defineProperty(cookies, name, {
|
|
48
|
+
value,
|
|
49
|
+
writable: true,
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
cookies[name] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -367,12 +367,16 @@ function createCookieStore(
|
|
|
367
367
|
|
|
368
368
|
getAll(name?: string): Cookie[] {
|
|
369
369
|
recordLoaderIdentityRead("cookies()");
|
|
370
|
-
const all = ctx._readCookies();
|
|
371
370
|
if (name !== undefined) {
|
|
372
|
-
|
|
371
|
+
// The single-name read, not an index into the plain object
|
|
372
|
+
// _readCookies() returns: that would answer an inherited member.
|
|
373
|
+
const value = ctx._readCookie(name);
|
|
373
374
|
return value !== undefined ? [{ name, value }] : [];
|
|
374
375
|
}
|
|
375
|
-
return Object.entries(
|
|
376
|
+
return Object.entries(ctx._readCookies()).map(([n, v]) => ({
|
|
377
|
+
name: n,
|
|
378
|
+
value: v,
|
|
379
|
+
}));
|
|
376
380
|
},
|
|
377
381
|
|
|
378
382
|
has(name: string): boolean {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./context.js";
|
|
6
6
|
import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
|
|
7
7
|
import { pushedByKey } from "../handles/pushed-by.js";
|
|
8
|
+
import { defineSlot } from "./slot-accessors.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Handle data structure: handleName -> segmentId -> entries[]
|
|
@@ -394,6 +395,34 @@ export interface HandleStore {
|
|
|
394
395
|
): void;
|
|
395
396
|
}
|
|
396
397
|
|
|
398
|
+
/**
|
|
399
|
+
* settled and fullySettled: shared descriptors (slot-accessors.ts) that call
|
|
400
|
+
* the store's closure in SETTLEMENT_SLOT with the lane they wait for.
|
|
401
|
+
*/
|
|
402
|
+
const SETTLEMENT_SLOT: unique symbol = Symbol("rango.handleStore.settlement");
|
|
403
|
+
|
|
404
|
+
type SettlementSlot = { [SETTLEMENT_SLOT]: (full: boolean) => Promise<void> };
|
|
405
|
+
|
|
406
|
+
const SETTLEMENT_ACCESSORS: Record<
|
|
407
|
+
"settled" | "fullySettled",
|
|
408
|
+
PropertyDescriptor
|
|
409
|
+
> = {
|
|
410
|
+
settled: {
|
|
411
|
+
get(this: SettlementSlot): Promise<void> {
|
|
412
|
+
return this[SETTLEMENT_SLOT](false);
|
|
413
|
+
},
|
|
414
|
+
enumerable: true,
|
|
415
|
+
configurable: true,
|
|
416
|
+
},
|
|
417
|
+
fullySettled: {
|
|
418
|
+
get(this: SettlementSlot): Promise<void> {
|
|
419
|
+
return this[SETTLEMENT_SLOT](true);
|
|
420
|
+
},
|
|
421
|
+
enumerable: true,
|
|
422
|
+
configurable: true,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
|
|
397
426
|
/**
|
|
398
427
|
* Create a new HandleStore instance.
|
|
399
428
|
*
|
|
@@ -774,7 +803,9 @@ export function createHandleStore(): HandleStore {
|
|
|
774
803
|
});
|
|
775
804
|
}
|
|
776
805
|
|
|
777
|
-
|
|
806
|
+
// Data members only: the accessors are SETTLEMENT_ACCESSORS.
|
|
807
|
+
const store: Omit<HandleStore, "settled" | "fullySettled"> &
|
|
808
|
+
ThisType<HandleStore> = {
|
|
778
809
|
track<T>(promise: Promise<T>): Promise<T> {
|
|
779
810
|
inflightCount++;
|
|
780
811
|
return trackSettlement(promise, () => {
|
|
@@ -793,22 +824,6 @@ export function createHandleStore(): HandleStore {
|
|
|
793
824
|
sealInternal();
|
|
794
825
|
},
|
|
795
826
|
|
|
796
|
-
get settled(): Promise<void> {
|
|
797
|
-
if (sealed && inflightCount === 0) return Promise.resolve();
|
|
798
|
-
return new Promise<void>((resolve) => {
|
|
799
|
-
drainWaiters.push(resolve);
|
|
800
|
-
});
|
|
801
|
-
},
|
|
802
|
-
|
|
803
|
-
get fullySettled(): Promise<void> {
|
|
804
|
-
if (sealed && inflightCount === 0 && auxInflightCount === 0) {
|
|
805
|
-
return Promise.resolve();
|
|
806
|
-
}
|
|
807
|
-
return new Promise<void>((resolve) => {
|
|
808
|
-
fullDrainWaiters.push(resolve);
|
|
809
|
-
});
|
|
810
|
-
},
|
|
811
|
-
|
|
812
827
|
push(
|
|
813
828
|
handleName: string,
|
|
814
829
|
segmentId: string,
|
|
@@ -1213,4 +1228,20 @@ export function createHandleStore(): HandleStore {
|
|
|
1213
1228
|
signalEmission();
|
|
1214
1229
|
},
|
|
1215
1230
|
};
|
|
1231
|
+
// settled: the handler lane drained; fullySettled: the auxiliary lane too.
|
|
1232
|
+
defineSlot(store, SETTLEMENT_SLOT, (full: boolean): Promise<void> => {
|
|
1233
|
+
if (sealed && inflightCount === 0 && (!full || auxInflightCount === 0)) {
|
|
1234
|
+
return Promise.resolve();
|
|
1235
|
+
}
|
|
1236
|
+
return new Promise<void>((resolve) => {
|
|
1237
|
+
(full ? fullDrainWaiters : drainWaiters).push(resolve);
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
Object.defineProperty(store, "settled", SETTLEMENT_ACCESSORS.settled);
|
|
1241
|
+
Object.defineProperty(
|
|
1242
|
+
store,
|
|
1243
|
+
"fullySettled",
|
|
1244
|
+
SETTLEMENT_ACCESSORS.fullySettled,
|
|
1245
|
+
);
|
|
1246
|
+
return store as HandleStore;
|
|
1216
1247
|
}
|