import {DocumentCodes, SdkVerifierType, SupportedLanguages} from '../generated/zenid-enums.generated.js'; import {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', SetCameraName = 'nativeSetCameraName', InitOrRestart = 'nativeInitOrRestart', RestartWithPreviousSettings = 'nativeRestartWithPreviousSettings', GetRenderCommands = 'nativeGetRenderCommands', ReadyForFrame = 'nativeReadyForFrame', GetStateContainerJson = 'nativeGetStateContainerJson', GetFrameProcessingResolution = 'nativeGetFrameProcessingResolution', SubmitExternalResponse = 'nativeSubmitExternalResponse', SubmitPhotoFrame = 'nativeSubmitPhotoFrame', 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', } // A tiny module with i32x4.relaxed_trunc_f32x4_s. It validates only in browsers with relaxed SIMD (not Safari). const relaxedSimdProbe = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 15, 1, 13, 0, 65, 1, 253, 15, 65, 2, 253, 15, 253, 128, 2, 11]); // Custom locations of the multi-threaded (or single-threaded) module. The relaxed pair is optional and used only with relaxed SIMD. export type AssetUrls = { wasmUrl?: string; emscriptenJsUrl?: string; relaxedWasmUrl?: string; relaxedEmscriptenJsUrl?: 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 readonly frameProcessingResolutions = new Map>(); 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?: AssetUrls): 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); // The .wasm and .js must come from the same module, so custom URLs switch to relaxed only as a full pair. const relaxedSimd = !useSingleThreaded && WebAssembly.validate(relaxedSimdProbe); const urls = relaxedSimd && assetUrls?.relaxedWasmUrl && assetUrls.relaxedEmscriptenJsUrl ? { wasmUrl: assetUrls.relaxedWasmUrl, emscriptenJsUrl: assetUrls.relaxedEmscriptenJsUrl } : assetUrls; const defaultRelaxed = relaxedSimd && !urls?.wasmUrl && !urls?.emscriptenJsUrl; const wasmUrl = urls?.wasmUrl ?? (useSingleThreaded ? new URL('./zenid-wasm-st.wasm', import.meta.url).href : defaultRelaxed ? new URL('./zenid-wasm-mt-relaxed.wasm', import.meta.url).href : new URL('./zenid-wasm-mt.wasm', import.meta.url).href); const emscriptenJsUrl = urls?.emscriptenJsUrl ?? (useSingleThreaded ? new URL('./zenid-wasm-st.js', import.meta.url).href : defaultRelaxed ? new URL('./zenid-wasm-mt-relaxed.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) { if (type === WorkerResponseType.Error) { const detail = error ?? 'Unknown worker error'; zenidLog.error(`Worker error [${id}]: ${detail}`); const initCallbacks = this.callbackMap.get(WorkerResponseType.Initialized); if (initCallbacks) { this.callbackMap.delete(WorkerResponseType.Initialized); initCallbacks.reject(new Error(detail)); } return; } 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 setCameraName(cameraName: string): Promise { return this.callNative(NativeFunction.SetCameraName, cameraName); } 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); } /** * Submit the GiveMe photo as raw RGBA pixels — skips the JPEG/base64/JSON round trip. * The pixel buffer is transferred (not copied) to the worker and consumed by the call. */ public submitPhotoFrame( feature: SdkVerifierType, imageData: Uint8ClampedArray, width: number, height: number ): Promise { return this.callNative(NativeFunction.SubmitPhotoFrame, feature, imageData, width, height); } /** * 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[]; } // A per-verifier constant set in the native constructor, so one query per feature answers before // the models load and later callers are not queued behind a pending init. public getFrameProcessingResolution(feature: SdkVerifierType): Promise { let resolution = this.frameProcessingResolutions.get(feature); if (!resolution) { resolution = this.callNative(NativeFunction.GetFrameProcessingResolution, feature); this.frameProcessingResolutions.set(feature, resolution); // A failed call must not stay cached; the next caller asks native again. resolution.catch(() => this.frameProcessingResolutions.delete(feature)); } return resolution; } /** * 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;