Actor-based concurrency with message passing in Arc
Arc brings Erlang’s actor model to JavaScript. Every call to Arc.spawn() creates a new BEAM process with isolated memory and message-passing communication.
Actors maintain private state using local variables and message loops:
var parent = Arc.self();var counter = Arc.spawn(() => { var count = 0; // Private state while (true) { var msg = Arc.receive(); if (msg.type === 'inc') { count = count + msg.n; Arc.log('counter: incremented by', msg.n, '-> now', count); } if (msg.type === 'dec') { count = count - msg.n; Arc.log('counter: decremented by', msg.n, '-> now', count); } if (msg.type === 'get') { Arc.send(msg.from, { type: 'value', count: count }); } if (msg.type === 'stop') { Arc.log('counter: stopping with final value', count); Arc.send(msg.from, { type: 'stopped', count: count }); return; // Exit the loop to terminate the process } }});Arc.send(counter, { type: 'inc', n: 10 });Arc.send(counter, { type: 'inc', n: 5 });Arc.send(counter, { type: 'dec', n: 3 });Arc.send(counter, { type: 'inc', n: 100 });Arc.send(counter, { type: 'get', from: parent });var result = Arc.receive(1000);Arc.log('Main got counter value:', result.count);Arc.send(counter, { type: 'stop', from: parent });var stopped = Arc.receive(1000);Arc.log('Counter stopped with:', stopped.count);
Output:
counter: incremented by 10 -> now 10counter: incremented by 5 -> now 15counter: decremented by 3 -> now 12counter: incremented by 100 -> now 112Main got counter value: 112counter: stopping with final value 112Counter stopped with: 112
The count variable is private to the counter process. The main process can only read it by sending a get message.
Message serialization is implemented in src/arc/vm/builtins/arc.gleam using serialize() and deserialize(). It converts JavaScript values to Erlang terms.
A classic Erlang concurrency demo — pass a token around a ring of N processes:
var N = 100; // number of processes in the ringvar M = 10; // number of laps around the ring// Build a ring of N processes. Each one receives a message and// forwards it to the next process in the ring.var first = Arc.self();var prev = first;var i = N;while (i > 0) { var next = prev; prev = Arc.spawn(() => { while (true) { var msg = Arc.receive(); if (msg.type === 'stop') { return; } Arc.send(next, { type: 'token', lap: msg.lap, hops: msg.hops + 1 }); } }); i = i - 1;}Arc.log('Ring of', N, 'processes created. Sending token for', M, 'laps...');// Send the token into the ringArc.send(prev, { type: 'token', lap: 0, hops: 0 });// Main process acts as the "first" node — receives and re-sendsvar lap = 0;while (lap < M) { var msg = Arc.receive(5000); if (msg === undefined) { Arc.log('Timed out waiting for token!'); lap = M; } else { lap = msg.lap + 1; Arc.log('Lap', lap, 'complete -', msg.hops, 'hops'); if (lap < M) { Arc.send(prev, { type: 'token', lap: lap, hops: 0 }); } }}Arc.log('Done!', N, 'processes x', M, 'laps =', N * M, 'total message passes');
Output:
Ring of 100 processes created. Sending token for 10 laps...Lap 1 complete - 100 hopsLap 2 complete - 100 hops...Lap 10 complete - 100 hopsDone! 100 processes x 10 laps = 1000 total message passes
This demonstrates spawning many lightweight processes and coordinating them with message passing.