@powerhousedao/reactor-attachments 6.2.2-dev.4 → 6.2.2-dev.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -1,6 +1,37 @@
1
- import { A as ReserveAttachmentOptions, B as SizeMismatch, C as AttachmentMetadata, D as AttachmentUploadResult, E as AttachmentTransportConfig, F as AttachmentNotFound, I as AttachmentPending, L as HashMismatch, M as TransportResponse, N as UploadFirstReserveAttachmentOptions, O as HashFirstReserveAttachmentOptions, P as AttachmentAlreadyExists, R as InvalidAttachmentRef, S as AttachmentHeader, T as AttachmentStatus, V as UploadTooLarge, _ as IAttachmentTransport, a as RemoteAttachmentUpload, b as IAttachmentUploadFactory, c as SwitchboardAttachmentTransport, d as createRef, f as parseRef, g as IAttachmentStore, h as IAttachmentService, i as RemoteAttachmentUploadFactory, j as TransportFetchResult, k as Reservation, l as SwitchboardTransportConfig, m as IAttachmentReader, n as createRemoteAttachmentService, o as RemoteReservationStore, p as AttachmentService, r as RemoteAttachmentStore, s as SwitchboardClientConfig, t as NullAttachmentTransport, u as ParsedRef, v as IAttachmentTransportFactory, w as AttachmentResponse, x as IReservationStore, y as IAttachmentUpload, z as ReservationNotFound } from "./null-attachment-transport-BBhQIk5A.js";
1
+ import { $ as UploadTooLarge, A as AttachmentHeader, B as ReserveAttachmentOptions, C as IAttachmentUploadFactory, D as AttachmentDownloadOptions, F as AttachmentTransportConfig, G as AttachmentNotFound, H as TransportResponse, I as AttachmentUploadResult, J as AttachmentTransferStage, K as AttachmentPending, L as AttachmentUploadTarget, M as AttachmentResponse, N as AttachmentStatus, O as AttachmentDownloadTarget, P as AttachmentTargetHeaders, Q as SizeMismatch, R as HashFirstReserveAttachmentOptions, S as IAttachmentUpload, U as UploadFirstReserveAttachmentOptions, V as TransportFetchResult, W as AttachmentAlreadyExists, X as InvalidAttachmentRef, Y as HashMismatch, Z as ReservationNotFound, _ as IAttachmentReader, a as RemoteAttachmentUpload, b as IAttachmentTransport, c as SwitchboardAttachmentTransport, d as createRef, f as parseRef, h as parseAttachmentUploadTarget, i as RemoteAttachmentUploadFactory, j as AttachmentMetadata, k as AttachmentDownloadTargetOptions, l as SwitchboardTransportConfig, m as parseAttachmentDownloadTarget, n as createRemoteAttachmentService, o as RemoteReservationStore, p as AttachmentService, q as AttachmentTransferError, r as RemoteAttachmentStore, s as SwitchboardClientConfig, t as NullAttachmentTransport, u as ParsedRef, v as IAttachmentService, w as IReservationStore, x as IAttachmentTransportFactory, y as IAttachmentStore, z as Reservation } from "./null-attachment-transport-D1Bj3hLU.js";
2
2
  import { AttachmentHash, AttachmentRef } from "@powerhousedao/reactor";
3
3
 
4
+ //#region src/concurrency.d.ts
5
+ /**
6
+ * Ordered result of one batch item. `index` always mirrors the input
7
+ * position, so callers can correlate results with inputs regardless of
8
+ * completion order, and successes are retained when siblings fail.
9
+ */
10
+ type BatchItemResult<R> = {
11
+ index: number;
12
+ status: "fulfilled";
13
+ value: R;
14
+ } | {
15
+ index: number;
16
+ status: "rejected";
17
+ error: unknown;
18
+ };
19
+ type RunWithConcurrencyOptions = {
20
+ /** Maximum simultaneously running workers. Must be a positive integer. */concurrency: number;
21
+ /**
22
+ * Whole-batch cancellation: unstarted items are rejected with the signal's
23
+ * reason without ever starting, while already-started items keep running —
24
+ * per-item signals are the mechanism for interrupting active work.
25
+ */
26
+ signal?: AbortSignal;
27
+ };
28
+ /**
29
+ * Runs `worker` over `items` with a hard upper bound on simultaneous
30
+ * executions. Bounding starts (not just transfers) is what keeps memory flat:
31
+ * an item's preprocessing (hashing/buffering) only begins when a slot frees.
32
+ */
33
+ declare function runWithConcurrency<T, R>(items: readonly T[], worker: (item: T, index: number) => Promise<R>, options: RunWithConcurrencyOptions): Promise<BatchItemResult<R>[]>;
34
+ //#endregion
4
35
  //#region src/client.d.ts
