@jsenv/navi 0.29.325 → 0.29.327

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.
@@ -3235,6 +3235,15 @@ const resolveActionProxies = (actionSet) => {
3235
3235
 
3236
3236
  const NO_PARAMS = { __no_params__: true };
3237
3237
  const mergeActionParams = (currentParams, newParams) => {
3238
+ if (newParams === null) {
3239
+ // `null` is a question that cannot be asked: a callback destructuring its
3240
+ // params throws on it. It is what a computed says when it has not got
3241
+ // enough to ask with, and "nothing to ask" is said with `undefined`
3242
+ // everywhere below (and by the run guards reading paramsSignal), so it
3243
+ // becomes that here, at the one door params coming from a binding go
3244
+ // through.
3245
+ newParams = undefined;
3246
+ }
3238
3247
  // The order of these two checks is load-bearing. Checking `undefined` first
3239
3248
  // looks symmetric — "no new params, keep whatever is there, NO_PARAMS
3240
3249
  // included" — and breaks routing: merge(NO_PARAMS, undefined) must yield
@@ -3492,7 +3501,10 @@ const createAction = (callback, rootOptions = {}) => {
3492
3501
  *
3493
3502
  * @param {any} newParamsOrSignal - params, or a signal holding them (the
3494
3503
  * result then retargets itself as the signal changes), or an object whose
3495
- * values may be signals.
3504
+ * values may be signals. A signal with nothing to ask yet — a question
3505
+ * computed from a form still missing a field — holds `undefined` or
3506
+ * `null`: the instance then has no params, which is what
3507
+ * `useAsyncData(action, { run: true })` reads to start nothing.
3496
3508
  * @param {object} [options]
3497
3509
  * @param {number} [options.debounce] - milliseconds the params must stay
3498
3510
  * stable before the instance follows them. What it buys is a screen that
@@ -11276,9 +11288,11 @@ const swipeTypeOf = (axis, pulled) => {
11276
11288
  * copy is captured where it lands rather than where it was let go of.
11277
11289
  *
11278
11290
  * And the promise matters in both cases: the gesture holds its copy until the
11279
- * answer settles. Returning the transition is what makes a landing continuous; a
11280
- * `toss` that rejects brings the copy back, because the thing still exists and the
11281
- * screen has to say so.
11291
+ * answer settles. Returning the transition is what makes a landing continuous;
11292
+ * rejecting brings the copy back over the original, because the thing still
11293
+ * exists and the screen has to say so. That holds for every outcome that carries
11294
+ * a copy — `land` and `reorder` as much as `toss` and `leave` — so a place can be
11295
+ * asked about rather than taken: refuse, and the board is as it was.
11282
11296
  *
11283
11297
  * Starting a document transition is the application's call and not navi's: a
11284
11298
  * `view-transition-name` must be unique per document, so only the application can
@@ -17101,6 +17115,11 @@ const generateSignalId = () => {
17101
17115
  * as the user moved. `"push"` is for a state whose values ARE places one came
17102
17116
  * from (the photo one is looking at in a gallery). Whatever is said here, one
17103
17117
  * write can say otherwise: `signal.set(value, { history })`.
17118
+ * Either way every write reaches the address, synchronously, and browsers
17119
+ * refuse an address written too often (Safari: 100 writes per 10 s, then a
17120
+ * SecurityError). A value one DRAGS is written at 60 Hz: keep it in the
17121
+ * gesture while the finger is down and write the state once, on release —
17122
+ * an address is not a recording of a gesture.
17104
17123
  * @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
17105
17124
  * @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
17106
17125
  *
@@ -27294,11 +27313,7 @@ const setupBrowserIntegrationViaHistory = ({
27294
27313
  [NAV_DEPTH_STATE_KEY]: getNavDepth(),
27295
27314
  },
27296
27315
  });
27297
- if (navigationType === "push") {
27298
- window.history.pushState(effectiveState, null, url);
27299
- } else {
27300
- window.history.replaceState(effectiveState, null, url);
27301
- }
27316
+ writeHistoryEntry(navigationType, effectiveState, url);
27302
27317
  rememberEntryIsOfThisDocument();
27303
27318
  updateDocumentUrl(url);
27304
27319
  updateDocumentState(effectiveState);
@@ -27318,14 +27333,14 @@ const setupBrowserIntegrationViaHistory = ({
27318
27333
  [NAV_DEPTH_STATE_KEY]: getNavDepth(),
27319
27334
  },
27320
27335
  });
27321
- window.history.replaceState(state, null, url);
27336
+ writeHistoryEntry("replace", state, url);
27322
27337
  rememberEntryIsOfThisDocument();
27323
27338
  } else if (redirected) {
27324
27339
  // The entry the browser is on names an address that only sends
27325
27340
  // elsewhere — a cold load on it, or a back into it. Written over where
27326
27341
  // it stands (the entry keeps its place in the stack, hence its state
27327
27342
  // and the depth in it): pressing back must not walk into it again.
27328
- window.history.replaceState(state, null, url);
27343
+ writeHistoryEntry("replace", state, url);
27329
27344
  rememberEntryIsOfThisDocument();
27330
27345
  }
27331
27346
  updateDocumentUrl(url);
@@ -27578,7 +27593,7 @@ const setupBrowserIntegrationViaHistory = ({
27578
27593
  // The entry itself has to lose them too, not just the document state:
27579
27594
  // getDocumentState() reads the entry back, and every state write copies
27580
27595
  // what it finds there onto the next one.
27581
- window.history.replaceState(state, null, url);
27596
+ writeHistoryEntry("replace", state, url);
27582
27597
  }
27583
27598
  handleRoutingTask(url, {
27584
27599
  reason: "routing initialization",
@@ -27605,6 +27620,57 @@ const setupBrowserIntegrationViaHistory = ({
27605
27620
  // replace onto the url already displayed. Never a traverse — the browser has
27606
27621
  // already moved window.location.href when a popstate is handled, so the
27607
27622
  // comparison would say "same" about every back and forward.
27623
+ // Browsers refuse an address written too often: WebKit throws a SecurityError
27624
+ // past 100 writes per 10 seconds, Chromium and Firefox drop the write. A state
27625
+ // bound to the url and written per frame gets there in under two seconds, and
27626
+ // a developer working in Chromium never sees it. Every write goes through here
27627
+ // so the rate is measured against the strictest budget before any browser
27628
+ // refuses, and so a refusal names its cause rather than `replaceState`.
27629
+ const HISTORY_WRITE_BUDGET = 100;
27630
+ const HISTORY_WRITE_WINDOW_MS = 10_000;
27631
+ const ADDRESS_WRITE_ADVICE = `An address is not a recording of a gesture: a value dragged per frame belongs to the gesture, and the state bound to the url is written once, on release.`;
27632
+ const writeHistoryEntry = (navigationType, state, url) => {
27633
+ noteHistoryWrite(url);
27634
+ try {
27635
+ if (navigationType === "push") {
27636
+ window.history.pushState(state, null, url);
27637
+ } else {
27638
+ window.history.replaceState(state, null, url);
27639
+ }
27640
+ } catch (e) {
27641
+ if (e.name !== "SecurityError") {
27642
+ throw e;
27643
+ }
27644
+ throw new Error(
27645
+ `The browser refused to write the address ${url}: ${e.message}. ${ADDRESS_WRITE_ADVICE}`,
27646
+ { cause: e },
27647
+ );
27648
+ }
27649
+ };
27650
+ const noteHistoryWrite = (() => {
27651
+ const writeTimes = [];
27652
+ let warned = false;
27653
+ return (url) => {
27654
+ if (warned) {
27655
+ return;
27656
+ }
27657
+ const now = performance.now();
27658
+ writeTimes.push(now);
27659
+ while (writeTimes[0] < now - HISTORY_WRITE_WINDOW_MS) {
27660
+ writeTimes.shift();
27661
+ }
27662
+ if (writeTimes.length < HISTORY_WRITE_BUDGET) {
27663
+ return;
27664
+ }
27665
+ warned = true;
27666
+ const seconds = ((now - writeTimes[0]) / 1000).toFixed(1);
27667
+ console.warn(
27668
+ `[navi] the address was written ${HISTORY_WRITE_BUDGET} times in ${seconds}s (last: ${url}). Safari refuses more than ${HISTORY_WRITE_BUDGET} writes per ${HISTORY_WRITE_WINDOW_MS / 1000}s with a SecurityError, and the state then runs ahead of the address. ${ADDRESS_WRITE_ADVICE}`,
27669
+ );
27670
+ };
27671
+ })()
27672
+ ;
27673
+
27608
27674
  const isStateOnlyNavigation = (
27609
27675
  url,
27610
27676
  { navigationType },