@mmstack/resource 21.1.0 → 21.1.2

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.
@@ -1,7 +1,7 @@
1
+ import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext, HttpParams, httpResource, HttpClient } from '@angular/common/http';
1
2
  import * as i0 from '@angular/core';
2
3
  import { isDevMode, untracked, computed, InjectionToken, inject, signal, effect, Injector, linkedSignal, Injectable, DestroyRef } from '@angular/core';
3
4
  import { mutable, toWritable, sensor, nestedEffect } from '@mmstack/primitives';
4
- import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext, HttpParams, httpResource, HttpClient } from '@angular/common/http';
5
5
  import { of, tap, map, finalize, shareReplay, interval, firstValueFrom, catchError, combineLatestWith, filter } from 'rxjs';
6
6
  import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
7
7
 
@@ -193,6 +193,12 @@ class Cache {
193
193
  const value = syncTabs.deserialize(msg.entry.value);
194
194
  if (value === null)
195
195
  return;
196
+ // Last-write-wins by `updated` timestamp. If our local entry was
197
+ // written more recently than the broadcast we just received, the
198
+ // broadcast is stale (in-flight when we wrote locally) — drop it.
199
+ const existing = untracked(this.internal).get(msg.entry.key);
200
+ if (existing && existing.updated >= msg.entry.updated)
201
+ return;
196
202
  this.storeInternal(msg.entry.key, value, msg.entry.stale - msg.entry.updated, msg.entry.expiresAt - msg.entry.updated, true, false);
197
203
  }
