@periskope/baileys 6.7.18-17-2 → 6.7.18-17-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GroupCipher = void 0;
|
|
4
|
+
/* @ts-ignore */
|
|
4
5
|
const crypto_1 = require("libsignal/src/crypto");
|
|
5
6
|
const sender_key_message_1 = require("./sender-key-message");
|
|
6
7
|
class GroupCipher {
|
|
@@ -30,13 +31,9 @@ class GroupCipher {
|
|
|
30
31
|
throw new Error('No SenderKeyRecord found for decryption');
|
|
31
32
|
}
|
|
32
33
|
const senderKeyMessage = new sender_key_message_1.SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
|
|
33
|
-
|
|
34
|
-
// Fallback: try to get the latest sender key state if specific keyId not found
|
|
34
|
+
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
|
|
35
35
|
if (!senderKeyState) {
|
|
36
|
-
|
|
37
|
-
if (!senderKeyState) {
|
|
38
|
-
throw new Error('No session found to decrypt message');
|
|
39
|
-
}
|
|
36
|
+
throw new Error('No session found to decrypt message');
|
|
40
37
|
}
|
|
41
38
|
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
|
|
42
39
|
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration());
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { SignalAuthState } from '../Types';
|
|
2
|
-
import { SignalRepository } from '../Types/Signal';
|
|
1
|
+
import type { SignalAuthState } from '../Types';
|
|
2
|
+
import type { SignalRepository } from '../Types/Signal';
|
|
3
3
|
export declare function makeLibSignalRepository(auth: SignalAuthState): SignalRepository;
|
package/lib/Signal/libsignal.js
CHANGED
|
@@ -42,12 +42,13 @@ const sender_key_record_1 = require("./Group/sender-key-record");
|
|
|
42
42
|
const Group_1 = require("./Group");
|
|
43
43
|
function makeLibSignalRepository(auth) {
|
|
44
44
|
const storage = signalStorage(auth);
|
|
45
|
+
const parsedKeys = auth.keys;
|
|
45
46
|
return {
|
|
46
47
|
decryptGroupMessage({ group, authorJid, msg }) {
|
|
47
48
|
const senderName = jidToSignalSenderKeyName(group, authorJid);
|
|
48
49
|
const cipher = new Group_1.GroupCipher(storage, senderName);
|
|
49
50
|
// Use transaction to ensure atomicity
|
|
50
|
-
return
|
|
51
|
+
return parsedKeys.transaction(async () => {
|
|
51
52
|
return cipher.decrypt(msg);
|
|
52
53
|
});
|
|
53
54
|
},
|
|
@@ -59,7 +60,11 @@ function makeLibSignalRepository(auth) {
|
|
|
59
60
|
const senderName = jidToSignalSenderKeyName(item.groupId, authorJid);
|
|
60
61
|
const senderMsg = new Group_1.SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage);
|
|
61
62
|
const senderNameStr = senderName.toString();
|
|
62
|
-
|
|
63
|
+
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
64
|
+
if (!senderKey) {
|
|
65
|
+
await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord());
|
|
66
|
+
}
|
|
67
|
+
return parsedKeys.transaction(async () => {
|
|
63
68
|
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
64
69
|
if (!senderKey) {
|
|
65
70
|
await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord());
|
|
@@ -70,8 +75,8 @@ function makeLibSignalRepository(auth) {
|
|
|
70
75
|
async decryptMessage({ jid, type, ciphertext }) {
|
|
71
76
|
const addr = jidToSignalProtocolAddress(jid);
|
|
72
77
|
const session = new libsignal.SessionCipher(storage, addr);
|
|
73
|
-
// Use transaction to ensure
|
|
74
|
-
return
|
|
78
|
+
// Use transaction to ensure atomicity
|
|
79
|
+
return parsedKeys.transaction(async () => {
|
|
75
80
|
let result;
|
|
76
81
|
switch (type) {
|
|
77
82
|
case 'pkmsg':
|
|
@@ -80,6 +85,8 @@ function makeLibSignalRepository(auth) {
|
|
|
80
85
|
case 'msg':
|
|
81
86
|
result = await session.decryptWhisperMessage(ciphertext);
|
|
82
87
|
break;
|
|
88
|
+
default:
|
|
89
|
+
throw new Error(`Unknown message type: ${type}`);
|
|
83
90
|
}
|
|
84
91
|
return result;
|
|
85
92
|
});
|
|
@@ -87,8 +94,8 @@ function makeLibSignalRepository(auth) {
|
|
|
87
94
|
async encryptMessage({ jid, data }) {
|
|
88
95
|
const addr = jidToSignalProtocolAddress(jid);
|
|
89
96
|
const cipher = new libsignal.SessionCipher(storage, addr);
|
|
90
|
-
// Use transaction to ensure
|
|
91
|
-
return
|
|
97
|
+
// Use transaction to ensure atomicity
|
|
98
|
+
return parsedKeys.transaction(async () => {
|
|
92
99
|
const { type: sigType, body } = await cipher.encrypt(data);
|
|
93
100
|
const type = sigType === 3 ? 'pkmsg' : 'msg';
|
|
94
101
|
return { type, ciphertext: Buffer.from(body, 'binary') };
|
|
@@ -98,8 +105,7 @@ function makeLibSignalRepository(auth) {
|
|
|
98
105
|
const senderName = jidToSignalSenderKeyName(group, meId);
|
|
99
106
|
const builder = new Group_1.GroupSessionBuilder(storage);
|
|
100
107
|
const senderNameStr = senderName.toString();
|
|
101
|
-
|
|
102
|
-
return auth.keys.transaction(async () => {
|
|
108
|
+
return parsedKeys.transaction(async () => {
|
|
103
109
|
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
104
110
|
if (!senderKey) {
|
|
105
111
|
await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord());
|
|
@@ -115,8 +121,7 @@ function makeLibSignalRepository(auth) {
|
|
|
115
121
|
},
|
|
116
122
|
async injectE2ESession({ jid, session }) {
|
|
117
123
|
const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid));
|
|
118
|
-
|
|
119
|
-
return auth.keys.transaction(async () => {
|
|
124
|
+
return parsedKeys.transaction(async () => {
|
|
120
125
|
await cipher.initOutgoing(session);
|
|
121
126
|
});
|
|
122
127
|
},
|
|
@@ -140,6 +145,7 @@ function signalStorage({ creds, keys }) {
|
|
|
140
145
|
return libsignal.SessionRecord.deserialize(sess);
|
|
141
146
|
}
|
|
142
147
|
},
|
|
148
|
+
// TODO: Replace with libsignal.SessionRecord when type exports are added to libsignal
|
|
143
149
|
storeSession: async (id, session) => {
|
|
144
150
|
await keys.set({ session: { [id]: session.serialize() } });
|
|
145
151
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthenticationCreds, CacheStore, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types';
|
|
2
|
-
import { ILogger } from './logger';
|
|
2
|
+
import type { ILogger } from './logger';
|
|
3
3
|
/**
|
|
4
4
|
* Adds caching capability to a SignalKeyStore
|
|
5
5
|
* @param store the store to add caching to
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -71,11 +71,9 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
71
71
|
});
|
|
72
72
|
},
|
|
73
73
|
async clear() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await ((_a = store.clear) === null || _a === void 0 ? void 0 : _a.call(store));
|
|
78
|
-
});
|
|
74
|
+
var _a;
|
|
75
|
+
cache.flushAll();
|
|
76
|
+
await ((_a = store.clear) === null || _a === void 0 ? void 0 : _a.call(store));
|
|
79
77
|
}
|
|
80
78
|
};
|
|
81
79
|
}
|
|
@@ -113,8 +111,12 @@ async function handlePreKeyOperations(data, keyType, transactionCache, mutations
|
|
|
113
111
|
}
|
|
114
112
|
// Process updates first (no validation needed)
|
|
115
113
|
for (const keyId of updateKeys) {
|
|
116
|
-
transactionCache[keyType]
|
|
117
|
-
|
|
114
|
+
if (transactionCache[keyType]) {
|
|
115
|
+
transactionCache[keyType][keyId] = keyData[keyId];
|
|
116
|
+
}
|
|
117
|
+
if (mutations[keyType]) {
|
|
118
|
+
mutations[keyType][keyId] = keyData[keyId];
|
|
119
|
+
}
|
|
118
120
|
}
|
|
119
121
|
// Process deletions with validation
|
|
120
122
|
if (deletionKeys.length === 0)
|
|
@@ -122,9 +124,12 @@ async function handlePreKeyOperations(data, keyType, transactionCache, mutations
|
|
|
122
124
|
if (isInTransaction) {
|
|
123
125
|
// In transaction, only allow deletion if key exists in cache
|
|
124
126
|
for (const keyId of deletionKeys) {
|
|
125
|
-
if (transactionCache[keyType]
|
|
127
|
+
if (transactionCache[keyType]) {
|
|
126
128
|
transactionCache[keyType][keyId] = null;
|
|
127
|
-
mutations[keyType]
|
|
129
|
+
if (mutations[keyType]) {
|
|
130
|
+
// Mark for deletion in mutations
|
|
131
|
+
mutations[keyType][keyId] = null;
|
|
132
|
+
}
|
|
128
133
|
}
|
|
129
134
|
else {
|
|
130
135
|
logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`);
|
|
@@ -138,8 +143,10 @@ async function handlePreKeyOperations(data, keyType, transactionCache, mutations
|
|
|
138
143
|
const existingKeys = await state.get(keyType, deletionKeys);
|
|
139
144
|
for (const keyId of deletionKeys) {
|
|
140
145
|
if (existingKeys[keyId]) {
|
|
141
|
-
transactionCache[keyType]
|
|
142
|
-
|
|
146
|
+
if (transactionCache[keyType])
|
|
147
|
+
transactionCache[keyType][keyId] = null;
|
|
148
|
+
if (mutations[keyType])
|
|
149
|
+
mutations[keyType][keyId] = null;
|
|
143
150
|
}
|
|
144
151
|
else {
|
|
145
152
|
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`);
|
|
@@ -170,7 +177,8 @@ async function processPreKeyDeletions(data, keyType, state, logger) {
|
|
|
170
177
|
const existingKeys = await state.get(keyType, [keyId]);
|
|
171
178
|
if (!existingKeys[keyId]) {
|
|
172
179
|
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`);
|
|
173
|
-
|
|
180
|
+
if (data[keyType])
|
|
181
|
+
delete data[keyType][keyId];
|
|
174
182
|
}
|
|
175
183
|
}
|
|
176
184
|
}
|
|
@@ -208,30 +216,6 @@ async function withMutexes(keyTypes, getKeyTypeMutex, fn) {
|
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Attempts to commit transaction with retry mechanism
|
|
213
|
-
* Uses async-mutex's withTimeout for better timeout handling
|
|
214
|
-
*/
|
|
215
|
-
async function commitWithRetry(mutations, state, getKeyTypeMutex, maxRetries, delayMs, logger) {
|
|
216
|
-
let tries = maxRetries;
|
|
217
|
-
while (tries > 0) {
|
|
218
|
-
tries -= 1;
|
|
219
|
-
try {
|
|
220
|
-
// Use basic withMutexes - withTimeout is for decorating mutexes, not functions
|
|
221
|
-
await withMutexes(Object.keys(mutations), getKeyTypeMutex, async () => {
|
|
222
|
-
await state.set(mutations);
|
|
223
|
-
logger.trace('committed transaction');
|
|
224
|
-
});
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`);
|
|
229
|
-
if (tries > 0) {
|
|
230
|
-
await (0, generics_1.delay)(delayMs);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
219
|
/**
|
|
236
220
|
* Adds DB like transaction capability (https://en.wikipedia.org/wiki/Database_transaction) to the SignalKeyStore,
|
|
237
221
|
* this allows batch read & write operations & improves the performance of the lib
|
|
@@ -240,17 +224,52 @@ async function commitWithRetry(mutations, state, getKeyTypeMutex, maxRetries, de
|
|
|
240
224
|
* @returns SignalKeyStore with transaction capability
|
|
241
225
|
*/
|
|
242
226
|
const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetweenTriesMs }) => {
|
|
243
|
-
// Mutex for each key type (session, pre-key, etc.)
|
|
244
|
-
const keyTypeMutexes = new Map();
|
|
245
|
-
// Per-sender-key-name mutexes for fine-grained serialization
|
|
246
|
-
const senderKeyMutexes = new Map();
|
|
247
|
-
// Global transaction mutex
|
|
248
|
-
const transactionMutex = new async_mutex_1.Mutex();
|
|
249
227
|
// number of queries made to the DB during the transaction
|
|
250
228
|
// only there for logging purposes
|
|
251
229
|
let dbQueriesInTransaction = 0;
|
|
252
230
|
let transactionCache = {};
|
|
253
231
|
let mutations = {};
|
|
232
|
+
// Mutex for each key type (session, pre-key, etc.)
|
|
233
|
+
const keyTypeMutexes = new Map();
|
|
234
|
+
// Per-sender-key-name mutexes for fine-grained serialization
|
|
235
|
+
const senderKeyMutexes = new Map();
|
|
236
|
+
// Track last usage time for sender key mutexes (for cleanup)
|
|
237
|
+
const senderKeyMutexLastUsed = new Map();
|
|
238
|
+
// Mutex expiration time: 1 hour in milliseconds
|
|
239
|
+
const SENDER_KEY_MUTEX_EXPIRY_MS = 60 * 60 * 1000;
|
|
240
|
+
// Cleanup interval: every 30 minutes
|
|
241
|
+
const CLEANUP_INTERVAL_MS = 30 * 60 * 1000;
|
|
242
|
+
// Cleanup timer
|
|
243
|
+
let cleanupTimer = null;
|
|
244
|
+
// Start cleanup timer if not already running
|
|
245
|
+
function startCleanupTimer() {
|
|
246
|
+
if (!cleanupTimer) {
|
|
247
|
+
cleanupTimer = setInterval(() => {
|
|
248
|
+
cleanupExpiredSenderKeyMutexes();
|
|
249
|
+
}, CLEANUP_INTERVAL_MS);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Clean up expired sender key mutexes
|
|
253
|
+
function cleanupExpiredSenderKeyMutexes() {
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const expiredKeys = [];
|
|
256
|
+
for (const [senderKeyName, lastUsed] of senderKeyMutexLastUsed.entries()) {
|
|
257
|
+
if (now - lastUsed > SENDER_KEY_MUTEX_EXPIRY_MS) {
|
|
258
|
+
const mutex = senderKeyMutexes.get(senderKeyName);
|
|
259
|
+
// Only remove if mutex is not currently being used
|
|
260
|
+
if (mutex && !mutex.isLocked()) {
|
|
261
|
+
expiredKeys.push(senderKeyName);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (expiredKeys.length > 0) {
|
|
266
|
+
for (const key of expiredKeys) {
|
|
267
|
+
senderKeyMutexes.delete(key);
|
|
268
|
+
senderKeyMutexLastUsed.delete(key);
|
|
269
|
+
}
|
|
270
|
+
logger.info({ expiredKeys: expiredKeys.length }, 'cleaned up expired sender key mutexes');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
254
273
|
let transactionsInProgress = 0;
|
|
255
274
|
// Get or create a mutex for a specific key type
|
|
256
275
|
function getKeyTypeMutex(type) {
|
|
@@ -267,9 +286,14 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
267
286
|
let mutex = senderKeyMutexes.get(senderKeyName);
|
|
268
287
|
if (!mutex) {
|
|
269
288
|
mutex = new async_mutex_1.Mutex();
|
|
289
|
+
if (senderKeyMutexes.size === 0) {
|
|
290
|
+
startCleanupTimer();
|
|
291
|
+
}
|
|
270
292
|
senderKeyMutexes.set(senderKeyName, mutex);
|
|
271
293
|
logger.info({ senderKeyName }, 'created new sender key mutex');
|
|
272
294
|
}
|
|
295
|
+
// Update last used time
|
|
296
|
+
senderKeyMutexLastUsed.set(senderKeyName, Date.now());
|
|
273
297
|
return mutex;
|
|
274
298
|
}
|
|
275
299
|
// Sender key operations with proper mutex sequencing
|
|
@@ -341,10 +365,11 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
341
365
|
set: async (data) => {
|
|
342
366
|
if (isInTransaction()) {
|
|
343
367
|
logger.trace({ types: Object.keys(data) }, 'caching in transaction');
|
|
344
|
-
for (const
|
|
368
|
+
for (const key_ in data) {
|
|
369
|
+
const key = key_;
|
|
345
370
|
transactionCache[key] = transactionCache[key] || {};
|
|
346
371
|
// Special handling for pre-keys and signed-pre-keys
|
|
347
|
-
if (key === 'pre-key'
|
|
372
|
+
if (key === 'pre-key') {
|
|
348
373
|
await handlePreKeyOperations(data, key, transactionCache, mutations, logger, true);
|
|
349
374
|
}
|
|
350
375
|
else {
|
|
@@ -363,6 +388,7 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
363
388
|
for (const senderKeyName of senderKeyNames) {
|
|
364
389
|
await queueSenderKeyOperation(senderKeyName, async () => {
|
|
365
390
|
// Create data subset for this specific sender key
|
|
391
|
+
// @ts-ignore
|
|
366
392
|
const senderKeyData = {
|
|
367
393
|
'sender-key': {
|
|
368
394
|
[senderKeyName]: data['sender-key'][senderKeyName]
|
|
@@ -380,8 +406,9 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
380
406
|
if (Object.keys(nonSenderKeyData).length > 0) {
|
|
381
407
|
await withMutexes(Object.keys(nonSenderKeyData), getKeyTypeMutex, async () => {
|
|
382
408
|
// Process pre-keys and signed-pre-keys separately with specialized mutexes
|
|
383
|
-
for (const
|
|
384
|
-
|
|
409
|
+
for (const key_ in nonSenderKeyData) {
|
|
410
|
+
const keyType = key_;
|
|
411
|
+
if (keyType === 'pre-key') {
|
|
385
412
|
await processPreKeyDeletions(nonSenderKeyData, keyType, state, logger);
|
|
386
413
|
}
|
|
387
414
|
}
|
|
@@ -394,8 +421,9 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
394
421
|
// No sender keys - use original logic
|
|
395
422
|
await withMutexes(Object.keys(data), getKeyTypeMutex, async () => {
|
|
396
423
|
// Process pre-keys and signed-pre-keys separately with specialized mutexes
|
|
397
|
-
for (const
|
|
398
|
-
|
|
424
|
+
for (const key_ in data) {
|
|
425
|
+
const keyType = key_;
|
|
426
|
+
if (keyType === 'pre-key') {
|
|
399
427
|
await processPreKeyDeletions(data, keyType, state, logger);
|
|
400
428
|
}
|
|
401
429
|
}
|
|
@@ -406,49 +434,49 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
406
434
|
}
|
|
407
435
|
},
|
|
408
436
|
isInTransaction,
|
|
409
|
-
...(state.clear ? { clear: state.clear } : {}),
|
|
410
437
|
async transaction(work) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
438
|
+
let result;
|
|
439
|
+
transactionsInProgress += 1;
|
|
440
|
+
if (transactionsInProgress === 1) {
|
|
441
|
+
logger.trace('entering transaction');
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
result = await work();
|
|
445
|
+
// commit if this is the outermost transaction
|
|
446
|
+
if (transactionsInProgress === 1) {
|
|
447
|
+
if (Object.keys(mutations).length) {
|
|
448
|
+
logger.trace('committing transaction');
|
|
449
|
+
// retry mechanism to ensure we've some recovery
|
|
450
|
+
// in case a transaction fails in the first attempt
|
|
451
|
+
let tries = maxCommitRetries;
|
|
452
|
+
while (tries) {
|
|
453
|
+
tries -= 1;
|
|
454
|
+
//eslint-disable-next-line max-depth
|
|
455
|
+
try {
|
|
456
|
+
await state.set(mutations);
|
|
457
|
+
logger.trace({ dbQueriesInTransaction }, 'committed transaction');
|
|
458
|
+
break;
|
|
430
459
|
}
|
|
431
|
-
|
|
432
|
-
logger.
|
|
460
|
+
catch (error) {
|
|
461
|
+
logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`);
|
|
462
|
+
await (0, generics_1.delay)(delayBetweenTriesMs);
|
|
433
463
|
}
|
|
434
464
|
}
|
|
435
465
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (transactionsInProgress === 0) {
|
|
439
|
-
transactionCache = {};
|
|
440
|
-
mutations = {};
|
|
441
|
-
dbQueriesInTransaction = 0;
|
|
442
|
-
}
|
|
466
|
+
else {
|
|
467
|
+
logger.trace('no mutations in transaction');
|
|
443
468
|
}
|
|
444
|
-
return result;
|
|
445
469
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
transactionsInProgress -= 1;
|
|
473
|
+
if (transactionsInProgress === 0) {
|
|
474
|
+
transactionCache = {};
|
|
475
|
+
mutations = {};
|
|
476
|
+
dbQueriesInTransaction = 0;
|
|
450
477
|
}
|
|
451
|
-
}
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
452
480
|
}
|
|
453
481
|
};
|
|
454
482
|
};
|