@jsenv/navi 0.29.392 → 0.29.394
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.
- package/dist/dev/jsenv_navi.js +442 -65
- package/dist/dev/jsenv_navi.js.map +11 -9
- package/dist/jsenv_navi.js +393 -36
- package/dist/jsenv_navi.js.map +11 -9
- package/docs/AI_INSTRUCTIONS.md +4 -2
- package/docs/data_states.md +4 -1
- package/docs/navigation.md +41 -0
- package/docs/network_policy.md +5 -2
- package/docs/resource.md +95 -1
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -16177,6 +16177,219 @@ const getParamScope = (params) => {
|
|
|
16177
16177
|
return newParamScope;
|
|
16178
16178
|
};
|
|
16179
16179
|
|
|
16180
|
+
/*
|
|
16181
|
+
* The rows a resource's GET landed with, mirrored into a signal the app hands
|
|
16182
|
+
* over — a `stateSignal` with `persists` for a copy that survives a reload, so
|
|
16183
|
+
* that the reload starts on the "refresh over a known answer" line of
|
|
16184
|
+
* docs/data_states.md rather than on the first-load one. Where the signal
|
|
16185
|
+
* keeps its value is the signal's business; this module only decides what the
|
|
16186
|
+
* copy contains, when it enters the store, and when it is rewritten.
|
|
16187
|
+
*
|
|
16188
|
+
* What the signal holds: one item per params the GET was asked with (`""` for
|
|
16189
|
+
* a GET without params), as the store holds it with its relations written
|
|
16190
|
+
* inline — `user: { … }`, `my_games: [ … ]`. That is the shape the GET
|
|
16191
|
+
* callback itself returns, so the copy re-enters the store through the same
|
|
16192
|
+
* setters a real answer goes through and its relations normalize into their
|
|
16193
|
+
* own stores.
|
|
16194
|
+
*
|
|
16195
|
+
* It re-enters at the first run of the GET asking for it, never at
|
|
16196
|
+
* declaration: relations are declared after `resource()` returns, and a row
|
|
16197
|
+
* upserted before them keeps its relation values as plain data.
|
|
16198
|
+
*
|
|
16199
|
+
* The write follows the store, not the callback: an effect reads the item and
|
|
16200
|
+
* every row it reaches through a relation, so a PUT on the item or a list
|
|
16201
|
+
* upserting one of its children rewrites the copy. The signal turning
|
|
16202
|
+
* `undefined` from outside is "forget": nothing is written until a GET lands
|
|
16203
|
+
* again.
|
|
16204
|
+
*/
|
|
16205
|
+
const createResourcePersistence = (
|
|
16206
|
+
store,
|
|
16207
|
+
{ idKey, name, signal: persistedSignal, when },
|
|
16208
|
+
) => {
|
|
16209
|
+
if (
|
|
16210
|
+
!persistedSignal ||
|
|
16211
|
+
typeof persistedSignal !== "object" ||
|
|
16212
|
+
typeof persistedSignal.subscribe !== "function"
|
|
16213
|
+
) {
|
|
16214
|
+
throw new TypeError(
|
|
16215
|
+
`resource("${name}").persist needs a signal to write the copy into, received ${persistedSignal}`,
|
|
16216
|
+
);
|
|
16217
|
+
}
|
|
16218
|
+
|
|
16219
|
+
// paramsKey → item the signal holds that is not in the store yet
|
|
16220
|
+
const storedItemMap = new Map();
|
|
16221
|
+
// paramsKey → id of the row the GET holds for these params
|
|
16222
|
+
const entryMapSignal = signal(new Map());
|
|
16223
|
+
const recordItem = (paramsKey, itemId) => {
|
|
16224
|
+
const entryMap = entryMapSignal.peek();
|
|
16225
|
+
if (entryMap.get(paramsKey) === itemId) {
|
|
16226
|
+
return;
|
|
16227
|
+
}
|
|
16228
|
+
const entryMapUpdated = new Map(entryMap);
|
|
16229
|
+
entryMapUpdated.set(paramsKey, itemId);
|
|
16230
|
+
entryMapSignal.value = entryMapUpdated;
|
|
16231
|
+
};
|
|
16232
|
+
const isAllowed = () => {
|
|
16233
|
+
return when ? Boolean(when()) : true;
|
|
16234
|
+
};
|
|
16235
|
+
|
|
16236
|
+
// An empty signal means "nothing is known": whoever emptied it — sign-out
|
|
16237
|
+
// writing undefined, or `when` saying no — nothing is written again until a
|
|
16238
|
+
// GET lands.
|
|
16239
|
+
const forget = () => {
|
|
16240
|
+
storedItemMap.clear();
|
|
16241
|
+
if (entryMapSignal.peek().size > 0) {
|
|
16242
|
+
entryMapSignal.value = new Map();
|
|
16243
|
+
}
|
|
16244
|
+
};
|
|
16245
|
+
let lastWritten;
|
|
16246
|
+
const write = (value) => {
|
|
16247
|
+
if (compareTwoJsValues(value, lastWritten)) {
|
|
16248
|
+
return;
|
|
16249
|
+
}
|
|
16250
|
+
lastWritten = value;
|
|
16251
|
+
persistedSignal.value = value;
|
|
16252
|
+
};
|
|
16253
|
+
// Runs at once with what the signal already holds (the copy read from
|
|
16254
|
+
// storage), then on every write — the module's own ones are recognized
|
|
16255
|
+
// and left alone.
|
|
16256
|
+
persistedSignal.subscribe((value) => {
|
|
16257
|
+
if (value === lastWritten) {
|
|
16258
|
+
return;
|
|
16259
|
+
}
|
|
16260
|
+
lastWritten = value;
|
|
16261
|
+
storedItemMap.clear();
|
|
16262
|
+
if (value === undefined || value === null) {
|
|
16263
|
+
forget();
|
|
16264
|
+
return;
|
|
16265
|
+
}
|
|
16266
|
+
if (typeof value !== "object") {
|
|
16267
|
+
return;
|
|
16268
|
+
}
|
|
16269
|
+
for (const paramsKey of Object.keys(value)) {
|
|
16270
|
+
storedItemMap.set(paramsKey, value[paramsKey]);
|
|
16271
|
+
}
|
|
16272
|
+
});
|
|
16273
|
+
|
|
16274
|
+
const findItem = (params) => {
|
|
16275
|
+
const paramsKey = getParamsKey(params);
|
|
16276
|
+
return untracked(() => {
|
|
16277
|
+
const itemId = entryMapSignal.peek().get(paramsKey);
|
|
16278
|
+
if (itemId !== undefined) {
|
|
16279
|
+
const item = store.select(itemId);
|
|
16280
|
+
if (item) {
|
|
16281
|
+
return item;
|
|
16282
|
+
}
|
|
16283
|
+
}
|
|
16284
|
+
if (!isAllowed()) {
|
|
16285
|
+
return null;
|
|
16286
|
+
}
|
|
16287
|
+
const storedItem = storedItemMap.get(paramsKey);
|
|
16288
|
+
if (!storedItem || typeof storedItem !== "object") {
|
|
16289
|
+
return null;
|
|
16290
|
+
}
|
|
16291
|
+
storedItemMap.delete(paramsKey);
|
|
16292
|
+
const item = store.upsert(storedItem);
|
|
16293
|
+
recordItem(paramsKey, item[idKey]);
|
|
16294
|
+
return item;
|
|
16295
|
+
});
|
|
16296
|
+
};
|
|
16297
|
+
const recordGetItem = (params, itemId) => {
|
|
16298
|
+
recordItem(getParamsKey(params), itemId);
|
|
16299
|
+
};
|
|
16300
|
+
|
|
16301
|
+
effect(() => {
|
|
16302
|
+
const entryMap = entryMapSignal.value;
|
|
16303
|
+
if (!isAllowed()) {
|
|
16304
|
+
write(undefined);
|
|
16305
|
+
forget();
|
|
16306
|
+
return;
|
|
16307
|
+
}
|
|
16308
|
+
if (entryMap.size === 0) {
|
|
16309
|
+
// Nothing is known yet: the copy is what a GET will draw, it must not
|
|
16310
|
+
// be erased by a page that has not asked anything.
|
|
16311
|
+
return;
|
|
16312
|
+
}
|
|
16313
|
+
const stored = {};
|
|
16314
|
+
for (const [paramsKey, storedItem] of storedItemMap) {
|
|
16315
|
+
stored[paramsKey] = storedItem;
|
|
16316
|
+
}
|
|
16317
|
+
for (const [paramsKey, itemId] of entryMap) {
|
|
16318
|
+
const item = store.select(itemId);
|
|
16319
|
+
if (!item) {
|
|
16320
|
+
continue;
|
|
16321
|
+
}
|
|
16322
|
+
stored[paramsKey] = serializeItem(item, new Set());
|
|
16323
|
+
}
|
|
16324
|
+
write(stored);
|
|
16325
|
+
});
|
|
16326
|
+
|
|
16327
|
+
return {
|
|
16328
|
+
findItem,
|
|
16329
|
+
recordGetItem,
|
|
16330
|
+
};
|
|
16331
|
+
};
|
|
16332
|
+
|
|
16333
|
+
const getParamsKey = (params) => {
|
|
16334
|
+
if (params === undefined || params === NO_PARAMS) {
|
|
16335
|
+
return "";
|
|
16336
|
+
}
|
|
16337
|
+
return stableStringify(params);
|
|
16338
|
+
};
|
|
16339
|
+
|
|
16340
|
+
const stableStringify = (value) => {
|
|
16341
|
+
if (value === null || typeof value !== "object") {
|
|
16342
|
+
return JSON.stringify(value);
|
|
16343
|
+
}
|
|
16344
|
+
if (Array.isArray(value)) {
|
|
16345
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
16346
|
+
}
|
|
16347
|
+
const keys = Object.keys(value).sort();
|
|
16348
|
+
const parts = [];
|
|
16349
|
+
for (const key of keys) {
|
|
16350
|
+
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
16351
|
+
}
|
|
16352
|
+
return `{${parts.join(",")}}`;
|
|
16353
|
+
};
|
|
16354
|
+
|
|
16355
|
+
// The item with its relations inline: a relation value stands for what its
|
|
16356
|
+
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
16357
|
+
// twice on the same path is written as its id — the setters accept both.
|
|
16358
|
+
const serializeItem = (item, ancestorSet) => {
|
|
16359
|
+
ancestorSet.add(item);
|
|
16360
|
+
const serialized = {};
|
|
16361
|
+
for (const key of Object.keys(item)) {
|
|
16362
|
+
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
16363
|
+
}
|
|
16364
|
+
ancestorSet.delete(item);
|
|
16365
|
+
return serialized;
|
|
16366
|
+
};
|
|
16367
|
+
const serializeValue = (value, ancestorSet) => {
|
|
16368
|
+
if (value === null || typeof value !== "object") {
|
|
16369
|
+
return value;
|
|
16370
|
+
}
|
|
16371
|
+
if (Array.isArray(value)) {
|
|
16372
|
+
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
16373
|
+
}
|
|
16374
|
+
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
16375
|
+
if (objectSignal) {
|
|
16376
|
+
const held = objectSignal.value;
|
|
16377
|
+
if (held === undefined || held === null) {
|
|
16378
|
+
return null;
|
|
16379
|
+
}
|
|
16380
|
+
return serializeValue(held, ancestorSet);
|
|
16381
|
+
}
|
|
16382
|
+
if (typeof value.toJSON === "function") {
|
|
16383
|
+
return value;
|
|
16384
|
+
}
|
|
16385
|
+
if (ancestorSet.has(value)) {
|
|
16386
|
+
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
16387
|
+
? value[SYMBOL_IDENTITY]
|
|
16388
|
+
: null;
|
|
16389
|
+
}
|
|
16390
|
+
return serializeItem(value, ancestorSet);
|
|
16391
|
+
};
|
|
16392
|
+
|
|
16180
16393
|
/*
|
|
16181
16394
|
* GET_RANGE: reading a resource one slice at a time, for a list that draws its
|
|
16182
16395
|
* rows as it goes (`<List.Items itemsAction>`).
|
|
@@ -16427,6 +16640,20 @@ const debug$3 = (...args) => {
|
|
|
16427
16640
|
* @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
16428
16641
|
* @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
|
|
16429
16642
|
* @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
|
|
16643
|
+
* @param {{ signal: import("@preact/signals").Signal, when?: () => boolean }} [restCallbacks.persist] -
|
|
16644
|
+
* mirrors the rows `GET` lands with into `signal` — a `stateSignal` with
|
|
16645
|
+
* `persists: true` and `type: "object"` keeps them across a reload, whose
|
|
16646
|
+
* first `GET` run then draws the last known row while the request is out (a
|
|
16647
|
+
* refresh, not a first load — see docs/data_states.md); the request goes out
|
|
16648
|
+
* as usual and its answer replaces the row. The copy follows the store: a
|
|
16649
|
+
* `PUT`, a list upserting a child, a relation changing rewrite it. Put
|
|
16650
|
+
* whatever makes a stored copy unusable in the signal's `id` (the deployed
|
|
16651
|
+
* version, say): a new id starts clean. `when` is read whenever the copy is
|
|
16652
|
+
* about to be read or written: `false` neither reads it nor writes it, and
|
|
16653
|
+
* empties the signal. Writing `undefined` into the signal (sign-out) forgets
|
|
16654
|
+
* the copy until a `GET` lands again. One row is kept per params the GET was
|
|
16655
|
+
* asked with, so it is meant for a singleton or a handful of rows, not for a
|
|
16656
|
+
* resource whose GET is asked for every id.
|
|
16430
16657
|
* @see docs/resource.md — relationships, callback return contracts, decision table
|
|
16431
16658
|
*
|
|
16432
16659
|
* @example
|
|
@@ -16445,6 +16672,7 @@ const resource = (
|
|
|
16445
16672
|
uniqueKeys = [],
|
|
16446
16673
|
rerunOn,
|
|
16447
16674
|
dependencies,
|
|
16675
|
+
persist,
|
|
16448
16676
|
|
|
16449
16677
|
GET,
|
|
16450
16678
|
GET_MANY,
|
|
@@ -16502,15 +16730,20 @@ const resource = (
|
|
|
16502
16730
|
return item;
|
|
16503
16731
|
},
|
|
16504
16732
|
});
|
|
16733
|
+
const persistence = persist
|
|
16734
|
+
? createResourcePersistence(store, { idKey, name, ...persist })
|
|
16735
|
+
: null;
|
|
16505
16736
|
// The row a GET designates, when the store already holds it — by its params:
|
|
16506
16737
|
// the value under idKey may be the id or any unique key (a route opening a
|
|
16507
16738
|
// user by id or by slug names both `id`), and a unique key may be given under
|
|
16508
|
-
// its own name
|
|
16509
|
-
//
|
|
16510
|
-
//
|
|
16511
|
-
//
|
|
16512
|
-
//
|
|
16513
|
-
//
|
|
16739
|
+
// its own name, or, on a persisted resource, as the row kept for these
|
|
16740
|
+
// params (a GET without params has one row). findItemForGet is what a GET
|
|
16741
|
+
// draws while its request is in flight (the action's provisionalValue).
|
|
16742
|
+
// findItemInStore adds, when the params name no row (a GET without params,
|
|
16743
|
+
// or whose params carry no key), the row the action last completed with:
|
|
16744
|
+
// that fallback answers a GET under a network policy (see
|
|
16745
|
+
// applyNetworkPolicy) and only there — a GET of a row the store lacks must
|
|
16746
|
+
// draw its skeleton, never the row read just before.
|
|
16514
16747
|
const selectByAnyKey = (value) => {
|
|
16515
16748
|
const item = store.select(value);
|
|
16516
16749
|
if (item) {
|
|
@@ -16549,11 +16782,21 @@ const resource = (
|
|
|
16549
16782
|
}
|
|
16550
16783
|
return null;
|
|
16551
16784
|
};
|
|
16785
|
+
const findItemForGet = (params) => {
|
|
16786
|
+
const itemByParams = findItemByParams(params);
|
|
16787
|
+
if (itemByParams) {
|
|
16788
|
+
return itemByParams;
|
|
16789
|
+
}
|
|
16790
|
+
if (persistence) {
|
|
16791
|
+
return persistence.findItem(params);
|
|
16792
|
+
}
|
|
16793
|
+
return null;
|
|
16794
|
+
};
|
|
16552
16795
|
const findItemInStore = (params, action) => {
|
|
16553
16796
|
return untracked(() => {
|
|
16554
|
-
const
|
|
16555
|
-
if (
|
|
16556
|
-
return
|
|
16797
|
+
const itemForGet = findItemForGet(params);
|
|
16798
|
+
if (itemForGet) {
|
|
16799
|
+
return itemForGet;
|
|
16557
16800
|
}
|
|
16558
16801
|
const lastItemId = getLastGetItemId(action);
|
|
16559
16802
|
if (lastItemId === undefined) {
|
|
@@ -16565,7 +16808,8 @@ const resource = (
|
|
|
16565
16808
|
const createRestActionForRoot = createRestActionFactoryForRoot(name, {
|
|
16566
16809
|
idKey,
|
|
16567
16810
|
store,
|
|
16568
|
-
|
|
16811
|
+
findItemForGet,
|
|
16812
|
+
persistence,
|
|
16569
16813
|
declarationSite,
|
|
16570
16814
|
});
|
|
16571
16815
|
return createResource(name, {
|
|
@@ -17663,7 +17907,8 @@ const createRestActionFactoryForRoot = (
|
|
|
17663
17907
|
{
|
|
17664
17908
|
idKey,
|
|
17665
17909
|
store, // see array_signal_store.js
|
|
17666
|
-
|
|
17910
|
+
findItemForGet,
|
|
17911
|
+
persistence,
|
|
17667
17912
|
declarationSite,
|
|
17668
17913
|
},
|
|
17669
17914
|
) => {
|
|
@@ -17730,6 +17975,9 @@ const createRestActionFactoryForRoot = (
|
|
|
17730
17975
|
recordGetResultProperties(action, Object.keys(result));
|
|
17731
17976
|
const itemId = applyResultToValue(result);
|
|
17732
17977
|
lastGetItemIdWeakMap.set(action, itemId);
|
|
17978
|
+
if (persistence) {
|
|
17979
|
+
persistence.recordGetItem(action.params, itemId);
|
|
17980
|
+
}
|
|
17733
17981
|
return itemId;
|
|
17734
17982
|
}
|
|
17735
17983
|
return applyResultToValue(result);
|
|
@@ -17741,7 +17989,7 @@ const createRestActionFactoryForRoot = (
|
|
|
17741
17989
|
provisionalValue:
|
|
17742
17990
|
verb === "GET"
|
|
17743
17991
|
? (params) => {
|
|
17744
|
-
const item = untracked(() =>
|
|
17992
|
+
const item = untracked(() => findItemForGet(params));
|
|
17745
17993
|
return item ? item[idKey] : undefined;
|
|
17746
17994
|
}
|
|
17747
17995
|
: undefined,
|
|
@@ -18144,7 +18392,15 @@ const generateSignalId = () => {
|
|
|
18144
18392
|
* refuse an address written too often (Safari: 100 writes per 10 s, then a
|
|
18145
18393
|
* SecurityError). A value one DRAGS is written at 60 Hz: keep it in the
|
|
18146
18394
|
* gesture while the finger is down and write the state once, on release —
|
|
18147
|
-
* an address is not a recording of a gesture.
|
|
18395
|
+
* an address is not a recording of a gesture. When every intermediate value
|
|
18396
|
+
* IS a valid address (a map centre), see `debounceUrl`.
|
|
18397
|
+
* @param {number} [options.debounceUrl=0] - Milliseconds the browser's address
|
|
18398
|
+
* waits for this state to settle before a replace is written: writes closer
|
|
18399
|
+
* together than that make one write, with the last value. The routes, the
|
|
18400
|
+
* document url and the renders still move on every write; only
|
|
18401
|
+
* `window.location` is behind. For a state legitimately written per frame
|
|
18402
|
+
* (the centre of a panned map, the hour of a dragged sun), e.g. 200. A push
|
|
18403
|
+
* is never debounced.
|
|
18148
18404
|
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
18149
18405
|
* @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.
|
|
18150
18406
|
*
|
|
@@ -18219,6 +18475,7 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18219
18475
|
autoFix,
|
|
18220
18476
|
weak = false,
|
|
18221
18477
|
history = "replace",
|
|
18478
|
+
debounceUrl = 0,
|
|
18222
18479
|
} = options;
|
|
18223
18480
|
|
|
18224
18481
|
if (history !== "replace" && history !== "push") {
|
|
@@ -18226,6 +18483,11 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18226
18483
|
`stateSignal "${id}": history must be "replace" or "push", got ${history}.`,
|
|
18227
18484
|
);
|
|
18228
18485
|
}
|
|
18486
|
+
if (typeof debounceUrl !== "number" || debounceUrl < 0) {
|
|
18487
|
+
throw new TypeError(
|
|
18488
|
+
`stateSignal "${id}": debounceUrl must be a number of milliseconds, got ${debounceUrl}.`,
|
|
18489
|
+
);
|
|
18490
|
+
}
|
|
18229
18491
|
if (weak && persists) {
|
|
18230
18492
|
throw new TypeError(
|
|
18231
18493
|
`stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
|
|
@@ -18581,6 +18843,7 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
18581
18843
|
...options,
|
|
18582
18844
|
history,
|
|
18583
18845
|
getHistory: () => historyForCurrentWrite || history,
|
|
18846
|
+
debounceUrl,
|
|
18584
18847
|
};
|
|
18585
18848
|
globalSignalRegistry.set(signalIdString, {
|
|
18586
18849
|
signal: facadeSignal,
|
|
@@ -26853,7 +27116,10 @@ const route = (
|
|
|
26853
27116
|
const routeUrl = route.buildUrl(params);
|
|
26854
27117
|
return integration.navTo(routeUrl, options);
|
|
26855
27118
|
};
|
|
26856
|
-
route.redirectTo = (
|
|
27119
|
+
route.redirectTo = (
|
|
27120
|
+
params,
|
|
27121
|
+
{ callReason, history = "replace", debounceUrl } = {},
|
|
27122
|
+
) => {
|
|
26857
27123
|
if (!integration) {
|
|
26858
27124
|
{
|
|
26859
27125
|
console.warn(
|
|
@@ -26865,12 +27131,13 @@ const route = (
|
|
|
26865
27131
|
const routeUrl = route.buildUrl(params);
|
|
26866
27132
|
return integration.navTo(routeUrl, {
|
|
26867
27133
|
replace: history !== "push",
|
|
27134
|
+
debounce: debounceUrl,
|
|
26868
27135
|
callReason,
|
|
26869
27136
|
});
|
|
26870
27137
|
};
|
|
26871
27138
|
route.replaceParams = (
|
|
26872
27139
|
newParams,
|
|
26873
|
-
{ callReason, isSignalChange, history = "replace" } = {},
|
|
27140
|
+
{ callReason, isSignalChange, history = "replace", debounceUrl } = {},
|
|
26874
27141
|
) => {
|
|
26875
27142
|
const matching = route.matchingSignal.peek();
|
|
26876
27143
|
if (!matching) {
|
|
@@ -26925,6 +27192,7 @@ const route = (
|
|
|
26925
27192
|
return mostSpecificRoute.redirectTo(newParams, {
|
|
26926
27193
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
26927
27194
|
history,
|
|
27195
|
+
debounceUrl,
|
|
26928
27196
|
});
|
|
26929
27197
|
}
|
|
26930
27198
|
|
|
@@ -26946,6 +27214,7 @@ const route = (
|
|
|
26946
27214
|
);
|
|
26947
27215
|
return integration.navTo(targetUrl, {
|
|
26948
27216
|
replace: history !== "push",
|
|
27217
|
+
debounce: debounceUrl,
|
|
26949
27218
|
callReason,
|
|
26950
27219
|
});
|
|
26951
27220
|
};
|
|
@@ -27023,6 +27292,9 @@ const route = (
|
|
|
27023
27292
|
// which is what a param qualifying a screen is.
|
|
27024
27293
|
const historyOfWrite = () =>
|
|
27025
27294
|
paramSignal.options?.getHistory?.() || "replace";
|
|
27295
|
+
// Milliseconds the browser's address waits for this state to settle
|
|
27296
|
+
// (see stateSignal's `debounceUrl`); 0 writes at once. A plain signal says nothing.
|
|
27297
|
+
const debounceUrl = paramSignal.options?.debounceUrl || 0;
|
|
27026
27298
|
if (debug) {
|
|
27027
27299
|
console.debug(
|
|
27028
27300
|
`[route] connecting url param "${paramName}" to signal`,
|
|
@@ -27066,6 +27338,7 @@ const route = (
|
|
|
27066
27338
|
callReason: `${paramName} signal change on ${route}`,
|
|
27067
27339
|
isSignalChange: true,
|
|
27068
27340
|
history: historyOfWrite(),
|
|
27341
|
+
debounceUrl,
|
|
27069
27342
|
},
|
|
27070
27343
|
);
|
|
27071
27344
|
return;
|
|
@@ -27084,6 +27357,7 @@ const route = (
|
|
|
27084
27357
|
callReason: `${paramName} signal reset to default on ${route}`,
|
|
27085
27358
|
isSignalChange: true,
|
|
27086
27359
|
history: historyOfWrite(),
|
|
27360
|
+
debounceUrl,
|
|
27087
27361
|
},
|
|
27088
27362
|
);
|
|
27089
27363
|
return;
|
|
@@ -27104,6 +27378,7 @@ const route = (
|
|
|
27104
27378
|
callReason: `${paramName} signal change on ${route}`,
|
|
27105
27379
|
isSignalChange: true,
|
|
27106
27380
|
history: historyOfWrite(),
|
|
27381
|
+
debounceUrl,
|
|
27107
27382
|
},
|
|
27108
27383
|
);
|
|
27109
27384
|
});
|
|
@@ -28415,8 +28690,6 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28415
28690
|
applyRouting,
|
|
28416
28691
|
isRouting,
|
|
28417
28692
|
}) => {
|
|
28418
|
-
const { history } = window;
|
|
28419
|
-
|
|
28420
28693
|
let globalAbortController = new AbortController();
|
|
28421
28694
|
const triggerGlobalAbort = (reason) => {
|
|
28422
28695
|
globalAbortController.abort(reason);
|
|
@@ -28434,7 +28707,8 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28434
28707
|
setActionDispatcher(dispatchActions);
|
|
28435
28708
|
|
|
28436
28709
|
const getDocumentState = () => {
|
|
28437
|
-
|
|
28710
|
+
const entryState = readEntryState();
|
|
28711
|
+
return entryState ? { ...entryState } : null;
|
|
28438
28712
|
};
|
|
28439
28713
|
|
|
28440
28714
|
const historyStartAtStart = getDocumentState();
|
|
@@ -28448,7 +28722,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28448
28722
|
const visitedUrlsSignal = signal(0);
|
|
28449
28723
|
|
|
28450
28724
|
const isVisited = (url) => {
|
|
28451
|
-
url = new URL(url,
|
|
28725
|
+
url = new URL(url, readAddress()).href;
|
|
28452
28726
|
return visitedUrlSet.has(url);
|
|
28453
28727
|
};
|
|
28454
28728
|
const markUrlAsVisited = (url) => {
|
|
@@ -28510,20 +28784,20 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28510
28784
|
let abortController = null;
|
|
28511
28785
|
const handleRoutingTask = (target, options) => {
|
|
28512
28786
|
// Everything below this line reasons on the URL as a whole: it is compared
|
|
28513
|
-
// to
|
|
28787
|
+
// to the address, looked up in the history stack, written into the
|
|
28514
28788
|
// document url signal and parsed there. A relative target ("/", "../x")
|
|
28515
28789
|
// would silently lose every one of those — the browser would still resolve
|
|
28516
28790
|
// it in pushState, but nothing else here would. So it is resolved once, at
|
|
28517
28791
|
// the single door every navigation goes through, rather than by each caller
|
|
28518
28792
|
// (navBack's fallback in particular arrives here raw).
|
|
28519
|
-
const url = new URL(target,
|
|
28793
|
+
const url = new URL(target, readAddress()).href;
|
|
28520
28794
|
// An address that only sends elsewhere never becomes anything here: asked
|
|
28521
28795
|
// before the announcement, before the history write and before the routes,
|
|
28522
28796
|
// so that what follows is entirely about where the reader is going (see
|
|
28523
28797
|
// resolveRouteRedirection).
|
|
28524
28798
|
const redirectionUrl = resolveRouteRedirection(url);
|
|
28525
28799
|
if (redirectionUrl) {
|
|
28526
|
-
if (redirectionUrl ===
|
|
28800
|
+
if (redirectionUrl === readAddress()) {
|
|
28527
28801
|
// Asked to go where we already are: an address that redirects is never
|
|
28528
28802
|
// the one being displayed, so there is nothing to go to and nothing to
|
|
28529
28803
|
// stack on the history.
|
|
@@ -28542,7 +28816,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28542
28816
|
if (
|
|
28543
28817
|
options.navigationType === "push" &&
|
|
28544
28818
|
options.state === undefined &&
|
|
28545
|
-
url !==
|
|
28819
|
+
url !== readAddress()
|
|
28546
28820
|
) {
|
|
28547
28821
|
const delta = adjacentEntryDelta(url);
|
|
28548
28822
|
if (delta === -1) {
|
|
@@ -28583,12 +28857,13 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28583
28857
|
const applyRoutingTask = (url, options) => {
|
|
28584
28858
|
// Read before the history is written: the url the reader is being taken
|
|
28585
28859
|
// away from, which is what decides where a push lands (see startAtTop).
|
|
28586
|
-
const urlLeft =
|
|
28860
|
+
const urlLeft = readAddress();
|
|
28587
28861
|
const {
|
|
28588
28862
|
reason,
|
|
28589
28863
|
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
28590
28864
|
redirected,
|
|
28591
28865
|
landOn,
|
|
28866
|
+
debounce,
|
|
28592
28867
|
} = options;
|
|
28593
28868
|
let { state } = options;
|
|
28594
28869
|
|
|
@@ -28607,7 +28882,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28607
28882
|
[NAV_DEPTH_STATE_KEY]: getNavDepth(),
|
|
28608
28883
|
},
|
|
28609
28884
|
});
|
|
28610
|
-
|
|
28885
|
+
if (navigationType === "replace" && debounce && state === undefined) {
|
|
28886
|
+
// Nothing of its own for the entry: the address moves, the state is
|
|
28887
|
+
// carried over. The one kind of write allowed to lag, and only when
|
|
28888
|
+
// the state written asked for it (see replaceAddressWhenSettled).
|
|
28889
|
+
replaceAddressWhenSettled(effectiveState, url, debounce);
|
|
28890
|
+
} else {
|
|
28891
|
+
writeHistoryEntry(navigationType, effectiveState, url);
|
|
28892
|
+
}
|
|
28611
28893
|
rememberEntryIsOfThisDocument();
|
|
28612
28894
|
updateDocumentUrl(url);
|
|
28613
28895
|
updateDocumentState(effectiveState);
|
|
@@ -28798,7 +29080,20 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28798
29080
|
// being left must be recorded from the first pixel scrolled.
|
|
28799
29081
|
installScrollRestoration();
|
|
28800
29082
|
|
|
29083
|
+
// The document is leaving, or may be killed in the background: what the
|
|
29084
|
+
// entry says about it has to be what the document says (see
|
|
29085
|
+
// replaceAddressWhenSettled).
|
|
29086
|
+
window.addEventListener("pagehide", flushPendingReplace);
|
|
29087
|
+
document.addEventListener("visibilitychange", () => {
|
|
29088
|
+
if (document.visibilityState === "hidden") {
|
|
29089
|
+
flushPendingReplace();
|
|
29090
|
+
}
|
|
29091
|
+
});
|
|
29092
|
+
|
|
28801
29093
|
window.addEventListener("popstate", (popstateEvent) => {
|
|
29094
|
+
// The entry a pending replace was for is no longer the current one, and
|
|
29095
|
+
// the History API has no way to write it (see replaceAddressWhenSettled).
|
|
29096
|
+
dropPendingReplace();
|
|
28802
29097
|
const url = window.location.href;
|
|
28803
29098
|
const state = popstateEvent.state;
|
|
28804
29099
|
const landOn = landOnPending;
|
|
@@ -28827,11 +29122,18 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28827
29122
|
updateDocumentUrl(window.location.href);
|
|
28828
29123
|
});
|
|
28829
29124
|
|
|
28830
|
-
const navTo = async (
|
|
29125
|
+
const navTo = async (
|
|
29126
|
+
url,
|
|
29127
|
+
{ replace, state, routeTransition, debounce } = {},
|
|
29128
|
+
) => {
|
|
28831
29129
|
handleRoutingTask(url, {
|
|
28832
29130
|
reason: `navTo called with "${url}"`,
|
|
28833
29131
|
navigationType: replace ? "replace" : "push",
|
|
28834
29132
|
state,
|
|
29133
|
+
// Milliseconds the browser's address waits for this replace to settle
|
|
29134
|
+
// (see replaceAddressWhenSettled); said by the state being written
|
|
29135
|
+
// (stateSignal's `debounceUrl`), never assumed.
|
|
29136
|
+
debounce,
|
|
28835
29137
|
// What this one navigation asks of a route transition, said by the call
|
|
28836
29138
|
// that starts it rather than by an element — the programmatic half of
|
|
28837
29139
|
// what a <Link routeTransition> says (see route_transition.jsx).
|
|
@@ -28844,8 +29146,8 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28844
29146
|
};
|
|
28845
29147
|
|
|
28846
29148
|
const reload = () => {
|
|
28847
|
-
const url =
|
|
28848
|
-
const state =
|
|
29149
|
+
const url = readAddress();
|
|
29150
|
+
const state = readEntryState();
|
|
28849
29151
|
handleRoutingTask(url, {
|
|
28850
29152
|
reason: "reload called",
|
|
28851
29153
|
navigationType: "reload",
|
|
@@ -28917,20 +29219,83 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28917
29219
|
};
|
|
28918
29220
|
};
|
|
28919
29221
|
|
|
28920
|
-
// Read before the history is written, in both places that ask: a push or a
|
|
28921
|
-
// replace onto the url already displayed. Never a traverse — the browser has
|
|
28922
|
-
// already moved window.location.href when a popstate is handled, so the
|
|
28923
|
-
// comparison would say "same" about every back and forward.
|
|
28924
29222
|
// Browsers refuse an address written too often: WebKit throws a SecurityError
|
|
28925
29223
|
// past 100 writes per 10 seconds, Chromium and Firefox drop the write. A state
|
|
28926
29224
|
// bound to the url and written per frame gets there in under two seconds, and
|
|
28927
|
-
// a developer working in Chromium never sees it. Every write goes through
|
|
28928
|
-
// so the rate is measured against the strictest budget
|
|
28929
|
-
// refuses, and so a refusal names its cause rather than
|
|
29225
|
+
// a developer working in Chromium never sees it. Every write goes through
|
|
29226
|
+
// writeHistoryEntry so the rate is measured against the strictest budget
|
|
29227
|
+
// before any browser refuses, and so a refusal names its cause rather than
|
|
29228
|
+
// `replaceState`.
|
|
29229
|
+
//
|
|
29230
|
+
// The address is written when the state is, and a state written per frame is
|
|
29231
|
+
// the app's to hold during the gesture (an address is not a recording of a
|
|
29232
|
+
// gesture). The one exception is asked for by the state itself (stateSignal's
|
|
29233
|
+
// `debounceUrl`): a value whose every intermediate IS a valid address — the
|
|
29234
|
+
// centre of a panned map. Its replaces are debounced: the browser is written
|
|
29235
|
+
// once the state has stayed still for that long, with the last value. Nothing
|
|
29236
|
+
// else in the document waits for it: the routes, the signals and the renders
|
|
29237
|
+
// have moved on the spot, and only the browser's copy of the address
|
|
29238
|
+
// (`window.location`) is behind — which is why everything here reads the
|
|
29239
|
+
// address through readAddress / readEntryState rather than from the browser.
|
|
29240
|
+
//
|
|
29241
|
+
// What never waits: a push (an entry the reader can come back to, which must
|
|
29242
|
+
// stand on top of the address as it is, so a pending replace is written
|
|
29243
|
+
// first), a write that puts something INTO the entry (a state written by
|
|
29244
|
+
// useNavState, a landOn — a record, not a mirror), and the writes a traversal
|
|
29245
|
+
// makes on the entry it lands on. A traversal DROPS a pending replace: the
|
|
29246
|
+
// browser has already moved to another entry, and the History API cannot
|
|
29247
|
+
// write the one just left. The address kept on it is the last one written.
|
|
28930
29248
|
const HISTORY_WRITE_BUDGET = 100;
|
|
28931
29249
|
const HISTORY_WRITE_WINDOW_MS = 10_000;
|
|
28932
|
-
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.`;
|
|
29250
|
+
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. A state whose every intermediate value is a valid address can declare debounceUrl (milliseconds) instead.`;
|
|
29251
|
+
|
|
29252
|
+
// The replace waiting for the state to settle, and the address this document
|
|
29253
|
+
// stands at meanwhile. Read by everything here that would otherwise read
|
|
29254
|
+
// window.location.href or window.history.state: the document is at the
|
|
29255
|
+
// pending address, the browser just does not say so yet.
|
|
29256
|
+
let pendingReplace = null;
|
|
29257
|
+
let pendingReplaceTimer = null;
|
|
29258
|
+
const readAddress = () => {
|
|
29259
|
+
return pendingReplace ? pendingReplace.url : window.location.href;
|
|
29260
|
+
};
|
|
29261
|
+
const readEntryState = () => {
|
|
29262
|
+
return pendingReplace ? pendingReplace.state : window.history.state;
|
|
29263
|
+
};
|
|
29264
|
+
const replaceAddressWhenSettled = (state, url, debounce) => {
|
|
29265
|
+
if (pendingReplaceTimer !== null) {
|
|
29266
|
+
clearTimeout(pendingReplaceTimer);
|
|
29267
|
+
}
|
|
29268
|
+
pendingReplace = { state, url };
|
|
29269
|
+
pendingReplaceTimer = setTimeout(flushPendingReplace, debounce);
|
|
29270
|
+
};
|
|
29271
|
+
const flushPendingReplace = () => {
|
|
29272
|
+
const pending = pendingReplace;
|
|
29273
|
+
dropPendingReplace();
|
|
29274
|
+
if (pending) {
|
|
29275
|
+
writeHistoryEntryNow("replace", pending.state, pending.url);
|
|
29276
|
+
}
|
|
29277
|
+
};
|
|
29278
|
+
const dropPendingReplace = () => {
|
|
29279
|
+
pendingReplace = null;
|
|
29280
|
+
if (pendingReplaceTimer !== null) {
|
|
29281
|
+
clearTimeout(pendingReplaceTimer);
|
|
29282
|
+
pendingReplaceTimer = null;
|
|
29283
|
+
}
|
|
29284
|
+
};
|
|
28933
29285
|
const writeHistoryEntry = (navigationType, state, url) => {
|
|
29286
|
+
if (pendingReplace) {
|
|
29287
|
+
if (pendingReplace.url === url) {
|
|
29288
|
+
// This write stands where the pending one would: its state was built on
|
|
29289
|
+
// the pending state (see readEntryState), so it says everything the
|
|
29290
|
+
// pending write had to say.
|
|
29291
|
+
dropPendingReplace();
|
|
29292
|
+
} else {
|
|
29293
|
+
flushPendingReplace();
|
|
29294
|
+
}
|
|
29295
|
+
}
|
|
29296
|
+
writeHistoryEntryNow(navigationType, state, url);
|
|
29297
|
+
};
|
|
29298
|
+
const writeHistoryEntryNow = (navigationType, state, url) => {
|
|
28934
29299
|
noteHistoryWrite(url);
|
|
28935
29300
|
try {
|
|
28936
29301
|
if (navigationType === "push") {
|
|
@@ -28972,10 +29337,14 @@ const noteHistoryWrite = (() => {
|
|
|
28972
29337
|
})()
|
|
28973
29338
|
;
|
|
28974
29339
|
|
|
29340
|
+
// Read before the history is written, in both places that ask: a push or a
|
|
29341
|
+
// replace onto the url already displayed. Never a traverse — the browser has
|
|
29342
|
+
// already moved the address when a popstate is handled, so the comparison
|
|
29343
|
+
// would say "same" about every back and forward.
|
|
28975
29344
|
const isStateOnlyNavigation = (
|
|
28976
29345
|
url,
|
|
28977
29346
|
{ navigationType },
|
|
28978
|
-
urlLeft =
|
|
29347
|
+
urlLeft = readAddress(),
|
|
28979
29348
|
) =>
|
|
28980
29349
|
url === urlLeft &&
|
|
28981
29350
|
(navigationType === "push" || navigationType === "replace");
|
|
@@ -29102,8 +29471,10 @@ setRouteIntegration(browserIntegration);
|
|
|
29102
29471
|
|
|
29103
29472
|
const navIntegratedVia = browserIntegration.integration;
|
|
29104
29473
|
const navTo = (target, options) => {
|
|
29105
|
-
|
|
29474
|
+
// Resolved against the address this document says it is at, not the one the
|
|
29475
|
+
// browser shows: a replace may be on its way there (see via_history.js).
|
|
29106
29476
|
const currentUrl = documentUrlSignal.peek();
|
|
29477
|
+
const url = new URL(target, currentUrl).href;
|
|
29107
29478
|
if (url === currentUrl) {
|
|
29108
29479
|
if (options?.state === undefined) {
|
|
29109
29480
|
return null;
|
|
@@ -29164,39 +29535,43 @@ const idUsageMap = new Map();
|
|
|
29164
29535
|
// Keys found in the entry with nobody to claim them, already reported.
|
|
29165
29536
|
const orphanKeysReported = new Set();
|
|
29166
29537
|
const useNavStateWithWarnings = (id, options) => {
|
|
29167
|
-
const
|
|
29168
|
-
if (
|
|
29169
|
-
|
|
29170
|
-
|
|
29171
|
-
|
|
29538
|
+
const ownerRef = useRef(null);
|
|
29539
|
+
if (!ownerRef.current) {
|
|
29540
|
+
ownerRef.current = {};
|
|
29541
|
+
}
|
|
29542
|
+
const owner = ownerRef.current;
|
|
29543
|
+
// Claimed during render so warnAboutOrphanGeneratedKeys sees every id of
|
|
29544
|
+
// the commit, including the ones whose effect has not run yet. The conflict
|
|
29545
|
+
// is judged in the effect: when a screen replaces another in one commit,
|
|
29546
|
+
// preact renders the new children before unmounting the old ones, so at
|
|
29547
|
+
// render time the id may still be held by a component about to go.
|
|
29548
|
+
if (id !== undefined && !idUsageMap.has(id)) {
|
|
29549
|
+
idUsageMap.set(id, { owner, stackTrace: new Error().stack });
|
|
29550
|
+
}
|
|
29172
29551
|
|
|
29552
|
+
useEffect(() => {
|
|
29553
|
+
if (id === undefined) {
|
|
29554
|
+
return undefined;
|
|
29555
|
+
}
|
|
29173
29556
|
const usage = idUsageMap.get(id);
|
|
29174
|
-
if (
|
|
29175
|
-
idUsageMap.set(id, {
|
|
29176
|
-
stackTrace: new Error().stack,
|
|
29177
|
-
});
|
|
29178
|
-
} else {
|
|
29557
|
+
if (usage && usage.owner !== owner) {
|
|
29179
29558
|
console.warn(
|
|
29180
29559
|
`useNavState ID conflict detected!
|
|
29181
29560
|
ID "${id}" is already in use by another component.
|
|
29182
29561
|
This can cause UI state conflicts and unexpected behavior.
|
|
29183
29562
|
Consider using unique IDs for each component instance.`,
|
|
29184
29563
|
);
|
|
29185
|
-
}
|
|
29186
|
-
|
|
29187
|
-
|
|
29188
|
-
|
|
29189
|
-
|
|
29190
|
-
// Suspense parks a subtree by running every hook cleanup in it, and the
|
|
29191
|
-
// render resuming it carries the same id.
|
|
29192
|
-
if (!idUsageMap.has(id)) {
|
|
29193
|
-
idUsageMap.set(id, {
|
|
29194
|
-
stackTrace: new Error().stack,
|
|
29195
|
-
});
|
|
29564
|
+
} else {
|
|
29565
|
+
// Also set when absent: preact/compat's Suspense parks a subtree by
|
|
29566
|
+
// running every hook cleanup in it, and the render resuming it carries
|
|
29567
|
+
// the same id.
|
|
29568
|
+
idUsageMap.set(id, { owner, stackTrace: new Error().stack });
|
|
29196
29569
|
}
|
|
29197
29570
|
warnAboutOrphanGeneratedKeys();
|
|
29198
29571
|
return () => {
|
|
29199
|
-
idUsageMap.
|
|
29572
|
+
if (idUsageMap.get(id)?.owner === owner) {
|
|
29573
|
+
idUsageMap.delete(id);
|
|
29574
|
+
}
|
|
29200
29575
|
};
|
|
29201
29576
|
}, [id]);
|
|
29202
29577
|
|
|
@@ -29310,7 +29685,7 @@ const useNavStateBasic = (
|
|
|
29310
29685
|
return;
|
|
29311
29686
|
}
|
|
29312
29687
|
currentStateCopy[id] = value;
|
|
29313
|
-
navTo(
|
|
29688
|
+
navTo(documentUrlSignal.peek(), {
|
|
29314
29689
|
replace: effectiveType !== "push",
|
|
29315
29690
|
state: currentStateCopy,
|
|
29316
29691
|
});
|
|
@@ -29344,12 +29719,12 @@ const useNavStateBasic = (
|
|
|
29344
29719
|
}
|
|
29345
29720
|
delete currentStateCopy[id];
|
|
29346
29721
|
browserIntegration.navBack({
|
|
29347
|
-
landOn: { url:
|
|
29722
|
+
landOn: { url: documentUrlSignal.peek(), state: currentStateCopy },
|
|
29348
29723
|
});
|
|
29349
29724
|
return;
|
|
29350
29725
|
}
|
|
29351
29726
|
delete currentStateCopy[id];
|
|
29352
|
-
navTo(
|
|
29727
|
+
navTo(documentUrlSignal.peek(), {
|
|
29353
29728
|
replace: true,
|
|
29354
29729
|
state: currentStateCopy,
|
|
29355
29730
|
});
|
|
@@ -29404,7 +29779,7 @@ const useUrlSearchParam = (paramName, defaultValue) => {
|
|
|
29404
29779
|
}
|
|
29405
29780
|
|
|
29406
29781
|
const setSearchParamValue = (newValue, { replace = false } = {}) => {
|
|
29407
|
-
const newUrlObject = new URL(
|
|
29782
|
+
const newUrlObject = new URL(documentUrlSignal.peek());
|
|
29408
29783
|
newUrlObject.searchParams.set(paramName, newValue);
|
|
29409
29784
|
const newUrl = newUrlObject.href;
|
|
29410
29785
|
navTo(newUrl, { replace });
|
|
@@ -36906,7 +37281,9 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
36906
37281
|
return;
|
|
36907
37282
|
}
|
|
36908
37283
|
writeInSignal(signal, closedValue, { history: "replace" });
|
|
36909
|
-
|
|
37284
|
+
// The address this document says it is at: the one the browser shows may
|
|
37285
|
+
// be a replace behind it (see via_history.js).
|
|
37286
|
+
navBack({ landOn: { url: documentUrlSignal.peek() } });
|
|
36910
37287
|
return;
|
|
36911
37288
|
}
|
|
36912
37289
|
writeInSignal(signal, closedValue, { history: "replace" });
|