import {DocumentCodes, SdkVerifierType, SupportedLanguages} from '../generated/zenid-enums.generated.js'; import {EncodedImage, SdkWizardStep, UploadReadyData} from '../generated/zenid-types.generated.js'; import {WorkerCallAction, WorkerInitAction, WorkerResponse, WorkerResponseType, WorkerTerminateAction} from './worker-interfaces.js'; import {LogLevel, zenidLog} from '../zenid-log.js'; // For native function names export enum NativeFunction { ProcessFrame = 'nativeProcessFrame', SetVisualizerVersion = 'nativeSetVisualizerVersion', InitOrRestart = 'nativeInitOrRestart', RestartWithPreviousSettings = 'nativeRestartWithPreviousSettings', GetRenderCommands = 'nativeGetRenderCommands', ReadyForFrame = 'nativeReadyForFrame', GetStateContainerJson = 'nativeGetStateContainerJson', GetFrameProcessingResolution = 'nativeGetFrameProcessingResolution', SubmitExternalResponse = 'nativeSubmitExternalResponse', GetUploadReadyData = 'nativeGetUploadReadyData', SetDebugVisualization = 'nativeSetDebugVisualization', SetLanguage = 'nativeSetLanguage', GetLanguageLocale = 'nativeGetLanguageLocale', SetupResourceLoader = 'nativeSetupResourceLoader', Authorize = 'nativeAuthorize', GetChallengeToken = 'nativeGetChallengeToken', SelectProfile = 'nativeSelectProfile', Unload = 'nativeUnload', GetSdkWizardSteps = 'nativeGetSdkWizardSteps', MeasureAvailableRAM = 'nativePrepareAndEstimateMemory', GetSupportedPageCount = 'nativeGetSupportedPageCount', } export enum DataType { object = 'object', string = 'string', } // Interface for callback storage interface CallbackPair { resolve: (value: any) => void; reject: (reason: any) => void; } export class ZenidSDK { private worker: Worker | null; private callbackMap: Map; private callIdCounter: number; private initialized: boolean = false; private initPromise: Promise | null; private useSingleThread: boolean | null = null; // Global debug state accessible by verifiers public debugVisualizationEnabled: boolean = false; // Transform ZenidSDK to singleton pattern private static instance: ZenidSDK; public static get Instance(): ZenidSDK { if (!ZenidSDK.instance) { ZenidSDK.instance = new ZenidSDK(); } return ZenidSDK.instance; } private constructor() { this.worker = null; this.callbackMap = new Map(); this.callIdCounter = 0; this.initPromise = null; } /** * Initialize the SDK * @returns {Promise} Resolves when SDK is initialized */ public setup(resourcePath: string, useSingleThreaded: boolean, assetUrls?: { wasmUrl?: string; emscriptenJsUrl?: string }): Promise { if (this.initPromise) { return this.initPromise; } this.initPromise = new Promise(async (resolve, reject) => { try { // Use static new URL(..., import.meta.url) pattern for bundler compatibility. // Both Webpack 5+ and Vite recognize this pattern and emit the referenced files // (worker, emscripten glue, wasm) into the customer's bundle output. // Branches must be static literals so bundlers can analyze them. const workerUrl = useSingleThreaded ? new URL('./zenid-worker-st.js', import.meta.url) : new URL('./zenid-worker.js', import.meta.url); const wasmUrl = assetUrls?.wasmUrl ?? (useSingleThreaded ? new URL('./zenid-wasm-st.wasm', import.meta.url).href : new URL('./zenid-wasm-mt.wasm', import.meta.url).href); const emscriptenJsUrl = assetUrls?.emscriptenJsUrl ?? (useSingleThreaded ? new URL('./zenid-wasm-st.js', import.meta.url).href : new URL('./zenid-wasm-mt.js', import.meta.url).href); // Pass absolute URLs to the worker via query string so it doesn't have to // derive them from self.location.href (which is wrong under bundling). workerUrl.searchParams.set('wasm', wasmUrl); workerUrl.searchParams.set('js', emscriptenJsUrl); this.worker = new Worker(workerUrl); this.useSingleThread = useSingleThreaded; // register initialized, but set initialized to true in resolve this.callbackMap.set(WorkerResponseType.Initialized, { resolve: async () => { this.initialized = true; await this.callNative(NativeFunction.SetupResourceLoader, resourcePath); resolve(); }, reject: (error) => { this.initialized = false; reject(error); } }); // Set up message handling this.worker.onmessage = (e: MessageEvent) => this.handleWorkerMessage(e); // Call init function this.worker.postMessage(new WorkerInitAction()); } catch (error) { reject(error); } }); return this.initPromise; } /** * Generic method to call any native function * @param {string} functionName - Name of the native function to call * @param {...any} args - Arguments to pass to the function * @returns {Promise} Resolves with the result of the function call */ public callNative(functionName: string, ...args: any[]): Promise { if (!this.initialized) { return Promise.reject(new Error('SDK not initialized. Call init() first.')); } return new Promise((resolve, reject) => { if (!this.worker) { reject(new Error('Worker not initialized')); return; } const callId = this.getNextCallId(); const transferables = this.findTransferables(args); // Register callback this.registerCallback(callId, resolve, reject); // Call the function const callMessage = new WorkerCallAction(callId, functionName, args, transferables); this.worker.postMessage(callMessage, transferables); }); } /** * Finds ArrayBuffers that can be transferred instead of copied * @param {Array} args - Arguments to search * @returns {Array} - Transferable objects */ private findTransferables(args: any[]): ArrayBuffer[] { const transferables: ArrayBuffer[] = []; const findBuffer = (obj: any): void => { if (!obj) return; if (obj instanceof ArrayBuffer) { transferables.push(obj); } else if (ArrayBuffer.isView(obj) && obj.buffer) { transferables.push(obj.buffer as ArrayBuffer); } else if (typeof obj === DataType.object) { for (const key in obj) { findBuffer(obj[key]); } } }; args.forEach(findBuffer); return transferables; } /** * Handle messages from the worker * @param {MessageEvent} e - Message event */ private handleWorkerMessage(e: MessageEvent): void { const { type, id, result, error } = e.data; // zenidLog.debug("ZenidSDK: Received worker message:", JSON.stringify(e.data)); if (type === WorkerResponseType.Initialized) { const callbacks = this.callbackMap.get(WorkerResponseType.Initialized); if (callbacks) { callbacks.resolve(undefined); this.callbackMap.delete(WorkerResponseType.Initialized); } return; } if (type === WorkerResponseType.Log) { const logLevel = e.data.logLevel as LogLevel; const logMessage = e.data.logMessage as string; zenidLog.log(logLevel, logMessage); return; } if (id === undefined) { //console.warn('Message received with no ID'); return; } const callbacks = this.callbackMap.get(id); if (!callbacks) { zenidLog.warn(`No callback found for message with id: ${id}`); return; } const { resolve, reject } = callbacks; this.callbackMap.delete(id); if (type === WorkerResponseType.Error && error) { reject(new Error(error)); } else if (type === WorkerResponseType.Result) { resolve(result); } } /** * Register callbacks for a specific call ID * @param {string|number} id - Call ID * @param {Function} resolve - Resolve function * @param {Function} reject - Reject function */ private registerCallback(id: string | number, resolve: (value: any) => void, reject: (reason: any) => void): void { this.callbackMap.set(id, { resolve, reject }); } /** * Get a unique call ID * @returns {number} Unique call ID */ private getNextCallId(): number { return this.callIdCounter++; } /** * Process a frame * @param {string} feature - Feature name * @param {Uint8ClampedArray} imageData - Pointer to the input image * @param {number} width - Image width * @param {number} height - Image height * @returns {Promise} - Processing result */ public processFrame(feature: SdkVerifierType, imageData: Uint8ClampedArray, width: number, height: number): Promise { return this.callNative(NativeFunction.ProcessFrame, feature, imageData, width, height); } public destroy(): void { zenidLog.info('Destroying ZenidSDK instance and cleaning up resources'); try { // Terminate the worker if (this.worker) { this.worker.terminate(); this.worker = null; } // Clear all pending callbacks this.callbackMap.clear(); // Reset instance state this.initialized = false; this.initPromise = null; this.callIdCounter = 0; // Release any additional resources here if needed } catch (error) { zenidLog.error('Error during SDK cleanup:', error); } } /** * Kill all threads and workers * This is a utility method to forcefully terminate all resources * Especially useful for iOS WebKit memory.atomic.wait32 bug */ public killAll(): void { zenidLog.info('Forcefully killing all threads and workers'); this.worker?.postMessage(new WorkerTerminateAction()); zenidLog.info('All threads and workers have been terminated'); this.initPromise = null; this.worker = null; } public IsUsingSingleThreaded(): boolean { return this.useSingleThread === true; } // Convenience methods for common operations public setVisualizerVersion( visualizerVersion: number ): Promise { return this.callNative( NativeFunction.SetVisualizerVersion, visualizerVersion ); } public initOrRestart(feature: SdkVerifierType, jsSettings: string): Promise { return this.callNative(NativeFunction.InitOrRestart, feature, jsSettings); } public restartWithPreviousSettings(feature: SdkVerifierType): Promise { return this.callNative(NativeFunction.RestartWithPreviousSettings, feature); } public getRenderCommands(feature: SdkVerifierType, width: number, height: number): Promise { return this.callNative(NativeFunction.GetRenderCommands, feature, width, height); } public readyForFrame(feature: SdkVerifierType): Promise { return this.callNative(NativeFunction.ReadyForFrame, feature); } public submitExternalResponse( feature: SdkVerifierType, request: string ): Promise { return this.callNative(NativeFunction.SubmitExternalResponse, feature, request); } public submitPhotoImage( feature: SdkVerifierType, encodedImage: EncodedImage ): Promise { const request = JSON.stringify(encodedImage); return this.callNative(NativeFunction.SubmitExternalResponse, feature, request); } /** * Get upload-ready data as a JSON string * @param {string} feature - Feature name * @returns {Promise} - Parsed upload-ready data object */ public async getUploadReadyData(feature: SdkVerifierType): Promise { let jsonString = await this.callNative(NativeFunction.GetUploadReadyData, feature); return JSON.parse(jsonString) as UploadReadyData; } public async setDebugVisualization(isEnabled: boolean): Promise { this.debugVisualizationEnabled = isEnabled; return this.callNative(NativeFunction.SetDebugVisualization, isEnabled); } public setLanguage(language: SupportedLanguages): Promise { return this.callNative(NativeFunction.SetLanguage, language); } public getLanguageLocale(): Promise { return this.callNative(NativeFunction.GetLanguageLocale); } public authorize(responseToken: string): Promise { return this.callNative(NativeFunction.Authorize, responseToken); } public unloadAll(): Promise { return this.callNative(NativeFunction.Unload, ""); } public unloadFeature(feature: SdkVerifierType): Promise { return this.callNative(NativeFunction.Unload, feature); } public getChallengeToken(): Promise { return this.callNative(NativeFunction.GetChallengeToken); } public selectProfile(profileName: string): Promise { return this.callNative(NativeFunction.SelectProfile, profileName); } public getStateContainerJson(feature: SdkVerifierType): Promise { return this.callNative(NativeFunction.GetStateContainerJson, feature); } public async getSdkWizardSteps(): Promise { let wizardStepsJson = await this.callNative(NativeFunction.GetSdkWizardSteps); return JSON.parse(wizardStepsJson) as SdkWizardStep[]; } public getFrameProcessingResolution(feature: SdkVerifierType): Promise { return this.callNative(NativeFunction.GetFrameProcessingResolution, feature); } /** * Measure available RAM by attempting to allocate memory blocks * @returns {Promise} Available RAM in bytes */ public measureAvailableRAM(): Promise { return this.callNative(NativeFunction.MeasureAvailableRAM); } /** * Gets the supported page count for a specific document code * @param documentCode - The document code value (from DocumentCodes enum) * @returns Number of supported pages (1 or 2) */ public getSupportedPageCount(documentCode: DocumentCodes): Promise { return this.callNative(NativeFunction.GetSupportedPageCount, documentCode); } } // Export the SDK export default ZenidSDK;