All files / web/src/lib/vision/opencv loader.ts

0% Statements 0/166
0% Branches 0/1
0% Functions 0/1
0% Lines 0/166

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167                                                                                                                                                                                                                                                                                                                                             
/**
 * OpenCV.js Lazy Loader
 *
 * Handles lazy loading of OpenCV.js (~8MB) with caching and error handling.
 * Non-React module - can be used directly or wrapped in a hook.
 */

import type { CV, WindowWithOpenCV } from './types'

/** Singleton state for OpenCV loading */
let cvInstance: CV | null = null
let loadPromise: Promise<CV> | null = null
let loadError: Error | null = null

/**
 * Check if OpenCV is fully initialized and ready to use
 */
export function isOpenCVReady(): boolean {
  const win = window as unknown as WindowWithOpenCV
  return !!(win.cv && typeof win.cv.imread === 'function')
}

/**
 * Get the OpenCV instance if already loaded
 */
export function getOpenCV(): CV | null {
  return cvInstance
}

/**
 * Get any error that occurred during loading
 */
export function getOpenCVError(): Error | null {
  return loadError
}

/**
 * Check if OpenCV is currently loading
 */
export function isOpenCVLoading(): boolean {
  return loadPromise !== null && cvInstance === null && loadError === null
}

/**
 * Load OpenCV.js lazily. Returns a promise that resolves to the CV instance.
 * Subsequent calls return the same promise/instance.
 *
 * @param scriptUrl - URL to load OpenCV.js from (default: '/opencv.js')
 * @param timeout - Maximum time to wait for OpenCV to initialize (default: 30000ms)
 */
export async function loadOpenCV(
  scriptUrl: string = '/opencv.js',
  timeout: number = 30000
): Promise<CV> {
  // Already loaded
  if (cvInstance) return cvInstance

  // Already loading
  if (loadPromise) return loadPromise

  // Start loading
  loadPromise = (async () => {
    if (typeof window === 'undefined') {
      throw new Error('OpenCV.js can only be loaded in a browser environment')
    }

    const win = window as unknown as WindowWithOpenCV

    // Check if already initialized
    if (isOpenCVReady()) {
      cvInstance = win.cv as CV
      return cvInstance
    }

    // Check if script is already in DOM
    const existingScript = document.querySelector(`script[src="${scriptUrl}"]`)

    if (!existingScript) {
      // Load the script
      await new Promise<void>((resolve, reject) => {
        const script = document.createElement('script')
        script.src = scriptUrl
        script.async = true

        const timeoutId = setTimeout(() => {
          reject(new Error(`OpenCV.js loading timed out after ${timeout}ms`))
        }, timeout)

        script.onload = () => {
          clearTimeout(timeoutId)
          waitForInitialization(resolve, reject, timeout)
        }

        script.onerror = () => {
          clearTimeout(timeoutId)
          reject(new Error(`Failed to load OpenCV.js from ${scriptUrl}`))
        }

        document.head.appendChild(script)
      })
    } else {
      // Script exists, wait for initialization
      await new Promise<void>((resolve, reject) => {
        waitForInitialization(resolve, reject, timeout)
      })
    }

    cvInstance = win.cv as CV
    return cvInstance
  })()

  try {
    return await loadPromise
  } catch (err) {
    loadError = err instanceof Error ? err : new Error('Failed to load OpenCV')
    loadPromise = null
    throw loadError
  }
}

/**
 * Wait for OpenCV runtime to initialize
 */
function waitForInitialization(
  resolve: () => void,
  reject: (err: Error) => void,
  timeout: number
): void {
  const startTime = Date.now()

  const checkReady = () => {
    if (isOpenCVReady()) {
      resolve()
      return
    }

    if (Date.now() - startTime > timeout) {
      reject(new Error(`OpenCV.js initialization timed out after ${timeout}ms`))
      return
    }

    const win = window as unknown as WindowWithOpenCV
    if (win.cv) {
      // OpenCV object exists but not fully initialized - hook into callback
      const previousCallback = win.cv.onRuntimeInitialized
      win.cv.onRuntimeInitialized = () => {
        previousCallback?.()
        resolve()
      }
    } else {
      // Keep polling
      setTimeout(checkReady, 100)
    }
  }

  checkReady()
}

/**
 * Reset the loader state (for testing)
 */
export function resetOpenCVLoader(): void {
  cvInstance = null
  loadPromise = null
  loadError = null
}