@jsenv/navi 0.30.9 → 0.30.11

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.
@@ -4302,9 +4302,10 @@ const createAction = (callback, rootOptions = {}) => {
4302
4302
  }
4303
4303
  }
4304
4304
  runningStateSignal.value = RUNNING;
4305
- if (!isPrerun) {
4306
- isPrerunSignal.value = false;
4307
- }
4305
+ // What fills the instance says what it holds: an answer only a
4306
+ // prerun asked for is one nothing on screen has read yet, and the
4307
+ // run that asks for it next is promoted onto it (see updateActions).
4308
+ isPrerunSignal.value = isPrerun;
4308
4309
  });
4309
4310
 
4310
4311
  const args = [];
@@ -5106,10 +5107,16 @@ const actionRunEffect = (
5106
5107
  runUnwatched(() =>
5107
5108
  actionTarget.run({ reason: "params restored from falsy state" }),
5108
5109
  );
5109
- } else if (getActionPrivateProperties(actionTarget).keepsAnswers) {
5110
+ } else if (
5111
+ getActionPrivateProperties(actionTarget).keepsAnswers ||
5112
+ actionTarget.isPrerunSignal.peek()
5113
+ ) {
5110
5114
  // The lookup that moved the binding here already reset an answer
5111
5115
  // older than its `keep`: what the instance holds is still good, and
5112
- // a prerun still out is joined rather than asked again.
5116
+ // a prerun still out is joined rather than asked again. So is an
5117
+ // answer only a prerun asked for, kept or not: asked ahead for this
5118
+ // very arrival (a <RouteTravel prefetch> neighbour), nothing has read
5119
+ // it yet, and the run promotes it.
5113
5120
  runUnwatched(() => actionTarget.run({ reason: "params modified" }));
5114
5121
  } else {
5115
5122
  runUnwatched(() => actionTarget.rerun({ reason: "params modified" }));
@@ -8425,17 +8432,35 @@ const route = (pattern, options = {}) => {
8425
8432
  return `route "${cleanPattern}"`;
8426
8433
  },
8427
8434
  };
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 = () => {
8435
+ /**
8436
+ * Asks ahead of the arrival for what this route's actions will ask: the
8437
+ * instances the arrival promotes to runs (see prerunAhead in route_action.js).
8438
+ *
8439
+ * Without params, only what asks nothing of the address: the page's code, a
8440
+ * read without params — what a link preloads, since it has no state to read
8441
+ * the params of. Given params, `{}` included, the address `buildUrl(params)`
8442
+ * leads to: every route action of this route reads its params as if the
8443
+ * route matched there (see statesAt).
8444
+ *
8445
+ * @param {object} [params]
8446
+ */
8447
+ route.preload = (params) => {
8433
8448
  const preloadSet = routePreloadMap.get(route);
8434
- if (!preloadSet) {
8449
+ if (preloadSet) {
8450
+ for (const preload of preloadSet) {
8451
+ preload();
8452
+ }
8453
+ }
8454
+ if (params === undefined) {
8435
8455
  return;
8436
8456
  }
8437
- for (const preload of preloadSet) {
8438
- preload();
8457
+ const statePreloadSet = routeStatePreloadMap.get(route);
8458
+ if (!statePreloadSet) {
8459
+ return;
8460
+ }
8461
+ const values = statesAt(route, params);
8462
+ for (const preload of statePreloadSet) {
8463
+ preload(values);
8439
8464
  }
8440
8465
  };
8441
8466
  Object.preventExtensions(route);
@@ -9034,6 +9059,42 @@ const preloadState = (signal, value) => {
9034
9059
  }
9035
9060
  }
9036
9061
  };
9062
+ // What the states of a route will hold once the address is `route.buildUrl(params)`,
9063
+ // as the routing pass will write them (see sync_routes_with_url): a param given is
9064
+ // what its state holds there; a weak one not given is back to its default, a url
9065
+ // built for another visit never carries it (readSignalForUrlBuild); any other one
9066
+ // is carried over by that url and keeps what it holds, so it is left out.
9067
+ const statesAt = (route, params) => {
9068
+ const values = new Map();
9069
+ for (const connection of getRoutePrivateProperties(route).routePattern
9070
+ .connections) {
9071
+ const { paramName, signal } = connection;
9072
+ if (!signal) {
9073
+ continue;
9074
+ }
9075
+ if (!(paramName in params)) {
9076
+ if (connection.weak) {
9077
+ values.set(signal, connection.getDefaultValue());
9078
+ }
9079
+ continue;
9080
+ }
9081
+ const value = params[paramName];
9082
+ if (value !== undefined) {
9083
+ values.set(signal, value);
9084
+ continue;
9085
+ }
9086
+ // An address silent about a persisted search param leaves it alone.
9087
+ if (
9088
+ connection.persists &&
9089
+ connection.paramType === "query" &&
9090
+ connection.history !== "push"
9091
+ ) {
9092
+ continue;
9093
+ }
9094
+ values.set(signal, connection.getDefaultValue());
9095
+ }
9096
+ return values;
9097
+ };
9037
9098
  const setupRoutes = (routes) => {
9038
9099
  if (setupRoutesCalled) {
9039
9100
  throw new Error(
@@ -9562,6 +9623,35 @@ const activeRouteActionsSignal = computed(() => {
9562
9623
  return active;
9563
9624
  });
9564
9625
 
9626
+ /**
9627
+ * Calls back once every route action asking something for the page on screen
9628
+ * has answered, none running and none about to: what is asked on behalf of
9629
+ * another page waits for it, and never competes with the page's own reads.
9630
+ *
9631
+ * @param {() => void} callback
9632
+ * @returns {() => void} cancel
9633
+ */
9634
+ const whenPageReadsAnswered = (callback) => {
9635
+ let cancelled = false;
9636
+ const stop = effect(function () {
9637
+ for (const routeAction of activeRouteActionsSignal.value) {
9638
+ const runningState = routeAction.runningStateSignal.value;
9639
+ // IDLE: asking, and run by an effect of the same batch that has not run yet.
9640
+ if (runningState === RUNNING || runningState === IDLE) {
9641
+ return;
9642
+ }
9643
+ }
9644
+ this.dispose();
9645
+ if (!cancelled) {
9646
+ untracked(callback);
9647
+ }
9648
+ });
9649
+ return () => {
9650
+ cancelled = true;
9651
+ stop();
9652
+ };
9653
+ };
9654
+
9565
9655
  /**
9566
9656
  * Whether a route action of the page on screen waits for app state to ask: its
9567
9657
  * route matches and its `when` says no. Params that say no are not waiting:
@@ -9642,7 +9732,9 @@ const activeRouteReadsSignal = computed(() => {
9642
9732
  * come from the address is not prefetched by a link — a link's prefetch has
9643
9733
  * no address. It is by the press that opens a popup bound to a state these
9644
9734
  * params read: that address is the current one with the state set to the
9645
- * popup's value (see `prefetch` on Button).
9735
+ * popup's value (see `prefetch` on Button). And by `route.preload(params)`,
9736
+ * which names the address: what a `<RouteTravel prefetch>` asks of the pages
9737
+ * on each side of the one on screen.
9646
9738
  * @param {object} [options]
9647
9739
  * @param {() => any} [options.when] - a condition on app state the action
9648
9740
  * asks under, read off signals like the params: falsy, it runs nothing.
@@ -9723,7 +9815,7 @@ const routeAction = (
9723
9815
  for (const route of routes) {
9724
9816
  if (paramsEffect) {
9725
9817
  registerRouteStatePreload(route, (values) => {
9726
- prefetchAsIf(action, readParams, values);
9818
+ prefetchAsIf(action, readParams, readParamsAsked, values);
9727
9819
  });
9728
9820
  } else {
9729
9821
  registerRoutePreload(route, () => {
@@ -9739,15 +9831,17 @@ const routeAction = (
9739
9831
  };
9740
9832
 
9741
9833
  // 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) => {
9834
+ // (see readStateAsIf), prerun only when they differ from what the action asks
9835
+ // now: params the states leave alone are the ones it already runs on — or is
9836
+ // meant not to, and a prerun would retry a FAILED one behind the screen's back.
9837
+ // An action whose route does not match asks nothing: whatever its params read,
9838
+ // the arrival runs it on them.
9839
+ const prefetchAsIf = (action, readParams, readParamsAsked, values) => {
9746
9840
  const params = readStateAsIf(values, readParams);
9747
9841
  if (!params) {
9748
9842
  return;
9749
9843
  }
9750
- if (compareTwoJsValues(params, untracked(readParams))) {
9844
+ if (compareTwoJsValues(params, untracked(readParamsAsked))) {
9751
9845
  return;
9752
9846
  }
9753
9847
  prerunAhead(action.bindParams(params));
@@ -28021,6 +28115,16 @@ const createResourceLifecycleManager = () => {
28021
28115
  }
28022
28116
  }
28023
28117
 
28118
+ // A head start the write made stale is dropped, not asked again: nothing
28119
+ // on screen reads it, and its owner, finding it idle, asks on arriving
28120
+ // instead of promoting what left before the write.
28121
+ for (const action of actionsToRerun) {
28122
+ if (action.isPrerunSignal.peek()) {
28123
+ actionsToRerun.delete(action);
28124
+ actionsToReset.add(action);
28125
+ }
28126
+ }
28127
+
28024
28128
  return {
28025
28129
  actionsToRerun,
28026
28130
  actionsToReset,
@@ -28108,9 +28212,8 @@ const createResourceLifecycleManager = () => {
28108
28212
  // A read to ask again after a write: answered before it landed, or still out —
28109
28213
  // an answer that left before the write is as stale as a completed one, and
28110
28214
  // would replace what the write put in the store (a rerun aborts it and asks
28111
- // again). A head start is its owner's to ask.
28215
+ // again). A head start is its owner's to ask (findEffectOnActions resets it).
28112
28216
  const isAnsweredOrAsking = (action, triggeringAction) =>
28113
- !action.isPrerun &&
28114
28217
  (action.completed || action.runningState === RUNNING) &&
28115
28218
  action !== triggeringAction;
28116
28219
  // A write named in `writes` says for itself which reads of its own resource it
@@ -40652,6 +40755,7 @@ const ROW_READING = {
40652
40755
  * travelByDrag?: boolean,
40653
40756
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
40654
40757
  * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
40758
+ * prefetch?: boolean,
40655
40759
  * }>}
40656
40760
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
40657
40761
  * over something longer, in order. Read from the <Route> children by default,
@@ -40675,6 +40779,12 @@ const ROW_READING = {
40675
40779
  * `{ route, params }`, or the entry of `routes` as it was given, and is a column
40676
40780
  * the height of the row, as the page is. Its route does not match while it stands
40677
40781
  * there: what it reads is not asked for.
40782
+ * @param {boolean} [props.prefetch=false] - whether the pages on each side of the one
40783
+ * on screen have their reads asked for ahead of the hand: at rest, once the page on
40784
+ * screen has its answers and has painted them, the next page first, each with
40785
+ * `route.preload(params)`. A swipe then builds a page whose answers are already
40786
+ * there, with no wait to show. Off by default: every page one stops on costs the
40787
+ * reads of two more.
40678
40788
  *
40679
40789
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
40680
40790
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -40691,6 +40801,7 @@ const RouteTravel = ({
40691
40801
  routeTransition: "none"
40692
40802
  }),
40693
40803
  renderNeighbour,
40804
+ prefetch = false,
40694
40805
  className,
40695
40806
  children,
40696
40807
  ...rest
@@ -40700,7 +40811,8 @@ const RouteTravel = ({
40700
40811
  const liveRef = useRef();
40701
40812
  const picturesRef = useRef();
40702
40813
  const windowed = Boolean(routesProp);
40703
- const [, render] = useReducer(count => count + 1, 0);
40814
+ // Counts the movements ended: endMovement renders through it.
40815
+ const [restCount, render] = useReducer(count => count + 1, 0);
40704
40816
  const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
40705
40817
  const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
40706
40818
  // Held while the row moves: a window recentred under a hand would move the very
@@ -41274,6 +41386,45 @@ const RouteTravel = ({
41274
41386
  }
41275
41387
  }, [currentIndex]);
41276
41388
 
41389
+ // What a hand can bring is asked for at rest, the next page first: never before
41390
+ // the page on screen has its answers and has painted them. A row in movement is
41391
+ // not at rest; the render that ends the movement asks again. Read as pages, not as
41392
+ // entries: the pages of <Route> children are collected again at every render.
41393
+ const neighboursRef = useRef([null, null]);
41394
+ const nextPage = currentIndex === -1 ? null : pages[currentIndex + 1] || null;
41395
+ const previousPage = currentIndex === -1 ? null : pages[currentIndex - 1] || null;
41396
+ if (!samePage(neighboursRef.current[0], nextPage) || !samePage(neighboursRef.current[1], previousPage)) {
41397
+ neighboursRef.current = [nextPage, previousPage];
41398
+ }
41399
+ const neighbours = neighboursRef.current;
41400
+ useLayoutEffect(() => {
41401
+ if (!prefetch || movementRef.current) {
41402
+ return undefined;
41403
+ }
41404
+ if (!neighbours[0] && !neighbours[1]) {
41405
+ return undefined;
41406
+ }
41407
+ let cancelPaint = null;
41408
+ const cancelWait = whenPageReadsAnswered(() => {
41409
+ cancelPaint = afterPaint(() => {
41410
+ if (movementRef.current) {
41411
+ return;
41412
+ }
41413
+ for (const page of neighbours) {
41414
+ if (page) {
41415
+ page.route.preload(page.params || {});
41416
+ }
41417
+ }
41418
+ });
41419
+ });
41420
+ return () => {
41421
+ cancelWait();
41422
+ if (cancelPaint) {
41423
+ cancelPaint();
41424
+ }
41425
+ };
41426
+ }, [prefetch, neighbours, restCount]);
41427
+
41277
41428
  // A neighbour shows from the first frame the compositor moves the row, before
41278
41429
  // any scroll event: a finger landing is the last word before that.
41279
41430
  const paintsNeighbours = Boolean(renderNeighbour);