@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.
- package/README.md +508 -73
- package/fesm2022/mmstack-resource.mjs +151 -67
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/package.json +5 -3
- package/types/mmstack-resource.d.ts +81 -11
|
@@ -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
|
|
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
|
-
|
|
776
|
+
threshold: 5,
|
|
732
777
|
timeout: 30000,
|
|
733
778
|
shouldFail: () => true,
|
|
734
779
|
shouldFailForever: () => false,
|
|
735
780
|
};
|
|
736
781
|
/** @internal */
|
|
737
|
-
function internalCeateCircuitBreaker(
|
|
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() >=
|
|
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(
|
|
801
|
+
failureCount.set(threshold - 1);
|
|
756
802
|
};
|
|
757
|
-
|
|
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
|
-
|
|
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`:
|
|
837
|
-
* - `
|
|
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 {
|
|
944
|
+
const { threshold, timeout, shouldFail, shouldFailForever } = {
|
|
861
945
|
...injectCircuitBreakerOptions(injector),
|
|
862
|
-
...opt,
|
|
946
|
+
...normalizeThreshold(opt),
|
|
863
947
|
};
|
|
864
|
-
return internalCeateCircuitBreaker(
|
|
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
|
|
1241
|
-
|
|
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
|
|
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.
|
|
1273
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
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.
|
|
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 (
|
|
1437
|
+
if (disabledReason() !== null)
|
|
1341
1438
|
return undefined;
|
|
1342
|
-
const req =
|
|
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(() =>
|
|
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);
|