monsqlize 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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") {
@@ -532,6 +524,9 @@ function normalizePopulateConfig(path5) {
532
524
  }
533
525
 
534
526
  // src/capabilities/model/model-registry.ts
527
+ function defineModel(collectionName, definition) {
528
+ return { collectionName, definition };
529
+ }
535
530
  var Model = class {
536
531
  static {
537
532
  // collection name → registered model definition
@@ -546,21 +541,19 @@ var Model = class {
546
541
  /** Collection names that have been redefined (used by the runtime to trigger hot-reload). */
547
542
  this._redefinedNames = /* @__PURE__ */ new Set();
548
543
  }
549
- /**
550
- * Register a new model definition.
551
- * Throws MODEL_ALREADY_EXISTS if a model with the same name already exists.
552
- * Validates the definition and resolves the timestamps option.
553
- */
554
- static define(collectionName, definition) {
544
+ static define(collectionNameOrDescriptor, definition) {
545
+ const collectionName = typeof collectionNameOrDescriptor === "string" ? collectionNameOrDescriptor : collectionNameOrDescriptor.collectionName;
546
+ const modelDefinition = typeof collectionNameOrDescriptor === "string" ? definition : collectionNameOrDescriptor.definition;
555
547
  const normalizedName = validateCollectionName(collectionName);
556
548
  if (this.registry.has(normalizedName)) {
557
549
  throw createError(ErrorCodes.MODEL_ALREADY_EXISTS, `Model '${normalizedName}' is already defined.`);
558
550
  }
559
- validateDefinition(definition);
560
- processTimestamps(definition);
551
+ validateDefinition(modelDefinition);
552
+ const resolvedDefinition = modelDefinition;
553
+ processTimestamps(resolvedDefinition);
561
554
  this.registry.set(normalizedName, {
562
555
  collectionName: normalizedName,
563
- definition
556
+ definition: resolvedDefinition
564
557
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
565
558
  });
566
559
  this.bumpRevision(normalizedName);
@@ -717,6 +710,40 @@ function serializeDocument(document) {
717
710
  }
718
711
 
719
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
+ }
720
747
  var VERSION_LOOKUP_OPTION_KEYS = [
721
748
  "session",
722
749
  "readConcern",
@@ -802,11 +829,14 @@ function mapModelSchemaValidationErrors(errors = []) {
802
829
  }
803
830
  function validateModelSchemaPayload(context, document, options, metadata = {}) {
804
831
  const shouldValidate = context.validateEnabled || options?.validate === true;
805
- if (!shouldValidate) {
832
+ if (!shouldValidate || options?.skipValidation) {
806
833
  return document;
807
834
  }
808
- if (options?.skipValidation) {
809
- 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
+ );
810
840
  }
811
841
  if (!context.schemaCache || !context.schemaValidateFn) {
812
842
  return document;
@@ -840,22 +870,31 @@ function validateModelSchemaPayload(context, document, options, metadata = {}) {
840
870
  }
841
871
  return normalized;
842
872
  }
873
+ function buildModelVisibleSoftDeleteCondition(config) {
874
+ return { [config.field]: config.type === "boolean" ? { $ne: true } : null };
875
+ }
843
876
  function applyModelSoftDeleteFilter(query, options, softDeleteConfig) {
844
877
  if (!softDeleteConfig?.enabled) {
845
878
  return query ?? {};
846
879
  }
847
880
  const resolvedOptions = options ?? {};
848
881
  const resolvedQuery = query ?? {};
849
- 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)) {
850
889
  return resolvedQuery;
851
890
  }
852
891
  if (resolvedOptions.withDeleted) {
853
892
  return resolvedQuery;
854
893
  }
855
894
  if (resolvedOptions.onlyDeleted) {
856
- return { ...resolvedQuery, [softDeleteConfig.field]: { $ne: null } };
895
+ return { ...resolvedQuery, [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : { $ne: null } };
857
896
  }
858
- return { ...resolvedQuery, [softDeleteConfig.field]: null };
897
+ return { ...resolvedQuery, ...buildModelVisibleSoftDeleteCondition(softDeleteConfig) };
859
898
  }
860
899
  function applyModelInsertTimestamps(document, timestampsConfig, nowFactory) {
861
900
  if (!timestampsConfig) {
@@ -1150,7 +1189,9 @@ function projectPopulatedDocument(document, select, nestedPaths) {
1150
1189
  return document;
1151
1190
  }
1152
1191
  const nestedRelationFields = nestedPaths.map((path5) => normalizePopulateConfig(path5).path);
1153
- 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;
1154
1195
  }
1155
1196
  async function populateModelPath(context, docs, path5, traversalState) {
1156
1197
  const config = normalizePopulateConfig(path5);
@@ -1172,7 +1213,13 @@ async function populateModelPath(context, docs, path5, traversalState) {
1172
1213
  }
1173
1214
  const localValues = docs.map((doc) => getByPath(doc, relation.localField));
1174
1215
  const keys = unique(localValues.flatMap((value) => flattenComparableValues(value)));
1175
- 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) {
1176
1223
  for (const doc of docs) {
1177
1224
  doc[config.path] = relation.single ? null : [];
1178
1225
  }
@@ -1192,36 +1239,63 @@ async function populateModelPath(context, docs, path5, traversalState) {
1192
1239
  ...effectiveSelect,
1193
1240
  relation.foreignField,
1194
1241
  "_id",
1195
- ...collectNestedLocalFields(relatedModel, nestedPaths)
1242
+ ...Object.keys(config.sort ?? {}),
1243
+ ...collectNestedLocalFields(relatedModel, nestedPaths),
1244
+ ...relatedModel?.softDeleteConfig?.enabled ? [relatedModel.softDeleteConfig.field] : []
1196
1245
  ])] : [];
1197
- const relatedDocs = await relatedCollection.find({
1198
- [relation.foreignField]: { $in: keys },
1199
- ...config.match ?? {}
1200
- }, effectiveSelect ? {
1201
- projection: Object.fromEntries(projectionFields.map((field) => [field, 1]))
1202
- } : void 0);
1203
- let hydrated = relatedModel ? relatedModel.hydrateDocuments(relatedDocs) : relatedDocs.map((item) => ({ ...item }));
1204
- if (nestedPaths.length > 0 && relatedModel) {
1205
- if (state.depth + 1 > state.maxDepth) {
1206
- 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
+ });
1207
1270
  }
1208
- hydrated = await relatedModel.populateDocuments(hydrated, nestedPaths, {
1209
- depth: state.depth + 1,
1210
- maxDepth: state.maxDepth,
1211
- activeConfigs: state.activeConfigs
1212
- });
1213
- }
1214
- const grouped = groupBy(hydrated, (item) => getByPath(item, relation.foreignField));
1215
- for (const [index, doc] of docs.entries()) {
1216
- let matches = rebuildPopulateMatches(localValues[index], grouped, config.sort);
1217
- if (config.skip) {
1218
- 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;
1286
+ }
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)));
1219
1291
  }
1220
- if (config.limit !== void 0) {
1221
- matches = matches.slice(0, config.limit);
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;
1222
1298
  }
1223
- const projectedMatches = matches.map((item) => projectPopulatedDocument(item, effectiveSelect, nestedPaths));
1224
- doc[config.path] = relation.single ? projectedMatches[0] ?? null : projectedMatches;
1225
1299
  }
1226
1300
  return docs;
