The Model

Every value is owned by one thread, shared read-only, or a cell built for sharing.

A thread is a worker with its own JavaScript heap, and objects can't cross between heaps. That's why every value is one of three kinds:

Kind In the types How it crosses threads Use it for
Owned Ordinary types, and Sending<T> where it moves It moves. It's copied once, and the sender loses it. Work a thread does alone, and its result
Shared Shared<T>, which is deeply readonly It's frozen. Every thread reads its own copy, made once. Tables, settings and lookups
Cell struct(...), SharedArray, SharedMap, Lock, Channel It doesn't. There's only one copy, and it's in shared memory. Data many threads read and write in place, and what orders them

Code inside a thread is plain JavaScript on plain objects, so it runs at full speed. The cost is at the crossings, and the types tell you where they are.

import {log, share, spawn, type Sending, type Shared} from '@arv/threads';

type Order = {id: number; items: number[]};

// The order moves to the thread. The thread owns it now, and may change it.
function total(order: Sending<Order>): Sending<number> {
  order.items.push(5);
  return order.items.reduce((sum, x) => sum + x, 0);
}

// The rates are shared. They're frozen, and any thread can read them.
function convert(
  amount: number,
  rates: Shared<Record<string, number>>,
  to: string,
): Sending<number> {
  return amount * rates[to];
}

export function main() {
  const order = {id: 1, items: [10, 20, 30]};
  log(spawn(total, order).join()); // 65. Using `order` here is an error.
  const rates = share({USD: 1, EUR: 0.92, SEK: 10.5});
  log(spawn(convert, 100, rates, 'EUR').join());
}

Threads can also talk to each other the way Go does, with goroutines and channels: see Go-Style Concurrency. The tutorial shows what happens underneath when values cross between threads.

Docs