@powerhousedao/reactor-attachments 6.2.3-dev.8 → 6.2.3-staging.0

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/index.js CHANGED
@@ -1,4 +1,4 @@
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-CrsUafCi.js";
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";
@@ -9,7 +9,7 @@ import { finished } from "node:stream/promises";
9
9
  import { generatorTypeDefs } from "@powerhousedao/document-engineering/graphql";
10
10
  import { constantCase, pascalCase } from "change-case";
11
11
  import { Kind, buildASTSchema, getNamedType, isInputObjectType, isListType, isNonNullType, parse, print } from "graphql";
12
- import { BaseReadModel } from "@powerhousedao/reactor";
12
+ import { BaseReadModel, defaultReadModelIndexingConfig } from "@powerhousedao/reactor";
13
13
  import { Buffer as Buffer$1 } from "node:buffer";
14
14
  import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
15
15
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -262,6 +262,9 @@ function wrapStreamWithCleanup(source, cleanup) {
262
262
  });
263
263
  }
264
264
  var KyselyAttachmentStore = class {
265
+ db;
266
+ transport;
267
+ basePath;
265
268
  activeReaders = /* @__PURE__ */ new Map();
266
269
  constructor(db, transport, basePath) {
267
270
  this.db = db;
@@ -290,18 +293,10 @@ var KyselyAttachmentStore = class {
290
293
  async has(hash) {
291
294
  return (await this.db.selectFrom("attachment").select("status").where("hash", "=", hash).executeTakeFirst())?.status === "available";
292
295
  }
293
- async get(hash, signal) {
296
+ async get(hash, signal, documentId) {
294
297
  const row = await this.db.selectFrom("attachment").selectAll().where("hash", "=", hash).executeTakeFirst();
295
298
  if (row) {
296
- if (row.status === "evicted") {
297
- const remote = await this.transport.fetch(hash, signal);
298
- if (remote.kind === "data") {
299
- await this.put(hash, remote.response.metadata, remote.response.body);
300
- return this.get(hash, signal);
301
- }
302
- if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
303
- throw new AttachmentNotFound(hash);
304
- }
299
+ if (row.status === "evicted") return this.fetchRemote(hash, signal, documentId);
305
300
  const now = (/* @__PURE__ */ new Date()).toISOString();
306
301
  await this.db.updateTable("attachment").set({ last_accessed_at_utc: now }).where("hash", "=", hash).execute();
307
302
  const header = rowToHeader$1(row);
@@ -319,13 +314,7 @@ var KyselyAttachmentStore = class {
319
314
  fileName: pending.file_name,
320
315
  sizeBytes: pending.size_bytes
321
316
  });
322
- const remote = await this.transport.fetch(hash, signal);
323
- if (remote.kind === "data") {
324
- await this.put(hash, remote.response.metadata, remote.response.body);
325
- return this.get(hash, signal);
326
- }
327
- if (remote.kind === "pending") throw new AttachmentPending(hash, remote.expiresAtUtc);
328
- throw new AttachmentNotFound(hash);
317
+ return this.fetchRemote(hash, signal, documentId);
329
318
  }
330
319
  async put(hash, metadata, data) {
331
320
  const existing = await this.db.selectFrom("attachment").select(["hash", "status"]).where("hash", "=", hash).executeTakeFirst();
@@ -365,6 +354,16 @@ var KyselyAttachmentStore = class {
365
354
  const result = await this.db.selectFrom("attachment").select(sql`COALESCE(SUM(size_bytes), 0)`.as("total")).where("status", "=", "available").executeTakeFirst();
366
355
  return Number(result?.total ?? 0);
367
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
+ }
368
367
  async findPendingReservation(hash, now) {
369
368
  const row = await this.db.selectFrom("attachment_reservation as r").leftJoin("attachment as a", "a.hash", "r.client_hash").select([
370
369
  "r.mime_type",
@@ -398,7 +397,7 @@ var KyselyAttachmentStore = class {
398
397
  };
399
398
  //#endregion
400
399
  //#region src/storage/kysely/reservation-store.ts
401
- const DEFAULT_RESERVATION_TTL_MS = 1440 * 60 * 1e3;
400
+ const DEFAULT_RESERVATION_TTL_MS = 864e5;
402
401
  function rowToReservation(row) {
403
402
  return {
404
403
  reservationId: row.reservation_id,
@@ -412,6 +411,7 @@ function rowToReservation(row) {
412
411
  };
413
412
  }
414
413
  var KyselyReservationStore = class {
414
+ db;
415
415
  ttlMs;
416
416
  constructor(db, ttlMs = DEFAULT_RESERVATION_TTL_MS) {
417
417
  this.db = db;
@@ -601,6 +601,11 @@ function rowToHeader(row) {
601
601
  };
602
602
  }
603
603
  var DirectAttachmentUpload = class {
604
+ reservation;
605
+ db;
606
+ basePath;
607
+ reservations;
608
+ maxBytes;
604
609
  reservationId;
605
610
  ref;
606
611
  expiresAtUtc;
@@ -670,6 +675,10 @@ var DirectAttachmentUpload = class {
670
675
  //#endregion
671
676
  //#region src/direct/direct-attachment-upload-factory.ts
672
677
  var DirectAttachmentUploadFactory = class {
678
+ db;
679
+ basePath;
680
+ reservations;
681
+ maxBytes;
673
682
  constructor(db, basePath, reservations, maxBytes) {
674
683
  this.db = db;
675
684
  this.basePath = basePath;
@@ -688,6 +697,8 @@ var DirectAttachmentUploadFactory = class {
688
697
  * can never accidentally return a direct-provider target.
689
698
  */
690
699
  var FilesystemAttachmentBackend = class {
700
+ store;
701
+ config;
691
702
  kind = "filesystem";
692
703
  constructor(store, config) {
693
704
  this.store = store;
@@ -739,6 +750,8 @@ var S3AttachmentUploadFactory = class {
739
750
  //#endregion
740
751
  //#region src/attachment-builder.ts
741
752
  var AttachmentBuilder = class {
753
+ db;
754
+ storagePath;
742
755
  transport = new NullAttachmentTransport();
743
756
  customUploadFactory;
744
757
  maxUploadBytes;
@@ -811,7 +824,7 @@ var AttachmentBuilder = class {
811
824
  //#endregion
812
825
  //#region src/reference-index/attachment-schema-compiler.ts
813
826
  const ATTACHMENT_REF_TYPE = "AttachmentRef";
814
- const CODEGEN_SCALAR_NAMES = new Set([
827
+ const CODEGEN_SCALAR_NAMES = /* @__PURE__ */ new Set([
815
828
  "Unknown",
816
829
  "DateTime",
817
830
  "Address",
@@ -859,11 +872,12 @@ function buildEffectiveSchema(specification, context) {
859
872
  const stateSchemas = Object.values(specification.state).map((state) => state.schema);
860
873
  const operationSchemas = specification.modules.flatMap((moduleSpecification) => moduleSpecification.operations.flatMap((operation) => operation.schema === null ? [] : [operation.schema]));
861
874
  try {
862
- return buildASTSchema(dedupeTypeDefinitions(parse([
875
+ const document = parse([
863
876
  ...scalarSchemas,
864
877
  ...stateSchemas,
865
878
  ...operationSchemas
866
- ].filter(Boolean).join("\n\n"))));
879
+ ].filter(Boolean).join("\n\n"));
880
+ return buildASTSchema(dedupeTypeDefinitions(document));
867
881
  } catch {
868
882
  throw compilationError(context, "the effective GraphQL schema is invalid");
869
883
  }
@@ -1001,6 +1015,8 @@ function extractValue(plan, value, path, context, refs, seenRefs, activeObjects)
1001
1015
  }
1002
1016
  }
1003
1017
  var SchemaCompiledAttachmentExtractor = class {
1018
+ context;
1019
+ rootPlan;
1004
1020
  constructor(context, rootPlan) {
1005
1021
  this.context = context;
1006
1022
  this.rootPlan = rootPlan;
@@ -1063,12 +1079,16 @@ var AttachmentSchemaCompiler = class {
1063
1079
  //#endregion
1064
1080
  //#region src/read-models/attachment-reference/kysely-attachment-reference-store.ts
1065
1081
  var KyselyAttachmentReferenceStore = class {
1082
+ db;
1066
1083
  constructor(db) {
1067
1084
  this.db = db;
1068
1085
  }
1069
1086
  async hasReference(documentId, ref) {
1070
1087
  return await this.db.selectFrom("attachment_reference").select("document_id").where("document_id", "=", documentId).where("attachment_ref", "=", ref).executeTakeFirst() !== void 0;
1071
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
+ }
1072
1092
  async addReferences(references) {
1073
1093
  if (references.length === 0) return;
1074
1094
  await this.db.insertInto("attachment_reference").values(references.map((reference) => ({
@@ -1160,6 +1180,7 @@ async function getAttachmentReferenceMigrationStatus(db, schema = ATTACHMENT_REF
1160
1180
  //#endregion
1161
1181
  //#region src/read-models/attachment-reference/index-builder.ts
1162
1182
  var AttachmentReferenceIndexBuilder = class {
1183
+ db;
1163
1184
  constructor(db) {
1164
1185
  this.db = db;
1165
1186
  }
@@ -1172,49 +1193,28 @@ var AttachmentReferenceIndexBuilder = class {
1172
1193
  //#endregion
1173
1194
  //#region src/read-models/attachment-reference/attachment-reference-read-model.ts
1174
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, and re-indexing
1201
+ * the same range writes nothing new: the catch-up sweep needs no stream suffix.
1202
+ */
1175
1203
  var AttachmentReferenceReadModel = class extends BaseReadModel {
1176
- indexingQueue = Promise.resolve();
1177
- checkpointTarget;
1178
- /**
1179
- * How far a replay may skip ahead of the cursor: the highest ordinal already
1180
- * pulled from the index while the cursor was parked.
1181
- *
1182
- * NOT a claim that every ordinal below it committed -- the parked ordinal is
1183
- * precisely the one that did not -- so it is a hint, and {@link replayFrom}
1184
- * re-probes the gap before honouring it.
1185
- */
1186
- replayedThrough;
1187
- warnedCheckpoint;
1204
+ documentModelRegistry;
1205
+ schemaCompiler;
1206
+ referenceWriter;
1188
1207
  constructor(db, operationIndex, writeCache, consistencyTracker, documentModelRegistry, schemaCompiler, referenceWriter) {
1189
1208
  super(db, operationIndex, writeCache, consistencyTracker, {
1190
1209
  readModelId: ATTACHMENT_REFERENCE_READ_MODEL_ID,
1191
- rebuildStateOnInit: false
1210
+ rebuildStateOnInit: false,
1211
+ indexing: defaultReadModelIndexingConfig,
1212
+ replayStreamSuffix: false
1192
1213
  });
1193
1214
  this.documentModelRegistry = documentModelRegistry;
1194
1215
  this.schemaCompiler = schemaCompiler;
1195
1216
  this.referenceWriter = referenceWriter;
1196
1217
  }
1197
- indexOperations(items) {
1198
- return this.enqueue(() => this.indexOperationsInOrdinalOrder(items));
1199
- }
1200
- init() {
1201
- return this.enqueue(async () => {
1202
- const viewState = await this.loadState();
1203
- if (viewState !== void 0) this.lastOrdinal = viewState;
1204
- else await this.initializeState();
1205
- let page = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
1206
- while (page.results.length > 0) {
1207
- await this.indexOperationsInOrdinalOrder(page.results);
1208
- if (!page.next) break;
1209
- page = await page.next();
1210
- }
1211
- });
1212
- }
1213
- enqueue(work) {
1214
- const result = this.indexingQueue.then(work);
1215
- this.indexingQueue = result.catch(() => void 0);
1216
- return result;
1217
- }
1218
1218
  async commitOperations(items) {
1219
1219
  const references = [];
1220
1220
  for (const { operation, context } of items) {
@@ -1232,102 +1232,6 @@ var AttachmentReferenceReadModel = class extends BaseReadModel {
1232
1232
  }
1233
1233
  if (references.length > 0) await this.referenceWriter.addReferences(references);
1234
1234
  }
1235
- async indexOperationsInOrdinalOrder(incoming) {
1236
- let candidates = this.sortAndDedupe(incoming);
1237
- if (candidates.length === 0) return;
1238
- const incomingMax = candidates[candidates.length - 1].context.ordinal;
1239
- if (this.contiguousEnd(candidates) < incomingMax) {
1240
- const replayed = await this.loadThroughOrdinal(incomingMax);
1241
- candidates = this.sortAndDedupe([...replayed, ...candidates]);
1242
- }
1243
- const checkpoint = this.contiguousEnd(candidates);
1244
- if (checkpoint < incomingMax && checkpoint !== this.warnedCheckpoint) {
1245
- this.warnedCheckpoint = checkpoint;
1246
- console.warn(`[${this.config.readModelId}] indexed through ordinal ${incomingMax} but parked the cursor at ${checkpoint}: ordinal ${checkpoint + 1} is missing`);
1247
- }
1248
- const previousOrdinal = this.lastOrdinal;
1249
- this.checkpointTarget = checkpoint;
1250
- try {
1251
- await super.indexOperations(candidates);
1252
- } catch (error) {
1253
- this.lastOrdinal = previousOrdinal;
1254
- this.replayedThrough = void 0;
1255
- throw error;
1256
- } finally {
1257
- this.checkpointTarget = void 0;
1258
- }
1259
- if (checkpoint > previousOrdinal) this.replayedThrough = void 0;
1260
- else if (checkpoint < incomingMax) this.replayedThrough = Math.max(this.replayedThrough ?? checkpoint, incomingMax);
1261
- }
1262
- /**
1263
- * Writes the cursor parked by indexOperationsInOrdinalOrder instead of the
1264
- * batch maximum, so indexing an operation above a gap never advances past it.
1265
- */
1266
- async saveState(trx, items) {
1267
- const target = this.checkpointTarget;
1268
- if (target === void 0) {
1269
- await super.saveState(trx, items);
1270
- return;
1271
- }
1272
- this.lastOrdinal = target;
1273
- await trx.updateTable("ViewState").set({
1274
- lastOrdinal: target,
1275
- lastOperationTimestamp: /* @__PURE__ */ new Date()
1276
- }).where("readModelId", "=", this.config.readModelId).execute();
1277
- }
1278
- /** Last ordinal of the contiguous run starting at lastOrdinal + 1. */
1279
- contiguousEnd(items) {
1280
- let expectedOrdinal = this.lastOrdinal + 1;
1281
- for (const item of items) {
1282
- const ordinal = item.context.ordinal;
1283
- if (ordinal < expectedOrdinal) continue;
1284
- if (ordinal > expectedOrdinal) break;
1285
- expectedOrdinal++;
1286
- }
1287
- return expectedOrdinal - 1;
1288
- }
1289
- /**
1290
- * Opens a replay at the lowest ordinal still worth reading.
1291
- *
1292
- * {@link replayedThrough} is what keeps a permanently held hole -- a
1293
- * rolled-back insert, which never fills -- from re-reading the whole tail on
1294
- * every batch. It cannot be trusted on its own: a hole that fills without
1295
- * being delivered here is visible only in the index, and a mark that is
1296
- * never questioned would hide that operation for the life of the process.
1297
- * That happens whenever another writer commits the gap (a second reactor on
1298
- * the same database), and it leaves the cursor parked below an operation
1299
- * whose references were never written -- `hasReference` then answers false
1300
- * for an attachment that is genuinely referenced.
1301
- *
1302
- * So a parked batch spends one page probing the gap: the first row above the
1303
- * cursor is the missing ordinal itself once it commits. Finding it drops the
1304
- * mark and replays from the cursor; not finding it leaves the mark standing,
1305
- * which is the cheap path and the common one.
1306
- */
1307
- async replayFrom() {
1308
- const fromCursor = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
1309
- if (this.replayedThrough === void 0) return fromCursor;
1310
- if (fromCursor.results[0]?.context.ordinal === this.lastOrdinal + 1) {
1311
- this.replayedThrough = void 0;
1312
- return fromCursor;
1313
- }
1314
- return this.operationIndex.getSinceOrdinal(this.replayedThrough);
1315
- }
1316
- async loadThroughOrdinal(maxOrdinal) {
1317
- const operations = [];
1318
- let page = await this.replayFrom();
1319
- for (;;) {
1320
- for (const item of page.results) if (item.context.ordinal <= maxOrdinal) operations.push(item);
1321
- if (page.results.some(({ context }) => context.ordinal >= maxOrdinal) || !page.next) break;
1322
- page = await page.next();
1323
- }
1324
- return operations;
1325
- }
1326
- sortAndDedupe(items) {
1327
- const byOrdinal = /* @__PURE__ */ new Map();
1328
- for (const item of items) byOrdinal.set(item.context.ordinal, item);
1329
- return [...byOrdinal.values()].sort((left, right) => left.context.ordinal - right.context.ordinal);
1330
- }
1331
1235
  };
1332
1236
  //#endregion
1333
1237
  //#region src/storage/s3/config.ts
@@ -1341,7 +1245,7 @@ function required(env, name) {
1341
1245
  if (value.trim() !== value) throw new Error(`${name} must not have leading or trailing whitespace`);
1342
1246
  return value;
1343
1247
  }
1344
- const LOOPBACK_HOSTNAMES = new Set([
1248
+ const LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set([
1345
1249
  "127.0.0.1",
1346
1250
  "localhost",
1347
1251
  "[::1]"
@@ -1423,9 +1327,10 @@ function sha256HexToBase64(hash) {
1423
1327
  //#region src/storage/s3/primitives.ts
1424
1328
  const defaultPresigner = (client, command, expiresInSeconds) => getSignedUrl(client, command, {
1425
1329
  expiresIn: expiresInSeconds,
1426
- unhoistableHeaders: new Set(["x-amz-checksum-sha256"])
1330
+ unhoistableHeaders: /* @__PURE__ */ new Set(["x-amz-checksum-sha256"])
1427
1331
  });
1428
1332
  var S3AttachmentPrimitives = class {
1333
+ config;
1429
1334
  client;
1430
1335
  presign;
1431
1336
  now;
@@ -1496,7 +1401,7 @@ function createS3AttachmentPrimitives(config, dependencies = {}) {
1496
1401
  //#endregion
1497
1402
  //#region src/storage/s3/backend.ts
1498
1403
  const READINESS_PROBE_HASH = "0".repeat(64);
1499
- const OBJECT_NOT_FOUND_NAMES = new Set([
1404
+ const OBJECT_NOT_FOUND_NAMES = /* @__PURE__ */ new Set([
1500
1405
  "NotFound",
1501
1406
  "NoSuchKey",
1502
1407
  "NoSuchObject"
@@ -1514,6 +1419,8 @@ function requireHashFirstReservation(reservation) {
1514
1419
  }
1515
1420
  /** Server-only S3 capability. Callers complete authorization before download. */
1516
1421
  var S3AttachmentBackend = class {
1422
+ db;
1423
+ config;
1517
1424
  kind = "s3";
1518
1425
  primitives;
1519
1426
  now;