@jsenv/navi 0.29.56 → 0.29.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +146 -25
- package/dist/jsenv_navi.js.map +9 -5
- package/docs/AI_INSTRUCTIONS.md +2 -1
- package/docs/list_refresh.md +39 -7
- package/docs/resource.md +8 -2
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -6,7 +6,7 @@ import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visua
|
|
|
6
6
|
export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
7
7
|
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
8
8
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
|
-
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
9
|
+
import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
|
|
10
10
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
11
11
|
import { useContext, useLayoutEffect, useCallback, useRef, useState, useEffect, useMemo, useId, useErrorBoundary } from "preact/hooks";
|
|
12
12
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
@@ -33903,31 +33903,61 @@ const getParamScope = (params) => {
|
|
|
33903
33903
|
* replays it, and takes a place in the rerun graph — two things a slice must
|
|
33904
33904
|
* not do: the list already holds the slices it received and glues them back
|
|
33905
33905
|
* together, and a mutation would otherwise send every slice ever loaded back to
|
|
33906
|
-
* the network at once. So the reader keeps
|
|
33907
|
-
* the range the list asks for, writes what comes back into the store, and
|
|
33908
|
-
* the rows back as store items — never a copy of the JSON, so the
|
|
33909
|
-
* row reads are the shared ones and a request sent from a row is
|
|
33910
|
-
* it. A row following its own fields through a write reads them
|
|
33911
|
-
* (`RESOURCE.useById(id)`): an update replaces the item object,
|
|
33912
|
-
* list is holding is the one it was given.
|
|
33906
|
+
* the network at once. So the reader keeps no response. It runs the callback
|
|
33907
|
+
* with the range the list asks for, writes what comes back into the store, and
|
|
33908
|
+
* hands the rows back as store items — never a copy of the JSON, so the
|
|
33909
|
+
* relations a row reads are the shared ones and a request sent from a row is
|
|
33910
|
+
* read back on it. A row following its own fields through a write reads them
|
|
33911
|
+
* from the store (`RESOURCE.useById(id)`): an update replaces the item object,
|
|
33912
|
+
* and the one the list is holding is the one it was given.
|
|
33913
|
+
*
|
|
33914
|
+
* What the reader does keep is the collection's composition: which rank holds
|
|
33915
|
+
* which id, and how many ranks there are, per resolved bound params. The rows
|
|
33916
|
+
* themselves are in the store already; the composition is the one thing about a
|
|
33917
|
+
* paginated collection the store cannot model, and without it a list that left
|
|
33918
|
+
* the screen comes back to skeletons and a request for rows it had a second
|
|
33919
|
+
* before. Ids and a count, so nothing here can hold a stale copy of a row, and
|
|
33920
|
+
* a row dropped from the store simply stops resolving. A list that comes back
|
|
33921
|
+
* draws the composition it left and asks again for the window it draws — the
|
|
33922
|
+
* revalidation an invalidation goes through, from a fresh mount.
|
|
33913
33923
|
*
|
|
33914
33924
|
* The reader is a function, so a list feeds on it the way it feeds on any other
|
|
33915
33925
|
* source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
|
|
33916
33926
|
*
|
|
33917
|
-
*
|
|
33918
|
-
*
|
|
33919
|
-
*
|
|
33920
|
-
* and asks again — the counterpart, for a reader,
|
|
33921
|
-
* action. Every reader made by `bindParams` shares
|
|
33922
|
-
*
|
|
33927
|
+
* A mutation that decides who belongs to the collection (a POST, a DELETE,
|
|
33928
|
+
* whatever `rerunOn.GET_RANGE` says) bumps `invalidationSignal` and drops the
|
|
33929
|
+
* compositions: they stand for an order that is gone. Whoever reads slices
|
|
33930
|
+
* through the reader then goes and asks again — the counterpart, for a reader,
|
|
33931
|
+
* of what a rerun is for an action. Every reader made by `bindParams` shares
|
|
33932
|
+
* the signal and the compositions of the one it comes from: the params say
|
|
33933
|
+
* which slices are read, not which collection.
|
|
33923
33934
|
*/
|
|
33924
33935
|
|
|
33925
33936
|
|
|
33926
33937
|
const createRangeReader = (
|
|
33927
33938
|
actionName,
|
|
33928
33939
|
callback,
|
|
33929
|
-
{
|
|
33940
|
+
{
|
|
33941
|
+
store,
|
|
33942
|
+
params: boundParams,
|
|
33943
|
+
invalidationSignal = signal(0),
|
|
33944
|
+
compositionSet = new Set(),
|
|
33945
|
+
},
|
|
33930
33946
|
) => {
|
|
33947
|
+
// Which composition this reader is about: the values its params hold, not its
|
|
33948
|
+
// own identity, so two `bindParams({ scope: "thread" })` made in two places
|
|
33949
|
+
// read and write the same one. There are as many compositions as there are
|
|
33950
|
+
// collections read through this reader — a handful, walked with the deep
|
|
33951
|
+
// comparison the rest of the codebase memoizes on.
|
|
33952
|
+
const currentParams = () => untracked(() => resolveParams(boundParams));
|
|
33953
|
+
const findComposition = (params) => {
|
|
33954
|
+
for (const composition of compositionSet) {
|
|
33955
|
+
if (compareTwoJsValues(composition.params, params)) {
|
|
33956
|
+
return composition;
|
|
33957
|
+
}
|
|
33958
|
+
}
|
|
33959
|
+
return null;
|
|
33960
|
+
};
|
|
33931
33961
|
const readRange = async (range = {}) => {
|
|
33932
33962
|
const { signal, ...rangeParams } = range;
|
|
33933
33963
|
const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
|
|
@@ -33959,14 +33989,80 @@ const createRangeReader = (
|
|
|
33959
33989
|
// stand for a composition that is gone.
|
|
33960
33990
|
readRange.invalidationSignal = invalidationSignal;
|
|
33961
33991
|
readRange.invalidate = () => {
|
|
33992
|
+
compositionSet.clear();
|
|
33962
33993
|
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
33963
33994
|
};
|
|
33995
|
+
// The rows of a composition, drawn from the store: a rank whose row is gone
|
|
33996
|
+
// from the store resolves to nothing and is asked for again.
|
|
33997
|
+
readRange.readComposition = () => {
|
|
33998
|
+
const composition = findComposition(currentParams());
|
|
33999
|
+
if (!composition) {
|
|
34000
|
+
return null;
|
|
34001
|
+
}
|
|
34002
|
+
const byIndex = new Map();
|
|
34003
|
+
untracked(() => {
|
|
34004
|
+
for (const [index, id] of composition.idByIndex) {
|
|
34005
|
+
const item = store.select(id);
|
|
34006
|
+
if (item) {
|
|
34007
|
+
byIndex.set(index, item);
|
|
34008
|
+
}
|
|
34009
|
+
}
|
|
34010
|
+
});
|
|
34011
|
+
return { byIndex, count: composition.count };
|
|
34012
|
+
};
|
|
34013
|
+
// `replace` is a revalidation: the ranks that are not in what just came back
|
|
34014
|
+
// stood for a composition that has moved on. Otherwise the ranks are merged,
|
|
34015
|
+
// so two lists reading the same collection through their own windows add up
|
|
34016
|
+
// to one composition instead of taking turns erasing each other.
|
|
34017
|
+
readRange.writeComposition = ({ byIndex, count, replace }) => {
|
|
34018
|
+
const params = currentParams();
|
|
34019
|
+
let composition = findComposition(params);
|
|
34020
|
+
if (!composition) {
|
|
34021
|
+
composition = { params, idByIndex: new Map(), count };
|
|
34022
|
+
compositionSet.add(composition);
|
|
34023
|
+
} else if (replace) {
|
|
34024
|
+
composition.idByIndex = new Map();
|
|
34025
|
+
}
|
|
34026
|
+
composition.count = count;
|
|
34027
|
+
for (const [index, item] of byIndex) {
|
|
34028
|
+
const id = item ? item[store.idKey] : undefined;
|
|
34029
|
+
if (id !== undefined) {
|
|
34030
|
+
composition.idByIndex.set(index, id);
|
|
34031
|
+
}
|
|
34032
|
+
}
|
|
34033
|
+
};
|
|
34034
|
+
// The same trade a list makes with the rows it holds, applied to what is kept
|
|
34035
|
+
// for the next mount. Two lists on one collection each trim by their own
|
|
34036
|
+
// window; the rows a list is drawing stay on its screen regardless — a rank
|
|
34037
|
+
// dropped here is one that gets asked for again after a remount.
|
|
34038
|
+
readRange.trimComposition = (keepFrom, keepTo, budget) => {
|
|
34039
|
+
const composition = findComposition(currentParams());
|
|
34040
|
+
if (!composition || !budget || composition.idByIndex.size <= budget) {
|
|
34041
|
+
return;
|
|
34042
|
+
}
|
|
34043
|
+
for (const index of composition.idByIndex.keys()) {
|
|
34044
|
+
if (index < keepFrom || index > keepTo) {
|
|
34045
|
+
composition.idByIndex.delete(index);
|
|
34046
|
+
}
|
|
34047
|
+
}
|
|
34048
|
+
};
|
|
34049
|
+
// Memoized for the reasons an action's bindParams is (see actions.js): params
|
|
34050
|
+
// and the reader they make have synchronized lifetimes, and params equal in
|
|
34051
|
+
// value give back the reader that already exists instead of a second one.
|
|
34052
|
+
const readerByParams = createJsValueWeakMap();
|
|
33964
34053
|
readRange.bindParams = (paramsToBind) => {
|
|
33965
|
-
|
|
34054
|
+
const existing = readerByParams.get(paramsToBind);
|
|
34055
|
+
if (existing) {
|
|
34056
|
+
return existing;
|
|
34057
|
+
}
|
|
34058
|
+
const reader = createRangeReader(actionName, callback, {
|
|
33966
34059
|
store,
|
|
33967
34060
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
33968
34061
|
invalidationSignal,
|
|
34062
|
+
compositionSet,
|
|
33969
34063
|
});
|
|
34064
|
+
readerByParams.set(paramsToBind, reader);
|
|
34065
|
+
return reader;
|
|
33970
34066
|
};
|
|
33971
34067
|
return readRange;
|
|
33972
34068
|
};
|
|
@@ -58697,19 +58793,30 @@ const useItemStore = ({
|
|
|
58697
58793
|
itemsAction,
|
|
58698
58794
|
memoryBudget
|
|
58699
58795
|
}) => {
|
|
58796
|
+
// What the source kept of the collection when the screen it was on went away
|
|
58797
|
+
// (a range reader keeps the composition: see resource_range_reader.js). The
|
|
58798
|
+
// rows are drawn from it right away and the window is asked for again — the
|
|
58799
|
+
// revalidation below, entered from a fresh mount rather than from a write.
|
|
58700
58800
|
const pagesRef = useRef(null);
|
|
58801
|
+
let restored = false;
|
|
58701
58802
|
if (!pagesRef.current) {
|
|
58702
|
-
|
|
58703
|
-
|
|
58704
|
-
|
|
58705
|
-
|
|
58803
|
+
const composition = typeof itemsAction === "function" && itemsAction.readComposition ? itemsAction.readComposition() : null;
|
|
58804
|
+
if (composition && composition.count !== undefined) {
|
|
58805
|
+
pagesRef.current = composition;
|
|
58806
|
+
restored = true;
|
|
58807
|
+
} else {
|
|
58808
|
+
pagesRef.current = {
|
|
58809
|
+
byIndex: new Map(),
|
|
58810
|
+
count: undefined
|
|
58811
|
+
};
|
|
58812
|
+
}
|
|
58706
58813
|
}
|
|
58707
58814
|
const pages = pagesRef.current;
|
|
58708
58815
|
const [, setPageVersion] = useState(0);
|
|
58709
58816
|
// The rows held are out of date and the run has not asked for the new ones
|
|
58710
58817
|
// yet. They stay on screen until the answer comes: what is drawn is from
|
|
58711
58818
|
// before, which is not the same thing as nothing to draw.
|
|
58712
|
-
const staleRef = useRef(
|
|
58819
|
+
const staleRef = useRef(restored);
|
|
58713
58820
|
const [refreshing, setRefreshing] = useState(false);
|
|
58714
58821
|
// A source that says when what it reads has moved (a resource range reader
|
|
58715
58822
|
// does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
|
|
@@ -58767,16 +58874,21 @@ const useItemStore = ({
|
|
|
58767
58874
|
// trade the render window makes, one order of magnitude further out.
|
|
58768
58875
|
forget: (windowFrom, windowTo) => {
|
|
58769
58876
|
const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
|
|
58770
|
-
if (!budget
|
|
58877
|
+
if (!budget) {
|
|
58771
58878
|
return;
|
|
58772
58879
|
}
|
|
58773
58880
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
58774
58881
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
58775
|
-
|
|
58776
|
-
|
|
58777
|
-
|
|
58882
|
+
if (pages.byIndex.size > budget) {
|
|
58883
|
+
for (const index of pages.byIndex.keys()) {
|
|
58884
|
+
if (index < keepFrom || index > keepTo) {
|
|
58885
|
+
pages.byIndex.delete(index);
|
|
58886
|
+
}
|
|
58778
58887
|
}
|
|
58779
58888
|
}
|
|
58889
|
+
if (typeof itemsAction === "function" && itemsAction.trimComposition) {
|
|
58890
|
+
itemsAction.trimComposition(keepFrom, keepTo, budget);
|
|
58891
|
+
}
|
|
58780
58892
|
},
|
|
58781
58893
|
retry: () => {
|
|
58782
58894
|
const request = requestRef.current;
|
|
@@ -58942,6 +59054,15 @@ const useItemStore = ({
|
|
|
58942
59054
|
i++;
|
|
58943
59055
|
}
|
|
58944
59056
|
pages.count = pageCount;
|
|
59057
|
+
// Which rank holds which id, kept by the source so a list drawing
|
|
59058
|
+
// this collection again finds it drawn (see readComposition above).
|
|
59059
|
+
if (itemsAction.writeComposition) {
|
|
59060
|
+
itemsAction.writeComposition({
|
|
59061
|
+
byIndex: pages.byIndex,
|
|
59062
|
+
count: pageCount,
|
|
59063
|
+
replace: revalidating
|
|
59064
|
+
});
|
|
59065
|
+
}
|
|
58945
59066
|
virtual.pagesSignal.value = virtual.pagesSignal.peek() + 1;
|
|
58946
59067
|
setPageVersion(version => version + 1);
|
|
58947
59068
|
};
|