11import  'dart:async' ;
22import  'dart:developer' ;
33import  'dart:js_interop' ;
4- import  'dart:js_interop_unsafe' ;
54
65import  'package:sqlite3/common.dart' ;
76import  'package:sqlite3_web/sqlite3_web.dart' ;
8- import  'package:sqlite3_web/protocol_utils.dart'  as  proto;
97import  'package:sqlite_async/sqlite_async.dart' ;
108import  'package:sqlite_async/src/utils/profiler.dart' ;
119import  'package:sqlite_async/src/web/database/broadcast_updates.dart' ;
@@ -97,72 +95,85 @@ class WebDatabase
9795  @override 
9896  Future <T > readLock <T >(Future <T > Function (SqliteReadContext  tx) callback,
9997      {Duration ?  lockTimeout, String ?  debugContext}) async  {
100-     if  (_mutex case  var  mutex? ) {
101-       return  await  mutex.lock (timeout:  lockTimeout, () {
102-         return  ScopedReadContext .assumeReadLock (
103-             _UnscopedContext (this ), callback);
104-       });
105-     } else  {
106-       // No custom mutex, coordinate locks through shared worker. 
107-       await  _database.customRequest (
108-           CustomDatabaseMessage (CustomDatabaseMessageKind .requestSharedLock));
109- 
110-       try  {
111-         return  await  ScopedReadContext .assumeReadLock (
112-             _UnscopedContext (this ), callback);
113-       } finally  {
114-         await  _database.customRequest (
115-             CustomDatabaseMessage (CustomDatabaseMessageKind .releaseLock));
116-       }
117-     }
98+     // Since there is only a single physical connection per database on the web, 
99+     // we can't enable concurrent readers to a writer. Even supporting multiple 
100+     // readers alone isn't safe, since these readers could start read 
101+     // transactions where we need to block other tabs from sending `BEGIN` and 
102+     // `COMMIT` statements if they were to start their own transactions. 
103+     return  _lockInternal (
104+       (unscoped) =>  ScopedReadContext .assumeReadLock (unscoped, callback),
105+       lockTimeout:  lockTimeout,
106+       debugContext:  debugContext,
107+       flush:  false ,
108+     );
118109  }
119110
120111  @override 
121112  Future <T > writeTransaction <T >(
122113      Future <T > Function (SqliteWriteContext  tx) callback,
123114      {Duration ?  lockTimeout,
124115      bool ?  flush}) {
125-     return  writeLock ((writeContext) {
126-       return  ScopedWriteContext .assumeWriteLock (
127-         _UnscopedContext (this ),
128-         (ctx) async  {
129-           return  await  ctx.writeTransaction (callback);
130-         },
131-       );
132-     },
133-         debugContext:  'writeTransaction()' ,
134-         lockTimeout:  lockTimeout,
135-         flush:  flush);
116+     return  _lockInternal (
117+       (context) {
118+         return  ScopedWriteContext .assumeWriteLock (
119+           context,
120+           (ctx) async  {
121+             return  await  ctx.writeTransaction (callback);
122+           },
123+         );
124+       },
125+       flush:  flush ??  true ,
126+       lockTimeout:  lockTimeout,
127+     );
136128  }
137129
138130  @override 
139131  Future <T > writeLock <T >(Future <T > Function (SqliteWriteContext  tx) callback,
140132      {Duration ?  lockTimeout, String ?  debugContext, bool ?  flush}) async  {
133+     return  await  _lockInternal (
134+       (unscoped) {
135+         return  ScopedWriteContext .assumeWriteLock (unscoped, callback);
136+       },
137+       flush:  flush ??  true ,
138+       debugContext:  debugContext,
139+       lockTimeout:  lockTimeout,
140+     );
141+   }
142+ 
143+   Future <T > _lockInternal <T >(
144+     Future <T > Function (_UnscopedContext ) callback, {
145+     required  bool  flush,
146+     Duration ?  lockTimeout,
147+     String ?  debugContext,
148+   }) async  {
141149    if  (_mutex case  var  mutex? ) {
142150      return  await  mutex.lock (timeout:  lockTimeout, () async  {
143-         final  context =  _UnscopedContext (this );
151+         final  context =  _UnscopedContext (this ,  null );
144152        try  {
145-           return  await  ScopedWriteContext . assumeWriteLock (context, callback );
153+           return  await  callback (context);
146154        } finally  {
147-           if  (flush  !=   false ) {
155+           if  (flush) {
148156            await  this .flush ();
149157          }
150158        }
151159      });
152160    } else  {
153-       // No custom mutex, coordinate locks through shared worker. 
154-       await  _database.customRequest (CustomDatabaseMessage (
155-           CustomDatabaseMessageKind .requestExclusiveLock));
156-       final  context =  _UnscopedContext (this );
157-       try  {
158-         return  await  ScopedWriteContext .assumeWriteLock (context, callback);
159-       } finally  {
160-         if  (flush !=  false ) {
161-           await  this .flush ();
161+       final  abortTrigger =  switch  (lockTimeout) {
162+         null  =>  null ,
163+         final  duration =>  Future .delayed (duration),
164+       };
165+ 
166+       return  await  _database.requestLock (abortTrigger:  abortTrigger,
167+           (token) async  {
168+         final  context =  _UnscopedContext (this , token);
169+         try  {
170+           return  await  callback (context);
171+         } finally  {
172+           if  (flush) {
173+             await  this .flush ();
174+           }
162175        }
163-         await  _database.customRequest (
164-             CustomDatabaseMessage (CustomDatabaseMessageKind .releaseLock));
165-       }
176+       });
166177    }
167178  }
168179
@@ -184,9 +195,20 @@ class WebDatabase
184195final  class  _UnscopedContext  extends  UnscopedContext  {
185196  final  WebDatabase  _database;
186197
198+   /// If this context is scoped to a lock on the database, the [LockToken]  from 
199+   /// `package:sqlite3_web` . 
200+   /// 
201+   /// This token needs to be passed to queries to run them. While a lock token 
202+   /// exists on the database, all queries not passing that token are blocked. 
203+    final  LockToken ?  _lock;
204+ 
187205  final  TimelineTask ?  _task;
188206
189-   _UnscopedContext (this ._database)
207+   /// Whether statements should be rejected if the database is not in an 
208+   /// autocommit state. 
209+    bool  _checkInTransaction =  false ;
210+ 
211+   _UnscopedContext (this ._database, this ._lock)
190212      :  _task =  _database.profileQueries ?  TimelineTask () :  null ;
191213
192214  @override 
@@ -213,8 +235,15 @@ final class _UnscopedContext extends UnscopedContext {
213235      sql:  sql,
214236      parameters:  parameters,
215237      () async  {
216-         return  await  wrapSqliteException (
217-             () =>  _database._database.select (sql, parameters));
238+         return  await  wrapSqliteException (() async  {
239+           final  result =  await  _database._database.select (
240+             sql,
241+             parameters:  parameters,
242+             token:  _lock,
243+             checkInTransaction:  _checkInTransaction,
244+           );
245+           return  result.result;
246+         });
218247      },
219248    );
220249  }
@@ -234,8 +263,15 @@ final class _UnscopedContext extends UnscopedContext {
234263  @override 
235264  Future <ResultSet > execute (String  sql, [List <Object ?> parameters =  const  []]) {
236265    return  _task.timeAsync ('execute' , sql:  sql, parameters:  parameters, () {
237-       return  wrapSqliteException (
238-           () =>  _database._database.select (sql, parameters));
266+       return  wrapSqliteException (() async  {
267+         final  result =  await  _database._database.select (
268+           sql,
269+           parameters:  parameters,
270+           token:  _lock,
271+           checkInTransaction:  _checkInTransaction,
272+         );
273+         return  result.result;
274+       });
239275    });
240276  }
241277
@@ -246,7 +282,12 @@ final class _UnscopedContext extends UnscopedContext {
246282        for  (final  set  in  parameterSets) {
247283          // use execute instead of select to avoid transferring rows from the 
248284          // worker to this context. 
249-           await  _database._database.execute (sql, set );
285+           await  _database._database.execute (
286+             sql,
287+             parameters:  set ,
288+             token:  _lock,
289+             checkInTransaction:  _checkInTransaction,
290+           );
250291        }
251292      });
252293    });
@@ -256,75 +297,7 @@ final class _UnscopedContext extends UnscopedContext {
256297  UnscopedContext  interceptOutermostTransaction () {
257298    // All execute calls done in the callback will be checked for the 
258299    // autocommit state 
259-     return  _ExclusiveTransactionContext (_database);
260-   }
261- }
262- 
263- final  class  _ExclusiveTransactionContext  extends  _UnscopedContext  {
264-   _ExclusiveTransactionContext (super ._database);
265- 
266-   Future <ResultSet > _executeInternal (
267-       String  sql, List <Object ?> parameters) async  {
268-     // Operations inside transactions are executed with custom requests 
269-     // in order to verify that the connection does not have autocommit enabled. 
270-     // The worker will check if autocommit = true before executing the SQL. 
271-     // An exception will be thrown if autocommit is enabled. 
272-     // The custom request which does the above will return the ResultSet as a formatted 
273-     // JavaScript object. This is the converted into a Dart ResultSet. 
274-     return  await  wrapSqliteException (() async  {
275-       var  res =  await  _database._database.customRequest (CustomDatabaseMessage (
276-               CustomDatabaseMessageKind .executeInTransaction, sql, parameters))
277-           as  JSObject ;
278- 
279-       if  (res.has ('format' ) &&  (res['format' ] as  JSNumber ).toDartInt ==  2 ) {
280-         // Newer workers use a serialization format more efficient than dartify(). 
281-         return  proto.deserializeResultSet (res['r' ] as  JSObject );
282-       }
283- 
284-       var  result =  Map <String , dynamic >.from (res.dartify () as  Map );
285-       final  columnNames =  [
286-         for  (final  entry in  result['columnNames' ]) entry as  String 
287-       ];
288-       final  rawTableNames =  result['tableNames' ];
289-       final  tableNames =  rawTableNames !=  null 
290-           ?  [
291-               for  (final  entry in  (rawTableNames as  List <Object ?>))
292-                 entry as  String 
293-             ]
294-           :  null ;
295- 
296-       final  rows =  < List <Object ?>> [];
297-       for  (final  row in  (result['rows' ] as  List <Object ?>)) {
298-         final  dartRow =  < Object ? > [];
299- 
300-         for  (final  column in  (row as  List <Object ?>)) {
301-           dartRow.add (column);
302-         }
303- 
304-         rows.add (dartRow);
305-       }
306-       final  resultSet =  ResultSet (columnNames, tableNames, rows);
307-       return  resultSet;
308-     });
309-   }
310- 
311-   @override 
312-   Future <ResultSet > execute (String  sql,
313-       [List <Object ?> parameters =  const  []]) async  {
314-     return  _task.timeAsync ('execute' , sql:  sql, parameters:  parameters, () {
315-       return  _executeInternal (sql, parameters);
316-     });
317-   }
318- 
319-   @override 
320-   Future <void > executeBatch (
321-       String  sql, List <List <Object ?>> parameterSets) async  {
322-     return  _task.timeAsync ('executeBatch' , sql:  sql, () async  {
323-       for  (final  set  in  parameterSets) {
324-         await  _database._database.customRequest (CustomDatabaseMessage (
325-             CustomDatabaseMessageKind .executeBatchInTransaction, sql, set ));
326-       }
327-     });
300+     return  _UnscopedContext (_database, _lock).._checkInTransaction =  true ;
328301  }
329302}
330303
@@ -337,6 +310,11 @@ Future<T> wrapSqliteException<T>(Future<T> Function() callback) async {
337310      throw  serializedCause;
338311    }
339312
313+     if  (ex.message.contains ('Database is not in a transaction' )) {
314+       throw  SqliteException (
315+           0 , "Transaction rolled back by earlier statement. Cannot execute." );
316+     }
317+ 
340318    // Older versions of package:sqlite_web reported SqliteExceptions as strings 
341319    // only. 
342320    if  (ex.toString ().contains ('SqliteException' )) {
0 commit comments