@mmstack/resource 20.2.10 → 20.3.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/fesm2022/mmstack-resource.mjs +156 -24
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/index.d.ts +5 -0
- package/package.json +1 -3
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { isDevMode, untracked, computed, InjectionToken, inject, signal, effect, Injector, linkedSignal, Injectable, DestroyRef } from '@angular/core';
|
|
2
3
|
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
3
4
|
import { of, tap, map, finalize, shareReplay, interval, firstValueFrom, catchError, combineLatestWith, filter } from 'rxjs';
|
|
4
5
|
import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext, HttpParams, httpResource, HttpClient } from '@angular/common/http';
|
|
5
|
-
import { mutable, toWritable } from '@mmstack/primitives';
|
|
6
|
-
import { v7 } from 'uuid';
|
|
7
|
-
import { keys, hash, entries } from '@mmstack/object';
|
|
6
|
+
import { mutable, toWritable, sensor } from '@mmstack/primitives';
|
|
8
7
|
|
|
9
8
|
function createNoopDB() {
|
|
10
9
|
return {
|
|
@@ -93,6 +92,12 @@ function createSingleStoreDB(name, getStoreName, version = 1) {
|
|
|
93
92
|
});
|
|
94
93
|
}
|
|
95
94
|
|
|
95
|
+
function generateID() {
|
|
96
|
+
if (globalThis.crypto?.randomUUID) {
|
|
97
|
+
return globalThis.crypto.randomUUID();
|
|
98
|
+
}
|
|
99
|
+
return Math.random().toString(36).substring(2);
|
|
100
|
+
}
|
|
96
101
|
function isSyncMessage(msg) {
|
|
97
102
|
return (typeof msg === 'object' &&
|
|
98
103
|
msg !== null &&
|
|
@@ -117,7 +122,7 @@ class Cache {
|
|
|
117
122
|
db;
|
|
118
123
|
internal = mutable(new Map());
|
|
119
124
|
cleanupOpt;
|
|
120
|
-
id =
|
|
125
|
+
id = generateID();
|
|
121
126
|
/**
|
|
122
127
|
* Destroys the cache instance, cleaning up any resources used by the cache.
|
|
123
128
|
* This method is called automatically when the cache instance is garbage collected.
|
|
@@ -926,6 +931,77 @@ function createDedupeRequestsInterceptor(allowed = ['GET', 'DELETE', 'HEAD', 'OP
|
|
|
926
931
|
};
|
|
927
932
|
}
|
|
928
933
|
|
|
934
|
+
/**
|
|
935
|
+
* Checks if `value` is a plain JavaScript object (e.g., `{}` or `new Object()`).
|
|
936
|
+
* Distinguishes from arrays, null, and class instances. Acts as a type predicate,
|
|
937
|
+
* narrowing `value` to `UnknownObject` if `true`.
|
|
938
|
+
*
|
|
939
|
+
* @param value The value to check.
|
|
940
|
+
* @returns {value is UnknownObject} `true` if `value` is a plain object, otherwise `false`.
|
|
941
|
+
* @example
|
|
942
|
+
* isPlainObject({}) // => true
|
|
943
|
+
* isPlainObject([]) // => false
|
|
944
|
+
* isPlainObject(null) // => false
|
|
945
|
+
* isPlainObject(new Date()) // => false
|
|
946
|
+
*/
|
|
947
|
+
function isPlainObject(value) {
|
|
948
|
+
return (typeof value === 'object' && value !== null && value.constructor === Object);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Internal helper to generate a stable JSON string from an array.
|
|
952
|
+
* Sorts keys of plain objects within the array alphabetically before serialization
|
|
953
|
+
* to ensure hash stability regardless of key order.
|
|
954
|
+
*
|
|
955
|
+
* @param queryKey The array of values to serialize.
|
|
956
|
+
* @returns A stable JSON string representation.
|
|
957
|
+
* @internal
|
|
958
|
+
*/
|
|
959
|
+
function hashKey(queryKey) {
|
|
960
|
+
return JSON.stringify(queryKey, (_, val) => isPlainObject(val)
|
|
961
|
+
? Object.keys(val)
|
|
962
|
+
.toSorted()
|
|
963
|
+
.reduce((result, key) => {
|
|
964
|
+
result[key] = val[key];
|
|
965
|
+
return result;
|
|
966
|
+
}, {})
|
|
967
|
+
: val);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Generates a stable, unique string hash from one or more arguments.
|
|
971
|
+
* Useful for creating cache keys or identifiers where object key order shouldn't matter.
|
|
972
|
+
*
|
|
973
|
+
* How it works:
|
|
974
|
+
* - Plain objects within the arguments have their keys sorted alphabetically before hashing.
|
|
975
|
+
* This ensures that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same hash.
|
|
976
|
+
* - Uses `JSON.stringify` internally with custom sorting for plain objects via `hashKey`.
|
|
977
|
+
* - Non-plain objects (arrays, Dates, etc.) and primitives are serialized naturally.
|
|
978
|
+
*
|
|
979
|
+
* @param {...unknown} args Values to include in the hash.
|
|
980
|
+
* @returns A stable string hash representing the input arguments.
|
|
981
|
+
* @example
|
|
982
|
+
* const userQuery = (id: number) => ['user', { id, timestamp: Date.now() }];
|
|
983
|
+
*
|
|
984
|
+
* const obj1 = { a: 1, b: 2 };
|
|
985
|
+
* const obj2 = { b: 2, a: 1 }; // Same keys/values, different order
|
|
986
|
+
*
|
|
987
|
+
* hash('posts', 10);
|
|
988
|
+
* // => '["posts",10]'
|
|
989
|
+
*
|
|
990
|
+
* hash('config', obj1);
|
|
991
|
+
* // => '["config",{"a":1,"b":2}]'
|
|
992
|
+
*
|
|
993
|
+
* hash('config', obj2);
|
|
994
|
+
* // => '["config",{"a":1,"b":2}]' (Same as above due to key sorting)
|
|
995
|
+
*
|
|
996
|
+
* hash(['todos', { status: 'done', owner: obj1 }]);
|
|
997
|
+
* // => '[["todos",{"owner":{"a":1,"b":2},"status":"done"}]]'
|
|
998
|
+
*
|
|
999
|
+
* // Be mindful of values JSON.stringify cannot handle (functions, undefined, Symbols)
|
|
1000
|
+
* // hash('a', undefined, function() {}) => '["a",null,null]'
|
|
1001
|
+
*/
|
|
1002
|
+
function hash(...args) {
|
|
1003
|
+
return hashKey(args);
|
|
1004
|
+
}
|
|
929
1005
|
function equalTransferCache(a, b) {
|
|
930
1006
|
if (!a && !b)
|
|
931
1007
|
return true;
|
|
@@ -992,8 +1068,8 @@ function equalParams(a, b) {
|
|
|
992
1068
|
return false;
|
|
993
1069
|
const aObj = a instanceof HttpParams ? paramToObject(a) : a;
|
|
994
1070
|
const bObj = b instanceof HttpParams ? paramToObject(b) : b;
|
|
995
|
-
const aKeys = keys(aObj);
|
|
996
|
-
const bKeys = keys(bObj);
|
|
1071
|
+
const aKeys = Object.keys(aObj);
|
|
1072
|
+
const bKeys = Object.keys(bObj);
|
|
997
1073
|
if (aKeys.length !== bKeys.length)
|
|
998
1074
|
return false;
|
|
999
1075
|
return aKeys.every((key) => {
|
|
@@ -1017,8 +1093,8 @@ function equalHeaders(a, b) {
|
|
|
1017
1093
|
return false;
|
|
1018
1094
|
const aObj = a instanceof HttpHeaders ? headersToObject(a) : a;
|
|
1019
1095
|
const bObj = b instanceof HttpHeaders ? headersToObject(b) : b;
|
|
1020
|
-
const aKeys = keys(aObj);
|
|
1021
|
-
const bKeys = keys(bObj);
|
|
1096
|
+
const aKeys = Object.keys(aObj);
|
|
1097
|
+
const bKeys = Object.keys(bObj);
|
|
1022
1098
|
if (aKeys.length !== bKeys.length)
|
|
1023
1099
|
return false;
|
|
1024
1100
|
return aKeys.every((key) => {
|
|
@@ -1028,16 +1104,31 @@ function equalHeaders(a, b) {
|
|
|
1028
1104
|
return aObj[key] === bObj[key];
|
|
1029
1105
|
});
|
|
1030
1106
|
}
|
|
1107
|
+
function toHttpContextEntries(ctx) {
|
|
1108
|
+
if (!ctx)
|
|
1109
|
+
return [];
|
|
1110
|
+
if (ctx instanceof HttpContext) {
|
|
1111
|
+
const tokens = Array.from(ctx.keys());
|
|
1112
|
+
return tokens.map((key) => [key.toString(), ctx.get(key)]);
|
|
1113
|
+
}
|
|
1114
|
+
if (typeof ctx === 'object') {
|
|
1115
|
+
return Object.entries(ctx);
|
|
1116
|
+
}
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1031
1119
|
function equalContext(a, b) {
|
|
1032
1120
|
if (!a && !b)
|
|
1033
1121
|
return true;
|
|
1034
1122
|
if (!a || !b)
|
|
1035
1123
|
return false;
|
|
1036
|
-
const
|
|
1037
|
-
const
|
|
1038
|
-
if (
|
|
1124
|
+
const aEntries = toHttpContextEntries(a);
|
|
1125
|
+
const bEntries = toHttpContextEntries(b);
|
|
1126
|
+
if (aEntries.length !== bEntries.length)
|
|
1039
1127
|
return false;
|
|
1040
|
-
|
|
1128
|
+
if (aEntries.length === 0)
|
|
1129
|
+
return true;
|
|
1130
|
+
const bMap = new Map(bEntries);
|
|
1131
|
+
return aEntries.every(([key, value]) => value === bMap.get(key));
|
|
1041
1132
|
}
|
|
1042
1133
|
function createEqualRequest(equalResult) {
|
|
1043
1134
|
const eqb = equalResult ?? equalBody;
|
|
@@ -1173,6 +1264,21 @@ function retryOnError(res, opt) {
|
|
|
1173
1264
|
};
|
|
1174
1265
|
}
|
|
1175
1266
|
|
|
1267
|
+
class ResourceSensors {
|
|
1268
|
+
networkStatus = sensor('networkStatus');
|
|
1269
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ResourceSensors, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1270
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ResourceSensors, providedIn: 'root' });
|
|
1271
|
+
}
|
|
1272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ResourceSensors, decorators: [{
|
|
1273
|
+
type: Injectable,
|
|
1274
|
+
args: [{
|
|
1275
|
+
providedIn: 'root',
|
|
1276
|
+
}]
|
|
1277
|
+
}] });
|
|
1278
|
+
function injectNetworkStatus() {
|
|
1279
|
+
return inject(ResourceSensors).networkStatus;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1176
1282
|
function toResourceObject(res) {
|
|
1177
1283
|
return {
|
|
1178
1284
|
asReadonly: () => res.asReadonly(),
|
|
@@ -1195,7 +1301,7 @@ function normalizeParams(params) {
|
|
|
1195
1301
|
if (params instanceof HttpParams)
|
|
1196
1302
|
return params.toString();
|
|
1197
1303
|
const paramMap = new Map();
|
|
1198
|
-
for (const [key, value] of entries(params)) {
|
|
1304
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1199
1305
|
if (Array.isArray(value)) {
|
|
1200
1306
|
paramMap.set(key, value.map(encodeURIComponent).join(','));
|
|
1201
1307
|
}
|
|
@@ -1222,16 +1328,24 @@ function queryResource(request, options) {
|
|
|
1222
1328
|
const cb = createCircuitBreaker(options?.circuitBreaker === true
|
|
1223
1329
|
? undefined
|
|
1224
1330
|
: (options?.circuitBreaker ?? false), options?.injector);
|
|
1331
|
+
const networkAvailable = injectNetworkStatus();
|
|
1332
|
+
const eq = options?.triggerOnSameRequest
|
|
1333
|
+
? undefined
|
|
1334
|
+
: createEqualRequest(options?.equal);
|
|
1225
1335
|
const stableRequest = computed(() => {
|
|
1226
|
-
if (cb.isOpen())
|
|
1336
|
+
if (!networkAvailable() || cb.isOpen())
|
|
1227
1337
|
return undefined;
|
|
1228
1338
|
return request() ?? undefined;
|
|
1229
|
-
}, ...(ngDevMode ? [{ debugName: "stableRequest", equal:
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1339
|
+
}, ...(ngDevMode ? [{ debugName: "stableRequest", equal: (a, b) => {
|
|
1340
|
+
if (eq)
|
|
1341
|
+
return eq(a, b);
|
|
1342
|
+
return a === b;
|
|
1343
|
+
} }] : [{
|
|
1344
|
+
equal: (a, b) => {
|
|
1345
|
+
if (eq)
|
|
1346
|
+
return eq(a, b);
|
|
1347
|
+
return a === b;
|
|
1348
|
+
},
|
|
1235
1349
|
}]));
|
|
1236
1350
|
const hashFn = typeof options?.cache === 'object'
|
|
1237
1351
|
? (options.cache.hash ?? urlWithParams)
|
|
@@ -1301,6 +1415,7 @@ function queryResource(request, options) {
|
|
|
1301
1415
|
resource = persistResourceValues(resource, options?.keepPrevious, options?.equal);
|
|
1302
1416
|
const value = options?.cache
|
|
1303
1417
|
? toWritable(computed(() => {
|
|
1418
|
+
resource.value();
|
|
1304
1419
|
return cacheEntry()?.value ?? resource.value();
|
|
1305
1420
|
}), resource.value.set, resource.value.update)
|
|
1306
1421
|
: resource.value;
|
|
@@ -1447,6 +1562,17 @@ function mutationResource(request, options = {}) {
|
|
|
1447
1562
|
return eq(a, b);
|
|
1448
1563
|
},
|
|
1449
1564
|
}]));
|
|
1565
|
+
const queue = signal([], ...(ngDevMode ? [{ debugName: "queue" }] : []));
|
|
1566
|
+
let ctx = undefined;
|
|
1567
|
+
const queueRef = effect(() => {
|
|
1568
|
+
const nextInQueue = queue().at(0);
|
|
1569
|
+
if (!nextInQueue || next() !== null)
|
|
1570
|
+
return;
|
|
1571
|
+
queue.update((q) => q.slice(1));
|
|
1572
|
+
const [value, ictx] = nextInQueue;
|
|
1573
|
+
ctx = onMutate?.(value, ictx);
|
|
1574
|
+
next.set(value);
|
|
1575
|
+
}, ...(ngDevMode ? [{ debugName: "queueRef" }] : []));
|
|
1450
1576
|
const req = computed(() => {
|
|
1451
1577
|
const nr = next();
|
|
1452
1578
|
if (!nr)
|
|
@@ -1479,7 +1605,6 @@ function mutationResource(request, options = {}) {
|
|
|
1479
1605
|
circuitBreaker: cb,
|
|
1480
1606
|
defaultValue: null, // doesnt matter since .value is not accessible
|
|
1481
1607
|
});
|
|
1482
|
-
let ctx = undefined;
|
|
1483
1608
|
const destroyRef = options.injector
|
|
1484
1609
|
? options.injector.get(DestroyRef)
|
|
1485
1610
|
: inject(DestroyRef);
|
|
@@ -1510,15 +1635,22 @@ function mutationResource(request, options = {}) {
|
|
|
1510
1635
|
ctx = undefined;
|
|
1511
1636
|
next.set(null);
|
|
1512
1637
|
});
|
|
1638
|
+
const shouldQueue = options.queue ?? false;
|
|
1513
1639
|
return {
|
|
1514
1640
|
...resource,
|
|
1515
1641
|
destroy: () => {
|
|
1516
1642
|
statusSub.unsubscribe();
|
|
1517
1643
|
resource.destroy();
|
|
1644
|
+
queueRef.destroy();
|
|
1518
1645
|
},
|
|
1519
1646
|
mutate: (value, ictx) => {
|
|
1520
|
-
|
|
1521
|
-
|
|
1647
|
+
if (shouldQueue) {
|
|
1648
|
+
return queue.update((q) => [...q, [value, ictx]]);
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
ctx = onMutate?.(value, ictx);
|
|
1652
|
+
next.set(value);
|
|
1653
|
+
}
|
|
1522
1654
|
},
|
|
1523
1655
|
current: next,
|
|
1524
1656
|
// redeclare disabled with last value so that it is not affected by the resource's internal disablement logic
|