@rsc-kit/core 0.30.18 → 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/commands.js CHANGED
@@ -36,6 +36,8 @@ export const COMMAND_MODE = Symbol.for('rsc-kit.command');
36
36
  export function defineCommand(command) {
37
37
  return command;
38
38
  }
39
+ /** Resolves after the event loop has taken a turn, which is when it hands out the signals that arrived while it was busy. */
40
+ const nextTurn = () => new Promise((resolve) => (typeof setImmediate === 'function' ? setImmediate(resolve) : setTimeout(resolve, 0)));
39
41
  /**
40
42
  * The entry that loads the app owns the process's SIGTERM and SIGINT, and
41
43
  * leaves this on `globalThis` for the runner.
@@ -174,6 +176,11 @@ export async function runCommand(argv, options) {
174
176
  await options.ready();
175
177
  registered = true;
176
178
  }
179
+ // A stop that arrived while the loop was busy with a synchronous load has
180
+ // not been delivered: a signal is delivered on the loop's next turn. One
181
+ // turn here, so that the command is told before it starts rather than a
182
+ // moment after - the same on Node, on Bun and in a binary.
183
+ await nextTurn();
177
184
  return exitCodeOf(await module.default(args, context), name);
178
185
  }
179
186
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * App commands: the files in `src/commands`, run as `./app <name>`.\n *\n * A compiled app is one binary that starts a server, and a release step or a\n * queue worker is the same code started differently - `./app migrate`,\n * `./app work`. The same entry also runs from a plain build, as\n * `node .output/server/commands.mjs <name>`, so nothing about a command\n * depends on Bun, on a platform, or on the binary.\n *\n * Each command is a file with a default export, `(args, context) => number\n * | void`, where `context` is `{ signal, deadline }`. It shares the build's code and dependencies, so the database\n * package and the logger the pages use are the ones it uses, in the same\n * bundle. It is loaded only when it is named: the server never evaluates one.\n *\n * `register()` from instrumentation.ts is skipped unless the command says\n * `export const register = true`. Importing instrumentation.ts is not\n * skipped - a bad environment fails there, at import, for a command as for a\n * server - but a command that only needs the schema should not pay for, or\n * fail on, a connection it never uses.\n *\n * Nothing here imports `node:` anything: the runner is bundled into the\n * server's entry for every host, and a host without those modules must still\n * load it. `process` is reached lazily, inside the functions that need it.\n */\n\n/**\n * Set on `globalThis` before the server's entry is imported, by whatever runs\n * a command. The entry would otherwise call register() the moment it loads, as\n * a server does at startup, which a command has not asked for.\n */\nexport const COMMAND_MODE = Symbol.for('rsc-kit.command')\n\n/** What a command receives beside its arguments. */\nexport interface CommandContext {\n /** Aborted by SIGTERM or SIGINT: a long-running command stops when it is. */\n signal: AbortSignal\n /**\n * When the runner stops waiting for the command, as a time in milliseconds\n * (`Date.now()`'s), once it has been told to stop; undefined until then.\n * A worker finishing its job reads `deadline - Date.now()` rather than\n * RSC_SHUTDOWN_TIMEOUT and its default.\n *\n * A getter, because what it answers changes: read `context.deadline` when\n * you need it. `({ deadline }) => ...` reads it once, as the command\n * starts, when it is always undefined, and keeps that.\n */\n readonly deadline: number | undefined\n}\n\n/** A command's default export. */\nexport type CommandFunction = (args: string[], context: CommandContext) => number | void | Promise<number | void>\n\n/**\n * Types a command's default export, so its arguments and context are known\n * without annotating them: `export default defineCommand(async (args, context) => {})`.\n * It returns what it is given.\n */\nexport function defineCommand(command: CommandFunction): CommandFunction {\n return command\n}\n\n/**\n * The entry that loads the app owns the process's SIGTERM and SIGINT, and\n * leaves this on `globalThis` for the runner.\n *\n * Loading can take a while, and a signal before the runner exists finds only\n * the default action, which ends the process with 143. So the entry holds\n * them, and the runner is handed over to: the count of those that came first,\n * and every later one. The entry's listener is never taken off. A signal is\n * delivered on the event loop's next turn, not when it arrives, and a listener\n * removed in between has the signal taken from it without anyone getting it.\n */\nexport const EARLY_SIGNALS = Symbol.for('rsc-kit.command.signals')\n\ninterface EarlySignals {\n /** Stops that arrived before there was a runner to give them to. */\n count: number\n /** From now on, the entry gives each signal to this, and stops holding the process to its own timeout. */\n handoff: (listener: (() => void) | undefined) => void\n}\n\n/** What the build knows of one command file: how to load it, and a line to list it by. */\nexport interface CommandEntry {\n load: () => Promise<{ default?: unknown; register?: unknown }>\n description?: string\n}\n\nexport interface RunCommandOptions {\n commands: Record<string, CommandEntry>\n /** instrumentation.ts's register(), once. Awaited only for a command that exports `register = true`. */\n ready: () => Promise<unknown>\n /** instrumentation.ts's shutdown(), called after a command that was given `register()`. */\n shutdown?: () => unknown\n /** How the usage line names this program: `./app`, `node .output/server/commands.mjs`. */\n program?: string\n out?: (text: string) => void\n err?: (text: string) => void\n /** SIGTERM and SIGINT. The process, unless a test brings its own. */\n signals?: {\n on: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n off: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n }\n /** Milliseconds a command gets to stop once told to, and shutdown() after it. RSC_SHUTDOWN_TIMEOUT seconds (10) by default. */\n limit?: number\n}\n\n/** Whether this process was started to run a command rather than to serve. */\nexport function inCommandMode(): boolean {\n return Boolean((globalThis as Record<symbol, unknown>)[COMMAND_MODE])\n}\n\n/** The list `--help` prints, and what an unknown name is answered with. */\nexport function commandHelp(commands: Record<string, { description?: string }>, program = 'app'): string {\n const names = Object.keys(commands).sort()\n const width = Math.max(0, ...names.map((name) => name.length))\n const rows = names.map((name) => {\n const description = commands[name]?.description\n\n return description ? ` ${name.padEnd(width)} ${description}` : ` ${name}`\n })\n\n return (\n `Usage: ${program} <command> [args]\\n\\n` +\n (rows.length ? `Commands:\\n${rows.join('\\n')}\\n` : 'There are no commands: add a file to src/commands.\\n')\n )\n}\n\n/**\n * An exit code from what a command returned. Nothing is success; a number is\n * the code. Anything else is the command's mistake, and said so, because the\n * alternative is a process that exits 0 after returning a string.\n */\nfunction exitCodeOf(result: unknown, name: string): number {\n if (result === undefined) return 0\n\n if (typeof result === 'number' && Number.isInteger(result) && result >= 0 && result <= 255) return result\n\n throw new TypeError(\n `the command \"${name}\" returned ${JSON.stringify(result)}. A command returns an exit code from 0 to 255, or nothing.`,\n )\n}\n\nfunction unref(timer: unknown): void {\n const handle = timer as { unref?: () => void } | undefined\n\n if (handle && typeof handle.unref === 'function') handle.unref()\n}\n\n/** The seconds a command and its shutdown() get after a signal, in milliseconds. */\nfunction defaultLimit(): number {\n const seconds = Number(typeof process !== 'undefined' ? process.env.RSC_SHUTDOWN_TIMEOUT : undefined)\n\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 10) * 1000\n}\n\n/**\n * Run the command `argv` names and say how it ended.\n *\n * Resolves to the exit code and leaves exiting to the caller, so a test sees\n * the code and an entry decides when the process stops. 0 is success and 1 is\n * a command that threw or would not stop; 2 is a name that is not a command,\n * as it is for a tool that was misused.\n */\nexport async function runCommand(argv: readonly string[], options: RunCommandOptions): Promise<number> {\n const out = options.out ?? ((text: string) => void process.stdout.write(text))\n const err = options.err ?? ((text: string) => void process.stderr.write(text))\n const [name, ...args] = argv\n\n if (name === '--help' || name === '-h') {\n out(commandHelp(options.commands, options.program))\n\n return 0\n }\n\n if (name === undefined) {\n err(commandHelp(options.commands, options.program))\n\n return 2\n }\n\n const command = Object.hasOwn(options.commands, name) ? options.commands[name] : undefined\n\n if (!command) {\n err(`Not a command: ${name}\\n\\n${commandHelp(options.commands, options.program)}`)\n\n return 2\n }\n\n const limit = options.limit ?? defaultLimit()\n const signals = options.signals ?? (typeof process !== 'undefined' ? process : undefined)\n const controller = new AbortController()\n\n let registered = false\n let stop: ((code: number) => void) | undefined\n let stopTimer: unknown\n let deadline: number | undefined\n let signalled = 0\n const context: CommandContext = {\n signal: controller.signal,\n get deadline() {\n return deadline\n },\n }\n\n // The first signal asks the command to stop and gives it `limit` to do so,\n // as the server gives in-flight requests theirs. The second is a person who\n // meant it, and nothing waits.\n const onSignal = () => {\n signalled += 1\n\n if (signalled === 1) {\n deadline = Date.now() + limit\n controller.abort()\n\n stopTimer = setTimeout(() => {\n err(`[rsc-kit] the command \"${name}\" did not stop within ${limit / 1000} seconds.\\n`)\n stop?.(1)\n }, limit)\n\n unref(stopTimer)\n\n return\n }\n\n stop?.(1)\n }\n\n const stopped = new Promise<number>((resolve) => {\n stop = resolve\n })\n\n // Behind an entry that loaded the app, the process's signals are the\n // entry's: it hands each one over, and what came while the app was loading\n // is counted here. Told to stop while loading is told to stop.\n const early = (globalThis as Record<symbol, EarlySignals | undefined>)[EARLY_SIGNALS]\n\n if (early) {\n early.handoff(onSignal)\n\n for (let caught = 0; caught < early.count; caught++) onSignal()\n } else {\n signals?.on('SIGTERM', onSignal)\n signals?.on('SIGINT', onSignal)\n }\n\n // A command that is only waiting - for its signal, for a message that has\n // not come - holds nothing open, and Node answers an event loop with nothing\n // in it by exiting 13 with a warning about an unsettled await, where Bun\n // simply waits. A command that is running is running until it returns.\n const alive = setInterval(() => {}, 2 ** 30)\n\n const ran = (async (): Promise<number> => {\n try {\n const module = await command.load()\n\n if (typeof module.default !== 'function') {\n throw new TypeError(`src/commands/${name} has no default export. Export a function: export default async (args) => {}`)\n }\n\n // Opted in, and only then: a migration that needs the schema should not\n // fail because the queue it never touches is not up.\n if (module.register === true) {\n await options.ready()\n registered = true\n }\n\n return exitCodeOf(await (module.default as (...a: unknown[]) => unknown)(args, context), name)\n } catch (error) {\n err(`[rsc-kit] the command \"${name}\" failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n\n return 1\n }\n })()\n\n let code: number\n\n try {\n code = await Promise.race([ran, stopped])\n } finally {\n clearInterval(alive)\n clearTimeout(stopTimer as Parameters<typeof clearTimeout>[0])\n\n if (early) early.handoff(undefined)\n else {\n signals?.off('SIGTERM', onSignal)\n signals?.off('SIGINT', onSignal)\n }\n }\n\n // What register() opened is closed here, bounded like the server's: a\n // close that hangs must not hold a release step open.\n if (registered && options.shutdown) {\n let timer: unknown\n\n await Promise.race([\n Promise.resolve()\n .then(() => options.shutdown?.())\n .catch((error) => err(`[rsc-kit] instrumentation.ts shutdown() failed: ${error instanceof Error ? error.message : String(error)}\\n`)),\n new Promise((resolve) => {\n timer = setTimeout(resolve, limit)\n unref(timer)\n }),\n ])\n\n clearTimeout(timer as Parameters<typeof clearTimeout>[0])\n }\n\n return code\n}\n"]}
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * App commands: the files in `src/commands`, run as `./app <name>`.\n *\n * A compiled app is one binary that starts a server, and a release step or a\n * queue worker is the same code started differently - `./app migrate`,\n * `./app work`. The same entry also runs from a plain build, as\n * `node .output/server/commands.mjs <name>`, so nothing about a command\n * depends on Bun, on a platform, or on the binary.\n *\n * Each command is a file with a default export, `(args, context) => number\n * | void`, where `context` is `{ signal, deadline }`. It shares the build's code and dependencies, so the database\n * package and the logger the pages use are the ones it uses, in the same\n * bundle. It is loaded only when it is named: the server never evaluates one.\n *\n * `register()` from instrumentation.ts is skipped unless the command says\n * `export const register = true`. Importing instrumentation.ts is not\n * skipped - a bad environment fails there, at import, for a command as for a\n * server - but a command that only needs the schema should not pay for, or\n * fail on, a connection it never uses.\n *\n * Nothing here imports `node:` anything: the runner is bundled into the\n * server's entry for every host, and a host without those modules must still\n * load it. `process` is reached lazily, inside the functions that need it.\n */\n\n/**\n * Set on `globalThis` before the server's entry is imported, by whatever runs\n * a command. The entry would otherwise call register() the moment it loads, as\n * a server does at startup, which a command has not asked for.\n */\nexport const COMMAND_MODE = Symbol.for('rsc-kit.command')\n\n/** What a command receives beside its arguments. */\nexport interface CommandContext {\n /** Aborted by SIGTERM or SIGINT: a long-running command stops when it is. */\n signal: AbortSignal\n /**\n * When the runner stops waiting for the command, as a time in milliseconds\n * (`Date.now()`'s), once it has been told to stop; undefined until then.\n * A worker finishing its job reads `deadline - Date.now()` rather than\n * RSC_SHUTDOWN_TIMEOUT and its default.\n *\n * A getter, because what it answers changes: read `context.deadline` when\n * you need it. `({ deadline }) => ...` reads it once, as the command\n * starts, when it is always undefined, and keeps that.\n */\n readonly deadline: number | undefined\n}\n\n/** A command's default export. */\nexport type CommandFunction = (args: string[], context: CommandContext) => number | void | Promise<number | void>\n\n/**\n * Types a command's default export, so its arguments and context are known\n * without annotating them: `export default defineCommand(async (args, context) => {})`.\n * It returns what it is given.\n */\nexport function defineCommand(command: CommandFunction): CommandFunction {\n return command\n}\n\n/** Resolves after the event loop has taken a turn, which is when it hands out the signals that arrived while it was busy. */\nconst nextTurn = () => new Promise<void>((resolve) => (typeof setImmediate === 'function' ? setImmediate(resolve) : setTimeout(resolve, 0)))\n\n/**\n * The entry that loads the app owns the process's SIGTERM and SIGINT, and\n * leaves this on `globalThis` for the runner.\n *\n * Loading can take a while, and a signal before the runner exists finds only\n * the default action, which ends the process with 143. So the entry holds\n * them, and the runner is handed over to: the count of those that came first,\n * and every later one. The entry's listener is never taken off. A signal is\n * delivered on the event loop's next turn, not when it arrives, and a listener\n * removed in between has the signal taken from it without anyone getting it.\n */\nexport const EARLY_SIGNALS = Symbol.for('rsc-kit.command.signals')\n\ninterface EarlySignals {\n /** Stops that arrived before there was a runner to give them to. */\n count: number\n /** From now on, the entry gives each signal to this, and stops holding the process to its own timeout. */\n handoff: (listener: (() => void) | undefined) => void\n}\n\n/** What the build knows of one command file: how to load it, and a line to list it by. */\nexport interface CommandEntry {\n load: () => Promise<{ default?: unknown; register?: unknown }>\n description?: string\n}\n\nexport interface RunCommandOptions {\n commands: Record<string, CommandEntry>\n /** instrumentation.ts's register(), once. Awaited only for a command that exports `register = true`. */\n ready: () => Promise<unknown>\n /** instrumentation.ts's shutdown(), called after a command that was given `register()`. */\n shutdown?: () => unknown\n /** How the usage line names this program: `./app`, `node .output/server/commands.mjs`. */\n program?: string\n out?: (text: string) => void\n err?: (text: string) => void\n /** SIGTERM and SIGINT. The process, unless a test brings its own. */\n signals?: {\n on: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n off: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n }\n /** Milliseconds a command gets to stop once told to, and shutdown() after it. RSC_SHUTDOWN_TIMEOUT seconds (10) by default. */\n limit?: number\n}\n\n/** Whether this process was started to run a command rather than to serve. */\nexport function inCommandMode(): boolean {\n return Boolean((globalThis as Record<symbol, unknown>)[COMMAND_MODE])\n}\n\n/** The list `--help` prints, and what an unknown name is answered with. */\nexport function commandHelp(commands: Record<string, { description?: string }>, program = 'app'): string {\n const names = Object.keys(commands).sort()\n const width = Math.max(0, ...names.map((name) => name.length))\n const rows = names.map((name) => {\n const description = commands[name]?.description\n\n return description ? ` ${name.padEnd(width)} ${description}` : ` ${name}`\n })\n\n return (\n `Usage: ${program} <command> [args]\\n\\n` +\n (rows.length ? `Commands:\\n${rows.join('\\n')}\\n` : 'There are no commands: add a file to src/commands.\\n')\n )\n}\n\n/**\n * An exit code from what a command returned. Nothing is success; a number is\n * the code. Anything else is the command's mistake, and said so, because the\n * alternative is a process that exits 0 after returning a string.\n */\nfunction exitCodeOf(result: unknown, name: string): number {\n if (result === undefined) return 0\n\n if (typeof result === 'number' && Number.isInteger(result) && result >= 0 && result <= 255) return result\n\n throw new TypeError(\n `the command \"${name}\" returned ${JSON.stringify(result)}. A command returns an exit code from 0 to 255, or nothing.`,\n )\n}\n\nfunction unref(timer: unknown): void {\n const handle = timer as { unref?: () => void } | undefined\n\n if (handle && typeof handle.unref === 'function') handle.unref()\n}\n\n/** The seconds a command and its shutdown() get after a signal, in milliseconds. */\nfunction defaultLimit(): number {\n const seconds = Number(typeof process !== 'undefined' ? process.env.RSC_SHUTDOWN_TIMEOUT : undefined)\n\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 10) * 1000\n}\n\n/**\n * Run the command `argv` names and say how it ended.\n *\n * Resolves to the exit code and leaves exiting to the caller, so a test sees\n * the code and an entry decides when the process stops. 0 is success and 1 is\n * a command that threw or would not stop; 2 is a name that is not a command,\n * as it is for a tool that was misused.\n */\nexport async function runCommand(argv: readonly string[], options: RunCommandOptions): Promise<number> {\n const out = options.out ?? ((text: string) => void process.stdout.write(text))\n const err = options.err ?? ((text: string) => void process.stderr.write(text))\n const [name, ...args] = argv\n\n if (name === '--help' || name === '-h') {\n out(commandHelp(options.commands, options.program))\n\n return 0\n }\n\n if (name === undefined) {\n err(commandHelp(options.commands, options.program))\n\n return 2\n }\n\n const command = Object.hasOwn(options.commands, name) ? options.commands[name] : undefined\n\n if (!command) {\n err(`Not a command: ${name}\\n\\n${commandHelp(options.commands, options.program)}`)\n\n return 2\n }\n\n const limit = options.limit ?? defaultLimit()\n const signals = options.signals ?? (typeof process !== 'undefined' ? process : undefined)\n const controller = new AbortController()\n\n let registered = false\n let stop: ((code: number) => void) | undefined\n let stopTimer: unknown\n let deadline: number | undefined\n let signalled = 0\n const context: CommandContext = {\n signal: controller.signal,\n get deadline() {\n return deadline\n },\n }\n\n // The first signal asks the command to stop and gives it `limit` to do so,\n // as the server gives in-flight requests theirs. The second is a person who\n // meant it, and nothing waits.\n const onSignal = () => {\n signalled += 1\n\n if (signalled === 1) {\n deadline = Date.now() + limit\n controller.abort()\n\n stopTimer = setTimeout(() => {\n err(`[rsc-kit] the command \"${name}\" did not stop within ${limit / 1000} seconds.\\n`)\n stop?.(1)\n }, limit)\n\n unref(stopTimer)\n\n return\n }\n\n stop?.(1)\n }\n\n const stopped = new Promise<number>((resolve) => {\n stop = resolve\n })\n\n // Behind an entry that loaded the app, the process's signals are the\n // entry's: it hands each one over, and what came while the app was loading\n // is counted here. Told to stop while loading is told to stop.\n const early = (globalThis as Record<symbol, EarlySignals | undefined>)[EARLY_SIGNALS]\n\n if (early) {\n early.handoff(onSignal)\n\n for (let caught = 0; caught < early.count; caught++) onSignal()\n } else {\n signals?.on('SIGTERM', onSignal)\n signals?.on('SIGINT', onSignal)\n }\n\n // A command that is only waiting - for its signal, for a message that has\n // not come - holds nothing open, and Node answers an event loop with nothing\n // in it by exiting 13 with a warning about an unsettled await, where Bun\n // simply waits. A command that is running is running until it returns.\n const alive = setInterval(() => {}, 2 ** 30)\n\n const ran = (async (): Promise<number> => {\n try {\n const module = await command.load()\n\n if (typeof module.default !== 'function') {\n throw new TypeError(`src/commands/${name} has no default export. Export a function: export default async (args) => {}`)\n }\n\n // Opted in, and only then: a migration that needs the schema should not\n // fail because the queue it never touches is not up.\n if (module.register === true) {\n await options.ready()\n registered = true\n }\n\n // A stop that arrived while the loop was busy with a synchronous load has\n // not been delivered: a signal is delivered on the loop's next turn. One\n // turn here, so that the command is told before it starts rather than a\n // moment after - the same on Node, on Bun and in a binary.\n await nextTurn()\n\n return exitCodeOf(await (module.default as (...a: unknown[]) => unknown)(args, context), name)\n } catch (error) {\n err(`[rsc-kit] the command \"${name}\" failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n\n return 1\n }\n })()\n\n let code: number\n\n try {\n code = await Promise.race([ran, stopped])\n } finally {\n clearInterval(alive)\n clearTimeout(stopTimer as Parameters<typeof clearTimeout>[0])\n\n if (early) early.handoff(undefined)\n else {\n signals?.off('SIGTERM', onSignal)\n signals?.off('SIGINT', onSignal)\n }\n }\n\n // What register() opened is closed here, bounded like the server's: a\n // close that hangs must not hold a release step open.\n if (registered && options.shutdown) {\n let timer: unknown\n\n await Promise.race([\n Promise.resolve()\n .then(() => options.shutdown?.())\n .catch((error) => err(`[rsc-kit] instrumentation.ts shutdown() failed: ${error instanceof Error ? error.message : String(error)}\\n`)),\n new Promise((resolve) => {\n timer = setTimeout(resolve, limit)\n unref(timer)\n }),\n ])\n\n clearTimeout(timer as Parameters<typeof clearTimeout>[0])\n }\n\n return code\n}\n"]}
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 { done, value } = await reader.read();
96
- if (done) {
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 { done, value } = await reader.read();
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;
@@ -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: ancestors(name, "layout").map((n) => n),
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: names.filter((n) => SECTION_FILE.test(n + ".tsx") && dirOf(n) === dirOf(name)),
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
- notePageProps(page.props)
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, page.props)
5085
+ return createElement(Section, props)
5055
5086
  }
5056
5087
 
5057
5088
  // hasOwn, so a target of __proto__ or constructor names nothing.