skills/local-first/references/devtools-debugging.md

304 lines
6.9 KiB
Markdown

---
title: DevTools and Debugging
description: Browser DevTools inspection and debugging utilities for local-first storage, OPFS, sync events, and CRDT state
tags:
[devtools, debugging, indexeddb, opfs, inspection, dexie, yjs, sync, storage]
---
## IndexedDB Inspection
### Chrome / Edge
1. Open DevTools (F12)
2. Navigate to **Application** tab
3. Expand **IndexedDB** in the left sidebar
4. Click a database to see object stores
5. Click an object store to browse records
6. Right-click a record to edit or delete
### Firefox
1. Open DevTools (F12)
2. Navigate to **Storage** tab
3. Expand **Indexed DB** in the left sidebar
4. Browse databases, object stores, and records
### Programmatic Listing
```ts
async function listDatabases(): Promise<IDBDatabaseInfo[]> {
if (indexedDB.databases) {
return indexedDB.databases();
}
return [];
}
async function inspectDatabase(name: string): Promise<{
stores: string[];
version: number;
}> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name);
request.onsuccess = () => {
const db = request.result;
resolve({
stores: Array.from(db.objectStoreNames),
version: db.version,
});
db.close();
};
request.onerror = () => reject(request.error);
});
}
```
## OPFS Inspection
OPFS is not visible in standard DevTools storage panels. Use browser extensions or programmatic access.
### OPFS Explorer Extension
Available for Chrome and Firefox. Adds an **OPFS Explorer** panel to DevTools that displays the OPFS file tree, file sizes, and allows downloading files.
Install from: Chrome Web Store or Firefox Add-ons (search "OPFS Explorer").
### OPFS Viewer
A standalone web-based tool for inspecting OPFS contents. Useful when extensions are not available.
### Programmatic OPFS Inspection
```ts
async function listOPFSFiles(
dir?: FileSystemDirectoryHandle,
path = '',
): Promise<{ path: string; kind: string }[]> {
const root = dir ?? (await navigator.storage.getDirectory());
const entries: { path: string; kind: string }[] = [];
for await (const [name, handle] of root.entries()) {
const fullPath = path ? `${path}/${name}` : name;
entries.push({ path: fullPath, kind: handle.kind });
if (handle.kind === 'directory') {
const children = await listOPFSFiles(
handle as FileSystemDirectoryHandle,
fullPath,
);
entries.push(...children);
}
}
return entries;
}
```
## Storage Overview Utility
```ts
async function debugStorageInfo(): Promise<{
estimate: { usage: number; quota: number; percentUsed: number };
persisted: boolean;
databases: IDBDatabaseInfo[];
opfsFiles: { path: string; kind: string }[];
}> {
const estimate = await navigator.storage.estimate();
const persisted = (await navigator.storage.persisted?.()) ?? false;
let databases: IDBDatabaseInfo[] = [];
if (indexedDB.databases) {
databases = await indexedDB.databases();
}
let opfsFiles: { path: string; kind: string }[] = [];
try {
opfsFiles = await listOPFSFiles();
} catch {
// OPFS not available
}
return {
estimate: {
usage: estimate.usage ?? 0,
quota: estimate.quota ?? 0,
percentUsed: ((estimate.usage ?? 0) / (estimate.quota ?? 1)) * 100,
},
persisted,
databases,
opfsFiles,
};
}
```
## Dexie.js Debug Mode
Dexie provides built-in logging for IndexedDB operations.
```ts
import Dexie from 'dexie';
Dexie.debug = true;
Dexie.debug = 'dexie';
```
With `Dexie.debug = true`, all IndexedDB transactions, queries, and mutations are logged to the console with timing information. Set to `'dexie'` for verbose output including internal operations.
Disable in production:
```ts
if (import.meta.env.DEV) {
Dexie.debug = true;
}
```
## Yjs Document State Inspector
```ts
import { type Doc as YDoc } from 'yjs';
function inspectYDoc(doc: YDoc): {
clientID: number;
guid: string;
sharedTypes: { name: string; type: string; length: number }[];
stateVector: Map<number, number>;
updateSize: number;
} {
const { encodeStateAsUpdate, encodeStateVector, decodeStateVector } =
require('yjs') as typeof import('yjs');
const update = encodeStateAsUpdate(doc);
const sv = decodeStateVector(encodeStateVector(doc));
const sharedTypes: { name: string; type: string; length: number }[] = [];
doc.share.forEach((type, name) => {
sharedTypes.push({
name,
type: type.constructor.name,
length: type.length,
});
});
return {
clientID: doc.clientID,
guid: doc.guid,
sharedTypes,
stateVector: sv,
updateSize: update.byteLength,
};
}
```
## Sync Event Replay
Instrument sync providers to capture and replay synchronization events for debugging.
### Sync Event Logger
```ts
type SyncDirection = 'send' | 'receive';
interface SyncEvent {
direction: SyncDirection;
timestamp: number;
size: number;
type: string;
data?: Uint8Array;
}
class SyncEventLogger {
private events: SyncEvent[] = [];
private maxEvents: number;
constructor(maxEvents = 1000) {
this.maxEvents = maxEvents;
}
log(direction: SyncDirection, type: string, data?: Uint8Array): void {
if (this.events.length >= this.maxEvents) {
this.events.shift();
}
this.events.push({
direction,
timestamp: performance.now(),
size: data?.byteLength ?? 0,
type,
data: data ? new Uint8Array(data) : undefined,
});
}
getEvents(): readonly SyncEvent[] {
return this.events;
}
getSummary(): {
totalSent: number;
totalReceived: number;
bytesSent: number;
bytesReceived: number;
eventTypes: Record<string, number>;
} {
let totalSent = 0;
let totalReceived = 0;
let bytesSent = 0;
let bytesReceived = 0;
const eventTypes: Record<string, number> = {};
for (const event of this.events) {
if (event.direction === 'send') {
totalSent++;
bytesSent += event.size;
} else {
totalReceived++;
bytesReceived += event.size;
}
eventTypes[event.type] = (eventTypes[event.type] ?? 0) + 1;
}
return { totalSent, totalReceived, bytesSent, bytesReceived, eventTypes };
}
clear(): void {
this.events = [];
}
}
```
### Instrumenting a WebSocket Sync Provider
```ts
function instrumentWebSocket(
ws: WebSocket,
logger: SyncEventLogger,
): WebSocket {
const originalSend = ws.send.bind(ws);
ws.send = (data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
const size =
data instanceof ArrayBuffer
? data.byteLength
: typeof data === 'string'
? data.length
: 0;
logger.log(
'send',
'ws-message',
data instanceof Uint8Array ? data : undefined,
);
originalSend(data);
};
ws.addEventListener('message', (event: MessageEvent) => {
const data = event.data;
logger.log(
'receive',
'ws-message',
data instanceof ArrayBuffer ? new Uint8Array(data) : undefined,
);
});
return ws;
}
```