@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.
@@ -1,10 +1,9 @@
1
- import { isDevMode, untracked, computed, InjectionToken, inject, signal, effect, Injector, linkedSignal, DestroyRef } from '@angular/core';
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 = v7();
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 aKeys = keys(a);
1037
- const bKeys = keys(b);
1038
- if (aKeys.length !== bKeys.length)
1124
+ const aEntries = toHttpContextEntries(a);
1125
+ const bEntries = toHttpContextEntries(b);
1126
+ if (aEntries.length !== bEntries.length)
1039
1127
  return false;
1040
- return aKeys.every((key) => a[key] === b[key]);
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: options?.triggerOnSameRequest
1230
- ? undefined
1231
- : createEqualRequest(options?.equal) }] : [{
1232
- equal: options?.triggerOnSameRequest
1233
- ? undefined
1234
- : createEqualRequest(options?.equal),
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
- ctx = onMutate?.(value, ictx);
1521
- next.set(value);
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