@wasmedia/wenblend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +367 -0
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/README.md +98 -0
  4. package/dist/cli.d.ts +27 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +167 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/filesystem.d.ts +25 -0
  9. package/dist/filesystem.d.ts.map +1 -0
  10. package/dist/filesystem.js +75 -0
  11. package/dist/filesystem.js.map +1 -0
  12. package/dist/handler.d.ts +62 -0
  13. package/dist/handler.d.ts.map +1 -0
  14. package/dist/handler.js +91 -0
  15. package/dist/handler.js.map +1 -0
  16. package/dist/index.d.ts +17 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +17 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/loader.d.ts +74 -0
  21. package/dist/loader.d.ts.map +1 -0
  22. package/dist/loader.js +181 -0
  23. package/dist/loader.js.map +1 -0
  24. package/dist/operations.d.ts +45 -0
  25. package/dist/operations.d.ts.map +1 -0
  26. package/dist/operations.js +94 -0
  27. package/dist/operations.js.map +1 -0
  28. package/dist/staging.d.ts +37 -0
  29. package/dist/staging.d.ts.map +1 -0
  30. package/dist/staging.js +157 -0
  31. package/dist/staging.js.map +1 -0
  32. package/dist/tiff-header.d.ts +16 -0
  33. package/dist/tiff-header.d.ts.map +1 -0
  34. package/dist/tiff-header.js +97 -0
  35. package/dist/tiff-header.js.map +1 -0
  36. package/dist/types.d.ts +128 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +47 -0
  39. package/dist/types.js.map +1 -0
  40. package/dist/wenblend.js +2 -0
  41. package/dist/wenblend.wasm +0 -0
  42. package/dist/wenfuse.js +2 -0
  43. package/dist/wenfuse.wasm +0 -0
  44. package/dist/wopfs.d.ts +37 -0
  45. package/dist/wopfs.d.ts.map +1 -0
  46. package/dist/wopfs.js +38 -0
  47. package/dist/wopfs.js.map +1 -0
  48. package/dist/worker.d.ts +16 -0
  49. package/dist/worker.d.ts.map +1 -0
  50. package/dist/worker.js +29 -0
  51. package/dist/worker.js.map +1 -0
  52. package/package.json +67 -0
  53. package/src/cli.ts +201 -0
  54. package/src/filesystem.ts +85 -0
  55. package/src/handler.ts +112 -0
  56. package/src/index.ts +41 -0
  57. package/src/loader.ts +223 -0
  58. package/src/operations.ts +147 -0
  59. package/src/staging.ts +173 -0
  60. package/src/tiff-header.ts +94 -0
  61. package/src/types.ts +153 -0
  62. package/src/wopfs.ts +70 -0
  63. package/src/worker.ts +31 -0
  64. package/types/wasm.d.ts +12 -0
