| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; |
| import type * as Protocol from '../../generated/protocol.js'; |
| import * as Common from '../common/common.js'; |
| import * as i18n from '../i18n/i18n.js'; |
| import type * as Platform from '../platform/platform.js'; |
| |
| import type {NameValue} from './NetworkRequest.js'; |
| import {SDKModel} from './SDKModel.js'; |
| import {type BucketEvent, Events as StorageBucketsModelEvents, StorageBucketsModel} from './StorageBucketsModel.js'; |
| import {Capability, type Target} from './Target.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Text in Service Worker Cache Model |
| * @example {https://cache} PH1 |
| * @example {error message} PH2 |
| */ |
| serviceworkercacheagentError: '`ServiceWorkerCacheAgent` error deleting cache entry {PH1} in cache: {PH2}', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('core/sdk/ServiceWorkerCacheModel.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export class ServiceWorkerCacheModel extends SDKModel<EventTypes> implements ProtocolProxyApi.StorageDispatcher { |
| readonly cacheAgent: ProtocolProxyApi.CacheStorageApi; |
| readonly #storageAgent: ProtocolProxyApi.StorageApi; |
| readonly #storageBucketModel: StorageBucketsModel; |
| |
| readonly #caches = new Map<string, Cache>(); |
| readonly #storageKeysTracked = new Set<string>(); |
| readonly #storageBucketsUpdated = new Set<Protocol.Storage.StorageBucket>(); |
| readonly #throttler = new Common.Throttler.Throttler(2000); |
| #enabled = false; |
| |
| // Used by tests to remove the Throttler timeout. |
| #scheduleAsSoonAsPossible = false; |
| |
| /** |
| * Invariant: This #model can only be constructed on a ServiceWorker target. |
| */ |
| constructor(target: Target) { |
| super(target); |
| target.registerStorageDispatcher(this); |
| |
| this.cacheAgent = target.cacheStorageAgent(); |
| this.#storageAgent = target.storageAgent(); |
| this.#storageBucketModel = (target.model(StorageBucketsModel) as StorageBucketsModel); |
| } |
| |
| enable(): void { |
| if (this.#enabled) { |
| return; |
| } |
| |
| this.#storageBucketModel.addEventListener(StorageBucketsModelEvents.BUCKET_ADDED, this.storageBucketAdded, this); |
| this.#storageBucketModel.addEventListener( |
| StorageBucketsModelEvents.BUCKET_REMOVED, this.storageBucketRemoved, this); |
| |
| for (const storageBucket of this.#storageBucketModel.getBuckets()) { |
| this.addStorageBucket(storageBucket.bucket); |
| } |
| this.#enabled = true; |
| } |
| |
| clearForStorageKey(storageKey: string): void { |
| for (const [opaqueId, cache] of this.#caches.entries()) { |
| if (cache.storageKey === storageKey) { |
| this.#caches.delete((opaqueId)); |
| this.cacheRemoved((cache)); |
| } |
| } |
| for (const storageBucket of this.#storageBucketModel.getBucketsForStorageKey(storageKey)) { |
| void this.loadCacheNames(storageBucket.bucket); |
| } |
| } |
| |
| refreshCacheNames(): void { |
| for (const cache of this.#caches.values()) { |
| this.cacheRemoved(cache); |
| } |
| this.#caches.clear(); |
| const storageBuckets = this.#storageBucketModel.getBuckets(); |
| for (const storageBucket of storageBuckets) { |
| void this.loadCacheNames(storageBucket.bucket); |
| } |
| } |
| |
| async deleteCache(cache: Cache): Promise<void> { |
| const response = await this.cacheAgent.invoke_deleteCache({cacheId: cache.cacheId}); |
| if (response.getError()) { |
| console.error(`ServiceWorkerCacheAgent error deleting cache ${cache.toString()}: ${response.getError()}`); |
| return; |
| } |
| this.#caches.delete(cache.cacheId); |
| this.cacheRemoved(cache); |
| } |
| |
| async deleteCacheEntry(cache: Cache, request: string): Promise<void> { |
| const response = await this.cacheAgent.invoke_deleteEntry({cacheId: cache.cacheId, request}); |
| if (response.getError()) { |
| Common.Console.Console.instance().error(i18nString( |
| UIStrings.serviceworkercacheagentError, {PH1: cache.toString(), PH2: String(response.getError())})); |
| return; |
| } |
| } |
| |
| loadCacheData( |
| cache: Cache, skipCount: number, pageSize: number, pathFilter: string, |
| callback: (arg0: Protocol.CacheStorage.DataEntry[], arg1: number) => void): void { |
| void this.requestEntries(cache, skipCount, pageSize, pathFilter, callback); |
| } |
| |
| loadAllCacheData( |
| cache: Cache, pathFilter: string, |
| callback: (arg0: Protocol.CacheStorage.DataEntry[], arg1: number) => void): void { |
| void this.requestAllEntries(cache, pathFilter, callback); |
| } |
| |
| caches(): Cache[] { |
| return [...this.#caches.values()]; |
| } |
| |
| override dispose(): void { |
| for (const cache of this.#caches.values()) { |
| this.cacheRemoved(cache); |
| } |
| this.#caches.clear(); |
| if (this.#enabled) { |
| this.#storageBucketModel.removeEventListener( |
| StorageBucketsModelEvents.BUCKET_ADDED, this.storageBucketAdded, this); |
| this.#storageBucketModel.removeEventListener( |
| StorageBucketsModelEvents.BUCKET_REMOVED, this.storageBucketRemoved, this); |
| } |
| } |
| |
| private addStorageBucket(storageBucket: Protocol.Storage.StorageBucket): void { |
| void this.loadCacheNames(storageBucket); |
| if (!this.#storageKeysTracked.has(storageBucket.storageKey)) { |
| this.#storageKeysTracked.add(storageBucket.storageKey); |
| void this.#storageAgent.invoke_trackCacheStorageForStorageKey({storageKey: storageBucket.storageKey}); |
| } |
| } |
| |
| private removeStorageBucket(storageBucket: Protocol.Storage.StorageBucket): void { |
| let storageKeyCount = 0; |
| for (const [opaqueId, cache] of this.#caches.entries()) { |
| if (storageBucket.storageKey === cache.storageKey) { |
| storageKeyCount++; |
| } |
| if (cache.inBucket(storageBucket)) { |
| storageKeyCount--; |
| this.#caches.delete((opaqueId)); |
| this.cacheRemoved((cache)); |
| } |
| } |
| if (storageKeyCount === 0) { |
| this.#storageKeysTracked.delete(storageBucket.storageKey); |
| void this.#storageAgent.invoke_untrackCacheStorageForStorageKey({storageKey: storageBucket.storageKey}); |
| } |
| } |
| |
| private async loadCacheNames(storageBucket: Protocol.Storage.StorageBucket): Promise<void> { |
| const response = await this.cacheAgent.invoke_requestCacheNames({storageBucket}); |
| if (response.getError()) { |
| return; |
| } |
| this.updateCacheNames(storageBucket, response.caches); |
| } |
| |
| private updateCacheNames(storageBucket: Protocol.Storage.StorageBucket, cachesJson: Protocol.CacheStorage.Cache[]): |
| void { |
| function deleteAndSaveOldCaches(this: ServiceWorkerCacheModel, cache: Cache): void { |
| if (cache.inBucket(storageBucket) && !updatingCachesIds.has(cache.cacheId)) { |
| oldCaches.set(cache.cacheId, cache); |
| this.#caches.delete(cache.cacheId); |
| } |
| } |
| |
| const updatingCachesIds = new Set<string>(); |
| const newCaches = new Map<string, Cache>(); |
| const oldCaches = new Map<string, Cache>(); |
| |
| for (const cacheJson of cachesJson) { |
| const storageBucket = cacheJson.storageBucket ?? |
| this.#storageBucketModel.getDefaultBucketForStorageKey(cacheJson.storageKey)?.bucket; |
| if (!storageBucket) { |
| continue; |
| } |
| const cache = new Cache(this, storageBucket, 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); |
| } |
| |
| private storageBucketAdded({data: {bucketInfo: {bucket}}}: Common.EventTarget.EventTargetEvent<BucketEvent>): void { |
| this.addStorageBucket(bucket); |
| } |
| |
| private storageBucketRemoved({data: {bucketInfo: {bucket}}}: Common.EventTarget.EventTargetEvent<BucketEvent>): void { |
| this.removeStorageBucket(bucket); |
| } |
| |
| private cacheAdded(cache: Cache): void { |
| this.dispatchEventToListeners(Events.CACHE_ADDED, {model: this, cache}); |
| } |
| |
| private cacheRemoved(cache: Cache): void { |
| this.dispatchEventToListeners(Events.CACHE_REMOVED, {model: this, cache}); |
| } |
| |
| private async requestEntries( |
| cache: Cache, skipCount: number, pageSize: number, pathFilter: string, |
| callback: (arg0: Protocol.CacheStorage.DataEntry[], arg1: number) => void): Promise<void> { |
| const response = |
| await this.cacheAgent.invoke_requestEntries({cacheId: cache.cacheId, skipCount, pageSize, pathFilter}); |
| if (response.getError()) { |
| console.error('ServiceWorkerCacheAgent error while requesting entries: ', response.getError()); |
| return; |
| } |
| callback(response.cacheDataEntries, response.returnCount); |
| } |
| |
| private async requestAllEntries( |
| cache: Cache, pathFilter: string, |
| callback: (arg0: Protocol.CacheStorage.DataEntry[], arg1: number) => void): Promise<void> { |
| const response = await this.cacheAgent.invoke_requestEntries({cacheId: cache.cacheId, pathFilter}); |
| if (response.getError()) { |
| console.error('ServiceWorkerCacheAgent error while requesting entries: ', response.getError()); |
| return; |
| } |
| callback(response.cacheDataEntries, response.returnCount); |
| } |
| |
| cacheStorageListUpdated({bucketId}: Protocol.Storage.CacheStorageListUpdatedEvent): void { |
| const storageBucket = this.#storageBucketModel.getBucketById(bucketId)?.bucket; |
| if (storageBucket) { |
| this.#storageBucketsUpdated.add(storageBucket); |
| |
| void this.#throttler.schedule( |
| () => { |
| const promises = |
| Array.from(this.#storageBucketsUpdated, storageBucket => this.loadCacheNames(storageBucket)); |
| this.#storageBucketsUpdated.clear(); |
| return Promise.all(promises); |
| }, |
| this.#scheduleAsSoonAsPossible ? Common.Throttler.Scheduling.AS_SOON_AS_POSSIBLE : |
| Common.Throttler.Scheduling.DEFAULT); |
| } |
| } |
| |
| cacheStorageContentUpdated({bucketId, cacheName}: Protocol.Storage.CacheStorageContentUpdatedEvent): void { |
| const storageBucket = this.#storageBucketModel.getBucketById(bucketId)?.bucket; |
| if (storageBucket) { |
| this.dispatchEventToListeners(Events.CACHE_STORAGE_CONTENT_UPDATED, {storageBucket, cacheName}); |
| } |
| } |
| |
| attributionReportingTriggerRegistered(_event: Protocol.Storage.AttributionReportingTriggerRegisteredEvent): void { |
| } |
| |
| indexedDBListUpdated(_event: Protocol.Storage.IndexedDBListUpdatedEvent): void { |
| } |
| |
| indexedDBContentUpdated(_event: Protocol.Storage.IndexedDBContentUpdatedEvent): void { |
| } |
| |
| interestGroupAuctionEventOccurred(_event: Protocol.Storage.InterestGroupAuctionEventOccurredEvent): void { |
| } |
| |
| interestGroupAccessed(_event: Protocol.Storage.InterestGroupAccessedEvent): void { |
| } |
| |
| interestGroupAuctionNetworkRequestCreated(_event: Protocol.Storage.InterestGroupAuctionNetworkRequestCreatedEvent): |
| void { |
| } |
| |
| sharedStorageAccessed(_event: Protocol.Storage.SharedStorageAccessedEvent): void { |
| } |
| |
| sharedStorageWorkletOperationExecutionFinished( |
| _event: Protocol.Storage.SharedStorageWorkletOperationExecutionFinishedEvent): void { |
| } |
| |
| storageBucketCreatedOrUpdated(_event: Protocol.Storage.StorageBucketCreatedOrUpdatedEvent): void { |
| } |
| |
| storageBucketDeleted(_event: Protocol.Storage.StorageBucketDeletedEvent): void { |
| } |
| |
| setThrottlerSchedulesAsSoonAsPossibleForTest(): void { |
| this.#scheduleAsSoonAsPossible = true; |
| } |
| |
| attributionReportingSourceRegistered(_event: Protocol.Storage.AttributionReportingSourceRegisteredEvent): void { |
| } |
| |
| attributionReportingReportSent(_event: Protocol.Storage.AttributionReportingReportSentEvent): void { |
| } |
| |
| attributionReportingVerboseDebugReportSent(_event: Protocol.Storage.AttributionReportingVerboseDebugReportSentEvent): |
| void { |
| } |
| } |
| |
| export const enum Events { |
| CACHE_ADDED = 'CacheAdded', |
| CACHE_REMOVED = 'CacheRemoved', |
| CACHE_STORAGE_CONTENT_UPDATED = 'CacheStorageContentUpdated', |
| } |
| |
| export interface CacheEvent { |
| model: ServiceWorkerCacheModel; |
| cache: Cache; |
| } |
| |
| export interface CacheStorageContentUpdatedEvent { |
| storageBucket: Protocol.Storage.StorageBucket; |
| cacheName: string; |
| } |
| |
| export interface EventTypes { |
| [Events.CACHE_ADDED]: CacheEvent; |
| [Events.CACHE_REMOVED]: CacheEvent; |
| [Events.CACHE_STORAGE_CONTENT_UPDATED]: CacheStorageContentUpdatedEvent; |
| } |
| |
| export class Cache { |
| readonly #model: ServiceWorkerCacheModel; |
| storageKey: string; |
| storageBucket: Protocol.Storage.StorageBucket; |
| cacheName: string; |
| cacheId: Protocol.CacheStorage.CacheId; |
| |
| constructor( |
| model: ServiceWorkerCacheModel, storageBucket: Protocol.Storage.StorageBucket, cacheName: string, |
| cacheId: Protocol.CacheStorage.CacheId) { |
| this.#model = model; |
| this.storageBucket = storageBucket; |
| this.storageKey = storageBucket.storageKey; |
| this.cacheName = cacheName; |
| this.cacheId = cacheId; |
| } |
| |
| inBucket(storageBucket: Protocol.Storage.StorageBucket): boolean { |
| return this.storageKey === storageBucket.storageKey && this.storageBucket.name === storageBucket.name; |
| } |
| |
| equals(cache: Cache): boolean { |
| return this.cacheId === cache.cacheId; |
| } |
| |
| toString(): string { |
| return this.storageKey + this.cacheName; |
| } |
| |
| async requestCachedResponse(url: Platform.DevToolsPath.UrlString, requestHeaders: NameValue[]): |
| Promise<Protocol.CacheStorage.CachedResponse|null> { |
| const response = await this.#model.cacheAgent.invoke_requestCachedResponse( |
| {cacheId: this.cacheId, requestURL: url, requestHeaders}); |
| if (response.getError()) { |
| return null; |
| } |
| return response.response; |
| } |
| } |
| |
| SDKModel.register(ServiceWorkerCacheModel, {capabilities: Capability.STORAGE, autostart: false}); |