@mmstack/resource 19.2.0 → 19.3.1
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/LICENSE +21 -21
- package/README.md +120 -111
- package/fesm2022/mmstack-resource.mjs +788 -139
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/index.d.ts +4 -1
- package/lib/manual-query.d.ts +38 -0
- package/lib/mutation-resource.d.ts +20 -19
- package/lib/query-resource.d.ts +50 -7
- package/lib/util/cache/{cache.interceptor.d.ts → cache-interceptor.d.ts} +4 -0
- package/lib/util/cache/cache.d.ts +51 -5
- package/lib/util/cache/index.d.ts +1 -1
- package/lib/util/cache/persistence.d.ts +10 -0
- package/lib/util/cache/public_api.d.ts +1 -1
- package/lib/util/catch-value-error.d.ts +2 -0
- package/lib/util/circuit-breaker.d.ts +36 -7
- package/lib/util/index.d.ts +4 -1
- package/lib/util/persist.d.ts +1 -1
- package/lib/util/public_api.d.ts +2 -2
- package/lib/util/sensors.d.ts +7 -0
- package/lib/util/to-resource-object.d.ts +2 -0
- package/lib/util/url-with-params.d.ts +1 -1
- package/package.json +4 -6
- package/lib/public_api.d.ts +0 -3
- /package/lib/util/{dedupe.interceptor.d.ts → dedupe-interceptor.d.ts} +0 -0
|
@@ -1,11 +1,109 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { isDevMode, untracked, computed, InjectionToken, inject, signal, effect, Injector, linkedSignal, ResourceStatus, Injectable, DestroyRef } from '@angular/core';
|
|
3
|
+
import { mutable, toWritable, sensor, nestedEffect } from '@mmstack/primitives';
|
|
4
|
+
import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext, HttpParams, httpResource, HttpClient } from '@angular/common/http';
|
|
5
|
+
import { of, tap, map, finalize, shareReplay, interval, firstValueFrom, catchError, combineLatestWith, filter } from 'rxjs';
|
|
2
6
|
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
3
|
-
import { of, tap, map, finalize, shareReplay, interval, firstValueFrom, combineLatestWith, filter } from 'rxjs';
|
|
4
|
-
import { HttpContextToken, HttpContext, HttpResponse, 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';
|
|
8
7
|
|
|
8
|
+
function createNoopDB() {
|
|
9
|
+
return {
|
|
10
|
+
getAll: async () => [],
|
|
11
|
+
store: async () => {
|
|
12
|
+
// noop
|
|
13
|
+
},
|
|
14
|
+
remove: async () => {
|
|
15
|
+
// noop
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function toCacheDB(db, storeName) {
|
|
20
|
+
const getAll = async () => {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
return new Promise((res, rej) => {
|
|
23
|
+
const transaction = db.transaction(storeName, 'readonly');
|
|
24
|
+
const store = transaction.objectStore(storeName);
|
|
25
|
+
const request = store.getAll();
|
|
26
|
+
request.onsuccess = () => res(request.result);
|
|
27
|
+
request.onerror = () => rej(request.error);
|
|
28
|
+
})
|
|
29
|
+
.then((entries) => entries.filter((e) => e.expiresAt > now))
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
if (isDevMode())
|
|
32
|
+
console.error('Error getting all items from cache DB:', err);
|
|
33
|
+
return [];
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const store = (value) => {
|
|
37
|
+
return new Promise((res, rej) => {
|
|
38
|
+
const transaction = db.transaction(storeName, 'readwrite');
|
|
39
|
+
const store = transaction.objectStore(storeName);
|
|
40
|
+
store.put(value);
|
|
41
|
+
transaction.oncomplete = () => res();
|
|
42
|
+
transaction.onerror = () => rej(transaction.error);
|
|
43
|
+
}).catch((err) => {
|
|
44
|
+
if (isDevMode())
|
|
45
|
+
console.error('Error storing item in cache DB:', err);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const remove = (key) => {
|
|
49
|
+
return new Promise((res, rej) => {
|
|
50
|
+
const transaction = db.transaction(storeName, 'readwrite');
|
|
51
|
+
const store = transaction.objectStore(storeName);
|
|
52
|
+
store.delete(key);
|
|
53
|
+
transaction.oncomplete = () => res();
|
|
54
|
+
transaction.onerror = () => rej(transaction.error);
|
|
55
|
+
}).catch((err) => {
|
|
56
|
+
if (isDevMode())
|
|
57
|
+
console.error('Error removing item from cache DB:', err);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
getAll,
|
|
62
|
+
store,
|
|
63
|
+
remove,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function createSingleStoreDB(name, getStoreName, version = 1) {
|
|
67
|
+
const storeName = getStoreName(version);
|
|
68
|
+
if (!globalThis.indexedDB)
|
|
69
|
+
return Promise.resolve(createNoopDB());
|
|
70
|
+
return new Promise((res, rej) => {
|
|
71
|
+
if (version < 1)
|
|
72
|
+
rej(new Error('Version must be 1 or greater'));
|
|
73
|
+
const req = indexedDB.open(name, version);
|
|
74
|
+
req.onupgradeneeded = (event) => {
|
|
75
|
+
const db = req.result;
|
|
76
|
+
const oldVersion = event.oldVersion;
|
|
77
|
+
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
78
|
+
if (oldVersion > 0) {
|
|
79
|
+
db.deleteObjectStore(getStoreName(oldVersion));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
req.onerror = () => {
|
|
83
|
+
rej(req.error);
|
|
84
|
+
};
|
|
85
|
+
req.onsuccess = () => res(req.result);
|
|
86
|
+
})
|
|
87
|
+
.then((db) => toCacheDB(db, storeName))
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
if (isDevMode())
|
|
90
|
+
console.error('Error creating query DB:', err);
|
|
91
|
+
return createNoopDB();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function generateID() {
|
|
96
|
+
if (globalThis.crypto?.randomUUID) {
|
|
97
|
+
return globalThis.crypto.randomUUID();
|
|
98
|
+
}
|
|
99
|
+
return Math.random().toString(36).substring(2);
|
|
100
|
+
}
|
|
101
|
+
function isSyncMessage(msg) {
|
|
102
|
+
return (typeof msg === 'object' &&
|
|
103
|
+
msg !== null &&
|
|
104
|
+
'type' in msg &&
|
|
105
|
+
msg.type === 'cache-sync-message');
|
|
106
|
+
}
|
|
9
107
|
const ONE_DAY = 1000 * 60 * 60 * 24;
|
|
10
108
|
const ONE_HOUR = 1000 * 60 * 60;
|
|
11
109
|
const DEFAULT_CLEANUP_OPT = {
|
|
@@ -21,8 +119,18 @@ const DEFAULT_CLEANUP_OPT = {
|
|
|
21
119
|
class Cache {
|
|
22
120
|
ttl;
|
|
23
121
|
staleTime;
|
|
122
|
+
db;
|
|
24
123
|
internal = mutable(new Map());
|
|
25
124
|
cleanupOpt;
|
|
125
|
+
id = generateID();
|
|
126
|
+
/**
|
|
127
|
+
* Destroys the cache instance, cleaning up any resources used by the cache.
|
|
128
|
+
* This method is called automatically when the cache instance is garbage collected.
|
|
129
|
+
*/
|
|
130
|
+
destroy;
|
|
131
|
+
broadcast = () => {
|
|
132
|
+
// noop
|
|
133
|
+
};
|
|
26
134
|
/**
|
|
27
135
|
* Creates a new `Cache` instance.
|
|
28
136
|
*
|
|
@@ -31,14 +139,17 @@ class Cache {
|
|
|
31
139
|
* stale but can still be used while revalidation occurs in the background. Defaults to 1 hour.
|
|
32
140
|
* @param cleanupOpt - Options for configuring the cache cleanup strategy. Defaults to LRU with a
|
|
33
141
|
* `maxSize` of 200 and a `checkInterval` of one hour.
|
|
142
|
+
* @param syncTabs - If provided, the cache will use the options a BroadcastChannel to send updates between tabs.
|
|
143
|
+
* Defaults to `undefined`, meaning no synchronization across tabs.
|
|
34
144
|
*/
|
|
35
145
|
constructor(ttl = ONE_DAY, staleTime = ONE_HOUR, cleanupOpt = {
|
|
36
146
|
type: 'lru',
|
|
37
147
|
maxSize: 1000,
|
|
38
148
|
checkInterval: ONE_HOUR,
|
|
39
|
-
}) {
|
|
149
|
+
}, syncTabs, db = Promise.resolve(createNoopDB())) {
|
|
40
150
|
this.ttl = ttl;
|
|
41
151
|
this.staleTime = staleTime;
|
|
152
|
+
this.db = db;
|
|
42
153
|
this.cleanupOpt = {
|
|
43
154
|
...DEFAULT_CLEANUP_OPT,
|
|
44
155
|
...cleanupOpt,
|
|
@@ -49,14 +160,82 @@ class Cache {
|
|
|
49
160
|
const cleanupInterval = setInterval(() => {
|
|
50
161
|
this.cleanup();
|
|
51
162
|
}, cleanupOpt.checkInterval);
|
|
52
|
-
|
|
163
|
+
let destroySyncTabs = () => {
|
|
164
|
+
// noop
|
|
165
|
+
};
|
|
166
|
+
if (syncTabs) {
|
|
167
|
+
const channel = new BroadcastChannel(syncTabs.id);
|
|
168
|
+
this.broadcast = (msg) => {
|
|
169
|
+
if (msg.action === 'invalidate')
|
|
170
|
+
return channel.postMessage({
|
|
171
|
+
action: 'invalidate',
|
|
172
|
+
entry: { key: msg.entry.key },
|
|
173
|
+
cacheId: this.id,
|
|
174
|
+
type: 'cache-sync-message',
|
|
175
|
+
});
|
|
176
|
+
return channel.postMessage({
|
|
177
|
+
...msg,
|
|
178
|
+
entry: {
|
|
179
|
+
...msg.entry,
|
|
180
|
+
value: syncTabs.serialize(msg.entry.value),
|
|
181
|
+
},
|
|
182
|
+
cacheId: this.id,
|
|
183
|
+
type: 'cache-sync-message',
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
channel.onmessage = (event) => {
|
|
187
|
+
const msg = event.data;
|
|
188
|
+
if (!isSyncMessage(msg))
|
|
189
|
+
return;
|
|
190
|
+
if (msg.cacheId === this.id)
|
|
191
|
+
return; // ignore messages from this cache
|
|
192
|
+
if (msg.action === 'store') {
|
|
193
|
+
const value = syncTabs.deserialize(msg.entry.value);
|
|
194
|
+
if (value === null)
|
|
195
|
+
return;
|
|
196
|
+
this.storeInternal(msg.entry.key, value, msg.entry.stale - msg.entry.updated, msg.entry.expiresAt - msg.entry.updated, true, false);
|
|
197
|
+
}
|
|
198
|
+
else if (msg.action === 'invalidate') {
|
|
199
|
+
this.invalidateInternal(msg.entry.key, true);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
destroySyncTabs = () => {
|
|
203
|
+
channel.close();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
let destroyed = false;
|
|
207
|
+
const destroy = () => {
|
|
208
|
+
if (destroyed)
|
|
209
|
+
return;
|
|
210
|
+
destroyed = true;
|
|
211
|
+
clearInterval(cleanupInterval);
|
|
212
|
+
destroySyncTabs();
|
|
213
|
+
};
|
|
214
|
+
this.db
|
|
215
|
+
.then(async (db) => {
|
|
216
|
+
if (destroyed)
|
|
217
|
+
return [];
|
|
218
|
+
return db.getAll();
|
|
219
|
+
})
|
|
220
|
+
.then((entries) => {
|
|
221
|
+
if (destroyed)
|
|
222
|
+
return;
|
|
223
|
+
// load entries into the cache
|
|
224
|
+
const current = untracked(this.internal);
|
|
225
|
+
entries.forEach((entry) => {
|
|
226
|
+
if (current.has(entry.key))
|
|
227
|
+
return;
|
|
228
|
+
this.storeInternal(entry.key, entry.value, entry.stale - entry.updated, entry.expiresAt - entry.updated, true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
this.destroy = destroy;
|
|
53
232
|
// cleanup if object is garbage collected, this is because the cache can be quite large from a memory standpoint & we dont want all that floating garbage
|
|
54
233
|
const registry = new FinalizationRegistry((id) => {
|
|
55
|
-
if (id ===
|
|
56
|
-
|
|
234
|
+
if (id === this.id) {
|
|
235
|
+
destroy();
|
|
57
236
|
}
|
|
58
237
|
});
|
|
59
|
-
registry.register(this,
|
|
238
|
+
registry.register(this, this.id);
|
|
60
239
|
}
|
|
61
240
|
/** @internal */
|
|
62
241
|
getInternal(key) {
|
|
@@ -96,6 +275,17 @@ class Cache {
|
|
|
96
275
|
get(key) {
|
|
97
276
|
return this.getInternal(key);
|
|
98
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Retrieves a cache entry or an object with the key if not found.
|
|
280
|
+
*
|
|
281
|
+
* @param key - A function that returns the cache key. The key is a signal, allowing for dynamic keys. If the function returns null the value is also null.
|
|
282
|
+
* @returns A signal that holds the cache entry or an object with the key if not found. The signal
|
|
283
|
+
* updates whenever the cache entry changes (e.g., due to revalidation or expiration).
|
|
284
|
+
*/
|
|
285
|
+
getEntryOrKey(key) {
|
|
286
|
+
const valueSig = this.getInternal(key);
|
|
287
|
+
return computed(() => valueSig() ?? key());
|
|
288
|
+
}
|
|
99
289
|
/**
|
|
100
290
|
* Stores a value in the cache.
|
|
101
291
|
*
|
|
@@ -104,7 +294,10 @@ class Cache {
|
|
|
104
294
|
* @param staleTime - (Optional) The stale time for this entry, in milliseconds. Overrides the default `staleTime`.
|
|
105
295
|
* @param ttl - (Optional) The TTL for this entry, in milliseconds. Overrides the default `ttl`.
|
|
106
296
|
*/
|
|
107
|
-
store(key, value, staleTime = this.staleTime, ttl = this.ttl) {
|
|
297
|
+
store(key, value, staleTime = this.staleTime, ttl = this.ttl, persist = false) {
|
|
298
|
+
this.storeInternal(key, value, staleTime, ttl, false, persist);
|
|
299
|
+
}
|
|
300
|
+
storeInternal(key, value, staleTime = this.staleTime, ttl = this.ttl, fromSync = false, persist = false) {
|
|
108
301
|
const entry = this.getUntracked(key);
|
|
109
302
|
if (entry) {
|
|
110
303
|
clearTimeout(entry.timeout); // stop invalidation
|
|
@@ -114,17 +307,30 @@ class Cache {
|
|
|
114
307
|
if (ttl < staleTime)
|
|
115
308
|
staleTime = ttl;
|
|
116
309
|
const now = Date.now();
|
|
310
|
+
const next = {
|
|
311
|
+
value,
|
|
312
|
+
created: entry?.created ?? now,
|
|
313
|
+
updated: now,
|
|
314
|
+
useCount: prevCount + 1,
|
|
315
|
+
stale: now + staleTime,
|
|
316
|
+
expiresAt: now + ttl,
|
|
317
|
+
key,
|
|
318
|
+
};
|
|
117
319
|
this.internal.mutate((map) => {
|
|
118
320
|
map.set(key, {
|
|
119
|
-
|
|
120
|
-
created: entry?.created ?? now,
|
|
121
|
-
useCount: prevCount + 1,
|
|
122
|
-
stale: now + staleTime,
|
|
123
|
-
expiresAt: now + ttl,
|
|
321
|
+
...next,
|
|
124
322
|
timeout: setTimeout(() => this.invalidate(key), ttl),
|
|
125
323
|
});
|
|
126
324
|
return map;
|
|
127
325
|
});
|
|
326
|
+
if (!fromSync) {
|
|
327
|
+
if (persist)
|
|
328
|
+
this.db.then((db) => db.store(next));
|
|
329
|
+
this.broadcast({
|
|
330
|
+
action: 'store',
|
|
331
|
+
entry: next,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
128
334
|
}
|
|
129
335
|
/**
|
|
130
336
|
* Invalidates (removes) a cache entry.
|
|
@@ -132,6 +338,9 @@ class Cache {
|
|
|
132
338
|
* @param key - The key of the entry to invalidate.
|
|
133
339
|
*/
|
|
134
340
|
invalidate(key) {
|
|
341
|
+
this.invalidateInternal(key);
|
|
342
|
+
}
|
|
343
|
+
invalidateInternal(key, fromSync = false) {
|
|
135
344
|
const entry = this.getUntracked(key);
|
|
136
345
|
if (!entry)
|
|
137
346
|
return;
|
|
@@ -140,6 +349,10 @@ class Cache {
|
|
|
140
349
|
map.delete(key);
|
|
141
350
|
return map;
|
|
142
351
|
});
|
|
352
|
+
if (!fromSync) {
|
|
353
|
+
this.db.then((db) => db.remove(key));
|
|
354
|
+
this.broadcast({ action: 'invalidate', entry: { key } });
|
|
355
|
+
}
|
|
143
356
|
}
|
|
144
357
|
/** @internal */
|
|
145
358
|
cleanup() {
|
|
@@ -186,9 +399,80 @@ const CLIENT_CACHE_TOKEN = new InjectionToken('INTERNAL_CLIENT_CACHE');
|
|
|
186
399
|
* };
|
|
187
400
|
*/
|
|
188
401
|
function provideQueryCache(opt) {
|
|
402
|
+
const serialize = (value) => {
|
|
403
|
+
const headersRecord = {};
|
|
404
|
+
const headerKeys = value.headers.keys();
|
|
405
|
+
headerKeys.forEach((key) => {
|
|
406
|
+
const values = value.headers.getAll(key);
|
|
407
|
+
if (!values)
|
|
408
|
+
return;
|
|
409
|
+
headersRecord[key] = values;
|
|
410
|
+
});
|
|
411
|
+
return JSON.stringify({
|
|
412
|
+
body: value.body,
|
|
413
|
+
status: value.status,
|
|
414
|
+
statusText: value.statusText,
|
|
415
|
+
headers: headerKeys.length > 0 ? headersRecord : undefined,
|
|
416
|
+
url: value.url,
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
const deserialize = (value) => {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(value);
|
|
422
|
+
if (!parsed || typeof parsed !== 'object' || !('body' in parsed))
|
|
423
|
+
throw new Error('Invalid cache entry format');
|
|
424
|
+
const headers = parsed.headers
|
|
425
|
+
? new HttpHeaders(parsed.headers)
|
|
426
|
+
: undefined;
|
|
427
|
+
return new HttpResponse({
|
|
428
|
+
body: parsed.body,
|
|
429
|
+
status: parsed.status,
|
|
430
|
+
statusText: parsed.statusText,
|
|
431
|
+
headers: headers,
|
|
432
|
+
url: parsed.url,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
if (isDevMode())
|
|
437
|
+
console.error('Failed to deserialize cache entry:', err);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const syncTabsOpt = opt?.syncTabs
|
|
442
|
+
? {
|
|
443
|
+
id: 'mmstack-query-cache-sync',
|
|
444
|
+
serialize,
|
|
445
|
+
deserialize,
|
|
446
|
+
}
|
|
447
|
+
: undefined;
|
|
448
|
+
const db = opt?.persist === false
|
|
449
|
+
? undefined
|
|
450
|
+
: createSingleStoreDB('mmstack-query-cache-db', (version) => `query-store_v${version}`, opt?.version).then((db) => {
|
|
451
|
+
return {
|
|
452
|
+
getAll: () => {
|
|
453
|
+
return db.getAll().then((entries) => {
|
|
454
|
+
return entries
|
|
455
|
+
.map((entry) => {
|
|
456
|
+
const value = deserialize(entry.value);
|
|
457
|
+
if (value === null)
|
|
458
|
+
return null;
|
|
459
|
+
return {
|
|
460
|
+
...entry,
|
|
461
|
+
value,
|
|
462
|
+
};
|
|
463
|
+
})
|
|
464
|
+
.filter((e) => e !== null);
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
store: (entry) => {
|
|
468
|
+
return db.store({ ...entry, value: serialize(entry.value) });
|
|
469
|
+
},
|
|
470
|
+
remove: db.remove,
|
|
471
|
+
};
|
|
472
|
+
});
|
|
189
473
|
return {
|
|
190
474
|
provide: CLIENT_CACHE_TOKEN,
|
|
191
|
-
useValue: new Cache(opt?.ttl, opt?.staleTime, opt?.cleanup),
|
|
475
|
+
useValue: new Cache(opt?.ttl, opt?.staleTime, opt?.cleanup, syncTabsOpt, db),
|
|
192
476
|
};
|
|
193
477
|
}
|
|
194
478
|
class NoopCache extends Cache {
|
|
@@ -322,30 +606,25 @@ function parseCacheControlHeader(req) {
|
|
|
322
606
|
};
|
|
323
607
|
return directives;
|
|
324
608
|
}
|
|
325
|
-
function resolveTimings(cacheControl,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
ttl,
|
|
329
|
-
};
|
|
609
|
+
function resolveTimings(cacheControl, optStaleTime, optTTL) {
|
|
610
|
+
let staleTime = optStaleTime;
|
|
611
|
+
let ttl = optTTL;
|
|
330
612
|
if (cacheControl.immutable)
|
|
331
613
|
return {
|
|
332
614
|
staleTime: Infinity,
|
|
333
615
|
ttl: Infinity,
|
|
334
616
|
};
|
|
617
|
+
if (cacheControl.maxAge !== null)
|
|
618
|
+
ttl = cacheControl.maxAge * 1000;
|
|
619
|
+
if (cacheControl.staleWhileRevalidate !== null)
|
|
620
|
+
staleTime = cacheControl.staleWhileRevalidate * 1000;
|
|
335
621
|
// if no-cache is set, we must always revalidate
|
|
336
622
|
if (cacheControl.noCache || cacheControl.mustRevalidate)
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
if (cacheControl.maxAge !== null)
|
|
341
|
-
timings.ttl = cacheControl.maxAge * 1000;
|
|
342
|
-
// if stale-while-revalidate is set, we must revalidate after that time at the latest, but we can still serve the stale data
|
|
343
|
-
if (cacheControl.staleWhileRevalidate !== null) {
|
|
344
|
-
const ms = cacheControl.staleWhileRevalidate * 1000;
|
|
345
|
-
if (timings.staleTime === undefined || timings.staleTime > ms)
|
|
346
|
-
timings.staleTime = ms;
|
|
623
|
+
staleTime = 0;
|
|
624
|
+
if (ttl !== undefined && staleTime !== undefined && ttl < staleTime) {
|
|
625
|
+
staleTime = ttl;
|
|
347
626
|
}
|
|
348
|
-
return
|
|
627
|
+
return { staleTime, ttl };
|
|
349
628
|
}
|
|
350
629
|
/**
|
|
351
630
|
* Creates an `HttpInterceptorFn` that implements caching for HTTP requests. This interceptor
|
|
@@ -397,13 +676,31 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
397
676
|
if (lastModified) {
|
|
398
677
|
req = req.clone({ setHeaders: { 'If-Modified-Since': lastModified } });
|
|
399
678
|
}
|
|
679
|
+
if (opt.bustBrowserCache) {
|
|
680
|
+
req = req.clone({
|
|
681
|
+
setParams: { _cb: Date.now().toString() },
|
|
682
|
+
});
|
|
683
|
+
}
|
|
400
684
|
return next(req).pipe(tap((event) => {
|
|
401
685
|
if (event instanceof HttpResponse && event.ok) {
|
|
402
686
|
const cacheControl = parseCacheControlHeader(event);
|
|
403
|
-
if (cacheControl.noStore)
|
|
687
|
+
if (cacheControl.noStore && !opt.ignoreCacheControl)
|
|
404
688
|
return;
|
|
405
|
-
const { staleTime, ttl } =
|
|
406
|
-
|
|
689
|
+
const { staleTime, ttl } = opt.ignoreCacheControl
|
|
690
|
+
? opt
|
|
691
|
+
: resolveTimings(cacheControl, opt.staleTime, opt.ttl);
|
|
692
|
+
if (opt.ttl === 0)
|
|
693
|
+
return; // no point
|
|
694
|
+
const parsedResponse = opt.parse
|
|
695
|
+
? new HttpResponse({
|
|
696
|
+
body: opt.parse(event.body),
|
|
697
|
+
headers: event.headers,
|
|
698
|
+
status: event.status,
|
|
699
|
+
statusText: event.statusText,
|
|
700
|
+
url: event.url ?? undefined,
|
|
701
|
+
})
|
|
702
|
+
: event;
|
|
703
|
+
cache.store(key, parsedResponse, staleTime, ttl, opt.persist);
|
|
407
704
|
}
|
|
408
705
|
}), map((event) => {
|
|
409
706
|
// handle 304 responses due to eTag/last-modified
|
|
@@ -415,39 +712,82 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
415
712
|
};
|
|
416
713
|
}
|
|
417
714
|
|
|
715
|
+
function catchValueError(resource, fallback) {
|
|
716
|
+
return {
|
|
717
|
+
...resource,
|
|
718
|
+
value: toWritable(computed(() => {
|
|
719
|
+
try {
|
|
720
|
+
return resource.value();
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
return fallback;
|
|
724
|
+
}
|
|
725
|
+
}), (value) => resource.value.set(value)),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** @internal */
|
|
730
|
+
const DEFAULT_OPTIONS = {
|
|
731
|
+
treshold: 5,
|
|
732
|
+
timeout: 30000,
|
|
733
|
+
shouldFail: () => true,
|
|
734
|
+
shouldFailForever: () => false,
|
|
735
|
+
};
|
|
418
736
|
/** @internal */
|
|
419
|
-
function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000) {
|
|
737
|
+
function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldFail = () => true, shouldFailForever = () => false) {
|
|
420
738
|
const halfOpen = signal(false);
|
|
421
739
|
const failureCount = signal(0);
|
|
422
740
|
const status = computed(() => {
|
|
423
741
|
if (failureCount() >= treshold)
|
|
424
|
-
return '
|
|
425
|
-
return halfOpen() ? 'HALF_OPEN' : '
|
|
742
|
+
return 'OPEN';
|
|
743
|
+
return halfOpen() ? 'HALF_OPEN' : 'CLOSED';
|
|
426
744
|
});
|
|
427
|
-
const isClosed = computed(() => status()
|
|
745
|
+
const isClosed = computed(() => status() !== 'OPEN');
|
|
746
|
+
const isOpen = computed(() => status() !== 'CLOSED');
|
|
428
747
|
const success = () => {
|
|
429
748
|
failureCount.set(0);
|
|
430
749
|
halfOpen.set(false);
|
|
431
750
|
};
|
|
432
751
|
const tryOnce = () => {
|
|
433
|
-
if (!untracked(
|
|
752
|
+
if (!untracked(isOpen))
|
|
434
753
|
return;
|
|
435
754
|
halfOpen.set(true);
|
|
436
755
|
failureCount.set(treshold - 1);
|
|
437
756
|
};
|
|
757
|
+
let failForeverResetId = null;
|
|
438
758
|
const effectRef = effect((cleanup) => {
|
|
439
|
-
if (!
|
|
759
|
+
if (!isOpen())
|
|
440
760
|
return;
|
|
441
761
|
const timeout = setTimeout(tryOnce, resetTimeout);
|
|
442
|
-
|
|
762
|
+
failForeverResetId = timeout;
|
|
763
|
+
return cleanup(() => {
|
|
764
|
+
clearTimeout(timeout);
|
|
765
|
+
failForeverResetId = null;
|
|
766
|
+
});
|
|
443
767
|
});
|
|
444
|
-
const
|
|
768
|
+
const failInternal = () => {
|
|
445
769
|
failureCount.set(failureCount() + 1);
|
|
446
770
|
halfOpen.set(false);
|
|
447
771
|
};
|
|
772
|
+
const failForever = () => {
|
|
773
|
+
if (failForeverResetId)
|
|
774
|
+
clearTimeout(failForeverResetId);
|
|
775
|
+
effectRef.destroy();
|
|
776
|
+
failureCount.set(Infinity);
|
|
777
|
+
halfOpen.set(false);
|
|
778
|
+
return;
|
|
779
|
+
};
|
|
780
|
+
const fail = (err) => {
|
|
781
|
+
if (shouldFailForever(err))
|
|
782
|
+
return failForever();
|
|
783
|
+
if (shouldFail(err))
|
|
784
|
+
return failInternal();
|
|
785
|
+
// If the error does not trigger a failure, we do nothing.
|
|
786
|
+
};
|
|
448
787
|
return {
|
|
449
788
|
status,
|
|
450
789
|
isClosed,
|
|
790
|
+
isOpen,
|
|
451
791
|
fail,
|
|
452
792
|
success,
|
|
453
793
|
halfOpen: tryOnce,
|
|
@@ -457,8 +797,9 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000) {
|
|
|
457
797
|
/** @internal */
|
|
458
798
|
function createNeverBrokenCircuitBreaker() {
|
|
459
799
|
return {
|
|
460
|
-
isClosed: computed(() =>
|
|
461
|
-
|
|
800
|
+
isClosed: computed(() => true),
|
|
801
|
+
isOpen: computed(() => false),
|
|
802
|
+
status: signal('CLOSED'),
|
|
462
803
|
fail: () => {
|
|
463
804
|
// noop
|
|
464
805
|
},
|
|
@@ -473,6 +814,21 @@ function createNeverBrokenCircuitBreaker() {
|
|
|
473
814
|
},
|
|
474
815
|
};
|
|
475
816
|
}
|
|
817
|
+
const CB_DEFAULT_OPTIONS = new InjectionToken('MMSTACK_CIRCUIT_BREAKER_DEFAULT_OPTIONS');
|
|
818
|
+
function provideCircuitBreakerDefaultOptions(options) {
|
|
819
|
+
return {
|
|
820
|
+
provide: CB_DEFAULT_OPTIONS,
|
|
821
|
+
useValue: {
|
|
822
|
+
...DEFAULT_OPTIONS,
|
|
823
|
+
...options,
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function injectCircuitBreakerOptions(injector = inject(Injector)) {
|
|
828
|
+
return injector.get(CB_DEFAULT_OPTIONS, DEFAULT_OPTIONS, {
|
|
829
|
+
optional: true,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
476
832
|
/**
|
|
477
833
|
* Creates a circuit breaker instance.
|
|
478
834
|
*
|
|
@@ -496,12 +852,16 @@ function createNeverBrokenCircuitBreaker() {
|
|
|
496
852
|
* const resource1 = queryResource(..., { circuitBreaker: sharedBreaker });
|
|
497
853
|
* const resource2 = mutationResource(..., { circuitBreaker: sharedBreaker });
|
|
498
854
|
*/
|
|
499
|
-
function createCircuitBreaker(opt) {
|
|
855
|
+
function createCircuitBreaker(opt, injector) {
|
|
500
856
|
if (opt === false)
|
|
501
857
|
return createNeverBrokenCircuitBreaker();
|
|
502
858
|
if (typeof opt === 'object' && 'isClosed' in opt)
|
|
503
859
|
return opt;
|
|
504
|
-
|
|
860
|
+
const { treshold, timeout, shouldFail, shouldFailForever } = {
|
|
861
|
+
...injectCircuitBreakerOptions(injector),
|
|
862
|
+
...opt,
|
|
863
|
+
};
|
|
864
|
+
return internalCeateCircuitBreaker(treshold, timeout, shouldFail, shouldFailForever);
|
|
505
865
|
}
|
|
506
866
|
|
|
507
867
|
// Heavily inspired by: https://dev.to/kasual1/request-deduplication-in-angular-3pd8
|
|
@@ -571,6 +931,82 @@ function createDedupeRequestsInterceptor(allowed = ['GET', 'DELETE', 'HEAD', 'OP
|
|
|
571
931
|
};
|
|
572
932
|
}
|
|
573
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
|
+
if (value === null || typeof value !== 'object')
|
|
949
|
+
return false;
|
|
950
|
+
const proto = Object.getPrototypeOf(value);
|
|
951
|
+
if (proto === null)
|
|
952
|
+
return false; // remove Object.create(null);
|
|
953
|
+
return proto === Object.prototype;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Internal helper to generate a stable JSON string from an array.
|
|
957
|
+
* Sorts keys of plain objects within the array alphabetically before serialization
|
|
958
|
+
* to ensure hash stability regardless of key order.
|
|
959
|
+
*
|
|
960
|
+
* @param queryKey The array of values to serialize.
|
|
961
|
+
* @returns A stable JSON string representation.
|
|
962
|
+
* @internal
|
|
963
|
+
*/
|
|
964
|
+
function hashKey(queryKey) {
|
|
965
|
+
return JSON.stringify(queryKey, (_, val) => isPlainObject(val)
|
|
966
|
+
? Object.keys(val)
|
|
967
|
+
.toSorted()
|
|
968
|
+
.reduce((result, key) => {
|
|
969
|
+
result[key] = val[key];
|
|
970
|
+
return result;
|
|
971
|
+
}, {})
|
|
972
|
+
: val);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Generates a stable, unique string hash from one or more arguments.
|
|
976
|
+
* Useful for creating cache keys or identifiers where object key order shouldn't matter.
|
|
977
|
+
*
|
|
978
|
+
* How it works:
|
|
979
|
+
* - Plain objects within the arguments have their keys sorted alphabetically before hashing.
|
|
980
|
+
* This ensures that `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` produce the same hash.
|
|
981
|
+
* - Uses `JSON.stringify` internally with custom sorting for plain objects via `hashKey`.
|
|
982
|
+
* - Non-plain objects (arrays, Dates, etc.) and primitives are serialized naturally.
|
|
983
|
+
*
|
|
984
|
+
* @param {...unknown} args Values to include in the hash.
|
|
985
|
+
* @returns A stable string hash representing the input arguments.
|
|
986
|
+
* @example
|
|
987
|
+
* const userQuery = (id: number) => ['user', { id, timestamp: Date.now() }];
|
|
988
|
+
*
|
|
989
|
+
* const obj1 = { a: 1, b: 2 };
|
|
990
|
+
* const obj2 = { b: 2, a: 1 }; // Same keys/values, different order
|
|
991
|
+
*
|
|
992
|
+
* hash('posts', 10);
|
|
993
|
+
* // => '["posts",10]'
|
|
994
|
+
*
|
|
995
|
+
* hash('config', obj1);
|
|
996
|
+
* // => '["config",{"a":1,"b":2}]'
|
|
997
|
+
*
|
|
998
|
+
* hash('config', obj2);
|
|
999
|
+
* // => '["config",{"a":1,"b":2}]' (Same as above due to key sorting)
|
|
1000
|
+
*
|
|
1001
|
+
* hash(['todos', { status: 'done', owner: obj1 }]);
|
|
1002
|
+
* // => '[["todos",{"owner":{"a":1,"b":2},"status":"done"}]]'
|
|
1003
|
+
*
|
|
1004
|
+
* // Be mindful of values JSON.stringify cannot handle (functions, undefined, Symbols)
|
|
1005
|
+
* // hash('a', undefined, function() {}) => '["a",null,null]'
|
|
1006
|
+
*/
|
|
1007
|
+
function hash(...args) {
|
|
1008
|
+
return hashKey(args);
|
|
1009
|
+
}
|
|
574
1010
|
function equalTransferCache(a, b) {
|
|
575
1011
|
if (!a && !b)
|
|
576
1012
|
return true;
|
|
@@ -591,16 +1027,62 @@ function equalTransferCache(a, b) {
|
|
|
591
1027
|
const aSet = new Set(a.includeHeaders ?? []);
|
|
592
1028
|
return b.includeHeaders.every((header) => aSet.has(header));
|
|
593
1029
|
}
|
|
1030
|
+
function equalParamArray(a, b) {
|
|
1031
|
+
if (!a && !b)
|
|
1032
|
+
return true;
|
|
1033
|
+
if (!a || !b)
|
|
1034
|
+
return false;
|
|
1035
|
+
if (a.length !== b.length)
|
|
1036
|
+
return false;
|
|
1037
|
+
return a.every((value) => b.includes(value));
|
|
1038
|
+
}
|
|
1039
|
+
function headersToObject(headerClass) {
|
|
1040
|
+
const headers = {};
|
|
1041
|
+
headerClass.keys().forEach((key) => {
|
|
1042
|
+
const value = headerClass.getAll(key);
|
|
1043
|
+
if (value === null)
|
|
1044
|
+
return;
|
|
1045
|
+
if (value.length === 1) {
|
|
1046
|
+
headers[key] = value[0];
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
headers[key] = value;
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
return headers;
|
|
1053
|
+
}
|
|
1054
|
+
function paramToObject(paramsClass) {
|
|
1055
|
+
const params = {};
|
|
1056
|
+
paramsClass.keys().forEach((key) => {
|
|
1057
|
+
const value = paramsClass.getAll(key);
|
|
1058
|
+
if (value === null)
|
|
1059
|
+
return;
|
|
1060
|
+
if (value.length === 1) {
|
|
1061
|
+
params[key] = value[0];
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
params[key] = value;
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
return params;
|
|
1068
|
+
}
|
|
594
1069
|
function equalParams(a, b) {
|
|
595
1070
|
if (!a && !b)
|
|
596
1071
|
return true;
|
|
597
1072
|
if (!a || !b)
|
|
598
1073
|
return false;
|
|
599
|
-
const
|
|
600
|
-
const
|
|
1074
|
+
const aObj = a instanceof HttpParams ? paramToObject(a) : a;
|
|
1075
|
+
const bObj = b instanceof HttpParams ? paramToObject(b) : b;
|
|
1076
|
+
const aKeys = Object.keys(aObj);
|
|
1077
|
+
const bKeys = Object.keys(bObj);
|
|
601
1078
|
if (aKeys.length !== bKeys.length)
|
|
602
1079
|
return false;
|
|
603
|
-
return aKeys.every((key) =>
|
|
1080
|
+
return aKeys.every((key) => {
|
|
1081
|
+
if (Array.isArray(aObj[key]) || Array.isArray(bObj[key])) {
|
|
1082
|
+
return equalParamArray(Array.isArray(aObj[key]) ? aObj[key] : [aObj[key]], Array.isArray(bObj[key]) ? bObj[key] : [bObj[key]]);
|
|
1083
|
+
}
|
|
1084
|
+
return aObj[key] === bObj[key];
|
|
1085
|
+
});
|
|
604
1086
|
}
|
|
605
1087
|
function equalBody(a, b) {
|
|
606
1088
|
if (!a && !b)
|
|
@@ -614,22 +1096,44 @@ function equalHeaders(a, b) {
|
|
|
614
1096
|
return true;
|
|
615
1097
|
if (!a || !b)
|
|
616
1098
|
return false;
|
|
617
|
-
const
|
|
618
|
-
const
|
|
1099
|
+
const aObj = a instanceof HttpHeaders ? headersToObject(a) : a;
|
|
1100
|
+
const bObj = b instanceof HttpHeaders ? headersToObject(b) : b;
|
|
1101
|
+
const aKeys = Object.keys(aObj);
|
|
1102
|
+
const bKeys = Object.keys(bObj);
|
|
619
1103
|
if (aKeys.length !== bKeys.length)
|
|
620
1104
|
return false;
|
|
621
|
-
return aKeys.every((key) =>
|
|
1105
|
+
return aKeys.every((key) => {
|
|
1106
|
+
if (Array.isArray(aObj[key]) || Array.isArray(bObj[key])) {
|
|
1107
|
+
return equalParamArray(Array.isArray(aObj[key]) ? aObj[key] : [aObj[key]], Array.isArray(bObj[key]) ? bObj[key] : [bObj[key]]);
|
|
1108
|
+
}
|
|
1109
|
+
return aObj[key] === bObj[key];
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
function toHttpContextEntries(ctx) {
|
|
1113
|
+
if (!ctx)
|
|
1114
|
+
return [];
|
|
1115
|
+
if (ctx instanceof HttpContext) {
|
|
1116
|
+
const tokens = Array.from(ctx.keys());
|
|
1117
|
+
return tokens.map((key) => [key.toString(), ctx.get(key)]);
|
|
1118
|
+
}
|
|
1119
|
+
if (typeof ctx === 'object') {
|
|
1120
|
+
return Object.entries(ctx);
|
|
1121
|
+
}
|
|
1122
|
+
return [];
|
|
622
1123
|
}
|
|
623
1124
|
function equalContext(a, b) {
|
|
624
1125
|
if (!a && !b)
|
|
625
1126
|
return true;
|
|
626
1127
|
if (!a || !b)
|
|
627
1128
|
return false;
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
if (
|
|
1129
|
+
const aEntries = toHttpContextEntries(a);
|
|
1130
|
+
const bEntries = toHttpContextEntries(b);
|
|
1131
|
+
if (aEntries.length !== bEntries.length)
|
|
631
1132
|
return false;
|
|
632
|
-
|
|
1133
|
+
if (aEntries.length === 0)
|
|
1134
|
+
return true;
|
|
1135
|
+
const bMap = new Map(bEntries);
|
|
1136
|
+
return aEntries.every(([key, value]) => value === bMap.get(key));
|
|
633
1137
|
}
|
|
634
1138
|
function createEqualRequest(equalResult) {
|
|
635
1139
|
const eqb = equalResult ?? equalBody;
|
|
@@ -672,38 +1176,33 @@ function hasSlowConnection() {
|
|
|
672
1176
|
return false;
|
|
673
1177
|
}
|
|
674
1178
|
|
|
675
|
-
function
|
|
1179
|
+
function persist(src, equal) {
|
|
676
1180
|
// linkedSignal allows us to access previous source value
|
|
677
1181
|
const persisted = linkedSignal({
|
|
678
|
-
source: () =>
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
usePrevious: usePrevious(),
|
|
682
|
-
};
|
|
683
|
-
},
|
|
684
|
-
computation: (source, prev) => {
|
|
685
|
-
if (source.usePrevious && prev)
|
|
1182
|
+
source: () => src(),
|
|
1183
|
+
computation: (next, prev) => {
|
|
1184
|
+
if (next === undefined && prev !== undefined)
|
|
686
1185
|
return prev.value;
|
|
687
|
-
return
|
|
1186
|
+
return next;
|
|
688
1187
|
},
|
|
689
1188
|
equal,
|
|
690
1189
|
});
|
|
691
1190
|
// if original value was WritableSignal then override linkedSignal methods to original...angular uses linkedSignal under the hood in ResourceImpl, this applies to that.
|
|
692
|
-
if ('set' in
|
|
693
|
-
persisted.set =
|
|
694
|
-
persisted.update =
|
|
695
|
-
persisted.asReadonly =
|
|
1191
|
+
if ('set' in src) {
|
|
1192
|
+
persisted.set = src.set;
|
|
1193
|
+
persisted.update = src.update;
|
|
1194
|
+
persisted.asReadonly = src.asReadonly;
|
|
696
1195
|
}
|
|
697
1196
|
return persisted;
|
|
698
1197
|
}
|
|
699
|
-
function persistResourceValues(resource,
|
|
700
|
-
if (!
|
|
1198
|
+
function persistResourceValues(resource, shouldPersist = false, equal) {
|
|
1199
|
+
if (!shouldPersist)
|
|
701
1200
|
return resource;
|
|
702
1201
|
return {
|
|
703
1202
|
...resource,
|
|
704
|
-
statusCode:
|
|
705
|
-
headers:
|
|
706
|
-
value:
|
|
1203
|
+
statusCode: persist(resource.statusCode),
|
|
1204
|
+
headers: persist(resource.headers),
|
|
1205
|
+
value: persist(resource.value, equal),
|
|
707
1206
|
};
|
|
708
1207
|
}
|
|
709
1208
|
|
|
@@ -770,11 +1269,44 @@ function retryOnError(res, opt) {
|
|
|
770
1269
|
};
|
|
771
1270
|
}
|
|
772
1271
|
|
|
1272
|
+
class ResourceSensors {
|
|
1273
|
+
networkStatus = sensor('networkStatus');
|
|
1274
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ResourceSensors, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1275
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ResourceSensors, providedIn: 'root' });
|
|
1276
|
+
}
|
|
1277
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ResourceSensors, decorators: [{
|
|
1278
|
+
type: Injectable,
|
|
1279
|
+
args: [{
|
|
1280
|
+
providedIn: 'root',
|
|
1281
|
+
}]
|
|
1282
|
+
}] });
|
|
1283
|
+
function injectNetworkStatus() {
|
|
1284
|
+
return inject(ResourceSensors).networkStatus;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function toResourceObject(res) {
|
|
1288
|
+
return {
|
|
1289
|
+
asReadonly: () => res.asReadonly(),
|
|
1290
|
+
destroy: () => res.destroy(),
|
|
1291
|
+
error: res.error,
|
|
1292
|
+
headers: res.headers,
|
|
1293
|
+
isLoading: res.isLoading,
|
|
1294
|
+
progress: res.progress,
|
|
1295
|
+
status: res.status,
|
|
1296
|
+
statusCode: res.statusCode,
|
|
1297
|
+
value: res.value,
|
|
1298
|
+
reload: () => res.reload(),
|
|
1299
|
+
hasValue: (() => res.hasValue()),
|
|
1300
|
+
set: (v) => res.set(v),
|
|
1301
|
+
update: (v) => res.update(v),
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
773
1305
|
function normalizeParams(params) {
|
|
774
1306
|
if (params instanceof HttpParams)
|
|
775
1307
|
return params.toString();
|
|
776
1308
|
const paramMap = new Map();
|
|
777
|
-
for (const [key, value] of entries(params)) {
|
|
1309
|
+
for (const [key, value] of Object.entries(params)) {
|
|
778
1310
|
if (Array.isArray(value)) {
|
|
779
1311
|
paramMap.set(key, value.map(encodeURIComponent).join(','));
|
|
780
1312
|
}
|
|
@@ -783,6 +1315,7 @@ function normalizeParams(params) {
|
|
|
783
1315
|
}
|
|
784
1316
|
}
|
|
785
1317
|
return Array.from(paramMap.entries())
|
|
1318
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
786
1319
|
.map(([key, value]) => `${key}=${value}`)
|
|
787
1320
|
.join('&');
|
|
788
1321
|
}
|
|
@@ -799,13 +1332,26 @@ function queryResource(request, options) {
|
|
|
799
1332
|
: inject(DestroyRef);
|
|
800
1333
|
const cb = createCircuitBreaker(options?.circuitBreaker === true
|
|
801
1334
|
? undefined
|
|
802
|
-
: (options?.circuitBreaker ?? false));
|
|
1335
|
+
: (options?.circuitBreaker ?? false), options?.injector);
|
|
1336
|
+
const networkAvailable = injectNetworkStatus();
|
|
1337
|
+
const eq = options?.triggerOnSameRequest
|
|
1338
|
+
? undefined
|
|
1339
|
+
: createEqualRequest(options?.equal);
|
|
803
1340
|
const stableRequest = computed(() => {
|
|
804
|
-
if (cb.
|
|
1341
|
+
if (!networkAvailable() || cb.isOpen())
|
|
1342
|
+
return undefined;
|
|
1343
|
+
const req = request();
|
|
1344
|
+
if (!req)
|
|
805
1345
|
return undefined;
|
|
806
|
-
|
|
1346
|
+
if (typeof req === 'string')
|
|
1347
|
+
return { method: 'GET', url: req };
|
|
1348
|
+
return req;
|
|
807
1349
|
}, {
|
|
808
|
-
equal:
|
|
1350
|
+
equal: (a, b) => {
|
|
1351
|
+
if (eq)
|
|
1352
|
+
return eq(a, b);
|
|
1353
|
+
return a === b;
|
|
1354
|
+
},
|
|
809
1355
|
});
|
|
810
1356
|
const hashFn = typeof options?.cache === 'object'
|
|
811
1357
|
? (options.cache.hash ?? urlWithParams)
|
|
@@ -818,6 +1364,11 @@ function queryResource(request, options) {
|
|
|
818
1364
|
return null;
|
|
819
1365
|
return hashFn(r);
|
|
820
1366
|
});
|
|
1367
|
+
const bustBrowserCache = typeof options?.cache === 'object' &&
|
|
1368
|
+
options.cache.bustBrowserCache === true;
|
|
1369
|
+
const ignoreCacheControl = typeof options?.cache === 'object' &&
|
|
1370
|
+
options.cache.ignoreCacheControl === true;
|
|
1371
|
+
const persist = typeof options?.cache === 'object' && options.cache.persist === true;
|
|
821
1372
|
const cachedRequest = options?.cache
|
|
822
1373
|
? computed(() => {
|
|
823
1374
|
const r = stableRequest();
|
|
@@ -829,31 +1380,39 @@ function queryResource(request, options) {
|
|
|
829
1380
|
staleTime,
|
|
830
1381
|
ttl,
|
|
831
1382
|
key: cacheKey() ?? hashFn(r),
|
|
1383
|
+
bustBrowserCache,
|
|
1384
|
+
ignoreCacheControl,
|
|
1385
|
+
persist,
|
|
832
1386
|
}),
|
|
833
1387
|
};
|
|
834
1388
|
})
|
|
835
1389
|
: stableRequest;
|
|
836
|
-
let resource = httpResource(cachedRequest, {
|
|
1390
|
+
let resource = toResourceObject(httpResource(cachedRequest, {
|
|
837
1391
|
...options,
|
|
838
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
839
1392
|
parse: options?.parse, // Not my favorite thing to do, but here it is completely safe.
|
|
840
|
-
});
|
|
1393
|
+
}));
|
|
1394
|
+
resource = catchValueError(resource, options?.defaultValue);
|
|
841
1395
|
// get full HttpResonse from Cache
|
|
842
|
-
const cachedEvent = cache.
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
source: () => actualCacheValue(),
|
|
853
|
-
computation: (source, prev) => {
|
|
854
|
-
if (!source && prev)
|
|
1396
|
+
const cachedEvent = cache.getEntryOrKey(cacheKey);
|
|
1397
|
+
const cacheEntry = linkedSignal({
|
|
1398
|
+
source: () => cachedEvent(),
|
|
1399
|
+
computation: (entry, prev) => {
|
|
1400
|
+
if (!entry)
|
|
1401
|
+
return null;
|
|
1402
|
+
if (typeof entry === 'string' &&
|
|
1403
|
+
prev &&
|
|
1404
|
+
prev.value !== null &&
|
|
1405
|
+
prev.value.key === entry) {
|
|
855
1406
|
return prev.value;
|
|
856
|
-
|
|
1407
|
+
}
|
|
1408
|
+
if (typeof entry === 'string')
|
|
1409
|
+
return { key: entry, value: null };
|
|
1410
|
+
if (!(entry.value instanceof HttpResponse))
|
|
1411
|
+
return { key: entry.key, value: null };
|
|
1412
|
+
return {
|
|
1413
|
+
value: entry.value.body,
|
|
1414
|
+
key: entry.key,
|
|
1415
|
+
};
|
|
857
1416
|
},
|
|
858
1417
|
});
|
|
859
1418
|
resource = refresh(resource, destroyRef, options?.refresh);
|
|
@@ -861,7 +1420,8 @@ function queryResource(request, options) {
|
|
|
861
1420
|
resource = persistResourceValues(resource, options?.keepPrevious, options?.equal);
|
|
862
1421
|
const value = options?.cache
|
|
863
1422
|
? toWritable(computed(() => {
|
|
864
|
-
|
|
1423
|
+
resource.value();
|
|
1424
|
+
return cacheEntry()?.value ?? resource.value();
|
|
865
1425
|
}), resource.value.set, resource.value.update)
|
|
866
1426
|
: resource.value;
|
|
867
1427
|
const onError = options?.onError; // Put in own variable to ensure value remains even if options are somehow mutated in-line
|
|
@@ -882,7 +1442,7 @@ function queryResource(request, options) {
|
|
|
882
1442
|
const cbEffectRef = effect(() => {
|
|
883
1443
|
const status = resource.status();
|
|
884
1444
|
if (status === ResourceStatus.Error)
|
|
885
|
-
cb.fail();
|
|
1445
|
+
cb.fail(untracked(resource.error));
|
|
886
1446
|
else if (status === ResourceStatus.Resolved)
|
|
887
1447
|
cb.success();
|
|
888
1448
|
});
|
|
@@ -894,7 +1454,7 @@ function queryResource(request, options) {
|
|
|
894
1454
|
body: value,
|
|
895
1455
|
status: 200,
|
|
896
1456
|
statusText: 'OK',
|
|
897
|
-
}));
|
|
1457
|
+
}), staleTime, ttl, persist);
|
|
898
1458
|
};
|
|
899
1459
|
const update = (updater) => {
|
|
900
1460
|
set(updater(untracked(resource.value)));
|
|
@@ -907,7 +1467,9 @@ function queryResource(request, options) {
|
|
|
907
1467
|
value,
|
|
908
1468
|
set,
|
|
909
1469
|
update,
|
|
910
|
-
|
|
1470
|
+
statusCode: linkedSignal(resource.statusCode),
|
|
1471
|
+
headers: linkedSignal(resource.headers),
|
|
1472
|
+
disabled: computed(() => cb.isOpen() || stableRequest() === undefined),
|
|
911
1473
|
reload: () => {
|
|
912
1474
|
cb.halfOpen(); // open the circuit for manual reload
|
|
913
1475
|
return resource.reload();
|
|
@@ -920,16 +1482,35 @@ function queryResource(request, options) {
|
|
|
920
1482
|
prefetch: async (partial) => {
|
|
921
1483
|
if (!options?.cache || hasSlowConnection())
|
|
922
1484
|
return Promise.resolve();
|
|
923
|
-
const request = untracked(
|
|
924
|
-
|
|
925
|
-
return Promise.resolve();
|
|
1485
|
+
const request = untracked(stableRequest);
|
|
1486
|
+
const partialReq = typeof partial === 'string' ? { method: 'GET', url: partial } : partial;
|
|
926
1487
|
const prefetchRequest = {
|
|
927
1488
|
...request,
|
|
928
|
-
...
|
|
1489
|
+
...partialReq,
|
|
929
1490
|
};
|
|
1491
|
+
if (!prefetchRequest.url)
|
|
1492
|
+
return Promise.resolve();
|
|
1493
|
+
const key = hashFn({
|
|
1494
|
+
...prefetchRequest,
|
|
1495
|
+
url: prefetchRequest.url ?? '',
|
|
1496
|
+
});
|
|
1497
|
+
const found = cache.getUntracked(key);
|
|
1498
|
+
if (found && !found.isStale)
|
|
1499
|
+
return Promise.resolve();
|
|
930
1500
|
try {
|
|
931
1501
|
await firstValueFrom(client.request(prefetchRequest.method ?? 'GET', prefetchRequest.url, {
|
|
932
1502
|
...prefetchRequest,
|
|
1503
|
+
context: setCacheContext(prefetchRequest.context, {
|
|
1504
|
+
staleTime,
|
|
1505
|
+
ttl,
|
|
1506
|
+
key: hashFn({
|
|
1507
|
+
...prefetchRequest,
|
|
1508
|
+
url: prefetchRequest.url ?? '',
|
|
1509
|
+
}),
|
|
1510
|
+
bustBrowserCache,
|
|
1511
|
+
ignoreCacheControl,
|
|
1512
|
+
persist,
|
|
1513
|
+
}),
|
|
933
1514
|
headers: prefetchRequest.headers,
|
|
934
1515
|
observe: 'response',
|
|
935
1516
|
}));
|
|
@@ -944,6 +1525,46 @@ function queryResource(request, options) {
|
|
|
944
1525
|
};
|
|
945
1526
|
}
|
|
946
1527
|
|
|
1528
|
+
function manualQueryResource(request, options) {
|
|
1529
|
+
const trigger = signal({ epoch: 0 }, {
|
|
1530
|
+
equal: (a, b) => a.epoch === b.epoch,
|
|
1531
|
+
});
|
|
1532
|
+
const injector = options?.injector ?? inject(Injector);
|
|
1533
|
+
const req = computed(() => {
|
|
1534
|
+
const state = trigger();
|
|
1535
|
+
if (state.epoch === 0)
|
|
1536
|
+
return;
|
|
1537
|
+
if (state.override)
|
|
1538
|
+
return state.override;
|
|
1539
|
+
return untracked(request);
|
|
1540
|
+
}, {
|
|
1541
|
+
equal: () => false,
|
|
1542
|
+
});
|
|
1543
|
+
const resource = queryResource(req, options);
|
|
1544
|
+
return {
|
|
1545
|
+
...resource,
|
|
1546
|
+
trigger: (override, injectorOverride) => {
|
|
1547
|
+
trigger.update((s) => ({
|
|
1548
|
+
epoch: s.epoch + 1,
|
|
1549
|
+
override,
|
|
1550
|
+
}));
|
|
1551
|
+
return new Promise((res, rej) => {
|
|
1552
|
+
const watcher = nestedEffect(() => {
|
|
1553
|
+
const status = resource.status();
|
|
1554
|
+
if (status === ResourceStatus.Resolved) {
|
|
1555
|
+
watcher.destroy();
|
|
1556
|
+
res(untracked(resource.value));
|
|
1557
|
+
}
|
|
1558
|
+
else if (status === ResourceStatus.Error) {
|
|
1559
|
+
watcher.destroy();
|
|
1560
|
+
rej(untracked(resource.error));
|
|
1561
|
+
}
|
|
1562
|
+
}, { injector: injectorOverride ?? injector });
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
947
1568
|
/**
|
|
948
1569
|
* Creates a resource for performing mutations (e.g., POST, PUT, PATCH, DELETE requests).
|
|
949
1570
|
* Unlike `queryResource`, `mutationResource` is designed for one-off operations that change data.
|
|
@@ -951,96 +1572,124 @@ function queryResource(request, options) {
|
|
|
951
1572
|
* managing the mutation lifecycle (pending, error, success) and provides callbacks for handling
|
|
952
1573
|
* these states.
|
|
953
1574
|
*
|
|
954
|
-
* @param request A function that returns the base `HttpResourceRequest` to be made.
|
|
955
|
-
* function is called reactively. Unlike `queryResource`, the `body` property
|
|
956
|
-
* of the request is provided when `mutate` is called, *not* here. If the
|
|
957
|
-
* function returns `undefined`, the mutation is considered "disabled." All properties,
|
|
958
|
-
* except the body, can be set here.
|
|
1575
|
+
* @param request A function that returns the base `HttpResourceRequest` to be made. This function is called reactively. The parameter is the mutation value provided by the `mutate` method.
|
|
959
1576
|
* @param options Configuration options for the mutation resource. This includes callbacks
|
|
960
1577
|
* for `onMutate`, `onError`, `onSuccess`, and `onSettled`.
|
|
961
1578
|
* @typeParam TResult - The type of the expected result from the mutation.
|
|
962
1579
|
* @typeParam TRaw - The raw response type from the HTTP request (defaults to TResult).
|
|
1580
|
+
* @typeParam TMutation - The type of the mutation value (the request body).
|
|
1581
|
+
* @typeParam TICTX - The type of the initial context value passed to `onMutate`.
|
|
963
1582
|
* @typeParam TCTX - The type of the context value returned by `onMutate`.
|
|
1583
|
+
* @typeParam TMethod - The HTTP method to be used for the mutation (defaults to `HttpResourceRequest['method']`).
|
|
964
1584
|
* @returns A `MutationResourceRef` instance, which provides methods for triggering the mutation
|
|
965
1585
|
* and observing its status.
|
|
966
1586
|
*/
|
|
967
1587
|
function mutationResource(request, options = {}) {
|
|
968
1588
|
const { onMutate, onError, onSuccess, onSettled, equal, ...rest } = options;
|
|
969
1589
|
const requestEqual = createEqualRequest(equal);
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
});
|
|
973
|
-
const nextRequest = signal(null, {
|
|
1590
|
+
const eq = equal ?? Object.is;
|
|
1591
|
+
const next = signal(null, {
|
|
974
1592
|
equal: (a, b) => {
|
|
975
1593
|
if (!a && !b)
|
|
976
1594
|
return true;
|
|
977
1595
|
if (!a || !b)
|
|
978
1596
|
return false;
|
|
979
|
-
return
|
|
1597
|
+
return eq(a, b);
|
|
980
1598
|
},
|
|
981
1599
|
});
|
|
1600
|
+
const queue = signal([]);
|
|
1601
|
+
let ctx = undefined;
|
|
1602
|
+
const queueRef = effect(() => {
|
|
1603
|
+
const nextInQueue = queue().at(0);
|
|
1604
|
+
if (!nextInQueue || next() !== null)
|
|
1605
|
+
return;
|
|
1606
|
+
queue.update((q) => q.slice(1));
|
|
1607
|
+
const [value, ictx] = nextInQueue;
|
|
1608
|
+
ctx = onMutate?.(value, ictx);
|
|
1609
|
+
next.set(value);
|
|
1610
|
+
});
|
|
982
1611
|
const req = computed(() => {
|
|
983
|
-
const nr =
|
|
1612
|
+
const nr = next();
|
|
984
1613
|
if (!nr)
|
|
985
1614
|
return;
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1615
|
+
return request(nr) ?? undefined;
|
|
1616
|
+
}, {
|
|
1617
|
+
equal: requestEqual,
|
|
1618
|
+
});
|
|
1619
|
+
const lastValue = linkedSignal({
|
|
1620
|
+
source: next,
|
|
1621
|
+
computation: (next, prev) => {
|
|
1622
|
+
if (next === null && !!prev)
|
|
1623
|
+
return prev.value;
|
|
1624
|
+
return next;
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1627
|
+
const lastValueRequest = computed(() => {
|
|
1628
|
+
const nr = lastValue();
|
|
1629
|
+
if (!nr)
|
|
989
1630
|
return;
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
...nr,
|
|
994
|
-
url,
|
|
995
|
-
method,
|
|
996
|
-
};
|
|
1631
|
+
return request(nr) ?? undefined;
|
|
1632
|
+
}, {
|
|
1633
|
+
equal: requestEqual,
|
|
997
1634
|
});
|
|
1635
|
+
const cb = createCircuitBreaker(options?.circuitBreaker === true
|
|
1636
|
+
? undefined
|
|
1637
|
+
: (options?.circuitBreaker ?? false), options?.injector);
|
|
998
1638
|
const resource = queryResource(req, {
|
|
999
1639
|
...rest,
|
|
1640
|
+
circuitBreaker: cb,
|
|
1000
1641
|
defaultValue: null, // doesnt matter since .value is not accessible
|
|
1001
1642
|
});
|
|
1002
|
-
let ctx = undefined;
|
|
1003
1643
|
const destroyRef = options.injector
|
|
1004
1644
|
? options.injector.get(DestroyRef)
|
|
1005
1645
|
: inject(DestroyRef);
|
|
1006
1646
|
const error$ = toObservable(resource.error);
|
|
1007
|
-
const value$ = toObservable(resource.value);
|
|
1647
|
+
const value$ = toObservable(resource.value).pipe(catchError(() => of(null)));
|
|
1008
1648
|
const statusSub = toObservable(resource.status)
|
|
1009
1649
|
.pipe(combineLatestWith(error$, value$), map(([status, error, value]) => {
|
|
1010
1650
|
if (status === ResourceStatus.Error && error) {
|
|
1011
1651
|
return {
|
|
1012
|
-
status:
|
|
1652
|
+
status: 'error',
|
|
1013
1653
|
error,
|
|
1014
1654
|
};
|
|
1015
1655
|
}
|
|
1016
1656
|
if (status === ResourceStatus.Resolved && value !== null) {
|
|
1017
1657
|
return {
|
|
1018
|
-
status:
|
|
1658
|
+
status: 'resolved',
|
|
1019
1659
|
value,
|
|
1020
1660
|
};
|
|
1021
1661
|
}
|
|
1022
1662
|
return null;
|
|
1023
1663
|
}), filter((v) => v !== null), takeUntilDestroyed(destroyRef))
|
|
1024
1664
|
.subscribe((result) => {
|
|
1025
|
-
if (result.status ===
|
|
1665
|
+
if (result.status === 'error')
|
|
1026
1666
|
onError?.(result.error, ctx);
|
|
1027
1667
|
else
|
|
1028
1668
|
onSuccess?.(result.value, ctx);
|
|
1029
1669
|
onSettled?.(ctx);
|
|
1030
1670
|
ctx = undefined;
|
|
1031
|
-
|
|
1671
|
+
next.set(null);
|
|
1032
1672
|
});
|
|
1673
|
+
const shouldQueue = options.queue ?? false;
|
|
1033
1674
|
return {
|
|
1034
1675
|
...resource,
|
|
1035
1676
|
destroy: () => {
|
|
1036
1677
|
statusSub.unsubscribe();
|
|
1037
1678
|
resource.destroy();
|
|
1679
|
+
queueRef.destroy();
|
|
1038
1680
|
},
|
|
1039
1681
|
mutate: (value, ictx) => {
|
|
1040
|
-
|
|
1041
|
-
|
|
1682
|
+
if (shouldQueue) {
|
|
1683
|
+
return queue.update((q) => [...q, [value, ictx]]);
|
|
1684
|
+
}
|
|
1685
|
+
else {
|
|
1686
|
+
ctx = onMutate?.(value, ictx);
|
|
1687
|
+
next.set(value);
|
|
1688
|
+
}
|
|
1042
1689
|
},
|
|
1043
|
-
current:
|
|
1690
|
+
current: next,
|
|
1691
|
+
// redeclare disabled with last value so that it is not affected by the resource's internal disablement logic
|
|
1692
|
+
disabled: computed(() => cb.isOpen() || lastValueRequest() === undefined),
|
|
1044
1693
|
};
|
|
1045
1694
|
}
|
|
1046
1695
|
|
|
@@ -1048,5 +1697,5 @@ function mutationResource(request, options = {}) {
|
|
|
1048
1697
|
* Generated bundle index. Do not edit.
|
|
1049
1698
|
*/
|
|
1050
1699
|
|
|
1051
|
-
export { Cache, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, injectQueryCache, mutationResource, noDedupe, provideQueryCache, queryResource };
|
|
1700
|
+
export { Cache, createCacheInterceptor, createCircuitBreaker, createDedupeRequestsInterceptor, injectQueryCache, manualQueryResource, mutationResource, noDedupe, provideCircuitBreakerDefaultOptions, provideQueryCache, queryResource };
|
|
1052
1701
|
//# sourceMappingURL=mmstack-resource.mjs.map
|