Skip to content

Instantly share code, notes, and snippets.

@AtkinsSJ
Created June 18, 2026 21:31
Show Gist options
  • Select an option

  • Save AtkinsSJ/506a66edb8aa796b20ce24fd930f693b to your computer and use it in GitHub Desktop.

Select an option

Save AtkinsSJ/506a66edb8aa796b20ce24fd930f693b to your computer and use it in GitHub Desktop.
IndexedDB test page
<!DOCTYPE html>
<meta charset="utf-8">
<title>IndexedDB DevTools Manual Test</title>
<style>
:root {
color-scheme: light dark;
--bg: #f7f7f4;
--panel: #ffffff;
--panel-2: #f0f3f5;
--text: #15181b;
--muted: #5f6a72;
--line: #cfd6dc;
--accent: #0f6b63;
--accent-2: #285f9f;
--danger: #b3261e;
--code: #eef3f6;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #151719;
--panel: #202427;
--panel-2: #292f33;
--text: #edf1f2;
--muted: #a5afb6;
--line: #3d464d;
--accent: #5db4a9;
--accent-2: #78a8d8;
--danger: #ff8f86;
--code: #151b1f;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font: 14px/1.45 system-ui, sans-serif;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
background: var(--panel);
}
h1 {
margin: 0;
font-size: 18px;
font-weight: 700;
}
main {
display: grid;
grid-template-columns: minmax(220px, 0.9fr) minmax(260px, 1fr) minmax(360px, 1.6fr);
gap: 12px;
padding: 12px;
}
section {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
overflow: hidden;
}
section > h2 {
margin: 0;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
background: var(--panel-2);
font-size: 14px;
}
.content {
padding: 12px;
}
.toolbar,
.row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.toolbar {
margin-bottom: 10px;
}
label {
display: grid;
gap: 4px;
min-width: 0;
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
input,
textarea,
select,
button {
color: var(--text);
font: inherit;
}
input,
textarea,
select {
width: 100%;
min-width: 0;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
padding: 7px 8px;
}
textarea {
min-height: 140px;
resize: vertical;
font-family: ui-monospace, monospace;
font-size: 13px;
line-height: 1.35;
tab-size: 2;
}
button {
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel-2);
padding: 7px 10px;
cursor: pointer;
}
button.primary {
border-color: var(--accent);
background: var(--accent);
color: white;
}
button.danger {
border-color: var(--danger);
color: var(--danger);
}
button:disabled {
cursor: default;
opacity: 0.55;
}
.list {
display: grid;
gap: 6px;
margin-top: 10px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
padding: 8px 10px;
text-align: left;
}
.item.selected {
border-color: var(--accent);
box-shadow: inset 3px 0 0 var(--accent);
}
.item-name {
overflow-wrap: anywhere;
font-weight: 600;
}
.item-meta {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.grid {
display: grid;
gap: 10px;
}
.two {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.records {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.records th,
.records td {
border-bottom: 1px solid var(--line);
padding: 8px;
text-align: left;
vertical-align: top;
}
.records th {
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.records pre,
#log {
overflow: auto;
margin: 0;
border-radius: 6px;
background: var(--code);
padding: 8px;
font-family: ui-monospace, monospace;
font-size: 12px;
line-height: 1.35;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
#status {
color: var(--muted);
font-size: 12px;
}
.split {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: end;
}
@media (max-width: 980px) {
main {
grid-template-columns: 1fr;
}
}
</style>
<header>
<h1>IndexedDB DevTools Manual Test</h1>
<div class="toolbar">
<button id="refreshButton">Refresh</button>
<button id="seedButton" class="primary">Seed</button>
<button id="clearLogButton">Clear Log</button>
<span id="status"></span>
</div>
</header>
<main>
<section>
<h2>Databases</h2>
<div class="content">
<form id="databaseForm" class="split">
<label>
Name
<input id="databaseName" autocomplete="off" value="devtools-fixture">
</label>
<button class="primary">Add</button>
</form>
<form id="renameDatabaseForm" class="split">
<label>
Rename to
<input id="renameDatabaseName" autocomplete="off">
</label>
<button disabled>Rename</button>
</form>
<div class="toolbar">
<button id="deleteDatabaseButton" class="danger" disabled>Delete</button>
<button id="reloadDatabaseButton" disabled>Reload</button>
</div>
<div id="databaseList" class="list"></div>
</div>
</section>
<section>
<h2>Tables</h2>
<div class="content">
<form id="storeForm" class="split">
<label>
Name
<input id="storeName" autocomplete="off" value="people">
</label>
<button class="primary" disabled>Add</button>
</form>
<form id="renameStoreForm" class="split">
<label>
Rename to
<input id="renameStoreName" autocomplete="off">
</label>
<button disabled>Rename</button>
</form>
<div class="toolbar">
<button id="deleteStoreButton" class="danger" disabled>Delete</button>
<button id="clearStoreButton" disabled>Clear</button>
</div>
<div id="storeList" class="list"></div>
</div>
</section>
<section>
<h2>Records</h2>
<div class="content grid">
<form id="recordForm" class="grid">
<div class="two">
<label>
Key JSON
<input id="recordKey" autocomplete="off" value="1">
</label>
<label>
Mode
<select id="recordMode">
<option value="put">Put</option>
<option value="add">Add</option>
</select>
</label>
</div>
<label>
Value JSON
<textarea id="recordValue">{
"name": "Ada",
"role": "Engineer"
}</textarea>
</label>
<div class="toolbar">
<button class="primary" disabled>Save</button>
<button id="deleteRecordButton" class="danger" type="button" disabled>Delete</button>
<button id="reloadRecordsButton" type="button" disabled>Reload Records</button>
</div>
</form>
<table class="records">
<thead>
<tr>
<th style="width: 35%">Key</th>
<th>Value</th>
</tr>
</thead>
<tbody id="recordRows"></tbody>
</table>
<pre id="log"></pre>
</div>
</section>
</main>
<script>
const state = {
databases: [],
selectedDatabase: "",
selectedStore: "",
selectedRecordKey: undefined,
pollTimer: 0,
};
const elements = {};
for (const id of [
"status", "databaseForm", "databaseName", "renameDatabaseForm", "renameDatabaseName",
"databaseList", "deleteDatabaseButton", "reloadDatabaseButton", "storeForm",
"storeName", "renameStoreForm", "renameStoreName", "storeList", "deleteStoreButton",
"clearStoreButton", "recordForm", "recordKey", "recordMode", "recordValue",
"deleteRecordButton", "reloadRecordsButton", "recordRows", "log",
"refreshButton", "seedButton", "clearLogButton"
]) {
elements[id] = document.getElementById(id);
}
function log(message) {
const time = new Date().toLocaleTimeString();
elements.log.textContent = `[${time}] ${message}\n${elements.log.textContent}`;
}
function setStatus(message) {
elements.status.textContent = message;
}
function serialize(value) {
return JSON.stringify(value, null, 2);
}
function parseJsonInput(input, label) {
try {
return JSON.parse(input.value);
} catch (error) {
input.focus();
throw new Error(`${label}: ${error.message}`);
}
}
function transactionDone(transaction) {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
});
}
function stringListToArray(list) {
const result = [];
for (let i = 0; i < list.length; ++i)
result.push(list.item(i));
return result;
}
function openDatabase(name, version, upgradeCallback) {
return new Promise((resolve, reject) => {
const request = version === undefined ? indexedDB.open(name) : indexedDB.open(name, version);
request.onupgradeneeded = event => upgradeCallback && upgradeCallback(request.result, request.transaction, event.oldVersion, event.newVersion);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error || new Error("Could not open database"));
request.onblocked = () => log(`Open blocked for ${name}`);
});
}
async function withDatabase(name, callback) {
const db = await openDatabase(name);
try {
return await callback(db);
} finally {
db.close();
}
}
async function listDatabases() {
if (indexedDB.databases) {
const databases = await indexedDB.databases();
return databases.map(database => ({
name: database.name || "",
version: database.version || 0,
})).filter(database => database.name).sort((a, b) => a.name.localeCompare(b.name));
}
return [];
}
async function createDatabase(name) {
const db = await openDatabase(name, 1);
db.close();
}
async function deleteDatabase(name) {
await new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(name);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error || new Error("Could not delete database"));
request.onblocked = () => log(`Delete blocked for ${name}`);
});
}
async function copyDatabase(sourceName, destinationName) {
if (sourceName === destinationName)
return;
const source = state.databases.find(database => database.name === sourceName);
if (!source)
throw new Error(`Unknown database: ${sourceName}`);
await deleteDatabase(destinationName);
const destination = await openDatabase(destinationName, 1, database => {
for (const store of source.stores)
database.createObjectStore(store.name);
});
destination.close();
for (const store of source.stores) {
for (const record of store.records)
await putRecord(destinationName, store.name, record.key, record.value, "put");
}
}
async function renameDatabase(sourceName, destinationName) {
await copyDatabase(sourceName, destinationName);
await deleteDatabase(sourceName);
}
async function createStore(databaseName, storeName) {
await withDatabase(databaseName, async db => {
if (db.objectStoreNames.contains(storeName))
return;
const nextVersion = db.version + 1;
db.close();
const upgraded = await openDatabase(databaseName, nextVersion, database => {
if (!database.objectStoreNames.contains(storeName))
database.createObjectStore(storeName);
});
upgraded.close();
});
}
async function renameStore(databaseName, oldName, newName) {
if (oldName === newName)
return;
await withDatabase(databaseName, async db => {
if (!db.objectStoreNames.contains(oldName))
throw new Error(`Unknown table: ${oldName}`);
if (db.objectStoreNames.contains(newName))
throw new Error(`Table already exists: ${newName}`);
const nextVersion = db.version + 1;
db.close();
const upgraded = await openDatabase(databaseName, nextVersion, (database, transaction) => {
transaction.objectStore(oldName).name = newName;
});
upgraded.close();
});
}
async function deleteStore(databaseName, storeName) {
await withDatabase(databaseName, async db => {
if (!db.objectStoreNames.contains(storeName))
return;
const nextVersion = db.version + 1;
db.close();
const upgraded = await openDatabase(databaseName, nextVersion, database => {
if (database.objectStoreNames.contains(storeName))
database.deleteObjectStore(storeName);
});
upgraded.close();
});
}
async function listStores(databaseName) {
if (!databaseName)
return [];
return await withDatabase(databaseName, db => stringListToArray(db.objectStoreNames));
}
async function putRecord(databaseName, storeName, key, value, mode) {
await withDatabase(databaseName, async db => {
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
if (mode === "add")
store.add(value, key);
else
store.put(value, key);
await transactionDone(transaction);
});
}
async function deleteRecord(databaseName, storeName, key) {
await withDatabase(databaseName, async db => {
const transaction = db.transaction(storeName, "readwrite");
transaction.objectStore(storeName).delete(key);
await transactionDone(transaction);
});
}
async function clearStore(databaseName, storeName) {
await withDatabase(databaseName, async db => {
const transaction = db.transaction(storeName, "readwrite");
transaction.objectStore(storeName).clear();
await transactionDone(transaction);
});
}
async function listRecords(databaseName, storeName) {
if (!databaseName || !storeName)
return [];
return await withDatabase(databaseName, db => {
return new Promise((resolve, reject) => {
const rows = [];
const transaction = db.transaction(storeName, "readonly");
const request = transaction.objectStore(storeName).openCursor();
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) {
resolve(rows);
return;
}
rows.push({ key: cursor.key, value: cursor.value });
cursor.continue();
};
request.onerror = () => reject(request.error || new Error("Could not read records"));
transaction.onerror = () => reject(transaction.error || new Error("Read transaction failed"));
});
});
}
function selectDatabase(name) {
state.selectedDatabase = name;
state.selectedStore = "";
state.selectedRecordKey = undefined;
elements.renameDatabaseName.value = name;
elements.renameStoreName.value = "";
render();
refresh();
}
function selectStore(name) {
state.selectedStore = name;
state.selectedRecordKey = undefined;
elements.renameStoreName.value = name;
render();
refresh();
}
function selectRecord(key, value) {
state.selectedRecordKey = key;
elements.recordKey.value = serialize(key);
elements.recordValue.value = serialize(value);
render();
}
function currentDatabase() {
return state.databases.find(database => database.name === state.selectedDatabase);
}
function render() {
const database = currentDatabase();
const databaseSelected = !!database;
const storeSelected = databaseSelected && !!state.selectedStore;
elements.deleteDatabaseButton.disabled = !databaseSelected;
elements.reloadDatabaseButton.disabled = !databaseSelected;
elements.renameDatabaseForm.querySelector("button").disabled = !databaseSelected;
elements.storeForm.querySelector("button").disabled = !databaseSelected;
elements.renameStoreForm.querySelector("button").disabled = !storeSelected;
elements.deleteStoreButton.disabled = !storeSelected;
elements.clearStoreButton.disabled = !storeSelected;
elements.recordForm.querySelector("button").disabled = !storeSelected;
elements.deleteRecordButton.disabled = !storeSelected || state.selectedRecordKey === undefined;
elements.reloadRecordsButton.disabled = !storeSelected;
elements.databaseList.replaceChildren(...state.databases.map(database => {
const button = document.createElement("button");
button.type = "button";
button.className = `item${database.name === state.selectedDatabase ? " selected" : ""}`;
button.onclick = () => selectDatabase(database.name);
const name = document.createElement("span");
name.className = "item-name";
name.textContent = database.name;
const meta = document.createElement("span");
meta.className = "item-meta";
meta.textContent = `v${database.version}`;
button.append(name, meta);
return button;
}));
const stores = database ? database.stores : [];
elements.storeList.replaceChildren(...stores.map(store => {
const button = document.createElement("button");
button.type = "button";
button.className = `item${store.name === state.selectedStore ? " selected" : ""}`;
button.onclick = () => selectStore(store.name);
const name = document.createElement("span");
name.className = "item-name";
name.textContent = store.name;
const meta = document.createElement("span");
meta.className = "item-meta";
meta.textContent = `${store.records.length}`;
button.append(name, meta);
return button;
}));
const selectedStore = stores.find(store => store.name === state.selectedStore);
const records = selectedStore ? selectedStore.records : [];
elements.recordRows.replaceChildren(...records.map(record => {
const row = document.createElement("tr");
row.onclick = () => selectRecord(record.key, record.value);
const key = document.createElement("td");
const keyText = document.createElement("pre");
keyText.textContent = serialize(record.key);
key.append(keyText);
const value = document.createElement("td");
const valueText = document.createElement("pre");
valueText.textContent = serialize(record.value);
value.append(valueText);
row.append(key, value);
return row;
}));
}
async function refresh() {
try {
const selectedDatabase = state.selectedDatabase;
const selectedStore = state.selectedStore;
const databases = await listDatabases();
for (const database of databases) {
database.stores = [];
try {
const stores = await listStores(database.name);
for (const storeName of stores) {
let records = [];
try {
records = await listRecords(database.name, storeName);
} catch (error) {
log(`Read ${database.name}/${storeName}: ${error.message}`);
}
database.stores.push({ name: storeName, records });
}
} catch (error) {
log(`Read ${database.name}: ${error.message}`);
}
}
state.databases = databases;
if (selectedDatabase && databases.some(database => database.name === selectedDatabase))
state.selectedDatabase = selectedDatabase;
else
state.selectedDatabase = databases[0] ? databases[0].name : "";
if (document.activeElement !== elements.renameDatabaseName)
elements.renameDatabaseName.value = state.selectedDatabase;
const database = currentDatabase();
const stores = database ? database.stores : [];
if (selectedStore && stores.some(store => store.name === selectedStore))
state.selectedStore = selectedStore;
else
state.selectedStore = stores[0] ? stores[0].name : "";
if (document.activeElement !== elements.renameStoreName)
elements.renameStoreName.value = state.selectedStore;
setStatus(`${databases.length} database${databases.length === 1 ? "" : "s"}`);
render();
} catch (error) {
setStatus(error.message);
log(error.message);
}
}
async function runAction(label, callback) {
try {
await callback();
log(label);
await refresh();
} catch (error) {
log(`${label}: ${error.message}`);
}
}
elements.databaseForm.onsubmit = event => {
event.preventDefault();
const name = elements.databaseName.value.trim();
if (!name)
return;
runAction(`Created database ${name}`, async () => {
await createDatabase(name);
state.selectedDatabase = name;
elements.renameDatabaseName.value = name;
});
};
elements.renameDatabaseForm.onsubmit = event => {
event.preventDefault();
const oldName = state.selectedDatabase;
const newName = elements.renameDatabaseName.value.trim();
if (!oldName || !newName)
return;
runAction(`Renamed database ${oldName} to ${newName}`, async () => {
await renameDatabase(oldName, newName);
state.selectedDatabase = newName;
});
};
elements.deleteDatabaseButton.onclick = () => {
const name = state.selectedDatabase;
if (!name)
return;
runAction(`Deleted database ${name}`, async () => {
await deleteDatabase(name);
state.selectedDatabase = "";
state.selectedStore = "";
state.selectedRecordKey = undefined;
});
};
elements.reloadDatabaseButton.onclick = () => refresh();
elements.storeForm.onsubmit = event => {
event.preventDefault();
const databaseName = state.selectedDatabase;
const storeName = elements.storeName.value.trim();
if (!databaseName || !storeName)
return;
runAction(`Created table ${databaseName}/${storeName}`, async () => {
await createStore(databaseName, storeName);
state.selectedStore = storeName;
elements.renameStoreName.value = storeName;
});
};
elements.renameStoreForm.onsubmit = event => {
event.preventDefault();
const databaseName = state.selectedDatabase;
const oldName = state.selectedStore;
const newName = elements.renameStoreName.value.trim();
if (!databaseName || !oldName || !newName)
return;
runAction(`Renamed table ${databaseName}/${oldName} to ${newName}`, async () => {
await renameStore(databaseName, oldName, newName);
state.selectedStore = newName;
});
};
elements.deleteStoreButton.onclick = () => {
const databaseName = state.selectedDatabase;
const storeName = state.selectedStore;
if (!databaseName || !storeName)
return;
runAction(`Deleted table ${databaseName}/${storeName}`, async () => {
await deleteStore(databaseName, storeName);
state.selectedStore = "";
state.selectedRecordKey = undefined;
});
};
elements.clearStoreButton.onclick = () => {
const databaseName = state.selectedDatabase;
const storeName = state.selectedStore;
if (!databaseName || !storeName)
return;
runAction(`Cleared table ${databaseName}/${storeName}`, () => clearStore(databaseName, storeName));
};
elements.recordForm.onsubmit = event => {
event.preventDefault();
const databaseName = state.selectedDatabase;
const storeName = state.selectedStore;
if (!databaseName || !storeName)
return;
runAction(`Saved record ${databaseName}/${storeName}`, async () => {
const key = parseJsonInput(elements.recordKey, "Key JSON");
const value = parseJsonInput(elements.recordValue, "Value JSON");
await putRecord(databaseName, storeName, key, value, elements.recordMode.value);
state.selectedRecordKey = key;
});
};
elements.deleteRecordButton.onclick = () => {
const databaseName = state.selectedDatabase;
const storeName = state.selectedStore;
if (!databaseName || !storeName || state.selectedRecordKey === undefined)
return;
const key = state.selectedRecordKey;
runAction(`Deleted record ${databaseName}/${storeName}`, async () => {
await deleteRecord(databaseName, storeName, key);
state.selectedRecordKey = undefined;
});
};
elements.reloadRecordsButton.onclick = () => refresh();
elements.refreshButton.onclick = () => refresh();
elements.clearLogButton.onclick = () => elements.log.textContent = "";
elements.seedButton.onclick = () => {
runAction("Seeded fixture data", async () => {
await deleteDatabase("devtools-fixture");
await createDatabase("devtools-fixture");
await createStore("devtools-fixture", "people");
await createStore("devtools-fixture", "tasks");
await putRecord("devtools-fixture", "people", 1, { name: "Ada", role: "Engineer" }, "put");
await putRecord("devtools-fixture", "people", 2, { name: "Grace", role: "Debugger" }, "put");
await putRecord("devtools-fixture", "tasks", "alpha", { title: "Inspect storage", done: false }, "put");
state.selectedDatabase = "devtools-fixture";
state.selectedStore = "people";
});
};
refresh();
state.pollTimer = setInterval(refresh, 1000);
window.onpagehide = () => clearInterval(state.pollTimer);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment