monsqlize 3.3.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") {
@@ -735,6 +727,40 @@ function serializeDocument(document) {
735
727
  }
736
728
 
737
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
+ }
738
764
  var VERSION_LOOKUP_OPTION_KEYS = [
739
765
  "session",
740
766
  "readConcern",
@@ -820,11 +846,14 @@ function mapModelSchemaValidationErrors(errors = []) {
820
846
  }
821
847
  function validateModelSchemaPayload(context, document, options, metadata = {}) {
822
848
  const shouldValidate = context.validateEnabled || options?.validate === true;
823
- if (!shouldValidate) {
849
+ if (!shouldValidate || options?.skipValidation) {
824
850
  return document;
825
851
  }
826
- if (options?.skipValidation) {
827
- 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
+ );
828
857
  }
829
858
  if (!context.schemaCache || !context.schemaValidateFn) {
830
859
  return document;
@@ -858,22 +887,31 @@ function validateModelSchemaPayload(context, document, options, metadata = {}) {
858
887
  }
859
888
  return normalized;
860
889
  }
890
+ function buildModelVisibleSoftDeleteCondition(config) {
891
+ return { [config.field]: config.type === "boolean" ? { $ne: true } : null };
892
+ }
861
893
  function applyModelSoftDeleteFilter(query, options, softDeleteConfig) {
862
894
  if (!softDeleteConfig?.enabled) {
863
895
  return query ?? {};
864
896
  }
865
897
  const resolvedOptions = options ?? {};
866
898
  const resolvedQuery = query ?? {};
867
- 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)) {
868
906
  return resolvedQuery;
869
907
  }
870
908
  if (resolvedOptions.withDeleted) {
871
909
  return resolvedQuery;
872
910
  }
873
911
  if (resolvedOptions.onlyDeleted) {
874
- return { ...resolvedQuery, [softDeleteConfig.field]: { $ne: null } };
912
+ return { ...resolvedQuery, [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : { $ne: null } };
875
913
  }
876
- return { ...resolvedQuery, [softDeleteConfig.field]: null };
914
+ return { ...resolvedQuery, ...buildModelVisibleSoftDeleteCondition(softDeleteConfig) };
877
915
  }
878
916
  function applyModelInsertTimestamps(document, timestampsConfig, nowFactory) {
879
917
  if (!timestampsConfig) {
@@ -1168,7 +1206,9 @@ function projectPopulatedDocument(document, select, nestedPaths) {
1168
1206
  return document;
1169
1207
  }
1170
1208
  const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
1171
- 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;
1172
1212
  }
1173
1213
  async function populateModelPath(context, docs, path5, traversalState) {
1174
1214
  const config = normalizePopulateConfig(path5);
@@ -1190,7 +1230,13 @@ async function populateModelPath(context, docs, path5, traversalState) {
1190
1230
  }
1191
1231
  const localValues = docs.map((doc) => getByPath(doc, relation.localField));
1192
1232
  const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
1193
- 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) {
1194
1240
  for (const doc of docs) {
1195
1241
  doc[config.path] = relation.single ? null : [];
1196
1242
  }
@@ -1210,36 +1256,63 @@ async function populateModelPath(context, docs, path5, traversalState) {
1210
1256
  ...effectiveSelect,
1211
1257
  relation.foreignField,
1212
1258
  "_id",
1213
- ...collectNestedLocalFields(relatedModel, nestedPaths)
1259
+ ...Object.keys(config.sort ?? {}),
1260
+ ...collectNestedLocalFields(relatedModel, nestedPaths),
1261
+ ...relatedModel?.softDeleteConfig?.enabled ? [relatedModel.softDeleteConfig.field] : []
1214
1262
  ])] : [];
1215
- const relatedDocs = await relatedCollection.find({
1216
- [relation.foreignField]: { $in: keys },
1217
- ...config.match ?? {}
1218
- }, effectiveSelect ? {
1219
- projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
1220
- } : void 0);
1221
- let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
1222
- if (nestedPaths.length > 0 && relatedModel) {
1223
- if (state.depth + 1 > state.maxDepth) {
1224
- 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
+ });
1225
1287
  }
1226
- hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1227
- depth: state.depth + 1,
1228
- maxDepth: state.maxDepth,
1229
- activeConfigs: state.activeConfigs
1230
- });
1231
- }
1232
- const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1233
- for (const [index, doc] of docs.entries()) {
1234
- let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1235
- if (config.skip) {
1236
- 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;
1237
1303
  }
1238
- if (config.limit !== void 0) {
1239
- matches = matches.slice(0, config.limit);
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)));
1308
+ }
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;
1240
1315
  }
1241
- const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1242
- doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1243
1316
  }
1244
1317
  return docs;
