@mmstack/resource 22.1.2 → 22.1.4

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.
@@ -841,13 +841,46 @@ const SAFE_RAW_HEADERS = new Set([
841
841
  'content-language',
842
842
  'content-type',
843
843
  ]);
844
+ const UNSAFE_HEADER_MESSAGES = new Map([
845
+ [
846
+ 'cookie',
847
+ "[@mmstack/resource]: varyHeaders includes 'cookie'. Browser-attached cookies never appear on the request object (so this usually partitions nothing), and manually-set cookie values often rotate per-request (shredding the hit rate). The header IS still honored (digested) — but prefer varying on 'Authorization' or a tenant header.",
848
+ ],
849
+ [
850
+ 'set-cookie',
851
+ "[@mmstack/resource]: varyHeaders includes 'set-cookie'. Browser-attached cookies never appear on the request object (so this usually partitions nothing), and manually-set cookie values often rotate per-request (shredding the hit rate). The header IS still honored (digested) — but prefer varying on 'Authorization' or a tenant header.",
852
+ ],
853
+ [
854
+ 'authorization',
855
+ "[@mmstack/resource]: varyHeaders includes 'Authorization'. If your token rotates frequently (e.g., short-lived JWTs), this will cause 100% cache churn on refresh. Consider adding a namespace prefix with the users sub, not using it as a cache-key or using a custom 'cache.hash' function with a stable session/user ID instead.",
856
+ ],
857
+ [
858
+ 'x-request-id',
859
+ "[@mmstack/resource]: varyHeaders includes 'X-Request-ID'. This header is often set to a unique value per-request, which will cause 100% cache churn. Consider removing it from varyHeaders or using a custom 'cache.hash' function that ignores it.",
860
+ ],
861
+ [
862
+ 'x-correlation-id',
863
+ "[@mmstack/resource]: varyHeaders includes 'X-Correlation-ID'. This header is often set to a unique value per-request, which will cause 100% cache churn. Consider removing it from varyHeaders or using a custom 'cache.hash' function that ignores it.",
864
+ ],
865
+ [
866
+ 'if-none-match',
867
+ "[@mmstack/resource]: varyHeaders includes 'If-None-Match'. This header contains ETags that change whenever the server's resource version changes, which will cause cache misses on every update. Consider removing it from varyHeaders or using a custom 'cache.hash' function that ignores it.",
868
+ ],
869
+ [
870
+ 'if-modified-since',
871
+ "[@mmstack/resource]: varyHeaders includes 'If-Modified-Since'. This header contains timestamps that change whenever the server's resource version changes, which will cause cache misses on every update. Consider removing it from varyHeaders or using a custom 'cache.hash' function that ignores it.",
872
+ ],
873
+ ]);
844
874
  function normalizeVaryHeaders(headers, names) {
875
+ const isDev = isDevMode();
845
876
  return names
846
877
  .map((n) => n.toLowerCase())
847
878
  .toSorted()
848
879
  .map((name) => {
849
- if (isDevMode() && (name === 'cookie' || name === 'set-cookie')) {
850
- console.warn(`[@mmstack/resource]: varyHeaders includes '${name}'. Browser-attached cookies never appear on the request object (so this usually partitions nothing), and manually-set cookie values often rotate per-request (shredding the hit rate). The header IS still honored (digested) — but prefer varying on 'Authorization' or a tenant header.`);
880
+ if (isDev) {
881
+ const warning = UNSAFE_HEADER_MESSAGES.get(name);
882
+ if (warning)
883
+ console.warn(warning);
851
884
  }
852
885
  const value = readHeader(headers, name);
853
886
  if (value === null)
@@ -861,13 +894,17 @@ function normalizeVaryHeaders(headers, names) {
861
894
  .join('&');
862
895
  }
863
896
  function normalizeParams(params) {
864
- const p = params instanceof HttpParams ? params : new HttpParams({ fromObject: params });
897
+ const p = params instanceof HttpParams
898
+ ? params
899
+ : new HttpParams({ fromObject: params });
865
900
  return p
866
901
  .keys()
867
902
  .toSorted()
868
903
  .map((key) => {
869
904
  const encodedKey = encodeURIComponent(key);
870
- return (p.getAll(key) ?? []).map((v) => `${encodedKey}=${encodeURIComponent(v)}`).join('&');
905
+ return (p.getAll(key) ?? [])
906
+ .map((v) => `${encodedKey}=${encodeURIComponent(v)}`)
907
+ .join('&');
871
908
  })
872
909
  .join('&');
873
910
  }
@@ -887,7 +924,8 @@ function hashBody(body) {
887
924
  entries.sort(([ak, av], [bk, bv]) => ak.localeCompare(bk) || av.localeCompare(bv));
888
925
  return `FormData:${entries.map(([k, v]) => `${k}=${v}`).join('&')}`;
889
926
  }
890
- if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
927
+ if (typeof URLSearchParams !== 'undefined' &&
928
+ body instanceof URLSearchParams) {
891
929
  const sp = new URLSearchParams(body);
892
930
  sp.sort();
893
931
  return `URLSearchParams:${sp.toString()}`;
@@ -1640,6 +1678,57 @@ function hasSlowConnection() {
1640
1678
  return false;
1641
1679
  }
1642
1680
 
1681
+ /**
1682
+ * Deep merges multiple circuit breaker options.
1683
+ * The latter options override the former.
1684
+ */
1685
+ function mergeCircuitBreakerOptions(global, query, local) {
1686
+ if (!global && !query && !local)
1687
+ return undefined;
1688
+ return {
1689
+ ...(global === true ? {} : global),
1690
+ ...(query === true ? {} : query),
1691
+ ...(local === true ? {} : local),
1692
+ };
1693
+ }
1694
+ /**
1695
+ * Deep merges multiple retry options.
1696
+ * The latter options override the former.
1697
+ */
1698
+ function mergeRetryOptions(global, query, local) {
1699
+ if (global === undefined && query === undefined && local === undefined)
1700
+ return undefined;
1701
+ return {
1702
+ ...(typeof global === 'number' ? { max: global } : global),
1703
+ ...(typeof query === 'number' ? { max: query } : query),
1704
+ ...(typeof local === 'number' ? { max: local } : local),
1705
+ };
1706
+ }
1707
+ /**
1708
+ * Deep merges multiple cache options.
1709
+ * The latter options override the former.
1710
+ */
1711
+ function mergeCacheOptions(query, local) {
1712
+ if (query === undefined && local === undefined)
1713
+ return undefined;
1714
+ return {
1715
+ ...(query === true ? {} : query),
1716
+ ...(local === true ? {} : local),
1717
+ };
1718
+ }
1719
+ /**
1720
+ * Deep merges multiple refresh options.
1721
+ * The latter options override the former.
1722
+ */
1723
+ function mergeRefreshOptions(query, local) {
1724
+ if (query === undefined && local === undefined)
1725
+ return undefined;
1726
+ return {
1727
+ ...(typeof query === 'number' ? { interval: query } : query),
1728
+ ...(typeof local === 'number' ? { interval: local } : local),
1729
+ };
1730
+ }
1731
+
1643
1732
  function persistResourceValues(resource, shouldPersist = false, equal) {
1644
1733
  if (!shouldPersist)
1645
1734
  return resource;
@@ -1860,10 +1949,16 @@ function injectQueryResourceOptions(injector) {
1860
1949
  const PAUSED = Symbol('@mmstack/resource:paused');
1861
1950
  function queryResource(request, options0) {
1862
1951
  // Two-layer option injection: per-call > provideQueryResourceOptions > provideResourceOptions.
1952
+ const globalOpts = injectResourceOptions(options0?.injector);
1953
+ const queryOpts = injectQueryResourceOptions(options0?.injector);
1863
1954
  const options = {
1864
- ...injectResourceOptions(options0?.injector),
1865
- ...injectQueryResourceOptions(options0?.injector),
1955
+ ...globalOpts,
1956
+ ...queryOpts,
1866
1957
  ...options0,
1958
+ cache: mergeCacheOptions(queryOpts.cache, options0?.cache),
1959
+ circuitBreaker: mergeCircuitBreakerOptions(globalOpts.circuitBreaker, queryOpts.circuitBreaker, options0?.circuitBreaker),
1960
+ retry: mergeRetryOptions(globalOpts.retry, queryOpts.retry, options0?.retry),
1961
+ refresh: mergeRefreshOptions(queryOpts.refresh, options0?.refresh),
1867
1962
  };
1868
1963
  const cache = injectQueryCache(options?.injector);
1869
1964
  const destroyRef = options?.injector
@@ -2346,10 +2441,14 @@ function injectMutationResourceOptions(injector) {
2346
2441
  */
2347
2442
  function mutationResource(request, options0 = {}) {
2348
2443
  // Two-layer option injection: per-call > provideMutationResourceOptions > provideResourceOptions.
2444
+ const globalOpts = injectResourceOptions(options0.injector);
2445
+ const mutOpts = injectMutationResourceOptions(options0.injector);
2349
2446
  const options = {
2350
- ...injectResourceOptions(options0.injector),
2351
- ...injectMutationResourceOptions(options0.injector),
2447
+ ...globalOpts,
2448
+ ...mutOpts,
2352
2449
  ...options0,
2450
+ circuitBreaker: mergeCircuitBreakerOptions(globalOpts.circuitBreaker, mutOpts.circuitBreaker, options0?.circuitBreaker),
2451
+ retry: mergeRetryOptions(globalOpts.retry, mutOpts.retry, options0?.retry),
2353
2452
  };
2354
2453
  // `register` is pulled out (and forced off on the inner query below) so the mutation ref is
2355
2454
  // the only thing registered into the transition scope, not its internal query resource.
@@ -2391,8 +2490,7 @@ function mutationResource(request, options0 = {}) {
2391
2490
  if (isDevMode())
2392
2491
  console.error('[@mmstack/resource]: error thrown in onMutate hook, mutation was not applied', mutationErr);
2393
2492
  }
2394
- }, /* @ts-ignore */
2395
- ...(ngDevMode ? [{ debugName: "queueRef" }] : /* istanbul ignore next */ []));
2493
+ }, { ...(ngDevMode ? { debugName: "queueRef" } : /* istanbul ignore next */ {}), injector: options.injector });
2396
2494
  const req = computed(() => {
2397
2495
  const nr = next();
2398
2496
  if (nr === NULL_VALUE)
@@ -2438,9 +2536,13 @@ function mutationResource(request, options0 = {}) {
2438
2536
  const destroyRef = options.injector
2439
2537
  ? options.injector.get(DestroyRef)
2440
2538
  : inject(DestroyRef);
2441
- const error$ = toObservable(resource.error);
2442
- const value$ = toObservable(resource.value).pipe(catchError(() => of(NULL_VALUE)));
2443
- const statusSub = toObservable(resource.status)
2539
+ const error$ = toObservable(resource.error, { injector: options.injector });
2540
+ const value$ = toObservable(resource.value, {
2541
+ injector: options.injector,
2542
+ }).pipe(catchError(() => of(NULL_VALUE)));
2543
+ const statusSub = toObservable(resource.status, {
2544
+ injector: options.injector,
2545
+ })
2444
2546
  .pipe(combineLatestWith(error$, value$), map(([status, error, value]) => {
2445
2547
  if (status === 'error' && error) {
2446
2548
  return {
@@ -2532,5 +2634,5 @@ function mutationResource(request, options0 = {}) {
2532
2634
  * Generated bundle index. Do not edit.
2533
2635
  */
2534
2636
 
2535
- export { Cache, PAUSED, applyResourceRegistration, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, infiniteQueryResource, injectQueryCache, injectResourceOptions, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideMutationResourceOptions, provideQueryCache, provideQueryResourceOptions, provideResourceOptions, provideTypedResourceOptions, queryResource };
2637
+ export { Cache, PAUSED, applyResourceRegistration, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, hashRequest, infiniteQueryResource, injectQueryCache, injectResourceOptions, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideMutationResourceOptions, provideQueryCache, provideQueryResourceOptions, provideResourceOptions, provideTypedResourceOptions, queryResource };
2536
2638
  //# sourceMappingURL=mmstack-resource.mjs.map