feat(standalone/import): improve importing speed

This commit is contained in:
Elian Doran
2026-03-27 18:27:19 +02:00
parent 22c86cf3b5
commit 91d526b15f
4 changed files with 73 additions and 11 deletions

View File

@@ -122,6 +122,45 @@ function createRoute(router: BrowserRouter) {
};
}
/**
* Async variant of createRoute for handlers that return Promises (e.g. import).
* Uses transactionalAsync (manual BEGIN/COMMIT/ROLLBACK) instead of the synchronous
* transactional() wrapper, which would commit an empty transaction immediately when
* passed an async callback.
*/
function createAsyncRoute(router: BrowserRouter) {
return (method: HttpMethod, path: string, _middleware: any[], handler: (req: any, res: any) => Promise<unknown>, resultHandler?: ((req: any, res: any, result: unknown) => unknown) | null) => {
router.register(method, path, (req: BrowserRequest) => {
return getContext().init(async () => {
setContextFromHeaders(req);
const expressLikeReq = toExpressLikeReq(req);
const mockRes = createMockExpressResponse();
const result = await getSql().transactionalAsync(() => handler(expressLikeReq, mockRes));
// If the handler used the mock response (e.g. image routes that call res.send()),
// return it as a raw response so BrowserRouter doesn't JSON-serialize it.
if (mockRes._used) {
return {
[RAW_RESPONSE]: true as const,
status: mockRes._status,
headers: mockRes._headers,
body: mockRes._body
};
}
if (resultHandler) {
// Create a minimal response object that captures what apiResultHandler sets.
const res = createResultHandlerResponse();
resultHandler(expressLikeReq, res, result);
return res.result;
}
return result;
});
});
};
}
/**
* Creates a mock Express response object that captures calls to set(), send(), sendStatus(), etc.
* Used for route handlers (like image routes) that write directly to the response.
@@ -220,7 +259,7 @@ export function registerRoutes(router: BrowserRouter): void {
const apiRoute = createApiRoute(router, true);
routes.buildSharedApiRoutes({
route: createRoute(router),
asyncRoute: createRoute(router),
asyncRoute: createAsyncRoute(router),
apiRoute,
asyncApiRoute: createApiRoute(router, false),
apiResultHandler,

View File

@@ -501,9 +501,12 @@ export default class BrowserSqlProvider implements DatabaseProvider {
// Helper function to execute within a transaction
const executeTransaction = (beginStatement: string, ...args: unknown[]): T => {
// If we're already in a transaction, use SAVEPOINTs for nesting
// This mimics better-sqlite3's behavior
if (self._inTransaction) {
// If we're already in a transaction (either tracked via JS flag or via actual SQLite
// autocommit state), use SAVEPOINTs for nesting — this handles the case where a manual
// BEGIN was issued directly (e.g. transactionalAsync) without going through transaction().
const sqliteInTransaction = self.db?.pointer !== undefined
&& (self.sqlite3!.capi as any).sqlite3_get_autocommit(self.db!.pointer) === 0;
if (self._inTransaction || sqliteInTransaction) {
const savepointName = `sp_${++savepointCounter}_${Date.now()}`;
self.db!.exec(`SAVEPOINT ${savepointName}`);
try {

View File

@@ -7,7 +7,7 @@ interface ImportRequest<P> extends Request<P> {
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import enexImportService from "../../services/import/enex.js";
// import enexImportService from "../../services/import/enex.js";
import opmlImportService from "../../services/import/opml.js";
import singleImportService from "../../services/import/single.js";
import zipImportService from "../../services/import/zip.js";
@@ -64,12 +64,12 @@ async function importNotesToBranch(req: ImportRequest<{ parentNoteId: string }>)
return importResult;
}
} else if (extension === ".enex" && options.explodeArchives) {
const importResult = await enexImportService.importEnex(taskContext, file, parentNote);
if (!Array.isArray(importResult)) {
note = importResult;
} else {
return importResult;
}
// const importResult = await enexImportService.importEnex(taskContext, file, parentNote);
// if (!Array.isArray(importResult)) {
// note = importResult;
// } else {
// return importResult;
// }
} else {
note = singleImportService.importSingleFile(taskContext, file, parentNote);
}

View File

@@ -312,6 +312,26 @@ export class SqlService {
}
}
/**
* Async-safe transaction wrapper for use in Web Workers and other single-threaded async contexts.
* Uses manual BEGIN/COMMIT/ROLLBACK because the synchronous `transactional()` cannot await promises.
*/
async transactionalAsync<T>(func: () => Promise<T>): Promise<T> {
this.execute("BEGIN IMMEDIATE");
try {
const result = await func();
this.execute("COMMIT");
if (!this.dbConnection.inTransaction) {
this.params.onTransactionCommit();
}
return result;
} catch (e) {
this.execute("ROLLBACK");
this.params.onTransactionRollback();
throw e;
}
}
fillParamList(paramIds: string[] | Set<string>, truncate = true) {
if ("length" in paramIds && paramIds.length === 0) {
return;