1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154

content / browser / resources / indexed_db / database.ts [blame]

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import './transaction_table.js';

import {CustomElement} from 'chrome://resources/js/custom_element.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import type {UnguessableToken} from 'chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-webui.js';

import type {BucketClientInfo} from './bucket_client_info.mojom-webui.js';
import {getTemplate} from './database.html.js';
import {IdbInternalsHandler} from './indexed_db_internals.mojom-webui.js';
import type {IdbDatabaseMetadata, IdbTransactionMetadata} from './indexed_db_internals_types.mojom-webui.js';
import type {ExecutionContextToken} from './tokens.mojom-webui.js';
import type {IndexedDbTransactionTable} from './transaction_table.js';

export class IndexedDbDatabase extends CustomElement {
  clients: BucketClientInfo[];

  static override get template() {
    return getTemplate();
  }

  // Similar to CustomElement.$, but asserts that the element exists.
  $a<T extends HTMLElement = HTMLElement>(query: string): T {
    return this.getRequiredElement<T>(query);
  }

  // Setter for `data` property. Updates the component contents with the
  // provided metadata.
  set data(metadata: IdbDatabaseMetadata) {
    const openDatabasesElement = this.$a('.open-databases');
    const openConnectionElement = this.$a('.connection-count.open');
    const activeConnectionElement = this.$a('.connection-count.active');
    const pendingConnectionElement = this.$a('.connection-count.pending');

    openDatabasesElement.textContent = mojoString16ToString(metadata.name);

    openConnectionElement.hidden = metadata.connectionCount === 0n;
    openConnectionElement.querySelector('.value')!.textContent =
        metadata.connectionCount.toString();

    activeConnectionElement.hidden = metadata.activeOpenDelete === 0n;
    activeConnectionElement.querySelector('.value')!.textContent =
        metadata.activeOpenDelete.toString();

    pendingConnectionElement.hidden = metadata.pendingOpenDelete === 0n;
    pendingConnectionElement.querySelector('.value')!.textContent =
        metadata.pendingOpenDelete.toString();

    this.groupAndShowTransactionsByClient(metadata.transactions);
  }

  private groupAndShowTransactionsByClient(transactions:
                                               IdbTransactionMetadata[]) {
    const groupedTransactions = new Map<string, IdbTransactionMetadata[]>();
    for (const transaction of transactions) {
      const client = transaction.clientToken;
      if (!groupedTransactions.has(client)) {
        groupedTransactions.set(client, []);
      }
      groupedTransactions.get(client)!.push(transaction);
    }

    const transactionsBlockElement = this.$a('#transactions');
    transactionsBlockElement.textContent = '';
    for (const [clientToken, clientTransactions] of groupedTransactions) {
      const container = this.createClientTransactionsContainer(
          clientToken, clientTransactions);
      container.classList.add('metadata-list-item');
      transactionsBlockElement.appendChild(container);
    }
  }

  // Creates a div containing an instantiation of the client metadata template
  // and a table of transactions.
  private createClientTransactionsContainer(
      clientToken: string,
      transactions: IdbTransactionMetadata[]): HTMLElement {
    const clientMetadataTemplate =
        this.$a<HTMLTemplateElement>('#client-metadata');
    const clientMetadata =
        clientMetadataTemplate.content.cloneNode(true) as DocumentFragment;
    clientMetadata.querySelector('.client-id')!.textContent = clientToken;
    clientMetadata.querySelector('.control.inspect')!.addEventListener(
        'click', () => {
          // If there are non-zero clients, inspecting any of them should have
          // the same effect.
          const client = this.getClientsMatchingToken(clientToken).pop();
          if (!client) {
            return;
          }
          IdbInternalsHandler.getRemote()
              .inspectClient(client)
              .then(message => {
                if (message.error) {
                  console.error(message.error);
                }
              })
              .catch(errorMsg => console.error(errorMsg));
        });
    const transactionTable =
        document.createElement('indexeddb-transaction-table') as
        IndexedDbTransactionTable;
    transactionTable.transactions = transactions;
    const container = document.createElement('div');
    container.appendChild(clientMetadata);
    container.appendChild(transactionTable);
    return container;
  }

  // Returns the list of clients whose `documentToken` or `contextToken` matches
  // the supplied `token`.
  private getClientsMatchingToken(token: string): BucketClientInfo[] {
    const matchedClients: BucketClientInfo[] = [];
    for (const client of this.clients) {
      const tokenValue = client.documentToken ?
          client.documentToken.value :
          this.getExecutionContextTokenValue(client.contextToken);
      if (tokenValue && this.tokenToHexString(tokenValue) === token) {
        matchedClients.push(client);
      }
    }
    return matchedClients;
  }

  private getExecutionContextTokenValue(token: ExecutionContextToken):
      UnguessableToken {
    if (token.localFrameToken) {
      return token.localFrameToken.value;
    }
    if (token.dedicatedWorkerToken) {
      return token.dedicatedWorkerToken.value;
    }
    if (token.serviceWorkerToken) {
      return token.serviceWorkerToken.value;
    }
    if (token.sharedWorkerToken) {
      return token.sharedWorkerToken.value;
    }
    throw new Error('Unrecognized ExecutionContextToken');
  }

  // This is the equivalent of `base::UnguessableToken::ToString()`.
  private tokenToHexString(token: UnguessableToken) {
    // Return the concatenation of the upper-case hexadecimal representations
    // of high and low, both padded to be 16 characters long.
    return token.high.toString(16).padStart(16, '0').toUpperCase() +
        token.low.toString(16).padStart(16, '0').toUpperCase();
  }
}

customElements.define('indexeddb-database', IndexedDbDatabase);