im trying to use CacheStorage\'s promises in Angular 5 like in the docs :
let test = caches.open(\'test\');
test.then((result : Cache) => {
result.a
This is easy to miss -
1) CacheStorage API (or Cache API) are only available on localhost and HTTP/2.
2) For purists reason, prefer to use window.caches.open(CACHE_NAME)
as caches
is a singleton instance present in window
object.
I had the same problem in one of my projects. I wanted my application to store some assets in the CacheStorage
.
My code looked something like this:
window.caches.open(CACHE_NAME)
.then(cache => cache.addAll(resources))
.catch(err => console.log('error while caching', err));
I got the same Error:
ERROR Error: Uncaught (in promise): TypeError: the given value is not a Promise
While searching for the problem I found out that Angular is replacing the browser's global Promise
implementation by it's own ZoneAwarePromise
to keep track of promises. Unfortunately the CacheStorage
somehow expects a native Promise
instead of Angular's ZoneAwarePromise
and throws an error.
I tried to get around this problem by storing the ZoneAwarePromise
temporarily and reset it with the original Promise
. This turned out to be tricky, resulting in me giving up to exchange back the implementation.
I found two suitable workarounds for this.
For my second (and I think better) workaround I open a channel for client <=> service worker communication and let the service worker do the dynamic caching.
Implement the message
event listener in the service worker.
The service worker will listen to the message
event to receive and cache a list of urls from the client.
self.addEventListener('message', event => {
const urls = event.data;
if (!urls) {
event.waitUntil(Promise.reject('nothing to cache'));
event.ports[0].postMessage(false);
}
else {
event.waitUntil(
caches.open(version)
.then(cache => cache.addAll(urls))
.then(self.skipWaiting())
.then(() => console.log('dynamic urls cached'))
);
event.ports[0].postMessage(true);
}
});
Add caching for new urls to the client.
This submits a message to the service worker with the list of urls as payload.
function addToCache(urls: string[]): Promise<any> {
return new Promise((resolve, reject) => {
let messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.error) reject(event.data.error);
else resolve(event.data);
};
if (!navigator.serviceWorker.controller) reject('no service worker contoller found')
else navigator.serviceWorker.controller.postMessage(urls, [messageChannel.port2]);
});
}
Make sure the service worker is active for caching.
I use the controllerchange
event in the client to trigger re-caching if a new service worker is taking control. (see: service worker lifecycle documentation).
const resources = ['some/url/to/cache.png', 'another/url/to/cache.css']
navigator.serviceWorker.addEventListener('controllerchange', () =>
addToCache(resources)
.then(data => console.log('urls cached', data))
.catch(err => console.log('error while caching', err))
);
iframe
My first workaround for this problem is really just a hack but it works for now.
const hiddenFrame = document.createElement('iframe');
hiddenFrame.style.display = 'none';
document.documentElement.appendChild(hiddenFrame);
hiddenFrame.contentWindow.caches.open(CACHE_NAME)
.then(cache => cache.addAll(resources))
.then(() => document.documentElement.removeChild(hiddenFrame))
.catch(err => console.log('error while caching', err));
What I'm doing here:
I create a hidden iframe
and append it to my document. Inside the iframe
Angular is not running and so there is the native Promise
implementation present. Then I access the cache from the iframe's contentWindow
object. After successful caching I remove the iframe
.
I'm still looking for a better solution.
My next try is to send a message to a service worker and let it trigger the caching of the resources. The service worker has no angular running either.