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