@solidjs/router 2.0.0-next.19 → 2.0.0-next.20

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, flush, createComponent, createRoot, Show, createEffect, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
2
  import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
3
  import { subscribeFlightData, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
@@ -844,18 +844,15 @@ function getLazyBoundary(thunk) {
844
844
  * synchronously once available, the in-flight promise otherwise.
845
845
  *
846
846
  * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
847
- * re-fetches a failed dynamic import): a client rejection is held through its
848
- * settlement flush. The recomputes the settled promise triggers — the parked
849
- * transition's and the mainline commit's — both consume it here as a
850
- * synchronous throw, becoming cached error status that reaches the nearest
851
- * error boundary like a failed lazy() component. The hold clears a microtask
852
- * after the first delivery, so any later recompute — boundary reset(), a new
853
- * navigation — finds a clean record and retries the import. Holding through
854
- * the flush is what keeps the erroring computation from refiring the import
855
- * in a tight loop while it fails. Server records are shared across requests
856
- * and hold nothing — each request retries. Commit is always async — even for
857
- * thunks returning arrays — so the version bump never writes a signal from
858
- * inside a render computation.
847
+ * re-fetches a failed dynamic import): each client router's async boundary
848
+ * reader retains the rejection as reactive error status, so the destination
849
+ * error boundary receives it without refiring the import. The shared record
850
+ * holds that rejection through the current turn for concurrent readers, then
851
+ * clears so a later tracked retry — boundary reset() or a new navigation —
852
+ * starts a fresh import. Server records are shared across requests and hold
853
+ * nothing, so each request retries. Commit is always async — even for thunks
854
+ * returning arrays — so the version bump never writes a signal from inside a
855
+ * render computation.
859
856
  */
860
857
  function resolveLazySubtree(record) {
861
858
  if (record.resolved) return record.resolved;
@@ -874,7 +871,15 @@ function resolveLazySubtree(record) {
874
871
  return record.resolved;
875
872
  }, e => {
876
873
  // ?? Error(): a held undefined would read as "no failure" and refire
877
- if (!isServer) record.error = e ?? new Error();
874
+ if (!isServer) {
875
+ record.error = e ?? new Error();
876
+ record.sweep = true;
877
+ // Existing per-router async readers retain the rejection in their
878
+ // reactive error status. Hold the shared record through this turn so
879
+ // concurrent readers receive the same failure, then permit remounts
880
+ // and boundary resets to create a fresh import attempt.
881
+ queueMicrotask(() => record.error = record.sweep = undefined);
882
+ }
878
883
  record.promise = undefined;
879
884
  throw e;
880
885
  });
@@ -1097,9 +1102,9 @@ let flashDecoder;
1097
1102
  function provideFlashDecoder(decoder) {
1098
1103
  flashDecoder || (flashDecoder = decoder);
1099
1104
  }
1100
- let intent;
1105
+ let preloadIntent;
1101
1106
  function getIntent() {
1102
- return intent;
1107
+ return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
1103
1108
  }
1104
1109
  let inPreloadFn = false;
1105
1110
  function getInPreloadFn() {
@@ -1125,26 +1130,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1125
1130
  setSource({
1126
1131
  value: basePath,
1127
1132
  replace: true,
1128
- scroll: false
1133
+ scroll: false,
1134
+ _navigation: isServer ? undefined : 1
1129
1135
  });
1130
1136
  }
1131
- const [isNavigating, setIsRouting] = createSignal(false, {
1132
- ownedWrite: true
1133
- });
1134
-
1135
- // Navigate override written from event handlers.
1136
- const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
1137
- ownedWrite: true
1138
- });
1139
-
1140
- // Keep track of last target, so that last call to navigate wins
1141
- let lastTransitionTarget;
1142
-
1143
- // source() remains canonical for native history changes; navigateTarget()
1144
- // temporarily overrides it for in-flight programmatic navigation.
1145
- const effective = createMemo(() => navigateTarget() ?? source());
1146
- const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
1147
- const referrers = [];
1137
+ const location = createLocation(() => source().value, () => source().state, utils.queryWrapper);
1148
1138
  // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
1149
1139
  // must be appended before streaming flushes the response headers, and an
1150
1140
  // unread outcome must not haunt a later request's render. Only detection
