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.
@@ -170,12 +170,17 @@ function createRedisCacheAdapter(redisUrlOrInstance, options) {
170
170
 
171
171
  // src/capabilities/cache/distributed-cache-invalidator.ts
172
172
  import { randomBytes } from "crypto";
173
+ import { MemoryCache as BaseMemoryCache2 } from "cache-hub";
173
174
  var DistributedCacheInvalidator = class {
174
175
  constructor(options) {
175
176
  if (!options.cache) {
176
177
  throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
177
178
  }
178
179
  this._cache = options.cache;
180
+ const safeMemoryCache = this._cache instanceof BaseMemoryCache2 || this._cache.local instanceof BaseMemoryCache2 && this._cache.remote === void 0;
181
+ if (!safeMemoryCache && typeof this._cache.invalidateFromRemote !== "function") {
182
+ throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires receive-only cache invalidation");
183
+ }
179
184
  this._logger = options.logger ?? null;
180
185
  this.channel = options.channel ?? "monsqlize:cache:invalidate";
181
186
  this.instanceId = options.instanceId ?? `instance-${Date.now()}-${randomBytes(4).toString("hex")}`;
@@ -223,18 +228,10 @@ var DistributedCacheInvalidator = class {
223
228
  this._stats.messagesReceived++;
224
229
  try {
225
230
  if (isPatternInvalidation) {
226
- for (const targetCache of this._getPatternTargetCaches()) {
227
- await targetCache.delPattern?.(String(message.pattern));
228
- }
231
+ await this._invalidateFromRemote("pattern", String(message.pattern));
229
232
  this._logger?.debug?.(`[DistributedCacheInvalidator] Invalidated cache targets: ${String(message.pattern)}`);
230
233
  } else {
231
- for (const targetCache of this._getKeyTargetCaches()) {
232
- if (typeof targetCache.del === "function") {
233
- await targetCache.del(String(message.key));
234
- } else {
235
- await targetCache.delete?.(String(message.key));
236
- }
237
- }
234
+ await this._invalidateFromRemote("key", String(message.key));
238
235
  this._logger?.debug?.(`[DistributedCacheInvalidator] Invalidated cache key targets: ${String(message.key)}`);
239
236
  }
240
237
  this._stats.invalidationsTriggered++;
@@ -244,32 +241,18 @@ var DistributedCacheInvalidator = class {
244
241
  }
245
242
  });
246
243
  }
247
- _getPatternTargetCaches() {
248
- const targets = /* @__PURE__ */ new Set();
249
- if (typeof this._cache?.delPattern === "function") {
250
- targets.add(this._cache);
251
- }
252
- if (this._cache?.local && typeof this._cache.local.delPattern === "function") {
253
- targets.add(this._cache.local);
254
- }
255
- if (this._cache?.remote && typeof this._cache.remote.delPattern === "function") {
256
- targets.add(this._cache.remote);
257
- }
258
- return [...targets];
259
- }
260
- _getKeyTargetCaches() {
261
- const targets = /* @__PURE__ */ new Set();
262
- const supportsKeyDelete = (cache) => typeof cache?.del === "function" || typeof cache?.delete === "function";
263
- if (supportsKeyDelete(this._cache)) {
264
- targets.add(this._cache);
265
- }
266
- if (supportsKeyDelete(this._cache?.local)) {
267
- targets.add(this._cache.local);
244
+ async _invalidateFromRemote(kind, value) {
245
+ if (this._cache instanceof BaseMemoryCache2) {
246
+ if (kind === "pattern") await this._cache.delPattern(value);
247
+ else await this._cache.del(value);
248
+ return;
268
249
  }
269
- if (supportsKeyDelete(this._cache?.remote)) {
270
- targets.add(this._cache.remote);
250
+ if (this._cache.local instanceof BaseMemoryCache2 && this._cache.remote === void 0) {
251
+ if (kind === "pattern") await this._cache.local.delPattern(value);
252
+ else await this._cache.local.del(value);
253
+ return;
271
254
  }
272
- return [...targets];
255
+ await this._cache.invalidateFromRemote?.(kind, value);
273
256
  }
274
257
  async invalidate(pattern) {
275
258
  if (!pattern) return;
@@ -341,6 +324,15 @@ var MultiLevelCache = class extends BaseMultiLevelCache {
341
324
  this._localCompat.setLockManager?.(lockManager);
342
325
  this._remoteCompat?.setLockManager?.(lockManager);
343
326
  }
327
+ async invalidateFromRemote(kind, value) {
328
+ if (kind === "pattern") {
329
+ await this._localCompat.delPattern(value);
330
+ await this._remoteCompat?.delPattern(value);
331
+ } else {
332
+ await this._localCompat.del(value);
333
+ await this._remoteCompat?.del(value);
334
+ }
335
+ }
344
336
  };
345
337
  function adaptLegacyCacheLike(v1Cache) {
346
338
  if ("has" in v1Cache && typeof v1Cache.has === "function") {
@@ -718,6 +710,40 @@ function serializeDocument(document) {
718
710
  }
719
711
 
720
712
  // src/capabilities/model/model-write-helpers.ts
713
+ async function* iterateModelCandidateBatches(context, filter, options, batchSize) {
714
+ const lookupOptions = buildModelVersionLookupOptions(options, {
715
+ _id: 1,
716
+ [context.versionConfig?.field ?? "__v"]: 1
717
+ });
718
+ const stream = context.collection.stream(filter, {
719
+ ...lookupOptions,
720
+ sort: { _id: 1 },
721
+ batchSize
722
+ });
723
+ let batch = [];
724
+ try {
725
+ for await (const document of stream) {
726
+ batch.push(document);
727
+ if (batch.length === batchSize) {
728
+ yield batch;
729
+ batch = [];
730
+ }
731
+ }
732
+ if (batch.length > 0) yield batch;
733
+ } finally {
734
+ stream.destroy();
735
+ }
736
+ }
737
+ function buildStrictCandidateFilter(filter, id, versionField, expectedVersion) {
738
+ return { $and: [filter ?? {}, { _id: id }, { [versionField]: expectedVersion }] };
739
+ }
740
+ async function isModelVersionConflict(context, filter, id, versionField, expectedVersion, options) {
741
+ const current = await context.collection.findOne(
742
+ { $and: [filter ?? {}, { _id: id }] },
743
+ buildModelVersionLookupOptions(options, { [versionField]: 1 })
744
+ );
745
+ return current !== null && current !== void 0 && current[versionField] !== expectedVersion;
746
+ }
721
747
  var VERSION_LOOKUP_OPTION_KEYS = [
722
748
  "session",
723
749
  "readConcern",
@@ -803,11 +829,14 @@ function mapModelSchemaValidationErrors(errors = []) {
803
829
  }
804
830
  function validateModelSchemaPayload(context, document, options, metadata = {}) {
805
831
  const shouldValidate = context.validateEnabled || options?.validate === true;
806
- if (!shouldValidate) {
832
+ if (!shouldValidate || options?.skipValidation) {
807
833
  return document;
808
834
  }
809
- if (options?.skipValidation) {
810
- return document;
835
+ if (context.schemaError) {
836
+ throw withModelErrorMetadata(
837
+ createError(ErrorCodes.VALIDATION_ERROR, `Schema initialization failed: ${context.schemaError.message}`),
838
+ { errors: [{ field: "_schema", message: context.schemaError.message }], ...metadata }
839
+ );
811
840
  }
812
841
  if (!context.schemaCache || !context.schemaValidateFn) {
813
842
  return document;
@@ -841,22 +870,31 @@ function validateModelSchemaPayload(context, document, options, metadata = {}) {
841
870
  }
842
871
  return normalized;
843
872
  }
873
+ function buildModelVisibleSoftDeleteCondition(config) {
874
+ return { [config.field]: config.type === "boolean" ? { $ne: true } : null };
875
+ }
844
876
  function applyModelSoftDeleteFilter(query, options, softDeleteConfig) {
845
877
  if (!softDeleteConfig?.enabled) {
846
878
  return query ?? {};
847
879
  }
848
880
  const resolvedOptions = options ?? {};
849
881
  const resolvedQuery = query ?? {};
850
- if (resolvedQuery[softDeleteConfig.field] !== void 0) {
882
+ if (resolvedOptions.withDeleted !== void 0 && typeof resolvedOptions.withDeleted !== "boolean") {
883
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "withDeleted must be a boolean.");
884
+ }
885
+ if (resolvedOptions.onlyDeleted !== void 0 && typeof resolvedOptions.onlyDeleted !== "boolean") {
886
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "onlyDeleted must be a boolean.");
887
+ }
888
+ if (Object.prototype.hasOwnProperty.call(resolvedQuery, softDeleteConfig.field)) {
851
889
  return resolvedQuery;
852
890
  }
853
891
  if (resolvedOptions.withDeleted) {
854
892
  return resolvedQuery;
855
893
  }
856
894
  if (resolvedOptions.onlyDeleted) {
857
- return { ...resolvedQuery, [softDeleteConfig.field]: { $ne: null } };
895
+ return { ...resolvedQuery, [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : { $ne: null } };
858
896
  }
859
- return { ...resolvedQuery, [softDeleteConfig.field]: null };
897
+ return { ...resolvedQuery, ...buildModelVisibleSoftDeleteCondition(softDeleteConfig) };
860
898
  }
861
899
  function applyModelInsertTimestamps(document, timestampsConfig, nowFactory) {
862
900
  if (!timestampsConfig) {
@@ -1151,7 +1189,9 @@ function projectPopulatedDocument(document, select, nestedPaths) {
1151
1189
  return document;
1152
1190
  }
1153
1191
  const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
1154
- return pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
1192
+ const projected = pickFields(document, [.../* @__PURE__ */ new Set([...select, ...nestedRelationFields])]);
1193
+ if (!select.includes("_id")) delete projected._id;
1194
+ return projected;
1155
1195
  }
1156
1196
  async function populateModelPath(context, docs, path5, traversalState) {
1157
1197
  const config = normalizePopulateConfig(path5);
@@ -1173,7 +1213,13 @@ async function populateModelPath(context, docs, path5, traversalState) {
1173
1213
  }
1174
1214
  const localValues = docs.map((doc) => getByPath(doc, relation.localField));
1175
1215
  const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
1176
- if (keys.length === 0) {
1216
+ if (config.limit !== void 0 && (!Number.isSafeInteger(config.limit) || config.limit < 0)) {
1217
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "populate limit must be a non-negative integer");
1218
+ }
1219
+ if (config.skip !== void 0 && (!Number.isSafeInteger(config.skip) || config.skip < 0)) {
1220
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "populate skip must be a non-negative integer");
1221
+ }
1222
+ if (keys.length === 0 || config.limit === 0) {
1177
1223
  for (const doc of docs) {
1178
1224
  doc[config.path] = relation.single ? null : [];
1179
1225
  }
@@ -1193,36 +1239,63 @@ async function populateModelPath(context, docs, path5, traversalState) {
1193
1239
  ...effectiveSelect,
1194
1240
  relation.foreignField,
1195
1241
  "_id",
1196
- ...collectNestedLocalFields(relatedModel, nestedPaths)
1242
+ ...Object.keys(config.sort ?? {}),
1243
+ ...collectNestedLocalFields(relatedModel, nestedPaths),
1244
+ ...relatedModel?.softDeleteConfig?.enabled ? [relatedModel.softDeleteConfig.field] : []
1197
1245
  ])] : [];
1198
- const relatedDocs = await relatedCollection.find({
1199
- [relation.foreignField]: { $in: keys },
1200
- ...config.match ?? {}
1201
- }, effectiveSelect ? {
1202
- projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
1203
- } : void 0);
1204
- let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
1205
- if (nestedPaths.length > 0 && relatedModel) {
1206
- if (state.depth + 1 > state.maxDepth) {
1207
- throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1246
+ const readOptions = {
1247
+ ...effectiveSelect ? { projection: Object.fromEntries(projectionFields.map((field) => [field, 1])) } : {},
1248
+ ...config.sort ? { sort: config.sort } : {}
1249
+ };
1250
+ const visibilityMatch = applyModelSoftDeleteFilter(config.match ?? {}, {}, relatedModel?.softDeleteConfig ?? null);
1251
+ const readForKeys = (values, options = readOptions) => {
1252
+ const keyFilter = { [relation.foreignField]: { $in: values } };
1253
+ return readPopulateRows(
1254
+ relatedCollection,
1255
+ Object.keys(visibilityMatch).length === 0 ? keyFilter : { $and: [keyFilter, visibilityMatch] },
1256
+ options
1257
+ );
1258
+ };
1259
+ const hydrateRelated = async (rows) => {
1260
+ let hydrated = relatedModel ? relatedModel.hydrateDocuments(rows) : rows.map((item) => ({ ...item }));
1261
+ if (nestedPaths.length > 0 && relatedModel) {
1262
+ if (state.depth + 1 > state.maxDepth) {
1263
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `populate maxDepth exceeded: ${state.maxDepth}`);
1264
+ }
1265
+ hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1266
+ depth: state.depth + 1,
1267
+ maxDepth: state.maxDepth,
1268
+ activeConfigs: state.activeConfigs
1269
+ });
1208
1270
  }
1209
- hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1210
- depth: state.depth + 1,
1211
- maxDepth: state.maxDepth,
1212
- activeConfigs: state.activeConfigs
1213
- });
1214
- }
1215
- const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1216
- for (const [index, doc] of docs.entries()) {
1217
- let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1218
- if (config.skip) {
1219
- matches = matches.slice(config.skip);
1271
+ return hydrated;
1272
+ };
1273
+ if (config.limit !== void 0 || config.skip !== void 0) {
1274
+ const scopedOptions = {
1275
+ ...readOptions,
1276
+ ...config.skip !== void 0 ? { skip: config.skip } : {},
1277
+ ...config.limit !== void 0 ? { limit: config.limit } : {}
1278
+ };
1279
+ for (const [index, doc] of docs.entries()) {
1280
+ const parentKeys = unique(flattenComparableValues(localValues[index]));
1281
+ const rows = parentKeys.length > 0 ? await readForKeys(parentKeys, scopedOptions) : [];
1282
+ const hydrated = await hydrateRelated(rows);
1283
+ const matches = dedupeDocuments(hydrated);
1284
+ const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1285
+ doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1220
1286
  }
1221
- if (config.limit !== void 0) {
1222
- matches = matches.slice(0, config.limit);
1287
+ } else {
1288
+ const rows = [];
1289
+ for (let offset = 0; offset < keys.length; offset += 200) {
1290
+ rows.push(...await readForKeys(keys.slice(offset, offset + 200)));
1291
+ }
1292
+ const hydrated = await hydrateRelated(rows);
1293
+ const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1294
+ for (const [index, doc] of docs.entries()) {
1295
+ const matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1296
+ const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1297
+ doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1223
1298
  }
1224
- const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1225
- doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1226
1299
  }
1227
1300
  return docs;
1228
1301
  } finally {
@@ -1423,12 +1496,28 @@ async function saveModelDocument(collection, document, options = {}) {
1423
1496
  document._id = result.insertedId;
1424
1497
  return document;
1425
1498
  }
1426
- async function removeModelDocument(collection, document) {
1499
+ async function removeModelDocument(collection, document, deleteOne = (filter) => collection.deleteOne(filter)) {
1427
1500
  if (document._id === void 0) {
1428
1501
  return false;
1429
1502
  }
1430
- const result = await collection.deleteOne({ _id: document._id });
1431
- return Boolean(result.deletedCount ?? result.acknowledged);
1503
+ const result = await deleteOne({ _id: document._id });
1504
+ if (result.matchedCount === void 0 && result.modifiedCount === void 0 && result.deletedCount === void 0) {
1505
+ return result.acknowledged === true;
1506
+ }
1507
+ return Boolean(result.matchedCount || result.modifiedCount || result.deletedCount);
1508
+ }
1509
+ async function readPopulateRows(collection, query, options) {
1510
+ if (typeof collection.stream !== "function") {
1511
+ return await collection.find(query, options);
1512
+ }
1513
+ const stream = collection.stream(query, options);
1514
+ const rows = [];
1515
+ try {
1516
+ for await (const row of stream) rows.push(row);
1517
+ } finally {
1518
+ stream.destroy();
1519
+ }
1520
+ return rows;
1432
1521
  }
1433
1522
 
1434
1523
  // src/capabilities/model/model-soft-delete-helpers.ts
@@ -1436,7 +1525,53 @@ function deletedFilter(filter, softDeleteConfig) {
1436
1525
  if (!softDeleteConfig) {
1437
1526
  return filter ?? {};
1438
1527
  }
1439
- return { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } };
1528
+ return applyModelSoftDeleteFilter(filter, { onlyDeleted: true }, softDeleteConfig);
1529
+ }
1530
+ function filterVisibleBySoftDelete(docs, options, config) {
1531
+ if (!config?.enabled) return docs.filter((doc) => Boolean(doc));
1532
+ const rawOptions = options ?? {};
1533
+ applyModelSoftDeleteFilter({}, rawOptions, config);
1534
+ if (rawOptions.withDeleted) return docs.filter((doc) => Boolean(doc));
1535
+ return docs.filter((doc) => {
1536
+ if (!doc) return false;
1537
+ const value = doc[config.field];
1538
+ const deleted = config.type === "boolean" ? value === true : value !== void 0 && value !== null;
1539
+ return rawOptions.onlyDeleted ? deleted : !deleted;
1540
+ });
1541
+ }
1542
+ function withSoftDeleteProjection(options, config) {
1543
+ if (!config?.enabled) return { options, stripField: false };
1544
+ const rawOptions = options ?? {};
1545
+ const field = config.field;
1546
+ const projection = rawOptions.projection ?? rawOptions.project;
1547
+ if (Array.isArray(projection)) {
1548
+ if (projection.includes(field)) return { options, stripField: false };
1549
+ return { options: { ...rawOptions, projection: [...projection, field] }, stripField: true };
1550
+ }
1551
+ if (!projection || typeof projection !== "object") return { options, stripField: false };
1552
+ const projectionRecord = projection;
1553
+ if (projectionRecord[field] === 1 || projectionRecord[field] === true) return { options, stripField: false };
1554
+ const inclusion = Object.entries(projectionRecord).some(([key, value]) => key !== "_id" && (value === 1 || value === true));
1555
+ const nextProjection = { ...projectionRecord };
1556
+ if (inclusion) nextProjection[field] = 1;
1557
+ else delete nextProjection[field];
1558
+ return { options: { ...rawOptions, projection: nextProjection }, stripField: true };
1559
+ }
1560
+ function applySoftDeleteFindPageOptions(options, config) {
1561
+ const rawOptions = options ?? {};
1562
+ return { ...rawOptions, query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, config) };
1563
+ }
1564
+ function applySoftDeleteAggregatePipeline(pipeline, options, config) {
1565
+ if (!config?.enabled) return pipeline ?? [];
1566
+ const rawOptions = options ?? {};
1567
+ if (rawOptions.withDeleted) return pipeline ?? [];
1568
+ const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, config);
1569
+ const matchStage = { $match: softDeleteMatch };
1570
+ const stages = [...pipeline ?? []];
1571
+ if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
1572
+ return [stages[0], matchStage, ...stages.slice(1)];
1573
+ }
1574
+ return [matchStage, ...stages];
1440
1575
  }
1441
1576
  function findWithDeletedDocuments(context, query, options) {
1442
1577
  return new PopulatePromise(async (paths) => {
@@ -1479,7 +1614,7 @@ function restoreSoftDeletedDocuments(context, filter, options) {
1479
1614
  return Promise.resolve({ modifiedCount: 0 });
1480
1615
  }
1481
1616
  return context.collection.updateOne(
1482
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1617
+ deletedFilter(filter, softDeleteConfig),
1483
1618
  { $unset: { [softDeleteConfig.field]: 1 } },
1484
1619
  options
1485
1620
  );
@@ -1490,7 +1625,7 @@ function restoreManySoftDeletedDocuments(context, filter, options) {
1490
1625
  return Promise.resolve({ modifiedCount: 0 });
1491
1626
  }
1492
1627
  return context.collection.updateMany(
1493
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1628
+ deletedFilter(filter, softDeleteConfig),
1494
1629
  { $unset: { [softDeleteConfig.field]: 1 } },
1495
1630
  options
1496
1631
  );
@@ -3106,6 +3241,9 @@ var modelWriteSourceStorage = new AsyncLocalStorage();
3106
3241
  function isPlainObject2(value) {
3107
3242
  return value !== null && typeof value === "object" && !Array.isArray(value);
3108
3243
  }
3244
+ function ownValue(record, key) {
3245
+ return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
3246
+ }
3109
3247
  function invalidConfig(path5, reason) {
3110
3248
  throw createError(
3111
3249
  ErrorCodes.INVALID_OPERATION,
@@ -3126,19 +3264,19 @@ function normalizeRule(input, path5) {
3126
3264
  if (!isPlainObject2(input)) {
3127
3265
  invalidConfig(path5, "rule must be a string mode or an object");
3128
3266
  }
3129
- const mode = input.mode ?? DEFAULT_RULE.mode;
3267
+ const mode = ownValue(input, "mode") ?? DEFAULT_RULE.mode;
3130
3268
  if (!ALLOWED_MODES.has(mode)) {
3131
3269
  invalidConfig(`${path5}.mode`, 'must be "allow-both" or "model-only"');
3132
3270
  }
3133
- const raw = input.raw ?? DEFAULT_RULE.raw;
3271
+ const raw = ownValue(input, "raw") ?? DEFAULT_RULE.raw;
3134
3272
  if (!ALLOWED_RAW.has(raw)) {
3135
3273
  invalidConfig(`${path5}.raw`, 'must be "inherit", "allow", or "block"');
3136
3274
  }
3137
- const management = input.management ?? DEFAULT_RULE.management;
3275
+ const management = ownValue(input, "management") ?? DEFAULT_RULE.management;
3138
3276
  if (!ALLOWED_MANAGEMENT.has(management)) {
3139
3277
  invalidConfig(`${path5}.management`, 'must be "inherit", "allow", or "block"');
3140
3278
  }
3141
- const onViolation = input.onViolation ?? DEFAULT_RULE.onViolation;
3279
+ const onViolation = ownValue(input, "onViolation") ?? DEFAULT_RULE.onViolation;
3142
3280
  if (!ALLOWED_VIOLATION_ACTIONS.has(onViolation)) {
3143
3281
  invalidConfig(`${path5}.onViolation`, 'must be "throw" or "warn"');
3144
3282
  }
@@ -3154,15 +3292,15 @@ function validateWritePathPolicyConfig(options) {
3154
3292
  }
3155
3293
  function normalizeWritePathPolicy(options) {
3156
3294
  if (options === void 0) {
3157
- return { default: { ...DEFAULT_RULE }, namespaces: {}, enabled: false };
3295
+ return { default: { ...DEFAULT_RULE }, namespaces: /* @__PURE__ */ Object.create(null), enabled: false };
3158
3296
  }
3159
3297
  if (!isPlainObject2(options)) {
3160
3298
  invalidConfig("writePathPolicy", "must be an object");
3161
3299
  }
3162
3300
  const typedOptions = options;
3163
- const defaultRule = normalizeRule(typedOptions.default, "writePathPolicy.default");
3164
- const namespacesInput = options.namespaces;
3165
- const namespaces = {};
3301
+ const defaultRule = normalizeRule(ownValue(typedOptions, "default"), "writePathPolicy.default");
3302
+ const namespacesInput = ownValue(options, "namespaces");
3303
+ const namespaces = /* @__PURE__ */ Object.create(null);
3166
3304
  if (namespacesInput !== void 0) {
3167
3305
  if (!isPlainObject2(namespacesInput)) {
3168
3306
  invalidConfig("writePathPolicy.namespaces", "must be an object");
@@ -3210,8 +3348,8 @@ function resolveWritePathRule(policy, namespace) {
3210
3348
  if (candidate === "default") {
3211
3349
  return { key: "default", rule: effectivePolicy.default };
3212
3350
  }
3213
- const rule = effectivePolicy.namespaces[candidate];
3214
- if (rule) {
3351
+ if (Object.prototype.hasOwnProperty.call(effectivePolicy.namespaces, candidate)) {
3352
+ const rule = effectivePolicy.namespaces[candidate];
3215
3353
  return { key: candidate, rule };
3216
3354
  }
3217
3355
  }
@@ -3354,16 +3492,17 @@ var fallbackModelIndexTasks = /* @__PURE__ */ new Map();
3354
3492
  function toCompatDefinition(definition) {
3355
3493
  return definition;
3356
3494
  }
3357
- function stableIndexStringify(value) {
3495
+ function stableIndexStringify(value, mode = "options") {
3358
3496
  if (value instanceof Date) {
3359
3497
  return JSON.stringify(value.toISOString());
3360
3498
  }
3361
3499
  if (Array.isArray(value)) {
3362
- return `[${value.map((item) => stableIndexStringify(item)).join(",")}]`;
3500
+ return `[${value.map((item) => stableIndexStringify(item, mode)).join(",")}]`;
3363
3501
  }
3364
3502
  if (value && typeof value === "object") {
3365
- const entries = Object.entries(value).filter(([, current]) => current !== void 0).sort(([left], [right]) => left.localeCompare(right));
3366
- return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableIndexStringify(current)}`).join(",")}}`;
3503
+ const entries = Object.entries(value).filter(([, current]) => current !== void 0);
3504
+ if (mode === "options") entries.sort(([left], [right]) => left.localeCompare(right));
3505
+ return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableIndexStringify(current, key === "key" ? "key" : mode)}`).join(",")}}`;
3367
3506
  }
3368
3507
  return JSON.stringify(value) ?? "undefined";
3369
3508
  }
@@ -3397,7 +3536,7 @@ function declaredOptionEntries(options) {
3397
3536
  });
3398
3537
  }
3399
3538
  function indexOptionsMatch(existing, declared) {
3400
- if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
3539
+ if (stableIndexStringify(getExistingIndexKey(existing), "key") !== stableIndexStringify(declared.key, "key")) {
3401
3540
  return false;
3402
3541
  }
3403
3542
  for (const [name, value] of declaredOptionEntries(declared.options)) {
@@ -3416,8 +3555,8 @@ function findExistingIndexByName(existingIndexes, name) {
3416
3555
  return existingIndexes.find((index) => index.name === name);
3417
3556
  }
3418
3557
  function findExistingIndexByKey(existingIndexes, key) {
3419
- const fingerprint = stableIndexStringify(key);
3420
- return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
3558
+ const fingerprint = stableIndexStringify(key, "key");
3559
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index), "key") === fingerprint);
3421
3560
  }
3422
3561
  function createIndexEnsureError(message, result, cause) {
3423
3562
  return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
@@ -4157,7 +4296,7 @@ async function readNearestBookmark(cache, namespace, keyDims, targetPage) {
4157
4296
 
4158
4297
  // src/adapters/mongodb/queries/query-helpers.ts
4159
4298
  import { createHmac, timingSafeEqual } from "node:crypto";
4160
- import { ObjectId as ObjectId2 } from "mongodb";
4299
+ import { BSON, ObjectId as ObjectId2 } from "mongodb";
4161
4300
 
4162
4301
  // src/utils/normalize.ts
4163
4302
  function normalizeProjection(p) {
@@ -4255,12 +4394,17 @@ function shouldConvertQueryField(autoConvert, path5, depth) {
4255
4394
  if (!config.enabled || depth > config.maxDepth) return false;
4256
4395
  return shouldConvertObjectIdPath(path5, config);
4257
4396
  }
4397
+ function isPlainQueryRecord(value) {
4398
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4399
+ const prototype = Object.getPrototypeOf(value);
4400
+ return prototype === Object.prototype || prototype === null;
4401
+ }
4258
4402
  function normalizeQueryArray(value, autoConvert, path5, depth) {
4259
4403
  return value.map((item, index) => {
4260
4404
  if (typeof item === "string" && item.length === 24 && ObjectId2.isValid(item) && shouldConvertQueryField(autoConvert, path5, depth)) {
4261
4405
  return new ObjectId2(item);
4262
4406
  }
4263
- if (item && typeof item === "object" && !Array.isArray(item) && !(item instanceof ObjectId2)) {
4407
+ if (isPlainQueryRecord(item)) {
4264
4408
  return normalizeQueryFilter(item, autoConvert, `${path5}[${index}]`, depth + 1);
4265
4409
  }
4266
4410
  return item;
@@ -4276,7 +4420,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4276
4420
  }
4277
4421
  for (const [key, value] of Object.entries(filter)) {
4278
4422
  if (key === "$and" || key === "$or" || key === "$nor") {
4279
- result[key] = Array.isArray(value) ? value.map((item) => item && typeof item === "object" ? normalizeQueryFilter(item, autoConvert, fieldPath, depth + 1) : item) : value;
4423
+ result[key] = Array.isArray(value) ? value.map((item) => isPlainQueryRecord(item) ? normalizeQueryFilter(item, autoConvert, fieldPath, depth + 1) : item) : value;
4280
4424
  continue;
4281
4425
  }
4282
4426
  const currentPath = fieldPath ? `${fieldPath}.${key}` : key;
@@ -4285,7 +4429,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4285
4429
  result[key] = shouldConvert ? new ObjectId2(value) : value;
4286
4430
  } else if (Array.isArray(value)) {
4287
4431
  result[key] = shouldConvert ? normalizeQueryArray(value, autoConvert, currentPath, depth + 1) : value;
4288
- } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof ObjectId2)) {
4432
+ } else if (isPlainQueryRecord(value)) {
4289
4433
  const nested = value;
4290
4434
  const hasOperators = Object.keys(nested).some((k) => k.startsWith("$"));
4291
4435
  if (hasOperators) {
@@ -4297,7 +4441,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4297
4441
  );
4298
4442
  } else if (shouldConvert && QUERY_OBJECTID_SCALAR_OPERATORS.has(op) && typeof opVal === "string" && opVal.length === 24 && ObjectId2.isValid(opVal)) {
4299
4443
  nestedResult[op] = new ObjectId2(opVal);
4300
- } else if (op === "$elemMatch" && opVal && typeof opVal === "object" && !Array.isArray(opVal)) {
4444
+ } else if (op === "$elemMatch" && isPlainQueryRecord(opVal)) {
4301
4445
  nestedResult[op] = normalizeQueryFilter(opVal, autoConvert, currentPath, depth + 1);
4302
4446
  } else {
4303
4447
  nestedResult[op] = opVal;
@@ -4471,36 +4615,29 @@ async function isCollectionCacheBarrierActive(queryCache, collection, defaults =
4471
4615
  buildCollectionCacheBarrierNamespaces(collection, defaults)
4472
4616
  );
4473
4617
  }
4474
- function normalizeCacheKeyValue(value) {
4475
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
4476
- return void 0;
4477
- }
4478
- if (value instanceof Date) {
4479
- return { $date: value.toISOString() };
4480
- }
4481
- if (value instanceof ObjectId2) {
4482
- return { $oid: value.toHexString() };
4483
- }
4484
- if (value instanceof RegExp) {
4485
- return { $regex: value.source, $flags: value.flags };
4486
- }
4487
- if (Array.isArray(value)) {
4488
- return value.map((item) => normalizeCacheKeyValue(item));
4489
- }
4490
- if (value && typeof value === "object") {
4491
- const sorted = {};
4492
- for (const key of Object.keys(value).sort()) {
4493
- const normalized = normalizeCacheKeyValue(value[key]);
4494
- if (normalized !== void 0) {
4495
- sorted[key] = normalized;
4496
- }
4497
- }
4498
- return sorted;
4499
- }
4500
- return value;
4501
- }
4502
- function stableCacheKeyString(value) {
4503
- return JSON.stringify(normalizeCacheKeyValue(value));
4618
+ var ORDERED_OPTION_FIELDS = /* @__PURE__ */ new Set(["query", "filter", "pipeline", "sort", "hint", "projection", "min", "max"]);
4619
+ function normalizeCacheKeyValue(value, mode) {
4620
+ if (value === null) return ["null"];
4621
+ if (value === void 0) return ["undefined"];
4622
+ if (typeof value === "number") return ["number", String(value)];
4623
+ if (typeof value === "bigint") return ["bigint", value.toString()];
4624
+ if (typeof value !== "object") return [typeof value, value];
4625
+ if (value instanceof Date) return ["date", String(value.getTime())];
4626
+ if (value instanceof RegExp) return ["regexp", value.source, value.flags];
4627
+ if (Array.isArray(value)) return ["array", value.map((item) => normalizeCacheKeyValue(item, mode))];
4628
+ const bsonType = value._bsontype;
4629
+ if (typeof bsonType === "string") {
4630
+ return ["bson", bsonType, BSON.EJSON.serialize(value, { relaxed: false })];
4631
+ }
4632
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0 && typeof item !== "function" && typeof item !== "symbol");
4633
+ if (mode === "options") entries.sort(([left], [right]) => left.localeCompare(right));
4634
+ return ["object", entries.map(([key, item]) => [
4635
+ key,
4636
+ normalizeCacheKeyValue(item, mode === "options" && ORDERED_OPTION_FIELDS.has(key) ? "document" : mode)
4637
+ ])];
4638
+ }
4639
+ function stableCacheKeyString(value, mode = "options") {
4640
+ return `v2:${JSON.stringify(normalizeCacheKeyValue(value, mode))}`;
4504
4641
  }
4505
4642
  function buildResultCacheKeyOptions(options = {}) {
4506
4643
  const keyOptions = {};
@@ -4621,7 +4758,7 @@ function buildFindCacheKey(collection, defaults = {}, query, options) {
4621
4758
  ...options ?? {}
4622
4759
  });
4623
4760
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4624
- return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4761
+ return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4625
4762
  }
4626
4763
  function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4627
4764
  const executeOptions = normalizeFindProjectionOptions({
@@ -4629,7 +4766,7 @@ function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4629
4766
  ...options ?? {}
4630
4767
  });
4631
4768
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4632
- return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4769
+ return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4633
4770
  }
4634
4771
  function buildCountCacheKey(collection, defaults = {}, query, options) {
4635
4772
  const merged = {
@@ -4639,7 +4776,7 @@ function buildCountCacheKey(collection, defaults = {}, query, options) {
4639
4776
  const { cache: _cache, ...keyOptions } = merged;
4640
4777
  void _cache;
4641
4778
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4642
- return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4779
+ return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4643
4780
  }
4644
4781
  function buildDistinctCacheKey(collection, defaults = {}, field, query, options) {
4645
4782
  const rawOptions = options ?? {};
@@ -4669,7 +4806,7 @@ function buildAggregateCacheKey(collection, defaults = {}, pipeline, options) {
4669
4806
  ...options ?? {}
4670
4807
  };
4671
4808
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4672
- return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4809
+ return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4673
4810
  }
4674
4811
  function buildFindOneByIdCacheKey(collection, defaults = {}, id, options) {
4675
4812
  const objectId = parseRequiredObjectId(id);
@@ -4684,6 +4821,8 @@ function buildFindByIdsCacheKey(collection, defaults = {}, ids, options) {
4684
4821
  function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4685
4822
  const ext = options;
4686
4823
  const cursorTypes = ext.cursorTypes && typeof ext.cursorTypes === "object" && !Array.isArray(ext.cursorTypes) ? ext.cursorTypes : void 0;
4824
+ const { let: nestedLet, ...nestedOptions } = options.options ?? {};
4825
+ const effectiveLet = ext.let !== void 0 ? ext.let : nestedLet;
4687
4826
  const payload = {
4688
4827
  query: normalizeQueryForCache(options.query, defaults),
4689
4828
  sort: normalizeSortShape(options.sort),
@@ -4705,9 +4844,10 @@ function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4705
4844
  hint: ext.hint,
4706
4845
  collation: ext.collation,
4707
4846
  batchSize: ext.batchSize,
4708
- options: options.options
4847
+ let: effectiveLet,
4848
+ options: nestedOptions
4709
4849
  };
4710
- const keyHash = hashPayload(payload);
4850
+ const keyHash = hashPayload(stableCacheKeyString(payload));
4711
4851
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4712
4852
  return { key: `findPage:${namespace}:${keyHash}`, keyHash };
4713
4853
  }
@@ -5054,29 +5194,30 @@ async function runStrictUpdateMany(context, filter, update, options) {
5054
5194
  if (rawOptions.upsert === true) {
5055
5195
  throw createError(ErrorCodes.INVALID_ARGUMENT, 'versionMode "strict" does not support upsert.');
5056
5196
  }
5057
- const docs = await context.collection.find(filter, {
5058
- ...buildModelVersionLookupOptions(rawOptions, {
5059
- _id: 1,
5060
- [context.versionConfig.field]: 1
5061
- })
5062
- });
5063
5197
  let matchedCount = 0;
5064
5198
  let modifiedCount = 0;
5065
5199
  const conflictedIds = [];
5066
- for (const doc of docs) {
5067
- const id = doc._id;
5068
- const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
5069
- const result = await context.collection.updateOne(
5070
- { _id: id, [context.versionConfig.field]: expectedVersion },
5071
- update,
5072
- options
5073
- );
5074
- if ((result?.matchedCount ?? 0) === 0) {
5075
- conflictedIds.push(id);
5076
- continue;
5200
+ const skippedIds = [];
5201
+ for await (const batch of iterateModelCandidateBatches(context, filter, rawOptions, 200)) {
5202
+ for (const doc of batch) {
5203
+ const id = doc._id;
5204
+ const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
5205
+ const result = await context.collection.updateOne(
5206
+ buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
5207
+ update,
5208
+ options
5209
+ );
5210
+ if ((result?.matchedCount ?? 0) === 0) {
5211
+ if (await isModelVersionConflict(context, filter, id, context.versionConfig.field, expectedVersion, options)) {
5212
+ conflictedIds.push(id);
5213
+ } else {
5214
+ skippedIds.push(id);
5215
+ }
5216
+ continue;
5217
+ }
5218
+ matchedCount += result.matchedCount ?? 0;
5219
+ modifiedCount += result.modifiedCount ?? 0;
5077
5220
  }
5078
- matchedCount += result.matchedCount ?? 0;
5079
- modifiedCount += result.modifiedCount ?? 0;
5080
5221
  }
5081
5222
  return {
5082
5223
  acknowledged: true,
@@ -5085,7 +5226,9 @@ async function runStrictUpdateMany(context, filter, update, options) {
5085
5226
  upsertedCount: 0,
5086
5227
  upsertedId: null,
5087
5228
  conflictCount: conflictedIds.length,
5088
- conflictedIds
5229
+ conflictedIds,
5230
+ skippedCount: skippedIds.length,
5231
+ skippedIds
5089
5232
  };
5090
5233
  }
5091
5234
  async function runStrictUpdateBatch(context, filter, update, options) {
@@ -5108,7 +5251,7 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5108
5251
  retryAttempts: _retryAttempts,
5109
5252
  retryDelay: _retryDelay,
5110
5253
  onRetry: _onRetry,
5111
- sort = { _id: 1 },
5254
+ sort: _sort,
5112
5255
  ...driverOptions
5113
5256
  } = rawOptions;
5114
5257
  void _batchSize;
@@ -5117,38 +5260,42 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5117
5260
  void _retryAttempts;
5118
5261
  void _retryDelay;
5119
5262
  void _onRetry;
5120
- const docs = await context.collection.find(filter, {
5121
- ...buildModelVersionLookupOptions(driverOptions, {
5122
- _id: 1,
5123
- [context.versionConfig.field]: 1
5124
- }),
5125
- sort
5126
- });
5263
+ void _sort;
5264
+ const countOptions = buildModelVersionLookupOptions(driverOptions, {});
5265
+ delete countOptions.projection;
5266
+ const totalCount = await context.collection.count(filter, countOptions);
5127
5267
  const result = {
5128
5268
  acknowledged: true,
5129
- totalCount: docs.length,
5269
+ totalCount,
5130
5270
  matchedCount: 0,
5131
5271
  modifiedCount: 0,
5132
5272
  upsertedCount: 0,
5133
- batchCount: Math.ceil(docs.length / batchSize),
5273
+ batchCount: 0,
5134
5274
  errors: [],
5135
5275
  retries: [],
5136
5276
  conflictCount: 0,
5137
- conflictedIds: []
5277
+ conflictedIds: [],
5278
+ skippedCount: 0,
5279
+ skippedIds: []
5138
5280
  };
5139
- for (let offset = 0; offset < docs.length; offset += batchSize) {
5140
- const batch = docs.slice(offset, offset + batchSize);
5281
+ for await (const batch of iterateModelCandidateBatches(context, filter, driverOptions, batchSize)) {
5282
+ result.batchCount++;
5141
5283
  for (const doc of batch) {
5142
5284
  const id = doc._id;
5143
5285
  const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateBatch");
5144
5286
  const updateResult = await context.collection.updateOne(
5145
- { _id: id, [context.versionConfig.field]: expectedVersion },
5287
+ buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
5146
5288
  update,
5147
5289
  driverOptions
5148
5290
  );
5149
5291
  if ((updateResult?.matchedCount ?? 0) === 0) {
5150
- result.conflictCount = (result.conflictCount ?? 0) + 1;
5151
- result.conflictedIds?.push(id);
5292
+ if (await isModelVersionConflict(context, filter, id, context.versionConfig.field, expectedVersion, driverOptions)) {
5293
+ result.conflictCount = (result.conflictCount ?? 0) + 1;
5294
+ result.conflictedIds?.push(id);
5295
+ } else {
5296
+ result.skippedCount = (result.skippedCount ?? 0) + 1;
5297
+ result.skippedIds?.push(id);
5298
+ }
5152
5299
  continue;
5153
5300
  }
5154
5301
  result.matchedCount += updateResult.matchedCount ?? 0;
@@ -5156,15 +5303,16 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5156
5303
  }
5157
5304
  if (typeof onProgress === "function") {
5158
5305
  onProgress({
5159
- currentBatch: Math.floor(offset / batchSize) + 1,
5160
- totalBatches: result.batchCount,
5306
+ currentBatch: result.batchCount,
5307
+ totalBatches: Math.ceil(totalCount / batchSize),
5161
5308
  modified: result.modifiedCount,
5162
5309
  matched: result.matchedCount,
5163
- total: docs.length,
5164
- percentage: docs.length > 0 ? Math.round(result.modifiedCount / docs.length * 100) : null,
5310
+ total: totalCount,
5311
+ percentage: totalCount > 0 ? Math.round(result.modifiedCount / totalCount * 100) : null,
5165
5312
  errors: result.errors.length,
5166
5313
  retries: result.retries.length,
5167
- conflicts: result.conflictCount ?? 0
5314
+ conflicts: result.conflictCount ?? 0,
5315
+ skipped: result.skippedCount ?? 0
5168
5316
  });
5169
5317
  }
5170
5318
  }
@@ -5177,10 +5325,7 @@ function buildSoftDeletePatch(config, now) {
5177
5325
  return { [config.field]: config.type === "boolean" ? true : now };
5178
5326
  }
5179
5327
  function buildSoftDeleteFilter(filter, config) {
5180
- return {
5181
- ...filter ?? {},
5182
- ...config?.enabled ? { [config.field]: null } : {}
5183
- };
5328
+ return applyModelSoftDeleteFilter(filter, {}, config);
5184
5329
  }
5185
5330
  function applyModelIncrementVersion(field, increment, versionConfig) {
5186
5331
  if (!versionConfig?.enabled || field === void 0) {
@@ -5220,7 +5365,8 @@ async function orchestrateModelInsertOne(context, document, options) {
5220
5365
  payload = validateModelSchemaPayload({
5221
5366
  validateEnabled: context.validateEnabled,
5222
5367
  schemaCache: context.schemaCache,
5223
- schemaValidateFn: context.schemaValidateFn
5368
+ schemaValidateFn: context.schemaValidateFn,
5369
+ schemaError: context.schemaError
5224
5370
  }, payload, options);
5225
5371
  payload = applyModelInsertTimestamps(payload, context.timestampsConfig, () => context.nowDate());
5226
5372
  payload = applyModelInsertVersion(payload, context.versionConfig);
@@ -5254,7 +5400,8 @@ async function orchestrateModelInsertMany(context, documents, options) {
5254
5400
  doc = validateModelSchemaPayload({
5255
5401
  validateEnabled: context.validateEnabled,
5256
5402
  schemaCache: context.schemaCache,
5257
- schemaValidateFn: context.schemaValidateFn
5403
+ schemaValidateFn: context.schemaValidateFn,
5404
+ schemaError: context.schemaError
5258
5405
  }, doc, resolvedOptions, { index });
5259
5406
  doc = applyModelInsertTimestamps(doc, context.timestampsConfig, () => context.nowDate());
5260
5407
  doc = applyModelInsertVersion(doc, context.versionConfig);
@@ -5339,7 +5486,8 @@ async function orchestrateModelReplaceOne(context, filter, replacement, options)
5339
5486
  let nextReplacement = validateModelSchemaPayload({
5340
5487
  validateEnabled: context.validateEnabled,
5341
5488
  schemaCache: context.schemaCache,
5342
- schemaValidateFn: context.schemaValidateFn
5489
+ schemaValidateFn: context.schemaValidateFn,
5490
+ schemaError: context.schemaError
5343
5491
  }, replacement, options);
5344
5492
  nextReplacement = preserveModelReplaceCreatedAt(
5345
5493
  replacement,
@@ -5398,7 +5546,8 @@ async function orchestrateModelFindOneAndReplace(context, filter, replacement, o
5398
5546
  let nextReplacement = validateModelSchemaPayload({
5399
5547
  validateEnabled: context.validateEnabled,
5400
5548
  schemaCache: context.schemaCache,
5401
- schemaValidateFn: context.schemaValidateFn
5549
+ schemaValidateFn: context.schemaValidateFn,
5550
+ schemaError: context.schemaError
5402
5551
  }, replacement, options);
5403
5552
  nextReplacement = preserveModelReplaceCreatedAt(
5404
5553
  replacement,
@@ -5510,7 +5659,8 @@ async function orchestrateModelInsertBatch(context, docs, options) {
5510
5659
  record = validateModelSchemaPayload({
5511
5660
  validateEnabled: context.validateEnabled,
5512
5661
  schemaCache: context.schemaCache,
5513
- schemaValidateFn: context.schemaValidateFn
5662
+ schemaValidateFn: context.schemaValidateFn,
5663
+ schemaError: context.schemaError
5514
5664
  }, record, resolvedOptions, { index });
5515
5665
  record = applyModelInsertTimestamps(record, context.timestampsConfig, () => context.nowDate());
5516
5666
  record = applyModelInsertVersion(record, context.versionConfig);
@@ -5588,7 +5738,7 @@ async function orchestrateModelDeleteOne(context, filter, options) {
5588
5738
  let result;
5589
5739
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5590
5740
  result = await context.collection.updateOne(
5591
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5741
+ buildSoftDeleteFilter(filter, softDeleteConfig),
5592
5742
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5593
5743
  options
5594
5744
  );
@@ -5622,7 +5772,7 @@ async function orchestrateModelDeleteMany(context, filter, options) {
5622
5772
  let result;
5623
5773
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5624
5774
  result = await context.collection.updateMany(
5625
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5775
+ { $and: [filter ?? {}, buildModelVisibleSoftDeleteCondition(softDeleteConfig)] },
5626
5776
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5627
5777
  options
5628
5778
  );
@@ -5786,7 +5936,7 @@ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targ
5786
5936
  [inbound.localField]: { $in: targetValues }
5787
5937
  };
5788
5938
  const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
5789
- const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
5939
+ const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, buildModelVisibleSoftDeleteCondition(sourceSoftDelete)] } : baseQuery;
5790
5940
  try {
5791
5941
  const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
5792
5942
  if (!collection) {
@@ -5968,7 +6118,7 @@ async function executeProtectedRelationDelete(host, filter, options, force) {
5968
6118
  const deleteOptions = stripRelationUsageOptions(options);
5969
6119
  let target;
5970
6120
  try {
5971
- const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : { ...filter, [host.softDeleteConfig.field]: null };
6121
+ const targetFilter = force || !host.softDeleteConfig?.enabled ? filter : applyModelSoftDeleteFilter(filter, void 0, host.softDeleteConfig);
5972
6122
  const rawTarget = await host.collection.findOne(
5973
6123
  targetFilter,
5974
6124
  relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
@@ -6052,7 +6202,7 @@ var ModelInstance = class {
6052
6202
  this.softDeleteConfig = this._softDeleteConfig;
6053
6203
  this._versionConfig = resolveModelVersionConfig(options.definition);
6054
6204
  this._v1HooksFactory = resolveModelHooksFactory(options.definition);
6055
- scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6205
+ if (!this._schemaError) scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6056
6206
  runtime: this.runtime,
6057
6207
  dbName: options.dbName,
6058
6208
  poolName: options.poolName,
@@ -6112,46 +6262,6 @@ var ModelInstance = class {
6112
6262
  }
6113
6263
  return result;
6114
6264
  }
6115
- isVisibleBySoftDelete(doc, options) {
6116
- if (!doc || !this._softDeleteConfig?.enabled) {
6117
- return Boolean(doc);
6118
- }
6119
- const rawOptions = options ?? {};
6120
- if (rawOptions.withDeleted) {
6121
- return true;
6122
- }
6123
- const value = doc[this._softDeleteConfig.field];
6124
- if (rawOptions.onlyDeleted) {
6125
- return value !== void 0 && value !== null;
6126
- }
6127
- return value === void 0 || value === null;
6128
- }
6129
- filterVisibleBySoftDelete(docs, options) {
6130
- return docs.filter((doc) => this.isVisibleBySoftDelete(doc, options));
6131
- }
6132
- applySoftDeleteFindPageOptions(options) {
6133
- const rawOptions = options ?? {};
6134
- return {
6135
- ...rawOptions,
6136
- query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, this._softDeleteConfig)
6137
- };
6138
- }
6139
- applySoftDeleteAggregatePipeline(pipeline, options) {
6140
- if (!this._softDeleteConfig?.enabled) {
6141
- return pipeline ?? [];
6142
- }
6143
- const rawOptions = options ?? {};
6144
- if (rawOptions.withDeleted) {
6145
- return pipeline ?? [];
6146
- }
6147
- const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, this._softDeleteConfig);
6148
- const matchStage = { $match: softDeleteMatch };
6149
- const stages = [...pipeline ?? []];
6150
- if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
6151
- return [stages[0], matchStage, ...stages.slice(1)];
6152
- }
6153
- return [matchStage, ...stages];
6154
- }
6155
6265
  // ── public API ────────────────────────────────────────────────────────────────
6156
6266
  find(query, options) {
6157
6267
  return new PopulatePromise(async (paths) => {
@@ -6187,8 +6297,12 @@ var ModelInstance = class {
6187
6297
  }
6188
6298
  findOneById(id, options) {
6189
6299
  return new PopulatePromise(async (paths) => {
6190
- const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], (nextId, nextOptions) => this.collection.findOneById(nextId, nextOptions)) : await this.collection.findOneById(id, options);
6191
- return this.populateSingle(this.hydrateDocument(this.isVisibleBySoftDelete(doc, options) ? doc : null), paths);
6300
+ const lookupById = (nextId, nextOptions) => this._softDeleteConfig?.enabled ? this.collection.findOne(
6301
+ applyModelSoftDeleteFilter({ _id: parseRequiredObjectId(nextId) }, nextOptions, this._softDeleteConfig),
6302
+ nextOptions
6303
+ ) : this.collection.findOneById(nextId, nextOptions);
6304
+ const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], lookupById) : await lookupById(id, options);
6305
+ return this.populateSingle(this.hydrateDocument(doc), paths);
6192
6306
  });
6193
6307
  }
6194
6308
  findById(id, options) {
@@ -6196,13 +6310,25 @@ var ModelInstance = class {
6196
6310
  }
6197
6311
  findByIds(ids, options) {
6198
6312
  return new PopulatePromise(async (paths) => {
6199
- const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], (nextIds, nextOptions) => this.collection.findByIds(nextIds, nextOptions)) : await this.collection.findByIds(ids, options);
6200
- return this.populateDocuments(this.hydrateDocuments(this.filterVisibleBySoftDelete(docs, options)), paths);
6313
+ let stripField = false;
6314
+ const lookupByIds = (nextIds, nextOptions) => {
6315
+ const projection = withSoftDeleteProjection(nextOptions, this._softDeleteConfig);
6316
+ stripField = projection.stripField;
6317
+ return this.collection.findByIds(nextIds, projection.options);
6318
+ };
6319
+ const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], lookupByIds) : await lookupByIds(ids, options);
6320
+ const visible = filterVisibleBySoftDelete(docs, options, this._softDeleteConfig);
6321
+ const projected = stripField && this._softDeleteConfig ? visible.map((doc) => {
6322
+ const copy = { ...doc };
6323
+ delete copy[this._softDeleteConfig.field];
6324
+ return copy;
6325
+ }) : visible;
6326
+ return this.populateDocuments(this.hydrateDocuments(projected), paths);
6201
6327
  });
6202
6328
  }
6203
6329
  findPage(options) {
6204
6330
  return new PopulatePromise(async (paths) => {
6205
- const filteredOptions = this.applySoftDeleteFindPageOptions(options);
6331
+ const filteredOptions = applySoftDeleteFindPageOptions(options, this._softDeleteConfig);
6206
6332
  const result = this._v1HooksFactory ? await this.runV1HookedOperation("find", [filteredOptions], (nextOptions) => this.collection.findPage(nextOptions)) : await this.collection.findPage(filteredOptions);
6207
6333
  return {
6208
6334
  ...result,
@@ -6354,7 +6480,7 @@ var ModelInstance = class {
6354
6480
  return this.runV1HookedOperation("find", [key, filteredQuery, options], (nextKey, nextQuery, nextOptions) => this.collection.distinct(nextKey, nextQuery, nextOptions));
6355
6481
  }
6356
6482
  aggregate(pipeline, options) {
6357
- const filteredPipeline = this.applySoftDeleteAggregatePipeline(pipeline, options);
6483
+ const filteredPipeline = applySoftDeleteAggregatePipeline(pipeline, options, this._softDeleteConfig);
6358
6484
  return this.runV1HookedOperation("find", [filteredPipeline, options], (nextPipeline, nextOptions) => {
6359
6485
  const execute = () => this.collection.aggregate(nextPipeline, nextOptions);
6360
6486
  if (resolveAggregateWriteTarget(Array.isArray(nextPipeline) ? nextPipeline : [])) {
@@ -6483,13 +6609,14 @@ var ModelInstance = class {
6483
6609
  schemaValidationContext: {
6484
6610
  validateEnabled: this._validateEnabled,
6485
6611
  schemaCache: this._schemaCache,
6486
- schemaValidateFn: this._schemaValidateFn
6612
+ schemaValidateFn: this._schemaValidateFn,
6613
+ schemaError: this._schemaError
6487
6614
  },
6488
6615
  nowFactory: () => this.nowDate()
6489
6616
  }));
6490
6617
  }
6491
6618
  async removeDocument(document) {
6492
- return this.runModelWrite(() => removeModelDocument(this.collection, document));
6619
+ return this.runModelWrite(() => removeModelDocument(this.collection, document, (filter) => this.deleteOne(filter)));
6493
6620
  }
6494
6621
  applyDefaults(document) {
6495
6622
  return applyModelDefaults(this.definition, document);
@@ -6507,6 +6634,7 @@ var ModelInstance = class {
6507
6634
  validateEnabled: this._validateEnabled,
6508
6635
  schemaCache: this._schemaCache,
6509
6636
  schemaValidateFn: this._schemaValidateFn,
6637
+ schemaError: this._schemaError,
6510
6638
  hooksFactory: this._v1HooksFactory,
6511
6639
  runHook: (hookName, context) => this.runHook(hookName, context)
6512
6640
  };
@@ -6522,6 +6650,11 @@ var ModelInstance = class {
6522
6650
 
6523
6651
  // src/capabilities/transaction/index.ts
6524
6652
  import { randomBytes as randomBytes2 } from "node:crypto";
6653
+ function assertManagedTransactionWriteAllowed(options) {
6654
+ const session = options?.session;
6655
+ const managed = session?.__monSQLizeTransaction;
6656
+ if (typeof managed?.assertAcceptingWrites === "function") managed.assertAcceptingWrites();
6657
+ }
6525
6658
  function toPublicTransactionStatus(state) {
6526
6659
  return state === "active" ? "started" : state;
6527
6660
  }
@@ -6622,6 +6755,7 @@ var Transaction = class {
6622
6755
  this.options = options;
6623
6756
  this.id = `tx_${randomBytes2(8).toString("hex")}`;
6624
6757
  this.state = "pending";
6758
+ this.acceptingWrites = false;
6625
6759
  this.startedAt = null;
6626
6760
  this.timeoutTimer = null;
6627
6761
  this.pendingInvalidations = /* @__PURE__ */ new Set();
@@ -6640,6 +6774,7 @@ var Transaction = class {
6640
6774
  }
6641
6775
  this.session.startTransaction(this.options.transactionOptions);
6642
6776
  this.state = "active";
6777
+ this.acceptingWrites = true;
6643
6778
  this.startedAt = Date.now();
6644
6779
  const timeout = this.options.timeout ?? 3e4;
6645
6780
  if (timeout > 0) {
@@ -6662,6 +6797,7 @@ var Transaction = class {
6662
6797
  if (this.state !== "active") {
6663
6798
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
6664
6799
  }
6800
+ this.acceptingWrites = false;
6665
6801
  if (typeof this.session.commitTransaction === "function") {
6666
6802
  await commitTransactionWithRetry(this.session, this.options.logger);
6667
6803
  }
@@ -6687,6 +6823,7 @@ var Transaction = class {
6687
6823
  * @since v1.4.0
6688
6824
  */
6689
6825
  async abort() {
6826
+ this.acceptingWrites = false;
6690
6827
  if (this.state !== "pending" && this.state !== "active") {
6691
6828
  return;
6692
6829
  }
@@ -6711,10 +6848,16 @@ var Transaction = class {
6711
6848
  * @since v1.4.0
6712
6849
  */
6713
6850
  async end() {
6851
+ this.acceptingWrites = false;
6714
6852
  this.clearTimeout();
6715
6853
  this.options.lockManager?.releaseLocks(this.id);
6716
6854
  await this.session.endSession();
6717
6855
  }
6856
+ assertAcceptingWrites() {
6857
+ if (this.state !== "active" || !this.acceptingWrites) {
6858
+ throw createError(ErrorCodes.INVALID_OPERATION, `Managed transaction ${this.id} is no longer accepting writes.`);
6859
+ }
6860
+ }
6718
6861
  /**
6719
6862
  * Record a cache invalidation intent.
6720
6863
  * @since v1.4.0
@@ -6888,12 +7031,12 @@ var TransactionManager = class {
6888
7031
  return result;
6889
7032
  } catch (error) {
6890
7033
  lastError = error;
6891
- await transaction.abort();
6892
- this.recordStats(transaction, Date.now() - startedAt, false);
6893
7034
  if (transaction.state === "committed") {
6894
7035
  throw error;
6895
7036
  }
6896
- if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
7037
+ await transaction.abort();
7038
+ this.recordStats(transaction, Date.now() - startedAt, false);
7039
+ if (!enableRetry || attempt === maxRetries || isUnknownTransactionCommitResult(error) || !isTransientTransactionError(error)) {
6897
7040
  throw error;
6898
7041
  }
6899
7042
  await sleep(retryDelay * Math.pow(retryBackoff, attempt));
@@ -6994,22 +7137,34 @@ function stringifySessionId(id) {
6994
7137
  }
6995
7138
  return String(id);
6996
7139
  }
6997
- function isTransientTransactionError(error) {
6998
- if (!error || typeof error !== "object") {
6999
- return false;
7000
- }
7001
- const candidate = error;
7002
- if (typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("TransientTransactionError")) {
7003
- return true;
7140
+ function transactionErrorChain(error) {
7141
+ const chain = [];
7142
+ const seen = /* @__PURE__ */ new Set();
7143
+ let current = error;
7144
+ while (current && typeof current === "object" && chain.length < 8 && !seen.has(current)) {
7145
+ seen.add(current);
7146
+ const candidate = current;
7147
+ chain.push(candidate);
7148
+ current = candidate.cause;
7149
+ }
7150
+ return chain;
7151
+ }
7152
+ function hasTransactionErrorLabel(candidate, label) {
7153
+ if (Array.isArray(candidate.errorLabels) && candidate.errorLabels.includes(label)) return true;
7154
+ if (typeof candidate.hasErrorLabel === "function") {
7155
+ try {
7156
+ return Boolean(candidate.hasErrorLabel.call(candidate, label));
7157
+ } catch {
7158
+ }
7004
7159
  }
7005
- return candidate.code === 112 || candidate.code === 117;
7160
+ return false;
7006
7161
  }
7007
7162
  function isUnknownTransactionCommitResult(error) {
7008
- if (!error || typeof error !== "object") {
7009
- return false;
7010
- }
7011
- const candidate = error;
7012
- return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
7163
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "UnknownTransactionCommitResult"));
7164
+ }
7165
+ function isTransientTransactionError(error) {
7166
+ if (isUnknownTransactionCommitResult(error)) return false;
7167
+ return transactionErrorChain(error).some((candidate) => hasTransactionErrorLabel(candidate, "TransientTransactionError") || candidate.code === 112 || candidate.code === 117 || candidate.codeName === "WriteConflict" || candidate.codeName === "LockTimeout");
7013
7168
  }
7014
7169
  async function commitTransactionWithRetry(session, logger) {
7015
7170
  const maxAttempts = 3;
@@ -7525,26 +7680,27 @@ var CountQueue = class {
7525
7680
  * @returns The result of fn
7526
7681
  * @throws A controlled error when the queue is full or the wait times out
7527
7682
  */
7528
- async execute(fn) {
7683
+ async execute(fn, options = {}) {
7529
7684
  const startTime = Date.now();
7685
+ const signal = options.signal;
7686
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7530
7687
  if (this.running >= this.concurrency) {
7531
7688
  if (this.queue.length >= this.maxQueueSize) {
7532
7689
  this.stats.rejected += 1;
7533
7690
  throw createError(ErrorCodes.INVALID_OPERATION, `Count queue is full (${this.maxQueueSize})`);
7534
7691
  }
7535
7692
  this.stats.queued += 1;
7536
- await this._waitInQueue(startTime);
7693
+ await this._waitInQueue(startTime, signal);
7537
7694
  }
7695
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7538
7696
  this.running += 1;
7539
7697
  this.stats.executed += 1;
7540
- try {
7541
- const elapsed = Date.now() - startTime;
7542
- const remainingMs = Math.max(1, this.timeout - elapsed);
7543
- return await this._executeWithTimeout(fn, remainingMs);
7544
- } finally {
7698
+ const elapsed = Date.now() - startTime;
7699
+ const remainingMs = Math.max(1, this.timeout - elapsed);
7700
+ return this._executeWithTimeout(fn, remainingMs, signal, () => {
7545
7701
  this.running -= 1;
7546
7702
  this._wakeNext();
7547
- }
7703
+ });
7548
7704
  }
7549
7705
  /**
7550
7706
  * Get queue statistics (including live state).
@@ -7579,17 +7735,28 @@ var CountQueue = class {
7579
7735
  const entry = this.queue.shift();
7580
7736
  if (entry) {
7581
7737
  clearTimeout(entry.timer);
7738
+ entry.cleanup();
7582
7739
  entry.reject(createError(ErrorCodes.INVALID_OPERATION, "Count queue was cleared before execution"));
7583
7740
  }
7584
7741
  }
7585
7742
  }
7586
- _waitInQueue(startTime) {
7743
+ _waitInQueue(startTime, signal) {
7587
7744
  return new Promise((resolve, reject) => {
7588
7745
  let entry;
7746
+ const onAbort = () => {
7747
+ const index = this.queue.indexOf(entry);
7748
+ if (index !== -1) {
7749
+ this.queue.splice(index, 1);
7750
+ clearTimeout(entry.timer);
7751
+ entry.cleanup();
7752
+ reject(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
7753
+ }
7754
+ };
7589
7755
  const timer = setTimeout(() => {
7590
7756
  const index = this.queue.indexOf(entry);
7591
7757
  if (index !== -1) {
7592
7758
  this.queue.splice(index, 1);
7759
+ entry.cleanup();
7593
7760
  this.stats.timeout += 1;
7594
7761
  reject(createError(ErrorCodes.OPERATION_TIMEOUT, `Count queue wait timeout (${this.timeout}ms)`));
7595
7762
  }
@@ -7602,9 +7769,12 @@ var CountQueue = class {
7602
7769
  },
7603
7770
  reject,
7604
7771
  timer,
7605
- startTime
7772
+ startTime,
7773
+ cleanup: () => signal?.removeEventListener("abort", onAbort)
7606
7774
  };
7607
7775
  this.queue.push(entry);
7776
+ signal?.addEventListener("abort", onAbort, { once: true });
7777
+ if (signal?.aborted) onAbort();
7608
7778
  });
7609
7779
  }
7610
7780
  _wakeNext() {
@@ -7612,6 +7782,7 @@ var CountQueue = class {
7612
7782
  const entry = this.queue.shift();
7613
7783
  if (entry) {
7614
7784
  clearTimeout(entry.timer);
7785
+ entry.cleanup();
7615
7786
  entry.resolve();
7616
7787
  }
7617
7788
  }
@@ -7628,10 +7799,12 @@ var CountQueue = class {
7628
7799
  }
7629
7800
  return value;
7630
7801
  }
7631
- _executeWithTimeout(fn, timeoutMs = this.timeout) {
7802
+ _executeWithTimeout(fn, timeoutMs, signal, release) {
7632
7803
  let timer = null;
7633
7804
  const controller = new AbortController();
7634
- const timeoutPromise = new Promise((_, reject) => {
7805
+ let rejectCancellation;
7806
+ const cancellation = new Promise((_, reject) => {
7807
+ rejectCancellation = reject;
7635
7808
  timer = setTimeout(
7636
7809
  () => {
7637
7810
  controller.abort();
@@ -7641,14 +7814,22 @@ var CountQueue = class {
7641
7814
  timeoutMs
7642
7815
  );
7643
7816
  });
7817
+ const onAbort = () => {
7818
+ controller.abort();
7819
+ rejectCancellation(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
7820
+ };
7821
+ signal?.addEventListener("abort", onAbort, { once: true });
7644
7822
  let taskPromise;
7645
7823
  try {
7646
7824
  taskPromise = Promise.resolve(fn(controller.signal));
7647
7825
  } catch (error) {
7648
7826
  taskPromise = Promise.reject(error);
7649
7827
  }
7650
- return Promise.race([taskPromise, timeoutPromise]).finally(() => {
7828
+ void taskPromise.then(release, release);
7829
+ if (signal?.aborted) onAbort();
7830
+ return Promise.race([taskPromise, cancellation]).finally(() => {
7651
7831
  if (timer !== null) clearTimeout(timer);
7832
+ signal?.removeEventListener("abort", onAbort);
7652
7833
  });
7653
7834
  }
7654
7835
  _updateWaitTimeStats(waitTime) {
@@ -8177,6 +8358,9 @@ var ConnectionPoolManager = class {
8177
8358
  this._closed = false;
8178
8359
  this._configs = /* @__PURE__ */ new Map();
8179
8360
  this._pendingAdds = /* @__PURE__ */ new Set();
8361
+ this._pendingAddTasks = /* @__PURE__ */ new Set();
8362
+ this._generation = 0;
8363
+ this._closePromise = null;
8180
8364
  this.logger = options.logger ?? null;
8181
8365
  this.maxPoolsCount = options.maxPoolsCount ?? 10;
8182
8366
  this.strategy = options.poolStrategy ?? "auto";
@@ -8203,35 +8387,21 @@ var ConnectionPoolManager = class {
8203
8387
  */
8204
8388
  async addPool(config) {
8205
8389
  validatePoolConfig(config);
8390
+ if (this._closed) {
8391
+ throw createError(ErrorCodes.INVALID_OPERATION, "Connection pool manager is closed");
8392
+ }
8206
8393
  if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
8207
8394
  throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8208
8395
  }
8209
- if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
8396
+ if (this.maxPoolsCount > 0 && this.pools.size + this._pendingAdds.size >= this.maxPoolsCount) {
8210
8397
  throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
8211
8398
  }
8212
8399
  this._pendingAdds.add(config.name);
8400
+ const generation = this._generation;
8401
+ const task = this._finishAddPool(config, generation);
8402
+ this._pendingAddTasks.add(task);
8213
8403
  try {
8214
- const client = await this.clientFactory(config);
8215
- if (this.pools.has(config.name)) {
8216
- await client.close().catch(() => {
8217
- });
8218
- throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8219
- }
8220
- this.pools.set(config.name, {
8221
- client,
8222
- config,
8223
- createdAt: Date.now()
8224
- });
8225
- this.healthStatus.set(config.name, {
8226
- status: "up",
8227
- consecutiveFailures: 0,
8228
- lastCheckTime: null,
8229
- lastError: null,
8230
- uptime: 0
8231
- });
8232
- this.stats.set(config.name, createEmptyPoolStats(config.name));
8233
- this._configs.set(config.name, config);
8234
- this._healthChecker.register(config.name, config.healthCheck ?? {});
8404
+ await task;
8235
8405
  } catch (err) {
8236
8406
  const error = err;
8237
8407
  const msg = error.message ?? "";
@@ -8245,7 +8415,30 @@ var ConnectionPoolManager = class {
8245
8415
  throw err;
8246
8416
  } finally {
8247
8417
  this._pendingAdds.delete(config.name);
8418
+ this._pendingAddTasks.delete(task);
8419
+ }
8420
+ }
8421
+ async _finishAddPool(config, generation) {
8422
+ const client = await this.clientFactory(config);
8423
+ if (this._closed || generation !== this._generation || this.pools.has(config.name)) {
8424
+ await client.close().catch(() => {
8425
+ });
8426
+ throw createError(
8427
+ this._closed || generation !== this._generation ? ErrorCodes.INVALID_OPERATION : ErrorCodes.INVALID_CONFIG,
8428
+ this._closed || generation !== this._generation ? "Connection pool manager closed while adding a pool" : `Pool '${config.name}' already exists`
8429
+ );
8248
8430
  }
8431
+ this.pools.set(config.name, { client, config, createdAt: Date.now() });
8432
+ this.healthStatus.set(config.name, {
8433
+ status: "up",
8434
+ consecutiveFailures: 0,
8435
+ lastCheckTime: null,
8436
+ lastError: null,
8437
+ uptime: 0
8438
+ });
8439
+ this.stats.set(config.name, createEmptyPoolStats(config.name));
8440
+ this._configs.set(config.name, config);
8441
+ this._healthChecker.register(config.name, config.healthCheck ?? {});
8249
8442
  }
8250
8443
  /**
8251
8444
  * Remove a connection pool.
@@ -8402,16 +8595,24 @@ var ConnectionPoolManager = class {
8402
8595
  * @since v1.0.8
8403
8596
  */
8404
8597
  async close() {
8598
+ if (this._closePromise) return this._closePromise;
8599
+ this._closed = true;
8600
+ this._generation += 1;
8601
+ this._closePromise = this._closeAllPools();
8602
+ return this._closePromise;
8603
+ }
8604
+ async _closeAllPools() {
8405
8605
  this.stopHealthCheck();
8406
8606
  this._healthChecker.stop();
8407
8607
  this._stats.close();
8608
+ await Promise.allSettled([...this._pendingAddTasks]);
8408
8609
  for (const pool of this.pools.values()) {
8409
8610
  await pool.client.close();
8410
8611
  }
8411
8612
  this.pools.clear();
8412
8613
  this.healthStatus.clear();
8413
8614
  this.stats.clear();
8414
- this._closed = true;
8615
+ this._configs.clear();
8415
8616
  }
8416
8617
  // ─── v1 compat methods ───────────────────────────────────────────────────────────
8417
8618
  /** v1 compat: get pool health Map */
@@ -8599,6 +8800,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
8599
8800
  logger
8600
8801
  });
8601
8802
  } catch (err) {
8803
+ if (err?.code === ErrorCodes.INVALID_CONFIG) throw err;
8602
8804
  logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
8603
8805
  return null;
8604
8806
  }
@@ -8677,6 +8879,33 @@ async function loadModelFiles(options, logger, opts = {}) {
8677
8879
  }
8678
8880
  }
8679
8881
 
8882
+ // src/entry/runtime-health.ts
8883
+ function summarizeRuntimeHealth(cacheOption, cache, poolManager, invalidator) {
8884
+ const cacheStats = cache.getStats?.();
8885
+ const poolHealth = poolManager?.getHealthStatus();
8886
+ const distributedStats = invalidator?.getStats() ?? null;
8887
+ const cacheInput = cacheOption;
8888
+ const cacheEnabled = cacheInput?.enabled !== false;
8889
+ const distributedEnabled = cacheInput?.distributed !== void 0 && cacheInput.distributed.enabled !== false;
8890
+ const cacheProbe = cacheEnabled ? cache.getHealthStatus?.() : void 0;
8891
+ const cacheProbeStatus = cacheProbe === true || cacheProbe === "up" ? "up" : cacheProbe === false || cacheProbe === "down" ? "down" : "unknown";
8892
+ const redisState = invalidator;
8893
+ const redisStatuses = [redisState?.pub?.status, redisState?.sub?.status];
8894
+ const distributedStatus = !distributedEnabled || !invalidator ? "unknown" : redisStatuses.every((status) => status === "ready") ? "up" : redisStatuses.some((status) => status === "end" || status === "close") ? "down" : "unknown";
8895
+ const poolStates = Object.values(poolHealth ?? {}).map((health) => health.status);
8896
+ const poolsStatus = poolStates.length === 0 ? "unknown" : poolStates.some((status) => status === "down") ? "down" : poolStates.every((status) => status === "up") ? "up" : "unknown";
8897
+ return {
8898
+ cacheStats,
8899
+ poolHealth,
8900
+ distributedStats,
8901
+ cacheEnabled,
8902
+ distributedEnabled,
8903
+ cacheProbeStatus,
8904
+ distributedStatus,
8905
+ poolsStatus
8906
+ };
8907
+ }
8908
+
8680
8909
  // src/entry/runtime-defaults.ts
8681
8910
  function isPlainObject3(value) {
8682
8911
  if (value === null || typeof value !== "object") return false;
@@ -9327,36 +9556,36 @@ var FUNC_REGEX = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|S
9327
9556
  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;
9328
9557
  function compileInnerExpression(expression) {
9329
9558
  const expr2 = expression.trim();
9330
- const funcMatch = expr2.match(FUNC_REGEX);
9331
- if (funcMatch) {
9332
- return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
9333
- }
9334
- const andParts = splitTopLevel2(expr2, "&&");
9335
- if (andParts.length > 1) {
9336
- return { $and: andParts.map((part) => compileInnerExpression(part.trim())) };
9559
+ if (!expr2) throw createError(ErrorCodes.INVALID_EXPRESSION, "Expression cannot be empty");
9560
+ const positions = topLevelPositions(expr2);
9561
+ const unwrapped = stripOuterParentheses(expr2);
9562
+ if (unwrapped !== expr2) return compileInnerExpression(unwrapped);
9563
+ if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(expr2)) return Number(expr2);
9564
+ const conditional = findConditional(expr2, positions);
9565
+ if (conditional) {
9566
+ return { $cond: {
9567
+ if: compileInnerExpression(expr2.slice(0, conditional.question)),
9568
+ then: compileInnerExpression(expr2.slice(conditional.question + 1, conditional.colon)),
9569
+ else: compileInnerExpression(expr2.slice(conditional.colon + 1))
9570
+ } };
9571
+ }
9572
+ const nullCoalParts = splitTopLevel2(expr2, "??", positions);
9573
+ if (nullCoalParts.length > 1) {
9574
+ return nullCoalParts.slice(1).reduce(
9575
+ (left, right) => ({ $ifNull: [left, compileInnerExpression(right)] }),
9576
+ compileInnerExpression(nullCoalParts[0])
9577
+ );
9337
9578
  }
9338
- const orParts = splitTopLevel2(expr2, "||");
9579
+ const orParts = splitTopLevel2(expr2, "||", positions);
9339
9580
  if (orParts.length > 1) {
9340
- return { $or: orParts.map((part) => compileInnerExpression(part.trim())) };
9341
- }
9342
- const ternary = /^([^?]+)\s*\?\s*([^:]+)\s*:\s*(.+)$/.exec(expr2);
9343
- if (ternary) {
9344
- const [, condition, thenPart, elsePart] = ternary;
9345
- return {
9346
- $cond: {
9347
- if: compileInnerExpression(condition.trim()),
9348
- then: parseThenElse(thenPart.trim()),
9349
- else: parseThenElse(elsePart.trim())
9350
- }
9351
- };
9581
+ return { $or: orParts.map((part) => compileInnerExpression(part)) };
9352
9582
  }
9353
- const nullCoalParts = splitTopLevel2(expr2, "??");
9354
- if (nullCoalParts.length > 1) {
9355
- return { $ifNull: [parseValue(nullCoalParts[0].trim()), parseValue(nullCoalParts[1].trim())] };
9583
+ const andParts = splitTopLevel2(expr2, "&&", positions);
9584
+ if (andParts.length > 1) {
9585
+ return { $and: andParts.map((part) => compileInnerExpression(part)) };
9356
9586
  }
9357
- const cmpMatch = /^(.+?)\s*(===|!==|>=|<=|>|<)\s*(.+)$/.exec(expr2);
9358
- if (cmpMatch) {
9359
- const [, left, operator, right] = cmpMatch;
9587
+ const comparison = findBinaryOperator(expr2, positions, ["===", "!==", ">=", "<=", ">", "<"]);
9588
+ if (comparison) {
9360
9589
  const operatorMap = {
9361
9590
  "===": "$eq",
9362
9591
  "!==": "$ne",
@@ -9365,24 +9594,39 @@ function compileInnerExpression(expression) {
9365
9594
  ">": "$gt",
9366
9595
  "<": "$lt"
9367
9596
  };
9368
- return {
9369
- [operatorMap[operator]]: [
9370
- compileInnerExpression(left.trim()),
9371
- compileInnerExpression(right.trim())
9372
- ]
9373
- };
9597
+ return { [operatorMap[comparison.operator]]: [
9598
+ compileInnerExpression(expr2.slice(0, comparison.index)),
9599
+ compileInnerExpression(expr2.slice(comparison.index + comparison.operator.length))
9600
+ ] };
9601
+ }
9602
+ const additive = findBinaryOperator(expr2, positions, ["+", "-"], true);
9603
+ if (additive) {
9604
+ return { [additive.operator === "+" ? "$add" : "$subtract"]: [
9605
+ compileInnerExpression(expr2.slice(0, additive.index)),
9606
+ compileInnerExpression(expr2.slice(additive.index + 1))
9607
+ ] };
9608
+ }
9609
+ const multiplicative = findBinaryOperator(expr2, positions, ["*", "/", "%"], true);
9610
+ if (multiplicative) {
9611
+ const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
9612
+ return { [operatorMap[multiplicative.operator]]: [
9613
+ compileInnerExpression(expr2.slice(0, multiplicative.index)),
9614
+ compileInnerExpression(expr2.slice(multiplicative.index + 1))
9615
+ ] };
9374
9616
  }
9375
- const addSubMatch = /^(.+?)\s*([+\-])\s*(.+)$/.exec(expr2);
9376
- if (addSubMatch) {
9377
- const [, left, operator, right] = addSubMatch;
9378
- const operatorMap = { "+": "$add", "-": "$subtract" };
9379
- return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
9617
+ if (expr2.startsWith("-")) {
9618
+ return { $multiply: [-1, compileInnerExpression(expr2.slice(1))] };
9380
9619
  }
9381
- const mulDivMatch = /^(.+?)\s*([*\/%])\s*(.+)$/.exec(expr2);
9382
- if (mulDivMatch) {
9383
- const [, left, operator, right] = mulDivMatch;
9384
- const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
9385
- return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
9620
+ const funcMatch = expr2.match(FUNC_REGEX);
9621
+ if (funcMatch) {
9622
+ try {
9623
+ return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
9624
+ } catch (error) {
9625
+ if (error && typeof error === "object" && "code" in error && error.code === ErrorCodes.INVALID_EXPRESSION) {
9626
+ throw error;
9627
+ }
9628
+ throw createError(ErrorCodes.INVALID_EXPRESSION, `Invalid arguments for ${funcMatch[1].toUpperCase()}`);
9629
+ }
9386
9630
  }
9387
9631
  const genericFuncCallRe = /^[A-Za-z_][A-Za-z0-9_]*\s*\(.+\)$/;
9388
9632
  if (genericFuncCallRe.test(expr2)) {
@@ -9391,9 +9635,6 @@ function compileInnerExpression(expression) {
9391
9635
  }
9392
9636
  return parseValue(expr2);
9393
9637
  }
9394
- function parseThenElse(source) {
9395
- return source.includes("?") && source.includes(":") ? compileInnerExpression(source) : parseValue(source);
9396
- }
9397
9638
  function parseValue(value) {
9398
9639
  const normalized = stripOuterParentheses(value.trim());
9399
9640
  if (normalized.startsWith("'") && normalized.endsWith("'") || normalized.startsWith('"') && normalized.endsWith('"')) {
@@ -9403,18 +9644,22 @@ function parseValue(value) {
9403
9644
  if (normalized === "true") return true;
9404
9645
  if (normalized === "false") return false;
9405
9646
  if (!isNaN(Number(normalized)) && normalized !== "") return Number(normalized);
9406
- if (IS_FUNC_CALL_RE.test(normalized)) {
9647
+ if (normalized !== value.trim() || IS_FUNC_CALL_RE.test(normalized) || FUNC_REGEX.test(normalized)) {
9407
9648
  return compileInnerExpression(normalized);
9408
9649
  }
9409
- if (normalized.startsWith("$")) return normalized;
9410
- return `$${normalized}`;
9411
- }
9412
- function parseOperand(value) {
9413
- const normalized = stripOuterParentheses(value);
9414
- if (/[+\-*/%]/.test(normalized)) {
9650
+ if (normalized.startsWith("[") || normalized.startsWith("{")) {
9651
+ try {
9652
+ return JSON.parse(normalized);
9653
+ } catch {
9654
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Invalid array or object literal");
9655
+ }
9656
+ }
9657
+ const positions = topLevelPositions(normalized);
9658
+ if (positions.some((index) => /[?+\-*/%><&|]/.test(normalized[index]))) {
9415
9659
  return compileInnerExpression(normalized);
9416
9660
  }
9417
- return parseValue(normalized);
9661
+ if (normalized.startsWith("$")) return normalized;
9662
+ return `$${normalized}`;
9418
9663
  }
9419
9664
  function dispatchFunction(name, argsStr) {
9420
9665
  const args = splitArgsStr(argsStr);
@@ -9716,72 +9961,82 @@ function compileMapExpression(exprStr, varName) {
9716
9961
  return compileInnerExpression(replaced);
9717
9962
  }
9718
9963
  function splitArgsStr(argsStr) {
9719
- const args = [];
9720
- let current = "";
9721
- let inString = false;
9722
- let stringChar = "";
9723
- let parenDepth = 0;
9724
- for (let index = 0; index < argsStr.length; index++) {
9725
- const ch = argsStr[index];
9726
- if ((ch === '"' || ch === "'") && (index === 0 || argsStr[index - 1] !== "\\")) {
9727
- if (!inString) {
9728
- inString = true;
9729
- stringChar = ch;
9730
- } else if (ch === stringChar) {
9731
- inString = false;
9732
- stringChar = "";
9733
- }
9734
- current += ch;
9735
- } else if (ch === "(" && !inString) {
9736
- parenDepth++;
9737
- current += ch;
9738
- } else if (ch === ")" && !inString) {
9739
- parenDepth--;
9740
- current += ch;
9741
- } else if (ch === "," && !inString && parenDepth === 0) {
9742
- args.push(current.trim());
9743
- current = "";
9744
- } else {
9745
- current += ch;
9746
- }
9747
- }
9748
- if (current.trim()) args.push(current.trim());
9749
- return args;
9964
+ if (!argsStr.trim()) return [];
9965
+ return splitTopLevel2(argsStr, ",");
9750
9966
  }
9751
- function splitTopLevel2(source, separator) {
9752
- const parts = [];
9753
- let current = "";
9754
- let parenDepth = 0;
9755
- let inString = false;
9756
- let stringChar = "";
9967
+ function topLevelPositions(source) {
9968
+ const positions = [];
9969
+ const stack = [];
9970
+ let quote = null;
9757
9971
  for (let index = 0; index < source.length; index++) {
9758
9972
  const ch = source[index];
9759
- if ((ch === '"' || ch === "'") && (index === 0 || source[index - 1] !== "\\")) {
9760
- if (!inString) {
9761
- inString = true;
9762
- stringChar = ch;
9763
- } else if (ch === stringChar) {
9764
- inString = false;
9765
- stringChar = "";
9973
+ if (quote) {
9974
+ if (ch === quote && source[index - 1] !== "\\") quote = null;
9975
+ continue;
9976
+ }
9977
+ if (ch === '"' || ch === "'") {
9978
+ quote = ch;
9979
+ continue;
9980
+ }
9981
+ if (ch === "(" || ch === "[" || ch === "{") {
9982
+ stack.push(ch);
9983
+ continue;
9984
+ }
9985
+ if (ch === ")" || ch === "]" || ch === "}") {
9986
+ const opening = stack.pop();
9987
+ if (ch === ")" && opening !== "(" || ch === "]" && opening !== "[" || ch === "}" && opening !== "{") {
9988
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Unbalanced expression delimiters");
9766
9989
  }
9767
- current += ch;
9768
- } else if (!inString && ch === "(") {
9769
- parenDepth++;
9770
- current += ch;
9771
- } else if (!inString && ch === ")") {
9772
- parenDepth--;
9773
- current += ch;
9774
- } else if (!inString && parenDepth === 0 && source.startsWith(separator, index)) {
9775
- parts.push(current);
9776
- current = "";
9777
- index += separator.length - 1;
9778
- } else {
9779
- current += ch;
9990
+ continue;
9780
9991
  }
9992
+ if (stack.length === 0) positions.push(index);
9993
+ }
9994
+ if (quote || stack.length > 0) {
9995
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Unbalanced expression delimiters");
9996
+ }
9997
+ return positions;
9998
+ }
9999
+ function splitTopLevel2(source, separator, positions = topLevelPositions(source)) {
10000
+ const parts = [];
10001
+ let start = 0;
10002
+ for (const index of positions) {
10003
+ if (index < start || !source.startsWith(separator, index)) continue;
10004
+ parts.push(source.slice(start, index).trim());
10005
+ start = index + separator.length;
10006
+ }
10007
+ if (parts.length === 0) return [source];
10008
+ parts.push(source.slice(start).trim());
10009
+ if (parts.some((part) => !part)) {
10010
+ throw createError(ErrorCodes.INVALID_EXPRESSION, `Missing operand around ${separator}`);
9781
10011
  }
9782
- parts.push(current);
9783
10012
  return parts;
9784
10013
  }
10014
+ function findConditional(source, positions) {
10015
+ const question = positions.find((index) => source[index] === "?" && source[index - 1] !== "?" && source[index + 1] !== "?");
10016
+ if (question === void 0) return null;
10017
+ let nested = 0;
10018
+ for (const index of positions) {
10019
+ if (index <= question) continue;
10020
+ if (source[index] === "?" && source[index - 1] !== "?" && source[index + 1] !== "?") nested++;
10021
+ if (source[index] === ":") {
10022
+ if (nested === 0) return { question, colon: index };
10023
+ nested--;
10024
+ }
10025
+ }
10026
+ throw createError(ErrorCodes.INVALID_EXPRESSION, "Conditional expression is missing a branch");
10027
+ }
10028
+ function findBinaryOperator(source, positions, operators, rightmost = false) {
10029
+ let match = null;
10030
+ for (const index of positions) {
10031
+ const operator = operators.find((candidate) => source.startsWith(candidate, index));
10032
+ if (!operator) continue;
10033
+ if ((operator === "+" || operator === "-") && !source.slice(0, index).trim()) continue;
10034
+ if ((operator === "+" || operator === "-") && /[+\-*/%<>=!?&|:,]$/.test(source.slice(0, index).trimEnd())) continue;
10035
+ match = { index, operator };
10036
+ if (!rightmost) break;
10037
+ }
10038
+ return match;
10039
+ }
9785
10040
  function stripOuterParentheses(source) {
9786
10041
  let normalized = source.trim();
9787
10042
  while (normalized.startsWith("(") && normalized.endsWith(")") && isWrappedByOuterParentheses(normalized)) {
@@ -9827,10 +10082,15 @@ function isExpressionObject(value) {
9827
10082
  value && typeof value === "object" && "__expr__" in value && typeof value.__expr__ === "string" && "__compiled__" in value && typeof value.__compiled__ === "boolean"
9828
10083
  );
9829
10084
  }
10085
+ function isPlainExpressionRecord(value) {
10086
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
10087
+ const prototype = Object.getPrototypeOf(value);
10088
+ return prototype === Object.prototype || prototype === null;
10089
+ }
9830
10090
  function hasExpressionInObject(value) {
9831
10091
  if (isExpressionObject(value)) return true;
9832
- if (!value || typeof value !== "object") return false;
9833
10092
  if (Array.isArray(value)) return value.some((item) => hasExpressionInObject(item));
10093
+ if (!isPlainExpressionRecord(value)) return false;
9834
10094
  return Object.values(value).some((item) => hasExpressionInObject(item));
9835
10095
  }
9836
10096
  function hasExpressionInPipeline(pipeline) {
@@ -9847,7 +10107,7 @@ function transformExpressions(value, context) {
9847
10107
  if (Array.isArray(value)) {
9848
10108
  return value.map((item) => transformStageEntry(item));
9849
10109
  }
9850
- if (!value || typeof value !== "object") return value;
10110
+ if (!isPlainExpressionRecord(value)) return value;
9851
10111
  const entries = Object.entries(value);
9852
10112
  const result = {};
9853
10113
  for (const [key, current] of entries) {
@@ -9856,7 +10116,7 @@ function transformExpressions(value, context) {
9856
10116
  return result;
9857
10117
  }
9858
10118
  function transformStageEntry(stage) {
9859
- if (!stage || typeof stage !== "object" || Array.isArray(stage)) {
10119
+ if (!isPlainExpressionRecord(stage)) {
9860
10120
  return transformExpressions(stage, "project");
9861
10121
  }
9862
10122
  const entries = Object.entries(stage);
@@ -9880,7 +10140,7 @@ function transformStageValue(value, context) {
9880
10140
  if (Array.isArray(value)) {
9881
10141
  return value.map((item) => transformStageValue(item, context));
9882
10142
  }
9883
- if (!value || typeof value !== "object") return value;
10143
+ if (!isPlainExpressionRecord(value)) return value;
9884
10144
  const entries = Object.entries(value);
9885
10145
  const result = {};
9886
10146
  for (const [key, current] of entries) {
@@ -10811,6 +11071,7 @@ var AggregateChain = class {
10811
11071
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).explain(verbosity === true ? "queryPlanner" : verbosity);
10812
11072
  }
10813
11073
  stream() {
11074
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10814
11075
  const stream = this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).stream();
10815
11076
  const onWriteComplete = this.writePipelineHooks?.onWriteComplete;
10816
11077
  if (onWriteComplete) {
@@ -10828,6 +11089,7 @@ var AggregateChain = class {
10828
11089
  throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
10829
11090
  }
10830
11091
  this.executed = true;
11092
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10831
11093
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray().then(async (result) => {
10832
11094
  await this.writePipelineHooks?.onWriteComplete?.();
10833
11095
  return result;
@@ -11144,48 +11406,59 @@ function stripWriteCacheControlOptions(options) {
11144
11406
 
11145
11407
  // src/adapters/mongodb/writes/write-basic.ts
11146
11408
  async function insertOneDocument(collection, ...args) {
11409
+ assertManagedTransactionWriteAllowed(args[1]);
11147
11410
  return collection.insertOne(...args);
11148
11411
  }
11149
11412
  async function insertManyDocuments(collection, ...args) {
11413
+ assertManagedTransactionWriteAllowed(args[1]);
11150
11414
  return collection.insertMany(...args);
11151
11415
  }
11152
11416
  async function updateOneDocument(collection, ...args) {
11417
+ assertManagedTransactionWriteAllowed(args[2]);
11153
11418
  return collection.updateOne(...args);
11154
11419
  }
11155
11420
  async function updateManyDocuments(collection, ...args) {
11421
+ assertManagedTransactionWriteAllowed(args[2]);
11156
11422
  return collection.updateMany(...args);
11157
11423
  }
11158
11424
  async function replaceOneDocument(collection, ...args) {
11425
+ assertManagedTransactionWriteAllowed(args[2]);
11159
11426
  return collection.replaceOne(...args);
11160
11427
  }
11161
11428
  async function findOneAndUpdateDocument(collection, filter, update, options) {
11429
+ assertManagedTransactionWriteAllowed(options);
11162
11430
  if (options !== void 0) {
11163
11431
  return collection.findOneAndUpdate(filter, update, options);
11164
11432
  }
11165
11433
  return collection.findOneAndUpdate(filter, update);
11166
11434
  }
11167
11435
  async function findOneAndReplaceDocument(collection, filter, replacement, options) {
11436
+ assertManagedTransactionWriteAllowed(options);
11168
11437
  if (options !== void 0) {
11169
11438
  return collection.findOneAndReplace(filter, replacement, options);
11170
11439
  }
11171
11440
  return collection.findOneAndReplace(filter, replacement);
11172
11441
  }
11173
11442
  async function findOneAndDeleteDocument(collection, filter, options) {
11443
+ assertManagedTransactionWriteAllowed(options);
11174
11444
  if (options !== void 0) {
11175
11445
  return collection.findOneAndDelete(filter, options);
11176
11446
  }
11177
11447
  return collection.findOneAndDelete(filter);
11178
11448
  }
11179
11449
  async function upsertOneDocument(collection, filter, update, options = {}) {
11450
+ assertManagedTransactionWriteAllowed(options);
11180
11451
  return collection.updateOne(filter, update, {
11181
11452
  ...options,
11182
11453
  upsert: true
11183
11454
  });
11184
11455
  }
11185
11456
  async function deleteOneDocument(collection, ...args) {
11457
+ assertManagedTransactionWriteAllowed(args[1]);
11186
11458
  return collection.deleteOne(...args);
11187
11459
  }
11188
11460
  async function deleteManyDocuments(collection, ...args) {
11461
+ assertManagedTransactionWriteAllowed(args[1]);
11189
11462
  return collection.deleteMany(...args);
11190
11463
  }
11191
11464
  async function incrementOneDocument(collection, filter, field, incrementOrOptions, maybeOptions) {
@@ -11212,6 +11485,7 @@ async function incrementOneDocument(collection, filter, field, incrementOrOption
11212
11485
  includeResultMetadata: true
11213
11486
  };
11214
11487
  if (normalizedProjection) findOptions.projection = normalizedProjection;
11488
+ assertManagedTransactionWriteAllowed(findOptions);
11215
11489
  const rawResult = await collection.findOneAndUpdate(
11216
11490
  filter,
11217
11491
  updateDocument,
@@ -11344,14 +11618,25 @@ async function runWithConcurrency(items, concurrency, worker) {
11344
11618
  return;
11345
11619
  }
11346
11620
  let nextIndex = 0;
11621
+ let stopAcquiring = false;
11622
+ let firstFatal;
11347
11623
  const workerCount = Math.min(concurrency, items.length);
11348
11624
  await Promise.all(Array.from({ length: workerCount }, async () => {
11349
- while (nextIndex < items.length) {
11625
+ while (!stopAcquiring && nextIndex < items.length) {
11350
11626
  const item = items[nextIndex];
11351
11627
  nextIndex += 1;
11352
- await worker(item);
11628
+ try {
11629
+ await worker(item);
11630
+ } catch (error) {
11631
+ if (!stopAcquiring) {
11632
+ firstFatal = error;
11633
+ stopAcquiring = true;
11634
+ }
11635
+ break;
11636
+ }
11353
11637
  }
11354
11638
  }));
11639
+ if (stopAcquiring) throw firstFatal;
11355
11640
  }
11356
11641
  async function insertBatchDocuments(collection, documents, options = {}) {
11357
11642
  if (!Array.isArray(documents)) {
@@ -11398,12 +11683,14 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11398
11683
  insertedIds: {},
11399
11684
  retries: []
11400
11685
  };
11686
+ assertManagedTransactionWriteAllowed(driverOptions);
11401
11687
  await runWithConcurrency(batchPlans, effectiveConcurrency, async ({ batchIndex, batch, batchOffset: batchOffset2 }) => {
11402
11688
  let pendingBatch = batch;
11403
11689
  let pendingIndexes = batch.map((_, index) => index);
11404
11690
  let attempts = 0;
11405
11691
  while (true) {
11406
11692
  try {
11693
+ assertManagedTransactionWriteAllowed(driverOptions);
11407
11694
  const batchResult = await collection.insertMany(pendingBatch, {
11408
11695
  ...driverOptions,
11409
11696
  ordered
@@ -11423,6 +11710,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11423
11710
  });
11424
11711
  break;
11425
11712
  } catch (cause) {
11713
+ assertManagedTransactionWriteAllowed(driverOptions);
11426
11714
  const errorRecord = {
11427
11715
  batchIndex,
11428
11716
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11494,6 +11782,10 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11494
11782
  });
11495
11783
  return result;
11496
11784
  }
11785
+ function batchWriteFilter(filter, batch) {
11786
+ const ids = { _id: { $in: batch } };
11787
+ return Object.prototype.hasOwnProperty.call(filter, "_id") ? { $and: [filter, ids] } : { ...filter, ...ids };
11788
+ }
11497
11789
  async function updateBatchDocuments(collection, filter, update, options = {}) {
11498
11790
  if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
11499
11791
  throw createError(ErrorCodes.INVALID_ARGUMENT, "filter must be a non-array object");
@@ -11537,6 +11829,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11537
11829
  "updateBatch retry requires an idempotent update; non-idempotent operators such as $inc/$push cannot be safely replayed"
11538
11830
  );
11539
11831
  }
11832
+ assertManagedTransactionWriteAllowed(driverOptions);
11540
11833
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11541
11834
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11542
11835
  const result = {
@@ -11553,8 +11846,9 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11553
11846
  let attempts = 0;
11554
11847
  while (true) {
11555
11848
  try {
11849
+ assertManagedTransactionWriteAllowed(driverOptions);
11556
11850
  const batchResult = await collection.updateMany(
11557
- { _id: { $in: batch } },
11851
+ batchWriteFilter(filter, batch),
11558
11852
  update,
11559
11853
  driverOptions
11560
11854
  );
@@ -11572,6 +11866,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11572
11866
  });
11573
11867
  break;
11574
11868
  } catch (cause) {
11869
+ assertManagedTransactionWriteAllowed(driverOptions);
11575
11870
  const errorRecord = {
11576
11871
  batchIndex,
11577
11872
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11634,6 +11929,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11634
11929
  if (!Number.isInteger(retryAttempts) || retryAttempts < 0) {
11635
11930
  throw createError(ErrorCodes.INVALID_ARGUMENT, "retryAttempts must be a non-negative integer");
11636
11931
  }
11932
+ assertManagedTransactionWriteAllowed(driverOptions);
11637
11933
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11638
11934
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11639
11935
  const result = {
@@ -11648,8 +11944,9 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11648
11944
  let attempts = 0;
11649
11945
  while (true) {
11650
11946
  try {
11947
+ assertManagedTransactionWriteAllowed(driverOptions);
11651
11948
  const batchResult = await collection.deleteMany(
11652
- { _id: { $in: batch } },
11949
+ batchWriteFilter(filter, batch),
11653
11950
  driverOptions
11654
11951
  );
11655
11952
  result.deletedCount += batchResult.deletedCount;
@@ -11664,6 +11961,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11664
11961
  });
11665
11962
  break;
11666
11963
  } catch (cause) {
11964
+ assertManagedTransactionWriteAllowed(driverOptions);
11667
11965
  const errorRecord = {
11668
11966
  batchIndex,
11669
11967
  message: cause instanceof Error ? cause.message : String(cause),
@@ -12192,9 +12490,11 @@ var MongoCollectionAccessor = class {
12192
12490
  const normalizedQuery = this._cvFilter(query);
12193
12491
  const maxTimeMS = this.management.defaults?.maxTimeMS;
12194
12492
  const merged = { ...maxTimeMS !== void 0 ? { maxTimeMS } : {}, ...options ?? {} };
12195
- const cacheTTL = typeof merged.cache === "number" ? merged.cache : 0;
12493
+ const cacheTTL = merged.explain === void 0 || merged.explain === false ? typeof merged.cache === "number" ? merged.cache : 0 : 0;
12196
12494
  const { cache: _cache, ...keyOptions } = merged;
12197
12495
  void _cache;
12496
+ const callerSignal = keyOptions.signal;
12497
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12198
12498
  const executeCount = (signal) => countDocuments(
12199
12499
  this.collectionRef,
12200
12500
  normalizedQuery ?? {},
@@ -12205,15 +12505,16 @@ var MongoCollectionAccessor = class {
12205
12505
  const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
12206
12506
  const cached = await Promise.resolve(this.management.queryCache.get(cacheKey));
12207
12507
  if (cached !== void 0) {
12508
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12208
12509
  return Promise.resolve(wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, cached));
12209
12510
  }
12210
- const runner2 = countQueue ? countQueue.execute(executeCount) : executeCount();
12511
+ const runner2 = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12211
12512
  return runner2.then(async (result) => {
12212
12513
  await Promise.resolve(this.management.queryCache?.set(cacheKey, result, cacheTTL));
12213
12514
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result);
12214
12515
  });
12215
12516
  }
12216
- const runner = countQueue ? countQueue.execute(executeCount) : executeCount();
12517
+ const runner = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12217
12518
  return runner.then((result) => wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result));
12218
12519
  }
12219
12520
  /** Runs an aggregation pipeline and returns a chainable aggregate cursor. */
@@ -13502,20 +13803,21 @@ var DistributedCacheLockManager = class {
13502
13803
  }
13503
13804
  }
13504
13805
  async withLock(key, callback, options = {}) {
13806
+ let lock;
13505
13807
  try {
13506
- const lock = await this.acquireLock(key, options);
13507
- try {
13508
- return await callback();
13509
- } finally {
13510
- await lock.release().catch(() => {
13511
- });
13512
- }
13808
+ lock = await this.acquireLock(key, options);
13513
13809
  } catch (error) {
13514
13810
  if (this._isRedisConnectionError(error) && options.fallbackToNoLock) {
13515
13811
  return callback();
13516
13812
  }
13517
13813
  throw error;
13518
13814
  }
13815
+ try {
13816
+ return await callback();
13817
+ } finally {
13818
+ await lock.release().catch(() => {
13819
+ });
13820
+ }
13519
13821
  }
13520
13822
  async acquireLock(key, options = {}) {
13521
13823
  const ttl = options.ttl ?? 1e4;
@@ -14618,8 +14920,6 @@ var SlowQueryLogManager = class {
14618
14920
  };
14619
14921
 
14620
14922
  // src/capabilities/sync/index.ts
14621
- import { copyFile, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
14622
- import path2 from "node:path";
14623
14923
  import { MongoClient as MongoDriverClient3 } from "mongodb";
14624
14924
 
14625
14925
  // src/capabilities/sync/idempotency.ts
@@ -14722,25 +15022,19 @@ function validateSyncIdempotencyConfig(config) {
14722
15022
  }
14723
15023
  }
14724
15024
 
14725
- // src/capabilities/sync/index.ts
15025
+ // src/capabilities/sync/resume-token-store.ts
15026
+ import { copyFile, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
15027
+ import path2 from "node:path";
14726
15028
  function normalizeError(error) {
14727
15029
  return error instanceof Error ? error : new Error(String(error));
14728
15030
  }
14729
15031
  function delay2(ms) {
14730
- if (ms <= 0) {
14731
- return Promise.resolve();
14732
- }
15032
+ if (ms <= 0) return Promise.resolve();
14733
15033
  return new Promise((resolve) => {
14734
15034
  const timer = setTimeout(resolve, ms);
14735
15035
  timer.unref?.();
14736
15036
  });
14737
15037
  }
14738
- function normalizeCollectionFilter(collections) {
14739
- if (!collections?.length || collections.includes("*")) {
14740
- return null;
14741
- }
14742
- return new Set(collections);
14743
- }
14744
15038
  async function syncDirectory(directory) {
14745
15039
  try {
14746
15040
  const handle = await open(directory, "r");
@@ -14752,35 +15046,6 @@ async function syncDirectory(directory) {
14752
15046
  } catch {
14753
15047
  }
14754
15048
  }
14755
- function validateTargetConfig(target, index) {
14756
- if (!target || typeof target !== "object") {
14757
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
14758
- }
14759
- if (typeof target.name !== "string" || target.name.trim() === "") {
14760
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
14761
- }
14762
- if (!target.apply && !target.uri && !target.pool) {
14763
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
14764
- }
14765
- if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
14766
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
14767
- }
14768
- if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
14769
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
14770
- }
14771
- if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
14772
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
14773
- }
14774
- if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
14775
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
14776
- }
14777
- if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14778
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
14779
- }
14780
- if (target.apply !== void 0 && typeof target.apply !== "function") {
14781
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
14782
- }
14783
- }
14784
15049
  function validateResumeTokenConfig(config) {
14785
15050
  if (!config || typeof config !== "object") {
14786
15051
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken must be an object.");
@@ -14811,39 +15076,6 @@ function validateResumeTokenConfig(config) {
14811
15076
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken.saveRetryDelayMs must be a non-negative integer.");
14812
15077
  }
14813
15078
  }
14814
- function validateSyncConfig(config) {
14815
- if (!config || typeof config !== "object") {
14816
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
14817
- }
14818
- if (typeof config.enabled !== "boolean") {
14819
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
14820
- }
14821
- if (!config.enabled) {
14822
- return;
14823
- }
14824
- if (!Array.isArray(config.targets) || config.targets.length === 0) {
14825
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
14826
- }
14827
- if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
14828
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
14829
- }
14830
- if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14831
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
14832
- }
14833
- if (config.filter !== void 0 && typeof config.filter !== "function") {
14834
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
14835
- }
14836
- if (config.transform !== void 0 && typeof config.transform !== "function") {
14837
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
14838
- }
14839
- validateSyncIdempotencyConfig(config.idempotency);
14840
- config.targets.forEach((target, index) => {
14841
- validateTargetConfig(target, index);
14842
- });
14843
- if (config.resumeToken) {
14844
- validateResumeTokenConfig(config.resumeToken);
14845
- }
14846
- }
14847
15079
  var ResumeTokenStore = class {
14848
15080
  constructor(options = {}) {
14849
15081
  this.storage = options.storage ?? "file";
@@ -14867,16 +15099,9 @@ var ResumeTokenStore = class {
14867
15099
  return JSON.parse(payload);
14868
15100
  } catch (error) {
14869
15101
  const code = error?.code;
14870
- if (code !== "ENOENT") {
14871
- this.logger?.warn?.("[Sync] failed to load resume token", error);
14872
- }
15102
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to load resume token", error);
14873
15103
  if (code !== "ENOENT" && this.strictLoad) {
14874
- throw createError(
14875
- ErrorCodes.DATABASE_ERROR,
14876
- "[Sync] failed to load resume token",
14877
- void 0,
14878
- normalizeError(error)
14879
- );
15104
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to load resume token", void 0, normalizeError(error));
14880
15105
  }
14881
15106
  return null;
14882
15107
  }
@@ -14902,12 +15127,7 @@ var ResumeTokenStore = class {
14902
15127
  }
14903
15128
  this.logger?.error?.("[Sync] failed to save resume token", lastError);
14904
15129
  if (this.strictSave) {
14905
- throw createError(
14906
- ErrorCodes.DATABASE_ERROR,
14907
- "[Sync] failed to save resume token",
14908
- void 0,
14909
- lastError ?? void 0
14910
- );
15130
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to save resume token", void 0, lastError ?? void 0);
14911
15131
  }
14912
15132
  }
14913
15133
  async writePayload(payload) {
@@ -14940,9 +15160,7 @@ var ResumeTokenStore = class {
14940
15160
  await copyFile(this.path, `${this.path}.bak`);
14941
15161
  } catch (error) {
14942
15162
  const code = error?.code;
14943
- if (code !== "ENOENT") {
14944
- throw error;
14945
- }
15163
+ if (code !== "ENOENT") throw error;
14946
15164
  }
14947
15165
  }
14948
15166
  async clear() {
@@ -14954,12 +15172,80 @@ var ResumeTokenStore = class {
14954
15172
  await unlink(this.path);
14955
15173
  } catch (error) {
14956
15174
  const code = error?.code;
14957
- if (code !== "ENOENT") {
14958
- this.logger?.warn?.("[Sync] failed to clear resume token", error);
14959
- }
15175
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to clear resume token", error);
14960
15176
  }
14961
15177
  }
14962
15178
  };
15179
+
15180
+ // src/capabilities/sync/index.ts
15181
+ function normalizeCollectionFilter(collections) {
15182
+ if (!collections?.length || collections.includes("*")) {
15183
+ return null;
15184
+ }
15185
+ return new Set(collections);
15186
+ }
15187
+ function validateTargetConfig(target, index) {
15188
+ if (!target || typeof target !== "object") {
15189
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
15190
+ }
15191
+ if (typeof target.name !== "string" || target.name.trim() === "") {
15192
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
15193
+ }
15194
+ if (!target.apply && !target.uri && !target.pool) {
15195
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
15196
+ }
15197
+ if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
15198
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
15199
+ }
15200
+ if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
15201
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
15202
+ }
15203
+ if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
15204
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
15205
+ }
15206
+ if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
15207
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
15208
+ }
15209
+ if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
15210
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
15211
+ }
15212
+ if (target.apply !== void 0 && typeof target.apply !== "function") {
15213
+ throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
15214
+ }
15215
+ }
15216
+ function validateSyncConfig(config) {
15217
+ if (!config || typeof config !== "object") {
15218
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
15219
+ }
15220
+ if (typeof config.enabled !== "boolean") {
15221
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
15222
+ }
15223
+ if (!config.enabled) {
15224
+ return;
15225
+ }
15226
+ if (!Array.isArray(config.targets) || config.targets.length === 0) {
15227
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
15228
+ }
15229
+ if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
15230
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
15231
+ }
15232
+ if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
15233
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
15234
+ }
15235
+ if (config.filter !== void 0 && typeof config.filter !== "function") {
15236
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
15237
+ }
15238
+ if (config.transform !== void 0 && typeof config.transform !== "function") {
15239
+ throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
15240
+ }
15241
+ validateSyncIdempotencyConfig(config.idempotency);
15242
+ config.targets.forEach((target, index) => {
15243
+ validateTargetConfig(target, index);
15244
+ });
15245
+ if (config.resumeToken) {
15246
+ validateResumeTokenConfig(config.resumeToken);
15247
+ }
15248
+ }
14963
15249
  var ChangeStreamSyncManager = class {
14964
15250
  constructor(options) {
14965
15251
  this.targets = [];
@@ -14968,7 +15254,10 @@ var ChangeStreamSyncManager = class {
14968
15254
  this.closingStreamPromise = null;
14969
15255
  this.changeQueue = Promise.resolve();
14970
15256
  this.running = false;
15257
+ this.stopping = false;
15258
+ this.stopPromise = null;
14971
15259
  this.fatalSyncError = null;
15260
+ this.generation = 0;
14972
15261
  this.stats = {
14973
15262
  eventCount: 0,
14974
15263
  syncedCount: 0,
@@ -15001,40 +15290,62 @@ var ChangeStreamSyncManager = class {
15001
15290
  * @since v1.0.9
15002
15291
  */
15003
15292
  async start() {
15293
+ if (this.stopPromise) await this.stopPromise;
15004
15294
  if (this.running || !this.config.enabled) return;
15005
15295
  if (this.startPromise) return this.startPromise;
15296
+ const generation = ++this.generation;
15006
15297
  const startup = (async () => {
15007
15298
  await this.closingStreamPromise;
15299
+ await this.changeQueue.catch(() => void 0);
15300
+ this.assertCurrentGeneration(generation, true);
15008
15301
  if (this.running) return;
15009
15302
  this.fatalSyncError = null;
15010
- await this.validateEnvironment();
15011
- await this.initializeTargets();
15012
- const resumeAfter = await this.tokenStore.load();
15013
- const options = { fullDocument: "updateLookup" };
15014
- if (resumeAfter) options.resumeAfter = resumeAfter;
15015
- const stream = this.db.watch(this.buildPipeline(), options);
15016
- stream.on("change", (event) => this.enqueueChange(event));
15017
- stream.on("error", (error) => {
15018
- const normalized = normalizeError(error);
15019
- this.stats.errorCount += 1;
15020
- this.stats.lastError = normalized;
15021
- this.logger?.error?.("[Sync] change stream error", normalized);
15022
- });
15023
- stream.on("close", () => {
15024
- if (!this.running || this.changeStream !== stream) {
15025
- this.logger?.debug?.("[Sync] change stream closed");
15026
- return;
15027
- }
15028
- const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
15029
- this.running = false;
15030
- this.changeStream = null;
15031
- this.stats.errorCount += 1;
15032
- this.stats.lastError = error;
15033
- this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
15034
- });
15035
- this.changeStream = stream;
15036
- this.running = true;
15037
- this.stats.startTime = /* @__PURE__ */ new Date();
15303
+ await this.closeTargets();
15304
+ let stagedTargets = [];
15305
+ let stream = null;
15306
+ try {
15307
+ await this.validateEnvironment();
15308
+ this.assertCurrentGeneration(generation);
15309
+ stagedTargets = await this.initializeTargets();
15310
+ this.assertCurrentGeneration(generation);
15311
+ const resumeAfter = await this.tokenStore.load();
15312
+ this.assertCurrentGeneration(generation);
15313
+ const options = { fullDocument: "updateLookup" };
15314
+ if (resumeAfter) options.resumeAfter = resumeAfter;
15315
+ stream = this.db.watch(this.buildPipeline(), options);
15316
+ const activeStream = stream;
15317
+ activeStream.on("change", (event) => this.enqueueChange(event, generation));
15318
+ activeStream.on("error", (error) => {
15319
+ if (generation !== this.generation) return;
15320
+ const normalized = normalizeError(error);
15321
+ this.stats.errorCount += 1;
15322
+ this.stats.lastError = normalized;
15323
+ this.logger?.error?.("[Sync] change stream error", normalized);
15324
+ this.stopAfterFatalSyncError(normalized, generation);
15325
+ });
15326
+ activeStream.on("close", () => {
15327
+ if (generation !== this.generation || !this.running || this.changeStream !== activeStream) {
15328
+ this.logger?.debug?.("[Sync] change stream closed");
15329
+ return;
15330
+ }
15331
+ const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
15332
+ this.stats.errorCount += 1;
15333
+ this.stats.lastError = error;
15334
+ this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
15335
+ this.stopAfterFatalSyncError(error, generation);
15336
+ });
15337
+ this.assertCurrentGeneration(generation);
15338
+ this.targets.push(...stagedTargets);
15339
+ stagedTargets = [];
15340
+ this.changeStream = activeStream;
15341
+ stream = null;
15342
+ this.running = true;
15343
+ this.stats.startTime = /* @__PURE__ */ new Date();
15344
+ } catch (error) {
15345
+ if (stream) await this.closeChangeStream(stream).catch(() => void 0);
15346
+ for (const target of stagedTargets.reverse()) await target.close().catch(() => void 0);
15347
+ throw error;
15348
+ }
15038
15349
  })();
15039
15350
  this.startPromise = startup;
15040
15351
  try {
@@ -15048,16 +15359,37 @@ var ChangeStreamSyncManager = class {
15048
15359
  * @since v1.0.9
15049
15360
  */
15050
15361
  async stop() {
15051
- this.running = false;
15052
- await this.startPromise;
15053
- const stream = this.changeStream;
15054
- this.changeStream = null;
15055
- if (stream) await this.closeChangeStream(stream);
15056
- else await this.closingStreamPromise;
15057
- await this.changeQueue.catch(() => void 0);
15362
+ if (this.stopPromise) return this.stopPromise;
15363
+ this.stopping = true;
15364
+ const cleanup = (async () => {
15365
+ if (this.startPromise && !this.running) this.generation += 1;
15366
+ await this.startPromise?.catch(() => void 0);
15367
+ await this.changeQueue.catch(() => void 0);
15368
+ this.generation += 1;
15369
+ this.running = false;
15370
+ const stream = this.changeStream;
15371
+ this.changeStream = null;
15372
+ if (stream) await this.closeChangeStream(stream);
15373
+ else await this.closingStreamPromise;
15374
+ await this.closeTargets();
15375
+ })();
15376
+ this.stopPromise = cleanup;
15377
+ try {
15378
+ await cleanup;
15379
+ } finally {
15380
+ if (this.stopPromise === cleanup) this.stopPromise = null;
15381
+ this.stopping = false;
15382
+ }
15383
+ }
15384
+ assertCurrentGeneration(generation, allowPreviousFatal = false) {
15385
+ if (generation !== this.generation) {
15386
+ throw createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] startup was superseded by stop or restart");
15387
+ }
15388
+ if (!allowPreviousFatal && this.fatalSyncError) throw this.fatalSyncError;
15389
+ }
15390
+ async closeTargets() {
15058
15391
  while (this.targets.length > 0) {
15059
- const target = this.targets.pop();
15060
- await target?.close();
15392
+ await this.targets.pop()?.close();
15061
15393
  }
15062
15394
  }
15063
15395
  /**
@@ -15122,11 +15454,13 @@ var ChangeStreamSyncManager = class {
15122
15454
  return pipeline;
15123
15455
  }
15124
15456
  async initializeTargets() {
15125
- if (this.targets.length > 0) {
15126
- return;
15127
- }
15128
- for (const target of this.config.targets) {
15129
- this.targets.push(await this.resolveTarget(target));
15457
+ const staged = [];
15458
+ try {
15459
+ for (const target of this.config.targets) staged.push(await this.resolveTarget(target));
15460
+ return staged;
15461
+ } catch (error) {
15462
+ for (const target of staged.reverse()) await target.close().catch(() => void 0);
15463
+ throw error;
15130
15464
  }
15131
15465
  }
15132
15466
  async resolveTarget(target) {
@@ -15163,14 +15497,15 @@ var ChangeStreamSyncManager = class {
15163
15497
  const client = await this.clientFactory(target.uri, target.options);
15164
15498
  return createMongoTarget(target.name, collections, client, target.databaseName ?? this.db.databaseName, true);
15165
15499
  }
15166
- async handleChange(event) {
15167
- if (this.fatalSyncError) {
15500
+ async handleChange(event, generation = this.generation) {
15501
+ if (generation !== this.generation || !this.running || this.fatalSyncError) {
15168
15502
  this.logger?.warn?.("[Sync] skipping queued change after fatal sync error", this.fatalSyncError);
15169
15503
  return;
15170
15504
  }
15171
15505
  this.stats.eventCount += 1;
15172
15506
  this.stats.lastEventTime = /* @__PURE__ */ new Date();
15173
15507
  if (this.config.filter && !this.config.filter(event)) {
15508
+ await this.saveProcessedToken(event, generation);
15174
15509
  return;
15175
15510
  }
15176
15511
  let document = event.fullDocument;
@@ -15182,6 +15517,7 @@ var ChangeStreamSyncManager = class {
15182
15517
  let duplicateTargets = 0;
15183
15518
  const targetErrors = [];
15184
15519
  for (const target of this.targets) {
15520
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15185
15521
  if (target.collections && !target.collections.has(event.ns.coll)) {
15186
15522
  continue;
15187
15523
  }
@@ -15198,11 +15534,13 @@ var ChangeStreamSyncManager = class {
15198
15534
  if (idempotencyKey && this.idempotency.markMode === "start") {
15199
15535
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "start");
15200
15536
  }
15537
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15201
15538
  await target.apply(event, document, {
15202
15539
  targetName: target.name,
15203
15540
  ...idempotencyKey ? { idempotencyKey } : {}
15204
15541
  });
15205
15542
  if (idempotencyKey && this.idempotency.markMode === "success") {
15543
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15206
15544
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "success");
15207
15545
  }
15208
15546
  target.stats.syncCount += 1;
@@ -15223,26 +15561,30 @@ var ChangeStreamSyncManager = class {
15223
15561
  }
15224
15562
  }
15225
15563
  if (targetErrors.length > 0) {
15226
- this.stopAfterFatalSyncError(targetErrors[0]);
15564
+ this.stopAfterFatalSyncError(targetErrors[0], generation);
15227
15565
  return;
15228
15566
  }
15229
- if (eligibleTargets > 0 && succeeded === eligibleTargets) {
15230
- try {
15231
- await this.tokenStore.save(event._id);
15232
- } catch (error) {
15233
- const normalized = normalizeError(error);
15234
- this.stats.tokenSaveErrorCount += 1;
15235
- this.stats.lastTokenSaveError = normalized;
15236
- this.stats.lastError = normalized;
15237
- this.stopAfterFatalSyncError(normalized);
15238
- throw normalized;
15239
- }
15240
- this.stats.syncedCount += 1;
15567
+ if (succeeded === eligibleTargets) {
15568
+ await this.saveProcessedToken(event, generation);
15241
15569
  if (duplicateTargets > 0 && duplicateTargets === eligibleTargets) {
15242
15570
  this.stats.duplicateEventCount += 1;
15243
15571
  }
15244
15572
  }
15245
15573
  }
15574
+ async saveProcessedToken(event, generation) {
15575
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15576
+ try {
15577
+ await this.tokenStore.save(event._id);
15578
+ } catch (error) {
15579
+ const normalized = normalizeError(error);
15580
+ this.stats.tokenSaveErrorCount += 1;
15581
+ this.stats.lastTokenSaveError = normalized;
15582
+ this.stats.lastError = normalized;
15583
+ this.stopAfterFatalSyncError(normalized, generation);
15584
+ throw normalized;
15585
+ }
15586
+ if (generation === this.generation && !this.fatalSyncError && this.running) this.stats.syncedCount += 1;
15587
+ }
15246
15588
  buildTargetIdempotencyKey(targetName, event) {
15247
15589
  if (!this.idempotency.enabled) {
15248
15590
  return null;
@@ -15265,7 +15607,8 @@ var ChangeStreamSyncManager = class {
15265
15607
  ts: Date.now()
15266
15608
  }, this.idempotency.ttl));
15267
15609
  }
15268
- stopAfterFatalSyncError(error) {
15610
+ stopAfterFatalSyncError(error, generation) {
15611
+ if (generation !== this.generation) return;
15269
15612
  this.fatalSyncError = error;
15270
15613
  this.running = false;
15271
15614
  const stream = this.changeStream;
@@ -15283,12 +15626,14 @@ var ChangeStreamSyncManager = class {
15283
15626
  if (this.closingStreamPromise === closing) this.closingStreamPromise = null;
15284
15627
  });
15285
15628
  }
15286
- enqueueChange(event) {
15287
- this.changeQueue = this.changeQueue.then(() => this.handleChange(event)).catch((error) => {
15629
+ enqueueChange(event, generation) {
15630
+ if (generation !== this.generation || !this.running || this.stopping || this.fatalSyncError) return;
15631
+ this.changeQueue = this.changeQueue.then(() => this.handleChange(event, generation)).catch((error) => {
15288
15632
  const normalized = normalizeError(error);
15289
15633
  this.stats.errorCount += 1;
15290
15634
  this.stats.lastError = normalized;
15291
15635
  this.logger?.error?.("[Sync] change handling failed", normalized);
15636
+ this.stopAfterFatalSyncError(normalized, generation);
15292
15637
  });
15293
15638
  }
15294
15639
  };
@@ -15717,7 +16062,9 @@ var SSHTunnelSSH2 = class {
15717
16062
  passphrase,
15718
16063
  port = 22,
15719
16064
  readyTimeout = 2e4,
15720
- keepaliveInterval = 3e4
16065
+ keepaliveInterval = 3e4,
16066
+ hostHash,
16067
+ hostVerifier
15721
16068
  } = this._sshConfig;
15722
16069
  if (!host || !username) {
15723
16070
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
@@ -15726,6 +16073,8 @@ var SSHTunnelSSH2 = class {
15726
16073
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
15727
16074
  }
15728
16075
  const config = { host, port, username, readyTimeout, keepaliveInterval };
16076
+ if (hostHash !== void 0) config.hostHash = hostHash;
16077
+ if (hostVerifier !== void 0) config.hostVerifier = hostVerifier;
15729
16078
  if (password) {
15730
16079
  config.password = password;
15731
16080
  } else if (privateKey) {
@@ -15744,6 +16093,25 @@ var SSHTunnelSSH2 = class {
15744
16093
  return new Promise((resolve, reject) => {
15745
16094
  const ssh = new SshClient();
15746
16095
  let settled = false;
16096
+ const fail = (err) => {
16097
+ if (settled) {
16098
+ this.markDisconnected();
16099
+ return;
16100
+ }
16101
+ settled = true;
16102
+ this.isConnected = false;
16103
+ this.localPort = null;
16104
+ this.server = null;
16105
+ this.sshClient = null;
16106
+ for (const socket of this._activeSockets) socket.destroy();
16107
+ this._activeSockets.clear();
16108
+ void closeServer(server);
16109
+ try {
16110
+ ssh.end();
16111
+ } catch {
16112
+ }
16113
+ reject(err);
16114
+ };
15747
16115
  const server = net.createServer((socket) => {
15748
16116
  this._activeSockets.add(socket);
15749
16117
  socket.on("close", () => this._activeSockets.delete(socket));
@@ -15769,12 +16137,7 @@ var SSHTunnelSSH2 = class {
15769
16137
  }
15770
16138
  );
15771
16139
  });
15772
- server.on("error", (err) => {
15773
- if (!settled) {
15774
- settled = true;
15775
- reject(err);
15776
- }
15777
- });
16140
+ server.on("error", fail);
15778
16141
  server.listen(this._sshConfig.localPort ?? 0, "127.0.0.1", () => {
15779
16142
  this.server = server;
15780
16143
  const addr = server.address();
@@ -15789,25 +16152,25 @@ var SSHTunnelSSH2 = class {
15789
16152
  });
15790
16153
  ssh.on("error", (err) => {
15791
16154
  if (!settled) {
15792
- settled = true;
15793
- server.close();
15794
- reject(err);
16155
+ fail(err);
15795
16156
  return;
15796
16157
  }
15797
16158
  this.markDisconnected();
15798
16159
  });
15799
16160
  const handleDisconnect = () => {
15800
16161
  if (!settled) {
15801
- settled = true;
15802
- server.close();
15803
- reject(createError(ErrorCodes.CONNECTION_FAILED, "SSH connection closed before the tunnel became ready"));
16162
+ fail(createError(ErrorCodes.CONNECTION_FAILED, "SSH connection closed before the tunnel became ready"));
15804
16163
  return;
15805
16164
  }
15806
16165
  this.markDisconnected();
15807
16166
  };
15808
16167
  ssh.on("close", handleDisconnect);
15809
16168
  ssh.on("end", handleDisconnect);
15810
- ssh.connect(authConfig);
16169
+ try {
16170
+ ssh.connect(authConfig);
16171
+ } catch (err) {
16172
+ fail(err);
16173
+ }
15811
16174
  });
15812
16175
  });
15813
16176
  }
@@ -15838,6 +16201,7 @@ var SSHTunnelSSH2 = class {
15838
16201
  ssh?.end();
15839
16202
  }
15840
16203
  markDisconnected() {
16204
+ const ssh = this.sshClient;
15841
16205
  this.isConnected = false;
15842
16206
  this.localPort = null;
15843
16207
  this.sshClient = null;
@@ -15848,6 +16212,10 @@ var SSHTunnelSSH2 = class {
15848
16212
  }
15849
16213
  this._activeSockets.clear();
15850
16214
  void closeServer(server);
16215
+ try {
16216
+ ssh?.end();
16217
+ } catch {
16218
+ }
15851
16219
  }
15852
16220
  };
15853
16221
  function parseHostFromUri(uri) {
@@ -15934,12 +16302,17 @@ async function prepareSshTunnelConnectConfig(connectConfig, databaseName, logger
15934
16302
  const remotePort = Number(cfg["dstPort"] ?? rawCfg["remotePort"] ?? rawCfg["mongoPort"] ?? parsePortFromUri(uri));
15935
16303
  const tunnel = new SSHTunnelSSH2(sshCfg, remoteHost, remotePort, { name: databaseName });
15936
16304
  logger.info?.(`[SSH] Establishing tunnel to ${remoteHost}:${remotePort} via ${String(cfg["host"])}`);
15937
- await tunnel.connect();
15938
- logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
15939
- return {
15940
- tunnel,
15941
- connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
15942
- };
16305
+ try {
16306
+ await tunnel.connect();
16307
+ logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
16308
+ return {
16309
+ tunnel,
16310
+ connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
16311
+ };
16312
+ } catch (error) {
16313
+ await tunnel.close();
16314
+ throw error;
16315
+ }
15943
16316
  }
15944
16317
 
15945
16318
  // src/entry/runtime-schema-dsl.ts
@@ -16234,7 +16607,7 @@ function makePageResult(rows, ctx) {
16234
16607
  import { createHash as createHash4 } from "node:crypto";
16235
16608
  import os2 from "node:os";
16236
16609
  import path3 from "node:path";
16237
- import { BSON } from "mongodb";
16610
+ import { BSON as BSON2 } from "mongodb";
16238
16611
 
16239
16612
  // src/capabilities/data-tasks/support.ts
16240
16613
  var INDEX_OPTION_KEYS = [
@@ -16342,7 +16715,7 @@ function canonicalize(value) {
16342
16715
  return value;
16343
16716
  }
16344
16717
  function canonicalStringify(value) {
16345
- return JSON.stringify(canonicalize(BSON.EJSON.serialize(value, { relaxed: false })));
16718
+ return JSON.stringify(canonicalize(BSON2.EJSON.serialize(value, { relaxed: false })));
16346
16719
  }
16347
16720
  function hashDataTaskValue(value) {
16348
16721
  return createHash4("sha256").update(canonicalStringify(value)).digest("hex");
@@ -16831,9 +17204,8 @@ var MonSQLizeRuntime = class {
16831
17204
  driverError = error instanceof Error ? error.message : String(error);
16832
17205
  }
16833
17206
  }
16834
- const cacheStats = this._cache.getStats?.();
16835
- const poolHealth = this._poolManager?.getHealthStatus();
16836
- const distributedStats = this._distributedInvalidator?.getStats() ?? null;
17207
+ const health = summarizeRuntimeHealth(this.options.cache, this._cache, this._poolManager, this._distributedInvalidator);
17208
+ const { cacheStats, poolHealth, distributedStats, cacheEnabled, distributedEnabled, cacheProbeStatus, distributedStatus, poolsStatus } = health;
16837
17209
  const connected = this._connected && driverConnected;
16838
17210
  return {
16839
17211
  status: connected ? "up" : "down",
@@ -16841,7 +17213,7 @@ var MonSQLizeRuntime = class {
16841
17213
  driver: { connected: driverConnected, ...driverError ? { error: driverError } : {} },
16842
17214
  defaults: this.getDefaults(),
16843
17215
  cache: {
16844
- enabled: true,
17216
+ enabled: cacheEnabled,
16845
17217
  stats: cacheStats,
16846
17218
  pools: poolHealth,
16847
17219
  distributed: distributedStats
@@ -16853,18 +17225,18 @@ var MonSQLizeRuntime = class {
16853
17225
  ...driverError ? { error: driverError } : {}
16854
17226
  },
16855
17227
  cache: {
16856
- status: "up",
16857
- enabled: true,
17228
+ status: cacheProbeStatus,
17229
+ enabled: cacheEnabled,
16858
17230
  hasStats: cacheStats !== void 0
16859
17231
  },
16860
17232
  distributedCacheInvalidator: {
16861
- status: distributedStats ? "up" : "unknown",
16862
- enabled: distributedStats !== null,
17233
+ status: distributedStatus,
17234
+ enabled: distributedEnabled,
16863
17235
  stats: distributedStats
16864
17236
  },
16865
17237
  pools: {
16866
- status: poolHealth ? "up" : "unknown",
16867
- enabled: poolHealth !== void 0,
17238
+ status: poolsStatus,
17239
+ enabled: this._poolManager !== null,
16868
17240
  health: poolHealth
16869
17241
  }
16870
17242
  },
@@ -17226,7 +17598,7 @@ var MonSQLizeRuntime = class {
17226
17598
  };
17227
17599
 
17228
17600
  // src/capabilities/data-tasks/job-planner.ts
17229
- import { BSON as BSON2 } from "mongodb";
17601
+ import { BSON as BSON3 } from "mongodb";
17230
17602
 
17231
17603
  // src/capabilities/data-tasks/document-utils.ts
17232
17604
  function isPlainRecord(value) {
@@ -17708,7 +18080,7 @@ async function planCollection(sourceHost, targetHost, config, sampleSize) {
17708
18080
  before: change.before,
17709
18081
  after: change.after
17710
18082
  }));
17711
- const backupBytes = changes.reduce((total, change) => total + Buffer.byteLength(`${BSON2.EJSON.stringify({
18083
+ const backupBytes = changes.reduce((total, change) => total + Buffer.byteLength(`${BSON3.EJSON.stringify({
17712
18084
  ...change,
17713
18085
  beforeHash: hashDataTaskValue(change.before),
17714
18086
  plannedAfterHash: hashDataTaskValue(change.after)
@@ -17793,7 +18165,7 @@ import { mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename
17793
18165
  import path4 from "node:path";
17794
18166
  import { gzip, gunzip } from "node:zlib";
17795
18167
  import { promisify } from "node:util";
17796
- import { BSON as BSON3 } from "mongodb";
18168
+ import { BSON as BSON4, Int32, Long } from "mongodb";
17797
18169
  var gzipAsync = promisify(gzip);
17798
18170
  var gunzipAsync = promisify(gunzip);
17799
18171
  var BACKUP_TARGET = /* @__PURE__ */ Symbol("dataTaskBackupTarget");
@@ -17810,10 +18182,31 @@ function checksum(buffer) {
17810
18182
  return createHash5("sha256").update(buffer).digest("hex");
17811
18183
  }
17812
18184
  function ejsonLine(value) {
17813
- return BSON3.EJSON.stringify(value, { relaxed: false });
18185
+ return BSON4.EJSON.stringify(value, { relaxed: false });
17814
18186
  }
17815
18187
  function ejsonManifest(value) {
17816
- return BSON3.EJSON.stringify(value, void 0, 2, { relaxed: false });
18188
+ return BSON4.EJSON.stringify(value, void 0, 2, { relaxed: false });
18189
+ }
18190
+ function manifestInteger(value, field, minimum) {
18191
+ const numeric = value instanceof Int32 ? value.valueOf() : Long.isLong(value) ? value.toBigInt() : value;
18192
+ if (typeof numeric === "bigint") {
18193
+ if (numeric < BigInt(minimum) || numeric > BigInt(Number.MAX_SAFE_INTEGER)) {
18194
+ throw new Error(`invalid backup ${field}`);
18195
+ }
18196
+ return Number(numeric);
18197
+ }
18198
+ if (typeof numeric !== "number" || !Number.isSafeInteger(numeric) || numeric < minimum) {
18199
+ throw new Error(`invalid backup ${field}`);
18200
+ }
18201
+ return numeric;
18202
+ }
18203
+ function decodeManifestControls(raw) {
18204
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid backup manifest");
18205
+ const manifest = raw;
18206
+ manifest.version = manifestInteger(manifest.version, "manifest", 1);
18207
+ manifest.entryCount = manifestInteger(manifest.entryCount, "entry count", 0);
18208
+ if (manifest.maxBytes !== void 0) manifest.maxBytes = manifestInteger(manifest.maxBytes, "max bytes", 1);
18209
+ return manifest;
17817
18210
  }
17818
18211
  function sanitizeName(value) {
17819
18212
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "data-task";
@@ -17991,10 +18384,9 @@ async function createRestoreSafetyBackup(source, entries, beforeIndexes, targetH
17991
18384
  }
17992
18385
  async function readDataTaskBackup(ref) {
17993
18386
  try {
17994
- const manifest = BSON3.EJSON.parse(await readFile2(ref.manifestPath, "utf8"), { relaxed: true });
18387
+ const manifest = decodeManifestControls(BSON4.EJSON.parse(await readFile2(ref.manifestPath, "utf8"), { relaxed: false }));
17995
18388
  if (manifest.kind !== "monsqlize-data-task-backup" || manifest.version !== 1 || manifest.runId !== ref.runId) throw new Error("invalid backup manifest");
17996
18389
  if (manifest.compression !== "gzip" && manifest.compression !== "none") throw new Error("invalid backup compression");
17997
- if (!Number.isSafeInteger(manifest.entryCount) || manifest.entryCount < 0) throw new Error("invalid backup entry count");
17998
18390
  if (typeof manifest.checksum !== "string" || !/^[a-f0-9]{64}$/i.test(manifest.checksum)) throw new Error("invalid backup checksum");
17999
18391
  if (typeof manifest.dataFile !== "string" || manifest.dataFile.length === 0 || path4.isAbsolute(manifest.dataFile) || path4.basename(manifest.dataFile) !== manifest.dataFile) {
18000
18392
  throw new Error("backup data file must stay beside its manifest");
@@ -18007,7 +18399,7 @@ async function readDataTaskBackup(ref) {
18007
18399
  if (digest !== manifest.checksum || digest !== ref.checksum) throw new Error("backup checksum mismatch");
18008
18400
  const plain = manifest.compression === "gzip" ? await gunzipAsync(contents) : contents;
18009
18401
  const lines = plain.toString("utf8").split(/\r?\n/).filter(Boolean);
18010
- const entries = lines.map((line) => BSON3.EJSON.parse(line, { relaxed: false }));
18402
+ const entries = lines.map((line) => BSON4.EJSON.parse(line, { relaxed: false }));
18011
18403
  if (entries.length !== manifest.entryCount) throw new Error("backup entry count mismatch");
18012
18404
  return { ref: attachTarget({ ...ref }, backupTargetHint(ref)), manifest, entries };
18013
18405
  } catch (error) {
@@ -18325,6 +18717,7 @@ async function acquireDataTaskLease(host, options) {
18325
18717
  }
18326
18718
 
18327
18719
  // src/capabilities/data-tasks/job-restore.ts
18720
+ import { Int32 as Int322, Long as Long2 } from "mongodb";
18328
18721
  async function findCurrent(collection, identity) {
18329
18722
  let chain = collection.find(identity);
18330
18723
  if (typeof chain.limit === "function") chain = chain.limit(2);
@@ -18345,13 +18738,20 @@ function restoreApproval(hashes) {
18345
18738
  };
18346
18739
  return { ...payload, token: hashDataTaskValue(payload) };
18347
18740
  }
18741
+ function comparableIndexKey(key) {
18742
+ return Object.fromEntries(Object.entries(key).map(([field, value]) => {
18743
+ if (value instanceof Int322) return [field, value.valueOf()];
18744
+ if (Long2.isLong(value) && value.toBigInt() <= BigInt(Number.MAX_SAFE_INTEGER) && value.toBigInt() >= BigInt(Number.MIN_SAFE_INTEGER)) return [field, Number(value.toBigInt())];
18745
+ return [field, value];
18746
+ }));
18747
+ }
18348
18748
  function exactIndex(index, current) {
18349
- return classifyDataTaskIndexes([{ key: index.key, name: index.name, options: index.options }], current)[0]?.status === "existing";
18749
+ return classifyDataTaskIndexes([{ key: comparableIndexKey(index.key), name: index.name, options: index.options }], current)[0]?.status === "existing";
18350
18750
  }
18351
18751
  function pendingIndexMatches(index, current) {
18352
18752
  return current.filter((candidate) => {
18353
18753
  if (index.name && candidate.name !== index.name) return false;
18354
- return classifyDataTaskIndexes([{ key: index.key, options: index.options }], [candidate])[0]?.status === "existing";
18754
+ return classifyDataTaskIndexes([{ key: comparableIndexKey(index.key), options: index.options }], [candidate])[0]?.status === "existing";
18355
18755
  });
18356
18756
  }
18357
18757
  function plannedDocumentMatches(current, entry) {
@@ -18619,7 +19019,7 @@ async function restoreDataTaskPlan(plan, target, targetHint, lease) {
18619
19019
  await updateDataTaskBackup(safety, (manifest) => {
18620
19020
  manifest.pendingIndexes = [...manifest.pendingIndexes ?? [], pending];
18621
19021
  });
18622
- await collection.createIndex(index.key, { ...index.options, name: index.name });
19022
+ await collection.createIndex(comparableIndexKey(index.key), { ...index.options, name: index.name });
18623
19023
  if (!exactIndex(index, await collection.listIndexes())) {
18624
19024
  throw new DataTaskJobError("RESTORE_FAILED", `index "${index.name}" failed immediate readback.`, "restore");
18625
19025
  }