@jsenv/navi 0.29.436 → 0.29.438

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.
@@ -2975,6 +2975,41 @@ const setActionPrivateProperties = (action, properties) => {
2975
2975
  actionPrivatePropertiesWeakMap.set(action, properties);
2976
2976
  };
2977
2977
 
2978
+ /*
2979
+ * Where an action that declares no provisionalValue of its own finds one: the
2980
+ * value the page kept from the previous document, for a route action that is
2981
+ * not a resource read (see keepPageOnScreen in nav/page_kept.js). routeAction
2982
+ * adds the source when it binds the action; a run asks for it at its start,
2983
+ * like it asks provisionalValue (actions.js).
2984
+ *
2985
+ * A registry beside actions.js rather than an import of the kept reads: those
2986
+ * know about stores and params keys, which import actions.js.
2987
+ */
2988
+
2989
+ const keptValueSourcesWeakMap = new WeakMap();
2990
+ const addKeptValueSource = (rootAction, source) => {
2991
+ let sources = keptValueSourcesWeakMap.get(rootAction);
2992
+ if (!sources) {
2993
+ sources = [];
2994
+ keptValueSourcesWeakMap.set(rootAction, sources);
2995
+ }
2996
+ sources.push(source);
2997
+ };
2998
+ const takeKeptValue = (action, params) => {
2999
+ const rootAction = action.rootAction || action;
3000
+ const sources = keptValueSourcesWeakMap.get(rootAction);
3001
+ if (!sources) {
3002
+ return undefined;
3003
+ }
3004
+ for (const source of sources) {
3005
+ const value = source(params);
3006
+ if (value !== undefined) {
3007
+ return value;
3008
+ }
3009
+ }
3010
+ return undefined;
3011
+ };
3012
+
2978
3013
  /*
2979
3014
  * Actions: async callbacks wrapped in reactive state.
2980
3015
  *
@@ -3696,8 +3731,11 @@ const actionWeakMap = new WeakMap();
3696
3731
  * than a first load (data_states.md). Called at the start of every run whose
3697
3732
  * value is undefined; the answer replaces it as usual, and `undefined` means
3698
3733
  * "nothing known". A resource `GET` uses it to draw the row its store holds.
3699
- * An action declaring it is asked for it by a binding moving to new params,
3700
- * rather than handed the previous params' answer (see inheritData).
3734
+ * @param {boolean} [rootOptions.inheritData=true] - `false`: a binding moving
3735
+ * to new params starts the new instance without the previous params' answer
3736
+ * (see createActionProxyFromSignal). For an answer that would say something
3737
+ * false about the new params (a `GET` drawing another row), not for one that
3738
+ * stands in while the next one comes (a list of search results).
3701
3739
  * @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
3702
3740
  * answer stays good once it has landed. Without it an answer lives exactly as
3703
3741
  * long as something references it: the screen that asked goes away, and
@@ -3740,6 +3778,7 @@ const createAction = (callback, rootOptions = {}) => {
3740
3778
  resultToValue,
3741
3779
  valueToData,
3742
3780
  provisionalValue,
3781
+ inheritData = true,
3743
3782
  dataDefault,
3744
3783
  data = dataDefault,
3745
3784
 
@@ -4206,8 +4245,12 @@ const createAction = (callback, rootOptions = {}) => {
4206
4245
  actionAbortMap.set(action, abort);
4207
4246
 
4208
4247
  batch(() => {
4209
- if (provisionalValue && valueSignal.peek() === undefined) {
4210
- const provisional = provisionalValue(params, action);
4248
+ if (valueSignal.peek() === undefined) {
4249
+ // An action saying nothing of its own may still have a value the
4250
+ // page kept from the previous document (see kept_values.js).
4251
+ const provisional = provisionalValue
4252
+ ? provisionalValue(params, action)
4253
+ : takeKeptValue(action, params);
4211
4254
  if (provisional !== undefined) {
4212
4255
  valueSignal.value = provisional;
4213
4256
  }
@@ -4416,6 +4459,7 @@ const createAction = (callback, rootOptions = {}) => {
4416
4459
  const privateProperties = {
4417
4460
  valueInitial,
4418
4461
  provisionalValue,
4462
+ inheritData,
4419
4463
 
4420
4464
  performRun,
4421
4465
  performReset,
@@ -4448,7 +4492,7 @@ const createAction = (callback, rootOptions = {}) => {
4448
4492
  * a list of filters changes, providing real-time results without user interaction.
4449
4493
  * @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
4450
4494
  * By default (true), the proxy carries over the previous target's value and error into the new action —
4451
- * unless the action declares a `provisionalValue`, which then says what the new one holds.
4495
+ * unless the action itself declares `inheritData: false` (createAction), which a resource `GET` does.
4452
4496
  * This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
4453
4497
  * new action completes, not when it starts loading. Code that needs to distinguish loading state can still
4454
4498
  * check `action.runningState`, while code that just reads `action.data` always sees the most recent
@@ -4495,12 +4539,12 @@ const createActionProxyFromSignal = (
4495
4539
  let currentActionPrivateProperties = getActionPrivateProperties(action);
4496
4540
  let actionTargetPreviousWeakRef = null;
4497
4541
 
4498
- // An action saying for itself what it holds for params it has not answered
4499
- // (provisionalValue: a resource GET draws the row its store holds) is not
4500
- // handed the previous params' answer — an answer about another row. What it
4501
- // does not know stays unknown: a skeleton, never a different row.
4542
+ // An action whose answer for other params would be false for these (a
4543
+ // resource GET: another row) says so itself, and is not handed it. What it
4544
+ // does not know stays unknown: a skeleton, or its provisionalValue, never a
4545
+ // different row.
4502
4546
  const inheritsAnswer =
4503
- inheritData && !getActionPrivateProperties(action).provisionalValue;
4547
+ inheritData && getActionPrivateProperties(action).inheritData;
4504
4548
  const createTarget = (params) => {
4505
4549
  if (inheritsAnswer) {
4506
4550
  const previousActionTarget = actionTargetPreviousWeakRef?.deref();
@@ -10651,6 +10695,9 @@ const paramsTargetCanPlace = (redirectRoute, urlParams) => {
10651
10695
 
10652
10696
  let setupRoutesCalled = false;
10653
10697
  let activeRouteSet = null;
10698
+ // True once the routes have been matched against an address. Before that, no
10699
+ // route matching says nothing about the page: nothing has been asked yet.
10700
+ const routedSignal = signal(false);
10654
10701
 
10655
10702
  const routePreloadMap = new WeakMap();
10656
10703
  const registerRoutePreload = (route, preload) => {
@@ -11075,6 +11122,7 @@ This prevents cross-test pollution and ensures clean state.`,
11075
11122
  }
11076
11123
  // Reset flag after URL -> Signal synchronization is complete
11077
11124
  isUpdatingRoutesFromUrl = false;
11125
+ routedSignal.value = true;
11078
11126
  Object.assign(returnValue, { matchingRouteSet });
11079
11127
  for (const [route, repairedParams] of repairedParamsByRoute) {
11080
11128
  route.replaceParams(repairedParams, {
@@ -11124,6 +11172,7 @@ This prevents cross-test pollution and ensures clean state.`,
11124
11172
  droppingRouteSet = null;
11125
11173
  setupRoutesCalled = false;
11126
11174
  activeRouteSet = null;
11175
+ routedSignal.value = false;
11127
11176
  };
11128
11177
  return {
11129
11178
  updateRoutes,
@@ -15628,6 +15677,13 @@ const createOpenController = (
15628
15677
  change();
15629
15678
  };
15630
15679
  const runChange = (change, { opened, event }) => {
15680
+ // What the controller answers about itself is the decision, written before
15681
+ // the change runs, whatever runs it. Whoever asks meanwhile reads it: the
15682
+ // render an opening forces (onOpen, mountContent) reads the open state
15683
+ // the caller holds, and an onOpen writing the popup's own `signal` has
15684
+ // useOpenPropsEffectOnOpenController asking for this very opening — a
15685
+ // second one, nested in the first, if `opened` still said closed.
15686
+ controller.opened = opened;
15631
15687
  const applyChange = () => {
15632
15688
  // Recorded with the change itself: what the DOM shows is what a render
15633
15689
  // landing between the ask and the picture must draw (see `openedInDom`).
@@ -15643,12 +15699,8 @@ const createOpenController = (
15643
15699
  applyChange();
15644
15700
  return;
15645
15701
  }
15646
- // What the controller answers about itself does not wait for the picture:
15647
- // whoever just asked reads `opened` on the spot (see
15648
- // useOpenPropsEffectOnOpenController, which writes it back into the
15649
- // caller's own signal), and a request arriving before the change lands
15650
- // runs it first rather than reading a DOM that disagrees.
15651
- controller.opened = opened;
15702
+ // A request arriving before the change lands runs it first rather than
15703
+ // reading a DOM that disagrees.
15652
15704
  changeAwaitingTransition = applyChange;
15653
15705
  transitionChange(
15654
15706
  () => {
@@ -15661,7 +15713,6 @@ const createOpenController = (
15661
15713
  };
15662
15714
 
15663
15715
  const performClose = (closeEvent) => {
15664
- controller.opened = false;
15665
15716
  // Read before any close effect touches the DOM: closing a native <dialog>
15666
15717
  // hands the focus back to whatever held it at showModal() time, so by the
15667
15718
  // time the close cleanup runs, the popup's content has already lost the
@@ -15929,7 +15980,6 @@ const createOpenController = (
15929
15980
  // told it opened right after (see popup_content_mount.js and
15930
15981
  // use_displayed_layout_effect.js).
15931
15982
  controller.openedInDom = true;
15932
- controller.opened = true;
15933
15983
  // Which press it opened during, so the release of that press is not
15934
15984
  // read as somebody dismissing it (see openedDuringThisPress).
15935
15985
  controller.pressCountAtOpen = pressCount;
@@ -16374,8 +16424,10 @@ const useOpenPropsEffectOnOpenController = (
16374
16424
  return undefined;
16375
16425
  }
16376
16426
  // Skip when the controller is already in the desired state.
16377
- // openController.opened tracks actual open/close (updated by onopen/onclose,
16378
- // not by renders) so it is the authoritative check against feedback loops.
16427
+ // openController.opened is what the controller decided, written before an
16428
+ // opening renders anything (see runChange), so it is the authoritative
16429
+ // check against feedback loops — the popup's own onOpen writing its
16430
+ // `signal` included.
16379
16431
  if (open === openController.opened) {
16380
16432
  return undefined;
16381
16433
  }
@@ -26725,6 +26777,172 @@ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
26725
26777
  return childStore !== undefined && childStore === getChildStore(ownerId);
26726
26778
  };
26727
26779
 
26780
+ /*
26781
+ * What a previous document kept of the page it was on (see keepPageOnScreen in
26782
+ * nav/page_kept.js), waiting for the reads that draw it again, and what those
26783
+ * reads are made of on the way out: rows with their relations inline.
26784
+ *
26785
+ * A kept read is found by the read's name and the params it was asked with
26786
+ * (getKeptReadKey). It is handed out once: the first run of the read draws it
26787
+ * while its request is out, and from then on the store holds the rows. Handed
26788
+ * out again, after a reset say, it would write the previous document's rows
26789
+ * over a fresher answer.
26790
+ *
26791
+ * This module is the half the reads see: `resource()` and the range reader
26792
+ * take from it and say which of their reads are kept as rows, routeAction takes
26793
+ * from it for any other action (through action/kept_values.js). Which reads are
26794
+ * on the page on screen, and what the slot is written into, is page_kept.js.
26795
+ */
26796
+
26797
+ // key → what the read answered in the previous document, not handed out yet
26798
+ const offeredReadMap = new Map();
26799
+ const offerKeptReads = (entryMap) => {
26800
+ offeredReadMap.clear();
26801
+ for (const [key, entry] of entryMap) {
26802
+ offeredReadMap.set(key, entry);
26803
+ }
26804
+ };
26805
+ const withdrawKeptReads = () => {
26806
+ offeredReadMap.clear();
26807
+ };
26808
+ const takeKeptRead = (key) => {
26809
+ const entry = offeredReadMap.get(key);
26810
+ if (entry === undefined) {
26811
+ return undefined;
26812
+ }
26813
+ offeredReadMap.delete(key);
26814
+ return entry;
26815
+ };
26816
+
26817
+ // The root actions whose answer is rows of a store: a resource's GET and
26818
+ // GET_MANY. They are kept as rows, since their value (ids) names rows a new
26819
+ // document's store does not hold. Any other route action is kept by its value
26820
+ // (see page_kept.js).
26821
+ const keepableReadWeakSet = new WeakSet();
26822
+ const markKeepableRead = (rootAction) => {
26823
+ keepableReadWeakSet.add(rootAction);
26824
+ };
26825
+ const isKeepableRead = (action) => {
26826
+ const rootAction = action.rootAction || action;
26827
+ return keepableReadWeakSet.has(rootAction);
26828
+ };
26829
+
26830
+ // The range readers a <List.Items> on screen reads through, each with the number
26831
+ // of lists reading it: the reads of the page a route action does not cover.
26832
+ const shownRangeReadersSignal = signal(new Map());
26833
+ const showRangeReader = (reader) => {
26834
+ const shownMap = new Map(shownRangeReadersSignal.peek());
26835
+ shownMap.set(reader, (shownMap.get(reader) || 0) + 1);
26836
+ shownRangeReadersSignal.value = shownMap;
26837
+ return () => {
26838
+ const shownMapNow = new Map(shownRangeReadersSignal.peek());
26839
+ const count = shownMapNow.get(reader);
26840
+ if (count === 1) {
26841
+ shownMapNow.delete(reader);
26842
+ } else {
26843
+ shownMapNow.set(reader, count - 1);
26844
+ }
26845
+ shownRangeReadersSignal.value = shownMapNow;
26846
+ };
26847
+ };
26848
+
26849
+ // A row already in the store is the fresher one: the kept copy only stands in
26850
+ // for rows nothing has answered yet. Its relations are written as they were
26851
+ // kept, which a real answer corrects like any other.
26852
+ const upsertKeptItem = (store, keptItem) => {
26853
+ const id = keptItem[store.idKey];
26854
+ if (id !== undefined) {
26855
+ const existing = store.select(id);
26856
+ if (existing) {
26857
+ return existing;
26858
+ }
26859
+ }
26860
+ return store.upsert(keptItem);
26861
+ };
26862
+
26863
+ const getKeptReadKey = (name, params) => {
26864
+ return `${name}|${getParamsKey(params)}`;
26865
+ };
26866
+ // `true` is what a route action without params is bound to (see routeAction),
26867
+ // and a key holding undefined says nothing a missing one does not.
26868
+ const getParamsKey = (params) => {
26869
+ if (params === undefined || params === NO_PARAMS || params === true) {
26870
+ return "";
26871
+ }
26872
+ if (isPlainParams(params)) {
26873
+ const keys = Object.keys(params).filter((key) => params[key] !== undefined);
26874
+ if (keys.length === 0) {
26875
+ return "";
26876
+ }
26877
+ }
26878
+ return stableStringify(params);
26879
+ };
26880
+ const isPlainParams = (value) => {
26881
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26882
+ };
26883
+ const stableStringify = (value) => {
26884
+ if (value === null || typeof value !== "object") {
26885
+ return JSON.stringify(value);
26886
+ }
26887
+ if (Array.isArray(value)) {
26888
+ return `[${value.map(stableStringify).join(",")}]`;
26889
+ }
26890
+ const keys = Object.keys(value)
26891
+ .filter((key) => value[key] !== undefined)
26892
+ .sort();
26893
+ const parts = [];
26894
+ for (const key of keys) {
26895
+ parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
26896
+ }
26897
+ return `{${parts.join(",")}}`;
26898
+ };
26899
+
26900
+ // What a read's data is made of, ready to be written: a row, or the rows of a
26901
+ // list, each with its relations inline.
26902
+ const serializeData = (data) => {
26903
+ if (Array.isArray(data)) {
26904
+ return data.map((item) => serializeItem(item, new Set()));
26905
+ }
26906
+ return serializeItem(data, new Set());
26907
+ };
26908
+ // The item with its relations inline: a relation value stands for what its
26909
+ // signal holds (the child row, the child rows, or nothing), a row reached
26910
+ // twice on the same path is written as its id — the setters accept both.
26911
+ const serializeItem = (item, ancestorSet) => {
26912
+ ancestorSet.add(item);
26913
+ const serialized = {};
26914
+ for (const key of Object.keys(item)) {
26915
+ serialized[key] = serializeValue(item[key], ancestorSet);
26916
+ }
26917
+ ancestorSet.delete(item);
26918
+ return serialized;
26919
+ };
26920
+ const serializeValue = (value, ancestorSet) => {
26921
+ if (value === null || typeof value !== "object") {
26922
+ return value;
26923
+ }
26924
+ if (Array.isArray(value)) {
26925
+ return value.map((entry) => serializeValue(entry, ancestorSet));
26926
+ }
26927
+ const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
26928
+ if (objectSignal) {
26929
+ const held = objectSignal.value;
26930
+ if (held === undefined || held === null) {
26931
+ return null;
26932
+ }
26933
+ return serializeValue(held, ancestorSet);
26934
+ }
26935
+ if (typeof value.toJSON === "function") {
26936
+ return value;
26937
+ }
26938
+ if (ancestorSet.has(value)) {
26939
+ return Object.hasOwn(value, SYMBOL_IDENTITY)
26940
+ ? value[SYMBOL_IDENTITY]
26941
+ : null;
26942
+ }
26943
+ return serializeItem(value, ancestorSet);
26944
+ };
26945
+
26728
26946
  const paramScopeWeakSet = createIterableWeakSet();