@@ -1164,6 +1154,24 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1164
1154
  }
1165
1155
  }
1166
1156
  let submissions;
1157
+
1158
+ // NotReadyError's source must be a reactive async node, not the raw
1159
+ // Promise. Keep one reader per boundary for this router owner so rejection
1160
+ // is delivered through the graph's error channel and resolution wakes the
1161
+ // parked matches computation.
1162
+ const routerOwner = getOwner();
1163
+ const lazyReaders = new WeakMap();
1164
+ const readLazySubtree = record => {
1165
+ let read = lazyReaders.get(record);
1166
+ if (!read) {
1167
+ read = runWithOwner(routerOwner, () => createMemo(() => {
1168
+ const result = resolveLazySubtree(record);
1169
+ return result instanceof Promise ? result.then(() => undefined) : undefined;
1170
+ }));
1171
+ lazyReaders.set(record, read);
1172
+ }
1173
+ return read();
1174
+ };
1167
1175
  const matches = createMemo(() => {
1168
1176
  const pathname = typeof options.transformUrl === "function" ? options.transformUrl(location.pathname) : location.pathname;
1169
1177
  const m = getRouteMatches(branches(), pathname);
@@ -1176,26 +1184,22 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1176
1184
  // inside a boundary just parks the recomputed chain again.
1177
1185
  const pending = unresolvedLazyMatches(m);
1178
1186
  if (pending.length) {
1179
- const all = Promise.all(pending.map(resolveLazySubtree));
1180
- // pre-handle rejections: the failure reaches the app through this
1181
- // computation's error status, so the raw chain must not also surface
1182
- // as an unhandled rejection
1183
- all.catch(() => {});
1184
- throw new NotReadyError(all);
1187
+ if (isServer) {
1188
+ // SSR carries the Promise through NotReadyError so the streaming
1189
+ // renderer can resume without attempting to serialize route
1190
+ // definitions (which contain component functions).
1191
+ const all = Promise.all(pending.map(resolveLazySubtree));
1192
+ all.catch(() => {});
1193
+ throw new NotReadyError(all);
1194
+ } else {
1195
+ // On the client the source must be a reactive async node so transition
1196
+ // settlement and rejection delivery remain inside the signals graph.
1197
+ for (const boundary of pending) readLazySubtree(boundary);
1198
+ }
1185
1199
  }
1186
1200
  return m;
1187
1201
  });
