@rangojs/router 0.33.1 → 0.34.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/bin/{chunk-TTXC2U3G.js → chunk-4HHFQSDC.js} +10 -2
- package/dist/bin/{chunk-3B7CKBRO.js → chunk-MWOHBDPM.js} +9 -9
- package/dist/bin/{client-urls-projection-DQLWPDAT.js → client-urls-projection-4Q5EQDAL.js} +2 -2
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-EPUDEHO4.js → runtime-6WLCJCN7.js} +1 -1
- package/dist/bin/{runtime-discovery-3HH6BP3K.js → runtime-discovery-2ZTYRZ3V.js} +2 -2
- package/dist/bin/{temp-rsc-server-JALTIWLT.js → temp-rsc-server-EUNKRFJD.js} +3 -3
- package/dist/testing/vitest.js +5 -0
- package/dist/types/browser/history-state.d.ts +21 -0
- package/dist/types/browser/navigation-transaction.d.ts +2 -0
- package/dist/types/client-urls/server-projection.d.ts +7 -0
- package/dist/types/client-urls/types.d.ts +13 -14
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/loader-store.d.ts +11 -1
- package/dist/types/prefetch-markers.d.ts +13 -4
- package/dist/types/route-definition/helpers-types.d.ts +14 -10
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +39 -21
- package/dist/types/server/context.d.ts +6 -5
- package/dist/types/testing/render-route.d.ts +21 -1
- package/dist/types/testing/serve-shell-request.d.ts +7 -7
- package/dist/types/testing/vitest-stubs/plugin-rsc-browser.d.ts +33 -0
- package/dist/types/types/loader-types.d.ts +10 -8
- package/dist/types/urls/path-helper-types.d.ts +9 -8
- package/dist/vite/index.js +30 -13
- package/package.json +1 -1
- package/skills/app-structure/large-app.md +3 -0
- package/skills/client-urls/SKILL.md +13 -10
- package/skills/hooks/data.md +5 -0
- package/skills/hooks/state.md +13 -11
- package/skills/intercept/SKILL.md +8 -4
- package/skills/links/SKILL.md +15 -12
- package/skills/loader/SKILL.md +52 -39
- package/skills/ppr/SKILL.md +1 -1
- package/skills/route/SKILL.md +15 -9
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +67 -19
- package/skills/testing/serve-shell-request.md +19 -7
- package/skills/ux-architecture/SKILL.md +16 -11
- package/src/browser/history-state.ts +48 -0
- package/src/browser/navigation-bridge.ts +12 -1
- package/src/browser/navigation-transaction.ts +14 -6
- package/src/browser/partial-update.ts +12 -4
- package/src/browser/prefetch/fetch.ts +3 -3
- package/src/client-urls/client-urls.ts +11 -7
- package/src/client-urls/server-projection.ts +21 -3
- package/src/client-urls/types.ts +13 -14
- package/src/errors.ts +17 -0
- package/src/loader-store.ts +41 -4
- package/src/prefetch-markers.ts +18 -4
- package/src/route-definition/dsl-helpers.ts +13 -6
- package/src/route-definition/helpers-types.ts +14 -10
- package/src/router/loader-resolution.ts +4 -4
- package/src/router/match-api.ts +1 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +108 -50
- package/src/rsc/rsc-rendering.ts +1 -1
- package/src/server/context.ts +6 -5
- package/src/testing/render-route.tsx +122 -7
- package/src/testing/serve-shell-request.ts +7 -7
- package/src/testing/vitest-stubs/plugin-rsc-browser.ts +61 -0
- package/src/testing/vitest.ts +5 -0
- package/src/types/loader-types.ts +10 -8
- package/src/urls/path-helper-types.ts +9 -8
- package/src/use-loader.tsx +29 -7
package/skills/loader/SKILL.md
CHANGED
|
@@ -1191,20 +1191,22 @@ The costs and constraints:
|
|
|
1191
1191
|
Also available in `clientUrls()` route groups (`/client-urls`), where the
|
|
1192
1192
|
loader-heavy shape makes it most useful.
|
|
1193
1193
|
|
|
1194
|
-
## `prefetch: false` — Keep a Loader Out of
|
|
1194
|
+
## `prefetch: false` and `prefetch: "intent"` — Keep a Loader Out of Prefetches
|
|
1195
1195
|
|
|
1196
1196
|
A `<Link>` prefetch is a navigation sent early: the server renders every
|
|
1197
1197
|
handler and every loader for it. With the default `"viewport"` strategy that
|
|
1198
1198
|
means a grid of forty product links runs forty copies of your slowest loader
|
|
1199
1199
|
for pages most visitors never open. Pass `prefetch: false` to keep one loader
|
|
1200
|
-
out of
|
|
1200
|
+
out of every prefetch, or `prefetch: "intent"` to keep it out of the
|
|
1201
|
+
speculative ones only:
|
|
1201
1202
|
|
|
1202
1203
|
```tsx
|
|
1203
1204
|
import { Suspense } from "react";
|
|
1204
1205
|
|
|
1205
1206
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
1206
|
-
loader(PriceLoader), // cheap:
|
|
1207
|
-
loader(
|
|
1207
|
+
loader(PriceLoader), // cheap: runs in every prefetch
|
|
1208
|
+
loader(RelatedLoader, { prefetch: "intent" }), // skipped in a viewport prefetch, runs on hover
|
|
1209
|
+
loader(ReviewsLoader, { prefetch: false }), // expensive: skipped in every prefetch
|
|
1208
1210
|
loading(<ProductSkeleton />),
|
|
1209
1211
|
]),
|
|
1210
1212
|
```
|
|
@@ -1212,22 +1214,30 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
1212
1214
|
(`prefetch` sits beside `ssr` in `LoaderOptions`; combine them as
|
|
1213
1215
|
`{ ssr: false, prefetch: false }` if you need both.)
|
|
1214
1216
|
|
|
1215
|
-
|
|
1217
|
+
| Request | unflagged | `prefetch: false` | `prefetch: "intent"` |
|
|
1218
|
+
| ------------------------------------------------------------ | ---------- | ------------------------ | ------------------------ |
|
|
1219
|
+
| Document, navigation | runs | runs | runs |
|
|
1220
|
+
| Viewport or render prefetch (`X-Rango-Prefetch: 1`) | runs | skipped, filled at click | skipped, filled at click |
|
|
1221
|
+
| Hover, `useRouter().prefetch()` (`X-Rango-Prefetch: intent`) | runs | skipped, filled at click | runs, no fill |
|
|
1222
|
+
| A prefetch already cached or in flight when the hover lands | used as is | used as is | used as is |
|
|
1223
|
+
|
|
1224
|
+
A hover means the visitor is about to click. A hover prefetch (on a
|
|
1216
1225
|
`"hover"`, `"viewport"` or `"adaptive"` link, or a delegated plain anchor) and
|
|
1217
|
-
`useRouter().prefetch()`
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1226
|
+
`useRouter().prefetch()` run a loader marked `"intent"` with the rest of the
|
|
1227
|
+
page; when every flagged loader and `loading()` on the route is marked
|
|
1228
|
+
`"intent"`, the click commits complete and sends no second request. A hover
|
|
1229
|
+
sends its prefetch only when the link has none cached or in flight: on a link
|
|
1230
|
+
a viewport prefetch already answered, the hover sends nothing and the click
|
|
1231
|
+
fills the deferred loader as below. The cost of `"intent"` is plain: each
|
|
1232
|
+
hover that misses the prefetch cache runs the loader, so a pointer passing
|
|
1233
|
+
over a list of `"hover"` links runs it once per link. Keep a loader that is
|
|
1234
|
+
too expensive for that on `false`. Touch devices have no hover (`"adaptive"`
|
|
1235
|
+
resolves to viewport there) and keep the fill.
|
|
1236
|
+
|
|
1237
|
+
What happens in a prefetch that skips the loader:
|
|
1238
|
+
|
|
1239
|
+
- The prefetch does not run the loader and does not read its `cache()`; the
|
|
1240
|
+
payload marks it deferred. The rest of the page prefetches as usual.
|
|
1231
1241
|
- Only on a loader new to the page. A loader already on screen is never
|
|
1232
1242
|
deferred: if its revalidation (its `revalidate()` or the default) re-runs
|
|
1233
1243
|
it, it runs in the prefetch with the rest of the destination, exactly as
|
|
@@ -1277,18 +1287,18 @@ function Reviews() {
|
|
|
1277
1287
|
|
|
1278
1288
|
The limits:
|
|
1279
1289
|
|
|
1280
|
-
- **A click before its
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
prefetch
|
|
1286
|
-
flag, and sends the fill when the prefetch answers. The page shows no later
|
|
1290
|
+
- **A click before its prefetch has answered waits for it, then fetches the
|
|
1291
|
+
deferred part.** The flag is there to keep expensive or slow work out of
|
|
1292
|
+
prefetches, so that an API that is costly to call is not called for every
|
|
1293
|
+
link a visitor might click. A click that comes while the link's prefetch is
|
|
1294
|
+
still in flight reuses that prefetch, as it does without the flag, and
|
|
1295
|
+
sends the fill when the prefetch answers. The page shows no later
|
|
1287
1296
|
than with no prefetch; the deferred part shows its fallback until the fill
|
|
1288
1297
|
lands, and can complete later than a click with no prefetch, by up to what
|
|
1289
1298
|
was left of the prefetch. That is the price of keeping the work out of the
|
|
1290
|
-
prefetch. A click while a hover's prefetch is in flight
|
|
1291
|
-
fill: the loader is already
|
|
1299
|
+
prefetch. A click while a hover's prefetch is in flight, on a route whose
|
|
1300
|
+
flags are all `"intent"`, adopts it with no fill: the loader is already
|
|
1301
|
+
running in it.
|
|
1292
1302
|
- **A navigation started before the fill answers cancels it.** It is
|
|
1293
1303
|
cancelled when the newer navigation starts, not when that one commits. If
|
|
1294
1304
|
the newer navigation then ends without replacing the page (a redirect the
|
|
@@ -1313,8 +1323,9 @@ The limits:
|
|
|
1313
1323
|
every request kind (a document load included, so you see it the first time
|
|
1314
1324
|
you open the page in development) if it calls `await ctx.rendered()`. The
|
|
1315
1325
|
fill does not re-run the entry's handler, so there is no render to wait for
|
|
1316
|
-
and no handle data to read.
|
|
1317
|
-
|
|
1326
|
+
and no handle data to read. That holds for `"intent"` too, on every request.
|
|
1327
|
+
Remove the `prefetch` flag from the loader, or stop calling
|
|
1328
|
+
`ctx.rendered()`. Pushing a handle from the loader is fine.
|
|
1318
1329
|
- **`ctx.set()` values from held handlers are not visible.** The fill runs the
|
|
1319
1330
|
deferred work and middleware only. A handler or layout the client already
|
|
1320
1331
|
holds does not run again, so a variable it set with `ctx.set()` is missing
|
|
@@ -1376,16 +1387,17 @@ The limits:
|
|
|
1376
1387
|
need it), or drop the flag from a loader the page cannot render without.
|
|
1377
1388
|
|
|
1378
1389
|
- **`clientUrls()` routes take it on `loader()`.** In a `clientUrls()`
|
|
1379
|
-
definition `loader(Def, { prefetch: false })`
|
|
1390
|
+
definition `loader(Def, { prefetch: false })` and `{ prefetch: "intent" }`
|
|
1391
|
+
work as here: a click
|
|
1380
1392
|
inside the group shows the destination at once, commits the prefetched
|
|
1381
1393
|
data with the URL, and one fill runs the flagged loaders (see
|
|
1382
1394
|
`/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
|
|
1383
1395
|
on a group route, from the group or from a server layout above it.
|
|
1384
|
-
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
click opens the modal at once, with its `loading()`, and one fill,
|
|
1388
|
-
from the source, runs the loader. The page under the modal stays. With no
|
|
1396
|
+
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a prefetch of
|
|
1397
|
+
the intercept's target from its source page defers the flagged loader (a
|
|
1398
|
+
hover's runs a loader marked `"intent"`, and its click sends no fill for
|
|
1399
|
+
it): the click opens the modal at once, with its `loading()`, and one fill,
|
|
1400
|
+
sent from the source, runs the loader. The page under the modal stays. With no
|
|
1389
1401
|
`loading()` the slot is empty until the fill answers, as on a plain click
|
|
1390
1402
|
until its response. The modal's loaders resolve together: it shows its
|
|
1391
1403
|
`loading()` until every one has arrived. A flagged `loading()` inside
|
|
@@ -1401,13 +1413,14 @@ The limits:
|
|
|
1401
1413
|
`/caching`, `/prerender`, `/ppr` → "`prefetch: false` on a `ppr` route").
|
|
1402
1414
|
|
|
1403
1415
|
To skip a handler along with its loaders, flag the `loading()` instead (see
|
|
1404
|
-
`/route` → "`loading()` options"). `loader(Def, { prefetch
|
|
1405
|
-
|
|
1416
|
+
`/route` → "`loading()` options"). `loader(Def, { prefetch })` accepts a
|
|
1417
|
+
boolean or `"intent"`; anything else throws
|
|
1418
|
+
`loader() prefetch must be a boolean or "intent"`.
|
|
1406
1419
|
|
|
1407
1420
|
Test it with `serveShellRequest(router, url, { partial: { prefetch: true } })`
|
|
1408
1421
|
and `result.readDeferred()` (see `/testing`). That is a speculative prefetch;
|
|
1409
1422
|
add `headers: { "X-Rango-Prefetch": "intent" }` to serve a hover's, which
|
|
1410
|
-
defers
|
|
1423
|
+
runs the `"intent"` loaders and defers the `false` ones.
|
|
1411
1424
|
|
|
1412
1425
|
## Fetchable Loaders
|
|
1413
1426
|
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -1176,7 +1176,7 @@ document request captured:
|
|
|
1176
1176
|
| Replays a shell a document request captured (`HIT`) | Served from its pin, not deferred, never run; the click sends nothing |
|
|
1177
1177
|
| Replays a navigation-only shell, or misses (cold page) | Deferred: the click sends one fill that runs it |
|
|
1178
1178
|
| Serves an explicit `cache()` hit | Deferred: the click sends one fill that runs it |
|
|
1179
|
-
| Hover prefetch (`X-Rango-Prefetch: intent`) |
|
|
1179
|
+
| Hover prefetch (`X-Rango-Prefetch: intent`) | As the rows above; a loader marked `prefetch: "intent"` runs instead |
|
|
1180
1180
|
|
|
1181
1181
|
A cold prefetch still schedules the background capture that stores the
|
|
1182
1182
|
page's navigation shell, and that capture runs the loader once. Once a
|
package/skills/route/SKILL.md
CHANGED
|
@@ -242,10 +242,11 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
242
242
|
|
|
243
243
|
`loading(fallback, options?)` takes two options:
|
|
244
244
|
|
|
245
|
-
| Option
|
|
246
|
-
|
|
|
247
|
-
| `ssr: false`
|
|
248
|
-
| `prefetch: false`
|
|
245
|
+
| Option | Effect |
|
|
246
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
247
|
+
| `ssr: false` | Skip the fallback on document requests; it shows on client navigations only. |
|
|
248
|
+
| `prefetch: false` | Keep everything behind this fallback out of every prefetch, hovers included; the click shows the fallback and fetches that part itself. |
|
|
249
|
+
| `prefetch: "intent"` | The same for viewport and render prefetches; a hover prefetch or `useRouter().prefetch()` runs it, and its click sends no second request. |
|
|
249
250
|
|
|
250
251
|
```tsx
|
|
251
252
|
path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
@@ -255,13 +256,17 @@ path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
|
255
256
|
```
|
|
256
257
|
|
|
257
258
|
**What `prefetch: false` defers.** The flag applies when the entry is new to
|
|
258
|
-
the browser: the page the prefetch is sent from does not show it. A
|
|
259
|
-
|
|
259
|
+
the browser: the page the prefetch is sent from does not show it. A prefetch
|
|
260
|
+
then skips the entry's handler, its loaders, its orphan layouts and its
|
|
260
261
|
parallel slots. On a `layout()` the fallback covers the whole outlet, so every
|
|
261
262
|
deeper entry is skipped with it. A parallel slot with its own flagged
|
|
262
263
|
`loading()` is a separate unit: the route beside it still renders in the
|
|
263
264
|
prefetch. The prefetch payload carries a placeholder for each skipped piece,
|
|
264
|
-
marked deferred, with its fallback: the page's skeleton.
|
|
265
|
+
marked deferred, with its fallback: the page's skeleton. `prefetch: "intent"`
|
|
266
|
+
defers the same pieces in a viewport or render prefetch; a hover's prefetch
|
|
267
|
+
renders them, so a route whose flags are all `"intent"` is prefetched whole
|
|
268
|
+
on hover. Everything below about `false` holds for `"intent"` in the
|
|
269
|
+
prefetches that defer it.
|
|
265
270
|
|
|
266
271
|
**An entry the browser already holds is never deferred.** Inside a section
|
|
267
272
|
whose `layout()` has a flagged `loading()`, the layout is on screen, so its
|
|
@@ -347,7 +352,7 @@ the block's work, its loaders included, out of prefetches. See `/caching`,
|
|
|
347
352
|
flag does nothing there. The last `loading()` on an entry wins, flag
|
|
348
353
|
included.
|
|
349
354
|
- A loader behind the fallback cannot call `ctx.rendered()`; it throws on every
|
|
350
|
-
request kind. See `/loader` → "`prefetch: false`" for this and the other
|
|
355
|
+
request kind, for either flag. See `/loader` → "`prefetch: false`" for this and the other
|
|
351
356
|
loader limits (`ctx.set()` values, late `redirect()`/`notFound()`, a loader a
|
|
352
357
|
handler awaits).
|
|
353
358
|
- A layout the client already holds is not deferred (its fallback would not
|
|
@@ -373,7 +378,8 @@ the block's work, its loaders included, out of prefetches. See `/caching`,
|
|
|
373
378
|
- `clientUrls()` routes and `intercept()` ignore the flag.
|
|
374
379
|
|
|
375
380
|
To defer a single loader and keep the handler in the prefetch, use
|
|
376
|
-
`loader(Def, { prefetch: false })` instead (see
|
|
381
|
+
`loader(Def, { prefetch: false })` or `{ prefetch: "intent" }` instead (see
|
|
382
|
+
`/loader`).
|
|
377
383
|
|
|
378
384
|
A `loading()` skeleton that has appeared stays at least about 300 ms before React reveals what replaces it (React's fallback throttle, [#1078](https://github.com/rangojs/rango/issues/1078)); see "Streaming with Suspense" in [loader](../loader/SKILL.md).
|
|
379
385
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -101,6 +101,7 @@ Each primitive links to its sub-file (API + recipe + caveats).
|
|
|
101
101
|
| a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
|
|
102
102
|
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
103
103
|
| a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
|
|
104
|
+
| a component that refreshes with `useLoader().load()` or `useRefreshLoaders()`, and the readers that show the answer | unit (DOM) | [`renderRoute` + `router.respondToLoads`](./client-components.md#load-and-refresh-groups) | `@rangojs/router/testing/dom` |
|
|
104
105
|
| a component reading `useLocationState(Def)` after `router.push(url, { state: [Def(value)] })` | unit (DOM) | [`renderRoute` + `withLocationStateKey`](./client-components.md#location-state) | `@rangojs/router/testing[/dom]` |
|
|
105
106
|
| location state while a navigation is pending (a loader the test holds), or after a back/forward | unit (DOM) | [`navigate(url, { state, loaders })`, a `popstate` event](./client-components.md#state-while-a-navigation-is-pending) | `@rangojs/router/testing[/dom]` |
|
|
106
107
|
| a CLIENT component's hydration: its first client render against its own server HTML (`typeof window`, a late `<Suspense>`) | unit (DOM) | [`renderRoute` + `hydrate: true`](./client-components.md#hydration) | `@rangojs/router/testing/dom` |
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
|
|
34
34
|
### Context — client hooks it makes resolve (what your code receives)
|
|
35
35
|
|
|
36
|
-
| Hook | Meaning
|
|
37
|
-
| ------------------------------ |
|
|
38
|
-
| `useParams` | Params from the matched leaf pattern, with `options.params` merged over.
|
|
39
|
-
| `useReverse` | Reverse a name->pattern map to a URL; merges `useParams()` and the `mount`/`basename` prefix.
|
|
40
|
-
| `useHref` | Resolve an href against the mount/basename.
|
|
41
|
-
| `useMount` | The `include()` mount prefix (`options.mount`), else `"/"`.
|
|
42
|
-
| `useNavigation` | Navigation controller state: `loading` from a `navigate()` call to its commit, else `idle`.
|
|
43
|
-
| `useRouter` | The router handle, including `.basename`.
|
|
44
|
-
| `usePathname` | Current committed pathname.
|
|
45
|
-
| `useSearchParams` | Search params from the `request` URL.
|
|
46
|
-
| `useNonce` | SEEDED CSP nonce (`options.nonce`), else `undefined` (the browser default).
|
|
47
|
-
| `useLoader` / `useFetchLoader` | SEEDED loader data (read path, not run path). Held-navigation `isLoading` is modeled (below). |
|
|
48
|
-
| `useLocationState` | SEEDED `history.state` value, or the `state` a navigation or a `popstate` brought (below).
|
|
49
|
-
| `useHandle` | SEEDED handle output (globally accumulated).
|
|
50
|
-
| `Outlet` | Renders the next segment in the chain (layout nesting).
|
|
51
|
-
| `useOutlet` | Next-segment `content` plus SEEDED `options.outletPending`.
|
|
52
|
-
| `useTheme` | Theme; throws without `options.theme` (see caveat).
|
|
36
|
+
| Hook | Meaning |
|
|
37
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `useParams` | Params from the matched leaf pattern, with `options.params` merged over. |
|
|
39
|
+
| `useReverse` | Reverse a name->pattern map to a URL; merges `useParams()` and the `mount`/`basename` prefix. |
|
|
40
|
+
| `useHref` | Resolve an href against the mount/basename. |
|
|
41
|
+
| `useMount` | The `include()` mount prefix (`options.mount`), else `"/"`. |
|
|
42
|
+
| `useNavigation` | Navigation controller state: `loading` from a `navigate()` call to its commit, else `idle`. |
|
|
43
|
+
| `useRouter` | The router handle, including `.basename`. |
|
|
44
|
+
| `usePathname` | Current committed pathname. |
|
|
45
|
+
| `useSearchParams` | Search params from the `request` URL. |
|
|
46
|
+
| `useNonce` | SEEDED CSP nonce (`options.nonce`), else `undefined` (the browser default). |
|
|
47
|
+
| `useLoader` / `useFetchLoader` | SEEDED loader data (read path, not run path). Held-navigation `isLoading` is modeled (below), and `load()` is answered from the seeds ([`load()`](#load-and-refresh-groups)). |
|
|
48
|
+
| `useLocationState` | SEEDED `history.state` value, or the `state` a navigation or a `popstate` brought (below). |
|
|
49
|
+
| `useHandle` | SEEDED handle output (globally accumulated). |
|
|
50
|
+
| `Outlet` | Renders the next segment in the chain (layout nesting). |
|
|
51
|
+
| `useOutlet` | Next-segment `content` plus SEEDED `options.outletPending`. |
|
|
52
|
+
| `useTheme` | Theme; throws without `options.theme` (see caveat). |
|
|
53
53
|
|
|
54
54
|
### Returns — `RenderRouteResult`
|
|
55
55
|
|
|
@@ -77,8 +77,11 @@ type RenderRouteResult = RenderResult & {
|
|
|
77
77
|
},
|
|
78
78
|
): Promise<void>;
|
|
79
79
|
// re-render the current location, as router.refresh() does:
|
|
80
|
-
// a spec's when gets kind "revalidate" (`to` is `from`)
|
|
81
|
-
|
|
80
|
+
// a spec's when gets kind "revalidate" (`to` is `from`); `loaders` is the
|
|
81
|
+
// data the response carries
|
|
82
|
+
refresh(options?: {
|
|
83
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
84
|
+
}): Promise<void>;
|
|
82
85
|
// keep `url` as a prefetch made now carries it (the seeds then, `loaders`
|
|
83
86
|
// over them); a later navigate(url) without `loaders` commits that data
|
|
84
87
|
prefetch(
|
|
@@ -94,6 +97,11 @@ type RenderRouteResult = RenderResult & {
|
|
|
94
97
|
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
95
98
|
keepClientCache?: boolean;
|
|
96
99
|
}): Promise<void>;
|
|
100
|
+
// what a later useLoader().load() (or refreshGroup refresh) of each
|
|
101
|
+
// loader resolves to; renders nothing
|
|
102
|
+
respondToLoads(options: {
|
|
103
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
104
|
+
}): void;
|
|
97
105
|
pathname(): string;
|
|
98
106
|
params(): Record<string, string>;
|
|
99
107
|
store: NavigationStore; // advanced
|
|
@@ -454,6 +462,46 @@ it("dims the held price while the next product streams", async () => {
|
|
|
454
462
|
- Pass settled data as a plain value, not `Promise.resolve(value)`: React has not observed a plain promise yet, so it counts as pending and renders one `stale` frame. (A Flight chunk that has already settled carries its status and is skipped, which a plain value mirrors.)
|
|
455
463
|
- `loading()` is not a spec field: a `<Suspense>` in your layout component stands in for it.
|
|
456
464
|
|
|
465
|
+
## `load()` and refresh groups
|
|
466
|
+
|
|
467
|
+
`useLoader().load()`, `useFetchLoader().load()` and a `useRefreshLoaders()` refresh reach renderRoute's loader endpoint, so a refresh button, a polling component, or a value one reader refreshes and another shows is unit-testable without a server or a `fetch` stub. The endpoint answers a GET for a loader with the last value the test gave it: the render-time `loaders`/`loaderData`, or `router.respondToLoads({ loaders })`, which only sets the answer (a `navigate()`, `prefetch()`, `action()` or `refresh()` seeds the page it commits, not the answer). A `load({ params })` is answered with a `loaderError`. The value is read when `load()` sends its request. A pending Promise holds the answer until it settles (`isLoading` stays `true`), and a rejected one arrives as the loader's error. The value reaches the component as given, never serialized.
|
|
468
|
+
|
|
469
|
+
The answer decodes through `@vitejs/plugin-rsc/browser`, which `rangoTestAliases()` (and `rangoTestConfig()`) alias to the testing kit's stub; remove any `vi.mock("@vitejs/plugin-rsc/browser")` you wrote for it.
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
473
|
+
import { renderRoute } from "@rangojs/router/testing/dom";
|
|
474
|
+
import { useLoader } from "@rangojs/router/client";
|
|
475
|
+
import { CartLoader } from "../src/loaders"; // createLoader(fn, true)
|
|
476
|
+
|
|
477
|
+
function CartBadge() {
|
|
478
|
+
const { data, load, isLoading } = useLoader(CartLoader);
|
|
479
|
+
return (
|
|
480
|
+
<button data-testid="cart" onClick={() => load()}>
|
|
481
|
+
{isLoading ? "..." : data.count}
|
|
482
|
+
</button>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
it("refreshes the count from the server", async () => {
|
|
487
|
+
const { getByTestId, router } = await renderRoute(
|
|
488
|
+
[{ path: "/cart", Component: CartBadge }],
|
|
489
|
+
{ request: "/cart", loaders: [[CartLoader, { count: 1 }]] },
|
|
490
|
+
);
|
|
491
|
+
router.respondToLoads({ loaders: [[CartLoader, { count: 3 }]] });
|
|
492
|
+
fireEvent.click(getByTestId("cart"));
|
|
493
|
+
// load() loads its decoder lazily: wait for the answer, not one act()
|
|
494
|
+
await waitFor(() => expect(getByTestId("cart").textContent).toBe("3"));
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
| `load()` call | Under `renderRoute` |
|
|
499
|
+
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
500
|
+
| `load()` with no options, a `key`, or a `refreshGroup` | Answered. Readers of a registered loader converge on the value; a reader that mounts later, or that a navigation brings new route data, shows that route data instead, as in production. |
|
|
501
|
+
| `load({ params })`, `load({ method: "POST", body })` | NOT answered: the request goes to the `fetch` your test environment has. Test the loader body with `runLoader`, and the round trip at e2e. |
|
|
502
|
+
| A loader the test never gave a value | Answered with an error naming the loader (`error` set; `throwOnError` throws it). |
|
|
503
|
+
| Any other request (a navigation's or action's Flight payload) | Passed through to the `fetch` installed before the mount. renderRoute never decodes one: a `navigate()` or `action()` seeds its data directly. A `fetch` stub the test installs after the mount replaces the endpoint. |
|
|
504
|
+
|
|
457
505
|
## Caveats
|
|
458
506
|
|
|
459
507
|
- Client tree ONLY. Does NOT catch server/client boundary reference-identity remount bugs, real Flight serialization errors, loader execution, middleware, or handler ordering — those are `renderServerTree` / `renderHandler` / e2e territory. Loader data is SEEDED, never run. `hydrate: true` adds an HTML pass of this same client tree ([Hydration](#hydration)), not a render of your routes on a server.
|
|
@@ -20,13 +20,13 @@ A `Prerender` route is served as in production: from the artifact `router.matchF
|
|
|
20
20
|
|
|
21
21
|
### Options — `ServeShellRequestOptions`
|
|
22
22
|
|
|
23
|
-
| Field | Type | Meaning
|
|
24
|
-
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
-
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store.
|
|
26
|
-
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them.
|
|
27
|
-
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`.
|
|
28
|
-
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends the speculative kind a viewport or render `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false` work is deferred; add `headers: { "X-Rango-Prefetch": "intent" }` to send a hover's or `prefetch()`'s intent prefetch instead, which defers
|
|
29
|
-
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`.
|
|
23
|
+
| Field | Type | Meaning |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `cacheStore` | `SegmentCacheStore` | The store for this request, in place of the store your `createRouter({ cache })` config returns. The rest of that config (`enabled`, `searchParams`) still applies. Omit it to use the router's own store. |
|
|
26
|
+
| `env` | `TEnv` | Bindings, as `router.fetch(request, { env })` passes them. A function `cache` config receives them. |
|
|
27
|
+
| `headers` | `HeadersInit` | Request headers (e.g. `cookie`). A document request defaults `accept` to `text/html`. |
|
|
28
|
+
| `partial` | `true \| { from?: string; rewriteTarget?: string; segments?: string[]; prefetch?: boolean; fill?: boolean; interceptSource?: string; stale?: boolean; boundary?: string }` | Serve the navigation request the browser sends instead of a document GET: from `from` (default the site root) with `segments` mounted (default none). `prefetch: true` sends the speculative kind a viewport or render `<Link>` prefetch sends (`X-Rango-Prefetch: 1`), so `prefetch: false` and `prefetch: "intent"` work is deferred; add `headers: { "X-Rango-Prefetch": "intent" }` to send a hover's or `prefetch()`'s intent prefetch instead, which runs the `"intent"` work and defers the `false` work. `fill: true` sends the follow-up request (`_rsc_fill=1`) the browser makes after adopting a prefetch; pass the prefetch's delivered ids, without the deferred ones, as `segments`, and the same URL as `from`. `interceptSource` sends the intercept source (`X-RSC-Router-Intercept-Source`) the browser sends with the fill of an intercept, whose `from` is that source too. `rewriteTarget` sends what the page at `from` carried as its payload's `metadata.rewriteTarget` when middleware rewrote it (`_rsc_rt`), as the browser does, so the server diffs against the route that rendered that page. `stale: true` sends a stale request (`_rsc_stale`), as the browser sends it: every `revalidate()` sees `stale: true` (see `revalidate()`'s `stale`), segments take an action's revalidation defaults, and it renders live, as an action does: neither the route cache (`cache()`) nor the document cache serves or stores it, and a `ppr` route's `replayStatus` is `{ outcome: "BYPASS", reason: "stale" }`. `boundary` names the error segment of a thrown action's boundary the page at `from` shows (`_rsc_boundary`), as the background revalidation an earlier action owes that page sends it, with `stale: true` and no `segments`: the response renders every segment above the boundary and nothing at or below it, claims the boundary in `matched` and sends no segment for it. The server reads it only beside `stale`. |
|
|
29
|
+
| `action` | `{ ref: Function; id: string; args?: unknown[]; segments?: string[]; rewriteTarget?: string }` | Send the request a JS server action call makes from the page at `url`: a POST of `args` (encoded as the browser encodes them) with `?_rsc_action`, the `rsc-action` header, `X-RSC-Router-Client-Path` and `_rsc_segments` (the page's mounted segment ids, default none), plus the page's `rewriteTarget` as for `partial`. Registers `ref` under `id` (`<module id>#<export name>`). Not with `partial` or `form`. |
|
|
30
30
|
|
|
31
31
|
### Returns — `ServeShellRequestResult`
|
|
32
32
|
|
|
@@ -294,6 +294,18 @@ expect(await fill.readDeferred()).toEqual([]); // a fill defers nothing
|
|
|
294
294
|
|
|
295
295
|
`deliveredIds` is the payload's `metadata.matched`, read from `prefetch.flight`. Count the loader's runs in your own fixture: the prefetch ran it zero times, the fill once.
|
|
296
296
|
|
|
297
|
+
For a loader marked `prefetch: "intent"`, send the hover's prefetch too: it runs that loader and still defers a `prefetch: false` one beside it.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const hover = await serveShellRequest(router, "/product/1", {
|
|
301
|
+
cacheStore,
|
|
302
|
+
headers: { "X-Rango-Prefetch": "intent" },
|
|
303
|
+
partial: { from: "/", prefetch: true },
|
|
304
|
+
});
|
|
305
|
+
// loader(RelatedLoader, { prefetch: "intent" }) ran; ReviewsLoader is deferred.
|
|
306
|
+
expect(await hover.readDeferred()).toHaveLength(1);
|
|
307
|
+
```
|
|
308
|
+
|
|
297
309
|
### Intercepts
|
|
298
310
|
|
|
299
311
|
A prefetch of an `intercept()`'s target from its source page defers the intercept's flagged loaders, loader segments of its slot (`${parent}.@modalD${i}.${$$id}`). The browser sends the fill of an intercept from the source, with the intercept source header: pass both.
|
|
@@ -314,16 +314,20 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
314
314
|
already on screen whose `revalidate()` re-runs it for the destination is
|
|
315
315
|
prefetched with it, so the click does not wait on a fill for data the page
|
|
316
316
|
is already showing.
|
|
317
|
-
- `prefetch: false` applies to
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
- `prefetch: false` applies to every prefetch, hovers included.
|
|
318
|
+
`prefetch: "intent"` applies to speculative prefetches only (viewport and
|
|
319
|
+
render): a hover, or `useRouter().prefetch()`, is intent to click and runs
|
|
320
|
+
the loader, so on a route whose flags are all `"intent"` its click needs no
|
|
321
|
+
fill. Pick `"intent"` for a loader worth starting on hover, `false` for one
|
|
322
|
+
too expensive to run on every cursor pass. A hover over a link a viewport
|
|
323
|
+
prefetch already answered sends nothing, and that click still fills.
|
|
321
324
|
- The trade-off of a deferring prefetch, measured on a `clientUrls()` tab in
|
|
322
|
-
a consumer app (dev, a loaded machine)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
a consumer app (dev, a loaded machine): with the tab's loader skipped by
|
|
326
|
+
the hover's prefetch, a hover then a click 150 ms later showed the content
|
|
327
|
+
in 843 to 918 ms; with the tab prefetched whole, in 77 to 157 ms. The
|
|
328
|
+
deferred click pays the fill request and then React's 300 ms on the tab's
|
|
329
|
+
fallback. `prefetch: "intent"` buys the second number on hover; a
|
|
330
|
+
viewport-prefetched link still pays the first.
|
|
327
331
|
- A click that adopts a prefetch with a flagged loader shows the page at once
|
|
328
332
|
and sends one fill request for the flagged loaders. Read a flagged loader
|
|
329
333
|
under its own `<Suspense>`: with no boundary, React keeps the page being
|
|
@@ -335,7 +339,7 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
335
339
|
or a grid does not prefetch each link again after every navigation. The server
|
|
336
340
|
makes an answer page-only (`x-rsc-prefetch-scope: source`) when sharing it
|
|
337
341
|
would be wrong: an intercept targets the route or the page sending it, the
|
|
338
|
-
answer defers a
|
|
342
|
+
answer defers a flagged unit the page does not show yet, a
|
|
339
343
|
`revalidate()` on the target read `currentUrl` and kept its segment, a rule
|
|
340
344
|
on the route (or inside its `path()`) read the page being left and kept the
|
|
341
345
|
segment through the route's default, or a kept answer sends no segment at
|
|
@@ -388,7 +392,8 @@ The rule: **prefetch broadly, and flag what is expensive.**
|
|
|
388
392
|
and the list stays mounted under it; a document load of the URL renders the
|
|
389
393
|
full page; Back closes the modal. Recipe 3.5.
|
|
390
394
|
- An intercept's loaders each take their own `revalidate()`, and a flagged
|
|
391
|
-
`prefetch: false` loader opens the modal at once with its
|
|
395
|
+
(`prefetch: false` or `"intent"`) loader opens the modal at once with its
|
|
396
|
+
`loading()`. Its
|
|
392
397
|
errors render the declaring layout's `errorBoundary()`.
|
|
393
398
|
- Inside a group, `intercept()` takes a dot-local named target in the same
|
|
394
399
|
definition and `loader()`/`loading()` only, and only navigations from inside
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
replaceCurrentHistoryState,
|
|
9
9
|
resolveLocationStateEntries,
|
|
10
10
|
stampLocationState,
|
|
11
|
+
type LocationStateSnapshot,
|
|
11
12
|
} from "./react/location-state-shared.js";
|
|
12
13
|
import { WIRE } from "../wire-params.js";
|
|
13
14
|
|
|
@@ -110,6 +111,53 @@ export function buildHistoryState(
|
|
|
110
111
|
return Object.keys(result).length > 0 ? result : null;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The entry a commit onto the entry on screen writes (router.refresh(), the
|
|
116
|
+
* cross-tab refresh): `entryState`, read when the commit lands, keeps its
|
|
117
|
+
* location state. Its scroll key and idx go (pushHistoryWithIdx and
|
|
118
|
+
* ensureHistoryKey write them again), and its intercept marks are this
|
|
119
|
+
* commit's: a refresh renders the URL's full page. Server-set state merges
|
|
120
|
+
* over the kept state. A flash a reader already cleared from the entry stays
|
|
121
|
+
* cleared: the entry is copied, never rebuilt from what readers show.
|
|
122
|
+
*/
|
|
123
|
+
export function keptEntryState(
|
|
124
|
+
entryState: unknown,
|
|
125
|
+
routerState?: { intercept?: boolean; sourceUrl?: string },
|
|
126
|
+
serverState?: Record<string, unknown>,
|
|
127
|
+
): Record<string, unknown> | null {
|
|
128
|
+
let result: Record<string, unknown> =
|
|
129
|
+
entryState !== null && typeof entryState === "object"
|
|
130
|
+
? { ...entryState }
|
|
131
|
+
: {};
|
|
132
|
+
delete result.key;
|
|
133
|
+
delete result.idx;
|
|
134
|
+
delete result.intercept;
|
|
135
|
+
delete result.sourceUrl;
|
|
136
|
+
if (routerState?.intercept) {
|
|
137
|
+
result.intercept = true;
|
|
138
|
+
if (routerState.sourceUrl) result.sourceUrl = routerState.sourceUrl;
|
|
139
|
+
}
|
|
140
|
+
if (serverState && Object.keys(serverState).length > 0) {
|
|
141
|
+
result = addLocationState(result, serverState);
|
|
142
|
+
}
|
|
143
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* What readers get from a commit onto the entry on screen: the state they
|
|
148
|
+
* show, plus the server's, recorded under the client's version. Not re-read
|
|
149
|
+
* from the entry, so a flash value on screen stays until the next navigation
|
|
150
|
+
* or back/forward.
|
|
151
|
+
*/
|
|
152
|
+
export function shownLocationState(
|
|
153
|
+
shown: LocationStateSnapshot,
|
|
154
|
+
serverState?: Record<string, unknown>,
|
|
155
|
+
): Record<string, unknown> {
|
|
156
|
+
const result: Record<string, unknown> = { ...shown, ...serverState };
|
|
157
|
+
stampLocationState(result);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
113
161
|
/** Check if a history state object contains location state keys. */
|
|
114
162
|
export function hasLocationState(state: unknown): boolean {
|
|
115
163
|
if (!state || typeof state !== "object") return false;
|
|
@@ -489,6 +489,9 @@ export function createNavigationBridge(
|
|
|
489
489
|
// Clear intercept source URL to ensure server doesn't treat this as intercept
|
|
490
490
|
store.setInterceptSourceUrl(null);
|
|
491
491
|
}
|
|
492
|
+
// The page under an open intercept; null once leaving. Navigations
|
|
493
|
+
// send it so the server resolves the target from there.
|
|
494
|
+
const interceptSourceUrl = store.getInterceptSourceUrl() ?? undefined;
|
|
492
495
|
|
|
493
496
|
// Before navigating away, update the source page's cache with the latest
|
|
494
497
|
// handleData. This ensures the cache has correct handleData even if handles
|
|
@@ -560,6 +563,8 @@ export function createNavigationBridge(
|
|
|
560
563
|
!cachedSegments.some((s) => s.type === "error") &&
|
|
561
564
|
!isInterceptOnlyCache(cachedSegments) &&
|
|
562
565
|
!isLeavingIntercept &&
|
|
566
|
+
// The history entry's segments are not what the client holds here.
|
|
567
|
+
!interceptSourceUrl &&
|
|
563
568
|
!cached?.stale &&
|
|
564
569
|
!options?._skipCache &&
|
|
565
570
|
// A cached intercept entry for this URL (from any source page) means
|
|
@@ -637,6 +642,7 @@ export function createNavigationBridge(
|
|
|
637
642
|
? { type: "leave-intercept" as const }
|
|
638
643
|
: {
|
|
639
644
|
type: "navigate" as const,
|
|
645
|
+
...(interceptSourceUrl && { interceptSourceUrl }),
|
|
640
646
|
...(hasUsableCache
|
|
641
647
|
? {
|
|
642
648
|
targetCacheSegments: cachedSegments,
|
|
@@ -769,7 +775,12 @@ export function createNavigationBridge(
|
|
|
769
775
|
[],
|
|
770
776
|
false,
|
|
771
777
|
tx.handle.signal,
|
|
772
|
-
tx.with({
|
|
778
|
+
tx.with({
|
|
779
|
+
url: window.location.href,
|
|
780
|
+
replace: true,
|
|
781
|
+
scroll: false,
|
|
782
|
+
sameEntry: true,
|
|
783
|
+
}),
|
|
773
784
|
{ type: "navigate", refresh: true, boundary: shown[at]?.id },
|
|
774
785
|
);
|
|
775
786
|
} catch (error) {
|