cache.js (5703B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 BaseStorageActor, 9 } = require("resource://devtools/server/actors/resources/storage/index.js"); 10 11 class CacheStorageActor extends BaseStorageActor { 12 constructor(storageActor) { 13 super(storageActor, "Cache"); 14 } 15 16 async populateStoresForHost(host) { 17 const storeMap = new Map(); 18 const caches = await this.getCachesForHost(host); 19 try { 20 for (const name of await caches.keys()) { 21 storeMap.set(name, await caches.open(name)); 22 } 23 } catch (ex) { 24 console.warn(`Failed to enumerate CacheStorage for host ${host}: ${ex}`); 25 } 26 this.hostVsStores.set(host, storeMap); 27 } 28 29 async getCachesForHost(host) { 30 const win = this.storageActor.getWindowFromHost(host); 31 if (!win) { 32 return null; 33 } 34 35 const principal = win.document.effectiveStoragePrincipal; 36 37 // The first argument tells if you want to get |content| cache or |chrome| 38 // cache. 39 // The |content| cache is the cache explicitely named by the web content 40 // (service worker or web page). 41 // The |chrome| cache is the cache implicitely cached by the platform, 42 // hosting the source file of the service worker. 43 const { CacheStorage } = win; 44 45 if (!CacheStorage) { 46 return null; 47 } 48 49 const cache = new CacheStorage("content", principal); 50 return cache; 51 } 52 53 form() { 54 const hosts = {}; 55 for (const host of this.hosts) { 56 hosts[host] = this.getNamesForHost(host); 57 } 58 59 return { 60 actor: this.actorID, 61 hosts, 62 traits: this._getTraits(), 63 }; 64 } 65 66 getNamesForHost(host) { 67 // UI code expect each name to be a JSON string of an array :/ 68 return [...this.hostVsStores.get(host).keys()].map(a => { 69 return JSON.stringify([a]); 70 }); 71 } 72 73 async getValuesForHost(host, name) { 74 if (!name) { 75 // if we get here, we most likely clicked on the refresh button 76 // which called getStoreObjects, itself calling this method, 77 // all that, without having selected any particular cache name. 78 // 79 // Try to detect if a new cache has been added and notify the client 80 // asynchronously, via a RDP event. 81 const previousCaches = [...this.hostVsStores.get(host).keys()]; 82 await this.populateStoresForHosts(); 83 const updatedCaches = [...this.hostVsStores.get(host).keys()]; 84 const newCaches = updatedCaches.filter( 85 cacheName => !previousCaches.includes(cacheName) 86 ); 87 newCaches.forEach(cacheName => 88 this.onItemUpdated("added", host, [cacheName]) 89 ); 90 const removedCaches = previousCaches.filter( 91 cacheName => !updatedCaches.includes(cacheName) 92 ); 93 removedCaches.forEach(cacheName => 94 this.onItemUpdated("deleted", host, [cacheName]) 95 ); 96 return []; 97 } 98 // UI is weird and expect a JSON stringified array... and pass it back :/ 99 name = JSON.parse(name)[0]; 100 101 const cache = this.hostVsStores.get(host).get(name); 102 const requests = await cache.keys(); 103 const results = []; 104 for (const request of requests) { 105 let response = await cache.match(request); 106 // Unwrap the response to get access to all its properties if the 107 // response happen to be 'opaque', when it is a Cross Origin Request. 108 response = response.cloneUnfiltered(); 109 results.push(await this.processEntry(request, response)); 110 } 111 return results; 112 } 113 114 async processEntry(request, response) { 115 return { 116 url: String(request.url), 117 status: String(response.statusText), 118 }; 119 } 120 121 async getFields() { 122 return [ 123 { name: "url", editable: false }, 124 { name: "status", editable: false }, 125 ]; 126 } 127 128 /** 129 * Given a url, correctly determine its protocol + hostname part. 130 */ 131 getSchemaAndHost(url) { 132 const uri = Services.io.newURI(url); 133 return uri.scheme + "://" + uri.hostPort; 134 } 135 136 toStoreObject(item) { 137 return item; 138 } 139 140 async removeItem(host, name) { 141 const cacheMap = this.hostVsStores.get(host); 142 if (!cacheMap) { 143 return; 144 } 145 146 const parsedName = JSON.parse(name); 147 148 if (parsedName.length == 1) { 149 // Delete the whole Cache object 150 const [cacheName] = parsedName; 151 cacheMap.delete(cacheName); 152 const cacheStorage = await this.getCachesForHost(host); 153 await cacheStorage.delete(cacheName); 154 this.onItemUpdated("deleted", host, [cacheName]); 155 } else if (parsedName.length == 2) { 156 // Delete one cached request 157 const [cacheName, url] = parsedName; 158 const cache = cacheMap.get(cacheName); 159 if (cache) { 160 await cache.delete(url); 161 this.onItemUpdated("deleted", host, [cacheName, url]); 162 } 163 } 164 } 165 166 async removeAll(host, name) { 167 const cacheMap = this.hostVsStores.get(host); 168 if (!cacheMap) { 169 return; 170 } 171 172 const parsedName = JSON.parse(name); 173 174 // Only a Cache object is a valid object to clear 175 if (parsedName.length == 1) { 176 const [cacheName] = parsedName; 177 const cache = cacheMap.get(cacheName); 178 if (cache) { 179 const keys = await cache.keys(); 180 await Promise.all(keys.map(key => cache.delete(key))); 181 this.onItemUpdated("cleared", host, [cacheName]); 182 } 183 } 184 } 185 186 /** 187 * CacheStorage API doesn't support any notifications, we must fake them 188 */ 189 onItemUpdated(action, host, path) { 190 this.storageActor.update(action, "Cache", { 191 [host]: [JSON.stringify(path)], 192 }); 193 } 194 } 195 exports.CacheStorageActor = CacheStorageActor;