@alxia/react-router 0.10.0 → 0.11.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.
Files changed (54) hide show
  1. package/README.md +176 -1
  2. package/dist/errors/api-error.d.ts +86 -0
  3. package/dist/errors/api-error.d.ts.map +1 -0
  4. package/dist/errors/api-errors.d.ts +96 -0
  5. package/dist/errors/api-errors.d.ts.map +1 -0
  6. package/dist/errors/call-failure.d.ts +45 -0
  7. package/dist/errors/call-failure.d.ts.map +1 -0
  8. package/dist/errors/catalogue-check.d.ts +24 -0
  9. package/dist/errors/catalogue-check.d.ts.map +1 -0
  10. package/dist/errors/catalogue-read.d.ts +33 -0
  11. package/dist/errors/catalogue-read.d.ts.map +1 -0
  12. package/dist/errors/catalogue.d.ts +59 -0
  13. package/dist/errors/catalogue.d.ts.map +1 -0
  14. package/dist/errors/code-resolver.d.ts +44 -0
  15. package/dist/errors/code-resolver.d.ts.map +1 -0
  16. package/dist/errors/describe-refusal.d.ts +6 -0
  17. package/dist/errors/describe-refusal.d.ts.map +1 -0
  18. package/dist/errors/describe.d.ts +3 -0
  19. package/dist/errors/describe.d.ts.map +1 -0
  20. package/dist/errors/fetcher-result.d.ts +21 -0
  21. package/dist/errors/fetcher-result.d.ts.map +1 -0
  22. package/dist/errors/form-errors.d.ts +8 -0
  23. package/dist/errors/form-errors.d.ts.map +1 -0
  24. package/dist/errors/index.d.ts +15 -0
  25. package/dist/errors/index.d.ts.map +1 -0
  26. package/dist/errors/index.js +602 -0
  27. package/dist/errors/index.js.map +29 -0
  28. package/dist/errors/json-actions.d.ts +38 -0
  29. package/dist/errors/json-actions.d.ts.map +1 -0
  30. package/dist/errors/log.d.ts +8 -0
  31. package/dist/errors/log.d.ts.map +1 -0
  32. package/dist/errors/message-values.d.ts +11 -0
  33. package/dist/errors/message-values.d.ts.map +1 -0
  34. package/dist/errors/messages.d.ts +63 -0
  35. package/dist/errors/messages.d.ts.map +1 -0
  36. package/dist/errors/page-path.d.ts +9 -0
  37. package/dist/errors/page-path.d.ts.map +1 -0
  38. package/dist/errors/provider.d.ts +20 -0
  39. package/dist/errors/provider.d.ts.map +1 -0
  40. package/dist/errors/request-id.d.ts +5 -0
  41. package/dist/errors/request-id.d.ts.map +1 -0
  42. package/dist/errors/route-error.d.ts +31 -0
  43. package/dist/errors/route-error.d.ts.map +1 -0
  44. package/dist/errors/standard-schema.d.ts +34 -0
  45. package/dist/errors/standard-schema.d.ts.map +1 -0
  46. package/dist/errors/submit-form.d.ts +16 -0
  47. package/dist/errors/submit-form.d.ts.map +1 -0
  48. package/dist/errors/use-json-fetcher.d.ts +18 -0
  49. package/dist/errors/use-json-fetcher.d.ts.map +1 -0
  50. package/docs/README.md +2 -1
  51. package/docs/guide/errors.md +838 -0
  52. package/docs/roadmap.md +19 -0
  53. package/docs/troubleshooting.md +481 -0
  54. package/package.json +13 -3
