tempest-react-sdk 0.38.2 → 0.38.3
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/bin/lib/design/collect.mjs +19 -0
- package/bin/lib/design/functions.mjs +5 -0
- package/bin/lib/design/index.mjs +8 -3
- package/bin/lib/design/index.test.mjs +31 -0
- package/bin/lib/design/scan.mjs +34 -2
- package/bin/lib/design/scan.test.mjs +78 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
- package/bin/tempest.mjs +90 -6
- package/dist/audio/level-meter.cjs.map +1 -1
- package/dist/audio/level-meter.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio-recorder.js.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/auth/use-passkey.js.map +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +8 -0
- package/dist/capture/media-recorder.cjs.map +1 -1
- package/dist/capture/media-recorder.js.map +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-screen-capture.js.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.js.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/capture/use-video-recorder.js.map +1 -1
- package/dist/charts/types.cjs.map +1 -1
- package/dist/charts/types.js.map +1 -1
- package/dist/charts.d.ts +8 -0
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/qr-encode.cjs.map +1 -1
- package/dist/components/QRCode/qr-encode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/FormField.js.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/estimate.cjs.map +1 -1
- package/dist/geo/estimate.js.map +1 -1
- package/dist/geo/projection.cjs.map +1 -1
- package/dist/geo/projection.js.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-event-listener.js.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-local-storage.js.map +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/i18n/I18nProvider.js.map +1 -1
- package/dist/imaging/canvas.cjs.map +1 -1
- package/dist/imaging/canvas.js.map +1 -1
- package/dist/imaging.d.ts +5 -0
- package/dist/logger/logger.cjs.map +1 -1
- package/dist/logger/logger.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/perf/cache-size.cjs +1 -1
- package/dist/perf/cache-size.cjs.map +1 -1
- package/dist/perf/cache-size.js +27 -6
- package/dist/perf/cache-size.js.map +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw.d.ts +12 -0
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/predictor.cjs.map +1 -1
- package/dist/tabular/predictor.js.map +1 -1
- package/dist/tabular.d.ts +7 -2
- package/dist/tempest-react-sdk.d.ts +133 -6
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/data-viz-ramps.cjs +1 -1
- package/dist/theme/data-viz-ramps.cjs.map +1 -1
- package/dist/theme/data-viz-ramps.js +1 -1
- package/dist/theme/data-viz-ramps.js.map +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js.map +1 -1
- package/dist/vision/core/canvas.cjs.map +1 -1
- package/dist/vision/core/canvas.js.map +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/graph.cjs.map +1 -1
- package/dist/vision/core/graph.js.map +1 -1
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js.map +1 -1
- package/dist/vision/core/providers.cjs.map +1 -1
- package/dist/vision/core/providers.js.map +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/core/timing.cjs.map +1 -1
- package/dist/vision/core/timing.js.map +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/labels.cjs.map +1 -1
- package/dist/vision/labels.js.map +1 -1
- package/dist/vision/luminance.cjs.map +1 -1
- package/dist/vision/luminance.js.map +1 -1
- package/dist/vision/postprocess/classification.cjs.map +1 -1
- package/dist/vision/postprocess/classification.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/types.cjs.map +1 -1
- package/dist/vision/types.js.map +1 -1
- package/dist/vision.d.ts +6 -0
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-icons.js.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.js.map +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumable-upload.js","names":[],"sources":["../../src/http/resumable-upload.ts"],"sourcesContent":["import { buildApiError, TempestApiError } from \"./errors\";\nimport { generateIdempotencyKey } from \"./idempotency\";\nimport { retry, type RetryOptions } from \"./retry\";\n\n/** The tus protocol version this client speaks. */\nexport const TUS_VERSION = \"1.0.0\";\n\n/** Default chunk size: 5 MiB, the size most tus servers are tuned for. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;\n\n/**\n * Where a resumable upload is.\n *\n * `\"paused\"` and `\"aborted\"` are both \"not running\", but only `\"paused\"` keeps the\n * persisted offset — `abort({ discard: true })` throws it away.\n */\nexport type ResumableUploadState =\n \"idle\" | \"creating\" | \"uploading\" | \"paused\" | \"done\" | \"error\" | \"aborted\";\n\n/** Byte-level progress for a resumable upload. */\nexport interface ResumableUploadProgress {\n /** Bytes the server holds, including anything a resume skipped. */\n loaded: number;\n /** Total size of the file. */\n total: number;\n /** `loaded / total`, between 0 and 1. */\n fraction: number;\n /** Bytes already on the server when this run started. `0` on a fresh upload. */\n resumedFrom: number;\n}\n\n/** What has to survive a page reload for a resume to be possible. */\nexport interface ResumableUploadRecord {\n /** Upload URL the creation POST returned, absolute. */\n url: string;\n /** Last offset the server confirmed. */\n offset: number;\n /** File size, so a different file under the same key is not resumed into. */\n size: number;\n /** Idempotency key of the creation request, reused if creation is retried. */\n idempotencyKey: string;\n /** Epoch ms of the last write, so an app can sweep stale records. */\n updatedAt: number;\n}\n\n/**\n * Persistence for resume state. Sync or async — both are awaited.\n *\n * Implement it over anything: the default is `localStorage`, and\n * `createOfflineStore` from `@/offline` slots in when you already have a Dexie\n * database open.\n */\nexport interface ResumableUploadStorage {\n /** Read the record for `key`, or `null`. */\n get(key: string): Promise<ResumableUploadRecord | null> | ResumableUploadRecord | null;\n /** Write the record for `key`. */\n set(key: string, record: ResumableUploadRecord): Promise<void> | void;\n /** Forget the record for `key`. */\n delete(key: string): Promise<void> | void;\n}\n\n/** Options for {@link createResumableUpload}. */\nexport interface ResumableUploadOptions {\n /** tus creation endpoint, e.g. `\"/api/uploads\"`. */\n endpoint: string;\n /** The bytes to upload. A `File` also supplies the default resume key. */\n file: Blob | File;\n /** Bytes per `PATCH`. Default {@link DEFAULT_CHUNK_SIZE}. */\n chunkSize?: number;\n /** Sent as `Upload-Metadata` (base64-encoded values), e.g. `{ filename }`. */\n metadata?: Record<string, string>;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n /** Returns the current bearer token, read before each request. */\n getToken?: () => string | null | undefined;\n /** Send cookies. Default `false`. */\n withCredentials?: boolean;\n /**\n * Resume key. Defaults to a fingerprint of endpoint + file name/size/mtime, so\n * picking the same file after a reload resumes instead of restarting.\n */\n key?: string;\n /**\n * Where to persist resume state. Defaults to `localStorage`. Pass `null` to\n * disable persistence — resume then only survives a network blip, not a reload.\n */\n storage?: ResumableUploadStorage | null;\n /** Backoff for a failed chunk. Forwarded to `retry`. Default 5 attempts. */\n retry?: RetryOptions;\n /** Called on every upload-progress tick and after every confirmed chunk. */\n onProgress?: (progress: ResumableUploadProgress) => void;\n /** Called whenever {@link ResumableUpload.state} changes. */\n onStateChange?: (state: ResumableUploadState) => void;\n}\n\n/** What a finished upload resolves with. */\nexport interface ResumableUploadResult {\n /** The tus upload URL — hand this to your API to link the stored file. */\n url: string;\n /** Total bytes uploaded. */\n size: number;\n}\n\n/** A resumable upload in progress. Build one with {@link createResumableUpload}. */\nexport interface ResumableUpload {\n /**\n * Create (or re-attach to) the upload and push chunks until it is complete.\n *\n * Resolves `null` when the run stopped because of `pause()` or `abort()` —\n * neither is a failure. Rejects with a `TempestApiError` when the server\n * refused and the retries ran out.\n */\n start(): Promise<ResumableUploadResult | null>;\n /** Stop after the in-flight chunk is dropped, keeping the resume point. */\n pause(): void;\n /** Continue from the server's offset. Same resolution contract as `start`. */\n resume(): Promise<ResumableUploadResult | null>;\n /**\n * Stop for good.\n *\n * @param options - `discard: true` also sends `DELETE` (tus termination) and\n * forgets the persisted record, so the next `start()` uploads from zero.\n */\n abort(options?: { discard?: boolean }): Promise<void>;\n /** Current state. */\n readonly state: ResumableUploadState;\n /** Bytes the server has confirmed. */\n readonly offset: number;\n /** The upload URL, once creation succeeded. */\n readonly url: string | null;\n /** The resume key in use. */\n readonly key: string;\n}\n\ninterface RawResponse {\n status: number;\n text: string;\n header(name: string): string | null;\n}\n\n/**\n * Encode a string as standard base64 (padded), UTF-8 first.\n *\n * `Upload-Metadata` carries base64 values precisely so a filename with accents\n * survives an HTTP header, so the UTF-8 step is not optional: `btoa` alone throws\n * on any code point above U+00FF.\n *\n * @param value - Text to encode.\n * @returns Padded base64.\n */\nfunction base64Utf8(value: string): string {\n const bytes = new TextEncoder().encode(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/**\n * Build the `Upload-Metadata` header value: comma-separated `key base64(value)`.\n *\n * @param metadata - Plain string map.\n * @returns The header value, or `null` when there is nothing to send.\n */\nfunction encodeMetadata(metadata: Record<string, string> | undefined): string | null {\n if (!metadata) return null;\n const parts = Object.entries(metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`);\n return parts.length > 0 ? parts.join(\",\") : null;\n}\n\n/**\n * A stable-enough identity for a file, used as the default resume key.\n *\n * Name + size + last-modified is what the tus reference clients fingerprint on:\n * it is cheap (hashing the bytes of a 400 MB recording is not) and it changes\n * whenever the file does, which is the property that matters — resuming into the\n * wrong file would corrupt it silently.\n *\n * @param endpoint - Creation endpoint, so the same file to two servers is two uploads.\n * @param file - The blob or file being uploaded.\n * @returns A key safe to use in `localStorage`.\n */\nexport function uploadFingerprint(endpoint: string, file: Blob | File): string {\n const named = file as File;\n const name = typeof named.name === \"string\" ? named.name : \"blob\";\n const modified = typeof named.lastModified === \"number\" ? named.lastModified : 0;\n return `${endpoint}|${name}|${file.size}|${file.type}|${modified}`;\n}\n\n/**\n * `localStorage`-backed resume storage — the default.\n *\n * `localStorage` and not IndexedDB on purpose. The record is four fields and a\n * URL; the requirement is only that it survives a reload, and pulling Dexie in for\n * that would put an IndexedDB dependency in the bundle of every app that uploads a\n * file. Apps that already have `createOfflineStore` open can pass their own\n * {@link ResumableUploadStorage} instead.\n *\n * @param prefix - Key prefix. Default `\"tempest-upload:\"`.\n * @returns A storage that no-ops when `localStorage` is unavailable.\n */\nexport function createLocalUploadStorage(prefix = \"tempest-upload:\"): ResumableUploadStorage {\n function backend(): Storage | null {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n }\n\n return {\n get(key) {\n const raw = backend()?.getItem(prefix + key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as ResumableUploadRecord;\n } catch {\n return null;\n }\n },\n set(key, record) {\n backend()?.setItem(prefix + key, JSON.stringify(record));\n },\n delete(key) {\n backend()?.removeItem(prefix + key);\n },\n };\n}\n\n/**\n * Send one request over `XMLHttpRequest`.\n *\n * `XMLHttpRequest` rather than `fetch` for the same reason `uploadWithProgress`\n * uses it — `fetch` still cannot report upload progress in any browser — plus one\n * more: tus answers every write with the new `Upload-Offset` in a **response\n * header**, and `uploadWithProgress` only hands back a parsed body, so it could\n * not be reused here.\n *\n * @param init - Method, URL, headers, optional body and progress callback.\n * @returns Status, raw text and a header reader.\n */\nfunction sendRequest(init: {\n method: \"POST\" | \"HEAD\" | \"PATCH\" | \"DELETE\";\n url: string;\n headers: Record<string, string>;\n body?: Blob;\n withCredentials: boolean;\n onProgress?: (loaded: number) => void;\n register: (xhr: XMLHttpRequest) => void;\n}): Promise<RawResponse> {\n return new Promise<RawResponse>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(init.method, init.url);\n xhr.withCredentials = init.withCredentials;\n for (const [name, value] of Object.entries(init.headers)) {\n xhr.setRequestHeader(name, value);\n }\n if (init.onProgress) {\n const report = init.onProgress;\n xhr.upload.onprogress = (event: ProgressEvent) => report(event.loaded);\n }\n xhr.onload = () =>\n resolve({\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n });\n xhr.onerror = () =>\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload resumível.\",\n }),\n );\n xhr.onabort = () => reject(new DOMException(\"Aborted\", \"AbortError\"));\n init.register(xhr);\n xhr.send(init.body);\n });\n}\n\nfunction parseOffset(response: RawResponse): number | null {\n const raw = response.header(\"Upload-Offset\");\n if (raw === null) return null;\n const value = Number(raw);\n return Number.isFinite(value) && value >= 0 ? value : null;\n}\n\n/**\n * Read an error body without assuming it is JSON.\n *\n * A tus proxy that rejects a chunk often answers with plain text or an HTML error\n * page, and `JSON.parse` throwing there would replace a useful status with a parse\n * error.\n *\n * @param text - Raw response text.\n * @returns The parsed object, the raw text, or `null` when the body was empty.\n */\nfunction parseErrorBody(text: string): unknown {\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/**\n * Turn a refused tus response into a `TempestApiError`.\n *\n * The fallback `detail` is used unless the server sent a real error envelope,\n * because `buildApiError`'s own fallback (`\"Erro 409\"`) says nothing about which\n * step of the protocol broke — and that is the whole diagnostic value here.\n *\n * @param response - The raw response that was not acceptable.\n * @param detail - Message to use when the body carries none.\n * @returns The error to throw.\n */\nfunction failed(response: RawResponse, detail: string): TempestApiError {\n const body = parseErrorBody(response.text);\n const envelope = buildApiError(response.status, body, { get: response.header });\n const hasDetail =\n typeof body === \"object\" && body !== null && (\"detail\" in body || \"message\" in body);\n return new TempestApiError({ ...envelope, detail: hasDetail ? envelope.detail : detail });\n}\n\n/**\n * Resolve a `Location` header against the page, so a relative upload URL works.\n *\n * tus servers are free to answer creation with either an absolute URL or a\n * path, and the spec does not prefer one — a client that only handles absolute\n * URLs breaks against half the implementations.\n *\n * @param value - The raw `Location` header.\n * @returns An absolute URL, or the input when there is no base to resolve against.\n */\nfunction resolveUploadUrl(value: string): string {\n const base = typeof window === \"undefined\" ? undefined : window.location.href;\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Chunked, resumable upload speaking the **tus 1.0.0** protocol (core plus the\n * *creation* and *termination* extensions).\n *\n * ## Why tus and not a bespoke scheme\n *\n * A resumable client whose wire format is undocumented cannot be integrated, and\n * inventing one means the backend is ours forever. tus is a published spec with\n * off-the-shelf servers (`tusd`, `tuspy`, `tus-node-server`), so a caller can point\n * this at something they did not write.\n *\n * ## What the backend must implement\n *\n * Every request carries `Tus-Resumable: 1.0.0`.\n *\n * | Step | Request | Expected response |\n * | --- | --- | --- |\n * | Create | `POST {endpoint}` + `Upload-Length`, `Upload-Metadata`, `Idempotency-Key` | `201` + `Location` (the upload URL, absolute or endpoint-relative) |\n * | Probe | `HEAD {uploadUrl}` | `200`/`204` + `Upload-Offset` |\n * | Write | `PATCH {uploadUrl}` + `Upload-Offset`, `Content-Type: application/offset+octet-stream`, chunk body | `204` + the new `Upload-Offset`; `409` when the offset does not match |\n * | Discard | `DELETE {uploadUrl}` | `204` |\n *\n * ## The failure that actually happens\n *\n * A chunk that the server stored but whose response never arrived. The client\n * cannot tell that from a chunk that was lost, and re-sending it blindly would\n * duplicate bytes. Two things prevent that:\n *\n * - **Writes are addressed, not appended.** Every `PATCH` states the offset it\n * writes at, so a retry after a lost response is asked to write bytes the server\n * already has and answers `409`. On any retry the client re-reads the truth with\n * `HEAD` first and continues from there.\n * - **Creation carries an `Idempotency-Key`** (from `generateIdempotencyKey`),\n * persisted before the first attempt and reused on retry. tus has no idempotent\n * creation of its own, so without this a lost `201` leaves an orphan upload on\n * the server. A backend that honours the header returns the same `Location`; one\n * that ignores it still works, it just keeps the orphan.\n *\n * @param options - Endpoint, file, and the knobs above.\n * @returns A handle with `start`/`pause`/`resume`/`abort` and live `state`/`offset`.\n *\n * @example\n * const upload = createResumableUpload({\n * endpoint: \"/api/uploads\",\n * file: recording,\n * metadata: { filename: \"nota.webm\", ticket: ticketId },\n * getToken: () => auth.getToken(),\n * onProgress: ({ fraction }) => setPercent(Math.round(fraction * 100)),\n * });\n *\n * const done = await upload.start();\n * if (done) await api.post(\"/api/tickets/1/audio\", { body: { url: done.url } });\n */\nexport function createResumableUpload(options: ResumableUploadOptions): ResumableUpload {\n const {\n endpoint,\n file,\n chunkSize = DEFAULT_CHUNK_SIZE,\n metadata,\n headers = {},\n getToken,\n withCredentials = false,\n key = uploadFingerprint(endpoint, file),\n storage = createLocalUploadStorage(),\n retry: retryOptions,\n onProgress,\n onStateChange,\n } = options;\n\n let state: ResumableUploadState = \"idle\";\n let offset = 0;\n let url: string | null = null;\n let idempotencyKey: string | null = null;\n let stopping: \"pause\" | \"abort\" | null = null;\n let inFlight: XMLHttpRequest | null = null;\n let resumedFrom = 0;\n\n function setState(next: ResumableUploadState): void {\n if (state === next) return;\n state = next;\n onStateChange?.(next);\n }\n\n function report(loaded: number): void {\n onProgress?.({\n loaded,\n total: file.size,\n fraction: file.size === 0 ? 1 : loaded / file.size,\n resumedFrom,\n });\n }\n\n function baseHeaders(): Record<string, string> {\n const result: Record<string, string> = { ...headers, \"Tus-Resumable\": TUS_VERSION };\n const token = getToken?.();\n if (token && !(\"Authorization\" in result)) result.Authorization = `Bearer ${token}`;\n return result;\n }\n\n function register(xhr: XMLHttpRequest): void {\n inFlight = xhr;\n }\n\n async function persist(): Promise<void> {\n if (!storage || !url || !idempotencyKey) return;\n await storage.set(key, {\n url,\n offset,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n /**\n * Ask the server how much it holds. The only source of truth after any failure.\n */\n async function probe(target: string): Promise<number> {\n const response = await sendRequest({\n method: \"HEAD\",\n url: target,\n headers: baseHeaders(),\n withCredentials,\n register,\n });\n if (response.status === 404 || response.status === 410) {\n throw new TempestApiError({\n status: response.status,\n detail: \"O upload expirou no servidor. Comece de novo.\",\n });\n }\n const confirmed = parseOffset(response);\n if (confirmed === null) throw failed(response, \"HEAD sem Upload-Offset.\");\n return confirmed;\n }\n\n /**\n * Re-attach to a persisted upload, or create a new one.\n *\n * The persisted record is only trusted when the file size still matches, and the\n * offset it holds is re-checked with `HEAD` — the client's copy can be ahead of\n * the server's whenever the last response was lost.\n */\n async function ensureUpload(): Promise<string> {\n const stored = storage ? await storage.get(key) : null;\n if (stored && stored.size === file.size) {\n idempotencyKey = stored.idempotencyKey;\n if (stored.url) {\n try {\n offset = await probe(stored.url);\n url = stored.url;\n return stored.url;\n } catch {\n offset = 0;\n }\n }\n }\n\n setState(\"creating\");\n idempotencyKey ??= generateIdempotencyKey();\n url = null;\n offset = 0;\n if (storage) {\n await storage.set(key, {\n url: \"\",\n offset: 0,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n const creationHeaders: Record<string, string> = {\n ...baseHeaders(),\n \"Upload-Length\": String(file.size),\n \"Idempotency-Key\": idempotencyKey,\n };\n const encoded = encodeMetadata(metadata);\n if (encoded) creationHeaders[\"Upload-Metadata\"] = encoded;\n\n const response = await sendRequest({\n method: \"POST\",\n url: endpoint,\n headers: creationHeaders,\n withCredentials,\n register,\n });\n if (response.status !== 201) throw failed(response, \"Criação do upload recusada.\");\n const locationHeader = response.header(\"Location\");\n if (!locationHeader) throw failed(response, \"Criação do upload sem cabeçalho Location.\");\n\n url = resolveUploadUrl(locationHeader);\n await persist();\n return url;\n }\n\n /** Push one chunk, resyncing the offset first when a previous attempt failed. */\n async function writeChunk(target: string, resync: { needed: boolean }): Promise<void> {\n if (resync.needed) {\n offset = await probe(target);\n resync.needed = false;\n report(offset);\n await persist();\n if (offset >= file.size) return;\n }\n\n const end = Math.min(offset + chunkSize, file.size);\n const from = offset;\n const response = await sendRequest({\n method: \"PATCH\",\n url: target,\n headers: {\n ...baseHeaders(),\n \"Content-Type\": \"application/offset+octet-stream\",\n \"Upload-Offset\": String(from),\n },\n body: file.slice(from, end),\n withCredentials,\n onProgress: (loaded) => report(Math.min(from + loaded, file.size)),\n register,\n });\n\n if (response.status === 409 || response.status === 412) {\n resync.needed = true;\n throw failed(response, \"Offset divergente — o servidor já tinha esses bytes.\");\n }\n if (response.status !== 204 && response.status !== 200) {\n throw failed(response, \"Chunk recusado pelo servidor.\");\n }\n\n offset = parseOffset(response) ?? end;\n report(offset);\n await persist();\n }\n\n /**\n * Drive the whole upload: attach or create, then chunk until complete.\n *\n * The `shouldRetry` predicate does double duty — besides deciding, it arms\n * `resync` so the next attempt re-reads the server's offset with `HEAD` before\n * writing. That is deliberate: it is the one place that sees *every* chunk\n * failure, whatever the cause, and after any failure the client's idea of the\n * offset is exactly what cannot be trusted.\n *\n * @returns The result, or `null` when `pause`/`abort` stopped the run.\n */\n async function run(): Promise<ResumableUploadResult | null> {\n stopping = null;\n const target = await ensureUpload();\n resumedFrom = offset;\n setState(\"uploading\");\n report(offset);\n\n const resync = { needed: false };\n while (offset < file.size) {\n if (stopping) break;\n await retry(() => writeChunk(target, resync), {\n retries: 5,\n ...retryOptions,\n shouldRetry: (error, attempt) => {\n if (stopping) return false;\n if (error instanceof DOMException && error.name === \"AbortError\") return false;\n resync.needed = true;\n return retryOptions?.shouldRetry?.(error, attempt) ?? true;\n },\n });\n }\n\n if (stopping === \"pause\") {\n setState(\"paused\");\n return null;\n }\n if (stopping === \"abort\") {\n setState(\"aborted\");\n return null;\n }\n\n setState(\"done\");\n if (storage) await storage.delete(key);\n return { url: target, size: file.size };\n }\n\n async function guarded(): Promise<ResumableUploadResult | null> {\n try {\n return await run();\n } catch (error) {\n if (\n stopping !== null ||\n (error instanceof DOMException && error.name === \"AbortError\")\n ) {\n setState(stopping === \"abort\" ? \"aborted\" : \"paused\");\n return null;\n }\n setState(\"error\");\n throw error;\n } finally {\n inFlight = null;\n }\n }\n\n function stop(reason: \"pause\" | \"abort\"): void {\n stopping = reason;\n inFlight?.abort();\n inFlight = null;\n }\n\n return {\n start: guarded,\n resume: guarded,\n pause: () => stop(\"pause\"),\n abort: async ({ discard = false } = {}) => {\n stop(\"abort\");\n setState(\"aborted\");\n if (!discard) return;\n if (url) {\n await sendRequest({\n method: \"DELETE\",\n url,\n headers: baseHeaders(),\n withCredentials,\n register: () => undefined,\n }).catch(() => undefined);\n }\n if (storage) await storage.delete(key);\n },\n get state() {\n return state;\n },\n get offset() {\n return offset;\n },\n get url() {\n return url;\n },\n key,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resumable-upload.js","names":[],"sources":["../../src/http/resumable-upload.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a resumable upload is one long-lived\n * state machine: chunk the file, negotiate the offset the server already has, upload\n * with retry and backoff, honour pause, resume and abort, and report progress\n * throughout. Every stage reads the same cursor and the same abort signal, and\n * createResumableUpload is the closure that owns them.\n */\nimport { buildApiError, TempestApiError } from \"./errors\";\nimport { generateIdempotencyKey } from \"./idempotency\";\nimport { retry, type RetryOptions } from \"./retry\";\n\n/** The tus protocol version this client speaks. */\nexport const TUS_VERSION = \"1.0.0\";\n\n/** Default chunk size: 5 MiB, the size most tus servers are tuned for. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;\n\n/**\n * Where a resumable upload is.\n *\n * `\"paused\"` and `\"aborted\"` are both \"not running\", but only `\"paused\"` keeps the\n * persisted offset — `abort({ discard: true })` throws it away.\n */\nexport type ResumableUploadState =\n \"idle\" | \"creating\" | \"uploading\" | \"paused\" | \"done\" | \"error\" | \"aborted\";\n\n/** Byte-level progress for a resumable upload. */\nexport interface ResumableUploadProgress {\n /** Bytes the server holds, including anything a resume skipped. */\n loaded: number;\n /** Total size of the file. */\n total: number;\n /** `loaded / total`, between 0 and 1. */\n fraction: number;\n /** Bytes already on the server when this run started. `0` on a fresh upload. */\n resumedFrom: number;\n}\n\n/** What has to survive a page reload for a resume to be possible. */\nexport interface ResumableUploadRecord {\n /** Upload URL the creation POST returned, absolute. */\n url: string;\n /** Last offset the server confirmed. */\n offset: number;\n /** File size, so a different file under the same key is not resumed into. */\n size: number;\n /** Idempotency key of the creation request, reused if creation is retried. */\n idempotencyKey: string;\n /** Epoch ms of the last write, so an app can sweep stale records. */\n updatedAt: number;\n}\n\n/**\n * Persistence for resume state. Sync or async — both are awaited.\n *\n * Implement it over anything: the default is `localStorage`, and\n * `createOfflineStore` from `@/offline` slots in when you already have a Dexie\n * database open.\n */\nexport interface ResumableUploadStorage {\n /** Read the record for `key`, or `null`. */\n get(key: string): Promise<ResumableUploadRecord | null> | ResumableUploadRecord | null;\n /** Write the record for `key`. */\n set(key: string, record: ResumableUploadRecord): Promise<void> | void;\n /** Forget the record for `key`. */\n delete(key: string): Promise<void> | void;\n}\n\n/** Options for {@link createResumableUpload}. */\nexport interface ResumableUploadOptions {\n /** tus creation endpoint, e.g. `\"/api/uploads\"`. */\n endpoint: string;\n /** The bytes to upload. A `File` also supplies the default resume key. */\n file: Blob | File;\n /** Bytes per `PATCH`. Default {@link DEFAULT_CHUNK_SIZE}. */\n chunkSize?: number;\n /** Sent as `Upload-Metadata` (base64-encoded values), e.g. `{ filename }`. */\n metadata?: Record<string, string>;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n /** Returns the current bearer token, read before each request. */\n getToken?: () => string | null | undefined;\n /** Send cookies. Default `false`. */\n withCredentials?: boolean;\n /**\n * Resume key. Defaults to a fingerprint of endpoint + file name/size/mtime, so\n * picking the same file after a reload resumes instead of restarting.\n */\n key?: string;\n /**\n * Where to persist resume state. Defaults to `localStorage`. Pass `null` to\n * disable persistence — resume then only survives a network blip, not a reload.\n */\n storage?: ResumableUploadStorage | null;\n /** Backoff for a failed chunk. Forwarded to `retry`. Default 5 attempts. */\n retry?: RetryOptions;\n /** Called on every upload-progress tick and after every confirmed chunk. */\n onProgress?: (progress: ResumableUploadProgress) => void;\n /** Called whenever {@link ResumableUpload.state} changes. */\n onStateChange?: (state: ResumableUploadState) => void;\n}\n\n/** What a finished upload resolves with. */\nexport interface ResumableUploadResult {\n /** The tus upload URL — hand this to your API to link the stored file. */\n url: string;\n /** Total bytes uploaded. */\n size: number;\n}\n\n/** A resumable upload in progress. Build one with {@link createResumableUpload}. */\nexport interface ResumableUpload {\n /**\n * Create (or re-attach to) the upload and push chunks until it is complete.\n *\n * Resolves `null` when the run stopped because of `pause()` or `abort()` —\n * neither is a failure. Rejects with a `TempestApiError` when the server\n * refused and the retries ran out.\n */\n start(): Promise<ResumableUploadResult | null>;\n /** Stop after the in-flight chunk is dropped, keeping the resume point. */\n pause(): void;\n /** Continue from the server's offset. Same resolution contract as `start`. */\n resume(): Promise<ResumableUploadResult | null>;\n /**\n * Stop for good.\n *\n * @param options - `discard: true` also sends `DELETE` (tus termination) and\n * forgets the persisted record, so the next `start()` uploads from zero.\n */\n abort(options?: { discard?: boolean }): Promise<void>;\n /** Current state. */\n readonly state: ResumableUploadState;\n /** Bytes the server has confirmed. */\n readonly offset: number;\n /** The upload URL, once creation succeeded. */\n readonly url: string | null;\n /** The resume key in use. */\n readonly key: string;\n}\n\ninterface RawResponse {\n status: number;\n text: string;\n header(name: string): string | null;\n}\n\n/**\n * Encode a string as standard base64 (padded), UTF-8 first.\n *\n * `Upload-Metadata` carries base64 values precisely so a filename with accents\n * survives an HTTP header, so the UTF-8 step is not optional: `btoa` alone throws\n * on any code point above U+00FF.\n *\n * @param value - Text to encode.\n * @returns Padded base64.\n */\nfunction base64Utf8(value: string): string {\n const bytes = new TextEncoder().encode(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/**\n * Build the `Upload-Metadata` header value: comma-separated `key base64(value)`.\n *\n * @param metadata - Plain string map.\n * @returns The header value, or `null` when there is nothing to send.\n */\nfunction encodeMetadata(metadata: Record<string, string> | undefined): string | null {\n if (!metadata) return null;\n const parts = Object.entries(metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`);\n return parts.length > 0 ? parts.join(\",\") : null;\n}\n\n/**\n * A stable-enough identity for a file, used as the default resume key.\n *\n * Name + size + last-modified is what the tus reference clients fingerprint on:\n * it is cheap (hashing the bytes of a 400 MB recording is not) and it changes\n * whenever the file does, which is the property that matters — resuming into the\n * wrong file would corrupt it silently.\n *\n * @param endpoint - Creation endpoint, so the same file to two servers is two uploads.\n * @param file - The blob or file being uploaded.\n * @returns A key safe to use in `localStorage`.\n */\nexport function uploadFingerprint(endpoint: string, file: Blob | File): string {\n const named = file as File;\n const name = typeof named.name === \"string\" ? named.name : \"blob\";\n const modified = typeof named.lastModified === \"number\" ? named.lastModified : 0;\n return `${endpoint}|${name}|${file.size}|${file.type}|${modified}`;\n}\n\n/**\n * `localStorage`-backed resume storage — the default.\n *\n * `localStorage` and not IndexedDB on purpose. The record is four fields and a\n * URL; the requirement is only that it survives a reload, and pulling Dexie in for\n * that would put an IndexedDB dependency in the bundle of every app that uploads a\n * file. Apps that already have `createOfflineStore` open can pass their own\n * {@link ResumableUploadStorage} instead.\n *\n * @param prefix - Key prefix. Default `\"tempest-upload:\"`.\n * @returns A storage that no-ops when `localStorage` is unavailable.\n */\nexport function createLocalUploadStorage(prefix = \"tempest-upload:\"): ResumableUploadStorage {\n function backend(): Storage | null {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n }\n\n return {\n get(key) {\n const raw = backend()?.getItem(prefix + key);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as ResumableUploadRecord;\n } catch {\n return null;\n }\n },\n set(key, record) {\n backend()?.setItem(prefix + key, JSON.stringify(record));\n },\n delete(key) {\n backend()?.removeItem(prefix + key);\n },\n };\n}\n\n/**\n * Send one request over `XMLHttpRequest`.\n *\n * `XMLHttpRequest` rather than `fetch` for the same reason `uploadWithProgress`\n * uses it — `fetch` still cannot report upload progress in any browser — plus one\n * more: tus answers every write with the new `Upload-Offset` in a **response\n * header**, and `uploadWithProgress` only hands back a parsed body, so it could\n * not be reused here.\n *\n * @param init - Method, URL, headers, optional body and progress callback.\n * @returns Status, raw text and a header reader.\n */\nfunction sendRequest(init: {\n method: \"POST\" | \"HEAD\" | \"PATCH\" | \"DELETE\";\n url: string;\n headers: Record<string, string>;\n body?: Blob;\n withCredentials: boolean;\n onProgress?: (loaded: number) => void;\n register: (xhr: XMLHttpRequest) => void;\n}): Promise<RawResponse> {\n return new Promise<RawResponse>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(init.method, init.url);\n xhr.withCredentials = init.withCredentials;\n for (const [name, value] of Object.entries(init.headers)) {\n xhr.setRequestHeader(name, value);\n }\n if (init.onProgress) {\n const report = init.onProgress;\n xhr.upload.onprogress = (event: ProgressEvent) => report(event.loaded);\n }\n xhr.onload = () =>\n resolve({\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n });\n xhr.onerror = () =>\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload resumível.\",\n }),\n );\n xhr.onabort = () => reject(new DOMException(\"Aborted\", \"AbortError\"));\n init.register(xhr);\n xhr.send(init.body);\n });\n}\n\nfunction parseOffset(response: RawResponse): number | null {\n const raw = response.header(\"Upload-Offset\");\n if (raw === null) return null;\n const value = Number(raw);\n return Number.isFinite(value) && value >= 0 ? value : null;\n}\n\n/**\n * Read an error body without assuming it is JSON.\n *\n * A tus proxy that rejects a chunk often answers with plain text or an HTML error\n * page, and `JSON.parse` throwing there would replace a useful status with a parse\n * error.\n *\n * @param text - Raw response text.\n * @returns The parsed object, the raw text, or `null` when the body was empty.\n */\nfunction parseErrorBody(text: string): unknown {\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/**\n * Turn a refused tus response into a `TempestApiError`.\n *\n * The fallback `detail` is used unless the server sent a real error envelope,\n * because `buildApiError`'s own fallback (`\"Erro 409\"`) says nothing about which\n * step of the protocol broke — and that is the whole diagnostic value here.\n *\n * @param response - The raw response that was not acceptable.\n * @param detail - Message to use when the body carries none.\n * @returns The error to throw.\n */\nfunction failed(response: RawResponse, detail: string): TempestApiError {\n const body = parseErrorBody(response.text);\n const envelope = buildApiError(response.status, body, { get: response.header });\n const hasDetail =\n typeof body === \"object\" && body !== null && (\"detail\" in body || \"message\" in body);\n return new TempestApiError({ ...envelope, detail: hasDetail ? envelope.detail : detail });\n}\n\n/**\n * Resolve a `Location` header against the page, so a relative upload URL works.\n *\n * tus servers are free to answer creation with either an absolute URL or a\n * path, and the spec does not prefer one — a client that only handles absolute\n * URLs breaks against half the implementations.\n *\n * @param value - The raw `Location` header.\n * @returns An absolute URL, or the input when there is no base to resolve against.\n */\nfunction resolveUploadUrl(value: string): string {\n const base = typeof window === \"undefined\" ? undefined : window.location.href;\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Chunked, resumable upload speaking the **tus 1.0.0** protocol (core plus the\n * *creation* and *termination* extensions).\n *\n * ## Why tus and not a bespoke scheme\n *\n * A resumable client whose wire format is undocumented cannot be integrated, and\n * inventing one means the backend is ours forever. tus is a published spec with\n * off-the-shelf servers (`tusd`, `tuspy`, `tus-node-server`), so a caller can point\n * this at something they did not write.\n *\n * ## What the backend must implement\n *\n * Every request carries `Tus-Resumable: 1.0.0`.\n *\n * | Step | Request | Expected response |\n * | --- | --- | --- |\n * | Create | `POST {endpoint}` + `Upload-Length`, `Upload-Metadata`, `Idempotency-Key` | `201` + `Location` (the upload URL, absolute or endpoint-relative) |\n * | Probe | `HEAD {uploadUrl}` | `200`/`204` + `Upload-Offset` |\n * | Write | `PATCH {uploadUrl}` + `Upload-Offset`, `Content-Type: application/offset+octet-stream`, chunk body | `204` + the new `Upload-Offset`; `409` when the offset does not match |\n * | Discard | `DELETE {uploadUrl}` | `204` |\n *\n * ## The failure that actually happens\n *\n * A chunk that the server stored but whose response never arrived. The client\n * cannot tell that from a chunk that was lost, and re-sending it blindly would\n * duplicate bytes. Two things prevent that:\n *\n * - **Writes are addressed, not appended.** Every `PATCH` states the offset it\n * writes at, so a retry after a lost response is asked to write bytes the server\n * already has and answers `409`. On any retry the client re-reads the truth with\n * `HEAD` first and continues from there.\n * - **Creation carries an `Idempotency-Key`** (from `generateIdempotencyKey`),\n * persisted before the first attempt and reused on retry. tus has no idempotent\n * creation of its own, so without this a lost `201` leaves an orphan upload on\n * the server. A backend that honours the header returns the same `Location`; one\n * that ignores it still works, it just keeps the orphan.\n *\n * @param options - Endpoint, file, and the knobs above.\n * @returns A handle with `start`/`pause`/`resume`/`abort` and live `state`/`offset`.\n *\n * @example\n * const upload = createResumableUpload({\n * endpoint: \"/api/uploads\",\n * file: recording,\n * metadata: { filename: \"nota.webm\", ticket: ticketId },\n * getToken: () => auth.getToken(),\n * onProgress: ({ fraction }) => setPercent(Math.round(fraction * 100)),\n * });\n *\n * const done = await upload.start();\n * if (done) await api.post(\"/api/tickets/1/audio\", { body: { url: done.url } });\n */\nexport function createResumableUpload(options: ResumableUploadOptions): ResumableUpload {\n const {\n endpoint,\n file,\n chunkSize = DEFAULT_CHUNK_SIZE,\n metadata,\n headers = {},\n getToken,\n withCredentials = false,\n key = uploadFingerprint(endpoint, file),\n storage = createLocalUploadStorage(),\n retry: retryOptions,\n onProgress,\n onStateChange,\n } = options;\n\n let state: ResumableUploadState = \"idle\";\n let offset = 0;\n let url: string | null = null;\n let idempotencyKey: string | null = null;\n let stopping: \"pause\" | \"abort\" | null = null;\n let inFlight: XMLHttpRequest | null = null;\n let resumedFrom = 0;\n\n function setState(next: ResumableUploadState): void {\n if (state === next) return;\n state = next;\n onStateChange?.(next);\n }\n\n function report(loaded: number): void {\n onProgress?.({\n loaded,\n total: file.size,\n fraction: file.size === 0 ? 1 : loaded / file.size,\n resumedFrom,\n });\n }\n\n function baseHeaders(): Record<string, string> {\n const result: Record<string, string> = { ...headers, \"Tus-Resumable\": TUS_VERSION };\n const token = getToken?.();\n if (token && !(\"Authorization\" in result)) result.Authorization = `Bearer ${token}`;\n return result;\n }\n\n function register(xhr: XMLHttpRequest): void {\n inFlight = xhr;\n }\n\n async function persist(): Promise<void> {\n if (!storage || !url || !idempotencyKey) return;\n await storage.set(key, {\n url,\n offset,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n /**\n * Ask the server how much it holds. The only source of truth after any failure.\n */\n async function probe(target: string): Promise<number> {\n const response = await sendRequest({\n method: \"HEAD\",\n url: target,\n headers: baseHeaders(),\n withCredentials,\n register,\n });\n if (response.status === 404 || response.status === 410) {\n throw new TempestApiError({\n status: response.status,\n detail: \"O upload expirou no servidor. Comece de novo.\",\n });\n }\n const confirmed = parseOffset(response);\n if (confirmed === null) throw failed(response, \"HEAD sem Upload-Offset.\");\n return confirmed;\n }\n\n /**\n * Re-attach to a persisted upload, or create a new one.\n *\n * The persisted record is only trusted when the file size still matches, and the\n * offset it holds is re-checked with `HEAD` — the client's copy can be ahead of\n * the server's whenever the last response was lost.\n */\n async function ensureUpload(): Promise<string> {\n const stored = storage ? await storage.get(key) : null;\n if (stored && stored.size === file.size) {\n idempotencyKey = stored.idempotencyKey;\n if (stored.url) {\n try {\n offset = await probe(stored.url);\n url = stored.url;\n return stored.url;\n } catch {\n offset = 0;\n }\n }\n }\n\n setState(\"creating\");\n idempotencyKey ??= generateIdempotencyKey();\n url = null;\n offset = 0;\n if (storage) {\n await storage.set(key, {\n url: \"\",\n offset: 0,\n size: file.size,\n idempotencyKey,\n updatedAt: Date.now(),\n });\n }\n\n const creationHeaders: Record<string, string> = {\n ...baseHeaders(),\n \"Upload-Length\": String(file.size),\n \"Idempotency-Key\": idempotencyKey,\n };\n const encoded = encodeMetadata(metadata);\n if (encoded) creationHeaders[\"Upload-Metadata\"] = encoded;\n\n const response = await sendRequest({\n method: \"POST\",\n url: endpoint,\n headers: creationHeaders,\n withCredentials,\n register,\n });\n if (response.status !== 201) throw failed(response, \"Criação do upload recusada.\");\n const locationHeader = response.header(\"Location\");\n if (!locationHeader) throw failed(response, \"Criação do upload sem cabeçalho Location.\");\n\n url = resolveUploadUrl(locationHeader);\n await persist();\n return url;\n }\n\n /** Push one chunk, resyncing the offset first when a previous attempt failed. */\n async function writeChunk(target: string, resync: { needed: boolean }): Promise<void> {\n if (resync.needed) {\n offset = await probe(target);\n resync.needed = false;\n report(offset);\n await persist();\n if (offset >= file.size) return;\n }\n\n const end = Math.min(offset + chunkSize, file.size);\n const from = offset;\n const response = await sendRequest({\n method: \"PATCH\",\n url: target,\n headers: {\n ...baseHeaders(),\n \"Content-Type\": \"application/offset+octet-stream\",\n \"Upload-Offset\": String(from),\n },\n body: file.slice(from, end),\n withCredentials,\n onProgress: (loaded) => report(Math.min(from + loaded, file.size)),\n register,\n });\n\n if (response.status === 409 || response.status === 412) {\n resync.needed = true;\n throw failed(response, \"Offset divergente — o servidor já tinha esses bytes.\");\n }\n if (response.status !== 204 && response.status !== 200) {\n throw failed(response, \"Chunk recusado pelo servidor.\");\n }\n\n offset = parseOffset(response) ?? end;\n report(offset);\n await persist();\n }\n\n /**\n * Drive the whole upload: attach or create, then chunk until complete.\n *\n * The `shouldRetry` predicate does double duty — besides deciding, it arms\n * `resync` so the next attempt re-reads the server's offset with `HEAD` before\n * writing. That is deliberate: it is the one place that sees *every* chunk\n * failure, whatever the cause, and after any failure the client's idea of the\n * offset is exactly what cannot be trusted.\n *\n * @returns The result, or `null` when `pause`/`abort` stopped the run.\n */\n async function run(): Promise<ResumableUploadResult | null> {\n stopping = null;\n const target = await ensureUpload();\n resumedFrom = offset;\n setState(\"uploading\");\n report(offset);\n\n const resync = { needed: false };\n while (offset < file.size) {\n if (stopping) break;\n await retry(() => writeChunk(target, resync), {\n retries: 5,\n ...retryOptions,\n shouldRetry: (error, attempt) => {\n if (stopping) return false;\n if (error instanceof DOMException && error.name === \"AbortError\") return false;\n resync.needed = true;\n return retryOptions?.shouldRetry?.(error, attempt) ?? true;\n },\n });\n }\n\n if (stopping === \"pause\") {\n setState(\"paused\");\n return null;\n }\n if (stopping === \"abort\") {\n setState(\"aborted\");\n return null;\n }\n\n setState(\"done\");\n if (storage) await storage.delete(key);\n return { url: target, size: file.size };\n }\n\n async function guarded(): Promise<ResumableUploadResult | null> {\n try {\n return await run();\n } catch (error) {\n if (\n stopping !== null ||\n (error instanceof DOMException && error.name === \"AbortError\")\n ) {\n setState(stopping === \"abort\" ? \"aborted\" : \"paused\");\n return null;\n }\n setState(\"error\");\n throw error;\n } finally {\n inFlight = null;\n }\n }\n\n function stop(reason: \"pause\" | \"abort\"): void {\n stopping = reason;\n inFlight?.abort();\n inFlight = null;\n }\n\n return {\n start: guarded,\n resume: guarded,\n pause: () => stop(\"pause\"),\n abort: async ({ discard = false } = {}) => {\n stop(\"abort\");\n setState(\"aborted\");\n if (!discard) return;\n if (url) {\n await sendRequest({\n method: \"DELETE\",\n url,\n headers: baseHeaders(),\n withCredentials,\n register: () => undefined,\n }).catch(() => undefined);\n }\n if (storage) await storage.delete(key);\n },\n get state() {\n return state;\n },\n get offset() {\n return offset;\n },\n get url() {\n return url;\n },\n key,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-with-progress.cjs","names":[],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"upload-with-progress.cjs","names":[],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n withCredentials = false,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n } = options;\n\n return new Promise<T>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n if (token && !(\"Authorization\" in finalHeaders)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n }\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const isSuccess = xhr.status >= 200 && xhr.status < 300;\n const contentType = xhr.getResponseHeader(\"content-type\") ?? \"\";\n\n if (!isSuccess) {\n const errorBody = parseErrorBody(xhr.responseText);\n reject(\n new TempestApiError(\n buildApiError(\n xhr.status,\n errorBody,\n { get: (name) => xhr.getResponseHeader(name) },\n sentRequestId,\n ),\n ),\n );\n return;\n }\n\n if (xhr.status === 204 || !xhr.responseText) {\n resolve(undefined as T);\n return;\n }\n\n if (contentType.includes(\"application/json\")) {\n try {\n resolve(parser(xhr.responseText) as T);\n } catch (err) {\n reject(err);\n }\n } else {\n resolve(xhr.responseText as unknown as T);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":"8DAyCA,SAAS,EAAe,EAAsB,CAC1C,GAAI,CAAC,EAAK,OAAO,KACjB,GAAI,CACA,OAAO,KAAK,MAAM,CAAG,CACzB,MAAQ,CACJ,OAAO,CACX,CACJ,CAWA,SAAgB,EAAgC,EAAgD,CAC5F,GAAM,CACF,MACA,OACA,SAAS,OACT,UAAU,CAAC,EACX,WACA,kBAAkB,GAClB,aACA,SACA,SAAS,KAAK,MACd,aACA,EAEJ,OAAO,IAAI,SAAY,EAAS,IAAW,CACvC,GAAI,GAAQ,QAAS,CACjB,EAAO,IAAI,aAAa,UAAW,YAAY,CAAC,EAChD,MACJ,CAEA,IAAM,EAAM,IAAI,eAChB,EAAI,KAAK,EAAQ,CAAG,EACpB,EAAI,gBAAkB,EAEtB,IAAM,EAAQ,IAAW,EACnB,EAAgB,EAAY,EAAU,EAAI,EAAA,SAAS,EACnD,EAAuC,CAAE,GAAG,CAAQ,EACtD,GAAS,EAAE,kBAAmB,KAC9B,EAAa,cAAgB,UAAU,KAEvC,GAAiB,EAAE,iBAAkB,KACrC,EAAa,gBAAkB,GAEnC,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAY,EAClD,EAAI,iBAAiB,EAAK,CAAK,EAG/B,IACA,EAAI,OAAO,WAAc,GAAU,CAC/B,EAAW,CACP,OAAQ,EAAM,OACd,MAAO,EAAM,MACb,SAAU,EAAM,iBAAmB,EAAM,OAAS,EAAM,MAAQ,KAChE,iBAAkB,EAAM,gBAC5B,CAAC,CACL,GAGJ,SAAS,GAAoB,CACzB,EAAI,MAAM,CACd,CACA,GAAQ,iBAAiB,QAAS,CAAW,EAE7C,EAAI,WAAe,CACf,GAAQ,oBAAoB,QAAS,CAAW,EAChD,IAAM,EAAY,EAAI,QAAU,KAAO,EAAI,OAAS,IAC9C,EAAc,EAAI,kBAAkB,cAAc,GAAK,GAE7D,GAAI,CAAC,EAAW,CACZ,IAAM,EAAY,EAAe,EAAI,YAAY,EACjD,EACI,IAAI,EAAA,gBACA,EAAA,cACI,EAAI,OACJ,EACA,CAAE,IAAM,GAAS,EAAI,kBAAkB,CAAI,CAAE,EAC7C,CACJ,CACJ,CACJ,EACA,MACJ,CAEA,GAAI,EAAI,SAAW,KAAO,CAAC,EAAI,aAAc,CACzC,EAAQ,IAAA,EAAc,EACtB,MACJ,CAEA,GAAI,EAAY,SAAS,kBAAkB,EACvC,GAAI,CACA,EAAQ,EAAO,EAAI,YAAY,CAAM,CACzC,OAAS,EAAK,CACV,EAAO,CAAG,CACd,MAEA,EAAQ,EAAI,YAA4B,CAEhD,EAEA,EAAI,YAAgB,CAChB,GAAQ,oBAAoB,QAAS,CAAW,EAChD,EACI,IAAI,EAAA,gBAAgB,CAChB,OAAQ,EACR,OAAQ,2BACR,UAAW,GAAiB,IAAA,EAChC,CAAC,CACL,CACJ,EAEA,EAAI,YAAgB,CAChB,GAAQ,oBAAoB,QAAS,CAAW,EAChD,EAAO,IAAI,aAAa,UAAW,YAAY,CAAC,CACpD,EAEA,EAAI,KAAK,CAAI,CACjB,CAAC,CACL"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-with-progress.js","names":[],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"upload-with-progress.js","names":[],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n withCredentials = false,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n } = options;\n\n return new Promise<T>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n if (token && !(\"Authorization\" in finalHeaders)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n }\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const isSuccess = xhr.status >= 200 && xhr.status < 300;\n const contentType = xhr.getResponseHeader(\"content-type\") ?? \"\";\n\n if (!isSuccess) {\n const errorBody = parseErrorBody(xhr.responseText);\n reject(\n new TempestApiError(\n buildApiError(\n xhr.status,\n errorBody,\n { get: (name) => xhr.getResponseHeader(name) },\n sentRequestId,\n ),\n ),\n );\n return;\n }\n\n if (xhr.status === 204 || !xhr.responseText) {\n resolve(undefined as T);\n return;\n }\n\n if (contentType.includes(\"application/json\")) {\n try {\n resolve(parser(xhr.responseText) as T);\n } catch (err) {\n reject(err);\n }\n } else {\n resolve(xhr.responseText as unknown as T);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":";;;AAyCA,SAAS,EAAe,GAAsB;CAC1C,IAAI,CAAC,GAAK,OAAO;CACjB,IAAI;EACA,OAAO,KAAK,MAAM,CAAG;CACzB,QAAQ;EACJ,OAAO;CACX;AACJ;AAWA,SAAgB,EAAgC,GAAgD;CAC5F,IAAM,EACF,QACA,SACA,YAAS,QACT,aAAU,CAAC,GACX,aACA,qBAAkB,IAClB,eACA,WACA,YAAS,KAAK,OACd,iBACA;CAEJ,OAAO,IAAI,SAAY,GAAS,MAAW;EACvC,IAAI,GAAQ,SAAS;GACjB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;GAChD;EACJ;EAEA,IAAM,IAAM,IAAI,eAAe;EAE/B,AADA,EAAI,KAAK,GAAQ,CAAG,GACpB,EAAI,kBAAkB;EAEtB,IAAM,IAAQ,IAAW,GACnB,IAAgB,IAAY,EAAU,IAAI,EAAS,GACnD,IAAuC,EAAE,GAAG,EAAQ;EAI1D,AAHI,KAAS,EAAE,mBAAmB,OAC9B,EAAa,gBAAgB,UAAU,MAEvC,KAAiB,EAAE,kBAAkB,OACrC,EAAa,kBAAkB;EAEnC,KAAK,IAAM,CAAC,GAAK,MAAU,OAAO,QAAQ,CAAY,GAClD,EAAI,iBAAiB,GAAK,CAAK;EAGnC,AAAI,MACA,EAAI,OAAO,cAAc,MAAU;GAC/B,EAAW;IACP,QAAQ,EAAM;IACd,OAAO,EAAM;IACb,UAAU,EAAM,mBAAmB,EAAM,SAAS,EAAM,QAAQ;IAChE,kBAAkB,EAAM;GAC5B,CAAC;EACL;EAGJ,SAAS,IAAoB;GACzB,EAAI,MAAM;EACd;EAuDA,AAtDA,GAAQ,iBAAiB,SAAS,CAAW,GAE7C,EAAI,eAAe;GACf,GAAQ,oBAAoB,SAAS,CAAW;GAChD,IAAM,IAAY,EAAI,UAAU,OAAO,EAAI,SAAS,KAC9C,IAAc,EAAI,kBAAkB,cAAc,KAAK;GAE7D,IAAI,CAAC,GAAW;IACZ,IAAM,IAAY,EAAe,EAAI,YAAY;IACjD,EACI,IAAI,EACA,EACI,EAAI,QACJ,GACA,EAAE,MAAM,MAAS,EAAI,kBAAkB,CAAI,EAAE,GAC7C,CACJ,CACJ,CACJ;IACA;GACJ;GAEA,IAAI,EAAI,WAAW,OAAO,CAAC,EAAI,cAAc;IACzC,EAAQ,KAAA,CAAc;IACtB;GACJ;GAEA,IAAI,EAAY,SAAS,kBAAkB,GACvC,IAAI;IACA,EAAQ,EAAO,EAAI,YAAY,CAAM;GACzC,SAAS,GAAK;IACV,EAAO,CAAG;GACd;QAEA,EAAQ,EAAI,YAA4B;EAEhD,GAEA,EAAI,gBAAgB;GAEhB,AADA,GAAQ,oBAAoB,SAAS,CAAW,GAChD,EACI,IAAI,EAAgB;IAChB,QAAQ;IACR,QAAQ;IACR,WAAW,KAAiB,KAAA;GAChC,CAAC,CACL;EACJ,GAEA,EAAI,gBAAgB;GAEhB,AADA,GAAQ,oBAAoB,SAAS,CAAW,GAChD,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;EACpD,GAEA,EAAI,KAAK,CAAI;CACjB,CAAC;AACL"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"I18nProvider.cjs","names":[],"sources":["../../src/i18n/I18nProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createI18n, type Catalog, type I18n } from \"./create-i18n\";\n\nexport interface I18nContextValue extends I18n {\n /** Switch the active locale. Persisted when `storageKey` is set. */\n setLocale: (locale: string) => void;\n /** Locales available in the catalog. */\n availableLocales: string[];\n}\n\nconst I18nContext = createContext<I18nContextValue | null>(null);\n\nexport interface I18nProviderProps {\n children: ReactNode;\n /** Initial locale. */\n locale: string;\n /** Fallback locale used when a key is missing. */\n fallbackLocale?: string;\n /** Translation catalog. */\n messages: Catalog;\n /** localStorage key for persisting the user choice. Pass `null` to disable. Default: `\"tempest-locale\"`. */\n storageKey?: string | null;\n}\n\nfunction readStored(storageKey: string | null, available: string[]): string | null {\n if (!storageKey || typeof window === \"undefined\") return null;\n try {\n const value = window.localStorage.getItem(storageKey);\n if (value && available.includes(value)) return value;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * React provider for the SDK's lightweight i18n. Exposes the translation\n * helpers and a `setLocale` setter to children.\n */\nexport function I18nProvider({\n children,\n locale: initialLocale,\n fallbackLocale,\n messages,\n storageKey = \"tempest-locale\",\n}: I18nProviderProps) {\n const availableLocales = useMemo(() => Object.keys(messages), [messages]);\n const [locale, setLocaleState] = useState<string>(\n () => readStored(storageKey, availableLocales) ?? initialLocale,\n );\n\n useEffect(() => {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"lang\", locale);\n }\n }, [locale]);\n\n const setLocale = useCallback(\n (next: string) => {\n setLocaleState(next);\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n /*
|
|
1
|
+
{"version":3,"file":"I18nProvider.cjs","names":[],"sources":["../../src/i18n/I18nProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createI18n, type Catalog, type I18n } from \"./create-i18n\";\n\nexport interface I18nContextValue extends I18n {\n /** Switch the active locale. Persisted when `storageKey` is set. */\n setLocale: (locale: string) => void;\n /** Locales available in the catalog. */\n availableLocales: string[];\n}\n\nconst I18nContext = createContext<I18nContextValue | null>(null);\n\nexport interface I18nProviderProps {\n children: ReactNode;\n /** Initial locale. */\n locale: string;\n /** Fallback locale used when a key is missing. */\n fallbackLocale?: string;\n /** Translation catalog. */\n messages: Catalog;\n /** localStorage key for persisting the user choice. Pass `null` to disable. Default: `\"tempest-locale\"`. */\n storageKey?: string | null;\n}\n\nfunction readStored(storageKey: string | null, available: string[]): string | null {\n if (!storageKey || typeof window === \"undefined\") return null;\n try {\n const value = window.localStorage.getItem(storageKey);\n if (value && available.includes(value)) return value;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * React provider for the SDK's lightweight i18n. Exposes the translation\n * helpers and a `setLocale` setter to children.\n *\n * @tempest-limits empty-catch — remembering the chosen locale is best-effort. The\n * switch itself is React state and already happened by the time the write runs, so\n * a storage failure costs the preference on the next load and nothing in this one.\n */\nexport function I18nProvider({\n children,\n locale: initialLocale,\n fallbackLocale,\n messages,\n storageKey = \"tempest-locale\",\n}: I18nProviderProps) {\n const availableLocales = useMemo(() => Object.keys(messages), [messages]);\n const [locale, setLocaleState] = useState<string>(\n () => readStored(storageKey, availableLocales) ?? initialLocale,\n );\n\n useEffect(() => {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"lang\", locale);\n }\n }, [locale]);\n\n const setLocale = useCallback(\n (next: string) => {\n setLocaleState(next);\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n /* empty */\n }\n }\n },\n [storageKey],\n );\n\n const value = useMemo<I18nContextValue>(() => {\n const i18n = createI18n({ locale, fallbackLocale, messages });\n return { ...i18n, setLocale, availableLocales };\n }, [locale, fallbackLocale, messages, setLocale, availableLocales]);\n\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;\n}\n\n/**\n * Access translation helpers. Must be used inside an {@link I18nProvider}.\n */\nexport function useI18n(): I18nContextValue {\n const ctx = useContext(I18nContext);\n if (!ctx) throw new Error(\"useI18n must be used inside an <I18nProvider>\");\n return ctx;\n}\n\n/**\n * Shortcut for components that only need the `t` function — avoids destructuring.\n */\nexport function useTranslate(): I18nContextValue[\"t\"] {\n return useI18n().t;\n}\n"],"mappings":"2FAWA,IAAM,GAAA,EAAA,EAAA,cAAA,CAAqD,IAAI,EAc/D,SAAS,EAAW,EAA2B,EAAoC,CAC/E,GAAI,CAAC,GAAc,OAAO,OAAW,IAAa,OAAO,KACzD,GAAI,CACA,IAAM,EAAQ,OAAO,aAAa,QAAQ,CAAU,EAEpD,OADI,GAAS,EAAU,SAAS,CAAK,EAAU,EACxC,IACX,MAAQ,CACJ,OAAO,IACX,CACJ,CAUA,SAAgB,EAAa,CACzB,WACA,OAAQ,EACR,iBACA,WACA,aAAa,kBACK,CAClB,IAAM,GAAA,EAAA,EAAA,QAAA,KAAiC,OAAO,KAAK,CAAQ,EAAG,CAAC,CAAQ,CAAC,EAClE,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,KACL,EAAW,EAAY,CAAgB,GAAK,CACtD,GAEA,EAAA,EAAA,UAAA,KAAgB,CACR,OAAO,SAAa,KACpB,SAAS,gBAAgB,aAAa,OAAQ,CAAM,CAE5D,EAAG,CAAC,CAAM,CAAC,EAEX,IAAM,GAAA,EAAA,EAAA,YAAA,CACD,GAAiB,CAEd,GADA,EAAe,CAAI,EACf,GAAc,OAAO,OAAW,IAChC,GAAI,CACA,OAAO,aAAa,QAAQ,EAAY,CAAI,CAChD,MAAQ,CAER,CAER,EACA,CAAC,CAAU,CACf,EAEM,GAAA,EAAA,EAAA,QAAA,MAEK,CAAE,GADI,EAAA,WAAW,CAAE,SAAQ,iBAAgB,UAAS,CAC/C,EAAM,YAAW,kBAAiB,GAC/C,CAAC,EAAQ,EAAgB,EAAU,EAAW,CAAgB,CAAC,EAElE,OAAO,EAAA,EAAA,IAAA,CAAC,EAAY,SAAb,CAA6B,QAAQ,UAA+B,CAAA,CAC/E,CAKA,SAAgB,GAA4B,CACxC,IAAM,GAAA,EAAA,EAAA,WAAA,CAAiB,CAAW,EAClC,GAAI,CAAC,EAAK,MAAU,MAAM,+CAA+C,EACzE,OAAO,CACX,CAKA,SAAgB,GAAsC,CAClD,OAAO,EAAQ,CAAC,CAAC,CACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"I18nProvider.js","names":[],"sources":["../../src/i18n/I18nProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createI18n, type Catalog, type I18n } from \"./create-i18n\";\n\nexport interface I18nContextValue extends I18n {\n /** Switch the active locale. Persisted when `storageKey` is set. */\n setLocale: (locale: string) => void;\n /** Locales available in the catalog. */\n availableLocales: string[];\n}\n\nconst I18nContext = createContext<I18nContextValue | null>(null);\n\nexport interface I18nProviderProps {\n children: ReactNode;\n /** Initial locale. */\n locale: string;\n /** Fallback locale used when a key is missing. */\n fallbackLocale?: string;\n /** Translation catalog. */\n messages: Catalog;\n /** localStorage key for persisting the user choice. Pass `null` to disable. Default: `\"tempest-locale\"`. */\n storageKey?: string | null;\n}\n\nfunction readStored(storageKey: string | null, available: string[]): string | null {\n if (!storageKey || typeof window === \"undefined\") return null;\n try {\n const value = window.localStorage.getItem(storageKey);\n if (value && available.includes(value)) return value;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * React provider for the SDK's lightweight i18n. Exposes the translation\n * helpers and a `setLocale` setter to children.\n */\nexport function I18nProvider({\n children,\n locale: initialLocale,\n fallbackLocale,\n messages,\n storageKey = \"tempest-locale\",\n}: I18nProviderProps) {\n const availableLocales = useMemo(() => Object.keys(messages), [messages]);\n const [locale, setLocaleState] = useState<string>(\n () => readStored(storageKey, availableLocales) ?? initialLocale,\n );\n\n useEffect(() => {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"lang\", locale);\n }\n }, [locale]);\n\n const setLocale = useCallback(\n (next: string) => {\n setLocaleState(next);\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n /*
|
|
1
|
+
{"version":3,"file":"I18nProvider.js","names":[],"sources":["../../src/i18n/I18nProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createI18n, type Catalog, type I18n } from \"./create-i18n\";\n\nexport interface I18nContextValue extends I18n {\n /** Switch the active locale. Persisted when `storageKey` is set. */\n setLocale: (locale: string) => void;\n /** Locales available in the catalog. */\n availableLocales: string[];\n}\n\nconst I18nContext = createContext<I18nContextValue | null>(null);\n\nexport interface I18nProviderProps {\n children: ReactNode;\n /** Initial locale. */\n locale: string;\n /** Fallback locale used when a key is missing. */\n fallbackLocale?: string;\n /** Translation catalog. */\n messages: Catalog;\n /** localStorage key for persisting the user choice. Pass `null` to disable. Default: `\"tempest-locale\"`. */\n storageKey?: string | null;\n}\n\nfunction readStored(storageKey: string | null, available: string[]): string | null {\n if (!storageKey || typeof window === \"undefined\") return null;\n try {\n const value = window.localStorage.getItem(storageKey);\n if (value && available.includes(value)) return value;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * React provider for the SDK's lightweight i18n. Exposes the translation\n * helpers and a `setLocale` setter to children.\n *\n * @tempest-limits empty-catch — remembering the chosen locale is best-effort. The\n * switch itself is React state and already happened by the time the write runs, so\n * a storage failure costs the preference on the next load and nothing in this one.\n */\nexport function I18nProvider({\n children,\n locale: initialLocale,\n fallbackLocale,\n messages,\n storageKey = \"tempest-locale\",\n}: I18nProviderProps) {\n const availableLocales = useMemo(() => Object.keys(messages), [messages]);\n const [locale, setLocaleState] = useState<string>(\n () => readStored(storageKey, availableLocales) ?? initialLocale,\n );\n\n useEffect(() => {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"lang\", locale);\n }\n }, [locale]);\n\n const setLocale = useCallback(\n (next: string) => {\n setLocaleState(next);\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n /* empty */\n }\n }\n },\n [storageKey],\n );\n\n const value = useMemo<I18nContextValue>(() => {\n const i18n = createI18n({ locale, fallbackLocale, messages });\n return { ...i18n, setLocale, availableLocales };\n }, [locale, fallbackLocale, messages, setLocale, availableLocales]);\n\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;\n}\n\n/**\n * Access translation helpers. Must be used inside an {@link I18nProvider}.\n */\nexport function useI18n(): I18nContextValue {\n const ctx = useContext(I18nContext);\n if (!ctx) throw new Error(\"useI18n must be used inside an <I18nProvider>\");\n return ctx;\n}\n\n/**\n * Shortcut for components that only need the `t` function — avoids destructuring.\n */\nexport function useTranslate(): I18nContextValue[\"t\"] {\n return useI18n().t;\n}\n"],"mappings":";;;;AAWA,IAAM,IAAc,EAAuC,IAAI;AAc/D,SAAS,EAAW,GAA2B,GAAoC;CAC/E,IAAI,CAAC,KAAc,OAAO,SAAW,KAAa,OAAO;CACzD,IAAI;EACA,IAAM,IAAQ,OAAO,aAAa,QAAQ,CAAU;EAEpD,OADI,KAAS,EAAU,SAAS,CAAK,IAAU,IACxC;CACX,QAAQ;EACJ,OAAO;CACX;AACJ;AAUA,SAAgB,EAAa,EACzB,aACA,QAAQ,GACR,mBACA,aACA,gBAAa,oBACK;CAClB,IAAM,IAAmB,QAAc,OAAO,KAAK,CAAQ,GAAG,CAAC,CAAQ,CAAC,GAClE,CAAC,GAAQ,KAAkB,QACvB,EAAW,GAAY,CAAgB,KAAK,CACtD;CAEA,QAAgB;EACZ,AAAI,OAAO,WAAa,OACpB,SAAS,gBAAgB,aAAa,QAAQ,CAAM;CAE5D,GAAG,CAAC,CAAM,CAAC;CAEX,IAAM,IAAY,GACb,MAAiB;EAEd,IADA,EAAe,CAAI,GACf,KAAc,OAAO,SAAW,KAChC,IAAI;GACA,OAAO,aAAa,QAAQ,GAAY,CAAI;EAChD,QAAQ,CAER;CAER,GACA,CAAC,CAAU,CACf,GAEM,IAAQ,SAEH;EAAE,GADI,EAAW;GAAE;GAAQ;GAAgB;EAAS,CAC/C;EAAM;EAAW;CAAiB,IAC/C;EAAC;EAAQ;EAAgB;EAAU;EAAW;CAAgB,CAAC;CAElE,OAAO,kBAAC,EAAY,UAAb;EAA6B;EAAQ;CAA+B,CAAA;AAC/E;AAKA,SAAgB,IAA4B;CACxC,IAAM,IAAM,EAAW,CAAW;CAClC,IAAI,CAAC,GAAK,MAAU,MAAM,+CAA+C;CACzE,OAAO;AACX;AAKA,SAAgB,IAAsC;CAClD,OAAO,EAAQ,CAAC,CAAC;AACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.cjs","names":[],"sources":["../../src/imaging/canvas.ts"],"sourcesContent":["/**\n * The drawing surface, and the one thing everyone gets wrong on it.\n *\n * **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the\n * transparent pixels black. Filling the surface first is the difference\n * between a photo on a white background and one with a black hole in it.\n *\n * What is *not* here is worth recording. The received wisdom for downscaling\n * on a canvas is to halve repeatedly, because a single `drawImage` into a\n * much smaller box was said to alias. That was implemented here, and then\n * measured: on a 512 px checkerboard reduced to 32 px, the stepwise result\n * and the single high-quality draw were **pixel-identical** (standard\n * deviation 0.0 on both) in Chromium and Firefox — while stepwise cost\n * **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and\n * allocated three intermediate canvases on a device that may not have the\n * memory. Modern engines honour `imageSmoothingQuality = \"high\"`, which is\n * what this module sets. The halving was deleted rather than kept \"just in\n * case\": unmeasurable benefit at 300x the cost is not insurance, it is\n * ballast.\n */\n\nimport { ImagingUnavailableError } from \"./exceptions\";\n\n/** A canvas this module can draw on, on the main thread or in a worker. */\nexport type Surface = OffscreenCanvas | HTMLCanvasElement;\n\n/** A 2-D context from either surface kind. */\nexport type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n/**\n * Create a drawing surface, preferring `OffscreenCanvas`.\n *\n * `OffscreenCanvas` works inside a worker, which is where a PWA wants this\n * running: resizing a 12-megapixel photo on the main thread blocks the UI\n * for tens of milliseconds per image.\n *\n * @param width Surface width in pixels.\n * @param height Surface height in pixels.\n * @returns The surface.\n * @throws {@link ImagingUnavailableError} when neither kind exists — a\n * server render, or a test environment without a canvas.\n */\nexport function createSurface(width: number, height: number): Surface {\n const safeWidth = Math.max(1, Math.round(width));\n const safeHeight = Math.max(1, Math.round(height));\n\n if (typeof OffscreenCanvas !== \"undefined\") {\n return new OffscreenCanvas(safeWidth, safeHeight);\n }\n if (typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n canvas.width = safeWidth;\n canvas.height = safeHeight;\n return canvas;\n }\n throw new ImagingUnavailableError(\n \"No canvas is available here. This module needs a browser (or a worker \" +\n \"with OffscreenCanvas); it does not run under plain Node.\",\n );\n}\n\n/**\n * Get a 2-D context configured for image work.\n *\n * `imageSmoothingQuality = \"high\"` is the setting that makes a steep\n * downscale average its source pixels instead of sampling them sparsely.\n *\n * @param surface The surface to draw on.\n * @param background Optional fill painted before anything else.\n * @returns The context.\n * @throws {@link ImagingUnavailableError} when the context cannot be created.\n */\nexport function getContext(surface: Surface, background?: string): SurfaceContext {\n const context = surface.getContext(\"2d\") as SurfaceContext | null;\n if (context === null) {\n throw new ImagingUnavailableError(\"Could not get a 2-D context from the canvas.\");\n }\n context.imageSmoothingEnabled = true;\n context.imageSmoothingQuality = \"high\";\n if (background !== undefined) {\n context.fillStyle = background;\n context.fillRect(0, 0, surface.width, surface.height);\n }\n return context;\n}\n\n/**\n * Draw a bitmap into a surface with high-quality filtering.\n *\n * @param bitmap The source pixels.\n * @param target Destination surface.\n * @param box Where to draw inside the destination.\n * @param background Fill painted before drawing.\n */\nexport function drawScaled(\n bitmap: ImageBitmap,\n target: Surface,\n box: { x: number; y: number; width: number; height: number },\n background?: string,\n): void {\n const context = getContext(target, background);\n context.drawImage(bitmap, box.x, box.y, box.width, box.height);\n}\n"],"mappings":"oCA0CA,SAAgB,EAAc,EAAe,EAAyB,CAClE,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EACzC,EAAa,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,EAEjD,GAAI,OAAO,gBAAoB,IAC3B,OAAO,IAAI,gBAAgB,EAAW,CAAU,EAEpD,GAAI,OAAO,SAAa,IAAa,CACjC,IAAM,EAAS,SAAS,cAAc,QAAQ,EAG9C,MAFA,GAAO,MAAQ,EACf,EAAO,OAAS,EACT,CACX,CACA,MAAM,IAAI,EAAA,wBACN,gIAEJ,CACJ,CAaA,SAAgB,EAAW,EAAkB,EAAqC,CAC9E,IAAM,EAAU,EAAQ,WAAW,IAAI,EACvC,GAAI,IAAY,KACZ,MAAM,IAAI,EAAA,wBAAwB,8CAA8C,EAQpF,MANA,GAAQ,sBAAwB,GAChC,EAAQ,sBAAwB,OAC5B,IAAe,IAAA,KACf,EAAQ,UAAY,EACpB,EAAQ,SAAS,EAAG,EAAG,EAAQ,MAAO,EAAQ,MAAM,GAEjD,CACX,
|
|
1
|
+
{"version":3,"file":"canvas.cjs","names":[],"sources":["../../src/imaging/canvas.ts"],"sourcesContent":["/**\n * The drawing surface, and the one thing everyone gets wrong on it.\n *\n * **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the\n * transparent pixels black. Filling the surface first is the difference\n * between a photo on a white background and one with a black hole in it.\n *\n * What is *not* here is worth recording. The received wisdom for downscaling\n * on a canvas is to halve repeatedly, because a single `drawImage` into a\n * much smaller box was said to alias. That was implemented here, and then\n * measured: on a 512 px checkerboard reduced to 32 px, the stepwise result\n * and the single high-quality draw were **pixel-identical** (standard\n * deviation 0.0 on both) in Chromium and Firefox — while stepwise cost\n * **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and\n * allocated three intermediate canvases on a device that may not have the\n * memory. Modern engines honour `imageSmoothingQuality = \"high\"`, which is\n * what this module sets. The halving was deleted rather than kept \"just in\n * case\": unmeasurable benefit at 300x the cost is not insurance, it is\n * ballast.\n */\n\nimport { ImagingUnavailableError } from \"./exceptions\";\n\n/** A canvas this module can draw on, on the main thread or in a worker. */\nexport type Surface = OffscreenCanvas | HTMLCanvasElement;\n\n/** A 2-D context from either surface kind. */\nexport type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n/**\n * Create a drawing surface, preferring `OffscreenCanvas`.\n *\n * `OffscreenCanvas` works inside a worker, which is where a PWA wants this\n * running: resizing a 12-megapixel photo on the main thread blocks the UI\n * for tens of milliseconds per image.\n *\n * @param width Surface width in pixels.\n * @param height Surface height in pixels.\n * @returns The surface.\n * @throws {@link ImagingUnavailableError} when neither kind exists — a\n * server render, or a test environment without a canvas.\n */\nexport function createSurface(width: number, height: number): Surface {\n const safeWidth = Math.max(1, Math.round(width));\n const safeHeight = Math.max(1, Math.round(height));\n\n if (typeof OffscreenCanvas !== \"undefined\") {\n return new OffscreenCanvas(safeWidth, safeHeight);\n }\n if (typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n canvas.width = safeWidth;\n canvas.height = safeHeight;\n return canvas;\n }\n throw new ImagingUnavailableError(\n \"No canvas is available here. This module needs a browser (or a worker \" +\n \"with OffscreenCanvas); it does not run under plain Node.\",\n );\n}\n\n/**\n * Get a 2-D context configured for image work.\n *\n * `imageSmoothingQuality = \"high\"` is the setting that makes a steep\n * downscale average its source pixels instead of sampling them sparsely.\n *\n * @param surface The surface to draw on.\n * @param background Optional fill painted before anything else.\n * @returns The context.\n * @throws {@link ImagingUnavailableError} when the context cannot be created.\n */\nexport function getContext(surface: Surface, background?: string): SurfaceContext {\n const context = surface.getContext(\"2d\") as SurfaceContext | null;\n if (context === null) {\n throw new ImagingUnavailableError(\"Could not get a 2-D context from the canvas.\");\n }\n context.imageSmoothingEnabled = true;\n context.imageSmoothingQuality = \"high\";\n if (background !== undefined) {\n context.fillStyle = background;\n context.fillRect(0, 0, surface.width, surface.height);\n }\n return context;\n}\n\n/**\n * Draw a bitmap into a surface with high-quality filtering.\n *\n * @param bitmap The source pixels.\n * @param target Destination surface.\n * @param box Where to draw inside the destination.\n * @param background Fill painted before drawing.\n *\n * @tempest-limits param-count — source, destination, destination geometry, and an\n * optional background: the same four things `CanvasRenderingContext2D.drawImage`\n * takes, in the same order. Public surface, and a wrapper over a browser primitive\n * reads best when it keeps that primitive's shape.\n */\nexport function drawScaled(\n bitmap: ImageBitmap,\n target: Surface,\n box: { x: number; y: number; width: number; height: number },\n background?: string,\n): void {\n const context = getContext(target, background);\n context.drawImage(bitmap, box.x, box.y, box.width, box.height);\n}\n"],"mappings":"oCA0CA,SAAgB,EAAc,EAAe,EAAyB,CAClE,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EACzC,EAAa,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,EAEjD,GAAI,OAAO,gBAAoB,IAC3B,OAAO,IAAI,gBAAgB,EAAW,CAAU,EAEpD,GAAI,OAAO,SAAa,IAAa,CACjC,IAAM,EAAS,SAAS,cAAc,QAAQ,EAG9C,MAFA,GAAO,MAAQ,EACf,EAAO,OAAS,EACT,CACX,CACA,MAAM,IAAI,EAAA,wBACN,gIAEJ,CACJ,CAaA,SAAgB,EAAW,EAAkB,EAAqC,CAC9E,IAAM,EAAU,EAAQ,WAAW,IAAI,EACvC,GAAI,IAAY,KACZ,MAAM,IAAI,EAAA,wBAAwB,8CAA8C,EAQpF,MANA,GAAQ,sBAAwB,GAChC,EAAQ,sBAAwB,OAC5B,IAAe,IAAA,KACf,EAAQ,UAAY,EACpB,EAAQ,SAAS,EAAG,EAAG,EAAQ,MAAO,EAAQ,MAAM,GAEjD,CACX,CAeA,SAAgB,EACZ,EACA,EACA,EACA,EACI,CAEJ,EAD2B,EAAQ,CACnC,CAAA,CAAQ,UAAU,EAAQ,EAAI,EAAG,EAAI,EAAG,EAAI,MAAO,EAAI,MAAM,CACjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","names":[],"sources":["../../src/imaging/canvas.ts"],"sourcesContent":["/**\n * The drawing surface, and the one thing everyone gets wrong on it.\n *\n * **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the\n * transparent pixels black. Filling the surface first is the difference\n * between a photo on a white background and one with a black hole in it.\n *\n * What is *not* here is worth recording. The received wisdom for downscaling\n * on a canvas is to halve repeatedly, because a single `drawImage` into a\n * much smaller box was said to alias. That was implemented here, and then\n * measured: on a 512 px checkerboard reduced to 32 px, the stepwise result\n * and the single high-quality draw were **pixel-identical** (standard\n * deviation 0.0 on both) in Chromium and Firefox — while stepwise cost\n * **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and\n * allocated three intermediate canvases on a device that may not have the\n * memory. Modern engines honour `imageSmoothingQuality = \"high\"`, which is\n * what this module sets. The halving was deleted rather than kept \"just in\n * case\": unmeasurable benefit at 300x the cost is not insurance, it is\n * ballast.\n */\n\nimport { ImagingUnavailableError } from \"./exceptions\";\n\n/** A canvas this module can draw on, on the main thread or in a worker. */\nexport type Surface = OffscreenCanvas | HTMLCanvasElement;\n\n/** A 2-D context from either surface kind. */\nexport type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n/**\n * Create a drawing surface, preferring `OffscreenCanvas`.\n *\n * `OffscreenCanvas` works inside a worker, which is where a PWA wants this\n * running: resizing a 12-megapixel photo on the main thread blocks the UI\n * for tens of milliseconds per image.\n *\n * @param width Surface width in pixels.\n * @param height Surface height in pixels.\n * @returns The surface.\n * @throws {@link ImagingUnavailableError} when neither kind exists — a\n * server render, or a test environment without a canvas.\n */\nexport function createSurface(width: number, height: number): Surface {\n const safeWidth = Math.max(1, Math.round(width));\n const safeHeight = Math.max(1, Math.round(height));\n\n if (typeof OffscreenCanvas !== \"undefined\") {\n return new OffscreenCanvas(safeWidth, safeHeight);\n }\n if (typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n canvas.width = safeWidth;\n canvas.height = safeHeight;\n return canvas;\n }\n throw new ImagingUnavailableError(\n \"No canvas is available here. This module needs a browser (or a worker \" +\n \"with OffscreenCanvas); it does not run under plain Node.\",\n );\n}\n\n/**\n * Get a 2-D context configured for image work.\n *\n * `imageSmoothingQuality = \"high\"` is the setting that makes a steep\n * downscale average its source pixels instead of sampling them sparsely.\n *\n * @param surface The surface to draw on.\n * @param background Optional fill painted before anything else.\n * @returns The context.\n * @throws {@link ImagingUnavailableError} when the context cannot be created.\n */\nexport function getContext(surface: Surface, background?: string): SurfaceContext {\n const context = surface.getContext(\"2d\") as SurfaceContext | null;\n if (context === null) {\n throw new ImagingUnavailableError(\"Could not get a 2-D context from the canvas.\");\n }\n context.imageSmoothingEnabled = true;\n context.imageSmoothingQuality = \"high\";\n if (background !== undefined) {\n context.fillStyle = background;\n context.fillRect(0, 0, surface.width, surface.height);\n }\n return context;\n}\n\n/**\n * Draw a bitmap into a surface with high-quality filtering.\n *\n * @param bitmap The source pixels.\n * @param target Destination surface.\n * @param box Where to draw inside the destination.\n * @param background Fill painted before drawing.\n */\nexport function drawScaled(\n bitmap: ImageBitmap,\n target: Surface,\n box: { x: number; y: number; width: number; height: number },\n background?: string,\n): void {\n const context = getContext(target, background);\n context.drawImage(bitmap, box.x, box.y, box.width, box.height);\n}\n"],"mappings":";;AA0CA,SAAgB,EAAc,GAAe,GAAyB;CAClE,IAAM,IAAY,KAAK,IAAI,GAAG,KAAK,MAAM,CAAK,CAAC,GACzC,IAAa,KAAK,IAAI,GAAG,KAAK,MAAM,CAAM,CAAC;CAEjD,IAAI,OAAO,kBAAoB,KAC3B,OAAO,IAAI,gBAAgB,GAAW,CAAU;CAEpD,IAAI,OAAO,WAAa,KAAa;EACjC,IAAM,IAAS,SAAS,cAAc,QAAQ;EAG9C,OAFA,EAAO,QAAQ,GACf,EAAO,SAAS,GACT;CACX;CACA,MAAM,IAAI,EACN,gIAEJ;AACJ;AAaA,SAAgB,EAAW,GAAkB,GAAqC;CAC9E,IAAM,IAAU,EAAQ,WAAW,IAAI;CACvC,IAAI,MAAY,MACZ,MAAM,IAAI,EAAwB,8CAA8C;CAQpF,OANA,EAAQ,wBAAwB,IAChC,EAAQ,wBAAwB,QAC5B,MAAe,KAAA,MACf,EAAQ,YAAY,GACpB,EAAQ,SAAS,GAAG,GAAG,EAAQ,OAAO,EAAQ,MAAM,IAEjD;AACX;
|
|
1
|
+
{"version":3,"file":"canvas.js","names":[],"sources":["../../src/imaging/canvas.ts"],"sourcesContent":["/**\n * The drawing surface, and the one thing everyone gets wrong on it.\n *\n * **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the\n * transparent pixels black. Filling the surface first is the difference\n * between a photo on a white background and one with a black hole in it.\n *\n * What is *not* here is worth recording. The received wisdom for downscaling\n * on a canvas is to halve repeatedly, because a single `drawImage` into a\n * much smaller box was said to alias. That was implemented here, and then\n * measured: on a 512 px checkerboard reduced to 32 px, the stepwise result\n * and the single high-quality draw were **pixel-identical** (standard\n * deviation 0.0 on both) in Chromium and Firefox — while stepwise cost\n * **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and\n * allocated three intermediate canvases on a device that may not have the\n * memory. Modern engines honour `imageSmoothingQuality = \"high\"`, which is\n * what this module sets. The halving was deleted rather than kept \"just in\n * case\": unmeasurable benefit at 300x the cost is not insurance, it is\n * ballast.\n */\n\nimport { ImagingUnavailableError } from \"./exceptions\";\n\n/** A canvas this module can draw on, on the main thread or in a worker. */\nexport type Surface = OffscreenCanvas | HTMLCanvasElement;\n\n/** A 2-D context from either surface kind. */\nexport type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n/**\n * Create a drawing surface, preferring `OffscreenCanvas`.\n *\n * `OffscreenCanvas` works inside a worker, which is where a PWA wants this\n * running: resizing a 12-megapixel photo on the main thread blocks the UI\n * for tens of milliseconds per image.\n *\n * @param width Surface width in pixels.\n * @param height Surface height in pixels.\n * @returns The surface.\n * @throws {@link ImagingUnavailableError} when neither kind exists — a\n * server render, or a test environment without a canvas.\n */\nexport function createSurface(width: number, height: number): Surface {\n const safeWidth = Math.max(1, Math.round(width));\n const safeHeight = Math.max(1, Math.round(height));\n\n if (typeof OffscreenCanvas !== \"undefined\") {\n return new OffscreenCanvas(safeWidth, safeHeight);\n }\n if (typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n canvas.width = safeWidth;\n canvas.height = safeHeight;\n return canvas;\n }\n throw new ImagingUnavailableError(\n \"No canvas is available here. This module needs a browser (or a worker \" +\n \"with OffscreenCanvas); it does not run under plain Node.\",\n );\n}\n\n/**\n * Get a 2-D context configured for image work.\n *\n * `imageSmoothingQuality = \"high\"` is the setting that makes a steep\n * downscale average its source pixels instead of sampling them sparsely.\n *\n * @param surface The surface to draw on.\n * @param background Optional fill painted before anything else.\n * @returns The context.\n * @throws {@link ImagingUnavailableError} when the context cannot be created.\n */\nexport function getContext(surface: Surface, background?: string): SurfaceContext {\n const context = surface.getContext(\"2d\") as SurfaceContext | null;\n if (context === null) {\n throw new ImagingUnavailableError(\"Could not get a 2-D context from the canvas.\");\n }\n context.imageSmoothingEnabled = true;\n context.imageSmoothingQuality = \"high\";\n if (background !== undefined) {\n context.fillStyle = background;\n context.fillRect(0, 0, surface.width, surface.height);\n }\n return context;\n}\n\n/**\n * Draw a bitmap into a surface with high-quality filtering.\n *\n * @param bitmap The source pixels.\n * @param target Destination surface.\n * @param box Where to draw inside the destination.\n * @param background Fill painted before drawing.\n *\n * @tempest-limits param-count — source, destination, destination geometry, and an\n * optional background: the same four things `CanvasRenderingContext2D.drawImage`\n * takes, in the same order. Public surface, and a wrapper over a browser primitive\n * reads best when it keeps that primitive's shape.\n */\nexport function drawScaled(\n bitmap: ImageBitmap,\n target: Surface,\n box: { x: number; y: number; width: number; height: number },\n background?: string,\n): void {\n const context = getContext(target, background);\n context.drawImage(bitmap, box.x, box.y, box.width, box.height);\n}\n"],"mappings":";;AA0CA,SAAgB,EAAc,GAAe,GAAyB;CAClE,IAAM,IAAY,KAAK,IAAI,GAAG,KAAK,MAAM,CAAK,CAAC,GACzC,IAAa,KAAK,IAAI,GAAG,KAAK,MAAM,CAAM,CAAC;CAEjD,IAAI,OAAO,kBAAoB,KAC3B,OAAO,IAAI,gBAAgB,GAAW,CAAU;CAEpD,IAAI,OAAO,WAAa,KAAa;EACjC,IAAM,IAAS,SAAS,cAAc,QAAQ;EAG9C,OAFA,EAAO,QAAQ,GACf,EAAO,SAAS,GACT;CACX;CACA,MAAM,IAAI,EACN,gIAEJ;AACJ;AAaA,SAAgB,EAAW,GAAkB,GAAqC;CAC9E,IAAM,IAAU,EAAQ,WAAW,IAAI;CACvC,IAAI,MAAY,MACZ,MAAM,IAAI,EAAwB,8CAA8C;CAQpF,OANA,EAAQ,wBAAwB,IAChC,EAAQ,wBAAwB,QAC5B,MAAe,KAAA,MACf,EAAQ,YAAY,GACpB,EAAQ,SAAS,GAAG,GAAG,EAAQ,OAAO,EAAQ,MAAM,IAEjD;AACX;AAeA,SAAgB,EACZ,GACA,GACA,GACA,GACI;CAEJ,EAD2B,GAAQ,CACnC,CAAA,CAAQ,UAAU,GAAQ,EAAI,GAAG,EAAI,GAAG,EAAI,OAAO,EAAI,MAAM;AACjE"}
|
package/dist/imaging.d.ts
CHANGED
|
@@ -185,6 +185,11 @@ export declare interface CompressOptions extends ResizeOptions {
|
|
|
185
185
|
* @param target Destination surface.
|
|
186
186
|
* @param box Where to draw inside the destination.
|
|
187
187
|
* @param background Fill painted before drawing.
|
|
188
|
+
*
|
|
189
|
+
* @tempest-limits param-count — source, destination, destination geometry, and an
|
|
190
|
+
* optional background: the same four things `CanvasRenderingContext2D.drawImage`
|
|
191
|
+
* takes, in the same order. Public surface, and a wrapper over a browser primitive
|
|
192
|
+
* reads best when it keeps that primitive's shape.
|
|
188
193
|
*/
|
|
189
194
|
export declare function drawScaled(bitmap: ImageBitmap, target: Surface, box: {
|
|
190
195
|
x: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.cjs","names":[],"sources":["../../src/logger/logger.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface LogEntry {\n level: LogLevel;\n message: string;\n context?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface LoggerSink {\n (entry: LogEntry): void;\n}\n\nexport interface Logger {\n debug: (message: string, context?: Record<string, unknown>) => void;\n info: (message: string, context?: Record<string, unknown>) => void;\n warn: (message: string, context?: Record<string, unknown>) => void;\n error: (message: string, context?: Record<string, unknown>) => void;\n /** Build a child logger that prefixes the namespace to each message. */\n child: (namespace: string) => Logger;\n}\n\nexport interface CreateLoggerOptions {\n /** Lowest level recorded. Default: `\"info\"`. */\n level?: LogLevel;\n /** Output adapters. Default: a `console.*` sink. */\n sinks?: LoggerSink[];\n /** Initial namespace (prepended to every message). */\n namespace?: string;\n}\n\nconst LEVELS: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\nfunction shouldLog(threshold: LogLevel, level: LogLevel): boolean {\n return LEVELS.indexOf(level) >= LEVELS.indexOf(threshold);\n}\n\n/** Default sink that writes to the browser console. */\nexport const consoleSink: LoggerSink = ({ level, message, context }) => {\n const method = level === \"debug\" ? \"log\" : level;\n if (context) {\n console[method](message, context);\n } else {\n console[method](message);\n }\n};\n\n/**\n * Create a structured leveled logger. Plug arbitrary sinks (Sentry, Datadog,\n * remote ingestion) by implementing the `LoggerSink` interface.\n */\nexport function createLogger(options: CreateLoggerOptions = {}): Logger {\n const threshold = options.level ?? \"info\";\n const sinks = options.sinks ?? [consoleSink];\n const namespace = options.namespace ?? \"\";\n\n function emit(level: LogLevel, message: string, context?: Record<string, unknown>): void {\n if (!shouldLog(threshold, level)) return;\n const entry: LogEntry = {\n level,\n message: namespace ? `[${namespace}] ${message}` : message,\n context,\n timestamp: Date.now(),\n };\n for (const sink of sinks) {\n try {\n sink(entry);\n } catch {\n /*
|
|
1
|
+
{"version":3,"file":"logger.cjs","names":[],"sources":["../../src/logger/logger.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface LogEntry {\n level: LogLevel;\n message: string;\n context?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface LoggerSink {\n (entry: LogEntry): void;\n}\n\nexport interface Logger {\n debug: (message: string, context?: Record<string, unknown>) => void;\n info: (message: string, context?: Record<string, unknown>) => void;\n warn: (message: string, context?: Record<string, unknown>) => void;\n error: (message: string, context?: Record<string, unknown>) => void;\n /** Build a child logger that prefixes the namespace to each message. */\n child: (namespace: string) => Logger;\n}\n\nexport interface CreateLoggerOptions {\n /** Lowest level recorded. Default: `\"info\"`. */\n level?: LogLevel;\n /** Output adapters. Default: a `console.*` sink. */\n sinks?: LoggerSink[];\n /** Initial namespace (prepended to every message). */\n namespace?: string;\n}\n\nconst LEVELS: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\nfunction shouldLog(threshold: LogLevel, level: LogLevel): boolean {\n return LEVELS.indexOf(level) >= LEVELS.indexOf(threshold);\n}\n\n/** Default sink that writes to the browser console. */\nexport const consoleSink: LoggerSink = ({ level, message, context }) => {\n const method = level === \"debug\" ? \"log\" : level;\n if (context) {\n console[method](message, context);\n } else {\n console[method](message);\n }\n};\n\n/**\n * Create a structured leveled logger. Plug arbitrary sinks (Sentry, Datadog,\n * remote ingestion) by implementing the `LoggerSink` interface.\n *\n * @tempest-limits empty-catch — a sink that throws must not take the app with it,\n * and it must not stop the sinks after it in the list either. Logging is the thing\n * you reach for when something is already going wrong; a transport failure that\n * propagates would turn a diagnostic into the outage. Nothing here can report the\n * failure without recursing into the logger that just failed.\n */\nexport function createLogger(options: CreateLoggerOptions = {}): Logger {\n const threshold = options.level ?? \"info\";\n const sinks = options.sinks ?? [consoleSink];\n const namespace = options.namespace ?? \"\";\n\n function emit(level: LogLevel, message: string, context?: Record<string, unknown>): void {\n if (!shouldLog(threshold, level)) return;\n const entry: LogEntry = {\n level,\n message: namespace ? `[${namespace}] ${message}` : message,\n context,\n timestamp: Date.now(),\n };\n for (const sink of sinks) {\n try {\n sink(entry);\n } catch {\n /* empty */\n }\n }\n }\n\n return {\n debug: (message, context) => emit(\"debug\", message, context),\n info: (message, context) => emit(\"info\", message, context),\n warn: (message, context) => emit(\"warn\", message, context),\n error: (message, context) => emit(\"error\", message, context),\n child: (childNamespace: string) =>\n createLogger({\n level: threshold,\n sinks,\n namespace: namespace ? `${namespace}:${childNamespace}` : childNamespace,\n }),\n };\n}\n"],"mappings":"AA+BA,IAAM,EAAqB,CAAC,QAAS,OAAQ,OAAQ,OAAO,EAE5D,SAAS,EAAU,EAAqB,EAA0B,CAC9D,OAAO,EAAO,QAAQ,CAAK,GAAK,EAAO,QAAQ,CAAS,CAC5D,CAGA,IAAa,GAA2B,CAAE,QAAO,UAAS,aAAc,CACpE,IAAM,EAAS,IAAU,QAAU,MAAQ,EACvC,EACA,QAAQ,EAAO,CAAC,EAAS,CAAO,EAEhC,QAAQ,EAAO,CAAC,CAAO,CAE/B,EAYA,SAAgB,EAAa,EAA+B,CAAC,EAAW,CACpE,IAAM,EAAY,EAAQ,OAAS,OAC7B,EAAQ,EAAQ,OAAS,CAAC,CAAW,EACrC,EAAY,EAAQ,WAAa,GAEvC,SAAS,EAAK,EAAiB,EAAiB,EAAyC,CACrF,GAAI,CAAC,EAAU,EAAW,CAAK,EAAG,OAClC,IAAM,EAAkB,CACpB,QACA,QAAS,EAAY,IAAI,EAAU,IAAI,IAAY,EACnD,UACA,UAAW,KAAK,IAAI,CACxB,EACA,IAAK,IAAM,KAAQ,EACf,GAAI,CACA,EAAK,CAAK,CACd,MAAQ,CAER,CAER,CAEA,MAAO,CACH,OAAQ,EAAS,IAAY,EAAK,QAAS,EAAS,CAAO,EAC3D,MAAO,EAAS,IAAY,EAAK,OAAQ,EAAS,CAAO,EACzD,MAAO,EAAS,IAAY,EAAK,OAAQ,EAAS,CAAO,EACzD,OAAQ,EAAS,IAAY,EAAK,QAAS,EAAS,CAAO,EAC3D,MAAQ,GACJ,EAAa,CACT,MAAO,EACP,QACA,UAAW,EAAY,GAAG,EAAU,GAAG,IAAmB,CAC9D,CAAC,CACT,CACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","names":[],"sources":["../../src/logger/logger.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface LogEntry {\n level: LogLevel;\n message: string;\n context?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface LoggerSink {\n (entry: LogEntry): void;\n}\n\nexport interface Logger {\n debug: (message: string, context?: Record<string, unknown>) => void;\n info: (message: string, context?: Record<string, unknown>) => void;\n warn: (message: string, context?: Record<string, unknown>) => void;\n error: (message: string, context?: Record<string, unknown>) => void;\n /** Build a child logger that prefixes the namespace to each message. */\n child: (namespace: string) => Logger;\n}\n\nexport interface CreateLoggerOptions {\n /** Lowest level recorded. Default: `\"info\"`. */\n level?: LogLevel;\n /** Output adapters. Default: a `console.*` sink. */\n sinks?: LoggerSink[];\n /** Initial namespace (prepended to every message). */\n namespace?: string;\n}\n\nconst LEVELS: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\nfunction shouldLog(threshold: LogLevel, level: LogLevel): boolean {\n return LEVELS.indexOf(level) >= LEVELS.indexOf(threshold);\n}\n\n/** Default sink that writes to the browser console. */\nexport const consoleSink: LoggerSink = ({ level, message, context }) => {\n const method = level === \"debug\" ? \"log\" : level;\n if (context) {\n console[method](message, context);\n } else {\n console[method](message);\n }\n};\n\n/**\n * Create a structured leveled logger. Plug arbitrary sinks (Sentry, Datadog,\n * remote ingestion) by implementing the `LoggerSink` interface.\n */\nexport function createLogger(options: CreateLoggerOptions = {}): Logger {\n const threshold = options.level ?? \"info\";\n const sinks = options.sinks ?? [consoleSink];\n const namespace = options.namespace ?? \"\";\n\n function emit(level: LogLevel, message: string, context?: Record<string, unknown>): void {\n if (!shouldLog(threshold, level)) return;\n const entry: LogEntry = {\n level,\n message: namespace ? `[${namespace}] ${message}` : message,\n context,\n timestamp: Date.now(),\n };\n for (const sink of sinks) {\n try {\n sink(entry);\n } catch {\n /*
|
|
1
|
+
{"version":3,"file":"logger.js","names":[],"sources":["../../src/logger/logger.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface LogEntry {\n level: LogLevel;\n message: string;\n context?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface LoggerSink {\n (entry: LogEntry): void;\n}\n\nexport interface Logger {\n debug: (message: string, context?: Record<string, unknown>) => void;\n info: (message: string, context?: Record<string, unknown>) => void;\n warn: (message: string, context?: Record<string, unknown>) => void;\n error: (message: string, context?: Record<string, unknown>) => void;\n /** Build a child logger that prefixes the namespace to each message. */\n child: (namespace: string) => Logger;\n}\n\nexport interface CreateLoggerOptions {\n /** Lowest level recorded. Default: `\"info\"`. */\n level?: LogLevel;\n /** Output adapters. Default: a `console.*` sink. */\n sinks?: LoggerSink[];\n /** Initial namespace (prepended to every message). */\n namespace?: string;\n}\n\nconst LEVELS: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\nfunction shouldLog(threshold: LogLevel, level: LogLevel): boolean {\n return LEVELS.indexOf(level) >= LEVELS.indexOf(threshold);\n}\n\n/** Default sink that writes to the browser console. */\nexport const consoleSink: LoggerSink = ({ level, message, context }) => {\n const method = level === \"debug\" ? \"log\" : level;\n if (context) {\n console[method](message, context);\n } else {\n console[method](message);\n }\n};\n\n/**\n * Create a structured leveled logger. Plug arbitrary sinks (Sentry, Datadog,\n * remote ingestion) by implementing the `LoggerSink` interface.\n *\n * @tempest-limits empty-catch — a sink that throws must not take the app with it,\n * and it must not stop the sinks after it in the list either. Logging is the thing\n * you reach for when something is already going wrong; a transport failure that\n * propagates would turn a diagnostic into the outage. Nothing here can report the\n * failure without recursing into the logger that just failed.\n */\nexport function createLogger(options: CreateLoggerOptions = {}): Logger {\n const threshold = options.level ?? \"info\";\n const sinks = options.sinks ?? [consoleSink];\n const namespace = options.namespace ?? \"\";\n\n function emit(level: LogLevel, message: string, context?: Record<string, unknown>): void {\n if (!shouldLog(threshold, level)) return;\n const entry: LogEntry = {\n level,\n message: namespace ? `[${namespace}] ${message}` : message,\n context,\n timestamp: Date.now(),\n };\n for (const sink of sinks) {\n try {\n sink(entry);\n } catch {\n /* empty */\n }\n }\n }\n\n return {\n debug: (message, context) => emit(\"debug\", message, context),\n info: (message, context) => emit(\"info\", message, context),\n warn: (message, context) => emit(\"warn\", message, context),\n error: (message, context) => emit(\"error\", message, context),\n child: (childNamespace: string) =>\n createLogger({\n level: threshold,\n sinks,\n namespace: namespace ? `${namespace}:${childNamespace}` : childNamespace,\n }),\n };\n}\n"],"mappings":";AA+BA,IAAM,IAAqB;CAAC;CAAS;CAAQ;CAAQ;AAAO;AAE5D,SAAS,EAAU,GAAqB,GAA0B;CAC9D,OAAO,EAAO,QAAQ,CAAK,KAAK,EAAO,QAAQ,CAAS;AAC5D;AAGA,IAAa,KAA2B,EAAE,UAAO,YAAS,iBAAc;CACpE,IAAM,IAAS,MAAU,UAAU,QAAQ;CAC3C,AAAI,IACA,QAAQ,EAAO,CAAC,GAAS,CAAO,IAEhC,QAAQ,EAAO,CAAC,CAAO;AAE/B;AAYA,SAAgB,EAAa,IAA+B,CAAC,GAAW;CACpE,IAAM,IAAY,EAAQ,SAAS,QAC7B,IAAQ,EAAQ,SAAS,CAAC,CAAW,GACrC,IAAY,EAAQ,aAAa;CAEvC,SAAS,EAAK,GAAiB,GAAiB,GAAyC;EACrF,IAAI,CAAC,EAAU,GAAW,CAAK,GAAG;EAClC,IAAM,IAAkB;GACpB;GACA,SAAS,IAAY,IAAI,EAAU,IAAI,MAAY;GACnD;GACA,WAAW,KAAK,IAAI;EACxB;EACA,KAAK,IAAM,KAAQ,GACf,IAAI;GACA,EAAK,CAAK;EACd,QAAQ,CAER;CAER;CAEA,OAAO;EACH,QAAQ,GAAS,MAAY,EAAK,SAAS,GAAS,CAAO;EAC3D,OAAO,GAAS,MAAY,EAAK,QAAQ,GAAS,CAAO;EACzD,OAAO,GAAS,MAAY,EAAK,QAAQ,GAAS,CAAO;EACzD,QAAQ,GAAS,MAAY,EAAK,SAAS,GAAS,CAAO;EAC3D,QAAQ,MACJ,EAAa;GACT,OAAO;GACP;GACA,WAAW,IAAY,GAAG,EAAU,GAAG,MAAmB;EAC9D,CAAC;CACT;AACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoogleSignIn.cjs","names":[],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"GoogleSignIn.cjs","names":[],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — component, theme, text, shape, size and width are\n * Google's own button parameters, passed through with their names intact so the\n * reader can match them to Google's documentation;\n * onSuccess/onError/locale/disableOneTap are the wrapper's.\n */\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type { OAuthCredential, OAuthError } from \"./types\";\n\nexport type GoogleSignInTheme = \"filled_blue\" | \"filled_black\" | \"outline\";\nexport type GoogleSignInText = \"signin_with\" | \"signup_with\" | \"continue_with\" | \"signin\";\nexport type GoogleSignInShape = \"rectangular\" | \"pill\" | \"circle\" | \"square\";\nexport type GoogleSignInSize = \"large\" | \"medium\" | \"small\";\n\nexport interface GoogleSignInProps {\n /**\n * `GoogleLogin` component from `@react-oauth/google`. The caller imports\n * it and passes it through so the SDK doesn't take `@react-oauth/google`\n * as a peer dep.\n */\n component: (props: Record<string, unknown>) => ReactNode;\n /** Fired with the validated credential on success. */\n onSuccess: (credential: OAuthCredential) => void | Promise<void>;\n /** Fired with a normalised error on failure. */\n onError?: (error: OAuthError) => void;\n /** Optional locale override (e.g. `\"pt-BR\"`). Falls back to browser locale. */\n locale?: string;\n /** Visual theme — passed through to `<GoogleLogin>`. */\n theme?: GoogleSignInTheme;\n /** Button text variant. */\n text?: GoogleSignInText;\n /** Button shape. */\n shape?: GoogleSignInShape;\n /** Button size. */\n size?: GoogleSignInSize;\n /** Disable Google's \"One Tap\" auto-prompt. Default `false`. */\n disableOneTap?: boolean;\n /** Optional `width` override (px) — Google's button is fixed-width. */\n width?: number;\n /** Optional className applied to the wrapper. */\n className?: string;\n /** Optional inline style applied to the wrapper. */\n style?: CSSProperties;\n}\n\n/**\n * Thin wrapper over `@react-oauth/google`'s `<GoogleLogin>` that:\n *\n * 1. Normalises the success payload into [[OAuthCredential]] (`idToken`,\n * `provider: \"google\"`, `raw`).\n * 2. Normalises errors into [[OAuthError]].\n * 3. Lets you pass `GoogleLogin` via the `component` prop, so the SDK does\n * not declare `@react-oauth/google` as a peer dep — apps that don't use\n * Google never pay for it.\n *\n * @example\n * import { GoogleLogin, GoogleOAuthProvider } from \"@react-oauth/google\";\n * import { GoogleSignIn } from \"tempest-react-sdk\";\n *\n * <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>\n * <GoogleSignIn\n * component={GoogleLogin}\n * onSuccess={async ({ idToken }) => {\n * await api.post(\"/auth/google\", { body: { id_token: idToken } });\n * }}\n * onError={(err) => toast.error(err.message)}\n * />\n * </GoogleOAuthProvider>\n */\nexport function GoogleSignIn({\n component: Component,\n onSuccess,\n onError,\n locale,\n theme,\n text,\n shape,\n size,\n disableOneTap,\n width,\n className,\n style,\n}: GoogleSignInProps) {\n return (\n <div className={className} style={style}>\n {Component({\n onSuccess: (response: { credential?: string }) => {\n if (!response.credential) {\n onError?.({\n provider: \"google\",\n message: \"Google returned no credential\",\n raw: response,\n });\n return;\n }\n return onSuccess({\n idToken: response.credential,\n provider: \"google\",\n raw: response,\n });\n },\n onError: () => {\n onError?.({\n provider: \"google\",\n message: \"Google login failed\",\n });\n },\n locale,\n theme,\n text,\n shape,\n size,\n useOneTap: !disableOneTap,\n width,\n })}\n </div>\n );\n}\n"],"mappings":"mCAqEA,SAAgB,EAAa,CACzB,UAAW,EACX,YACA,UACA,SACA,QACA,OACA,QACA,OACA,gBACA,QACA,YACA,SACkB,CAClB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAgB,YAAkB,iBAC7B,EAAU,CACP,UAAY,GAAsC,CAC9C,GAAI,CAAC,EAAS,WAAY,CACtB,IAAU,CACN,SAAU,SACV,QAAS,gCACT,IAAK,CACT,CAAC,EACD,MACJ,CACA,OAAO,EAAU,CACb,QAAS,EAAS,WAClB,SAAU,SACV,IAAK,CACT,CAAC,CACL,EACA,YAAe,CACX,IAAU,CACN,SAAU,SACV,QAAS,qBACb,CAAC,CACL,EACA,SACA,QACA,OACA,QACA,OACA,UAAW,CAAC,EACZ,OACJ,CAAC,CACA,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoogleSignIn.js","names":[],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"GoogleSignIn.js","names":[],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — component, theme, text, shape, size and width are\n * Google's own button parameters, passed through with their names intact so the\n * reader can match them to Google's documentation;\n * onSuccess/onError/locale/disableOneTap are the wrapper's.\n */\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type { OAuthCredential, OAuthError } from \"./types\";\n\nexport type GoogleSignInTheme = \"filled_blue\" | \"filled_black\" | \"outline\";\nexport type GoogleSignInText = \"signin_with\" | \"signup_with\" | \"continue_with\" | \"signin\";\nexport type GoogleSignInShape = \"rectangular\" | \"pill\" | \"circle\" | \"square\";\nexport type GoogleSignInSize = \"large\" | \"medium\" | \"small\";\n\nexport interface GoogleSignInProps {\n /**\n * `GoogleLogin` component from `@react-oauth/google`. The caller imports\n * it and passes it through so the SDK doesn't take `@react-oauth/google`\n * as a peer dep.\n */\n component: (props: Record<string, unknown>) => ReactNode;\n /** Fired with the validated credential on success. */\n onSuccess: (credential: OAuthCredential) => void | Promise<void>;\n /** Fired with a normalised error on failure. */\n onError?: (error: OAuthError) => void;\n /** Optional locale override (e.g. `\"pt-BR\"`). Falls back to browser locale. */\n locale?: string;\n /** Visual theme — passed through to `<GoogleLogin>`. */\n theme?: GoogleSignInTheme;\n /** Button text variant. */\n text?: GoogleSignInText;\n /** Button shape. */\n shape?: GoogleSignInShape;\n /** Button size. */\n size?: GoogleSignInSize;\n /** Disable Google's \"One Tap\" auto-prompt. Default `false`. */\n disableOneTap?: boolean;\n /** Optional `width` override (px) — Google's button is fixed-width. */\n width?: number;\n /** Optional className applied to the wrapper. */\n className?: string;\n /** Optional inline style applied to the wrapper. */\n style?: CSSProperties;\n}\n\n/**\n * Thin wrapper over `@react-oauth/google`'s `<GoogleLogin>` that:\n *\n * 1. Normalises the success payload into [[OAuthCredential]] (`idToken`,\n * `provider: \"google\"`, `raw`).\n * 2. Normalises errors into [[OAuthError]].\n * 3. Lets you pass `GoogleLogin` via the `component` prop, so the SDK does\n * not declare `@react-oauth/google` as a peer dep — apps that don't use\n * Google never pay for it.\n *\n * @example\n * import { GoogleLogin, GoogleOAuthProvider } from \"@react-oauth/google\";\n * import { GoogleSignIn } from \"tempest-react-sdk\";\n *\n * <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>\n * <GoogleSignIn\n * component={GoogleLogin}\n * onSuccess={async ({ idToken }) => {\n * await api.post(\"/auth/google\", { body: { id_token: idToken } });\n * }}\n * onError={(err) => toast.error(err.message)}\n * />\n * </GoogleOAuthProvider>\n */\nexport function GoogleSignIn({\n component: Component,\n onSuccess,\n onError,\n locale,\n theme,\n text,\n shape,\n size,\n disableOneTap,\n width,\n className,\n style,\n}: GoogleSignInProps) {\n return (\n <div className={className} style={style}>\n {Component({\n onSuccess: (response: { credential?: string }) => {\n if (!response.credential) {\n onError?.({\n provider: \"google\",\n message: \"Google returned no credential\",\n raw: response,\n });\n return;\n }\n return onSuccess({\n idToken: response.credential,\n provider: \"google\",\n raw: response,\n });\n },\n onError: () => {\n onError?.({\n provider: \"google\",\n message: \"Google login failed\",\n });\n },\n locale,\n theme,\n text,\n shape,\n size,\n useOneTap: !disableOneTap,\n width,\n })}\n </div>\n );\n}\n"],"mappings":";;AAqEA,SAAgB,EAAa,EACzB,WAAW,GACX,cACA,YACA,WACA,UACA,SACA,UACA,SACA,kBACA,UACA,cACA,YACkB;CAClB,OACI,kBAAC,OAAD;EAAgB;EAAkB;YAC7B,EAAU;GACP,YAAY,MAAsC;IAC9C,IAAI,CAAC,EAAS,YAAY;KACtB,IAAU;MACN,UAAU;MACV,SAAS;MACT,KAAK;KACT,CAAC;KACD;IACJ;IACA,OAAO,EAAU;KACb,SAAS,EAAS;KAClB,UAAU;KACV,KAAK;IACT,CAAC;GACL;GACA,eAAe;IACX,IAAU;KACN,UAAU;KACV,SAAS;IACb,CAAC;GACL;GACA;GACA;GACA;GACA;GACA;GACA,WAAW,CAAC;GACZ;EACJ,CAAC;CACA,CAAA;AAEb"}
|