@mmstack/resource 21.4.2 → 21.4.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.
@@ -837,13 +837,46 @@ const SAFE_RAW_HEADERS = new Set([
837
837
  'content-language',
838
838
  'content-type',
839
839
  ]);
840
+ const UNSAFE_HEADER_MESSAGES = new Map([
841
+ [
842
+ 'cookie',
843
+ "[@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.",
844
+ ],
845
+ [
846
+ 'set-cookie',
847
+ "[@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.",
848
+ ],
849
+ [
850
+ 'authorization',
851
+ "[@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.",
852
+ ],
853
+ [
854
+ 'x-request-id',
855
+ "[@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.",
856
+ ],
857
+ [
858
+ 'x-correlation-id',
859
+ "[@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.",
860
+ ],
861
+ [
862
+ 'if-none-match',
863
+ "[@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.",
864
+ ],
865
+ [
866
+ 'if-modified-since',
867
+ "[@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.",
868
+ ],
869
+ ]);
840
870
  function normalizeVaryHeaders(headers, names) {
871
+ const isDev = isDevMode();
841
872
  return names
842
873
  .map((n) => n.toLowerCase())
843
874
  .toSorted()
844
875
  .map((name) => {
845
- if (isDevMode() && (name === 'cookie' || name === 'set-cookie')) {
846
- 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.`);
876
+ if (isDev) {
877
+ const warning = UNSAFE_HEADER_MESSAGES.get(name);
878
+ if (warning)
879
+ console.warn(warning);
847
880
  }
848
881
  const value = readHeader(headers, name);
849
882
  if (value === null)
@@ -857,13 +890,17 @@ function normalizeVaryHeaders(headers, names) {
857
890
  .join('&');
858
891
  }
859
892
  function normalizeParams(params) {
860
- const p = params instanceof HttpParams ? params : new HttpParams({ fromObject: params });
893
+ const p = params instanceof HttpParams
894
+ ? params
895
+ : new HttpParams({ fromObject: params });
861
896
  return p
862
897
  .keys()
863
898
  .toSorted()
864
899
  .map((key) => {
865
900
  const encodedKey = encodeURIComponent(key);
866
- return (p.getAll(key) ?? []).map((v) => `${encodedKey}=${encodeURIComponent(v)}`).join('&');
901
+ return (p.getAll(key) ?? [])
902
+ .map((v) => `${encodedKey}=${encodeURIComponent(v)}`)
903
+ .join('&');
867
904
  })
868
905
  .join('&');
869
906
  }
@@ -883,7 +920,8 @@ function hashBody(body) {
883
920
  entries.sort(([ak, av], [bk, bv]) => ak.localeCompare(bk) || av.localeCompare(bv));
884
921
  return `FormData:${entries.map(([k, v]) => `${k}=${v}`).join('&')}`;
885
922
  }
886
- if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
923
+ if (typeof URLSearchParams !== 'undefined' &&
924
+ body instanceof URLSearchParams) {
887
925
  const sp = new URLSearchParams(body);
888
926
  sp.sort();
889
927
  return `URLSearchParams:${sp.toString()}`;
@@ -1629,6 +1667,57 @@ function hasSlowConnection() {
1629
1667
  return false;
1630
1668
  }
1631
1669
 
1670
+ /**
1671
+ * Deep merges multiple circuit breaker options.
1672
+ * The latter options override the former.
1673
+ */
1674
+ function mergeCircuitBreakerOptions(global, query, local) {
1675
+ if (!global && !query && !local)
1676
+ return undefined;
1677
+ return {
1678
+ ...(global === true ? {} : global),
1679
+ ...(query === true ? {} : query),
1680
+ ...(local === true ? {} : local),
1681
+ };
1682
+ }
1683
+ /**
1684
+ * Deep merges multiple retry options.
1685
+ * The latter options override the former.
1686
+ */
1687
+ function mergeRetryOptions(global, query, local) {
1688
+ if (global === undefined && query === undefined && local === undefined)
1689
+ return undefined;
1690
+ return {
1691
+ ...(typeof global === 'number' ? { max: global } : global),
1692
+ ...(typeof query === 'number' ? { max: query } : query),
1693
+ ...(typeof local === 'number' ? { max: local } : local),
1694
+ };
1695
+ }
1696
+ /**
1697
+ * Deep merges multiple cache options.
1698
+ * The latter options override the former.
1699
+ */
1700
+ function mergeCacheOptions(query, local) {
1701
+ if (query === undefined && local === undefined)
1702
+ return undefined;
1703
+ return {
1704
+ ...(query === true ? {} : query),
1705
+ ...(local === true ? {} : local),
1706
+ };
1707
+ }
1708
+ /**
1709
+ * Deep merges multiple refresh options.
1710
+ * The latter options override the former.
1711
+ */
1712
+ function mergeRefreshOptions(query, local) {
1713
+ if (query === undefined && local === undefined)
1714
+ return undefined;
1715
+ return {
1716
+ ...(typeof query === 'number' ? { interval: query } : query),
1717
+ ...(typeof local === 'number' ? { interval: local } : local),
1718
+ };
1719
+ }
1720
+
1632
1721
  function persistResourceValues(resource, shouldPersist = false, equal) {
1633
1722
  if (!shouldPersist)
1634
1723
  return resource;
@@ -1848,10 +1937,16 @@ function injectQueryResourceOptions(injector) {
1848
1937
  const PAUSED = Symbol('@mmstack/resource:paused');
1849
1938
  function queryResource(request, options0) {
1850
1939
  // Two-layer option injection: per-call > provideQueryResourceOptions > provideResourceOptions.
1940
+ const globalOpts = injectResourceOptions(options0?.injector);
1941
+ const queryOpts = injectQueryResourceOptions(options0?.injector);
1851
1942
  const options = {
1852
- ...injectResourceOptions(options0?.injector),
1853
- ...injectQueryResourceOptions(options0?.injector),
1943
+ ...globalOpts,
1944
+ ...queryOpts,
1854
1945
  ...options0,
1946
+ cache: mergeCacheOptions(queryOpts.cache, options0?.cache),
1947
+ circuitBreaker: mergeCircuitBreakerOptions(globalOpts.circuitBreaker, queryOpts.circuitBreaker, options0?.circuitBreaker),
1948
+ retry: mergeRetryOptions(globalOpts.retry, queryOpts.retry, options0?.retry),
1949
+ refresh: mergeRefreshOptions(queryOpts.refresh, options0?.refresh),
1855
1950
  };
1856
1951
  const cache = injectQueryCache(options?.injector);
1857
1952
  const destroyRef = options?.injector
@@ -2323,10 +2418,14 @@ function injectMutationResourceOptions(injector) {
2323
2418
  */
2324
2419
  function mutationResource(request, options0 = {}) {
2325
2420
  // Two-layer option injection: per-call > provideMutationResourceOptions > provideResourceOptions.
2421
+ const globalOpts = injectResourceOptions(options0.injector);
2422
+ const mutOpts = injectMutationResourceOptions(options0.injector);
2326
2423
  const options = {
2327
- ...injectResourceOptions(options0.injector),
2328
- ...injectMutationResourceOptions(options0.injector),
2424
+ ...globalOpts,
2425
+ ...mutOpts,
2329
2426
  ...options0,
2427
+ circuitBreaker: mergeCircuitBreakerOptions(globalOpts.circuitBreaker, mutOpts.circuitBreaker, options0?.circuitBreaker),
2428
+ retry: mergeRetryOptions(globalOpts.retry, mutOpts.retry, options0?.retry),
2330
2429
  };
2331
2430
  // `register` is pulled out (and forced off on the inner query below) so the mutation ref is
2332
2431
  // the only thing registered into the transition scope, not its internal query resource.
@@ -2367,7 +2466,7 @@ function mutationResource(request, options0 = {}) {
2367
2466
  if (isDevMode())
2368
2467
  console.error('[@mmstack/resource]: error thrown in onMutate hook, mutation was not applied', mutationErr);
2369
2468
  }
2370
- }, ...(ngDevMode ? [{ debugName: "queueRef" }] : /* istanbul ignore next */ []));
2469
+ }, { ...(ngDevMode ? { debugName: "queueRef" } : /* istanbul ignore next */ {}), injector: options.injector });
2371
2470
  const req = computed(() => {
2372
2471
  const nr = next();
2373
2472
  if (nr === NULL_VALUE)
@@ -2413,9 +2512,13 @@ function mutationResource(request, options0 = {}) {
2413
2512
  const destroyRef = options.injector
2414
2513
  ? options.injector.get(DestroyRef)
2415
2514
  : inject(DestroyRef);
2416
- const error$ = toObservable(resource.error);
2417
- const value$ = toObservable(resource.value).pipe(catchError(() => of(NULL_VALUE)));
2418
- const statusSub = toObservable(resource.status)
2515
+ const error$ = toObservable(resource.error, { injector: options.injector });
2516
+ const value$ = toObservable(resource.value, {
2517
+ injector: options.injector,
2518
+ }).pipe(catchError(() => of(NULL_VALUE)));
2519
+ const statusSub = toObservable(resource.status, {
2520
+ injector: options.injector,
2521
+ })
2419
2522
  .pipe(combineLatestWith(error$, value$), map(([status, error, value]) => {
2420
2523
  if (status === 'error' && error) {
2421
2524
  return {
@@ -2507,5 +2610,5 @@ function mutationResource(request, options0 = {}) {
2507
2610
  * Generated bundle index. Do not edit.
2508
2611
  */
2509
2612
 
2510
- export { Cache, PAUSED, applyResourceRegistration, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, infiniteQueryResource, injectQueryCache, injectResourceOptions, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideMutationResourceOptions, provideQueryCache, provideQueryResourceOptions, provideResourceOptions, provideTypedResourceOptions, queryResource };
2613
+ export { Cache, PAUSED, applyResourceRegistration, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, hashRequest, infiniteQueryResource, injectQueryCache, injectResourceOptions, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideMutationResourceOptions, provideQueryCache, provideQueryResourceOptions, provideResourceOptions, provideTypedResourceOptions, queryResource };
2511
2614
  //# sourceMappingURL=mmstack-resource.mjs.map