@rsc-kit/core 0.30.2 → 0.30.3
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/js/errors.d.ts +25 -0
- package/dist/js/errors.js +14 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/vite.js +7 -1
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/js/errors.d.ts
CHANGED
|
@@ -4,6 +4,31 @@ export declare class ServerValidationError extends Error {
|
|
|
4
4
|
readonly errors: Record<string, string[]>;
|
|
5
5
|
constructor(message: string, errors: Record<string, string[]>);
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* What a call to a server action resolves with when the answer was a
|
|
9
|
+
* redirect: not the action's result, and never `T`.
|
|
10
|
+
*
|
|
11
|
+
* Resolved rather than thrown. The navigation is already under way, and a
|
|
12
|
+
* caller that awaits inside `startTransition` with no catch would have its
|
|
13
|
+
* rejection reach React and unmount the root - a white page on every logout.
|
|
14
|
+
* So the type says what the value can be, and `isRedirected` narrows it.
|
|
15
|
+
*/
|
|
16
|
+
export interface Redirected {
|
|
17
|
+
/** Where the visitor is being taken. */
|
|
18
|
+
redirected: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether this is the answer to a redirect, rather than the result you asked
|
|
22
|
+
* for.
|
|
23
|
+
*
|
|
24
|
+
* const secret = await databasesReveal(id);
|
|
25
|
+
* if (isRedirected(secret)) return; // the page is on its way to /login
|
|
26
|
+
* copy(secret.Password);
|
|
27
|
+
*
|
|
28
|
+
* A void action needs it too: `await stub(); toast.success()` runs after the
|
|
29
|
+
* redirect has started, and shows success for a write that never happened.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isRedirected(value: unknown): value is Redirected;
|
|
7
32
|
export declare function noteRedirected(location: string): void;
|
|
8
33
|
/** The redirect the action just performed, if it did - read once. */
|
|
9
34
|
export declare function redirectedTo(): string | null;
|
package/dist/js/errors.js
CHANGED
|
@@ -28,6 +28,20 @@ export class ServerValidationError extends Error {
|
|
|
28
28
|
this.errors = errors;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether this is the answer to a redirect, rather than the result you asked
|
|
33
|
+
* for.
|
|
34
|
+
*
|
|
35
|
+
* const secret = await databasesReveal(id);
|
|
36
|
+
* if (isRedirected(secret)) return; // the page is on its way to /login
|
|
37
|
+
* copy(secret.Password);
|
|
38
|
+
*
|
|
39
|
+
* A void action needs it too: `await stub(); toast.success()` runs after the
|
|
40
|
+
* redirect has started, and shows success for a write that never happened.
|
|
41
|
+
*/
|
|
42
|
+
export function isRedirected(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && typeof value.redirected === "string";
|
|
44
|
+
}
|
|
31
45
|
/** An action answered with a location instead of a result. */
|
|
32
46
|
/**
|
|
33
47
|
* The redirect the last action answered with, for the one caller that asks.
|
package/dist/js/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/js/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which of these an error is, readable by every copy of this module.\n *\n * The app's code and the server's are bundled apart, so an app can hold one\n * copy of these classes while the rpc() client throws from another - and\n * instanceof compares classes, so a server component's\n * `error instanceof ServerAuthenticationError` never matched an rpc() that\n * failed because the visitor was signed out. Each instance is marked under a\n * registry symbol both copies share, and instanceof reads the mark.\n */\nconst KIND = Symbol.for(\"rsc-kit.error-kind\");\n\nfunction mark(error: Error, kind: string): void {\n Object.defineProperty(error, KIND, { value: kind });\n}\n\nfunction isKind(value: unknown, kind: string): boolean {\n return typeof value === \"object\" && value !== null && (value as Record<symbol, unknown>)[KIND] === kind;\n}\n\nexport class ServerValidationError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerValidationError\");\n }\n\n public readonly errors: Record<string, string[]>;\n\n constructor(message: string, errors: Record<string, string[]>) {\n super(message);\n this.name = \"ServerValidationError\";\n mark(this, \"ServerValidationError\");\n this.errors = errors;\n }\n}\n\n/** An action answered with a location instead of a result. */\n/**\n * The redirect the last action answered with, for the one caller that asks.\n *\n * An action that redirects resolves - the navigation is already under way\n * and the caller has nothing to do - which leaves a form that wants to know\n * whether to say \"saved\" with no way to tell a redirect from a void answer.\n * callServer notes the destination here; <Form> clears it before its action\n * runs and reads it, always, right after its await - so a form only ever sees\n * a redirect its own action made. A note nobody reads (an action called\n * outside a Form) is cleared by the next form's submit instead of being taken\n * for that form's redirect. Nothing else needs to read it.\n */\nlet lastRedirect: string | null = null;\n\nexport function noteRedirected(location: string): void {\n lastRedirect = location;\n}\n\n/** The redirect the action just performed, if it did - read once. */\nexport function redirectedTo(): string | null {\n const location = lastRedirect;\n\n lastRedirect = null;\n\n return location;\n}\n\nexport class ServerRedirectError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerRedirectError\");\n }\n\n public readonly location: string;\n\n constructor(location: string) {\n super(`Server action redirected to ${location}`);\n this.name = \"ServerRedirectError\";\n mark(this, \"ServerRedirectError\");\n this.location = location;\n }\n}\n\nexport class ServerAuthenticationError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerAuthenticationError\");\n }\n\n constructor(message: string = \"Unauthenticated.\") {\n super(message);\n this.name = \"ServerAuthenticationError\";\n mark(this, \"ServerAuthenticationError\");\n }\n}\n\nexport class ServerAuthorizationError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerAuthorizationError\");\n }\n\n constructor(message: string = \"This action is unauthorized.\") {\n super(message);\n this.name = \"ServerAuthorizationError\";\n mark(this, \"ServerAuthorizationError\");\n }\n}\n\nexport class ServerDumpError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerDumpError\");\n }\n\n constructor() {\n super(\"Server returned a dump response.\");\n this.name = \"ServerDumpError\";\n mark(this, \"ServerDumpError\");\n }\n}\n\nexport class ServerSessionExpiredError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerSessionExpiredError\");\n }\n\n constructor(message: string = \"Your session has expired. Please refresh the page.\") {\n super(message);\n this.name = \"ServerSessionExpiredError\";\n mark(this, \"ServerSessionExpiredError\");\n }\n}\n\n/**\n * Turn a failed server-action response into the error it describes.\n *\n * A server action that does not succeed answers with JSON or a redirect\n * header rather than a Flight stream. Passing one of those to the Flight\n * decoder does not produce the server's message — it produces an internal\n * parser failure (\"enqueueModel is not a function\") or a truncated read\n * (\"Connection closed.\"), which is what reached onError before this existed.\n *\n * Returns without throwing when the response is a stream to be decoded.\n */\nexport async function throwForFailedAction(response: Response): Promise<void> {\n // Before the status: a redirect the action asked for is a 204 with the\n // destination in this header - an ok answer with no Flight in it - and\n // an expired session's is a 401. Read first, the header decides for both.\n // Read after `ok`, the 204 fell through to the Flight decoder with an\n // empty body, and the form waited forever.\n const location = response.headers.get(\"X-RSC-Redirect\");\n\n if (location !== null && location !== \"\") {\n throw new ServerRedirectError(location);\n }\n\n if (response.ok) return;\n\n if (response.status === 422) {\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; errors?: Record<string, string[]> }\n | null;\n\n throw new ServerValidationError(\n payload?.message ?? \"Validation failed\",\n payload?.errors ?? {},\n );\n }\n\n if (response.status === 413) {\n throw new Error(\n \"Server action refused: the request body is larger than the server accepts. \" +\n \"A file upload is the usual cause; the limit is the host's maxActionBody.\",\n );\n }\n\n throw new Error(`Server action failed with ${response.status}`);\n}\n\n/**\n * Reject a payload response that is not one.\n *\n * The page a PPR route serves is a static shell: real HTML, status 200, with\n * its Suspense fallbacks showing. Everything below them arrives in a second\n * request. If that request fails there is nothing on screen to say so — the\n * skeletons simply stay, for ever — and handing the failure body to the Flight\n * decoder reports the decoder's confusion rather than the status.\n */\nexport function throwForFailedPayload(response: Response): void {\n if (response.ok) return;\n\n // A 404 that is a payload is the not-found page, rendered: a url a\n // pattern's shell answered with a 200 and a page that, run for real,\n // found no row - a subcategory under the wrong category. The page is\n // what the visitor should see; refusing it left the shell's content on\n // screen with nothing hydrated behind it, and every tap dead.\n if (isPayload(response)) return;\n\n throw new Error(`RSC payload request failed with ${response.status}`);\n}\n\n/** Whether a response is a Flight payload, whatever its status. */\nexport function isPayload(response: Response): boolean {\n return (response.headers.get(\"Content-Type\") ?? \"\").includes(\"text/x-component\");\n}\n\n/**\n * Announce a failure that nothing else will.\n *\n * Dispatched as well as logged: an app that wants to replace a stuck skeleton\n * with something honest has no other way to find out.\n */\nexport function reportClientFailure(scope: string, error: unknown): void {\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(new CustomEvent(\"rsc-client-error\", { detail: { scope, error } }));\n }\n\n console.error(`[rsc-kit] ${scope}`, error);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/js/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which of these an error is, readable by every copy of this module.\n *\n * The app's code and the server's are bundled apart, so an app can hold one\n * copy of these classes while the rpc() client throws from another - and\n * instanceof compares classes, so a server component's\n * `error instanceof ServerAuthenticationError` never matched an rpc() that\n * failed because the visitor was signed out. Each instance is marked under a\n * registry symbol both copies share, and instanceof reads the mark.\n */\nconst KIND = Symbol.for(\"rsc-kit.error-kind\");\n\nfunction mark(error: Error, kind: string): void {\n Object.defineProperty(error, KIND, { value: kind });\n}\n\nfunction isKind(value: unknown, kind: string): boolean {\n return typeof value === \"object\" && value !== null && (value as Record<symbol, unknown>)[KIND] === kind;\n}\n\nexport class ServerValidationError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerValidationError\");\n }\n\n public readonly errors: Record<string, string[]>;\n\n constructor(message: string, errors: Record<string, string[]>) {\n super(message);\n this.name = \"ServerValidationError\";\n mark(this, \"ServerValidationError\");\n this.errors = errors;\n }\n}\n\n/**\n * What a call to a server action resolves with when the answer was a\n * redirect: not the action's result, and never `T`.\n *\n * Resolved rather than thrown. The navigation is already under way, and a\n * caller that awaits inside `startTransition` with no catch would have its\n * rejection reach React and unmount the root - a white page on every logout.\n * So the type says what the value can be, and `isRedirected` narrows it.\n */\nexport interface Redirected {\n /** Where the visitor is being taken. */\n redirected: string;\n}\n\n/**\n * Whether this is the answer to a redirect, rather than the result you asked\n * for.\n *\n * const secret = await databasesReveal(id);\n * if (isRedirected(secret)) return; // the page is on its way to /login\n * copy(secret.Password);\n *\n * A void action needs it too: `await stub(); toast.success()` runs after the\n * redirect has started, and shows success for a write that never happened.\n */\nexport function isRedirected(value: unknown): value is Redirected {\n return typeof value === \"object\" && value !== null && typeof (value as Redirected).redirected === \"string\";\n}\n\n/** An action answered with a location instead of a result. */\n/**\n * The redirect the last action answered with, for the one caller that asks.\n *\n * An action that redirects resolves - the navigation is already under way\n * and the caller has nothing to do - which leaves a form that wants to know\n * whether to say \"saved\" with no way to tell a redirect from a void answer.\n * callServer notes the destination here; <Form> clears it before its action\n * runs and reads it, always, right after its await - so a form only ever sees\n * a redirect its own action made. A note nobody reads (an action called\n * outside a Form) is cleared by the next form's submit instead of being taken\n * for that form's redirect. Nothing else needs to read it.\n */\nlet lastRedirect: string | null = null;\n\nexport function noteRedirected(location: string): void {\n lastRedirect = location;\n}\n\n/** The redirect the action just performed, if it did - read once. */\nexport function redirectedTo(): string | null {\n const location = lastRedirect;\n\n lastRedirect = null;\n\n return location;\n}\n\nexport class ServerRedirectError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerRedirectError\");\n }\n\n public readonly location: string;\n\n constructor(location: string) {\n super(`Server action redirected to ${location}`);\n this.name = \"ServerRedirectError\";\n mark(this, \"ServerRedirectError\");\n this.location = location;\n }\n}\n\nexport class ServerAuthenticationError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerAuthenticationError\");\n }\n\n constructor(message: string = \"Unauthenticated.\") {\n super(message);\n this.name = \"ServerAuthenticationError\";\n mark(this, \"ServerAuthenticationError\");\n }\n}\n\nexport class ServerAuthorizationError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerAuthorizationError\");\n }\n\n constructor(message: string = \"This action is unauthorized.\") {\n super(message);\n this.name = \"ServerAuthorizationError\";\n mark(this, \"ServerAuthorizationError\");\n }\n}\n\nexport class ServerDumpError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerDumpError\");\n }\n\n constructor() {\n super(\"Server returned a dump response.\");\n this.name = \"ServerDumpError\";\n mark(this, \"ServerDumpError\");\n }\n}\n\nexport class ServerSessionExpiredError extends Error {\n /** Whichever copy of this module threw it; see KIND. */\n static [Symbol.hasInstance](value: unknown): boolean {\n return isKind(value, \"ServerSessionExpiredError\");\n }\n\n constructor(message: string = \"Your session has expired. Please refresh the page.\") {\n super(message);\n this.name = \"ServerSessionExpiredError\";\n mark(this, \"ServerSessionExpiredError\");\n }\n}\n\n/**\n * Turn a failed server-action response into the error it describes.\n *\n * A server action that does not succeed answers with JSON or a redirect\n * header rather than a Flight stream. Passing one of those to the Flight\n * decoder does not produce the server's message — it produces an internal\n * parser failure (\"enqueueModel is not a function\") or a truncated read\n * (\"Connection closed.\"), which is what reached onError before this existed.\n *\n * Returns without throwing when the response is a stream to be decoded.\n */\nexport async function throwForFailedAction(response: Response): Promise<void> {\n // Before the status: a redirect the action asked for is a 204 with the\n // destination in this header - an ok answer with no Flight in it - and\n // an expired session's is a 401. Read first, the header decides for both.\n // Read after `ok`, the 204 fell through to the Flight decoder with an\n // empty body, and the form waited forever.\n const location = response.headers.get(\"X-RSC-Redirect\");\n\n if (location !== null && location !== \"\") {\n throw new ServerRedirectError(location);\n }\n\n if (response.ok) return;\n\n if (response.status === 422) {\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; errors?: Record<string, string[]> }\n | null;\n\n throw new ServerValidationError(\n payload?.message ?? \"Validation failed\",\n payload?.errors ?? {},\n );\n }\n\n if (response.status === 413) {\n throw new Error(\n \"Server action refused: the request body is larger than the server accepts. \" +\n \"A file upload is the usual cause; the limit is the host's maxActionBody.\",\n );\n }\n\n throw new Error(`Server action failed with ${response.status}`);\n}\n\n/**\n * Reject a payload response that is not one.\n *\n * The page a PPR route serves is a static shell: real HTML, status 200, with\n * its Suspense fallbacks showing. Everything below them arrives in a second\n * request. If that request fails there is nothing on screen to say so — the\n * skeletons simply stay, for ever — and handing the failure body to the Flight\n * decoder reports the decoder's confusion rather than the status.\n */\nexport function throwForFailedPayload(response: Response): void {\n if (response.ok) return;\n\n // A 404 that is a payload is the not-found page, rendered: a url a\n // pattern's shell answered with a 200 and a page that, run for real,\n // found no row - a subcategory under the wrong category. The page is\n // what the visitor should see; refusing it left the shell's content on\n // screen with nothing hydrated behind it, and every tap dead.\n if (isPayload(response)) return;\n\n throw new Error(`RSC payload request failed with ${response.status}`);\n}\n\n/** Whether a response is a Flight payload, whatever its status. */\nexport function isPayload(response: Response): boolean {\n return (response.headers.get(\"Content-Type\") ?? \"\").includes(\"text/x-component\");\n}\n\n/**\n * Announce a failure that nothing else will.\n *\n * Dispatched as well as logged: an app that wants to replace a stuck skeleton\n * with something honest has no other way to find out.\n */\nexport function reportClientFailure(scope: string, error: unknown): void {\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(new CustomEvent(\"rsc-client-error\", { detail: { scope, error } }));\n }\n\n console.error(`[rsc-kit] ${scope}`, error);\n}\n"]}
|
package/dist/vite.js
CHANGED
|
@@ -2207,13 +2207,19 @@ function renderHostActions() {
|
|
|
2207
2207
|
"// @generated — do not edit. Written by the RSC build from the host action map.",
|
|
2208
2208
|
"",
|
|
2209
2209
|
];
|
|
2210
|
+
// Type-only, so the module stays what "use server" allows: functions.
|
|
2211
|
+
lines.push('import type { Redirected } from "@rsc-kit/core/errors";', "");
|
|
2210
2212
|
lines.push(FORM_ARGS);
|
|
2211
2213
|
for (const [name, target] of Object.entries(hostActions)) {
|
|
2212
2214
|
const sig = hostTypes[target];
|
|
2213
2215
|
// Typed where the backend described it: its own parameters, or the form
|
|
2214
2216
|
// that posts to it, and what it returns. Untyped otherwise, as before.
|
|
2215
2217
|
if (sig) {
|
|
2216
|
-
|
|
2218
|
+
// Never just the result: the backend can answer any call with a
|
|
2219
|
+
// redirect (an expired session, a guard), and the call then resolves
|
|
2220
|
+
// with where the visitor is going. A type that said T let callers read
|
|
2221
|
+
// it as data.
|
|
2222
|
+
const result = "Promise<" + (sig.result ? tsOf(sig.result) : "void") + " | Redirected>";
|
|
2217
2223
|
lines.push("export async function " + name + "(" + tsParams(sig) + "): " + result + ";");
|
|
2218
2224
|
lines.push("export async function " + name + "(form: FormData): " + result + ";");
|
|
2219
2225
|
}
|