blob: 99892fa8ace13a00ab56fa3c770148579c62d989 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './database.js';
import {assert} from 'chrome://resources/js/assert.js';
import {getRequiredElement} from 'chrome://resources/js/util.js';
import {render} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {BucketId} from './bucket_id.mojom-webui.js';
import type {IndexedDbDatabase} from './database.js';
import type {IdbInternalsHandlerInterface, IdbPartitionMetadata} from './indexed_db_internals.mojom-webui.js';
import {IdbInternalsHandler} from './indexed_db_internals.mojom-webui.js';
import type {IdbBucketMetadata} from './indexed_db_internals_types.mojom-webui.js';
import {getHtml} from './indexeddb_list.html.js';
interface MojomResponse<T> {
error: string|null;
[key: string]: T|string|null;
}
function promisifyMojoResult<T>(
remotePromise: Promise<MojomResponse<T>>,
valueProp: keyof MojomResponse<T>): Promise<T> {
return new Promise((resolve, reject) => {
remotePromise.then((response: MojomResponse<T>) => {
if (response.error !== null) {
reject(response.error);
} else {
resolve(response[valueProp] as T);
}
});
});
}
class IdbInternalsRemote {
private handler: IdbInternalsHandlerInterface =
IdbInternalsHandler.getRemote();
getAllBucketsAcrossAllStorageKeys(): Promise<IdbPartitionMetadata[]> {
return promisifyMojoResult(
this.handler.getAllBucketsAcrossAllStorageKeys(), 'partitions');
}
stopMetadataRecording(bucketId: BucketId): Promise<IdbBucketMetadata[]> {
return promisifyMojoResult(
this.handler.stopMetadataRecording(bucketId), 'metadata');
}
}
const internalsRemote = new IdbInternalsRemote();
function initialize() {
internalsRemote.getAllBucketsAcrossAllStorageKeys()
.then(onStorageKeysReady)
.catch(errorMsg => console.error(errorMsg));
}
class BucketElement extends HTMLElement {
// this field is filled by the jstemplate annotations in the HTML code
idbBucketId: BucketId;
progressNode: HTMLElement;
connectionCountNode: HTMLElement;
seriesCurrentSnapshotIndex: number|null;
seriesData: IdbBucketMetadata[]|null;
constructor() {
super();
this.getNode(`.control.download`).addEventListener('click', () => {
// Show loading
this.progressNode.style.display = 'inline';
IdbInternalsHandler.getRemote()
.downloadBucketData(this.idbBucketId)
.then(this.onLoadComplete.bind(this))
.catch(errorMsg => console.error(errorMsg));
});
this.getNode(`.control.force-close`).addEventListener('click', () => {
// Show loading
this.progressNode.style.display = 'inline';
IdbInternalsHandler.getRemote()
.forceClose(this.idbBucketId)
.then(this.onLoadComplete.bind(this))
.catch(errorMsg => console.error(errorMsg));
});
this.getNode(`.control.start-record`).addEventListener('click', () => {
this.getNode(`.control.stop-record`).hidden = false;
this.getNode(`.control.start-record`).hidden = true;
IdbInternalsHandler.getRemote()
.startMetadataRecording(this.idbBucketId)
.then(this.onLoadComplete.bind(this))
.catch(errorMsg => console.error(errorMsg));
if (!this.getNode('.snapshots').hidden) {
this.getNode('.snapshots').hidden = true;
this.setRecordingSnapshot(null);
}
});
this.getNode(`.control.stop-record`).addEventListener('click', () => {
// Show loading
this.progressNode.style.display = 'inline';
this.getNode(`.control.start-record`).hidden = false;
this.getNode(`.control.stop-record`).hidden = true;
new IdbInternalsRemote()
.stopMetadataRecording(this.idbBucketId)
.then(this.onMetadataRecordingReady.bind(this))
.catch(errorMsg => console.error(errorMsg));
});
this.getNode('.snapshots input.slider')
.addEventListener('input', (event: Event) => {
const input = event.target as HTMLInputElement;
this.setRecordingSnapshot(parseInt(input.value));
});
this.getNode('.snapshots .prev').addEventListener('click', () => {
this.setRecordingSnapshot((this.seriesCurrentSnapshotIndex || 0) - 1);
});
this.getNode('.snapshots .next').addEventListener('click', () => {
this.setRecordingSnapshot((this.seriesCurrentSnapshotIndex || 0) + 1);
});
this.progressNode = this.getNode('.download-status');
this.connectionCountNode = this.getNode('.connection-count');
}
private setRecordingSnapshot(idx: number | null) {
this.getNode('.database-view').textContent = '';
if (!this.seriesData || idx === null ||
(idx < 0 || idx > this.seriesData.length - 1)) {
return;
}
const slider =
this.getNode<HTMLInputElement>('.snapshots input.slider');
this.seriesCurrentSnapshotIndex = idx;
const snapshot = this.seriesData[this.seriesCurrentSnapshotIndex];
if (snapshot === undefined) {
return;
}
slider.value = idx.toString();
slider.max = (this.seriesData.length - 1).toString();
this.getNode('.snapshots .current-snapshot').textContent = slider.value;
this.getNode('.snapshots .total-snapshots').textContent = slider.max;
this.getNode('.snapshots .snapshot-delta').textContent =
`+${snapshot.deltaRecordingStartMs}ms`;
for (const db of snapshot.databases || []) {
const dbView = document.createElement('indexeddb-database');
const dbElement = this.getNode('.database-view').appendChild(dbView) as
IndexedDbDatabase;
dbElement.clients = snapshot.clients;
dbElement.data = db;
}
}
private getNode<T extends HTMLElement>(selector: string) {
const controlNode = this.querySelector<T>(`${selector}`);
assert(controlNode);
return controlNode;
}
private onLoadComplete() {
this.progressNode.style.display = 'none';
this.connectionCountNode.innerText = '0';
}
private onMetadataRecordingReady(metadata: IdbBucketMetadata[]) {
this.seriesData = metadata;
this.onLoadComplete();
this.getNode('.snapshots').hidden = false;
this.getNode('.snapshots .controls').hidden = metadata.length === 0;
if (metadata.length === 0) {
this.setRecordingSnapshot(null);
this.getNode('.snapshots .message').innerText =
'No snapshots were captured.';
return;
}
this.getNode('.snapshots .message').innerText = '';
this.setRecordingSnapshot(0);
}
}
function onStorageKeysReady(partitions: IdbPartitionMetadata[]) {
const currentOriginFilter = () => window.location.hash.replace('#', '');
const processTemplate = () => {
render(
getHtml(partitions, currentOriginFilter()),
getRequiredElement('indexeddb-list'));
};
processTemplate();
// Re process the template when the origin filter is updated.
const originFilterInput =
document
.querySelector<HTMLInputElement>('#origin-filter')!;
originFilterInput.value = currentOriginFilter();
originFilterInput.addEventListener('input', (event: Event) => {
const input = event.target as HTMLInputElement;
window.location.hash = input.value;
processTemplate();
});
}
customElements.define('indexeddb-bucket', BucketElement);
document.addEventListener('DOMContentLoaded', initialize);