import type { NativeOcctHistoryResponse, NativeOcctHistoryStepProvider } from './nativeHistoryProvider' import { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, type NativeOcctHistoryCapabilities, type NativeOcctHistoryProvider, type NativeOcctHistoryProtocolResponse, type NativeOcctHistoryRequest } from './nativeHistoryProtocol' type WorkerLike = Pick & { addEventListener(type: 'message', listener: (event: MessageEvent) => void): void addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void removeEventListener(type: 'error', listener: (event: ErrorEvent) => void): void } type WorkerResponse = { type: 'ready'; capabilities: NativeOcctHistoryCapabilities } | { type: 'response'; response: NativeOcctHistoryProtocolResponse } | { type: 'error'; requestId?: string; error: string } const abortError = () => new DOMException('Native OCCT history Worker request cancelled.', 'AbortError') export type NativeOcctHistoryWorkerOptions = { moduleUrl?: string initializationTimeoutMs?: number workerFactory?: () => WorkerLike } const defaultWorkerFactory = () => new Worker(new URL('./nativeHistoryWorkerEntry.ts', import.meta.url), { type: 'module', name: 'occt-native-history' }) export class NativeOcctHistoryWorkerProvider implements NativeOcctHistoryProvider { private worker: WorkerLike private readonly workerFactory: () => WorkerLike private disposed = false private readonly options: Required> private readonly pending = new Map void; reject: (error: Error) => void }>() private initialization: Promise | null = null private initializationReject: ((error: Error) => void) | null = null private current: NativeOcctHistoryCapabilities = { providerId: 'occt-native.history-step', providerVersion: 'unavailable', occtVersion: 'unknown', availability: 'unavailable', operations: [], transport: 'step-text', reason: 'Native OCCT history Worker has not been initialized.' } private readonly onMessage = (event: MessageEvent) => { if (event.data.type === 'ready') { this.current = event.data.capabilities return } if (event.data.type === 'error') { const requestId = event.data.requestId const pending = requestId ? this.pending.get(requestId) : null if (pending && requestId) { this.pending.delete(requestId); pending.reject(new Error(event.data.error)) } if (!requestId) this.initializationReject?.(new Error(event.data.error)) return } const pending = this.pending.get(event.data.response.requestId) if (!pending) return this.pending.delete(event.data.response.requestId) pending.resolve(event.data.response) } private readonly onError = (event: ErrorEvent) => { const error = new Error(event.message || 'Native OCCT history Worker failed.') for (const pending of this.pending.values()) pending.reject(error) this.pending.clear() this.initialization = null this.initializationReject?.(error) this.initializationReject = null this.current = { ...this.current, availability: 'unavailable', reason: error.message } if (!this.disposed) this.replaceWorker() } private attachWorker(worker: WorkerLike) { worker.addEventListener('message', this.onMessage) worker.addEventListener('error', this.onError) } private detachWorker(worker: WorkerLike) { worker.removeEventListener('message', this.onMessage) worker.removeEventListener('error', this.onError) } private replaceWorker() { const previous = this.worker this.detachWorker(previous) previous.terminate() this.worker = this.workerFactory() this.attachWorker(this.worker) } constructor(options: NativeOcctHistoryWorkerOptions = {}) { this.workerFactory = options.workerFactory || defaultWorkerFactory this.worker = this.workerFactory() this.options = { moduleUrl: options.moduleUrl || '/native/occt-history/bitbybit-occt-history.js', initializationTimeoutMs: options.initializationTimeoutMs ?? 120_000, } this.attachWorker(this.worker) } capabilities(): NativeOcctHistoryCapabilities { return { ...this.current, operations: [...this.current.operations] } } initialize(): Promise { if (this.current.availability === 'available') return Promise.resolve(this.capabilities()) if (this.initialization) return this.initialization this.initialization = new Promise((resolve, reject) => { let settled = false const timer = setTimeout(() => fail(new Error('Native OCCT history Worker initialization timed out.')), this.options.initializationTimeoutMs) const fail = (error: Error) => { if (settled) return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initialization = null; this.initializationReject = null; this.current = { ...this.current, reason: error.message }; reject(error) } this.initializationReject = fail const waitForReady = (event: MessageEvent) => { if (event.data.type === 'error') return fail(new Error(event.data.error)); if (event.data.type !== 'ready') return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initializationReject = null; resolve(this.capabilities()) } this.worker.addEventListener('message', waitForReady) this.worker.postMessage({ type: 'initialize', moduleUrl: this.options.moduleUrl }) }) return this.initialization } async capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise { if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`) await this.initialize() if (signal.aborted) throw abortError() const response = new Promise((resolve, reject) => { this.pending.set(request.requestId, { resolve, reject }) this.worker.postMessage({ type: 'capture', request }) }) const cancel = () => { this.worker.postMessage({ type: 'cancel', requestId: request.requestId }); this.pending.get(request.requestId)?.reject(abortError()); this.pending.delete(request.requestId) } signal.addEventListener('abort', cancel, { once: true }) try { return await response } finally { signal.removeEventListener('abort', cancel) } } dispose() { this.disposed = true this.worker.postMessage({ type: 'dispose' }) this.detachWorker(this.worker) this.worker.terminate() this.initializationReject?.(new Error('Native OCCT history Worker disposed.')) this.initializationReject = null for (const pending of this.pending.values()) pending.reject(new Error('Native OCCT history Worker disposed.')) this.pending.clear() this.initialization = null } } export type NativeOcctHistoryWorkerModule = NativeOcctHistoryStepProvider & { default?: never } export type NativeOcctHistoryWorkerResponse = NativeOcctHistoryResponse