@mmstack/resource 20.2.11 → 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.
@@ -4,8 +4,6 @@ import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
4
4
  import { of, tap, map, finalize, shareReplay, interval, firstValueFrom, catchError, combineLatestWith, filter } from 'rxjs';
5
5
  import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext, HttpParams, httpResource, HttpClient } from '@angular/common/http';
6
6
  import { mutable, toWritable, sensor } from '@mmstack/primitives';
7
- import { v7 } from 'uuid';
8
- import { keys, hash, entries } from '@mmstack/object';
9
7
 
10
8
  function createNoopDB() {
11
9
  return {
@@ -94,6 +92,12 @@ function createSingleStoreDB(name, getStoreName, version = 1) {
94
92
  });
95
93
  }
96
94
 
95
+ function generateID() {
96
+ if (globalThis.crypto?.randomUUID) {
97
+ return globalThis.crypto.randomUUID();
98
+ }
99
+ return Math.random().toString(36).substring(2);
100
+ }
97
101
  function isSyncMessage(msg) {
98
102
  return (typeof msg === 'object' &&
99
103
  msg !== null &&
@@ -118,7 +122,7 @@ class Cache {
118
122
  db;
119
123
  internal = mutable(new Map());
120
124
  cleanupOpt;
121
- id = v7();
125
+ id = generateID();
122
126
  /**
123
127
  * Destroys the cache instance, cleaning up any resources used by the cache.
124
128
  * This method is called automatically when the cache instance is garbage collected.
@@ -927,6 +931,77 @@ function createDedupeRequestsInterceptor(allowed = ['GET', 'DELETE', 'HEAD', 'OP
927
931
  };
928
932
  }
929
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
+ }
930
1005
  function equalTransferCache(a, b) {
931
1006
  if (!a && !b)
932
1007
  return true;
@@ -993,8 +1068,8 @@ function equalParams(a, b) {
993
1068
  return false;
994
1069
  const aObj = a instanceof HttpParams ? paramToObject(a) : a;
995
1070
  const bObj = b instanceof HttpParams ? paramToObject(b) : b;
996
- const aKeys = keys(aObj);
997
- const bKeys = keys(bObj);
1071
+ const aKeys = Object.keys(aObj);
1072
+ const bKeys = Object.keys(bObj);
998
1073
  if (aKeys.length !== bKeys.length)
999
1074
  return false;
1000
1075
  return aKeys.every((key) => {
@@ -1018,8 +1093,8 @@ function equalHeaders(a, b) {
1018
1093
  return false;
1019
1094
  const aObj = a instanceof HttpHeaders ? headersToObject(a) : a;
1020
1095
  const bObj = b instanceof HttpHeaders ? headersToObject(b) : b;
1021
- const aKeys = keys(aObj);
1022
- const bKeys = keys(bObj);
1096
+ const aKeys = Object.keys(aObj);
1097
+ const bKeys = Object.keys(bObj);
1023
1098
  if (aKeys.length !== bKeys.length)
1024
1099
  return false;
1025
1100
  return aKeys.every((key) => {
@@ -1029,16 +1104,31 @@ function equalHeaders(a, b) {
1029
1104
  return aObj[key] === bObj[key];
1030
1105
  });
1031
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
+ }
1032
1119
  function equalContext(a, b) {
1033
1120
  if (!a && !b)
1034
1121
  return true;
1035
1122
  if (!a || !b)
1036
1123
  return false;
1037
- const aKeys = keys(a);
1038
- const bKeys = keys(b);
1039
- if (aKeys.length !== bKeys.length)
1124
+ const aEntries = toHttpContextEntries(a);
1125
+ const bEntries = toHttpContextEntries(b);
1126
+ if (aEntries.length !== bEntries.length)
1040
1127
  return false;
1041
- 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));
1042
1132
  }
1043
1133
  function createEqualRequest(equalResult) {
1044
1134
  const eqb = equalResult ?? equalBody;
@@ -1211,7 +1301,7 @@ function normalizeParams(params) {
1211
1301
  if (params instanceof HttpParams)
1212
1302
  return params.toString();
1213
1303
  const paramMap = new Map();
1214
- for (const [key, value] of entries(params)) {
1304
+ for (const [key, value] of Object.entries(params)) {
1215
1305
  if (Array.isArray(value)) {
1216
1306
  paramMap.set(key, value.map(encodeURIComponent).join(','));
1217
1307
  }