@nexim/upload-sdk
Version:
TypeScript SDK for seamless integration with Nexim Media Upload Service. It provides state machine-based upload handling, progress tracking, and type-safe API for image optimization and file uploads.
8 lines (7 loc) • 18.5 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/main.ts", "../src/lib/upload-file-machine.ts", "../src/lib/upload-image-machine.ts", "../src/lib/virtual-file-input.ts"],
"sourcesContent": ["import { packageTracer } from '@alwatr/package-tracer';\n\n__dev_mode__: packageTracer.add(__package_name__, __package_version__);\n\nexport * from './lib/upload-file-machine.js';\nexport * from './lib/upload-image-machine.js';\nexport * from './lib/virtual-file-input.js';\n", "import { newFlatomise } from '@alwatr/flatomise';\nimport { AlwatrFluxStateMachine, AlwatrJsonFetchStateMachine, type FetchOptions } from '@alwatr/flux';\nimport { actionState } from '@nexim/action-state';\n\nimport type { ServiceResponse } from '@nexim/upload-types';\n\n/**\n * Configuration options for initializing an UploadFileMachine instance.\n */\nexport type UploadFileMachineOptions = {\n /**\n * Path where the file will be saved, without the file extension\n */\n pathWithoutExtension: string;\n\n /**\n * Authorization header value for API requests\n */\n authHeader: string;\n\n /**\n * Description of the file for maintenance purposes\n */\n description: string;\n\n /**\n * API endpoint URL for upload requests\n */\n apiEndpoint: string;\n};\n\nexport type UploadFileMachineState = 'initial' | 'loading' | 'failed' | 'complete';\ntype Event = 'request' | 'loading_failed' | 'loading_success';\n\n/**\n * A state machine for managing file uploads.\n * Handles file upload lifecycle including state management, API requests, and error handling.\n *\n * @noInheritDoc\n */\nexport class UploadFileMachine extends AlwatrFluxStateMachine<UploadFileMachineState, Event> {\n protected fileBlob_: Blob | null = null;\n\n private apiRequestFetchMachine__;\n\n constructor(options: UploadFileMachineOptions) {\n super({ name: options.pathWithoutExtension, initialState: 'initial' });\n\n this.stateRecord_ = {\n initial: {\n request: 'loading',\n },\n loading: {\n loading_failed: 'failed',\n loading_success: 'complete',\n },\n failed: {\n request: 'loading',\n },\n complete: {},\n };\n\n this.actionRecord_ = {\n on_state_loading_enter: this.uploadFile_.bind(this),\n on_state_complete_enter: this.clear.bind(this),\n };\n\n this.apiRequestFetchMachine__ = new AlwatrJsonFetchStateMachine<ServiceResponse<DictionaryReq<never>>>({\n name: `upload:${this.name_}`,\n fetch: this.generateFetchOption_(options),\n });\n\n this.apiRequestFetchMachine__.subscribe(({ state }) => {\n if (state === 'complete') {\n this.transition('loading_success');\n }\n else if (state === 'failed') {\n this.transition('loading_failed');\n }\n });\n }\n\n /**\n * Uploads an file synchronously and returns the default path upon completion.\n * Creates a promise wrapper around the asynchronous upload process.\n * Subscribes to state changes and resolves/rejects based on upload completion state.\n *\n * @param file - The blob object containing the file data to upload\n * @returns Promise that resolves with the default path string if successful, null if failed\n *\n * @example\n * ```ts\n * const uploadFileMachine = new UploadFileMachine({\n * pathWithoutExtension: 'path/to/file',\n * authHeader: 'Bearer token',\n * description: 'File description',\n * apiEndpoint: 'https://api.example.com/upload',\n * });\n * uploadFileMachine.syncUpload(file)\n * .then((path) => {\n * console.log('File uploaded successfully to:', path);\n * })\n * .catch(() => {\n * console.error('File upload failed');\n * });\n * ```\n */\n syncUpload(file: Blob): Promise<boolean> {\n this.logger_.logMethodArgs?.('syncUpload', { rawImage: file });\n\n const flatomise = newFlatomise<boolean>();\n\n const { unsubscribe } = this.subscribe(({ state }) => {\n actionState(state, {\n complete: () => {\n flatomise.resolve(true);\n unsubscribe();\n },\n failed: () => {\n flatomise.reject(false);\n unsubscribe();\n },\n });\n });\n\n this.upload(file);\n\n return flatomise.promise;\n }\n\n /**\n * Upload file.\n *\n * @param file - File to upload\n *\n * @example\n * ```ts\n * const uploadFileMachine = new UploadFileMachine({\n * pathWithoutExtension: 'path/to/file',\n * authHeader: 'Bearer token',\n * description: 'File description',\n * apiEndpoint: 'https://api.example.com/upload',\n * });\n * uploadFileMachine.subscribe(({state}) => {\n * if (state === 'complete') {\n * console.log('File uploaded successfully');\n * }\n * else if (state === 'failed') {\n * console.error('File upload failed');\n * }\n * });\n *\n * uploadFileMachine.upload(file);\n * ```\n */\n upload(file: Blob): void {\n this.logger_.logMethodArgs?.('upload', { file });\n\n this.fileBlob_ = file;\n this.transition('request');\n }\n\n /**\n * Clear the state.\n */\n clear(): void {\n this.logger_.logMethod?.('clear');\n\n this.fileBlob_ = null;\n this.apiRequestFetchMachine__.clean();\n }\n\n protected generateFetchOption_(options: UploadFileMachineOptions): FetchOptions {\n this.logger_.logMethod?.('generateFetchOption_');\n\n return {\n removeDuplicate: 'auto',\n retry: 2,\n retryDelay: 2_000,\n timeout: 60_000,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n authorization: options.authHeader,\n },\n url: options.apiEndpoint,\n queryParams: {\n path: options.pathWithoutExtension,\n description: options.description,\n },\n };\n }\n\n protected uploadFile_(): void {\n if (this.fileBlob_ === null) return;\n this.logger_.logMethod?.('uploadFile_');\n\n const reader = new FileReader();\n\n reader.addEventListener('loadend', (event) => {\n const buffer = event.target?.result;\n if (!buffer) {\n this.transition('loading_failed');\n return;\n }\n\n this.apiRequestFetchMachine__.request({ body: buffer });\n });\n\n reader.addEventListener('error', () => {\n this.logger_.error('uploadFile_', 'File reading error');\n this.transition('loading_failed');\n });\n\n reader.readAsArrayBuffer(this.fileBlob_);\n }\n}\n", "import { uploadImagePresetRecord } from '@nexim/upload-types';\n\nimport { UploadFileMachine, type UploadFileMachineOptions, type UploadFileMachineState } from './upload-file-machine.js';\n\nimport type { FetchOptions } from '@alwatr/flux';\n\n/**\n * Configuration options for initializing an UploadImageMachine instance.\n * Extends UploadFileMachineOptions with image-specific options.\n */\nexport type UploadImageMachineOptions = UploadFileMachineOptions & {\n /**\n * The preset name that defines image processing parameters.\n * Must be a key from the uploadImagePresetRecord.\n */\n presetName: keyof typeof uploadImagePresetRecord;\n};\n\n/**\n * States for the image upload machine.\n * Inherits all states from UploadFileMachineState.\n */\nexport type UploadImageMachineState = UploadFileMachineState;\n\n/**\n * Specialized state machine for handling image uploads.\n * Extends UploadFileMachine with image-specific functionality including:\n * - Image resizing based on preset configurations\n * - Client-side image optimization\n * - Automatic format conversion\n */\nexport class UploadImageMachine extends UploadFileMachine {\n /** The default path where the image will be saved */\n defaultPath;\n\n /** Stores preset configuration for the current upload */\n private uploadImagePresetRecord__;\n\n /** Temporarily stores the original file before optimization */\n private nonOptimizedFileBlob__: Blob | null = null;\n\n constructor(options: UploadImageMachineOptions) {\n super(options);\n\n this.actionRecord_ = {\n ...this.actionRecord_,\n on_state_loading_enter: this.uploadAfterResize__.bind(this),\n };\n\n this.uploadImagePresetRecord__ = uploadImagePresetRecord[options.presetName];\n this.defaultPath = options.pathWithoutExtension + this.uploadImagePresetRecord__.client.defaultAppendName;\n }\n\n /**\n * Initiates an image upload process.\n * The image will be optimized according to the preset configuration before uploading.\n *\n * @param file - The image file to upload\n *\n * @example\n * ```ts\n * const uploadImageMachine = new UploadImageMachine({\n * pathWithoutExtension: 'path/to/image',\n * authHeader: 'Bearer token',\n * description: 'Image description',\n * apiEndpoint: 'https://api.example.com/upload',\n * presetName: 'thumbnail',\n * });\n * uploadImageMachine.subscribe(({state}) => {\n * if (state === 'complete') {\n * console.log('Image uploaded successfully');\n * }\n * else if (state === 'failed') {\n * console.error('Image upload failed');\n * }\n * });\n *\n * uploadImageMachine.upload(file);\n * ```\n */\n override upload(file: Blob): void {\n this.logger_.logMethodArgs?.('upload', { file });\n\n this.nonOptimizedFileBlob__ = file;\n this.transition('request');\n }\n\n /**\n * Handles the image resizing process before uploading.\n * Called automatically when entering the loading state.\n */\n private uploadAfterResize__(): void {\n if (this.nonOptimizedFileBlob__ === null) return;\n this.resizeImage__(this.nonOptimizedFileBlob__)\n .then((blob) => {\n this.fileBlob_ = blob;\n this.uploadFile_();\n })\n .catch((error) => {\n this.logger_.error('resizeImage', 'catch', { error });\n this.transition('loading_failed');\n });\n }\n\n /**\n * Generates fetch options for the image upload request.\n * Extends the base fetch options with image-specific parameters.\n */\n protected override generateFetchOption_(options: UploadImageMachineOptions): FetchOptions {\n this.logger_.logMethod?.('generateFetchOption_');\n\n return {\n removeDuplicate: 'auto',\n retry: 2,\n retryDelay: 2_000,\n timeout: 30_000,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n authorization: options.authHeader,\n },\n url: options.apiEndpoint,\n queryParams: {\n path: options.pathWithoutExtension,\n presetName: options.presetName,\n description: options.description,\n },\n };\n }\n\n /**\n * Resizes an image according to preset configuration.\n * Maintains aspect ratio when only width or height is specified.\n *\n * @param file - The original image file\n * @returns A promise resolving to the resized image blob\n */\n private resizeImage__(file: Blob): Promise<Blob> {\n this.logger_.logMethod?.('resizeImage__');\n\n return new Promise((resolve, reject) => {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) {\n resolve(file);\n return;\n }\n\n let width = this.uploadImagePresetRecord__.client.width;\n let height = this.uploadImagePresetRecord__.client.height;\n\n const image = new Image();\n image.onload = () => {\n const aspect = image.width / image.height;\n\n if (width && height === -1) {\n height = width / aspect;\n }\n else if (height && width === -1) {\n width = height * aspect;\n }\n\n canvas.width = width;\n canvas.height = height;\n\n context.drawImage(image, 0, 0, width, height);\n canvas.toBlob(\n (blob) => {\n resolve(blob!);\n },\n file.type,\n this.uploadImagePresetRecord__.client.quality / 100,\n );\n };\n\n image.addEventListener('error', (error) => {\n this.logger_.error('resizeImage', 'image_onerror', { error });\n reject(error);\n });\n\n image.src = URL.createObjectURL(file);\n });\n }\n}\n", "import { createLogger } from '@alwatr/logger';\nimport type { MaybePromise } from '@alwatr/type-helper';\n\nconst logger = createLogger(__package_name__);\n\n/**\n * Creates and triggers a virtual file input element to handle file selection.\n * This utility creates a temporary file input element, attaches event handlers,\n * and removes itself after use.\n *\n * Features:\n * - Creates a hidden file input element\n * - Handles file selection through browser's native interface\n * - Automatically cleans up after selection\n * - Supports async callbacks\n *\n * @example\n * ```ts\n * pickAndProcessFile('image/*', async (file) => {\n * await uploadImage(file);\n * });\n * ```\n *\n * @param accept - Comma-separated list of allowed file types\n * @param callback - Function to handle the selected file\n */\nexport function pickAndProcessFile(accept: string, callback: (file: File) => MaybePromise<void>): void {\n logger.logMethod?.('pickAndProcessFile');\n\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n\n input.addEventListener('change', async () => {\n logger.logOther?.('changed');\n if (input.files === null || input.files.length === 0) return;\n const file = input.files[0];\n\n logger.logOther?.('pickAndProcessFile.change', { file });\n\n input.remove(); // Remove the input element from the DOM to free-up\n\n await callback(file);\n });\n\n input.click();\n}\n"],
"mappings": "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",
"names": []
}