@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.
- package/dist/dev/jsenv_navi.js +953 -119
- package/dist/dev/jsenv_navi.js.map +17 -11
- package/dist/jsenv_navi.js +953 -119
- package/dist/jsenv_navi.js.map +17 -11
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/data_states.md +3 -1
- package/docs/list_refresh.md +4 -1
- package/docs/network_policy.md +9 -4
- package/docs/resource.md +111 -1
- package/docs/scroll.md +3 -1
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2943,6 +2943,41 @@ const setActionPrivateProperties = (action, properties) => {
|
|
|
2943
2943
|
actionPrivatePropertiesWeakMap.set(action, properties);
|
|
2944
2944
|
};
|
|
2945
2945
|
|
|
2946
|
+
/*
|
|
2947
|
+
* Where an action that declares no provisionalValue of its own finds one: the
|
|
2948
|
+
* value the page kept from the previous document, for a route action that is
|
|
2949
|
+
* not a resource read (see keepPageOnScreen in nav/page_kept.js). routeAction
|
|
2950
|
+
* adds the source when it binds the action; a run asks for it at its start,
|
|
2951
|
+
* like it asks provisionalValue (actions.js).
|
|
2952
|
+
*
|
|
2953
|
+
* A registry beside actions.js rather than an import of the kept reads: those
|
|
2954
|
+
* know about stores and params keys, which import actions.js.
|
|
2955
|
+
*/
|
|
2956
|
+
|
|
2957
|
+
const keptValueSourcesWeakMap = new WeakMap();
|
|
2958
|
+
const addKeptValueSource = (rootAction, source) => {
|
|
2959
|
+
let sources = keptValueSourcesWeakMap.get(rootAction);
|
|
2960
|
+
if (!sources) {
|
|
2961
|
+
sources = [];
|
|
2962
|
+
keptValueSourcesWeakMap.set(rootAction, sources);
|
|
2963
|
+
}
|
|
2964
|
+
sources.push(source);
|
|
2965
|
+
};
|
|
2966
|
+
const takeKeptValue = (action, params) => {
|
|
2967
|
+
const rootAction = action.rootAction || action;
|
|
2968
|
+
const sources = keptValueSourcesWeakMap.get(rootAction);
|
|
2969
|
+
if (!sources) {
|
|
2970
|
+
return undefined;
|
|
2971
|
+
}
|
|
2972
|
+
for (const source of sources) {
|
|
2973
|
+
const value = source(params);
|
|
2974
|
+
if (value !== undefined) {
|
|
2975
|
+
return value;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
return undefined;
|
|
2979
|
+
};
|
|
2980
|
+
|
|
2946
2981
|
/*
|
|
2947
2982
|
* Actions: async callbacks wrapped in reactive state.
|
|
2948
2983
|
*
|
|
@@ -3548,8 +3583,11 @@ const actionWeakMap = new WeakMap();
|
|
|
3548
3583
|
* than a first load (data_states.md). Called at the start of every run whose
|
|
3549
3584
|
* value is undefined; the answer replaces it as usual, and `undefined` means
|
|
3550
3585
|
* "nothing known". A resource `GET` uses it to draw the row its store holds.
|
|
3551
|
-
*
|
|
3552
|
-
*
|
|
3586
|
+
* @param {boolean} [rootOptions.inheritData=true] - `false`: a binding moving
|
|
3587
|
+
* to new params starts the new instance without the previous params' answer
|
|
3588
|
+
* (see createActionProxyFromSignal). For an answer that would say something
|
|
3589
|
+
* false about the new params (a `GET` drawing another row), not for one that
|
|
3590
|
+
* stands in while the next one comes (a list of search results).
|
|
3553
3591
|
* @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
|
|
3554
3592
|
* answer stays good once it has landed. Without it an answer lives exactly as
|
|
3555
3593
|
* long as something references it: the screen that asked goes away, and
|
|
@@ -3592,6 +3630,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3592
3630
|
resultToValue,
|
|
3593
3631
|
valueToData,
|
|
3594
3632
|
provisionalValue,
|
|
3633
|
+
inheritData = true,
|
|
3595
3634
|
dataDefault,
|
|
3596
3635
|
data = dataDefault,
|
|
3597
3636
|
|
|
@@ -4054,8 +4093,12 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4054
4093
|
actionAbortMap.set(action, abort);
|
|
4055
4094
|
|
|
4056
4095
|
batch(() => {
|
|
4057
|
-
if (
|
|
4058
|
-
|
|
4096
|
+
if (valueSignal.peek() === undefined) {
|
|
4097
|
+
// An action saying nothing of its own may still have a value the
|
|
4098
|
+
// page kept from the previous document (see kept_values.js).
|
|
4099
|
+
const provisional = provisionalValue
|
|
4100
|
+
? provisionalValue(params, action)
|
|
4101
|
+
: takeKeptValue(action, params);
|
|
4059
4102
|
if (provisional !== undefined) {
|
|
4060
4103
|
valueSignal.value = provisional;
|
|
4061
4104
|
}
|
|
@@ -4255,6 +4298,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4255
4298
|
const privateProperties = {
|
|
4256
4299
|
valueInitial,
|
|
4257
4300
|
provisionalValue,
|
|
4301
|
+
inheritData,
|
|
4258
4302
|
|
|
4259
4303
|
performRun,
|
|
4260
4304
|
performReset,
|
|
@@ -4287,7 +4331,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4287
4331
|
* a list of filters changes, providing real-time results without user interaction.
|
|
4288
4332
|
* @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
|
|
4289
4333
|
* By default (true), the proxy carries over the previous target's value and error into the new action —
|
|
4290
|
-
* unless the action declares
|
|
4334
|
+
* unless the action itself declares `inheritData: false` (createAction), which a resource `GET` does.
|
|
4291
4335
|
* This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
|
|
4292
4336
|
* new action completes, not when it starts loading. Code that needs to distinguish loading state can still
|
|
4293
4337
|
* check `action.runningState`, while code that just reads `action.data` always sees the most recent
|
|
@@ -4334,12 +4378,12 @@ const createActionProxyFromSignal = (
|
|
|
4334
4378
|
let currentActionPrivateProperties = getActionPrivateProperties(action);
|
|
4335
4379
|
let actionTargetPreviousWeakRef = null;
|
|
4336
4380
|
|
|
4337
|
-
// An action
|
|
4338
|
-
//
|
|
4339
|
-
//
|
|
4340
|
-
//
|
|
4381
|
+
// An action whose answer for other params would be false for these (a
|
|
4382
|
+
// resource GET: another row) says so itself, and is not handed it. What it
|
|
4383
|
+
// does not know stays unknown: a skeleton, or its provisionalValue, never a
|
|
4384
|
+
// different row.
|
|
4341
4385
|
const inheritsAnswer =
|
|
4342
|
-
inheritData &&
|
|
4386
|
+
inheritData && getActionPrivateProperties(action).inheritData;
|
|
4343
4387
|
const createTarget = (params) => {
|
|
4344
4388
|
if (inheritsAnswer) {
|
|
4345
4389
|
const previousActionTarget = actionTargetPreviousWeakRef?.deref();
|
|
@@ -10242,6 +10286,9 @@ const paramsTargetCanPlace = (redirectRoute, urlParams) => {
|
|
|
10242
10286
|
|
|
10243
10287
|
let setupRoutesCalled = false;
|
|
10244
10288
|
let activeRouteSet = null;
|
|
10289
|
+
// True once the routes have been matched against an address. Before that, no
|
|
10290
|
+
// route matching says nothing about the page: nothing has been asked yet.
|
|
10291
|
+
const routedSignal = signal(false);
|
|
10245
10292
|
|
|
10246
10293
|
const routePreloadMap = new WeakMap();
|
|
10247
10294
|
const registerRoutePreload = (route, preload) => {
|
|
@@ -10666,6 +10713,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
10666
10713
|
}
|
|
10667
10714
|
// Reset flag after URL -> Signal synchronization is complete
|
|
10668
10715
|
isUpdatingRoutesFromUrl = false;
|
|
10716
|
+
routedSignal.value = true;
|
|
10669
10717
|
Object.assign(returnValue, { matchingRouteSet });
|
|
10670
10718
|
for (const [route, repairedParams] of repairedParamsByRoute) {
|
|
10671
10719
|
route.replaceParams(repairedParams, {
|
|
@@ -10715,6 +10763,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
10715
10763
|
droppingRouteSet = null;
|
|
10716
10764
|
setupRoutesCalled = false;
|
|
10717
10765
|
activeRouteSet = null;
|
|
10766
|
+
routedSignal.value = false;
|
|
10718
10767
|
};
|
|
10719
10768
|
return {
|
|
10720
10769
|
updateRoutes,
|
|
@@ -15033,6 +15082,13 @@ const createOpenController = (
|
|
|
15033
15082
|
change();
|
|
15034
15083
|
};
|
|
15035
15084
|
const runChange = (change, { opened, event }) => {
|
|
15085
|
+
// What the controller answers about itself is the decision, written before
|
|
15086
|
+
// the change runs, whatever runs it. Whoever asks meanwhile reads it: the
|
|
15087
|
+
// render an opening forces (onOpen, mountContent) reads the open state
|
|
15088
|
+
// the caller holds, and an onOpen writing the popup's own `signal` has
|
|
15089
|
+
// useOpenPropsEffectOnOpenController asking for this very opening — a
|
|
15090
|
+
// second one, nested in the first, if `opened` still said closed.
|
|
15091
|
+
controller.opened = opened;
|
|
15036
15092
|
const applyChange = () => {
|
|
15037
15093
|
// Recorded with the change itself: what the DOM shows is what a render
|
|
15038
15094
|
// landing between the ask and the picture must draw (see `openedInDom`).
|
|
@@ -15048,12 +15104,8 @@ const createOpenController = (
|
|
|
15048
15104
|
applyChange();
|
|
15049
15105
|
return;
|
|
15050
15106
|
}
|
|
15051
|
-
//
|
|
15052
|
-
//
|
|
15053
|
-
// useOpenPropsEffectOnOpenController, which writes it back into the
|
|
15054
|
-
// caller's own signal), and a request arriving before the change lands
|
|
15055
|
-
// runs it first rather than reading a DOM that disagrees.
|
|
15056
|
-
controller.opened = opened;
|
|
15107
|
+
// A request arriving before the change lands runs it first rather than
|
|
15108
|
+
// reading a DOM that disagrees.
|
|
15057
15109
|
changeAwaitingTransition = applyChange;
|
|
15058
15110
|
transitionChange(
|
|
15059
15111
|
() => {
|
|
@@ -15066,7 +15118,6 @@ const createOpenController = (
|
|
|
15066
15118
|
};
|
|
15067
15119
|
|
|
15068
15120
|
const performClose = (closeEvent) => {
|
|
15069
|
-
controller.opened = false;
|
|
15070
15121
|
// Read before any close effect touches the DOM: closing a native <dialog>
|
|
15071
15122
|
// hands the focus back to whatever held it at showModal() time, so by the
|
|
15072
15123
|
// time the close cleanup runs, the popup's content has already lost the
|
|
@@ -15334,7 +15385,6 @@ const createOpenController = (
|
|
|
15334
15385
|
// told it opened right after (see popup_content_mount.js and
|
|
15335
15386
|
// use_displayed_layout_effect.js).
|
|
15336
15387
|
controller.openedInDom = true;
|
|
15337
|
-
controller.opened = true;
|
|
15338
15388
|
// Which press it opened during, so the release of that press is not
|
|
15339
15389
|
// read as somebody dismissing it (see openedDuringThisPress).
|
|
15340
15390
|
controller.pressCountAtOpen = pressCount;
|
|
@@ -15761,8 +15811,10 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15761
15811
|
return undefined;
|
|
15762
15812
|
}
|
|
15763
15813
|
// Skip when the controller is already in the desired state.
|
|
15764
|
-
// openController.opened
|
|
15765
|
-
//
|
|
15814
|
+
// openController.opened is what the controller decided, written before an
|
|
15815
|
+
// opening renders anything (see runChange), so it is the authoritative
|
|
15816
|
+
// check against feedback loops — the popup's own onOpen writing its
|
|
15817
|
+
// `signal` included.
|
|
15766
15818
|
if (open === openController.opened) {
|
|
15767
15819
|
return undefined;
|
|
15768
15820
|
}
|
|
@@ -25828,6 +25880,172 @@ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
|
|
|
25828
25880
|
return childStore !== undefined && childStore === getChildStore(ownerId);
|
|
25829
25881
|
};
|
|
25830
25882
|
|
|
25883
|
+
/*
|
|
25884
|
+
* What a previous document kept of the page it was on (see keepPageOnScreen in
|
|
25885
|
+
* nav/page_kept.js), waiting for the reads that draw it again, and what those
|
|
25886
|
+
* reads are made of on the way out: rows with their relations inline.
|
|
25887
|
+
*
|
|
25888
|
+
* A kept read is found by the read's name and the params it was asked with
|
|
25889
|
+
* (getKeptReadKey). It is handed out once: the first run of the read draws it
|
|
25890
|
+
* while its request is out, and from then on the store holds the rows. Handed
|
|
25891
|
+
* out again, after a reset say, it would write the previous document's rows
|
|
25892
|
+
* over a fresher answer.
|
|
25893
|
+
*
|
|
25894
|
+
* This module is the half the reads see: `resource()` and the range reader
|
|
25895
|
+
* take from it and say which of their reads are kept as rows, routeAction takes
|
|
25896
|
+
* from it for any other action (through action/kept_values.js). Which reads are
|
|
25897
|
+
* on the page on screen, and what the slot is written into, is page_kept.js.
|
|
25898
|
+
*/
|
|
25899
|
+
|
|
25900
|
+
// key → what the read answered in the previous document, not handed out yet
|
|
25901
|
+
const offeredReadMap = new Map();
|
|
25902
|
+
const offerKeptReads = (entryMap) => {
|
|
25903
|
+
offeredReadMap.clear();
|
|
25904
|
+
for (const [key, entry] of entryMap) {
|
|
25905
|
+
offeredReadMap.set(key, entry);
|
|
25906
|
+
}
|
|
25907
|
+
};
|
|
25908
|
+
const withdrawKeptReads = () => {
|
|
25909
|
+
offeredReadMap.clear();
|
|
25910
|
+
};
|
|
25911
|
+
const takeKeptRead = (key) => {
|
|
25912
|
+
const entry = offeredReadMap.get(key);
|
|
25913
|
+
if (entry === undefined) {
|
|
25914
|
+
return undefined;
|
|
25915
|
+
}
|
|
25916
|
+
offeredReadMap.delete(key);
|
|
25917
|
+
return entry;
|
|
25918
|
+
};
|
|
25919
|
+
|
|
25920
|
+
// The root actions whose answer is rows of a store: a resource's GET and
|
|
25921
|
+
// GET_MANY. They are kept as rows, since their value (ids) names rows a new
|
|
25922
|
+
// document's store does not hold. Any other route action is kept by its value
|
|
25923
|
+
// (see page_kept.js).
|
|
25924
|
+
const keepableReadWeakSet = new WeakSet();
|
|
25925
|
+
const markKeepableRead = (rootAction) => {
|
|
25926
|
+
keepableReadWeakSet.add(rootAction);
|
|
25927
|
+
};
|
|
25928
|
+
const isKeepableRead = (action) => {
|
|
25929
|
+
const rootAction = action.rootAction || action;
|
|
25930
|
+
return keepableReadWeakSet.has(rootAction);
|
|
25931
|
+
};
|
|
25932
|
+
|
|
25933
|
+
// The range readers a <List.Items> on screen reads through, each with the number
|
|
25934
|
+
// of lists reading it: the reads of the page a route action does not cover.
|
|
25935
|
+
const shownRangeReadersSignal = signal(new Map());
|
|
25936
|
+
const showRangeReader = (reader) => {
|
|
25937
|
+
const shownMap = new Map(shownRangeReadersSignal.peek());
|
|
25938
|
+
shownMap.set(reader, (shownMap.get(reader) || 0) + 1);
|
|
25939
|
+
shownRangeReadersSignal.value = shownMap;
|
|
25940
|
+
return () => {
|
|
25941
|
+
const shownMapNow = new Map(shownRangeReadersSignal.peek());
|
|
25942
|
+
const count = shownMapNow.get(reader);
|
|
25943
|
+
if (count === 1) {
|
|
25944
|
+
shownMapNow.delete(reader);
|
|
25945
|
+
} else {
|
|
25946
|
+
shownMapNow.set(reader, count - 1);
|
|
25947
|
+
}
|
|
25948
|
+
shownRangeReadersSignal.value = shownMapNow;
|
|
25949
|
+
};
|
|
25950
|
+
};
|
|
25951
|
+
|
|
25952
|
+
// A row already in the store is the fresher one: the kept copy only stands in
|
|
25953
|
+
// for rows nothing has answered yet. Its relations are written as they were
|
|
25954
|
+
// kept, which a real answer corrects like any other.
|
|
25955
|
+
const upsertKeptItem = (store, keptItem) => {
|
|
25956
|
+
const id = keptItem[store.idKey];
|
|
25957
|
+
if (id !== undefined) {
|
|
25958
|
+
const existing = store.select(id);
|
|
25959
|
+
if (existing) {
|
|
25960
|
+
return existing;
|
|
25961
|
+
}
|
|
25962
|
+
}
|
|
25963
|
+
return store.upsert(keptItem);
|
|
25964
|
+
};
|
|
25965
|
+
|
|
25966
|
+
const getKeptReadKey = (name, params) => {
|
|
25967
|
+
return `${name}|${getParamsKey(params)}`;
|
|
25968
|
+
};
|
|
25969
|
+
// `true` is what a route action without params is bound to (see routeAction),
|
|
25970
|
+
// and a key holding undefined says nothing a missing one does not.
|
|
25971
|
+
const getParamsKey = (params) => {
|
|
25972
|
+
if (params === undefined || params === NO_PARAMS || params === true) {
|
|
25973
|
+
return "";
|
|
25974
|
+
}
|
|
25975
|
+
if (isPlainParams(params)) {
|
|
25976
|
+
const keys = Object.keys(params).filter((key) => params[key] !== undefined);
|
|
25977
|
+
if (keys.length === 0) {
|
|
25978
|
+
return "";
|
|
25979
|
+
}
|
|
25980
|
+
}
|
|
25981
|
+
return stableStringify(params);
|
|
25982
|
+
};
|
|
25983
|
+
const isPlainParams = (value) => {
|
|
25984
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25985
|
+
};
|
|
25986
|
+
const stableStringify = (value) => {
|
|
25987
|
+
if (value === null || typeof value !== "object") {
|
|
25988
|
+
return JSON.stringify(value);
|
|
25989
|
+
}
|
|
25990
|
+
if (Array.isArray(value)) {
|
|
25991
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
25992
|
+
}
|
|
25993
|
+
const keys = Object.keys(value)
|
|
25994
|
+
.filter((key) => value[key] !== undefined)
|
|
25995
|
+
.sort();
|
|
25996
|
+
const parts = [];
|
|
25997
|
+
for (const key of keys) {
|
|
25998
|
+
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
25999
|
+
}
|
|
26000
|
+
return `{${parts.join(",")}}`;
|
|
26001
|
+
};
|
|
26002
|
+
|
|
26003
|
+
// What a read's data is made of, ready to be written: a row, or the rows of a
|
|
26004
|
+
// list, each with its relations inline.
|
|
26005
|
+
const serializeData = (data) => {
|
|
26006
|
+
if (Array.isArray(data)) {
|
|
26007
|
+
return data.map((item) => serializeItem(item, new Set()));
|
|
26008
|
+
}
|
|
26009
|
+
return serializeItem(data, new Set());
|
|
26010
|
+
};
|
|
26011
|
+
// The item with its relations inline: a relation value stands for what its
|
|
26012
|
+
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
26013
|
+
// twice on the same path is written as its id — the setters accept both.
|
|
26014
|
+
const serializeItem = (item, ancestorSet) => {
|
|
26015
|
+
ancestorSet.add(item);
|
|
26016
|
+
const serialized = {};
|
|
26017
|
+
for (const key of Object.keys(item)) {
|
|
26018
|
+
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
26019
|
+
}
|
|
26020
|
+
ancestorSet.delete(item);
|
|
26021
|
+
return serialized;
|
|
26022
|
+
};
|
|
26023
|
+
const serializeValue = (value, ancestorSet) => {
|
|
26024
|
+
if (value === null || typeof value !== "object") {
|
|
26025
|
+
return value;
|
|
26026
|
+
}
|
|
26027
|
+
if (Array.isArray(value)) {
|
|
26028
|
+
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
26029
|
+
}
|
|
26030
|
+
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
26031
|
+
if (objectSignal) {
|
|
26032
|
+
const held = objectSignal.value;
|
|
26033
|
+
if (held === undefined || held === null) {
|
|
26034
|
+
return null;
|
|
26035
|
+
}
|
|
26036
|
+
return serializeValue(held, ancestorSet);
|
|
26037
|
+
}
|
|
26038
|
+
if (typeof value.toJSON === "function") {
|
|
26039
|
+
return value;
|
|
26040
|
+
}
|
|
26041
|
+
if (ancestorSet.has(value)) {
|
|
26042
|
+
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
26043
|
+
? value[SYMBOL_IDENTITY]
|
|
26044
|
+
: null;
|
|
26045
|
+
}
|
|
26046
|
+
return serializeItem(value, ancestorSet);
|
|
26047
|
+
};
|
|
26048
|
+
|
|
25831
26049
|
const paramScopeWeakSet = createIterableWeakSet();
|
|
25832
26050
|
let paramScopeIdCounter = 0;
|
|
25833
26051
|
const getParamScope = (params) => {
|
|
@@ -25998,66 +26216,6 @@ const createResourcePersistence = (
|
|
|
25998
26216
|
};
|
|
25999
26217
|
};
|
|
26000
26218
|
|
|
26001
|
-
const getParamsKey = (params) => {
|
|
26002
|
-
if (params === undefined || params === NO_PARAMS) {
|
|
26003
|
-
return "";
|
|
26004
|
-
}
|
|
26005
|
-
return stableStringify(params);
|
|
26006
|
-
};
|
|
26007
|
-
|
|
26008
|
-
const stableStringify = (value) => {
|
|
26009
|
-
if (value === null || typeof value !== "object") {
|
|
26010
|
-
return JSON.stringify(value);
|
|
26011
|
-
}
|
|
26012
|
-
if (Array.isArray(value)) {
|
|
26013
|
-
return `[${value.map(stableStringify).join(",")}]`;
|
|
26014
|
-
}
|
|
26015
|
-
const keys = Object.keys(value).sort();
|
|
26016
|
-
const parts = [];
|
|
26017
|
-
for (const key of keys) {
|
|
26018
|
-
parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
26019
|
-
}
|
|
26020
|
-
return `{${parts.join(",")}}`;
|
|
26021
|
-
};
|
|
26022
|
-
|
|
26023
|
-
// The item with its relations inline: a relation value stands for what its
|
|
26024
|
-
// signal holds (the child row, the child rows, or nothing), a row reached
|
|
26025
|
-
// twice on the same path is written as its id — the setters accept both.
|
|
26026
|
-
const serializeItem = (item, ancestorSet) => {
|
|
26027
|
-
ancestorSet.add(item);
|
|
26028
|
-
const serialized = {};
|
|
26029
|
-
for (const key of Object.keys(item)) {
|
|
26030
|
-
serialized[key] = serializeValue(item[key], ancestorSet);
|
|
26031
|
-
}
|
|
26032
|
-
ancestorSet.delete(item);
|
|
26033
|
-
return serialized;
|
|
26034
|
-
};
|
|
26035
|
-
const serializeValue = (value, ancestorSet) => {
|
|
26036
|
-
if (value === null || typeof value !== "object") {
|
|
26037
|
-
return value;
|
|
26038
|
-
}
|
|
26039
|
-
if (Array.isArray(value)) {
|
|
26040
|
-
return value.map((entry) => serializeValue(entry, ancestorSet));
|
|
26041
|
-
}
|
|
26042
|
-
const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
|
|
26043
|
-
if (objectSignal) {
|
|
26044
|
-
const held = objectSignal.value;
|
|
26045
|
-
if (held === undefined || held === null) {
|
|
26046
|
-
return null;
|
|
26047
|
-
}
|
|
26048
|
-
return serializeValue(held, ancestorSet);
|
|
26049
|
-
}
|
|
26050
|
-
if (typeof value.toJSON === "function") {
|
|
26051
|
-
return value;
|
|
26052
|
-
}
|
|
26053
|
-
if (ancestorSet.has(value)) {
|
|
26054
|
-
return Object.hasOwn(value, SYMBOL_IDENTITY)
|
|
26055
|
-
? value[SYMBOL_IDENTITY]
|
|
26056
|
-
: null;
|
|
26057
|
-
}
|
|
26058
|
-
return serializeItem(value, ancestorSet);
|
|
26059
|
-
};
|
|
26060
|
-
|
|
26061
26219
|
/*
|
|
26062
26220
|
* GET_RANGE: reading a resource one slice at a time, for a list that draws its
|
|
26063
26221
|
* rows as it goes (`<List.Items itemsAction>`).
|
|
@@ -26094,9 +26252,23 @@ const serializeValue = (value, ancestorSet) => {
|
|
|
26094
26252
|
* of what a rerun is for an action. Every reader made by `bindParams` shares
|
|
26095
26253
|
* the signal and the compositions of the one it comes from: the params say
|
|
26096
26254
|
* which slices are read, not which collection.
|
|
26255
|
+
*
|
|
26256
|
+
* A composition also outlives the document when a list on the page on screen
|
|
26257
|
+
* reads it (see keepPageOnScreen): the ranks around the window it draws are
|
|
26258
|
+
* written with their rows, and the first read of the next document finds them
|
|
26259
|
+
* as a composition left by a list that went away — the same return, after a
|
|
26260
|
+
* longer absence.
|
|
26097
26261
|
*/
|
|
26098
26262
|
|
|
26099
26263
|
|
|
26264
|
+
// The ranks kept of a composition for the next document: the window a list
|
|
26265
|
+
// draws, widened by at least one step on each side, and snapped to steps so a
|
|
26266
|
+
// scroll within one draws the same ranks and writes nothing.
|
|
26267
|
+
const KEPT_RANK_STEP = 20;
|
|
26268
|
+
// Counts the answers every composition took, so that one landing can be told
|
|
26269
|
+
// from one seen before (see readKept).
|
|
26270
|
+
let answerSequence = 0;
|
|
26271
|
+
|
|
26100
26272
|
const createRangeReader = (
|
|
26101
26273
|
actionName,
|
|
26102
26274
|
callback,
|
|
@@ -26105,6 +26277,9 @@ const createRangeReader = (
|
|
|
26105
26277
|
params: boundParams,
|
|
26106
26278
|
invalidationSignal = signal(0),
|
|
26107
26279
|
compositionSet = new Set(),
|
|
26280
|
+
// Bumped whenever what is kept of a composition for the next document
|
|
26281
|
+
// changes (see readKept); shared like the compositions themselves.
|
|
26282
|
+
keptVersionSignal = signal(0),
|
|
26108
26283
|
},
|
|
26109
26284
|
) => {
|
|
26110
26285
|
// Which composition this reader is about: the values its params hold, not its
|
|
@@ -26121,6 +26296,40 @@ const createRangeReader = (
|
|
|
26121
26296
|
}
|
|
26122
26297
|
return null;
|
|
26123
26298
|
};
|
|
26299
|
+
const keptKeyOf = (params) => getKeptReadKey(actionName, params);
|
|
26300
|
+
const bumpKeptVersion = () => {
|
|
26301
|
+
keptVersionSignal.value = keptVersionSignal.peek() + 1;
|
|
26302
|
+
};
|
|
26303
|
+
// The composition the previous document kept for these params, entering the
|
|
26304
|
+
// store and the compositions as if a list had just left it.
|
|
26305
|
+
const restoreKeptComposition = (params) => {
|
|
26306
|
+
const kept = takeKeptRead(keptKeyOf(params));
|
|
26307
|
+
if (!kept || typeof kept.count !== "number" || !kept.items) {
|
|
26308
|
+
return null;
|
|
26309
|
+
}
|
|
26310
|
+
const composition = {
|
|
26311
|
+
params,
|
|
26312
|
+
idByIndex: new Map(),
|
|
26313
|
+
count: kept.count,
|
|
26314
|
+
keptFrom: undefined,
|
|
26315
|
+
keptTo: undefined,
|
|
26316
|
+
answeredAt: 0,
|
|
26317
|
+
};
|
|
26318
|
+
for (const rank of Object.keys(kept.items)) {
|
|
26319
|
+
const index = Number(rank);
|
|
26320
|
+
const item = upsertKeptItem(store, kept.items[rank]);
|
|
26321
|
+
composition.idByIndex.set(index, item[store.idKey]);
|
|
26322
|
+
if (composition.keptFrom === undefined || index < composition.keptFrom) {
|
|
26323
|
+
composition.keptFrom = index;
|
|
26324
|
+
}
|
|
26325
|
+
if (composition.keptTo === undefined || index + 1 > composition.keptTo) {
|
|
26326
|
+
composition.keptTo = index + 1;
|
|
26327
|
+
}
|
|
26328
|
+
}
|
|
26329
|
+
compositionSet.add(composition);
|
|
26330
|
+
bumpKeptVersion();
|
|
26331
|
+
return composition;
|
|
26332
|
+
};
|
|
26124
26333
|
const readRange = async (range = {}) => {
|
|
26125
26334
|
const { signal, ...rangeParams } = range;
|
|
26126
26335
|
const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
|
|
@@ -26154,11 +26363,15 @@ const createRangeReader = (
|
|
|
26154
26363
|
readRange.invalidate = () => {
|
|
26155
26364
|
compositionSet.clear();
|
|
26156
26365
|
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
26366
|
+
bumpKeptVersion();
|
|
26157
26367
|
};
|
|
26158
26368
|
// The rows of a composition, drawn from the store: a rank whose row is gone
|
|
26159
26369
|
// from the store resolves to nothing and is asked for again.
|
|
26160
26370
|
readRange.readComposition = () => {
|
|
26161
|
-
const
|
|
26371
|
+
const params = currentParams();
|
|
26372
|
+
const composition = untracked(
|
|
26373
|
+
() => findComposition(params) || restoreKeptComposition(params),
|
|
26374
|
+
);
|
|
26162
26375
|
if (!composition) {
|
|
26163
26376
|
return null;
|
|
26164
26377
|
}
|
|
@@ -26182,18 +26395,85 @@ const createRangeReader = (
|
|
|
26182
26395
|
const params = currentParams();
|
|
26183
26396
|
let composition = findComposition(params);
|
|
26184
26397
|
if (!composition) {
|
|
26185
|
-
composition = {
|
|
26398
|
+
composition = {
|
|
26399
|
+
params,
|
|
26400
|
+
idByIndex: new Map(),
|
|
26401
|
+
count,
|
|
26402
|
+
keptFrom: undefined,
|
|
26403
|
+
keptTo: undefined,
|
|
26404
|
+
answeredAt: 0,
|
|
26405
|
+
};
|
|
26186
26406
|
compositionSet.add(composition);
|
|
26187
26407
|
} else if (replace) {
|
|
26188
26408
|
composition.idByIndex = new Map();
|
|
26189
26409
|
}
|
|
26190
26410
|
composition.count = count;
|
|
26411
|
+
answerSequence++;
|
|
26412
|
+
composition.answeredAt = answerSequence;
|
|
26191
26413
|
for (const [index, item] of byIndex) {
|
|
26192
26414
|
const id = item ? item[store.idKey] : undefined;
|
|
26193
26415
|
if (id !== undefined) {
|
|
26194
26416
|
composition.idByIndex.set(index, id);
|
|
26195
26417
|
}
|
|
26196
26418
|
}
|
|
26419
|
+
bumpKeptVersion();
|
|
26420
|
+
};
|
|
26421
|
+
// The ranks [windowFrom, windowTo) a list draws of this composition: what is
|
|
26422
|
+
// kept of it for the next document is around them (see readKept).
|
|
26423
|
+
readRange.frameComposition = (windowFrom, windowTo) => {
|
|
26424
|
+
const composition = findComposition(currentParams());
|
|
26425
|
+
if (!composition) {
|
|
26426
|
+
return;
|
|
26427
|
+
}
|
|
26428
|
+
let keptFrom =
|
|
26429
|
+
Math.floor((windowFrom - KEPT_RANK_STEP) / KEPT_RANK_STEP) *
|
|
26430
|
+
KEPT_RANK_STEP;
|
|
26431
|
+
if (keptFrom < 0) {
|
|
26432
|
+
keptFrom = 0;
|
|
26433
|
+
}
|
|
26434
|
+
const keptTo =
|
|
26435
|
+
Math.ceil((windowTo + KEPT_RANK_STEP) / KEPT_RANK_STEP) * KEPT_RANK_STEP;
|
|
26436
|
+
if (composition.keptFrom === keptFrom && composition.keptTo === keptTo) {
|
|
26437
|
+
return;
|
|
26438
|
+
}
|
|
26439
|
+
composition.keptFrom = keptFrom;
|
|
26440
|
+
composition.keptTo = keptTo;
|
|
26441
|
+
bumpKeptVersion();
|
|
26442
|
+
};
|
|
26443
|
+
// What a document keeps of the composition these params read, for the
|
|
26444
|
+
// next one (see keepPageOnScreen): the count, and the ranks around the
|
|
26445
|
+
// window last drawn with their rows inline. Read inside an effect, it
|
|
26446
|
+
// follows the composition and the rows. `answeredAt` grows with every answer
|
|
26447
|
+
// the composition takes (0: none in this document, restored from a kept one).
|
|
26448
|
+
readRange.readKept = () => {
|
|
26449
|
+
// eslint-disable-next-line no-unused-expressions
|
|
26450
|
+
keptVersionSignal.value;
|
|
26451
|
+
const params = resolveParams(boundParams);
|
|
26452
|
+
const key = keptKeyOf(params);
|
|
26453
|
+
const composition = findComposition(params);
|
|
26454
|
+
if (!composition || composition.count === undefined) {
|
|
26455
|
+
return { key, answeredAt: 0, readEntry: () => undefined };
|
|
26456
|
+
}
|
|
26457
|
+
const readEntry = () => {
|
|
26458
|
+
const items = {};
|
|
26459
|
+
for (const [index, id] of composition.idByIndex) {
|
|
26460
|
+
if (
|
|
26461
|
+
composition.keptFrom !== undefined &&
|
|
26462
|
+
index < composition.keptFrom
|
|
26463
|
+
) {
|
|
26464
|
+
continue;
|
|
26465
|
+
}
|
|
26466
|
+
if (composition.keptTo !== undefined && index >= composition.keptTo) {
|
|
26467
|
+
continue;
|
|
26468
|
+
}
|
|
26469
|
+
const item = store.select(id);
|
|
26470
|
+
if (item) {
|
|
26471
|
+
items[index] = serializeItem(item, new Set());
|
|
26472
|
+
}
|
|
26473
|
+
}
|
|
26474
|
+
return { count: composition.count, items };
|
|
26475
|
+
};
|
|
26476
|
+
return { key, answeredAt: composition.answeredAt, readEntry };
|
|
26197
26477
|
};
|
|
26198
26478
|
// The same trade a list makes with the rows it holds, applied to what is kept
|
|
26199
26479
|
// for the next mount. Two lists on one collection each trim by their own
|
|
@@ -26204,11 +26484,16 @@ const createRangeReader = (
|
|
|
26204
26484
|
if (!composition || composition.idByIndex.size <= budget) {
|
|
26205
26485
|
return;
|
|
26206
26486
|
}
|
|
26487
|
+
let trimmed = false;
|
|
26207
26488
|
for (const index of composition.idByIndex.keys()) {
|
|
26208
26489
|
if (index < keepFrom || index > keepTo) {
|
|
26209
26490
|
composition.idByIndex.delete(index);
|
|
26491
|
+
trimmed = true;
|
|
26210
26492
|
}
|
|
26211
26493
|
}
|
|
26494
|
+
if (trimmed) {
|
|
26495
|
+
bumpKeptVersion();
|
|
26496
|
+
}
|
|
26212
26497
|
};
|
|
26213
26498
|
// Memoized for the reasons an action's bindParams is (see actions.js): params
|
|
26214
26499
|
// and the reader they make have synchronized lifetimes, and params equal in
|
|
@@ -26224,6 +26509,7 @@ const createRangeReader = (
|
|
|
26224
26509
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
26225
26510
|
invalidationSignal,
|
|
26226
26511
|
compositionSet,
|
|
26512
|
+
keptVersionSignal,
|
|
26227
26513
|
});
|
|
26228
26514
|
readerByParams.set(paramsToBind, reader);
|
|
26229
26515
|
return reader;
|
|
@@ -27658,7 +27944,7 @@ const createRestActionFactoryForRoot = (
|
|
|
27658
27944
|
`${name}.${verb}`,
|
|
27659
27945
|
declarationSite,
|
|
27660
27946
|
);
|
|
27661
|
-
|
|
27947
|
+
const action = createAction(callback, {
|
|
27662
27948
|
name: `${name}.${verb}`,
|
|
27663
27949
|
meta: { verb, isMany: false, paramScope },
|
|
27664
27950
|
resultToValue: (result, action) => {
|
|
@@ -27694,17 +27980,33 @@ const createRestActionFactoryForRoot = (
|
|
|
27694
27980
|
},
|
|
27695
27981
|
valueToData: (itemId) => store.select(itemId),
|
|
27696
27982
|
// While the request is out, the row the store already holds for these
|
|
27697
|
-
// params is drawn
|
|
27983
|
+
// params is drawn, or the row the page kept from the previous document
|
|
27984
|
+
// (kept_reads.js): the action starts on the "refresh over a known
|
|
27698
27985
|
// answer" line of data_states.md rather than the first-load one.
|
|
27699
27986
|
provisionalValue:
|
|
27700
27987
|
verb === "GET"
|
|
27701
|
-
? (params) => {
|
|
27702
|
-
const item = untracked(() =>
|
|
27988
|
+
? (params, action) => {
|
|
27989
|
+
const item = untracked(() => {
|
|
27990
|
+
const itemForGet = findItemForGet(params);
|
|
27991
|
+
if (itemForGet) {
|
|
27992
|
+
return itemForGet;
|
|
27993
|
+
}
|
|
27994
|
+
const keptItem = takeKeptRead(
|
|
27995
|
+
getKeptReadKey(action.name, params),
|
|
27996
|
+
);
|
|
27997
|
+
return keptItem ? upsertKeptItem(store, keptItem) : null;
|
|
27998
|
+
});
|
|
27703
27999
|
return item ? item[idKey] : undefined;
|
|
27704
28000
|
}
|
|
27705
28001
|
: undefined,
|
|
28002
|
+
// Another params' answer is another row.
|
|
28003
|
+
inheritData: verb !== "GET",
|
|
27706
28004
|
completeSideEffect: onActionComplete,
|
|
27707
28005
|
});
|
|
28006
|
+
if (verb === "GET") {
|
|
28007
|
+
markKeepableRead(action);
|
|
28008
|
+
}
|
|
28009
|
+
return action;
|
|
27708
28010
|
};
|
|
27709
28011
|
const createActionAffectingManyItems = (
|
|
27710
28012
|
verb,
|
|
@@ -27719,7 +28021,7 @@ const createRestActionFactoryForRoot = (
|
|
|
27719
28021
|
return itemArray.map((item) => item[idKey]);
|
|
27720
28022
|
};
|
|
27721
28023
|
|
|
27722
|
-
|
|
28024
|
+
const action = createAction(callback, {
|
|
27723
28025
|
meta: { verb, isMany: true, paramScope },
|
|
27724
28026
|
name: `${name}.${verb}_MANY`,
|
|
27725
28027
|
resultToValue: applyResultToValue,
|
|
@@ -27727,6 +28029,25 @@ const createRestActionFactoryForRoot = (
|
|
|
27727
28029
|
// undefined so a screen can tell "not asked yet" from "answered: none".
|
|
27728
28030
|
valueToData: (idArray) =>
|
|
27729
28031
|
idArray === undefined ? undefined : store.selectAll(idArray),
|
|
28032
|
+
// The rows the page kept from the previous document (kept_reads.js),
|
|
28033
|
+
// drawn while the request is out. A binding moving to new params still
|
|
28034
|
+
// hands over the previous answer when there are none: a list standing
|
|
28035
|
+
// in for the next one beats a skeleton.
|
|
28036
|
+
provisionalValue:
|
|
28037
|
+
verb === "GET"
|
|
28038
|
+
? (params, action) =>
|
|
28039
|
+
untracked(() => {
|
|
28040
|
+
const keptItems = takeKeptRead(
|
|
28041
|
+
getKeptReadKey(action.name, params),
|
|
28042
|
+
);
|
|
28043
|
+
if (!Array.isArray(keptItems)) {
|
|
28044
|
+
return undefined;
|
|
28045
|
+
}
|
|
28046
|
+
return keptItems.map(
|
|
28047
|
+
(keptItem) => upsertKeptItem(store, keptItem)[idKey],
|
|
28048
|
+
);
|
|
28049
|
+
})
|
|
28050
|
+
: undefined,
|
|
27730
28051
|
completeSideEffect: (actionCompleted) => {
|
|
27731
28052
|
onActionComplete(actionCompleted);
|
|
27732
28053
|
if (
|
|
@@ -27742,6 +28063,10 @@ const createRestActionFactoryForRoot = (
|
|
|
27742
28063
|
return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
|
|
27743
28064
|
},
|
|
27744
28065
|
});
|
|
28066
|
+
if (verb === "GET") {
|
|
28067
|
+
markKeepableRead(action);
|
|
28068
|
+
}
|
|
28069
|
+
return action;
|
|
27745
28070
|
};
|
|
27746
28071
|
|
|
27747
28072
|
return createActionForRoot;
|
|
@@ -27752,10 +28077,12 @@ const createRestActionFactoryForRoot = (
|
|
|
27752
28077
|
// NetworkPolicyError carrying the policy's reason. A read is called or not depending
|
|
27753
28078
|
// on where the policy answers reads from. Answered from the store, a GET of a
|
|
27754
28079
|
// root resource completes with the row the store holds for it (see
|
|
27755
|
-
// findItemInStore)
|
|
27756
|
-
//
|
|
27757
|
-
//
|
|
27758
|
-
//
|
|
28080
|
+
// findItemInStore), and a root GET or GET_MANY holding rows for these params
|
|
28081
|
+
// (the kept page, drawn while the request is out) with those — handing them
|
|
28082
|
+
// back is an upsert without effect, so the action completes with what it had
|
|
28083
|
+
// and nothing is asked. A relationship GET has no row of its own to answer
|
|
28084
|
+
// with and settles with the same error. A completed GET asked to rerun never
|
|
28085
|
+
// gets here (actions.js holds it).
|
|
27759
28086
|
const applyNetworkPolicy = (
|
|
27760
28087
|
restCallback,
|
|
27761
28088
|
{ verb, isMany, findItemInStore },
|
|
@@ -27769,6 +28096,12 @@ const applyNetworkPolicy = (
|
|
|
27769
28096
|
if (!policy.readsFromStore) {
|
|
27770
28097
|
return restCallback(params, context);
|
|
27771
28098
|
}
|
|
28099
|
+
if (isKeepableRead(context.action)) {
|
|
28100
|
+
const dataHeld = context.action.dataSignal.peek();
|
|
28101
|
+
if (dataHeld !== undefined) {
|
|
28102
|
+
return dataHeld;
|
|
28103
|
+
}
|
|
28104
|
+
}
|
|
27772
28105
|
if (!isMany && findItemInStore) {
|
|
27773
28106
|
const item = findItemInStore(params, context.action);
|
|
27774
28107
|
if (item) {
|
|
@@ -37919,6 +38252,46 @@ const writeCover = (edge, element, size) => {
|
|
|
37919
38252
|
style.setProperty(property, value);
|
|
37920
38253
|
};
|
|
37921
38254
|
|
|
38255
|
+
// Every route action, with whether it asks something of the address at the
|
|
38256
|
+
// moment: its route matches and its params getter returns something.
|
|
38257
|
+
const routeActionEntriesSignal = signal([]);
|
|
38258
|
+
// What a route action is called in the page kept for the next document (see
|
|
38259
|
+
// keepPageOnScreen): its place among the route actions declared, which the
|
|
38260
|
+
// next document of the same build declares in the same order. A resource read
|
|
38261
|
+
// has a name of its own and is kept by it; an action made from a callback is
|
|
38262
|
+
// most often "anonymous", and several of them read the same page.
|
|
38263
|
+
const keptNameWeakMap = new WeakMap();
|
|
38264
|
+
const getRouteActionKeptName = (routeAction) => {
|
|
38265
|
+
return keptNameWeakMap.get(routeAction);
|
|
38266
|
+
};
|
|
38267
|
+
let activeRouteActionsPrevious = new Set();
|
|
38268
|
+
/**
|
|
38269
|
+
* The route actions asking something for the page on screen: their route
|
|
38270
|
+
* matches and their params getter returns something — the running ones, not
|
|
38271
|
+
* every matching one, so an action bound to every page that asks only when a
|
|
38272
|
+
* popup is open over it counts while it is open. Read to rerun what the page
|
|
38273
|
+
* reads (the app back from the background), and by `keepPageOnScreen`.
|
|
38274
|
+
* The set is handed out again only when its members change.
|
|
38275
|
+
*
|
|
38276
|
+
* A page's code is declared as a route action too; `action.meta.verb` says
|
|
38277
|
+
* which ones read data.
|
|
38278
|
+
*
|
|
38279
|
+
* @type {import("@preact/signals").ReadonlySignal<Set<object>>}
|
|
38280
|
+
*/
|
|
38281
|
+
const activeRouteActionsSignal = computed(() => {
|
|
38282
|
+
const active = new Set();
|
|
38283
|
+
for (const { routeAction, askingSignal } of routeActionEntriesSignal.value) {
|
|
38284
|
+
if (askingSignal.value) {
|
|
38285
|
+
active.add(routeAction);
|
|
38286
|
+
}
|
|
38287
|
+
}
|
|
38288
|
+
if (haveSameMembers(active, activeRouteActionsPrevious)) {
|
|
38289
|
+
return activeRouteActionsPrevious;
|
|
38290
|
+
}
|
|
38291
|
+
activeRouteActionsPrevious = active;
|
|
38292
|
+
return active;
|
|
38293
|
+
});
|
|
38294
|
+
|
|
37922
38295
|
/**
|
|
37923
38296
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
37924
38297
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
@@ -37975,7 +38348,23 @@ const routeAction = (
|
|
|
37975
38348
|
}
|
|
37976
38349
|
return params;
|
|
37977
38350
|
};
|
|
38351
|
+
const rootAction = rootActionOf(action);
|
|
38352
|
+
const keptName = `route_action#${routeActionEntriesSignal.peek().length} ${rootAction.name}`;
|
|
38353
|
+
if (!isKeepableRead(rootAction)) {
|
|
38354
|
+
// Before the effect below: it may run the action at once.
|
|
38355
|
+
addKeptValueSource(rootAction, (params) =>
|
|
38356
|
+
takeKeptRead(getKeptReadKey(keptName, params)),
|
|
38357
|
+
);
|
|
38358
|
+
}
|
|
37978
38359
|
const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
|
|
38360
|
+
keptNameWeakMap.set(actionBoundToRoute, keptName);
|
|
38361
|
+
routeActionEntriesSignal.value = [
|
|
38362
|
+
...routeActionEntriesSignal.peek(),
|
|
38363
|
+
{
|
|
38364
|
+
routeAction: actionBoundToRoute,
|
|
38365
|
+
askingSignal: computed(() => Boolean(readParamsAsked())),
|
|
38366
|
+
},
|
|
38367
|
+
];
|
|
37979
38368
|
for (const route of routes) {
|
|
37980
38369
|
// Asked of the route's params as they are, like the effect asks them.
|
|
37981
38370
|
registerRouteReload(route, () =>
|
|
@@ -38059,6 +38448,416 @@ const anyMatchingRouteSignal = (routes) => {
|
|
|
38059
38448
|
return anyMatchingSignal;
|
|
38060
38449
|
};
|
|
38061
38450
|
|
|
38451
|
+
const rootActionOf = (action) => {
|
|
38452
|
+
if (action.isProxy) {
|
|
38453
|
+
return rootActionOf(getActionPrivateProperties(action).currentAction);
|
|
38454
|
+
}
|
|
38455
|
+
return action.rootAction || action;
|
|
38456
|
+
};
|
|
38457
|
+
|
|
38458
|
+
const haveSameMembers = (setA, setB) => {
|
|
38459
|
+
if (setA.size !== setB.size) {
|
|
38460
|
+
return false;
|
|
38461
|
+
}
|
|
38462
|
+
for (const value of setA) {
|
|
38463
|
+
if (!setB.has(value)) {
|
|
38464
|
+
return false;
|
|
38465
|
+
}
|
|
38466
|
+
}
|
|
38467
|
+
return true;
|
|
38468
|
+
};
|
|
38469
|
+
|
|
38470
|
+
/*
|
|
38471
|
+
* The page on screen, kept for the next document: what its reads answered,
|
|
38472
|
+
* written into a signal the app hands over, and drawn again by the same reads
|
|
38473
|
+
* in a document opening on the same page (a reload, a tab the system
|
|
38474
|
+
* discarded). The read side is state/rest/kept_reads.js; this is what decides
|
|
38475
|
+
* which reads are the page, and when the slot is written.
|
|
38476
|
+
*
|
|
38477
|
+
* The unit is the page, not the resource. One resource is read by several
|
|
38478
|
+
* screens — a search, someone's games, a game opened from anywhere — and kept
|
|
38479
|
+
* per resource it would keep every row ever opened. The page's reads are what
|
|
38480
|
+
* navi already runs for it: the route actions asking something for the page
|
|
38481
|
+
* (activeRouteActionsSignal) and the compositions the lists on screen read
|
|
38482
|
+
* (shownRangeReadersSignal). A search typed on the page is neither, and is not
|
|
38483
|
+
* kept.
|
|
38484
|
+
*
|
|
38485
|
+
* What a read is made of on the way out depends on what it holds. A resource
|
|
38486
|
+
* read holds ids, which name rows a new document's store does not have: its
|
|
38487
|
+
* rows are written, and re-enter the store. Any other route action holds its
|
|
38488
|
+
* value, written as it is when JSON brings it back as it was — a page's code
|
|
38489
|
+
* (a function), a Date built by the callback, a Map, are not, and are left
|
|
38490
|
+
* out. Such an action is recognized in the next document by its place among
|
|
38491
|
+
* the route actions declared (see getRouteActionKeptName), not by a name the
|
|
38492
|
+
* app would have to give every action made from a callback.
|
|
38493
|
+
*
|
|
38494
|
+
* A read is written once it has answered in this document. Before that it
|
|
38495
|
+
* keeps what the previous document kept for it, never what it shows meanwhile:
|
|
38496
|
+
* an answer handed over from other params (a list standing in for the next
|
|
38497
|
+
* one) says nothing about these.
|
|
38498
|
+
*
|
|
38499
|
+
* The write waits for the page to settle and is flushed when the document is
|
|
38500
|
+
* hidden. A navigation goes through pages half there — the route matched, its
|
|
38501
|
+
* list not mounted yet, the page left still drawn under a transition — and
|
|
38502
|
+
* each would otherwise be written out in full. Hidden is the moment the system
|
|
38503
|
+
* may discard the document, so nothing is lost by waiting until then.
|
|
38504
|
+
*
|
|
38505
|
+
* The signal turning `undefined` from outside (sign-out) forgets the page, and
|
|
38506
|
+
* nothing is written until a read lands again: written at once, the page still
|
|
38507
|
+
* on screen would put back the account just left.
|
|
38508
|
+
*/
|
|
38509
|
+
|
|
38510
|
+
const WRITE_DELAY = 1000;
|
|
38511
|
+
const NOTHING_PENDING = {};
|
|
38512
|
+
let stopKeepingCurrent = null;
|
|
38513
|
+
|
|
38514
|
+
/**
|
|
38515
|
+
* Keeps what the page on screen reads, so that a new document opening on it —
|
|
38516
|
+
* a reload, a pull-to-refresh, a tab the system discarded in the background —
|
|
38517
|
+
* draws it again while its reads go out, rather than skeletons.
|
|
38518
|
+
*
|
|
38519
|
+
* What the page reads is what navi runs for it: the route actions asking
|
|
38520
|
+
* something for it (see `activeRouteActionsSignal`), and the `GET_RANGE`
|
|
38521
|
+
* compositions the `<List.Items>` on screen read, around the window they draw.
|
|
38522
|
+
* A resource read keeps its rows; any other route action keeps its value when
|
|
38523
|
+
* JSON brings it back as it was. A navigation replaces the slot with the next
|
|
38524
|
+
* page's reads, and the page left is dropped. In the next document, the first
|
|
38525
|
+
* run of the same read with the same params draws the kept answer as its
|
|
38526
|
+
* provisional value: `data` set while `loading` is `true`, the answer replacing
|
|
38527
|
+
* it. Under a network policy answering reads from the store, a resource read
|
|
38528
|
+
* completes with it. The copy follows the store: a `PUT` on a row rewrites it.
|
|
38529
|
+
*
|
|
38530
|
+
* Call it before the routes start: the first runs are the ones that look. And
|
|
38531
|
+
* declare the route actions where the app starts: one that is not a resource
|
|
38532
|
+
* read is recognized by its place among them.
|
|
38533
|
+
*
|
|
38534
|
+
* @param {object} options
|
|
38535
|
+
* @param {import("@preact/signals").Signal} options.signal - where the slot is
|
|
38536
|
+
* written: a `stateSignal` with `persists: true` and `type: "object"`, its
|
|
38537
|
+
* `id` holding whatever makes a kept page unusable (the deployed version).
|
|
38538
|
+
* Writing `undefined` into it (sign-out) forgets the page, and nothing is
|
|
38539
|
+
* written again until a read lands.
|
|
38540
|
+
* @param {() => boolean} [options.when] - read whenever the slot is about to be
|
|
38541
|
+
* read or written: `false` reads nothing, writes nothing, and empties the
|
|
38542
|
+
* signal (a tab viewing as someone else). A function, so it can read a signal.
|
|
38543
|
+
* @param {object[]} [options.always] - routes whose reads stay kept once the
|
|
38544
|
+
* page is left, until it is visited again: the page an app opens on when it
|
|
38545
|
+
* is launched rather than reloaded.
|
|
38546
|
+
* @returns {() => void} stops keeping the page.
|
|
38547
|
+
* @see docs/resource.md — what is kept and what is not, what stays the app's
|
|
38548
|
+
*/
|
|
38549
|
+
const keepPageOnScreen = ({
|
|
38550
|
+
signal: keptSignal,
|
|
38551
|
+
when,
|
|
38552
|
+
always = [],
|
|
38553
|
+
} = {}) => {
|
|
38554
|
+
if (!isSignal(keptSignal)) {
|
|
38555
|
+
throw new TypeError(
|
|
38556
|
+
`keepPageOnScreen() needs a signal to write the page into, received ${keptSignal}`,
|
|
38557
|
+
);
|
|
38558
|
+
}
|
|
38559
|
+
if (stopKeepingCurrent) {
|
|
38560
|
+
stopKeepingCurrent();
|
|
38561
|
+
}
|
|
38562
|
+
const isAllowed = () => {
|
|
38563
|
+
return when ? Boolean(when()) : true;
|
|
38564
|
+
};
|
|
38565
|
+
const alwaysMatchingSignal = anyMatchingRouteSignal(always);
|
|
38566
|
+
|
|
38567
|
+
// What the signal held when it was read: the previous document's page.
|
|
38568
|
+
let loadedEntryMap = new Map();
|
|
38569
|
+
let loadedAlwaysKeys = [];
|
|
38570
|
+
// Which reads answered in this document (since the last forget): only
|
|
38571
|
+
// those write what they show.
|
|
38572
|
+
let answeredActionWeakSet = new WeakSet();
|
|
38573
|
+
let answeredRangeKeySet = new Set();
|
|
38574
|
+
// The state each action was last seen in, and the last answer seen of each
|
|
38575
|
+
// range reader: a read lands when they move, and not because the slot looks
|
|
38576
|
+
// again at something that had answered before a forget.
|
|
38577
|
+
const stateSeenWeakMap = new WeakMap();
|
|
38578
|
+
const answeredAtSeenWeakMap = new WeakMap();
|
|
38579
|
+
// Forgotten: nothing is written until a read lands.
|
|
38580
|
+
let paused = false;
|
|
38581
|
+
// The reads of the always-kept page, as they were the last time it was on
|
|
38582
|
+
// screen in this document; null until then.
|
|
38583
|
+
let alwaysReads = null;
|
|
38584
|
+
|
|
38585
|
+
const forget = () => {
|
|
38586
|
+
loadedEntryMap = new Map();
|
|
38587
|
+
loadedAlwaysKeys = [];
|
|
38588
|
+
answeredActionWeakSet = new WeakSet();
|
|
38589
|
+
answeredRangeKeySet = new Set();
|
|
38590
|
+
alwaysReads = null;
|
|
38591
|
+
paused = true;
|
|
38592
|
+
withdrawKeptReads();
|
|
38593
|
+
};
|
|
38594
|
+
|
|
38595
|
+
let lastWritten;
|
|
38596
|
+
let pendingWrite = NOTHING_PENDING;
|
|
38597
|
+
let writeTimeout;
|
|
38598
|
+
const flush = () => {
|
|
38599
|
+
clearTimeout(writeTimeout);
|
|
38600
|
+
if (pendingWrite === NOTHING_PENDING) {
|
|
38601
|
+
return;
|
|
38602
|
+
}
|
|
38603
|
+
const value = pendingWrite;
|
|
38604
|
+
pendingWrite = NOTHING_PENDING;
|
|
38605
|
+
if (compareTwoJsValues(value, lastWritten)) {
|
|
38606
|
+
return;
|
|
38607
|
+
}
|
|
38608
|
+
lastWritten = value;
|
|
38609
|
+
keptSignal.value = value;
|
|
38610
|
+
};
|
|
38611
|
+
const scheduleWrite = (value) => {
|
|
38612
|
+
pendingWrite = value;
|
|
38613
|
+
clearTimeout(writeTimeout);
|
|
38614
|
+
writeTimeout = setTimeout(flush, WRITE_DELAY);
|
|
38615
|
+
};
|
|
38616
|
+
const onVisibilityChange = () => {
|
|
38617
|
+
if (document.visibilityState === "hidden") {
|
|
38618
|
+
flush();
|
|
38619
|
+
}
|
|
38620
|
+
};
|
|
38621
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
38622
|
+
window.addEventListener("pagehide", flush);
|
|
38623
|
+
|
|
38624
|
+
// Runs at once with what the signal already holds (the slot read from
|
|
38625
|
+
// storage), then on every write — the slot's own ones are recognized and
|
|
38626
|
+
// left alone.
|
|
38627
|
+
const unsubscribe = keptSignal.subscribe((value) => {
|
|
38628
|
+
if (value === lastWritten) {
|
|
38629
|
+
return;
|
|
38630
|
+
}
|
|
38631
|
+
lastWritten = value;
|
|
38632
|
+
pendingWrite = NOTHING_PENDING;
|
|
38633
|
+
clearTimeout(writeTimeout);
|
|
38634
|
+
if (
|
|
38635
|
+
value === undefined ||
|
|
38636
|
+
value === null ||
|
|
38637
|
+
typeof value !== "object" ||
|
|
38638
|
+
!isAllowed()
|
|
38639
|
+
) {
|
|
38640
|
+
forget();
|
|
38641
|
+
return;
|
|
38642
|
+
}
|
|
38643
|
+
const entries =
|
|
38644
|
+
value.entries && typeof value.entries === "object" ? value.entries : {};
|
|
38645
|
+
loadedEntryMap = new Map(Object.entries(entries));
|
|
38646
|
+
loadedAlwaysKeys = Array.isArray(value.always)
|
|
38647
|
+
? value.always.filter((key) => loadedEntryMap.has(key))
|
|
38648
|
+
: [];
|
|
38649
|
+
alwaysReads = null;
|
|
38650
|
+
paused = false;
|
|
38651
|
+
offerKeptReads(loadedEntryMap);
|
|
38652
|
+
});
|
|
38653
|
+
|
|
38654
|
+
const actionReadWeakMap = new WeakMap();
|
|
38655
|
+
const actionReadOf = (action, keptName, readEntryOf) => {
|
|
38656
|
+
let read = actionReadWeakMap.get(action);
|
|
38657
|
+
if (read) {
|
|
38658
|
+
return read;
|
|
38659
|
+
}
|
|
38660
|
+
read = () => {
|
|
38661
|
+
const state = action.runningStateSignal.value;
|
|
38662
|
+
const stateSeen = stateSeenWeakMap.get(action);
|
|
38663
|
+
stateSeenWeakMap.set(action, state);
|
|
38664
|
+
const landed = state === COMPLETED && stateSeen !== COMPLETED;
|
|
38665
|
+
if (landed) {
|
|
38666
|
+
answeredActionWeakSet.add(action);
|
|
38667
|
+
}
|
|
38668
|
+
return {
|
|
38669
|
+
key: getKeptReadKey(keptName, action.params),
|
|
38670
|
+
answered: answeredActionWeakSet.has(action),
|
|
38671
|
+
landed,
|
|
38672
|
+
readEntry: () => readEntryOf(action),
|
|
38673
|
+
};
|
|
38674
|
+
};
|
|
38675
|
+
actionReadWeakMap.set(action, read);
|
|
38676
|
+
return read;
|
|
38677
|
+
};
|
|
38678
|
+
const rangeReadWeakMap = new WeakMap();
|
|
38679
|
+
const rangeReadOf = (reader) => {
|
|
38680
|
+
let read = rangeReadWeakMap.get(reader);
|
|
38681
|
+
if (read) {
|
|
38682
|
+
return read;
|
|
38683
|
+
}
|
|
38684
|
+
read = () => {
|
|
38685
|
+
const { key, answeredAt, readEntry } = reader.readKept();
|
|
38686
|
+
const answeredAtSeen = answeredAtSeenWeakMap.get(reader) || 0;
|
|
38687
|
+
answeredAtSeenWeakMap.set(reader, answeredAt);
|
|
38688
|
+
const landed = answeredAt > answeredAtSeen;
|
|
38689
|
+
if (landed) {
|
|
38690
|
+
answeredRangeKeySet.add(key);
|
|
38691
|
+
}
|
|
38692
|
+
return {
|
|
38693
|
+
key,
|
|
38694
|
+
answered: answeredRangeKeySet.has(key),
|
|
38695
|
+
landed,
|
|
38696
|
+
readEntry,
|
|
38697
|
+
};
|
|
38698
|
+
};
|
|
38699
|
+
rangeReadWeakMap.set(reader, read);
|
|
38700
|
+
return read;
|
|
38701
|
+
};
|
|
38702
|
+
const readsOnScreen = () => {
|
|
38703
|
+
const reads = [];
|
|
38704
|
+
for (const routeAction of activeRouteActionsSignal.value) {
|
|
38705
|
+
// The instance is what is kept, so that a page left keeps what it read.
|
|
38706
|
+
// The binding's callSource names that instance and is rewritten once the
|
|
38707
|
+
// binding has moved to another one: read, it brings the slot along.
|
|
38708
|
+
// eslint-disable-next-line no-unused-expressions
|
|
38709
|
+
routeAction.callSource;
|
|
38710
|
+
const action = getActionPrivateProperties(routeAction).currentAction;
|
|
38711
|
+
if (action.params === NO_PARAMS) {
|
|
38712
|
+
continue;
|
|
38713
|
+
}
|
|
38714
|
+
if (isKeepableRead(action)) {
|
|
38715
|
+
reads.push(actionReadOf(action, action.name, readRows));
|
|
38716
|
+
} else {
|
|
38717
|
+
reads.push(
|
|
38718
|
+
actionReadOf(
|
|
38719
|
+
action,
|
|
38720
|
+
getRouteActionKeptName(routeAction),
|
|
38721
|
+
readPlainValue,
|
|
38722
|
+
),
|
|
38723
|
+
);
|
|
38724
|
+
}
|
|
38725
|
+
}
|
|
38726
|
+
for (const reader of shownRangeReadersSignal.value.keys()) {
|
|
38727
|
+
reads.push(rangeReadOf(reader));
|
|
38728
|
+
}
|
|
38729
|
+
return reads;
|
|
38730
|
+
};
|
|
38731
|
+
|
|
38732
|
+
const stopEffect = effect(() => {
|
|
38733
|
+
const allowed = isAllowed();
|
|
38734
|
+
const routed = routedSignal.value;
|
|
38735
|
+
const alwaysMatching = alwaysMatchingSignal.value;
|
|
38736
|
+
if (!allowed) {
|
|
38737
|
+
forget();
|
|
38738
|
+
pendingWrite = undefined;
|
|
38739
|
+
flush();
|
|
38740
|
+
return;
|
|
38741
|
+
}
|
|
38742
|
+
if (!routed) {
|
|
38743
|
+
// No page yet: the slot is what the first reads will draw.
|
|
38744
|
+
return;
|
|
38745
|
+
}
|
|
38746
|
+
const screenReads = readsOnScreen();
|
|
38747
|
+
if (alwaysMatching) {
|
|
38748
|
+
alwaysReads = screenReads;
|
|
38749
|
+
}
|
|
38750
|
+
const entries = {};
|
|
38751
|
+
let landed = false;
|
|
38752
|
+
const collect = (reads) => {
|
|
38753
|
+
const keys = [];
|
|
38754
|
+
for (const read of reads) {
|
|
38755
|
+
const result = read();
|
|
38756
|
+
if (result.landed) {
|
|
38757
|
+
landed = true;
|
|
38758
|
+
}
|
|
38759
|
+
const entry = result.answered
|
|
38760
|
+
? result.readEntry()
|
|
38761
|
+
: loadedEntryMap.get(result.key);
|
|
38762
|
+
if (entry === undefined) {
|
|
38763
|
+
continue;
|
|
38764
|
+
}
|
|
38765
|
+
entries[result.key] = entry;
|
|
38766
|
+
keys.push(result.key);
|
|
38767
|
+
}
|
|
38768
|
+
return keys;
|
|
38769
|
+
};
|
|
38770
|
+
const screenKeys = collect(screenReads);
|
|
38771
|
+
let alwaysKeys;
|
|
38772
|
+
if (alwaysReads === screenReads) {
|
|
38773
|
+
alwaysKeys = screenKeys;
|
|
38774
|
+
} else if (alwaysReads) {
|
|
38775
|
+
alwaysKeys = collect(alwaysReads);
|
|
38776
|
+
} else {
|
|
38777
|
+
alwaysKeys = loadedAlwaysKeys;
|
|
38778
|
+
for (const key of alwaysKeys) {
|
|
38779
|
+
entries[key] = loadedEntryMap.get(key);
|
|
38780
|
+
}
|
|
38781
|
+
}
|
|
38782
|
+
if (paused) {
|
|
38783
|
+
if (!landed) {
|
|
38784
|
+
return;
|
|
38785
|
+
}
|
|
38786
|
+
paused = false;
|
|
38787
|
+
}
|
|
38788
|
+
if (screenKeys.length === 0 && alwaysKeys.length === 0) {
|
|
38789
|
+
scheduleWrite(undefined);
|
|
38790
|
+
return;
|
|
38791
|
+
}
|
|
38792
|
+
scheduleWrite({ entries, screen: screenKeys, always: alwaysKeys });
|
|
38793
|
+
});
|
|
38794
|
+
|
|
38795
|
+
const stop = () => {
|
|
38796
|
+
flush();
|
|
38797
|
+
stopEffect();
|
|
38798
|
+
unsubscribe();
|
|
38799
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
38800
|
+
window.removeEventListener("pagehide", flush);
|
|
38801
|
+
withdrawKeptReads();
|
|
38802
|
+
if (stopKeepingCurrent === stop) {
|
|
38803
|
+
stopKeepingCurrent = null;
|
|
38804
|
+
}
|
|
38805
|
+
};
|
|
38806
|
+
stopKeepingCurrent = stop;
|
|
38807
|
+
return stop;
|
|
38808
|
+
};
|
|
38809
|
+
|
|
38810
|
+
const readRows = (action) => {
|
|
38811
|
+
const data = action.dataSignal.value;
|
|
38812
|
+
if (data === undefined || data === null) {
|
|
38813
|
+
return undefined;
|
|
38814
|
+
}
|
|
38815
|
+
return serializeData(data);
|
|
38816
|
+
};
|
|
38817
|
+
const readPlainValue = (action) => {
|
|
38818
|
+
const value = action.valueSignal.value;
|
|
38819
|
+
if (value === undefined || !isPlainData(value, new Set())) {
|
|
38820
|
+
return undefined;
|
|
38821
|
+
}
|
|
38822
|
+
return value;
|
|
38823
|
+
};
|
|
38824
|
+
// What JSON writes and reads back as it was. `undefined` inside is accepted:
|
|
38825
|
+
// JSON drops the key, which reads the same.
|
|
38826
|
+
const isPlainData = (value, ancestorSet) => {
|
|
38827
|
+
if (value === null || value === undefined) {
|
|
38828
|
+
return true;
|
|
38829
|
+
}
|
|
38830
|
+
const type = typeof value;
|
|
38831
|
+
if (type === "string" || type === "boolean") {
|
|
38832
|
+
return true;
|
|
38833
|
+
}
|
|
38834
|
+
if (type === "number") {
|
|
38835
|
+
return Number.isFinite(value);
|
|
38836
|
+
}
|
|
38837
|
+
if (type !== "object") {
|
|
38838
|
+
return false;
|
|
38839
|
+
}
|
|
38840
|
+
if (ancestorSet.has(value)) {
|
|
38841
|
+
return false;
|
|
38842
|
+
}
|
|
38843
|
+
const isArray = Array.isArray(value);
|
|
38844
|
+
if (!isArray) {
|
|
38845
|
+
const proto = Object.getPrototypeOf(value);
|
|
38846
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
38847
|
+
return false;
|
|
38848
|
+
}
|
|
38849
|
+
}
|
|
38850
|
+
ancestorSet.add(value);
|
|
38851
|
+
const entries = isArray ? value : Object.values(value);
|
|
38852
|
+
for (const entry of entries) {
|
|
38853
|
+
if (!isPlainData(entry, ancestorSet)) {
|
|
38854
|
+
return false;
|
|
38855
|
+
}
|
|
38856
|
+
}
|
|
38857
|
+
ancestorSet.delete(value);
|
|
38858
|
+
return true;
|
|
38859
|
+
};
|
|
38860
|
+
|
|
38062
38861
|
/**
|
|
38063
38862
|
* A popup that is the anchor, brought to the front.
|
|
38064
38863
|
*
|
|
@@ -65131,7 +65930,7 @@ const css$E = /* css */`
|
|
|
65131
65930
|
/* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
|
|
65132
65931
|
by default until .show()/.showModal() adds [open]) — needs its own
|
|
65133
65932
|
starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
|
|
65134
|
-
own backdropProps (recomputed from openController.
|
|
65933
|
+
own backdropProps (recomputed from openController.openedInDom on every
|
|
65135
65934
|
render, present from the very first one), then toggled by plain
|
|
65136
65935
|
removeAttribute/setAttribute in openEffect/close, never an explicit
|
|
65137
65936
|
display override — removing the attribute just lets this rule stop
|
|
@@ -66732,7 +67531,7 @@ const useDialogProps = props => {
|
|
|
66732
67531
|
"navi-out-of-flow": "",
|
|
66733
67532
|
"baseClassName": "navi_dialog_backdrop",
|
|
66734
67533
|
"aria-hidden": "true",
|
|
66735
|
-
// Recomputed fresh on every render from openController.
|
|
67534
|
+
// Recomputed fresh on every render from openController.openedInDom (not
|
|
66736
67535
|
// driven through a mount-time layout effect, unlike this file's own
|
|
66737
67536
|
// imperative open/close toggling below) — present in the DOM
|
|
66738
67537
|
// synchronously from the very first commit, matching this file's own
|
|
@@ -66742,7 +67541,7 @@ const useDialogProps = props => {
|
|
|
66742
67541
|
// needs — see popover.jsx's own identical prop for the full reasoning.
|
|
66743
67542
|
"aria-expanded": openController.openedInDom ? "true" : "false",
|
|
66744
67543
|
// Present from this very first render (recomputed fresh on every one
|
|
66745
|
-
// from openController.
|
|
67544
|
+
// from openController.openedInDom, not a frozen mount-time constant) so
|
|
66746
67545
|
// there's no gap for the browser to ever paint this plain-div backdrop
|
|
66747
67546
|
// visible before anything has actually opened it — see popover.jsx's
|
|
66748
67547
|
// own identical prop for the full reasoning, and this file's own CSS
|
|
@@ -66762,7 +67561,7 @@ const useDialogProps = props => {
|
|
|
66762
67561
|
// (kept once, not repeated here).
|
|
66763
67562
|
"aria-expanded": openController.openedInDom ? "true" : "false",
|
|
66764
67563
|
// Present from the very first render (recomputed fresh from
|
|
66765
|
-
// openController.
|
|
67564
|
+
// openController.openedInDom every time, not a frozen mount-time constant —
|
|
66766
67565
|
// see popover.jsx's own identical prop for the full reasoning) so a
|
|
66767
67566
|
// consumer whose own CSS also sets display (e.g. Popup's flex prop)
|
|
66768
67567
|
// can't silently defeat showModal()/close()'s native open/close — see
|
|
@@ -67138,7 +67937,7 @@ const css$D = /* css */`
|
|
|
67138
67937
|
without this it would flash visible for one frame on mount, before
|
|
67139
67938
|
openEffect's own JS ever gets a chance to hide it. [navi-hidden] is
|
|
67140
67939
|
set from usePopoverProps' own contentProps (recomputed from
|
|
67141
|
-
openController.
|
|
67940
|
+
openController.openedInDom on every render, present from the very first
|
|
67142
67941
|
one — see there for why), then toggled by plain
|
|
67143
67942
|
removeAttribute/setAttribute in openEffect/close, never an explicit
|
|
67144
67943
|
display override: removing the attribute just lets this rule stop
|
|
@@ -68494,7 +69293,7 @@ const usePopoverProps = props => {
|
|
|
68494
69293
|
"navi-out-of-flow": "",
|
|
68495
69294
|
"baseClassName": "navi_popover_backdrop",
|
|
68496
69295
|
"aria-hidden": "true",
|
|
68497
|
-
// Recomputed fresh on every render from openController.
|
|
69296
|
+
// Recomputed fresh on every render from openController.openedInDom (not a
|
|
68498
69297
|
// frozen mount-time constant, same pattern as navi-hidden just below)
|
|
68499
69298
|
// rather than driven through a mount-time layout effect — its own
|
|
68500
69299
|
// actual open/close toggling is still done entirely imperatively
|
|
@@ -68569,7 +69368,7 @@ const usePopoverProps = props => {
|
|
|
68569
69368
|
// starting-hidden default) — present from this very first render so
|
|
68570
69369
|
// there's no gap for the browser to ever paint it visible before
|
|
68571
69370
|
// anything has actually opened it. Recomputed fresh on every render from
|
|
68572
|
-
// openController.
|
|
69371
|
+
// openController.openedInDom (not a frozen mount-time constant) — Preact
|
|
68573
69372
|
// only touches the DOM for a prop whose value actually changed since
|
|
68574
69373
|
// the last render, so as long as this always reflects the *current*
|
|
68575
69374
|
// truth, it never fights the imperative
|
|
@@ -72995,7 +73794,6 @@ const ListUI = props => {
|
|
|
72995
73794
|
// What the runs ask for and stand for, in items: a source paginates in items,
|
|
72996
73795
|
// and asks for the first picture's items and the rest in one round trip.
|
|
72997
73796
|
listItems.pageSize = PAGE_SIZE_DEFAULT;
|
|
72998
|
-
listItems.scrolled = scrolled ?? defaultScrolled;
|
|
72999
73797
|
const {
|
|
73000
73798
|
virtualItemSizeSignal,
|
|
73001
73799
|
renderWindow,
|
|
@@ -73335,6 +74133,45 @@ const useListScrollSync = ({
|
|
|
73335
74133
|
columns
|
|
73336
74134
|
}) => {
|
|
73337
74135
|
const debugScroll = useDebugScroll();
|
|
74136
|
+
// Where the list must be: what the caller holds it at (`scrolled`), or where
|
|
74137
|
+
// it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
|
|
74138
|
+
// the caller moving the list, so the hold is armed again — that is what makes
|
|
74139
|
+
// it controlled. A `defaultScrolled` that changes is where the list opens,
|
|
74140
|
+
// read again from something that answered again (a count fresher than the
|
|
74141
|
+
// one kept from the previous visit): followed for as long as nobody has
|
|
74142
|
+
// moved the list, and compared by value, since it is said at every render.
|
|
74143
|
+
// Nothing to hold it at (a position not saved yet) is not a position: the
|
|
74144
|
+
// list opens where it opens.
|
|
74145
|
+
const startPlaceRef = useRef(null);
|
|
74146
|
+
if (startPlaceRef.current === null) {
|
|
74147
|
+
startPlaceRef.current = {
|
|
74148
|
+
userTookOver: false,
|
|
74149
|
+
wanted: scrolled ?? defaultScrolled,
|
|
74150
|
+
scrolled,
|
|
74151
|
+
defaultScrolled,
|
|
74152
|
+
way: null,
|
|
74153
|
+
arrived: false
|
|
74154
|
+
};
|
|
74155
|
+
} else {
|
|
74156
|
+
const place = startPlaceRef.current;
|
|
74157
|
+
if (place.scrolled !== scrolled) {
|
|
74158
|
+
place.scrolled = scrolled;
|
|
74159
|
+
place.defaultScrolled = defaultScrolled;
|
|
74160
|
+
place.wanted = scrolled ?? defaultScrolled;
|
|
74161
|
+
place.userTookOver = false;
|
|
74162
|
+
} else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
|
|
74163
|
+
place.defaultScrolled = defaultScrolled;
|
|
74164
|
+
if (!place.userTookOver) {
|
|
74165
|
+
place.wanted = defaultScrolled;
|
|
74166
|
+
// Placed again from where it stands: the way to the previous place
|
|
74167
|
+
// says nothing about whose the next scrolls are.
|
|
74168
|
+
place.way = null;
|
|
74169
|
+
place.arrived = false;
|
|
74170
|
+
}
|
|
74171
|
+
}
|
|
74172
|
+
}
|
|
74173
|
+
const scrolledWanted = startPlaceRef.current.wanted;
|
|
74174
|
+
listItems.scrolled = scrolledWanted;
|
|
73338
74175
|
// The items drawn, [start, end) among the list's own. A ref as well as a
|
|
73339
74176
|
// state: the render moves it where the list is held (holdWindow) without a
|
|
73340
74177
|
// commit of its own.
|
|
@@ -73349,7 +74186,7 @@ const useListScrollSync = ({
|
|
|
73349
74186
|
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
|
|
73350
74187
|
inLines: Boolean(columns),
|
|
73351
74188
|
windowLeavesItemsOut,
|
|
73352
|
-
scrolledWanted
|
|
74189
|
+
scrolledWanted,
|
|
73353
74190
|
// The fillers hold the room of items above the screen at this size: a
|
|
73354
74191
|
// size that changes moves what is on screen, like items landing above it.
|
|
73355
74192
|
beforeSizeChange: () => listItems.captureAnchor()
|
|
@@ -73680,25 +74517,7 @@ const useListScrollSync = ({
|
|
|
73680
74517
|
const newEnd = newStart + windowSize;
|
|
73681
74518
|
updateRenderWindow(newStart, newEnd);
|
|
73682
74519
|
};
|
|
73683
|
-
|
|
73684
|
-
// Where the list must be: what the caller holds it at (`scrolled`), or where
|
|
73685
|
-
// it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
|
|
73686
|
-
// the caller moving the list, so the hold is armed again — that is what makes
|
|
73687
|
-
// it controlled.
|
|
73688
|
-
// Nothing to hold it at (a position not saved yet) is not a position: the
|
|
73689
|
-
// list opens where it opens.
|
|
73690
|
-
const scrolledWanted = scrolled ?? defaultScrolled;
|
|
73691
74520
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
73692
|
-
const startPlaceRef = useRef({
|
|
73693
|
-
userTookOver: false,
|
|
73694
|
-
wanted: scrolledWanted,
|
|
73695
|
-
way: null,
|
|
73696
|
-
arrived: false
|
|
73697
|
-
});
|
|
73698
|
-
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
73699
|
-
startPlaceRef.current.wanted = scrolledWanted;
|
|
73700
|
-
startPlaceRef.current.userTookOver = false;
|
|
73701
|
-
}
|
|
73702
74521
|
// Only one thing owns the scroll at a time: while the list is holding itself
|
|
73703
74522
|
// somewhere, the anchoring stays out of it (holding an item still is precisely
|
|
73704
74523
|
// not being at the end anymore once what is above it shrinks).
|
|
@@ -77418,6 +78237,17 @@ const useItemStore = ({
|
|
|
77418
78237
|
}
|
|
77419
78238
|
const pages = pagesRef.current;
|
|
77420
78239
|
const [, setPageVersion] = useState(0);
|
|
78240
|
+
// A list on screen is part of what the page reads: the composition it reads
|
|
78241
|
+
// is kept with the page for the next document (see keepPageOnScreen).
|
|
78242
|
+
useLayoutEffect(() => {
|
|
78243
|
+
if (inMemory || typeof itemsAction !== "function") {
|
|
78244
|
+
return undefined;
|
|
78245
|
+
}
|
|
78246
|
+
if (!itemsAction.isRangeReader) {
|
|
78247
|
+
return undefined;
|
|
78248
|
+
}
|
|
78249
|
+
return showRangeReader(itemsAction);
|
|
78250
|
+
}, [inMemory, itemsAction]);
|
|
77421
78251
|
// The ranks whose item is from before: the run came back to the screen, or
|
|
77422
78252
|
// the source said the collection moved. They stay drawn until a page confirms
|
|
77423
78253
|
// or replaces them, rank by rank. A page covers what the window framed when it
|
|
@@ -77551,6 +78381,7 @@ const useItemStore = ({
|
|
|
77551
78381
|
}
|
|
77552
78382
|
}
|
|
77553
78383
|
if (typeof itemsAction === "function" && itemsAction.trimComposition) {
|
|
78384
|
+
itemsAction.frameComposition(windowFrom, windowTo);
|
|
77554
78385
|
itemsAction.trimComposition(keepFrom, keepTo, budget);
|
|
77555
78386
|
}
|
|
77556
78387
|
},
|
|
@@ -78120,6 +78951,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78120
78951
|
* room has to be restated as a number by whoever asks. The `index` and
|
|
78121
78952
|
* `visibleCount` it also hands out say where to aim before the item is found,
|
|
78122
78953
|
* and how many items to draw before the first paint (see `renderBudget`).
|
|
78954
|
+
* A value that changes before anyone has moved the list is followed (a place
|
|
78955
|
+
* read from a count that answered again): it is compared by value, so it can
|
|
78956
|
+
* be computed at every render.
|
|
78123
78957
|
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
78124
78958
|
* The same, but held: the list goes back there every time this changes, even
|
|
78125
78959
|
* after the user has scrolled — the caller owns where the list is (see
|
|
@@ -93540,5 +94374,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
93540
94374
|
})
|
|
93541
94375
|
});
|
|
93542
94376
|
|
|
93543
|
-
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 };
|
|
94377
|
+
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 };
|
|
93544
94378
|
//# sourceMappingURL=jsenv_navi.js.map
|