Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
97cfd62
use esbuild for transpiling and bundling
hotzenklotz Jul 18, 2025
71adb3c
stuff
hotzenklotz Jul 21, 2025
fe467c9
it kinda works
hotzenklotz Jul 21, 2025
f348e3f
enable esbuild in proxy
hotzenklotz Jul 21, 2025
ed5aa9e
stuff
hotzenklotz Jul 21, 2025
4008d88
comments
hotzenklotz Jul 21, 2025
cbef713
use a temp directory for esbuild dev server
hotzenklotz Jul 22, 2025
aea8e11
move esbuild plugins into separate files
hotzenklotz Jul 22, 2025
1406071
Merge branch 'master' of github.com:scalableminds/webknossos into esb…
hotzenklotz Jul 22, 2025
e3a4d61
added verbosity and dynamic build target
hotzenklotz Jul 22, 2025
562d3f4
removed webpack
hotzenklotz Jul 22, 2025
13efe94
added explanations to custom esbuild plugins
hotzenklotz Jul 22, 2025
3f94638
fix typing
hotzenklotz Jul 22, 2025
95c9c5d
Merge branch 'master' of github.com:scalableminds/webknossos into esb…
hotzenklotz Jul 22, 2025
baed423
finally bundle everything correctly
hotzenklotz Jul 23, 2025
50e792c
cleanup and comments
hotzenklotz Jul 23, 2025
30e38a4
Merge branch 'esbuild-bundling' of github.com:scalableminds/webknosso…
hotzenklotz Jul 24, 2025
fa489fc
Merge branch 'master' of github.com:scalableminds/webknossos into esb…
hotzenklotz Aug 7, 2025
a9f802b
improvements
hotzenklotz Aug 8, 2025
6bde645
add wasm support
hotzenklotz Aug 8, 2025
2ed9654
DRY
hotzenklotz Aug 8, 2025
1cb89c5
cleanup
hotzenklotz Aug 8, 2025
16d681a
apply feedback
hotzenklotz Aug 8, 2025
ba39e02
Update tools/esbuild/protoPlugin.js
hotzenklotz Aug 8, 2025
42bcd80
Merge branch 'master' into esbuild-bundling
hotzenklotz Aug 8, 2025
2a1e6fb
Use caching for worker plugin
hotzenklotz Aug 14, 2025
2da3880
use different tmp directory
hotzenklotz Aug 14, 2025
3c386c8
fix loading of lazy modules by using express server for serving esbui…
philippotto Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_test_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ jobs:
env:
WK_VERSION: ${{ github.run_id }}

- name: Build webknossos (webpack)
- name: Build webknossos (esbuild)
run: yarn build

- name: Build webknossos (sbt)
Expand Down
9 changes: 1 addition & 8 deletions app/views/main.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@
<meta name="robot" content="noindex" />
}
<link rel="shortcut icon" type="image/png" href="/assets/images/favicon.png" />
<link
rel="stylesheet"
type="text/css"
media="screen"
href="/assets/bundle/vendors~main.css?nocache=@(webknossos.BuildInfo.commitHash)"
/>
<link
rel="stylesheet"
type="text/css"
Expand All @@ -44,8 +38,7 @@
data-airbrake-project-key="@(conf.Airbrake.projectKey)"
data-airbrake-environment-name="@(conf.Airbrake.environment)"
></script>
<script src="/assets/bundle/vendors~main.js?nocache=@(webknossos.BuildInfo.commitHash)"></script>
<script src="/assets/bundle/main.js?nocache=@(webknossos.BuildInfo.commitHash)"></script>
<script src="/assets/bundle/main.js?nocache=@(webknossos.BuildInfo.commitHash)" type="module"></script>
<script type="text/javascript" src="https://app.olvy.co/script.js" defer="defer"></script>
</head>
<body>
Expand Down
228 changes: 228 additions & 0 deletions esbuild_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
const esbuild = require("esbuild");
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");

const express = require("express");

const app = express();

const srcPath = path.resolve(__dirname, "frontend/javascripts/");
const outputPath = path.resolve(__dirname, "public/bundle/");
const protoPath = path.join(__dirname, "webknossos-datastore/proto");

