@calmlens/sdk 0.16.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +425 -181
  2. package/bin/calmlens.js +16016 -0
  3. package/bin/package.json +1 -0
  4. package/cjs/BrowserUpload-BT091eOB.d.ts +11 -0
  5. package/cjs/BrowserUpload-CQwXEGFv.js +10 -0
  6. package/cjs/CalmLensAppClient-CHQGu-Do.js +89 -0
  7. package/cjs/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
  8. package/cjs/CalmLensConnection-B8Bu2j3y.js +659 -0
  9. package/cjs/CalmLensServerClient-C5o2A8MW.js +645 -0
  10. package/cjs/CalmLensServerClient-JtP687a3.d.ts +235 -0
  11. package/cjs/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
  12. package/cjs/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
  13. package/cjs/CryptoUtils-B_x8lMIx-YBW1-bvL.js +42 -0
  14. package/cjs/FileIcons-Ob003MdS.js +175 -0
  15. package/cjs/Jurisdiction-FusEXb4U-zVs21tDv.js +91 -0
  16. package/cjs/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
  17. package/cjs/ModerationContextLimits-DelWfmiJ.js +1311 -0
  18. package/cjs/ReactUpload-CAyCsyG6.d.ts +17 -0
  19. package/cjs/ReactUpload-Cfg5JYUq.js +54 -0
  20. package/cjs/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
  21. package/cjs/TestTriggers-DTc_iMJF-BjMPq4_3.js +1327 -0
  22. package/cjs/UploadController-BGO1eyzb.js +618 -0
  23. package/{esm/index-C9Xw1aid.d.ts → cjs/WebhookPayload-2aL31v0J.d.ts} +131 -155
  24. package/cjs/app.d.ts +7 -44
  25. package/cjs/app.js +11 -89
  26. package/cjs/elements.d.ts +88 -0
  27. package/cjs/elements.js +555 -0
  28. package/cjs/file-icons.d.ts +67 -0
  29. package/cjs/file-icons.js +10 -0
  30. package/cjs/index-CI4BzN4W.d.ts +252 -0
  31. package/cjs/index.d.ts +6 -4
  32. package/cjs/index.js +9 -4
  33. package/cjs/react-native.d.ts +56 -0
  34. package/cjs/react-native.js +495 -0
  35. package/cjs/react.d.ts +30 -0
  36. package/cjs/react.js +370 -0
  37. package/cjs/server.d.ts +12 -191
  38. package/cjs/server.js +18 -378
  39. package/cjs/test-inputs.d.ts +2 -2
  40. package/cjs/test-inputs.js +9 -264
  41. package/cjs/testing.d.ts +285 -190
  42. package/cjs/testing.js +2393 -1203
  43. package/cjs/ui-CC1GOoMg.js +583 -0
  44. package/cjs/ui-wIiMfqMB.d.ts +526 -0
  45. package/cjs/ui.d.ts +2 -0
  46. package/cjs/ui.js +27 -0
  47. package/cjs/vue.d.ts +490 -0
  48. package/cjs/vue.js +419 -0
  49. package/esm/BrowserUpload-BT091eOB.d.ts +11 -0
  50. package/esm/BrowserUpload-D2xBWqhm.js +5 -0
  51. package/esm/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
  52. package/esm/CalmLensAppClient-DuFas_dW.js +84 -0
  53. package/esm/CalmLensConnection-MxwwLXdl.js +582 -0
  54. package/esm/CalmLensServerClient-D80_mKpW.js +586 -0
  55. package/esm/CalmLensServerClient-JtP687a3.d.ts +235 -0
  56. package/esm/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
  57. package/esm/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
  58. package/esm/CryptoUtils-B_x8lMIx-DhuWGzqn.js +25 -0
  59. package/esm/FileIcons-M6dzjFxd.js +128 -0
  60. package/esm/Jurisdiction-FusEXb4U-1ov6m5yE.js +46 -0
  61. package/esm/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
  62. package/esm/ModerationContextLimits-BaX2QnU8.js +1114 -0
  63. package/esm/ReactUpload-CAyCsyG6.d.ts +17 -0
  64. package/esm/ReactUpload-CL2XC8WS.js +37 -0
  65. package/esm/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
  66. package/esm/TestTriggers-DTc_iMJF-HfB533Gx.js +1267 -0
  67. package/esm/UploadController-qjoYGCwx.js +589 -0
  68. package/{cjs/index-C9Xw1aid.d.ts → esm/WebhookPayload-2aL31v0J.d.ts} +131 -155
  69. package/esm/app.d.ts +7 -44
  70. package/esm/app.js +3 -86
  71. package/esm/elements.d.ts +88 -0
  72. package/esm/elements.js +528 -0
  73. package/esm/file-icons.d.ts +67 -0
  74. package/esm/file-icons.js +1 -0
  75. package/esm/index-CI4BzN4W.d.ts +252 -0
  76. package/esm/index.d.ts +6 -4
  77. package/esm/index.js +1 -1
  78. package/esm/react-native.d.ts +56 -0
  79. package/esm/react-native.js +467 -0
  80. package/esm/react.d.ts +30 -0
  81. package/esm/react.js +343 -0
  82. package/esm/server.d.ts +12 -191
  83. package/esm/server.js +3 -369
  84. package/esm/styles.css +147 -0
  85. package/esm/svelte/CalmLensMedia.svelte +134 -0
  86. package/esm/svelte/CalmLensUploader.svelte +175 -0
  87. package/esm/svelte/Icon.svelte +7 -0
  88. package/esm/svelte/index.d.ts +37 -0
  89. package/esm/svelte/index.js +6 -0
  90. package/esm/test-inputs.d.ts +2 -2
  91. package/esm/test-inputs.js +1 -256
  92. package/esm/testing.d.ts +285 -190
  93. package/esm/testing.js +2329 -1122
  94. package/esm/ui-CCY3yXFn.js +458 -0
  95. package/esm/ui-wIiMfqMB.d.ts +526 -0
  96. package/esm/ui.d.ts +2 -0
  97. package/esm/ui.js +3 -0
  98. package/esm/vue.d.ts +490 -0
  99. package/esm/vue.js +392 -0
  100. package/package.json +118 -13
  101. package/cjs/CalmLensConnection-BNUHp_AQ.js +0 -234
  102. package/cjs/CalmLensUtils-o171ownK.js +0 -699
  103. package/cjs/CalmLensVersion-DwhgxQK5.js +0 -480
  104. package/cjs/CalmLensWebhooks-BKrq30Yp.js +0 -201
  105. package/cjs/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
  106. package/cjs/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
  107. package/cjs/WebhookPayload-CJoIkGBD.d.ts +0 -11753
  108. package/esm/CalmLensConnection-XTqmlwzG.js +0 -193
  109. package/esm/CalmLensUtils-DsF8Aa_x.js +0 -574
  110. package/esm/CalmLensVersion-DcifRKsx.js +0 -415
  111. package/esm/CalmLensWebhooks-BvGyk5d3.js +0 -148
  112. package/esm/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
  113. package/esm/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
  114. package/esm/WebhookPayload-CJoIkGBD.d.ts +0 -11753
