@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.
package/dist/files.d.cts CHANGED
@@ -381,7 +381,7 @@ interface FileUrls {
381
381
  * `?inline=1` matches the exact spelling the backend emits on
382
382
  * `FileRecord.url`, keeping cache keys identical either way.
383
383
  */
384
- declare function fileUrls(filesBaseUrl: string, fileId: string): FileUrls;
384
+ declare function buildFileUrls(filesBaseUrl: string, fileId: string): FileUrls;
385
385
  /**
386
386
  * The clean public byte-streaming share endpoint (`GET /share/{token}`,
387
387
  * inline disposition) — the canonical embeddable URL for a minted link.
@@ -615,6 +615,66 @@ declare function fileErrorFromStatus(status: number, fileId?: string): FileError
615
615
  */
616
616
  declare function classifyUnavailable(err: unknown): MediaUnavailableReason;
617
617
 
618
+ /**
619
+ * Upload dedup guard — one in-flight/just-landed claim per (folder, name, size).
620
+ * Origin: @ai-matrx/media files/engine/upload/uploadDedupGuard.ts (architecture step 14).
621
+ *
622
+ * Root cause (`common-docs/projects/acquisition-frontier/own-files/VERIFICATION.md`
623
+ * §12, 2026-09-18): in a 7-file pile, both zero-byte files were uploaded
624
+ * TWICE, 18s apart, the second auto-renamed "(1)" by `uniqueName()` in
625
+ * `features/files/redux/thunks.ts` — right after the dev server had refused
626
+ * connections. A client-side retry of the whole `uploadFiles()` dispatch
627
+ * after a transient failure re-sends a file whose first attempt may already
628
+ * have landed server-side (the client only knows its OWN request failed —
629
+ * a connection reset gives no proof the server never received the bytes).
630
+ *
631
+ * The obvious fix is the backend's OWN `X-Idempotency-Key` mechanism
632
+ * (`lib/python-client.ts` already sends it, `aidream` already stores it in
633
+ * `metadata._idempotency_key`) — but it is not the full answer here: the
634
+ * key is only reused *within* a single `uploadFiles()` dispatch
635
+ * (`newRequestId()` mints a fresh one per worker() call in thunks.ts), and
636
+ * server-side `_replayable_412` in `aidream/aidream/api/routers/files/__init__.py`
637
+ * only replays a 412 (precondition-failed) response under the key — a
638
+ * SUCCESSFUL upload is never deduped by the server on replay. So a second,
639
+ * independent `uploadFiles()` dispatch for the same intended file — the
640
+ * shape a caller-level retry takes — is indistinguishable from a genuinely
641
+ * new upload to both client and server.
642
+ *
643
+ * This guard closes the gap client-side, per (folder, name, size): the
644
+ * FIRST claim for a signature performs the real network request; any
645
+ * SECOND claim for the same signature while that request is still in
646
+ * flight, or shortly after it landed, reuses the first attempt's outcome
647
+ * instead of sending the bytes again. A claim that ends in failure is
648
+ * cleared immediately, so a genuine "it never landed, please retry" case is
649
+ * never blocked — only a request whose outcome is still pending, or already
650
+ * known-good, is deduped.
651
+ */
652
+ interface DedupedUploadOutcome {
653
+ fileId: string;
654
+ filePath: string;
655
+ }
656
+ /** The dedup key: same folder, same original name, same byte count. */
657
+ declare function uploadDedupKey(folderPath: string, fileName: string, fileSize: number): string;
658
+ type UploadClaim = {
659
+ shared: true;
660
+ promise: Promise<DedupedUploadOutcome>;
661
+ } | {
662
+ shared: false;
663
+ /**
664
+ * The caller that owns this claim MUST call `settle` exactly once
665
+ * with the real upload's outcome (or a rejected promise on failure).
666
+ */
667
+ settle: (outcome: Promise<DedupedUploadOutcome>) => void;
668
+ };
669
+ /**
670
+ * Claim a signature before starting an upload. See module doc for the
671
+ * contract: a `shared: true` result means someone else already owns this
672
+ * signature — await its `promise` instead of sending the file again.
673
+ */
674
+ declare function claimUpload(key: string): UploadClaim;
675
+ /** Test-only: clears every claim so suites don't leak state between tests. */
676
+ declare function __resetUploadDedupGuardForTests(): void;
677
+
618
678
  /**
619
679
  * @ai-matrx/data/files — the `mx_files_session` cookie bootstrap.
620
680
  *
@@ -683,4 +743,4 @@ interface FilesSession {
683
743
  }
684
744
  declare function createFilesSession(options: FilesSessionOptions): FilesSession;
685
745
 
686
- export { type BlobCacheEntry, type BlobCachePort, type DurableSrc, ExternalFetchError, FileAccessDeniedError, FileDeletedError, FileError, type FileErrorCode, type FileMetadata, type FileMetadataPort, FileNotFoundError, type FileOrigins, type FileRecordWire, type FileSessionResponseWire, type FileShownTo, FileUploadError, type FileUploadResponseWire, type FileUrlRecognizer, type FileUrls, type FilesBlobHandle, type FilesDiagnosticsEvent, type FilesDiagnosticsSink, type FilesRecoveryReport, type FilesRecoveryVerdict, type FilesResolution, type FilesSession, type FilesSessionOptions, type FilesSessionSnapshot, type MatrxFilesClient, type MatrxFilesClientConfig, type MediaKind, type MediaMultiUploadResult, type MediaRef, type MediaRefLike, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type MediaUrlKind, SIGNED_URL_RE, ShareLinkInvalidError, type ShareLinkRecordWire, TUS_TRANSPORT_THRESHOLD_BYTES, type UploadTransport, classifyMediaUrl, classifyUnavailable, createFileUrlRecognizer, createFilesSession, createMatrxFilesClient, createMemoryBlobCache, fileErrorFromStatus, fileNameFromUrl, fileUrls, isDurableMediaUrl, isSignedUrl, mimeFromUrl, resolveUploadTransport, shareUrl, shareableMediaUrl, uploadAccessFields };
746
+ export { type BlobCacheEntry, type BlobCachePort, type DedupedUploadOutcome, type DurableSrc, ExternalFetchError, FileAccessDeniedError, FileDeletedError, FileError, type FileErrorCode, type FileMetadata, type FileMetadataPort, FileNotFoundError, type FileOrigins, type FileRecordWire, type FileSessionResponseWire, type FileShownTo, FileUploadError, type FileUploadResponseWire, type FileUrlRecognizer, type FileUrls, type FilesBlobHandle, type FilesDiagnosticsEvent, type FilesDiagnosticsSink, type FilesRecoveryReport, type FilesRecoveryVerdict, type FilesResolution, type FilesSession, type FilesSessionOptions, type FilesSessionSnapshot, type MatrxFilesClient, type MatrxFilesClientConfig, type MediaKind, type MediaMultiUploadResult, type MediaRef, type MediaRefLike, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type MediaUrlKind, SIGNED_URL_RE, ShareLinkInvalidError, type ShareLinkRecordWire, TUS_TRANSPORT_THRESHOLD_BYTES, type UploadClaim, type UploadTransport, __resetUploadDedupGuardForTests, buildFileUrls, claimUpload, classifyMediaUrl, classifyUnavailable, createFileUrlRecognizer, createFilesSession, createMatrxFilesClient, createMemoryBlobCache, fileErrorFromStatus, fileNameFromUrl, isDurableMediaUrl, isSignedUrl, mimeFromUrl, resolveUploadTransport, shareUrl, shareableMediaUrl, uploadAccessFields, uploadDedupKey };
package/dist/files.d.ts CHANGED
@@ -381,7 +381,7 @@ interface FileUrls {
381
381
  * `?inline=1` matches the exact spelling the backend emits on
382
382
  * `FileRecord.url`, keeping cache keys identical either way.
383
383
  */
384
- declare function fileUrls(filesBaseUrl: string, fileId: string): FileUrls;
384
+ declare function buildFileUrls(filesBaseUrl: string, fileId: string): FileUrls;
385
385
  /**
386
386
  * The clean public byte-streaming share endpoint (`GET /share/{token}`,
387
387
  * inline disposition) — the canonical embeddable URL for a minted link.
@@ -615,6 +615,66 @@ declare function fileErrorFromStatus(status: number, fileId?: string): FileError
615
615
  */
616
616
  declare function classifyUnavailable(err: unknown): MediaUnavailableReason;
617
617
 
618
+ /**
619
+ * Upload dedup guard — one in-flight/just-landed claim per (folder, name, size).
620
+ * Origin: @ai-matrx/media files/engine/upload/uploadDedupGuard.ts (architecture step 14).
621
+ *
622
+ * Root cause (`common-docs/projects/acquisition-frontier/own-files/VERIFICATION.md`
623
+ * §12, 2026-09-18): in a 7-file pile, both zero-byte files were uploaded
624
+ * TWICE, 18s apart, the second auto-renamed "(1)" by `uniqueName()` in
625
+ * `features/files/redux/thunks.ts` — right after the dev server had refused
626
+ * connections. A client-side retry of the whole `uploadFiles()` dispatch
627
+ * after a transient failure re-sends a file whose first attempt may already
628
+ * have landed server-side (the client only knows its OWN request failed —
629
+ * a connection reset gives no proof the server never received the bytes).
630
+ *
631
+ * The obvious fix is the backend's OWN `X-Idempotency-Key` mechanism
632
+ * (`lib/python-client.ts` already sends it, `aidream` already stores it in
633
+ * `metadata._idempotency_key`) — but it is not the full answer here: the
634
+ * key is only reused *within* a single `uploadFiles()` dispatch
635
+ * (`newRequestId()` mints a fresh one per worker() call in thunks.ts), and
636
+ * server-side `_replayable_412` in `aidream/aidream/api/routers/files/__init__.py`
637
+ * only replays a 412 (precondition-failed) response under the key — a
638
+ * SUCCESSFUL upload is never deduped by the server on replay. So a second,
639
+ * independent `uploadFiles()` dispatch for the same intended file — the
640
+ * shape a caller-level retry takes — is indistinguishable from a genuinely
641
+ * new upload to both client and server.
642
+ *
643
+ * This guard closes the gap client-side, per (folder, name, size): the
644
+ * FIRST claim for a signature performs the real network request; any
645
+ * SECOND claim for the same signature while that request is still in
646
+ * flight, or shortly after it landed, reuses the first attempt's outcome
647
+ * instead of sending the bytes again. A claim that ends in failure is
648
+ * cleared immediately, so a genuine "it never landed, please retry" case is
649
+ * never blocked — only a request whose outcome is still pending, or already
650
+ * known-good, is deduped.
651
+ */
652
+ interface DedupedUploadOutcome {
653
+ fileId: string;
654
+ filePath: string;
655
+ }
656
+ /** The dedup key: same folder, same original name, same byte count. */
657
+ declare function uploadDedupKey(folderPath: string, fileName: string, fileSize: number): string;
658
+ type UploadClaim = {
659
+ shared: true;
660
+ promise: Promise<DedupedUploadOutcome>;
661
+ } | {
662
+ shared: false;
663
+ /**
664
+ * The caller that owns this claim MUST call `settle` exactly once
665
+ * with the real upload's outcome (or a rejected promise on failure).
666
+ */
667
+ settle: (outcome: Promise<DedupedUploadOutcome>) => void;
668
+ };
669
+ /**
670
+ * Claim a signature before starting an upload. See module doc for the
671
+ * contract: a `shared: true` result means someone else already owns this
672
+ * signature — await its `promise` instead of sending the file again.
673
+ */
674
+ declare function claimUpload(key: string): UploadClaim;
675
+ /** Test-only: clears every claim so suites don't leak state between tests. */
676
+ declare function __resetUploadDedupGuardForTests(): void;
677
+
618
678
  /**
619
679
  * @ai-matrx/data/files — the `mx_files_session` cookie bootstrap.
620
680
  *
@@ -683,4 +743,4 @@ interface FilesSession {
683
743
  }
684
744
  declare function createFilesSession(options: FilesSessionOptions): FilesSession;
685
745
 
686
- export { type BlobCacheEntry, type BlobCachePort, type DurableSrc, ExternalFetchError, FileAccessDeniedError, FileDeletedError, FileError, type FileErrorCode, type FileMetadata, type FileMetadataPort, FileNotFoundError, type FileOrigins, type FileRecordWire, type FileSessionResponseWire, type FileShownTo, FileUploadError, type FileUploadResponseWire, type FileUrlRecognizer, type FileUrls, type FilesBlobHandle, type FilesDiagnosticsEvent, type FilesDiagnosticsSink, type FilesRecoveryReport, type FilesRecoveryVerdict, type FilesResolution, type FilesSession, type FilesSessionOptions, type FilesSessionSnapshot, type MatrxFilesClient, type MatrxFilesClientConfig, type MediaKind, type MediaMultiUploadResult, type MediaRef, type MediaRefLike, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type MediaUrlKind, SIGNED_URL_RE, ShareLinkInvalidError, type ShareLinkRecordWire, TUS_TRANSPORT_THRESHOLD_BYTES, type UploadTransport, classifyMediaUrl, classifyUnavailable, createFileUrlRecognizer, createFilesSession, createMatrxFilesClient, createMemoryBlobCache, fileErrorFromStatus, fileNameFromUrl, fileUrls, isDurableMediaUrl, isSignedUrl, mimeFromUrl, resolveUploadTransport, shareUrl, shareableMediaUrl, uploadAccessFields };
746
+ export { type BlobCacheEntry, type BlobCachePort, type DedupedUploadOutcome, type DurableSrc, ExternalFetchError, FileAccessDeniedError, FileDeletedError, FileError, type FileErrorCode, type FileMetadata, type FileMetadataPort, FileNotFoundError, type FileOrigins, type FileRecordWire, type FileSessionResponseWire, type FileShownTo, FileUploadError, type FileUploadResponseWire, type FileUrlRecognizer, type FileUrls, type FilesBlobHandle, type FilesDiagnosticsEvent, type FilesDiagnosticsSink, type FilesRecoveryReport, type FilesRecoveryVerdict, type FilesResolution, type FilesSession, type FilesSessionOptions, type FilesSessionSnapshot, type MatrxFilesClient, type MatrxFilesClientConfig, type MediaKind, type MediaMultiUploadResult, type MediaRef, type MediaRefLike, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type MediaUrlKind, SIGNED_URL_RE, ShareLinkInvalidError, type ShareLinkRecordWire, TUS_TRANSPORT_THRESHOLD_BYTES, type UploadClaim, type UploadTransport, __resetUploadDedupGuardForTests, buildFileUrls, claimUpload, classifyMediaUrl, classifyUnavailable, createFileUrlRecognizer, createFilesSession, createMatrxFilesClient, createMemoryBlobCache, fileErrorFromStatus, fileNameFromUrl, isDurableMediaUrl, isSignedUrl, mimeFromUrl, resolveUploadTransport, shareUrl, shareableMediaUrl, uploadAccessFields, uploadDedupKey };
package/dist/files.js CHANGED
@@ -197,7 +197,7 @@ function fileNameFromUrl(url) {
197
197
  if (!last || !/\.[a-z0-9]{1,5}$/i.test(last)) return null;
198
198
  return decodeURIComponent(last);
199
199
  }
200
- function fileUrls(filesBaseUrl, fileId) {
200
+ function buildFileUrls(filesBaseUrl, fileId) {
201
201
  const id = encodeURIComponent(fileId);
202
202
  const base = `${filesBaseUrl.replace(/\/$/, "")}/files/${id}/download`;
203
203
  return { download: base, inline: `${base}?inline=1` };
@@ -699,7 +699,7 @@ function createMatrxFilesClient(config) {
699
699
  const inflightDownloads = /* @__PURE__ */ new Map();
700
700
  async function fetchFileBytes(fileId) {
701
701
  const headers = await authHeaders();
702
- const response = await fetchImpl(fileUrls(filesBase(), fileId).download, {
702
+ const response = await fetchImpl(buildFileUrls(filesBase(), fileId).download, {
703
703
  headers
704
704
  });
705
705
  if (!response.ok) {
@@ -961,7 +961,7 @@ function createMatrxFilesClient(config) {
961
961
  }
962
962
  const transport = kind === "video" || kind === "audio" ? "element" : "blob";
963
963
  const resolution = {
964
- src: mint(fileUrls(filesBase(), fileId).inline),
964
+ src: mint(buildFileUrls(filesBase(), fileId).inline),
965
965
  kind,
966
966
  transport,
967
967
  recoverable: true
@@ -1027,7 +1027,7 @@ function createMatrxFilesClient(config) {
1027
1027
  mintStatus: session.snapshot().status,
1028
1028
  fetchBytes: async (base, id) => {
1029
1029
  const headers = await authHeaders();
1030
- return fetchImpl(fileUrls(base, id).download, { headers });
1030
+ return fetchImpl(buildFileUrls(base, id).download, { headers });
1031
1031
  },
1032
1032
  cacheBlob: (id, blob) => {
1033
1033
  const already = blobCache.get(id);
@@ -1079,6 +1079,43 @@ function createMatrxFilesClient(config) {
1079
1079
  };
1080
1080
  return client;
1081
1081
  }
1082
+
1083
+ // src/files/dedup.ts
1084
+ var CLAIM_WINDOW_MS = 3 * 60 * 1e3;
1085
+ var claims = /* @__PURE__ */ new Map();
1086
+ function uploadDedupKey(folderPath, fileName, fileSize) {
1087
+ return [folderPath, fileName, String(fileSize)].join("\\0");
1088
+ }
1089
+ function claimUpload(key) {
1090
+ const now = Date.now();
1091
+ const existing = claims.get(key);
1092
+ if (existing && existing.expiresAt > now) {
1093
+ return { shared: true, promise: existing.promise };
1094
+ }
1095
+ let settleFn;
1096
+ const publicPromise = new Promise(
1097
+ (resolve, reject) => {
1098
+ settleFn = (outcome) => {
1099
+ outcome.then(
1100
+ (value) => {
1101
+ resolve(value);
1102
+ },
1103
+ (err) => {
1104
+ claims.delete(key);
1105
+ reject(err instanceof Error ? err : new Error(String(err)));
1106
+ }
1107
+ );
1108
+ };
1109
+ }
1110
+ );
1111
+ publicPromise.catch(() => {
1112
+ });
1113
+ claims.set(key, { promise: publicPromise, expiresAt: now + CLAIM_WINDOW_MS });
1114
+ return { shared: false, settle: settleFn };
1115
+ }
1116
+ function __resetUploadDedupGuardForTests() {
1117
+ claims.clear();
1118
+ }
1082
1119
  export {
1083
1120
  ExternalFetchError,
1084
1121
  FileAccessDeniedError,
@@ -1089,6 +1126,9 @@ export {
1089
1126
  SIGNED_URL_RE,
1090
1127
  ShareLinkInvalidError,
1091
1128
  TUS_TRANSPORT_THRESHOLD_BYTES,
1129
+ __resetUploadDedupGuardForTests,
1130
+ buildFileUrls,
1131
+ claimUpload,
1092
1132
  classifyMediaUrl,
1093
1133
  classifyUnavailable,
1094
1134
  createFileUrlRecognizer,
@@ -1097,13 +1137,13 @@ export {
1097
1137
  createMemoryBlobCache,
1098
1138
  fileErrorFromStatus,
1099
1139
  fileNameFromUrl,
1100
- fileUrls,
1101
1140
  isDurableMediaUrl,
1102
1141
  isSignedUrl,
1103
1142
  mimeFromUrl,
1104
1143
  resolveUploadTransport,
1105
1144
  shareUrl,
1106
1145
  shareableMediaUrl,
1107
- uploadAccessFields
1146
+ uploadAccessFields,
1147
+ uploadDedupKey
1108
1148
  };
1109
1149
  //# sourceMappingURL=files.js.map