@mmstack/resource 20.2.7 → 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 +143 -14
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/index.d.ts +21 -2
- package/package.json +2 -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,14 +65,66 @@ 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) {
|
|
@@ -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 {
|