blob: cee3e8c01577a00e90f733e512d135913d498554 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @implements {Protocol.StorageDispatcher}
* @unrestricted
*/
export default class ServiceWorkerCacheModel extends SDK.SDKModel {
/**
* Invariant: This model can only be constructed on a ServiceWorker target.
* @param {!SDK.Target} target
*/
constructor(target) {
super(target);
target.registerStorageDispatcher(this);
/** @type {!Map<string, !Cache>} */
this._caches = new Map();
this._cacheAgent = target.cacheStorageAgent();
this._storageAgent = target.storageAgent();
this._securityOriginManager = target.model(SDK.SecurityOriginManager);
this._originsUpdated = new Set();
this._throttler = new Common.Throttler(2000);
/** @type {boolean} */
this._enabled = false;
}
enable() {
if (this._enabled) {
return;
}
this._securityOriginManager.addEventListener(
SDK.SecurityOriginManager.Events.SecurityOriginAdded, this._securityOriginAdded, this);
this._securityOriginManager.addEventListener(
SDK.SecurityOriginManager.Events.SecurityOriginRemoved, this._securityOriginRemoved, this);
for (const securityOrigin of this._securityOriginManager.securityOrigins()) {
this._addOrigin(securityOrigin);
}
this._enabled = true;
}
/**
* @param {string} origin
*/
clearForOrigin(origin) {
this._removeOrigin(origin);
this._addOrigin(origin);
}
refreshCacheNames() {
for (const cache of this._caches.values()) {
this._cacheRemoved(cache);
}
this._caches.clear();
const securityOrigins = this._securityOriginManager.securityOrigins();
for (const securityOrigin of securityOrigins) {
this._loadCacheNames(securityOrigin);
}
}
/**
* @param {!Cache} cache
*/
async deleteCache(cache) {
const response = await this._cacheAgent.invoke_deleteCache({cacheId: cache.cacheId});
if (response[Protocol.Error]) {
console.error(`ServiceWorkerCacheAgent error deleting cache ${cache.toString()}: ${response[Protocol.Error]}`);
return;
}
this._caches.delete(cache.cacheId);
this._cacheRemoved(cache);
}
/**
* @param {!Cache} cache
* @param {string} request
* @return {!Promise}
*/
async deleteCacheEntry(cache, request) {
const response = await this._cacheAgent.invoke_deleteEntry({cacheId: cache.cacheId, request});
if (!response[Protocol.Error]) {
return;
}
Common.console.error(Common.UIString(
'ServiceWorkerCacheAgent error deleting cache entry %s in cache: %s', cache.toString(),
response[Protocol.Error]));
}
/**
* @param {!Cache} cache
* @param {number} skipCount
* @param {number} pageSize
* @param {string} pathFilter
* @param {function(!Array.<!Protocol.CacheStorage.DataEntry>, number)} callback
*/
loadCacheData(cache, skipCount, pageSize, pathFilter, callback) {
this._requestEntries(cache, skipCount, pageSize, pathFilter, callback);
}
/**
* @param {!Cache} cache
* @param {string} pathFilter
* @param {function(!Array.<!Protocol.CacheStorage.DataEntry>, number)} callback
*/
loadAllCacheData(cache, pathFilter, callback) {
this._requestAllEntries(cache, pathFilter, callback);
}
/**
* @return {!Array.<!Cache>}
*/
caches() {
const caches = new Array();
for (const cache of this._caches.values()) {
caches.push(cache);
}
return caches;
}
/**
* @override
*/
dispose() {
for (const cache of this._caches.values()) {
this._cacheRemoved(cache);
}
this._caches.clear();
if (this._enabled) {
this._securityOriginManager.removeEventListener(
SDK.SecurityOriginManager.Events.SecurityOriginAdded, this._securityOriginAdded, this);
this._securityOriginManager.removeEventListener(
SDK.SecurityOriginManager.Events.SecurityOriginRemoved, this._securityOriginRemoved, this);
}
}
_addOrigin(securityOrigin) {
this._loadCacheNames(securityOrigin);
if (this._isValidSecurityOrigin(securityOrigin)) {
this._storageAgent.trackCacheStorageForOrigin(securityOrigin);
}
}
/**
* @param {string} securityOrigin
*/
_removeOrigin(securityOrigin) {
for (const opaqueId of this._caches.keys()) {
const cache = this._caches.get(opaqueId);
if (cache.securityOrigin === securityOrigin) {
this._caches.delete(opaqueId);
this._cacheRemoved(cache);
}
}
if (this._isValidSecurityOrigin(securityOrigin)) {
this._storageAgent.untrackCacheStorageForOrigin(securityOrigin);
}
}
/**
* @param {string} securityOrigin
* @return {boolean}
*/
_isValidSecurityOrigin(securityOrigin) {
const parsedURL = Common.ParsedURL.fromString(securityOrigin);
return !!parsedURL && parsedURL.scheme.startsWith('http');
}
/**
* @param {string} securityOrigin
*/
async _loadCacheNames(securityOrigin) {
const caches = await this._cacheAgent.requestCacheNames(securityOrigin);
if (!caches) {
return;
}
this._updateCacheNames(securityOrigin, caches);
}
/**
* @param {string} securityOrigin
* @param {!Array} cachesJson
*/
_updateCacheNames(securityOrigin, cachesJson) {
/**
* @param {!Cache} cache
* @this {ServiceWorkerCacheModel}
*/
function deleteAndSaveOldCaches(cache) {
if (cache.securityOrigin === securityOrigin && !updatingCachesIds.has(cache.cacheId)) {
oldCaches.set(cache.cacheId, cache);
this._caches.delete(cache.cacheId);
}
}
/** @type {!Set<string>} */
const updatingCachesIds = new Set();
/** @type {!Map<string, !Cache>} */
const newCaches = new Map();
/** @type {!Map<string, !Cache>} */
const oldCaches = new Map();
for (const cacheJson of cachesJson) {
const cache = new Cache(this, cacheJson.securityOrigin, cacheJson.cacheName, cacheJson.cacheId);
updatingCachesIds.add(cache.cacheId);
if (this._caches.has(cache.cacheId)) {
continue;
}
newCaches.set(cache.cacheId, cache);
this._caches.set(cache.cacheId, cache);
}
this._caches.forEach(deleteAndSaveOldCaches, this);
newCaches.forEach(this._cacheAdded, this);
oldCaches.forEach(this._cacheRemoved, this);
}
/**
* @param {!Common.Event} event
*/
_securityOriginAdded(event) {
const securityOrigin = /** @type {string} */ (event.data);
this._addOrigin(securityOrigin);
}
/**
* @param {!Common.Event} event
*/
_securityOriginRemoved(event) {
const securityOrigin = /** @type {string} */ (event.data);
this._removeOrigin(securityOrigin);
}
/**
* @param {!Cache} cache
*/
_cacheAdded(cache) {
this.dispatchEventToListeners(Events.CacheAdded, {model: this, cache: cache});
}
/**
* @param {!Cache} cache
*/
_cacheRemoved(cache) {
this.dispatchEventToListeners(Events.CacheRemoved, {model: this, cache: cache});
}
/**
* @param {!Cache} cache
* @param {number} skipCount
* @param {number} pageSize
* @param {string} pathFilter
* @param {function(!Array<!Protocol.CacheStorage.DataEntry>, number)} callback
*/
async _requestEntries(cache, skipCount, pageSize, pathFilter, callback) {
const response =
await this._cacheAgent.invoke_requestEntries({cacheId: cache.cacheId, skipCount, pageSize, pathFilter});
if (response[Protocol.Error]) {
console.error('ServiceWorkerCacheAgent error while requesting entries: ', response[Protocol.Error]);
return;
}
callback(response.cacheDataEntries, response.returnCount);
}
/**
* @param {!Cache} cache
* @param {string} pathFilter
* @param {function(!Array<!Protocol.CacheStorage.DataEntry>, number)} callback
*/
async _requestAllEntries(cache, pathFilter, callback) {
const response = await this._cacheAgent.invoke_requestEntries({cacheId: cache.cacheId, pathFilter});
if (response[Protocol.Error]) {
console.error('ServiceWorkerCacheAgent error while requesting entries: ', response[Protocol.Error]);
return;
}
callback(response.cacheDataEntries, response.returnCount);
}
/**
* @param {string} origin
* @override
*/
cacheStorageListUpdated(origin) {
this._originsUpdated.add(origin);
this._throttler.schedule(() => {
const promises = Array.from(this._originsUpdated, origin => this._loadCacheNames(origin));
this._originsUpdated.clear();
return Promise.all(promises);
});
}
/**
* @param {string} origin
* @param {string} cacheName
* @override
*/
cacheStorageContentUpdated(origin, cacheName) {
this.dispatchEventToListeners(Events.CacheStorageContentUpdated, {origin: origin, cacheName: cacheName});
}
/**
* @param {string} origin
* @override
*/
indexedDBListUpdated(origin) {
}
/**
* @param {string} origin
* @param {string} databaseName
* @param {string} objectStoreName
* @override
*/
indexedDBContentUpdated(origin, databaseName, objectStoreName) {
}
}
/** @enum {symbol} */
export const Events = {
CacheAdded: Symbol('CacheAdded'),
CacheRemoved: Symbol('CacheRemoved'),
CacheStorageContentUpdated: Symbol('CacheStorageContentUpdated')
};
/**
* @unrestricted
*/
export class Cache {
/**
* @param {!ServiceWorkerCacheModel} model
* @param {string} securityOrigin
* @param {string} cacheName
* @param {string} cacheId
*/
constructor(model, securityOrigin, cacheName, cacheId) {
this._model = model;
this.securityOrigin = securityOrigin;
this.cacheName = cacheName;
this.cacheId = cacheId;
}
/**
* @param {!Cache} cache
* @return {boolean}
*/
equals(cache) {
return this.cacheId === cache.cacheId;
}
/**
* @override
* @return {string}
*/
toString() {
return this.securityOrigin + this.cacheName;
}
/**
* @param {string} url
* @param {!Array.<!SDK.NetworkRequest.NameValue>} requestHeaders
* @return {!Promise<?Protocol.CacheStorage.CachedResponse>}
*/
requestCachedResponse(url, requestHeaders) {
return this._model._cacheAgent.requestCachedResponse(this.cacheId, url, requestHeaders);
}
}
/* Legacy exported object */
self.SDK = self.SDK || {};
/* Legacy exported object */
SDK = SDK || {};
/** @constructor */
SDK.ServiceWorkerCacheModel = ServiceWorkerCacheModel;
/** @enum {symbol} */
SDK.ServiceWorkerCacheModel.Events = Events;
/** @constructor */
SDK.ServiceWorkerCacheModel.Cache = Cache;
SDK.SDKModel.register(SDK.ServiceWorkerCacheModel, SDK.Target.Capability.Storage, false);