@ai-matrx/data 0.20.0 → 0.22.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.
@@ -0,0 +1,214 @@
1
+ // src/files/tus.ts
2
+ import * as tus from "tus-js-client";
3
+ var TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;
4
+ var TUS_UPLOAD_PATH = "/files/upload/tus";
5
+ function buildUploadMetadataEnvelope(metadata) {
6
+ const envelope = {
7
+ // Origin tag aids backend log triage when something goes wrong.
8
+ origin: "cloudUpload",
9
+ ...metadata ?? {}
10
+ };
11
+ if (process.env.NODE_ENV !== "production" && !hasUploadOrganization(envelope)) {
12
+ console.warn(
13
+ "[buildUploadMetadataEnvelope] upload carries no scope.organization_id \u2014 it will be filed in an organization nobody chose. Route this upload through the host's organization gate before the bytes move."
14
+ );
15
+ }
16
+ return envelope;
17
+ }
18
+ function hasUploadOrganization(envelope) {
19
+ if (typeof envelope.organization_id === "string" && envelope.organization_id) return true;
20
+ const scope = envelope.scope;
21
+ return Boolean(
22
+ scope && typeof scope === "object" && typeof scope.organization_id === "string" && scope.organization_id
23
+ );
24
+ }
25
+ var URL_DB_NAME = "mtx-tus-urls";
26
+ var URL_STORE = "urls";
27
+ var urlDbPromise = null;
28
+ function openUrlDb() {
29
+ if (urlDbPromise) return urlDbPromise;
30
+ urlDbPromise = new Promise((resolve, reject) => {
31
+ if (typeof indexedDB === "undefined") {
32
+ reject(new Error("[tusUpload] IndexedDB unavailable"));
33
+ return;
34
+ }
35
+ const req = indexedDB.open(URL_DB_NAME, 1);
36
+ req.onupgradeneeded = () => {
37
+ const db = req.result;
38
+ if (!db.objectStoreNames.contains(URL_STORE)) {
39
+ const store = db.createObjectStore(URL_STORE, { keyPath: "urlStorageKey" });
40
+ store.createIndex("fingerprint", "fingerprint", { unique: false });
41
+ }
42
+ };
43
+ req.onsuccess = () => resolve(req.result);
44
+ req.onerror = () => reject(req.error ?? new Error("[tusUpload] failed to open mtx-tus-urls"));
45
+ });
46
+ urlDbPromise.catch(() => {
47
+ urlDbPromise = null;
48
+ });
49
+ return urlDbPromise;
50
+ }
51
+ function idbRequest(req) {
52
+ return new Promise((resolve, reject) => {
53
+ req.onsuccess = () => resolve(req.result);
54
+ req.onerror = () => reject(req.error ?? new Error("IndexedDB failed"));
55
+ });
56
+ }
57
+ function createTusUrlStorage() {
58
+ async function write(record, deleteKey) {
59
+ const db = await openUrlDb();
60
+ const tx = db.transaction(URL_STORE, "readwrite");
61
+ if (record) tx.objectStore(URL_STORE).put(record);
62
+ else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);
63
+ await new Promise((resolve, reject) => {
64
+ tx.oncomplete = () => resolve();
65
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB tx failed"));
66
+ });
67
+ }
68
+ return {
69
+ async findAllUploads() {
70
+ const db = await openUrlDb();
71
+ const tx = db.transaction(URL_STORE, "readonly");
72
+ return idbRequest(tx.objectStore(URL_STORE).getAll());
73
+ },
74
+ async findUploadsByFingerprint(fingerprint) {
75
+ const db = await openUrlDb();
76
+ const tx = db.transaction(URL_STORE, "readonly");
77
+ return idbRequest(
78
+ tx.objectStore(URL_STORE).index("fingerprint").getAll(fingerprint)
79
+ );
80
+ },
81
+ async removeUpload(urlStorageKey) {
82
+ await write(null, urlStorageKey);
83
+ },
84
+ async addUpload(fingerprint, upload) {
85
+ const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;
86
+ await write({
87
+ urlStorageKey,
88
+ fingerprint,
89
+ size: upload.size,
90
+ metadata: upload.metadata,
91
+ creationTime: upload.creationTime,
92
+ uploadUrl: upload.uploadUrl,
93
+ parallelUploadUrls: upload.parallelUploadUrls
94
+ });
95
+ return urlStorageKey;
96
+ }
97
+ };
98
+ }
99
+ async function listStoredTusUploads() {
100
+ try {
101
+ const db = await openUrlDb();
102
+ const tx = db.transaction(URL_STORE, "readonly");
103
+ const all = await idbRequest(tx.objectStore(URL_STORE).getAll());
104
+ return all.map((r) => ({
105
+ urlStorageKey: r.urlStorageKey,
106
+ fingerprint: r.fingerprint,
107
+ size: r.size,
108
+ metadata: r.metadata,
109
+ creationTime: r.creationTime,
110
+ uploadUrl: r.uploadUrl
111
+ }));
112
+ } catch (err) {
113
+ console.error("[tusUpload] listStoredTusUploads failed:", err);
114
+ return [];
115
+ }
116
+ }
117
+ var FORWARDED_HEADERS = ["Authorization", "X-Guest-Fingerprint", "X-Organization-Id"];
118
+ function errorText(err) {
119
+ if (err instanceof Error) return err.message;
120
+ return typeof err === "string" ? err : String(err);
121
+ }
122
+ async function tusTransfer(file, opts) {
123
+ const fileName = typeof File !== "undefined" && file instanceof File ? file.name : opts.filePath.split("/").pop() ?? "upload";
124
+ const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);
125
+ let cldFileId = null;
126
+ let aborted = false;
127
+ try {
128
+ await new Promise((resolve, reject) => {
129
+ const upload = new tus.Upload(file, {
130
+ endpoint: opts.endpoint,
131
+ chunkSize: TUS_CHUNK_SIZE_BYTES,
132
+ retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
133
+ removeFingerprintOnSuccess: true,
134
+ urlStorage: opts.urlStorage ?? createTusUrlStorage(),
135
+ ...opts.httpStack ? { httpStack: opts.httpStack } : {},
136
+ ...opts.fileReader ? { fileReader: opts.fileReader } : {},
137
+ metadata: {
138
+ filename: fileName,
139
+ filepath: opts.filePath,
140
+ // tus-js-client base64-encodes the value per the Upload-Metadata
141
+ // spec; the server merges it exactly like the buffered form field.
142
+ metadata_json: JSON.stringify(metadataEnvelope),
143
+ ...opts.visibility ? { visibility: opts.visibility } : {}
144
+ },
145
+ onBeforeRequest: async (req) => {
146
+ const headers = await opts.headers();
147
+ for (const name of FORWARDED_HEADERS) {
148
+ const value = headers[name];
149
+ if (value) req.setHeader(name, value);
150
+ }
151
+ if (req.getMethod() === "POST") req.setHeader("X-Idempotency-Key", opts.idempotencyKey);
152
+ },
153
+ onAfterResponse: (_req, res) => {
154
+ const id = res.getHeader("X-Cld-File-Id");
155
+ if (id) cldFileId = id;
156
+ },
157
+ onProgress: (bytesSent, bytesTotal) => {
158
+ opts.onProgress?.(bytesSent, bytesTotal);
159
+ },
160
+ onSuccess: () => resolve(),
161
+ onError: (error) => reject(error)
162
+ });
163
+ if (opts.signal) {
164
+ if (opts.signal.aborted) {
165
+ aborted = true;
166
+ reject(new Error("Upload cancelled"));
167
+ return;
168
+ }
169
+ opts.signal.addEventListener(
170
+ "abort",
171
+ () => {
172
+ aborted = true;
173
+ void upload.abort();
174
+ reject(new Error("Upload cancelled"));
175
+ },
176
+ { once: true }
177
+ );
178
+ }
179
+ void upload.findPreviousUploads().then((previous) => {
180
+ const first = previous[0];
181
+ if (first) upload.resumeFromPreviousUpload(first);
182
+ upload.start();
183
+ }).catch((err) => {
184
+ console.warn("[tusUpload] previous-upload lookup failed \u2014 starting fresh:", err);
185
+ upload.start();
186
+ });
187
+ });
188
+ } catch (err) {
189
+ if (cldFileId && !aborted) {
190
+ console.warn(
191
+ `[tusUpload] transfer reported an error but the server exposed X-Cld-File-Id=${cldFileId} \u2014 recovering the completed session.`
192
+ );
193
+ } else {
194
+ return { ok: false, error: errorText(err), errorCode: aborted ? "upload_cancelled" : "tus_upload_failed" };
195
+ }
196
+ }
197
+ if (!cldFileId) {
198
+ return {
199
+ ok: false,
200
+ error: "TUS upload finished but the server never exposed X-Cld-File-Id \u2014 the file cannot be resolved. Check server CORS expose_headers.",
201
+ errorCode: "tus_missing_file_id"
202
+ };
203
+ }
204
+ return { ok: true, fileId: cldFileId };
205
+ }
206
+ export {
207
+ TUS_CHUNK_SIZE_BYTES,
208
+ TUS_UPLOAD_PATH,
209
+ buildUploadMetadataEnvelope,
210
+ createTusUrlStorage,
211
+ listStoredTusUploads,
212
+ tusTransfer
213
+ };
214
+ //# sourceMappingURL=tus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/files/tus.ts"],"sourcesContent":["/**\n * @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.\n *\n * Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture\n * step 14, D1: only byte transport moves to data). Media keeps the Redux\n * side and the record hydration; every host — the app, the extension, the\n * desktop — rides this one wire:\n *\n * - creation POST carries `X-Idempotency-Key` (one intended upload, one key);\n * - FRESH headers on EVERY request (long uploads outlive a JWT; the\n * organization header must reach every chunk — aidream 8e5ee0b93 400s an\n * unscoped upload);\n * - one validated `metadata_json` envelope, byte-identical to the buffered\n * form field ({@link buildUploadMetadataEnvelope});\n * - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;\n * - the created file id comes back in `X-Cld-File-Id`, including the\n * completed-session recovery when the final response was lost.\n *\n * Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.\n */\n\nimport * as tus from \"tus-js-client\";\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\n/** The TUS creation route on the files service. */\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes it into the `metadata_json` form field; the TUS path serializes\n * the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒\n * the two transports can never drift.\n *\n * `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The\n * server stamps `files.files.organization_id` from it (membership-checked);\n * omitted, the server falls back to the uploader's own organization — a silent\n * mis-filing (2026-08-30). Loud in development, never a hard failure.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (process.env.NODE_ENV !== \"production\" && !hasUploadOrganization(envelope)) {\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through the host's organization gate before the bytes move.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id) return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, { keyPath: \"urlStorageKey\" });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<ReturnType<tus.Upload[\"findPreviousUploads\"]>>[number];\n\n/** tus-js-client HttpStack port (tests inject a mock wire). */\nexport type TusHttpStack = tus.HttpStack;\n/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */\nexport type TusFileReader = ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n\n/** tus-js-client UrlStorage port. */\nexport type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]>;\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from any recorder chunk journal. */\nexport function createTusUrlStorage(): TusUrlStorage {\n async function write(record: StoredTusUpload | null, deleteKey?: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n if (record) tx.objectStore(URL_STORE).put(record);\n else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n }\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n },\n async findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(\n tx.objectStore(URL_STORE).index(\"fingerprint\").getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n await write(null, urlStorageKey);\n },\n async addUpload(fingerprint: string, upload: PreviousUpload): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n await write({\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry. Read-only — resuming happens inside\n * {@link tusTransfer}; this only lets management surfaces SHOW a pending\n * session. Returns [] where IndexedDB is unavailable (never throws).\n */\nexport async function listStoredTusUploads(): Promise<StoredTusUploadSummary[]> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusTransferOptions {\n /** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */\n endpoint: string;\n /** Destination path including the file name. */\n filePath: string;\n /** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */\n metadata?: Record<string, unknown> | undefined;\n /** Optional Upload-Metadata `visibility` (transitional wire input). */\n visibility?: string | undefined;\n /** One intended upload, one key — sent on the creation POST only. */\n idempotencyKey: string;\n /**\n * FRESH request headers, read before EVERY request. Only `Authorization`,\n * `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.\n */\n headers: () => Promise<Record<string, string | undefined>>;\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Injected HttpStack (tests: mock wire, no XHR). */\n httpStack?: TusHttpStack | undefined;\n /** Injected file reader (tests: Node builds cannot slice browser Files). */\n fileReader?: TusFileReader | undefined;\n /** Override URL storage (tests). Default: {@link createTusUrlStorage}. */\n urlStorage?: TusUrlStorage | undefined;\n}\n\nexport type TusTransferErrorCode = \"upload_cancelled\" | \"tus_upload_failed\" | \"tus_missing_file_id\";\n\nexport type TusTransferResult =\n | { ok: true; fileId: string }\n | { ok: false; error: string; errorCode: TusTransferErrorCode };\n\nconst FORWARDED_HEADERS = [\"Authorization\", \"X-Guest-Fingerprint\", \"X-Organization-Id\"] as const;\n\nfunction errorText(err: unknown): string {\n if (err instanceof Error) return err.message;\n return typeof err === \"string\" ? err : String(err);\n}\n\n/**\n * Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on\n * failure. Resumes a previous session for the same file automatically; when the\n * transfer errors but the server already exposed `X-Cld-File-Id`, the completed\n * session is recovered (loudly) instead of failing or re-uploading.\n */\nexport async function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult> {\n const fileName = typeof File !== \"undefined\" && file instanceof File ? file.name : opts.filePath.split(\"/\").pop() ?? \"upload\";\n const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new tus.Upload(file, {\n endpoint: opts.endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: opts.urlStorage ?? createTusUrlStorage(),\n ...(opts.httpStack ? { httpStack: opts.httpStack } : {}),\n ...(opts.fileReader ? { fileReader: opts.fileReader } : {}),\n metadata: {\n filename: fileName,\n filepath: opts.filePath,\n // tus-js-client base64-encodes the value per the Upload-Metadata\n // spec; the server merges it exactly like the buffered form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(opts.visibility ? { visibility: opts.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n const headers = await opts.headers();\n for (const name of FORWARDED_HEADERS) {\n const value = headers[name];\n if (value) req.setHeader(name, value);\n }\n if (req.getMethod() === \"POST\") req.setHeader(\"X-Idempotency-Key\", opts.idempotencyKey);\n },\n onAfterResponse: (_req, res) => {\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n opts.onProgress?.(bytesSent, bytesTotal);\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n opts.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n void upload\n .findPreviousUploads()\n .then((previous) => {\n const first = previous[0];\n if (first) upload.resumeFromPreviousUpload(first);\n upload.start();\n })\n .catch((err) => {\n console.warn(\"[tusUpload] previous-upload lookup failed — starting fresh:\", err);\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return { ok: false, error: errorText(err), errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\" };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n };\n }\n return { ok: true, fileId: cldFileId };\n}\n"],"mappings":"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","names":[]}
package/dist/files.cjs CHANGED
@@ -29,6 +29,9 @@ __export(files_exports, {
29
29
  SIGNED_URL_RE: () => SIGNED_URL_RE,
30
30
  ShareLinkInvalidError: () => ShareLinkInvalidError,
31
31
  TUS_TRANSPORT_THRESHOLD_BYTES: () => TUS_TRANSPORT_THRESHOLD_BYTES,
32
+ __resetUploadDedupGuardForTests: () => __resetUploadDedupGuardForTests,
33
+ buildFileUrls: () => buildFileUrls,
34
+ claimUpload: () => claimUpload,
32
35
  classifyMediaUrl: () => classifyMediaUrl,
33
36
  classifyUnavailable: () => classifyUnavailable,
34
37
  createFileUrlRecognizer: () => createFileUrlRecognizer,
@@ -37,14 +40,14 @@ __export(files_exports, {
37
40
  createMemoryBlobCache: () => createMemoryBlobCache,
38
41
  fileErrorFromStatus: () => fileErrorFromStatus,
39
42
  fileNameFromUrl: () => fileNameFromUrl,
40
- fileUrls: () => fileUrls,
41
43
  isDurableMediaUrl: () => isDurableMediaUrl,
42
44
  isSignedUrl: () => isSignedUrl,
43
45
  mimeFromUrl: () => mimeFromUrl,
44
46
  resolveUploadTransport: () => resolveUploadTransport,
45
47
  shareUrl: () => shareUrl,
46
48
  shareableMediaUrl: () => shareableMediaUrl,
47
- uploadAccessFields: () => uploadAccessFields
49
+ uploadAccessFields: () => uploadAccessFields,
50
+ uploadDedupKey: () => uploadDedupKey
48
51
  });
49
52
  module.exports = __toCommonJS(files_exports);
50
53
 
@@ -247,7 +250,7 @@ function fileNameFromUrl(url) {
247
250
  if (!last || !/\.[a-z0-9]{1,5}$/i.test(last)) return null;
248
251
  return decodeURIComponent(last);
249
252
  }
250
- function fileUrls(filesBaseUrl, fileId) {
253
+ function buildFileUrls(filesBaseUrl, fileId) {
251
254
  const id = encodeURIComponent(fileId);
252
255
  const base = `${filesBaseUrl.replace(/\/$/, "")}/files/${id}/download`;
253
256
  return { download: base, inline: `${base}?inline=1` };
@@ -749,7 +752,7 @@ function createMatrxFilesClient(config) {
749
752
  const inflightDownloads = /* @__PURE__ */ new Map();
750
753
  async function fetchFileBytes(fileId) {
751
754
  const headers = await authHeaders();
752
- const response = await fetchImpl(fileUrls(filesBase(), fileId).download, {
755
+ const response = await fetchImpl(buildFileUrls(filesBase(), fileId).download, {
753
756
  headers
754
757
  });
755
758
  if (!response.ok) {
@@ -1011,7 +1014,7 @@ function createMatrxFilesClient(config) {
1011
1014
  }
1012
1015
  const transport = kind === "video" || kind === "audio" ? "element" : "blob";
1013
1016
  const resolution = {
1014
- src: mint(fileUrls(filesBase(), fileId).inline),
1017
+ src: mint(buildFileUrls(filesBase(), fileId).inline),
1015
1018
  kind,
1016
1019
  transport,
1017
1020
  recoverable: true
@@ -1077,7 +1080,7 @@ function createMatrxFilesClient(config) {
1077
1080
  mintStatus: session.snapshot().status,
1078
1081
  fetchBytes: async (base, id) => {
1079
1082
  const headers = await authHeaders();
1080
- return fetchImpl(fileUrls(base, id).download, { headers });
1083
+ return fetchImpl(buildFileUrls(base, id).download, { headers });
1081
1084
  },
1082
1085
  cacheBlob: (id, blob) => {
1083
1086
  const already = blobCache.get(id);
@@ -1129,4 +1132,41 @@ function createMatrxFilesClient(config) {
1129
1132
  };
1130
1133
  return client;
1131
1134
  }
1135
+
1136
+ // src/files/dedup.ts
1137
+ var CLAIM_WINDOW_MS = 3 * 60 * 1e3;
1138
+ var claims = /* @__PURE__ */ new Map();
1139
+ function uploadDedupKey(folderPath, fileName, fileSize) {
1140
+ return [folderPath, fileName, String(fileSize)].join("\\0");
1141
+ }
1142
+ function claimUpload(key) {
1143
+ const now = Date.now();
1144
+ const existing = claims.get(key);
1145
+ if (existing && existing.expiresAt > now) {
1146
+ return { shared: true, promise: existing.promise };
1147
+ }
1148
+ let settleFn;
1149
+ const publicPromise = new Promise(
1150
+ (resolve, reject) => {
1151
+ settleFn = (outcome) => {
1152
+ outcome.then(
1153
+ (value) => {
1154
+ resolve(value);
1155
+ },
1156
+ (err) => {
1157
+ claims.delete(key);
1158
+ reject(err instanceof Error ? err : new Error(String(err)));
1159
+ }
1160
+ );
1161
+ };
1162
+ }
1163
+ );
1164
+ publicPromise.catch(() => {
1165
+ });
1166
+ claims.set(key, { promise: publicPromise, expiresAt: now + CLAIM_WINDOW_MS });
1167
+ return { shared: false, settle: settleFn };
1168
+ }
1169
+ function __resetUploadDedupGuardForTests() {
1170
+ claims.clear();
1171
+ }
1132
1172
  //# sourceMappingURL=files.cjs.map