1188
-
1189
- // Every write is a transition in Solid 2, so a native history pop forks the
1190
- // source signal exactly like programmatic navigation does. isRouting is
1191
- // therefore derived: the manual flag covers navigateFromRoute's explicit
1192
- // window, and isPending over the location/matches read reports any
1193
- // in-flight fork — including popstate traversals and the lazy-subtree
1194
- // resolution matches() parks on.
1195
- const isRouting = createMemo(() => isNavigating() || isPending(() => {
1196
- // A real error means the navigation settled (failed) — it surfaces
1197
- // to the app through render reads, not through this derivation.
1198
- // Not-ready must keep propagating so isPending sees the parking.
1202
+ const routingPending = createMemo(() => isPending(() => {
1199
1203
  try {
1200
1204
  matches();
1201
1205
  } catch (e) {
@@ -1204,6 +1208,17 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1204
1208
  location.search;
1205
1209
  location.hash;
1206
1210
  }));
1211
+ const isRouting = () => routingPending() || isPending(source);
1212
+ const transitionIntent = () => {
1213
+ if (!isPending(source)) return;
1214
+ const navigation = latest(source)._navigation;
1215
+ return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
1216
+ };
1217
+ const pendingNavigation = () => {
1218
+ if (!isRouting()) return;
1219
+ const target = latest(source);
1220
+ return target._navigation && target._navigation > 0 ? target : undefined;
1221
+ };
1207
1222
  const buildParams = () => mergeParams(matches());
1208
1223
  const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
1209
1224
  const params = wrapParams(buildParams);
@@ -1222,8 +1237,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1222
1237
  params,
1223
1238
  wrapParams,
1224
1239
  isRouting,
1240
+ intent: transitionIntent,
1225
1241
  get pendingTarget() {
1226
- return lastTransitionTarget;
1242
+ return pendingNavigation();
1227
1243
  },
1228
1244
  renderPath,
1229
1245
  parsePath,
@@ -1285,11 +1301,13 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1285
1301
  }
1286
1302
  if (resolvedTo === undefined) {
1287
1303
  throw new Error(`Path '${to}' is not a routable path`);
1288
- } else if (referrers.length >= MAX_REDIRECTS) {
1304
+ }
1305
+ const headed = latest(source);
1306
+ const navigationDepth = !isServer && isPending(source) && headed._navigation !== undefined && headed._navigation > 0 ? headed._navigation : 0;
1307
+ if (navigationDepth >= MAX_REDIRECTS) {
1289
1308
  throw new Error("Too many redirects");
1290
1309
  }
1291
- const current = effective();
1292
- if (resolvedTo !== current.value || nextState !== current.state) {
1310
+ if (resolvedTo !== headed.value || nextState !== headed.state) {
1293
1311
  if (isServer) {
1294
1312
  const e = getRequestEvent();
1295
1313
  e && (e.response = {
@@ -1305,38 +1323,13 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1305
1323
  state: nextState
1306
1324
  });
1307
1325
  } else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
1308
- referrers.push({
1309
- value: current.value,
1310
- replace,
1311
- scroll,
1312
- state: current.state
1313
- });
1314
- const newTarget = {
1326
+ runWithOwner(null, () => setSource({
1315
1327
  value: resolvedTo,
1316
- state: nextState
1317
- };
1318
- const firstNavigation = lastTransitionTarget === undefined;
1319
- intent = "navigate";
1320
- // assign the target before flushing so effects that run for the
1321
- // isRouting flip (e.g. pending link state) can read it
1322
- lastTransitionTarget = newTarget;
1323
- if (firstNavigation) {
1324
- setIsRouting(true);
1325
- flush();
1326
- }
1327
- if (lastTransitionTarget === newTarget) {
1328
- setNavigateTarget({
1329
- ...lastTransitionTarget
1330
- });
1331
- queueMicrotask(() => {
1332
- if (lastTransitionTarget !== newTarget) return;
1333
- intent = undefined;
1334
- navigateEnd(lastTransitionTarget);
1335
- setNavigateTarget(undefined);
1336
- setIsRouting(false);
1337
- lastTransitionTarget = undefined;
1338
- });
1339
- }
1328
+ state: nextState,
1329
+ replace: navigationDepth ? headed.replace : replace,
1330
+ scroll: navigationDepth ? headed.scroll : scroll,
1331
+ _navigation: navigationDepth + 1
1332
+ }));
1340
1333
  }
1341
1334
  }
1342
1335
  });
@@ -1346,17 +1339,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1346
1339
  route = route || useOptionalContext(RouteContextObj) || baseRoute;
1347
1340
  return (to, options) => navigateFromRoute(route, to, options);
1348
1341
  }
1349
- function navigateEnd(next) {
1350
- const first = referrers[0];
1351
- if (first) {
1352
- setSource({
1353
- ...next,
1354
- replace: first.replace,
1355
- scroll: first.scroll
1356
- });
1357
- referrers.length = 0;
1358
- }
1359
- }
1360
1342
  function preloadRoute(url, preloadData) {
1361
1343
  const matches = getRouteMatches(branches(), url.pathname);
1362
1344
  // An unresolved lazy subtree in the chain: the placeholder's
@@ -1370,8 +1352,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1370
1352
  resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
1371
1353
  } catch {}
1372
1354
  }
1373
- const prevIntent = intent;
1374
- intent = "preload";
1355
+ const prevIntent = preloadIntent;
1356
+ preloadIntent = "preload";
1375
1357
  for (let match in matches) {
1376
1358
  const {
1377
1359
  route,
@@ -1396,7 +1378,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1396
1378
  }));
1397
1379
  inPreloadFn = false;
1398
1380
  }
1399
- intent = prevIntent;
1381
+ preloadIntent = prevIntent;
1400
1382
  }
1401
1383
 
1402
1384
  // Seeds the initial submission from a no-JS form post: the server
@@ -1437,7 +1419,7 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
1437
1419
  const data = preload ? preload({
1438
1420
  params,
1439
1421
  location,
1440
- intent: intent || "initial"
1422
+ intent: router.intent?.() || "initial"
1441
1423
  }) : undefined;
