Documentation/Worker
class

Worker

js/worker.js:1
Worker(source[, name])
Worker({ file }[, name])

Runs JavaScript in a separate JSplitter Worker realm and Worker thread.
JSplitter Workers use a Web-Worker-inspired programming model, but this page describes the actual JSplitter behaviour and should be treated as the primary guide. A Worker has its own global scope and event loop, communicates with its parent panel through structured-clone messages, and has no direct access to the panel UI.

Why use a Worker?

A panel script shares its thread with panel/UI work. Expensive JavaScript, large metadata aggregation, image processing, or repeated frame preparation performed there can make the interface less responsive. A Worker moves suitable work to another thread and lets the panel remain focused on interaction and presentation.

Panel realm Worker realm UI / callbacks own JS realm + event loop | | | new Worker(source / { file }) | |------------------------------------->| | | script starts | postMessage(data) | |------------------------------------->| process / render / I/O | | | postMessage(result) | |<-------------------------------------| | repaint / present result | | | | terminate() close() ----|

Creating a Worker

The Worker constructor has two explicit startup forms:

  • new Worker(source[, name]) — evaluates JavaScript source text supplied by the panel script.
  • new Worker({ file: 'path/to/worker.js' }[, name]) — reads and evaluates a JavaScript file.

In the file-backed form, file may be an absolute or relative path. Relative paths use the caller/package/component search roots, and the resolved file becomes the Worker's startup script origin.

The optional second name argument has the same meaning in both startup forms: it is the Worker's immutable identity for its lifetime. Supply it when the Worker is created; inside the Worker the same value is exposed through read-only self.name. JSplitter also uses this exact name in window.JsMemoryStats (Workers[].Name) and as the first diagnostic metadata line of every unhandled Worker exception (see below). Giving long-lived or multiple concurrent Workers short descriptive names therefore makes both memory inspection and failures much easier to identify.

Here are the examples of Worker creation:

const inlineWorker = new Worker(`
    include('workers/main.js');
`,
'inline-worker');

const fileWorker = new Worker(
    { file: 'workers/main.js' },
    'file-worker'
);

This also means an inline Worker based on source text can be any piece of executable JavaScript code or only a tiny bootstrap that include()s ordinary files, while a file-backed Worker can start from the same file directly. Relative paths are resolved from the currently executing script file first when one exists, so a file-backed workers/main.js can include('./helpers.js'); otherwise the Worker's inherited script/package roots and component path are used.

Source text passed to Worker is ordinary JavaScript. JSplitter evaluates it in a new Worker realm running on its own thread:

const worker = new Worker(`
    const value = 21 * 2;
    console.log('Worker result: ' + value);
`, 'example-worker');

The code inside the string runs in the Worker, not in the panel. It can use the APIs available to Worker code just as ordinary panel JavaScript can use panel APIs.

This example is intentionally not very useful: it performs one calculation and does not communicate with the panel. The next section explains an important consequence of this execution model: the Worker remains alive even after that startup code reaches its end. Later sections show how messaging turns it into a useful long-lived Worker.

Worker lifetime and the event loop

Reaching the end of the Worker's initial source does not mean that the Worker has finished. In the example above, execution reaches the end immediately after console.log(), but the Worker itself remains alive. JSplitter keeps it running in its event loop. In this documentation, event loop simply means the Worker waits for the next unit of work and dispatches it when it arrives: a message from the panel, a timer, a Worker-capable asynchronous host completion, observer delivery, or Promise work associated with a task. When there is nothing to do, the Worker waits; it does not repeatedly execute the initial source and it does not need a user-written loop.

This also means that processing one message does not end the Worker. The same Worker can receive many later messages through the same onmessage handler with one MessageEvent parameter. If the panel stays loaded and keeps the Worker alive, and neither side closes it, the Worker remains alive waiting for more work. It is therefore important to end Workers that are no longer needed rather than assuming that returning from the startup script or from a message handler stops them.

