@bsv/overlay 2.3.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/Open-BSV-License-4-standard-pre-2026.txt +28 -0
- package/README.md +171 -1
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/dist/cjs/mod.d.ts +4 -1
- package/dist/cjs/mod.d.ts.map +1 -1
- package/dist/cjs/mod.js +8 -1
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +13 -5
- package/dist/cjs/src/BASM.d.ts +6 -0
- package/dist/cjs/src/BASM.d.ts.map +1 -1
- package/dist/cjs/src/BASM.js +21 -8
- package/dist/cjs/src/BASM.js.map +1 -1
- package/dist/cjs/src/BASMRemote.d.ts +8 -1
- package/dist/cjs/src/BASMRemote.d.ts.map +1 -1
- package/dist/cjs/src/BASMRemote.js +179 -13
- package/dist/cjs/src/BASMRemote.js.map +1 -1
- package/dist/cjs/src/BASMValidation.d.ts +27 -0
- package/dist/cjs/src/BASMValidation.d.ts.map +1 -0
- package/dist/cjs/src/BASMValidation.js +108 -0
- package/dist/cjs/src/BASMValidation.js.map +1 -0
- package/dist/cjs/src/DiscoveryAdvertisementValidation.d.ts +11 -0
- package/dist/cjs/src/DiscoveryAdvertisementValidation.d.ts.map +1 -0
- package/dist/cjs/src/DiscoveryAdvertisementValidation.js +60 -0
- package/dist/cjs/src/DiscoveryAdvertisementValidation.js.map +1 -0
- package/dist/cjs/src/Engine.d.ts +38 -13
- package/dist/cjs/src/Engine.d.ts.map +1 -1
- package/dist/cjs/src/Engine.js +1735 -336
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/EngineAdmission.d.ts +70 -0
- package/dist/cjs/src/EngineAdmission.d.ts.map +1 -0
- package/dist/cjs/src/EngineAdmission.js +297 -0
- package/dist/cjs/src/EngineAdmission.js.map +1 -0
- package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts +4 -3
- package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +27 -43
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts +22 -16
- package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +206 -57
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/RemoteSecurity.d.ts +50 -0
- package/dist/cjs/src/RemoteSecurity.d.ts.map +1 -0
- package/dist/cjs/src/RemoteSecurity.js +301 -0
- package/dist/cjs/src/RemoteSecurity.js.map +1 -0
- package/dist/cjs/src/TopicManager.d.ts +13 -3
- package/dist/cjs/src/TopicManager.d.ts.map +1 -1
- package/dist/cjs/src/storage/AdmissionStorage.d.ts +185 -0
- package/dist/cjs/src/storage/AdmissionStorage.d.ts.map +1 -0
- package/dist/cjs/src/storage/AdmissionStorage.js +93 -0
- package/dist/cjs/src/storage/AdmissionStorage.js.map +1 -0
- package/dist/cjs/src/storage/RecoveryContract.d.ts +50 -0
- package/dist/cjs/src/storage/RecoveryContract.d.ts.map +1 -0
- package/dist/cjs/src/storage/RecoveryContract.js +35 -0
- package/dist/cjs/src/storage/RecoveryContract.js.map +1 -0
- package/dist/cjs/src/storage/Storage.d.ts +32 -19
- package/dist/cjs/src/storage/Storage.d.ts.map +1 -1
- package/dist/cjs/src/storage/Storage.js +5 -0
- package/dist/cjs/src/storage/Storage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.d.ts +8 -8
- package/dist/cjs/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +98 -61
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.js +5 -1
- package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts +4 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js +36 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts +5 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.js +16 -0
- package/dist/cjs/src/storage/knex/migrations/2026-09-20-001-spent-by.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.d.ts +18 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.js +229 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionPlan.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.d.ts +112 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.js +675 -0
- package/dist/cjs/src/storage/mongo/MongoAdmissionStorage.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoOverlayStorage.d.ts +71 -0
- package/dist/cjs/src/storage/mongo/MongoOverlayStorage.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoOverlayStorage.js +453 -0
- package/dist/cjs/src/storage/mongo/MongoOverlayStorage.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoPayloadStore.d.ts +134 -0
- package/dist/cjs/src/storage/mongo/MongoPayloadStore.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoPayloadStore.js +756 -0
- package/dist/cjs/src/storage/mongo/MongoPayloadStore.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoReadGuards.d.ts +44 -0
- package/dist/cjs/src/storage/mongo/MongoReadGuards.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoReadGuards.js +135 -0
- package/dist/cjs/src/storage/mongo/MongoReadGuards.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoSchema.d.ts +67 -0
- package/dist/cjs/src/storage/mongo/MongoSchema.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoSchema.js +743 -0
- package/dist/cjs/src/storage/mongo/MongoSchema.js.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoTransactionRunner.d.ts +69 -0
- package/dist/cjs/src/storage/mongo/MongoTransactionRunner.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo/MongoTransactionRunner.js +368 -0
- package/dist/cjs/src/storage/mongo/MongoTransactionRunner.js.map +1 -0
- package/dist/cjs/src/storage/mongo.d.ts +9 -0
- package/dist/cjs/src/storage/mongo.d.ts.map +1 -0
- package/dist/cjs/src/storage/mongo.js +25 -0
- package/dist/cjs/src/storage/mongo.js.map +1 -0
- package/dist/esm/mod.js +3 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/BASM.js +21 -8
- package/dist/esm/src/BASM.js.map +1 -1
- package/dist/esm/src/BASMRemote.js +176 -12
- package/dist/esm/src/BASMRemote.js.map +1 -1
- package/dist/esm/src/BASMValidation.js +95 -0
- package/dist/esm/src/BASMValidation.js.map +1 -0
- package/dist/esm/src/DiscoveryAdvertisementValidation.js +57 -0
- package/dist/esm/src/DiscoveryAdvertisementValidation.js.map +1 -0
- package/dist/esm/src/Engine.js +1720 -326
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/EngineAdmission.js +280 -0
- package/dist/esm/src/EngineAdmission.js.map +1 -0
- package/dist/esm/src/GASP/OverlayGASPRemote.js +27 -43
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +200 -56
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/RemoteSecurity.js +279 -0
- package/dist/esm/src/RemoteSecurity.js.map +1 -0
- package/dist/esm/src/storage/AdmissionStorage.js +85 -0
- package/dist/esm/src/storage/AdmissionStorage.js.map +1 -0
- package/dist/esm/src/storage/RecoveryContract.js +31 -0
- package/dist/esm/src/storage/RecoveryContract.js.map +1 -0
- package/dist/esm/src/storage/Storage.js +4 -1
- package/dist/esm/src/storage/Storage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +98 -61
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/all-migrations.js +5 -1
- package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js +32 -0
- package/dist/esm/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.js.map +1 -0
- package/dist/esm/src/storage/knex/migrations/2026-09-20-001-spent-by.js +12 -0
- package/dist/esm/src/storage/knex/migrations/2026-09-20-001-spent-by.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoAdmissionPlan.js +217 -0
- package/dist/esm/src/storage/mongo/MongoAdmissionPlan.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoAdmissionStorage.js +673 -0
- package/dist/esm/src/storage/mongo/MongoAdmissionStorage.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoOverlayStorage.js +451 -0
- package/dist/esm/src/storage/mongo/MongoOverlayStorage.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoPayloadStore.js +747 -0
- package/dist/esm/src/storage/mongo/MongoPayloadStore.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoReadGuards.js +129 -0
- package/dist/esm/src/storage/mongo/MongoReadGuards.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoSchema.js +730 -0
- package/dist/esm/src/storage/mongo/MongoSchema.js.map +1 -0
- package/dist/esm/src/storage/mongo/MongoTransactionRunner.js +371 -0
- package/dist/esm/src/storage/mongo/MongoTransactionRunner.js.map +1 -0
- package/dist/esm/src/storage/mongo.js +9 -0
- package/dist/esm/src/storage/mongo.js.map +1 -0
- package/dist/types/mod.d.ts +4 -1
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/BASM.d.ts +6 -0
- package/dist/types/src/BASM.d.ts.map +1 -1
- package/dist/types/src/BASMRemote.d.ts +8 -1
- package/dist/types/src/BASMRemote.d.ts.map +1 -1
- package/dist/types/src/BASMValidation.d.ts +27 -0
- package/dist/types/src/BASMValidation.d.ts.map +1 -0
- package/dist/types/src/DiscoveryAdvertisementValidation.d.ts +11 -0
- package/dist/types/src/DiscoveryAdvertisementValidation.d.ts.map +1 -0
- package/dist/types/src/Engine.d.ts +38 -13
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/EngineAdmission.d.ts +70 -0
- package/dist/types/src/EngineAdmission.d.ts.map +1 -0
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts +4 -3
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +22 -16
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/RemoteSecurity.d.ts +50 -0
- package/dist/types/src/RemoteSecurity.d.ts.map +1 -0
- package/dist/types/src/TopicManager.d.ts +13 -3
- package/dist/types/src/TopicManager.d.ts.map +1 -1
- package/dist/types/src/storage/AdmissionStorage.d.ts +185 -0
- package/dist/types/src/storage/AdmissionStorage.d.ts.map +1 -0
- package/dist/types/src/storage/RecoveryContract.d.ts +50 -0
- package/dist/types/src/storage/RecoveryContract.d.ts.map +1 -0
- package/dist/types/src/storage/Storage.d.ts +32 -19
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +8 -8
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2026-09-17-001-topical-uniqueness.d.ts.map +1 -0
- package/dist/types/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts +5 -0
- package/dist/types/src/storage/knex/migrations/2026-09-20-001-spent-by.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoAdmissionPlan.d.ts +18 -0
- package/dist/types/src/storage/mongo/MongoAdmissionPlan.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoAdmissionStorage.d.ts +112 -0
- package/dist/types/src/storage/mongo/MongoAdmissionStorage.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoOverlayStorage.d.ts +71 -0
- package/dist/types/src/storage/mongo/MongoOverlayStorage.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoPayloadStore.d.ts +134 -0
- package/dist/types/src/storage/mongo/MongoPayloadStore.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoReadGuards.d.ts +44 -0
- package/dist/types/src/storage/mongo/MongoReadGuards.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoSchema.d.ts +67 -0
- package/dist/types/src/storage/mongo/MongoSchema.d.ts.map +1 -0
- package/dist/types/src/storage/mongo/MongoTransactionRunner.d.ts +69 -0
- package/dist/types/src/storage/mongo/MongoTransactionRunner.d.ts.map +1 -0
- package/dist/types/src/storage/mongo.d.ts +9 -0
- package/dist/types/src/storage/mongo.d.ts.map +1 -0
- package/package.json +14 -6
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoPayloadStore = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_events_1 = require("node:events");
|
|
6
|
+
const promises_1 = require("node:timers/promises");
|
|
7
|
+
const mongodb_1 = require("mongodb");
|
|
8
|
+
const MongoSchema_js_1 = require("./MongoSchema.js");
|
|
9
|
+
const MAX_UINT64 = (BigInt(1) << BigInt(64)) - BigInt(1);
|
|
10
|
+
const HEX_256 = /^[0-9a-f]{64}$/;
|
|
11
|
+
function duplicateKey(error) {
|
|
12
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === 11000;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Content-addressed blob primitives for the Mongo adapter. The store deliberately
|
|
16
|
+
* has no Engine/Storage methods: S03 combines its reference operations with the
|
|
17
|
+
* owner record in an admission transaction.
|
|
18
|
+
*/
|
|
19
|
+
class MongoPayloadStore {
|
|
20
|
+
constructor(db, scope, options = {}) {
|
|
21
|
+
var _a, _b, _c, _d, _e;
|
|
22
|
+
this.db = db;
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.writeConcern = { w: 'majority', j: true };
|
|
25
|
+
this.scope = { network: scope.network, genesisHash: scope.genesisHash, nodeId: scope.nodeId };
|
|
26
|
+
this.inlineCeiling = (_a = options.inlineCeilingBytes) !== null && _a !== void 0 ? _a : 256 * 1024;
|
|
27
|
+
this.gridFsChunkBytes = (_b = options.gridFsChunkBytes) !== null && _b !== void 0 ? _b : 255 * 1024;
|
|
28
|
+
this.uploadLeaseMs = (_c = options.uploadLeaseMs) !== null && _c !== void 0 ? _c : 60000;
|
|
29
|
+
this.maxPayloadBytes = (_d = options.maxPayloadBytes) !== null && _d !== void 0 ? _d : BigInt(64 * 1024 * 1024);
|
|
30
|
+
this.now = (_e = options.now) !== null && _e !== void 0 ? _e : (() => new Date());
|
|
31
|
+
if (!Number.isSafeInteger(this.inlineCeiling) ||
|
|
32
|
+
this.inlineCeiling < 1 ||
|
|
33
|
+
this.inlineCeiling > 1024 * 1024)
|
|
34
|
+
throw new Error('Invalid Mongo payload inline ceiling');
|
|
35
|
+
if (!Number.isSafeInteger(this.gridFsChunkBytes) ||
|
|
36
|
+
this.gridFsChunkBytes < 1 ||
|
|
37
|
+
this.gridFsChunkBytes > 255 * 1024)
|
|
38
|
+
throw new Error('Invalid Mongo GridFS chunk size');
|
|
39
|
+
if (!Number.isSafeInteger(this.uploadLeaseMs) || this.uploadLeaseMs < 1)
|
|
40
|
+
throw new Error('Invalid Mongo upload lease');
|
|
41
|
+
if (this.maxPayloadBytes < BigInt(1) || this.maxPayloadBytes > MAX_UINT64)
|
|
42
|
+
throw new Error('Invalid Mongo payload byte bound');
|
|
43
|
+
this.bucket = new mongodb_1.GridFSBucket(db, {
|
|
44
|
+
bucketName: MongoSchema_js_1.MongoGridFsBucketName,
|
|
45
|
+
chunkSizeBytes: this.gridFsChunkBytes,
|
|
46
|
+
writeConcern: this.writeConcern
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async read(payload, range) {
|
|
50
|
+
const payloadId = this.payloadId(this.scope, payload);
|
|
51
|
+
const record = await this.db
|
|
52
|
+
.collection(MongoSchema_js_1.MongoCollectionNames.payloads)
|
|
53
|
+
.findOne({
|
|
54
|
+
_id: payloadId,
|
|
55
|
+
state: 'ready'
|
|
56
|
+
});
|
|
57
|
+
if (record === null)
|
|
58
|
+
throw new Error('Mongo payload is not ready');
|
|
59
|
+
const total = BigInt((0, MongoSchema_js_1.decodeMongoUint64)(record.byteLength));
|
|
60
|
+
const offset = range === undefined ? BigInt(0) : BigInt(range.offset);
|
|
61
|
+
const length = range === undefined ? total : BigInt(range.byteLength);
|
|
62
|
+
if (offset < BigInt(0) || length < BigInt(0) || offset + length > total)
|
|
63
|
+
throw new Error('Mongo payload range is invalid');
|
|
64
|
+
if (offset + length > BigInt(Number.MAX_SAFE_INTEGER))
|
|
65
|
+
throw new Error('Mongo payload range exceeds safe integer');
|
|
66
|
+
const bytes = await this.readBytes(record);
|
|
67
|
+
return bytes.subarray(Number(offset), Number(offset + length));
|
|
68
|
+
}
|
|
69
|
+
async publish(input) {
|
|
70
|
+
input = { ...input };
|
|
71
|
+
this.validateInput(input);
|
|
72
|
+
const payloadId = (0, MongoSchema_js_1.mongoRecordKey)((0, MongoSchema_js_1.mongoChainKey)(this.scope), input.kind, input.digest);
|
|
73
|
+
const payloads = this.db.collection(MongoSchema_js_1.MongoCollectionNames.payloads);
|
|
74
|
+
const reserved = await this.reserveUpload(payloads, payloadId, input);
|
|
75
|
+
if (reserved.ready)
|
|
76
|
+
return this.refFor(input, reserved.record);
|
|
77
|
+
return await this.completeUpload(payloads, input, payloadId, reserved.record);
|
|
78
|
+
}
|
|
79
|
+
/** Must be called in the same transaction that creates the referenced owner record. */
|
|
80
|
+
async addReference(session, reference, operation = {}) {
|
|
81
|
+
this.assertOperation(operation);
|
|
82
|
+
this.assertTransaction(session);
|
|
83
|
+
this.validateReference(reference);
|
|
84
|
+
const payloadId = this.payloadId(reference.scope, reference.payload);
|
|
85
|
+
const refs = this.db.collection(MongoSchema_js_1.MongoCollectionNames.payloadReferences);
|
|
86
|
+
const payloads = this.db.collection(MongoSchema_js_1.MongoCollectionNames.payloads);
|
|
87
|
+
const refId = (0, MongoSchema_js_1.mongoRecordKey)((0, MongoSchema_js_1.mongoNodeKey)(reference.scope), reference.ownerKind, reference.ownerId, reference.slot);
|
|
88
|
+
const existing = await refs.findOne({ _id: refId }, { session, timeoutMS: operation.timeoutMS });
|
|
89
|
+
if (existing !== null) {
|
|
90
|
+
if (existing.payloadId !== payloadId)
|
|
91
|
+
throw new Error('Mongo payload reference slot already names different content');
|
|
92
|
+
// An idempotent reference still needs a live ready payload. The row may
|
|
93
|
+
// have been reclaimed since the reference was written. This CAS also
|
|
94
|
+
// serializes the pin refresh below against a concurrent GC claim.
|
|
95
|
+
const guarded = await payloads.updateOne({ _id: payloadId, state: 'ready' }, { $set: { updatedAt: this.now() } }, { session, timeoutMS: operation.timeoutMS });
|
|
96
|
+
if (guarded.matchedCount !== 1)
|
|
97
|
+
throw new Error('Mongo payload is not ready for reference');
|
|
98
|
+
if (reference.ownerKind === 'pin')
|
|
99
|
+
await this.refreshPinExpiry(refs, refId, reference, existing, session, operation);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// This conditional write is the guard; a snapshot read followed by an insert is unsafe.
|
|
103
|
+
const guarded = await payloads.updateOne({ _id: payloadId, state: 'ready' }, { $set: { updatedAt: this.now() } }, { session, timeoutMS: operation.timeoutMS });
|
|
104
|
+
if (guarded.matchedCount !== 1)
|
|
105
|
+
throw new Error('Mongo payload is not ready for reference');
|
|
106
|
+
await refs.insertOne({
|
|
107
|
+
_id: refId,
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
network: reference.scope.network,
|
|
110
|
+
genesisHash: reference.scope.genesisHash,
|
|
111
|
+
nodeId: reference.scope.nodeId,
|
|
112
|
+
payloadId,
|
|
113
|
+
ownerKind: reference.ownerKind,
|
|
114
|
+
ownerId: reference.ownerId,
|
|
115
|
+
slot: reference.slot,
|
|
116
|
+
createdAt: this.now(),
|
|
117
|
+
updatedAt: this.now(),
|
|
118
|
+
...(reference.expiresAt === undefined ? {} : { expiresAt: reference.expiresAt })
|
|
119
|
+
}, { session, timeoutMS: operation.timeoutMS });
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Refreshes an existing pin's expiry when the same slot is re-added: an
|
|
123
|
+
* expired pin is reactivated with the caller's new expiry, and a still-live
|
|
124
|
+
* pin may only be extended, never silently shortened. Called only after the
|
|
125
|
+
* caller's {_id: payloadId, state: 'ready'} CAS has already matched in this
|
|
126
|
+
* transaction, so this stays serialized against a concurrent GC claim.
|
|
127
|
+
*/
|
|
128
|
+
async refreshPinExpiry(refs, refId, reference, existing, session, operation) {
|
|
129
|
+
const now = this.now();
|
|
130
|
+
// validateReference requires expiresAt for every pin reference.
|
|
131
|
+
const requested = reference.expiresAt;
|
|
132
|
+
const current = existing.expiresAt;
|
|
133
|
+
const currentExpired = current === undefined || current <= now;
|
|
134
|
+
if (!currentExpired && requested < current)
|
|
135
|
+
throw new Error('Mongo payload pin re-add must not shorten an unexpired expiry');
|
|
136
|
+
if (currentExpired || requested.getTime() !== current.getTime()) {
|
|
137
|
+
await refs.updateOne({ _id: refId }, { $set: { expiresAt: requested, updatedAt: now } }, { session, timeoutMS: operation.timeoutMS });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Must be called in the transaction that releases the owner record's payload obligation. */
|
|
141
|
+
async releaseReference(session, reference, operation = {}) {
|
|
142
|
+
this.assertOperation(operation);
|
|
143
|
+
this.assertTransaction(session);
|
|
144
|
+
this.validateReference(reference);
|
|
145
|
+
const refId = (0, MongoSchema_js_1.mongoRecordKey)((0, MongoSchema_js_1.mongoNodeKey)(reference.scope), reference.ownerKind, reference.ownerId, reference.slot);
|
|
146
|
+
const payloadId = this.payloadId(reference.scope, reference.payload);
|
|
147
|
+
const deleted = await this.db
|
|
148
|
+
.collection(MongoSchema_js_1.MongoCollectionNames.payloadReferences)
|
|
149
|
+
.deleteOne({ _id: refId, payloadId }, { session, timeoutMS: operation.timeoutMS });
|
|
150
|
+
if (deleted.deletedCount !== 1)
|
|
151
|
+
throw new Error('Mongo payload reference does not exist');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Creates one bounded manifest component and its durable pin in one caller
|
|
155
|
+
* transaction. S03 can combine this with its manifest owner record; this
|
|
156
|
+
* method does not parse or reconstruct BEEF.
|
|
157
|
+
*/
|
|
158
|
+
async addManifestComponent(session, component) {
|
|
159
|
+
this.assertTransaction(session);
|
|
160
|
+
if (!this.isUint64(component.ordinal) || component.manifestId.length === 0)
|
|
161
|
+
throw new Error('Invalid Mongo manifest component');
|
|
162
|
+
if (component.kind !== component.payload.kind)
|
|
163
|
+
throw new Error('Mongo manifest component kind mismatch');
|
|
164
|
+
const scope = this.scope;
|
|
165
|
+
const ownerId = component.manifestId;
|
|
166
|
+
const slot = component.ordinal;
|
|
167
|
+
await this.addReference(session, {
|
|
168
|
+
scope,
|
|
169
|
+
payload: component.payload,
|
|
170
|
+
ownerKind: 'manifest',
|
|
171
|
+
ownerId,
|
|
172
|
+
slot
|
|
173
|
+
});
|
|
174
|
+
const payloadId = this.payloadId(scope, component.payload);
|
|
175
|
+
const id = (0, MongoSchema_js_1.mongoRecordKey)((0, MongoSchema_js_1.mongoNodeKey)(scope), component.manifestId, component.ordinal);
|
|
176
|
+
const existing = await this.db
|
|
177
|
+
.collection(MongoSchema_js_1.MongoCollectionNames.manifestComponents)
|
|
178
|
+
.findOne({ _id: id }, { session });
|
|
179
|
+
if (existing !== null) {
|
|
180
|
+
if (existing.payloadId !== payloadId || existing.kind !== component.kind)
|
|
181
|
+
throw new Error('Mongo manifest ordinal already names different content');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const now = this.now();
|
|
185
|
+
await this.db
|
|
186
|
+
.collection(MongoSchema_js_1.MongoCollectionNames.manifestComponents)
|
|
187
|
+
.insertOne({
|
|
188
|
+
_id: id,
|
|
189
|
+
schemaVersion: 1,
|
|
190
|
+
network: scope.network,
|
|
191
|
+
genesisHash: scope.genesisHash,
|
|
192
|
+
nodeId: scope.nodeId,
|
|
193
|
+
manifestId: component.manifestId,
|
|
194
|
+
ordinal: (0, MongoSchema_js_1.encodeMongoUint64)(component.ordinal),
|
|
195
|
+
payloadId,
|
|
196
|
+
kind: component.kind,
|
|
197
|
+
createdAt: now,
|
|
198
|
+
updatedAt: now
|
|
199
|
+
}, { session });
|
|
200
|
+
}
|
|
201
|
+
/** Claims logical deletion inside a caller transaction; physical deletion resumes outside it. */
|
|
202
|
+
async claimGarbage(session, payload, operation = {}) {
|
|
203
|
+
var _a, _b;
|
|
204
|
+
this.assertOperation(operation);
|
|
205
|
+
this.assertTransaction(session);
|
|
206
|
+
const payloadId = this.payloadId(this.scope, payload);
|
|
207
|
+
const refs = this.db.collection(MongoSchema_js_1.MongoCollectionNames.payloadReferences);
|
|
208
|
+
const liveReferences = await refs.countDocuments({
|
|
209
|
+
payloadId,
|
|
210
|
+
$or: [{ ownerKind: { $ne: 'pin' } }, { $expr: { $gt: ['$expiresAt', '$$NOW'] } }]
|
|
211
|
+
}, { session, timeoutMS: operation.timeoutMS });
|
|
212
|
+
if (liveReferences !== 0)
|
|
213
|
+
return false;
|
|
214
|
+
const claimed = await this.db
|
|
215
|
+
.collection(MongoSchema_js_1.MongoCollectionNames.payloads)
|
|
216
|
+
.findOneAndUpdate({ _id: payloadId, state: 'ready' }, [
|
|
217
|
+
{
|
|
218
|
+
$set: {
|
|
219
|
+
state: 'deleting',
|
|
220
|
+
guard: (0, node_crypto_1.randomUUID)(),
|
|
221
|
+
updatedAt: '$$NOW',
|
|
222
|
+
leaseUntil: {
|
|
223
|
+
$dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.uploadLeaseMs }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
], { session, timeoutMS: operation.timeoutMS, returnDocument: 'after' });
|
|
228
|
+
if (claimed === null)
|
|
229
|
+
return false;
|
|
230
|
+
// Every pin row still present here was already confirmed expired by the
|
|
231
|
+
// liveReferences count above; delete it in this same transaction so the
|
|
232
|
+
// unique slot cannot outlive the payload it named and short-circuit a
|
|
233
|
+
// future addReference on that slot.
|
|
234
|
+
await refs.deleteMany({ payloadId, ownerKind: 'pin', $expr: { $lte: ['$expiresAt', '$$NOW'] } }, { session, timeoutMS: operation.timeoutMS });
|
|
235
|
+
await ((_b = (_a = this.options.hooks) === null || _a === void 0 ? void 0 : _a.afterDeleteClaim) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
/** Safe to retry after process death. It only deletes the file named by the deleting record. */
|
|
239
|
+
async finishGarbage(payload) {
|
|
240
|
+
const payloadId = this.payloadId(this.scope, payload);
|
|
241
|
+
const payloads = this.db.collection(MongoSchema_js_1.MongoCollectionNames.payloads);
|
|
242
|
+
const record = await payloads.findOne({ _id: payloadId, state: 'deleting' });
|
|
243
|
+
if (record === null)
|
|
244
|
+
return false;
|
|
245
|
+
if (record.fileId !== undefined) {
|
|
246
|
+
const owned = await this.db.collection(`${MongoSchema_js_1.MongoGridFsBucketName}.files`).findOne({
|
|
247
|
+
_id: record.fileId,
|
|
248
|
+
'metadata.ownerId': record.ownerId,
|
|
249
|
+
'metadata.fencingToken': record.fencingToken
|
|
250
|
+
});
|
|
251
|
+
if (owned !== null) {
|
|
252
|
+
await this.bucket.delete(record.fileId).catch(error => {
|
|
253
|
+
// The driver reports an already-removed file as MongoRuntimeError
|
|
254
|
+
// "File not found for id <id>"; a concurrent finisher got there first.
|
|
255
|
+
if (!(error instanceof Error) || !/File not found/i.test(error.message))
|
|
256
|
+
throw error;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const result = await payloads.updateOne({ _id: payloadId, state: 'deleting', guard: record.guard }, {
|
|
261
|
+
$set: {
|
|
262
|
+
state: 'deleted',
|
|
263
|
+
updatedAt: this.now(),
|
|
264
|
+
...(record.fileId === undefined ? {} : { retiredFileId: record.fileId })
|
|
265
|
+
},
|
|
266
|
+
$unset: { inlineData: '', fileId: '' }
|
|
267
|
+
}, { writeConcern: this.writeConcern });
|
|
268
|
+
return result.modifiedCount === 1;
|
|
269
|
+
}
|
|
270
|
+
/** Recovery may retain content; it never guesses that a missing external owner is disposable. */
|
|
271
|
+
async recoverUploads() {
|
|
272
|
+
const payloads = this.db.collection(MongoSchema_js_1.MongoCollectionNames.payloads);
|
|
273
|
+
const stale = await payloads
|
|
274
|
+
.find({
|
|
275
|
+
network: this.scope.network,
|
|
276
|
+
genesisHash: this.scope.genesisHash,
|
|
277
|
+
state: 'uploading',
|
|
278
|
+
$expr: { $lte: ['$leaseUntil', '$$NOW'] }
|
|
279
|
+
})
|
|
280
|
+
.toArray();
|
|
281
|
+
for (const record of stale)
|
|
282
|
+
await this.recoverStaleUpload(payloads, record);
|
|
283
|
+
const deleting = await payloads
|
|
284
|
+
.find({ network: this.scope.network, genesisHash: this.scope.genesisHash, state: 'deleting' })
|
|
285
|
+
.toArray();
|
|
286
|
+
for (const record of deleting)
|
|
287
|
+
await this.finishGarbage({ kind: record.kind, digest: record.digest });
|
|
288
|
+
}
|
|
289
|
+
async completeUpload(payloads, input, payloadId, reserved) {
|
|
290
|
+
var _a, _b;
|
|
291
|
+
const ownerId = reserved.ownerId;
|
|
292
|
+
const fence = reserved.fencingToken;
|
|
293
|
+
let fileId;
|
|
294
|
+
try {
|
|
295
|
+
const streamed = await this.streamInput(input, payloadId, ownerId, fence);
|
|
296
|
+
this.throwIfAborted(input.signal);
|
|
297
|
+
fileId = streamed.fileId;
|
|
298
|
+
this.assertStreamedPayload(input, streamed);
|
|
299
|
+
const binary = streamed.inline === undefined ? undefined : new mongodb_1.Binary(Buffer.concat(streamed.inline));
|
|
300
|
+
if (binary !== undefined && !this.fitsInline(payloadId, input, binary, this.now()))
|
|
301
|
+
throw new Error('Mongo payload inline BSON document exceeds safety ceiling');
|
|
302
|
+
if (fileId !== undefined)
|
|
303
|
+
await this.stageGridFsUpload(payloads, input, payloadId, ownerId, fence, fileId);
|
|
304
|
+
await ((_b = (_a = this.options.hooks) === null || _a === void 0 ? void 0 : _a.beforeReadyCas) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
305
|
+
this.throwIfAborted(input.signal);
|
|
306
|
+
const ready = await payloads.findOneAndUpdate({
|
|
307
|
+
_id: payloadId,
|
|
308
|
+
state: 'uploading',
|
|
309
|
+
ownerId,
|
|
310
|
+
guard: ownerId,
|
|
311
|
+
$expr: { $gt: ['$leaseUntil', '$$NOW'] }
|
|
312
|
+
}, {
|
|
313
|
+
$set: {
|
|
314
|
+
state: 'ready',
|
|
315
|
+
updatedAt: this.now(),
|
|
316
|
+
...(fileId === undefined ? { inlineData: binary } : { fileId })
|
|
317
|
+
},
|
|
318
|
+
$unset: { uploadId: '' }
|
|
319
|
+
}, { returnDocument: 'after', writeConcern: this.writeConcern });
|
|
320
|
+
if (ready === null)
|
|
321
|
+
throw new Error('Mongo payload upload lease was fenced before publication');
|
|
322
|
+
return this.refFor(input, ready);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
await this.abandonUpload(payloads, payloadId, ownerId, fence, fileId, error);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
assertStreamedPayload(input, streamed) {
|
|
330
|
+
if (streamed.digest !== input.digest || streamed.length !== BigInt(input.byteLength))
|
|
331
|
+
throw new Error('Mongo payload digest or declared length mismatch');
|
|
332
|
+
if (input.kind === 'raw-transaction' && input.txid !== undefined && input.txid !== streamed.txid)
|
|
333
|
+
throw new Error('Mongo raw transaction id does not match bytes');
|
|
334
|
+
}
|
|
335
|
+
async stageGridFsUpload(payloads, input, payloadId, ownerId, fence, fileId) {
|
|
336
|
+
var _a, _b, _c, _d;
|
|
337
|
+
const staged = await payloads.updateOne({ _id: payloadId, state: 'uploading', ownerId, guard: ownerId }, { $set: { fileId, updatedAt: this.now() } }, { writeConcern: this.writeConcern });
|
|
338
|
+
if (staged.matchedCount !== 1)
|
|
339
|
+
throw new Error('Mongo payload upload lease was fenced before staging');
|
|
340
|
+
await this.verifyGridFs(fileId, input, input.signal);
|
|
341
|
+
this.throwIfAborted(input.signal);
|
|
342
|
+
await ((_b = (_a = this.options.hooks) === null || _a === void 0 ? void 0 : _a.afterGridFsUploaded) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
343
|
+
this.throwIfAborted(input.signal);
|
|
344
|
+
const published = await this.db.collection(`${MongoSchema_js_1.MongoGridFsBucketName}.files`).updateOne({
|
|
345
|
+
_id: fileId,
|
|
346
|
+
'metadata.ownerId': ownerId,
|
|
347
|
+
'metadata.fencingToken': fence,
|
|
348
|
+
'metadata.state': 'staged'
|
|
349
|
+
}, { $set: { 'metadata.state': 'published' } }, { writeConcern: this.writeConcern });
|
|
350
|
+
if (published.matchedCount !== 1)
|
|
351
|
+
throw new Error('Mongo GridFS staged file was lost');
|
|
352
|
+
await ((_d = (_c = this.options.hooks) === null || _c === void 0 ? void 0 : _c.afterGridFsPublished) === null || _d === void 0 ? void 0 : _d.call(_c));
|
|
353
|
+
}
|
|
354
|
+
async abandonUpload(payloads, payloadId, ownerId, fence, fileId, error) {
|
|
355
|
+
// Never remove a file by payload identity: a newer fenced owner may have published it.
|
|
356
|
+
if (fileId !== undefined) {
|
|
357
|
+
const published = await this.db.collection(`${MongoSchema_js_1.MongoGridFsBucketName}.files`).findOne({
|
|
358
|
+
_id: fileId,
|
|
359
|
+
'metadata.ownerId': ownerId,
|
|
360
|
+
'metadata.fencingToken': fence,
|
|
361
|
+
'metadata.state': 'published'
|
|
362
|
+
});
|
|
363
|
+
if (published !== null)
|
|
364
|
+
throw error;
|
|
365
|
+
await this.retireOwnedFile(fileId, ownerId);
|
|
366
|
+
}
|
|
367
|
+
await payloads.updateOne({ _id: payloadId, state: 'uploading', ownerId, guard: ownerId }, {
|
|
368
|
+
$set: {
|
|
369
|
+
state: 'deleted',
|
|
370
|
+
updatedAt: this.now(),
|
|
371
|
+
...(fileId === undefined ? {} : { retiredFileId: fileId })
|
|
372
|
+
},
|
|
373
|
+
$unset: { inlineData: '', fileId: '' }
|
|
374
|
+
}, { writeConcern: this.writeConcern });
|
|
375
|
+
}
|
|
376
|
+
async recoverStaleUpload(payloads, record) {
|
|
377
|
+
if (record.fileId !== undefined) {
|
|
378
|
+
try {
|
|
379
|
+
await this.verifyGridFs(record.fileId, {
|
|
380
|
+
digest: record.digest,
|
|
381
|
+
byteLength: BigInt(record.byteLength).toString()
|
|
382
|
+
});
|
|
383
|
+
await this.db.collection(`${MongoSchema_js_1.MongoGridFsBucketName}.files`).updateOne({
|
|
384
|
+
_id: record.fileId,
|
|
385
|
+
'metadata.ownerId': record.ownerId,
|
|
386
|
+
'metadata.fencingToken': record.fencingToken
|
|
387
|
+
}, { $set: { 'metadata.state': 'published' } }, { writeConcern: this.writeConcern });
|
|
388
|
+
const recovered = await payloads.updateOne({ _id: record._id, state: 'uploading', ownerId: record.ownerId, guard: record.guard }, { $set: { state: 'ready', updatedAt: this.now() }, $unset: { uploadId: '' } }, { writeConcern: this.writeConcern });
|
|
389
|
+
if (recovered.modifiedCount === 1)
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
/* an incomplete upload is retired below */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const current = await payloads.findOne({ _id: record._id });
|
|
397
|
+
if (current === null ||
|
|
398
|
+
current.state !== 'uploading' ||
|
|
399
|
+
current.ownerId !== record.ownerId ||
|
|
400
|
+
current.guard !== record.guard) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (record.fileId !== undefined)
|
|
404
|
+
await this.retireOwnedFile(record.fileId, record.ownerId, record.fencingToken);
|
|
405
|
+
await payloads.updateOne({ _id: record._id, state: 'uploading', ownerId: record.ownerId, guard: record.guard }, {
|
|
406
|
+
$set: {
|
|
407
|
+
state: 'deleted',
|
|
408
|
+
updatedAt: this.now(),
|
|
409
|
+
...(record.fileId === undefined ? {} : { retiredFileId: record.fileId })
|
|
410
|
+
},
|
|
411
|
+
$unset: { inlineData: '', fileId: '' }
|
|
412
|
+
}, { writeConcern: this.writeConcern });
|
|
413
|
+
}
|
|
414
|
+
async reserveUpload(payloads, payloadId, input) {
|
|
415
|
+
const deadline = Date.now() + this.uploadLeaseMs;
|
|
416
|
+
while (true) {
|
|
417
|
+
this.throwIfAborted(input.signal);
|
|
418
|
+
const observed = await payloads.findOne({ _id: payloadId });
|
|
419
|
+
if ((observed === null || observed === void 0 ? void 0 : observed.state) === 'ready')
|
|
420
|
+
return { ready: true, record: observed };
|
|
421
|
+
const claimed = await this.tryClaimUpload(payloads, payloadId, input, observed);
|
|
422
|
+
if (claimed !== undefined)
|
|
423
|
+
return claimed;
|
|
424
|
+
const ready = await payloads.findOne({ _id: payloadId, state: 'ready' });
|
|
425
|
+
if (ready !== null)
|
|
426
|
+
return { ready: true, record: ready };
|
|
427
|
+
if (Date.now() > deadline)
|
|
428
|
+
throw new Error('Mongo payload upload reservation was lost');
|
|
429
|
+
await (0, promises_1.setTimeout)(50);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async tryClaimUpload(payloads, payloadId, input, observed) {
|
|
433
|
+
var _a;
|
|
434
|
+
const ownerId = (0, node_crypto_1.randomUUID)();
|
|
435
|
+
const fence = (0, MongoSchema_js_1.encodeMongoUint64)((BigInt((_a = observed === null || observed === void 0 ? void 0 : observed.fencingToken) !== null && _a !== void 0 ? _a : '00000000000000000000') + BigInt(1)).toString());
|
|
436
|
+
try {
|
|
437
|
+
const claimed = await payloads.findOneAndUpdate(observed === null
|
|
438
|
+
? { _id: payloadId, state: { $exists: false } }
|
|
439
|
+
: {
|
|
440
|
+
_id: payloadId,
|
|
441
|
+
state: observed.state,
|
|
442
|
+
guard: observed.guard,
|
|
443
|
+
fencingToken: observed.fencingToken,
|
|
444
|
+
$or: [{ state: 'deleted' }, { $expr: { $lte: ['$leaseUntil', '$$NOW'] } }]
|
|
445
|
+
}, [
|
|
446
|
+
{
|
|
447
|
+
$set: {
|
|
448
|
+
schemaVersion: { $ifNull: ['$schemaVersion', 1] },
|
|
449
|
+
network: { $ifNull: ['$network', this.scope.network] },
|
|
450
|
+
genesisHash: { $ifNull: ['$genesisHash', this.scope.genesisHash] },
|
|
451
|
+
kind: { $ifNull: ['$kind', input.kind] },
|
|
452
|
+
digest: { $ifNull: ['$digest', input.digest] },
|
|
453
|
+
byteLength: {
|
|
454
|
+
$cond: [
|
|
455
|
+
{ $eq: ['$state', 'deleted'] },
|
|
456
|
+
(0, MongoSchema_js_1.encodeMongoUint64)(input.byteLength),
|
|
457
|
+
{ $ifNull: ['$byteLength', (0, MongoSchema_js_1.encodeMongoUint64)(input.byteLength)] }
|
|
458
|
+
]
|
|
459
|
+
},
|
|
460
|
+
createdAt: { $ifNull: ['$createdAt', '$$NOW'] },
|
|
461
|
+
state: 'uploading',
|
|
462
|
+
guard: ownerId,
|
|
463
|
+
ownerNodeId: this.scope.nodeId,
|
|
464
|
+
ownerId,
|
|
465
|
+
fencingToken: fence,
|
|
466
|
+
leaseUntil: {
|
|
467
|
+
$dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.uploadLeaseMs }
|
|
468
|
+
},
|
|
469
|
+
updatedAt: '$$NOW',
|
|
470
|
+
uploadId: ownerId
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
{ $unset: ['inlineData', 'fileId'] }
|
|
474
|
+
], { upsert: observed === null, returnDocument: 'after', writeConcern: this.writeConcern });
|
|
475
|
+
if (claimed !== null && claimed.ownerId === ownerId && claimed.guard === ownerId) {
|
|
476
|
+
if ((observed === null || observed === void 0 ? void 0 : observed.fileId) !== undefined)
|
|
477
|
+
await this.retireOwnedFile(observed.fileId, observed.ownerId, observed.fencingToken);
|
|
478
|
+
return { ready: false, record: claimed };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
if (!duplicateKey(error))
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
async streamInput(input, payloadId, ownerId, fencingToken) {
|
|
488
|
+
var _a, _b;
|
|
489
|
+
const stream = {
|
|
490
|
+
hash: (0, node_crypto_1.createHash)('sha256'),
|
|
491
|
+
length: BigInt(0),
|
|
492
|
+
inline: [],
|
|
493
|
+
upload: undefined,
|
|
494
|
+
fileId: undefined
|
|
495
|
+
};
|
|
496
|
+
const iterator = input.bytes[Symbol.asyncIterator]();
|
|
497
|
+
try {
|
|
498
|
+
while (true) {
|
|
499
|
+
const next = await this.nextWithAbort(iterator, input.signal);
|
|
500
|
+
if (next.done)
|
|
501
|
+
break;
|
|
502
|
+
await this.consumePayloadChunk(input, payloadId, ownerId, fencingToken, next.value, stream);
|
|
503
|
+
}
|
|
504
|
+
await this.finishPayloadUpload(stream.upload, input.signal);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
(_a = stream.upload) === null || _a === void 0 ? void 0 : _a.destroy(error instanceof Error ? error : new Error('Mongo payload stream failed'));
|
|
508
|
+
await ((_b = iterator.return) === null || _b === void 0 ? void 0 : _b.call(iterator).catch(() => undefined));
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
const digest = stream.hash.digest();
|
|
512
|
+
const result = {
|
|
513
|
+
digest: digest.toString('hex'),
|
|
514
|
+
length: stream.length,
|
|
515
|
+
inline: stream.inline,
|
|
516
|
+
fileId: stream.fileId
|
|
517
|
+
};
|
|
518
|
+
if (input.kind === 'raw-transaction')
|
|
519
|
+
result.txid = (0, node_crypto_1.createHash)('sha256').update(digest).digest().reverse().toString('hex');
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
async consumePayloadChunk(input, payloadId, ownerId, fencingToken, chunk, stream) {
|
|
523
|
+
var _a;
|
|
524
|
+
var _b;
|
|
525
|
+
if ((_a = input.signal) === null || _a === void 0 ? void 0 : _a.aborted)
|
|
526
|
+
throw (_b = input.signal.reason) !== null && _b !== void 0 ? _b : new Error('Mongo payload upload aborted');
|
|
527
|
+
if (!(chunk instanceof Uint8Array))
|
|
528
|
+
throw new Error('Mongo payload stream yielded a non-byte chunk');
|
|
529
|
+
stream.length += BigInt(chunk.byteLength);
|
|
530
|
+
if (stream.length > BigInt(input.byteLength))
|
|
531
|
+
throw new Error('Mongo payload stream exceeds declared length');
|
|
532
|
+
if (stream.length > this.maxPayloadBytes)
|
|
533
|
+
throw new Error('Mongo payload stream exceeds configured byte bound');
|
|
534
|
+
stream.hash.update(chunk);
|
|
535
|
+
if (stream.inline !== undefined) {
|
|
536
|
+
stream.inline.push(chunk);
|
|
537
|
+
if (stream.length > BigInt(this.inlineCeiling))
|
|
538
|
+
await this.spillInlineToGridFs(input, payloadId, ownerId, fencingToken, stream);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (stream.upload === undefined)
|
|
542
|
+
throw new Error('Mongo payload stream writer missing');
|
|
543
|
+
if (!stream.upload.write(Buffer.from(chunk)))
|
|
544
|
+
await this.awaitAbort((0, node_events_1.once)(stream.upload, 'drain'), input.signal);
|
|
545
|
+
}
|
|
546
|
+
async spillInlineToGridFs(input, payloadId, ownerId, fencingToken, stream) {
|
|
547
|
+
const buffered = stream.inline;
|
|
548
|
+
if (buffered === undefined)
|
|
549
|
+
throw new Error('Mongo payload stream writer missing');
|
|
550
|
+
stream.fileId = new mongodb_1.ObjectId();
|
|
551
|
+
stream.upload = this.bucket.openUploadStreamWithId(stream.fileId, payloadId, {
|
|
552
|
+
chunkSizeBytes: this.gridFsChunkBytes,
|
|
553
|
+
metadata: {
|
|
554
|
+
state: 'staged',
|
|
555
|
+
payloadId,
|
|
556
|
+
kind: input.kind,
|
|
557
|
+
digest: input.digest,
|
|
558
|
+
byteLength: (0, MongoSchema_js_1.encodeMongoUint64)(input.byteLength),
|
|
559
|
+
ownerId,
|
|
560
|
+
fencingToken
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
stream.inline = undefined;
|
|
564
|
+
for (const item of buffered)
|
|
565
|
+
if (!stream.upload.write(Buffer.from(item)))
|
|
566
|
+
await this.awaitAbort((0, node_events_1.once)(stream.upload, 'drain'), input.signal);
|
|
567
|
+
}
|
|
568
|
+
async finishPayloadUpload(upload, signal) {
|
|
569
|
+
if (upload === undefined)
|
|
570
|
+
return;
|
|
571
|
+
upload.end();
|
|
572
|
+
await this.awaitAbort((0, node_events_1.once)(upload, 'finish'), signal);
|
|
573
|
+
}
|
|
574
|
+
fitsInline(payloadId, input, data, now) {
|
|
575
|
+
const candidate = {
|
|
576
|
+
_id: payloadId,
|
|
577
|
+
schemaVersion: 1,
|
|
578
|
+
network: this.scope.network,
|
|
579
|
+
genesisHash: this.scope.genesisHash,
|
|
580
|
+
kind: input.kind,
|
|
581
|
+
digest: input.digest,
|
|
582
|
+
byteLength: (0, MongoSchema_js_1.encodeMongoUint64)(input.byteLength),
|
|
583
|
+
state: 'ready',
|
|
584
|
+
guard: '0'.repeat(36),
|
|
585
|
+
ownerNodeId: this.scope.nodeId,
|
|
586
|
+
ownerId: '0'.repeat(36),
|
|
587
|
+
fencingToken: (0, MongoSchema_js_1.encodeMongoUint64)('1'),
|
|
588
|
+
leaseUntil: now,
|
|
589
|
+
createdAt: now,
|
|
590
|
+
updatedAt: now,
|
|
591
|
+
inlineData: data
|
|
592
|
+
};
|
|
593
|
+
return mongodb_1.BSON.serialize(candidate).byteLength < 1024 * 1024;
|
|
594
|
+
}
|
|
595
|
+
async retireOwnedFile(fileId, ownerId, fencingToken) {
|
|
596
|
+
const file = await this.db.collection(`${MongoSchema_js_1.MongoGridFsBucketName}.files`).findOne({
|
|
597
|
+
_id: fileId,
|
|
598
|
+
'metadata.ownerId': ownerId,
|
|
599
|
+
...(fencingToken === undefined ? {} : { 'metadata.fencingToken': fencingToken })
|
|
600
|
+
});
|
|
601
|
+
if (file !== null)
|
|
602
|
+
await this.bucket.delete(fileId).catch(() => undefined);
|
|
603
|
+
}
|
|
604
|
+
async readBytes(record) {
|
|
605
|
+
if (record.inlineData !== undefined)
|
|
606
|
+
return Buffer.from(record.inlineData.buffer);
|
|
607
|
+
if (record.fileId === undefined)
|
|
608
|
+
throw new Error('Mongo ready payload has no bytes');
|
|
609
|
+
const chunks = [];
|
|
610
|
+
const download = this.bucket.openDownloadStream(record.fileId);
|
|
611
|
+
const iterator = download[Symbol.asyncIterator]();
|
|
612
|
+
try {
|
|
613
|
+
while (true) {
|
|
614
|
+
const next = await iterator.next();
|
|
615
|
+
if (next.done)
|
|
616
|
+
break;
|
|
617
|
+
const chunk = next.value;
|
|
618
|
+
if (!(chunk instanceof Uint8Array))
|
|
619
|
+
throw new Error('Mongo GridFS returned a non-byte chunk');
|
|
620
|
+
chunks.push(Buffer.from(chunk));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
download.destroy(error instanceof Error ? error : new Error('Mongo payload read failed'));
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
return Buffer.concat(chunks);
|
|
628
|
+
}
|
|
629
|
+
async verifyGridFs(fileId, input, signal) {
|
|
630
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
631
|
+
let length = BigInt(0);
|
|
632
|
+
const download = this.bucket.openDownloadStream(fileId);
|
|
633
|
+
const iterator = download[Symbol.asyncIterator]();
|
|
634
|
+
try {
|
|
635
|
+
while (true) {
|
|
636
|
+
const next = await this.nextWithAbort(iterator, signal);
|
|
637
|
+
if (next.done)
|
|
638
|
+
break;
|
|
639
|
+
const chunk = next.value;
|
|
640
|
+
if (!(chunk instanceof Uint8Array))
|
|
641
|
+
throw new Error('Mongo GridFS returned a non-byte chunk');
|
|
642
|
+
length += BigInt(chunk.byteLength);
|
|
643
|
+
hash.update(chunk);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
download.destroy(error instanceof Error ? error : new Error('Mongo GridFS verification failed'));
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
if (length !== BigInt(input.byteLength) || hash.digest('hex') !== input.digest)
|
|
651
|
+
throw new Error('Mongo GridFS staged payload verification failed');
|
|
652
|
+
}
|
|
653
|
+
refFor(input, record) {
|
|
654
|
+
if (record !== undefined && (0, MongoSchema_js_1.decodeMongoUint64)(record.byteLength) !== input.byteLength)
|
|
655
|
+
throw new Error('Mongo payload byte length does not match ready content');
|
|
656
|
+
const ref = {
|
|
657
|
+
kind: input.kind,
|
|
658
|
+
digest: input.digest,
|
|
659
|
+
byteLength: input.byteLength
|
|
660
|
+
};
|
|
661
|
+
if (input.kind === 'raw-transaction') {
|
|
662
|
+
ref.txid = (0, node_crypto_1.createHash)('sha256')
|
|
663
|
+
.update(Buffer.from(input.digest, 'hex'))
|
|
664
|
+
.digest()
|
|
665
|
+
.reverse()
|
|
666
|
+
.toString('hex');
|
|
667
|
+
if (input.txid !== undefined && input.txid !== ref.txid)
|
|
668
|
+
throw new Error('Mongo raw transaction id does not match bytes');
|
|
669
|
+
}
|
|
670
|
+
return ref;
|
|
671
|
+
}
|
|
672
|
+
payloadId(scope, payload) {
|
|
673
|
+
if (!HEX_256.test(payload.digest))
|
|
674
|
+
throw new Error('Invalid Mongo payload digest');
|
|
675
|
+
return (0, MongoSchema_js_1.mongoRecordKey)((0, MongoSchema_js_1.mongoChainKey)(scope), payload.kind, payload.digest);
|
|
676
|
+
}
|
|
677
|
+
validateInput(input) {
|
|
678
|
+
this.payloadId(this.scope, input);
|
|
679
|
+
if (!this.isUint64(input.byteLength))
|
|
680
|
+
throw new Error('Invalid Mongo payload byte length');
|
|
681
|
+
if (BigInt(input.byteLength) > this.maxPayloadBytes)
|
|
682
|
+
throw new Error('Mongo payload exceeds configured byte bound');
|
|
683
|
+
if (input.txid !== undefined && (input.kind !== 'raw-transaction' || !HEX_256.test(input.txid)))
|
|
684
|
+
throw new Error('Invalid Mongo raw transaction id');
|
|
685
|
+
}
|
|
686
|
+
validateReference(reference) {
|
|
687
|
+
this.payloadId(reference.scope, reference.payload);
|
|
688
|
+
if (reference.scope.network !== this.scope.network ||
|
|
689
|
+
reference.scope.genesisHash !== this.scope.genesisHash ||
|
|
690
|
+
reference.scope.nodeId !== this.scope.nodeId)
|
|
691
|
+
throw new Error('Mongo payload reference scope does not match this store');
|
|
692
|
+
if (reference.ownerId.length === 0 || reference.slot.length === 0)
|
|
693
|
+
throw new Error('Invalid Mongo payload reference owner');
|
|
694
|
+
if ((reference.ownerKind === 'pin') !== (reference.expiresAt !== undefined))
|
|
695
|
+
throw new Error('Mongo payload expiry is valid only for explicit pins');
|
|
696
|
+
if (reference.expiresAt !== undefined && reference.expiresAt <= this.now())
|
|
697
|
+
throw new Error('Mongo payload pin must be unexpired');
|
|
698
|
+
}
|
|
699
|
+
assertTransaction(session) {
|
|
700
|
+
if (!session.inTransaction())
|
|
701
|
+
throw new Error('Mongo payload reference changes require an active transaction');
|
|
702
|
+
}
|
|
703
|
+
assertOperation(operation) {
|
|
704
|
+
var _a;
|
|
705
|
+
var _b;
|
|
706
|
+
if ((_a = operation.signal) === null || _a === void 0 ? void 0 : _a.aborted)
|
|
707
|
+
throw (_b = operation.signal.reason) !== null && _b !== void 0 ? _b : new Error('Mongo payload operation aborted');
|
|
708
|
+
if (operation.timeoutMS !== undefined &&
|
|
709
|
+
(!Number.isSafeInteger(operation.timeoutMS) || operation.timeoutMS < 1))
|
|
710
|
+
throw new Error('Invalid Mongo payload operation timeout');
|
|
711
|
+
}
|
|
712
|
+
async nextWithAbort(iterator, signal) {
|
|
713
|
+
var _a;
|
|
714
|
+
if (signal === undefined)
|
|
715
|
+
return await iterator.next();
|
|
716
|
+
if (signal.aborted)
|
|
717
|
+
throw (_a = signal.reason) !== null && _a !== void 0 ? _a : new Error('Mongo payload upload aborted');
|
|
718
|
+
return await this.raceAbort(iterator.next(), signal);
|
|
719
|
+
}
|
|
720
|
+
async awaitAbort(promise, signal) {
|
|
721
|
+
var _a;
|
|
722
|
+
if (signal === undefined)
|
|
723
|
+
return await promise;
|
|
724
|
+
if (signal.aborted)
|
|
725
|
+
throw (_a = signal.reason) !== null && _a !== void 0 ? _a : new Error('Mongo payload upload aborted');
|
|
726
|
+
return await this.raceAbort(promise, signal);
|
|
727
|
+
}
|
|
728
|
+
async raceAbort(promise, signal) {
|
|
729
|
+
return await new Promise((resolve, reject) => {
|
|
730
|
+
const abort = () => {
|
|
731
|
+
var _a;
|
|
732
|
+
cleanup();
|
|
733
|
+
reject((_a = signal.reason) !== null && _a !== void 0 ? _a : new Error('Mongo payload upload aborted'));
|
|
734
|
+
};
|
|
735
|
+
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
736
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
737
|
+
promise.then(value => {
|
|
738
|
+
cleanup();
|
|
739
|
+
resolve(value);
|
|
740
|
+
}, error => {
|
|
741
|
+
cleanup();
|
|
742
|
+
reject(error);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
throwIfAborted(signal) {
|
|
747
|
+
var _a;
|
|
748
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
749
|
+
throw (_a = signal.reason) !== null && _a !== void 0 ? _a : new Error('Mongo payload upload aborted');
|
|
750
|
+
}
|
|
751
|
+
isUint64(value) {
|
|
752
|
+
return /^(0|[1-9]\d{0,19})$/.test(value) && BigInt(value) <= MAX_UINT64;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
exports.MongoPayloadStore = MongoPayloadStore;
|
|
756
|
+
//# sourceMappingURL=MongoPayloadStore.js.map
|