1442
1424
  inPreloadFn = false;
1443
1425
  const route = {
@@ -1985,23 +1967,37 @@ function defineRoute(route) {
1985
1967
  }
1986
1968
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
1987
1969
  function createIntegration(history) {
1988
- let ignore = false;
1970
+ let committing = false;
1989
1971
  const wrap = value => typeof value === "string" ? {
1990
1972
  value
1991
1973
  } : value;
1992
1974
  const [read, write] = createSignal(wrap(history.get()), {
1993
- equals: (a, b) => a.value === b.value && a.state === b.state,
1975
+ equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
1994
1976
  ownedWrite: true
1995
1977
  });
1996
1978
  const signal = [read, next => {
1997
- !ignore && history.set(next);
1998
1979
  if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
1999
1980
  write(next);
1981
+ if (next._navigation && next._navigation > 0) {
1982
+ // Register out of band so a destination error boundary replacing the
1983
+ // Router subtree cannot suppress the winning history commit.
1984
+ runWithOwner(null, () => onSettled(() => {
1985
+ if (read() !== next) return;
1986
+ committing = true;
1987
+ try {
1988
+ history.set(next);
1989
+ } finally {
1990
+ committing = false;
1991
+ }
1992
+ }));
1993
+ }
2000
1994
  }];
2001
1995
  history.init && onCleanup(history.init((value = history.get()) => {
2002
- ignore = true;
2003
- signal[1](wrap(value));
2004
- ignore = false;
1996
+ if (committing) return;
1997
+ signal[1]({
1998
+ ...wrap(value),
1999
+ _navigation: -1
2000
+ });
2005
2001
  }));
2006
2002
  return {
2007
2003
  signal,
@@ -1,5 +1,5 @@
1
1
  /*@refresh skip*/
2
- import { createSignal, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
2
+ import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, sharedConfig, untrack } from "solid-js";
3
3
  // standalone import: `DEV` is undefined in solid's production build, so app
4
4
  // bundlers fold `DEV &&` diagnostics out of shipped bundles
5
5
  import { DEV } from "solid-js";
@@ -27,26 +27,40 @@ export function defineRoute(route) {
27
27
  }
28
28
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
29
29
  function createIntegration(history) {
30
- let ignore = false;
30
+ let committing = false;
31
31
  const wrap = (value) => (typeof value === "string" ? { value } : value);
32
32
  const [read, write] = createSignal(wrap(history.get()), {
33
- equals: (a, b) => a.value === b.value && a.state === b.state,
33
+ equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
34
34
  ownedWrite: true
35
35
  });
36
36
  const signal = [
37
37
  read,
38
38
  (next) => {
39
- !ignore && history.set(next);
40
39
  if (sharedConfig.registry && !sharedConfig.done)
41
40
  sharedConfig.done = true;
42
41
  write(next);
42
+ if (next._navigation && next._navigation > 0) {
43
+ // Register out of band so a destination error boundary replacing the
44
+ // Router subtree cannot suppress the winning history commit.
45
+ runWithOwner(null, () => onSettled(() => {
46
+ if (read() !== next)
47
+ return;
48
+ committing = true;
49
+ try {
50
+ history.set(next);
51
+ }
52
+ finally {
53
+ committing = false;
54
+ }
55
+ }));
56
+ }
43
57
  }
44
58
  ];
45
59
  history.init &&
46
60
  onCleanup(history.init((value = history.get()) => {
47
- ignore = true;
48
- signal[1](wrap(value));
49
- ignore = false;
61
+ if (committing)
62
+ return;
63
+ signal[1]({ ...wrap(value), _navigation: -1 });
50
64
  }));
51
65
  return { signal, utils: history.utils };
52
66
  }
package/dist/routing.d.ts CHANGED
@@ -192,18 +192,15 @@ export declare function peekLazySubtrees(): number;
192
192
  * synchronously once available, the in-flight promise otherwise.
193
193
  *
194
194
  * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
195
- * re-fetches a failed dynamic import): a client rejection is held through its
196
- * settlement flush. The recomputes the settled promise triggers — the parked
197
- * transition's and the mainline commit's — both consume it here as a
198
- * synchronous throw, becoming cached error status that reaches the nearest
199
- * error boundary like a failed lazy() component. The hold clears a microtask
200
- * after the first delivery, so any later recompute — boundary reset(), a new
201
- * navigation — finds a clean record and retries the import. Holding through
202
- * the flush is what keeps the erroring computation from refiring the import
203
- * in a tight loop while it fails. Server records are shared across requests
204
- * and hold nothing — each request retries. Commit is always async — even for
205
- * thunks returning arrays — so the version bump never writes a signal from
206
- * inside a render computation.
195
+ * re-fetches a failed dynamic import): each client router's async boundary
196
+ * reader retains the rejection as reactive error status, so the destination
197
+ * error boundary receives it without refiring the import. The shared record
198
+ * holds that rejection through the current turn for concurrent readers, then
199
+ * clears so a later tracked retry — boundary reset() or a new navigation —
200
+ * starts a fresh import. Server records are shared across requests and hold
201
+ * nothing, so each request retries. Commit is always async — even for thunks
202
+ * returning arrays — so the version bump never writes a signal from inside a
203
+ * render computation.
207
204
  */
208
205
  export declare function resolveLazySubtree(record: LazyBoundary): readonly RouteDefinition[] | Promise<readonly RouteDefinition[]>;
209
206
  /**
package/dist/routing.js CHANGED
@@ -1,8 +1,8 @@
1
- import { flush, runWithOwner } from "solid-js";
1
+ import { runWithOwner } from "solid-js";
2
2
  // standalone import: `DEV` is undefined in solid's production build, so app
3
3
  // bundlers fold `DEV &&` diagnostics out of shipped bundles
4
4
  import { DEV } from "solid-js";
5
- import { createComponent, createContext, createMemo, createSignal, isPending, NotReadyError, untrack, useContext } from "solid-js";
5
+ import { createComponent, createContext, createMemo, createSignal, getOwner, isPending, latest, NotReadyError, untrack, useContext } from "solid-js";
6
6
  import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
7
7
  import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
8
8
  import { HREF } from "./paths.js";
@@ -289,18 +289,15 @@ function getLazyBoundary(thunk) {
289
289
  * synchronously once available, the in-flight promise otherwise.
290
290
  *
291
291
  * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
292
- * re-fetches a failed dynamic import): a client rejection is held through its
293
- * settlement flush. The recomputes the settled promise triggers — the parked
294
- * transition's and the mainline commit's — both consume it here as a
295
- * synchronous throw, becoming cached error status that reaches the nearest
296
- * error boundary like a failed lazy() component. The hold clears a microtask
297
- * after the first delivery, so any later recompute — boundary reset(), a new
298
- * navigation — finds a clean record and retries the import. Holding through
299
- * the flush is what keeps the erroring computation from refiring the import
300
- * in a tight loop while it fails. Server records are shared across requests
301
- * and hold nothing — each request retries. Commit is always async — even for
302
- * thunks returning arrays — so the version bump never writes a signal from
303
- * inside a render computation.
292
+ * re-fetches a failed dynamic import): each client router's async boundary
293
+ * reader retains the rejection as reactive error status, so the destination
294
+ * error boundary receives it without refiring the import. The shared record
295
+ * holds that rejection through the current turn for concurrent readers, then
296
+ * clears so a later tracked retry — boundary reset() or a new navigation —
297
+ * starts a fresh import. Server records are shared across requests and hold
298
+ * nothing, so each request retries. Commit is always async — even for thunks
299
+ * returning arrays — so the version bump never writes a signal from inside a
300
+ * render computation.
304
301
  */
305
302
  export function resolveLazySubtree(record) {
306
303
  if (record.resolved)
@@ -325,8 +322,15 @@ export function resolveLazySubtree(record) {
325
322
  return record.resolved;
326
323
  }, e => {
327
324
  // ?? Error(): a held undefined would read as "no failure" and refire
328
- if (!isServer)
325
+ if (!isServer) {
329
326
  record.error = e ?? new Error();
327
+ record.sweep = true;
328
+ // Existing per-router async readers retain the rejection in their
329
+ // reactive error status. Hold the shared record through this turn so
330
+ // concurrent readers receive the same failure, then permit remounts
331
+ // and boundary resets to create a fresh import attempt.
332
+ queueMicrotask(() => (record.error = record.sweep = undefined));
333
+ }
330
334
  record.promise = undefined;
331
335
  throw e;
332
336
  }));