1227
1301
  } finally {
@@ -1422,12 +1496,28 @@ async function saveModelDocument(collection, document, options = {}) {
1422
1496
  document._id = result.insertedId;
1423
1497
  return document;
1424
1498
  }
1425
- async function removeModelDocument(collection, document) {
1499
+ async function removeModelDocument(collection, document, deleteOne = (filter) => collection.deleteOne(filter)) {
1426
1500
  if (document._id === void 0) {
1427
1501
  return false;
1428
1502
  }
1429
- const result = await collection.deleteOne({ _id: document._id });
1430
- 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;
1431
1521
  }
1432
1522
 
1433
1523
  // src/capabilities/model/model-soft-delete-helpers.ts
@@ -1435,7 +1525,53 @@ function deletedFilter(filter, softDeleteConfig) {
1435
1525
  if (!softDeleteConfig) {
1436
1526
  return filter ?? {};
1437
1527
  }
1438
- 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];
1439
1575
  }
1440
1576
  function findWithDeletedDocuments(context, query, options) {
1441
1577
  return new PopulatePromise(async (paths) => {
@@ -1478,7 +1614,7 @@ function restoreSoftDeletedDocuments(context, filter, options) {
1478
1614
  return Promise.resolve({ modifiedCount: 0 });
1479
1615
  }
1480
1616
  return context.collection.updateOne(
1481
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1617
+ deletedFilter(filter, softDeleteConfig),
1482
1618
  { $unset: { [softDeleteConfig.field]: 1 } },
1483
1619
  options
1484
1620
  );
@@ -1489,7 +1625,7 @@ function restoreManySoftDeletedDocuments(context, filter, options) {
1489
1625
  return Promise.resolve({ modifiedCount: 0 });
1490
1626
  }
1491
1627
  return context.collection.updateMany(
1492
- { ...filter ?? {}, [softDeleteConfig.field]: { $ne: null } },
1628
+ deletedFilter(filter, softDeleteConfig),
1493
1629
  { $unset: { [softDeleteConfig.field]: 1 } },
1494
1630
  options
1495
1631
  );
@@ -3105,6 +3241,9 @@ var modelWriteSourceStorage = new AsyncLocalStorage();
3105
3241
  function isPlainObject2(value) {
3106
3242
  return value !== null && typeof value === "object" && !Array.isArray(value);
3107
3243
  }
3244
+ function ownValue(record, key) {
3245
+ return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
3246
+ }
3108
3247
  function invalidConfig(path5, reason) {
3109
3248
  throw createError(
3110
3249
  ErrorCodes.INVALID_OPERATION,
@@ -3125,19 +3264,19 @@ function normalizeRule(input, path5) {
3125
3264
  if (!isPlainObject2(input)) {
3126
3265
  invalidConfig(path5, "rule must be a string mode or an object");
3127
3266
  }
3128
- const mode = input.mode ?? DEFAULT_RULE.mode;
3267
+ const mode = ownValue(input, "mode") ?? DEFAULT_RULE.mode;
3129
3268
  if (!ALLOWED_MODES.has(mode)) {
3130
3269
  invalidConfig(`${path5}.mode`, 'must be "allow-both" or "model-only"');
3131
3270
  }
3132
- const raw = input.raw ?? DEFAULT_RULE.raw;
3271
+ const raw = ownValue(input, "raw") ?? DEFAULT_RULE.raw;
3133
3272
  if (!ALLOWED_RAW.has(raw)) {
3134
3273
  invalidConfig(`${path5}.raw`, 'must be "inherit", "allow", or "block"');
3135
3274
  }
3136
- const management = input.management ?? DEFAULT_RULE.management;
3275
+ const management = ownValue(input, "management") ?? DEFAULT_RULE.management;
3137
3276
  if (!ALLOWED_MANAGEMENT.has(management)) {
3138
3277
  invalidConfig(`${path5}.management`, 'must be "inherit", "allow", or "block"');
3139
3278
  }
3140
- const onViolation = input.onViolation ?? DEFAULT_RULE.onViolation;
3279
+ const onViolation = ownValue(input, "onViolation") ?? DEFAULT_RULE.onViolation;
3141
3280
  if (!ALLOWED_VIOLATION_ACTIONS.has(onViolation)) {
3142
3281
  invalidConfig(`${path5}.onViolation`, 'must be "throw" or "warn"');
3143
3282
  }
@@ -3153,15 +3292,15 @@ function validateWritePathPolicyConfig(options) {
3153
3292
  }
3154
3293
  function normalizeWritePathPolicy(options) {
3155
3294
  if (options === void 0) {
3156
- return { default: { ...DEFAULT_RULE }, namespaces: {}, enabled: false };
3295
+ return { default: { ...DEFAULT_RULE }, namespaces: /* @__PURE__ */ Object.create(null), enabled: false };
3157
3296
  }
3158
3297
  if (!isPlainObject2(options)) {
3159
3298
  invalidConfig("writePathPolicy", "must be an object");
3160
3299
  }
3161
3300
  const typedOptions = options;
3162
- const defaultRule = normalizeRule(typedOptions.default, "writePathPolicy.default");
3163
- const namespacesInput = options.namespaces;
3164
- const namespaces = {};
3301
+ const defaultRule = normalizeRule(ownValue(typedOptions, "default"), "writePathPolicy.default");
3302
+ const namespacesInput = ownValue(options, "namespaces");
3303
+ const namespaces = /* @__PURE__ */ Object.create(null);
3165
3304
  if (namespacesInput !== void 0) {
3166
3305
  if (!isPlainObject2(namespacesInput)) {
3167
3306
  invalidConfig("writePathPolicy.namespaces", "must be an object");
@@ -3209,8 +3348,8 @@ function resolveWritePathRule(policy, namespace) {
3209
3348
  if (candidate === "default") {
3210
3349
  return { key: "default", rule: effectivePolicy.default };
3211
3350
  }
3212
- const rule = effectivePolicy.namespaces[candidate];
3213
- if (rule) {
3351
+ if (Object.prototype.hasOwnProperty.call(effectivePolicy.namespaces, candidate)) {
3352
+ const rule = effectivePolicy.namespaces[candidate];
3214
3353
  return { key: candidate, rule };
3215
3354
  }
3216
3355
  }
@@ -3353,16 +3492,17 @@ var fallbackModelIndexTasks = /* @__PURE__ */ new Map();
3353
3492
  function toCompatDefinition(definition) {
3354
3493
  return definition;
3355
3494
  }
3356
- function stableIndexStringify(value) {
3495
+ function stableIndexStringify(value, mode = "options") {
3357
3496
  if (value instanceof Date) {
3358
3497
  return JSON.stringify(value.toISOString());
3359
3498
  }
3360
3499
  if (Array.isArray(value)) {
3361
- return `[${value.map((item) => stableIndexStringify(item)).join(",")}]`;
3500
+ return `[${value.map((item) => stableIndexStringify(item, mode)).join(",")}]`;
3362
3501
  }
3363
3502
  if (value && typeof value === "object") {
3364
- const entries = Object.entries(value).filter(([, current]) => current !== void 0).sort(([left], [right]) => left.localeCompare(right));
3365
- 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(",")}}`;
3366
3506
  }
3367
3507
  return JSON.stringify(value) ?? "undefined";
3368
3508
  }
@@ -3396,7 +3536,7 @@ function declaredOptionEntries(options) {
3396
3536
  });
3397
3537
  }
3398
3538
  function indexOptionsMatch(existing, declared) {
3399
- if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
3539
+ if (stableIndexStringify(getExistingIndexKey(existing), "key") !== stableIndexStringify(declared.key, "key")) {
3400
3540
  return false;
3401
3541
  }
3402
3542
  for (const [name, value] of declaredOptionEntries(declared.options)) {
@@ -3415,8 +3555,8 @@ function findExistingIndexByName(existingIndexes, name) {
3415
3555
  return existingIndexes.find((index) => index.name === name);
3416
3556
  }
3417
3557
  function findExistingIndexByKey(existingIndexes, key) {
3418
- const fingerprint = stableIndexStringify(key);
3419
- return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
3558
+ const fingerprint = stableIndexStringify(key, "key");
3559
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index), "key") === fingerprint);
3420
3560
  }
3421
3561
  function createIndexEnsureError(message, result, cause) {
3422
3562
  return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
@@ -4156,7 +4296,7 @@ async function readNearestBookmark(cache, namespace, keyDims, targetPage) {
4156
4296
 
4157
4297
  // src/adapters/mongodb/queries/query-helpers.ts
4158
4298
  import { createHmac, timingSafeEqual } from "node:crypto";
4159
- import { ObjectId as ObjectId2 } from "mongodb";
4299
+ import { BSON, ObjectId as ObjectId2 } from "mongodb";
4160
4300
 
4161
4301
  // src/utils/normalize.ts
4162
4302
  function normalizeProjection(p) {
@@ -4254,12 +4394,17 @@ function shouldConvertQueryField(autoConvert, path5, depth) {
4254
4394
  if (!config.enabled || depth > config.maxDepth) return false;
4255
4395
  return shouldConvertObjectIdPath(path5, config);
4256
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
+ }
4257
4402
  function normalizeQueryArray(value, autoConvert, path5, depth) {
4258
4403
  return value.map((item, index) => {
4259
4404
  if (typeof item === "string" && item.length === 24 && ObjectId2.isValid(item) && shouldConvertQueryField(autoConvert, path5, depth)) {
4260
4405
  return new ObjectId2(item);
4261
4406
  }
4262
- if (item && typeof item === "object" && !Array.isArray(item) && !(item instanceof ObjectId2)) {
4407
+ if (isPlainQueryRecord(item)) {
4263
4408
  return normalizeQueryFilter(item, autoConvert, `${path5}[${index}]`, depth + 1);
4264
4409
  }
4265
4410
  return item;
@@ -4275,7 +4420,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4275
4420
  }
4276
4421
  for (const [key, value] of Object.entries(filter)) {
4277
4422
  if (key === "$and" || key === "$or" || key === "$nor") {
4278
- 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;
4279
4424
  continue;
4280
4425
  }
4281
4426
  const currentPath = fieldPath ? `${fieldPath}.${key}` : key;
@@ -4284,7 +4429,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4284
4429
  result[key] = shouldConvert ? new ObjectId2(value) : value;
4285
4430
  } else if (Array.isArray(value)) {
4286
4431
  result[key] = shouldConvert ? normalizeQueryArray(value, autoConvert, currentPath, depth + 1) : value;
4287
- } else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof ObjectId2)) {
4432
+ } else if (isPlainQueryRecord(value)) {
4288
4433
  const nested = value;
4289
4434
  const hasOperators = Object.keys(nested).some((k) => k.startsWith("$"));
4290
4435
  if (hasOperators) {
@@ -4296,7 +4441,7 @@ function normalizeQueryFilter(filter, autoConvert, fieldPath = "", depth = 0) {
4296
4441
  );
4297
4442
  } else if (shouldConvert && QUERY_OBJECTID_SCALAR_OPERATORS.has(op) && typeof opVal === "string" && opVal.length === 24 && ObjectId2.isValid(opVal)) {
4298
4443
  nestedResult[op] = new ObjectId2(opVal);
4299
- } else if (op === "$elemMatch" && opVal && typeof opVal === "object" && !Array.isArray(opVal)) {
4444
+ } else if (op === "$elemMatch" && isPlainQueryRecord(opVal)) {
4300
4445
  nestedResult[op] = normalizeQueryFilter(opVal, autoConvert, currentPath, depth + 1);
4301
4446
  } else {
4302
4447
  nestedResult[op] = opVal;
@@ -4470,36 +4615,29 @@ async function isCollectionCacheBarrierActive(queryCache, collection, defaults =
4470
4615
  buildCollectionCacheBarrierNamespaces(collection, defaults)
4471
4616
  );
4472
4617
  }
4473
- function normalizeCacheKeyValue(value) {
4474
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
4475
- return void 0;
4476
- }
4477
- if (value instanceof Date) {
4478
- return { $date: value.toISOString() };
4479
- }
4480
- if (value instanceof ObjectId2) {
4481
- return { $oid: value.toHexString() };
4482
- }
4483
- if (value instanceof RegExp) {
4484
- return { $regex: value.source, $flags: value.flags };
4485
- }
4486
- if (Array.isArray(value)) {
4487
- return value.map((item) => normalizeCacheKeyValue(item));
4488
- }
4489
- if (value && typeof value === "object") {
4490
- const sorted = {};
4491
- for (const key of Object.keys(value).sort()) {
4492
- const normalized = normalizeCacheKeyValue(value[key]);
4493
- if (normalized !== void 0) {
4494
- sorted[key] = normalized;
4495
- }
4496
- }
4497
- return sorted;
4498
- }
4499
- return value;
4500
- }
4501
- function stableCacheKeyString(value) {
4502
- 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))}`;
4503
4641
  }
4504
4642
  function buildResultCacheKeyOptions(options = {}) {
4505
4643
  const keyOptions = {};
@@ -4620,7 +4758,7 @@ function buildFindCacheKey(collection, defaults = {}, query, options) {
4620
4758
  ...options ?? {}
4621
4759
  });
4622
4760
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4623
- return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4761
+ return `find:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4624
4762
  }
4625
4763
  function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4626
4764
  const executeOptions = normalizeFindProjectionOptions({
@@ -4628,7 +4766,7 @@ function buildFindOneCacheKey(collection, defaults = {}, query, options) {
4628
4766
  ...options ?? {}
4629
4767
  });
4630
4768
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4631
- return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4769
+ return `findOne:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4632
4770
  }
4633
4771
  function buildCountCacheKey(collection, defaults = {}, query, options) {
4634
4772
  const merged = {
@@ -4638,7 +4776,7 @@ function buildCountCacheKey(collection, defaults = {}, query, options) {
4638
4776
  const { cache: _cache, ...keyOptions } = merged;
4639
4777
  void _cache;
4640
4778
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4641
- return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4779
+ return `count:${namespace}:${stableCacheKeyString(normalizeQueryForCache(query, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(keyOptions))}`;
4642
4780
  }
4643
4781
  function buildDistinctCacheKey(collection, defaults = {}, field, query, options) {
4644
4782
  const rawOptions = options ?? {};
@@ -4668,7 +4806,7 @@ function buildAggregateCacheKey(collection, defaults = {}, pipeline, options) {
4668
4806
  ...options ?? {}
4669
4807
  };
4670
4808
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4671
- return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults))}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4809
+ return `aggregate:${namespace}:${stableCacheKeyString(normalizePipelineForCache(pipeline, defaults), "document")}:${stableCacheKeyString(buildResultCacheKeyOptions(executeOptions))}`;
4672
4810
  }
4673
4811
  function buildFindOneByIdCacheKey(collection, defaults = {}, id, options) {
4674
4812
  const objectId = parseRequiredObjectId(id);
@@ -4683,6 +4821,8 @@ function buildFindByIdsCacheKey(collection, defaults = {}, ids, options) {
4683
4821
  function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4684
4822
  const ext = options;
4685
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;
4686
4826
  const payload = {
4687
4827
  query: normalizeQueryForCache(options.query, defaults),
4688
4828
  sort: normalizeSortShape(options.sort),
@@ -4704,9 +4844,10 @@ function buildFindPageCacheKey(collection, defaults = {}, options = {}) {
4704
4844
  hint: ext.hint,
4705
4845
  collation: ext.collation,
4706
4846
  batchSize: ext.batchSize,
4707
- options: options.options
4847
+ let: effectiveLet,
4848
+ options: nestedOptions
4708
4849
  };
4709
- const keyHash = hashPayload(payload);
4850
+ const keyHash = hashPayload(stableCacheKeyString(payload));
4710
4851
  const namespace = buildCollectionCacheNamespace(collection, defaults);
4711
4852
  return { key: `findPage:${namespace}:${keyHash}`, keyHash };
4712
4853
  }
@@ -5053,29 +5194,30 @@ async function runStrictUpdateMany(context, filter, update, options) {
5053
5194
  if (rawOptions.upsert === true) {
5054
5195
  throw createError(ErrorCodes.INVALID_ARGUMENT, 'versionMode "strict" does not support upsert.');
5055
5196
  }
5056
- const docs = await context.collection.find(filter, {
5057
- ...buildModelVersionLookupOptions(rawOptions, {
5058
- _id: 1,
5059
- [context.versionConfig.field]: 1
5060
- })
5061
- });
5062
5197
  let matchedCount = 0;
5063
5198
  let modifiedCount = 0;
5064
5199
  const conflictedIds = [];
5065
- for (const doc of docs) {
5066
- const id = doc._id;
5067
- const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateMany");
5068
- const result = await context.collection.updateOne(
5069
- { _id: id, [context.versionConfig.field]: expectedVersion },
5070
- update,
5071
- options
5072
- );
5073
- if ((result?.matchedCount ?? 0) === 0) {
5074
- conflictedIds.push(id);
5075
- 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;
5076
5220
  }
5077
- matchedCount += result.matchedCount ?? 0;
5078
- modifiedCount += result.modifiedCount ?? 0;
5079
5221
  }
5080
5222
  return {
5081
5223
  acknowledged: true,
@@ -5084,7 +5226,9 @@ async function runStrictUpdateMany(context, filter, update, options) {
5084
5226
  upsertedCount: 0,
5085
5227
  upsertedId: null,
5086
5228
  conflictCount: conflictedIds.length,
5087
- conflictedIds
5229
+ conflictedIds,
5230
+ skippedCount: skippedIds.length,
5231
+ skippedIds
5088
5232
  };
5089
5233
  }
5090
5234
  async function runStrictUpdateBatch(context, filter, update, options) {
@@ -5107,7 +5251,7 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5107
5251
  retryAttempts: _retryAttempts,
5108
5252
  retryDelay: _retryDelay,
5109
5253
  onRetry: _onRetry,
5110
- sort = { _id: 1 },
5254
+ sort: _sort,
5111
5255
  ...driverOptions
5112
5256
  } = rawOptions;
5113
5257
  void _batchSize;
@@ -5116,38 +5260,42 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5116
5260
  void _retryAttempts;
5117
5261
  void _retryDelay;
5118
5262
  void _onRetry;
5119
- const docs = await context.collection.find(filter, {
5120
- ...buildModelVersionLookupOptions(driverOptions, {
5121
- _id: 1,
5122
- [context.versionConfig.field]: 1
5123
- }),
5124
- sort
5125
- });
5263
+ void _sort;
5264
+ const countOptions = buildModelVersionLookupOptions(driverOptions, {});
5265
+ delete countOptions.projection;
5266
+ const totalCount = await context.collection.count(filter, countOptions);
5126
5267
  const result = {
5127
5268
  acknowledged: true,
5128
- totalCount: docs.length,
5269
+ totalCount,
5129
5270
  matchedCount: 0,
5130
5271
  modifiedCount: 0,
5131
5272
  upsertedCount: 0,
5132
- batchCount: Math.ceil(docs.length / batchSize),
5273
+ batchCount: 0,
5133
5274
  errors: [],
5134
5275
  retries: [],
5135
5276
  conflictCount: 0,
5136
- conflictedIds: []
5277
+ conflictedIds: [],
5278
+ skippedCount: 0,
5279
+ skippedIds: []
5137
5280
  };
5138
- for (let offset = 0; offset < docs.length; offset += batchSize) {
5139
- const batch = docs.slice(offset, offset + batchSize);
5281
+ for await (const batch of iterateModelCandidateBatches(context, filter, driverOptions, batchSize)) {
5282
+ result.batchCount++;
5140
5283
  for (const doc of batch) {
5141
5284
  const id = doc._id;
5142
5285
  const expectedVersion = assertNumericExpectedVersion(doc[context.versionConfig.field], "updateBatch");
5143
5286
  const updateResult = await context.collection.updateOne(
5144
- { _id: id, [context.versionConfig.field]: expectedVersion },
5287
+ buildStrictCandidateFilter(filter, id, context.versionConfig.field, expectedVersion),
5145
5288
  update,
5146
5289
  driverOptions
5147
5290
  );
5148
5291
  if ((updateResult?.matchedCount ?? 0) === 0) {
5149
- result.conflictCount = (result.conflictCount ?? 0) + 1;
5150
- 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
+ }
5151
5299
  continue;
5152
5300
  }
5153
5301
  result.matchedCount += updateResult.matchedCount ?? 0;
@@ -5155,15 +5303,16 @@ async function runStrictUpdateBatch(context, filter, update, options) {
5155
5303
  }
5156
5304
  if (typeof onProgress === "function") {
5157
5305
  onProgress({
5158
- currentBatch: Math.floor(offset / batchSize) + 1,
5159
- totalBatches: result.batchCount,
5306
+ currentBatch: result.batchCount,
5307
+ totalBatches: Math.ceil(totalCount / batchSize),
5160
5308
  modified: result.modifiedCount,
5161
5309
  matched: result.matchedCount,
5162
- total: docs.length,
5163
- 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,
5164
5312
  errors: result.errors.length,
5165
5313
  retries: result.retries.length,
5166
- conflicts: result.conflictCount ?? 0
5314
+ conflicts: result.conflictCount ?? 0,
5315
+ skipped: result.skippedCount ?? 0
5167
5316
  });
5168
5317
  }
5169
5318
  }
@@ -5176,10 +5325,7 @@ function buildSoftDeletePatch(config, now) {
5176
5325
  return { [config.field]: config.type === "boolean" ? true : now };
5177
5326
  }
5178
5327
  function buildSoftDeleteFilter(filter, config) {
5179
- return {
5180
- ...filter ?? {},
5181
- ...config?.enabled ? { [config.field]: null } : {}
5182
- };
5328
+ return applyModelSoftDeleteFilter(filter, {}, config);
5183
5329
  }
5184
5330
  function applyModelIncrementVersion(field, increment, versionConfig) {
5185
5331
  if (!versionConfig?.enabled || field === void 0) {
@@ -5219,7 +5365,8 @@ async function orchestrateModelInsertOne(context, document, options) {
5219
5365
  payload = validateModelSchemaPayload({
5220
5366
  validateEnabled: context.validateEnabled,
5221
5367
  schemaCache: context.schemaCache,
5222
- schemaValidateFn: context.schemaValidateFn
5368
+ schemaValidateFn: context.schemaValidateFn,
5369
+ schemaError: context.schemaError
5223
5370
  }, payload, options);
5224
5371
  payload = applyModelInsertTimestamps(payload, context.timestampsConfig, () => context.nowDate());
5225
5372
  payload = applyModelInsertVersion(payload, context.versionConfig);
@@ -5253,7 +5400,8 @@ async function orchestrateModelInsertMany(context, documents, options) {
5253
5400
  doc = validateModelSchemaPayload({
5254
5401
  validateEnabled: context.validateEnabled,
5255
5402
  schemaCache: context.schemaCache,
5256
- schemaValidateFn: context.schemaValidateFn
5403
+ schemaValidateFn: context.schemaValidateFn,
5404
+ schemaError: context.schemaError
5257
5405
  }, doc, resolvedOptions, { index });
5258
5406
  doc = applyModelInsertTimestamps(doc, context.timestampsConfig, () => context.nowDate());
5259
5407
  doc = applyModelInsertVersion(doc, context.versionConfig);
@@ -5338,7 +5486,8 @@ async function orchestrateModelReplaceOne(context, filter, replacement, options)
5338
5486
  let nextReplacement = validateModelSchemaPayload({
5339
5487
  validateEnabled: context.validateEnabled,
5340
5488
  schemaCache: context.schemaCache,
5341
- schemaValidateFn: context.schemaValidateFn
5489
+ schemaValidateFn: context.schemaValidateFn,
5490
+ schemaError: context.schemaError
5342
5491
  }, replacement, options);
5343
5492
  nextReplacement = preserveModelReplaceCreatedAt(
5344
5493
  replacement,
@@ -5397,7 +5546,8 @@ async function orchestrateModelFindOneAndReplace(context, filter, replacement, o
5397
5546
  let nextReplacement = validateModelSchemaPayload({
5398
5547
  validateEnabled: context.validateEnabled,
5399
5548
  schemaCache: context.schemaCache,
5400
- schemaValidateFn: context.schemaValidateFn
5549
+ schemaValidateFn: context.schemaValidateFn,
5550
+ schemaError: context.schemaError
5401
5551
  }, replacement, options);
5402
5552
  nextReplacement = preserveModelReplaceCreatedAt(
5403
5553
  replacement,
@@ -5509,7 +5659,8 @@ async function orchestrateModelInsertBatch(context, docs, options) {
5509
5659
  record = validateModelSchemaPayload({
5510
5660
  validateEnabled: context.validateEnabled,
5511
5661
  schemaCache: context.schemaCache,
5512
- schemaValidateFn: context.schemaValidateFn
5662
+ schemaValidateFn: context.schemaValidateFn,
5663
+ schemaError: context.schemaError
5513
5664
  }, record, resolvedOptions, { index });
5514
5665
  record = applyModelInsertTimestamps(record, context.timestampsConfig, () => context.nowDate());
5515
5666
  record = applyModelInsertVersion(record, context.versionConfig);
@@ -5587,7 +5738,7 @@ async function orchestrateModelDeleteOne(context, filter, options) {
5587
5738
  let result;
5588
5739
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5589
5740
  result = await context.collection.updateOne(
5590
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5741
+ buildSoftDeleteFilter(filter, softDeleteConfig),
5591
5742
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5592
5743
  options
5593
5744
  );
@@ -5621,7 +5772,7 @@ async function orchestrateModelDeleteMany(context, filter, options) {
5621
5772
  let result;
5622
5773
  if (softDeleteConfig?.enabled && !resolvedOptions._forceDelete) {
5623
5774
  result = await context.collection.updateMany(
5624
- { ...filter ?? {}, [softDeleteConfig.field]: null },
5775
+ { $and: [filter ?? {}, buildModelVisibleSoftDeleteCondition(softDeleteConfig)] },
5625
5776
  { $set: { [softDeleteConfig.field]: softDeleteConfig.type === "boolean" ? true : context.nowDate() } },
5626
5777
  options
5627
5778
  );
@@ -5785,7 +5936,7 @@ async function inspectDeclaredRelationUsage(context, targets, inputOptions, targ
5785
5936
  [inbound.localField]: { $in: targetValues }
5786
5937
  };
5787
5938
  const sourceSoftDelete = resolveModelSoftDeleteConfig(inbound.definition);
5788
- const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, { [sourceSoftDelete.field]: null }] } : baseQuery;
5939
+ const query = !options.includeSoftDeletedReferences && sourceSoftDelete?.enabled ? { $and: [baseQuery, buildModelVisibleSoftDeleteCondition(sourceSoftDelete)] } : baseQuery;
5789
5940
  try {
5790
5941
  const collection = context.runtime.scopedCollection(inbound.sourceCollection, scope);
5791
5942
  if (!collection) {
@@ -5967,7 +6118,7 @@ async function executeProtectedRelationDelete(host, filter, options, force) {
5967
6118
  const deleteOptions = stripRelationUsageOptions(options);
5968
6119
  let target;
5969
6120
  try {
5970
- 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);
5971
6122
  const rawTarget = await host.collection.findOne(
5972
6123
  targetFilter,
5973
6124
  relationTargetReadOptions(options, buildRelationUsageTargetProjection(host.modelName))
@@ -6051,7 +6202,7 @@ var ModelInstance = class {
6051
6202
  this.softDeleteConfig = this._softDeleteConfig;
6052
6203
  this._versionConfig = resolveModelVersionConfig(options.definition);
6053
6204
  this._v1HooksFactory = resolveModelHooksFactory(options.definition);
6054
- scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6205
+ if (!this._schemaError) scheduleModelIndexes(this.collection, options.definition, this._softDeleteConfig, {
6055
6206
  runtime: this.runtime,
6056
6207
  dbName: options.dbName,
6057
6208
  poolName: options.poolName,
@@ -6111,46 +6262,6 @@ var ModelInstance = class {
6111
6262
  }
6112
6263
  return result;
6113
6264
  }
6114
- isVisibleBySoftDelete(doc, options) {
6115
- if (!doc || !this._softDeleteConfig?.enabled) {
6116
- return Boolean(doc);
6117
- }
6118
- const rawOptions = options ?? {};
6119
- if (rawOptions.withDeleted) {
6120
- return true;
6121
- }
6122
- const value = doc[this._softDeleteConfig.field];
6123
- if (rawOptions.onlyDeleted) {
6124
- return value !== void 0 && value !== null;
6125
- }
6126
- return value === void 0 || value === null;
6127
- }
6128
- filterVisibleBySoftDelete(docs, options) {
6129
- return docs.filter((doc) => this.isVisibleBySoftDelete(doc, options));
6130
- }
6131
- applySoftDeleteFindPageOptions(options) {
6132
- const rawOptions = options ?? {};
6133
- return {
6134
- ...rawOptions,
6135
- query: applyModelSoftDeleteFilter(rawOptions.query, rawOptions, this._softDeleteConfig)
6136
- };
6137
- }
6138
- applySoftDeleteAggregatePipeline(pipeline, options) {
6139
- if (!this._softDeleteConfig?.enabled) {
6140
- return pipeline ?? [];
6141
- }
6142
- const rawOptions = options ?? {};
6143
- if (rawOptions.withDeleted) {
6144
- return pipeline ?? [];
6145
- }
6146
- const softDeleteMatch = applyModelSoftDeleteFilter({}, rawOptions, this._softDeleteConfig);
6147
- const matchStage = { $match: softDeleteMatch };
6148
- const stages = [...pipeline ?? []];
6149
- if (stages.length > 0 && stages[0] && typeof stages[0] === "object" && "$geoNear" in stages[0]) {
6150
- return [stages[0], matchStage, ...stages.slice(1)];
6151
- }
6152
- return [matchStage, ...stages];
6153
- }
6154
6265
  // ── public API ────────────────────────────────────────────────────────────────
6155
6266
  find(query, options) {
6156
6267
  return new PopulatePromise(async (paths) => {
@@ -6186,8 +6297,12 @@ var ModelInstance = class {
6186
6297
  }
6187
6298
  findOneById(id, options) {
6188
6299
  return new PopulatePromise(async (paths) => {
6189
- const doc = this._v1HooksFactory ? await this.runV1HookedOperation("find", [id, options], (nextId, nextOptions) => this.collection.findOneById(nextId, nextOptions)) : await this.collection.findOneById(id, options);
6190
- 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);
6191
6306
  });
6192
6307
  }
6193
6308
  findById(id, options) {
@@ -6195,13 +6310,25 @@ var ModelInstance = class {
6195
6310
  }
6196
6311
  findByIds(ids, options) {
6197
6312
  return new PopulatePromise(async (paths) => {
6198
- const docs = this._v1HooksFactory ? await this.runV1HookedOperation("find", [ids, options], (nextIds, nextOptions) => this.collection.findByIds(nextIds, nextOptions)) : await this.collection.findByIds(ids, options);
6199
- 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);
6200
6327
  });
6201
6328
  }
6202
6329
  findPage(options) {
6203
6330
  return new PopulatePromise(async (paths) => {
6204
- const filteredOptions = this.applySoftDeleteFindPageOptions(options);
6331
+ const filteredOptions = applySoftDeleteFindPageOptions(options, this._softDeleteConfig);
6205
6332
  const result = this._v1HooksFactory ? await this.runV1HookedOperation("find", [filteredOptions], (nextOptions) => this.collection.findPage(nextOptions)) : await this.collection.findPage(filteredOptions);
6206
6333
  return {
6207
6334
  ...result,
@@ -6353,7 +6480,7 @@ var ModelInstance = class {
6353
6480
  return this.runV1HookedOperation("find", [key, filteredQuery, options], (nextKey, nextQuery, nextOptions) => this.collection.distinct(nextKey, nextQuery, nextOptions));
6354
6481
  }
6355
6482
  aggregate(pipeline, options) {
6356
- const filteredPipeline = this.applySoftDeleteAggregatePipeline(pipeline, options);
6483
+ const filteredPipeline = applySoftDeleteAggregatePipeline(pipeline, options, this._softDeleteConfig);
6357
6484
  return this.runV1HookedOperation("find", [filteredPipeline, options], (nextPipeline, nextOptions) => {
6358
6485
  const execute = () => this.collection.aggregate(nextPipeline, nextOptions);
6359
6486
  if (resolveAggregateWriteTarget(Array.isArray(nextPipeline) ? nextPipeline : [])) {
@@ -6482,13 +6609,14 @@ var ModelInstance = class {
6482
6609
  schemaValidationContext: {
6483
6610
  validateEnabled: this._validateEnabled,
6484
6611
  schemaCache: this._schemaCache,
6485
- schemaValidateFn: this._schemaValidateFn
6612
+ schemaValidateFn: this._schemaValidateFn,
6613
+ schemaError: this._schemaError
6486
6614
  },
6487
6615
  nowFactory: () => this.nowDate()
6488
6616
  }));
6489
6617
  }
6490
6618
  async removeDocument(document) {
6491
- return this.runModelWrite(() => removeModelDocument(this.collection, document));
6619
+ return this.runModelWrite(() => removeModelDocument(this.collection, document, (filter) => this.deleteOne(filter)));
6492
6620
  }
6493
6621
  applyDefaults(document) {
6494
6622
  return applyModelDefaults(this.definition, document);
@@ -6506,6 +6634,7 @@ var ModelInstance = class {
6506
6634
  validateEnabled: this._validateEnabled,
6507
6635
  schemaCache: this._schemaCache,
6508
6636
  schemaValidateFn: this._schemaValidateFn,
6637
+ schemaError: this._schemaError,
6509
6638
  hooksFactory: this._v1HooksFactory,
6510
6639
  runHook: (hookName, context) => this.runHook(hookName, context)
6511
6640
  };
@@ -6521,6 +6650,11 @@ var ModelInstance = class {
6521
6650
 
6522
6651
  // src/capabilities/transaction/index.ts
6523
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
+ }
6524
6658
  function toPublicTransactionStatus(state) {
6525
6659
  return state === "active" ? "started" : state;
6526
6660
  }
@@ -6621,6 +6755,7 @@ var Transaction = class {
6621
6755
  this.options = options;
6622
6756
  this.id = `tx_${randomBytes2(8).toString("hex")}`;
6623
6757
  this.state = "pending";
6758
+ this.acceptingWrites = false;
6624
6759
  this.startedAt = null;
6625
6760
  this.timeoutTimer = null;
6626
6761
  this.pendingInvalidations = /* @__PURE__ */ new Set();
@@ -6639,6 +6774,7 @@ var Transaction = class {
6639
6774
  }
6640
6775
  this.session.startTransaction(this.options.transactionOptions);
6641
6776
  this.state = "active";
6777
+ this.acceptingWrites = true;
6642
6778
  this.startedAt = Date.now();
6643
6779
  const timeout = this.options.timeout ?? 3e4;
6644
6780
  if (timeout > 0) {
@@ -6661,6 +6797,7 @@ var Transaction = class {
6661
6797
  if (this.state !== "active") {
6662
6798
  throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
6663
6799
  }
6800
+ this.acceptingWrites = false;
6664
6801
  if (typeof this.session.commitTransaction === "function") {
6665
6802
  await commitTransactionWithRetry(this.session, this.options.logger);
6666
6803
  }
@@ -6686,6 +6823,7 @@ var Transaction = class {
6686
6823
  * @since v1.4.0
6687
6824
  */
6688
6825
  async abort() {
6826
+ this.acceptingWrites = false;
6689
6827
  if (this.state !== "pending" && this.state !== "active") {
6690
6828
  return;
6691
6829
  }
@@ -6710,10 +6848,16 @@ var Transaction = class {
6710
6848
  * @since v1.4.0
6711
6849
  */
6712
6850
  async end() {
6851
+ this.acceptingWrites = false;
6713
6852
  this.clearTimeout();
6714
6853
  this.options.lockManager?.releaseLocks(this.id);
6715
6854
  await this.session.endSession();
6716
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
+ }
6717
6861
  /**
6718
6862
  * Record a cache invalidation intent.
6719
6863
  * @since v1.4.0
@@ -6887,12 +7031,12 @@ var TransactionManager = class {
6887
7031
  return result;
6888
7032
  } catch (error) {
6889
7033
  lastError = error;
6890
- await transaction.abort();
6891
- this.recordStats(transaction, Date.now() - startedAt, false);
6892
7034
  if (transaction.state === "committed") {
6893
7035
  throw error;
6894
7036
  }
6895
- 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)) {
6896
7040
  throw error;
6897
7041
  }
6898
7042
  await sleep(retryDelay * Math.pow(retryBackoff, attempt));
@@ -6993,22 +7137,34 @@ function stringifySessionId(id) {
6993
7137
  }
6994
7138
  return String(id);
6995
7139
  }
6996
- function isTransientTransactionError(error) {
6997
- if (!error || typeof error !== "object") {
6998
- return false;
6999
- }
7000
- const candidate = error;
7001
- if (typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("TransientTransactionError")) {
7002
- 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
+ }
7003
7159
  }
7004
- return candidate.code === 112 || candidate.code === 117;
7160
+ return false;
7005
7161
  }
7006
7162
  function isUnknownTransactionCommitResult(error) {
7007
- if (!error || typeof error !== "object") {
7008
- return false;
7009
- }
7010
- const candidate = error;
7011
- 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");
7012
7168
  }
7013
7169
  async function commitTransactionWithRetry(session, logger) {
7014
7170
  const maxAttempts = 3;
@@ -7524,26 +7680,27 @@ var CountQueue = class {
7524
7680
  * @returns The result of fn
7525
7681
  * @throws A controlled error when the queue is full or the wait times out
7526
7682
  */
7527
- async execute(fn) {
7683
+ async execute(fn, options = {}) {
7528
7684
  const startTime = Date.now();
7685
+ const signal = options.signal;
7686
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7529
7687
  if (this.running >= this.concurrency) {
7530
7688
  if (this.queue.length >= this.maxQueueSize) {
7531
7689
  this.stats.rejected += 1;
7532
7690
  throw createError(ErrorCodes.INVALID_OPERATION, `Count queue is full (${this.maxQueueSize})`);
7533
7691
  }
7534
7692
  this.stats.queued += 1;
7535
- await this._waitInQueue(startTime);
7693
+ await this._waitInQueue(startTime, signal);
7536
7694
  }
7695
+ if (signal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
7537
7696
  this.running += 1;
7538
7697
  this.stats.executed += 1;
7539
- try {
7540
- const elapsed = Date.now() - startTime;
7541
- const remainingMs = Math.max(1, this.timeout - elapsed);
7542
- return await this._executeWithTimeout(fn, remainingMs);
7543
- } finally {
7698
+ const elapsed = Date.now() - startTime;
7699
+ const remainingMs = Math.max(1, this.timeout - elapsed);
7700
+ return this._executeWithTimeout(fn, remainingMs, signal, () => {
7544
7701
  this.running -= 1;
7545
7702
  this._wakeNext();
7546
- }
7703
+ });
7547
7704
  }
7548
7705
  /**
7549
7706
  * Get queue statistics (including live state).
@@ -7578,17 +7735,28 @@ var CountQueue = class {
7578
7735
  const entry = this.queue.shift();
7579
7736
  if (entry) {
7580
7737
  clearTimeout(entry.timer);
7738
+ entry.cleanup();
7581
7739
  entry.reject(createError(ErrorCodes.INVALID_OPERATION, "Count queue was cleared before execution"));
7582
7740
  }
7583
7741
  }
7584
7742
  }
7585
- _waitInQueue(startTime) {
7743
+ _waitInQueue(startTime, signal) {
7586
7744
  return new Promise((resolve, reject) => {
7587
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
+ };
7588
7755
  const timer = setTimeout(() => {
7589
7756
  const index = this.queue.indexOf(entry);
7590
7757
  if (index !== -1) {
7591
7758
  this.queue.splice(index, 1);
7759
+ entry.cleanup();
7592
7760
  this.stats.timeout += 1;
7593
7761
  reject(createError(ErrorCodes.OPERATION_TIMEOUT, `Count queue wait timeout (${this.timeout}ms)`));
7594
7762
  }
@@ -7601,9 +7769,12 @@ var CountQueue = class {
7601
7769
  },
7602
7770
  reject,
7603
7771
  timer,
7604
- startTime
7772
+ startTime,
7773
+ cleanup: () => signal?.removeEventListener("abort", onAbort)
7605
7774
  };
7606
7775
  this.queue.push(entry);
7776
+ signal?.addEventListener("abort", onAbort, { once: true });
7777
+ if (signal?.aborted) onAbort();
7607
7778
  });
7608
7779
  }
7609
7780
  _wakeNext() {
@@ -7611,6 +7782,7 @@ var CountQueue = class {
7611
7782
  const entry = this.queue.shift();
7612
7783
  if (entry) {
7613
7784
  clearTimeout(entry.timer);
7785
+ entry.cleanup();
7614
7786
  entry.resolve();
7615
7787
  }
7616
7788
  }
@@ -7627,10 +7799,12 @@ var CountQueue = class {
7627
7799
  }
7628
7800
  return value;
7629
7801
  }
7630
- _executeWithTimeout(fn, timeoutMs = this.timeout) {
7802
+ _executeWithTimeout(fn, timeoutMs, signal, release) {
7631
7803
  let timer = null;
7632
7804
  const controller = new AbortController();
7633
- const timeoutPromise = new Promise((_, reject) => {
7805
+ let rejectCancellation;
7806
+ const cancellation = new Promise((_, reject) => {
7807
+ rejectCancellation = reject;
7634
7808
  timer = setTimeout(
7635
7809
  () => {
7636
7810
  controller.abort();
@@ -7640,14 +7814,22 @@ var CountQueue = class {
7640
7814
  timeoutMs
7641
7815
  );
7642
7816
  });
7817
+ const onAbort = () => {
7818
+ controller.abort();
7819
+ rejectCancellation(createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted"));
7820
+ };
7821
+ signal?.addEventListener("abort", onAbort, { once: true });
7643
7822
  let taskPromise;
7644
7823
  try {
7645
7824
  taskPromise = Promise.resolve(fn(controller.signal));
7646
7825
  } catch (error) {
7647
7826
  taskPromise = Promise.reject(error);
7648
7827
  }
7649
- return Promise.race([taskPromise, timeoutPromise]).finally(() => {
7828
+ void taskPromise.then(release, release);
7829
+ if (signal?.aborted) onAbort();
7830
+ return Promise.race([taskPromise, cancellation]).finally(() => {
7650
7831
  if (timer !== null) clearTimeout(timer);
7832
+ signal?.removeEventListener("abort", onAbort);
7651
7833
  });
7652
7834
  }
7653
7835
  _updateWaitTimeStats(waitTime) {
@@ -7660,6 +7842,7 @@ var CountQueue = class {
7660
7842
  };
7661
7843
 
7662
7844
  // src/capabilities/pool/pool-health-checker.ts
7845
+ var SILENT_HEALTH_CHECKER_LOGGER = {};
7663
7846
  var HealthChecker = class {
7664
7847
  constructor(options = {}) {
7665
7848
  this._healthStatus = /* @__PURE__ */ new Map();
@@ -7669,7 +7852,7 @@ var HealthChecker = class {
7669
7852
  this._inProgress = /* @__PURE__ */ new Set();
7670
7853
  this._started = false;
7671
7854
  this._poolManager = options.poolManager ?? null;
7672
- this._logger = options.logger ?? console;
7855
+ this._logger = options.logger ?? SILENT_HEALTH_CHECKER_LOGGER;
7673
7856
  }
7674
7857
  register(poolNameOrConfig, configOrClient) {
7675
7858
  let poolName;
@@ -7793,6 +7976,7 @@ var HealthChecker = class {
7793
7976
  };
7794
7977
 
7795
7978
  // src/capabilities/pool/pool-selector.ts
7979
+ var SILENT_POOL_SELECTOR_LOGGER = {};
7796
7980
  var POOL_STRATEGY_ALIASES = {
7797
7981
  "round-robin": "roundRobin",
7798
7982
  "least-connections": "leastConnections",
@@ -7806,7 +7990,7 @@ var PoolSelector = class {
7806
7990
  constructor(options = {}) {
7807
7991
  this._roundRobinIndex = /* @__PURE__ */ new Map();
7808
7992
  this._strategy = normalizePoolStrategy(options.strategy);
7809
- this._logger = options.logger ?? console;
7993
+ this._logger = options.logger ?? SILENT_POOL_SELECTOR_LOGGER;
7810
7994
  }
7811
7995
  select(pools, context) {
7812
7996
  if (!pools || pools.length === 0) {
@@ -7937,11 +8121,12 @@ var PoolSelector = class {
7937
8121
  };
7938
8122
 
7939
8123
  // src/capabilities/pool/pool-stats-manager.ts
8124
+ var SILENT_POOL_STATS_LOGGER = {};
7940
8125
  var PoolStatsManager = class {
7941
8126
  constructor(options = {}) {
7942
8127
  this._stats = /* @__PURE__ */ new Map();
7943
8128
  this._buffer = [];
7944
- this._logger = options.logger ?? console;
8129
+ this._logger = options.logger ?? SILENT_POOL_STATS_LOGGER;
7945
8130
  this._batchInterval = setInterval(() => {
7946
8131
  this._flush();
7947
8132
  }, 100);
@@ -8173,6 +8358,9 @@ var ConnectionPoolManager = class {
8173
8358
  this._closed = false;
8174
8359
  this._configs = /* @__PURE__ */ new Map();
8175
8360
  this._pendingAdds = /* @__PURE__ */ new Set();
8361
+ this._pendingAddTasks = /* @__PURE__ */ new Set();
8362
+ this._generation = 0;
8363
+ this._closePromise = null;
8176
8364
  this.logger = options.logger ?? null;
8177
8365
  this.maxPoolsCount = options.maxPoolsCount ?? 10;
8178
8366
  this.strategy = options.poolStrategy ?? "auto";
@@ -8189,9 +8377,9 @@ var ConnectionPoolManager = class {
8189
8377
  this._fallbackConfig = this.fallback;
8190
8378
  this._fallback = this.fallback;
8191
8379
  this._pools = this.pools;
8192
- this._selector = new PoolSelector({ strategy: this.strategy });
8193
- this._healthChecker = new HealthChecker({ poolManager: this });
8194
- this._stats = new PoolStatsManager({ logger: options.logger ?? void 0 });
8380
+ this._selector = new PoolSelector({ strategy: this.strategy, logger: this.logger ?? void 0 });
8381
+ this._healthChecker = new HealthChecker({ poolManager: this, logger: this.logger ?? void 0 });
8382
+ this._stats = new PoolStatsManager({ logger: this.logger ?? void 0 });
8195
8383
  }
8196
8384
  /**
8197
8385
  * Add a connection pool.
@@ -8199,35 +8387,21 @@ var ConnectionPoolManager = class {
8199
8387
  */
8200
8388
  async addPool(config) {
8201
8389
  validatePoolConfig(config);
8390
+ if (this._closed) {
8391
+ throw createError(ErrorCodes.INVALID_OPERATION, "Connection pool manager is closed");
8392
+ }
8202
8393
  if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
8203
8394
  throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8204
8395
  }
8205
- if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
8396
+ if (this.maxPoolsCount > 0 && this.pools.size + this._pendingAdds.size >= this.maxPoolsCount) {
8206
8397
  throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
8207
8398
  }
8208
8399
  this._pendingAdds.add(config.name);
8400
+ const generation = this._generation;
8401
+ const task = this._finishAddPool(config, generation);
8402
+ this._pendingAddTasks.add(task);
8209
8403
  try {
8210
- const client = await this.clientFactory(config);
8211
- if (this.pools.has(config.name)) {
8212
- await client.close().catch(() => {
8213
- });
8214
- throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
8215
- }
8216
- this.pools.set(config.name, {
8217
- client,
8218
- config,
8219
- createdAt: Date.now()
8220
- });
8221
- this.healthStatus.set(config.name, {
8222
- status: "up",
8223
- consecutiveFailures: 0,
8224
- lastCheckTime: null,
8225
- lastError: null,
8226
- uptime: 0
8227
- });
8228
- this.stats.set(config.name, createEmptyPoolStats(config.name));
8229
- this._configs.set(config.name, config);
8230
- this._healthChecker.register(config.name, config.healthCheck ?? {});
8404
+ await task;
8231
8405
  } catch (err) {
8232
8406
  const error = err;
8233
8407
  const msg = error.message ?? "";
@@ -8241,8 +8415,31 @@ var ConnectionPoolManager = class {
8241
8415
  throw err;
8242
8416
  } finally {
8243
8417
  this._pendingAdds.delete(config.name);
8418
+ this._pendingAddTasks.delete(task);
8244
8419
  }
8245
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
+ );
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 ?? {});
8442
+ }
8246
8443
  /**
8247
8444
  * Remove a connection pool.
8248
8445
  * @since v1.0.8
@@ -8398,16 +8595,24 @@ var ConnectionPoolManager = class {
8398
8595
  * @since v1.0.8
8399
8596
  */
8400
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() {
8401
8605
  this.stopHealthCheck();
8402
8606
  this._healthChecker.stop();
8403
8607
  this._stats.close();
8608
+ await Promise.allSettled([...this._pendingAddTasks]);
8404
8609
  for (const pool of this.pools.values()) {
8405
8610
  await pool.client.close();
8406
8611
  }
8407
8612
  this.pools.clear();
8408
8613
  this.healthStatus.clear();
8409
8614
  this.stats.clear();
8410
- this._closed = true;
8615
+ this._configs.clear();
8411
8616
  }
8412
8617
  // ─── v1 compat methods ───────────────────────────────────────────────────────────
8413
8618
  /** v1 compat: get pool health Map */
@@ -8595,6 +8800,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
8595
8800
  logger
8596
8801
  });
8597
8802
  } catch (err) {
8803
+ if (err?.code === ErrorCodes.INVALID_CONFIG) throw err;
8598
8804
  logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
8599
8805
  return null;
8600
8806
  }
@@ -8673,6 +8879,33 @@ async function loadModelFiles(options, logger, opts = {}) {
8673
8879
  }
8674
8880
  }
8675
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
+
8676
8909
  // src/entry/runtime-defaults.ts
8677
8910
  function isPlainObject3(value) {
8678
8911
  if (value === null || typeof value !== "object") return false;
@@ -9323,36 +9556,36 @@ var FUNC_REGEX = /^(CONCAT|UPPER|LOWER|TRIM|SUBSTR|LENGTH|ABS|CEIL|FLOOR|ROUND|S
9323
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;
9324
9557
  function compileInnerExpression(expression) {
9325
9558
  const expr2 = expression.trim();
9326
- const funcMatch = expr2.match(FUNC_REGEX);
9327
- if (funcMatch) {
9328
- return dispatchFunction(funcMatch[1].toUpperCase(), funcMatch[2] ?? "");
9329
- }
9330
- const andParts = splitTopLevel2(expr2, "&&");
9331
- if (andParts.length > 1) {
9332
- 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
+ );
9333
9578
  }
9334
- const orParts = splitTopLevel2(expr2, "||");
9579
+ const orParts = splitTopLevel2(expr2, "||", positions);
9335
9580
  if (orParts.length > 1) {
9336
- return { $or: orParts.map((part) => compileInnerExpression(part.trim())) };
9337
- }
9338
- const ternary = /^([^?]+)\s*\?\s*([^:]+)\s*:\s*(.+)$/.exec(expr2);
9339
- if (ternary) {
9340
- const [, condition, thenPart, elsePart] = ternary;
9341
- return {
9342
- $cond: {
9343
- if: compileInnerExpression(condition.trim()),
9344
- then: parseThenElse(thenPart.trim()),
9345
- else: parseThenElse(elsePart.trim())
9346
- }
9347
- };
9581
+ return { $or: orParts.map((part) => compileInnerExpression(part)) };
9348
9582
  }
9349
- const nullCoalParts = splitTopLevel2(expr2, "??");
9350
- if (nullCoalParts.length > 1) {
9351
- 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)) };
9352
9586
  }
9353
- const cmpMatch = /^(.+?)\s*(===|!==|>=|<=|>|<)\s*(.+)$/.exec(expr2);
9354
- if (cmpMatch) {
9355
- const [, left, operator, right] = cmpMatch;
9587
+ const comparison = findBinaryOperator(expr2, positions, ["===", "!==", ">=", "<=", ">", "<"]);
9588
+ if (comparison) {
9356
9589
  const operatorMap = {
9357
9590
  "===": "$eq",
9358
9591
  "!==": "$ne",
@@ -9361,24 +9594,39 @@ function compileInnerExpression(expression) {
9361
9594
  ">": "$gt",
9362
9595
  "<": "$lt"
9363
9596
  };
9364
- return {
9365
- [operatorMap[operator]]: [
9366
- compileInnerExpression(left.trim()),
9367
- compileInnerExpression(right.trim())
9368
- ]
9369
- };
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
+ ] };
9370
9616
  }
9371
- const addSubMatch = /^(.+?)\s*([+\-])\s*(.+)$/.exec(expr2);
9372
- if (addSubMatch) {
9373
- const [, left, operator, right] = addSubMatch;
9374
- const operatorMap = { "+": "$add", "-": "$subtract" };
9375
- return { [operatorMap[operator]]: [parseOperand(left.trim()), parseOperand(right.trim())] };
9617
+ if (expr2.startsWith("-")) {
9618
+ return { $multiply: [-1, compileInnerExpression(expr2.slice(1))] };
9376
9619
  }
9377
- const mulDivMatch = /^(.+?)\s*([*\/%])\s*(.+)$/.exec(expr2);
9378
- if (mulDivMatch) {
9379
- const [, left, operator, right] = mulDivMatch;
9380
- const operatorMap = { "*": "$multiply", "/": "$divide", "%": "$mod" };
9381
- 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
+ }
9382
9630
  }
9383
9631
  const genericFuncCallRe = /^[A-Za-z_][A-Za-z0-9_]*\s*\(.+\)$/;
9384
9632
  if (genericFuncCallRe.test(expr2)) {
@@ -9387,9 +9635,6 @@ function compileInnerExpression(expression) {
9387
9635
  }
9388
9636
  return parseValue(expr2);
9389
9637
  }
9390
- function parseThenElse(source) {
9391
- return source.includes("?") && source.includes(":") ? compileInnerExpression(source) : parseValue(source);
9392
- }
9393
9638
  function parseValue(value) {
9394
9639
  const normalized = stripOuterParentheses(value.trim());
9395
9640
  if (normalized.startsWith("'") && normalized.endsWith("'") || normalized.startsWith('"') && normalized.endsWith('"')) {
@@ -9399,18 +9644,22 @@ function parseValue(value) {
9399
9644
  if (normalized === "true") return true;
9400
9645
  if (normalized === "false") return false;
9401
9646
  if (!isNaN(Number(normalized)) && normalized !== "") return Number(normalized);
9402
- if (IS_FUNC_CALL_RE.test(normalized)) {
9647
+ if (normalized !== value.trim() || IS_FUNC_CALL_RE.test(normalized) || FUNC_REGEX.test(normalized)) {
9403
9648
  return compileInnerExpression(normalized);
9404
9649
  }
9405
- if (normalized.startsWith("$")) return normalized;
9406
- return `$${normalized}`;
9407
- }
9408
- function parseOperand(value) {
9409
- const normalized = stripOuterParentheses(value);
9410
- 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]))) {
9411
9659
  return compileInnerExpression(normalized);
9412
9660
  }
9413
- return parseValue(normalized);
9661
+ if (normalized.startsWith("$")) return normalized;
9662
+ return `$${normalized}`;
9414
9663
  }
9415
9664
  function dispatchFunction(name, argsStr) {
9416
9665
  const args = splitArgsStr(argsStr);
@@ -9712,72 +9961,82 @@ function compileMapExpression(exprStr, varName) {
9712
9961
  return compileInnerExpression(replaced);
9713
9962
  }
9714
9963
  function splitArgsStr(argsStr) {
9715
- const args = [];
9716
- let current = "";
9717
- let inString = false;
9718
- let stringChar = "";
9719
- let parenDepth = 0;
9720
- for (let index = 0; index < argsStr.length; index++) {
9721
- const ch = argsStr[index];
9722
- if ((ch === '"' || ch === "'") && (index === 0 || argsStr[index - 1] !== "\\")) {
9723
- if (!inString) {
9724
- inString = true;
9725
- stringChar = ch;
9726
- } else if (ch === stringChar) {
9727
- inString = false;
9728
- stringChar = "";
9729
- }
9730
- current += ch;
9731
- } else if (ch === "(" && !inString) {
9732
- parenDepth++;
9733
- current += ch;
9734
- } else if (ch === ")" && !inString) {
9735
- parenDepth--;
9736
- current += ch;
9737
- } else if (ch === "," && !inString && parenDepth === 0) {
9738
- args.push(current.trim());
9739
- current = "";
9740
- } else {
9741
- current += ch;
9742
- }
9743
- }
9744
- if (current.trim()) args.push(current.trim());
9745
- return args;
9964
+ if (!argsStr.trim()) return [];
9965
+ return splitTopLevel2(argsStr, ",");
9746
9966
  }
9747
- function splitTopLevel2(source, separator) {
9748
- const parts = [];
9749
- let current = "";
9750
- let parenDepth = 0;
9751
- let inString = false;
9752
- let stringChar = "";
9967
+ function topLevelPositions(source) {
9968
+ const positions = [];
9969
+ const stack = [];
9970
+ let quote = null;
9753
9971
  for (let index = 0; index < source.length; index++) {
9754
9972
  const ch = source[index];
9755
- if ((ch === '"' || ch === "'") && (index === 0 || source[index - 1] !== "\\")) {
9756
- if (!inString) {
9757
- inString = true;
9758
- stringChar = ch;
9759
- } else if (ch === stringChar) {
9760
- inString = false;
9761
- 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");
9762
9989
  }
9763
- current += ch;
9764
- } else if (!inString && ch === "(") {
9765
- parenDepth++;
9766
- current += ch;
9767
- } else if (!inString && ch === ")") {
9768
- parenDepth--;
9769
- current += ch;
9770
- } else if (!inString && parenDepth === 0 && source.startsWith(separator, index)) {
9771
- parts.push(current);
9772
- current = "";
9773
- index += separator.length - 1;
9774
- } else {
9775
- current += ch;
9990
+ continue;
9776
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}`);
9777
10011
  }
9778
- parts.push(current);
9779
10012
  return parts;
9780
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
+ }
9781
10040
  function stripOuterParentheses(source) {
9782
10041
  let normalized = source.trim();
9783
10042
  while (normalized.startsWith("(") && normalized.endsWith(")") && isWrappedByOuterParentheses(normalized)) {
@@ -9823,10 +10082,15 @@ function isExpressionObject(value) {
9823
10082
  value && typeof value === "object" && "__expr__" in value && typeof value.__expr__ === "string" && "__compiled__" in value && typeof value.__compiled__ === "boolean"
9824
10083
  );
9825
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
+ }
9826
10090
  function hasExpressionInObject(value) {
9827
10091
  if (isExpressionObject(value)) return true;
9828
- if (!value || typeof value !== "object") return false;
9829
10092
  if (Array.isArray(value)) return value.some((item) => hasExpressionInObject(item));
10093
+ if (!isPlainExpressionRecord(value)) return false;
9830
10094
  return Object.values(value).some((item) => hasExpressionInObject(item));
9831
10095
  }
9832
10096
  function hasExpressionInPipeline(pipeline) {
@@ -9843,7 +10107,7 @@ function transformExpressions(value, context) {
9843
10107
  if (Array.isArray(value)) {
9844
10108
  return value.map((item) => transformStageEntry(item));
9845
10109
  }
9846
- if (!value || typeof value !== "object") return value;
10110
+ if (!isPlainExpressionRecord(value)) return value;
9847
10111
  const entries = Object.entries(value);
9848
10112
  const result = {};
9849
10113
  for (const [key, current] of entries) {
@@ -9852,7 +10116,7 @@ function transformExpressions(value, context) {
9852
10116
  return result;
9853
10117
  }
9854
10118
  function transformStageEntry(stage) {
9855
- if (!stage || typeof stage !== "object" || Array.isArray(stage)) {
10119
+ if (!isPlainExpressionRecord(stage)) {
9856
10120
  return transformExpressions(stage, "project");
9857
10121
  }
9858
10122
  const entries = Object.entries(stage);
@@ -9876,7 +10140,7 @@ function transformStageValue(value, context) {
9876
10140
  if (Array.isArray(value)) {
9877
10141
  return value.map((item) => transformStageValue(item, context));
9878
10142
  }
9879
- if (!value || typeof value !== "object") return value;
10143
+ if (!isPlainExpressionRecord(value)) return value;
9880
10144
  const entries = Object.entries(value);
9881
10145
  const result = {};
9882
10146
  for (const [key, current] of entries) {
@@ -10807,6 +11071,7 @@ var AggregateChain = class {
10807
11071
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).explain(verbosity === true ? "queryPlanner" : verbosity);
10808
11072
  }
10809
11073
  stream() {
11074
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10810
11075
  const stream = this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).stream();
10811
11076
  const onWriteComplete = this.writePipelineHooks?.onWriteComplete;
10812
11077
  if (onWriteComplete) {
@@ -10824,6 +11089,7 @@ var AggregateChain = class {
10824
11089
  throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
10825
11090
  }
10826
11091
  this.executed = true;
11092
+ if (this.writePipelineHooks) assertManagedTransactionWriteAllowed(this.buildExecuteOptions());
10827
11093
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray().then(async (result) => {
10828
11094
  await this.writePipelineHooks?.onWriteComplete?.();
10829
11095
  return result;
@@ -11140,48 +11406,59 @@ function stripWriteCacheControlOptions(options) {
11140
11406
 
11141
11407
  // src/adapters/mongodb/writes/write-basic.ts
11142
11408
  async function insertOneDocument(collection, ...args) {
11409
+ assertManagedTransactionWriteAllowed(args[1]);
11143
11410
  return collection.insertOne(...args);
11144
11411
  }
11145
11412
  async function insertManyDocuments(collection, ...args) {
11413
+ assertManagedTransactionWriteAllowed(args[1]);
11146
11414
  return collection.insertMany(...args);
11147
11415
  }
11148
11416
  async function updateOneDocument(collection, ...args) {
11417
+ assertManagedTransactionWriteAllowed(args[2]);
11149
11418
  return collection.updateOne(...args);
11150
11419
  }
11151
11420
  async function updateManyDocuments(collection, ...args) {
11421
+ assertManagedTransactionWriteAllowed(args[2]);
11152
11422
  return collection.updateMany(...args);
11153
11423
  }
11154
11424
  async function replaceOneDocument(collection, ...args) {
11425
+ assertManagedTransactionWriteAllowed(args[2]);
11155
11426
  return collection.replaceOne(...args);
11156
11427
  }
11157
11428
  async function findOneAndUpdateDocument(collection, filter, update, options) {
11429
+ assertManagedTransactionWriteAllowed(options);
11158
11430
  if (options !== void 0) {
11159
11431
  return collection.findOneAndUpdate(filter, update, options);
11160
11432
  }
11161
11433
  return collection.findOneAndUpdate(filter, update);
11162
11434
  }
11163
11435
  async function findOneAndReplaceDocument(collection, filter, replacement, options) {
11436
+ assertManagedTransactionWriteAllowed(options);
11164
11437
  if (options !== void 0) {
11165
11438
  return collection.findOneAndReplace(filter, replacement, options);
11166
11439
  }
11167
11440
  return collection.findOneAndReplace(filter, replacement);
11168
11441
  }
11169
11442
  async function findOneAndDeleteDocument(collection, filter, options) {
11443
+ assertManagedTransactionWriteAllowed(options);
11170
11444
  if (options !== void 0) {
11171
11445
  return collection.findOneAndDelete(filter, options);
11172
11446
  }
11173
11447
  return collection.findOneAndDelete(filter);
11174
11448
  }
11175
11449
  async function upsertOneDocument(collection, filter, update, options = {}) {
11450
+ assertManagedTransactionWriteAllowed(options);
11176
11451
  return collection.updateOne(filter, update, {
11177
11452
  ...options,
11178
11453
  upsert: true
11179
11454
  });
11180
11455
  }
11181
11456
  async function deleteOneDocument(collection, ...args) {
11457
+ assertManagedTransactionWriteAllowed(args[1]);
11182
11458
  return collection.deleteOne(...args);
11183
11459
  }
11184
11460
  async function deleteManyDocuments(collection, ...args) {
11461
+ assertManagedTransactionWriteAllowed(args[1]);
11185
11462
  return collection.deleteMany(...args);
11186
11463
  }
11187
11464
  async function incrementOneDocument(collection, filter, field, incrementOrOptions, maybeOptions) {
@@ -11208,6 +11485,7 @@ async function incrementOneDocument(collection, filter, field, incrementOrOption
11208
11485
  includeResultMetadata: true
11209
11486
  };
11210
11487
  if (normalizedProjection) findOptions.projection = normalizedProjection;
11488
+ assertManagedTransactionWriteAllowed(findOptions);
11211
11489
  const rawResult = await collection.findOneAndUpdate(
11212
11490
  filter,
11213
11491
  updateDocument,
@@ -11340,14 +11618,25 @@ async function runWithConcurrency(items, concurrency, worker) {
11340
11618
  return;
11341
11619
  }
11342
11620
  let nextIndex = 0;
11621
+ let stopAcquiring = false;
11622
+ let firstFatal;
11343
11623
  const workerCount = Math.min(concurrency, items.length);
11344
11624
  await Promise.all(Array.from({ length: workerCount }, async () => {
11345
- while (nextIndex < items.length) {
11625
+ while (!stopAcquiring && nextIndex < items.length) {
11346
11626
  const item = items[nextIndex];
11347
11627
  nextIndex += 1;
11348
- 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
+ }
11349
11637
  }
11350
11638
  }));
11639
+ if (stopAcquiring) throw firstFatal;
11351
11640
  }
11352
11641
  async function insertBatchDocuments(collection, documents, options = {}) {
11353
11642
  if (!Array.isArray(documents)) {
@@ -11394,12 +11683,14 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11394
11683
  insertedIds: {},
11395
11684
  retries: []
11396
11685
  };
11686
+ assertManagedTransactionWriteAllowed(driverOptions);
11397
11687
  await runWithConcurrency(batchPlans, effectiveConcurrency, async ({ batchIndex, batch, batchOffset: batchOffset2 }) => {
11398
11688
  let pendingBatch = batch;
11399
11689
  let pendingIndexes = batch.map((_, index) => index);
11400
11690
  let attempts = 0;
11401
11691
  while (true) {
11402
11692
  try {
11693
+ assertManagedTransactionWriteAllowed(driverOptions);
11403
11694
  const batchResult = await collection.insertMany(pendingBatch, {
11404
11695
  ...driverOptions,
11405
11696
  ordered
@@ -11419,6 +11710,7 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11419
11710
  });
11420
11711
  break;
11421
11712
  } catch (cause) {
11713
+ assertManagedTransactionWriteAllowed(driverOptions);
11422
11714
  const errorRecord = {
11423
11715
  batchIndex,
11424
11716
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11490,6 +11782,10 @@ async function insertBatchDocuments(collection, documents, options = {}) {
11490
11782
  });
11491
11783
  return result;
11492
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
+ }
11493
11789
  async function updateBatchDocuments(collection, filter, update, options = {}) {
11494
11790
  if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
11495
11791
  throw createError(ErrorCodes.INVALID_ARGUMENT, "filter must be a non-array object");
@@ -11533,6 +11829,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11533
11829
  "updateBatch retry requires an idempotent update; non-idempotent operators such as $inc/$push cannot be safely replayed"
11534
11830
  );
11535
11831
  }
11832
+ assertManagedTransactionWriteAllowed(driverOptions);
11536
11833
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11537
11834
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11538
11835
  const result = {
@@ -11549,8 +11846,9 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11549
11846
  let attempts = 0;
11550
11847
  while (true) {
11551
11848
  try {
11849
+ assertManagedTransactionWriteAllowed(driverOptions);
11552
11850
  const batchResult = await collection.updateMany(
11553
- { _id: { $in: batch } },
11851
+ batchWriteFilter(filter, batch),
11554
11852
  update,
11555
11853
  driverOptions
11556
11854
  );
@@ -11568,6 +11866,7 @@ async function updateBatchDocuments(collection, filter, update, options = {}) {
11568
11866
  });
11569
11867
  break;
11570
11868
  } catch (cause) {
11869
+ assertManagedTransactionWriteAllowed(driverOptions);
11571
11870
  const errorRecord = {
11572
11871
  batchIndex,
11573
11872
  message: cause instanceof Error ? cause.message : String(cause),
@@ -11630,6 +11929,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11630
11929
  if (!Number.isInteger(retryAttempts) || retryAttempts < 0) {
11631
11930
  throw createError(ErrorCodes.INVALID_ARGUMENT, "retryAttempts must be a non-negative integer");
11632
11931
  }
11932
+ assertManagedTransactionWriteAllowed(driverOptions);
11633
11933
  const estimatedTotal = await estimateBatchTotal(collection, filter, driverOptions, estimateProgress);
11634
11934
  const totalBatches = estimatedTotal === null ? null : Math.ceil(estimatedTotal / batchSize);
11635
11935
  const result = {
@@ -11644,8 +11944,9 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11644
11944
  let attempts = 0;
11645
11945
  while (true) {
11646
11946
  try {
11947
+ assertManagedTransactionWriteAllowed(driverOptions);
11647
11948
  const batchResult = await collection.deleteMany(
11648
- { _id: { $in: batch } },
11949
+ batchWriteFilter(filter, batch),
11649
11950
  driverOptions
11650
11951
  );
11651
11952
  result.deletedCount += batchResult.deletedCount;
@@ -11660,6 +11961,7 @@ async function deleteBatchDocuments(collection, filter, options = {}) {
11660
11961
  });
11661
11962
  break;
11662
11963
  } catch (cause) {
11964
+ assertManagedTransactionWriteAllowed(driverOptions);
11663
11965
  const errorRecord = {
11664
11966
  batchIndex,
11665
11967
  message: cause instanceof Error ? cause.message : String(cause),
@@ -12188,9 +12490,11 @@ var MongoCollectionAccessor = class {
12188
12490
  const normalizedQuery = this._cvFilter(query);
12189
12491
  const maxTimeMS = this.management.defaults?.maxTimeMS;
12190
12492
  const merged = { ...maxTimeMS !== void 0 ? { maxTimeMS } : {}, ...options ?? {} };
12191
- 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;
12192
12494
  const { cache: _cache, ...keyOptions } = merged;
12193
12495
  void _cache;
12496
+ const callerSignal = keyOptions.signal;
12497
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12194
12498
  const executeCount = (signal) => countDocuments(
12195
12499
  this.collectionRef,
12196
12500
  normalizedQuery ?? {},
@@ -12201,15 +12505,16 @@ var MongoCollectionAccessor = class {
12201
12505
  const cacheKey = buildCountCacheKey(this.collectionRef, this.management.defaults, normalizedQuery, keyOptions);
12202
12506
  const cached = await Promise.resolve(this.management.queryCache.get(cacheKey));
12203
12507
  if (cached !== void 0) {
12508
+ if (callerSignal?.aborted) throw createError(ErrorCodes.INVALID_OPERATION, "Count operation aborted");
12204
12509
  return Promise.resolve(wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, cached));
12205
12510
  }
12206
- const runner2 = countQueue ? countQueue.execute(executeCount) : executeCount();
12511
+ const runner2 = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12207
12512
  return runner2.then(async (result) => {
12208
12513
  await Promise.resolve(this.management.queryCache?.set(cacheKey, result, cacheTTL));
12209
12514
  return wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result);
12210
12515
  });
12211
12516
  }
12212
- const runner = countQueue ? countQueue.execute(executeCount) : executeCount();
12517
+ const runner = countQueue ? countQueue.execute(executeCount, { signal: callerSignal }) : executeCount();
12213
12518
  return runner.then((result) => wrapQueryResultWithMeta(this.collectionRef, this.management.defaults, "count", merged, startTs, result));
12214
12519
  }
12215
12520
  /** Runs an aggregation pipeline and returns a chainable aggregate cursor. */
@@ -12766,10 +13071,10 @@ function assertCompatPoolExists(poolManager, poolName) {
12766
13071
  function createPoolScope(runtime, poolName) {
12767
13072
  return {
12768
13073
  collection: (name) => runtime.scopedCollection(name, { pool: poolName }),
12769
- model: (name) => runtime.scopedModel(name, { pool: poolName }),
13074
+ model: (model) => runtime.scopedModel(model, { pool: poolName }),
12770
13075
  use: (dbName) => ({
12771
13076
  collection: (name) => runtime.scopedCollection(name, { pool: poolName, database: dbName }),
12772
- model: (name) => runtime.scopedModel(name, { pool: poolName, database: dbName })
13077
+ model: (model) => runtime.scopedModel(model, { pool: poolName, database: dbName })
12773
13078
  })
12774
13079
  };
12775
13080
  }
@@ -13498,20 +13803,21 @@ var DistributedCacheLockManager = class {
13498
13803
  }
13499
13804
  }
13500
13805
  async withLock(key, callback, options = {}) {
13806
+ let lock;
13501
13807
  try {
13502
- const lock = await this.acquireLock(key, options);
13503
- try {
13504
- return await callback();
13505
- } finally {
13506
- await lock.release().catch(() => {
13507
- });
13508
- }
13808
+ lock = await this.acquireLock(key, options);
13509
13809
  } catch (error) {
13510
13810
  if (this._isRedisConnectionError(error) && options.fallbackToNoLock) {
13511
13811
  return callback();
13512
13812
  }
13513
13813
  throw error;
13514
13814
  }
13815
+ try {
13816
+ return await callback();
13817
+ } finally {
13818
+ await lock.release().catch(() => {
13819
+ });
13820
+ }
13515
13821
  }
13516
13822
  async acquireLock(key, options = {}) {
13517
13823
  const ttl = options.ttl ?? 1e4;
@@ -14614,8 +14920,6 @@ var SlowQueryLogManager = class {
14614
14920
  };
14615
14921
 
14616
14922
  // src/capabilities/sync/index.ts
14617
- import { copyFile, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
14618
- import path2 from "node:path";
14619
14923
  import { MongoClient as MongoDriverClient3 } from "mongodb";
14620
14924
 
14621
14925
  // src/capabilities/sync/idempotency.ts
@@ -14718,25 +15022,19 @@ function validateSyncIdempotencyConfig(config) {
14718
15022
  }
14719
15023
  }
14720
15024
 
14721
- // 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";
14722
15028
  function normalizeError(error) {
14723
15029
  return error instanceof Error ? error : new Error(String(error));
14724
15030
  }
14725
15031
  function delay2(ms) {
14726
- if (ms <= 0) {
14727
- return Promise.resolve();
14728
- }
15032
+ if (ms <= 0) return Promise.resolve();
14729
15033
  return new Promise((resolve) => {
14730
15034
  const timer = setTimeout(resolve, ms);
14731
15035
  timer.unref?.();
14732
15036
  });
14733
15037
  }
14734
- function normalizeCollectionFilter(collections) {
14735
- if (!collections?.length || collections.includes("*")) {
14736
- return null;
14737
- }
14738
- return new Set(collections);
14739
- }
14740
15038
  async function syncDirectory(directory) {
14741
15039
  try {
14742
15040
  const handle = await open(directory, "r");
@@ -14748,35 +15046,6 @@ async function syncDirectory(directory) {
14748
15046
  } catch {
14749
15047
  }
14750
15048
  }
14751
- function validateTargetConfig(target, index) {
14752
- if (!target || typeof target !== "object") {
14753
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] must be an object.`);
14754
- }
14755
- if (typeof target.name !== "string" || target.name.trim() === "") {
14756
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].name must be a non-empty string.`);
14757
- }
14758
- if (!target.apply && !target.uri && !target.pool) {
14759
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}] requires one of apply / uri / pool.`);
14760
- }
14761
- if (target.uri !== void 0 && (typeof target.uri !== "string" || target.uri.trim() === "")) {
14762
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].uri must be a non-empty string when provided.`);
14763
- }
14764
- if (target.pool !== void 0 && (typeof target.pool !== "string" || target.pool.trim() === "")) {
14765
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].pool must be a non-empty string when provided.`);
14766
- }
14767
- if (target.databaseName !== void 0 && (typeof target.databaseName !== "string" || target.databaseName.trim() === "")) {
14768
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].databaseName must be a non-empty string when provided.`);
14769
- }
14770
- if (target.collections !== void 0 && (!Array.isArray(target.collections) || target.collections.length === 0)) {
14771
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must be a non-empty array when provided.`);
14772
- }
14773
- if (target.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14774
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].collections must contain non-empty strings.`);
14775
- }
14776
- if (target.apply !== void 0 && typeof target.apply !== "function") {
14777
- throw createError(ErrorCodes.INVALID_CONFIG, `[Sync] targets[${index}].apply must be a function when provided.`);
14778
- }
14779
- }
14780
15049
  function validateResumeTokenConfig(config) {
14781
15050
  if (!config || typeof config !== "object") {
14782
15051
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken must be an object.");
@@ -14807,39 +15076,6 @@ function validateResumeTokenConfig(config) {
14807
15076
  throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] resumeToken.saveRetryDelayMs must be a non-negative integer.");
14808
15077
  }
14809
15078
  }
14810
- function validateSyncConfig(config) {
14811
- if (!config || typeof config !== "object") {
14812
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] config must be an object.");
14813
- }
14814
- if (typeof config.enabled !== "boolean") {
14815
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] enabled must be a boolean.");
14816
- }
14817
- if (!config.enabled) {
14818
- return;
14819
- }
14820
- if (!Array.isArray(config.targets) || config.targets.length === 0) {
14821
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] targets must be a non-empty array.");
14822
- }
14823
- if (config.collections !== void 0 && (!Array.isArray(config.collections) || config.collections.length === 0)) {
14824
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must be a non-empty array when provided.");
14825
- }
14826
- if (config.collections?.some((item) => typeof item !== "string" || item.trim() === "")) {
14827
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] collections must contain non-empty strings.");
14828
- }
14829
- if (config.filter !== void 0 && typeof config.filter !== "function") {
14830
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] filter must be a function.");
14831
- }
14832
- if (config.transform !== void 0 && typeof config.transform !== "function") {
14833
- throw createError(ErrorCodes.INVALID_CONFIG, "[Sync] transform must be a function.");
14834
- }
14835
- validateSyncIdempotencyConfig(config.idempotency);
14836
- config.targets.forEach((target, index) => {
14837
- validateTargetConfig(target, index);
14838
- });
14839
- if (config.resumeToken) {
14840
- validateResumeTokenConfig(config.resumeToken);
14841
- }
14842
- }
14843
15079
  var ResumeTokenStore = class {
14844
15080
  constructor(options = {}) {
14845
15081
  this.storage = options.storage ?? "file";
@@ -14863,16 +15099,9 @@ var ResumeTokenStore = class {
14863
15099
  return JSON.parse(payload);
14864
15100
  } catch (error) {
14865
15101
  const code = error?.code;
14866
- if (code !== "ENOENT") {
14867
- this.logger?.warn?.("[Sync] failed to load resume token", error);
14868
- }
15102
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to load resume token", error);
14869
15103
  if (code !== "ENOENT" && this.strictLoad) {
14870
- throw createError(
14871
- ErrorCodes.DATABASE_ERROR,
14872
- "[Sync] failed to load resume token",
14873
- void 0,
14874
- normalizeError(error)
14875
- );
15104
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to load resume token", void 0, normalizeError(error));
14876
15105
  }
14877
15106
  return null;
14878
15107
  }
@@ -14898,12 +15127,7 @@ var ResumeTokenStore = class {
14898
15127
  }
14899
15128
  this.logger?.error?.("[Sync] failed to save resume token", lastError);
14900
15129
  if (this.strictSave) {
14901
- throw createError(
14902
- ErrorCodes.DATABASE_ERROR,
14903
- "[Sync] failed to save resume token",
14904
- void 0,
14905
- lastError ?? void 0
14906
- );
15130
+ throw createError(ErrorCodes.DATABASE_ERROR, "[Sync] failed to save resume token", void 0, lastError ?? void 0);
14907
15131
  }
14908
15132
  }
14909
15133
  async writePayload(payload) {
@@ -14936,9 +15160,7 @@ var ResumeTokenStore = class {
14936
15160
  await copyFile(this.path, `${this.path}.bak`);
14937
15161
  } catch (error) {
14938
15162
  const code = error?.code;
14939
- if (code !== "ENOENT") {
14940
- throw error;
14941
- }
15163
+ if (code !== "ENOENT") throw error;
14942
15164
  }
14943
15165
  }
14944
15166
  async clear() {
@@ -14950,12 +15172,80 @@ var ResumeTokenStore = class {
14950
15172
  await unlink(this.path);
14951
15173
  } catch (error) {
14952
15174
  const code = error?.code;
14953
- if (code !== "ENOENT") {
14954
- this.logger?.warn?.("[Sync] failed to clear resume token", error);
14955
- }
15175
+ if (code !== "ENOENT") this.logger?.warn?.("[Sync] failed to clear resume token", error);
14956
15176
  }
14957
15177
  }
14958
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
+ }
14959
15249
  var ChangeStreamSyncManager = class {
14960
15250
  constructor(options) {
14961
15251
  this.targets = [];
@@ -14964,7 +15254,10 @@ var ChangeStreamSyncManager = class {
14964
15254
  this.closingStreamPromise = null;
14965
15255
  this.changeQueue = Promise.resolve();
14966
15256
  this.running = false;
15257
+ this.stopping = false;
15258
+ this.stopPromise = null;
14967
15259
  this.fatalSyncError = null;
15260
+ this.generation = 0;
14968
15261
  this.stats = {
14969
15262
  eventCount: 0,
14970
15263
  syncedCount: 0,
@@ -14997,40 +15290,62 @@ var ChangeStreamSyncManager = class {
14997
15290
  * @since v1.0.9
14998
15291
  */
14999
15292
  async start() {
15293
+ if (this.stopPromise) await this.stopPromise;
15000
15294
  if (this.running || !this.config.enabled) return;
15001
15295
  if (this.startPromise) return this.startPromise;
15296
+ const generation = ++this.generation;
15002
15297
  const startup = (async () => {
15003
15298
  await this.closingStreamPromise;
15299
+ await this.changeQueue.catch(() => void 0);
15300
+ this.assertCurrentGeneration(generation, true);
15004
15301
  if (this.running) return;
15005
15302
  this.fatalSyncError = null;
15006
- await this.validateEnvironment();
15007
- await this.initializeTargets();
15008
- const resumeAfter = await this.tokenStore.load();
15009
- const options = { fullDocument: "updateLookup" };
15010
- if (resumeAfter) options.resumeAfter = resumeAfter;
15011
- const stream = this.db.watch(this.buildPipeline(), options);
15012
- stream.on("change", (event) => this.enqueueChange(event));
15013
- stream.on("error", (error) => {
15014
- const normalized = normalizeError(error);
15015
- this.stats.errorCount += 1;
15016
- this.stats.lastError = normalized;
15017
- this.logger?.error?.("[Sync] change stream error", normalized);
15018
- });
15019
- stream.on("close", () => {
15020
- if (!this.running || this.changeStream !== stream) {
15021
- this.logger?.debug?.("[Sync] change stream closed");
15022
- return;
15023
- }
15024
- const error = createError(ErrorCodes.CONNECTION_CLOSED, "[Sync] change stream closed unexpectedly");
15025
- this.running = false;
15026
- this.changeStream = null;
15027
- this.stats.errorCount += 1;
15028
- this.stats.lastError = error;
15029
- this.logger?.warn?.("[Sync] change stream closed unexpectedly", error);
15030
- });
15031
- this.changeStream = stream;
15032
- this.running = true;
15033
- 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
+ }
15034
15349
  })();
15035
15350
  this.startPromise = startup;
15036
15351
  try {
@@ -15044,16 +15359,37 @@ var ChangeStreamSyncManager = class {
15044
15359
  * @since v1.0.9
15045
15360
  */
15046
15361
  async stop() {
15047
- this.running = false;
15048
- await this.startPromise;
15049
- const stream = this.changeStream;
15050
- this.changeStream = null;
15051
- if (stream) await this.closeChangeStream(stream);
15052
- else await this.closingStreamPromise;
15053
- 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() {
15054
15391
  while (this.targets.length > 0) {
15055
- const target = this.targets.pop();
15056
- await target?.close();
15392
+ await this.targets.pop()?.close();
15057
15393
  }
15058
15394
  }
15059
15395
  /**
@@ -15118,11 +15454,13 @@ var ChangeStreamSyncManager = class {
15118
15454
  return pipeline;
15119
15455
  }
15120
15456
  async initializeTargets() {
15121
- if (this.targets.length > 0) {
15122
- return;
15123
- }
15124
- for (const target of this.config.targets) {
15125
- 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;
15126
15464
  }
15127
15465
  }
15128
15466
  async resolveTarget(target) {
@@ -15159,14 +15497,15 @@ var ChangeStreamSyncManager = class {
15159
15497
  const client = await this.clientFactory(target.uri, target.options);
15160
15498
  return createMongoTarget(target.name, collections, client, target.databaseName ?? this.db.databaseName, true);
15161
15499
  }
15162
- async handleChange(event) {
15163
- if (this.fatalSyncError) {
15500
+ async handleChange(event, generation = this.generation) {
15501
+ if (generation !== this.generation || !this.running || this.fatalSyncError) {
15164
15502
  this.logger?.warn?.("[Sync] skipping queued change after fatal sync error", this.fatalSyncError);
15165
15503
  return;
15166
15504
  }
15167
15505
  this.stats.eventCount += 1;
15168
15506
  this.stats.lastEventTime = /* @__PURE__ */ new Date();
15169
15507
  if (this.config.filter && !this.config.filter(event)) {
15508
+ await this.saveProcessedToken(event, generation);
15170
15509
  return;
15171
15510
  }
15172
15511
  let document = event.fullDocument;
@@ -15178,6 +15517,7 @@ var ChangeStreamSyncManager = class {
15178
15517
  let duplicateTargets = 0;
15179
15518
  const targetErrors = [];
15180
15519
  for (const target of this.targets) {
15520
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15181
15521
  if (target.collections && !target.collections.has(event.ns.coll)) {
15182
15522
  continue;
15183
15523
  }
@@ -15194,11 +15534,13 @@ var ChangeStreamSyncManager = class {
15194
15534
  if (idempotencyKey && this.idempotency.markMode === "start") {
15195
15535
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "start");
15196
15536
  }
15537
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15197
15538
  await target.apply(event, document, {
15198
15539
  targetName: target.name,
15199
15540
  ...idempotencyKey ? { idempotencyKey } : {}
15200
15541
  });
15201
15542
  if (idempotencyKey && this.idempotency.markMode === "success") {
15543
+ if (generation !== this.generation || this.fatalSyncError || !this.running) return;
15202
15544
  await this.markTargetEventApplied(idempotencyKey, event, target.name, "success");
15203
15545
  }
15204
15546
  target.stats.syncCount += 1;
@@ -15219,26 +15561,30 @@ var ChangeStreamSyncManager = class {
15219
15561
  }
15220
15562
  }
15221
15563
  if (targetErrors.length > 0) {
15222
- this.stopAfterFatalSyncError(targetErrors[0]);
15564
+ this.stopAfterFatalSyncError(targetErrors[0], generation);
15223
15565
  return;
15224
15566
  }
15225
- if (eligibleTargets > 0 && succeeded === eligibleTargets) {
15226
- try {
15227
- await this.tokenStore.save(event._id);
15228
- } catch (error) {
15229
- const normalized = normalizeError(error);
15230
- this.stats.tokenSaveErrorCount += 1;
15231
- this.stats.lastTokenSaveError = normalized;
15232
- this.stats.lastError = normalized;
15233
- this.stopAfterFatalSyncError(normalized);
15234
- throw normalized;
15235
- }
15236
- this.stats.syncedCount += 1;
15567
+ if (succeeded === eligibleTargets) {
15568
+ await this.saveProcessedToken(event, generation);
15237
15569
  if (duplicateTargets > 0 && duplicateTargets === eligibleTargets) {
15238
15570
  this.stats.duplicateEventCount += 1;
15239
15571
  }
15240
15572
  }
15241
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
+ }
15242
15588
  buildTargetIdempotencyKey(targetName, event) {
15243
15589
  if (!this.idempotency.enabled) {
15244
15590
  return null;
@@ -15261,7 +15607,8 @@ var ChangeStreamSyncManager = class {
15261
15607
  ts: Date.now()
15262
15608
  }, this.idempotency.ttl));
15263
15609
  }
15264
- stopAfterFatalSyncError(error) {
15610
+ stopAfterFatalSyncError(error, generation) {
15611
+ if (generation !== this.generation) return;
15265
15612
  this.fatalSyncError = error;
15266
15613
  this.running = false;
15267
15614
  const stream = this.changeStream;
@@ -15279,12 +15626,14 @@ var ChangeStreamSyncManager = class {
15279
15626
  if (this.closingStreamPromise === closing) this.closingStreamPromise = null;
15280
15627
  });
15281
15628
  }
15282
- enqueueChange(event) {
15283
- 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) => {
15284
15632
  const normalized = normalizeError(error);
15285
15633
  this.stats.errorCount += 1;
15286
15634
  this.stats.lastError = normalized;
15287
15635
  this.logger?.error?.("[Sync] change handling failed", normalized);
15636
+ this.stopAfterFatalSyncError(normalized, generation);
15288
15637
  });
15289
15638
  }
15290
15639
  };
@@ -15713,7 +16062,9 @@ var SSHTunnelSSH2 = class {
15713
16062
  passphrase,
15714
16063
  port = 22,
15715
16064
  readyTimeout = 2e4,
15716
- keepaliveInterval = 3e4
16065
+ keepaliveInterval = 3e4,
16066
+ hostHash,
16067
+ hostVerifier
15717
16068
  } = this._sshConfig;
15718
16069
  if (!host || !username) {
15719
16070
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
@@ -15722,6 +16073,8 @@ var SSHTunnelSSH2 = class {
15722
16073
  throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
15723
16074
  }
15724
16075
  const config = { host, port, username, readyTimeout, keepaliveInterval };
16076
+ if (hostHash !== void 0) config.hostHash = hostHash;
16077
+ if (hostVerifier !== void 0) config.hostVerifier = hostVerifier;
15725
16078
  if (password) {
15726
16079
  config.password = password;
15727
16080
  } else if (privateKey) {
@@ -15740,6 +16093,25 @@ var SSHTunnelSSH2 = class {
15740
16093
  return new Promise((resolve, reject) => {
15741
16094
  const ssh = new SshClient();
15742
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
+ };
15743
16115
  const server = net.createServer((socket) => {
15744
16116
  this._activeSockets.add(socket);
15745
16117
  socket.on("close", () => this._activeSockets.delete(socket));
@@ -15765,12 +16137,7 @@ var SSHTunnelSSH2 = class {
15765
16137
  }
15766
16138
  );
15767
16139
  });
15768
- server.on("error", (err) => {
15769
- if (!settled) {
15770
- settled = true;
15771
- reject(err);
15772
- }
15773
- });
16140
+ server.on("error", fail);
15774
16141
  server.listen(this._sshConfig.localPort ?? 0, "127.0.0.1", () => {
15775
16142
  this.server = server;
15776
16143
  const addr = server.address();
@@ -15785,25 +16152,25 @@ var SSHTunnelSSH2 = class {
15785
16152
  });
15786
16153
  ssh.on("error", (err) => {
15787
16154
  if (!settled) {
15788
- settled = true;
15789
- server.close();
15790
- reject(err);
16155
+ fail(err);
15791
16156
  return;
15792
16157
  }
15793
16158
  this.markDisconnected();
15794
16159
  });
15795
16160
  const handleDisconnect = () => {
15796
16161
  if (!settled) {
15797
- settled = true;
15798
- server.close();
15799
- 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"));
15800
16163
  return;
15801
16164
  }
15802
16165
  this.markDisconnected();
15803
16166
  };
15804
16167
  ssh.on("close", handleDisconnect);
15805
16168
  ssh.on("end", handleDisconnect);
15806
- ssh.connect(authConfig);
16169
+ try {
16170
+ ssh.connect(authConfig);
16171
+ } catch (err) {
16172
+ fail(err);
16173
+ }
15807
16174
  });
15808
16175
  });
15809
16176
  }
@@ -15834,6 +16201,7 @@ var SSHTunnelSSH2 = class {
15834
16201
  ssh?.end();
15835
16202
  }
15836
16203
  markDisconnected() {
16204
+ const ssh = this.sshClient;
15837
16205
  this.isConnected = false;
15838
16206
  this.localPort = null;
15839
16207
  this.sshClient = null;
@@ -15844,6 +16212,10 @@ var SSHTunnelSSH2 = class {
15844
16212
  }
15845
16213
  this._activeSockets.clear();
15846
16214
  void closeServer(server);
16215
+ try {
16216
+ ssh?.end();
16217
+ } catch {
16218
+ }
15847
16219
  }
15848
16220
  };
15849
16221
  function parseHostFromUri(uri) {
@@ -15930,12 +16302,17 @@ async function prepareSshTunnelConnectConfig(connectConfig, databaseName, logger
15930
16302
  const remotePort = Number(cfg["dstPort"] ?? rawCfg["remotePort"] ?? rawCfg["mongoPort"] ?? parsePortFromUri(uri));
15931
16303
  const tunnel = new SSHTunnelSSH2(sshCfg, remoteHost, remotePort, { name: databaseName });
15932
16304
  logger.info?.(`[SSH] Establishing tunnel to ${remoteHost}:${remotePort} via ${String(cfg["host"])}`);
15933
- await tunnel.connect();
15934
- logger.info?.(`[SSH] Tunnel ready \u2014 local address: ${tunnel.getLocalAddress()}`);
15935
- return {
15936
- tunnel,
15937
- connectConfig: connectConfig?.uri ? { ...connectConfig, uri: tunnel.getTunnelUri("mongodb", connectConfig.uri) } : connectConfig
15938
- };
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
+ }
15939
16316
  }
15940
16317
 
15941
16318
  // src/entry/runtime-schema-dsl.ts
@@ -16230,7 +16607,7 @@ function makePageResult(rows, ctx) {
16230
16607
  import { createHash as createHash4 } from "node:crypto";
16231
16608
  import os2 from "node:os";
16232
16609
  import path3 from "node:path";
16233
- import { BSON } from "mongodb";
16610
+ import { BSON as BSON2 } from "mongodb";
16234
16611
 
16235
16612
  // src/capabilities/data-tasks/support.ts
16236
16613
  var INDEX_OPTION_KEYS = [
@@ -16338,7 +16715,7 @@ function canonicalize(value) {
16338
16715
  return value;
16339
16716
  }
16340
16717
  function canonicalStringify(value) {
16341
- return JSON.stringify(canonicalize(BSON.EJSON.serialize(value, { relaxed: false })));
16718
+ return JSON.stringify(canonicalize(BSON2.EJSON.serialize(value, { relaxed: false })));
16342
16719
  }
16343
16720
  function hashDataTaskValue(value) {
16344
16721
  return createHash4("sha256").update(canonicalStringify(value)).digest("hex");
@@ -16827,9 +17204,8 @@ var MonSQLizeRuntime = class {
16827
17204
  driverError = error instanceof Error ? error.message : String(error);
16828
17205
  }
16829
17206
  }
16830
- const cacheStats = this._cache.getStats?.();
16831
- const poolHealth = this._poolManager?.getHealthStatus();
16832
- 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;
16833
17209
  const connected = this._connected && driverConnected;
16834
17210
  return {
16835
17211
  status: connected ? "up" : "down",
@@ -16837,7 +17213,7 @@ var MonSQLizeRuntime = class {
16837
17213
  driver: { connected: driverConnected, ...driverError ? { error: driverError } : {} },
16838
17214
  defaults: this.getDefaults(),
16839
17215
  cache: {
16840
- enabled: true,
17216
+ enabled: cacheEnabled,
16841
17217
  stats: cacheStats,
16842
17218
  pools: poolHealth,
16843
17219
  distributed: distributedStats
@@ -16849,18 +17225,18 @@ var MonSQLizeRuntime = class {
16849
17225
  ...driverError ? { error: driverError } : {}
16850
17226
  },
16851
17227
  cache: {
16852
- status: "up",
16853
- enabled: true,
17228
+ status: cacheProbeStatus,
17229
+ enabled: cacheEnabled,
16854
17230
  hasStats: cacheStats !== void 0
16855
17231
  },
16856
17232
  distributedCacheInvalidator: {
16857
- status: distributedStats ? "up" : "unknown",
16858
- enabled: distributedStats !== null,
17233
+ status: distributedStatus,
17234
+ enabled: distributedEnabled,
16859
17235
  stats: distributedStats
16860
17236
  },
16861
17237
  pools: {
16862
- status: poolHealth ? "up" : "unknown",
16863
- enabled: poolHealth !== void 0,
17238
+ status: poolsStatus,
17239
+ enabled: this._poolManager !== null,
16864
17240
  health: poolHealth
16865
17241
  }
16866
17242
  },
@@ -16928,7 +17304,7 @@ var MonSQLizeRuntime = class {
16928
17304
  requireCompatDbInstance(this);
16929
17305
  return {
16930
17306
  collection: (collectionName) => this.scopedCollection(collectionName, { database: name }),
16931
- model: (modelName) => this.scopedModel(modelName, { database: name })
17307
+ model: (model) => this.scopedModel(model, { database: name })
16932
17308
  };
16933
17309
  }
16934
17310
  pool(poolName) {
@@ -16960,7 +17336,8 @@ var MonSQLizeRuntime = class {
16960
17336
  });
16961
17337
  }
16962
17338
  // Model accessors ---------------------------------------------------------
16963
- scopedModel(name, options = {}) {
17339
+ scopedModel(model, options = {}) {
17340
+ const name = typeof model === "string" ? model : model.collectionName;
16964
17341
  const dbInstance = requireCompatDbInstance(this);
16965
17342
  if (this._client) {
16966
17343
  return this.createModelInstance(name, options);
@@ -16984,7 +17361,8 @@ var MonSQLizeRuntime = class {
16984
17361
  schemaEngine: this._schemaDslEngine
16985
17362
  });
16986
17363
  }
16987
- model(name) {
17364
+ model(model) {
17365
+ const name = typeof model === "string" ? model : model.collectionName;
16988
17366
  if (this._client) {
16989
17367
  this.ensureConnected();
16990
17368
  return this.createModelInstance(name, {
@@ -17220,7 +17598,7 @@ var MonSQLizeRuntime = class {
17220
17598
  };
17221
17599
 
17222
17600
  // src/capabilities/data-tasks/job-planner.ts
17223
- import { BSON as BSON2 } from "mongodb";
17601
+ import { BSON as BSON3 } from "mongodb";
17224
17602
 
17225
17603
  // src/capabilities/data-tasks/document-utils.ts
17226
17604
  function isPlainRecord(value) {
@@ -17702,7 +18080,7 @@ async function planCollection(sourceHost, targetHost, config, sampleSize) {
17702
18080
  before: change.before,
17703
18081
  after: change.after
17704
18082
  }));
17705
- 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({
17706
18084
  ...change,
17707
18085
  beforeHash: hashDataTaskValue(change.before),
17708
18086
  plannedAfterHash: hashDataTaskValue(change.after)
@@ -17787,7 +18165,7 @@ import { mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename
17787
18165
  import path4 from "node:path";
17788
18166
  import { gzip, gunzip } from "node:zlib";
17789
18167
  import { promisify } from "node:util";
17790
- import { BSON as BSON3 } from "mongodb";
18168
+ import { BSON as BSON4, Int32, Long } from "mongodb";
17791
18169
  var gzipAsync = promisify(gzip);
17792
18170
  var gunzipAsync = promisify(gunzip);
17793
18171
  var BACKUP_TARGET = /* @__PURE__ */ Symbol("dataTaskBackupTarget");
@@ -17804,10 +18182,31 @@ function checksum(buffer) {
17804
18182
  return createHash5("sha256").update(buffer).digest("hex");
17805
18183
  }
17806
18184
  function ejsonLine(value) {
17807
- return BSON3.EJSON.stringify(value, { relaxed: false });
18185
+ return BSON4.EJSON.stringify(value, { relaxed: false });
17808
18186
  }
17809
18187
  function ejsonManifest(value) {
17810
- 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;
17811
18210
  }
17812
18211
  function sanitizeName(value) {
17813
18212
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "data-task";
@@ -17985,10 +18384,9 @@ async function createRestoreSafetyBackup(source, entries, beforeIndexes, targetH
17985
18384
  }
17986
18385
  async function readDataTaskBackup(ref) {
17987
18386
  try {
17988
- 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 }));
17989
18388
  if (manifest.kind !== "monsqlize-data-task-backup" || manifest.version !== 1 || manifest.runId !== ref.runId) throw new Error("invalid backup manifest");
17990
18389
  if (manifest.compression !== "gzip" && manifest.compression !== "none") throw new Error("invalid backup compression");
17991
- if (!Number.isSafeInteger(manifest.entryCount) || manifest.entryCount < 0) throw new Error("invalid backup entry count");
17992
18390
  if (typeof manifest.checksum !== "string" || !/^[a-f0-9]{64}$/i.test(manifest.checksum)) throw new Error("invalid backup checksum");
17993
18391
  if (typeof manifest.dataFile !== "string" || manifest.dataFile.length === 0 || path4.isAbsolute(manifest.dataFile) || path4.basename(manifest.dataFile) !== manifest.dataFile) {
17994
18392
  throw new Error("backup data file must stay beside its manifest");
@@ -18001,7 +18399,7 @@ async function readDataTaskBackup(ref) {
18001
18399
  if (digest !== manifest.checksum || digest !== ref.checksum) throw new Error("backup checksum mismatch");
18002
18400
  const plain = manifest.compression === "gzip" ? await gunzipAsync(contents) : contents;
18003
18401
  const lines = plain.toString("utf8").split(/\r?\n/).filter(Boolean);
18004
- const entries = lines.map((line) => BSON3.EJSON.parse(line, { relaxed: false }));
18402
+ const entries = lines.map((line) => BSON4.EJSON.parse(line, { relaxed: false }));
18005
18403
  if (entries.length !== manifest.entryCount) throw new Error("backup entry count mismatch");
18006
18404
  return { ref: attachTarget({ ...ref }, backupTargetHint(ref)), manifest, entries };
18007
18405
  } catch (error) {
@@ -18319,6 +18717,7 @@ async function acquireDataTaskLease(host, options) {
18319
18717
  }
18320
18718
 
18321
18719
  // src/capabilities/data-tasks/job-restore.ts
18720
+ import { Int32 as Int322, Long as Long2 } from "mongodb";
18322
18721
  async function findCurrent(collection, identity) {
18323
18722
  let chain = collection.find(identity);
18324
18723
  if (typeof chain.limit === "function") chain = chain.limit(2);
@@ -18339,13 +18738,20 @@ function restoreApproval(hashes) {
18339
18738
  };
18340
18739
  return { ...payload, token: hashDataTaskValue(payload) };
18341
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
+ }
18342
18748
  function exactIndex(index, current) {
18343
- 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";
18344
18750
  }
18345
18751
  function pendingIndexMatches(index, current) {
18346
18752
  return current.filter((candidate) => {
18347
18753
  if (index.name && candidate.name !== index.name) return false;
18348
- 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";
18349
18755
  });
18350
18756
  }
18351
18757
  function plannedDocumentMatches(current, entry) {
@@ -18613,7 +19019,7 @@ async function restoreDataTaskPlan(plan, target, targetHint, lease) {
18613
19019
  await updateDataTaskBackup(safety, (manifest) => {
18614
19020
  manifest.pendingIndexes = [...manifest.pendingIndexes ?? [], pending];
18615
19021
  });
18616
- await collection.createIndex(index.key, { ...index.options, name: index.name });
19022
+ await collection.createIndex(comparableIndexKey(index.key), { ...index.options, name: index.name });
18617
19023
  if (!exactIndex(index, await collection.listIndexes())) {
18618
19024
  throw new DataTaskJobError("RESTORE_FAILED", `index "${index.name}" failed immediate readback.`, "restore");
18619
19025
  }
@@ -18785,6 +19191,7 @@ MonSQLize.PoolSelector = PoolSelector;
18785
19191
  MonSQLize.PoolStats = PoolStatsManager;
18786
19192
  MonSQLize.validatePoolConfig = validatePoolConfig;
18787
19193
  MonSQLize.validatePoolConfigSafe = validatePoolConfigSafe;
19194
+ MonSQLize.defineModel = defineModel;
18788
19195
  MonSQLize.Model = Model;
18789
19196
  MonSQLize.ModelInstance = ModelInstance;
18790
19197
  MonSQLize.expr = expr;
@@ -18874,6 +19281,7 @@ export {
18874
19281
  dataTasks,
18875
19282
  decodeCursor2 as decodeCursor,
18876
19283
  index_default as default,
19284
+ defineModel,
18877
19285
  encodeCursor2 as encodeCursor,
18878
19286
  expr,
18879
19287
  generateQueryHash,