@powerhousedao/reactor-attachments 6.2.3-dev.3 → 6.2.3-dev.30
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/dist/client.d.ts +51 -36
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +20 -4
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +92 -42
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -50
- package/dist/index.js.map +1 -1
- package/dist/{null-attachment-transport-CMrO_ZKA.d.ts → null-attachment-transport-BkM6rq4R.d.ts} +30 -18
- package/dist/null-attachment-transport-BkM6rq4R.d.ts.map +1 -0
- package/dist/{null-attachment-transport-CrsUafCi.js → null-attachment-transport-D53Rz9lE.js} +20 -13
- package/dist/null-attachment-transport-D53Rz9lE.js.map +1 -0
- package/package.json +6 -6
- package/dist/null-attachment-transport-CMrO_ZKA.d.ts.map +0 -1
- package/dist/null-attachment-transport-CrsUafCi.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { C as UploadTooLarge, S as SizeMismatch, _ as AttachmentPending, a as RemoteAttachmentUpload, b as InvalidAttachmentRef, c as createFetchUploadTransport, d as createRef, f as parseRef, g as AttachmentNotFound, h as AttachmentAlreadyExists, i as RemoteAttachmentUploadFactory, l as SwitchboardAttachmentTransport, m as parseAttachmentUploadTarget, n as createRemoteAttachmentService, o as RemoteReservationStore, p as parseAttachmentDownloadTarget, r as RemoteAttachmentStore, s as createXhrUploadTransport, t as NullAttachmentTransport, u as AttachmentService, v as AttachmentTransferError, x as ReservationNotFound, y as HashMismatch } from "./null-attachment-transport-
|
|
1
|
+
import { C as UploadTooLarge, S as SizeMismatch, _ as AttachmentPending, a as RemoteAttachmentUpload, b as InvalidAttachmentRef, c as createFetchUploadTransport, d as createRef, f as parseRef, g as AttachmentNotFound, h as AttachmentAlreadyExists, i as RemoteAttachmentUploadFactory, l as SwitchboardAttachmentTransport, m as parseAttachmentUploadTarget, n as createRemoteAttachmentService, o as RemoteReservationStore, p as parseAttachmentDownloadTarget, r as RemoteAttachmentStore, s as createXhrUploadTransport, t as NullAttachmentTransport, u as AttachmentService, v as AttachmentTransferError, x as ReservationNotFound, y as HashMismatch } from "./null-attachment-transport-D53Rz9lE.js";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { Migrator, sql } from "kysely";
|
|
4
4
|
import { mkdir, rename, rm } from "node:fs/promises";
|
|
5
5
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
|
+
import { finished } from "node:stream/promises";
|
|
8
9
|
import { generatorTypeDefs } from "@powerhousedao/document-engineering/graphql";
|
|
9
10
|
import { constantCase, pascalCase } from "change-case";
|
|
10
11
|
import { Kind, buildASTSchema, getNamedType, isInputObjectType, isListType, isNonNullType, parse, print } from "graphql";
|
|
11
|
-
import { BaseReadModel } from "@powerhousedao/reactor";
|
|
12
|
+
import { BaseReadModel, defaultReadModelIndexingConfig } from "@powerhousedao/reactor";
|
|
12
13
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
13
14
|
import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
14
15
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
@@ -46,6 +47,13 @@ function storageRelativePath(hash) {
|
|
|
46
47
|
*
|
|
47
48
|
* Returns the number of bytes written.
|
|
48
49
|
*/
|
|
50
|
+
async function discardTempFile(writer, tempPath) {
|
|
51
|
+
writer.destroy();
|
|
52
|
+
try {
|
|
53
|
+
await finished(writer, { error: false });
|
|
54
|
+
} catch {}
|
|
55
|
+
await rm(tempPath, { force: true });
|
|
56
|
+
}
|
|
49
57
|
async function writeAttachmentBytes(path, data) {
|
|
50
58
|
await mkdir(dirname(path), { recursive: true });
|
|
51
59
|
const tempPath = join(dirname(path), `${randomUUID()}.tmp`);
|
|
@@ -82,8 +90,7 @@ async function writeAttachmentBytes(path, data) {
|
|
|
82
90
|
reader.releaseLock();
|
|
83
91
|
}
|
|
84
92
|
if (caughtError) {
|
|
85
|
-
writer
|
|
86
|
-
await rm(tempPath, { force: true });
|
|
93
|
+
await discardTempFile(writer, tempPath);
|
|
87
94
|
throw caughtError;
|
|
88
95
|
}
|
|
89
96
|
try {
|
|
@@ -96,7 +103,7 @@ async function writeAttachmentBytes(path, data) {
|
|
|
96
103
|
});
|
|
97
104
|
await rename(tempPath, path);
|
|
98
105
|
} catch (err) {
|
|
99
|
-
await
|
|
106
|
+
await discardTempFile(writer, tempPath);
|
|
100
107
|
throw err instanceof Error ? err : new Error(String(err));
|
|
101
108
|
}
|
|
102
109
|
return bytesWritten;
|
|
@@ -255,6 +262,9 @@ function wrapStreamWithCleanup(source, cleanup) {
|
|
|
255
262
|
});
|
|
256
263
|
}
|
|
257
264
|
var KyselyAttachmentStore = class {
|
|
265
|
+
db;
|
|
266
|
+
transport;
|
|
267
|
+
basePath;
|
|
258
268
|
activeReaders = /* @__PURE__ */ new Map();
|
|
259
269
|
constructor(db, transport, basePath) {
|
|
260
270
|
this.db = db;
|
|
@@ -283,18 +293,10 @@ var KyselyAttachmentStore = class {
|
|
|
283
293
|
async has(hash) {
|
|
284
294
|
return (await this.db.selectFrom("attachment").select("status").where("hash", "=", hash).executeTakeFirst())?.status === "available";
|
|
285
295
|
}
|
|
286
|
-
async get(hash, signal) {
|
|
296
|
+
async get(hash, signal, documentId) {
|
|
287
297
|
const row = await this.db.selectFrom("attachment").selectAll().where("hash", "=", hash).executeTakeFirst();
|
|
288
298
|
if (row) {
|
|
289
|
-
if (row.status === "evicted")
|
|
290
|
-
const remote = await this.transport.fetch(hash, signal);
|
|
291
|
-
if (remote.kind === "data") {
|
|
292
|
-
await this.put(hash, remote.response.metadata, remote.response.body);
|
|
293
|
-
return this.get(hash, signal);
|
|
294
|
-
}
|
|
295
|
-
if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
|
|
296
|
-
throw new AttachmentNotFound(hash);
|
|
297
|
-
}
|
|
299
|
+
if (row.status === "evicted") return this.fetchRemote(hash, signal, documentId);
|
|
298
300
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
299
301
|
await this.db.updateTable("attachment").set({ last_accessed_at_utc: now }).where("hash", "=", hash).execute();
|
|
300
302
|
const header = rowToHeader$1(row);
|
|
@@ -312,13 +314,7 @@ var KyselyAttachmentStore = class {
|
|
|
312
314
|
fileName: pending.file_name,
|
|
313
315
|
sizeBytes: pending.size_bytes
|
|
314
316
|
});
|
|
315
|
-
|
|
316
|
-
if (remote.kind === "data") {
|
|
317
|
-
await this.put(hash, remote.response.metadata, remote.response.body);
|
|
318
|
-
return this.get(hash, signal);
|
|
319
|
-
}
|
|
320
|
-
if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
|
|
321
|
-
throw new AttachmentNotFound(hash);
|
|
317
|
+
return this.fetchRemote(hash, signal, documentId);
|
|
322
318
|
}
|
|
323
319
|
async put(hash, metadata, data) {
|
|
324
320
|
const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
|
|
@@ -358,6 +354,16 @@ var KyselyAttachmentStore = class {
|
|
|
358
354
|
const result = await this.db.selectFrom("attachment").select(sql`COALESCE(SUM(size_bytes), 0)`.as("total")).where("status", "=", "available").executeTakeFirst();
|
|
359
355
|
return Number(result?.total ?? 0);
|
|
360
356
|
}
|
|
357
|
+
async fetchRemote(hash, signal, documentId) {
|
|
358
|
+
if (documentId === void 0) throw new AttachmentNotFound(hash);
|
|
359
|
+
const remote = await this.transport.fetch(hash, documentId, signal);
|
|
360
|
+
if (remote.kind === "data") {
|
|
361
|
+
await this.put(hash, remote.response.metadata, remote.response.body);
|
|
362
|
+
return this.get(hash, signal);
|
|
363
|
+
}
|
|
364
|
+
if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
|
|
365
|
+
throw new AttachmentNotFound(hash);
|
|
366
|
+
}
|
|
361
367
|
async findPendingReservation(hash, now) {
|
|
362
368
|
const row = await this.db.selectFrom("attachment_reservation as r").leftJoin("attachment as a", "a.hash", "r.client_hash").select([
|
|
363
369
|
"r.mime_type",
|
|
@@ -391,7 +397,7 @@ var KyselyAttachmentStore = class {
|
|
|
391
397
|
};
|
|
392
398
|
//#endregion
|
|
393
399
|
//#region src/storage/kysely/reservation-store.ts
|
|
394
|
-
const DEFAULT_RESERVATION_TTL_MS =
|
|
400
|
+
const DEFAULT_RESERVATION_TTL_MS = 864e5;
|
|
395
401
|
function rowToReservation(row) {
|
|
396
402
|
return {
|
|
397
403
|
reservationId: row.reservation_id,
|
|
@@ -405,6 +411,7 @@ function rowToReservation(row) {
|
|
|
405
411
|
};
|
|
406
412
|
}
|
|
407
413
|
var KyselyReservationStore = class {
|
|
414
|
+
db;
|
|
408
415
|
ttlMs;
|
|
409
416
|
constructor(db, ttlMs = DEFAULT_RESERVATION_TTL_MS) {
|
|
410
417
|
this.db = db;
|
|
@@ -594,6 +601,11 @@ function rowToHeader(row) {
|
|
|
594
601
|
};
|
|
595
602
|
}
|
|
596
603
|
var DirectAttachmentUpload = class {
|
|
604
|
+
reservation;
|
|
605
|
+
db;
|
|
606
|
+
basePath;
|
|
607
|
+
reservations;
|
|
608
|
+
maxBytes;
|
|
597
609
|
reservationId;
|
|
598
610
|
ref;
|
|
599
611
|
expiresAtUtc;
|
|
@@ -663,6 +675,10 @@ var DirectAttachmentUpload = class {
|
|
|
663
675
|
//#endregion
|
|
664
676
|
//#region src/direct/direct-attachment-upload-factory.ts
|
|
665
677
|
var DirectAttachmentUploadFactory = class {
|
|
678
|
+
db;
|
|
679
|
+
basePath;
|
|
680
|
+
reservations;
|
|
681
|
+
maxBytes;
|
|
666
682
|
constructor(db, basePath, reservations, maxBytes) {
|
|
667
683
|
this.db = db;
|
|
668
684
|
this.basePath = basePath;
|
|
@@ -681,6 +697,8 @@ var DirectAttachmentUploadFactory = class {
|
|
|
681
697
|
* can never accidentally return a direct-provider target.
|
|
682
698
|
*/
|
|
683
699
|
var FilesystemAttachmentBackend = class {
|
|
700
|
+
store;
|
|
701
|
+
config;
|
|
684
702
|
kind = "filesystem";
|
|
685
703
|
constructor(store, config) {
|
|
686
704
|
this.store = store;
|
|
@@ -732,6 +750,8 @@ var S3AttachmentUploadFactory = class {
|
|
|
732
750
|
//#endregion
|
|
733
751
|
//#region src/attachment-builder.ts
|
|
734
752
|
var AttachmentBuilder = class {
|
|
753
|
+
db;
|
|
754
|
+
storagePath;
|
|
735
755
|
transport = new NullAttachmentTransport();
|
|
736
756
|
customUploadFactory;
|
|
737
757
|
maxUploadBytes;
|
|
@@ -804,7 +824,7 @@ var AttachmentBuilder = class {
|
|
|
804
824
|
//#endregion
|
|
805
825
|
//#region src/reference-index/attachment-schema-compiler.ts
|
|
806
826
|
const ATTACHMENT_REF_TYPE = "AttachmentRef";
|
|
807
|
-
const CODEGEN_SCALAR_NAMES = new Set([
|
|
827
|
+
const CODEGEN_SCALAR_NAMES = /* @__PURE__ */ new Set([
|
|
808
828
|
"Unknown",
|
|
809
829
|
"DateTime",
|
|
810
830
|
"Address",
|
|
@@ -852,11 +872,12 @@ function buildEffectiveSchema(specification, context) {
|
|
|
852
872
|
const stateSchemas = Object.values(specification.state).map((state) => state.schema);
|
|
853
873
|
const operationSchemas = specification.modules.flatMap((moduleSpecification) => moduleSpecification.operations.flatMap((operation) => operation.schema === null ? [] : [operation.schema]));
|
|
854
874
|
try {
|
|
855
|
-
|
|
875
|
+
const document = parse([
|
|
856
876
|
...scalarSchemas,
|
|
857
877
|
...stateSchemas,
|
|
858
878
|
...operationSchemas
|
|
859
|
-
].filter(Boolean).join("\n\n"))
|
|
879
|
+
].filter(Boolean).join("\n\n"));
|
|
880
|
+
return buildASTSchema(dedupeTypeDefinitions(document));
|
|
860
881
|
} catch {
|
|
861
882
|
throw compilationError(context, "the effective GraphQL schema is invalid");
|
|
862
883
|
}
|
|
@@ -994,6 +1015,8 @@ function extractValue(plan, value, path, context, refs, seenRefs, activeObjects)
|
|
|
994
1015
|
}
|
|
995
1016
|
}
|
|
996
1017
|
var SchemaCompiledAttachmentExtractor = class {
|
|
1018
|
+
context;
|
|
1019
|
+
rootPlan;
|
|
997
1020
|
constructor(context, rootPlan) {
|
|
998
1021
|
this.context = context;
|
|
999
1022
|
this.rootPlan = rootPlan;
|
|
@@ -1056,12 +1079,16 @@ var AttachmentSchemaCompiler = class {
|
|
|
1056
1079
|
//#endregion
|
|
1057
1080
|
//#region src/read-models/attachment-reference/kysely-attachment-reference-store.ts
|
|
1058
1081
|
var KyselyAttachmentReferenceStore = class {
|
|
1082
|
+
db;
|
|
1059
1083
|
constructor(db) {
|
|
1060
1084
|
this.db = db;
|
|
1061
1085
|
}
|
|
1062
1086
|
async hasReference(documentId, ref) {
|
|
1063
1087
|
return await this.db.selectFrom("attachment_reference").select("document_id").where("document_id", "=", documentId).where("attachment_ref", "=", ref).executeTakeFirst() !== void 0;
|
|
1064
1088
|
}
|
|
1089
|
+
async referencingScopes(documentId, ref) {
|
|
1090
|
+
return (await this.db.selectFrom("attachment_reference").select("scope").distinct().where("document_id", "=", documentId).where("attachment_ref", "=", ref).execute()).map((row) => row.scope);
|
|
1091
|
+
}
|
|
1065
1092
|
async addReferences(references) {
|
|
1066
1093
|
if (references.length === 0) return;
|
|
1067
1094
|
await this.db.insertInto("attachment_reference").values(references.map((reference) => ({
|
|
@@ -1153,6 +1180,7 @@ async function getAttachmentReferenceMigrationStatus(db, schema = ATTACHMENT_REF
|
|
|
1153
1180
|
//#endregion
|
|
1154
1181
|
//#region src/read-models/attachment-reference/index-builder.ts
|
|
1155
1182
|
var AttachmentReferenceIndexBuilder = class {
|
|
1183
|
+
db;
|
|
1156
1184
|
constructor(db) {
|
|
1157
1185
|
this.db = db;
|
|
1158
1186
|
}
|
|
@@ -1165,12 +1193,36 @@ var AttachmentReferenceIndexBuilder = class {
|
|
|
1165
1193
|
//#endregion
|
|
1166
1194
|
//#region src/read-models/attachment-reference/attachment-reference-read-model.ts
|
|
1167
1195
|
const ATTACHMENT_REFERENCE_READ_MODEL_ID = "attachment-reference-read-model";
|
|
1196
|
+
/**
|
|
1197
|
+
* Indexes attachment references in chunks.
|
|
1198
|
+
*
|
|
1199
|
+
* Each reference row stands on its own and is written insert-or-do-nothing, so
|
|
1200
|
+
* a batch that commits in pieces exposes no partial structure: a reader either
|
|
1201
|
+
* sees a reference or does not, and re-indexing the same range writes nothing
|
|
1202
|
+
* new. Batches here are the largest in the system, which is what chunking is
|
|
1203
|
+
* for.
|
|
1204
|
+
*/
|
|
1168
1205
|
var AttachmentReferenceReadModel = class extends BaseReadModel {
|
|
1206
|
+
documentModelRegistry;
|
|
1207
|
+
schemaCompiler;
|
|
1208
|
+
referenceWriter;
|
|
1169
1209
|
indexingQueue = Promise.resolve();
|
|
1210
|
+
checkpointTarget;
|
|
1211
|
+
/**
|
|
1212
|
+
* How far a replay may skip ahead of the cursor: the highest ordinal already
|
|
1213
|
+
* pulled from the index while the cursor was parked.
|
|
1214
|
+
*
|
|
1215
|
+
* NOT a claim that every ordinal below it committed -- the parked ordinal is
|
|
1216
|
+
* precisely the one that did not -- so it is a hint, and {@link replayFrom}
|
|
1217
|
+
* re-probes the gap before honouring it.
|
|
1218
|
+
*/
|
|
1219
|
+
replayedThrough;
|
|
1220
|
+
warnedCheckpoint;
|
|
1170
1221
|
constructor(db, operationIndex, writeCache, consistencyTracker, documentModelRegistry, schemaCompiler, referenceWriter) {
|
|
1171
1222
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
1172
1223
|
readModelId: ATTACHMENT_REFERENCE_READ_MODEL_ID,
|
|
1173
|
-
rebuildStateOnInit: false
|
|
1224
|
+
rebuildStateOnInit: false,
|
|
1225
|
+
indexing: defaultReadModelIndexingConfig
|
|
1174
1226
|
});
|
|
1175
1227
|
this.documentModelRegistry = documentModelRegistry;
|
|
1176
1228
|
this.schemaCompiler = schemaCompiler;
|
|
@@ -1215,43 +1267,89 @@ var AttachmentReferenceReadModel = class extends BaseReadModel {
|
|
|
1215
1267
|
if (references.length > 0) await this.referenceWriter.addReferences(references);
|
|
1216
1268
|
}
|
|
1217
1269
|
async indexOperationsInOrdinalOrder(incoming) {
|
|
1218
|
-
|
|
1219
|
-
if (
|
|
1220
|
-
const incomingMax =
|
|
1221
|
-
|
|
1222
|
-
if (!this.isContiguousThrough(candidates, incomingMax)) {
|
|
1270
|
+
let candidates = this.sortAndDedupe(incoming);
|
|
1271
|
+
if (candidates.length === 0) return;
|
|
1272
|
+
const incomingMax = candidates[candidates.length - 1].context.ordinal;
|
|
1273
|
+
if (this.contiguousEnd(candidates) < incomingMax) {
|
|
1223
1274
|
const replayed = await this.loadThroughOrdinal(incomingMax);
|
|
1224
|
-
candidates = this.sortAndDedupe([...replayed, ...
|
|
1275
|
+
candidates = this.sortAndDedupe([...replayed, ...candidates]);
|
|
1225
1276
|
}
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
if (ordinal < expectedOrdinal) continue;
|
|
1231
|
-
if (ordinal > expectedOrdinal || ordinal > incomingMax) break;
|
|
1232
|
-
contiguous.push(item);
|
|
1233
|
-
expectedOrdinal++;
|
|
1277
|
+
const checkpoint = this.contiguousEnd(candidates);
|
|
1278
|
+
if (checkpoint < incomingMax && checkpoint !== this.warnedCheckpoint) {
|
|
1279
|
+
this.warnedCheckpoint = checkpoint;
|
|
1280
|
+
console.warn(`[${this.config.readModelId}] indexed through ordinal ${incomingMax} but parked the cursor at ${checkpoint}: ordinal ${checkpoint + 1} is missing`);
|
|
1234
1281
|
}
|
|
1235
|
-
if (expectedOrdinal <= incomingMax) throw new Error(`Attachment reference read model cannot advance past missing ordinal ${expectedOrdinal}`);
|
|
1236
1282
|
const previousOrdinal = this.lastOrdinal;
|
|
1283
|
+
this.checkpointTarget = checkpoint;
|
|
1237
1284
|
try {
|
|
1238
|
-
await super.indexOperations(
|
|
1285
|
+
await super.indexOperations(candidates);
|
|
1239
1286
|
} catch (error) {
|
|
1240
1287
|
this.lastOrdinal = previousOrdinal;
|
|
1288
|
+
this.replayedThrough = void 0;
|
|
1241
1289
|
throw error;
|
|
1290
|
+
} finally {
|
|
1291
|
+
this.checkpointTarget = void 0;
|
|
1242
1292
|
}
|
|
1293
|
+
if (checkpoint > previousOrdinal) this.replayedThrough = void 0;
|
|
1294
|
+
else if (checkpoint < incomingMax) this.replayedThrough = Math.max(this.replayedThrough ?? checkpoint, incomingMax);
|
|
1243
1295
|
}
|
|
1244
|
-
|
|
1296
|
+
/**
|
|
1297
|
+
* Writes the cursor parked by indexOperationsInOrdinalOrder instead of the
|
|
1298
|
+
* batch maximum, so indexing an operation above a gap never advances past it.
|
|
1299
|
+
*/
|
|
1300
|
+
async saveState(trx, items) {
|
|
1301
|
+
const target = this.checkpointTarget;
|
|
1302
|
+
if (target === void 0) {
|
|
1303
|
+
await super.saveState(trx, items);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
this.lastOrdinal = target;
|
|
1307
|
+
await trx.updateTable("ViewState").set({
|
|
1308
|
+
lastOrdinal: target,
|
|
1309
|
+
lastOperationTimestamp: /* @__PURE__ */ new Date()
|
|
1310
|
+
}).where("readModelId", "=", this.config.readModelId).execute();
|
|
1311
|
+
}
|
|
1312
|
+
/** Last ordinal of the contiguous run starting at lastOrdinal + 1. */
|
|
1313
|
+
contiguousEnd(items) {
|
|
1245
1314
|
let expectedOrdinal = this.lastOrdinal + 1;
|
|
1246
1315
|
for (const item of items) {
|
|
1247
|
-
|
|
1316
|
+
const ordinal = item.context.ordinal;
|
|
1317
|
+
if (ordinal < expectedOrdinal) continue;
|
|
1318
|
+
if (ordinal > expectedOrdinal) break;
|
|
1248
1319
|
expectedOrdinal++;
|
|
1249
1320
|
}
|
|
1250
|
-
return expectedOrdinal
|
|
1321
|
+
return expectedOrdinal - 1;
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Opens a replay at the lowest ordinal still worth reading.
|
|
1325
|
+
*
|
|
1326
|
+
* {@link replayedThrough} is what keeps a permanently held hole -- a
|
|
1327
|
+
* rolled-back insert, which never fills -- from re-reading the whole tail on
|
|
1328
|
+
* every batch. It cannot be trusted on its own: a hole that fills without
|
|
1329
|
+
* being delivered here is visible only in the index, and a mark that is
|
|
1330
|
+
* never questioned would hide that operation for the life of the process.
|
|
1331
|
+
* That happens whenever another writer commits the gap (a second reactor on
|
|
1332
|
+
* the same database), and it leaves the cursor parked below an operation
|
|
1333
|
+
* whose references were never written -- `hasReference` then answers false
|
|
1334
|
+
* for an attachment that is genuinely referenced.
|
|
1335
|
+
*
|
|
1336
|
+
* So a parked batch spends one page probing the gap: the first row above the
|
|
1337
|
+
* cursor is the missing ordinal itself once it commits. Finding it drops the
|
|
1338
|
+
* mark and replays from the cursor; not finding it leaves the mark standing,
|
|
1339
|
+
* which is the cheap path and the common one.
|
|
1340
|
+
*/
|
|
1341
|
+
async replayFrom() {
|
|
1342
|
+
const fromCursor = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
|
|
1343
|
+
if (this.replayedThrough === void 0) return fromCursor;
|
|
1344
|
+
if (fromCursor.results[0]?.context.ordinal === this.lastOrdinal + 1) {
|
|
1345
|
+
this.replayedThrough = void 0;
|
|
1346
|
+
return fromCursor;
|
|
1347
|
+
}
|
|
1348
|
+
return this.operationIndex.getSinceOrdinal(this.replayedThrough);
|
|
1251
1349
|
}
|
|
1252
1350
|
async loadThroughOrdinal(maxOrdinal) {
|
|
1253
1351
|
const operations = [];
|
|
1254
|
-
let page = await this.
|
|
1352
|
+
let page = await this.replayFrom();
|
|
1255
1353
|
for (;;) {
|
|
1256
1354
|
for (const item of page.results) if (item.context.ordinal <= maxOrdinal) operations.push(item);
|
|
1257
1355
|
if (page.results.some(({ context }) => context.ordinal >= maxOrdinal) || !page.next) break;
|
|
@@ -1277,7 +1375,7 @@ function required(env, name) {
|
|
|
1277
1375
|
if (value.trim() !== value) throw new Error(`${name} must not have leading or trailing whitespace`);
|
|
1278
1376
|
return value;
|
|
1279
1377
|
}
|
|
1280
|
-
const LOOPBACK_HOSTNAMES = new Set([
|
|
1378
|
+
const LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
1281
1379
|
"127.0.0.1",
|
|
1282
1380
|
"localhost",
|
|
1283
1381
|
"[::1]"
|
|
@@ -1359,9 +1457,10 @@ function sha256HexToBase64(hash) {
|
|
|
1359
1457
|
//#region src/storage/s3/primitives.ts
|
|
1360
1458
|
const defaultPresigner = (client, command, expiresInSeconds) => getSignedUrl(client, command, {
|
|
1361
1459
|
expiresIn: expiresInSeconds,
|
|
1362
|
-
unhoistableHeaders: new Set(["x-amz-checksum-sha256"])
|
|
1460
|
+
unhoistableHeaders: /* @__PURE__ */ new Set(["x-amz-checksum-sha256"])
|
|
1363
1461
|
});
|
|
1364
1462
|
var S3AttachmentPrimitives = class {
|
|
1463
|
+
config;
|
|
1365
1464
|
client;
|
|
1366
1465
|
presign;
|
|
1367
1466
|
now;
|
|
@@ -1432,7 +1531,7 @@ function createS3AttachmentPrimitives(config, dependencies = {}) {
|
|
|
1432
1531
|
//#endregion
|
|
1433
1532
|
//#region src/storage/s3/backend.ts
|
|
1434
1533
|
const READINESS_PROBE_HASH = "0".repeat(64);
|
|
1435
|
-
const OBJECT_NOT_FOUND_NAMES = new Set([
|
|
1534
|
+
const OBJECT_NOT_FOUND_NAMES = /* @__PURE__ */ new Set([
|
|
1436
1535
|
"NotFound",
|
|
1437
1536
|
"NoSuchKey",
|
|
1438
1537
|
"NoSuchObject"
|
|
@@ -1450,6 +1549,8 @@ function requireHashFirstReservation(reservation) {
|
|
|
1450
1549
|
}
|
|
1451
1550
|
/** Server-only S3 capability. Callers complete authorization before download. */
|
|
1452
1551
|
var S3AttachmentBackend = class {
|
|
1552
|
+
db;
|
|
1553
|
+
config;
|
|
1453
1554
|
kind = "s3";
|
|
1454
1555
|
primitives;
|
|
1455
1556
|
now;
|