@@ -551,9 +555,9 @@ let flashDecoder;
551
555
  export function provideFlashDecoder(decoder) {
552
556
  flashDecoder || (flashDecoder = decoder);
553
557
  }
554
- let intent;
558
+ let preloadIntent;
555
559
  export function getIntent() {
556
- return intent;
560
+ return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
557
561
  }
558
562
  let inPreloadFn = false;
559
563
  export function getInPreloadFn() {
@@ -574,20 +578,14 @@ export function createRouterContext(integration, branches, getContext, options =
574
578
  throw new Error(`${basePath} is not a valid base path`);
575
579
  }
576
580
  else if (basePath && !initialSource.value) {
577
- setSource({ value: basePath, replace: true, scroll: false });
581
+ setSource({
582
+ value: basePath,
583
+ replace: true,
584
+ scroll: false,
585
+ _navigation: isServer ? undefined : 1
586
+ });
578
587
  }
579
- const [isNavigating, setIsRouting] = createSignal(false, { ownedWrite: true });
580
- // Navigate override written from event handlers.
581
- const [navigateTarget, setNavigateTarget] = createSignal(undefined, {
582
- ownedWrite: true
583
- });
584
- // Keep track of last target, so that last call to navigate wins
585
- let lastTransitionTarget;
586
- // source() remains canonical for native history changes; navigateTarget()
587
- // temporarily overrides it for in-flight programmatic navigation.
588
- const effective = createMemo(() => navigateTarget() ?? source());
589
- const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
590
- const referrers = [];
588
+ const location = createLocation(() => source().value, () => source().state, utils.queryWrapper);
591
589
  // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
592
590
  // must be appended before streaming flushes the response headers, and an
593
591
  // unread outcome must not haunt a later request's render. Only detection
@@ -608,6 +606,23 @@ export function createRouterContext(integration, branches, getContext, options =
608
606
  }
609
607
  }
