monsqlize 2.0.6 → 3.0.0

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.
@@ -3,6 +3,52 @@
3
3
  // src/capabilities/transaction/index.ts
4
4
  var import_node_crypto = require("node:crypto");
5
5
 
6
+ // src/core/cache-invalidation-barrier.ts
7
+ var READ_CACHE_PATTERN_PREFIXES = /* @__PURE__ */ new Set([
8
+ "find",
9
+ "findOne",
10
+ "count",
11
+ "findOneById",
12
+ "findByIds",
13
+ "findPage",
14
+ "findPageTotals",
15
+ "aggregate",
16
+ "distinct"
17
+ ]);
18
+ function buildCacheInvalidationBarrierKey(namespace) {
19
+ return `cacheDirty:${namespace}`;
20
+ }
21
+ function extractCacheInvalidationBarrierNamespaces(values) {
22
+ const namespaces = /* @__PURE__ */ new Set();
23
+ for (const value of values) {
24
+ if (!value) {
25
+ continue;
26
+ }
27
+ if (!value.includes("*")) {
28
+ namespaces.add(value);
29
+ continue;
30
+ }
31
+ const match = /^([^:]+):(.+):\*$/.exec(value);
32
+ if (match && READ_CACHE_PATTERN_PREFIXES.has(match[1])) {
33
+ namespaces.add(match[2]);
34
+ }
35
+ }
36
+ return [...namespaces];
37
+ }
38
+ async function clearCacheInvalidationBarrier(cache, namespacesOrPatterns) {
39
+ if (!cache) {
40
+ return;
41
+ }
42
+ const remove = cache.del ?? cache.delete;
43
+ if (!remove) {
44
+ return;
45
+ }
46
+ const namespaces = extractCacheInvalidationBarrierNamespaces(namespacesOrPatterns);
47
+ for (const namespace of namespaces) {
48
+ await Promise.resolve(remove.call(cache, buildCacheInvalidationBarrierKey(namespace)));
49
+ }
50
+ }
51
+
6
52
  // src/core/errors/index.ts
