@gusnips/react 0.9.24 → 0.9.26
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 +173 -36
- package/dist/api-client.d.ts +51 -51
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +3 -3
- package/dist/api-error.d.ts +11 -11
- package/dist/api-error.js +11 -11
- package/dist/auth-email.d.ts +40 -0
- package/dist/auth-email.d.ts.map +1 -0
- package/dist/auth-email.js +106 -0
- package/dist/auth-email.js.map +1 -0
- package/dist/auth-store.d.ts +23 -23
- package/dist/auth-store.js +13 -13
- package/dist/commands.d.ts +80 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +192 -0
- package/dist/commands.js.map +1 -0
- package/dist/copy.d.ts +3 -3
- package/dist/copy.js +3 -3
- package/dist/deploy-recovery.d.ts +40 -40
- package/dist/deploy-recovery.js +40 -40
- package/dist/describe-error.d.ts +68 -68
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +5 -5
- package/dist/error-boundary.d.ts +12 -12
- package/dist/guards.d.ts +15 -15
- package/dist/guards.js +3 -3
- package/dist/hydrate.d.ts +15 -16
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +9 -9
- package/dist/hydrate.js.map +1 -1
- package/dist/i18n.d.ts +15 -16
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +5 -6
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal-path.d.ts +8 -7
- package/dist/internal-path.d.ts.map +1 -1
- package/dist/internal-path.js +8 -7
- package/dist/internal-path.js.map +1 -1
- package/dist/prerender-contract.d.ts +7 -7
- package/dist/prerender-contract.js +7 -7
- package/dist/query.d.ts +16 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +2 -2
- package/dist/query.js.map +1 -1
- package/dist/sse.d.ts +19 -20
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +18 -19
- package/dist/sse.js.map +1 -1
- package/dist/states.d.ts +12 -12
- package/dist/states.d.ts.map +1 -1
- package/dist/supabase-session.d.ts +47 -48
- package/dist/supabase-session.d.ts.map +1 -1
- package/dist/supabase-session.js +35 -35
- package/dist/supabase-session.js.map +1 -1
- package/dist/theme.d.ts +4 -4
- package/dist/theme.js +4 -4
- package/dist/ui/class-name.d.ts +3 -4
- package/dist/ui/class-name.d.ts.map +1 -1
- package/dist/ui/class-name.js +3 -4
- package/dist/ui/class-name.js.map +1 -1
- package/dist/ui/combobox.d.ts +11 -11
- package/dist/ui/combobox.d.ts.map +1 -1
- package/dist/ui/dialog.d.ts +5 -5
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/drawer.d.ts +10 -10
- package/dist/ui/drawer.d.ts.map +1 -1
- package/dist/ui/drawer.js +2 -2
- package/dist/ui/input.d.ts +2 -2
- package/dist/ui/input.js +2 -2
- package/dist/ui/menu.d.ts +9 -9
- package/dist/ui/menu.d.ts.map +1 -1
- package/dist/ui/menu.js +4 -4
- package/dist/ui/menu.js.map +1 -1
- package/dist/ui/select.d.ts +12 -12
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +3 -3
- package/dist/ui/tabs.d.ts +14 -15
- package/dist/ui/tabs.d.ts.map +1 -1
- package/dist/ui/tabs.js +13 -14
- package/dist/ui/tabs.js.map +1 -1
- package/package.json +5 -1
- package/src/api-client.ts +51 -51
- package/src/api-error.ts +11 -11
- package/src/auth-email.test.ts +145 -0
- package/src/auth-email.ts +112 -0
- package/src/auth-store.ts +23 -23
- package/src/commands-hook.test.ts +444 -0
- package/src/commands.test.ts +245 -0
- package/src/commands.ts +268 -0
- package/src/copy.ts +3 -3
- package/src/deploy-recovery.ts +40 -40
- package/src/describe-error.ts +68 -68
- package/src/error-boundary.tsx +12 -12
- package/src/guards.tsx +15 -15
- package/src/hydrate.ts +15 -16
- package/src/i18n.ts +15 -16
- package/src/index.ts +2 -0
- package/src/internal-path.ts +8 -7
- package/src/prerender-contract.ts +7 -7
- package/src/query.ts +16 -17
- package/src/sse.ts +19 -20
- package/src/states.ts +12 -12
- package/src/supabase-session.ts +46 -47
- package/src/theme.ts +4 -4
- package/src/ui/class-name.ts +3 -4
- package/src/ui/combobox.tsx +11 -11
- package/src/ui/dialog.tsx +5 -5
- package/src/ui/drawer.tsx +10 -10
- package/src/ui/input.tsx +2 -2
- package/src/ui/menu.tsx +8 -8
- package/src/ui/select.tsx +12 -12
- package/src/ui/tabs.tsx +14 -15
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ bun add @gusnips/react
|
|
|
11
11
|
const user = await api.get<User>("/me");
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
That is the whole call. `user` is a `User
|
|
14
|
+
That is the whole call. `user` is a `User`: not a `Response`, not `res.json()`, not a null
|
|
15
15
|
check. The client attached the token, unwrapped the envelope, and threw a typed `ApiError`
|
|
16
16
|
carrying the code and the request id if the server refused.
|
|
17
17
|
|
|
@@ -30,18 +30,18 @@ const api = createApiClient({
|
|
|
30
30
|
## The client knows three things people learn the hard way
|
|
31
31
|
|
|
32
32
|
**Six queries firing at once send one refresh, not six.** The auth server rotates the refresh
|
|
33
|
-
token when you use it, so the losers of that race each invalidate the winner
|
|
33
|
+
token when you use it, so the losers of that race each invalidate the winner, and the person is
|
|
34
34
|
signed out in the middle of a load that was working. Three separate codebases arrived at
|
|
35
35
|
single-flight refresh independently. This is that.
|
|
36
36
|
|
|
37
37
|
**Only auth ANSWERING is a "no".** Dropping a packet tells you nothing about whether a session is
|
|
38
|
-
good, and neither does a 500
|
|
38
|
+
good, and neither does a 500. That is auth failing, not auth answering. So `refresh()` answers
|
|
39
39
|
`{ token, reachedAuth }`, and only a real refusal signs anyone out. Before that split, a Wi-Fi
|
|
40
40
|
blip logged people out mid-load; without the second half, one bad minute at the auth server signs
|
|
41
41
|
out everybody whose token happened to need refreshing.
|
|
42
42
|
|
|
43
43
|
**A sign-out has a fail-safe timer.** Awaiting `signOut()` before redirecting covers a rejection,
|
|
44
|
-
not a hang
|
|
44
|
+
not a hang, and a hang leaves someone signed out in name only: every request 401ing, nothing
|
|
45
45
|
left that could redirect it.
|
|
46
46
|
|
|
47
47
|
### Supabase sessions
|
|
@@ -65,14 +65,14 @@ import { isAuthOutage } from "@gusnips/react/supabase";
|
|
|
65
65
|
|
|
66
66
|
// A callback that just failed to trade its one-use link.
|
|
67
67
|
if (isAuthOutage(error))
|
|
68
|
-
showRetry(); // the link was never spent
|
|
68
|
+
showRetry(); // the link was never spent: the same one still works
|
|
69
69
|
else showLinkSpent(); // auth said no: only a new link helps
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
`isAuthOutage` is true when auth FAILED rather than answered
|
|
72
|
+
`isAuthOutage` is true when auth FAILED rather than answered: a request that never landed, or a
|
|
73
73
|
5xx. Only a real refusal is an answer, and only an answer may end a session or a link. Use it
|
|
74
|
-
anywhere that decision is made; the sentence you show is a separate question, and a narrower one
|
|
75
|
-
|
|
74
|
+
anywhere that decision is made; the sentence you show is a separate question, and a narrower one:
|
|
75
|
+
"check your connection" is true of a request that never landed and false of a 502, which is
|
|
76
76
|
ours. auth-js marks "nothing came back", and only that, with status 0.
|
|
77
77
|
|
|
78
78
|
It is safe to hand it the `error` from a `{ data, error }` result without checking it first:
|
|
@@ -120,7 +120,7 @@ Supabase email links have two complete patterns. Keep either one, never half of
|
|
|
120
120
|
- A link carrying `ConfirmationURL` needs `detectSessionInUrl` enabled. That reader consumes an
|
|
121
121
|
implicit `#access_token` or a PKCE `?code`; it does not consume `token_hash`.
|
|
122
122
|
|
|
123
|
-
Either pattern can carry a destination
|
|
123
|
+
Either pattern can carry a destination: the `token_hash` one has to be asked. GoTrue hands every
|
|
124
124
|
template a `{{ .RedirectTo }}`: the `redirectTo` the app passed, already checked against the
|
|
125
125
|
allowlist, falling back to the Site URL when it was absent or not on it. A `{{ .ConfirmationURL }}`
|
|
126
126
|
link carries it for you. A template that writes its own landing drops it unless it asks:
|
|
@@ -130,7 +130,7 @@ link carries it for you. A template that writes its own landing drops it unless
|
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
Leave that parameter off and `redirectTo` and `emailRedirectTo` do nothing on every e-mail flow,
|
|
133
|
-
however carefully the app sets them
|
|
133
|
+
however carefully the app sets them: four of the codebases this came from pass one that the
|
|
134
134
|
template throws away, so a person bounced off a protected page signs in and lands on the front
|
|
135
135
|
door. Nothing reports it: the app's call is correct, the link works, and the destination was
|
|
136
136
|
dropped in an HTML file hosted somewhere else.
|
|
@@ -147,7 +147,7 @@ Validate it on arrival even though GoTrue already did: that check is against a l
|
|
|
147
147
|
so it says the link may come back here, not which page it may open.
|
|
148
148
|
|
|
149
149
|
Recovery and invite links continue to the new-password screen after the session opens. A callback
|
|
150
|
-
must also tell a used or expired link apart from a request that never landed
|
|
150
|
+
must also tell a used or expired link apart from a request that never landed, or one that landed
|
|
151
151
|
and came back 5xx: the first needs a new link, and the other two can retry the same one, because
|
|
152
152
|
the hash was never spent.
|
|
153
153
|
|
|
@@ -165,7 +165,7 @@ implicit-flow tokens; in a browser the client has already taken it, so wait for
|
|
|
165
165
|
React Native hand `credential` to `exchangeCodeForSession` or `setSession`. `error` is a failure
|
|
166
166
|
GoTrue wrote into the address. `invalid` is a link that was cut short or edited.
|
|
167
167
|
|
|
168
|
-
Pass the fragment too
|
|
168
|
+
Pass the fragment too: `parseAuthCallback(location.search, location.hash)`. GoTrue writes a
|
|
169
169
|
failure into the fragment every time and into the query only sometimes: after an implicit-flow
|
|
170
170
|
email link it is in the fragment alone, so a query-only reader shows nothing to somebody whose
|
|
171
171
|
link expired.
|
|
@@ -197,7 +197,7 @@ new QueryClient({
|
|
|
197
197
|
});
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
Retries 408, 425, a transient 429, 5xx, and no-response-at-all. Nothing else
|
|
200
|
+
Retries 408, 425, a transient 429, 5xx, and no-response-at-all. Nothing else: every other 4xx is
|
|
201
201
|
an answer, and a second attempt just says it again. The rule itself is
|
|
202
202
|
[`@gusnips/http/retry`](../http#should-this-request-be-tried-again), so an SDK with no React in it
|
|
203
203
|
can retry the same way.
|
|
@@ -210,7 +210,7 @@ those codes and they are never retried.
|
|
|
210
210
|
You only need that list where your API stays quiet. If it answers `Retry-After: 2`, the wait is
|
|
211
211
|
sat out; past `maxRetryWaitSecs` (10 seconds by default) the wait is the answer and the screen
|
|
212
212
|
says so instead of holding a spinner. And if it sends `details.retryAfterSecs: null`, it is
|
|
213
|
-
saying waiting will never fix this
|
|
213
|
+
saying waiting will never fix this: a cap that frees only when somebody deletes something, a
|
|
214
214
|
slot that frees when another job ends. That `null` is read as the claim it is, not as a field
|
|
215
215
|
somebody forgot, so the code never reaches your list.
|
|
216
216
|
|
|
@@ -244,14 +244,14 @@ offer comes from `describeError(view.error).recover`, below.
|
|
|
244
244
|
## Survives your deploys
|
|
245
245
|
|
|
246
246
|
An app with `lazy()` routes serves chunks by hashed filename. Deploy while someone has a tab
|
|
247
|
-
open, and their next click asks for a file that no longer exists
|
|
247
|
+
open, and their next click asks for a file that no longer exists: a white screen on a button
|
|
248
248
|
that worked a minute ago. The same deploy breaks a second way that looks nothing like the first:
|
|
249
249
|
the old bundle reads a field your API just renamed, and throws a plain `TypeError` with nothing
|
|
250
250
|
in it about a deploy.
|
|
251
251
|
|
|
252
252
|
**Fix the cause in your host config first.** This package cannot do it for you and it is worth
|
|
253
253
|
more than everything below. A static host answers a missing `/assets/x.js` with your SPA
|
|
254
|
-
fallback
|
|
254
|
+
fallback (`index.html`, at a 200, as `text/html`), and that is what the browser's "MIME type
|
|
255
255
|
text/html" module-script refusal actually is. Hosts then match cache rules against the _request
|
|
256
256
|
path_ rather than the outcome, so the `immutable, max-age=31536000` rule you wrote for hashed
|
|
257
257
|
assets lands on that HTML body: one poisoned asset URL, at the edge and in every browser that
|
|
@@ -264,12 +264,12 @@ Then two detectors, for the two failures:
|
|
|
264
264
|
if (await isStaleBuild()) reloadOnce();
|
|
265
265
|
```
|
|
266
266
|
|
|
267
|
-
`isStaleBuild` asks the server instead of reading the error
|
|
267
|
+
`isStaleBuild` asks the server instead of reading the error: it compares the entry script this
|
|
268
268
|
document loaded against the one the live page names. That is the only thing that catches the
|
|
269
269
|
renamed-field crash, where the message carries no signal at all. It asks for the page, not for
|
|
270
270
|
the asset: a CDN serves assets with a long `s-maxage`, so a bundle retired an hour ago still
|
|
271
271
|
answers 200 from the edge and would report "current" during exactly the window when skew is most
|
|
272
|
-
likely. Anything ambiguous
|
|
272
|
+
likely. Anything ambiguous (offline, a non-200, a timeout) answers `false`, because dressing a
|
|
273
273
|
real bug up as an update hides it from you too.
|
|
274
274
|
|
|
275
275
|
```ts
|
|
@@ -277,12 +277,12 @@ if (isChunkLoadError(error) && reloadOnce()) return null;
|
|
|
277
277
|
```
|
|
278
278
|
|
|
279
279
|
`isChunkLoadError` matches the message, which is all you have for a failure that never reaches a
|
|
280
|
-
boundary
|
|
280
|
+
boundary: an `unhandledrejection`, or a listener. Every browser's phrasing is in there.
|
|
281
281
|
|
|
282
282
|
`reloadOnce` reloads at most once a minute, and there is **one** stamp for every reason to
|
|
283
283
|
reload. A tab can reload because a chunk 404'd, because the server says it is behind, or from a
|
|
284
|
-
guard inlined in `index.html` that runs before any module does. Three detectors, one predicament
|
|
285
|
-
|
|
284
|
+
guard inlined in `index.html` that runs before any module does. Three detectors, one predicament:
|
|
285
|
+
give each its own key and a broken deploy gets three reloads a minute to take turns with. That
|
|
286
286
|
inline guard cannot import anything, so the key it has to copy is exported as `RELOAD_GUARD_KEY`.
|
|
287
287
|
|
|
288
288
|
And in your browser entry:
|
|
@@ -294,7 +294,7 @@ installPreloadErrorHandler();
|
|
|
294
294
|
```
|
|
295
295
|
|
|
296
296
|
It is narrower than it looks: it swallows Vite's CSS _preload hint_ failure and nothing else. A
|
|
297
|
-
hint that fails is harmless
|
|
297
|
+
hint that fails is harmless: the import runs straight after and usually works. Taking Vite's
|
|
298
298
|
`preventDefault()` for every preload error instead resolves the dynamic import with `undefined`,
|
|
299
299
|
`React.lazy` reads `.default` off nothing, and you get a crash screen plus a TypeError with only
|
|
300
300
|
React frames in it, naming no chunk.
|
|
@@ -315,7 +315,7 @@ const RequireStaff = requireProfile(
|
|
|
315
315
|
|
|
316
316
|
`onDenied` and `onError` are separate because `!me?.isStaff` reads a 500 as "not staff". An
|
|
317
317
|
operator arriving while `/auth/me` is down gets told the page does not exist: the wrong cause, no
|
|
318
|
-
retry, and no request id to quote. A guard has three answers
|
|
318
|
+
retry, and no request id to quote. A guard has three answers (wait, fail, refuse) and only a
|
|
319
319
|
real `false` reaches the refusal.
|
|
320
320
|
|
|
321
321
|
`createRequireAuth` sends an anonymous visitor to `?next=`, carrying path, query and hash. A hash
|
|
@@ -333,7 +333,7 @@ const next = safeInternalPath(new URLSearchParams(location.search).get("next"))
|
|
|
333
333
|
`createRequireAnonymous` already performs that check and falls back to the home path you gave it.
|
|
334
334
|
|
|
335
335
|
Writing a return target down is the other direction, and it has its own hazard. A Supabase client
|
|
336
|
-
with no `flowType` is on the **implicit** flow
|
|
336
|
+
with no `flowType` is on the **implicit** flow (the default), so a callback comes back as
|
|
337
337
|
`#access_token=…&refresh_token=…`. A guard renders exactly when there is no session yet, which is
|
|
338
338
|
that callback's own state while the client parses the fragment: point an OAuth `redirectTo` at a
|
|
339
339
|
guarded route and a concatenated `pathname + search + hash` puts a refresh token into a query
|
|
@@ -347,8 +347,40 @@ const returnTo = returnPathFromLocation(window.location);
|
|
|
347
347
|
```
|
|
348
348
|
|
|
349
349
|
It keeps the page and drops only a fragment carrying a credential, because the page is the part
|
|
350
|
-
worth returning to. `createRequireAuth` uses it; use it anywhere else you record where someone was
|
|
351
|
-
|
|
350
|
+
worth returning to. `createRequireAuth` uses it; use it anywhere else you record where someone was:
|
|
351
|
+
a dead-session redirect is the common one.
|
|
352
|
+
|
|
353
|
+
## Keep the email between sign-in screens
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { recallAuthEmail, rememberAuthEmail } from "@gusnips/react";
|
|
357
|
+
|
|
358
|
+
<input
|
|
359
|
+
name="email"
|
|
360
|
+
type="email"
|
|
361
|
+
defaultValue={recallAuthEmail()}
|
|
362
|
+
onChange={(event) => rememberAuthEmail(event.currentTarget.value)}
|
|
363
|
+
/>;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Sign in, "use a code instead", forgot password and create account are separate screens, and each
|
|
367
|
+
one used to open with an empty email field. Put those two props on every one of them and the
|
|
368
|
+
address follows the person. Call `forgetAuthEmail()` where your app learns a session exists, so the
|
|
369
|
+
next person at this browser starts empty.
|
|
370
|
+
|
|
371
|
+
It is kept in `localStorage` for 30 minutes after the last keystroke, and never in a URL, where it
|
|
372
|
+
would reach `Referer`, access logs, history and analytics. It is `localStorage` and not
|
|
373
|
+
`sessionStorage` because an emailed link opens in a new tab, and a new tab does not inherit
|
|
374
|
+
`sessionStorage`. That tab is where a person lands on sign-in after confirming an account. Where
|
|
375
|
+
the browser blocks site data, or on React Native, the address lasts in memory until the page or the
|
|
376
|
+
app is left.
|
|
377
|
+
|
|
378
|
+
- Read it once, when the screen opens. It is not a store: nothing re-renders when it changes. On a
|
|
379
|
+
prerendered page, read it after hydration, never while rendering.
|
|
380
|
+
- Leave the field uncontrolled. A `defaultValue` and a change handler are enough. Also call
|
|
381
|
+
`rememberAuthEmail(value)` where you read the field at submit, because some password managers fill
|
|
382
|
+
a form without firing events.
|
|
383
|
+
- Never put a password, a code or a token here.
|
|
352
384
|
|
|
353
385
|
## Never dead-end anyone
|
|
354
386
|
|
|
@@ -356,13 +388,13 @@ worth returning to. `createRequireAuth` uses it; use it anywhere else you record
|
|
|
356
388
|
import type { ErrorStateProps } from "@gusnips/react";
|
|
357
389
|
```
|
|
358
390
|
|
|
359
|
-
No component ships
|
|
391
|
+
No component ships: seven codebases have an `ErrorState.tsx` and they overlap 9–37%, because one
|
|
360
392
|
draws a tinted icon, one an illustration, one a mascot. What they share is the prop shape, and
|
|
361
393
|
the prop shape _is_ the rule: `problem`, `cause`, `fix`, and `action`. **`fix` and `action` are
|
|
362
394
|
required.** A required prop is the only version of "always offer a way out" that a caller in a
|
|
363
395
|
hurry cannot skip.
|
|
364
396
|
|
|
365
|
-
`createErrorDescriber` turns a thrown `ApiError` into the `cause` and `fix` to put in it
|
|
397
|
+
`createErrorDescriber` turns a thrown `ApiError` into the `cause` and `fix` to put in it, plus
|
|
366
398
|
which control to offer, and the request id to print under it:
|
|
367
399
|
|
|
368
400
|
```ts
|
|
@@ -393,7 +425,7 @@ the catalog has, the reader gets it in their own language.
|
|
|
393
425
|
The field is `fix`, the same word as the prop above. It was `hint` until 0.6.0, which left every
|
|
394
426
|
adopter writing `fix={hint}` at every error surface.
|
|
395
427
|
|
|
396
|
-
`recover` is derived from `shouldRetry`
|
|
428
|
+
`recover` is derived from `shouldRetry` (the same rule react-query retries on), so the button a
|
|
397
429
|
reader sees and the retry that actually happens cannot disagree. Give it the same
|
|
398
430
|
`durableLimitCodes` list you give `queryDefaults`, and a spent quota offers no button instead of
|
|
399
431
|
one that cannot work. The words follow it too: "try again in a moment" appears only on a 5xx that
|
|
@@ -420,7 +452,7 @@ fallback: ({ says, wait }) => ({
|
|
|
420
452
|
});
|
|
421
453
|
```
|
|
422
454
|
|
|
423
|
-
Some APIs write `message` for whoever reads the screen. Some write it for whoever reads the log
|
|
455
|
+
Some APIs write `message` for whoever reads the screen. Some write it for whoever reads the log,
|
|
424
456
|
in English, in an app that ships three languages. Your arm also gets the stated wait, which the
|
|
425
457
|
default has nowhere to put.
|
|
426
458
|
|
|
@@ -497,6 +529,110 @@ toast needs no `catch`. Outside a component, `copyText(text)` does the write on
|
|
|
497
529
|
The words are yours, and so is the live region: a label that changes needs `aria-live` to be
|
|
498
530
|
heard.
|
|
499
531
|
|
|
532
|
+
## Slash commands in a text field
|
|
533
|
+
|
|
534
|
+
A slash command is text like `/help` that picks an action instead of sending a message.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
import { parseCommand } from "@gusnips/react/commands";
|
|
538
|
+
|
|
539
|
+
parseCommand("/help");
|
|
540
|
+
// { name: "help" }
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Names have no leading slash. Give each command other names (`aliases`) and search words (`tags`):
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
const commands = [
|
|
547
|
+
{
|
|
548
|
+
name: "help",
|
|
549
|
+
description: "See commands",
|
|
550
|
+
aliases: ["commands", "shortcuts"],
|
|
551
|
+
tags: ["keyboard", "guide"],
|
|
552
|
+
},
|
|
553
|
+
];
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
`/commands` runs `/help`. Searching for `keyboard` finds its menu row, but `/keyboard` is not an
|
|
557
|
+
executable name. `matchCommands(commands, query)` searches names, aliases, tags and descriptions,
|
|
558
|
+
ignoring case and accents. Exact names rank first, then aliases, name prefixes, alias prefixes,
|
|
559
|
+
tags and descriptions. Equal ranks keep registry order; each name gets one row.
|
|
560
|
+
|
|
561
|
+
`resolveCommand(commands, name)` answers `matched`, `unknown` or `ambiguous`. An exact name wins
|
|
562
|
+
over an alias. Two commands sharing an alias are ambiguous, not a reason to run the first one.
|
|
563
|
+
|
|
564
|
+
For a controlled text field, use the hook:
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
import { useSlashCommands } from "@gusnips/react/commands";
|
|
568
|
+
|
|
569
|
+
const slash = useSlashCommands({
|
|
570
|
+
text,
|
|
571
|
+
commands,
|
|
572
|
+
onChange: setText,
|
|
573
|
+
onExecute: runCommand,
|
|
574
|
+
onError: showCommandError,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
function submit() {
|
|
578
|
+
if (slash.submit()) return;
|
|
579
|
+
sendMessage(text);
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
The functions in that example belong to your app. `runCommand(command, arg, altKey)` gets the
|
|
584
|
+
original command object, the argument and whether Alt was held. It owns permissions and action
|
|
585
|
+
errors. Supply only commands allowed in this field, with descriptions and tags in the current
|
|
586
|
+
language. Changing those commands or the language refreshes the menu even if `text` stays the same.
|
|
587
|
+
|
|
588
|
+
Call `slash.handleKeyDown(event)` first in the field's key handler. If it returns `true`, stop
|
|
589
|
+
your own handler. Arrows move the selection; Enter picks it. Tab completes the name without
|
|
590
|
+
running it. Escape hides the menu until the query changes. Shift+Enter stays yours. During IME
|
|
591
|
+
composition (when a keyboard combines keystrokes into a character), the hook lets the browser
|
|
592
|
+
finish that character without running or sending anything.
|
|
593
|
+
|
|
594
|
+
Render the rows in your own menu. Keep focus in the field with the browser's `preventDefault`
|
|
595
|
+
on mouse down; use click for the action so keyboard and screen-reader clicks work too:
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
return slash.open ? (
|
|
599
|
+
<div id={slash.listId} role="listbox" aria-label="Commands">
|
|
600
|
+
{slash.items.map((command, index) => (
|
|
601
|
+
<button
|
|
602
|
+
key={command.name}
|
|
603
|
+
id={slash.itemId(command)}
|
|
604
|
+
type="button"
|
|
605
|
+
role="option"
|
|
606
|
+
tabIndex={-1}
|
|
607
|
+
aria-selected={slash.index === index}
|
|
608
|
+
onMouseEnter={() => slash.highlight(index)}
|
|
609
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
610
|
+
onClick={(event) => slash.pick(command, event.altKey)}
|
|
611
|
+
>
|
|
612
|
+
/{command.name}: {command.description}
|
|
613
|
+
</button>
|
|
614
|
+
))}
|
|
615
|
+
</div>
|
|
616
|
+
) : null;
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Give the field `role="combobox"`, `aria-autocomplete="list"`, `aria-expanded={slash.open}`,
|
|
620
|
+
`aria-controls={slash.open ? slash.listId : undefined}` and `aria-activedescendant={slash.activeId}`.
|
|
621
|
+
Translate the menu label too. If there are no rows, explain how to find commands and offer a
|
|
622
|
+
"See commands" button that calls `slash.showAll()`. It opens the full list, even after Escape.
|
|
623
|
+
`onError` is required: show what was not recognized, or which names share an alias, with the
|
|
624
|
+
same way back to the list.
|
|
625
|
+
|
|
626
|
+
Set `takesArg: true` when a command accepts an argument. Selecting it, or submitting its bare
|
|
627
|
+
alias, completes `/name ` without running it. Completion keeps arguments already typed.
|
|
628
|
+
`parseCommand` leaves multiline text as a normal message.
|
|
629
|
+
|
|
630
|
+
Call `submit()` before every normal send, queue or quota check, including button and imperative
|
|
631
|
+
paths. It returns `true` for commands it ran, completed or rejected, so unknown commands never
|
|
632
|
+
silently become model messages. It returns `false` for ordinary text. The hook never sends a
|
|
633
|
+
message, clears a draft or changes attached files; those stay with the app. It also has no timer
|
|
634
|
+
or scheduler: offer a repeat command only when your app has real scheduling behind it.
|
|
635
|
+
|
|
500
636
|
## Also here
|
|
501
637
|
|
|
502
638
|
`ErrorBoundary`, an SSE reader split into a platform-free parser and a stream wrapper,
|
|
@@ -511,13 +647,13 @@ Detection walks the browser's language list in the reader's order, exact tag fir
|
|
|
511
647
|
subtag: `[pt-PT, en]` gets `pt-BR` when that is what you ship, and `[en-GB, pt-BR]` gets `en`.
|
|
512
648
|
`localeGateScript` uses the same rule, so the gate and the app agree about a reader.
|
|
513
649
|
|
|
514
|
-
`createAuthStore` is at `@gusnips/react/store
|
|
650
|
+
`createAuthStore` is at `@gusnips/react/store`: a zustand store whose `isLoading` starts `false`
|
|
515
651
|
where there is no window. A session bootstrap can only be in flight in a browser, and `true`
|
|
516
652
|
during a build is a wait that never ends: it once shipped a spinner as the indexable body of a
|
|
517
653
|
page whose whole job was to be found.
|
|
518
654
|
|
|
519
655
|
When your store needs more than those flags, `authSlice` in the same place gives you them as a
|
|
520
|
-
plain object to spread, so `create` stays yours
|
|
656
|
+
plain object to spread, so `create` stays yours, and with it `persist`, your own actions, and
|
|
521
657
|
fields of your own:
|
|
522
658
|
|
|
523
659
|
```ts
|
|
@@ -537,7 +673,7 @@ writes you no longer own.
|
|
|
537
673
|
## On a phone
|
|
538
674
|
|
|
539
675
|
The main entry works in React Native. It imports `react` and nothing else you would have to go
|
|
540
|
-
find
|
|
676
|
+
find (no `react-dom`, no router, no store), so the fetch client, the retry rule and the SSE
|
|
541
677
|
parser all come along, and the four things that need a browser stay behind the subpaths above.
|
|
542
678
|
`useCopy` imports fine there but has no `navigator.clipboard` to write to, so every copy answers
|
|
543
679
|
`"failed"`: use the platform's clipboard on a phone.
|
|
@@ -555,8 +691,8 @@ no wrapper added scroll lock, focus trap, ESC, outside-dismiss, focus return, ro
|
|
|
555
691
|
typeahead. Base UI already does all of that, and a wrapper "adding" them is a second
|
|
556
692
|
implementation of something that works. Twenty were dropped.
|
|
557
693
|
|
|
558
|
-
What the remaining seven buy is composition you cannot skip
|
|
559
|
-
dialog
|
|
694
|
+
What the remaining seven buy is composition you cannot skip (no caller can ship a scrimless
|
|
695
|
+
dialog), required a11y props expressed as types, and a handful of facts that each cost somebody
|
|
560
696
|
an afternoon. z-index goes on the Viewport, not the Popup, because `position: fixed` makes a
|
|
561
697
|
stacking context. A drawer _ties_ with dialog rather than beating it, or a modal opened from
|
|
562
698
|
inside a drawer never paints. A wrapper earns its place by knowing something, not by styling
|
|
@@ -577,9 +713,10 @@ lives behind a subpath, so you install a dependency only if you import the thing
|
|
|
577
713
|
| `@gusnips/react/ui` | the seven wrappers | @base-ui/react |
|
|
578
714
|
| `@gusnips/react/contract` | two prerender names | nothing |
|
|
579
715
|
| `@gusnips/react/theme` | `createTheme`, `startTheme` | react |
|
|
716
|
+
| `@gusnips/react/commands` | slash matching and `useSlashCommands` | react |
|
|
580
717
|
|
|
581
718
|
The rule behind that table: **a peer marked optional must not be reachable from the main entry
|
|
582
|
-
point.** An optional peer the barrel imports anyway is not optional
|
|
719
|
+
point.** An optional peer the barrel imports anyway is not optional: it is a required one whose
|
|
583
720
|
error moved from install time to your first build, which is the worse of the two places to learn
|
|
584
721
|
about it.
|
|
585
722
|
|
package/dist/api-client.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { ApiError } from "./api-error.ts";
|
|
|
3
3
|
/**
|
|
4
4
|
* The app's one door to the API. Nothing else should call `fetch`.
|
|
5
5
|
*
|
|
6
|
-
* Every route answers the same envelope, so unwrapping and error-shaping belong here once
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Every route answers the same envelope, so unwrapping and error-shaping belong here once rather
|
|
7
|
+
* than in every hook. Three donors built this independently and all three arrived at single-flight
|
|
8
|
+
* token refresh. See {@link SessionAdapter.refresh} for the reason, which is the sharpest thing in
|
|
9
|
+
* this file.
|
|
10
10
|
*
|
|
11
11
|
* Four things deliberately did NOT come across from the donors, because they belong to a
|
|
12
12
|
* product and not to a client:
|
|
@@ -14,7 +14,7 @@ import { ApiError } from "./api-error.ts";
|
|
|
14
14
|
* - **Toasts.** Whether a failure is spoken aloud is a design decision that differs per surface.
|
|
15
15
|
* Throw, and let the caller decide.
|
|
16
16
|
* - **In-flight GET deduplication.** react-query already dedups by key, so the client's copy is
|
|
17
|
-
* redundant
|
|
17
|
+
* redundant. One donor's version also returned a `clone()` of a body already being consumed,
|
|
18
18
|
* which is a race with no symptom until it has one.
|
|
19
19
|
* - **Service-status tracking and impersonation.** One donor's, and firmly its own.
|
|
20
20
|
* - **Analytics correlation headers.** Injected through {@link ApiClientOptions.headers}.
|
|
@@ -23,8 +23,8 @@ import { ApiError } from "./api-error.ts";
|
|
|
23
23
|
export interface RefreshResult {
|
|
24
24
|
token: string | null;
|
|
25
25
|
/**
|
|
26
|
-
* False when the refresh never got an ANSWER out of the auth server
|
|
27
|
-
* a 5xx
|
|
26
|
+
* False when the refresh never got an ANSWER out of the auth server because of a dropped
|
|
27
|
+
* connection, a 5xx or a captive portal.
|
|
28
28
|
*
|
|
29
29
|
* This flag is invariant 3, and only one of three donors had it. Losing a packet says nothing
|
|
30
30
|
* about whether a session is still good, so a client that collapses "auth said no" into
|
|
@@ -35,8 +35,8 @@ export interface RefreshResult {
|
|
|
35
35
|
reachedAuth: boolean;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* The seam onto whatever holds the session. Supabase in every donor so far, but nothing here
|
|
39
|
-
*
|
|
38
|
+
* The seam onto whatever holds the session. Supabase in every donor so far, but nothing here knows
|
|
39
|
+
* that. An adapter is four lines and keeps the auth vendor out of this package.
|
|
40
40
|
*/
|
|
41
41
|
export interface SessionAdapter {
|
|
42
42
|
getToken(): Promise<string | null>;
|
|
@@ -44,9 +44,9 @@ export interface SessionAdapter {
|
|
|
44
44
|
signOut(): Promise<void>;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* How long anything here waits on a sign-out: the default for
|
|
48
|
-
*
|
|
49
|
-
* `@gusnips/react/supabase`. Shared so the two give up together
|
|
47
|
+
* How long anything here waits on a sign-out: the default for {@link
|
|
48
|
+
* ApiClientOptions.signOutTimeoutMs}, and the deadline of `signOutEvenOffline` in
|
|
49
|
+
* `@gusnips/react/supabase`. Shared so the two give up together. A redirect that gave up sooner
|
|
50
50
|
* would navigate away while the session was still in storage.
|
|
51
51
|
*/
|
|
52
52
|
export declare const SIGN_OUT_TIMEOUT_MS = 3000;
|
|
@@ -64,54 +64,54 @@ export interface ApiClientOptions {
|
|
|
64
64
|
*/
|
|
65
65
|
headers?: () => Record<string, string>;
|
|
66
66
|
/**
|
|
67
|
-
* The session is gone and cannot be renewed
|
|
67
|
+
* The session is gone and cannot be renewed. Send them to sign in.
|
|
68
68
|
*
|
|
69
69
|
* The client does not know the route, and it deliberately still THROWS after calling this:
|
|
70
70
|
* `location.replace()` does not stop the current frame, so a caller's own `onError` must
|
|
71
71
|
* still run or a half-finished screen keeps rendering against data that will never arrive.
|
|
72
72
|
* The error it throws carries `expected: true`.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* Routing to the sign-in screen without a reload is equally supported. It is the case that keeps
|
|
75
|
+
* this client alive across the next sign-in. The sign-out latch is keyed on the refused token for
|
|
76
|
+
* exactly that reason; see `refusedToken`.
|
|
77
77
|
*/
|
|
78
78
|
onSessionDead: () => void;
|
|
79
79
|
/**
|
|
80
80
|
* Every failed response, seen once, just before it is thrown.
|
|
81
81
|
*
|
|
82
|
-
* For a reaction that belongs to the whole app rather than to one call site. The donor's case
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
82
|
+
* For a reaction that belongs to the whole app rather than to one call site. The donor's case is
|
|
83
|
+
* the sharp one: a mid-session account suspension 403s every authed route except `GET /auth/me`,
|
|
84
|
+
* so the moment one arrives the app has to refresh `me` and route to the screen that explains it.
|
|
85
|
+
* Otherwise every query on the page fails at once and the shell half-renders behind an error
|
|
86
|
+
* storm until `me` goes stale on its own.
|
|
87
87
|
*
|
|
88
|
-
* It cannot live at a call site, because the point is that it fires from whichever call
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* It cannot live at a call site, because the point is that it fires from whichever call happened
|
|
89
|
+
* to be first. It went in the donor's client directly, which made the client import its query
|
|
90
|
+
* cache and its query keys, creating a cycle that this hook removes.
|
|
91
91
|
*
|
|
92
92
|
* Observation only: the error is thrown either way, and throwing from here would replace a
|
|
93
93
|
* real API failure with whatever the listener hit.
|
|
94
94
|
*
|
|
95
95
|
* **The response comes with it, because a refusal's headers are otherwise unreachable.**
|
|
96
|
-
* `request` throws before the `Response` escapes, so an adopter whose API states something on
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
96
|
+
* `request` throws before the `Response` escapes, so an adopter whose API states something on the
|
|
97
|
+
* error path had no way to read it at all. One reports what every call cost, refusals included,
|
|
98
|
+
* which is how "a blocked page costs nothing" becomes a thing a customer can check rather than a
|
|
99
|
+
* thing we assert. That is the distinction worth keeping: field-error flattening and a per-call
|
|
100
|
+
* meter callback both stayed in their products, because a product can write those itself. This
|
|
101
|
+
* one it cannot, at any price, and a capability the kit makes unreachable is a regression the kit
|
|
102
|
+
* has to undo.
|
|
103
103
|
*
|
|
104
|
-
* The BODY has already been read by the time this runs
|
|
105
|
-
*
|
|
104
|
+
* The BODY has already been read by the time this runs to build the error from the envelope, so
|
|
105
|
+
* `response.json()` here throws. The headers are what is left, and what this is for.
|
|
106
106
|
*/
|
|
107
107
|
onError?: (error: ApiError, response: Response) => void;
|
|
108
108
|
/**
|
|
109
109
|
* Abort a request that has not answered. Default 30s.
|
|
110
110
|
*
|
|
111
|
-
* Neither donor bounded its authenticated requests
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
111
|
+
* Neither donor bounded its authenticated requests. Both bounded only their keyless clients, so a
|
|
112
|
+
* stalled connection was a spinner with no end. providerkit learned the same lesson about
|
|
113
|
+
* streams: a request with no deadline is a bug that only shows up on a bad network, which is
|
|
114
|
+
* exactly when nobody can reproduce it.
|
|
115
115
|
*/
|
|
116
116
|
timeoutMs?: number;
|
|
117
117
|
/** How many times to refresh-and-retry a 401. Default 2. */
|
|
@@ -121,9 +121,9 @@ export interface ApiClientOptions {
|
|
|
121
121
|
/**
|
|
122
122
|
* How long to wait for `signOut()` before redirecting anyway. Default 3s.
|
|
123
123
|
*
|
|
124
|
-
* A fail-safe, and one donor added it after the failure it prevents: awaiting `signOut()`
|
|
125
|
-
*
|
|
126
|
-
*
|
|
124
|
+
* A fail-safe, and one donor added it after the failure it prevents: awaiting `signOut()` covers
|
|
125
|
+
* a rejection but not a HANG, and a hang leaves the tab signed out in name only: still on the
|
|
126
|
+
* page, every request 401ing, nothing left that could redirect it.
|
|
127
127
|
*/
|
|
128
128
|
signOutTimeoutMs?: number;
|
|
129
129
|
/** Header carrying the server's request id, echoed onto {@link ApiError.requestId}. */
|
|
@@ -143,9 +143,9 @@ export interface ApiClient {
|
|
|
143
143
|
put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
144
144
|
patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
|
|
145
145
|
/**
|
|
146
|
-
* DELETE, discarding whatever comes back. Returns `void` because 204 is the usual answer and
|
|
147
|
-
*
|
|
148
|
-
*
|
|
146
|
+
* DELETE, discarding whatever comes back. Returns `void` because 204 is the usual answer and a
|
|
147
|
+
* 204 has NO BODY. `res.json()` on one throws `Unexpected end of JSON input`, which reads like a
|
|
148
|
+
* malformed response and is really just a success nobody was allowed to parse.
|
|
149
149
|
*
|
|
150
150
|
* Split from {@link ApiClient.delJson} rather than guessing per response, because the guess is
|
|
151
151
|
* the part that hides a bug: a route that was supposed to answer with data and returned
|
|
@@ -153,21 +153,21 @@ export interface ApiClient {
|
|
|
153
153
|
* layers away. The donor that hit this had written exactly these two functions.
|
|
154
154
|
*/
|
|
155
155
|
del(path: string, options?: RequestOptions): Promise<void>;
|
|
156
|
-
/** The DELETE that answers with something worth reading
|
|
156
|
+
/** The DELETE that answers with something worth reading, such as a queue entry it handed back. */
|
|
157
157
|
delJson<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
158
158
|
/**
|
|
159
|
-
* The whole envelope rather than just `data`, for a route whose `meta` is half the answer
|
|
160
|
-
* a list's `total
|
|
159
|
+
* The whole envelope rather than just `data`, for a route whose `meta` is half the answer, such
|
|
160
|
+
* as a list's `total` or a metered call's receipt.
|
|
161
161
|
*/
|
|
162
162
|
page<T, M>(path: string, options?: RequestOptions): Promise<ApiSuccess<T, M>>;
|
|
163
163
|
/**
|
|
164
164
|
* The same, off a POST.
|
|
165
165
|
*
|
|
166
|
-
* Two adopters needed this and both wrote the same workaround: `client.request(…)` followed by
|
|
167
|
-
*
|
|
168
|
-
* assertion about the success shape. One of them left the reason sitting in a comment
|
|
169
|
-
*
|
|
170
|
-
*
|
|
166
|
+
* Two adopters needed this and both wrote the same workaround: `client.request(…)` followed by a
|
|
167
|
+
* hand-rolled `res.json()`, stepping around the client's own envelope reader and its one
|
|
168
|
+
* assertion about the success shape. One of them left the reason sitting in a comment: "the whole
|
|
169
|
+
* envelope is reachable off a GET and not off a POST, so this one reads the body itself". This
|
|
170
|
+
* package's gap was described in somebody else's file.
|
|
171
171
|
*
|
|
172
172
|
* A metered route takes a body whenever its input is too long or too punctuated to be a path
|
|
173
173
|
* segment (a URL, a search query), and then the receipt has to survive the POST too. When the
|
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAE5D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnC,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC;;;;;;;;;;;OAWG;IACH,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IACxD;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,0FAA0F;IAC1F,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnE,0CAA0C;IAC1C,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3D,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5E,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3E,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E;;;;;;;;;OASG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/api-client.js
CHANGED
|
@@ -2,9 +2,9 @@ import { isApiError } from "@gusnips/http";
|
|
|
2
2
|
import { parseRetryAfter } from "@gusnips/http/retry";
|
|
3
3
|
import { ApiError } from "./api-error.js";
|
|
4
4
|
/**
|
|
5
|
-
* How long anything here waits on a sign-out: the default for
|
|
6
|
-
*
|
|
7
|
-
* `@gusnips/react/supabase`. Shared so the two give up together
|
|
5
|
+
* How long anything here waits on a sign-out: the default for {@link
|
|
6
|
+
* ApiClientOptions.signOutTimeoutMs}, and the deadline of `signOutEvenOffline` in
|
|
7
|
+
* `@gusnips/react/supabase`. Shared so the two give up together. A redirect that gave up sooner
|
|
8
8
|
* would navigate away while the session was still in storage.
|
|
9
9
|
*/
|
|
10
10
|
export const SIGN_OUT_TIMEOUT_MS = 3_000;
|