@powerhousedao/reactor-attachments 6.1.0-dev.16 → 6.1.0-dev.18
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 +635 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +743 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +237 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +495 -92
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["isRecord","contentTypeFallback","isAttachmentMetadata","isRecord","isRecord"],"sources":["../src/errors.ts","../src/ref.ts","../src/attachment-service.ts","../src/switchboard/build-auth-headers.ts","../src/switchboard/switchboard-attachment-transport.ts","../src/switchboard/remote-reservation-store.ts","../src/switchboard/remote-attachment-upload.ts","../src/switchboard/remote-attachment-upload-factory.ts","../src/switchboard/remote-attachment-store.ts","../src/switchboard/create-remote-attachment-service.ts","../src/null-attachment-transport.ts","../src/client.ts"],"sourcesContent":["import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\n\n/**\n * Thrown when an attachment ref or hash is not known to the store.\n */\nexport class AttachmentNotFound extends Error {\n constructor(identifier: string) {\n super(`Attachment not found: ${identifier}`);\n this.name = \"AttachmentNotFound\";\n }\n}\n\n/**\n * Thrown when a reservation ID is not found in the reservation store.\n */\nexport class ReservationNotFound extends Error {\n constructor(reservationId: string) {\n super(`Reservation not found: ${reservationId}`);\n this.name = \"ReservationNotFound\";\n }\n}\n\n/**\n * Thrown when an attachment ref string does not match the expected format.\n */\nexport class InvalidAttachmentRef extends Error {\n constructor(ref: string) {\n super(`Invalid attachment ref: ${ref}`);\n this.name = \"InvalidAttachmentRef\";\n }\n}\n\n/**\n * Thrown when an upload exceeds the configured maximum byte cap.\n * Route handlers should map this to HTTP 413 Payload Too Large.\n */\nexport class UploadTooLarge extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`Upload exceeds maximum size of ${maxBytes} bytes`);\n this.name = \"UploadTooLarge\";\n this.maxBytes = maxBytes;\n }\n}\n\n/**\n * Thrown by reserve() when the claimed hash is already available in the store.\n * The caller should use err.ref directly and upload nothing -- this is the\n * dedup fast path: duplicate content never leaves the client.\n */\nexport class AttachmentAlreadyExists extends Error {\n readonly hash: AttachmentHash;\n readonly ref: AttachmentRef;\n constructor(hash: AttachmentHash, ref: AttachmentRef) {\n super(`Attachment already exists for hash: ${hash}`);\n this.name = \"AttachmentAlreadyExists\";\n this.hash = hash;\n this.ref = ref;\n }\n}\n\n/**\n * Thrown by send() when the server-computed hash of the uploaded bytes\n * does not match the hash claimed at reservation time. Nothing is committed;\n * the reservation is retained so the client can retry with correct bytes.\n */\nexport class HashMismatch extends Error {\n readonly claimed: AttachmentHash;\n readonly actual: AttachmentHash;\n constructor(claimed: AttachmentHash, actual: AttachmentHash) {\n super(`Hash mismatch: claimed ${claimed} but computed ${actual}`);\n this.name = \"HashMismatch\";\n this.claimed = claimed;\n this.actual = actual;\n }\n}\n\n/**\n * Thrown by send() when the uploaded byte count does not equal the\n * sizeBytes declared at reservation time. The handle may reject\n * mid-stream as soon as the count exceeds the declaration.\n * Nothing is committed; the reservation is retained for retry.\n *\n * \"actual\" is the byte count received from the stream before aborting --\n * it includes the chunk that crossed the declaration and can exceed bytes\n * persisted. On mid-stream aborts the true total is unknown; at least\n * \"actual\" bytes were sent.\n */\nexport class SizeMismatch extends Error {\n readonly declared: number;\n readonly actual: number;\n constructor(declared: number, actual: number) {\n super(`Size mismatch: declared ${declared} bytes but received ${actual}`);\n this.name = \"SizeMismatch\";\n this.declared = declared;\n this.actual = actual;\n }\n}\n\n/**\n * Thrown by get() when the hash is reserved by an in-flight upload and\n * bytes are not yet available anywhere. Deliberately NOT a subclass of\n * AttachmentNotFound -- callers must distinguish \"retry later\" from \"unknown\".\n * After expiresAtUtc has passed the hash reads as not found.\n *\n * metadata is populated when the reservation is local and its fields are\n * known (mimeType, fileName, sizeBytes). It is undefined when the pending\n * state is learned from a remote transport that did not supply the full\n * Attachment-Pending header (transport-pending / degraded wire case).\n */\nexport class AttachmentPending extends Error {\n readonly hash: AttachmentHash;\n readonly expiresAtUtc: string;\n readonly metadata:\n | {\n readonly mimeType: string;\n readonly fileName: string;\n readonly sizeBytes: number;\n }\n | undefined;\n\n constructor(\n hash: AttachmentHash,\n expiresAtUtc: string,\n meta?: { mimeType: string; fileName: string; sizeBytes: number },\n ) {\n super(\n `Attachment pending upload for hash: ${hash}, expires: ${expiresAtUtc}`,\n );\n this.name = \"AttachmentPending\";\n this.hash = hash;\n this.expiresAtUtc = expiresAtUtc;\n this.metadata = meta;\n }\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { InvalidAttachmentRef } from \"./errors.js\";\n\nconst REF_PATTERN = /^attachment:\\/\\/v(\\d+):(.+)$/;\nconst DEFAULT_VERSION = 1;\n\nexport type ParsedRef = {\n version: number;\n hash: AttachmentHash;\n};\n\nexport function parseRef(ref: AttachmentRef): ParsedRef {\n const match = REF_PATTERN.exec(ref);\n if (!match) {\n throw new InvalidAttachmentRef(ref);\n }\n return {\n version: Number(match[1]),\n hash: match[2],\n };\n}\n\nexport function createRef(\n hash: AttachmentHash,\n version: number = DEFAULT_VERSION,\n): AttachmentRef {\n return `attachment://v${version}:${hash}`;\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport {\n AttachmentAlreadyExists,\n AttachmentNotFound,\n AttachmentPending,\n} from \"./errors.js\";\nimport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nimport { createRef, parseRef } from \"./ref.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n ReserveAttachmentOptions,\n} from \"./types.js\";\n\nconst CLIENT_HASH_PATTERN = /^[a-f0-9]{64}$/;\n\nexport class AttachmentService implements IAttachmentService {\n constructor(\n private readonly store: IAttachmentReader,\n private readonly reservations: IReservationStore,\n private readonly uploadFactory: IAttachmentUploadFactory,\n ) {}\n\n async reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload> {\n if (options.clientHash !== undefined) {\n return this.reserveHashFirst(options);\n }\n const reservation = await this.reservations.create(options);\n return this.uploadFactory.createUpload(reservation);\n }\n\n async stat(ref: AttachmentRef): Promise<AttachmentHeader> {\n const { hash } = parseRef(ref);\n return this.store.stat(hash);\n }\n\n async get(\n ref: AttachmentRef,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const { hash } = parseRef(ref);\n return this.store.get(hash, signal);\n }\n\n private async reserveHashFirst(\n options: ReserveAttachmentOptions,\n ): Promise<IAttachmentUpload> {\n const normalized = options.clientHash!.toLowerCase() as AttachmentHash;\n if (!CLIENT_HASH_PATTERN.test(normalized)) {\n throw new Error(\n `clientHash must be a 64-character lowercase hex string, got: ${options.clientHash}`,\n );\n }\n if (\n options.sizeBytes === undefined ||\n !Number.isInteger(options.sizeBytes) ||\n options.sizeBytes <= 0 ||\n !Number.isSafeInteger(options.sizeBytes)\n ) {\n throw new Error(\n \"sizeBytes must be a positive safe integer when clientHash is provided\",\n );\n }\n\n const normalizedOptions: ReserveAttachmentOptions = {\n ...options,\n clientHash: normalized,\n };\n\n let existingHeader: AttachmentHeader | null = null;\n try {\n existingHeader = await this.store.stat(normalized);\n } catch (err) {\n if (\n !(err instanceof AttachmentNotFound) &&\n !(err instanceof AttachmentPending)\n ) {\n throw err;\n }\n }\n\n if (existingHeader !== null && existingHeader.status === \"available\") {\n throw new AttachmentAlreadyExists(normalized, createRef(normalized));\n }\n\n const reservation = await this.reservations.create(normalizedOptions);\n return this.uploadFactory.createUpload(reservation);\n }\n}\n","import type { JwtHandler } from \"@powerhousedao/reactor\";\n\nexport async function buildAuthHeaders(\n url: string,\n jwtHandler: JwtHandler | undefined,\n): Promise<Record<string, string>> {\n const headers: Record<string, string> = {};\n if (jwtHandler) {\n const token = await jwtHandler(url);\n if (token) {\n headers[\"Authorization\"] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport type { AttachmentMetadata, TransportFetchResult } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardTransportConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nexport class SwitchboardAttachmentTransport implements IAttachmentTransport {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardTransportConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = (config.fetchFn ?? globalThis.fetch).bind(globalThis);\n }\n\n async fetch(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<TransportFetchResult> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 202) {\n const expiresAtUtc = this.parsePendingExpiry(response);\n if (!expiresAtUtc) {\n throw new Error(\n \"Attachment fetch returned 202 with missing or malformed Attachment-Pending header\",\n );\n }\n const retryAfterMs = parseRetryAfterMs(response);\n return { kind: \"pending\", hash, expiresAtUtc, retryAfterMs };\n }\n\n if (response.status === 404) {\n return { kind: \"not-found\" };\n }\n\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = this.parseMetadataHeaders(response);\n const body = response.body;\n if (!body) {\n throw new Error(\"Response body is null\");\n }\n\n return { kind: \"data\", response: { hash, metadata, body } };\n }\n\n async announce(_hash: AttachmentHash): Promise<void> {\n // No-op for switchboard -- data is already on the server after upload.\n }\n\n async push(\n hash: AttachmentHash,\n remote: string,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n const url = `${remote}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n body: data,\n headers,\n // @ts-expect-error Node fetch requires duplex for streaming request bodies\n duplex: \"half\",\n });\n\n if (!response.ok) {\n throw new Error(\n `Attachment push failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n private parsePendingExpiry(response: Response): string | null {\n const header = response.headers.get(\"Attachment-Pending\");\n if (!header) return null;\n try {\n const parsed: unknown = JSON.parse(header);\n if (!isRecord(parsed)) return null;\n if (typeof parsed.expiresAtUtc !== \"string\") return null;\n return parsed.expiresAtUtc;\n } catch {\n return null;\n }\n }\n\n private parseMetadataHeaders(response: Response): AttachmentMetadata {\n // Compute the fallback at most once; both the recovery path inside the\n // header parser and the outer \"no header / parse failed\" path share it.\n let fallbackCache: AttachmentMetadata | undefined;\n const fallback = (): AttachmentMetadata => {\n if (fallbackCache === undefined) {\n fallbackCache = contentTypeFallback(response);\n }\n return fallbackCache;\n };\n\n const metaHeader = response.headers.get(\"Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed)) {\n if (parsed.extension === undefined) {\n parsed.extension = null;\n }\n if (parsed.createdAtUtc === undefined) {\n parsed.createdAtUtc = fallback().createdAtUtc;\n }\n if (parsed.lastAccessedAtUtc === undefined) {\n parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;\n }\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return fallback();\n }\n}\n\nconst DEFAULT_RETRY_AFTER_MS = 5000;\n\nfunction parseRetryAfterMs(response: Response): number {\n const retryAfter = response.headers.get(\"Retry-After\");\n if (!retryAfter) return DEFAULT_RETRY_AFTER_MS;\n const seconds = Number(retryAfter);\n if (!Number.isFinite(seconds) || seconds < 0) return DEFAULT_RETRY_AFTER_MS;\n return Math.round(seconds * 1000);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n if (typeof value.createdAtUtc !== \"string\") return false;\n if (\n value.lastAccessedAtUtc !== undefined &&\n typeof value.lastAccessedAtUtc !== \"string\"\n ) {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when Attachment-Metadata is absent. If that's missing too,\n // fall back to the response Date header (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n lastAccessedAtUtc: createdAtUtc,\n };\n}\n","import type { AttachmentRef, JwtHandler } from \"@powerhousedao/reactor\";\nimport { AttachmentAlreadyExists, ReservationNotFound } from \"../errors.js\";\nimport type { IReservationStore } from \"../interfaces.js\";\nimport { createRef, parseRef } from \"../ref.js\";\nimport type { Reservation, ReserveAttachmentOptions } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\n\nexport type SwitchboardClientConfig = {\n remoteUrl: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction deriveExtension(fileName: string): string | null {\n const idx = fileName.lastIndexOf(\".\");\n if (idx <= 0 || idx === fileName.length - 1) return null;\n return fileName.slice(idx + 1).toLowerCase();\n}\n\nfunction isReservationBase(value: unknown): value is Record<string, unknown> & {\n reservationId: string;\n mimeType: string;\n fileName: string;\n extension: string | null;\n createdAtUtc: string;\n expiresAtUtc: string;\n} {\n if (!isRecord(value)) return false;\n if (typeof value.reservationId !== \"string\") return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n if (typeof value.createdAtUtc !== \"string\") return false;\n if (typeof value.expiresAtUtc !== \"string\") return false;\n return true;\n}\n\nfunction isReservation(value: unknown): value is Reservation {\n if (!isReservationBase(value)) return false;\n // clientHash and sizeBytes may be absent on responses from older\n // switchboards; treat missing as null (normalized below in get()).\n if (\n value.clientHash !== undefined &&\n value.clientHash !== null &&\n typeof value.clientHash !== \"string\"\n ) {\n return false;\n }\n if (\n value.sizeBytes !== undefined &&\n value.sizeBytes !== null &&\n typeof value.sizeBytes !== \"number\"\n ) {\n return false;\n }\n return true;\n}\n\nexport class RemoteReservationStore implements IReservationStore {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = (config.fetchFn ?? globalThis.fetch).bind(globalThis);\n }\n\n async create(options: ReserveAttachmentOptions): Promise<Reservation> {\n const url = `${this.remoteUrl}/attachments/reservations`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n const extension = options.extension ?? deriveExtension(options.fileName);\n\n const bodyObj: Record<string, unknown> = {\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension,\n };\n if (options.clientHash !== undefined) {\n bodyObj.clientHash = options.clientHash;\n }\n if (options.sizeBytes !== undefined) {\n bodyObj.sizeBytes = options.sizeBytes;\n }\n\n const response = await this.fetchFn(url, {\n method: \"POST\",\n headers: { ...authHeaders, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(bodyObj),\n });\n\n if (response.status === 409) {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n throw new Error(\n `Reservation create failed: ${response.status} ${response.statusText}`,\n );\n }\n if (\n isRecord(body) &&\n body.error === \"already_exists\" &&\n options.clientHash !== undefined\n ) {\n let ref: AttachmentRef;\n if (typeof body.ref === \"string\") {\n try {\n parseRef(body.ref as AttachmentRef);\n ref = body.ref as AttachmentRef;\n } catch {\n ref = createRef(options.clientHash);\n }\n } else {\n ref = createRef(options.clientHash);\n }\n throw new AttachmentAlreadyExists(options.clientHash, ref);\n }\n throw new Error(\n `Reservation create failed: ${response.status} ${response.statusText}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Reservation create failed: ${response.status} ${response.statusText}`,\n );\n }\n\n let json: unknown;\n try {\n json = await response.json();\n } catch {\n throw new Error(\"Reservation create returned non-JSON response\");\n }\n if (\n typeof json !== \"object\" ||\n json === null ||\n typeof (json as Record<string, unknown>).reservationId !== \"string\" ||\n ((json as Record<string, unknown>).reservationId as string).length === 0\n ) {\n throw new Error(\n \"Reservation create returned a payload missing a non-empty reservationId string\",\n );\n }\n const body = json as {\n reservationId: string;\n ref?: string | null;\n createdAtUtc?: string;\n expiresAtUtc?: string;\n };\n // The server is the source of truth for both timestamps. We synthesize\n // only as a last-resort fallback for older switchboards that don't\n // include them in the response; in that case the client cannot know the\n // server's TTL, so expiresAtUtc is a best-effort placeholder.\n const now = new Date();\n return {\n reservationId: body.reservationId,\n mimeType: options.mimeType,\n fileName: options.fileName,\n extension,\n createdAtUtc: body.createdAtUtc ?? now.toISOString(),\n expiresAtUtc:\n body.expiresAtUtc ??\n new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),\n clientHash: options.clientHash ?? null,\n sizeBytes: options.sizeBytes ?? null,\n };\n }\n\n async get(reservationId: string): Promise<Reservation> {\n const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { headers: authHeaders });\n\n if (response.status === 404) {\n throw new ReservationNotFound(reservationId);\n }\n if (!response.ok) {\n throw new Error(\n `Reservation get failed: ${response.status} ${response.statusText}`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch {\n throw new Error(\"Reservation get returned non-JSON response\");\n }\n if (!isReservation(parsed)) {\n throw new Error(\n \"Reservation get returned a payload that does not match the Reservation shape\",\n );\n }\n return {\n reservationId: parsed.reservationId,\n mimeType: parsed.mimeType,\n fileName: parsed.fileName,\n extension: parsed.extension,\n createdAtUtc: parsed.createdAtUtc,\n expiresAtUtc: parsed.expiresAtUtc,\n // Normalize fields that may be absent on older switchboard responses.\n clientHash: parsed.clientHash ?? null,\n sizeBytes: parsed.sizeBytes ?? null,\n };\n }\n\n async delete(reservationId: string): Promise<void> {\n const url = `${this.remoteUrl}/attachments/reservations/${encodeURIComponent(reservationId)}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"DELETE\",\n headers: authHeaders,\n });\n\n // 2xx = success; 404 / 410 = already gone, treat as idempotent success.\n if (!response.ok && response.status !== 404 && response.status !== 410) {\n throw new Error(\n `Reservation delete failed: ${response.status} ${response.statusText}`,\n );\n }\n }\n\n // Sweeping is the server's responsibility; clients have no authority to\n // delete reservations on a remote switchboard.\n deleteExpired(): Promise<number> {\n return Promise.reject(\n new Error(\"RemoteReservationStore.deleteExpired is not supported\"),\n );\n }\n}\n","import type { AttachmentRef, JwtHandler } from \"@powerhousedao/reactor\";\nimport { HashMismatch, SizeMismatch } from \"../errors.js\";\nimport type { IAttachmentUpload } from \"../interfaces.js\";\nimport { createRef } from \"../ref.js\";\nimport type { AttachmentUploadResult, Reservation } from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class RemoteAttachmentUpload implements IAttachmentUpload {\n readonly reservationId: string;\n readonly ref: AttachmentRef | null;\n readonly expiresAtUtc: string;\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(reservation: Reservation, config: SwitchboardClientConfig) {\n this.reservationId = reservation.reservationId;\n this.ref =\n reservation.clientHash !== null\n ? createRef(reservation.clientHash)\n : null;\n this.expiresAtUtc = reservation.expiresAtUtc;\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = (config.fetchFn ?? globalThis.fetch).bind(globalThis);\n }\n\n async send(\n data: ReadableStream<Uint8Array>,\n ): Promise<AttachmentUploadResult> {\n const url = `${this.remoteUrl}/attachments/reservations/${this.reservationId}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n // Buffer the stream to a Blob. Streaming request bodies aren't universally\n // supported in browsers (Firefox stringifies the stream to \"[object\n // ReadableStream]\" even with duplex: \"half\"); buffering is the only\n // portable option for attachment uploads.\n const body = await new Response(data).blob();\n\n // Always upload as octet-stream. The server reads the real mime type from\n // the reservation row; sending the user's mime type here (e.g. application/json)\n // would let Express body-parser drain the request body before our handler runs,\n // silently writing zero bytes.\n const response = await this.fetchFn(url, {\n method: \"PUT\",\n headers: { ...authHeaders, \"Content-Type\": \"application/octet-stream\" },\n body,\n });\n\n if (response.status === 422) {\n let errorBody: unknown;\n try {\n errorBody = await response.json();\n } catch {\n throw new Error(\n `Attachment upload failed: ${response.status} ${response.statusText}`,\n );\n }\n if (isRecord(errorBody)) {\n if (\n errorBody.error === \"hash_mismatch\" &&\n typeof errorBody.claimed === \"string\" &&\n typeof errorBody.actual === \"string\"\n ) {\n throw new HashMismatch(errorBody.claimed, errorBody.actual);\n }\n if (\n errorBody.error === \"size_mismatch\" &&\n typeof errorBody.declared === \"number\" &&\n typeof errorBody.actual === \"number\"\n ) {\n throw new SizeMismatch(errorBody.declared, errorBody.actual);\n }\n }\n throw new Error(\n `Attachment upload failed: ${response.status} ${response.statusText}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Attachment upload failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return (await response.json()) as AttachmentUploadResult;\n }\n}\n","import type {\n IAttachmentUpload,\n IAttachmentUploadFactory,\n} from \"../interfaces.js\";\nimport type { Reservation } from \"../types.js\";\nimport { RemoteAttachmentUpload } from \"./remote-attachment-upload.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nexport class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {\n constructor(private readonly config: SwitchboardClientConfig) {}\n\n createUpload(reservation: Reservation): IAttachmentUpload {\n return new RemoteAttachmentUpload(reservation, this.config);\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { JwtHandler } from \"@powerhousedao/reactor\";\nimport { AttachmentNotFound, AttachmentPending } from \"../errors.js\";\nimport type { IAttachmentReader } from \"../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n} from \"../types.js\";\nimport { buildAuthHeaders } from \"./build-auth-headers.js\";\nimport type { SwitchboardClientConfig } from \"./remote-reservation-store.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isAttachmentMetadata(value: unknown): value is AttachmentMetadata {\n if (!isRecord(value)) return false;\n if (typeof value.mimeType !== \"string\") return false;\n if (typeof value.fileName !== \"string\") return false;\n if (\n typeof value.sizeBytes !== \"number\" ||\n !Number.isFinite(value.sizeBytes) ||\n value.sizeBytes < 0\n ) {\n return false;\n }\n if (value.extension !== null && typeof value.extension !== \"string\") {\n return false;\n }\n if (typeof value.createdAtUtc !== \"string\") return false;\n if (\n value.lastAccessedAtUtc !== undefined &&\n typeof value.lastAccessedAtUtc !== \"string\"\n ) {\n return false;\n }\n return true;\n}\n\nfunction contentTypeFallback(response: Response): AttachmentMetadata {\n const contentLength = response.headers.get(\"Content-Length\");\n if (contentLength === null) {\n throw new Error(\n \"Switchboard response missing both Attachment-Metadata and Content-Length headers\",\n );\n }\n const sizeBytes = Number(contentLength);\n if (!Number.isInteger(sizeBytes) || sizeBytes < 0) {\n throw new Error(\n `Switchboard response has invalid Content-Length header: ${JSON.stringify(contentLength)}`,\n );\n }\n // Last-Modified is the closest legitimate signal we have for an original\n // creation time when Attachment-Metadata is absent. If that's missing too,\n // fall back to the response date (still server-attributed). This is\n // imperfect — Last-Modified reflects the most recent change, not the\n // original upload — but unlike sizeBytes there is no zero-equivalent\n // sentinel for a date, and downstream consumers expect a value.\n const lastModified = response.headers.get(\"Last-Modified\");\n const dateHeader = response.headers.get(\"Date\");\n const createdAtUtc = lastModified\n ? new Date(lastModified).toISOString()\n : dateHeader\n ? new Date(dateHeader).toISOString()\n : new Date().toISOString();\n\n return {\n // application/octet-stream is the RFC 2046 sentinel for \"unknown binary\",\n // and \"unknown\" is a non-real filename sentinel; neither is a fabricated\n // semantic value the way Content-Length=0 would be.\n mimeType:\n response.headers.get(\"Content-Type\") ?? \"application/octet-stream\",\n fileName: \"unknown\",\n sizeBytes,\n extension: null,\n createdAtUtc,\n lastAccessedAtUtc: createdAtUtc,\n };\n}\n\nfunction parseMetadata(response: Response): AttachmentMetadata {\n // Compute the fallback at most once; both the recovery path inside the\n // header parser and the outer \"no header / parse failed\" path share it.\n let fallbackCache: AttachmentMetadata | undefined;\n const fallback = (): AttachmentMetadata => {\n if (fallbackCache === undefined) {\n fallbackCache = contentTypeFallback(response);\n }\n return fallbackCache;\n };\n\n const metaHeader = response.headers.get(\"Attachment-Metadata\");\n if (metaHeader) {\n try {\n const parsed: unknown = JSON.parse(metaHeader);\n if (isRecord(parsed)) {\n if (parsed.extension === undefined) {\n parsed.extension = null;\n }\n // Older switchboards may omit these timestamps; fall back to the\n // Date/Last-Modified header so we never produce client-clock-stamped\n // values when the server has authority.\n if (parsed.createdAtUtc === undefined) {\n parsed.createdAtUtc = fallback().createdAtUtc;\n }\n if (parsed.lastAccessedAtUtc === undefined) {\n parsed.lastAccessedAtUtc = fallback().lastAccessedAtUtc;\n }\n }\n if (isAttachmentMetadata(parsed)) {\n return parsed;\n }\n } catch {\n // fall through to Content-Type fallback\n }\n }\n return fallback();\n}\n\ntype PendingInfo = {\n expiresAtUtc: string;\n mimeType: string;\n fileName: string;\n sizeBytes: number;\n};\n\ntype PartialPendingInfo = {\n expiresAtUtc: string;\n mimeType?: string;\n fileName?: string;\n sizeBytes?: number;\n};\n\nfunction parsePendingExpiry(response: Response): PartialPendingInfo | null {\n const header = response.headers.get(\"Attachment-Pending\");\n if (!header) return null;\n try {\n const parsed: unknown = JSON.parse(header);\n if (!isRecord(parsed)) return null;\n if (typeof parsed.expiresAtUtc !== \"string\") return null;\n const result: PartialPendingInfo = { expiresAtUtc: parsed.expiresAtUtc };\n if (typeof parsed.mimeType === \"string\") result.mimeType = parsed.mimeType;\n if (typeof parsed.fileName === \"string\") result.fileName = parsed.fileName;\n if (\n typeof parsed.sizeBytes === \"number\" &&\n Number.isFinite(parsed.sizeBytes) &&\n parsed.sizeBytes >= 0\n ) {\n result.sizeBytes = parsed.sizeBytes;\n }\n return result;\n } catch {\n return null;\n }\n}\n\nfunction parsePendingHeader(response: Response): PendingInfo | null {\n const partial = parsePendingExpiry(response);\n if (!partial) return null;\n if (\n typeof partial.mimeType !== \"string\" ||\n typeof partial.fileName !== \"string\" ||\n partial.sizeBytes === undefined\n ) {\n return null;\n }\n return {\n expiresAtUtc: partial.expiresAtUtc,\n mimeType: partial.mimeType,\n fileName: partial.fileName,\n sizeBytes: partial.sizeBytes,\n };\n}\n\nexport class RemoteAttachmentStore implements IAttachmentReader {\n private readonly remoteUrl: string;\n private readonly jwtHandler?: JwtHandler;\n private readonly fetchFn: typeof fetch;\n\n constructor(config: SwitchboardClientConfig) {\n this.remoteUrl = config.remoteUrl;\n this.jwtHandler = config.jwtHandler;\n this.fetchFn = (config.fetchFn ?? globalThis.fetch).bind(globalThis);\n }\n\n /**\n * Get attachment metadata. Normally returns a pending AttachmentHeader\n * (status: 'pending') when the server responds 202 with a full\n * Attachment-Pending header. When only expiresAtUtc is present in the\n * header (degraded wire), throws AttachmentPending instead -- the\n * AttachmentPending throw is the degraded-wire case.\n */\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const authHeaders = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, {\n method: \"HEAD\",\n headers: authHeaders,\n });\n\n if (response.status === 202) {\n const fullPending = parsePendingHeader(response);\n if (fullPending) {\n return buildPendingHeader(hash, fullPending);\n }\n const partial = parsePendingExpiry(response);\n if (partial) {\n throw new AttachmentPending(hash, partial.expiresAtUtc);\n }\n throw new Error(\n \"Attachment stat returned 202 with missing or malformed Attachment-Pending header\",\n );\n }\n\n if (response.status === 404) {\n throw new AttachmentNotFound(hash);\n }\n if (!response.ok) {\n throw new Error(\n `Attachment stat failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const metadata = parseMetadata(response);\n return buildHeader(hash, metadata);\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n return this.fetchAttachment(hash, signal);\n }\n\n private async fetchAttachment(\n hash: AttachmentHash,\n signal?: AbortSignal,\n ): Promise<AttachmentResponse> {\n const url = `${this.remoteUrl}/attachments/${hash}`;\n const headers = await buildAuthHeaders(url, this.jwtHandler);\n\n const response = await this.fetchFn(url, { signal, headers });\n\n if (response.status === 202) {\n const pending = parsePendingExpiry(response);\n if (!pending) {\n throw new Error(\n \"Attachment fetch returned 202 with missing or malformed Attachment-Pending header\",\n );\n }\n throw new AttachmentPending(hash, pending.expiresAtUtc);\n }\n\n if (response.status === 404) {\n throw new AttachmentNotFound(hash);\n }\n if (!response.ok) {\n throw new Error(\n `Attachment fetch failed: ${response.status} ${response.statusText}`,\n );\n }\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n const metadata = parseMetadata(response);\n return { header: buildHeader(hash, metadata), body: response.body };\n }\n}\n\nfunction buildHeader(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n): AttachmentHeader {\n return {\n hash,\n mimeType: metadata.mimeType,\n fileName: metadata.fileName,\n sizeBytes: metadata.sizeBytes,\n extension: metadata.extension,\n status: \"available\",\n source: \"sync\",\n createdAtUtc: metadata.createdAtUtc,\n lastAccessedAtUtc: metadata.lastAccessedAtUtc ?? metadata.createdAtUtc,\n expiresAtUtc: null,\n };\n}\n\nfunction buildPendingHeader(\n hash: AttachmentHash,\n pending: PendingInfo,\n): AttachmentHeader {\n const now = new Date().toISOString();\n return {\n hash,\n mimeType: pending.mimeType,\n fileName: pending.fileName,\n sizeBytes: pending.sizeBytes,\n extension: null,\n status: \"pending\",\n source: \"sync\",\n createdAtUtc: now,\n lastAccessedAtUtc: now,\n expiresAtUtc: pending.expiresAtUtc,\n };\n}\n","import { AttachmentService } from \"../attachment-service.js\";\nimport type { IAttachmentService } from \"../interfaces.js\";\nimport { RemoteAttachmentStore } from \"./remote-attachment-store.js\";\nimport { RemoteAttachmentUploadFactory } from \"./remote-attachment-upload-factory.js\";\nimport {\n RemoteReservationStore,\n type SwitchboardClientConfig,\n} from \"./remote-reservation-store.js\";\n\nexport function createRemoteAttachmentService(\n config: SwitchboardClientConfig,\n): IAttachmentService {\n const reservations = new RemoteReservationStore(config);\n const uploadFactory = new RemoteAttachmentUploadFactory(config);\n const store = new RemoteAttachmentStore(config);\n return new AttachmentService(store, reservations, uploadFactory);\n}\n","import type { IAttachmentTransport } from \"./interfaces.js\";\nimport type { TransportFetchResult } from \"./types.js\";\n\n/**\n * No-op transport for deployments without remote sync.\n * fetch() always returns not-found, announce() and push() are no-ops.\n */\nexport class NullAttachmentTransport implements IAttachmentTransport {\n fetch(): Promise<TransportFetchResult> {\n return Promise.resolve({ kind: \"not-found\" });\n }\n\n announce(): Promise<void> {\n return Promise.resolve();\n }\n\n push(): Promise<void> {\n return Promise.resolve();\n }\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { AttachmentAlreadyExists } from \"./errors.js\";\nimport type { IAttachmentService, IAttachmentUpload } from \"./interfaces.js\";\nimport { createRef } from \"./ref.js\";\nimport type {\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n} from \"./types.js\";\n\nexport { AttachmentService } from \"./attachment-service.js\";\nexport {\n AttachmentAlreadyExists,\n AttachmentNotFound,\n AttachmentPending,\n HashMismatch,\n InvalidAttachmentRef,\n ReservationNotFound,\n SizeMismatch,\n UploadTooLarge,\n} from \"./errors.js\";\nexport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentStore,\n IAttachmentTransport,\n IAttachmentTransportFactory,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nexport { parseRef, createRef } from \"./ref.js\";\nexport type { ParsedRef } from \"./ref.js\";\nexport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n AttachmentTransportConfig,\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n UploadFirstReserveAttachmentOptions,\n Reservation,\n ReserveAttachmentOptions,\n TransportFetchResult,\n TransportResponse,\n} from \"./types.js\";\nexport {\n SwitchboardAttachmentTransport,\n type SwitchboardTransportConfig,\n RemoteReservationStore,\n type SwitchboardClientConfig,\n RemoteAttachmentUpload,\n RemoteAttachmentUploadFactory,\n RemoteAttachmentStore,\n createRemoteAttachmentService,\n} from \"./switchboard/index.js\";\nexport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\n\nexport type PreprocessResult = {\n ref: AttachmentRef;\n hash: AttachmentHash;\n sizeBytes: number;\n options: HashFirstReserveAttachmentOptions;\n data: ReadableStream<Uint8Array>;\n stream: () => ReadableStream<Uint8Array>;\n};\n\nexport interface IAttachmentClient {\n preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult>;\n reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult>;\n}\n\nfunction streamFromBuffer(buf: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buf);\n controller.close();\n },\n });\n}\n\nclass AttachmentClientImpl implements IAttachmentClient {\n constructor(private readonly service: IAttachmentService) {}\n\n async preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult> {\n const buf = await file.arrayBuffer();\n const bytes = new Uint8Array(buf);\n const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", bytes);\n const hash = Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\") as AttachmentHash;\n const ref = createRef(hash);\n const sizeBytes = file.size;\n const mimeType = opts?.mimeType ?? file.type;\n const fileName =\n opts?.fileName ?? (file instanceof File ? file.name : \"attachment\");\n const options: HashFirstReserveAttachmentOptions = {\n mimeType,\n fileName,\n clientHash: hash,\n sizeBytes,\n };\n const data = streamFromBuffer(bytes);\n const stream = (): ReadableStream<Uint8Array> => streamFromBuffer(bytes);\n return { ref, hash, sizeBytes, options, data, stream };\n }\n\n async reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult> {\n let handle: IAttachmentUpload;\n try {\n handle = await this.service.reserve(options);\n } catch (err) {\n if (err instanceof AttachmentAlreadyExists) {\n const header = await this.service.stat(err.ref);\n return { hash: err.hash, ref: err.ref, header };\n }\n throw err;\n }\n return send(handle);\n }\n}\n\nexport function createAttachmentClient(\n service: IAttachmentService,\n): IAttachmentClient {\n return new AttachmentClientImpl(service);\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,15 +20,88 @@ declare class ReservationNotFound extends Error {
|
|
|
20
20
|
declare class InvalidAttachmentRef extends Error {
|
|
21
21
|
constructor(ref: string);
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when an upload exceeds the configured maximum byte cap.
|
|
25
|
+
* Route handlers should map this to HTTP 413 Payload Too Large.
|
|
26
|
+
*/
|
|
27
|
+
declare class UploadTooLarge extends Error {
|
|
28
|
+
readonly maxBytes: number;
|
|
29
|
+
constructor(maxBytes: number);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Thrown by reserve() when the claimed hash is already available in the store.
|
|
33
|
+
* The caller should use err.ref directly and upload nothing -- this is the
|
|
34
|
+
* dedup fast path: duplicate content never leaves the client.
|
|
35
|
+
*/
|
|
36
|
+
declare class AttachmentAlreadyExists extends Error {
|
|
37
|
+
readonly hash: AttachmentHash;
|
|
38
|
+
readonly ref: AttachmentRef;
|
|
39
|
+
constructor(hash: AttachmentHash, ref: AttachmentRef);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by send() when the server-computed hash of the uploaded bytes
|
|
43
|
+
* does not match the hash claimed at reservation time. Nothing is committed;
|
|
44
|
+
* the reservation is retained so the client can retry with correct bytes.
|
|
45
|
+
*/
|
|
46
|
+
declare class HashMismatch extends Error {
|
|
47
|
+
readonly claimed: AttachmentHash;
|
|
48
|
+
readonly actual: AttachmentHash;
|
|
49
|
+
constructor(claimed: AttachmentHash, actual: AttachmentHash);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Thrown by send() when the uploaded byte count does not equal the
|
|
53
|
+
* sizeBytes declared at reservation time. The handle may reject
|
|
54
|
+
* mid-stream as soon as the count exceeds the declaration.
|
|
55
|
+
* Nothing is committed; the reservation is retained for retry.
|
|
56
|
+
*
|
|
57
|
+
* "actual" is the byte count received from the stream before aborting --
|
|
58
|
+
* it includes the chunk that crossed the declaration and can exceed bytes
|
|
59
|
+
* persisted. On mid-stream aborts the true total is unknown; at least
|
|
60
|
+
* "actual" bytes were sent.
|
|
61
|
+
*/
|
|
62
|
+
declare class SizeMismatch extends Error {
|
|
63
|
+
readonly declared: number;
|
|
64
|
+
readonly actual: number;
|
|
65
|
+
constructor(declared: number, actual: number);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Thrown by get() when the hash is reserved by an in-flight upload and
|
|
69
|
+
* bytes are not yet available anywhere. Deliberately NOT a subclass of
|
|
70
|
+
* AttachmentNotFound -- callers must distinguish "retry later" from "unknown".
|
|
71
|
+
* After expiresAtUtc has passed the hash reads as not found.
|
|
72
|
+
*
|
|
73
|
+
* metadata is populated when the reservation is local and its fields are
|
|
74
|
+
* known (mimeType, fileName, sizeBytes). It is undefined when the pending
|
|
75
|
+
* state is learned from a remote transport that did not supply the full
|
|
76
|
+
* Attachment-Pending header (transport-pending / degraded wire case).
|
|
77
|
+
*/
|
|
78
|
+
declare class AttachmentPending extends Error {
|
|
79
|
+
readonly hash: AttachmentHash;
|
|
80
|
+
readonly expiresAtUtc: string;
|
|
81
|
+
readonly metadata: {
|
|
82
|
+
readonly mimeType: string;
|
|
83
|
+
readonly fileName: string;
|
|
84
|
+
readonly sizeBytes: number;
|
|
85
|
+
} | undefined;
|
|
86
|
+
constructor(hash: AttachmentHash, expiresAtUtc: string, meta?: {
|
|
87
|
+
mimeType: string;
|
|
88
|
+
fileName: string;
|
|
89
|
+
sizeBytes: number;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
23
92
|
//#endregion
|
|
24
93
|
//#region src/types.d.ts
|
|
25
94
|
/**
|
|
26
95
|
* Status of attachment data in the local store.
|
|
96
|
+
* 'pending' is a virtual status synthesized at query time from live,
|
|
97
|
+
* hash-bearing reservations -- it never appears in the attachment table.
|
|
27
98
|
*/
|
|
28
|
-
type AttachmentStatus = "available" | "evicted";
|
|
99
|
+
type AttachmentStatus = "available" | "evicted" | "pending";
|
|
29
100
|
/**
|
|
30
|
-
* Metadata about an attachment.
|
|
31
|
-
*
|
|
101
|
+
* Metadata about an attachment. For committed attachments (available/evicted),
|
|
102
|
+
* expiresAtUtc is null. For pending attachments synthesized from a live
|
|
103
|
+
* reservation, expiresAtUtc carries the reservation expiry so callers can
|
|
104
|
+
* emit Retry-After or bound polling loops.
|
|
32
105
|
*/
|
|
33
106
|
type AttachmentHeader = {
|
|
34
107
|
hash: AttachmentHash;
|
|
@@ -40,6 +113,7 @@ type AttachmentHeader = {
|
|
|
40
113
|
source: "local" | "sync";
|
|
41
114
|
createdAtUtc: string;
|
|
42
115
|
lastAccessedAtUtc: string;
|
|
116
|
+
expiresAtUtc: string | null;
|
|
43
117
|
};
|
|
44
118
|
/**
|
|
45
119
|
* Metadata provided alongside attachment data during sync, and returned
|
|
@@ -69,13 +143,49 @@ type AttachmentMetadata = {
|
|
|
69
143
|
lastAccessedAtUtc?: string;
|
|
70
144
|
};
|
|
71
145
|
/**
|
|
72
|
-
*
|
|
146
|
+
* Upload-first reservation. clientHash and sizeBytes are absent;
|
|
147
|
+
* the ref is only known after send() completes.
|
|
148
|
+
*/
|
|
149
|
+
type UploadFirstReserveAttachmentOptions = {
|
|
150
|
+
mimeType: string;
|
|
151
|
+
fileName: string;
|
|
152
|
+
extension?: string | null;
|
|
153
|
+
clientHash?: undefined;
|
|
154
|
+
sizeBytes?: undefined;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Hash-first reservation. clientHash is present and sizeBytes is required.
|
|
158
|
+
* The ref is known at reservation time; send() verifies the uploaded bytes
|
|
159
|
+
* against the claimed hash and declared size. The explicit "?: undefined"
|
|
160
|
+
* on UploadFirstReserveAttachmentOptions makes clientHash a narrowing discriminant:
|
|
161
|
+
* checking options.clientHash !== undefined narrows sizeBytes to number.
|
|
73
162
|
*/
|
|
74
|
-
type
|
|
163
|
+
type HashFirstReserveAttachmentOptions = {
|
|
75
164
|
mimeType: string;
|
|
76
165
|
fileName: string;
|
|
77
166
|
extension?: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* Content hash claimed by the client (lowercase SHA-256 hex).
|
|
169
|
+
*/
|
|
170
|
+
clientHash: AttachmentHash;
|
|
171
|
+
/**
|
|
172
|
+
* Declared size in bytes. Required when clientHash is present.
|
|
173
|
+
* Reported by stat() during the pending window and enforced on
|
|
174
|
+
* ingest: an upload whose actual byte count differs is rejected.
|
|
175
|
+
*/
|
|
176
|
+
sizeBytes: number;
|
|
78
177
|
};
|
|
178
|
+
/**
|
|
179
|
+
* Options provided when reserving an attachment slot.
|
|
180
|
+
*
|
|
181
|
+
* Use HashFirstReserveAttachmentOptions when clientHash is known up front;
|
|
182
|
+
* the service then operates in hash-first mode: reserve() rejects if the
|
|
183
|
+
* content is already available, and send() verifies the uploaded bytes.
|
|
184
|
+
*
|
|
185
|
+
* Use UploadFirstReserveAttachmentOptions (or omit clientHash) for the
|
|
186
|
+
* upload-first flow where the ref is only known after send() completes.
|
|
187
|
+
*/
|
|
188
|
+
type ReserveAttachmentOptions = UploadFirstReserveAttachmentOptions | HashFirstReserveAttachmentOptions;
|
|
79
189
|
/**
|
|
80
190
|
* Result of uploading attachment data through a handle.
|
|
81
191
|
*/
|
|
@@ -102,6 +212,23 @@ type TransportResponse = {
|
|
|
102
212
|
metadata: AttachmentMetadata;
|
|
103
213
|
body: ReadableStream<Uint8Array>;
|
|
104
214
|
};
|
|
215
|
+
/**
|
|
216
|
+
* Three-way result from IAttachmentTransport.fetch(). Replaces
|
|
217
|
+
* TransportResponse | null to make the pending state explicit, so
|
|
218
|
+
* peers receiving a synced operation whose attachment is in-flight can
|
|
219
|
+
* distinguish "retry later" from "permanently unknown".
|
|
220
|
+
*/
|
|
221
|
+
type TransportFetchResult = {
|
|
222
|
+
kind: "data";
|
|
223
|
+
response: TransportResponse;
|
|
224
|
+
} | {
|
|
225
|
+
kind: "pending";
|
|
226
|
+
hash: AttachmentHash;
|
|
227
|
+
expiresAtUtc: string;
|
|
228
|
+
retryAfterMs: number;
|
|
229
|
+
} | {
|
|
230
|
+
kind: "not-found";
|
|
231
|
+
};
|
|
105
232
|
/**
|
|
106
233
|
* Configuration for creating an attachment transport instance.
|
|
107
234
|
*/
|
|
@@ -113,6 +240,8 @@ type AttachmentTransportConfig = {
|
|
|
113
240
|
* A reservation for an in-progress attachment upload.
|
|
114
241
|
* Created by reserve(), deleted when upload.send() completes or
|
|
115
242
|
* once expiresAtUtc has passed and a sweep runs.
|
|
243
|
+
* clientHash and sizeBytes are set in hash-first mode and null in
|
|
244
|
+
* upload-first mode.
|
|
116
245
|
*/
|
|
117
246
|
type Reservation = {
|
|
118
247
|
reservationId: string;
|
|
@@ -121,6 +250,8 @@ type Reservation = {
|
|
|
121
250
|
extension: string | null;
|
|
122
251
|
createdAtUtc: string;
|
|
123
252
|
expiresAtUtc: string;
|
|
253
|
+
clientHash: string | null;
|
|
254
|
+
sizeBytes: number | null;
|
|
124
255
|
};
|
|
125
256
|
//#endregion
|
|
126
257
|
//#region src/interfaces.d.ts
|
|
@@ -132,22 +263,43 @@ interface IAttachmentService {
|
|
|
132
263
|
/**
|
|
133
264
|
* Reserve a new attachment slot and return an upload handle.
|
|
134
265
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
266
|
+
* When options.clientHash is provided (hash-first mode):
|
|
267
|
+
* - @throws AttachmentAlreadyExists if data for that hash is currently
|
|
268
|
+
* available. The error carries the canonical ref; the caller uses it
|
|
269
|
+
* directly and uploads nothing (dedup fast path).
|
|
270
|
+
* - If the hash is evicted, the reservation is created: the client holds
|
|
271
|
+
* the bytes and the upload restores them.
|
|
272
|
+
* - If the hash is pending (another in-flight reservation), the
|
|
273
|
+
* reservation is created: concurrent reservations are deliberately
|
|
274
|
+
* permitted (see design doc -- no uniqueness race).
|
|
275
|
+
* - The returned handle's ref field is set immediately to the computed ref.
|
|
276
|
+
*
|
|
277
|
+
* When options.clientHash is absent (upload-first mode):
|
|
278
|
+
* - No pre-check against the store.
|
|
279
|
+
* - The returned handle's ref field is null until send() completes.
|
|
138
280
|
*/
|
|
139
281
|
reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload>;
|
|
140
282
|
/**
|
|
141
283
|
* Get attachment metadata by ref.
|
|
142
284
|
*
|
|
143
285
|
* @throws AttachmentNotFound if the ref is unknown.
|
|
286
|
+
* Returns an AttachmentHeader with status='pending' and expiresAtUtc set if
|
|
287
|
+
* the hash has an active reservation but no committed bytes. Callers must
|
|
288
|
+
* check header.status to distinguish pending from available.
|
|
144
289
|
*/
|
|
145
290
|
stat(ref: AttachmentRef): Promise<AttachmentHeader>;
|
|
146
291
|
/**
|
|
147
292
|
* Retrieve attachment data.
|
|
148
293
|
*
|
|
149
|
-
* Always succeeds for any known ref. The underlying store
|
|
150
|
-
* re-fetching evicted data from the transport transparently.
|
|
294
|
+
* Always succeeds for any known, available ref. The underlying store
|
|
295
|
+
* handles re-fetching evicted data from the transport transparently.
|
|
296
|
+
*
|
|
297
|
+
* @throws AttachmentPending if the hash is reserved but bytes not yet
|
|
298
|
+
* available. There is no store-level wait; polling across the
|
|
299
|
+
* pending window is the caller's loop, bounded by the error's
|
|
300
|
+
* expiresAtUtc. A wait inside get() would hold request handlers
|
|
301
|
+
* open across multi-second windows and hide retry policy where
|
|
302
|
+
* callers cannot tune it.
|
|
151
303
|
*/
|
|
152
304
|
get(ref: AttachmentRef, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
153
305
|
}
|
|
@@ -160,6 +312,18 @@ interface IAttachmentUpload {
|
|
|
160
312
|
* Unique identifier for this reservation.
|
|
161
313
|
*/
|
|
162
314
|
reservationId: string;
|
|
315
|
+
/**
|
|
316
|
+
* The ref this upload will produce. Set immediately when the
|
|
317
|
+
* reservation carries a client hash (hash-first mode); null in the
|
|
318
|
+
* upload-first flow, where the ref is only known after send() completes.
|
|
319
|
+
*/
|
|
320
|
+
ref: AttachmentRef | null;
|
|
321
|
+
/**
|
|
322
|
+
* Reservation TTL contract, from the server in remote mode.
|
|
323
|
+
* ISO 8601 UTC string indicating when the reservation expires.
|
|
324
|
+
* Clients use this to bound retry windows and populate the pending-upload queue.
|
|
325
|
+
*/
|
|
326
|
+
readonly expiresAtUtc: string;
|
|
163
327
|
/**
|
|
164
328
|
* Stream attachment data through this handle.
|
|
165
329
|
*
|
|
@@ -172,6 +336,15 @@ interface IAttachmentUpload {
|
|
|
172
336
|
* exists, send() returns the existing ref. Content-addressed
|
|
173
337
|
* storage means identical uploads converge on the same hash.
|
|
174
338
|
*
|
|
339
|
+
* When the reservation carries a client hash, the handle verifies
|
|
340
|
+
* the received bytes against the claims:
|
|
341
|
+
* - @throws SizeMismatch if the byte count differs from the declared
|
|
342
|
+
* sizeBytes. The handle may reject mid-stream as soon as the count
|
|
343
|
+
* exceeds the declaration, without consuming the rest.
|
|
344
|
+
* - @throws HashMismatch if the server-computed hash differs from the
|
|
345
|
+
* claimed hash. Nothing is committed; the reservation is retained
|
|
346
|
+
* so the client can retry with the correct bytes.
|
|
347
|
+
*
|
|
175
348
|
* @returns The content hash, ref, and header for the uploaded attachment.
|
|
176
349
|
*/
|
|
177
350
|
send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
|
|
@@ -192,6 +365,9 @@ interface IAttachmentReader {
|
|
|
192
365
|
* not a data access.
|
|
193
366
|
*
|
|
194
367
|
* @throws AttachmentNotFound if the hash is unknown.
|
|
368
|
+
* Returns an AttachmentHeader with status='pending' and expiresAtUtc set if
|
|
369
|
+
* the hash has an active reservation but no committed bytes. Callers must
|
|
370
|
+
* check header.status to distinguish pending from available.
|
|
195
371
|
*/
|
|
196
372
|
stat(hash: AttachmentHash): Promise<AttachmentHeader>;
|
|
197
373
|
/**
|
|
@@ -201,10 +377,13 @@ interface IAttachmentReader {
|
|
|
201
377
|
* If the data has been evicted, re-fetches it from the transport,
|
|
202
378
|
* restores it locally via put(), and returns the data. This makes
|
|
203
379
|
* eviction transparent to callers -- get() always succeeds for
|
|
204
|
-
* any known hash.
|
|
380
|
+
* any known, available hash.
|
|
205
381
|
*
|
|
206
382
|
* @throws AttachmentNotFound if the hash is unknown (no metadata
|
|
207
|
-
* record exists).
|
|
383
|
+
* record exists and no pending reservation).
|
|
384
|
+
* @throws AttachmentPending if the hash is reserved by an in-flight
|
|
385
|
+
* upload; bytes are not yet available. There is no store-level
|
|
386
|
+
* wait -- polling is the caller's responsibility.
|
|
208
387
|
*/
|
|
209
388
|
get(hash: AttachmentHash, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
210
389
|
}
|
|
@@ -219,6 +398,7 @@ interface IAttachmentStore extends IAttachmentReader {
|
|
|
219
398
|
* Check whether attachment data is available locally.
|
|
220
399
|
* Returns true if the bytes can be served from this reactor's store
|
|
221
400
|
* without a transport round-trip. Does not trigger a remote fetch.
|
|
401
|
+
* Returns false for pending and evicted hashes.
|
|
222
402
|
*/
|
|
223
403
|
has(hash: AttachmentHash): Promise<boolean>;
|
|
224
404
|
/**
|
|
@@ -266,16 +446,16 @@ interface IAttachmentTransport {
|
|
|
266
446
|
/**
|
|
267
447
|
* Fetch attachment data by hash from a remote source.
|
|
268
448
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
449
|
+
* Returns a three-way discriminated union so callers can distinguish
|
|
450
|
+
* "data available", "upload in flight -- retry after expiry", and
|
|
451
|
+
* "not found -- possibly permanently". Conflating the last two would
|
|
452
|
+
* cause callers to apply long backoff to transient pending state,
|
|
453
|
+
* or to retry indefinitely on a permanently missing hash.
|
|
271
454
|
*
|
|
272
455
|
* @param hash - Content hash of the attachment
|
|
273
456
|
* @param signal - Abort signal for cancellation
|
|
274
|
-
* @returns The attachment data with metadata, or null if not available.
|
|
275
|
-
* Returns TransportResponse (not AttachmentResponse) because
|
|
276
|
-
* remote peers cannot populate local concerns like status/source.
|
|
277
457
|
*/
|
|
278
|
-
fetch(hash: AttachmentHash, signal?: AbortSignal): Promise<
|
|
458
|
+
fetch(hash: AttachmentHash, signal?: AbortSignal): Promise<TransportFetchResult>;
|
|
279
459
|
/**
|
|
280
460
|
* Announce that this reactor has attachment data available.
|
|
281
461
|
*
|
|
@@ -325,7 +505,7 @@ interface IReservationStore {
|
|
|
325
505
|
* that knows how to stream bytes to the appropriate backend.
|
|
326
506
|
*/
|
|
327
507
|
interface IAttachmentUploadFactory {
|
|
328
|
-
createUpload(
|
|
508
|
+
createUpload(reservation: Reservation): IAttachmentUpload;
|
|
329
509
|
}
|
|
330
510
|
//#endregion
|
|
331
511
|
//#region src/attachment-service.d.ts
|
|
@@ -337,6 +517,7 @@ declare class AttachmentService implements IAttachmentService {
|
|
|
337
517
|
reserve(options: ReserveAttachmentOptions): Promise<IAttachmentUpload>;
|
|
338
518
|
stat(ref: AttachmentRef): Promise<AttachmentHeader>;
|
|
339
519
|
get(ref: AttachmentRef, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
520
|
+
private reserveHashFirst;
|
|
340
521
|
}
|
|
341
522
|
//#endregion
|
|
342
523
|
//#region src/ref.d.ts
|
|
@@ -368,6 +549,8 @@ interface AttachmentReservationTable {
|
|
|
368
549
|
created_at_utc: string;
|
|
369
550
|
expires_at_utc: string;
|
|
370
551
|
deleted_at_utc: string | null;
|
|
552
|
+
client_hash: string | null;
|
|
553
|
+
size_bytes: number | null;
|
|
371
554
|
}
|
|
372
555
|
interface AttachmentDatabase {
|
|
373
556
|
attachment: AttachmentTable;
|
|
@@ -387,12 +570,14 @@ declare class KyselyAttachmentStore implements IAttachmentStore {
|
|
|
387
570
|
put(hash: AttachmentHash, metadata: AttachmentMetadata, data: ReadableStream<Uint8Array>): Promise<void>;
|
|
388
571
|
evict(hash: AttachmentHash): Promise<void>;
|
|
389
572
|
storageUsed(): Promise<number>;
|
|
573
|
+
private findPendingReservation;
|
|
390
574
|
private acquireReader;
|
|
391
575
|
private releaseReader;
|
|
392
576
|
private hasActiveReaders;
|
|
393
577
|
}
|
|
394
578
|
//#endregion
|
|
395
579
|
//#region src/storage/kysely/reservation-store.d.ts
|
|
580
|
+
declare const DEFAULT_RESERVATION_TTL_MS: number;
|
|
396
581
|
declare class KyselyReservationStore implements IReservationStore {
|
|
397
582
|
private readonly db;
|
|
398
583
|
private readonly ttlMs;
|
|
@@ -414,13 +599,15 @@ declare function runAttachmentMigrations(db: Kysely<any>, schema?: string): Prom
|
|
|
414
599
|
//#endregion
|
|
415
600
|
//#region src/direct/direct-attachment-upload.d.ts
|
|
416
601
|
declare class DirectAttachmentUpload implements IAttachmentUpload {
|
|
417
|
-
private readonly
|
|
602
|
+
private readonly reservation;
|
|
418
603
|
private readonly db;
|
|
419
604
|
private readonly basePath;
|
|
420
605
|
private readonly reservations;
|
|
421
606
|
private readonly maxBytes?;
|
|
422
607
|
readonly reservationId: string;
|
|
423
|
-
|
|
608
|
+
readonly ref: AttachmentRef | null;
|
|
609
|
+
readonly expiresAtUtc: string;
|
|
610
|
+
constructor(reservation: Reservation, db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore, maxBytes?: number | undefined);
|
|
424
611
|
send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
|
|
425
612
|
}
|
|
426
613
|
//#endregion
|
|
@@ -431,7 +618,7 @@ declare class DirectAttachmentUploadFactory implements IAttachmentUploadFactory
|
|
|
431
618
|
private readonly reservations;
|
|
432
619
|
private readonly maxBytes?;
|
|
433
620
|
constructor(db: Kysely<AttachmentDatabase>, basePath: string, reservations: IReservationStore, maxBytes?: number | undefined);
|
|
434
|
-
createUpload(
|
|
621
|
+
createUpload(reservation: Reservation): IAttachmentUpload;
|
|
435
622
|
}
|
|
436
623
|
//#endregion
|
|
437
624
|
//#region src/switchboard/switchboard-attachment-transport.d.ts
|
|
@@ -445,9 +632,10 @@ declare class SwitchboardAttachmentTransport implements IAttachmentTransport {
|
|
|
445
632
|
private readonly jwtHandler?;
|
|
446
633
|
private readonly fetchFn;
|
|
447
634
|
constructor(config: SwitchboardTransportConfig);
|
|
448
|
-
fetch(hash: AttachmentHash, signal?: AbortSignal): Promise<
|
|
635
|
+
fetch(hash: AttachmentHash, signal?: AbortSignal): Promise<TransportFetchResult>;
|
|
449
636
|
announce(_hash: AttachmentHash): Promise<void>;
|
|
450
637
|
push(hash: AttachmentHash, remote: string, data: ReadableStream<Uint8Array>): Promise<void>;
|
|
638
|
+
private parsePendingExpiry;
|
|
451
639
|
private parseMetadataHeaders;
|
|
452
640
|
}
|
|
453
641
|
//#endregion
|
|
@@ -471,11 +659,12 @@ declare class RemoteReservationStore implements IReservationStore {
|
|
|
471
659
|
//#region src/switchboard/remote-attachment-upload.d.ts
|
|
472
660
|
declare class RemoteAttachmentUpload implements IAttachmentUpload {
|
|
473
661
|
readonly reservationId: string;
|
|
662
|
+
readonly ref: AttachmentRef | null;
|
|
663
|
+
readonly expiresAtUtc: string;
|
|
474
664
|
private readonly remoteUrl;
|
|
475
665
|
private readonly jwtHandler?;
|
|
476
666
|
private readonly fetchFn;
|
|
477
|
-
|
|
478
|
-
constructor(reservationId: string, options: ReserveAttachmentOptions, config: SwitchboardClientConfig);
|
|
667
|
+
constructor(reservation: Reservation, config: SwitchboardClientConfig);
|
|
479
668
|
send(data: ReadableStream<Uint8Array>): Promise<AttachmentUploadResult>;
|
|
480
669
|
}
|
|
481
670
|
//#endregion
|
|
@@ -483,7 +672,7 @@ declare class RemoteAttachmentUpload implements IAttachmentUpload {
|
|
|
483
672
|
declare class RemoteAttachmentUploadFactory implements IAttachmentUploadFactory {
|
|
484
673
|
private readonly config;
|
|
485
674
|
constructor(config: SwitchboardClientConfig);
|
|
486
|
-
createUpload(
|
|
675
|
+
createUpload(reservation: Reservation): IAttachmentUpload;
|
|
487
676
|
}
|
|
488
677
|
//#endregion
|
|
489
678
|
//#region src/switchboard/remote-attachment-store.d.ts
|
|
@@ -492,6 +681,13 @@ declare class RemoteAttachmentStore implements IAttachmentReader {
|
|
|
492
681
|
private readonly jwtHandler?;
|
|
493
682
|
private readonly fetchFn;
|
|
494
683
|
constructor(config: SwitchboardClientConfig);
|
|
684
|
+
/**
|
|
685
|
+
* Get attachment metadata. Normally returns a pending AttachmentHeader
|
|
686
|
+
* (status: 'pending') when the server responds 202 with a full
|
|
687
|
+
* Attachment-Pending header. When only expiresAtUtc is present in the
|
|
688
|
+
* header (degraded wire), throws AttachmentPending instead -- the
|
|
689
|
+
* AttachmentPending throw is the degraded-wire case.
|
|
690
|
+
*/
|
|
495
691
|
stat(hash: AttachmentHash): Promise<AttachmentHeader>;
|
|
496
692
|
get(hash: AttachmentHash, signal?: AbortSignal): Promise<AttachmentResponse>;
|
|
497
693
|
private fetchAttachment;
|
|
@@ -503,10 +699,10 @@ declare function createRemoteAttachmentService(config: SwitchboardClientConfig):
|
|
|
503
699
|
//#region src/null-attachment-transport.d.ts
|
|
504
700
|
/**
|
|
505
701
|
* No-op transport for deployments without remote sync.
|
|
506
|
-
* fetch() always returns
|
|
702
|
+
* fetch() always returns not-found, announce() and push() are no-ops.
|
|
507
703
|
*/
|
|
508
704
|
declare class NullAttachmentTransport implements IAttachmentTransport {
|
|
509
|
-
fetch(): Promise<
|
|
705
|
+
fetch(): Promise<TransportFetchResult>;
|
|
510
706
|
announce(): Promise<void>;
|
|
511
707
|
push(): Promise<void>;
|
|
512
708
|
}
|
|
@@ -516,7 +712,8 @@ type AttachmentBuildResult = {
|
|
|
516
712
|
service: AttachmentService;
|
|
517
713
|
store: KyselyAttachmentStore;
|
|
518
714
|
reservations: KyselyReservationStore;
|
|
519
|
-
uploadFactory: IAttachmentUploadFactory;
|
|
715
|
+
uploadFactory: IAttachmentUploadFactory; /** Stops the reservation sweep timer, if one was configured via withReservationSweepMs(). */
|
|
716
|
+
destroy: () => void;
|
|
520
717
|
};
|
|
521
718
|
declare class AttachmentBuilder {
|
|
522
719
|
private readonly db;
|
|
@@ -524,12 +721,22 @@ declare class AttachmentBuilder {
|
|
|
524
721
|
private transport;
|
|
525
722
|
private customUploadFactory?;
|
|
526
723
|
private maxUploadBytes?;
|
|
724
|
+
private reservationSweepMs?;
|
|
527
725
|
constructor(db: Kysely<any>, storagePath: string);
|
|
528
726
|
withTransport(transport: IAttachmentTransport): this;
|
|
529
727
|
withUploadFactory(factory: IAttachmentUploadFactory): this;
|
|
530
728
|
withMaxUploadBytes(maxBytes: number): this;
|
|
729
|
+
/**
|
|
730
|
+
* Configure a recurring sweep that deletes expired reservations.
|
|
731
|
+
* The sweep calls reservations.deleteExpired() on the given interval.
|
|
732
|
+
* When set, the built result's destroy() clears the timer.
|
|
733
|
+
* Without this option no sweep runs -- deleteExpired() is never called
|
|
734
|
+
* automatically. Call withReservationSweepMs in production to prevent
|
|
735
|
+
* expired reservation rows from accumulating indefinitely.
|
|
736
|
+
*/
|
|
737
|
+
withReservationSweepMs(intervalMs: number): this;
|
|
531
738
|
build(): Promise<AttachmentBuildResult>;
|
|
532
739
|
}
|
|
533
740
|
//#endregion
|
|
534
|
-
export { ATTACHMENT_SCHEMA, type AttachmentBuildResult, AttachmentBuilder, type AttachmentDatabase, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, DirectAttachmentUpload, DirectAttachmentUploadFactory, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, type ParsedRef, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SwitchboardAttachmentTransport, type SwitchboardClientConfig, type SwitchboardTransportConfig, type TransportResponse, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
|
|
741
|
+
export { ATTACHMENT_SCHEMA, AttachmentAlreadyExists, type AttachmentBuildResult, AttachmentBuilder, type AttachmentDatabase, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, AttachmentPending, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, DEFAULT_RESERVATION_TTL_MS, DirectAttachmentUpload, DirectAttachmentUploadFactory, type HashFirstReserveAttachmentOptions, HashMismatch, type IAttachmentReader, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, KyselyAttachmentStore, KyselyReservationStore, NullAttachmentTransport, type ParsedRef, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SizeMismatch, SwitchboardAttachmentTransport, type SwitchboardClientConfig, type SwitchboardTransportConfig, type TransportFetchResult, type TransportResponse, type UploadFirstReserveAttachmentOptions, UploadTooLarge, createRef, createRemoteAttachmentService, parseRef, runAttachmentMigrations };
|
|
535
742
|
//# sourceMappingURL=index.d.ts.map
|