package/src/staging.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * How a run's files reach the tool and come back.
3
+ *
4
+ * runTool() has one flow: stage, callMain, collect, release. This interface
5
+ * is what varies. stageBuffers is the MEMFS path every caller used before file
6
+ * mode existed, and its behaviour is unchanged. stageFiles (file mode) mounts
7
+ * files through wopfs.
8
+ */
9
+
10
+ import type { FileInput, InputImage, LoaderOptions, OutputDir, WasmModule, WrittenFile } from "./types.js";
11
+ import { createWorkDir, stageInputs, collectOutputs, cleanWorkDir, nextWorkDirName, toBytes } from "./filesystem.js";
12
+ import { tiffPixelCount, tiffPixelCountAt, type ReadAt } from "./tiff-header.js";
13
+ import { loadWopfs, type WopfsCommittedFile } from "./wopfs.js";
14
+
15
+ export interface Staging {
16
+ /** The directory the tool runs in (its cwd). */
17
+ readonly workDir: string;
18
+ readonly inputNames: ReadonlySet<string>;
19
+ /** Everything the run created. Buffer mode: bytes. File mode: committed files. */
20
+ collect(): Promise<Map<string, Uint8Array | WrittenFile>>;
21
+ /** Total input pixels, for OutOfMemoryError. Callable after release(). */
22
+ pixelCount(): Promise<number | undefined>;
23
+ /** Idempotent. runTool calls it in a finally on every path. */
24
+ release(): Promise<void>;
25
+ }
26
+
27
+ export function stageBuffers(module: WasmModule, inputs: InputImage[]): Staging {
28
+ const workDir = createWorkDir(module);
29
+ const inputNames = stageInputs(module, workDir, inputs);
30
+ let released = false;
31
+ return {
32
+ workDir,
33
+ inputNames,
34
+ async collect() {
35
+ return collectOutputs(module, workDir, inputNames);
36
+ },
37
+ async pixelCount() {
38
+ // With no inputs no header states a count, so none is known; 0 would be a claim.
39
+ if (inputs.length === 0) return undefined;
40
+ let total = 0;
41
+ for (const input of inputs) {
42
+ const n = tiffPixelCount(toBytes(input.data));
43
+ if (n === undefined) return undefined;
44
+ total += n;
45
+ }
46
+ return total;
47
+ },
48
+ async release() {
49
+ if (released) return;
50
+ released = true;
51
+ // The instance is discarded after this call (never reused, per
52
+ // createInstance's contract), so there is no previous cwd to restore.
53
+ cleanWorkDir(module, workDir);
54
+ },
55
+ };
56
+ }
57
+
58
+ /** True for `{ name, file }`, the file-mode input shape. */
59
+ export function isFileInput(x: unknown): x is FileInput {
60
+ return typeof x === "object" && x !== null && "file" in x && "name" in x && !("data" in x);
61
+ }
62
+
63
+ // getopt_long accepts any unambiguous prefix of a long option. Over-counting
64
+ // costs an empty pre-opened file; under-counting fails the run, so a prefix
65
+ // match errs on the generous side.
66
+ const longName = (arg: string) => (arg.startsWith("--") ? arg.slice(2).split("=", 1)[0]! : "");
67
+ const names = (argv: readonly string[], option: string) =>
68
+ argv.some((arg) => { const n = longName(arg); return n.length >= 2 && option.startsWith(n); });
69
+
70
+ /**
71
+ * How many files a run can create, from its argv: the -o output, plus
72
+ * --visualize's image, plus a soft and a hard mask per input for
73
+ * --save-mask(s). enblend and enfuse create no temporary files (spec,
74
+ * finding 2). --load-mask(s) reads, so it adds nothing here.
75
+ */
76
+ export function scratchFor(argv: readonly string[], inputCount: number): number {
77
+ let n = 1;
78
+ if (names(argv, "visualize")) n += 1;
79
+ if (names(argv, "save-masks")) n += 2 * inputCount;
80
+ return n;
81
+ }
82
+
83
+ /** A reader for the pixel count: a byte range of a file, without reading the rest. */
84
+ function readerFor(file: FileSystemFileHandle | string): ReadAt {
85
+ if (typeof file === "string") {
86
+ return async (offset, length) => {
87
+ // A variable specifier: the browser build never resolves node:fs.
88
+ const specifier = "node:fs/promises";
89
+ const { open } = await import(/* @vite-ignore */ specifier);
90
+ const handle = await open(file, "r");
91
+ try {
92
+ const buffer = new Uint8Array(length);
93
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
94
+ return buffer.subarray(0, bytesRead);
95
+ } finally {
96
+ await handle.close();
97
+ }
98
+ };
99
+ }
100
+ return async (offset, length) =>
101
+ new Uint8Array(await (await file.getFile()).slice(offset, offset + length).arrayBuffer());
102
+ }
103
+
104
+ /**
105
+ * Turn what wopfs committed into a WrittenFile. On the main thread wopfs
106
+ * falls back to MEMFS, which returns bytes and ignores outputDir. Write those
107
+ * into outputDir here, so file mode hands back a file in every environment.
108
+ */
109
+ async function toWrittenFile(c: WopfsCommittedFile, outputDir: OutputDir): Promise<WrittenFile> {
110
+ if (c.handle) return { name: c.path, file: c.handle, size: c.size };
111
+ if (c.hostPath) return { name: c.path, file: c.hostPath, size: c.size };
112
+ if (c.bytes && typeof outputDir !== "string") {
113
+ // Check before creating the file, so a failure leaves no empty file behind.
114
+ if (typeof FileSystemFileHandle === "undefined" || !("createWritable" in FileSystemFileHandle.prototype)) {
115
+ throw new Error("file mode on this thread needs FileSystemFileHandle.createWritable; run it in a dedicated worker instead");
116
+ }
117
+ const handle = await outputDir.getFileHandle(c.path, { create: true });
118
+ const writable = await handle.createWritable();
119
+ await writable.write(c.bytes as Uint8Array<ArrayBuffer>);
120
+ await writable.close();
121
+ return { name: c.path, file: handle, size: c.bytes.length };
122
+ }
123
+ throw new Error(`wopfs committed "${c.path}" with neither a file nor bytes`);
124
+ }
125
+
126
+ /**
127
+ * Mount the run's files through wopfs. Everything is opened here, before
128
+ * callMain, because the tool cannot await. The pool is strict: spilling
129
+ * would put new files back in memory and defeat the point (spec, Decisions).
130
+ */
131
+ export async function stageFiles(
132
+ module: WasmModule,
133
+ inputs: FileInput[],
134
+ outputDir: OutputDir,
135
+ argv: readonly string[],
136
+ options?: LoaderOptions,
137
+ ): Promise<Staging> {
138
+ const wopfs = await loadWopfs(options);
139
+ const workDir = nextWorkDirName();
140
+ const mount = await wopfs.mountFiles(module, workDir, {
141
+ inputs: Object.fromEntries(inputs.map((i) => [i.name, i.file])),
142
+ outputDir,
143
+ scratch: scratchFor(argv, inputs.length),
144
+ onExhausted: "strict",
145
+ });
146
+ let released = false;
147
+ return {
148
+ workDir,
149
+ inputNames: new Set(inputs.map((i) => i.name)),
150
+ async collect() {
151
+ const out = new Map<string, Uint8Array | WrittenFile>();
152
+ for (const c of await mount.commit()) out.set(c.path, await toWrittenFile(c, outputDir));
153
+ return out;
154
+ },
155
+ async pixelCount() {
156
+ if (inputs.length === 0) return undefined;
157
+ let total = 0;
158
+ for (const input of inputs) {
159
+ const n = await tiffPixelCountAt(readerFor(input.file));
160
+ if (n === undefined) return undefined;
161
+ total += n;
162
+ }
163
+ return total;
164
+ },
165
+ async release() {
166
+ if (released) return;
167
+ released = true;
168
+ // runTool ran the tool inside the mount; wopfs >= 0.2.0 moves the cwd
169
+ // out before unmounting (https://gitlab.com/wasmedia/wopfs/-/work_items/2).
170
+ await mount.release();
171
+ },
172
+ };
173
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pixel count of a TIFF, read from its header and image directories only.
3
+ *
4
+ * One walker serves both callers: buffer mode, which has the whole file's
5
+ * bytes, and file mode, where the file sits in OPFS or on disk and only the
6
+ * few hundred bytes the walk needs should be read. The walker is a generator
7
+ * that yields the byte ranges it needs; tiffPixelCount drives it from a byte
8
+ * array, and tiffPixelCountAt from an async reader.
9
+ */
10
+
11
+ type Need = { offset: number; length: number };
12
+
13
+ /** A random-access reader. A short or empty result means past the end. */
14
+ export type ReadAt = (offset: number, length: number) => Promise<Uint8Array>;
15
+
16
+ const view = (b: Uint8Array) => new DataView(b.buffer, b.byteOffset, b.byteLength);
17
+
18
+ /**
19
+ * Sum of width x height over every image directory, from the ImageWidth and
20
+ * ImageLength tags. Undefined for anything that cannot be read with certainty:
21
+ * not a classic TIFF (BigTIFF included), truncated, a cycle, or a directory
22
+ * missing either tag. Never a guess.
23
+ */
24
+ function* walk(): Generator<Need, number | undefined, Uint8Array | undefined> {
25
+ const head = yield { offset: 0, length: 8 };
26
+ if (!head || head.length < 8) return undefined;
27
+ const hv = view(head);
28
+ const order = hv.getUint16(0);
29
+ if (order !== 0x4949 && order !== 0x4d4d) return undefined;
30
+ const le = order === 0x4949;
31
+ if (hv.getUint16(2, le) !== 42) return undefined;
32
+ let offset = hv.getUint32(4, le);
33
+ let total = 0;
34
+ const visited = new Set<number>();
35
+ while (offset !== 0) {
36
+ if (visited.has(offset)) return undefined;
37
+ visited.add(offset);
38
+ const countBytes = yield { offset, length: 2 };
39
+ if (!countBytes || countBytes.length < 2) return undefined;
40
+ const count = view(countBytes).getUint16(0, le);
41
+ const size = count * 12 + 4;
42
+ const dir = yield { offset: offset + 2, length: size };
43
+ if (!dir || dir.length < size) return undefined;
44
+ const dv = view(dir);
45
+ let width: number | undefined;
46
+ let height: number | undefined;
47
+ for (let i = 0; i < count; i++) {
48
+ const entry = i * 12;
49
+ const tag = dv.getUint16(entry, le);
50
+ if (tag !== 256 && tag !== 257) continue;
51
+ const type = dv.getUint16(entry + 2, le);
52
+ // SHORT (3) or LONG (4), exactly one value, stored in the entry itself.
53
+ // Any other count is malformed; reading it as one value would misreport.
54
+ const values = dv.getUint32(entry + 4, le);
55
+ const value = values !== 1 ? undefined
56
+ : type === 3 ? dv.getUint16(entry + 8, le)
57
+ : type === 4 ? dv.getUint32(entry + 8, le)
58
+ : undefined;
59
+ if (tag === 256) width = value;
60
+ else height = value;
61
+ }
62
+ if (width === undefined || height === undefined) return undefined;
63
+ total += width * height;
64
+ offset = dv.getUint32(count * 12, le);
65
+ }
66
+ return total;
67
+ }
68
+
69
+ /** The pixel count of a TIFF held in memory. */
70
+ export function tiffPixelCount(bytes: Uint8Array): number | undefined {
71
+ const it = walk();
72
+ let step = it.next();
73
+ while (!step.done) {
74
+ const { offset, length } = step.value;
75
+ step = it.next(bytes.subarray(offset, offset + length));
76
+ }
77
+ return step.value;
78
+ }
79
+
80
+ /** The pixel count of a TIFF read through `readAt`, fetching only what the walk needs. */
81
+ export async function tiffPixelCountAt(readAt: ReadAt): Promise<number | undefined> {
82
+ const it = walk();
83
+ let step = it.next();
84
+ while (!step.done) {
85
+ let chunk: Uint8Array | undefined;
86
+ try {
87
+ chunk = await readAt(step.value.offset, step.value.length);
88
+ } catch {
89
+ chunk = undefined;
90
+ }
91
+ step = it.next(chunk);
92
+ }
93
+ return step.value;
94
+ }
package/src/types.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared types for wenblend.
3
+ *
4
+ * Only three C symbols are exported by the wasm modules (`_malloc`,
5
+ * `_free`, `_main`), so `WasmModule` describes the Emscripten runtime
6
+ * surface rather than a bound C API.
7
+ */
8
+
9
+ /** Emscripten MEMFS node stat. */
10
+ export interface FSStat {
11
+ size: number;
12
+ mode: number;
13
+ }
14
+
15
+ /** The subset of Emscripten's FS we use. */
16
+ export interface EmscriptenFS {
17
+ mkdir(path: string): void;
18
+ chdir(path: string): void;
19
+ cwd(): string;
20
+ writeFile(path: string, data: Uint8Array): void;
21
+ readFile(path: string, opts?: { encoding?: "binary" | "utf8" }): Uint8Array;
22
+ readdir(path: string): string[];
23
+ unlink(path: string): void;
24
+ rmdir(path: string): void;
25
+ stat(path: string): FSStat;
26
+ analyzePath(path: string): { exists: boolean };
27
+ }
28
+
29
+ /** The Emscripten module surface this wrapper relies on. */
30
+ export interface WasmModule {
31
+ FS: EmscriptenFS;
32
+ callMain(argv: string[]): number | undefined;
33
+ print?: (text: string) => void;
34
+ printErr?: (text: string) => void;
35
+ _malloc(size: number): number;
36
+ _free(ptr: number): void;
37
+ HEAPU8: Uint8Array;
38
+ }
39
+
40
+ /** A named input image. Callers control the names their argv references. */
41
+ export interface InputImage {
42
+ name: string;
43
+ data: Uint8Array | ArrayBuffer;
44
+ }
45
+
46
+ /** The result of one CLI invocation. */
47
+ export interface CLIResult {
48
+ exitCode: number;
49
+ stdout: string;
50
+ stderr: string;
51
+ /** Files present in the work directory that were not inputs. */
52
+ outputFiles: Map<string, Uint8Array>;
53
+ }
54
+
55
+ /** A file a file-mode run committed to its output directory. */
56
+ export interface WrittenFile {
57
+ name: string;
58
+ /** The committed file: an OPFS handle in a browser, a host path under Node. */
59
+ file: FileSystemFileHandle | string;
60
+ size: number;
61
+ }
62
+
63
+ /** An input that stays in a file: an OPFS handle in a browser, a host path under Node. */
64
+ export interface FileInput {
65
+ name: string;
66
+ file: FileSystemFileHandle | string;
67
+ }
68
+
69
+ /** Where file mode commits outputs: an OPFS directory, or a host directory under Node. */
70
+ export type OutputDir = FileSystemDirectoryHandle | string;
71
+
72
+ /** A file-mode CLI result: outputs are committed files, not bytes. */
73
+ export interface CLIFileResult {
74
+ exitCode: number;
75
+ stdout: string;
76
+ stderr: string;
77
+ /** Committed outputs by name. Empty unless the tool exited 0. */
78
+ written: Map<string, WrittenFile>;
79
+ }
80
+
81
+ export type FileModeOptions = LoaderOptions & { outputDir: OutputDir };
82
+
83
+ /** Options common to both tools. */
84
+ export interface CommonOptions {
85
+ /** Output filename inside MEMFS. Defaults to `output.tif`. */
86
+ output?: string;
87
+ /** Extra argv appended verbatim, exactly as typed on a command line. */
88
+ args?: string[];
89
+ }
90
+
91
+ export type BlendOptions = CommonOptions;
92
+ export type FuseOptions = CommonOptions;
93
+
94
+ /** Progress phases reported by the loader. */
95
+ export type LoadPhase = "loading" | "initializing" | "ready";
96
+
97
+ export interface LoaderOptions {
98
+ /** Override the module URL (for bundlers or custom hosting). */
99
+ wasmPath?: string;
100
+ /**
101
+ * Where file mode loads @wasmedia/wopfs from. The default imports
102
+ * "@wasmedia/wopfs" itself, which works under Node and with a bundler. A
103
+ * browser without a bundler (plain ES modules, workers) passes the URL of
104
+ * wopfs's dist/index.js.
105
+ */
106
+ wopfsPath?: string;
107
+ onProgress?: (phase: LoadPhase) => void;
108
+ }
109
+
110
+ /** A non-zero exit from enblend or enfuse. Carries the tool's own diagnostics. */
111
+ export class EnblendError extends Error {
112
+ readonly exitCode: number;
113
+ readonly stderr: string;
114
+
115
+ constructor(message: string, exitCode: number, stderr: string) {
116
+ super(message);
117
+ this.name = "EnblendError";
118
+ this.exitCode = exitCode;
119
+ this.stderr = stderr;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Allocation failure.
125
+ *
126
+ * Nothing bounds memory in this port: enblend's image cache was removed
127
+ * upstream in 4.2.1, so there is no spill-to-disk and no graceful
128
+ * degradation. `ALLOW_MEMORY_GROWTH=1` with `MAXIMUM_MEMORY=4GB` is the
129
+ * only ceiling and browsers allocate less in practice. This is the
130
+ * runtime signal for the documented ceiling in `docs/building.md`.
131
+ */
132
+ export class OutOfMemoryError extends Error {
133
+ /**
134
+ * Total input pixels, read from the inputs' TIFF headers (every image
135
+ * directory of every input). Undefined when any input's header could not
136
+ * be read with certainty; it is never estimated from byte counts, which
137
+ * for compressed TIFFs can be off several-fold.
138
+ */
139
+ readonly pixels: number | undefined;
140
+ /** The tool's stderr, including its own `out of memory` line. */
141
+ readonly stderr: string;
142
+
143
+ constructor(pixels: number | undefined, stderr = "") {
144
+ super(
145
+ `Out of memory${pixels === undefined ? "" : ` blending ${pixels} input pixels`}. ` +
146
+ `This port has no image cache, so memory scales with total input area. ` +
147
+ `See the measured ceiling in docs/building.md.`,
148
+ );
149
+ this.name = "OutOfMemoryError";
150
+ this.pixels = pixels;
151
+ this.stderr = stderr;
152
+ }
153
+ }
package/src/wopfs.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The slice of @wasmedia/wopfs that file mode uses, and its lazy loader.
3
+ *
4
+ * The types are declared here structurally rather than imported, so a
5
+ * consumer who never uses file mode needs neither the package nor its type
6
+ * declarations. Nothing else in src/ imports wopfs; the package is loaded
7
+ * on the first file-mode call, the way each tool's module is loaded on its
8
+ * first use.
9
+ */
10
+
11
+ import type { LoaderOptions } from "./types.js";
12
+
13
+ export interface WopfsCommittedFile {
14
+ path: string;
15
+ size: number;
16
+ handle?: FileSystemFileHandle;
17
+ hostPath?: string;
18
+ bytes?: Uint8Array;
19
+ }
20
+
21
+ export interface WopfsMount {
22
+ readonly backend: "opfs" | "node" | "memfs";
23
+ commit(): Promise<WopfsCommittedFile[]>;
24
+ release(): Promise<void>;
25
+ }
26
+
27
+ export interface WopfsMountOptions {
28
+ inputs?: Record<string, FileSystemFileHandle | Uint8Array | Blob | string>;
29
+ outputDir?: FileSystemDirectoryHandle | string;
30
+ scratch?: number;
31
+ onExhausted?: "spill" | "strict";
32
+ backend?: "opfs" | "node" | "memfs" | "auto";
33
+ }
34
+
35
+ export interface WopfsModule {
36
+ mountFiles(module: unknown, mountpoint: string, options?: WopfsMountOptions): Promise<WopfsMount>;
37
+ }
38
+
39
+ const DEFAULT_SPECIFIER = "@wasmedia/wopfs";
40
+
41
+ // The default is a LITERAL import so a bundler resolves and code-splits it (it
42
+ // stays lazy: nothing evaluates it until the first file-mode call) and Node
43
+ // resolves it from node_modules. An explicit wopfsPath is a URL the bundler
44
+ // cannot know, so it goes through a variable specifier.
45
+ let importer: (specifier: string) => Promise<WopfsModule> = (specifier) =>
46
+ (specifier === DEFAULT_SPECIFIER
47
+ ? import("@wasmedia/wopfs")
48
+ : import(/* @vite-ignore */ specifier)) as Promise<WopfsModule>;
49
+ const cache = new Map<string, Promise<WopfsModule>>();
50
+
51
+ /** Load wopfs once per specifier. A failed load is not cached, so a retry can succeed. */
52
+ export function loadWopfs(options?: LoaderOptions): Promise<WopfsModule> {
53
+ const specifier = options?.wopfsPath ?? DEFAULT_SPECIFIER;
54
+ let loading = cache.get(specifier);
55
+ if (!loading) {
56
+ loading = importer(specifier).catch((e: unknown) => {
57
+ cache.delete(specifier);
58
+ const why = e instanceof Error ? e.message : String(e);
59
+ throw new Error(`file mode needs @wasmedia/wopfs, which could not be loaded from "${specifier}": ${why}`);
60
+ });
61
+ cache.set(specifier, loading);
62
+ }
63
+ return loading;
64
+ }
65
+
66
+ /** Test seam: replace the importer. Clears the cache. */
67
+ export function __setWopfsLoader(fn: (specifier: string) => Promise<WopfsModule>): void {
68
+ importer = fn;
69
+ cache.clear();
70
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Dedicated Web Worker entry — the package's `./worker` export.
3
+ *
4
+ * Blending is CPU-bound and single-threaded (OpenMP is off), so it
5
+ * blocks whatever thread it runs on. Running it in a worker keeps the
6
+ * page responsive.
7
+ *
8
+ * This is the ONLY module in the package with an import-time side
9
+ * effect: loaded inside a worker, it registers a `message` listener.
10
+ * The main entry (`src/index.ts`) re-exports the handler from
11
+ * `./handler.js`, never from here, so importing the library inside a
12
+ * consumer's own worker attaches nothing. package.json's `sideEffects`
13
+ * field names this file alone so bundlers know the difference.
14
+ */
15
+
16
+ import { handleMessage, isWorkerScope, type WorkerRequest } from "./handler.js";
17
+
18
+ export { handleMessage, isWorkerScope, type WorkerRequest, type WorkerResponse } from "./handler.js";
19
+
20
+ // Only register when actually running inside a worker, so importing this
21
+ // entry in a test or on a main thread is harmless.
22
+ if (isWorkerScope()) {
23
+ self.addEventListener("message", (event: MessageEvent<WorkerRequest>) => {
24
+ void handleMessage(event.data).then((response) => {
25
+ // Transfer the output buffer rather than copying it (buffer mode only).
26
+ const transfer = response.result && response.result instanceof Uint8Array ? [response.result.buffer] : [];
27
+ (self as unknown as { postMessage: (m: unknown, t: Transferable[]) => void })
28
+ .postMessage(response, transfer as Transferable[]);
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Types for the raw Emscripten glue, exported as `@wasmedia/wenblend/wasm/enblend`
3
+ * and `@wasmedia/wenblend/wasm/enfuse`. Each default-exports its tool's module
4
+ * factory. Prefer `createInstance()`, which also supplies the cached compiled
5
+ * module; calling the factory directly compiles the .wasm on every call, and
6
+ * `print`/`printErr` must be passed in the config object (assigning them after
7
+ * construction has no effect).
8
+ */
9
+ import type { ModuleFactory } from "../dist/loader.js";
10
+
11
+ declare const factory: ModuleFactory;
12
+ export default factory;