Skip to content

Commit

Permalink
Improve web worker factory api
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed Mar 4, 2024
1 parent 36bab8b commit e633360
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 36 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,33 @@ esm.sh supports `?worker` query to load the module as a web worker:
```js
import workerFactory from "https://esm.sh/monaco-editor/esm/vs/editor/editor.worker?worker";
// create a worker
const worker = workerFactory();
// you can also rename the worker by adding the `name` option
const worker = workerFactory({ name: "editor.worker" });
```

You can pass some custom code snippet to the worker when calling the factory function:
You can import any module as a worker from esm.sh with the `?worker` query. The module will be loaded in a web worker
as variable `$module`, then you can use it in the `inject` code.

For example, you can use the `xxhash-wasm` module to hash a string in a web worker:

```js
const workerAddon = `
self.onmessage = function (e) {
console.log(e.data)
}
import workerFactory from "https://esm.sh/xxhash-wasm@1.0.2?worker";
const inject = `
// variable '$module' is the xxhash-wasm module
$module.default().then(hasher => {
self.postMessage(hasher.h64ToString(e.data));
})
`;
const worker = workerFactory(workerAddon);
const worker = workerFactory({ inject });
worker.onmessage = (e) => console.log("hash:", e.data);
worker.postMessage("The string that is being hashed");
```

> Note: The `inject` must be a valid JavaScript code, and it will be executed in the worker context.

### Package CSS

```html
Expand Down
50 changes: 25 additions & 25 deletions server/esm_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package server
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -571,10 +570,6 @@ func esmHandler() rex.Handle {
}
}
if err == nil {
r, err := fs.OpenFile(savePath)
if err != nil {
return rex.Status(500, err.Error())
}
if reqType == "types" {
header.Set("Content-Type", "application/typescript; charset=utf-8")
} else if endsWith(pathname, ".js", ".mjs", ".jsx", ".ts", ".mts", ".tsx") {
Expand All @@ -584,14 +579,16 @@ func esmHandler() rex.Handle {
}
header.Set("Cache-Control", "public, max-age=31536000, immutable")
if ctx.Form.Has("worker") && reqType == "builds" {
defer r.Close()
buf, err := io.ReadAll(r)
if err != nil {
return rex.Status(500, err.Error())
}
code := bytes.TrimSuffix(buf, []byte(fmt.Sprintf(`//# sourceMappingURL=%s.map`, path.Base(savePath))))
header.Set("Content-Type", "application/javascript; charset=utf-8")
return fmt.Sprintf(`export default function workerFactory(inject) { const blob = new Blob([%s, typeof inject === "string" ? "\n// inject\n" + inject : ""], { type: "application/javascript" }); return new Worker(URL.createObjectURL(blob), { type: "module" })}`, utils.MustEncodeJSON(string(code)))
moduleUrl := fmt.Sprintf("%s%s%s", cdnOrigin, cfg.CdnBasePath, pathname)
return fmt.Sprintf(
`export default function workerFactory(injectOrOptions) { const options = typeof injectOrOptions === "string" ? { inject: injectOrOptions }: injectOrOptions ?? {}; const { inject, name = "%s" } = options; const blob = new Blob(['import * as $module from "%s";', inject].filter(Boolean), { type: "application/javascript" }); return new Worker(URL.createObjectURL(blob), { type: "module", name })}`,
moduleUrl,
moduleUrl,
)
}
r, err := fs.OpenFile(savePath)
if err != nil {
return rex.Status(500, err.Error())
}
return rex.Content(savePath, fi.ModTime(), r) // auto closed
}
Expand Down Expand Up @@ -848,7 +845,7 @@ func esmHandler() rex.Handle {
Pkg: reqPkg,
Target: target,
Dev: isDev,
Bundle: bundle || isWorker,
Bundle: bundle,
NoBundle: noBundle,
}

Expand Down Expand Up @@ -922,18 +919,16 @@ func esmHandler() rex.Handle {
return rex.Status(500, err.Error())
}
header.Set("Cache-Control", "public, max-age=31536000, immutable")
if isWorker && endsWith(savePath, ".mjs", ".js") {
buf, err := io.ReadAll(f)
f.Close()
if err != nil {
return rex.Status(500, err.Error())
}
code := bytes.TrimSuffix(buf, []byte(fmt.Sprintf(`//# sourceMappingURL=%s.map`, path.Base(savePath))))
header.Set("Content-Type", "application/javascript; charset=utf-8")
return fmt.Sprintf(`export default function workerFactory(inject) { const blob = new Blob([%s, typeof inject === "string" ? "\n// inject\n" + inject : ""], { type: "application/javascript" }); return new Worker(URL.createObjectURL(blob), { type: "module" })}`, utils.MustEncodeJSON(string(code)))
}
if endsWith(savePath, ".mjs", ".js") {
header.Set("Content-Type", "application/javascript; charset=utf-8")
if isWorker {
moduleUrl := fmt.Sprintf("%s%s/%s", cdnOrigin, cfg.CdnBasePath, buildId)
return fmt.Sprintf(
`export default function workerFactory(injectOrOptions) { const options = typeof injectOrOptions === "string" ? { inject: injectOrOptions }: injectOrOptions ?? {}; const { inject, name = "%s" } = options; const blob = new Blob(['import * as $module from "%s";', inject].filter(Boolean), { type: "application/javascript" }); return new Worker(URL.createObjectURL(blob), { type: "module", name })}`,
moduleUrl,
moduleUrl,
)
}
}
return rex.Content(savePath, fi.ModTime(), f) // auto closed
}
Expand All @@ -942,7 +937,12 @@ func esmHandler() rex.Handle {
fmt.Fprintf(buf, `/* esm.sh - %v */%s`, reqPkg, EOL)

if isWorker {
fmt.Fprintf(buf, `export { default } from "%s/%s?worker";`, cfg.CdnBasePath, buildId)
moduleUrl := fmt.Sprintf("%s%s/%s", cdnOrigin, cfg.CdnBasePath, buildId)
fmt.Fprintf(buf,
`export default function workerFactory(injectOrOptions) { const options = typeof injectOrOptions === "string" ? { inject: injectOrOptions }: injectOrOptions ?? {}; const { inject, name = "%s" } = options; const blob = new Blob(['import * as $module from "%s";', inject].filter(Boolean), { type: "application/javascript" }); return new Worker(URL.createObjectURL(blob), { type: "module", name })}`,
moduleUrl,
moduleUrl,
)
} else {
if len(esm.Deps) > 0 {
// TODO: lookup deps of deps?
Expand Down
26 changes: 21 additions & 5 deletions test/worker/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { assertEquals } from "https://deno.land/std@0.210.0/testing/asserts.ts";

import workerFactory from "http://localhost:8080/xxhash-wasm@1.0.2?worker";

const workerInject = `
const inject = `
self.onmessage = (e) => {
// variable 'E' is the xxhash-wasm module default export
E().then(hasher => {
// variable '$module' is the xxhash-wasm module
$module.default().then(hasher => {
self.postMessage(hasher.h64ToString(e.data));
})
}
`;

Deno.test("?worker", async () => {
const worker = workerFactory(workerInject);
Deno.test("worker (legacy api)", async () => {
const worker = workerFactory(inject);
const hashText = await new Promise((resolve, reject) => {
const t = setTimeout(() => {
reject("timeout");
Expand All @@ -23,6 +23,22 @@ Deno.test("?worker", async () => {
});
worker.postMessage("The string that is being hashed");
});
assertEquals(hashText, "502b0c5fc4a5704c");
worker.terminate();
});

Deno.test("worker", async () => {
const worker = workerFactory({ inject, name: "xxhash-wasm" });
const hashText = await new Promise((resolve, reject) => {
const t = setTimeout(() => {
reject("timeout");
}, 1000);
worker.addEventListener("message", (e) => {
clearTimeout(t);
resolve(e.data);
});
worker.postMessage("The string that is being hashed");
});
assertEquals(hashText, "502b0c5fc4a5704c");
worker.terminate();
});

0 comments on commit e633360

Please sign in to comment.