There are two normal ways to end a Worker:

  • The parent panel calls terminate() when it decides that the Worker is no longer needed.
  • Worker code calls Worker-global close() when the Worker itself knows that its work is finished. New work is no longer accepted, the current task is allowed to finish, and then the event loop exits.

Next example shows typical worker <-> panel messaging:

const worker = new Worker(`
onmessage = function (event) {
    if (event.data === 'stop') {
        // WORKER: the Worker decides that it has finished.
        postMessage('finished');
        close();
        return;
    }

    postMessage('processed: ' + event.data);
};
`);

// PANEL: receives messages sent by postMessage() inside the Worker here
worker.onmessage = function (event) {
    console.log('Worker says: ' + event.data);
};

// PANEL: sends message to worker
worker.postMessage('hello');
// -> Worker says: processed: hello

// If Worker is no longer needed then:

// Option A: ask the Worker to finish through its own message protocol
worker.postMessage('stop');
// -> Worker says: finished

// Option B: stop Worker directly from the outside instead.
// Calling terminate() here is also safe, after the Worker has already called close()
// worker.terminate();

JSplitter automatically terminates all Workers still owned by a panel when that panel is unloaded, so explicit cleanup is not required merely for panel teardown. Explicit terminate() is for ending a Worker earlier, while the panel continues to live. Conversely, while the panel remains loaded, a Worker that is still kept alive and is never closed or terminated will remain waiting in its event loop and will continue to hold its Worker realm and associated resources.

Do not treat a Worker as a one-shot function call.
If the panel keeps a Worker alive after its useful work is finished, returning from the Worker source or from its last message handler does not dispose it. Call terminate() when the panel is done with it, or design the Worker protocol so Worker-global close() is called when the Worker knows it is finished.

Handling Worker errors

An exception that is not caught by Worker code and escapes Worker startup code, a Worker message handler, a timer callback, or another Worker task is reported as an ErrorEvent. A normal try...catch handles the exception locally and prevents this error-reporting path from being used.

For uncaught exceptions, Worker-local handling comes first: if Worker-global onerror or an error listener is installed, the error is handled there and is not forwarded to the parent. Otherwise it is forwarded to the parent side, where Worker.onerror or an error listener can handle it.

worker.onerror = function (event) {
    console.log('Worker error: ' + event.message);
    console.log('Source: ' + event.filename);
    console.log('Location: ' + event.lineno + ':' + event.colno);
};

If the error reaches the parent and no parent error handler is installed, JSplitter shows the normal panel-script error UI. A named inline Worker can produce a diagnostic like this:

z is not defined

Worker: spectrum-renderer
File: worker.js
Line: 42, Column: 5
Stack trace:
  scheduleNext@worker.js:42:5
  onmessage@worker.js:68:3

Read it from top to bottom:

  • Error messagez is not defined is the exception text.
  • Worker: — the immutable Worker name supplied at construction time. An unnamed Worker shows <unnamed>.
  • File: — the Worker source name. Inline Workers use worker.js; file-backed Workers use the startup script basename.
  • Line / Column: — the original location of the exception in Worker code.
  • Stack trace: — the original Worker call chain when SpiderMonkey provides it.

The resolved full path of a file-backed Worker is retained internally for relative file resolution, but diagnostics intentionally use the script basename to match ordinary panel-script diagnostics.

A parent-side error handler can also deliberately turn a Worker failure into a normal panel-script exception by throwing. The event parameter below is the incoming ErrorEvent, so the original Worker location is available directly on that object:

worker.onerror = function (event) {
    console.log('Original Worker location: ' +
        event.filename + ':' + event.lineno + ':' + event.colno);

    throw new Error('Worker failed: ' + event.message);
};

Sending structured data

Messages are not limited to strings. Worker.postMessage() and Worker-global postMessage() use structured-clone semantics: the sender serializes the value and the receiver reconstructs its own independent value. The two realms do not share the same JavaScript object.