198
204
  else if (msg.action === 'invalidate') {
@@ -239,7 +245,7 @@ class Cache {
239
245
  }
240
246
  /** @internal */
241
247
  getInternal(key) {
242
- const keySignal = computed(() => key(), ...(ngDevMode ? [{ debugName: "keySignal" }] : []));
248
+ const keySignal = computed(() => key(), ...(ngDevMode ? [{ debugName: "keySignal" }] : /* istanbul ignore next */ []));
243
249
  return computed(() => {
244
250
  const key = keySignal();
245
251
  if (!key)
@@ -340,6 +346,31 @@ class Cache {
340
346
  invalidate(key) {
341
347
  this.invalidateInternal(key);
342
348
  }
349
+ /**
350
+ * Invalidates every cache entry whose key starts with `prefix`. Common after a
351
+ * list-mutating operation (e.g. invalidate every paginated `GET /api/posts*`
352
+ * after a POST). Returns the number of entries removed.
353
+ *
354
+ * @example
355
+ * cache.invalidatePrefix('GET https://api.example.com/posts');
356
+ */
357
+ invalidatePrefix(prefix) {
358
+ return this.invalidateWhere((key) => key.startsWith(prefix));
359
+ }
360
+ /**
361
+ * Invalidates every cache entry whose key matches the predicate. Use for
362
+ * arbitrary bulk invalidation that doesn't fit prefix matching (e.g.
363
+ * "everything containing `userId=42`"). Returns the number of entries removed.
364
+ *
365
+ * @example
366
+ * cache.invalidateWhere((key) => key.includes('/me/'));
367
+ */
368
+ invalidateWhere(predicate) {
369
+ const keys = Array.from(untracked(this.internal).keys()).filter(predicate);
370
+ for (const key of keys)
371
+ this.invalidateInternal(key);
372
+ return keys.length;
373
+ }
343
374
  invalidateInternal(key, fromSync = false) {
344
375
  const entry = this.getUntracked(key);
345
376
  if (!entry)
@@ -476,6 +507,7 @@ function provideQueryCache(opt) {
476
507
  };
477
508
  }
478
509
  class NoopCache extends Cache {
510
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
479
511
  store(_, __, ___ = super.staleTime, ____ = super.ttl) {
480
512
  // noop
481
513
  }
@@ -682,7 +714,9 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
682
714
  });
683
715
  }
684
716
  return next(req).pipe(tap((event) => {
685
- if (event instanceof HttpResponse && event.ok) {
717
+ if (!(event instanceof HttpResponse))
718
+ return;
719
+ if (event.ok) {
686
720
  const cacheControl = parseCacheControlHeader(event);
687
721
  if (cacheControl.noStore && !opt.ignoreCacheControl)
688
722
  return;
@@ -701,6 +735,17 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
701
735
  })
702
736
  : event;
703
737
  cache.store(key, parsedResponse, staleTime, ttl, opt.persist);
738
+ return;
739
+ }
740
+ // 304 → server confirmed our cached entry is still valid. Re-stamp the
741
+ // existing entry so subsequent reads within the new freshness window
742
+ // don't trigger another revalidation round-trip.
743
+ if (event.status === 304 && entry) {
744
+ const cacheControl = parseCacheControlHeader(event);
745
+ const { staleTime, ttl } = opt.ignoreCacheControl
746
+ ? opt
747
+ : resolveTimings(cacheControl, opt.staleTime, opt.ttl);
748
+ cache.store(key, entry.value, staleTime, ttl, opt.persist);
704
749
  }
705
750
  }), map((event) => {
706
751
  // handle 304 responses due to eTag/last-modified
@@ -728,22 +773,23 @@ function catchValueError(resource, fallback) {
728
773
 
729
774
  /** @internal */
730
775
  const DEFAULT_OPTIONS = {
731
- treshold: 5,
776
+ threshold: 5,
732
777
  timeout: 30000,
733
778
  shouldFail: () => true,
734
779
  shouldFailForever: () => false,
735
780
  };
736
781
  /** @internal */
737
- function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldFail = () => true, shouldFailForever = () => false) {
738
- const halfOpen = signal(false, ...(ngDevMode ? [{ debugName: "halfOpen" }] : []));
739
- const failureCount = signal(0, ...(ngDevMode ? [{ debugName: "failureCount" }] : []));
782
+ function internalCeateCircuitBreaker(threshold = 5, resetTimeout = 30000, shouldFail = () => true, shouldFailForever = () => false) {
783
+ const halfOpen = signal(false, ...(ngDevMode ? [{ debugName: "halfOpen" }] : /* istanbul ignore next */ []));
784
+ const failureCount = signal(0, ...(ngDevMode ? [{ debugName: "failureCount" }] : /* istanbul ignore next */ []));
785
+ const failedForever = signal(false, ...(ngDevMode ? [{ debugName: "failedForever" }] : /* istanbul ignore next */ []));
740
786
  const status = computed(() => {
741
- if (failureCount() >= treshold)
787
+ if (failedForever() || failureCount() >= threshold)
742
788
  return 'OPEN';
743
789
  return halfOpen() ? 'HALF_OPEN' : 'CLOSED';
744
- }, ...(ngDevMode ? [{ debugName: "status" }] : []));
745
- const isClosed = computed(() => status() !== 'OPEN', ...(ngDevMode ? [{ debugName: "isClosed" }] : []));
746
- const isOpen = computed(() => status() !== 'CLOSED', ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
790
+ }, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
791
+ const isClosed = computed(() => status() !== 'OPEN', ...(ngDevMode ? [{ debugName: "isClosed" }] : /* istanbul ignore next */ []));
792
+ const isOpen = computed(() => status() !== 'CLOSED', ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
747
793
  const success = () => {
748
794
  failureCount.set(0);
749
795
  halfOpen.set(false);
@@ -752,30 +798,26 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
752
798
  if (!untracked(isOpen))
753
799
  return;
754
800
  halfOpen.set(true);
755
- failureCount.set(treshold - 1);
801
+ failureCount.set(threshold - 1);
756
802
  };
757
- let failForeverResetId = null;
803
+ // Auto-probe effect: schedules a half-open retry after `resetTimeout` whenever
804
+ // the breaker is open, *unless* we've been failed forever (in which case only
805
+ // hardReset() can recover).
758
806
  const effectRef = effect((cleanup) => {
759
- if (!isOpen())
807
+ if (!isOpen() || failedForever())
760
808
  return;
761
809
  const timeout = setTimeout(tryOnce, resetTimeout);
762
- failForeverResetId = timeout;
763
810
  return cleanup(() => {
764
811
  clearTimeout(timeout);
765
- failForeverResetId = null;
766
812
  });
767
- }, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
813
+ }, ...(ngDevMode ? [{ debugName: "effectRef" }] : /* istanbul ignore next */ []));
768
814
  const failInternal = () => {
769
815
  failureCount.set(failureCount() + 1);
770
816
  halfOpen.set(false);
771
817
  };
772
818
  const failForever = () => {
773
- if (failForeverResetId)
774
- clearTimeout(failForeverResetId);
775
- effectRef.destroy();
776
- failureCount.set(Infinity);
819
+ failedForever.set(true);
777
820
  halfOpen.set(false);
778
- return;
779
821
  };
780
822
  const fail = (err) => {
781
823
  if (shouldFailForever(err))
@@ -784,6 +826,11 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
784
826
  return failInternal();
785
827
  // If the error does not trigger a failure, we do nothing.
786
828
  };
829
+ const hardReset = () => {
830
+ failedForever.set(false);
831
+ failureCount.set(0);
832
+ halfOpen.set(false);
833
+ };
787
834
  return {
788
835
  status,
789
836
  isClosed,
@@ -791,6 +838,7 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
791
838
  fail,
792
839
  success,
793
840
  halfOpen: tryOnce,
841
+ hardReset,
794
842
  destroy: () => effectRef.destroy(),
795
843
  };
796
844
  }
@@ -809,18 +857,43 @@ function createNeverBrokenCircuitBreaker() {
809
857
  halfOpen: () => {
810
858
  // noop
811
859
  },
860
+ hardReset: () => {
861
+ // noop
862
+ },
812
863
  destroy: () => {
813
864
  // noop
814
865
  },
815
866
  };
816
867
  }
817
868
  const CB_DEFAULT_OPTIONS = new InjectionToken('MMSTACK_CIRCUIT_BREAKER_DEFAULT_OPTIONS');
869
+ /**
870
+ * Provides application-wide default options for {@link createCircuitBreaker}.
871
+ * Any `createCircuitBreaker()` call without explicit options (or with only
872
+ * partial options) merges these defaults in, so you can centralize threshold /
873
+ * timeout / failure-classifier behavior in one place.
874
+ *
875
+ * Per-call options always win over the provided defaults.
876
+ *
877
+ * @example
878
+ * ```ts
879
+ * bootstrapApplication(AppComponent, {
880
+ * providers: [
881
+ * provideCircuitBreakerDefaultOptions({
882
+ * threshold: 10,
883
+ * timeout: 60_000,
884
+ * shouldFailForever: (err) =>
885
+ * err instanceof HttpErrorResponse && [401, 403].includes(err.status),
886
+ * }),
887
+ * ],
888
+ * });
889
+ * ```
890
+ */
818
891
  function provideCircuitBreakerDefaultOptions(options) {
819
892
  return {
820
893
  provide: CB_DEFAULT_OPTIONS,
821
894
  useValue: {
822
895
  ...DEFAULT_OPTIONS,
823
- ...options,
896
+ ...normalizeThreshold(options),
824
897
  },
825
898
  };
826
899
  }
@@ -829,12 +902,23 @@ function injectCircuitBreakerOptions(injector = inject(Injector)) {
829
902
  optional: true,
830
903
  });
831
904
  }
905
+ /** @internal — strips the deprecated `treshold` field and folds it into `threshold` */
906
+ function normalizeThreshold(opt) {
907
+ if (!opt || typeof opt !== 'object' || 'isClosed' in opt)
908
+ return {};
909
+ const { treshold, threshold, ...rest } = opt;
910
+ return {
911
+ ...rest,
912
+ threshold: threshold ?? treshold,
913
+ };
914
+ }
832
915
  /**
833
916
  * Creates a circuit breaker instance.
834
917
  *
835
918
  * @param options - Configuration options for the circuit breaker. Can be:
836
- * - `undefined`: Creates a "no-op" circuit breaker that is always open (never trips).
837
- * - `true`: Creates a circuit breaker with default settings (threshold: 5, timeout: 30000ms).
919
+ * - `undefined`: Uses defaults (threshold: 5, timeout: 30000ms) or provided defaults via {@link provideCircuitBreakerDefaultOptions}.
920
+ * - `false`: Creates a "no-op" circuit breaker that is always closed (never trips).
921
+ * - `true`: Creates a circuit breaker with default settings.
838
922
  * - `CircuitBreaker`: Reuses an existing `CircuitBreaker` instance.
839
923
  * - `{ threshold?: number; timeout?: number; }`: Creates a circuit breaker with the specified threshold and timeout.
840
924
  *
@@ -857,11 +941,11 @@ function createCircuitBreaker(opt, injector) {
857
941
  return createNeverBrokenCircuitBreaker();
858
942
  if (typeof opt === 'object' && 'isClosed' in opt)
859
943
  return opt;
860
- const { treshold, timeout, shouldFail, shouldFailForever } = {
944
+ const { threshold, timeout, shouldFail, shouldFailForever } = {
861
945
  ...injectCircuitBreakerOptions(injector),
862
- ...opt,
946
+ ...normalizeThreshold(opt),
863
947
  };
864
- return internalCeateCircuitBreaker(treshold, timeout, shouldFail, shouldFailForever);
948
+ return internalCeateCircuitBreaker(threshold, timeout, shouldFail, shouldFailForever);
865
949
  }
866
950
 
867
951
  // Heavily inspired by: https://dev.to/kasual1/request-deduplication-in-angular-3pd8
@@ -1178,7 +1262,7 @@ function hasSlowConnection() {
1178
1262
 
1179
1263
  function persist(src, equal) {
1180
1264
  // linkedSignal allows us to access previous source value
1181
- const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : {}), source: () => src(),
1265
+ const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : /* istanbul ignore next */ {}), source: () => src(),
1182
1266
  computation: (next, prev) => {
1183
1267
  if (next === undefined && prev !== undefined)
1184
1268
  return prev.value;
@@ -1232,13 +1316,16 @@ function refresh(resource, destroyRef, refresh) {
1232
1316
  }
1233
1317
 
1234
1318
  // Retry on error, if number is provided it will retry that many times with exponential backoff, otherwise it will use the options provided
1235
- function retryOnError(res, opt) {
1319
+ function retryOnError(res, opt, onError) {
1236
1320
  const max = opt ? (typeof opt === 'number' ? opt : (opt.max ?? 0)) : 0;
1237
1321
  const backoff = typeof opt === 'object' ? (opt.backoff ?? 1000) : 1000;
1238
1322
  let retries = 0;
1239
1323
  let timeout;
1240
- const onError = () => {
1241
- if (retries >= max)
1324
+ const handleError = () => {
1325
+ const err = untracked(res.error);
1326
+ const isFinal = retries >= max;
1327
+ onError?.(err, retries, isFinal);
1328
+ if (isFinal)
1242
1329
  return;
1243
1330
  retries++;
1244
1331
  if (timeout)
@@ -1253,11 +1340,11 @@ function retryOnError(res, opt) {
1253
1340
  const ref = effect(() => {
1254
1341
  switch (res.status()) {
1255
1342
  case 'error':
1256
- return onError();
1343
+ return handleError();
1257
1344
  case 'resolved':
1258
1345
  return onSuccess();
1259
1346
  }
1260
- }, ...(ngDevMode ? [{ debugName: "ref" }] : []));
1347
+ }, ...(ngDevMode ? [{ debugName: "ref" }] : /* istanbul ignore next */ []));
1261
1348
  return {
1262
1349
  ...res,
1263
1350
  destroy: () => {
@@ -1269,10 +1356,10 @@ function retryOnError(res, opt) {
1269
1356
 
1270
1357
  class ResourceSensors {
1271
1358
  networkStatus = sensor('networkStatus');
1272
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResourceSensors, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1273
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResourceSensors, providedIn: 'root' });
1359
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ResourceSensors, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1360
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ResourceSensors, providedIn: 'root' });
1274
1361
  }
1275
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResourceSensors, decorators: [{
1362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ResourceSensors, decorators: [{
1276
1363
  type: Injectable,
1277
1364
  args: [{
1278
1365
  providedIn: 'root',
@@ -1336,16 +1423,26 @@ function queryResource(request, options) {
1336
1423
  const eq = options?.triggerOnSameRequest
1337
1424
  ? undefined
1338
1425
  : createEqualRequest(options?.equal);
1426
+ const rawRequest = computed(() => request() ?? undefined, ...(ngDevMode ? [{ debugName: "rawRequest" }] : /* istanbul ignore next */ []));
1427
+ const disabledReason = computed(() => {
1428
+ if (!networkAvailable())
1429
+ return 'offline';
1430
+ if (cb.isOpen())
1431
+ return 'circuit-open';
1432
+ if (!rawRequest())
1433
+ return 'no-request';
1434
+ return null;
1435
+ }, ...(ngDevMode ? [{ debugName: "disabledReason" }] : /* istanbul ignore next */ []));
1339
1436
  const stableRequest = computed(() => {
1340
- if (!networkAvailable() || cb.isOpen())
1437
+ if (disabledReason() !== null)
1341
1438
  return undefined;
1342
- const req = request();
1439
+ const req = rawRequest();
1343
1440
  if (!req)
1344
1441
  return undefined;
1345
1442
  if (typeof req === 'string')
1346
1443
  return { method: 'GET', url: req };
1347
1444
  return req;
1348
- }, { ...(ngDevMode ? { debugName: "stableRequest" } : {}), equal: (a, b) => {
1445
+ }, { ...(ngDevMode ? { debugName: "stableRequest" } : /* istanbul ignore next */ {}), equal: (a, b) => {
1349
1446
  if (eq)
1350
1447
  return eq(a, b);
1351
1448
  return a === b;
@@ -1360,7 +1457,7 @@ function queryResource(request, options) {
1360
1457
  if (!r)
1361
1458
  return null;
1362
1459
  return hashFn(r);
1363
- }, ...(ngDevMode ? [{ debugName: "cacheKey" }] : []));
1460
+ }, ...(ngDevMode ? [{ debugName: "cacheKey" }] : /* istanbul ignore next */ []));
1364
1461
  const bustBrowserCache = typeof options?.cache === 'object' &&
1365
1462
  options.cache.bustBrowserCache === true;
1366
1463
  const ignoreCacheControl = typeof options?.cache === 'object' &&
@@ -1391,7 +1488,7 @@ function queryResource(request, options) {
1391
1488
  resource = catchValueError(resource, options?.defaultValue);
1392
1489
  // get full HttpResonse from Cache
1393
1490
  const cachedEvent = cache.getEntryOrKey(cacheKey);
1394
- const cacheEntry = linkedSignal({ ...(ngDevMode ? { debugName: "cacheEntry" } : {}), source: () => cachedEvent(),
1491
+ const cacheEntry = linkedSignal({ ...(ngDevMode ? { debugName: "cacheEntry" } : /* istanbul ignore next */ {}), source: () => cachedEvent(),
1395
1492
  computation: (entry, prev) => {
1396
1493
  if (!entry)
1397
1494
  return null;
@@ -1411,7 +1508,7 @@ function queryResource(request, options) {
1411
1508
  };
1412
1509
  } });
1413
1510
  resource = refresh(resource, destroyRef, options?.refresh);
1414
- resource = retryOnError(resource, options?.retry);
1511
+ resource = retryOnError(resource, options?.retry, options?.onError);
1415
1512
  resource = persistResourceValues(resource, options?.keepPrevious, options?.equal);
1416
1513
  const value = options?.cache
1417
1514
  ? toWritable(computed(() => {
@@ -1419,20 +1516,6 @@ function queryResource(request, options) {
1419
1516
  return cacheEntry()?.value ?? resource.value();
1420
1517
  }), resource.value.set, resource.value.update)
1421
1518
  : resource.value;
1422
- const onError = options?.onError; // Put in own variable to ensure value remains even if options are somehow mutated in-line
1423
- if (onError) {
1424
- const onErrorRef = effect(() => {
1425
- const err = resource.error();
1426
- if (err)
1427
- onError(err);
1428
- }, ...(ngDevMode ? [{ debugName: "onErrorRef" }] : []));
1429
- // cleanup on manual destroy, I'm comfortable setting these props in-line as we have yet to 'release' the object out of this lexical scope
1430
- const destroyRest = resource.destroy;
1431
- resource.destroy = () => {
1432
- onErrorRef.destroy();
1433
- destroyRest();
1434
- };
1435
- }
1436
1519
  // iterate circuit breaker state, is effect as a computed would cause a circular dependency (resource -> cb -> resource)
1437
1520
  const cbEffectRef = effect(() => {
1438
1521
  const status = resource.status();
@@ -1440,7 +1523,7 @@ function queryResource(request, options) {
1440
1523
  cb.fail(untracked(resource.error));
1441
1524
  else if (status === 'resolved')
1442
1525
  cb.success();
1443
- }, ...(ngDevMode ? [{ debugName: "cbEffectRef" }] : []));
1526
+ }, ...(ngDevMode ? [{ debugName: "cbEffectRef" }] : /* istanbul ignore next */ []));
1444
1527
  const set = (value) => {
1445
1528
  resource.value.set(value);
1446
1529
  const k = untracked(cacheKey);
@@ -1464,7 +1547,8 @@ function queryResource(request, options) {
1464
1547
  update,
1465
1548
  statusCode: linkedSignal(resource.statusCode),
1466
1549
  headers: linkedSignal(resource.headers),
1467
- disabled: computed(() => cb.isOpen() || stableRequest() === undefined),
1550
+ disabled: computed(() => disabledReason() !== null),
1551
+ disabledReason,
1468
1552
  reload: () => {
1469
1553
  cb.halfOpen(); // open the circuit for manual reload
1470
1554
  return resource.reload();
@@ -1527,7 +1611,7 @@ function queryResource(request, options) {
1527
1611
  }
1528
1612
 
1529
1613
  function manualQueryResource(request, options) {
1530
- const trigger = signal({ epoch: 0 }, { ...(ngDevMode ? { debugName: "trigger" } : {}), equal: (a, b) => a.epoch === b.epoch });
1614
+ const trigger = signal({ epoch: 0 }, { ...(ngDevMode ? { debugName: "trigger" } : /* istanbul ignore next */ {}), equal: (a, b) => a.epoch === b.epoch });
1531
1615
  const injector = options?.injector ?? inject(Injector);
1532
1616
  const req = computed(() => {
1533
1617
  const state = trigger();
@@ -1536,7 +1620,7 @@ function manualQueryResource(request, options) {
1536
1620
  if (state.override)
1537
1621
  return state.override;
1538
1622
  return untracked(request);
1539
- }, { ...(ngDevMode ? { debugName: "req" } : {}), equal: () => false });
1623
+ }, { ...(ngDevMode ? { debugName: "req" } : /* istanbul ignore next */ {}), equal: () => false });
1540
1624
  const resource = queryResource(req, options);
1541
1625
  return {
1542
1626
  ...resource,
@@ -1585,14 +1669,14 @@ function mutationResource(request, options = {}) {
1585
1669
  const { onMutate, onError, onSuccess, onSettled, equal, ...rest } = options;
1586
1670
  const requestEqual = createEqualRequest(equal);
1587
1671
  const eq = equal ?? Object.is;
1588
- const next = signal(null, { ...(ngDevMode ? { debugName: "next" } : {}), equal: (a, b) => {
1672
+ const next = signal(null, { ...(ngDevMode ? { debugName: "next" } : /* istanbul ignore next */ {}), equal: (a, b) => {
1589
1673
  if (!a && !b)
1590
1674
  return true;
1591
1675
  if (!a || !b)
1592
1676
  return false;
1593
1677
  return eq(a, b);
1594
1678
  } });
1595
- const queue = signal([], ...(ngDevMode ? [{ debugName: "queue" }] : []));
1679
+ const queue = signal([], ...(ngDevMode ? [{ debugName: "queue" }] : /* istanbul ignore next */ []));
1596
1680
  let ctx = undefined;
1597
1681
  const queueRef = effect(() => {
1598
1682
  const nextInQueue = queue().at(0);
@@ -1602,14 +1686,14 @@ function mutationResource(request, options = {}) {
1602
1686
  const [value, ictx] = nextInQueue;
1603
1687
  ctx = onMutate?.(value, ictx);
1604
1688
  next.set(value);
1605
- }, ...(ngDevMode ? [{ debugName: "queueRef" }] : []));
1689
+ }, ...(ngDevMode ? [{ debugName: "queueRef" }] : /* istanbul ignore next */ []));
1606
1690
  const req = computed(() => {
1607
1691
  const nr = next();
1608
1692
  if (!nr)
1609
1693
  return;
1610
1694
  return request(nr) ?? undefined;
1611
- }, { ...(ngDevMode ? { debugName: "req" } : {}), equal: requestEqual });
1612
- const lastValue = linkedSignal({ ...(ngDevMode ? { debugName: "lastValue" } : {}), source: next,
1695
+ }, { ...(ngDevMode ? { debugName: "req" } : /* istanbul ignore next */ {}), equal: requestEqual });
1696
+ const lastValue = linkedSignal({ ...(ngDevMode ? { debugName: "lastValue" } : /* istanbul ignore next */ {}), source: next,
1613
1697
  computation: (next, prev) => {
1614
1698
  if (next === null && !!prev)
1615
1699
  return prev.value;
@@ -1620,7 +1704,7 @@ function mutationResource(request, options = {}) {
1620
1704
  if (!nr)
1621
1705
  return;
1622
1706
  return request(nr) ?? undefined;
1623
- }, { ...(ngDevMode ? { debugName: "lastValueRequest" } : {}), equal: requestEqual });
1707
+ }, { ...(ngDevMode ? { debugName: "lastValueRequest" } : /* istanbul ignore next */ {}), equal: requestEqual });
1624
1708
  const cb = createCircuitBreaker(options?.circuitBreaker === true
1625
1709
  ? undefined
1626
1710
  : (options?.circuitBreaker ?? false), options?.injector);