@gusnips/react 0.9.14 → 0.9.16
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 +25 -3
- package/dist/describe-error.d.ts +55 -39
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +27 -19
- package/dist/describe-error.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/states.d.ts +5 -0
- package/dist/states.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/describe-error.test.ts +65 -0
- package/src/describe-error.ts +112 -68
- package/src/index.ts +1 -0
- package/src/states.ts +5 -0
package/README.md
CHANGED
|
@@ -284,8 +284,10 @@ operator arriving while `/auth/me` is down gets told the page does not exist: th
|
|
|
284
284
|
retry, and no request id to quote. A guard has three answers — wait, fail, refuse — and only a
|
|
285
285
|
real `false` reaches the refusal.
|
|
286
286
|
|
|
287
|
-
`createRequireAuth` sends an anonymous visitor to `?next=`, carrying path, query and hash.
|
|
288
|
-
|
|
287
|
+
`createRequireAuth` sends an anonymous visitor to `?next=`, carrying path, query and hash. A hash
|
|
288
|
+
that carries a sign-in token (`#access_token=…`, as an OAuth callback does) is left off, so the
|
|
289
|
+
token never lands in the address, the logs or the history. The URL survives reloads, OAuth and
|
|
290
|
+
email links; router state does not. Read it through the main entry's
|
|
289
291
|
validator before navigating yourself:
|
|
290
292
|
|
|
291
293
|
```ts
|
|
@@ -334,6 +336,26 @@ const { cause, fix, recover, reference } = describeError(error);
|
|
|
334
336
|
// recover: "retry" | "signin" | "wait" | "none"
|
|
335
337
|
```
|
|
336
338
|
|
|
339
|
+
Build it once. An app in one language writes its four sentences out:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
const inTime = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
343
|
+
|
|
344
|
+
export const describeError = createErrorDescriber({
|
|
345
|
+
copy: {
|
|
346
|
+
network: "We could not reach the server.",
|
|
347
|
+
networkHint: "Check your connection, then try again.",
|
|
348
|
+
unexpected: "Something went wrong on our side.",
|
|
349
|
+
retrySoon: "Try again in a moment.",
|
|
350
|
+
},
|
|
351
|
+
formatWait: (secs) => inTime.format(secs, "second"),
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
An app that translates passes `t`, `copyPrefix`, `messageKeyPrefix` and `knownMessageKeys`
|
|
356
|
+
instead. Its catalog holds those four under `copyPrefix`, and when the server names a sentence
|
|
357
|
+
the catalog has, the reader gets it in their own language.
|
|
358
|
+
|
|
337
359
|
The field is `fix`, the same word as the prop above. It was `hint` until 0.6.0, which left every
|
|
338
360
|
adopter writing `fix={hint}` at every error surface.
|
|
339
361
|
|
|
@@ -342,7 +364,7 @@ reader sees and the retry that actually happens cannot disagree. Give it the sam
|
|
|
342
364
|
`durableLimitCodes` list you give `queryDefaults`, and a spent quota offers no button instead of
|
|
343
365
|
one that cannot work.
|
|
344
366
|
|
|
345
|
-
|
|
367
|
+
`formatWait` says how a stated wait becomes words:
|
|
346
368
|
|
|
347
369
|
```ts
|
|
348
370
|
formatWait: (secs) => humanizeWait(t, secs, "errors."); // three ICU plural keys
|
package/dist/describe-error.d.ts
CHANGED
|
@@ -123,10 +123,8 @@ export interface ErrorContext {
|
|
|
123
123
|
*/
|
|
124
124
|
recover: RecoveryKind;
|
|
125
125
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
/** Catalog namespace for this module's own copy, e.g. `"errors."`. */
|
|
129
|
-
copyPrefix: Prefix;
|
|
126
|
+
/** What both forms share: everything but where the words come from. */
|
|
127
|
+
interface DescriberRules {
|
|
130
128
|
/**
|
|
131
129
|
* How a stated wait becomes words, for the `wait` an arm reads off its context.
|
|
132
130
|
*
|
|
@@ -137,6 +135,44 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
|
|
|
137
135
|
* `(secs) => formatIn(secs, locale)` — and the catalog you need is the one you can see.
|
|
138
136
|
*/
|
|
139
137
|
formatWait: (secs: number) => string;
|
|
138
|
+
/**
|
|
139
|
+
* Per-code copy. Everything not listed falls through to the default arm.
|
|
140
|
+
*
|
|
141
|
+
* The arms look up the app's own keys, so they close over the app's `t` rather than being
|
|
142
|
+
* handed one — this module has no names for that copy and no business typing it.
|
|
143
|
+
*
|
|
144
|
+
* Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
|
|
145
|
+
* code that does not exist is a compile error at the arm rather than a branch that never runs.
|
|
146
|
+
*/
|
|
147
|
+
codes?: Partial<Record<string, ErrorArm>>;
|
|
148
|
+
/**
|
|
149
|
+
* What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
|
|
150
|
+
* with no envelope at all.
|
|
151
|
+
*
|
|
152
|
+
* The built-in arm hands the reader the server's own `message`, and for one API in the fleet
|
|
153
|
+
* that is right — its message IS the sentence a person should read. For another it is exactly
|
|
154
|
+
* wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
|
|
155
|
+
* ships three languages, so the built-in would put developer English in front of a reader who
|
|
156
|
+
* does not speak it. One default cannot be right for both, and the one baked in here was
|
|
157
|
+
* simply the first adopter's convention, mistaken for a fact.
|
|
158
|
+
*
|
|
159
|
+
* An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
|
|
160
|
+
* with no arm of its own could state its expiry and have that go unsaid.
|
|
161
|
+
*/
|
|
162
|
+
fallback?: ErrorArm;
|
|
163
|
+
/**
|
|
164
|
+
* The codes that mean "this limit does not clear by waiting" — **the same list you hand
|
|
165
|
+
* `queryDefaults`**, because it answers the same question and a second copy is a second
|
|
166
|
+
* chance to disagree with the retry rule. Declare it once in the app and pass it twice.
|
|
167
|
+
*/
|
|
168
|
+
durableLimitCodes?: readonly string[];
|
|
169
|
+
/** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
|
|
170
|
+
maxRetryWaitSecs?: number;
|
|
171
|
+
}
|
|
172
|
+
export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix extends string, ServerName extends string> extends DescriberRules {
|
|
173
|
+
t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
|
|
174
|
+
/** Catalog namespace for this module's own copy, e.g. `"errors."`. */
|
|
175
|
+
copyPrefix: Prefix;
|
|
140
176
|
/**
|
|
141
177
|
* The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
|
|
142
178
|
* ignored — the server names a sentence, it does not get to name any key in the app.
|
|
@@ -176,42 +212,22 @@ export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix exten
|
|
|
176
212
|
* deliberately takes only the params — an arm that needs the error has `ctx` for that.
|
|
177
213
|
*/
|
|
178
214
|
localizeParams?: (params: Record<string, string | number> | undefined) => Record<string, unknown> | undefined;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
* What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
|
|
191
|
-
* with no envelope at all.
|
|
192
|
-
*
|
|
193
|
-
* The built-in arm hands the reader the server's own `message`, and for one API in the fleet
|
|
194
|
-
* that is right — its message IS the sentence a person should read. For another it is exactly
|
|
195
|
-
* wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
|
|
196
|
-
* ships three languages, so the built-in would put developer English in front of a reader who
|
|
197
|
-
* does not speak it. One default cannot be right for both, and the one baked in here was
|
|
198
|
-
* simply the first adopter's convention, mistaken for a fact.
|
|
199
|
-
*
|
|
200
|
-
* An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
|
|
201
|
-
* with no arm of its own could state its expiry and have that go unsaid.
|
|
202
|
-
*/
|
|
203
|
-
fallback?: ErrorArm;
|
|
204
|
-
/**
|
|
205
|
-
* The codes that mean "this limit does not clear by waiting" — **the same list you hand
|
|
206
|
-
* `queryDefaults`**, because it answers the same question and a second copy is a second
|
|
207
|
-
* chance to disagree with the retry rule. Declare it once in the app and pass it twice.
|
|
208
|
-
*/
|
|
209
|
-
durableLimitCodes?: readonly string[];
|
|
210
|
-
/** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
|
|
211
|
-
maxRetryWaitSecs?: number;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* An app in one language writes its four sentences out, and needs no `t`, no prefixes and no
|
|
218
|
+
* catalog.
|
|
219
|
+
*
|
|
220
|
+
* With no catalog, the server cannot name a sentence the app carries, so a refusal with no arm of
|
|
221
|
+
* its own shows the server's `message`. In a one-language product that is the right sentence: the
|
|
222
|
+
* server writes in the same language the app does.
|
|
223
|
+
*/
|
|
224
|
+
export interface OneLanguageDescriberOptions extends DescriberRules {
|
|
225
|
+
copy: Record<CopyName, string>;
|
|
212
226
|
}
|
|
213
227
|
export type ErrorArm = (ctx: ErrorContext) => ErrorCopy;
|
|
214
|
-
/** Build the describer. */
|
|
215
|
-
export declare function createErrorDescriber
|
|
228
|
+
/** Build the describer for an app in one language. */
|
|
229
|
+
export declare function createErrorDescriber(options: OneLanguageDescriberOptions): (error: unknown) => DescribedError;
|
|
230
|
+
/** Build the describer for an app that translates. */
|
|
231
|
+
export declare function createErrorDescriber<Prefix extends string, ServerPrefix extends string, ServerName extends string>(options: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError;
|
|
216
232
|
export {};
|
|
217
233
|
//# sourceMappingURL=describe-error.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,YAAY;AACtB,mDAAmD;AACjD,OAAO;AACT,gEAAgE;GAC9D,QAAQ;AACV,gFAAgF;GAC9E,MAAM;AACR,iFAAiF;GAC/E,MAAM,CAAC;AAEX,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,mFAAmF;IACnF,OAAO,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,IAAI,CACnD,GAAG,EAAE,GAAG,EACR,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC;AAEZ,wFAAwF;AACxF,KAAK,QAAQ,GAAG,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;AAE5D;;;;;;;;GAQG;AACH,KAAK,QAAQ,GAAG,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,CAAC;AAEvE;;;;;;;;;;GAUG;AACH,KAAK,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AACtE,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,GAAG,MAAM,IAAI,IAAI,YAAY,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC;AA4BzF;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,MAAM,EAChD,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,CAMR;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,sFAAsF;IACtF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;;OAKG;IACH,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB,CACpC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM;
|
|
1
|
+
{"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
|
package/dist/describe-error.js
CHANGED
|
@@ -24,8 +24,7 @@ export function humanizeWait(t, secs, prefix) {
|
|
|
24
24
|
return t(`${prefix}waitMinutes`, { count: Math.ceil(secs / 60) });
|
|
25
25
|
return t(`${prefix}waitHours`, { count: Math.ceil(secs / 3600) });
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPrefix, knownMessageKeys, localizeParams = (params) => params, codes = {}, fallback, durableLimitCodes = [], maxRetryWaitSecs, }) {
|
|
27
|
+
function translatedWords({ t, copyPrefix, messageKeyPrefix, knownMessageKeys, localizeParams = (params) => params, }) {
|
|
29
28
|
// Both spellings of every name: the one the catalog stores, and — for a plural — the one the
|
|
30
29
|
// server sends. `Object.keys` and not `in`, which would have accepted "toString".
|
|
31
30
|
const known = new Set();
|
|
@@ -36,19 +35,28 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
|
|
|
36
35
|
// Still sound, and still the reason nothing here needs a cast: the set holds exactly the raw
|
|
37
36
|
// keys and their plural bases, which is exactly what the predicate claims.
|
|
38
37
|
const has = (name) => known.has(name);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
return {
|
|
39
|
+
copyOf: (name) => t(`${copyPrefix}${name}`),
|
|
40
|
+
serverSentence(error) {
|
|
41
|
+
const lookup = (named) => {
|
|
42
|
+
if (named === undefined || !named.startsWith(messageKeyPrefix))
|
|
43
|
+
return null;
|
|
44
|
+
const name = named.slice(messageKeyPrefix.length);
|
|
45
|
+
return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
|
|
46
|
+
};
|
|
47
|
+
// `messageKey` first: an API that sends both means the key, and the code is what the UI
|
|
48
|
+
// switches on. The code is only consulted when there is no key to prefer, and it is spelled
|
|
49
|
+
// under the same prefix so the gate is identical.
|
|
50
|
+
return (lookup(error.messageKey) ??
|
|
51
|
+
(error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`)));
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function createErrorDescriber(options) {
|
|
56
|
+
const { formatWait, codes = {}, fallback, durableLimitCodes = [], maxRetryWaitSecs } = options;
|
|
57
|
+
const { copyOf, serverSentence } = "copy" in options
|
|
58
|
+
? { copyOf: (name) => options.copy[name], serverSentence: () => null }
|
|
59
|
+
: translatedWords(options);
|
|
52
60
|
/**
|
|
53
61
|
* Which control to offer, decided once and never switched on separately.
|
|
54
62
|
*
|
|
@@ -86,8 +94,8 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
|
|
|
86
94
|
*/
|
|
87
95
|
const unmapped = fallback ??
|
|
88
96
|
(({ error, says }) => ({
|
|
89
|
-
cause: says ?? ((error.code === undefined ? "" : error.message) ||
|
|
90
|
-
fix: error.status >= 500 ?
|
|
97
|
+
cause: says ?? ((error.code === undefined ? "" : error.message) || copyOf("unexpected")),
|
|
98
|
+
fix: error.status >= 500 ? copyOf("retrySoon") : undefined,
|
|
91
99
|
}));
|
|
92
100
|
return function describeError(error) {
|
|
93
101
|
if (!(error instanceof ApiError)) {
|
|
@@ -95,10 +103,10 @@ export function createErrorDescriber({ t, copyPrefix, formatWait, messageKeyPref
|
|
|
95
103
|
// never worth showing a stack trace for. `shouldRetry` answers a non-response the same way,
|
|
96
104
|
// so these two agree here without either being told about the other.
|
|
97
105
|
return {
|
|
98
|
-
cause:
|
|
106
|
+
cause: copyOf("network"),
|
|
99
107
|
// The catalog KEY keeps its name while the field becomes `fix`: renaming it would cost
|
|
100
108
|
// every adopter a JSON edit in every language, for a string no caller ever types.
|
|
101
|
-
fix:
|
|
109
|
+
fix: copyOf("networkHint"),
|
|
102
110
|
recover: "retry",
|
|
103
111
|
};
|
|
104
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA2GzC,MAAM,aAAa,GAAG,iCAAiC,CAAC;AA2BxD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAC1B,CAAoC,EACpC,IAAY,EACZ,MAAc;IAEd,+FAA+F;IAC/F,+BAA+B;IAC/B,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,MAAM,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IACzF,IAAI,IAAI,GAAG,EAAE,GAAG,EAAE;QAAE,OAAO,CAAC,CAAC,GAAG,MAAM,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACtF,OAAO,CAAC,CAAC,GAAG,MAAM,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;AACpE,CAAC;
|
|
1
|
+
{"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { createApiClient, type ApiClient, type ApiClientOptions, type RefreshRes
|
|
|
7
7
|
export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
|
|
8
8
|
export { queryDefaults, queryView, retryDelayMs, shouldRetry, type QueryDefaultsOptions, type QueryView, } from "./query.ts";
|
|
9
9
|
export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, } from "./error-boundary.tsx";
|
|
10
|
-
export { createErrorDescriber, humanizeWait, type DescribedError, type ErrorArm, type ErrorContext, type ErrorCopy, type ErrorDescriberOptions, type RecoveryKind, type Translate, } from "./describe-error.ts";
|
|
10
|
+
export { createErrorDescriber, humanizeWait, type DescribedError, type ErrorArm, type ErrorContext, type ErrorCopy, type ErrorDescriberOptions, type OneLanguageDescriberOptions, type RecoveryKind, type Translate, } from "./describe-error.ts";
|
|
11
11
|
export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, isStaleBuild, RELOAD_GUARD_KEY, reloadOnce, } from "./deploy-recovery.ts";
|
|
12
12
|
export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
|
|
13
13
|
export { applyBrandVars, i18nInitOptions, type I18nInitOptions } from "./i18n.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,4FAA4F;AAC5F,8FAA8F;AAC9F,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAoC,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,GAGZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,4FAA4F;AAC5F,8FAA8F;AAC9F,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAoC,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,GAGZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GASb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
|
package/dist/states.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface EmptyStateProps {
|
|
|
33
33
|
*
|
|
34
34
|
* Only one donor had this, and it is the difference between a screen reader announcing a
|
|
35
35
|
* page and announcing nothing.
|
|
36
|
+
*
|
|
37
|
+
* **The type cannot make your panel read it.** A panel that always renders an `<h3>` accepts
|
|
38
|
+
* `headingLevel={1}` and ignores it, and the page ships with no `h1`. The guide's own example
|
|
39
|
+
* app did exactly that on its 404 until a browser check found it. Render the heading from the
|
|
40
|
+
* prop: `` const Heading = `h${headingLevel}` as const ``.
|
|
36
41
|
*/
|
|
37
42
|
headingLevel?: 1 | 2 | 3;
|
|
38
43
|
className?: string;
|
package/dist/states.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"states.d.ts","sourceRoot":"","sources":["../src/states.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,WAAW,EAAE,SAAS,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB
|
|
1
|
+
{"version":3,"file":"states.d.ts","sourceRoot":"","sources":["../src/states.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,WAAW,EAAE,SAAS,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,iCAAiC;IACjC,GAAG,EAAE,SAAS,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,SAAS,CAAC;IAClB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.16",
|
|
4
4
|
"description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, stale-tab recovery, i18n bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"release:major": "bun pm version major && bun publish --access public"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@gusnips/http": "0.1.
|
|
83
|
+
"@gusnips/http": "0.1.3",
|
|
84
84
|
"clsx": "^2.1.1",
|
|
85
85
|
"tailwind-merge": "^3.6.0"
|
|
86
86
|
},
|
|
@@ -386,3 +386,68 @@ describe("createErrorDescriber, when the code names the sentence", () => {
|
|
|
386
386
|
expect(describeError(error).cause).toBe("errors.network");
|
|
387
387
|
});
|
|
388
388
|
});
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The form for an app in one language. It had to pass `copyPrefix`, `messageKeyPrefix` and an
|
|
392
|
+
* empty `knownMessageKeys` to say "I have no catalog", and typing `t`'s argument by hand broke the
|
|
393
|
+
* inference. It writes its four sentences out instead.
|
|
394
|
+
*/
|
|
395
|
+
describe("one language", () => {
|
|
396
|
+
const COPY = {
|
|
397
|
+
network: "We could not reach the server.",
|
|
398
|
+
networkHint: "Check your connection, then try again.",
|
|
399
|
+
unexpected: "Something went wrong on our side.",
|
|
400
|
+
retrySoon: "Try again in a moment.",
|
|
401
|
+
};
|
|
402
|
+
const describeError = createErrorDescriber({
|
|
403
|
+
copy: COPY,
|
|
404
|
+
formatWait: (secs) => `in ${secs}s`,
|
|
405
|
+
codes: {
|
|
406
|
+
RATE_LIMIT_EXCEEDED: ({ wait }) => ({ cause: "Too many at once.", fix: `Try ${wait}.` }),
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("takes its own sentences from `copy`", () => {
|
|
411
|
+
expect(describeError(new TypeError("Failed to fetch"))).toEqual({
|
|
412
|
+
cause: COPY.network,
|
|
413
|
+
fix: COPY.networkHint,
|
|
414
|
+
recover: "retry",
|
|
415
|
+
});
|
|
416
|
+
expect(describeError(new ApiError(502, null))).toMatchObject({
|
|
417
|
+
cause: COPY.unexpected,
|
|
418
|
+
fix: COPY.retrySoon,
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// No catalog, so nothing the server names can reach the screen as a key. The server writes in
|
|
423
|
+
// the app's one language, so its own sentence is what an unmapped refusal shows.
|
|
424
|
+
it("shows the server's sentence and never a key the server named", () => {
|
|
425
|
+
const error = new ApiError(409, {
|
|
426
|
+
code: "SLUG_TAKEN",
|
|
427
|
+
message: "That address is taken.",
|
|
428
|
+
messageKey: "serverErrors.slugTaken",
|
|
429
|
+
});
|
|
430
|
+
expect(describeError(error).cause).toBe("That address is taken.");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("still runs the arms and the stated wait", () => {
|
|
434
|
+
const error = new ApiError(
|
|
435
|
+
429,
|
|
436
|
+
{ code: "RATE_LIMIT_EXCEEDED", message: "slow down" },
|
|
437
|
+
{ retryAfterSecs: 30 },
|
|
438
|
+
);
|
|
439
|
+
expect(describeError(error)).toMatchObject({
|
|
440
|
+
cause: "Too many at once.",
|
|
441
|
+
fix: "Try in 30s.",
|
|
442
|
+
recover: "wait",
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("refuses `copy` with a sentence missing", () => {
|
|
447
|
+
// @ts-expect-error -- `retrySoon` is missing, and a 5xx would show `undefined` without it.
|
|
448
|
+
createErrorDescriber({
|
|
449
|
+
copy: { network: "", networkHint: "", unexpected: "" },
|
|
450
|
+
formatWait: String,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
});
|
package/src/describe-error.ts
CHANGED
|
@@ -175,14 +175,8 @@ export interface ErrorContext {
|
|
|
175
175
|
recover: RecoveryKind;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
ServerPrefix extends string,
|
|
181
|
-
ServerName extends string,
|
|
182
|
-
> {
|
|
183
|
-
t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
|
|
184
|
-
/** Catalog namespace for this module's own copy, e.g. `"errors."`. */
|
|
185
|
-
copyPrefix: Prefix;
|
|
178
|
+
/** What both forms share: everything but where the words come from. */
|
|
179
|
+
interface DescriberRules {
|
|
186
180
|
/**
|
|
187
181
|
* How a stated wait becomes words, for the `wait` an arm reads off its context.
|
|
188
182
|
*
|
|
@@ -193,6 +187,49 @@ export interface ErrorDescriberOptions<
|
|
|
193
187
|
* `(secs) => formatIn(secs, locale)` — and the catalog you need is the one you can see.
|
|
194
188
|
*/
|
|
195
189
|
formatWait: (secs: number) => string;
|
|
190
|
+
/**
|
|
191
|
+
* Per-code copy. Everything not listed falls through to the default arm.
|
|
192
|
+
*
|
|
193
|
+
* The arms look up the app's own keys, so they close over the app's `t` rather than being
|
|
194
|
+
* handed one — this module has no names for that copy and no business typing it.
|
|
195
|
+
*
|
|
196
|
+
* Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
|
|
197
|
+
* code that does not exist is a compile error at the arm rather than a branch that never runs.
|
|
198
|
+
*/
|
|
199
|
+
codes?: Partial<Record<string, ErrorArm>>;
|
|
200
|
+
/**
|
|
201
|
+
* What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
|
|
202
|
+
* with no envelope at all.
|
|
203
|
+
*
|
|
204
|
+
* The built-in arm hands the reader the server's own `message`, and for one API in the fleet
|
|
205
|
+
* that is right — its message IS the sentence a person should read. For another it is exactly
|
|
206
|
+
* wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
|
|
207
|
+
* ships three languages, so the built-in would put developer English in front of a reader who
|
|
208
|
+
* does not speak it. One default cannot be right for both, and the one baked in here was
|
|
209
|
+
* simply the first adopter's convention, mistaken for a fact.
|
|
210
|
+
*
|
|
211
|
+
* An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
|
|
212
|
+
* with no arm of its own could state its expiry and have that go unsaid.
|
|
213
|
+
*/
|
|
214
|
+
fallback?: ErrorArm;
|
|
215
|
+
/**
|
|
216
|
+
* The codes that mean "this limit does not clear by waiting" — **the same list you hand
|
|
217
|
+
* `queryDefaults`**, because it answers the same question and a second copy is a second
|
|
218
|
+
* chance to disagree with the retry rule. Declare it once in the app and pass it twice.
|
|
219
|
+
*/
|
|
220
|
+
durableLimitCodes?: readonly string[];
|
|
221
|
+
/** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
|
|
222
|
+
maxRetryWaitSecs?: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface ErrorDescriberOptions<
|
|
226
|
+
Prefix extends string,
|
|
227
|
+
ServerPrefix extends string,
|
|
228
|
+
ServerName extends string,
|
|
229
|
+
> extends DescriberRules {
|
|
230
|
+
t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>;
|
|
231
|
+
/** Catalog namespace for this module's own copy, e.g. `"errors."`. */
|
|
232
|
+
copyPrefix: Prefix;
|
|
196
233
|
/**
|
|
197
234
|
* The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
|
|
198
235
|
* ignored — the server names a sentence, it does not get to name any key in the app.
|
|
@@ -234,60 +271,39 @@ export interface ErrorDescriberOptions<
|
|
|
234
271
|
localizeParams?: (
|
|
235
272
|
params: Record<string, string | number> | undefined,
|
|
236
273
|
) => Record<string, unknown> | undefined;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
* with no envelope at all.
|
|
250
|
-
*
|
|
251
|
-
* The built-in arm hands the reader the server's own `message`, and for one API in the fleet
|
|
252
|
-
* that is right — its message IS the sentence a person should read. For another it is exactly
|
|
253
|
-
* wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
|
|
254
|
-
* ships three languages, so the built-in would put developer English in front of a reader who
|
|
255
|
-
* does not speak it. One default cannot be right for both, and the one baked in here was
|
|
256
|
-
* simply the first adopter's convention, mistaken for a fact.
|
|
257
|
-
*
|
|
258
|
-
* An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
|
|
259
|
-
* with no arm of its own could state its expiry and have that go unsaid.
|
|
260
|
-
*/
|
|
261
|
-
fallback?: ErrorArm;
|
|
262
|
-
/**
|
|
263
|
-
* The codes that mean "this limit does not clear by waiting" — **the same list you hand
|
|
264
|
-
* `queryDefaults`**, because it answers the same question and a second copy is a second
|
|
265
|
-
* chance to disagree with the retry rule. Declare it once in the app and pass it twice.
|
|
266
|
-
*/
|
|
267
|
-
durableLimitCodes?: readonly string[];
|
|
268
|
-
/** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
|
|
269
|
-
maxRetryWaitSecs?: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* An app in one language writes its four sentences out, and needs no `t`, no prefixes and no
|
|
278
|
+
* catalog.
|
|
279
|
+
*
|
|
280
|
+
* With no catalog, the server cannot name a sentence the app carries, so a refusal with no arm of
|
|
281
|
+
* its own shows the server's `message`. In a one-language product that is the right sentence: the
|
|
282
|
+
* server writes in the same language the app does.
|
|
283
|
+
*/
|
|
284
|
+
export interface OneLanguageDescriberOptions extends DescriberRules {
|
|
285
|
+
copy: Record<CopyName, string>;
|
|
270
286
|
}
|
|
271
287
|
|
|
272
288
|
export type ErrorArm = (ctx: ErrorContext) => ErrorCopy;
|
|
273
289
|
|
|
274
|
-
/**
|
|
275
|
-
|
|
290
|
+
/** Where the describer's words come from: its own four, and the server's when it named one. */
|
|
291
|
+
interface Words {
|
|
292
|
+
copyOf: (name: CopyName) => string;
|
|
293
|
+
serverSentence: (error: ApiError) => string | null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function translatedWords<
|
|
276
297
|
Prefix extends string,
|
|
277
298
|
ServerPrefix extends string,
|
|
278
299
|
ServerName extends string,
|
|
279
300
|
>({
|
|
280
301
|
t,
|
|
281
302
|
copyPrefix,
|
|
282
|
-
formatWait,
|
|
283
303
|
messageKeyPrefix,
|
|
284
304
|
knownMessageKeys,
|
|
285
305
|
localizeParams = (params) => params,
|
|
286
|
-
|
|
287
|
-
fallback,
|
|
288
|
-
durableLimitCodes = [],
|
|
289
|
-
maxRetryWaitSecs,
|
|
290
|
-
}: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError {
|
|
306
|
+
}: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): Words {
|
|
291
307
|
// Both spellings of every name: the one the catalog stores, and — for a plural — the one the
|
|
292
308
|
// server sends. `Object.keys` and not `in`, which would have accepted "toString".
|
|
293
309
|
const known = new Set<string>();
|
|
@@ -300,20 +316,49 @@ export function createErrorDescriber<
|
|
|
300
316
|
// keys and their plural bases, which is exactly what the predicate claims.
|
|
301
317
|
const has = (name: string): name is ServerName | PluralBase<ServerName> => known.has(name);
|
|
302
318
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
319
|
+
return {
|
|
320
|
+
copyOf: (name) => t(`${copyPrefix}${name}`),
|
|
321
|
+
serverSentence(error) {
|
|
322
|
+
const lookup = (named: string | undefined): string | null => {
|
|
323
|
+
if (named === undefined || !named.startsWith(messageKeyPrefix)) return null;
|
|
324
|
+
const name = named.slice(messageKeyPrefix.length);
|
|
325
|
+
return has(name) ? t(`${messageKeyPrefix}${name}`, localizeParams(error.params)) : null;
|
|
326
|
+
};
|
|
327
|
+
// `messageKey` first: an API that sends both means the key, and the code is what the UI
|
|
328
|
+
// switches on. The code is only consulted when there is no key to prefer, and it is spelled
|
|
329
|
+
// under the same prefix so the gate is identical.
|
|
330
|
+
return (
|
|
331
|
+
lookup(error.messageKey) ??
|
|
332
|
+
(error.code === undefined ? null : lookup(`${messageKeyPrefix}${error.code}`))
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Build the describer for an app in one language. */
|
|
339
|
+
export function createErrorDescriber(
|
|
340
|
+
options: OneLanguageDescriberOptions,
|
|
341
|
+
): (error: unknown) => DescribedError;
|
|
342
|
+
/** Build the describer for an app that translates. */
|
|
343
|
+
export function createErrorDescriber<
|
|
344
|
+
Prefix extends string,
|
|
345
|
+
ServerPrefix extends string,
|
|
346
|
+
ServerName extends string,
|
|
347
|
+
>(
|
|
348
|
+
options: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>,
|
|
349
|
+
): (error: unknown) => DescribedError;
|
|
350
|
+
export function createErrorDescriber<
|
|
351
|
+
Prefix extends string,
|
|
352
|
+
ServerPrefix extends string,
|
|
353
|
+
ServerName extends string,
|
|
354
|
+
>(
|
|
355
|
+
options: OneLanguageDescriberOptions | ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>,
|
|
356
|
+
): (error: unknown) => DescribedError {
|
|
357
|
+
const { formatWait, codes = {}, fallback, durableLimitCodes = [], maxRetryWaitSecs } = options;
|
|
358
|
+
const { copyOf, serverSentence }: Words =
|
|
359
|
+
"copy" in options
|
|
360
|
+
? { copyOf: (name) => options.copy[name], serverSentence: () => null }
|
|
361
|
+
: translatedWords(options);
|
|
317
362
|
|
|
318
363
|
/**
|
|
319
364
|
* Which control to offer, decided once and never switched on separately.
|
|
@@ -351,9 +396,8 @@ export function createErrorDescriber<
|
|
|
351
396
|
const unmapped: ErrorArm =
|
|
352
397
|
fallback ??
|
|
353
398
|
(({ error, says }) => ({
|
|
354
|
-
cause:
|
|
355
|
-
|
|
356
|
-
fix: error.status >= 500 ? t(`${copyPrefix}retrySoon`) : undefined,
|
|
399
|
+
cause: says ?? ((error.code === undefined ? "" : error.message) || copyOf("unexpected")),
|
|
400
|
+
fix: error.status >= 500 ? copyOf("retrySoon") : undefined,
|
|
357
401
|
}));
|
|
358
402
|
|
|
359
403
|
return function describeError(error: unknown): DescribedError {
|
|
@@ -362,10 +406,10 @@ export function createErrorDescriber<
|
|
|
362
406
|
// never worth showing a stack trace for. `shouldRetry` answers a non-response the same way,
|
|
363
407
|
// so these two agree here without either being told about the other.
|
|
364
408
|
return {
|
|
365
|
-
cause:
|
|
409
|
+
cause: copyOf("network"),
|
|
366
410
|
// The catalog KEY keeps its name while the field becomes `fix`: renaming it would cost
|
|
367
411
|
// every adopter a JSON edit in every language, for a string no caller ever types.
|
|
368
|
-
fix:
|
|
412
|
+
fix: copyOf("networkHint"),
|
|
369
413
|
recover: "retry",
|
|
370
414
|
};
|
|
371
415
|
}
|
package/src/index.ts
CHANGED
package/src/states.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface EmptyStateProps {
|
|
|
35
35
|
*
|
|
36
36
|
* Only one donor had this, and it is the difference between a screen reader announcing a
|
|
37
37
|
* page and announcing nothing.
|
|
38
|
+
*
|
|
39
|
+
* **The type cannot make your panel read it.** A panel that always renders an `<h3>` accepts
|
|
40
|
+
* `headingLevel={1}` and ignores it, and the page ships with no `h1`. The guide's own example
|
|
41
|
+
* app did exactly that on its 404 until a browser check found it. Render the heading from the
|
|
42
|
+
* prop: `` const Heading = `h${headingLevel}` as const ``.
|
|
38
43
|
*/
|
|
39
44
|
headingLevel?: 1 | 2 | 3;
|
|
40
45
|
className?: string;
|