@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
JavaScript
/* @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