@rsc-kit/core 0.30.19 → 0.30.20
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/compress.js +47 -5
- package/dist/compress.js.map +1 -1
- package/dist/vite.js +36 -5
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/compress.js
CHANGED
|
@@ -17,6 +17,18 @@ import { deflate } from "./compressRuntime.js";
|
|
|
17
17
|
const COMPRESSIBLE = /^(?:text\/|application\/(?:json|javascript|xml|manifest\+json|rss\+xml|atom\+xml)|image\/svg\+xml)/i;
|
|
18
18
|
/** Answers too small to be worth a compressor's frame. */
|
|
19
19
|
const MIN_BYTES = 1024;
|
|
20
|
+
/**
|
|
21
|
+
* How long a body that has begun is given to end, or to reach MIN_BYTES,
|
|
22
|
+
* before it is taken to be streaming.
|
|
23
|
+
*
|
|
24
|
+
* A complete answer built from a string ends in the same turn it starts, so
|
|
25
|
+
* this is only ever spent on one that is still being written. Waiting for it
|
|
26
|
+
* to finish was the bug: a client navigation that skips every layout sends a
|
|
27
|
+
* shell under a kilobyte and then the page's slow data, and the shell sat in
|
|
28
|
+
* the peek until that data was in. The loading state it carried reached
|
|
29
|
+
* nobody, and the old page stayed on screen the whole time.
|
|
30
|
+
*/
|
|
31
|
+
const PEEK_MS = 5;
|
|
20
32
|
/** Stored answers, compressed once: the same bytes go to every visitor who accepts gzip. */
|
|
21
33
|
const stored = new Map();
|
|
22
34
|
const STORED_CAP = 1000;
|
|
@@ -87,18 +99,31 @@ export async function compressed(request, response, storedKey) {
|
|
|
87
99
|
// first kilobyte decides. An answer that ends inside it - /api/health, a
|
|
88
100
|
// 15-byte "ok" - goes out as it was; anything longer is compressed from
|
|
89
101
|
// its first byte, the peeked part included, and keeps streaming.
|
|
102
|
+
//
|
|
103
|
+
// The first chunk is waited for however long it takes, since it is the
|
|
104
|
+
// first byte. After it, only PEEK_MS: a body still being written after
|
|
105
|
+
// that is a stream, and a stream is compressed from its first chunk - its
|
|
106
|
+
// shell must not wait for the rest of it to decide a question of size.
|
|
90
107
|
const reader = response.body.getReader();
|
|
91
108
|
const peeked = [];
|
|
92
109
|
let size = 0;
|
|
93
110
|
let ended = false;
|
|
111
|
+
// A read that lost the race is still a read: the next chunk is its, and
|
|
112
|
+
// asking again would skip one.
|
|
113
|
+
let pending = null;
|
|
94
114
|
while (size < MIN_BYTES) {
|
|
95
|
-
const
|
|
96
|
-
|
|
115
|
+
const read = pending ?? reader.read();
|
|
116
|
+
pending = read;
|
|
117
|
+
const result = size === 0 ? await read : await within(read, PEEK_MS);
|
|
118
|
+
if (result === null)
|
|
119
|
+
break;
|
|
120
|
+
pending = null;
|
|
121
|
+
if (result.done) {
|
|
97
122
|
ended = true;
|
|
98
123
|
break;
|
|
99
124
|
}
|
|
100
|
-
peeked.push(value);
|
|
101
|
-
size += value.byteLength;
|
|
125
|
+
peeked.push(result.value);
|
|
126
|
+
size += result.value.byteLength;
|
|
102
127
|
}
|
|
103
128
|
if (ended) {
|
|
104
129
|
return new Response(size === 0 ? null : concat(peeked, size), {
|
|
@@ -113,7 +138,9 @@ export async function compressed(request, response, storedKey) {
|
|
|
113
138
|
controller.enqueue(chunk);
|
|
114
139
|
},
|
|
115
140
|
async pull(controller) {
|
|
116
|
-
const
|
|
141
|
+
const read = pending ?? reader.read();
|
|
142
|
+
pending = null;
|
|
143
|
+
const { done, value } = await read;
|
|
117
144
|
if (done)
|
|
118
145
|
controller.close();
|
|
119
146
|
else
|
|
@@ -125,6 +152,21 @@ export async function compressed(request, response, storedKey) {
|
|
|
125
152
|
});
|
|
126
153
|
return withEncoding(response, gzip.stream(rest));
|
|
127
154
|
}
|
|
155
|
+
/** What the promise settles with, or null if it has not within `ms`. */
|
|
156
|
+
async function within(promise, ms) {
|
|
157
|
+
let timer;
|
|
158
|
+
try {
|
|
159
|
+
return await Promise.race([
|
|
160
|
+
promise,
|
|
161
|
+
new Promise((resolve) => {
|
|
162
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
163
|
+
}),
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
128
170
|
function concat(chunks, size) {
|
|
129
171
|
const out = new Uint8Array(size);
|
|
130
172
|
let at = 0;
|
package/dist/compress.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compress.js","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"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","sourcesContent":["// Compression for what the host answers, where nothing in front of it will.\n//\n// Behind Cloudflare, Vercel or nginx the platform compresses and this never\n// runs. A bun or node server answering the internet by itself sent every\n// byte raw - a 90 kB document and 145 kB of stylesheet at their full size -\n// and that alone was the mobile Lighthouse regression a port measured:\n// first paint at 3.8 s where 1.2 s was the baseline. Public assets are\n// precompressed by the build and served by Nitro with their encoding; the\n// pages, payloads, stored answers and api routes come through here.\n//\n// gzip, flushed on every chunk. A streamed page is a shell followed by the\n// holes as they fill, and a compressor left to its own buffer would hold\n// the shell back until enough had accumulated - so each write is flushed\n// out, which costs a little ratio and keeps the stream a stream.\n\nimport { deflate } from \"./compressRuntime.js\";\n\n/** What is worth compressing: text of every kind, and the formats that are text. */\nconst COMPRESSIBLE = /^(?:text\\/|application\\/(?:json|javascript|xml|manifest\\+json|rss\\+xml|atom\\+xml)|image\\/svg\\+xml)/i;\n\n/** Answers too small to be worth a compressor's frame. */\nconst MIN_BYTES = 1024;\n\n/** Stored answers, compressed once: the same bytes go to every visitor who accepts gzip. */\nconst stored = new Map<string, Uint8Array>();\n\nconst STORED_CAP = 1000;\n\nfunction acceptsGzip(request: Request): boolean {\n const accept = request.headers.get(\"accept-encoding\") ?? \"\";\n\n return /(?:^|,)\\s*(?:gzip|\\*)\\s*(?:;\\s*q=(?!0(?:\\.0*)?\\s*(?:,|$)))?/i.test(accept);\n}\n\n/** Whether this answer is one to compress at all. */\nexport function shouldCompress(request: Request, response: Response): boolean {\n if (response.body === null || request.method === \"HEAD\") return false;\n if (response.status === 204 || response.status === 304 || response.status < 200) return false;\n if (response.headers.has(\"content-encoding\")) return false;\n if (/\\bno-transform\\b/i.test(response.headers.get(\"cache-control\") ?? \"\")) return false;\n const type = response.headers.get(\"content-type\") ?? \"\";\n\n if (!COMPRESSIBLE.test(type)) return false;\n // A stream of events is text, and must not be compressed: each message is\n // a few bytes that has to reach the browser the moment it is written, and\n // a compressor between them delivered nothing at all - not even headers -\n // to a browser that sent Accept-Encoding: gzip, which is every browser.\n if (/^text\\/event-stream\\b/i.test(type)) return false;\n\n const length = Number(response.headers.get(\"content-length\"));\n\n if (Number.isFinite(length) && length > 0 && length < MIN_BYTES) return false;\n\n return acceptsGzip(request);\n}\n\nfunction withEncoding(response: Response, body: BodyInit): Response {\n const headers = new Headers(response.headers);\n\n headers.set(\"Content-Encoding\", \"gzip\");\n headers.delete(\"Content-Length\");\n\n const vary = headers.get(\"Vary\");\n\n if (!vary) headers.set(\"Vary\", \"Accept-Encoding\");\n else if (!/\\baccept-encoding\\b/i.test(vary) && vary.trim() !== \"*\") headers.set(\"Vary\", vary + \", Accept-Encoding\");\n\n return new Response(body, { status: response.status, statusText: response.statusText, headers });\n}\n\n/**\n * The response, gzipped if the request accepts it and the answer is worth it.\n *\n * `storedKey` names an answer the build wrote - the same bytes for every\n * visitor - so it is compressed once and kept; everything else is streamed\n * through the compressor as it is produced.\n */\nexport async function compressed(\n request: Request,\n response: Response,\n storedKey?: string,\n): Promise<Response> {\n if (!shouldCompress(request, response)) return response;\n\n const gzip = await deflate();\n\n if (!gzip) return response;\n\n if (storedKey !== undefined) {\n let bytes = stored.get(storedKey);\n\n if (!bytes) {\n bytes = await gzip.whole(new Uint8Array(await response.arrayBuffer()));\n\n if (stored.size >= STORED_CAP) stored.clear();\n\n stored.set(storedKey, bytes);\n }\n\n return withEncoding(response, bytes as unknown as BodyInit);\n }\n\n // A Response made from a string or JSON carries no Content-Length for the\n // check above to read, so the body is peeked: what arrives before the\n // first kilobyte decides. An answer that ends inside it - /api/health, a\n // 15-byte \"ok\" - goes out as it was; anything longer is compressed from\n // its first byte, the peeked part included, and keeps streaming.\n const reader = response.body!.getReader();\n const peeked: Uint8Array[] = [];\n let size = 0;\n let ended = false;\n\n while (size < MIN_BYTES) {\n const { done, value } = await reader.read();\n\n if (done) {\n ended = true;\n break;\n }\n\n peeked.push(value);\n size += value.byteLength;\n }\n\n if (ended) {\n return new Response(size === 0 ? null : (concat(peeked, size) as unknown as BodyInit), {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n const rest = new ReadableStream<Uint8Array>({\n start(controller) {\n for (const chunk of peeked) controller.enqueue(chunk);\n },\n async pull(controller) {\n const { done, value } = await reader.read();\n\n if (done) controller.close();\n else controller.enqueue(value);\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n\n return withEncoding(response, gzip.stream(rest));\n}\n\nfunction concat(chunks: Uint8Array[], size: number): Uint8Array {\n const out = new Uint8Array(size);\n let at = 0;\n\n for (const chunk of chunks) {\n out.set(chunk, at);\n at += chunk.byteLength;\n }\n\n return out;\n}\n\n/** For a test, or a redeploy that reuses the process: forget the stored bytes. */\nexport function forgetCompressed(): void {\n stored.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"compress.js","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"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","sourcesContent":["// Compression for what the host answers, where nothing in front of it will.\n//\n// Behind Cloudflare, Vercel or nginx the platform compresses and this never\n// runs. A bun or node server answering the internet by itself sent every\n// byte raw - a 90 kB document and 145 kB of stylesheet at their full size -\n// and that alone was the mobile Lighthouse regression a port measured:\n// first paint at 3.8 s where 1.2 s was the baseline. Public assets are\n// precompressed by the build and served by Nitro with their encoding; the\n// pages, payloads, stored answers and api routes come through here.\n//\n// gzip, flushed on every chunk. A streamed page is a shell followed by the\n// holes as they fill, and a compressor left to its own buffer would hold\n// the shell back until enough had accumulated - so each write is flushed\n// out, which costs a little ratio and keeps the stream a stream.\n\nimport { deflate } from \"./compressRuntime.js\";\n\n/** What is worth compressing: text of every kind, and the formats that are text. */\nconst COMPRESSIBLE = /^(?:text\\/|application\\/(?:json|javascript|xml|manifest\\+json|rss\\+xml|atom\\+xml)|image\\/svg\\+xml)/i;\n\n/** Answers too small to be worth a compressor's frame. */\nconst MIN_BYTES = 1024;\n\n/**\n * How long a body that has begun is given to end, or to reach MIN_BYTES,\n * before it is taken to be streaming.\n *\n * A complete answer built from a string ends in the same turn it starts, so\n * this is only ever spent on one that is still being written. Waiting for it\n * to finish was the bug: a client navigation that skips every layout sends a\n * shell under a kilobyte and then the page's slow data, and the shell sat in\n * the peek until that data was in. The loading state it carried reached\n * nobody, and the old page stayed on screen the whole time.\n */\nconst PEEK_MS = 5;\n\n/** Stored answers, compressed once: the same bytes go to every visitor who accepts gzip. */\nconst stored = new Map<string, Uint8Array>();\n\nconst STORED_CAP = 1000;\n\nfunction acceptsGzip(request: Request): boolean {\n const accept = request.headers.get(\"accept-encoding\") ?? \"\";\n\n return /(?:^|,)\\s*(?:gzip|\\*)\\s*(?:;\\s*q=(?!0(?:\\.0*)?\\s*(?:,|$)))?/i.test(accept);\n}\n\n/** Whether this answer is one to compress at all. */\nexport function shouldCompress(request: Request, response: Response): boolean {\n if (response.body === null || request.method === \"HEAD\") return false;\n if (response.status === 204 || response.status === 304 || response.status < 200) return false;\n if (response.headers.has(\"content-encoding\")) return false;\n if (/\\bno-transform\\b/i.test(response.headers.get(\"cache-control\") ?? \"\")) return false;\n const type = response.headers.get(\"content-type\") ?? \"\";\n\n if (!COMPRESSIBLE.test(type)) return false;\n // A stream of events is text, and must not be compressed: each message is\n // a few bytes that has to reach the browser the moment it is written, and\n // a compressor between them delivered nothing at all - not even headers -\n // to a browser that sent Accept-Encoding: gzip, which is every browser.\n if (/^text\\/event-stream\\b/i.test(type)) return false;\n\n const length = Number(response.headers.get(\"content-length\"));\n\n if (Number.isFinite(length) && length > 0 && length < MIN_BYTES) return false;\n\n return acceptsGzip(request);\n}\n\nfunction withEncoding(response: Response, body: BodyInit): Response {\n const headers = new Headers(response.headers);\n\n headers.set(\"Content-Encoding\", \"gzip\");\n headers.delete(\"Content-Length\");\n\n const vary = headers.get(\"Vary\");\n\n if (!vary) headers.set(\"Vary\", \"Accept-Encoding\");\n else if (!/\\baccept-encoding\\b/i.test(vary) && vary.trim() !== \"*\") headers.set(\"Vary\", vary + \", Accept-Encoding\");\n\n return new Response(body, { status: response.status, statusText: response.statusText, headers });\n}\n\n/**\n * The response, gzipped if the request accepts it and the answer is worth it.\n *\n * `storedKey` names an answer the build wrote - the same bytes for every\n * visitor - so it is compressed once and kept; everything else is streamed\n * through the compressor as it is produced.\n */\nexport async function compressed(\n request: Request,\n response: Response,\n storedKey?: string,\n): Promise<Response> {\n if (!shouldCompress(request, response)) return response;\n\n const gzip = await deflate();\n\n if (!gzip) return response;\n\n if (storedKey !== undefined) {\n let bytes = stored.get(storedKey);\n\n if (!bytes) {\n bytes = await gzip.whole(new Uint8Array(await response.arrayBuffer()));\n\n if (stored.size >= STORED_CAP) stored.clear();\n\n stored.set(storedKey, bytes);\n }\n\n return withEncoding(response, bytes as unknown as BodyInit);\n }\n\n // A Response made from a string or JSON carries no Content-Length for the\n // check above to read, so the body is peeked: what arrives before the\n // first kilobyte decides. An answer that ends inside it - /api/health, a\n // 15-byte \"ok\" - goes out as it was; anything longer is compressed from\n // its first byte, the peeked part included, and keeps streaming.\n //\n // The first chunk is waited for however long it takes, since it is the\n // first byte. After it, only PEEK_MS: a body still being written after\n // that is a stream, and a stream is compressed from its first chunk - its\n // shell must not wait for the rest of it to decide a question of size.\n const reader = response.body!.getReader();\n const peeked: Uint8Array[] = [];\n let size = 0;\n let ended = false;\n // A read that lost the race is still a read: the next chunk is its, and\n // asking again would skip one.\n let pending: ReturnType<typeof reader.read> | null = null;\n\n while (size < MIN_BYTES) {\n const read = pending ?? reader.read();\n\n pending = read;\n\n const result = size === 0 ? await read : await within(read, PEEK_MS);\n\n if (result === null) break;\n\n pending = null;\n\n if (result.done) {\n ended = true;\n break;\n }\n\n peeked.push(result.value);\n size += result.value.byteLength;\n }\n\n if (ended) {\n return new Response(size === 0 ? null : (concat(peeked, size) as unknown as BodyInit), {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n const rest = new ReadableStream<Uint8Array>({\n start(controller) {\n for (const chunk of peeked) controller.enqueue(chunk);\n },\n async pull(controller) {\n const read = pending ?? reader.read();\n\n pending = null;\n\n const { done, value } = await read;\n\n if (done) controller.close();\n else controller.enqueue(value);\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n\n return withEncoding(response, gzip.stream(rest));\n}\n\n/** What the promise settles with, or null if it has not within `ms`. */\nasync function within<T>(promise: Promise<T>, ms: number): Promise<T | null> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n try {\n return await Promise.race([\n promise,\n new Promise<null>((resolve) => {\n timer = setTimeout(() => resolve(null), ms);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction concat(chunks: Uint8Array[], size: number): Uint8Array {\n const out = new Uint8Array(size);\n let at = 0;\n\n for (const chunk of chunks) {\n out.set(chunk, at);\n at += chunk.byteLength;\n }\n\n return out;\n}\n\n/** For a test, or a redeploy that reuses the process: forget the stored bytes. */\nexport function forgetCompressed(): void {\n stored.clear();\n}\n"]}
|
package/dist/vite.js
CHANGED
|
@@ -671,6 +671,21 @@ function routeManifest() {
|
|
|
671
671
|
!isIntercept(n) &&
|
|
672
672
|
isUnder(dirOf(name), dirOf(n)))
|
|
673
673
|
.sort((a, b) => a.length - b.length);
|
|
674
|
+
/**
|
|
675
|
+
* The sections a page can refresh by name: those beside it, then those beside
|
|
676
|
+
* each of its layouts, nearest first.
|
|
677
|
+
*
|
|
678
|
+
* A layout stays mounted across the pages under it, which makes it where the
|
|
679
|
+
* part of a screen that must not blink between them belongs - and a section is
|
|
680
|
+
* how that part stays current. A page could not reach one: a section beside a
|
|
681
|
+
* layout was in no page's list, so a refresh from a tab below it was refused.
|
|
682
|
+
*
|
|
683
|
+
* Nearest first because the server takes the first section of a name, so a
|
|
684
|
+
* page's own `stats` shadows a layout's. The two are one region on screen,
|
|
685
|
+
* which is why a name has to be unique along the chain, as it already was
|
|
686
|
+
* within a page.
|
|
687
|
+
*/
|
|
688
|
+
const sectionsOf = (name, layouts) => [...new Set([dirOf(name), ...layouts.map(dirOf).reverse()])].flatMap((dir) => names.filter((n) => SECTION_FILE.test(n + ".tsx") && dirOf(n) === dir));
|
|
674
689
|
/** Project-root-relative, posix — the same string on every machine. */
|
|
675
690
|
const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g, "/");
|
|
676
691
|
/** The host's config file in a directory, if the host named one and it exists. */
|
|
@@ -778,16 +793,17 @@ function routeManifest() {
|
|
|
778
793
|
if (isUnder(dirOf(name), candidate.split("/@")[0]))
|
|
779
794
|
slots[slot] = candidate;
|
|
780
795
|
}
|
|
796
|
+
const layouts = ancestors(name, "layout");
|
|
781
797
|
routes.push({
|
|
782
798
|
component: name,
|
|
783
799
|
segments: urlSegments(name),
|
|
784
|
-
layouts
|
|
800
|
+
layouts,
|
|
785
801
|
loadings: ancestors(name, "loading").map((n) => n),
|
|
786
802
|
errors: ancestors(name, "error"),
|
|
787
803
|
notFounds: ancestors(name, "not-found"),
|
|
788
804
|
middleware: ancestors(name, "middleware").map((n) => n),
|
|
789
805
|
slots,
|
|
790
|
-
sections:
|
|
806
|
+
sections: sectionsOf(name, layouts),
|
|
791
807
|
config: configIn(join(sourceDir, dirOf(name))),
|
|
792
808
|
hostMiddleware: hostMiddleware(dirOf(name)),
|
|
793
809
|
ancestorConfigs: ancestorConfigs(dirOf(name)),
|
|
@@ -4962,7 +4978,9 @@ interface PageContext {
|
|
|
4962
4978
|
loadings: string[]
|
|
4963
4979
|
parallelSlots: Record<string, string>
|
|
4964
4980
|
/**
|
|
4965
|
-
* The sections this route declares, which is what bounds a revalidate
|
|
4981
|
+
* The sections this route declares, which is what bounds a revalidate:
|
|
4982
|
+
* the ones beside the page, then the ones beside each of its layouts,
|
|
4983
|
+
* nearest first, so the first of a name is the one a refresh renders.
|
|
4966
4984
|
*
|
|
4967
4985
|
* Optional only because a host built against an older manifest may not send
|
|
4968
4986
|
* it; absent, no named section can be revalidated at all. Refusing is the
|
|
@@ -5049,9 +5067,22 @@ async function renderRevalidated(target: string, page: PageContext, skipElsewher
|
|
|
5049
5067
|
// The component, not the wrapper section() returned. The client replaces
|
|
5050
5068
|
// what is inside the boundary, so sending the wrapper would nest a new
|
|
5051
5069
|
// boundary inside the old one on every refresh.
|
|
5052
|
-
|
|
5070
|
+
//
|
|
5071
|
+
// With the props its page renders with: the page's params, awaitable and
|
|
5072
|
+
// through its schema, and its searchParams. page.props is what the host
|
|
5073
|
+
// bound from the url - { id: '7' } - and handed over as it was, a section
|
|
5074
|
+
// reading { params } got nothing and threw, on every refresh of every
|
|
5075
|
+
// section that reads them, which is why a refresh from a host crashed
|
|
5076
|
+
// where the same section rendered by its page did not.
|
|
5077
|
+
const schemas = urlSchemas[page.component]
|
|
5078
|
+
const props = {
|
|
5079
|
+
params: checkedParams(schemas, Promise.resolve(page.props)),
|
|
5080
|
+
searchParams: checkedSearchParams(schemas, pageSearchParams()),
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
notePageProps(props)
|
|
5053
5084
|
|
|
5054
|
-
return createElement(Section,
|
|
5085
|
+
return createElement(Section, props)
|
|
5055
5086
|
}
|
|
5056
5087
|
|
|
5057
5088
|
// hasOwn, so a target of __proto__ or constructor names nothing.
|