@gusnips/react 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/describe-error.d.ts +52 -31
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +16 -21
- 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/package.json +1 -1
- package/src/describe-error.test.ts +3 -1
- package/src/describe-error.ts +113 -49
- package/src/index.ts +1 -0
package/dist/describe-error.d.ts
CHANGED
|
@@ -23,8 +23,21 @@ export interface DescribedError {
|
|
|
23
23
|
/** What to do about it. Its absence should be a decision, not an oversight. */
|
|
24
24
|
hint?: string;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The subset of i18next's `t` this module needs — typed here so `i18next` stays optional.
|
|
28
|
+
*
|
|
29
|
+
* `Key` is a parameter and not plain `string` for a reason the first migration found. Every app
|
|
30
|
+
* on this stack declares `CustomTypeOptions.resources`, which types `t` to accept ONLY the keys
|
|
31
|
+
* its catalog has — so a `t` is not assignable to anything asking for `(key: string) => string`,
|
|
32
|
+
* and the adopter is left with a cast. Naming the keys instead turns that around: each function
|
|
33
|
+
* below asks for exactly the keys it looks up, a typed `t` accepts a superset of them and goes
|
|
34
|
+
* in directly, and a catalog missing one is a compile error rather than a raw key on screen.
|
|
35
|
+
*/
|
|
36
|
+
export type Translate<Key extends string = string> = (key: Key, params?: Record<string, unknown>) => string;
|
|
37
|
+
/** The three names {@link humanizeWait} looks up, under whatever prefix you pass it. */
|
|
38
|
+
type WaitName = "waitSeconds" | "waitMinutes" | "waitHours";
|
|
39
|
+
/** Every name {@link createErrorDescriber} looks up itself. Your catalog carries all of them. */
|
|
40
|
+
type CopyName = WaitName | "network" | "networkHint" | "unexpected" | "retrySoon";
|
|
28
41
|
/** Seconds until a refusal clears, when the envelope carries them. */
|
|
29
42
|
export declare function retryAfterSecs(error: ApiError): number | null;
|
|
30
43
|
/**
|
|
@@ -32,31 +45,10 @@ export declare function retryAfterSecs(error: ApiError): number | null;
|
|
|
32
45
|
*
|
|
33
46
|
* The thresholds are deliberately not round: 90 seconds rather than 60, so "in 75 seconds" does
|
|
34
47
|
* not become the less precise "in 1 minute", and 90 minutes rather than 60 for the same reason
|
|
35
|
-
* one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours`
|
|
48
|
+
* one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours` under `prefix`, each with a
|
|
36
49
|
* `count` plural.
|
|
37
50
|
*/
|
|
38
|
-
export declare function humanizeWait(t: Translate
|
|
39
|
-
export interface ErrorDescriberOptions<Code extends string> {
|
|
40
|
-
t: Translate;
|
|
41
|
-
/**
|
|
42
|
-
* The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
|
|
43
|
-
* ignored — the server names a sentence, it does not get to name any key in the app.
|
|
44
|
-
*/
|
|
45
|
-
messageKeyPrefix?: string;
|
|
46
|
-
/**
|
|
47
|
-
* The keys this build actually carries, usually `Object.keys(en.serverErrors)`.
|
|
48
|
-
*
|
|
49
|
-
* Membership is checked with `hasOwnProperty` and not `in`, which would accept `"toString"`.
|
|
50
|
-
* A key that is absent falls back to the server's English `message`, because a deploy can land
|
|
51
|
-
* ahead of the bundle a tab is still running and a missing translation must degrade to
|
|
52
|
-
* readable rather than render the key itself at somebody.
|
|
53
|
-
*/
|
|
54
|
-
knownMessageKeys?: readonly string[] | Record<string, unknown>;
|
|
55
|
-
/** Catalog namespace for this module's own copy. Defaults to `"errors."`. */
|
|
56
|
-
copyPrefix?: string;
|
|
57
|
-
/** Per-code copy. Everything not listed falls through to the default arm below. */
|
|
58
|
-
codes?: Partial<Record<Code, (ctx: ErrorContext) => DescribedError>>;
|
|
59
|
-
}
|
|
51
|
+
export declare function humanizeWait<Prefix extends string>(t: Translate<`${Prefix}${WaitName}`>, secs: number, prefix: Prefix): string;
|
|
60
52
|
export interface ErrorContext {
|
|
61
53
|
error: ApiError;
|
|
62
54
|
/** The server's own localized sentence, when `messageKey` named one this build carries. */
|
|
@@ -64,11 +56,40 @@ export interface ErrorContext {
|
|
|
64
56
|
/** Seconds until it clears, already humanized. Null when the envelope did not say. */
|
|
65
57
|
wait: string | null;
|
|
66
58
|
waitSecs: number | null;
|
|
67
|
-
t: Translate;
|
|
68
59
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
export interface ErrorDescriberOptions<Prefix extends string, ServerPrefix extends string, ServerName extends string> {
|
|
61
|
+
t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName}`>;
|
|
62
|
+
/** Catalog namespace for this module's own copy, e.g. `"errors."`. */
|
|
63
|
+
copyPrefix: Prefix;
|
|
64
|
+
/**
|
|
65
|
+
* The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
|
|
66
|
+
* ignored — the server names a sentence, it does not get to name any key in the app.
|
|
67
|
+
*/
|
|
68
|
+
messageKeyPrefix: ServerPrefix;
|
|
69
|
+
/**
|
|
70
|
+
* The keys this build actually carries, usually the `serverErrors` object out of the English
|
|
71
|
+
* catalog. Passing the catalog itself is both the membership test and the proof the compiler
|
|
72
|
+
* wants: a name that is in it IS a key, so the lookup below needs no cast.
|
|
73
|
+
*
|
|
74
|
+
* It is required because leaving it out is not a smaller version of this — it is the bug the
|
|
75
|
+
* option exists to prevent. i18next answers a key it does not have with the key itself, so a
|
|
76
|
+
* deploy landing ahead of the bundle a tab is still running would render `serverErrors.foo` at
|
|
77
|
+
* somebody. A name that is absent falls back to the server's English `message` instead.
|
|
78
|
+
*/
|
|
79
|
+
knownMessageKeys: Record<ServerName, unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* Per-code copy. Everything not listed falls through to the default arm.
|
|
82
|
+
*
|
|
83
|
+
* The arms look up the app's own keys, so they close over the app's `t` rather than being
|
|
84
|
+
* handed one — this module has no names for that copy and no business typing it.
|
|
85
|
+
*
|
|
86
|
+
* Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
|
|
87
|
+
* code that does not exist is a compile error at the arm rather than a branch that never runs.
|
|
88
|
+
*/
|
|
89
|
+
codes?: Partial<Record<string, ErrorArm>>;
|
|
90
|
+
}
|
|
91
|
+
export type ErrorArm = (ctx: ErrorContext) => DescribedError;
|
|
92
|
+
/** Build the describer. */
|
|
93
|
+
export declare function createErrorDescriber<Prefix extends string, ServerPrefix extends string, ServerName extends string>({ t, copyPrefix, messageKeyPrefix, knownMessageKeys, codes, }: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError;
|
|
94
|
+
export {};
|
|
74
95
|
//# sourceMappingURL=describe-error.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED
|
|
1
|
+
{"version":3,"file":"describe-error.d.ts","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;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,iGAAiG;AACjG,KAAK,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,CAAC;AA8BlF,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAK7D;AAED;;;;;;;GAOG;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,CAIR;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;CACzB;AAED,MAAM,WAAW,qBAAqB,CACpC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM;IAEzB,CAAC,EAAE,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,GAAG,GAAG,YAAY,GAAG,UAAU,EAAE,CAAC,CAAC;IACtE,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,gBAAgB,EAAE,YAAY,CAAC;IAC/B;;;;;;;;;OASG;IACH,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC9C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,cAAc,CAAC;AAE7D,2BAA2B;AAC3B,wBAAgB,oBAAoB,CAClC,MAAM,SAAS,MAAM,EACrB,YAAY,SAAS,MAAM,EAC3B,UAAU,SAAS,MAAM,EACzB,EACA,CAAC,EACD,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,KAAU,GACX,EAAE,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CA0C9F"}
|
package/dist/describe-error.js
CHANGED
|
@@ -12,39 +12,35 @@ export function retryAfterSecs(error) {
|
|
|
12
12
|
*
|
|
13
13
|
* The thresholds are deliberately not round: 90 seconds rather than 60, so "in 75 seconds" does
|
|
14
14
|
* not become the less precise "in 1 minute", and 90 minutes rather than 60 for the same reason
|
|
15
|
-
* one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours`
|
|
15
|
+
* one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours` under `prefix`, each with a
|
|
16
16
|
* `count` plural.
|
|
17
17
|
*/
|
|
18
|
-
export function humanizeWait(t, secs, prefix
|
|
19
|
-
const key = (name) => `${prefix}${name}`;
|
|
18
|
+
export function humanizeWait(t, secs, prefix) {
|
|
20
19
|
if (secs < 90)
|
|
21
|
-
return t(
|
|
20
|
+
return t(`${prefix}waitSeconds`, { count: Math.max(1, Math.round(secs)) });
|
|
22
21
|
if (secs < 90 * 60)
|
|
23
|
-
return t(
|
|
24
|
-
return t(
|
|
22
|
+
return t(`${prefix}waitMinutes`, { count: Math.round(secs / 60) });
|
|
23
|
+
return t(`${prefix}waitHours`, { count: Math.round(secs / 3600) });
|
|
25
24
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
: new Set(Object.keys(knownMessageKeys ?? {}));
|
|
34
|
-
const has = (name) => known.size === 0 || known.has(name);
|
|
35
|
-
const key = (name) => `${copyPrefix}${name}`;
|
|
25
|
+
/** Build the describer. */
|
|
26
|
+
export function createErrorDescriber({ t, copyPrefix, messageKeyPrefix, knownMessageKeys, codes = {}, }) {
|
|
27
|
+
const known = new Set(Object.keys(knownMessageKeys));
|
|
28
|
+
// Sound, and the reason nothing here needs a cast: the set was built from the keys of
|
|
29
|
+
// `knownMessageKeys`, so membership really does establish the claim. `Object.keys` and not
|
|
30
|
+
// `in`, which would have accepted "toString".
|
|
31
|
+
const has = (name) => known.has(name);
|
|
36
32
|
function serverSentence(error) {
|
|
37
33
|
const messageKey = error.messageKey;
|
|
38
34
|
if (messageKey === undefined || !messageKey.startsWith(messageKeyPrefix))
|
|
39
35
|
return null;
|
|
40
36
|
const name = messageKey.slice(messageKeyPrefix.length);
|
|
41
|
-
return has(name) ? t(
|
|
37
|
+
return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
|
|
42
38
|
}
|
|
43
39
|
return function describeError(error) {
|
|
44
40
|
if (!(error instanceof ApiError)) {
|
|
45
41
|
// Not a response at all — the request never landed. Almost always the network, and almost
|
|
46
42
|
// never worth showing a stack trace for.
|
|
47
|
-
return { cause: t(
|
|
43
|
+
return { cause: t(`${copyPrefix}network`), hint: t(`${copyPrefix}networkHint`) };
|
|
48
44
|
}
|
|
49
45
|
const waitSecs = retryAfterSecs(error);
|
|
50
46
|
const ctx = {
|
|
@@ -52,7 +48,6 @@ export function createErrorDescriber({ t, messageKeyPrefix = "serverErrors.", kn
|
|
|
52
48
|
says: serverSentence(error),
|
|
53
49
|
wait: waitSecs === null ? null : humanizeWait(t, waitSecs, copyPrefix),
|
|
54
50
|
waitSecs,
|
|
55
|
-
t,
|
|
56
51
|
};
|
|
57
52
|
const arm = error.code === undefined ? undefined : codes[error.code];
|
|
58
53
|
if (arm)
|
|
@@ -62,8 +57,8 @@ export function createErrorDescriber({ t, messageKeyPrefix = "serverErrors.", kn
|
|
|
62
57
|
// genuinely does clear on its own — a 4xx does not, and saying so would be a lie that costs
|
|
63
58
|
// the reader another attempt.
|
|
64
59
|
return {
|
|
65
|
-
cause: ctx.says ?? (error.message || t(
|
|
66
|
-
hint: error.status >= 500 ? t(
|
|
60
|
+
cause: ctx.says ?? (error.message || t(`${copyPrefix}unexpected`)),
|
|
61
|
+
hint: error.status >= 500 ? t(`${copyPrefix}retrySoon`) : undefined,
|
|
67
62
|
};
|
|
68
63
|
};
|
|
69
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,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
|
@@ -4,7 +4,7 @@ export { createApiClient, type ApiClient, type ApiClientOptions, type RefreshRes
|
|
|
4
4
|
export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
|
|
5
5
|
export { queryDefaults, shouldRetry, type QueryDefaultsOptions } from "./query.ts";
|
|
6
6
|
export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, } from "./error-boundary.tsx";
|
|
7
|
-
export { createErrorDescriber, humanizeWait, retryAfterSecs, type DescribedError, type ErrorContext, type ErrorDescriberOptions, type Translate, } from "./describe-error.ts";
|
|
7
|
+
export { createErrorDescriber, humanizeWait, retryAfterSecs, type DescribedError, type ErrorArm, type ErrorContext, type ErrorDescriberOptions, type Translate, } from "./describe-error.ts";
|
|
8
8
|
export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, reloadOnceForChunkError, } from "./chunk-reload.ts";
|
|
9
9
|
export { hydrateOrMount, PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./hydrate.ts";
|
|
10
10
|
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":"AAqBA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,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,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,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":"AAqBA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,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,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,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,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,EAAE;AACF,iGAAiG;AACjG,6FAA6F;AAC7F,iGAAiG;AACjG,+FAA+F;AAC/F,0FAA0F;AAC1F,qFAAqF;AAErF,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,
|
|
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,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,EAAE;AACF,iGAAiG;AACjG,6FAA6F;AAC7F,iGAAiG;AACjG,+FAA+F;AAC/F,0FAA0F;AAC1F,qFAAqF;AAErF,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,GAMf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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, chunk-reload recovery, i18n bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,9 @@ describe("retryAfterSecs", () => {
|
|
|
42
42
|
describe("createErrorDescriber", () => {
|
|
43
43
|
const describeError = createErrorDescriber({
|
|
44
44
|
t,
|
|
45
|
-
|
|
45
|
+
copyPrefix: "errors.",
|
|
46
|
+
messageKeyPrefix: "serverErrors.",
|
|
47
|
+
knownMessageKeys: { quotaDay: "", suspended: "" },
|
|
46
48
|
codes: {
|
|
47
49
|
QUOTA_EXCEEDED: ({ says, wait }) => ({
|
|
48
50
|
cause: says ?? "errors.quota",
|
package/src/describe-error.ts
CHANGED
|
@@ -26,8 +26,54 @@ export interface DescribedError {
|
|
|
26
26
|
hint?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The subset of i18next's `t` this module needs — typed here so `i18next` stays optional.
|
|
31
|
+
*
|
|
32
|
+
* `Key` is a parameter and not plain `string` for a reason the first migration found. Every app
|
|
33
|
+
* on this stack declares `CustomTypeOptions.resources`, which types `t` to accept ONLY the keys
|
|
34
|
+
* its catalog has — so a `t` is not assignable to anything asking for `(key: string) => string`,
|
|
35
|
+
* and the adopter is left with a cast. Naming the keys instead turns that around: each function
|
|
36
|
+
* below asks for exactly the keys it looks up, a typed `t` accepts a superset of them and goes
|
|
37
|
+
* in directly, and a catalog missing one is a compile error rather than a raw key on screen.
|
|
38
|
+
*/
|
|
39
|
+
export type Translate<Key extends string = string> = (
|
|
40
|
+
key: Key,
|
|
41
|
+
params?: Record<string, unknown>,
|
|
42
|
+
) => string;
|
|
43
|
+
|
|
44
|
+
/** The three names {@link humanizeWait} looks up, under whatever prefix you pass it. */
|
|
45
|
+
type WaitName = "waitSeconds" | "waitMinutes" | "waitHours";
|
|
46
|
+
|
|
47
|
+
/** Every name {@link createErrorDescriber} looks up itself. Your catalog carries all of them. */
|
|
48
|
+
type CopyName = WaitName | "network" | "networkHint" | "unexpected" | "retrySoon";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The paragraph above, pinned so it cannot quietly stop being true.
|
|
52
|
+
*
|
|
53
|
+
* `ClosedCatalog` is what an app's `t` looks like once it declares `CustomTypeOptions.resources`:
|
|
54
|
+
* a closed union, nothing else accepted. It must go straight into the options below. Widen `Key`
|
|
55
|
+
* back to `string` and this stops compiling HERE, rather than at the eleventh adopter reaching
|
|
56
|
+
* for the cast the house rules forbid.
|
|
57
|
+
*/
|
|
58
|
+
type Satisfied<T extends true> = T;
|
|
59
|
+
type ClosedCatalog = (
|
|
60
|
+
key:
|
|
61
|
+
| "errors.network"
|
|
62
|
+
| "errors.networkHint"
|
|
63
|
+
| "errors.unexpected"
|
|
64
|
+
| "errors.retrySoon"
|
|
65
|
+
| "errors.waitSeconds"
|
|
66
|
+
| "errors.waitMinutes"
|
|
67
|
+
| "errors.waitHours"
|
|
68
|
+
| "serverErrors.quotaDay"
|
|
69
|
+
| "billing.plan",
|
|
70
|
+
params?: Record<string, unknown>,
|
|
71
|
+
) => string;
|
|
72
|
+
type _TypedCatalogNeedsNoCast = Satisfied<
|
|
73
|
+
ClosedCatalog extends ErrorDescriberOptions<"errors.", "serverErrors.", "quotaDay">["t"]
|
|
74
|
+
? true
|
|
75
|
+
: false
|
|
76
|
+
>;
|
|
31
77
|
|
|
32
78
|
/** Seconds until a refusal clears, when the envelope carries them. */
|
|
33
79
|
export function retryAfterSecs(error: ApiError): number | null {
|
|
@@ -42,78 +88,97 @@ export function retryAfterSecs(error: ApiError): number | null {
|
|
|
42
88
|
*
|
|
43
89
|
* The thresholds are deliberately not round: 90 seconds rather than 60, so "in 75 seconds" does
|
|
44
90
|
* not become the less precise "in 1 minute", and 90 minutes rather than 60 for the same reason
|
|
45
|
-
* one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours`
|
|
91
|
+
* one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours` under `prefix`, each with a
|
|
46
92
|
* `count` plural.
|
|
47
93
|
*/
|
|
48
|
-
export function humanizeWait
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
export function humanizeWait<Prefix extends string>(
|
|
95
|
+
t: Translate<`${Prefix}${WaitName}`>,
|
|
96
|
+
secs: number,
|
|
97
|
+
prefix: Prefix,
|
|
98
|
+
): string {
|
|
99
|
+
if (secs < 90) return t(`${prefix}waitSeconds`, { count: Math.max(1, Math.round(secs)) });
|
|
100
|
+
if (secs < 90 * 60) return t(`${prefix}waitMinutes`, { count: Math.round(secs / 60) });
|
|
101
|
+
return t(`${prefix}waitHours`, { count: Math.round(secs / 3600) });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ErrorContext {
|
|
105
|
+
error: ApiError;
|
|
106
|
+
/** The server's own localized sentence, when `messageKey` named one this build carries. */
|
|
107
|
+
says: string | null;
|
|
108
|
+
/** Seconds until it clears, already humanized. Null when the envelope did not say. */
|
|
109
|
+
wait: string | null;
|
|
110
|
+
waitSecs: number | null;
|
|
53
111
|
}
|
|
54
112
|
|
|
55
|
-
export interface ErrorDescriberOptions<
|
|
56
|
-
|
|
113
|
+
export interface ErrorDescriberOptions<
|
|
114
|
+
Prefix extends string,
|
|
115
|
+
ServerPrefix extends string,
|
|
116
|
+
ServerName extends string,
|
|
117
|
+
> {
|
|
118
|
+
t: Translate<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName}`>;
|
|
119
|
+
/** Catalog namespace for this module's own copy, e.g. `"errors."`. */
|
|
120
|
+
copyPrefix: Prefix;
|
|
57
121
|
/**
|
|
58
122
|
* The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
|
|
59
123
|
* ignored — the server names a sentence, it does not get to name any key in the app.
|
|
60
124
|
*/
|
|
61
|
-
messageKeyPrefix
|
|
125
|
+
messageKeyPrefix: ServerPrefix;
|
|
126
|
+
/**
|
|
127
|
+
* The keys this build actually carries, usually the `serverErrors` object out of the English
|
|
128
|
+
* catalog. Passing the catalog itself is both the membership test and the proof the compiler
|
|
129
|
+
* wants: a name that is in it IS a key, so the lookup below needs no cast.
|
|
130
|
+
*
|
|
131
|
+
* It is required because leaving it out is not a smaller version of this — it is the bug the
|
|
132
|
+
* option exists to prevent. i18next answers a key it does not have with the key itself, so a
|
|
133
|
+
* deploy landing ahead of the bundle a tab is still running would render `serverErrors.foo` at
|
|
134
|
+
* somebody. A name that is absent falls back to the server's English `message` instead.
|
|
135
|
+
*/
|
|
136
|
+
knownMessageKeys: Record<ServerName, unknown>;
|
|
62
137
|
/**
|
|
63
|
-
*
|
|
138
|
+
* Per-code copy. Everything not listed falls through to the default arm.
|
|
139
|
+
*
|
|
140
|
+
* The arms look up the app's own keys, so they close over the app's `t` rather than being
|
|
141
|
+
* handed one — this module has no names for that copy and no business typing it.
|
|
64
142
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* ahead of the bundle a tab is still running and a missing translation must degrade to
|
|
68
|
-
* readable rather than render the key itself at somebody.
|
|
143
|
+
* Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
|
|
144
|
+
* code that does not exist is a compile error at the arm rather than a branch that never runs.
|
|
69
145
|
*/
|
|
70
|
-
|
|
71
|
-
/** Catalog namespace for this module's own copy. Defaults to `"errors."`. */
|
|
72
|
-
copyPrefix?: string;
|
|
73
|
-
/** Per-code copy. Everything not listed falls through to the default arm below. */
|
|
74
|
-
codes?: Partial<Record<Code, (ctx: ErrorContext) => DescribedError>>;
|
|
146
|
+
codes?: Partial<Record<string, ErrorArm>>;
|
|
75
147
|
}
|
|
76
148
|
|
|
77
|
-
export
|
|
78
|
-
error: ApiError;
|
|
79
|
-
/** The server's own localized sentence, when `messageKey` named one this build carries. */
|
|
80
|
-
says: string | null;
|
|
81
|
-
/** Seconds until it clears, already humanized. Null when the envelope did not say. */
|
|
82
|
-
wait: string | null;
|
|
83
|
-
waitSecs: number | null;
|
|
84
|
-
t: Translate;
|
|
85
|
-
}
|
|
149
|
+
export type ErrorArm = (ctx: ErrorContext) => DescribedError;
|
|
86
150
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
151
|
+
/** Build the describer. */
|
|
152
|
+
export function createErrorDescriber<
|
|
153
|
+
Prefix extends string,
|
|
154
|
+
ServerPrefix extends string,
|
|
155
|
+
ServerName extends string,
|
|
156
|
+
>({
|
|
92
157
|
t,
|
|
93
|
-
|
|
158
|
+
copyPrefix,
|
|
159
|
+
messageKeyPrefix,
|
|
94
160
|
knownMessageKeys,
|
|
95
|
-
copyPrefix = "errors.",
|
|
96
161
|
codes = {},
|
|
97
|
-
}: ErrorDescriberOptions<
|
|
98
|
-
const known =
|
|
99
|
-
? new Set<string>(knownMessageKeys)
|
|
100
|
-
: new Set(Object.keys(knownMessageKeys ?? {}));
|
|
162
|
+
}: ErrorDescriberOptions<Prefix, ServerPrefix, ServerName>): (error: unknown) => DescribedError {
|
|
163
|
+
const known = new Set<string>(Object.keys(knownMessageKeys));
|
|
101
164
|
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
// Sound, and the reason nothing here needs a cast: the set was built from the keys of
|
|
166
|
+
// `knownMessageKeys`, so membership really does establish the claim. `Object.keys` and not
|
|
167
|
+
// `in`, which would have accepted "toString".
|
|
168
|
+
const has = (name: string): name is ServerName => known.has(name);
|
|
104
169
|
|
|
105
170
|
function serverSentence(error: ApiError): string | null {
|
|
106
171
|
const messageKey = error.messageKey;
|
|
107
172
|
if (messageKey === undefined || !messageKey.startsWith(messageKeyPrefix)) return null;
|
|
108
173
|
const name = messageKey.slice(messageKeyPrefix.length);
|
|
109
|
-
return has(name) ? t(
|
|
174
|
+
return has(name) ? t(`${messageKeyPrefix}${name}`, error.params) : null;
|
|
110
175
|
}
|
|
111
176
|
|
|
112
177
|
return function describeError(error: unknown): DescribedError {
|
|
113
178
|
if (!(error instanceof ApiError)) {
|
|
114
179
|
// Not a response at all — the request never landed. Almost always the network, and almost
|
|
115
180
|
// never worth showing a stack trace for.
|
|
116
|
-
return { cause: t(
|
|
181
|
+
return { cause: t(`${copyPrefix}network`), hint: t(`${copyPrefix}networkHint`) };
|
|
117
182
|
}
|
|
118
183
|
|
|
119
184
|
const waitSecs = retryAfterSecs(error);
|
|
@@ -122,10 +187,9 @@ export function createErrorDescriber<Code extends string>({
|
|
|
122
187
|
says: serverSentence(error),
|
|
123
188
|
wait: waitSecs === null ? null : humanizeWait(t, waitSecs, copyPrefix),
|
|
124
189
|
waitSecs,
|
|
125
|
-
t,
|
|
126
190
|
};
|
|
127
191
|
|
|
128
|
-
const arm = error.code === undefined ? undefined : codes[error.code
|
|
192
|
+
const arm = error.code === undefined ? undefined : codes[error.code];
|
|
129
193
|
if (arm) return arm(ctx);
|
|
130
194
|
|
|
131
195
|
// Anything unmapped: the server's own sentence is still the most specific thing we have,
|
|
@@ -133,8 +197,8 @@ export function createErrorDescriber<Code extends string>({
|
|
|
133
197
|
// genuinely does clear on its own — a 4xx does not, and saying so would be a lie that costs
|
|
134
198
|
// the reader another attempt.
|
|
135
199
|
return {
|
|
136
|
-
cause: ctx.says ?? (error.message || t(
|
|
137
|
-
hint: error.status >= 500 ? t(
|
|
200
|
+
cause: ctx.says ?? (error.message || t(`${copyPrefix}unexpected`)),
|
|
201
|
+
hint: error.status >= 500 ? t(`${copyPrefix}retrySoon`) : undefined,
|
|
138
202
|
};
|
|
139
203
|
};
|
|
140
204
|
}
|