5
36
  type PreprocessResult = {
6
37
  ref: AttachmentRef;
@@ -10,14 +41,106 @@ type PreprocessResult = {
10
41
  data: ReadableStream<Uint8Array>;
11
42
  stream: () => ReadableStream<Uint8Array>;
12
43
  };
44
+ type AttachmentStage = "hashing" | "reserving" | "uploading" | "requesting-download-target" | "downloading" | "done" | "error";
45
+ type AttachmentStageListener = (stage: AttachmentStage) => void;
46
+ type AttachmentUploadInput = {
47
+ file: Blob;
48
+ fileName?: string;
49
+ mimeType?: string; /** Per-item cancellation, checked between stages. */
50
+ signal?: AbortSignal;
51
+ };
52
+ /**
53
+ * Every remote download names the document that authorizes its ref; batches
54
+ * may freely mix documents because the anchor travels with each item.
55
+ */
56
+ type AttachmentDownloadInput = {
57
+ documentId: string;
58
+ ref: AttachmentRef;
59
+ signal?: AbortSignal;
60
+ };
61
+ /**
62
+ * The document keeps its own name and type for an attachment (the same
63
+ * bytes may appear under different names in different documents), so the
64
+ * blob-producing conveniences let callers override what the server header
65
+ * reports from upload time.
66
+ */
67
+ type AttachmentBlobOptions = {
68
+ mimeType?: string;
69
+ };
70
+ type AttachmentSaveOptions = {
71
+ fileName?: string;
72
+ mimeType?: string;
73
+ };
74
+ type AttachmentBlobResult = {
75
+ blob: Blob;
76
+ header: AttachmentHeader;
77
+ };
78
+ type AttachmentObjectUrl = {
79
+ /** Ready for img/iframe/video src. Pins memory until revoke() is called. */url: string;
80
+ header: AttachmentHeader;
81
+ revoke: () => void;
82
+ };
83
+ type AttachmentShareLinkInput = {
84
+ documentId: string;
85
+ ref: AttachmentRef; /** Requested link lifetime in seconds; the server clamps to its maximum. */
86
+ expiresIn?: number;
87
+ signal?: AbortSignal;
88
+ };
89
+ /**
90
+ * A self-contained public URL: anyone holding it can fetch the bytes until
91
+ * expiresAtUtc, with no login and no document access. Minting one requires
92
+ * document read access; once minted it cannot be revoked before expiry.
93
+ */
94
+ type AttachmentShareLink = {
95
+ url: string;
96
+ expiresAtUtc: string;
97
+ };
98
+ type AttachmentBatchOptions = {
99
+ /** Bounds preprocessing and transfer together. Defaults to 4. */concurrency?: number; /** Whole-batch cancellation: stops unstarted items. */
100
+ signal?: AbortSignal;
101
+ onStage?: (index: number, stage: AttachmentStage) => void;
102
+ };
103
+ declare const DEFAULT_ATTACHMENT_BATCH_CONCURRENCY = 4;
13
104
  interface IAttachmentClient {
14
105
  preprocess(file: Blob, opts?: {
15
106
  fileName?: string;
16
107
  mimeType?: string;
17
108
  }): Promise<PreprocessResult>;
18
109
  reserve(options: HashFirstReserveAttachmentOptions, send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>): Promise<AttachmentUploadResult>;
110
+ /** Hash, reserve, and transfer one file; confirmed dedup skips the transfer. */
111
+ upload(input: AttachmentUploadInput, onStage?: AttachmentStageListener): Promise<AttachmentUploadResult>;
112
+ /** Document-authorized download of one ref. */
113
+ download(input: AttachmentDownloadInput, onStage?: AttachmentStageListener): Promise<AttachmentResponse>;
114
+ /**
115
+ * Document-authorized download materialized as a typed Blob. The Blob's
116
+ * type comes from the server header unless overridden — that's what makes
117
+ * browsers render PDFs inline and images correctly.
118
+ */
119
+ downloadBlob(input: AttachmentDownloadInput, options?: AttachmentBlobOptions, onStage?: AttachmentStageListener): Promise<AttachmentBlobResult>;
120
+ /**
121
+ * Download and hand the bytes to the browser's save-file flow. Browser
122
+ * only. fileName defaults to the server header's; pass the document's own
123
+ * name for per-document naming.
124
+ */
125
+ saveAttachment(input: AttachmentDownloadInput, options?: AttachmentSaveOptions, onStage?: AttachmentStageListener): Promise<void>;
126
+ /**
127
+ * Download and expose the bytes as an object URL for inline rendering
128
+ * (img/iframe/video src). Callers MUST call revoke() when done — the URL
129
+ * pins the blob in memory until then.
130
+ */
131
+ downloadObjectUrl(input: AttachmentDownloadInput, options?: AttachmentBlobOptions, onStage?: AttachmentStageListener): Promise<AttachmentObjectUrl>;
132
+ /**
133
+ * Mint a public share link: a presigned URL anyone can fetch until it
134
+ * expires, with no login. Authorized exactly like a download (document
135
+ * read access + the reference index). Requires a presigned-capable
136
+ * storage backend (S3); rejects when the server answers with an
137
+ * authenticated switchboard target, which would not be public.
138
+ */
139
+ getShareLink(input: AttachmentShareLinkInput): Promise<AttachmentShareLink>;
140
+ uploadMany(inputs: readonly AttachmentUploadInput[], options?: AttachmentBatchOptions): Promise<BatchItemResult<AttachmentUploadResult>[]>;
141
+ downloadMany(inputs: readonly AttachmentDownloadInput[], options?: AttachmentBatchOptions): Promise<BatchItemResult<AttachmentResponse>[]>;
19
142
  }
20
143
  declare function createAttachmentClient(service: IAttachmentService): IAttachmentClient;
21
144
  //#endregion
22
- export { AttachmentAlreadyExists, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, AttachmentPending, type AttachmentResponse, AttachmentService, type AttachmentStatus, type AttachmentTransportConfig, type AttachmentUploadResult, type HashFirstReserveAttachmentOptions, HashMismatch, IAttachmentClient, type IAttachmentReader, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, NullAttachmentTransport, type ParsedRef, PreprocessResult, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, SizeMismatch, SwitchboardAttachmentTransport, type SwitchboardClientConfig, type SwitchboardTransportConfig, type TransportFetchResult, type TransportResponse, type UploadFirstReserveAttachmentOptions, UploadTooLarge, createAttachmentClient, createRef, createRemoteAttachmentService, parseRef };
145
+ export { AttachmentAlreadyExists, AttachmentBatchOptions, AttachmentBlobOptions, AttachmentBlobResult, AttachmentDownloadInput, type AttachmentDownloadOptions, type AttachmentDownloadTarget, type AttachmentDownloadTargetOptions, type AttachmentHeader, type AttachmentMetadata, AttachmentNotFound, AttachmentObjectUrl, AttachmentPending, type AttachmentResponse, AttachmentSaveOptions, AttachmentService, AttachmentShareLink, AttachmentShareLinkInput, AttachmentStage, AttachmentStageListener, type AttachmentStatus, type AttachmentTargetHeaders, AttachmentTransferError, type AttachmentTransferStage, type AttachmentTransportConfig, AttachmentUploadInput, type AttachmentUploadResult, type AttachmentUploadTarget, type BatchItemResult, DEFAULT_ATTACHMENT_BATCH_CONCURRENCY, type HashFirstReserveAttachmentOptions, HashMismatch, IAttachmentClient, type IAttachmentReader, type IAttachmentService, type IAttachmentStore, type IAttachmentTransport, type IAttachmentTransportFactory, type IAttachmentUpload, type IAttachmentUploadFactory, type IReservationStore, InvalidAttachmentRef, NullAttachmentTransport, type ParsedRef, PreprocessResult, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, type Reservation, ReservationNotFound, type ReserveAttachmentOptions, type RunWithConcurrencyOptions, SizeMismatch, SwitchboardAttachmentTransport, type SwitchboardClientConfig, type SwitchboardTransportConfig, type TransportFetchResult, type TransportResponse, type UploadFirstReserveAttachmentOptions, UploadTooLarge, createAttachmentClient, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget, parseAttachmentUploadTarget, parseRef, runWithConcurrency };
23
146
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;KA0DY,gBAAA;EACV,GAAA,EAAK,aAAA;EACL,IAAA,EAAM,cAAA;EACN,SAAA;EACA,OAAA,EAAS,iCAAA;EACT,IAAA,EAAM,cAAA,CAAe,UAAA;EACrB,MAAA,QAAc,cAAA,CAAe,UAAA;AAAA;AAAA,UAGd,iBAAA;EACf,UAAA,CACE,IAAA,EAAM,IAAA,EACN,IAAA;IAAS,QAAA;IAAmB,QAAA;EAAA,IAC3B,OAAA,CAAQ,gBAAA;EACX,OAAA,CACE,OAAA,EAAS,iCAAA,EACT,IAAA,GAAO,MAAA,EAAQ,iBAAA,KAAsB,OAAA,CAAQ,sBAAA,IAC5C,OAAA,CAAQ,sBAAA;AAAA;AAAA,iBA2DG,sBAAA,CACd,OAAA,EAAS,kBAAA,GACR,iBAAA"}
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/concurrency.ts","../src/client.ts"],"mappings":";;;;;;;;AAKA;KAAY,eAAA;EACN,KAAA;EAAe,MAAA;EAAqB,KAAA,EAAO,CAAA;AAAA;EAC3C,KAAA;EAAe,MAAA;EAAoB,KAAA;AAAA;AAAA,KAE7B,yBAAA;EAF6B,0EAIvC,WAAA;EAJ4C;AAE9C;;;;EAQE,MAAA,GAAS,WAAA;AAAA;;;;AAQX;;iBAAsB,kBAAA,MAAA,CACpB,KAAA,WAAgB,CAAA,IAChB,MAAA,GAAS,IAAA,EAAM,CAAA,EAAG,KAAA,aAAkB,OAAA,CAAQ,CAAA,GAC5C,OAAA,EAAS,yBAAA,GACR,OAAA,CAAQ,eAAA,CAAgB,CAAA;;;KCgDf,gBAAA;EACV,GAAA,EAAK,aAAA;EACL,IAAA,EAAM,cAAA;EACN,SAAA;EACA,OAAA,EAAS,iCAAA;EACT,IAAA,EAAM,cAAA,CAAe,UAAA;EACrB,MAAA,QAAc,cAAA,CAAe,UAAA;AAAA;AAAA,KAGnB,eAAA;AAAA,KASA,uBAAA,IAA2B,KAAA,EAAO,eAAA;AAAA,KAElC,qBAAA;EACV,IAAA,EAAM,IAAA;EACN,QAAA;EACA,QAAA,WD3EsC;EC6EtC,MAAA,GAAS,WAAA;AAAA;;;;;KAOC,uBAAA;EACV,UAAA;EACA,GAAA,EAAK,aAAA;EACL,MAAA,GAAS,WAAA;AAAA;;;;;;;KASC,qBAAA;EACV,QAAA;AAAA;AAAA,KAGU,qBAAA;EACV,QAAA;EACA,QAAA;AAAA;AAAA,KAGU,oBAAA;EACV,IAAA,EAAM,IAAA;EACN,MAAA,EAAQ,gBAAA;AAAA;AAAA,KAGE,mBAAA;8EAEV,GAAA;EACA,MAAA,EAAQ,gBAAA;EACR,MAAA;AAAA;AAAA,KAGU,wBAAA;EACV,UAAA;EACA,GAAA,EAAK,aAAA,EA9DC;EAgEN,SAAA;EACA,MAAA,GAAS,WAAA;AAAA;;;;;;KAQC,mBAAA;EACV,GAAA;EACA,YAAA;AAAA;AAAA,KAGU,sBAAA;EA9EW,iEAgFrB,WAAA,WA/Ec;EAiFd,MAAA,GAAS,WAAA;EACT,OAAA,IAAW,KAAA,UAAe,KAAA,EAAO,eAAA;AAAA;AAAA,cAGtB,oCAAA;AAAA,UAEI,iBAAA;EACf,UAAA,CACE,IAAA,EAAM,IAAA,EACN,IAAA;IAAS,QAAA;IAAmB,QAAA;EAAA,IAC3B,OAAA,CAAQ,gBAAA;EACX,OAAA,CACE,OAAA,EAAS,iCAAA,EACT,IAAA,GAAO,MAAA,EAAQ,iBAAA,KAAsB,OAAA,CAAQ,sBAAA,IAC5C,OAAA,CAAQ,sBAAA;;EAEX,MAAA,CACE,KAAA,EAAO,qBAAA,EACP,OAAA,GAAU,uBAAA,GACT,OAAA,CAAQ,sBAAA;EAxFgD;EA0F3D,QAAA,CACE,KAAA,EAAO,uBAAA,EACP,OAAA,GAAU,uBAAA,GACT,OAAA,CAAQ,kBAAA;EA3FoB;;;;;EAiG/B,YAAA,CACE,KAAA,EAAO,uBAAA,EACP,OAAA,GAAU,qBAAA,EACV,OAAA,GAAU,uBAAA,GACT,OAAA,CAAQ,oBAAA;EAlGX;;;;;EAwGA,cAAA,CACE,KAAA,EAAO,uBAAA,EACP,OAAA,GAAU,qBAAA,EACV,OAAA,GAAU,uBAAA,GACT,OAAA;EAnG8B;;;;;EAyGjC,iBAAA,CACE,KAAA,EAAO,uBAAA,EACP,OAAA,GAAU,qBAAA,EACV,OAAA,GAAU,uBAAA,GACT,OAAA,CAAQ,mBAAA;EA1GX;;;;AASF;;;EAyGE,YAAA,CAAa,KAAA,EAAO,wBAAA,GAA2B,OAAA,CAAQ,mBAAA;EACvD,UAAA,CACE,MAAA,WAAiB,qBAAA,IACjB,OAAA,GAAU,sBAAA,GACT,OAAA,CAAQ,eAAA,CAAgB,sBAAA;EAC3B,YAAA,CACE,MAAA,WAAiB,uBAAA,IACjB,OAAA,GAAU,sBAAA,GACT,OAAA,CAAQ,eAAA,CAAgB,kBAAA;AAAA;AAAA,iBAyPb,sBAAA,CACd,OAAA,EAAS,kBAAA,GACR,iBAAA"}
package/dist/client.js CHANGED
@@ -1,5 +1,62 @@
1
- import { _ as SizeMismatch, a as RemoteAttachmentUpload, c as AttachmentService, d as AttachmentAlreadyExists, f as AttachmentNotFound, g as ReservationNotFound, h as InvalidAttachmentRef, i as RemoteAttachmentUploadFactory, l as createRef, m as HashMismatch, n as createRemoteAttachmentService, o as RemoteReservationStore, p as AttachmentPending, r as RemoteAttachmentStore, s as SwitchboardAttachmentTransport, t as NullAttachmentTransport, u as parseRef, v as UploadTooLarge } from "./null-attachment-transport-Drx03s02.js";
1
+ import { _ as HashMismatch, a as RemoteAttachmentUpload, b as SizeMismatch, c as AttachmentService, d as parseAttachmentDownloadTarget, f as parseAttachmentUploadTarget, g as AttachmentTransferError, h as AttachmentPending, i as RemoteAttachmentUploadFactory, l as createRef, m as AttachmentNotFound, n as createRemoteAttachmentService, o as RemoteReservationStore, p as AttachmentAlreadyExists, r as RemoteAttachmentStore, s as SwitchboardAttachmentTransport, t as NullAttachmentTransport, u as parseRef, v as InvalidAttachmentRef, x as UploadTooLarge, y as ReservationNotFound } from "./null-attachment-transport-CTBrRCDe.js";
2
+ //#region src/concurrency.ts
3
+ /**
4
+ * Runs `worker` over `items` with a hard upper bound on simultaneous
5
+ * executions. Bounding starts (not just transfers) is what keeps memory flat:
6
+ * an item's preprocessing (hashing/buffering) only begins when a slot frees.
7
+ */
8
+ async function runWithConcurrency(items, worker, options) {
9
+ const { concurrency, signal } = options;
10
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`concurrency must be a positive integer, got: ${concurrency}`);
11
+ const results = new Array(items.length);
12
+ let nextIndex = 0;
13
+ async function runLane() {
14
+ for (;;) {
15
+ const index = nextIndex++;
16
+ if (index >= items.length) return;
17
+ if (signal?.aborted) {
18
+ results[index] = {
19
+ index,
20
+ status: "rejected",
21
+ error: signalReason(signal)
22
+ };
23
+ continue;
24
+ }
25
+ try {
26
+ results[index] = {
27
+ index,
28
+ status: "fulfilled",
29
+ value: await worker(items[index], index)
30
+ };
31
+ } catch (error) {
32
+ results[index] = {
33
+ index,
34
+ status: "rejected",
35
+ error
36
+ };
37
+ }
38
+ }
39
+ }
40
+ const lanes = Array.from({ length: Math.min(concurrency, items.length) }, () => runLane());
41
+ await Promise.all(lanes);
42
+ return results;
43
+ }
44
+ function signalReason(signal) {
45
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
46
+ }
47
+ //#endregion
2
48
  //#region src/client.ts
