2
2
3
3
import assert from 'assert' ;
4
4
import crypto from 'crypto' ;
5
+ import Deque from 'double-ended-queue' ;
5
6
import { clone , filterChange , uuid } from 'pouchdb-utils' ;
6
7
import {
7
8
collectConflicts ,
@@ -131,13 +132,90 @@ function callbackify(fn) {
131
132
} ;
132
133
}
133
134
135
+ /**
136
+ * @param {fdb.Transaction } db
137
+ * @param {TransactionState } state
138
+ */
139
+ async function executeQueue ( db , state ) {
140
+ const { queue } = state ;
141
+
142
+ while ( queue . length > 0 ) {
143
+ const task = /** @type {Task } */ ( queue . peekFront ( ) ) ;
144
+
145
+ if ( ! task . write ) {
146
+ const readTasks = [ task ] ;
147
+
148
+ for ( let i = 1 ; i < queue . length ; i ++ ) {
149
+ const nextTask = /** @type {Task } */ ( queue . get ( i ) ) ;
150
+
151
+ if ( nextTask . write ) {
152
+ break ;
153
+ }
154
+
155
+ readTasks . push ( nextTask ) ;
156
+ }
157
+
158
+ await Promise . all ( readTasks . map (
159
+ ( { fn, resolve, reject } ) => fn ( db ) . then ( resolve , reject )
160
+ ) ) ;
161
+
162
+ readTasks . forEach ( ( ) => {
163
+ queue . shift ( ) ;
164
+ } ) ;
165
+ } else {
166
+ const { fn, resolve, reject } = task ;
167
+
168
+ await fn ( db ) . then ( resolve , reject ) ;
169
+
170
+ queue . shift ( ) ;
171
+ }
172
+ }
173
+ }
174
+
134
175
/**
135
176
* @typedef {(
136
177
* | fdb.Database
137
178
* | fdb.Transaction
138
179
* )} Actionable
180
+ * @typedef {{ queue: Deque<Task> } } TransactionState
181
+ * @typedef {{
182
+ * fn: (tn: fdb.Transaction) => Promise<unknown>,
183
+ * resolve: (value: unknown) => void,
184
+ * reject: (reason?: any) => void,
185
+ * write: boolean
186
+ * }} Task
139
187
*/
140
188
189
+ /**
190
+ * A quasi-global state needed for locks for when concurrent PouchDB operations are made with the
191
+ * same transaction. The key is the hidden context object of the transaction.
192
+ * @type {WeakMap<fdb.Transaction, Record<string, TransactionState>> }
193
+ */
194
+ const transactionState = new WeakMap ( ) ;
195
+
196
+ /**
197
+ * @param {fdb.Transaction } tn
198
+ * @param {string } name
199
+ * @returns {TransactionState }
200
+ */
201
+ function getTransactionState ( tn , name ) {
202
+ let state = transactionState . get ( /** @type {any } */ ( tn ) . _ctx ) ;
203
+
204
+ if ( state == null ) {
205
+ state = { } ;
206
+
207
+ transactionState . set ( /** @type {any } */ ( tn ) . _ctx , state ) ;
208
+ }
209
+
210
+ if ( ! ( name in state ) ) {
211
+ state [ name ] = {
212
+ queue : new Deque ( )
213
+ } ;
214
+ }
215
+
216
+ return state [ name ] ;
217
+ }
218
+
141
219
/**
142
220
* @param {any } api
143
221
* @param {{ db: Actionable, name: string, revs_limit?: number } } opts
@@ -148,12 +226,46 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
148
226
* @param {(tn: fdb.Transaction) => Promise<T> } fn
149
227
* @returns {Promise<T> }
150
228
*/
151
- function doTn ( fn ) {
229
+ function doReadTn ( fn ) {
230
+ return doTn ( fn , false ) ;
231
+ }
232
+
233
+ /**
234
+ * @template T
235
+ * @param {(tn: fdb.Transaction) => Promise<T> } fn
236
+ * @returns {Promise<T> }
237
+ */
238
+ function doWriteTn ( fn ) {
239
+ return doTn ( fn , true ) ;
240
+ }
241
+
242
+ /**
243
+ * @template T
244
+ * @param {(tn: fdb.Transaction) => Promise<T> } fn
245
+ * @param {boolean } write
246
+ * @returns {Promise<T> }
247
+ */
248
+ function doTn ( fn , write ) {
152
249
if ( 'doTn' in db ) {
153
250
return db . doTn ( fn ) ;
154
251
}
155
252
156
- return fn ( db ) ;
253
+ const state = getTransactionState ( db , name ) ;
254
+
255
+ const promise = new Promise ( ( resolve , reject ) => {
256
+ state . queue . push ( {
257
+ fn,
258
+ write,
259
+ resolve,
260
+ reject
261
+ } ) ;
262
+ } ) ;
263
+
264
+ if ( state . queue . length === 1 ) {
265
+ executeQueue ( db , state ) ;
266
+ }
267
+
268
+ return promise ;
157
269
}
158
270
159
271
const subspace = db . subspace . at ( fdb . tuple . pack ( name ) , defaultTransformer , defaultTransformer ) ;
@@ -187,9 +299,11 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
187
299
188
300
api . _id = callbackify (
189
301
/**
302
+ * TODO: That is concurrency-safe with most other read operations and, in most cases, with
303
+ * itself so it might not be necessary registeer it as a write transaction.
190
304
* @returns {Promise<string> }
191
305
*/
192
- ( ) => doTn ( async tn => {
306
+ ( ) => doWriteTn ( async tn => {
193
307
let instanceId = await /** @type {Promise<undefined | string> } */ (
194
308
tn . at ( stores . metaStore ) . get ( UUID_KEY )
195
309
) ;
@@ -208,7 +322,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
208
322
/**
209
323
* @returns {Promise<{ doc_count: number, update_seq: number }> }
210
324
*/
211
- async ( ) => doTn ( async tn => {
325
+ async ( ) => doReadTn ( async tn => {
212
326
const [ docCount = 0 , updateSeq = 0 ] = await Promise . all ( [
213
327
/** @type {Promise<undefined | number> } */ ( tn . at ( stores . metaStore ) . get ( DOC_COUNT_KEY ) ) ,
214
328
/** @type {Promise<undefined | number> } */ ( tn . at ( stores . metaStore ) . get ( UPDATE_SEQ_KEY ) )
@@ -226,7 +340,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
226
340
* @param {import('./types.js').Id } id
227
341
* @param {{ rev?: import('./types.js').Rev, latest?: boolean } } param
228
342
*/
229
- ( id , { rev, latest } = { } ) => doTn ( async tn => {
343
+ ( id , { rev, latest } = { } ) => doReadTn ( async tn => {
230
344
const metadata = await tn . at ( stores . docStore ) . get ( id ) ;
231
345
232
346
if ( metadata == null ) {
@@ -278,7 +392,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
278
392
* @param {{ binary?: boolean } } param3
279
393
* @returns {Promise<Buffer | string> }
280
394
*/
281
- ( docId , attachId , attachment , { binary } = { } ) => doTn ( async tn => {
395
+ ( docId , attachId , attachment , { binary } = { } ) => doReadTn ( async tn => {
282
396
const { digest } = attachment ;
283
397
284
398
const attach = await tn . at ( stores . binaryStore ) . get ( digest ) ?? Buffer . allocUnsafe ( 0 ) ;
@@ -298,7 +412,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
298
412
* @param {{ new_edits: boolean } } opts
299
413
* @returns {Promise<import('./types.js').BulkDocsResultRow[]> }
300
414
*/
301
- ( { docs : userDocs } , opts ) => doTn ( async tn => {
415
+ ( { docs : userDocs } , opts ) => doWriteTn ( async tn => {
302
416
const docInfos = userDocs . map ( doc => {
303
417
if ( isLocalDoc ( doc ) ) {
304
418
return doc ;
@@ -561,7 +675,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
561
675
* }} opts
562
676
* @returns {Promise<import('./types.js').AllDocsResult> }
563
677
*/
564
- opts => doTn ( async tn => {
678
+ opts => doReadTn ( async tn => {
565
679
/** @type {[import('./types.js').AllDocsResultRow[], number, number] } */
566
680
const [ rows , docCount = 0 , updateSeq = 0 ] = await Promise . all ( [
567
681
getAllDocsRows ( opts , tn ) ,
@@ -876,7 +990,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
876
990
877
991
for ( ; ; ) {
878
992
// eslint-disable-next-line no-loop-func
879
- const { it, watch } = await doTn ( async tn => {
993
+ const { it, watch } = await doReadTn ( async tn => {
880
994
const it = await tn . at ( stores . bySeqStore ) . getRangeAll (
881
995
lastSeq ,
882
996
Infinity ,
@@ -909,7 +1023,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
909
1023
910
1024
if ( ! metadata ) {
911
1025
metadata = /** @type {import('./types.js').Metadata } */ (
912
- await doTn ( tn => tn . at ( stores . docStore ) . get ( doc . _id ) )
1026
+ await doReadTn ( tn => tn . at ( stores . docStore ) . get ( doc . _id ) )
913
1027
) ;
914
1028
915
1029
if ( isLocalId ( metadata . id ) ) {
@@ -930,7 +1044,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
930
1044
931
1045
const winningDoc = winningRev === doc . _rev
932
1046
? doc
933
- : await doTn ( tn => tn . at ( stores . bySeqStore ) . get ( metadata . rev_map [ winningRev ] ) ) ;
1047
+ : await doReadTn ( tn => tn . at ( stores . bySeqStore ) . get ( metadata . rev_map [ winningRev ] ) ) ;
934
1048
935
1049
assert ( winningDoc ) ;
936
1050
@@ -952,7 +1066,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
952
1066
953
1067
// fetch attachment immediately for the benefit
954
1068
// of live listeners
955
- change . doc . _attachments = await doTn ( async tn => Object . fromEntries (
1069
+ change . doc . _attachments = await doReadTn ( async tn => Object . fromEntries (
956
1070
await Promise . all ( Object . entries ( attachments ) . map (
957
1071
async ( [ fileName , att ] ) => [
958
1072
fileName ,
@@ -1018,7 +1132,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
1018
1132
* @returns {Promise<import('./types.js').RevTreePath[]> }
1019
1133
*/
1020
1134
async docId => {
1021
- const metadata = await doTn ( tn => tn . at ( stores . docStore ) . get ( docId ) ) ;
1135
+ const metadata = await doReadTn ( tn => tn . at ( stores . docStore ) . get ( docId ) ) ;
1022
1136
1023
1137
if ( metadata == null ) {
1024
1138
throw createError ( MISSING_DOC ) ;
@@ -1033,7 +1147,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
1033
1147
* @param {import('./types.js').Id } docId
1034
1148
* @param {import('./types.js').Rev[] } revs
1035
1149
*/
1036
- ( docId , revs ) => doTn ( tn => doCompaction ( docId , revs , tn ) )
1150
+ ( docId , revs ) => doWriteTn ( tn => doCompaction ( docId , revs , tn ) )
1037
1151
) ;
1038
1152
1039
1153
/**
@@ -1129,7 +1243,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
1129
1243
* @returns {Promise<import('./types.js').LocalDoc> }
1130
1244
*/
1131
1245
async id => {
1132
- const value = await doTn ( tn => tn . at ( stores . localStore ) . get ( id ) ) ;
1246
+ const value = await doReadTn ( tn => tn . at ( stores . localStore ) . get ( id ) ) ;
1133
1247
1134
1248
if ( value == null ) {
1135
1249
throw createError ( MISSING_DOC ) ;
@@ -1147,7 +1261,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
1147
1261
*/
1148
1262
( doc , opts ) => opts ?. ctx
1149
1263
? putLocal ( doc , opts . ctx )
1150
- : doTn ( tn => putLocal ( doc , tn ) )
1264
+ : doWriteTn ( tn => putLocal ( doc , tn ) )
1151
1265
) ;
1152
1266
1153
1267
/**
@@ -1187,7 +1301,7 @@ export default function FoundationdbAdapter(api, { db, name, revs_limit: revLimi
1187
1301
*/
1188
1302
( doc , opts ) => opts ?. ctx
1189
1303
? removeLocal ( doc , opts . ctx )
1190
- : doTn ( tn => removeLocal ( doc , tn ) )
1304
+ : doWriteTn ( tn => removeLocal ( doc , tn ) )
1191
1305
) ;
1192
1306
1193
1307
/**
0 commit comments