// Community plugins
const browserslistToEsbuild = require("browserslist-to-esbuild");
const { lessLoader } = require("esbuild-plugin-less");
const copyPlugin = require("esbuild-plugin-copy").default;
const polyfillNode = require("esbuild-plugin-polyfill-node").polyfillNode;
const esbuildPluginWorker = require("@chialab/esbuild-plugin-worker").default;
const { wasmLoader } = require("esbuild-plugin-wasm");

// Custom Plugins for Webknossos
const { createWorkerPlugin } = require("./tools/esbuild/workerPlugin.js");
const { createProtoPlugin } = require("./tools/esbuild/protoPlugin.js");

const target = browserslistToEsbuild([
"last 3 Chrome versions",
"last 3 Firefox versions",
"last 2 Edge versions",
"last 1 Safari versions",
"last 1 iOS versions",
]);
Comment on lines +26 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Modern browser targets may break compatibility

The browser targets are set to very recent versions (last 3 Chrome/Firefox, last 2 Edge, last 1 Safari/iOS). This aggressive targeting will drop support for older browsers and could affect users on enterprise or locked-down systems.

Based on the PR objectives mentioning "drops legacy browser support" as an expected impact, this appears intentional. However, ensure this aligns with your user base requirements. Consider documenting the minimum supported browser versions explicitly in your documentation.

The current configuration effectively requires:

  • Chrome 136+ (3 versions back from 139)
  • Firefox 140+ (3 versions back from 143)
  • Safari 18+ (1 version back)
  • Module script and module worker support