49
+ const DEFAULT_ATTACHMENT_BATCH_CONCURRENCY = 4;
50
+ /**
51
+ * Duck-typed dedup detection: bundlers (notably Vite dev pre-bundling) can
52
+ * load two copies of this package's error classes, one for the service and
53
+ * one for the client wrapper, making a plain instanceof check miss the
54
+ * cross-copy throw. Name plus payload shape identifies the error reliably.
55
+ */
56
+ function isAttachmentAlreadyExists(err) {
57
+ if (err instanceof AttachmentAlreadyExists) return true;
58
+ return err instanceof Error && err.name === "AttachmentAlreadyExists" && typeof err.hash === "string" && typeof err.ref === "string";
59
+ }
3
60
  function streamFromBuffer(buf) {
4
61
  return new ReadableStream({ start(controller) {
5
62
  controller.enqueue(buf);
@@ -39,7 +96,7 @@ var AttachmentClientImpl = class {
39
96
  try {
40
97
  handle = await this.service.reserve(options);
41
98
  } catch (err) {
42
- if (err instanceof AttachmentAlreadyExists) {
99
+ if (isAttachmentAlreadyExists(err)) {
43
100
  const header = await this.service.stat(err.ref);
44
101
  return {
45
102
  hash: err.hash,
@@ -51,11 +108,128 @@ var AttachmentClientImpl = class {
51
108
  }
52
109
  return send(handle);
53
110
  }
111
+ async upload(input, onStage) {
112
+ try {
113
+ input.signal?.throwIfAborted();
114
+ onStage?.("hashing");
115
+ const preprocessed = await this.preprocess(input.file, {
116
+ ...input.fileName !== void 0 ? { fileName: input.fileName } : {},
117
+ ...input.mimeType !== void 0 ? { mimeType: input.mimeType } : {}
118
+ });
119
+ input.signal?.throwIfAborted();
120
+ onStage?.("reserving");
121
+ const result = await this.reserve(preprocessed.options, (handle) => {
122
+ input.signal?.throwIfAborted();
123
+ onStage?.("uploading");
124
+ return handle.send(preprocessed.stream());
125
+ });
126
+ onStage?.("done");
127
+ return result;
128
+ } catch (err) {
129
+ onStage?.("error");
130
+ throw err;
131
+ }
132
+ }
133
+ async download(input, onStage) {
134
+ try {
135
+ input.signal?.throwIfAborted();
136
+ onStage?.("requesting-download-target");
137
+ const response = await this.service.get(input.ref, {
138
+ documentId: input.documentId,
139
+ signal: input.signal
140
+ });
141
+ onStage?.("downloading");
142
+ onStage?.("done");
143
+ return response;
144
+ } catch (err) {
145
+ onStage?.("error");
146
+ throw err;
147
+ }
148
+ }
149
+ async downloadBlob(input, options, onStage) {
150
+ try {
151
+ input.signal?.throwIfAborted();
152
+ onStage?.("requesting-download-target");
153
+ const { header, body } = await this.service.get(input.ref, {
154
+ documentId: input.documentId,
155
+ signal: input.signal
156
+ });
157
+ onStage?.("downloading");
158
+ const reader = body.getReader();
159
+ const chunks = [];
160
+ for (;;) {
161
+ const { done, value } = await reader.read();
162
+ if (done) break;
163
+ chunks.push(value);
164
+ }
165
+ const blob = new Blob(chunks, { type: options?.mimeType ?? header.mimeType });
166
+ onStage?.("done");
167
+ return {
168
+ blob,
169
+ header
170
+ };
171
+ } catch (err) {
172
+ onStage?.("error");
173
+ throw err;
174
+ }
175
+ }
176
+ async saveAttachment(input, options, onStage) {
177
+ if (typeof document === "undefined") throw new Error("saveAttachment requires a browser environment; use downloadBlob elsewhere");
178
+ const { blob, header } = await this.downloadBlob(input, { mimeType: options?.mimeType }, onStage);
179
+ const url = URL.createObjectURL(blob);
180
+ try {
181
+ const anchor = document.createElement("a");
182
+ anchor.href = url;
183
+ anchor.download = options?.fileName ?? header.fileName;
184
+ anchor.click();
185
+ } finally {
186
+ URL.revokeObjectURL(url);
187
+ }
188
+ }
189
+ async downloadObjectUrl(input, options, onStage) {
190
+ const { blob, header } = await this.downloadBlob(input, options, onStage);
191
+ const url = URL.createObjectURL(blob);
192
+ let revoked = false;
193
+ return {
194
+ url,
195
+ header,
196
+ revoke: () => {
197
+ if (revoked) return;
198
+ revoked = true;
199
+ URL.revokeObjectURL(url);
200
+ }
201
+ };
202
+ }
203
+ async getShareLink(input) {
204
+ input.signal?.throwIfAborted();
205
+ const target = await this.service.getDownloadTarget(input.ref, {
206
+ documentId: input.documentId,
207
+ ...input.expiresIn !== void 0 ? { expiresIn: input.expiresIn } : {},
208
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
209
+ });
210
+ if (target.kind !== "presigned-get") throw new Error("Public share links require a presigned-capable storage backend (S3); this server answered with an authenticated target");
211
+ return {
212
+ url: target.url,
213
+ expiresAtUtc: target.expiresAtUtc
214
+ };
215
+ }
216
+ uploadMany(inputs, options) {
217
+ return runWithConcurrency(inputs, (input, index) => this.upload(input, (stage) => options?.onStage?.(index, stage)), {
218
+ concurrency: options?.concurrency ?? 4,
219
+ signal: options?.signal
220
+ });
221
+ }
222
+ downloadMany(inputs, options) {
223
+ return runWithConcurrency(inputs, (input, index) => this.download(input, (stage) => options?.onStage?.(index, stage)), {
224
+ concurrency: options?.concurrency ?? 4,
225
+ signal: options?.signal
226
+ });
227
+ }
54
228
  };
55
229
  function createAttachmentClient(service) {
56
230
  return new AttachmentClientImpl(service);
57
231
  }
58
232
  //#endregion
59
- export { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentService, HashMismatch, InvalidAttachmentRef, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, SizeMismatch, SwitchboardAttachmentTransport, UploadTooLarge, createAttachmentClient, createRef, createRemoteAttachmentService, parseRef };
233
+ export { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentService, AttachmentTransferError, DEFAULT_ATTACHMENT_BATCH_CONCURRENCY, HashMismatch, InvalidAttachmentRef, NullAttachmentTransport, RemoteAttachmentStore, RemoteAttachmentUpload, RemoteAttachmentUploadFactory, RemoteReservationStore, ReservationNotFound, SizeMismatch, SwitchboardAttachmentTransport, UploadTooLarge, createAttachmentClient, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget, parseAttachmentUploadTarget, parseRef, runWithConcurrency };
60
234
 
61
235
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { AttachmentAlreadyExists } from \"./errors.js\";\nimport type { IAttachmentService, IAttachmentUpload } from \"./interfaces.js\";\nimport { createRef } from \"./ref.js\";\nimport type {\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n} from \"./types.js\";\n\nexport { AttachmentService } from \"./attachment-service.js\";\nexport {\n AttachmentAlreadyExists,\n AttachmentNotFound,\n AttachmentPending,\n HashMismatch,\n InvalidAttachmentRef,\n ReservationNotFound,\n SizeMismatch,\n UploadTooLarge,\n} from \"./errors.js\";\nexport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentStore,\n IAttachmentTransport,\n IAttachmentTransportFactory,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nexport { parseRef, createRef } from \"./ref.js\";\nexport type { ParsedRef } from \"./ref.js\";\nexport type {\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n AttachmentTransportConfig,\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n UploadFirstReserveAttachmentOptions,\n Reservation,\n ReserveAttachmentOptions,\n TransportFetchResult,\n TransportResponse,\n} from \"./types.js\";\nexport {\n SwitchboardAttachmentTransport,\n type SwitchboardTransportConfig,\n RemoteReservationStore,\n type SwitchboardClientConfig,\n RemoteAttachmentUpload,\n RemoteAttachmentUploadFactory,\n RemoteAttachmentStore,\n createRemoteAttachmentService,\n} from \"./switchboard/index.js\";\nexport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\n\nexport type PreprocessResult = {\n ref: AttachmentRef;\n hash: AttachmentHash;\n sizeBytes: number;\n options: HashFirstReserveAttachmentOptions;\n data: ReadableStream<Uint8Array>;\n stream: () => ReadableStream<Uint8Array>;\n};\n\nexport interface IAttachmentClient {\n preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult>;\n reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult>;\n}\n\nfunction streamFromBuffer(buf: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buf);\n controller.close();\n },\n });\n}\n\nclass AttachmentClientImpl implements IAttachmentClient {\n constructor(private readonly service: IAttachmentService) {}\n\n async preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult> {\n const buf = await file.arrayBuffer();\n const bytes = new Uint8Array(buf);\n const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", bytes);\n const hash = Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\") as AttachmentHash;\n const ref = createRef(hash);\n const sizeBytes = file.size;\n const mimeType = opts?.mimeType ?? file.type;\n const fileName =\n opts?.fileName ?? (file instanceof File ? file.name : \"attachment\");\n const options: HashFirstReserveAttachmentOptions = {\n mimeType,\n fileName,\n clientHash: hash,\n sizeBytes,\n };\n const data = streamFromBuffer(bytes);\n const stream = (): ReadableStream<Uint8Array> => streamFromBuffer(bytes);\n return { ref, hash, sizeBytes, options, data, stream };\n }\n\n async reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult> {\n let handle: IAttachmentUpload;\n try {\n handle = await this.service.reserve(options);\n } catch (err) {\n if (err instanceof AttachmentAlreadyExists) {\n const header = await this.service.stat(err.ref);\n return { hash: err.hash, ref: err.ref, header };\n }\n throw err;\n }\n return send(handle);\n }\n}\n\nexport function createAttachmentClient(\n service: IAttachmentService,\n): IAttachmentClient {\n return new AttachmentClientImpl(service);\n}\n"],"mappings":";;AA8EA,SAAS,iBAAiB,KAA6C;AACrE,QAAO,IAAI,eAAe,EACxB,MAAM,YAAY;AAChB,aAAW,QAAQ,IAAI;AACvB,aAAW,OAAO;IAErB,CAAC;;AAGJ,IAAM,uBAAN,MAAwD;CACtD,YAAY,SAA8C;AAA7B,OAAA,UAAA;;CAE7B,MAAM,WACJ,MACA,MAC2B;EAC3B,MAAM,MAAM,MAAM,KAAK,aAAa;EACpC,MAAM,QAAQ,IAAI,WAAW,IAAI;EACjC,MAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,MAAM;EACtE,MAAM,OAAO,MAAM,KAAK,IAAI,WAAW,OAAO,CAAC,CAC5C,KAAK,MAAM,EAAE,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAC3C,KAAK,GAAG;EACX,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,YAAY,KAAK;EAIvB,MAAM,UAA6C;GACjD,UAJe,MAAM,YAAY,KAAK;GAKtC,UAHA,MAAM,aAAa,gBAAgB,OAAO,KAAK,OAAO;GAItD,YAAY;GACZ;GACD;EACD,MAAM,OAAO,iBAAiB,MAAM;EACpC,MAAM,eAA2C,iBAAiB,MAAM;AACxE,SAAO;GAAE;GAAK;GAAM;GAAW;GAAS;GAAM;GAAQ;;CAGxD,MAAM,QACJ,SACA,MACiC;EACjC,IAAI;AACJ,MAAI;AACF,YAAS,MAAM,KAAK,QAAQ,QAAQ,QAAQ;WACrC,KAAK;AACZ,OAAI,eAAe,yBAAyB;IAC1C,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,IAAI,IAAI;AAC/C,WAAO;KAAE,MAAM,IAAI;KAAM,KAAK,IAAI;KAAK;KAAQ;;AAEjD,SAAM;;AAER,SAAO,KAAK,OAAO;;;AAIvB,SAAgB,uBACd,SACmB;AACnB,QAAO,IAAI,qBAAqB,QAAQ"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/concurrency.ts","../src/client.ts"],"sourcesContent":["/**\n * Ordered result of one batch item. `index` always mirrors the input\n * position, so callers can correlate results with inputs regardless of\n * completion order, and successes are retained when siblings fail.\n */\nexport type BatchItemResult<R> =\n | { index: number; status: \"fulfilled\"; value: R }\n | { index: number; status: \"rejected\"; error: unknown };\n\nexport type RunWithConcurrencyOptions = {\n /** Maximum simultaneously running workers. Must be a positive integer. */\n concurrency: number;\n /**\n * Whole-batch cancellation: unstarted items are rejected with the signal's\n * reason without ever starting, while already-started items keep running —\n * per-item signals are the mechanism for interrupting active work.\n */\n signal?: AbortSignal;\n};\n\n/**\n * Runs `worker` over `items` with a hard upper bound on simultaneous\n * executions. Bounding starts (not just transfers) is what keeps memory flat:\n * an item's preprocessing (hashing/buffering) only begins when a slot frees.\n */\nexport async function runWithConcurrency<T, R>(\n items: readonly T[],\n worker: (item: T, index: number) => Promise<R>,\n options: RunWithConcurrencyOptions,\n): Promise<BatchItemResult<R>[]> {\n const { concurrency, signal } = options;\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new Error(\n `concurrency must be a positive integer, got: ${concurrency}`,\n );\n }\n\n const results: BatchItemResult<R>[] = new Array<BatchItemResult<R>>(\n items.length,\n );\n let nextIndex = 0;\n\n async function runLane(): Promise<void> {\n for (;;) {\n const index = nextIndex++;\n if (index >= items.length) return;\n if (signal?.aborted) {\n results[index] = {\n index,\n status: \"rejected\",\n error: signalReason(signal),\n };\n continue;\n }\n try {\n const value = await worker(items[index], index);\n results[index] = { index, status: \"fulfilled\", value };\n } catch (error) {\n results[index] = { index, status: \"rejected\", error };\n }\n }\n }\n\n const lanes = Array.from(\n { length: Math.min(concurrency, items.length) },\n () => runLane(),\n );\n await Promise.all(lanes);\n return results;\n}\n\nfunction signalReason(signal: AbortSignal): unknown {\n return (\n (signal as { reason?: unknown }).reason ??\n new DOMException(\"The operation was aborted\", \"AbortError\")\n );\n}\n","import type { AttachmentHash, AttachmentRef } from \"@powerhousedao/reactor\";\nimport { runWithConcurrency, type BatchItemResult } from \"./concurrency.js\";\nimport { AttachmentAlreadyExists } from \"./errors.js\";\nexport type { AttachmentTransferStage } from \"./errors.js\";\nexport {\n runWithConcurrency,\n type BatchItemResult,\n type RunWithConcurrencyOptions,\n} from \"./concurrency.js\";\nimport type { IAttachmentService, IAttachmentUpload } from \"./interfaces.js\";\nimport { createRef } from \"./ref.js\";\nimport type {\n AttachmentHeader,\n AttachmentResponse,\n AttachmentUploadResult,\n HashFirstReserveAttachmentOptions,\n} from \"./types.js\";\n\nexport { AttachmentService } from \"./attachment-service.js\";\nexport {\n AttachmentAlreadyExists,\n AttachmentNotFound,\n AttachmentPending,\n AttachmentTransferError,\n HashMismatch,\n InvalidAttachmentRef,\n ReservationNotFound,\n SizeMismatch,\n UploadTooLarge,\n} from \"./errors.js\";\nexport type {\n IAttachmentReader,\n IAttachmentService,\n IAttachmentStore,\n IAttachmentTransport,\n IAttachmentTransportFactory,\n IAttachmentUpload,\n IAttachmentUploadFactory,\n IReservationStore,\n} from \"./interfaces.js\";\nexport { parseRef, createRef } from \"./ref.js\";\nexport type { ParsedRef } from \"./ref.js\";\nexport type {\n AttachmentDownloadOptions,\n AttachmentDownloadTarget,\n AttachmentDownloadTargetOptions,\n AttachmentHeader,\n AttachmentMetadata,\n AttachmentResponse,\n AttachmentStatus,\n AttachmentTransportConfig,\n AttachmentUploadResult,\n AttachmentTargetHeaders,\n AttachmentUploadTarget,\n HashFirstReserveAttachmentOptions,\n UploadFirstReserveAttachmentOptions,\n Reservation,\n ReserveAttachmentOptions,\n TransportFetchResult,\n TransportResponse,\n} from \"./types.js\";\nexport {\n parseAttachmentDownloadTarget,\n parseAttachmentUploadTarget,\n} from \"./targets.js\";\nexport {\n SwitchboardAttachmentTransport,\n type SwitchboardTransportConfig,\n RemoteReservationStore,\n type SwitchboardClientConfig,\n RemoteAttachmentUpload,\n RemoteAttachmentUploadFactory,\n RemoteAttachmentStore,\n createRemoteAttachmentService,\n} from \"./switchboard/index.js\";\nexport { NullAttachmentTransport } from \"./null-attachment-transport.js\";\n\nexport type PreprocessResult = {\n ref: AttachmentRef;\n hash: AttachmentHash;\n sizeBytes: number;\n options: HashFirstReserveAttachmentOptions;\n data: ReadableStream<Uint8Array>;\n stream: () => ReadableStream<Uint8Array>;\n};\n\nexport type AttachmentStage =\n | \"hashing\"\n | \"reserving\"\n | \"uploading\"\n | \"requesting-download-target\"\n | \"downloading\"\n | \"done\"\n | \"error\";\n\nexport type AttachmentStageListener = (stage: AttachmentStage) => void;\n\nexport type AttachmentUploadInput = {\n file: Blob;\n fileName?: string;\n mimeType?: string;\n /** Per-item cancellation, checked between stages. */\n signal?: AbortSignal;\n};\n\n/**\n * Every remote download names the document that authorizes its ref; batches\n * may freely mix documents because the anchor travels with each item.\n */\nexport type AttachmentDownloadInput = {\n documentId: string;\n ref: AttachmentRef;\n signal?: AbortSignal;\n};\n\n/**\n * The document keeps its own name and type for an attachment (the same\n * bytes may appear under different names in different documents), so the\n * blob-producing conveniences let callers override what the server header\n * reports from upload time.\n */\nexport type AttachmentBlobOptions = {\n mimeType?: string;\n};\n\nexport type AttachmentSaveOptions = {\n fileName?: string;\n mimeType?: string;\n};\n\nexport type AttachmentBlobResult = {\n blob: Blob;\n header: AttachmentHeader;\n};\n\nexport type AttachmentObjectUrl = {\n /** Ready for img/iframe/video src. Pins memory until revoke() is called. */\n url: string;\n header: AttachmentHeader;\n revoke: () => void;\n};\n\nexport type AttachmentShareLinkInput = {\n documentId: string;\n ref: AttachmentRef;\n /** Requested link lifetime in seconds; the server clamps to its maximum. */\n expiresIn?: number;\n signal?: AbortSignal;\n};\n\n/**\n * A self-contained public URL: anyone holding it can fetch the bytes until\n * expiresAtUtc, with no login and no document access. Minting one requires\n * document read access; once minted it cannot be revoked before expiry.\n */\nexport type AttachmentShareLink = {\n url: string;\n expiresAtUtc: string;\n};\n\nexport type AttachmentBatchOptions = {\n /** Bounds preprocessing and transfer together. Defaults to 4. */\n concurrency?: number;\n /** Whole-batch cancellation: stops unstarted items. */\n signal?: AbortSignal;\n onStage?: (index: number, stage: AttachmentStage) => void;\n};\n\nexport const DEFAULT_ATTACHMENT_BATCH_CONCURRENCY = 4;\n\nexport interface IAttachmentClient {\n preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult>;\n reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult>;\n /** Hash, reserve, and transfer one file; confirmed dedup skips the transfer. */\n upload(\n input: AttachmentUploadInput,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentUploadResult>;\n /** Document-authorized download of one ref. */\n download(\n input: AttachmentDownloadInput,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentResponse>;\n /**\n * Document-authorized download materialized as a typed Blob. The Blob's\n * type comes from the server header unless overridden — that's what makes\n * browsers render PDFs inline and images correctly.\n */\n downloadBlob(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentBlobResult>;\n /**\n * Download and hand the bytes to the browser's save-file flow. Browser\n * only. fileName defaults to the server header's; pass the document's own\n * name for per-document naming.\n */\n saveAttachment(\n input: AttachmentDownloadInput,\n options?: AttachmentSaveOptions,\n onStage?: AttachmentStageListener,\n ): Promise<void>;\n /**\n * Download and expose the bytes as an object URL for inline rendering\n * (img/iframe/video src). Callers MUST call revoke() when done — the URL\n * pins the blob in memory until then.\n */\n downloadObjectUrl(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentObjectUrl>;\n /**\n * Mint a public share link: a presigned URL anyone can fetch until it\n * expires, with no login. Authorized exactly like a download (document\n * read access + the reference index). Requires a presigned-capable\n * storage backend (S3); rejects when the server answers with an\n * authenticated switchboard target, which would not be public.\n */\n getShareLink(input: AttachmentShareLinkInput): Promise<AttachmentShareLink>;\n uploadMany(\n inputs: readonly AttachmentUploadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentUploadResult>[]>;\n downloadMany(\n inputs: readonly AttachmentDownloadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentResponse>[]>;\n}\n\n/**\n * Duck-typed dedup detection: bundlers (notably Vite dev pre-bundling) can\n * load two copies of this package's error classes, one for the service and\n * one for the client wrapper, making a plain instanceof check miss the\n * cross-copy throw. Name plus payload shape identifies the error reliably.\n */\nfunction isAttachmentAlreadyExists(\n err: unknown,\n): err is AttachmentAlreadyExists {\n if (err instanceof AttachmentAlreadyExists) return true;\n return (\n err instanceof Error &&\n err.name === \"AttachmentAlreadyExists\" &&\n typeof (err as { hash?: unknown }).hash === \"string\" &&\n typeof (err as { ref?: unknown }).ref === \"string\"\n );\n}\n\nfunction streamFromBuffer(buf: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buf);\n controller.close();\n },\n });\n}\n\nclass AttachmentClientImpl implements IAttachmentClient {\n constructor(private readonly service: IAttachmentService) {}\n\n async preprocess(\n file: Blob,\n opts?: { fileName?: string; mimeType?: string },\n ): Promise<PreprocessResult> {\n const buf = await file.arrayBuffer();\n const bytes = new Uint8Array(buf);\n const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", bytes);\n const hash = Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\") as AttachmentHash;\n const ref = createRef(hash);\n const sizeBytes = file.size;\n const mimeType = opts?.mimeType ?? file.type;\n const fileName =\n opts?.fileName ?? (file instanceof File ? file.name : \"attachment\");\n const options: HashFirstReserveAttachmentOptions = {\n mimeType,\n fileName,\n clientHash: hash,\n sizeBytes,\n };\n const data = streamFromBuffer(bytes);\n const stream = (): ReadableStream<Uint8Array> => streamFromBuffer(bytes);\n return { ref, hash, sizeBytes, options, data, stream };\n }\n\n async reserve(\n options: HashFirstReserveAttachmentOptions,\n send: (handle: IAttachmentUpload) => Promise<AttachmentUploadResult>,\n ): Promise<AttachmentUploadResult> {\n let handle: IAttachmentUpload;\n try {\n handle = await this.service.reserve(options);\n } catch (err) {\n if (isAttachmentAlreadyExists(err)) {\n const header = await this.service.stat(err.ref);\n return { hash: err.hash, ref: err.ref, header };\n }\n throw err;\n }\n return send(handle);\n }\n\n async upload(\n input: AttachmentUploadInput,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentUploadResult> {\n try {\n input.signal?.throwIfAborted();\n onStage?.(\"hashing\");\n const preprocessed = await this.preprocess(input.file, {\n ...(input.fileName !== undefined ? { fileName: input.fileName } : {}),\n ...(input.mimeType !== undefined ? { mimeType: input.mimeType } : {}),\n });\n\n input.signal?.throwIfAborted();\n onStage?.(\"reserving\");\n const result = await this.reserve(preprocessed.options, (handle) => {\n input.signal?.throwIfAborted();\n onStage?.(\"uploading\");\n return handle.send(preprocessed.stream());\n });\n onStage?.(\"done\");\n return result;\n } catch (err) {\n onStage?.(\"error\");\n throw err;\n }\n }\n\n async download(\n input: AttachmentDownloadInput,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentResponse> {\n try {\n input.signal?.throwIfAborted();\n onStage?.(\"requesting-download-target\");\n const response = await this.service.get(input.ref, {\n documentId: input.documentId,\n signal: input.signal,\n });\n onStage?.(\"downloading\");\n onStage?.(\"done\");\n return response;\n } catch (err) {\n onStage?.(\"error\");\n throw err;\n }\n }\n\n async downloadBlob(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentBlobResult> {\n try {\n input.signal?.throwIfAborted();\n onStage?.(\"requesting-download-target\");\n const { header, body } = await this.service.get(input.ref, {\n documentId: input.documentId,\n signal: input.signal,\n });\n onStage?.(\"downloading\");\n const reader = body.getReader();\n const chunks: BlobPart[] = [];\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value as BlobPart);\n }\n const blob = new Blob(chunks, {\n type: options?.mimeType ?? header.mimeType,\n });\n onStage?.(\"done\");\n return { blob, header };\n } catch (err) {\n onStage?.(\"error\");\n throw err;\n }\n }\n\n async saveAttachment(\n input: AttachmentDownloadInput,\n options?: AttachmentSaveOptions,\n onStage?: AttachmentStageListener,\n ): Promise<void> {\n if (typeof document === \"undefined\") {\n throw new Error(\n \"saveAttachment requires a browser environment; use downloadBlob elsewhere\",\n );\n }\n const { blob, header } = await this.downloadBlob(\n input,\n { mimeType: options?.mimeType },\n onStage,\n );\n const url = URL.createObjectURL(blob);\n try {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = options?.fileName ?? header.fileName;\n anchor.click();\n } finally {\n URL.revokeObjectURL(url);\n }\n }\n\n async downloadObjectUrl(\n input: AttachmentDownloadInput,\n options?: AttachmentBlobOptions,\n onStage?: AttachmentStageListener,\n ): Promise<AttachmentObjectUrl> {\n const { blob, header } = await this.downloadBlob(input, options, onStage);\n const url = URL.createObjectURL(blob);\n let revoked = false;\n return {\n url,\n header,\n revoke: () => {\n if (revoked) return;\n revoked = true;\n URL.revokeObjectURL(url);\n },\n };\n }\n\n async getShareLink(\n input: AttachmentShareLinkInput,\n ): Promise<AttachmentShareLink> {\n input.signal?.throwIfAborted();\n const target = await this.service.getDownloadTarget(input.ref, {\n documentId: input.documentId,\n ...(input.expiresIn !== undefined ? { expiresIn: input.expiresIn } : {}),\n ...(input.signal !== undefined ? { signal: input.signal } : {}),\n });\n if (target.kind !== \"presigned-get\") {\n throw new Error(\n \"Public share links require a presigned-capable storage backend (S3); this server answered with an authenticated target\",\n );\n }\n return { url: target.url, expiresAtUtc: target.expiresAtUtc };\n }\n\n uploadMany(\n inputs: readonly AttachmentUploadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentUploadResult>[]> {\n return runWithConcurrency(\n inputs,\n (input, index) =>\n this.upload(input, (stage) => options?.onStage?.(index, stage)),\n {\n concurrency:\n options?.concurrency ?? DEFAULT_ATTACHMENT_BATCH_CONCURRENCY,\n signal: options?.signal,\n },\n );\n }\n\n downloadMany(\n inputs: readonly AttachmentDownloadInput[],\n options?: AttachmentBatchOptions,\n ): Promise<BatchItemResult<AttachmentResponse>[]> {\n return runWithConcurrency(\n inputs,\n (input, index) =>\n this.download(input, (stage) => options?.onStage?.(index, stage)),\n {\n concurrency:\n options?.concurrency ?? DEFAULT_ATTACHMENT_BATCH_CONCURRENCY,\n signal: options?.signal,\n },\n );\n }\n}\n\nexport function createAttachmentClient(\n service: IAttachmentService,\n): IAttachmentClient {\n return new AttachmentClientImpl(service);\n}\n"],"mappings":"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"}