@@ -16,6 +16,7 @@ import type {
1616} from '@mastra/core/storage' ;
1717
1818import { DEFAULT_CONNECTION_TIMEOUT_MS } from './db' ;
19+ import { withClientWriteLock } from './db/write-lock' ;
1920import { LibSQLStore } from './index' ;
2021
2122export interface LibSQLFactoryStorageConfig {
@@ -99,27 +100,20 @@ function serializeDefault(value: string | number | boolean): string {
99100 return String ( value ) ;
100101}
101102
102- /** Simple FIFO in-process mutex serializing the single-writer paths. */
103- class Mutex {
104- #tail: Promise < unknown > = Promise . resolve ( ) ;
105-
106- run < T > ( fn : ( ) => Promise < T > ) : Promise < T > {
107- const result = this . #tail. then ( fn , fn ) ;
108- this . #tail = result . catch ( ( ) => { } ) ;
109- return result ;
110- }
111- }
112-
113103type LibSQLExecutor = Pick < Client , 'execute' > ;
104+ type WriteGate = < T > ( fn : ( ) => Promise < T > ) => Promise < T > ;
105+
106+ const runWithoutWriteLock : WriteGate = fn => fn ( ) ;
114107
115108class LibSQLFactoryStorageOps implements FactoryStorageOps {
116109 readonly #client: LibSQLExecutor ;
117110 readonly #schemas: Map < string , CollectionSchema > ;
118- readonly #writeMutex = new Mutex ( ) ;
111+ readonly #withWriteLock: WriteGate ;
119112
120- constructor ( client : LibSQLExecutor , schemas : Map < string , CollectionSchema > ) {
113+ constructor ( client : LibSQLExecutor , schemas : Map < string , CollectionSchema > , withWriteLock : WriteGate ) {
121114 this . #client = client ;
122115 this . #schemas = schemas ;
116+ this . #withWriteLock = withWriteLock ;
123117 }
124118
125119 #schema( collection : string ) : CollectionSchema {
@@ -291,7 +285,7 @@ class LibSQLFactoryStorageOps implements FactoryStorageOps {
291285 return this . #select< T > ( collection , where , opts ) ;
292286 }
293287
294- async insertOne < T extends Record < string , unknown > > ( collection : string , row : Partial < T > ) : Promise < T > {
288+ async # insertOne< T extends Record < string , unknown > > ( collection : string , row : Partial < T > ) : Promise < T > {
295289 const schema = this . #schema( collection ) ;
296290 const pk = primaryKeyOf ( schema ) ;
297291
@@ -320,7 +314,11 @@ class LibSQLFactoryStorageOps implements FactoryStorageOps {
320314 return inserted ;
321315 }
322316
323- async upsertOne < T extends Record < string , unknown > > (
317+ async insertOne < T extends Record < string , unknown > > ( collection : string , row : Partial < T > ) : Promise < T > {
318+ return this . #withWriteLock( ( ) => this . #insertOne< T > ( collection , row ) ) ;
319+ }
320+
321+ async #upsertOne< T extends Record < string , unknown > > (
324322 collection : string ,
325323 conflictKeys : string [ ] ,
326324 row : Partial < T > ,
@@ -340,14 +338,14 @@ class LibSQLFactoryStorageOps implements FactoryStorageOps {
340338 ) ,
341339 ) ;
342340 if ( Object . keys ( set ) . length > 0 ) {
343- await this . updateMany ( collection , { [ pk ] : existing [ pk ] as CollectionValue } , set ) ;
341+ await this . # updateMany( collection , { [ pk ] : existing [ pk ] as CollectionValue } , set ) ;
344342 }
345343 const updated = await this . findOne < T > ( collection , { [ pk ] : existing [ pk ] as CollectionValue } ) ;
346344 if ( ! updated ) continue ; // deleted concurrently; retry
347345 return updated ;
348346 }
349347 try {
350- return await this . insertOne < T > ( collection , row ) ;
348+ return await this . # insertOne< T > ( collection , row ) ;
351349 } catch ( error ) {
352350 if ( ! ( error instanceof UniqueViolationError ) ) throw error ;
353351 lastError = error ; // lost an insert race; retry as update
@@ -356,7 +354,15 @@ class LibSQLFactoryStorageOps implements FactoryStorageOps {
356354 throw lastError ?? new Error ( `LibSQLFactoryStorage: upsert into '${ collection } ' did not converge` ) ;
357355 }
358356
359- async updateMany ( collection : string , where : CollectionWhere , set : Record < string , unknown > ) : Promise < number > {
357+ async upsertOne < T extends Record < string , unknown > > (
358+ collection : string ,
359+ conflictKeys : string [ ] ,
360+ row : Partial < T > ,
361+ ) : Promise < T > {
362+ return this . #withWriteLock( ( ) => this . #upsertOne< T > ( collection , conflictKeys , row ) ) ;
363+ }
364+
365+ async #updateMany( collection : string , where : CollectionWhere , set : Record < string , unknown > ) : Promise < number > {
360366 const schema = this . #schema( collection ) ;
361367 const columns = Object . keys ( set ) . filter ( column => set [ column ] !== undefined ) ;
362368 if ( columns . length === 0 ) return 0 ;
@@ -367,7 +373,11 @@ class LibSQLFactoryStorageOps implements FactoryStorageOps {
367373 return result . rowsAffected ;
368374 }
369375
370- async deleteMany ( collection : string , where : CollectionWhere ) : Promise < number > {
376+ async updateMany ( collection : string , where : CollectionWhere , set : Record < string , unknown > ) : Promise < number > {
377+ return this . #withWriteLock( ( ) => this . #updateMany( collection , where , set ) ) ;
378+ }
379+
380+ async #deleteMany( collection : string , where : CollectionWhere ) : Promise < number > {
371381 const schema = this . #schema( collection ) ;
372382 const filter = this . #buildWhere( schema , where ) ;
373383 const result = await this . #client. execute ( {
@@ -377,22 +387,24 @@ class LibSQLFactoryStorageOps implements FactoryStorageOps {
377387 return result . rowsAffected ;
378388 }
379389
390+ async deleteMany ( collection : string , where : CollectionWhere ) : Promise < number > {
391+ return this . #withWriteLock( ( ) => this . #deleteMany( collection , where ) ) ;
392+ }
393+
380394 async updateAtomic < T extends Record < string , unknown > > (
381395 collection : string ,
382396 where : CollectionWhere ,
383397 fn : ( row : T ) => Partial < T > | null | Promise < Partial < T > | null > ,
384398 ) : Promise < T | null > {
385- const schema = this . #schema( collection ) ;
386- const pk = primaryKeyOf ( schema ) ;
387- // libsql local is single-writer; serializing read-modify-write in process
388- // gives the same "no lost updates" guarantee pg gets from FOR UPDATE.
389- return this . #writeMutex. run ( async ( ) => {
399+ return this . #withWriteLock( async ( ) => {
400+ const schema = this . #schema( collection ) ;
401+ const pk = primaryKeyOf ( schema ) ;
390402 const row = await this . findOne < T > ( collection , where ) ;
391403 if ( ! row ) return null ;
392404 const patch = await fn ( row ) ;
393405 if ( patch === null ) return row ;
394406 const pkWhere = { [ pk ] : row [ pk ] as CollectionValue } as CollectionWhere ;
395- await this . updateMany ( collection , pkWhere , patch ) ;
407+ await this . # updateMany( collection , pkWhere , patch ) ;
396408 return this . findOne < T > ( collection , pkWhere ) ;
397409 } ) ;
398410 }
@@ -420,7 +432,7 @@ export class LibSQLFactoryStorage extends FactoryStorage {
420432 ...( config . authToken ? { authToken : config . authToken } : { } ) ,
421433 ...( isLocalDb ? { timeout : DEFAULT_CONNECTION_TIMEOUT_MS } : { } ) ,
422434 } ) ;
423- this . ops = new LibSQLFactoryStorageOps ( this . #client, this . #schemas) ;
435+ this . ops = new LibSQLFactoryStorageOps ( this . #client, this . #schemas, fn => withClientWriteLock ( this . #client , fn ) ) ;
424436 }
425437
426438 getMastraStorage ( ) : MastraCompositeStore {
@@ -437,25 +449,31 @@ export class LibSQLFactoryStorage extends FactoryStorage {
437449 }
438450
439451 async withTransaction < T > ( fn : ( ops : FactoryStorageOps ) => Promise < T > ) : Promise < T > {
440- if ( this . #config. url . includes ( ':memory:' ) ) return fn ( this . ops ) ;
441- const transaction = await this . #client. transaction ( 'write' ) ;
442- try {
443- const result = await fn ( new LibSQLFactoryStorageOps ( transaction , this . #schemas) ) ;
444- await transaction . commit ( ) ;
445- return result ;
446- } catch ( error ) {
447- await transaction . rollback ( ) ;
448- throw error ;
449- } finally {
450- transaction . close ( ) ;
451- }
452+ return withClientWriteLock ( this . #client, async ( ) => {
453+ if ( this . #config. url . includes ( ':memory:' ) ) {
454+ return fn ( new LibSQLFactoryStorageOps ( this . #client, this . #schemas, runWithoutWriteLock ) ) ;
455+ }
456+ const transaction = await this . #client. transaction ( 'write' ) ;
457+ try {
458+ const result = await fn ( new LibSQLFactoryStorageOps ( transaction , this . #schemas, runWithoutWriteLock ) ) ;
459+ await transaction . commit ( ) ;
460+ return result ;
461+ } catch ( error ) {
462+ await transaction . rollback ( ) ;
463+ throw error ;
464+ } finally {
465+ transaction . close ( ) ;
466+ }
467+ } ) ;
452468 }
453469
454470 async ensureCollections ( schemas : CollectionSchema [ ] ) : Promise < void > {
455- for ( const schema of schemas ) {
456- await this . #ensureCollection( schema ) ;
457- this . #schemas. set ( schema . name , schema ) ;
458- }
471+ await withClientWriteLock ( this . #client, async ( ) => {
472+ for ( const schema of schemas ) {
473+ await this . #ensureCollection( schema ) ;
474+ this . #schemas. set ( schema . name , schema ) ;
475+ }
476+ } ) ;
459477 }
460478
461479 async close ( ) : Promise < void > {
0 commit comments