monsqlize 3.3.0 → 3.3.2

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,5 +1,51 @@
1
1
  "use strict";
2
2
 
3
+ // src/core/cache-coordination.ts
4
+ var coordinators = /* @__PURE__ */ new WeakMap();
5
+ var origins = /* @__PURE__ */ new WeakMap();
6
+ function coordinator(cache) {
7
+ const origin = origins.get(cache) ?? cache;
8
+ let state = coordinators.get(origin);
9
+ if (!state) {
10
+ state = { tickets: /* @__PURE__ */ new Set(), tails: /* @__PURE__ */ new Map() };
11
+ coordinators.set(origin, state);
12
+ }
13
+ return state;
14
+ }
15
+ function matches(intent, key) {
16
+ if (intent.type === "key") return intent.value === key;
17
+ const pattern = intent.value.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
18
+ return new RegExp(`^${pattern}$`).test(key);
19
+ }
20
+ function serialize(state, key, task) {
21
+ const result = (state.tails.get(key) ?? Promise.resolve()).catch(() => void 0).then(task);
22
+ state.tails.set(key, result);
23
+ void result.finally(() => {
24
+ if (state.tails.get(key) === result) state.tails.delete(key);
25
+ }).catch(() => void 0);
26
+ return result;
27
+ }
28
+ async function coordinateInvalidation(cache, intents, task) {
29
+ const state = coordinator(cache);
30
+ for (const ticket of state.tickets) {
31
+ if (intents.some((intent) => matches(intent, ticket.key))) ticket.valid = false;
32
+ }
33
+ const keys = [...state.tails.keys()].filter((key) => intents.some((intent) => matches(intent, key)));
34
+ const previous = keys.map((key) => state.tails.get(key));
35
+ let finish;
36
+ const done = new Promise((resolve) => {
37
+ finish = resolve;
38
+ });
39
+ const pending = keys.map((key) => serialize(state, key, () => done));
40
+ try {
41
+ await Promise.allSettled(previous);
42
+ return await task();
43
+ } finally {
44
+ finish();
45
+ await Promise.allSettled(pending);
46
+ }
47
+ }
48
+
3
49
  // src/capabilities/transaction/index.ts
4
50
  var import_node_crypto = require("node:crypto");
5
51
 
