Files
onenote-tools/src/worker/onenote.client.ts

240 lines
6.1 KiB
TypeScript

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<WorkerResponse>) => 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<string, PendingRequest>();
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<ParseWorkerResponse> {
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<PageWorkerResponse> {
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<ResourceWorkerResponse> {
return this.getResource(sessionId, resourceId, 'preview', sectionId);
}
getResourceDownload(
sessionId: string,
resourceId: string,
sectionId?: string
): Promise<ResourceWorkerResponse> {
return this.getResource(sessionId, resourceId, 'download', sectionId);
}
private getResource(
sessionId: string,
resourceId: string,
intent: WorkerResourceIntent,
sectionId?: string
): Promise<ResourceWorkerResponse> {
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<ExportWorkerResponse> {
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();
}
}