If you need to support slightly older browsers, consider adjusting to:

 const target = browserslistToEsbuild([
-  "last 3 Chrome versions",
-  "last 3 Firefox versions",
-  "last 2 Edge versions",
-  "last 1 Safari versions",
-  "last 1 iOS versions",
+  "last 6 Chrome versions",
+  "last 6 Firefox versions", 
+  "last 4 Edge versions",
+  "last 2 Safari versions",
+  "last 2 iOS versions",
 ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const target = browserslistToEsbuild([
"last 3 Chrome versions",
"last 3 Firefox versions",
"last 2 Edge versions",
"last 1 Safari versions",
"last 1 iOS versions",
]);
const target = browserslistToEsbuild([
"last 6 Chrome versions",
"last 6 Firefox versions",
"last 4 Edge versions",
"last 2 Safari versions",
"last 2 iOS versions",
]);
🤖 Prompt for AI Agents
In esbuild_config.js around lines 26 to 32, the esbuild browser targets are set
very aggressively (last 3 Chrome/Firefox, last 2 Edge, last 1 Safari/iOS) which
will drop older/enterprise browsers; either relax the target list to include
older major versions you must support (e.g., increase the "last N" or add
explicit minimum versions for Chrome/Firefox/Safari/Edge/iOS) or add a separate
legacy build/targets map for older browsers and ensure module/script fallbacks,
and document the exact minimum supported browser versions in your repo docs (or
a comment next to this config) so the impact is explicit and reproducible.


function now() {
const d = new Date();
return d.toISOString().replace("T", " ").replace("Z", "");
}

async function build(env = {}) {
const isProduction = env.production || process.env.NODE_ENV === "production";
const isWatch = env.watch;

// Determine output directory for bundles.
// In watch mode, it's a temp dir. In production, it's the public bundle dir.
const buildOutDir = isWatch
? path.join(os.tmpdir(), "webknossos-esbuild-dev")
: outputPath;

console.log("buildOutDir", buildOutDir);

if (isWatch) {
// Ensure a clean, stable directory for development builds
fs.rmSync(buildOutDir, { recursive: true, force: true });
fs.mkdirSync(buildOutDir, { recursive: true });
}
Comment on lines +51 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Temporary directory cleanup in watch mode could cause issues

The code forcefully removes and recreates the temporary build directory on each watch mode start. This could cause issues if multiple instances are running or if the directory is in use.

 if (isWatch) {
   // Ensure a clean, stable directory for development builds
-  fs.rmSync(buildOutDir, { recursive: true, force: true });
-  fs.mkdirSync(buildOutDir, { recursive: true });
+  try {
+    fs.rmSync(buildOutDir, { recursive: true, force: true });
+    fs.mkdirSync(buildOutDir, { recursive: true });
+  } catch (error) {
+    console.error(`Failed to prepare build directory: ${error.message}`);
+    throw error;
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isWatch) {
// Ensure a clean, stable directory for development builds
fs.rmSync(buildOutDir, { recursive: true, force: true });
fs.mkdirSync(buildOutDir, { recursive: true });
}
if (isWatch) {
// Ensure a clean, stable directory for development builds
try {
fs.rmSync(buildOutDir, { recursive: true, force: true });
fs.mkdirSync(buildOutDir, { recursive: true });
} catch (error) {
console.error(`Failed to prepare build directory: ${error.message}`);
throw error;
}
}
🤖 Prompt for AI Agents
In esbuild_config.js around lines 51-55, avoid force-removing and recreating the
shared buildOutDir in watch mode; instead create a per-process temporary output
directory (e.g., use os.tmpdir()/fs.mkdtemp or append process.pid and a
timestamp to buildOutDir) and update references to use that dir, and if you must
remove an existing dir use non-force deletion with try/catch, locking/exists
checks, and exponential backoff retries to handle in-use directories rather than
fs.rmSync(..., { force: true }).


let onBuildDone = () => {}

// Track ongoing builds so that incoming requests can be paused until all builds are finished.
let currentBuildCount = 0;
let buildStartTime = null;
let buildCounterPlugin = {
name: 'buildCounterPlugin',
setup(build) {
build.onStart(() => {
if (currentBuildCount === 0) {
buildStartTime = performance.now();
}
currentBuildCount++;
console.log(now(), 'build started. currentBuildCount=', currentBuildCount)
})
build.onEnd(() => {
currentBuildCount--;
if (currentBuildCount === 0) {
console.log("Build took", performance.now() - buildStartTime);
}
console.log(now(), 'build ended. currentBuildCount=', currentBuildCount)
if (currentBuildCount === 0) {
onBuildDone();
}
})
},
}
Comment on lines +60 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Build tracking mechanism needs proper error handling

The build counter plugin tracks concurrent builds but doesn't handle errors that might leave the counter in an inconsistent state.

If a build fails catastrophically without triggering onEnd, the counter could remain elevated, causing requests to be permanently queued. Consider adding error handling:

 let buildCounterPlugin = {
   name: 'buildCounterPlugin',
   setup(build) {
     build.onStart(() => {
       if (currentBuildCount === 0) {
         buildStartTime = performance.now();
       }
       currentBuildCount++;
       console.log(now(), 'build started. currentBuildCount=', currentBuildCount)
     })
-    build.onEnd(() => {
+    build.onEnd((result) => {
       currentBuildCount--;
       if (currentBuildCount === 0) {
         console.log("Build took", performance.now() - buildStartTime);
       }
       console.log(now(), 'build ended. currentBuildCount=', currentBuildCount)
+      // Log errors if present
+      if (result.errors.length > 0) {
+        console.error(`Build completed with ${result.errors.length} errors`);
+      }
       if (currentBuildCount === 0) {
         onBuildDone();
       }
     })
+    // Ensure counter is decremented even on dispose
+    build.onDispose(() => {
+      if (currentBuildCount > 0) {
+        console.warn('Build context disposed with active builds, resetting counter');
+        currentBuildCount = 0;
+        onBuildDone();
+      }
+    })
   },
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
esbuild_config.js lines 60-83: the build counter increments/decrements globally
and can remain elevated if a build fails without triggering onEnd; replace the
simple counter with a per-build tracking approach and add fail-safes: onStart
should generate and store a unique build id in a Map (or Set) and increment only
that entry; onEnd should remove that id and compute currentBuildCount from the
Map size so stray increments can't persist; additionally add a watchdog timeout
for each start (store a timer with the id) that will auto-clean the id and call
onBuildDone if onEnd never arrives, and register process-level handlers for
uncaughtException/unhandledRejection to clear active builds and call
onBuildDone; ensure all timers are cleared when builds finish.


const plugins = [
polyfillNode(),
createProtoPlugin(protoPath),
wasmLoader(),
lessLoader({
javascriptEnabled: true,
}),
copyPlugin({
patterns: [
{
from: "node_modules/@zip.js/zip.js/dist/z-worker.js",
to: path.join(buildOutDir, "z-worker.js"),
},
],
}),
createWorkerPlugin({ logLevel: env.logLevel }), // Resolves import Worker from myFunc.worker;
esbuildPluginWorker(), // Resolves new Worker(myWorker.js)
buildCounterPlugin
];


const buildOptions = {
entryPoints: {
main: path.resolve(srcPath, "main.tsx"),
},
bundle: true,
outdir: buildOutDir,
format: "esm",
target: target,
platform: "browser",
splitting: true,
chunkNames: "[name].[hash]",
assetNames: "[name].[hash]",
sourcemap: isProduction ? "external" : "inline",
Comment on lines +117 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing cache busting for worker files

The PR objectives mention "cache-busting for workers (emit hashed filenames or append nocache)" as a follow-up item. The current configuration generates hashed chunk names but workers might not benefit from this.

The createWorkerPlugin in tools/esbuild/workerPlugin.js currently generates workers with static filenames (e.g., workerBaseName.js). This could cause caching issues when workers are updated.

Would you like me to help implement cache-busted filenames for workers or create an issue to track this task?

minify: isProduction,
define: {
"process.env.NODE_ENV": JSON.stringify(isProduction ? "production" : "development"),
"process.env.BABEL_ENV": JSON.stringify(process.env.BABEL_ENV || "development"),
"process.browser": "true",
"global": "globalThis"
},
loader: {
".woff": "file",
".woff2": "file",
".ttf": "file",
".eot": "file",
".svg": "file",
".png": "file",
".jpg": "file",
".jpeg": "file",
".gif": "file",
".webp": "file",
".ico": "file",
".mp4": "file",
".webm": "file",
".ogg": "file",
},
resolveExtensions: [".ts", ".tsx", ".js", ".json", ".proto", ".wasm"],
alias: {
react: path.resolve(__dirname, "node_modules/react"),
},
plugins,
external: ["/assets/images/*", "fs"],
logLevel: env.logLevel || "info",
legalComments: isProduction ? "inline" : "none",
publicPath: "/assets/bundle/",
metafile: !isWatch, // Don't generate metafile for dev server
logOverride: {
"direct-eval": "silent",
},
};

if (env.watch) {
// Development server mode
const ctx = await esbuild.context(buildOptions);
const port = env.PORT || 9002;

// queue for waiting requests
let waiting = [];

function gateMiddleware(req, res, next) {
if (currentBuildCount === 0) {
return next(); // allow immediately
}

// hold request
waiting.push({ req, res, next });
}
Comment on lines +165 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Request queueing mechanism could cause memory issues

The middleware queues all incoming requests when builds are in progress, which could lead to memory issues if many requests arrive during a long build.

Consider adding a queue size limit or timeout:

 // queue for waiting requests
 let waiting = [];
+const MAX_QUEUE_SIZE = 1000;
+const QUEUE_TIMEOUT_MS = 30000;

 function gateMiddleware(req, res, next) {
   if (currentBuildCount === 0) {
     return next(); // allow immediately
   }

+  if (waiting.length >= MAX_QUEUE_SIZE) {
+    res.status(503).send("Server too busy, please try again later");
+    return;
+  }
+
+  const timeout = setTimeout(() => {
+    const index = waiting.findIndex(w => w.req === req);
+    if (index !== -1) {
+      waiting.splice(index, 1);
+      res.status(504).send("Build timeout");
+    }
+  }, QUEUE_TIMEOUT_MS);
+
   // hold request
-  waiting.push({ req, res, next });
+  waiting.push({ req, res, next, timeout });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function gateMiddleware(req, res, next) {
if (currentBuildCount === 0) {
return next(); // allow immediately
}
// hold request
waiting.push({ req, res, next });
}
// queue for waiting requests
let waiting = [];
const MAX_QUEUE_SIZE = 1000;
const QUEUE_TIMEOUT_MS = 30000;
function gateMiddleware(req, res, next) {
if (currentBuildCount === 0) {
return next(); // allow immediately
}
if (waiting.length >= MAX_QUEUE_SIZE) {
res.status(503).send("Server too busy, please try again later");
return;
}
const timeout = setTimeout(() => {
const index = waiting.findIndex(w => w.req === req);
if (index !== -1) {
waiting.splice(index, 1);
res.status(504).send("Build timeout");
}
}, QUEUE_TIMEOUT_MS);
// hold request
waiting.push({ req, res, next, timeout });
}
🤖 Prompt for AI Agents
In esbuild_config.js around lines 165 to 172, the gateMiddleware currently
pushes every incoming request into waiting while a build runs, which can cause
unbounded memory growth; add a bounded queue and timeout: define a
MAX_QUEUE_LENGTH and REQUEST_TIMEOUT_MS constant, and when pushing a request
check queue length and immediately respond with a 503 (or next an error) if the
queue is full; when enqueuing set a timer that will remove the request from the
waiting array and respond with 503 if it exceeds REQUEST_TIMEOUT_MS; ensure
timers are cleared when requests are later drained after the build completes so
no leaks occur, and log queue overflow/timeouts for observability.


app.use("/", gateMiddleware, express.static(buildOutDir));

onBuildDone = function releaseRequests() {
const queued = waiting;
waiting = [];
queued.forEach(({ next }) => {
return next();
});
}

app.listen(port, "127.0.0.1", () => {
console.log(`Server running at http://localhost:${port}/assets/bundle/`);
});

console.log(`Development server running at http://localhost:${port}`);
console.log(`Serving files from temporary directory: ${buildOutDir}`);

process.on("SIGINT", async () => {
await ctx.dispose();
process.exit(0);
});
Comment on lines +191 to +194
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

SIGINT handler should handle cleanup errors

The signal handler for graceful shutdown doesn't handle potential errors during context disposal.

 process.on("SIGINT", async () => {
+  console.log("\nShutting down development server...");
+  try {
     await ctx.dispose();
+    console.log("Build context disposed successfully");
+  } catch (error) {
+    console.error("Error disposing build context:", error);
+  }
   process.exit(0);
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process.on("SIGINT", async () => {
await ctx.dispose();
process.exit(0);
});
process.on("SIGINT", async () => {
console.log("\nShutting down development server...");
try {
await ctx.dispose();
console.log("Build context disposed successfully");
} catch (error) {
console.error("Error disposing build context:", error);
}
process.exit(0);
});
🤖 Prompt for AI Agents
In esbuild_config.js around lines 191 to 194, the SIGINT handler calls await
ctx.dispose() without error handling; wrap the dispose call in a try/catch, log
or report any error thrown during ctx.dispose() (using console.error or the
project logger), and then call process.exit(0) on success or process.exit(1) on
failure so the process exits even when cleanup fails.

} else {
// Production build
const result = await esbuild.build(buildOptions);

if (result.metafile) {
await fs.promises.writeFile(
path.join(buildOutDir, "metafile.json"),
JSON.stringify(result.metafile, null, 2)
);
}

console.log("Build completed successfully!");
}
}

module.exports = { build };

// If called directly
if (require.main === module) {
const args = process.argv.slice(2);
const env = {
logLevel: "info", // Default log level
};

args.forEach(arg => {
if (arg === "--production") env.production = true;
if (arg === "--watch") env.watch = true;
if (arg.startsWith("--port=")) env.PORT = Number.parseInt(arg.split("=")[1]);
if (arg === "--verbose") env.logLevel = "verbose";
if (arg === "--silent") env.logLevel = "silent";
});

build(env).catch(console.error);
}
1 change: 0 additions & 1 deletion frontend/javascripts/libs/DRACOLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ class DRACOLoader extends Loader {
_getWorker(taskID, taskCost) {
return this._initDecoder().then(() => {
if (this.workerPool.length < this.workerLimit) {
// See https://webpack.js.org/guides/web-workers/
const worker = new Worker(new URL("./DRACOWorker.worker.js", import.meta.url));

worker._callbacks = {};
Expand Down
6 changes: 0 additions & 6 deletions frontend/javascripts/test/_ava_polyfill_provider.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import deepFreezeLib from "deep-freeze";
import _ from "lodash";

// Do not use the deep-freeze library in production
// process.env.NODE_ENV is being substituted by webpack
// process.env.NODE_ENV is being substituted by esbuild
let deepFreeze = deepFreezeLib;
if (process.env.NODE_ENV === "production") deepFreeze = _.identity;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@ant-design/icons";
import { Tree as AntdTree, Dropdown, type GetRef, Space, Tooltip, type TreeProps } from "antd";
import type { EventDataNode } from "antd/es/tree";
import { useLifecycle } from "beautiful-react-hooks";
import useLifecycle from "beautiful-react-hooks/useLifecycle";
import { InputKeyboard } from "libs/input";
import { useEffectOnlyOnce } from "libs/react_hooks";
import { useWkSelector } from "libs/react_hooks";
Expand Down
19 changes: 13 additions & 6 deletions frontend/javascripts/viewer/workers/comlink_wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import {
throwTransferHandlerWithResponseSupport,
} from "viewer/workers/headers_transfer_handler";

// Worker modules export bare functions and typically instantiated with new Worker("./path/to/my/worker.js");

// JS bundlers (like esbuild) try to match these calls and generate new entry points/bundles for the worker code.
// However, our esbuild pipeline is a bit different:
// 1) We import our worker code with regular import statements, e.g. import worker from './my.worker';
// 2) We consolidated all worker/Comlink stuff into this wrapper, calling new Worker(workerFunction). Since workerFunction is a variable it is usually not identified by esbuild as it matches not pattern.
// Similar to the webpack worker-loader, we have a custom esbuild plugin to load worker codes. See tools/esbuild/workerPlugin.js.

function importComlink() {
const isNodeContext = typeof process !== "undefined" && process.title !== "browser";

Expand Down Expand Up @@ -36,8 +44,7 @@ const { wrap, transferHandlers, _expose, _transfer } = importComlink();
transferHandlers.set("requestOptions", requestOptionsTransferHandler);
// Overwrite the default throw handler with ours that supports responses.
transferHandlers.set("throw", throwTransferHandlerWithResponseSupport);
// Worker modules export bare functions, but webpack turns these into Worker classes which need to be
// instantiated first.

// To ensure that code always executes the necessary instantiation, we cheat a bit with the typing in the following code.
// In reality, `expose` receives a function and returns it again. However, we tell flow that it wraps the function, so that
// unwrapping becomes necessary.
Expand All @@ -47,18 +54,18 @@ type UseCreateWorkerToUseMe<T> = {
readonly _wrapped: T;
};
export function createWorker<T extends (...args: any) => any>(
WorkerClass: UseCreateWorkerToUseMe<T>,
workerFunction: UseCreateWorkerToUseMe<T>,
): (...params: Parameters<T>) => Promise<ReturnType<T>> {
if (wrap == null) {
// In a node context (e.g., when executing tests), we don't create web workers which is why
// we can simply return the input function here.
// @ts-ignore
return WorkerClass;
// @ts-expect-error
return workerFunction;
}

return wrap(
// @ts-ignore
new WorkerClass(),
new Worker(workerFunction, { type: "module" }),
);
}
export function expose<T>(fn: T): UseCreateWorkerToUseMe<T> {
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/viewer/workers/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ See `compress.worker.js` for an example.
- Accessing global state (e.g., the Store) is not directly possible from web workers, since they have their own execution context. Pass necessary information into web workers via parameters.
- By default, parameters and return values are either structurally cloned or transferred (if they support it) to/from the web worker. Copying is potentially performance-intensive and also won't propagate any mutations across the main-thread/webworker border. If objects are transferable (e.g., for ArrayBuffers, but not TypedArrays), they are moved to the new context, which means that they cannot be accessed in the old thread, anymore. In both cases, care has to be taken. In general, web workers should only be responsible for a very small (but cpu intensive) task with a bare minimum of dependencies.
- Not all objects can be passed between main thread and web workers (e.g., Header objects). For these cases, you have to implement and register a specific transfer handler for the object type. See `headers_transfer_handler.js` as an example.
- Web worker files can import NPM modules and also modules from within this code base, but beware that the execution context between the main thread and web workers is strictly isolated. Webpack will create a separate JS file for each web worker into which all imported code is compiled.
- Web worker files can import NPM modules and also modules from within this code base, but beware that the execution context between the main thread and web workers is strictly isolated. esbuild will create a separate JS file for each web worker into which all imported code is compiled.

Learn more about the Comlink module we use [here](https://github.com/GoogleChromeLabs/comlink).
Loading