@mmstack/resource 20.8.2 → 20.8.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.
@@ -874,13 +874,46 @@ const SAFE_RAW_HEADERS = new Set([
874
874
  'content-language',
875
875
  'content-type',
876
876
  ]);
877
+ const UNSAFE_HEADER_MESSAGES = new Map([
878
+ [
879
+ 'cookie',
880
+ "[@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.",
881
+ ],
882
+ [
883
+ 'set-cookie',
884
+ "[@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.",
885
+ ],
886
+ [
887
+ 'authorization',
888
+ "[@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.",
889
+ ],
890
+ [
891
+ 'x-request-id',
892
+ "[@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.",
893
+ ],
894
+ [
895
+ 'x-correlation-id',
896
+ "[@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.",
897
+ ],
898
+ [
899
+ 'if-none-match',
900
+ "[@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.",
901
+ ],
902
+ [
903
+ 'if-modified-since',
904
+ "[@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.",
905
+ ],
906
+ ]);
877
907
  function normalizeVaryHeaders(headers, names) {
908
+ const isDev = isDevMode();
878
909
  return names
879
910
  .map((n) => n.toLowerCase())
880
911
  .toSorted()
881
912
  .map((name) => {
882
- if (isDevMode() && (name === 'cookie' || name === 'set-cookie')) {
883
- 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.`);
913
+ if (isDev) {
914
+ const warning = UNSAFE_HEADER_MESSAGES.get(name);
915
+ if (warning)
916
+ console.warn(warning);
884
917
  }
885
918
  const value = readHeader(headers, name);
886
919
  if (value === null)
@@ -894,13 +927,17 @@ function normalizeVaryHeaders(headers, names) {
894
927
  .join('&');
895
928
  }
896
929
  function normalizeParams(params) {
897
- const p = params instanceof HttpParams ? params : new HttpParams({ fromObject: params });
930
+ const p = params instanceof HttpParams
931
+ ? params
932
+ : new HttpParams({ fromObject: params });
898
933
  return p
899
934
  .keys()
900
935
  .toSorted()
901
936
  .map((key) => {
902
937
  const encodedKey = encodeURIComponent(key);
903
- return (p.getAll(key) ?? []).map((v) => `${encodedKey}=${encodeURIComponent(v)}`).join('&');
938
+ return (p.getAll(key) ?? [])
939
+ .map((v) => `${encodedKey}=${encodeURIComponent(v)}`)
940
+ .join('&');
904
941
  })
905
942
  .join('&');
906
943
  }
@@ -920,7 +957,8 @@ function hashBody(body) {
920
957
  entries.sort(([ak, av], [bk, bv]) => ak.localeCompare(bk) || av.localeCompare(bv));
921
958
  return `FormData:${entries.map(([k, v]) => `${k}=${v}`).join('&')}`;
922
959
  }
923
- if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
960
+ if (typeof URLSearchParams !== 'undefined' &&
961
+ body instanceof URLSearchParams) {
924
962
  const sp = new URLSearchParams(body);
925
963
  sp.sort();
926
964
  return `URLSearchParams:${sp.toString()}`;
@@ -1666,6 +1704,57 @@ function hasSlowConnection() {
1666
1704
  return false;
1667
1705
  }
1668
1706
 
1707
+ /**
1708
+ * Deep merges multiple circuit breaker options.
1709
+ * The latter options override the former.
1710
+ */
1711
+ function mergeCircuitBreakerOptions(global, query, local) {
1712
+ if (!global && !query && !local)
1713
+ return undefined;
1714
+ return {
1715
+ ...(global === true ? {} : global),
1716
+ ...(query === true ? {} : query),
1717
+ ...(local === true ? {} : local),
1718
+ };
1719
+ }
1720
+ /**
1721
+ * Deep merges multiple retry options.
1722
+ * The latter options override the former.
1723
+ */
1724
+ function mergeRetryOptions(global, query, local) {
1725
+ if (global === undefined && query === undefined && local === undefined)
1726
+ return undefined;
1727
+ return {
1728
+ ...(typeof global === 'number' ? { max: global } : global),
1729
+ ...(typeof query === 'number' ? { max: query } : query),
1730
+ ...(typeof local === 'number' ? { max: local } : local),
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Deep merges multiple cache options.
1735
+ * The latter options override the former.
1736
+ */
1737
+ function mergeCacheOptions(query, local) {
1738
+ if (query === undefined && local === undefined)
1739
+ return undefined;
1740
+ return {
1741
+ ...(query === true ? {} : query),
1742
+ ...(local === true ? {} : local),
1743
+ };
1744
+ }
1745
+ /**
1746
+ * Deep merges multiple refresh options.
1747
+ * The latter options override the former.
1748
+ */
1749
+ function mergeRefreshOptions(query, local) {
1750
+ if (query === undefined && local === undefined)
1751
+ return undefined;
1752
+ return {
1753
+ ...(typeof query === 'number' ? { interval: query } : query),
1754
+ ...(typeof local === 'number' ? { interval: local } : local),
1755
+ };
1756
+ }
1757
+
1669
1758
  function persistResourceValues(resource, shouldPersist = false, equal) {
1670
1759
  if (!shouldPersist)
1671
1760
  return resource;
@@ -1847,10 +1936,16 @@ function injectQueryResourceOptions(injector) {
1847
1936
  const PAUSED = Symbol('@mmstack/resource:paused');
1848
1937
  function queryResource(request, options0) {
1849
1938
  // Two-layer option injection: per-call > provideQueryResourceOptions > provideResourceOptions.
1939
+ const globalOpts = injectResourceOptions(options0?.injector);
1940
+ const queryOpts = injectQueryResourceOptions(options0?.injector);
1850
1941
  const options = {
1851
- ...injectResourceOptions(options0?.injector),
1852
- ...injectQueryResourceOptions(options0?.injector),
1942
+ ...globalOpts,
1943
+ ...queryOpts,
1853
1944
  ...options0,
1945
+ cache: mergeCacheOptions(queryOpts.cache, options0?.cache),
1946
+ circuitBreaker: mergeCircuitBreakerOptions(globalOpts.circuitBreaker, queryOpts.circuitBreaker, options0?.circuitBreaker),
1947
+ retry: mergeRetryOptions(globalOpts.retry, queryOpts.retry, options0?.retry),
1948
+ refresh: mergeRefreshOptions(queryOpts.refresh, options0?.refresh),
1854
1949
  };
1855
1950
  const cache = injectQueryCache(options?.injector);
1856
1951
  const destroyRef = options?.injector
@@ -2369,10 +2464,14 @@ function injectMutationResourceOptions(injector) {
2369
2464
  */
2370
2465
  function mutationResource(request, options0 = {}) {
2371
2466
  // Two-layer option injection: per-call > provideMutationResourceOptions > provideResourceOptions.
2467
+ const globalOpts = injectResourceOptions(options0.injector);
2468
+ const mutOpts = injectMutationResourceOptions(options0.injector);
2372
2469
  const options = {
2373
- ...injectResourceOptions(options0.injector),
2374
- ...injectMutationResourceOptions(options0.injector),
2470
+ ...globalOpts,
2471
+ ...mutOpts,
2375
2472
  ...options0,
2473
+ circuitBreaker: mergeCircuitBreakerOptions(globalOpts.circuitBreaker, mutOpts.circuitBreaker, options0?.circuitBreaker),
2474
+ retry: mergeRetryOptions(globalOpts.retry, mutOpts.retry, options0?.retry),
2376
2475
  };
2377
2476
  // `register` is pulled out (and forced off on the inner query below) so the mutation ref is
2378
2477
  // the only thing registered into the transition scope, not its internal query resource.
@@ -2423,7 +2522,7 @@ function mutationResource(request, options0 = {}) {
2423
2522
  if (isDevMode())
2424
2523
  console.error('[@mmstack/resource]: error thrown in onMutate hook, mutation was not applied', mutationErr);
2425
2524
  }
2426
- }, ...(ngDevMode ? [{ debugName: "queueRef" }] : []));
2525
+ }, ...(ngDevMode ? [{ debugName: "queueRef", injector: options.injector }] : [{ injector: options.injector }]));
2427
2526
  const req = computed(() => {
2428
2527
  const nr = next();
2429
2528
  if (nr === NULL_VALUE)
@@ -2494,9 +2593,13 @@ function mutationResource(request, options0 = {}) {
2494
2593
  const destroyRef = options.injector
2495
2594
  ? options.injector.get(DestroyRef)
2496
2595
  : inject(DestroyRef);
2497
- const error$ = toObservable(resource.error);
2498
- const value$ = toObservable(resource.value).pipe(catchError(() => of(NULL_VALUE)));
2499
- const statusSub = toObservable(resource.status)
2596
+ const error$ = toObservable(resource.error, { injector: options.injector });
2597
+ const value$ = toObservable(resource.value, {
2598
+ injector: options.injector,
2599
+ }).pipe(catchError(() => of(NULL_VALUE)));
2600
+ const statusSub = toObservable(resource.status, {
2601
+ injector: options.injector,
2602
+ })
2500
2603
  .pipe(combineLatestWith(error$, value$), map(([status, error, value]) => {
2501
2604
  if (status === 'error' && error) {
2502
2605
  return {
@@ -2588,5 +2691,5 @@ function mutationResource(request, options0 = {}) {
2588
2691
  * Generated bundle index. Do not edit.
2589
2692
  */
2590
2693
 
2591
- export { Cache, PAUSED, applyResourceRegistration, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, infiniteQueryResource, injectQueryCache, injectResourceOptions, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideMutationResourceOptions, provideQueryCache, provideQueryResourceOptions, provideResourceOptions, provideTypedResourceOptions, queryResource };
2694
+ export { Cache, PAUSED, applyResourceRegistration, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, hashRequest, infiniteQueryResource, injectQueryCache, injectResourceOptions, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideMutationResourceOptions, provideQueryCache, provideQueryResourceOptions, provideResourceOptions, provideTypedResourceOptions, queryResource };
2592
2695
  //# sourceMappingURL=mmstack-resource.mjs.map