@mmstack/resource 20.2.8 → 20.2.9
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/README.md +8 -0
- package/fesm2022/mmstack-resource.mjs +207 -73
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/index.d.ts +31 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,14 @@ export const appConfig: ApplicationConfig = {
|
|
|
37
37
|
providers: [
|
|
38
38
|
// ..other providers
|
|
39
39
|
provideQueryCache(),
|
|
40
|
+
|
|
41
|
+
// --- Example of a more advanced setup ---
|
|
42
|
+
// provideQueryCache({
|
|
43
|
+
// persist: true, // Enable IndexedDB persistence
|
|
44
|
+
// version: 1, // Version for the cache schema
|
|
45
|
+
// syncTabs: true // enable BroadcastChannel
|
|
46
|
+
// }),
|
|
47
|
+
|
|
40
48
|
provideHttpClient(withInterceptors([createCacheInterceptor(), createDedupeRequestsInterceptor()])),
|
|
41
49
|
],
|
|
42
50
|
};
|
|
@@ -1,11 +1,100 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDevMode, inject, PLATFORM_ID, untracked, computed, InjectionToken, 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
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
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
7
8
|
import { keys, hash, entries } from '@mmstack/object';
|
|
8
9
|
|
|
10
|
+
function createNoopDB() {
|
|
11
|
+
return {
|
|
12
|
+
getAll: async () => [],
|
|
13
|
+
store: async () => {
|
|
14
|
+
// noop
|
|
15
|
+
},
|
|
16
|
+
remove: async () => {
|
|
17
|
+
// noop
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function toCacheDB(db, storeName) {
|
|
22
|
+
const getAll = async () => {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
return new Promise((res, rej) => {
|
|
25
|
+
const transaction = db.transaction(storeName, 'readonly');
|
|
26
|
+
const store = transaction.objectStore(storeName);
|
|
27
|
+
const request = store.getAll();
|
|
28
|
+
request.onsuccess = () => res(request.result);
|
|
29
|
+
request.onerror = () => rej(request.error);
|
|
30
|
+
})
|
|
31
|
+
.then((entries) => entries.filter((e) => e.expiresAt > now))
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
if (isDevMode())
|
|
34
|
+
console.error('Error getting all items from cache DB:', err);
|
|
35
|
+
return [];
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
const store = (value) => {
|
|
39
|
+
return new Promise((res, rej) => {
|
|
40
|
+
const transaction = db.transaction(storeName, 'readwrite');
|
|
41
|
+
const store = transaction.objectStore(storeName);
|
|
42
|
+
store.put(value);
|
|
43
|
+
transaction.oncomplete = () => res();
|
|
44
|
+
transaction.onerror = () => rej(transaction.error);
|
|
45
|
+
}).catch((err) => {
|
|
46
|
+
if (isDevMode())
|
|
47
|
+
console.error('Error storing item in cache DB:', err);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const remove = (key) => {
|
|
51
|
+
return new Promise((res, rej) => {
|
|
52
|
+
const transaction = db.transaction(storeName, 'readwrite');
|
|
53
|
+
const store = transaction.objectStore(storeName);
|
|
54
|
+
store.delete(key);
|
|
55
|
+
transaction.oncomplete = () => res();
|
|
56
|
+
transaction.onerror = () => rej(transaction.error);
|
|
57
|
+
}).catch((err) => {
|
|
58
|
+
if (isDevMode())
|
|
59
|
+
console.error('Error removing item from cache DB:', err);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
getAll,
|
|
64
|
+
store,
|
|
65
|
+
remove,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createSingleStoreDB(name, getStoreName, version = 1) {
|
|
69
|
+
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
70
|
+
const storeName = getStoreName(version);
|
|
71
|
+
if (!isBrowser)
|
|
72
|
+
return Promise.resolve(createNoopDB());
|
|
73
|
+
return new Promise((res, rej) => {
|
|
74
|
+
if (version < 1)
|
|
75
|
+
rej(new Error('Version must be 1 or greater'));
|
|
76
|
+
const req = indexedDB.open(name, version);
|
|
77
|
+
req.onupgradeneeded = (event) => {
|
|
78
|
+
const db = req.result;
|
|
79
|
+
const oldVersion = event.oldVersion;
|
|
80
|
+
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
81
|
+
if (oldVersion > 0) {
|
|
82
|
+
db.deleteObjectStore(getStoreName(oldVersion));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
req.onerror = () => {
|
|
86
|
+
rej(req.error);
|
|
87
|
+
};
|
|
88
|
+
req.onsuccess = () => res(req.result);
|
|
89
|
+
})
|
|
90
|
+
.then((db) => toCacheDB(db, storeName))
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
if (isDevMode())
|
|
93
|
+
console.error('Error creating query DB:', err);
|
|
94
|
+
return createNoopDB();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
9
98
|
function isSyncMessage(msg) {
|
|
10
99
|
return (typeof msg === 'object' &&
|
|
11
100
|
msg !== null &&
|
|
@@ -27,8 +116,10 @@ const DEFAULT_CLEANUP_OPT = {
|
|
|
27
116
|
class Cache {
|
|
28
117
|
ttl;
|
|
29
118
|
staleTime;
|
|
119
|
+
db;
|
|
30
120
|
internal = mutable(new Map());
|
|
31
121
|
cleanupOpt;
|
|
122
|
+
id = v7();
|
|
32
123
|
/**
|
|
33
124
|
* Destroys the cache instance, cleaning up any resources used by the cache.
|
|
34
125
|
* This method is called automatically when the cache instance is garbage collected.
|
|
@@ -52,9 +143,10 @@ class Cache {
|
|
|
52
143
|
type: 'lru',
|
|
53
144
|
maxSize: 1000,
|
|
54
145
|
checkInterval: ONE_HOUR,
|
|
55
|
-
}, syncTabs) {
|
|
146
|
+
}, syncTabs, db = Promise.resolve(createNoopDB())) {
|
|
56
147
|
this.ttl = ttl;
|
|
57
148
|
this.staleTime = staleTime;
|
|
149
|
+
this.db = db;
|
|
58
150
|
this.cleanupOpt = {
|
|
59
151
|
...DEFAULT_CLEANUP_OPT,
|
|
60
152
|
...cleanupOpt,
|
|
@@ -65,7 +157,6 @@ class Cache {
|
|
|
65
157
|
const cleanupInterval = setInterval(() => {
|
|
66
158
|
this.cleanup();
|
|
67
159
|
}, cleanupOpt.checkInterval);
|
|
68
|
-
const cacheId = v7();
|
|
69
160
|
let destroySyncTabs = () => {
|
|
70
161
|
// noop
|
|
71
162
|
};
|
|
@@ -76,7 +167,7 @@ class Cache {
|
|
|
76
167
|
return channel.postMessage({
|
|
77
168
|
action: 'invalidate',
|
|
78
169
|
entry: { key: msg.entry.key },
|
|
79
|
-
cacheId,
|
|
170
|
+
cacheId: this.id,
|
|
80
171
|
type: 'cache-sync-message',
|
|
81
172
|
});
|
|
82
173
|
return channel.postMessage({
|
|
@@ -85,7 +176,7 @@ class Cache {
|
|
|
85
176
|
...msg.entry,
|
|
86
177
|
value: syncTabs.serialize(msg.entry.value),
|
|
87
178
|
},
|
|
88
|
-
cacheId,
|
|
179
|
+
cacheId: this.id,
|
|
89
180
|
type: 'cache-sync-message',
|
|
90
181
|
});
|
|
91
182
|
};
|
|
@@ -93,13 +184,13 @@ class Cache {
|
|
|
93
184
|
const msg = event.data;
|
|
94
185
|
if (!isSyncMessage(msg))
|
|
95
186
|
return;
|
|
96
|
-
if (msg.cacheId ===
|
|
187
|
+
if (msg.cacheId === this.id)
|
|
97
188
|
return; // ignore messages from this cache
|
|
98
189
|
if (msg.action === 'store') {
|
|
99
190
|
const value = syncTabs.deserialize(msg.entry.value);
|
|
100
191
|
if (value === null)
|
|
101
192
|
return;
|
|
102
|
-
this.storeInternal(msg.entry.key, value, msg.entry.stale - msg.entry.updated, msg.entry.expiresAt - msg.entry.updated, true);
|
|
193
|
+
this.storeInternal(msg.entry.key, value, msg.entry.stale - msg.entry.updated, msg.entry.expiresAt - msg.entry.updated, true, false);
|
|
103
194
|
}
|
|
104
195
|
else if (msg.action === 'invalidate') {
|
|
105
196
|
this.invalidateInternal(msg.entry.key, true);
|
|
@@ -117,14 +208,31 @@ class Cache {
|
|
|
117
208
|
clearInterval(cleanupInterval);
|
|
118
209
|
destroySyncTabs();
|
|
119
210
|
};
|
|
211
|
+
this.db
|
|
212
|
+
.then(async (db) => {
|
|
213
|
+
if (destroyed)
|
|
214
|
+
return [];
|
|
215
|
+
return db.getAll();
|
|
216
|
+
})
|
|
217
|
+
.then((entries) => {
|
|
218
|
+
if (destroyed)
|
|
219
|
+
return;
|
|
220
|
+
// load entries into the cache
|
|
221
|
+
const current = untracked(this.internal);
|
|
222
|
+
entries.forEach((entry) => {
|
|
223
|
+
if (current.has(entry.key))
|
|
224
|
+
return;
|
|
225
|
+
this.storeInternal(entry.key, entry.value, entry.stale - entry.updated, entry.expiresAt - entry.updated, true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
120
228
|
this.destroy = destroy;
|
|
121
229
|
// 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
|
|
122
230
|
const registry = new FinalizationRegistry((id) => {
|
|
123
|
-
if (id ===
|
|
231
|
+
if (id === this.id) {
|
|
124
232
|
destroy();
|
|
125
233
|
}
|
|
126
234
|
});
|
|
127
|
-
registry.register(this,
|
|
235
|
+
registry.register(this, this.id);
|
|
128
236
|
}
|
|
129
237
|
/** @internal */
|
|
130
238
|
getInternal(key) {
|
|
@@ -183,10 +291,10 @@ class Cache {
|
|
|
183
291
|
* @param staleTime - (Optional) The stale time for this entry, in milliseconds. Overrides the default `staleTime`.
|
|
184
292
|
* @param ttl - (Optional) The TTL for this entry, in milliseconds. Overrides the default `ttl`.
|
|
185
293
|
*/
|
|
186
|
-
store(key, value, staleTime = this.staleTime, ttl = this.ttl) {
|
|
187
|
-
this.storeInternal(key, value, staleTime, ttl);
|
|
294
|
+
store(key, value, staleTime = this.staleTime, ttl = this.ttl, persist = false) {
|
|
295
|
+
this.storeInternal(key, value, staleTime, ttl, false, persist);
|
|
188
296
|
}
|
|
189
|
-
storeInternal(key, value, staleTime = this.staleTime, ttl = this.ttl, fromSync = false) {
|
|
297
|
+
storeInternal(key, value, staleTime = this.staleTime, ttl = this.ttl, fromSync = false, persist = false) {
|
|
190
298
|
const entry = this.getUntracked(key);
|
|
191
299
|
if (entry) {
|
|
192
300
|
clearTimeout(entry.timeout); // stop invalidation
|
|
@@ -212,11 +320,14 @@ class Cache {
|
|
|
212
320
|
});
|
|
213
321
|
return map;
|
|
214
322
|
});
|
|
215
|
-
if (!fromSync)
|
|
323
|
+
if (!fromSync) {
|
|
324
|
+
if (persist)
|
|
325
|
+
this.db.then((db) => db.store(next));
|
|
216
326
|
this.broadcast({
|
|
217
327
|
action: 'store',
|
|
218
328
|
entry: next,
|
|
219
329
|
});
|
|
330
|
+
}
|
|
220
331
|
}
|
|
221
332
|
/**
|
|
222
333
|
* Invalidates (removes) a cache entry.
|
|
@@ -235,8 +346,10 @@ class Cache {
|
|
|
235
346
|
map.delete(key);
|
|
236
347
|
return map;
|
|
237
348
|
});
|
|
238
|
-
if (!fromSync)
|
|
349
|
+
if (!fromSync) {
|
|
350
|
+
this.db.then((db) => db.remove(key));
|
|
239
351
|
this.broadcast({ action: 'invalidate', entry: { key } });
|
|
352
|
+
}
|
|
240
353
|
}
|
|
241
354
|
/** @internal */
|
|
242
355
|
cleanup() {
|
|
@@ -283,53 +396,80 @@ const CLIENT_CACHE_TOKEN = new InjectionToken('INTERNAL_CLIENT_CACHE');
|
|
|
283
396
|
* };
|
|
284
397
|
*/
|
|
285
398
|
function provideQueryCache(opt) {
|
|
286
|
-
const
|
|
399
|
+
const serialize = (value) => {
|
|
400
|
+
const headersRecord = {};
|
|
401
|
+
const headerKeys = value.headers.keys();
|
|
402
|
+
headerKeys.forEach((key) => {
|
|
403
|
+
const values = value.headers.getAll(key);
|
|
404
|
+
if (!values)
|
|
405
|
+
return;
|
|
406
|
+
headersRecord[key] = values;
|
|
407
|
+
});
|
|
408
|
+
return JSON.stringify({
|
|
409
|
+
body: value.body,
|
|
410
|
+
status: value.status,
|
|
411
|
+
statusText: value.statusText,
|
|
412
|
+
headers: headerKeys.length > 0 ? headersRecord : undefined,
|
|
413
|
+
url: value.url,
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
const deserialize = (value) => {
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(value);
|
|
419
|
+
if (!parsed || typeof parsed !== 'object' || !('body' in parsed))
|
|
420
|
+
throw new Error('Invalid cache entry format');
|
|
421
|
+
const headers = parsed.headers
|
|
422
|
+
? new HttpHeaders(parsed.headers)
|
|
423
|
+
: undefined;
|
|
424
|
+
return new HttpResponse({
|
|
425
|
+
body: parsed.body,
|
|
426
|
+
status: parsed.status,
|
|
427
|
+
statusText: parsed.statusText,
|
|
428
|
+
headers: headers,
|
|
429
|
+
url: parsed.url,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
if (isDevMode())
|
|
434
|
+
console.error('Failed to deserialize cache entry:', err);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const syncTabsOpt = opt?.syncTabs
|
|
287
439
|
? {
|
|
288
|
-
id:
|
|
289
|
-
serialize
|
|
290
|
-
|
|
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
|
-
},
|
|
440
|
+
id: 'mmstack-query-cache-sync',
|
|
441
|
+
serialize,
|
|
442
|
+
deserialize,
|
|
328
443
|
}
|
|
329
444
|
: undefined;
|
|
445
|
+
let db = opt?.persist === false
|
|
446
|
+
? undefined
|
|
447
|
+
: createSingleStoreDB('mmstack-query-cache-db', (version) => `query-store_v${version}`, opt?.version).then((db) => {
|
|
448
|
+
return {
|
|
449
|
+
getAll: () => {
|
|
450
|
+
return db.getAll().then((entries) => {
|
|
451
|
+
return entries
|
|
452
|
+
.map((entry) => {
|
|
453
|
+
const value = deserialize(entry.value);
|
|
454
|
+
if (value === null)
|
|
455
|
+
return null;
|
|
456
|
+
return {
|
|
457
|
+
...entry,
|
|
458
|
+
value,
|
|
459
|
+
};
|
|
460
|
+
})
|
|
461
|
+
.filter((e) => e !== null);
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
store: (entry) => {
|
|
465
|
+
return db.store({ ...entry, value: serialize(entry.value) });
|
|
466
|
+
},
|
|
467
|
+
remove: db.remove,
|
|
468
|
+
};
|
|
469
|
+
});
|
|
330
470
|
return {
|
|
331
471
|
provide: CLIENT_CACHE_TOKEN,
|
|
332
|
-
useValue: new Cache(opt?.ttl, opt?.staleTime, opt?.cleanup, syncTabsOpt),
|
|
472
|
+
useValue: new Cache(opt?.ttl, opt?.staleTime, opt?.cleanup, syncTabsOpt, db),
|
|
333
473
|
};
|
|
334
474
|
}
|
|
335
475
|
class NoopCache extends Cache {
|
|
@@ -385,17 +525,7 @@ function setCacheContext(ctx = new HttpContext(), opt) {
|
|
|
385
525
|
function getCacheContext(ctx) {
|
|
386
526
|
return ctx.get(CACHE_CONTEXT);
|
|
387
527
|
}
|
|
388
|
-
function parseCacheControlHeader(req
|
|
389
|
-
if (ignoreCacheControl) {
|
|
390
|
-
return {
|
|
391
|
-
noStore: false,
|
|
392
|
-
noCache: false,
|
|
393
|
-
mustRevalidate: false,
|
|
394
|
-
immutable: false,
|
|
395
|
-
maxAge: null,
|
|
396
|
-
staleWhileRevalidate: null,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
528
|
+
function parseCacheControlHeader(req) {
|
|
399
529
|
const header = req.headers.get('Cache-Control');
|
|
400
530
|
let sMaxAge = null;
|
|
401
531
|
const directives = {
|
|
@@ -550,10 +680,12 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
550
680
|
}
|
|
551
681
|
return next(req).pipe(tap((event) => {
|
|
552
682
|
if (event instanceof HttpResponse && event.ok) {
|
|
553
|
-
const cacheControl = parseCacheControlHeader(event
|
|
554
|
-
if (cacheControl.noStore)
|
|
683
|
+
const cacheControl = parseCacheControlHeader(event);
|
|
684
|
+
if (cacheControl.noStore && !opt.ignoreCacheControl)
|
|
555
685
|
return;
|
|
556
|
-
const { staleTime, ttl } =
|
|
686
|
+
const { staleTime, ttl } = opt.ignoreCacheControl
|
|
687
|
+
? opt
|
|
688
|
+
: resolveTimings(cacheControl, opt.staleTime, opt.ttl);
|
|
557
689
|
if (opt.ttl === 0)
|
|
558
690
|
return; // no point
|
|
559
691
|
const parsedResponse = opt.parse
|
|
@@ -565,7 +697,7 @@ function createCacheInterceptor(allowedMethods = ['GET', 'HEAD', 'OPTIONS']) {
|
|
|
565
697
|
url: event.url ?? undefined,
|
|
566
698
|
})
|
|
567
699
|
: event;
|
|
568
|
-
cache.store(key, parsedResponse, staleTime, ttl);
|
|
700
|
+
cache.store(key, parsedResponse, staleTime, ttl, opt.persist);
|
|
569
701
|
}
|
|
570
702
|
}), map((event) => {
|
|
571
703
|
// handle 304 responses due to eTag/last-modified
|
|
@@ -1118,7 +1250,7 @@ function queryResource(request, options) {
|
|
|
1118
1250
|
options.cache.bustBrowserCache === true;
|
|
1119
1251
|
const ignoreCacheControl = typeof options?.cache === 'object' &&
|
|
1120
1252
|
options.cache.ignoreCacheControl === true;
|
|
1121
|
-
const
|
|
1253
|
+
const persist = typeof options?.cache === 'object' && options.cache.persist === true;
|
|
1122
1254
|
const cachedRequest = options?.cache
|
|
1123
1255
|
? computed(() => {
|
|
1124
1256
|
const r = stableRequest();
|
|
@@ -1132,6 +1264,7 @@ function queryResource(request, options) {
|
|
|
1132
1264
|
key: cacheKey() ?? hashFn(r),
|
|
1133
1265
|
bustBrowserCache,
|
|
1134
1266
|
ignoreCacheControl,
|
|
1267
|
+
persist,
|
|
1135
1268
|
}),
|
|
1136
1269
|
};
|
|
1137
1270
|
})
|
|
@@ -1203,7 +1336,7 @@ function queryResource(request, options) {
|
|
|
1203
1336
|
body: value,
|
|
1204
1337
|
status: 200,
|
|
1205
1338
|
statusText: 'OK',
|
|
1206
|
-
}));
|
|
1339
|
+
}), staleTime, ttl, persist);
|
|
1207
1340
|
};
|
|
1208
1341
|
const update = (updater) => {
|
|
1209
1342
|
set(updater(untracked(resource.value)));
|
|
@@ -1262,6 +1395,7 @@ function queryResource(request, options) {
|
|
|
1262
1395
|
}),
|
|
1263
1396
|
bustBrowserCache,
|
|
1264
1397
|
ignoreCacheControl,
|
|
1398
|
+
persist,
|
|
1265
1399
|
}),
|
|
1266
1400
|
headers: prefetchRequest.headers,
|
|
1267
1401
|
observe: 'response',
|