@@ -148,8 +194,13 @@ var Transaction = class {
148
194
  constructor(session, options = {}) {
149
195
  this.session = session;
150
196
  this.options = options;
197
+ this.commitPromise = null;
198
+ this.abortPromise = null;
199
+ this.endPromise = null;
200
+ this.commitOutcomeUnknown = false;
151
201
  this.id = `tx_${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
152
202
  this.state = "pending";
203
+ this.acceptingWrites = false;
153
204
  this.startedAt = null;
154
205
  this.timeoutTimer = null;
155
206
  this.pendingInvalidations = /* @__PURE__ */ new Set();
@@ -163,11 +214,13 @@ var Transaction = class {
163
214
  * @since v1.4.0
164
215
  */
165
216
  async start() {
217
+ if (this.endPromise || this.abortPromise) throw createError(ErrorCodes.INVALID_OPERATION, "Transaction is already ending.");
166
218
  if (this.state !== "pending") {
167
219
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
168
220
  }
169
221
  this.session.startTransaction(this.options.transactionOptions);
170
222
  this.state = "active";
223
+ this.acceptingWrites = true;
171
224
  this.startedAt = Date.now();
172
225
  const timeout = this.options.timeout ?? 3e4;
173
226
  if (timeout > 0) {
@@ -187,11 +240,42 @@ var Transaction = class {
187
240
  * @since v1.4.0
188
241
  */
189
242
  async commit() {
243
+ if (this.commitPromise) return this.commitPromise;
244
+ if (this.endPromise || this.abortPromise) {
245
+ throw createError(ErrorCodes.INVALID_OPERATION, "Transaction is already ending.");
246
+ }
190
247
  if (this.state !== "active") {
191
248
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
192
249
  }
250
+ this.acceptingWrites = false;
251
+ const timeout = this.options.timeout ?? 3e4;
252
+ if (!this.commitOutcomeUnknown && timeout > 0 && this.getDuration() >= timeout) {
253
+ await this.abort();
254
+ throw createError(ErrorCodes.OPERATION_TIMEOUT, "Transaction work exceeded its timeout before commit.");
255
+ }
256
+ this.clearTimeout();
257
+ const committing = this.commitOnce();
258
+ this.commitPromise = committing;
259
+ try {
260
+ await committing;
261
+ } finally {
262
+ if (this.commitPromise === committing) this.commitPromise = null;
263
+ }
264
+ }
265
+ async commitOnce() {
193
266
  if (typeof this.session.commitTransaction === "function") {
194
- await commitTransactionWithRetry(this.session, this.options.logger);
267
+ try {
268
+ await commitTransactionWithRetry(this.session, this.options.logger);
269
+ this.commitOutcomeUnknown = false;
270
+ } catch (error) {
271
+ this.commitOutcomeUnknown = isUnknownTransactionCommitResult(error);
272
+ if (this.commitOutcomeUnknown) {
273
+ await this.flushPendingInvalidations().catch((cacheError) => {
274
+ this.options.logger?.warn?.("[Transaction] uncertain commit cache invalidation failed.", cacheError);
275
+ });
276
+ }
277
+ throw error;
278
+ }
195
279
  }
196
280
  this.state = "committed";
197
281
  try {
@@ -215,6 +299,14 @@ var Transaction = class {
215
299
  * @since v1.4.0
216
300
  */
217
301
  async abort() {
302
+ this.acceptingWrites = false;
303
+ if (this.commitPromise) await this.commitPromise.catch(() => void 0);
304
+ if (this.commitOutcomeUnknown) return;
305
+ if (this.abortPromise) return this.abortPromise;
306
+ this.abortPromise = this.abortOnce();
307
+ return this.abortPromise;
308
+ }
309
+ async abortOnce() {
218
310
  if (this.state !== "pending" && this.state !== "active") {
219
311
  return;
220
312
  }
@@ -239,10 +331,24 @@ var Transaction = class {
239
331
  * @since v1.4.0
240
332
  */
241
333
  async end() {
334
+ if (this.endPromise) return this.endPromise;
335
+ this.endPromise = this.endOnce();
336
+ return this.endPromise;
337
+ }
338
+ async endOnce() {
339
+ this.acceptingWrites = false;
340
+ if (this.commitPromise) await this.commitPromise.catch(() => void 0);
341
+ if (this.abortPromise) await this.abortPromise;
342
+ else if (this.state === "active" && !this.commitOutcomeUnknown) await this.abort();
242
343
  this.clearTimeout();
243
344
  this.options.lockManager?.releaseLocks(this.id);
244
345
  await this.session.endSession();
245
346
  }
347
+ assertAcceptingWrites() {
348
+ if (this.state !== "active" || !this.acceptingWrites) {
349
+ throw createError(ErrorCodes.INVALID_OPERATION, `Managed transaction ${this.id} is no longer accepting writes.`);
350
+ }
351
+ }
246
352
  /**
247
353
  * Record a cache invalidation intent.
248
354
  * @since v1.4.0
@@ -270,15 +376,17 @@ var Transaction = class {
270
376
  return;
271
377
  }
272
378
  const cacheWithDelete = this.options.cache;
273
- for (const intent of this.pendingInvalidationIntents.values()) {
274
- if (intent.type === "pattern") {
275
- await this.options.cache.delPattern?.(intent.value);
276
- } else if (typeof cacheWithDelete.del === "function") {
277
- await cacheWithDelete.del(intent.value);
278
- } else if (typeof cacheWithDelete.delete === "function") {
279
- await cacheWithDelete.delete(intent.value);
379
+ await coordinateInvalidation(this.options.cache, [...this.pendingInvalidationIntents.values()], async () => {
380
+ for (const intent of this.pendingInvalidationIntents.values()) {
381
+ if (intent.type === "pattern") {
382
+ await cacheWithDelete.delPattern?.(intent.value);
383
+ } else if (typeof cacheWithDelete.del === "function") {
384
+ await cacheWithDelete.del(intent.value);
385
+ } else if (typeof cacheWithDelete.delete === "function") {
386
+ await cacheWithDelete.delete(intent.value);
387
+ }
280
388
  }
281
- }
389
+ });
282
390
  }
283
391
  async clearPendingInvalidationBarriers() {
284
392
  if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
@@ -353,12 +461,30 @@ function stringifySessionId(id) {
353
461
  }
354
462
  return String(id);
355
463
  }
356
- function isUnknownTransactionCommitResult(error) {
357
- if (!error || typeof error !== "object") {
358
- return false;
464
+ function transactionErrorChain(error) {
465
+ const chain = [];
466
+ const seen = /* @__PURE__ */ new Set();
467
+ let current = error;
468
+ while (current && typeof current === "object" && chain.length < 8 && !seen.has(current)) {
469
+ seen.add(current);
470
+ const candidate = current;
471
+ chain.push(candidate);
472
+ current = candidate.cause;
473
+ }
474
+ return chain;
475
+ }
476
+ function hasTransactionErrorLabel(candidate, label) {
477
+ if (Array.isArray(candidate.errorLabels) && candidate.errorLabels.includes(label)) return true;
478
+ if (typeof candidate.hasErrorLabel === "function") {
479
+ try {
480
+ return Boolean(candidate.hasErrorLabel.call(candidate, label));
481
+ } catch {
482
+ }
359
483
  }
360
- const candidate = error;
361
- return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
484
+ return false;
485
+ }
486
+ function isUnknownTransactionCommitResult(error) {
487
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "UnknownTransactionCommitResult"));
362
488
  }
363
489
  async function commitTransactionWithRetry(session, logger) {
364
490
  const maxAttempts = 3;
@@ -1,5 +1,51 @@
1
1
  "use strict";
2
2
 
3
+ // src/core/cache-coordination.ts
4
+ var coordinators = /* @__PURE__ */ new WeakMap();
5
+ var origins = /* @__PURE__ */ new WeakMap();
6
+ function coordinator(cache) {
7
+ const origin = origins.get(cache) ?? cache;
8
+ let state = coordinators.get(origin);
9
+ if (!state) {
10
+ state = { tickets: /* @__PURE__ */ new Set(), tails: /* @__PURE__ */ new Map() };
11
+ coordinators.set(origin, state);
12
+ }
13
+ return state;
14
+ }
15
+ function matches(intent, key) {
16
+ if (intent.type === "key") return intent.value === key;
17
+ const pattern = intent.value.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
18
+ return new RegExp(`^${pattern}$`).test(key);
19
+ }
20
+ function serialize(state, key, task) {
21
+ const result = (state.tails.get(key) ?? Promise.resolve()).catch(() => void 0).then(task);
22
+ state.tails.set(key, result);
23
+ void result.finally(() => {
24
+ if (state.tails.get(key) === result) state.tails.delete(key);
25
+ }).catch(() => void 0);
26
+ return result;
27
+ }
28
+ async function coordinateInvalidation(cache, intents, task) {
29
+ const state = coordinator(cache);
30
+ for (const ticket of state.tickets) {
31
+ if (intents.some((intent) => matches(intent, ticket.key))) ticket.valid = false;
32
+ }
33
+ const keys = [...state.tails.keys()].filter((key) => intents.some((intent) => matches(intent, key)));
34
+ const previous = keys.map((key) => state.tails.get(key));
35
+ let finish;
36
+ const done = new Promise((resolve) => {
37
+ finish = resolve;
38
+ });
39
+ const pending = keys.map((key) => serialize(state, key, () => done));
40
+ try {
41
+ await Promise.allSettled(previous);
42
+ return await task();
43
+ } finally {
44
+ finish();
45
+ await Promise.allSettled(pending);
46
+ }
47
+ }
48
+
3
49
  // src/capabilities/transaction/index.ts
4
50
  var import_node_crypto = require("node:crypto");
5
51
 
@@ -148,8 +194,13 @@ var Transaction = class {
148
194
  constructor(session, options = {}) {
149
195
  this.session = session;
150
196
  this.options = options;
197
+ this.commitPromise = null;
198
+ this.abortPromise = null;
199
+ this.endPromise = null;
200
+ this.commitOutcomeUnknown = false;
151
201
  this.id = `tx_${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
152
202
  this.state = "pending";
203
+ this.acceptingWrites = false;
153
204
  this.startedAt = null;
154
205
  this.timeoutTimer = null;
155
206
  this.pendingInvalidations = /* @__PURE__ */ new Set();
@@ -163,11 +214,13 @@ var Transaction = class {
163
214
  * @since v1.4.0
164
215
  */
165
216
  async start() {
217
+ if (this.endPromise || this.abortPromise) throw createError(ErrorCodes.INVALID_OPERATION, "Transaction is already ending.");
166
218
  if (this.state !== "pending") {
167
219
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
168
220
  }
169
221
  this.session.startTransaction(this.options.transactionOptions);
170
222
  this.state = "active";
223
+ this.acceptingWrites = true;
171
224
  this.startedAt = Date.now();
172
225
  const timeout = this.options.timeout ?? 3e4;
173
226
  if (timeout > 0) {
@@ -187,11 +240,42 @@ var Transaction = class {
187
240
  * @since v1.4.0
188
241
  */
189
242
  async commit() {
243
+ if (this.commitPromise) return this.commitPromise;
244
+ if (this.endPromise || this.abortPromise) {
245
+ throw createError(ErrorCodes.INVALID_OPERATION, "Transaction is already ending.");
246
+ }
190
247
  if (this.state !== "active") {
191
248
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
192
249
  }
250
+ this.acceptingWrites = false;
251
+ const timeout = this.options.timeout ?? 3e4;
252
+ if (!this.commitOutcomeUnknown && timeout > 0 && this.getDuration() >= timeout) {
253
+ await this.abort();
254
+ throw createError(ErrorCodes.OPERATION_TIMEOUT, "Transaction work exceeded its timeout before commit.");
255
+ }
256
+ this.clearTimeout();
257
+ const committing = this.commitOnce();
258
+ this.commitPromise = committing;
259
+ try {
260
+ await committing;
261
+ } finally {
262
+ if (this.commitPromise === committing) this.commitPromise = null;
263
+ }
264
+ }
265
+ async commitOnce() {
193
266
  if (typeof this.session.commitTransaction === "function") {
194
- await commitTransactionWithRetry(this.session, this.options.logger);
267
+ try {
268
+ await commitTransactionWithRetry(this.session, this.options.logger);
269
+ this.commitOutcomeUnknown = false;
270
+ } catch (error) {
271
+ this.commitOutcomeUnknown = isUnknownTransactionCommitResult(error);
272
+ if (this.commitOutcomeUnknown) {
273
+ await this.flushPendingInvalidations().catch((cacheError) => {
274
+ this.options.logger?.warn?.("[Transaction] uncertain commit cache invalidation failed.", cacheError);
275
+ });
276
+ }
277
+ throw error;
278
+ }
195
279
  }
196
280
  this.state = "committed";
197
281
  try {
@@ -215,6 +299,14 @@ var Transaction = class {
215
299
  * @since v1.4.0
216
300
  */
217
301
  async abort() {
302
+ this.acceptingWrites = false;
303
+ if (this.commitPromise) await this.commitPromise.catch(() => void 0);
304
+ if (this.commitOutcomeUnknown) return;
305
+ if (this.abortPromise) return this.abortPromise;
306
+ this.abortPromise = this.abortOnce();
307
+ return this.abortPromise;
308
+ }
309
+ async abortOnce() {
218
310
  if (this.state !== "pending" && this.state !== "active") {
219
311
  return;
220
312
  }
@@ -239,10 +331,24 @@ var Transaction = class {
239
331
  * @since v1.4.0
240
332
  */
241
333
  async end() {
334
+ if (this.endPromise) return this.endPromise;
335
+ this.endPromise = this.endOnce();
336
+ return this.endPromise;
337
+ }
338
+ async endOnce() {
339
+ this.acceptingWrites = false;
340
+ if (this.commitPromise) await this.commitPromise.catch(() => void 0);
341
+ if (this.abortPromise) await this.abortPromise;
342
+ else if (this.state === "active" && !this.commitOutcomeUnknown) await this.abort();
242
343
  this.clearTimeout();
243
344
  this.options.lockManager?.releaseLocks(this.id);
244
345
  await this.session.endSession();
245
346
  }
347
+ assertAcceptingWrites() {
348
+ if (this.state !== "active" || !this.acceptingWrites) {
349
+ throw createError(ErrorCodes.INVALID_OPERATION, `Managed transaction ${this.id} is no longer accepting writes.`);
350
+ }
351
+ }
246
352
  /**
247
353
  * Record a cache invalidation intent.
248
354
  * @since v1.4.0
@@ -270,15 +376,17 @@ var Transaction = class {
270
376
  return;
271
377
  }
272
378
  const cacheWithDelete = this.options.cache;
273
- for (const intent of this.pendingInvalidationIntents.values()) {
274
- if (intent.type === "pattern") {
275
- await this.options.cache.delPattern?.(intent.value);
276
- } else if (typeof cacheWithDelete.del === "function") {
277
- await cacheWithDelete.del(intent.value);
278
- } else if (typeof cacheWithDelete.delete === "function") {
279
- await cacheWithDelete.delete(intent.value);
379
+ await coordinateInvalidation(this.options.cache, [...this.pendingInvalidationIntents.values()], async () => {
380
+ for (const intent of this.pendingInvalidationIntents.values()) {
381
+ if (intent.type === "pattern") {
382
+ await cacheWithDelete.delPattern?.(intent.value);
383
+ } else if (typeof cacheWithDelete.del === "function") {
384
+ await cacheWithDelete.del(intent.value);
385
+ } else if (typeof cacheWithDelete.delete === "function") {
386
+ await cacheWithDelete.delete(intent.value);
387
+ }
280
388
  }
281
- }
389
+ });
282
390
  }
283
391
  async clearPendingInvalidationBarriers() {
284
392
  if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
@@ -416,12 +524,12 @@ var TransactionManager = class {
416
524
  return result;
417
525
  } catch (error) {
418
526
  lastError = error;
419
- await transaction.abort();
420
- this.recordStats(transaction, Date.now() - startedAt, false);
421
527
  if (transaction.state === "committed") {
422
528
  throw error;
423
529
  }
424
- if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
530
+ await transaction.abort();
531
+ this.recordStats(transaction, Date.now() - startedAt, false);
532
+ if (!enableRetry || attempt === maxRetries || isUnknownTransactionCommitResult(error) || !isTransientTransactionError(error)) {
425
533
  throw error;
426
534
  }
427
535
  await sleep(retryDelay * Math.pow(retryBackoff, attempt));
@@ -522,22 +630,34 @@ function stringifySessionId(id) {
522
630
  }
523
631
  return String(id);
524
632
  }
525
- function isTransientTransactionError(error) {
526
- if (!error || typeof error !== "object") {
527
- return false;
528
- }
529
- const candidate = error;
530
- if (typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("TransientTransactionError")) {
531
- return true;
633
+ function transactionErrorChain(error) {
634
+ const chain = [];
635
+ const seen = /* @__PURE__ */ new Set();
636
+ let current = error;
637
+ while (current && typeof current === "object" && chain.length < 8 && !seen.has(current)) {
638
+ seen.add(current);
639
+ const candidate = current;
640
+ chain.push(candidate);
641
+ current = candidate.cause;
642
+ }
643
+ return chain;
644
+ }
645
+ function hasTransactionErrorLabel(candidate, label) {
646
+ if (Array.isArray(candidate.errorLabels) && candidate.errorLabels.includes(label)) return true;
647
+ if (typeof candidate.hasErrorLabel === "function") {
648
+ try {
649
+ return Boolean(candidate.hasErrorLabel.call(candidate, label));
650
+ } catch {
651
+ }
532
652
  }
533
- return candidate.code === 112 || candidate.code === 117;
653
+ return false;
534
654
  }
535
655
  function isUnknownTransactionCommitResult(error) {
536
- if (!error || typeof error !== "object") {
537
- return false;
538
- }
539
- const candidate = error;
540
- return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
656
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "UnknownTransactionCommitResult"));
657
+ }
658
+ function isTransientTransactionError(error) {
659
+ if (isUnknownTransactionCommitResult(error)) return false;
660
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "TransientTransactionError") || candidate.code === 112 || candidate.code === 117 || candidate.codeName === "WriteConflict" || candidate.codeName === "LockTimeout");
541
661
  }
542
662
  async function commitTransactionWithRetry(session, logger) {
543
663
  const maxAttempts = 3;