const worker = new Worker(`
onmessage = function (event) {
    const request = event.data;

    if (request.command === 'sum') {
        const total = request.values.reduce((a, b) => a + b, 0);

        // WORKER: send a structured result object back to the panel.
        postMessage({
            command: 'sum-result',
            total: total
        });
    }
};
`);

worker.onmessage = function (event) {
    console.log(event.data.command); // sum-result
    console.log(event.data.total);   // 10

    // PANEL: this one-shot example has its result, so it no longer needs
    // the Worker to remain alive waiting for more messages.
    worker.terminate();
};

// PANEL: the object and nested array are cloned for the Worker.
worker.postMessage({
    command: 'sum',
    values: [1, 2, 3, 4]
});

Ordinary JavaScript objects, arrays, maps, sets, typed arrays, ArrayBuffers and other supported structured-clone values can be sent this way. Selected JSplitter host wrappers can also cross the message boundary; the exact types and their ownership rules are listed later under Host objects in messages.

Transferring ownership

A transferable value uses the same message channel but a different ownership model. Normal structured clone leaves the source usable and creates a representation for the receiver. Transfer instead moves ownership of the underlying resource without cloning. After a successful transfer, the source wrapper is detached and must not be used again by the sender.

The syntax difference is the transfer list passed as the second argument. The transfer list is separate from the message payload. It identifies transferable objects contained anywhere in the payload that should have their ownership moved instead of being cloned.

// Clone: both objects remain usable in the panel.
worker.postMessage({
    bitmap: bitmap,
    chunk: audioChunk
});

// Transfer: the message payload is still the first argument.
// The transfer list only specifies which transferable objects inside that
// payload move to the Worker instead of being cloned.
worker.postMessage(
    {
        bitmap: bitmap,
        chunk: audioChunk,
        values: [1, 2, 3, 4]
    },
    [bitmap, audioChunk]
);

// After a successful transfer, bitmap and audioChunk are detached in the
// panel. Other values in the message, such as "values", are cloned normally.

Transfers are atomic with respect to the transfer list. A call to postMessage(value, transferList) either transfers every transferable object in the list or transfers none of them.

If the message cannot be serialized, or any object in the transfer list cannot be transferred, postMessage() fails and all source objects remain usable. No object in the list is detached.

After postMessage() successfully serializes the message and commits the transfer, all objects listed in the transfer list are detached in the sender.

A message is delivered only after it has been serialized successfully. If serialization and transfer succeed but the receiving realm cannot reconstruct one of the message values, that endpoint receives a messageerror event and remains usable for later messages. This is a receiver-side delivery failure: a completed transfer is not rolled back, and transferred source objects remain detached.

JSplitter adds two diagnostic fields to that MessageEvent: errorMessage tells you why reconstruction failed, while direction tells you whether the failed delivery was panel-to-worker or worker-to-panel. The original payload is not available because reconstruction did not complete, so data is undefined.

worker.onmessageerror = function (event) {
    // PANEL: a Worker result reached this panel but could not be reconstructed.
    console.log('Failed direction: ' + event.direction);
    console.log('Reason: ' + event.errorMessage);
};

Inside a Worker, Worker-global onmessageerror exposes the same fields; its direction is panel-to-worker. This is a delivery/structured-clone failure, not an uncaught Worker exception, so it is separate from onerror.

Host objects in messages

The examples above cover normal structured data and transfer syntax. JSplitter also defines explicit messaging rules for its native host wrappers. The lists below are the reference for which wrappers may cross between the panel and Worker realms.

Cloneable host objects. Sending one of these without a transfer list leaves the source object usable in the sender and creates an independent representation for the receiving realm.

Cloneable host objects

Transferable host objects. These are the JSplitter host wrappers that support the transfer-list ownership model demonstrated above. After a successful transfer the source wrapper is detached and no longer usable on the sender side; if serialization itself fails, the transfer is rolled back and the source remains usable.

Transfer support is intentionally limited to objects that own a significant dynamic resource whose ownership can be moved efficiently and unambiguously between realms. Smaller value-like objects are simply cloned, while shared or realm-bound resources are not made transferable.