1245
1318
  } finally {
@@ -1440,12 +1513,28 @@ async function saveModelDocument(collection, document, options = {}) {
1440
1513
  document._id = result.insertedId;
1441
1514
  return document;
1442
1515
  }
1443
- async function removeModelDocument(collection, document) {
1516
+ async function removeModelDocument(collection, document, deleteOne = (filter) => collection.deleteOne(filter)) {
1444
1517
  if (document._id === void 0) {
1445
1518
  return false;
1446
1519
  }
1447
- const result = await collection.deleteOne({ _id: document._id });
1448
- 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;
1449
1538
  }
1450
1539
 
1451
1540
  // src/capabilities/model/model-soft-delete-helpers.ts
@@ -1453,7 +1542,53 @@ function deletedFilter(filter, softDeleteConfig) {
1453
1542
  if (!softDeleteConfig) {
1454
1543
  return filter ?? {};
1455
1544
  }
1456
- 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];
1457
1592
  }
1458
1593
  function findWithDeletedDocuments(context, query, options) {
1459
1594
  return new PopulatePromise(async (paths) => {
@@ -1496,7 +1631,7 @@ function restoreSoftDeletedDocuments(context, filter, options) {
1496
1631
  return Promise.resolve({ modifiedCount: 0 });
1497
1632
  }
1498
1633
  return context.collection.updateOne(
1499
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1634
+ deletedFilter(filter, softDeleteConfig),
1500
1635
  { $unset: { [softDeleteConfig.field]: 1 } },
1501
1636
  options
1502
1637
  );
@@ -1507,7 +1642,7 @@ function restoreManySoftDeletedDocuments(context, filter, options) {
1507
1642
  return Promise.resolve({ modifiedCount: 0 });
1508
1643
  }
1509
1644
  return context.collection.updateMany(
1510
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1645
+ deletedFilter(filter, softDeleteConfig),
1511
1646
  { $unset: { [softDeleteConfig.field]: 1 } },
1512
1647
  options
1513
1648
  );
@@ -3123,6 +3258,9 @@ var modelWriteSourceStorage = new import_node_async_hooks.AsyncLocalStorage();
3123
3258
  function isPlainObject2(value) {
3124
3259
  return value !== null && typeof value === "object" && !Array.isArray(value);
3125
3260
  }
3261
+ function ownValue(record, key) {
3262
+ return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
3263
+ }
3126
3264
  function invalidConfig(path5, reason) {
3127
3265
  throw createError(
3128
3266
  ErrorCodes.INVALID_OPERATION,
@@ -3143,19 +3281,19 @@ function normalizeRule(input, path5) {
3143
3281
  if (!isPlainObject2(input)) {
3144
3282
  invalidConfig(path5, "rule must be a string mode or an object");
3145
3283
  }
3146
- const mode = input.mode ?? DEFAULT_RULE.mode;
3284
+ const mode = ownValue(input, "mode") ?? DEFAULT_RULE.mode;
3147
3285
  if (!ALLOWED_MODES.has(mode)) {
3148
3286
  invalidConfig(`${path5}.mode`, 'must be "allow-both" or "model-only"');
3149
3287
  }
3150
- const raw = input.raw ?? DEFAULT_RULE.raw;
3288
+ const raw = ownValue(input, "raw") ?? DEFAULT_RULE.raw;
3151
3289
  if (!ALLOWED_RAW.has(raw)) {
3152
3290
  invalidConfig(`${path5}.raw`, 'must be "inherit", "allow", or "block"');
3153
3291
  }
3154
- const management = input.management ?? DEFAULT_RULE.management;
3292
+ const management = ownValue(input, "management") ?? DEFAULT_RULE.management;
3155
3293
  if (!ALLOWED_MANAGEMENT.has(management)) {
3156
3294
  invalidConfig(`${path5}.management`, 'must be "inherit", "allow", or "block"');
3157
3295
  }
3158
- const onViolation = input.onViolation ?? DEFAULT_RULE.onViolation;
3296
+ const onViolation = ownValue(input, "onViolation") ?? DEFAULT_RULE.onViolation;
3159
3297
  if (!ALLOWED_VIOLATION_ACTIONS.has(onViolation)) {
3160
3298
  invalidConfig(`${path5}.onViolation`, 'must be "throw" or "warn"');
3161
3299
  }
@@ -3171,15 +3309,15 @@ function validateWritePathPolicyConfig(options) {
3171
3309
  }
3172
3310
  function normalizeWritePathPolicy(options) {
3173
3311
  if (options === void 0) {
3174
- return { default: { ...DEFAULT_RULE }, namespaces: {}, enabled: false };
3312
+ return { default: { ...DEFAULT_RULE }, namespaces: /* @__PURE__ */ Object.create(null), enabled: false };
3175
3313
  }
3176
3314
  if (!isPlainObject2(options)) {
3177
3315
  invalidConfig("writePathPolicy", "must be an object");
3178
3316
  }
3179
3317
  const typedOptions = options;
3180
- const defaultRule = normalizeRule(typedOptions.default, "writePathPolicy.default");
3181
- const namespacesInput = options.namespaces;
3182
- const namespaces = {};
3318
+ const defaultRule = normalizeRule(ownValue(typedOptions, "default"), "writePathPolicy.default");
3319
+ const namespacesInput = ownValue(options, "namespaces");
3320
+ const namespaces = /* @__PURE__ */ Object.create(null);
3183
3321
  if (namespacesInput !== void 0) {
3184
3322
  if (!isPlainObject2(namespacesInput)) {
3185
3323
  invalidConfig("writePathPolicy.namespaces", "must be an object");
@@ -3227,8 +3365,8 @@ function resolveWritePathRule(policy, namespace) {
3227
3365
  if (candidate === "default") {
3228
3366
  return { key: "default", rule: effectivePolicy.default };
3229
3367
  }
3230
- const rule = effectivePolicy.namespaces[candidate];
3231
- if (rule) {
3368
+ if (Object.prototype.hasOwnProperty.call(effectivePolicy.namespaces, candidate)) {
3369
+ const rule = effectivePolicy.namespaces[candidate];
3232
3370
  return { key: candidate, rule };
3233
3371
  }
3234
3372
  }
@@ -3371,16 +3509,17 @@ var fallbackModelIndexTasks = /* @__PURE__ */ new Map();
3371
3509
  function toCompatDefinition(definition) {
3372
3510
  return definition;
3373
3511
  }
3374
- function stableIndexStringify(value) {
3512
+ function stableIndexStringify(value, mode = "options") {
3375
3513
  if (value instanceof Date) {
3376
3514
  return JSON.stringify(value.toISOString());
3377
3515
  }
3378
3516
  if (Array.isArray(value)) {
3379
- return `[${value.map((item) => stableIndexStringify(item)).join(",")}]`;
3517
+ return `[${value.map((item) => stableIndexStringify(item, mode)).join(",")}]`;
3380
3518
  }
3381
3519
  if (value && typeof value === "object") {
3382
- const entries = Object.entries(value).filter(([, current]) => current !== void 0).sort(([left], [right]) => left.localeCompare(right));
3383
- 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(",")}}`;
3384
3523
  }
3385
3524
  return JSON.stringify(value) ?? "undefined";
3386
3525
  }
@@ -3414,7 +3553,7 @@ function declaredOptionEntries(options) {
3414
3553
  });
3415
3554
  }
3416
3555
  function indexOptionsMatch(existing, declared) {
3417
- if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
3556
+ if (stableIndexStringify(getExistingIndexKey(existing), "key") !== stableIndexStringify(declared.key, "key")) {
3418
3557
  return false;
3419
3558
  }
3420
3559
  for (const [name, value] of declaredOptionEntries(declared.options)) {
@@ -3433,8 +3572,8 @@ function findExistingIndexByName(existingIndexes, name) {
3433
3572
  return existingIndexes.find((index) => index.name === name);
3434
3573
  }
3435
3574
  function findExistingIndexByKey(existingIndexes, key) {
3436
- const fingerprint = stableIndexStringify(key);
3437
- return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
3575
+ const fingerprint = stableIndexStringify(key, "key");
3576
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index), "key") === fingerprint);
3438
3577
  }
3439
3578
  function createIndexEnsureError(message, result, cause) {
3440
3579
  return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
@@ -4272,12 +4411,17 @@ function shouldConvertQueryField(autoConvert, path5, depth) {
4272
4411
  if (!config.enabled || depth > config.maxDepth) return false;
4273
4412
  return shouldConvertObjectIdPath(path5, config);
4274
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
+ }
4275
4419
  function normalizeQueryArray(value, autoConvert, path5, depth) {
4276
4420
  return value.map((item, index) => {
4277
4421
  if (typeof item === "string" && item.length === 24 && import_mongodb2.ObjectId.isValid(item) && shouldConvertQueryField(autoConvert, path5, depth)) {
4278
4422
  return new import_mongodb2.ObjectId(item);
4279
4423
  }
4280
- if (item && typeof item === "object" && !Array.isArray(item) && !(item instanceof import_mongodb2.ObjectId)) {
4424
+ if (isPlainQueryRecord(item)) {
4281
4425
  return normalizeQueryFilter(item, autoConvert, `${path5}[${index}]`, depth + 1);
4282
4426
  }
4283
4427
  return item;
@@ -4293,7 +4437,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4293
4437
  }
4294
4438
  for (const [key, value] of Object.entries(filter)) {
4295
4439
  if (key === "$and" || key === "$or" || key === "$nor") {
4296
- 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;
4297
4441
  continue;
4298
4442
  }
4299
4443
  const currentPath = fieldPath ? `${fieldPath}.${key}` : key;
@@ -4302,7 +4446,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4302
4446
  result[key] = shouldConvert ? new import_mongodb2.ObjectId(value) : value;
4303
4447
  } else if (Array.isArray(value)) {
4304
4448
  result[key] = shouldConvert ? normalizeQueryArray(value, autoConvert, currentPath, depth + 1) : value;
4305
- } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof import_mongodb2.ObjectId)) {
4449
+ } else if (isPlainQueryRecord(value)) {
4306
4450
  const nested = value;
4307
4451
  const hasOperators = Object.keys(nested).some((k) => k.startsWith("$"));
4308
4452
  if (hasOperators) {
@@ -4314,7 +4458,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4314
4458
  );
4315
4459
  } else if (shouldConvert && QUERY_OBJECTID_SCALAR_OPERATORS.has(op) && typeof opVal === "string" && opVal.length === 24 && import_mongodb2.ObjectId.isValid(opVal)) {
4316
4460
  nestedResult[op] = new import_mongodb2.ObjectId(opVal);
4317
- } else if (op === "$elemMatch" && opVal && typeof opVal === "object" && !Array.isArray(opVal)) {
4461
+ } else if (op === "$elemMatch" && isPlainQueryRecord(opVal)) {
4318
4462
  nestedResult[op] = normalizeQueryFilter(opVal, autoConvert, currentPath, depth + 1);
4319
4463
  } else {
4320
4464
  nestedResult[op] = opVal;
@@ -4488,36 +4632,29 @@ async function isCollectionCacheBarrierActive(queryCache, collection, defaults =
4488
4632
  buildCollectionCacheBarrierNamespaces(collection, defaults)
4489
4633
  );
4490
4634
  }
4491
- function normalizeCacheKeyValue(value) {
4492
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
4493
- return void 0;
4494
- }
4495
- if (value instanceof Date) {
4496
- return { $date: value.toISOString() };
4497
- }
4498
- if (value instanceof import_mongodb2.ObjectId) {
4499
- return { $oid: value.toHexString() };
4500
- }
4501
- if (value instanceof RegExp) {
4502
- return { $regex: value.source, $flags: value.flags };
4503
- }
4504
- if (Array.isArray(value)) {
4505
- return value.map((item) => normalizeCacheKeyValue(item));
4506
- }
4507
- if (value && typeof value === "object") {
4508
- const sorted = {};
4509
- for (const key of Object.keys(value).sort()) {
4510
- const normalized = normalizeCacheKeyValue(value[key]);
4511
- if (normalized !== void 0) {
4512
- sorted[key] = normalized;
4513
- }
4514
- }
4515
- return sorted;
4516
- }
4517
- return value;
4518
- }
4519
- function stableCacheKeyString(value) {
4520
- 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))}`;
4521
4658
  }
4522
4659
  function buildResultCacheKeyOptions(options = {}) {
4523
4660
  const keyOptions = {};
@@ -4638,7 +4775,7 @@ function buildFindCacheKey(collection, defaults = {}, query, options) {
4638
4775
  ...options ?? {}
4639
4776
  });
4640
4777
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4641
- return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4778
+ return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4642
4779
  }
4643
4780
  function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4644
4781
  const executeOptions = normalizeFindProjectionOptions({
@@ -4646,7 +4783,7 @@ function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4646
4783
  ...options ?? {}
4647
4784
  });
4648
4785
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4649
- return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4786
+ return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4650
4787
  }
4651
4788
  function buildCountCacheKey(collection, defaults = {}, query, options) {
4652
4789
  const merged = {
@@ -4656,7 +4793,7 @@ function buildCountCacheKey(collection, defaults = {}, query, options) {
4656
4793
  const { cache: _cache, ...keyOptions } = merged;
4657
4794
  void _cache;
4658
4795
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4659
- return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4796
+ return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4660
4797
  }
4661
4798
  function buildDistinctCacheKey(collection, defaults = {}, field, query, options) {
4662
4799
  const rawOptions = options ?? {};
@@ -4686,7 +4823,7 @@ function buildAggregateCacheKey(collection, defaults = {}, pipeline, options) {
4686
4823
  ...options ?? {}
4687
4824
  };
4688
4825
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4689
- return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4826
+ return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4690
4827
  }
4691
4828
  function buildFindOneByIdCacheKey(collection, defaults = {}, id, options) {
4692
4829
  const objectId = parseRequiredObjectId(id);
@@ -4701,6 +4838,8 @@ function buildFindByIdsCacheKey(collection, defaults = {}, ids, options) {
4701
4838
  function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4702
4839
  const ext = options;
4703
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;
4704
4843
  const payload = {
4705
4844
  query: normalizeQueryForCache(options.query, defaults),
4706
4845
  sort: normalizeSortShape(options.sort),
@@ -4722,9 +4861,10 @@ function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4722
4861
  hint: ext.hint,
4723
4862
  collation: ext.collation,
4724
4863
  batchSize: ext.batchSize,
4725
- options: options.options
4864
+ let: effectiveLet,
4865
+ options: nestedOptions
4726
4866
  };
4727
- const keyHash = hashPayload(payload);
4867
+ const keyHash = hashPayload(stableCacheKeyString(payload));
4728
4868
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4729
4869
  return { key: `findPage:${namespace}:${keyHash}`, keyHash };
4730
4870
  }
@@ -5071,29 +5211,30 @@ async function runStrictUpdateMany(context, filter, update, options) {
5071
5211
  if (rawOptions.upsert === true) {
5072
5212
  throw createError(ErrorCodes.INVALID_ARGUMENT, 'versionMode "strict" does not support upsert.');
5073
5213
  }
5074
- const docs = await context.collection.find(filter, {
5075
- ...buildModelVersionLookupOptions(rawOptions, {
5076
- _id: 1,
5077
- [context.versionConfig.field]: 1
5078
- })
5079
- });
5080
5214
  let matchedCount = 0;
5081
5215
  let modifiedCount = 0;
5082
5216
  const conflictedIds = [];
5083
- for (const doc of docs) {
5084
- const id = doc._id;
5085
- const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
5086
- const result = await context.collection.updateOne(
5087
- { _id: id, [context.versionConfig.field]: expectedVersion },
5088
- update,
5089
- options
5090
- );
5091
- if ((result?.matchedCount ?? 0) === 0) {
5092
- conflictedIds.push(id);
5093
- 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;
5094
5237
  }
5095
- matchedCount += result.matchedCount ?? 0;
5096
- modifiedCount += result.modifiedCount ?? 0;
5097
5238
  }
5098
5239
  return {
5099
5240
  acknowledged: true,
@@ -5102,7 +5243,9 @@ async function runStrictUpdateMany(context, filter, update, options) {
5102
5243
  upsertedCount: 0,
5103
5244
  upsertedId: null,
5104
5245
  conflictCount: conflictedIds.length,
5105
- conflictedIds
5246
+ conflictedIds,
5247
+ skippedCount: skippedIds.length,
5248
+ skippedIds
5106
5249
  };
5107
5250
  }
5108
5251
  async function runStrictUpdateBatch(context, filter, update, options) {
@@ -5125,7 +5268,7 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5125
5268
  retryAttempts: _retryAttempts,
5126
5269
  retryDelay: _retryDelay,
5127
5270
  onRetry: _onRetry,
5128
- sort = { _id: 1 },
5271
+ sort: _sort,
5129
5272
  ...driverOptions
5130
5273
  } = rawOptions;
5131
5274
  void _batchSize;
@@ -5134,38 +5277,42 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5134
5277
  void _retryAttempts;
5135
5278
  void _retryDelay;
5136
5279
  void _onRetry;
5137
- const docs = await context.collection.find(filter, {
5138
- ...buildModelVersionLookupOptions(driverOptions, {
5139
- _id: 1,
5140
- [context.versionConfig.field]: 1
5141
- }),
5142
- sort
5143
- });
5280
+ void _sort;
5281
+ const countOptions = buildModelVersionLookupOptions(driverOptions, {});
5282
+ delete countOptions.projection;
5283
+ const totalCount = await context.collection.count(filter, countOptions);
5144
5284
  const result = {
5145
5285
  acknowledged: true,
5146
- totalCount: docs.length,
5286
+ totalCount,
5147
5287
  matchedCount: 0,
5148
5288
  modifiedCount: 0,
5149
5289
  upsertedCount: 0,
5150
- batchCount: Math.ceil(docs.length / batchSize),
5290
+ batchCount: 0,
5151
5291
  errors: [],
5152
5292
  retries: [],
5153
5293
  conflictCount: 0,
5154
- conflictedIds: []
5294
+ conflictedIds: [],
5295
+ skippedCount: 0,
5296
+ skippedIds: []
5155
5297
  };
5156
- for (let offset = 0; offset < docs.length; offset += batchSize) {
5157
- const batch = docs.slice(offset, offset + batchSize);
5298
+ for await (const batch of iterateModelCandidateBatches(context, filter, driverOptions, batchSize)) {
5299
+ result.batchCount++;
5158
5300
  for (const doc of batch) {
5159
5301
  const id = doc._id;
5160
5302
  const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateBatch");
5161
5303
  const updateResult = await context.collection.updateOne(
5162
- { _id: id, [context.versionConfig.field]: expectedVersion },
5304
+ buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
5163
5305
  update,
5164
5306
  driverOptions
5165
5307
  );
5166
5308
  if ((updateResult?.matchedCount ?? 0) === 0) {
5167
- result.conflictCount = (result.conflictCount ?? 0) + 1;
5168
- 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
+ }
5169
5316
  continue;
5170
5317
  }
5171
5318
  result.matchedCount += updateResult.matchedCount ?? 0;
@@ -5173,15 +5320,16 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5173
5320
  }
5174
5321
  if (typeof onProgress === "function") {
5175
5322
  onProgress({
5176
- currentBatch: Math.floor(offset / batchSize) + 1,
5177
- totalBatches: result.batchCount,
5323
+ currentBatch: result.batchCount,
5324
+ totalBatches: Math.ceil(totalCount / batchSize),
5178
5325
  modified: result.modifiedCount,
5179
5326
  matched: result.matchedCount,
5180
- total: docs.length,
5181
- 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,
5182
5329
  errors: result.errors.length,
5183
5330
  retries: result.retries.length,
5184
- conflicts: result.conflictCount ?? 0
5331
+ conflicts: result.conflictCount ?? 0,
5332
+ skipped: result.skippedCount ?? 0
5185
5333
  });
5186
5334
  }
5187
5335
  }
@@ -5194,10 +5342,7 @@ function buildSoftDeletePatch(config, now) {
5194
5342
  return { [config.field]: config.type === "boolean" ? true : now };
5195
5343
  }
5196
5344
  function buildSoftDeleteFilter(filter, config) {
5197
- return {
5198
- ...filter ?? {},
5199
- ...config?.enabled ? { [config.field]: null } : {}
5200
- };
5345
+ return applyModelSoftDeleteFilter(filter, {}, config);
5201
5346
  }
5202
5347
  function applyModelIncrementVersion(field, increment, versionConfig) {
5203
5348
  if (!versionConfig?.enabled || field === void 0) {
@@ -5237,7 +5382,8 @@ async function orchestrateModelInsertOne(context, document, options) {
5237
5382
  payload = validateModelSchemaPayload({
5238
5383
  validateEnabled: context.validateEnabled,
5239
5384
  schemaCache: context.schemaCache,
5240
- schemaValidateFn: context.schemaValidateFn
5385
+ schemaValidateFn: context.schemaValidateFn,
5386
+ schemaError: context.schemaError
5241
5387
  }, payload, options);
5242
5388
  payload = applyModelInsertTimestamps(payload, context.timestampsConfig, () => context.nowDate());
5243
5389
  payload = applyModelInsertVersion(payload, context.versionConfig);
@@ -5271,7 +5417,8 @@ async function orchestrateModelInsertMany(context, documents, options) {
5271
5417
  doc = validateModelSchemaPayload({
5272
5418
  validateEnabled: context.validateEnabled,
5273
5419
  schemaCache: context.schemaCache,
5274
- schemaValidateFn: context.schemaValidateFn
5420
+ schemaValidateFn: context.schemaValidateFn,
5421
+ schemaError: context.schemaError
5275
5422
  }, doc, resolvedOptions, { index });
5276
5423
  doc = applyModelInsertTimestamps(doc, context.timestampsConfig, () => context.nowDate());
5277
5424
  doc = applyModelInsertVersion(doc, context.versionConfig);
@@ -5356,7 +5503,8 @@ async function orchestrateModelReplaceOne(context, filter, replacement, options)
5356
5503
  let nextReplacement = validateModelSchemaPayload({
5357
5504
  validateEnabled: context.validateEnabled,
5358
5505
  schemaCache: context.schemaCache,
5359
- schemaValidateFn: context.schemaValidateFn
5506
+ schemaValidateFn: context.schemaValidateFn,
5507
+ schemaError: context.schemaError
5360
5508
  }, replacement, options);
5361
5509
  nextReplacement = preserveModelReplaceCreatedAt(
5362
5510
  replacement,
@@ -5415,7 +5563,8 @@ async function orchestrateModelFindOneAndReplace(context, filter, replacement, o
5415
5563
  let nextReplacement = validateModelSchemaPayload({
5416
5564
  validateEnabled: context.validateEnabled,
5417
5565
  schemaCache: context.schemaCache,
5418
- schemaValidateFn: context.schemaValidateFn
5566
+ schemaValidateFn: context.schemaValidateFn,
5567
+ schemaError: context.schemaError
5419
5568
  }, replacement, options);
5420
5569
  nextReplacement = preserveModelReplaceCreatedAt(
5421
5570
  replacement,
@@ -5527,7 +5676,8 @@ async function orchestrateModelInsertBatch(context, docs, options) {
5527
5676
  record = validateModelSchemaPayload({
5528
5677
  validateEnabled: context.validateEnabled,
5529
5678
  schemaCache: context.schemaCache,
5530
- schemaValidateFn: context.schemaValidateFn
5679
+ schemaValidateFn: context.schemaValidateFn,
5680
+ schemaError: context.schemaError
5531
5681
  }, record, resolvedOptions, { index });
5532
5682
  record = applyModelInsertTimestamps(record, context.timestampsConfig, () => context.nowDate());
5533
5683
  record = applyModelInsertVersion(record, context.versionConfig);
@@ -5605,7 +5755,7 @@ async function orchestrateModelDeleteOne(context, filter, options) {
5605
5755
  let result;
5606
5756
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5607
5757
  result = await context.collection.updateOne(
5608
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5758
+ buildSoftDeleteFilter(filter, softDeleteConfig),
5609
5759
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5610
5760
  options
5611
5761
  );
@@ -5639,7 +5789,7 @@ async function orchestrateModelDeleteMany(context, filter, options) {
5639
5789
  let result;
5640
5790
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5641
5791
  result = await context.collection.updateMany(
5642
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5792
+ { $and: [filter ?? {}, buildModelVisibleSoftDeleteCondition(softDeleteConfig)] },
5643
5793
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5644
5794
  options
5645
5795
  );
@@ -5803,7 +5953,7 @@ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targ
5803
5953
  [inbound.localField]: { $in: targetValues }
5804
5954
  };
5805
5955
  const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
5806
- const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
5956
+ const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, buildModelVisibleSoftDeleteCondition(sourceSoftDelete)] } : baseQuery;
5807
5957
  try {
5808
5958
  const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
5809
5959
  if (!collection) {
@@ -5985,7 +6135,7 @@ async function executeProtectedRelationDelete(host, filter, options, force) {
5985
6135
  const deleteOptions = stripRelationUsageOptions(options);
5986
6136
  let target;
5987
6137
  try {
5988
- 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);
5989
6139
  const rawTarget = await host.collection.findOne(
5990
6140
  targetFilter,
5991
6141
  relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
@@ -6069,7 +6219,7 @@ var ModelInstance = class {
6069
6219
  this.softDeleteConfig = this._softDeleteConfig;
6070
6220
  this._versionConfig = resolveModelVersionConfig(options.definition);
6071
6221
  this._v1HooksFactory = resolveModelHooksFactory(options.definition);
6072
- scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6222
+ if (!this._schemaError) scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6073
6223
  runtime: this.runtime,
6074
6224
  dbName: options.dbName,
6075
6225
  poolName: options.poolName,
@@ -6129,46 +6279,6 @@ var ModelInstance = class {
6129
6279
  }
6130
6280
  return result;
6131
6281
  }
6132
- isVisibleBySoftDelete(doc, options) {
6133
- if (!doc || !this._softDeleteConfig?.enabled) {
6134
- return Boolean(doc);
6135
- }
6136
- const rawOptions = options ?? {};
6137
- if (rawOptions.withDeleted) {
6138
- return true;
6139
- }
6140
- const value = doc[this._softDeleteConfig.field];
6141
- if (rawOptions.onlyDeleted) {
6142
- return value !== void 0 && value !== null;
6143
- }
6144
- return value === void 0 || value === null;
6145
- }
6146
- filterVisibleBySoftDelete(docs, options) {
6147
- return docs.filter((doc) => this.isVisibleBySoftDelete(doc, options));
6148
- }
6149
- applySoftDeleteFindPageOptions(options) {
6150
- const rawOptions = options ?? {};
6151
- return {
6152
- ...rawOptions,
6153
- query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, this._softDeleteConfig)
6154
- };
6155
- }
6156
- applySoftDeleteAggregatePipeline(pipeline, options) {
6157
- if (!this._softDeleteConfig?.enabled) {
6158
- return pipeline ?? [];
6159
- }
6160
- const rawOptions = options ?? {};
6161
- if (rawOptions.withDeleted) {
6162
- return pipeline ?? [];
6163
- }
6164
- const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, this._softDeleteConfig);
6165
- const matchStage = { $match: softDeleteMatch };
6166
- const stages = [...pipeline ?? []];
6167
- if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
6168
- return [stages[0], matchStage, ...stages.slice(1)];
6169
- }
6170
- return [matchStage, ...stages];
6171
- }
6172
6282
  // ── public API ────────────────────────────────────────────────────────────────
6173
6283
  find(query, options) {
6174
6284
  return new PopulatePromise(async (paths) => {
@@ -6204,8 +6314,12 @@ var ModelInstance = class {
6204
6314
  }
6205
6315
  findOneById(id, options) {
6206
6316
  return new PopulatePromise(async (paths) => {
6207
- const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], (nextId, nextOptions) => this.collection.findOneById(nextId, nextOptions)) : await this.collection.findOneById(id, options);
6208
- 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);
6209
6323
  });
6210
6324
  }
6211
6325
  findById(id, options) {
@@ -6213,13 +6327,25 @@ var ModelInstance = class {
6213
6327
  }
6214
6328
  findByIds(ids, options) {
6215
6329
  return new PopulatePromise(async (paths) => {
6216
- const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], (nextIds, nextOptions) => this.collection.findByIds(nextIds, nextOptions)) : await this.collection.findByIds(ids, options);
6217
- 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);
6218
6344
  });
6219
6345
  }
6220
6346
  findPage(options) {
6221
6347
  return new PopulatePromise(async (paths) => {
6222
- const filteredOptions = this.applySoftDeleteFindPageOptions(options);
6348
+ const filteredOptions = applySoftDeleteFindPageOptions(options, this._softDeleteConfig);
6223
6349
  const result = this._v1HooksFactory ? await this.runV1HookedOperation("find", [filteredOptions], (nextOptions) => this.collection.findPage(nextOptions)) : await this.collection.findPage(filteredOptions);
6224
6350
  return {
6225
6351
  ...result,
@@ -6371,7 +6497,7 @@ var ModelInstance = class {
6371
6497
  return this.runV1HookedOperation("find", [key, filteredQuery, options], (nextKey, nextQuery, nextOptions) => this.collection.distinct(nextKey, nextQuery, nextOptions));
6372
6498
  }
6373
6499
  aggregate(pipeline, options) {
6374
- const filteredPipeline = this.applySoftDeleteAggregatePipeline(pipeline, options);
6500
+ const filteredPipeline = applySoftDeleteAggregatePipeline(pipeline, options, this._softDeleteConfig);
6375
6501
  return this.runV1HookedOperation("find", [filteredPipeline, options], (nextPipeline, nextOptions) => {
6376
6502
  const execute = () => this.collection.aggregate(nextPipeline, nextOptions);
6377
6503
  if (resolveAggregateWriteTarget(Array.isArray(nextPipeline) ? nextPipeline : [])) {
@@ -6500,13 +6626,14 @@ var ModelInstance = class {
6500
6626
  schemaValidationContext: {
6501
6627
  validateEnabled: this._validateEnabled,
6502
6628
  schemaCache: this._schemaCache,
6503
- schemaValidateFn: this._schemaValidateFn
6629
+ schemaValidateFn: this._schemaValidateFn,
6630
+ schemaError: this._schemaError
6504
6631
  },
6505
6632
  nowFactory: () => this.nowDate()
6506
6633
  }));
6507
6634
  }
6508
6635
  async removeDocument(document) {
6509
- return this.runModelWrite(() => removeModelDocument(this.collection, document));
6636
+ return this.runModelWrite(() => removeModelDocument(this.collection, document, (filter) => this.deleteOne(filter)));
6510
6637
  }
6511
6638
  applyDefaults(document) {
6512
6639
  return applyModelDefaults(this.definition, document);
@@ -6524,6 +6651,7 @@ var ModelInstance = class {
6524
6651
  validateEnabled: this._validateEnabled,
6525
6652
  schemaCache: this._schemaCache,
6526
6653
  schemaValidateFn: this._schemaValidateFn,
6654
+ schemaError: this._schemaError,
6527
6655
  hooksFactory: this._v1HooksFactory,
6528
6656
  runHook: (hookName, context) => this.runHook(hookName, context)
6529
6657
  };
@@ -6539,6 +6667,11 @@ var ModelInstance = class {
6539
6667
 
6540
6668
  // src/capabilities/transaction/index.ts
6541
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
+ }
6542
6675
  function toPublicTransactionStatus(state) {
6543
6676
  return state === "active" ? "started" : state;
6544
6677
  }
@@ -6639,6 +6772,7 @@ var Transaction = class {
6639
6772
  this.options = options;
6640
6773
  this.id = `tx_${(0, import_node_crypto3.randomBytes)(8).toString("hex")}`;
6641
6774
  this.state = "pending";
6775
+ this.acceptingWrites = false;
6642
6776
  this.startedAt = null;
6643
6777
  this.timeoutTimer = null;
6644
6778
  this.pendingInvalidations = /* @__PURE__ */ new Set();
@@ -6657,6 +6791,7 @@ var Transaction = class {
6657
6791
  }
6658
6792
  this.session.startTransaction(this.options.transactionOptions);
6659
6793
  this.state = "active";
6794
+ this.acceptingWrites = true;
6660
6795
  this.startedAt = Date.now();
6661
6796
  const timeout = this.options.timeout ?? 3e4;
6662
6797
  if (timeout > 0) {
@@ -6679,6 +6814,7 @@ var Transaction = class {
6679
6814
  if (this.state !== "active") {
6680
6815
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
6681
6816
  }
6817
+ this.acceptingWrites = false;
6682
6818
  if (typeof this.session.commitTransaction === "function") {
6683
6819
  await commitTransactionWithRetry(this.session, this.options.logger);
6684
6820
  }
@@ -6704,6 +6840,7 @@ var Transaction = class {
6704
6840
  * @since v1.4.0
6705
6841
  */
6706
6842
  async abort() {
6843
+ this.acceptingWrites = false;
6707
6844
  if (this.state !== "pending" && this.state !== "active") {
6708
6845
  return;
6709
6846
  }
@@ -6728,10 +6865,16 @@ var Transaction = class {
6728
6865
  * @since v1.4.0
6729
6866
  */
6730
6867
  async end() {
6868
+ this.acceptingWrites = false;
6731
6869
  this.clearTimeout();
6732
6870
  this.options.lockManager?.releaseLocks(this.id);
6733
6871
  await this.session.endSession();
6734
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
+ }
6735
6878
  /**
6736
6879
  * Record a cache invalidation intent.
6737
6880
  * @since v1.4.0
@@ -6905,12 +7048,12 @@ var TransactionManager = class {
6905
7048
  return result;
6906
7049
  } catch (error) {
6907
7050
  lastError = error;
6908
- await transaction.abort();
6909
- this.recordStats(transaction, Date.now() - startedAt, false);
6910
7051
  if (transaction.state === "committed") {
6911
7052
  throw error;
6912
7053
  }
6913
- 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)) {
6914
7057
  throw error;
6915
7058
  }
6916
7059
  await sleep(retryDelay * Math.pow(retryBackoff, attempt));
@@ -7011,22 +7154,34 @@ function stringifySessionId(id) {
7011
7154
  }
7012
7155
  return String(id);
7013
7156
  }
7014
- function isTransientTransactionError(error) {
7015
- if (!error || typeof error !== "object") {
7016
- return false;
7017
- }
7018
- const candidate = error;
7019
- if (typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("TransientTransactionError")) {
7020
- 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
+ }
7021
7176
  }
7022
- return candidate.code === 112 || candidate.code === 117;
7177
+ return false;
7023
7178
  }
7024
7179
  function isUnknownTransactionCommitResult(error) {
7025
- if (!error || typeof error !== "object") {
7026
- return false;
7027
- }
7028
- const candidate = error;
7029
- 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");
7030
7185
  }
7031
7186
  async function commitTransactionWithRetry(session, logger) {
7032
7187
  const maxAttempts = 3;
@@ -7542,26 +7697,27 @@ var CountQueue = class {
7542
7697
  * @returns The result of fn
7543
7698
  * @throws A controlled error when the queue is full or the wait times out
7544
7699
  */
7545
- async execute(fn) {
7700
+ async execute(fn, options = {}) {
7546
7701
  const startTime = Date.now();
7702
+ const signal = options.signal;
7703
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7547
7704
  if (this.running >= this.concurrency) {
7548
7705
  if (this.queue.length >= this.maxQueueSize) {
7549
7706
  this.stats.rejected += 1;
7550
7707
  throw createError(ErrorCodes.INVALID_OPERATION, `Count queue is full (${this.maxQueueSize})`);
7551
7708
  }
7552
7709
  this.stats.queued += 1;
7553
- await this._waitInQueue(startTime);
7710
+ await this._waitInQueue(startTime, signal);
7554
7711
  }
7712
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7555
7713
  this.running += 1;
7556
7714
  this.stats.executed += 1;
7557
- try {
7558
- const elapsed = Date.now() - startTime;
7559
- const remainingMs = Math.max(1, this.timeout - elapsed);
7560
- return await this._executeWithTimeout(fn, remainingMs);
7561
- } finally {
7715
+ const elapsed = Date.now() - startTime;
7716
+ const remainingMs = Math.max(1, this.timeout - elapsed);
7717
+ return this._executeWithTimeout(fn, remainingMs, signal, () => {
7562
7718
  this.running -= 1;
7563
7719
  this._wakeNext();
7564
- }
7720
+ });
7565
7721
  }
7566
7722
  /**
7567
7723
  * Get queue statistics (including live state).
@@ -7596,17 +7752,28 @@ var CountQueue = class {
7596
7752
  const entry = this.queue.shift();
7597
7753
  if (entry) {
7598
7754
  clearTimeout(entry.timer);
7755
+ entry.cleanup();
7599
7756
  entry.reject(createError(ErrorCodes.INVALID_OPERATION, "Count queue was cleared before execution"));
7600
7757
  }
7601
7758
  }
7602
7759
  }
7603
- _waitInQueue(startTime) {
7760
+ _waitInQueue(startTime, signal) {
7604
7761
  return new Promise((resolve, reject) => {
7605
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
+ };
7606
7772
  const timer = setTimeout(() => {
7607
7773
  const index = this.queue.indexOf(entry);
7608
7774
  if (index !== -1) {
7609
7775
  this.queue.splice(index, 1);
7776
+ entry.cleanup();
7610
7777
  this.stats.timeout += 1;
7611
7778
  reject(createError(ErrorCodes.OPERATION_TIMEOUT, `Count queue wait timeout (${this.timeout}ms)`));
7612
7779
  }
@@ -7619,9 +7786,12 @@ var CountQueue = class {
7619
7786
  },
7620
7787
  reject,
7621
7788
  timer,
7622
- startTime
7789
+ startTime,
7790
+ cleanup: () => signal?.removeEventListener("abort", onAbort)
7623
7791
  };
7624
7792
  this.queue.push(entry);
7793
+ signal?.addEventListener("abort", onAbort, { once: true });
7794
+ if (signal?.aborted) onAbort();
7625
7795
  });
7626
7796
  }
7627
7797
  _wakeNext() {
@@ -7629,6 +7799,7 @@ var CountQueue = class {
7629
7799
  const entry = this.queue.shift();
7630
7800
  if (entry) {
7631
7801
  clearTimeout(entry.timer);
7802
+ entry.cleanup();
7632
7803
  entry.resolve();
7633
7804
  }
7634
7805
  }
@@ -7645,10 +7816,12 @@ var CountQueue = class {
7645
7816
  }
7646
7817
  return value;
7647
7818
  }
7648
- _executeWithTimeout(fn, timeoutMs = this.timeout) {
7819
+ _executeWithTimeout(fn, timeoutMs, signal, release) {
7649
7820
  let timer = null;
7650
7821
  const controller = new AbortController();
7651
- const timeoutPromise = new Promise((_, reject) => {
7822
+ let rejectCancellation;
7823
+ const cancellation = new Promise((_, reject) => {
7824
+ rejectCancellation = reject;
7652
7825
  timer = setTimeout(
7653
7826
  () => {
7654
7827
  controller.abort();
@@ -7658,14 +7831,22 @@ var CountQueue = class {
7658
7831
  timeoutMs
7659
7832
  );
7660
7833
  });
7834
+ const onAbort = () => {
7835
+ controller.abort();
7836
+ rejectCancellation(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
7837
+ };
7838
+ signal?.addEventListener("abort", onAbort, { once: true });
7661
7839
  let taskPromise;
7662
7840
  try {
7663
7841
  taskPromise = Promise.resolve(fn(controller.signal));
7664
7842
  } catch (error) {
7665
7843
  taskPromise = Promise.reject(error);
7666
7844
  }
7667
- return Promise.race([taskPromise, timeoutPromise]).finally(() => {
7845
+ void taskPromise.then(release, release);
7846
+ if (signal?.aborted) onAbort();
7847
+ return Promise.race([taskPromise, cancellation]).finally(() => {
7668
7848
  if (timer !== null) clearTimeout(timer);
7849
+ signal?.removeEventListener("abort", onAbort);
7669
7850
  });
7670
7851
  }
7671
7852
  _updateWaitTimeStats(waitTime) {
@@ -8194,6 +8375,9 @@ var ConnectionPoolManager = class {
8194
8375
  this._closed = false;
8195
8376
  this._configs = /* @__PURE__ */ new Map();
8196
8377
  this._pendingAdds = /* @__PURE__ */ new Set();
8378
+ this._pendingAddTasks = /* @__PURE__ */ new Set();
8379
+ this._generation = 0;
8380
+ this._closePromise = null;
8197
8381
  this.logger = options.logger ?? null;
8198
8382
  this.maxPoolsCount = options.maxPoolsCount ?? 10;
8199
8383
  this.strategy = options.poolStrategy ?? "auto";
@@ -8220,35 +8404,21 @@ var ConnectionPoolManager = class {
8220
8404
  */
8221
8405
  async addPool(config) {
8222
8406
  validatePoolConfig(config);
8407
+ if (this._closed) {
8408
+ throw createError(ErrorCodes.INVALID_OPERATION, "Connection pool manager is closed");
8409
+ }
8223
8410
  if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
8224
8411
  throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8225
8412
  }
8226
- if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
8413
+ if (this.maxPoolsCount > 0 && this.pools.size + this._pendingAdds.size >= this.maxPoolsCount) {
8227
8414
  throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
8228
8415
  }
8229
8416
  this._pendingAdds.add(config.name);
8417
+ const generation = this._generation;
8418
+ const task = this._finishAddPool(config, generation);
8419
+ this._pendingAddTasks.add(task);
8230
8420
  try {
8231
- const client = await this.clientFactory(config);
8232
- if (this.pools.has(config.name)) {
8233
- await client.close().catch(() => {
8234
- });
8235
- throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8236
- }
8237
- this.pools.set(config.name, {
8238
- client,
8239
- config,
8240
- createdAt: Date.now()
8241
- });
8242
- this.healthStatus.set(config.name, {
8243
- status: "up",
8244
- consecutiveFailures: 0,
8245
- lastCheckTime: null,
8246
- lastError: null,
8247
- uptime: 0
8248
- });
8249
- this.stats.set(config.name, createEmptyPoolStats(config.name));
8250
- this._configs.set(config.name, config);
8251
- this._healthChecker.register(config.name, config.healthCheck ?? {});
8421
+ await task;
8252
8422
  } catch (err) {
8253
8423
  const error = err;
8254
8424
  const msg = error.message ?? "";
@@ -8262,7 +8432,30 @@ var ConnectionPoolManager = class {
8262
8432
  throw err;
8263
8433
  } finally {
8264
8434
  this._pendingAdds.delete(config.name);
8435
+ this._pendingAddTasks.delete(task);
8436
+ }
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
+ );
8265
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 ?? {});
8266
8459
  }
8267
8460
  /**
8268
8461
  * Remove a connection pool.
@@ -8419,16 +8612,24 @@ var ConnectionPoolManager = class {
8419
8612
  * @since v1.0.8
8420
8613
  */
8421
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() {
8422
8622
  this.stopHealthCheck();
8423
8623
  this._healthChecker.stop();
8424
8624
  this._stats.close();
8625
+ await Promise.allSettled([...this._pendingAddTasks]);
8425
8626
  for (const pool of this.pools.values()) {
8426
8627
  await pool.client.close();
8427
8628
  }
8428
8629
  this.pools.clear();
8429
8630
  this.healthStatus.clear();
8430
8631
  this.stats.clear();
8431
- this._closed = true;
8632
+ this._configs.clear();
8432
8633
  }
8433
8634
  // ─── v1 compat methods ───────────────────────────────────────────────────────────
8434
8635
  /** v1 compat: get pool health Map */
@@ -8616,6 +8817,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
8616
8817
  logger
8617
8818
  });
8618
8819
  } catch (err) {
8820
+ if (err?.code === ErrorCodes.INVALID_CONFIG) throw err;
8619
8821
  logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
8620
8822
  return null;
8621
8823
  }
@@ -8694,6 +8896,33 @@ async function loadModelFiles(options, logger, opts = {}) {
8694
8896
  }
8695
8897
  }
8696
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
+
8697
8926
  // src/entry/runtime-defaults.ts
8698
8927
  function isPlainObject3(value) {
8699
8928
  if (value === null || typeof value !== "object") return false;
@@ -9344,36 +9573,36 @@ var FUNC_REGEX = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|S
9344
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;
9345
9574
  function compileInnerExpression(expression) {
9346
9575
  const expr2 = expression.trim();
9347
- const funcMatch = expr2.match(FUNC_REGEX);
9348
- if (funcMatch) {
9349
- return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
9350
- }
9351
- const andParts = splitTopLevel2(expr2, "&&");
9352
- if (andParts.length > 1) {
9353
- 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
+ );
9354
9595
  }
9355
- const orParts = splitTopLevel2(expr2, "||");
9596
+ const orParts = splitTopLevel2(expr2, "||", positions);
9356
9597
  if (orParts.length > 1) {
9357
- return { $or: orParts.map((part) => compileInnerExpression(part.trim())) };
9358
- }
9359
- const ternary = /^([^?]+)\s*\?\s*([^:]+)\s*:\s*(.+)$/.exec(expr2);
9360
- if (ternary) {
9361
- const [, condition, thenPart, elsePart] = ternary;
9362
- return {
9363
- $cond: {
9364
- if: compileInnerExpression(condition.trim()),
9365
- then: parseThenElse(thenPart.trim()),
9366
- else: parseThenElse(elsePart.trim())
9367
- }
9368
- };
9598
+ return { $or: orParts.map((part) => compileInnerExpression(part)) };
9369
9599
  }
9370
- const nullCoalParts = splitTopLevel2(expr2, "??");
9371
- if (nullCoalParts.length > 1) {
9372
- 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)) };
9373
9603
  }
9374
- const cmpMatch = /^(.+?)\s*(===|!==|>=|<=|>|<)\s*(.+)$/.exec(expr2);
9375
- if (cmpMatch) {
9376
- const [, left, operator, right] = cmpMatch;
9604
+ const comparison = findBinaryOperator(expr2, positions, ["===", "!==", ">=", "<=", ">", "<"]);
9605
+ if (comparison) {
9377
9606
  const operatorMap = {
9378
9607
  "===": "$eq",
9379
9608
  "!==": "$ne",
@@ -9382,24 +9611,39 @@ function compileInnerExpression(expression) {
9382
9611
  ">": "$gt",
9383
9612
  "<": "$lt"
9384
9613
  };
9385
- return {
9386
- [operatorMap[operator]]: [
9387
- compileInnerExpression(left.trim()),
9388
- compileInnerExpression(right.trim())
9389
- ]
9390
- };
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
+ ] };
9391
9633
  }
9392
- const addSubMatch = /^(.+?)\s*([+\-])\s*(.+)$/.exec(expr2);
9393
- if (addSubMatch) {
9394
- const [, left, operator, right] = addSubMatch;
9395
- const operatorMap = { "+": "$add", "-": "$subtract" };
9396
- return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
9634
+ if (expr2.startsWith("-")) {
9635
+ return { $multiply: [-1, compileInnerExpression(expr2.slice(1))] };
9397
9636
  }
9398
- const mulDivMatch = /^(.+?)\s*([*\/%])\s*(.+)$/.exec(expr2);
9399
- if (mulDivMatch) {
9400
- const [, left, operator, right] = mulDivMatch;
9401
- const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
9402
- 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
+ }
9403
9647
  }
9404
9648
  const genericFuncCallRe = /^[A-Za-z_][A-Za-z0-9_]*\s*\(.+\)$/;
9405
9649
  if (genericFuncCallRe.test(expr2)) {
@@ -9408,9 +9652,6 @@ function compileInnerExpression(expression) {
9408
9652
  }
9409
9653
  return parseValue(expr2);
9410
9654
  }
9411
- function parseThenElse(source) {
9412
- return source.includes("?") && source.includes(":") ? compileInnerExpression(source) : parseValue(source);
9413
- }
9414
9655
  function parseValue(value) {
9415
9656
  const normalized = stripOuterParentheses(value.trim());
9416
9657
  if (normalized.startsWith("'") && normalized.endsWith("'") || normalized.startsWith('"') && normalized.endsWith('"')) {
@@ -9420,18 +9661,22 @@ function parseValue(value) {
9420
9661
  if (normalized === "true") return true;
9421
9662
  if (normalized === "false") return false;
9422
9663
  if (!isNaN(Number(normalized)) && normalized !== "") return Number(normalized);
9423
- if (IS_FUNC_CALL_RE.test(normalized)) {
9664
+ if (normalized !== value.trim() || IS_FUNC_CALL_RE.test(normalized) || FUNC_REGEX.test(normalized)) {
9424
9665
  return compileInnerExpression(normalized);
9425
9666
  }
9426
- if (normalized.startsWith("$")) return normalized;
9427
- return `$${normalized}`;
9428
- }
9429
- function parseOperand(value) {
9430
- const normalized = stripOuterParentheses(value);
9431
- 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]))) {
9432
9676
  return compileInnerExpression(normalized);
9433
9677
  }
9434
- return parseValue(normalized);
9678
+ if (normalized.startsWith("$")) return normalized;
9679
+ return `$${normalized}`;
9435
9680
  }
9436
9681
  function dispatchFunction(name, argsStr) {
9437
9682
  const args = splitArgsStr(argsStr);
@@ -9733,72 +9978,82 @@ function compileMapExpression(exprStr, varName) {
9733
9978
  return compileInnerExpression(replaced);
9734
9979
  }
9735
9980
  function splitArgsStr(argsStr) {
9736
- const args = [];
9737
- let current = "";
9738
- let inString = false;
9739
- let stringChar = "";
9740
- let parenDepth = 0;
9741
- for (let index = 0; index < argsStr.length; index++) {
9742
- const ch = argsStr[index];
9743
- if ((ch === '"' || ch === "'") && (index === 0 || argsStr[index - 1] !== "\\")) {
9744
- if (!inString) {
9745
- inString = true;
9746
- stringChar = ch;
9747
- } else if (ch === stringChar) {
9748
- inString = false;
9749
- stringChar = "";
9750
- }
9751
- current += ch;
9752
- } else if (ch === "(" && !inString) {
9753
- parenDepth++;
9754
- current += ch;
9755
- } else if (ch === ")" && !inString) {
9756
- parenDepth--;
9757
- current += ch;
9758
- } else if (ch === "," && !inString && parenDepth === 0) {
9759
- args.push(current.trim());
9760
- current = "";
9761
- } else {
9762
- current += ch;
9763
- }
9764
- }
9765
- if (current.trim()) args.push(current.trim());
9766
- return args;
9981
+ if (!argsStr.trim()) return [];
9982
+ return splitTopLevel2(argsStr, ",");
9767
9983
  }
9768
- function splitTopLevel2(source, separator) {
9769
- const parts = [];
9770
- let current = "";
9771
- let parenDepth = 0;
9772
- let inString = false;
9773
- let stringChar = "";
9984
+ function topLevelPositions(source) {
9985
+ const positions = [];
9986
+ const stack = [];
9987
+ let quote = null;
9774
9988
  for (let index = 0; index < source.length; index++) {
9775
9989
  const ch = source[index];
9776
- if ((ch === '"' || ch === "'") && (index === 0 || source[index - 1] !== "\\")) {
9777
- if (!inString) {
9778
- inString = true;
9779
- stringChar = ch;
9780
- } else if (ch === stringChar) {
9781
- inString = false;
9782
- 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");
9783
10006
  }
9784
- current += ch;
9785
- } else if (!inString && ch === "(") {
9786
- parenDepth++;
9787
- current += ch;
9788
- } else if (!inString && ch === ")") {
9789
- parenDepth--;
9790
- current += ch;
9791
- } else if (!inString && parenDepth === 0 && source.startsWith(separator, index)) {
9792
- parts.push(current);
9793
- current = "";
9794
- index += separator.length - 1;
9795
- } else {
9796
- current += ch;
10007
+ continue;
9797
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}`);
9798
10028
  }
9799
- parts.push(current);
9800
10029
  return parts;
9801
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
+ }
9802
10057
  function stripOuterParentheses(source) {
9803
10058
  let normalized = source.trim();
9804
10059
  while (normalized.startsWith("(") && normalized.endsWith(")") && isWrappedByOuterParentheses(normalized)) {
@@ -9844,10 +10099,15 @@ function isExpressionObject(value) {
9844
10099
  value && typeof value === "object" && "__expr__" in value && typeof value.__expr__ === "string" && "__compiled__" in value && typeof value.__compiled__ === "boolean"
9845
10100
  );
9846
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
+ }
9847
10107
  function hasExpressionInObject(value) {
9848
10108
  if (isExpressionObject(value)) return true;
9849
- if (!value || typeof value !== "object") return false;
9850
10109
  if (Array.isArray(value)) return value.some((item) => hasExpressionInObject(item));
10110
+ if (!isPlainExpressionRecord(value)) return false;
9851
10111
  return Object.values(value).some((item) => hasExpressionInObject(item));
9852
10112
  }
9853
10113
  function hasExpressionInPipeline(pipeline) {
@@ -9864,7 +10124,7 @@ function transformExpressions(value, context) {
9864
10124
  if (Array.isArray(value)) {
9865
10125
  return value.map((item) => transformStageEntry(item));
9866
10126
  }
9867
- if (!value || typeof value !== "object") return value;
10127
+ if (!isPlainExpressionRecord(value)) return value;
9868
10128
  const entries = Object.entries(value);
9869
10129
  const result = {};
9870
10130
  for (const [key, current] of entries) {
@@ -9873,7 +10133,7 @@ function transformExpressions(value, context) {
9873
10133
  return result;
9874
10134
  }
9875
10135
  function transformStageEntry(stage) {
9876
- if (!stage || typeof stage !== "object" || Array.isArray(stage)) {
10136
+ if (!isPlainExpressionRecord(stage)) {
9877
10137
  return transformExpressions(stage, "project");
9878
10138
  }
9879
10139
  const entries = Object.entries(stage);
@@ -9897,7 +10157,7 @@ function transformStageValue(value, context) {
9897
10157
  if (Array.isArray(value)) {
9898
10158
  return value.map((item) => transformStageValue(item, context));
9899
10159
  }
9900
- if (!value || typeof value !== "object") return value;
10160
+ if (!isPlainExpressionRecord(value)) return value;
9901
10161
  const entries = Object.entries(value);
9902
10162
  const result = {};
9903
10163
  for (const [key, current] of entries) {
@@ -10828,6 +11088,7 @@ var AggregateChain = class {
10828
11088
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).explain(verbosity === true ? "queryPlanner" : verbosity);
10829
11089
  }
10830
11090
  stream() {
11091
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10831
11092
  const stream = this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).stream();
10832
11093
  const onWriteComplete = this.writePipelineHooks?.onWriteComplete;
10833
11094
  if (onWriteComplete) {
@@ -10845,6 +11106,7 @@ var AggregateChain = class {
10845
11106
  throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
10846
11107
  }
10847
11108
  this.executed = true;
11109
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10848
11110
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray().then(async (result) => {
10849
11111
  await this.writePipelineHooks?.onWriteComplete?.();
10850
11112
  return result;
@@ -11161,48 +11423,59 @@ function stripWriteCacheControlOptions(options) {
11161
11423
 
11162
11424
  // src/adapters/mongodb/writes/write-basic.ts
11163
11425
  async function insertOneDocument(collection, ...args) {
11426
+ assertManagedTransactionWriteAllowed(args[1]);
11164
11427
  return collection.insertOne(...args);
11165
11428
  }
11166
11429
  async function insertManyDocuments(collection, ...args) {
11430
+ assertManagedTransactionWriteAllowed(args[1]);
11167
11431
  return collection.insertMany(...args);
11168
11432
  }
11169
11433
  async function updateOneDocument(collection, ...args) {
11434
+ assertManagedTransactionWriteAllowed(args[2]);
11170
11435
  return collection.updateOne(...args);
11171
11436
  }
11172
11437
  async function updateManyDocuments(collection, ...args) {
11438
+ assertManagedTransactionWriteAllowed(args[2]);
11173
11439
  return collection.updateMany(...args);
11174
11440
  }
11175
11441
  async function replaceOneDocument(collection, ...args) {
11442
+ assertManagedTransactionWriteAllowed(args[2]);
11176
11443
  return collection.replaceOne(...args);
11177
11444
  }
11178
11445
  async function findOneAndUpdateDocument(collection, filter, update, options) {
11446
+ assertManagedTransactionWriteAllowed(options);
11179
11447
  if (options !== void 0) {
11180
11448
  return collection.findOneAndUpdate(filter, update, options);
11181
11449
  }
11182
11450
  return collection.findOneAndUpdate(filter, update);
11183
11451
  }
11184
11452
  async function findOneAndReplaceDocument(collection, filter, replacement, options) {
11453
+ assertManagedTransactionWriteAllowed(options);
11185
11454
  if (options !== void 0) {
11186
11455
  return collection.findOneAndReplace(filter, replacement, options);
11187
11456
  }
11188
11457
  return collection.findOneAndReplace(filter, replacement);
11189
11458
  }
11190
11459
  async function findOneAndDeleteDocument(collection, filter, options) {
11460
+ assertManagedTransactionWriteAllowed(options);
11191
11461
  if (options !== void 0) {
11192
11462
  return collection.findOneAndDelete(filter, options);
11193
11463
  }
11194
11464
  return collection.findOneAndDelete(filter);
11195
11465
  }
11196
11466
  async function upsertOneDocument(collection, filter, update, options = {}) {
11467
+ assertManagedTransactionWriteAllowed(options);
11197
11468
  return collection.updateOne(filter, update, {
11198
11469
  ...options,
11199
11470
  upsert: true
11200
11471
  });
11201
11472
  }
11202
11473
  async function deleteOneDocument(collection, ...args) {
11474
+ assertManagedTransactionWriteAllowed(args[1]);
11203
11475
  return collection.deleteOne(...args);
11204
11476
  }
11205
11477
  async function deleteManyDocuments(collection, ...args) {
11478
+ assertManagedTransactionWriteAllowed(args[1]);
11206
11479
  return collection.deleteMany(...args);
11207
11480
  }
11208
11481
  async function incrementOneDocument(collection, filter, field, incrementOrOptions, maybeOptions) {
@@ -11229,6 +11502,7 @@ async function incrementOneDocument(collection, filter, field, incrementOrOption
11229
11502
  includeResultMetadata: true
11230
11503
  };
11231
11504
  if (normalizedProjection) findOptions.projection = normalizedProjection;
11505
+ assertManagedTransactionWriteAllowed(findOptions);
11232
11506
  const rawResult = await collection.findOneAndUpdate(
11233
11507
  filter,
11234
11508
  updateDocument,
@@ -11361,14 +11635,25 @@ async function runWithConcurrency(items, concurrency, worker) {
11361
11635
  return;
11362
11636
  }
11363
11637
  let nextIndex = 0;
11638
+ let stopAcquiring = false;
11639
+ let firstFatal;
11364
11640
  const workerCount = Math.min(concurrency, items.length);
11365
11641
  await Promise.all(Array.from({ length: workerCount }, async () => {
11366
- while (nextIndex < items.length) {
11642
+ while (!stopAcquiring && nextIndex < items.length) {
11367
11643
  const item = items[nextIndex];
11368
11644
  nextIndex += 1;
11369
- 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
+ }
11370
11654
  }
11371
11655
  }));
11656
+ if (stopAcquiring) throw firstFatal;
11372
11657
  }
11373
11658
  async function insertBatchDocuments(collection, documents, options = {}) {
11374
11659
  if (!Array.isArray(documents)) {
@@ -11415,12 +11700,14 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11415
11700
  insertedIds: {},
11416
11701
  retries: []
11417
11702
  };
11703
+ assertManagedTransactionWriteAllowed(driverOptions);
11418
11704
  await runWithConcurrency(batchPlans, effectiveConcurrency, async ({ batchIndex, batch, batchOffset: batchOffset2 }) => {
11419
11705
  let pendingBatch = batch;
11420
11706
  let pendingIndexes = batch.map((_, index) => index);
11421
11707
  let attempts = 0;
11422
11708
  while (true) {
11423
11709
  try {
11710
+ assertManagedTransactionWriteAllowed(driverOptions);
11424
11711
  const batchResult = await collection.insertMany(pendingBatch, {
11425
11712
  ...driverOptions,
11426
11713
  ordered
@@ -11440,6 +11727,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11440
11727
  });
11441
11728
  break;
11442
11729
  } catch (cause) {
11730
+ assertManagedTransactionWriteAllowed(driverOptions);
11443
11731
  const errorRecord = {
11444
11732
  batchIndex,
11445
11733
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11511,6 +11799,10 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11511
11799
  });
11512
11800
  return result;
11513
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
+ }
11514
11806
  async function updateBatchDocuments(collection, filter, update, options = {}) {
11515
11807
  if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
11516
11808
  throw createError(ErrorCodes.INVALID_ARGUMENT, "filter must be a non-array object");
@@ -11554,6 +11846,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11554
11846
  "updateBatch retry requires an idempotent update; non-idempotent operators such as $inc/$push cannot be safely replayed"
11555
11847
  );
11556
11848
  }
11849
+ assertManagedTransactionWriteAllowed(driverOptions);
11557
11850
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11558
11851
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11559
11852
  const result = {
@@ -11570,8 +11863,9 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11570
11863
  let attempts = 0;
11571
11864
  while (true) {
11572
11865
  try {
11866
+ assertManagedTransactionWriteAllowed(driverOptions);
11573
11867
  const batchResult = await collection.updateMany(
11574
- { _id: { $in: batch } },
11868
+ batchWriteFilter(filter, batch),
11575
11869
  update,
11576
11870
  driverOptions
11577
11871
  );
@@ -11589,6 +11883,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11589
11883
  });
11590
11884
  break;
11591
11885
  } catch (cause) {
11886
+ assertManagedTransactionWriteAllowed(driverOptions);
11592
11887
  const errorRecord = {
11593
11888
  batchIndex,
11594
11889
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11651,6 +11946,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11651
11946
  if (!Number.isInteger(retryAttempts) || retryAttempts < 0) {
11652
11947
  throw createError(ErrorCodes.INVALID_ARGUMENT, "retryAttempts must be a non-negative integer");
11653
11948
  }
11949
+ assertManagedTransactionWriteAllowed(driverOptions);
11654
11950
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11655
11951
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11656
11952
  const result = {
@@ -11665,8 +11961,9 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11665
11961
  let attempts = 0;
11666
11962
  while (true) {
11667
11963
  try {
11964
+ assertManagedTransactionWriteAllowed(driverOptions);
11668
11965
  const batchResult = await collection.deleteMany(
11669
- { _id: { $in: batch } },
11966
+ batchWriteFilter(filter, batch),
11670
11967
  driverOptions
11671
11968
  );
11672
11969
  result.deletedCount += batchResult.deletedCount;
@@ -11681,6 +11978,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11681
11978
  });
11682
11979
  break;
11683
11980
  } catch (cause) {
11981
+ assertManagedTransactionWriteAllowed(driverOptions);
11684
11982
  const errorRecord = {
11685
11983
  batchIndex,
11686
11984
  message: cause instanceof Error ? cause.message : String(cause),
@@ -12209,9 +12507,11 @@ var MongoCollectionAccessor = class {
12209
12507
  const normalizedQuery = this._cvFilter(query);
12210
12508
  const maxTimeMS = this.management.defaults?.maxTimeMS;
12211
12509
  const merged = { ...maxTimeMS !== void 0 ? { maxTimeMS } : {}, ...options ?? {} };
12212
- 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;
12213
12511
  const { cache: _cache, ...keyOptions } = merged;
12214
12512
  void _cache;
12513
+ const callerSignal = keyOptions.signal;
12514
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12215
12515
  const executeCount = (signal) => countDocuments(
12216
12516
  this.collectionRef,
12217
12517
  normalizedQuery ?? {},
@@ -12222,15 +12522,16 @@ var MongoCollectionAccessor = class {
12222
12522
  const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
12223
12523
  const cached = await Promise.resolve(this.management.queryCache.get(cacheKey));
12224
12524
  if (cached !== void 0) {
12525
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12225
12526
  return Promise.resolve(wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, cached));
12226
12527
  }
12227
- const runner2 = countQueue ? countQueue.execute(executeCount) : executeCount();
12528
+ const runner2 = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12228
12529
  return runner2.then(async (result) => {
12229
12530
  await Promise.resolve(this.management.queryCache?.set(cacheKey, result, cacheTTL));
12230
12531
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result);
12231
12532
  });
12232
12533
  }
12233
- const runner = countQueue ? countQueue.execute(executeCount) : executeCount();
12534
+ const runner = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12234
12535
  return runner.then((result) => wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result));
12235
12536
  }
12236
12537
  /** Runs an aggregation pipeline and returns a chainable aggregate cursor. */
@@ -13519,20 +13820,21 @@ var DistributedCacheLockManager = class {
13519
13820
  }
13520
13821
  }
13521
13822
  async withLock(key, callback, options = {}) {
13823
+ let lock;
13522
13824
  try {
13523
- const lock = await this.acquireLock(key, options);
13524
- try {
13525
- return await callback();
13526
- } finally {
13527
- await lock.release().catch(() => {
13528
- });
13529
- }
13825
+ lock = await this.acquireLock(key, options);
13530
13826
  } catch (error) {
13531
13827
  if (this._isRedisConnectionError(error) && options.fallbackToNoLock) {
13532
13828
  return callback();
13533
13829
  }
13534
13830
  throw error;
13535
13831
  }
13832
+ try {
13833
+ return await callback();
13834
+ } finally {
13835
+ await lock.release().catch(() => {
13836
+ });
13837
+ }
13536
13838
  }
13537
13839
  async acquireLock(key, options = {}) {
13538
13840
  const ttl = options.ttl ?? 1e4;
@@ -14635,8 +14937,6 @@ var SlowQueryLogManager = class {
14635
14937
  };
14636
14938
 
14637
14939
  // src/capabilities/sync/index.ts
14638
- var import_promises = require("node:fs/promises");
14639
- var import_node_path2 = __toESM(require("node:path"));
14640
14940
  var import_mongodb7 = require("mongodb");
14641
14941
 
14642
14942
  // src/capabilities/sync/idempotency.ts
@@ -14739,25 +15039,19 @@ function validateSyncIdempotencyConfig(config) {
14739
15039
  }
14740
15040
  }
14741
15041
 
14742
- // 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"));
14743
15045
  function normalizeError(error) {
14744
15046
  return error instanceof Error ? error : new Error(String(error));
14745
15047
  }
14746
15048
  function delay2(ms) {
14747
- if (ms <= 0) {
14748
- return Promise.resolve();
14749
- }
15049
+ if (ms <= 0) return Promise.resolve();
14750
15050
  return new Promise((resolve) => {
14751
15051
  const timer = setTimeout(resolve, ms);
14752
15052
  timer.unref?.();
14753
15053
  });
14754
15054
  }
14755
- function normalizeCollectionFilter(collections) {
14756
- if (!collections?.length || collections.includes("*")) {
14757
- return null;
14758
- }
14759
- return new Set(collections);
14760
- }
14761
15055
  async function syncDirectory(directory) {
14762
15056
  try {
14763
15057
  const handle = await (0, import_promises.open)(directory, "r");
@@ -14769,35 +15063,6 @@ async function syncDirectory(directory) {
14769
15063
  } catch {
14770
15064
  }
14771
15065
  }
14772
- function validateTargetConfig(target, index) {
14773
- if (!target || typeof target !== "object") {
14774
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
14775
- }
14776
- if (typeof target.name !== "string" || target.name.trim() === "") {
14777
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
14778
- }
14779
- if (!target.apply && !target.uri && !target.pool) {
14780
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
14781
- }
14782
- if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
14783
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
14784
- }
14785
- if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
14786
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
14787
- }
14788
- if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
14789
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
14790
- }
14791
- if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
14792
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
14793
- }
14794
- if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14795
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
14796
- }
14797
- if (target.apply !== void 0 && typeof target.apply !== "function") {
14798
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
14799
- }
14800
- }
14801
15066
  function validateResumeTokenConfig(config) {
14802
15067
  if (!config || typeof config !== "object") {
14803
15068
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken must be an object.");
@@ -14828,39 +15093,6 @@ function validateResumeTokenConfig(config) {
14828
15093
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken.saveRetryDelayMs must be a non-negative integer.");
14829
15094
  }
14830
15095
  }
14831
- function validateSyncConfig(config) {
14832
- if (!config || typeof config !== "object") {
14833
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
14834
- }
14835
- if (typeof config.enabled !== "boolean") {
14836
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
14837
- }
14838
- if (!config.enabled) {
14839
- return;
14840
- }
14841
- if (!Array.isArray(config.targets) || config.targets.length === 0) {
14842
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
14843
- }
14844
- if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
14845
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
14846
- }
14847
- if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14848
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
14849
- }
14850
- if (config.filter !== void 0 && typeof config.filter !== "function") {
14851
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
14852
- }
14853
- if (config.transform !== void 0 && typeof config.transform !== "function") {
14854
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
14855
- }
14856
- validateSyncIdempotencyConfig(config.idempotency);
14857
- config.targets.forEach((target, index) => {
14858
- validateTargetConfig(target, index);
14859
- });
14860
- if (config.resumeToken) {
14861
- validateResumeTokenConfig(config.resumeToken);
14862
- }
14863
- }
14864
15096
  var ResumeTokenStore = class {
14865
15097
  constructor(options = {}) {
14866
15098
  this.storage = options.storage ?? "file";
@@ -14884,16 +15116,9 @@ var ResumeTokenStore = class {
14884
15116
  return JSON.parse(payload);
14885
15117
  } catch (error) {
14886
15118
  const code = error?.code;
14887
- if (code !== "ENOENT") {
14888
- this.logger?.warn?.("[Sync] failed to load resume token", error);
14889
- }
15119
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to load resume token", error);
14890
15120
  if (code !== "ENOENT" && this.strictLoad) {
14891
- throw createError(
14892
- ErrorCodes.DATABASE_ERROR,
14893
- "[Sync] failed to load resume token",
14894
- void 0,
14895
- normalizeError(error)
14896
- );
15121
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to load resume token", void 0, normalizeError(error));
14897
15122
  }
14898
15123
  return null;
14899
15124
  }
@@ -14919,12 +15144,7 @@ var ResumeTokenStore = class {
14919
15144
  }
14920
15145
  this.logger?.error?.("[Sync] failed to save resume token", lastError);
14921
15146
  if (this.strictSave) {
14922
- throw createError(
14923
- ErrorCodes.DATABASE_ERROR,
14924
- "[Sync] failed to save resume token",
14925
- void 0,
14926
- lastError ?? void 0
14927
- );
15147
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to save resume token", void 0, lastError ?? void 0);
14928
15148
  }
14929
15149
  }
14930
15150
  async writePayload(payload) {
@@ -14957,9 +15177,7 @@ var ResumeTokenStore = class {
14957
15177
  await (0, import_promises.copyFile)(this.path, `${this.path}.bak`);
14958
15178
  } catch (error) {
14959
15179
  const code = error?.code;
14960
- if (code !== "ENOENT") {
14961
- throw error;
14962
- }
15180
+ if (code !== "ENOENT") throw error;
14963
15181
  }
14964
15182
  }
14965
15183
  async clear() {
@@ -14971,12 +15189,80 @@ var ResumeTokenStore = class {
14971
15189
  await (0, import_promises.unlink)(this.path);
14972
15190
  } catch (error) {
14973
15191
  const code = error?.code;
14974
- if (code !== "ENOENT") {
14975
- this.logger?.warn?.("[Sync] failed to clear resume token", error);
14976
- }
15192
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to clear resume token", error);
14977
15193
  }
14978
15194
  }
14979
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
+ }
14980
15266
  var ChangeStreamSyncManager = class {
14981
15267
  constructor(options) {
14982
15268
  this.targets = [];
@@ -14985,7 +15271,10 @@ var ChangeStreamSyncManager = class {
14985
15271
  this.closingStreamPromise = null;
14986
15272
  this.changeQueue = Promise.resolve();
14987
15273
  this.running = false;
15274
+ this.stopping = false;
15275
+ this.stopPromise = null;
14988
15276
  this.fatalSyncError = null;
15277
+ this.generation = 0;
14989
15278
  this.stats = {
14990
15279
  eventCount: 0,
14991
15280
  syncedCount: 0,
@@ -15018,40 +15307,62 @@ var ChangeStreamSyncManager = class {
15018
15307
  * @since v1.0.9
15019
15308
  */
15020
15309
  async start() {
15310
+ if (this.stopPromise) await this.stopPromise;
15021
15311
  if (this.running || !this.config.enabled) return;
15022
15312
  if (this.startPromise) return this.startPromise;
15313
+ const generation = ++this.generation;
15023
15314
  const startup = (async () => {
15024
15315
  await this.closingStreamPromise;
15316
+ await this.changeQueue.catch(() => void 0);
15317
+ this.assertCurrentGeneration(generation, true);
15025
15318
  if (this.running) return;
15026
15319
  this.fatalSyncError = null;
15027
- await this.validateEnvironment();
15028
- await this.initializeTargets();
15029
- const resumeAfter = await this.tokenStore.load();
15030
- const options = { fullDocument: "updateLookup" };
15031
- if (resumeAfter) options.resumeAfter = resumeAfter;
15032
- const stream = this.db.watch(this.buildPipeline(), options);
15033
- stream.on("change", (event) => this.enqueueChange(event));
15034
- stream.on("error", (error) => {
15035
- const normalized = normalizeError(error);
15036
- this.stats.errorCount += 1;
15037
- this.stats.lastError = normalized;
15038
- this.logger?.error?.("[Sync] change stream error", normalized);
15039
- });
15040
- stream.on("close", () => {
15041
- if (!this.running || this.changeStream !== stream) {
15042
- this.logger?.debug?.("[Sync] change stream closed");
15043
- return;
15044
- }
15045
- const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
15046
- this.running = false;
15047
- this.changeStream = null;
15048
- this.stats.errorCount += 1;
15049
- this.stats.lastError = error;
15050
- this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
15051
- });
15052
- this.changeStream = stream;
15053
- this.running = true;
15054
- 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
+ }
15055
15366
  })();
15056
15367
  this.startPromise = startup;
15057
15368
  try {
@@ -15065,16 +15376,37 @@ var ChangeStreamSyncManager = class {
15065
15376
  * @since v1.0.9
15066
15377
  */
15067
15378
  async stop() {
15068
- this.running = false;
15069
- await this.startPromise;
15070
- const stream = this.changeStream;
15071
- this.changeStream = null;
15072
- if (stream) await this.closeChangeStream(stream);
15073
- else await this.closingStreamPromise;
15074
- 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() {
15075
15408
  while (this.targets.length > 0) {
15076
- const target = this.targets.pop();
15077
- await target?.close();
15409
+ await this.targets.pop()?.close();
15078
15410
  }
15079
15411
  }
15080
15412
  /**
@@ -15139,11 +15471,13 @@ var ChangeStreamSyncManager = class {
15139
15471
  return pipeline;
15140
15472
  }
15141
15473
  async initializeTargets() {
15142
- if (this.targets.length > 0) {
15143
- return;
15144
- }
15145
- for (const target of this.config.targets) {
15146
- 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;
15147
15481
  }
15148
15482
  }
15149
15483
  async resolveTarget(target) {
@@ -15180,14 +15514,15 @@ var ChangeStreamSyncManager = class {
15180
15514
  const client = await this.clientFactory(target.uri, target.options);
15181
15515
  return createMongoTarget(target.name, collections, client, target.databaseName ?? this.db.databaseName, true);
15182
15516
  }
15183
- async handleChange(event) {
15184
- if (this.fatalSyncError) {
15517
+ async handleChange(event, generation = this.generation) {
15518
+ if (generation !== this.generation || !this.running || this.fatalSyncError) {
15185
15519
  this.logger?.warn?.("[Sync] skipping queued change after fatal sync error", this.fatalSyncError);
15186
15520
  return;
15187
15521
  }
15188
15522
  this.stats.eventCount += 1;
15189
15523
  this.stats.lastEventTime = /* @__PURE__ */ new Date();
15190
15524
  if (this.config.filter && !this.config.filter(event)) {
15525
+ await this.saveProcessedToken(event, generation);
15191
15526
  return;
15192
15527
  }
15193
15528
  let document = event.fullDocument;
@@ -15199,6 +15534,7 @@ var ChangeStreamSyncManager = class {
15199
15534
  let duplicateTargets = 0;
15200
15535
  const targetErrors = [];
15201
15536
  for (const target of this.targets) {
15537
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15202
15538
  if (target.collections && !target.collections.has(event.ns.coll)) {
15203
15539
  continue;
15204
15540
  }
@@ -15215,11 +15551,13 @@ var ChangeStreamSyncManager = class {
15215
15551
  if (idempotencyKey && this.idempotency.markMode === "start") {
15216
15552
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "start");
15217
15553
  }
15554
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15218
15555
  await target.apply(event, document, {
15219
15556
  targetName: target.name,
15220
15557
  ...idempotencyKey ? { idempotencyKey } : {}
15221
15558
  });
15222
15559
  if (idempotencyKey && this.idempotency.markMode === "success") {
15560
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15223
15561
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "success");
15224
15562
  }
15225
15563
  target.stats.syncCount += 1;
@@ -15240,26 +15578,30 @@ var ChangeStreamSyncManager = class {
15240
15578
  }
15241
15579
  }
15242
15580
  if (targetErrors.length > 0) {
15243
- this.stopAfterFatalSyncError(targetErrors[0]);
15581
+ this.stopAfterFatalSyncError(targetErrors[0], generation);
15244
15582
  return;
15245
15583
  }
15246
- if (eligibleTargets > 0 && succeeded === eligibleTargets) {
15247
- try {
15248
- await this.tokenStore.save(event._id);
15249
- } catch (error) {
15250
- const normalized = normalizeError(error);
15251
- this.stats.tokenSaveErrorCount += 1;
15252
- this.stats.lastTokenSaveError = normalized;
15253
- this.stats.lastError = normalized;
15254
- this.stopAfterFatalSyncError(normalized);
15255
- throw normalized;
15256
- }
15257
- this.stats.syncedCount += 1;
15584
+ if (succeeded === eligibleTargets) {
15585
+ await this.saveProcessedToken(event, generation);
15258
15586
  if (duplicateTargets > 0 && duplicateTargets === eligibleTargets) {
15259
15587
  this.stats.duplicateEventCount += 1;
15260
15588
  }
15261
15589
  }
15262
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
+ }
15263
15605
  buildTargetIdempotencyKey(targetName, event) {
15264
15606
  if (!this.idempotency.enabled) {
15265
15607
  return null;
@@ -15282,7 +15624,8 @@ var ChangeStreamSyncManager = class {
15282
15624
  ts: Date.now()
15283
15625
  }, this.idempotency.ttl));
15284
15626
  }
15285
- stopAfterFatalSyncError(error) {
15627
+ stopAfterFatalSyncError(error, generation) {
15628
+ if (generation !== this.generation) return;
15286
15629
  this.fatalSyncError = error;
15287
15630
  this.running = false;
15288
15631
  const stream = this.changeStream;
@@ -15300,12 +15643,14 @@ var ChangeStreamSyncManager = class {
15300
15643
  if (this.closingStreamPromise === closing) this.closingStreamPromise = null;
15301
15644
  });
15302
15645
  }
15303
- enqueueChange(event) {
15304
- 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) => {
15305
15649
  const normalized = normalizeError(error);
15306
15650
  this.stats.errorCount += 1;
15307
15651
  this.stats.lastError = normalized;
15308
15652
  this.logger?.error?.("[Sync] change handling failed", normalized);
15653
+ this.stopAfterFatalSyncError(normalized, generation);
15309
15654
  });
15310
15655
  }
15311
15656
  };
@@ -15734,7 +16079,9 @@ var SSHTunnelSSH2 = class {
15734
16079
  passphrase,
15735
16080
  port = 22,
15736
16081
  readyTimeout = 2e4,
15737
- keepaliveInterval = 3e4
16082
+ keepaliveInterval = 3e4,
16083
+ hostHash,
16084
+ hostVerifier
15738
16085
  } = this._sshConfig;
15739
16086
  if (!host || !username) {
15740
16087
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
@@ -15743,6 +16090,8 @@ var SSHTunnelSSH2 = class {
15743
16090
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
15744
16091
  }
15745
16092
  const config = { host, port, username, readyTimeout, keepaliveInterval };
16093
+ if (hostHash !== void 0) config.hostHash = hostHash;
16094
+ if (hostVerifier !== void 0) config.hostVerifier = hostVerifier;
15746
16095
  if (password) {
15747
16096
  config.password = password;
15748
16097
  } else if (privateKey) {
@@ -15761,6 +16110,25 @@ var SSHTunnelSSH2 = class {
15761
16110
  return new Promise((resolve, reject) => {
15762
16111
  const ssh = new SshClient();
15763
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
+ };
15764
16132
  const server = net.createServer((socket) => {
15765
16133
  this._activeSockets.add(socket);
15766
16134
  socket.on("close", () => this._activeSockets.delete(socket));
@@ -15786,12 +16154,7 @@ var SSHTunnelSSH2 = class {
15786
16154
  }
15787
16155
  );
15788
16156
  });
15789
- server.on("error", (err) => {
15790
- if (!settled) {
15791
- settled = true;
15792
- reject(err);
15793
- }
15794
- });
16157
+ server.on("error", fail);
15795
16158
  server.listen(this._sshConfig.localPort ?? 0, "127.0.0.1", () => {
15796
16159
  this.server = server;
15797
16160
  const addr = server.address();
@@ -15806,25 +16169,25 @@ var SSHTunnelSSH2 = class {
15806
16169
  });
15807
16170
  ssh.on("error", (err) => {
15808
16171
  if (!settled) {
15809
- settled = true;
15810
- server.close();
15811
- reject(err);
16172
+ fail(err);
15812
16173
  return;
15813
16174
  }
15814
16175
  this.markDisconnected();
15815
16176
  });
15816
16177
  const handleDisconnect = () => {
15817
16178
  if (!settled) {
15818
- settled = true;
15819
- server.close();
15820
- 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"));
15821
16180
  return;
15822
16181
  }
15823
16182
  this.markDisconnected();
15824
16183
  };
15825
16184
  ssh.on("close", handleDisconnect);
15826
16185
  ssh.on("end", handleDisconnect);
15827
- ssh.connect(authConfig);
16186
+ try {
16187
+ ssh.connect(authConfig);
16188
+ } catch (err) {
16189
+ fail(err);
16190
+ }
15828
16191
  });
15829
16192
  });
15830
16193
  }
@@ -15855,6 +16218,7 @@ var SSHTunnelSSH2 = class {
15855
16218
  ssh?.end();
15856
16219
  }
15857
16220
  markDisconnected() {
16221
+ const ssh = this.sshClient;
15858
16222
  this.isConnected = false;
15859
16223
  this.localPort = null;
15860
16224
  this.sshClient = null;
@@ -15865,6 +16229,10 @@ var SSHTunnelSSH2 = class {
15865
16229
  }
15866
16230
  this._activeSockets.clear();
15867
16231
  void closeServer(server);
16232
+ try {
16233
+ ssh?.end();
16234
+ } catch {
16235
+ }
15868
16236
  }
15869
16237
  };
15870
16238
  function parseHostFromUri(uri) {
@@ -15951,12 +16319,17 @@ async function prepareSshTunnelConnectConfig(connectConfig, databaseName, logger
15951
16319
  const remotePort = Number(cfg["dstPort"] ?? rawCfg["remotePort"] ?? rawCfg["mongoPort"] ?? parsePortFromUri(uri));
15952
16320
  const tunnel = new SSHTunnelSSH2(sshCfg, remoteHost, remotePort, { name: databaseName });
15953
16321
  logger.info?.(`[SSH] Establishing tunnel to ${remoteHost}:${remotePort} via ${String(cfg["host"])}`);
15954
- await tunnel.connect();
15955
- logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
15956
- return {
15957
- tunnel,
15958
- connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
15959
- };
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
+ }
15960
16333
  }
15961
16334
 
15962
16335
  // src/entry/runtime-schema-dsl.ts
@@ -16002,14 +16375,14 @@ async function cleanupFailedRuntimeConnection(resources, logger) {
16002
16375
  }
16003
16376
 
16004
16377
  // src/capabilities/function-cache/index.ts
16005
- var import_cache_hub3 = require("cache-hub");
16378
+ var import_cache_hub4 = require("cache-hub");
16006
16379
  var import_function_cache = require("cache-hub/function-cache");
16007
16380
  function isValidCache(cache) {
16008
16381
  return cache !== null && typeof cache === "object" && typeof cache.get === "function" && typeof cache.set === "function";
16009
16382
  }
16010
16383
  function resolveCacheSource(cacheOrDb) {
16011
16384
  if (cacheOrDb === void 0) {
16012
- return new import_cache_hub3.MemoryCache();
16385
+ return new import_cache_hub4.MemoryCache();
16013
16386
  }
16014
16387
  if (isValidCache(cacheOrDb)) {
16015
16388
  return cacheOrDb;
@@ -16095,7 +16468,7 @@ function withCache(fn, options = {}) {
16095
16468
  namespace: namespace ?? "fn",
16096
16469
  keyBuilder,
16097
16470
  condition,
16098
- cache: externalCache ?? new import_cache_hub3.MemoryCache(),
16471
+ cache: externalCache ?? new import_cache_hub4.MemoryCache(),
16099
16472
  enableStats
16100
16473
  });
16101
16474
  const baseStats = wrapped.stats.bind(wrapped);
@@ -16845,9 +17218,8 @@ var MonSQLizeRuntime = class {
16845
17218
  driverError = error instanceof Error ? error.message : String(error);
16846
17219
  }
16847
17220
  }
16848
- const cacheStats = this._cache.getStats?.();
16849
- const poolHealth = this._poolManager?.getHealthStatus();
16850
- 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;
16851
17223
  const connected = this._connected && driverConnected;
16852
17224
  return {
16853
17225
  status: connected ? "up" : "down",
@@ -16855,7 +17227,7 @@ var MonSQLizeRuntime = class {
16855
17227
  driver: { connected: driverConnected, ...driverError ? { error: driverError } : {} },
16856
17228
  defaults: this.getDefaults(),
16857
17229
  cache: {
16858
- enabled: true,
17230
+ enabled: cacheEnabled,
16859
17231
  stats: cacheStats,
16860
17232
  pools: poolHealth,
16861
17233
  distributed: distributedStats
@@ -16867,18 +17239,18 @@ var MonSQLizeRuntime = class {
16867
17239
  ...driverError ? { error: driverError } : {}
16868
17240
  },
16869
17241
  cache: {
16870
- status: "up",
16871
- enabled: true,
17242
+ status: cacheProbeStatus,
17243
+ enabled: cacheEnabled,
16872
17244
  hasStats: cacheStats !== void 0
16873
17245
  },
16874
17246
  distributedCacheInvalidator: {
16875
- status: distributedStats ? "up" : "unknown",
16876
- enabled: distributedStats !== null,
17247
+ status: distributedStatus,
17248
+ enabled: distributedEnabled,
16877
17249
  stats: distributedStats
16878
17250
  },
16879
17251
  pools: {
16880
- status: poolHealth ? "up" : "unknown",
16881
- enabled: poolHealth !== void 0,
17252
+ status: poolsStatus,
17253
+ enabled: this._poolManager !== null,
16882
17254
  health: poolHealth
16883
17255
  }
16884
17256
  },
@@ -17829,6 +18201,27 @@ function ejsonLine(value) {
17829
18201
  function ejsonManifest(value) {
17830
18202
  return import_mongodb11.BSON.EJSON.stringify(value, void 0, 2, { relaxed: false });
17831
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
+ }
17832
18225
  function sanitizeName(value) {
17833
18226
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "data-task";
17834
18227
  }
@@ -18005,10 +18398,9 @@ async function createRestoreSafetyBackup(source, entries, beforeIndexes, targetH
18005
18398
  }
18006
18399
  async function readDataTaskBackup(ref) {
18007
18400
  try {
18008
- 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 }));
18009
18402
  if (manifest.kind !== "monsqlize-data-task-backup" || manifest.version !== 1 || manifest.runId !== ref.runId) throw new Error("invalid backup manifest");
18010
18403
  if (manifest.compression !== "gzip" && manifest.compression !== "none") throw new Error("invalid backup compression");
18011
- if (!Number.isSafeInteger(manifest.entryCount) || manifest.entryCount < 0) throw new Error("invalid backup entry count");
18012
18404
  if (typeof manifest.checksum !== "string" || !/^[a-f0-9]{64}$/i.test(manifest.checksum)) throw new Error("invalid backup checksum");
18013
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) {
18014
18406
  throw new Error("backup data file must stay beside its manifest");
@@ -18339,6 +18731,7 @@ async function acquireDataTaskLease(host, options) {
18339
18731
  }
18340
18732
 
18341
18733
  // src/capabilities/data-tasks/job-restore.ts
18734
+ var import_mongodb12 = require("mongodb");
18342
18735
  async function findCurrent(collection, identity) {
18343
18736
  let chain = collection.find(identity);
18344
18737
  if (typeof chain.limit === "function") chain = chain.limit(2);
@@ -18359,13 +18752,20 @@ function restoreApproval(hashes) {
18359
18752
  };
18360
18753
  return { ...payload, token: hashDataTaskValue(payload) };
18361
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
+ }
18362
18762
  function exactIndex(index, current) {
18363
- 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";
18364
18764
  }
18365
18765
  function pendingIndexMatches(index, current) {
18366
18766
  return current.filter((candidate) => {
18367
18767
  if (index.name && candidate.name !== index.name) return false;
18368
- 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";
18369
18769
  });
18370
18770
  }
18371
18771
  function plannedDocumentMatches(current, entry) {
@@ -18633,7 +19033,7 @@ async function restoreDataTaskPlan(plan, target, targetHint, lease) {
18633
19033
  await updateDataTaskBackup(safety, (manifest) => {
18634
19034
  manifest.pendingIndexes = [...manifest.pendingIndexes ?? [], pending];
18635
19035
  });
18636
- await collection.createIndex(index.key, { ...index.options, name: index.name });
19036
+ await collection.createIndex(comparableIndexKey(index.key), { ...index.options, name: index.name });
18637
19037
  if (!exactIndex(index, await collection.listIndexes())) {
18638
19038
  throw new DataTaskJobError("RESTORE_FAILED", `index "${index.name}" failed immediate readback.`, "restore");
18639
19039
  }