aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/persistence/compressionWorker.ts
blob: 7b93b20bc6ec6d9764532512c1720306875742a8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/*
 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
 *
 * SPDX-License-Identifier: EPL-2.0
 */

import type { Zstd } from '@hpcc-js/wasm';
// We need to use a deep import for proper code splitting with `vite-plugin-pwa`.
// @ts-expect-error Typescript doesn't find the declarations for the deep import.
import { Zstd as zstdLoader } from '@hpcc-js/wasm/zstd';

import type {
  CompressResponse,
  CompressorRequest,
  DecompressResponse,
  ErrorResponse,
} from './compressionMessages';

const CONTENT_TYPE = 'application/octet-stream';

const URI_PREFIX = `data:${CONTENT_TYPE};base64,`;

async function base64Encode(buffer: Uint8Array): Promise<string> {
  const uri = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(new File([buffer], '', { type: CONTENT_TYPE }));
  });
  if (typeof uri !== 'string') {
    throw new Error(`Unexpected FileReader result type: ${typeof uri}`);
  }
  const base64 = uri.substring(URI_PREFIX.length);
  return base64.replace(/[+/]/g, (c) => {
    if (c === '+') {
      return '-';
    }
    if (c === '/') {
      return '_';
    }
    return c;
  });
}

async function base64Decode(compressedText: string): Promise<Uint8Array> {
  const base64 = compressedText.replace(/[-_]/g, (c) => {
    if (c === '-') {
      return '+';
    }
    if (c === '_') {
      return '/';
    }
    return c;
  });
  const result = await fetch(`${URI_PREFIX}${base64}`);
  return new Uint8Array(await result.arrayBuffer());
}

let zstd: Awaited<ReturnType<typeof Zstd.load>> | undefined;

globalThis.onmessage = (event) => {
  (async () => {
    if (zstd === undefined) {
      // Since we don't have types for the deep import, we have to cast here.
      zstd = await (zstdLoader as { load: typeof Zstd.load }).load();
    }
    // Since the render thread will only send us valid messages,
    // we can save a bit of bundle size by using a cast instead of `parse`
    // to avoid having to include `zod` in the worker.
    const message = event.data as CompressorRequest;
    if (message.request === 'compress') {
      const encoder = new TextEncoder();
      const encodedBuffer = encoder.encode(message.text);
      const compressedBuffer = zstd.compress(encodedBuffer, 3);
      const compressedText = await base64Encode(compressedBuffer);
      globalThis.postMessage({
        response: 'compressed',
        compressedText,
      } satisfies CompressResponse);
    } else if (message.request === 'decompress') {
      const decodedBuffer = await base64Decode(message.compressedText);
      const uncompressedBuffer = zstd.decompress(decodedBuffer);
      const decoder = new TextDecoder();
      const text = decoder.decode(uncompressedBuffer);
      globalThis.postMessage({
        response: 'decompressed',
        text,
      } satisfies DecompressResponse);
    } else {
      throw new Error(`Unknown request: ${JSON.stringify(event.data)}`);
    }
  })().catch((error) => {
    globalThis.postMessage({
      response: 'error',
      message: String(error),
    } satisfies ErrorResponse);
  });
};