Transferable host objects

All three types are also cloneable. Choose ordinary cloning when both realms still need a usable object. Transfer moves bitmap pixel storage or the FbAudioChunk sample buffer to the receiving realm without keeping a usable source wrapper.

Realm-local host objects. Other wrappers represent live services, graphics state or resources whose meaning is tied to the realm that created them. They can be used normally inside that realm, including inside a Worker when the relevant API is available there, but they cannot cross a structured-clone message boundary. Attempting to post one fails serialization rather than silently sharing the native object.

Realm-local host objects

MainMenuManager and ContextMenuManager are additionally panel-only because their factories are not exposed in Workers.

Panel realm versus Worker realm

A Worker is not another panel. It has no panel HWND, no window object, no panel graphics render target, and no panel UI/input callbacks such as on_paint, on_key_down or on_mouse_move. This does not mean Worker code has no callbacks at all: message/event handlers and completion callbacks belonging to Worker-capable asynchronous host APIs remain available where documented. A Worker has its own fb, plman, utils, gdi, console and performance namespace objects. When Direct2D is initialized before the Worker is created, d2d is available as well.

Direct2D versions earlier than 1.1 are not supported in the Worker.
Minimum OS requirements: Windows 8+ or Windows 7 Service Pack 1 with the Platform Update for Windows 7 installed.

These APIs are exposed in the Worker realm, but this does not mean that every operation runs on the Worker thread: methods marked MAIN THREAD cross to the main thread internally but, for example, GDI and Direct2D resources can be created and used by the Worker for offscreen rendering in its own thread.

The Worker environment implements the JSplitter subset of familiar Worker primitives: EventTarget, Event, MessageEvent, ErrorEvent, PromiseRejectionEvent, WorkerGlobalScope, timers and promises. The local JSplitter interface pages describe the supported surface and behaviour.

Worker API coverage

Most useful non-UI JSplitter API is available inside a Worker. Namespaces marked mostly available below expose the large majority of their normal surface; the relatively small set of exclusions is listed separately in APIs intentionally unavailable in Workers.

  • Core Worker environment — messaging, events, timers, promises, include() are available.
  • fbmostly available. Playback, library/selection data, title formatting, DSP/output state and most non-modal host operations are exposed.
  • plmanmostly available. Playlist manipulation, playback queue, sorting, undo/redo and the playlist recycler are exposed.
  • utilsmostly available. Filesystem, hashing, text/binary/INI, package/system information, HTML parsing, drives, image/album-art loading, process, HTTP and download helpers are exposed.
  • gdifully available as a DrawMode-aware Worker namespace. In GDI mode its factories create GDI+ resources; when the Worker is created from a Direct2D panel, the same gdi.* factories route to the Worker Direct2D backend and create the corresponding D2D resources.
  • d2dfully available as a Worker namespace when Direct2D is initialized before Worker creation, including effects, compile support and Worker-local offscreen rendering. Set window.DrawMode to 1 in the panel before creating a Worker that needs Direct2D.
  • consolefully available inside a Worker.
  • performancefully available inside a Worker.

APIs intentionally unavailable in Workers

Panel and browser globals. The panel window namespace, panel UI/input callbacks such as on_paint, on_key_down and on_mouse_move, tooltips, theme/HWND services and the panel graphics render target are not Worker concepts. In particular, the panel-provided GdiGraphics or D2DGraphics graphics context passed to UI painting callbacks is not available to Worker code. Also panel-only compatibility globals such as ActiveXObject and Enumerator, and nested Worker creation are also unavailable.

The mostly available namespaces listed above also omit a small number of operations that are modal, interactive, tied to live panel UI state, or otherwise unsuitable for a Worker:

fb
plman
utils

Main-thread host operations

Many APIs are callable from Worker code but ultimately use foobar2000 services that can only execute on the main thread (this is a foobar2000 SDK requirement). When the Worker calls one of these APIs, it waits synchronously until that host operation finishes. Occasional calls are fine; a tight loop of them defeats much of the reason for moving work to a Worker.

