Angular — quickstart
A minimal counter in Angular: one shared field, one worker method, one component. Steps 2–4 are the same for every framework — only the last step differs.
1 · Install
npm install @atolljs/core @atolljs/angular2 · Declare shared memory — imported by both threads
import { defineSharedMemory, field } from '@atolljs/core';
export const counterMemory = defineSharedMemory({
count: field.number(),
});3 · Define the worker — methods live here
import { defineWorker } from '@atolljs/core';
import { counterMemory } from './counter.memory';
// defineWorker wires the message loop and registers every method.
// Plain functions type the client from their signature.
export const counterWorker = defineWorker({
sharedMemory: counterMemory,
methods: {
increment(delta: number) {
const next = counterMemory.count.read() + delta;
counterMemory.count.write(next); // write in place — no postMessage
return next;
},
},
});
export type CounterWorker = typeof counterWorker;4 · Connect from the main thread — type only
import { connectWorker } from '@atolljs/core';
import { counterMemory } from './counter.memory';
import type { CounterWorker } from './counter.worker'; // no worker code in this bundle
export const counter = connectWorker<CounterWorker>({
sharedMemory: counterMemory,
// Inline new Worker(new URL(..., import.meta.url)) — every bundler's
// worker transform can see the entry point this way.
worker: () => new Worker(new URL('./counter.worker.ts', import.meta.url), { type: 'module' }),
poolSize: 'auto', // navigator.hardwareConcurrency, or pass a number
});
// counter.increment(1) → Promise<number>. The pool spawns on first call
// (SSR-safe to import); counter.terminate() tears it down.5 · Bind it in Angular
provideAtoll registers the client as a DI provider (terminated on app teardown); the signal factories must run in an injection context.
// main.ts — the connectWorker client registers as a DI provider,
// terminated on app teardown (it re-spawns lazily on the next call)
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
provideAtoll({ pools: [{ name: 'counter', client: counter }] }),
],
});
// app.component.ts
import { Component, effect } from '@angular/core';
import { injectAtollPool, sharedValue, taskState } from '@atolljs/angular';
import { counterMemory } from './counter.memory';
import { counter } from './counter';
@Component({
selector: 'app-root',
template: `<button (click)="increment.run(1)">
count: {{ count() ?? '…' }}</button>`,
})
export class AppComponent {
private readonly counter = injectAtollPool<typeof counter>('counter');
count = sharedValue(counterMemory, 'count');
increment = taskState(this.counter.increment);
constructor() {
effect(() => console.log('count →', this.count()));
}
}With shared memory: cross-origin isolation
SharedArrayBuffer only exists when the page is cross-origin isolated — see Hosting & headers for the COOP/COEP setup. Leave sharedMemory out of both defineWorker and connectWorker and you have a typed, pooled worker RPC that needs neither header.