Skip to content

Commit

Permalink
Various small bugfixes (#400)
Browse files Browse the repository at this point in the history
* Catch error and redirect if example database creation is tried twice

* Attempt to retry IDB requests if the db was closed by the browser

It seems chrome and maybe safari might unexpectedly close the db sometimes: jakearchibald/idb#229

* Refresh the branch graph if it's found to be outdated when fetching migrations info

* Fixes for various small issues caught by sentry
  • Loading branch information
jaclarke authored Jan 16, 2025
1 parent 42b4841 commit 7537414
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 135 deletions.
12 changes: 11 additions & 1 deletion shared/common/branchGraph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export const BranchGraphContext = createContext<{
) => Promise<{script: string; sdl: string | null}[]>;
}>(null!);

class MissingMigrationsError extends Error {}

export const BranchGraph = observer(function BranchGraph({
instanceId,
instanceState,
Expand Down Expand Up @@ -176,7 +178,8 @@ export const BranchGraph = observer(function BranchGraph({
.filter((name) => !migrations.has(name));

if (missingMigrations.length) {
throw new Error(
setRefreshing(true);
throw new MissingMigrationsError(
`Migrations not found for ${missingMigrations.join(", ")}`
);
}
Expand Down Expand Up @@ -864,6 +867,13 @@ const MigrationsPanel = observer(function MigrationsPanel({
});
}
})
.catch((err) => {
if (err instanceof MissingMigrationsError) {
closePanel();
} else {
throw err;
}
})
.finally(() => setFetching(false));
}
}, [history, fetching]);
Expand Down
272 changes: 157 additions & 115 deletions shared/studio/idbStore/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {openDB, DBSchema} from "idb";
import {openDB, DBSchema, IDBPDatabase} from "idb";
import {ProtocolVersion} from "edgedb/dist/ifaces";
import {StoredSchemaData} from "../state/database";
import {StoredSessionStateData} from "../state/sessionState";
Expand Down Expand Up @@ -68,53 +68,83 @@ interface IDBStore extends DBSchema {
};
}

const db = openDB<IDBStore>("EdgeDBStudio", 4, {
upgrade(db, oldVersion) {
switch (oldVersion) {
// @ts-ignore fallthrough
case 0: {
db.createObjectStore("schemaData").createIndex(
"byInstanceId",
"instanceId"
);
}
// @ts-ignore fallthrough
case 1: {
db.createObjectStore("queryHistory", {
keyPath: ["instanceId", "dbName", "timestamp"],
}).createIndex("byInstanceId", "instanceId");
db.createObjectStore("replHistory", {
keyPath: ["instanceId", "dbName", "timestamp"],
}).createIndex("byInstanceId", "instanceId");
function _initDB() {
return openDB<IDBStore>("EdgeDBStudio", 4, {
upgrade(db, oldVersion) {
switch (oldVersion) {
// @ts-ignore fallthrough
case 0: {
db.createObjectStore("schemaData").createIndex(
"byInstanceId",
"instanceId"
);
}
// @ts-ignore fallthrough
case 1: {
db.createObjectStore("queryHistory", {
keyPath: ["instanceId", "dbName", "timestamp"],
}).createIndex("byInstanceId", "instanceId");
db.createObjectStore("replHistory", {
keyPath: ["instanceId", "dbName", "timestamp"],
}).createIndex("byInstanceId", "instanceId");

db.createObjectStore("queryResultData");
}
// @ts-ignore fallthrough
case 2: {
db.createObjectStore("sessionState", {
keyPath: ["instanceId", "dbName"],
});
db.createObjectStore("queryResultData");
}
// @ts-ignore fallthrough
case 2: {
db.createObjectStore("sessionState", {
keyPath: ["instanceId", "dbName"],
});
}
// @ts-ignore fallthrough
case 3: {
db.createObjectStore("aiPlaygroundChatHistory", {
keyPath: ["instanceId", "dbName", "timestamp"],
}).createIndex("byInstanceId", "instanceId");
}
}
// @ts-ignore fallthrough
case 3: {
db.createObjectStore("aiPlaygroundChatHistory", {
keyPath: ["instanceId", "dbName", "timestamp"],
}).createIndex("byInstanceId", "instanceId");
},
});
}

let _db: IDBPDatabase<IDBStore> | null = null;
async function retryingIDBRequest<T>(
request: (db: IDBPDatabase<IDBStore>) => Promise<T>
): Promise<T> {
let i = 3;
while (true) {
i--;
if (!_db) {
_db = await _initDB();
}
try {
return await request(_db);
} catch (err) {
if (
i === 0 ||
!(
err instanceof DOMException &&
(err.message.includes("closing") || err.message.includes("closed"))
)
) {
throw err;
}
_db = null;
}
},
});
}
}

// session state

export async function fetchSessionState(instanceId: string, dbName: string) {
return (
(await (await db).get("sessionState", [instanceId, dbName]))?.data ?? null
return retryingIDBRequest(
async (db) =>
(await db.get("sessionState", [instanceId, dbName]))?.data ?? null
);
}

export async function storeSessionState(data: SessionStateData) {
await (await db).put("sessionState", data);
await retryingIDBRequest((db) => db.put("sessionState", data));
}

// query / repl history
Expand All @@ -125,15 +155,17 @@ async function _storeHistoryItem(
item: QueryHistoryItem,
resultData?: QueryResultData
) {
const tx = (await db).transaction([storeId, "queryResultData"], "readwrite");
return retryingIDBRequest(async (db) => {
const tx = db.transaction([storeId, "queryResultData"], "readwrite");

return Promise.all([
tx.objectStore(storeId).add(item),
resultData
? tx.objectStore("queryResultData").add(resultData, itemId)
: null,
tx.done,
]);
return Promise.all([
tx.objectStore(storeId).add(item),
resultData
? tx.objectStore("queryResultData").add(resultData, itemId)
: null,
tx.done,
]);
});
}

export function storeQueryHistoryItem(
Expand All @@ -159,24 +191,26 @@ async function _fetchHistory(
fromTimestamp: number,
count = 50
) {
const tx = (await db).transaction(storeId, "readonly");
let cursor = await tx.store.openCursor(
IDBKeyRange.bound(
[instanceId, dbName, -Infinity],
[instanceId, dbName, fromTimestamp],
true,
true
),
"prev"
);
const items: QueryHistoryItem[] = [];
let i = 0;
while (cursor && i < count) {
items.push(cursor.value);
i++;
cursor = await cursor.continue();
}
return items;
return retryingIDBRequest(async (db) => {
const tx = db.transaction(storeId, "readonly");
let cursor = await tx.store.openCursor(
IDBKeyRange.bound(
[instanceId, dbName, -Infinity],
[instanceId, dbName, fromTimestamp],
true,
true
),
"prev"
);
const items: QueryHistoryItem[] = [];
let i = 0;
while (cursor && i < count) {
items.push(cursor.value);
i++;
cursor = await cursor.continue();
}
return items;
});
}

async function _clearHistory(
Expand All @@ -185,27 +219,29 @@ async function _clearHistory(
dbName: string,
getResultDataId: (item: QueryHistoryItem) => string | null
) {
const tx = (await db).transaction([storeId, "queryResultData"], "readwrite");
let cursor = await tx
.objectStore(storeId)
.openCursor(
IDBKeyRange.bound(
[instanceId, dbName, -Infinity],
[instanceId, dbName, Infinity]
)
);
const deletes: Promise<any>[] = [];
while (cursor) {
const currentItem = cursor;
const resultDataId = getResultDataId(currentItem.value);
if (resultDataId) {
deletes.push(tx.objectStore("queryResultData").delete(resultDataId));
return retryingIDBRequest(async (db) => {
const tx = db.transaction([storeId, "queryResultData"], "readwrite");
let cursor = await tx
.objectStore(storeId)
.openCursor(
IDBKeyRange.bound(
[instanceId, dbName, -Infinity],
[instanceId, dbName, Infinity]
)
);
const deletes: Promise<any>[] = [];
while (cursor) {
const currentItem = cursor;
const resultDataId = getResultDataId(currentItem.value);
if (resultDataId) {
deletes.push(tx.objectStore("queryResultData").delete(resultDataId));
}
deletes.push(currentItem.delete());
cursor = await cursor.continue();
}
deletes.push(currentItem.delete());
cursor = await cursor.continue();
}
deletes.push(tx.done);
return await Promise.all(deletes);
deletes.push(tx.done);
return await Promise.all(deletes);
});
}

export function fetchQueryHistory(
Expand Down Expand Up @@ -247,7 +283,7 @@ export function clearReplHistory(
}

export async function fetchResultData(itemId: string) {
return (await db).get("queryResultData", itemId);
return retryingIDBRequest((db) => db.get("queryResultData", itemId));
}

// schema data
Expand All @@ -257,14 +293,16 @@ export async function storeSchemaData(
instanceId: string,
data: StoredSchemaData
) {
await (
await db
).put("schemaData", {instanceId, data}, `${instanceId}/${dbName}`);
await retryingIDBRequest((db) =>
db.put("schemaData", {instanceId, data}, `${instanceId}/${dbName}`)
);
}

export async function fetchSchemaData(dbName: string, instanceId: string) {
const result = await (await db).get("schemaData", `${instanceId}/${dbName}`);
return result?.data;
return retryingIDBRequest(async (db) => {
const result = await db.get("schemaData", `${instanceId}/${dbName}`);
return result?.data;
});
}

export async function cleanupOldSchemaDataForInstance(
Expand All @@ -274,20 +312,22 @@ export async function cleanupOldSchemaDataForInstance(
const currentDbKeys = new Set(
currentDbNames.map((dbName) => `${instanceId}/${dbName}`)
);
const tx = (await db).transaction("schemaData", "readwrite");
const dbKeys = await tx.store.index("byInstanceId").getAllKeys(instanceId);
await Promise.all([
...dbKeys
.filter((dbKey) => !currentDbKeys.has(dbKey))
.map((dbKey) => tx.store.delete(dbKey)),
tx.done,
]);
await retryingIDBRequest(async (db) => {
const tx = db.transaction("schemaData", "readwrite");
const dbKeys = await tx.store.index("byInstanceId").getAllKeys(instanceId);
return Promise.all([
...dbKeys
.filter((dbKey) => !currentDbKeys.has(dbKey))
.map((dbKey) => tx.store.delete(dbKey)),
tx.done,
]);
});
}

// ai playground chat

export async function storeAIPlaygroundChatItem(item: AIPlaygroundChatItem) {
await (await db).add("aiPlaygroundChatHistory", item);
await retryingIDBRequest((db) => db.add("aiPlaygroundChatHistory", item));
}

export async function fetchAIPlaygroundChatHistory(
Expand All @@ -296,22 +336,24 @@ export async function fetchAIPlaygroundChatHistory(
fromTimestamp: number,
count = 50
) {
const tx = (await db).transaction("aiPlaygroundChatHistory", "readonly");
let cursor = await tx.store.openCursor(
IDBKeyRange.bound(
[instanceId, dbName, -Infinity],
[instanceId, dbName, fromTimestamp],
true,
true
),
"prev"
);
const items: AIPlaygroundChatItem[] = [];
let i = 0;
while (cursor && i < count) {
items.push(cursor.value);
i++;
cursor = await cursor.continue();
}
return items;
return retryingIDBRequest(async (db) => {
const tx = db.transaction("aiPlaygroundChatHistory", "readonly");
let cursor = await tx.store.openCursor(
IDBKeyRange.bound(
[instanceId, dbName, -Infinity],
[instanceId, dbName, fromTimestamp],
true,
true
),
"prev"
);
const items: AIPlaygroundChatItem[] = [];
let i = 0;
while (cursor && i < count) {
items.push(cursor.value);
i++;
cursor = await cursor.continue();
}
return items;
});
}
Loading

0 comments on commit 7537414

Please sign in to comment.