@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 +125 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +177 -3
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +210 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +817 -32
- package/dist/index.js.map +1 -1
- package/dist/{null-attachment-transport-Drx03s02.js → null-attachment-transport-CTBrRCDe.js} +241 -22
- package/dist/null-attachment-transport-CTBrRCDe.js.map +1 -0
- package/dist/{null-attachment-transport-BBhQIk5A.d.ts → null-attachment-transport-D1Bj3hLU.d.ts} +156 -7
- package/dist/null-attachment-transport-D1Bj3hLU.d.ts.map +1 -0
- package/package.json +11 -3
- package/dist/null-attachment-transport-BBhQIk5A.d.ts.map +0 -1
- package/dist/null-attachment-transport-Drx03s02.js.map +0 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
import { A as
|
|
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
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../src/concurrency.ts","../src/client.ts"],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
|
-
import { _ as
|
|
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
|
|
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
|
package/dist/client.js.map
CHANGED
|
@@ -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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"}
|