diff --git a/shared/common/branchGraph/index.tsx b/shared/common/branchGraph/index.tsx index 9082c801..0d33aa02 100644 --- a/shared/common/branchGraph/index.tsx +++ b/shared/common/branchGraph/index.tsx @@ -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, @@ -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(", ")}` ); } @@ -864,6 +867,13 @@ const MigrationsPanel = observer(function MigrationsPanel({ }); } }) + .catch((err) => { + if (err instanceof MissingMigrationsError) { + closePanel(); + } else { + throw err; + } + }) .finally(() => setFetching(false)); } }, [history, fetching]); diff --git a/shared/studio/idbStore/index.ts b/shared/studio/idbStore/index.ts index 04fd6adc..0f605b26 100644 --- a/shared/studio/idbStore/index.ts +++ b/shared/studio/idbStore/index.ts @@ -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"; @@ -68,53 +68,83 @@ interface IDBStore extends DBSchema { }; } -const db = openDB("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("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 | null = null; +async function retryingIDBRequest( + request: (db: IDBPDatabase) => Promise +): Promise { + 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 @@ -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( @@ -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( @@ -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[] = []; - 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[] = []; + 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( @@ -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 @@ -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( @@ -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( @@ -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; + }); } diff --git a/shared/studio/state/instance.ts b/shared/studio/state/instance.ts index 4d88dce7..6363a8c8 100644 --- a/shared/studio/state/instance.ts +++ b/shared/studio/state/instance.ts @@ -15,7 +15,7 @@ import { frozen, } from "mobx-keystone"; -import {AuthenticationError} from "edgedb"; +import {AuthenticationError, DuplicateDatabaseDefinitionError} from "edgedb"; import {Options} from "edgedb/dist/options"; import {AdminUIFetchConnection} from "edgedb/dist/fetchConn"; @@ -250,18 +250,24 @@ export class InstanceState extends Model({ runInAction(() => (this.creatingExampleDB = true)); try { const schemaScript = await exampleSchema; - await this.defaultConnection!.execute(`create database _example`); - const exampleConn = new Connection({ - config: frozen({ - serverUrl: this.serverUrl, - authToken: this.authToken!, - database: "_example", - user: this.authUsername ?? this.roles![0], - }), - serverVersion: frozen(this.serverVersion), - }); - await exampleConn.execute(schemaScript); - await this.fetchInstanceInfo(); + try { + await this.defaultConnection!.execute(`create database _example`); + const exampleConn = new Connection({ + config: frozen({ + serverUrl: this.serverUrl, + authToken: this.authToken!, + database: "_example", + user: this.authUsername ?? this.roles![0], + }), + serverVersion: frozen(this.serverVersion), + }); + await exampleConn.execute(schemaScript); + } catch (err) { + if (!(err instanceof DuplicateDatabaseDefinitionError)) { + throw err; + } + } + await this.fetchDatabaseInfo(); } finally { runInAction(() => (this.creatingExampleDB = false)); } diff --git a/shared/studio/tabs/dataview/state/index.ts b/shared/studio/tabs/dataview/state/index.ts index 76ccfab2..78737349 100644 --- a/shared/studio/tabs/dataview/state/index.ts +++ b/shared/studio/tabs/dataview/state/index.ts @@ -948,10 +948,13 @@ export class DataInspector extends Model({ return; } - const dbState = dbCtx.get(this)!; - this.runningDataFetch?.abortController.abort(); + const dbState = dbCtx.get(this); + if (!dbState) { + return; + } + this.runningDataFetch = {abortController: new AbortController(), offset}; dbState.setLoadingTab(DataView, true); diff --git a/shared/studio/tabs/queryEditor/state/index.ts b/shared/studio/tabs/queryEditor/state/index.ts index 5340d5a9..6d975d84 100644 --- a/shared/studio/tabs/queryEditor/state/index.ts +++ b/shared/studio/tabs/queryEditor/state/index.ts @@ -819,7 +819,6 @@ export class QueryEditor extends Model({ }); return {success: true, capabilities, status}; } catch (e: any) { - console.error(e); this.addHistoryCell({ queryData, timestamp, diff --git a/shared/studio/tabs/schema/renderers/module.tsx b/shared/studio/tabs/schema/renderers/module.tsx index be5c7ee0..d098719f 100644 --- a/shared/studio/tabs/schema/renderers/module.tsx +++ b/shared/studio/tabs/schema/renderers/module.tsx @@ -26,7 +26,10 @@ export function ModuleRenderer({ if (isSticky) { const observer = new IntersectionObserver( ([e]) => { - if (e.boundingClientRect.top < e.rootBounds!.height / 2) { + if ( + e.rootBounds && + e.boundingClientRect.top < e.rootBounds.height / 2 + ) { setIsStuck(e.intersectionRatio < 1); } }, diff --git a/shared/studio/tabs/schema/state/textView.ts b/shared/studio/tabs/schema/state/textView.ts index 81405383..90352d84 100644 --- a/shared/studio/tabs/schema/state/textView.ts +++ b/shared/studio/tabs/schema/state/textView.ts @@ -144,8 +144,8 @@ export class SchemaTextView extends Model({ } getRenderHeight(index: number) { - const item = this.renderListItems.itemsList[index].item; - return item.schemaType === "Module" + const item = this.renderListItems.itemsList[index]?.item; + return !item || item.schemaType === "Module" ? 42 : this.renderHeights.get(item.id) ?? 42; }