diff --git a/src/App.test.tsx b/src/App.test.tsx index e20d377..a87ec0e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -52,10 +52,14 @@ class SuccessfulParserWorker implements WorkerPort { onmessage: ((event: MessageEvent) => void) | null = null; onerror: ((event: ErrorEvent) => void) | null = null; terminated = false; + lastMessage: WorkerRequest | undefined; + lastTransfer: Transferable[] = []; constructor(private readonly packageResult?: OneNotePackageDto) {} - postMessage(message: WorkerRequest): void { + postMessage(message: WorkerRequest, transfer: Transferable[] = []): void { + this.lastMessage = message; + this.lastTransfer = transfer; queueMicrotask(() => { if (message.type === 'parse-one') { this.respond({ @@ -158,7 +162,7 @@ describe('OneNoteApplication', () => { ); await user.upload( - screen.getByLabelText('Choose a OneNote file'), + screen.getByLabelText('Choose OneNote files'), new File([new Uint8Array([1, 2, 3])], 'Section.one') ); @@ -189,23 +193,74 @@ describe('OneNoteApplication', () => { tree: { interpreted: false, nodes: [] }, diagnostics: [], }; + const worker = new SuccessfulParserWorker(notebook); render( - new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook)) - } + createWorkerClient={() => new OneNoteWorkerClient(() => worker)} /> ); + const part3 = new File([new Uint8Array([3])], 'Notebook-3.cab'); + const primary = new File([new Uint8Array([1])], 'Notebook.onepkg'); + const part2 = new File([new Uint8Array([2])], 'Notebook-2.cab'); fireEvent.drop(screen.getByTestId('file-drop-zone'), { dataTransfer: { - files: [new File([new Uint8Array([1])], 'Notebook.onepkg')], + files: [part3, primary, part2], }, }); expect( await screen.findByRole('heading', { name: 'Package summary' }) ).toBeInTheDocument(); + expect(worker.lastMessage).toEqual( + expect.objectContaining({ + type: 'parse-onepkg', + fileName: 'Notebook.onepkg', + cabinetParts: [ + expect.objectContaining({ fileName: 'Notebook-3.cab' }), + expect.objectContaining({ fileName: 'Notebook-2.cab' }), + ], + }) + ); + expect(worker.lastTransfer).toHaveLength(3); + }); + + it('hands a multi-file picker selection to the package worker', async () => { + const notebook: OneNotePackageDto = { + format: 'onepkg', + sourceName: 'Notebook.onepkg', + sourceSize: 2, + entries: [], + sections: [], + tree: { interpreted: false, nodes: [] }, + diagnostics: [], + }; + const worker = new SuccessfulParserWorker(notebook); + const user = userEvent.setup(); + render( + new OneNoteWorkerClient(() => worker)} + /> + ); + + const input = screen.getByLabelText('Choose OneNote files'); + expect(input).toHaveAttribute('multiple'); + expect(input).toHaveAttribute('accept', '.one,.onepkg,.cab'); + await user.upload(input, [ + new File([new Uint8Array([2])], 'Notebook-2.cab'), + new File([new Uint8Array([1])], 'Notebook.onepkg'), + ]); + + expect( + await screen.findByRole('heading', { name: 'Package summary' }) + ).toBeInTheDocument(); + expect(worker.lastMessage).toEqual( + expect.objectContaining({ + type: 'parse-onepkg', + fileName: 'Notebook.onepkg', + cabinetParts: [expect.objectContaining({ fileName: 'Notebook-2.cab' })], + }) + ); }); it('keeps a failed packaged section visible beside a readable one', async () => { @@ -299,7 +354,7 @@ describe('OneNoteApplication', () => { ); await user.upload( - screen.getByLabelText('Choose a OneNote file'), + screen.getByLabelText('Choose OneNote files'), new File([new Uint8Array([1])], 'Notebook.onepkg') ); @@ -336,7 +391,7 @@ describe('OneNoteApplication', () => { ); await user.upload( - screen.getByLabelText('Choose a OneNote file'), + screen.getByLabelText('Choose OneNote files'), new File(['not OneNote'], 'Broken.one') ); @@ -353,12 +408,12 @@ describe('OneNoteApplication', () => { render(); await user.upload( - screen.getByLabelText('Choose a OneNote file'), + screen.getByLabelText('Choose OneNote files'), new File(['not a notebook'], 'notes.zip') ); expect(await screen.findByRole('alert')).toHaveTextContent( - 'Choose a file whose name ends in .one or .onepkg.' + 'Choose one .one file, or one .onepkg file together with its .cab parts.' ); }); @@ -371,7 +426,7 @@ describe('OneNoteApplication', () => { value: MAX_SOURCE_FILE_BYTES + 1, }); - await user.upload(screen.getByLabelText('Choose a OneNote file'), file); + await user.upload(screen.getByLabelText('Choose OneNote files'), file); expect(await screen.findByRole('alert')).toHaveTextContent('512 MiB'); }); diff --git a/src/App.tsx b/src/App.tsx index 85f8d89..7f359d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,10 +20,7 @@ import type { OneNoteSectionDto, } from './onenote/model/dto.js'; import type { ParserDiagnostic } from './onenote/model/diagnostics.js'; -import { - MAX_SOURCE_FILE_BYTES, - sourceKindFromFileName, -} from './source-file.js'; +import { selectSourceFiles } from './source-file.js'; import { toolboxApp } from './toolbox/manifest.js'; import { OneNoteWorkerClient, @@ -174,21 +171,13 @@ export function OneNoteApplication({ } }; - const openFile = async (file: File) => { - const sourceKind = sourceKindFromFileName(file.name); - if (!sourceKind) { + const openFiles = async (files: File[]) => { + const selection = selectSourceFiles(files); + if (!selection.ok) { setState({ phase: 'error', - title: 'Unsupported file name', - message: 'Choose a file whose name ends in .one or .onepkg.', - }); - return; - } - if (file.size > MAX_SOURCE_FILE_BYTES) { - setState({ - phase: 'error', - title: 'File is too large', - message: 'The selected file exceeds the current 512 MiB safety limit.', + title: selection.title, + message: selection.message, }); return; } @@ -201,12 +190,24 @@ export function OneNoteApplication({ activeClient.current = client; setSelectedSectionId(undefined); setPageState({ phase: 'empty' }); - setState({ phase: 'reading', fileName: file.name }); + setState({ phase: 'reading', fileName: selection.source.name }); try { - const bytes = await file.arrayBuffer(); + const buffers = await Promise.all( + [selection.source, ...selection.cabinetParts].map((file) => + file.arrayBuffer() + ) + ); if (generation.current !== requestGeneration) return; - const response = await client.parseSource(sourceKind, file.name, bytes); + const response = await client.parseSource( + selection.kind, + selection.source.name, + buffers[0]!, + selection.cabinetParts.map((part, index) => ({ + fileName: part.name, + bytes: buffers[index + 1]!, + })) + ); if (generation.current !== requestGeneration) return; if (response.type === 'failure') { @@ -226,7 +227,7 @@ export function OneNoteApplication({ if (response.type === 'section') { setState({ phase: 'loaded', - fileName: file.name, + fileName: selection.source.name, source: { kind: 'one', sessionId: response.sessionId, @@ -253,7 +254,7 @@ export function OneNoteApplication({ setSelectedSectionId(preferredSection?.id); setState({ phase: 'loaded', - fileName: file.name, + fileName: selection.source.name, source: { kind: 'onepkg', sessionId: response.sessionId, @@ -373,7 +374,7 @@ export function OneNoteApplication({ return (
{state.phase === 'idle' ? ( - void openFile(file)} /> + void openFiles(files)} /> ) : null} {state.phase === 'reading' ? ( diff --git a/src/components/FileDropZone.tsx b/src/components/FileDropZone.tsx index d51f020..65bf345 100644 --- a/src/components/FileDropZone.tsx +++ b/src/components/FileDropZone.tsx @@ -2,25 +2,25 @@ import { useRef, useState, type ChangeEvent, type DragEvent } from 'react'; interface FileDropZoneProps { disabled?: boolean; - onFile(file: File): void; + onFiles(files: File[]): void; } -export function FileDropZone({ disabled = false, onFile }: FileDropZoneProps) { +export function FileDropZone({ disabled = false, onFiles }: FileDropZoneProps) { const input = useRef(null); const [dragging, setDragging] = useState(false); - const selectFile = (event: ChangeEvent) => { - const file = event.target.files?.[0]; + const selectFiles = (event: ChangeEvent) => { + const files = [...(event.target.files ?? [])]; event.target.value = ''; - if (file) onFile(file); + if (files.length > 0) onFiles(files); }; - const dropFile = (event: DragEvent) => { + const dropFiles = (event: DragEvent) => { event.preventDefault(); setDragging(false); if (disabled) return; - const file = event.dataTransfer.files[0]; - if (file) onFile(file); + const files = [...event.dataTransfer.files]; + if (files.length > 0) onFiles(files); }; return ( @@ -36,23 +36,24 @@ export function FileDropZone({ disabled = false, onFile }: FileDropZoneProps) { setDragging(false); } }} - onDrop={dropFile} + onDrop={dropFiles} data-testid="file-drop-zone" >

Private, local-first reader

Open a OneNote file

- Inspect a .one section or .onepkg notebook - package without sending it to a server. + Inspect a .one section, or a .onepkg notebook + with any linked .cab parts, without sending it to a server.

- or drop one .one or .onepkg file here + or drop one .one, or one .onepkg with its .cab parts, here ); diff --git a/src/source-file.test.ts b/src/source-file.test.ts new file mode 100644 index 0000000..8a26e4d --- /dev/null +++ b/src/source-file.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; + +import { + MAX_SOURCE_FILE_BYTES, + selectSourceFiles, + sourceKindFromFileName, +} from './source-file.js'; + +function source(name: string, size = 1) { + return { name, size }; +} + +describe('local source selection', () => { + it('accepts exactly one .one file', () => { + const section = source('Section.ONE'); + expect(sourceKindFromFileName(section.name)).toBe('one'); + expect(selectSourceFiles([section])).toEqual({ + ok: true, + kind: 'one', + source: section, + cabinetParts: [], + }); + }); + + it('finds one .onepkg and keeps CAB companions in selection order', () => { + const part3 = source('Notebook-3.CAB'); + const primary = source('Notebook.OnePkg'); + const part2 = source('Notebook-2.cab'); + expect(selectSourceFiles([part3, primary, part2])).toEqual({ + ok: true, + kind: 'onepkg', + source: primary, + cabinetParts: [part3, part2], + }); + }); + + it('rejects duplicate names, unsupported files, and ambiguous primaries', () => { + expect( + selectSourceFiles([ + source('Notebook.onepkg'), + source('Part.cab'), + source('PART.CAB'), + ]) + ).toMatchObject({ ok: false, title: 'Duplicate file name' }); + expect(selectSourceFiles([source('Notebook.zip')])).toMatchObject({ + ok: false, + title: 'Unsupported file name', + }); + expect( + selectSourceFiles([source('First.onepkg'), source('Second.onepkg')]) + ).toMatchObject({ ok: false, title: 'Choose one OneNote source' }); + expect( + selectSourceFiles([source('Section.one'), source('Notebook.onepkg')]) + ).toMatchObject({ ok: false, title: 'Choose one OneNote source' }); + expect(selectSourceFiles([source('Part.cab')])).toMatchObject({ + ok: false, + title: 'Choose one OneNote source', + }); + }); + + it('applies both per-file and aggregate 512 MiB limits', () => { + expect( + selectSourceFiles([source('Notebook.onepkg', MAX_SOURCE_FILE_BYTES + 1)]) + ).toMatchObject({ ok: false, title: 'File is too large' }); + expect( + selectSourceFiles([ + source('Notebook.onepkg', 300 * 1024 * 1024), + source('Notebook-2.cab', 300 * 1024 * 1024), + ]) + ).toMatchObject({ ok: false, title: 'Package set is too large' }); + }); +}); diff --git a/src/source-file.ts b/src/source-file.ts index aaaabfe..ffd9ca4 100644 --- a/src/source-file.ts +++ b/src/source-file.ts @@ -1,6 +1,33 @@ import type { SourceKind } from './worker/onenote.client.js'; export const MAX_SOURCE_FILE_BYTES = 512 * 1024 * 1024; +export const MAX_SOURCE_PARTS = 256; + +export interface SourceFileDescriptor { + name: string; + size: number; +} + +interface SourceFileSelectionFailure { + ok: false; + title: string; + message: string; +} + +export type SourceFileSelection = + | { + ok: true; + kind: 'one'; + source: T; + cabinetParts: []; + } + | { + ok: true; + kind: 'onepkg'; + source: T; + cabinetParts: T[]; + } + | SourceFileSelectionFailure; export function sourceKindFromFileName(fileName: string): SourceKind | null { const normalized = fileName.toLocaleLowerCase('en-US'); @@ -8,3 +35,101 @@ export function sourceKindFromFileName(fileName: string): SourceKind | null { if (normalized.endsWith('.one')) return 'one'; return null; } + +export function isCabinetPartFileName(fileName: string): boolean { + return fileName.toLocaleLowerCase('en-US').endsWith('.cab'); +} + +export function selectSourceFiles( + files: readonly T[] +): SourceFileSelection { + if (files.length === 0) { + return invalidSelection(); + } + if (files.length > MAX_SOURCE_PARTS) { + return { + ok: false, + title: 'Too many package parts', + message: `Choose at most ${MAX_SOURCE_PARTS} files for one cabinet set.`, + }; + } + + const sections: T[] = []; + const packages: T[] = []; + const cabinetParts: T[] = []; + for (const file of files) { + const kind = sourceKindFromFileName(file.name); + if (kind === 'one') sections.push(file); + else if (kind === 'onepkg') packages.push(file); + else if (isCabinetPartFileName(file.name)) cabinetParts.push(file); + else { + return { + ok: false, + title: 'Unsupported file name', + message: + 'Choose one .one file, or one .onepkg file together with its .cab parts.', + }; + } + } + + const validSection = + sections.length === 1 && packages.length === 0 && cabinetParts.length === 0; + const validPackage = sections.length === 0 && packages.length === 1; + if (!validSection && !validPackage) return invalidSelection(); + + const seenNames = new Set(); + let totalSize = 0; + for (const file of files) { + const nameKey = file.name.normalize('NFC').toLocaleLowerCase('en-US'); + if (seenNames.has(nameKey)) { + return { + ok: false, + title: 'Duplicate file name', + message: `The selection contains more than one file named ${file.name}.`, + }; + } + seenNames.add(nameKey); + if (!Number.isSafeInteger(file.size) || file.size < 0) { + return { + ok: false, + title: 'Invalid file size', + message: `The browser reported an invalid size for ${file.name}.`, + }; + } + if (file.size > MAX_SOURCE_FILE_BYTES) { + return { + ok: false, + title: 'File is too large', + message: `${file.name} exceeds the current 512 MiB safety limit.`, + }; + } + totalSize += file.size; + if (!Number.isSafeInteger(totalSize) || totalSize > MAX_SOURCE_FILE_BYTES) { + return { + ok: false, + title: 'Package set is too large', + message: + 'The selected .onepkg and .cab files exceed the combined 512 MiB safety limit.', + }; + } + } + + if (validSection) { + return { ok: true, kind: 'one', source: sections[0]!, cabinetParts: [] }; + } + return { + ok: true, + kind: 'onepkg', + source: packages[0]!, + cabinetParts, + }; +} + +function invalidSelection(): SourceFileSelectionFailure { + return { + ok: false, + title: 'Choose one OneNote source', + message: + 'Choose one .one file, or one .onepkg file together with its .cab parts.', + }; +} diff --git a/src/worker/onenote.client.test.ts b/src/worker/onenote.client.test.ts index 6f3abde..6900068 100644 --- a/src/worker/onenote.client.test.ts +++ b/src/worker/onenote.client.test.ts @@ -29,20 +29,29 @@ class FakeWorker implements WorkerPort { } describe('OneNoteWorkerClient', () => { - it('uses the typed parse-onepkg protocol and transfers the source buffer', async () => { + it('transfers a ONEPKG and every named CAB companion', async () => { const worker = new FakeWorker(); const client = new OneNoteWorkerClient(() => worker); const bytes = new ArrayBuffer(16); - const result = client.parseSource('onepkg', 'Notebook.onepkg', bytes); + const second = new ArrayBuffer(8); + const third = new ArrayBuffer(4); + const result = client.parseSource('onepkg', 'Notebook.onepkg', bytes, [ + { fileName: 'part-3.cab', bytes: third }, + { fileName: 'part-2.cab', bytes: second }, + ]); expect(worker.lastMessage).toEqual( expect.objectContaining({ type: 'parse-onepkg', fileName: 'Notebook.onepkg', bytes, + cabinetParts: [ + { fileName: 'part-3.cab', bytes: third }, + { fileName: 'part-2.cab', bytes: second }, + ], }) ); - expect(worker.lastTransfer).toEqual([bytes]); + expect(worker.lastTransfer).toEqual([bytes, third, second]); const requestId = worker.lastMessage && 'requestId' in worker.lastMessage @@ -143,7 +152,16 @@ describe('OneNoteWorkerClient', () => { it('rejects outstanding work when the worker is terminated', async () => { const worker = new FakeWorker(); const client = new OneNoteWorkerClient(() => worker); - const result = client.parseSource('one', 'Section.one', new ArrayBuffer(8)); + const bytes = new ArrayBuffer(8); + const result = client.parseSource('one', 'Section.one', bytes); + + expect(worker.lastMessage).toEqual({ + type: 'parse-one', + requestId: 'parse-1', + fileName: 'Section.one', + bytes, + }); + expect(worker.lastTransfer).toEqual([bytes]); client.terminate(); diff --git a/src/worker/onenote.client.ts b/src/worker/onenote.client.ts index 3235f1c..7fb9f88 100644 --- a/src/worker/onenote.client.ts +++ b/src/worker/onenote.client.ts @@ -4,6 +4,7 @@ import type { ParseWorkerResponse, ResourceWorkerResponse, WorkerRequest, + WorkerCabinetPart, WorkerFailure, WorkerResponse, } from './worker-protocol.js'; @@ -68,7 +69,8 @@ export class OneNoteWorkerClient { parseSource( kind: SourceKind, fileName: string, - bytes: ArrayBuffer + bytes: ArrayBuffer, + cabinetParts: readonly WorkerCabinetPart[] = [] ): Promise { if (this.terminated) { return Promise.reject( @@ -78,6 +80,14 @@ export class OneNoteWorkerClient { ) ); } + 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 = { @@ -85,6 +95,9 @@ export class OneNoteWorkerClient { requestId, fileName, bytes, + ...(kind === 'onepkg' && cabinetParts.length > 0 + ? { cabinetParts: [...cabinetParts] } + : {}), }; return new Promise((resolve, reject) => { @@ -92,7 +105,10 @@ export class OneNoteWorkerClient { resolve: (response) => resolve(response as ParseWorkerResponse), reject, }); - this.worker.postMessage(request, [bytes]); + this.worker.postMessage(request, [ + bytes, + ...cabinetParts.map((part) => part.bytes), + ]); }); } diff --git a/src/worker/onenote.worker.ts b/src/worker/onenote.worker.ts index 1aa51a0..601b17c 100644 --- a/src/worker/onenote.worker.ts +++ b/src/worker/onenote.worker.ts @@ -3,6 +3,7 @@ import type { OneNoteResource } from '../onenote/one/content-model.js'; import { failureFromUnknown, openPackageForWorker, + openPackageSetForWorker, openSectionForWorker, resourcePayload, workerPageKey, @@ -59,10 +60,25 @@ async function handleRequest(data: WorkerRequest): Promise { } case 'parse-onepkg': try { - const parsed = await openPackageForWorker( - new Uint8Array(data.bytes), - data.fileName - ); + const parsed = + data.cabinetParts && data.cabinetParts.length > 0 + ? await openPackageSetForWorker( + [ + { + fileName: data.fileName, + bytes: new Uint8Array(data.bytes), + }, + ...data.cabinetParts.map((part) => ({ + fileName: part.fileName, + bytes: new Uint8Array(part.bytes), + })), + ], + data.fileName + ) + : await openPackageForWorker( + new Uint8Array(data.bytes), + data.fileName + ); const sessionId = createSession(parsed.pages, parsed.resources); workerScope.postMessage({ type: 'package', diff --git a/src/worker/parser-adapter.test.ts b/src/worker/parser-adapter.test.ts index 7b86057..7a02dfb 100644 --- a/src/worker/parser-adapter.test.ts +++ b/src/worker/parser-adapter.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'; import { failureFromUnknown, openPackageForWorker, + openPackageSetForWorker, openSectionForWorker, resourcePayload, workerPageKey, @@ -16,6 +17,15 @@ function fixtureBytes(path: string): Uint8Array { return Uint8Array.from(Buffer.from(encoded.replaceAll(/\s/gu, ''), 'base64')); } +function multiCabPart(index: number) { + return { + fileName: `cabd_multi_basic_pt${index}.cab`, + bytes: fixtureBytes( + `../../tests/fixtures/libmspack-multi-basic-pt${index}.cab.b64` + ), + }; +} + describe('worker parser adapter', () => { it('opens a real section and retains stable page identifiers', () => { const result = openSectionForWorker( @@ -83,6 +93,24 @@ describe('worker parser adapter', () => { expect(result.resources).toBeInstanceOf(Map); }); + it('opens named multi-cabinet package parts in arbitrary input order', async () => { + const parts = [4, 1, 5, 2, 3].map(multiCabPart); + const result = await openPackageSetForWorker(parts, 'Notebook.onepkg'); + + expect(result.notebook).toMatchObject({ + sourceName: 'Notebook.onepkg', + sourceSize: parts.reduce((sum, part) => sum + part.bytes.length, 0), + entries: [ + { path: 'test1.txt', kind: 'other' }, + { path: 'test2.txt', kind: 'other' }, + { path: 'test3.txt', kind: 'other' }, + ], + sections: [], + }); + expect(result.pages).toHaveLength(0); + expect(result.resources).toHaveLength(0); + }); + it('converts malformed section input into a controlled worker failure', () => { try { openSectionForWorker(new Uint8Array(20), 'broken.one'); diff --git a/src/worker/parser-adapter.ts b/src/worker/parser-adapter.ts index a81eaf0..c88e5f6 100644 --- a/src/worker/parser-adapter.ts +++ b/src/worker/parser-adapter.ts @@ -15,7 +15,13 @@ import type { OneNoteResource, OneNoteSectionContentModel, } from '../onenote/one/content-model.js'; -import { OnePkgError, openOneNotePackage } from '../onenote/onepkg/index.js'; +import { + OnePkgError, + openOneNotePackage, + openOneNotePackageSet, + type CabSetPart, + type OneNotePackageOpenOptions, +} from '../onenote/onepkg/index.js'; import type { WorkerFailure } from './worker-protocol.js'; export interface ParsedWorkerSection { @@ -135,12 +141,28 @@ export function openSectionForWorker( export async function openPackageForWorker( bytes: Uint8Array, sourceName: string +): Promise { + return openPackageSourceForWorker({ kind: 'single', bytes }, sourceName); +} + +export async function openPackageSetForWorker( + parts: CabSetPart[], + sourceName: string +): Promise { + return openPackageSourceForWorker({ kind: 'set', parts }, sourceName); +} + +async function openPackageSourceForWorker( + source: + | { kind: 'single'; bytes: Uint8Array } + | { kind: 'set'; parts: CabSetPart[] }, + sourceName: string ): Promise { const contentBySection = new Map(); - const result = await openOneNotePackage(bytes, { + const options: OneNotePackageOpenOptions = { sourceName, cancellation: { - yield: () => new Promise((resolve) => setTimeout(resolve, 0)), + yield: () => new Promise((resolve) => setTimeout(resolve, 0)), }, parseSection: ({ sourceName: sectionName, bytes: sectionBytes }) => { try { @@ -174,7 +196,11 @@ export async function openPackageForWorker( throw error; } }, - }); + }; + const result = + source.kind === 'single' + ? await openOneNotePackage(source.bytes, options) + : await openOneNotePackageSet(source.parts, options); const pages = new Map(); const resources = new Map(); diff --git a/src/worker/worker-protocol.ts b/src/worker/worker-protocol.ts index 08060f6..2f8339c 100644 --- a/src/worker/worker-protocol.ts +++ b/src/worker/worker-protocol.ts @@ -5,6 +5,11 @@ import type { OneNoteSectionDto, } from '../onenote/model/dto.js'; +export interface WorkerCabinetPart { + fileName: string; + bytes: ArrayBuffer; +} + export type WorkerRequest = | { type: 'parse-one'; @@ -17,6 +22,7 @@ export type WorkerRequest = requestId: string; fileName: string; bytes: ArrayBuffer; + cabinetParts?: WorkerCabinetPart[]; } | { type: 'get-page';