@gusnips/react 0.9.21 → 0.9.23
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 +2 -1
- package/dist/api-error.d.ts +1 -1
- package/dist/api-error.d.ts.map +1 -1
- package/dist/describe-error.js +22 -5
- package/dist/describe-error.js.map +1 -1
- package/dist/query.js +1 -1
- package/package.json +4 -3
- package/src/api-error.ts +1 -1
- package/src/describe-error.test.ts +36 -4
- package/src/describe-error.ts +23 -5
- package/src/error-boundary.test.ts +48 -0
- package/src/query.ts +1 -1
- package/src/supabase-session.test.ts +1 -1
package/README.md
CHANGED
|
@@ -396,7 +396,8 @@ adopter writing `fix={hint}` at every error surface.
|
|
|
396
396
|
`recover` is derived from `shouldRetry` — the same rule react-query retries on — so the button a
|
|
397
397
|
reader sees and the retry that actually happens cannot disagree. Give it the same
|
|
398
398
|
`durableLimitCodes` list you give `queryDefaults`, and a spent quota offers no button instead of
|
|
399
|
-
one that cannot work.
|
|
399
|
+
one that cannot work. The words follow it too: "try again in a moment" appears only on a 5xx that
|
|
400
|
+
react-query would retry by itself, so never beside a durable refusal or a long countdown.
|
|
400
401
|
|
|
401
402
|
`formatWait` says how a stated wait becomes words:
|
|
402
403
|
|
package/dist/api-error.d.ts
CHANGED
package/dist/api-error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-error.d.ts","sourceRoot":"","sources":["../src/api-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"api-error.d.ts","sourceRoot":"","sources":["../src/api-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;;;;;;;;;GAUG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;;;;;;;OASG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5C;;;;;;;;OAQG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;gBAGzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI,EAClC,IAAI,GAAE;QACJ,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,cAAc,CAAC,EAAE,MAAM,CAAC;KACpB;CAaT;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEpD"}
|
package/dist/describe-error.js
CHANGED
|
@@ -74,15 +74,28 @@ export function createErrorDescriber(options) {
|
|
|
74
74
|
if (error.status === 401)
|
|
75
75
|
return "signin";
|
|
76
76
|
// It named its own expiry, so there is a real number to count down beside a disabled button.
|
|
77
|
+
// Before the durable list on purpose: a server that says when a refusal clears is believed
|
|
78
|
+
// (invariant 8). "wait" is a countdown the reader watches, not a request anyone sends, so a
|
|
79
|
+
// durable quota that resets in an hour is honest as one. Whether the query layer should wait
|
|
80
|
+
// that hour by itself is a different question, and `shouldRetry` answers it: no.
|
|
77
81
|
if (waitSecs !== null)
|
|
78
82
|
return "wait";
|
|
79
83
|
return shouldRetry(error, durableLimitCodes, maxRetryWaitSecs) ? "retry" : "none";
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
82
86
|
* Anything `codes` does not name. The server's own sentence is still the most specific thing
|
|
83
|
-
* we have, and support can act on it. A 5xx additionally gets "try again shortly"
|
|
84
|
-
*
|
|
85
|
-
*
|
|
87
|
+
* we have, and support can act on it. A 5xx additionally gets "try again shortly" — but only
|
|
88
|
+
* when `recover` says trying can work. A 503 whose code is on `durableLimitCodes` clears with a
|
|
89
|
+
* deploy, never a wait, and "try again in a moment" above no button is two answers to one
|
|
90
|
+
* question: the drift `recover` exists to prevent, arriving through the words instead. One
|
|
91
|
+
* adopter's checkout said "…write to us. Try again in a moment." with `recover: "none"`.
|
|
92
|
+
*
|
|
93
|
+
* A stated wait gets the line only while it is a moment: short enough that `shouldRetry` waits
|
|
94
|
+
* it out itself (`maxRetryWaitSecs`). Past that, "in a moment" over an hour's countdown is wrong
|
|
95
|
+
* the other way round, so the countdown carries it alone and an arm with `ctx.wait` can say when.
|
|
96
|
+
*
|
|
97
|
+
* A 4xx gets nothing here even where the rule retries it (408, 425, a 429). A 429 usually states
|
|
98
|
+
* its wait, and those words belong to an arm for the same reason.
|
|
86
99
|
*
|
|
87
100
|
* No `code` means no envelope, and then `message` is not the server's: it is the client's own
|
|
88
101
|
* `Request failed (502)`, written for a log. A gateway answering with HTML while the API
|
|
@@ -93,9 +106,13 @@ export function createErrorDescriber(options) {
|
|
|
93
106
|
* `fallback`.
|
|
94
107
|
*/
|
|
95
108
|
const unmapped = fallback ??
|
|
96
|
-
(({ error, says }) => ({
|
|
109
|
+
(({ error, says, recover }) => ({
|
|
97
110
|
cause: says ?? ((error.code === undefined ? "" : error.message) || copyOf("unexpected")),
|
|
98
|
-
fix: error.status >= 500
|
|
111
|
+
fix: error.status >= 500 &&
|
|
112
|
+
(recover === "retry" ||
|
|
113
|
+
(recover === "wait" && shouldRetry(error, durableLimitCodes, maxRetryWaitSecs)))
|
|
114
|
+
? copyOf("retrySoon")
|
|
115
|
+
: undefined,
|
|
99
116
|
}));
|
|
100
117
|
return function describeError(error) {
|
|
101
118
|
if (!(error instanceof ApiError)) {
|
|
@@ -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;AAkJD,SAAS,eAAe,CAItB,EACA,CAAC,EACD,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,GACqB;IACxD,6FAA6F;IAC7F,kFAAkF;IAClF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACjD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,6FAA6F;IAC7F,2EAA2E;IAC3E,MAAM,GAAG,GAAG,CAAC,IAAY,EAA+C,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAE3F,OAAO;QACL,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC;QAC3C,cAAc,CAAC,KAAK;YAClB,MAAM,MAAM,GAAG,CAAC,KAAyB,EAAiB,EAAE;gBAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;gBAClD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,gBAAgB,GAAG,IAAI,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC1F,CAAC,CAAC;YACF,wFAAwF;YACxF,4FAA4F;YAC5F,kDAAkD;YAClD,OAAO,CACL,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;gBACxB,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,gBAAgB,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAC/E,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAcD,MAAM,UAAU,oBAAoB,CAKlC,OAA8F;IAE9F,MAAM,EAAE,UAAU,EAAE,KAAK,GAAG,EAAE,EAAE,QAAQ,EAAE,iBAAiB,GAAG,EAAE,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAC/F,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAC9B,MAAM,IAAI,OAAO;QACf,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE;QACtE,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAE/B;;;;;OAKG;IACH,SAAS,WAAW,CAAC,KAAe,EAAE,QAAuB;QAC3D,6FAA6F;QAC7F,0FAA0F;QAC1F,kFAAkF;QAClF,IAAI,KAAK,CAAC,QAAQ;YAAE,OAAO,MAAM,CAAC;QAClC,6FAA6F;QAC7F,0FAA0F;QAC1F,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,QAAQ,CAAC;QAC1C,6FAA6F;QAC7F,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC;QACrC,OAAO,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACpF,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"describe-error.js","sourceRoot":"","sources":["../src/describe-error.ts"],"names":[],"mappings":"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"}
|
package/dist/query.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
],
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "rm -rf dist && tsc",
|
|
72
|
-
"typecheck": "tsc --noEmit && tsc -p
|
|
72
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
73
73
|
"test": "vitest run --passWithNoTests",
|
|
74
74
|
"test:watch": "vitest",
|
|
75
75
|
"lint": "eslint src",
|
|
@@ -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.
|
|
83
|
+
"@gusnips/http": "0.2.0",
|
|
84
84
|
"clsx": "^2.1.1",
|
|
85
85
|
"tailwind-merge": "^3.6.0"
|
|
86
86
|
},
|
|
@@ -125,6 +125,7 @@
|
|
|
125
125
|
"@base-ui/react": "^1.8.0",
|
|
126
126
|
"@supabase/supabase-js": "^2.112.4",
|
|
127
127
|
"@tanstack/react-query": "^5.102.8",
|
|
128
|
+
"@types/node": "^24.12.0",
|
|
128
129
|
"@types/react": "^19.2.14",
|
|
129
130
|
"@types/react-dom": "^19.2.3",
|
|
130
131
|
"happy-dom": "^20.14.5",
|
package/src/api-error.ts
CHANGED
|
@@ -152,8 +152,8 @@ describe("createErrorDescriber", () => {
|
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
it("offers a retry on a 5xx and withholds it on a 4xx", () => {
|
|
155
|
-
// A 5xx
|
|
156
|
-
//
|
|
155
|
+
// A plain 5xx clears on its own. A 403 does not, and saying so would cost the reader another
|
|
156
|
+
// attempt for nothing.
|
|
157
157
|
expect(describeError(new ApiError(503, null)).fix).toBe("errors.retrySoon");
|
|
158
158
|
expect(describeError(new ApiError(403, null)).fix).toBeUndefined();
|
|
159
159
|
});
|
|
@@ -178,7 +178,7 @@ describe("the recovery kind", () => {
|
|
|
178
178
|
copyPrefix: "errors.",
|
|
179
179
|
messageKeyPrefix: "serverErrors.",
|
|
180
180
|
knownMessageKeys: { quotaDay: "" },
|
|
181
|
-
durableLimitCodes: ["QUOTA_EXCEEDED"],
|
|
181
|
+
durableLimitCodes: ["QUOTA_EXCEEDED", "NOT_CONFIGURED"],
|
|
182
182
|
formatWait: (secs) => humanizeWait(t, secs, "errors."),
|
|
183
183
|
});
|
|
184
184
|
|
|
@@ -223,6 +223,37 @@ describe("the recovery kind", () => {
|
|
|
223
223
|
expect(describeError(error).recover).toBe("retry");
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
+
it("never says to try again beside a 5xx the rule will not retry", () => {
|
|
227
|
+
// A 503 only a deploy clears. "Try again in a moment" over no button is the same two answers
|
|
228
|
+
// to one question, reached through the words instead of the control.
|
|
229
|
+
const durable = describeError(new ApiError(503, { code: "NOT_CONFIGURED", message: "off" }));
|
|
230
|
+
expect(durable.recover).toBe("none");
|
|
231
|
+
expect(durable.fix).toBeUndefined();
|
|
232
|
+
// The server can say it too, with an explicit `null` wait, and no list involved.
|
|
233
|
+
const never = describeError(
|
|
234
|
+
new ApiError(503, { code: "X", message: "", details: { retryAfterSecs: null } }),
|
|
235
|
+
);
|
|
236
|
+
expect(never.recover).toBe("none");
|
|
237
|
+
expect(never.fix).toBeUndefined();
|
|
238
|
+
|
|
239
|
+
expect(describeError(new ApiError(503, null))).toMatchObject({
|
|
240
|
+
recover: "retry",
|
|
241
|
+
fix: "errors.retrySoon",
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("says 'in a moment' over a countdown only when the wait is one", () => {
|
|
246
|
+
// An hour is not a moment. The countdown says when, and the words stay out of its way.
|
|
247
|
+
const long = describeError(new ApiError(503, null, { retryAfterSecs: 3600 }));
|
|
248
|
+
expect(long.recover).toBe("wait");
|
|
249
|
+
expect(long.fix).toBeUndefined();
|
|
250
|
+
// Short enough that react-query waits it out itself, so "in a moment" is true.
|
|
251
|
+
expect(describeError(new ApiError(503, null, { retryAfterSecs: 5 }))).toMatchObject({
|
|
252
|
+
recover: "wait",
|
|
253
|
+
fix: "errors.retrySoon",
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
226
257
|
it("carries the request id the error contract reserves a slot for", () => {
|
|
227
258
|
const error = new ApiError(500, null, { requestId: "req_123" });
|
|
228
259
|
expect(describeError(error).reference).toBe("req_123");
|
|
@@ -341,7 +372,8 @@ describe("fallback", () => {
|
|
|
341
372
|
{ retryAfterSecs: 120 },
|
|
342
373
|
);
|
|
343
374
|
expect(describeError(error).fix).toBe('errors.retryIn(errors.waitMinutes({"count":2}))');
|
|
344
|
-
|
|
375
|
+
// Two minutes is past what the query layer waits out, so the built-in says nothing at all.
|
|
376
|
+
expect(createErrorDescriber(options)(error).fix).toBeUndefined();
|
|
345
377
|
});
|
|
346
378
|
|
|
347
379
|
it("leaves the derived recovery kind alone unless the arm narrows it", () => {
|
package/src/describe-error.ts
CHANGED
|
@@ -385,15 +385,28 @@ export function createErrorDescriber<
|
|
|
385
385
|
// the session is fine and it is a different account that would help, so it falls through.
|
|
386
386
|
if (error.status === 401) return "signin";
|
|
387
387
|
// It named its own expiry, so there is a real number to count down beside a disabled button.
|
|
388
|
+
// Before the durable list on purpose: a server that says when a refusal clears is believed
|
|
389
|
+
// (invariant 8). "wait" is a countdown the reader watches, not a request anyone sends, so a
|
|
390
|
+
// durable quota that resets in an hour is honest as one. Whether the query layer should wait
|
|
391
|
+
// that hour by itself is a different question, and `shouldRetry` answers it: no.
|
|
388
392
|
if (waitSecs !== null) return "wait";
|
|
389
393
|
return shouldRetry(error, durableLimitCodes, maxRetryWaitSecs) ? "retry" : "none";
|
|
390
394
|
}
|
|
391
395
|
|
|
392
396
|
/**
|
|
393
397
|
* Anything `codes` does not name. The server's own sentence is still the most specific thing
|
|
394
|
-
* we have, and support can act on it. A 5xx additionally gets "try again shortly"
|
|
395
|
-
*
|
|
396
|
-
*
|
|
398
|
+
* we have, and support can act on it. A 5xx additionally gets "try again shortly" — but only
|
|
399
|
+
* when `recover` says trying can work. A 503 whose code is on `durableLimitCodes` clears with a
|
|
400
|
+
* deploy, never a wait, and "try again in a moment" above no button is two answers to one
|
|
401
|
+
* question: the drift `recover` exists to prevent, arriving through the words instead. One
|
|
402
|
+
* adopter's checkout said "…write to us. Try again in a moment." with `recover: "none"`.
|
|
403
|
+
*
|
|
404
|
+
* A stated wait gets the line only while it is a moment: short enough that `shouldRetry` waits
|
|
405
|
+
* it out itself (`maxRetryWaitSecs`). Past that, "in a moment" over an hour's countdown is wrong
|
|
406
|
+
* the other way round, so the countdown carries it alone and an arm with `ctx.wait` can say when.
|
|
407
|
+
*
|
|
408
|
+
* A 4xx gets nothing here even where the rule retries it (408, 425, a 429). A 429 usually states
|
|
409
|
+
* its wait, and those words belong to an arm for the same reason.
|
|
397
410
|
*
|
|
398
411
|
* No `code` means no envelope, and then `message` is not the server's: it is the client's own
|
|
399
412
|
* `Request failed (502)`, written for a log. A gateway answering with HTML while the API
|
|
@@ -405,9 +418,14 @@ export function createErrorDescriber<
|
|
|
405
418
|
*/
|
|
406
419
|
const unmapped: ErrorArm =
|
|
407
420
|
fallback ??
|
|
408
|
-
(({ error, says }) => ({
|
|
421
|
+
(({ error, says, recover }) => ({
|
|
409
422
|
cause: says ?? ((error.code === undefined ? "" : error.message) || copyOf("unexpected")),
|
|
410
|
-
fix:
|
|
423
|
+
fix:
|
|
424
|
+
error.status >= 500 &&
|
|
425
|
+
(recover === "retry" ||
|
|
426
|
+
(recover === "wait" && shouldRetry(error, durableLimitCodes, maxRetryWaitSecs)))
|
|
427
|
+
? copyOf("retrySoon")
|
|
428
|
+
: undefined,
|
|
411
429
|
}));
|
|
412
430
|
|
|
413
431
|
return function describeError(error: unknown): DescribedError {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ErrorBoundary } from "./error-boundary.tsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The two statics are pure, which is why the reset rule lives in them: it can be checked with no
|
|
6
|
+
* DOM, no renderer and no thrown error.
|
|
7
|
+
*
|
|
8
|
+
* What is worth pinning is that a caught error belongs to the screen it happened on. Wrong one way,
|
|
9
|
+
* navigating away carries the wreckage to every page after it. Wrong the other way, the fallback
|
|
10
|
+
* clears itself at once and re-renders the component that just threw, which throws again: a
|
|
11
|
+
* flicker instead of a state. Lifted from an adopter's copy of this class, deleted when it moved
|
|
12
|
+
* onto the kit's.
|
|
13
|
+
*/
|
|
14
|
+
const props = (resetKey: string | undefined) => ({
|
|
15
|
+
resetKey,
|
|
16
|
+
fallback: () => null,
|
|
17
|
+
children: null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("ErrorBoundary", () => {
|
|
21
|
+
it("records the error that was thrown", () => {
|
|
22
|
+
const error = new TypeError("outcome is not a key of TONE");
|
|
23
|
+
expect(ErrorBoundary.getDerivedStateFromError(error)).toEqual({ error });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("keeps the error while the route stays put", () => {
|
|
27
|
+
const state = { error: new Error("boom"), key: "/jobs/abc" };
|
|
28
|
+
expect(ErrorBoundary.getDerivedStateFromProps(props("/jobs/abc"), state)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("clears the error when the route changes", () => {
|
|
32
|
+
const state = { error: new Error("boom"), key: "/jobs/abc" };
|
|
33
|
+
expect(ErrorBoundary.getDerivedStateFromProps(props("/keys"), state)).toEqual({
|
|
34
|
+
error: null,
|
|
35
|
+
key: "/keys",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("tracks the route while nothing has thrown, so the first error is scoped to it", () => {
|
|
40
|
+
// Without this the key would still hold the route the boundary mounted on, and the first
|
|
41
|
+
// error on any later route would clear itself on the next re-render.
|
|
42
|
+
const state = { error: null, key: "/" };
|
|
43
|
+
expect(ErrorBoundary.getDerivedStateFromProps(props("/usage"), state)).toEqual({
|
|
44
|
+
error: null,
|
|
45
|
+
key: "/usage",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/query.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retryDelayMs as delayFor,
|
|
1
|
+
import { retryDelayMs as delayFor, isRetryable as retryRule } from "@gusnips/http/retry";
|
|
2
2
|
import type { DefaultOptions } from "@tanstack/react-query";
|
|
3
3
|
import { ApiError } from "./api-error.ts";
|
|
4
4
|
|
|
@@ -85,7 +85,7 @@ function reachable(paths: string[] = []): typeof fetch {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function authClient(stored: SupabaseSessionStorage, fetch: typeof fetch) {
|
|
88
|
+
function authClient(stored: SupabaseSessionStorage, fetch: typeof globalThis.fetch) {
|
|
89
89
|
return createClient(PROJECT_URL, "anon-key", {
|
|
90
90
|
// Off so that starting the client does not refresh an expired session by itself: the refresh
|
|
91
91
|
// under test is the one `signOut()` makes.
|