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