monsqlize 3.3.1 → 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,6 +194,10 @@ 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";
153
203
  this.acceptingWrites = false;
@@ -164,6 +214,7 @@ var Transaction = class {
164
214
  * @since v1.4.0
165
215
  */
166
216
  async start() {
217
+ if (this.endPromise || this.abortPromise) throw createError(ErrorCodes.INVALID_OPERATION, "Transaction is already ending.");
167
218
  if (this.state !== "pending") {
168
219
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
169
220
  }
@@ -189,12 +240,42 @@ var Transaction = class {
189
240
  * @since v1.4.0
190
241
  */
191
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
+ }
192
247
  if (this.state !== "active") {
193
248
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
194
249
  }
195
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() {
196
266
  if (typeof this.session.commitTransaction === "function") {
197
- 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
+ }
198
279
  }
199
280
  this.state = "committed";
200
281
  try {
@@ -219,6 +300,13 @@ var Transaction = class {
219
300
  */
220
301
  async abort() {
221
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() {
222
310
  if (this.state !== "pending" && this.state !== "active") {
223
311
  return;
224
312
  }
@@ -243,7 +331,15 @@ var Transaction = class {
243
331
  * @since v1.4.0
244
332
  */
245
333
  async end() {
334
+ if (this.endPromise) return this.endPromise;
335
+ this.endPromise = this.endOnce();
336
+ return this.endPromise;
337
+ }
338
+ async endOnce() {
246
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();
247
343
  this.clearTimeout();
248
344
  this.options.lockManager?.releaseLocks(this.id);
249
345
  await this.session.endSession();
@@ -280,15 +376,17 @@ var Transaction = class {
280
376
  return;
281
377
  }
282
378
  const cacheWithDelete = this.options.cache;
283
- for (const intent of this.pendingInvalidationIntents.values()) {
284
- if (intent.type === "pattern") {
285
- await this.options.cache.delPattern?.(intent.value);
286
- } else if (typeof cacheWithDelete.del === "function") {
287
- await cacheWithDelete.del(intent.value);
288
- } else if (typeof cacheWithDelete.delete === "function") {
289
- 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
+ }
290
388
  }
291
- }
389
+ });
292
390
  }
293
391
  async clearPendingInvalidationBarriers() {
294
392
  if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
@@ -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,6 +194,10 @@ 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";
153
203
  this.acceptingWrites = false;
@@ -164,6 +214,7 @@ var Transaction = class {
164
214
  * @since v1.4.0
165
215
  */
166
216
  async start() {
217
+ if (this.endPromise || this.abortPromise) throw createError(ErrorCodes.INVALID_OPERATION, "Transaction is already ending.");
167
218
  if (this.state !== "pending") {
168
219
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
169
220
  }
@@ -189,12 +240,42 @@ var Transaction = class {
189
240
  * @since v1.4.0
190
241
  */
191
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
+ }
192
247
  if (this.state !== "active") {
193
248
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
194
249
  }
195
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() {
196
266
  if (typeof this.session.commitTransaction === "function") {
197
- 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
+ }
198
279
  }
199
280
  this.state = "committed";
200
281
  try {
@@ -219,6 +300,13 @@ var Transaction = class {
219
300
  */
220
301
  async abort() {
221
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() {
222
310
  if (this.state !== "pending" && this.state !== "active") {
223
311
  return;
224
312
  }
@@ -243,7 +331,15 @@ var Transaction = class {
243
331
  * @since v1.4.0
244
332
  */
245
333
  async end() {
334
+ if (this.endPromise) return this.endPromise;
335
+ this.endPromise = this.endOnce();
336
+ return this.endPromise;
337
+ }
338
+ async endOnce() {
246
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();
247
343
  this.clearTimeout();
248
344
  this.options.lockManager?.releaseLocks(this.id);
249
345
  await this.session.endSession();
@@ -280,15 +376,17 @@ var Transaction = class {
280
376
  return;
281
377
  }
282
378
  const cacheWithDelete = this.options.cache;
283
- for (const intent of this.pendingInvalidationIntents.values()) {
284
- if (intent.type === "pattern") {
285
- await this.options.cache.delPattern?.(intent.value);
286
- } else if (typeof cacheWithDelete.del === "function") {
287
- await cacheWithDelete.del(intent.value);
288
- } else if (typeof cacheWithDelete.delete === "function") {
289
- 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
+ }
290
388
  }
291
- }
389
+ });
292
390
  }
293
391
  async clearPendingInvalidationBarriers() {
294
392
  if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {