@jsenv/navi 0.29.325 → 0.29.326

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
@@ -17101,6 +17113,11 @@ const generateSignalId = () => {
17101
17113
  * as the user moved. `"push"` is for a state whose values ARE places one came
17102
17114
  * from (the photo one is looking at in a gallery). Whatever is said here, one
17103
17115
  * write can say otherwise: `signal.set(value, { history })`.
17116
+ * Either way every write reaches the address, synchronously, and browsers
17117
+ * refuse an address written too often (Safari: 100 writes per 10 s, then a
17118
+ * SecurityError). A value one DRAGS is written at 60 Hz: keep it in the
17119
+ * gesture while the finger is down and write the state once, on release —
17120
+ * an address is not a recording of a gesture.
17104
17121
  * @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
17105
17122
  * @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
17123
  *
@@ -27294,11 +27311,7 @@ const setupBrowserIntegrationViaHistory = ({
27294
27311
  [NAV_DEPTH_STATE_KEY]: getNavDepth(),
27295
27312
  },
27296
27313
  });
27297
- if (navigationType === "push") {
27298
- window.history.pushState(effectiveState, null, url);
27299
- } else {
27300
- window.history.replaceState(effectiveState, null, url);
27301
- }
27314
+ writeHistoryEntry(navigationType, effectiveState, url);
27302
27315
  rememberEntryIsOfThisDocument();
27303
27316
  updateDocumentUrl(url);
27304
27317
  updateDocumentState(effectiveState);
@@ -27318,14 +27331,14 @@ const setupBrowserIntegrationViaHistory = ({
27318
27331
  [NAV_DEPTH_STATE_KEY]: getNavDepth(),
27319
27332
  },
27320
27333
  });
27321
- window.history.replaceState(state, null, url);
27334
+ writeHistoryEntry("replace", state, url);
27322
27335
  rememberEntryIsOfThisDocument();
27323
27336
  } else if (redirected) {
27324
27337
  // The entry the browser is on names an address that only sends
27325
27338
  // elsewhere — a cold load on it, or a back into it. Written over where
27326
27339
  // it stands (the entry keeps its place in the stack, hence its state
27327
27340
  // and the depth in it): pressing back must not walk into it again.
27328
- window.history.replaceState(state, null, url);
27341
+ writeHistoryEntry("replace", state, url);
27329
27342
  rememberEntryIsOfThisDocument();
27330
27343
  }
27331
27344
  updateDocumentUrl(url);
@@ -27578,7 +27591,7 @@ const setupBrowserIntegrationViaHistory = ({
27578
27591
  // The entry itself has to lose them too, not just the document state:
27579
27592
  // getDocumentState() reads the entry back, and every state write copies
27580
27593
  // what it finds there onto the next one.
27581
- window.history.replaceState(state, null, url);
27594
+ writeHistoryEntry("replace", state, url);
27582
27595
  }
27583
27596
  handleRoutingTask(url, {
27584
27597
  reason: "routing initialization",
@@ -27605,6 +27618,57 @@ const setupBrowserIntegrationViaHistory = ({
27605
27618
  // replace onto the url already displayed. Never a traverse — the browser has
27606
27619
  // already moved window.location.href when a popstate is handled, so the
27607
27620
  // comparison would say "same" about every back and forward.
27621
+ // Browsers refuse an address written too often: WebKit throws a SecurityError
27622
+ // past 100 writes per 10 seconds, Chromium and Firefox drop the write. A state
27623
+ // bound to the url and written per frame gets there in under two seconds, and
27624
+ // a developer working in Chromium never sees it. Every write goes through here
27625
+ // so the rate is measured against the strictest budget before any browser
27626
+ // refuses, and so a refusal names its cause rather than `replaceState`.
27627
+ const HISTORY_WRITE_BUDGET = 100;
27628
+ const HISTORY_WRITE_WINDOW_MS = 10_000;
27629
+ 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.`;
27630
+ const writeHistoryEntry = (navigationType, state, url) => {
27631
+ noteHistoryWrite(url);
27632
+ try {
27633
+ if (navigationType === "push") {
27634
+ window.history.pushState(state, null, url);
27635
+ } else {
27636
+ window.history.replaceState(state, null, url);
27637
+ }
27638
+ } catch (e) {
27639
+ if (e.name !== "SecurityError") {
27640
+ throw e;
27641
+ }
27642
+ throw new Error(
27643
+ `The browser refused to write the address ${url}: ${e.message}. ${ADDRESS_WRITE_ADVICE}`,
27644
+ { cause: e },
27645
+ );
27646
+ }
27647
+ };
27648
+ const noteHistoryWrite = (() => {
27649
+ const writeTimes = [];
27650
+ let warned = false;
27651
+ return (url) => {
27652
+ if (warned) {
27653
+ return;
27654
+ }
27655
+ const now = performance.now();
27656
+ writeTimes.push(now);
27657
+ while (writeTimes[0] < now - HISTORY_WRITE_WINDOW_MS) {
27658
+ writeTimes.shift();
27659
+ }
27660
+ if (writeTimes.length < HISTORY_WRITE_BUDGET) {
27661
+ return;
27662
+ }
27663
+ warned = true;
27664
+ const seconds = ((now - writeTimes[0]) / 1000).toFixed(1);
27665
+ console.warn(
27666
+ `[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}`,
27667
+ );
27668
+ };
27669
+ })()
27670
+ ;
27671
+
27608
27672
  const isStateOnlyNavigation = (
27609
27673
  url,
27610
27674
  { navigationType },