26729
26947
  let paramScopeIdCounter = 0;
26730
26948
  const getParamScope = (params) => {
@@ -26895,66 +27113,6 @@ const createResourcePersistence = (
26895
27113
  };
26896
27114
  };
26897
27115
 
26898
- const getParamsKey = (params) => {
26899
- if (params === undefined || params === NO_PARAMS) {
26900
- return "";
26901
- }
26902
- return stableStringify(params);
26903
- };
26904
-
26905
- const stableStringify = (value) => {
26906
- if (value === null || typeof value !== "object") {
26907
- return JSON.stringify(value);
26908
- }
26909
- if (Array.isArray(value)) {
26910
- return `[${value.map(stableStringify).join(",")}]`;
26911
- }
26912
- const keys = Object.keys(value).sort();
26913
- const parts = [];
26914
- for (const key of keys) {
26915
- parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
26916
- }
26917
- return `{${parts.join(",")}}`;
26918
- };
26919
-
26920
- // The item with its relations inline: a relation value stands for what its
26921
- // signal holds (the child row, the child rows, or nothing), a row reached
26922
- // twice on the same path is written as its id — the setters accept both.
26923
- const serializeItem = (item, ancestorSet) => {
26924
- ancestorSet.add(item);
26925
- const serialized = {};
26926
- for (const key of Object.keys(item)) {
26927
- serialized[key] = serializeValue(item[key], ancestorSet);
26928
- }
26929
- ancestorSet.delete(item);
26930
- return serialized;
26931
- };
26932
- const serializeValue = (value, ancestorSet) => {
26933
- if (value === null || typeof value !== "object") {
26934
- return value;
26935
- }
26936
- if (Array.isArray(value)) {
26937
- return value.map((entry) => serializeValue(entry, ancestorSet));
26938
- }
26939
- const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
26940
- if (objectSignal) {
26941
- const held = objectSignal.value;
26942
- if (held === undefined || held === null) {
26943
- return null;
26944
- }
26945
- return serializeValue(held, ancestorSet);
26946
- }
26947
- if (typeof value.toJSON === "function") {
26948
- return value;
26949
- }
26950
- if (ancestorSet.has(value)) {
26951
- return Object.hasOwn(value, SYMBOL_IDENTITY)
26952
- ? value[SYMBOL_IDENTITY]
26953
- : null;
26954
- }
26955
- return serializeItem(value, ancestorSet);
26956
- };
26957
-
26958
27116
  /*
26959
27117
  * GET_RANGE: reading a resource one slice at a time, for a list that draws its
26960
27118
  * rows as it goes (`<List.Items itemsAction>`).
@@ -26991,9 +27149,23 @@ const serializeValue = (value, ancestorSet) => {
26991
27149
  * of what a rerun is for an action. Every reader made by `bindParams` shares
26992
27150
  * the signal and the compositions of the one it comes from: the params say
26993
27151
  * which slices are read, not which collection.
27152
+ *
27153
+ * A composition also outlives the document when a list on the page on screen
27154
+ * reads it (see keepPageOnScreen): the ranks around the window it draws are
27155
+ * written with their rows, and the first read of the next document finds them
27156
+ * as a composition left by a list that went away — the same return, after a
27157
+ * longer absence.
26994
27158
  */