610
608
  let submissions;
609
+ // NotReadyError's source must be a reactive async node, not the raw
610
+ // Promise. Keep one reader per boundary for this router owner so rejection
611
+ // is delivered through the graph's error channel and resolution wakes the
612
+ // parked matches computation.
613
+ const routerOwner = getOwner();
614
+ const lazyReaders = new WeakMap();
615
+ const readLazySubtree = (record) => {
616
+ let read = lazyReaders.get(record);
617
+ if (!read) {
618
+ read = runWithOwner(routerOwner, () => createMemo(() => {
619
+ const result = resolveLazySubtree(record);
620
+ return result instanceof Promise ? result.then(() => undefined) : undefined;
621
+ }));
622
+ lazyReaders.set(record, read);
623
+ }
624
+ return read();
625
+ };
611
626
  const matches = createMemo(() => {
612
627
  const pathname = typeof options.transformUrl === "function"
613
628
  ? options.transformUrl(location.pathname)
@@ -622,36 +637,47 @@ export function createRouterContext(integration, branches, getContext, options =
622
637
  // inside a boundary just parks the recomputed chain again.
623
638
  const pending = unresolvedLazyMatches(m);
624
639
  if (pending.length) {
625
- const all = Promise.all(pending.map(resolveLazySubtree));
626
- // pre-handle rejections: the failure reaches the app through this
627
- // computation's error status, so the raw chain must not also surface
628
- // as an unhandled rejection
629
- all.catch(() => { });
630
- throw new NotReadyError(all);
640
+ if (isServer) {
641
+ // SSR carries the Promise through NotReadyError so the streaming
642
+ // renderer can resume without attempting to serialize route
643
+ // definitions (which contain component functions).
644
+ const all = Promise.all(pending.map(resolveLazySubtree));
645
+ all.catch(() => { });
646
+ throw new NotReadyError(all);
647
+ }
648
+ else {
649
+ // On the client the source must be a reactive async node so transition
650
+ // settlement and rejection delivery remain inside the signals graph.
651
+ for (const boundary of pending)
652
+ readLazySubtree(boundary);
653
+ }
631
654
  }
632
655
  return m;
633
656
  });
634
- // Every write is a transition in Solid 2, so a native history pop forks the
635
- // source signal exactly like programmatic navigation does. isRouting is
636
- // therefore derived: the manual flag covers navigateFromRoute's explicit
637
- // window, and isPending over the location/matches read reports any
638
- // in-flight fork — including popstate traversals and the lazy-subtree
639
- // resolution matches() parks on.
640
- const isRouting = createMemo(() => isNavigating() ||
641
- isPending(() => {
642
- // A real error means the navigation settled (failed) — it surfaces
643
- // to the app through render reads, not through this derivation.
644
- // Not-ready must keep propagating so isPending sees the parking.
645
- try {
646
- matches();
647
- }
648
- catch (e) {
649
- if (e instanceof NotReadyError)
650
- throw e;
651
- }
652
- location.search;
653
- location.hash;
654
- }));
657
+ const routingPending = createMemo(() => isPending(() => {
658
+ try {
659
+ matches();
660
+ }
661
+ catch (e) {
662
+ if (e instanceof NotReadyError)
663
+ throw e;
664
+ }
665
+ location.search;
666
+ location.hash;
667
+ }));
668
+ const isRouting = () => routingPending() || isPending(source);
669
+ const transitionIntent = () => {
670
+ if (!isPending(source))
671
+ return;
672
+ const navigation = latest(source)._navigation;
673
+ return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
674
+ };
675
+ const pendingNavigation = () => {
676
+ if (!isRouting())
677
+ return;
678
+ const target = latest(source);
679
+ return target._navigation && target._navigation > 0 ? target : undefined;
680
+ };
655
681
  const buildParams = () => mergeParams(matches());
656
682
  const wrapParams = utils.paramsWrapper
657
683
  ? (getParams) => utils.paramsWrapper(getParams, branches)
@@ -672,8 +698,9 @@ export function createRouterContext(integration, branches, getContext, options =
672
698
  params,
673
699
  wrapParams,
674
700
  isRouting,
701
+ intent: transitionIntent,
675
702
  get pendingTarget() {
676
- return lastTransitionTarget;
703
+ return pendingNavigation();
677
704
  },
678
705
  renderPath,
679
706
  parsePath,
@@ -739,43 +766,30 @@ export function createRouterContext(integration, branches, getContext, options =
739
766
  if (resolvedTo === undefined) {
740
767
  throw new Error(`Path '${to}' is not a routable path`);
741
768
  }
742
- else if (referrers.length >= MAX_REDIRECTS) {
769
+ const headed = latest(source);
770
+ const navigationDepth = !isServer &&
771
+ isPending(source) &&
772
+ headed._navigation !== undefined &&
773
+ headed._navigation > 0
774
+ ? headed._navigation
775
+ : 0;
776
+ if (navigationDepth >= MAX_REDIRECTS) {
743
777
  throw new Error("Too many redirects");
744
778
  }
745
- const current = effective();
746
- if (resolvedTo !== current.value || nextState !== current.state) {
779
+ if (resolvedTo !== headed.value || nextState !== headed.state) {
747
780
  if (isServer) {
748
781
  const e = getRequestEvent();
749
782
  e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
750
783
  setSource({ value: resolvedTo, replace, scroll, state: nextState });
751
784
  }
752
785
  else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
753
- referrers.push({ value: current.value, replace, scroll, state: current.state });
754
- const newTarget = {
786
+ runWithOwner(null, () => setSource({
755
787
  value: resolvedTo,
756
- state: nextState
757
- };
758
- const firstNavigation = lastTransitionTarget === undefined;
759
- intent = "navigate";
760
- // assign the target before flushing so effects that run for the
761
- // isRouting flip (e.g. pending link state) can read it
762
- lastTransitionTarget = newTarget;
763
- if (firstNavigation) {
764
- setIsRouting(true);
765
- flush();
766
- }
767
- if (lastTransitionTarget === newTarget) {
768
- setNavigateTarget({ ...lastTransitionTarget });
769
- queueMicrotask(() => {
770
- if (lastTransitionTarget !== newTarget)
771
- return;
772
- intent = undefined;
773
- navigateEnd(lastTransitionTarget);
774
- setNavigateTarget(undefined);
775
- setIsRouting(false);
776
- lastTransitionTarget = undefined;
777
- });
778
- }
788
+ state: nextState,
789
+ replace: navigationDepth ? headed.replace : replace,
790
+ scroll: navigationDepth ? headed.scroll : scroll,
791
+ _navigation: navigationDepth + 1
792
+ }));
779
793
  }
780
794
  }
781
795
  });
@@ -785,17 +799,6 @@ export function createRouterContext(integration, branches, getContext, options =
785
799
  route = route || useOptionalContext(RouteContextObj) || baseRoute;
786
800
  return (to, options) => navigateFromRoute(route, to, options);
787
801
  }
788
- function navigateEnd(next) {
789
- const first = referrers[0];
790
- if (first) {
791
- setSource({
792
- ...next,
793
- replace: first.replace,
794
- scroll: first.scroll
795
- });
796
- referrers.length = 0;
797
- }
798
- }
799
802
  function preloadRoute(url, preloadData) {
800
803
  const matches = getRouteMatches(branches(), url.pathname);
801
804
  // An unresolved lazy subtree in the chain: the placeholder's
@@ -810,8 +813,8 @@ export function createRouterContext(integration, branches, getContext, options =
810
813
  }
811
814
  catch { }
812
815
  }
813
- const prevIntent = intent;
814
- intent = "preload";
816
+ const prevIntent = preloadIntent;
817
+ preloadIntent = "preload";
815
818
  for (let match in matches) {
816
819
  const { route, params } = matches[match];
817
820
  route.component &&
@@ -835,7 +838,7 @@ export function createRouterContext(integration, branches, getContext, options =
835
838
  }));
836
839
  inPreloadFn = false;
837
840
  }