@@ -0,0 +1,6 @@
1
+ import type { ApiErrors } from './api-errors';
2
+ /** The API's refusal of `error`, or `undefined` when it is not one. */
3
+ export declare function refusalOf<L extends string>(errors: Pick<ApiErrors<L>, 'apiErrorMessage' | 'unavailableMessage'>, error: unknown, language: L): ReturnType<ApiErrors<L>['apiRefusal']>;
4
+ /** The worded message of each field an API error names, by field path. */
5
+ export declare function fieldErrorsOf<L extends string>(errors: Pick<ApiErrors<L>, 'apiErrorMessage' | 'translateCode'>, error: Parameters<ApiErrors<L>['fieldErrors']>[0], language: L): ReturnType<ApiErrors<L>['fieldErrors']>;
6
+ //# sourceMappingURL=describe-refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-refusal.d.ts","sourceRoot":"","sources":["../../src/errors/describe-refusal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EACzC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,GAAG,oBAAoB,CAAC,EACpE,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,CAAC,GACT,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAWxC;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAC7C,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,GAAG,eAAe,CAAC,EAC/D,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,EACjD,QAAQ,EAAE,CAAC,GACT,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAYzC"}
@@ -0,0 +1,3 @@
1
+ import type { ApiErrors, ApiErrorsOptions } from './api-errors';
2
+ export declare function describeApiErrors<L extends string>({ messages, unavailable, refused, byCode, isApiCode, }: ApiErrorsOptions<L>): ApiErrors<L>;
3
+ //# sourceMappingURL=describe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../../src/errors/describe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMhE,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,EACnD,QAAQ,EACR,WAAW,EACX,OAAO,EACP,MAAW,EACX,SAAyC,GACzC,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAkDpC"}
@@ -0,0 +1,21 @@
1
+ import type { ApiErrors, ApiRefusal } from './api-errors';
2
+ import { type RouteErrorOptions } from './route-error';
3
+ /**
4
+ * A fetcher's refusal: the API's (`apiRefusal`), an unreachable API's too,
5
+ * so the page stays where it was. A 401 (the session caller redirects
6
+ * first) or any other failure is thrown, for the error page.
7
+ */
8
+ export declare function fetcherRefusal<L extends string>(errors: Pick<ApiErrors<L>, 'apiRefusal'>, error: unknown, language: L, route?: RouteErrorOptions): ApiRefusal;
9
+ /**
10
+ * Runs the mutation of an action a fetcher posted to: `{ ok: true }` with
11
+ * `about` and the mutation's value, or the refusal (`fetcherRefusal`) as a
12
+ * worded `error`, with its HTTP status, for a toast or a form message.
13
+ */
14
+ export declare function fetcherResult<L extends string, About extends object, Value extends object = Record<never, never>>(errors: Pick<ApiErrors<L>, 'apiRefusal'>, language: L, about: About, run: () => Promise<Value>, route?: RouteErrorOptions): Promise<import("react-router").UNSAFE_DataWithResponseInit<{
15
+ ok: false;
16
+ } & About & {
17
+ error: string;
18
+ }> | ({
19
+ ok: true;
20
+ } & About & Awaited<Value>)>;
21
+ //# sourceMappingURL=fetcher-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher-result.d.ts","sourceRoot":"","sources":["../../src/errors/fetcher-result.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,eAAe,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAC9C,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,EACxC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,CAAC,EACX,KAAK,CAAC,EAAE,iBAAiB,GACvB,UAAU,CAOZ;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAClC,CAAC,SAAS,MAAM,EAChB,KAAK,SAAS,MAAM,EACpB,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAE3C,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,EACxC,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,EACzB,KAAK,CAAC,EAAE,iBAAiB;;;;;;6BAQzB"}
@@ -0,0 +1,8 @@
1
+ /** What a form action answers when the API refused the input. */
2
+ export type FormErrors = {
3
+ /** Messages by form field, already translated. */
4
+ fieldErrors?: Record<string, string>;
5
+ /** A message for the form as a whole, already translated. */
6
+ formError?: string | undefined;
7
+ };
8
+ //# sourceMappingURL=form-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-errors.d.ts","sourceRoot":"","sources":["../../src/errors/form-errors.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAAG;IACxB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B,CAAC"}
@@ -0,0 +1,15 @@
1
+ export { type ApiErrorEntryLike, type ApiErrorExtensionsLike, type ApiErrorLike, type ApiErrorParamsLike, type ApiFieldErrorLike, isApiErrorLike, waitMinutes, } from './api-error';
2
+ export { type ApiErrors, type ApiErrorsOptions, type ApiRefusal, createApiErrors, } from './api-errors';
3
+ export { apiCallFailure, type CallFailureOptions, type DocumentLike, operationName, } from './call-failure';
4
+ export { type CatalogueReply, createErrorCatalogue, type ErrorCatalogue, type ErrorCatalogueOptions, } from './catalogue';
5
+ export { type ApiCodeResolver, type ApiCodeResolverOptions, apiCodeResolver, type CodeIssueLike, } from './code-resolver';
6
+ export type { FormErrors } from './form-errors';
7
+ export { type JsonActions, type JsonActionsOptions, jsonActions, } from './json-actions';
8
+ export { type ApiMessages, type ApiMessagesOptions, type CatalogueMessage, type CatalogueMessages, createApiMessages, type MessageFormat, type MessageValues, type ParamKind, } from './messages';
9
+ export { nextOf } from './page-path';
10
+ export { ApiMessagesProvider, type ApiMessagesProviderProps, } from './provider';
11
+ export { REQUEST_ID_HEADER, requestIdOf } from './request-id';
12
+ export { errorRequestId, type RouteErrorData, type RouteErrorOptions, routeError, } from './route-error';
13
+ export type { StandardSchemaV1 } from './standard-schema';
14
+ export { type JsonBody, useJsonFetcher } from './use-json-fetcher';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,cAAc,EACd,WAAW,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,eAAe,GACf,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,aAAa,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,eAAe,EACf,KAAK,aAAa,GAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,WAAW,GACX,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,SAAS,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EACN,mBAAmB,EACnB,KAAK,wBAAwB,GAC7B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EACN,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,UAAU,GACV,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,KAAK,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,602 @@
1
+ // src/errors/api-error.ts
2
+ var isNamed = (error, name) => error instanceof Error && error.name === name;
3
+ function isApiErrorLike(error) {
4
+ return isNamed(error, "ApiError") && Array.isArray(error.errors) && typeof error.status === "number";
5
+ }
6
+ function waitMinutes(extensions) {
7
+ const minutes = extensions?.params?.minutes ?? extensions?.minutes;
8
+ if (minutes !== undefined)
9
+ return Math.max(1, Number(minutes) || 1);
10
+ const seconds = extensions?.params?.retryAfter ?? extensions?.retryAfter;
11
+ return Math.max(1, Math.ceil(Number(seconds) / 60) || 1);
12
+ }
13
+ function valuesOf(extensions) {
14
+ if (!extensions)
15
+ return;
16
+ return extensions.params ? { ...extensions, ...extensions.params } : extensions;
17
+ }
18
+ var isStatusError = (error) => isNamed(error, "ApiStatusError") && typeof error.status === "number";
19
+ var isUnavailableError = (error) => isNamed(error, "ApiUnavailableError") && typeof error.reason === "string";
20
+ // src/errors/code-resolver.ts
21
+ var API_CODE = /^[a-z-]+\.errors\.[a-z0-9-]+$/;
22
+ function paramsCode(issue) {
23
+ const code = issue.params?.code;
24
+ return typeof code === "string" ? code : undefined;
25
+ }
26
+ function apiCodeResolver(messages, options) {
27
+ const isApiCode = (key) => options.isApiCode ? options.isApiCode(key) : API_CODE.test(key);
28
+ return {
29
+ translate(key, context, provideLanguage) {
30
+ if (!isApiCode(key))
31
+ return options.translate(key, context, provideLanguage);
32
+ const language = typeof provideLanguage === "function" ? provideLanguage() : provideLanguage ?? options.language();
33
+ return messages.apiMessage(key, context, language) ?? key;
34
+ },
35
+ codeOf(issue) {
36
+ return options.codeOf ? options.codeOf(issue) : paramsCode(issue);
37
+ }
38
+ };
39
+ }
40
+
41
+ // src/errors/describe-refusal.ts
42
+ function refusalOf(errors, error, language) {
43
+ if (isApiErrorLike(error)) {
44
+ return {
45
+ message: errors.apiErrorMessage(error, language),
46
+ status: error.status
47
+ };
48
+ }
49
+ if (error instanceof Response && error.status === 503) {
50
+ return { message: errors.unavailableMessage(language), status: 503 };
51
+ }
52
+ return;
53
+ }
54
+ function fieldErrorsOf(errors, error, language) {
55
+ const extensions = error.errors[0]?.extensions;
56
+ const fields = Array.isArray(extensions?.fields) ? extensions.fields : [];
57
+ if (!fields.length && extensions?.field) {
58
+ return { [extensions.field]: errors.apiErrorMessage(error, language) };
59
+ }
60
+ return Object.fromEntries(fields.map(({ path, code, message, params }) => [
61
+ path,
62
+ errors.translateCode(code, message, language, params)
63
+ ]));
64
+ }
65
+
66
+ // src/errors/fetcher-result.ts
67
+ import { data as data2 } from "react-router";
68
+
69
+ // src/errors/route-error.ts
70
+ import { data } from "react-router";
71
+
72
+ // src/errors/request-id.ts
73
+ var REQUEST_ID_HEADER = "x-request-id";
74
+ function requestIdOf(response) {
75
+ return response.headers.get(REQUEST_ID_HEADER) || undefined;
76
+ }
77
+
78
+ // src/errors/route-error.ts
79
+ var ROUTED = new Set([401, 403, 404, 503]);
80
+ function routeError(error, { statuses = ROUTED } = {}) {
81
+ if (isApiErrorLike(error)) {
82
+ const { requestId } = error;
83
+ if (statuses.has(error.status)) {
84
+ return data({ code: error.code, message: error.message, requestId }, { status: error.status });
85
+ }
86
+ return requestId ? data({ requestId }, { status: 500 }) : error;
87
+ }
88
+ if (error instanceof Response && statuses.has(error.status)) {
89
+ const requestId = requestIdOf(error);
90
+ if (requestId) {
91
+ return data({ requestId }, { status: error.status });
92
+ }
93
+ }
94
+ return error;
95
+ }
96
+ function errorRequestId(error) {
97
+ if (typeof error !== "object" || error === null)
98
+ return;
99
+ const { data: body } = error;
100
+ if (typeof body !== "object" || body === null)
101
+ return;
102
+ const { requestId } = body;
103
+ return typeof requestId === "string" && requestId ? requestId : undefined;
104
+ }
105
+
106
+ // src/errors/fetcher-result.ts
107
+ function fetcherRefusal(errors, error, language, route) {
108
+ const refusal = isApiErrorLike(error) && error.status === 401 ? undefined : errors.apiRefusal(error, language);
109
+ if (!refusal)
110
+ throw routeError(error, route);
111
+ return refusal;
112
+ }
113
+ async function fetcherResult(errors, language, about, run, route) {
114
+ try {
115
+ return { ok: true, ...about, ...await run() };
116
+ } catch (error) {
117
+ const { message, status } = fetcherRefusal(errors, error, language, route);
118
+ return data2({ ok: false, ...about, error: message }, { status });
119
+ }
120
+ }
121
+
122
+ // src/errors/submit-form.ts
123
+ import { data as data3 } from "react-router";
124
+ async function submitForm(errors, language, run, fieldOf, route) {
125
+ try {
126
+ return { ok: true, value: await run() };
127
+ } catch (error) {
128
+ if (!isApiErrorLike(error) || error.status === 401) {
129
+ throw routeError(error, route);
130
+ }
131
+ const field = error.errors[0]?.extensions?.field;
132
+ if (error.status === 404 && !(field && fieldOf(field))) {
133
+ throw routeError(error, route);
134
+ }
135
+ const byField = {};
136
+ const unplaced = [];
137
+ for (const [path, message] of Object.entries(errors.fieldErrors(error, language))) {
138
+ const field = fieldOf(path);
139
+ if (field)
140
+ byField[field] = message;
141
+ else
142
+ unplaced.push(message);
143
+ }
144
+ const formError = Object.keys(byField).length && !unplaced.length ? undefined : [errors.apiErrorMessage(error, language), ...unplaced].join(" ");
145
+ return data3({ fieldErrors: byField, formError }, { status: error.status });
146
+ }
147
+ }
148
+
149
+ // src/errors/describe.ts
150
+ function describeApiErrors({
151
+ messages,
152
+ unavailable,
153
+ refused,
154
+ byCode = {},
155
+ isApiCode = (code) => API_CODE.test(code)
156
+ }) {
157
+ const translateCode = (code, fallback, language, values) => {
158
+ if (!code)
159
+ return fallback;
160
+ return messages.apiMessage(code, values, language) ?? fallback;
161
+ };
162
+ const apiErrorMessage = (error, language) => {
163
+ const code = error.code;
164
+ if (refused && (!code || !isApiCode(code)))
165
+ return refused(language);
166
+ const own = code && Object.hasOwn(byCode, code) ? byCode[code] : undefined;
167
+ if (own)
168
+ return own(error, language);
169
+ return translateCode(code, error.message, language, valuesOf(error.errors[0]?.extensions));
170
+ };
171
+ const unavailableMessage = (language) => unavailable(language);
172
+ const apiRefusal = (error, language) => refusalOf({ apiErrorMessage, unavailableMessage }, error, language);
173
+ const fieldErrors = (error, language) => fieldErrorsOf({ apiErrorMessage, translateCode }, error, language);
174
+ const parts = { apiRefusal, apiErrorMessage, fieldErrors };
175
+ return {
176
+ translateCode,
177
+ apiErrorMessage,
178
+ unavailableMessage,
179
+ fieldErrors,
180
+ apiRefusal,
181
+ fetcherRefusal: (error, language, route) => fetcherRefusal(parts, error, language, route),
182
+ fetcherResult: (language, about, run, route) => fetcherResult(parts, language, about, run, route),
183
+ submitForm: (language, run, fieldOf, route) => submitForm(parts, language, run, fieldOf, route)
184
+ };
185
+ }
186
+
187
+ // src/errors/api-errors.ts
188
+ function createApiErrors(options) {
189
+ return describeApiErrors(options);
190
+ }
191
+ // src/errors/log.ts
192
+ function quietLog(log, methods) {
193
+ const quiet = {};
194
+ for (const method of methods) {
195
+ quiet[method] = (...args) => {
196
+ try {
197
+ log[method](...args);
198
+ } catch {}
199
+ };
200
+ }
201
+ return quiet;
202
+ }
203
+
204
+ // src/errors/call-failure.ts
205
+ var PASSED_THROUGH = new Set([401, 403, 404]);
206
+ var NO_BODY = new Set([101, 204, 205, 304]);
207
+ function apiCallFailure(error, {
208
+ api,
209
+ operation = "operation",
210
+ requestId,
211
+ signal,
212
+ passedThrough = PASSED_THROUGH,
213
+ log: appLog = console
214
+ }) {
215
+ const log = quietLog(appLog, ["warn", "error"]);
216
+ const call = `${api}: ${operation}${requestId ? ` (request ${requestId})` : ""}`;
217
+ const headers = requestId ? { [REQUEST_ID_HEADER]: requestId } : {};
218
+ const unavailable = (reason, cause) => {
219
+ log.error(`${call} ${reason}`, cause ?? "");
220
+ return new Response(null, { status: 503, headers });
221
+ };
222
+ if (isApiErrorLike(error)) {
223
+ if (requestId)
224
+ error.requestId = requestId;
225
+ const line = `${call} answered ${error.status}${error.code ? ` ${error.code}` : ""}`;
226
+ if (error.status >= 500)
227
+ log.error(line);
228
+ else
229
+ log.warn(line);
230
+ return error;
231
+ }
232
+ if (isStatusError(error)) {
233
+ if (passedThrough.has(error.status)) {
234
+ log.warn(`${call} answered ${error.status}`);
235
+ const statusText = typeof error.statusText === "string" ? error.statusText : "";
236
+ if (NO_BODY.has(error.status)) {
237
+ return new Response(null, {
238
+ status: error.status,
239
+ statusText,
240
+ headers
241
+ });
242
+ }
243
+ return new Response(statusText, {
244
+ status: error.status,
245
+ statusText,
246
+ headers
247
+ });
248
+ }
249
+ return unavailable(`answered ${error.status} without GraphQL`, error.body);
250
+ }
251
+ if (isUnavailableError(error))
252
+ return unavailable(error.reason, error.cause);
253
+ if (signal?.aborted)
254
+ return unavailable("aborted", error);
255
+ return error;
256
+ }
257
+ function operationName(document) {
258
+ for (const definition of document.definitions) {
259
+ if (definition.kind === "OperationDefinition") {
260
+ return definition.name?.value ?? definition.operation ?? "operation";
261
+ }
262
+ }
263
+ return "operation";
264
+ }
265
+ // src/errors/catalogue-check.ts
266
+ async function checkVersions({ reader, versions, ttl, retryMs, log }, now) {
267
+ try {
268
+ const latest = await versions();
269
+ for (const [language, entry] of [...reader.held]) {
270
+ if (latest[language] !== entry.version) {
271
+ await reader.load(language, now);
272
+ } else {
273
+ entry.checkedAt = now;
274
+ }
275
+ }
276
+ } catch (error) {
277
+ log.error("alxia-react-router: the error catalogue was not checked, keeping what is held", error);
278
+ for (const entry of reader.held.values()) {
279
+ entry.checkedAt = now - ttl() + retryMs;
280
+ }
281
+ }
282
+ }
283
+ function safeTtl(ttlMs, fallback, log) {
284
+ return () => {
285
+ try {
286
+ const ttl = typeof ttlMs === "function" ? ttlMs() : ttlMs;
287
+ if (typeof ttl === "number" && Number.isFinite(ttl) && ttl >= 0) {
288
+ return ttl;
289
+ }
290
+ log.error("alxia-react-router: ttlMs is not a duration, using retryMs");
291
+ } catch (error) {
292
+ log.error("alxia-react-router: ttlMs threw, using retryMs", error);
293
+ }
294
+ return fallback;
295
+ };
296
+ }
297
+
298
+ // src/errors/catalogue-read.ts
299
+ async function boundedWait(pending, firstReadWaitMs) {
300
+ const left = firstReadWaitMs - (performance.now() - pending.started);
301
+ if (left <= 0)
302
+ return;
303
+ let timer;
304
+ const limit = new Promise((resolve) => {
305
+ timer = setTimeout(resolve, left);
306
+ });
307
+ await Promise.race([pending.read, limit]);
308
+ clearTimeout(timer);
309
+ }
310
+ function createCatalogueReader({
311
+ messages,
312
+ read,
313
+ retryMs,
314
+ firstReadWaitMs,
315
+ log
316
+ }) {
317
+ const held = new Map;
318
+ const loading = new Map;
319
+ const failedAt = new Map;
320
+ async function load(language, now) {
321
+ const reply = await read(language);
322
+ const entries = Object.create(null);
323
+ for (const { code, message, params } of reply.messages) {
324
+ entries[code] = { message, params };
325
+ }
326
+ messages.set(language, entries);
327
+ held.set(language, { version: reply.version, checkedAt: now });
328
+ }
329
+ function loadOnce(language, now) {
330
+ let pending = loading.get(language);
331
+ if (!pending) {
332
+ const started = performance.now();
333
+ const reading = load(language, now).then(() => {
334
+ failedAt.delete(language);
335
+ }).catch((error) => {
336
+ failedAt.set(language, now + (performance.now() - started));
337
+ log.error(`alxia-react-router: the error catalogue (${language}) was not read`, error);
338
+ }).finally(() => {
339
+ loading.delete(language);
340
+ });
341
+ pending = { read: reading, started };
342
+ loading.set(language, pending);
343
+ }
344
+ return pending;
345
+ }
346
+ return {
347
+ held,
348
+ load,
349
+ async first(language, now) {
350
+ const failed = failedAt.get(language);
351
+ if (failed === undefined || now - failed >= retryMs) {
352
+ await boundedWait(loadOnce(language, now), firstReadWaitMs);
353
+ }
354
+ },
355
+ reset() {
356
+ held.clear();
357
+ loading.clear();
358
+ failedAt.clear();
359
+ }
360
+ };
361
+ }
362
+
363
+ // src/errors/catalogue.ts
364
+ function clientMessages(messages, languages, clientCodes) {
365
+ const byLanguage = {};
366
+ for (const language of languages) {
367
+ const entries = Object.create(null);
368
+ for (const code of clientCodes) {
369
+ const entry = messages.messageOf(language, code);
370
+ if (entry)
371
+ entries[code] = entry;
372
+ }
373
+ byLanguage[language] = entries;
374
+ }
375
+ return byLanguage;
376
+ }
377
+ function createErrorCatalogue({
378
+ messages,
379
+ languages,
380
+ read,
381
+ versions,
382
+ ttlMs,
383
+ retryMs = 5000,
384
+ firstReadWaitMs = 1500,
385
+ clientCodes,
386
+ log: appLog = console
387
+ }) {
388
+ if (languages.length === 0) {
389
+ throw new TypeError("createErrorCatalogue(): needs at least one language.");
390
+ }
391
+ const log = quietLog(appLog, ["error"]);
392
+ const ttl = safeTtl(ttlMs, retryMs, log);
393
+ const reader = createCatalogueReader({
394
+ messages,
395
+ read,
396
+ retryMs,
397
+ firstReadWaitMs,
398
+ log
399
+ });
400
+ const context = { reader, versions, ttl, retryMs, log };
401
+ let checking;
402
+ async function fresh(language, now = Date.now()) {
403
+ const entry = reader.held.get(language);
404
+ if (!entry)
405
+ return reader.first(language, now);
406
+ if (now - entry.checkedAt >= ttl() && !checking) {
407
+ checking = checkVersions(context, now).finally(() => {
408
+ checking = undefined;
409
+ }).catch(() => {});
410
+ }
411
+ }
412
+ const freshAll = async (now = Date.now()) => {
413
+ await Promise.all(languages.map((language) => fresh(language, now)));
414
+ };
415
+ return {
416
+ fresh,
417
+ freshAll,
418
+ warm() {
419
+ freshAll().catch((error) => {
420
+ log.error("alxia-react-router: the error catalogue was not warmed", error);
421
+ });
422
+ },
423
+ clientMessages: () => clientMessages(messages, languages, clientCodes),
424
+ reset() {
425
+ reader.reset();
426
+ checking = undefined;
427
+ messages.clear();
428
+ },
429
+ heldVersion: (language) => reader.held.get(language)?.version,
430
+ settled: () => checking ?? Promise.resolve()
431
+ };
432
+ }
433
+ // src/errors/json-actions.ts
434
+ import { data as data4, redirect } from "react-router";
435
+
436
+ // src/errors/page-path.ts
437
+ function nextOf(url) {
438
+ const { pathname, searchParams } = new URL(url);
439
+ const trimmed = pathname.endsWith("/_.data") ? pathname.slice(0, -"_.data".length) : pathname.replace(/\.data$/, "");
440
+ const path = trimmed.replace(/^\/{2,}/, "/");
441
+ const params = new URLSearchParams(searchParams);
442
+ params.delete("_routes");
443
+ for (const value of params.getAll("index")) {
444
+ if (!value)
445
+ params.delete("index", value);
446
+ }
447
+ const search = params.toString();
448
+ return search ? `${path}?${search}` : path;
449
+ }
450
+
451
+ // src/errors/json-actions.ts
452
+ function jsonActions(options = {}) {
453
+ const ownOrigin = (request) => {
454
+ const { origin } = options;
455
+ const set = typeof origin === "function" ? origin() : origin;
456
+ return set ?? new URL(request.url).origin;
457
+ };
458
+ const isJson = (request) => request.headers.get("content-type")?.startsWith("application/json");
459
+ async function readJson(request) {
460
+ const origin = request.headers.get("origin");
461
+ const site = request.headers.get("sec-fetch-site");
462
+ if (origin && origin !== ownOrigin(request) || site && site !== "same-origin" && site !== "none") {
463
+ throw data4("Cross-site request refused", { status: 403 });
464
+ }
465
+ if (!isJson(request)) {
466
+ throw data4("Expected a JSON body", { status: 415 });
467
+ }
468
+ return request.json().catch(() => {
469
+ throw data4("Malformed JSON body", { status: 400 });
470
+ });
471
+ }
472
+ return {
473
+ readJson,
474
+ requireJsonPost(request) {
475
+ if (!isJson(request))
476
+ throw redirect(nextOf(request.url));
477
+ },
478
+ async readInput(request, schema) {
479
+ const body = await readJson(request);
480
+ const result = await schema["~standard"].validate(body?.input);
481
+ if (result.issues) {
482
+ throw data4("The form sent an input the API would refuse", {
483
+ status: 400
484
+ });
485
+ }
486
+ return result.value;
487
+ }
488
+ };
489
+ }
490
+ // src/errors/message-values.ts
491
+ var DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
492
+ function dayOf(value) {
493
+ const parts = DAY.exec(value);
494
+ if (!parts)
495
+ return;
496
+ return new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]));
497
+ }
498
+ function valuesFor(params, values) {
499
+ const worded = { ...values };
500
+ for (const { name, kind } of params) {
501
+ const value = values[name];
502
+ if (typeof value !== "string")
503
+ continue;
504
+ if (kind === "DATE") {
505
+ const day = dayOf(value);
506
+ if (day)
507
+ worded[name] = day;
508
+ } else if ((kind === "NUMBER" || kind === "PLURAL") && value.trim() !== "" && !Number.isNaN(Number(value))) {
509
+ worded[name] = Number(value);
510
+ }
511
+ }
512
+ return worded;
513
+ }
514
+
515
+ // src/errors/messages.ts
516
+ function createApiMessages({
517
+ format
518
+ }) {
519
+ const catalogues = new Map;
520
+ const messageOf = (language, code) => catalogues.get(language)?.get(code);
521
+ function formatEntry(entry, values, language) {
522
+ const worded = valuesFor(entry.params, values ?? {});
523
+ let failed = false;
524
+ try {
525
+ const text = format(entry.message, language, worded, {
526
+ onError: () => {
527
+ failed = true;
528
+ }
529
+ });
530
+ return failed ? undefined : text;
531
+ } catch {
532
+ return;
533
+ }
534
+ }
535
+ return {
536
+ set(language, messages) {
537
+ catalogues.set(language, new Map(Object.entries(messages)));
538
+ },
539
+ add(language, messages) {
540
+ const held = catalogues.get(language) ?? new Map;
541
+ for (const [code, message] of Object.entries(messages)) {
542
+ held.set(code, message);
543
+ }
544
+ catalogues.set(language, held);
545
+ },
546
+ clear() {
547
+ catalogues.clear();
548
+ },
549
+ messageOf,
550
+ format: formatEntry,
551
+ apiMessage(code, values, language) {
552
+ const entry = messageOf(language, code);
553
+ return entry ? formatEntry(entry, values, language) : undefined;
554
+ }
555
+ };
556
+ }
557
+ // src/errors/provider.ts
558
+ function ApiMessagesProvider({
559
+ messages,
560
+ value,
561
+ children
562
+ }) {
563
+ const inBrowser = typeof globalThis.document !== "undefined";
564
+ if (value && inBrowser) {
565
+ for (const [language, held] of Object.entries(value)) {
566
+ messages.add(language, held);
567
+ }
568
+ }
569
+ return children;
570
+ }
571
+ // src/errors/use-json-fetcher.ts
572
+ import { useFetcher } from "react-router";
573
+ function useJsonFetcher(key, action) {
574
+ const fetcher = useFetcher(key ? { key } : undefined);
575
+ const post = (body, to = action) => fetcher.submit(body, {
576
+ method: "post",
577
+ encType: "application/json",
578
+ ...to ? { action: to } : {}
579
+ });
580
+ return { fetcher, post, busy: fetcher.state !== "idle" };
581
+ }
582
+ export {
583
+ ApiMessagesProvider,
584
+ REQUEST_ID_HEADER,
585
+ apiCallFailure,
586
+ apiCodeResolver,
587
+ createApiErrors,
588
+ createApiMessages,
589
+ createErrorCatalogue,
590
+ errorRequestId,
591
+ isApiErrorLike,
592
+ jsonActions,
593
+ nextOf,
594
+ operationName,
595
+ requestIdOf,
596
+ routeError,
597
+ useJsonFetcher,
598
+ waitMinutes
599
+ };
600
+
601
+ //# debugId=0AF5BB2424365EAA64756E2164756E21
602
+ //# sourceMappingURL=index.js.map