diff --git a/.gitignore b/.gitignore index bb1cf5f..623ce73 100644 --- a/.gitignore +++ b/.gitignore @@ -302,3 +302,6 @@ Session.vim #mise local mise.local.toml + +# npmrc +.npmrc diff --git a/packages/analytics/CHANGELOG.md b/packages/analytics/CHANGELOG.md index 08ebeec..b0de83f 100644 --- a/packages/analytics/CHANGELOG.md +++ b/packages/analytics/CHANGELOG.md @@ -1,5 +1,11 @@ # @microblink/analytics +## 1.0.1 + +### Patch Changes + +- Added `logErrorEvent(...)` to standardize error and crash pinglets with formatted error messages and optional stack traces. + ## 1.0.0 ### Major Changes diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 4185842..37f14d5 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/analytics", "private": true, - "version": "1.0.0", + "version": "1.0.1", "scripts": { "build": "tsc", "build:publish": "tsc", diff --git a/packages/analytics/src/AnalyticService.test.ts b/packages/analytics/src/AnalyticService.test.ts index 2de1f35..2b01b68 100644 --- a/packages/analytics/src/AnalyticService.test.ts +++ b/packages/analytics/src/AnalyticService.test.ts @@ -53,6 +53,49 @@ describe("AnalyticsService", () => { }); }); + describe("logErrorEvent", () => { + it("queues error pinglets with origin-prefixed messages", async () => { + const error = new Error("aborted"); + + await analyticsService.logErrorEvent({ + origin: "worker.onAbort", + error, + errorType: "Crash", + sessionNumber: 7, + }); + + expect(mockPingFn).toHaveBeenCalledWith({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: 7, + data: { + errorType: "Crash", + errorMessage: "worker.onAbort: aborted", + stackTrace: error.stack, + }, + }); + }); + + it("serializes non-Error objects to JSON", async () => { + await analyticsService.logErrorEvent({ + origin: "worker.onerror", + error: { code: "E_FAIL" }, + errorType: "NonFatal", + }); + + expect(mockPingFn).toHaveBeenCalledWith({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: undefined, + data: { + errorType: "NonFatal", + errorMessage: 'worker.onerror: {"code":"E_FAIL"}', + stackTrace: undefined, + }, + }); + }); + }); + describe("camera events", () => { it("should log camera started event", async () => { await analyticsService.logCameraStartedEvent(); diff --git a/packages/analytics/src/AnalyticService.ts b/packages/analytics/src/AnalyticService.ts index 8c41c0f..b68a9a9 100644 --- a/packages/analytics/src/AnalyticService.ts +++ b/packages/analytics/src/AnalyticService.ts @@ -12,6 +12,7 @@ import type { PingBrowserDeviceInfoData, PingCameraPermissionData, PingCameraPermission, + PingErrorData, } from "./ping"; /** @@ -87,6 +88,22 @@ export class AnalyticService { }; } + #formatErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === "object" && error !== null) { + try { + return JSON.stringify(error); + } catch { + return String(error); + } + } + + return String(error); + } + logCameraStartedEvent() { return this.#safePing( this.#createUxEventPing({ @@ -257,4 +274,27 @@ export class AnalyticService { data: pingData, }); } + + logErrorEvent({ + origin, + error, + errorType, + sessionNumber, + }: { + origin: string; + error: unknown; + errorType: PingErrorData["errorType"]; + sessionNumber?: number; + }) { + return this.#safePing({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber, + data: { + errorType, + errorMessage: `${origin}: ${this.#formatErrorMessage(error)}`, + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + } } diff --git a/packages/blinkcard-core/CHANGELOG.md b/packages/blinkcard-core/CHANGELOG.md index 56b671e..0f2aa5e 100644 --- a/packages/blinkcard-core/CHANGELOG.md +++ b/packages/blinkcard-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @microblink/blinkcard-core +## 3000.0.3 + +### Patch Changes + +- Surfaces worker frame-transfer failures as explicit `FrameTransferError`s through the proxy-worker layer, improving diagnostics for invalid or detached frame buffers. +- Updated dependencies + - @microblink/core-common@1.0.1 + - @microblink/blinkcard-worker@3000.0.3 + - @microblink/blinkcard-wasm@3000.0.3 + ## 3000.0.2 ### Patch Changes diff --git a/packages/blinkcard-core/docs/classes/BlinkCardWorker.md b/packages/blinkcard-core/docs/classes/BlinkCardWorker.md index 4ad2ffc..e28e372 100644 --- a/packages/blinkcard-core/docs/classes/BlinkCardWorker.md +++ b/packages/blinkcard-core/docs/classes/BlinkCardWorker.md @@ -116,11 +116,11 @@ This method initializes the BlinkCard Wasm module. ### reportPinglet() -> **reportPinglet**(`__namedParameters`): `void` +> **reportPinglet**(`pinglet`): `void` #### Parameters -##### \_\_namedParameters +##### pinglet `Ping` diff --git a/packages/blinkcard-core/package.json b/packages/blinkcard-core/package.json index 37cf169..ab112b0 100644 --- a/packages/blinkcard-core/package.json +++ b/packages/blinkcard-core/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkcard-core", "description": "BlinkCard Core SDK", - "version": "3000.0.2", + "version": "3000.0.3", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/blinkcard-ux-manager/CHANGELOG.md b/packages/blinkcard-ux-manager/CHANGELOG.md index 2e9f6df..5111aff 100644 --- a/packages/blinkcard-ux-manager/CHANGELOG.md +++ b/packages/blinkcard-ux-manager/CHANGELOG.md @@ -1,5 +1,16 @@ # @microblink/blinkcard-ux-manager +## 3000.0.3 + +### Patch Changes + +- Keeps the feedback overlay visible whenever no SDK modal is open, preventing it from disappearing during intro, transition, and success states. +- Added non-fatal analytics reporting for UX-manager creation failures, frame-capture failures, `CameraManager` frame-loop errors, and session result retrieval failures. +- Updated dependencies + - @microblink/camera-manager@7.3.1 + - @microblink/analytics@1.0.1 + - @microblink/blinkcard-core@3000.0.3 + ## 3000.0.2 ### Patch Changes @@ -19,6 +30,12 @@ - Adds `destroy()` to `BlinkCardUxManager` for explicit teardown. - Deprecates `rawUiStateKey` and replaces it with two explicit getters: `uiStateKey` returns the stabilized, visible state key (what the UI shows); `mappedUiStateKey` returns the latest raw candidate key from the detector before stabilization (useful for debugging). - Introduces automatic chained UI state transitions after `FIRST_SIDE_CAPTURED`: the manager advances through `FLIP_CARD` to `INTRO_BACK`, then resumes capture for the back side. Integrations that depend on exact UI-state keys or transition timing should account for these new intermediate states. +- Renames several UI state keys. Integrations that reference state keys by name should update accordingly. Each `SENSING_*` state has been split into a framing-feedback state (`CARD_NOT_IN_FRAME_*`) and a new intro guidance state (`INTRO_*`). The old `FLIP_CARD` capture event is now `FIRST_SIDE_CAPTURED`; `FLIP_CARD` continues to exist as a page-transition state: + | Old key | New key(s) | + |---|---| + | `SENSING_FRONT` | `CARD_NOT_IN_FRAME_FRONT`, `INTRO_FRONT` | + | `SENSING_BACK` | `CARD_NOT_IN_FRAME_BACK`, `INTRO_BACK` | + | `FLIP_CARD` (capture event) | `FIRST_SIDE_CAPTURED` | - Updated dependencies - @microblink/camera-manager@7.3.0 - @microblink/blinkcard-core@3000.0.1 diff --git a/packages/blinkcard-ux-manager/package.json b/packages/blinkcard-ux-manager/package.json index b50cebb..cb11eb7 100644 --- a/packages/blinkcard-ux-manager/package.json +++ b/packages/blinkcard-ux-manager/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkcard-ux-manager", "description": "BlinkCard UX Manager provides user feedback based on the blinkcard process results.", - "version": "3000.0.2", + "version": "3000.0.3", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.test.ts b/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.test.ts index a8ac7d0..c2b5c04 100644 --- a/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.test.ts +++ b/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.test.ts @@ -4,22 +4,25 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -// Mock the sleep utility to resolve immediately, preventing tests from hanging -// when code awaits sleep() with fake timers enabled. +// ============================================================================ +// Hoisted Mocks & State +// ============================================================================ + const mockSleep = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +// Mock the sleep utility to resolve immediately, preventing tests from hanging +// when code awaits sleep() with fake timers enabled. vi.mock("@microblink/ux-common/utils", async (importOriginal) => { const actual = await importOriginal(); - return { ...actual, sleep: mockSleep }; + return { + ...actual, + sleep: mockSleep, + }; }); -import type { - ProcessResultWithBuffer, - BlinkCardSessionSettings, - DeviceInfo, -} from "@microblink/blinkcard-core"; -import type { CameraManager } from "@microblink/camera-manager"; +import { AnalyticService } from "@microblink/analytics/AnalyticService"; +import type { ProcessResultWithBuffer } from "@microblink/blinkcard-core"; import { createFakeImageData, enableRafAwareFakeTimers, @@ -31,7 +34,6 @@ import { blinkCardUiStateMap } from "./blinkcard-ui-state"; import { BlinkCardUxManager } from "./BlinkCardUxManager"; import { createBlinkCardCameraHarness, - createBlinkCardManager, createBlinkCardUnitSessionMock, type BlinkCardSessionMock, } from "./test-helpers.integration"; @@ -39,6 +41,7 @@ import { createDeviceInfo, createProcessResult, createScanningResult, + createSessionSettings, } from "./__testdata/blinkcardTestFixtures"; /** @@ -50,66 +53,12 @@ import { * and does not own end-to-end scan flow coverage (see integration tests). */ -type AnalyticServiceMock = { - logDeviceInfo: ReturnType; - logDeviceOrientation: ReturnType; - logCameraStartedEvent: ReturnType; - logCameraClosedEvent: ReturnType; - logCameraPermissionCheck: ReturnType; - logCameraPermissionRequest: ReturnType; - logCameraPermissionUserResponse: ReturnType; - logCameraInputInfo: ReturnType; - sendPinglets: ReturnType; - logErrorMessageEvent: ReturnType; - logStepTimeoutEvent: ReturnType; - logFlashlightState: ReturnType; - logHelpOpenedEvent: ReturnType; - logHelpClosedEvent: ReturnType; - logHelpTooltipDisplayedEvent: ReturnType; - logCloseButtonClickedEvent: ReturnType; - logAlertDisplayedEvent: ReturnType; - logOnboardingDisplayedEvent: ReturnType; -}; - -const analyticsInstances: AnalyticServiceMock[] = []; -const screenOrientationMock = { - type: "portrait-primary", - addEventListener: vi.fn(), - removeEventListener: vi.fn(), -} as unknown as ScreenOrientation; - -vi.mock("@microblink/analytics/AnalyticService", () => ({ - AnalyticService: vi.fn().mockImplementation(() => { - const instance: AnalyticServiceMock = { - logDeviceInfo: vi.fn(), - logDeviceOrientation: vi.fn(), - logCameraStartedEvent: vi.fn(), - logCameraClosedEvent: vi.fn(), - logCameraPermissionCheck: vi.fn(), - logCameraPermissionRequest: vi.fn(), - logCameraPermissionUserResponse: vi.fn(), - logCameraInputInfo: vi.fn(), - sendPinglets: vi.fn(), - logErrorMessageEvent: vi.fn(), - logStepTimeoutEvent: vi.fn(), - logFlashlightState: vi.fn(), - logHelpOpenedEvent: vi.fn(), - logHelpClosedEvent: vi.fn(), - logHelpTooltipDisplayedEvent: vi.fn(), - logCloseButtonClickedEvent: vi.fn(), - logAlertDisplayedEvent: vi.fn(), - logOnboardingDisplayedEvent: vi.fn(), - }; - analyticsInstances.push(instance); - return instance; - }), -})); - +type BlinkCardCameraHarness = ReturnType; type CreateManagerOptions = { - sessionSettings?: BlinkCardSessionSettings; + sessionSettings?: Parameters[0]; showDemoOverlay?: boolean; showProductionOverlay?: boolean; - deviceInfo?: DeviceInfo; + deviceInfo?: ReturnType; }; const trackManager = setupDestroyableTeardown(); @@ -128,547 +77,668 @@ const applyStabilizedUiStateForContractTest = async ( }; const createBlinkCardUxManager = ( - cameraManager: CameraManager, + cameraHarness: BlinkCardCameraHarness, scanningSession: BlinkCardSessionMock, options: CreateManagerOptions = {}, ) => - trackManager(createBlinkCardManager(cameraManager, scanningSession, options)); + trackManager( + new BlinkCardUxManager( + cameraHarness.cameraManager, + scanningSession as unknown as ConstructorParameters< + typeof BlinkCardUxManager + >[1], + {}, + options.sessionSettings ?? createSessionSettings(), + options.showDemoOverlay ?? false, + options.showProductionOverlay ?? false, + options.deviceInfo ?? createDeviceInfo(), + ), + ); + +const createBlinkCardTestContext = ({ + initialCameraPermission, + fakeCameraOptions, + sessionSettings, + managerOptions, +}: { + initialCameraPermission?: "prompt" | "granted" | "denied" | "blocked"; + fakeCameraOptions?: Parameters[0]; + sessionSettings?: Parameters[0]; + managerOptions?: CreateManagerOptions; +} = {}) => { + const cameraHarness = createBlinkCardCameraHarness( + fakeCameraOptions ?? + (initialCameraPermission + ? { initialState: { cameraPermission: initialCameraPermission } } + : undefined), + ); + const scanningSession = createBlinkCardUnitSessionMock(sessionSettings); + const manager = createBlinkCardUxManager( + cameraHarness, + scanningSession, + managerOptions, + ); + + return { + cameraHarness, + scanningSession, + manager, + }; +}; -const getAnalytics = () => analyticsInstances[analyticsInstances.length - 1]; +beforeEach(() => { + vi.clearAllMocks(); +}); -describe("BlinkCardUxManager", () => { - beforeEach(() => { - if (!("orientation" in screen) || !screen.orientation) { - Object.defineProperty(screen, "orientation", { - configurable: true, - value: screenOrientationMock, - }); - } - analyticsInstances.length = 0; - vi.clearAllMocks(); +describe("BlinkCardUxManager - startup and camera analytics", () => { + test("logs device info and playback events", () => { + const logDeviceInfoSpy = vi.spyOn( + AnalyticService.prototype, + "logDeviceInfo", + ); + const { cameraHarness, manager } = createBlinkCardTestContext(); + + expect(logDeviceInfoSpy).toHaveBeenCalledWith(manager.deviceInfo); + logDeviceInfoSpy.mockRestore(); + + const logCameraStartedEventSpy = vi.spyOn( + manager.analytics, + "logCameraStartedEvent", + ); + const logCameraClosedEventSpy = vi.spyOn( + manager.analytics, + "logCameraClosedEvent", + ); + const sendPingletsSpy = vi.spyOn(manager.analytics, "sendPinglets"); + + logCameraStartedEventSpy.mockClear(); + logCameraClosedEventSpy.mockClear(); + sendPingletsSpy.mockClear(); + + cameraHarness.emitPlaybackState("capturing"); + expect(logCameraStartedEventSpy).toHaveBeenCalledTimes(1); + expect(sendPingletsSpy).toHaveBeenCalled(); + + cameraHarness.emitPlaybackState("idle"); + expect(logCameraClosedEventSpy).toHaveBeenCalledTimes(1); }); +}); + +describe("BlinkCardUxManager - package-specific: camera permission analytics", () => { + type PermissionTransitionCase = { + name: string; + initialCameraPermission?: "prompt" | "granted" | "denied" | "blocked"; + nextCameraPermission: "prompt" | "granted" | "denied"; + expectedCheck?: boolean; + expectedRequest: boolean; + expectedResponse?: boolean; + }; + + const permissionTransitionCases: PermissionTransitionCase[] = [ + { + name: "logs permission check and request for undefined -> prompt", + nextCameraPermission: "prompt", + expectedCheck: false, + expectedRequest: true, + }, + { + name: "logs permission check and request for denied -> prompt", + initialCameraPermission: "denied", + nextCameraPermission: "prompt", + expectedCheck: false, + expectedRequest: true, + }, + { + name: "logs user response for prompt -> granted", + initialCameraPermission: "prompt", + nextCameraPermission: "granted", + expectedRequest: false, + expectedResponse: true, + }, + { + name: "logs user response for prompt -> denied", + initialCameraPermission: "prompt", + nextCameraPermission: "denied", + expectedRequest: false, + expectedResponse: false, + }, + ]; + + test.each(permissionTransitionCases)("$name", (testCase) => { + const { cameraHarness, manager } = createBlinkCardTestContext({ + initialCameraPermission: testCase.initialCameraPermission, + }); - describe("construction and configuration", () => { - test("logs device info and playback events", () => { - const { cameraManager, emitPlaybackState } = - createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const deviceInfo = createDeviceInfo(); - const manager = createBlinkCardUxManager(cameraManager, session, { - showProductionOverlay: true, - deviceInfo, - }); + const checkSpy = vi.spyOn(manager.analytics, "logCameraPermissionCheck"); + const requestSpy = vi.spyOn( + manager.analytics, + "logCameraPermissionRequest", + ); + const responseSpy = vi.spyOn( + manager.analytics, + "logCameraPermissionUserResponse", + ); + const sendSpy = vi.spyOn(manager.analytics, "sendPinglets"); + + checkSpy.mockClear(); + requestSpy.mockClear(); + responseSpy.mockClear(); + sendSpy.mockClear(); + + cameraHarness.fakeCameraManager.emitState({ + cameraPermission: testCase.nextCameraPermission, + }); - expect(manager.getShowDemoOverlay()).toBe(false); - expect(manager.getShowProductionOverlay()).toBe(true); + if (testCase.expectedCheck === undefined) { + expect(checkSpy).not.toHaveBeenCalled(); + } else { + expect(checkSpy).toHaveBeenCalledTimes(1); + expect(checkSpy).toHaveBeenCalledWith(testCase.expectedCheck); + } - const analytics = getAnalytics(); - expect(analytics.logDeviceInfo).toHaveBeenCalledWith(deviceInfo); + if (testCase.expectedRequest) { + expect(requestSpy).toHaveBeenCalledTimes(1); + } else { + expect(requestSpy).not.toHaveBeenCalled(); + } - emitPlaybackState("capturing"); - expect(analytics.logCameraStartedEvent).toHaveBeenCalledTimes(1); - expect(analytics.sendPinglets).toHaveBeenCalled(); + if (testCase.expectedResponse === undefined) { + expect(responseSpy).not.toHaveBeenCalled(); + } else { + expect(responseSpy).toHaveBeenCalledTimes(1); + expect(responseSpy).toHaveBeenCalledWith(testCase.expectedResponse); + } - emitPlaybackState("idle"); - expect(analytics.logCameraClosedEvent).toHaveBeenCalledTimes(1); - }); + expect(sendSpy).toHaveBeenCalledTimes(1); }); +}); - describe("package-specific: camera permission analytics", () => { - test("logs permission check and request for undefined -> prompt", () => { - const { cameraManager, fakeCameraManager } = - createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); +describe("BlinkCardUxManager - package-specific: camera input analytics", () => { + beforeEach(() => { + enableRafAwareFakeTimers(); + }); - const analytics = getAnalytics(); - analytics.logCameraPermissionCheck.mockClear(); - analytics.logCameraPermissionRequest.mockClear(); - analytics.logCameraPermissionUserResponse.mockClear(); - analytics.sendPinglets.mockClear(); + afterEach(() => { + vi.useRealTimers(); + }); - fakeCameraManager.emitState({ cameraPermission: "prompt" }); + const fakeCameraOptions = { + initialState: { + selectedCamera: { name: "default-camera", facingMode: "back" as const }, + videoResolution: { width: 1920, height: 1080 }, + }, + }; - expect(analytics.logCameraPermissionCheck).toHaveBeenCalledTimes(1); - expect(analytics.logCameraPermissionCheck).toHaveBeenCalledWith(false); - expect(analytics.logCameraPermissionRequest).toHaveBeenCalledTimes(1); - expect(analytics.logCameraPermissionUserResponse).not.toHaveBeenCalled(); - expect(analytics.sendPinglets).toHaveBeenCalledTimes(1); + test("coalesces orientation-like updates into one camera input ping", async () => { + const { cameraHarness, manager } = createBlinkCardTestContext({ + fakeCameraOptions, }); + const logCameraInputInfoSpy = vi.spyOn( + manager.analytics, + "logCameraInputInfo", + ); - test("logs permission check and request for denied -> prompt", () => { - const { cameraManager, fakeCameraManager } = createBlinkCardCameraHarness( - { - initialState: { cameraPermission: "denied" }, - }, - ); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); - - const analytics = getAnalytics(); - analytics.logCameraPermissionCheck.mockClear(); - analytics.logCameraPermissionRequest.mockClear(); - analytics.logCameraPermissionUserResponse.mockClear(); - analytics.sendPinglets.mockClear(); - - fakeCameraManager.emitState({ cameraPermission: "prompt" }); - - expect(analytics.logCameraPermissionCheck).toHaveBeenCalledTimes(1); - expect(analytics.logCameraPermissionCheck).toHaveBeenCalledWith(false); - expect(analytics.logCameraPermissionRequest).toHaveBeenCalledTimes(1); - expect(analytics.logCameraPermissionUserResponse).not.toHaveBeenCalled(); - expect(analytics.sendPinglets).toHaveBeenCalledTimes(1); - }); + logCameraInputInfoSpy.mockClear(); - test("logs user response for prompt -> granted", () => { - const { cameraManager, fakeCameraManager } = createBlinkCardCameraHarness( - { - initialState: { cameraPermission: "prompt" }, - }, - ); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); + cameraHarness.emitCameraState({ + videoResolution: { width: 1080, height: 1920 }, + extractionArea: { x: 0, y: 0, width: 1080, height: 1920 }, + }); - const analytics = getAnalytics(); - analytics.logCameraPermissionCheck.mockClear(); - analytics.logCameraPermissionRequest.mockClear(); - analytics.logCameraPermissionUserResponse.mockClear(); - analytics.sendPinglets.mockClear(); + cameraHarness.emitCameraState({ + videoResolution: { width: 1920, height: 1080 }, + }); - fakeCameraManager.emitState({ cameraPermission: "granted" }); + cameraHarness.emitCameraState({ + videoResolution: { width: 1080, height: 1920 }, + extractionArea: { x: 0, y: 5, width: 1080, height: 1910 }, + }); - expect(analytics.logCameraPermissionCheck).not.toHaveBeenCalled(); - expect(analytics.logCameraPermissionRequest).not.toHaveBeenCalled(); - expect(analytics.logCameraPermissionUserResponse).toHaveBeenCalledTimes( - 1, - ); - expect(analytics.logCameraPermissionUserResponse).toHaveBeenCalledWith( - true, - ); - expect(analytics.sendPinglets).toHaveBeenCalledTimes(1); + expect(logCameraInputInfoSpy).not.toHaveBeenCalled(); + await vi.runOnlyPendingTimersAsync(); + + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(1); + expect(logCameraInputInfoSpy).toHaveBeenCalledWith({ + deviceId: "default-camera", + cameraFacing: "Back", + cameraFrameWidth: 1080, + cameraFrameHeight: 1920, + roiWidth: 1080, + roiHeight: 1910, + viewPortAspectRatio: 1080 / 1910, }); + }); - test("logs user response for prompt -> denied", () => { - const { cameraManager, fakeCameraManager } = createBlinkCardCameraHarness( - { - initialState: { cameraPermission: "prompt" }, - }, - ); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); + test("sends immediate and debounced pings for separated updates", async () => { + const { cameraHarness, manager } = createBlinkCardTestContext({ + fakeCameraOptions, + }); + const logCameraInputInfoSpy = vi.spyOn( + manager.analytics, + "logCameraInputInfo", + ); - const analytics = getAnalytics(); - analytics.logCameraPermissionCheck.mockClear(); - analytics.logCameraPermissionRequest.mockClear(); - analytics.logCameraPermissionUserResponse.mockClear(); - analytics.sendPinglets.mockClear(); + logCameraInputInfoSpy.mockClear(); - fakeCameraManager.emitState({ cameraPermission: "denied" }); + cameraHarness.emitCameraState({ + selectedCamera: { name: "default-camera", facingMode: "back" }, + }); + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(1); - expect(analytics.logCameraPermissionCheck).not.toHaveBeenCalled(); - expect(analytics.logCameraPermissionRequest).not.toHaveBeenCalled(); - expect(analytics.logCameraPermissionUserResponse).toHaveBeenCalledTimes( - 1, - ); - expect(analytics.logCameraPermissionUserResponse).toHaveBeenCalledWith( - false, - ); - expect(analytics.sendPinglets).toHaveBeenCalledTimes(1); + cameraHarness.emitCameraState({ + extractionArea: { x: 0, y: 5, width: 1080, height: 1910 }, }); + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(1); + + await vi.runOnlyPendingTimersAsync(); + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(2); }); - describe("package-specific: camera input analytics", () => { - beforeEach(() => { - enableRafAwareFakeTimers(); - }); + test("does not send delayed camera input ping after reset or observer cleanup", async () => { + const resetContext = createBlinkCardTestContext({ fakeCameraOptions }); + const resetSpy = vi.spyOn( + resetContext.manager.analytics, + "logCameraInputInfo", + ); - afterEach(() => { - vi.useRealTimers(); + resetSpy.mockClear(); + resetContext.cameraHarness.emitCameraState({ + videoResolution: { width: 1000, height: 500 }, }); - - test("coalesces orientation-like updates into one camera input ping", async () => { - const { cameraManager, emitCameraState } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); - - const analytics = getAnalytics(); - - emitCameraState({ - videoResolution: { width: 1080, height: 1920 }, - extractionArea: { x: 0, y: 0, width: 1080, height: 1920 }, - }); - - emitCameraState({ videoResolution: { width: 1920, height: 1080 } }); - - emitCameraState({ - videoResolution: { width: 1080, height: 1920 }, - extractionArea: { x: 0, y: 5, width: 1080, height: 1910 }, - }); - - expect(analytics.logCameraInputInfo).not.toHaveBeenCalled(); - await vi.runOnlyPendingTimersAsync(); - - expect(analytics.logCameraInputInfo).toHaveBeenCalledTimes(1); - expect(analytics.logCameraInputInfo).toHaveBeenCalledWith({ - deviceId: "default-camera", - cameraFacing: "Back", - cameraFrameWidth: 1080, - cameraFrameHeight: 1920, - roiWidth: 1080, - roiHeight: 1910, - viewPortAspectRatio: 1080 / 1910, - }); + resetContext.manager.reset(); + await vi.runOnlyPendingTimersAsync(); + expect(resetSpy).not.toHaveBeenCalled(); + + const cleanupContext = createBlinkCardTestContext({ fakeCameraOptions }); + const cleanupSpy = vi.spyOn( + cleanupContext.manager.analytics, + "logCameraInputInfo", + ); + + cleanupSpy.mockClear(); + cleanupContext.cameraHarness.emitCameraState({ + videoResolution: { width: 1000, height: 500 }, }); + cleanupContext.manager.cleanupAllObservers(); + await vi.runOnlyPendingTimersAsync(); + expect(cleanupSpy).not.toHaveBeenCalled(); + }); +}); - test("sends immediate + debounced pings for separated updates", async () => { - const { cameraManager, emitCameraState } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); - - const analytics = getAnalytics(); +describe("BlinkCardUxManager - package-specific: camera frame-capture loop errors", () => { + test("logs a non-fatal ping when camera manager reports a frame-loop error", () => { + const { cameraHarness, manager } = createBlinkCardTestContext(); + const logErrorEventSpy = vi.spyOn(manager.analytics, "logErrorEvent"); + const sendPingletsSpy = vi.spyOn(manager.analytics, "sendPinglets"); + const error = new Error( + "Frame capture callback did not return an ArrayBuffer.", + ); - emitCameraState({ - selectedCamera: { name: "default-camera", facingMode: "back" }, - }); - expect(analytics.logCameraInputInfo).toHaveBeenCalledTimes(1); + logErrorEventSpy.mockClear(); + sendPingletsSpy.mockClear(); - emitCameraState({ - extractionArea: { x: 0, y: 5, width: 1080, height: 1910 }, - }); - expect(analytics.logCameraInputInfo).toHaveBeenCalledTimes(1); + cameraHarness.fakeCameraManager.emitError(error); - await vi.runOnlyPendingTimersAsync(); - expect(analytics.logCameraInputInfo).toHaveBeenCalledTimes(2); + expect(logErrorEventSpy).toHaveBeenCalledWith({ + origin: "cameraManager.error", + error, + errorType: "NonFatal", }); + expect(sendPingletsSpy).toHaveBeenCalledTimes(1); + }); +}); - test("does not send delayed camera input ping after reset", async () => { - const { cameraManager, emitCameraState } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); +describe("BlinkCardUxManager - session lifecycle: reset behavior", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); - emitCameraState({ videoResolution: { width: 1000, height: 500 } }); - manager.reset(); - await vi.runOnlyPendingTimersAsync(); + afterEach(() => { + vi.useRealTimers(); + }); - expect(getAnalytics().logCameraInputInfo).not.toHaveBeenCalled(); - }); + test("resetScanningSession starts camera stream when inactive", async () => { + const { cameraHarness, manager } = createBlinkCardTestContext(); + cameraHarness.setIsActive(false); - test("does not send delayed camera input ping after observer cleanup", async () => { - const { cameraManager, emitCameraState } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + await manager.resetScanningSession(true); - emitCameraState({ videoResolution: { width: 1000, height: 500 } }); - manager.cleanupAllObservers(); - await vi.runOnlyPendingTimersAsync(); + expect(cameraHarness.startCameraStream).toHaveBeenCalledTimes(1); + expect(cameraHarness.startFrameCapture).toHaveBeenCalledTimes(1); + }); - expect(getAnalytics().logCameraInputInfo).not.toHaveBeenCalled(); - }); + test("resetScanningSession skips frame capture when startFrameCapture is false", async () => { + const { manager, cameraHarness } = createBlinkCardTestContext(); + + await manager.resetScanningSession(false); + + expect(cameraHarness.startFrameCapture).not.toHaveBeenCalled(); }); - describe("callbacks and public API", () => { - beforeEach(() => { - enableRafAwareFakeTimers(); - }); + test("reset clears all callbacks", async () => { + const { cameraHarness, manager, scanningSession } = + createBlinkCardTestContext(); + const uiStateSpy = vi.fn(); + const resultSpy = vi.fn(); + const frameProcessSpy = vi.fn(); + const errorSpy = vi.fn(); - afterEach(() => { - vi.useRealTimers(); - }); + manager.addOnUiStateChangedCallback(uiStateSpy); + manager.addOnResultCallback(resultSpy); + manager.addOnFrameProcessCallback(frameProcessSpy); + manager.addOnErrorCallback(errorSpy); - test("registers and cleans up UI state callbacks via RAF loop", async () => { - const { cameraManager } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + manager.reset(); - const uiStateSpy = vi.fn(); - const cleanup = manager.addOnUiStateChangedCallback(uiStateSpy); + scanningSession.process.mockResolvedValue( + createProcessResult({ + inputImageAnalysisResult: { + processingStatus: "awaiting-other-side", + }, + }), + ); - // Inject a deterministic UI state into the stabilizer to assert RAF callback wiring. - await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); + await cameraHarness.emitFrame(createFakeImageData()); + await flushUiRaf(); - expect(uiStateSpy).toHaveBeenCalledWith( - blinkCardUiStateMap.BLUR_DETECTED, - ); - expect(uiStateSpy).toHaveBeenCalledTimes(1); + manager.setTimeoutDuration(1000); + cameraHarness.emitPlaybackState("capturing"); + vi.advanceTimersByTime(1000); - cleanup(); + expect(uiStateSpy).not.toHaveBeenCalled(); + expect(resultSpy).not.toHaveBeenCalled(); + expect(frameProcessSpy).not.toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); + }); +}); - // Inject another UI state; cleanup should prevent further callback notifications. - await applyStabilizedUiStateForContractTest( - manager, - "CARD_NOT_IN_FRAME_FRONT", - ); +describe("BlinkCardUxManager - timeout behavior", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); - expect(uiStateSpy).toHaveBeenCalledTimes(1); - }); + afterEach(() => { + vi.useRealTimers(); + }); - test("forwards help/alert analytics events", () => { - const { cameraManager } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); - - const analytics = getAnalytics(); - - manager.logHelpOpened(); - manager.logHelpClosed(true); - manager.logHelpTooltipDisplayed(); - manager.logCloseButtonClicked(); - manager.logAlertDisplayed("NetworkError"); - manager.logOnboardingDisplayed(); - - expect(analytics.logHelpOpenedEvent).toHaveBeenCalledTimes(1); - expect(analytics.logHelpClosedEvent).toHaveBeenCalledWith(true); - expect(analytics.logHelpTooltipDisplayedEvent).toHaveBeenCalledTimes(1); - expect(analytics.logCloseButtonClickedEvent).toHaveBeenCalledTimes(1); - expect(analytics.logAlertDisplayedEvent).toHaveBeenCalledWith( - "NetworkError", - ); - expect(analytics.logOnboardingDisplayedEvent).toHaveBeenCalledTimes(1); - }); + test("triggers timeout and error callback", () => { + const { cameraHarness, manager } = createBlinkCardTestContext(); + const timeoutSpy = vi.spyOn(manager.analytics, "logStepTimeoutEvent"); + const errorCallback = vi.fn(); - test("reset() clears all callbacks", async () => { - const { cameraManager, emitFrame } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + manager.addOnErrorCallback(errorCallback); + manager.setTimeoutDuration(1000); - const uiStateSpy = vi.fn(); - const resultSpy = vi.fn(); - const frameProcessSpy = vi.fn(); - const errorSpy = vi.fn(); + timeoutSpy.mockClear(); + cameraHarness.emitPlaybackState("capturing"); + vi.advanceTimersByTime(1000); - manager.addOnUiStateChangedCallback(uiStateSpy); - manager.addOnResultCallback(resultSpy); - manager.addOnFrameProcessCallback(frameProcessSpy); - manager.addOnErrorCallback(errorSpy); + expect(errorCallback).toHaveBeenCalledWith("timeout"); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); + expect(timeoutSpy).toHaveBeenCalledTimes(1); + }); - manager.reset(); + test("clears timeout when stopping capture", () => { + const { cameraHarness, manager } = createBlinkCardTestContext(); + const errorCallback = vi.fn(); - vi.mocked(session.process).mockResolvedValue( - createProcessResult({ - inputImageAnalysisResult: { - processingStatus: "awaiting-other-side", - }, - }), - ); + manager.addOnErrorCallback(errorCallback); + manager.setTimeoutDuration(1000); - await emitFrame(createFakeImageData()); - await flushUiRaf(); + cameraHarness.emitPlaybackState("capturing"); + cameraHarness.emitPlaybackState("idle"); + vi.advanceTimersByTime(1100); - expect(uiStateSpy).not.toHaveBeenCalled(); - expect(resultSpy).not.toHaveBeenCalled(); - expect(frameProcessSpy).not.toHaveBeenCalled(); - expect(errorSpy).not.toHaveBeenCalled(); - }); + expect(errorCallback).not.toHaveBeenCalled(); }); - describe("timeout behavior", () => { - beforeEach(() => { - enableRafAwareFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); + test("does not set timeout when timeout duration is null", () => { + const { cameraHarness, manager } = createBlinkCardTestContext(); + const errorCallback = vi.fn(); - test("triggers timeout on capturing and clears on idle", async () => { - const { cameraManager, emitPlaybackState, stopFrameCapture } = - createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + manager.addOnErrorCallback(errorCallback); + manager.setTimeoutDuration(null); - const errorSpy = vi.fn(); - manager.addOnErrorCallback(errorSpy); - manager.setTimeoutDuration(1000); + cameraHarness.emitPlaybackState("capturing"); + vi.advanceTimersByTime(20_000); - emitPlaybackState("capturing"); - await vi.advanceTimersByTimeAsync(1000); + expect(errorCallback).not.toHaveBeenCalled(); + }); +}); - expect(errorSpy).toHaveBeenCalledWith("timeout"); - expect(stopFrameCapture).toHaveBeenCalled(); - expect(getAnalytics().logStepTimeoutEvent).toHaveBeenCalled(); +describe("BlinkCardUxManager - state transitions: shared callback contracts", () => { + beforeEach(() => { + enableRafAwareFakeTimers(); + }); - errorSpy.mockClear(); - emitPlaybackState("capturing"); - emitPlaybackState("idle"); - await vi.advanceTimersByTimeAsync(1000); - expect(errorSpy).not.toHaveBeenCalled(); - }); + afterEach(() => { + vi.useRealTimers(); }); - describe("session lifecycle: reset behavior", () => { - test("starts camera stream when inactive", async () => { - const { - cameraManager, - setIsActive, - startCameraStream, - startFrameCapture, - } = createBlinkCardCameraHarness(); - setIsActive(false); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + test("logs error message events when UI state changes to an error state", async () => { + const { manager } = createBlinkCardTestContext(); + const logErrorMessageEventSpy = vi.spyOn( + manager.analytics, + "logErrorMessageEvent", + ); - await manager.resetScanningSession(true); + logErrorMessageEventSpy.mockClear(); + await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); - expect(startCameraStream).toHaveBeenCalledTimes(1); - expect(startFrameCapture).toHaveBeenCalledTimes(1); - }); + expect(logErrorMessageEventSpy).toHaveBeenCalledWith("EliminateBlur"); + }); - test("skips frame capture when startFrameCapture is false", async () => { - const { cameraManager, startFrameCapture } = - createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + test("triggers short haptic feedback when the RAF loop transitions to an error state", async () => { + const { manager } = createBlinkCardTestContext(); + const shortSpy = vi.spyOn( + manager.getHapticFeedbackManager(), + "triggerShort", + ); - await manager.resetScanningSession(false); - expect(startFrameCapture).not.toHaveBeenCalled(); - }); + shortSpy.mockClear(); + await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); + + expect(shortSpy).toHaveBeenCalled(); }); +}); - describe("state transitions and capture flow", () => { - beforeEach(() => { - enableRafAwareFakeTimers(); - }); +describe("BlinkCardUxManager - state transitions: capture flow integration", () => { + beforeEach(() => { + enableRafAwareFakeTimers(); + }); - afterEach(() => { - vi.useRealTimers(); - }); + afterEach(() => { + vi.useRealTimers(); + }); - test("FIRST_SIDE_CAPTURED stops capture immediately and INTRO_BACK resumes capture", async () => { - const { cameraManager, emitFrame, stopFrameCapture, startFrameCapture } = - createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + test("stops capture after first-side success and resumes capture on INTRO_BACK", async () => { + const { cameraHarness, manager, scanningSession } = + createBlinkCardTestContext(); - const processResult = createProcessResult({ + scanningSession.process.mockResolvedValue( + createProcessResult({ inputImageAnalysisResult: { processingStatus: "awaiting-other-side", }, - }); - vi.mocked(session.process).mockResolvedValue(processResult); + }), + ); - await emitFrame(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); - // stopFrameCapture called immediately from frame processing side-effects - expect(stopFrameCapture).toHaveBeenCalledTimes(1); - expect(startFrameCapture).not.toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalledTimes(1); + expect(cameraHarness.startFrameCapture).not.toHaveBeenCalled(); - // Intro states should resume frame capture when they become active. - await applyStabilizedUiStateForContractTest(manager, "INTRO_BACK"); + await applyStabilizedUiStateForContractTest(manager, "INTRO_BACK"); - expect(startFrameCapture).toHaveBeenCalled(); - }); - - test("CARD_CAPTURED stops capture from frame processing and emits result when CARD_CAPTURED UI state is applied", async () => { - const { cameraManager, emitFrame, stopFrameCapture } = - createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + expect(cameraHarness.startFrameCapture).toHaveBeenCalled(); + }); - const resultCallback = vi.fn(); - manager.addOnResultCallback(resultCallback); + test("emits scan result when CARD_CAPTURED UI state is applied", async () => { + const { cameraHarness, manager, scanningSession } = + createBlinkCardTestContext(); + const resultCallback = vi.fn(); - const processResult = createProcessResult({ + manager.addOnResultCallback(resultCallback); + scanningSession.process.mockResolvedValue( + createProcessResult({ resultCompleteness: { scanningStatus: "card-scanned" }, - }); - const scanResult = createScanningResult(); - - vi.mocked(session.process).mockResolvedValue(processResult); - vi.mocked(session.getResult).mockResolvedValue(scanResult); + }), + ); + scanningSession.getResult.mockResolvedValue(createScanningResult()); - await emitFrame(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); - // stopFrameCapture called immediately - expect(stopFrameCapture).toHaveBeenCalledTimes(1); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalledTimes(1); - // Inject CARD_CAPTURED directly to isolate result emission behavior. - // sleep(uiState.minDuration) is mocked to resolve immediately in this file. - await applyStabilizedUiStateForContractTest(manager, "CARD_CAPTURED"); + await applyStabilizedUiStateForContractTest(manager, "CARD_CAPTURED"); - expect(resultCallback).toHaveBeenCalledWith(scanResult); - }); - - test("logs error message events when UI state changes to an error state", async () => { - const { cameraManager } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); - - // logErrorMessageEvent is called from #updateUiState when the RAF loop - // applies an error state, not from the processing loop. - await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); + expect(resultCallback).toHaveBeenCalledWith(createScanningResult()); + }); - expect(getAnalytics().logErrorMessageEvent).toHaveBeenCalledWith( - "EliminateBlur", + test("fires result_retrieval_failed error when getResult rejects", async () => { + const { manager, scanningSession } = createBlinkCardTestContext(); + const errorCallback = vi.fn(); + const resultCallback = vi.fn(); + + manager.addOnErrorCallback(errorCallback); + manager.addOnResultCallback(resultCallback); + scanningSession.getResult.mockRejectedValue( + new Error("Worker RPC failure"), + ); + + await applyStabilizedUiStateForContractTest(manager, "CARD_CAPTURED"); + + await vi.waitFor(() => { + expect(scanningSession.getResult).toHaveBeenCalled(); + expect(errorCallback).toHaveBeenCalledWith("result_retrieval_failed"); + expect(resultCallback).not.toHaveBeenCalled(); + expect(scanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: "ux.getSessionResult: Worker RPC failure", + }), + }), ); + expect(scanningSession.sendPinglets).toHaveBeenCalledTimes(1); }); + }); - test("triggers short haptic feedback when the RAF loop transitions to an error state", async () => { - const { cameraManager } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + test("reports non-fatal pinglets when frame processing rejects with a recoverable error", async () => { + const { cameraHarness, scanningSession } = createBlinkCardTestContext(); + scanningSession.process.mockRejectedValue( + new Error("Worker process failure"), + ); + + await expect( + cameraHarness.emitFrame(createFakeImageData()), + ).rejects.toThrow("Worker process failure"); + + expect(scanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: "ux.frameCapture: Worker process failure", + }), + }), + ); + expect(scanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); - const hapticManager = manager.getHapticFeedbackManager(); - const shortSpy = vi.spyOn(hapticManager, "triggerShort"); + test("reports non-fatal pinglets when frame processing rejects with a WASM runtime error", async () => { + const { cameraHarness, scanningSession } = createBlinkCardTestContext(); + scanningSession.process.mockRejectedValue( + new Error("table index is out of bounds RuntimeError"), + ); + + await expect( + cameraHarness.emitFrame(createFakeImageData()), + ).rejects.toThrow("table index is out of bounds RuntimeError"); + + expect(scanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: + "ux.frameCapture: table index is out of bounds RuntimeError", + }), + }), + ); + expect(scanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); - // Inject an error state directly to verify haptic behavior on RAF transition. - await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); + test("reports non-fatal pinglets when frame processing rejects with a frame transfer error", async () => { + const { cameraHarness, scanningSession } = createBlinkCardTestContext(); + const frameTransferError = new Error("Failed to transfer frame to worker"); + frameTransferError.name = "FrameTransferError"; - expect(shortSpy).toHaveBeenCalled(); - }); + scanningSession.process.mockRejectedValue(frameTransferError); - test("fires result_retrieval_failed error and does not emit result when getResult rejects", async () => { - const { cameraManager } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - const manager = createBlinkCardUxManager(cameraManager, session); + await expect( + cameraHarness.emitFrame(createFakeImageData()), + ).rejects.toThrow("Failed to transfer frame to worker"); - const errorCallback = vi.fn(); - const resultCallback = vi.fn(); - manager.addOnErrorCallback(errorCallback); - manager.addOnResultCallback(resultCallback); + expect(scanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: "ux.frameCapture: Failed to transfer frame to worker", + }), + }), + ); + expect(scanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); - vi.mocked(session.getResult).mockRejectedValue( - new Error("Worker RPC failure"), - ); + test("skips overlapping process calls while a previous frame is still processing", async () => { + const { cameraHarness, scanningSession } = createBlinkCardTestContext(); + let resolveFirst!: (value: ProcessResultWithBuffer) => void; + const firstProcessPromise = new Promise( + (resolve) => { + resolveFirst = resolve; + }, + ); - await applyStabilizedUiStateForContractTest(manager, "CARD_CAPTURED"); + scanningSession.process.mockReturnValueOnce(firstProcessPromise); - await vi.waitFor(() => { - expect(session.getResult).toHaveBeenCalled(); - expect(errorCallback).toHaveBeenCalledWith("result_retrieval_failed"); - expect(resultCallback).not.toHaveBeenCalled(); - }); - }); + const firstFramePromise = cameraHarness.emitFrame(createFakeImageData()); + const secondFrameResult = await cameraHarness.emitFrame( + createFakeImageData(), + ); - test("drops second frame while first is still processing (busy guard)", async () => { - const { cameraManager, emitFrame } = createBlinkCardCameraHarness(); - const session = createBlinkCardUnitSessionMock(); - createBlinkCardUxManager(cameraManager, session); + expect(secondFrameResult).toBeUndefined(); + expect(scanningSession.process).toHaveBeenCalledTimes(1); - let resolveFirst!: (value: ProcessResultWithBuffer) => void; - const firstProcessPromise = new Promise( - (resolve) => { - resolveFirst = resolve; + resolveFirst( + createProcessResult({ + inputImageAnalysisResult: { + processingStatus: "detection-failed", }, - ); + }), + ); + await firstFramePromise; + await tickRaf(); - vi.mocked(session.process).mockReturnValueOnce(firstProcessPromise); + expect(scanningSession.process).toHaveBeenCalledTimes(1); + }); - const firstFramePromise = emitFrame(createFakeImageData()); - const secondFrameResult = await emitFrame(createFakeImageData()); + test("skips further process calls after terminal card capture", async () => { + const { cameraHarness, scanningSession } = createBlinkCardTestContext(); - expect(secondFrameResult).toBeUndefined(); - expect(session.process).toHaveBeenCalledTimes(1); + scanningSession.process.mockResolvedValue( + createProcessResult({ + resultCompleteness: { scanningStatus: "card-scanned" }, + }), + ); + scanningSession.getResult.mockResolvedValue(createScanningResult()); - resolveFirst( - createProcessResult({ - inputImageAnalysisResult: { - processingStatus: "detection-failed", - }, - }), - ); - await firstFramePromise; - await tickRaf(); + await cameraHarness.emitFrame(createFakeImageData()); + await tickRaf(); + await cameraHarness.emitFrame(createFakeImageData()); - expect(session.process).toHaveBeenCalledTimes(1); - }); + expect(scanningSession.process).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.ts b/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.ts index 691d740..8749c57 100644 --- a/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.ts +++ b/packages/blinkcard-ux-manager/src/core/BlinkCardUxManager.ts @@ -197,8 +197,11 @@ export class BlinkCardUxManager { const removeFrameCaptureCallback = this.cameraManager.addFrameCaptureCallback(this.#frameCaptureCallback); + const removeCameraManagerErrorCallback = + this.cameraManager.addErrorCallback(this.#handleCameraManagerError); this.#cleanupCallbacks.add(removeFrameCaptureCallback); + this.#cleanupCallbacks.add(removeCameraManagerErrorCallback); this.startUiUpdateLoop(); } @@ -712,6 +715,14 @@ export class BlinkCardUxManager { } return processResult.arrayBuffer; + } catch (error) { + await this.#analytics.logErrorEvent({ + origin: "ux.frameCapture", + error, + errorType: "NonFatal", + }); + await this.#analytics.sendPinglets(); + throw error; } finally { if (this.#processingLifecycleState === "busy") { this.#processingLifecycleState = "ready"; @@ -719,6 +730,15 @@ export class BlinkCardUxManager { } }; + #handleCameraManagerError = (error: Error) => { + void this.#analytics.logErrorEvent({ + origin: "cameraManager.error", + error, + errorType: "NonFatal", + }); + void this.#analytics.sendPinglets(); + }; + /** * Handles frame-level side effects without touching the UI directly: * queues analytics pings and stops frame capture on success. @@ -854,7 +874,6 @@ export class BlinkCardUxManager { "result_retrieval_failed", "onError", ); - void this.#analytics.sendPinglets(); } } }; @@ -956,7 +975,17 @@ export class BlinkCardUxManager { * @returns The result. */ async getSessionResult(): Promise { - return this.scanningSession.getResult(); + try { + return await this.scanningSession.getResult(); + } catch (error) { + await this.#analytics.logErrorEvent({ + origin: "ux.getSessionResult", + error, + errorType: "NonFatal", + }); + await this.#analytics.sendPinglets(); + throw error; + } } /** diff --git a/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.test.ts b/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.test.ts index a94c39e..61a7d17 100644 --- a/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.test.ts +++ b/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.test.ts @@ -71,4 +71,31 @@ describe("createBlinkCardUxManager", () => { deviceInfo, ); }); + + test("best-effort reports setup failures through the scanning session", async () => { + const cameraManager = {} as CameraManager; + const scanningSession = createFakeScanningSession({ + overrides: { + getSettings: vi.fn().mockRejectedValue(new Error("rpc failed")), + }, + }); + + await expect( + createBlinkCardUxManager( + cameraManager, + scanningSession as unknown as RemoteScanningSession, + ), + ).rejects.toThrow("rpc failed"); + + expect(scanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "Crash", + errorMessage: "ux.createBlinkCardUxManager: rpc failed", + }), + }), + ); + expect(scanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.ts b/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.ts index c32302a..65cb1be 100644 --- a/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.ts +++ b/packages/blinkcard-ux-manager/src/core/createBlinkCardUxManager.ts @@ -31,21 +31,52 @@ export const createBlinkCardUxManager = async ( scanningSession: RemoteScanningSession, options: BlinkCardUxManagerOptions = {}, ): Promise => { - const [sessionSettings, showDemoOverlay, showProductionOverlay, deviceInfo] = - await Promise.all([ + try { + const [ + sessionSettings, + showDemoOverlay, + showProductionOverlay, + deviceInfo, + ] = await Promise.all([ scanningSession.getSettings(), scanningSession.showDemoOverlay(), scanningSession.showProductionOverlay(), getDeviceInfo(), ]); - return new BlinkCardUxManager( - cameraManager, - scanningSession, - options, - sessionSettings, - showDemoOverlay, - showProductionOverlay, - deviceInfo, - ); + return new BlinkCardUxManager( + cameraManager, + scanningSession, + options, + sessionSettings, + showDemoOverlay, + showProductionOverlay, + deviceInfo, + ); + } catch (error) { + try { + await scanningSession.ping({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + data: { + errorType: "Crash", + errorMessage: `ux.createBlinkCardUxManager: ${ + error instanceof Error ? error.message : String(error) + }`, + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + } catch (pingError) { + console.warn("Failed to report error pinglet:", pingError); + throw error; + } + + try { + await scanningSession.sendPinglets(); + } catch (sendError) { + console.warn("Failed to flush error pinglets:", sendError); + } + + throw error; + } }; diff --git a/packages/blinkcard-ux-manager/src/core/test-helpers.integration.ts b/packages/blinkcard-ux-manager/src/core/test-helpers.integration.ts index 57824b9..7df4a8e 100644 --- a/packages/blinkcard-ux-manager/src/core/test-helpers.integration.ts +++ b/packages/blinkcard-ux-manager/src/core/test-helpers.integration.ts @@ -7,13 +7,14 @@ import type { BlinkCardSessionSettings, DeviceInfo, ProcessResultWithBuffer, - RemoteScanningSession, } from "@microblink/blinkcard-core"; import type { CameraManager } from "@microblink/camera-manager"; import { + createFakeCameraHarness, createFakeScanningSession, + type CreateFakeCameraManagerOptions, + type FakeCameraHarness, type FakeScanningSession, - FakeCameraManager, } from "@microblink/test-utils"; import { BlinkCardUxManager } from "./BlinkCardUxManager"; import { @@ -21,28 +22,12 @@ import { createSessionSettings, } from "./__testdata/blinkcardTestFixtures"; -export type CameraInputState = { - selectedCamera?: { name: string; facingMode?: "front" | "back" }; - videoResolution?: { width: number; height: number }; - extractionArea?: { x: number; y: number; width: number; height: number }; -}; - -export type BlinkCardCameraHarness = { - cameraManager: CameraManager; - fakeCameraManager: FakeCameraManager; - emitPlaybackState: (playbackState: "idle" | "playback" | "capturing") => void; - emitFrame: (imageData: ImageData) => Promise; - emitCameraState: (nextState: Partial) => void; - setIsActive: (value: boolean) => void; - stopFrameCapture: FakeCameraManager["stopFrameCapture"]; - startFrameCapture: FakeCameraManager["startFrameCapture"]; - startCameraStream: FakeCameraManager["startCameraStream"]; -}; +export type BlinkCardCameraHarness = FakeCameraHarness; export const createBlinkCardCameraHarness = ( - fakeCameraOptions?: ConstructorParameters[0], -): BlinkCardCameraHarness => { - const fakeCameraManager = new FakeCameraManager( + fakeCameraOptions?: CreateFakeCameraManagerOptions, +): BlinkCardCameraHarness => + createFakeCameraHarness( fakeCameraOptions ?? { initialState: { selectedCamera: { name: "default-camera", facingMode: "back" }, @@ -51,22 +36,6 @@ export const createBlinkCardCameraHarness = ( }, ); - return { - cameraManager: fakeCameraManager as unknown as CameraManager, - fakeCameraManager, - emitPlaybackState: (playbackState) => - fakeCameraManager.emitPlaybackState(playbackState), - emitFrame: (imageData) => fakeCameraManager.emitFrame(imageData), - emitCameraState: (nextState) => fakeCameraManager.emitState(nextState), - setIsActive: (value) => { - fakeCameraManager.isActive = value; - }, - stopFrameCapture: fakeCameraManager.stopFrameCapture, - startFrameCapture: fakeCameraManager.startFrameCapture, - startCameraStream: fakeCameraManager.startCameraStream, - }; -}; - export type BlinkCardSessionMock = FakeScanningSession< ProcessResultWithBuffer, BlinkCardSessionSettings, @@ -78,61 +47,46 @@ export type CreateBlinkCardIntegrationContextOptions = { showDemoOverlay?: boolean; showProductionOverlay?: boolean; deviceInfo?: DeviceInfo; - fakeCameraOptions?: ConstructorParameters[0]; + fakeCameraOptions?: CreateFakeCameraManagerOptions; sessionOverrides?: Partial; }; -export type BlinkCardIntegrationContext = { - manager: BlinkCardUxManager; - fakeCameraManager: FakeCameraManager; - scanningSession: BlinkCardSessionMock; -}; - -type CreateBlinkCardSessionMockOptions = { - sessionSettings?: BlinkCardSessionSettings; - showDemoOverlay?: boolean; - showProductionOverlay?: boolean; - sessionOverrides?: Partial; -}; - -export const createBlinkCardSessionMock = ( - options: CreateBlinkCardSessionMockOptions = {}, +export const createBlinkCardUnitSessionMock = ( + sessionSettings: BlinkCardSessionSettings = createSessionSettings(), ): BlinkCardSessionMock => createFakeScanningSession< ProcessResultWithBuffer, BlinkCardSessionSettings, BlinkCardScanningResult + >({ + settings: sessionSettings, + showDemoOverlay: false, + showProductionOverlay: true, + }); + +export const createBlinkCardIntegrationContext = ( + options: CreateBlinkCardIntegrationContextOptions = {}, +): { + manager: BlinkCardUxManager; + fakeCameraManager: BlinkCardCameraHarness["fakeCameraManager"]; + scanningSession: BlinkCardSessionMock; +} => { + const cameraHarness = createBlinkCardCameraHarness(options.fakeCameraOptions); + const scanningSession = createFakeScanningSession< + ProcessResultWithBuffer, + BlinkCardSessionSettings, + BlinkCardScanningResult >({ settings: options.sessionSettings ?? createSessionSettings(), showDemoOverlay: options.showDemoOverlay ?? false, showProductionOverlay: options.showProductionOverlay ?? false, overrides: options.sessionOverrides, }); - -export const createBlinkCardUnitSessionMock = ( - sessionSettings: BlinkCardSessionSettings = createSessionSettings(), -): BlinkCardSessionMock => - createBlinkCardSessionMock({ - sessionSettings, - showDemoOverlay: false, - showProductionOverlay: true, - }); - -type CreateBlinkCardManagerOptions = { - sessionSettings?: BlinkCardSessionSettings; - showDemoOverlay?: boolean; - showProductionOverlay?: boolean; - deviceInfo?: DeviceInfo; -}; - -export const createBlinkCardManager = ( - cameraManager: CameraManager, - scanningSession: BlinkCardSessionMock, - options: CreateBlinkCardManagerOptions = {}, -): BlinkCardUxManager => - new BlinkCardUxManager( - cameraManager, - scanningSession as unknown as RemoteScanningSession, + const manager = new BlinkCardUxManager( + cameraHarness.cameraManager, + scanningSession as unknown as ConstructorParameters< + typeof BlinkCardUxManager + >[1], {}, options.sessionSettings ?? createSessionSettings(), options.showDemoOverlay ?? false, @@ -140,27 +94,6 @@ export const createBlinkCardManager = ( options.deviceInfo ?? createDeviceInfo(), ); -export const createBlinkCardIntegrationContext = ( - options: CreateBlinkCardIntegrationContextOptions = {}, -): BlinkCardIntegrationContext => { - const cameraHarness = createBlinkCardCameraHarness(options.fakeCameraOptions); - const scanningSession = createBlinkCardSessionMock({ - sessionSettings: options.sessionSettings, - showDemoOverlay: options.showDemoOverlay, - showProductionOverlay: options.showProductionOverlay, - sessionOverrides: options.sessionOverrides, - }); - const manager = createBlinkCardManager( - cameraHarness.cameraManager, - scanningSession, - { - sessionSettings: options.sessionSettings, - showDemoOverlay: options.showDemoOverlay, - showProductionOverlay: options.showProductionOverlay, - deviceInfo: options.deviceInfo, - }, - ); - return { manager, fakeCameraManager: cameraHarness.fakeCameraManager, diff --git a/packages/blinkcard-ux-manager/src/ui/BlinkCardFeedbackUi.tsx b/packages/blinkcard-ux-manager/src/ui/BlinkCardFeedbackUi.tsx index 733ac37..e51c3c9 100644 --- a/packages/blinkcard-ux-manager/src/ui/BlinkCardFeedbackUi.tsx +++ b/packages/blinkcard-ux-manager/src/ui/BlinkCardFeedbackUi.tsx @@ -15,13 +15,7 @@ import { Switch, } from "solid-js"; import { createWithSignal } from "solid-zustand"; -import { - blinkCardPageTransitionKeys, - blinkCardUiIntroStateKeys, - BlinkCardUiState, - BlinkCardUiStateKey, - blinkCardUiSuccessKeys, -} from "../core/blinkcard-ui-state"; +import { BlinkCardUiState } from "../core/blinkcard-ui-state"; import { LocalizationProvider, PartialLocalizationStrings, @@ -90,20 +84,8 @@ export const BlinkCardFeedbackUi: Component<{ const isProcessing = () => playbackState() === "capturing"; - /** - * These UI states pause frame processing, however we treat them as if we are - * still in processing state from a UX perspective - */ - const pseudoProcessingKeys: BlinkCardUiStateKey[] = [ - ...blinkCardUiIntroStateKeys, - ...blinkCardPageTransitionKeys, - ...blinkCardUiSuccessKeys, - ]; - - // Processing is stopped, but we still want to show the feedback - const shouldShowFeedback = () => { - return isProcessing() || pseudoProcessingKeys.includes(uiState().key); - }; + // TODO: Cover cases where frame processing is paused by 3rd party modal dialogs + const shouldShowFeedback = () => !isModalOpen(); const displayTimeoutModal = () => Boolean(store.showTimeoutModal) && store.errorState === "timeout"; diff --git a/packages/blinkcard-wasm/CHANGELOG.md b/packages/blinkcard-wasm/CHANGELOG.md index d053472..a0698e7 100644 --- a/packages/blinkcard-wasm/CHANGELOG.md +++ b/packages/blinkcard-wasm/CHANGELOG.md @@ -1,5 +1,7 @@ # @microblink/blinkcard-wasm +## 3000.0.3 + ## 3000.0.2 ### Patch Changes diff --git a/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.js b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.js index e0115f8..8c33827 100644 --- a/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.js +++ b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.js @@ -1,7 +1,7 @@ async function createModule(moduleArg={}){var moduleRtn;var h=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope,m=ba&&self.name?.startsWith("em-pthread");let ca;(ca=h).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);h.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof m&&m||a||async function(b){async function c(l,n){var q;(q=h).dataFileDownloads??(q.dataFileDownloads={});try{var p=await fetch(l)}catch(x){throw Error(`Network Error: ${l}`,{e:x});}if(!p.ok)throw Error(`${p.status}: ${p.url}`);q=[];n=Number(p.headers.get("Content-Length")??n);let u=0;h.setStatus?.("Downloading data...");for(p=p.body.getReader();;){var {done:z,value:v}=await p.read();if(z)break; -q.push(v);u+=v.length;h.dataFileDownloads[l]={loaded:u,total:n};let x=0,F=0;for(var w of Object.values(h.dataFileDownloads))x+=w.loaded,F+=w.total;h.setStatus?.(`Downloading data... (${x}/${F})`)}l=new Uint8Array(q.map(x=>x.length).reduce((x,F)=>x+F,0));w=0;for(const x of q)l.set(x,w),w+=x.length;return l.buffer}async function d(l){l.FS_createPath("/","microblink",!0,!0);l.FS_createPath("/microblink","blinkcard",!0,!0);for(var n of b.files)l.addRunDependency(`fp ${n.filename}`);l.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.data"); -l.preloadResults??(l.preloadResults={});l.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.data"]={qd:!1};k||=await g;(async function(q){if(!q)throw Error("Loading data file failed.");if(q.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");q=new Uint8Array(q);for(var p of b.files){var u=p.filename;l.FS_createDataFile(u,null,q.subarray(p.start,p.end),!0,!0,!0);l.removeRunDependency(`fp ${u}`)}l.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.data")})(k)} +q.push(v);u+=v.length;h.dataFileDownloads[l]={loaded:u,total:n};let x=0,F=0;for(var w of Object.values(h.dataFileDownloads))x+=w.loaded,F+=w.total;h.setStatus?.(`Downloading data... (${x}/${F})`)}l=new Uint8Array(q.map(x=>x.length).reduce((x,F)=>x+F,0));w=0;for(const x of q)l.set(x,w),w+=x.length;return l.buffer}async function d(l){l.FS_createPath("/","microblink",!0,!0);l.FS_createPath("/microblink","blinkcard",!0,!0);for(var n of b.files)l.addRunDependency(`fp ${n.filename}`);l.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.data"); +l.preloadResults??(l.preloadResults={});l.preloadResults["/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.data"]={qd:!1};k||=await g;(async function(q){if(!q)throw Error("Loading data file failed.");if(q.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");q=new Uint8Array(q);for(var p of b.files){var u=p.filename;l.FS_createDataFile(u,null,q.subarray(p.start,p.end),!0,!0,!0);l.removeRunDependency(`fp ${u}`)}l.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.data")})(k)} "object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=h.locateFile?.("BlinkCardModule.data","")??"BlinkCardModule.data",f=b.remote_package_size,g,k=h.getPreloadedPackage?.(e,f);k||(g=c(e,f));if(h.calledRun)d(h);else{let l;((l=h).preRun??(l.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkcard/Model_1118d9d674e23996f70c6416b2bf5a6ce6ef24a6ad2c92f0ddd1e198e5f05305.strop", start:0,end:19102},{filename:"/microblink/blinkcard/Model_349432d66ef2b216155673b634f7d5c47795bed35719b954f726b5f0856740f3.strop",start:19102,end:67419},{filename:"/microblink/blinkcard/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:67419,end:336140},{filename:"/microblink/blinkcard/Model_5065f3a3bc1c2fece482ee66e9275fc198b9be239547e08b6086c59f347ca72f.strop",start:336140,end:2591295},{filename:"/microblink/blinkcard/Model_830c13896f96c1cb6d5cad725f44e6aae470f8672d640d20b3272ed4bb839699.strop", start:2591295,end:2848318},{filename:"/microblink/blinkcard/Model_9f6734be0f5c1e4f3c6c621f4a72db8241feaf7c8705dc68a9cc07a7b634ee85.strop",start:2848318,end:2931954},{filename:"/microblink/blinkcard/Model_abb3e9795585a24a9bbd1dd41ec97daa2e1d7d42087aacd981411fd8b26bf493.strop",start:2931954,end:3264110},{filename:"/microblink/blinkcard/Model_b9263312a9b623d1a3b75b643ccdcbc36aae52c278d721443468147c50e44583.strop",start:3264110,end:3532384},{filename:"/microblink/blinkcard/Model_c99d9c4b96e424a1b5a17758060a8116912f78d14318e471e0606709227b9497.strop", @@ -133,7 +133,7 @@ const b=Symbol.dispose;b&&(a[b]=a["delete"])})(); Object.assign(Ze.prototype,{Dc(a){this.rc&&(a=this.rc(a));return a},lc(a){this.nb?.(a)},ob:ve,$a:function(a){function b(){return this.Mb?Ye(this.Wa.wb,{Xa:this.Vc,Ua:c,hb:this,bb:a}):Ye(this.Wa.wb,{Xa:this,Ua:a})}var c=this.Dc(a);if(!c)return this.lc(a),null;var d=Xe(this.Wa,c);if(void 0!==d){if(0===d.Qa.count.value)return d.Qa.Ua=c,d.Qa.bb=a,d.clone();d=d.clone();this.lc(a);return d}d=this.Wa.Cc(c);d=Ke[d];if(!d)return b.call(this);d=this.Lb?d.yc:d.pointerType;var e=Ve(c,this.Wa,d.Wa);return null=== e?b.call(this):this.Mb?Ye(d.Wa.wb,{Xa:d,Ua:e,hb:this,bb:a}):Ye(d.Wa.wb,{Xa:d,Ua:e})}});(async function(){Lf=new Nf;if(!m){Qc("library_fetch_init");try{Pf=await Of()}catch(a){Pf=!1}finally{Pc("library_fetch_init")}}})();m||(oa=h.wasmMemory?h.wasmMemory:new WebAssembly.Memory({initial:(h.INITIAL_MEMORY||209715200)/65536,maximum:32768,shared:!0}),pa());h.noExitRuntime&&(Xc=h.noExitRuntime);h.preloadPlugins&&(Id=h.preloadPlugins);h.print&&(ja=h.print);h.printErr&&(r=h.printErr);h.wasmBinary&&(ka=h.wasmBinary); h.thisProgram&&(da=h.thisProgram);if(h.preInit)for("function"==typeof h.preInit&&(h.preInit=[h.preInit]);0{var l=b?nd(jd(a+"/"+b)):a,n=`cp ${l}`;Qc(n);try{var q=c;"string"==typeof c&&(q=await Hd(c));q=await Jd(q,l);k?.();f||oe(a,b,q,d,e,g)}finally{Pc(n)}};h.FS_unlink=(...a)=>ge(...a);h.FS_createPath=(...a)=>me(...a);h.FS_createDevice=(...a)=>Ma(...a); -h.FS_createDataFile=(...a)=>oe(...a);h.FS_createLazyFile=(...a)=>qe(...a);var zf=[qc,Yc,ed,ab,bb,cb,db,eb,fb,gb,Qb,Rb,Sb,dc,ec,gc,hc,ic,jc],yf={205290:(a,b,c,d)=>{a=U(a);b=U(b);c=U(c);d=U(d);throw Error(a+b+c+d);},205506:(a,b)=>{a=U(a);b=U(b);throw Error(a+b);},205616:a=>{a=U(a);throw Error(a);},205684:a=>{a=U(a);throw Error(a);}};function lc(){var a=self.navigator.userAgent,b=sd(a)+1,c=vc(b);Y(a,c,b);return c}function Ub(){var a=h.allowedThreads;return a?a:navigator.hardwareConcurrency} +h.FS_createDataFile=(...a)=>oe(...a);h.FS_createLazyFile=(...a)=>qe(...a);var zf=[qc,Yc,ed,ab,bb,cb,db,eb,fb,gb,Qb,Rb,Sb,dc,ec,gc,hc,ic,jc],yf={205306:(a,b,c,d)=>{a=U(a);b=U(b);c=U(c);d=U(d);throw Error(a+b+c+d);},205522:(a,b)=>{a=U(a);b=U(b);throw Error(a+b);},205632:a=>{a=U(a);throw Error(a);},205700:a=>{a=U(a);throw Error(a);}};function lc(){var a=self.navigator.userAgent,b=sd(a)+1,c=vc(b);Y(a,c,b);return c}function Ub(){var a=h.allowedThreads;return a?a:navigator.hardwareConcurrency} function kc(){var a=stackTrace(),b=sd(a)+1,c=vc(b);Y(a,c,b);return c}var tc,Aa,uc,vc,wc,xc,xa,Ea,yc,zc,Ac,Bc,Cc,Dc,Ec,Fc,Gc,Hc,Ic;h.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=44384;var Ya;function nc(a,b){var c=Hc();try{O(a)(b)}catch(d){Fc(c);if(d!==d+0)throw d;Dc(1,0)}}function pc(a,b,c,d){var e=Hc();try{O(a)(b,c,d)}catch(f){Fc(e);if(f!==f+0)throw f;Dc(1,0)}}function oc(a,b,c){var d=Hc();try{O(a)(b,c)}catch(e){Fc(d);if(e!==e+0)throw e;Dc(1,0)}} function mc(a,b,c,d){var e=Hc();try{return O(a)(b,c,d)}catch(f){Fc(e);if(f!==f+0)throw f;Dc(1,0)}} function va(){function a(){h.calledRun=!0;if(!ma&&(Ka(),qa?.(h),h.onRuntimeInitialized?.(),!m)){if(h.postRun)for("function"==typeof h.postRun&&(h.postRun=[h.postRun]);h.postRun.length;){var b=h.postRun.shift();cd.push(b)}Lc(cd)}}if(0{setTimeout(()=>h.setStatus(""),1);a()},1)):a()}} diff --git a/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.wasm b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.wasm index 06dda2f..a35a170 100755 Binary files a/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.wasm and b/packages/blinkcard-wasm/dist/advanced-threads/BlinkCardModule.wasm differ diff --git a/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.js b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.js index 91553c8..061a063 100644 --- a/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.js +++ b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.js @@ -1,12 +1,12 @@ async function createModule(moduleArg={}){var moduleRtn;var l=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope;let ca;(ca=l).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);l.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof ENVIRONMENT_IS_PTHREAD&&ENVIRONMENT_IS_PTHREAD||a||async function(b){async function c(k,m){var p;(p=l).dataFileDownloads??(p.dataFileDownloads={});try{var n=await fetch(k)}catch(x){throw Error(`Network Error: ${k}`,{e:x});}if(!n.ok)throw Error(`${n.status}: ${n.url}`);p=[];m=Number(n.headers.get("Content-Length")??m);let t=0;l.setStatus?.("Downloading data...");for(n=n.body.getReader();;){var {done:z, value:u}=await n.read();if(z)break;p.push(u);t+=u.length;l.dataFileDownloads[k]={loaded:t,total:m};let x=0,E=0;for(var w of Object.values(l.dataFileDownloads))x+=w.loaded,E+=w.total;l.setStatus?.(`Downloading data... (${x}/${E})`)}k=new Uint8Array(p.map(x=>x.length).reduce((x,E)=>x+E,0));w=0;for(const x of p)k.set(x,w),w+=x.length;return k.buffer}async function d(k){k.FS_createPath("/","microblink",!0,!0);k.FS_createPath("/microblink","blinkcard",!0,!0);for(var m of b.files)k.addRunDependency(`fp ${m.filename}`); -k.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.data"]={Jc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p); -for(var n of b.files){var t=n.filename;k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0, -location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkCardModule.data","")??"BlinkCardModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkcard/Model_1118d9d674e23996f70c6416b2bf5a6ce6ef24a6ad2c92f0ddd1e198e5f05305.strop",start:0,end:19102},{filename:"/microblink/blinkcard/Model_349432d66ef2b216155673b634f7d5c47795bed35719b954f726b5f0856740f3.strop", -start:19102,end:67419},{filename:"/microblink/blinkcard/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:67419,end:336140},{filename:"/microblink/blinkcard/Model_5065f3a3bc1c2fece482ee66e9275fc198b9be239547e08b6086c59f347ca72f.strop",start:336140,end:2591295},{filename:"/microblink/blinkcard/Model_830c13896f96c1cb6d5cad725f44e6aae470f8672d640d20b3272ed4bb839699.strop",start:2591295,end:2848318},{filename:"/microblink/blinkcard/Model_9f6734be0f5c1e4f3c6c621f4a72db8241feaf7c8705dc68a9cc07a7b634ee85.strop", -start:2848318,end:2931954},{filename:"/microblink/blinkcard/Model_abb3e9795585a24a9bbd1dd41ec97daa2e1d7d42087aacd981411fd8b26bf493.strop",start:2931954,end:3264110},{filename:"/microblink/blinkcard/Model_b9263312a9b623d1a3b75b643ccdcbc36aae52c278d721443468147c50e44583.strop",start:3264110,end:3532384},{filename:"/microblink/blinkcard/Model_c99d9c4b96e424a1b5a17758060a8116912f78d14318e471e0606709227b9497.strop",start:3532384,end:6369917},{filename:"/microblink/blinkcard/Model_cc1fab8df49d9a21de6c7b76ccf0dac40b17fcfb7073cc520eca073cbf8e33e9.strop", -start:6369917,end:6373648},{filename:"/microblink/blinkcard/Model_f132d1bd7614b1274fafb8a41ec6c047b84b2a43654ae2da5ddd78a2765601c6.strop",start:6373648,end:7208473},{filename:"/microblink/blinkcard/bin-database_1.0.zzip",start:7208473,end:8578325}],remote_package_size:8578325})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; +k.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.data"]={Jc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t, +null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkCardModule.data", +"")??"BlinkCardModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkcard/Model_1118d9d674e23996f70c6416b2bf5a6ce6ef24a6ad2c92f0ddd1e198e5f05305.strop",start:0,end:19102},{filename:"/microblink/blinkcard/Model_349432d66ef2b216155673b634f7d5c47795bed35719b954f726b5f0856740f3.strop",start:19102,end:67419},{filename:"/microblink/blinkcard/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop", +start:67419,end:336140},{filename:"/microblink/blinkcard/Model_5065f3a3bc1c2fece482ee66e9275fc198b9be239547e08b6086c59f347ca72f.strop",start:336140,end:2591295},{filename:"/microblink/blinkcard/Model_830c13896f96c1cb6d5cad725f44e6aae470f8672d640d20b3272ed4bb839699.strop",start:2591295,end:2848318},{filename:"/microblink/blinkcard/Model_9f6734be0f5c1e4f3c6c621f4a72db8241feaf7c8705dc68a9cc07a7b634ee85.strop",start:2848318,end:2931954},{filename:"/microblink/blinkcard/Model_abb3e9795585a24a9bbd1dd41ec97daa2e1d7d42087aacd981411fd8b26bf493.strop", +start:2931954,end:3264110},{filename:"/microblink/blinkcard/Model_b9263312a9b623d1a3b75b643ccdcbc36aae52c278d721443468147c50e44583.strop",start:3264110,end:3532384},{filename:"/microblink/blinkcard/Model_c99d9c4b96e424a1b5a17758060a8116912f78d14318e471e0606709227b9497.strop",start:3532384,end:6369917},{filename:"/microblink/blinkcard/Model_cc1fab8df49d9a21de6c7b76ccf0dac40b17fcfb7073cc520eca073cbf8e33e9.strop",start:6369917,end:6373648},{filename:"/microblink/blinkcard/Model_f132d1bd7614b1274fafb8a41ec6c047b84b2a43654ae2da5ddd78a2765601c6.strop", +start:6373648,end:7208473},{filename:"/microblink/blinkcard/bin-database_1.0.zzip",start:7208473,end:8578325}],remote_package_size:8578325})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; (function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkCardModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; if(aa||ba){try{fa=(new URL(".",ea)).href}catch{}ba&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});ha=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);}}var ja=console.log.bind(console),q=console.error.bind(console),ka,la=!1,ma,na,oa,r,v,A,pa,B,D,qa,sa,G,ta,ua=!1; function va(){var a=wa.buffer;r=new Int8Array(a);A=new Int16Array(a);v=new Uint8Array(a);pa=new Uint16Array(a);B=new Int32Array(a);D=new Uint32Array(a);qa=new Float32Array(a);sa=new Float64Array(a);G=new BigInt64Array(a);ta=new BigUint64Array(a)}var wa;function H(a){l.onAbort?.(a);a="Aborted("+a+")";q(a);la=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa?.(a);throw a;}var xa; @@ -90,7 +90,7 @@ const b=Symbol.dispose;b&&(a[b]=a["delete"])})(); Object.assign(Kc.prototype,{cc(a){this.Tb&&(a=this.Tb(a));return a},Nb(a){this.Sa?.(a)},Ta:gc,Ga:function(a){function b(){return this.qb?Jc(this.Ca.$a,{Da:this.tc,Aa:c,Na:this,Ia:a}):Jc(this.Ca.$a,{Da:this,Aa:a})}var c=this.cc(a);if(!c)return this.Nb(a),null;var d=Ic(this.Ca,c);if(void 0!==d){if(0===d.wa.count.value)return d.wa.Aa=c,d.wa.Ia=a,d.clone();d=d.clone();this.Nb(a);return d}d=this.Ca.bc(c);d=vc[d];if(!d)return b.call(this);d=this.pb?d.Yb:d.pointerType;var e=Gc(c,this.Ca,d.Ca);return null=== e?b.call(this):this.qb?Jc(d.Ca.$a,{Da:d,Aa:e,Na:this,Ia:a}):Jc(d.Ca.$a,{Da:d,Aa:e})}});(async function(){Y=new yd;nb("library_fetch_init");try{Ad=await zd()}catch(a){Ad=!1}finally{mb("library_fetch_init")}})();wa=l.wasmMemory?l.wasmMemory:new WebAssembly.Memory({initial:(l.INITIAL_MEMORY||209715200)/65536,maximum:32768});va();l.noExitRuntime&&(Ga=l.noExitRuntime);l.preloadPlugins&&(ob=l.preloadPlugins);l.print&&(ja=l.print);l.printErr&&(q=l.printErr);l.wasmBinary&&(ka=l.wasmBinary); l.thisProgram&&(da=l.thisProgram);if(l.preInit)for("function"==typeof l.preInit&&(l.preInit=[l.preInit]);0{var k=b?Ra(Na(a+"/"+b)):a,m=`cp ${k}`;nb(m);try{var p=c;"string"==typeof c&&(p=await jb(c));p=await pb(p,k);h?.();f||Yb(a,b,p,d,e,g)}finally{mb(m)}};l.FS_unlink=(...a)=>Pb(...a);l.FS_createPath=(...a)=>Wb(...a);l.FS_createDevice=(...a)=>Zb(...a); -l.FS_createDataFile=(...a)=>Yb(...a);l.FS_createLazyFile=(...a)=>ac(...a);var Kd={203602:(a,b,c,d)=>{a=a?M(v,a):"";b=b?M(v,b):"";c=c?M(v,c):"";d=d?M(v,d):"";throw Error(a+b+c+d);},203818:(a,b)=>{a=a?M(v,a):"";b=b?M(v,b):"";throw Error(a+b);},203928:a=>{a=a?M(v,a):"";throw Error(a);},203996:a=>{a=a?M(v,a):"";throw Error(a);}},Nc,Cd,Oc,cb,Ld,Md,Nd,Od,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; +l.FS_createDataFile=(...a)=>Yb(...a);l.FS_createLazyFile=(...a)=>ac(...a);var Kd={203586:(a,b,c,d)=>{a=a?M(v,a):"";b=b?M(v,b):"";c=c?M(v,c):"";d=d?M(v,d):"";throw Error(a+b+c+d);},203802:(a,b)=>{a=a?M(v,a):"";b=b?M(v,b):"";throw Error(a+b);},203912:a=>{a=a?M(v,a):"";throw Error(a);},203980:a=>{a=a?M(v,a):"";throw Error(a);}},Nc,Cd,Oc,cb,Ld,Md,Nd,Od,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; var Td={F:(a,b)=>K(a)(b),t:function(a,b,c){Ka=c;try{var d=R(a);switch(b){case 0:var e=La();if(0>e)break;for(;sb[e];)e++;return Ib(d,e).Za;case 1:case 2:return 0;case 3:return d.flags;case 4:return e=La(),d.flags|=e,0;case 12:return e=La(),A[e+0>>1]=2,0;case 13:case 14:return 0}return-28}catch(f){if("undefined"==typeof T||"ErrnoError"!==f.name)throw f;return-f.Fa}},Z:function(a,b){try{var c=R(a),d=c.node,e=c.xa.Pa;a=e?c:d;e??=d.za.Pa;Gb(e);var f=e(a);return dc(b,f)}catch(g){if("undefined"==typeof T|| "ErrnoError"!==g.name)throw g;return-g.Fa}},_:function(a,b,c){Ka=c;try{var d=R(a);switch(b){case 21509:return d.Ba?0:-59;case 21505:if(!d.Ba)return-59;if(d.Ba.Wa.kc){a=[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];var e=La();B[e>>2]=25856;B[e+4>>2]=5;B[e+8>>2]=191;B[e+12>>2]=35387;for(var f=0;32>f;f++)r[e+f+17]=a[f]||0}return 0;case 21510:case 21511:case 21512:return d.Ba?0:-59;case 21506:case 21507:case 21508:if(!d.Ba)return-59;if(d.Ba.Wa.lc)for(e=La(),a=[],f=0;32> f;f++)a.push(r[e+f+17]);return 0;case 21519:if(!d.Ba)return-59;e=La();return B[e>>2]=0;case 21520:return d.Ba?-28:-59;case 21537:case 21531:e=La();if(!d.xa.jc)throw new O(59);return d.xa.jc(d,b,e);case 21523:if(!d.Ba)return-59;d.Ba.Wa.mc&&(f=[24,80],e=La(),A[e>>1]=f[0],A[e+2>>1]=f[1]);return 0;case 21524:return d.Ba?0:-59;case 21515:return d.Ba?0:-59;default:return-28}}catch(g){if("undefined"==typeof T||"ErrnoError"!==g.name)throw g;return-g.Fa}},X:function(a,b){try{return a=a?M(v,a):"",dc(b,Qb(a, diff --git a/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.wasm b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.wasm index f030ef6..f4d6810 100755 Binary files a/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.wasm and b/packages/blinkcard-wasm/dist/advanced/BlinkCardModule.wasm differ diff --git a/packages/blinkcard-wasm/dist/basic/BlinkCardModule.js b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.js index 35c6eb1..017becc 100644 --- a/packages/blinkcard-wasm/dist/basic/BlinkCardModule.js +++ b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.js @@ -1,12 +1,12 @@ async function createModule(moduleArg={}){var moduleRtn;var l=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope;let ca;(ca=l).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);l.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof ENVIRONMENT_IS_PTHREAD&&ENVIRONMENT_IS_PTHREAD||a||async function(b){async function c(k,m){var p;(p=l).dataFileDownloads??(p.dataFileDownloads={});try{var n=await fetch(k)}catch(x){throw Error(`Network Error: ${k}`,{e:x});}if(!n.ok)throw Error(`${n.status}: ${n.url}`);p=[];m=Number(n.headers.get("Content-Length")??m);let t=0;l.setStatus?.("Downloading data...");for(n=n.body.getReader();;){var {done:z, value:u}=await n.read();if(z)break;p.push(u);t+=u.length;l.dataFileDownloads[k]={loaded:t,total:m};let x=0,E=0;for(var w of Object.values(l.dataFileDownloads))x+=w.loaded,E+=w.total;l.setStatus?.(`Downloading data... (${x}/${E})`)}k=new Uint8Array(p.map(x=>x.length).reduce((x,E)=>x+E,0));w=0;for(const x of p)k.set(x,w),w+=x.length;return k.buffer}async function d(k){k.FS_createPath("/","microblink",!0,!0);k.FS_createPath("/microblink","blinkcard",!0,!0);for(var m of b.files)k.addRunDependency(`fp ${m.filename}`); -k.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.data"]={Jc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p); -for(var n of b.files){var t=n.filename;k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0, -location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkCardModule.data","")??"BlinkCardModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkcard/Model_1118d9d674e23996f70c6416b2bf5a6ce6ef24a6ad2c92f0ddd1e198e5f05305.strop",start:0,end:19102},{filename:"/microblink/blinkcard/Model_349432d66ef2b216155673b634f7d5c47795bed35719b954f726b5f0856740f3.strop", -start:19102,end:67419},{filename:"/microblink/blinkcard/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:67419,end:336140},{filename:"/microblink/blinkcard/Model_5065f3a3bc1c2fece482ee66e9275fc198b9be239547e08b6086c59f347ca72f.strop",start:336140,end:2591295},{filename:"/microblink/blinkcard/Model_830c13896f96c1cb6d5cad725f44e6aae470f8672d640d20b3272ed4bb839699.strop",start:2591295,end:2848318},{filename:"/microblink/blinkcard/Model_9f6734be0f5c1e4f3c6c621f4a72db8241feaf7c8705dc68a9cc07a7b634ee85.strop", -start:2848318,end:2931954},{filename:"/microblink/blinkcard/Model_abb3e9795585a24a9bbd1dd41ec97daa2e1d7d42087aacd981411fd8b26bf493.strop",start:2931954,end:3264110},{filename:"/microblink/blinkcard/Model_b9263312a9b623d1a3b75b643ccdcbc36aae52c278d721443468147c50e44583.strop",start:3264110,end:3532384},{filename:"/microblink/blinkcard/Model_c99d9c4b96e424a1b5a17758060a8116912f78d14318e471e0606709227b9497.strop",start:3532384,end:6369917},{filename:"/microblink/blinkcard/Model_cc1fab8df49d9a21de6c7b76ccf0dac40b17fcfb7073cc520eca073cbf8e33e9.strop", -start:6369917,end:6373648},{filename:"/microblink/blinkcard/Model_f132d1bd7614b1274fafb8a41ec6c047b84b2a43654ae2da5ddd78a2765601c6.strop",start:6373648,end:7208473},{filename:"/microblink/blinkcard/bin-database_1.0.zzip",start:7208473,end:8578325}],remote_package_size:8578325})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; +k.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.data"]={Jc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t, +null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkCardModule.data", +"")??"BlinkCardModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkcard/Model_1118d9d674e23996f70c6416b2bf5a6ce6ef24a6ad2c92f0ddd1e198e5f05305.strop",start:0,end:19102},{filename:"/microblink/blinkcard/Model_349432d66ef2b216155673b634f7d5c47795bed35719b954f726b5f0856740f3.strop",start:19102,end:67419},{filename:"/microblink/blinkcard/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop", +start:67419,end:336140},{filename:"/microblink/blinkcard/Model_5065f3a3bc1c2fece482ee66e9275fc198b9be239547e08b6086c59f347ca72f.strop",start:336140,end:2591295},{filename:"/microblink/blinkcard/Model_830c13896f96c1cb6d5cad725f44e6aae470f8672d640d20b3272ed4bb839699.strop",start:2591295,end:2848318},{filename:"/microblink/blinkcard/Model_9f6734be0f5c1e4f3c6c621f4a72db8241feaf7c8705dc68a9cc07a7b634ee85.strop",start:2848318,end:2931954},{filename:"/microblink/blinkcard/Model_abb3e9795585a24a9bbd1dd41ec97daa2e1d7d42087aacd981411fd8b26bf493.strop", +start:2931954,end:3264110},{filename:"/microblink/blinkcard/Model_b9263312a9b623d1a3b75b643ccdcbc36aae52c278d721443468147c50e44583.strop",start:3264110,end:3532384},{filename:"/microblink/blinkcard/Model_c99d9c4b96e424a1b5a17758060a8116912f78d14318e471e0606709227b9497.strop",start:3532384,end:6369917},{filename:"/microblink/blinkcard/Model_cc1fab8df49d9a21de6c7b76ccf0dac40b17fcfb7073cc520eca073cbf8e33e9.strop",start:6369917,end:6373648},{filename:"/microblink/blinkcard/Model_f132d1bd7614b1274fafb8a41ec6c047b84b2a43654ae2da5ddd78a2765601c6.strop", +start:6373648,end:7208473},{filename:"/microblink/blinkcard/bin-database_1.0.zzip",start:7208473,end:8578325}],remote_package_size:8578325})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; (function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkCardModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; if(aa||ba){try{fa=(new URL(".",ea)).href}catch{}ba&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});ha=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);}}var ja=console.log.bind(console),q=console.error.bind(console),ka,la=!1,ma,na,oa,r,v,A,pa,B,D,qa,sa,G,ta,ua=!1; function va(){var a=wa.buffer;r=new Int8Array(a);A=new Int16Array(a);v=new Uint8Array(a);pa=new Uint16Array(a);B=new Int32Array(a);D=new Uint32Array(a);qa=new Float32Array(a);sa=new Float64Array(a);G=new BigInt64Array(a);ta=new BigUint64Array(a)}var wa;function H(a){l.onAbort?.(a);a="Aborted("+a+")";q(a);la=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa?.(a);throw a;}var xa; @@ -90,7 +90,7 @@ const b=Symbol.dispose;b&&(a[b]=a["delete"])})(); Object.assign(Kc.prototype,{cc(a){this.Tb&&(a=this.Tb(a));return a},Nb(a){this.Sa?.(a)},Ta:gc,Ga:function(a){function b(){return this.qb?Jc(this.Ca.$a,{Da:this.tc,Aa:c,Na:this,Ia:a}):Jc(this.Ca.$a,{Da:this,Aa:a})}var c=this.cc(a);if(!c)return this.Nb(a),null;var d=Ic(this.Ca,c);if(void 0!==d){if(0===d.wa.count.value)return d.wa.Aa=c,d.wa.Ia=a,d.clone();d=d.clone();this.Nb(a);return d}d=this.Ca.bc(c);d=vc[d];if(!d)return b.call(this);d=this.pb?d.Yb:d.pointerType;var e=Gc(c,this.Ca,d.Ca);return null=== e?b.call(this):this.qb?Jc(d.Ca.$a,{Da:d,Aa:e,Na:this,Ia:a}):Jc(d.Ca.$a,{Da:d,Aa:e})}});(async function(){Y=new yd;nb("library_fetch_init");try{Ad=await zd()}catch(a){Ad=!1}finally{mb("library_fetch_init")}})();wa=l.wasmMemory?l.wasmMemory:new WebAssembly.Memory({initial:(l.INITIAL_MEMORY||209715200)/65536,maximum:32768});va();l.noExitRuntime&&(Ga=l.noExitRuntime);l.preloadPlugins&&(ob=l.preloadPlugins);l.print&&(ja=l.print);l.printErr&&(q=l.printErr);l.wasmBinary&&(ka=l.wasmBinary); l.thisProgram&&(da=l.thisProgram);if(l.preInit)for("function"==typeof l.preInit&&(l.preInit=[l.preInit]);0{var k=b?Ra(Na(a+"/"+b)):a,m=`cp ${k}`;nb(m);try{var p=c;"string"==typeof c&&(p=await jb(c));p=await pb(p,k);h?.();f||Yb(a,b,p,d,e,g)}finally{mb(m)}};l.FS_unlink=(...a)=>Pb(...a);l.FS_createPath=(...a)=>Wb(...a);l.FS_createDevice=(...a)=>Zb(...a); -l.FS_createDataFile=(...a)=>Yb(...a);l.FS_createLazyFile=(...a)=>ac(...a);var Kd={203602:(a,b,c,d)=>{a=a?M(v,a):"";b=b?M(v,b):"";c=c?M(v,c):"";d=d?M(v,d):"";throw Error(a+b+c+d);},203818:(a,b)=>{a=a?M(v,a):"";b=b?M(v,b):"";throw Error(a+b);},203928:a=>{a=a?M(v,a):"";throw Error(a);},203996:a=>{a=a?M(v,a):"";throw Error(a);}},Nc,Cd,Oc,cb,Ld,Md,Nd,Od,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; +l.FS_createDataFile=(...a)=>Yb(...a);l.FS_createLazyFile=(...a)=>ac(...a);var Kd={203618:(a,b,c,d)=>{a=a?M(v,a):"";b=b?M(v,b):"";c=c?M(v,c):"";d=d?M(v,d):"";throw Error(a+b+c+d);},203834:(a,b)=>{a=a?M(v,a):"";b=b?M(v,b):"";throw Error(a+b);},203944:a=>{a=a?M(v,a):"";throw Error(a);},204012:a=>{a=a?M(v,a):"";throw Error(a);}},Nc,Cd,Oc,cb,Ld,Md,Nd,Od,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; var Td={F:(a,b)=>K(a)(b),t:function(a,b,c){Ka=c;try{var d=R(a);switch(b){case 0:var e=La();if(0>e)break;for(;sb[e];)e++;return Ib(d,e).Za;case 1:case 2:return 0;case 3:return d.flags;case 4:return e=La(),d.flags|=e,0;case 12:return e=La(),A[e+0>>1]=2,0;case 13:case 14:return 0}return-28}catch(f){if("undefined"==typeof T||"ErrnoError"!==f.name)throw f;return-f.Fa}},Z:function(a,b){try{var c=R(a),d=c.node,e=c.xa.Pa;a=e?c:d;e??=d.za.Pa;Gb(e);var f=e(a);return dc(b,f)}catch(g){if("undefined"==typeof T|| "ErrnoError"!==g.name)throw g;return-g.Fa}},_:function(a,b,c){Ka=c;try{var d=R(a);switch(b){case 21509:return d.Ba?0:-59;case 21505:if(!d.Ba)return-59;if(d.Ba.Wa.kc){a=[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];var e=La();B[e>>2]=25856;B[e+4>>2]=5;B[e+8>>2]=191;B[e+12>>2]=35387;for(var f=0;32>f;f++)r[e+f+17]=a[f]||0}return 0;case 21510:case 21511:case 21512:return d.Ba?0:-59;case 21506:case 21507:case 21508:if(!d.Ba)return-59;if(d.Ba.Wa.lc)for(e=La(),a=[],f=0;32> f;f++)a.push(r[e+f+17]);return 0;case 21519:if(!d.Ba)return-59;e=La();return B[e>>2]=0;case 21520:return d.Ba?-28:-59;case 21537:case 21531:e=La();if(!d.xa.jc)throw new O(59);return d.xa.jc(d,b,e);case 21523:if(!d.Ba)return-59;d.Ba.Wa.mc&&(f=[24,80],e=La(),A[e>>1]=f[0],A[e+2>>1]=f[1]);return 0;case 21524:return d.Ba?0:-59;case 21515:return d.Ba?0:-59;default:return-28}}catch(g){if("undefined"==typeof T||"ErrnoError"!==g.name)throw g;return-g.Fa}},X:function(a,b){try{return a=a?M(v,a):"",dc(b,Qb(a, diff --git a/packages/blinkcard-wasm/dist/basic/BlinkCardModule.wasm b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.wasm index b179459..5b27f0e 100755 Binary files a/packages/blinkcard-wasm/dist/basic/BlinkCardModule.wasm and b/packages/blinkcard-wasm/dist/basic/BlinkCardModule.wasm differ diff --git a/packages/blinkcard-wasm/dist/size-manifest.json b/packages/blinkcard-wasm/dist/size-manifest.json index 6dd40d7..ed6fdcc 100644 --- a/packages/blinkcard-wasm/dist/size-manifest.json +++ b/packages/blinkcard-wasm/dist/size-manifest.json @@ -1,12 +1,12 @@ { "wasm": { - "basic": 2253340, - "advanced": 2256272, - "advanced-threads": 2279966 + "advanced": 2256595, + "basic": 2252883, + "advanced-threads": 2280165 }, "data": { - "basic": 8578325, "advanced": 8578325, + "basic": 8578325, "advanced-threads": 8578325 } } \ No newline at end of file diff --git a/packages/blinkcard-wasm/package.json b/packages/blinkcard-wasm/package.json index ebf329f..4411892 100644 --- a/packages/blinkcard-wasm/package.json +++ b/packages/blinkcard-wasm/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkcard-wasm", "private": true, - "version": "3000.0.2", + "version": "3000.0.3", "scripts": { "build": "tsc", "build:publish": "tsc", diff --git a/packages/blinkcard-worker/CHANGELOG.md b/packages/blinkcard-worker/CHANGELOG.md index d9e9554..c86adfb 100644 --- a/packages/blinkcard-worker/CHANGELOG.md +++ b/packages/blinkcard-worker/CHANGELOG.md @@ -1,5 +1,16 @@ # @microblink/blinkcard-worker +## 3000.0.3 + +### Patch Changes + +- Added crash reporting for worker runtime failures, unhandled promise rejections, Wasm aborts, session creation failures, session method failures, and frame-transfer failures. +- Flushes init-time pinglets only when BlinkCard SDK initialization fails, preventing successful initialization from sending queued analytics prematurely. +- Updated dependencies + - @microblink/worker-common@1.0.2 + - @microblink/analytics@1.0.1 + - @microblink/blinkcard-wasm@3000.0.3 + ## 3000.0.2 ### Patch Changes diff --git a/packages/blinkcard-worker/docs/classes/BlinkCardWorker.md b/packages/blinkcard-worker/docs/classes/BlinkCardWorker.md index 7e5aa4f..cc6e7d5 100644 --- a/packages/blinkcard-worker/docs/classes/BlinkCardWorker.md +++ b/packages/blinkcard-worker/docs/classes/BlinkCardWorker.md @@ -116,11 +116,11 @@ This method initializes the BlinkCard Wasm module. ### reportPinglet() -> **reportPinglet**(`__namedParameters`): `void` +> **reportPinglet**(`pinglet`): `void` #### Parameters -##### \_\_namedParameters +##### pinglet `Ping` diff --git a/packages/blinkcard-worker/package.json b/packages/blinkcard-worker/package.json index 2ebe2d8..f629172 100644 --- a/packages/blinkcard-worker/package.json +++ b/packages/blinkcard-worker/package.json @@ -2,7 +2,7 @@ "name": "@microblink/blinkcard-worker", "description": "Provides a worker which runs the BlinkCard WebAssembly in separate thread", "private": true, - "version": "3000.0.2", + "version": "3000.0.3", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", "build:js": "vite build --mode ${VITE_BUILD_MODE:-production}", diff --git a/packages/blinkcard-worker/src/BlinkCardWorker.initBlinkCard.test.ts b/packages/blinkcard-worker/src/BlinkCardWorker.initBlinkCard.test.ts index ebfa44b..de0e39b 100644 --- a/packages/blinkcard-worker/src/BlinkCardWorker.initBlinkCard.test.ts +++ b/packages/blinkcard-worker/src/BlinkCardWorker.initBlinkCard.test.ts @@ -2,16 +2,22 @@ * Copyright (c) 2026 Microblink Ltd. All rights reserved. */ +import type { BlinkCardScanningSession } from "@microblink/blinkcard-wasm"; import { LicenseError, ServerPermissionError, } from "@microblink/worker-common/errors"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as Comlink from "comlink"; import { createWasmModuleMock, + getLastModuleOverrides, + resetLastModuleOverrides, setWasmModuleMock, } from "@microblink/test-utils/mocks/wasmModuleFactory"; +import { createFakeImageData } from "@microblink/test-utils/mocks/imageData"; import { createLicenseUnlockResult } from "@microblink/test-utils/mocks/licensing"; +import { createScanningSessionMock } from "@microblink/test-utils/mocks/scanningSession"; import { BlinkCardWasmModule } from "@microblink/blinkcard-wasm"; const getCrossOriginWorkerURLMock = vi.fn(); @@ -20,6 +26,7 @@ const detectWasmFeaturesMock = vi.fn(); const validateLicenseProxyPermissionsMock = vi.fn(); const sanitizeProxyUrlsMock = vi.fn(); const obtainNewServerPermissionMock = vi.fn(); +let workerEventListeners = new Map(); /** Deterministic values for stubbed globals and mock return shapes. */ const hostName = "example.com" as const; @@ -59,6 +66,29 @@ vi.mock("@microblink/worker-common/licencing", () => ({ let BlinkCardWorker: typeof import("./BlinkCardWorker").BlinkCardWorker; +const getLatestWorkerListener = (type: string) => { + const listeners = workerEventListeners.get(type); + + if (!listeners || listeners.length === 0) { + return undefined; + } + + return listeners[listeners.length - 1]; +}; + +const getLastQueuedPinglet = (queuePingletMock: ReturnType) => { + const serializedPinglet = queuePingletMock.mock.calls[ + queuePingletMock.mock.calls.length - 1 + ]?.[0] as string; + + return JSON.parse(serializedPinglet) as Record; +}; + +const getLastQueuedPingletSessionNumber = ( + queuePingletMock: ReturnType, +): unknown => + queuePingletMock.mock.calls[queuePingletMock.mock.calls.length - 1]?.[3]; + describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { const baseInitSettings = { licenseKey: "test-license", @@ -74,12 +104,30 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { sanitizeProxyUrlsMock.mockReset(); obtainNewServerPermissionMock.mockReset(); + workerEventListeners = new Map(); + // Deterministic hostname/userAgent so ping payload and runtime context are stable. vi.stubGlobal("self", { setTimeout: vi.fn(), close: vi.fn(), location: { hostname: hostName }, navigator: { userAgent: "Chrome" }, + addEventListener: vi.fn( + (type: string, listener: EventListenerOrEventListenerObject) => { + const listeners = workerEventListeners.get(type) ?? []; + listeners.push(listener as EventListener); + workerEventListeners.set(type, listeners); + }, + ), + removeEventListener: vi.fn( + (type: string, listener: EventListenerOrEventListenerObject) => { + const listeners = workerEventListeners.get(type) ?? []; + workerEventListeners.set( + type, + listeners.filter((entry) => entry !== listener), + ); + }, + ), }); // Worker loads wasm from this URL; mock factory serves the seeded module. @@ -101,11 +149,12 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { afterEach(() => { setWasmModuleMock(null); + resetLastModuleOverrides(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); - it("sends pinglets only after server permission flow completes", async () => { + it("does not flush pinglets after successful server permission flow", async () => { const { module, spies } = createWasmModuleMock({ initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult({ @@ -123,29 +172,28 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { expect(module.queuePinglet).toHaveBeenCalledOnce(); expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(module.submitServerPermission).toHaveBeenCalled(); - expect(module.sendPinglets).toHaveBeenCalledOnce(); + expect(module.sendPinglets).not.toHaveBeenCalled(); expect(module.initializeSdk).toHaveBeenCalledOnce(); - // Init pinglet is queued first; license and permission steps must complete before flush. + // Init pinglet is queued first; license and permission steps must complete before SDK init. expect(spies.queuePinglet.mock.invocationCallOrder[0]).toBeLessThan( obtainNewServerPermissionMock.mock.invocationCallOrder[0], ); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); - // ensure we flush pinglets after initializeSdk - expect(spies.initializeSdk.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); + expect(spies.queuePinglet.mock.invocationCallOrder[0]).toBeLessThan( + spies.initializeSdk.mock.invocationCallOrder[0], ); }); - it("sets ping proxy URL before sending pinglets when ping proxy is allowed", async () => { + it("sets ping proxy URL when ping proxy is allowed without flushing pinglets", async () => { const proxyUrl = "https://proxy.example.com"; const { module, spies } = createWasmModuleMock({ initializeWithLicenseKey: vi.fn(() => @@ -172,23 +220,23 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(spies.submitServerPermission).toHaveBeenCalledOnce(); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); + expect(spies.sendPinglets).not.toHaveBeenCalled(); - // Ping route must be set before flush so pings go through the proxy. + // Ping route must be set before SDK init so future pings use the proxy. expect(spies.setPingProxyUrl.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + spies.initializeSdk.mock.invocationCallOrder[0], ); expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); - expect(spies.initializeSdk.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); + expect(spies.setPingProxyUrl.mock.invocationCallOrder[0]).toBeLessThan( + spies.submitServerPermission.mock.invocationCallOrder[0], ); }); @@ -218,7 +266,7 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[0]); }); - it("uses ping and baltazar proxies and flushes pinglets after permission flow", async () => { + it("uses ping and baltazar proxies without flushing pinglets on successful init", async () => { const proxyUrl = "https://proxy.example.com"; const licenseUnlockResult = createLicenseUnlockResult({ unlockResult: "requires-server-permission", @@ -249,21 +297,21 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(spies.submitServerPermission).toHaveBeenCalledOnce(); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); + expect(spies.sendPinglets).not.toHaveBeenCalled(); - // Ping proxy set before flush; permission flow completes before flush. + // Ping proxy is set before SDK init; permission flow completes before SDK init. expect(spies.setPingProxyUrl.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + spies.initializeSdk.mock.invocationCallOrder[0], ); expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); }); it("throws Error and does not send pinglets when server permission request fails", async () => { @@ -323,7 +371,7 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[0]); }); - it("throws and does send pinglets when initializeSdk fails", async () => { + it("queues crash pinglet and flushes when initializeSdk fails", async () => { const { module, spies } = createWasmModuleMock({ initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult({ @@ -334,7 +382,8 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { setWasmModuleMock(module); const worker = new BlinkCardWorker(); - // Flush happens after initializeSdk; failure must leave pinglets buffered. + // The init start pinglet is already queued; initializeSdk failure adds ping.error + // and the init catch block flushes after recording the error. spies.initializeSdk.mockImplementation(() => { throw new Error("initializeSdk-error"); }); @@ -348,15 +397,272 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[0]); - expect( - spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[1]); expect(spies.initializeSdk.mock.invocationCallOrder[0]).greaterThan( spies.queuePinglet.mock.invocationCallOrder[0], ); - expect(spies.initializeSdk.mock.invocationCallOrder[0]).toBeLessThan( - spies.queuePinglet.mock.invocationCallOrder[1], + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "initializeSdk-error", + }); + }); + + it("reports worker error events as crash pinglets after init", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + getLatestWorkerListener("error")?.({ + error: new Error("boom"), + message: "boom", + } as unknown as Event); + + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "boom", + }); + }); + + it("reports unhandled rejections as crash pinglets after init", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + getLatestWorkerListener("unhandledrejection")?.({ + reason: new Error("rejected"), + } as unknown as Event); + + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "rejected", + }); + }); + + it("reports Emscripten aborts as crash pinglets after init", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + const moduleOverrides = getLastModuleOverrides(); + expect(moduleOverrides?.onAbort).toEqual(expect.any(Function)); + + (moduleOverrides?.onAbort as (what: unknown) => void)("fatal abort"); + + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "fatal abort", + }); + }); + + it("reports scanning session creation failures as crash pinglets", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => { + throw new Error("session-create-failed"); + }), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => worker.createScanningSession()).toThrow( + "session-create-failed", + ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "session-create-failed", + }); + }); + + it("reports thrown process calls as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + process: vi.fn(() => { + throw new Error("process-failed"); + }), + getSessionNumber: vi.fn(() => 1), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.process(createFakeImageData())).toThrow( + "process-failed", + ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "process-failed", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports process failures as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + process: vi.fn(() => { + throw new Error("table index is out of bounds RuntimeError"); + }), + getSessionNumber: vi.fn(() => 1), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.process(createFakeImageData())).toThrow( + "table index is out of bounds RuntimeError", + ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "table index is out of bounds RuntimeError", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports frame return transfer failures as crash pinglets", async () => { + const transferSpy = vi + .spyOn(Comlink, "transfer") + .mockImplementationOnce(() => { + throw new Error("buffer-transfer-failed"); + }); + const session = createScanningSessionMock({ + process: vi.fn(() => ({ cardNumber: "4111111111111111" }) as never), + getSessionNumber: vi.fn(() => 1), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.process(createFakeImageData())).toThrow( + "Failed to transfer frame from worker: buffer-transfer-failed", ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: + "Failed to transfer frame from worker: buffer-transfer-failed", + }); + }); + + it("reports getResult failures as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + getResult: vi.fn(() => { + throw new Error("get-result-failed"); + }), + getSessionNumber: vi.fn(() => 1), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.getResult()).toThrow("get-result-failed"); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "get-result-failed", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports reset failures as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + reset: vi.fn(() => { + throw new Error("reset-failed"); + }), + getSessionNumber: vi.fn(() => 1), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkCardWorker(); + await worker.initBlinkCard(baseInitSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.reset()).toThrow("reset-failed"); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "reset-failed", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); }); it("uses baltazar proxy for server permission and does not set ping proxy URL", async () => { @@ -389,17 +695,17 @@ describe("BlinkCardWorker initBlinkCard ping flush and proxy ordering", () => { expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(spies.submitServerPermission).toHaveBeenCalledOnce(); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); + expect(spies.sendPinglets).not.toHaveBeenCalled(); // allowPingProxy is false so ping proxy is not set; permission flow order unchanged. expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); }); }); diff --git a/packages/blinkcard-worker/src/BlinkCardWorker.test.ts b/packages/blinkcard-worker/src/BlinkCardWorker.test.ts index 7f784a2..e4dd296 100644 --- a/packages/blinkcard-worker/src/BlinkCardWorker.test.ts +++ b/packages/blinkcard-worker/src/BlinkCardWorker.test.ts @@ -45,6 +45,8 @@ describe("BlinkCardWorker", () => { close: vi.fn(), location: { hostname: "example.com" }, navigator: { userAgent: "Chrome" }, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), }); ({ BlinkCardWorker } = await import("./BlinkCardWorker")); @@ -158,11 +160,8 @@ describe("BlinkCardWorker", () => { expect(result.arrayBuffer).toBe(image.data.buffer); }); - it("reports pinglet on session errors when process throws", () => { + it("rethrows session process errors without a loaded wasm module", () => { const worker = new BlinkCardWorker(); - const reportPingletSpy = vi - .spyOn(worker, "reportPinglet") - .mockImplementation(() => undefined); const error = new Error("boom"); const session = createScanningSessionMock({ process: vi.fn().mockImplementation(() => { @@ -174,10 +173,5 @@ describe("BlinkCardWorker", () => { const image = createFakeImageData(); expect(() => proxySession.process(image)).toThrow(error); - expect(reportPingletSpy).toHaveBeenCalledWith( - expect.objectContaining({ - schemaName: "ping.error", - }), - ); }); }); diff --git a/packages/blinkcard-worker/src/BlinkCardWorker.ts b/packages/blinkcard-worker/src/BlinkCardWorker.ts index 6f2f267..164d9dc 100644 --- a/packages/blinkcard-worker/src/BlinkCardWorker.ts +++ b/packages/blinkcard-worker/src/BlinkCardWorker.ts @@ -37,9 +37,25 @@ import type { WasmVariant, } from "@microblink/blinkcard-wasm"; import { OverrideProperties } from "type-fest"; +import { installWorkerCrashReporter } from "@microblink/worker-common/workerCrashReporter"; export type { DownloadProgress } from "@microblink/worker-common/downloadResourceBuffer"; +const FRAME_TRANSFER_ERROR_NAME = "FrameTransferError"; + +const createFrameTransferError = (message: string, error: unknown) => { + const causeMessage = + error instanceof Error && error.message ? `: ${error.message}` : ""; + + const frameTransferError = new Error( + `${message}${causeMessage}`, + error instanceof Error ? { cause: error } : undefined, + ); + frameTransferError.name = FRAME_TRANSFER_ERROR_NAME; + + return frameTransferError; +}; + /** * The BlinkCard worker. */ @@ -77,6 +93,32 @@ export class BlinkCardWorker { #userId!: string; + #cleanupCrashReporter: (() => void) | undefined; + + constructor() { + this.#cleanupCrashReporter = installWorkerCrashReporter({ + getSessionNumber: () => this.#currentSessionNumber, + onError: ({ error, sessionNumber }) => { + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber, + data: { + errorType: "Crash", + errorMessage: + error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); + }, + }); + } + /** * This method loads the Wasm module. */ @@ -219,6 +261,44 @@ export class BlinkCardWorker { locateFile: (path) => { return `${variantUrl}/${wasmVariant}/${path}`; }, + onAbort: (what) => { + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: what instanceof Error ? what.message : String(what), + stackTrace: what instanceof Error ? what.stack : undefined, + }, + }); + this.sendPinglets(); + }, + printErr: (message) => { + console.error(message); + + if (/\babort(ed)?\b/i.test(message)) { + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: String(message), + stackTrace: undefined, + }, + }); + this.sendPinglets(); + } + }, // pthreads build breaks without this: // "Failed to execute 'createObjectURL' on 'URL': Overload resolution failed." mainScriptUrlOrBlob: crossOriginWorkerUrl, @@ -235,30 +315,20 @@ export class BlinkCardWorker { } } - reportPinglet({ data, schemaName, schemaVersion, sessionNumber }: Ping) { + reportPinglet(pinglet: Ping) { if (!this.#wasmModule) { throw new Error("Cannot report pinglet: Wasm module not loaded"); } - if (!this.#wasmModule.isPingEnabled()) { - // Ping is not enabled, do nothing - return; - } - try { this.#wasmModule.queuePinglet( - JSON.stringify(data), - schemaName, - schemaVersion, - sessionNumber!, // we know sesion number is provided because we're using proxy function + JSON.stringify(pinglet.data), + pinglet.schemaName, + pinglet.schemaVersion, + pinglet.sessionNumber ?? this.#currentSessionNumber, ); } catch (error) { - console.warn("Failed to queue pinglet:", error, { - data, - schemaName, - schemaVersion, - sessionNumber, - }); + console.warn("Failed to queue pinglet:", error, pinglet); } } @@ -309,7 +379,7 @@ export class BlinkCardWorker { false, ); - // Queue init pinglet before remote license check; flush only after full flow + // Queue init pinglet before remote license check; flush only if init fails. this.reportPinglet({ schemaName: "ping.sdk.init.start", schemaVersion: "1.1.0", @@ -393,10 +463,9 @@ export class BlinkCardWorker { stackTrace: error instanceof Error ? error.stack : undefined, }, }); - throw error; - } finally { - // flush pinglets after initializing the SDK + // Flush only for failed SDK initialization. this.sendPinglets(); + throw error; } } @@ -411,16 +480,31 @@ export class BlinkCardWorker { throw new Error("Wasm module not loaded"); } - const session = this.#wasmModule.createScanningSession( - sessionSettings ?? {}, - this.#userId, - ); + try { + const session = this.#wasmModule.createScanningSession( + sessionSettings ?? {}, + this.#userId, + ); - this.#currentSessionNumber++; + this.#currentSessionNumber++; - this.sendPinglets(); + this.sendPinglets(); - return this.createProxySession(session); + return this.createProxySession(session); + } catch (error) { + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); + throw error; + } } /** @@ -443,6 +527,10 @@ export class BlinkCardWorker { try { return session.getResult(); } catch (error) { + if (!this.#wasmModule) { + throw error; + } + this.reportPinglet({ schemaName: "ping.error", schemaVersion: "1.0.0", @@ -451,8 +539,10 @@ export class BlinkCardWorker { errorType: "NonFatal", errorMessage: error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, }, }); + this.sendPinglets(); throw error; } }, @@ -460,16 +550,53 @@ export class BlinkCardWorker { try { const processResult = session.process(image); - const transferPackage: ProcessResultWithBuffer = transfer( - { - ...processResult, - arrayBuffer: image.data.buffer, - }, - [image.data.buffer], - ); + let transferPackage: ProcessResultWithBuffer; + + try { + transferPackage = transfer( + { + ...processResult, + arrayBuffer: image.data.buffer, + }, + [image.data.buffer], + ); + } catch (error) { + const frameTransferError = createFrameTransferError( + "Failed to transfer frame from worker", + error, + ); + + if (!this.#wasmModule) { + throw frameTransferError; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: session.getSessionNumber(), + data: { + errorType: "Crash", + errorMessage: frameTransferError.message, + stackTrace: frameTransferError.stack, + }, + }); + this.sendPinglets(); + throw frameTransferError; + } return transferPackage; } catch (error) { + if ( + error instanceof Error && + error.name === FRAME_TRANSFER_ERROR_NAME + ) { + throw error; + } + + if (!this.#wasmModule) { + throw error; + } + this.reportPinglet({ schemaName: "ping.error", schemaVersion: "1.0.0", @@ -481,6 +608,7 @@ export class BlinkCardWorker { stackTrace: error instanceof Error ? error.stack : undefined, }, }); + this.sendPinglets(); throw error; } }, @@ -498,6 +626,10 @@ export class BlinkCardWorker { try { session.reset(); } catch (error) { + if (!this.#wasmModule) { + throw error; + } + this.reportPinglet({ schemaName: "ping.error", schemaVersion: "1.0.0", @@ -509,6 +641,7 @@ export class BlinkCardWorker { stackTrace: error instanceof Error ? error.stack : undefined, }, }); + this.sendPinglets(); throw error; } }, @@ -566,12 +699,30 @@ export class BlinkCardWorker { "Failed to delete BlinkCard session during terminate:", error, ); + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "NonFatal", + errorMessage: + error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); } finally { this.#activeSession = undefined; } } if (!this.#wasmModule) { + this.#cleanupCrashReporter?.(); + this.#cleanupCrashReporter = undefined; console.warn( "No Wasm module loaded during worker termination. Skipping cleanup.", ); @@ -600,6 +751,8 @@ export class BlinkCardWorker { } this.#wasmModule = undefined; + this.#cleanupCrashReporter?.(); + this.#cleanupCrashReporter = undefined; console.debug("BlinkCardWorker terminated 🔴"); self.close(); diff --git a/packages/blinkcard/CHANGELOG.md b/packages/blinkcard/CHANGELOG.md index 617f10a..1f6224b 100644 --- a/packages/blinkcard/CHANGELOG.md +++ b/packages/blinkcard/CHANGELOG.md @@ -1,5 +1,15 @@ # @microblink/blinkcard +## 3000.0.3 + +### Patch Changes + +- Added crash reporting for failures during `createBlinkCard(...)`, including SDK initialization, scanning-session creation, UX-manager setup, and UI startup. +- Updated dependencies + - @microblink/camera-manager@7.3.1 + - @microblink/blinkcard-ux-manager@3000.0.3 + - @microblink/blinkcard-core@3000.0.3 + ## 3000.0.2 ### Patch Changes diff --git a/packages/blinkcard/docs/README.md b/packages/blinkcard/docs/README.md index 1357559..8755add 100644 --- a/packages/blinkcard/docs/README.md +++ b/packages/blinkcard/docs/README.md @@ -76,6 +76,7 @@ - [DeviceScreenInfo](type-aliases/DeviceScreenInfo.md) - [DismountCallback](type-aliases/DismountCallback.md) - [DownloadProgress](type-aliases/DownloadProgress.md) +- [ErrorCallback](type-aliases/ErrorCallback.md) - [ExtractionArea](type-aliases/ExtractionArea.md) - [ExtractionSettings](type-aliases/ExtractionSettings.md) - [FacingMode](type-aliases/FacingMode.md) diff --git a/packages/blinkcard/docs/classes/BlinkCardWorker.md b/packages/blinkcard/docs/classes/BlinkCardWorker.md index 3fe3e54..faf89ac 100644 --- a/packages/blinkcard/docs/classes/BlinkCardWorker.md +++ b/packages/blinkcard/docs/classes/BlinkCardWorker.md @@ -116,11 +116,11 @@ This method initializes the BlinkCard Wasm module. ### reportPinglet() -> **reportPinglet**(`__namedParameters`): `void` +> **reportPinglet**(`pinglet`): `void` #### Parameters -##### \_\_namedParameters +##### pinglet `Ping` diff --git a/packages/blinkcard/docs/classes/CameraManager.md b/packages/blinkcard/docs/classes/CameraManager.md index 0558750..c407249 100644 --- a/packages/blinkcard/docs/classes/CameraManager.md +++ b/packages/blinkcard/docs/classes/CameraManager.md @@ -257,6 +257,26 @@ CameraManager from throwing errors when the user interrupts the process. ## Methods +### addErrorCallback() + +> **addErrorCallback**(`errorCallback`): () => `boolean` + +#### Parameters + +##### errorCallback + +[`ErrorCallback`](../type-aliases/ErrorCallback.md) + +#### Returns + +> (): `boolean` + +##### Returns + +`boolean` + +*** + ### addFrameCaptureCallback() > **addFrameCaptureCallback**(`frameCaptureCallback`): () => `boolean` diff --git a/packages/blinkcard/docs/type-aliases/ErrorCallback.md b/packages/blinkcard/docs/type-aliases/ErrorCallback.md new file mode 100644 index 0000000..29c2578 --- /dev/null +++ b/packages/blinkcard/docs/type-aliases/ErrorCallback.md @@ -0,0 +1,19 @@ +[**@microblink/blinkcard**](../README.md) + +*** + +[@microblink/blinkcard](../README.md) / ErrorCallback + +# Type Alias: ErrorCallback() + +> **ErrorCallback** = (`error`) => `void` + +## Parameters + +### error + +`Error` + +## Returns + +`void` diff --git a/packages/blinkcard/package.json b/packages/blinkcard/package.json index 3643e42..761c9f1 100644 --- a/packages/blinkcard/package.json +++ b/packages/blinkcard/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkcard", "description": "All-in-one BlinkCard browser SDK for fast and accurate debit and credit card scanning and recognition in web applications.", - "version": "3000.0.2", + "version": "3000.0.3", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/blinkcard/src/createBlinkCard.test.ts b/packages/blinkcard/src/createBlinkCard.test.ts index a2647e2..f5df220 100644 --- a/packages/blinkcard/src/createBlinkCard.test.ts +++ b/packages/blinkcard/src/createBlinkCard.test.ts @@ -18,6 +18,8 @@ const fakeCameraManagerRef = vi.hoisted(() => ({ const { mockCreateSession, mockTerminate, + mockReportPinglet, + mockSendPinglets, mockCreateBlinkCardUxManager, mockCreateBlinkCardFeedbackUi, mockAddOnResultCallback, @@ -28,6 +30,8 @@ const { } = vi.hoisted(() => { const mockTerminate = vi.fn().mockResolvedValue(undefined); const mockCreateSession = vi.fn(); + const mockReportPinglet = vi.fn().mockResolvedValue(undefined); + const mockSendPinglets = vi.fn().mockResolvedValue(undefined); const mockAddOnResultCallback = vi.fn(); const mockAddOnErrorCallback = vi.fn(); const mockCreateBlinkCardUxManager = vi.fn().mockResolvedValue({ @@ -42,6 +46,8 @@ const { return { mockTerminate, mockCreateSession, + mockReportPinglet, + mockSendPinglets, mockCreateBlinkCardUxManager, mockCreateBlinkCardFeedbackUi, mockAddOnResultCallback, @@ -61,6 +67,8 @@ vi.mock("@microblink/blinkcard-core", () => { loadBlinkCardCore: vi.fn().mockResolvedValue({ createScanningSession: mockCreateSession, terminate: mockTerminate, + reportPinglet: mockReportPinglet, + sendPinglets: mockSendPinglets, }), }; }); @@ -289,6 +297,50 @@ describe("createBlinkCard", () => { ).toHaveBeenCalledTimes(1); }); + test("best-effort reports crashes through the core before a session exists", async () => { + mockCreateSession.mockRejectedValueOnce(new Error("session failed")); + + await expect(createBlinkCard({ licenseKey: "test-key" })).rejects.toThrow( + "session failed", + ); + + expect(mockReportPinglet).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + sessionNumber: 0, + data: expect.objectContaining({ + errorType: "Crash", + errorMessage: "sdk.createBlinkCard: session failed", + }), + }), + ); + expect(mockSendPinglets).toHaveBeenCalledTimes(1); + }); + + test("best-effort reports crashes through the core after session creation", async () => { + const scanningSession = createFakeScanningSession(); + mockCreateSession.mockResolvedValueOnce(scanningSession); + mockCreateBlinkCardUxManager.mockRejectedValueOnce(new Error("ux failed")); + + await expect(createBlinkCard({ licenseKey: "test-key" })).rejects.toThrow( + "ux failed", + ); + + expect(mockReportPinglet).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + sessionNumber: 0, + data: expect.objectContaining({ + errorType: "Crash", + errorMessage: "sdk.createBlinkCard: ux failed", + }), + }), + ); + expect(mockSendPinglets).toHaveBeenCalledTimes(1); + expect(scanningSession.ping).not.toHaveBeenCalled(); + expect(scanningSession.sendPinglets).not.toHaveBeenCalled(); + }); + test("creates feedback UI only once even if playbackState fires multiple times", async () => { await createBlinkCard({ licenseKey: "test-key" }); diff --git a/packages/blinkcard/src/createBlinkCard.ts b/packages/blinkcard/src/createBlinkCard.ts index 3aa611e..a4d8437 100644 --- a/packages/blinkcard/src/createBlinkCard.ts +++ b/packages/blinkcard/src/createBlinkCard.ts @@ -130,78 +130,118 @@ export const createBlinkCard = async ({ wasmVariant, feedbackUiOptions, }: BlinkCardComponentOptions) => { - // we first initialize the direct API. This loads the WASM module and initializes the engine - const blinkCardCore = await loadBlinkCardCore({ - licenseKey, - microblinkProxyUrl, - initialMemory, - resourcesLocation, - wasmVariant, - }); - - const scanningSession = await blinkCardCore.createScanningSession({ - scanningSettings, - }); - - // we create the camera manager - const cameraManager = new CameraManager(); - - // we create the UX manager - const blinkCardUxManager = await createBlinkCardUxManager( - cameraManager, - scanningSession, - ); - - // this creates the UI and attaches it to the DOM - const cameraUi = await createCameraManagerUi( - cameraManager, - targetNode, - cameraManagerUiOptions, - ); - - const unsub = cameraManager.subscribe( - (s) => s.playbackState, - (state) => { - if (state === "playback") { - // this creates the feedback UI and attaches it to the camera UI - createBlinkCardFeedbackUi( - blinkCardUxManager, - cameraUi, - feedbackUiOptions ?? {}, - ); - - if (feedbackUiOptions?.showOnboardingGuide === false) { - void cameraManager.startFrameCapture(); + let blinkCardCore: BlinkCardCore | undefined; + let scanningSession: + | Awaited> + | undefined; + + try { + // we first initialize the direct API. This loads the WASM module and initializes the engine + blinkCardCore = await loadBlinkCardCore({ + licenseKey, + microblinkProxyUrl, + initialMemory, + resourcesLocation, + wasmVariant, + }); + + scanningSession = await blinkCardCore.createScanningSession({ + scanningSettings, + }); + + // we create the camera manager + const cameraManager = new CameraManager(); + + // we create the UX manager + const blinkCardUxManager = await createBlinkCardUxManager( + cameraManager, + scanningSession, + ); + + // this creates the UI and attaches it to the DOM + const cameraUi = await createCameraManagerUi( + cameraManager, + targetNode, + cameraManagerUiOptions, + ); + + const unsub = cameraManager.subscribe( + (s) => s.playbackState, + (state) => { + if (state === "playback") { + // this creates the feedback UI and attaches it to the camera UI + createBlinkCardFeedbackUi( + blinkCardUxManager, + cameraUi, + feedbackUiOptions ?? {}, + ); + + if (feedbackUiOptions?.showOnboardingGuide === false) { + void cameraManager.startFrameCapture(); + } + + unsub(); // unsubscribe from the playback state } + }, + ); + + // selects the camera and starts the stream + await cameraManager.startCameraStream(); + + if (!blinkCardCore) { + throw new Error("BlinkCard core not initialized"); + } - unsub(); // unsubscribe from the playback state + const loadedBlinkCardCore = blinkCardCore; + + const destroy = async () => { + cameraUi.dismount(); + try { + await loadedBlinkCardCore.terminate(); + } catch (error) { + console.warn(error); + } + }; + + const returnObject: BlinkCardComponent = { + blinkCardCore: loadedBlinkCardCore, + cameraManager, + blinkCardUxManager, + cameraUi, + destroy, + addOnErrorCallback: + blinkCardUxManager.addOnErrorCallback.bind(blinkCardUxManager), + addOnResultCallback: + blinkCardUxManager.addOnResultCallback.bind(blinkCardUxManager), + }; + + return returnObject; + } catch (error) { + if (blinkCardCore) { + const data = { + errorType: "Crash" as const, + errorMessage: + "sdk.createBlinkCard: " + + (error instanceof Error ? error.message : String(error)), + stackTrace: error instanceof Error ? error.stack : undefined, + }; + + try { + await blinkCardCore.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: 0, + data, + }); + await blinkCardCore.sendPinglets(); + } catch (reportError) { + console.warn( + "Failed to report BlinkCard SDK crash pinglet:", + reportError, + ); } - }, - ); - - // selects the camera and starts the stream - await cameraManager.startCameraStream(); - - const destroy = async () => { - cameraUi.dismount(); - try { - await blinkCardCore.terminate(); - } catch (error) { - console.warn(error); } - }; - - const returnObject: BlinkCardComponent = { - blinkCardCore, - cameraManager, - blinkCardUxManager, - cameraUi, - destroy, - addOnErrorCallback: - blinkCardUxManager.addOnErrorCallback.bind(blinkCardUxManager), - addOnResultCallback: - blinkCardUxManager.addOnResultCallback.bind(blinkCardUxManager), - }; - - return returnObject; + + throw error; + } }; diff --git a/packages/blinkid-core/CHANGELOG.md b/packages/blinkid-core/CHANGELOG.md index 5583dc6..29f6498 100644 --- a/packages/blinkid-core/CHANGELOG.md +++ b/packages/blinkid-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @microblink/blinkid-core +## 7.7.2 + +### Patch Changes + +- Surfaces worker frame-transfer failures as explicit `FrameTransferError`s through the proxy-worker layer, improving diagnostics for invalid or detached frame buffers. +- Updated dependencies + - @microblink/core-common@1.0.1 + - @microblink/blinkid-worker@7.7.2 + - @microblink/blinkid-wasm@7.7.2 + ## 7.7.1 ### Patch Changes diff --git a/packages/blinkid-core/docs/classes/BlinkIdWorker.md b/packages/blinkid-core/docs/classes/BlinkIdWorker.md index 5e9ebdf..26880e7 100644 --- a/packages/blinkid-core/docs/classes/BlinkIdWorker.md +++ b/packages/blinkid-core/docs/classes/BlinkIdWorker.md @@ -208,11 +208,11 @@ This method initializes everything. ### reportPinglet() -> **reportPinglet**(`__namedParameters`): `void` +> **reportPinglet**(`pinglet`): `void` #### Parameters -##### \_\_namedParameters +##### pinglet `Ping` diff --git a/packages/blinkid-core/package.json b/packages/blinkid-core/package.json index 97ffe31..42a7c69 100644 --- a/packages/blinkid-core/package.json +++ b/packages/blinkid-core/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkid-core", "description": "BlinkID Core SDK", - "version": "7.7.1", + "version": "7.7.2", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/blinkid-ux-manager/CHANGELOG.md b/packages/blinkid-ux-manager/CHANGELOG.md index 8d0179e..43f3bf0 100644 --- a/packages/blinkid-ux-manager/CHANGELOG.md +++ b/packages/blinkid-ux-manager/CHANGELOG.md @@ -1,5 +1,16 @@ # @microblink/blinkid-ux-manager +## 7.7.2 + +### Patch Changes + +- Keeps the feedback overlay visible whenever no SDK modal is open, preventing it from disappearing during intro, transition, and success states. +- Added non-fatal analytics reporting for UX-manager creation failures, frame-capture failures, `CameraManager` frame-loop errors, and session result retrieval failures. +- Updated dependencies + - @microblink/camera-manager@7.3.1 + - @microblink/analytics@1.0.1 + - @microblink/blinkid-core@7.7.2 + ## 7.7.1 ### Patch Changes @@ -27,7 +38,17 @@ - Adds `destroy()` to `BlinkIdUxManager` for explicit teardown. - Deprecates `rawUiStateKey` and replaces it with two explicit getters: `uiStateKey` returns the stabilized, visible state key (what the UI shows); `mappedUiStateKey` returns the latest raw candidate key from the detector before stabilization (useful for debugging). - Introduces automatic chained UI state transitions after `PAGE_CAPTURED`: the manager advances through a document-type-specific transition state and into the appropriate intro state before resuming capture (e.g. `PAGE_CAPTURED → FLIP_CARD → INTRO_BACK_PAGE` for two-sided IDs, `PAGE_CAPTURED → MOVE_LAST_PAGE → INTRO_LAST_PAGE` for passports with barcode). Integrations that depend on exact UI-state keys or transition timing should account for these new intermediate states. -- Renames several UI state keys (e.g. `SENSING_FRONT` is now `FRONT_PAGE_NOT_IN_FRAME`). Integrations that reference state keys by name should update accordingly. +- Renames several UI state keys. Integrations that reference state keys by name should update accordingly. Each `SENSING_*` state has been split into a framing-feedback state (`*_NOT_IN_FRAME`) and a new intro guidance state (`INTRO_*`): + | Old key | New key(s) | + |---|---| + | `SENSING_FRONT` | `FRONT_PAGE_NOT_IN_FRAME`, `INTRO_FRONT_PAGE` | + | `SENSING_BACK` | `BACK_PAGE_NOT_IN_FRAME`, `INTRO_BACK_PAGE` | + | `SENSING_DATA_PAGE` | `DATA_PAGE_NOT_IN_FRAME`, `INTRO_DATA_PAGE` | + | `SENSING_TOP_PAGE` | `TOP_PAGE_NOT_IN_FRAME`, `INTRO_TOP_PAGE` | + | `SENSING_LEFT_PAGE` | `LEFT_PAGE_NOT_IN_FRAME`, `INTRO_LEFT_PAGE` | + | `SENSING_RIGHT_PAGE` | `RIGHT_PAGE_NOT_IN_FRAME`, `INTRO_RIGHT_PAGE` | + | `SENSING_LAST_PAGE` | `LAST_PAGE_NOT_IN_FRAME`, `INTRO_LAST_PAGE` | + | `SCAN_BARCODE` | `PROCESSING_BARCODE` | - Updated dependencies - @microblink/blinkid-core@7.7.0 - @microblink/camera-manager@7.3.0 diff --git a/packages/blinkid-ux-manager/docs/interfaces/BlinkIdUxManager.md b/packages/blinkid-ux-manager/docs/interfaces/BlinkIdUxManager.md index 7d1149d..d9090b5 100644 --- a/packages/blinkid-ux-manager/docs/interfaces/BlinkIdUxManager.md +++ b/packages/blinkid-ux-manager/docs/interfaces/BlinkIdUxManager.md @@ -508,6 +508,22 @@ Returns the timeout duration in ms. Null if timeout won't be triggered ever. *** +### handleCameraManagerError() + +> **handleCameraManagerError**(`error`): `void` + +#### Parameters + +##### error + +`Error` + +#### Returns + +`void` + +*** + ### isHapticFeedbackEnabled() > **isHapticFeedbackEnabled**(): `boolean` diff --git a/packages/blinkid-ux-manager/package.json b/packages/blinkid-ux-manager/package.json index 6b2b5dd..88ea4af 100644 --- a/packages/blinkid-ux-manager/package.json +++ b/packages/blinkid-ux-manager/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkid-ux-manager", "description": "BlinkID UX Manager provides user feedback based on the blinkid process results.", - "version": "7.7.1", + "version": "7.7.2", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.test.ts b/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.test.ts index 5ff86f8..0536ab6 100644 --- a/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.test.ts +++ b/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.test.ts @@ -21,16 +21,11 @@ vi.mock("@microblink/ux-common/utils", async (importOriginal) => { }; }); +import { AnalyticService } from "@microblink/analytics/AnalyticService"; import type { BlinkIdScanningResult, - RemoteScanningSession, DocumentClassInfo, } from "@microblink/blinkid-core"; -import type { - CameraManager, - FrameCaptureCallback, - PlaybackState, -} from "@microblink/camera-manager"; import { createFakeImageData, enableRafAwareFakeTimers, @@ -42,10 +37,12 @@ import { import type { BlinkIdUiState, BlinkIdUiStateKey } from "./blinkid-ui-state"; import { blinkIdUiStateMap } from "./blinkid-ui-state"; import type { BlinkIdUxManager } from "./BlinkIdUxManager"; -import type { BlinkIdUxManagerOptions } from "./createBlinkIdUxManager"; +import { + createBlinkIdUxManager as createBlinkIdUxManagerFactory, + type BlinkIdUxManagerOptions, +} from "./createBlinkIdUxManager"; import { createBlinkIdCameraHarness, - createBlinkIdManager, createBlinkIdUnitSessionMock, type BlinkIdUnitSessionMock, } from "./test-helpers.integration"; @@ -60,44 +57,7 @@ import { createProcessResult } from "./__testdata/blinkidTestFixtures"; * and does not own end-to-end scan flow coverage (see integration tests). */ -// ============================================================================ -// Shared Types -// ============================================================================ - -interface TestCameraState { - playbackState: PlaybackState; - videoElement: HTMLVideoElement | undefined; -} - -// ============================================================================ -// Mock Factories -// ============================================================================ - -const createMockCameraManager = (overrides?: { - playbackStateCallback?: (callback: (state: PlaybackState) => void) => void; -}) => ({ - addFrameCaptureCallback: vi.fn().mockReturnValue(vi.fn()), - subscribe: overrides?.playbackStateCallback - ? vi - .fn() - .mockImplementationOnce( - ( - _selector: (s: TestCameraState) => PlaybackState, - callback: (state: PlaybackState) => void, - ) => { - overrides.playbackStateCallback!(callback); - return vi.fn(); - }, - ) - .mockImplementation(() => vi.fn()) - : vi.fn().mockReturnValue(vi.fn()), - stopFrameCapture: vi.fn(), - startFrameCapture: vi.fn().mockResolvedValue(undefined), - startCameraStream: vi.fn().mockResolvedValue(undefined), - isActive: true, -}); - -type MockCameraManager = ReturnType; +type BlinkIdCameraHarness = ReturnType; type MockScanningSession = BlinkIdUnitSessionMock; const trackManager = setupDestroyableTeardown(); @@ -115,38 +75,94 @@ const applyStabilizedUiStateForContractTest = async ( await flushUiRaf(); }; -const createBlinkIdUxManager = async ( - mockCameraManager: MockCameraManager, +const createManagedBlinkIdUxManager = async ( + cameraHarness: BlinkIdCameraHarness, mockScanningSession: MockScanningSession, options?: BlinkIdUxManagerOptions, ): Promise => trackManager( - await createBlinkIdManager( - mockCameraManager as unknown as CameraManager, - mockScanningSession as unknown as RemoteScanningSession, + await createBlinkIdUxManagerFactory( + cameraHarness.cameraManager, + mockScanningSession as unknown as Parameters< + typeof createBlinkIdUxManagerFactory + >[1], options, ), ); -const createBlinkIdUxManagerWithFakeCamera = async ( - initialCameraPermission?: "prompt" | "granted" | "denied" | "blocked", -) => { +const createBlinkIdTestContext = async ({ + initialCameraPermission, + fakeCameraOptions, + sessionSettings, + managerOptions, +}: { + initialCameraPermission?: "prompt" | "granted" | "denied" | "blocked"; + fakeCameraOptions?: Parameters[0]; + sessionSettings?: Parameters[0]; + managerOptions?: BlinkIdUxManagerOptions; +} = {}) => { const cameraHarness = createBlinkIdCameraHarness( - initialCameraPermission - ? { initialState: { cameraPermission: initialCameraPermission } } - : undefined, + fakeCameraOptions ?? + (initialCameraPermission + ? { initialState: { cameraPermission: initialCameraPermission } } + : undefined), ); - const session = createBlinkIdUnitSessionMock(); - const manager = trackManager( - await createBlinkIdManager(cameraHarness.cameraManager, session), + const scanningSession = createBlinkIdUnitSessionMock(sessionSettings); + const manager = await createManagedBlinkIdUxManager( + cameraHarness, + scanningSession, + managerOptions, ); + // Even already-resolved async setup uses microtasks before session data is visible. + await Promise.resolve(); + return { + cameraHarness, + scanningSession, manager, - fakeCameraManager: cameraHarness.fakeCameraManager, }; }; +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("BlinkIdUxManager - startup and camera analytics", () => { + test("logs device info and playback events", async () => { + const logDeviceInfoSpy = vi.spyOn( + AnalyticService.prototype, + "logDeviceInfo", + ); + + const { cameraHarness, manager } = await createBlinkIdTestContext(); + + expect(logDeviceInfoSpy).toHaveBeenCalledWith(manager.deviceInfo); + logDeviceInfoSpy.mockRestore(); + + const logCameraStartedEventSpy = vi.spyOn( + manager.analytics, + "logCameraStartedEvent", + ); + const logCameraClosedEventSpy = vi.spyOn( + manager.analytics, + "logCameraClosedEvent", + ); + const sendPingletsSpy = vi.spyOn(manager.analytics, "sendPinglets"); + + logCameraStartedEventSpy.mockClear(); + logCameraClosedEventSpy.mockClear(); + sendPingletsSpy.mockClear(); + + cameraHarness.emitPlaybackState("capturing"); + expect(logCameraStartedEventSpy).toHaveBeenCalledTimes(1); + expect(sendPingletsSpy).toHaveBeenCalled(); + + cameraHarness.emitPlaybackState("idle"); + expect(logCameraClosedEventSpy).toHaveBeenCalledTimes(1); + }); +}); + describe("BlinkIdUxManager - package-specific: camera permission analytics", () => { type PermissionTransitionCase = { name: string; @@ -188,10 +204,9 @@ describe("BlinkIdUxManager - package-specific: camera permission analytics", () ]; test.each(permissionTransitionCases)("$name", async (testCase) => { - const { manager, fakeCameraManager } = - await createBlinkIdUxManagerWithFakeCamera( - testCase.initialCameraPermission, - ); + const { cameraHarness, manager } = await createBlinkIdTestContext({ + initialCameraPermission: testCase.initialCameraPermission, + }); const checkSpy = vi.spyOn(manager.analytics, "logCameraPermissionCheck"); const requestSpy = vi.spyOn( @@ -209,7 +224,7 @@ describe("BlinkIdUxManager - package-specific: camera permission analytics", () responseSpy.mockClear(); sendSpy.mockClear(); - fakeCameraManager.emitState({ + cameraHarness.fakeCameraManager.emitState({ cameraPermission: testCase.nextCameraPermission, }); @@ -237,26 +252,159 @@ describe("BlinkIdUxManager - package-specific: camera permission analytics", () }); }); +describe("BlinkIdUxManager - package-specific: camera input analytics", () => { + beforeEach(() => { + enableRafAwareFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + const fakeCameraOptions = { + initialState: { + selectedCamera: { name: "default-camera", facingMode: "back" as const }, + videoResolution: { width: 1920, height: 1080 }, + }, + }; + + test("coalesces orientation-like updates into one camera input ping", async () => { + const { cameraHarness, manager } = await createBlinkIdTestContext({ + fakeCameraOptions, + }); + + const logCameraInputInfoSpy = vi.spyOn( + manager.analytics, + "logCameraInputInfo", + ); + + logCameraInputInfoSpy.mockClear(); + + cameraHarness.emitCameraState({ + videoResolution: { width: 1080, height: 1920 }, + extractionArea: { x: 0, y: 0, width: 1080, height: 1920 }, + }); + + cameraHarness.emitCameraState({ + videoResolution: { width: 1920, height: 1080 }, + }); + + cameraHarness.emitCameraState({ + videoResolution: { width: 1080, height: 1920 }, + extractionArea: { x: 0, y: 5, width: 1080, height: 1910 }, + }); + + expect(logCameraInputInfoSpy).not.toHaveBeenCalled(); + await vi.runOnlyPendingTimersAsync(); + + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(1); + expect(logCameraInputInfoSpy).toHaveBeenCalledWith({ + deviceId: "default-camera", + cameraFacing: "Back", + cameraFrameWidth: 1080, + cameraFrameHeight: 1920, + roiWidth: 1080, + roiHeight: 1910, + viewPortAspectRatio: 1080 / 1910, + }); + }); + + test("sends immediate and debounced pings for separated updates", async () => { + const { cameraHarness, manager } = await createBlinkIdTestContext({ + fakeCameraOptions, + }); + + const logCameraInputInfoSpy = vi.spyOn( + manager.analytics, + "logCameraInputInfo", + ); + + logCameraInputInfoSpy.mockClear(); + + cameraHarness.emitCameraState({ + selectedCamera: { name: "default-camera", facingMode: "back" }, + }); + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(1); + + cameraHarness.emitCameraState({ + extractionArea: { x: 0, y: 5, width: 1080, height: 1910 }, + }); + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(1); + + await vi.runOnlyPendingTimersAsync(); + expect(logCameraInputInfoSpy).toHaveBeenCalledTimes(2); + }); + + test("does not send delayed camera input ping after reset or observer cleanup", async () => { + const resetContext = await createBlinkIdTestContext({ fakeCameraOptions }); + const resetSpy = vi.spyOn( + resetContext.manager.analytics, + "logCameraInputInfo", + ); + + resetSpy.mockClear(); + resetContext.cameraHarness.emitCameraState({ + videoResolution: { width: 1000, height: 500 }, + }); + resetContext.manager.reset(); + await vi.runOnlyPendingTimersAsync(); + expect(resetSpy).not.toHaveBeenCalled(); + + const cleanupContext = await createBlinkIdTestContext({ + fakeCameraOptions, + }); + const cleanupSpy = vi.spyOn( + cleanupContext.manager.analytics, + "logCameraInputInfo", + ); + + cleanupSpy.mockClear(); + cleanupContext.cameraHarness.emitCameraState({ + videoResolution: { width: 1000, height: 500 }, + }); + cleanupContext.manager.cleanupAllObservers(); + await vi.runOnlyPendingTimersAsync(); + expect(cleanupSpy).not.toHaveBeenCalled(); + }); +}); + +describe("BlinkIdUxManager - package-specific: camera frame-capture loop errors", () => { + test("logs a non-fatal ping when camera manager reports a frame-loop error", async () => { + const { cameraHarness, manager } = await createBlinkIdTestContext(); + const error = new Error( + "Frame capture callback did not return an ArrayBuffer.", + ); + const logErrorSpy = vi.spyOn(manager.analytics, "logErrorEvent"); + const sendPingletsSpy = vi.spyOn(manager.analytics, "sendPinglets"); + + logErrorSpy.mockClear(); + sendPingletsSpy.mockClear(); + + cameraHarness.fakeCameraManager.emitError(error); + + expect(logErrorSpy).toHaveBeenCalledWith({ + origin: "cameraManager.error", + error, + errorType: "NonFatal", + }); + expect(sendPingletsSpy).toHaveBeenCalledTimes(1); + }); +}); + describe("BlinkIdUxManager - package-specific: document class filtering", () => { - let mockCameraManager: MockCameraManager; + let cameraHarness: BlinkIdCameraHarness; let mockScanningSession: MockScanningSession; let manager: BlinkIdUxManager; - let frameCaptureCallback: FrameCaptureCallback; + let emitFrame: BlinkIdCameraHarness["emitFrame"]; beforeEach(async () => { - vi.clearAllMocks(); - - mockCameraManager = createMockCameraManager(); + cameraHarness = createBlinkIdCameraHarness(); mockScanningSession = createBlinkIdUnitSessionMock(); - - manager = await createBlinkIdUxManager( - mockCameraManager, + manager = await createManagedBlinkIdUxManager( + cameraHarness, mockScanningSession, ); - - expect(mockCameraManager.addFrameCaptureCallback).toHaveBeenCalledTimes(1); - frameCaptureCallback = - mockCameraManager.addFrameCaptureCallback.mock.calls[0][0]; + emitFrame = cameraHarness.emitFrame; }); afterEach(() => { @@ -288,7 +436,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => return docInfo.country !== "usa"; }); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Verify callback was invoked with the document class info expect(documentFilteredSpy).toHaveBeenCalledWith( @@ -324,7 +472,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => return docInfo.country === "usa"; }); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Verify callback was invoked with the document class info expect(documentFilteredSpy).not.toHaveBeenCalledWith(mockDocumentClassInfo); @@ -351,7 +499,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => // Add filter that would reject all documents const filterCleanup = manager.addDocumentClassFilter(() => false); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Filter shouldn't be applied because document info is incomplete expect(mockProcessResult.inputImageAnalysisResult?.processingStatus).toBe( @@ -372,7 +520,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => // Add filter that would reject all documents const filterCleanup = manager.addDocumentClassFilter(() => false); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Filter shouldn't be applied because document info is undefined expect(mockProcessResult.inputImageAnalysisResult?.processingStatus).toBe( @@ -397,7 +545,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => mockScanningSession.process.mockResolvedValue(mockProcessResult); // No filter added - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); expect(mockProcessResult.inputImageAnalysisResult?.processingStatus).toBe( "success", @@ -426,7 +574,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => mockScanningSession.process.mockResolvedValue(mockProcessResult); const filterCleanup = manager.addDocumentClassFilter(() => false); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); expect(documentFilteredSpy).toHaveBeenCalledTimes(1); @@ -436,7 +584,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => // Remove the filter and run again filterCleanup(); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Should not invoke callback when filter is removed expect(documentFilteredSpy).not.toHaveBeenCalled(); @@ -465,7 +613,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => // Add second filter that accepts all documents const secondFilterCleanup = manager.addDocumentClassFilter(() => true); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Second filter should take precedence expect(mockProcessResult.inputImageAnalysisResult?.processingStatus).toBe( @@ -506,7 +654,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => resultsReceived.push(result); }); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Verify document filtered callback was called expect(documentFilteredSpy).toHaveBeenCalledWith( @@ -514,7 +662,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => ); // Verify camera was stopped - expect(mockCameraManager.stopFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); // Verify no results were emitted expect(resultsReceived.length).toBe(0); @@ -548,7 +696,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => const cleanupFrameProcessCallback = manager.addOnFrameProcessCallback(frameProcessSpy); - await frameCaptureCallback(createFakeImageData()); + await emitFrame(createFakeImageData()); // Verify frame process callback was called with the process result expect(frameProcessSpy).toHaveBeenCalledWith(mockProcessResult); @@ -579,7 +727,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => // Add filter that rejects all documents const filterCleanup = manager.addDocumentClassFilter(() => false); - const result = await frameCaptureCallback(createFakeImageData()); + const result = await emitFrame(createFakeImageData()); // Verify the arrayBuffer is returned from the callback expect(result).toBe(mockArrayBuffer); @@ -611,7 +759,7 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => // Add filter that accepts all documents const filterCleanup = manager.addDocumentClassFilter(() => true); - const result = await frameCaptureCallback(createFakeImageData()); + const result = await emitFrame(createFakeImageData()); // Verify the arrayBuffer is returned and mapped candidate was captured expect(result).toBe(mockArrayBuffer); @@ -623,30 +771,18 @@ describe("BlinkIdUxManager - package-specific: document class filtering", () => describe("BlinkIdUxManager - session lifecycle: reset behavior", () => { let manager: BlinkIdUxManager; - let frameCaptureCallback: FrameCaptureCallback; - let playbackStateCallback: (state: PlaybackState) => void; - let mockCameraManager: ReturnType; + let cameraHarness: BlinkIdCameraHarness; let mockScanningSession: MockScanningSession; beforeEach(async () => { - vi.clearAllMocks(); vi.useFakeTimers(); - mockCameraManager = createMockCameraManager({ - playbackStateCallback: (cb) => { - playbackStateCallback = cb; - }, - }); + cameraHarness = createBlinkIdCameraHarness(); mockScanningSession = createBlinkIdUnitSessionMock(); - - manager = await createBlinkIdUxManager( - mockCameraManager, + manager = await createManagedBlinkIdUxManager( + cameraHarness, mockScanningSession, ); - - expect(mockCameraManager.addFrameCaptureCallback).toHaveBeenCalledTimes(1); - frameCaptureCallback = - mockCameraManager.addFrameCaptureCallback.mock.calls[0][0]; }); afterEach(() => { @@ -665,22 +801,22 @@ describe("BlinkIdUxManager - session lifecycle: reset behavior", () => { // Verify internal state reset expect(mockScanningSession.reset).toHaveBeenCalled(); expect(uiStateChangedSpy).toHaveBeenCalled(); - expect(mockCameraManager.startFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.startFrameCapture).toHaveBeenCalled(); }); test("resetScanningSession should start camera if not active", async () => { - mockCameraManager.isActive = false; + cameraHarness.setIsActive(false); await manager.resetScanningSession(true); - expect(mockCameraManager.startCameraStream).toHaveBeenCalled(); - expect(mockCameraManager.startFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.startCameraStream).toHaveBeenCalled(); + expect(cameraHarness.startFrameCapture).toHaveBeenCalled(); }); test("resetScanningSession should not start frame capture when startFrameCapture is false", async () => { await manager.resetScanningSession(false); - expect(mockCameraManager.startFrameCapture).not.toHaveBeenCalled(); + expect(cameraHarness.startFrameCapture).not.toHaveBeenCalled(); }); test("should allow overriding initial UI state through manager API", async () => { @@ -698,8 +834,8 @@ describe("BlinkIdUxManager - session lifecycle: reset behavior", () => { }); test("should use constructor initialUiStateKey override", async () => { - const customManager = await createBlinkIdUxManager( - mockCameraManager, + const customManager = await createManagedBlinkIdUxManager( + cameraHarness, mockScanningSession, { initialUiStateKey: "INTRO_DATA_PAGE", @@ -748,12 +884,12 @@ describe("BlinkIdUxManager - session lifecycle: reset behavior", () => { mockScanningSession.process.mockResolvedValue(mockProcessResult); // Process a frame to trigger potential callbacks - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); // Simulate timeout to trigger error callback const timeoutDuration = 5000; manager.setTimeoutDuration(timeoutDuration); - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); vi.advanceTimersByTime(timeoutDuration); // Verify no callbacks were called @@ -767,30 +903,18 @@ describe("BlinkIdUxManager - session lifecycle: reset behavior", () => { describe("BlinkIdUxManager - timeout behavior", () => { let manager: BlinkIdUxManager; - let frameCaptureCallback: FrameCaptureCallback; - let playbackStateCallback: (state: PlaybackState) => void; - let mockCameraManager: ReturnType; + let cameraHarness: BlinkIdCameraHarness; let mockScanningSession: MockScanningSession; beforeEach(async () => { - vi.clearAllMocks(); vi.useFakeTimers(); - mockCameraManager = createMockCameraManager({ - playbackStateCallback: (cb) => { - playbackStateCallback = cb; - }, - }); + cameraHarness = createBlinkIdCameraHarness(); mockScanningSession = createBlinkIdUnitSessionMock(); - - manager = await createBlinkIdUxManager( - mockCameraManager, + manager = await createManagedBlinkIdUxManager( + cameraHarness, mockScanningSession, ); - - expect(mockCameraManager.addFrameCaptureCallback).toHaveBeenCalledTimes(1); - frameCaptureCallback = - mockCameraManager.addFrameCaptureCallback.mock.calls[0][0]; }); afterEach(() => { @@ -806,13 +930,13 @@ describe("BlinkIdUxManager - timeout behavior", () => { manager.addOnErrorCallback(errorCallback); // Simulate camera starting capture - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); // Advance timer to trigger timeout (fake timers: instant, no real wait) vi.advanceTimersByTime(timeoutDuration); expect(errorCallback).toHaveBeenCalledWith("timeout"); - expect(mockCameraManager.stopFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); }); test("should clear timeout when stopping capture", () => { @@ -823,10 +947,10 @@ describe("BlinkIdUxManager - timeout behavior", () => { manager.addOnErrorCallback(errorCallback); // Simulate camera starting capture - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); // Simulate camera stopping capture - playbackStateCallback("idle"); + cameraHarness.emitPlaybackState("idle"); // Advance timer past timeout duration (fake timers: instant) vi.advanceTimersByTime(timeoutDuration + 1000); @@ -841,7 +965,7 @@ describe("BlinkIdUxManager - timeout behavior", () => { manager.addOnErrorCallback(errorCallback); // Simulate camera starting capture - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); // Advance timer (fake timers: instant, no real wait) vi.advanceTimersByTime(20000); @@ -857,7 +981,7 @@ describe("BlinkIdUxManager - timeout behavior", () => { manager.addOnUiStateChangedCallback(uiStateChangedSpy); // Simulate camera starting capture - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); // Advance timer to trigger timeout (fake timers: instant) vi.advanceTimersByTime(timeoutDuration); @@ -888,14 +1012,14 @@ describe("BlinkIdUxManager - timeout behavior", () => { mockScanningSession.process.mockResolvedValue(mockProcessResult); // Start capture and process a frame - playbackStateCallback("capturing"); - await frameCaptureCallback(createFakeImageData()); + cameraHarness.emitPlaybackState("capturing"); + await cameraHarness.emitFrame(createFakeImageData()); // Advance timer but not enough to trigger timeout vi.advanceTimersByTime(timeoutDuration / 2); // Stop capture (this should clear the timeout) - playbackStateCallback("idle"); + cameraHarness.emitPlaybackState("idle"); // Advance timer past where original timeout would have triggered vi.advanceTimersByTime(timeoutDuration / 2 + 100); @@ -905,45 +1029,61 @@ describe("BlinkIdUxManager - timeout behavior", () => { }); }); +describe("BlinkIdUxManager - state transitions: shared callback contracts", () => { + beforeEach(() => { + enableRafAwareFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test("logs error message events when UI state changes to an error state", async () => { + const { manager } = await createBlinkIdTestContext(); + const logErrorMessageEventSpy = vi.spyOn( + manager.analytics, + "logErrorMessageEvent", + ); + + logErrorMessageEventSpy.mockClear(); + await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); + + expect(logErrorMessageEventSpy).toHaveBeenCalledWith("EliminateBlur"); + }); + + test("triggers short haptic feedback when the RAF loop transitions to an error state", async () => { + const { manager } = await createBlinkIdTestContext(); + const shortSpy = vi.spyOn( + manager.getHapticFeedbackManager(), + "triggerShort", + ); + + shortSpy.mockClear(); + await applyStabilizedUiStateForContractTest(manager, "BLUR_DETECTED"); + + expect(shortSpy).toHaveBeenCalled(); + }); +}); + describe( "BlinkIdUxManager - state transitions: intro timing", { timeout: 8000 }, () => { let manager: BlinkIdUxManager; - let mockCameraManager: ReturnType; + let cameraHarness: BlinkIdCameraHarness; let mockScanningSession: MockScanningSession; - let frameCaptureCallback: FrameCaptureCallback; - let playbackStateCallback: (state: PlaybackState) => void; beforeEach(async () => { - vi.clearAllMocks(); vi.useFakeTimers(); - mockCameraManager = createMockCameraManager({ - playbackStateCallback: (cb) => { - playbackStateCallback = cb; + const context = await createBlinkIdTestContext({ + sessionSettings: { + skipImagesWithBlur: true, }, }); - mockScanningSession = createBlinkIdUnitSessionMock({ - skipImagesWithBlur: true, - }); - - manager = await createBlinkIdUxManager( - mockCameraManager, - mockScanningSession, - ); - - expect(mockCameraManager.addFrameCaptureCallback).toHaveBeenCalledTimes( - 1, - ); - frameCaptureCallback = - mockCameraManager.addFrameCaptureCallback.mock.calls[0][0]; - - // The constructor fires async getSettings()/showDemoOverlay()/showProductionOverlay() - // calls. Even though the mocks return already-resolved promises, .then() callbacks - // are always microtasks and never run synchronously. Awaiting here lets the - // microtask queue drain so #sessionSettings is populated before any test runs. - await Promise.resolve(); + cameraHarness = context.cameraHarness; + mockScanningSession = context.scanningSession; + manager = context.manager; }); afterEach(() => { @@ -969,15 +1109,15 @@ describe( // Simulate long idle before capture starts without flushing RAF callbacks. vi.setSystemTime(Date.now() + 10_000); - playbackStateCallback("capturing"); - await frameCaptureCallback(createFakeImageData()); + cameraHarness.emitPlaybackState("capturing"); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // Intro should still be active immediately after first captured frame. expect(manager.uiState.key).toBe("INTRO_FRONT_PAGE"); await jumpTime(blinkIdUiStateMap.INTRO_FRONT_PAGE.minDuration + 100); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); expect(manager.mappedUiStateKey).toBe("PAGE_CAPTURED"); }); @@ -1002,7 +1142,7 @@ describe( // Process first frame at 100ms await jumpTime(100); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // UI state should still be INTRO_FRONT_PAGE @@ -1010,7 +1150,7 @@ describe( // Process another frame at 500ms await jumpTime(400); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // UI state should still be INTRO_FRONT_PAGE @@ -1018,7 +1158,7 @@ describe( // Process another frame at 1500ms (still within intro duration) await jumpTime(1000); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // UI state should still be INTRO_FRONT_PAGE @@ -1042,7 +1182,7 @@ describe( mockScanningSession.process.mockResolvedValue(mockProcessResult); // Process first frame during intro period (before minDuration elapses) - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // First frame is processed without asserting eventual mapped output timing. @@ -1052,7 +1192,7 @@ describe( await jumpTime(introDuration + 100); // Process another frame after intro duration - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // Candidate state should now be mapped from process result. @@ -1080,7 +1220,7 @@ describe( // Process a frame during intro duration (before minDuration elapses) // The FeedbackStabilizer should block state changes until minDuration passes - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // Should still be INTRO_FRONT_PAGE because minDuration hasn't elapsed yet @@ -1101,7 +1241,7 @@ describe( mockScanningSession.process.mockResolvedValue(mockSuccessResult); // Process a frame after mapping produces PAGE_CAPTURED. - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); // PAGE_CAPTURED should be the mapped candidate @@ -1125,8 +1265,8 @@ describe( mockScanningSession.process.mockResolvedValue(mockSuccessResult); // First capture transition consumes pending intro anchor. - playbackStateCallback("capturing"); - await frameCaptureCallback(createFakeImageData()); + cameraHarness.emitPlaybackState("capturing"); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); expect(restartStateTimerSpy).toHaveBeenCalledTimes(1); expect(manager.uiState.key).toBe("INTRO_FRONT_PAGE"); @@ -1136,10 +1276,10 @@ describe( expect(manager.uiState.key).toBe("INTRO_FRONT_PAGE"); // Toggling capture again should not restart INTRO_FRONT_PAGE timing. - playbackStateCallback("idle"); - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("idle"); + cameraHarness.emitPlaybackState("capturing"); await jumpTime(150); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); expect(restartStateTimerSpy).toHaveBeenCalledTimes(1); @@ -1169,39 +1309,16 @@ describe( { timeout: 8000 }, () => { let manager: BlinkIdUxManager; - let mockCameraManager: ReturnType; + let cameraHarness: BlinkIdCameraHarness; let mockScanningSession: MockScanningSession; - let frameCaptureCallback: FrameCaptureCallback; - let playbackStateCallback: (state: PlaybackState) => void; beforeEach(async () => { - vi.clearAllMocks(); enableRafAwareFakeTimers(); - mockCameraManager = createMockCameraManager({ - playbackStateCallback: (cb) => { - playbackStateCallback = cb; - }, - }); - mockScanningSession = createBlinkIdUnitSessionMock(); - - manager = await createBlinkIdUxManager( - mockCameraManager, - mockScanningSession, - ); - - // Capture the frame capture callback for testing - expect(mockCameraManager.addFrameCaptureCallback).toHaveBeenCalledTimes( - 1, - ); - frameCaptureCallback = - mockCameraManager.addFrameCaptureCallback.mock.calls[0][0]; - - // The constructor fires async getSettings()/showDemoOverlay()/showProductionOverlay() - // calls. Even though the mocks return already-resolved promises, .then() callbacks - // are always microtasks and never run synchronously. Awaiting here lets the - // microtask queue drain so #sessionSettings is populated before any test runs. - await Promise.resolve(); + const context = await createBlinkIdTestContext(); + cameraHarness = context.cameraHarness; + mockScanningSession = context.scanningSession; + manager = context.manager; }); afterEach(() => { @@ -1230,10 +1347,10 @@ describe( mockScanningSession.process.mockResolvedValue(mockProcessResult); mockScanningSession.getResult.mockResolvedValue(mockResult); - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); // Simulate frame capture - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); // Flush RAF-driven state update await tickRaf(); @@ -1247,7 +1364,7 @@ describe( // Verify the complete flow expect(manager.uiState.key).toBe("DOCUMENT_CAPTURED"); - expect(mockCameraManager.stopFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); await vi.waitFor(() => { expect(mockScanningSession.getResult).toHaveBeenCalled(); expect(resultCallback).toHaveBeenCalledWith(mockResult); @@ -1326,7 +1443,7 @@ describe( mockScanningSession.process.mockResolvedValue(mockProcessResult); // Simulate frame capture - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); // Wait for PAGE_CAPTURED state (SUCCESS_DURATION = 800ms) await jumpTime(blinkIdUiStateMap.PAGE_CAPTURED.minDuration + 100); @@ -1342,7 +1459,7 @@ describe( expect(manager.mappedUiStateKey).toBe("PAGE_CAPTURED"); // Verify camera capture was stopped when PAGE_CAPTURED state was reached - expect(mockCameraManager.stopFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); }, ); @@ -1363,8 +1480,8 @@ describe( // state (stabilizer, mappedUiStateKey, pendingIntroAnchorKey) is consistent. manager.setInitialUiStateKey("INTRO_BACK_PAGE", true); - playbackStateCallback("capturing"); - await frameCaptureCallback(createFakeImageData()); + cameraHarness.emitPlaybackState("capturing"); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); expect(manager.mappedUiStateKey).toBe("DOCUMENT_CAPTURED"); @@ -1393,7 +1510,7 @@ describe( mockScanningSession.process.mockResolvedValue(mockProcessResult); mockScanningSession.getResult.mockResolvedValue({ someData: "test" }); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); await jumpTime(blinkIdUiStateMap.INTRO_FRONT_PAGE.minDuration + 100); await tickRaf(); @@ -1418,12 +1535,12 @@ describe( mockScanningSession.getResult.mockResolvedValue({ someData: "test" }); // Simulate frame capture - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); expect(manager.mappedUiStateKey).toBe("DOCUMENT_CAPTURED"); // Verify camera capture was stopped when DOCUMENT_CAPTURED state was reached - expect(mockCameraManager.stopFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); }); test("should not trigger document capture flow for incomplete results", async () => { @@ -1442,7 +1559,7 @@ describe( mockScanningSession.process.mockResolvedValue(mockProcessResult); // Simulate frame capture - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); @@ -1463,8 +1580,8 @@ describe( }); mockScanningSession.process.mockResolvedValue(mockProcessResult); - await frameCaptureCallback(createFakeImageData()); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); const pageCapturedQueueEntries = manager.feedbackStabilizer @@ -1499,8 +1616,8 @@ describe( async () => pendingProcessResult, ); - const firstFramePromise = frameCaptureCallback(createFakeImageData()); - const secondFrameResult = await frameCaptureCallback( + const firstFramePromise = cameraHarness.emitFrame(createFakeImageData()); + const secondFrameResult = await cameraHarness.emitFrame( createFakeImageData(), ); @@ -1515,6 +1632,73 @@ describe( expect(mockScanningSession.process).toHaveBeenCalledTimes(1); }); + test("reports non-fatal pinglets when frame processing rejects with a recoverable error", async () => { + mockScanningSession.process.mockRejectedValue( + new Error("Worker process failure"), + ); + + await expect( + cameraHarness.emitFrame(createFakeImageData()), + ).rejects.toThrow("Worker process failure"); + + expect(mockScanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: "ux.frameCapture: Worker process failure", + }), + }), + ); + expect(mockScanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); + + test("reports non-fatal pinglets when frame processing rejects with a WASM runtime error", async () => { + mockScanningSession.process.mockRejectedValue( + new Error("RuntimeError: Out of bounds memory access"), + ); + + await expect( + cameraHarness.emitFrame(createFakeImageData()), + ).rejects.toThrow("RuntimeError: Out of bounds memory access"); + + expect(mockScanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: + "ux.frameCapture: RuntimeError: Out of bounds memory access", + }), + }), + ); + expect(mockScanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); + + test("reports non-fatal pinglets when frame processing rejects with a frame transfer error", async () => { + const frameTransferError = new Error( + "Failed to transfer frame to worker", + ); + frameTransferError.name = "FrameTransferError"; + + mockScanningSession.process.mockRejectedValue(frameTransferError); + + await expect( + cameraHarness.emitFrame(createFakeImageData()), + ).rejects.toThrow("Failed to transfer frame to worker"); + + expect(mockScanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: "ux.frameCapture: Failed to transfer frame to worker", + }), + }), + ); + expect(mockScanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); + test("should skip further process calls after terminal document capture", async () => { const mockProcessResult = createProcessResult({ inputImageAnalysisResult: { @@ -1528,9 +1712,9 @@ describe( mockScanningSession.process.mockResolvedValue(mockProcessResult); mockScanningSession.getResult.mockResolvedValue({ someData: "done" }); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); await tickRaf(); - await frameCaptureCallback(createFakeImageData()); + await cameraHarness.emitFrame(createFakeImageData()); expect(mockScanningSession.process).toHaveBeenCalledTimes(1); }); @@ -1543,17 +1727,13 @@ describe( manager.setTimeoutDuration(1000); // Simulate starting capture - const subscribeCall = mockCameraManager.subscribe.mock.calls[0]; - const playbackStateCallback = subscribeCall[1] as ( - state: PlaybackState, - ) => void; - playbackStateCallback("capturing"); + cameraHarness.emitPlaybackState("capturing"); // Advance time past timeout await vi.advanceTimersByTimeAsync(1100); // Verify timeout handling - expect(mockCameraManager.stopFrameCapture).toHaveBeenCalled(); + expect(cameraHarness.stopFrameCapture).toHaveBeenCalled(); expect(errorCallback).toHaveBeenCalledWith("timeout"); expect(mockScanningSession.getResult).not.toHaveBeenCalled(); }); @@ -1576,6 +1756,16 @@ describe( expect(errorCallback).toHaveBeenCalledWith("result_retrieval_failed"); // result callback must NOT fire — no result was retrieved expect(resultCallback).not.toHaveBeenCalled(); + expect(mockScanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "NonFatal", + errorMessage: "ux.getSessionResult: Worker RPC failure", + }), + }), + ); + expect(mockScanningSession.sendPinglets).toHaveBeenCalledTimes(1); }); }); }, diff --git a/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.ts b/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.ts index 992a0ae..82dea09 100644 --- a/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.ts +++ b/packages/blinkid-ux-manager/src/core/BlinkIdUxManager.ts @@ -213,8 +213,11 @@ export class BlinkIdUxManager { const removeFrameCaptureCallback = this.cameraManager.addFrameCaptureCallback(this.#frameCaptureCallback); + const removeCameraManagerErrorCallback = + this.cameraManager.addErrorCallback(this.handleCameraManagerError); this.#cleanupCallbacks.add(removeFrameCaptureCallback); + this.#cleanupCallbacks.add(removeCameraManagerErrorCallback); this.startUiUpdateLoop(); } @@ -1010,6 +1013,14 @@ export class BlinkIdUxManager { } return processResult.arrayBuffer; + } catch (error) { + await this.#analytics.logErrorEvent({ + origin: "ux.frameCapture", + error, + errorType: "NonFatal", + }); + await this.#analytics.sendPinglets(); + throw error; } finally { if (this.#processingLifecycleState === "busy") { this.#processingLifecycleState = "ready"; @@ -1017,6 +1028,15 @@ export class BlinkIdUxManager { } }; + handleCameraManagerError = (error: Error) => { + void this.#analytics.logErrorEvent({ + origin: "cameraManager.error", + error, + errorType: "NonFatal", + }); + void this.#analytics.sendPinglets(); + }; + #handleProcessResultSideEffects = ( mappedUiStateKey?: BlinkIdUiStateKey, ): void => { @@ -1152,7 +1172,6 @@ export class BlinkIdUxManager { err, ); this.#invokeOnErrorCallbacks("result_retrieval_failed"); - void this.#analytics.sendPinglets(); } finally { this.#processingLifecycleState = "terminal"; } @@ -1214,7 +1233,17 @@ export class BlinkIdUxManager { * @returns The result. */ async getSessionResult(): Promise { - return this.scanningSession.getResult(); + try { + return await this.scanningSession.getResult(); + } catch (error) { + await this.#analytics.logErrorEvent({ + origin: "ux.getSessionResult", + error, + errorType: "NonFatal", + }); + await this.#analytics.sendPinglets(); + throw error; + } } /** diff --git a/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.test.ts b/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.test.ts new file mode 100644 index 0000000..67ba824 --- /dev/null +++ b/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.test.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2026 Microblink Ltd. All rights reserved. + */ + +import type { RemoteScanningSession } from "@microblink/blinkid-core"; +import { getDeviceInfo } from "@microblink/blinkid-core"; +import type { CameraManager } from "@microblink/camera-manager"; +import { createFakeScanningSession } from "@microblink/test-utils"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { BlinkIdUxManager } from "./BlinkIdUxManager"; +import { createBlinkIdUxManager } from "./createBlinkIdUxManager"; + +vi.mock("./BlinkIdUxManager", () => ({ + BlinkIdUxManager: vi.fn(), +})); + +vi.mock("@microblink/blinkid-core", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + getDeviceInfo: vi.fn(), + }; +}); + +describe("createBlinkIdUxManager", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("wires async dependencies into the BlinkIdUxManager constructor", async () => { + const SHOW_DEMO_OVERLAY = true; + const SHOW_PRODUCTION_OVERLAY = false; + + const cameraManager = {} as CameraManager; + const sessionSettings = { inputImageSource: "video", scanningSettings: {} }; + const scanningSession = createFakeScanningSession({ + settings: sessionSettings, + showDemoOverlay: SHOW_DEMO_OVERLAY, + showProductionOverlay: SHOW_PRODUCTION_OVERLAY, + }); + + const deviceInfo = { userAgent: "ua" } as Awaited< + ReturnType + >; + vi.mocked(getDeviceInfo).mockResolvedValue(deviceInfo); + + const instance = {} as BlinkIdUxManager; + vi.mocked(BlinkIdUxManager).mockImplementation(() => instance); + + const result = await createBlinkIdUxManager( + cameraManager, + scanningSession as unknown as RemoteScanningSession, + ); + + expect(result).toBe(instance); + expect(getDeviceInfo).toHaveBeenCalledTimes(1); + expect(BlinkIdUxManager).toHaveBeenCalledWith( + cameraManager, + scanningSession, + {}, + sessionSettings, + SHOW_DEMO_OVERLAY, + SHOW_PRODUCTION_OVERLAY, + deviceInfo, + ); + }); + + test("best-effort reports setup failures through the scanning session", async () => { + const cameraManager = {} as CameraManager; + const scanningSession = createFakeScanningSession({ + overrides: { + getSettings: vi.fn().mockRejectedValue(new Error("rpc failed")), + }, + }); + + await expect( + createBlinkIdUxManager( + cameraManager, + scanningSession as unknown as RemoteScanningSession, + ), + ).rejects.toThrow("rpc failed"); + + expect(scanningSession.ping).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + data: expect.objectContaining({ + errorType: "Crash", + errorMessage: "ux.createBlinkIdUxManager: rpc failed", + }), + }), + ); + expect(scanningSession.sendPinglets).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.ts b/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.ts index 1138594..a49bd07 100644 --- a/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.ts +++ b/packages/blinkid-ux-manager/src/core/createBlinkIdUxManager.ts @@ -30,21 +30,52 @@ export const createBlinkIdUxManager = async ( scanningSession: RemoteScanningSession, options: BlinkIdUxManagerOptions = {}, ): Promise => { - const [sessionSettings, showDemoOverlay, showProductionOverlay, deviceInfo] = - await Promise.all([ + try { + const [ + sessionSettings, + showDemoOverlay, + showProductionOverlay, + deviceInfo, + ] = await Promise.all([ scanningSession.getSettings(), scanningSession.showDemoOverlay(), scanningSession.showProductionOverlay(), getDeviceInfo(), ]); - return new BlinkIdUxManager( - cameraManager, - scanningSession, - options, - sessionSettings, - showDemoOverlay, - showProductionOverlay, - deviceInfo, - ); + return new BlinkIdUxManager( + cameraManager, + scanningSession, + options, + sessionSettings, + showDemoOverlay, + showProductionOverlay, + deviceInfo, + ); + } catch (error) { + try { + await scanningSession.ping({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + data: { + errorType: "Crash", + errorMessage: `ux.createBlinkIdUxManager: ${ + error instanceof Error ? error.message : String(error) + }`, + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + } catch (pingError) { + console.warn("Failed to report error pinglet:", pingError); + throw error; + } + + try { + await scanningSession.sendPinglets(); + } catch (sendError) { + console.warn("Failed to flush error pinglets:", sendError); + } + + throw error; + } }; diff --git a/packages/blinkid-ux-manager/src/core/test-helpers.integration.ts b/packages/blinkid-ux-manager/src/core/test-helpers.integration.ts index a99ca09..ecf02e4 100644 --- a/packages/blinkid-ux-manager/src/core/test-helpers.integration.ts +++ b/packages/blinkid-ux-manager/src/core/test-helpers.integration.ts @@ -5,64 +5,31 @@ import type { BlinkIdScanningResult, BlinkIdSessionSettings, - RemoteScanningSession, ScanningSettings, } from "@microblink/blinkid-core"; import type { CameraManager } from "@microblink/camera-manager"; import { + createFakeCameraHarness, createFakeScanningSession, + type CreateFakeCameraManagerOptions, + type FakeCameraHarness, type FakeScanningSession, - FakeCameraManager, } from "@microblink/test-utils"; import type { BlinkIdUxManager } from "./BlinkIdUxManager"; import { createProcessResult, createSessionSettings, } from "./__testdata/blinkidTestFixtures"; -import { - createBlinkIdUxManager, - type BlinkIdUxManagerOptions, -} from "./createBlinkIdUxManager"; - -export type CameraInputState = { - selectedCamera?: { name: string; facingMode?: "front" | "back" }; - videoResolution?: { width: number; height: number }; - extractionArea?: { x: number; y: number; width: number; height: number }; -}; +import { createBlinkIdUxManager } from "./createBlinkIdUxManager"; -export type BlinkIdCameraHarness = { - cameraManager: CameraManager; - fakeCameraManager: FakeCameraManager; - emitPlaybackState: (playbackState: "idle" | "playback" | "capturing") => void; - emitFrame: (imageData: ImageData) => Promise; - emitCameraState: (nextState: Partial) => void; - setIsActive: (value: boolean) => void; - stopFrameCapture: FakeCameraManager["stopFrameCapture"]; - startFrameCapture: FakeCameraManager["startFrameCapture"]; - startCameraStream: FakeCameraManager["startCameraStream"]; -}; +export type BlinkIdCameraHarness = FakeCameraHarness; export const createBlinkIdCameraHarness = ( - fakeCameraOptions?: ConstructorParameters[0], -): BlinkIdCameraHarness => { - const fakeCameraManager = new FakeCameraManager(fakeCameraOptions); - return { - cameraManager: fakeCameraManager as unknown as CameraManager, - fakeCameraManager, - emitPlaybackState: (playbackState) => - fakeCameraManager.emitPlaybackState(playbackState), - emitFrame: (imageData) => fakeCameraManager.emitFrame(imageData), - emitCameraState: (nextState) => fakeCameraManager.emitState(nextState), - setIsActive: (value) => { - fakeCameraManager.isActive = value; - }, - stopFrameCapture: fakeCameraManager.stopFrameCapture, - startFrameCapture: fakeCameraManager.startFrameCapture, - startCameraStream: fakeCameraManager.startCameraStream, - }; -}; + fakeCameraOptions?: CreateFakeCameraManagerOptions, +): BlinkIdCameraHarness => + createFakeCameraHarness(fakeCameraOptions); -export type BlinkIdSessionMock = FakeScanningSession< +type BlinkIdSessionMock = FakeScanningSession< ReturnType, BlinkIdSessionSettings, BlinkIdScanningResult @@ -78,37 +45,10 @@ export type CreateBlinkIdIntegrationContextOptions = { sessionSettings?: BlinkIdSessionSettings; showDemoOverlay?: boolean; showProductionOverlay?: boolean; - fakeCameraOptions?: ConstructorParameters[0]; + fakeCameraOptions?: CreateFakeCameraManagerOptions; sessionOverrides?: Partial; }; -export type BlinkIdIntegrationContext = { - manager: BlinkIdUxManager; - fakeCameraManager: FakeCameraManager; - scanningSession: BlinkIdSessionMock; -}; - -type CreateBlinkIdSessionMockOptions = { - sessionSettings?: BlinkIdSessionSettings; - showDemoOverlay?: boolean; - showProductionOverlay?: boolean; - sessionOverrides?: Partial; -}; - -export const createBlinkIdSessionMock = ( - options: CreateBlinkIdSessionMockOptions = {}, -): BlinkIdSessionMock => - createFakeScanningSession< - ReturnType, - BlinkIdSessionSettings, - BlinkIdScanningResult - >({ - settings: options.sessionSettings ?? createSessionSettings(), - showDemoOverlay: options.showDemoOverlay ?? false, - showProductionOverlay: options.showProductionOverlay ?? false, - overrides: options.sessionOverrides, - }); - export const createBlinkIdUnitSessionMock = ( overrideSettings?: Partial, ): BlinkIdUnitSessionMock => @@ -122,30 +62,27 @@ export const createBlinkIdUnitSessionMock = ( showProductionOverlay: false, }); -export const createBlinkIdManager = async ( - cameraManager: CameraManager, - scanningSession: BlinkIdSessionMock | RemoteScanningSession, - options?: BlinkIdUxManagerOptions, -): Promise => - createBlinkIdUxManager( - cameraManager, - scanningSession as unknown as RemoteScanningSession, - options, - ); - export const createBlinkIdIntegrationContext = async ( options: CreateBlinkIdIntegrationContextOptions = {}, -): Promise => { +): Promise<{ + manager: BlinkIdUxManager; + fakeCameraManager: BlinkIdCameraHarness["fakeCameraManager"]; + scanningSession: BlinkIdSessionMock; +}> => { const cameraHarness = createBlinkIdCameraHarness(options.fakeCameraOptions); - const scanningSession = createBlinkIdSessionMock({ - sessionSettings: options.sessionSettings, - showDemoOverlay: options.showDemoOverlay, - showProductionOverlay: options.showProductionOverlay, - sessionOverrides: options.sessionOverrides, + const scanningSession = createFakeScanningSession< + ReturnType, + BlinkIdSessionSettings, + BlinkIdScanningResult + >({ + settings: options.sessionSettings ?? createSessionSettings(), + showDemoOverlay: options.showDemoOverlay ?? false, + showProductionOverlay: options.showProductionOverlay ?? false, + overrides: options.sessionOverrides, }); - const manager = await createBlinkIdManager( + const manager = await createBlinkIdUxManager( cameraHarness.cameraManager, - scanningSession, + scanningSession as unknown as Parameters[1], ); return { diff --git a/packages/blinkid-ux-manager/src/ui/BlinkIdFeedbackUi.tsx b/packages/blinkid-ux-manager/src/ui/BlinkIdFeedbackUi.tsx index 6fecc09..421e6ea 100644 --- a/packages/blinkid-ux-manager/src/ui/BlinkIdFeedbackUi.tsx +++ b/packages/blinkid-ux-manager/src/ui/BlinkIdFeedbackUi.tsx @@ -15,13 +15,7 @@ import { Switch, } from "solid-js"; import { createWithSignal } from "solid-zustand"; -import { - blinkIdPageTransitionKeys, - blinkIdUiIntroStateKeys, - BlinkIdUiState, - BlinkIdUiStateKey, - blinkIdUiStepSuccessKeys, -} from "../core/blinkid-ui-state"; +import { BlinkIdUiState } from "../core/blinkid-ui-state"; import { LocalizationProvider, LocalizationStrings, @@ -102,25 +96,8 @@ export const BlinkIdFeedbackUi: Component<{ const isProcessing = () => playbackState() === "capturing"; - /** - * These UI states pause frame processing, however we treat them as if we are - * still in processing state from a UX perspective - */ - const pseudoProcessingKeys: BlinkIdUiStateKey[] = [ - ...blinkIdUiIntroStateKeys, - ...blinkIdPageTransitionKeys, - ...blinkIdUiStepSuccessKeys, - ]; - - // Processing is stopped, but we still want to show the feedback - const shouldShowFeedback = () => { - return ( - // processing + pseudo-processing - (isProcessing() || pseudoProcessingKeys.includes(uiState().key)) && - // never show while modal is open - !isModalOpen() - ); - }; + // TODO: Cover cases where frame processing is paused by 3rd party modal dialogs + const shouldShowFeedback = () => !isModalOpen(); const displayTimeoutModal = () => Boolean(store.showTimeoutModal) && store.errorState === "timeout"; diff --git a/packages/blinkid-wasm/CHANGELOG.md b/packages/blinkid-wasm/CHANGELOG.md index 5b1a605..615d5e1 100644 --- a/packages/blinkid-wasm/CHANGELOG.md +++ b/packages/blinkid-wasm/CHANGELOG.md @@ -1,5 +1,7 @@ # @microblink/blinkid-wasm +## 7.7.2 + ## 7.7.1 ### Patch Changes diff --git a/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.js b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.js index bfbdad7..1f14b02 100644 --- a/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.js +++ b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.js @@ -1,7 +1,7 @@ async function createModule(moduleArg={}){var moduleRtn;var h=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope,m=ba&&self.name?.startsWith("em-pthread");let ca;(ca=h).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);h.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof m&&m||a||async function(b){async function c(l,n){var q;(q=h).dataFileDownloads??(q.dataFileDownloads={});try{var p=await fetch(l)}catch(x){throw Error(`Network Error: ${l}`,{e:x});}if(!p.ok)throw Error(`${p.status}: ${p.url}`);q=[];n=Number(p.headers.get("Content-Length")??n);let u=0;h.setStatus?.("Downloading data...");for(p=p.body.getReader();;){var {done:z,value:v}=await p.read();if(z)break; -q.push(v);u+=v.length;h.dataFileDownloads[l]={loaded:u,total:n};let x=0,F=0;for(var w of Object.values(h.dataFileDownloads))x+=w.loaded,F+=w.total;h.setStatus?.(`Downloading data... (${x}/${F})`)}l=new Uint8Array(q.map(x=>x.length).reduce((x,F)=>x+F,0));w=0;for(const x of q)l.set(x,w),w+=x.length;return l.buffer}async function d(l){l.FS_createPath("/","microblink",!0,!0);l.FS_createPath("/microblink","blinkid",!0,!0);for(var n of b.files)l.addRunDependency(`fp ${n.filename}`);l.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.data"); -l.preloadResults??(l.preloadResults={});l.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.data"]={od:!1};k||=await g;(async function(q){if(!q)throw Error("Loading data file failed.");if(q.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");q=new Uint8Array(q);for(var p of b.files){var u=p.filename;l.FS_createDataFile(u,null,q.subarray(p.start,p.end),!0,!0,!0);l.removeRunDependency(`fp ${u}`)}l.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.data")})(k)} +q.push(v);u+=v.length;h.dataFileDownloads[l]={loaded:u,total:n};let x=0,F=0;for(var w of Object.values(h.dataFileDownloads))x+=w.loaded,F+=w.total;h.setStatus?.(`Downloading data... (${x}/${F})`)}l=new Uint8Array(q.map(x=>x.length).reduce((x,F)=>x+F,0));w=0;for(const x of q)l.set(x,w),w+=x.length;return l.buffer}async function d(l){l.FS_createPath("/","microblink",!0,!0);l.FS_createPath("/microblink","blinkid",!0,!0);for(var n of b.files)l.addRunDependency(`fp ${n.filename}`);l.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.data"); +l.preloadResults??(l.preloadResults={});l.preloadResults["/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.data"]={od:!1};k||=await g;(async function(q){if(!q)throw Error("Loading data file failed.");if(q.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");q=new Uint8Array(q);for(var p of b.files){var u=p.filename;l.FS_createDataFile(u,null,q.subarray(p.start,p.end),!0,!0,!0);l.removeRunDependency(`fp ${u}`)}l.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.data")})(k)} "object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=h.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,k=h.getPreloadedPackage?.(e,f);k||(g=c(e,f));if(h.calledRun)d(h);else{let l;((l=h).preRun??(l.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop", start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop",start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop", start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop",start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8326c5065a6d8451d6d004db73babf4a4a9f502c68643a14934cc098d04bf44f.strop",start:2512692,end:2900386},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop", diff --git a/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.wasm b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.wasm index 6bc13d6..d77894d 100755 Binary files a/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.wasm and b/packages/blinkid-wasm/dist/full/advanced-threads/BlinkIdModule.wasm differ diff --git a/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.js b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.js index 68db986..00da2d6 100644 --- a/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.js +++ b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.js @@ -1,14 +1,14 @@ async function createModule(moduleArg={}){var moduleRtn;var l=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope;let ca;(ca=l).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);l.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof ENVIRONMENT_IS_PTHREAD&&ENVIRONMENT_IS_PTHREAD||a||async function(b){async function c(k,m){var p;(p=l).dataFileDownloads??(p.dataFileDownloads={});try{var n=await fetch(k)}catch(x){throw Error(`Network Error: ${k}`,{e:x});}if(!n.ok)throw Error(`${n.status}: ${n.url}`);p=[];m=Number(n.headers.get("Content-Length")??m);let t=0;l.setStatus?.("Downloading data...");for(n=n.body.getReader();;){var {done:z, value:u}=await n.read();if(z)break;p.push(u);t+=u.length;l.dataFileDownloads[k]={loaded:t,total:m};let x=0,E=0;for(var v of Object.values(l.dataFileDownloads))x+=v.loaded,E+=v.total;l.setStatus?.(`Downloading data... (${x}/${E})`)}k=new Uint8Array(p.map(x=>x.length).reduce((x,E)=>x+E,0));v=0;for(const x of p)k.set(x,v),v+=x.length;return k.buffer}async function d(k){k.FS_createPath("/","microblink",!0,!0);k.FS_createPath("/microblink","blinkid",!0,!0);for(var m of b.files)k.addRunDependency(`fp ${m.filename}`); -k.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.data"]={Fc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p= -new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&& -encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop", -start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop",start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop", -start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop",start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8326c5065a6d8451d6d004db73babf4a4a9f502c68643a14934cc098d04bf44f.strop",start:2512692,end:2900386},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2900386,end:3927241},{filename:"/microblink/blinkid/Model_93607315418f2c5d3abec06d0b2a81e69abeb1b8d0506ed6373d7d74bd5e4b05.strop", -start:3927241,end:5172232},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop",start:5172232,end:5468034},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop",start:5468034,end:5474530},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop",start:5474530,end:9570500},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop", -start:9570500,end:9695688},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop",start:9695688,end:9770133},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop",start:9770133,end:9845535},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop",start:9845535,end:10508588},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop", -start:10508588,end:10524261},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:10524261,end:10544730},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:10544730,end:13747128}],remote_package_size:13747128})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; +k.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.data"]={Fc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t, +null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data", +"")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop", +start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop", +start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8326c5065a6d8451d6d004db73babf4a4a9f502c68643a14934cc098d04bf44f.strop",start:2512692,end:2900386},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2900386,end:3927241},{filename:"/microblink/blinkid/Model_93607315418f2c5d3abec06d0b2a81e69abeb1b8d0506ed6373d7d74bd5e4b05.strop",start:3927241,end:5172232},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop", +start:5172232,end:5468034},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop",start:5468034,end:5474530},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop",start:5474530,end:9570500},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop",start:9570500,end:9695688},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop", +start:9695688,end:9770133},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop",start:9770133,end:9845535},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop",start:9845535,end:10508588},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop",start:10508588,end:10524261},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:10524261, +end:10544730},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:10544730,end:13747128}],remote_package_size:13747128})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; (function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkIdModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; if(aa||ba){try{fa=(new URL(".",ea)).href}catch{}ba&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});ha=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);}}var ja=console.log.bind(console),q=console.error.bind(console),ka,la=!1,ma,na,oa,r,w,A,pa,B,D,qa,ra,G,ta,ua=!1; function va(){var a=wa.buffer;r=new Int8Array(a);A=new Int16Array(a);w=new Uint8Array(a);pa=new Uint16Array(a);B=new Int32Array(a);D=new Uint32Array(a);qa=new Float32Array(a);ra=new Float64Array(a);G=new BigInt64Array(a);ta=new BigUint64Array(a)}var wa;function J(a){l.onAbort?.(a);a="Aborted("+a+")";q(a);la=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa?.(a);throw a;}var xa; @@ -91,7 +91,7 @@ const b=Symbol.dispose;b&&(a[b]=a["delete"])})(); Object.assign(Lc.prototype,{Zb(a){this.Pb&&(a=this.Pb(a));return a},Jb(a){this.Oa?.(a)},Pa:hc,Ca:function(a){function b(){return this.mb?Kc(this.wa.Xa,{za:this.pc,xa:c,Ja:this,Ea:a}):Kc(this.wa.Xa,{za:this,xa:a})}var c=this.Zb(a);if(!c)return this.Jb(a),null;var d=Jc(this.wa,c);if(void 0!==d){if(0===d.sa.count.value)return d.sa.xa=c,d.sa.Ea=a,d.clone();d=d.clone();this.Jb(a);return d}d=this.wa.Yb(c);d=wc[d];if(!d)return b.call(this);d=this.lb?d.Ub:d.pointerType;var e=Hc(c,this.wa,d.wa);return null=== e?b.call(this):this.mb?Kc(d.wa.Xa,{za:d,xa:e,Ja:this,Ea:a}):Kc(d.wa.Xa,{za:d,xa:e})}});(async function(){Y=new yd;ob("library_fetch_init");try{Ad=await zd()}catch(a){Ad=!1}finally{nb("library_fetch_init")}})();wa=l.wasmMemory?l.wasmMemory:new WebAssembly.Memory({initial:(l.INITIAL_MEMORY||209715200)/65536,maximum:32768});va();l.noExitRuntime&&(Ha=l.noExitRuntime);l.preloadPlugins&&(pb=l.preloadPlugins);l.print&&(ja=l.print);l.printErr&&(q=l.printErr);l.wasmBinary&&(ka=l.wasmBinary); l.thisProgram&&(da=l.thisProgram);if(l.preInit)for("function"==typeof l.preInit&&(l.preInit=[l.preInit]);0{var k=b?Sa(Oa(a+"/"+b)):a,m=`cp ${k}`;ob(m);try{var p=c;"string"==typeof c&&(p=await kb(c));p=await qb(p,k);h?.();f||Zb(a,b,p,d,e,g)}finally{nb(m)}};l.FS_unlink=(...a)=>Qb(...a);l.FS_createPath=(...a)=>Xb(...a);l.FS_createDevice=(...a)=>$b(...a); -l.FS_createDataFile=(...a)=>Zb(...a);l.FS_createLazyFile=(...a)=>bc(...a);var Kd={342642:(a,b,c,d)=>{a=a?K(w,a):"";b=b?K(w,b):"";c=c?K(w,c):"";d=d?K(w,d):"";throw Error(a+b+c+d);},342858:(a,b)=>{a=a?K(w,a):"";b=b?K(w,b):"";throw Error(a+b);}},Oc,Cd,Pc,db,Ld,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; +l.FS_createDataFile=(...a)=>Zb(...a);l.FS_createLazyFile=(...a)=>bc(...a);var Kd={342626:(a,b,c,d)=>{a=a?K(w,a):"";b=b?K(w,b):"";c=c?K(w,c):"";d=d?K(w,d):"";throw Error(a+b+c+d);},342842:(a,b)=>{a=a?K(w,a):"";b=b?K(w,b):"";throw Error(a+b);}},Oc,Cd,Pc,db,Ld,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; var Md={A:(a,b)=>Ka(a)(b),q:function(a,b,c){La=c;try{var d=Q(a);switch(b){case 0:var e=Ma();if(0>e)break;for(;tb[e];)e++;return Jb(d,e).Ua;case 1:case 2:return 0;case 3:return d.flags;case 4:return e=Ma(),d.flags|=e,0;case 12:return e=Ma(),A[e+0>>1]=2,0;case 13:case 14:return 0}return-28}catch(f){if("undefined"==typeof S||"ErrnoError"!==f.name)throw f;return-f.Ba}},U:function(a,b){try{var c=Q(a),d=c.node,e=c.ta.La;a=e?c:d;e??=d.va.La;Hb(e);var f=e(a);return ec(b,f)}catch(g){if("undefined"==typeof S|| "ErrnoError"!==g.name)throw g;return-g.Ba}},V:function(a,b,c){La=c;try{var d=Q(a);switch(b){case 21509:return d.ya?0:-59;case 21505:if(!d.ya)return-59;if(d.ya.Sa.fc){a=[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];var e=Ma();B[e>>2]=25856;B[e+4>>2]=5;B[e+8>>2]=191;B[e+12>>2]=35387;for(var f=0;32>f;f++)r[e+f+17]=a[f]||0}return 0;case 21510:case 21511:case 21512:return d.ya?0:-59;case 21506:case 21507:case 21508:if(!d.ya)return-59;if(d.ya.Sa.hc)for(e=Ma(),a=[],f=0;32> f;f++)a.push(r[e+f+17]);return 0;case 21519:if(!d.ya)return-59;e=Ma();return B[e>>2]=0;case 21520:return d.ya?-28:-59;case 21537:case 21531:e=Ma();if(!d.ta.ec)throw new N(59);return d.ta.ec(d,b,e);case 21523:if(!d.ya)return-59;d.ya.Sa.ic&&(f=[24,80],e=Ma(),A[e>>1]=f[0],A[e+2>>1]=f[1]);return 0;case 21524:return d.ya?0:-59;case 21515:return d.ya?0:-59;default:return-28}}catch(g){if("undefined"==typeof S||"ErrnoError"!==g.name)throw g;return-g.Ba}},S:function(a,b){try{return a=a?K(w,a):"",ec(b,Rb(a, diff --git a/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.wasm b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.wasm index 428a9c2..83569cc 100755 Binary files a/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.wasm and b/packages/blinkid-wasm/dist/full/advanced/BlinkIdModule.wasm differ diff --git a/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.js b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.js index 92bd634..942022e 100644 --- a/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.js +++ b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.js @@ -1,14 +1,14 @@ async function createModule(moduleArg={}){var moduleRtn;var l=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope;let ca;(ca=l).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);l.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof ENVIRONMENT_IS_PTHREAD&&ENVIRONMENT_IS_PTHREAD||a||async function(b){async function c(k,m){var p;(p=l).dataFileDownloads??(p.dataFileDownloads={});try{var n=await fetch(k)}catch(x){throw Error(`Network Error: ${k}`,{e:x});}if(!n.ok)throw Error(`${n.status}: ${n.url}`);p=[];m=Number(n.headers.get("Content-Length")??m);let t=0;l.setStatus?.("Downloading data...");for(n=n.body.getReader();;){var {done:z, value:u}=await n.read();if(z)break;p.push(u);t+=u.length;l.dataFileDownloads[k]={loaded:t,total:m};let x=0,E=0;for(var v of Object.values(l.dataFileDownloads))x+=v.loaded,E+=v.total;l.setStatus?.(`Downloading data... (${x}/${E})`)}k=new Uint8Array(p.map(x=>x.length).reduce((x,E)=>x+E,0));v=0;for(const x of p)k.set(x,v),v+=x.length;return k.buffer}async function d(k){k.FS_createPath("/","microblink",!0,!0);k.FS_createPath("/microblink","blinkid",!0,!0);for(var m of b.files)k.addRunDependency(`fp ${m.filename}`); -k.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.data"]={Ec:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p); -for(var n of b.files){var t=n.filename;k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0, -location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198, -end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop",start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop", -start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop",start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8326c5065a6d8451d6d004db73babf4a4a9f502c68643a14934cc098d04bf44f.strop",start:2512692,end:2900386},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2900386,end:3927241},{filename:"/microblink/blinkid/Model_93607315418f2c5d3abec06d0b2a81e69abeb1b8d0506ed6373d7d74bd5e4b05.strop", -start:3927241,end:5172232},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop",start:5172232,end:5468034},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop",start:5468034,end:5474530},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop",start:5474530,end:9570500},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop", -start:9570500,end:9695688},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop",start:9695688,end:9770133},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop",start:9770133,end:9845535},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop",start:9845535,end:10508588},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop", -start:10508588,end:10524261},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:10524261,end:10544730},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:10544730,end:13747128}],remote_package_size:13747128})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; +k.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.data"]={Ec:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t, +null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data", +"")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop", +start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop", +start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8326c5065a6d8451d6d004db73babf4a4a9f502c68643a14934cc098d04bf44f.strop",start:2512692,end:2900386},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2900386,end:3927241},{filename:"/microblink/blinkid/Model_93607315418f2c5d3abec06d0b2a81e69abeb1b8d0506ed6373d7d74bd5e4b05.strop",start:3927241,end:5172232},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop", +start:5172232,end:5468034},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop",start:5468034,end:5474530},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop",start:5474530,end:9570500},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop",start:9570500,end:9695688},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop", +start:9695688,end:9770133},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop",start:9770133,end:9845535},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop",start:9845535,end:10508588},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop",start:10508588,end:10524261},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:10524261, +end:10544730},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:10544730,end:13747128}],remote_package_size:13747128})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{}; (function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkIdModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; if(aa||ba){try{fa=(new URL(".",ea)).href}catch{}ba&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});ha=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);}}var ja=console.log.bind(console),q=console.error.bind(console),ka,la=!1,ma,na,oa,r,w,A,pa,B,D,qa,ra,G,ta,ua=!1; function va(){var a=wa.buffer;r=new Int8Array(a);A=new Int16Array(a);w=new Uint8Array(a);pa=new Uint16Array(a);B=new Int32Array(a);D=new Uint32Array(a);qa=new Float32Array(a);ra=new Float64Array(a);G=new BigInt64Array(a);ta=new BigUint64Array(a)}var wa;function J(a){l.onAbort?.(a);a="Aborted("+a+")";q(a);la=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa?.(a);throw a;}var xa; @@ -91,7 +91,7 @@ const b=Symbol.dispose;b&&(a[b]=a["delete"])})(); Object.assign(Lc.prototype,{Yb(a){this.Ob&&(a=this.Ob(a));return a},Hb(a){this.Na?.(a)},Oa:hc,Aa:function(a){function b(){return this.lb?Kc(this.va.Wa,{ya:this.oc,wa:c,Ia:this,Da:a}):Kc(this.va.Wa,{ya:this,wa:a})}var c=this.Yb(a);if(!c)return this.Hb(a),null;var d=Jc(this.va,c);if(void 0!==d){if(0===d.ra.count.value)return d.ra.wa=c,d.ra.Da=a,d.clone();d=d.clone();this.Hb(a);return d}d=this.va.Xb(c);d=wc[d];if(!d)return b.call(this);d=this.kb?d.Tb:d.pointerType;var e=Hc(c,this.va,d.va);return null=== e?b.call(this):this.lb?Kc(d.va.Wa,{ya:d,wa:e,Ia:this,Da:a}):Kc(d.va.Wa,{ya:d,wa:e})}});(async function(){Y=new yd;nb("library_fetch_init");try{Ad=await zd()}catch(a){Ad=!1}finally{mb("library_fetch_init")}})();wa=l.wasmMemory?l.wasmMemory:new WebAssembly.Memory({initial:(l.INITIAL_MEMORY||209715200)/65536,maximum:32768});va();l.noExitRuntime&&(Ha=l.noExitRuntime);l.preloadPlugins&&(ob=l.preloadPlugins);l.print&&(ja=l.print);l.printErr&&(q=l.printErr);l.wasmBinary&&(ka=l.wasmBinary); l.thisProgram&&(da=l.thisProgram);if(l.preInit)for("function"==typeof l.preInit&&(l.preInit=[l.preInit]);0{var k=b?Ra(Na(a+"/"+b)):a,m=`cp ${k}`;nb(m);try{var p=c;"string"==typeof c&&(p=await jb(c));p=await pb(p,k);h?.();f||Zb(a,b,p,d,e,g)}finally{mb(m)}};l.FS_unlink=(...a)=>Qb(...a);l.FS_createPath=(...a)=>Xb(...a);l.FS_createDevice=(...a)=>$b(...a); -l.FS_createDataFile=(...a)=>Zb(...a);l.FS_createLazyFile=(...a)=>bc(...a);var Kd={342160:(a,b,c,d)=>{a=a?M(w,a):"";b=b?M(w,b):"";c=c?M(w,c):"";d=d?M(w,d):"";throw Error(a+b+c+d);},342376:(a,b)=>{a=a?M(w,a):"";b=b?M(w,b):"";throw Error(a+b);}},Oc,Cd,Pc,cb,Ld,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; +l.FS_createDataFile=(...a)=>Zb(...a);l.FS_createLazyFile=(...a)=>bc(...a);var Kd={342144:(a,b,c,d)=>{a=a?M(w,a):"";b=b?M(w,b):"";c=c?M(w,c):"";d=d?M(w,d):"";throw Error(a+b+c+d);},342360:(a,b)=>{a=a?M(w,a):"";b=b?M(w,b):"";throw Error(a+b);}},Oc,Cd,Pc,cb,Ld,Ja;l.__ZN2MB2NN28LinearDefragmentingAllocator10Allocation4nullE=1024; var Md={A:(a,b)=>Ka(a)(b),q:function(a,b,c){La=c;try{var d=Hb(a);switch(b){case 0:var e=K();if(0>e)break;for(;sb[e];)e++;return Jb(d,e).Ta;case 1:case 2:return 0;case 3:return d.flags;case 4:return e=K(),d.flags|=e,0;case 12:return e=K(),A[e+0>>1]=2,0;case 13:case 14:return 0}return-28}catch(f){if("undefined"==typeof S||"ErrnoError"!==f.name)throw f;return-f.Ba}},T:function(a,b){try{var c=Hb(a),d=c.node,e=c.ta.Ka;a=e?c:d;e??=d.ua.Ka;Gb(e);var f=e(a);return ec(b,f)}catch(g){if("undefined"==typeof S|| "ErrnoError"!==g.name)throw g;return-g.Ba}},U:function(a,b,c){La=c;try{var d=Hb(a);switch(b){case 21509:return d.xa?0:-59;case 21505:if(!d.xa)return-59;if(d.xa.Ra.ec){a=[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];var e=K();B[e>>2]=25856;B[e+4>>2]=5;B[e+8>>2]=191;B[e+12>>2]=35387;for(var f=0;32>f;f++)r[e+f+17]=a[f]||0}return 0;case 21510:case 21511:case 21512:return d.xa?0:-59;case 21506:case 21507:case 21508:if(!d.xa)return-59;if(d.xa.Ra.fc)for(e=K(),a=[],f=0;32>f;f++)a.push(r[e+ f+17]);return 0;case 21519:if(!d.xa)return-59;e=K();return B[e>>2]=0;case 21520:return d.xa?-28:-59;case 21537:case 21531:e=K();if(!d.ta.dc)throw new O(59);return d.ta.dc(d,b,e);case 21523:if(!d.xa)return-59;d.xa.Ra.hc&&(f=[24,80],e=K(),A[e>>1]=f[0],A[e+2>>1]=f[1]);return 0;case 21524:return d.xa?0:-59;case 21515:return d.xa?0:-59;default:return-28}}catch(g){if("undefined"==typeof S||"ErrnoError"!==g.name)throw g;return-g.Ba}},R:function(a,b){try{return a=a?M(w,a):"",ec(b,Rb(a,!0))}catch(c){if("undefined"== diff --git a/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.wasm b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.wasm index 12ec188..81c8a50 100755 Binary files a/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.wasm and b/packages/blinkid-wasm/dist/full/basic/BlinkIdModule.wasm differ diff --git a/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.js b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.js index b12a9b8..44928f8 100644 --- a/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.js +++ b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.js @@ -1,7 +1,7 @@ async function createModule(moduleArg={}){var moduleRtn;var h=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope,m=ba&&self.name?.startsWith("em-pthread");let ca;(ca=h).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);h.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof m&&m||a||async function(b){async function c(l,n){var q;(q=h).dataFileDownloads??(q.dataFileDownloads={});try{var p=await fetch(l)}catch(x){throw Error(`Network Error: ${l}`,{e:x});}if(!p.ok)throw Error(`${p.status}: ${p.url}`);q=[];n=Number(p.headers.get("Content-Length")??n);let u=0;h.setStatus?.("Downloading data...");for(p=p.body.getReader();;){var {done:z,value:v}=await p.read();if(z)break; -q.push(v);u+=v.length;h.dataFileDownloads[l]={loaded:u,total:n};let x=0,F=0;for(var w of Object.values(h.dataFileDownloads))x+=w.loaded,F+=w.total;h.setStatus?.(`Downloading data... (${x}/${F})`)}l=new Uint8Array(q.map(x=>x.length).reduce((x,F)=>x+F,0));w=0;for(const x of q)l.set(x,w),w+=x.length;return l.buffer}async function d(l){l.FS_createPath("/","microblink",!0,!0);l.FS_createPath("/microblink","blinkid",!0,!0);for(var n of b.files)l.addRunDependency(`fp ${n.filename}`);l.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.data"); -l.preloadResults??(l.preloadResults={});l.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.data"]={od:!1};k||=await g;(async function(q){if(!q)throw Error("Loading data file failed.");if(q.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");q=new Uint8Array(q);for(var p of b.files){var u=p.filename;l.FS_createDataFile(u,null,q.subarray(p.start,p.end),!0,!0,!0);l.removeRunDependency(`fp ${u}`)}l.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.data")})(k)} +q.push(v);u+=v.length;h.dataFileDownloads[l]={loaded:u,total:n};let x=0,F=0;for(var w of Object.values(h.dataFileDownloads))x+=w.loaded,F+=w.total;h.setStatus?.(`Downloading data... (${x}/${F})`)}l=new Uint8Array(q.map(x=>x.length).reduce((x,F)=>x+F,0));w=0;for(const x of q)l.set(x,w),w+=x.length;return l.buffer}async function d(l){l.FS_createPath("/","microblink",!0,!0);l.FS_createPath("/microblink","blinkid",!0,!0);for(var n of b.files)l.addRunDependency(`fp ${n.filename}`);l.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.data"); +l.preloadResults??(l.preloadResults={});l.preloadResults["/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.data"]={od:!1};k||=await g;(async function(q){if(!q)throw Error("Loading data file failed.");if(q.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");q=new Uint8Array(q);for(var p of b.files){var u=p.filename;l.FS_createDataFile(u,null,q.subarray(p.start,p.end),!0,!0,!0);l.removeRunDependency(`fp ${u}`)}l.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.data")})(k)} "object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=h.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,k=h.getPreloadedPackage?.(e,f);k||(g=c(e,f));if(h.calledRun)d(h);else{let l;((l=h).preRun??(l.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop", start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop",start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop", start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop",start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2512692,end:3539547},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop", diff --git a/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.wasm b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.wasm index 47f0b5c..54e8af6 100755 Binary files a/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.wasm and b/packages/blinkid-wasm/dist/lightweight/advanced-threads/BlinkIdModule.wasm differ diff --git a/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.js b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.js index 0fa0547..33230da 100644 --- a/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.js +++ b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.js @@ -1,14 +1,14 @@ async function createModule(moduleArg={}){var moduleRtn;var l=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope;let ca;(ca=l).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);l.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof ENVIRONMENT_IS_PTHREAD&&ENVIRONMENT_IS_PTHREAD||a||async function(b){async function c(k,m){var p;(p=l).dataFileDownloads??(p.dataFileDownloads={});try{var n=await fetch(k)}catch(x){throw Error(`Network Error: ${k}`,{e:x});}if(!n.ok)throw Error(`${n.status}: ${n.url}`);p=[];m=Number(n.headers.get("Content-Length")??m);let t=0;l.setStatus?.("Downloading data...");for(n=n.body.getReader();;){var {done:z, value:u}=await n.read();if(z)break;p.push(u);t+=u.length;l.dataFileDownloads[k]={loaded:t,total:m};let x=0,E=0;for(var v of Object.values(l.dataFileDownloads))x+=v.loaded,E+=v.total;l.setStatus?.(`Downloading data... (${x}/${E})`)}k=new Uint8Array(p.map(x=>x.length).reduce((x,E)=>x+E,0));v=0;for(const x of p)k.set(x,v),v+=x.length;return k.buffer}async function d(k){k.FS_createPath("/","microblink",!0,!0);k.FS_createPath("/microblink","blinkid",!0,!0);for(var m of b.files)k.addRunDependency(`fp ${m.filename}`); -k.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.data"]={Fc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData"); -p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&& -encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop", -start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop",start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop", -start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop",start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2512692,end:3539547},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop",start:3539547,end:3835349},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop", -start:3835349,end:3841845},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop",start:3841845,end:7937815},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop",start:7937815,end:8063003},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop",start:8063003,end:8137448},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop", -start:8137448,end:8212850},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop",start:8212850,end:8875903},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop",start:8875903,end:8891576},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:8891576,end:8912045},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:8912045,end:12114443}],remote_package_size:12114443})})(); -l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{};(function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkIdModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; +k.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.data"]={Fc:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p);for(var n of b.files){var t=n.filename; +k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+ +"/");var e=l.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop", +start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop", +start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2512692,end:3539547},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop",start:3539547,end:3835349},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop",start:3835349,end:3841845},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop", +start:3841845,end:7937815},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop",start:7937815,end:8063003},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop",start:8063003,end:8137448},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop",start:8137448,end:8212850},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop", +start:8212850,end:8875903},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop",start:8875903,end:8891576},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:8891576,end:8912045},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:8912045,end:12114443}],remote_package_size:12114443})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}}; +l="undefined"!==typeof l?l:{};(function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkIdModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; if(aa||ba){try{fa=(new URL(".",ea)).href}catch{}ba&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});ha=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);}}var ja=console.log.bind(console),q=console.error.bind(console),ka,la=!1,ma,na,oa,r,w,A,pa,B,D,qa,ra,G,ta,ua=!1; function va(){var a=wa.buffer;r=new Int8Array(a);A=new Int16Array(a);w=new Uint8Array(a);pa=new Uint16Array(a);B=new Int32Array(a);D=new Uint32Array(a);qa=new Float32Array(a);ra=new Float64Array(a);G=new BigInt64Array(a);ta=new BigUint64Array(a)}var wa;function J(a){l.onAbort?.(a);a="Aborted("+a+")";q(a);la=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa?.(a);throw a;}var xa; async function ya(a){if(!ka)try{var b=await ha(a);return new Uint8Array(b)}catch{}if(a==xa&&ka)a=new Uint8Array(ka);else if(ia)a=ia(a);else throw"both async and sync fetching of the wasm failed";return a}async function za(a,b){try{var c=await ya(a);return await WebAssembly.instantiate(c,b)}catch(d){q(`failed to asynchronously prepare wasm: ${d}`),J(d)}} diff --git a/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.wasm b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.wasm index d1751f7..8fd53ec 100755 Binary files a/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.wasm and b/packages/blinkid-wasm/dist/lightweight/advanced/BlinkIdModule.wasm differ diff --git a/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.js b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.js index 3009776..6725d1c 100644 --- a/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.js +++ b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.js @@ -1,14 +1,14 @@ async function createModule(moduleArg={}){var moduleRtn;var l=moduleArg,aa=!!globalThis.window,ba=!!globalThis.WorkerGlobalScope;let ca;(ca=l).expectedDataFileDownloads??(ca.expectedDataFileDownloads=0);l.expectedDataFileDownloads++; (()=>{var a="undefined"!=typeof ENVIRONMENT_IS_WASM_WORKER&&ENVIRONMENT_IS_WASM_WORKER;"undefined"!=typeof ENVIRONMENT_IS_PTHREAD&&ENVIRONMENT_IS_PTHREAD||a||async function(b){async function c(k,m){var p;(p=l).dataFileDownloads??(p.dataFileDownloads={});try{var n=await fetch(k)}catch(x){throw Error(`Network Error: ${k}`,{e:x});}if(!n.ok)throw Error(`${n.status}: ${n.url}`);p=[];m=Number(n.headers.get("Content-Length")??m);let t=0;l.setStatus?.("Downloading data...");for(n=n.body.getReader();;){var {done:z, value:u}=await n.read();if(z)break;p.push(u);t+=u.length;l.dataFileDownloads[k]={loaded:t,total:m};let x=0,E=0;for(var v of Object.values(l.dataFileDownloads))x+=v.loaded,E+=v.total;l.setStatus?.(`Downloading data... (${x}/${E})`)}k=new Uint8Array(p.map(x=>x.length).reduce((x,E)=>x+E,0));v=0;for(const x of p)k.set(x,v),v+=x.length;return k.buffer}async function d(k){k.FS_createPath("/","microblink",!0,!0);k.FS_createPath("/microblink","blinkid",!0,!0);for(var m of b.files)k.addRunDependency(`fp ${m.filename}`); -k.addRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.data"]={Ec:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData"); -p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t,null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/android-arm64-samsung-galaxy-a30s/root/E0/b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&& -encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data","")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop", -start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop",start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop", -start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop",start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2512692,end:3539547},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop",start:3539547,end:3835349},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop", -start:3835349,end:3841845},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop",start:3841845,end:7937815},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop",start:7937815,end:8063003},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop",start:8063003,end:8137448},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop", -start:8137448,end:8212850},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop",start:8212850,end:8875903},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop",start:8875903,end:8891576},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:8891576,end:8912045},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:8912045,end:12114443}],remote_package_size:12114443})})(); -l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}};l="undefined"!==typeof l?l:{};(function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkIdModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; +k.addRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.data");k.preloadResults??(k.preloadResults={});k.preloadResults["/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.data"]={Ec:!1};h||=await g;(async function(p){if(!p)throw Error("Loading data file failed.");if(p.constructor.name!==ArrayBuffer.name)throw Error("bad input to processPackageData");p=new Uint8Array(p);for(var n of b.files){var t=n.filename;k.FS_createDataFile(t, +null,p.subarray(n.start,n.end),!0,!0,!0);k.removeRunDependency(`fp ${t}`)}k.removeRunDependency("datafile_/opt/jenkins/root/E0/b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.data")})(h)}"object"===typeof window?window.encodeURIComponent(window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/"))+"/"):"undefined"===typeof process&&"undefined"!==typeof location&&encodeURIComponent(location.pathname.substring(0,location.pathname.lastIndexOf("/"))+"/");var e=l.locateFile?.("BlinkIdModule.data", +"")??"BlinkIdModule.data",f=b.remote_package_size,g,h=l.getPreloadedPackage?.(e,f);h||(g=c(e,f));if(l.calledRun)d(l);else{let k;((k=l).preRun??(k.preRun=[])).push(d)}}({files:[{filename:"/microblink/blinkid/Model_07c7ab860e77ec2e92bb822f6d62424b8595a5beb4340f6b2f7f6a4cffa5d050.strop",start:0,end:103198},{filename:"/microblink/blinkid/Model_2880751121560047e6dc571bc8ff4683aeb863886f7c0789234594ceb1e23577.strop",start:103198,end:106933},{filename:"/microblink/blinkid/Model_36d8e94f4cb46097bd6b7385f2aa91fcdee1a5fefeec59e56d1b9e82c94b00a7.strop", +start:106933,end:451389},{filename:"/microblink/blinkid/Model_36fe2b262231378031366de1c6b94db590b4415d21d4b42b7dab50968a26519c.strop",start:451389,end:1913035},{filename:"/microblink/blinkid/Model_38a69625879a5def5ed58d768f00e49df43fce8fc74c2e37099b680b526b96df.strop",start:1913035,end:2237522},{filename:"/microblink/blinkid/Model_3b11c3ffacbbf390b932fb9a7024f1a0016f66281ea8c790f8b5903374ad89c2.strop",start:2237522,end:2506243},{filename:"/microblink/blinkid/Model_473ac5f5e256623c0a6b282698c1f1b033b9fb5359e6aa142e9fb4a4022afe4e.strop", +start:2506243,end:2512692},{filename:"/microblink/blinkid/Model_8c7727da554fd257fa758787ca3d0f517b47f6c5ba791792f516da7dd210fde8.strop",start:2512692,end:3539547},{filename:"/microblink/blinkid/Model_a1516add883f909ebecf9b565238086f8f31bbb3e7fb2113258e3ae7c659555e.strop",start:3539547,end:3835349},{filename:"/microblink/blinkid/Model_b452fd3cd4037080cfcb4c423dba5bc58c8ceafc28427fdf9c5778fb353f088e.strop",start:3835349,end:3841845},{filename:"/microblink/blinkid/Model_bafe286f878a41ed7fb28c49fee65824638a53f5c05bb6540096a0bd15f55415.strop", +start:3841845,end:7937815},{filename:"/microblink/blinkid/Model_e7cd6730eb024801e7309b9c7b5654e684d9c4563935cf64617e16d6c750790a.strop",start:7937815,end:8063003},{filename:"/microblink/blinkid/Model_e89a89677e2469c66d02864bd83e04ceb72f0c95cd943b883875f8796b370495.strop",start:8063003,end:8137448},{filename:"/microblink/blinkid/Model_f2874f5766b779c063dbe30467979163497dc4b29c43bab23d70c798ee32993a.strop",start:8137448,end:8212850},{filename:"/microblink/blinkid/Model_f531088a1f65a31d6ba039e09446fe24c18360080fec3792308ac01514803620.strop", +start:8212850,end:8875903},{filename:"/microblink/blinkid/Model_faf688599c7e261a2a7404730f69ed029115bb0d3e7a3657731d88e701720efe.strop",start:8875903,end:8891576},{filename:"/microblink/blinkid/ccc_lookup_table.zzip",start:8891576,end:8912045},{filename:"/microblink/blinkid/serialized_embedder_db_1.10.bin",start:8912045,end:12114443}],remote_package_size:12114443})})();l.onAbort=function(a){q("Aborted with message: "+a);try{throw new WebAssembly.RuntimeError(a);}catch(b){q("Stacktrace: "+b.stack)}}; +l="undefined"!==typeof l?l:{};(function(){var a="",b=!1;try{if("undefined"!==typeof self&&self.location&&self.location.href){var c=self.location.href;0===c.indexOf("blob:")&&(a=c,b=!0)}}catch(d){}b&&!l.locateFile&&(l.locateFile=function(d,e){return"BlinkIdModule.wasm"===d?a:e+d})})();var da="./this.program",ea=import.meta.url,fa="",ha,ia; if(aa||ba){try{fa=(new URL(".",ea)).href}catch{}ba&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});ha=async a=>{a=await fetch(a,{credentials:"same-origin"});if(a.ok)return a.arrayBuffer();throw Error(a.status+" : "+a.url);}}var ja=console.log.bind(console),q=console.error.bind(console),ka,la=!1,ma,na,oa,r,w,A,pa,B,D,qa,ra,G,ta,ua=!1; function va(){var a=wa.buffer;r=new Int8Array(a);A=new Int16Array(a);w=new Uint8Array(a);pa=new Uint16Array(a);B=new Int32Array(a);D=new Uint32Array(a);qa=new Float32Array(a);ra=new Float64Array(a);G=new BigInt64Array(a);ta=new BigUint64Array(a)}var wa;function J(a){l.onAbort?.(a);a="Aborted("+a+")";q(a);la=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");oa?.(a);throw a;}var xa; async function ya(a){if(!ka)try{var b=await ha(a);return new Uint8Array(b)}catch{}if(a==xa&&ka)a=new Uint8Array(ka);else if(ia)a=ia(a);else throw"both async and sync fetching of the wasm failed";return a}async function za(a,b){try{var c=await ya(a);return await WebAssembly.instantiate(c,b)}catch(d){q(`failed to asynchronously prepare wasm: ${d}`),J(d)}} diff --git a/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.wasm b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.wasm index 7c07d8a..0c385e4 100755 Binary files a/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.wasm and b/packages/blinkid-wasm/dist/lightweight/basic/BlinkIdModule.wasm differ diff --git a/packages/blinkid-wasm/dist/size-manifest.json b/packages/blinkid-wasm/dist/size-manifest.json index 8d4f969..9151974 100644 --- a/packages/blinkid-wasm/dist/size-manifest.json +++ b/packages/blinkid-wasm/dist/size-manifest.json @@ -1,16 +1,16 @@ { "wasm": { "basic": { - "full": 3287361, - "lightweight": 3327137 + "full": 3288111, + "lightweight": 3326644 }, "advanced": { - "full": 3300553, - "lightweight": 3337066 + "full": 3300823, + "lightweight": 3336970 }, "advanced-threads": { - "full": 3352820, - "lightweight": 3387495 + "full": 3352383, + "lightweight": 3387693 } }, "data": { diff --git a/packages/blinkid-wasm/package.json b/packages/blinkid-wasm/package.json index 83fb650..703c983 100644 --- a/packages/blinkid-wasm/package.json +++ b/packages/blinkid-wasm/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkid-wasm", "private": true, - "version": "7.7.1", + "version": "7.7.2", "scripts": { "build": "tsc", "build:publish": "tsc", diff --git a/packages/blinkid-worker/CHANGELOG.md b/packages/blinkid-worker/CHANGELOG.md index a990c6c..82df0a0 100644 --- a/packages/blinkid-worker/CHANGELOG.md +++ b/packages/blinkid-worker/CHANGELOG.md @@ -1,5 +1,16 @@ # @microblink/blinkid-worker +## 7.7.2 + +### Patch Changes + +- Added crash reporting for worker runtime failures, unhandled promise rejections, Wasm aborts, session creation failures, session method failures, and frame-transfer failures. +- Flushes init-time pinglets only when BlinkID SDK initialization fails, preventing successful initialization from sending queued analytics prematurely. +- Updated dependencies + - @microblink/worker-common@1.0.2 + - @microblink/analytics@1.0.1 + - @microblink/blinkid-wasm@7.7.2 + ## 7.7.1 ### Patch Changes diff --git a/packages/blinkid-worker/docs/classes/BlinkIdWorker.md b/packages/blinkid-worker/docs/classes/BlinkIdWorker.md index 3759e16..d205e32 100644 --- a/packages/blinkid-worker/docs/classes/BlinkIdWorker.md +++ b/packages/blinkid-worker/docs/classes/BlinkIdWorker.md @@ -208,11 +208,11 @@ This method initializes everything. ### reportPinglet() -> **reportPinglet**(`__namedParameters`): `void` +> **reportPinglet**(`pinglet`): `void` #### Parameters -##### \_\_namedParameters +##### pinglet `Ping` diff --git a/packages/blinkid-worker/package.json b/packages/blinkid-worker/package.json index df53b91..0f5edbe 100644 --- a/packages/blinkid-worker/package.json +++ b/packages/blinkid-worker/package.json @@ -2,7 +2,7 @@ "name": "@microblink/blinkid-worker", "description": "Provides a worker which runs the BlinkID WebAssembly in separate thread", "private": true, - "version": "7.7.1", + "version": "7.7.2", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", "build:js": "vite build --mode ${VITE_BUILD_MODE:-production}", diff --git a/packages/blinkid-worker/src/BlinkIdWorker.initBlinkId.test.ts b/packages/blinkid-worker/src/BlinkIdWorker.initBlinkId.test.ts index cd84164..f950691 100644 --- a/packages/blinkid-worker/src/BlinkIdWorker.initBlinkId.test.ts +++ b/packages/blinkid-worker/src/BlinkIdWorker.initBlinkId.test.ts @@ -2,17 +2,25 @@ * Copyright (c) 2026 Microblink Ltd. All rights reserved. */ -import type { BlinkIdSessionSettings } from "@microblink/blinkid-wasm"; +import type { + BlinkIdScanningSession, + BlinkIdSessionSettings, +} from "@microblink/blinkid-wasm"; import { LicenseError, ServerPermissionError, } from "@microblink/worker-common/errors"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as Comlink from "comlink"; import { createWasmModuleMock, + getLastModuleOverrides, + resetLastModuleOverrides, setWasmModuleMock, } from "@microblink/test-utils/mocks/wasmModuleFactory"; +import { createFakeImageData } from "@microblink/test-utils/mocks/imageData"; import { createLicenseUnlockResult } from "@microblink/test-utils/mocks/licensing"; +import { createScanningSessionMock } from "@microblink/test-utils/mocks/scanningSession"; import { BlinkIdWasmModule } from "@microblink/blinkid-wasm"; const getCrossOriginWorkerURLMock = vi.fn(); @@ -21,6 +29,7 @@ const detectWasmFeaturesMock = vi.fn(); const validateLicenseProxyPermissionsMock = vi.fn(); const sanitizeProxyUrlsMock = vi.fn(); const obtainNewServerPermissionMock = vi.fn(); +let workerEventListeners = new Map(); /** Deterministic values for stubbed globals and mock return shapes. */ const hostName = "example.com" as const; @@ -61,6 +70,29 @@ vi.mock("@microblink/worker-common/licencing", () => ({ let BlinkIdWorker: typeof import("./BlinkIdWorker").BlinkIdWorker; +const getLatestWorkerListener = (type: string) => { + const listeners = workerEventListeners.get(type); + + if (!listeners || listeners.length === 0) { + return undefined; + } + + return listeners[listeners.length - 1]; +}; + +const getLastQueuedPinglet = (queuePingletMock: ReturnType) => { + const serializedPinglet = queuePingletMock.mock.calls[ + queuePingletMock.mock.calls.length - 1 + ]?.[0] as string; + + return JSON.parse(serializedPinglet) as Record; +}; + +const getLastQueuedPingletSessionNumber = ( + queuePingletMock: ReturnType, +): unknown => + queuePingletMock.mock.calls[queuePingletMock.mock.calls.length - 1]?.[3]; + describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { const baseInitSettings = { licenseKey: "test-license", @@ -78,12 +110,30 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { sanitizeProxyUrlsMock.mockReset(); obtainNewServerPermissionMock.mockReset(); + workerEventListeners = new Map(); + // Deterministic hostname/userAgent so ping payload and runtime context are stable. vi.stubGlobal("self", { setTimeout: vi.fn(), close: vi.fn(), location: { hostname: hostName }, navigator: { userAgent: "Chrome" }, + addEventListener: vi.fn( + (type: string, listener: EventListenerOrEventListenerObject) => { + const listeners = workerEventListeners.get(type) ?? []; + listeners.push(listener as EventListener); + workerEventListeners.set(type, listeners); + }, + ), + removeEventListener: vi.fn( + (type: string, listener: EventListenerOrEventListenerObject) => { + const listeners = workerEventListeners.get(type) ?? []; + workerEventListeners.set( + type, + listeners.filter((entry) => entry !== listener), + ); + }, + ), }); // Worker loads wasm from this URL; mock factory serves the seeded module. @@ -104,11 +154,12 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { afterEach(() => { setWasmModuleMock(null); + resetLastModuleOverrides(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); - it("sends pinglets only after server permission flow completes", async () => { + it("does not flush pinglets after successful server permission flow", async () => { const { module, spies } = createWasmModuleMock({ initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult({ @@ -125,27 +176,27 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(spies.submitServerPermission).toHaveBeenCalledOnce(); expect(spies.queuePinglet).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); + expect(spies.sendPinglets).not.toHaveBeenCalled(); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - // Init pinglet is queued first; license and permission steps must complete before flush. + // Init pinglet is queued first; license and permission steps must complete before SDK init. expect(spies.queuePinglet.mock.invocationCallOrder[0]).toBeLessThan( obtainNewServerPermissionMock.mock.invocationCallOrder[0], ); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); - expect(spies.initializeSdk.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); + expect(spies.queuePinglet.mock.invocationCallOrder[0]).toBeLessThan( + spies.initializeSdk.mock.invocationCallOrder[0], ); }); - it("sets ping proxy URL before sending pinglets when ping proxy is allowed", async () => { + it("sets ping proxy URL when ping proxy is allowed without flushing pinglets", async () => { const proxyUrl = "https://proxy.example.com"; const { module, spies } = createWasmModuleMock({ initializeWithLicenseKey: vi.fn(() => @@ -170,14 +221,14 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { expect(sanitizeProxyUrlsMock).toHaveBeenCalledWith(proxyUrl); expect(spies.setPingProxyUrl).toHaveBeenCalledWith(`${proxyUrl}/ping`); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); - // Ping route must be set before flush so pings go through the proxy. + expect(spies.sendPinglets).not.toHaveBeenCalled(); + // Ping route must be set before SDK init so future pings use the proxy. expect(spies.setPingProxyUrl.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + spies.initializeSdk.mock.invocationCallOrder[0], ); }); - it("uses ping and baltazar proxies and flushes pinglets after permission flow", async () => { + it("uses ping and baltazar proxies without flushing pinglets on successful init", async () => { const proxyUrl = "https://proxy.example.com"; const licenseUnlockResult = createLicenseUnlockResult({ unlockResult: "requires-server-permission", @@ -211,21 +262,21 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(spies.submitServerPermission).toHaveBeenCalledOnce(); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); + expect(spies.sendPinglets).not.toHaveBeenCalled(); - // Ping proxy set before flush; permission flow completes before flush. + // Ping proxy is set before SDK init; permission flow completes before SDK init. expect(spies.setPingProxyUrl.mock.invocationCallOrder[0]).toBeLessThan( - spies.sendPinglets.mock.invocationCallOrder[0], + spies.initializeSdk.mock.invocationCallOrder[0], ); expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); }); it("throws Error and does not send pinglets when server permission request fails", async () => { @@ -284,7 +335,7 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[0]); }); - it("throws and does send pinglets when initializeSdk fails", async () => { + it("queues crash pinglet and flushes when initializeSdk fails", async () => { const { module, spies } = createWasmModuleMock({ initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult({ @@ -295,7 +346,8 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { setWasmModuleMock(module); const worker = new BlinkIdWorker(); - // Flush happens after initializeSdk; failure must leave pinglets buffered. + // The init start pinglet is already queued; initializeSdk failure adds ping.error + // and the init catch block flushes after recording the error. spies.initializeSdk.mockImplementation(() => { throw new Error("initializeSdk-error"); }); @@ -311,15 +363,306 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[0]); - expect( - spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.queuePinglet.mock.invocationCallOrder[1]); expect(spies.initializeSdk.mock.invocationCallOrder[0]).greaterThan( spies.queuePinglet.mock.invocationCallOrder[0], ); - expect(spies.initializeSdk.mock.invocationCallOrder[0]).toBeLessThan( - spies.queuePinglet.mock.invocationCallOrder[1], + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "initializeSdk-error", + }); + }); + + it("reports worker error events as crash pinglets after init", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + getLatestWorkerListener("error")?.({ + error: new Error("boom"), + message: "boom", + } as unknown as Event); + + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "boom", + }); + }); + + it("reports unhandled rejections as crash pinglets after init", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + getLatestWorkerListener("unhandledrejection")?.({ + reason: new Error("rejected"), + } as unknown as Event); + + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "rejected", + }); + }); + + it("reports Emscripten aborts as crash pinglets after init", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + const moduleOverrides = getLastModuleOverrides(); + expect(moduleOverrides?.onAbort).toEqual(expect.any(Function)); + + (moduleOverrides?.onAbort as (what: unknown) => void)("fatal abort"); + + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "fatal abort", + }); + }); + + it("reports scanning session creation failures as crash pinglets", async () => { + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => { + throw new Error("session-create-failed"); + }), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => worker.createScanningSession()).toThrow( + "session-create-failed", ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: "session-create-failed", + }); + }); + + it("reports thrown process calls as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + process: vi.fn(() => { + throw new Error("process-failed"); + }), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.process(createFakeImageData())).toThrow( + "process-failed", + ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "process-failed", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports process failures as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + process: vi.fn(() => { + throw new Error("RuntimeError: Out of bounds memory access"); + }), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.process(createFakeImageData())).toThrow( + "RuntimeError: Out of bounds memory access", + ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "RuntimeError: Out of bounds memory access", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports frame return transfer failures as crash pinglets", async () => { + const transferSpy = vi + .spyOn(Comlink, "transfer") + .mockImplementationOnce(() => { + throw new Error("buffer-transfer-failed"); + }); + const session = createScanningSessionMock({ + process: vi.fn( + () => + ({ + inputImageAnalysisResult: { + documentClassInfo: {}, + documentRotation: "not-available", + }, + }) as never, + ), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.process(createFakeImageData())).toThrow( + "Failed to transfer frame from worker: buffer-transfer-failed", + ); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "Crash", + errorMessage: + "Failed to transfer frame from worker: buffer-transfer-failed", + }); + + transferSpy.mockRestore(); + }); + + it("reports sentinel process results as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + process: vi.fn(() => ({ error: "document-scanned" as const })), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + const result = proxySession.process(createFakeImageData()); + + expect(result).toMatchObject({ error: "document-scanned" }); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "document-scanned", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports getResult failures as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + getResult: vi.fn(() => { + throw new Error("get-result-failed"); + }), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.getResult()).toThrow("get-result-failed"); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "get-result-failed", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); + }); + + it("reports reset failures as non-fatal pinglets", async () => { + const session = createScanningSessionMock({ + reset: vi.fn(() => { + throw new Error("reset-failed"); + }), + }); + const { module, spies } = createWasmModuleMock({ + initializeWithLicenseKey: vi.fn(() => createLicenseUnlockResult()), + createScanningSession: vi.fn(() => session), + }); + setWasmModuleMock(module); + + const worker = new BlinkIdWorker(); + await worker.initBlinkId(baseInitSettings, defaultSessionSettings); + + const proxySession = worker.createScanningSession(); + spies.queuePinglet.mockClear(); + spies.sendPinglets.mockClear(); + + expect(() => proxySession.reset()).toThrow("reset-failed"); + expect(spies.queuePinglet).toHaveBeenCalledTimes(1); + expect(spies.sendPinglets).toHaveBeenCalledTimes(1); + expect(getLastQueuedPinglet(spies.queuePinglet)).toMatchObject({ + errorType: "NonFatal", + errorMessage: "reset-failed", + }); + expect(getLastQueuedPingletSessionNumber(spies.queuePinglet)).toBe(1); }); it("uses baltazar proxy for server permission and does not set ping proxy URL", async () => { @@ -355,18 +698,18 @@ describe("BlinkIdWorker initBlinkId ping flush and proxy ordering", () => { expect(obtainNewServerPermissionMock).toHaveBeenCalledOnce(); expect(spies.submitServerPermission).toHaveBeenCalledOnce(); expect(spies.initializeSdk).toHaveBeenCalledOnce(); - expect(spies.sendPinglets).toHaveBeenCalledOnce(); + expect(spies.sendPinglets).not.toHaveBeenCalled(); // allowPingProxy is false so ping proxy is not set; permission flow order unchanged. expect( obtainNewServerPermissionMock.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.submitServerPermission.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); expect( spies.initializeWithLicenseKey.mock.invocationCallOrder[0], - ).toBeLessThan(spies.sendPinglets.mock.invocationCallOrder[0]); + ).toBeLessThan(spies.initializeSdk.mock.invocationCallOrder[0]); }); it("throws LicenseError and does not send pinglets when license is invalid", async () => { diff --git a/packages/blinkid-worker/src/BlinkIdWorker.test.ts b/packages/blinkid-worker/src/BlinkIdWorker.test.ts index 5647cd0..152ea2a 100644 --- a/packages/blinkid-worker/src/BlinkIdWorker.test.ts +++ b/packages/blinkid-worker/src/BlinkIdWorker.test.ts @@ -49,6 +49,8 @@ describe("BlinkIdWorker", () => { close: vi.fn(), location: { hostname: "example.com" }, navigator: { userAgent: "Chrome" }, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), }); ({ BlinkIdWorker } = await import("./BlinkIdWorker")); @@ -181,11 +183,8 @@ describe("BlinkIdWorker", () => { expect(proxySession.showProductionOverlay()).toBe(true); }); - it("reports pinglet on session errors when process returns error result", () => { + it("returns session error results without a loaded wasm module", () => { const worker = new BlinkIdWorker(); - const reportPingletSpy = vi - .spyOn(worker, "reportPinglet") - .mockImplementation(() => undefined); const sessionSettings = {} as BlinkIdSessionSettings; const session = createScanningSessionMock({ process: vi.fn(() => ({ error: "document-scanned" as const })), @@ -197,10 +196,5 @@ describe("BlinkIdWorker", () => { const result = proxySession.process(image); expect(result).toMatchObject({ error: "document-scanned" }); expect(result.arrayBuffer).toBe(image.data.buffer); - expect(reportPingletSpy).toHaveBeenCalledWith( - expect.objectContaining({ - schemaName: "ping.error", - }), - ); }); }); diff --git a/packages/blinkid-worker/src/BlinkIdWorker.ts b/packages/blinkid-worker/src/BlinkIdWorker.ts index dd31785..b55fb70 100644 --- a/packages/blinkid-worker/src/BlinkIdWorker.ts +++ b/packages/blinkid-worker/src/BlinkIdWorker.ts @@ -45,9 +45,25 @@ import { LicenseError, ServerPermissionError, } from "@microblink/worker-common/errors"; +import { installWorkerCrashReporter } from "@microblink/worker-common/workerCrashReporter"; export type { DownloadProgress } from "@microblink/worker-common/downloadResourceBuffer"; +const FRAME_TRANSFER_ERROR_NAME = "FrameTransferError"; + +const createFrameTransferError = (message: string, error: unknown) => { + const causeMessage = + error instanceof Error && error.message ? `: ${error.message}` : ""; + + const frameTransferError = new Error( + `${message}${causeMessage}`, + error instanceof Error ? { cause: error } : undefined, + ); + frameTransferError.name = FRAME_TRANSFER_ERROR_NAME; + + return frameTransferError; +}; + /** * The BlinkID worker. */ @@ -91,6 +107,32 @@ export class BlinkIdWorker { #userId!: string; + #cleanupCrashReporter: (() => void) | undefined; + + constructor() { + this.#cleanupCrashReporter = installWorkerCrashReporter({ + getSessionNumber: () => this.#currentSessionNumber, + onError: ({ error, sessionNumber }) => { + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber, + data: { + errorType: "Crash", + errorMessage: + error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); + }, + }); + } + /** * This method loads the Wasm module. */ @@ -246,6 +288,44 @@ export class BlinkIdWorker { locateFile: (path) => { return `${variantUrl}/${wasmVariant}/${path}`; }, + onAbort: (what) => { + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: what instanceof Error ? what.message : String(what), + stackTrace: what instanceof Error ? what.stack : undefined, + }, + }); + this.sendPinglets(); + }, + printErr: (message) => { + console.error(message); + + if (/\babort(ed)?\b/i.test(message)) { + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: String(message), + stackTrace: undefined, + }, + }); + this.sendPinglets(); + } + }, // pthreads build breaks without this: // "Failed to execute 'createObjectURL' on 'URL': Overload resolution failed." mainScriptUrlOrBlob: crossOriginWorkerUrl, @@ -262,30 +342,20 @@ export class BlinkIdWorker { } } - reportPinglet({ data, schemaName, schemaVersion, sessionNumber }: Ping) { + reportPinglet(pinglet: Ping) { if (!this.#wasmModule) { throw new Error("Cannot report pinglet: Wasm module not loaded"); } - if (!this.#wasmModule.isPingEnabled()) { - // Ping is not enabled, do nothing - return; - } - try { this.#wasmModule.queuePinglet( - JSON.stringify(data), - schemaName, - schemaVersion, - sessionNumber!, // we know sesion number is provided because we're using proxy function + JSON.stringify(pinglet.data), + pinglet.schemaName, + pinglet.schemaVersion, + pinglet.sessionNumber ?? this.#currentSessionNumber, ); } catch (error) { - console.warn("Failed to queue pinglet:", error, { - data, - schemaName, - schemaVersion, - sessionNumber, - }); + console.warn("Failed to queue pinglet:", error, pinglet); } } @@ -342,7 +412,7 @@ export class BlinkIdWorker { false, ); - // Queue init pinglet before remote license check; flush only after full flow + // Queue init pinglet before remote license check; flush only if init fails. this.reportPinglet({ schemaName: "ping.sdk.init.start", schemaVersion: "1.1.0", @@ -424,10 +494,9 @@ export class BlinkIdWorker { stackTrace: error instanceof Error ? error.stack : undefined, }, }); - throw error; - } finally { - // flush pinglets after initializing the SDK + // Flush only for failed SDK initialization. this.sendPinglets(); + throw error; } } @@ -442,21 +511,36 @@ export class BlinkIdWorker { throw new Error("Wasm module not loaded"); } - const sessionSettings = buildSessionSettings( - options, - this.#defaultSessionSettings, - ); + try { + const sessionSettings = buildSessionSettings( + options, + this.#defaultSessionSettings, + ); - const session = this.#wasmModule.createScanningSession( - sessionSettings, - this.#userId, - ); + const session = this.#wasmModule.createScanningSession( + sessionSettings, + this.#userId, + ); - this.#currentSessionNumber++; + this.#currentSessionNumber++; - this.sendPinglets(); + this.sendPinglets(); - return this.createProxySession(session, sessionSettings); + return this.createProxySession(session, sessionSettings); + } catch (error) { + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); + throw error; + } } /** @@ -497,6 +581,10 @@ export class BlinkIdWorker { try { return session.getResult(); } catch (error) { + if (!this.#wasmModule) { + throw error; + } + this.reportPinglet({ schemaName: "ping.error", schemaVersion: "1.0.0", @@ -505,81 +593,140 @@ export class BlinkIdWorker { errorType: "NonFatal", errorMessage: error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, }, }); + this.sendPinglets(); throw error; } }, process: (image) => { - const processResult = session.process(image); - - if ("error" in processResult) { - // processResult is BlinkIdSessionErrorWithBuffer - this.reportPinglet({ - schemaName: "ping.error", - schemaVersion: "1.0.0", - sessionNumber: this.#currentSessionNumber, - data: { - errorType: "NonFatal", - errorMessage: processResult.error, - }, - }); - - // not an error: processResult is ProcessResultWithBuffer - } else { - /** - * As documentClassInfo is not an optional property, assume that `type` being - * defined means the whole object is defined and can be cached. - */ - if (processResult.inputImageAnalysisResult.documentClassInfo.type) { - // cache class info for future use - cachedClassInfo = - processResult.inputImageAnalysisResult.documentClassInfo; + try { + const processResult = session.process(image); + + if ("error" in processResult) { + // processResult is BlinkIdSessionErrorWithBuffer + if (this.#wasmModule) { + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "NonFatal", + errorMessage: String(processResult.error), + stackTrace: undefined, + }, + }); + this.sendPinglets(); + } + + // not an error: processResult is ProcessResultWithBuffer + } else { + /** + * As documentClassInfo is not an optional property, assume that `type` being + * defined means the whole object is defined and can be cached. + */ + if (processResult.inputImageAnalysisResult.documentClassInfo.type) { + // cache class info for future use + cachedClassInfo = + processResult.inputImageAnalysisResult.documentClassInfo; + } + + /** + * Cache rotation, assume that rotation remains the same if document is not detected, + * i.e. rotation is only updated when detection is successful. + */ + if ( + processResult.inputImageAnalysisResult.documentRotation !== + "not-available" + ) { + // cache rotation for future use + cachedRotation = + processResult.inputImageAnalysisResult.documentRotation; + } + + if ( + cachedClassInfo && + cachedClassInfo?.type !== + processResult.inputImageAnalysisResult.documentClassInfo.type + ) { + processResult.inputImageAnalysisResult.documentClassInfo = + cachedClassInfo; + } + + if ( + cachedRotation && + cachedRotation !== + processResult.inputImageAnalysisResult.documentRotation + ) { + processResult.inputImageAnalysisResult.documentRotation = + cachedRotation; + } } - /** - * Cache rotation, assume that rotation remains the same if document is not detected, - * i.e. rotation is only updated when detection is successful. - */ - if ( - processResult.inputImageAnalysisResult.documentRotation !== - "not-available" - ) { - // cache rotation for future use - cachedRotation = - processResult.inputImageAnalysisResult.documentRotation; + let transferPackage: + | ProcessResultWithBuffer + | BlinkIdSessionErrorWithBuffer; + + try { + transferPackage = transfer( + { + ...processResult, + arrayBuffer: image.data.buffer, + }, + [image.data.buffer], + ); + } catch (error) { + const frameTransferError = createFrameTransferError( + "Failed to transfer frame from worker", + error, + ); + + if (!this.#wasmModule) { + throw frameTransferError; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "Crash", + errorMessage: frameTransferError.message, + stackTrace: frameTransferError.stack, + }, + }); + this.sendPinglets(); + throw frameTransferError; } + return transferPackage; + } catch (error) { if ( - cachedClassInfo && - cachedClassInfo?.type !== - processResult.inputImageAnalysisResult.documentClassInfo.type + error instanceof Error && + error.name === FRAME_TRANSFER_ERROR_NAME ) { - processResult.inputImageAnalysisResult.documentClassInfo = - cachedClassInfo; + throw error; } - if ( - cachedRotation && - cachedRotation !== - processResult.inputImageAnalysisResult.documentRotation - ) { - processResult.inputImageAnalysisResult.documentRotation = - cachedRotation; + if (!this.#wasmModule) { + throw error; } - } - const transferPackage: - | ProcessResultWithBuffer - | BlinkIdSessionErrorWithBuffer = transfer( - { - ...processResult, - arrayBuffer: image.data.buffer, - }, - [image.data.buffer], - ); - - return transferPackage; + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "NonFatal", + errorMessage: + error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); + throw error; + } }, ping: (ping: Ping) => { this.reportPinglet({ @@ -595,6 +742,10 @@ export class BlinkIdWorker { cachedClassInfo = null; cachedRotation = null; } catch (error) { + if (!this.#wasmModule) { + throw error; + } + this.reportPinglet({ schemaName: "ping.error", schemaVersion: "1.0.0", @@ -606,6 +757,7 @@ export class BlinkIdWorker { stackTrace: error instanceof Error ? error.stack : undefined, }, }); + this.sendPinglets(); throw error; } }, @@ -663,12 +815,30 @@ export class BlinkIdWorker { "Failed to delete BlinkId session during terminate:", error, ); + if (!this.#wasmModule) { + return; + } + + this.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: this.#currentSessionNumber, + data: { + errorType: "NonFatal", + errorMessage: + error instanceof Error ? error.message : String(error), + stackTrace: error instanceof Error ? error.stack : undefined, + }, + }); + this.sendPinglets(); } finally { this.#activeSession = undefined; } } if (!this.#wasmModule) { + this.#cleanupCrashReporter?.(); + this.#cleanupCrashReporter = undefined; console.warn( "No Wasm module loaded during worker termination. Skipping cleanup.", ); @@ -696,6 +866,8 @@ export class BlinkIdWorker { await new Promise((resolve) => setTimeout(resolve, 100)); } this.#wasmModule = undefined; + this.#cleanupCrashReporter?.(); + this.#cleanupCrashReporter = undefined; console.debug("BlinkIdWorker terminated 🔴"); self.close(); diff --git a/packages/blinkid/CHANGELOG.md b/packages/blinkid/CHANGELOG.md index 4771c8a..eae731b 100644 --- a/packages/blinkid/CHANGELOG.md +++ b/packages/blinkid/CHANGELOG.md @@ -1,5 +1,15 @@ # @microblink/blinkid +## 7.7.2 + +### Patch Changes + +- Added crash reporting for failures during `createBlinkId(...)`, including SDK initialization, scanning-session creation, UX-manager setup, and UI startup. +- Updated dependencies + - @microblink/camera-manager@7.3.1 + - @microblink/blinkid-core@7.7.2 + - @microblink/blinkid-ux-manager@7.7.2 + ## 7.7.1 ### Patch Changes diff --git a/packages/blinkid/docs/README.md b/packages/blinkid/docs/README.md index 28a273b..477b22a 100644 --- a/packages/blinkid/docs/README.md +++ b/packages/blinkid/docs/README.md @@ -97,6 +97,7 @@ - [DocumentType](type-aliases/DocumentType.md) - [DownloadProgress](type-aliases/DownloadProgress.md) - [DriverLicenceDetailedInfo](type-aliases/DriverLicenceDetailedInfo.md) +- [ErrorCallback](type-aliases/ErrorCallback.md) - [ExtractionArea](type-aliases/ExtractionArea.md) - [FacingMode](type-aliases/FacingMode.md) - [FeedbackUiOptions](type-aliases/FeedbackUiOptions.md) diff --git a/packages/blinkid/docs/classes/BlinkIdWorker.md b/packages/blinkid/docs/classes/BlinkIdWorker.md index da7c88f..6be9d9d 100644 --- a/packages/blinkid/docs/classes/BlinkIdWorker.md +++ b/packages/blinkid/docs/classes/BlinkIdWorker.md @@ -208,11 +208,11 @@ This method initializes everything. ### reportPinglet() -> **reportPinglet**(`__namedParameters`): `void` +> **reportPinglet**(`pinglet`): `void` #### Parameters -##### \_\_namedParameters +##### pinglet `Ping` diff --git a/packages/blinkid/docs/classes/CameraManager.md b/packages/blinkid/docs/classes/CameraManager.md index 3cd8c28..a67257b 100644 --- a/packages/blinkid/docs/classes/CameraManager.md +++ b/packages/blinkid/docs/classes/CameraManager.md @@ -257,6 +257,26 @@ CameraManager from throwing errors when the user interrupts the process. ## Methods +### addErrorCallback() + +> **addErrorCallback**(`errorCallback`): () => `boolean` + +#### Parameters + +##### errorCallback + +[`ErrorCallback`](../type-aliases/ErrorCallback.md) + +#### Returns + +> (): `boolean` + +##### Returns + +`boolean` + +*** + ### addFrameCaptureCallback() > **addFrameCaptureCallback**(`frameCaptureCallback`): () => `boolean` diff --git a/packages/blinkid/docs/interfaces/BlinkIdUxManager.md b/packages/blinkid/docs/interfaces/BlinkIdUxManager.md index 0ca88d4..67fef88 100644 --- a/packages/blinkid/docs/interfaces/BlinkIdUxManager.md +++ b/packages/blinkid/docs/interfaces/BlinkIdUxManager.md @@ -37,6 +37,22 @@ event queues, and call restartCurrentStateTimer() for help-tooltip resets. *** +### handleCameraManagerError() + +> **handleCameraManagerError**: (`error`) => `void` + +#### Parameters + +##### error + +`Error` + +#### Returns + +`void` + +*** + ### scanningSession > `readonly` **scanningSession**: [`RemoteScanningSession`](../type-aliases/RemoteScanningSession.md) diff --git a/packages/blinkid/docs/type-aliases/ErrorCallback.md b/packages/blinkid/docs/type-aliases/ErrorCallback.md new file mode 100644 index 0000000..987e783 --- /dev/null +++ b/packages/blinkid/docs/type-aliases/ErrorCallback.md @@ -0,0 +1,19 @@ +[**@microblink/blinkid**](../README.md) + +*** + +[@microblink/blinkid](../README.md) / ErrorCallback + +# Type Alias: ErrorCallback() + +> **ErrorCallback** = (`error`) => `void` + +## Parameters + +### error + +`Error` + +## Returns + +`void` diff --git a/packages/blinkid/package.json b/packages/blinkid/package.json index e568914..bccce83 100644 --- a/packages/blinkid/package.json +++ b/packages/blinkid/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/blinkid", "description": "All-in-one BlinkID browser SDK for fast and accurate ID document scanning and recognition in web applications.", - "version": "7.7.1", + "version": "7.7.2", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/blinkid/src/createBlinkIdUi.test.ts b/packages/blinkid/src/createBlinkIdUi.test.ts index 5278f5b..354bacf 100644 --- a/packages/blinkid/src/createBlinkIdUi.test.ts +++ b/packages/blinkid/src/createBlinkIdUi.test.ts @@ -18,6 +18,8 @@ const fakeCameraManagerRef = vi.hoisted(() => ({ const { mockCreateSession, mockTerminate, + mockReportPinglet, + mockSendPinglets, mockCreateBlinkIdUxManager, mockCreateBlinkIdFeedbackUi, mockAddOnResultCallback, @@ -30,6 +32,8 @@ const { } = vi.hoisted(() => { const mockTerminate = vi.fn().mockResolvedValue(undefined); const mockCreateSession = vi.fn(); + const mockReportPinglet = vi.fn().mockResolvedValue(undefined); + const mockSendPinglets = vi.fn().mockResolvedValue(undefined); const mockAddOnResultCallback = vi.fn(); const mockAddOnErrorCallback = vi.fn(); const mockAddOnDocumentFilteredCallback = vi.fn(); @@ -48,6 +52,8 @@ const { return { mockTerminate, mockCreateSession, + mockReportPinglet, + mockSendPinglets, mockCreateBlinkIdUxManager, mockCreateBlinkIdFeedbackUi, mockAddOnResultCallback, @@ -68,6 +74,8 @@ vi.mock("@microblink/blinkid-core", () => ({ loadBlinkIdCore: vi.fn().mockResolvedValue({ createScanningSession: mockCreateSession, terminate: mockTerminate, + reportPinglet: mockReportPinglet, + sendPinglets: mockSendPinglets, }), BlinkIdSessionSettings: class {}, })); @@ -308,6 +316,55 @@ describe("createBlinkId", () => { ).toHaveBeenCalledTimes(1); }); + test("best-effort reports crashes through the core before a session exists", async () => { + mockCreateSession.mockRejectedValueOnce(new Error("session failed")); + + await expect(createBlinkId({ licenseKey: "test-key" })).rejects.toThrow( + "session failed", + ); + + expect(mockReportPinglet).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + sessionNumber: 0, + data: expect.objectContaining({ + errorType: "Crash", + errorMessage: "sdk.createBlinkId: session failed", + }), + }), + ); + expect(mockSendPinglets).toHaveBeenCalledTimes(1); + }); + + test("best-effort reports crashes through the core after session creation", async () => { + const scanningSession = createFakeScanningSession(); + mockCreateSession.mockResolvedValueOnce(scanningSession); + mockCreateBlinkIdUxManager.mockRejectedValueOnce(new Error("ux failed")); + + await expect(createBlinkId({ licenseKey: "test-key" })).rejects.toThrow( + "ux failed", + ); + + expect(mockReportPinglet).toHaveBeenCalledWith( + expect.objectContaining({ + schemaName: "ping.error", + sessionNumber: 0, + data: expect.objectContaining({ + errorType: "Crash", + errorMessage: "sdk.createBlinkId: ux failed", + }), + }), + ); + const firstPinglet = mockReportPinglet.mock.calls[0]?.[0] as + | { sessionNumber?: number } + | undefined; + + expect(firstPinglet?.sessionNumber).toBe(0); + expect(mockSendPinglets).toHaveBeenCalledTimes(1); + expect(scanningSession.ping).not.toHaveBeenCalled(); + expect(scanningSession.sendPinglets).not.toHaveBeenCalled(); + }); + test("creates feedback UI only once even if playbackState fires multiple times", async () => { await createBlinkId({ licenseKey: "test-key" }); diff --git a/packages/blinkid/src/createBlinkIdUi.ts b/packages/blinkid/src/createBlinkIdUi.ts index 6b4bc45..670b40b 100644 --- a/packages/blinkid/src/createBlinkIdUi.ts +++ b/packages/blinkid/src/createBlinkIdUi.ts @@ -140,84 +140,124 @@ export const createBlinkId = async ({ scanningMode, feedbackUiOptions, }: BlinkIdComponentOptions) => { - // we first initialize the direct API. This loads the WASM module and initializes the engine - const blinkIdCore = await loadBlinkIdCore({ - licenseKey, - microblinkProxyUrl, - initialMemory, - resourcesLocation, - useLightweightBuild, - wasmVariant, - }); - - const scanningSession = await blinkIdCore.createScanningSession({ - scanningMode, - scanningSettings, - }); - - // we create the camera manager - const cameraManager = new CameraManager(); - - // we create the UX manager - const blinkIdUxManager = await createBlinkIdUxManager( - cameraManager, - scanningSession, - ); - - // this creates the UI and attaches it to the DOM - const cameraUi = await createCameraManagerUi( - cameraManager, - targetNode, - cameraManagerUiOptions, - ); - - const unsub = cameraManager.subscribe( - (s) => s.playbackState, - (state) => { - if (state === "playback") { - // this creates the feedback UI and attaches it to the camera UI - createBlinkIdFeedbackUi( - blinkIdUxManager, - cameraUi, - feedbackUiOptions ?? {}, - ); + let blinkIdCore: BlinkIdCore | undefined; + let scanningSession: + | Awaited> + | undefined; + + try { + // we first initialize the direct API. This loads the WASM module and initializes the engine + blinkIdCore = await loadBlinkIdCore({ + licenseKey, + microblinkProxyUrl, + initialMemory, + resourcesLocation, + useLightweightBuild, + wasmVariant, + }); + + scanningSession = await blinkIdCore.createScanningSession({ + scanningMode, + scanningSettings, + }); + + // we create the camera manager + const cameraManager = new CameraManager(); + + // we create the UX manager + const blinkIdUxManager = await createBlinkIdUxManager( + cameraManager, + scanningSession, + ); - if (feedbackUiOptions?.showOnboardingGuide === false) { - void cameraManager.startFrameCapture(); + // this creates the UI and attaches it to the DOM + const cameraUi = await createCameraManagerUi( + cameraManager, + targetNode, + cameraManagerUiOptions, + ); + + const unsub = cameraManager.subscribe( + (s) => s.playbackState, + (state) => { + if (state === "playback") { + // this creates the feedback UI and attaches it to the camera UI + createBlinkIdFeedbackUi( + blinkIdUxManager, + cameraUi, + feedbackUiOptions ?? {}, + ); + + if (feedbackUiOptions?.showOnboardingGuide === false) { + void cameraManager.startFrameCapture(); + } + + unsub(); // unsubscribe from the playback state } + }, + ); + + // selects the camera and starts the stream + await cameraManager.startCameraStream(); - unsub(); // unsubscribe from the playback state + if (!blinkIdCore) { + throw new Error("BlinkID core not initialized"); + } + + const loadedBlinkIdCore = blinkIdCore; + + const destroy = async () => { + cameraUi.dismount(); + try { + await loadedBlinkIdCore.terminate(); + } catch (error) { + console.warn(error); + } + }; + + const returnObject: BlinkIdComponent = { + blinkIdCore: loadedBlinkIdCore, + cameraManager, + blinkIdUxManager, + cameraUi, + destroy, + addOnErrorCallback: + blinkIdUxManager.addOnErrorCallback.bind(blinkIdUxManager), + addOnResultCallback: + blinkIdUxManager.addOnResultCallback.bind(blinkIdUxManager), + addOnDocumentFilteredCallback: + blinkIdUxManager.addOnDocumentFilteredCallback.bind(blinkIdUxManager), + addDocumentClassFilter: + blinkIdUxManager.addDocumentClassFilter.bind(blinkIdUxManager), + }; + + return returnObject; + } catch (error) { + if (blinkIdCore) { + const data = { + errorType: "Crash" as const, + errorMessage: + "sdk.createBlinkId: " + + (error instanceof Error ? error.message : String(error)), + stackTrace: error instanceof Error ? error.stack : undefined, + }; + + try { + await blinkIdCore.reportPinglet({ + schemaName: "ping.error", + schemaVersion: "1.0.0", + sessionNumber: 0, + data, + }); + await blinkIdCore.sendPinglets(); + } catch (reportError) { + console.warn( + "Failed to report BlinkID SDK crash pinglet:", + reportError, + ); } - }, - ); - - // selects the camera and starts the stream - await cameraManager.startCameraStream(); - - const destroy = async () => { - cameraUi.dismount(); - try { - await blinkIdCore.terminate(); - } catch (error) { - console.warn(error); } - }; - - const returnObject: BlinkIdComponent = { - blinkIdCore, - cameraManager, - blinkIdUxManager, - cameraUi, - destroy, - addOnErrorCallback: - blinkIdUxManager.addOnErrorCallback.bind(blinkIdUxManager), - addOnResultCallback: - blinkIdUxManager.addOnResultCallback.bind(blinkIdUxManager), - addOnDocumentFilteredCallback: - blinkIdUxManager.addOnDocumentFilteredCallback.bind(blinkIdUxManager), - addDocumentClassFilter: - blinkIdUxManager.addDocumentClassFilter.bind(blinkIdUxManager), - }; - - return returnObject; + + throw error; + } }; diff --git a/packages/camera-manager/CHANGELOG.md b/packages/camera-manager/CHANGELOG.md index 98bdda6..fe3b707 100644 --- a/packages/camera-manager/CHANGELOG.md +++ b/packages/camera-manager/CHANGELOG.md @@ -1,5 +1,12 @@ # @microblink/camera-manager +## 7.3.1 + +### Patch Changes + +- Added `addErrorCallback(...)` so consumers can observe frame-loop failures from `CameraManager`. +- Reattaches returned `ArrayBufferView` buffers before throwing when a frame callback returns a view instead of the underlying `ArrayBuffer`, avoiding detached-buffer fallout in subsequent captures. + ## 7.3.0 ### Minor Changes diff --git a/packages/camera-manager/docs/README.md b/packages/camera-manager/docs/README.md index b8b4f8d..a355b62 100644 --- a/packages/camera-manager/docs/README.md +++ b/packages/camera-manager/docs/README.md @@ -25,6 +25,7 @@ - [CameraUiRefs](type-aliases/CameraUiRefs.md) - [CanvasRenderingMode](type-aliases/CanvasRenderingMode.md) - [DismountCallback](type-aliases/DismountCallback.md) +- [ErrorCallback](type-aliases/ErrorCallback.md) - [ExtractionArea](type-aliases/ExtractionArea.md) - [FacingMode](type-aliases/FacingMode.md) - [FrameCaptureCallback](type-aliases/FrameCaptureCallback.md) diff --git a/packages/camera-manager/docs/classes/CameraManager.md b/packages/camera-manager/docs/classes/CameraManager.md index 652a2a6..4075743 100644 --- a/packages/camera-manager/docs/classes/CameraManager.md +++ b/packages/camera-manager/docs/classes/CameraManager.md @@ -212,6 +212,26 @@ CameraManager from throwing errors when the user interrupts the process. ## Methods +### addErrorCallback() + +> **addErrorCallback**(`errorCallback`): () => `boolean` + +#### Parameters + +##### errorCallback + +[`ErrorCallback`](../type-aliases/ErrorCallback.md) + +#### Returns + +> (): `boolean` + +##### Returns + +`boolean` + +*** + ### addFrameCaptureCallback() > **addFrameCaptureCallback**(`frameCaptureCallback`): () => `boolean` diff --git a/packages/camera-manager/docs/type-aliases/ErrorCallback.md b/packages/camera-manager/docs/type-aliases/ErrorCallback.md new file mode 100644 index 0000000..004e08d --- /dev/null +++ b/packages/camera-manager/docs/type-aliases/ErrorCallback.md @@ -0,0 +1,19 @@ +[**@microblink/camera-manager**](../README.md) + +*** + +[@microblink/camera-manager](../README.md) / ErrorCallback + +# Type Alias: ErrorCallback() + +> **ErrorCallback** = (`error`) => `void` + +## Parameters + +### error + +`Error` + +## Returns + +`void` diff --git a/packages/camera-manager/package.json b/packages/camera-manager/package.json index f42f99c..10253e7 100644 --- a/packages/camera-manager/package.json +++ b/packages/camera-manager/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/camera-manager", "description": "Manages cameras and stream, provides both a headless component and a UI component", - "version": "7.3.0", + "version": "7.3.1", "author": "Microblink", "scripts": { "build": "concurrently pnpm:build:js pnpm:build:types", diff --git a/packages/camera-manager/src/core/CameraManager.ts b/packages/camera-manager/src/core/CameraManager.ts index f77e2da..bf5f8f4 100644 --- a/packages/camera-manager/src/core/CameraManager.ts +++ b/packages/camera-manager/src/core/CameraManager.ts @@ -128,6 +128,7 @@ export class CameraManager { * "capturing". */ #frameCaptureCallbacks = new Set(); + #errorCallbacks = new Set(); /** * Creates a new CameraManager instance. @@ -363,6 +364,11 @@ export class CameraManager { return () => this.#frameCaptureCallbacks.delete(frameCaptureCallback); } + addErrorCallback(errorCallback: ErrorCallback) { + this.#errorCallbacks.add(errorCallback); + return () => this.#errorCallbacks.delete(errorCallback); + } + /** * Cleans up the video element, and stops the stream. */ @@ -923,71 +929,83 @@ export class CameraManager { * The main recognition loop. Do not call this method directly, use `#queueFrame` instead. */ async #loop() { - const state = store.getState(); + try { + const state = store.getState(); - if (this.#videoFrameRequestId === undefined) { - console.error("Missing request ID"); - return; - } + if (this.#videoFrameRequestId === undefined) { + console.error("Missing request ID"); + return; + } - if (!state.videoElement) { - // shouldn't happen as disconnecting is handled by an observer which will - // pause the loop - console.warn("Missing video element, should not happen"); - return; - } + if (!state.videoElement) { + // shouldn't happen as disconnecting is handled by an observer which will + // pause the loop + console.warn("Missing video element, should not happen"); + return; + } - if (!state.extractionArea) { - console.warn( - "Stream started before extraction area was set, skipping frame.", - ); - return; - } + if (!state.extractionArea) { + console.warn( + "Stream started before extraction area was set, skipping frame.", + ); + return; + } - const isSameOrientation = - state.videoElement.videoHeight >= state.videoElement.videoWidth === - state.extractionArea.height >= state.extractionArea.width; + const isSameOrientation = + state.videoElement.videoHeight >= state.videoElement.videoWidth === + state.extractionArea.height >= state.extractionArea.width; - if (!isSameOrientation) { - // elements not in sync, wait for next frame - return this.#queueFrame(); - } + if (!isSameOrientation) { + // elements not in sync, wait for next frame + return this.#queueFrame(); + } - if (this.#frameCaptureCallbacks.size !== 0) { - const capturedFrame = this.#videoFrameProcessor.getImageData( - state.videoElement, - state.extractionArea, - ); + if (this.#frameCaptureCallbacks.size !== 0) { + const capturedFrame = this.#videoFrameProcessor.getImageData( + state.videoElement, + state.extractionArea, + ); - // Iterate over all frame capture callbacks - for (const callback of this.#frameCaptureCallbacks) { - const workingFrame = isBufferDetached(capturedFrame.data.buffer) - ? this.#videoFrameProcessor.getCurrentImageData() - : capturedFrame; + // Iterate over all frame capture callbacks + for (const callback of this.#frameCaptureCallbacks) { + const workingFrame = isBufferDetached(capturedFrame.data.buffer) + ? this.#videoFrameProcessor.getCurrentImageData() + : capturedFrame; - // Process the frame and potentially get a new buffer back - const returnedBuffer = await callback(workingFrame); + // Process the frame and potentially get a new buffer back + const returnedBuffer = await callback(workingFrame); - // Exit current iteration if we didn't get a buffer back - if (!returnedBuffer) { - continue; - } + // Exit current iteration if we didn't get a buffer back + if (!returnedBuffer) { + continue; + } - if (!(returnedBuffer instanceof ArrayBuffer)) { - throw new Error( - stripIndents` - Frame capture callback did not return an ArrayBuffer. - Make sure to return the underlying buffer, not the view. - `, - ); - } + if (!(returnedBuffer instanceof ArrayBuffer)) { + if (ArrayBuffer.isView(returnedBuffer)) { + this.#videoFrameProcessor.reattachArrayBuffer( + returnedBuffer.buffer, + ); + } + + throw new Error( + stripIndents` + Frame capture callback did not return an ArrayBuffer. + Make sure to return the underlying buffer, not the view. + `, + ); + } - // Return the buffer to the processor - this.#videoFrameProcessor.reattachArrayBuffer(returnedBuffer); + // Return the buffer to the processor + this.#videoFrameProcessor.reattachArrayBuffer(returnedBuffer); + } } - } - this.#queueFrame(); + this.#queueFrame(); + } catch (error) { + const frameCaptureError = asError(error); + this.#invokeErrorCallbacks(frameCaptureError); + throw frameCaptureError; + } } /** @@ -1103,10 +1121,21 @@ export class CameraManager { reset() { console.debug("Resetting camera manager"); this.#frameCaptureCallbacks.clear(); + this.#errorCallbacks.clear(); this.userInitiatedAbort = false; this.stopStream(); resetStore(); } + + #invokeErrorCallbacks(error: Error) { + for (const callback of this.#errorCallbacks) { + try { + callback(error); + } catch (callbackError) { + console.error("Error in error callback", callbackError); + } + } + } } /** @@ -1120,6 +1149,8 @@ export type FrameCaptureCallback = ( frame: ImageData, ) => Promisable; +export type ErrorCallback = (error: Error) => void; + /** * A camera getter. * diff --git a/packages/core-common/CHANGELOG.md b/packages/core-common/CHANGELOG.md index 1e5b3da..553a7ee 100644 --- a/packages/core-common/CHANGELOG.md +++ b/packages/core-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @microblink/core-common +## 1.0.1 + +### Patch Changes + +- `createProxyWorker(...)` now throws an explicit `FrameTransferError` when transferring a frame to the worker fails, making frame-transfer failures easier to detect and report. + ## 1.0.0 ### Major Changes diff --git a/packages/core-common/package.json b/packages/core-common/package.json index 4b440af..7c8e533 100644 --- a/packages/core-common/package.json +++ b/packages/core-common/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/core-common", "private": true, - "version": "1.0.0", + "version": "1.0.1", "scripts": { "build": "tsc", "build:publish": "tsc", diff --git a/packages/core-common/src/createProxyWorker.ts b/packages/core-common/src/createProxyWorker.ts index c48d57b..da8b1b6 100644 --- a/packages/core-common/src/createProxyWorker.ts +++ b/packages/core-common/src/createProxyWorker.ts @@ -7,6 +7,28 @@ import { releaseProxy, transfer, wrap } from "comlink"; import { oneLineTrim } from "common-tags"; import { getCrossOriginWorkerURL } from "./getCrossOriginWorkerURL"; +export const FRAME_TRANSFER_ERROR_NAME = "FrameTransferError"; + +export class FrameTransferError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = FRAME_TRANSFER_ERROR_NAME; + } +} + +const createFrameTransferError = (message: string, error: unknown) => { + const causeMessage = + error instanceof Error && error.message ? `: ${error.message}` : ""; + + if (error instanceof Error) { + return new FrameTransferError(`${message}${causeMessage}`, { + cause: error, + }); + } + + return new FrameTransferError(`${message}${causeMessage}`); +}; + /** * Checks if a URL is a data URL * @param url URL to check @@ -167,8 +189,21 @@ export async function createProxyWorker( colorSpace: imageData.colorSpace ?? "srgb", } satisfies ImageData; + let transferredImageData: ImageData; + + try { + transferredImageData = transfer(imageDataLike, [ + imageData.data.buffer, + ]); + } catch (error) { + throw createFrameTransferError( + "Failed to transfer frame to worker", + error, + ); + } + return (sessionTarget as BaseScanningSession).process( - transfer(imageDataLike, [imageData.data.buffer]), + transferredImageData, ); }; } diff --git a/packages/test-utils/src/mocks/wasmModuleFactory.ts b/packages/test-utils/src/mocks/wasmModuleFactory.ts index c266dd1..29269a1 100644 --- a/packages/test-utils/src/mocks/wasmModuleFactory.ts +++ b/packages/test-utils/src/mocks/wasmModuleFactory.ts @@ -11,6 +11,7 @@ import { vi } from "vitest"; let wasmModuleMock: WasmModule | null = null; +let lastModuleOverrides: Record | undefined; export function setWasmModuleMock>( module: T | null, @@ -18,6 +19,14 @@ export function setWasmModuleMock>( wasmModuleMock = module; } +export function getLastModuleOverrides() { + return lastModuleOverrides; +} + +export function resetLastModuleOverrides() { + lastModuleOverrides = undefined; +} + type WasmModuleSpies> = { [K in keyof T]: ReturnType; }; @@ -67,9 +76,12 @@ export function getWasmModuleMock(): Promise> { * Default runtime factory for dynamic worker imports in tests. * Extra arguments are ignored (worker code passes Emscripten module options). */ -export default function createMockModule(): Promise< +export default function createMockModule( + moduleOverrides?: Record, +): Promise< WasmModule > { + lastModuleOverrides = moduleOverrides; if (!wasmModuleMock) { throw new Error( "Mock WASM module not set. Call setWasmModuleMock in test.", diff --git a/packages/test-utils/src/vitest/cameraManager.test.ts b/packages/test-utils/src/vitest/cameraManager.test.ts index a508224..1f0cb84 100644 --- a/packages/test-utils/src/vitest/cameraManager.test.ts +++ b/packages/test-utils/src/vitest/cameraManager.test.ts @@ -3,7 +3,11 @@ */ import { describe, expect, test, vi } from "vitest"; -import { FakeCameraManager, type FakeCameraManagerState } from "./cameraManager"; +import { + createFakeCameraHarness, + FakeCameraManager, + type FakeCameraManagerState, +} from "./cameraManager"; describe("FakeCameraManager", () => { const imageData = {} as ImageData; @@ -40,11 +44,7 @@ describe("FakeCameraManager", () => { expect(rootListener).toHaveBeenCalledTimes(3); // fireImmediately + capturing + idle (second capturing should be filtered by Object.is) expect(selectorListener).toHaveBeenCalledTimes(3); - expect(selectorListener).toHaveBeenNthCalledWith( - 2, - "capturing", - "idle", - ); + expect(selectorListener).toHaveBeenNthCalledWith(2, "capturing", "idle"); }); test("supports custom selector equality function", () => { @@ -75,4 +75,40 @@ describe("FakeCameraManager", () => { expect(result).toBeUndefined(); }); + + test("registers and unregisters frame-capture error callback", () => { + const manager = new FakeCameraManager(); + const callback = vi.fn(); + const error = new Error("frame capture failed"); + + const cleanup = manager.addErrorCallback(callback); + manager.emitError(error); + expect(callback).toHaveBeenCalledWith(error); + // Explicit count before cleanup so the post-cleanup assertion only verifies unregistration. + expect(callback).toHaveBeenCalledTimes(1); + + expect(cleanup()).toBe(true); + manager.emitError(error); + expect(callback).toHaveBeenCalledTimes(1); + }); + + test("creates a reusable camera harness around the fake camera manager", async () => { + const harness = createFakeCameraHarness<{ tag: "camera-manager" }>({ + initialState: { + playbackState: "idle", + }, + }); + const callback = vi.fn(); + + harness.fakeCameraManager.addFrameCaptureCallback(callback); + harness.emitPlaybackState("capturing"); + await harness.emitFrame(imageData); + harness.setIsActive(false); + + expect(callback).toHaveBeenCalledTimes(1); + expect(harness.fakeCameraManager.getCurrentState().playbackState).toBe( + "capturing", + ); + expect(harness.fakeCameraManager.isActive).toBe(false); + }); }); diff --git a/packages/test-utils/src/vitest/cameraManager.ts b/packages/test-utils/src/vitest/cameraManager.ts index 3adce54..fa71788 100644 --- a/packages/test-utils/src/vitest/cameraManager.ts +++ b/packages/test-utils/src/vitest/cameraManager.ts @@ -3,9 +3,11 @@ */ import { vi } from "vitest"; + type FrameCaptureCallback = ( frame: ImageData, ) => Promise | ArrayBufferLike | void; +type ErrorCallback = (error: Error) => void; type PlaybackState = "idle" | "playback" | "capturing"; type CameraPermission = "prompt" | "granted" | "denied" | "blocked" | undefined; @@ -27,11 +29,23 @@ export type FakeCameraManagerState = { cameraPermission?: CameraPermission; }; -type CreateFakeCameraManagerOptions = { +export type CreateFakeCameraManagerOptions = { initialState?: Partial; isActive?: boolean; }; +export type FakeCameraHarness = { + cameraManager: TCameraManager; + fakeCameraManager: FakeCameraManager; + emitPlaybackState: (playbackState: PlaybackState) => void; + emitFrame: (imageData: ImageData) => Promise; + emitCameraState: (nextState: Partial) => void; + setIsActive: (value: boolean) => void; + stopFrameCapture: FakeCameraManager["stopFrameCapture"]; + startFrameCapture: FakeCameraManager["startFrameCapture"]; + startCameraStream: FakeCameraManager["startCameraStream"]; +}; + type SelectorSubscription = { selector: (state: FakeCameraManagerState) => unknown; listener: (selectedState: unknown, previousSelectedState: unknown) => void; @@ -60,6 +74,7 @@ const defaultState: FakeCameraManagerState = { export class FakeCameraManager { #state: FakeCameraManagerState; #frameCaptureCallback: FrameCaptureCallback | undefined; + #errorCallbacks = new Set(); #isActive: boolean; #rootSubscriptions = new Set(); #selectorSubscriptions = new Set(); @@ -96,6 +111,11 @@ export class FakeCameraManager { }; }); + addErrorCallback = vi.fn((callback: ErrorCallback) => { + this.#errorCallbacks.add(callback); + return () => this.#errorCallbacks.delete(callback); + }); + subscribe = vi.fn( (selectorOrListener: unknown, listener?: unknown, optionsArg?: unknown) => { if (typeof listener === "function") { @@ -161,8 +181,34 @@ export class FakeCameraManager { return this.#frameCaptureCallback?.(imageData); } + emitError(error: Error) { + for (const callback of this.#errorCallbacks) { + callback(error); + } + } + getCurrentState() { return this.#state; } } +export const createFakeCameraHarness = ( + fakeCameraOptions?: CreateFakeCameraManagerOptions, +): FakeCameraHarness => { + const fakeCameraManager = new FakeCameraManager(fakeCameraOptions); + + return { + cameraManager: fakeCameraManager as unknown as TCameraManager, + fakeCameraManager, + emitPlaybackState: (playbackState) => + fakeCameraManager.emitPlaybackState(playbackState), + emitFrame: (imageData) => fakeCameraManager.emitFrame(imageData), + emitCameraState: (nextState) => fakeCameraManager.emitState(nextState), + setIsActive: (value) => { + fakeCameraManager.isActive = value; + }, + stopFrameCapture: fakeCameraManager.stopFrameCapture, + startFrameCapture: fakeCameraManager.startFrameCapture, + startCameraStream: fakeCameraManager.startCameraStream, + }; +}; diff --git a/packages/worker-common/CHANGELOG.md b/packages/worker-common/CHANGELOG.md index 77e24c0..744edf5 100644 --- a/packages/worker-common/CHANGELOG.md +++ b/packages/worker-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @microblink/worker-common +## 1.0.2 + +### Patch Changes + +- Added the `workerCrashReporter` entry point with `installWorkerCrashReporter(...)` for reporting `error` and `unhandledrejection` events from worker scope. + ## 1.0.1 ### Patch Changes diff --git a/packages/worker-common/package.json b/packages/worker-common/package.json index ea1aa9f..4517230 100644 --- a/packages/worker-common/package.json +++ b/packages/worker-common/package.json @@ -1,7 +1,7 @@ { "name": "@microblink/worker-common", "private": true, - "version": "1.0.1", + "version": "1.0.2", "scripts": { "build": "tsc", "build:publish": "tsc", @@ -52,6 +52,10 @@ "types": "./types/downloadResourceBuffer.d.ts", "import": "./dist/downloadResourceBuffer.js" }, + "./workerCrashReporter": { + "types": "./types/workerCrashReporter.d.ts", + "import": "./dist/workerCrashReporter.js" + }, "./package.json": "./package.json" }, "files": [ diff --git a/packages/worker-common/src/workerCrashReporter.test.ts b/packages/worker-common/src/workerCrashReporter.test.ts new file mode 100644 index 0000000..e917321 --- /dev/null +++ b/packages/worker-common/src/workerCrashReporter.test.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2026 Microblink Ltd. All rights reserved. + */ + +import { describe, expect, it, vi } from "vitest"; +import { installWorkerCrashReporter } from "./workerCrashReporter"; + +function makeScope() { + const listeners = new Map(); + return { + listeners, + workerScope: { + addEventListener: vi.fn((type: string, listener: EventListenerOrEventListenerObject | null) => { + listeners.set(type, listener as EventListener); + }), + removeEventListener: vi.fn((type: string, listener: EventListenerOrEventListenerObject | null) => { + listeners.set(`removed:${type}`, listener as EventListener); + }), + }, + }; +} + +describe("installWorkerCrashReporter", () => { + it("reports worker errors with the current session number", () => { + const { listeners, workerScope } = makeScope(); + const onError = vi.fn(); + + installWorkerCrashReporter({ workerScope, getSessionNumber: () => 4, onError }); + + listeners.get("error")!({ + error: new Error("boom"), + message: "boom", + } as unknown as Event); + + expect(onError).toHaveBeenCalledWith({ + origin: "worker.onerror", + error: expect.any(Error), + sessionNumber: 4, + }); + }); + + it("reports unhandled rejections with the default session number", () => { + const { listeners, workerScope } = makeScope(); + const onError = vi.fn(); + + installWorkerCrashReporter({ workerScope, onError }); + + listeners.get("unhandledrejection")!({ reason: "rejected" } as unknown as Event); + + expect(onError).toHaveBeenCalledWith({ + origin: "worker.unhandledrejection", + error: "rejected", + sessionNumber: 0, + }); + }); + + it("falls back to message string when error property is absent", () => { + const { listeners, workerScope } = makeScope(); + const onError = vi.fn(); + + installWorkerCrashReporter({ workerScope, onError }); + + listeners.get("error")!({ message: "something went wrong" } as unknown as Event); + + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ origin: "worker.onerror", error: "something went wrong" }), + ); + }); + + it("falls back to 'Unknown worker error' when both error and message are absent", () => { + const { listeners, workerScope } = makeScope(); + const onError = vi.fn(); + + installWorkerCrashReporter({ workerScope, onError }); + + listeners.get("error")!({} as unknown as Event); + + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ origin: "worker.onerror", error: "Unknown worker error" }), + ); + }); + + it("falls back to 'Unhandled worker rejection' when reason is absent", () => { + const { listeners, workerScope } = makeScope(); + const onError = vi.fn(); + + installWorkerCrashReporter({ workerScope, onError }); + + listeners.get("unhandledrejection")!({} as unknown as Event); + + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ origin: "worker.unhandledrejection", error: "Unhandled worker rejection" }), + ); + }); + + it("removes the same listener references on teardown", () => { + const { listeners, workerScope } = makeScope(); + + const uninstall = installWorkerCrashReporter({ workerScope, onError: vi.fn() }); + uninstall(); + + expect(listeners.get("removed:error")).toBe(listeners.get("error")); + expect(listeners.get("removed:unhandledrejection")).toBe(listeners.get("unhandledrejection")); + }); + + it("suppresses reentrant errors fired during the onError callback", () => { + const { listeners, workerScope } = makeScope(); + const onError = vi.fn(() => { + listeners.get("error")!({ error: new Error("reentrant") } as unknown as Event); + }); + + installWorkerCrashReporter({ workerScope, onError }); + + listeners.get("error")!({ error: new Error("original") } as unknown as Event); + + expect(onError).toHaveBeenCalledOnce(); + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ error: expect.objectContaining({ message: "original" }) }), + ); + }); + + it("resumes reporting after onError throws", () => { + const { listeners, workerScope } = makeScope(); + let callCount = 0; + const onError = vi.fn(() => { + callCount++; + if (callCount === 1) throw new Error("callback failure"); + }); + + installWorkerCrashReporter({ workerScope, onError }); + + expect(() => + listeners.get("error")!({ error: new Error("first") } as unknown as Event), + ).toThrow("callback failure"); + + listeners.get("error")!({ error: new Error("second") } as unknown as Event); + + expect(onError).toHaveBeenCalledTimes(2); + expect(onError).toHaveBeenLastCalledWith( + expect.objectContaining({ error: expect.objectContaining({ message: "second" }) }), + ); + }); +}); diff --git a/packages/worker-common/src/workerCrashReporter.ts b/packages/worker-common/src/workerCrashReporter.ts new file mode 100644 index 0000000..6474fa5 --- /dev/null +++ b/packages/worker-common/src/workerCrashReporter.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2026 Microblink Ltd. All rights reserved. + */ + +type WorkerEventTarget = Pick< + EventTarget, + "addEventListener" | "removeEventListener" +>; + +type WorkerErrorCallback = (params: { + origin: "worker.onerror" | "worker.unhandledrejection"; + error: unknown; + sessionNumber: number; +}) => void; + +/** + * Installs a worker crash reporter. Adds error and unhandled rejection + * listeners to the worker and reports the error or rejection to the provided + * callback. + * + * @param params - The parameters for the worker crash reporter. + * @param params.workerScope - The worker scope to install the crash reporter + * on. Defaults to `self`. + * @param params.getSessionNumber - A function to get the current session + * number. Defaults to `() => 0`. + * @param params.onError - A callback to call when an error or unhandled + * rejection occurs. + * + * @returns A function to uninstall the worker crash reporter. + */ +export function installWorkerCrashReporter({ + workerScope, + getSessionNumber, + onError, +}: { + workerScope?: WorkerEventTarget; + getSessionNumber?: () => number; + onError: WorkerErrorCallback; +}) { + const target = workerScope ?? self; + const readSessionNumber = getSessionNumber ?? (() => 0); + let isReporting = false; + + const report = ( + origin: "worker.onerror" | "worker.unhandledrejection", + error: unknown, + ) => { + if (isReporting) { + return; + } + + isReporting = true; + + try { + onError({ + origin, + error, + sessionNumber: readSessionNumber(), + }); + } finally { + isReporting = false; + } + }; + + const handleError = (event: Event) => { + const errorEvent = event as ErrorEvent; + + report( + "worker.onerror", + errorEvent.error ?? errorEvent.message ?? "Unknown worker error", + ); + }; + + const handleUnhandledRejection = (event: Event) => { + const rejectionEvent = event as PromiseRejectionEvent; + + report( + "worker.unhandledrejection", + rejectionEvent.reason ?? "Unhandled worker rejection", + ); + }; + + target.addEventListener("error", handleError); + target.addEventListener("unhandledrejection", handleUnhandledRejection); + + return () => { + target.removeEventListener("error", handleError); + target.removeEventListener("unhandledrejection", handleUnhandledRejection); + }; +}