838
- intent = prevIntent;
841
+ preloadIntent = prevIntent;
839
842
  }
840
843
  // Seeds the initial submission from a no-JS form post: the server
841
844
  // function runtime redirected back with the outcome in a one-shot flash
@@ -872,7 +875,9 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
872
875
  component.preload &&
873
876
  component.preload();
874
877
  inPreloadFn = true;
875
- const data = preload ? preload({ params, location, intent: intent || "initial" }) : undefined;
878
+ const data = preload
879
+ ? preload({ params, location, intent: router.intent?.() || "initial" })
880
+ : undefined;
876
881
  inPreloadFn = false;
877
882
  const route = {
878
883
  parent,
package/dist/types.d.ts CHANGED
@@ -97,6 +97,8 @@ export interface LocationChange<S = unknown> {
97
97
  scroll?: boolean;
98
98
  state?: S;
99
99
  rawPath?: string;
100
+ /** @internal Positive for programmatic navigation depth; `-1` for native history. */
101
+ _navigation?: number;
100
102
  }
101
103
  export interface RouterIntegration {
102
104
  signal: [get: () => LocationChange, set: (next: LocationChange) => void];
@@ -231,9 +233,9 @@ export interface LazyBoundary {
231
233
  thunk: LazyRouteChildren;
232
234
  promise?: Promise<readonly RouteDefinition[]>;
233
235
  resolved?: readonly RouteDefinition[];
234
- /** A client load failure, held through its settlement flush so every
235
- * recompute it triggers delivers the same error instead of refiring the
236
- * import; cleared a microtask after first delivery so retries can run. */
236
+ /** A client load failure, retained by each router's async reader and held
237
+ * here through the rejection turn for concurrent readers; then cleared so
238
+ * a later boundary reset or navigation can retry the import. */
237
239
  error?: unknown;
238
240
  /** The pending clear of `error` has been scheduled. */
239
241
  sweep?: boolean;
@@ -267,6 +269,8 @@ export interface RouterContext {
267
269
  wrapParams: (getParams: () => Params) => Params;
268
270
  navigatorFactory: NavigatorFactory;
269
271
  isRouting: () => boolean;
272
+ /** @internal Intent of the in-flight canonical location transition. */
273
+ intent?: () => Intent | undefined;
270
274
  /** The target of the in-flight navigation transition, if any. Not reactive. */
271
275
  readonly pendingTarget?: LocationChange;
272
276
  matches: () => RouteMatch[];
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.19",
9
+ "version": "2.0.0-next.20",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",