@@ -8,19 +8,26 @@ import {
8
8
TransactionContext ,
9
9
UpdateCallback ,
10
10
SQLBatchTuple ,
11
- OpenOptions
11
+ OpenOptions ,
12
+ QueryResult
12
13
} from './types' ;
13
14
14
15
import uuid from 'uuid' ;
15
16
import _ from 'lodash' ;
16
17
import { enhanceQueryResult } from './utils' ;
17
- import { registerUpdateHook } from './table-updates' ;
18
+ import { DBListenerManagerInternal } from './DBListenerManager' ;
19
+ import { LockHooks , TransactionHooks } from './lock-hooks' ;
18
20
19
21
type LockCallbackRecord = {
20
22
callback : ( context : LockContext ) => Promise < any > ;
21
23
timeout ?: NodeJS . Timeout ;
22
24
} ;
23
25
26
+ enum TransactionFinalizer {
27
+ COMMIT = 'commit' ,
28
+ ROLLBACK = 'rollback'
29
+ }
30
+
24
31
const DEFAULT_READ_CONNECTIONS = 4 ;
25
32
26
33
const LockCallbacks : Record < ContextLockID , LockCallbackRecord > = { } ;
@@ -90,14 +97,17 @@ export function setupOpen(QuickSQLite: ISQLite) {
90
97
numReadConnections : options ?. numReadConnections ?? DEFAULT_READ_CONNECTIONS
91
98
} ) ;
92
99
100
+ const listenerManager = new DBListenerManagerInternal ( { dbName } ) ;
101
+
93
102
/**
94
103
* Wraps lock requests and their callbacks in order to resolve the lock
95
104
* request with the callback result once triggered from the connection pool.
96
105
*/
97
106
const requestLock = < T > (
98
107
type : ConcurrentLockType ,
99
108
callback : ( context : LockContext ) => Promise < T > ,
100
- options ?: LockOptions
109
+ options ?: LockOptions ,
110
+ hooks ?: LockHooks
101
111
) : Promise < T > => {
102
112
const id = uuid . v4 ( ) ; // TODO maybe do this in C++
103
113
// Wrap the callback in a promise that will resolve to the callback result
@@ -106,12 +116,22 @@ export function setupOpen(QuickSQLite: ISQLite) {
106
116
const record = ( LockCallbacks [ id ] = {
107
117
callback : async ( context : LockContext ) => {
108
118
try {
109
- const res = await callback ( context ) ;
119
+ await hooks ?. lockAcquired ?.( ) ;
120
+ const res = await callback ( {
121
+ ...context ,
122
+ execute : async ( sql , args ) => {
123
+ const result = await context . execute ( sql , args ) ;
124
+ await hooks ?. execute ?.( sql , args ) ;
125
+ return result ;
126
+ }
127
+ } ) ;
110
128
111
129
// Ensure that we only resolve after locks are freed
112
130
_ . defer ( ( ) => resolve ( res ) ) ;
113
131
} catch ( ex ) {
114
132
_ . defer ( ( ) => reject ( ex ) ) ;
133
+ } finally {
134
+ _ . defer ( ( ) => hooks ?. lockReleased ?.( ) ) ;
115
135
}
116
136
}
117
137
} as LockCallbackRecord ) ;
@@ -137,15 +157,20 @@ export function setupOpen(QuickSQLite: ISQLite) {
137
157
const readLock = < T > ( callback : ( context : LockContext ) => Promise < T > , options ?: LockOptions ) : Promise < T > =>
138
158
requestLock ( ConcurrentLockType . READ , callback , options ) ;
139
159
140
- const writeLock = < T > ( callback : ( context : LockContext ) => Promise < T > , options ?: LockOptions ) : Promise < T > =>
141
- requestLock ( ConcurrentLockType . WRITE , callback , options ) ;
160
+ const writeLock = < T > (
161
+ callback : ( context : LockContext ) => Promise < T > ,
162
+ options ?: LockOptions ,
163
+ hooks ?: LockHooks
164
+ ) : Promise < T > => requestLock ( ConcurrentLockType . WRITE , callback , options , hooks ) ;
142
165
143
166
const wrapTransaction = async < T > (
144
167
context : LockContext ,
145
168
callback : ( context : TransactionContext ) => Promise < T > ,
146
- defaultFinally : 'commit' | 'rollback' = 'commit'
169
+ defaultFinalizer : TransactionFinalizer = TransactionFinalizer . COMMIT ,
170
+ hooks ?: TransactionHooks
147
171
) => {
148
172
await context . execute ( 'BEGIN TRANSACTION' ) ;
173
+ await hooks ?. begin ( ) ;
149
174
let finalized = false ;
150
175
151
176
const finalizedStatement =
@@ -158,19 +183,29 @@ export function setupOpen(QuickSQLite: ISQLite) {
158
183
return action ( ) ;
159
184
} ;
160
185
161
- const commit = finalizedStatement ( ( ) => context . execute ( 'COMMIT' ) ) ;
162
- const commitAsync = finalizedStatement ( ( ) => context . execute ( 'COMMIT' ) ) ;
186
+ const commit = finalizedStatement ( async ( ) => {
187
+ const result = await context . execute ( 'COMMIT' ) ;
188
+ await hooks ?. commit ?.( ) ;
189
+ return result ;
190
+ } ) ;
163
191
164
- const rollback = finalizedStatement ( ( ) => context . execute ( 'ROLLBACK' ) ) ;
165
- const rollbackAsync = finalizedStatement ( ( ) => context . execute ( 'ROLLBACK' ) ) ;
192
+ const rollback = finalizedStatement ( async ( ) => {
193
+ const result = await context . execute ( 'ROLLBACK' ) ;
194
+ await hooks ?. rollback ?.( ) ;
195
+ return result ;
196
+ } ) ;
166
197
167
198
const wrapExecute =
168
- < T > ( method : ( sql : string , params ?: any [ ] ) => T ) : ( ( sql : string , params ?: any [ ] ) => T ) =>
169
- ( sql : string , params ?: any [ ] ) => {
199
+ < T > (
200
+ method : ( sql : string , params ?: any [ ] ) => Promise < QueryResult >
201
+ ) : ( ( sql : string , params ?: any [ ] ) => Promise < QueryResult > ) =>
202
+ async ( sql : string , params ?: any [ ] ) => {
170
203
if ( finalized ) {
171
204
throw new Error ( `Cannot execute in transaction after it has been finalized with commit/rollback.` ) ;
172
205
}
173
- return method ( sql , params ) ;
206
+ const result = await method ( sql , params ) ;
207
+ await hooks ?. execute ?.( sql , params ) ;
208
+ return result ;
174
209
} ;
175
210
176
211
try {
@@ -180,17 +215,17 @@ export function setupOpen(QuickSQLite: ISQLite) {
180
215
rollback,
181
216
execute : wrapExecute ( context . execute )
182
217
} ) ;
183
- switch ( defaultFinally ) {
184
- case 'commit' :
185
- await commitAsync ( ) ;
218
+ switch ( defaultFinalizer ) {
219
+ case TransactionFinalizer . COMMIT :
220
+ await commit ( ) ;
186
221
break ;
187
- case 'rollback' :
188
- await rollbackAsync ( ) ;
222
+ case TransactionFinalizer . ROLLBACK :
223
+ await rollback ( ) ;
189
224
break ;
190
225
}
191
226
return res ;
192
227
} catch ( ex ) {
193
- await rollbackAsync ( ) ;
228
+ await rollback ( ) ;
194
229
throw ex ;
195
230
}
196
231
} ;
@@ -202,20 +237,55 @@ export function setupOpen(QuickSQLite: ISQLite) {
202
237
readLock,
203
238
readTransaction : async < T > ( callback : ( context : TransactionContext ) => Promise < T > , options ?: LockOptions ) =>
204
239
readLock ( ( context ) => wrapTransaction ( context , callback ) ) ,
205
- writeLock,
240
+ writeLock : async < T > ( callback : ( context : TransactionContext ) => Promise < T > , options ?: LockOptions ) =>
241
+ writeLock ( callback , options , {
242
+ execute : async ( sql ) => {
243
+ if ( ! listenerManager . writeTransactionActive ) {
244
+ // check if starting a transaction
245
+ if ( sql == 'BEGIN' || sql == 'BEGIN IMMEDIATE' ) {
246
+ listenerManager . transactionStarted ( ) ;
247
+ return ;
248
+ }
249
+ }
250
+ // check if finishing a transaction
251
+ switch ( sql ) {
252
+ case 'ROLLBACK' :
253
+ listenerManager . transactionReverted ( ) ;
254
+ break ;
255
+ case 'COMMIT' :
256
+ case 'END TRANSACTION' :
257
+ listenerManager . transactionCommitted ( ) ;
258
+ break ;
259
+ }
260
+ } ,
261
+ lockReleased : async ( ) => {
262
+ if ( listenerManager . writeTransactionActive ) {
263
+ // The lock was completed without ending the transaction.
264
+ // This should not occur, but do not report these updates
265
+ listenerManager . transactionReverted ( ) ;
266
+ }
267
+ }
268
+ } ) ,
206
269
writeTransaction : async < T > ( callback : ( context : TransactionContext ) => Promise < T > , options ?: LockOptions ) =>
207
- writeLock ( ( context ) => wrapTransaction ( context , callback ) , options ) ,
208
- registerUpdateHook : ( callback : UpdateCallback ) => {
209
- registerUpdateHook ( dbName , callback ) ;
210
- } ,
270
+ writeLock (
271
+ ( context ) =>
272
+ wrapTransaction ( context , callback , TransactionFinalizer . COMMIT , {
273
+ begin : async ( ) => listenerManager . transactionStarted ( ) ,
274
+ commit : async ( ) => listenerManager . transactionCommitted ( ) ,
275
+ rollback : async ( ) => listenerManager . transactionReverted ( )
276
+ } ) ,
277
+ options
278
+ ) ,
279
+ registerUpdateHook : ( callback : UpdateCallback ) => listenerManager . registerListener ( { tableUpdated : callback } ) ,
211
280
delete : ( ) => QuickSQLite . delete ( dbName , options ?. location ) ,
212
281
executeBatch : ( commands : SQLBatchTuple [ ] ) =>
213
282
writeLock ( ( context ) => QuickSQLite . executeBatch ( dbName , commands , ( context as any ) . _contextId ) ) ,
214
283
attach : ( dbNameToAttach : string , alias : string , location ?: string ) =>
215
284
QuickSQLite . attach ( dbName , dbNameToAttach , alias , location ) ,
216
285
detach : ( alias : string ) => QuickSQLite . detach ( dbName , alias ) ,
217
286
loadFile : ( location : string ) =>
218
- writeLock ( ( context ) => QuickSQLite . loadFile ( dbName , location , ( context as any ) . _contextId ) )
287
+ writeLock ( ( context ) => QuickSQLite . loadFile ( dbName , location , ( context as any ) . _contextId ) ) ,
288
+ listenerManager
219
289
} ;
220
290
}
221
291
} ;
0 commit comments