@@ -5,52 +5,48 @@ import type { MetadataSchema, PathConfig, TranslationEntry } from "../types";
55import { getLingoDir } from "../utils/path-helpers" ;
66import { logger } from "../utils/logger" ;
77
8- // Special key for storing stats
98const STATS_KEY = "__stats__" ;
10-
11- // Metadata directory names for each environment
129const METADATA_DIR_DEV = "metadata-dev" ;
1310const METADATA_DIR_BUILD = "metadata-build" ;
1411
1512/**
16- * Opens an LMDB database connection at the given path.
13+ * Opens a short-lived LMDB connection.
14+ *
15+ * Short-lived over singleton: bundlers (Webpack/Next.js) spawn isolated workers
16+ * that can't share a single connection. LMDB's MVCC handles concurrent access.
1717 */
18- function openDatabase ( dbPath : string ) : RootDatabase {
18+ function openDatabaseConnection ( dbPath : string ) : RootDatabase {
1919 fs . mkdirSync ( dbPath , { recursive : true } ) ;
2020
21- // Build mode: disable fsync - metadata is deleted immediately after build, so durability is not needed.
22- // Dev mode: keep sync enabled for consistency during long-running sessions.
2321 const isBuildMode = dbPath . endsWith ( METADATA_DIR_BUILD ) ;
2422
2523 try {
2624 return open ( {
2725 path : dbPath ,
2826 compression : true ,
27+ // Build: skip fsync (data is ephemeral). Dev: sync for durability.
2928 noSync : isBuildMode ,
3029 } ) ;
3130 } catch ( error ) {
3231 const message = error instanceof Error ? error . message : String ( error ) ;
33- throw new Error (
34- `Failed to open LMDB metadata database at ${ dbPath } . Error: ${ message } ` ,
35- ) ;
32+ throw new Error ( `Failed to open LMDB at ${ dbPath } : ${ message } ` ) ;
3633 }
3734}
3835
3936/**
40- * Safely close database connection .
37+ * Releases file handles to allow directory cleanup (avoids EBUSY/EPERM on Windows) .
4138 */
42- async function closeDatabase ( db : RootDatabase , dbPath : string ) : Promise < void > {
39+ async function closeDatabaseConnection (
40+ db : RootDatabase ,
41+ dbPath : string ,
42+ ) : Promise < void > {
4343 try {
4444 await db . close ( ) ;
4545 } catch ( e ) {
4646 logger . debug ( `Error closing database at ${ dbPath } : ${ e } ` ) ;
4747 }
4848}
4949
50- /**
51- * Read all entries from an open database.
52- * Internal helper - does not manage connection lifecycle.
53- */
5450function readEntriesFromDb ( db : RootDatabase ) : MetadataSchema {
5551 const entries : Record < string , TranslationEntry > = { } ;
5652
@@ -84,62 +80,46 @@ export function createEmptyMetadata(): MetadataSchema {
8480 } ;
8581}
8682
87- /**
88- * Load metadata from LMDB database.
89- */
9083export async function loadMetadata ( dbPath : string ) : Promise < MetadataSchema > {
91- const db = openDatabase ( dbPath ) ;
84+ const db = openDatabaseConnection ( dbPath ) ;
9285 try {
9386 return readEntriesFromDb ( db ) ;
9487 } finally {
95- await closeDatabase ( db , dbPath ) ;
88+ await closeDatabaseConnection ( db , dbPath ) ;
9689 }
9790}
9891
9992/**
100- * Save translation entries to the metadata database .
93+ * Persists translation entries to LMDB .
10194 *
102- * LMDB handles concurrency via MVCC, so multiple processes can write safely.
103- *
104- * @param dbPath - Path to the LMDB database directory
105- * @param entries - Translation entries to add/update
106- * @returns The updated metadata schema
95+ * Uses transactionSync to batch all writes into a single commit.
96+ * Async transactions are slow in Vite (~80-100ms) due to setImmediate scheduling.
10797 */
10898export async function saveMetadata (
10999 dbPath : string ,
110100 entries : TranslationEntry [ ] ,
111- ) : Promise < MetadataSchema > {
112- const db = openDatabase ( dbPath ) ;
101+ ) : Promise < void > {
102+ const db = openDatabaseConnection ( dbPath ) ;
103+
113104 try {
114- await db . transaction ( ( ) => {
105+ db . transactionSync ( ( ) => {
115106 for ( const entry of entries ) {
116- db . put ( entry . hash , entry ) ;
107+ db . putSync ( entry . hash , entry ) ;
117108 }
118109
119- // Count entries explicitly (excluding stats key) for clarity
120- let entryCount = 0 ;
121- for ( const { key } of db . getRange ( ) ) {
122- if ( key !== STATS_KEY ) {
123- entryCount ++ ;
124- }
125- }
126-
127- const stats = {
110+ const totalKeys = db . getKeysCount ( ) ;
111+ const entryCount =
112+ db . get ( STATS_KEY ) !== undefined ? totalKeys - 1 : totalKeys ;
113+ db . putSync ( STATS_KEY , {
128114 totalEntries : entryCount ,
129115 lastUpdated : new Date ( ) . toISOString ( ) ,
130- } ;
131- db . put ( STATS_KEY , stats ) ;
116+ } ) ;
132117 } ) ;
133-
134- return readEntriesFromDb ( db ) ;
135118 } finally {
136- await closeDatabase ( db , dbPath ) ;
119+ await closeDatabaseConnection ( db , dbPath ) ;
137120 }
138121}
139122
140- /**
141- * Clean up the metadata database directory.
142- */
143123export function cleanupExistingMetadata ( metadataDbPath : string ) : void {
144124 logger . debug ( `Cleaning up metadata database: ${ metadataDbPath } ` ) ;
145125
@@ -164,12 +144,6 @@ export function cleanupExistingMetadata(metadataDbPath: string): void {
164144 }
165145}
166146
167- /**
168- * Get the absolute path to the metadata database directory
169- *
170- * @param config - Config with sourceRoot, lingoDir, and environment
171- * @returns Absolute path to metadata database directory
172- */
173147export function getMetadataPath ( config : PathConfig ) : string {
174148 const dirname =
175149 config . environment === "development"
0 commit comments