@jsenv/navi 0.29.98 → 0.29.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jsenv_navi.js +741 -553
- package/dist/jsenv_navi.js.map +7 -7
- package/docs/navigation.md +127 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2819,7 +2819,10 @@ const readSignalForUrlBuild = (connection) => {
|
|
|
2819
2819
|
/**
|
|
2820
2820
|
* Creates a custom route pattern matcher
|
|
2821
2821
|
*/
|
|
2822
|
-
const createRoutePattern = (
|
|
2822
|
+
const createRoutePattern = (
|
|
2823
|
+
pattern,
|
|
2824
|
+
{ searchParams = {}, params: paramConstraints = {} } = {},
|
|
2825
|
+
) => {
|
|
2823
2826
|
// Detect and process path signals in the pattern
|
|
2824
2827
|
const [cleanPattern, pathConnections] = detectSignals(pattern);
|
|
2825
2828
|
|
|
@@ -2844,9 +2847,11 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2844
2847
|
// All connections (path + query) for ancestor/descendant signal resolution
|
|
2845
2848
|
const connections = [...pathConnections, ...queryConnectionMap.values()];
|
|
2846
2849
|
|
|
2850
|
+
const paramConstraintMap = createParamConstraintMap(paramConstraints);
|
|
2847
2851
|
const parsedPattern = parsePattern(cleanPattern, {
|
|
2848
2852
|
pathConnectionMap,
|
|
2849
2853
|
queryConnectionMap,
|
|
2854
|
+
paramConstraintMap,
|
|
2850
2855
|
});
|
|
2851
2856
|
|
|
2852
2857
|
debug$3(`[CustomPattern] Created pattern:`, parsedPattern);
|
|
@@ -2854,11 +2859,21 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2854
2859
|
debug$3(`[CustomPattern] Path connections:`, pathConnectionMap.size);
|
|
2855
2860
|
debug$3(`[CustomPattern] Query connections:`, queryConnectionMap.size);
|
|
2856
2861
|
|
|
2857
|
-
|
|
2862
|
+
/**
|
|
2863
|
+
* @param {string} url
|
|
2864
|
+
* @param {object} [options]
|
|
2865
|
+
* @param {boolean} [options.exact] - Only match the url this pattern is the
|
|
2866
|
+
* address of: a trailing slash or a wildcard stops catching what lies below
|
|
2867
|
+
* it. What "/" means for a container ("everything under me") and what it
|
|
2868
|
+
* means for a redirection ("that very address") are not the same question.
|
|
2869
|
+
*/
|
|
2870
|
+
const applyOn = (url, { exact = false } = {}) => {
|
|
2858
2871
|
const result = matchUrl(parsedPattern, url, {
|
|
2859
2872
|
baseUrl,
|
|
2860
2873
|
baseFileUrl,
|
|
2861
2874
|
queryConnectionMap,
|
|
2875
|
+
paramConstraintMap,
|
|
2876
|
+
exact,
|
|
2862
2877
|
patternObj: patternObject,
|
|
2863
2878
|
});
|
|
2864
2879
|
|
|
@@ -3524,7 +3539,9 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3524
3539
|
const entryIsMeaningful = (conn) => {
|
|
3525
3540
|
const entry = intended.get(conn.paramName);
|
|
3526
3541
|
return (
|
|
3527
|
-
Boolean(entry) &&
|
|
3542
|
+
Boolean(entry) &&
|
|
3543
|
+
entry.value !== undefined &&
|
|
3544
|
+
conn.isCustomValue(entry.value)
|
|
3528
3545
|
);
|
|
3529
3546
|
};
|
|
3530
3547
|
if (ancestorPattern !== patternObject.parent) {
|
|
@@ -3714,6 +3731,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3714
3731
|
connections,
|
|
3715
3732
|
pathConnectionMap, // Separate map for path parameters
|
|
3716
3733
|
queryConnectionMap, // Separate map for query parameters
|
|
3734
|
+
paramConstraintMap, // Which values each param accepts (Map<paramName, test>)
|
|
3717
3735
|
parsedPattern,
|
|
3718
3736
|
children: [],
|
|
3719
3737
|
parent: null,
|
|
@@ -3821,10 +3839,60 @@ const detectSignals = (routePattern) => {
|
|
|
3821
3839
|
return [updatedPattern, signalConnections];
|
|
3822
3840
|
};
|
|
3823
3841
|
|
|
3842
|
+
const EMPTY_PARAM_CONSTRAINT_MAP = new Map();
|
|
3843
|
+
|
|
3844
|
+
/**
|
|
3845
|
+
* Turns `{ gameId: /^W-[A-Z0-9]{8}$/i }` into `Map<paramName, (value) => boolean>`.
|
|
3846
|
+
* A constraint is a regexp, a list of accepted values, or a predicate.
|
|
3847
|
+
* A constraint decides which url segments the param accepts; a segment it
|
|
3848
|
+
* declines makes the whole route a non-match, so only path params can carry
|
|
3849
|
+
* one — a search param is extracted from a url the path already matched.
|
|
3850
|
+
*/
|
|
3851
|
+
const createParamConstraintMap = (paramConstraints) => {
|
|
3852
|
+
const entries = Object.entries(paramConstraints);
|
|
3853
|
+
if (entries.length === 0) {
|
|
3854
|
+
return EMPTY_PARAM_CONSTRAINT_MAP;
|
|
3855
|
+
}
|
|
3856
|
+
const paramConstraintMap = new Map();
|
|
3857
|
+
for (const [paramName, constraint] of entries) {
|
|
3858
|
+
if (constraint instanceof RegExp) {
|
|
3859
|
+
paramConstraintMap.set(paramName, (value) => {
|
|
3860
|
+
constraint.lastIndex = 0; // a /g or /y regexp would otherwise resume where the previous test stopped
|
|
3861
|
+
return constraint.test(value);
|
|
3862
|
+
});
|
|
3863
|
+
continue;
|
|
3864
|
+
}
|
|
3865
|
+
if (Array.isArray(constraint)) {
|
|
3866
|
+
// a url segment is a string, so the list is compared as strings:
|
|
3867
|
+
// `oneOf: [1, 2, 3]` accepts "/2"
|
|
3868
|
+
const acceptedValueSet = new Set(
|
|
3869
|
+
constraint.map((value) => String(value)),
|
|
3870
|
+
);
|
|
3871
|
+
paramConstraintMap.set(paramName, (value) => acceptedValueSet.has(value));
|
|
3872
|
+
continue;
|
|
3873
|
+
}
|
|
3874
|
+
if (typeof constraint === "function") {
|
|
3875
|
+
paramConstraintMap.set(paramName, (value) => Boolean(constraint(value)));
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
throw new TypeError(
|
|
3879
|
+
`params.${paramName} must be a regexp, an array of values or a function, got ${constraint}`,
|
|
3880
|
+
);
|
|
3881
|
+
}
|
|
3882
|
+
return paramConstraintMap;
|
|
3883
|
+
};
|
|
3884
|
+
|
|
3824
3885
|
/**
|
|
3825
3886
|
* Parse a route pattern string into structured segments
|
|
3826
3887
|
*/
|
|
3827
|
-
const parsePattern = (
|
|
3888
|
+
const parsePattern = (
|
|
3889
|
+
pattern,
|
|
3890
|
+
{
|
|
3891
|
+
pathConnectionMap,
|
|
3892
|
+
queryConnectionMap,
|
|
3893
|
+
paramConstraintMap = EMPTY_PARAM_CONSTRAINT_MAP,
|
|
3894
|
+
},
|
|
3895
|
+
) => {
|
|
3828
3896
|
// Build queryParams from queryConnectionMap
|
|
3829
3897
|
const queryParams = [];
|
|
3830
3898
|
for (const [paramName, connection] of queryConnectionMap) {
|
|
@@ -3878,13 +3946,14 @@ const parsePattern = (pattern, { pathConnectionMap, queryConnectionMap }) => {
|
|
|
3878
3946
|
// 1. Explicitly marked with ?
|
|
3879
3947
|
// 2. Has a default value
|
|
3880
3948
|
// 3. Connected signal has undefined value and no explicit default (allows /map to match /map/:panel)
|
|
3949
|
+
// — unless the param is constrained: "no segment" is not one of the values it accepts
|
|
3881
3950
|
const connection =
|
|
3882
3951
|
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
3883
3952
|
const hasDefault =
|
|
3884
3953
|
connection && connection.getDefaultValue() !== undefined;
|
|
3885
3954
|
let isOptional = seg.endsWith("?") || hasDefault;
|
|
3886
3955
|
|
|
3887
|
-
if (!isOptional) {
|
|
3956
|
+
if (!isOptional && !paramConstraintMap.has(paramName)) {
|
|
3888
3957
|
// Check if connected signal has undefined value (making parameter optional for index routes)
|
|
3889
3958
|
if (
|
|
3890
3959
|
connection &&
|
|
@@ -3995,7 +4064,14 @@ const tryExtractChildParameters = (
|
|
|
3995
4064
|
// We need to verify that this parameter isn't already captured by parent
|
|
3996
4065
|
if (!(segment.name in existingParams)) {
|
|
3997
4066
|
const urlSegment = remainingSegments[remainingIndex];
|
|
3998
|
-
|
|
4067
|
+
const paramValue = decodeURIComponent(urlSegment);
|
|
4068
|
+
const paramConstraint = childPattern.paramConstraintMap.get(
|
|
4069
|
+
segment.name,
|
|
4070
|
+
);
|
|
4071
|
+
if (paramConstraint && !paramConstraint(paramValue)) {
|
|
4072
|
+
return null; // the child declines this value, it cannot explain these segments
|
|
4073
|
+
}
|
|
4074
|
+
childParams[segment.name] = paramValue;
|
|
3999
4075
|
remainingIndex++;
|
|
4000
4076
|
}
|
|
4001
4077
|
} else if (
|
|
@@ -4022,7 +4098,14 @@ const tryExtractChildParameters = (
|
|
|
4022
4098
|
const matchUrl = (
|
|
4023
4099
|
parsedPattern,
|
|
4024
4100
|
url,
|
|
4025
|
-
{
|
|
4101
|
+
{
|
|
4102
|
+
baseUrl,
|
|
4103
|
+
baseFileUrl,
|
|
4104
|
+
queryConnectionMap,
|
|
4105
|
+
paramConstraintMap = EMPTY_PARAM_CONSTRAINT_MAP,
|
|
4106
|
+
exact = false,
|
|
4107
|
+
patternObj = null,
|
|
4108
|
+
},
|
|
4026
4109
|
) => {
|
|
4027
4110
|
// Parse the URL
|
|
4028
4111
|
const urlObj = new URL(url, baseUrl);
|
|
@@ -4064,7 +4147,7 @@ const matchUrl = (
|
|
|
4064
4147
|
}
|
|
4065
4148
|
|
|
4066
4149
|
// Root route with trailing slash matches all sub-paths (prefix matching, like other trailing-slash routes)
|
|
4067
|
-
if (parsedPattern.trailingSlash) {
|
|
4150
|
+
if (parsedPattern.trailingSlash && !exact) {
|
|
4068
4151
|
return extractSearchParams(urlObj, queryConnectionMap);
|
|
4069
4152
|
}
|
|
4070
4153
|
|
|
@@ -4133,7 +4216,12 @@ const matchUrl = (
|
|
|
4133
4216
|
|
|
4134
4217
|
// Capture URL segment as parameter value
|
|
4135
4218
|
const urlSeg = urlSegments[urlSegmentIndex];
|
|
4136
|
-
|
|
4219
|
+
const paramValue = decodeURIComponent(urlSeg);
|
|
4220
|
+
const paramConstraint = paramConstraintMap.get(patternSeg.name);
|
|
4221
|
+
if (paramConstraint && !paramConstraint(paramValue)) {
|
|
4222
|
+
return null; // the param declines this value: the route does not match
|
|
4223
|
+
}
|
|
4224
|
+
params[patternSeg.name] = paramValue;
|
|
4137
4225
|
urlSegmentIndex++;
|
|
4138
4226
|
}
|
|
4139
4227
|
}
|
|
@@ -4142,8 +4230,7 @@ const matchUrl = (
|
|
|
4142
4230
|
// Patterns with trailing slashes can match additional URL segments (like wildcards)
|
|
4143
4231
|
// Patterns without trailing slashes should match exactly (unless they're wildcards)
|
|
4144
4232
|
if (
|
|
4145
|
-
!parsedPattern.wildcard &&
|
|
4146
|
-
!parsedPattern.trailingSlash &&
|
|
4233
|
+
(exact || (!parsedPattern.wildcard && !parsedPattern.trailingSlash)) &&
|
|
4147
4234
|
urlSegmentIndex < urlSegments.length
|
|
4148
4235
|
) {
|
|
4149
4236
|
return null; // Pattern without trailing slash/wildcard should not match extra segments
|
|
@@ -4862,8 +4949,61 @@ const getRoutePrivateProperties = (route) => {
|
|
|
4862
4949
|
const ROUTE_NOT_MATCHING_PARAMS = {};
|
|
4863
4950
|
// Flag to prevent signal-to-URL synchronization during URL-to-signal synchronization
|
|
4864
4951
|
let isUpdatingRoutesFromUrl = false;
|
|
4865
|
-
|
|
4866
|
-
|
|
4952
|
+
/**
|
|
4953
|
+
* Declares a route from an url pattern.
|
|
4954
|
+
*
|
|
4955
|
+
* @param {string} pattern - The url pattern, where `:name` is a path param and
|
|
4956
|
+
* `:name=${signal}` binds that param to a signal.
|
|
4957
|
+
* @param {object} [options]
|
|
4958
|
+
* @param {Object<string, import("@preact/signals").Signal>} [options.searchParams]
|
|
4959
|
+
* Search params this route two-way syncs with, by name.
|
|
4960
|
+
* @param {Object<string, RegExp | Array | ((value: string) => boolean)>} [options.params]
|
|
4961
|
+
* Which values a path param accepts, by param name: a regexp tested against the
|
|
4962
|
+
* decoded segment, the list of accepted values (compared as strings, so it can
|
|
4963
|
+
* be the `oneOf` of the signal bound to that param), or a predicate. A route
|
|
4964
|
+
* whose param declines the segment does not match at all: no signal is
|
|
4965
|
+
* written, `<Route fallback>` is reachable, and `/:gameId` can sit at the root
|
|
4966
|
+
* without swallowing `/cgu`. A constrained param is also required — no segment
|
|
4967
|
+
* is not one of the values it accepts — so `/:gameId` does not match `/`.
|
|
4968
|
+
*
|
|
4969
|
+
* Constrain the shape, not the existence: whether that game exists is for the
|
|
4970
|
+
* route action and the page to answer, on a route that did match.
|
|
4971
|
+
*
|
|
4972
|
+
* ```js
|
|
4973
|
+
* route(`/:gameId=${gamePageIdSignal}`, { params: { gameId: /^W-[A-Z0-9]{8}$/i } });
|
|
4974
|
+
* route(`/games/:section=${sectionSignal}`, { params: { section: SECTIONS } });
|
|
4975
|
+
* ```
|
|
4976
|
+
* @param {object} [options.redirectRoute]
|
|
4977
|
+
* This address only sends elsewhere: the route it resolves to. The
|
|
4978
|
+
* redirection happens at the door of the navigation, so this address is never
|
|
4979
|
+
* displayed, never enters the history, runs no route action and writes no
|
|
4980
|
+
* signal — going back lands on the page before it, not on the redirection
|
|
4981
|
+
* again. It fires on this route's own address only: a trailing slash still
|
|
4982
|
+
* catches what lies below it for rendering, never for redirecting.
|
|
4983
|
+
*
|
|
4984
|
+
* The params found in the url carry over to the ones the target route
|
|
4985
|
+
* declares under the same name; what it cannot place is left behind.
|
|
4986
|
+
*
|
|
4987
|
+
* ```js
|
|
4988
|
+
* route("/", { redirectRoute: MY_GAMES_PAGE });
|
|
4989
|
+
* // gameId carries over on its own, shareState is not a param of GAME_PAGE
|
|
4990
|
+
* route("/:gameId/:shareState", { redirectRoute: GAME_PAGE });
|
|
4991
|
+
* ```
|
|
4992
|
+
* @param {object|Function|null} [options.redirectRouteParams]
|
|
4993
|
+
* What to change about the params carried over: an object, or a function of
|
|
4994
|
+
* the params found in the url returning one. Written params win over inherited
|
|
4995
|
+
* ones, `undefined` drops one, and `null` carries nothing over at all.
|
|
4996
|
+
*
|
|
4997
|
+
* ```js
|
|
4998
|
+
* route("/legacy/:id", { redirectRoute: GAME_PAGE, redirectRouteParams: ({ id }) => ({ gameId: id }) });
|
|
4999
|
+
* route("/:gameId/invite", { redirectRoute: HOME_PAGE, redirectRouteParams: null });
|
|
5000
|
+
* ```
|
|
5001
|
+
*/
|
|
5002
|
+
const route = (
|
|
5003
|
+
pattern,
|
|
5004
|
+
{ searchParams, params, redirectRoute, redirectRouteParams } = {},
|
|
5005
|
+
) => {
|
|
5006
|
+
const routePattern = createRoutePattern(pattern, { searchParams, params });
|
|
4867
5007
|
const { cleanPattern } = routePattern;
|
|
4868
5008
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
4869
5009
|
|
|
@@ -4906,6 +5046,8 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
4906
5046
|
{
|
|
4907
5047
|
const routePrivateProperties = {
|
|
4908
5048
|
routePattern,
|
|
5049
|
+
redirectRoute,
|
|
5050
|
+
redirectRouteParams,
|
|
4909
5051
|
setup: null,
|
|
4910
5052
|
updateStatus: null,
|
|
4911
5053
|
cleanup: null,
|
|
@@ -5229,6 +5371,113 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
5229
5371
|
|
|
5230
5372
|
const [publishRouteMutations, observeRouteMutations] = createPubSub();
|
|
5231
5373
|
|
|
5374
|
+
let redirectingRouteSet = null;
|
|
5375
|
+
/**
|
|
5376
|
+
* Where does this url really lead?
|
|
5377
|
+
*
|
|
5378
|
+
* Asked at the door of every navigation, before the url is written anywhere —
|
|
5379
|
+
* an address that only sends elsewhere must not become a page, an entry in the
|
|
5380
|
+
* history, or a route anything can see matching. Answered without reading a
|
|
5381
|
+
* single signal: the url alone says it.
|
|
5382
|
+
*
|
|
5383
|
+
* The chain is followed here rather than by letting each redirection navigate
|
|
5384
|
+
* in turn, so one navigation happens and the addresses in between leave no
|
|
5385
|
+
* trace at all.
|
|
5386
|
+
*
|
|
5387
|
+
* @param {string} url
|
|
5388
|
+
* @returns {string|null} The url to go to instead, or null.
|
|
5389
|
+
*/
|
|
5390
|
+
const resolveRouteRedirection = (url) => {
|
|
5391
|
+
if (!redirectingRouteSet || redirectingRouteSet.size === 0) {
|
|
5392
|
+
return null;
|
|
5393
|
+
}
|
|
5394
|
+
let urlToResolve = url;
|
|
5395
|
+
let redirectionUrl = null;
|
|
5396
|
+
const urlChain = [url];
|
|
5397
|
+
while (true) {
|
|
5398
|
+
const nextUrl = resolveRedirectionOnce(urlToResolve);
|
|
5399
|
+
if (!nextUrl || nextUrl === urlToResolve) {
|
|
5400
|
+
break;
|
|
5401
|
+
}
|
|
5402
|
+
if (urlChain.includes(nextUrl)) {
|
|
5403
|
+
throw new Error(
|
|
5404
|
+
`Redirection cycle: ${[...urlChain, nextUrl].join(" -> ")}`,
|
|
5405
|
+
);
|
|
5406
|
+
}
|
|
5407
|
+
urlChain.push(nextUrl);
|
|
5408
|
+
redirectionUrl = nextUrl;
|
|
5409
|
+
urlToResolve = nextUrl;
|
|
5410
|
+
}
|
|
5411
|
+
return redirectionUrl;
|
|
5412
|
+
};
|
|
5413
|
+
const resolveRedirectionOnce = (url) => {
|
|
5414
|
+
let redirectingRoute = null;
|
|
5415
|
+
let redirectingRouteParams = null;
|
|
5416
|
+
let maxDepth = -1;
|
|
5417
|
+
for (const route of redirectingRouteSet) {
|
|
5418
|
+
const { routePattern } = getRoutePrivateProperties(route);
|
|
5419
|
+
// exact: what a trailing slash catches is what a container renders, not
|
|
5420
|
+
// what an address redirects — "/" redirecting must not take "/cgu" with it
|
|
5421
|
+
const urlParams = routePattern.applyOn(url, { exact: true });
|
|
5422
|
+
if (!urlParams) {
|
|
5423
|
+
continue;
|
|
5424
|
+
}
|
|
5425
|
+
// Deeper = more specific, the reading the whole router shares (see
|
|
5426
|
+
// replaceParams): "/:gameId/invite" is a child of "/:gameId/:shareState",
|
|
5427
|
+
// so it answers for an url both of them match.
|
|
5428
|
+
if (routePattern.depth > maxDepth) {
|
|
5429
|
+
maxDepth = routePattern.depth;
|
|
5430
|
+
redirectingRoute = route;
|
|
5431
|
+
redirectingRouteParams = urlParams;
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
if (!redirectingRoute) {
|
|
5435
|
+
return null;
|
|
5436
|
+
}
|
|
5437
|
+
return buildRedirectionUrl(redirectingRoute, redirectingRouteParams);
|
|
5438
|
+
};
|
|
5439
|
+
const buildRedirectionUrl = (route, urlParams) => {
|
|
5440
|
+
const { redirectRoute, redirectRouteParams } =
|
|
5441
|
+
getRoutePrivateProperties(route);
|
|
5442
|
+
const paramsCarriedOver =
|
|
5443
|
+
redirectRouteParams === null
|
|
5444
|
+
? {}
|
|
5445
|
+
: paramsTargetCanPlace(redirectRoute, urlParams);
|
|
5446
|
+
if (!redirectRouteParams) {
|
|
5447
|
+
return redirectRoute.buildUrl(paramsCarriedOver);
|
|
5448
|
+
}
|
|
5449
|
+
const paramsWritten =
|
|
5450
|
+
typeof redirectRouteParams === "function"
|
|
5451
|
+
? redirectRouteParams(urlParams)
|
|
5452
|
+
: redirectRouteParams;
|
|
5453
|
+
// A param written as undefined is a param dropped: `paramName in params` is
|
|
5454
|
+
// what tells a url build "this one is decided", value included (resolveParams)
|
|
5455
|
+
return redirectRoute.buildUrl({ ...paramsCarriedOver, ...paramsWritten });
|
|
5456
|
+
};
|
|
5457
|
+
|
|
5458
|
+
/**
|
|
5459
|
+
* The params of the url being left that the target route can put somewhere:
|
|
5460
|
+
* its own path segments and the search params it declares. What it cannot
|
|
5461
|
+
* place would otherwise be appended to the url as a query param, and a share
|
|
5462
|
+
* link resolving to "/W-ABC234PQ?shareState=1" is not the address anyone meant.
|
|
5463
|
+
*/
|
|
5464
|
+
const paramsTargetCanPlace = (redirectRoute, urlParams) => {
|
|
5465
|
+
const { routePattern } = getRoutePrivateProperties(redirectRoute);
|
|
5466
|
+
const { parsedPattern, queryConnectionMap } = routePattern;
|
|
5467
|
+
const paramsCarriedOver = {};
|
|
5468
|
+
for (const paramName of Object.keys(urlParams)) {
|
|
5469
|
+
const canPlace =
|
|
5470
|
+
queryConnectionMap.has(paramName) ||
|
|
5471
|
+
parsedPattern.segments.some(
|
|
5472
|
+
(segment) => segment.type === "param" && segment.name === paramName,
|
|
5473
|
+
);
|
|
5474
|
+
if (canPlace) {
|
|
5475
|
+
paramsCarriedOver[paramName] = urlParams[paramName];
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
return paramsCarriedOver;
|
|
5479
|
+
};
|
|
5480
|
+
|
|
5232
5481
|
let setupRoutesCalled = false;
|
|
5233
5482
|
const setupRoutes = (routes) => {
|
|
5234
5483
|
if (setupRoutesCalled) {
|
|
@@ -5258,6 +5507,22 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
5258
5507
|
setup({ routeSet, getUrl });
|
|
5259
5508
|
}
|
|
5260
5509
|
|
|
5510
|
+
// Checked here rather than at declaration: a route may redirect to one
|
|
5511
|
+
// declared after it, and reading the target then would forbid that order.
|
|
5512
|
+
redirectingRouteSet = new Set();
|
|
5513
|
+
for (const route of routeSet) {
|
|
5514
|
+
const { redirectRoute } = getRoutePrivateProperties(route);
|
|
5515
|
+
if (!redirectRoute) {
|
|
5516
|
+
continue;
|
|
5517
|
+
}
|
|
5518
|
+
if (!redirectRoute.isRoute) {
|
|
5519
|
+
throw new TypeError(
|
|
5520
|
+
`${route} redirects to ${redirectRoute}, expecting a route object`,
|
|
5521
|
+
);
|
|
5522
|
+
}
|
|
5523
|
+
redirectingRouteSet.add(route);
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5261
5526
|
// Store previous route states to detect changes
|
|
5262
5527
|
const routePreviousStateMap = new WeakMap();
|
|
5263
5528
|
const updateRoutes = (
|
|
@@ -5501,6 +5766,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
5501
5766
|
routePrivatePropertiesMap.delete(route);
|
|
5502
5767
|
}
|
|
5503
5768
|
routeSet.clear();
|
|
5769
|
+
redirectingRouteSet = null;
|
|
5504
5770
|
setupRoutesCalled = false;
|
|
5505
5771
|
};
|
|
5506
5772
|
return {
|
|
@@ -11938,6 +12204,39 @@ const reportErrorIfNobodyDisplaysIt = (error, { action } = {}) => {
|
|
|
11938
12204
|
|
|
11939
12205
|
const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
11940
12206
|
|
|
12207
|
+
/*
|
|
12208
|
+
* Actions: async callbacks wrapped in reactive state.
|
|
12209
|
+
*
|
|
12210
|
+
* An action owns a set of signals (params, runningState, error, value, data)
|
|
12211
|
+
* and moves through IDLE → RUNNING → COMPLETED / FAILED / ABORTED (see
|
|
12212
|
+
* action_run_states.js). `createAction(callback)` returns the root action;
|
|
12213
|
+
* `.bindParams(params)` derives child actions (one per params value, cached),
|
|
12214
|
+
* and binding a signal (or an object containing signals) returns an action
|
|
12215
|
+
* *proxy* that retargets itself to the right child action as the signal
|
|
12216
|
+
* changes (see createActionProxyFromSignal).
|
|
12217
|
+
*
|
|
12218
|
+
* How things run: prerun/run/rerun/reset never execute the action directly —
|
|
12219
|
+
* they go through `dispatchActions`, which the navigation integration can
|
|
12220
|
+
* replace via `setActionDispatcher` so that every action update participates
|
|
12221
|
+
* in the browser navigation lifecycle (abort signals, navigation events).
|
|
12222
|
+
* The default dispatcher calls `updateActions`, the single entry point that
|
|
12223
|
+
* resolves priorities between the four operation sets (reset > rerun > run >
|
|
12224
|
+
* prerun) and performs them.
|
|
12225
|
+
*
|
|
12226
|
+
* Memory design (the surprising part): nothing here keeps actions alive.
|
|
12227
|
+
* Child actions are held through ephemerons (createJsValueWeakMap) so a child
|
|
12228
|
+
* and its params are garbage-collected together, running actions live in
|
|
12229
|
+
* iterable *weak* sets, and property/signal mirroring uses weakEffect. Two
|
|
12230
|
+
* consequences to be aware of:
|
|
12231
|
+
* - an action can exist in several places only if everyone shares the same
|
|
12232
|
+
* instance (the caches above are what makes lookups return it);
|
|
12233
|
+
* - prerun actions may have no other reference yet, so
|
|
12234
|
+
* prerunProtectionRegistry pins them for a few minutes.
|
|
12235
|
+
*
|
|
12236
|
+
* An action run never throws: failures land in errorSignal and are reported
|
|
12237
|
+
* once, by one rule, in action_error_report.js (see the comment in onRunError).
|
|
12238
|
+
*/
|
|
12239
|
+
|
|
11941
12240
|
let DEBUG$1 = false;
|
|
11942
12241
|
const enableDebugActions = () => {
|
|
11943
12242
|
DEBUG$1 = true;
|
|
@@ -11976,7 +12275,7 @@ const getActionDispatcher = () => dispatchActions;
|
|
|
11976
12275
|
const rerunActions = async (actionSet, options) => {
|
|
11977
12276
|
return dispatchActions({
|
|
11978
12277
|
rerunSet: actionSet,
|
|
11979
|
-
reason: "rerunActions was
|
|
12278
|
+
reason: "rerunActions was called",
|
|
11980
12279
|
...options,
|
|
11981
12280
|
});
|
|
11982
12281
|
};
|
|
@@ -11995,7 +12294,7 @@ const rerunActions = async (actionSet, options) => {
|
|
|
11995
12294
|
*/
|
|
11996
12295
|
const prerunProtectionRegistry = (() => {
|
|
11997
12296
|
const protectedActionMap = new Map(); // action -> { timeoutId, timestamp }
|
|
11998
|
-
const PROTECTION_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
12297
|
+
const PROTECTION_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
11999
12298
|
|
|
12000
12299
|
const unprotect = (action) => {
|
|
12001
12300
|
const protection = protectedActionMap.get(action);
|
|
@@ -12009,7 +12308,7 @@ const prerunProtectionRegistry = (() => {
|
|
|
12009
12308
|
|
|
12010
12309
|
return {
|
|
12011
12310
|
protect(action) {
|
|
12012
|
-
//
|
|
12311
|
+
// already protected: extend the protection
|
|
12013
12312
|
if (protectedActionMap.has(action)) {
|
|
12014
12313
|
const existing = protectedActionMap.get(action);
|
|
12015
12314
|
clearTimeout(existing.timeoutId);
|
|
@@ -12029,29 +12328,11 @@ const prerunProtectionRegistry = (() => {
|
|
|
12029
12328
|
},
|
|
12030
12329
|
|
|
12031
12330
|
unprotect,
|
|
12032
|
-
|
|
12033
|
-
isProtected(action) {
|
|
12034
|
-
return protectedActionMap.has(action);
|
|
12035
|
-
},
|
|
12036
|
-
|
|
12037
|
-
// Pour debugging
|
|
12038
|
-
getProtectedActions() {
|
|
12039
|
-
return Array.from(protectedActionMap.keys());
|
|
12040
|
-
},
|
|
12041
|
-
|
|
12042
|
-
// Nettoyage manuel si nécessaire
|
|
12043
|
-
clear() {
|
|
12044
|
-
for (const [, protection] of protectedActionMap) {
|
|
12045
|
-
clearTimeout(protection.timeoutId);
|
|
12046
|
-
}
|
|
12047
|
-
protectedActionMap.clear();
|
|
12048
|
-
},
|
|
12049
12331
|
};
|
|
12050
12332
|
})();
|
|
12051
12333
|
|
|
12052
12334
|
const formatActionSet = (actionSet, prefix = "") => {
|
|
12053
|
-
let message =
|
|
12054
|
-
message += `${prefix}`;
|
|
12335
|
+
let message = prefix;
|
|
12055
12336
|
for (const action of actionSet) {
|
|
12056
12337
|
message += "\n";
|
|
12057
12338
|
message += prefixFirstAndIndentRemainingLines(String(action), {
|
|
@@ -12386,7 +12667,6 @@ ${lines.join("\n")}`);
|
|
|
12386
12667
|
};
|
|
12387
12668
|
|
|
12388
12669
|
const NO_PARAMS = { __no_params__: true };
|
|
12389
|
-
const initialParamsDefault = NO_PARAMS;
|
|
12390
12670
|
const mergeActionParams = (currentParams, newParams) => {
|
|
12391
12671
|
if (currentParams === NO_PARAMS) {
|
|
12392
12672
|
return newParams;
|
|
@@ -12434,7 +12714,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12434
12714
|
} = options;
|
|
12435
12715
|
if (!Object.hasOwn(options, "params")) {
|
|
12436
12716
|
// even undefined should be respected it's only when not provided at all we use default
|
|
12437
|
-
params =
|
|
12717
|
+
params = NO_PARAMS;
|
|
12438
12718
|
}
|
|
12439
12719
|
if (value === undefined && data !== undefined) {
|
|
12440
12720
|
value = data;
|
|
@@ -12447,11 +12727,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12447
12727
|
const errorSignal = signal(error);
|
|
12448
12728
|
const valueSignal = signal(valueInitial);
|
|
12449
12729
|
const dataSignal = valueToData
|
|
12450
|
-
? computed(() =>
|
|
12451
|
-
const value = valueSignal.value;
|
|
12452
|
-
const data = valueToData(value);
|
|
12453
|
-
return data;
|
|
12454
|
-
})
|
|
12730
|
+
? computed(() => valueToData(valueSignal.value))
|
|
12455
12731
|
: valueSignal;
|
|
12456
12732
|
|
|
12457
12733
|
const prerun = (options) => {
|
|
@@ -12484,7 +12760,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12484
12760
|
return dispatchSingleAction(action, "reset", options);
|
|
12485
12761
|
};
|
|
12486
12762
|
const abort = (reason) => {
|
|
12487
|
-
if (
|
|
12763
|
+
if (runningStateSignal.peek() !== RUNNING) {
|
|
12488
12764
|
return false;
|
|
12489
12765
|
}
|
|
12490
12766
|
const actionAbort = actionAbortMap.get(action);
|
|
@@ -12510,7 +12786,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12510
12786
|
*/
|
|
12511
12787
|
const childActionWeakMap = createJsValueWeakMap();
|
|
12512
12788
|
const _bindParams = (newParamsOrSignal, options = {}) => {
|
|
12513
|
-
//
|
|
12789
|
+
// Case 1: a signal → proxy that retargets as the signal changes
|
|
12514
12790
|
if (isSignal(newParamsOrSignal)) {
|
|
12515
12791
|
const combinedParamsSignal = computed(() => {
|
|
12516
12792
|
const newParams = newParamsOrSignal.value;
|
|
@@ -12524,7 +12800,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12524
12800
|
);
|
|
12525
12801
|
}
|
|
12526
12802
|
|
|
12527
|
-
//
|
|
12803
|
+
// Case 2: a plain object → child action, or proxy when it contains signals
|
|
12528
12804
|
if (isPlainObject$1(newParamsOrSignal)) {
|
|
12529
12805
|
const staticParams = {};
|
|
12530
12806
|
const signalMap = new Map();
|
|
@@ -12545,7 +12821,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12545
12821
|
}
|
|
12546
12822
|
|
|
12547
12823
|
if (signalMap.size === 0) {
|
|
12548
|
-
//
|
|
12824
|
+
// no signals: plain static merge
|
|
12549
12825
|
if (
|
|
12550
12826
|
params === null ||
|
|
12551
12827
|
typeof params !== "object" ||
|
|
@@ -12563,24 +12839,27 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12563
12839
|
});
|
|
12564
12840
|
}
|
|
12565
12841
|
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
const params = {};
|
|
12842
|
+
const combinedParamsSignal = computed(() => {
|
|
12843
|
+
const combinedParams = {};
|
|
12569
12844
|
for (const key of keyArray) {
|
|
12570
12845
|
const signalForThisKey = signalMap.get(key);
|
|
12571
12846
|
if (signalForThisKey) {
|
|
12572
12847
|
// eslint-disable-next-line signals/no-conditional-value-read
|
|
12573
|
-
|
|
12848
|
+
combinedParams[key] = signalForThisKey.value;
|
|
12574
12849
|
} else {
|
|
12575
|
-
|
|
12850
|
+
combinedParams[key] = staticParams[key];
|
|
12576
12851
|
}
|
|
12577
12852
|
}
|
|
12578
|
-
return
|
|
12853
|
+
return combinedParams;
|
|
12579
12854
|
});
|
|
12580
|
-
return createActionProxyFromSignal(
|
|
12855
|
+
return createActionProxyFromSignal(
|
|
12856
|
+
action,
|
|
12857
|
+
combinedParamsSignal,
|
|
12858
|
+
options,
|
|
12859
|
+
);
|
|
12581
12860
|
}
|
|
12582
12861
|
|
|
12583
|
-
//
|
|
12862
|
+
// Case 3: a primitive or non-plain object (DOM event, …) → child action
|
|
12584
12863
|
return createChildAction({
|
|
12585
12864
|
params: newParamsOrSignal,
|
|
12586
12865
|
...options,
|
|
@@ -12613,7 +12892,6 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12613
12892
|
return childAction;
|
|
12614
12893
|
};
|
|
12615
12894
|
|
|
12616
|
-
// ✅ Implement matchAllSelfOrDescendant
|
|
12617
12895
|
const matchAllSelfOrDescendant = (predicate, { includeProxies } = {}) => {
|
|
12618
12896
|
const matches = [];
|
|
12619
12897
|
|
|
@@ -12648,32 +12926,31 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12648
12926
|
generateActionCallSource(name, params),
|
|
12649
12927
|
);
|
|
12650
12928
|
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
}
|
|
12929
|
+
// The action is a callable: `ACTION(params)` is `ACTION.bindParams(params).rerun()`
|
|
12930
|
+
action = function actionFunction(...args) {
|
|
12931
|
+
if (args.length === 0) {
|
|
12932
|
+
return action.rerun();
|
|
12933
|
+
}
|
|
12934
|
+
const boundAction = bindParams(...args);
|
|
12935
|
+
return boundAction.rerun();
|
|
12936
|
+
};
|
|
12937
|
+
Object.defineProperty(action, "name", {
|
|
12938
|
+
configurable: true,
|
|
12939
|
+
get() {
|
|
12940
|
+
return actionNameSignal.value;
|
|
12941
|
+
},
|
|
12942
|
+
});
|
|
12943
|
+
Object.defineProperty(action, "callSource", {
|
|
12944
|
+
configurable: true,
|
|
12945
|
+
get() {
|
|
12946
|
+
return actionCallSourceSignal.value;
|
|
12947
|
+
},
|
|
12948
|
+
set(v) {
|
|
12949
|
+
actionCallSourceSignal.value = v;
|
|
12950
|
+
},
|
|
12951
|
+
});
|
|
12952
|
+
// makes createAction(anAction) return the action itself
|
|
12953
|
+
actionWeakMap.set(action, action);
|
|
12677
12954
|
|
|
12678
12955
|
// Assign all the action properties and methods to the function
|
|
12679
12956
|
Object.assign(action, {
|
|
@@ -12695,7 +12972,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12695
12972
|
reset,
|
|
12696
12973
|
abort,
|
|
12697
12974
|
bindParams,
|
|
12698
|
-
matchAllSelfOrDescendant,
|
|
12975
|
+
matchAllSelfOrDescendant,
|
|
12699
12976
|
replaceParams: (newParams) => {
|
|
12700
12977
|
const currentParams = paramsSignal.value;
|
|
12701
12978
|
const nextParams = mergeActionParams(currentParams, newParams);
|
|
@@ -12723,7 +13000,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12723
13000
|
toString: () => action.callSource,
|
|
12724
13001
|
meta,
|
|
12725
13002
|
debug: (...args) => {
|
|
12726
|
-
if (!meta.debug
|
|
13003
|
+
if (!meta.debug && !DEBUG$1) {
|
|
12727
13004
|
return;
|
|
12728
13005
|
}
|
|
12729
13006
|
console.debug(...args);
|
|
@@ -12738,7 +13015,8 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12738
13015
|
});
|
|
12739
13016
|
Object.preventExtensions(action);
|
|
12740
13017
|
|
|
12741
|
-
//
|
|
13018
|
+
// Mirror signals into plain properties (action.error, action.data, …)
|
|
13019
|
+
// so non-reactive code can read them without subscribing.
|
|
12742
13020
|
{
|
|
12743
13021
|
weakEffect([action], (actionRef) => {
|
|
12744
13022
|
isPrerun = isPrerunSignal.value;
|
|
@@ -12764,7 +13042,6 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
12764
13042
|
});
|
|
12765
13043
|
}
|
|
12766
13044
|
|
|
12767
|
-
// Propriétés privées
|
|
12768
13045
|
{
|
|
12769
13046
|
const ui = {
|
|
12770
13047
|
renderLoaded: null,
|
|
@@ -13043,8 +13320,8 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
13043
13320
|
* @param {boolean} options.rerunOnChange - Ensures the action is rerun every time a signal value is modified.
|
|
13044
13321
|
* This enables live updates - for example, performing an HTTP GET request every time
|
|
13045
13322
|
* a list of filters changes, providing real-time results without user interaction.
|
|
13046
|
-
* @param {boolean} options.inheritData - When
|
|
13047
|
-
* By default (
|
|
13323
|
+
* @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
|
|
13324
|
+
* By default (true), the proxy carries over the previous target's value and error into the new action.
|
|
13048
13325
|
* This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
|
|
13049
13326
|
* new action completes, not when it starts loading. Code that needs to distinguish loading state can still
|
|
13050
13327
|
* check `action.runningState`, while code that just reads `action.data` always sees the most recent
|
|
@@ -13138,6 +13415,7 @@ const createActionProxyFromSignal = (
|
|
|
13138
13415
|
currentAction = actionTarget;
|
|
13139
13416
|
currentActionPrivateProperties = getActionPrivateProperties(actionTarget);
|
|
13140
13417
|
}
|
|
13418
|
+
|
|
13141
13419
|
actionTargetPreviousWeakRef = actionTarget
|
|
13142
13420
|
? new WeakRef(actionTarget)
|
|
13143
13421
|
: null;
|
|
@@ -13163,25 +13441,22 @@ const createActionProxyFromSignal = (
|
|
|
13163
13441
|
|
|
13164
13442
|
const nameSignal = signal(action.name);
|
|
13165
13443
|
const callSourceSignal = signal(`[Proxy] ${action.callSource}`);
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
});
|
|
13183
|
-
actionWeakMap.set(actionProxy, actionProxy);
|
|
13184
|
-
}
|
|
13444
|
+
const actionProxy = function actionProxyFunction() {
|
|
13445
|
+
return actionProxy.rerun();
|
|
13446
|
+
};
|
|
13447
|
+
Object.defineProperty(actionProxy, "name", {
|
|
13448
|
+
configurable: true,
|
|
13449
|
+
get() {
|
|
13450
|
+
return nameSignal.value;
|
|
13451
|
+
},
|
|
13452
|
+
});
|
|
13453
|
+
Object.defineProperty(actionProxy, "callSource", {
|
|
13454
|
+
configurable: true,
|
|
13455
|
+
get() {
|
|
13456
|
+
return callSourceSignal.value;
|
|
13457
|
+
},
|
|
13458
|
+
});
|
|
13459
|
+
actionWeakMap.set(actionProxy, actionProxy);
|
|
13185
13460
|
|
|
13186
13461
|
// Create our own signal for params that we control completely
|
|
13187
13462
|
const proxyParamsSignal = signal(paramsSignal.value);
|
|
@@ -13384,11 +13659,6 @@ const isPlainObject$1 = (obj) => {
|
|
|
13384
13659
|
);
|
|
13385
13660
|
};
|
|
13386
13661
|
|
|
13387
|
-
const COMPLETED_ACTION = createAction(() => undefined, {
|
|
13388
|
-
name: "ACTION.COMPLETED",
|
|
13389
|
-
});
|
|
13390
|
-
getActionPrivateProperties(COMPLETED_ACTION).performRun({});
|
|
13391
|
-
|
|
13392
13662
|
// used by form elements such as <input>, <select>, <textarea> to have their own action bound to a single parameter
|
|
13393
13663
|
// when inside a <form> the form params are updated when the form element single param is updated
|
|
13394
13664
|
const useActionBoundToOneParam = (action, paramsSignal) => {
|
|
@@ -13410,19 +13680,27 @@ const useAction = (action, paramsSignal) => {
|
|
|
13410
13680
|
};
|
|
13411
13681
|
|
|
13412
13682
|
const useBoundAction = (action, actionParamsSignal) => {
|
|
13413
|
-
|
|
13683
|
+
// The cache gives an inline function a stable action identity across renders.
|
|
13684
|
+
// That identity is only wanted while `action` stays the same kind
|
|
13685
|
+
// (function to function); when the kind changes — none ↔ function ↔ action
|
|
13686
|
+
// object — each branch clears the other kind's refs so the control picks up
|
|
13687
|
+
// its new role instead of the action it was born with.
|
|
13688
|
+
const noopActionRef = useRef();
|
|
13689
|
+
const actionFromFunctionRef = useRef();
|
|
13414
13690
|
const actionCallbackRef = useRef();
|
|
13415
13691
|
|
|
13416
13692
|
if (!action) {
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13693
|
+
actionFromFunctionRef.current = undefined;
|
|
13694
|
+
actionCallbackRef.current = undefined;
|
|
13695
|
+
const existingNoopAction = noopActionRef.current;
|
|
13696
|
+
if (existingNoopAction) {
|
|
13697
|
+
return existingNoopAction;
|
|
13420
13698
|
}
|
|
13421
13699
|
const noopAction = createAction(() => {}, { params: undefined });
|
|
13422
13700
|
const noopActionBound = actionParamsSignal
|
|
13423
13701
|
? noopAction.bindParams(actionParamsSignal)
|
|
13424
13702
|
: noopAction;
|
|
13425
|
-
|
|
13703
|
+
noopActionRef.current = noopActionBound;
|
|
13426
13704
|
return noopActionBound;
|
|
13427
13705
|
}
|
|
13428
13706
|
const isFunction = typeof action === "function";
|
|
@@ -13433,7 +13711,7 @@ const useBoundAction = (action, actionParamsSignal) => {
|
|
|
13433
13711
|
}
|
|
13434
13712
|
if (isFunctionButNotAnActionFunction(action)) {
|
|
13435
13713
|
actionCallbackRef.current = action;
|
|
13436
|
-
const existingAction =
|
|
13714
|
+
const existingAction = actionFromFunctionRef.current;
|
|
13437
13715
|
if (existingAction) {
|
|
13438
13716
|
return existingAction;
|
|
13439
13717
|
}
|
|
@@ -13449,14 +13727,16 @@ const useBoundAction = (action, actionParamsSignal) => {
|
|
|
13449
13727
|
},
|
|
13450
13728
|
);
|
|
13451
13729
|
if (!actionParamsSignal) {
|
|
13452
|
-
|
|
13730
|
+
actionFromFunctionRef.current = actionFromFunction;
|
|
13453
13731
|
return actionFromFunction;
|
|
13454
13732
|
}
|
|
13455
13733
|
const actionBoundToParams =
|
|
13456
13734
|
actionFromFunction.bindParams(actionParamsSignal);
|
|
13457
|
-
|
|
13735
|
+
actionFromFunctionRef.current = actionBoundToParams;
|
|
13458
13736
|
return actionBoundToParams;
|
|
13459
13737
|
}
|
|
13738
|
+
actionFromFunctionRef.current = undefined;
|
|
13739
|
+
actionCallbackRef.current = undefined;
|
|
13460
13740
|
if (actionParamsSignal) {
|
|
13461
13741
|
return action.bindParams(actionParamsSignal);
|
|
13462
13742
|
}
|
|
@@ -21508,6 +21788,24 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21508
21788
|
// the single door every navigation goes through, rather than by each caller
|
|
21509
21789
|
// (navBack's fallback in particular arrives here raw).
|
|
21510
21790
|
const url = new URL(target, window.location.href).href;
|
|
21791
|
+
// An address that only sends elsewhere never becomes anything here: asked
|
|
21792
|
+
// before the announcement, before the history write and before the routes,
|
|
21793
|
+
// so that what follows is entirely about where the reader is going (see
|
|
21794
|
+
// resolveRouteRedirection).
|
|
21795
|
+
const redirectionUrl = resolveRouteRedirection(url);
|
|
21796
|
+
if (redirectionUrl) {
|
|
21797
|
+
if (redirectionUrl === window.location.href) {
|
|
21798
|
+
// Asked to go where we already are: an address that redirects is never
|
|
21799
|
+
// the one being displayed, so there is nothing to go to and nothing to
|
|
21800
|
+
// stack on the history.
|
|
21801
|
+
return undefined;
|
|
21802
|
+
}
|
|
21803
|
+
return handleRoutingTask(redirectionUrl, {
|
|
21804
|
+
...options,
|
|
21805
|
+
reason: `${options.reason} (redirected from ${url})`,
|
|
21806
|
+
redirected: true,
|
|
21807
|
+
});
|
|
21808
|
+
}
|
|
21511
21809
|
// Decided before anything is announced: an elided push IS the traversal it
|
|
21512
21810
|
// becomes, and the traversal will make its own announcements when the
|
|
21513
21811
|
// browser answers — a before/after cycle here would be about a navigation
|
|
@@ -21556,6 +21854,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21556
21854
|
reason,
|
|
21557
21855
|
navigationType, // "load", "reload", "replace", "push", "traverse"
|
|
21558
21856
|
state,
|
|
21857
|
+
redirected,
|
|
21559
21858
|
} = options;
|
|
21560
21859
|
|
|
21561
21860
|
// Where the entry being reached stands in this document's own stack —
|
|
@@ -21598,6 +21897,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
21598
21897
|
} else {
|
|
21599
21898
|
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
21600
21899
|
markUrlAsVisited(url);
|
|
21900
|
+
if (redirected) {
|
|
21901
|
+
// The entry the browser is on names an address that only sends
|
|
21902
|
+
// elsewhere — a cold load on it, or a back into it. Written over where
|
|
21903
|
+
// it stands (the entry keeps its place in the stack, hence its state
|
|
21904
|
+
// and the depth in it): pressing back must not walk into it again.
|
|
21905
|
+
window.history.replaceState(state, null, url);
|
|
21906
|
+
rememberEntryIsOfThisDocument();
|
|
21907
|
+
}
|
|
21601
21908
|
updateDocumentUrl(url);
|
|
21602
21909
|
updateDocumentState(state);
|
|
21603
21910
|
}
|
|
@@ -34602,7 +34909,7 @@ const describeRangeAsked = (rangeParams) => {
|
|
|
34602
34909
|
};
|
|
34603
34910
|
|
|
34604
34911
|
const resourceLifecycleManager = createResourceLifecycleManager();
|
|
34605
|
-
const debug$2 = (args) => {
|
|
34912
|
+
const debug$2 = (...args) => {
|
|
34606
34913
|
{
|
|
34607
34914
|
return;
|
|
34608
34915
|
}
|
|
@@ -34667,6 +34974,7 @@ const resource = (
|
|
|
34667
34974
|
DELETE_MANY,
|
|
34668
34975
|
} = {},
|
|
34669
34976
|
) => {
|
|
34977
|
+
const declarationSite = getDeclarationSite();
|
|
34670
34978
|
if (idKey === undefined) {
|
|
34671
34979
|
idKey = uniqueKeys.length === 0 ? "id" : uniqueKeys[0];
|
|
34672
34980
|
}
|
|
@@ -34715,6 +35023,7 @@ const resource = (
|
|
|
34715
35023
|
const createRestActionForRoot = createRestActionFactoryForRoot(name, {
|
|
34716
35024
|
idKey,
|
|
34717
35025
|
store,
|
|
35026
|
+
declarationSite,
|
|
34718
35027
|
});
|
|
34719
35028
|
return createResource(name, {
|
|
34720
35029
|
idKey,
|
|
@@ -34753,11 +35062,8 @@ const createResource = (
|
|
|
34753
35062
|
paramScope,
|
|
34754
35063
|
rerunOn,
|
|
34755
35064
|
dependencies,
|
|
34756
|
-
}
|
|
35065
|
+
},
|
|
34757
35066
|
) => {
|
|
34758
|
-
if (idKey === undefined) {
|
|
34759
|
-
idKey = uniqueKeys.length === 0 ? "id" : uniqueKeys[0];
|
|
34760
|
-
}
|
|
34761
35067
|
const params = paramScope.params;
|
|
34762
35068
|
const stateFacade = {
|
|
34763
35069
|
// public
|
|
@@ -34778,7 +35084,6 @@ const createResource = (
|
|
|
34778
35084
|
store,
|
|
34779
35085
|
addItemSetup,
|
|
34780
35086
|
};
|
|
34781
|
-
const lifecycleCtx = { onComplete: null };
|
|
34782
35087
|
|
|
34783
35088
|
resourceLifecycleManager.registerResource(stateFacade, {
|
|
34784
35089
|
rerunOn,
|
|
@@ -34786,7 +35091,7 @@ const createResource = (
|
|
|
34786
35091
|
dependencies,
|
|
34787
35092
|
uniqueKeys,
|
|
34788
35093
|
});
|
|
34789
|
-
|
|
35094
|
+
const onActionComplete = (actionCompleted) => {
|
|
34790
35095
|
resourceLifecycleManager.onActionComplete(actionCompleted, {
|
|
34791
35096
|
resourceScope: stateFacade,
|
|
34792
35097
|
});
|
|
@@ -34823,6 +35128,7 @@ const createResource = (
|
|
|
34823
35128
|
paramsToInject,
|
|
34824
35129
|
{ dependencies: withParamsDeps, rerunOn: withParamsRerunOn } = {},
|
|
34825
35130
|
) => {
|
|
35131
|
+
const declarationSite = getDeclarationSite();
|
|
34826
35132
|
if (!paramsToInject || Object.keys(paramsToInject).length === 0) {
|
|
34827
35133
|
throw new Error(`resource(${name}).withParams() requires parameters`);
|
|
34828
35134
|
}
|
|
@@ -34833,6 +35139,7 @@ const createResource = (
|
|
|
34833
35139
|
const createRestActionWithParams = createRestActionFactoryForRoot(name, {
|
|
34834
35140
|
idKey,
|
|
34835
35141
|
store,
|
|
35142
|
+
declarationSite,
|
|
34836
35143
|
});
|
|
34837
35144
|
return createResource(name, {
|
|
34838
35145
|
idKey,
|
|
@@ -34885,40 +35192,37 @@ const createResource = (
|
|
|
34885
35192
|
DELETE,
|
|
34886
35193
|
} = {},
|
|
34887
35194
|
) => {
|
|
35195
|
+
const declarationSite = getDeclarationSite();
|
|
34888
35196
|
const childName = `${name}.${propertyName}`;
|
|
35197
|
+
const childIdKey = childResource.idKey;
|
|
35198
|
+
const childStore = childResource.store;
|
|
34889
35199
|
addItemSetup((item) => {
|
|
34890
|
-
const childIdKeyForSetup = childResource.idKey;
|
|
34891
35200
|
const childItemIdSignal = signal();
|
|
34892
35201
|
const updateChildItemId = (value) => {
|
|
34893
35202
|
const currentChildItemId = childItemIdSignal.peek();
|
|
35203
|
+
let childItemProps;
|
|
34894
35204
|
if (isProps(value)) {
|
|
34895
|
-
|
|
34896
|
-
|
|
34897
|
-
|
|
34898
|
-
|
|
34899
|
-
|
|
34900
|
-
childItemIdSignal.value = childItemId;
|
|
34901
|
-
return true;
|
|
34902
|
-
}
|
|
34903
|
-
if (primitiveCanBeId(value)) {
|
|
34904
|
-
const childItemProps = { [childIdKeyForSetup]: value };
|
|
34905
|
-
const childItem = childResource.store.upsert(childItemProps);
|
|
34906
|
-
const childItemId = childItem[childIdKeyForSetup];
|
|
34907
|
-
if (currentChildItemId === childItemId) {
|
|
35205
|
+
childItemProps = value;
|
|
35206
|
+
} else if (primitiveCanBeId(value)) {
|
|
35207
|
+
childItemProps = { [childIdKey]: value };
|
|
35208
|
+
} else {
|
|
35209
|
+
if (currentChildItemId === undefined) {
|
|
34908
35210
|
return false;
|
|
34909
35211
|
}
|
|
34910
|
-
childItemIdSignal.value =
|
|
35212
|
+
childItemIdSignal.value = undefined;
|
|
34911
35213
|
return true;
|
|
34912
35214
|
}
|
|
34913
|
-
|
|
35215
|
+
const childItem = childStore.upsert(childItemProps);
|
|
35216
|
+
const childItemId = childItem[childIdKey];
|
|
35217
|
+
if (currentChildItemId === childItemId) {
|
|
34914
35218
|
return false;
|
|
34915
35219
|
}
|
|
34916
|
-
childItemIdSignal.value =
|
|
35220
|
+
childItemIdSignal.value = childItemId;
|
|
34917
35221
|
return true;
|
|
34918
35222
|
};
|
|
34919
35223
|
updateChildItemId(item[propertyName]);
|
|
34920
35224
|
const childItemSignal = computed(() =>
|
|
34921
|
-
|
|
35225
|
+
childStore.select(childItemIdSignal.value),
|
|
34922
35226
|
);
|
|
34923
35227
|
const childItemFacadeSignal = computed(() => {
|
|
34924
35228
|
const childItem = childItemSignal.value;
|
|
@@ -34949,9 +35253,7 @@ const createResource = (
|
|
|
34949
35253
|
);
|
|
34950
35254
|
});
|
|
34951
35255
|
|
|
34952
|
-
const
|
|
34953
|
-
const childStore = childResource.store;
|
|
34954
|
-
const createRestActionForOne = (verb, callback, { lifecycleCtx }) => {
|
|
35256
|
+
const createRestActionForOne = (verb, callback, { onActionComplete }) => {
|
|
34955
35257
|
const applyResultToValue =
|
|
34956
35258
|
verb === "DELETE"
|
|
34957
35259
|
? (itemId) => {
|
|
@@ -34963,33 +35265,18 @@ const createResource = (
|
|
|
34963
35265
|
});
|
|
34964
35266
|
return childItemId;
|
|
34965
35267
|
}
|
|
34966
|
-
: //
|
|
34967
|
-
//
|
|
34968
|
-
// [idKey]: 123,
|
|
34969
|
-
// [propertyName]: {
|
|
34970
|
-
// [childIdKey]: 456, ...childProps
|
|
34971
|
-
// }
|
|
34972
|
-
// }
|
|
34973
|
-
// the following could happen too if there is no relationship
|
|
34974
|
-
// {
|
|
34975
|
-
// [idKey]: 123,
|
|
34976
|
-
// [propertyName]: null
|
|
34977
|
-
// }
|
|
35268
|
+
: // GET/PUT contract (see .one() JSDoc): the parent object with the
|
|
35269
|
+
// relationship nested inside, or null for no relationship.
|
|
34978
35270
|
(result) => {
|
|
34979
35271
|
const item = store.upsert(result);
|
|
34980
35272
|
const childItem = item[propertyName];
|
|
34981
|
-
|
|
34982
|
-
return childItemId;
|
|
35273
|
+
return childItem ? childItem[childIdKey] : undefined;
|
|
34983
35274
|
};
|
|
34984
|
-
|
|
34985
|
-
|
|
34986
|
-
|
|
34987
|
-
|
|
34988
|
-
|
|
34989
|
-
: callerInfo.raw || "unknown location";
|
|
34990
|
-
const originalActionName = `${name}.${verb}`;
|
|
34991
|
-
|
|
34992
|
-
const actionAffectingOneItem = createAction(callback, {
|
|
35275
|
+
const throwInvalidResult = createInvalidResultThrower(
|
|
35276
|
+
`${name}.${verb}`,
|
|
35277
|
+
declarationSite,
|
|
35278
|
+
);
|
|
35279
|
+
return createAction(callback, {
|
|
34993
35280
|
meta: {
|
|
34994
35281
|
verb,
|
|
34995
35282
|
isMany: false,
|
|
@@ -34997,35 +35284,30 @@ const createResource = (
|
|
|
34997
35284
|
},
|
|
34998
35285
|
name: `${name}.${verb}`,
|
|
34999
35286
|
resultToValue: (result, action) => {
|
|
35000
|
-
const actionLabel = action.name;
|
|
35001
|
-
|
|
35002
35287
|
if (verb === "DELETE") {
|
|
35003
35288
|
if (!isProps(result) && !primitiveCanBeId(result)) {
|
|
35004
|
-
|
|
35005
|
-
|
|
35006
|
-
|
|
35289
|
+
throwInvalidResult(
|
|
35290
|
+
action.name,
|
|
35291
|
+
`an object (that will be used to drop "${name}" resource)`,
|
|
35292
|
+
result,
|
|
35007
35293
|
);
|
|
35008
35294
|
}
|
|
35009
|
-
|
|
35010
|
-
|
|
35011
|
-
|
|
35012
|
-
|
|
35013
|
-
|
|
35014
|
-
${originalActionName} source location: ${locationInfo}`,
|
|
35295
|
+
} else if (!isProps(result)) {
|
|
35296
|
+
throwInvalidResult(
|
|
35297
|
+
action.name,
|
|
35298
|
+
`an object (that will be used to upsert "${name}" resource)`,
|
|
35299
|
+
result,
|
|
35015
35300
|
);
|
|
35016
35301
|
}
|
|
35017
35302
|
return applyResultToValue(result);
|
|
35018
35303
|
},
|
|
35019
35304
|
valueToData: (childItemId) => childStore.select(childItemId),
|
|
35020
|
-
completeSideEffect:
|
|
35021
|
-
lifecycleCtx.onComplete(actionCompleted);
|
|
35022
|
-
},
|
|
35305
|
+
completeSideEffect: onActionComplete,
|
|
35023
35306
|
});
|
|
35024
|
-
return actionAffectingOneItem;
|
|
35025
35307
|
};
|
|
35026
35308
|
|
|
35027
35309
|
return createResource(childName, {
|
|
35028
|
-
idKey:
|
|
35310
|
+
idKey: childIdKey,
|
|
35029
35311
|
restCallbacks: {
|
|
35030
35312
|
GET,
|
|
35031
35313
|
PUT,
|
|
@@ -35082,49 +35364,21 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35082
35364
|
DELETE_MANY,
|
|
35083
35365
|
} = {},
|
|
35084
35366
|
) => {
|
|
35367
|
+
const declarationSite = getDeclarationSite();
|
|
35085
35368
|
const childStore = childResource.store;
|
|
35086
35369
|
const childIdKey = childResource.idKey;
|
|
35087
35370
|
const childName = `${name}.${propertyName}`;
|
|
35088
35371
|
addItemSetup((item) => {
|
|
35089
35372
|
const childItemIdArraySignal = signal([]);
|
|
35090
|
-
const updateChildItemIdArray = (
|
|
35091
|
-
|
|
35092
|
-
|
|
35093
|
-
|
|
35094
|
-
|
|
35095
|
-
return;
|
|
35096
|
-
}
|
|
35097
|
-
let i = 0;
|
|
35098
|
-
const idArray = [];
|
|
35099
|
-
let modified = false;
|
|
35100
|
-
while (i < valueArray.length) {
|
|
35101
|
-
const value = valueArray[i];
|
|
35102
|
-
const currentIdAtIndex = currentIdArray[idArray.length];
|
|
35103
|
-
i++;
|
|
35104
|
-
if (isProps(value)) {
|
|
35105
|
-
const childItem = childResource.store.upsert(value);
|
|
35106
|
-
const childItemId = childItem[childIdKey];
|
|
35107
|
-
if (currentIdAtIndex !== childItemId) modified = true;
|
|
35108
|
-
idArray.push(childItemId);
|
|
35109
|
-
continue;
|
|
35110
|
-
}
|
|
35111
|
-
if (primitiveCanBeId(value)) {
|
|
35112
|
-
const childItemProps = { [childIdKey]: value };
|
|
35113
|
-
const childItem = childResource.store.upsert(childItemProps);
|
|
35114
|
-
const childItemId = childItem[childIdKey];
|
|
35115
|
-
if (currentIdAtIndex !== childItemId) modified = true;
|
|
35116
|
-
idArray.push(childItemId);
|
|
35117
|
-
continue;
|
|
35118
|
-
}
|
|
35119
|
-
}
|
|
35120
|
-
if (modified || currentIdArray.length !== idArray.length) {
|
|
35121
|
-
childItemIdArraySignal.value = idArray;
|
|
35122
|
-
}
|
|
35123
|
-
};
|
|
35373
|
+
const updateChildItemIdArray = createChildIdArrayUpdater(
|
|
35374
|
+
childStore,
|
|
35375
|
+
childIdKey,
|
|
35376
|
+
childItemIdArraySignal,
|
|
35377
|
+
);
|
|
35124
35378
|
updateChildItemIdArray(item[propertyName]);
|
|
35125
35379
|
const childItemArraySignal = computed(() => {
|
|
35126
35380
|
const idArray = childItemIdArraySignal.value;
|
|
35127
|
-
const arr =
|
|
35381
|
+
const arr = childStore.selectAll(idArray);
|
|
35128
35382
|
Object.defineProperty(arr, SYMBOL_OBJECT_SIGNAL, {
|
|
35129
35383
|
value: childItemArraySignal,
|
|
35130
35384
|
writable: false,
|
|
@@ -35137,23 +35391,27 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35137
35391
|
get: () => childItemArraySignal.value,
|
|
35138
35392
|
set: updateChildItemIdArray,
|
|
35139
35393
|
});
|
|
35140
|
-
syncIdArrayOnRename(
|
|
35141
|
-
childResource.store,
|
|
35142
|
-
childIdKey,
|
|
35143
|
-
childItemIdArraySignal,
|
|
35144
|
-
);
|
|
35394
|
+
syncIdArrayOnRename(childStore, childIdKey, childItemIdArraySignal);
|
|
35145
35395
|
});
|
|
35146
35396
|
const createRestActionForMany = (
|
|
35147
35397
|
verb,
|
|
35148
35398
|
callback,
|
|
35149
|
-
{ isMany,
|
|
35399
|
+
{ isMany, onActionComplete },
|
|
35150
35400
|
) => {
|
|
35151
35401
|
if (!isMany) {
|
|
35152
|
-
return createRestActionAffectingOneItem(verb, callback,
|
|
35402
|
+
return createRestActionAffectingOneItem(verb, callback, {
|
|
35403
|
+
onActionComplete,
|
|
35404
|
+
});
|
|
35153
35405
|
}
|
|
35154
|
-
return createRestActionAffectingManyItems(verb, callback,
|
|
35406
|
+
return createRestActionAffectingManyItems(verb, callback, {
|
|
35407
|
+
onActionComplete,
|
|
35408
|
+
});
|
|
35155
35409
|
};
|
|
35156
|
-
const createRestActionAffectingOneItem = (
|
|
35410
|
+
const createRestActionAffectingOneItem = (
|
|
35411
|
+
verb,
|
|
35412
|
+
callback,
|
|
35413
|
+
{ onActionComplete },
|
|
35414
|
+
) => {
|
|
35157
35415
|
const applyResultToValue =
|
|
35158
35416
|
verb === "DELETE"
|
|
35159
35417
|
? ([itemId, childItemId]) => {
|
|
@@ -35162,8 +35420,7 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35162
35420
|
const childItemArrayWithoutThisOne = [];
|
|
35163
35421
|
let found = false;
|
|
35164
35422
|
for (const childItemCandidate of childItemArray) {
|
|
35165
|
-
|
|
35166
|
-
if (childItemCandidateId === childItemId) {
|
|
35423
|
+
if (childItemCandidate[childIdKey] === childItemId) {
|
|
35167
35424
|
found = true;
|
|
35168
35425
|
} else {
|
|
35169
35426
|
childItemArrayWithoutThisOne.push(childItemCandidate);
|
|
@@ -35178,162 +35435,127 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35178
35435
|
return childItemId;
|
|
35179
35436
|
}
|
|
35180
35437
|
: (childData) => {
|
|
35438
|
+
// an array is [property, value, props], used to rename the child id
|
|
35181
35439
|
const childItem = Array.isArray(childData)
|
|
35182
35440
|
? childStore.upsert(...childData)
|
|
35183
35441
|
: childStore.upsert(childData);
|
|
35184
|
-
|
|
35185
|
-
return childItemId;
|
|
35442
|
+
return childItem[childIdKey];
|
|
35186
35443
|
};
|
|
35187
|
-
|
|
35188
|
-
|
|
35189
|
-
|
|
35190
|
-
|
|
35191
|
-
|
|
35192
|
-
: callerInfo.raw || "unknown location";
|
|
35193
|
-
const originalActionName = `${name}.${verb}`;
|
|
35194
|
-
|
|
35195
|
-
const actionAffectingOneItem = createAction(callback, {
|
|
35444
|
+
const throwInvalidResult = createInvalidResultThrower(
|
|
35445
|
+
`${name}.${verb}`,
|
|
35446
|
+
declarationSite,
|
|
35447
|
+
);
|
|
35448
|
+
return createAction(callback, {
|
|
35196
35449
|
meta: { verb, isMany: false, paramScope },
|
|
35197
35450
|
name: `${name}.${verb}`,
|
|
35198
35451
|
resultToValue: (result, action) => {
|
|
35199
|
-
const actionLabel = action.name;
|
|
35200
|
-
|
|
35201
35452
|
if (verb === "DELETE") {
|
|
35202
35453
|
if (!Array.isArray(result) || result.length !== 2) {
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35454
|
+
throwInvalidResult(
|
|
35455
|
+
action.name,
|
|
35456
|
+
`an array [itemId, childItemId] (that will be used to remove relationship)`,
|
|
35457
|
+
result,
|
|
35206
35458
|
);
|
|
35207
35459
|
}
|
|
35208
|
-
|
|
35209
|
-
|
|
35210
|
-
|
|
35211
|
-
|
|
35212
|
-
|
|
35213
|
-
${originalActionName} source location: ${locationInfo}`,
|
|
35460
|
+
} else if (!isProps(result)) {
|
|
35461
|
+
throwInvalidResult(
|
|
35462
|
+
action.name,
|
|
35463
|
+
`an object (that will be used to upsert child item)`,
|
|
35464
|
+
result,
|
|
35214
35465
|
);
|
|
35215
35466
|
}
|
|
35216
35467
|
return applyResultToValue(result);
|
|
35217
35468
|
},
|
|
35218
35469
|
valueToData: (childItemId) => childStore.select(childItemId),
|
|
35219
|
-
completeSideEffect:
|
|
35220
|
-
lifecycleCtx.onComplete(actionCompleted);
|
|
35221
|
-
},
|
|
35470
|
+
completeSideEffect: onActionComplete,
|
|
35222
35471
|
});
|
|
35223
|
-
return actionAffectingOneItem;
|
|
35224
35472
|
};
|
|
35225
35473
|
const createRestActionAffectingManyItems = (
|
|
35226
35474
|
verb,
|
|
35227
35475
|
callback,
|
|
35228
|
-
|
|
35476
|
+
{ onActionComplete },
|
|
35229
35477
|
) => {
|
|
35230
35478
|
const applyResultToValue =
|
|
35231
35479
|
verb === "GET"
|
|
35232
35480
|
? (result) => {
|
|
35233
|
-
//
|
|
35234
|
-
//
|
|
35235
|
-
// [idKey]: 123,
|
|
35236
|
-
// [propertyName]: [
|
|
35237
|
-
// { [childIdKey]: 456, ...childProps },
|
|
35238
|
-
// { [childIdKey]: 789, ...childProps },
|
|
35239
|
-
// ...
|
|
35240
|
-
// ]
|
|
35241
|
-
// }
|
|
35242
|
-
// the array can be empty
|
|
35481
|
+
// GET_MANY contract (see .many() JSDoc): the parent object with
|
|
35482
|
+
// the child array nested inside; the array replaces the relationship.
|
|
35243
35483
|
const item = store.upsert(result);
|
|
35244
35484
|
const childItemArray = item[propertyName];
|
|
35245
|
-
|
|
35246
|
-
(childItem) => childItem[childIdKey],
|
|
35247
|
-
);
|
|
35248
|
-
return childItemIdArray;
|
|
35485
|
+
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
35249
35486
|
}
|
|
35250
35487
|
: verb === "DELETE"
|
|
35251
35488
|
? ([itemIdOrMutableId, childItemIdOrMutableIdArray]) => {
|
|
35252
35489
|
const item = store.select(itemIdOrMutableId);
|
|
35253
35490
|
const childItemArray = item[propertyName];
|
|
35254
|
-
const
|
|
35255
|
-
|
|
35256
|
-
let someFound = false;
|
|
35257
|
-
const deletedChildItemArray = childStore.select(
|
|
35258
|
-
childItemIdOrMutableIdArray,
|
|
35491
|
+
const deletedChildItemSet = new Set(
|
|
35492
|
+
childStore.selectAll(childItemIdOrMutableIdArray),
|
|
35259
35493
|
);
|
|
35494
|
+
const deletedChildItemIdArray = [];
|
|
35495
|
+
const childItemArrayWithoutThose = [];
|
|
35260
35496
|
for (const childItemCandidate of childItemArray) {
|
|
35261
|
-
if (
|
|
35262
|
-
someFound = true;
|
|
35497
|
+
if (deletedChildItemSet.has(childItemCandidate)) {
|
|
35263
35498
|
deletedChildItemIdArray.push(
|
|
35264
35499
|
childItemCandidate[childIdKey],
|
|
35265
35500
|
);
|
|
35266
35501
|
} else {
|
|
35267
|
-
|
|
35502
|
+
childItemArrayWithoutThose.push(childItemCandidate);
|
|
35268
35503
|
}
|
|
35269
35504
|
}
|
|
35270
|
-
if (
|
|
35505
|
+
if (deletedChildItemIdArray.length > 0) {
|
|
35271
35506
|
store.upsert({
|
|
35272
35507
|
[idKey]: item[idKey],
|
|
35273
|
-
[propertyName]:
|
|
35508
|
+
[propertyName]: childItemArrayWithoutThose,
|
|
35274
35509
|
});
|
|
35275
35510
|
}
|
|
35276
35511
|
return deletedChildItemIdArray;
|
|
35277
35512
|
}
|
|
35278
35513
|
: (childDataArray) => {
|
|
35279
35514
|
const childItemArray = childStore.upsert(childDataArray);
|
|
35280
|
-
|
|
35281
|
-
(childItem) => childItem[childIdKey],
|
|
35282
|
-
);
|
|
35283
|
-
return childItemIdArray;
|
|
35515
|
+
return childItemArray.map((childItem) => childItem[childIdKey]);
|
|
35284
35516
|
};
|
|
35285
|
-
|
|
35286
|
-
|
|
35287
|
-
|
|
35288
|
-
|
|
35289
|
-
|
|
35290
|
-
: callerInfo.raw || "unknown location";
|
|
35291
|
-
const originalActionName = `${name}.${verb}[many]`;
|
|
35292
|
-
|
|
35293
|
-
const actionAffectingManyItem = createAction(callback, {
|
|
35517
|
+
const throwInvalidResult = createInvalidResultThrower(
|
|
35518
|
+
`${name}.${verb}[many]`,
|
|
35519
|
+
declarationSite,
|
|
35520
|
+
);
|
|
35521
|
+
return createAction(callback, {
|
|
35294
35522
|
meta: { verb, isMany: true, paramScope },
|
|
35295
35523
|
name: `${name}.${verb}[many]`,
|
|
35296
35524
|
dataDefault: [],
|
|
35297
35525
|
resultToValue: (result, action) => {
|
|
35298
|
-
const actionLabel = action.name;
|
|
35299
|
-
|
|
35300
35526
|
if (verb === "GET") {
|
|
35301
35527
|
if (!isProps(result)) {
|
|
35302
|
-
|
|
35303
|
-
|
|
35304
|
-
${
|
|
35528
|
+
throwInvalidResult(
|
|
35529
|
+
action.name,
|
|
35530
|
+
`an object (that will be used to upsert "${name}" resource with many relationships)`,
|
|
35531
|
+
result,
|
|
35305
35532
|
);
|
|
35306
35533
|
}
|
|
35307
|
-
|
|
35308
|
-
}
|
|
35309
|
-
if (verb === "DELETE") {
|
|
35534
|
+
} else if (verb === "DELETE") {
|
|
35310
35535
|
if (
|
|
35311
35536
|
!Array.isArray(result) ||
|
|
35312
35537
|
result.length !== 2 ||
|
|
35313
35538
|
!Array.isArray(result[1])
|
|
35314
35539
|
) {
|
|
35315
|
-
|
|
35316
|
-
|
|
35317
|
-
|
|
35540
|
+
throwInvalidResult(
|
|
35541
|
+
action.name,
|
|
35542
|
+
`an array [itemId, childItemIdArray] (that will be used to remove relationships)`,
|
|
35543
|
+
result,
|
|
35318
35544
|
);
|
|
35319
35545
|
}
|
|
35320
|
-
|
|
35321
|
-
|
|
35322
|
-
|
|
35323
|
-
|
|
35324
|
-
|
|
35325
|
-
${originalActionName} source location: ${locationInfo}`,
|
|
35546
|
+
} else if (!Array.isArray(result)) {
|
|
35547
|
+
throwInvalidResult(
|
|
35548
|
+
action.name,
|
|
35549
|
+
`an array of objects (that will be used to upsert child items)`,
|
|
35550
|
+
result,
|
|
35326
35551
|
);
|
|
35327
35552
|
}
|
|
35328
35553
|
return applyResultToValue(result);
|
|
35329
35554
|
},
|
|
35330
35555
|
valueToData: (childItemIdArray) =>
|
|
35331
35556
|
childStore.selectAll(childItemIdArray),
|
|
35332
|
-
completeSideEffect:
|
|
35333
|
-
lifecycleCtx.onComplete(actionCompleted);
|
|
35334
|
-
},
|
|
35557
|
+
completeSideEffect: onActionComplete,
|
|
35335
35558
|
});
|
|
35336
|
-
return actionAffectingManyItem;
|
|
35337
35559
|
};
|
|
35338
35560
|
|
|
35339
35561
|
return createResource(childName, {
|
|
@@ -35398,26 +35620,20 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35398
35620
|
) => {
|
|
35399
35621
|
const childName = `${name}.${propertyName}`;
|
|
35400
35622
|
|
|
35401
|
-
//
|
|
35402
|
-
//
|
|
35403
|
-
const
|
|
35404
|
-
const childAddItemSetup = (callback) =>
|
|
35405
|
-
|
|
35406
|
-
const scopedItemMap = new Map(); // ownerId → stable child item object
|
|
35407
|
-
const scopedSignalMap = new Map(); // ownerId → signal<childItem | null>
|
|
35623
|
+
// Callbacks added by chained .one()/.many() on the child resource,
|
|
35624
|
+
// applied to each per-owner child object when it is first created.
|
|
35625
|
+
const childSetupCallbackSet = new Set();
|
|
35626
|
+
const childAddItemSetup = (callback) => childSetupCallbackSet.add(callback);
|
|
35627
|
+
const applyPropsMap = new Map(); // ownerId → applyProps(props | null)
|
|
35408
35628
|
addItemSetup((ownerItem) => {
|
|
35409
35629
|
const ownerId = ownerItem[idKey];
|
|
35410
|
-
//
|
|
35411
|
-
//
|
|
35412
|
-
// so they survive across multiple prop updates.
|
|
35630
|
+
// A stable child object, mutated in place: reactive getters/setters from
|
|
35631
|
+
// chained .one() etc. are defined on it once and survive prop updates.
|
|
35413
35632
|
const childItem = {};
|
|
35414
|
-
for (const childSetup of
|
|
35633
|
+
for (const childSetup of childSetupCallbackSet) {
|
|
35415
35634
|
childSetup(childItem);
|
|
35416
35635
|
}
|
|
35417
|
-
scopedItemMap.set(ownerId, childItem);
|
|
35418
35636
|
const childSignal = signal(null);
|
|
35419
|
-
scopedSignalMap.set(ownerId, childSignal);
|
|
35420
|
-
|
|
35421
35637
|
const applyProps = (props) => {
|
|
35422
35638
|
if (!props) {
|
|
35423
35639
|
childSignal.value = null;
|
|
@@ -35431,6 +35647,7 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35431
35647
|
childSignal.value = childItem; // first activation: null → childItem
|
|
35432
35648
|
}
|
|
35433
35649
|
};
|
|
35650
|
+
applyPropsMap.set(ownerId, applyProps);
|
|
35434
35651
|
|
|
35435
35652
|
applyProps(ownerItem[propertyName]);
|
|
35436
35653
|
|
|
@@ -35439,9 +35656,13 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35439
35656
|
set: applyProps,
|
|
35440
35657
|
});
|
|
35441
35658
|
});
|
|
35442
|
-
const createRestActionForScopedOne = (
|
|
35659
|
+
const createRestActionForScopedOne = (
|
|
35660
|
+
verb,
|
|
35661
|
+
callback,
|
|
35662
|
+
{ onActionComplete },
|
|
35663
|
+
) => {
|
|
35443
35664
|
const childActionName = `${childName}.${verb}`;
|
|
35444
|
-
|
|
35665
|
+
return createAction(callback, {
|
|
35445
35666
|
name: childActionName,
|
|
35446
35667
|
meta: { verb, isMany: false, paramScope },
|
|
35447
35668
|
resultToValue: (result) => {
|
|
@@ -35458,33 +35679,20 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35458
35679
|
uniqueKeys,
|
|
35459
35680
|
childActionName,
|
|
35460
35681
|
);
|
|
35461
|
-
const
|
|
35462
|
-
if (!
|
|
35682
|
+
const applyProps = applyPropsMap.get(ownerId);
|
|
35683
|
+
if (!applyProps) {
|
|
35463
35684
|
throw new Error(
|
|
35464
35685
|
`${childActionName}: no item found for scope id "${ownerId}"`,
|
|
35465
35686
|
);
|
|
35466
35687
|
}
|
|
35467
|
-
|
|
35468
|
-
if (props) {
|
|
35469
|
-
for (const [key, value] of Object.entries(props)) {
|
|
35470
|
-
childItem[key] = value;
|
|
35471
|
-
}
|
|
35472
|
-
if (childSignal.peek() !== childItem) {
|
|
35473
|
-
childSignal.value = childItem;
|
|
35474
|
-
}
|
|
35475
|
-
} else {
|
|
35476
|
-
childSignal.value = null;
|
|
35477
|
-
}
|
|
35688
|
+
applyProps(props);
|
|
35478
35689
|
return [ownerId, props];
|
|
35479
35690
|
},
|
|
35480
|
-
completeSideEffect:
|
|
35481
|
-
lifecycleCtx.onComplete(actionCompleted);
|
|
35482
|
-
},
|
|
35691
|
+
completeSideEffect: onActionComplete,
|
|
35483
35692
|
});
|
|
35484
|
-
return restAction;
|
|
35485
35693
|
};
|
|
35486
35694
|
|
|
35487
|
-
|
|
35695
|
+
return createResource(childName, {
|
|
35488
35696
|
idKey: childIdKey,
|
|
35489
35697
|
restCallbacks: {
|
|
35490
35698
|
GET,
|
|
@@ -35500,7 +35708,6 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35500
35708
|
rerunOn: scopedOneRerunOn ?? rerunOn,
|
|
35501
35709
|
dependencies: scopedOneDependencies ?? dependencies,
|
|
35502
35710
|
});
|
|
35503
|
-
return childResource;
|
|
35504
35711
|
};
|
|
35505
35712
|
|
|
35506
35713
|
/**
|
|
@@ -35554,108 +35761,76 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35554
35761
|
) => {
|
|
35555
35762
|
const childName = `${name}.${propertyName}`;
|
|
35556
35763
|
|
|
35557
|
-
//
|
|
35558
|
-
//
|
|
35764
|
+
// Callbacks added by chained .one()/.many() on the child resource,
|
|
35765
|
+
// applied to each child item created in a per-owner store.
|
|
35559
35766
|
const childSetupCallbackSet = new Set();
|
|
35560
35767
|
const childAddItemSetup = (callback) => childSetupCallbackSet.add(callback);
|
|
35561
|
-
|
|
35562
|
-
|
|
35768
|
+
// ownerKey (id or any uniqueKey value) → { childStore, idArraySignal }
|
|
35769
|
+
// One owner can be registered under several keys, all pointing to the same scope.
|
|
35770
|
+
const scopeMap = new Map();
|
|
35771
|
+
const createScope = (ownerKey) => {
|
|
35772
|
+
const childStore = arraySignalStore([], childIdKey, {
|
|
35773
|
+
name: `${childName}#${ownerKey} store`,
|
|
35774
|
+
createItem: (props) => {
|
|
35775
|
+
const childItem = {};
|
|
35776
|
+
Object.assign(childItem, props);
|
|
35777
|
+
for (const childSetup of childSetupCallbackSet) {
|
|
35778
|
+
childSetup(childItem);
|
|
35779
|
+
}
|
|
35780
|
+
return childItem;
|
|
35781
|
+
},
|
|
35782
|
+
});
|
|
35783
|
+
const scope = { childStore, idArraySignal: signal([]) };
|
|
35784
|
+
scopeMap.set(ownerKey, scope);
|
|
35785
|
+
return scope;
|
|
35786
|
+
};
|
|
35563
35787
|
addItemSetup((item) => {
|
|
35564
35788
|
const ownerId = item[idKey];
|
|
35565
35789
|
|
|
35566
|
-
// Reuse an existing
|
|
35567
|
-
// (e.g. rows were fetched by tablename before the full table was loaded).
|
|
35568
|
-
let
|
|
35569
|
-
|
|
35570
|
-
if (!childStore) {
|
|
35790
|
+
// Reuse an existing scope if one was already created under a uniqueKey
|
|
35791
|
+
// value (e.g. rows were fetched by tablename before the full table was loaded).
|
|
35792
|
+
let scope = scopeMap.get(ownerId);
|
|
35793
|
+
if (!scope) {
|
|
35571
35794
|
for (const uniqueKey of uniqueKeys) {
|
|
35572
35795
|
const uniqueKeyValue = item[uniqueKey];
|
|
35573
|
-
if (uniqueKeyValue !== undefined) {
|
|
35574
|
-
|
|
35575
|
-
|
|
35576
|
-
childStore = existing;
|
|
35577
|
-
childItemIdArraySignal =
|
|
35578
|
-
scopedIdArraySignalMap.get(uniqueKeyValue);
|
|
35579
|
-
break;
|
|
35580
|
-
}
|
|
35796
|
+
if (uniqueKeyValue !== undefined && scopeMap.has(uniqueKeyValue)) {
|
|
35797
|
+
scope = scopeMap.get(uniqueKeyValue);
|
|
35798
|
+
break;
|
|
35581
35799
|
}
|
|
35582
35800
|
}
|
|
35583
35801
|
}
|
|
35584
|
-
if (!
|
|
35585
|
-
|
|
35586
|
-
name: `${childName}#${ownerId} store`,
|
|
35587
|
-
createItem: (props) => {
|
|
35588
|
-
const childItem = {};
|
|
35589
|
-
Object.assign(childItem, props);
|
|
35590
|
-
for (const childSetup of childSetupCallbackSet) {
|
|
35591
|
-
childSetup(childItem);
|
|
35592
|
-
}
|
|
35593
|
-
return childItem;
|
|
35594
|
-
},
|
|
35595
|
-
});
|
|
35596
|
-
childItemIdArraySignal = signal([]);
|
|
35802
|
+
if (!scope) {
|
|
35803
|
+
scope = createScope(ownerId);
|
|
35597
35804
|
}
|
|
35598
|
-
|
|
35599
|
-
//
|
|
35600
|
-
|
|
35805
|
+
// Register the scope under the id and every uniqueKey value so that
|
|
35806
|
+
// resolveOwnerId can address it whichever key a callback returns.
|
|
35807
|
+
scopeMap.set(ownerId, scope);
|
|
35601
35808
|
for (const uniqueKey of uniqueKeys) {
|
|
35602
35809
|
const uniqueKeyValue = item[uniqueKey];
|
|
35603
35810
|
if (uniqueKeyValue !== undefined) {
|
|
35604
|
-
|
|
35811
|
+
scopeMap.set(uniqueKeyValue, scope);
|
|
35605
35812
|
}
|
|
35606
35813
|
}
|
|
35607
35814
|
|
|
35608
|
-
|
|
35609
|
-
|
|
35610
|
-
|
|
35611
|
-
|
|
35612
|
-
|
|
35613
|
-
|
|
35815
|
+
const { childStore, idArraySignal } = scope;
|
|
35816
|
+
const updateChildItemIdArray = createChildIdArrayUpdater(
|
|
35817
|
+
childStore,
|
|
35818
|
+
childIdKey,
|
|
35819
|
+
idArraySignal,
|
|
35820
|
+
);
|
|
35821
|
+
// The parent may not carry the property at all (e.g. created by a POST
|
|
35822
|
+
// that does not embed it): leave the collection of a reused scope
|
|
35823
|
+
// untouched — children may have been fetched by uniqueKey before the
|
|
35824
|
+
// parent was loaded. Only an explicit value replaces the collection.
|
|
35825
|
+
if (item[propertyName] !== undefined) {
|
|
35826
|
+
updateChildItemIdArray(item[propertyName]);
|
|
35614
35827
|
}
|
|
35615
35828
|
|
|
35616
|
-
const updateChildItemIdArray = (valueArray) => {
|
|
35617
|
-
const currentIdArray = childItemIdArraySignal.peek();
|
|
35618
|
-
if (!Array.isArray(valueArray)) {
|
|
35619
|
-
if (currentIdArray.length === 0) return;
|
|
35620
|
-
childItemIdArraySignal.value = [];
|
|
35621
|
-
return;
|
|
35622
|
-
}
|
|
35623
|
-
let i = 0;
|
|
35624
|
-
const idArray = [];
|
|
35625
|
-
let modified = false;
|
|
35626
|
-
while (i < valueArray.length) {
|
|
35627
|
-
const value = valueArray[i];
|
|
35628
|
-
const currentIdAtIndex = currentIdArray[idArray.length];
|
|
35629
|
-
i++;
|
|
35630
|
-
if (isProps(value)) {
|
|
35631
|
-
const childItem = childStore.upsert(value);
|
|
35632
|
-
const childItemId = childItem[childIdKey];
|
|
35633
|
-
if (currentIdAtIndex !== childItemId) modified = true;
|
|
35634
|
-
idArray.push(childItemId);
|
|
35635
|
-
continue;
|
|
35636
|
-
}
|
|
35637
|
-
if (primitiveCanBeId(value)) {
|
|
35638
|
-
const childItemProps = { [childIdKey]: value };
|
|
35639
|
-
const childItem = childStore.upsert(childItemProps);
|
|
35640
|
-
const childItemId = childItem[childIdKey];
|
|
35641
|
-
if (currentIdAtIndex !== childItemId) modified = true;
|
|
35642
|
-
idArray.push(childItemId);
|
|
35643
|
-
continue;
|
|
35644
|
-
}
|
|
35645
|
-
}
|
|
35646
|
-
if (modified || currentIdArray.length !== idArray.length) {
|
|
35647
|
-
childItemIdArraySignal.value = idArray;
|
|
35648
|
-
}
|
|
35649
|
-
};
|
|
35650
|
-
|
|
35651
|
-
updateChildItemIdArray(item[propertyName]);
|
|
35652
|
-
|
|
35653
35829
|
// When an id is renamed (PUT/PATCH changes the idKey), patch the id array.
|
|
35654
|
-
syncIdArrayOnRename(childStore, childIdKey,
|
|
35830
|
+
syncIdArrayOnRename(childStore, childIdKey, idArraySignal);
|
|
35655
35831
|
|
|
35656
35832
|
const childItemArraySignal = computed(() => {
|
|
35657
|
-
const
|
|
35658
|
-
const childItemArray = childStore.selectAll(childItemIdArray);
|
|
35833
|
+
const childItemArray = childStore.selectAll(idArraySignal.value);
|
|
35659
35834
|
Object.defineProperty(childItemArray, SYMBOL_OBJECT_SIGNAL, {
|
|
35660
35835
|
value: childItemArraySignal,
|
|
35661
35836
|
writable: false,
|
|
@@ -35673,13 +35848,10 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35673
35848
|
const createRestActionForScopedMany = (
|
|
35674
35849
|
verb,
|
|
35675
35850
|
callback,
|
|
35676
|
-
{ isMany,
|
|
35851
|
+
{ isMany, onActionComplete },
|
|
35677
35852
|
) => {
|
|
35678
|
-
if (!callback) {
|
|
35679
|
-
return undefined;
|
|
35680
|
-
}
|
|
35681
35853
|
const childActionName = `${childName}.${verb}`;
|
|
35682
|
-
|
|
35854
|
+
return createAction(callback, {
|
|
35683
35855
|
name: childActionName,
|
|
35684
35856
|
meta: { verb, isMany, paramScope },
|
|
35685
35857
|
resultToValue: (result) => {
|
|
@@ -35696,48 +35868,33 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35696
35868
|
uniqueKeys,
|
|
35697
35869
|
childActionName,
|
|
35698
35870
|
);
|
|
35699
|
-
|
|
35700
|
-
|
|
35701
|
-
|
|
35702
|
-
|
|
35703
|
-
childStore = arraySignalStore([], childIdKey, {
|
|
35704
|
-
name: `${childName}#${ownerId} store`,
|
|
35705
|
-
createItem: (props) => {
|
|
35706
|
-
const childItem = {};
|
|
35707
|
-
Object.assign(childItem, props);
|
|
35708
|
-
for (const childSetup of childSetupCallbackSet) {
|
|
35709
|
-
childSetup(childItem);
|
|
35710
|
-
}
|
|
35711
|
-
return childItem;
|
|
35712
|
-
},
|
|
35713
|
-
});
|
|
35714
|
-
scopedStoreMap.set(ownerId, childStore);
|
|
35715
|
-
const newIdArraySignal = signal([]);
|
|
35716
|
-
scopedIdArraySignalMap.set(ownerId, newIdArraySignal);
|
|
35717
|
-
}
|
|
35718
|
-
const childItemIdArraySignal = scopedIdArraySignalMap.get(ownerId);
|
|
35871
|
+
// Owner not in store yet: create the scope so actions can run before
|
|
35872
|
+
// the parent item has been loaded (e.g. rows fetched before their table).
|
|
35873
|
+
const scope = scopeMap.get(ownerId) || createScope(ownerId);
|
|
35874
|
+
const { childStore, idArraySignal } = scope;
|
|
35719
35875
|
|
|
35720
35876
|
if (verb === "DELETE") {
|
|
35721
35877
|
if (isMany) {
|
|
35722
35878
|
const idArray = childStore.drop(rest[0]);
|
|
35723
35879
|
const toRemoveSet = new Set(idArray);
|
|
35724
|
-
|
|
35880
|
+
idArraySignal.value = idArraySignal
|
|
35725
35881
|
.peek()
|
|
35726
35882
|
.filter((id) => !toRemoveSet.has(id));
|
|
35727
35883
|
return [ownerId, idArray];
|
|
35728
35884
|
}
|
|
35729
35885
|
const childId = childStore.drop(rest[0]);
|
|
35730
|
-
|
|
35886
|
+
idArraySignal.value = idArraySignal
|
|
35731
35887
|
.peek()
|
|
35732
35888
|
.filter((id) => id !== childId);
|
|
35733
35889
|
return [ownerId, childId];
|
|
35734
35890
|
}
|
|
35735
35891
|
|
|
35736
35892
|
if (isMany) {
|
|
35737
|
-
// GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items
|
|
35893
|
+
// GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items,
|
|
35894
|
+
// and it replaces the whole collection.
|
|
35738
35895
|
const itemArray = childStore.upsert(rest[0]);
|
|
35739
|
-
const idArray = itemArray.map((
|
|
35740
|
-
|
|
35896
|
+
const idArray = itemArray.map((childItem) => childItem[childIdKey]);
|
|
35897
|
+
idArraySignal.value = idArray;
|
|
35741
35898
|
return [ownerId, idArray];
|
|
35742
35899
|
}
|
|
35743
35900
|
|
|
@@ -35749,25 +35906,23 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35749
35906
|
return [ownerId, childItem[childIdKey]];
|
|
35750
35907
|
},
|
|
35751
35908
|
valueToData: (value) => {
|
|
35752
|
-
if (!value)
|
|
35909
|
+
if (!value) {
|
|
35910
|
+
return isMany ? [] : undefined;
|
|
35911
|
+
}
|
|
35753
35912
|
const [ownerId, idOrIdArray] = value;
|
|
35754
|
-
const
|
|
35755
|
-
if (!
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35760
|
-
|
|
35913
|
+
const scope = scopeMap.get(ownerId);
|
|
35914
|
+
if (!scope) {
|
|
35915
|
+
return isMany ? [] : undefined;
|
|
35916
|
+
}
|
|
35917
|
+
if (isMany) {
|
|
35918
|
+
return scope.childStore.selectAll(idOrIdArray);
|
|
35919
|
+
}
|
|
35920
|
+
return scope.childStore.select(idOrIdArray);
|
|
35761
35921
|
},
|
|
35922
|
+
completeSideEffect: onActionComplete,
|
|
35762
35923
|
});
|
|
35763
|
-
return childAction;
|
|
35764
35924
|
};
|
|
35765
35925
|
|
|
35766
|
-
// When a child (scopedMany) item is mutated via POST, the parent GET must
|
|
35767
|
-
// re-fetch because the parent embeds the child array and we cannot know the
|
|
35768
|
-
// new ordering without asking the backend again.
|
|
35769
|
-
// (scopedOne does NOT need this: the mutation result contains the updated
|
|
35770
|
-
// item directly, so no parent re-fetch is necessary.)
|
|
35771
35926
|
const childResource = createResource(childName, {
|
|
35772
35927
|
idKey: childIdKey,
|
|
35773
35928
|
restCallbacks: {
|
|
@@ -35789,19 +35944,23 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35789
35944
|
rerunOn: scopedManyRerunOn ?? rerunOn,
|
|
35790
35945
|
dependencies: scopedManyDependencies ?? dependencies,
|
|
35791
35946
|
});
|
|
35792
|
-
//
|
|
35947
|
+
// When a scoped child collection is mutated (POST etc.), the parent GET must
|
|
35948
|
+
// re-fetch: the parent embeds the child array and only the backend knows the
|
|
35949
|
+
// new ordering. (scopedOne does not need this: the mutation result contains
|
|
35950
|
+
// the updated object directly.)
|
|
35793
35951
|
resourceLifecycleManager.addDependency(
|
|
35794
35952
|
childResource,
|
|
35795
35953
|
stateFacade,
|
|
35796
35954
|
propertyName,
|
|
35797
35955
|
);
|
|
35798
|
-
childResource.getChildStore = (ownerKey) =>
|
|
35956
|
+
childResource.getChildStore = (ownerKey) =>
|
|
35957
|
+
scopeMap.get(ownerKey)?.childStore;
|
|
35799
35958
|
return childResource;
|
|
35800
35959
|
};
|
|
35801
35960
|
|
|
35802
|
-
// expose
|
|
35961
|
+
// expose one action (or range reader) per provided rest callback
|
|
35803
35962
|
for (const [restCallbackKey, restCallback] of Object.entries(restCallbacks)) {
|
|
35804
|
-
if (restCallback
|
|
35963
|
+
if (!restCallback) {
|
|
35805
35964
|
continue;
|
|
35806
35965
|
}
|
|
35807
35966
|
if (restCallbackKey === "GET_RANGE") {
|
|
@@ -35823,25 +35982,16 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35823
35982
|
const verb = isMany
|
|
35824
35983
|
? restCallbackKey.replace("_MANY", "")
|
|
35825
35984
|
: restCallbackKey;
|
|
35826
|
-
|
|
35985
|
+
let restAction = createRestAction(verb, restCallback, {
|
|
35827
35986
|
isMany,
|
|
35828
|
-
|
|
35987
|
+
onActionComplete,
|
|
35829
35988
|
paramScope,
|
|
35830
35989
|
});
|
|
35831
|
-
if (!restAction) {
|
|
35832
|
-
console.error("no action returned (here to see when it happens)");
|
|
35833
|
-
continue;
|
|
35834
|
-
}
|
|
35835
|
-
let actionToRegister;
|
|
35836
35990
|
if (params) {
|
|
35837
|
-
|
|
35838
|
-
stateFacade[restCallbackKey] = restActionBound;
|
|
35839
|
-
actionToRegister = restActionBound;
|
|
35840
|
-
} else {
|
|
35841
|
-
stateFacade[restCallbackKey] = restAction;
|
|
35842
|
-
actionToRegister = restAction;
|
|
35991
|
+
restAction = restAction.bindParams(params);
|
|
35843
35992
|
}
|
|
35844
|
-
|
|
35993
|
+
stateFacade[restCallbackKey] = restAction;
|
|
35994
|
+
resourceLifecycleManager.registerAction(stateFacade, restAction);
|
|
35845
35995
|
}
|
|
35846
35996
|
|
|
35847
35997
|
return stateFacade;
|
|
@@ -35852,76 +36002,64 @@ const createRestActionFactoryForRoot = (
|
|
|
35852
36002
|
{
|
|
35853
36003
|
idKey,
|
|
35854
36004
|
store, // see array_signal_store.js
|
|
36005
|
+
declarationSite,
|
|
35855
36006
|
},
|
|
35856
36007
|
) => {
|
|
35857
36008
|
const createActionForRoot = (
|
|
35858
36009
|
verb,
|
|
35859
36010
|
restCallback,
|
|
35860
|
-
{ isMany,
|
|
36011
|
+
{ isMany, onActionComplete, paramScope },
|
|
35861
36012
|
) => {
|
|
35862
36013
|
if (!isMany) {
|
|
35863
36014
|
return createActionAffectingOneItem(verb, restCallback, {
|
|
35864
|
-
|
|
36015
|
+
onActionComplete,
|
|
35865
36016
|
paramScope,
|
|
35866
36017
|
});
|
|
35867
36018
|
}
|
|
35868
36019
|
return createActionAffectingManyItems(verb, restCallback, {
|
|
35869
|
-
|
|
36020
|
+
onActionComplete,
|
|
35870
36021
|
paramScope,
|
|
35871
36022
|
});
|
|
35872
36023
|
};
|
|
35873
36024
|
const createActionAffectingOneItem = (
|
|
35874
36025
|
verb,
|
|
35875
36026
|
callback,
|
|
35876
|
-
{
|
|
36027
|
+
{ onActionComplete, paramScope },
|
|
35877
36028
|
) => {
|
|
35878
36029
|
const applyResultToValue =
|
|
35879
36030
|
verb === "DELETE"
|
|
35880
|
-
? (itemIdOrItemProps) =>
|
|
35881
|
-
const itemId = store.drop(itemIdOrItemProps);
|
|
35882
|
-
return itemId;
|
|
35883
|
-
}
|
|
36031
|
+
? (itemIdOrItemProps) => store.drop(itemIdOrItemProps)
|
|
35884
36032
|
: (result) => {
|
|
35885
|
-
|
|
35886
|
-
|
|
35887
|
-
|
|
35888
|
-
|
|
35889
|
-
|
|
35890
|
-
|
|
35891
|
-
item = store.upsert(...result);
|
|
35892
|
-
} else {
|
|
35893
|
-
item = store.upsert(result);
|
|
35894
|
-
}
|
|
35895
|
-
const itemId = item[idKey];
|
|
35896
|
-
return itemId;
|
|
36033
|
+
// An array result is [property, value, props] — used to rename the
|
|
36034
|
+
// idKey of an item: store.upsert("name", "currentName", { name: "newName" })
|
|
36035
|
+
const item = Array.isArray(result)
|
|
36036
|
+
? store.upsert(...result)
|
|
36037
|
+
: store.upsert(result);
|
|
36038
|
+
return item[idKey];
|
|
35897
36039
|
};
|
|
35898
|
-
|
|
35899
|
-
|
|
35900
|
-
|
|
35901
|
-
|
|
35902
|
-
|
|
35903
|
-
? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
|
|
35904
|
-
: callerInfo.raw || "unknown location";
|
|
35905
|
-
const originalActionName = `${name}.${verb}`;
|
|
35906
|
-
const actionAffectingOneItem = createAction(callback, {
|
|
36040
|
+
const throwInvalidResult = createInvalidResultThrower(
|
|
36041
|
+
`${name}.${verb}`,
|
|
36042
|
+
declarationSite,
|
|
36043
|
+
);
|
|
36044
|
+
return createAction(callback, {
|
|
35907
36045
|
name: `${name}.${verb}`,
|
|
35908
36046
|
meta: { verb, isMany: false, paramScope },
|
|
35909
36047
|
resultToValue: (result, action) => {
|
|
35910
|
-
const actionLabel = action.name;
|
|
35911
|
-
|
|
35912
36048
|
if (verb === "DELETE") {
|
|
35913
36049
|
if (!isProps(result) && !primitiveCanBeId(result)) {
|
|
35914
|
-
|
|
35915
|
-
|
|
35916
|
-
|
|
36050
|
+
throwInvalidResult(
|
|
36051
|
+
action.name,
|
|
36052
|
+
`an object (that will be used to drop "${name}" resource)`,
|
|
36053
|
+
result,
|
|
35917
36054
|
);
|
|
35918
36055
|
}
|
|
35919
36056
|
return applyResultToValue(result);
|
|
35920
36057
|
}
|
|
35921
36058
|
if (!isProps(result)) {
|
|
35922
|
-
|
|
35923
|
-
|
|
35924
|
-
|
|
36059
|
+
throwInvalidResult(
|
|
36060
|
+
action.name,
|
|
36061
|
+
`an object (that will be used to upsert "${name}" resource)`,
|
|
36062
|
+
result,
|
|
35925
36063
|
);
|
|
35926
36064
|
}
|
|
35927
36065
|
// Track which top-level properties the GET response contained so that
|
|
@@ -35932,40 +36070,30 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35932
36070
|
return applyResultToValue(result);
|
|
35933
36071
|
},
|
|
35934
36072
|
valueToData: (itemId) => store.select(itemId),
|
|
35935
|
-
completeSideEffect:
|
|
35936
|
-
lifecycleCtx.onComplete(actionCompleted);
|
|
35937
|
-
},
|
|
36073
|
+
completeSideEffect: onActionComplete,
|
|
35938
36074
|
});
|
|
35939
|
-
return actionAffectingOneItem;
|
|
35940
36075
|
};
|
|
35941
36076
|
const createActionAffectingManyItems = (
|
|
35942
36077
|
verb,
|
|
35943
36078
|
callback,
|
|
35944
|
-
{
|
|
36079
|
+
{ onActionComplete, paramScope },
|
|
35945
36080
|
) => {
|
|
35946
36081
|
const applyResultToValue =
|
|
35947
36082
|
verb === "DELETE"
|
|
35948
|
-
? (idOrMutableIdArray) =>
|
|
35949
|
-
const idArray = store.drop(idOrMutableIdArray);
|
|
35950
|
-
return idArray;
|
|
35951
|
-
}
|
|
36083
|
+
? (idOrMutableIdArray) => store.drop(idOrMutableIdArray)
|
|
35952
36084
|
: (dataArray) => {
|
|
35953
36085
|
const itemArray = store.upsert(dataArray);
|
|
35954
|
-
|
|
35955
|
-
return idArray;
|
|
36086
|
+
return itemArray.map((item) => item[idKey]);
|
|
35956
36087
|
};
|
|
35957
36088
|
|
|
35958
|
-
|
|
36089
|
+
return createAction(callback, {
|
|
35959
36090
|
meta: { verb, isMany: true, paramScope },
|
|
35960
36091
|
name: `${name}.${verb}_MANY`,
|
|
35961
36092
|
dataDefault: [],
|
|
35962
36093
|
resultToValue: applyResultToValue,
|
|
35963
|
-
valueToData: (idArray) =>
|
|
35964
|
-
const items = store.selectAll(idArray);
|
|
35965
|
-
return items;
|
|
35966
|
-
},
|
|
36094
|
+
valueToData: (idArray) => store.selectAll(idArray),
|
|
35967
36095
|
completeSideEffect: (actionCompleted) => {
|
|
35968
|
-
|
|
36096
|
+
onActionComplete(actionCompleted);
|
|
35969
36097
|
if (
|
|
35970
36098
|
verb === "DELETE" ||
|
|
35971
36099
|
actionCompleted.valueSignal.peek().length === 0
|
|
@@ -35979,12 +36107,70 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
35979
36107
|
return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
|
|
35980
36108
|
},
|
|
35981
36109
|
});
|
|
35982
|
-
return actionAffectingManyItems;
|
|
35983
36110
|
};
|
|
35984
36111
|
|
|
35985
36112
|
return createActionForRoot;
|
|
35986
36113
|
};
|
|
35987
36114
|
|
|
36115
|
+
// Captures the "file:line:column" of the user code that invoked the public
|
|
36116
|
+
// function (resource(), .one(), .many(), withParams, …), so invalid-result
|
|
36117
|
+
// errors can point at where the callbacks were declared, not at this file.
|
|
36118
|
+
// Must be called directly from the public function: the stack offset accounts
|
|
36119
|
+
// for exactly two frames (getCallerInfo → getDeclarationSite → public fn → user code).
|
|
36120
|
+
const getDeclarationSite = () => {
|
|
36121
|
+
const callerInfo = getCallerInfo(null, 1);
|
|
36122
|
+
if (callerInfo.file && callerInfo.line && callerInfo.column) {
|
|
36123
|
+
return `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`;
|
|
36124
|
+
}
|
|
36125
|
+
return callerInfo.raw || "unknown location";
|
|
36126
|
+
};
|
|
36127
|
+
|
|
36128
|
+
const createInvalidResultThrower = (originalActionName, declarationSite) => {
|
|
36129
|
+
return (actionLabel, expected, result) => {
|
|
36130
|
+
throw new TypeError(
|
|
36131
|
+
`${actionLabel} must return ${expected}, received ${result}.
|
|
36132
|
+
${originalActionName} source location: ${declarationSite}`,
|
|
36133
|
+
);
|
|
36134
|
+
};
|
|
36135
|
+
};
|
|
36136
|
+
|
|
36137
|
+
// Shared by .many() and .scopedMany(): converts a raw relationship value (an
|
|
36138
|
+
// array of child props/ids, or anything else meaning "empty") into an array of
|
|
36139
|
+
// child ids, upserting each entry into the child store. The id array signal is
|
|
36140
|
+
// only touched when the resulting ids actually differ.
|
|
36141
|
+
const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
|
|
36142
|
+
return (valueArray) => {
|
|
36143
|
+
const currentIdArray = idArraySignal.peek();
|
|
36144
|
+
if (!Array.isArray(valueArray)) {
|
|
36145
|
+
if (currentIdArray.length > 0) {
|
|
36146
|
+
idArraySignal.value = [];
|
|
36147
|
+
}
|
|
36148
|
+
return;
|
|
36149
|
+
}
|
|
36150
|
+
const idArray = [];
|
|
36151
|
+
let modified = false;
|
|
36152
|
+
for (const value of valueArray) {
|
|
36153
|
+
let childItemProps;
|
|
36154
|
+
if (isProps(value)) {
|
|
36155
|
+
childItemProps = value;
|
|
36156
|
+
} else if (primitiveCanBeId(value)) {
|
|
36157
|
+
childItemProps = { [childIdKey]: value };
|
|
36158
|
+
} else {
|
|
36159
|
+
continue;
|
|
36160
|
+
}
|
|
36161
|
+
const childItem = childStore.upsert(childItemProps);
|
|
36162
|
+
const childItemId = childItem[childIdKey];
|
|
36163
|
+
if (currentIdArray[idArray.length] !== childItemId) {
|
|
36164
|
+
modified = true;
|
|
36165
|
+
}
|
|
36166
|
+
idArray.push(childItemId);
|
|
36167
|
+
}
|
|
36168
|
+
if (modified || currentIdArray.length !== idArray.length) {
|
|
36169
|
+
idArraySignal.value = idArray;
|
|
36170
|
+
}
|
|
36171
|
+
};
|
|
36172
|
+
};
|
|
36173
|
+
|
|
35988
36174
|
const syncIdArrayOnRename = (store, idKey, idArraySignal) => {
|
|
35989
36175
|
return store.observeProperties((mutations) => {
|
|
35990
36176
|
const idArray = idArraySignal.peek();
|
|
@@ -36044,7 +36230,7 @@ const resolveOwnerId = (rawOwnerId, store, idKey, uniqueKeys, actionName) => {
|
|
|
36044
36230
|
return item[idKey];
|
|
36045
36231
|
}
|
|
36046
36232
|
throw new TypeError(
|
|
36047
|
-
`${actionName}: the first element of the returned array is { ${propName}: "${propValue}" } but "${propName}" is neither the idKey ("${idKey}") nor a declared uniqueKey (${uniqueKeys.length ? uniqueKeys.join(", ") : "none"}).
|
|
36233
|
+
`${actionName}: the first element of the returned array is { ${propName}: "${propValue}" } but "${propName}" is neither the idKey ("${idKey}") nor a declared uniqueKey (${uniqueKeys.length ? uniqueKeys.join(", ") : "none"}).
|
|
36048
36234
|
Return a primitive id or a single-property object whose key is the idKey or a uniqueKey.`,
|
|
36049
36235
|
);
|
|
36050
36236
|
}
|
|
@@ -36053,7 +36239,7 @@ Return a primitive id or a single-property object whose key is the idKey or a un
|
|
|
36053
36239
|
if (idKey in rawOwnerId) {
|
|
36054
36240
|
const resolvedId = rawOwnerId[idKey];
|
|
36055
36241
|
console.warn(
|
|
36056
|
-
`${actionName}: the first element of the returned array is an object with multiple properties.
|
|
36242
|
+
`${actionName}: the first element of the returned array is an object with multiple properties.
|
|
36057
36243
|
Only "${idKey}" is needed. Consider returning a primitive id or { ${idKey}: value } instead.`,
|
|
36058
36244
|
);
|
|
36059
36245
|
return resolvedId;
|
|
@@ -36065,8 +36251,10 @@ Received an object with keys: ${keys.join(", ")}.`,
|
|
|
36065
36251
|
);
|
|
36066
36252
|
};
|
|
36067
36253
|
|
|
36068
|
-
/**
|
|
36069
|
-
*
|
|
36254
|
+
/**
|
|
36255
|
+
* Keeps external signals in sync with properties of the resource's store items:
|
|
36256
|
+
* when a tracked property changes on an item, the corresponding signal is
|
|
36257
|
+
* updated automatically.
|
|
36070
36258
|
*
|
|
36071
36259
|
* Since signals are typically connected to route parameters via the route template
|
|
36072
36260
|
* syntax, this keeps the URL in sync when a store item's mutable key is renamed.
|