@rivetjs/router 0.1.0-alpha.0 → 0.1.0-alpha.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/adapters/files.d.ts +13 -0
- package/adapters/files.js +58 -6
- package/adapters/node.d.ts +22 -5
- package/adapters/node.js +113 -67
- package/adapters/static.js +1 -1
- package/adapters/target-build.d.ts +5 -2
- package/adapters/vercel.d.ts +4 -2
- package/adapters/vercel.js +1 -1
- package/build/codegen.d.ts +13 -0
- package/build/codegen.js +152 -35
- package/build/dev-errors.d.ts +6 -2
- package/build/dev-errors.js +15 -4
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +20 -4
- package/build/migrate.js +300 -61
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/routes.d.ts +9 -4
- package/build/routes.js +30 -13
- package/build/vite.d.ts +13 -0
- package/build/vite.js +165 -7
- package/cli/rivet.js +3 -2
- package/client/dev.js +17 -0
- package/client/index.d.ts +4 -0
- package/client/index.js +34 -0
- package/client/route-context.d.ts +1 -1
- package/index.d.ts +22 -25
- package/index.js +2 -1
- package/package.json +4 -4
- package/server/app.d.ts +7 -1
- package/server/app.js +38 -5
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/handler.d.ts +56 -12
- package/server/handler.js +46 -8
- package/server/index.d.ts +1 -1
- package/server/module.d.ts +7 -7
- package/server/module.js +52 -34
- package/server/render.d.ts +2 -0
- package/server/render.js +3 -0
- package/server/route-cache.d.ts +74 -4
- package/server/route-cache.js +77 -6
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/shared/server-exports.d.ts +78 -0
- package/shared/server-exports.js +77 -0
package/server/route-cache.d.ts
CHANGED
|
@@ -53,6 +53,25 @@ export interface RouteRule {
|
|
|
53
53
|
* cache's own `maxAge` / `swr`, `isr` (and `maxAge: Infinity`) rules `s-maxage=0, must-revalidate`.
|
|
54
54
|
*/
|
|
55
55
|
readonly cdn?: CdnRule;
|
|
56
|
+
/**
|
|
57
|
+
* Shell cache (partial prerendering, `server/shell-cache.ts`): the document up to its first streamed boundary (the shell:
|
|
58
|
+
* everything outside `<Async fallback>` holes reading deferred loaders) is stored once and sent at once on later
|
|
59
|
+
* requests — signed-in ones too (cookies never bypass it, never key it) — while the holes render live per request in
|
|
60
|
+
* the same response. `true`: the rule's `cache` / `swr` / `isr` times, else stored until `revalidate()` (needs a fixed
|
|
61
|
+
* `origin`, like `isr`); an object sets them. Fills that read request input outside the key (cookies, headers, locals,
|
|
62
|
+
* unkeyed query parameters) are not stored (dev logs why). `false` turns off a less specific rule's shell. Not with `cdn`.
|
|
63
|
+
*/
|
|
64
|
+
readonly shell?: boolean | ShellRule;
|
|
65
|
+
}
|
|
66
|
+
/** `RouteRule.shell` times and keys (defaults: the rule's `cache` / `swr` / `isr`, else stored until `revalidate()`). */
|
|
67
|
+
export interface ShellRule {
|
|
68
|
+
readonly maxAge?: number;
|
|
69
|
+
readonly swr?: number | true;
|
|
70
|
+
readonly tags?: readonly string[];
|
|
71
|
+
/** Request headers whose values key separate shells (never `cookie` / `authorization`). */
|
|
72
|
+
readonly varies?: readonly string[];
|
|
73
|
+
/** `true` (default): one shell per query string; `false`: query-less URLs only; a list: those parameters (other URLs render without the shell). */
|
|
74
|
+
readonly query?: boolean | readonly string[];
|
|
56
75
|
}
|
|
57
76
|
/**
|
|
58
77
|
* Rules by path pattern (pathname below the app's base): `/about` exact, `*` one segment (`/blog/*`), a trailing
|
|
@@ -124,15 +143,45 @@ interface Cache {
|
|
|
124
143
|
/** Headers responses named in `Vary` (beyond `varies`): part of the key from then on. */
|
|
125
144
|
readonly learned: string[];
|
|
126
145
|
}
|
|
127
|
-
|
|
146
|
+
/** @internal A rule's normalised cache (exported for `shell-cache.ts`). */
|
|
147
|
+
export type { Cache as _Cache };
|
|
148
|
+
/** @internal */
|
|
149
|
+
export interface Resolved {
|
|
128
150
|
readonly headers: readonly (readonly [string, string])[];
|
|
129
151
|
readonly cache: Cache | undefined;
|
|
152
|
+
/** Shell cache (`RouteRule.shell`): wins over `cache` for documents. */
|
|
153
|
+
readonly shell?: Cache;
|
|
130
154
|
}
|
|
131
155
|
/**
|
|
132
156
|
* Compile `rules` (throws `TypeError` on a malformed pattern or rule) into a pathname → rule lookup. `origin`: a fixed
|
|
133
157
|
* origin is configured (`isr` rules need one).
|
|
134
158
|
*/
|
|
135
159
|
export declare function compileRules(rules: RouteRules, origin?: boolean): (pathname: string) => Resolved | undefined;
|
|
160
|
+
/** Bounded page store, CLOCK eviction (a hit sets one bit). Shell entries (`shell-cache.ts`) live here too (key `…\0shell`). */
|
|
161
|
+
declare class Store {
|
|
162
|
+
readonly pages: Map<string, CachedPage>;
|
|
163
|
+
/** Shell cache: rejected fills per route id (3 in a row → no more attempts until a `revalidate()`). */
|
|
164
|
+
readonly sticky: Map<string, number>;
|
|
165
|
+
bytes: number;
|
|
166
|
+
/** Fills running now (`path`, `tags`): a `revalidate()` matching one voids it (it answers but does not store). */
|
|
167
|
+
readonly filling: Set<{
|
|
168
|
+
readonly path: string;
|
|
169
|
+
readonly tags: readonly string[];
|
|
170
|
+
void: boolean;
|
|
171
|
+
}>;
|
|
172
|
+
readonly maxBytes: number;
|
|
173
|
+
readonly maxEntries: number;
|
|
174
|
+
readonly base: string;
|
|
175
|
+
constructor(maxBytes: number, maxEntries: number, base: string);
|
|
176
|
+
get(key: string): CachedPage | undefined;
|
|
177
|
+
set(page: CachedPage): void;
|
|
178
|
+
/** `page` (stored) grew by `bytes`. */
|
|
179
|
+
grow(page: CachedPage, bytes: number): void;
|
|
180
|
+
private evict;
|
|
181
|
+
delete(key: string): boolean;
|
|
182
|
+
}
|
|
183
|
+
/** @internal The page store type (`shell-cache.ts` keeps its entries in it). */
|
|
184
|
+
export type { Store as _Store };
|
|
136
185
|
/**
|
|
137
186
|
* The router's view of a raw pathname: `match` — segments percent-decoded (a decoded `/` stays `%2F`: one segment),
|
|
138
187
|
* what rules match and keys use; `canonical` — the raw path is that path's one encoding (only those are stored, so
|
|
@@ -167,9 +216,30 @@ export interface CachePolicy {
|
|
|
167
216
|
}
|
|
168
217
|
/** Own property the app sets on a request whose render reported a failure (`onError`: an error fallback was rendered). */
|
|
169
218
|
export declare const RENDER_FAILED: unique symbol;
|
|
219
|
+
/** @internal What `withRouteRules` hands the shell cache (`shell-cache.ts` `createShells`). */
|
|
220
|
+
export interface ShellHost {
|
|
221
|
+
readonly handler: AppHandler;
|
|
222
|
+
readonly store: Store;
|
|
223
|
+
readonly csp: CachePolicy;
|
|
224
|
+
readonly fixed: string | undefined;
|
|
225
|
+
readonly dev: boolean;
|
|
226
|
+
/** Build identity (the resume runtime's URL): part of each shell's `h`. */
|
|
227
|
+
readonly build: string;
|
|
228
|
+
keyOf(cache: Cache, origin: string, path: string, search: string, header: HeaderOf): string;
|
|
229
|
+
secondMiss(cache: Cache, search: string, header: HeaderOf): boolean;
|
|
230
|
+
readonly doorkeeper: Set<string>;
|
|
231
|
+
withHeaders(response: Response, headers: Resolved["headers"]): Response;
|
|
232
|
+
}
|
|
233
|
+
/** @internal The shell cache's request entry (`shell-cache.ts`); undefined: render without it. */
|
|
234
|
+
export type ShellServe = (request: Request, context: Parameters<AppHandler>[1], resolved: Resolved, origin: string, path: string, search: string) => Promise<Response> | undefined;
|
|
170
235
|
/**
|
|
171
236
|
* Wrap the app's handler with route rules. `base`: the app's base path (stripped before matching). The result
|
|
172
|
-
* carries `ROUTE_CACHE` (node adapter fast path).
|
|
237
|
+
* carries `ROUTE_CACHE` (node adapter fast path). `shells`: the shell cache module (`createApp` loads it only for
|
|
238
|
+
* rules with `shell`); `internal.dev`: shell rules only probe (never store; reasons logged), `internal.build`: build id.
|
|
173
239
|
*/
|
|
174
|
-
export declare function withRouteRules(handler: AppHandler, rules: RouteRules, csp: CachePolicy, base?: string, options?: RouteCacheOptions
|
|
175
|
-
|
|
240
|
+
export declare function withRouteRules(handler: AppHandler, rules: RouteRules, csp: CachePolicy, base?: string, options?: RouteCacheOptions, shells?: {
|
|
241
|
+
createShells(host: ShellHost): ShellServe;
|
|
242
|
+
}, internal?: {
|
|
243
|
+
dev?: boolean;
|
|
244
|
+
build?: string;
|
|
245
|
+
}): CachingHandler;
|
package/server/route-cache.js
CHANGED
|
@@ -3,7 +3,8 @@ import { hashPolicy, noncedSources } from "../shared/csp-hash.js";
|
|
|
3
3
|
import { DATA_PARAM } from "../shared/data.js";
|
|
4
4
|
/** Own property of a handler with route rules: the cache lookup. */
|
|
5
5
|
export const ROUTE_CACHE = Symbol.for("rivet.routeCache");
|
|
6
|
-
const RULE_KEYS = new Set(["headers", "cache", "swr", "isr", "cdn"]);
|
|
6
|
+
const RULE_KEYS = new Set(["headers", "cache", "swr", "isr", "cdn", "shell"]);
|
|
7
|
+
const SHELL_KEYS = new Set(["maxAge", "swr", "tags", "varies", "query"]);
|
|
7
8
|
const CREDENTIALS = new Set(["cookie", "authorization"]);
|
|
8
9
|
// Request headers a fill / refresh never forwards (unless the rule's `varies` names them): credentials and what
|
|
9
10
|
// identifies the visitor behind a proxy — a stored page must not depend on the visitor whose miss rendered it.
|
|
@@ -82,6 +83,37 @@ function cacheOf(rule, pattern, origin) {
|
|
|
82
83
|
learned: [],
|
|
83
84
|
};
|
|
84
85
|
}
|
|
86
|
+
/** Check and normalise one rule's `shell` (defaults: the rule's own cache times, else stored until `revalidate()`). */
|
|
87
|
+
function shellOf(rule, pattern, origin, cache) {
|
|
88
|
+
const where = `"${pattern}"`;
|
|
89
|
+
const given = rule.shell;
|
|
90
|
+
if (given === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (given === false)
|
|
93
|
+
return false;
|
|
94
|
+
if (given !== true && (typeof given !== "object" || given === null))
|
|
95
|
+
throw new TypeError(`routeRules ${where}: shell must be true, false or { maxAge, swr, tags, varies, query }`);
|
|
96
|
+
if (rule.cdn !== undefined)
|
|
97
|
+
throw new TypeError(`routeRules ${where}: shell cannot take cdn (shell documents carry per-visitor holes: private, no-cache)`);
|
|
98
|
+
const own = given === true ? {} : given;
|
|
99
|
+
for (const key of Object.keys(own))
|
|
100
|
+
if (!SHELL_KEYS.has(key))
|
|
101
|
+
throw new TypeError(`routeRules ${where}: unknown shell option "${key}"`);
|
|
102
|
+
const base = cache || undefined;
|
|
103
|
+
const maxAge = own.maxAge ?? base?.maxAge ?? Infinity;
|
|
104
|
+
const swr = own.swr ?? (base ? (base.swr === Infinity ? true : base.swr) : undefined);
|
|
105
|
+
const normal = cacheOf({
|
|
106
|
+
cache: {
|
|
107
|
+
maxAge,
|
|
108
|
+
...(swr !== undefined ? { swr } : {}),
|
|
109
|
+
...((own.tags ?? base?.tags)?.length ? { tags: own.tags ?? base.tags } : {}),
|
|
110
|
+
...((own.varies ?? base?.varies)?.length ? { varies: own.varies ?? base.varies } : {}),
|
|
111
|
+
...(own.query !== undefined ? { query: own.query } : base ? { query: base.query } : {}),
|
|
112
|
+
},
|
|
113
|
+
}, pattern + " shell", origin);
|
|
114
|
+
// Documents with live holes: never shared-cacheable, revalidated by the browser every time.
|
|
115
|
+
return normal ? { ...normal, control: "private, no-cache" } : undefined;
|
|
116
|
+
}
|
|
85
117
|
/**
|
|
86
118
|
* Compile `rules` (throws `TypeError` on a malformed pattern or rule) into a pathname → rule lookup. `origin`: a fixed
|
|
87
119
|
* origin is configured (`isr` rules need one).
|
|
@@ -89,6 +121,7 @@ function cacheOf(rule, pattern, origin) {
|
|
|
89
121
|
export function compileRules(rules, origin = false) {
|
|
90
122
|
const compiled = [];
|
|
91
123
|
const caches = new Map();
|
|
124
|
+
const shells = new Map();
|
|
92
125
|
for (const [pattern, rule] of Object.entries(rules)) {
|
|
93
126
|
const segments = pattern.split("/");
|
|
94
127
|
if (!pattern.startsWith("/") ||
|
|
@@ -103,7 +136,10 @@ export function compileRules(rules, origin = false) {
|
|
|
103
136
|
for (const [name, value] of Object.entries(rule.headers))
|
|
104
137
|
if (!/^[!#$%&'*+.^`|~\w-]+$/.test(name) || typeof value !== "string" || /[\r\n\0]/.test(value))
|
|
105
138
|
throw new TypeError(`routeRules "${pattern}": invalid header ${JSON.stringify(name)}`);
|
|
106
|
-
|
|
139
|
+
const own = cacheOf(rule, pattern, origin);
|
|
140
|
+
caches.set(rule, own);
|
|
141
|
+
if (rule.shell !== undefined)
|
|
142
|
+
shells.set(rule, shellOf(rule, pattern, origin, own));
|
|
107
143
|
const any = pattern.endsWith("/**");
|
|
108
144
|
const body = any ? pattern.slice(0, -3) : pattern;
|
|
109
145
|
const source = body
|
|
@@ -120,6 +156,7 @@ export function compileRules(rules, origin = false) {
|
|
|
120
156
|
if (hit === undefined) {
|
|
121
157
|
let headers;
|
|
122
158
|
let cache;
|
|
159
|
+
let shell;
|
|
123
160
|
let matched = false;
|
|
124
161
|
for (const { test, rule } of compiled) {
|
|
125
162
|
if (!test.test(pathname))
|
|
@@ -131,8 +168,16 @@ export function compileRules(rules, origin = false) {
|
|
|
131
168
|
const own = caches.get(rule);
|
|
132
169
|
if (own !== undefined)
|
|
133
170
|
cache = own;
|
|
171
|
+
const shelled = shells.get(rule);
|
|
172
|
+
if (shelled !== undefined)
|
|
173
|
+
shell = shelled;
|
|
134
174
|
}
|
|
135
|
-
|
|
175
|
+
// A shell rule wins for documents: no whole-page copy of them (their holes are per visitor).
|
|
176
|
+
hit = matched
|
|
177
|
+
? shell
|
|
178
|
+
? { headers: headers ? [...headers] : [], cache: undefined, shell }
|
|
179
|
+
: { headers: headers ? [...headers] : [], cache: cache || undefined }
|
|
180
|
+
: null;
|
|
136
181
|
// Bounded: request paths are client-controlled.
|
|
137
182
|
if (memo.size >= 2048)
|
|
138
183
|
memo.clear();
|
|
@@ -141,9 +186,11 @@ export function compileRules(rules, origin = false) {
|
|
|
141
186
|
return hit ?? undefined;
|
|
142
187
|
};
|
|
143
188
|
}
|
|
144
|
-
/** Bounded page store, CLOCK eviction (a hit sets one bit). */
|
|
189
|
+
/** Bounded page store, CLOCK eviction (a hit sets one bit). Shell entries (`shell-cache.ts`) live here too (key `…\0shell`). */
|
|
145
190
|
class Store {
|
|
146
191
|
pages = new Map();
|
|
192
|
+
/** Shell cache: rejected fills per route id (3 in a row → no more attempts until a `revalidate()`). */
|
|
193
|
+
sticky = new Map();
|
|
147
194
|
bytes = 0;
|
|
148
195
|
/** Fills running now (`path`, `tags`): a `revalidate()` matching one voids it (it answers but does not store). */
|
|
149
196
|
filling = new Set();
|
|
@@ -268,6 +315,7 @@ function dropLocal(target) {
|
|
|
268
315
|
for (const fill of store.filling)
|
|
269
316
|
if (matches(fill))
|
|
270
317
|
fill.void = true;
|
|
318
|
+
store.sticky.clear();
|
|
271
319
|
for (const page of [...store.pages.values()])
|
|
272
320
|
if (matches(page) && store.delete(page.key))
|
|
273
321
|
dropped++;
|
|
@@ -398,9 +446,10 @@ const warn = (path, why) => {
|
|
|
398
446
|
const backoff = (failures) => Math.min(1000 * 2 ** (failures - 1), 300_000);
|
|
399
447
|
/**
|
|
400
448
|
* Wrap the app's handler with route rules. `base`: the app's base path (stripped before matching). The result
|
|
401
|
-
* carries `ROUTE_CACHE` (node adapter fast path).
|
|
449
|
+
* carries `ROUTE_CACHE` (node adapter fast path). `shells`: the shell cache module (`createApp` loads it only for
|
|
450
|
+
* rules with `shell`); `internal.dev`: shell rules only probe (never store; reasons logged), `internal.build`: build id.
|
|
402
451
|
*/
|
|
403
|
-
export function withRouteRules(handler, rules, csp, base = "", options = {}) {
|
|
452
|
+
export function withRouteRules(handler, rules, csp, base = "", options = {}, shells, internal = {}) {
|
|
404
453
|
const fixed = options.origin === undefined ? undefined : new URL(options.origin).origin;
|
|
405
454
|
const resolve = compileRules(rules, fixed !== undefined);
|
|
406
455
|
const store = new Store(options.maxBytes ?? 32 * 1024 * 1024, options.maxEntries ?? 5000, base);
|
|
@@ -491,6 +540,18 @@ export function withRouteRules(handler, rules, csp, base = "", options = {}) {
|
|
|
491
540
|
return copy;
|
|
492
541
|
}
|
|
493
542
|
};
|
|
543
|
+
const shellServe = shells?.createShells({
|
|
544
|
+
handler,
|
|
545
|
+
store,
|
|
546
|
+
csp,
|
|
547
|
+
fixed,
|
|
548
|
+
dev: internal.dev === true,
|
|
549
|
+
build: internal.build ?? "",
|
|
550
|
+
keyOf,
|
|
551
|
+
secondMiss,
|
|
552
|
+
doorkeeper,
|
|
553
|
+
withHeaders,
|
|
554
|
+
});
|
|
494
555
|
const age = (page, now) => String(Math.max(0, Math.floor((now - page.created) / 1000)));
|
|
495
556
|
/** Serve a stored page (`state`: HIT / STALE), 304 when the request's validators hold. */
|
|
496
557
|
const serve = (page, request, state) => {
|
|
@@ -698,6 +759,16 @@ export function withRouteRules(handler, rules, csp, base = "", options = {}) {
|
|
|
698
759
|
const resolved = raw === null ? undefined : resolve(path);
|
|
699
760
|
if (!resolved)
|
|
700
761
|
return handler(request, context);
|
|
762
|
+
// Seeded `locals` (`context.locals`): the page depends on input that is not in the URL, so it is neither read
|
|
763
|
+
// from nor written to the store (rule headers still apply).
|
|
764
|
+
if (context?.locals)
|
|
765
|
+
return withHeaders(await handler(request, context), resolved.headers);
|
|
766
|
+
// Shell rules (documents): stored shell + live holes (`shell-cache.ts`); undefined = render as usual.
|
|
767
|
+
if (resolved.shell && shellServe && canonical) {
|
|
768
|
+
const served = shellServe(request, context, resolved, origin, path, search);
|
|
769
|
+
if (served)
|
|
770
|
+
return served;
|
|
771
|
+
}
|
|
701
772
|
const header = (name) => request.headers.get(name);
|
|
702
773
|
const cache = cacheFor(resolved, request.method, origin, canonical, search, header);
|
|
703
774
|
if (!cache)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ShellHost, ShellServe } from "./route-cache.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where the shell ends in `text` (latin1 view of the document so far): after the root state tail — the state script,
|
|
4
|
+
* the loader script and (with pending boundaries, `data-stream`) the stream add-on. -1: not yet (or never).
|
|
5
|
+
*/
|
|
6
|
+
export declare function shellCut(text: string): number;
|
|
7
|
+
/** The route rules' shell entry point (`withRouteRules`): undefined = render without the shell. */
|
|
8
|
+
export declare function createShells(host: ShellHost): ShellServe;
|