monsqlize 3.5.1 → 3.5.3

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.
package/CHANGELOG.md CHANGED
@@ -1,18 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
- > Summary index — the 3.5.1 versioned details are in [changelogs/v3.5.1.md](./changelogs/v3.5.1.md). A versioned note does not establish npm publication; check the registry and immutable tag before deployment.
4
- > **Last updated**: 2026-10-28
3
+ > Summary index — the 3.5.3 versioned details are in [changelogs/v3.5.3.md](./changelogs/v3.5.3.md). A versioned note does not establish npm publication; check the registry and immutable tag before deployment.
4
+ > **Last updated**: 2026-11-02
5
5
 
6
6
  ---
7
7
 
8
8
  ## Unreleased
9
9
 
10
- README, docs-site share card and release tooling fixes are assigned to **3.5.1** ([versioned notes](./changelogs/v3.5.1.md)); GridFS buckets are in [3.5.0](./changelogs/v3.5.0.md); the Search index lifecycle API is in [3.4.0](./changelogs/v3.4.0.md); the weekly-scan cache-key, health-check and lockfile security fixes are in [3.3.4](./changelogs/v3.3.4.md); the schema-dsl 3.0.5 upgrade is in [3.3.3](./changelogs/v3.3.3.md); the audited 3.3.2 repairs are in its [versioned notes](./changelogs/v3.3.2.md) and the [next-change staging log](https://github.com/devcodex-labs/monSQLize/blob/main/changelogs/unreleased.md). Untagged checkouts remain release candidates.
10
+ GridFS and Search index fixes from the 2026-11-02 weekly scan, including the idempotent `finalizeResumableUpload` retry, are assigned to **3.5.3** ([versioned notes](./changelogs/v3.5.3.md)); vector search guidance, Search docs regrouping and the vector example are in [3.5.2](./changelogs/v3.5.2.md); README, docs-site share card and release tooling fixes are in [3.5.1](./changelogs/v3.5.1.md); GridFS buckets are in [3.5.0](./changelogs/v3.5.0.md); the Search index lifecycle API is in [3.4.0](./changelogs/v3.4.0.md); the weekly-scan cache-key, health-check and lockfile security fixes are in [3.3.4](./changelogs/v3.3.4.md); the schema-dsl 3.0.5 upgrade is in [3.3.3](./changelogs/v3.3.3.md); the audited 3.3.2 repairs are in its [versioned notes](./changelogs/v3.3.2.md) and the [next-change staging log](https://github.com/devcodex-labs/monSQLize/blob/main/changelogs/unreleased.md). Untagged checkouts remain release candidates.
11
11
 
12
12
  ## Version Overview
13
13
 
14
14
  | Version | Date | Summary | Details |
15
15
  |---------|------|---------|---------|
16
+ | [v3.5.3](./changelogs/v3.5.3.md) | 2026-11-02 | Patch: GridFS upload/download/resumable-session fixes, idempotent finalize retry (contract change), long Search index wait timeouts and appendChunks cost | [View](./changelogs/v3.5.3.md) |
17
+ | [v3.5.2](./changelogs/v3.5.2.md) | 2026-11-02 | Patch: corrected vector search guidance, regrouped Search docs and a new vector search example; no runtime changes | [View](./changelogs/v3.5.2.md) |
16
18
  | [v3.5.1](./changelogs/v3.5.1.md) | 2026-10-28 | Patch: README support wording and native-driver example, docs-site share card and locale meta; no runtime changes | [View](./changelogs/v3.5.1.md) |
17
19
  | [v3.5.0](./changelogs/v3.5.0.md) | 2026-10-27 | Minor: GridFS buckets with upload, download, ranges, ensureIndexes and phase-1 resumable upload | [View](./changelogs/v3.5.0.md) |
18
20
  | [v3.4.0](./changelogs/v3.4.0.md) | 2026-10-27 | Minor: Search index lifecycle API for Atlas Search and Vector Search | [View](./changelogs/v3.4.0.md) |
package/README.md CHANGED
@@ -14,7 +14,7 @@ Upgrade and security: [Migration Guide](./MIGRATION.md) · [Private Vulnerabilit
14
14
 
15
15
  The npm `latest` dist-tag and GitHub Pages are the stable channel; `main` may contain the next release. Pages deployment accepts only a versioned Git tag whose package version is already present on npm, so unpublished APIs are not promoted as stable documentation. The Google site ownership verification file is copied separately from the workflow commit, with its source commit and content hash recorded in the deployment summary.
16
16
 
17
- This source tree targets **3.5.1**, a documentation patch with no runtime changes (see the [3.5.1 notes](./changelogs/v3.5.1.md)); 3.5.0 added GridFS buckets on `msq.gridfs()` and `model.gridfs()`, and 3.4.0 added the Search index lifecycle API for Atlas Search and Vector Search. The 3.3.2 cache, Model, pagination and sync fixes, including cold-cache upgrades and checkpoint rollback limits, are described in the [3.3.2 notes](./changelogs/v3.3.2.md) and [migration guide](./MIGRATION.md). Read the upgrade guide in [English](https://github.com/devcodex-labs/monSQLize/blob/main/docs/en/upgrade-3.3.2.md) or [简体中文](https://github.com/devcodex-labs/monSQLize/blob/main/docs/zh/upgrade-3.3.2.md). Untagged checkouts remain release candidates; verify the immutable tag and requested registry version before deployment. Candidate preparation does not publish npm or update stable Pages.
17
+ This source tree targets **3.5.3**, a GridFS and Search index fix patch that also makes `finalizeResumableUpload` retries idempotent (see the [3.5.3 notes](./changelogs/v3.5.3.md)); 3.5.0 added GridFS buckets on `msq.gridfs()` and `model.gridfs()`, and 3.4.0 added the Search index lifecycle API for Atlas Search and Vector Search. The 3.3.2 cache, Model, pagination and sync fixes, including cold-cache upgrades and checkpoint rollback limits, are described in the [3.3.2 notes](./changelogs/v3.3.2.md) and [migration guide](./MIGRATION.md). Read the upgrade guide in [English](https://github.com/devcodex-labs/monSQLize/blob/main/docs/en/upgrade-3.3.2.md) or [简体中文](https://github.com/devcodex-labs/monSQLize/blob/main/docs/zh/upgrade-3.3.2.md). Untagged checkouts remain release candidates; verify the immutable tag and requested registry version before deployment. Candidate preparation does not publish npm or update stable Pages.
18
18
 
19
19
  This development source is pinned to verified registry `schema-dsl@3.0.5`. The v3 package remains subject to the full release gate before an immutable tag and npm publication.
20
20
 
@@ -0,0 +1,18 @@
1
+ # monSQLize v3.5.2
2
+
3
+ > Release date: 2026-11-02
4
+ > Publication authority: the immutable tag and npm registry. An untagged checkout is a release candidate; the date above does not establish registry availability.
5
+
6
+ ## Highlights
7
+
8
+ Patch release for documentation. There are no runtime or library changes: public APIs, defaults, bundle output and dependency versions are the same as 3.5.1.
9
+
10
+ ## Documentation
11
+
12
+ - The Vector Search page no longer claims that no vector index lifecycle API is provided; it links the Search index page, which manages Atlas Search and Vector Search indexes since 3.4.0.
13
+ - `vectorSearch()` and the Search index pages are grouped under Search and vectors in the docs site, and the capability traceability tables gain a Search index row. Pages that belong in the sidebar but were unlisted are now published.
14
+ - A runnable `examples/docs/vector-search.ts` example is added, long Search index behavior tables are split for mobile, and terminology is unified across English and Chinese pages.
15
+
16
+ ## Upgrade and rollback
17
+
18
+ No migration is required. Rollback to 3.5.1 is safe and changes only the packaged README and changelog.
@@ -0,0 +1,32 @@
1
+ # monSQLize v3.5.3
2
+
3
+ > Release date: 2026-11-02
4
+ > Publication authority: the immutable tag and npm registry. An untagged checkout is a release candidate; the date above does not establish registry availability.
5
+
6
+ ## Highlights
7
+
8
+ Patch release with GridFS and Search index fixes found by the 2026-11-02 weekly scan, plus one GridFS contract change: retrying `finalizeResumableUpload` is now idempotent.
9
+
10
+ ## Contract change
11
+
12
+ - `finalizeResumableUpload` retried while its upload session still exists, for an id whose file is already visible with the same `metadata.sha256` and length as the session, now **returns that committed file and deletes the session** instead of throwing `GRIDFS_FILE_EXISTS`. A visible file with a different sha256 or length still throws `GRIDFS_FILE_EXISTS`. A retry after the session is gone still throws `GRIDFS_FILE_NOT_FOUND`; use `getFile(uploadId)` to check the result. Callers that treated `GRIDFS_FILE_EXISTS` on retry as "already finalized" now receive the file info instead.
13
+
14
+ ## Fixes
15
+
16
+ - `uploadStream` with an `id` that already has a file now rejects with `GRIDFS_FILE_EXISTS` before writing; a duplicate-key write failure no longer runs cleanup that deleted the original file's chunks and files document.
17
+ - `downloadStream` closes the driver stream and its server cursor when the consumer destroys it early; `download` does the same on a read error.
18
+ - `appendChunks` that loses a race with `abortResumableUpload` now rejects with `GRIDFS_FILE_NOT_FOUND` and removes the chunks it wrote.
19
+ - `drop()` resets the bucket's index latch, so `ensureIndexes()` and later writes on the same bucket object recreate the indexes.
20
+ - `uploadStream` failures before the driver stream opens now destroy the returned `Writable` with the mapped error; `upload` with an existing `id` releases a `Readable` source.
21
+ - `finalizeResumableUpload` no longer fails after the files document is committed when only the session cleanup fails (logged as a warning; the TTL removes the session); post-insert reads and filename lookups map driver errors. Retrying `finalizeResumableUpload` while the session exists is now idempotent: a visible file with the same id, sha256 and length is returned and the session deleted; a different file still throws `GRIDFS_FILE_EXISTS`, and a retry after the session is gone still throws `GRIDFS_FILE_NOT_FOUND`.
22
+ - `abortResumableUpload` deletes leftover chunks even when the session expired; `beginResumableUpload` with a reused explicit `id` clears leftover chunks; `GRIDFS_RESUME_MISMATCH` details report the real `expectedNextChunk`.
23
+ - `waitForSearchIndex` arms its deadline in segments, so `timeoutMs` above 2,147,483,647 ms no longer times out after 1 ms.
24
+ - `appendChunks` reads only chunks at or after the session's `nextChunk`, restoring a near-constant cost per append (3,000 chunks of 1 KiB on a local memory server: the last 500 appends averaged 16.76 ms before and 3.69 ms after).
25
+
26
+ ## Packaging
27
+
28
+ The runtime bundle ceilings in `config/package-budget.json` were raised for these fixes. No dependency versions changed.
29
+
30
+ ## Upgrade and rollback
31
+
32
+ No migration is required. Rolling back to 3.5.2 restores the previous defects, including data loss when `uploadStream` reuses an existing id, and the old `GRIDFS_FILE_EXISTS` result on finalize retries.
@@ -31,7 +31,7 @@ var import_node_url = require("node:url");
31
31
  // package.json
32
32
  var package_default = {
33
33
  name: "monsqlize",
34
- version: "3.5.1",
34
+ version: "3.5.3",
35
35
  description: "TypeScript production data runtime layer for MongoDB today, with cache, transactions, pools, models, sync, and observability",
36
36
  type: "commonjs",
37
37
  main: "./dist/cjs/index.cjs",
@@ -52,6 +52,8 @@ var package_default = {
52
52
  "./package.json": "./package.json"
53
53
  },
54
54
  files: [
55
+ "changelogs/v3.5.3.md",
56
+ "changelogs/v3.5.2.md",
55
57
  "changelogs/v3.5.1.md",
56
58
  "changelogs/v3.5.0.md",
57
59
  "changelogs/v3.4.0.md",
@@ -6406,6 +6406,7 @@ async function uploadFile(ctx, filename, data, opts) {
6406
6406
  }
6407
6407
  await ctx.indexes.ensure();
6408
6408
  if (options.id && await getFile(ctx, options.id)) {
6409
+ if (source) releaseReadable(source);
6409
6410
  throw fileExists("upload", ctx, { id: options.id, filename, sha256: options.sha256 });
6410
6411
  }
6411
6412
  if (ifExists !== "duplicate") {
@@ -6441,6 +6442,9 @@ function openUploadStream(ctx, filename, opts) {
6441
6442
  let target;
6442
6443
  try {
6443
6444
  await ctx.indexes.ensure();
6445
+ if (await getFile(ctx, id)) {
6446
+ throw fileExists("uploadStream", ctx, { id, filename, sha256: options.sha256 });
6447
+ }
6444
6448
  target = ctx.driver.openUploadStream(id, filename, {
6445
6449
  chunkSizeBytes,
6446
6450
  metadata,
@@ -6452,8 +6456,10 @@ function openUploadStream(ctx, filename, opts) {
6452
6456
  if (!info) throw fileNotFound("uploadStream", ctx, { id, filename });
6453
6457
  return info;
6454
6458
  } catch (error) {
6455
- if (target) await cleanupFailedWrite(ctx, id, target);
6456
- throw mapDriverError(error, "uploadStream", ctx, { id, filename });
6459
+ if (target) await cleanupFailedWrite(ctx, id, target, error);
6460
+ const mapped = mapDriverError(error, "uploadStream", ctx, { id, filename });
6461
+ if (!input.destroyed) input.destroy(mapped);
6462
+ throw mapped;
6457
6463
  }
6458
6464
  })();
6459
6465
  void done.catch(() => void 0);
@@ -6466,6 +6472,7 @@ async function downloadFile(ctx, idOrName) {
6466
6472
  try {
6467
6473
  return await readReadable(source);
6468
6474
  } catch (error) {
6475
+ abortDriverRead(source);
6469
6476
  throw mapDriverError(error, "download", ctx, { id: file.id, filename: file.filename });
6470
6477
  }
6471
6478
  }
@@ -6478,8 +6485,12 @@ function openDownloadStream(ctx, idOrName, range) {
6478
6485
  const file = await resolveFile(ctx, idOrName, "downloadStream");
6479
6486
  if (!file) throw fileNotFound("downloadStream", ctx, targetOf(idOrName));
6480
6487
  const bounds = rangeBounds(shape, file.length, "downloadStream", ctx.bucketName);
6488
+ if (output.destroyed) return;
6481
6489
  const source = ctx.driver.openDownloadStream(file.id, bounds);
6482
6490
  source.on("error", (error) => output.destroy(mapDriverError(error, "downloadStream", ctx, { id: file.id, filename: file.filename })));
6491
+ output.once("close", () => {
6492
+ if (!source.readableEnded) abortDriverRead(source);
6493
+ });
6483
6494
  source.pipe(output);
6484
6495
  } catch (error) {
6485
6496
  output.destroy(mapDriverError(error, "downloadStream", ctx, targetOf(idOrName)));
@@ -6544,6 +6555,7 @@ async function dropBucket(ctx) {
6544
6555
  await dropIfExists(ctx, chunksCollectionName(ctx.bucketName));
6545
6556
  await dropIfExists(ctx, filesCollectionName(ctx.bucketName));
6546
6557
  await dropIfExists(ctx, uploadsCollectionName(ctx.bucketName));
6558
+ ctx.indexes.reset();
6547
6559
  }
6548
6560
  async function dropIfExists(ctx, name) {
6549
6561
  try {
@@ -6555,7 +6567,12 @@ async function dropIfExists(ctx, name) {
6555
6567
  async function findConflict(ctx, filename, sha256) {
6556
6568
  const files = ctx.db.collection(filesCollectionName(ctx.bucketName));
6557
6569
  const filter = sha256 !== void 0 ? { "metadata.sha256": sha256 } : { filename };
6558
- const docs = await files.find(filter).sort({ uploadDate: -1 }).limit(1).toArray();
6570
+ let docs;
6571
+ try {
6572
+ docs = await files.find(filter).sort({ uploadDate: -1 }).limit(1).toArray();
6573
+ } catch (error) {
6574
+ throw mapDriverError(error, "upload", ctx, { filename });
6575
+ }
6559
6576
  return docs[0] ? toFileInfo(docs[0]) : null;
6560
6577
  }
6561
6578
  async function resolveFile(ctx, idOrName, operation) {
@@ -6570,15 +6587,28 @@ async function resolveFile(ctx, idOrName, operation) {
6570
6587
  }
6571
6588
  const files = ctx.db.collection(filesCollectionName(ctx.bucketName));
6572
6589
  const cursor = revision >= 0 ? files.find({ filename: idOrName.filename }).sort({ uploadDate: 1 }).skip(revision).limit(1) : files.find({ filename: idOrName.filename }).sort({ uploadDate: -1 }).skip(-revision - 1).limit(1);
6573
- const docs = await cursor.toArray();
6590
+ let docs;
6591
+ try {
6592
+ docs = await cursor.toArray();
6593
+ } catch (error) {
6594
+ throw mapDriverError(error, operation, ctx, { filename: idOrName.filename });
6595
+ }
6574
6596
  return docs[0] ? toFileInfo(docs[0]) : null;
6575
6597
  }
6598
+ function abortDriverRead(source) {
6599
+ const abortable = source;
6600
+ if (typeof abortable.abort === "function") {
6601
+ void abortable.abort().catch(() => void 0);
6602
+ } else if (!source.destroyed) {
6603
+ source.destroy();
6604
+ }
6605
+ }
6576
6606
  async function writeBuffer(ctx, id, filename, payload, options) {
6577
6607
  const stream = ctx.driver.openUploadStream(id, filename, options);
6578
6608
  try {
6579
6609
  await endWritable(stream, payload);
6580
6610
  } catch (error) {
6581
- await cleanupFailedWrite(ctx, id, stream);
6611
+ await cleanupFailedWrite(ctx, id, stream, error);
6582
6612
  throw mapDriverError(error, "upload", ctx, { id, filename });
6583
6613
  }
6584
6614
  }
@@ -6588,7 +6618,7 @@ async function writeReadable(ctx, id, filename, source, options) {
6588
6618
  const { pipeline } = await import("node:stream/promises");
6589
6619
  await pipeline(source, stream);
6590
6620
  } catch (error) {
6591
- await cleanupFailedWrite(ctx, id, stream);
6621
+ await cleanupFailedWrite(ctx, id, stream, error);
6592
6622
  throw mapDriverError(error, "upload", ctx, { id, filename });
6593
6623
  }
6594
6624
  }
@@ -6603,7 +6633,8 @@ function endWritable(stream, payload) {
6603
6633
  stream.end(payload);
6604
6634
  });
6605
6635
  }
6606
- async function cleanupFailedWrite(ctx, id, stream) {
6636
+ async function cleanupFailedWrite(ctx, id, stream, cause) {
6637
+ if (isDuplicateKey(cause)) return;
6607
6638
  try {
6608
6639
  if (typeof stream.abort === "function") await stream.abort();
6609
6640
  } catch {
@@ -6657,6 +6688,11 @@ async function beginResumableUpload(ctx, filename, opts) {
6657
6688
  } catch (error) {
6658
6689
  throw mapDriverError(error, "beginResumableUpload", ctx, { uploadId, filename });
6659
6690
  }
6691
+ try {
6692
+ await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
6693
+ } catch (error) {
6694
+ throw mapDriverError(error, "beginResumableUpload", ctx, { uploadId, filename });
6695
+ }
6660
6696
  return { uploadId, nextChunk: 0, chunkSizeBytes };
6661
6697
  }
6662
6698
  async function appendChunks(ctx, uploadId, fromChunk, data) {
@@ -6665,7 +6701,7 @@ async function appendChunks(ctx, uploadId, fromChunk, data) {
6665
6701
  const payload = await payloadBytes(data);
6666
6702
  if (payload.length === 0) throw badChunkSize();
6667
6703
  const session = await requirePending(ctx, uploadId, "appendChunks");
6668
- const nextChunk = await contiguousNext(ctx, uploadId);
6704
+ const nextChunk = await contiguousNext(ctx, uploadId, session.nextChunk);
6669
6705
  if (fromChunk > nextChunk) {
6670
6706
  throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk, expectedNextChunk: nextChunk });
6671
6707
  }
@@ -6675,23 +6711,32 @@ async function appendChunks(ctx, uploadId, fromChunk, data) {
6675
6711
  throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk, expectedNextChunk: nextChunk });
6676
6712
  }
6677
6713
  for (const slice of slices) {
6678
- if (slice.index < nextChunk) await assertStoredChunk(ctx, uploadId, slice.index, slice.bytes);
6679
- else await writeChunk(ctx, uploadId, slice.index, slice.bytes);
6714
+ if (slice.index < nextChunk) await assertStoredChunk(ctx, uploadId, slice.index, slice.bytes, nextChunk);
6715
+ else await writeChunk(ctx, uploadId, slice.index, slice.bytes, nextChunk);
6716
+ }
6717
+ const advanced = await contiguousNext(ctx, uploadId, nextChunk);
6718
+ let matched;
6719
+ try {
6720
+ const updated = await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).updateOne(
6721
+ { _id: uploadId, status: "pending" },
6722
+ { $max: { nextChunk: advanced }, $set: { updatedAt: /* @__PURE__ */ new Date() } }
6723
+ );
6724
+ matched = updated.matchedCount;
6725
+ } catch (error) {
6726
+ throw mapDriverError(error, "appendChunks", ctx, { uploadId });
6727
+ }
6728
+ if (matched === 0) {
6729
+ await removeInvisibleChunks(ctx, uploadId, "appendChunks");
6730
+ throw fileNotFound("appendChunks", ctx, { id: uploadId });
6680
6731
  }
6681
- const advanced = await contiguousNext(ctx, uploadId);
6682
- await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).updateOne(
6683
- { _id: uploadId },
6684
- { $set: { nextChunk: advanced, updatedAt: /* @__PURE__ */ new Date() } }
6685
- );
6686
6732
  return { nextChunk: advanced, bytesAccepted: payload.length };
6687
6733
  }
6688
6734
  async function finalizeResumableUpload(ctx, uploadId) {
6689
6735
  assertObjectId(uploadId, "finalizeResumableUpload", "uploadId");
6690
6736
  const session = await requirePending(ctx, uploadId, "finalizeResumableUpload");
6691
6737
  const files = ctx.db.collection(filesCollectionName(ctx.bucketName));
6692
- if (await files.findOne({ _id: uploadId }, { projection: { _id: 1 } })) {
6693
- throw fileExists("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename, sha256: session.sha256 });
6694
- }
6738
+ const committed = await committedFile(ctx, uploadId, session);
6739
+ if (committed) return committed;
6695
6740
  const expected = chunkCount(session.length, session.chunkSizeBytes);
6696
6741
  const hash = (0, import_node_crypto4.createHash)("sha256");
6697
6742
  let total = 0;
@@ -6748,12 +6793,22 @@ async function finalizeResumableUpload(ctx, uploadId) {
6748
6793
  });
6749
6794
  } catch (error) {
6750
6795
  if (isDuplicateKey(error)) {
6751
- throw fileExists("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename, sha256: session.sha256 });
6796
+ const raced = await committedFile(ctx, uploadId, session);
6797
+ if (raced) return raced;
6752
6798
  }
6753
6799
  throw mapDriverError(error, "finalizeResumableUpload", ctx, { uploadId, filename: session.filename });
6754
6800
  }
6755
- await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).deleteOne({ _id: uploadId });
6756
- const info = await files.findOne({ _id: uploadId });
6801
+ try {
6802
+ await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).deleteOne({ _id: uploadId });
6803
+ } catch (error) {
6804
+ ctx.logger?.warn?.("[gridfs] finalizeResumableUpload could not delete the upload session", { bucket: ctx.bucketName, uploadId: String(uploadId), error });
6805
+ }
6806
+ let info;
6807
+ try {
6808
+ info = await files.findOne({ _id: uploadId });
6809
+ } catch (error) {
6810
+ throw mapDriverError(error, "finalizeResumableUpload", ctx, { uploadId, filename: session.filename });
6811
+ }
6757
6812
  if (!info) throw fileNotFound("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename });
6758
6813
  return toFileInfo(info);
6759
6814
  }
@@ -6762,17 +6817,37 @@ async function abortResumableUpload(ctx, uploadId) {
6762
6817
  try {
6763
6818
  const uploads = ctx.db.collection(uploadsCollectionName(ctx.bucketName));
6764
6819
  const session = await uploads.findOne({ _id: uploadId });
6765
- if (!session || session.status === "aborted" || session.status === "finalized") return;
6820
+ if (session?.status === "finalized") return;
6821
+ await removeInvisibleChunks(ctx, uploadId, "abortResumableUpload");
6822
+ if (session) await uploads.deleteOne({ _id: uploadId });
6823
+ } catch (error) {
6824
+ throw mapDriverError(error, "abortResumableUpload", ctx, { uploadId });
6825
+ }
6826
+ }
6827
+ async function committedFile(ctx, uploadId, session) {
6828
+ const target = { uploadId, filename: session.filename };
6829
+ try {
6830
+ const doc = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne({ _id: uploadId });
6831
+ if (!doc) return null;
6832
+ const info = toFileInfo(doc);
6833
+ if (info.metadata?.sha256 !== session.sha256 || info.length !== session.length) {
6834
+ throw fileExists("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename, sha256: session.sha256 });
6835
+ }
6836
+ await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).deleteOne({ _id: uploadId, status: "pending" });
6837
+ return info;
6838
+ } catch (error) {
6839
+ throw mapDriverError(error, "finalizeResumableUpload", ctx, target);
6840
+ }
6841
+ }
6842
+ async function removeInvisibleChunks(ctx, uploadId, operation) {
6843
+ try {
6766
6844
  const visible = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne(
6767
6845
  { _id: uploadId },
6768
6846
  { projection: { _id: 1 } }
6769
6847
  );
6770
- if (!visible) {
6771
- await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
6772
- }
6773
- await uploads.deleteOne({ _id: uploadId });
6848
+ if (!visible) await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
6774
6849
  } catch (error) {
6775
- throw mapDriverError(error, "abortResumableUpload", ctx, { uploadId });
6850
+ throw mapDriverError(error, operation, ctx, { uploadId });
6776
6851
  }
6777
6852
  }
6778
6853
  async function payloadBytes(data) {
@@ -6803,27 +6878,33 @@ async function requirePending(ctx, uploadId, operation) {
6803
6878
  if (!doc || doc.status !== "pending") throw fileNotFound(operation, ctx, { id: uploadId });
6804
6879
  return doc;
6805
6880
  }
6806
- async function contiguousNext(ctx, uploadId) {
6807
- const docs = await ctx.db.collection(chunksCollectionName(ctx.bucketName)).find({ files_id: uploadId }).sort({ n: 1 }).project({ n: 1 }).toArray();
6808
- let expected = 0;
6881
+ async function contiguousNext(ctx, uploadId, from) {
6882
+ const start = Number.isInteger(from) && from > 0 ? from : 0;
6883
+ let docs;
6884
+ try {
6885
+ docs = await ctx.db.collection(chunksCollectionName(ctx.bucketName)).find({ files_id: uploadId, n: { $gte: start } }).sort({ n: 1 }).project({ n: 1 }).toArray();
6886
+ } catch (error) {
6887
+ throw mapDriverError(error, "appendChunks", ctx, { uploadId });
6888
+ }
6889
+ let expected = start;
6809
6890
  for (const doc of docs) {
6810
6891
  if (doc.n !== expected) break;
6811
6892
  expected += 1;
6812
6893
  }
6813
6894
  return expected;
6814
6895
  }
6815
- async function assertStoredChunk(ctx, uploadId, n, slice) {
6896
+ async function assertStoredChunk(ctx, uploadId, n, slice, expectedNextChunk) {
6816
6897
  const existing = await ctx.db.collection(chunksCollectionName(ctx.bucketName)).findOne({ files_id: uploadId, n });
6817
6898
  if (!existing || !sameBytes(binaryToBuffer(existing.data, "appendChunks"), slice)) {
6818
- throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk: n, expectedNextChunk: n });
6899
+ throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk: n, expectedNextChunk });
6819
6900
  }
6820
6901
  }
6821
- async function writeChunk(ctx, uploadId, n, slice) {
6902
+ async function writeChunk(ctx, uploadId, n, slice, expectedNextChunk) {
6822
6903
  try {
6823
6904
  await ctx.db.collection(chunksCollectionName(ctx.bucketName)).insertOne({ files_id: uploadId, n, data: slice });
6824
6905
  } catch (error) {
6825
6906
  if (!isDuplicateKey(error)) throw mapDriverError(error, "appendChunks", ctx, { uploadId });
6826
- await assertStoredChunk(ctx, uploadId, n, slice);
6907
+ await assertStoredChunk(ctx, uploadId, n, slice, expectedNextChunk);
6827
6908
  }
6828
6909
  }
6829
6910
 
@@ -6958,18 +7039,25 @@ var GridFSIndexLatch = class {
6958
7039
  this.bucketName = bucketName;
6959
7040
  this.inflight = null;
6960
7041
  this.ready = false;
7042
+ this.generation = 0;
6961
7043
  }
6962
7044
  ensure() {
6963
7045
  if (this.ready) return Promise.resolve();
6964
7046
  if (!this.inflight) {
7047
+ const generation = this.generation;
6965
7048
  this.inflight = this.create().then(() => {
6966
- this.ready = true;
7049
+ if (generation === this.generation) this.ready = true;
6967
7050
  }).finally(() => {
6968
7051
  this.inflight = null;
6969
7052
  });
6970
7053
  }
6971
7054
  return this.inflight;
6972
7055
  }
7056
+ /** Forget a completed ensure, e.g. after the bucket collections were dropped, so the next call recreates indexes. */
7057
+ reset() {
7058
+ this.ready = false;
7059
+ this.generation += 1;
7060
+ }
6973
7061
  async create() {
6974
7062
  const files = this.db.collection(filesCollectionName(this.bucketName));
6975
7063
  const chunks2 = this.db.collection(chunksCollectionName(this.bucketName));
@@ -10536,15 +10624,24 @@ function sleep2(ms, signal) {
10536
10624
  signal?.addEventListener("abort", onAbort, { once: true });
10537
10625
  });
10538
10626
  }
10627
+ var MAX_TIMER_MS = 2147483647;
10539
10628
  function linkSignal(userSignal, remainingMs) {
10540
10629
  const controller = new AbortController();
10541
10630
  let expired = false;
10542
10631
  const onUserAbort = () => controller.abort(userSignal?.reason);
10543
10632
  userSignal?.addEventListener("abort", onUserAbort, { once: true });
10544
- const timer = setTimeout(() => {
10545
- expired = true;
10546
- controller.abort(new Error("search index wait deadline reached"));
10547
- }, Math.max(0, remainingMs));
10633
+ let timer;
10634
+ const arm = (ms) => {
10635
+ timer = setTimeout(() => {
10636
+ if (ms > MAX_TIMER_MS) {
10637
+ arm(ms - MAX_TIMER_MS);
10638
+ return;
10639
+ }
10640
+ expired = true;
10641
+ controller.abort(new Error("search index wait deadline reached"));
10642
+ }, Math.min(ms, MAX_TIMER_MS));
10643
+ };
10644
+ arm(Math.max(0, remainingMs));
10548
10645
  return {
10549
10646
  signal: controller.signal,
10550
10647
  timedOut: () => expired,
@@ -6389,6 +6389,7 @@ async function uploadFile(ctx, filename, data, opts) {
6389
6389
  }
6390
6390
  await ctx.indexes.ensure();
6391
6391
  if (options.id && await getFile(ctx, options.id)) {
6392
+ if (source) releaseReadable(source);
6392
6393
  throw fileExists("upload", ctx, { id: options.id, filename, sha256: options.sha256 });
6393
6394
  }
6394
6395
  if (ifExists !== "duplicate") {
@@ -6424,6 +6425,9 @@ function openUploadStream(ctx, filename, opts) {
6424
6425
  let target;
6425
6426
  try {
6426
6427
  await ctx.indexes.ensure();
6428
+ if (await getFile(ctx, id)) {
6429
+ throw fileExists("uploadStream", ctx, { id, filename, sha256: options.sha256 });
6430
+ }
6427
6431
  target = ctx.driver.openUploadStream(id, filename, {
6428
6432
  chunkSizeBytes,
6429
6433
  metadata,
@@ -6435,8 +6439,10 @@ function openUploadStream(ctx, filename, opts) {
6435
6439
  if (!info) throw fileNotFound("uploadStream", ctx, { id, filename });
6436
6440
  return info;
6437
6441
  } catch (error) {
6438
- if (target) await cleanupFailedWrite(ctx, id, target);
6439
- throw mapDriverError(error, "uploadStream", ctx, { id, filename });
6442
+ if (target) await cleanupFailedWrite(ctx, id, target, error);
6443
+ const mapped = mapDriverError(error, "uploadStream", ctx, { id, filename });
6444
+ if (!input.destroyed) input.destroy(mapped);
6445
+ throw mapped;
6440
6446
  }
6441
6447
  })();
6442
6448
  void done.catch(() => void 0);
@@ -6449,6 +6455,7 @@ async function downloadFile(ctx, idOrName) {
6449
6455
  try {
6450
6456
  return await readReadable(source);
6451
6457
  } catch (error) {
6458
+ abortDriverRead(source);
6452
6459
  throw mapDriverError(error, "download", ctx, { id: file.id, filename: file.filename });
6453
6460
  }
6454
6461
  }
@@ -6461,8 +6468,12 @@ function openDownloadStream(ctx, idOrName, range) {
6461
6468
  const file = await resolveFile(ctx, idOrName, "downloadStream");
6462
6469
  if (!file) throw fileNotFound("downloadStream", ctx, targetOf(idOrName));
6463
6470
  const bounds = rangeBounds(shape, file.length, "downloadStream", ctx.bucketName);
6471
+ if (output.destroyed) return;
6464
6472
  const source = ctx.driver.openDownloadStream(file.id, bounds);
6465
6473
  source.on("error", (error) => output.destroy(mapDriverError(error, "downloadStream", ctx, { id: file.id, filename: file.filename })));
6474
+ output.once("close", () => {
6475
+ if (!source.readableEnded) abortDriverRead(source);
6476
+ });
6466
6477
  source.pipe(output);
6467
6478
  } catch (error) {
6468
6479
  output.destroy(mapDriverError(error, "downloadStream", ctx, targetOf(idOrName)));
@@ -6527,6 +6538,7 @@ async function dropBucket(ctx) {
6527
6538
  await dropIfExists(ctx, chunksCollectionName(ctx.bucketName));
6528
6539
  await dropIfExists(ctx, filesCollectionName(ctx.bucketName));
6529
6540
  await dropIfExists(ctx, uploadsCollectionName(ctx.bucketName));
6541
+ ctx.indexes.reset();
6530
6542
  }
6531
6543
  async function dropIfExists(ctx, name) {
6532
6544
  try {
@@ -6538,7 +6550,12 @@ async function dropIfExists(ctx, name) {
6538
6550
  async function findConflict(ctx, filename, sha256) {
6539
6551
  const files = ctx.db.collection(filesCollectionName(ctx.bucketName));
6540
6552
  const filter = sha256 !== void 0 ? { "metadata.sha256": sha256 } : { filename };
6541
- const docs = await files.find(filter).sort({ uploadDate: -1 }).limit(1).toArray();
6553
+ let docs;
6554
+ try {
6555
+ docs = await files.find(filter).sort({ uploadDate: -1 }).limit(1).toArray();
6556
+ } catch (error) {
6557
+ throw mapDriverError(error, "upload", ctx, { filename });
6558
+ }
6542
6559
  return docs[0] ? toFileInfo(docs[0]) : null;
6543
6560
  }
6544
6561
  async function resolveFile(ctx, idOrName, operation) {
@@ -6553,15 +6570,28 @@ async function resolveFile(ctx, idOrName, operation) {
6553
6570
  }
6554
6571
  const files = ctx.db.collection(filesCollectionName(ctx.bucketName));
6555
6572
  const cursor = revision >= 0 ? files.find({ filename: idOrName.filename }).sort({ uploadDate: 1 }).skip(revision).limit(1) : files.find({ filename: idOrName.filename }).sort({ uploadDate: -1 }).skip(-revision - 1).limit(1);
6556
- const docs = await cursor.toArray();
6573
+ let docs;
6574
+ try {
6575
+ docs = await cursor.toArray();
6576
+ } catch (error) {
6577
+ throw mapDriverError(error, operation, ctx, { filename: idOrName.filename });
6578
+ }
6557
6579
  return docs[0] ? toFileInfo(docs[0]) : null;
6558
6580
  }
6581
+ function abortDriverRead(source) {
6582
+ const abortable = source;
6583
+ if (typeof abortable.abort === "function") {
6584
+ void abortable.abort().catch(() => void 0);
6585
+ } else if (!source.destroyed) {
6586
+ source.destroy();
6587
+ }
6588
+ }
6559
6589
  async function writeBuffer(ctx, id, filename, payload, options) {
6560
6590
  const stream = ctx.driver.openUploadStream(id, filename, options);
6561
6591
  try {
6562
6592
  await endWritable(stream, payload);
6563
6593
  } catch (error) {
6564
- await cleanupFailedWrite(ctx, id, stream);
6594
+ await cleanupFailedWrite(ctx, id, stream, error);
6565
6595
  throw mapDriverError(error, "upload", ctx, { id, filename });
6566
6596
  }
6567
6597
  }
@@ -6571,7 +6601,7 @@ async function writeReadable(ctx, id, filename, source, options) {
6571
6601
  const { pipeline } = await import("node:stream/promises");
6572
6602
  await pipeline(source, stream);
6573
6603
  } catch (error) {
6574
- await cleanupFailedWrite(ctx, id, stream);
6604
+ await cleanupFailedWrite(ctx, id, stream, error);
6575
6605
  throw mapDriverError(error, "upload", ctx, { id, filename });
6576
6606
  }
6577
6607
  }
@@ -6586,7 +6616,8 @@ function endWritable(stream, payload) {
6586
6616
  stream.end(payload);
6587
6617
  });
6588
6618
  }
6589
- async function cleanupFailedWrite(ctx, id, stream) {
6619
+ async function cleanupFailedWrite(ctx, id, stream, cause) {
6620
+ if (isDuplicateKey(cause)) return;
6590
6621
  try {
6591
6622
  if (typeof stream.abort === "function") await stream.abort();
6592
6623
  } catch {
@@ -6640,6 +6671,11 @@ async function beginResumableUpload(ctx, filename, opts) {
6640
6671
  } catch (error) {
6641
6672
  throw mapDriverError(error, "beginResumableUpload", ctx, { uploadId, filename });
6642
6673
  }
6674
+ try {
6675
+ await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
6676
+ } catch (error) {
6677
+ throw mapDriverError(error, "beginResumableUpload", ctx, { uploadId, filename });
6678
+ }
6643
6679
  return { uploadId, nextChunk: 0, chunkSizeBytes };
6644
6680
  }
6645
6681
  async function appendChunks(ctx, uploadId, fromChunk, data) {
@@ -6648,7 +6684,7 @@ async function appendChunks(ctx, uploadId, fromChunk, data) {
6648
6684
  const payload = await payloadBytes(data);
6649
6685
  if (payload.length === 0) throw badChunkSize();
6650
6686
  const session = await requirePending(ctx, uploadId, "appendChunks");
6651
- const nextChunk = await contiguousNext(ctx, uploadId);
6687
+ const nextChunk = await contiguousNext(ctx, uploadId, session.nextChunk);
6652
6688
  if (fromChunk > nextChunk) {
6653
6689
  throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk, expectedNextChunk: nextChunk });
6654
6690
  }
@@ -6658,23 +6694,32 @@ async function appendChunks(ctx, uploadId, fromChunk, data) {
6658
6694
  throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk, expectedNextChunk: nextChunk });
6659
6695
  }
6660
6696
  for (const slice of slices) {
6661
- if (slice.index < nextChunk) await assertStoredChunk(ctx, uploadId, slice.index, slice.bytes);
6662
- else await writeChunk(ctx, uploadId, slice.index, slice.bytes);
6697
+ if (slice.index < nextChunk) await assertStoredChunk(ctx, uploadId, slice.index, slice.bytes, nextChunk);
6698
+ else await writeChunk(ctx, uploadId, slice.index, slice.bytes, nextChunk);
6699
+ }
6700
+ const advanced = await contiguousNext(ctx, uploadId, nextChunk);
6701
+ let matched;
6702
+ try {
6703
+ const updated = await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).updateOne(
6704
+ { _id: uploadId, status: "pending" },
6705
+ { $max: { nextChunk: advanced }, $set: { updatedAt: /* @__PURE__ */ new Date() } }
6706
+ );
6707
+ matched = updated.matchedCount;
6708
+ } catch (error) {
6709
+ throw mapDriverError(error, "appendChunks", ctx, { uploadId });
6710
+ }
6711
+ if (matched === 0) {
6712
+ await removeInvisibleChunks(ctx, uploadId, "appendChunks");
6713
+ throw fileNotFound("appendChunks", ctx, { id: uploadId });
6663
6714
  }
6664
- const advanced = await contiguousNext(ctx, uploadId);
6665
- await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).updateOne(
6666
- { _id: uploadId },
6667
- { $set: { nextChunk: advanced, updatedAt: /* @__PURE__ */ new Date() } }
6668
- );
6669
6715
  return { nextChunk: advanced, bytesAccepted: payload.length };
6670
6716
  }
6671
6717
  async function finalizeResumableUpload(ctx, uploadId) {
6672
6718
  assertObjectId(uploadId, "finalizeResumableUpload", "uploadId");
6673
6719
  const session = await requirePending(ctx, uploadId, "finalizeResumableUpload");
6674
6720
  const files = ctx.db.collection(filesCollectionName(ctx.bucketName));
6675
- if (await files.findOne({ _id: uploadId }, { projection: { _id: 1 } })) {
6676
- throw fileExists("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename, sha256: session.sha256 });
6677
- }
6721
+ const committed = await committedFile(ctx, uploadId, session);
6722
+ if (committed) return committed;
6678
6723
  const expected = chunkCount(session.length, session.chunkSizeBytes);
6679
6724
  const hash = createHash3("sha256");
6680
6725
  let total = 0;
@@ -6731,12 +6776,22 @@ async function finalizeResumableUpload(ctx, uploadId) {
6731
6776
  });
6732
6777
  } catch (error) {
6733
6778
  if (isDuplicateKey(error)) {
6734
- throw fileExists("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename, sha256: session.sha256 });
6779
+ const raced = await committedFile(ctx, uploadId, session);
6780
+ if (raced) return raced;
6735
6781
  }
6736
6782
  throw mapDriverError(error, "finalizeResumableUpload", ctx, { uploadId, filename: session.filename });
6737
6783
  }
6738
- await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).deleteOne({ _id: uploadId });
6739
- const info = await files.findOne({ _id: uploadId });
6784
+ try {
6785
+ await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).deleteOne({ _id: uploadId });
6786
+ } catch (error) {
6787
+ ctx.logger?.warn?.("[gridfs] finalizeResumableUpload could not delete the upload session", { bucket: ctx.bucketName, uploadId: String(uploadId), error });
6788
+ }
6789
+ let info;
6790
+ try {
6791
+ info = await files.findOne({ _id: uploadId });
6792
+ } catch (error) {
6793
+ throw mapDriverError(error, "finalizeResumableUpload", ctx, { uploadId, filename: session.filename });
6794
+ }
6740
6795
  if (!info) throw fileNotFound("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename });
6741
6796
  return toFileInfo(info);
6742
6797
  }
@@ -6745,17 +6800,37 @@ async function abortResumableUpload(ctx, uploadId) {
6745
6800
  try {
6746
6801
  const uploads = ctx.db.collection(uploadsCollectionName(ctx.bucketName));
6747
6802
  const session = await uploads.findOne({ _id: uploadId });
6748
- if (!session || session.status === "aborted" || session.status === "finalized") return;
6803
+ if (session?.status === "finalized") return;
6804
+ await removeInvisibleChunks(ctx, uploadId, "abortResumableUpload");
6805
+ if (session) await uploads.deleteOne({ _id: uploadId });
6806
+ } catch (error) {
6807
+ throw mapDriverError(error, "abortResumableUpload", ctx, { uploadId });
6808
+ }
6809
+ }
6810
+ async function committedFile(ctx, uploadId, session) {
6811
+ const target = { uploadId, filename: session.filename };
6812
+ try {
6813
+ const doc = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne({ _id: uploadId });
6814
+ if (!doc) return null;
6815
+ const info = toFileInfo(doc);
6816
+ if (info.metadata?.sha256 !== session.sha256 || info.length !== session.length) {
6817
+ throw fileExists("finalizeResumableUpload", ctx, { id: uploadId, filename: session.filename, sha256: session.sha256 });
6818
+ }
6819
+ await ctx.db.collection(uploadsCollectionName(ctx.bucketName)).deleteOne({ _id: uploadId, status: "pending" });
6820
+ return info;
6821
+ } catch (error) {
6822
+ throw mapDriverError(error, "finalizeResumableUpload", ctx, target);
6823
+ }
6824
+ }
6825
+ async function removeInvisibleChunks(ctx, uploadId, operation) {
6826
+ try {
6749
6827
  const visible = await ctx.db.collection(filesCollectionName(ctx.bucketName)).findOne(
6750
6828
  { _id: uploadId },
6751
6829
  { projection: { _id: 1 } }
6752
6830
  );
6753
- if (!visible) {
6754
- await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
6755
- }
6756
- await uploads.deleteOne({ _id: uploadId });
6831
+ if (!visible) await ctx.db.collection(chunksCollectionName(ctx.bucketName)).deleteMany({ files_id: uploadId });
6757
6832
  } catch (error) {
6758
- throw mapDriverError(error, "abortResumableUpload", ctx, { uploadId });
6833
+ throw mapDriverError(error, operation, ctx, { uploadId });
6759
6834
  }
6760
6835
  }
6761
6836
  async function payloadBytes(data) {
@@ -6786,27 +6861,33 @@ async function requirePending(ctx, uploadId, operation) {
6786
6861
  if (!doc || doc.status !== "pending") throw fileNotFound(operation, ctx, { id: uploadId });
6787
6862
  return doc;
6788
6863
  }
6789
- async function contiguousNext(ctx, uploadId) {
6790
- const docs = await ctx.db.collection(chunksCollectionName(ctx.bucketName)).find({ files_id: uploadId }).sort({ n: 1 }).project({ n: 1 }).toArray();
6791
- let expected = 0;
6864
+ async function contiguousNext(ctx, uploadId, from) {
6865
+ const start = Number.isInteger(from) && from > 0 ? from : 0;
6866
+ let docs;
6867
+ try {
6868
+ docs = await ctx.db.collection(chunksCollectionName(ctx.bucketName)).find({ files_id: uploadId, n: { $gte: start } }).sort({ n: 1 }).project({ n: 1 }).toArray();
6869
+ } catch (error) {
6870
+ throw mapDriverError(error, "appendChunks", ctx, { uploadId });
6871
+ }
6872
+ let expected = start;
6792
6873
  for (const doc of docs) {
6793
6874
  if (doc.n !== expected) break;
6794
6875
  expected += 1;
6795
6876
  }
6796
6877
  return expected;
6797
6878
  }
6798
- async function assertStoredChunk(ctx, uploadId, n, slice) {
6879
+ async function assertStoredChunk(ctx, uploadId, n, slice, expectedNextChunk) {
6799
6880
  const existing = await ctx.db.collection(chunksCollectionName(ctx.bucketName)).findOne({ files_id: uploadId, n });
6800
6881
  if (!existing || !sameBytes(binaryToBuffer(existing.data, "appendChunks"), slice)) {
6801
- throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk: n, expectedNextChunk: n });
6882
+ throw resumeMismatch("appendChunks", ctx.bucketName, { uploadId, fromChunk: n, expectedNextChunk });
6802
6883
  }
6803
6884
  }
6804
- async function writeChunk(ctx, uploadId, n, slice) {
6885
+ async function writeChunk(ctx, uploadId, n, slice, expectedNextChunk) {
6805
6886
  try {
6806
6887
  await ctx.db.collection(chunksCollectionName(ctx.bucketName)).insertOne({ files_id: uploadId, n, data: slice });
6807
6888
  } catch (error) {
6808
6889
  if (!isDuplicateKey(error)) throw mapDriverError(error, "appendChunks", ctx, { uploadId });
6809
- await assertStoredChunk(ctx, uploadId, n, slice);
6890
+ await assertStoredChunk(ctx, uploadId, n, slice, expectedNextChunk);
6810
6891
  }
6811
6892
  }
6812
6893
 
@@ -6941,18 +7022,25 @@ var GridFSIndexLatch = class {
6941
7022
  this.bucketName = bucketName;
6942
7023
  this.inflight = null;
6943
7024
  this.ready = false;
7025
+ this.generation = 0;
6944
7026
  }
6945
7027
  ensure() {
6946
7028
  if (this.ready) return Promise.resolve();
6947
7029
  if (!this.inflight) {
7030
+ const generation = this.generation;
6948
7031
  this.inflight = this.create().then(() => {
6949
- this.ready = true;
7032
+ if (generation === this.generation) this.ready = true;
6950
7033
  }).finally(() => {
6951
7034
  this.inflight = null;
6952
7035
  });
6953
7036
  }
6954
7037
  return this.inflight;
6955
7038
  }
7039
+ /** Forget a completed ensure, e.g. after the bucket collections were dropped, so the next call recreates indexes. */
7040
+ reset() {
7041
+ this.ready = false;
7042
+ this.generation += 1;
7043
+ }
6956
7044
  async create() {
6957
7045
  const files = this.db.collection(filesCollectionName(this.bucketName));
6958
7046
  const chunks2 = this.db.collection(chunksCollectionName(this.bucketName));
@@ -10519,15 +10607,24 @@ function sleep2(ms, signal) {
10519
10607
  signal?.addEventListener("abort", onAbort, { once: true });
10520
10608
  });
10521
10609
  }
10610
+ var MAX_TIMER_MS = 2147483647;
10522
10611
  function linkSignal(userSignal, remainingMs) {
10523
10612
  const controller = new AbortController();
10524
10613
  let expired = false;
10525
10614
  const onUserAbort = () => controller.abort(userSignal?.reason);
10526
10615
  userSignal?.addEventListener("abort", onUserAbort, { once: true });
10527
- const timer = setTimeout(() => {
10528
- expired = true;
10529
- controller.abort(new Error("search index wait deadline reached"));
10530
- }, Math.max(0, remainingMs));
10616
+ let timer;
10617
+ const arm = (ms) => {
10618
+ timer = setTimeout(() => {
10619
+ if (ms > MAX_TIMER_MS) {
10620
+ arm(ms - MAX_TIMER_MS);
10621
+ return;
10622
+ }
10623
+ expired = true;
10624
+ controller.abort(new Error("search index wait deadline reached"));
10625
+ }, Math.min(ms, MAX_TIMER_MS));
10626
+ };
10627
+ arm(Math.max(0, remainingMs));
10531
10628
  return {
10532
10629
  signal: controller.signal,
10533
10630
  timedOut: () => expired,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monsqlize",
3
- "version": "3.5.1",
3
+ "version": "3.5.3",
4
4
  "description": "TypeScript production data runtime layer for MongoDB today, with cache, transactions, pools, models, sync, and observability",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/cjs/index.cjs",
@@ -21,6 +21,8 @@
21
21
  "./package.json": "./package.json"
22
22
  },
23
23
  "files": [
24
+ "changelogs/v3.5.3.md",
25
+ "changelogs/v3.5.2.md",
24
26
  "changelogs/v3.5.1.md",
25
27
  "changelogs/v3.5.0.md",
26
28
  "changelogs/v3.4.0.md",