@jsenv/navi 0.29.448 → 0.29.450

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.
@@ -4827,9 +4827,11 @@ const actionRunEffect = (
4827
4827
  return params;
4828
4828
  });
4829
4829
  // Whether the getter asks for a run is read off what it returned, never off
4830
- // the params of the instance it leads to: an action handed here holding
4831
- // params of its own (an instance, a binding) keeps them when the getter
4832
- // returns nothing — and nothing must still run nothing.
4830
+ // the instances it moves between: an action handed here holding params of
4831
+ // its own (an instance, a binding) keeps them when the getter returns
4832
+ // nothing — and nothing must still run nothing; an instance reset under the
4833
+ // effect (a same-resource DELETE) reads as never run — and the next params
4834
+ // the getter asks for must still run.
4833
4835
  let asking = Boolean(actionParamsSignal.peek());
4834
4836
  const actionRunnedByThisEffect = action.bindParams(actionParamsSignal, {
4835
4837
  debounce,
@@ -4856,11 +4858,7 @@ const actionRunEffect = (
4856
4858
  return;
4857
4859
  }
4858
4860
 
4859
- if (
4860
- actionTargetPrevious &&
4861
- !actionTargetPrevious.isPrerun &&
4862
- actionTarget
4863
- ) {
4861
+ if (actionTargetPrevious && actionTarget) {
4864
4862
  // params changed
4865
4863
  if (!asking) {
4866
4864
  // falsy params, don't run
@@ -72293,6 +72291,9 @@ const createListItems = () => {
72293
72291
  // what puts the view back once they have (see VirtualFiller).
72294
72292
  captureAnchor: () => {},
72295
72293
  holdViewStill: () => {},
72294
+ // The list has asked for a window no render has drawn yet: a run holding
72295
+ // the one before has nothing to ask for it (see useRequestMissing).
72296
+ windowOnItsWay: () => false,
72296
72297
  horizontal: false,
72297
72298
  virtualItemSizeSignal: null,
72298
72299
  roomById,
@@ -74462,6 +74463,7 @@ const useListScrollSync = ({
74462
74463
  const renderWindowRef = useRef(null);
74463
74464
  // A window was asked for that no render has drawn yet (see settleWindow).
74464
74465
  const windowAskedRef = useRef(false);
74466
+ listItems.windowOnItsWay = () => windowAskedRef.current;
74465
74467
  const windowLeavesItemsOut = () => {
74466
74468
  const renderWindow = renderWindowRef.current;
74467
74469
  if (!renderWindow) {
@@ -75933,6 +75935,7 @@ const useListScrollSync = ({
75933
75935
  }, [scrollerElResolved]);
75934
75936
  holdWindow();
75935
75937
  // Whatever was asked for until now is what this render draws.
75938
+ const windowWasAsked = windowAskedRef.current;
75936
75939
  windowAskedRef.current = false;
75937
75940
  const {
75938
75941
  start: windowStart,
@@ -75948,13 +75951,15 @@ const useListScrollSync = ({
75948
75951
  // column — started anywhere but on a line, every item drawn would sit in the
75949
75952
  // column of another. Started on one, an item is in the column its place says
75950
75953
  // whatever the window frames. The same object while the numbers hold, so the
75951
- // runs are not told about a window that did not move.
75954
+ // runs are not told about a window that did not move — except after a window
75955
+ // was asked for: a run waiting for it to ask (see useRequestMissing) must be
75956
+ // told, even when the window framed comes back to the same numbers.
75952
75957
  const windowDrawnRef = useRef(null);
75953
75958
  const drawnStart = windowStart - windowStart % itemsPerLine;
75954
75959
  const endPastLine = windowEnd % itemsPerLine;
75955
75960
  const drawnEnd = endPastLine === 0 ? windowEnd : windowEnd + itemsPerLine - endPastLine;
75956
75961
  let windowDrawn = windowDrawnRef.current;
75957
- if (!windowDrawn || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
75962
+ if (!windowDrawn || windowWasAsked || windowDrawn.start !== drawnStart || windowDrawn.end !== drawnEnd || windowDrawn.itemsPerLine !== itemsPerLine) {
75958
75963
  windowDrawn = {
75959
75964
  start: drawnStart,
75960
75965
  end: drawnEnd,
@@ -78089,6 +78094,15 @@ const VISIBILITY_HIDDEN_STYLE = {
78089
78094
  * @param {(range: object) => any} [props.itemsAction]
78090
78095
  * Where the items come from when the collection is read a slice at a time:
78091
78096
  * a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
78097
+ * @param {number} [props.count]
78098
+ * How many items the collection holds, when the app knows it before the
78099
+ * first answer: a total shown elsewhere, a profile saying how many there are.
78100
+ * The list is then as long as the collection from its first frame: it opens
78101
+ * where it is held, the scrollbar right, and asks for that window. Without
78102
+ * it, the run stands for skeletons up to the item the list opens on and a
78103
+ * page past it, until the first answer says how many there are. Not worth a
78104
+ * request of its own, since the first answer gives it (see docs/scroll.md,
78105
+ * "What a run knows when it opens").
78092
78106
  * @param {number} [props.debounce]
78093
78107
  * Milliseconds the reader must hold still before the run draws the
78094
78108
  * collection it reads: params following a field as the user types
@@ -78204,6 +78218,7 @@ const ListItemsRun = ({
78204
78218
  count,
78205
78219
  itemsAction,
78206
78220
  settling,
78221
+ pageSize: runProps.pageSize,
78207
78222
  memoryBudget,
78208
78223
  onRequestStateChange,
78209
78224
  onCountChange
@@ -78472,9 +78487,17 @@ const useRunItems = (store, {
78472
78487
  // A hole with nothing on either side (the scrollbar was thrown into
78473
78488
  // territory never visited): grow it both ways around what is on
78474
78489
  // screen.
78475
- const grow = Math.floor((itemsPerPage - holeSize) / 2);
78476
- askStart = missingStart - grow;
78477
- askEnd = missingEnd + grow;
78490
+ askStart = missingStart - Math.floor((itemsPerPage - holeSize) / 2);
78491
+ askEnd = askStart + itemsPerPage - 1;
78492
+ // Cut by an edge of the run (the first page, the last one), the page
78493
+ // takes the room on the other side.
78494
+ if (askStart < runStart) {
78495
+ askEnd += runStart - askStart;
78496
+ askStart = runStart;
78497
+ } else if (askEnd > runEnd - 1) {
78498
+ askStart -= askEnd - (runEnd - 1);
78499
+ askEnd = runEnd - 1;
78500
+ }
78478
78501
  }
78479
78502
  }
78480
78503
  if (askStart < runStart) {
@@ -78874,6 +78897,18 @@ const ListItemsFailure = ({
78874
78897
  const ITEM_STORE_MAX_DEFAULT = 1000;
78875
78898
  const ITEM_STORE_KEEP_AROUND = 250;
78876
78899
  const COUNT_NOT_SAID = {};
78900
+
78901
+ // The index of the item the list is held at, when the hold names one: a number,
78902
+ // or a position remembered with its index (see placeWhereHeld).
78903
+ const readHeldIndex = scrolled => {
78904
+ if (typeof scrolled === "number") {
78905
+ return scrolled;
78906
+ }
78907
+ if (scrolled && typeof scrolled === "object" && typeof scrolled.index === "number") {
78908
+ return scrolled.index;
78909
+ }
78910
+ return null;
78911
+ };
78877
78912
  const rangeIsSame = (a, b) => {
78878
78913
  if (!a || !b) {
78879
78914
  return a === b;
@@ -78891,6 +78926,7 @@ const useItemStore = ({
78891
78926
  count,
78892
78927
  itemsAction,
78893
78928
  settling,
78929
+ pageSize,
78894
78930
  memoryBudget,
78895
78931
  onRequestStateChange,
78896
78932
  onCountChange
@@ -78976,6 +79012,68 @@ const useItemStore = ({
78976
79012
  if (staleRanksRef.current === null) {
78977
79013
  staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
78978
79014
  }
79015
+ const takeInPage = ({
79016
+ pageItems,
79017
+ pageStart,
79018
+ pageCount
79019
+ }) => {
79020
+ // An item from before is gone from where it stood when the page puts
79021
+ // it at another rank (it would be drawn twice), or when its rank is
79022
+ // past the end of the collection.
79023
+ const staleRanks = staleRanksRef.current;
79024
+ const pageEnd = pageStart + pageItems.length;
79025
+ let dropped = false;
79026
+ if (staleRanks.size > 0) {
79027
+ const pageIds = new Set();
79028
+ for (const pageItem of pageItems) {
79029
+ if (pageItem && pageItem.id !== undefined) {
79030
+ pageIds.add(pageItem.id);
79031
+ }
79032
+ }
79033
+ for (const rank of staleRanks) {
79034
+ if (rank >= pageStart && rank < pageEnd) {
79035
+ continue;
79036
+ }
79037
+ const staleItem = pages.byIndex.get(rank);
79038
+ if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
79039
+ staleRanks.delete(rank);
79040
+ pages.byIndex.delete(rank);
79041
+ dropped = true;
79042
+ }
79043
+ }
79044
+ }
79045
+ let i = 0;
79046
+ while (i < pageItems.length) {
79047
+ pages.byIndex.set(pageStart + i, pageItems[i]);
79048
+ staleRanks.delete(pageStart + i);
79049
+ i++;
79050
+ }
79051
+ pages.count = pageCount;
79052
+ // Which rank holds which id, kept by the source so a list drawing
79053
+ // this collection again finds it drawn (see readComposition above).
79054
+ if (itemsAction.writeComposition) {
79055
+ itemsAction.writeComposition({
79056
+ byIndex: pages.byIndex,
79057
+ count: pageCount,
79058
+ replace: dropped
79059
+ });
79060
+ }
79061
+ };
79062
+ // The pages answered since the last render, taken in by the render and not
79063
+ // as they land: what the list reads of the run — how many items it stands for
79064
+ // (see listItems.take), where an item sits (its locator) — then moves in one
79065
+ // step. Taken in on landing, an item could be located past the count the list
79066
+ // still holds, and a list held on it would aim its window there at every
79067
+ // render, framed back each time.
79068
+ const pagesLandedRef = useRef([]);
79069
+ const pagesLanded = pagesLandedRef.current;
79070
+ const pagesTakenIn = pagesLanded.length > 0;
79071
+ if (pagesTakenIn) {
79072
+ pagesLandedRef.current = [];
79073
+ for (const page of pagesLanded) {
79074
+ takeInPage(page);
79075
+ }
79076
+ }
78979
79077
  const [refreshing, setRefreshing] = useState(false);
78980
79078
  // A source that says when what it reads has moved (a resource range reader
78981
79079
  // does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
@@ -79006,19 +79104,33 @@ const useItemStore = ({
79006
79104
  const [failure, setFailure] = useState(null);
79007
79105
  const listItems = useContext(ListItemsContext);
79008
79106
  // The items are there, which is what the list waits for to place itself on the
79009
- // item it is held at (see placeWhereHeld). Said from an effect: a signal read
79010
- // during this very render must not be written during it.
79107
+ // item it is held at (see placeWhereHeld). Said from an effect, once the run
79108
+ // has drawn them: a signal read during this very render must not be written
79109
+ // during it.
79011
79110
  useLayoutEffect(() => {
79012
- if (!inMemory || itemsHeldRef.current) {
79111
+ if (inMemory) {
79112
+ if (itemsHeldRef.current) {
79113
+ return;
79114
+ }
79115
+ itemsHeldRef.current = true;
79116
+ } else if (!pagesTakenIn) {
79013
79117
  return;
79014
79118
  }
79015
- itemsHeldRef.current = true;
79016
79119
  listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
79017
79120
  });
79018
79121
  // Before the first answer a run does not know how many items it stands for.
79019
79122
  // It stands for a page of them: a list that is about to be filled looks
79020
- // like items on their way, not like an empty list.
79021
- const itemCount = pages.count ?? count ?? listItems.pageSize;
79123
+ // like items on their way, not like an empty list. A list held on an item
79124
+ // (see placeWhereHeld) is about to be filled there: the run stands for the
79125
+ // items up to that one and a page past it, so the window frames the item
79126
+ // before any answer, and what the run asks for first is that window.
79127
+ const itemsPerPage = pageSize || listItems.pageSize;
79128
+ const countKnown = pages.count ?? count;
79129
+ let itemCount = countKnown;
79130
+ if (itemCount === undefined) {
79131
+ const heldIndex = readHeldIndex(listItems.scrolled);
79132
+ itemCount = heldIndex === null ? itemsPerPage : heldIndex + itemsPerPage;
79133
+ }
79022
79134
  // The items drawn are from before while they are read again, and while the
79023
79135
  // collection that replaces them waits for its params to hold still (see
79024
79136
  // useSettledReader).
@@ -79037,7 +79149,6 @@ const useItemStore = ({
79037
79149
  // say: the count the source answered or the caller gave, never the page the
79038
79150
  // run stands for before it knows. Said by each run on its first commit, so a
79039
79151
  // run drawing another collection takes back what the one before said.
79040
- const countKnown = pages.count ?? count;
79041
79152
  const onCountChangeRef = useRef(onCountChange);
79042
79153
  onCountChangeRef.current = onCountChange;
79043
79154
  const countSaidRef = useRef(COUNT_NOT_SAID);
@@ -79084,8 +79195,13 @@ const useItemStore = ({
79084
79195
  };
79085
79196
  // A list taken off the screen while it was asking leaves nothing ringing
79086
79197
  // behind it: whoever is drawing "looking for items" has to stop.
79198
+ const mountedRef = useRef(false);
79199
+ // Which render's ask is the one to send (see useRequestMissing).
79200
+ const askTurnRef = useRef(0);
79087
79201
  useLayoutEffect(() => {
79202
+ mountedRef.current = true;
79088
79203
  return () => {
79204
+ mountedRef.current = false;
79089
79205
  if (requestStateRef.current.busy && onRequestStateChangeRef.current) {
79090
79206
  onRequestStateChangeRef.current({
79091
79207
  busy: false,
@@ -79145,11 +79261,7 @@ const useItemStore = ({
79145
79261
  // what it hands over (see rankOf). The one thing read from the list
79146
79262
  // itself is where it is being held, which is a list item.
79147
79263
  const rankOfIndex = itemIndex => itemIndex - runStart;
79148
- // The very first ask has nothing to go on: the run does not even know
79149
- // how many items there are, so it asks for the items the list would open
79150
- // on — counting back from the end when that is where it opens, the way
79151
- // an HTTP range does.
79152
- const budget = listItems.pageSize;
79264
+ const budget = itemsPerPage;
79153
79265
  let start = missingStart;
79154
79266
  let end = missingEnd;
79155
79267
  let around;
@@ -79219,26 +79331,26 @@ const useItemStore = ({
79219
79331
  around = firstStale.id;
79220
79332
  }
79221
79333
  } else if (pages.count === undefined) {
79334
+ // The very first ask is what the window misses, like any other: the
79335
+ // run stands for the items up to where the list opens (see
79336
+ // itemCount), so the window frames that place already.
79222
79337
  const scrolled = listItems.scrolled;
79223
- if (scrolled === "end") {
79224
- // Counting back from the end, the way an HTTP range does: a list
79225
- // opening on its last items asks for them before it knows how many
79226
- // there are.
79338
+ if (scrolled === "end" && countKnown === undefined) {
79339
+ // Except the end, which no window can frame before the count is
79340
+ // known: counted back from, the way an HTTP range does.
79227
79341
  start = -budget;
79228
79342
  end = -1;
79229
- } else if (scrolled && scrolled.id !== undefined) {
79230
- // Asked for by name, since a place can have changed hands since it
79231
- // was written down — but the place it had is sent too, so a source
79232
- // that paginates by index has something to work with, and the answer
79233
- // says where it really landed (see the page's own `start`).
79234
- const from = typeof scrolled.index === "number" ? rankOfIndex(scrolled.index) - Math.floor(budget / 2) : 0;
79235
- start = from < 0 ? 0 : from;
79236
- end = start + budget - 1;
79343
+ } else if (start === -1) {
79344
+ // The window does not reach the run yet: its first page is asked
79345
+ // for all the same, which says how many items it stands for.
79346
+ start = 0;
79347
+ end = budget - 1;
79348
+ }
79349
+ if (scrolled && scrolled.id !== undefined) {
79350
+ // Asked for by name too, since a place can have changed hands since
79351
+ // it was written down: the answer says where it really landed (see
79352
+ // the page's own `start`).
79237
79353
  around = scrolled.id;
79238
- } else {
79239
- const first = typeof scrolled === "number" ? rankOfIndex(scrolled) - Math.floor(budget / 2) : 0;
79240
- start = first < 0 ? 0 : first;
79241
- end = start + budget - 1;
79242
79354
  }
79243
79355
  }
79244
79356
  const ask = () => {
@@ -79252,6 +79364,12 @@ const useItemStore = ({
79252
79364
  const debugAsk = outcome => {
79253
79365
  debugScroll(`ask ${start}-${end}: ${outcome}`, `(revalidating=${revalidating} holdPending=${listItems.holdPending} count=${pages.count})`);
79254
79366
  };
79367
+ if (listItems.windowOnItsWay()) {
79368
+ // The list moved its window after this render drew the one it had:
79369
+ // the run draws the new one in the list's next pass, and asks then.
79370
+ debugAsk("the list is moving its window");
79371
+ return;
79372
+ }
79255
79373
  if (start === -1) {
79256
79374
  // Nothing missing and nothing to revalidate: the run has what it
79257
79375
  // draws.
@@ -79347,51 +79465,21 @@ const useItemStore = ({
79347
79465
  const pageItems = Array.isArray(page) ? page : page.items;
79348
79466
  const pageStart = Array.isArray(page) ? 0 : page.start ?? 0;
79349
79467
  const pageCount = Array.isArray(page) ? pageItems.length : page.count ?? pageStart + pageItems.length;
79468
+ const pageLanded = {
79469
+ pageItems,
79470
+ pageStart,
79471
+ pageCount
79472
+ };
79473
+ if (!mountedRef.current) {
79474
+ // No render is coming to take it in, and the source still keeps
79475
+ // what came back for the next list drawing this collection.
79476
+ takeInPage(pageLanded);
79477
+ return;
79478
+ }
79350
79479
  // Before the items land: what is on screen has to stay where it is,
79351
79480
  // and the DOM still shows the state to hold onto.
79352
79481
  listItems.captureAnchor();
79353
- // An item from before is gone from where it stood when the page puts
79354
- // it at another rank (it would be drawn twice), or when its rank is
79355
- // past the end of the collection.
79356
- const staleRanksNow = staleRanksRef.current;
79357
- const pageEnd = pageStart + pageItems.length;
79358
- let dropped = false;
79359
- if (staleRanksNow.size > 0) {
79360
- const pageIds = new Set();
79361
- for (const pageItem of pageItems) {
79362
- if (pageItem && pageItem.id !== undefined) {
79363
- pageIds.add(pageItem.id);
79364
- }
79365
- }
79366
- for (const rank of staleRanksNow) {
79367
- if (rank >= pageStart && rank < pageEnd) {
79368
- continue;
79369
- }
79370
- const staleItem = pages.byIndex.get(rank);
79371
- if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
79372
- staleRanksNow.delete(rank);
79373
- pages.byIndex.delete(rank);
79374
- dropped = true;
79375
- }
79376
- }
79377
- }
79378
- let i = 0;
79379
- while (i < pageItems.length) {
79380
- pages.byIndex.set(pageStart + i, pageItems[i]);
79381
- staleRanksNow.delete(pageStart + i);
79382
- i++;
79383
- }
79384
- pages.count = pageCount;
79385
- // Which rank holds which id, kept by the source so a list drawing
79386
- // this collection again finds it drawn (see readComposition above).
79387
- if (itemsAction.writeComposition) {
79388
- itemsAction.writeComposition({
79389
- byIndex: pages.byIndex,
79390
- count: pageCount,
79391
- replace: dropped
79392
- });
79393
- }
79394
- listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
79482
+ pagesLandedRef.current.push(pageLanded);
79395
79483
  setPageVersion(version => version + 1);
79396
79484
  };
79397
79485
  const failed = error => {
@@ -79436,9 +79524,20 @@ const useItemStore = ({
79436
79524
  done(result);
79437
79525
  }
79438
79526
  };
79527
+ // Asked once the commit is over rather than from the effect: the list
79528
+ // frames its window after its runs, in that same commit — sized on the
79529
+ // screen, aimed at the item it is held on — and what the run asks for is
79530
+ // the window the list settles on. A render coming meanwhile asks instead.
79439
79531
  useLayoutEffect(() => {
79440
- ask();
79441
- publishRequestState();
79532
+ askTurnRef.current++;
79533
+ const turn = askTurnRef.current;
79534
+ queueMicrotask(() => {
79535
+ if (turn !== askTurnRef.current || !mountedRef.current) {
79536
+ return;
79537
+ }
79538
+ ask();
79539
+ publishRequestState();
79540
+ });
79442
79541
  });
79443
79542
  }
79444
79543
  };