monsqlize 3.5.2 → 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 +3 -2
- package/README.md +1 -1
- package/changelogs/v3.5.3.md +32 -0
- package/dist/cjs/cli/data-task.cjs +2 -1
- package/dist/cjs/index.cjs +136 -39
- package/dist/esm/index.mjs +136 -39
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
> Summary index — the 3.5.
|
|
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
4
|
> **Last updated**: 2026-11-02
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
-
|
|
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) |
|
|
16
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) |
|
|
17
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) |
|
|
18
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) |
|
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.
|
|
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,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.
|
|
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,7 @@ var package_default = {
|
|
|
52
52
|
"./package.json": "./package.json"
|
|
53
53
|
},
|
|
54
54
|
files: [
|
|
55
|
+
"changelogs/v3.5.3.md",
|
|
55
56
|
"changelogs/v3.5.2.md",
|
|
56
57
|
"changelogs/v3.5.1.md",
|
|
57
58
|
"changelogs/v3.5.0.md",
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6693
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6756
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
6808
|
-
let
|
|
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
|
|
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
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
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,
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6739
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
6791
|
-
let
|
|
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
|
|
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
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
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.
|
|
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,7 @@
|
|
|
21
21
|
"./package.json": "./package.json"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
+
"changelogs/v3.5.3.md",
|
|
24
25
|
"changelogs/v3.5.2.md",
|
|
25
26
|
"changelogs/v3.5.1.md",
|
|
26
27
|
"changelogs/v3.5.0.md",
|