@mmstack/resource 20.2.6 → 20.2.8
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 +187 -45
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/index.d.ts +597 -1
- package/package.json +4 -4
- package/lib/mutation-resource.d.ts +0 -88
- package/lib/public_api.d.ts +0 -3
- package/lib/query-resource.d.ts +0 -119
- package/lib/util/cache/cache.d.ts +0 -188
- package/lib/util/cache/cache.interceptor.d.ts +0 -42
- package/lib/util/cache/index.d.ts +0 -2
- package/lib/util/cache/public_api.d.ts +0 -2
- package/lib/util/catch-value-error.d.ts +0 -2
- package/lib/util/circuit-breaker.d.ts +0 -103
- package/lib/util/dedupe.interceptor.d.ts +0 -50
- package/lib/util/equality.d.ts +0 -3
- package/lib/util/has-slow-connection.d.ts +0 -1
- package/lib/util/index.d.ts +0 -11
- package/lib/util/persist.d.ts +0 -3
- package/lib/util/public_api.d.ts +0 -3
- package/lib/util/refresh.d.ts +0 -3
- package/lib/util/retry-on-error.d.ts +0 -6
- package/lib/util/to-resource-object.d.ts +0 -2
- package/lib/util/url-with-params.d.ts +0 -2
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { computed, untracked, InjectionToken,
|
|
1
|
+
import { computed, untracked, InjectionToken, isDevMode, inject, signal, effect, Injector, linkedSignal, DestroyRef } from '@angular/core';
|
|
2
2
|
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
3
3
|
import { of, tap, map, finalize, shareReplay, interval, firstValueFrom, catchError, combineLatestWith, filter } from 'rxjs';
|
|
4
|
-
import {
|
|
4
|
+
import { HttpHeaders, HttpResponse, HttpContextToken, HttpContext, HttpParams, httpResource, HttpClient } from '@angular/common/http';
|
|
5
5
|
import { mutable, toWritable } from '@mmstack/primitives';
|
|
6
6
|
import { v7 } from 'uuid';
|
|
7
7
|
import { keys, hash, entries } from '@mmstack/object';
|
|
8
8
|
|
|
9
|
+
function isSyncMessage(msg) {
|
|
10
|
+
return (typeof msg === 'object' &&
|
|
11
|
+
msg !== null &&
|
|
12
|
+
'type' in msg &&
|
|
13
|
+
msg.type === 'cache-sync-message');
|
|
14
|
+
}
|
|
9
15
|
const ONE_DAY = 1000 * 60 * 60 * 24;
|
|
10
16
|
const ONE_HOUR = 1000 * 60 * 60;
|
|
11
17
|
const DEFAULT_CLEANUP_OPT = {
|
|
@@ -23,6 +29,14 @@ class Cache {
|
|
|
23
29
|
staleTime;
|
|
24
30
|
internal = mutable(new Map());
|
|
25
31
|
cleanupOpt;
|
|
32
|
+
/**
|
|
33
|
+
* Destroys the cache instance, cleaning up any resources used by the cache.
|
|
34
|
+
* This method is called automatically when the cache instance is garbage collected.
|
|
35
|
+
*/
|
|
36
|
+
destroy;
|
|
37
|
+
broadcast = () => {
|
|
38
|
+
// noop
|
|
39
|
+
};
|
|
26
40
|
/**
|
|
27
41
|
* Creates a new `Cache` instance.
|
|
28
42
|
*
|
|
@@ -31,12 +45,14 @@ class Cache {
|
|
|
31
45
|
* stale but can still be used while revalidation occurs in the background. Defaults to 1 hour.
|
|
32
46
|
* @param cleanupOpt - Options for configuring the cache cleanup strategy. Defaults to LRU with a
|
|
33
47
|
* `maxSize` of 200 and a `checkInterval` of one hour.
|
|
48
|
+
* @param syncTabs - If provided, the cache will use the options a BroadcastChannel to send updates between tabs.
|
|
49
|
+
* Defaults to `undefined`, meaning no synchronization across tabs.
|
|
34
50
|
*/
|
|
35
51
|
constructor(ttl = ONE_DAY, staleTime = ONE_HOUR, cleanupOpt = {
|
|
36
52
|
type: 'lru',
|
|
37
53
|
maxSize: 1000,
|
|
38
54
|
checkInterval: ONE_HOUR,
|
|
39
|
-
}) {
|
|
55
|
+
}, syncTabs) {
|
|
40
56
|
this.ttl = ttl;
|
|
41
57
|
this.staleTime = staleTime;
|
|
42
58
|
this.cleanupOpt = {
|
|
@@ -49,18 +65,70 @@ class Cache {
|
|
|
49
65
|
const cleanupInterval = setInterval(() => {
|
|
50
66
|
this.cleanup();
|
|
51
67
|
}, cleanupOpt.checkInterval);
|
|
52
|
-
const
|
|
68
|
+
const cacheId = v7();
|
|
69
|
+
let destroySyncTabs = () => {
|
|
70
|
+
// noop
|
|
71
|
+
};
|
|
72
|
+
if (syncTabs) {
|
|
73
|
+
const channel = new BroadcastChannel(syncTabs.id);
|
|
74
|
+
this.broadcast = (msg) => {
|
|
75
|
+
if (msg.action === 'invalidate')
|
|
76
|
+
return channel.postMessage({
|
|
77
|
+
action: 'invalidate',
|
|
78
|
+
entry: { key: msg.entry.key },
|
|
79
|
+
cacheId,
|
|
80
|
+
type: 'cache-sync-message',
|
|
81
|
+
});
|
|
82
|
+
return channel.postMessage({
|
|
83
|
+
...msg,
|
|
84
|
+
entry: {
|
|
85
|
+
...msg.entry,
|
|
86
|
+
value: syncTabs.serialize(msg.entry.value),
|
|
87
|
+
},
|
|
88
|
+
cacheId,
|
|
89
|
+
type: 'cache-sync-message',
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
channel.onmessage = (event) => {
|
|
93
|
+
const msg = event.data;
|
|
94
|
+
if (!isSyncMessage(msg))
|
|
95
|
+
return;
|
|
96
|
+
if (msg.cacheId === cacheId)
|
|
97
|
+
return; // ignore messages from this cache
|
|
98
|
+
if (msg.action === 'store') {
|
|
99
|
+
const value = syncTabs.deserialize(msg.entry.value);
|
|
100
|
+
if (value === null)
|
|
101
|
+
return;
|
|
102
|
+
this.storeInternal(msg.entry.key, value, msg.entry.stale - msg.entry.updated, msg.entry.expiresAt - msg.entry.updated, true);
|
|
103
|
+
}
|
|
104
|
+
else if (msg.action === 'invalidate') {
|
|
105
|
+
this.invalidateInternal(msg.entry.key, true);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
destroySyncTabs = () => {
|
|
109
|
+
channel.close();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
let destroyed = false;
|
|
113
|
+
const destroy = () => {
|
|
114
|
+
if (destroyed)
|
|
115
|
+
return;
|
|
116
|
+
destroyed = true;
|
|
117
|
+
clearInterval(cleanupInterval);
|
|
118
|
+
destroySyncTabs();
|
|
119
|
+
};
|
|
120
|
+
this.destroy = destroy;
|
|
53
121
|
// 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
122
|
const registry = new FinalizationRegistry((id) => {
|
|
55
|
-
if (id ===
|
|
56
|
-
|
|
123
|
+
if (id === cacheId) {
|
|
124
|
+
destroy();
|
|
57
125
|
}
|
|
58
126
|
});
|
|
59
|
-
registry.register(this,
|
|
127
|
+
registry.register(this, cacheId);
|
|
60
128
|
}
|
|
61
129
|
/** @internal */
|
|
62
130
|
getInternal(key) {
|
|
63
|
-
const keySignal = computed(() => key());
|
|
131
|
+
const keySignal = computed(() => key(), ...(ngDevMode ? [{ debugName: "keySignal" }] : []));
|
|
64
132
|
return computed(() => {
|
|
65
133
|
const key = keySignal();
|
|
66
134
|
if (!key)
|
|
@@ -116,6 +184,9 @@ class Cache {
|
|
|
116
184
|
* @param ttl - (Optional) The TTL for this entry, in milliseconds. Overrides the default `ttl`.
|
|
117
185
|
*/
|
|
118
186
|
store(key, value, staleTime = this.staleTime, ttl = this.ttl) {
|
|
187
|
+
this.storeInternal(key, value, staleTime, ttl);
|
|
188
|
+
}
|
|
189
|
+
storeInternal(key, value, staleTime = this.staleTime, ttl = this.ttl, fromSync = false) {
|
|
119
190
|
const entry = this.getUntracked(key);
|
|
120
191
|
if (entry) {
|
|
121
192
|
clearTimeout(entry.timeout); // stop invalidation
|
|
@@ -125,18 +196,27 @@ class Cache {
|
|
|
125
196
|
if (ttl < staleTime)
|
|
126
197
|
staleTime = ttl;
|
|
127
198
|
const now = Date.now();
|
|
199
|
+
const next = {
|
|
200
|
+
value,
|
|
201
|
+
created: entry?.created ?? now,
|
|
202
|
+
updated: now,
|
|
203
|
+
useCount: prevCount + 1,
|
|
204
|
+
stale: now + staleTime,
|
|
205
|
+
expiresAt: now + ttl,
|
|
206
|
+
key,
|
|
207
|
+
};
|
|
128
208
|
this.internal.mutate((map) => {
|
|
129
209
|
map.set(key, {
|
|
130
|
-
|
|
131
|
-
created: entry?.created ?? now,
|
|
132
|
-
useCount: prevCount + 1,
|
|
133
|
-
stale: now + staleTime,
|
|
134
|
-
expiresAt: now + ttl,
|
|
210
|
+
...next,
|
|
135
211
|
timeout: setTimeout(() => this.invalidate(key), ttl),
|
|
136
|
-
key,
|
|
137
212
|
});
|
|
138
213
|
return map;
|
|
139
214
|
});
|
|
215
|
+
if (!fromSync)
|
|
216
|
+
this.broadcast({
|
|
217
|
+
action: 'store',
|
|
218
|
+
entry: next,
|
|
219
|
+
});
|
|
140
220
|
}
|
|
141
221
|
/**
|
|
142
222
|
* Invalidates (removes) a cache entry.
|
|
@@ -144,6 +224,9 @@ class Cache {
|
|
|
144
224
|
* @param key - The key of the entry to invalidate.
|
|
145
225
|
*/
|
|
146
226
|
invalidate(key) {
|
|
227
|
+
this.invalidateInternal(key);
|
|
228
|
+
}
|
|
229
|
+
invalidateInternal(key, fromSync = false) {
|
|
147
230
|
const entry = this.getUntracked(key);
|
|
148
231
|
if (!entry)
|
|
149
232
|
return;
|
|
@@ -152,6 +235,8 @@ class Cache {
|
|
|
152
235
|
map.delete(key);
|
|
153
236
|
return map;
|
|
154
237
|
});
|
|
238
|
+
if (!fromSync)
|
|
239
|
+
this.broadcast({ action: 'invalidate', entry: { key } });
|
|
155
240
|
}
|
|
156
241
|
/** @internal */
|
|
157
242
|
cleanup() {
|
|
@@ -198,9 +283,53 @@ const CLIENT_CACHE_TOKEN = new InjectionToken('INTERNAL_CLIENT_CACHE');
|
|
|
198
283
|
* };
|
|
199
284
|
*/
|
|
200
285
|
function provideQueryCache(opt) {
|
|
286
|
+
const syncTabsOpt = opt?.syncTabsId
|
|
287
|
+
? {
|
|
288
|
+
id: opt.syncTabsId,
|
|
289
|
+
serialize: (value) => {
|
|
290
|
+
const headersRecord = {};
|
|
291
|
+
const headerKeys = value.headers.keys();
|
|
292
|
+
headerKeys.forEach((key) => {
|
|
293
|
+
const values = value.headers.getAll(key);
|
|
294
|
+
if (!values)
|
|
295
|
+
return;
|
|
296
|
+
headersRecord[key] = values;
|
|
297
|
+
});
|
|
298
|
+
return JSON.stringify({
|
|
299
|
+
body: value.body,
|
|
300
|
+
status: value.status,
|
|
301
|
+
statusText: value.statusText,
|
|
302
|
+
headers: headerKeys.length > 0 ? headersRecord : undefined,
|
|
303
|
+
url: value.url,
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
deserialize: (value) => {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(value);
|
|
309
|
+
if (!parsed || typeof parsed !== 'object' || !('body' in parsed))
|
|
310
|
+
throw new Error('Invalid cache entry format');
|
|
311
|
+
const headers = parsed.headers
|
|
312
|
+
? new HttpHeaders(parsed.headers)
|
|
313
|
+
: undefined;
|
|
314
|
+
return new HttpResponse({
|
|
315
|
+
body: parsed.body,
|
|
316
|
+
status: parsed.status,
|
|
317
|
+
statusText: parsed.statusText,
|
|
318
|
+
headers: headers,
|
|
319
|
+
url: parsed.url,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
if (isDevMode())
|
|
324
|
+
console.error('Failed to deserialize cache entry:', err);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
: undefined;
|
|
201
330
|
return {
|
|
202
331
|
provide: CLIENT_CACHE_TOKEN,
|
|
203
|
-
useValue: new Cache(opt?.ttl, opt?.staleTime, opt?.cleanup),
|
|
332
|
+
useValue: new Cache(opt?.ttl, opt?.staleTime, opt?.cleanup, syncTabsOpt),
|
|
204
333
|
};
|
|
205
334
|
}
|
|
206
335
|
class NoopCache extends Cache {
|
|
@@ -471,15 +600,15 @@ const DEFAULT_OPTIONS = {
|
|
|
471
600
|
};
|
|
472
601
|
/** @internal */
|
|
473
602
|
function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldFail = () => true, shouldFailForever = () => false) {
|
|
474
|
-
const halfOpen = signal(false);
|
|
475
|
-
const failureCount = signal(0);
|
|
603
|
+
const halfOpen = signal(false, ...(ngDevMode ? [{ debugName: "halfOpen" }] : []));
|
|
604
|
+
const failureCount = signal(0, ...(ngDevMode ? [{ debugName: "failureCount" }] : []));
|
|
476
605
|
const status = computed(() => {
|
|
477
606
|
if (failureCount() >= treshold)
|
|
478
607
|
return 'OPEN';
|
|
479
608
|
return halfOpen() ? 'HALF_OPEN' : 'CLOSED';
|
|
480
|
-
});
|
|
481
|
-
const isClosed = computed(() => status() !== 'OPEN');
|
|
482
|
-
const isOpen = computed(() => status() !== 'CLOSED');
|
|
609
|
+
}, ...(ngDevMode ? [{ debugName: "status" }] : []));
|
|
610
|
+
const isClosed = computed(() => status() !== 'OPEN', ...(ngDevMode ? [{ debugName: "isClosed" }] : []));
|
|
611
|
+
const isOpen = computed(() => status() !== 'CLOSED', ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
483
612
|
const success = () => {
|
|
484
613
|
failureCount.set(0);
|
|
485
614
|
halfOpen.set(false);
|
|
@@ -500,7 +629,7 @@ function internalCeateCircuitBreaker(treshold = 5, resetTimeout = 30000, shouldF
|
|
|
500
629
|
clearTimeout(timeout);
|
|
501
630
|
failForeverResetId = null;
|
|
502
631
|
});
|
|
503
|
-
});
|
|
632
|
+
}, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
|
|
504
633
|
const failInternal = () => {
|
|
505
634
|
failureCount.set(failureCount() + 1);
|
|
506
635
|
halfOpen.set(false);
|
|
@@ -904,7 +1033,7 @@ function retryOnError(res, opt) {
|
|
|
904
1033
|
case 'resolved':
|
|
905
1034
|
return onSuccess();
|
|
906
1035
|
}
|
|
907
|
-
});
|
|
1036
|
+
}, ...(ngDevMode ? [{ debugName: "ref" }] : []));
|
|
908
1037
|
return {
|
|
909
1038
|
...res,
|
|
910
1039
|
destroy: () => {
|
|
@@ -967,11 +1096,13 @@ function queryResource(request, options) {
|
|
|
967
1096
|
if (cb.isOpen())
|
|
968
1097
|
return undefined;
|
|
969
1098
|
return request() ?? undefined;
|
|
970
|
-
}, {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
:
|
|
974
|
-
|
|
1099
|
+
}, ...(ngDevMode ? [{ debugName: "stableRequest", equal: options?.triggerOnSameRequest
|
|
1100
|
+
? undefined
|
|
1101
|
+
: createEqualRequest(options?.equal) }] : [{
|
|
1102
|
+
equal: options?.triggerOnSameRequest
|
|
1103
|
+
? undefined
|
|
1104
|
+
: createEqualRequest(options?.equal),
|
|
1105
|
+
}]));
|
|
975
1106
|
const hashFn = typeof options?.cache === 'object'
|
|
976
1107
|
? (options.cache.hash ?? urlWithParams)
|
|
977
1108
|
: urlWithParams;
|
|
@@ -982,7 +1113,7 @@ function queryResource(request, options) {
|
|
|
982
1113
|
if (!r)
|
|
983
1114
|
return null;
|
|
984
1115
|
return hashFn(r);
|
|
985
|
-
});
|
|
1116
|
+
}, ...(ngDevMode ? [{ debugName: "cacheKey" }] : []));
|
|
986
1117
|
const bustBrowserCache = typeof options?.cache === 'object' &&
|
|
987
1118
|
options.cache.bustBrowserCache === true;
|
|
988
1119
|
const ignoreCacheControl = typeof options?.cache === 'object' &&
|
|
@@ -1048,7 +1179,7 @@ function queryResource(request, options) {
|
|
|
1048
1179
|
const err = resource.error();
|
|
1049
1180
|
if (err)
|
|
1050
1181
|
onError(err);
|
|
1051
|
-
});
|
|
1182
|
+
}, ...(ngDevMode ? [{ debugName: "onErrorRef" }] : []));
|
|
1052
1183
|
// 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
|
|
1053
1184
|
const destroyRest = resource.destroy;
|
|
1054
1185
|
resource.destroy = () => {
|
|
@@ -1063,7 +1194,7 @@ function queryResource(request, options) {
|
|
|
1063
1194
|
cb.fail(untracked(resource.error));
|
|
1064
1195
|
else if (status === 'resolved')
|
|
1065
1196
|
cb.success();
|
|
1066
|
-
});
|
|
1197
|
+
}, ...(ngDevMode ? [{ debugName: "cbEffectRef" }] : []));
|
|
1067
1198
|
const set = (value) => {
|
|
1068
1199
|
resource.value.set(value);
|
|
1069
1200
|
const k = untracked(cacheKey);
|
|
@@ -1117,6 +1248,11 @@ function queryResource(request, options) {
|
|
|
1117
1248
|
try {
|
|
1118
1249
|
await firstValueFrom(client.request(prefetchRequest.method ?? 'GET', prefetchRequest.url, {
|
|
1119
1250
|
...prefetchRequest,
|
|
1251
|
+
credentials: prefetchRequest.credentials,
|
|
1252
|
+
priority: prefetchRequest.priority,
|
|
1253
|
+
cache: prefetchRequest.cache,
|
|
1254
|
+
mode: prefetchRequest.mode,
|
|
1255
|
+
redirect: prefetchRequest.redirect,
|
|
1120
1256
|
context: setCacheContext(prefetchRequest.context, {
|
|
1121
1257
|
staleTime,
|
|
1122
1258
|
ttl,
|
|
@@ -1164,23 +1300,29 @@ function mutationResource(request, options = {}) {
|
|
|
1164
1300
|
const { onMutate, onError, onSuccess, onSettled, equal, ...rest } = options;
|
|
1165
1301
|
const requestEqual = createEqualRequest(equal);
|
|
1166
1302
|
const eq = equal ?? Object.is;
|
|
1167
|
-
const next = signal(null, {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
return
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1303
|
+
const next = signal(null, ...(ngDevMode ? [{ debugName: "next", equal: (a, b) => {
|
|
1304
|
+
if (!a && !b)
|
|
1305
|
+
return true;
|
|
1306
|
+
if (!a || !b)
|
|
1307
|
+
return false;
|
|
1308
|
+
return eq(a, b);
|
|
1309
|
+
} }] : [{
|
|
1310
|
+
equal: (a, b) => {
|
|
1311
|
+
if (!a && !b)
|
|
1312
|
+
return true;
|
|
1313
|
+
if (!a || !b)
|
|
1314
|
+
return false;
|
|
1315
|
+
return eq(a, b);
|
|
1316
|
+
},
|
|
1317
|
+
}]));
|
|
1176
1318
|
const req = computed(() => {
|
|
1177
1319
|
const nr = next();
|
|
1178
1320
|
if (!nr)
|
|
1179
1321
|
return;
|
|
1180
1322
|
return request(nr) ?? undefined;
|
|
1181
|
-
}, {
|
|
1182
|
-
|
|
1183
|
-
|
|
1323
|
+
}, ...(ngDevMode ? [{ debugName: "req", equal: requestEqual }] : [{
|
|
1324
|
+
equal: requestEqual,
|
|
1325
|
+
}]));
|
|
1184
1326
|
const lastValue = linkedSignal({
|
|
1185
1327
|
source: next,
|
|
1186
1328
|
computation: (next, prev) => {
|
|
@@ -1194,9 +1336,9 @@ function mutationResource(request, options = {}) {
|
|
|
1194
1336
|
if (!nr)
|
|
1195
1337
|
return;
|
|
1196
1338
|
return request(nr) ?? undefined;
|
|
1197
|
-
}, {
|
|
1198
|
-
|
|
1199
|
-
|
|
1339
|
+
}, ...(ngDevMode ? [{ debugName: "lastValueRequest", equal: requestEqual }] : [{
|
|
1340
|
+
equal: requestEqual,
|
|
1341
|
+
}]));
|
|
1200
1342
|
const cb = createCircuitBreaker(options?.circuitBreaker === true
|
|
1201
1343
|
? undefined
|
|
1202
1344
|
: (options?.circuitBreaker ?? false), options?.injector);
|