@powerhousedao/reactor-attachments 6.2.3-dev.7 → 6.2.3-dev.70
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/attachment-schema-compiler-rIaJpiHO.js +262 -0
- package/dist/attachment-schema-compiler-rIaJpiHO.js.map +1 -0
- package/dist/client.d.ts +56 -36
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +33 -20
- package/dist/client.js.map +1 -1
- package/dist/hash-BobbR4r4.js +63 -0
- package/dist/hash-BobbR4r4.js.map +1 -0
- package/dist/index.d.ts +81 -111
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -405
- package/dist/index.js.map +1 -1
- package/dist/{null-attachment-transport-CMrO_ZKA.d.ts → interfaces-C4GRlKeB.d.ts} +34 -327
- package/dist/interfaces-C4GRlKeB.d.ts.map +1 -0
- package/dist/null-attachment-transport-Bs4JpmFx.d.ts +327 -0
- package/dist/null-attachment-transport-Bs4JpmFx.d.ts.map +1 -0
- package/dist/{null-attachment-transport-CrsUafCi.js → null-attachment-transport-Cm_kb5ww.js} +37 -283
- package/dist/null-attachment-transport-Cm_kb5ww.js.map +1 -0
- package/dist/replication.d.ts +955 -0
- package/dist/replication.d.ts.map +1 -0
- package/dist/replication.js +1975 -0
- package/dist/replication.js.map +1 -0
- package/dist/switchboard-attachment-transport-BH3ZAmxE.js +284 -0
- package/dist/switchboard-attachment-transport-BH3ZAmxE.js.map +1 -0
- package/dist/types-AagIcjTD.d.ts +61 -0
- package/dist/types-AagIcjTD.d.ts.map +1 -0
- package/package.json +16 -7
- package/dist/null-attachment-transport-CMrO_ZKA.d.ts.map +0 -1
- package/dist/null-attachment-transport-CrsUafCi.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replication.js","names":[],"sources":["../src/access/attachment-read-gate.ts","../src/local/protocol.ts","../src/local/local-attachment-server.ts","../src/local/local-attachment-transport.ts","../src/peers/attachment-peer-links.ts","../src/storage/local/bytes.ts","../src/peers/peered-attachment-transport.ts","../src/replication/retry-state.ts","../src/replication/types.ts","../src/replication/attachment-replicator.ts","../src/replication/operation-attachment-refs.ts","../src/storage/local/idb-attachment-backend.ts","../src/storage/local/local-attachment-store.ts","../src/storage/local/memory-attachment-backend.ts"],"sourcesContent":["import type {\n AttachmentHash,\n AttachmentRef,\n IReactorClient,\n} from \"@powerhousedao/reactor\";\nimport type { AuthSubject } from \"@powerhousedao/shared/document-model\";\nimport type { IAttachmentReferenceReader } from \"../read-models/attachment-reference/types.js\";\n\n/** The reactor's read gate, as an attachment read consults it. */\nexport type AttachmentReadGate = Pick<IReactorClient, \"isServed\" | \"get\">;\n\n/** Scopes a subject may read, by document id; `SyncScopeGate` satisfies it. */\nexport interface IDocumentScopeGate {\n scopePredicateById(\n documentId: string,\n subject: { address?: string; key?: string },\n branch: string,\n signal?: AbortSignal,\n ): Promise<(scope: string) => boolean>;\n}\n\n/** The scope an attachment's bytes belong to: the document's own domain state. */\nexport const ATTACHMENT_READ_SCOPE = \"global\";\n\n/** The branch an attachment reference is resolved against. */\nexport const ATTACHMENT_READ_BRANCH = \"main\";\n\nconst HASH_PATTERN = /^[a-f0-9]{64}$/;\n\n/** Whether `value` is a canonical v1 hash: 64 lowercase SHA-256 hex chars. */\nexport function isAttachmentHash(value: unknown): value is AttachmentHash {\n return typeof value === \"string\" && HASH_PATTERN.test(value);\n}\n\n/** Rethrows a failure: a read side that is down must not read as a denial. */\nexport async function scopeGateAllowsAttachmentRead(\n scopeGate: IDocumentScopeGate,\n documentId: string,\n subject: AuthSubject,\n): Promise<boolean> {\n const readable = await scopeGate.scopePredicateById(\n documentId,\n subject,\n ATTACHMENT_READ_BRANCH,\n );\n return readable(ATTACHMENT_READ_SCOPE);\n}\n\n/** Served to the subject, references `ref`, and a referencing scope is readable. */\nexport async function readGateAllowsAttachmentRead(\n readGate: AttachmentReadGate,\n references: Pick<IAttachmentReferenceReader, \"referencingScopes\">,\n documentId: string,\n ref: AttachmentRef,\n subject: AuthSubject,\n): Promise<boolean> {\n let served: boolean;\n try {\n served = await readGate.isServed(documentId, { subject });\n } catch {\n return false;\n }\n if (!served) {\n return false;\n }\n\n const scopes = await references.referencingScopes(documentId, ref);\n if (scopes.length === 0) {\n return false;\n }\n\n let held: Record<string, unknown>;\n try {\n const document = await readGate.get(documentId, { subject, scopes });\n held = document.state as Record<string, unknown>;\n } catch {\n return false;\n }\n return scopes.some((scope) => scope in held);\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type { AttachmentMetadata } from \"../types.js\";\n\n/**\n * Wire tag on every message, so one `LocalChannelPort` can carry this\n * request/response protocol without being confused with anything else on the\n * port, and so a version skew is a readable refusal rather than a crash.\n */\nexport const LOCAL_ATTACHMENT_PROTOCOL = \"ph-attachment/v1\";\n\n/** Default bytes per `chunk` message. */\nexport const DEFAULT_LOCAL_CHUNK_BYTES = 256 * 1024;\n\n/** Default wait for the next message of a request before it is abandoned. */\nexport const DEFAULT_LOCAL_REQUEST_TIMEOUT_MS = 30_000;\n\n/** Ask a peer for the bytes of `hash`, authorized by `documentId`. */\nexport type LocalAttachmentFetchRequest = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"fetch\";\n /** Unique per requesting instance; the peer echoes it on every reply. */\n id: string;\n hash: AttachmentHash;\n documentId: string;\n};\n\n/** Abandon an in-flight `fetch` so the peer stops reading and sending. */\nexport type LocalAttachmentCancelRequest = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"cancel\";\n id: string;\n};\n\nexport type LocalAttachmentRequest =\n | LocalAttachmentFetchRequest\n | LocalAttachmentCancelRequest;\n\n/**\n * The peer holds the bytes and is about to stream them. `metadata` arrives up\n * front so the requester can start a body stream before the first byte.\n */\nexport type LocalAttachmentBeginResponse = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"begin\";\n id: string;\n hash: AttachmentHash;\n metadata: AttachmentMetadata;\n};\n\n/**\n * One slice of the body. `bytes` is a `Uint8Array`, which both a browser\n * `MessagePort` and a `node:worker_threads` one structured-clone natively.\n * `seq` lets a receiver assert ordering rather than assume it.\n */\nexport type LocalAttachmentChunkResponse = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"chunk\";\n id: string;\n seq: number;\n bytes: Uint8Array;\n};\n\n/** The body is complete. */\nexport type LocalAttachmentEndResponse = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"end\";\n id: string;\n};\n\n/** Retry later: the peer holds a reservation without bytes, or is at its serve cap. */\nexport type LocalAttachmentPendingResponse = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"pending\";\n id: string;\n hash: AttachmentHash;\n expiresAtUtc: string;\n retryAfterMs: number;\n /** At its serve cap: the peer did not authorize or look up the hash. */\n busy?: true;\n};\n\n/**\n * The peer will not serve the hash: it does not hold the bytes, or the\n * document does not authorize the read.\n *\n * The two are deliberately one answer. Distinguishing them would tell a peer\n * which hashes exist without authorization, and the requester's handling is\n * the same either way -- a bounded retry, because an unauthorized answer is\n * most often the serving reactor's attachment reference index lagging its own\n * sync (see `AttachmentRetryPolicy.notFoundAttempts`).\n */\nexport type LocalAttachmentNotFoundResponse = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"not-found\";\n id: string;\n};\n\n/** The peer failed while serving; the requester treats this as a retryable error. */\nexport type LocalAttachmentErrorResponse = {\n protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;\n kind: \"error\";\n id: string;\n message: string;\n};\n\nexport type LocalAttachmentResponse =\n | LocalAttachmentBeginResponse\n | LocalAttachmentChunkResponse\n | LocalAttachmentEndResponse\n | LocalAttachmentPendingResponse\n | LocalAttachmentNotFoundResponse\n | LocalAttachmentErrorResponse;\n\nexport type LocalAttachmentMessage =\n | LocalAttachmentRequest\n | LocalAttachmentResponse;\n\nconst REQUEST_KINDS = new Set([\"fetch\", \"cancel\"]);\nconst RESPONSE_KINDS = new Set([\n \"begin\",\n \"chunk\",\n \"end\",\n \"pending\",\n \"not-found\",\n \"error\",\n]);\n\nfunction tagged(value: unknown): value is { kind: string; id: string } {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n const record = value as Record<string, unknown>;\n return (\n record.protocol === LOCAL_ATTACHMENT_PROTOCOL &&\n typeof record.kind === \"string\" &&\n typeof record.id === \"string\"\n );\n}\n\n/**\n * Whether `value` is a request for this protocol.\n *\n * Both halves live on one port -- each peer runs a transport AND a server over\n * the same `LocalChannelPort` -- so each half has to recognise only its own\n * side of the conversation and ignore the rest rather than treat it as\n * malformed.\n */\nexport function isLocalAttachmentRequest(\n value: unknown,\n): value is LocalAttachmentRequest {\n return tagged(value) && REQUEST_KINDS.has(value.kind);\n}\n\n/** Whether `value` is a response for this protocol. */\nexport function isLocalAttachmentResponse(\n value: unknown,\n): value is LocalAttachmentResponse {\n return tagged(value) && RESPONSE_KINDS.has(value.kind);\n}\n","import type { AttachmentHash, LocalChannelPort } from \"@powerhousedao/reactor\";\nimport type { AuthSubject } from \"@powerhousedao/shared/document-model\";\nimport {\n isAttachmentHash,\n readGateAllowsAttachmentRead,\n scopeGateAllowsAttachmentRead,\n type AttachmentReadGate,\n type IDocumentScopeGate,\n} from \"../access/attachment-read-gate.js\";\nimport { AttachmentPending } from \"../errors.js\";\nimport type { IAttachmentStore } from \"../interfaces.js\";\nimport type { IAttachmentReferenceReader } from \"../read-models/attachment-reference/types.js\";\nimport { createRef } from \"../ref.js\";\nimport {\n DEFAULT_LOCAL_CHUNK_BYTES,\n isLocalAttachmentRequest,\n LOCAL_ATTACHMENT_PROTOCOL,\n type LocalAttachmentFetchRequest,\n} from \"./protocol.js\";\n\n/** The peer a server answers: which reactor, over which channel. */\nexport type AttachmentPeerLink = { peerId: string; channelName: string };\n\n/**\n * Decides whether the linked peer may read `hash` through `documentId`. False\n * is answered as `not-found`, indistinguishable from absent bytes.\n */\nexport type LocalAttachmentAuthorizer = (\n link: AttachmentPeerLink,\n hash: AttachmentHash,\n documentId: string,\n) => Promise<boolean>;\n\nexport type LocalAttachmentServerOptions = {\n /** The brokered port to the peer; the same one the transport half uses. */\n port: LocalChannelPort;\n /** Who is asking; handed to {@link authorize}. */\n link: AttachmentPeerLink;\n /** The store bytes are served FROM. Only locally held bytes are served. */\n store: IAttachmentStore;\n /** Defaults to refusing every read; see {@link readGateAttachmentAuthorizer}. */\n authorize?: LocalAttachmentAuthorizer;\n /** Bytes per `chunk` message; defaults to {@link DEFAULT_LOCAL_CHUNK_BYTES}. */\n chunkSizeBytes?: number;\n /** Requests served at once; one past it is answered `pending`. */\n maxConcurrentServes?: number;\n onDiagnostic?: (message: string, error?: unknown) => void;\n};\n\n/** Default {@link LocalAttachmentServerOptions.maxConcurrentServes}. */\nexport const DEFAULT_LOCAL_MAX_CONCURRENT_SERVES = 4;\n\n/** A macrotask, so a cancel posted by the peer is handled between slices. */\nfunction nextTurn(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, 0));\n}\n\nexport type ReadGateAttachmentAuthorizerOptions = {\n readGate: AttachmentReadGate;\n /** `SyncScopeGate`; a reactor without a policy model passes one over `BareReadGate`. */\n scopeGate: IDocumentScopeGate;\n references: Pick<IAttachmentReferenceReader, \"referencingScopes\">;\n /** The subject a link reads as; undefined refuses every read on it. */\n subjectOf: (link: AttachmentPeerLink) => AuthSubject | undefined;\n};\n\n/**\n * Authorizes a peer read the way the Switchboard's attachment route does: the\n * link's subject may read the document's `global` scope, the reactor serves\n * the document to it, and a scope it may read references the hash.\n */\nexport function readGateAttachmentAuthorizer(\n options: ReadGateAttachmentAuthorizerOptions,\n): LocalAttachmentAuthorizer {\n return async (link, hash, documentId) => {\n const subject = options.subjectOf(link);\n if (!subject) {\n return false;\n }\n const readable = await scopeGateAllowsAttachmentRead(\n options.scopeGate,\n documentId,\n subject,\n );\n if (!readable) {\n return false;\n }\n return readGateAllowsAttachmentRead(\n options.readGate,\n options.references,\n documentId,\n createRef(hash),\n subject,\n );\n };\n}\n\n/**\n * Serves attachment bytes to a brokered local peer from this reactor's own\n * store.\n *\n * The answering half of {@link LocalAttachmentTransport}; both run on the same\n * `LocalChannelPort` and ignore each other's messages.\n *\n * It never chains. The store is read WITHOUT a document id, which means a\n * `LocalAttachmentStore` serves only bytes it actually holds and cannot reach\n * for its own transport on the requester's behalf. Without that, two peers\n * linked to each other would answer a miss by asking each other, and a hash\n * neither held would bounce between them.\n */\nexport class LocalAttachmentServer {\n private readonly port: LocalChannelPort;\n private readonly link: AttachmentPeerLink;\n private readonly store: IAttachmentStore;\n private readonly authorize: LocalAttachmentAuthorizer;\n private readonly chunkSizeBytes: number;\n private readonly maxConcurrentServes: number;\n private readonly onDiagnostic: (message: string, error?: unknown) => void;\n private readonly cancelled = new Set<string>();\n private readonly inFlight = new Set<string>();\n private readonly detachPort: () => void;\n private served = 0;\n private bytesServed = 0;\n private refused = 0;\n private closed = false;\n\n constructor(options: LocalAttachmentServerOptions) {\n this.port = options.port;\n this.link = { ...options.link };\n this.store = options.store;\n this.authorize =\n options.authorize ?? ((): Promise<boolean> => Promise.resolve(false));\n this.chunkSizeBytes = options.chunkSizeBytes ?? DEFAULT_LOCAL_CHUNK_BYTES;\n this.maxConcurrentServes =\n options.maxConcurrentServes ?? DEFAULT_LOCAL_MAX_CONCURRENT_SERVES;\n this.onDiagnostic = options.onDiagnostic ?? ((): void => undefined);\n this.detachPort = this.port.onMessage((data) => this.onMessage(data));\n }\n\n /** What this peer has handed out. */\n stats(): { served: number; bytesServed: number; refused: number } {\n return {\n served: this.served,\n bytesServed: this.bytesServed,\n refused: this.refused,\n };\n }\n\n /** Detaches from the port. In-flight serves stop at their next slice. */\n close(): void {\n if (this.closed) {\n return;\n }\n this.closed = true;\n this.detachPort();\n // Nothing will ever consume these again; a closed server holds no requests.\n this.cancelled.clear();\n this.inFlight.clear();\n }\n\n private onMessage(data: unknown): void {\n if (!isLocalAttachmentRequest(data)) {\n return;\n }\n if (data.kind === \"cancel\") {\n // Only track a cancel for a request that is actually in flight. A cancel\n // that arrives after a request has finished (or for one that never\n // started) would otherwise accumulate in the set forever, since nothing\n // downstream would ever match and drop it.\n if (this.inFlight.has(data.id)) {\n this.cancelled.add(data.id);\n }\n return;\n }\n // Any answer would settle the requester's live request under that id.\n if (this.inFlight.has(data.id)) {\n this.onDiagnostic(\n `ignoring a duplicate attachment request id ${data.id}`,\n );\n return;\n }\n if (\n !isAttachmentHash(data.hash) ||\n typeof data.documentId !== \"string\" ||\n data.documentId === \"\"\n ) {\n this.refuse(data);\n return;\n }\n if (this.inFlight.size >= this.maxConcurrentServes) {\n // Busy is a wait, not a fault, and not a reservation: nothing was\n // authorized or looked up.\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"pending\",\n id: data.id,\n hash: data.hash,\n expiresAtUtc: new Date(Date.now() + BUSY_PENDING_TTL_MS).toISOString(),\n retryAfterMs: BUSY_RETRY_MS,\n busy: true,\n });\n return;\n }\n void this.serve(data);\n }\n\n private stopped(id: string): boolean {\n return this.closed || this.cancelled.has(id);\n }\n\n private async serve(request: LocalAttachmentFetchRequest): Promise<void> {\n this.inFlight.add(request.id);\n try {\n await this.serveInner(request);\n } finally {\n this.inFlight.delete(request.id);\n this.cancelled.delete(request.id);\n }\n }\n\n private async serveInner(\n request: LocalAttachmentFetchRequest,\n ): Promise<void> {\n try {\n const authorized = await this.authorize(\n { ...this.link },\n request.hash,\n request.documentId,\n );\n if (!authorized) {\n this.refuse(request);\n return;\n }\n } catch (error) {\n this.onDiagnostic(\n `authorizing attachment ${request.hash} for ${request.documentId} failed`,\n error,\n );\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"error\",\n id: request.id,\n message: \"authorization check failed\",\n });\n return;\n }\n if (this.stopped(request.id)) {\n return;\n }\n\n let held: boolean;\n try {\n held = await this.store.has(request.hash);\n } catch (error) {\n this.failRequest(\n request,\n \"reading the local attachment store failed\",\n error,\n );\n return;\n }\n if (this.stopped(request.id)) {\n return;\n }\n if (!held) {\n this.refuse(request);\n return;\n }\n\n await this.stream(request);\n }\n\n private async stream(request: LocalAttachmentFetchRequest): Promise<void> {\n // No document id: a local store must serve its OWN bytes here and never\n // chain out to its transport on the requester's behalf. See the class doc.\n let response;\n try {\n response = await this.store.get(request.hash);\n } catch (error) {\n if (error instanceof AttachmentPending) {\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"pending\",\n id: request.id,\n hash: request.hash,\n expiresAtUtc: error.expiresAtUtc,\n retryAfterMs: DEFAULT_PENDING_RETRY_MS,\n });\n return;\n }\n // Anything else -- including a hash that went away between has() and\n // get() -- is reported as absent rather than as a server fault, which is\n // what it is from the requester's point of view.\n this.onDiagnostic(`serving attachment ${request.hash} failed`, error);\n this.refuse(request);\n return;\n }\n\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"begin\",\n id: request.id,\n hash: request.hash,\n metadata: {\n mimeType: response.header.mimeType,\n fileName: response.header.fileName,\n sizeBytes: response.header.sizeBytes,\n extension: response.header.extension,\n createdAtUtc: response.header.createdAtUtc,\n lastAccessedAtUtc: response.header.lastAccessedAtUtc,\n },\n });\n\n const reader = response.body.getReader();\n let seq = 0;\n try {\n for (;;) {\n if (this.stopped(request.id)) {\n await reader.cancel();\n return;\n }\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n for (\n let offset = 0;\n offset < value.byteLength;\n offset += this.chunkSizeBytes\n ) {\n if (offset > 0) {\n await nextTurn();\n }\n if (this.stopped(request.id)) {\n await reader.cancel();\n return;\n }\n const slice = value.subarray(\n offset,\n Math.min(offset + this.chunkSizeBytes, value.byteLength),\n );\n // Copied: a `subarray` shares its buffer, and a structured clone of\n // a view that outlives this loop is not something to rely on.\n const bytes = new Uint8Array(slice.byteLength);\n bytes.set(slice);\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"chunk\",\n id: request.id,\n seq: seq++,\n bytes,\n });\n this.bytesServed += bytes.byteLength;\n }\n }\n } catch (error) {\n this.onDiagnostic(`streaming attachment ${request.hash} failed`, error);\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"error\",\n id: request.id,\n message: \"reading the attachment body failed\",\n });\n return;\n }\n\n this.served += 1;\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"end\",\n id: request.id,\n });\n }\n\n /** Refused and absent are one answer, so a peer learns nothing from it. */\n private refuse(request: LocalAttachmentFetchRequest): void {\n this.refused += 1;\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"not-found\",\n id: request.id,\n });\n }\n\n private failRequest(\n request: LocalAttachmentFetchRequest,\n message: string,\n error: unknown,\n ): void {\n this.onDiagnostic(`${message} (${request.hash})`, error);\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"error\",\n id: request.id,\n message,\n });\n }\n\n private post(message: unknown): void {\n try {\n this.port.postMessage(message);\n } catch (error) {\n this.onDiagnostic(\"posting an attachment response failed\", error);\n }\n }\n}\n\n/**\n * Retry hint sent with a `pending` answer. A local store has no reservation\n * table, so this only arises when the served store is a server-side one; the\n * value mirrors `SwitchboardAttachmentTransport`'s own default.\n */\nconst DEFAULT_PENDING_RETRY_MS = 5_000;\n\n/** Retry hint sent with the `pending` that answers a request over the serve cap. */\nconst BUSY_RETRY_MS = 1_000;\n\n/** Expiry of that `pending`, for callers that surface it as `AttachmentPending`. */\nconst BUSY_PENDING_TTL_MS = 60_000;\n","import type { AttachmentHash, LocalChannelPort } from \"@powerhousedao/reactor\";\nimport { isAttachmentMetadata } from \"../attachment-metadata.js\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport type { TransportFetchResult } from \"../types.js\";\nimport {\n DEFAULT_LOCAL_REQUEST_TIMEOUT_MS,\n isLocalAttachmentResponse,\n LOCAL_ATTACHMENT_PROTOCOL,\n type LocalAttachmentResponse,\n} from \"./protocol.js\";\n\nexport type LocalAttachmentTransportOptions = {\n /**\n * The brokered port to the peer -- the same `LocalChannelPort` abstraction\n * `LocalChannel` sync runs over (W1.1), on its own channel so byte traffic\n * never interleaves with the sync wire.\n */\n port: LocalChannelPort;\n /**\n * How long to wait for the NEXT message of a request before abandoning it.\n * Reset by every message, so a slow but progressing transfer is not cut off\n * while a silent peer is. Defaults to\n * {@link DEFAULT_LOCAL_REQUEST_TIMEOUT_MS}.\n */\n requestTimeoutMs?: number;\n /** Swapped in tests; defaults to the realm's timers. */\n setTimer?: (callback: () => void, delayMs: number) => unknown;\n clearTimer?: (handle: unknown) => void;\n /** Identifies this instance's request ids; defaults to a random nonce. */\n instanceId?: string;\n /** Refuses a body the peer declares larger than this. */\n maxBytes?: number;\n};\n\ntype Pending = {\n hash: AttachmentHash;\n settle: (result: TransportFetchResult) => void;\n fail: (error: Error) => void;\n /** Set once `begin` has arrived and the body stream exists. */\n controller: ReadableStreamDefaultController<Uint8Array> | undefined;\n nextSeq: number;\n declaredBytes: number;\n receivedBytes: number;\n timer: unknown;\n detachAbort: (() => void) | undefined;\n};\n\n/**\n * `IAttachmentTransport` over a brokered `LocalChannelPort`: one linked reactor\n * pulling attachment bytes straight from its peer, with no Switchboard and no\n * HTTP.\n *\n * The counterpart of {@link LocalAttachmentServer}, and a pair of them runs on\n * each end of one port: a reactor both asks for bytes and serves them. The two\n * halves share the port and ignore each other's messages (see\n * `isLocalAttachmentRequest` / `isLocalAttachmentResponse`), and request ids\n * are namespaced per instance so two peers that both start counting at 1 never\n * match each other's replies.\n *\n * Pull-only by design. The agreed byte-movement model is lazy\n * fetch-on-reference, so {@link announce} is a no-op and {@link push} refuses\n * by name rather than silently succeeding: a push that reported success while\n * moving nothing would make an eager-replication caller believe bytes had\n * landed on a peer.\n */\nexport class LocalAttachmentTransport implements IAttachmentTransport {\n private readonly port: LocalChannelPort;\n private readonly requestTimeoutMs: number;\n private readonly setTimer: (callback: () => void, delayMs: number) => unknown;\n private readonly clearTimer: (handle: unknown) => void;\n private readonly instanceId: string;\n private readonly maxBytes: number | undefined;\n private readonly pending = new Map<string, Pending>();\n private readonly detachPort: () => void;\n private nextRequest = 0;\n private closed = false;\n\n constructor(options: LocalAttachmentTransportOptions) {\n this.port = options.port;\n this.requestTimeoutMs =\n options.requestTimeoutMs ?? DEFAULT_LOCAL_REQUEST_TIMEOUT_MS;\n this.setTimer =\n options.setTimer ??\n ((callback, delayMs) => setTimeout(callback, delayMs));\n this.clearTimer =\n options.clearTimer ??\n ((handle) => clearTimeout(handle as ReturnType<typeof setTimeout>));\n this.instanceId =\n options.instanceId ?? Math.random().toString(36).slice(2, 10);\n this.maxBytes = options.maxBytes;\n this.detachPort = this.port.onMessage((data) => this.onMessage(data));\n }\n\n fetch(\n hash: AttachmentHash,\n documentId: string,\n signal?: AbortSignal,\n ): Promise<TransportFetchResult> {\n if (this.closed) {\n return Promise.reject(\n new Error(\"Local attachment transport is closed; the link is severed\"),\n );\n }\n if (signal?.aborted) {\n return Promise.reject(new Error(\"Attachment fetch aborted\"));\n }\n\n const id = `${this.instanceId}:${++this.nextRequest}`;\n return new Promise<TransportFetchResult>((resolve, reject) => {\n const entry: Pending = {\n hash,\n settle: resolve,\n fail: reject,\n controller: undefined,\n nextSeq: 0,\n declaredBytes: 0,\n receivedBytes: 0,\n timer: undefined,\n detachAbort: undefined,\n };\n this.pending.set(id, entry);\n this.arm(id, entry);\n\n if (signal) {\n const onAbort = (): void => {\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"cancel\",\n id,\n });\n this.abandon(id, new Error(\"Attachment fetch aborted\"));\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n entry.detachAbort = () => signal.removeEventListener(\"abort\", onAbort);\n }\n\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"fetch\",\n id,\n hash,\n documentId,\n });\n });\n }\n\n /**\n * No-op: the agreed model is the peer PULLING on reference, so there is\n * nothing useful to tell it -- it will ask when one of its own operations\n * names the hash. An eager-announce protocol would need a peer that acts on\n * the notice, which nothing does.\n */\n announce(): Promise<void> {\n return Promise.resolve();\n }\n\n /** Refuses: this transport moves bytes only in answer to a peer's request. */\n push(): Promise<void> {\n return Promise.reject(\n new Error(\n \"LocalAttachmentTransport is pull-only: a linked peer fetches bytes on reference, so there is no push path to a local peer\",\n ),\n );\n }\n\n /** Detaches from the port and fails every in-flight request. */\n close(): void {\n if (this.closed) {\n return;\n }\n this.closed = true;\n this.detachPort();\n for (const id of [...this.pending.keys()]) {\n this.abandon(\n id,\n new Error(\n \"Local attachment transport closed while a fetch was in flight\",\n ),\n );\n }\n }\n\n private onMessage(data: unknown): void {\n if (!isLocalAttachmentResponse(data)) {\n return;\n }\n const entry = this.pending.get(data.id);\n if (!entry) {\n // A reply to a request this instance abandoned, or to the peer's own\n // conversation with someone else. Nothing to do.\n return;\n }\n this.arm(data.id, entry);\n this.handle(data.id, entry, data);\n }\n\n private handle(\n id: string,\n entry: Pending,\n message: LocalAttachmentResponse,\n ): void {\n if (\n entry.controller &&\n (message.kind === \"begin\" ||\n message.kind === \"pending\" ||\n message.kind === \"not-found\")\n ) {\n this.violate(\n id,\n `Local attachment peer sent ${message.kind} after the body began`,\n );\n return;\n }\n switch (message.kind) {\n case \"begin\": {\n if (\n message.hash !== entry.hash ||\n !isAttachmentMetadata(message.metadata)\n ) {\n this.violate(id, \"Local attachment peer sent a malformed begin\");\n return;\n }\n const sizeBytes = message.metadata.sizeBytes;\n if (this.maxBytes !== undefined && sizeBytes > this.maxBytes) {\n this.violate(\n id,\n `Local attachment of ${sizeBytes} bytes exceeds the ${this.maxBytes} byte limit`,\n );\n return;\n }\n entry.declaredBytes = sizeBytes;\n const body = new ReadableStream<Uint8Array>({\n start: (controller) => {\n entry.controller = controller;\n },\n cancel: () => {\n this.post({\n protocol: LOCAL_ATTACHMENT_PROTOCOL,\n kind: \"cancel\",\n id,\n });\n this.release(id, entry);\n },\n });\n entry.settle({\n kind: \"data\",\n response: {\n hash: message.hash,\n metadata: message.metadata,\n body,\n },\n });\n return;\n }\n case \"chunk\": {\n if (!entry.controller) {\n this.abandon(\n id,\n new Error(\n \"Local attachment peer sent body bytes before announcing the attachment\",\n ),\n );\n return;\n }\n if (!(message.bytes instanceof Uint8Array)) {\n this.violate(id, \"Local attachment peer sent a malformed chunk\");\n return;\n }\n if (message.seq !== entry.nextSeq) {\n // The port preserves order, so an out-of-order sequence means the\n // two sides disagree about the transfer; serving a body with a hole\n // in it would corrupt a content-addressed store.\n this.abandon(\n id,\n new Error(\n `Local attachment chunk out of order: expected ${entry.nextSeq}, got ${message.seq}`,\n ),\n );\n return;\n }\n entry.receivedBytes += message.bytes.byteLength;\n if (entry.receivedBytes > entry.declaredBytes) {\n this.violate(\n id,\n `Local attachment peer sent more than the ${entry.declaredBytes} bytes it declared`,\n );\n return;\n }\n entry.nextSeq += 1;\n entry.controller.enqueue(new Uint8Array(message.bytes));\n return;\n }\n case \"end\": {\n if (!entry.controller) {\n // A terminal reply with no body stream behind it: `end` arrived\n // without a `begin`, so the fetch promise was never settled. Failing\n // it (rather than silently releasing) rejects the awaiting fetch and\n // frees the concurrency slot, mirroring the `chunk`-before-`begin`\n // abandon path; a silent release would hang the fetch forever.\n this.abandon(\n id,\n new Error(\n \"Local attachment peer ended the transfer before announcing the attachment\",\n ),\n );\n return;\n }\n if (entry.receivedBytes !== entry.declaredBytes) {\n this.abandon(\n id,\n new Error(\n `Local attachment peer ended after ${entry.receivedBytes} of ${entry.declaredBytes} declared bytes`,\n ),\n );\n return;\n }\n entry.controller.close();\n this.release(id, entry);\n return;\n }\n case \"pending\": {\n if (\n message.hash !== entry.hash ||\n typeof message.expiresAtUtc !== \"string\"\n ) {\n this.violate(id, \"Local attachment peer sent a malformed pending\");\n return;\n }\n entry.settle({\n kind: \"pending\",\n hash: message.hash,\n expiresAtUtc: message.expiresAtUtc,\n // The replicator falls back to its policy for a non-finite delay.\n retryAfterMs:\n typeof message.retryAfterMs === \"number\"\n ? message.retryAfterMs\n : Number.NaN,\n ...(message.busy === true ? { busy: true as const } : {}),\n });\n this.release(id, entry);\n return;\n }\n case \"not-found\": {\n entry.settle({ kind: \"not-found\" });\n this.release(id, entry);\n return;\n }\n case \"error\": {\n this.abandon(\n id,\n new Error(`Local attachment peer failed: ${message.message}`),\n );\n return;\n }\n }\n }\n\n /** A reply that breaks the protocol: tell the peer to stop, fail the request. */\n private violate(id: string, message: string): void {\n this.post({ protocol: LOCAL_ATTACHMENT_PROTOCOL, kind: \"cancel\", id });\n this.abandon(id, new Error(message));\n }\n\n /** Resets this request's silence timer. */\n private arm(id: string, entry: Pending): void {\n if (entry.timer !== undefined) {\n this.clearTimer(entry.timer);\n }\n entry.timer = this.setTimer(() => {\n this.post({ protocol: LOCAL_ATTACHMENT_PROTOCOL, kind: \"cancel\", id });\n this.abandon(\n id,\n new Error(\n `Local attachment peer went silent for ${this.requestTimeoutMs}ms while serving ${entry.hash}`,\n ),\n );\n }, this.requestTimeoutMs);\n }\n\n /** Forgets a request that completed normally. */\n private release(id: string, entry: Pending): void {\n if (entry.timer !== undefined) {\n this.clearTimer(entry.timer);\n entry.timer = undefined;\n }\n entry.detachAbort?.();\n this.pending.delete(id);\n }\n\n /**\n * Forgets a request that failed, pushing the failure to whichever side is\n * still listening: the fetch promise before `begin`, the body stream after.\n */\n private abandon(id: string, error: Error): void {\n const entry = this.pending.get(id);\n if (!entry) {\n return;\n }\n this.release(id, entry);\n if (entry.controller) {\n entry.controller.error(error);\n return;\n }\n entry.fail(error);\n }\n\n private post(message: unknown): void {\n try {\n this.port.postMessage(message);\n } catch {\n // A closed port throws on post in some realms. The silence timer (or\n // close()) is what turns that into a reported failure; there is nothing\n // useful to do with the throw itself.\n }\n }\n}\n","import type { LocalChannelPort } from \"@powerhousedao/reactor\";\nimport type { IAttachmentStore } from \"../interfaces.js\";\nimport {\n LocalAttachmentServer,\n type LocalAttachmentAuthorizer,\n} from \"../local/local-attachment-server.js\";\nimport { LocalAttachmentTransport } from \"../local/local-attachment-transport.js\";\nimport type { PeeredAttachmentTransport } from \"./peered-attachment-transport.js\";\n\nexport type AttachmentServedStats = {\n served: number;\n bytesServed: number;\n refused: number;\n};\n\nexport type AttachmentPeerLinksOptions = {\n /** What linked peers are served from. */\n store: IAttachmentStore;\n /** Where each link's pulling half is registered. */\n transport: PeeredAttachmentTransport;\n /** Absent, and absent on the link, every read is refused. */\n authorize?: LocalAttachmentAuthorizer;\n chunkSizeBytes?: number;\n requestTimeoutMs?: number;\n /** Each link's transport refuses a body declared larger than this. */\n maxBytes?: number;\n /** Requests each link's server serves at once. */\n maxConcurrentServes?: number;\n /** Called after a link is added; a host re-chases terminal hashes here. */\n onPeerAdded?: (peerId: string, channelName: string) => void;\n onDiagnostic?: (message: string, error?: unknown) => void;\n};\n\nexport type AttachmentPeerLinkOptions = {\n /** Replaces the links-wide authorizer for this link. */\n authorize?: LocalAttachmentAuthorizer;\n};\n\n/** Holds one raw listener for the link's life and fans it out to both halves. */\nclass LinkPort {\n private readonly subscribers = new Set<(data: unknown) => void>();\n private readonly detachRaw: () => void;\n\n constructor(\n private readonly raw: LocalChannelPort,\n private readonly onDiagnostic: (message: string, error?: unknown) => void,\n ) {\n this.detachRaw = raw.onMessage((data) => this.dispatch(data));\n }\n\n /** A view for one half; its close() is a no-op, the link owns the port. */\n view(): LocalChannelPort {\n return {\n postMessage: (data) => this.raw.postMessage(data),\n onMessage: (callback) => {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n },\n close: () => undefined,\n };\n }\n\n detach(): void {\n this.subscribers.clear();\n this.detachRaw();\n }\n\n release(): void {\n this.detach();\n try {\n this.raw.close();\n } catch (error) {\n this.onDiagnostic(\"closing an attachment link port failed\", error);\n }\n }\n\n private dispatch(data: unknown): void {\n for (const subscriber of [...this.subscribers]) {\n try {\n subscriber(data);\n } catch (error) {\n this.onDiagnostic(\"an attachment link handler threw\", error);\n }\n }\n }\n}\n\ntype Link = {\n peerId: string;\n channelName: string;\n port: LinkPort;\n server: LocalAttachmentServer;\n transport: LocalAttachmentTransport;\n};\n\n/** Per-peer server and transport pairs, each on its own port, not the sync port. */\nexport class AttachmentPeerLinks {\n private readonly links = new Map<string, Link>();\n private readonly onDiagnostic: (message: string, error?: unknown) => void;\n\n constructor(private readonly options: AttachmentPeerLinksOptions) {\n this.onDiagnostic = options.onDiagnostic ?? ((): void => undefined);\n }\n\n has(peerId: string, channelName: string): boolean {\n return this.links.has(linkKey(peerId, channelName));\n }\n\n /** Takes ownership of `port` on success; on failure the port is untouched. */\n addPeer(\n peerId: string,\n channelName: string,\n port: LocalChannelPort,\n linkOptions: AttachmentPeerLinkOptions = {},\n ): void {\n const key = linkKey(peerId, channelName);\n if (this.links.has(key)) {\n throw new Error(\n `This reactor already holds an attachment link to peer '${peerId}' on channel '${channelName}'; remove it before linking again`,\n );\n }\n\n const linkPort = new LinkPort(port, this.onDiagnostic);\n const authorize = linkOptions.authorize ?? this.options.authorize;\n const server = new LocalAttachmentServer({\n port: linkPort.view(),\n link: { peerId, channelName },\n store: this.options.store,\n ...(authorize ? { authorize } : {}),\n ...(this.options.chunkSizeBytes !== undefined\n ? { chunkSizeBytes: this.options.chunkSizeBytes }\n : {}),\n ...(this.options.maxConcurrentServes !== undefined\n ? { maxConcurrentServes: this.options.maxConcurrentServes }\n : {}),\n onDiagnostic: this.onDiagnostic,\n });\n const transport = new LocalAttachmentTransport({\n port: linkPort.view(),\n ...(this.options.requestTimeoutMs !== undefined\n ? { requestTimeoutMs: this.options.requestTimeoutMs }\n : {}),\n ...(this.options.maxBytes !== undefined\n ? { maxBytes: this.options.maxBytes }\n : {}),\n });\n\n try {\n this.options.transport.addPeer(peerId, channelName, transport);\n } catch (error) {\n server.close();\n transport.close();\n linkPort.detach();\n throw error;\n }\n\n this.links.set(key, {\n peerId,\n channelName,\n port: linkPort,\n server,\n transport,\n });\n try {\n this.options.onPeerAdded?.(peerId, channelName);\n } catch (error) {\n this.onDiagnostic(\"an attachment onPeerAdded hook threw\", error);\n }\n }\n\n /** Closes the link's server, transport and port. False when absent. */\n removePeer(peerId: string, channelName: string): boolean {\n const key = linkKey(peerId, channelName);\n const link = this.links.get(key);\n if (!link) {\n return false;\n }\n this.links.delete(key);\n this.options.transport.removePeer(peerId, channelName);\n link.server.close();\n link.transport.close();\n link.port.release();\n return true;\n }\n\n servedStats(): AttachmentServedStats {\n const total: AttachmentServedStats = {\n served: 0,\n bytesServed: 0,\n refused: 0,\n };\n for (const link of this.links.values()) {\n const stats = link.server.stats();\n total.served += stats.served;\n total.bytesServed += stats.bytesServed;\n total.refused += stats.refused;\n }\n return total;\n }\n\n close(): void {\n for (const link of [...this.links.values()]) {\n this.removePeer(link.peerId, link.channelName);\n }\n }\n}\n\nfunction linkKey(peerId: string, channelName: string): string {\n return `${peerId}\\u0000${channelName}`;\n}\n","/**\n * Drains `stream` into one `Uint8Array`.\n *\n * The local stores hold whole blobs (see {@link ILocalAttachmentBackend}), so\n * the stream an `IAttachmentStore.put` is handed has to be collected before it\n * can be written. Realm-neutral: no `node:buffer`, no `Blob`.\n */\nexport async function collectStream(\n stream: ReadableStream<Uint8Array>,\n): Promise<Uint8Array> {\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n total += value.byteLength;\n }\n\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n}\n\n/** A one-chunk `ReadableStream` over `bytes`. */\nexport function streamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n}\n","import {\n GQL_CHANNEL_TYPE,\n type AttachmentHash,\n type ISyncManager,\n type JwtHandler,\n} from \"@powerhousedao/reactor\";\nimport type { IAttachmentTransport } from \"../interfaces.js\";\nimport { sha256Hex } from \"../replication/hash.js\";\nimport { collectStream, streamFromBytes } from \"../storage/local/bytes.js\";\nimport { SwitchboardAttachmentTransport } from \"../switchboard/switchboard-attachment-transport.js\";\nimport type { TransportFetchResult } from \"../types.js\";\n\ntype DataResult = Extract<TransportFetchResult, { kind: \"data\" }>;\n\ntype PendingResult = Extract<TransportFetchResult, { kind: \"pending\" }>;\n\n/**\n * What the sources that did not answer data said, ranked by the combine rule:\n * a reservation, then a busy source, then an error, then a not-found. A busy\n * source may hold the bytes; the replicator bounds a busy run.\n */\nclass Answers {\n pending: PendingResult | undefined;\n busy: PendingResult | undefined;\n firstError: Error | undefined;\n notFound = false;\n\n error(error: unknown): void {\n this.firstError ??=\n error instanceof Error ? error : new Error(String(error));\n }\n\n result(): TransportFetchResult {\n if (this.pending) {\n return this.pending;\n }\n if (this.busy) {\n // A source's own flag speaks only for the sources behind it.\n const { othersNotFound: _, ...busy } = this.busy;\n return this.notFound && this.firstError === undefined\n ? { ...busy, othersNotFound: true }\n : busy;\n }\n if (this.firstError !== undefined) {\n throw this.firstError;\n }\n return { kind: \"not-found\" };\n }\n}\n\n/** Collects and checks a body, so a source's wrong bytes are its own error. */\nasync function verified(\n hash: AttachmentHash,\n result: DataResult,\n): Promise<DataResult> {\n const bytes = await collectStream(result.response.body);\n const actual = await sha256Hex(bytes);\n if (actual !== hash) {\n throw new Error(\n `Attachment bytes for ${hash} hashed to ${actual}; the source served content that is not what was asked for`,\n );\n }\n return {\n kind: \"data\",\n response: { ...result.response, body: streamFromBytes(bytes) },\n };\n}\n\nexport type PeeredAttachmentTransportOptions = {\n /** Read at fetch time for the Switchboard origins of its gql remotes. */\n syncManager?: Pick<ISyncManager, \"list\">;\n /** An attachment host that is not one of the sync remotes. */\n switchboardUrl?: string;\n jwtHandler?: JwtHandler;\n fetchFn?: typeof fetch;\n};\n\n/**\n * Peers in parallel, then Switchboards in turn; verified data > pending >\n * busy > error > unanimous not-found.\n */\nexport class PeeredAttachmentTransport implements IAttachmentTransport {\n private readonly syncManager: Pick<ISyncManager, \"list\"> | undefined;\n private readonly switchboardUrl: string | undefined;\n private readonly jwtHandler: JwtHandler | undefined;\n private readonly fetchFn: typeof fetch | undefined;\n private readonly peers = new Map<\n string,\n { peerId: string; transport: IAttachmentTransport }\n >();\n private readonly switchboards = new Map<string, IAttachmentTransport>();\n\n constructor(options: PeeredAttachmentTransportOptions = {}) {\n this.syncManager = options.syncManager;\n this.switchboardUrl = options.switchboardUrl;\n this.jwtHandler = options.jwtHandler;\n this.fetchFn = options.fetchFn;\n }\n\n /** Keyed by (peerId, channelName), so one peer may hold several links. */\n addPeer(\n peerId: string,\n channelName: string,\n transport: IAttachmentTransport,\n ): void {\n const key = peerKey(peerId, channelName);\n if (this.peers.has(key)) {\n throw new Error(\n `An attachment peer is already registered for '${peerId}' on channel '${channelName}'; remove it before linking again`,\n );\n }\n this.peers.set(key, { peerId, transport });\n }\n\n /** Forgets one peer link; the caller owns closing its transport. */\n removePeer(\n peerId: string,\n channelName: string,\n ): IAttachmentTransport | undefined {\n const key = peerKey(peerId, channelName);\n const entry = this.peers.get(key);\n this.peers.delete(key);\n return entry?.transport;\n }\n\n /** Distinct linked peer ids. */\n peerNames(): readonly string[] {\n return [...new Set([...this.peers.values()].map((entry) => entry.peerId))];\n }\n\n /** The explicit Switchboard, then every gql remote's origin. */\n switchboardSources(): readonly string[] {\n const urls = new Set<string>();\n if (this.switchboardUrl) {\n urls.add(this.switchboardUrl);\n }\n for (const remote of this.syncManager?.list() ?? []) {\n if (remote.meta.channelConfig.type !== GQL_CHANNEL_TYPE) {\n continue;\n }\n const url = remote.meta.channelConfig.parameters.url;\n if (typeof url === \"string\" && url !== \"\") {\n urls.add(attachmentOriginOf(url));\n }\n }\n return [...urls];\n }\n\n async fetch(\n hash: AttachmentHash,\n documentId: string,\n signal?: AbortSignal,\n ): Promise<TransportFetchResult> {\n // An aborted fetch rejects; an answer would count as the sources' own.\n signal?.throwIfAborted();\n const peers = [...this.peers.values()].map((entry) => entry.transport);\n const switchboards = this.switchboardSources().map((url) =>\n this.switchboardFor(url),\n );\n if (peers.length === 0 && switchboards.length === 0) {\n return { kind: \"not-found\" };\n }\n\n const answers = new Answers();\n if (peers.length > 0) {\n const data = await this.askPeers(\n peers,\n hash,\n documentId,\n answers,\n signal,\n );\n if (data) {\n return data;\n }\n }\n for (const switchboard of switchboards) {\n signal?.throwIfAborted();\n const data = await this.ask(\n switchboard,\n hash,\n documentId,\n answers,\n signal,\n );\n if (data) {\n return data;\n }\n }\n signal?.throwIfAborted();\n return answers.result();\n }\n\n private async ask(\n source: IAttachmentTransport,\n hash: AttachmentHash,\n documentId: string,\n answers: Answers,\n signal: AbortSignal | undefined,\n ): Promise<DataResult | undefined> {\n let result: TransportFetchResult;\n try {\n result = await source.fetch(hash, documentId, signal);\n } catch (error) {\n answers.error(error);\n return undefined;\n }\n if (result.kind === \"pending\") {\n if (result.busy) {\n answers.busy ??= result;\n } else {\n answers.pending ??= result;\n }\n return undefined;\n }\n if (result.kind === \"not-found\") {\n answers.notFound = true;\n return undefined;\n }\n try {\n return await verified(hash, result);\n } catch (error) {\n answers.error(error);\n return undefined;\n }\n }\n\n /** The first verified data wins and cancels the other peers. */\n private askPeers(\n peers: IAttachmentTransport[],\n hash: AttachmentHash,\n documentId: string,\n answers: Answers,\n signal: AbortSignal | undefined,\n ): Promise<DataResult | undefined> {\n const controllers = peers.map(() => new AbortController());\n const abortAll = (): void => {\n for (const controller of controllers) controller.abort();\n };\n if (signal?.aborted) {\n abortAll();\n }\n signal?.addEventListener(\"abort\", abortAll, { once: true });\n\n return new Promise<DataResult | undefined>((resolve) => {\n let remaining = peers.length;\n let won = false;\n peers.forEach((peer, index) => {\n void this.ask(\n peer,\n hash,\n documentId,\n answers,\n controllers[index].signal,\n ).then((data) => {\n remaining -= 1;\n if (data && !won) {\n won = true;\n controllers.forEach((controller, other) => {\n if (other !== index) controller.abort();\n });\n resolve(data);\n return;\n }\n if (remaining === 0 && !won) {\n resolve(undefined);\n }\n });\n });\n }).finally(() => signal?.removeEventListener(\"abort\", abortAll));\n }\n\n /** Best effort across every source. */\n async announce(hash: AttachmentHash): Promise<void> {\n await Promise.allSettled(\n this.sources().map((source) => source.announce(hash)),\n );\n }\n\n push(): Promise<void> {\n return Promise.reject(\n new Error(\n \"PeeredAttachmentTransport is pull-only: bytes move when a reactor references a hash it lacks, not by being pushed\",\n ),\n );\n }\n\n private sources(): IAttachmentTransport[] {\n return [\n ...[...this.peers.values()].map((entry) => entry.transport),\n ...this.switchboardSources().map((url) => this.switchboardFor(url)),\n ];\n }\n\n private switchboardFor(url: string): IAttachmentTransport {\n const existing = this.switchboards.get(url);\n if (existing) {\n return existing;\n }\n const transport = new SwitchboardAttachmentTransport({\n remoteUrl: url,\n ...(this.jwtHandler ? { jwtHandler: this.jwtHandler } : {}),\n ...(this.fetchFn ? { fetchFn: this.fetchFn } : {}),\n });\n this.switchboards.set(url, transport);\n return transport;\n }\n}\n\n/** NUL-joined so no peer id or channel name can forge another pair's key. */\nfunction peerKey(peerId: string, channelName: string): string {\n return `${peerId}\\u0000${channelName}`;\n}\n\n/** Attachments are served from the origin, not under the GraphQL path. */\nexport function attachmentOriginOf(graphqlUrl: string): string {\n try {\n return new URL(graphqlUrl).origin;\n } catch {\n const marker = graphqlUrl.lastIndexOf(\"/graphql\");\n const base = marker >= 0 ? graphqlUrl.slice(0, marker) : graphqlUrl;\n return base.replace(/\\/+$/, \"\");\n }\n}\n","import type {\n AttachmentReplicationState,\n AttachmentRetryPolicy,\n} from \"./types.js\";\n\n/** setTimeout's largest delay; a longer one fires at once. */\nexport const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;\n\n/** One hash's retry bookkeeping; changed only by the functions below. */\nexport type RetryEntry = {\n /** Cumulative until resetRetry; an expired pending counts as one. */\n notFoundAnswers: number;\n /** Per run: errors since the last answer (pending, not-found or data). */\n errorRun: number;\n /** Per run: pendings since the last non-pending outcome; backoff only. */\n pendingRun: number;\n /** Per run: busy answers since the last other outcome; a run reaching the cap is one not-found or error. */\n busyRun: number;\n /** While live, a not-found from another document is not terminal. */\n livePending: { documentId: string; untilMs: number } | undefined;\n /** Never answered through, oldest first; asked before `asked`. */\n unasked: readonly string[];\n /** Next to ask first; pending moves to the front, not-found to the back. */\n asked: readonly string[];\n};\n\nexport type FetchOutcome =\n | { kind: \"data\" }\n | {\n kind: \"pending\";\n documentId: string;\n expiresAtUtc: string;\n retryAfterMs: number;\n }\n | {\n kind: \"busy\";\n documentId: string;\n retryAfterMs: number;\n /** Every source that was not busy answered not-found. */\n othersNotFound: boolean;\n }\n | { kind: \"not-found\"; documentId: string }\n | { kind: \"error\"; documentId: string }\n | { kind: \"aborted\" };\n\nexport type RetryState = \"held\" | \"queued\" | \"waiting\" | \"not-found\" | \"failed\";\n\nexport type RetryTransition = {\n entry: RetryEntry;\n state: RetryState;\n /** Set exactly when `state` is `waiting`. */\n delayMs: number | undefined;\n};\n\nexport function newRetryEntry(documentId: string): RetryEntry {\n return {\n notFoundAnswers: 0,\n errorRun: 0,\n pendingRun: 0,\n busyRun: 0,\n livePending: undefined,\n unasked: [documentId],\n asked: [],\n };\n}\n\n/** The document the next attempt asks through. */\nexport function documentToAsk(entry: RetryEntry): string | undefined {\n return entry.unasked[0] ?? entry.asked[0];\n}\n\n/** Every counter zeroed and every document unasked again. */\nexport function resetRetry(documentIds: readonly string[]): RetryEntry {\n return {\n notFoundAnswers: 0,\n errorRun: 0,\n pendingRun: 0,\n busyRun: 0,\n livePending: undefined,\n unasked: [...documentIds],\n asked: [],\n };\n}\n\n/** `revive`: a terminal entry earns one more attempt through the new document. */\nexport function withDocument(\n entry: RetryEntry,\n documentId: string,\n state: AttachmentReplicationState,\n): { entry: RetryEntry; revive: boolean } {\n return {\n entry: { ...entry, unasked: [...entry.unasked, documentId] },\n revive: state === \"not-found\" || state === \"failed\",\n };\n}\n\n/** The whole retry policy; `now` is epoch ms. */\nexport function nextAfter(\n entry: RetryEntry,\n outcome: FetchOutcome,\n now: number,\n policy: AttachmentRetryPolicy,\n): RetryTransition {\n switch (outcome.kind) {\n case \"data\":\n return {\n entry: {\n ...entry,\n errorRun: 0,\n pendingRun: 0,\n busyRun: 0,\n livePending: undefined,\n },\n state: \"held\",\n delayMs: undefined,\n };\n case \"aborted\":\n return { entry, state: \"queued\", delayMs: undefined };\n case \"pending\": {\n const untilMs =\n typeof outcome.expiresAtUtc === \"string\"\n ? Date.parse(outcome.expiresAtUtc)\n : Number.NaN;\n if (!(untilMs > now)) {\n return notFound(entry, outcome.documentId, now, policy);\n }\n const pendingRun = entry.pendingRun + 1;\n return {\n entry: {\n ...entry,\n errorRun: 0,\n pendingRun,\n busyRun: 0,\n livePending: { documentId: outcome.documentId, untilMs },\n unasked: without(entry.unasked, outcome.documentId),\n asked: [\n outcome.documentId,\n ...without(entry.asked, outcome.documentId),\n ],\n },\n state: \"waiting\",\n delayMs: pendingDelay(outcome.retryAfterMs, pendingRun, policy),\n };\n }\n case \"busy\": {\n // Not a reservation: no budget is spent and no run ends, but a run\n // that backs off to the cap counts as one not-found when every other\n // source said so, else as one error.\n const busyRun = entry.busyRun + 1;\n if (\n pendingGrowth(outcome.retryAfterMs, busyRun, policy) >=\n policy.maxPendingRetryMs\n ) {\n return outcome.othersNotFound\n ? notFound(entry, outcome.documentId, now, policy)\n : failure(entry, outcome.documentId, policy);\n }\n return {\n entry: { ...entry, busyRun },\n state: \"waiting\",\n delayMs: pendingDelay(outcome.retryAfterMs, busyRun, policy),\n };\n }\n case \"not-found\":\n return notFound(entry, outcome.documentId, now, policy);\n case \"error\":\n return failure(entry, outcome.documentId, policy);\n }\n}\n\nfunction failure(\n entry: RetryEntry,\n documentId: string,\n policy: AttachmentRetryPolicy,\n): RetryTransition {\n const errorRun = entry.errorRun + 1;\n if (errorRun < policy.errorAttempts) {\n return {\n entry: { ...entry, errorRun, pendingRun: 0, busyRun: 0 },\n state: \"waiting\",\n delayMs: backoff(policy.errorRetryMs, errorRun, policy),\n };\n }\n const next: RetryEntry = {\n ...entry,\n errorRun,\n pendingRun: 0,\n busyRun: 0,\n unasked: without(entry.unasked, documentId),\n asked: entry.asked.includes(documentId)\n ? entry.asked\n : [...entry.asked, documentId],\n };\n if (next.unasked.length > 0) {\n return { entry: next, state: \"waiting\", delayMs: policy.errorRetryMs };\n }\n return { entry: next, state: \"failed\", delayMs: undefined };\n}\n\nfunction notFound(\n entry: RetryEntry,\n documentId: string,\n now: number,\n policy: AttachmentRetryPolicy,\n): RetryTransition {\n const notFoundAnswers = entry.notFoundAnswers + 1;\n const livePending =\n entry.livePending &&\n entry.livePending.documentId !== documentId &&\n entry.livePending.untilMs > now\n ? entry.livePending\n : undefined;\n const next: RetryEntry = {\n ...entry,\n notFoundAnswers,\n errorRun: 0,\n pendingRun: 0,\n busyRun: 0,\n livePending,\n unasked: without(entry.unasked, documentId),\n asked: [...without(entry.asked, documentId), documentId],\n };\n // The next ask is an unasked document or the pending one; neither has\n // answered not-found, so neither earns a longer wait.\n if (next.unasked.length > 0 || livePending !== undefined) {\n return { entry: next, state: \"waiting\", delayMs: policy.notFoundRetryMs };\n }\n if (notFoundAnswers < policy.notFoundAttempts) {\n return {\n entry: next,\n state: \"waiting\",\n delayMs: backoff(policy.notFoundRetryMs, notFoundAnswers, policy),\n };\n }\n return { entry: next, state: \"not-found\", delayMs: undefined };\n}\n\n/** A run's uncapped wait: the asked delay, floored, doubled per answer. */\nfunction pendingGrowth(\n retryAfterMs: number,\n run: number,\n policy: AttachmentRetryPolicy,\n): number {\n const asked =\n Number.isFinite(retryAfterMs) && retryAfterMs >= 0\n ? retryAfterMs\n : policy.pendingRetryMs;\n return Math.max(asked, policy.minPendingRetryMs) * 2 ** (run - 1);\n}\n\nfunction pendingDelay(\n retryAfterMs: number,\n pendingRun: number,\n policy: AttachmentRetryPolicy,\n): number {\n const grown = Math.min(\n pendingGrowth(retryAfterMs, pendingRun, policy),\n policy.maxPendingRetryMs,\n );\n return Math.min(\n Math.max(grown, policy.minPendingRetryMs),\n MAX_TIMER_DELAY_MS,\n );\n}\n\nfunction backoff(\n baseMs: number,\n run: number,\n policy: AttachmentRetryPolicy,\n): number {\n return Math.min(\n baseMs * 2 ** (run - 1),\n policy.maxRetryMs,\n MAX_TIMER_DELAY_MS,\n );\n}\n\nfunction without(list: readonly string[], item: string): string[] {\n return list.filter((entry) => entry !== item);\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport type { OperationWithContext } from \"@powerhousedao/shared/document-model\";\n\n/**\n * Pulls the attachment refs an operation carries.\n *\n * The replicator depends on this rather than on the schema compiler directly\n * so a host that has no compiled document models (a test, a reactor whose\n * models declare no `AttachmentRef` field but carry refs by another\n * convention) can still feed it. The production implementation is\n * `SchemaCompiledOperationRefs`, which is the same compiler + registry pair\n * `AttachmentReferenceReadModel` uses, so the refs the replicator chases and\n * the refs the reference index records are extracted by one piece of code.\n */\nexport interface IOperationAttachmentRefs {\n refsOf(item: OperationWithContext): readonly AttachmentRef[];\n}\n\n/**\n * What the replicator is doing about one hash.\n *\n * - `queued`: known missing locally, waiting for a concurrency slot.\n * - `fetching`: a transport fetch is in flight.\n * - `held`: the bytes are in the local store. Terminal and the goal; a held\n * hash leaves the report.\n * - `waiting`: the last answer was an unexpired `pending`, a bounded\n * `not-found` (see {@link AttachmentRetryPolicy.notFoundAttempts}), or a\n * transport error; a retry is scheduled.\n * - `not-found`: the `not-found` budget is spent (an expired `pending` counts\n * as a `not-found`), no document is left unasked and no `pending` is live.\n * - `failed`: the transport kept erroring and no document is left unasked.\n *\n * Both terminal states wait for `retry()`, or for a document not yet seen to\n * reference the hash, which earns one more attempt through that document.\n */\nexport type AttachmentReplicationState =\n | \"queued\"\n | \"fetching\"\n | \"held\"\n | \"waiting\"\n | \"not-found\"\n | \"failed\";\n\n/**\n * Retry shape for the three answers a fetch can give, honouring the\n * `pending | not-found | data` triad that\n * {@link IAttachmentTransport.fetch} exists to express.\n *\n * `notFoundAttempts` is the reference-index race budget and the reason\n * `not-found` is retried at all: a peer authorizes a byte fetch through its\n * OWN attachment reference index, and that index lags that peer's sync. So the\n * first `not-found` for a freshly synced ref is far more likely to mean \"that\n * peer has not indexed this operation yet\" than \"these bytes do not exist\".\n * Retrying it a bounded number of times covers the lag without turning a\n * genuinely missing hash into an unbounded loop -- and the counts keep both\n * outcomes visible rather than papering over either.\n */\nexport type AttachmentRetryPolicy = {\n /** Fallback wait when a `pending` answer carries no `retryAfterMs`. */\n pendingRetryMs: number;\n /** Shortest wait honoured after a `pending`, whatever it asks for. */\n minPendingRetryMs: number;\n /** Cap for a run of `pending` answers, whose wait doubles per answer. */\n maxPendingRetryMs: number;\n /** `not-found` answers absorbed as index lag; cumulative until `retry()`. */\n notFoundAttempts: number;\n /** Base wait between `not-found` retries; doubles per attempt. */\n notFoundRetryMs: number;\n /** Transport errors in a row before giving up; any answer ends the run. */\n errorAttempts: number;\n /** Base wait between error retries; doubles per attempt. */\n errorRetryMs: number;\n /** Cap for not-found and error waits. */\n maxRetryMs: number;\n};\n\nexport const DEFAULT_ATTACHMENT_RETRY_POLICY: AttachmentRetryPolicy = {\n pendingRetryMs: 5_000,\n minPendingRetryMs: 250,\n maxPendingRetryMs: 300_000,\n notFoundAttempts: 3,\n notFoundRetryMs: 2_000,\n errorAttempts: 5,\n errorRetryMs: 1_000,\n maxRetryMs: 300_000,\n};\n\n/** Default bounded concurrency for in-flight attachment fetches. */\nexport const DEFAULT_ATTACHMENT_REPLICATION_CONCURRENCY = 3;\n\n/** Default number of held hashes a replicator remembers after dropping their entries. */\nexport const DEFAULT_ATTACHMENT_HELD_HASH_LIMIT = 10_000;\n\n/** Default page size for the boot re-scan over the reference backlog. */\nexport const DEFAULT_ATTACHMENT_BACKLOG_PAGE_SIZE = 200;\n\n/**\n * The replicator's observable state: what it has seen, what it holds, and what\n * it is still arguing about.\n *\n * Reported rather than inferred, because the two honest failure modes --\n * a peer that never had the bytes, and a peer whose reference index has not\n * caught up -- are indistinguishable from the outside at the moment they\n * happen and only separable over time. `notFound` and `waiting` are both\n * surfaced so an operator sees which one a reactor is in.\n */\nexport type AttachmentReplicatorStatus = {\n /** Whether the replicator is subscribed and scheduling. */\n running: boolean;\n /** Distinct hashes tracked: every entry plus the remembered held hashes. */\n refsSeen: number;\n /** Remembered held hashes, at most `heldHashLimit`. */\n held: number;\n /** `storageUsed()` of the local store, in bytes. */\n bytesHeld: number;\n /** Hashes waiting for a concurrency slot. */\n queued: number;\n /** Hashes with a fetch in flight. */\n fetching: number;\n /** Hashes with a retry scheduled (pending, lagging index, or error). */\n waiting: number;\n /** Hashes the peer answered `not-found` for, past the lag budget. */\n notFound: number;\n /** Hashes whose transport kept erroring. */\n failed: number;\n /** Whether the boot re-scan over the reference backlog has completed. */\n backlogScanned: boolean;\n /** The most recent transport error message, for the inspector. */\n lastError: string | undefined;\n};\n\n/** One hash's replication record, as reported for diagnostics. */\nexport type AttachmentReplicationEntry = {\n hash: AttachmentHash;\n state: AttachmentReplicationState;\n /** Documents whose operations referenced the hash; any of them may authorize. */\n documentIds: readonly string[];\n attempts: number;\n notFoundAnswers: number;\n /** Epoch ms of the next scheduled attempt, when one is scheduled. */\n nextAttemptAtMs: number | undefined;\n lastError: string | undefined;\n};\n","import {\n ReactorEventTypes,\n type AttachmentHash,\n type AttachmentRef,\n type IEventBus,\n type JobReadReadyEvent,\n type Unsubscribe,\n} from \"@powerhousedao/reactor\";\nimport type { OperationWithContext } from \"@powerhousedao/shared/document-model\";\nimport type { IAttachmentStore, IAttachmentTransport } from \"../interfaces.js\";\nimport type {\n AttachmentReferenceRow,\n IAttachmentReferenceScanner,\n} from \"../read-models/attachment-reference/types.js\";\nimport { parseRef } from \"../ref.js\";\nimport { collectStream, streamFromBytes } from \"../storage/local/bytes.js\";\nimport { sha256Hex } from \"./hash.js\";\nimport {\n documentToAsk,\n MAX_TIMER_DELAY_MS,\n newRetryEntry,\n nextAfter,\n resetRetry,\n withDocument,\n type FetchOutcome,\n type RetryEntry,\n} from \"./retry-state.js\";\nimport {\n DEFAULT_ATTACHMENT_BACKLOG_PAGE_SIZE,\n DEFAULT_ATTACHMENT_HELD_HASH_LIMIT,\n DEFAULT_ATTACHMENT_REPLICATION_CONCURRENCY,\n DEFAULT_ATTACHMENT_RETRY_POLICY,\n type AttachmentReplicationEntry,\n type AttachmentReplicationState,\n type AttachmentReplicatorStatus,\n type AttachmentRetryPolicy,\n type IOperationAttachmentRefs,\n} from \"./types.js\";\n\n/**\n * Clock and timer seam. Injected rather than taken from globals so a test can\n * drive every retry deadline deterministically, without fake timers racing the\n * replicator's own promises.\n */\nexport type ReplicationTimers = {\n /** Epoch ms; a pending answer's `expiresAtUtc` is compared against it. */\n now: () => number;\n setTimer: (callback: () => void, delayMs: number) => unknown;\n clearTimer: (handle: unknown) => void;\n};\n\nexport const SYSTEM_REPLICATION_TIMERS: ReplicationTimers = {\n now: () => Date.now(),\n setTimer: (callback, delayMs) => setTimeout(callback, delayMs),\n clearTimer: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n};\n\nexport type AttachmentReplicatorOptions = {\n /** Where bytes land, and the authority on what is already held. */\n store: IAttachmentStore;\n /** Where bytes come from. */\n transport: IAttachmentTransport;\n /** Pulls refs out of a committed operation. */\n refs: IOperationAttachmentRefs;\n /** The reactor's bus; the replicator subscribes to `JOB_READ_READY`. */\n eventBus: IEventBus;\n /**\n * The durable reference list re-scanned on {@link AttachmentReplicator.start}.\n * Omitted, the replicator only ever learns about refs from live operations --\n * correct for a reactor with no reference index, and NOT resumable across a\n * restart, which {@link AttachmentReplicatorStatus.backlogScanned} reports.\n */\n backlog?: IAttachmentReferenceScanner;\n concurrency?: number;\n retry?: Partial<AttachmentRetryPolicy>;\n backlogPageSize?: number;\n /**\n * Held hashes remembered after their entry is dropped, oldest evicted first.\n * An evicted hash referenced again costs one `store.has()`, never a fetch.\n */\n heldHashLimit?: number;\n /**\n * Whether fetched bytes are hashed and checked against the hash they were\n * requested under. Defaults to true: a local peer transport means the bytes\n * come from another reactor rather than from a trusted server, and a\n * content-addressed store that accepts bytes it never verified is no longer\n * content-addressed.\n */\n verifyHash?: boolean;\n timers?: ReplicationTimers;\n onDiagnostic?: (message: string, error?: unknown) => void;\n};\n\ntype Entry = {\n hash: AttachmentHash;\n state: AttachmentReplicationState;\n /** Insertion-ordered. */\n documentIds: string[];\n attempts: number;\n retry: RetryEntry;\n nextAttemptAtMs: number | undefined;\n lastError: string | undefined;\n};\n\n/**\n * Lazy fetch-on-reference: pulls attachment bytes a reactor's own operations\n * reference but whose hash its local store lacks.\n *\n * The seam is `ReactorEventTypes.JOB_READ_READY` on the reactor's event bus,\n * and that choice matters in two ways. It is AFTER the pre-ready read models,\n * so by the time the replicator asks a peer for a hash, this reactor's own\n * attachment reference index has recorded the reference -- the same reactor is\n * therefore able to serve the hash onward, and a reactor cannot be in the\n * position of chasing bytes it would refuse to authorize itself. And it is off\n * the write path: a slow or unreachable peer cannot delay a commit, because\n * the subscriber only notes hashes and returns.\n *\n * Resume is a re-scan of the reference index ({@link IAttachmentReferenceScanner}),\n * not a cursor: the index plus `store.has()` re-derives the exact outstanding\n * work set on every boot, idempotently, and a persisted cursor could only add a\n * second truth that is wrong in the one direction that loses data.\n *\n * Loop-safety is structural: one entry per hash, created once, and every\n * retry decision is {@link nextAfter}'s. A terminal entry (`not-found`,\n * `failed`) is never re-queued by a further reference from a document it\n * already knows; a document it has not seen earns it one more attempt, through\n * that document. Otherwise only {@link AttachmentReplicator.retry} moves a\n * terminal entry back. A held hash drops its entry and is remembered in a\n * bounded set, so the map holds only unfinished work.\n */\nexport class AttachmentReplicator {\n private readonly store: IAttachmentStore;\n private readonly transport: IAttachmentTransport;\n private readonly refs: IOperationAttachmentRefs;\n private readonly eventBus: IEventBus;\n private readonly backlog: IAttachmentReferenceScanner | undefined;\n private readonly concurrency: number;\n private readonly policy: AttachmentRetryPolicy;\n private readonly backlogPageSize: number;\n private readonly heldHashLimit: number;\n private readonly verifyHash: boolean;\n private readonly timers: ReplicationTimers;\n private readonly onDiagnostic: (message: string, error?: unknown) => void;\n\n private readonly entries = new Map<AttachmentHash, Entry>();\n private readonly heldHashes = new Set<AttachmentHash>();\n private readonly queue: AttachmentHash[] = [];\n private readonly idleWaiters: Array<() => void> = [];\n private readonly aborts = new Set<AbortController>();\n\n private unsubscribe: Unsubscribe | undefined;\n private running = false;\n private inFlight = 0;\n private timerHandle: unknown;\n private timerAtMs: number | undefined;\n private backlogScan: Promise<void> | undefined;\n private backlogDone = false;\n private lastError: string | undefined;\n\n constructor(options: AttachmentReplicatorOptions) {\n this.store = options.store;\n this.transport = options.transport;\n this.refs = options.refs;\n this.eventBus = options.eventBus;\n this.backlog = options.backlog;\n this.concurrency =\n options.concurrency ?? DEFAULT_ATTACHMENT_REPLICATION_CONCURRENCY;\n this.policy = { ...DEFAULT_ATTACHMENT_RETRY_POLICY, ...options.retry };\n this.backlogPageSize =\n options.backlogPageSize ?? DEFAULT_ATTACHMENT_BACKLOG_PAGE_SIZE;\n this.heldHashLimit =\n options.heldHashLimit ?? DEFAULT_ATTACHMENT_HELD_HASH_LIMIT;\n this.verifyHash = options.verifyHash ?? true;\n this.timers = options.timers ?? SYSTEM_REPLICATION_TIMERS;\n this.onDiagnostic = options.onDiagnostic ?? ((): void => undefined);\n }\n\n /**\n * Subscribes to committed operations and starts the boot re-scan.\n *\n * The subscription is taken BEFORE the scan so no operation committed during\n * the scan can slip between the two. A hash both paths find is one entry.\n *\n * A restart resumes its own unfinished work: a `stop()` empties the queue but\n * keeps the entries, so entries left `queued` or `waiting` are re-armed here.\n * Without that, a hash that was outstanding at `stop()` would sit in the map\n * forever unless an unrelated live operation happened to pump the queue --\n * the backlog re-scan does not rescue it, because `observeReference` treats\n * an already-known hash as nothing to do.\n */\n start(): void {\n if (this.running) {\n return;\n }\n this.running = true;\n this.unsubscribe = this.eventBus.subscribe<JobReadReadyEvent>(\n ReactorEventTypes.JOB_READ_READY,\n (_type, event) => {\n this.observeOperations(event.operations);\n },\n );\n this.resumeOutstanding();\n this.backlogScan = this.scanBacklog();\n }\n\n /**\n * Stops scheduling, drops the subscription and aborts in-flight fetches.\n *\n * Entries are kept: the in-memory retry counters are not durable state and a\n * restart legitimately starts the budgets over (a hash that was `not-found`\n * because a peer's index lagged deserves another look after a reboot), but a\n * `stop()` that is not a restart should not forget what is held either --\n * `store.has()` is re-checked before every fetch regardless.\n */\n async stop(): Promise<void> {\n this.running = false;\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n this.cancelTimer();\n for (const controller of this.aborts) {\n controller.abort();\n }\n this.aborts.clear();\n this.queue.length = 0;\n const scan = this.backlogScan;\n this.backlogScan = undefined;\n if (scan) {\n await scan.catch(() => undefined);\n }\n }\n\n /** Notes every ref carried by `items`. Synchronous: it only enqueues. */\n observeOperations(items: readonly OperationWithContext[]): void {\n for (const item of items) {\n for (const ref of this.refs.refsOf(item)) {\n this.observeReference(item.context.documentId, ref);\n }\n }\n }\n\n /** Notes one (document, ref) pair, queueing the hash if it is new. */\n observeReference(documentId: string, ref: AttachmentRef): void {\n let hash: AttachmentHash;\n try {\n hash = parseRef(ref).hash;\n } catch (error) {\n this.onDiagnostic(`ignoring malformed attachment ref ${ref}`, error);\n return;\n }\n\n if (this.heldHashes.has(hash)) {\n return;\n }\n const existing = this.entries.get(hash);\n if (existing) {\n if (existing.documentIds.includes(documentId)) {\n return;\n }\n existing.documentIds.push(documentId);\n const { entry, revive } = withDocument(\n existing.retry,\n documentId,\n existing.state,\n );\n existing.retry = entry;\n if (revive) {\n existing.state = \"queued\";\n if (!this.queue.includes(hash)) {\n this.queue.push(hash);\n }\n this.pump();\n }\n return;\n }\n\n this.entries.set(hash, {\n hash,\n state: \"queued\",\n documentIds: [documentId],\n attempts: 0,\n retry: newRetryEntry(documentId),\n nextAttemptAtMs: undefined,\n lastError: undefined,\n });\n this.queue.push(hash);\n this.pump();\n }\n\n async status(): Promise<AttachmentReplicatorStatus> {\n const counts: Record<AttachmentReplicationState, number> = {\n queued: 0,\n fetching: 0,\n held: 0,\n waiting: 0,\n \"not-found\": 0,\n failed: 0,\n };\n for (const entry of this.entries.values()) {\n counts[entry.state] += 1;\n }\n const bytesHeld = await this.store.storageUsed();\n return {\n running: this.running,\n refsSeen: this.entries.size + this.heldHashes.size,\n held: this.heldHashes.size,\n bytesHeld,\n queued: counts.queued,\n fetching: counts.fetching,\n waiting: counts.waiting,\n notFound: counts[\"not-found\"],\n failed: counts.failed,\n backlogScanned: this.backlogDone,\n lastError: this.lastError,\n };\n }\n\n /** Every hash not yet held and what is happening to it. */\n report(): AttachmentReplicationEntry[] {\n return [...this.entries.values()].map((entry) => ({\n hash: entry.hash,\n state: entry.state,\n documentIds: [...entry.documentIds],\n attempts: entry.attempts,\n notFoundAnswers: entry.retry.notFoundAnswers,\n nextAttemptAtMs: entry.nextAttemptAtMs,\n lastError: entry.lastError,\n }));\n }\n\n /**\n * Re-queues terminal entries so a `not-found` or `failed` hash is chased\n * again, resetting their budgets. `hash` narrows it to one; absent, every\n * terminal entry is retried.\n *\n * The deliberate manual lever over the reference-index race: when an\n * operator can see that a peer has caught up, nothing should make them wait\n * for a reboot to find out.\n */\n retry(hash?: AttachmentHash): void {\n const targets = hash\n ? [this.entries.get(hash)].filter((entry) => entry !== undefined)\n : [...this.entries.values()];\n for (const entry of targets) {\n if (entry.state === \"held\" || entry.state === \"fetching\") {\n continue;\n }\n entry.state = \"queued\";\n entry.retry = resetRetry(entry.documentIds);\n entry.nextAttemptAtMs = undefined;\n if (!this.queue.includes(entry.hash)) {\n this.queue.push(entry.hash);\n }\n }\n this.pump();\n }\n\n /**\n * Resolves when nothing is queued or in flight.\n *\n * Entries `waiting` on a retry deadline do NOT hold this open -- their next\n * attempt is the timer's business, and a caller that wants to reach it\n * advances the injected clock. A test seam, and what a load harness waits on.\n */\n idle(): Promise<void> {\n if (this.queue.length === 0 && this.inFlight === 0) {\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n this.idleWaiters.push(resolve);\n });\n }\n\n /** Awaits the boot re-scan; resolves immediately when there is no backlog. */\n async backlogScanned(): Promise<void> {\n await this.backlogScan;\n }\n\n /** Re-queues every non-terminal entry a previous `stop()` stranded. */\n private resumeOutstanding(): void {\n for (const entry of this.entries.values()) {\n if (entry.state !== \"queued\" && entry.state !== \"waiting\") {\n continue;\n }\n entry.state = \"queued\";\n entry.nextAttemptAtMs = undefined;\n if (!this.queue.includes(entry.hash)) {\n this.queue.push(entry.hash);\n }\n }\n this.pump();\n }\n\n private async scanBacklog(): Promise<void> {\n const backlog = this.backlog;\n if (!backlog) {\n // No reference index to re-scan: this reactor learns refs only from live\n // operations and is NOT resumable across a restart, so `backlogScanned`\n // stays false.\n return;\n }\n\n let cursor: string | undefined;\n for (;;) {\n if (!this.running) {\n return;\n }\n let page;\n try {\n page = await backlog.listReferences(cursor, this.backlogPageSize);\n } catch (error) {\n // A reference index that cannot be read is not a reason to stop\n // replicating live operations; it only costs resumability, which the\n // status then reports as an unfinished scan.\n this.onDiagnostic(\n \"scanning the attachment reference backlog failed\",\n error,\n );\n return;\n }\n for (const reference of page.references) {\n this.observeReference(reference.documentId, reference.ref);\n }\n if (page.nextCursor === undefined) {\n break;\n }\n cursor = page.nextCursor;\n }\n this.backlogDone = true;\n }\n\n /** Starts as many queued fetches as the concurrency bound allows. */\n private pump(): void {\n while (\n this.running &&\n this.inFlight < this.concurrency &&\n this.queue.length > 0\n ) {\n const hash = this.queue.shift();\n if (hash === undefined) {\n break;\n }\n const entry = this.entries.get(hash);\n if (!entry || entry.state !== \"queued\") {\n continue;\n }\n void this.fetchOne(entry);\n }\n this.settleIdle();\n }\n\n private async fetchOne(entry: Entry): Promise<void> {\n entry.state = \"fetching\";\n entry.attempts += 1;\n this.inFlight += 1;\n const controller = new AbortController();\n this.aborts.add(controller);\n const documentId = documentToAsk(entry.retry) ?? entry.documentIds[0];\n let outcome: FetchOutcome;\n try {\n outcome = await this.attempt(entry, documentId, controller.signal);\n } catch (error) {\n if (controller.signal.aborted) {\n outcome = { kind: \"aborted\" };\n } else {\n this.recordError(entry, error);\n outcome = { kind: \"error\", documentId };\n }\n }\n try {\n this.apply(entry, outcome);\n } finally {\n this.aborts.delete(controller);\n this.inFlight -= 1;\n this.pump();\n }\n }\n\n private async attempt(\n entry: Entry,\n documentId: string,\n signal: AbortSignal,\n ): Promise<FetchOutcome> {\n // Re-checked every attempt, not once at enqueue: another path (a local\n // upload, a store-level re-fetch, a previous attempt that lost the race)\n // may have landed the bytes meanwhile, and asking a peer for bytes already\n // held is the one wasted round trip worth a cheap local read to avoid.\n if (await this.store.has(entry.hash)) {\n return { kind: \"data\" };\n }\n signal.throwIfAborted();\n\n const result = await this.transport.fetch(entry.hash, documentId, signal);\n if (result.kind === \"pending\" && result.busy) {\n return {\n kind: \"busy\",\n documentId,\n retryAfterMs: result.retryAfterMs,\n othersNotFound: result.othersNotFound === true,\n };\n }\n if (result.kind === \"pending\") {\n return {\n kind: \"pending\",\n documentId,\n expiresAtUtc: result.expiresAtUtc,\n retryAfterMs: result.retryAfterMs,\n };\n }\n if (result.kind === \"not-found\") {\n return { kind: \"not-found\", documentId };\n }\n\n const bytes = await collectStream(result.response.body);\n if (this.verifyHash) {\n const actual = await sha256Hex(bytes);\n if (actual !== entry.hash) {\n throw new Error(\n `Attachment bytes for ${entry.hash} hashed to ${actual}; the peer served content that is not what was asked for`,\n );\n }\n }\n await this.store.put(\n entry.hash,\n result.response.metadata,\n streamFromBytes(bytes),\n );\n return { kind: \"data\" };\n }\n\n private apply(entry: Entry, outcome: FetchOutcome): void {\n const next = nextAfter(\n entry.retry,\n outcome,\n this.timers.now(),\n this.policy,\n );\n if (outcome.kind === \"busy\" && next.entry.errorRun > entry.retry.errorRun) {\n this.recordError(\n entry,\n new Error(\n `the attachment source stayed busy for ${entry.hash} through a full backoff`,\n ),\n );\n }\n entry.retry = next.entry;\n switch (next.state) {\n case \"held\":\n this.markHeld(entry);\n return;\n case \"waiting\":\n this.schedule(entry, next.delayMs ?? 0);\n return;\n case \"queued\":\n // Only stop() aborts. Stopped, the next start() re-queues it; already\n // restarted, resumeOutstanding saw it fetching, so queue it here.\n entry.state = \"queued\";\n entry.nextAttemptAtMs = undefined;\n if (this.running && !this.queue.includes(entry.hash)) {\n this.queue.push(entry.hash);\n }\n return;\n case \"not-found\":\n case \"failed\":\n entry.state = next.state;\n entry.nextAttemptAtMs = undefined;\n return;\n }\n }\n\n private markHeld(entry: Entry): void {\n entry.state = \"held\";\n entry.nextAttemptAtMs = undefined;\n this.entries.delete(entry.hash);\n this.heldHashes.delete(entry.hash);\n this.heldHashes.add(entry.hash);\n for (const oldest of this.heldHashes) {\n if (this.heldHashes.size <= this.heldHashLimit) {\n break;\n }\n this.heldHashes.delete(oldest);\n }\n }\n\n private recordError(entry: Entry, error: unknown): void {\n const message = error instanceof Error ? error.message : String(error);\n entry.lastError = message;\n this.lastError = message;\n this.onDiagnostic(`fetching attachment ${entry.hash} failed`, error);\n }\n\n private schedule(entry: Entry, delayMs: number): void {\n entry.state = \"waiting\";\n entry.nextAttemptAtMs =\n this.timers.now() + Math.min(Math.max(delayMs, 0), MAX_TIMER_DELAY_MS);\n this.armTimer();\n }\n\n /** Keeps exactly one timer, set to the earliest outstanding deadline. */\n private armTimer(): void {\n if (!this.running) {\n return;\n }\n let earliest: number | undefined;\n for (const entry of this.entries.values()) {\n if (entry.state !== \"waiting\" || entry.nextAttemptAtMs === undefined) {\n continue;\n }\n if (earliest === undefined || entry.nextAttemptAtMs < earliest) {\n earliest = entry.nextAttemptAtMs;\n }\n }\n if (earliest === undefined) {\n this.cancelTimer();\n return;\n }\n if (this.timerAtMs !== undefined && this.timerAtMs <= earliest) {\n return;\n }\n this.cancelTimer();\n this.timerAtMs = earliest;\n this.timerHandle = this.timers.setTimer(\n () => this.onTimer(),\n Math.max(earliest - this.timers.now(), 0),\n );\n }\n\n private onTimer(): void {\n this.timerHandle = undefined;\n this.timerAtMs = undefined;\n if (!this.running) {\n return;\n }\n const nowMs = this.timers.now();\n for (const entry of this.entries.values()) {\n if (\n entry.state === \"waiting\" &&\n entry.nextAttemptAtMs !== undefined &&\n entry.nextAttemptAtMs <= nowMs\n ) {\n entry.state = \"queued\";\n entry.nextAttemptAtMs = undefined;\n if (!this.queue.includes(entry.hash)) {\n this.queue.push(entry.hash);\n }\n }\n }\n this.pump();\n this.armTimer();\n }\n\n private cancelTimer(): void {\n if (this.timerHandle !== undefined) {\n this.timers.clearTimer(this.timerHandle);\n }\n this.timerHandle = undefined;\n this.timerAtMs = undefined;\n }\n\n private settleIdle(): void {\n if (this.queue.length > 0 || this.inFlight > 0) {\n return;\n }\n for (const resolve of this.idleWaiters.splice(0)) {\n resolve();\n }\n }\n}\n\n/** A scanner over a plain list, for a host without a reference index. */\nexport function staticAttachmentReferenceScanner(\n references: readonly AttachmentReferenceRow[],\n): IAttachmentReferenceScanner {\n return {\n listReferences: (cursor, limit) => {\n const start = cursor === undefined ? 0 : Number(cursor);\n const page = references.slice(start, start + limit);\n const next = start + page.length;\n return Promise.resolve({\n references: page,\n nextCursor: next < references.length ? String(next) : undefined,\n });\n },\n };\n}\n","import type {\n AttachmentRef,\n IDocumentModelRegistry,\n} from \"@powerhousedao/reactor\";\nimport {\n isPurgeMarker,\n type OperationWithContext,\n} from \"@powerhousedao/shared/document-model\";\nimport { AttachmentSchemaCompiler } from \"../reference-index/attachment-schema-compiler.js\";\nimport type { IAttachmentSchemaCompiler } from \"../reference-index/types.js\";\nimport type { IOperationAttachmentRefs } from \"./types.js\";\n\n/**\n * The production {@link IOperationAttachmentRefs}: the compiled, schema-driven\n * extractor, reused rather than reimplemented.\n *\n * This is the same `(registry, compiler)` pair `AttachmentReferenceReadModel`\n * runs, so a ref the index records and a ref the replicator chases are found by\n * one piece of code -- including the compiler's per-module/per-action cache,\n * which is held by the compiler instance a host shares between the two. A host\n * with no read model to share one with takes the default compiler.\n *\n * Extraction failures do NOT propagate. A malformed ref or an unregistered\n * document type must not take down the event-bus subscriber the replicator\n * runs on: that would turn an attachment problem into a reactor-wide write\n * failure, since `IEventBus.emit` aggregates subscriber errors back to the\n * emitter. The read model is the component whose job is to refuse a bad ref\n * loudly; this one is a best-effort byte chaser, so it reports and moves on.\n */\nexport class SchemaCompiledOperationRefs implements IOperationAttachmentRefs {\n constructor(\n private readonly registry: IDocumentModelRegistry,\n private readonly compiler: IAttachmentSchemaCompiler = new AttachmentSchemaCompiler(),\n private readonly onDiagnostic: (\n message: string,\n error: unknown,\n ) => void = () => undefined,\n ) {}\n\n refsOf(item: OperationWithContext): readonly AttachmentRef[] {\n const { operation, context } = item;\n if (isPurgeMarker(operation) || operation.error !== undefined) {\n return [];\n }\n try {\n const module = this.registry.getModule(context.documentType);\n return this.compiler\n .forModuleAction(module, operation.action.type)\n .extract(operation.action);\n } catch (error) {\n this.onDiagnostic(\n `extracting attachment refs from ${context.documentType}/${operation.action.type} failed`,\n error,\n );\n return [];\n }\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type {\n ILocalAttachmentBackend,\n LocalAttachmentRecord,\n} from \"./types.js\";\n\n/** Metadata rows, keyed by content hash. */\nexport const IDB_RECORD_STORE = \"records\";\n/** Blob rows (`{ hash, bytes }`), keyed by the same content hash. */\nexport const IDB_BLOB_STORE = \"blobs\";\n/** Index over {@link LocalAttachmentRecord.status}; what `storageUsed()` scans. */\nexport const IDB_STATUS_INDEX = \"by_status\";\n/** Default database name. One database holds one reactor's attachment store. */\nexport const DEFAULT_IDB_DATABASE = \"ph-attachments\";\nconst IDB_VERSION = 1;\n\n/** A blob row as it is stored. `bytes` is an ArrayBuffer: natively clone-safe. */\ntype BlobRow = {\n hash: AttachmentHash;\n bytes: ArrayBuffer;\n};\n\nexport type IdbAttachmentBackendOptions = {\n /**\n * Database name; defaults to {@link DEFAULT_IDB_DATABASE}. A host running\n * several reactors in one origin must namespace this per reactor, exactly as\n * the operation store is namespaced -- two reactors sharing one attachment\n * database would share evictions and storage accounting.\n */\n databaseName?: string;\n /**\n * The factory to open against; defaults to the realm's `indexedDB`. The seam\n * a shim (or a test double) is injected through, and the reason this backend\n * takes no dependency on a global being present at import time.\n */\n indexedDB?: IDBFactory;\n};\n\nfunction requestResult<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () =>\n reject(request.error ?? new Error(\"IndexedDB request failed\"));\n });\n}\n\nfunction transactionDone(transaction: IDBTransaction): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n transaction.oncomplete = () => resolve();\n transaction.onabort = () =>\n reject(transaction.error ?? new Error(\"IndexedDB transaction aborted\"));\n transaction.onerror = () =>\n reject(transaction.error ?? new Error(\"IndexedDB transaction failed\"));\n });\n}\n\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy.buffer;\n}\n\n/**\n * An IndexedDB-backed {@link ILocalAttachmentBackend}: the browser-resident\n * attachment store a reactor in a tab or a SharedWorker replicates into.\n *\n * Two object stores, written in one transaction so a record and its blob land\n * or fail together (the atomicity {@link ILocalAttachmentBackend.write}\n * requires). Bytes are kept as `ArrayBuffer` rather than `Blob`: both are\n * structured-clone-safe values IndexedDB stores natively, and an ArrayBuffer\n * needs no async read to get back to the `Uint8Array` the store serves.\n *\n * OPFS is the obvious later alternative for large blobs -- it streams and does\n * not hold a whole attachment in the realm's heap -- and nothing above this\n * interface would change: it is one more backend.\n *\n * The database handle is opened lazily on first use and reused, so constructing\n * this in a realm that has no `indexedDB` (a Node import of the barrel) costs\n * nothing and fails only if something actually reads or writes.\n */\nexport class IdbAttachmentBackend implements ILocalAttachmentBackend {\n private readonly databaseName: string;\n private readonly factory: IDBFactory | undefined;\n private opening: Promise<IDBDatabase> | undefined;\n private database: IDBDatabase | undefined;\n private closed = false;\n\n constructor(options: IdbAttachmentBackendOptions = {}) {\n this.databaseName = options.databaseName ?? DEFAULT_IDB_DATABASE;\n this.factory = options.indexedDB ?? globalThis.indexedDB;\n }\n\n async readRecord(\n hash: AttachmentHash,\n ): Promise<LocalAttachmentRecord | undefined> {\n const db = await this.open();\n const transaction = db.transaction(IDB_RECORD_STORE, \"readonly\");\n const row = await requestResult<LocalAttachmentRecord | undefined>(\n transaction.objectStore(IDB_RECORD_STORE).get(hash) as IDBRequest<\n LocalAttachmentRecord | undefined\n >,\n );\n return row ?? undefined;\n }\n\n async write(record: LocalAttachmentRecord, bytes: Uint8Array): Promise<void> {\n const db = await this.open();\n const transaction = db.transaction(\n [IDB_RECORD_STORE, IDB_BLOB_STORE],\n \"readwrite\",\n );\n const row: BlobRow = { hash: record.hash, bytes: toArrayBuffer(bytes) };\n transaction.objectStore(IDB_RECORD_STORE).put(record);\n transaction.objectStore(IDB_BLOB_STORE).put(row);\n await transactionDone(transaction);\n }\n\n async readBytes(hash: AttachmentHash): Promise<Uint8Array | undefined> {\n const db = await this.open();\n const transaction = db.transaction(IDB_BLOB_STORE, \"readonly\");\n const row = await requestResult<BlobRow | undefined>(\n transaction.objectStore(IDB_BLOB_STORE).get(hash) as IDBRequest<\n BlobRow | undefined\n >,\n );\n return row ? new Uint8Array(row.bytes) : undefined;\n }\n\n async evict(record: LocalAttachmentRecord): Promise<void> {\n const db = await this.open();\n const transaction = db.transaction(\n [IDB_RECORD_STORE, IDB_BLOB_STORE],\n \"readwrite\",\n );\n transaction.objectStore(IDB_RECORD_STORE).put(record);\n transaction.objectStore(IDB_BLOB_STORE).delete(record.hash);\n await transactionDone(transaction);\n }\n\n async touch(hash: AttachmentHash, lastAccessedAtUtc: string): Promise<void> {\n const db = await this.open();\n const transaction = db.transaction(IDB_RECORD_STORE, \"readwrite\");\n const store = transaction.objectStore(IDB_RECORD_STORE);\n const existing = await requestResult<LocalAttachmentRecord | undefined>(\n store.get(hash) as IDBRequest<LocalAttachmentRecord | undefined>,\n );\n if (existing) {\n store.put({ ...existing, lastAccessedAtUtc });\n }\n await transactionDone(transaction);\n }\n\n async availableBytes(): Promise<number> {\n const db = await this.open();\n const transaction = db.transaction(IDB_RECORD_STORE, \"readonly\");\n const index = transaction\n .objectStore(IDB_RECORD_STORE)\n .index(IDB_STATUS_INDEX);\n const rows = await requestResult<LocalAttachmentRecord[]>(\n index.getAll(\"available\") as IDBRequest<LocalAttachmentRecord[]>,\n );\n return rows.reduce((total, row) => total + row.sizeBytes, 0);\n }\n\n close(): Promise<void> {\n this.closed = true;\n this.database?.close();\n this.database = undefined;\n this.opening = undefined;\n return Promise.resolve();\n }\n\n private open(): Promise<IDBDatabase> {\n if (this.closed) {\n return Promise.reject(\n new Error(\n `IndexedDB attachment backend for database \"${this.databaseName}\" is closed`,\n ),\n );\n }\n if (this.database) {\n return Promise.resolve(this.database);\n }\n if (!this.opening) {\n const opening = this.openDatabase();\n this.opening = opening;\n opening.catch(() => {\n if (this.opening === opening) {\n this.opening = undefined;\n }\n });\n }\n return this.opening;\n }\n\n private openDatabase(): Promise<IDBDatabase> {\n const factory = this.factory;\n if (!factory) {\n // Stated rather than crashed on a missing global: this backend is\n // exported from a barrel a Node process imports, and the honest failure\n // is \"this realm has no IndexedDB\", not \"indexedDB is undefined\".\n return Promise.reject(\n new Error(\n \"No IndexedDB in this realm; the IndexedDB attachment backend needs a browser realm or an injected IDBFactory\",\n ),\n );\n }\n\n const request = factory.open(this.databaseName, IDB_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(IDB_RECORD_STORE)) {\n const records = db.createObjectStore(IDB_RECORD_STORE, {\n keyPath: \"hash\",\n });\n records.createIndex(IDB_STATUS_INDEX, \"status\", { unique: false });\n }\n if (!db.objectStoreNames.contains(IDB_BLOB_STORE)) {\n db.createObjectStore(IDB_BLOB_STORE, { keyPath: \"hash\" });\n }\n };\n\n return requestResult(request).then((db) => {\n if (this.closed) {\n // close() ran while this open was in flight. Keeping the handle would\n // leak a connection the backend will never release (and would re-arm\n // onversionchange over a dead instance); drop it and fail this caller\n // with the same closed error open() raises up front.\n db.close();\n throw new Error(\n `IndexedDB attachment backend for database \"${this.databaseName}\" is closed`,\n );\n }\n this.database = db;\n // A second tab asking for a higher version must not be blocked by this\n // handle; closing here loses nothing, the next call reopens.\n const forget = (): void => {\n if (this.database === db) {\n this.database = undefined;\n this.opening = undefined;\n }\n };\n db.onversionchange = () => {\n db.close();\n forget();\n };\n db.onclose = forget;\n return db;\n });\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport { AttachmentNotFound, AttachmentPending } from \"../../errors.js\";\nimport type {\n IAttachmentStore,\n IAttachmentTransport,\n} from \"../../interfaces.js\";\nimport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n} from \"../../types.js\";\nimport { sha256Hex } from \"../../replication/hash.js\";\nimport { collectStream, streamFromBytes } from \"./bytes.js\";\nimport type {\n ILocalAttachmentBackend,\n LocalAttachmentRecord,\n} from \"./types.js\";\n\nfunction headerOf(record: LocalAttachmentRecord): AttachmentHeader {\n return {\n hash: record.hash,\n mimeType: record.mimeType,\n fileName: record.fileName,\n sizeBytes: record.sizeBytes,\n extension: record.extension,\n status: record.status,\n source: record.source,\n createdAtUtc: record.createdAtUtc,\n lastAccessedAtUtc: record.lastAccessedAtUtc,\n // A local store holds no reservations, so a committed record never carries\n // an expiry. See LocalAttachmentRecord.\n expiresAtUtc: null,\n };\n}\n\n/**\n * A realm-resident, content-addressed {@link IAttachmentStore} over a\n * {@link ILocalAttachmentBackend} -- the browser-capable half of attachment\n * byte movement.\n *\n * The same class serves both backends, so an IndexedDB store in a tab and the\n * memory twin a Node test runs cannot diverge in behaviour: every semantic the\n * `IAttachmentStore` contract states is implemented once, here.\n *\n * Differences from `KyselyAttachmentStore`, all of them deliberate:\n *\n * - No reservation table, so `stat()`/`get()` never synthesize `pending` from\n * local state. `pending` only ever arrives as a TRANSPORT answer, which\n * `get()` raises as {@link AttachmentPending} exactly as the Kysely store\n * does for a remote pending.\n * - `get()` reads the whole blob before it hands back a stream, so there is no\n * such thing as an in-flight read for `evict()` to destroy. The contract's\n * \"skip hashes with active readers\" clause is satisfied by construction\n * rather than by a refcount; a caller already holding a stream holds a\n * snapshot.\n * - `sizeBytes` is the measured length of the bytes received, not the\n * producer's `AttachmentMetadata.sizeBytes` claim, so `storageUsed()` cannot\n * be talked into a wrong number by a remote.\n */\nexport class LocalAttachmentStore implements IAttachmentStore {\n constructor(\n private readonly backend: ILocalAttachmentBackend,\n private readonly transport: IAttachmentTransport,\n private readonly now: () => Date = () => new Date(),\n ) {}\n\n async stat(hash: AttachmentHash): Promise<AttachmentHeader> {\n const record = await this.backend.readRecord(hash);\n if (!record) {\n throw new AttachmentNotFound(hash);\n }\n return headerOf(record);\n }\n\n async has(hash: AttachmentHash): Promise<boolean> {\n const record = await this.backend.readRecord(hash);\n return record?.status === \"available\";\n }\n\n async get(\n hash: AttachmentHash,\n signal?: AbortSignal,\n documentId?: string,\n ): Promise<AttachmentResponse> {\n const record = await this.backend.readRecord(hash);\n if (!record || record.status === \"evicted\") {\n return this.fetchRemote(hash, signal, documentId);\n }\n\n const bytes = await this.backend.readBytes(hash);\n if (!bytes) {\n // A record that says `available` over a missing blob is a torn write the\n // backend contract forbids. Treat it as absent rather than serving an\n // empty body: the transport can restore it, and the next put() repairs\n // the row.\n return this.fetchRemote(hash, signal, documentId);\n }\n\n const accessedAt = this.now().toISOString();\n await this.backend.touch(hash, accessedAt);\n\n return {\n header: { ...headerOf(record), lastAccessedAtUtc: accessedAt },\n body: streamFromBytes(bytes),\n };\n }\n\n put(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n return this.store(hash, metadata, data, \"sync\", false);\n }\n\n async evict(hash: AttachmentHash): Promise<void> {\n const record = await this.backend.readRecord(hash);\n if (!record || record.status === \"evicted\") {\n return;\n }\n await this.backend.evict({ ...record, status: \"evicted\" });\n }\n\n storageUsed(): Promise<number> {\n return this.backend.availableBytes();\n }\n\n /** Releases the backend handle. */\n close(): Promise<void> {\n return this.backend.close();\n }\n\n /**\n * Stores bytes this realm produced itself, as `source: \"local\"`.\n *\n * `put()` is the transport's entry point and marks everything `sync`, which\n * is the right provenance for a replicated blob and the wrong one for a blob\n * that originated here. The server-side store gets `source: \"local\"` rows\n * from its upload handles; a local store has no upload handle, so this is\n * how a producer (an editor that just hashed a file, a test that seeds a\n * peer) says the bytes are its own.\n */\n putLocal(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n ): Promise<void> {\n return this.store(hash, metadata, data, \"local\", false);\n }\n\n /**\n * `overwrite` skips the available-hash dedup check.\n *\n * Only {@link fetchRemote} sets it, and only because it is reached when the\n * local copy is unusable -- including the torn case where the record says\n * `available` over a missing blob. Without it, that record would deduplicate\n * away its own repair and the hash could never be restored.\n */\n private async store(\n hash: AttachmentHash,\n metadata: AttachmentMetadata,\n data: ReadableStream<Uint8Array>,\n source: \"local\" | \"sync\",\n overwrite: boolean,\n ): Promise<void> {\n const existing = await this.backend.readRecord(hash);\n if (!overwrite && existing?.status === \"available\") {\n await data.cancel();\n return;\n }\n\n const bytes = await collectStream(data);\n await this.backend.write(\n {\n hash,\n mimeType: metadata.mimeType,\n fileName: metadata.fileName,\n sizeBytes: bytes.byteLength,\n extension: metadata.extension ?? null,\n status: \"available\",\n // A restored record keeps the provenance it was first stored with.\n source: existing?.source ?? source,\n createdAtUtc: existing?.createdAtUtc ?? metadata.createdAtUtc,\n lastAccessedAtUtc: this.now().toISOString(),\n },\n bytes,\n );\n }\n\n /**\n * Restores `hash` from the transport on behalf of `documentId`, persists it\n * through the same write path `put` uses (so provenance follows one rule)\n * and serves the result from the store rather than from the transport's\n * stream, which has already been consumed.\n *\n * The fetched bytes are hashed and refused when they are not what was asked\n * for, mirroring {@link AttachmentReplicator}: a transport peer is another\n * reactor rather than a trusted server, and a content-addressed store that\n * accepts bytes it never verified is no longer content-addressed. A mismatch\n * is surfaced as an error and nothing is written, so a lying peer cannot\n * poison the store.\n */\n private async fetchRemote(\n hash: AttachmentHash,\n signal: AbortSignal | undefined,\n documentId: string | undefined,\n ): Promise<AttachmentResponse> {\n if (documentId === undefined) {\n // Nothing authorizes a remote fetch, so only local bytes could have\n // answered, and there are none.\n throw new AttachmentNotFound(hash);\n }\n\n const remote = await this.transport.fetch(hash, documentId, signal);\n if (remote.kind === \"pending\") {\n throw new AttachmentPending(hash, remote.expiresAtUtc);\n }\n if (remote.kind === \"not-found\") {\n throw new AttachmentNotFound(hash);\n }\n\n const fetchedBytes = await collectStream(remote.response.body);\n const actual = await sha256Hex(fetchedBytes);\n if (actual !== hash) {\n throw new Error(\n `Attachment bytes for ${hash} hashed to ${actual}; the transport served content that is not what was asked for`,\n );\n }\n\n await this.store(\n hash,\n remote.response.metadata,\n streamFromBytes(fetchedBytes),\n \"sync\",\n true,\n );\n\n const record = await this.backend.readRecord(hash);\n const bytes = await this.backend.readBytes(hash);\n if (!record || record.status !== \"available\" || !bytes) {\n // The put did not land. Reporting not-found is the only honest answer,\n // and it does not re-enter the transport: a retry is the caller's (or\n // the replicator's) decision, not a loop inside get().\n throw new AttachmentNotFound(hash);\n }\n return { header: headerOf(record), body: streamFromBytes(bytes) };\n }\n}\n","import type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport type {\n ILocalAttachmentBackend,\n LocalAttachmentRecord,\n} from \"./types.js\";\n\n/**\n * The in-realm memory twin of {@link IdbAttachmentBackend}.\n *\n * It exists so the whole {@link LocalAttachmentStore} contract -- and the\n * replicator above it -- is testable in Node without an IndexedDB shim, and so\n * a reactor that wants a deliberately ephemeral attachment store (a test, a\n * throwaway reactor) has one. The backend contract suite runs against\n * this and against the IndexedDB backend, so the two answer identically.\n *\n * Records are copied in and out: a caller mutating a returned record must not\n * be able to change what the store holds, which is the behaviour a real\n * persistence layer has for free.\n */\nexport class MemoryAttachmentBackend implements ILocalAttachmentBackend {\n private readonly records = new Map<string, LocalAttachmentRecord>();\n private readonly blobs = new Map<string, Uint8Array>();\n\n readRecord(hash: AttachmentHash): Promise<LocalAttachmentRecord | undefined> {\n const record = this.records.get(hash);\n return Promise.resolve(record ? { ...record } : undefined);\n }\n\n write(record: LocalAttachmentRecord, bytes: Uint8Array): Promise<void> {\n this.records.set(record.hash, { ...record });\n this.blobs.set(record.hash, bytes.slice());\n return Promise.resolve();\n }\n\n readBytes(hash: AttachmentHash): Promise<Uint8Array | undefined> {\n const bytes = this.blobs.get(hash);\n return Promise.resolve(bytes ? bytes.slice() : undefined);\n }\n\n evict(record: LocalAttachmentRecord): Promise<void> {\n this.records.set(record.hash, { ...record });\n this.blobs.delete(record.hash);\n return Promise.resolve();\n }\n\n touch(hash: AttachmentHash, lastAccessedAtUtc: string): Promise<void> {\n const record = this.records.get(hash);\n if (record) {\n this.records.set(hash, { ...record, lastAccessedAtUtc });\n }\n return Promise.resolve();\n }\n\n availableBytes(): Promise<number> {\n let total = 0;\n for (const record of this.records.values()) {\n if (record.status === \"available\") {\n total += record.sizeBytes;\n }\n }\n return Promise.resolve(total);\n }\n\n close(): Promise<void> {\n this.records.clear();\n this.blobs.clear();\n return Promise.resolve();\n }\n}\n"],"mappings":"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"}
|