monsqlize 3.2.0 → 3.3.1

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.
@@ -187,12 +187,17 @@ function createRedisCacheAdapter(redisUrlOrInstance, options) {
187
187
 
188
188
  // src/capabilities/cache/distributed-cache-invalidator.ts
189
189
  var import_crypto = require("crypto");
190
+ var import_cache_hub2 = require("cache-hub");
190
191
  var DistributedCacheInvalidator = class {
191
192
  constructor(options) {
192
193
  if (!options.cache) {
193
194
  throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
194
195
  }
195
196
  this._cache = options.cache;
197
+ const safeMemoryCache = this._cache instanceof import_cache_hub2.MemoryCache || this._cache.local instanceof import_cache_hub2.MemoryCache && this._cache.remote === void 0;
198
+ if (!safeMemoryCache && typeof this._cache.invalidateFromRemote !== "function") {
199
+ throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires receive-only cache invalidation");
200
+ }
196
201
  this._logger = options.logger ?? null;
197
202
  this.channel = options.channel ?? "monsqlize:cache:invalidate";
198
203
  this.instanceId = options.instanceId ?? `instance-${Date.now()}-${(0, import_crypto.randomBytes)(4).toString("hex")}`;
@@ -240,18 +245,10 @@ var DistributedCacheInvalidator = class {
240
245
  this._stats.messagesReceived++;
241
246
  try {
242
247
  if (isPatternInvalidation) {
243
- for (const targetCache of this._getPatternTargetCaches()) {
244
- await targetCache.delPattern?.(String(message.pattern));
245
- }
248
+ await this._invalidateFromRemote("pattern", String(message.pattern));
246
249
  this._logger?.debug?.(`[DistributedCacheInvalidator] Invalidated cache targets: ${String(message.pattern)}`);
247
250
  } else {
248
- for (const targetCache of this._getKeyTargetCaches()) {
249
- if (typeof targetCache.del === "function") {
250
- await targetCache.del(String(message.key));
251
- } else {
252
- await targetCache.delete?.(String(message.key));
253
- }
254
- }
251
+ await this._invalidateFromRemote("key", String(message.key));
255
252
  this._logger?.debug?.(`[DistributedCacheInvalidator] Invalidated cache key targets: ${String(message.key)}`);
256
253
  }
257
254
  this._stats.invalidationsTriggered++;
@@ -261,32 +258,18 @@ var DistributedCacheInvalidator = class {
261
258
  }
262
259
  });
263
260
  }
264
- _getPatternTargetCaches() {
265
- const targets = /* @__PURE__ */ new Set();
266
- if (typeof this._cache?.delPattern === "function") {
267
- targets.add(this._cache);
268
- }
269
- if (this._cache?.local && typeof this._cache.local.delPattern === "function") {
270
- targets.add(this._cache.local);
271
- }
272
- if (this._cache?.remote && typeof this._cache.remote.delPattern === "function") {
273
- targets.add(this._cache.remote);
274
- }
275
- return [...targets];
276
- }
277
- _getKeyTargetCaches() {
278
- const targets = /* @__PURE__ */ new Set();
279
- const supportsKeyDelete = (cache) => typeof cache?.del === "function" || typeof cache?.delete === "function";
280
- if (supportsKeyDelete(this._cache)) {
281
- targets.add(this._cache);
282
- }
283
- if (supportsKeyDelete(this._cache?.local)) {
284
- targets.add(this._cache.local);
261
+ async _invalidateFromRemote(kind, value) {
262
+ if (this._cache instanceof import_cache_hub2.MemoryCache) {
263
+ if (kind === "pattern") await this._cache.delPattern(value);
264
+ else await this._cache.del(value);
265
+ return;
285
266
  }
286
- if (supportsKeyDelete(this._cache?.remote)) {
287
- targets.add(this._cache.remote);
267
+ if (this._cache.local instanceof import_cache_hub2.MemoryCache && this._cache.remote === void 0) {
268
+ if (kind === "pattern") await this._cache.local.delPattern(value);
269
+ else await this._cache.local.del(value);
270
+ return;
288
271
  }
289
- return [...targets];
272
+ await this._cache.invalidateFromRemote?.(kind, value);
290
273
  }
291
274
  async invalidate(pattern) {
292
275
  if (!pattern) return;
@@ -339,8 +322,8 @@ var DistributedCacheInvalidator = class {
339
322
  };
340
323
 
341
324
  // src/capabilities/cache/index.ts
342
- var import_cache_hub2 = require("cache-hub");
343
- var MultiLevelCache = class extends import_cache_hub2.MultiLevelCache {
325
+ var import_cache_hub3 = require("cache-hub");
326
+ var MultiLevelCache = class extends import_cache_hub3.MultiLevelCache {
344
327
  constructor(options) {
345
328
  const publishRef = { current: options.publish };
346
329
  super({
@@ -358,6 +341,15 @@ var MultiLevelCache = class extends import_cache_hub2.MultiLevelCache {
358
341
  this._localCompat.setLockManager?.(lockManager);
359
342
  this._remoteCompat?.setLockManager?.(lockManager);
360
343
  }
344
+ async invalidateFromRemote(kind, value) {
345
+ if (kind === "pattern") {
346
+ await this._localCompat.delPattern(value);
347
+ await this._remoteCompat?.delPattern(value);
348
+ } else {
349
+ await this._localCompat.del(value);
350
+ await this._remoteCompat?.del(value);
351
+ }
352
+ }
361
353
  };
362
354
  function adaptLegacyCacheLike(v1Cache) {
363
355
  if ("has" in v1Cache && typeof v1Cache.has === "function") {
@@ -549,6 +541,9 @@ function normalizePopulateConfig(path5) {
549
541
  }
550
542
 
551
543
  // src/capabilities/model/model-registry.ts
544
+ function defineModel(collectionName, definition) {
545
+ return { collectionName, definition };
546
+ }
552
547
  var Model = class {
553
548
  static {
554
549
  // collection name → registered model definition
@@ -563,21 +558,19 @@ var Model = class {
563
558
  /** Collection names that have been redefined (used by the runtime to trigger hot-reload). */
564
559
  this._redefinedNames = /* @__PURE__ */ new Set();
565
560
  }
566
- /**
567
- * Register a new model definition.
568
- * Throws MODEL_ALREADY_EXISTS if a model with the same name already exists.
569
- * Validates the definition and resolves the timestamps option.
570
- */
571
- static define(collectionName, definition) {
561
+ static define(collectionNameOrDescriptor, definition) {
562
+ const collectionName = typeof collectionNameOrDescriptor === "string" ? collectionNameOrDescriptor : collectionNameOrDescriptor.collectionName;
563
+ const modelDefinition = typeof collectionNameOrDescriptor === "string" ? definition : collectionNameOrDescriptor.definition;
572
564
  const normalizedName = validateCollectionName(collectionName);
573
565
  if (this.registry.has(normalizedName)) {
574
566
  throw createError(ErrorCodes.MODEL_ALREADY_EXISTS, `Model '${normalizedName}' is already defined.`);
575
567
  }
576
- validateDefinition(definition);
577
- processTimestamps(definition);
568
+ validateDefinition(modelDefinition);
569
+ const resolvedDefinition = modelDefinition;
570
+ processTimestamps(resolvedDefinition);
578
571
  this.registry.set(normalizedName, {
579
572
  collectionName: normalizedName,
580
- definition
573
+ definition: resolvedDefinition
581
574
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
582
575
  });
583
576
  this.bumpRevision(normalizedName);
@@ -734,6 +727,40 @@ function serializeDocument(document) {
734
727
  }
735
728
 
736
729
  // src/capabilities/model/model-write-helpers.ts
730
+ async function* iterateModelCandidateBatches(context, filter, options, batchSize) {
731
+ const lookupOptions = buildModelVersionLookupOptions(options, {
732
+ _id: 1,
733
+ [context.versionConfig?.field ?? "__v"]: 1
734
+ });
735
+ const stream = context.collection.stream(filter, {
736
+ ...lookupOptions,
737
+ sort: { _id: 1 },
738
+ batchSize
739
+ });
740
+ let batch = [];
741
+ try {
742
+ for await (const document of stream) {
743
+ batch.push(document);
744
+ if (batch.length === batchSize) {
745
+ yield batch;
746
+ batch = [];
747
+ }
748
+ }
749
+ if (batch.length > 0) yield batch;
750
+ } finally {
751
+ stream.destroy();
752
+ }
753
+ }
754
+ function buildStrictCandidateFilter(filter, id, versionField, expectedVersion) {
755
+ return { $and: [filter ?? {}, { _id: id }, { [versionField]: expectedVersion }] };
756
+ }
757
+ async function isModelVersionConflict(context, filter, id, versionField, expectedVersion, options) {
758
+ const current = await context.collection.findOne(
759
+ { $and: [filter ?? {}, { _id: id }] },
760
+ buildModelVersionLookupOptions(options, { [versionField]: 1 })
761
+ );
762
+ return current !== null && current !== void 0 && current[versionField] !== expectedVersion;
763
+ }
737
764
  var VERSION_LOOKUP_OPTION_KEYS = [
738
765
  "session",
739
766
  "readConcern",
@@ -819,11 +846,14 @@ function mapModelSchemaValidationErrors(errors = []) {
819
846
  }
820
847
  function validateModelSchemaPayload(context, document, options, metadata = {}) {
821
848
  const shouldValidate = context.validateEnabled || options?.validate === true;
822
- if (!shouldValidate) {
849
+ if (!shouldValidate || options?.skipValidation) {
823
850
  return document;
824
851
  }
825
- if (options?.skipValidation) {
826
- return document;
852
+ if (context.schemaError) {
853
+ throw withModelErrorMetadata(
854
+ createError(ErrorCodes.VALIDATION_ERROR, `Schema initialization failed: ${context.schemaError.message}`),
855
+ { errors: [{ field: "_schema", message: context.schemaError.message }], ...metadata }
856
+ );
827
857
  }
828
858
  if (!context.schemaCache || !context.schemaValidateFn) {
829
859
  return document;
@@ -857,22 +887,31 @@ function validateModelSchemaPayload(context, document, options, metadata = {}) {
857
887
  }
858
888
  return normalized;
859
889
  }
890
+ function buildModelVisibleSoftDeleteCondition(config) {
891
+ return { [config.field]: config.type === "boolean" ? { $ne: true } : null };
892
+ }
860
893
  function applyModelSoftDeleteFilter(query, options, softDeleteConfig) {
861
894
  if (!softDeleteConfig?.enabled) {
862
895
  return query ?? {};
863
896
  }
864
897
  const resolvedOptions = options ?? {};
865
898
  const resolvedQuery = query ?? {};
866
- if (resolvedQuery[softDeleteConfig.field] !== void 0) {
899
+ if (resolvedOptions.withDeleted !== void 0 && typeof resolvedOptions.withDeleted !== "boolean") {
900
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "withDeleted must be a boolean.");
901
+ }
902
+ if (resolvedOptions.onlyDeleted !== void 0 && typeof resolvedOptions.onlyDeleted !== "boolean") {
903
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "onlyDeleted must be a boolean.");
904
+ }
905
+ if (Object.prototype.hasOwnProperty.call(resolvedQuery, softDeleteConfig.field)) {
867
906
  return resolvedQuery;
868
907
  }
869
908
  if (resolvedOptions.withDeleted) {
870
909
  return resolvedQuery;
871
910
  }
872
911
  if (resolvedOptions.onlyDeleted) {
873
- return { ...resolvedQuery, [softDeleteConfig.field]: { $ne: null } };
912
+ return { ...resolvedQuery, [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : { $ne: null } };
874
913
  }
875
- return { ...resolvedQuery, [softDeleteConfig.field]: null };
914
+ return { ...resolvedQuery, ...buildModelVisibleSoftDeleteCondition(softDeleteConfig) };
876
915
  }
877
916
  function applyModelInsertTimestamps(document, timestampsConfig, nowFactory) {
878
917
  if (!timestampsConfig) {
@@ -1167,7 +1206,9 @@ function projectPopulatedDocument(document, select, nestedPaths) {
1167
1206
  return document;
1168
1207
  }
1169
1208
  const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
1170
- return pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
1209
+ const projected = pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
1210
+ if (!select.includes("_id")) delete projected._id;
1211
+ return projected;
1171
1212
  }
1172
1213
  async function populateModelPath(context, docs, path5, traversalState) {
1173
1214
  const config = normalizePopulateConfig(path5);
@@ -1189,7 +1230,13 @@ async function populateModelPath(context, docs, path5, traversalState) {
1189
1230
  }
1190
1231
  const localValues = docs.map((doc) => getByPath(doc, relation.localField));
1191
1232
  const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
1192
- if (keys.length === 0) {
1233
+ if (config.limit !== void 0 && (!Number.isSafeInteger(config.limit) || config.limit < 0)) {
1234
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "populate limit must be a non-negative integer");
1235
+ }
1236
+ if (config.skip !== void 0 && (!Number.isSafeInteger(config.skip) || config.skip < 0)) {
1237
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "populate skip must be a non-negative integer");
1238
+ }
1239
+ if (keys.length === 0 || config.limit === 0) {
1193
1240
  for (const doc of docs) {
1194
1241
  doc[config.path] = relation.single ? null : [];
1195
1242
  }
@@ -1209,36 +1256,63 @@ async function populateModelPath(context, docs, path5, traversalState) {
1209
1256
  ...effectiveSelect,
1210
1257
  relation.foreignField,
1211
1258
  "_id",
1212
- ...collectNestedLocalFields(relatedModel, nestedPaths)
1259
+ ...Object.keys(config.sort ?? {}),
1260
+ ...collectNestedLocalFields(relatedModel, nestedPaths),
1261
+ ...relatedModel?.softDeleteConfig?.enabled ? [relatedModel.softDeleteConfig.field] : []
1213
1262
  ])] : [];
1214
- const relatedDocs = await relatedCollection.find({
1215
- [relation.foreignField]: { $in: keys },
1216
- ...config.match ?? {}
1217
- }, effectiveSelect ? {
1218
- projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
1219
- } : void 0);
1220
- let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
1221
- if (nestedPaths.length > 0 && relatedModel) {
1222
- if (state.depth + 1 > state.maxDepth) {
1223
- throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1263
+ const readOptions = {
1264
+ ...effectiveSelect ? { projection: Object.fromEntries(projectionFields.map((field) => [field, 1])) } : {},
1265
+ ...config.sort ? { sort: config.sort } : {}
1266
+ };
1267
+ const visibilityMatch = applyModelSoftDeleteFilter(config.match ?? {}, {}, relatedModel?.softDeleteConfig ?? null);
1268
+ const readForKeys = (values, options = readOptions) => {
1269
+ const keyFilter = { [relation.foreignField]: { $in: values } };
1270
+ return readPopulateRows(
1271
+ relatedCollection,
1272
+ Object.keys(visibilityMatch).length === 0 ? keyFilter : { $and: [keyFilter, visibilityMatch] },
1273
+ options
1274
+ );
1275
+ };
1276
+ const hydrateRelated = async (rows) => {
1277
+ let hydrated = relatedModel ? relatedModel.hydrateDocuments(rows) : rows.map((item) => ({ ...item }));
1278
+ if (nestedPaths.length > 0 && relatedModel) {
1279
+ if (state.depth + 1 > state.maxDepth) {
1280
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1281
+ }
1282
+ hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1283
+ depth: state.depth + 1,
1284
+ maxDepth: state.maxDepth,
1285
+ activeConfigs: state.activeConfigs
1286
+ });
1224
1287
  }
1225
- hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1226
- depth: state.depth + 1,
1227
- maxDepth: state.maxDepth,
1228
- activeConfigs: state.activeConfigs
1229
- });
1230
- }
1231
- const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1232
- for (const [index, doc] of docs.entries()) {
1233
- let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1234
- if (config.skip) {
1235
- matches = matches.slice(config.skip);
1288
+ return hydrated;
1289
+ };
1290
+ if (config.limit !== void 0 || config.skip !== void 0) {
1291
+ const scopedOptions = {
1292
+ ...readOptions,
1293
+ ...config.skip !== void 0 ? { skip: config.skip } : {},
1294
+ ...config.limit !== void 0 ? { limit: config.limit } : {}
1295
+ };
1296
+ for (const [index, doc] of docs.entries()) {
1297
+ const parentKeys = unique(flattenComparableValues(localValues[index]));
1298
+ const rows = parentKeys.length > 0 ? await readForKeys(parentKeys, scopedOptions) : [];
1299
+ const hydrated = await hydrateRelated(rows);
1300
+ const matches = dedupeDocuments(hydrated);
1301
+ const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1302
+ doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1303
+ }
1304
+ } else {
1305
+ const rows = [];
1306
+ for (let offset = 0; offset < keys.length; offset += 200) {
1307
+ rows.push(...await readForKeys(keys.slice(offset, offset + 200)));
1236
1308
  }
1237
- if (config.limit !== void 0) {
1238
- matches = matches.slice(0, config.limit);
1309
+ const hydrated = await hydrateRelated(rows);
1310
+ const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1311
+ for (const [index, doc] of docs.entries()) {
1312
+ const matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1313
+ const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1314
+ doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1239
1315
  }
1240
- const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1241
- doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1242
1316
  }
1243
1317
  return docs;
1244
1318
  } finally {
@@ -1439,12 +1513,28 @@ async function saveModelDocument(collection, document, options = {}) {
1439
1513
  document._id = result.insertedId;
1440
1514
  return document;
1441
1515
  }
1442
- async function removeModelDocument(collection, document) {
1516
+ async function removeModelDocument(collection, document, deleteOne = (filter) => collection.deleteOne(filter)) {
1443
1517
  if (document._id === void 0) {
1444
1518
  return false;
1445
1519
  }
1446
- const result = await collection.deleteOne({ _id: document._id });
1447
- return Boolean(result.deletedCount ?? result.acknowledged);
1520
+ const result = await deleteOne({ _id: document._id });
1521
+ if (result.matchedCount === void 0 && result.modifiedCount === void 0 && result.deletedCount === void 0) {
1522
+ return result.acknowledged === true;
1523
+ }
1524
+ return Boolean(result.matchedCount || result.modifiedCount || result.deletedCount);
1525
+ }
1526
+ async function readPopulateRows(collection, query, options) {
1527
+ if (typeof collection.stream !== "function") {
1528
+ return await collection.find(query, options);
1529
+ }
1530
+ const stream = collection.stream(query, options);
1531
+ const rows = [];
1532
+ try {
1533
+ for await (const row of stream) rows.push(row);
1534
+ } finally {
1535
+ stream.destroy();
1536
+ }
1537
+ return rows;
1448
1538
  }
1449
1539
 
1450
1540
  // src/capabilities/model/model-soft-delete-helpers.ts
@@ -1452,7 +1542,53 @@ function deletedFilter(filter, softDeleteConfig) {
1452
1542
  if (!softDeleteConfig) {
1453
1543
  return filter ?? {};
1454
1544
  }
1455
- return { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } };
1545
+ return applyModelSoftDeleteFilter(filter, { onlyDeleted: true }, softDeleteConfig);
1546
+ }
1547
+ function filterVisibleBySoftDelete(docs, options, config) {
1548
+ if (!config?.enabled) return docs.filter((doc) => Boolean(doc));
1549
+ const rawOptions = options ?? {};
1550
+ applyModelSoftDeleteFilter({}, rawOptions, config);
1551
+ if (rawOptions.withDeleted) return docs.filter((doc) => Boolean(doc));
1552
+ return docs.filter((doc) => {
1553
+ if (!doc) return false;
1554
+ const value = doc[config.field];
1555
+ const deleted = config.type === "boolean" ? value === true : value !== void 0 && value !== null;
1556
+ return rawOptions.onlyDeleted ? deleted : !deleted;
1557
+ });
1558
+ }
1559
+ function withSoftDeleteProjection(options, config) {
1560
+ if (!config?.enabled) return { options, stripField: false };
1561
+ const rawOptions = options ?? {};
1562
+ const field = config.field;
1563
+ const projection = rawOptions.projection ?? rawOptions.project;
1564
+ if (Array.isArray(projection)) {
1565
+ if (projection.includes(field)) return { options, stripField: false };
1566
+ return { options: { ...rawOptions, projection: [...projection, field] }, stripField: true };
1567
+ }
1568
+ if (!projection || typeof projection !== "object") return { options, stripField: false };
1569
+ const projectionRecord = projection;
1570
+ if (projectionRecord[field] === 1 || projectionRecord[field] === true) return { options, stripField: false };
1571
+ const inclusion = Object.entries(projectionRecord).some(([key, value]) => key !== "_id" && (value === 1 || value === true));
1572
+ const nextProjection = { ...projectionRecord };
1573
+ if (inclusion) nextProjection[field] = 1;
1574
+ else delete nextProjection[field];
1575
+ return { options: { ...rawOptions, projection: nextProjection }, stripField: true };
1576
+ }
1577
+ function applySoftDeleteFindPageOptions(options, config) {
1578
+ const rawOptions = options ?? {};
1579
+ return { ...rawOptions, query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, config) };
1580
+ }
1581
+ function applySoftDeleteAggregatePipeline(pipeline, options, config) {
1582
+ if (!config?.enabled) return pipeline ?? [];
1583
+ const rawOptions = options ?? {};
1584
+ if (rawOptions.withDeleted) return pipeline ?? [];
1585
+ const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, config);
1586
+ const matchStage = { $match: softDeleteMatch };
1587
+ const stages = [...pipeline ?? []];
1588
+ if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
1589
+ return [stages[0], matchStage, ...stages.slice(1)];
1590
+ }
1591
+ return [matchStage, ...stages];
1456
1592
  }
1457
1593
  function findWithDeletedDocuments(context, query, options) {
1458
1594
  return new PopulatePromise(async (paths) => {
@@ -1495,7 +1631,7 @@ function restoreSoftDeletedDocuments(context, filter, options) {
1495
1631
  return Promise.resolve({ modifiedCount: 0 });
1496
1632
  }
1497
1633
  return context.collection.updateOne(
1498
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1634
+ deletedFilter(filter, softDeleteConfig),
1499
1635
  { $unset: { [softDeleteConfig.field]: 1 } },
1500
1636
  options
1501
1637
  );
@@ -1506,7 +1642,7 @@ function restoreManySoftDeletedDocuments(context, filter, options) {
1506
1642
  return Promise.resolve({ modifiedCount: 0 });
1507
1643
  }
1508
1644
  return context.collection.updateMany(
1509
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1645
+ deletedFilter(filter, softDeleteConfig),
1510
1646
  { $unset: { [softDeleteConfig.field]: 1 } },
1511
1647
  options
1512
1648
  );
@@ -3122,6 +3258,9 @@ var modelWriteSourceStorage = new import_node_async_hooks.AsyncLocalStorage();
3122
3258
  function isPlainObject2(value) {
3123
3259
  return value !== null && typeof value === "object" && !Array.isArray(value);
3124
3260
  }
3261
+ function ownValue(record, key) {
3262
+ return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
3263
+ }
3125
3264
  function invalidConfig(path5, reason) {
3126
3265
  throw createError(
3127
3266
  ErrorCodes.INVALID_OPERATION,
@@ -3142,19 +3281,19 @@ function normalizeRule(input, path5) {
3142
3281
  if (!isPlainObject2(input)) {
3143
3282
  invalidConfig(path5, "rule must be a string mode or an object");
3144
3283
  }
3145
- const mode = input.mode ?? DEFAULT_RULE.mode;
3284
+ const mode = ownValue(input, "mode") ?? DEFAULT_RULE.mode;
3146
3285
  if (!ALLOWED_MODES.has(mode)) {
3147
3286
  invalidConfig(`${path5}.mode`, 'must be "allow-both" or "model-only"');
3148
3287
  }
3149
- const raw = input.raw ?? DEFAULT_RULE.raw;
3288
+ const raw = ownValue(input, "raw") ?? DEFAULT_RULE.raw;
3150
3289
  if (!ALLOWED_RAW.has(raw)) {
3151
3290
  invalidConfig(`${path5}.raw`, 'must be "inherit", "allow", or "block"');
3152
3291
  }
3153
- const management = input.management ?? DEFAULT_RULE.management;
3292
+ const management = ownValue(input, "management") ?? DEFAULT_RULE.management;
3154
3293
  if (!ALLOWED_MANAGEMENT.has(management)) {
3155
3294
  invalidConfig(`${path5}.management`, 'must be "inherit", "allow", or "block"');
3156
3295
  }
3157
- const onViolation = input.onViolation ?? DEFAULT_RULE.onViolation;
3296
+ const onViolation = ownValue(input, "onViolation") ?? DEFAULT_RULE.onViolation;
3158
3297
  if (!ALLOWED_VIOLATION_ACTIONS.has(onViolation)) {
3159
3298
  invalidConfig(`${path5}.onViolation`, 'must be "throw" or "warn"');
3160
3299
  }
@@ -3170,15 +3309,15 @@ function validateWritePathPolicyConfig(options) {
3170
3309
  }
3171
3310
  function normalizeWritePathPolicy(options) {
3172
3311
  if (options === void 0) {
3173
- return { default: { ...DEFAULT_RULE }, namespaces: {}, enabled: false };
3312
+ return { default: { ...DEFAULT_RULE }, namespaces: /* @__PURE__ */ Object.create(null), enabled: false };
3174
3313
  }
3175
3314
  if (!isPlainObject2(options)) {
3176
3315
  invalidConfig("writePathPolicy", "must be an object");
3177
3316
  }
3178
3317
  const typedOptions = options;
3179
- const defaultRule = normalizeRule(typedOptions.default, "writePathPolicy.default");
3180
- const namespacesInput = options.namespaces;
3181
- const namespaces = {};
3318
+ const defaultRule = normalizeRule(ownValue(typedOptions, "default"), "writePathPolicy.default");
3319
+ const namespacesInput = ownValue(options, "namespaces");
3320
+ const namespaces = /* @__PURE__ */ Object.create(null);
3182
3321
  if (namespacesInput !== void 0) {
3183
3322
  if (!isPlainObject2(namespacesInput)) {
3184
3323
  invalidConfig("writePathPolicy.namespaces", "must be an object");
@@ -3226,8 +3365,8 @@ function resolveWritePathRule(policy, namespace) {
3226
3365
  if (candidate === "default") {
3227
3366
  return { key: "default", rule: effectivePolicy.default };
3228
3367
  }
3229
- const rule = effectivePolicy.namespaces[candidate];
3230
- if (rule) {
3368
+ if (Object.prototype.hasOwnProperty.call(effectivePolicy.namespaces, candidate)) {
3369
+ const rule = effectivePolicy.namespaces[candidate];
3231
3370
  return { key: candidate, rule };
3232
3371
  }
3233
3372
  }
@@ -3370,16 +3509,17 @@ var fallbackModelIndexTasks = /* @__PURE__ */ new Map();
3370
3509
  function toCompatDefinition(definition) {
3371
3510
  return definition;
3372
3511
  }
3373
- function stableIndexStringify(value) {
3512
+ function stableIndexStringify(value, mode = "options") {
3374
3513
  if (value instanceof Date) {
3375
3514
  return JSON.stringify(value.toISOString());
3376
3515
  }
3377
3516
  if (Array.isArray(value)) {
3378
- return `[${value.map((item) => stableIndexStringify(item)).join(",")}]`;
3517
+ return `[${value.map((item) => stableIndexStringify(item, mode)).join(",")}]`;
3379
3518
  }
3380
3519
  if (value && typeof value === "object") {
3381
- const entries = Object.entries(value).filter(([, current]) => current !== void 0).sort(([left], [right]) => left.localeCompare(right));
3382
- return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableIndexStringify(current)}`).join(",")}}`;
3520
+ const entries = Object.entries(value).filter(([, current]) => current !== void 0);
3521
+ if (mode === "options") entries.sort(([left], [right]) => left.localeCompare(right));
3522
+ return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableIndexStringify(current, key === "key" ? "key" : mode)}`).join(",")}}`;
3383
3523
  }
3384
3524
  return JSON.stringify(value) ?? "undefined";
3385
3525
  }
@@ -3413,7 +3553,7 @@ function declaredOptionEntries(options) {
3413
3553
  });
3414
3554
  }
3415
3555
  function indexOptionsMatch(existing, declared) {
3416
- if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
3556
+ if (stableIndexStringify(getExistingIndexKey(existing), "key") !== stableIndexStringify(declared.key, "key")) {
3417
3557
  return false;
3418
3558
  }
3419
3559
  for (const [name, value] of declaredOptionEntries(declared.options)) {
@@ -3432,8 +3572,8 @@ function findExistingIndexByName(existingIndexes, name) {
3432
3572
  return existingIndexes.find((index) => index.name === name);
3433
3573
  }
3434
3574
  function findExistingIndexByKey(existingIndexes, key) {
3435
- const fingerprint = stableIndexStringify(key);
3436
- return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
3575
+ const fingerprint = stableIndexStringify(key, "key");
3576
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index), "key") === fingerprint);
3437
3577
  }
3438
3578
  function createIndexEnsureError(message, result, cause) {
3439
3579
  return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
@@ -4271,12 +4411,17 @@ function shouldConvertQueryField(autoConvert, path5, depth) {
4271
4411
  if (!config.enabled || depth > config.maxDepth) return false;
4272
4412
  return shouldConvertObjectIdPath(path5, config);
4273
4413
  }
4414
+ function isPlainQueryRecord(value) {
4415
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4416
+ const prototype = Object.getPrototypeOf(value);
4417
+ return prototype === Object.prototype || prototype === null;
4418
+ }
4274
4419
  function normalizeQueryArray(value, autoConvert, path5, depth) {
4275
4420
  return value.map((item, index) => {
4276
4421
  if (typeof item === "string" && item.length === 24 && import_mongodb2.ObjectId.isValid(item) && shouldConvertQueryField(autoConvert, path5, depth)) {
4277
4422
  return new import_mongodb2.ObjectId(item);
4278
4423
  }
4279
- if (item && typeof item === "object" && !Array.isArray(item) && !(item instanceof import_mongodb2.ObjectId)) {
4424
+ if (isPlainQueryRecord(item)) {
4280
4425
  return normalizeQueryFilter(item, autoConvert, `${path5}[${index}]`, depth + 1);
4281
4426
  }
4282
4427
  return item;
@@ -4292,7 +4437,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4292
4437
  }
4293
4438
  for (const [key, value] of Object.entries(filter)) {
4294
4439
  if (key === "$and" || key === "$or" || key === "$nor") {
4295
- result[key] = Array.isArray(value) ? value.map((item) => item && typeof item === "object" ? normalizeQueryFilter(item, autoConvert, fieldPath, depth + 1) : item) : value;
4440
+ result[key] = Array.isArray(value) ? value.map((item) => isPlainQueryRecord(item) ? normalizeQueryFilter(item, autoConvert, fieldPath, depth + 1) : item) : value;
4296
4441
  continue;
4297
4442
  }
4298
4443
  const currentPath = fieldPath ? `${fieldPath}.${key}` : key;
@@ -4301,7 +4446,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4301
4446
  result[key] = shouldConvert ? new import_mongodb2.ObjectId(value) : value;
4302
4447
  } else if (Array.isArray(value)) {
4303
4448
  result[key] = shouldConvert ? normalizeQueryArray(value, autoConvert, currentPath, depth + 1) : value;
4304
- } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof import_mongodb2.ObjectId)) {
4449
+ } else if (isPlainQueryRecord(value)) {
4305
4450
  const nested = value;
4306
4451
  const hasOperators = Object.keys(nested).some((k) => k.startsWith("$"));
4307
4452
  if (hasOperators) {
@@ -4313,7 +4458,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4313
4458
  );
4314
4459
  } else if (shouldConvert && QUERY_OBJECTID_SCALAR_OPERATORS.has(op) && typeof opVal === "string" && opVal.length === 24 && import_mongodb2.ObjectId.isValid(opVal)) {
4315
4460
  nestedResult[op] = new import_mongodb2.ObjectId(opVal);
4316
- } else if (op === "$elemMatch" && opVal && typeof opVal === "object" && !Array.isArray(opVal)) {
4461
+ } else if (op === "$elemMatch" && isPlainQueryRecord(opVal)) {
4317
4462
  nestedResult[op] = normalizeQueryFilter(opVal, autoConvert, currentPath, depth + 1);
4318
4463
  } else {
4319
4464
  nestedResult[op] = opVal;
@@ -4487,36 +4632,29 @@ async function isCollectionCacheBarrierActive(queryCache, collection, defaults =
4487
4632
  buildCollectionCacheBarrierNamespaces(collection, defaults)
4488
4633
  );
4489
4634
  }
4490
- function normalizeCacheKeyValue(value) {
4491
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
4492
- return void 0;
4493
- }
4494
- if (value instanceof Date) {
4495
- return { $date: value.toISOString() };
4496
- }
4497
- if (value instanceof import_mongodb2.ObjectId) {
4498
- return { $oid: value.toHexString() };
4499
- }
4500
- if (value instanceof RegExp) {
4501
- return { $regex: value.source, $flags: value.flags };
4502
- }
4503
- if (Array.isArray(value)) {
4504
- return value.map((item) => normalizeCacheKeyValue(item));
4505
- }
4506
- if (value && typeof value === "object") {
4507
- const sorted = {};
4508
- for (const key of Object.keys(value).sort()) {
4509
- const normalized = normalizeCacheKeyValue(value[key]);
4510
- if (normalized !== void 0) {
4511
- sorted[key] = normalized;
4512
- }
4513
- }
4514
- return sorted;
4515
- }
4516
- return value;
4517
- }
4518
- function stableCacheKeyString(value) {
4519
- return JSON.stringify(normalizeCacheKeyValue(value));
4635
+ var ORDERED_OPTION_FIELDS = /* @__PURE__ */ new Set(["query", "filter", "pipeline", "sort", "hint", "projection", "min", "max"]);
4636
+ function normalizeCacheKeyValue(value, mode) {
4637
+ if (value === null) return ["null"];
4638
+ if (value === void 0) return ["undefined"];
4639
+ if (typeof value === "number") return ["number", String(value)];
4640
+ if (typeof value === "bigint") return ["bigint", value.toString()];
4641
+ if (typeof value !== "object") return [typeof value, value];
4642
+ if (value instanceof Date) return ["date", String(value.getTime())];
4643
+ if (value instanceof RegExp) return ["regexp", value.source, value.flags];
4644
+ if (Array.isArray(value)) return ["array", value.map((item) => normalizeCacheKeyValue(item, mode))];
4645
+ const bsonType = value._bsontype;
4646
+ if (typeof bsonType === "string") {
4647
+ return ["bson", bsonType, import_mongodb2.BSON.EJSON.serialize(value, { relaxed: false })];
4648
+ }
4649
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0 && typeof item !== "function" && typeof item !== "symbol");
4650
+ if (mode === "options") entries.sort(([left], [right]) => left.localeCompare(right));
4651
+ return ["object", entries.map(([key, item]) => [
4652
+ key,
4653
+ normalizeCacheKeyValue(item, mode === "options" && ORDERED_OPTION_FIELDS.has(key) ? "document" : mode)
4654
+ ])];
4655
+ }
4656
+ function stableCacheKeyString(value, mode = "options") {
4657
+ return `v2:${JSON.stringify(normalizeCacheKeyValue(value, mode))}`;
4520
4658
  }
4521
4659
  function buildResultCacheKeyOptions(options = {}) {
4522
4660
  const keyOptions = {};
@@ -4637,7 +4775,7 @@ function buildFindCacheKey(collection, defaults = {}, query, options) {
4637
4775
  ...options ?? {}
4638
4776
  });
4639
4777
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4640
- return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4778
+ return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4641
4779
  }
4642
4780
  function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4643
4781
  const executeOptions = normalizeFindProjectionOptions({
@@ -4645,7 +4783,7 @@ function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4645
4783
  ...options ?? {}
4646
4784
  });
4647
4785
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4648
- return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4786
+ return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4649
4787
  }
4650
4788
  function buildCountCacheKey(collection, defaults = {}, query, options) {
4651
4789
  const merged = {
@@ -4655,7 +4793,7 @@ function buildCountCacheKey(collection, defaults = {}, query, options) {
4655
4793
  const { cache: _cache, ...keyOptions } = merged;
4656
4794
  void _cache;
4657
4795
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4658
- return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4796
+ return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4659
4797
  }
4660
4798
  function buildDistinctCacheKey(collection, defaults = {}, field, query, options) {
4661
4799
  const rawOptions = options ?? {};
@@ -4685,7 +4823,7 @@ function buildAggregateCacheKey(collection, defaults = {}, pipeline, options) {
4685
4823
  ...options ?? {}
4686
4824
  };
4687
4825
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4688
- return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4826
+ return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4689
4827
  }
4690
4828
  function buildFindOneByIdCacheKey(collection, defaults = {}, id, options) {
4691
4829
  const objectId = parseRequiredObjectId(id);
@@ -4700,6 +4838,8 @@ function buildFindByIdsCacheKey(collection, defaults = {}, ids, options) {
4700
4838
  function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4701
4839
  const ext = options;
4702
4840
  const cursorTypes = ext.cursorTypes && typeof ext.cursorTypes === "object" && !Array.isArray(ext.cursorTypes) ? ext.cursorTypes : void 0;
4841
+ const { let: nestedLet, ...nestedOptions } = options.options ?? {};
4842
+ const effectiveLet = ext.let !== void 0 ? ext.let : nestedLet;
4703
4843
  const payload = {
4704
4844
  query: normalizeQueryForCache(options.query, defaults),
4705
4845
  sort: normalizeSortShape(options.sort),
@@ -4721,9 +4861,10 @@ function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4721
4861
  hint: ext.hint,
4722
4862
  collation: ext.collation,
4723
4863
  batchSize: ext.batchSize,
4724
- options: options.options
4864
+ let: effectiveLet,
4865
+ options: nestedOptions
4725
4866
  };
4726
- const keyHash = hashPayload(payload);
4867
+ const keyHash = hashPayload(stableCacheKeyString(payload));
4727
4868
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4728
4869
  return { key: `findPage:${namespace}:${keyHash}`, keyHash };
4729
4870
  }
@@ -5070,29 +5211,30 @@ async function runStrictUpdateMany(context, filter, update, options) {
5070
5211
  if (rawOptions.upsert === true) {
5071
5212
  throw createError(ErrorCodes.INVALID_ARGUMENT, 'versionMode "strict" does not support upsert.');
5072
5213
  }
5073
- const docs = await context.collection.find(filter, {
5074
- ...buildModelVersionLookupOptions(rawOptions, {
5075
- _id: 1,
5076
- [context.versionConfig.field]: 1
5077
- })
5078
- });
5079
5214
  let matchedCount = 0;
5080
5215
  let modifiedCount = 0;
5081
5216
  const conflictedIds = [];
5082
- for (const doc of docs) {
5083
- const id = doc._id;
5084
- const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
5085
- const result = await context.collection.updateOne(
5086
- { _id: id, [context.versionConfig.field]: expectedVersion },
5087
- update,
5088
- options
5089
- );
5090
- if ((result?.matchedCount ?? 0) === 0) {
5091
- conflictedIds.push(id);
5092
- continue;
5217
+ const skippedIds = [];
5218
+ for await (const batch of iterateModelCandidateBatches(context, filter, rawOptions, 200)) {
5219
+ for (const doc of batch) {
5220
+ const id = doc._id;
5221
+ const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
5222
+ const result = await context.collection.updateOne(
5223
+ buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
5224
+ update,
5225
+ options
5226
+ );
5227
+ if ((result?.matchedCount ?? 0) === 0) {
5228
+ if (await isModelVersionConflict(context, filter, id, context.versionConfig.field, expectedVersion, options)) {
5229
+ conflictedIds.push(id);
5230
+ } else {
5231
+ skippedIds.push(id);
5232
+ }
5233
+ continue;
5234
+ }
5235
+ matchedCount += result.matchedCount ?? 0;
5236
+ modifiedCount += result.modifiedCount ?? 0;
5093
5237
  }
5094
- matchedCount += result.matchedCount ?? 0;
5095
- modifiedCount += result.modifiedCount ?? 0;
5096
5238
  }
5097
5239
  return {
5098
5240
  acknowledged: true,
@@ -5101,7 +5243,9 @@ async function runStrictUpdateMany(context, filter, update, options) {
5101
5243
  upsertedCount: 0,
5102
5244
  upsertedId: null,
5103
5245
  conflictCount: conflictedIds.length,
5104
- conflictedIds
5246
+ conflictedIds,
5247
+ skippedCount: skippedIds.length,
5248
+ skippedIds
5105
5249
  };
5106
5250
  }
5107
5251
  async function runStrictUpdateBatch(context, filter, update, options) {
@@ -5124,7 +5268,7 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5124
5268
  retryAttempts: _retryAttempts,
5125
5269
  retryDelay: _retryDelay,
5126
5270
  onRetry: _onRetry,
5127
- sort = { _id: 1 },
5271
+ sort: _sort,
5128
5272
  ...driverOptions
5129
5273
  } = rawOptions;
5130
5274
  void _batchSize;
@@ -5133,38 +5277,42 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5133
5277
  void _retryAttempts;
5134
5278
  void _retryDelay;
5135
5279
  void _onRetry;
5136
- const docs = await context.collection.find(filter, {
5137
- ...buildModelVersionLookupOptions(driverOptions, {
5138
- _id: 1,
5139
- [context.versionConfig.field]: 1
5140
- }),
5141
- sort
5142
- });
5280
+ void _sort;
5281
+ const countOptions = buildModelVersionLookupOptions(driverOptions, {});
5282
+ delete countOptions.projection;
5283
+ const totalCount = await context.collection.count(filter, countOptions);
5143
5284
  const result = {
5144
5285
  acknowledged: true,
5145
- totalCount: docs.length,
5286
+ totalCount,
5146
5287
  matchedCount: 0,
5147
5288
  modifiedCount: 0,
5148
5289
  upsertedCount: 0,
5149
- batchCount: Math.ceil(docs.length / batchSize),
5290
+ batchCount: 0,
5150
5291
  errors: [],
5151
5292
  retries: [],
5152
5293
  conflictCount: 0,
5153
- conflictedIds: []
5294
+ conflictedIds: [],
5295
+ skippedCount: 0,
5296
+ skippedIds: []
5154
5297
  };
5155
- for (let offset = 0; offset < docs.length; offset += batchSize) {
5156
- const batch = docs.slice(offset, offset + batchSize);
5298
+ for await (const batch of iterateModelCandidateBatches(context, filter, driverOptions, batchSize)) {
5299
+ result.batchCount++;
5157
5300
  for (const doc of batch) {
5158
5301
  const id = doc._id;
5159
5302
  const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateBatch");
5160
5303
  const updateResult = await context.collection.updateOne(
5161
- { _id: id, [context.versionConfig.field]: expectedVersion },
5304
+ buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
5162
5305
  update,
5163
5306
  driverOptions
5164
5307
  );
5165
5308
  if ((updateResult?.matchedCount ?? 0) === 0) {
5166
- result.conflictCount = (result.conflictCount ?? 0) + 1;
5167
- result.conflictedIds?.push(id);
5309
+ if (await isModelVersionConflict(context, filter, id, context.versionConfig.field, expectedVersion, driverOptions)) {
5310
+ result.conflictCount = (result.conflictCount ?? 0) + 1;
5311
+ result.conflictedIds?.push(id);
5312
+ } else {
5313
+ result.skippedCount = (result.skippedCount ?? 0) + 1;
5314
+ result.skippedIds?.push(id);
5315
+ }
5168
5316
  continue;
5169
5317
  }
5170
5318
  result.matchedCount += updateResult.matchedCount ?? 0;
@@ -5172,15 +5320,16 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5172
5320
  }
5173
5321
  if (typeof onProgress === "function") {
5174
5322
  onProgress({
5175
- currentBatch: Math.floor(offset / batchSize) + 1,
5176
- totalBatches: result.batchCount,
5323
+ currentBatch: result.batchCount,
5324
+ totalBatches: Math.ceil(totalCount / batchSize),
5177
5325
  modified: result.modifiedCount,
5178
5326
  matched: result.matchedCount,
5179
- total: docs.length,
5180
- percentage: docs.length > 0 ? Math.round(result.modifiedCount / docs.length * 100) : null,
5327
+ total: totalCount,
5328
+ percentage: totalCount > 0 ? Math.round(result.modifiedCount / totalCount * 100) : null,
5181
5329
  errors: result.errors.length,
5182
5330
  retries: result.retries.length,
5183
- conflicts: result.conflictCount ?? 0
5331
+ conflicts: result.conflictCount ?? 0,
5332
+ skipped: result.skippedCount ?? 0
5184
5333
  });
5185
5334
  }
5186
5335
  }
@@ -5193,10 +5342,7 @@ function buildSoftDeletePatch(config, now) {
5193
5342
  return { [config.field]: config.type === "boolean" ? true : now };
5194
5343
  }
5195
5344
  function buildSoftDeleteFilter(filter, config) {
5196
- return {
5197
- ...filter ?? {},
5198
- ...config?.enabled ? { [config.field]: null } : {}
5199
- };
5345
+ return applyModelSoftDeleteFilter(filter, {}, config);
5200
5346
  }
5201
5347
  function applyModelIncrementVersion(field, increment, versionConfig) {
5202
5348
  if (!versionConfig?.enabled || field === void 0) {
@@ -5236,7 +5382,8 @@ async function orchestrateModelInsertOne(context, document, options) {
5236
5382
  payload = validateModelSchemaPayload({
5237
5383
  validateEnabled: context.validateEnabled,
5238
5384
  schemaCache: context.schemaCache,
5239
- schemaValidateFn: context.schemaValidateFn
5385
+ schemaValidateFn: context.schemaValidateFn,
5386
+ schemaError: context.schemaError
5240
5387
  }, payload, options);
5241
5388
  payload = applyModelInsertTimestamps(payload, context.timestampsConfig, () => context.nowDate());
5242
5389
  payload = applyModelInsertVersion(payload, context.versionConfig);
@@ -5270,7 +5417,8 @@ async function orchestrateModelInsertMany(context, documents, options) {
5270
5417
  doc = validateModelSchemaPayload({
5271
5418
  validateEnabled: context.validateEnabled,
5272
5419
  schemaCache: context.schemaCache,
5273
- schemaValidateFn: context.schemaValidateFn
5420
+ schemaValidateFn: context.schemaValidateFn,
5421
+ schemaError: context.schemaError
5274
5422
  }, doc, resolvedOptions, { index });
5275
5423
  doc = applyModelInsertTimestamps(doc, context.timestampsConfig, () => context.nowDate());
5276
5424
  doc = applyModelInsertVersion(doc, context.versionConfig);
@@ -5355,7 +5503,8 @@ async function orchestrateModelReplaceOne(context, filter, replacement, options)
5355
5503
  let nextReplacement = validateModelSchemaPayload({
5356
5504
  validateEnabled: context.validateEnabled,
5357
5505
  schemaCache: context.schemaCache,
5358
- schemaValidateFn: context.schemaValidateFn
5506
+ schemaValidateFn: context.schemaValidateFn,
5507
+ schemaError: context.schemaError
5359
5508
  }, replacement, options);
5360
5509
  nextReplacement = preserveModelReplaceCreatedAt(
5361
5510
  replacement,
@@ -5414,7 +5563,8 @@ async function orchestrateModelFindOneAndReplace(context, filter, replacement, o
5414
5563
  let nextReplacement = validateModelSchemaPayload({
5415
5564
  validateEnabled: context.validateEnabled,
5416
5565
  schemaCache: context.schemaCache,
5417
- schemaValidateFn: context.schemaValidateFn
5566
+ schemaValidateFn: context.schemaValidateFn,
5567
+ schemaError: context.schemaError
5418
5568
  }, replacement, options);
5419
5569
  nextReplacement = preserveModelReplaceCreatedAt(
5420
5570
  replacement,
@@ -5526,7 +5676,8 @@ async function orchestrateModelInsertBatch(context, docs, options) {
5526
5676
  record = validateModelSchemaPayload({
5527
5677
  validateEnabled: context.validateEnabled,
5528
5678
  schemaCache: context.schemaCache,
5529
- schemaValidateFn: context.schemaValidateFn
5679
+ schemaValidateFn: context.schemaValidateFn,
5680
+ schemaError: context.schemaError
5530
5681
  }, record, resolvedOptions, { index });
5531
5682
  record = applyModelInsertTimestamps(record, context.timestampsConfig, () => context.nowDate());
5532
5683
  record = applyModelInsertVersion(record, context.versionConfig);
@@ -5604,7 +5755,7 @@ async function orchestrateModelDeleteOne(context, filter, options) {
5604
5755
  let result;
5605
5756
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5606
5757
  result = await context.collection.updateOne(
5607
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5758
+ buildSoftDeleteFilter(filter, softDeleteConfig),
5608
5759
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5609
5760
  options
5610
5761
  );
@@ -5638,7 +5789,7 @@ async function orchestrateModelDeleteMany(context, filter, options) {
5638
5789
  let result;
5639
5790
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5640
5791
  result = await context.collection.updateMany(
5641
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5792
+ { $and: [filter ?? {}, buildModelVisibleSoftDeleteCondition(softDeleteConfig)] },
5642
5793
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5643
5794
  options
5644
5795
  );
@@ -5802,7 +5953,7 @@ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targ
5802
5953
  [inbound.localField]: { $in: targetValues }
5803
5954
  };
5804
5955
  const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
5805
- const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
5956
+ const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, buildModelVisibleSoftDeleteCondition(sourceSoftDelete)] } : baseQuery;
5806
5957
  try {
5807
5958
  const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
5808
5959
  if (!collection) {
@@ -5984,7 +6135,7 @@ async function executeProtectedRelationDelete(host, filter, options, force) {
5984
6135
  const deleteOptions = stripRelationUsageOptions(options);
5985
6136
  let target;
5986
6137
  try {
5987
- const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : { ...filter, [host.softDeleteConfig.field]: null };
6138
+ const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig);
5988
6139
  const rawTarget = await host.collection.findOne(
5989
6140
  targetFilter,
5990
6141
  relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
@@ -6068,7 +6219,7 @@ var ModelInstance = class {
6068
6219
  this.softDeleteConfig = this._softDeleteConfig;
6069
6220
  this._versionConfig = resolveModelVersionConfig(options.definition);
6070
6221
  this._v1HooksFactory = resolveModelHooksFactory(options.definition);
6071
- scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6222
+ if (!this._schemaError) scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6072
6223
  runtime: this.runtime,
6073
6224
  dbName: options.dbName,
6074
6225
  poolName: options.poolName,
@@ -6128,46 +6279,6 @@ var ModelInstance = class {
6128
6279
  }
6129
6280
  return result;
6130
6281
  }
6131
- isVisibleBySoftDelete(doc, options) {
6132
- if (!doc || !this._softDeleteConfig?.enabled) {
6133
- return Boolean(doc);
6134
- }
6135
- const rawOptions = options ?? {};
6136
- if (rawOptions.withDeleted) {
6137
- return true;
6138
- }
6139
- const value = doc[this._softDeleteConfig.field];
6140
- if (rawOptions.onlyDeleted) {
6141
- return value !== void 0 && value !== null;
6142
- }
6143
- return value === void 0 || value === null;
6144
- }
6145
- filterVisibleBySoftDelete(docs, options) {
6146
- return docs.filter((doc) => this.isVisibleBySoftDelete(doc, options));
6147
- }
6148
- applySoftDeleteFindPageOptions(options) {
6149
- const rawOptions = options ?? {};
6150
- return {
6151
- ...rawOptions,
6152
- query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, this._softDeleteConfig)
6153
- };
6154
- }
6155
- applySoftDeleteAggregatePipeline(pipeline, options) {
6156
- if (!this._softDeleteConfig?.enabled) {
6157
- return pipeline ?? [];
6158
- }
6159
- const rawOptions = options ?? {};
6160
- if (rawOptions.withDeleted) {
6161
- return pipeline ?? [];
6162
- }
6163
- const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, this._softDeleteConfig);
6164
- const matchStage = { $match: softDeleteMatch };
6165
- const stages = [...pipeline ?? []];
6166
- if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
6167
- return [stages[0], matchStage, ...stages.slice(1)];
6168
- }
6169
- return [matchStage, ...stages];
6170
- }
6171
6282
  // ── public API ────────────────────────────────────────────────────────────────
6172
6283
  find(query, options) {
6173
6284
  return new PopulatePromise(async (paths) => {
@@ -6203,8 +6314,12 @@ var ModelInstance = class {
6203
6314
  }
6204
6315
  findOneById(id, options) {
6205
6316
  return new PopulatePromise(async (paths) => {
6206
- const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], (nextId, nextOptions) => this.collection.findOneById(nextId, nextOptions)) : await this.collection.findOneById(id, options);
6207
- return this.populateSingle(this.hydrateDocument(this.isVisibleBySoftDelete(doc, options) ? doc : null), paths);
6317
+ const lookupById = (nextId, nextOptions) => this._softDeleteConfig?.enabled ? this.collection.findOne(
6318
+ applyModelSoftDeleteFilter({ _id: parseRequiredObjectId(nextId) }, nextOptions, this._softDeleteConfig),
6319
+ nextOptions
6320
+ ) : this.collection.findOneById(nextId, nextOptions);
6321
+ const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], lookupById) : await lookupById(id, options);
6322
+ return this.populateSingle(this.hydrateDocument(doc), paths);
6208
6323
  });
6209
6324
  }
6210
6325
  findById(id, options) {
@@ -6212,13 +6327,25 @@ var ModelInstance = class {
6212
6327
  }
6213
6328
  findByIds(ids, options) {
6214
6329
  return new PopulatePromise(async (paths) => {
6215
- const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], (nextIds, nextOptions) => this.collection.findByIds(nextIds, nextOptions)) : await this.collection.findByIds(ids, options);
6216
- return this.populateDocuments(this.hydrateDocuments(this.filterVisibleBySoftDelete(docs, options)), paths);
6330
+ let stripField = false;
6331
+ const lookupByIds = (nextIds, nextOptions) => {
6332
+ const projection = withSoftDeleteProjection(nextOptions, this._softDeleteConfig);
6333
+ stripField = projection.stripField;
6334
+ return this.collection.findByIds(nextIds, projection.options);
6335
+ };
6336
+ const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], lookupByIds) : await lookupByIds(ids, options);
6337
+ const visible = filterVisibleBySoftDelete(docs, options, this._softDeleteConfig);
6338
+ const projected = stripField && this._softDeleteConfig ? visible.map((doc) => {
6339
+ const copy = { ...doc };
6340
+ delete copy[this._softDeleteConfig.field];
6341
+ return copy;
6342
+ }) : visible;
6343
+ return this.populateDocuments(this.hydrateDocuments(projected), paths);
6217
6344
  });
6218
6345
  }
6219
6346
  findPage(options) {
6220
6347
  return new PopulatePromise(async (paths) => {
6221
- const filteredOptions = this.applySoftDeleteFindPageOptions(options);
6348
+ const filteredOptions = applySoftDeleteFindPageOptions(options, this._softDeleteConfig);
6222
6349
  const result = this._v1HooksFactory ? await this.runV1HookedOperation("find", [filteredOptions], (nextOptions) => this.collection.findPage(nextOptions)) : await this.collection.findPage(filteredOptions);
6223
6350
  return {
6224
6351
  ...result,
@@ -6370,7 +6497,7 @@ var ModelInstance = class {
6370
6497
  return this.runV1HookedOperation("find", [key, filteredQuery, options], (nextKey, nextQuery, nextOptions) => this.collection.distinct(nextKey, nextQuery, nextOptions));
6371
6498
  }
6372
6499
  aggregate(pipeline, options) {
6373
- const filteredPipeline = this.applySoftDeleteAggregatePipeline(pipeline, options);
6500
+ const filteredPipeline = applySoftDeleteAggregatePipeline(pipeline, options, this._softDeleteConfig);
6374
6501
  return this.runV1HookedOperation("find", [filteredPipeline, options], (nextPipeline, nextOptions) => {
6375
6502
  const execute = () => this.collection.aggregate(nextPipeline, nextOptions);
6376
6503
  if (resolveAggregateWriteTarget(Array.isArray(nextPipeline) ? nextPipeline : [])) {
@@ -6499,13 +6626,14 @@ var ModelInstance = class {
6499
6626
  schemaValidationContext: {
6500
6627
  validateEnabled: this._validateEnabled,
6501
6628
  schemaCache: this._schemaCache,
6502
- schemaValidateFn: this._schemaValidateFn
6629
+ schemaValidateFn: this._schemaValidateFn,
6630
+ schemaError: this._schemaError
6503
6631
  },
6504
6632
  nowFactory: () => this.nowDate()
6505
6633
  }));
6506
6634
  }
6507
6635
  async removeDocument(document) {
6508
- return this.runModelWrite(() => removeModelDocument(this.collection, document));
6636
+ return this.runModelWrite(() => removeModelDocument(this.collection, document, (filter) => this.deleteOne(filter)));
6509
6637
  }
6510
6638
  applyDefaults(document) {
6511
6639
  return applyModelDefaults(this.definition, document);
@@ -6523,6 +6651,7 @@ var ModelInstance = class {
6523
6651
  validateEnabled: this._validateEnabled,
6524
6652
  schemaCache: this._schemaCache,
6525
6653
  schemaValidateFn: this._schemaValidateFn,
6654
+ schemaError: this._schemaError,
6526
6655
  hooksFactory: this._v1HooksFactory,
6527
6656
  runHook: (hookName, context) => this.runHook(hookName, context)
6528
6657
  };
@@ -6538,6 +6667,11 @@ var ModelInstance = class {
6538
6667
 
6539
6668
  // src/capabilities/transaction/index.ts
6540
6669
  var import_node_crypto3 = require("node:crypto");
6670
+ function assertManagedTransactionWriteAllowed(options) {
6671
+ const session = options?.session;
6672
+ const managed = session?.__monSQLizeTransaction;
6673
+ if (typeof managed?.assertAcceptingWrites === "function") managed.assertAcceptingWrites();
6674
+ }
6541
6675
  function toPublicTransactionStatus(state) {
6542
6676
  return state === "active" ? "started" : state;
6543
6677
  }
@@ -6638,6 +6772,7 @@ var Transaction = class {
6638
6772
  this.options = options;
6639
6773
  this.id = `tx_${(0, import_node_crypto3.randomBytes)(8).toString("hex")}`;
6640
6774
  this.state = "pending";
6775
+ this.acceptingWrites = false;
6641
6776
  this.startedAt = null;
6642
6777
  this.timeoutTimer = null;
6643
6778
  this.pendingInvalidations = /* @__PURE__ */ new Set();
@@ -6656,6 +6791,7 @@ var Transaction = class {
6656
6791
  }
6657
6792
  this.session.startTransaction(this.options.transactionOptions);
6658
6793
  this.state = "active";
6794
+ this.acceptingWrites = true;
6659
6795
  this.startedAt = Date.now();
6660
6796
  const timeout = this.options.timeout ?? 3e4;
6661
6797
  if (timeout > 0) {
@@ -6678,6 +6814,7 @@ var Transaction = class {
6678
6814
  if (this.state !== "active") {
6679
6815
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
6680
6816
  }
6817
+ this.acceptingWrites = false;
6681
6818
  if (typeof this.session.commitTransaction === "function") {
6682
6819
  await commitTransactionWithRetry(this.session, this.options.logger);
6683
6820
  }
@@ -6703,6 +6840,7 @@ var Transaction = class {
6703
6840
  * @since v1.4.0
6704
6841
  */
6705
6842
  async abort() {
6843
+ this.acceptingWrites = false;
6706
6844
  if (this.state !== "pending" && this.state !== "active") {
6707
6845
  return;
6708
6846
  }
@@ -6727,10 +6865,16 @@ var Transaction = class {
6727
6865
  * @since v1.4.0
6728
6866
  */
6729
6867
  async end() {
6868
+ this.acceptingWrites = false;
6730
6869
  this.clearTimeout();
6731
6870
  this.options.lockManager?.releaseLocks(this.id);
6732
6871
  await this.session.endSession();
6733
6872
  }
6873
+ assertAcceptingWrites() {
6874
+ if (this.state !== "active" || !this.acceptingWrites) {
6875
+ throw createError(ErrorCodes.INVALID_OPERATION, `Managed transaction ${this.id} is no longer accepting writes.`);
6876
+ }
6877
+ }
6734
6878
  /**
6735
6879
  * Record a cache invalidation intent.
6736
6880
  * @since v1.4.0
@@ -6904,12 +7048,12 @@ var TransactionManager = class {
6904
7048
  return result;
6905
7049
  } catch (error) {
6906
7050
  lastError = error;
6907
- await transaction.abort();
6908
- this.recordStats(transaction, Date.now() - startedAt, false);
6909
7051
  if (transaction.state === "committed") {
6910
7052
  throw error;
6911
7053
  }
6912
- if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
7054
+ await transaction.abort();
7055
+ this.recordStats(transaction, Date.now() - startedAt, false);
7056
+ if (!enableRetry || attempt === maxRetries || isUnknownTransactionCommitResult(error) || !isTransientTransactionError(error)) {
6913
7057
  throw error;
6914
7058
  }
6915
7059
  await sleep(retryDelay * Math.pow(retryBackoff, attempt));
@@ -7010,22 +7154,34 @@ function stringifySessionId(id) {
7010
7154
  }
7011
7155
  return String(id);
7012
7156
  }
7013
- function isTransientTransactionError(error) {
7014
- if (!error || typeof error !== "object") {
7015
- return false;
7016
- }
7017
- const candidate = error;
7018
- if (typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("TransientTransactionError")) {
7019
- return true;
7157
+ function transactionErrorChain(error) {
7158
+ const chain = [];
7159
+ const seen = /* @__PURE__ */ new Set();
7160
+ let current = error;
7161
+ while (current && typeof current === "object" && chain.length < 8 && !seen.has(current)) {
7162
+ seen.add(current);
7163
+ const candidate = current;
7164
+ chain.push(candidate);
7165
+ current = candidate.cause;
7166
+ }
7167
+ return chain;
7168
+ }
7169
+ function hasTransactionErrorLabel(candidate, label) {
7170
+ if (Array.isArray(candidate.errorLabels) && candidate.errorLabels.includes(label)) return true;
7171
+ if (typeof candidate.hasErrorLabel === "function") {
7172
+ try {
7173
+ return Boolean(candidate.hasErrorLabel.call(candidate, label));
7174
+ } catch {
7175
+ }
7020
7176
  }
7021
- return candidate.code === 112 || candidate.code === 117;
7177
+ return false;
7022
7178
  }
7023
7179
  function isUnknownTransactionCommitResult(error) {
7024
- if (!error || typeof error !== "object") {
7025
- return false;
7026
- }
7027
- const candidate = error;
7028
- return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
7180
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "UnknownTransactionCommitResult"));
7181
+ }
7182
+ function isTransientTransactionError(error) {
7183
+ if (isUnknownTransactionCommitResult(error)) return false;
7184
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "TransientTransactionError") || candidate.code === 112 || candidate.code === 117 || candidate.codeName === "WriteConflict" || candidate.codeName === "LockTimeout");
7029
7185
  }
7030
7186
  async function commitTransactionWithRetry(session, logger) {
7031
7187
  const maxAttempts = 3;
@@ -7541,26 +7697,27 @@ var CountQueue = class {
7541
7697
  * @returns The result of fn
7542
7698
  * @throws A controlled error when the queue is full or the wait times out
7543
7699
  */
7544
- async execute(fn) {
7700
+ async execute(fn, options = {}) {
7545
7701
  const startTime = Date.now();
7702
+ const signal = options.signal;
7703
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7546
7704
  if (this.running >= this.concurrency) {
7547
7705
  if (this.queue.length >= this.maxQueueSize) {
7548
7706
  this.stats.rejected += 1;
7549
7707
  throw createError(ErrorCodes.INVALID_OPERATION, `Count queue is full (${this.maxQueueSize})`);
7550
7708
  }
7551
7709
  this.stats.queued += 1;
7552
- await this._waitInQueue(startTime);
7710
+ await this._waitInQueue(startTime, signal);
7553
7711
  }
7712
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7554
7713
  this.running += 1;
7555
7714
  this.stats.executed += 1;
7556
- try {
7557
- const elapsed = Date.now() - startTime;
7558
- const remainingMs = Math.max(1, this.timeout - elapsed);
7559
- return await this._executeWithTimeout(fn, remainingMs);
7560
- } finally {
7715
+ const elapsed = Date.now() - startTime;
7716
+ const remainingMs = Math.max(1, this.timeout - elapsed);
7717
+ return this._executeWithTimeout(fn, remainingMs, signal, () => {
7561
7718
  this.running -= 1;
7562
7719
  this._wakeNext();
7563
- }
7720
+ });
7564
7721
  }
7565
7722
  /**
7566
7723
  * Get queue statistics (including live state).
@@ -7595,17 +7752,28 @@ var CountQueue = class {
7595
7752
  const entry = this.queue.shift();
7596
7753
  if (entry) {
7597
7754
  clearTimeout(entry.timer);
7755
+ entry.cleanup();
7598
7756
  entry.reject(createError(ErrorCodes.INVALID_OPERATION, "Count queue was cleared before execution"));
7599
7757
  }
7600
7758
  }
7601
7759
  }
7602
- _waitInQueue(startTime) {
7760
+ _waitInQueue(startTime, signal) {
7603
7761
  return new Promise((resolve, reject) => {
7604
7762
  let entry;
7763
+ const onAbort = () => {
7764
+ const index = this.queue.indexOf(entry);
7765
+ if (index !== -1) {
7766
+ this.queue.splice(index, 1);
7767
+ clearTimeout(entry.timer);
7768
+ entry.cleanup();
7769
+ reject(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
7770
+ }
7771
+ };
7605
7772
  const timer = setTimeout(() => {
7606
7773
  const index = this.queue.indexOf(entry);
7607
7774
  if (index !== -1) {
7608
7775
  this.queue.splice(index, 1);
7776
+ entry.cleanup();
7609
7777
  this.stats.timeout += 1;
7610
7778
  reject(createError(ErrorCodes.OPERATION_TIMEOUT, `Count queue wait timeout (${this.timeout}ms)`));
7611
7779
  }
@@ -7618,9 +7786,12 @@ var CountQueue = class {
7618
7786
  },
7619
7787
  reject,
7620
7788
  timer,
7621
- startTime
7789
+ startTime,
7790
+ cleanup: () => signal?.removeEventListener("abort", onAbort)
7622
7791
  };
7623
7792
  this.queue.push(entry);
7793
+ signal?.addEventListener("abort", onAbort, { once: true });
7794
+ if (signal?.aborted) onAbort();
7624
7795
  });
7625
7796
  }
7626
7797
  _wakeNext() {
@@ -7628,6 +7799,7 @@ var CountQueue = class {
7628
7799
  const entry = this.queue.shift();
7629
7800
  if (entry) {
7630
7801
  clearTimeout(entry.timer);
7802
+ entry.cleanup();
7631
7803
  entry.resolve();
7632
7804
  }
7633
7805
  }
@@ -7644,10 +7816,12 @@ var CountQueue = class {
7644
7816
  }
7645
7817
  return value;
7646
7818
  }
7647
- _executeWithTimeout(fn, timeoutMs = this.timeout) {
7819
+ _executeWithTimeout(fn, timeoutMs, signal, release) {
7648
7820
  let timer = null;
7649
7821
  const controller = new AbortController();
7650
- const timeoutPromise = new Promise((_, reject) => {
7822
+ let rejectCancellation;
7823
+ const cancellation = new Promise((_, reject) => {
7824
+ rejectCancellation = reject;
7651
7825
  timer = setTimeout(
7652
7826
  () => {
7653
7827
  controller.abort();
@@ -7657,14 +7831,22 @@ var CountQueue = class {
7657
7831
  timeoutMs
7658
7832
  );
7659
7833
  });
7834
+ const onAbort = () => {
7835
+ controller.abort();
7836
+ rejectCancellation(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
7837
+ };
7838
+ signal?.addEventListener("abort", onAbort, { once: true });
7660
7839
  let taskPromise;
7661
7840
  try {
7662
7841
  taskPromise = Promise.resolve(fn(controller.signal));
7663
7842
  } catch (error) {
7664
7843
  taskPromise = Promise.reject(error);
7665
7844
  }
7666
- return Promise.race([taskPromise, timeoutPromise]).finally(() => {
7845
+ void taskPromise.then(release, release);
7846
+ if (signal?.aborted) onAbort();
7847
+ return Promise.race([taskPromise, cancellation]).finally(() => {
7667
7848
  if (timer !== null) clearTimeout(timer);
7849
+ signal?.removeEventListener("abort", onAbort);
7668
7850
  });
7669
7851
  }
7670
7852
  _updateWaitTimeStats(waitTime) {
@@ -7677,6 +7859,7 @@ var CountQueue = class {
7677
7859
  };
7678
7860
 
7679
7861
  // src/capabilities/pool/pool-health-checker.ts
7862
+ var SILENT_HEALTH_CHECKER_LOGGER = {};
7680
7863
  var HealthChecker = class {
7681
7864
  constructor(options = {}) {
7682
7865
  this._healthStatus = /* @__PURE__ */ new Map();
@@ -7686,7 +7869,7 @@ var HealthChecker = class {
7686
7869
  this._inProgress = /* @__PURE__ */ new Set();
7687
7870
  this._started = false;
7688
7871
  this._poolManager = options.poolManager ?? null;
7689
- this._logger = options.logger ?? console;
7872
+ this._logger = options.logger ?? SILENT_HEALTH_CHECKER_LOGGER;
7690
7873
  }
7691
7874
  register(poolNameOrConfig, configOrClient) {
7692
7875
  let poolName;
@@ -7810,6 +7993,7 @@ var HealthChecker = class {
7810
7993
  };
7811
7994
 
7812
7995
  // src/capabilities/pool/pool-selector.ts
7996
+ var SILENT_POOL_SELECTOR_LOGGER = {};
7813
7997
  var POOL_STRATEGY_ALIASES = {
7814
7998
  "round-robin": "roundRobin",
7815
7999
  "least-connections": "leastConnections",
@@ -7823,7 +8007,7 @@ var PoolSelector = class {
7823
8007
  constructor(options = {}) {
7824
8008
  this._roundRobinIndex = /* @__PURE__ */ new Map();
7825
8009
  this._strategy = normalizePoolStrategy(options.strategy);
7826
- this._logger = options.logger ?? console;
8010
+ this._logger = options.logger ?? SILENT_POOL_SELECTOR_LOGGER;
7827
8011
  }
7828
8012
  select(pools, context) {
7829
8013
  if (!pools || pools.length === 0) {
@@ -7954,11 +8138,12 @@ var PoolSelector = class {
7954
8138
  };
7955
8139
 
7956
8140
  // src/capabilities/pool/pool-stats-manager.ts
8141
+ var SILENT_POOL_STATS_LOGGER = {};
7957
8142
  var PoolStatsManager = class {
7958
8143
  constructor(options = {}) {
7959
8144
  this._stats = /* @__PURE__ */ new Map();
7960
8145
  this._buffer = [];
7961
- this._logger = options.logger ?? console;
8146
+ this._logger = options.logger ?? SILENT_POOL_STATS_LOGGER;
7962
8147
  this._batchInterval = setInterval(() => {
7963
8148
  this._flush();
7964
8149
  }, 100);
@@ -8190,6 +8375,9 @@ var ConnectionPoolManager = class {
8190
8375
  this._closed = false;
8191
8376
  this._configs = /* @__PURE__ */ new Map();
8192
8377
  this._pendingAdds = /* @__PURE__ */ new Set();
8378
+ this._pendingAddTasks = /* @__PURE__ */ new Set();
8379
+ this._generation = 0;
8380
+ this._closePromise = null;
8193
8381
  this.logger = options.logger ?? null;
8194
8382
  this.maxPoolsCount = options.maxPoolsCount ?? 10;
8195
8383
  this.strategy = options.poolStrategy ?? "auto";
@@ -8206,9 +8394,9 @@ var ConnectionPoolManager = class {
8206
8394
  this._fallbackConfig = this.fallback;
8207
8395
  this._fallback = this.fallback;
8208
8396
  this._pools = this.pools;
8209
- this._selector = new PoolSelector({ strategy: this.strategy });
8210
- this._healthChecker = new HealthChecker({ poolManager: this });
8211
- this._stats = new PoolStatsManager({ logger: options.logger ?? void 0 });
8397
+ this._selector = new PoolSelector({ strategy: this.strategy, logger: this.logger ?? void 0 });
8398
+ this._healthChecker = new HealthChecker({ poolManager: this, logger: this.logger ?? void 0 });
8399
+ this._stats = new PoolStatsManager({ logger: this.logger ?? void 0 });
8212
8400
  }
8213
8401
  /**
8214
8402
  * Add a connection pool.
@@ -8216,35 +8404,21 @@ var ConnectionPoolManager = class {
8216
8404
  */
8217
8405
  async addPool(config) {
8218
8406
  validatePoolConfig(config);
8407
+ if (this._closed) {
8408
+ throw createError(ErrorCodes.INVALID_OPERATION, "Connection pool manager is closed");
8409
+ }
8219
8410
  if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
8220
8411
  throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8221
8412
  }
8222
- if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
8413
+ if (this.maxPoolsCount > 0 && this.pools.size + this._pendingAdds.size >= this.maxPoolsCount) {
8223
8414
  throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
8224
8415
  }
8225
8416
  this._pendingAdds.add(config.name);
8417
+ const generation = this._generation;
8418
+ const task = this._finishAddPool(config, generation);
8419
+ this._pendingAddTasks.add(task);
8226
8420
  try {
8227
- const client = await this.clientFactory(config);
8228
- if (this.pools.has(config.name)) {
8229
- await client.close().catch(() => {
8230
- });
8231
- throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8232
- }
8233
- this.pools.set(config.name, {
8234
- client,
8235
- config,
8236
- createdAt: Date.now()
8237
- });
8238
- this.healthStatus.set(config.name, {
8239
- status: "up",
8240
- consecutiveFailures: 0,
8241
- lastCheckTime: null,
8242
- lastError: null,
8243
- uptime: 0
8244
- });
8245
- this.stats.set(config.name, createEmptyPoolStats(config.name));
8246
- this._configs.set(config.name, config);
8247
- this._healthChecker.register(config.name, config.healthCheck ?? {});
8421
+ await task;
8248
8422
  } catch (err) {
8249
8423
  const error = err;
8250
8424
  const msg = error.message ?? "";
@@ -8258,8 +8432,31 @@ var ConnectionPoolManager = class {
8258
8432
  throw err;
8259
8433
  } finally {
8260
8434
  this._pendingAdds.delete(config.name);
8435
+ this._pendingAddTasks.delete(task);
8261
8436
  }
8262
8437
  }
8438
+ async _finishAddPool(config, generation) {
8439
+ const client = await this.clientFactory(config);
8440
+ if (this._closed || generation !== this._generation || this.pools.has(config.name)) {
8441
+ await client.close().catch(() => {
8442
+ });
8443
+ throw createError(
8444
+ this._closed || generation !== this._generation ? ErrorCodes.INVALID_OPERATION : ErrorCodes.INVALID_CONFIG,
8445
+ this._closed || generation !== this._generation ? "Connection pool manager closed while adding a pool" : `Pool '${config.name}' already exists`
8446
+ );
8447
+ }
8448
+ this.pools.set(config.name, { client, config, createdAt: Date.now() });
8449
+ this.healthStatus.set(config.name, {
8450
+ status: "up",
8451
+ consecutiveFailures: 0,
8452
+ lastCheckTime: null,
8453
+ lastError: null,
8454
+ uptime: 0
8455
+ });
8456
+ this.stats.set(config.name, createEmptyPoolStats(config.name));
8457
+ this._configs.set(config.name, config);
8458
+ this._healthChecker.register(config.name, config.healthCheck ?? {});
8459
+ }
8263
8460
  /**
8264
8461
  * Remove a connection pool.
8265
8462
  * @since v1.0.8
@@ -8415,16 +8612,24 @@ var ConnectionPoolManager = class {
8415
8612
  * @since v1.0.8
8416
8613
  */
8417
8614
  async close() {
8615
+ if (this._closePromise) return this._closePromise;
8616
+ this._closed = true;
8617
+ this._generation += 1;
8618
+ this._closePromise = this._closeAllPools();
8619
+ return this._closePromise;
8620
+ }
8621
+ async _closeAllPools() {
8418
8622
  this.stopHealthCheck();
8419
8623
  this._healthChecker.stop();
8420
8624
  this._stats.close();
8625
+ await Promise.allSettled([...this._pendingAddTasks]);
8421
8626
  for (const pool of this.pools.values()) {
8422
8627
  await pool.client.close();
8423
8628
  }
8424
8629
  this.pools.clear();
8425
8630
  this.healthStatus.clear();
8426
8631
  this.stats.clear();
8427
- this._closed = true;
8632
+ this._configs.clear();
8428
8633
  }
8429
8634
  // ─── v1 compat methods ───────────────────────────────────────────────────────────
8430
8635
  /** v1 compat: get pool health Map */
@@ -8612,6 +8817,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
8612
8817
  logger
8613
8818
  });
8614
8819
  } catch (err) {
8820
+ if (err?.code === ErrorCodes.INVALID_CONFIG) throw err;
8615
8821
  logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
8616
8822
  return null;
8617
8823
  }
@@ -8690,6 +8896,33 @@ async function loadModelFiles(options, logger, opts = {}) {
8690
8896
  }
8691
8897
  }
8692
8898
 
8899
+ // src/entry/runtime-health.ts
8900
+ function summarizeRuntimeHealth(cacheOption, cache, poolManager, invalidator) {
8901
+ const cacheStats = cache.getStats?.();
8902
+ const poolHealth = poolManager?.getHealthStatus();
8903
+ const distributedStats = invalidator?.getStats() ?? null;
8904
+ const cacheInput = cacheOption;
8905
+ const cacheEnabled = cacheInput?.enabled !== false;
8906
+ const distributedEnabled = cacheInput?.distributed !== void 0 && cacheInput.distributed.enabled !== false;
8907
+ const cacheProbe = cacheEnabled ? cache.getHealthStatus?.() : void 0;
8908
+ const cacheProbeStatus = cacheProbe === true || cacheProbe === "up" ? "up" : cacheProbe === false || cacheProbe === "down" ? "down" : "unknown";
8909
+ const redisState = invalidator;
8910
+ const redisStatuses = [redisState?.pub?.status, redisState?.sub?.status];
8911
+ const distributedStatus = !distributedEnabled || !invalidator ? "unknown" : redisStatuses.every((status) => status === "ready") ? "up" : redisStatuses.some((status) => status === "end" || status === "close") ? "down" : "unknown";
8912
+ const poolStates = Object.values(poolHealth ?? {}).map((health) => health.status);
8913
+ const poolsStatus = poolStates.length === 0 ? "unknown" : poolStates.some((status) => status === "down") ? "down" : poolStates.every((status) => status === "up") ? "up" : "unknown";
8914
+ return {
8915
+ cacheStats,
8916
+ poolHealth,
8917
+ distributedStats,
8918
+ cacheEnabled,
8919
+ distributedEnabled,
8920
+ cacheProbeStatus,
8921
+ distributedStatus,
8922
+ poolsStatus
8923
+ };
8924
+ }
8925
+
8693
8926
  // src/entry/runtime-defaults.ts
8694
8927
  function isPlainObject3(value) {
8695
8928
  if (value === null || typeof value !== "object") return false;
@@ -9340,36 +9573,36 @@ var FUNC_REGEX = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|S
9340
9573
  var IS_FUNC_CALL_RE = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|SQRT|POW|SIZE|IN|SLICE|FIRST|LAST|ARRAY_ELEM_AT|FILTER|MAP|INDEX_OF|CONCAT_ARRAYS|TYPE|NOT|EXISTS|IS_NUMBER|IS_ARRAY|SUM|AVG|MAX|MIN|COUNT|PUSH|ADD_TO_SET|YEAR|MONTH|DAY_OF_MONTH|HOUR|MINUTE|SECOND|SPLIT|REPLACE|INDEX_OF_STR|LTRIM|RTRIM|SUBSTR_CP|REGEX|MERGE_OBJECTS|TO_INT|TO_STRING|OBJECT_TO_ARRAY|ARRAY_TO_OBJECT|SET_UNION|SWITCH)\s*\(/i;
9341
9574
  function compileInnerExpression(expression) {
9342
9575
  const expr2 = expression.trim();
9343
- const funcMatch = expr2.match(FUNC_REGEX);
9344
- if (funcMatch) {
9345
- return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
9346
- }
9347
- const andParts = splitTopLevel2(expr2, "&&");
9348
- if (andParts.length > 1) {
9349
- return { $and: andParts.map((part) => compileInnerExpression(part.trim())) };
9576
+ if (!expr2) throw createError(ErrorCodes.INVALID_EXPRESSION, "Expression cannot be empty");
9577
+ const positions = topLevelPositions(expr2);
9578
+ const unwrapped = stripOuterParentheses(expr2);
9579
+ if (unwrapped !== expr2) return compileInnerExpression(unwrapped);
9580
+ if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(expr2)) return Number(expr2);
9581
+ const conditional = findConditional(expr2, positions);
9582
+ if (conditional) {
9583
+ return { $cond: {
9584
+ if: compileInnerExpression(expr2.slice(0, conditional.question)),
9585
+ then: compileInnerExpression(expr2.slice(conditional.question + 1, conditional.colon)),
9586
+ else: compileInnerExpression(expr2.slice(conditional.colon + 1))
9587
+ } };
9588
+ }
9589
+ const nullCoalParts = splitTopLevel2(expr2, "??", positions);
9590
+ if (nullCoalParts.length > 1) {
9591
+ return nullCoalParts.slice(1).reduce(
9592
+ (left, right) => ({ $ifNull: [left, compileInnerExpression(right)] }),
9593
+ compileInnerExpression(nullCoalParts[0])
9594
+ );
9350
9595
  }
9351
- const orParts = splitTopLevel2(expr2, "||");
9596
+ const orParts = splitTopLevel2(expr2, "||", positions);
9352
9597
  if (orParts.length > 1) {
9353
- return { $or: orParts.map((part) => compileInnerExpression(part.trim())) };
9598
+ return { $or: orParts.map((part) => compileInnerExpression(part)) };
9354
9599
  }
9355
- const ternary = /^([^?]+)\s*\?\s*([^:]+)\s*:\s*(.+)$/.exec(expr2);
9356
- if (ternary) {
9357
- const [, condition, thenPart, elsePart] = ternary;
9358
- return {
9359
- $cond: {
9360
- if: compileInnerExpression(condition.trim()),
9361
- then: parseThenElse(thenPart.trim()),
9362
- else: parseThenElse(elsePart.trim())
9363
- }
9364
- };
9365
- }
9366
- const nullCoalParts = splitTopLevel2(expr2, "??");
9367
- if (nullCoalParts.length > 1) {
9368
- return { $ifNull: [parseValue(nullCoalParts[0].trim()), parseValue(nullCoalParts[1].trim())] };
9600
+ const andParts = splitTopLevel2(expr2, "&&", positions);
9601
+ if (andParts.length > 1) {
9602
+ return { $and: andParts.map((part) => compileInnerExpression(part)) };
9369
9603
  }
9370
- const cmpMatch = /^(.+?)\s*(===|!==|>=|<=|>|<)\s*(.+)$/.exec(expr2);
9371
- if (cmpMatch) {
9372
- const [, left, operator, right] = cmpMatch;
9604
+ const comparison = findBinaryOperator(expr2, positions, ["===", "!==", ">=", "<=", ">", "<"]);
9605
+ if (comparison) {
9373
9606
  const operatorMap = {
9374
9607
  "===": "$eq",
9375
9608
  "!==": "$ne",
@@ -9378,24 +9611,39 @@ function compileInnerExpression(expression) {
9378
9611
  ">": "$gt",
9379
9612
  "<": "$lt"
9380
9613
  };
9381
- return {
9382
- [operatorMap[operator]]: [
9383
- compileInnerExpression(left.trim()),
9384
- compileInnerExpression(right.trim())
9385
- ]
9386
- };
9614
+ return { [operatorMap[comparison.operator]]: [
9615
+ compileInnerExpression(expr2.slice(0, comparison.index)),
9616
+ compileInnerExpression(expr2.slice(comparison.index + comparison.operator.length))
9617
+ ] };
9618
+ }
9619
+ const additive = findBinaryOperator(expr2, positions, ["+", "-"], true);
9620
+ if (additive) {
9621
+ return { [additive.operator === "+" ? "$add" : "$subtract"]: [
9622
+ compileInnerExpression(expr2.slice(0, additive.index)),
9623
+ compileInnerExpression(expr2.slice(additive.index + 1))
9624
+ ] };
9625
+ }
9626
+ const multiplicative = findBinaryOperator(expr2, positions, ["*", "/", "%"], true);
9627
+ if (multiplicative) {
9628
+ const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
9629
+ return { [operatorMap[multiplicative.operator]]: [
9630
+ compileInnerExpression(expr2.slice(0, multiplicative.index)),
9631
+ compileInnerExpression(expr2.slice(multiplicative.index + 1))
9632
+ ] };
9387
9633
  }
9388
- const addSubMatch = /^(.+?)\s*([+\-])\s*(.+)$/.exec(expr2);
9389
- if (addSubMatch) {
9390
- const [, left, operator, right] = addSubMatch;
9391
- const operatorMap = { "+": "$add", "-": "$subtract" };
9392
- return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
9634
+ if (expr2.startsWith("-")) {
9635
+ return { $multiply: [-1, compileInnerExpression(expr2.slice(1))] };
9393
9636
  }
9394
- const mulDivMatch = /^(.+?)\s*([*\/%])\s*(.+)$/.exec(expr2);
9395
- if (mulDivMatch) {
9396
- const [, left, operator, right] = mulDivMatch;
9397
- const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
9398
- return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
9637
+ const funcMatch = expr2.match(FUNC_REGEX);
9638
+ if (funcMatch) {
9639
+ try {
9640
+ return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
9641
+ } catch (error) {
9642
+ if (error && typeof error === "object" && "code" in error && error.code === ErrorCodes.INVALID_EXPRESSION) {
9643
+ throw error;
9644
+ }
9645
+ throw createError(ErrorCodes.INVALID_EXPRESSION, `Invalid arguments for ${funcMatch[1].toUpperCase()}`);
9646
+ }
9399
9647
  }
9400
9648
  const genericFuncCallRe = /^[A-Za-z_][A-Za-z0-9_]*\s*\(.+\)$/;
9401
9649
  if (genericFuncCallRe.test(expr2)) {
@@ -9404,9 +9652,6 @@ function compileInnerExpression(expression) {
9404
9652
  }
9405
9653
  return parseValue(expr2);
9406
9654
  }
9407
- function parseThenElse(source) {
9408
- return source.includes("?") && source.includes(":") ? compileInnerExpression(source) : parseValue(source);
9409
- }
9410
9655
  function parseValue(value) {
9411
9656
  const normalized = stripOuterParentheses(value.trim());
9412
9657
  if (normalized.startsWith("'") && normalized.endsWith("'") || normalized.startsWith('"') && normalized.endsWith('"')) {
@@ -9416,18 +9661,22 @@ function parseValue(value) {
9416
9661
  if (normalized === "true") return true;
9417
9662
  if (normalized === "false") return false;
9418
9663
  if (!isNaN(Number(normalized)) && normalized !== "") return Number(normalized);
9419
- if (IS_FUNC_CALL_RE.test(normalized)) {
9664
+ if (normalized !== value.trim() || IS_FUNC_CALL_RE.test(normalized) || FUNC_REGEX.test(normalized)) {
9420
9665
  return compileInnerExpression(normalized);
9421
9666
  }
9422
- if (normalized.startsWith("$")) return normalized;
9423
- return `$${normalized}`;
9424
- }
9425
- function parseOperand(value) {
9426
- const normalized = stripOuterParentheses(value);
9427
- if (/[+\-*/%]/.test(normalized)) {
9667
+ if (normalized.startsWith("[") || normalized.startsWith("{")) {
9668
+ try {
9669
+ return JSON.parse(normalized);
9670
+ } catch {
9671
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Invalid array or object literal");
9672
+ }
9673
+ }
9674
+ const positions = topLevelPositions(normalized);
9675
+ if (positions.some((index) => /[?+\-*/%><&|]/.test(normalized[index]))) {
9428
9676
  return compileInnerExpression(normalized);
9429
9677
  }
9430
- return parseValue(normalized);
9678
+ if (normalized.startsWith("$")) return normalized;
9679
+ return `$${normalized}`;
9431
9680
  }
9432
9681
  function dispatchFunction(name, argsStr) {
9433
9682
  const args = splitArgsStr(argsStr);
@@ -9729,72 +9978,82 @@ function compileMapExpression(exprStr, varName) {
9729
9978
  return compileInnerExpression(replaced);
9730
9979
  }
9731
9980
  function splitArgsStr(argsStr) {
9732
- const args = [];
9733
- let current = "";
9734
- let inString = false;
9735
- let stringChar = "";
9736
- let parenDepth = 0;
9737
- for (let index = 0; index < argsStr.length; index++) {
9738
- const ch = argsStr[index];
9739
- if ((ch === '"' || ch === "'") && (index === 0 || argsStr[index - 1] !== "\\")) {
9740
- if (!inString) {
9741
- inString = true;
9742
- stringChar = ch;
9743
- } else if (ch === stringChar) {
9744
- inString = false;
9745
- stringChar = "";
9746
- }
9747
- current += ch;
9748
- } else if (ch === "(" && !inString) {
9749
- parenDepth++;
9750
- current += ch;
9751
- } else if (ch === ")" && !inString) {
9752
- parenDepth--;
9753
- current += ch;
9754
- } else if (ch === "," && !inString && parenDepth === 0) {
9755
- args.push(current.trim());
9756
- current = "";
9757
- } else {
9758
- current += ch;
9759
- }
9760
- }
9761
- if (current.trim()) args.push(current.trim());
9762
- return args;
9981
+ if (!argsStr.trim()) return [];
9982
+ return splitTopLevel2(argsStr, ",");
9763
9983
  }
9764
- function splitTopLevel2(source, separator) {
9765
- const parts = [];
9766
- let current = "";
9767
- let parenDepth = 0;
9768
- let inString = false;
9769
- let stringChar = "";
9984
+ function topLevelPositions(source) {
9985
+ const positions = [];
9986
+ const stack = [];
9987
+ let quote = null;
9770
9988
  for (let index = 0; index < source.length; index++) {
9771
9989
  const ch = source[index];
9772
- if ((ch === '"' || ch === "'") && (index === 0 || source[index - 1] !== "\\")) {
9773
- if (!inString) {
9774
- inString = true;
9775
- stringChar = ch;
9776
- } else if (ch === stringChar) {
9777
- inString = false;
9778
- stringChar = "";
9990
+ if (quote) {
9991
+ if (ch === quote && source[index - 1] !== "\\") quote = null;
9992
+ continue;
9993
+ }
9994
+ if (ch === '"' || ch === "'") {
9995
+ quote = ch;
9996
+ continue;
9997
+ }
9998
+ if (ch === "(" || ch === "[" || ch === "{") {
9999
+ stack.push(ch);
10000
+ continue;
10001
+ }
10002
+ if (ch === ")" || ch === "]" || ch === "}") {
10003
+ const opening = stack.pop();
10004
+ if (ch === ")" && opening !== "(" || ch === "]" && opening !== "[" || ch === "}" && opening !== "{") {
10005
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Unbalanced expression delimiters");
9779
10006
  }
9780
- current += ch;
9781
- } else if (!inString && ch === "(") {
9782
- parenDepth++;
9783
- current += ch;
9784
- } else if (!inString && ch === ")") {
9785
- parenDepth--;
9786
- current += ch;
9787
- } else if (!inString && parenDepth === 0 && source.startsWith(separator, index)) {
9788
- parts.push(current);
9789
- current = "";
9790
- index += separator.length - 1;
9791
- } else {
9792
- current += ch;
10007
+ continue;
9793
10008
  }
10009
+ if (stack.length === 0) positions.push(index);
10010
+ }
10011
+ if (quote || stack.length > 0) {
10012
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Unbalanced expression delimiters");
10013
+ }
10014
+ return positions;
10015
+ }
10016
+ function splitTopLevel2(source, separator, positions = topLevelPositions(source)) {
10017
+ const parts = [];
10018
+ let start = 0;
10019
+ for (const index of positions) {
10020
+ if (index < start || !source.startsWith(separator, index)) continue;
10021
+ parts.push(source.slice(start, index).trim());
10022
+ start = index + separator.length;
10023
+ }
10024
+ if (parts.length === 0) return [source];
10025
+ parts.push(source.slice(start).trim());
10026
+ if (parts.some((part) => !part)) {
10027
+ throw createError(ErrorCodes.INVALID_EXPRESSION, `Missing operand around ${separator}`);
9794
10028
  }
9795
- parts.push(current);
9796
10029
  return parts;
9797
10030
  }
10031
+ function findConditional(source, positions) {
10032
+ const question = positions.find((index) => source[index] === "?" && source[index - 1] !== "?" && source[index + 1] !== "?");
10033
+ if (question === void 0) return null;
10034
+ let nested = 0;
10035
+ for (const index of positions) {
10036
+ if (index <= question) continue;
10037
+ if (source[index] === "?" && source[index - 1] !== "?" && source[index + 1] !== "?") nested++;
10038
+ if (source[index] === ":") {
10039
+ if (nested === 0) return { question, colon: index };
10040
+ nested--;
10041
+ }
10042
+ }
10043
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Conditional expression is missing a branch");
10044
+ }
10045
+ function findBinaryOperator(source, positions, operators, rightmost = false) {
10046
+ let match = null;
10047
+ for (const index of positions) {
10048
+ const operator = operators.find((candidate) => source.startsWith(candidate, index));
10049
+ if (!operator) continue;
10050
+ if ((operator === "+" || operator === "-") && !source.slice(0, index).trim()) continue;
10051
+ if ((operator === "+" || operator === "-") && /[+\-*/%<>=!?&|:,]$/.test(source.slice(0, index).trimEnd())) continue;
10052
+ match = { index, operator };
10053
+ if (!rightmost) break;
10054
+ }
10055
+ return match;
10056
+ }
9798
10057
  function stripOuterParentheses(source) {
9799
10058
  let normalized = source.trim();
9800
10059
  while (normalized.startsWith("(") && normalized.endsWith(")") && isWrappedByOuterParentheses(normalized)) {
@@ -9840,10 +10099,15 @@ function isExpressionObject(value) {
9840
10099
  value && typeof value === "object" && "__expr__" in value && typeof value.__expr__ === "string" && "__compiled__" in value && typeof value.__compiled__ === "boolean"
9841
10100
  );
9842
10101
  }
10102
+ function isPlainExpressionRecord(value) {
10103
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
10104
+ const prototype = Object.getPrototypeOf(value);
10105
+ return prototype === Object.prototype || prototype === null;
10106
+ }
9843
10107
  function hasExpressionInObject(value) {
9844
10108
  if (isExpressionObject(value)) return true;
9845
- if (!value || typeof value !== "object") return false;
9846
10109
  if (Array.isArray(value)) return value.some((item) => hasExpressionInObject(item));
10110
+ if (!isPlainExpressionRecord(value)) return false;
9847
10111
  return Object.values(value).some((item) => hasExpressionInObject(item));
9848
10112
  }
9849
10113
  function hasExpressionInPipeline(pipeline) {
@@ -9860,7 +10124,7 @@ function transformExpressions(value, context) {
9860
10124
  if (Array.isArray(value)) {
9861
10125
  return value.map((item) => transformStageEntry(item));
9862
10126
  }
9863
- if (!value || typeof value !== "object") return value;
10127
+ if (!isPlainExpressionRecord(value)) return value;
9864
10128
  const entries = Object.entries(value);
9865
10129
  const result = {};
9866
10130
  for (const [key, current] of entries) {
@@ -9869,7 +10133,7 @@ function transformExpressions(value, context) {
9869
10133
  return result;
9870
10134
  }
9871
10135
  function transformStageEntry(stage) {
9872
- if (!stage || typeof stage !== "object" || Array.isArray(stage)) {
10136
+ if (!isPlainExpressionRecord(stage)) {
9873
10137
  return transformExpressions(stage, "project");
9874
10138
  }
9875
10139
  const entries = Object.entries(stage);
@@ -9893,7 +10157,7 @@ function transformStageValue(value, context) {
9893
10157
  if (Array.isArray(value)) {
9894
10158
  return value.map((item) => transformStageValue(item, context));
9895
10159
  }
9896
- if (!value || typeof value !== "object") return value;
10160
+ if (!isPlainExpressionRecord(value)) return value;
9897
10161
  const entries = Object.entries(value);
9898
10162
  const result = {};
9899
10163
  for (const [key, current] of entries) {
@@ -10824,6 +11088,7 @@ var AggregateChain = class {
10824
11088
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).explain(verbosity === true ? "queryPlanner" : verbosity);
10825
11089
  }
10826
11090
  stream() {
11091
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10827
11092
  const stream = this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).stream();
10828
11093
  const onWriteComplete = this.writePipelineHooks?.onWriteComplete;
10829
11094
  if (onWriteComplete) {
@@ -10841,6 +11106,7 @@ var AggregateChain = class {
10841
11106
  throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
10842
11107
  }
10843
11108
  this.executed = true;
11109
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10844
11110
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray().then(async (result) => {
10845
11111
  await this.writePipelineHooks?.onWriteComplete?.();
10846
11112
  return result;
@@ -11157,48 +11423,59 @@ function stripWriteCacheControlOptions(options) {
11157
11423
 
11158
11424
  // src/adapters/mongodb/writes/write-basic.ts
11159
11425
  async function insertOneDocument(collection, ...args) {
11426
+ assertManagedTransactionWriteAllowed(args[1]);
11160
11427
  return collection.insertOne(...args);
11161
11428
  }
11162
11429
  async function insertManyDocuments(collection, ...args) {
11430
+ assertManagedTransactionWriteAllowed(args[1]);
11163
11431
  return collection.insertMany(...args);
11164
11432
  }
11165
11433
  async function updateOneDocument(collection, ...args) {
11434
+ assertManagedTransactionWriteAllowed(args[2]);
11166
11435
  return collection.updateOne(...args);
11167
11436
  }
11168
11437
  async function updateManyDocuments(collection, ...args) {
11438
+ assertManagedTransactionWriteAllowed(args[2]);
11169
11439
  return collection.updateMany(...args);
11170
11440
  }
11171
11441
  async function replaceOneDocument(collection, ...args) {
11442
+ assertManagedTransactionWriteAllowed(args[2]);
11172
11443
  return collection.replaceOne(...args);
11173
11444
  }
11174
11445
  async function findOneAndUpdateDocument(collection, filter, update, options) {
11446
+ assertManagedTransactionWriteAllowed(options);
11175
11447
  if (options !== void 0) {
11176
11448
  return collection.findOneAndUpdate(filter, update, options);
11177
11449
  }
11178
11450
  return collection.findOneAndUpdate(filter, update);
11179
11451
  }
11180
11452
  async function findOneAndReplaceDocument(collection, filter, replacement, options) {
11453
+ assertManagedTransactionWriteAllowed(options);
11181
11454
  if (options !== void 0) {
11182
11455
  return collection.findOneAndReplace(filter, replacement, options);
11183
11456
  }
11184
11457
  return collection.findOneAndReplace(filter, replacement);
11185
11458
  }
11186
11459
  async function findOneAndDeleteDocument(collection, filter, options) {
11460
+ assertManagedTransactionWriteAllowed(options);
11187
11461
  if (options !== void 0) {
11188
11462
  return collection.findOneAndDelete(filter, options);
11189
11463
  }
11190
11464
  return collection.findOneAndDelete(filter);
11191
11465
  }
11192
11466
  async function upsertOneDocument(collection, filter, update, options = {}) {
11467
+ assertManagedTransactionWriteAllowed(options);
11193
11468
  return collection.updateOne(filter, update, {
11194
11469
  ...options,
11195
11470
  upsert: true
11196
11471
  });
11197
11472
  }
11198
11473
  async function deleteOneDocument(collection, ...args) {
11474
+ assertManagedTransactionWriteAllowed(args[1]);
11199
11475
  return collection.deleteOne(...args);
11200
11476
  }
11201
11477
  async function deleteManyDocuments(collection, ...args) {
11478
+ assertManagedTransactionWriteAllowed(args[1]);
11202
11479
  return collection.deleteMany(...args);
11203
11480
  }
11204
11481
  async function incrementOneDocument(collection, filter, field, incrementOrOptions, maybeOptions) {
@@ -11225,6 +11502,7 @@ async function incrementOneDocument(collection, filter, field, incrementOrOption
11225
11502
  includeResultMetadata: true
11226
11503
  };
11227
11504
  if (normalizedProjection) findOptions.projection = normalizedProjection;
11505
+ assertManagedTransactionWriteAllowed(findOptions);
11228
11506
  const rawResult = await collection.findOneAndUpdate(
11229
11507
  filter,
11230
11508
  updateDocument,
@@ -11357,14 +11635,25 @@ async function runWithConcurrency(items, concurrency, worker) {
11357
11635
  return;
11358
11636
  }
11359
11637
  let nextIndex = 0;
11638
+ let stopAcquiring = false;
11639
+ let firstFatal;
11360
11640
  const workerCount = Math.min(concurrency, items.length);
11361
11641
  await Promise.all(Array.from({ length: workerCount }, async () => {
11362
- while (nextIndex < items.length) {
11642
+ while (!stopAcquiring && nextIndex < items.length) {
11363
11643
  const item = items[nextIndex];
11364
11644
  nextIndex += 1;
11365
- await worker(item);
11645
+ try {
11646
+ await worker(item);
11647
+ } catch (error) {
11648
+ if (!stopAcquiring) {
11649
+ firstFatal = error;
11650
+ stopAcquiring = true;
11651
+ }
11652
+ break;
11653
+ }
11366
11654
  }
11367
11655
  }));
11656
+ if (stopAcquiring) throw firstFatal;
11368
11657
  }
11369
11658
  async function insertBatchDocuments(collection, documents, options = {}) {
11370
11659
  if (!Array.isArray(documents)) {
@@ -11411,12 +11700,14 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11411
11700
  insertedIds: {},
11412
11701
  retries: []
11413
11702
  };
11703
+ assertManagedTransactionWriteAllowed(driverOptions);
11414
11704
  await runWithConcurrency(batchPlans, effectiveConcurrency, async ({ batchIndex, batch, batchOffset: batchOffset2 }) => {
11415
11705
  let pendingBatch = batch;
11416
11706
  let pendingIndexes = batch.map((_, index) => index);
11417
11707
  let attempts = 0;
11418
11708
  while (true) {
11419
11709
  try {
11710
+ assertManagedTransactionWriteAllowed(driverOptions);
11420
11711
  const batchResult = await collection.insertMany(pendingBatch, {
11421
11712
  ...driverOptions,
11422
11713
  ordered
@@ -11436,6 +11727,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11436
11727
  });
11437
11728
  break;
11438
11729
  } catch (cause) {
11730
+ assertManagedTransactionWriteAllowed(driverOptions);
11439
11731
  const errorRecord = {
11440
11732
  batchIndex,
11441
11733
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11507,6 +11799,10 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11507
11799
  });
11508
11800
  return result;
11509
11801
  }
11802
+ function batchWriteFilter(filter, batch) {
11803
+ const ids = { _id: { $in: batch } };
11804
+ return Object.prototype.hasOwnProperty.call(filter, "_id") ? { $and: [filter, ids] } : { ...filter, ...ids };
11805
+ }
11510
11806
  async function updateBatchDocuments(collection, filter, update, options = {}) {
11511
11807
  if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
11512
11808
  throw createError(ErrorCodes.INVALID_ARGUMENT, "filter must be a non-array object");
@@ -11550,6 +11846,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11550
11846
  "updateBatch retry requires an idempotent update; non-idempotent operators such as $inc/$push cannot be safely replayed"
11551
11847
  );
11552
11848
  }
11849
+ assertManagedTransactionWriteAllowed(driverOptions);
11553
11850
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11554
11851
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11555
11852
  const result = {
@@ -11566,8 +11863,9 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11566
11863
  let attempts = 0;
11567
11864
  while (true) {
11568
11865
  try {
11866
+ assertManagedTransactionWriteAllowed(driverOptions);
11569
11867
  const batchResult = await collection.updateMany(
11570
- { _id: { $in: batch } },
11868
+ batchWriteFilter(filter, batch),
11571
11869
  update,
11572
11870
  driverOptions
11573
11871
  );
@@ -11585,6 +11883,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11585
11883
  });
11586
11884
  break;
11587
11885
  } catch (cause) {
11886
+ assertManagedTransactionWriteAllowed(driverOptions);
11588
11887
  const errorRecord = {
11589
11888
  batchIndex,
11590
11889
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11647,6 +11946,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11647
11946
  if (!Number.isInteger(retryAttempts) || retryAttempts < 0) {
11648
11947
  throw createError(ErrorCodes.INVALID_ARGUMENT, "retryAttempts must be a non-negative integer");
11649
11948
  }
11949
+ assertManagedTransactionWriteAllowed(driverOptions);
11650
11950
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11651
11951
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11652
11952
  const result = {
@@ -11661,8 +11961,9 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11661
11961
  let attempts = 0;
11662
11962
  while (true) {
11663
11963
  try {
11964
+ assertManagedTransactionWriteAllowed(driverOptions);
11664
11965
  const batchResult = await collection.deleteMany(
11665
- { _id: { $in: batch } },
11966
+ batchWriteFilter(filter, batch),
11666
11967
  driverOptions
11667
11968
  );
11668
11969
  result.deletedCount += batchResult.deletedCount;
@@ -11677,6 +11978,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11677
11978
  });
11678
11979
  break;
11679
11980
  } catch (cause) {
11981
+ assertManagedTransactionWriteAllowed(driverOptions);
11680
11982
  const errorRecord = {
11681
11983
  batchIndex,
11682
11984
  message: cause instanceof Error ? cause.message : String(cause),
@@ -12205,9 +12507,11 @@ var MongoCollectionAccessor = class {
12205
12507
  const normalizedQuery = this._cvFilter(query);
12206
12508
  const maxTimeMS = this.management.defaults?.maxTimeMS;
12207
12509
  const merged = { ...maxTimeMS !== void 0 ? { maxTimeMS } : {}, ...options ?? {} };
12208
- const cacheTTL = typeof merged.cache === "number" ? merged.cache : 0;
12510
+ const cacheTTL = merged.explain === void 0 || merged.explain === false ? typeof merged.cache === "number" ? merged.cache : 0 : 0;
12209
12511
  const { cache: _cache, ...keyOptions } = merged;
12210
12512
  void _cache;
12513
+ const callerSignal = keyOptions.signal;
12514
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12211
12515
  const executeCount = (signal) => countDocuments(
12212
12516
  this.collectionRef,
12213
12517
  normalizedQuery ?? {},
@@ -12218,15 +12522,16 @@ var MongoCollectionAccessor = class {
12218
12522
  const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
12219
12523
  const cached = await Promise.resolve(this.management.queryCache.get(cacheKey));
12220
12524
  if (cached !== void 0) {
12525
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12221
12526
  return Promise.resolve(wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, cached));
12222
12527
  }
12223
- const runner2 = countQueue ? countQueue.execute(executeCount) : executeCount();
12528
+ const runner2 = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12224
12529
  return runner2.then(async (result) => {
12225
12530
  await Promise.resolve(this.management.queryCache?.set(cacheKey, result, cacheTTL));
12226
12531
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result);
12227
12532
  });
12228
12533
  }
12229
- const runner = countQueue ? countQueue.execute(executeCount) : executeCount();
12534
+ const runner = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12230
12535
  return runner.then((result) => wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result));
12231
12536
  }
12232
12537
  /** Runs an aggregation pipeline and returns a chainable aggregate cursor. */
@@ -12783,10 +13088,10 @@ function assertCompatPoolExists(poolManager, poolName) {
12783
13088
  function createPoolScope(runtime, poolName) {
12784
13089
  return {
12785
13090
  collection: (name) => runtime.scopedCollection(name, { pool: poolName }),
12786
- model: (name) => runtime.scopedModel(name, { pool: poolName }),
13091
+ model: (model) => runtime.scopedModel(model, { pool: poolName }),
12787
13092
  use: (dbName) => ({
12788
13093
  collection: (name) => runtime.scopedCollection(name, { pool: poolName, database: dbName }),
12789
- model: (name) => runtime.scopedModel(name, { pool: poolName, database: dbName })
13094
+ model: (model) => runtime.scopedModel(model, { pool: poolName, database: dbName })
12790
13095
  })
12791
13096
  };
12792
13097
  }
@@ -13515,20 +13820,21 @@ var DistributedCacheLockManager = class {
13515
13820
  }
13516
13821
  }
13517
13822
  async withLock(key, callback, options = {}) {
13823
+ let lock;
13518
13824
  try {
13519
- const lock = await this.acquireLock(key, options);
13520
- try {
13521
- return await callback();
13522
- } finally {
13523
- await lock.release().catch(() => {
13524
- });
13525
- }
13825
+ lock = await this.acquireLock(key, options);
13526
13826
  } catch (error) {
13527
13827
  if (this._isRedisConnectionError(error) && options.fallbackToNoLock) {
13528
13828
  return callback();
13529
13829
  }
13530
13830
  throw error;
13531
13831
  }
13832
+ try {
13833
+ return await callback();
13834
+ } finally {
13835
+ await lock.release().catch(() => {
13836
+ });
13837
+ }
13532
13838
  }
13533
13839
  async acquireLock(key, options = {}) {
13534
13840
  const ttl = options.ttl ?? 1e4;
@@ -14631,8 +14937,6 @@ var SlowQueryLogManager = class {
14631
14937
  };
14632
14938
 
14633
14939
  // src/capabilities/sync/index.ts
14634
- var import_promises = require("node:fs/promises");
14635
- var import_node_path2 = __toESM(require("node:path"));
14636
14940
  var import_mongodb7 = require("mongodb");
14637
14941
 
14638
14942
  // src/capabilities/sync/idempotency.ts
@@ -14735,25 +15039,19 @@ function validateSyncIdempotencyConfig(config) {
14735
15039
  }
14736
15040
  }
14737
15041
 
14738
- // src/capabilities/sync/index.ts
15042
+ // src/capabilities/sync/resume-token-store.ts
15043
+ var import_promises = require("node:fs/promises");
15044
+ var import_node_path2 = __toESM(require("node:path"));
14739
15045
  function normalizeError(error) {
14740
15046
  return error instanceof Error ? error : new Error(String(error));
14741
15047
  }
14742
15048
  function delay2(ms) {
14743
- if (ms <= 0) {
14744
- return Promise.resolve();
14745
- }
15049
+ if (ms <= 0) return Promise.resolve();
14746
15050
  return new Promise((resolve) => {
14747
15051
  const timer = setTimeout(resolve, ms);
14748
15052
  timer.unref?.();
14749
15053
  });
14750
15054
  }
14751
- function normalizeCollectionFilter(collections) {
14752
- if (!collections?.length || collections.includes("*")) {
14753
- return null;
14754
- }
14755
- return new Set(collections);
14756
- }
14757
15055
  async function syncDirectory(directory) {
14758
15056
  try {
14759
15057
  const handle = await (0, import_promises.open)(directory, "r");
@@ -14765,35 +15063,6 @@ async function syncDirectory(directory) {
14765
15063
  } catch {
14766
15064
  }
14767
15065
  }
14768
- function validateTargetConfig(target, index) {
14769
- if (!target || typeof target !== "object") {
14770
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
14771
- }
14772
- if (typeof target.name !== "string" || target.name.trim() === "") {
14773
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
14774
- }
14775
- if (!target.apply && !target.uri && !target.pool) {
14776
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
14777
- }
14778
- if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
14779
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
14780
- }
14781
- if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
14782
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
14783
- }
14784
- if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
14785
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
14786
- }
14787
- if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
14788
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
14789
- }
14790
- if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14791
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
14792
- }
14793
- if (target.apply !== void 0 && typeof target.apply !== "function") {
14794
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
14795
- }
14796
- }
14797
15066
  function validateResumeTokenConfig(config) {
14798
15067
  if (!config || typeof config !== "object") {
14799
15068
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken must be an object.");
@@ -14824,39 +15093,6 @@ function validateResumeTokenConfig(config) {
14824
15093
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken.saveRetryDelayMs must be a non-negative integer.");
14825
15094
  }
14826
15095
  }
14827
- function validateSyncConfig(config) {
14828
- if (!config || typeof config !== "object") {
14829
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
14830
- }
14831
- if (typeof config.enabled !== "boolean") {
14832
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
14833
- }
14834
- if (!config.enabled) {
14835
- return;
14836
- }
14837
- if (!Array.isArray(config.targets) || config.targets.length === 0) {
14838
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
14839
- }
14840
- if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
14841
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
14842
- }
14843
- if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14844
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
14845
- }
14846
- if (config.filter !== void 0 && typeof config.filter !== "function") {
14847
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
14848
- }
14849
- if (config.transform !== void 0 && typeof config.transform !== "function") {
14850
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
14851
- }
14852
- validateSyncIdempotencyConfig(config.idempotency);
14853
- config.targets.forEach((target, index) => {
14854
- validateTargetConfig(target, index);
14855
- });
14856
- if (config.resumeToken) {
14857
- validateResumeTokenConfig(config.resumeToken);
14858
- }
14859
- }
14860
15096
  var ResumeTokenStore = class {
14861
15097
  constructor(options = {}) {
14862
15098
  this.storage = options.storage ?? "file";
@@ -14880,16 +15116,9 @@ var ResumeTokenStore = class {
14880
15116
  return JSON.parse(payload);
14881
15117
  } catch (error) {
14882
15118
  const code = error?.code;
14883
- if (code !== "ENOENT") {
14884
- this.logger?.warn?.("[Sync] failed to load resume token", error);
14885
- }
15119
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to load resume token", error);
14886
15120
  if (code !== "ENOENT" && this.strictLoad) {
14887
- throw createError(
14888
- ErrorCodes.DATABASE_ERROR,
14889
- "[Sync] failed to load resume token",
14890
- void 0,
14891
- normalizeError(error)
14892
- );
15121
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to load resume token", void 0, normalizeError(error));
14893
15122
  }
14894
15123
  return null;
14895
15124
  }
@@ -14915,12 +15144,7 @@ var ResumeTokenStore = class {
14915
15144
  }
14916
15145
  this.logger?.error?.("[Sync] failed to save resume token", lastError);
14917
15146
  if (this.strictSave) {
14918
- throw createError(
14919
- ErrorCodes.DATABASE_ERROR,
14920
- "[Sync] failed to save resume token",
14921
- void 0,
14922
- lastError ?? void 0
14923
- );
15147
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to save resume token", void 0, lastError ?? void 0);
14924
15148
  }
14925
15149
  }
14926
15150
  async writePayload(payload) {
@@ -14953,9 +15177,7 @@ var ResumeTokenStore = class {
14953
15177
  await (0, import_promises.copyFile)(this.path, `${this.path}.bak`);
14954
15178
  } catch (error) {
14955
15179
  const code = error?.code;
14956
- if (code !== "ENOENT") {
14957
- throw error;
14958
- }
15180
+ if (code !== "ENOENT") throw error;
14959
15181
  }
14960
15182
  }
14961
15183
  async clear() {
@@ -14967,12 +15189,80 @@ var ResumeTokenStore = class {
14967
15189
  await (0, import_promises.unlink)(this.path);
14968
15190
  } catch (error) {
14969
15191
  const code = error?.code;
14970
- if (code !== "ENOENT") {
14971
- this.logger?.warn?.("[Sync] failed to clear resume token", error);
14972
- }
15192
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to clear resume token", error);
14973
15193
  }
14974
15194
  }
14975
15195
  };
15196
+
15197
+ // src/capabilities/sync/index.ts
15198
+ function normalizeCollectionFilter(collections) {
15199
+ if (!collections?.length || collections.includes("*")) {
15200
+ return null;
15201
+ }
15202
+ return new Set(collections);
15203
+ }
15204
+ function validateTargetConfig(target, index) {
15205
+ if (!target || typeof target !== "object") {
15206
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
15207
+ }
15208
+ if (typeof target.name !== "string" || target.name.trim() === "") {
15209
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
15210
+ }
15211
+ if (!target.apply && !target.uri && !target.pool) {
15212
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
15213
+ }
15214
+ if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
15215
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
15216
+ }
15217
+ if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
15218
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
15219
+ }
15220
+ if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
15221
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
15222
+ }
15223
+ if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
15224
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
15225
+ }
15226
+ if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
15227
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
15228
+ }
15229
+ if (target.apply !== void 0 && typeof target.apply !== "function") {
15230
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
15231
+ }
15232
+ }
15233
+ function validateSyncConfig(config) {
15234
+ if (!config || typeof config !== "object") {
15235
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
15236
+ }
15237
+ if (typeof config.enabled !== "boolean") {
15238
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
15239
+ }
15240
+ if (!config.enabled) {
15241
+ return;
15242
+ }
15243
+ if (!Array.isArray(config.targets) || config.targets.length === 0) {
15244
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
15245
+ }
15246
+ if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
15247
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
15248
+ }
15249
+ if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
15250
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
15251
+ }
15252
+ if (config.filter !== void 0 && typeof config.filter !== "function") {
15253
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
15254
+ }
15255
+ if (config.transform !== void 0 && typeof config.transform !== "function") {
15256
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
15257
+ }
15258
+ validateSyncIdempotencyConfig(config.idempotency);
15259
+ config.targets.forEach((target, index) => {
15260
+ validateTargetConfig(target, index);
15261
+ });
15262
+ if (config.resumeToken) {
15263
+ validateResumeTokenConfig(config.resumeToken);
15264
+ }
15265
+ }
14976
15266
  var ChangeStreamSyncManager = class {
14977
15267
  constructor(options) {
14978
15268
  this.targets = [];
@@ -14981,7 +15271,10 @@ var ChangeStreamSyncManager = class {
14981
15271
  this.closingStreamPromise = null;
14982
15272
  this.changeQueue = Promise.resolve();
14983
15273
  this.running = false;
15274
+ this.stopping = false;
15275
+ this.stopPromise = null;
14984
15276
  this.fatalSyncError = null;
15277
+ this.generation = 0;
14985
15278
  this.stats = {
14986
15279
  eventCount: 0,
14987
15280
  syncedCount: 0,
@@ -15014,40 +15307,62 @@ var ChangeStreamSyncManager = class {
15014
15307
  * @since v1.0.9
15015
15308
  */
15016
15309
  async start() {
15310
+ if (this.stopPromise) await this.stopPromise;
15017
15311
  if (this.running || !this.config.enabled) return;
15018
15312
  if (this.startPromise) return this.startPromise;
15313
+ const generation = ++this.generation;
15019
15314
  const startup = (async () => {
15020
15315
  await this.closingStreamPromise;
15316
+ await this.changeQueue.catch(() => void 0);
15317
+ this.assertCurrentGeneration(generation, true);
15021
15318
  if (this.running) return;
15022
15319
  this.fatalSyncError = null;
15023
- await this.validateEnvironment();
15024
- await this.initializeTargets();
15025
- const resumeAfter = await this.tokenStore.load();
15026
- const options = { fullDocument: "updateLookup" };
15027
- if (resumeAfter) options.resumeAfter = resumeAfter;
15028
- const stream = this.db.watch(this.buildPipeline(), options);
15029
- stream.on("change", (event) => this.enqueueChange(event));
15030
- stream.on("error", (error) => {
15031
- const normalized = normalizeError(error);
15032
- this.stats.errorCount += 1;
15033
- this.stats.lastError = normalized;
15034
- this.logger?.error?.("[Sync] change stream error", normalized);
15035
- });
15036
- stream.on("close", () => {
15037
- if (!this.running || this.changeStream !== stream) {
15038
- this.logger?.debug?.("[Sync] change stream closed");
15039
- return;
15040
- }
15041
- const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
15042
- this.running = false;
15043
- this.changeStream = null;
15044
- this.stats.errorCount += 1;
15045
- this.stats.lastError = error;
15046
- this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
15047
- });
15048
- this.changeStream = stream;
15049
- this.running = true;
15050
- this.stats.startTime = /* @__PURE__ */ new Date();
15320
+ await this.closeTargets();
15321
+ let stagedTargets = [];
15322
+ let stream = null;
15323
+ try {
15324
+ await this.validateEnvironment();
15325
+ this.assertCurrentGeneration(generation);
15326
+ stagedTargets = await this.initializeTargets();
15327
+ this.assertCurrentGeneration(generation);
15328
+ const resumeAfter = await this.tokenStore.load();
15329
+ this.assertCurrentGeneration(generation);
15330
+ const options = { fullDocument: "updateLookup" };
15331
+ if (resumeAfter) options.resumeAfter = resumeAfter;
15332
+ stream = this.db.watch(this.buildPipeline(), options);
15333
+ const activeStream = stream;
15334
+ activeStream.on("change", (event) => this.enqueueChange(event, generation));
15335
+ activeStream.on("error", (error) => {
15336
+ if (generation !== this.generation) return;
15337
+ const normalized = normalizeError(error);
15338
+ this.stats.errorCount += 1;
15339
+ this.stats.lastError = normalized;
15340
+ this.logger?.error?.("[Sync] change stream error", normalized);
15341
+ this.stopAfterFatalSyncError(normalized, generation);
15342
+ });
15343
+ activeStream.on("close", () => {
15344
+ if (generation !== this.generation || !this.running || this.changeStream !== activeStream) {
15345
+ this.logger?.debug?.("[Sync] change stream closed");
15346
+ return;
15347
+ }
15348
+ const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
15349
+ this.stats.errorCount += 1;
15350
+ this.stats.lastError = error;
15351
+ this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
15352
+ this.stopAfterFatalSyncError(error, generation);
15353
+ });
15354
+ this.assertCurrentGeneration(generation);
15355
+ this.targets.push(...stagedTargets);
15356
+ stagedTargets = [];
15357
+ this.changeStream = activeStream;
15358
+ stream = null;
15359
+ this.running = true;
15360
+ this.stats.startTime = /* @__PURE__ */ new Date();
15361
+ } catch (error) {
15362
+ if (stream) await this.closeChangeStream(stream).catch(() => void 0);
15363
+ for (const target of stagedTargets.reverse()) await target.close().catch(() => void 0);
15364
+ throw error;
15365
+ }
15051
15366
  })();
15052
15367
  this.startPromise = startup;
15053
15368
  try {
@@ -15061,16 +15376,37 @@ var ChangeStreamSyncManager = class {
15061
15376
  * @since v1.0.9
15062
15377
  */
15063
15378
  async stop() {
15064
- this.running = false;
15065
- await this.startPromise;
15066
- const stream = this.changeStream;
15067
- this.changeStream = null;
15068
- if (stream) await this.closeChangeStream(stream);
15069
- else await this.closingStreamPromise;
15070
- await this.changeQueue.catch(() => void 0);
15379
+ if (this.stopPromise) return this.stopPromise;
15380
+ this.stopping = true;
15381
+ const cleanup = (async () => {
15382
+ if (this.startPromise && !this.running) this.generation += 1;
15383
+ await this.startPromise?.catch(() => void 0);
15384
+ await this.changeQueue.catch(() => void 0);
15385
+ this.generation += 1;
15386
+ this.running = false;
15387
+ const stream = this.changeStream;
15388
+ this.changeStream = null;
15389
+ if (stream) await this.closeChangeStream(stream);
15390
+ else await this.closingStreamPromise;
15391
+ await this.closeTargets();
15392
+ })();
15393
+ this.stopPromise = cleanup;
15394
+ try {
15395
+ await cleanup;
15396
+ } finally {
15397
+ if (this.stopPromise === cleanup) this.stopPromise = null;
15398
+ this.stopping = false;
15399
+ }
15400
+ }
15401
+ assertCurrentGeneration(generation, allowPreviousFatal = false) {
15402
+ if (generation !== this.generation) {
15403
+ throw createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] startup was superseded by stop or restart");
15404
+ }
15405
+ if (!allowPreviousFatal && this.fatalSyncError) throw this.fatalSyncError;
15406
+ }
15407
+ async closeTargets() {
15071
15408
  while (this.targets.length > 0) {
15072
- const target = this.targets.pop();
15073
- await target?.close();
15409
+ await this.targets.pop()?.close();
15074
15410
  }
15075
15411
  }
15076
15412
  /**
@@ -15135,11 +15471,13 @@ var ChangeStreamSyncManager = class {
15135
15471
  return pipeline;
15136
15472
  }
15137
15473
  async initializeTargets() {
15138
- if (this.targets.length > 0) {
15139
- return;
15140
- }
15141
- for (const target of this.config.targets) {
15142
- this.targets.push(await this.resolveTarget(target));
15474
+ const staged = [];
15475
+ try {
15476
+ for (const target of this.config.targets) staged.push(await this.resolveTarget(target));
15477
+ return staged;
15478
+ } catch (error) {
15479
+ for (const target of staged.reverse()) await target.close().catch(() => void 0);
15480
+ throw error;
15143
15481
  }
15144
15482
  }
15145
15483
  async resolveTarget(target) {
@@ -15176,14 +15514,15 @@ var ChangeStreamSyncManager = class {
15176
15514
  const client = await this.clientFactory(target.uri, target.options);
15177
15515
  return createMongoTarget(target.name, collections, client, target.databaseName ?? this.db.databaseName, true);
15178
15516
  }
15179
- async handleChange(event) {
15180
- if (this.fatalSyncError) {
15517
+ async handleChange(event, generation = this.generation) {
15518
+ if (generation !== this.generation || !this.running || this.fatalSyncError) {
15181
15519
  this.logger?.warn?.("[Sync] skipping queued change after fatal sync error", this.fatalSyncError);
15182
15520
  return;
15183
15521
  }
15184
15522
  this.stats.eventCount += 1;
15185
15523
  this.stats.lastEventTime = /* @__PURE__ */ new Date();
15186
15524
  if (this.config.filter && !this.config.filter(event)) {
15525
+ await this.saveProcessedToken(event, generation);
15187
15526
  return;
15188
15527
  }
15189
15528
  let document = event.fullDocument;
@@ -15195,6 +15534,7 @@ var ChangeStreamSyncManager = class {
15195
15534
  let duplicateTargets = 0;
15196
15535
  const targetErrors = [];
15197
15536
  for (const target of this.targets) {
15537
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15198
15538
  if (target.collections && !target.collections.has(event.ns.coll)) {
15199
15539
  continue;
15200
15540
  }
@@ -15211,11 +15551,13 @@ var ChangeStreamSyncManager = class {
15211
15551
  if (idempotencyKey && this.idempotency.markMode === "start") {
15212
15552
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "start");
15213
15553
  }
15554
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15214
15555
  await target.apply(event, document, {
15215
15556
  targetName: target.name,
15216
15557
  ...idempotencyKey ? { idempotencyKey } : {}
15217
15558
  });
15218
15559
  if (idempotencyKey && this.idempotency.markMode === "success") {
15560
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15219
15561
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "success");
15220
15562
  }
15221
15563
  target.stats.syncCount += 1;
@@ -15236,26 +15578,30 @@ var ChangeStreamSyncManager = class {
15236
15578
  }
15237
15579
  }
15238
15580
  if (targetErrors.length > 0) {
15239
- this.stopAfterFatalSyncError(targetErrors[0]);
15581
+ this.stopAfterFatalSyncError(targetErrors[0], generation);
15240
15582
  return;
15241
15583
  }
15242
- if (eligibleTargets > 0 && succeeded === eligibleTargets) {
15243
- try {
15244
- await this.tokenStore.save(event._id);
15245
- } catch (error) {
15246
- const normalized = normalizeError(error);
15247
- this.stats.tokenSaveErrorCount += 1;
15248
- this.stats.lastTokenSaveError = normalized;
15249
- this.stats.lastError = normalized;
15250
- this.stopAfterFatalSyncError(normalized);
15251
- throw normalized;
15252
- }
15253
- this.stats.syncedCount += 1;
15584
+ if (succeeded === eligibleTargets) {
15585
+ await this.saveProcessedToken(event, generation);
15254
15586
  if (duplicateTargets > 0 && duplicateTargets === eligibleTargets) {
15255
15587
  this.stats.duplicateEventCount += 1;
15256
15588
  }
15257
15589
  }
15258
15590
  }
15591
+ async saveProcessedToken(event, generation) {
15592
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15593
+ try {
15594
+ await this.tokenStore.save(event._id);
15595
+ } catch (error) {
15596
+ const normalized = normalizeError(error);
15597
+ this.stats.tokenSaveErrorCount += 1;
15598
+ this.stats.lastTokenSaveError = normalized;
15599
+ this.stats.lastError = normalized;
15600
+ this.stopAfterFatalSyncError(normalized, generation);
15601
+ throw normalized;
15602
+ }
15603
+ if (generation === this.generation && !this.fatalSyncError && this.running) this.stats.syncedCount += 1;
15604
+ }
15259
15605
  buildTargetIdempotencyKey(targetName, event) {
15260
15606
  if (!this.idempotency.enabled) {
15261
15607
  return null;
@@ -15278,7 +15624,8 @@ var ChangeStreamSyncManager = class {
15278
15624
  ts: Date.now()
15279
15625
  }, this.idempotency.ttl));
15280
15626
  }
15281
- stopAfterFatalSyncError(error) {
15627
+ stopAfterFatalSyncError(error, generation) {
15628
+ if (generation !== this.generation) return;
15282
15629
  this.fatalSyncError = error;
15283
15630
  this.running = false;
15284
15631
  const stream = this.changeStream;
@@ -15296,12 +15643,14 @@ var ChangeStreamSyncManager = class {
15296
15643
  if (this.closingStreamPromise === closing) this.closingStreamPromise = null;
15297
15644
  });
15298
15645
  }
15299
- enqueueChange(event) {
15300
- this.changeQueue = this.changeQueue.then(() => this.handleChange(event)).catch((error) => {
15646
+ enqueueChange(event, generation) {
15647
+ if (generation !== this.generation || !this.running || this.stopping || this.fatalSyncError) return;
15648
+ this.changeQueue = this.changeQueue.then(() => this.handleChange(event, generation)).catch((error) => {
15301
15649
  const normalized = normalizeError(error);
15302
15650
  this.stats.errorCount += 1;
15303
15651
  this.stats.lastError = normalized;
15304
15652
  this.logger?.error?.("[Sync] change handling failed", normalized);
15653
+ this.stopAfterFatalSyncError(normalized, generation);
15305
15654
  });
15306
15655
  }
15307
15656
  };
@@ -15730,7 +16079,9 @@ var SSHTunnelSSH2 = class {
15730
16079
  passphrase,
15731
16080
  port = 22,
15732
16081
  readyTimeout = 2e4,
15733
- keepaliveInterval = 3e4
16082
+ keepaliveInterval = 3e4,
16083
+ hostHash,
16084
+ hostVerifier
15734
16085
  } = this._sshConfig;
15735
16086
  if (!host || !username) {
15736
16087
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
@@ -15739,6 +16090,8 @@ var SSHTunnelSSH2 = class {
15739
16090
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
15740
16091
  }
15741
16092
  const config = { host, port, username, readyTimeout, keepaliveInterval };
16093
+ if (hostHash !== void 0) config.hostHash = hostHash;
16094
+ if (hostVerifier !== void 0) config.hostVerifier = hostVerifier;
15742
16095
  if (password) {
15743
16096
  config.password = password;
15744
16097
  } else if (privateKey) {
@@ -15757,6 +16110,25 @@ var SSHTunnelSSH2 = class {
15757
16110
  return new Promise((resolve, reject) => {
15758
16111
  const ssh = new SshClient();
15759
16112
  let settled = false;
16113
+ const fail = (err) => {
16114
+ if (settled) {
16115
+ this.markDisconnected();
16116
+ return;
16117
+ }
16118
+ settled = true;
16119
+ this.isConnected = false;
16120
+ this.localPort = null;
16121
+ this.server = null;
16122
+ this.sshClient = null;
16123
+ for (const socket of this._activeSockets) socket.destroy();
16124
+ this._activeSockets.clear();
16125
+ void closeServer(server);
16126
+ try {
16127
+ ssh.end();
16128
+ } catch {
16129
+ }
16130
+ reject(err);
16131
+ };
15760
16132
  const server = net.createServer((socket) => {
15761
16133
  this._activeSockets.add(socket);
15762
16134
  socket.on("close", () => this._activeSockets.delete(socket));
@@ -15782,12 +16154,7 @@ var SSHTunnelSSH2 = class {
15782
16154
  }
15783
16155
  );
15784
16156
  });
15785
- server.on("error", (err) => {
15786
- if (!settled) {
15787
- settled = true;
15788
- reject(err);
15789
- }
15790
- });
16157
+ server.on("error", fail);
15791
16158
  server.listen(this._sshConfig.localPort ?? 0, "127.0.0.1", () => {
15792
16159
  this.server = server;
15793
16160
  const addr = server.address();
@@ -15802,25 +16169,25 @@ var SSHTunnelSSH2 = class {
15802
16169
  });
15803
16170
  ssh.on("error", (err) => {
15804
16171
  if (!settled) {
15805
- settled = true;
15806
- server.close();
15807
- reject(err);
16172
+ fail(err);
15808
16173
  return;
15809
16174
  }
15810
16175
  this.markDisconnected();
15811
16176
  });
15812
16177
  const handleDisconnect = () => {
15813
16178
  if (!settled) {
15814
- settled = true;
15815
- server.close();
15816
- reject(createError(ErrorCodes.CONNECTION_FAILED, "SSH connection closed before the tunnel became ready"));
16179
+ fail(createError(ErrorCodes.CONNECTION_FAILED, "SSH connection closed before the tunnel became ready"));
15817
16180
  return;
15818
16181
  }
15819
16182
  this.markDisconnected();
15820
16183
  };
15821
16184
  ssh.on("close", handleDisconnect);
15822
16185
  ssh.on("end", handleDisconnect);
15823
- ssh.connect(authConfig);
16186
+ try {
16187
+ ssh.connect(authConfig);
16188
+ } catch (err) {
16189
+ fail(err);
16190
+ }
15824
16191
  });
15825
16192
  });
15826
16193
  }
@@ -15851,6 +16218,7 @@ var SSHTunnelSSH2 = class {
15851
16218
  ssh?.end();
15852
16219
  }
15853
16220
  markDisconnected() {
16221
+ const ssh = this.sshClient;
15854
16222
  this.isConnected = false;
15855
16223
  this.localPort = null;
15856
16224
  this.sshClient = null;
@@ -15861,6 +16229,10 @@ var SSHTunnelSSH2 = class {
15861
16229
  }
15862
16230
  this._activeSockets.clear();
15863
16231
  void closeServer(server);
16232
+ try {
16233
+ ssh?.end();
16234
+ } catch {
16235
+ }
15864
16236
  }
15865
16237
  };
15866
16238
  function parseHostFromUri(uri) {
@@ -15947,12 +16319,17 @@ async function prepareSshTunnelConnectConfig(connectConfig, databaseName, logger
15947
16319
  const remotePort = Number(cfg["dstPort"] ?? rawCfg["remotePort"] ?? rawCfg["mongoPort"] ?? parsePortFromUri(uri));
15948
16320
  const tunnel = new SSHTunnelSSH2(sshCfg, remoteHost, remotePort, { name: databaseName });
15949
16321
  logger.info?.(`[SSH] Establishing tunnel to ${remoteHost}:${remotePort} via ${String(cfg["host"])}`);
15950
- await tunnel.connect();
15951
- logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
15952
- return {
15953
- tunnel,
15954
- connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
15955
- };
16322
+ try {
16323
+ await tunnel.connect();
16324
+ logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
16325
+ return {
16326
+ tunnel,
16327
+ connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
16328
+ };
16329
+ } catch (error) {
16330
+ await tunnel.close();
16331
+ throw error;
16332
+ }
15956
16333
  }
15957
16334
 
15958
16335
  // src/entry/runtime-schema-dsl.ts
@@ -15998,14 +16375,14 @@ async function cleanupFailedRuntimeConnection(resources, logger) {
15998
16375
  }
15999
16376
 
16000
16377
  // src/capabilities/function-cache/index.ts
16001
- var import_cache_hub3 = require("cache-hub");
16378
+ var import_cache_hub4 = require("cache-hub");
16002
16379
  var import_function_cache = require("cache-hub/function-cache");
16003
16380
  function isValidCache(cache) {
16004
16381
  return cache !== null && typeof cache === "object" && typeof cache.get === "function" && typeof cache.set === "function";
16005
16382
  }
16006
16383
  function resolveCacheSource(cacheOrDb) {
16007
16384
  if (cacheOrDb === void 0) {
16008
- return new import_cache_hub3.MemoryCache();
16385
+ return new import_cache_hub4.MemoryCache();
16009
16386
  }
16010
16387
  if (isValidCache(cacheOrDb)) {
16011
16388
  return cacheOrDb;
@@ -16091,7 +16468,7 @@ function withCache(fn, options = {}) {
16091
16468
  namespace: namespace ?? "fn",
16092
16469
  keyBuilder,
16093
16470
  condition,
16094
- cache: externalCache ?? new import_cache_hub3.MemoryCache(),
16471
+ cache: externalCache ?? new import_cache_hub4.MemoryCache(),
16095
16472
  enableStats
16096
16473
  });
16097
16474
  const baseStats = wrapped.stats.bind(wrapped);
@@ -16841,9 +17218,8 @@ var MonSQLizeRuntime = class {
16841
17218
  driverError = error instanceof Error ? error.message : String(error);
16842
17219
  }
16843
17220
  }
16844
- const cacheStats = this._cache.getStats?.();
16845
- const poolHealth = this._poolManager?.getHealthStatus();
16846
- const distributedStats = this._distributedInvalidator?.getStats() ?? null;
17221
+ const health = summarizeRuntimeHealth(this.options.cache, this._cache, this._poolManager, this._distributedInvalidator);
17222
+ const { cacheStats, poolHealth, distributedStats, cacheEnabled, distributedEnabled, cacheProbeStatus, distributedStatus, poolsStatus } = health;
16847
17223
  const connected = this._connected && driverConnected;
16848
17224
  return {
16849
17225
  status: connected ? "up" : "down",
@@ -16851,7 +17227,7 @@ var MonSQLizeRuntime = class {
16851
17227
  driver: { connected: driverConnected, ...driverError ? { error: driverError } : {} },
16852
17228
  defaults: this.getDefaults(),
16853
17229
  cache: {
16854
- enabled: true,
17230
+ enabled: cacheEnabled,
16855
17231
  stats: cacheStats,
16856
17232
  pools: poolHealth,
16857
17233
  distributed: distributedStats
@@ -16863,18 +17239,18 @@ var MonSQLizeRuntime = class {
16863
17239
  ...driverError ? { error: driverError } : {}
16864
17240
  },
16865
17241
  cache: {
16866
- status: "up",
16867
- enabled: true,
17242
+ status: cacheProbeStatus,
17243
+ enabled: cacheEnabled,
16868
17244
  hasStats: cacheStats !== void 0
16869
17245
  },
16870
17246
  distributedCacheInvalidator: {
16871
- status: distributedStats ? "up" : "unknown",
16872
- enabled: distributedStats !== null,
17247
+ status: distributedStatus,
17248
+ enabled: distributedEnabled,
16873
17249
  stats: distributedStats
16874
17250
  },
16875
17251
  pools: {
16876
- status: poolHealth ? "up" : "unknown",
16877
- enabled: poolHealth !== void 0,
17252
+ status: poolsStatus,
17253
+ enabled: this._poolManager !== null,
16878
17254
  health: poolHealth
16879
17255
  }
16880
17256
  },
@@ -16942,7 +17318,7 @@ var MonSQLizeRuntime = class {
16942
17318
  requireCompatDbInstance(this);
16943
17319
  return {
16944
17320
  collection: (collectionName) => this.scopedCollection(collectionName, { database: name }),
16945
- model: (modelName) => this.scopedModel(modelName, { database: name })
17321
+ model: (model) => this.scopedModel(model, { database: name })
16946
17322
  };
16947
17323
  }
16948
17324
  pool(poolName) {
@@ -16974,7 +17350,8 @@ var MonSQLizeRuntime = class {
16974
17350
  });
16975
17351
  }
16976
17352
  // Model accessors ---------------------------------------------------------
16977
- scopedModel(name, options = {}) {
17353
+ scopedModel(model, options = {}) {
17354
+ const name = typeof model === "string" ? model : model.collectionName;
16978
17355
  const dbInstance = requireCompatDbInstance(this);
16979
17356
  if (this._client) {
16980
17357
  return this.createModelInstance(name, options);
@@ -16998,7 +17375,8 @@ var MonSQLizeRuntime = class {
16998
17375
  schemaEngine: this._schemaDslEngine
16999
17376
  });
17000
17377
  }
17001
- model(name) {
17378
+ model(model) {
17379
+ const name = typeof model === "string" ? model : model.collectionName;
17002
17380
  if (this._client) {
17003
17381
  this.ensureConnected();
17004
17382
  return this.createModelInstance(name, {
@@ -17823,6 +18201,27 @@ function ejsonLine(value) {
17823
18201
  function ejsonManifest(value) {
17824
18202
  return import_mongodb11.BSON.EJSON.stringify(value, void 0, 2, { relaxed: false });
17825
18203
  }
18204
+ function manifestInteger(value, field, minimum) {
18205
+ const numeric = value instanceof import_mongodb11.Int32 ? value.valueOf() : import_mongodb11.Long.isLong(value) ? value.toBigInt() : value;
18206
+ if (typeof numeric === "bigint") {
18207
+ if (numeric < BigInt(minimum) || numeric > BigInt(Number.MAX_SAFE_INTEGER)) {
18208
+ throw new Error(`invalid backup ${field}`);
18209
+ }
18210
+ return Number(numeric);
18211
+ }
18212
+ if (typeof numeric !== "number" || !Number.isSafeInteger(numeric) || numeric < minimum) {
18213
+ throw new Error(`invalid backup ${field}`);
18214
+ }
18215
+ return numeric;
18216
+ }
18217
+ function decodeManifestControls(raw) {
18218
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid backup manifest");
18219
+ const manifest = raw;
18220
+ manifest.version = manifestInteger(manifest.version, "manifest", 1);
18221
+ manifest.entryCount = manifestInteger(manifest.entryCount, "entry count", 0);
18222
+ if (manifest.maxBytes !== void 0) manifest.maxBytes = manifestInteger(manifest.maxBytes, "max bytes", 1);
18223
+ return manifest;
18224
+ }
17826
18225
  function sanitizeName(value) {
17827
18226
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "data-task";
17828
18227
  }
@@ -17999,10 +18398,9 @@ async function createRestoreSafetyBackup(source, entries, beforeIndexes, targetH
17999
18398
  }
18000
18399
  async function readDataTaskBackup(ref) {
18001
18400
  try {
18002
- const manifest = import_mongodb11.BSON.EJSON.parse(await (0, import_promises2.readFile)(ref.manifestPath, "utf8"), { relaxed: true });
18401
+ const manifest = decodeManifestControls(import_mongodb11.BSON.EJSON.parse(await (0, import_promises2.readFile)(ref.manifestPath, "utf8"), { relaxed: false }));
18003
18402
  if (manifest.kind !== "monsqlize-data-task-backup" || manifest.version !== 1 || manifest.runId !== ref.runId) throw new Error("invalid backup manifest");
18004
18403
  if (manifest.compression !== "gzip" && manifest.compression !== "none") throw new Error("invalid backup compression");
18005
- if (!Number.isSafeInteger(manifest.entryCount) || manifest.entryCount < 0) throw new Error("invalid backup entry count");
18006
18404
  if (typeof manifest.checksum !== "string" || !/^[a-f0-9]{64}$/i.test(manifest.checksum)) throw new Error("invalid backup checksum");
18007
18405
  if (typeof manifest.dataFile !== "string" || manifest.dataFile.length === 0 || import_node_path4.default.isAbsolute(manifest.dataFile) || import_node_path4.default.basename(manifest.dataFile) !== manifest.dataFile) {
18008
18406
  throw new Error("backup data file must stay beside its manifest");
@@ -18333,6 +18731,7 @@ async function acquireDataTaskLease(host, options) {
18333
18731
  }
18334
18732
 
18335
18733
  // src/capabilities/data-tasks/job-restore.ts
18734
+ var import_mongodb12 = require("mongodb");
18336
18735
  async function findCurrent(collection, identity) {
18337
18736
  let chain = collection.find(identity);
18338
18737
  if (typeof chain.limit === "function") chain = chain.limit(2);
@@ -18353,13 +18752,20 @@ function restoreApproval(hashes) {
18353
18752
  };
18354
18753
  return { ...payload, token: hashDataTaskValue(payload) };
18355
18754
  }
18755
+ function comparableIndexKey(key) {
18756
+ return Object.fromEntries(Object.entries(key).map(([field, value]) => {
18757
+ if (value instanceof import_mongodb12.Int32) return [field, value.valueOf()];
18758
+ if (import_mongodb12.Long.isLong(value) && value.toBigInt() <= BigInt(Number.MAX_SAFE_INTEGER) && value.toBigInt() >= BigInt(Number.MIN_SAFE_INTEGER)) return [field, Number(value.toBigInt())];
18759
+ return [field, value];
18760
+ }));
18761
+ }
18356
18762
  function exactIndex(index, current) {
18357
- return classifyDataTaskIndexes([{ key: index.key, name: index.name, options: index.options }], current)[0]?.status === "existing";
18763
+ return classifyDataTaskIndexes([{ key: comparableIndexKey(index.key), name: index.name, options: index.options }], current)[0]?.status === "existing";
18358
18764
  }
18359
18765
  function pendingIndexMatches(index, current) {
18360
18766
  return current.filter((candidate) => {
18361
18767
  if (index.name && candidate.name !== index.name) return false;
18362
- return classifyDataTaskIndexes([{ key: index.key, options: index.options }], [candidate])[0]?.status === "existing";
18768
+ return classifyDataTaskIndexes([{ key: comparableIndexKey(index.key), options: index.options }], [candidate])[0]?.status === "existing";
18363
18769
  });
18364
18770
  }
18365
18771
  function plannedDocumentMatches(current, entry) {
@@ -18627,7 +19033,7 @@ async function restoreDataTaskPlan(plan, target, targetHint, lease) {
18627
19033
  await updateDataTaskBackup(safety, (manifest) => {
18628
19034
  manifest.pendingIndexes = [...manifest.pendingIndexes ?? [], pending];
18629
19035
  });
18630
- await collection.createIndex(index.key, { ...index.options, name: index.name });
19036
+ await collection.createIndex(comparableIndexKey(index.key), { ...index.options, name: index.name });
18631
19037
  if (!exactIndex(index, await collection.listIndexes())) {
18632
19038
  throw new DataTaskJobError("RESTORE_FAILED", `index "${index.name}" failed immediate readback.`, "restore");
18633
19039
  }
@@ -18799,6 +19205,7 @@ MonSQLize.PoolSelector = PoolSelector;
18799
19205
  MonSQLize.PoolStats = PoolStatsManager;
18800
19206
  MonSQLize.validatePoolConfig = validatePoolConfig;
18801
19207
  MonSQLize.validatePoolConfigSafe = validatePoolConfigSafe;
19208
+ MonSQLize.defineModel = defineModel;
18802
19209
  MonSQLize.Model = Model;
18803
19210
  MonSQLize.expr = expr;
18804
19211
  MonSQLize.createExpression = createExpression;