26995
27159
 
26996
27160
 
27161
+ // The ranks kept of a composition for the next document: the window a list
27162
+ // draws, widened by at least one step on each side, and snapped to steps so a
27163
+ // scroll within one draws the same ranks and writes nothing.
27164
+ const KEPT_RANK_STEP = 20;
27165
+ // Counts the answers every composition took, so that one landing can be told
27166
+ // from one seen before (see readKept).
27167
+ let answerSequence = 0;
27168
+
26997
27169
  const createRangeReader = (
26998
27170
  actionName,
26999
27171
  callback,
@@ -27002,6 +27174,9 @@ const createRangeReader = (
27002
27174
  params: boundParams,
27003
27175
  invalidationSignal = signal(0),
27004
27176
  compositionSet = new Set(),
27177
+ // Bumped whenever what is kept of a composition for the next document
27178
+ // changes (see readKept); shared like the compositions themselves.
27179
+ keptVersionSignal = signal(0),
27005
27180
  },
27006
27181
  ) => {
27007
27182
  // Which composition this reader is about: the values its params hold, not its
@@ -27018,6 +27193,40 @@ const createRangeReader = (
27018
27193
  }
27019
27194
  return null;
27020
27195
  };
27196
+ const keptKeyOf = (params) => getKeptReadKey(actionName, params);
27197
+ const bumpKeptVersion = () => {
27198
+ keptVersionSignal.value = keptVersionSignal.peek() + 1;
27199
+ };
27200
+ // The composition the previous document kept for these params, entering the
27201
+ // store and the compositions as if a list had just left it.
27202
+ const restoreKeptComposition = (params) => {
27203
+ const kept = takeKeptRead(keptKeyOf(params));
27204
+ if (!kept || typeof kept.count !== "number" || !kept.items) {
27205
+ return null;
27206
+ }
27207
+ const composition = {
27208
+ params,
27209
+ idByIndex: new Map(),
27210
+ count: kept.count,
27211
+ keptFrom: undefined,
27212
+ keptTo: undefined,
27213
+ answeredAt: 0,
27214
+ };
27215
+ for (const rank of Object.keys(kept.items)) {
27216
+ const index = Number(rank);
27217
+ const item = upsertKeptItem(store, kept.items[rank]);
27218
+ composition.idByIndex.set(index, item[store.idKey]);
27219
+ if (composition.keptFrom === undefined || index < composition.keptFrom) {
27220
+ composition.keptFrom = index;
27221
+ }
27222
+ if (composition.keptTo === undefined || index + 1 > composition.keptTo) {
27223
+ composition.keptTo = index + 1;
27224
+ }
27225
+ }
27226
+ compositionSet.add(composition);
27227
+ bumpKeptVersion();
27228
+ return composition;
27229
+ };
27021
27230
  const readRange = async (range = {}) => {
27022
27231
  const { signal, ...rangeParams } = range;
27023
27232
  const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
@@ -27051,11 +27260,15 @@ const createRangeReader = (
27051
27260
  readRange.invalidate = () => {
27052
27261
  compositionSet.clear();
27053
27262
  invalidationSignal.value = invalidationSignal.peek() + 1;
27263
+ bumpKeptVersion();
27054
27264
  };
27055
27265
  // The rows of a composition, drawn from the store: a rank whose row is gone
27056
27266
  // from the store resolves to nothing and is asked for again.
27057
27267
  readRange.readComposition = () => {
27058
- const composition = findComposition(currentParams());
27268
+ const params = currentParams();
27269
+ const composition = untracked(
27270
+ () => findComposition(params) || restoreKeptComposition(params),
27271
+ );
27059
27272
  if (!composition) {
27060
27273
  return null;
27061
27274
  }
@@ -27079,18 +27292,85 @@ const createRangeReader = (
27079
27292
  const params = currentParams();
27080
27293
  let composition = findComposition(params);
27081
27294
  if (!composition) {
27082
- composition = { params, idByIndex: new Map(), count };
27295
+ composition = {
27296
+ params,
27297
+ idByIndex: new Map(),
27298
+ count,
27299
+ keptFrom: undefined,
27300
+ keptTo: undefined,
27301
+ answeredAt: 0,
27302
+ };
27083
27303
  compositionSet.add(composition);
27084
27304
  } else if (replace) {
27085
27305
  composition.idByIndex = new Map();
27086
27306
  }
27087
27307
  composition.count = count;
27308
+ answerSequence++;
27309
+ composition.answeredAt = answerSequence;
27088
27310
  for (const [index, item] of byIndex) {
27089
27311
  const id = item ? item[store.idKey] : undefined;
27090
27312
  if (id !== undefined) {
27091
27313
  composition.idByIndex.set(index, id);
27092
27314
  }
27093
27315
  }
27316
+ bumpKeptVersion();
27317
+ };
27318
+ // The ranks [windowFrom, windowTo) a list draws of this composition: what is
27319
+ // kept of it for the next document is around them (see readKept).
27320
+ readRange.frameComposition = (windowFrom, windowTo) => {
27321
+ const composition = findComposition(currentParams());
27322
+ if (!composition) {
27323
+ return;
27324
+ }
27325
+ let keptFrom =
27326
+ Math.floor((windowFrom - KEPT_RANK_STEP) / KEPT_RANK_STEP) *
27327
+ KEPT_RANK_STEP;
27328
+ if (keptFrom < 0) {
27329
+ keptFrom = 0;
27330
+ }
27331
+ const keptTo =
27332
+ Math.ceil((windowTo + KEPT_RANK_STEP) / KEPT_RANK_STEP) * KEPT_RANK_STEP;
27333
+ if (composition.keptFrom === keptFrom && composition.keptTo === keptTo) {
27334
+ return;
27335
+ }
27336
+ composition.keptFrom = keptFrom;
27337
+ composition.keptTo = keptTo;
27338
+ bumpKeptVersion();
27339
+ };
27340
+ // What a document keeps of the composition these params read, for the
27341
+ // next one (see keepPageOnScreen): the count, and the ranks around the
27342
+ // window last drawn with their rows inline. Read inside an effect, it
27343
+ // follows the composition and the rows. `answeredAt` grows with every answer
27344
+ // the composition takes (0: none in this document, restored from a kept one).
27345
+ readRange.readKept = () => {
27346
+ // eslint-disable-next-line no-unused-expressions
27347
+ keptVersionSignal.value;
27348
+ const params = resolveParams(boundParams);
27349
+ const key = keptKeyOf(params);
27350
+ const composition = findComposition(params);
27351
+ if (!composition || composition.count === undefined) {
27352
+ return { key, answeredAt: 0, readEntry: () => undefined };
27353
+ }
27354
+ const readEntry = () => {
27355
+ const items = {};
27356
+ for (const [index, id] of composition.idByIndex) {
27357
+ if (
27358
+ composition.keptFrom !== undefined &&
27359
+ index < composition.keptFrom
27360
+ ) {
27361
+ continue;
27362
+ }
27363
+ if (composition.keptTo !== undefined && index >= composition.keptTo) {
27364
+ continue;
27365
+ }
27366
+ const item = store.select(id);
27367
+ if (item) {
27368
+ items[index] = serializeItem(item, new Set());
27369
+ }
27370
+ }
27371
+ return { count: composition.count, items };
27372
+ };
27373
+ return { key, answeredAt: composition.answeredAt, readEntry };
27094
27374
  };
27095
27375
  // The same trade a list makes with the rows it holds, applied to what is kept
27096
27376
  // for the next mount. Two lists on one collection each trim by their own
@@ -27101,11 +27381,16 @@ const createRangeReader = (
27101
27381
  if (!composition || composition.idByIndex.size <= budget) {
27102
27382
  return;
27103
27383
  }
27384
+ let trimmed = false;
27104
27385
  for (const index of composition.idByIndex.keys()) {
27105
27386
  if (index < keepFrom || index > keepTo) {
27106
27387
  composition.idByIndex.delete(index);
27388
+ trimmed = true;
27107
27389
  }
27108
27390
  }
27391
+ if (trimmed) {
27392
+ bumpKeptVersion();
27393
+ }
27109
27394
  };
27110
27395
  // Memoized for the reasons an action's bindParams is (see actions.js): params
27111
27396
  // and the reader they make have synchronized lifetimes, and params equal in
@@ -27121,6 +27406,7 @@ const createRangeReader = (
27121
27406
  params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
27122
27407
  invalidationSignal,
27123
27408
  compositionSet,
27409
+ keptVersionSignal,
27124
27410
  });
27125
27411
  readerByParams.set(paramsToBind, reader);
27126
27412
  return reader;
@@ -28555,7 +28841,7 @@ const createRestActionFactoryForRoot = (
28555
28841
  `${name}.${verb}`,
28556
28842
  declarationSite,
28557
28843
  );
28558
- return createAction(callback, {
28844
+ const action = createAction(callback, {
28559
28845
  name: `${name}.${verb}`,
28560
28846
  meta: { verb, isMany: false, paramScope },
28561
28847
  resultToValue: (result, action) => {
@@ -28591,17 +28877,33 @@ const createRestActionFactoryForRoot = (
28591
28877
  },
28592
28878
  valueToData: (itemId) => store.select(itemId),
28593
28879
  // While the request is out, the row the store already holds for these
28594
- // params is drawn: the action starts on the "refresh over a known
28880
+ // params is drawn, or the row the page kept from the previous document
28881
+ // (kept_reads.js): the action starts on the "refresh over a known
28595
28882
  // answer" line of data_states.md rather than the first-load one.
28596
28883
  provisionalValue:
28597
28884
  verb === "GET"
28598
- ? (params) => {
28599
- const item = untracked(() => findItemForGet(params));
28885
+ ? (params, action) => {
28886
+ const item = untracked(() => {
28887
+ const itemForGet = findItemForGet(params);
28888
+ if (itemForGet) {
28889
+ return itemForGet;
28890
+ }
28891
+ const keptItem = takeKeptRead(
28892
+ getKeptReadKey(action.name, params),
28893
+ );
28894
+ return keptItem ? upsertKeptItem(store, keptItem) : null;
28895
+ });
28600
28896
  return item ? item[idKey] : undefined;
28601
28897
  }
28602
28898
  : undefined,
28899
+ // Another params' answer is another row.
28900
+ inheritData: verb !== "GET",
28603
28901
  completeSideEffect: onActionComplete,
28604
28902
  });
28903
+ if (verb === "GET") {
28904
+ markKeepableRead(action);
28905
+ }
28906
+ return action;
28605
28907
  };
28606
28908
  const createActionAffectingManyItems = (
28607
28909
  verb,
@@ -28616,7 +28918,7 @@ const createRestActionFactoryForRoot = (
28616
28918
  return itemArray.map((item) => item[idKey]);
28617
28919
  };
28618
28920
 
28619
- return createAction(callback, {
28921
+ const action = createAction(callback, {
28620
28922
  meta: { verb, isMany: true, paramScope },
28621
28923
  name: `${name}.${verb}_MANY`,
28622
28924
  resultToValue: applyResultToValue,
@@ -28624,6 +28926,25 @@ const createRestActionFactoryForRoot = (
28624
28926
  // undefined so a screen can tell "not asked yet" from "answered: none".
28625
28927
  valueToData: (idArray) =>
28626
28928
  idArray === undefined ? undefined : store.selectAll(idArray),
28929
+ // The rows the page kept from the previous document (kept_reads.js),
28930
+ // drawn while the request is out. A binding moving to new params still
28931
+ // hands over the previous answer when there are none: a list standing
28932
+ // in for the next one beats a skeleton.
28933
+ provisionalValue:
28934
+ verb === "GET"
28935
+ ? (params, action) =>
28936
+ untracked(() => {
28937
+ const keptItems = takeKeptRead(
28938
+ getKeptReadKey(action.name, params),
28939
+ );
28940
+ if (!Array.isArray(keptItems)) {
28941
+ return undefined;
28942
+ }
28943
+ return keptItems.map(
28944
+ (keptItem) => upsertKeptItem(store, keptItem)[idKey],
28945
+ );
28946
+ })
28947
+ : undefined,
28627
28948
  completeSideEffect: (actionCompleted) => {
28628
28949
  onActionComplete(actionCompleted);
28629
28950
  if (
@@ -28639,6 +28960,10 @@ const createRestActionFactoryForRoot = (
28639
28960
  return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
28640
28961
  },
28641
28962
  });
28963
+ if (verb === "GET") {
28964
+ markKeepableRead(action);
28965
+ }
28966
+ return action;
28642
28967
  };
28643
28968
 
28644
28969
  return createActionForRoot;
@@ -28649,10 +28974,12 @@ const createRestActionFactoryForRoot = (
28649
28974
  // NetworkPolicyError carrying the policy's reason. A read is called or not depending
28650
28975
  // on where the policy answers reads from. Answered from the store, a GET of a
28651
28976
  // root resource completes with the row the store holds for it (see
28652
- // findItemInStore) — handing the item back is an upsert without effect, so the
28653
- // action completes with what it had and nothing is asked; a relationship GET
28654
- // has no row of its own to answer with and settles with the same error. A
28655
- // completed GET asked to rerun never gets here (actions.js holds it).
28977
+ // findItemInStore), and a root GET or GET_MANY holding rows for these params
28978
+ // (the kept page, drawn while the request is out) with those — handing them
28979
+ // back is an upsert without effect, so the action completes with what it had
28980
+ // and nothing is asked. A relationship GET has no row of its own to answer
28981
+ // with and settles with the same error. A completed GET asked to rerun never
28982
+ // gets here (actions.js holds it).
28656
28983
  const applyNetworkPolicy = (
28657
28984
  restCallback,
28658
28985
  { verb, isMany, findItemInStore },
@@ -28666,6 +28993,12 @@ const applyNetworkPolicy = (
28666
28993
  if (!policy.readsFromStore) {
28667
28994
  return restCallback(params, context);
28668
28995
  }
28996
+ if (isKeepableRead(context.action)) {
28997
+ const dataHeld = context.action.dataSignal.peek();
28998
+ if (dataHeld !== undefined) {
28999
+ return dataHeld;
29000
+ }
29001
+ }
28669
29002
  if (!isMany && findItemInStore) {
28670
29003
  const item = findItemInStore(params, context.action);
28671
29004
  if (item) {
@@ -38915,6 +39248,46 @@ const writeCover = (edge, element, size) => {
38915
39248
  style.setProperty(property, value);
38916
39249
  };
38917
39250
 
39251
+ // Every route action, with whether it asks something of the address at the
39252
+ // moment: its route matches and its params getter returns something.
39253
+ const routeActionEntriesSignal = signal([]);
39254
+ // What a route action is called in the page kept for the next document (see
39255
+ // keepPageOnScreen): its place among the route actions declared, which the
39256
+ // next document of the same build declares in the same order. A resource read
39257
+ // has a name of its own and is kept by it; an action made from a callback is
39258
+ // most often "anonymous", and several of them read the same page.
39259
+ const keptNameWeakMap = new WeakMap();
39260
+ const getRouteActionKeptName = (routeAction) => {
39261
+ return keptNameWeakMap.get(routeAction);
39262
+ };
39263
+ let activeRouteActionsPrevious = new Set();
39264
+ /**
39265
+ * The route actions asking something for the page on screen: their route
39266
+ * matches and their params getter returns something — the running ones, not
39267
+ * every matching one, so an action bound to every page that asks only when a
39268
+ * popup is open over it counts while it is open. Read to rerun what the page
39269
+ * reads (the app back from the background), and by `keepPageOnScreen`.
39270
+ * The set is handed out again only when its members change.
39271
+ *
39272
+ * A page's code is declared as a route action too; `action.meta.verb` says
39273
+ * which ones read data.
39274
+ *
39275
+ * @type {import("@preact/signals").ReadonlySignal<Set<object>>}
39276
+ */
39277
+ const activeRouteActionsSignal = computed(() => {
39278
+ const active = new Set();
39279
+ for (const { routeAction, askingSignal } of routeActionEntriesSignal.value) {
39280
+ if (askingSignal.value) {
39281
+ active.add(routeAction);
39282
+ }
39283
+ }
39284
+ if (haveSameMembers(active, activeRouteActionsPrevious)) {
39285
+ return activeRouteActionsPrevious;
39286
+ }
39287
+ activeRouteActionsPrevious = active;
39288
+ return active;
39289
+ });
39290
+
38918
39291
  /**
38919
39292
  * Binds an action to a route: it runs when the route matches, with the params
38920
39293
  * the effect reads off the address, runs again on a `reload()`, and is aborted
@@ -38971,7 +39344,23 @@ const routeAction = (
38971
39344
  }
38972
39345
  return params;
38973
39346
  };
39347
+ const rootAction = rootActionOf(action);
39348
+ const keptName = `route_action#${routeActionEntriesSignal.peek().length} ${rootAction.name}`;
39349
+ if (!isKeepableRead(rootAction)) {
39350
+ // Before the effect below: it may run the action at once.
39351
+ addKeptValueSource(rootAction, (params) =>
39352
+ takeKeptRead(getKeptReadKey(keptName, params)),
39353
+ );
39354
+ }
38974
39355
  const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
39356
+ keptNameWeakMap.set(actionBoundToRoute, keptName);
39357
+ routeActionEntriesSignal.value = [
39358
+ ...routeActionEntriesSignal.peek(),
39359
+ {
39360
+ routeAction: actionBoundToRoute,
39361
+ askingSignal: computed(() => Boolean(readParamsAsked())),
39362
+ },
39363
+ ];
38975
39364
  for (const route of routes) {
38976
39365
  // Asked of the route's params as they are, like the effect asks them.
38977
39366
  registerRouteReload(route, () =>
@@ -39055,6 +39444,416 @@ const anyMatchingRouteSignal = (routes) => {
39055
39444
  return anyMatchingSignal;
39056
39445
  };
39057
39446
 
39447
+ const rootActionOf = (action) => {
39448
+ if (action.isProxy) {
39449
+ return rootActionOf(getActionPrivateProperties(action).currentAction);
39450
+ }
39451
+ return action.rootAction || action;
39452
+ };
39453
+
39454
+ const haveSameMembers = (setA, setB) => {
39455
+ if (setA.size !== setB.size) {
39456
+ return false;
39457
+ }
39458
+ for (const value of setA) {
39459
+ if (!setB.has(value)) {
39460
+ return false;
39461
+ }
39462
+ }
39463
+ return true;
39464
+ };
39465
+
39466
+ /*
39467
+ * The page on screen, kept for the next document: what its reads answered,
39468
+ * written into a signal the app hands over, and drawn again by the same reads
39469
+ * in a document opening on the same page (a reload, a tab the system
39470
+ * discarded). The read side is state/rest/kept_reads.js; this is what decides
39471
+ * which reads are the page, and when the slot is written.
39472
+ *
39473
+ * The unit is the page, not the resource. One resource is read by several
39474
+ * screens — a search, someone's games, a game opened from anywhere — and kept
39475
+ * per resource it would keep every row ever opened. The page's reads are what
39476
+ * navi already runs for it: the route actions asking something for the page
39477
+ * (activeRouteActionsSignal) and the compositions the lists on screen read
39478
+ * (shownRangeReadersSignal). A search typed on the page is neither, and is not
39479
+ * kept.
39480
+ *
39481
+ * What a read is made of on the way out depends on what it holds. A resource
39482
+ * read holds ids, which name rows a new document's store does not have: its
39483
+ * rows are written, and re-enter the store. Any other route action holds its
39484
+ * value, written as it is when JSON brings it back as it was — a page's code
39485
+ * (a function), a Date built by the callback, a Map, are not, and are left
39486
+ * out. Such an action is recognized in the next document by its place among
39487
+ * the route actions declared (see getRouteActionKeptName), not by a name the
39488
+ * app would have to give every action made from a callback.
39489
+ *
39490
+ * A read is written once it has answered in this document. Before that it
39491
+ * keeps what the previous document kept for it, never what it shows meanwhile:
39492
+ * an answer handed over from other params (a list standing in for the next
39493
+ * one) says nothing about these.
39494
+ *
39495
+ * The write waits for the page to settle and is flushed when the document is
39496
+ * hidden. A navigation goes through pages half there — the route matched, its
39497
+ * list not mounted yet, the page left still drawn under a transition — and
39498
+ * each would otherwise be written out in full. Hidden is the moment the system
39499
+ * may discard the document, so nothing is lost by waiting until then.
39500
+ *
39501
+ * The signal turning `undefined` from outside (sign-out) forgets the page, and
39502
+ * nothing is written until a read lands again: written at once, the page still
39503
+ * on screen would put back the account just left.
39504
+ */
39505
+
39506
+ const WRITE_DELAY = 1000;
39507
+ const NOTHING_PENDING = {};
39508
+ let stopKeepingCurrent = null;
39509
+
39510
+ /**
39511
+ * Keeps what the page on screen reads, so that a new document opening on it —
39512
+ * a reload, a pull-to-refresh, a tab the system discarded in the background —
39513
+ * draws it again while its reads go out, rather than skeletons.
39514
+ *
39515
+ * What the page reads is what navi runs for it: the route actions asking
39516
+ * something for it (see `activeRouteActionsSignal`), and the `GET_RANGE`
39517
+ * compositions the `<List.Items>` on screen read, around the window they draw.
39518
+ * A resource read keeps its rows; any other route action keeps its value when
39519
+ * JSON brings it back as it was. A navigation replaces the slot with the next
39520
+ * page's reads, and the page left is dropped. In the next document, the first
39521
+ * run of the same read with the same params draws the kept answer as its
39522
+ * provisional value: `data` set while `loading` is `true`, the answer replacing
39523
+ * it. Under a network policy answering reads from the store, a resource read
39524
+ * completes with it. The copy follows the store: a `PUT` on a row rewrites it.
39525
+ *
39526
+ * Call it before the routes start: the first runs are the ones that look. And
39527
+ * declare the route actions where the app starts: one that is not a resource
39528
+ * read is recognized by its place among them.
39529
+ *
39530
+ * @param {object} options
39531
+ * @param {import("@preact/signals").Signal} options.signal - where the slot is
39532
+ * written: a `stateSignal` with `persists: true` and `type: "object"`, its
39533
+ * `id` holding whatever makes a kept page unusable (the deployed version).
39534
+ * Writing `undefined` into it (sign-out) forgets the page, and nothing is
39535
+ * written again until a read lands.
39536
+ * @param {() => boolean} [options.when] - read whenever the slot is about to be
39537
+ * read or written: `false` reads nothing, writes nothing, and empties the
39538
+ * signal (a tab viewing as someone else). A function, so it can read a signal.
39539
+ * @param {object[]} [options.always] - routes whose reads stay kept once the
39540
+ * page is left, until it is visited again: the page an app opens on when it
39541
+ * is launched rather than reloaded.
39542
+ * @returns {() => void} stops keeping the page.
39543
+ * @see docs/resource.md — what is kept and what is not, what stays the app's
39544
+ */
39545
+ const keepPageOnScreen = ({
39546
+ signal: keptSignal,
39547
+ when,
39548
+ always = [],
39549
+ } = {}) => {
39550
+ if (!isSignal(keptSignal)) {
39551
+ throw new TypeError(
39552
+ `keepPageOnScreen() needs a signal to write the page into, received ${keptSignal}`,
39553
+ );
39554
+ }
39555
+ if (stopKeepingCurrent) {
39556
+ stopKeepingCurrent();
39557
+ }
39558
+ const isAllowed = () => {
39559
+ return when ? Boolean(when()) : true;
39560
+ };
39561
+ const alwaysMatchingSignal = anyMatchingRouteSignal(always);
39562
+
39563
+ // What the signal held when it was read: the previous document's page.
39564
+ let loadedEntryMap = new Map();
39565
+ let loadedAlwaysKeys = [];
39566
+ // Which reads answered in this document (since the last forget): only
39567
+ // those write what they show.
39568
+ let answeredActionWeakSet = new WeakSet();
39569
+ let answeredRangeKeySet = new Set();
39570
+ // The state each action was last seen in, and the last answer seen of each
39571
+ // range reader: a read lands when they move, and not because the slot looks
39572
+ // again at something that had answered before a forget.
39573
+ const stateSeenWeakMap = new WeakMap();
39574
+ const answeredAtSeenWeakMap = new WeakMap();
39575
+ // Forgotten: nothing is written until a read lands.
39576
+ let paused = false;
39577
+ // The reads of the always-kept page, as they were the last time it was on
39578
+ // screen in this document; null until then.
39579
+ let alwaysReads = null;
39580
+
39581
+ const forget = () => {
39582
+ loadedEntryMap = new Map();
39583
+ loadedAlwaysKeys = [];
39584
+ answeredActionWeakSet = new WeakSet();
39585
+ answeredRangeKeySet = new Set();
39586
+ alwaysReads = null;
39587
+ paused = true;
39588
+ withdrawKeptReads();
39589
+ };
39590
+
39591
+ let lastWritten;
39592
+ let pendingWrite = NOTHING_PENDING;
39593
+ let writeTimeout;
39594
+ const flush = () => {
39595
+ clearTimeout(writeTimeout);
39596
+ if (pendingWrite === NOTHING_PENDING) {
39597
+ return;
39598
+ }
39599
+ const value = pendingWrite;
39600
+ pendingWrite = NOTHING_PENDING;
39601
+ if (compareTwoJsValues(value, lastWritten)) {
39602
+ return;
39603
+ }
39604
+ lastWritten = value;
39605
+ keptSignal.value = value;
39606
+ };
39607
+ const scheduleWrite = (value) => {
39608
+ pendingWrite = value;
39609
+ clearTimeout(writeTimeout);
39610
+ writeTimeout = setTimeout(flush, WRITE_DELAY);
39611
+ };
39612
+ const onVisibilityChange = () => {
39613
+ if (document.visibilityState === "hidden") {
39614
+ flush();
39615
+ }
39616
+ };
39617
+ document.addEventListener("visibilitychange", onVisibilityChange);
39618
+ window.addEventListener("pagehide", flush);
39619
+
39620
+ // Runs at once with what the signal already holds (the slot read from
39621
+ // storage), then on every write — the slot's own ones are recognized and
39622
+ // left alone.
39623
+ const unsubscribe = keptSignal.subscribe((value) => {
39624
+ if (value === lastWritten) {
39625
+ return;
39626
+ }
39627
+ lastWritten = value;
39628
+ pendingWrite = NOTHING_PENDING;
39629
+ clearTimeout(writeTimeout);
39630
+ if (
39631
+ value === undefined ||
39632
+ value === null ||
39633
+ typeof value !== "object" ||
39634
+ !isAllowed()
39635
+ ) {
39636
+ forget();
39637
+ return;
39638
+ }
39639
+ const entries =
39640
+ value.entries && typeof value.entries === "object" ? value.entries : {};
39641
+ loadedEntryMap = new Map(Object.entries(entries));
39642
+ loadedAlwaysKeys = Array.isArray(value.always)
39643
+ ? value.always.filter((key) => loadedEntryMap.has(key))
39644
+ : [];
39645
+ alwaysReads = null;
39646
+ paused = false;
39647
+ offerKeptReads(loadedEntryMap);
39648
+ });
39649
+
39650
+ const actionReadWeakMap = new WeakMap();
39651
+ const actionReadOf = (action, keptName, readEntryOf) => {
39652
+ let read = actionReadWeakMap.get(action);
39653
+ if (read) {
39654
+ return read;
39655
+ }
39656
+ read = () => {
39657
+ const state = action.runningStateSignal.value;
39658
+ const stateSeen = stateSeenWeakMap.get(action);
39659
+ stateSeenWeakMap.set(action, state);
39660
+ const landed = state === COMPLETED && stateSeen !== COMPLETED;
39661
+ if (landed) {
39662
+ answeredActionWeakSet.add(action);
39663
+ }
39664
+ return {
39665
+ key: getKeptReadKey(keptName, action.params),
39666
+ answered: answeredActionWeakSet.has(action),
39667
+ landed,
39668
+ readEntry: () => readEntryOf(action),
39669
+ };
39670
+ };
39671
+ actionReadWeakMap.set(action, read);
39672
+ return read;
39673
+ };
39674
+ const rangeReadWeakMap = new WeakMap();
39675
+ const rangeReadOf = (reader) => {
39676
+ let read = rangeReadWeakMap.get(reader);
39677
+ if (read) {
39678
+ return read;
39679
+ }
39680
+ read = () => {
39681
+ const { key, answeredAt, readEntry } = reader.readKept();
39682
+ const answeredAtSeen = answeredAtSeenWeakMap.get(reader) || 0;
39683
+ answeredAtSeenWeakMap.set(reader, answeredAt);
39684
+ const landed = answeredAt > answeredAtSeen;
39685
+ if (landed) {
39686
+ answeredRangeKeySet.add(key);
39687
+ }
39688
+ return {
39689
+ key,
39690
+ answered: answeredRangeKeySet.has(key),
39691
+ landed,
39692
+ readEntry,
39693
+ };
39694
+ };
39695
+ rangeReadWeakMap.set(reader, read);
39696
+ return read;
39697
+ };
39698
+ const readsOnScreen = () => {
39699
+ const reads = [];
39700
+ for (const routeAction of activeRouteActionsSignal.value) {
39701
+ // The instance is what is kept, so that a page left keeps what it read.
39702
+ // The binding's callSource names that instance and is rewritten once the
39703
+ // binding has moved to another one: read, it brings the slot along.
39704
+ // eslint-disable-next-line no-unused-expressions
39705
+ routeAction.callSource;
39706
+ const action = getActionPrivateProperties(routeAction).currentAction;
39707
+ if (action.params === NO_PARAMS) {
39708
+ continue;
39709
+ }
39710
+ if (isKeepableRead(action)) {
39711
+ reads.push(actionReadOf(action, action.name, readRows));
39712
+ } else {
39713
+ reads.push(
39714
+ actionReadOf(
39715
+ action,
39716
+ getRouteActionKeptName(routeAction),
39717
+ readPlainValue,
39718
+ ),
39719
+ );
39720
+ }
39721
+ }
39722
+ for (const reader of shownRangeReadersSignal.value.keys()) {
39723
+ reads.push(rangeReadOf(reader));
39724
+ }
39725
+ return reads;
39726
+ };
39727
+
39728
+ const stopEffect = effect(() => {
39729
+ const allowed = isAllowed();
39730
+ const routed = routedSignal.value;
39731
+ const alwaysMatching = alwaysMatchingSignal.value;
39732
+ if (!allowed) {
39733
+ forget();
39734
+ pendingWrite = undefined;
39735
+ flush();
39736
+ return;
39737
+ }
39738
+ if (!routed) {
39739
+ // No page yet: the slot is what the first reads will draw.
39740
+ return;
39741
+ }
39742
+ const screenReads = readsOnScreen();
39743
+ if (alwaysMatching) {
39744
+ alwaysReads = screenReads;
39745
+ }
39746
+ const entries = {};
39747
+ let landed = false;
39748
+ const collect = (reads) => {
39749
+ const keys = [];
39750
+ for (const read of reads) {
39751
+ const result = read();
39752
+ if (result.landed) {
39753
+ landed = true;
39754
+ }
39755
+ const entry = result.answered
39756
+ ? result.readEntry()
39757
+ : loadedEntryMap.get(result.key);
39758
+ if (entry === undefined) {
39759
+ continue;
39760
+ }
39761
+ entries[result.key] = entry;
39762
+ keys.push(result.key);
39763
+ }
39764
+ return keys;
39765
+ };
39766
+ const screenKeys = collect(screenReads);
39767
+ let alwaysKeys;
39768
+ if (alwaysReads === screenReads) {
39769
+ alwaysKeys = screenKeys;
39770
+ } else if (alwaysReads) {
39771
+ alwaysKeys = collect(alwaysReads);
39772
+ } else {
39773
+ alwaysKeys = loadedAlwaysKeys;
39774
+ for (const key of alwaysKeys) {
39775
+ entries[key] = loadedEntryMap.get(key);
39776
+ }
39777
+ }
39778
+ if (paused) {
39779
+ if (!landed) {
39780
+ return;
39781
+ }
39782
+ paused = false;
39783
+ }
39784
+ if (screenKeys.length === 0 && alwaysKeys.length === 0) {
39785
+ scheduleWrite(undefined);
39786
+ return;
39787
+ }
39788
+ scheduleWrite({ entries, screen: screenKeys, always: alwaysKeys });
39789
+ });
39790
+
39791
+ const stop = () => {
39792
+ flush();
39793
+ stopEffect();
39794
+ unsubscribe();
39795
+ document.removeEventListener("visibilitychange", onVisibilityChange);
39796
+ window.removeEventListener("pagehide", flush);
39797
+ withdrawKeptReads();
39798
+ if (stopKeepingCurrent === stop) {
39799
+ stopKeepingCurrent = null;
39800
+ }
39801
+ };
39802
+ stopKeepingCurrent = stop;
39803
+ return stop;
39804
+ };
39805
+
39806
+ const readRows = (action) => {
39807
+ const data = action.dataSignal.value;
39808
+ if (data === undefined || data === null) {
39809
+ return undefined;
39810
+ }
39811
+ return serializeData(data);
39812
+ };
39813
+ const readPlainValue = (action) => {
39814
+ const value = action.valueSignal.value;
39815
+ if (value === undefined || !isPlainData(value, new Set())) {
39816
+ return undefined;
39817
+ }
39818
+ return value;
39819
+ };
39820
+ // What JSON writes and reads back as it was. `undefined` inside is accepted:
39821
+ // JSON drops the key, which reads the same.
39822
+ const isPlainData = (value, ancestorSet) => {
39823
+ if (value === null || value === undefined) {
39824
+ return true;
39825
+ }
39826
+ const type = typeof value;
39827
+ if (type === "string" || type === "boolean") {
39828
+ return true;
39829
+ }
39830
+ if (type === "number") {
39831
+ return Number.isFinite(value);
39832
+ }
39833
+ if (type !== "object") {
39834
+ return false;
39835
+ }
39836
+ if (ancestorSet.has(value)) {
39837
+ return false;
39838
+ }
39839
+ const isArray = Array.isArray(value);
39840
+ if (!isArray) {
39841
+ const proto = Object.getPrototypeOf(value);
39842
+ if (proto !== Object.prototype && proto !== null) {
39843
+ return false;
39844
+ }
39845
+ }
39846
+ ancestorSet.add(value);
39847
+ const entries = isArray ? value : Object.values(value);
39848
+ for (const entry of entries) {
39849
+ if (!isPlainData(entry, ancestorSet)) {
39850
+ return false;
39851
+ }
39852
+ }
39853
+ ancestorSet.delete(value);
39854
+ return true;
39855
+ };
39856
+
39058
39857
  /**
39059
39858
  * A popup that is the anchor, brought to the front.
39060
39859
  *
@@ -66417,7 +67216,7 @@ const css$E = /* css */`
66417
67216
  /* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
66418
67217
  by default until .show()/.showModal() adds [open]) — needs its own
66419
67218
  starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
66420
- own backdropProps (recomputed from openController.opened on every
67219
+ own backdropProps (recomputed from openController.openedInDom on every
66421
67220
  render, present from the very first one), then toggled by plain
66422
67221
  removeAttribute/setAttribute in openEffect/close, never an explicit
66423
67222
  display override — removing the attribute just lets this rule stop
@@ -68077,7 +68876,7 @@ const useDialogProps = props => {
68077
68876
  "navi-out-of-flow": "",
68078
68877
  "baseClassName": "navi_dialog_backdrop",
68079
68878
  "aria-hidden": "true",
68080
- // Recomputed fresh on every render from openController.opened (not
68879
+ // Recomputed fresh on every render from openController.openedInDom (not
68081
68880
  // driven through a mount-time layout effect, unlike this file's own
68082
68881
  // imperative open/close toggling below) — present in the DOM
68083
68882
  // synchronously from the very first commit, matching this file's own
@@ -68087,7 +68886,7 @@ const useDialogProps = props => {
68087
68886
  // needs — see popover.jsx's own identical prop for the full reasoning.
68088
68887
  "aria-expanded": openController.openedInDom ? "true" : "false",
68089
68888
  // Present from this very first render (recomputed fresh on every one
68090
- // from openController.opened, not a frozen mount-time constant) so
68889
+ // from openController.openedInDom, not a frozen mount-time constant) so
68091
68890
  // there's no gap for the browser to ever paint this plain-div backdrop
68092
68891
  // visible before anything has actually opened it — see popover.jsx's
68093
68892
  // own identical prop for the full reasoning, and this file's own CSS
@@ -68107,7 +68906,7 @@ const useDialogProps = props => {
68107
68906
  // (kept once, not repeated here).
68108
68907
  "aria-expanded": openController.openedInDom ? "true" : "false",
68109
68908
  // Present from the very first render (recomputed fresh from
68110
- // openController.opened every time, not a frozen mount-time constant —
68909
+ // openController.openedInDom every time, not a frozen mount-time constant —
68111
68910
  // see popover.jsx's own identical prop for the full reasoning) so a
68112
68911
  // consumer whose own CSS also sets display (e.g. Popup's flex prop)
68113
68912
  // can't silently defeat showModal()/close()'s native open/close — see
@@ -68510,7 +69309,7 @@ const css$D = /* css */`
68510
69309
  without this it would flash visible for one frame on mount, before
68511
69310
  openEffect's own JS ever gets a chance to hide it. [navi-hidden] is
68512
69311
  set from usePopoverProps' own contentProps (recomputed from
68513
- openController.opened on every render, present from the very first
69312
+ openController.openedInDom on every render, present from the very first
68514
69313
  one — see there for why), then toggled by plain
68515
69314
  removeAttribute/setAttribute in openEffect/close, never an explicit
68516
69315
  display override: removing the attribute just lets this rule stop
@@ -69875,7 +70674,7 @@ const usePopoverProps = props => {
69875
70674
  "navi-out-of-flow": "",
69876
70675
  "baseClassName": "navi_popover_backdrop",
69877
70676
  "aria-hidden": "true",
69878
- // Recomputed fresh on every render from openController.opened (not a
70677
+ // Recomputed fresh on every render from openController.openedInDom (not a
69879
70678
  // frozen mount-time constant, same pattern as navi-hidden just below)
69880
70679
  // rather than driven through a mount-time layout effect — its own
69881
70680
  // actual open/close toggling is still done entirely imperatively
@@ -69950,7 +70749,7 @@ const usePopoverProps = props => {
69950
70749
  // starting-hidden default) — present from this very first render so
69951
70750
  // there's no gap for the browser to ever paint it visible before
69952
70751
  // anything has actually opened it. Recomputed fresh on every render from
69953
- // openController.opened (not a frozen mount-time constant) — Preact
70752
+ // openController.openedInDom (not a frozen mount-time constant) — Preact
69954
70753
  // only touches the DOM for a prop whose value actually changed since
69955
70754
  // the last render, so as long as this always reflects the *current*
69956
70755
  // truth, it never fights the imperative
@@ -74390,7 +75189,6 @@ const ListUI = props => {
74390
75189
  // What the runs ask for and stand for, in items: a source paginates in items,
74391
75190
  // and asks for the first picture's items and the rest in one round trip.
74392
75191
  listItems.pageSize = PAGE_SIZE_DEFAULT;
74393
- listItems.scrolled = scrolled ?? defaultScrolled;
74394
75192
  const {
74395
75193
  virtualItemSizeSignal,
74396
75194
  renderWindow,
@@ -74769,6 +75567,45 @@ const useListScrollSync = ({
74769
75567
  columns
74770
75568
  }) => {
74771
75569
  const debugScroll = useDebugScroll();
75570
+ // Where the list must be: what the caller holds it at (`scrolled`), or where
75571
+ // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
75572
+ // the caller moving the list, so the hold is armed again — that is what makes
75573
+ // it controlled. A `defaultScrolled` that changes is where the list opens,
75574
+ // read again from something that answered again (a count fresher than the
75575
+ // one kept from the previous visit): followed for as long as nobody has
75576
+ // moved the list, and compared by value, since it is said at every render.
75577
+ // Nothing to hold it at (a position not saved yet) is not a position: the
75578
+ // list opens where it opens.
75579
+ const startPlaceRef = useRef(null);
75580
+ if (startPlaceRef.current === null) {
75581
+ startPlaceRef.current = {
75582
+ userTookOver: false,
75583
+ wanted: scrolled ?? defaultScrolled,
75584
+ scrolled,
75585
+ defaultScrolled,
75586
+ way: null,
75587
+ arrived: false
75588
+ };
75589
+ } else {
75590
+ const place = startPlaceRef.current;
75591
+ if (place.scrolled !== scrolled) {
75592
+ place.scrolled = scrolled;
75593
+ place.defaultScrolled = defaultScrolled;
75594
+ place.wanted = scrolled ?? defaultScrolled;
75595
+ place.userTookOver = false;
75596
+ } else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
75597
+ place.defaultScrolled = defaultScrolled;
75598
+ if (!place.userTookOver) {
75599
+ place.wanted = defaultScrolled;
75600
+ // Placed again from where it stands: the way to the previous place
75601
+ // says nothing about whose the next scrolls are.
75602
+ place.way = null;
75603
+ place.arrived = false;
75604
+ }
75605
+ }
75606
+ }
75607
+ const scrolledWanted = startPlaceRef.current.wanted;
75608
+ listItems.scrolled = scrolledWanted;
74772
75609
  // The items drawn, [start, end) among the list's own. A ref as well as a
74773
75610
  // state: the render moves it where the list is held (holdWindow) without a
74774
75611
  // commit of its own.
@@ -74783,7 +75620,7 @@ const useListScrollSync = ({
74783
75620
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
74784
75621
  inLines: Boolean(columns),
74785
75622
  windowLeavesItemsOut,
74786
- scrolledWanted: scrolled ?? defaultScrolled,
75623
+ scrolledWanted,
74787
75624
  // The fillers hold the room of items above the screen at this size: a
74788
75625
  // size that changes moves what is on screen, like items landing above it.
74789
75626
  beforeSizeChange: () => listItems.captureAnchor()
@@ -75116,25 +75953,7 @@ const useListScrollSync = ({
75116
75953
  const newEnd = newStart + windowSize;
75117
75954
  updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
75118
75955
  };
75119
-
75120
- // Where the list must be: what the caller holds it at (`scrolled`), or where
75121
- // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
75122
- // the caller moving the list, so the hold is armed again — that is what makes
75123
- // it controlled.
75124
- // Nothing to hold it at (a position not saved yet) is not a position: the
75125
- // list opens where it opens.
75126
- const scrolledWanted = scrolled ?? defaultScrolled;
75127
75956
  const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
75128
- const startPlaceRef = useRef({
75129
- userTookOver: false,
75130
- wanted: scrolledWanted,
75131
- way: null,
75132
- arrived: false
75133
- });
75134
- if (startPlaceRef.current.wanted !== scrolledWanted) {
75135
- startPlaceRef.current.wanted = scrolledWanted;
75136
- startPlaceRef.current.userTookOver = false;
75137
- }
75138
75957
  // Only one thing owns the scroll at a time: while the list is holding itself
75139
75958
  // somewhere, the anchoring stays out of it (holding an item still is precisely
75140
75959
  // not being at the end anymore once what is above it shrinks).
@@ -78962,6 +79781,17 @@ const useItemStore = ({
78962
79781
  }
78963
79782
  const pages = pagesRef.current;
78964
79783
  const [, setPageVersion] = useState(0);
79784
+ // A list on screen is part of what the page reads: the composition it reads
79785
+ // is kept with the page for the next document (see keepPageOnScreen).
79786
+ useLayoutEffect(() => {
79787
+ if (inMemory || typeof itemsAction !== "function") {
79788
+ return undefined;
79789
+ }
79790
+ if (!itemsAction.isRangeReader) {
79791
+ return undefined;
79792
+ }
79793
+ return showRangeReader(itemsAction);
79794
+ }, [inMemory, itemsAction]);
78965
79795
  // The ranks whose item is from before: the run came back to the screen, or
78966
79796
  // the source said the collection moved. They stay drawn until a page confirms
78967
79797
  // or replaces them, rank by rank. A page covers what the window framed when it
@@ -79095,6 +79925,7 @@ const useItemStore = ({
79095
79925
  }
79096
79926
  }
79097
79927
  if (typeof itemsAction === "function" && itemsAction.trimComposition) {
79928
+ itemsAction.frameComposition(windowFrom, windowTo);
79098
79929
  itemsAction.trimComposition(keepFrom, keepTo, budget);
79099
79930
  }
79100
79931
  },
@@ -79664,6 +80495,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
79664
80495
  * room has to be restated as a number by whoever asks. The `index` and
79665
80496
  * `visibleCount` it also hands out say where to aim before the item is found,
79666
80497
  * and how many items to draw before the first paint (see `renderBudget`).
80498
+ * A value that changes before anyone has moved the list is followed (a place
80499
+ * read from a count that answered again): it is compared by value, so it can
80500
+ * be computed at every render.
79667
80501
  * @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
79668
80502
  * The same, but held: the list goes back there every time this changes, even
79669
80503
  * after the user has scrolled — the caller owns where the list is (see
@@ -95758,5 +96592,5 @@ const UserSvg = () => jsx("svg", {
95758
96592
  })
95759
96593
  });
95760
96594
 
95761
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
96595
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
95762
96596
  //# sourceMappingURL=jsenv_navi.js.map