import type { ExportWorkerResponse, PageWorkerResponse, ParseWorkerRequest, ParseWorkerResponse, ResourceWorkerResponse, WorkerRequest, WorkerCabinetPart, WorkerExportFormat, WorkerFailure, WorkerResourceIntent, WorkerResponse, } from './worker-protocol.js'; export type SourceKind = 'one' | 'onepkg'; export interface WorkerPort { onmessage: ((event: MessageEvent) => void) | null; onerror: ((event: ErrorEvent) => void) | null; postMessage(message: WorkerRequest, transfer?: Transferable[]): void; terminate(): void; } type PendingRequest = { resolve: ( response: | ParseWorkerResponse | PageWorkerResponse | ResourceWorkerResponse | ExportWorkerResponse ) => void; reject: (error: WorkerClientError) => void; }; export class WorkerClientError extends Error { constructor( readonly code: WorkerFailure['code'], message: string ) { super(message); this.name = 'WorkerClientError'; } } function createModuleWorker(): WorkerPort { return new Worker(new URL('./onenote.worker.ts', import.meta.url), { type: 'module', }) as WorkerPort; } export class OneNoteWorkerClient { private readonly worker: WorkerPort; private readonly pending = new Map(); private requestSequence = 0; private terminated = false; constructor(workerFactory: () => WorkerPort = createModuleWorker) { this.worker = workerFactory(); this.worker.onmessage = ({ data }) => { if (!('requestId' in data)) return; const pending = this.pending.get(data.requestId); if (!pending) return; this.pending.delete(data.requestId); pending.resolve(data); }; this.worker.onerror = () => { const error = new WorkerClientError( 'worker-failure', 'The local parser worker stopped unexpectedly.' ); for (const request of this.pending.values()) request.reject(error); this.pending.clear(); }; } parseSource( kind: SourceKind, fileName: string, bytes: ArrayBuffer, cabinetParts: readonly WorkerCabinetPart[] = [] ): Promise { if (this.terminated) { return Promise.reject( new WorkerClientError( 'worker-failure', 'The local parser worker has already been terminated.' ) ); } if (kind === 'one' && cabinetParts.length > 0) { return Promise.reject( new WorkerClientError( 'invalid-format', 'CAB companions can only be supplied with a .onepkg source.' ) ); } const requestId = `parse-${++this.requestSequence}`; const request: ParseWorkerRequest = { type: kind === 'one' ? 'parse-one' : 'parse-onepkg', requestId, fileName, bytes, ...(kind === 'onepkg' && cabinetParts.length > 0 ? { cabinetParts: [...cabinetParts] } : {}), }; return new Promise((resolve, reject) => { this.pending.set(requestId, { resolve: (response) => resolve(response as ParseWorkerResponse), reject, }); this.worker.postMessage(request, [ bytes, ...cabinetParts.map((part) => part.bytes), ]); }); } getPage( sessionId: string, pageId: string, sectionId?: string ): Promise { if (this.terminated) { return Promise.reject( new WorkerClientError( 'worker-failure', 'The local parser worker has already been terminated.' ) ); } const requestId = `page-${++this.requestSequence}`; return new Promise((resolve, reject) => { this.pending.set(requestId, { resolve: (response) => resolve(response as PageWorkerResponse), reject, }); this.worker.postMessage({ type: 'get-page', requestId, sessionId, pageId, ...(sectionId === undefined ? {} : { sectionId }), }); }); } getResourcePreview( sessionId: string, resourceId: string, sectionId?: string ): Promise { return this.getResource(sessionId, resourceId, 'preview', sectionId); } getResourceDownload( sessionId: string, resourceId: string, sectionId?: string ): Promise { return this.getResource(sessionId, resourceId, 'download', sectionId); } private getResource( sessionId: string, resourceId: string, intent: WorkerResourceIntent, sectionId?: string ): Promise { if (this.terminated) { return Promise.reject( new WorkerClientError( 'worker-failure', 'The local parser worker has already been terminated.' ) ); } const requestId = `resource-${++this.requestSequence}`; return new Promise((resolve, reject) => { this.pending.set(requestId, { resolve: (response) => resolve(response as ResourceWorkerResponse), reject, }); this.worker.postMessage({ type: 'get-resource', requestId, sessionId, resourceId, intent, ...(sectionId === undefined ? {} : { sectionId }), }); }); } exportSession( sessionId: string, format: WorkerExportFormat ): Promise { if (this.terminated) { return Promise.reject( new WorkerClientError( 'worker-failure', 'The local parser worker has already been terminated.' ) ); } const requestId = `export-${++this.requestSequence}`; return new Promise((resolve, reject) => { this.pending.set(requestId, { resolve: (response) => resolve(response as ExportWorkerResponse), reject, }); this.worker.postMessage({ type: 'export-session', requestId, sessionId, format, }); }); } terminate(): void { if (this.terminated) return; this.terminated = true; this.worker.terminate(); const error = new WorkerClientError( 'worker-failure', 'The local parser request was cancelled.' ); for (const request of this.pending.values()) request.reject(error); this.pending.clear(); } }