@tearleads/client-sdk 0.1.33 → 0.1.35
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/client/containerContents.js +1 -1
- package/client/containerContents.js.map +1 -1
- package/client/containerContentsTypes.d.ts +2 -0
- package/client/containerContentsTypes.js.map +1 -1
- package/client/documents.js +1 -1
- package/client/documents.js.map +1 -1
- package/data/blobs/transferPool.js +53 -0
- package/data/blobs/transferPool.js.map +1 -0
- package/data/documents/blob/shared/types.d.ts +2 -2
- package/data/documents/blob/shared/types.js.map +1 -1
- package/data/sync/coordinatorLaneRun.js +154 -0
- package/data/sync/coordinatorLaneRun.js.map +1 -0
- package/data/sync/coordinatorPump.js +72 -169
- package/data/sync/coordinatorPump.js.map +1 -1
- package/data/sync/coordinatorState.d.ts +6 -0
- package/data/sync/coordinatorState.js +3 -1
- package/data/sync/coordinatorState.js.map +1 -1
- package/data/sync/syncCoordinator.js +4 -3
- package/data/sync/syncCoordinator.js.map +1 -1
- package/data/sync/syncLaneConfig.d.ts +2 -0
- package/data/sync/syncLaneConfig.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.js.map +1 -1
- package/internal/api-client/ApiClient.d.ts +2 -2
- package/internal/api-client/ApiClient.js +4 -4
- package/internal/api-client/ApiClient.js.map +1 -1
- package/internal/api-client/routes/blobs/get.d.ts +2 -2
- package/internal/api-client/routes/blobs/get.js +24 -21
- package/internal/api-client/routes/blobs/get.js.map +1 -1
- package/internal/api-client/routes/blobs/multipartUpload.js +16 -9
- package/internal/api-client/routes/blobs/multipartUpload.js.map +1 -1
- package/internal/api-client/routes/blobs/storageFetchOptions.js +11 -0
- package/internal/api-client/routes/blobs/storageFetchOptions.js.map +1 -0
- package/package.json +1 -1
- package/stores/documents/documentStore/attachmentHydration.js +33 -0
- package/stores/documents/documentStore/attachmentHydration.js.map +1 -1
- package/stores/documents/documentStore/attachmentRemoval.js +136 -0
- package/stores/documents/documentStore/attachmentRemoval.js.map +1 -0
- package/stores/documents/documentStore/attachments.js +31 -135
- package/stores/documents/documentStore/attachments.js.map +1 -1
- package/stores/documents/documentStore/deferredInitialUpdate.js +108 -0
- package/stores/documents/documentStore/deferredInitialUpdate.js.map +1 -0
- package/stores/documents/documentStore/initialization.js +10 -2
- package/stores/documents/documentStore/initialization.js.map +1 -1
- package/stores/documents/documentStore/persistence.js +7 -0
- package/stores/documents/documentStore/persistence.js.map +1 -1
- package/stores/documents/documentStore/remoteWork.js +1 -1
- package/stores/documents/documentStore/remoteWork.js.map +1 -1
- package/stores/documents/documentStore/state.js +1 -0
- package/stores/documents/documentStore/state.js.map +1 -1
- package/stores/documents/documentStore/sync.js +8 -1
- package/stores/documents/documentStore/sync.js.map +1 -1
- package/stores/documents/documentStore.d.ts +4 -4
- package/stores/documents/documentStore.js +14 -11
- package/stores/documents/documentStore.js.map +1 -1
- package/stores/documents/types.d.ts +9 -0
- package/stores/documents/types.js.map +1 -1
- package/sync/reconciliation/documentContentPull.js +7 -3
- package/sync/reconciliation/documentContentPull.js.map +1 -1
- package/sync/reconciliation/idleBackfill.js +14 -2
- package/sync/reconciliation/idleBackfill.js.map +1 -1
- package/sync/reconciliation/knownContainerSweep.js +5 -2
- package/sync/reconciliation/knownContainerSweep.js.map +1 -1
- package/sync/reconciliation/reconcileContainer.js +3 -3
- package/sync/reconciliation/reconcileContainer.js.map +1 -1
- package/sync/reconciliation/service.js +9 -4
- package/sync/reconciliation/service.js.map +1 -1
- package/sync/reconciliation/serviceTypes.d.ts +5 -3
- package/sync/reconciliation/serviceTypes.js.map +1 -1
- package/sync/reconciliation/triggers.d.ts +2 -1
- package/sync/reconciliation/triggers.js +5 -2
- package/sync/reconciliation/triggers.js.map +1 -1
- package/workflows/blobs/hydrate.d.ts +4 -2
- package/workflows/blobs/hydrate.js +21 -7
- package/workflows/blobs/hydrate.js.map +1 -1
- package/workflows/blobs/multipartUpload.js +57 -30
- package/workflows/blobs/multipartUpload.js.map +1 -1
- package/workflows/blobs/readAttachmentTransfer.js +42 -0
- package/workflows/blobs/readAttachmentTransfer.js.map +1 -0
- package/workflows/documents/syncLane.d.ts +1 -0
- package/workflows/documents/syncLane.js +1 -0
- package/workflows/documents/syncLane.js.map +1 -1
- package/workflows/registration/persistRegistrationBootstrap.js +3 -0
- package/workflows/registration/persistRegistrationBootstrap.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get.js","sourceRoot":"","sources":["get.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AAyBzC,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAAgC,EAChC,MAAc,EACd,gBAA+B,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC;IAE9D,wEAAwE;IACxE,4EAA4E;IAC5E,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,aAAa,CAC1C,wBAAwB,EACxB,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,EACtB,EAAE,YAAY,EAAE,KAAK,EAAE,CACxB,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACpD,OAAO,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAC1C,CAAC;YACD,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC;QAC/B,IAAI,UAAU,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YACjC,SAAS,CAAC,aAAa,CAAC;gBACtB,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,UAAU,MAAM,WAAW;gBACjC,MAAM,EAAE,GAAG;gBACX,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,QAAQ,MAAM,6CAA6C;aACrE,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACrC,OAAO,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC;YACH,uEAAuE;YACvE,qEAAqE;YACrE,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBAC1D,KAAK,EAAE,UAAU;gBACjB,WAAW,EAAE,MAAM;gBACnB,QAAQ,EAAE,OAAO;gBACjB,cAAc,EAAE,aAAa;aAC9B,CAAC,CAAC;YACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBAC5B,SAAS;gBACX,CAAC;gBACD,MAAM;YACR,CAAC;YACD,OAAO;gBACL,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,UAAU,EAAE,UAAU,CAAC,UAAU;gBACjC,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,cAAc,EAAE,QAAQ,CAAC,IAAI;oBAC3B,CAAC,CAAC,wBAAwB,CAAC,QAAQ,CAAC,IAAI,CAAC;oBACzC,CAAC,CAAC,0BAA0B,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;aAC7D,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;YACnE,yEAAyE;QAC3E,CAAC;IACH,CAAC;IACD,yEAAyE;IACzE,wEAAwE;IACxE,mEAAmE;IACnE,OAAO,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,wBAAwB,CAC/B,MAAkC;IAElC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;IAClC,0EAA0E;IAC1E,wEAAwE;IACxE,OAAO,IAAI,cAAc,CACvB;QACE,KAAK,CAAC,IAAI,CAAC,UAAU;YACnB,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC5C,IAAI,IAAI,EAAE,CAAC;oBACT,UAAU,CAAC,KAAK,EAAE,CAAC;oBACnB,MAAM,CAAC,WAAW,EAAE,CAAC;gBACvB,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,UAAU,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC,CAAC;gBACzD,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,CAAC;QACH,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,MAAM;YACjB,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACvD,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,CAAC;QACH,CAAC;KACF,EACD,EAAE,aAAa,EAAE,CAAC,EAAE,CACrB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,SAA0E,EAC1E,MAAc;IAEd,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,qBAAqB,CAAC,qBAAqB,EAAE;QAC1E,MAAM,EAAE,EAAE,MAAM,EAAE;KACnB,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IACrC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC;IAEtE,iEAAiE;IACjE,uEAAuE;IACvE,iFAAiF;IACjF,8EAA8E;IAC9E,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,qEAAqE;IACrE,MAAM,cAAc,GAClB,QAAQ,CAAC,IAAI,IAAI,0BAA0B,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IAE5E,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC;QAC1C,UAAU;QACV,cAAc;QACd,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,UAAU,CAAC;KAC/C,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,qEAAqE;AACrE,0DAA0D;AAC1D,yEAAyE;AACzE,yCAAyC;AACzC,SAAS,0BAA0B,CACjC,MAAmB;IAEnB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,OAAO,IAAI,cAAc,CAAa;QACpC,KAAK,CAAC,UAAU;YACd,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;gBACzB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;YACD,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n blobWireHeaderKeys,\n getBlobBytesOperation,\n getBlobDownloadOperation,\n} from \"@tearleads/validators/operation\";\nimport type { BinaryResponseOperationTransport } from \"../../binaryResponseOperationTransport\";\nimport type { JsonOperationTransport } from \"../../operationTransport\";\nimport type { OperationResponseRequestFn } from \"../../types\";\n\nexport interface BlobBytesResponse {\n readonly blobId: string;\n readonly byteLength: number;\n readonly encryptedBytes: ReadableStream<Uint8Array>;\n readonly sha256: string;\n}\n\nexport interface UploadMultipartBlobPartBytesRequest {\n readonly byteLength: number;\n readonly encryptedBytes: Blob | BufferSource;\n readonly sha256: string;\n readonly uploadId: string;\n}\n\ntype BlobDownloadTransport = Pick<JsonOperationTransport, \"requestResult\"> &\n Pick<BinaryResponseOperationTransport, \"requestBinaryResponse\"> &\n Pick<OperationResponseRequestFn, \"reportFailure\">;\n\ntype DownloadFetch = (url: string, init: RequestInit) => Promise<Response>;\n\nexport async function getBlobBytes(\n transport: BlobDownloadTransport,\n blobId: string,\n downloadFetch: DownloadFetch = (url, init) => fetch(url, init),\n): Promise<BlobBytesResponse | null> {\n // Refresh at most once. R2 expiration errors can appear as CORS/network\n // failures in browsers, so both rejected fetches and 403s need a fresh URL.\n for (let attempt = 0; attempt < 2; attempt += 1) {\n const result = await transport.requestResult(\n getBlobDownloadOperation,\n { params: { blobId } },\n { reportErrors: false },\n );\n if (!result.ok) {\n if (result.kind === \"http\" && result.status === 404) {\n return getBlobStream(transport, blobId);\n }\n result.report();\n return null;\n }\n const descriptor = result.data;\n if (descriptor.blobId !== blobId) {\n transport.reportFailure({\n kind: \"shape\",\n method: \"GET\",\n path: `/blobs/${blobId}/download`,\n status: 200,\n statusText: \"\",\n message: `Blob ${blobId} download descriptor has the wrong identity`,\n });\n return null;\n }\n if (descriptor.source.kind === \"api\") {\n return getBlobStream(transport, blobId);\n }\n try {\n // Never send the application's session or cookies to the object store.\n // Reject redirects and keep bearer URLs out of referrers and errors.\n const response = await downloadFetch(descriptor.source.url, {\n cache: \"no-store\",\n credentials: \"omit\",\n redirect: \"error\",\n referrerPolicy: \"no-referrer\",\n });\n if (response.status !== 200) {\n await response.body?.cancel().catch(() => {});\n if (response.status === 403) {\n continue;\n }\n break;\n }\n return {\n blobId: descriptor.blobId,\n byteLength: descriptor.byteLength,\n sha256: descriptor.sha256,\n encryptedBytes: response.body\n ? signedDownloadBodyStream(response.body)\n : bufferedResponseBodyStream(await response.arrayBuffer()),\n };\n } catch {\n // The downstream SDK still verifies length, hash and authenticated\n // encryption. An error after streaming begins propagates to that reader.\n }\n }\n // Preserve downloads when storage is unreachable or a native HTTP bridge\n // cannot use the signed URL. This endpoint checks current access again.\n // A storage failure alone must not signal that the API is offline.\n return getBlobStream(transport, blobId);\n}\n\nfunction signedDownloadBodyStream(\n stream: ReadableStream<Uint8Array>,\n): ReadableStream<Uint8Array> {\n const reader = stream.getReader();\n // Native network errors may contain the bearer URL. Keep failures generic\n // before downstream readers log them, without buffering or eager reads.\n return new ReadableStream<Uint8Array>(\n {\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n reader.releaseLock();\n } else {\n controller.enqueue(value);\n }\n } catch {\n controller.error(new Error(\"Blob download interrupted\"));\n reader.releaseLock();\n }\n },\n async cancel(reason) {\n try {\n await reader.cancel(reason);\n } catch {\n throw new Error(\"Blob download cancellation failed\");\n } finally {\n reader.releaseLock();\n }\n },\n },\n { highWaterMark: 0 },\n );\n}\n\nasync function getBlobStream(\n transport: Pick<BinaryResponseOperationTransport, \"requestBinaryResponse\">,\n blobId: string,\n): Promise<BlobBytesResponse | null> {\n const result = await transport.requestBinaryResponse(getBlobBytesOperation, {\n params: { blobId },\n });\n if (!result) {\n return null;\n }\n\n const { headers, response } = result;\n const byteLength = Number(headers[blobWireHeaderKeys.blobByteLength]);\n\n // On the browser / WKWebView-fetch path the body is streamed, so\n // `response.body` is a live ReadableStream. Under a native HTTP bridge\n // (CapacitorHttp — which the Capacitor iOS/Android builds enable so cross-origin\n // API requests reach the backend at all; the WKWebView's own fetch fails from\n // the app's https://localhost origin) the response is buffered natively and\n // `response.body` is null even for a valid 200. So take the bytes either way:\n // the stream when present, otherwise buffer the response and re-wrap it as a\n // one-shot ReadableStream so callers keep a single streaming interface. This\n // does not weaken validation — a truncated or corrupted body is still caught\n // downstream by the X-Tearleads-Blob-Sha256 integrity check; it only stops a\n // buffered-but-valid response from being rejected here as malformed.\n const encryptedBytes =\n response.body ?? bufferedResponseBodyStream(await response.arrayBuffer());\n\n return {\n blobId: headers[blobWireHeaderKeys.blobId],\n byteLength,\n encryptedBytes,\n sha256: headers[blobWireHeaderKeys.blobSha256],\n };\n}\n\n// Wraps a fully-buffered response body as a single-chunk ReadableStream, so a\n// native HTTP bridge that only exposes the buffered bytes (no WHATWG\n// `response.body` stream) can still satisfy the streaming\n// `BlobBytesResponse.encryptedBytes` contract. An empty buffer yields an\n// immediately-closed, zero-chunk stream.\nfunction bufferedResponseBodyStream(\n buffer: ArrayBuffer,\n): ReadableStream<Uint8Array> {\n const bytes = new Uint8Array(buffer);\n return new ReadableStream<Uint8Array>({\n start(controller) {\n if (bytes.byteLength > 0) {\n controller.enqueue(bytes);\n }\n controller.close();\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"get.js","sourceRoot":"","sources":["get.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n blobWireHeaderKeys,\n getBlobBytesOperation,\n getBlobDownloadOperation,\n} from \"@tearleads/validators/operation\";\nimport type { BinaryResponseOperationTransport } from \"../../binaryResponseOperationTransport\";\nimport type { JsonOperationTransport } from \"../../operationTransport\";\nimport type {\n OperationResponseRequestFn,\n RequestResultOptions,\n} from \"../../types\";\nimport { storageFetchOptions } from \"./storageFetchOptions\";\n\nexport interface BlobBytesResponse {\n readonly blobId: string;\n readonly byteLength: number;\n readonly encryptedBytes: ReadableStream<Uint8Array>;\n readonly sha256: string;\n}\n\nexport interface UploadMultipartBlobPartBytesRequest {\n readonly byteLength: number;\n readonly encryptedBytes: Blob | BufferSource;\n readonly sha256: string;\n readonly uploadId: string;\n}\n\ntype BlobDownloadTransport = Pick<JsonOperationTransport, \"requestResult\"> &\n Pick<BinaryResponseOperationTransport, \"requestBinaryResponse\"> &\n Pick<OperationResponseRequestFn, \"reportFailure\">;\n\ntype DownloadFetch = (url: string, init: RequestInit) => Promise<Response>;\n\nexport async function getBlobBytes(\n transport: BlobDownloadTransport,\n blobId: string,\n downloadFetch: DownloadFetch = (url, init) => fetch(url, init),\n options: RequestResultOptions = {},\n): Promise<BlobBytesResponse | null> {\n // Refresh at most once. R2 expiration errors can appear as CORS/network\n // failures in browsers, so both rejected fetches and 403s need a fresh URL.\n for (let attempt = 0; attempt < 2; attempt += 1) {\n if (options.signal?.aborted) return null;\n const result = await transport.requestResult(\n getBlobDownloadOperation,\n { params: { blobId } },\n { ...options, reportErrors: false },\n );\n if (!result.ok) {\n if (result.kind === \"http\" && result.status === 404) {\n return getBlobStream(transport, blobId, options);\n }\n result.report();\n return null;\n }\n const descriptor = result.data;\n if (descriptor.blobId !== blobId) {\n transport.reportFailure({\n kind: \"shape\",\n method: \"GET\",\n path: `/blobs/${blobId}/download`,\n status: 200,\n statusText: \"\",\n message: `Blob ${blobId} download descriptor has the wrong identity`,\n });\n return null;\n }\n if (descriptor.source.kind === \"api\") {\n return getBlobStream(transport, blobId, options);\n }\n try {\n // Never send the application's session or cookies to the object store.\n // Reject redirects and keep bearer URLs out of referrers and errors.\n const response = await downloadFetch(\n descriptor.source.url,\n storageFetchOptions(options),\n );\n if (response.status !== 200) {\n await response.body?.cancel().catch(() => {});\n if (response.status === 403) {\n continue;\n }\n break;\n }\n return await signedBlobResponse(descriptor, response);\n } catch {\n // The downstream SDK still verifies length, hash and authenticated\n // encryption. An error after streaming begins propagates to that reader.\n }\n }\n // Preserve downloads when storage is unreachable or a native HTTP bridge\n // cannot use the signed URL. This endpoint checks current access again.\n // A storage failure alone must not signal that the API is offline.\n return getBlobStream(transport, blobId, options);\n}\n\nasync function signedBlobResponse(\n descriptor: Pick<BlobBytesResponse, \"blobId\" | \"byteLength\" | \"sha256\">,\n response: Response,\n): Promise<BlobBytesResponse> {\n return {\n blobId: descriptor.blobId,\n byteLength: descriptor.byteLength,\n sha256: descriptor.sha256,\n encryptedBytes: response.body\n ? signedDownloadBodyStream(response.body)\n : bufferedResponseBodyStream(await response.arrayBuffer()),\n };\n}\n\nfunction signedDownloadBodyStream(\n stream: ReadableStream<Uint8Array>,\n): ReadableStream<Uint8Array> {\n const reader = stream.getReader();\n // Native network errors may contain the bearer URL. Keep failures generic\n // before downstream readers log them, without buffering or eager reads.\n return new ReadableStream<Uint8Array>(\n {\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n reader.releaseLock();\n } else {\n controller.enqueue(value);\n }\n } catch {\n controller.error(new Error(\"Blob download interrupted\"));\n reader.releaseLock();\n }\n },\n async cancel(reason) {\n try {\n await reader.cancel(reason);\n } catch {\n throw new Error(\"Blob download cancellation failed\");\n } finally {\n reader.releaseLock();\n }\n },\n },\n { highWaterMark: 0 },\n );\n}\n\nasync function getBlobStream(\n transport: Pick<BinaryResponseOperationTransport, \"requestBinaryResponse\">,\n blobId: string,\n options: RequestResultOptions,\n): Promise<BlobBytesResponse | null> {\n if (options.signal?.aborted) return null;\n const result = await transport.requestBinaryResponse(\n getBlobBytesOperation,\n {\n params: { blobId },\n },\n options,\n );\n if (!result) {\n return null;\n }\n\n const { headers, response } = result;\n const byteLength = Number(headers[blobWireHeaderKeys.blobByteLength]);\n\n // On the browser / WKWebView-fetch path the body is streamed, so\n // `response.body` is a live ReadableStream. Under a native HTTP bridge\n // (CapacitorHttp — which the Capacitor iOS/Android builds enable so cross-origin\n // API requests reach the backend at all; the WKWebView's own fetch fails from\n // the app's https://localhost origin) the response is buffered natively and\n // `response.body` is null even for a valid 200. So take the bytes either way:\n // the stream when present, otherwise buffer the response and re-wrap it as a\n // one-shot ReadableStream so callers keep a single streaming interface. This\n // does not weaken validation — a truncated or corrupted body is still caught\n // downstream by the X-Tearleads-Blob-Sha256 integrity check; it only stops a\n // buffered-but-valid response from being rejected here as malformed.\n const encryptedBytes =\n response.body ?? bufferedResponseBodyStream(await response.arrayBuffer());\n\n return {\n blobId: headers[blobWireHeaderKeys.blobId],\n byteLength,\n encryptedBytes,\n sha256: headers[blobWireHeaderKeys.blobSha256],\n };\n}\n\n// Wraps a fully-buffered response body as a single-chunk ReadableStream, so a\n// native HTTP bridge that only exposes the buffered bytes (no WHATWG\n// `response.body` stream) can still satisfy the streaming\n// `BlobBytesResponse.encryptedBytes` contract. An empty buffer yields an\n// immediately-closed, zero-chunk stream.\nfunction bufferedResponseBodyStream(\n buffer: ArrayBuffer,\n): ReadableStream<Uint8Array> {\n const bytes = new Uint8Array(buffer);\n return new ReadableStream<Uint8Array>({\n start(controller) {\n if (bytes.byteLength > 0) {\n controller.enqueue(bytes);\n }\n controller.close();\n },\n });\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import { md5 } from "@noble/hashes/legacy.js";
|
|
|
2
2
|
import { bytesToHex } from "../../../crypto/index.js";
|
|
3
3
|
import { authorizeMultipartBlobPartUploadOperation, blobWireHeaderKeys, operationRequestPath, uploadMultipartBlobPartBytesOperation, } from "../../../validators/operation/index.js";
|
|
4
4
|
import { MAX_MULTIPART_BLOB_PART_BYTES } from "../../../validators/util/index.js";
|
|
5
|
+
import { storageFetchOptions } from "./storageFetchOptions.js";
|
|
5
6
|
function invalidPart(transport, path) {
|
|
6
7
|
transport.reportFailure({
|
|
7
8
|
kind: "shape",
|
|
@@ -44,7 +45,9 @@ async function preparePart(input) {
|
|
|
44
45
|
const contentMd5 = btoa(String.fromCharCode(...md5(new Uint8Array(bytes))));
|
|
45
46
|
return { file, contentMd5 };
|
|
46
47
|
}
|
|
47
|
-
export async function uploadMultipartBlobPartBytes(transport, stageId, partNumber, input, uploadFetch = (url, init) => fetch(url, init)) {
|
|
48
|
+
export async function uploadMultipartBlobPartBytes(transport, stageId, partNumber, input, uploadFetch = (url, init) => fetch(url, init), options = {}) {
|
|
49
|
+
if (options.signal?.aborted)
|
|
50
|
+
return null;
|
|
48
51
|
const operation = authorizeMultipartBlobPartUploadOperation;
|
|
49
52
|
const params = { stageId, partNumber };
|
|
50
53
|
const path = operationRequestPath(operation, params);
|
|
@@ -62,7 +65,9 @@ export async function uploadMultipartBlobPartBytes(transport, stageId, partNumbe
|
|
|
62
65
|
// failure may be an expired URL hidden by R2 CORS, or a lost success response;
|
|
63
66
|
// reauthorize once and send the same immutable bytes to the same part number.
|
|
64
67
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
65
|
-
|
|
68
|
+
if (options.signal?.aborted)
|
|
69
|
+
return null;
|
|
70
|
+
const result = await transport.requestResult(operation, { params, body }, options);
|
|
66
71
|
if (!result.ok)
|
|
67
72
|
return null;
|
|
68
73
|
const descriptor = result.data;
|
|
@@ -91,7 +96,7 @@ export async function uploadMultipartBlobPartBytes(transport, stageId, partNumbe
|
|
|
91
96
|
[blobWireHeaderKeys.partSha256]: input.sha256,
|
|
92
97
|
[blobWireHeaderKeys.partUploadId]: input.uploadId,
|
|
93
98
|
},
|
|
94
|
-
}, { encodeBody: () => file });
|
|
99
|
+
}, { ...options, encodeBody: () => file });
|
|
95
100
|
}
|
|
96
101
|
try {
|
|
97
102
|
const response = await uploadFetch(descriptor.source.url, {
|
|
@@ -101,10 +106,7 @@ export async function uploadMultipartBlobPartBytes(transport, stageId, partNumbe
|
|
|
101
106
|
"Content-Type": "application/octet-stream",
|
|
102
107
|
"Content-MD5": contentMd5,
|
|
103
108
|
},
|
|
104
|
-
|
|
105
|
-
credentials: "omit",
|
|
106
|
-
redirect: "error",
|
|
107
|
-
referrerPolicy: "no-referrer",
|
|
109
|
+
...storageFetchOptions(options),
|
|
108
110
|
});
|
|
109
111
|
status = response.status;
|
|
110
112
|
const etag = response.headers.get("ETag")?.trim();
|
|
@@ -124,9 +126,15 @@ export async function uploadMultipartBlobPartBytes(transport, stageId, partNumbe
|
|
|
124
126
|
status = null;
|
|
125
127
|
}
|
|
126
128
|
}
|
|
129
|
+
if (options.signal?.aborted)
|
|
130
|
+
return null;
|
|
131
|
+
reportStorageFailure(transport, path, partNumber, status);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function reportStorageFailure(transport, path, partNumber, status) {
|
|
127
135
|
transport.reportFailure({
|
|
128
136
|
kind: "storage",
|
|
129
|
-
method:
|
|
137
|
+
method: authorizeMultipartBlobPartUploadOperation.method,
|
|
130
138
|
path,
|
|
131
139
|
status,
|
|
132
140
|
statusText: "",
|
|
@@ -134,6 +142,5 @@ export async function uploadMultipartBlobPartBytes(transport, stageId, partNumbe
|
|
|
134
142
|
? `Multipart part ${partNumber} storage response is missing ETag; check storage CORS exposure`
|
|
135
143
|
: `Multipart part ${partNumber} storage upload failed${status === null ? "" : ` (HTTP ${status})`}`,
|
|
136
144
|
});
|
|
137
|
-
return null;
|
|
138
145
|
}
|
|
139
146
|
//# sourceMappingURL=multipartUpload.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multipartUpload.js","sourceRoot":"","sources":["multipartUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EACL,yCAAyC,EACzC,kBAAkB,EAClB,oBAAoB,EACpB,qCAAqC,GACtC,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAM3E,SAAS,WAAW,CAAC,SAA6B,EAAE,IAAY;IAC9D,SAAS,CAAC,aAAa,CAAC;QACtB,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,yCAAyC,CAAC,MAAM;QACxD,IAAI;QACJ,MAAM,EAAE,IAAI;QACZ,UAAU,EAAE,EAAE;QACd,OAAO,EAAE,kEAAkE;KAC5E,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,OAAe;IAC/D,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,SAAS,GAAG,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC,CAAC;IACrE,IAAI,SAAS,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,KAA0C;IACnE,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,IAAI,GACR,KAAK,CAAC,cAAc,YAAY,IAAI;QAClC,CAAC,CAAC,KAAK,CAAC,cAAc;QACtB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,qBAAqB,EAAE;YACtD,IAAI,EAAE,0BAA0B;SACjC,CAAC,CAAC;IACT,IACE,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU;QAC9B,IAAI,CAAC,IAAI,GAAG,CAAC;QACb,IAAI,CAAC,IAAI,GAAG,6BAA6B,EACzC,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IACvC,IACE,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;QACxE,KAAK,CAAC,MAAM,EACZ,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,iDAAiD,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,SAA6B,EAC7B,OAAe,EACf,UAAkB,EAClB,KAA0C,EAC1C,cAA2B,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC;IAE1D,MAAM,SAAS,GAAG,yCAAyC,CAAC;IAC5D,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACvC,MAAM,IAAI,GAAG,oBAAoB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CACnD,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,CAC7B,CAAC;IACF,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAAC;IACtC,MAAM,IAAI,GAAG;QACX,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU;QACV,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAC;IACF,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,2EAA2E;IAC3E,+EAA+E;IAC/E,8EAA8E;IAC9E,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAC5B,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC;QAC/B,IACE,UAAU,CAAC,OAAO,KAAK,OAAO;YAC9B,UAAU,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ;YACtC,UAAU,CAAC,UAAU,KAAK,UAAU;YACpC,UAAU,CAAC,UAAU,KAAK,KAAK,CAAC,UAAU;YAC1C,UAAU,CAAC,UAAU,KAAK,UAAU,EACpC,CAAC;YACD,SAAS,CAAC,aAAa,CAAC;gBACtB,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,SAAS,CAAC,MAAM;gBACxB,IAAI;gBACJ,MAAM,EAAE,GAAG;gBACX,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,qDAAqD;aAC/D,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACrC,uEAAuE;YACvE,OAAO,SAAS,CAAC,oBAAoB,CACnC,qCAAqC,EACrC;gBACE,MAAM;gBACN,IAAI,EAAE,KAAK,CAAC,cAAc;gBAC1B,OAAO,EAAE;oBACP,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE;oBAChE,CAAC,kBAAkB,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM;oBAC7C,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC,QAAQ;iBAClD;aACF,EACD,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAC3B,CAAC;QACJ,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBACxD,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,IAAI;gBACV,OAAO,EAAE;oBACP,cAAc,EAAE,0BAA0B;oBAC1C,aAAa,EAAE,UAAU;iBAC1B;gBACD,KAAK,EAAE,UAAU;gBACjB,WAAW,EAAE,MAAM;gBACnB,QAAQ,EAAE,OAAO;gBACjB,cAAc,EAAE,aAAa;aAC9B,CAAC,CAAC;YACH,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;YACzB,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;YAClD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC9C,IAAI,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;gBAC3B,OAAO;oBACL,OAAO;oBACP,QAAQ,EAAE,KAAK,CAAC,QAAQ;oBACxB,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE;iBACzD,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,CAAC,MAAM,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAAE,MAAM;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,4EAA4E;YAC5E,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;IACD,SAAS,CAAC,aAAa,CAAC;QACtB,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,IAAI;QACJ,MAAM;QACN,UAAU,EAAE,EAAE;QACd,OAAO,EACL,MAAM,KAAK,GAAG;YACZ,CAAC,CAAC,kBAAkB,UAAU,gEAAgE;YAC9F,CAAC,CAAC,kBAAkB,UAAU,yBAAyB,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,GAAG,EAAE;KACxG,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { md5 } from \"@noble/hashes/legacy.js\";\nimport { bytesToHex } from \"@tearleads/crypto\";\nimport {\n authorizeMultipartBlobPartUploadOperation,\n blobWireHeaderKeys,\n operationRequestPath,\n uploadMultipartBlobPartBytesOperation,\n} from \"@tearleads/validators/operation\";\nimport type { UploadMultipartBlobPartResponse } from \"@tearleads/validators/response\";\nimport { MAX_MULTIPART_BLOB_PART_BYTES } from \"@tearleads/validators/util\";\nimport type { OperationTransport } from \"../../operationTransportFactory\";\nimport type { UploadMultipartBlobPartBytesRequest } from \"./get\";\n\ntype UploadFetch = (url: string, init: RequestInit) => Promise<Response>;\n\nfunction invalidPart(transport: OperationTransport, path: string) {\n transport.reportFailure({\n kind: \"shape\",\n method: authorizeMultipartBlobPartUploadOperation.method,\n path,\n status: null,\n statusText: \"\",\n message: \"Multipart part bytes do not match the declared length or SHA-256\",\n });\n return null;\n}\n\nasync function retryStorageStatus(status: number, attempt: number) {\n if (status === 403) return true;\n const transient = status === 429 || (status >= 500 && status <= 599);\n if (transient && attempt === 0) {\n await new Promise<void>((resolve) => setTimeout(resolve, 250));\n }\n return transient;\n}\n\nasync function preparePart(input: UploadMultipartBlobPartBytesRequest) {\n // File snapshots mutable buffers and preserves ciphertext across Capacitor's\n // native HTTP bridge. Uint8Array is UTF-8 decoded there; a plain Blob becomes\n // JSON. File's base64 bridge path is byte-for-byte, including non-UTF-8 data.\n const file =\n input.encryptedBytes instanceof File\n ? input.encryptedBytes\n : new File([input.encryptedBytes], \"encrypted-blob-part\", {\n type: \"application/octet-stream\",\n });\n if (\n file.size !== input.byteLength ||\n file.size < 1 ||\n file.size > MAX_MULTIPART_BLOB_PART_BYTES\n ) {\n throw new TypeError(\"Invalid multipart part byte length\");\n }\n const bytes = await file.arrayBuffer();\n if (\n bytesToHex(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes))) !==\n input.sha256\n ) {\n throw new TypeError(\"Multipart part SHA-256 does not match its bytes\");\n }\n const contentMd5 = btoa(String.fromCharCode(...md5(new Uint8Array(bytes))));\n return { file, contentMd5 };\n}\n\nexport async function uploadMultipartBlobPartBytes(\n transport: OperationTransport,\n stageId: string,\n partNumber: number,\n input: UploadMultipartBlobPartBytesRequest,\n uploadFetch: UploadFetch = (url, init) => fetch(url, init),\n): Promise<UploadMultipartBlobPartResponse | null> {\n const operation = authorizeMultipartBlobPartUploadOperation;\n const params = { stageId, partNumber };\n const path = operationRequestPath(operation, params);\n const prepared = await preparePart(input).catch(() =>\n invalidPart(transport, path),\n );\n if (!prepared) return null;\n const { file, contentMd5 } = prepared;\n const body = {\n byteLength: input.byteLength,\n contentMd5,\n uploadId: input.uploadId,\n };\n let status: number | null = null;\n // Sign just in time for each part, not once for the entire file. A network\n // failure may be an expired URL hidden by R2 CORS, or a lost success response;\n // reauthorize once and send the same immutable bytes to the same part number.\n for (let attempt = 0; attempt < 2; attempt += 1) {\n const result = await transport.requestResult(operation, { params, body });\n if (!result.ok) return null;\n const descriptor = result.data;\n if (\n descriptor.stageId !== stageId ||\n descriptor.uploadId !== input.uploadId ||\n descriptor.partNumber !== partNumber ||\n descriptor.byteLength !== input.byteLength ||\n descriptor.contentMd5 !== contentMd5\n ) {\n transport.reportFailure({\n kind: \"shape\",\n method: operation.method,\n path,\n status: 200,\n statusText: \"\",\n message: \"Multipart upload descriptor does not match its part\",\n });\n return null;\n }\n if (descriptor.source.kind === \"api\") {\n // A capability path for memory/custom stores; S3/R2 never proxy parts.\n return transport.requestBinaryRequest(\n uploadMultipartBlobPartBytesOperation,\n {\n params,\n body: input.encryptedBytes,\n headers: {\n [blobWireHeaderKeys.partByteLength]: input.byteLength.toString(),\n [blobWireHeaderKeys.partSha256]: input.sha256,\n [blobWireHeaderKeys.partUploadId]: input.uploadId,\n },\n },\n { encodeBody: () => file },\n );\n }\n try {\n const response = await uploadFetch(descriptor.source.url, {\n method: \"PUT\",\n body: file,\n headers: {\n \"Content-Type\": \"application/octet-stream\",\n \"Content-MD5\": contentMd5,\n },\n cache: \"no-store\",\n credentials: \"omit\",\n redirect: \"error\",\n referrerPolicy: \"no-referrer\",\n });\n status = response.status;\n const etag = response.headers.get(\"ETag\")?.trim();\n await response.body?.cancel().catch(() => {});\n if (status === 200 && etag) {\n return {\n stageId,\n uploadId: input.uploadId,\n part: { byteLength: input.byteLength, partNumber, etag },\n };\n }\n if (!(await retryStorageStatus(status, attempt))) break;\n } catch {\n // Never expose a provider exception or body: it may contain the bearer URL.\n status = null;\n }\n }\n transport.reportFailure({\n kind: \"storage\",\n method: operation.method,\n path,\n status,\n statusText: \"\",\n message:\n status === 200\n ? `Multipart part ${partNumber} storage response is missing ETag; check storage CORS exposure`\n : `Multipart part ${partNumber} storage upload failed${status === null ? \"\" : ` (HTTP ${status})`}`,\n });\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"multipartUpload.js","sourceRoot":"","sources":["multipartUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EACL,yCAAyC,EACzC,kBAAkB,EAClB,oBAAoB,EACpB,qCAAqC,GACtC,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAI3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAI5D,SAAS,WAAW,CAAC,SAA6B,EAAE,IAAY;IAC9D,SAAS,CAAC,aAAa,CAAC;QACtB,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,yCAAyC,CAAC,MAAM;QACxD,IAAI;QACJ,MAAM,EAAE,IAAI;QACZ,UAAU,EAAE,EAAE;QACd,OAAO,EAAE,kEAAkE;KAC5E,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,OAAe;IAC/D,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,SAAS,GAAG,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC,CAAC;IACrE,IAAI,SAAS,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,KAA0C;IACnE,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,IAAI,GACR,KAAK,CAAC,cAAc,YAAY,IAAI;QAClC,CAAC,CAAC,KAAK,CAAC,cAAc;QACtB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,qBAAqB,EAAE;YACtD,IAAI,EAAE,0BAA0B;SACjC,CAAC,CAAC;IACT,IACE,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU;QAC9B,IAAI,CAAC,IAAI,GAAG,CAAC;QACb,IAAI,CAAC,IAAI,GAAG,6BAA6B,EACzC,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IACvC,IACE,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;QACxE,KAAK,CAAC,MAAM,EACZ,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,iDAAiD,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,SAA6B,EAC7B,OAAe,EACf,UAAkB,EAClB,KAA0C,EAC1C,cAA2B,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,EAC1D,UAAgC,EAAE;IAElC,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,SAAS,GAAG,yCAAyC,CAAC;IAC5D,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACvC,MAAM,IAAI,GAAG,oBAAoB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CACnD,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,CAC7B,CAAC;IACF,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAAC;IACtC,MAAM,IAAI,GAAG;QACX,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU;QACV,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAC;IACF,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,2EAA2E;IAC3E,+EAA+E;IAC/E,8EAA8E;IAC9E,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;YAAE,OAAO,IAAI,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,aAAa,CAC1C,SAAS,EACT,EAAE,MAAM,EAAE,IAAI,EAAE,EAChB,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAC5B,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC;QAC/B,IACE,UAAU,CAAC,OAAO,KAAK,OAAO;YAC9B,UAAU,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ;YACtC,UAAU,CAAC,UAAU,KAAK,UAAU;YACpC,UAAU,CAAC,UAAU,KAAK,KAAK,CAAC,UAAU;YAC1C,UAAU,CAAC,UAAU,KAAK,UAAU,EACpC,CAAC;YACD,SAAS,CAAC,aAAa,CAAC;gBACtB,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,SAAS,CAAC,MAAM;gBACxB,IAAI;gBACJ,MAAM,EAAE,GAAG;gBACX,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,qDAAqD;aAC/D,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACrC,uEAAuE;YACvE,OAAO,SAAS,CAAC,oBAAoB,CACnC,qCAAqC,EACrC;gBACE,MAAM;gBACN,IAAI,EAAE,KAAK,CAAC,cAAc;gBAC1B,OAAO,EAAE;oBACP,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE;oBAChE,CAAC,kBAAkB,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM;oBAC7C,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC,QAAQ;iBAClD;aACF,EACD,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CACvC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE;gBACxD,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,IAAI;gBACV,OAAO,EAAE;oBACP,cAAc,EAAE,0BAA0B;oBAC1C,aAAa,EAAE,UAAU;iBAC1B;gBACD,GAAG,mBAAmB,CAAC,OAAO,CAAC;aAChC,CAAC,CAAC;YACH,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;YACzB,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;YAClD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC9C,IAAI,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;gBAC3B,OAAO;oBACL,OAAO;oBACP,QAAQ,EAAE,KAAK,CAAC,QAAQ;oBACxB,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE;iBACzD,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,CAAC,MAAM,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAAE,MAAM;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,4EAA4E;YAC5E,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;QAAE,OAAO,IAAI,CAAC;IACzC,oBAAoB,CAAC,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,oBAAoB,CAC3B,SAA6B,EAC7B,IAAY,EACZ,UAAkB,EAClB,MAAqB;IAErB,SAAS,CAAC,aAAa,CAAC;QACtB,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,yCAAyC,CAAC,MAAM;QACxD,IAAI;QACJ,MAAM;QACN,UAAU,EAAE,EAAE;QACd,OAAO,EACL,MAAM,KAAK,GAAG;YACZ,CAAC,CAAC,kBAAkB,UAAU,gEAAgE;YAC9F,CAAC,CAAC,kBAAkB,UAAU,yBAAyB,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,GAAG,EAAE;KACxG,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { md5 } from \"@noble/hashes/legacy.js\";\nimport { bytesToHex } from \"@tearleads/crypto\";\nimport {\n authorizeMultipartBlobPartUploadOperation,\n blobWireHeaderKeys,\n operationRequestPath,\n uploadMultipartBlobPartBytesOperation,\n} from \"@tearleads/validators/operation\";\nimport type { UploadMultipartBlobPartResponse } from \"@tearleads/validators/response\";\nimport { MAX_MULTIPART_BLOB_PART_BYTES } from \"@tearleads/validators/util\";\nimport type { OperationTransport } from \"../../operationTransportFactory\";\nimport type { RequestResultOptions } from \"../../types\";\nimport type { UploadMultipartBlobPartBytesRequest } from \"./get\";\nimport { storageFetchOptions } from \"./storageFetchOptions\";\n\ntype UploadFetch = (url: string, init: RequestInit) => Promise<Response>;\n\nfunction invalidPart(transport: OperationTransport, path: string) {\n transport.reportFailure({\n kind: \"shape\",\n method: authorizeMultipartBlobPartUploadOperation.method,\n path,\n status: null,\n statusText: \"\",\n message: \"Multipart part bytes do not match the declared length or SHA-256\",\n });\n return null;\n}\n\nasync function retryStorageStatus(status: number, attempt: number) {\n if (status === 403) return true;\n const transient = status === 429 || (status >= 500 && status <= 599);\n if (transient && attempt === 0) {\n await new Promise<void>((resolve) => setTimeout(resolve, 250));\n }\n return transient;\n}\n\nasync function preparePart(input: UploadMultipartBlobPartBytesRequest) {\n // File snapshots mutable buffers and preserves ciphertext across Capacitor's\n // native HTTP bridge. Uint8Array is UTF-8 decoded there; a plain Blob becomes\n // JSON. File's base64 bridge path is byte-for-byte, including non-UTF-8 data.\n const file =\n input.encryptedBytes instanceof File\n ? input.encryptedBytes\n : new File([input.encryptedBytes], \"encrypted-blob-part\", {\n type: \"application/octet-stream\",\n });\n if (\n file.size !== input.byteLength ||\n file.size < 1 ||\n file.size > MAX_MULTIPART_BLOB_PART_BYTES\n ) {\n throw new TypeError(\"Invalid multipart part byte length\");\n }\n const bytes = await file.arrayBuffer();\n if (\n bytesToHex(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", bytes))) !==\n input.sha256\n ) {\n throw new TypeError(\"Multipart part SHA-256 does not match its bytes\");\n }\n const contentMd5 = btoa(String.fromCharCode(...md5(new Uint8Array(bytes))));\n return { file, contentMd5 };\n}\n\nexport async function uploadMultipartBlobPartBytes(\n transport: OperationTransport,\n stageId: string,\n partNumber: number,\n input: UploadMultipartBlobPartBytesRequest,\n uploadFetch: UploadFetch = (url, init) => fetch(url, init),\n options: RequestResultOptions = {},\n): Promise<UploadMultipartBlobPartResponse | null> {\n if (options.signal?.aborted) return null;\n const operation = authorizeMultipartBlobPartUploadOperation;\n const params = { stageId, partNumber };\n const path = operationRequestPath(operation, params);\n const prepared = await preparePart(input).catch(() =>\n invalidPart(transport, path),\n );\n if (!prepared) return null;\n const { file, contentMd5 } = prepared;\n const body = {\n byteLength: input.byteLength,\n contentMd5,\n uploadId: input.uploadId,\n };\n let status: number | null = null;\n // Sign just in time for each part, not once for the entire file. A network\n // failure may be an expired URL hidden by R2 CORS, or a lost success response;\n // reauthorize once and send the same immutable bytes to the same part number.\n for (let attempt = 0; attempt < 2; attempt += 1) {\n if (options.signal?.aborted) return null;\n const result = await transport.requestResult(\n operation,\n { params, body },\n options,\n );\n if (!result.ok) return null;\n const descriptor = result.data;\n if (\n descriptor.stageId !== stageId ||\n descriptor.uploadId !== input.uploadId ||\n descriptor.partNumber !== partNumber ||\n descriptor.byteLength !== input.byteLength ||\n descriptor.contentMd5 !== contentMd5\n ) {\n transport.reportFailure({\n kind: \"shape\",\n method: operation.method,\n path,\n status: 200,\n statusText: \"\",\n message: \"Multipart upload descriptor does not match its part\",\n });\n return null;\n }\n if (descriptor.source.kind === \"api\") {\n // A capability path for memory/custom stores; S3/R2 never proxy parts.\n return transport.requestBinaryRequest(\n uploadMultipartBlobPartBytesOperation,\n {\n params,\n body: input.encryptedBytes,\n headers: {\n [blobWireHeaderKeys.partByteLength]: input.byteLength.toString(),\n [blobWireHeaderKeys.partSha256]: input.sha256,\n [blobWireHeaderKeys.partUploadId]: input.uploadId,\n },\n },\n { ...options, encodeBody: () => file },\n );\n }\n try {\n const response = await uploadFetch(descriptor.source.url, {\n method: \"PUT\",\n body: file,\n headers: {\n \"Content-Type\": \"application/octet-stream\",\n \"Content-MD5\": contentMd5,\n },\n ...storageFetchOptions(options),\n });\n status = response.status;\n const etag = response.headers.get(\"ETag\")?.trim();\n await response.body?.cancel().catch(() => {});\n if (status === 200 && etag) {\n return {\n stageId,\n uploadId: input.uploadId,\n part: { byteLength: input.byteLength, partNumber, etag },\n };\n }\n if (!(await retryStorageStatus(status, attempt))) break;\n } catch {\n // Never expose a provider exception or body: it may contain the bearer URL.\n status = null;\n }\n }\n if (options.signal?.aborted) return null;\n reportStorageFailure(transport, path, partNumber, status);\n return null;\n}\n\nfunction reportStorageFailure(\n transport: OperationTransport,\n path: string,\n partNumber: number,\n status: number | null,\n) {\n transport.reportFailure({\n kind: \"storage\",\n method: authorizeMultipartBlobPartUploadOperation.method,\n path,\n status,\n statusText: \"\",\n message:\n status === 200\n ? `Multipart part ${partNumber} storage response is missing ETag; check storage CORS exposure`\n : `Multipart part ${partNumber} storage upload failed${status === null ? \"\" : ` (HTTP ${status})`}`,\n });\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Keep bearer URLs and application credentials out of object-store requests. */
|
|
2
|
+
export function storageFetchOptions(options) {
|
|
3
|
+
return {
|
|
4
|
+
cache: "no-store",
|
|
5
|
+
credentials: "omit",
|
|
6
|
+
redirect: "error",
|
|
7
|
+
referrerPolicy: "no-referrer",
|
|
8
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=storageFetchOptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storageFetchOptions.js","sourceRoot":"","sources":["storageFetchOptions.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,MAAM,UAAU,mBAAmB,CACjC,OAA6B;IAE7B,OAAO;QACL,KAAK,EAAE,UAAU;QACjB,WAAW,EAAE,MAAM;QACnB,QAAQ,EAAE,OAAO;QACjB,cAAc,EAAE,aAAa;QAC7B,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC","sourcesContent":["import type { RequestResultOptions } from \"../../types\";\n\n/** Keep bearer URLs and application credentials out of object-store requests. */\nexport function storageFetchOptions(\n options: RequestResultOptions,\n): RequestInit {\n return {\n cache: \"no-store\",\n credentials: \"omit\",\n redirect: \"error\",\n referrerPolicy: \"no-referrer\",\n ...(options.signal ? { signal: options.signal } : {}),\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -5,6 +5,10 @@ import { createRuntimePrincipalPolicyWarmer } from "../../../workflows/principal
|
|
|
5
5
|
import { commitHydratedAttachment } from "./attachmentHydrationCommit.js";
|
|
6
6
|
import { runDocumentOrphanMaintenance } from "./orphanMaintenance.js";
|
|
7
7
|
import { isDocumentStoreSyncGenerationCurrent as isSyncGenerationCurrent, } from "./syncGeneration.js";
|
|
8
|
+
// A remote probe is evidence that bindings may have moved independently of
|
|
9
|
+
// content. Capture its sequence before I/O so a hint arriving during hydration
|
|
10
|
+
// survives for the trailing pass. Missing or changed local bytes always retry.
|
|
11
|
+
const checkedSignals = new WeakMap();
|
|
8
12
|
export async function hydrateAttachmentBlobs(state, currentDoc, currentRecord, expectedGeneration) {
|
|
9
13
|
const generationIsCurrent = () => !expectedGeneration || isSyncGenerationCurrent(state, expectedGeneration);
|
|
10
14
|
if (!generationIsCurrent())
|
|
@@ -21,9 +25,16 @@ export async function hydrateAttachmentBlobs(state, currentDoc, currentRecord, e
|
|
|
21
25
|
if (attachments.length === 0) {
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
28
|
+
const signalSequence = state.remoteUpdateSignalSeq;
|
|
29
|
+
if (bindingsAlreadyChecked(state, currentDoc, currentRecord.documentId, attachments, signalSequence))
|
|
30
|
+
return;
|
|
24
31
|
const expectedStorageKeys = { ...state.attachmentStorageKeyBySlotId };
|
|
32
|
+
let bindingsChecked = false;
|
|
25
33
|
const hydratedBlobs = await hydrateDocumentAttachmentBlobs({
|
|
26
34
|
apiClient: runtime.apiClient,
|
|
35
|
+
onBindingsChecked: () => {
|
|
36
|
+
bindingsChecked = true;
|
|
37
|
+
},
|
|
27
38
|
attachments,
|
|
28
39
|
documentId: currentRecord.documentId,
|
|
29
40
|
execSql: runtime.infra.execSql,
|
|
@@ -52,6 +63,28 @@ export async function hydrateAttachmentBlobs(state, currentDoc, currentRecord, e
|
|
|
52
63
|
expectedStorageKey: expectedStorageKeys[hydratedBlob.attachment.slotId] ?? null,
|
|
53
64
|
});
|
|
54
65
|
}
|
|
66
|
+
if (bindingsChecked &&
|
|
67
|
+
generationIsCurrent() &&
|
|
68
|
+
hydratedBlobs.every((blob) => state.attachmentBlobIdBySlotId[blob.attachment.slotId] ===
|
|
69
|
+
blob.binding.blobId)) {
|
|
70
|
+
checkedSignals.set(state, {
|
|
71
|
+
documentId: currentRecord.documentId,
|
|
72
|
+
document: new WeakRef(currentDoc),
|
|
73
|
+
sequence: signalSequence,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
55
76
|
void runDocumentOrphanMaintenance(state);
|
|
56
77
|
}
|
|
78
|
+
function bindingsAlreadyChecked(state, currentDoc, documentId, attachments, signalSequence) {
|
|
79
|
+
const checked = checkedSignals.get(state);
|
|
80
|
+
const needsBindingProbe = signalSequence >
|
|
81
|
+
(checked?.documentId === documentId &&
|
|
82
|
+
checked.document.deref() === currentDoc
|
|
83
|
+
? checked.sequence
|
|
84
|
+
: 0);
|
|
85
|
+
return (!needsBindingProbe &&
|
|
86
|
+
attachments.every((attachment) => state.attachmentStorageKeyBySlotId[attachment.slotId] &&
|
|
87
|
+
state.attachmentContentSha256BySlotId[attachment.slotId] ===
|
|
88
|
+
attachment.contentSha256));
|
|
89
|
+
}
|
|
57
90
|
//# sourceMappingURL=attachmentHydration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachmentHydration.js","sourceRoot":"","sources":["attachmentHydration.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAE1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,OAAO,EAAE,8BAA8B,EAAE,MAAM,0BAA0B,CAAC;AAE1E,OAAO,EAAE,kCAAkC,EAAE,MAAM,mDAAmD,CAAC;AACvG,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAEnE,OAAO,EAEL,oCAAoC,IAAI,uBAAuB,GAChE,MAAM,kBAAkB,CAAC;AAE1B,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,KAAyB,EACzB,UAAyB,EACzB,aAAoC,EACpC,kBAAgD;IAEhD,MAAM,mBAAmB,GAAG,GAAG,EAAE,CAC/B,CAAC,kBAAkB,IAAI,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC;IAC5E,IAAI,CAAC,mBAAmB,EAAE;QAAE,OAAO;IAEnC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,MAAM,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC;IACjE,IACE,CAAC,oBAAoB;QACrB,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe;QAC7B,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM;QACrB,CAAC,aAAa,EAAE,UAAU,EAC1B,CAAC;QACD,OAAO;IACT,CAAC;IAED,MAAM,WAAW,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;IACvD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO;IACT,CAAC;IAED,MAAM,mBAAmB,GAAG,EAAE,GAAG,KAAK,CAAC,4BAA4B,EAAE,CAAC;IACtE,MAAM,aAAa,GAAG,MAAM,8BAA8B,CAAC;QACzD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,WAAW;QACX,UAAU,EAAE,aAAa,CAAC,UAAU;QACpC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO;QAC9B,mBAAmB,EAAE,KAAK,CAAC,wBAAwB;QACnD,uBAAuB,EAAE,KAAK,CAAC,4BAA4B;QAC3D,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG;QACrB,sBAAsB,EAAE,KAAK,CAAC,gCAAgC;QAC9D,sBAAsB,EAAE,OAAO,CAAC,IAAI,CAAC,sBAAsB;QAC3D,wBAAwB,EACtB,kBAAkB,EAAE,wBAAwB;YAC5C,KAAK,CAAC,wBAAwB;QAChC,eAAe,EAAE,oBAAoB,CAAC,SAAS;QAC/C,YAAY,EAAE,mBAAmB;QACjC,+BAA+B,EAC7B,kCAAkC,CAAC,OAAO,CAAC;KAC9C,CAAC,CAAC;IACH,IAAI,CAAC,mBAAmB,EAAE;QAAE,OAAO;IACnC,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO;IACT,CAAC;IAED,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;QACzC,MAAM,wBAAwB,CAAC;YAC7B,KAAK;YACL,UAAU;YACV,YAAY;YACZ,mBAAmB;YACnB,kBAAkB,EAChB,mBAAmB,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI;SAC9D,CAAC,CAAC;IACL,CAAC;IACD,KAAK,4BAA4B,CAAC,KAAK,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["/** Hydrate only bytes matching the current document's authenticated attachment intent. */\n\nimport { getDocumentAttachments } from \"../../../data/documents/documentContent\";\nimport { hydrateDocumentAttachmentBlobs } from \"../../../workflows/blobs\";\nimport type { DocumentRecord } from \"../../../workflows/documents\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../workflows/principals/runtimePolicyWarmer\";\nimport { commitHydratedAttachment } from \"./attachmentHydrationCommit\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport type { DocumentState, DocumentStoreState } from \"./state\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent as isSyncGenerationCurrent,\n} from \"./syncGeneration\";\n\nexport async function hydrateAttachmentBlobs(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n currentRecord: DocumentRecord | null,\n expectedGeneration?: DocumentStoreSyncGeneration,\n) {\n const generationIsCurrent = () =>\n !expectedGeneration || isSyncGenerationCurrent(state, expectedGeneration);\n if (!generationIsCurrent()) return;\n\n const runtime = state.runtime;\n const encapsulationKeyPair = runtime.crypto.encapsulationKeyPair;\n if (\n !encapsulationKeyPair ||\n !runtime.auth.isAuthenticated ||\n !runtime.state.online ||\n !currentRecord?.documentId\n ) {\n return;\n }\n\n const attachments = getDocumentAttachments(currentDoc);\n if (attachments.length === 0) {\n return;\n }\n\n const expectedStorageKeys = { ...state.attachmentStorageKeyBySlotId };\n const hydratedBlobs = await hydrateDocumentAttachmentBlobs({\n apiClient: runtime.apiClient,\n attachments,\n documentId: currentRecord.documentId,\n execSql: runtime.infra.execSql,\n localBlobIdBySlotId: state.attachmentBlobIdBySlotId,\n localStorageKeyBySlotId: state.attachmentStorageKeyBySlotId,\n log: runtime.util.log,\n rejectedServedBindings: state.rejectedServedAttachmentBindings,\n reportSecurityIncident: runtime.util.reportSecurityIncident,\n resolveProjectionUserKey:\n expectedGeneration?.resolveProjectionUserKey ??\n state.resolveProjectionUserKey,\n targetSecretKey: encapsulationKeyPair.secretKey,\n stillCurrent: generationIsCurrent,\n warmReferencedPrincipalPolicies:\n createRuntimePrincipalPolicyWarmer(runtime),\n });\n if (!generationIsCurrent()) return;\n if (!hydratedBlobs) {\n return;\n }\n\n for (const hydratedBlob of hydratedBlobs) {\n await commitHydratedAttachment({\n state,\n currentDoc,\n hydratedBlob,\n generationIsCurrent,\n expectedStorageKey:\n expectedStorageKeys[hydratedBlob.attachment.slotId] ?? null,\n });\n }\n void runDocumentOrphanMaintenance(state);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"attachmentHydration.js","sourceRoot":"","sources":["attachmentHydration.ts"],"names":[],"mappings":"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","sourcesContent":["/** Hydrate only bytes matching the current document's authenticated attachment intent. */\n\nimport { getDocumentAttachments } from \"../../../data/documents/documentContent\";\nimport { hydrateDocumentAttachmentBlobs } from \"../../../workflows/blobs\";\nimport type { DocumentRecord } from \"../../../workflows/documents\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../../workflows/principals/runtimePolicyWarmer\";\nimport { commitHydratedAttachment } from \"./attachmentHydrationCommit\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport type { DocumentState, DocumentStoreState } from \"./state\";\nimport {\n type DocumentStoreSyncGeneration,\n isDocumentStoreSyncGenerationCurrent as isSyncGenerationCurrent,\n} from \"./syncGeneration\";\n\n// A remote probe is evidence that bindings may have moved independently of\n// content. Capture its sequence before I/O so a hint arriving during hydration\n// survives for the trailing pass. Missing or changed local bytes always retry.\nconst checkedSignals = new WeakMap<\n DocumentStoreState,\n { documentId: string; document: WeakRef<DocumentState>; sequence: number }\n>();\n\nexport async function hydrateAttachmentBlobs(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n currentRecord: DocumentRecord | null,\n expectedGeneration?: DocumentStoreSyncGeneration,\n) {\n const generationIsCurrent = () =>\n !expectedGeneration || isSyncGenerationCurrent(state, expectedGeneration);\n if (!generationIsCurrent()) return;\n\n const runtime = state.runtime;\n const encapsulationKeyPair = runtime.crypto.encapsulationKeyPair;\n if (\n !encapsulationKeyPair ||\n !runtime.auth.isAuthenticated ||\n !runtime.state.online ||\n !currentRecord?.documentId\n ) {\n return;\n }\n\n const attachments = getDocumentAttachments(currentDoc);\n if (attachments.length === 0) {\n return;\n }\n\n const signalSequence = state.remoteUpdateSignalSeq;\n if (\n bindingsAlreadyChecked(\n state,\n currentDoc,\n currentRecord.documentId,\n attachments,\n signalSequence,\n )\n )\n return;\n\n const expectedStorageKeys = { ...state.attachmentStorageKeyBySlotId };\n let bindingsChecked = false;\n const hydratedBlobs = await hydrateDocumentAttachmentBlobs({\n apiClient: runtime.apiClient,\n onBindingsChecked: () => {\n bindingsChecked = true;\n },\n attachments,\n documentId: currentRecord.documentId,\n execSql: runtime.infra.execSql,\n localBlobIdBySlotId: state.attachmentBlobIdBySlotId,\n localStorageKeyBySlotId: state.attachmentStorageKeyBySlotId,\n log: runtime.util.log,\n rejectedServedBindings: state.rejectedServedAttachmentBindings,\n reportSecurityIncident: runtime.util.reportSecurityIncident,\n resolveProjectionUserKey:\n expectedGeneration?.resolveProjectionUserKey ??\n state.resolveProjectionUserKey,\n targetSecretKey: encapsulationKeyPair.secretKey,\n stillCurrent: generationIsCurrent,\n warmReferencedPrincipalPolicies:\n createRuntimePrincipalPolicyWarmer(runtime),\n });\n if (!generationIsCurrent()) return;\n if (!hydratedBlobs) {\n return;\n }\n\n for (const hydratedBlob of hydratedBlobs) {\n await commitHydratedAttachment({\n state,\n currentDoc,\n hydratedBlob,\n generationIsCurrent,\n expectedStorageKey:\n expectedStorageKeys[hydratedBlob.attachment.slotId] ?? null,\n });\n }\n if (\n bindingsChecked &&\n generationIsCurrent() &&\n hydratedBlobs.every(\n (blob) =>\n state.attachmentBlobIdBySlotId[blob.attachment.slotId] ===\n blob.binding.blobId,\n )\n ) {\n checkedSignals.set(state, {\n documentId: currentRecord.documentId,\n document: new WeakRef(currentDoc),\n sequence: signalSequence,\n });\n }\n void runDocumentOrphanMaintenance(state);\n}\n\nfunction bindingsAlreadyChecked(\n state: DocumentStoreState,\n currentDoc: DocumentState,\n documentId: string,\n attachments: ReturnType<typeof getDocumentAttachments>,\n signalSequence: number,\n): boolean {\n const checked = checkedSignals.get(state);\n const needsBindingProbe =\n signalSequence >\n (checked?.documentId === documentId &&\n checked.document.deref() === currentDoc\n ? checked.sequence\n : 0);\n return (\n !needsBindingProbe &&\n attachments.every(\n (attachment) =>\n state.attachmentStorageKeyBySlotId[attachment.slotId] &&\n state.attachmentContentSha256BySlotId[attachment.slotId] ===\n attachment.contentSha256,\n )\n );\n}\n"]}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { encodeVersionVector, exportFullHistorySnapshot, } from "../../../internal/loro/index.js";
|
|
2
|
+
import { getDocumentAttachments, removeDocumentAttachment, } from "../../../data/documents/documentContent.js";
|
|
3
|
+
import { createPendingUpdateFields } from "../../../data/documents/documentSync.js";
|
|
4
|
+
import { requestDocumentStoreSync } from "../registry.js";
|
|
5
|
+
import { restoreFailedAttachmentMutation } from "./attachmentMutationPersistence.js";
|
|
6
|
+
import { assignLocalAttachmentSlots, removeLocalAttachmentSlot, } from "./attachmentPersistence.js";
|
|
7
|
+
import { ATTACHMENT_FAILURE_MESSAGE } from "./attachments.js";
|
|
8
|
+
import { captureAttachmentWriteGeneration, isAttachmentWriteGenerationCurrent, shouldAbortAttachmentPersistFollowup, } from "./attachmentWriteGeneration.js";
|
|
9
|
+
import { ensureDocumentStoreReady } from "./initialization.js";
|
|
10
|
+
import { runDocumentOrphanMaintenance } from "./orphanMaintenance.js";
|
|
11
|
+
import { advancePendingBaseVersion, pendingDeltaSinceBase, persistDocument, } from "./persistence.js";
|
|
12
|
+
import { reportDocumentStoreWriteFailure } from "./reportWriteFailure.js";
|
|
13
|
+
import { canWriteDocument, setReadySnapshot, } from "./state.js";
|
|
14
|
+
import { captureDocumentStoreSyncGeneration } from "./syncGeneration.js";
|
|
15
|
+
function installCommittedAttachmentRemoval(input) {
|
|
16
|
+
const { slotId, state, storageKey } = input;
|
|
17
|
+
if (!storageKey)
|
|
18
|
+
return;
|
|
19
|
+
const pending = state.pendingAttachments.find((attachment) => attachment.slotId === slotId && attachment.storageKey === storageKey);
|
|
20
|
+
if (input.syncedAttachment && pending?.upload) {
|
|
21
|
+
assignLocalAttachmentSlots(state, [
|
|
22
|
+
{
|
|
23
|
+
slotId,
|
|
24
|
+
storageKey,
|
|
25
|
+
blobId: pending.upload.blobId,
|
|
26
|
+
contentSha256: pending.contentSha256,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
state.pendingAttachments = state.pendingAttachments.filter((attachment) => attachment.slotId !== slotId || attachment.storageKey !== storageKey);
|
|
31
|
+
// The committed removal queued this key. A reset can leave another document
|
|
32
|
+
// uploading from the same copy; a detached marker keeps it until sync.
|
|
33
|
+
void runDocumentOrphanMaintenance(state);
|
|
34
|
+
if (input.syncedAttachment ||
|
|
35
|
+
state.attachmentStorageKeyBySlotId[slotId] !== storageKey) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
removeLocalAttachmentSlot(state, slotId);
|
|
39
|
+
setReadySnapshot(state, input.currentDoc, state.snapshot.syncing, state.snapshot.text, state.snapshot.structuredFields);
|
|
40
|
+
}
|
|
41
|
+
async function persistRemovedAttachment(state, slotId, writeGeneration) {
|
|
42
|
+
if (!isAttachmentWriteGenerationCurrent(state, writeGeneration))
|
|
43
|
+
return;
|
|
44
|
+
const currentDoc = state.doc;
|
|
45
|
+
if (!currentDoc) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const generation = captureDocumentStoreSyncGeneration(state, currentDoc);
|
|
49
|
+
if (!generation) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const existingAttachment = getDocumentAttachments(currentDoc).find((attachment) => attachment.slotId === slotId);
|
|
53
|
+
if (!existingAttachment) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// After a reset, a store opened cold holds the slot only as a pending upload.
|
|
57
|
+
const storageKey = state.attachmentStorageKeyBySlotId[slotId] ??
|
|
58
|
+
state.pendingAttachments.find((attachment) => attachment.slotId === slotId)
|
|
59
|
+
?.storageKey;
|
|
60
|
+
if (!isAttachmentWriteGenerationCurrent(state, writeGeneration) ||
|
|
61
|
+
state.doc !== currentDoc)
|
|
62
|
+
return;
|
|
63
|
+
const rollbackSnapshot = exportFullHistorySnapshot(currentDoc).slice();
|
|
64
|
+
removeDocumentAttachment(currentDoc, slotId);
|
|
65
|
+
const attachmentUpdate = pendingDeltaSinceBase(state, currentDoc);
|
|
66
|
+
const coveredVersion = encodeVersionVector(currentDoc);
|
|
67
|
+
const pendingUpdate = createPendingUpdateFields(attachmentUpdate);
|
|
68
|
+
const syncedAttachment = Boolean(state.record?.documentId);
|
|
69
|
+
// Preserve the optimistic text/structured fields (see persistAttachedFiles):
|
|
70
|
+
// removing an attachment while typing must not regress the editor.
|
|
71
|
+
let persisted;
|
|
72
|
+
try {
|
|
73
|
+
persisted = await persistDocument(state, currentDoc, { snapshotEndVersion: coveredVersion }, {
|
|
74
|
+
...(storageKey
|
|
75
|
+
? {
|
|
76
|
+
attachmentRemoval: {
|
|
77
|
+
mode: syncedAttachment
|
|
78
|
+
? "detach"
|
|
79
|
+
: "delete",
|
|
80
|
+
slotId,
|
|
81
|
+
storageKey,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
85
|
+
...(pendingUpdate ? { pendingUpdate } : {}),
|
|
86
|
+
preserveSnapshotStructuredFields: true,
|
|
87
|
+
preserveSnapshotText: true,
|
|
88
|
+
}, generation);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await restoreFailedAttachmentMutation({
|
|
92
|
+
generation,
|
|
93
|
+
rollbackSnapshot,
|
|
94
|
+
state,
|
|
95
|
+
});
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (shouldAbortAttachmentPersistFollowup(state, writeGeneration, generation, persisted)) {
|
|
99
|
+
// See persistAttachedFiles: only durable same-identity work re-arms sync.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
installCommittedAttachmentRemoval({
|
|
103
|
+
currentDoc,
|
|
104
|
+
slotId,
|
|
105
|
+
state,
|
|
106
|
+
storageKey,
|
|
107
|
+
syncedAttachment,
|
|
108
|
+
});
|
|
109
|
+
advancePendingBaseVersion(state, currentDoc);
|
|
110
|
+
state.runtime.util.log(`Removed attachment ${existingAttachment.name} from document ${state.localId}.`);
|
|
111
|
+
requestDocumentStoreSync(state);
|
|
112
|
+
}
|
|
113
|
+
export async function removeAttachmentFromDocumentStore(state, scheduleSync, slotId) {
|
|
114
|
+
let ready;
|
|
115
|
+
try {
|
|
116
|
+
ready = await ensureDocumentStoreReady(state, scheduleSync);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
reportDocumentStoreWriteFailure(state, ATTACHMENT_FAILURE_MESSAGE, error);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!ready || !state.doc || !canWriteDocument(state)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const writeGeneration = captureAttachmentWriteGeneration(state);
|
|
126
|
+
if (!writeGeneration)
|
|
127
|
+
return;
|
|
128
|
+
state.writeChain = state.writeChain
|
|
129
|
+
.catch(() => undefined)
|
|
130
|
+
.then(async () => persistRemovedAttachment(state, slotId, writeGeneration))
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
reportDocumentStoreWriteFailure(state, ATTACHMENT_FAILURE_MESSAGE, error);
|
|
133
|
+
});
|
|
134
|
+
return state.writeChain;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=attachmentRemoval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachmentRemoval.js","sourceRoot":"","sources":["attachmentRemoval.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n encodeVersionVector,\n exportFullHistorySnapshot,\n} from \"@tearleads/loro\";\nimport {\n getDocumentAttachments,\n removeDocumentAttachment,\n} from \"../../../data/documents/documentContent\";\nimport { createPendingUpdateFields } from \"../../../data/documents/documentSync\";\nimport { requestDocumentStoreSync } from \"../registry\";\nimport { restoreFailedAttachmentMutation } from \"./attachmentMutationPersistence\";\nimport {\n assignLocalAttachmentSlots,\n removeLocalAttachmentSlot,\n} from \"./attachmentPersistence\";\nimport { ATTACHMENT_FAILURE_MESSAGE } from \"./attachments\";\nimport {\n type AttachmentWriteGeneration,\n captureAttachmentWriteGeneration,\n isAttachmentWriteGenerationCurrent,\n shouldAbortAttachmentPersistFollowup,\n} from \"./attachmentWriteGeneration\";\nimport { ensureDocumentStoreReady } from \"./initialization\";\nimport { runDocumentOrphanMaintenance } from \"./orphanMaintenance\";\nimport {\n advancePendingBaseVersion,\n pendingDeltaSinceBase,\n persistDocument,\n} from \"./persistence\";\nimport { reportDocumentStoreWriteFailure } from \"./reportWriteFailure\";\nimport {\n canWriteDocument,\n type DocumentStoreState,\n setReadySnapshot,\n} from \"./state\";\nimport { captureDocumentStoreSyncGeneration } from \"./syncGeneration\";\n\nfunction installCommittedAttachmentRemoval(input: {\n currentDoc: NonNullable<DocumentStoreState[\"doc\"]>;\n slotId: string;\n state: DocumentStoreState;\n storageKey: string | undefined;\n syncedAttachment: boolean;\n}): void {\n const { slotId, state, storageKey } = input;\n if (!storageKey) return;\n const pending = state.pendingAttachments.find(\n (attachment) =>\n attachment.slotId === slotId && attachment.storageKey === storageKey,\n );\n if (input.syncedAttachment && pending?.upload) {\n assignLocalAttachmentSlots(state, [\n {\n slotId,\n storageKey,\n blobId: pending.upload.blobId,\n contentSha256: pending.contentSha256,\n },\n ]);\n }\n state.pendingAttachments = state.pendingAttachments.filter(\n (attachment) =>\n attachment.slotId !== slotId || attachment.storageKey !== storageKey,\n );\n // The committed removal queued this key. A reset can leave another document\n // uploading from the same copy; a detached marker keeps it until sync.\n void runDocumentOrphanMaintenance(state);\n if (\n input.syncedAttachment ||\n state.attachmentStorageKeyBySlotId[slotId] !== storageKey\n ) {\n return;\n }\n removeLocalAttachmentSlot(state, slotId);\n setReadySnapshot(\n state,\n input.currentDoc,\n state.snapshot.syncing,\n state.snapshot.text,\n state.snapshot.structuredFields,\n );\n}\n\nasync function persistRemovedAttachment(\n state: DocumentStoreState,\n slotId: string,\n writeGeneration: AttachmentWriteGeneration,\n) {\n if (!isAttachmentWriteGenerationCurrent(state, writeGeneration)) return;\n const currentDoc = state.doc;\n if (!currentDoc) {\n return;\n }\n const generation = captureDocumentStoreSyncGeneration(state, currentDoc);\n if (!generation) {\n return;\n }\n\n const existingAttachment = getDocumentAttachments(currentDoc).find(\n (attachment) => attachment.slotId === slotId,\n );\n if (!existingAttachment) {\n return;\n }\n\n // After a reset, a store opened cold holds the slot only as a pending upload.\n const storageKey =\n state.attachmentStorageKeyBySlotId[slotId] ??\n state.pendingAttachments.find((attachment) => attachment.slotId === slotId)\n ?.storageKey;\n if (\n !isAttachmentWriteGenerationCurrent(state, writeGeneration) ||\n state.doc !== currentDoc\n )\n return;\n const rollbackSnapshot = exportFullHistorySnapshot(currentDoc).slice();\n removeDocumentAttachment(currentDoc, slotId);\n const attachmentUpdate = pendingDeltaSinceBase(state, currentDoc);\n const coveredVersion = encodeVersionVector(currentDoc);\n const pendingUpdate = createPendingUpdateFields(attachmentUpdate);\n const syncedAttachment = Boolean(state.record?.documentId);\n\n // Preserve the optimistic text/structured fields (see persistAttachedFiles):\n // removing an attachment while typing must not regress the editor.\n let persisted: Awaited<ReturnType<typeof persistDocument>>;\n try {\n persisted = await persistDocument(\n state,\n currentDoc,\n { snapshotEndVersion: coveredVersion },\n {\n ...(storageKey\n ? {\n attachmentRemoval: {\n mode: syncedAttachment\n ? (\"detach\" as const)\n : (\"delete\" as const),\n slotId,\n storageKey,\n },\n }\n : {}),\n ...(pendingUpdate ? { pendingUpdate } : {}),\n preserveSnapshotStructuredFields: true,\n preserveSnapshotText: true,\n },\n generation,\n );\n } catch (error) {\n await restoreFailedAttachmentMutation({\n generation,\n rollbackSnapshot,\n state,\n });\n throw error;\n }\n if (\n shouldAbortAttachmentPersistFollowup(\n state,\n writeGeneration,\n generation,\n persisted,\n )\n ) {\n // See persistAttachedFiles: only durable same-identity work re-arms sync.\n return;\n }\n installCommittedAttachmentRemoval({\n currentDoc,\n slotId,\n state,\n storageKey,\n syncedAttachment,\n });\n advancePendingBaseVersion(state, currentDoc);\n state.runtime.util.log(\n `Removed attachment ${existingAttachment.name} from document ${state.localId}.`,\n );\n requestDocumentStoreSync(state);\n}\n\nexport async function removeAttachmentFromDocumentStore(\n state: DocumentStoreState,\n scheduleSync: () => void,\n slotId: string,\n) {\n let ready: boolean;\n try {\n ready = await ensureDocumentStoreReady(state, scheduleSync);\n } catch (error) {\n reportDocumentStoreWriteFailure(state, ATTACHMENT_FAILURE_MESSAGE, error);\n return;\n }\n\n if (!ready || !state.doc || !canWriteDocument(state)) {\n return;\n }\n\n const writeGeneration = captureAttachmentWriteGeneration(state);\n if (!writeGeneration) return;\n state.writeChain = state.writeChain\n .catch(() => undefined)\n .then(async () => persistRemovedAttachment(state, slotId, writeGeneration))\n .catch((error: unknown) => {\n reportDocumentStoreWriteFailure(state, ATTACHMENT_FAILURE_MESSAGE, error);\n });\n return state.writeChain;\n}\n"]}
|