@jsenv/navi 0.30.9 → 0.30.10

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.
@@ -8787,17 +8787,35 @@ const route = (pattern, options = {}) => {
8787
8787
  return `route "${cleanPattern}"`;
8788
8788
  },
8789
8789
  };
8790
- // Asks ahead of the arrival for what this route's actions ask without the
8791
- // address — a page's code, a read without params (see routeAction). An
8792
- // action keyed by a param waits for the arrival: a prefetch has no address
8793
- // (preloadState is the one that has one).
8794
- route.preload = () => {
8790
+ /**
8791
+ * Asks ahead of the arrival for what this route's actions will ask: the
8792
+ * instances the arrival promotes to runs (see prerunAhead in route_action.js).
8793
+ *
8794
+ * Without params, only what asks nothing of the address: the page's code, a
8795
+ * read without params — what a link preloads, since it has no state to read
8796
+ * the params of. Given params, `{}` included, the address `buildUrl(params)`
8797
+ * leads to: every route action of this route reads its params as if the
8798
+ * route matched there (see statesAt).
8799
+ *
8800
+ * @param {object} [params]
8801
+ */
8802
+ route.preload = (params) => {
8795
8803
  const preloadSet = routePreloadMap.get(route);
8796
- if (!preloadSet) {
8804
+ if (preloadSet) {
8805
+ for (const preload of preloadSet) {
8806
+ preload();
8807
+ }
8808
+ }
8809
+ if (params === undefined) {
8797
8810
  return;
8798
8811
  }
8799
- for (const preload of preloadSet) {
8800
- preload();
8812
+ const statePreloadSet = routeStatePreloadMap.get(route);
8813
+ if (!statePreloadSet) {
8814
+ return;
8815
+ }
8816
+ const values = statesAt(route, params);
8817
+ for (const preload of statePreloadSet) {
8818
+ preload(values);
8801
8819
  }
8802
8820
  };
8803
8821
  Object.preventExtensions(route);
@@ -9409,6 +9427,42 @@ const preloadState = (signal, value) => {
9409
9427
  }
9410
9428
  }
9411
9429
  };
9430
+ // What the states of a route will hold once the address is `route.buildUrl(params)`,
9431
+ // as the routing pass will write them (see sync_routes_with_url): a param given is
9432
+ // what its state holds there; a weak one not given is back to its default, a url
9433
+ // built for another visit never carries it (readSignalForUrlBuild); any other one
9434
+ // is carried over by that url and keeps what it holds, so it is left out.
9435
+ const statesAt = (route, params) => {
9436
+ const values = new Map();
9437
+ for (const connection of getRoutePrivateProperties(route).routePattern
9438
+ .connections) {
9439
+ const { paramName, signal } = connection;
9440
+ if (!signal) {
9441
+ continue;
9442
+ }
9443
+ if (!(paramName in params)) {
9444
+ if (connection.weak) {
9445
+ values.set(signal, connection.getDefaultValue());
9446
+ }
9447
+ continue;
9448
+ }
9449
+ const value = params[paramName];
9450
+ if (value !== undefined) {
9451
+ values.set(signal, value);
9452
+ continue;
9453
+ }
9454
+ // An address silent about a persisted search param leaves it alone.
9455
+ if (
9456
+ connection.persists &&
9457
+ connection.paramType === "query" &&
9458
+ connection.history !== "push"
9459
+ ) {
9460
+ continue;
9461
+ }
9462
+ values.set(signal, connection.getDefaultValue());
9463
+ }
9464
+ return values;
9465
+ };
9412
9466
  const setupRoutes = (routes) => {
9413
9467
  if (setupRoutesCalled) {
9414
9468
  throw new Error(
@@ -9956,6 +10010,35 @@ const activeRouteActionsSignal = computed(() => {
9956
10010
  return active;
9957
10011
  });
9958
10012
 
10013
+ /**
10014
+ * Calls back once every route action asking something for the page on screen
10015
+ * has answered, none running and none about to: what is asked on behalf of
10016
+ * another page waits for it, and never competes with the page's own reads.
10017
+ *
10018
+ * @param {() => void} callback
10019
+ * @returns {() => void} cancel
10020
+ */
10021
+ const whenPageReadsAnswered = (callback) => {
10022
+ let cancelled = false;
10023
+ const stop = effect(function () {
10024
+ for (const routeAction of activeRouteActionsSignal.value) {
10025
+ const runningState = routeAction.runningStateSignal.value;
10026
+ // IDLE: asking, and run by an effect of the same batch that has not run yet.
10027
+ if (runningState === RUNNING || runningState === IDLE) {
10028
+ return;
10029
+ }
10030
+ }
10031
+ this.dispose();
10032
+ if (!cancelled) {
10033
+ untracked(callback);
10034
+ }
10035
+ });
10036
+ return () => {
10037
+ cancelled = true;
10038
+ stop();
10039
+ };
10040
+ };
10041
+
9959
10042
  /**
9960
10043
  * Whether a route action of the page on screen waits for app state to ask: its
9961
10044
  * route matches and its `when` says no. Params that say no are not waiting:
@@ -10044,7 +10127,9 @@ const ROUTE_ACTION_BINDING_OPTIONS = new Set([
10044
10127
  * come from the address is not prefetched by a link — a link's prefetch has
10045
10128
  * no address. It is by the press that opens a popup bound to a state these
10046
10129
  * params read: that address is the current one with the state set to the
10047
- * popup's value (see `prefetch` on Button).
10130
+ * popup's value (see `prefetch` on Button). And by `route.preload(params)`,
10131
+ * which names the address: what a `<RouteTravel prefetch>` asks of the pages
10132
+ * on each side of the one on screen.
10048
10133
  * @param {object} [options]
10049
10134
  * @param {() => any} [options.when] - a condition on app state the action
10050
10135
  * asks under, read off signals like the params: falsy, it runs nothing.
@@ -10128,7 +10213,7 @@ const routeAction = (
10128
10213
  for (const route of routes) {
10129
10214
  if (paramsEffect) {
10130
10215
  registerRouteStatePreload(route, (values) => {
10131
- prefetchAsIf(action, readParams, values);
10216
+ prefetchAsIf(action, readParams, readParamsAsked, values);
10132
10217
  });
10133
10218
  } else {
10134
10219
  registerRoutePreload(route, () => {
@@ -10144,15 +10229,17 @@ const routeAction = (
10144
10229
  };
10145
10230
 
10146
10231
  // The params as they will read once the states hold what they are about to
10147
- // (see readStateAsIf), prerun only when that changes them: params the state
10148
- // leaves alone are the ones the action already runs on — or is meant not to,
10149
- // and a prerun would retry a FAILED one behind the screen's back.
10150
- const prefetchAsIf = (action, readParams, values) => {
10232
+ // (see readStateAsIf), prerun only when they differ from what the action asks
10233
+ // now: params the states leave alone are the ones it already runs on — or is
10234
+ // meant not to, and a prerun would retry a FAILED one behind the screen's back.
10235
+ // An action whose route does not match asks nothing: whatever its params read,
10236
+ // the arrival runs it on them.
10237
+ const prefetchAsIf = (action, readParams, readParamsAsked, values) => {
10151
10238
  const params = readStateAsIf(values, readParams);
10152
10239
  if (!params) {
10153
10240
  return;
10154
10241
  }
10155
- if (compareTwoJsValues(params, untracked(readParams))) {
10242
+ if (compareTwoJsValues(params, untracked(readParamsAsked))) {
10156
10243
  return;
10157
10244
  }
10158
10245
  prerunAhead(action.bindParams(params));
@@ -41943,6 +42030,7 @@ const ROW_READING = {
41943
42030
  * travelByDrag?: boolean,
41944
42031
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
41945
42032
  * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
42033
+ * prefetch?: boolean,
41946
42034
  * }>}
41947
42035
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
41948
42036
  * over something longer, in order. Read from the <Route> children by default,
@@ -41966,6 +42054,12 @@ const ROW_READING = {
41966
42054
  * `{ route, params }`, or the entry of `routes` as it was given, and is a column
41967
42055
  * the height of the row, as the page is. Its route does not match while it stands
41968
42056
  * there: what it reads is not asked for.
42057
+ * @param {boolean} [props.prefetch=false] - whether the pages on each side of the one
42058
+ * on screen have their reads asked for ahead of the hand: at rest, once the page on
42059
+ * screen has its answers and has painted them, the next page first, each with
42060
+ * `route.preload(params)`. A swipe then builds a page whose answers are already
42061
+ * there, with no wait to show. Off by default: every page one stops on costs the
42062
+ * reads of two more.
41969
42063
  *
41970
42064
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
41971
42065
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -41982,6 +42076,7 @@ const RouteTravel = ({
41982
42076
  routeTransition: "none"
41983
42077
  }),
41984
42078
  renderNeighbour,
42079
+ prefetch = false,
41985
42080
  className,
41986
42081
  children,
41987
42082
  ...rest
@@ -41991,7 +42086,8 @@ const RouteTravel = ({
41991
42086
  const liveRef = useRef();
41992
42087
  const picturesRef = useRef();
41993
42088
  const windowed = Boolean(routesProp);
41994
- const [, render] = useReducer(count => count + 1, 0);
42089
+ // Counts the movements ended: endMovement renders through it.
42090
+ const [restCount, render] = useReducer(count => count + 1, 0);
41995
42091
  const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
41996
42092
  const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
41997
42093
  // Held while the row moves: a window recentred under a hand would move the very
@@ -42565,6 +42661,45 @@ const RouteTravel = ({
42565
42661
  }
42566
42662
  }, [currentIndex]);
42567
42663
 
42664
+ // What a hand can bring is asked for at rest, the next page first: never before
42665
+ // the page on screen has its answers and has painted them. A row in movement is
42666
+ // not at rest; the render that ends the movement asks again. Read as pages, not as
42667
+ // entries: the pages of <Route> children are collected again at every render.
42668
+ const neighboursRef = useRef([null, null]);
42669
+ const nextPage = currentIndex === -1 ? null : pages[currentIndex + 1] || null;
42670
+ const previousPage = currentIndex === -1 ? null : pages[currentIndex - 1] || null;
42671
+ if (!samePage(neighboursRef.current[0], nextPage) || !samePage(neighboursRef.current[1], previousPage)) {
42672
+ neighboursRef.current = [nextPage, previousPage];
42673
+ }
42674
+ const neighbours = neighboursRef.current;
42675
+ useLayoutEffect(() => {
42676
+ if (!prefetch || movementRef.current) {
42677
+ return undefined;
42678
+ }
42679
+ if (!neighbours[0] && !neighbours[1]) {
42680
+ return undefined;
42681
+ }
42682
+ let cancelPaint = null;
42683
+ const cancelWait = whenPageReadsAnswered(() => {
42684
+ cancelPaint = afterPaint(() => {
42685
+ if (movementRef.current) {
42686
+ return;
42687
+ }
42688
+ for (const page of neighbours) {
42689
+ if (page) {
42690
+ page.route.preload(page.params || {});
42691
+ }
42692
+ }
42693
+ });
42694
+ });
42695
+ return () => {
42696
+ cancelWait();
42697
+ if (cancelPaint) {
42698
+ cancelPaint();
42699
+ }
42700
+ };
42701
+ }, [prefetch, neighbours, restCount]);
42702
+
42568
42703
  // A neighbour shows from the first frame the compositor moves the row, before
42569
42704
  // any scroll event: a finger landing is the last word before that.
42570
42705
  const paintsNeighbours = Boolean(renderNeighbour);