@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.
@@ -8425,17 +8425,35 @@ const route = (pattern, options = {}) => {
8425
8425
  return `route "${cleanPattern}"`;
8426
8426
  },
8427
8427
  };
8428
- // Asks ahead of the arrival for what this route's actions ask without the
8429
- // address — a page's code, a read without params (see routeAction). An
8430
- // action keyed by a param waits for the arrival: a prefetch has no address
8431
- // (preloadState is the one that has one).
8432
- route.preload = () => {
8428
+ /**
8429
+ * Asks ahead of the arrival for what this route's actions will ask: the
8430
+ * instances the arrival promotes to runs (see prerunAhead in route_action.js).
8431
+ *
8432
+ * Without params, only what asks nothing of the address: the page's code, a
8433
+ * read without params — what a link preloads, since it has no state to read
8434
+ * the params of. Given params, `{}` included, the address `buildUrl(params)`
8435
+ * leads to: every route action of this route reads its params as if the
8436
+ * route matched there (see statesAt).
8437
+ *
8438
+ * @param {object} [params]
8439
+ */
8440
+ route.preload = (params) => {
8433
8441
  const preloadSet = routePreloadMap.get(route);
8434
- if (!preloadSet) {
8442
+ if (preloadSet) {
8443
+ for (const preload of preloadSet) {
8444
+ preload();
8445
+ }
8446
+ }
8447
+ if (params === undefined) {
8435
8448
  return;
8436
8449
  }
8437
- for (const preload of preloadSet) {
8438
- preload();
8450
+ const statePreloadSet = routeStatePreloadMap.get(route);
8451
+ if (!statePreloadSet) {
8452
+ return;
8453
+ }
8454
+ const values = statesAt(route, params);
8455
+ for (const preload of statePreloadSet) {
8456
+ preload(values);
8439
8457
  }
8440
8458
  };
8441
8459
  Object.preventExtensions(route);
@@ -9034,6 +9052,42 @@ const preloadState = (signal, value) => {
9034
9052
  }
9035
9053
  }
9036
9054
  };
9055
+ // What the states of a route will hold once the address is `route.buildUrl(params)`,
9056
+ // as the routing pass will write them (see sync_routes_with_url): a param given is
9057
+ // what its state holds there; a weak one not given is back to its default, a url
9058
+ // built for another visit never carries it (readSignalForUrlBuild); any other one
9059
+ // is carried over by that url and keeps what it holds, so it is left out.
9060
+ const statesAt = (route, params) => {
9061
+ const values = new Map();
9062
+ for (const connection of getRoutePrivateProperties(route).routePattern
9063
+ .connections) {
9064
+ const { paramName, signal } = connection;
9065
+ if (!signal) {
9066
+ continue;
9067
+ }
9068
+ if (!(paramName in params)) {
9069
+ if (connection.weak) {
9070
+ values.set(signal, connection.getDefaultValue());
9071
+ }
9072
+ continue;
9073
+ }
9074
+ const value = params[paramName];
9075
+ if (value !== undefined) {
9076
+ values.set(signal, value);
9077
+ continue;
9078
+ }
9079
+ // An address silent about a persisted search param leaves it alone.
9080
+ if (
9081
+ connection.persists &&
9082
+ connection.paramType === "query" &&
9083
+ connection.history !== "push"
9084
+ ) {
9085
+ continue;
9086
+ }
9087
+ values.set(signal, connection.getDefaultValue());
9088
+ }
9089
+ return values;
9090
+ };
9037
9091
  const setupRoutes = (routes) => {
9038
9092
  if (setupRoutesCalled) {
9039
9093
  throw new Error(
@@ -9562,6 +9616,35 @@ const activeRouteActionsSignal = computed(() => {
9562
9616
  return active;
9563
9617
  });
9564
9618
 
9619
+ /**
9620
+ * Calls back once every route action asking something for the page on screen
9621
+ * has answered, none running and none about to: what is asked on behalf of
9622
+ * another page waits for it, and never competes with the page's own reads.
9623
+ *
9624
+ * @param {() => void} callback
9625
+ * @returns {() => void} cancel
9626
+ */
9627
+ const whenPageReadsAnswered = (callback) => {
9628
+ let cancelled = false;
9629
+ const stop = effect(function () {
9630
+ for (const routeAction of activeRouteActionsSignal.value) {
9631
+ const runningState = routeAction.runningStateSignal.value;
9632
+ // IDLE: asking, and run by an effect of the same batch that has not run yet.
9633
+ if (runningState === RUNNING || runningState === IDLE) {
9634
+ return;
9635
+ }
9636
+ }
9637
+ this.dispose();
9638
+ if (!cancelled) {
9639
+ untracked(callback);
9640
+ }
9641
+ });
9642
+ return () => {
9643
+ cancelled = true;
9644
+ stop();
9645
+ };
9646
+ };
9647
+
9565
9648
  /**
9566
9649
  * Whether a route action of the page on screen waits for app state to ask: its
9567
9650
  * route matches and its `when` says no. Params that say no are not waiting:
@@ -9642,7 +9725,9 @@ const activeRouteReadsSignal = computed(() => {
9642
9725
  * come from the address is not prefetched by a link — a link's prefetch has
9643
9726
  * no address. It is by the press that opens a popup bound to a state these
9644
9727
  * params read: that address is the current one with the state set to the
9645
- * popup's value (see `prefetch` on Button).
9728
+ * popup's value (see `prefetch` on Button). And by `route.preload(params)`,
9729
+ * which names the address: what a `<RouteTravel prefetch>` asks of the pages
9730
+ * on each side of the one on screen.
9646
9731
  * @param {object} [options]
9647
9732
  * @param {() => any} [options.when] - a condition on app state the action
9648
9733
  * asks under, read off signals like the params: falsy, it runs nothing.
@@ -9723,7 +9808,7 @@ const routeAction = (
9723
9808
  for (const route of routes) {
9724
9809
  if (paramsEffect) {
9725
9810
  registerRouteStatePreload(route, (values) => {
9726
- prefetchAsIf(action, readParams, values);
9811
+ prefetchAsIf(action, readParams, readParamsAsked, values);
9727
9812
  });
9728
9813
  } else {
9729
9814
  registerRoutePreload(route, () => {
@@ -9739,15 +9824,17 @@ const routeAction = (
9739
9824
  };
9740
9825
 
9741
9826
  // The params as they will read once the states hold what they are about to
9742
- // (see readStateAsIf), prerun only when that changes them: params the state
9743
- // leaves alone are the ones the action already runs on — or is meant not to,
9744
- // and a prerun would retry a FAILED one behind the screen's back.
9745
- const prefetchAsIf = (action, readParams, values) => {
9827
+ // (see readStateAsIf), prerun only when they differ from what the action asks
9828
+ // now: params the states leave alone are the ones it already runs on — or is
9829
+ // meant not to, and a prerun would retry a FAILED one behind the screen's back.
9830
+ // An action whose route does not match asks nothing: whatever its params read,
9831
+ // the arrival runs it on them.
9832
+ const prefetchAsIf = (action, readParams, readParamsAsked, values) => {
9746
9833
  const params = readStateAsIf(values, readParams);
9747
9834
  if (!params) {
9748
9835
  return;
9749
9836
  }
9750
- if (compareTwoJsValues(params, untracked(readParams))) {
9837
+ if (compareTwoJsValues(params, untracked(readParamsAsked))) {
9751
9838
  return;
9752
9839
  }
9753
9840
  prerunAhead(action.bindParams(params));
@@ -40652,6 +40739,7 @@ const ROW_READING = {
40652
40739
  * travelByDrag?: boolean,
40653
40740
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
40654
40741
  * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
40742
+ * prefetch?: boolean,
40655
40743
  * }>}
40656
40744
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
40657
40745
  * over something longer, in order. Read from the <Route> children by default,
@@ -40675,6 +40763,12 @@ const ROW_READING = {
40675
40763
  * `{ route, params }`, or the entry of `routes` as it was given, and is a column
40676
40764
  * the height of the row, as the page is. Its route does not match while it stands
40677
40765
  * there: what it reads is not asked for.
40766
+ * @param {boolean} [props.prefetch=false] - whether the pages on each side of the one
40767
+ * on screen have their reads asked for ahead of the hand: at rest, once the page on
40768
+ * screen has its answers and has painted them, the next page first, each with
40769
+ * `route.preload(params)`. A swipe then builds a page whose answers are already
40770
+ * there, with no wait to show. Off by default: every page one stops on costs the
40771
+ * reads of two more.
40678
40772
  *
40679
40773
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
40680
40774
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -40691,6 +40785,7 @@ const RouteTravel = ({
40691
40785
  routeTransition: "none"
40692
40786
  }),
40693
40787
  renderNeighbour,
40788
+ prefetch = false,
40694
40789
  className,
40695
40790
  children,
40696
40791
  ...rest
@@ -40700,7 +40795,8 @@ const RouteTravel = ({
40700
40795
  const liveRef = useRef();
40701
40796
  const picturesRef = useRef();
40702
40797
  const windowed = Boolean(routesProp);
40703
- const [, render] = useReducer(count => count + 1, 0);
40798
+ // Counts the movements ended: endMovement renders through it.
40799
+ const [restCount, render] = useReducer(count => count + 1, 0);
40704
40800
  const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
40705
40801
  const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
40706
40802
  // Held while the row moves: a window recentred under a hand would move the very
@@ -41274,6 +41370,45 @@ const RouteTravel = ({
41274
41370
  }
41275
41371
  }, [currentIndex]);
41276
41372
 
41373
+ // What a hand can bring is asked for at rest, the next page first: never before
41374
+ // the page on screen has its answers and has painted them. A row in movement is
41375
+ // not at rest; the render that ends the movement asks again. Read as pages, not as
41376
+ // entries: the pages of <Route> children are collected again at every render.
41377
+ const neighboursRef = useRef([null, null]);
41378
+ const nextPage = currentIndex === -1 ? null : pages[currentIndex + 1] || null;
41379
+ const previousPage = currentIndex === -1 ? null : pages[currentIndex - 1] || null;
41380
+ if (!samePage(neighboursRef.current[0], nextPage) || !samePage(neighboursRef.current[1], previousPage)) {
41381
+ neighboursRef.current = [nextPage, previousPage];
41382
+ }
41383
+ const neighbours = neighboursRef.current;
41384
+ useLayoutEffect(() => {
41385
+ if (!prefetch || movementRef.current) {
41386
+ return undefined;
41387
+ }
41388
+ if (!neighbours[0] && !neighbours[1]) {
41389
+ return undefined;
41390
+ }
41391
+ let cancelPaint = null;
41392
+ const cancelWait = whenPageReadsAnswered(() => {
41393
+ cancelPaint = afterPaint(() => {
41394
+ if (movementRef.current) {
41395
+ return;
41396
+ }
41397
+ for (const page of neighbours) {
41398
+ if (page) {
41399
+ page.route.preload(page.params || {});
41400
+ }
41401
+ }
41402
+ });
41403
+ });
41404
+ return () => {
41405
+ cancelWait();
41406
+ if (cancelPaint) {
41407
+ cancelPaint();
41408
+ }
41409
+ };
41410
+ }, [prefetch, neighbours, restCount]);
41411
+
41277
41412
  // A neighbour shows from the first frame the compositor moves the row, before
41278
41413
  // any scroll event: a finger landing is the last word before that.
41279
41414
  const paintsNeighbours = Boolean(renderNeighbour);