UNPKG

@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.

320 lines (314 loc) 9.73 kB
/* @nexim/upload-sdk v1.0.0 */ // src/main.ts import { packageTracer } from "@alwatr/package-tracer"; // src/lib/upload-file-machine.ts import { newFlatomise } from "@alwatr/flatomise"; import { AlwatrFluxStateMachine, AlwatrJsonFetchStateMachine } from "@alwatr/flux"; import { actionState } from "@nexim/action-state"; var UploadFileMachine = class extends AlwatrFluxStateMachine { constructor(options) { super({ name: options.pathWithoutExtension, initialState: "initial" }); this.fileBlob_ = null; this.stateRecord_ = { initial: { request: "loading" }, loading: { loading_failed: "failed", loading_success: "complete" }, failed: { request: "loading" }, complete: {} }; this.actionRecord_ = { on_state_loading_enter: this.uploadFile_.bind(this), on_state_complete_enter: this.clear.bind(this) }; this.apiRequestFetchMachine__ = new AlwatrJsonFetchStateMachine({ name: `upload:${this.name_}`, fetch: this.generateFetchOption_(options) }); this.apiRequestFetchMachine__.subscribe(({ state }) => { if (state === "complete") { this.transition("loading_success"); } else if (state === "failed") { this.transition("loading_failed"); } }); } /** * Uploads an file synchronously and returns the default path upon completion. * Creates a promise wrapper around the asynchronous upload process. * Subscribes to state changes and resolves/rejects based on upload completion state. * * @param file - The blob object containing the file data to upload * @returns Promise that resolves with the default path string if successful, null if failed * * @example * ```ts * const uploadFileMachine = new UploadFileMachine({ * pathWithoutExtension: 'path/to/file', * authHeader: 'Bearer token', * description: 'File description', * apiEndpoint: 'https://api.example.com/upload', * }); * uploadFileMachine.syncUpload(file) * .then((path) => { * console.log('File uploaded successfully to:', path); * }) * .catch(() => { * console.error('File upload failed'); * }); * ``` */ syncUpload(file) { this.logger_.logMethodArgs?.("syncUpload", { rawImage: file }); const flatomise = newFlatomise(); const { unsubscribe } = this.subscribe(({ state }) => { actionState(state, { complete: () => { flatomise.resolve(true); unsubscribe(); }, failed: () => { flatomise.reject(false); unsubscribe(); } }); }); this.upload(file); return flatomise.promise; } /** * Upload file. * * @param file - File to upload * * @example * ```ts * const uploadFileMachine = new UploadFileMachine({ * pathWithoutExtension: 'path/to/file', * authHeader: 'Bearer token', * description: 'File description', * apiEndpoint: 'https://api.example.com/upload', * }); * uploadFileMachine.subscribe(({state}) => { * if (state === 'complete') { * console.log('File uploaded successfully'); * } * else if (state === 'failed') { * console.error('File upload failed'); * } * }); * * uploadFileMachine.upload(file); * ``` */ upload(file) { this.logger_.logMethodArgs?.("upload", { file }); this.fileBlob_ = file; this.transition("request"); } /** * Clear the state. */ clear() { this.logger_.logMethod?.("clear"); this.fileBlob_ = null; this.apiRequestFetchMachine__.clean(); } generateFetchOption_(options) { this.logger_.logMethod?.("generateFetchOption_"); return { removeDuplicate: "auto", retry: 2, retryDelay: 2e3, timeout: 6e4, method: "POST", headers: { "Content-Type": "application/octet-stream", authorization: options.authHeader }, url: options.apiEndpoint, queryParams: { path: options.pathWithoutExtension, description: options.description } }; } uploadFile_() { if (this.fileBlob_ === null) return; this.logger_.logMethod?.("uploadFile_"); const reader = new FileReader(); reader.addEventListener("loadend", (event) => { const buffer = event.target?.result; if (!buffer) { this.transition("loading_failed"); return; } this.apiRequestFetchMachine__.request({ body: buffer }); }); reader.addEventListener("error", () => { this.logger_.error("uploadFile_", "File reading error"); this.transition("loading_failed"); }); reader.readAsArrayBuffer(this.fileBlob_); } }; // src/lib/upload-image-machine.ts import { uploadImagePresetRecord } from "@nexim/upload-types"; var UploadImageMachine = class extends UploadFileMachine { constructor(options) { super(options); /** Temporarily stores the original file before optimization */ this.nonOptimizedFileBlob__ = null; this.actionRecord_ = { ...this.actionRecord_, on_state_loading_enter: this.uploadAfterResize__.bind(this) }; this.uploadImagePresetRecord__ = uploadImagePresetRecord[options.presetName]; this.defaultPath = options.pathWithoutExtension + this.uploadImagePresetRecord__.client.defaultAppendName; } /** * Initiates an image upload process. * The image will be optimized according to the preset configuration before uploading. * * @param file - The image file to upload * * @example * ```ts * const uploadImageMachine = new UploadImageMachine({ * pathWithoutExtension: 'path/to/image', * authHeader: 'Bearer token', * description: 'Image description', * apiEndpoint: 'https://api.example.com/upload', * presetName: 'thumbnail', * }); * uploadImageMachine.subscribe(({state}) => { * if (state === 'complete') { * console.log('Image uploaded successfully'); * } * else if (state === 'failed') { * console.error('Image upload failed'); * } * }); * * uploadImageMachine.upload(file); * ``` */ upload(file) { this.logger_.logMethodArgs?.("upload", { file }); this.nonOptimizedFileBlob__ = file; this.transition("request"); } /** * Handles the image resizing process before uploading. * Called automatically when entering the loading state. */ uploadAfterResize__() { if (this.nonOptimizedFileBlob__ === null) return; this.resizeImage__(this.nonOptimizedFileBlob__).then((blob) => { this.fileBlob_ = blob; this.uploadFile_(); }).catch((error) => { this.logger_.error("resizeImage", "catch", { error }); this.transition("loading_failed"); }); } /** * Generates fetch options for the image upload request. * Extends the base fetch options with image-specific parameters. */ generateFetchOption_(options) { this.logger_.logMethod?.("generateFetchOption_"); return { removeDuplicate: "auto", retry: 2, retryDelay: 2e3, timeout: 3e4, method: "POST", headers: { "Content-Type": "application/octet-stream", authorization: options.authHeader }, url: options.apiEndpoint, queryParams: { path: options.pathWithoutExtension, presetName: options.presetName, description: options.description } }; } /** * Resizes an image according to preset configuration. * Maintains aspect ratio when only width or height is specified. * * @param file - The original image file * @returns A promise resolving to the resized image blob */ resizeImage__(file) { this.logger_.logMethod?.("resizeImage__"); return new Promise((resolve, reject) => { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); if (!context) { resolve(file); return; } let width = this.uploadImagePresetRecord__.client.width; let height = this.uploadImagePresetRecord__.client.height; const image = new Image(); image.onload = () => { const aspect = image.width / image.height; if (width && height === -1) { height = width / aspect; } else if (height && width === -1) { width = height * aspect; } canvas.width = width; canvas.height = height; context.drawImage(image, 0, 0, width, height); canvas.toBlob( (blob) => { resolve(blob); }, file.type, this.uploadImagePresetRecord__.client.quality / 100 ); }; image.addEventListener("error", (error) => { this.logger_.error("resizeImage", "image_onerror", { error }); reject(error); }); image.src = URL.createObjectURL(file); }); } }; // src/lib/virtual-file-input.ts import { createLogger } from "@alwatr/logger"; var logger = createLogger("@nexim/upload-sdk"); function pickAndProcessFile(accept, callback) { logger.logMethod?.("pickAndProcessFile"); const input = document.createElement("input"); input.type = "file"; input.accept = accept; input.addEventListener("change", async () => { logger.logOther?.("changed"); if (input.files === null || input.files.length === 0) return; const file = input.files[0]; logger.logOther?.("pickAndProcessFile.change", { file }); input.remove(); await callback(file); }); input.click(); } // src/main.ts __dev_mode__: packageTracer.add("@nexim/upload-sdk", "1.0.0"); export { UploadFileMachine, UploadImageMachine, pickAndProcessFile }; //# sourceMappingURL=main.mjs.map