@@ -0,0 +1,589 @@
1
+ import { O as CalmLensError, k as SDK_ERROR_CODES } from "./ModerationContextLimits-BaX2QnU8.js";
2
+ import { t as CalmLensAppClient } from "./CalmLensAppClient-DuFas_dW.js";
3
+ // src/contract/PlaceholderHashDecode.js
4
+ /**
5
+ * Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
6
+ *
7
+ * @param hash The bytes of the ThumbHash.
8
+ * @returns The width, height, and pixels of the rendered placeholder image.
9
+ */
10
+ function thumbHashToRGBA(hash) {
11
+ const { PI, min, max, cos, round } = Math;
12
+ const header24 = hash[0] | hash[1] << 8 | hash[2] << 16;
13
+ const header16 = hash[3] | hash[4] << 8;
14
+ const l_dc = (header24 & 63) / 63;
15
+ const p_dc = (header24 >> 6 & 63) / 31.5 - 1;
16
+ const q_dc = (header24 >> 12 & 63) / 31.5 - 1;
17
+ const l_scale = (header24 >> 18 & 31) / 31;
18
+ const hasAlpha = header24 >> 23;
19
+ const p_scale = (header16 >> 3 & 63) / 63;
20
+ const q_scale = (header16 >> 9 & 63) / 63;
21
+ const isLandscape = header16 >> 15;
22
+ const lx = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
23
+ const ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
24
+ const a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1;
25
+ const a_scale = (hash[5] >> 4) / 15;
26
+ const ac_start = hasAlpha ? 6 : 5;
27
+ let ac_index = 0;
28
+ const decodeChannel = (nx, ny, scale) => {
29
+ const ac = [];
30
+ for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) ac.push(((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2) & 15) / 7.5 - 1) * scale);
31
+ return ac;
32
+ };
33
+ const l_ac = decodeChannel(lx, ly, l_scale);
34
+ const p_ac = decodeChannel(3, 3, p_scale * 1.25);
35
+ const q_ac = decodeChannel(3, 3, q_scale * 1.25);
36
+ const a_ac = hasAlpha && decodeChannel(5, 5, a_scale);
37
+ const ratio = thumbHashToApproximateAspectRatio(hash);
38
+ const w = round(ratio > 1 ? 32 : 32 * ratio);
39
+ const h = round(ratio > 1 ? 32 / ratio : 32);
40
+ const rgba = new Uint8Array(w * h * 4);
41
+ const fx = [];
42
+ const fy = [];
43
+ for (let y = 0, i = 0; y < h; y++) for (let x = 0; x < w; x++, i += 4) {
44
+ let l = l_dc;
45
+ let p = p_dc;
46
+ let q = q_dc;
47
+ let a = a_dc;
48
+ for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) fx[cx] = cos(PI / w * (x + .5) * cx);
49
+ for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) fy[cy] = cos(PI / h * (y + .5) * cy);
50
+ for (let cy = 0, j = 0; cy < ly; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) l += l_ac[j] * fx[cx] * fy2;
51
+ for (let cy = 0, j = 0; cy < 3; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) {
52
+ const f = fx[cx] * fy2;
53
+ p += p_ac[j] * f;
54
+ q += q_ac[j] * f;
55
+ }
56
+ if (hasAlpha) for (let cy = 0, j = 0; cy < 5; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) a += a_ac[j] * fx[cx] * fy2;
57
+ const b = l - 2 / 3 * p;
58
+ const r = (3 * l - b + q) / 2;
59
+ const g = r - q;
60
+ rgba[i] = max(0, 255 * min(1, r));
61
+ rgba[i + 1] = max(0, 255 * min(1, g));
62
+ rgba[i + 2] = max(0, 255 * min(1, b));
63
+ rgba[i + 3] = max(0, 255 * min(1, a));
64
+ }
65
+ return {
66
+ w,
67
+ h,
68
+ rgba
69
+ };
70
+ }
71
+ /**
72
+ * Extracts the approximate aspect ratio of the original image.
73
+ *
74
+ * @param hash The bytes of the ThumbHash.
75
+ * @returns The approximate aspect ratio (i.e. width / height).
76
+ */
77
+ function thumbHashToApproximateAspectRatio(hash) {
78
+ const header = hash[3];
79
+ const hasAlpha = hash[2] & 128;
80
+ const isLandscape = hash[4] & 128;
81
+ return (isLandscape ? hasAlpha ? 5 : 7 : header & 7) / (isLandscape ? header & 7 : hasAlpha ? 5 : 7);
82
+ }
83
+ /**
84
+ * Encodes an RGBA image to PNG bytes. RGB should not be premultiplied by
85
+ * A. This is optimized for speed and simplicity and does not optimize for size
86
+ * at all. This doesn't do any compression (all values are stored uncompressed).
87
+ *
88
+ * @param w The width of the input image. Must be between 1 and 16383px.
89
+ * @param h The height of the input image. Must be between 1 and 65535px.
90
+ * @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements.
91
+ * @returns The bytes of a PNG for the input image.
92
+ */
93
+ function rgbaToPng(w, h, rgba) {
94
+ if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || w > 16383 || h < 1 || h > 65535 || rgba.length !== w * h * 4) throw new RangeError("Invalid PNG dimensions or pixels");
95
+ const row = w * 4 + 1;
96
+ const idat = 6 + h * (5 + row);
97
+ const bytes = [
98
+ 137,
99
+ 80,
100
+ 78,
101
+ 71,
102
+ 13,
103
+ 10,
104
+ 26,
105
+ 10,
106
+ 0,
107
+ 0,
108
+ 0,
109
+ 13,
110
+ 73,
111
+ 72,
112
+ 68,
113
+ 82,
114
+ 0,
115
+ 0,
116
+ w >> 8,
117
+ w & 255,
118
+ 0,
119
+ 0,
120
+ h >> 8,
121
+ h & 255,
122
+ 8,
123
+ 6,
124
+ 0,
125
+ 0,
126
+ 0,
127
+ 0,
128
+ 0,
129
+ 0,
130
+ 0,
131
+ idat >>> 24,
132
+ idat >> 16 & 255,
133
+ idat >> 8 & 255,
134
+ idat & 255,
135
+ 73,
136
+ 68,
137
+ 65,
138
+ 84,
139
+ 120,
140
+ 1
141
+ ];
142
+ const table = [
143
+ 0,
144
+ 498536548,
145
+ 997073096,
146
+ 651767980,
147
+ 1994146192,
148
+ 1802195444,
149
+ 1303535960,
150
+ 1342533948,
151
+ -306674912,
152
+ -267414716,
153
+ -690576408,
154
+ -882789492,
155
+ -1687895376,
156
+ -2032938284,
157
+ -1609899400,
158
+ -1111625188
159
+ ];
160
+ let a = 1;
161
+ let b = 0;
162
+ for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) {
163
+ bytes.push(y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, row >> 8 ^ 255, 0);
164
+ for (b = (b + a) % 65521; i < end; i++) {
165
+ const u = rgba[i] & 255;
166
+ bytes.push(u);
167
+ a = (a + u) % 65521;
168
+ b = (b + a) % 65521;
169
+ }
170
+ }
171
+ bytes.push(b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130);
172
+ for (let [start, end] of [[12, 29], [37, 41 + idat]]) {
173
+ let c = -1;
174
+ for (let i = start; i < end; i++) {
175
+ c ^= bytes[i];
176
+ c = c >>> 4 ^ table[c & 15];
177
+ c = c >>> 4 ^ table[c & 15];
178
+ }
179
+ c = ~c;
180
+ bytes[end++] = c >>> 24;
181
+ bytes[end++] = c >> 16 & 255;
182
+ bytes[end++] = c >> 8 & 255;
183
+ bytes[end++] = c & 255;
184
+ }
185
+ return Uint8Array.from(bytes);
186
+ }
187
+ /**
188
+ * Decodes a ThumbHash to a PNG data URL. This is a convenience function that
189
+ * just calls "thumbHashToRGBA" followed by "rgbaToDataURL".
190
+ *
191
+ * @param hash The bytes of the ThumbHash.
192
+ * @returns A data URL containing a PNG for the rendered ThumbHash.
193
+ */
194
+ const placeholderHashToBytes = (hash) => Uint8Array.from(atob(hash), (c) => c.charCodeAt(0));
195
+ /** Renders a base64 ThumbHash as a small PNG, which the browser scales to the element's size. */
196
+ function placeholderHashToPng(hash) {
197
+ const image = thumbHashToRGBA(placeholderHashToBytes(hash));
198
+ return rgbaToPng(image.w, image.h, image.rgba);
199
+ }
200
+ function placeholderHashToDataURL(hash) {
201
+ return `data:image/png;base64,${btoa(String.fromCharCode(...placeholderHashToPng(hash)))}`;
202
+ }
203
+ // src/ui/ProgressFetch.ts
204
+ /**
205
+ * A `fetch` that reports how many bytes of the request body have been sent, which `fetch` itself
206
+ * can't do. It sends through `XMLHttpRequest`, so the upload components pass it to
207
+ * `CalmLensAppClient` as `uploadFetch`. Without `XMLHttpRequest` it falls back to `fetch` and
208
+ * reports nothing.
209
+ */
210
+ const createProgressFetch = (onProgress) => {
211
+ if (typeof XMLHttpRequest === "undefined") return (input, init) => fetch(input, init);
212
+ return (input, init = {}) => new Promise((resolve, reject) => {
213
+ const signal = init.signal;
214
+ const abortError = () => signal?.reason ?? new DOMException("The upload was cancelled", "AbortError");
215
+ if (signal?.aborted) {
216
+ reject(abortError());
217
+ return;
218
+ }
219
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
220
+ const xhr = new XMLHttpRequest();
221
+ xhr.open(init.method ?? "GET", url);
222
+ xhr.responseType = "arraybuffer";
223
+ for (const [name, value] of new Headers(init.headers)) xhr.setRequestHeader(name, value);
224
+ xhr.upload.onprogress = (event) => {
225
+ if (event.lengthComputable) onProgress(event.loaded, event.total);
226
+ };
227
+ const onAbort = () => xhr.abort();
228
+ signal?.addEventListener("abort", onAbort, { once: true });
229
+ const done = () => signal?.removeEventListener("abort", onAbort);
230
+ xhr.onload = () => {
231
+ done();
232
+ const headers = new Headers();
233
+ for (const line of xhr.getAllResponseHeaders().trim().split(/[\r\n]+/)) {
234
+ const separator = line.indexOf(":");
235
+ if (separator > 0) headers.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
236
+ }
237
+ const empty = [
238
+ 101,
239
+ 204,
240
+ 205,
241
+ 304
242
+ ].includes(xhr.status);
243
+ resolve(new Response(empty ? null : xhr.response, {
244
+ status: xhr.status,
245
+ statusText: xhr.statusText,
246
+ headers
247
+ }));
248
+ };
249
+ xhr.onerror = () => {
250
+ done();
251
+ reject(/* @__PURE__ */ new TypeError("Network request failed"));
252
+ };
253
+ xhr.ontimeout = xhr.onerror;
254
+ xhr.onabort = () => {
255
+ done();
256
+ reject(abortError());
257
+ };
258
+ xhr.send(init.body ?? null);
259
+ });
260
+ };
261
+ // src/ui/UploadController.ts
262
+ const DEFAULT_MAX_FILES = 10;
263
+ const DEFAULT_CONCURRENCY = 3;
264
+ const DEFAULT_DECISION_TIMEOUT_MS = 15e3;
265
+ /** Whether a file matches an `accept` list such as `image/*,.pdf`. */
266
+ const isFileAccepted = (file, accept) => {
267
+ const patterns = (accept ?? "").split(",").map((pattern) => pattern.trim().toLowerCase()).filter(Boolean);
268
+ if (patterns.length === 0) return true;
269
+ const type = file.type.toLowerCase();
270
+ const name = file.name.toLowerCase();
271
+ return patterns.some((pattern) => {
272
+ if (pattern === "*" || pattern === "*/*") return true;
273
+ if (pattern.endsWith("/*")) return type.startsWith(pattern.slice(0, -1));
274
+ if (pattern.startsWith(".")) return name.endsWith(pattern);
275
+ return type === pattern;
276
+ });
277
+ };
278
+ /** A browser file or blob, which uploads as it is. A file with a `uri` is read for its bytes. */
279
+ const isBlob = (file) => !file.uri && typeof Blob !== "undefined" && file instanceof Blob;
280
+ /** An image preview for a picked file: its own URI, or an object URL for a browser file. */
281
+ const createPreviewUrl = (file) => {
282
+ if (!file.type.startsWith("image/")) return void 0;
283
+ if (file.uri) return file.uri;
284
+ return isBlob(file) && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
285
+ };
286
+ /** Releases an object URL `createPreviewUrl` made; a file's own URI is left alone. */
287
+ const revokePreviewUrl = (url) => {
288
+ if (url?.startsWith("blob:")) URL.revokeObjectURL(url);
289
+ };
290
+ const isRejected = (status) => status === "blocked" || status === "purged";
291
+ const sleep = (ms, signal) => new Promise((resolve) => {
292
+ const timer = setTimeout(resolve, ms);
293
+ signal.addEventListener("abort", () => {
294
+ clearTimeout(timer);
295
+ resolve();
296
+ });
297
+ });
298
+ let nextKey = 0;
299
+ /**
300
+ * Uploads files through an upload preset and tracks each one until it has a moderation
301
+ * decision. It holds no UI: the React, Vue, Svelte and web components render its snapshot, and
302
+ * you can render your own with `subscribe`, which also makes it a Svelte store.
303
+ */
304
+ var UploadController = class {
305
+ #options;
306
+ #snapshot;
307
+ #listeners;
308
+ #aborts;
309
+ /** How to check each upload again, with the client and token it was uploaded with. */
310
+ #checks;
311
+ constructor(options) {
312
+ this.#snapshot = {
313
+ items: [],
314
+ busy: false
315
+ };
316
+ this.#listeners = /* @__PURE__ */ new Set();
317
+ this.#aborts = /* @__PURE__ */ new Map();
318
+ this.#checks = /* @__PURE__ */ new Map();
319
+ this.getSnapshot = () => this.#snapshot;
320
+ this.subscribe = (listener) => {
321
+ this.#listeners.add(listener);
322
+ listener(this.#snapshot);
323
+ return () => {
324
+ this.#listeners.delete(listener);
325
+ };
326
+ };
327
+ this.#options = options;
328
+ }
329
+ /** The current options. */
330
+ get options() {
331
+ return this.#options;
332
+ }
333
+ /** Replaces the options, for example when a component's props change. */
334
+ setOptions(options) {
335
+ this.#options = options;
336
+ }
337
+ /**
338
+ * Adds files from a file input, a drop, a paste or a phone's picker, and starts uploading them,
339
+ * or keeps them `selected` with `autoUpload: false`.
340
+ */
341
+ addFiles(files) {
342
+ const { multiple = false, maxFiles = DEFAULT_MAX_FILES, accept, maxFileSize, autoUpload = true } = this.#options;
343
+ let picked = Array.from(files);
344
+ if (picked.length === 0) return;
345
+ if (!multiple) {
346
+ picked = picked.slice(0, 1);
347
+ for (const item of this.#snapshot.items) this.remove(item.key);
348
+ }
349
+ const room = (multiple ? maxFiles : 1) - this.#snapshot.items.length;
350
+ const added = picked.slice(0, Math.max(0, room)).map((file) => {
351
+ const key = `upload-${++nextKey}`;
352
+ const previewUrl = createPreviewUrl(file);
353
+ const errorCode = !isFileAccepted(file, accept) ? "file/not-accepted" : maxFileSize !== void 0 && file.size > maxFileSize ? "file/too-large" : void 0;
354
+ return errorCode ? {
355
+ key,
356
+ file,
357
+ previewUrl,
358
+ stage: "failed",
359
+ progress: null,
360
+ errorCode
361
+ } : {
362
+ key,
363
+ file,
364
+ previewUrl,
365
+ stage: autoUpload ? "queued" : "selected",
366
+ progress: null
367
+ };
368
+ });
369
+ this.#set([...this.#snapshot.items, ...added]);
370
+ for (const item of added) if (item.stage === "failed") this.#options.onFailed?.(item);
371
+ this.#pump();
372
+ }
373
+ /** Uploads every `selected` file, for an uploader with `autoUpload: false`. */
374
+ start() {
375
+ for (const item of this.#snapshot.items) if (item.stage === "selected") this.#update(item.key, { stage: "queued" });
376
+ this.#pump();
377
+ }
378
+ /**
379
+ * Tries a failed file again: an upload that finished but couldn't be checked is checked again,
380
+ * anything else is uploaded again. A finished upload still `pending` is checked again too.
381
+ */
382
+ retry(key) {
383
+ const item = this.#find(key);
384
+ if (!item || item.errorCode?.startsWith("file/")) return;
385
+ const pending = item.stage === "complete" && item.status === "pending";
386
+ if (item.stage !== "failed" && !(pending && this.#checks.has(key))) return;
387
+ this.#update(key, {
388
+ stage: "queued",
389
+ progress: null,
390
+ errorCode: void 0,
391
+ assetId: this.#checks.has(key) ? item.assetId : void 0
392
+ });
393
+ this.#pump();
394
+ }
395
+ /**
396
+ * Stops a queued or running file and keeps it in the list as failed with `sdk/cancelled`, so
397
+ * `retry` can upload it again. Finished files are left as they are.
398
+ */
399
+ cancel(key) {
400
+ const item = this.#find(key);
401
+ if (!item || item.stage !== "queued" && item.stage !== "uploading" && item.stage !== "checking") return;
402
+ this.#aborts.get(key)?.abort();
403
+ this.#aborts.delete(key);
404
+ this.#checks.delete(key);
405
+ this.#update(key, {
406
+ stage: "failed",
407
+ progress: null,
408
+ errorCode: SDK_ERROR_CODES.CANCELLED
409
+ });
410
+ this.#pump();
411
+ }
412
+ /** Cancels a file's upload if it's running and removes it from the list. */
413
+ remove(key) {
414
+ const item = this.#find(key);
415
+ if (!item) return;
416
+ this.#aborts.get(key)?.abort();
417
+ this.#aborts.delete(key);
418
+ this.#checks.delete(key);
419
+ revokePreviewUrl(item.previewUrl);
420
+ this.#set(this.#snapshot.items.filter((other) => other.key !== key));
421
+ this.#options.onRemoved?.(item);
422
+ this.#pump();
423
+ }
424
+ /** Cancels every upload and empties the list, without calling `onRemoved`. */
425
+ clear() {
426
+ for (const abort of this.#aborts.values()) abort.abort();
427
+ this.#aborts.clear();
428
+ this.#checks.clear();
429
+ for (const item of this.#snapshot.items) revokePreviewUrl(item.previewUrl);
430
+ this.#set([]);
431
+ }
432
+ #find(key) {
433
+ return this.#snapshot.items.find((item) => item.key === key);
434
+ }
435
+ #set(items) {
436
+ const busy = items.some((item) => item.stage === "queued" || item.stage === "uploading" || item.stage === "checking");
437
+ this.#snapshot = {
438
+ items,
439
+ busy
440
+ };
441
+ for (const listener of this.#listeners) listener(this.#snapshot);
442
+ }
443
+ #update(key, changes) {
444
+ if (!this.#find(key)) return;
445
+ this.#set(this.#snapshot.items.map((item) => item.key === key ? {
446
+ ...item,
447
+ ...changes
448
+ } : item));
449
+ }
450
+ #pump() {
451
+ const limit = this.#options.concurrency ?? DEFAULT_CONCURRENCY;
452
+ for (const item of this.#snapshot.items) {
453
+ if (this.#aborts.size >= limit) return;
454
+ if (item.stage === "queued" && !this.#aborts.has(item.key)) this.#upload(item);
455
+ }
456
+ }
457
+ async #upload({ key, file, assetId }) {
458
+ const abort = new AbortController();
459
+ this.#aborts.set(key, abort);
460
+ this.#update(key, {
461
+ stage: "uploading",
462
+ progress: 0
463
+ });
464
+ const options = this.#options;
465
+ try {
466
+ const onProgress = (sent, total) => {
467
+ if (!abort.signal.aborted) this.#update(key, { progress: total ? sent / total : null });
468
+ };
469
+ let check = assetId ? this.#checks.get(key) : void 0;
470
+ let asset;
471
+ if (assetId && check) {
472
+ this.#update(key, {
473
+ stage: "checking",
474
+ progress: null
475
+ });
476
+ asset = await check(assetId);
477
+ } else {
478
+ const client = await this.#clientFor(file, abort.signal, onProgress);
479
+ asset = await client.create();
480
+ check = client.retrieve;
481
+ if (check) this.#checks.set(key, check);
482
+ this.#update(key, {
483
+ stage: "checking",
484
+ progress: null,
485
+ assetId: asset.id
486
+ });
487
+ }
488
+ const deadline = Date.now() + (options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS);
489
+ for (let delay = 1e3; asset.status === "pending" && check && Date.now() < deadline; delay *= 2) {
490
+ await sleep(Math.min(delay, Math.max(0, deadline - Date.now())), abort.signal);
491
+ if (abort.signal.aborted) return;
492
+ asset = await check(asset.id);
493
+ }
494
+ if (abort.signal.aborted) return;
495
+ this.#update(key, {
496
+ stage: "complete",
497
+ status: asset.status
498
+ });
499
+ const item = this.#find(key);
500
+ if (!item) return;
501
+ if (isRejected(asset.status)) options.onRejected?.(item);
502
+ else options.onUploaded?.({
503
+ assetId: asset.id,
504
+ status: asset.status,
505
+ file
506
+ });
507
+ } catch (error) {
508
+ if (abort.signal.aborted) return;
509
+ const errorCode = CalmLensError.isCalmLensError(error) ? error.code : SDK_ERROR_CODES.TRANSPORT_ERROR;
510
+ this.#update(key, {
511
+ stage: "failed",
512
+ progress: null,
513
+ errorCode
514
+ });
515
+ const item = this.#find(key);
516
+ if (item) options.onFailed?.(item);
517
+ } finally {
518
+ if (this.#aborts.get(key) === abort) this.#aborts.delete(key);
519
+ this.#pump();
520
+ }
521
+ }
522
+ /** Creates and checks one file's upload, through `upload` or the upload preset. */
523
+ async #clientFor(file, signal, onProgress) {
524
+ const { upload, checkStatus, uploadPresetId, createClient, baseUrl } = this.#options;
525
+ if (upload) return {
526
+ create: async () => {
527
+ const { assetId, status } = await upload(file, {
528
+ signal,
529
+ onProgress
530
+ });
531
+ return {
532
+ id: assetId,
533
+ status
534
+ };
535
+ },
536
+ retrieve: checkStatus ? async (assetId) => ({
537
+ id: assetId,
538
+ status: await checkStatus(assetId, { signal })
539
+ }) : void 0
540
+ };
541
+ if (!uploadPresetId) throw new CalmLensError(SDK_ERROR_CODES.INVALID_CONFIG, { message: "The uploader needs an uploadPresetId or an upload function" });
542
+ const options = {
543
+ uploadPresetId,
544
+ token: await this.#tokenFor(file, signal),
545
+ baseUrl,
546
+ signal,
547
+ uploadFetch: createProgressFetch(onProgress)
548
+ };
549
+ const client = createClient ? createClient(options) : new CalmLensAppClient(options);
550
+ return {
551
+ create: async () => {
552
+ const type = file.type || "application/octet-stream";
553
+ return client.assets.create({
554
+ name: file.name,
555
+ content: isBlob(file) ? {
556
+ data: file,
557
+ type
558
+ } : {
559
+ uint8Array: new Uint8Array(await file.arrayBuffer()),
560
+ type
561
+ }
562
+ });
563
+ },
564
+ retrieve: (assetId) => client.assets.retrieve({ assetId })
565
+ };
566
+ }
567
+ async #tokenFor(file, signal) {
568
+ const { token, tokenUrl } = this.#options;
569
+ if (typeof token === "function") return await token(file);
570
+ if (token !== void 0) return token;
571
+ if (!tokenUrl) return void 0;
572
+ const response = await fetch(tokenUrl, {
573
+ method: "POST",
574
+ headers: { "Content-Type": "application/json" },
575
+ body: JSON.stringify({
576
+ name: file.name,
577
+ type: file.type,
578
+ size: file.size
579
+ }),
580
+ credentials: "same-origin",
581
+ signal
582
+ });
583
+ const body = await response.json().catch(() => ({}));
584
+ if (!response.ok || typeof body.token !== "string") throw new CalmLensError(typeof body.code === "string" ? body.code : "upload-token/refused", { message: `Your token endpoint answered ${response.status} without a token` });
585
+ return body.token;
586
+ }
587
+ };
588
+ const createUploadController = (options) => new UploadController(options);
589
+ export { placeholderHashToDataURL as a, createProgressFetch as i, createUploadController as n, isFileAccepted as r, UploadController as t };