@mmstack/resource 21.1.1 → 21.2.0
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.
- package/README.md +508 -73
- package/fesm2022/mmstack-resource.mjs +328 -173
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/package.json +3 -2
- package/types/mmstack-resource.d.ts +83 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext,
|
|
1
|
+
import { HttpHeaders, HttpResponse, HttpParams, HttpContextToken, HttpContext, httpResource, HttpClient } from '@angular/common/http';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
import { isDevMode, untracked, computed, InjectionToken, inject, signal, effect, Injector, linkedSignal, Injectable, DestroyRef } from '@angular/core';
|
|
4
4
|
import { mutable, toWritable, sensor, nestedEffect } from '@mmstack/primitives';
|
|
@@ -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)
|
|
@@ -520,6 +551,173 @@ function injectQueryCache(injector) {
|
|
|
520
551
|
return cache;
|
|
521
552
|
}
|
|
522
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Returns `true` for any object-like value whose own enumerable keys should
|
|
556
|
+
* be sorted for stable hashing. Excludes arrays (positional), `Date`
|
|
557
|
+
* (handled by `toJSON`), `Map`/`Set` (handled explicitly), and binary types
|
|
558
|
+
* (`Blob`/`FormData`/`URLSearchParams`/`ArrayBuffer`/typed arrays — these
|
|
559
|
+
* should be branched on before reaching `hash()`, typically by `hashRequest`).
|
|
560
|
+
*
|
|
561
|
+
* Plain objects, class instances, and `Object.create(null)` all qualify.
|
|
562
|
+
*/
|
|
563
|
+
function isHashableObject(value) {
|
|
564
|
+
if (value === null || typeof value !== 'object')
|
|
565
|
+
return false;
|
|
566
|
+
if (Array.isArray(value))
|
|
567
|
+
return false;
|
|
568
|
+
if (value instanceof Date)
|
|
569
|
+
return false;
|
|
570
|
+
if (value instanceof Map)
|
|
571
|
+
return false;
|
|
572
|
+
if (value instanceof Set)
|
|
573
|
+
return false;
|
|
574
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob)
|
|
575
|
+
return false;
|
|
576
|
+
if (typeof FormData !== 'undefined' && value instanceof FormData)
|
|
577
|
+
return false;
|
|
578
|
+
if (typeof URLSearchParams !== 'undefined' &&
|
|
579
|
+
value instanceof URLSearchParams)
|
|
580
|
+
return false;
|
|
581
|
+
if (value instanceof ArrayBuffer)
|
|
582
|
+
return false;
|
|
583
|
+
if (ArrayBuffer.isView(value))
|
|
584
|
+
return false;
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
function sortKeys(val) {
|
|
588
|
+
return Object.keys(val)
|
|
589
|
+
.toSorted()
|
|
590
|
+
.reduce((result, key) => {
|
|
591
|
+
result[key] = val[key];
|
|
592
|
+
return result;
|
|
593
|
+
}, {});
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Internal helper to generate a stable JSON string from an array.
|
|
597
|
+
* - Object-like values (plain, class instances, null-proto) get their own
|
|
598
|
+
* enumerable keys sorted alphabetically.
|
|
599
|
+
* - `Map` → marker object with sorted entries (sorted by `JSON.stringify(key)`).
|
|
600
|
+
* - `Set` → marker object with sorted values (sorted by `JSON.stringify(value)`).
|
|
601
|
+
* - Arrays preserve order. `Date` serializes via `toJSON`.
|
|
602
|
+
*
|
|
603
|
+
* @internal
|
|
604
|
+
*/
|
|
605
|
+
function hashKey(queryKey) {
|
|
606
|
+
return JSON.stringify(queryKey, (_, val) => {
|
|
607
|
+
if (val instanceof Map) {
|
|
608
|
+
// Schwartzian: compute each entry's sort key (recursive hash of the
|
|
609
|
+
// Map key) once, then sort by the cheap string compare.
|
|
610
|
+
const entries = [...val.entries()]
|
|
611
|
+
.map((e) => [hash(e[0]), e])
|
|
612
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
613
|
+
.map(([, e]) => e);
|
|
614
|
+
return { __map__: entries };
|
|
615
|
+
}
|
|
616
|
+
if (val instanceof Set) {
|
|
617
|
+
const values = [...val]
|
|
618
|
+
.map((v) => [hash(v), v])
|
|
619
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
620
|
+
.map(([, v]) => v);
|
|
621
|
+
return { __set__: values };
|
|
622
|
+
}
|
|
623
|
+
if (isHashableObject(val))
|
|
624
|
+
return sortKeys(val);
|
|
625
|
+
return val;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Generates a stable, unique string hash from one or more arguments.
|
|
630
|
+
* Useful for creating cache keys or identifiers where object key order shouldn't matter.
|
|
631
|
+
*
|
|
632
|
+
* How it works:
|
|
633
|
+
* - Object-like values (plain objects, class instances, `Object.create(null)`) have
|
|
634
|
+
* their own enumerable keys sorted alphabetically before hashing. This ensures
|
|
635
|
+
* `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same hash.
|
|
636
|
+
* - `Map` and `Set` are serialized via stable, sorted markers (`__map__` / `__set__`).
|
|
637
|
+
* - Arrays preserve positional order; `Date` uses its ISO string via `toJSON`.
|
|
638
|
+
*
|
|
639
|
+
* @param {...unknown} args Values to include in the hash.
|
|
640
|
+
* @returns A stable string hash representing the input arguments.
|
|
641
|
+
* @example
|
|
642
|
+
* hash('posts', 10);
|
|
643
|
+
* // => '["posts",10]'
|
|
644
|
+
*
|
|
645
|
+
* hash({ a: 1, b: 2 }) === hash({ b: 2, a: 1 }); // true
|
|
646
|
+
*
|
|
647
|
+
* hash(new Map([['a', 1]])) === hash(new Map([['a', 1]])); // true
|
|
648
|
+
*
|
|
649
|
+
* // Be mindful of values JSON.stringify cannot handle (functions, undefined, Symbols)
|
|
650
|
+
* // hash('a', undefined, function() {}) => '["a",null,null]'
|
|
651
|
+
*/
|
|
652
|
+
function hash(...args) {
|
|
653
|
+
return hashKey(args);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function normalizeParams(params) {
|
|
657
|
+
const p = params instanceof HttpParams
|
|
658
|
+
? params
|
|
659
|
+
: new HttpParams({ fromObject: params });
|
|
660
|
+
return p
|
|
661
|
+
.keys()
|
|
662
|
+
.toSorted()
|
|
663
|
+
.map((key) => {
|
|
664
|
+
const encodedKey = encodeURIComponent(key);
|
|
665
|
+
return (p.getAll(key) ?? [])
|
|
666
|
+
.map((v) => `${encodedKey}=${encodeURIComponent(v)}`)
|
|
667
|
+
.join('&');
|
|
668
|
+
})
|
|
669
|
+
.join('&');
|
|
670
|
+
}
|
|
671
|
+
function hashBody(body) {
|
|
672
|
+
// File extends Blob — must check File first
|
|
673
|
+
if (typeof File !== 'undefined' && body instanceof File) {
|
|
674
|
+
return `File:${body.name}:${body.type}:${body.size}:${body.lastModified}`;
|
|
675
|
+
}
|
|
676
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
677
|
+
return `Blob:${body.type}:${body.size}`;
|
|
678
|
+
}
|
|
679
|
+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
680
|
+
const entries = [];
|
|
681
|
+
body.forEach((value, key) => {
|
|
682
|
+
entries.push([key, hashBody(value)]);
|
|
683
|
+
});
|
|
684
|
+
entries.sort(([ak, av], [bk, bv]) => ak.localeCompare(bk) || av.localeCompare(bv));
|
|
685
|
+
return `FormData:${entries.map(([k, v]) => `${k}=${v}`).join('&')}`;
|
|
686
|
+
}
|
|
687
|
+
if (typeof URLSearchParams !== 'undefined' &&
|
|
688
|
+
body instanceof URLSearchParams) {
|
|
689
|
+
const sp = new URLSearchParams(body);
|
|
690
|
+
sp.sort();
|
|
691
|
+
return `URLSearchParams:${sp.toString()}`;
|
|
692
|
+
}
|
|
693
|
+
if (body instanceof ArrayBuffer) {
|
|
694
|
+
return `ArrayBuffer:${body.byteLength}`;
|
|
695
|
+
}
|
|
696
|
+
if (ArrayBuffer.isView(body)) {
|
|
697
|
+
return `${body.constructor.name}:${body.byteLength}`;
|
|
698
|
+
}
|
|
699
|
+
return hash(body);
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Builds a stable cache/dedupe key from an HTTP request shape (accepts both
|
|
703
|
+
* `HttpRequest` and `HttpResourceRequest`).
|
|
704
|
+
*
|
|
705
|
+
* Key composition: `${method}:${url}:${responseType}[:${params}][:${body}]`
|
|
706
|
+
* - `method` defaults to `'GET'`, `responseType` to `'json'` (Angular defaults).
|
|
707
|
+
* - Query params are sorted alphabetically and URL-encoded for stability.
|
|
708
|
+
* - Body hashing handles `File`/`Blob`/`FormData`/`URLSearchParams`/`ArrayBuffer`
|
|
709
|
+
* and typed arrays explicitly; everything else flows through key-sorted
|
|
710
|
+
* `JSON.stringify` via `hash()`.
|
|
711
|
+
*/
|
|
712
|
+
function hashRequest(req) {
|
|
713
|
+
const method = req.method ?? 'GET';
|
|
714
|
+
const responseType = req.responseType ?? 'json';
|
|
715
|
+
const base = `${method}:${req.url}:${responseType}`;
|
|
716
|
+
const params = req.params ? `:${normalizeParams(req.params)}` : '';
|
|
717
|
+
const body = req.body != null ? `:${hashBody(req.body)}` : '';
|
|
718
|
+
return base + params + body;
|
|
719
|
+
}
|
|
720
|
+
|
|
523
721
|
const CACHE_CONTEXT = new HttpContextToken(() => ({
|
|
524
722
|
cache: false,
|
|
525
723
|
}));
|
|
@@ -663,7 +861,7 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
663
861
|
const opt = getCacheContext(req.context);
|
|
664
862
|
if (!opt.cache)
|
|
665
863
|
return next(req);
|
|
666
|
-
const key = opt.key ?? req
|
|
864
|
+
const key = opt.key ?? hashRequest(req);
|
|
667
865
|
const entry = cache.getUntracked(key); // null if expired or not found
|
|
668
866
|
// If the entry is not stale, return it
|
|
669
867
|
if (entry && !entry.isStale)
|
|
@@ -683,7 +881,9 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
683
881
|
});
|
|
684
882
|
}
|
|
685
883
|
return next(req).pipe(tap((event) => {
|
|
686
|
-
if (event instanceof HttpResponse
|
|
884
|
+
if (!(event instanceof HttpResponse))
|
|
885
|
+
return;
|
|
886
|
+
if (event.ok) {
|
|
687
887
|
const cacheControl = parseCacheControlHeader(event);
|
|
688
888
|
if (cacheControl.noStore && !opt.ignoreCacheControl)
|
|
689
889
|
return;
|
|
@@ -702,6 +902,17 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
702
902
|
})
|
|
703
903
|
: event;
|
|
704
904
|
cache.store(key, parsedResponse, staleTime, ttl, opt.persist);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
// 304 → server confirmed our cached entry is still valid. Re-stamp the
|
|
908
|
+
// existing entry so subsequent reads within the new freshness window
|
|
909
|
+
// don't trigger another revalidation round-trip.
|
|
910
|
+
if (event.status === 304 && entry) {
|
|
911
|
+
const cacheControl = parseCacheControlHeader(event);
|
|
912
|
+
const { staleTime, ttl } = opt.ignoreCacheControl
|
|
913
|
+
? opt
|
|
914
|
+
: resolveTimings(cacheControl, opt.staleTime, opt.ttl);
|
|
915
|
+
cache.store(key, entry.value, staleTime, ttl, opt.persist);
|
|
705
916
|
}
|
|
706
917
|
}), map((event) => {
|
|
707
918
|
// handle 304 responses due to eTag/last-modified
|
|
@@ -729,22 +940,23 @@ function catchValueError(resource, fallback) {
|
|
|
729
940
|
|
|
730
941
|
/** @internal */
|
|
731
942
|
const DEFAULT_OPTIONS = {
|
|
732
|
-
|
|
943
|
+
threshold: 5,
|
|
733
944
|
timeout: 30000,
|
|
734
945
|
shouldFail: () => true,
|
|
735
946
|
shouldFailForever: () => false,
|
|
736
947
|
};
|
|
737
948
|
/** @internal */
|
|
738
|
-
function internalCeateCircuitBreaker(
|
|
739
|
-
const halfOpen = signal(false, ...(ngDevMode ? [{ debugName: "halfOpen" }] : []));
|
|
740
|
-
const failureCount = signal(0, ...(ngDevMode ? [{ debugName: "failureCount" }] : []));
|
|
949
|
+
function internalCeateCircuitBreaker(threshold = 5, resetTimeout = 30000, shouldFail = () => true, shouldFailForever = () => false) {
|
|
950
|
+
const halfOpen = signal(false, ...(ngDevMode ? [{ debugName: "halfOpen" }] : /* istanbul ignore next */ []));
|
|
951
|
+
const failureCount = signal(0, ...(ngDevMode ? [{ debugName: "failureCount" }] : /* istanbul ignore next */ []));
|
|
952
|
+
const failedForever = signal(false, ...(ngDevMode ? [{ debugName: "failedForever" }] : /* istanbul ignore next */ []));
|
|
741
953
|
const status = computed(() => {
|
|
742
|
-
if (failureCount() >=
|
|
954
|
+
if (failedForever() || failureCount() >= threshold)
|
|
743
955
|
return 'OPEN';
|
|
744
956
|
return halfOpen() ? 'HALF_OPEN' : 'CLOSED';
|
|
745
|
-
}, ...(ngDevMode ? [{ debugName: "status" }] : []));
|
|
746
|
-
const isClosed = computed(() => status() !== 'OPEN', ...(ngDevMode ? [{ debugName: "isClosed" }] : []));
|
|
747
|
-
const isOpen = computed(() => status() !== 'CLOSED', ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
957
|
+
}, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
958
|
+
const isClosed = computed(() => status() !== 'OPEN', ...(ngDevMode ? [{ debugName: "isClosed" }] : /* istanbul ignore next */ []));
|
|
959
|
+
const isOpen = computed(() => status() !== 'CLOSED', ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
748
960
|
const success = () => {
|
|
749
961
|
failureCount.set(0);
|
|
750
962
|
halfOpen.set(false);
|
|
@@ -753,30 +965,26 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
|
|
|
753
965
|
if (!untracked(isOpen))
|
|
754
966
|
return;
|
|
755
967
|
halfOpen.set(true);
|
|
756
|
-
failureCount.set(
|
|
968
|
+
failureCount.set(threshold - 1);
|
|
757
969
|
};
|
|
758
|
-
|
|
970
|
+
// Auto-probe effect: schedules a half-open retry after `resetTimeout` whenever
|
|
971
|
+
// the breaker is open, *unless* we've been failed forever (in which case only
|
|
972
|
+
// hardReset() can recover).
|
|
759
973
|
const effectRef = effect((cleanup) => {
|
|
760
|
-
if (!isOpen())
|
|
974
|
+
if (!isOpen() || failedForever())
|
|
761
975
|
return;
|
|
762
976
|
const timeout = setTimeout(tryOnce, resetTimeout);
|
|
763
|
-
failForeverResetId = timeout;
|
|
764
977
|
return cleanup(() => {
|
|
765
978
|
clearTimeout(timeout);
|
|
766
|
-
failForeverResetId = null;
|
|
767
979
|
});
|
|
768
|
-
}, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
|
|
980
|
+
}, ...(ngDevMode ? [{ debugName: "effectRef" }] : /* istanbul ignore next */ []));
|
|
769
981
|
const failInternal = () => {
|
|
770
982
|
failureCount.set(failureCount() + 1);
|
|
771
983
|
halfOpen.set(false);
|
|
772
984
|
};
|
|
773
985
|
const failForever = () => {
|
|
774
|
-
|
|
775
|
-
clearTimeout(failForeverResetId);
|
|
776
|
-
effectRef.destroy();
|
|
777
|
-
failureCount.set(Infinity);
|
|
986
|
+
failedForever.set(true);
|
|
778
987
|
halfOpen.set(false);
|
|
779
|
-
return;
|
|
780
988
|
};
|
|
781
989
|
const fail = (err) => {
|
|
782
990
|
if (shouldFailForever(err))
|
|
@@ -785,6 +993,11 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
|
|
|
785
993
|
return failInternal();
|
|
786
994
|
// If the error does not trigger a failure, we do nothing.
|
|
787
995
|
};
|
|
996
|
+
const hardReset = () => {
|
|
997
|
+
failedForever.set(false);
|
|
998
|
+
failureCount.set(0);
|
|
999
|
+
halfOpen.set(false);
|
|
1000
|
+
};
|
|
788
1001
|
return {
|
|
789
1002
|
status,
|
|
790
1003
|
isClosed,
|
|
@@ -792,6 +1005,7 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
|
|
|
792
1005
|
fail,
|
|
793
1006
|
success,
|
|
794
1007
|
halfOpen: tryOnce,
|
|
1008
|
+
hardReset,
|
|
795
1009
|
destroy: () => effectRef.destroy(),
|
|
796
1010
|
};
|
|
797
1011
|
}
|
|
@@ -810,18 +1024,43 @@ function createNeverBrokenCircuitBreaker() {
|
|
|
810
1024
|
halfOpen: () => {
|
|
811
1025
|
// noop
|
|
812
1026
|
},
|
|
1027
|
+
hardReset: () => {
|
|
1028
|
+
// noop
|
|
1029
|
+
},
|
|
813
1030
|
destroy: () => {
|
|
814
1031
|
// noop
|
|
815
1032
|
},
|
|
816
1033
|
};
|
|
817
1034
|
}
|
|
818
1035
|
const CB_DEFAULT_OPTIONS = new InjectionToken('MMSTACK_CIRCUIT_BREAKER_DEFAULT_OPTIONS');
|
|
1036
|
+
/**
|
|
1037
|
+
* Provides application-wide default options for {@link createCircuitBreaker}.
|
|
1038
|
+
* Any `createCircuitBreaker()` call without explicit options (or with only
|
|
1039
|
+
* partial options) merges these defaults in, so you can centralize threshold /
|
|
1040
|
+
* timeout / failure-classifier behavior in one place.
|
|
1041
|
+
*
|
|
1042
|
+
* Per-call options always win over the provided defaults.
|
|
1043
|
+
*
|
|
1044
|
+
* @example
|
|
1045
|
+
* ```ts
|
|
1046
|
+
* bootstrapApplication(AppComponent, {
|
|
1047
|
+
* providers: [
|
|
1048
|
+
* provideCircuitBreakerDefaultOptions({
|
|
1049
|
+
* threshold: 10,
|
|
1050
|
+
* timeout: 60_000,
|
|
1051
|
+
* shouldFailForever: (err) =>
|
|
1052
|
+
* err instanceof HttpErrorResponse && [401, 403].includes(err.status),
|
|
1053
|
+
* }),
|
|
1054
|
+
* ],
|
|
1055
|
+
* });
|
|
1056
|
+
* ```
|
|
1057
|
+
*/
|
|
819
1058
|
function provideCircuitBreakerDefaultOptions(options) {
|
|
820
1059
|
return {
|
|
821
1060
|
provide: CB_DEFAULT_OPTIONS,
|
|
822
1061
|
useValue: {
|
|
823
1062
|
...DEFAULT_OPTIONS,
|
|
824
|
-
...options,
|
|
1063
|
+
...normalizeThreshold(options),
|
|
825
1064
|
},
|
|
826
1065
|
};
|
|
827
1066
|
}
|
|
@@ -830,12 +1069,23 @@ function injectCircuitBreakerOptions(injector = inject(Injector)) {
|
|
|
830
1069
|
optional: true,
|
|
831
1070
|
});
|
|
832
1071
|
}
|
|
1072
|
+
/** @internal — strips the deprecated `treshold` field and folds it into `threshold` */
|
|
1073
|
+
function normalizeThreshold(opt) {
|
|
1074
|
+
if (!opt || typeof opt !== 'object' || 'isClosed' in opt)
|
|
1075
|
+
return {};
|
|
1076
|
+
const { treshold, threshold, ...rest } = opt;
|
|
1077
|
+
return {
|
|
1078
|
+
...rest,
|
|
1079
|
+
threshold: threshold ?? treshold,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
833
1082
|
/**
|
|
834
1083
|
* Creates a circuit breaker instance.
|
|
835
1084
|
*
|
|
836
1085
|
* @param options - Configuration options for the circuit breaker. Can be:
|
|
837
|
-
* - `undefined`:
|
|
838
|
-
* - `
|
|
1086
|
+
* - `undefined`: Uses defaults (threshold: 5, timeout: 30000ms) or provided defaults via {@link provideCircuitBreakerDefaultOptions}.
|
|
1087
|
+
* - `false`: Creates a "no-op" circuit breaker that is always closed (never trips).
|
|
1088
|
+
* - `true`: Creates a circuit breaker with default settings.
|
|
839
1089
|
* - `CircuitBreaker`: Reuses an existing `CircuitBreaker` instance.
|
|
840
1090
|
* - `{ threshold?: number; timeout?: number; }`: Creates a circuit breaker with the specified threshold and timeout.
|
|
841
1091
|
*
|
|
@@ -858,11 +1108,11 @@ function createCircuitBreaker(opt, injector) {
|
|
|
858
1108
|
return createNeverBrokenCircuitBreaker();
|
|
859
1109
|
if (typeof opt === 'object' && 'isClosed' in opt)
|
|
860
1110
|
return opt;
|
|
861
|
-
const {
|
|
1111
|
+
const { threshold, timeout, shouldFail, shouldFailForever } = {
|
|
862
1112
|
...injectCircuitBreakerOptions(injector),
|
|
863
|
-
...opt,
|
|
1113
|
+
...normalizeThreshold(opt),
|
|
864
1114
|
};
|
|
865
|
-
return internalCeateCircuitBreaker(
|
|
1115
|
+
return internalCeateCircuitBreaker(threshold, timeout, shouldFail, shouldFailForever);
|
|
866
1116
|
}
|
|
867
1117
|
|
|
868
1118
|
// Heavily inspired by: https://dev.to/kasual1/request-deduplication-in-angular-3pd8
|
|
@@ -894,6 +1144,9 @@ function noDedupe(ctx = new HttpContext()) {
|
|
|
894
1144
|
*
|
|
895
1145
|
* @param allowed - An array of HTTP methods for which deduplication should be enabled.
|
|
896
1146
|
* Defaults to `['GET', 'DELETE', 'HEAD', 'OPTIONS']`.
|
|
1147
|
+
* @param keyFn - Optional function to compute the dedupe key from a request.
|
|
1148
|
+
* Defaults to `hashRequest`, which includes method, URL,
|
|
1149
|
+
* response type, params, and body.
|
|
897
1150
|
*
|
|
898
1151
|
* @returns An `HttpInterceptorFn` that implements the request deduplication logic.
|
|
899
1152
|
*
|
|
@@ -917,97 +1170,22 @@ function noDedupe(ctx = new HttpContext()) {
|
|
|
917
1170
|
* ],
|
|
918
1171
|
* };
|
|
919
1172
|
*/
|
|
920
|
-
function createDedupeRequestsInterceptor(allowed = ['GET', 'DELETE', 'HEAD', 'OPTIONS']) {
|
|
1173
|
+
function createDedupeRequestsInterceptor(allowed = ['GET', 'DELETE', 'HEAD', 'OPTIONS'], keyFn = hashRequest) {
|
|
921
1174
|
const inFlight = new Map();
|
|
922
1175
|
const DEDUPE_METHODS = new Set(allowed);
|
|
923
1176
|
return (req, next) => {
|
|
924
1177
|
if (!DEDUPE_METHODS.has(req.method) || req.context.get(NO_DEDUPE))
|
|
925
1178
|
return next(req);
|
|
926
|
-
const
|
|
1179
|
+
const key = keyFn(req);
|
|
1180
|
+
const found = inFlight.get(key);
|
|
927
1181
|
if (found)
|
|
928
1182
|
return found;
|
|
929
|
-
const request = next(req).pipe(finalize(() => inFlight.delete(
|
|
930
|
-
inFlight.set(
|
|
1183
|
+
const request = next(req).pipe(finalize(() => inFlight.delete(key)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
1184
|
+
inFlight.set(key, request);
|
|
931
1185
|
return request;
|
|
932
1186
|
};
|
|
933
1187
|
}
|
|
934
1188
|
|
|
935
|
-
/**
|
|
936
|
-
* Checks if `value` is a plain JavaScript object (e.g., `{}` or `new Object()`).
|
|
937
|
-
* Distinguishes from arrays, null, and class instances. Acts as a type predicate,
|
|
938
|
-
* narrowing `value` to `UnknownObject` if `true`.
|
|
939
|
-
*
|
|
940
|
-
* @param value The value to check.
|
|
941
|
-
* @returns {value is UnknownObject} `true` if `value` is a plain object, otherwise `false`.
|
|
942
|
-
* @example
|
|
943
|
-
* isPlainObject({}) // => true
|
|
944
|
-
* isPlainObject([]) // => false
|
|
945
|
-
* isPlainObject(null) // => false
|
|
946
|
-
* isPlainObject(new Date()) // => false
|
|
947
|
-
*/
|
|
948
|
-
function isPlainObject(value) {
|
|
949
|
-
if (value === null || typeof value !== 'object')
|
|
950
|
-
return false;
|
|
951
|
-
const proto = Object.getPrototypeOf(value);
|
|
952
|
-
if (proto === null)
|
|
953
|
-
return false; // remove Object.create(null);
|
|
954
|
-
return proto === Object.prototype;
|
|
955
|
-
}
|
|
956
|
-
/**
|
|
957
|
-
* Internal helper to generate a stable JSON string from an array.
|
|
958
|
-
* Sorts keys of plain objects within the array alphabetically before serialization
|
|
959
|
-
* to ensure hash stability regardless of key order.
|
|
960
|
-
*
|
|
961
|
-
* @param queryKey The array of values to serialize.
|
|
962
|
-
* @returns A stable JSON string representation.
|
|
963
|
-
* @internal
|
|
964
|
-
*/
|
|
965
|
-
function hashKey(queryKey) {
|
|
966
|
-
return JSON.stringify(queryKey, (_, val) => isPlainObject(val)
|
|
967
|
-
? Object.keys(val)
|
|
968
|
-
.toSorted()
|
|
969
|
-
.reduce((result, key) => {
|
|
970
|
-
result[key] = val[key];
|
|
971
|
-
return result;
|
|
972
|
-
}, {})
|
|
973
|
-
: val);
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Generates a stable, unique string hash from one or more arguments.
|
|
977
|
-
* Useful for creating cache keys or identifiers where object key order shouldn't matter.
|
|
978
|
-
*
|
|
979
|
-
* How it works:
|
|
980
|
-
* - Plain objects within the arguments have their keys sorted alphabetically before hashing.
|
|
981
|
-
* This ensures that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same hash.
|
|
982
|
-
* - Uses `JSON.stringify` internally with custom sorting for plain objects via `hashKey`.
|
|
983
|
-
* - Non-plain objects (arrays, Dates, etc.) and primitives are serialized naturally.
|
|
984
|
-
*
|
|
985
|
-
* @param {...unknown} args Values to include in the hash.
|
|
986
|
-
* @returns A stable string hash representing the input arguments.
|
|
987
|
-
* @example
|
|
988
|
-
* const userQuery = (id: number) => ['user', { id, timestamp: Date.now() }];
|
|
989
|
-
*
|
|
990
|
-
* const obj1 = { a: 1, b: 2 };
|
|
991
|
-
* const obj2 = { b: 2, a: 1 }; // Same keys/values, different order
|
|
992
|
-
*
|
|
993
|
-
* hash('posts', 10);
|
|
994
|
-
* // => '["posts",10]'
|
|
995
|
-
*
|
|
996
|
-
* hash('config', obj1);
|
|
997
|
-
* // => '["config",{"a":1,"b":2}]'
|
|
998
|
-
*
|
|
999
|
-
* hash('config', obj2);
|
|
1000
|
-
* // => '["config",{"a":1,"b":2}]' (Same as above due to key sorting)
|
|
1001
|
-
*
|
|
1002
|
-
* hash(['todos', { status: 'done', owner: obj1 }]);
|
|
1003
|
-
* // => '[["todos",{"owner":{"a":1,"b":2},"status":"done"}]]'
|
|
1004
|
-
*
|
|
1005
|
-
* // Be mindful of values JSON.stringify cannot handle (functions, undefined, Symbols)
|
|
1006
|
-
* // hash('a', undefined, function() {}) => '["a",null,null]'
|
|
1007
|
-
*/
|
|
1008
|
-
function hash(...args) {
|
|
1009
|
-
return hashKey(args);
|
|
1010
|
-
}
|
|
1011
1189
|
function equalTransferCache(a, b) {
|
|
1012
1190
|
if (!a && !b)
|
|
1013
1191
|
return true;
|
|
@@ -1179,7 +1357,7 @@ function hasSlowConnection() {
|
|
|
1179
1357
|
|
|
1180
1358
|
function persist(src, equal) {
|
|
1181
1359
|
// linkedSignal allows us to access previous source value
|
|
1182
|
-
const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : {}), source: () => src(),
|
|
1360
|
+
const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : /* istanbul ignore next */ {}), source: () => src(),
|
|
1183
1361
|
computation: (next, prev) => {
|
|
1184
1362
|
if (next === undefined && prev !== undefined)
|
|
1185
1363
|
return prev.value;
|
|
@@ -1233,13 +1411,16 @@ function refresh(resource, destroyRef, refresh) {
|
|
|
1233
1411
|
}
|
|
1234
1412
|
|
|
1235
1413
|
// Retry on error, if number is provided it will retry that many times with exponential backoff, otherwise it will use the options provided
|
|
1236
|
-
function retryOnError(res, opt) {
|
|
1414
|
+
function retryOnError(res, opt, onError) {
|
|
1237
1415
|
const max = opt ? (typeof opt === 'number' ? opt : (opt.max ?? 0)) : 0;
|
|
1238
1416
|
const backoff = typeof opt === 'object' ? (opt.backoff ?? 1000) : 1000;
|
|
1239
1417
|
let retries = 0;
|
|
1240
1418
|
let timeout;
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1419
|
+
const handleError = () => {
|
|
1420
|
+
const err = untracked(res.error);
|
|
1421
|
+
const isFinal = retries >= max;
|
|
1422
|
+
onError?.(err, retries, isFinal);
|
|
1423
|
+
if (isFinal)
|
|
1243
1424
|
return;
|
|
1244
1425
|
retries++;
|
|
1245
1426
|
if (timeout)
|
|
@@ -1254,11 +1435,11 @@ function retryOnError(res, opt) {
|
|
|
1254
1435
|
const ref = effect(() => {
|
|
1255
1436
|
switch (res.status()) {
|
|
1256
1437
|
case 'error':
|
|
1257
|
-
return
|
|
1438
|
+
return handleError();
|
|
1258
1439
|
case 'resolved':
|
|
1259
1440
|
return onSuccess();
|
|
1260
1441
|
}
|
|
1261
|
-
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
1442
|
+
}, ...(ngDevMode ? [{ debugName: "ref" }] : /* istanbul ignore next */ []));
|
|
1262
1443
|
return {
|
|
1263
1444
|
...res,
|
|
1264
1445
|
destroy: () => {
|
|
@@ -1270,10 +1451,10 @@ function retryOnError(res, opt) {
|
|
|
1270
1451
|
|
|
1271
1452
|
class ResourceSensors {
|
|
1272
1453
|
networkStatus = sensor('networkStatus');
|
|
1273
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1274
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1454
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ResourceSensors, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1455
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ResourceSensors, providedIn: 'root' });
|
|
1275
1456
|
}
|
|
1276
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1457
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ResourceSensors, decorators: [{
|
|
1277
1458
|
type: Injectable,
|
|
1278
1459
|
args: [{
|
|
1279
1460
|
providedIn: 'root',
|
|
@@ -1302,29 +1483,6 @@ function toResourceObject(res) {
|
|
|
1302
1483
|
};
|
|
1303
1484
|
}
|
|
1304
1485
|
|
|
1305
|
-
function normalizeParams(params) {
|
|
1306
|
-
if (params instanceof HttpParams)
|
|
1307
|
-
return params.toString();
|
|
1308
|
-
const paramMap = new Map();
|
|
1309
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1310
|
-
if (Array.isArray(value)) {
|
|
1311
|
-
paramMap.set(key, value.map(encodeURIComponent).join(','));
|
|
1312
|
-
}
|
|
1313
|
-
else {
|
|
1314
|
-
paramMap.set(key, encodeURIComponent(value.toString()));
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
return Array.from(paramMap.entries())
|
|
1318
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
1319
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
1320
|
-
.join('&');
|
|
1321
|
-
}
|
|
1322
|
-
function urlWithParams(req) {
|
|
1323
|
-
if (!req.params)
|
|
1324
|
-
return req.url;
|
|
1325
|
-
return `${req.url}?${normalizeParams(req.params)}`;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
1486
|
function queryResource(request, options) {
|
|
1329
1487
|
const cache = injectQueryCache(options?.injector);
|
|
1330
1488
|
const destroyRef = options?.injector
|
|
@@ -1337,23 +1495,33 @@ function queryResource(request, options) {
|
|
|
1337
1495
|
const eq = options?.triggerOnSameRequest
|
|
1338
1496
|
? undefined
|
|
1339
1497
|
: createEqualRequest(options?.equal);
|
|
1498
|
+
const rawRequest = computed(() => request() ?? undefined, ...(ngDevMode ? [{ debugName: "rawRequest" }] : /* istanbul ignore next */ []));
|
|
1499
|
+
const disabledReason = computed(() => {
|
|
1500
|
+
if (!networkAvailable())
|
|
1501
|
+
return 'offline';
|
|
1502
|
+
if (cb.isOpen())
|
|
1503
|
+
return 'circuit-open';
|
|
1504
|
+
if (!rawRequest())
|
|
1505
|
+
return 'no-request';
|
|
1506
|
+
return null;
|
|
1507
|
+
}, ...(ngDevMode ? [{ debugName: "disabledReason" }] : /* istanbul ignore next */ []));
|
|
1340
1508
|
const stableRequest = computed(() => {
|
|
1341
|
-
if (
|
|
1509
|
+
if (disabledReason() !== null)
|
|
1342
1510
|
return undefined;
|
|
1343
|
-
const req =
|
|
1511
|
+
const req = rawRequest();
|
|
1344
1512
|
if (!req)
|
|
1345
1513
|
return undefined;
|
|
1346
1514
|
if (typeof req === 'string')
|
|
1347
1515
|
return { method: 'GET', url: req };
|
|
1348
1516
|
return req;
|
|
1349
|
-
}, { ...(ngDevMode ? { debugName: "stableRequest" } : {}), equal: (a, b) => {
|
|
1517
|
+
}, { ...(ngDevMode ? { debugName: "stableRequest" } : /* istanbul ignore next */ {}), equal: (a, b) => {
|
|
1350
1518
|
if (eq)
|
|
1351
1519
|
return eq(a, b);
|
|
1352
1520
|
return a === b;
|
|
1353
1521
|
} });
|
|
1354
1522
|
const hashFn = typeof options?.cache === 'object'
|
|
1355
|
-
? (options.cache.hash ??
|
|
1356
|
-
:
|
|
1523
|
+
? (options.cache.hash ?? hashRequest)
|
|
1524
|
+
: hashRequest;
|
|
1357
1525
|
const staleTime = typeof options?.cache === 'object' ? options.cache.staleTime : 0;
|
|
1358
1526
|
const ttl = typeof options?.cache === 'object' ? options.cache.ttl : undefined;
|
|
1359
1527
|
const cacheKey = computed(() => {
|
|
@@ -1361,7 +1529,7 @@ function queryResource(request, options) {
|
|
|
1361
1529
|
if (!r)
|
|
1362
1530
|
return null;
|
|
1363
1531
|
return hashFn(r);
|
|
1364
|
-
}, ...(ngDevMode ? [{ debugName: "cacheKey" }] : []));
|
|
1532
|
+
}, ...(ngDevMode ? [{ debugName: "cacheKey" }] : /* istanbul ignore next */ []));
|
|
1365
1533
|
const bustBrowserCache = typeof options?.cache === 'object' &&
|
|
1366
1534
|
options.cache.bustBrowserCache === true;
|
|
1367
1535
|
const ignoreCacheControl = typeof options?.cache === 'object' &&
|
|
@@ -1392,7 +1560,7 @@ function queryResource(request, options) {
|
|
|
1392
1560
|
resource = catchValueError(resource, options?.defaultValue);
|
|
1393
1561
|
// get full HttpResonse from Cache
|
|
1394
1562
|
const cachedEvent = cache.getEntryOrKey(cacheKey);
|
|
1395
|
-
const cacheEntry = linkedSignal({ ...(ngDevMode ? { debugName: "cacheEntry" } : {}), source: () => cachedEvent(),
|
|
1563
|
+
const cacheEntry = linkedSignal({ ...(ngDevMode ? { debugName: "cacheEntry" } : /* istanbul ignore next */ {}), source: () => cachedEvent(),
|
|
1396
1564
|
computation: (entry, prev) => {
|
|
1397
1565
|
if (!entry)
|
|
1398
1566
|
return null;
|
|
@@ -1412,7 +1580,7 @@ function queryResource(request, options) {
|
|
|
1412
1580
|
};
|
|
1413
1581
|
} });
|
|
1414
1582
|
resource = refresh(resource, destroyRef, options?.refresh);
|
|
1415
|
-
resource = retryOnError(resource, options?.retry);
|
|
1583
|
+
resource = retryOnError(resource, options?.retry, options?.onError);
|
|
1416
1584
|
resource = persistResourceValues(resource, options?.keepPrevious, options?.equal);
|
|
1417
1585
|
const value = options?.cache
|
|
1418
1586
|
? toWritable(computed(() => {
|
|
@@ -1420,20 +1588,6 @@ function queryResource(request, options) {
|
|
|
1420
1588
|
return cacheEntry()?.value ?? resource.value();
|
|
1421
1589
|
}), resource.value.set, resource.value.update)
|
|
1422
1590
|
: resource.value;
|
|
1423
|
-
const onError = options?.onError; // Put in own variable to ensure value remains even if options are somehow mutated in-line
|
|
1424
|
-
if (onError) {
|
|
1425
|
-
const onErrorRef = effect(() => {
|
|
1426
|
-
const err = resource.error();
|
|
1427
|
-
if (err)
|
|
1428
|
-
onError(err);
|
|
1429
|
-
}, ...(ngDevMode ? [{ debugName: "onErrorRef" }] : []));
|
|
1430
|
-
// 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
|
|
1431
|
-
const destroyRest = resource.destroy;
|
|
1432
|
-
resource.destroy = () => {
|
|
1433
|
-
onErrorRef.destroy();
|
|
1434
|
-
destroyRest();
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
1591
|
// iterate circuit breaker state, is effect as a computed would cause a circular dependency (resource -> cb -> resource)
|
|
1438
1592
|
const cbEffectRef = effect(() => {
|
|
1439
1593
|
const status = resource.status();
|
|
@@ -1441,7 +1595,7 @@ function queryResource(request, options) {
|
|
|
1441
1595
|
cb.fail(untracked(resource.error));
|
|
1442
1596
|
else if (status === 'resolved')
|
|
1443
1597
|
cb.success();
|
|
1444
|
-
}, ...(ngDevMode ? [{ debugName: "cbEffectRef" }] : []));
|
|
1598
|
+
}, ...(ngDevMode ? [{ debugName: "cbEffectRef" }] : /* istanbul ignore next */ []));
|
|
1445
1599
|
const set = (value) => {
|
|
1446
1600
|
resource.value.set(value);
|
|
1447
1601
|
const k = untracked(cacheKey);
|
|
@@ -1465,7 +1619,8 @@ function queryResource(request, options) {
|
|
|
1465
1619
|
update,
|
|
1466
1620
|
statusCode: linkedSignal(resource.statusCode),
|
|
1467
1621
|
headers: linkedSignal(resource.headers),
|
|
1468
|
-
disabled: computed(() =>
|
|
1622
|
+
disabled: computed(() => disabledReason() !== null),
|
|
1623
|
+
disabledReason,
|
|
1469
1624
|
reload: () => {
|
|
1470
1625
|
cb.halfOpen(); // open the circuit for manual reload
|
|
1471
1626
|
return resource.reload();
|
|
@@ -1528,7 +1683,7 @@ function queryResource(request, options) {
|
|
|
1528
1683
|
}
|
|
1529
1684
|
|
|
1530
1685
|
function manualQueryResource(request, options) {
|
|
1531
|
-
const trigger = signal({ epoch: 0 }, { ...(ngDevMode ? { debugName: "trigger" } : {}), equal: (a, b) => a.epoch === b.epoch });
|
|
1686
|
+
const trigger = signal({ epoch: 0 }, { ...(ngDevMode ? { debugName: "trigger" } : /* istanbul ignore next */ {}), equal: (a, b) => a.epoch === b.epoch });
|
|
1532
1687
|
const injector = options?.injector ?? inject(Injector);
|
|
1533
1688
|
const req = computed(() => {
|
|
1534
1689
|
const state = trigger();
|
|
@@ -1537,7 +1692,7 @@ function manualQueryResource(request, options) {
|
|
|
1537
1692
|
if (state.override)
|
|
1538
1693
|
return state.override;
|
|
1539
1694
|
return untracked(request);
|
|
1540
|
-
}, { ...(ngDevMode ? { debugName: "req" } : {}), equal: () => false });
|
|
1695
|
+
}, { ...(ngDevMode ? { debugName: "req" } : /* istanbul ignore next */ {}), equal: () => false });
|
|
1541
1696
|
const resource = queryResource(req, options);
|
|
1542
1697
|
return {
|
|
1543
1698
|
...resource,
|
|
@@ -1586,14 +1741,14 @@ function mutationResource(request, options = {}) {
|
|
|
1586
1741
|
const { onMutate, onError, onSuccess, onSettled, equal, ...rest } = options;
|
|
1587
1742
|
const requestEqual = createEqualRequest(equal);
|
|
1588
1743
|
const eq = equal ?? Object.is;
|
|
1589
|
-
const next = signal(null, { ...(ngDevMode ? { debugName: "next" } : {}), equal: (a, b) => {
|
|
1744
|
+
const next = signal(null, { ...(ngDevMode ? { debugName: "next" } : /* istanbul ignore next */ {}), equal: (a, b) => {
|
|
1590
1745
|
if (!a && !b)
|
|
1591
1746
|
return true;
|
|
1592
1747
|
if (!a || !b)
|
|
1593
1748
|
return false;
|
|
1594
1749
|
return eq(a, b);
|
|
1595
1750
|
} });
|
|
1596
|
-
const queue = signal([], ...(ngDevMode ? [{ debugName: "queue" }] : []));
|
|
1751
|
+
const queue = signal([], ...(ngDevMode ? [{ debugName: "queue" }] : /* istanbul ignore next */ []));
|
|
1597
1752
|
let ctx = undefined;
|
|
1598
1753
|
const queueRef = effect(() => {
|
|
1599
1754
|
const nextInQueue = queue().at(0);
|
|
@@ -1603,14 +1758,14 @@ function mutationResource(request, options = {}) {
|
|
|
1603
1758
|
const [value, ictx] = nextInQueue;
|
|
1604
1759
|
ctx = onMutate?.(value, ictx);
|
|
1605
1760
|
next.set(value);
|
|
1606
|
-
}, ...(ngDevMode ? [{ debugName: "queueRef" }] : []));
|
|
1761
|
+
}, ...(ngDevMode ? [{ debugName: "queueRef" }] : /* istanbul ignore next */ []));
|
|
1607
1762
|
const req = computed(() => {
|
|
1608
1763
|
const nr = next();
|
|
1609
1764
|
if (!nr)
|
|
1610
1765
|
return;
|
|
1611
1766
|
return request(nr) ?? undefined;
|
|
1612
|
-
}, { ...(ngDevMode ? { debugName: "req" } : {}), equal: requestEqual });
|
|
1613
|
-
const lastValue = linkedSignal({ ...(ngDevMode ? { debugName: "lastValue" } : {}), source: next,
|
|
1767
|
+
}, { ...(ngDevMode ? { debugName: "req" } : /* istanbul ignore next */ {}), equal: requestEqual });
|
|
1768
|
+
const lastValue = linkedSignal({ ...(ngDevMode ? { debugName: "lastValue" } : /* istanbul ignore next */ {}), source: next,
|
|
1614
1769
|
computation: (next, prev) => {
|
|
1615
1770
|
if (next === null && !!prev)
|
|
1616
1771
|
return prev.value;
|
|
@@ -1621,7 +1776,7 @@ function mutationResource(request, options = {}) {
|
|
|
1621
1776
|
if (!nr)
|
|
1622
1777
|
return;
|
|
1623
1778
|
return request(nr) ?? undefined;
|
|
1624
|
-
}, { ...(ngDevMode ? { debugName: "lastValueRequest" } : {}), equal: requestEqual });
|
|
1779
|
+
}, { ...(ngDevMode ? { debugName: "lastValueRequest" } : /* istanbul ignore next */ {}), equal: requestEqual });
|
|
1625
1780
|
const cb = createCircuitBreaker(options?.circuitBreaker === true
|
|
1626
1781
|
? undefined
|
|
1627
1782
|
: (options?.circuitBreaker ?? false), options?.injector);
|