7
53
  var ErrorCodes = {
8
54
  INVALID_ARGUMENT: "INVALID_ARGUMENT",
@@ -99,6 +145,9 @@ var Transaction = class {
99
145
  this.startedAt = null;
100
146
  this.timeoutTimer = null;
101
147
  this.pendingInvalidations = /* @__PURE__ */ new Set();
148
+ this.pendingInvalidationIntents = /* @__PURE__ */ new Map();
149
+ this.recordedWriteOperationCount = 0;
150
+ this.recordedInvalidationCount = 0;
102
151
  this.session.__monSQLizeTransaction = this;
103
152
  }
104
153
  /**
@@ -117,7 +166,9 @@ var Transaction = class {
117
166
  this.timeoutTimer = setTimeout(() => {
118
167
  if (this.state === "active") {
119
168
  this.options.logger?.warn?.(`[Transaction] auto-abort on timeout: ${this.id}`);
120
- void this.abort();
169
+ void this.abort().catch((error) => {
170
+ this.options.logger?.warn?.("[Transaction] auto-abort failed.", error);
171
+ });
121
172
  }
122
173
  }, timeout);
123
174
  this.timeoutTimer.unref?.();
@@ -132,12 +183,24 @@ var Transaction = class {
132
183
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
133
184
  }
134
185
  if (typeof this.session.commitTransaction === "function") {
135
- await this.session.commitTransaction();
186
+ await commitTransactionWithRetry(this.session, this.options.logger);
136
187
  }
137
188
  this.state = "committed";
138
- this.options.lockManager?.releaseLocks(this.id);
139
- this.pendingInvalidations.clear();
140
- this.clearTimeout();
189
+ try {
190
+ try {
191
+ await this.flushPendingInvalidations();
192
+ } catch (error) {
193
+ this.options.logger?.warn?.("[Transaction] post-commit cache invalidation failed.", error);
194
+ }
195
+ } finally {
196
+ await this.clearPendingInvalidationBarriers().catch((error) => {
197
+ this.options.logger?.warn?.("[Transaction] failed to clear cache invalidation barrier.", error);
198
+ });
199
+ this.options.lockManager?.releaseLocks(this.id);
200
+ this.pendingInvalidations.clear();
201
+ this.pendingInvalidationIntents.clear();
202
+ this.clearTimeout();
203
+ }
141
204
  }
142
205
  /**
143
206
  * Roll back the transaction.
@@ -147,15 +210,21 @@ var Transaction = class {
147
210
  if (this.state !== "pending" && this.state !== "active") {
148
211
  return;
149
212
  }
150
- if (this.state === "active") {
151
- if (typeof this.session.abortTransaction === "function") {
152
- await this.session.abortTransaction();
213
+ try {
214
+ if (this.state === "active") {
215
+ if (typeof this.session.abortTransaction === "function") {
216
+ await this.session.abortTransaction();
217
+ }
153
218
  }
219
+ } catch (error) {
220
+ this.options.logger?.warn?.("[Transaction] abortTransaction failed.", error);
221
+ } finally {
222
+ this.state = "aborted";
223
+ this.options.lockManager?.releaseLocks(this.id);
224
+ this.pendingInvalidations.clear();
225
+ this.pendingInvalidationIntents.clear();
226
+ this.clearTimeout();
154
227
  }
155
- this.state = "aborted";
156
- this.options.lockManager?.releaseLocks(this.id);
157
- this.pendingInvalidations.clear();
158
- this.clearTimeout();
159
228
  }
160
229
  /**
161
230
  * End the transaction session.
@@ -171,10 +240,45 @@ var Transaction = class {
171
240
  * @since v1.4.0
172
241
  */
173
242
  async recordInvalidation(pattern) {
174
- this.pendingInvalidations.add(pattern);
175
- this.options.lockManager?.addLock(pattern, this.id);
176
- if (this.options.cache?.delPattern) {
177
- await this.options.cache.delPattern(pattern);
243
+ await this.recordCacheInvalidation({ type: "pattern", value: pattern });
244
+ }
245
+ async recordCacheInvalidation(intent) {
246
+ if (!intent.value) {
247
+ return;
248
+ }
249
+ const id = `${intent.type}:${intent.value}`;
250
+ if (!this.pendingInvalidationIntents.has(id)) {
251
+ this.recordedInvalidationCount += 1;
252
+ }
253
+ this.pendingInvalidationIntents.set(id, intent);
254
+ this.pendingInvalidations.add(intent.value);
255
+ this.options.lockManager?.addLock(intent.value, this.id);
256
+ }
257
+ recordWriteOperation() {
258
+ this.recordedWriteOperationCount += 1;
259
+ }
260
+ async flushPendingInvalidations() {
261
+ if (!this.options.cache) {
262
+ return;
263
+ }
264
+ const cacheWithDelete = this.options.cache;
265
+ for (const intent of this.pendingInvalidationIntents.values()) {
266
+ if (intent.type === "pattern") {
267
+ await this.options.cache.delPattern?.(intent.value);
268
+ } else if (typeof cacheWithDelete.del === "function") {
269
+ await cacheWithDelete.del(intent.value);
270
+ } else if (typeof cacheWithDelete.delete === "function") {
271
+ await cacheWithDelete.delete(intent.value);
272
+ }
273
+ }
274
+ }
275
+ async clearPendingInvalidationBarriers() {
276
+ if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
277
+ return;
278
+ }
279
+ const patterns = [...this.pendingInvalidationIntents.values()].filter((intent) => intent.type === "pattern").map((intent) => intent.value);
280
+ if (patterns.length > 0) {
281
+ await clearCacheInvalidationBarrier(this.options.cache, patterns);
178
282
  }
179
283
  }
180
284
  /**
@@ -208,11 +312,14 @@ var Transaction = class {
208
312
  id: this.id,
209
313
  state: this.state,
210
314
  duration: this.getDuration(),
211
- hasWriteOperation: this.pendingInvalidations.size > 0,
212
- operationCount: this.pendingInvalidations.size,
213
- lockedKeysCount: this.pendingInvalidations.size
315
+ hasWriteOperation: this.hasWriteOperation(),
316
+ operationCount: this.recordedWriteOperationCount || this.recordedInvalidationCount,
317
+ lockedKeysCount: this.recordedInvalidationCount
214
318
  };
215
319
  }
320
+ hasWriteOperation() {
321
+ return this.recordedWriteOperationCount > 0 || this.recordedInvalidationCount > 0;
322
+ }
216
323
  clearTimeout() {
217
324
  if (this.timeoutTimer) {
218
325
  clearTimeout(this.timeoutTimer);
@@ -238,6 +345,31 @@ function stringifySessionId(id) {
238
345
  }
239
346
  return String(id);
240
347
  }
348
+ function isUnknownTransactionCommitResult(error) {
349
+ if (!error || typeof error !== "object") {
350
+ return false;
351
+ }
352
+ const candidate = error;
353
+ return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
354
+ }
355
+ async function commitTransactionWithRetry(session, logger) {
356
+ const maxAttempts = 3;
357
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
358
+ try {
359
+ await session.commitTransaction();
360
+ return;
361
+ } catch (error) {
362
+ if (!isUnknownTransactionCommitResult(error) || attempt === maxAttempts - 1) {
363
+ throw error;
364
+ }
365
+ logger?.warn?.(`[Transaction] retrying commit after UnknownTransactionCommitResult (attempt ${attempt + 2}/${maxAttempts})`);
366
+ await sleep(100 * (attempt + 1));
367
+ }
368
+ }
369
+ }
370
+ async function sleep(ms) {
371
+ await new Promise((resolve) => setTimeout(resolve, ms));
372
+ }
241
373
 
242
374
  // src/entry/compat/transaction/Transaction.ts
243
375
  module.exports = Transaction;
@@ -3,6 +3,52 @@
3
3
  // src/capabilities/transaction/index.ts
4
4
  var import_node_crypto = require("node:crypto");
5
5
 
6
+ // src/core/cache-invalidation-barrier.ts
7
+ var READ_CACHE_PATTERN_PREFIXES = /* @__PURE__ */ new Set([
8
+ "find",
9
+ "findOne",
10
+ "count",
11
+ "findOneById",
12
+ "findByIds",
13
+ "findPage",
14
+ "findPageTotals",
15
+ "aggregate",
16
+ "distinct"
17
+ ]);
18
+ function buildCacheInvalidationBarrierKey(namespace) {
19
+ return `cacheDirty:${namespace}`;
20
+ }
21
+ function extractCacheInvalidationBarrierNamespaces(values) {
22
+ const namespaces = /* @__PURE__ */ new Set();
23
+ for (const value of values) {
24
+ if (!value) {
25
+ continue;
26
+ }
27
+ if (!value.includes("*")) {
28
+ namespaces.add(value);
29
+ continue;
30
+ }
31
+ const match = /^([^:]+):(.+):\*$/.exec(value);
32
+ if (match && READ_CACHE_PATTERN_PREFIXES.has(match[1])) {
33
+ namespaces.add(match[2]);
34
+ }
35
+ }
36
+ return [...namespaces];
37
+ }
38
+ async function clearCacheInvalidationBarrier(cache, namespacesOrPatterns) {
39
+ if (!cache) {
40
+ return;
41
+ }
42
+ const remove = cache.del ?? cache.delete;
43
+ if (!remove) {
44
+ return;
45
+ }
46
+ const namespaces = extractCacheInvalidationBarrierNamespaces(namespacesOrPatterns);
47
+ for (const namespace of namespaces) {
48
+ await Promise.resolve(remove.call(cache, buildCacheInvalidationBarrierKey(namespace)));
49
+ }
50
+ }
51
+
6
52
  // src/core/errors/index.ts
7
53
  var ErrorCodes = {
8
54
  INVALID_ARGUMENT: "INVALID_ARGUMENT",
@@ -99,6 +145,9 @@ var Transaction = class {
99
145
  this.startedAt = null;
100
146
  this.timeoutTimer = null;
101
147
  this.pendingInvalidations = /* @__PURE__ */ new Set();
148
+ this.pendingInvalidationIntents = /* @__PURE__ */ new Map();
149
+ this.recordedWriteOperationCount = 0;
150
+ this.recordedInvalidationCount = 0;
102
151
  this.session.__monSQLizeTransaction = this;
103
152
  }
104
153
  /**
@@ -117,7 +166,9 @@ var Transaction = class {
117
166
  this.timeoutTimer = setTimeout(() => {
118
167
  if (this.state === "active") {
119
168
  this.options.logger?.warn?.(`[Transaction] auto-abort on timeout: ${this.id}`);
120
- void this.abort();
169
+ void this.abort().catch((error) => {
170
+ this.options.logger?.warn?.("[Transaction] auto-abort failed.", error);
171
+ });
121
172
  }
122
173
  }, timeout);
123
174
  this.timeoutTimer.unref?.();
@@ -132,12 +183,24 @@ var Transaction = class {
132
183
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
133
184
  }
134
185
  if (typeof this.session.commitTransaction === "function") {
135
- await this.session.commitTransaction();
186
+ await commitTransactionWithRetry(this.session, this.options.logger);
136
187
  }
137
188
  this.state = "committed";
138
- this.options.lockManager?.releaseLocks(this.id);
139
- this.pendingInvalidations.clear();
140
- this.clearTimeout();
189
+ try {
190
+ try {
191
+ await this.flushPendingInvalidations();
192
+ } catch (error) {
193
+ this.options.logger?.warn?.("[Transaction] post-commit cache invalidation failed.", error);
194
+ }
195
+ } finally {
196
+ await this.clearPendingInvalidationBarriers().catch((error) => {
197
+ this.options.logger?.warn?.("[Transaction] failed to clear cache invalidation barrier.", error);
198
+ });
199
+ this.options.lockManager?.releaseLocks(this.id);
200
+ this.pendingInvalidations.clear();
201
+ this.pendingInvalidationIntents.clear();
202
+ this.clearTimeout();
203
+ }
141
204
  }
142
205
  /**
143
206
  * Roll back the transaction.
@@ -147,15 +210,21 @@ var Transaction = class {
147
210
  if (this.state !== "pending" && this.state !== "active") {
148
211
  return;
149
212
  }
150
- if (this.state === "active") {
151
- if (typeof this.session.abortTransaction === "function") {
152
- await this.session.abortTransaction();
213
+ try {
214
+ if (this.state === "active") {
215
+ if (typeof this.session.abortTransaction === "function") {
216
+ await this.session.abortTransaction();
217
+ }
153
218
  }
219
+ } catch (error) {
220
+ this.options.logger?.warn?.("[Transaction] abortTransaction failed.", error);
221
+ } finally {
222
+ this.state = "aborted";
223
+ this.options.lockManager?.releaseLocks(this.id);
224
+ this.pendingInvalidations.clear();
225
+ this.pendingInvalidationIntents.clear();
226
+ this.clearTimeout();
154
227
  }
155
- this.state = "aborted";
156
- this.options.lockManager?.releaseLocks(this.id);
157
- this.pendingInvalidations.clear();
158
- this.clearTimeout();
159
228
  }
160
229
  /**
161
230
  * End the transaction session.
@@ -171,10 +240,45 @@ var Transaction = class {
171
240
  * @since v1.4.0
172
241
  */
173
242
  async recordInvalidation(pattern) {
174
- this.pendingInvalidations.add(pattern);
175
- this.options.lockManager?.addLock(pattern, this.id);
176
- if (this.options.cache?.delPattern) {
177
- await this.options.cache.delPattern(pattern);
243
+ await this.recordCacheInvalidation({ type: "pattern", value: pattern });
244
+ }
245
+ async recordCacheInvalidation(intent) {
246
+ if (!intent.value) {
247
+ return;
248
+ }
249
+ const id = `${intent.type}:${intent.value}`;
250
+ if (!this.pendingInvalidationIntents.has(id)) {
251
+ this.recordedInvalidationCount += 1;
252
+ }
253
+ this.pendingInvalidationIntents.set(id, intent);
254
+ this.pendingInvalidations.add(intent.value);
255
+ this.options.lockManager?.addLock(intent.value, this.id);
256
+ }
257
+ recordWriteOperation() {
258
+ this.recordedWriteOperationCount += 1;
259
+ }
260
+ async flushPendingInvalidations() {
261
+ if (!this.options.cache) {
262
+ return;
263
+ }
264
+ const cacheWithDelete = this.options.cache;
265
+ for (const intent of this.pendingInvalidationIntents.values()) {
266
+ if (intent.type === "pattern") {
267
+ await this.options.cache.delPattern?.(intent.value);
268
+ } else if (typeof cacheWithDelete.del === "function") {
269
+ await cacheWithDelete.del(intent.value);
270
+ } else if (typeof cacheWithDelete.delete === "function") {
271
+ await cacheWithDelete.delete(intent.value);
272
+ }
273
+ }
274
+ }
275
+ async clearPendingInvalidationBarriers() {
276
+ if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
277
+ return;
278
+ }
279
+ const patterns = [...this.pendingInvalidationIntents.values()].filter((intent) => intent.type === "pattern").map((intent) => intent.value);
280
+ if (patterns.length > 0) {
281
+ await clearCacheInvalidationBarrier(this.options.cache, patterns);
178
282
  }
179
283
  }
180
284
  /**
@@ -208,11 +312,14 @@ var Transaction = class {
208
312
  id: this.id,
209
313
  state: this.state,
210
314
  duration: this.getDuration(),
211
- hasWriteOperation: this.pendingInvalidations.size > 0,
212
- operationCount: this.pendingInvalidations.size,
213
- lockedKeysCount: this.pendingInvalidations.size
315
+ hasWriteOperation: this.hasWriteOperation(),
316
+ operationCount: this.recordedWriteOperationCount || this.recordedInvalidationCount,
317
+ lockedKeysCount: this.recordedInvalidationCount
214
318
  };
215
319
  }
320
+ hasWriteOperation() {
321
+ return this.recordedWriteOperationCount > 0 || this.recordedInvalidationCount > 0;
322
+ }
216
323
  clearTimeout() {
217
324
  if (this.timeoutTimer) {
218
325
  clearTimeout(this.timeoutTimer);
@@ -303,6 +410,9 @@ var TransactionManager = class {
303
410
  lastError = error;
304
411
  await transaction.abort();
305
412
  this.recordStats(transaction, Date.now() - startedAt, false);
413
+ if (transaction.state === "committed") {
414
+ throw error;
415
+ }
306
416
  if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
307
417
  throw error;
308
418
  }
@@ -364,7 +474,7 @@ var TransactionManager = class {
364
474
  } else {
365
475
  this.stats.failedTransactions += 1;
366
476
  }
367
- if (transaction.pendingInvalidations.size > 0) {
477
+ if (transaction.hasWriteOperation()) {
368
478
  this.stats.writeTransactions += 1;
369
479
  } else {
370
480
  this.stats.readOnlyTransactions += 1;
@@ -414,6 +524,28 @@ function isTransientTransactionError(error) {
414
524
  }
415
525
  return candidate.code === 112 || candidate.code === 117;
416
526
  }
527
+ function isUnknownTransactionCommitResult(error) {
528
+ if (!error || typeof error !== "object") {
529
+ return false;
530
+ }
531
+ const candidate = error;
532
+ return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
533
+ }
534
+ async function commitTransactionWithRetry(session, logger) {
535
+ const maxAttempts = 3;
536
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
537
+ try {
538
+ await session.commitTransaction();
539
+ return;
540
+ } catch (error) {
541
+ if (!isUnknownTransactionCommitResult(error) || attempt === maxAttempts - 1) {
542
+ throw error;
543
+ }
544
+ logger?.warn?.(`[Transaction] retrying commit after UnknownTransactionCommitResult (attempt ${attempt + 2}/${maxAttempts})`);
545
+ await sleep(100 * (attempt + 1));
546
+ }
547
+ }
548
+ }
417
549
  async function sleep(ms) {
418
550
  await new Promise((resolve) => setTimeout(resolve, ms));
419
551
  }