133 lines
7.2 KiB
TypeScript
133 lines
7.2 KiB
TypeScript
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<Worker, 'postMessage' | 'terminate'> & {
|
|
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<Pick<NativeOcctHistoryWorkerOptions, 'moduleUrl' | 'initializationTimeoutMs'>>
|
|
private readonly pending = new Map<string, { resolve: (response: NativeOcctHistoryProtocolResponse) => void; reject: (error: Error) => void }>()
|
|
private initialization: Promise<NativeOcctHistoryCapabilities> | 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<WorkerResponse>) => {
|
|
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<NativeOcctHistoryCapabilities> {
|
|
if (this.current.availability === 'available') return Promise.resolve(this.capabilities())
|
|
if (this.initialization) return this.initialization
|
|
this.initialization = new Promise<NativeOcctHistoryCapabilities>((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<WorkerResponse>) => { 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<NativeOcctHistoryProtocolResponse> {
|
|
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<NativeOcctHistoryProtocolResponse>((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
|