@rsc-kit/core 0.29.9 → 0.30.0

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/action.d.ts CHANGED
@@ -1,13 +1,27 @@
1
1
  import { type StandardSchemaV1 } from './js/standardSchema.js';
2
2
  import { type QueryOptions } from './query.js';
3
- /** What an action answers with. Exactly one of the four is set. */
4
- export interface ActionResult<Data> {
3
+ /**
4
+ * What an action answers with. One of `data`, `validationErrors`,
5
+ * `serverError` and `redirected` is set; `refusal` only ever beside
6
+ * `serverError`.
7
+ */
8
+ export interface ActionResult<Data, Refusal = never> {
5
9
  /** What the handler returned. */
6
10
  data?: Data;
7
11
  /** Field name to messages, in the shape a form already renders. */
8
12
  validationErrors?: Record<string, string[]>;
9
- /** Something else went wrong, reduced to a message the browser may see. */
13
+ /**
14
+ * The action did not happen, as a message the browser may see: a refusal's
15
+ * own message - see `refuse()` - or something unexpected, reduced by
16
+ * `onError` to one that says nothing a stranger should not see.
17
+ */
10
18
  serverError?: string;
19
+ /**
20
+ * What a refusal said beside its message - the things blocking a delete,
21
+ * say - checked against the schema the action declared with `.refusal()`
22
+ * and typed from it. Set only by `refuse()`, here or on the backend.
23
+ */
24
+ refusal?: Refusal;
11
25
  /**
12
26
  * Where the action sent the visitor. Set by the client, which has already
13
27
  * started the navigation: the page is on its way there, and there is
@@ -41,6 +55,38 @@ export declare function isActionValidationError(error: unknown): error is Action
41
55
  * particular, which is where the form already looks for one.
42
56
  */
43
57
  export type FieldErrorsFor<Input> = Partial<Record<(Input extends object ? keyof Input & string : string) | '', string | string[]>>;
58
+ /**
59
+ * An action declining to do what it was asked, on purpose, with a message for
60
+ * the person who asked and - optionally - data the page can act on: the
61
+ * records blocking a delete, the plan a quota belongs to.
62
+ *
63
+ * Not a validation error: the input was fine, the state of things says no.
64
+ * And not a failure: its message is meant to be seen, so it is never
65
+ * replaced by `onError`'s "Something went wrong.". It arrives as the result's
66
+ * `serverError`, which is where a form's `formError` already reads, and its
67
+ * data as `refusal`.
68
+ */
69
+ export declare class ActionRefusal extends Error {
70
+ readonly data: unknown;
71
+ /** The status a request that is not an action - a page, a form posted without javascript - answers with. */
72
+ readonly refusalStatus: number;
73
+ constructor(message: string, data?: unknown, status?: number);
74
+ }
75
+ /** Whether this is a refusal, whichever copy of the class built it. */
76
+ export declare function isActionRefusal(error: unknown): error is ActionRefusal;
77
+ /**
78
+ * Decline, with a message to show and optional data to act on. Throws.
79
+ *
80
+ * if (attached.length) refuse('Still in use', { blockers: attached })
81
+ *
82
+ * From a handler, a middleware or anything they call. The data reaches the
83
+ * page only when the action declares its shape with `.refusal(schema)`, and
84
+ * only once it matches - see ActionResult.refusal. Inside a handler, the
85
+ * `refuse` it is given is typed to that schema; this one takes anything.
86
+ */
87
+ export declare function refuse(message: string, data?: unknown, options?: {
88
+ status?: number;
89
+ }): never;
44
90
  /**
45
91
  * What a handler is given.
46
92
  *
@@ -50,9 +96,17 @@ export type FieldErrorsFor<Input> = Partial<Record<(Input extends object ? keyof
50
96
  * The bare export takes any string, for the rare check that runs outside a
51
97
  * handler.
52
98
  */
53
- export interface HandlerArgs<Input, Ctx> {
99
+ export interface HandlerArgs<Input, Ctx, Refusal = never> {
54
100
  input: Input;
55
101
  ctx: Ctx;
102
+ /**
103
+ * Decline, with a message and the data the action declared with
104
+ * `.refusal(schema)`. Throws; write `return refuse(...)`, as with
105
+ * fieldErrors.
106
+ */
107
+ refuse: [Refusal] extends [never] ? (message: string) => never : (message: string, data: Refusal, options?: {
108
+ status?: number;
109
+ }) => never;
56
110
  /**
57
111
  * Fail with errors on this input's fields. Throws; nothing after it runs.
58
112
  *
@@ -110,17 +164,23 @@ type InputOf<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown;
110
164
  * shape fails the typecheck rather than the validation - and FormData beside
111
165
  * it, which is how a form calls the same action. Without a schema, anything.
112
166
  */
113
- export type Action<Raw, Data> = unknown extends Raw ? (input?: unknown) => Promise<ActionResult<Data>> : undefined extends Raw ? (input?: Raw | FormData) => Promise<ActionResult<Data>> : (input: Raw | FormData) => Promise<ActionResult<Data>>;
114
- export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown> {
167
+ export type Action<Raw, Data, Refusal = never> = unknown extends Raw ? (input?: unknown) => Promise<ActionResult<Data, Refusal>> : undefined extends Raw ? (input?: Raw | FormData) => Promise<ActionResult<Data, Refusal>> : (input: Raw | FormData) => Promise<ActionResult<Data, Refusal>>;
168
+ export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown, Refusal = never> {
115
169
  /** Add a step, and whatever context it contributes. */
116
170
  use<Extra extends Record<string, unknown> = Record<string, never>>(middleware: (args: {
117
171
  ctx: Ctx;
118
172
  next: <E extends Record<string, unknown> = Record<string, never>>(opts?: {
119
173
  ctx?: E;
120
174
  }) => Promise<MiddlewareResult<E>>;
121
- }) => Promise<MiddlewareResult<Extra>>): ActionBuilder<Ctx & Extra, Input, Raw>;
175
+ }) => Promise<MiddlewareResult<Extra>>): ActionBuilder<Ctx & Extra, Input, Raw, Refusal>;
122
176
  /** Parse and check what the caller sent. The handler's `input` follows. */
123
- input<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Output<S>, InputOf<S>>;
177
+ input<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Output<S>, InputOf<S>, Refusal>;
178
+ /**
179
+ * The shape of what a refusal may carry - see `refuse()`. The page gets
180
+ * `result.refusal` typed from it, and only data that matches it: data that
181
+ * does not is a bug in the server, answered as one.
182
+ */
183
+ refusal<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Input, Raw, Output<S>>;
124
184
  /**
125
185
  * The body.
126
186
  *
@@ -130,7 +190,7 @@ export interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw =
130
190
  * reading `result.validationErrors` after a redirect reads undefined and
131
191
  * not a TypeError inside a transition, which unmounts the root.
132
192
  */
133
- handler<Data>(fn: (args: HandlerArgs<Input, Ctx>) => Promise<Data> | Data): Action<Raw, Data>;
193
+ handler<Data>(fn: (args: HandlerArgs<Input, Ctx, Refusal>) => Promise<Data> | Data): Action<Raw, Data, Refusal>;
134
194
  /**
135
195
  * The body of a READ, sharing this client's middleware and schema.
136
196
  *
package/dist/action.js CHANGED
@@ -82,6 +82,47 @@ export function isActionValidationError(error) {
82
82
  error !== null &&
83
83
  error[VALIDATION_MARK] === true);
84
84
  }
85
+ const REFUSAL_MARK = Symbol.for('@rsc-kit/core.action-refusal');
86
+ /**
87
+ * An action declining to do what it was asked, on purpose, with a message for
88
+ * the person who asked and - optionally - data the page can act on: the
89
+ * records blocking a delete, the plan a quota belongs to.
90
+ *
91
+ * Not a validation error: the input was fine, the state of things says no.
92
+ * And not a failure: its message is meant to be seen, so it is never
93
+ * replaced by `onError`'s "Something went wrong.". It arrives as the result's
94
+ * `serverError`, which is where a form's `formError` already reads, and its
95
+ * data as `refusal`.
96
+ */
97
+ export class ActionRefusal extends Error {
98
+ data;
99
+ /** The status a request that is not an action - a page, a form posted without javascript - answers with. */
100
+ refusalStatus;
101
+ constructor(message, data, status = 409) {
102
+ super(message);
103
+ this.name = 'ActionRefusal';
104
+ this.data = data;
105
+ this.refusalStatus = status;
106
+ this[REFUSAL_MARK] = true;
107
+ }
108
+ }
109
+ /** Whether this is a refusal, whichever copy of the class built it. */
110
+ export function isActionRefusal(error) {
111
+ return typeof error === 'object' && error !== null && error[REFUSAL_MARK] === true;
112
+ }
113
+ /**
114
+ * Decline, with a message to show and optional data to act on. Throws.
115
+ *
116
+ * if (attached.length) refuse('Still in use', { blockers: attached })
117
+ *
118
+ * From a handler, a middleware or anything they call. The data reaches the
119
+ * page only when the action declares its shape with `.refusal(schema)`, and
120
+ * only once it matches - see ActionResult.refusal. Inside a handler, the
121
+ * `refuse` it is given is typed to that schema; this one takes anything.
122
+ */
123
+ export function refuse(message, data, options = {}) {
124
+ throw new ActionRefusal(message, data, options.status);
125
+ }
85
126
  /**
86
127
  * Fail with field errors the form can show.
87
128
  *
@@ -112,7 +153,7 @@ const GENERIC = 'Something went wrong.';
112
153
  const fromFormData = (body, schema) => decodeFormData(body, schema);
113
154
  export function createActionClient(options = {}) {
114
155
  const report = options.onError ?? (() => GENERIC);
115
- function build(middlewares, schema) {
156
+ function build(middlewares, schema, refusalSchema = null) {
116
157
  /**
117
158
  * Validate, run the chain, call the body.
118
159
  *
@@ -145,6 +186,7 @@ export function createActionClient(options = {}) {
145
186
  // The same function as the export; the type on the way in is what
146
187
  // is different, and the type is the handler's input.
147
188
  fieldErrors: fieldErrors,
189
+ refuse: refuse,
148
190
  });
149
191
  }
150
192
  let continued = false;
@@ -166,12 +208,37 @@ export function createActionClient(options = {}) {
166
208
  };
167
209
  return await run();
168
210
  };
211
+ /**
212
+ * A refusal as the page gets it: its message, always - it was written to
213
+ * be seen - and its data only once the declared schema has checked it.
214
+ * Data with no schema to check it, or that fails the schema, never
215
+ * reaches the browser: the first is said in the server's log, the second
216
+ * is a bug in the server and answered as one.
217
+ */
218
+ const refusalResult = async (error) => {
219
+ if (error.data === undefined)
220
+ return { serverError: error.message };
221
+ if (!refusalSchema) {
222
+ console.error(`[rsc-kit] An action refused with data, and declares no .refusal(schema) for it - the message was sent, the data was not: ${error.message}`);
223
+ return { serverError: error.message };
224
+ }
225
+ const checked = await refusalSchema['~standard'].validate(error.data);
226
+ if (checked.issues) {
227
+ return {
228
+ serverError: report(new Error(`An action refused with data its .refusal(schema) does not accept: ${checked.issues.map((issue) => issue.message).join('; ')}`)),
229
+ };
230
+ }
231
+ return { serverError: error.message, refusal: checked.value };
232
+ };
169
233
  return {
170
234
  use(middleware) {
171
- return build([...middlewares, middleware], schema);
235
+ return build([...middlewares, middleware], schema, refusalSchema);
172
236
  },
173
237
  input(next) {
174
- return build(middlewares, next);
238
+ return build(middlewares, next, refusalSchema);
239
+ },
240
+ refusal(next) {
241
+ return build(middlewares, schema, next);
175
242
  },
176
243
  handler(fn) {
177
244
  return markClientBuilt(async (raw) => {
@@ -191,6 +258,8 @@ export function createActionClient(options = {}) {
191
258
  if (isActionValidationError(error)) {
192
259
  return { validationErrors: error.errors };
193
260
  }
261
+ if (isActionRefusal(error))
262
+ return (await refusalResult(error));
194
263
  return { serverError: report(error) };
195
264
  }
196
265
  });
@@ -211,6 +280,12 @@ export function createActionClient(options = {}) {
211
280
  if (isActionValidationError(error)) {
212
281
  throw new QueryValidationError(error.errors);
213
282
  }
283
+ // Its own message, as an action's is, and its checked data on
284
+ // the error for a fetcher that wants it.
285
+ if (isActionRefusal(error)) {
286
+ const refused = await refusalResult(error);
287
+ throw Object.assign(new Error(refused.serverError), { refusal: refused.refusal });
288
+ }
214
289
  throw new Error(report(error));
215
290
  }
216
291
  };
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"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","sourcesContent":["// Server actions with a schema, middleware, and types that follow from both.\n//\n// export const action = createActionClient({ onError: report })\n//\n// export const createPost = action\n// .use(async ({ next }) => next({ ctx: { user: await currentUser() } }))\n// .input(z.object({ title: z.string().min(3) }))\n// .handler(async ({ input, ctx }) => savePost(ctx.user.id, input.title))\n//\n// `input` is typed from the schema and `ctx` from every middleware that ran, so\n// the handler is checked against both without either being written down twice.\n//\n// The built action RETURNS its failures rather than throwing them, and that is\n// not a style choice. React serialises a rejected server action opaquely —\n// production strips the message and leaves a digest — so a thrown validation\n// error reaches the browser as \"an error occurred\" and the fields it named are\n// gone. A returned object crosses the boundary intact.\n//\n// Distinct from `middleware.ts` in a route directory, which decides whether a\n// page may render. This wraps one action. They are different questions: an\n// action is reachable without any page, which is why it defends itself.\n\nimport { validateWith, type StandardSchemaV1 } from './js/standardSchema.js'\nimport { decodeFormData } from './js/formEncoding.js'\nimport { markQuery, QueryValidationError, type QueryOptions } from './query.js'\nimport { isRedirectSignal } from './redirectDigest.js'\n\n/** What an action answers with. Exactly one of the four is set. */\nexport interface ActionResult<Data> {\n /** What the handler returned. */\n data?: Data\n /** Field name to messages, in the shape a form already renders. */\n validationErrors?: Record<string, string[]>\n /** Something else went wrong, reduced to a message the browser may see. */\n serverError?: string\n /**\n * Where the action sent the visitor. Set by the client, which has already\n * started the navigation: the page is on its way there, and there is\n * nothing for the caller to do. Never set on the server side of the call.\n */\n redirected?: string\n}\n\n/**\n * A middleware that neither continued nor refused.\n *\n * Never reported through `onError`: that reduces an error to a message for the\n * browser, and this one is for whoever wrote the middleware. A check that\n * forgot to call `next()` would otherwise look exactly like a check that\n * passed.\n */\nexport class ActionMisuse extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ActionMisuse'\n }\n}\n\n/** Refuse from inside a handler, naming the fields. */\n/**\n * Marks a refusal so it survives a bundle seam.\n *\n * A property rather than `instanceof`, for the reason this project keeps\n * running into: an app's actions are bundled separately from the engine, so\n * each gets its own copy of this module and its own copy of the class.\n * `instanceof` compares identity across that seam and is simply false — and\n * the refusal is then reported as a server error, so the form shows \"Something\n * went wrong\" instead of naming the fields. Everything works; nothing logs.\n *\n * Symbol.for, so the two copies agree on the key as well as the value.\n */\nconst VALIDATION_MARK = Symbol.for('@rsc-kit/core.action-validation')\n\n/**\n * Set on every function a client builds, so the build can tell them from a\n * bare \"use server\" export. The difference is the middleware: a bare export\n * runs with nothing checking who called it, and nothing else in the app\n * would ever say so. Symbol.for, because the build reads the mark from the\n * bundled copy of this module and the app's actions were built by another.\n */\nconst CLIENT_MARK = Symbol.for('@rsc-kit/core.action-client')\n\n/** Whether a server action was built by createActionClient, and so ran its chain. */\nexport function isClientBuilt(fn: unknown): boolean {\n return typeof fn === 'function' && (fn as unknown as Record<symbol, unknown>)[CLIENT_MARK] === true\n}\n\nfunction markClientBuilt<F extends Function>(fn: F): F {\n Object.defineProperty(fn, CLIENT_MARK, { value: true })\n\n return fn\n}\n\nexport class ActionValidationError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed')\n this.name = 'ActionValidationError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[VALIDATION_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionValidationError(error: unknown): error is ActionValidationError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[VALIDATION_MARK] === true\n )\n}\n\n/**\n * The field errors a handler may report, keyed by its own input's fields.\n *\n * `''` is the whole submission — for a refusal that is about no field in\n * particular, which is where the form already looks for one.\n */\nexport type FieldErrorsFor<Input> = Partial<\n Record<(Input extends object ? keyof Input & string : string) | '', string | string[]>\n>\n\n/**\n * What a handler is given.\n *\n * `fieldErrors` is here as well as exported on its own, and the one here is\n * the one to use: it is typed to this handler's input, so a field the schema\n * does not have is a type error rather than an error the form never shows.\n * The bare export takes any string, for the rare check that runs outside a\n * handler.\n */\nexport interface HandlerArgs<Input, Ctx> {\n input: Input\n ctx: Ctx\n /**\n * Fail with errors on this input's fields. Throws; nothing after it runs.\n *\n * Write `return fieldErrors(...)`. It throws either way, but TypeScript does\n * not treat a never-return as terminating when the callee is a destructured\n * binding — only a declaration or an explicitly annotated variable — so\n * without the return, a value checked on the line above is still possibly\n * undefined on the line below. The return is what lets the type narrow.\n */\n fieldErrors: (errors: FieldErrorsFor<Input>) => never\n}\n\n/**\n * Fail with field errors the form can show.\n *\n * For what a schema cannot know — a name already taken, a balance too low.\n * Throws, so the handler stops where it is; the action turns it into a\n * returned result on the way out.\n *\n * Untyped by field, because it has no handler to take the input from. Inside\n * one, use the `fieldErrors` the handler is given instead.\n */\nexport function fieldErrors(errors: Record<string, string[] | string>): never {\n const normalised: Record<string, string[]> = {}\n\n for (const [field, message] of Object.entries(errors)) {\n normalised[field] = Array.isArray(message) ? message : [message]\n }\n\n throw new ActionValidationError(normalised)\n}\n\n/**\n * What `next()` hands back, carrying what the step added.\n *\n * The context a middleware contributes cannot be inferred from the arguments\n * it passes to `next` — TypeScript infers from a function's return, not from a\n * call inside it. So `next` returns this, the middleware returns it, and the\n * addition is read off the middleware's own return type.\n */\nexport interface MiddlewareResult<Extra> {\n readonly ctx: Extra\n readonly value: unknown\n}\n\n/**\n * A step that runs before the handler.\n *\n * It calls `next` to continue, optionally adding to the context, and what it\n * adds shows up in the handler's types. Returning without calling `next` — or\n * throwing — stops the action, which is how a check refuses.\n */\nexport type ActionMiddleware<Ctx, Extra extends Record<string, unknown>> = (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n}) => Promise<MiddlewareResult<Extra>>\n\ntype Output<S> = S extends StandardSchemaV1<unknown, infer O> ? O : never\n\n/** What a caller may pass, before the schema parses it. */\ntype InputOf<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown\n\n/**\n * A built action, as its callers see it.\n *\n * Typed from the schema's input when there is one, so a call with the wrong\n * shape fails the typecheck rather than the validation - and FormData beside\n * it, which is how a form calls the same action. Without a schema, anything.\n */\nexport type Action<Raw, Data> = unknown extends Raw\n ? (input?: unknown) => Promise<ActionResult<Data>>\n : undefined extends Raw\n ? (input?: Raw | FormData) => Promise<ActionResult<Data>>\n : (input: Raw | FormData) => Promise<ActionResult<Data>>\n\nexport interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown> {\n /** Add a step, and whatever context it contributes. */\n use<Extra extends Record<string, unknown> = Record<string, never>>(\n middleware: (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n }) => Promise<MiddlewareResult<Extra>>,\n ): ActionBuilder<Ctx & Extra, Input, Raw>\n /** Parse and check what the caller sent. The handler's `input` follows. */\n input<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Output<S>, InputOf<S>>\n /**\n * The body.\n *\n * What the caller gets is always the result object: `data`, or a\n * refusal, or - when the action redirected - `redirected`, set by the\n * client once the navigation is under way. Always an object, so a caller\n * reading `result.validationErrors` after a redirect reads undefined and\n * not a TypeError inside a transition, which unmounts the root.\n */\n handler<Data>(\n fn: (args: HandlerArgs<Input, Ctx>) => Promise<Data> | Data,\n ): Action<Raw, Data>\n /**\n * The body of a READ, sharing this client's middleware and schema.\n *\n * export const getPosts = client.input(filter).query(async ({ input, ctx }) =>\n * db.posts(ctx.user.id, input))\n *\n * The same builder as `handler`, and deliberately so: an app configures its\n * auth check and its error reporting once, and both a mutation and a read go\n * through them.\n *\n * It fails differently, though, and that is not an oversight. An action\n * RETURNS its failures because React serialises a rejection opaquely. A query\n * is handed to a cache library as a fetcher, and every one of them reports\n * failure by rejection — so this returns the data directly and throws, and\n * the endpoint carries the message across for it.\n */\n query<Data>(\n fn: (args: { input: Input; ctx: Ctx }) => Promise<Data> | Data,\n options?: QueryOptions,\n ): (input?: unknown) => Promise<Data>\n}\n\nexport interface ActionClientOptions {\n /**\n * What the browser is told when something unexpected throws.\n *\n * Everything reaching here is a bug or an outage, and its message may say\n * more than a stranger should see — a query, a path, a host. Returning a\n * fixed string is the safe default; return the message only for errors you\n * raised deliberately.\n */\n onError?: (error: unknown) => string\n}\n\nconst GENERIC = 'Something went wrong.'\n\n/**\n * FormData in, a plain object out — what a schema expects to be handed.\n *\n * The form's own decoder, so the object validated here is the object the\n * browser validated. This had a flat decoder of its own: `items[0].name`\n * stayed a key spelled exactly that, `<Form>` had already parsed it into an\n * array, and a nested form passed in the browser and failed on the server\n * with the fields it named gone.\n */\nconst fromFormData = (body: FormData, schema: unknown): Record<string, unknown> => decodeFormData(body, schema)\n\nexport function createActionClient(\n options: ActionClientOptions = {},\n): ActionBuilder<Record<never, never>, undefined> {\n const report = options.onError ?? (() => GENERIC)\n\n function build<Ctx extends Record<string, unknown>, Input>(\n middlewares: ActionMiddleware<never, never>[],\n schema: StandardSchemaV1 | null,\n ): ActionBuilder<Ctx, Input> {\n /**\n * Validate, run the chain, call the body.\n *\n * Shared by both terminals, which is the point of putting a read on this\n * builder at all: one set of middleware, one schema, one place the auth\n * check lives. Refusals leave by throwing, and each terminal decides what\n * that should look like from the outside.\n */\n const pipeline = async (\n raw: unknown,\n fn: (args: HandlerArgs<never, never>) => unknown,\n ): Promise<unknown> => {\n const value = raw instanceof FormData ? fromFormData(raw, schema) : raw\n\n if (schema) {\n const invalid = await validateWith(schema, value)\n\n if (invalid) throw new ActionValidationError(invalid)\n }\n\n const parsed = schema\n ? ((await schema['~standard'].validate(value)).value as Input)\n : (value as Input)\n\n // Composed inside-out so the first `use` is the outermost — it sees the\n // others run, which is what makes timing and cleanup possible rather\n // than only checks.\n let ctx = {} as Ctx\n let index = 0\n\n const run = async (): Promise<unknown> => {\n const middleware = middlewares[index++]\n\n if (!middleware) {\n return await fn({\n input: parsed as never,\n ctx: ctx as never,\n // The same function as the export; the type on the way in is what\n // is different, and the type is the handler's input.\n fieldErrors: fieldErrors as never,\n })\n }\n\n let continued = false\n\n const result = await (middleware as unknown as ActionMiddleware<Ctx, Record<string, unknown>>)({\n ctx,\n next: (async (opts?: { ctx?: Record<string, unknown> }) => {\n continued = true\n ctx = { ...ctx, ...(opts?.ctx ?? {}) } as Ctx\n\n return { ctx, value: await run() }\n }) as never,\n })\n\n // Silence is not refusal. A middleware that neither called next() nor\n // threw has done nothing, and guessing which it meant turns a\n // forgotten `return` into a check that quietly passes.\n if (!continued) {\n throw new ActionMisuse(\n 'A middleware returned without calling next(). Call it to continue, or throw to refuse.',\n )\n }\n\n return (result as { value?: unknown })?.value\n }\n\n return await run()\n }\n\n return {\n use(middleware) {\n return build([...middlewares, middleware as never], schema) as never\n },\n input(next) {\n return build(middlewares, next) as never\n },\n handler(fn) {\n return markClientBuilt(async (raw?: unknown) => {\n try {\n return { data: (await pipeline(raw, fn)) as Awaited<ReturnType<typeof fn>> }\n } catch (error) {\n // Past onError deliberately — see ActionMisuse.\n if (error instanceof ActionMisuse) throw error\n\n // A redirect is an instruction, not a failure: it has to reach\n // the host, which answers the action with the destination. Caught\n // here it became { serverError: 'Something went wrong.' } - a\n // login that succeeded and then showed an error.\n if (isRedirectSignal(error)) throw error\n\n if (isActionValidationError(error)) {\n return { validationErrors: error.errors }\n }\n\n return { serverError: report(error) }\n }\n })\n },\n query(fn, options) {\n const read = async (raw?: unknown) => {\n try {\n return await pipeline(raw, fn)\n } catch (error) {\n if (error instanceof ActionMisuse) throw error\n if (isRedirectSignal(error)) throw error\n\n // Thrown, not returned. A cache library reports failure by\n // rejection, so a query that answered with an error-shaped object\n // would look like a successful read of something odd.\n if (isActionValidationError(error)) {\n throw new QueryValidationError(error.errors)\n }\n\n throw new Error(report(error))\n }\n }\n\n return markClientBuilt(markQuery(read, options)) as (input?: unknown) => Promise<never>\n },\n }\n }\n\n return build([], null)\n}\n"]}
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"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","sourcesContent":["// Server actions with a schema, middleware, and types that follow from both.\n//\n// export const action = createActionClient({ onError: report })\n//\n// export const createPost = action\n// .use(async ({ next }) => next({ ctx: { user: await currentUser() } }))\n// .input(z.object({ title: z.string().min(3) }))\n// .handler(async ({ input, ctx }) => savePost(ctx.user.id, input.title))\n//\n// `input` is typed from the schema and `ctx` from every middleware that ran, so\n// the handler is checked against both without either being written down twice.\n//\n// The built action RETURNS its failures rather than throwing them, and that is\n// not a style choice. React serialises a rejected server action opaquely —\n// production strips the message and leaves a digest — so a thrown validation\n// error reaches the browser as \"an error occurred\" and the fields it named are\n// gone. A returned object crosses the boundary intact.\n//\n// Distinct from `middleware.ts` in a route directory, which decides whether a\n// page may render. This wraps one action. They are different questions: an\n// action is reachable without any page, which is why it defends itself.\n\nimport { validateWith, type StandardSchemaV1 } from './js/standardSchema.js'\nimport { decodeFormData } from './js/formEncoding.js'\nimport { markQuery, QueryValidationError, type QueryOptions } from './query.js'\nimport { isRedirectSignal } from './redirectDigest.js'\n\n/**\n * What an action answers with. One of `data`, `validationErrors`,\n * `serverError` and `redirected` is set; `refusal` only ever beside\n * `serverError`.\n */\nexport interface ActionResult<Data, Refusal = never> {\n /** What the handler returned. */\n data?: Data\n /** Field name to messages, in the shape a form already renders. */\n validationErrors?: Record<string, string[]>\n /**\n * The action did not happen, as a message the browser may see: a refusal's\n * own message - see `refuse()` - or something unexpected, reduced by\n * `onError` to one that says nothing a stranger should not see.\n */\n serverError?: string\n /**\n * What a refusal said beside its message - the things blocking a delete,\n * say - checked against the schema the action declared with `.refusal()`\n * and typed from it. Set only by `refuse()`, here or on the backend.\n */\n refusal?: Refusal\n /**\n * Where the action sent the visitor. Set by the client, which has already\n * started the navigation: the page is on its way there, and there is\n * nothing for the caller to do. Never set on the server side of the call.\n */\n redirected?: string\n}\n\n/**\n * A middleware that neither continued nor refused.\n *\n * Never reported through `onError`: that reduces an error to a message for the\n * browser, and this one is for whoever wrote the middleware. A check that\n * forgot to call `next()` would otherwise look exactly like a check that\n * passed.\n */\nexport class ActionMisuse extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ActionMisuse'\n }\n}\n\n/** Refuse from inside a handler, naming the fields. */\n/**\n * Marks a refusal so it survives a bundle seam.\n *\n * A property rather than `instanceof`, for the reason this project keeps\n * running into: an app's actions are bundled separately from the engine, so\n * each gets its own copy of this module and its own copy of the class.\n * `instanceof` compares identity across that seam and is simply false — and\n * the refusal is then reported as a server error, so the form shows \"Something\n * went wrong\" instead of naming the fields. Everything works; nothing logs.\n *\n * Symbol.for, so the two copies agree on the key as well as the value.\n */\nconst VALIDATION_MARK = Symbol.for('@rsc-kit/core.action-validation')\n\n/**\n * Set on every function a client builds, so the build can tell them from a\n * bare \"use server\" export. The difference is the middleware: a bare export\n * runs with nothing checking who called it, and nothing else in the app\n * would ever say so. Symbol.for, because the build reads the mark from the\n * bundled copy of this module and the app's actions were built by another.\n */\nconst CLIENT_MARK = Symbol.for('@rsc-kit/core.action-client')\n\n/** Whether a server action was built by createActionClient, and so ran its chain. */\nexport function isClientBuilt(fn: unknown): boolean {\n return typeof fn === 'function' && (fn as unknown as Record<symbol, unknown>)[CLIENT_MARK] === true\n}\n\nfunction markClientBuilt<F extends Function>(fn: F): F {\n Object.defineProperty(fn, CLIENT_MARK, { value: true })\n\n return fn\n}\n\nexport class ActionValidationError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed')\n this.name = 'ActionValidationError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[VALIDATION_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionValidationError(error: unknown): error is ActionValidationError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[VALIDATION_MARK] === true\n )\n}\n\n/**\n * The field errors a handler may report, keyed by its own input's fields.\n *\n * `''` is the whole submission — for a refusal that is about no field in\n * particular, which is where the form already looks for one.\n */\nexport type FieldErrorsFor<Input> = Partial<\n Record<(Input extends object ? keyof Input & string : string) | '', string | string[]>\n>\n\nconst REFUSAL_MARK = Symbol.for('@rsc-kit/core.action-refusal')\n\n/**\n * An action declining to do what it was asked, on purpose, with a message for\n * the person who asked and - optionally - data the page can act on: the\n * records blocking a delete, the plan a quota belongs to.\n *\n * Not a validation error: the input was fine, the state of things says no.\n * And not a failure: its message is meant to be seen, so it is never\n * replaced by `onError`'s \"Something went wrong.\". It arrives as the result's\n * `serverError`, which is where a form's `formError` already reads, and its\n * data as `refusal`.\n */\nexport class ActionRefusal extends Error {\n public readonly data: unknown\n /** The status a request that is not an action - a page, a form posted without javascript - answers with. */\n public readonly refusalStatus: number\n\n constructor(message: string, data?: unknown, status = 409) {\n super(message)\n this.name = 'ActionRefusal'\n this.data = data\n this.refusalStatus = status\n ;(this as unknown as Record<symbol, boolean>)[REFUSAL_MARK] = true\n }\n}\n\n/** Whether this is a refusal, whichever copy of the class built it. */\nexport function isActionRefusal(error: unknown): error is ActionRefusal {\n return typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[REFUSAL_MARK] === true\n}\n\n/**\n * Decline, with a message to show and optional data to act on. Throws.\n *\n * if (attached.length) refuse('Still in use', { blockers: attached })\n *\n * From a handler, a middleware or anything they call. The data reaches the\n * page only when the action declares its shape with `.refusal(schema)`, and\n * only once it matches - see ActionResult.refusal. Inside a handler, the\n * `refuse` it is given is typed to that schema; this one takes anything.\n */\nexport function refuse(message: string, data?: unknown, options: { status?: number } = {}): never {\n throw new ActionRefusal(message, data, options.status)\n}\n\n/**\n * What a handler is given.\n *\n * `fieldErrors` is here as well as exported on its own, and the one here is\n * the one to use: it is typed to this handler's input, so a field the schema\n * does not have is a type error rather than an error the form never shows.\n * The bare export takes any string, for the rare check that runs outside a\n * handler.\n */\nexport interface HandlerArgs<Input, Ctx, Refusal = never> {\n input: Input\n ctx: Ctx\n /**\n * Decline, with a message and the data the action declared with\n * `.refusal(schema)`. Throws; write `return refuse(...)`, as with\n * fieldErrors.\n */\n refuse: [Refusal] extends [never]\n ? (message: string) => never\n : (message: string, data: Refusal, options?: { status?: number }) => never\n /**\n * Fail with errors on this input's fields. Throws; nothing after it runs.\n *\n * Write `return fieldErrors(...)`. It throws either way, but TypeScript does\n * not treat a never-return as terminating when the callee is a destructured\n * binding — only a declaration or an explicitly annotated variable — so\n * without the return, a value checked on the line above is still possibly\n * undefined on the line below. The return is what lets the type narrow.\n */\n fieldErrors: (errors: FieldErrorsFor<Input>) => never\n}\n\n/**\n * Fail with field errors the form can show.\n *\n * For what a schema cannot know — a name already taken, a balance too low.\n * Throws, so the handler stops where it is; the action turns it into a\n * returned result on the way out.\n *\n * Untyped by field, because it has no handler to take the input from. Inside\n * one, use the `fieldErrors` the handler is given instead.\n */\nexport function fieldErrors(errors: Record<string, string[] | string>): never {\n const normalised: Record<string, string[]> = {}\n\n for (const [field, message] of Object.entries(errors)) {\n normalised[field] = Array.isArray(message) ? message : [message]\n }\n\n throw new ActionValidationError(normalised)\n}\n\n/**\n * What `next()` hands back, carrying what the step added.\n *\n * The context a middleware contributes cannot be inferred from the arguments\n * it passes to `next` — TypeScript infers from a function's return, not from a\n * call inside it. So `next` returns this, the middleware returns it, and the\n * addition is read off the middleware's own return type.\n */\nexport interface MiddlewareResult<Extra> {\n readonly ctx: Extra\n readonly value: unknown\n}\n\n/**\n * A step that runs before the handler.\n *\n * It calls `next` to continue, optionally adding to the context, and what it\n * adds shows up in the handler's types. Returning without calling `next` — or\n * throwing — stops the action, which is how a check refuses.\n */\nexport type ActionMiddleware<Ctx, Extra extends Record<string, unknown>> = (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n}) => Promise<MiddlewareResult<Extra>>\n\ntype Output<S> = S extends StandardSchemaV1<unknown, infer O> ? O : never\n\n/** What a caller may pass, before the schema parses it. */\ntype InputOf<S> = S extends StandardSchemaV1<infer I, unknown> ? I : unknown\n\n/**\n * A built action, as its callers see it.\n *\n * Typed from the schema's input when there is one, so a call with the wrong\n * shape fails the typecheck rather than the validation - and FormData beside\n * it, which is how a form calls the same action. Without a schema, anything.\n */\nexport type Action<Raw, Data, Refusal = never> = unknown extends Raw\n ? (input?: unknown) => Promise<ActionResult<Data, Refusal>>\n : undefined extends Raw\n ? (input?: Raw | FormData) => Promise<ActionResult<Data, Refusal>>\n : (input: Raw | FormData) => Promise<ActionResult<Data, Refusal>>\n\nexport interface ActionBuilder<Ctx extends Record<string, unknown>, Input, Raw = unknown, Refusal = never> {\n /** Add a step, and whatever context it contributes. */\n use<Extra extends Record<string, unknown> = Record<string, never>>(\n middleware: (args: {\n ctx: Ctx\n next: <E extends Record<string, unknown> = Record<string, never>>(\n opts?: { ctx?: E },\n ) => Promise<MiddlewareResult<E>>\n }) => Promise<MiddlewareResult<Extra>>,\n ): ActionBuilder<Ctx & Extra, Input, Raw, Refusal>\n /** Parse and check what the caller sent. The handler's `input` follows. */\n input<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Output<S>, InputOf<S>, Refusal>\n /**\n * The shape of what a refusal may carry - see `refuse()`. The page gets\n * `result.refusal` typed from it, and only data that matches it: data that\n * does not is a bug in the server, answered as one.\n */\n refusal<S extends StandardSchemaV1>(schema: S): ActionBuilder<Ctx, Input, Raw, Output<S>>\n /**\n * The body.\n *\n * What the caller gets is always the result object: `data`, or a\n * refusal, or - when the action redirected - `redirected`, set by the\n * client once the navigation is under way. Always an object, so a caller\n * reading `result.validationErrors` after a redirect reads undefined and\n * not a TypeError inside a transition, which unmounts the root.\n */\n handler<Data>(\n fn: (args: HandlerArgs<Input, Ctx, Refusal>) => Promise<Data> | Data,\n ): Action<Raw, Data, Refusal>\n /**\n * The body of a READ, sharing this client's middleware and schema.\n *\n * export const getPosts = client.input(filter).query(async ({ input, ctx }) =>\n * db.posts(ctx.user.id, input))\n *\n * The same builder as `handler`, and deliberately so: an app configures its\n * auth check and its error reporting once, and both a mutation and a read go\n * through them.\n *\n * It fails differently, though, and that is not an oversight. An action\n * RETURNS its failures because React serialises a rejection opaquely. A query\n * is handed to a cache library as a fetcher, and every one of them reports\n * failure by rejection — so this returns the data directly and throws, and\n * the endpoint carries the message across for it.\n */\n query<Data>(\n fn: (args: { input: Input; ctx: Ctx }) => Promise<Data> | Data,\n options?: QueryOptions,\n ): (input?: unknown) => Promise<Data>\n}\n\nexport interface ActionClientOptions {\n /**\n * What the browser is told when something unexpected throws.\n *\n * Everything reaching here is a bug or an outage, and its message may say\n * more than a stranger should see — a query, a path, a host. Returning a\n * fixed string is the safe default; return the message only for errors you\n * raised deliberately.\n */\n onError?: (error: unknown) => string\n}\n\nconst GENERIC = 'Something went wrong.'\n\n/**\n * FormData in, a plain object out — what a schema expects to be handed.\n *\n * The form's own decoder, so the object validated here is the object the\n * browser validated. This had a flat decoder of its own: `items[0].name`\n * stayed a key spelled exactly that, `<Form>` had already parsed it into an\n * array, and a nested form passed in the browser and failed on the server\n * with the fields it named gone.\n */\nconst fromFormData = (body: FormData, schema: unknown): Record<string, unknown> => decodeFormData(body, schema)\n\nexport function createActionClient(\n options: ActionClientOptions = {},\n): ActionBuilder<Record<never, never>, undefined> {\n const report = options.onError ?? (() => GENERIC)\n\n function build<Ctx extends Record<string, unknown>, Input>(\n middlewares: ActionMiddleware<never, never>[],\n schema: StandardSchemaV1 | null,\n refusalSchema: StandardSchemaV1 | null = null,\n ): ActionBuilder<Ctx, Input> {\n /**\n * Validate, run the chain, call the body.\n *\n * Shared by both terminals, which is the point of putting a read on this\n * builder at all: one set of middleware, one schema, one place the auth\n * check lives. Refusals leave by throwing, and each terminal decides what\n * that should look like from the outside.\n */\n const pipeline = async (\n raw: unknown,\n fn: (args: HandlerArgs<never, never>) => unknown,\n ): Promise<unknown> => {\n const value = raw instanceof FormData ? fromFormData(raw, schema) : raw\n\n if (schema) {\n const invalid = await validateWith(schema, value)\n\n if (invalid) throw new ActionValidationError(invalid)\n }\n\n const parsed = schema\n ? ((await schema['~standard'].validate(value)).value as Input)\n : (value as Input)\n\n // Composed inside-out so the first `use` is the outermost — it sees the\n // others run, which is what makes timing and cleanup possible rather\n // than only checks.\n let ctx = {} as Ctx\n let index = 0\n\n const run = async (): Promise<unknown> => {\n const middleware = middlewares[index++]\n\n if (!middleware) {\n return await fn({\n input: parsed as never,\n ctx: ctx as never,\n // The same function as the export; the type on the way in is what\n // is different, and the type is the handler's input.\n fieldErrors: fieldErrors as never,\n refuse: refuse as never,\n })\n }\n\n let continued = false\n\n const result = await (middleware as unknown as ActionMiddleware<Ctx, Record<string, unknown>>)({\n ctx,\n next: (async (opts?: { ctx?: Record<string, unknown> }) => {\n continued = true\n ctx = { ...ctx, ...(opts?.ctx ?? {}) } as Ctx\n\n return { ctx, value: await run() }\n }) as never,\n })\n\n // Silence is not refusal. A middleware that neither called next() nor\n // threw has done nothing, and guessing which it meant turns a\n // forgotten `return` into a check that quietly passes.\n if (!continued) {\n throw new ActionMisuse(\n 'A middleware returned without calling next(). Call it to continue, or throw to refuse.',\n )\n }\n\n return (result as { value?: unknown })?.value\n }\n\n return await run()\n }\n\n /**\n * A refusal as the page gets it: its message, always - it was written to\n * be seen - and its data only once the declared schema has checked it.\n * Data with no schema to check it, or that fails the schema, never\n * reaches the browser: the first is said in the server's log, the second\n * is a bug in the server and answered as one.\n */\n const refusalResult = async (error: ActionRefusal): Promise<ActionResult<never, unknown>> => {\n if (error.data === undefined) return { serverError: error.message }\n\n if (!refusalSchema) {\n console.error(\n `[rsc-kit] An action refused with data, and declares no .refusal(schema) for it - the message was sent, the data was not: ${error.message}`,\n )\n\n return { serverError: error.message }\n }\n\n const checked = await refusalSchema['~standard'].validate(error.data)\n\n if (checked.issues) {\n return {\n serverError: report(\n new Error(\n `An action refused with data its .refusal(schema) does not accept: ${checked.issues.map((issue) => issue.message).join('; ')}`,\n ),\n ),\n }\n }\n\n return { serverError: error.message, refusal: checked.value }\n }\n\n return {\n use(middleware) {\n return build([...middlewares, middleware as never], schema, refusalSchema) as never\n },\n input(next) {\n return build(middlewares, next, refusalSchema) as never\n },\n refusal(next) {\n return build(middlewares, schema, next) as never\n },\n handler(fn) {\n return markClientBuilt(async (raw?: unknown) => {\n try {\n return { data: (await pipeline(raw, fn)) as Awaited<ReturnType<typeof fn>> }\n } catch (error) {\n // Past onError deliberately — see ActionMisuse.\n if (error instanceof ActionMisuse) throw error\n\n // A redirect is an instruction, not a failure: it has to reach\n // the host, which answers the action with the destination. Caught\n // here it became { serverError: 'Something went wrong.' } - a\n // login that succeeded and then showed an error.\n if (isRedirectSignal(error)) throw error\n\n if (isActionValidationError(error)) {\n return { validationErrors: error.errors }\n }\n\n if (isActionRefusal(error)) return (await refusalResult(error)) as ActionResult<never>\n\n return { serverError: report(error) }\n }\n })\n },\n query(fn, options) {\n const read = async (raw?: unknown) => {\n try {\n return await pipeline(raw, fn)\n } catch (error) {\n if (error instanceof ActionMisuse) throw error\n if (isRedirectSignal(error)) throw error\n\n // Thrown, not returned. A cache library reports failure by\n // rejection, so a query that answered with an error-shaped object\n // would look like a successful read of something odd.\n if (isActionValidationError(error)) {\n throw new QueryValidationError(error.errors)\n }\n\n // Its own message, as an action's is, and its checked data on\n // the error for a fetcher that wants it.\n if (isActionRefusal(error)) {\n const refused = await refusalResult(error)\n\n throw Object.assign(new Error(refused.serverError), { refusal: refused.refusal })\n }\n\n throw new Error(report(error))\n }\n }\n\n return markClientBuilt(markQuery(read, options)) as (input?: unknown) => Promise<never>\n },\n }\n }\n\n return build([], null)\n}\n"]}
@@ -39,6 +39,7 @@ export declare const CASES: {
39
39
  readonly 'Conformance.unauthorized': 'Refuse as signed in but not allowed.';
40
40
  readonly 'Conformance.notFound': "Refuse as not found (404), the way the framework does: Go's Refuse(404), Laravel's abort(404).";
41
41
  readonly 'Conformance.refuse': 'Refuse with status 429 and the message "Slow down."';
42
+ readonly 'Conformance.refuseWithData': string;
42
43
  readonly 'Conformance.invalid': 'Refuse the input: a validation error on the field "name".';
43
44
  readonly 'Conformance.redirect': 'Send the visitor to /login.';
44
45
  readonly 'Conformance.revalidate': 'Mark the region "orders" stale, and return "ok".';
@@ -23,6 +23,7 @@ import { argv, exit, stderr, stdout } from 'node:process';
23
23
  import { pathToFileURL } from 'node:url';
24
24
  import { httpHostCalls } from './hostCalls.js';
25
25
  import { isNotFoundSignal } from './notFound.js';
26
+ import { isActionRefusal } from './action.js';
26
27
  import { withRedirect } from './redirect.js';
27
28
  import { withRequest, withResponseDraft } from './request.js';
28
29
  import { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js';
@@ -40,6 +41,8 @@ export const CASES = {
40
41
  'Conformance.unauthorized': 'Refuse as signed in but not allowed.',
41
42
  'Conformance.notFound': "Refuse as not found (404), the way the framework does: Go's Refuse(404), Laravel's abort(404).",
42
43
  'Conformance.refuse': 'Refuse with status 429 and the message "Slow down."',
44
+ 'Conformance.refuseWithData': 'Refuse with status 409, the message "Still in use", and the data { blockers: [{ id: 7, href: "/orders/7" }] } - ' +
45
+ "the adapter's own refuse, carrying data for the page to act on.",
43
46
  'Conformance.invalid': 'Refuse the input: a validation error on the field "name".',
44
47
  'Conformance.redirect': 'Send the visitor to /login.',
45
48
  'Conformance.revalidate': 'Mark the region "orders" stale, and return "ok".',
@@ -57,6 +60,18 @@ export const CASES = {
57
60
  'ms - never + 1 alone. May hold the call up to wait ms for one to differ; may answer at once.',
58
61
  };
59
62
  const INSTANT = Date.parse('2026-01-02T03:04:05Z');
63
+ /** Two JSON values alike, whatever order an object's keys were written in. */
64
+ function sameValue(a, b) {
65
+ if (Array.isArray(a) || Array.isArray(b)) {
66
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
67
+ }
68
+ if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
69
+ const keys = Object.keys(a);
70
+ return (keys.length === Object.keys(b).length &&
71
+ keys.every((key) => sameValue(a[key], b[key])));
72
+ }
73
+ return a === b;
74
+ }
60
75
  const ECHOES = [null, 0, -1.5, 'héllo ✓ "quoted"', [1, 'a', null], { a: { b: [true, false] }, c: '' }];
61
76
  /**
62
77
  * Run the suite against a client. Every case runs; none stops another.
@@ -147,6 +162,15 @@ export async function conformance(call, options) {
147
162
  expect(error.refusalStatus === 429, 'status ' + String(error.refusalStatus));
148
163
  expect(String(error.message).includes('Slow down.'), 'message ' + JSON.stringify(error.message));
149
164
  });
165
+ await check('a refusal carries its data, its message and its status', async () => {
166
+ const error = (await rejection(() => call('Conformance.refuseWithData')));
167
+ expect(isActionRefusal(error), 'raised as ' + String(error) + ', not as a refusal - is refusalData in the reply?');
168
+ expect(error.message === 'Still in use', 'message ' + JSON.stringify(error.message));
169
+ expect(error.refusalStatus === 409, 'status ' + String(error.refusalStatus));
170
+ // Compared as values, not as text: an object's keys have no order, and
171
+ // Go writes a map's sorted.
172
+ expect(sameValue(error.data, { blockers: [{ id: 7, href: '/orders/7' }] }), 'data ' + JSON.stringify(error.data));
173
+ });
150
174
  await check('refused input names its field', async () => {
151
175
  const error = (await rejection(() => call('Conformance.invalid')));
152
176
  expect(Array.isArray(error.errors?.name) && error.errors.name.length > 0, 'errors ' + JSON.stringify(error.errors));