| // 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 'chrome://resources/js/jstemplate_compiled.js'; |
| import './database.js'; |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js'; |
| import {getRequiredElement} from 'chrome://resources/js/util.js'; |
| import type {String16} from 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js'; |
| import type {Time} from 'chrome://resources/mojo/mojo/public/mojom/base/time.mojom-webui.js'; |
| import type {Origin} from 'chrome://resources/mojo/url/mojom/origin.mojom-webui.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 type {SchemefulSite} from './schemeful_site.mojom-webui.js'; |
| |
| // TODO: This comes from components/flags_ui/resources/flags.ts. It should be |
| // extracted into a tools/typescript/definitions/jstemplate.d.ts file, and |
| // include that as part of build_webui()'s ts_definitions, instead of copying it |
| // here. |
| declare global { |
| class JsEvalContext { |
| constructor(data: any); |
| } |
| |
| function jstProcess(context: JsEvalContext, template: HTMLElement): void; |
| function jstGetTemplate(templateName: string): HTMLElement; |
| } |
| |
| // Methods to convert mojo values to strings or to objects with readable |
| // toString values. Accessible to jstemplate html code. |
| const stringifyMojo = { |
| time(mojoTime: Time): Date { |
| // The JS Date() is based off of the number of milliseconds since |
| // the UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| |
| // of the base::Time (represented in mojom.Time) represents the |
| // number of microseconds since the Windows FILETIME epoch |
| // (1601-01-01 00:00:00 UTC). This computes the final JS time by |
| // computing the epoch delta and the conversion from microseconds to |
| // milliseconds. |
| const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0); |
| const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0); |
| // |epochDeltaInMs| equals to |
| // base::Time::kTimeTToMicrosecondsOffset. |
| const epochDeltaInMs = unixEpoch - windowsEpoch; |
| const timeInMs = Number(mojoTime.internalValue) / 1000; |
| |
| return new Date(timeInMs - epochDeltaInMs); |
| }, |
| |
| string16(mojoString16: String16): string { |
| return mojoString16ToString(mojoString16); |
| }, |
| |
| scope(mojoScope: String16[]): string { |
| return `[${mojoScope.map(s => stringifyMojo.string16(s)).join(', ')}]`; |
| }, |
| |
| origin(mojoOrigin: Origin): string { |
| const {scheme, host, port} = mojoOrigin; |
| const portSuf = (port === 0 ? '' : `:${port}`); |
| return `${scheme}://${host}${portSuf}`; |
| }, |
| |
| schemefulSite(mojoSite: SchemefulSite): string { |
| return stringifyMojo.origin(mojoSite.siteAsOrigin); |
| }, |
| }; |
| |
| 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]; |
| 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.idbBucketId = this.idbBucketId; |
| 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 template = jstGetTemplate('indexeddb-list-template'); |
| getRequiredElement('indexeddb-list').appendChild(template); |
| const currentOriginFilter = () => window.location.hash.replace('#', ''); |
| const processTemplate = () => jstProcess( |
| new JsEvalContext({ |
| partitions, |
| stringifyMojo, |
| originFilter: currentOriginFilter(), |
| }), |
| template); |
| 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); |