@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.
@@ -2819,7 +2819,10 @@ const readSignalForUrlBuild = (connection) => {
2819
2819
  /**
2820
2820
  * Creates a custom route pattern matcher
2821
2821
  */
2822
- const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
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
- const applyOn = (url) => {
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) && entry.value !== undefined && conn.isCustomValue(entry.value)
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 = (pattern, { pathConnectionMap, queryConnectionMap }) => {
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
- childParams[segment.name] = decodeURIComponent(urlSegment);
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
- { baseUrl, baseFileUrl, queryConnectionMap, patternObj = null },
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
- params[patternSeg.name] = decodeURIComponent(urlSeg);
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
- const route = (pattern, { searchParams } = {}) => {
4866
- const routePattern = createRoutePattern(pattern, { searchParams });
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 calle",
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 en millisecondes
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
- // Si déjà protégée, étendre la protection
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 = initialParamsDefault;
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 (runningState !== RUNNING) {
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
- // CAS 1: Signal direct -> proxy
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
- // CAS 2: Objet -> vérifier s'il contient des signals
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
- // Pas de signals, merge statique normal
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
- // Combiner avec les params existants pour les valeurs statiques
12567
- const paramsSignal = computed(() => {
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
- params[key] = signalForThisKey.value;
12848
+ combinedParams[key] = signalForThisKey.value;
12574
12849
  } else {
12575
- params[key] = staticParams[key];
12850
+ combinedParams[key] = staticParams[key];
12576
12851
  }
12577
12852
  }
12578
- return params;
12853
+ return combinedParams;
12579
12854
  });
12580
- return createActionProxyFromSignal(action, paramsSignal, options);
12855
+ return createActionProxyFromSignal(
12856
+ action,
12857
+ combinedParamsSignal,
12858
+ options,
12859
+ );
12581
12860
  }
12582
12861
 
12583
- // CAS 3: Primitive or objects like DOMEvents etc -> action enfant
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
- // Create the action as a function that can be called directly
12653
- action = function actionFunction(...args) {
12654
- if (args.length === 0) {
12655
- return action.rerun();
12656
- }
12657
- const boundAction = bindParams(...args);
12658
- return boundAction.rerun();
12659
- };
12660
- Object.defineProperty(action, "name", {
12661
- configurable: true,
12662
- get() {
12663
- return actionNameSignal.value;
12664
- },
12665
- });
12666
- Object.defineProperty(action, "callSource", {
12667
- configurable: true,
12668
- get() {
12669
- return actionCallSourceSignal.value;
12670
- },
12671
- set(v) {
12672
- actionCallSourceSignal.value = v;
12673
- },
12674
- });
12675
- actionWeakMap.set(action, action);
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, // ✅ Add the new method
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 || DEBUG$1) {
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
- // Effects pour synchroniser les propriétés
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 true, each new target action starts fresh with no inherited state.
13047
- * By default (false), the proxy carries over the previous target's value and error into the new action.
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
- let actionProxy;
13167
- {
13168
- actionProxy = function actionProxyFunction() {
13169
- return actionProxy.rerun();
13170
- };
13171
- Object.defineProperty(actionProxy, "name", {
13172
- configurable: true,
13173
- get() {
13174
- return nameSignal.value;
13175
- },
13176
- });
13177
- Object.defineProperty(actionProxy, "callSource", {
13178
- configurable: true,
13179
- get() {
13180
- return callSourceSignal.value;
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
- const actionRef = useRef();
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
- const existingAction = actionRef.current;
13418
- if (existingAction) {
13419
- return existingAction;
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
- actionRef.current = noopActionBound;
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 = actionRef.current;
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
- actionRef.current = actionFromFunction;
13730
+ actionFromFunctionRef.current = actionFromFunction;
13453
13731
  return actionFromFunction;
13454
13732
  }
13455
13733
  const actionBoundToParams =
13456
13734
  actionFromFunction.bindParams(actionParamsSignal);
13457
- actionRef.current = actionBoundToParams;
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
- lifecycleCtx.onComplete = (actionCompleted) => {
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
- const childItem = childResource.store.upsert(value);
34896
- const childItemId = childItem[childIdKeyForSetup];
34897
- if (currentChildItemId === childItemId) {
34898
- return false;
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 = childItemId;
35212
+ childItemIdSignal.value = undefined;
34911
35213
  return true;
34912
35214
  }
34913
- if (currentChildItemId === undefined) {
35215
+ const childItem = childStore.upsert(childItemProps);
35216
+ const childItemId = childItem[childIdKey];
35217
+ if (currentChildItemId === childItemId) {
34914
35218
  return false;
34915
35219
  }
34916
- childItemIdSignal.value = undefined;
35220
+ childItemIdSignal.value = childItemId;
34917
35221
  return true;
34918
35222
  };
34919
35223
  updateChildItemId(item[propertyName]);
34920
35224
  const childItemSignal = computed(() =>
34921
- childResource.store.select(childItemIdSignal.value),
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 childIdKey = childResource.idKey;
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
- : // callback must return object with the following format:
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
- const childItemId = childItem ? childItem[childIdKey] : undefined;
34982
- return childItemId;
35273
+ return childItem ? childItem[childIdKey] : undefined;
34983
35274
  };
34984
-
34985
- const callerInfo = getCallerInfo(null, 2);
34986
- const locationInfo =
34987
- callerInfo.file && callerInfo.line && callerInfo.column
34988
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
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
- throw new TypeError(
35005
- `${actionLabel} must return an object (that will be used to drop "${name}" resource), received ${result}.
35006
- ${originalActionName} source location: ${locationInfo}`,
35289
+ throwInvalidResult(
35290
+ action.name,
35291
+ `an object (that will be used to drop "${name}" resource)`,
35292
+ result,
35007
35293
  );
35008
35294
  }
35009
- return applyResultToValue(result);
35010
- }
35011
- if (!isProps(result)) {
35012
- throw new TypeError(
35013
- `${actionLabel} must return an object (that will be used to upsert "${name}" resource), received ${result}.
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: (actionCompleted) => {
35021
- lifecycleCtx.onComplete(actionCompleted);
35022
- },
35305
+ completeSideEffect: onActionComplete,
35023
35306
  });
35024
- return actionAffectingOneItem;
35025
35307
  };
35026
35308
 
35027
35309
  return createResource(childName, {
35028
- idKey: childResource.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 = (valueArray) => {
35091
- const currentIdArray = childItemIdArraySignal.peek();
35092
- if (!Array.isArray(valueArray)) {
35093
- if (currentIdArray.length === 0) return;
35094
- childItemIdArraySignal.value = [];
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 = childResource.store.selectAll(idArray);
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, lifecycleCtx },
35399
+ { isMany, onActionComplete },
35150
35400
  ) => {
35151
35401
  if (!isMany) {
35152
- return createRestActionAffectingOneItem(verb, callback, lifecycleCtx);
35402
+ return createRestActionAffectingOneItem(verb, callback, {
35403
+ onActionComplete,
35404
+ });
35153
35405
  }
35154
- return createRestActionAffectingManyItems(verb, callback, lifecycleCtx);
35406
+ return createRestActionAffectingManyItems(verb, callback, {
35407
+ onActionComplete,
35408
+ });
35155
35409
  };
35156
- const createRestActionAffectingOneItem = (verb, callback, lifecycleCtx) => {
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
- const childItemCandidateId = childItemCandidate[childIdKey];
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
- const childItemId = childItem[childIdKey];
35185
- return childItemId;
35442
+ return childItem[childIdKey];
35186
35443
  };
35187
-
35188
- const callerInfo = getCallerInfo(null, 2);
35189
- const locationInfo =
35190
- callerInfo.file && callerInfo.line && callerInfo.column
35191
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
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
- throw new TypeError(
35204
- `${actionLabel} must return an array [itemId, childItemId] (that will be used to remove relationship), received ${result}.
35205
- ${originalActionName} source location: ${locationInfo}`,
35454
+ throwInvalidResult(
35455
+ action.name,
35456
+ `an array [itemId, childItemId] (that will be used to remove relationship)`,
35457
+ result,
35206
35458
  );
35207
35459
  }
35208
- return applyResultToValue(result);
35209
- }
35210
- if (!isProps(result)) {
35211
- throw new TypeError(
35212
- `${actionLabel} must return an object (that will be used to upsert child item), received ${result}.
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: (actionCompleted) => {
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
- lifecycleCtx,
35476
+ { onActionComplete },
35229
35477
  ) => {
35230
35478
  const applyResultToValue =
35231
35479
  verb === "GET"
35232
35480
  ? (result) => {
35233
- // callback must return object with the following format:
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
- const childItemIdArray = childItemArray.map(
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 deletedChildItemIdArray = [];
35255
- const childItemArrayWithoutThoose = [];
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 (deletedChildItemArray.includes(childItemCandidate)) {
35262
- someFound = true;
35497
+ if (deletedChildItemSet.has(childItemCandidate)) {
35263
35498
  deletedChildItemIdArray.push(
35264
35499
  childItemCandidate[childIdKey],
35265
35500
  );
35266
35501
  } else {
35267
- childItemArrayWithoutThoose.push(childItemCandidate);
35502
+ childItemArrayWithoutThose.push(childItemCandidate);
35268
35503
  }
35269
35504
  }
35270
- if (someFound) {
35505
+ if (deletedChildItemIdArray.length > 0) {
35271
35506
  store.upsert({
35272
35507
  [idKey]: item[idKey],
35273
- [propertyName]: childItemArrayWithoutThoose,
35508
+ [propertyName]: childItemArrayWithoutThose,
35274
35509
  });
35275
35510
  }
35276
35511
  return deletedChildItemIdArray;
35277
35512
  }
35278
35513
  : (childDataArray) => {
35279
35514
  const childItemArray = childStore.upsert(childDataArray);
35280
- const childItemIdArray = childItemArray.map(
35281
- (childItem) => childItem[childIdKey],
35282
- );
35283
- return childItemIdArray;
35515
+ return childItemArray.map((childItem) => childItem[childIdKey]);
35284
35516
  };
35285
-
35286
- const callerInfo = getCallerInfo(null, 2);
35287
- const locationInfo =
35288
- callerInfo.file && callerInfo.line && callerInfo.column
35289
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
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
- throw new TypeError(
35303
- `${actionLabel} must return an object (that will be used to upsert "${name}" resource with many relationships), received ${result}.
35304
- ${originalActionName} source location: ${locationInfo}`,
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
- return applyResultToValue(result);
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
- throw new TypeError(
35316
- `${actionLabel} must return an array [itemId, childItemIdArray] (that will be used to remove relationships), received ${result}.
35317
- ${originalActionName} source location: ${locationInfo}`,
35540
+ throwInvalidResult(
35541
+ action.name,
35542
+ `an array [itemId, childItemIdArray] (that will be used to remove relationships)`,
35543
+ result,
35318
35544
  );
35319
35545
  }
35320
- return applyResultToValue(result);
35321
- }
35322
- if (!Array.isArray(result)) {
35323
- throw new TypeError(
35324
- `${actionLabel} must return an array of objects (that will be used to upsert child items), received ${result}.
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: (actionCompleted) => {
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
- // setupCallbackSet: callbacks added by chained .one()/.many()
35402
- // Applied to each per-scope child item object when it is first created.
35403
- const childItemSetupCallbackSet = new Set();
35404
- const childAddItemSetup = (callback) =>
35405
- childItemSetupCallbackSet.add(callback);
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
- // Create a stable child item mutated in place via applyProps.
35411
- // Reactive getters/setters from chained .one() etc. are defined on this object now
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 childItemSetupCallbackSet) {
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 = (verb, callback, { lifecycleCtx }) => {
35659
+ const createRestActionForScopedOne = (
35660
+ verb,
35661
+ callback,
35662
+ { onActionComplete },
35663
+ ) => {
35443
35664
  const childActionName = `${childName}.${verb}`;
35444
- const restAction = createAction(callback, {
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 childItem = scopedItemMap.get(ownerId);
35462
- if (!childItem) {
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
- const childSignal = scopedSignalMap.get(ownerId);
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: (actionCompleted) => {
35481
- lifecycleCtx.onComplete(actionCompleted);
35482
- },
35691
+ completeSideEffect: onActionComplete,
35483
35692
  });
35484
- return restAction;
35485
35693
  };
35486
35694
 
35487
- const childResource = createResource(childName, {
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
- // setupCallbackSet: callbacks added by chained .one()/.many()
35558
- // Applied to each child item when it is created in a per-scope store.
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
- const scopedStoreMap = new Map(); // ownerId → childStore
35562
- const scopedIdArraySignalMap = new Map(); // ownerId childItemIdArraySignal
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 scoped store if one was already created via a uniqueKey
35567
- // (e.g. rows were fetched by tablename before the full table was loaded).
35568
- let childStore = scopedStoreMap.get(ownerId);
35569
- let childItemIdArraySignal = scopedIdArraySignalMap.get(ownerId);
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
- const existing = scopedStoreMap.get(uniqueKeyValue);
35575
- if (existing) {
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 (!childStore) {
35585
- childStore = arraySignalStore([], childIdKey, {
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
- scopedStoreMap.set(ownerId, childStore);
35599
- // Also register by each uniqueKey value so that resolveOwnerId works
35600
- // when a callback returns { [uniqueKey]: value } before the full item is loaded.
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
- scopedStoreMap.set(uniqueKeyValue, childStore);
35811
+ scopeMap.set(uniqueKeyValue, scope);
35605
35812
  }
35606
35813
  }
35607
35814
 
35608
- scopedIdArraySignalMap.set(ownerId, childItemIdArraySignal);
35609
- for (const uniqueKey of uniqueKeys) {
35610
- const uniqueKeyValue = item[uniqueKey];
35611
- if (uniqueKeyValue !== undefined) {
35612
- scopedIdArraySignalMap.set(uniqueKeyValue, childItemIdArraySignal);
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, childItemIdArraySignal);
35830
+ syncIdArrayOnRename(childStore, childIdKey, idArraySignal);
35655
35831
 
35656
35832
  const childItemArraySignal = computed(() => {
35657
- const childItemIdArray = childItemIdArraySignal.value;
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, lifecycleCtx },
35851
+ { isMany, onActionComplete },
35677
35852
  ) => {
35678
- if (!callback) {
35679
- return undefined;
35680
- }
35681
35853
  const childActionName = `${childName}.${verb}`;
35682
- const childAction = createAction(callback, {
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
- let childStore = scopedStoreMap.get(ownerId);
35700
- if (!childStore) {
35701
- // Owner not yet in store — lazily create scoped store so actions can run
35702
- // before the parent item has been fully loaded (e.g. rows fetched before table).
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
- childItemIdArraySignal.value = childItemIdArraySignal
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
- childItemIdArraySignal.value = childItemIdArraySignal
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((i) => i[childIdKey]);
35740
- childItemIdArraySignal.value = idArray;
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) return isMany ? [] : undefined;
35909
+ if (!value) {
35910
+ return isMany ? [] : undefined;
35911
+ }
35753
35912
  const [ownerId, idOrIdArray] = value;
35754
- const childStore = scopedStoreMap.get(ownerId);
35755
- if (!childStore) return isMany ? [] : undefined;
35756
- if (isMany) return childStore.selectAll(idOrIdArray);
35757
- return childStore.select(idOrIdArray);
35758
- },
35759
- completeSideEffect: (actionCompleted) => {
35760
- lifecycleCtx.onComplete(actionCompleted);
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
- // Register: when childResource fires, rerun parent (stateFacade) GETs.
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) => scopedStoreMap.get(ownerKey);
35956
+ childResource.getChildStore = (ownerKey) =>
35957
+ scopeMap.get(ownerKey)?.childStore;
35799
35958
  return childResource;
35800
35959
  };
35801
35960
 
35802
- // expose rest actions on the stateFacade
35961
+ // expose one action (or range reader) per provided rest callback
35803
35962
  for (const [restCallbackKey, restCallback] of Object.entries(restCallbacks)) {
35804
- if (restCallback === undefined) {
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
- const restAction = createRestAction(verb, restCallback, {
35985
+ let restAction = createRestAction(verb, restCallback, {
35827
35986
  isMany,
35828
- lifecycleCtx,
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
- const restActionBound = restAction.bindParams(params);
35838
- stateFacade[restCallbackKey] = restActionBound;
35839
- actionToRegister = restActionBound;
35840
- } else {
35841
- stateFacade[restCallbackKey] = restAction;
35842
- actionToRegister = restAction;
35991
+ restAction = restAction.bindParams(params);
35843
35992
  }
35844
- resourceLifecycleManager.registerAction(stateFacade, actionToRegister);
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, lifecycleCtx, paramScope },
36011
+ { isMany, onActionComplete, paramScope },
35861
36012
  ) => {
35862
36013
  if (!isMany) {
35863
36014
  return createActionAffectingOneItem(verb, restCallback, {
35864
- lifecycleCtx,
36015
+ onActionComplete,
35865
36016
  paramScope,
35866
36017
  });
35867
36018
  }
35868
36019
  return createActionAffectingManyItems(verb, restCallback, {
35869
- lifecycleCtx,
36020
+ onActionComplete,
35870
36021
  paramScope,
35871
36022
  });
35872
36023
  };
35873
36024
  const createActionAffectingOneItem = (
35874
36025
  verb,
35875
36026
  callback,
35876
- { lifecycleCtx, paramScope },
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
- let item;
35886
- if (Array.isArray(result)) {
35887
- // the callback is returning something like [property, value, props]
35888
- // this is to support a case like:
35889
- // store.upsert("name", "currentName", { name: "newName" })
35890
- // where we want to update the idKey of an item
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
- const callerInfo = getCallerInfo(null, 2);
35900
- // Provide more fallback options for better debugging
35901
- const locationInfo =
35902
- callerInfo.file && callerInfo.line && callerInfo.column
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
- throw new TypeError(
35915
- `${actionLabel} must return an object (that will be used to drop "${name}" resource), received ${result}.
35916
- ${originalActionName} source location: ${locationInfo}`,
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
- throw new TypeError(
35923
- `${actionLabel} must return an object (that will be used to upsert "${name}" resource), received ${result}.
35924
- ${originalActionName} source location: ${locationInfo}`,
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: (actionCompleted) => {
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
- { lifecycleCtx, paramScope },
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
- const idArray = itemArray.map((item) => item[idKey]);
35955
- return idArray;
36086
+ return itemArray.map((item) => item[idKey]);
35956
36087
  };
35957
36088
 
35958
- const actionAffectingManyItems = createAction(callback, {
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
- lifecycleCtx.onComplete(actionCompleted);
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
- /** so that when a tracked property changes
36069
- * on an item the corresponding signal is updated automatically.
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.