@alxia/react-router 0.9.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.
- package/README.md +301 -13
- package/dist/chunks/index-59vhfj6n.js +10 -0
- package/dist/chunks/index-59vhfj6n.js.map +10 -0
- package/dist/chunks/index-5gqcpyyg.js +34 -0
- package/dist/chunks/index-5gqcpyyg.js.map +10 -0
- package/dist/chunks/{index-zqa30gr7.js → index-9xv2a8et.js} +7 -38
- package/dist/chunks/index-9xv2a8et.js.map +11 -0
- package/dist/chunks/index-mppz0806.js +119 -0
- package/dist/chunks/index-mppz0806.js.map +11 -0
- package/dist/chunks/index-rfr02vg4.js +40 -0
- package/dist/chunks/index-rfr02vg4.js.map +10 -0
- package/dist/chunks/index-zdbqzcjp.js +97 -0
- package/dist/chunks/index-zdbqzcjp.js.map +12 -0
- package/dist/errors/api-error.d.ts +86 -0
- package/dist/errors/api-error.d.ts.map +1 -0
- package/dist/errors/api-errors.d.ts +96 -0
- package/dist/errors/api-errors.d.ts.map +1 -0
- package/dist/errors/call-failure.d.ts +45 -0
- package/dist/errors/call-failure.d.ts.map +1 -0
- package/dist/errors/catalogue-check.d.ts +24 -0
- package/dist/errors/catalogue-check.d.ts.map +1 -0
- package/dist/errors/catalogue-read.d.ts +33 -0
- package/dist/errors/catalogue-read.d.ts.map +1 -0
- package/dist/errors/catalogue.d.ts +59 -0
- package/dist/errors/catalogue.d.ts.map +1 -0
- package/dist/errors/code-resolver.d.ts +44 -0
- package/dist/errors/code-resolver.d.ts.map +1 -0
- package/dist/errors/describe-refusal.d.ts +6 -0
- package/dist/errors/describe-refusal.d.ts.map +1 -0
- package/dist/errors/describe.d.ts +3 -0
- package/dist/errors/describe.d.ts.map +1 -0
- package/dist/errors/fetcher-result.d.ts +21 -0
- package/dist/errors/fetcher-result.d.ts.map +1 -0
- package/dist/errors/form-errors.d.ts +8 -0
- package/dist/errors/form-errors.d.ts.map +1 -0
- package/dist/errors/index.d.ts +15 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +602 -0
- package/dist/errors/index.js.map +29 -0
- package/dist/errors/json-actions.d.ts +38 -0
- package/dist/errors/json-actions.d.ts.map +1 -0
- package/dist/errors/log.d.ts +8 -0
- package/dist/errors/log.d.ts.map +1 -0
- package/dist/errors/message-values.d.ts +11 -0
- package/dist/errors/message-values.d.ts.map +1 -0
- package/dist/errors/messages.d.ts +63 -0
- package/dist/errors/messages.d.ts.map +1 -0
- package/dist/errors/page-path.d.ts +9 -0
- package/dist/errors/page-path.d.ts.map +1 -0
- package/dist/errors/provider.d.ts +20 -0
- package/dist/errors/provider.d.ts.map +1 -0
- package/dist/errors/request-id.d.ts +5 -0
- package/dist/errors/request-id.d.ts.map +1 -0
- package/dist/errors/route-error.d.ts +31 -0
- package/dist/errors/route-error.d.ts.map +1 -0
- package/dist/errors/standard-schema.d.ts +34 -0
- package/dist/errors/standard-schema.d.ts.map +1 -0
- package/dist/errors/submit-form.d.ts +16 -0
- package/dist/errors/submit-form.d.ts.map +1 -0
- package/dist/errors/use-json-fetcher.d.ts +18 -0
- package/dist/errors/use-json-fetcher.d.ts.map +1 -0
- package/dist/pwa/build/build-worker.d.ts +41 -0
- package/dist/pwa/build/build-worker.d.ts.map +1 -0
- package/dist/pwa/build/index.d.ts +9 -0
- package/dist/pwa/build/index.d.ts.map +1 -0
- package/dist/pwa/build/index.js +9 -0
- package/dist/pwa/build/index.js.map +9 -0
- package/dist/pwa/build/precache.d.ts +17 -0
- package/dist/pwa/build/precache.d.ts.map +1 -0
- package/dist/pwa/index.js +34 -134
- package/dist/pwa/index.js.map +4 -8
- package/dist/pwa/react/index.js +1 -1
- package/dist/pwa/worker/index.d.ts +9 -0
- package/dist/pwa/worker/index.d.ts.map +1 -0
- package/dist/pwa/worker/index.js +144 -0
- package/dist/pwa/worker/index.js.map +12 -0
- package/dist/pwa/worker/injected.d.ts +15 -0
- package/dist/pwa/worker/injected.d.ts.map +1 -0
- package/dist/pwa/worker/plan.d.ts +65 -0
- package/dist/pwa/worker/plan.d.ts.map +1 -0
- package/dist/pwa/worker/scope.d.ts +18 -0
- package/dist/pwa/worker/scope.d.ts.map +1 -0
- package/dist/pwa/worker/start-worker.d.ts +17 -0
- package/dist/pwa/worker/start-worker.d.ts.map +1 -0
- package/dist/vite/index.d.ts +10 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +49 -3
- package/dist/vite/index.js.map +6 -5
- package/dist/vite/runtime.d.ts +1 -1
- package/dist/vite/runtime.d.ts.map +1 -1
- package/dist/vite/worker.d.ts +16 -0
- package/dist/vite/worker.d.ts.map +1 -0
- package/docs/README.md +3 -2
- package/docs/guide/errors.md +838 -0
- package/docs/guide/how-it-works.md +7 -1
- package/docs/guide/pwa.md +372 -211
- package/docs/roadmap.md +38 -5
- package/docs/troubleshooting.md +689 -3
- package/package.json +56 -4
- package/dist/chunks/index-zqa30gr7.js.map +0 -12
|
@@ -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
|