feat: export parser sessions from the browser
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { OneNoteApplication } from './App.js';
|
import { OneNoteApplication } from './App.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -105,6 +105,17 @@ class SuccessfulParserWorker implements WorkerPort {
|
|||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if (message.type === 'export-session') {
|
||||||
|
this.respond({
|
||||||
|
type: 'export-artifact',
|
||||||
|
requestId: message.requestId,
|
||||||
|
sessionId: message.sessionId,
|
||||||
|
artifact: {
|
||||||
|
filename: 'Project notes.zip',
|
||||||
|
mediaType: 'application/zip',
|
||||||
|
bytes: Uint8Array.of(0x50, 0x4b, 3, 4).buffer,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -183,6 +194,43 @@ describe('OneNoteApplication', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('downloads a worker-built export after explicit user action', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const worker = new SuccessfulParserWorker();
|
||||||
|
const createObjectUrl = vi
|
||||||
|
.spyOn(URL, 'createObjectURL')
|
||||||
|
.mockReturnValue('blob:local-export');
|
||||||
|
const click = vi
|
||||||
|
.spyOn(HTMLAnchorElement.prototype, 'click')
|
||||||
|
.mockImplementation(() => undefined);
|
||||||
|
render(
|
||||||
|
<OneNoteApplication
|
||||||
|
createWorkerClient={() => new OneNoteWorkerClient(() => worker)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.upload(
|
||||||
|
screen.getByLabelText('Choose OneNote files'),
|
||||||
|
new File([new Uint8Array([1, 2, 3])], 'Section.one')
|
||||||
|
);
|
||||||
|
await screen.findByRole('heading', { name: 'First meeting' });
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create export' }));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByText('The export download is ready.')
|
||||||
|
).toBeVisible();
|
||||||
|
expect(worker.lastMessage).toEqual({
|
||||||
|
type: 'export-session',
|
||||||
|
requestId: 'export-3',
|
||||||
|
sessionId: 'session-one',
|
||||||
|
format: 'static-zip',
|
||||||
|
});
|
||||||
|
expect(createObjectUrl).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: 'application/zip' })
|
||||||
|
);
|
||||||
|
expect(click).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it('opens a dropped OneNote package through the local worker', async () => {
|
it('opens a dropped OneNote package through the local worker', async () => {
|
||||||
const notebook: OneNotePackageDto = {
|
const notebook: OneNotePackageDto = {
|
||||||
format: 'onepkg',
|
format: 'onepkg',
|
||||||
|
|||||||
62
src/App.tsx
62
src/App.tsx
@@ -3,6 +3,10 @@ import { AppShell } from '@add-ideas/toolbox-shell-react';
|
|||||||
import '@add-ideas/toolbox-shell-react/styles.css';
|
import '@add-ideas/toolbox-shell-react/styles.css';
|
||||||
|
|
||||||
import { DiagnosticsPanel } from './components/DiagnosticsPanel.js';
|
import { DiagnosticsPanel } from './components/DiagnosticsPanel.js';
|
||||||
|
import {
|
||||||
|
ExportControls,
|
||||||
|
type OneNoteExportFormat,
|
||||||
|
} from './components/ExportControls.js';
|
||||||
import { FileDropZone } from './components/FileDropZone.js';
|
import { FileDropZone } from './components/FileDropZone.js';
|
||||||
import { HelpDialog } from './components/HelpDialog.js';
|
import { HelpDialog } from './components/HelpDialog.js';
|
||||||
import { PackageInspector } from './components/PackageInspector.js';
|
import { PackageInspector } from './components/PackageInspector.js';
|
||||||
@@ -337,21 +341,39 @@ export function OneNoteApplication({
|
|||||||
suggestedName ??
|
suggestedName ??
|
||||||
`onenote-resource.${resource.extension ?? 'bin'}`
|
`onenote-resource.${resource.extension ?? 'bin'}`
|
||||||
);
|
);
|
||||||
const url = URL.createObjectURL(
|
downloadBrowserFile(resource.bytes, resource.mediaType, filename);
|
||||||
new Blob([resource.bytes], { type: resource.mediaType })
|
|
||||||
);
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = filename;
|
|
||||||
anchor.rel = 'noopener';
|
|
||||||
document.body.append(anchor);
|
|
||||||
anchor.click();
|
|
||||||
anchor.remove();
|
|
||||||
window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
|
|
||||||
},
|
},
|
||||||
[loadSelectedResource]
|
[loadSelectedResource]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const exportLoadedSource = useCallback(
|
||||||
|
async (format: OneNoteExportFormat): Promise<void> => {
|
||||||
|
const client = activeClient.current;
|
||||||
|
if (!client || state.phase !== 'loaded') {
|
||||||
|
throw new WorkerClientError(
|
||||||
|
'session-not-available',
|
||||||
|
'The local parser session is no longer available.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const response = await client.exportSession(
|
||||||
|
state.source.sessionId,
|
||||||
|
format
|
||||||
|
);
|
||||||
|
if (response.type === 'failure') {
|
||||||
|
throw new WorkerClientError(
|
||||||
|
response.error.code,
|
||||||
|
response.error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
downloadBrowserFile(
|
||||||
|
response.artifact.bytes,
|
||||||
|
response.artifact.mediaType,
|
||||||
|
safeDownloadFilename(response.artifact.filename)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[state]
|
||||||
|
);
|
||||||
|
|
||||||
const visibleDiagnostics = uniqueDiagnostics(
|
const visibleDiagnostics = uniqueDiagnostics(
|
||||||
(() => {
|
(() => {
|
||||||
if (state.phase !== 'loaded') return [];
|
if (state.phase !== 'loaded') return [];
|
||||||
@@ -409,6 +431,8 @@ export function OneNoteApplication({
|
|||||||
<PackageInspector notebook={state.source.notebook} />
|
<PackageInspector notebook={state.source.notebook} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<ExportControls onExport={exportLoadedSource} />
|
||||||
|
|
||||||
<div className="reader-workspace">
|
<div className="reader-workspace">
|
||||||
{state.source.kind === 'one' ? (
|
{state.source.kind === 'one' ? (
|
||||||
<SingleSectionNavigation
|
<SingleSectionNavigation
|
||||||
@@ -486,6 +510,22 @@ function safeDownloadFilename(value: string): string {
|
|||||||
return (cleaned || 'onenote-resource.bin').slice(0, 180);
|
return (cleaned || 'onenote-resource.bin').slice(0, 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadBrowserFile(
|
||||||
|
bytes: ArrayBuffer,
|
||||||
|
mediaType: string,
|
||||||
|
filename: string
|
||||||
|
): void {
|
||||||
|
const url = URL.createObjectURL(new Blob([bytes], { type: mediaType }));
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = filename;
|
||||||
|
anchor.rel = 'noopener';
|
||||||
|
document.body.append(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
|
||||||
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [helpOpen, setHelpOpen] = useState(false);
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ p {
|
|||||||
.status-card,
|
.status-card,
|
||||||
.source-toolbar,
|
.source-toolbar,
|
||||||
.package-inspector,
|
.package-inspector,
|
||||||
|
.export-controls,
|
||||||
.diagnostics {
|
.diagnostics {
|
||||||
border: 1px solid #c9b9d5;
|
border: 1px solid #c9b9d5;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@@ -82,6 +83,60 @@ p {
|
|||||||
box-shadow: 0 0.75rem 2.5rem rgb(62 31 85 / 8%);
|
box-shadow: 0 0.75rem 2.5rem rgb(62 31 85 / 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(17rem, auto);
|
||||||
|
align-items: end;
|
||||||
|
gap: 0.7rem 1.5rem;
|
||||||
|
padding: 1.15rem 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls h2 {
|
||||||
|
margin: 0.15rem 0 0;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls > div > p:last-child {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
color: #716675;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls__actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(13rem, 1fr) auto;
|
||||||
|
gap: 0.35rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls__actions label {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
color: #574b5b;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls__actions select {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #a98ab9;
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
padding: 0.55rem 2rem 0.55rem 0.7rem;
|
||||||
|
color: inherit;
|
||||||
|
background: #fff;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls__status {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-height: 1.2em;
|
||||||
|
margin: 0;
|
||||||
|
color: #716675;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls__status--error {
|
||||||
|
color: #9b3131;
|
||||||
|
}
|
||||||
|
|
||||||
.drop-zone,
|
.drop-zone,
|
||||||
.status-card {
|
.status-card {
|
||||||
padding: clamp(1.5rem, 4vw, 3rem);
|
padding: clamp(1.5rem, 4vw, 3rem);
|
||||||
@@ -706,6 +761,10 @@ p {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.entry-table {
|
.entry-table {
|
||||||
min-width: 42rem;
|
min-width: 42rem;
|
||||||
}
|
}
|
||||||
@@ -722,6 +781,7 @@ p {
|
|||||||
.source-toolbar,
|
.source-toolbar,
|
||||||
.package-inspector,
|
.package-inspector,
|
||||||
.diagnostics,
|
.diagnostics,
|
||||||
|
.export-controls,
|
||||||
.reader-workspace,
|
.reader-workspace,
|
||||||
.page-reader,
|
.page-reader,
|
||||||
.help-dialog,
|
.help-dialog,
|
||||||
@@ -748,6 +808,21 @@ p {
|
|||||||
background: #35283d;
|
background: #35283d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-controls__actions select {
|
||||||
|
border-color: #755e82;
|
||||||
|
background: #35283d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls > div > p:last-child,
|
||||||
|
.export-controls__actions label,
|
||||||
|
.export-controls__status {
|
||||||
|
color: #cfc2d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-controls__status--error {
|
||||||
|
color: #ffb3b3;
|
||||||
|
}
|
||||||
|
|
||||||
code,
|
code,
|
||||||
.privacy-note,
|
.privacy-note,
|
||||||
.package-summary > div {
|
.package-summary > div {
|
||||||
|
|||||||
@@ -149,6 +149,39 @@ describe('OneNoteWorkerClient', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requests an export from the retained parser session', async () => {
|
||||||
|
const worker = new FakeWorker();
|
||||||
|
const client = new OneNoteWorkerClient(() => worker);
|
||||||
|
const result = client.exportSession('session-1', 'static-zip');
|
||||||
|
|
||||||
|
expect(worker.lastMessage).toEqual({
|
||||||
|
type: 'export-session',
|
||||||
|
requestId: 'export-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
format: 'static-zip',
|
||||||
|
});
|
||||||
|
expect(worker.lastTransfer).toEqual([]);
|
||||||
|
|
||||||
|
const bytes = Uint8Array.of(0x50, 0x4b, 3, 4).buffer;
|
||||||
|
worker.respond({
|
||||||
|
type: 'export-artifact',
|
||||||
|
requestId: 'export-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
artifact: {
|
||||||
|
filename: 'Notebook.zip',
|
||||||
|
mediaType: 'application/zip',
|
||||||
|
bytes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(result).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'export-artifact',
|
||||||
|
artifact: expect.objectContaining({ bytes }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects outstanding work when the worker is terminated', async () => {
|
it('rejects outstanding work when the worker is terminated', async () => {
|
||||||
const worker = new FakeWorker();
|
const worker = new FakeWorker();
|
||||||
const client = new OneNoteWorkerClient(() => worker);
|
const client = new OneNoteWorkerClient(() => worker);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type {
|
import type {
|
||||||
|
ExportWorkerResponse,
|
||||||
PageWorkerResponse,
|
PageWorkerResponse,
|
||||||
ParseWorkerRequest,
|
ParseWorkerRequest,
|
||||||
ParseWorkerResponse,
|
ParseWorkerResponse,
|
||||||
ResourceWorkerResponse,
|
ResourceWorkerResponse,
|
||||||
WorkerRequest,
|
WorkerRequest,
|
||||||
WorkerCabinetPart,
|
WorkerCabinetPart,
|
||||||
|
WorkerExportFormat,
|
||||||
WorkerFailure,
|
WorkerFailure,
|
||||||
WorkerResponse,
|
WorkerResponse,
|
||||||
} from './worker-protocol.js';
|
} from './worker-protocol.js';
|
||||||
@@ -20,7 +22,11 @@ export interface WorkerPort {
|
|||||||
|
|
||||||
type PendingRequest = {
|
type PendingRequest = {
|
||||||
resolve: (
|
resolve: (
|
||||||
response: ParseWorkerResponse | PageWorkerResponse | ResourceWorkerResponse
|
response:
|
||||||
|
| ParseWorkerResponse
|
||||||
|
| PageWorkerResponse
|
||||||
|
| ResourceWorkerResponse
|
||||||
|
| ExportWorkerResponse
|
||||||
) => void;
|
) => void;
|
||||||
reject: (error: WorkerClientError) => void;
|
reject: (error: WorkerClientError) => void;
|
||||||
};
|
};
|
||||||
@@ -172,6 +178,34 @@ export class OneNoteWorkerClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
terminate(): void {
|
||||||
if (this.terminated) return;
|
if (this.terminated) return;
|
||||||
this.terminated = true;
|
this.terminated = true;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { OneNotePageDto } from '../onenote/model/dto.js';
|
import { OneNoteExportError } from '../onenote/export/index.js';
|
||||||
|
import type {
|
||||||
|
OneNotePackageDto,
|
||||||
|
OneNotePageDto,
|
||||||
|
OneNoteSectionDto,
|
||||||
|
} from '../onenote/model/dto.js';
|
||||||
import type { OneNoteResource } from '../onenote/one/content-model.js';
|
import type { OneNoteResource } from '../onenote/one/content-model.js';
|
||||||
|
import { createWorkerExportArtifact } from './export-adapter.js';
|
||||||
import {
|
import {
|
||||||
failureFromUnknown,
|
failureFromUnknown,
|
||||||
openPackageForWorker,
|
openPackageForWorker,
|
||||||
@@ -9,7 +15,11 @@ import {
|
|||||||
workerPageKey,
|
workerPageKey,
|
||||||
workerResourceKey,
|
workerResourceKey,
|
||||||
} from './parser-adapter.js';
|
} from './parser-adapter.js';
|
||||||
import type { WorkerRequest, WorkerResponse } from './worker-protocol.js';
|
import type {
|
||||||
|
WorkerFailure,
|
||||||
|
WorkerRequest,
|
||||||
|
WorkerResponse,
|
||||||
|
} from './worker-protocol.js';
|
||||||
|
|
||||||
interface ModuleWorkerScope {
|
interface ModuleWorkerScope {
|
||||||
onmessage: ((event: MessageEvent<WorkerRequest>) => void) | null;
|
onmessage: ((event: MessageEvent<WorkerRequest>) => void) | null;
|
||||||
@@ -17,6 +27,7 @@ interface ModuleWorkerScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ParserSession {
|
interface ParserSession {
|
||||||
|
source: OneNoteSectionDto | OneNotePackageDto;
|
||||||
pages: Map<string, OneNotePageDto>;
|
pages: Map<string, OneNotePageDto>;
|
||||||
resources: Map<string, OneNoteResource>;
|
resources: Map<string, OneNoteResource>;
|
||||||
}
|
}
|
||||||
@@ -26,11 +37,12 @@ const sessions = new Map<string, ParserSession>();
|
|||||||
let sessionSequence = 0;
|
let sessionSequence = 0;
|
||||||
|
|
||||||
function createSession(
|
function createSession(
|
||||||
|
source: OneNoteSectionDto | OneNotePackageDto,
|
||||||
pages: Map<string, OneNotePageDto>,
|
pages: Map<string, OneNotePageDto>,
|
||||||
resources: Map<string, OneNoteResource>
|
resources: Map<string, OneNoteResource>
|
||||||
): string {
|
): string {
|
||||||
const id = `session-${++sessionSequence}`;
|
const id = `session-${++sessionSequence}`;
|
||||||
sessions.set(id, { pages, resources });
|
sessions.set(id, { source, pages, resources });
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +54,11 @@ async function handleRequest(data: WorkerRequest): Promise<void> {
|
|||||||
new Uint8Array(data.bytes),
|
new Uint8Array(data.bytes),
|
||||||
data.fileName
|
data.fileName
|
||||||
);
|
);
|
||||||
const sessionId = createSession(parsed.pages, parsed.resources);
|
const sessionId = createSession(
|
||||||
|
parsed.section,
|
||||||
|
parsed.pages,
|
||||||
|
parsed.resources
|
||||||
|
);
|
||||||
workerScope.postMessage({
|
workerScope.postMessage({
|
||||||
type: 'section',
|
type: 'section',
|
||||||
requestId: data.requestId,
|
requestId: data.requestId,
|
||||||
@@ -79,7 +95,11 @@ async function handleRequest(data: WorkerRequest): Promise<void> {
|
|||||||
new Uint8Array(data.bytes),
|
new Uint8Array(data.bytes),
|
||||||
data.fileName
|
data.fileName
|
||||||
);
|
);
|
||||||
const sessionId = createSession(parsed.pages, parsed.resources);
|
const sessionId = createSession(
|
||||||
|
parsed.notebook,
|
||||||
|
parsed.pages,
|
||||||
|
parsed.resources
|
||||||
|
);
|
||||||
workerScope.postMessage({
|
workerScope.postMessage({
|
||||||
type: 'package',
|
type: 'package',
|
||||||
requestId: data.requestId,
|
requestId: data.requestId,
|
||||||
@@ -172,6 +192,45 @@ async function handleRequest(data: WorkerRequest): Promise<void> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'export-session': {
|
||||||
|
const session = sessions.get(data.sessionId);
|
||||||
|
if (!session) {
|
||||||
|
workerScope.postMessage({
|
||||||
|
type: 'failure',
|
||||||
|
requestId: data.requestId,
|
||||||
|
error: {
|
||||||
|
code: 'session-not-available',
|
||||||
|
message: 'The parser session is no longer available.',
|
||||||
|
recoverable: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const artifact = createWorkerExportArtifact(
|
||||||
|
session.source,
|
||||||
|
session.pages,
|
||||||
|
session.resources,
|
||||||
|
data.format
|
||||||
|
);
|
||||||
|
workerScope.postMessage(
|
||||||
|
{
|
||||||
|
type: 'export-artifact',
|
||||||
|
requestId: data.requestId,
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
artifact,
|
||||||
|
},
|
||||||
|
[artifact.bytes]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
workerScope.postMessage({
|
||||||
|
type: 'failure',
|
||||||
|
requestId: data.requestId,
|
||||||
|
error: exportFailureFromUnknown(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'dispose':
|
case 'dispose':
|
||||||
sessions.delete(data.sessionId);
|
sessions.delete(data.sessionId);
|
||||||
workerScope.postMessage({
|
workerScope.postMessage({
|
||||||
@@ -181,6 +240,27 @@ async function handleRequest(data: WorkerRequest): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportFailureFromUnknown(error: unknown): WorkerFailure {
|
||||||
|
if (error instanceof OneNoteExportError) {
|
||||||
|
return {
|
||||||
|
code:
|
||||||
|
error.code === 'limit-exceeded' ? 'limit-exceeded' : 'export-failed',
|
||||||
|
message: error.message,
|
||||||
|
recoverable: true,
|
||||||
|
structure: 'OneNote export',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: 'export-failed',
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'The local notebook export could not be created.',
|
||||||
|
recoverable: true,
|
||||||
|
structure: 'OneNote export',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
workerScope.onmessage = ({ data }) => {
|
workerScope.onmessage = ({ data }) => {
|
||||||
void handleRequest(data);
|
void handleRequest(data);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ export interface WorkerCabinetPart {
|
|||||||
bytes: ArrayBuffer;
|
bytes: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerExportFormat =
|
||||||
|
'static-zip' | 'json' | 'text' | 'markdown' | 'html';
|
||||||
|
|
||||||
|
export interface WorkerExportArtifact {
|
||||||
|
filename: string;
|
||||||
|
mediaType: string;
|
||||||
|
bytes: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerRequest =
|
export type WorkerRequest =
|
||||||
| {
|
| {
|
||||||
type: 'parse-one';
|
type: 'parse-one';
|
||||||
@@ -38,6 +47,12 @@ export type WorkerRequest =
|
|||||||
resourceId: string;
|
resourceId: string;
|
||||||
sectionId?: string;
|
sectionId?: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'export-session';
|
||||||
|
requestId: string;
|
||||||
|
sessionId: string;
|
||||||
|
format: WorkerExportFormat;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'dispose';
|
type: 'dispose';
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -50,6 +65,7 @@ export interface WorkerFailure {
|
|||||||
| 'limit-exceeded'
|
| 'limit-exceeded'
|
||||||
| 'page-not-available'
|
| 'page-not-available'
|
||||||
| 'resource-not-available'
|
| 'resource-not-available'
|
||||||
|
| 'export-failed'
|
||||||
| 'session-not-available'
|
| 'session-not-available'
|
||||||
| 'worker-failure';
|
| 'worker-failure';
|
||||||
message: string;
|
message: string;
|
||||||
@@ -83,6 +99,12 @@ export type WorkerResponse =
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
resource: OneNoteResourcePayloadDto;
|
resource: OneNoteResourcePayloadDto;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'export-artifact';
|
||||||
|
requestId: string;
|
||||||
|
sessionId: string;
|
||||||
|
artifact: WorkerExportArtifact;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'failure';
|
type: 'failure';
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@@ -111,3 +133,8 @@ export type ResourceWorkerResponse = Extract<
|
|||||||
WorkerResponse,
|
WorkerResponse,
|
||||||
{ type: 'resource' | 'failure' }
|
{ type: 'resource' | 'failure' }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ExportWorkerResponse = Extract<
|
||||||
|
WorkerResponse,
|
||||||
|
{ type: 'export-artifact' | 'failure' }
|
||||||
|
>;
|
||||||
|
|||||||
Reference in New Issue
Block a user