aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/persistence/compressionWorker.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/persistence/compressionWorker.ts')
-rw-r--r--subprojects/frontend/src/persistence/compressionWorker.ts98
1 files changed, 98 insertions, 0 deletions
diff --git a/subprojects/frontend/src/persistence/compressionWorker.ts b/subprojects/frontend/src/persistence/compressionWorker.ts
new file mode 100644
index 00000000..7b93b20b
--- /dev/null
+++ b/subprojects/frontend/src/persistence/compressionWorker.ts
@@ -0,0 +1,98 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import type { Zstd } from '@hpcc-js/wasm';
8// We need to use a deep import for proper code splitting with `vite-plugin-pwa`.
9// @ts-expect-error Typescript doesn't find the declarations for the deep import.
10import { Zstd as zstdLoader } from '@hpcc-js/wasm/zstd';
11
12import type {
13 CompressResponse,
14 CompressorRequest,
15 DecompressResponse,
16 ErrorResponse,
17} from './compressionMessages';
18
19const CONTENT_TYPE = 'application/octet-stream';
20
21const URI_PREFIX = `data:${CONTENT_TYPE};base64,`;
22
23async function base64Encode(buffer: Uint8Array): Promise<string> {
24 const uri = await new Promise((resolve, reject) => {
25 const reader = new FileReader();
26 reader.onload = () => resolve(reader.result);
27 reader.onerror = () => reject(reader.error);
28 reader.readAsDataURL(new File([buffer], '', { type: CONTENT_TYPE }));
29 });
30 if (typeof uri !== 'string') {
31 throw new Error(`Unexpected FileReader result type: ${typeof uri}`);
32 }
33 const base64 = uri.substring(URI_PREFIX.length);
34 return base64.replace(/[+/]/g, (c) => {
35 if (c === '+') {
36 return '-';
37 }
38 if (c === '/') {
39 return '_';
40 }
41 return c;
42 });
43}
44
45async function base64Decode(compressedText: string): Promise<Uint8Array> {
46 const base64 = compressedText.replace(/[-_]/g, (c) => {
47 if (c === '-') {
48 return '+';
49 }
50 if (c === '_') {
51 return '/';
52 }
53 return c;
54 });
55 const result = await fetch(`${URI_PREFIX}${base64}`);
56 return new Uint8Array(await result.arrayBuffer());
57}
58
59let zstd: Awaited<ReturnType<typeof Zstd.load>> | undefined;
60
61globalThis.onmessage = (event) => {
62 (async () => {
63 if (zstd === undefined) {
64 // Since we don't have types for the deep import, we have to cast here.
65 zstd = await (zstdLoader as { load: typeof Zstd.load }).load();
66 }
67 // Since the render thread will only send us valid messages,
68 // we can save a bit of bundle size by using a cast instead of `parse`
69 // to avoid having to include `zod` in the worker.
70 const message = event.data as CompressorRequest;
71 if (message.request === 'compress') {
72 const encoder = new TextEncoder();
73 const encodedBuffer = encoder.encode(message.text);
74 const compressedBuffer = zstd.compress(encodedBuffer, 3);
75 const compressedText = await base64Encode(compressedBuffer);
76 globalThis.postMessage({
77 response: 'compressed',
78 compressedText,
79 } satisfies CompressResponse);
80 } else if (message.request === 'decompress') {
81 const decodedBuffer = await base64Decode(message.compressedText);
82 const uncompressedBuffer = zstd.decompress(decodedBuffer);
83 const decoder = new TextDecoder();
84 const text = decoder.decode(uncompressedBuffer);
85 globalThis.postMessage({
86 response: 'decompressed',
87 text,
88 } satisfies DecompressResponse);
89 } else {
90 throw new Error(`Unknown request: ${JSON.stringify(event.data)}`);
91 }
92 })().catch((error) => {
93 globalThis.postMessage({
94 response: 'error',
95 message: String(error),
96 } satisfies ErrorResponse);
97 });
98};