mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 12:25:37 +02:00
feat(standalone/import): improve importing speed
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user