GOOD panel: send one useful batch | v worker: process / aggregate / render | v panel: receive compact result POOR worker: main thread -> main thread -> main thread -> ...
Avoid building a Worker around repeated main-thread calls.
If most of a Worker consists of frequent synchronous host calls, moving that code to a Worker will usually provide no performance benefit and can make it slower because of repeated cross-thread synchronization. If the main thread is busy, the Worker waits too.

The lists below show the Worker-accessible API that uses this synchronous main-thread bridge:

plman

EVERY Worker-exposed plman function is main-thread bridged. These live properties also have main-thread accessors:

Reading plman.PlaylistRecycler itself only obtains the Worker-local FbPlaylistRecycler wrapper; its operations are main-thread bridged.

fb

Functions

Properties

utils / console

utils

console

The console methods above use the foobar2000 main-thread console manager on foobar2000 2.0 and newer; the legacy JSplitter-owned console backlog path is thread-safe and direct.

Host wrapper methods

A small note on fb.GetAudioChunk: it's normally accesses the visualisation stream directly from the Worker but its first use may briefly synchronize with the main thread while that stream is initialized.

A Worker waiting for a bridge call can normally be terminated cleanly. Once a main-thread callback has already begun executing, however, it cannot be interrupted halfway through; that in-progress host operation must return before it can be fully unwound. Blocking or modal host APIs are therefore deliberately excluded from the Worker surface.

Capability badges and terms

The detailed API pages use four local badges for the concepts introduced above:

  • WORKER — this API item is available from Worker code. Absence of this badge means the item is not part of the documented Worker surface.
  • MAIN THREAD — as described in Main-thread host operations above, the item is callable from a Worker but its foobar2000 host operation executes synchronously on the main thread. The Worker waits for that operation to return, so repeated calls should be avoided in hot Worker loops.
  • CLONEABLE — instances of this host type can be sent through postMessage() using structured clone while the source remains usable.
  • TRANSFERABLE — instances can additionally be placed in the transfer list so ownership moves to the receiver; after a successful transfer the source wrapper is detached and no longer usable on the sender side.

Samples

The samples are intentionally ordered. Start with lifecycle/messaging, then move through batch data processing, CPU-bound work, asynchronous image processing and finally a real-time audio/render pipeline.

1. Basic Messaging. The smallest complete lifecycle example: create a named inline Worker, send a structured object, receive a result, install parent/Worker messageerror diagnostics and a parent error handler, then let the Worker finish with Worker-global close(). The unload handler also shows that parent terminate() is safe during panel teardown.

// PANEL: create and immediately start a named Worker from prepared source text.
const worker = new Worker(source, 'basic-messaging');

// PANEL: values sent by Worker-global postMessage(...) arrive here as event.data.
worker.onmessage = event => console.log(event.data);

// PANEL: send one object to this Worker. JSplitter structured-clones it before
// the Worker's onmessage handler receives its own reconstructed copy.
worker.postMessage({ values: [1, 2, 3, 4] });

2. Playlist Statistics. Presents Media Library and the existing playlists as an interactive source list. The panel obtains one FbMetadbHandleList, then the Main / Worker switch runs the same metadata and FbFileInfo aggregation either synchronously in the panel realm or on the cloned list in the Worker. The detail view reports calculation and end-to-end time, making the responsiveness/overhead trade-off visible on large real-world batches.

// Panel: obtain one source batch. Media Library uses fb.GetLibraryItems();
// playlists use plman.GetPlaylistItems(...).
const handles = source.library ? fb.GetLibraryItems() : plman.GetPlaylistItems(source.index);

// Worker mode: clone the complete list once and aggregate it off the panel thread.
worker.postMessage({ requestId, handles });

// Main mode runs the same analyseHandles(handles) function in the panel realm.

