Skip to content

Architecture ​

Hybrid Compute is a TypeScript monorepo of four npm packages, managed with npm workspaces. Each package is an ES module with its own version, changelog and release.

Packages and how they depend on each other ​

core defines the interfaces and the orchestrator. Each backend implements ComputeBackendInterface from core, and nothing else depends on a backend, so an application installs core plus only the backends it uses.

PackageContainsRuntime dependencies
coreHybridCompute and createHybridCompute, the strategies, the shared interfaces, NoBackendErrorNone
localLocalCompute: runs registered tasks on the calling threadcore
workerThreadedCompute: runs tasks in a Web Worker through postMessagecore
remoteRemoteCompute: sends tasks to an endpoint over fetch or WebSocketcore
The packages and their dependencies The local, worker and remote packages each depend on core and implement its ComputeBackendInterface. Core has no runtime dependencies. @hybrid-compute/core HybridCompute · interfaces @hybrid-compute/local LocalCompute @hybrid-compute/worker ThreadedCompute @hybrid-compute/remote RemoteCompute
Arrows point to what a package depends on. Each backend implements ComputeBackendInterface from core; core imports no backend.

How a task is dispatched ​

How runTask reaches a backend The application calls runTask on core. With the local, worker or remote strategy, core uses that backend if it is configured and its canRun returns true. With auto, it uses the first backend whose canRun returns true, in the order worker, local, remote. If no backend matches, runTask rejects with a NoBackendError. Application runTask(name, input, strategy) core local · worker · remote that backend, if it is setand canRun(name) is trueautothe first backend whosecanRun(name) is true,in order: worker, local,then remoteno matchrejects with NoBackendError auto 1st auto 2nd auto 3rd worker Web Worker · postMessage local same thread remote fetch or WebSocket NoBackendError nothing can run the task
Every backend returns a Promise of the task's output.

runTask(taskName, input, strategy = 'auto') on core picks the backend:

  • local, worker or remote: that backend runs the task, if it is configured and its canRun(taskName) is true.
  • auto: the first backend whose canRun(taskName) is true, in the order worker, local, remote.
  • No match: runTask rejects with a NoBackendError, which has the taskName and strategy as properties.

Invalid input fails early: createHybridCompute throws a TypeError or RangeError for a missing, misspelt or incomplete backend, and runTask rejects with one for an empty task name or an unknown strategy.

A backend's canRun answers differently per package:

BackendcanRun(taskName) is true when
locala task with that name was registered with registerTask
workerthe name is in canRunTasks, and the runtime has Web Workers
remotethe name is in canRunTasks, or canRunTasks is '*' (accepts all)

Message formats ​

The backends that cross a boundary use small JSON messages. The id matches a result to its call, so several tasks can be in flight at once.

BackendSendsReceives
workerpostMessage({ id, task, input }), { id, cancel: true }{ id, result, error? }
remote (fetch)POST to the endpoint, JSON body { task, input }JSON { result, error? }
remote (WebSocket){ task, input, id } on a socket opened by the first task{ id, result, error? }

An error in the reply rejects the task's Promise. For remote, every failure rejects with a RemoteTaskError: an error reply, a non-2xx fetch status, a reply that isn't usable, a connection that fails or closes, and the timeout (30 seconds by default). For worker, every failure rejects with a WorkerTaskError: an error reply, a reply that isn't usable, a worker that crashes or doesn't load, and the timeout (none by default). Any task can also be cancelled with an AbortSignal passed to runTask; worker then sends { id, cancel: true } so the worker script can stop. The worker script and the remote service are yours to write (exposeTasks writes the worker side for you); see the worker and remote guides.

A task through the worker backend Your code calls runTask on ThreadedCompute, which posts a message with the task, input and an id to your worker script. The worker script posts back the id with a result or an error, and ThreadedCompute resolves or rejects the Promise. Your code main thread ThreadedCompute @hybrid-compute/worker Worker script your file runTask('resize', input) postMessage({ task, input, id }) { id, result } or { id, error } resolve(result) or reject(error)
The id matches each reply to its call, so several tasks can be in flight at once.
The remote backend's two transports With fetch, RemoteCompute posts the task and input as JSON to the endpoint you configured, and reads a result or an error from the response. With WebSocket, it sends the task, input and an id on a socket it opens for the first task and reuses, and matches replies with the same id, carrying a result or an error. Both reject with a RemoteTaskError on failure, and time out after 30 seconds by default. transport: 'fetch' RemoteCompute runTask(name, input) POST { task, input } { result, error? } Your endpoint the URL you configured transport: 'websocket' RemoteCompute runTask(name, input) send { task, input, id } { id, result, error? } Your endpoint socket opened by the first task
An error in the reply, a failed request or the timeout (30 seconds by default) rejects the task with a RemoteTaskError.

Design ​

  • Small and explicit. Tasks are registered and dispatched by name. There is no scheduling, queueing or retrying; an application adds those around runTask if it needs them.
  • Backends are interfaces. Anything implementing canRun and runTask can be passed to createHybridCompute, which also makes backends easy to replace with test doubles.
  • No hidden I/O. Only remote uses the network, and only to the endpoint it was given. What can and can't be relied on is in the security policy.

Repository layout ​

PathWhat's there
packages/<name>/srcEach package's source, with its tests in __tests__
scriptsRelease planning and checks, with their tests in __tests__
docsThis site (VitePress); docs/api is generated by TypeDoc at each release
.github/workflowsChecks, dependency review, Scorecard, publish and docs deploy

Build, test and release ​

  • Build: npm run build runs tsc -b with project references, building core first. Each package's tsconfig.build.json builds the published dist without tests; tsconfig.json type-checks the tests too.
  • Checks: every pull request is built, linted, type-checked and tested, and its API docs are checked, by the Checks workflow. Dependency review, the DCO sign-off and CodeQL must pass as well before it can merge.
  • Release: merging a pull request runs the publish workflow. It releases only the packages with releasable changes, in dependency order, publishes them to npm with trusted publishing and provenance, and attaches a software bill of materials to each GitHub release. Which commits release what is in the development guide.