3. Fractal Renderer. Demonstrates a CPU-bound render job that belongs entirely in a Worker. Pan and zoom reuse one completed frame locally for immediate feedback; drag sends one final viewport on release, while wheel and resize bursts are coalesced for 80 ms. The Worker renders only the settled viewport, transfers the completed bitmap, and stale results are rejected by view id. The GDI / D2D switch recreates the Worker so the same gdi.* facade can be compared in both rendering modes.

4. Playlist Album Gallery. A practical artwork-loading A/B test. Main / Worker runs the same visible-cover pipeline either in the panel or in the Worker. In Worker mode the panel sends one cloneable FbMetadbHandleList; the Worker deduplicates it into albums, loads artwork with GetAlbumArtAsyncV2, resizes each image and transfers completed bitmaps back individually. Only the covers that fit in the current viewport are requested, changing cover size intentionally reloads them, and the sample has no artwork cache. Concurrency controls how many artwork requests may be in flight, while GDI / D2D selects the rendering backend. Targeted RepaintRect updates and timing counters make first display, loading, delivery and paint cost visible.

5. Spectrum Analyzer. The real-time A/B test. Main / Worker runs the same audio-read, FFT and offscreen-render pipeline on either thread. Both paths allow only one completed frame at a time; the next frame starts only after the previous bitmap reaches on_paint, so the counters measure work that can actually be presented. Additional switches select GDI / D2D, FFT size, and either a 120 FPS limiter or Max mode. The overlay reports frame/paint rate plus audio, FFT, render and total processing time. Max is useful for exposing fixed per-frame overhead, but the sample is not intended as a general GDI-versus-D2D benchmark.

// Worker: read a short audio window and turn it into spectrum data.
const chunk = fb.GetAudioChunk(0.06, -0.03);
analyse(chunk);

// Render a complete offscreen frame, then transfer its ownership to the panel.
const frame = renderSpectrum();
postMessage({ bitmap: frame, timing }, [frame]);
// frame is detached here after a successful transfer and must not be reused.

// Panel: keep one completed frame and request presentation. Do not acknowledge
// it yet: the Worker remains paused until this exact frame reaches on_paint.
worker.onmessage = function (event) {
    frame = event.data.bitmap;
    window.Repaint();
};

function on_paint(gr) {
    gr.DrawImage(frame, 0, 0, window.Width, window.Height, 0, 0, frame.Width, frame.Height);
    worker.postMessage({ type: 'next' });
}

Parameters

NameTypeDescription
source(string|Object)

JavaScript source text, or a file descriptor object such as { file: 'workers/main.js' }.

name = ""optionalstring

Optional immutable Worker identity exposed through read-only Worker-global name, reported by window.JsMemoryStats, and shown in unhandled Worker exception diagnostics.

Example sources

property

onerror

js/worker.js:620
onerror: ?WorkerErrorCallback = null

Receives uncaught Worker errors reported to the parent endpoint as an ErrorEvent. The callback argument exposes the error text through message and the original Worker source location through filename, lineno and colno. If no parent error handler exists, the Worker error is promoted to the normal panel-script error UI/failure path. Throwing from this handler likewise propagates as a normal panel script exception, whose stack begins in the parent handler.

property

onmessage

js/worker.js:613
onmessage: ?WorkerMessageCallback = null

Receives messages sent from the Worker through Worker-global postMessage().

property

onmessageerror

js/worker.js:629
onmessageerror: ?WorkerMessageCallback = null

Receives a MessageEvent when a serialized message cannot be reconstructed by the parent endpoint. errorMessage contains the reconstruction failure and direction is worker-to-panel.

method

postMessage

js/worker.js:637
postMessage(data, transfer)

Serializes and sends a value to the Worker.

Parameters

NameTypeDescription
data*

Value to send.

transferoptional(Array<*>|Object)

Transfer list, either directly as an array or as an object containing { transfer: [...] }.

method

terminate

js/worker.js:645
terminate()

Stops this Worker from the parent panel and releases pending Worker-side state. Use this when the panel no longer needs the Worker; Workers are terminated automatically when their parent panel is unloaded. A main-thread host callback that has already begun executing cannot be interrupted halfway through.