Real-Time Communication

Stream server events with $sse, exchange bidirectional messages with $websocket or $webTransport, and offload work with ng-worker and ng-wasm.

AngularTS provides five building blocks for real-time and compute-heavy work:

  • $sse for Server-Sent Events.
  • $websocket for bidirectional WebSocket connections.
  • $webTransport for HTTP/3 WebTransport sessions with datagrams and streams.
  • ng-worker for JavaScript work in a Web Worker.
  • ng-wasm for loading WebAssembly modules.

Exact service and connection signatures live in TypeDoc:

Server-Sent Events

$sse creates a managed EventSource connection. It handles query parameters, JSON message parsing, heartbeat detection, automatic reconnection, and clean shutdown.

class NewsFeedController {
  static $inject = ["$sse", "$scope"];

  items: NewsItem[] = [];
  private connection: ng.SseConnection;

  constructor($sse: ng.SseService, $scope: ng.Scope) {
    this.connection = $sse("/api/news/stream", {
      withCredentials: true,
      params: { category: "top" },
      retryDelay: 3000,
      heartbeatTimeout: 30000,
      onMessage: (item: NewsItem) => {
        const view = $scope["$ctrl"] as NewsFeedController;

        view.items = [item, ...view.items].slice(0, 50);
      },
      onReconnect: (attempt) => {
        console.log("SSE reconnect", attempt);
      },
    });

    $scope.on("$destroy", () => this.connection.close());
  }
}

EventSource does not support arbitrary custom headers in browsers. For authenticated streams, use cookies with withCredentials or include a short-lived token in query params.

WebSockets

$websocket returns a managed WebSocket connection with reconnects, heartbeat handling, message transforms, and send support.

class ChatController {
  static $inject = ["$websocket", "$scope"];

  messages: ChatMessage[] = [];
  private socket: ng.WebSocketConnection;

  constructor($websocket: ng.WebSocketService, $scope: ng.Scope) {
    this.socket = $websocket("wss://api.example.com/chat", {
      protocols: ["v1"],
      retryDelay: 2000,
      maxRetries: 20,
      onMessage: (message: ChatMessage) => {
        const view = $scope["$ctrl"] as ChatController;

        view.messages = [...view.messages, message];
      },
      onClose: (event) => {
        console.log("WebSocket closed", event.code);
      },
    });

    $scope.on("$destroy", () => this.socket.close());
  }

  send(text: string) {
    this.socket.send({ type: "message", text });
  }
}

send() serializes values as JSON before passing them to the native WebSocket.

WebTransport

$webTransport opens a browser-native WebTransport session. Use it when an endpoint can serve HTTP/3 and the client benefits from unreliable datagrams, reliable streams, or both in the same session.

class TelemetryController {
  static $inject = ["$webTransport", "$scope"];

  events: string[] = [];
  private session: ng.WebTransportConnection;

  constructor($webTransport: ng.WebTransportService, $scope: ng.Scope) {
    this.session = $webTransport("https://localhost:4433/webtransport", {
      reconnect: true,
      retryDelay: 500,
      maxRetries: 5,
      requireUnreliable: true,
      transformDatagram: (data) => new TextDecoder().decode(data),
      onDatagram: ({ message }) => {
        const view = $scope["$ctrl"] as TelemetryController;

        view.events = [...view.events, String(message)];
      },
      onReconnect: ({ connection }) => {
        return connection.sendText(JSON.stringify({ subscribe: "telemetry" }));
      },
    });
  }

  send(value: string) {
    return this.session.sendText(value);
  }
}

The service expects the browser WebTransport API to exist and requires an https: URL with an explicit port. The test backend exposes certificate hash metadata at /webtransport/cert-hash for local browser tests.

Reconnect is opt-in at the service layer. When enabled, the WebTransportConnection object stays stable while its native transport instance is replaced. Use onReconnect as the renegotiation hook for subscriptions, authentication messages, or other session state that the server does not remember across HTTP/3 sessions.

For template-level feeds, ng-web-transport connects on load by default and evaluates lifecycle expressions. data-mode="datagram" is the default; data-mode="stream" reads server-opened unidirectional streams.

<div
  ng-web-transport="transportUrl"
  data-config="transportConfig"
  data-mode="datagram"
  data-transform="json"
  data-as="session"
  data-reconnect="true"
  on-message="events.push($message)"
  on-reconnect="reconnects = $attempt"
  on-error="error = error"
></div>

data-transform accepts bytes, text, or json. Message expressions receive $connection, $data, $message, $event, and $text for text/json modes. Reconnect is opt-in with data-reconnect="true"; tune it with data-retry-delay and data-max-retries. on-reconnect runs after the replacement session is ready and receives $attempt, $connection, error, and $url.

Web Workers

Use ng-worker when a view action should run CPU-heavy JavaScript outside the main thread.

<button
  ng-worker="./workers/compress.js"
  data-params="vm.fileBuffer"
  on-result="vm.compressed = $result"
  on-error="vm.error = error"
  trigger="click"
>
  Compress
</button>

<span
  ng-worker="./workers/sensor-reader.js"
  interval="5000"
  on-result="vm.sensorData = $result"
></span>

The directive evaluates data-params, posts the result to the worker, and exposes worker responses as $result in on-result. Worker output is never interpreted as HTML. Bind it into scope or a model and let normal directives render the result.

Register expensive or shared workers once and reference their injectable handle. data-request enables correlated request-response delivery:

app.worker("compressWorker", "./workers/compress.js", {
  restart: true,
  maxRestarts: 3,
});
<button
  ng-worker
  data-handle="compressWorker"
  data-request
  data-params="vm.fileBuffer"
  on-result="vm.compressed = $result"
  on-error="vm.error = error"
>
  Compress
</button>

The directive subscribes to a shared handle but does not terminate it when the element scope is destroyed. Workers created directly from an ng-worker script path remain element-owned and are terminated with that scope.

A worker module receives values through self.onmessage and returns results through self.postMessage.

self.onmessage = function ({ data: { limit } }) {
  const sieve = new Uint8Array(limit + 1).fill(1);
  sieve[0] = sieve[1] = 0;

  for (let i = 2; i * i <= limit; i++) {
    if (sieve[i]) {
      for (let j = i * i; j <= limit; j += i) sieve[j] = 0;
    }
  }

  const primes = [];
  for (let i = 2; i <= limit; i++) {
    if (sieve[i]) primes.push(i);
  }

  self.postMessage(primes);
};

WebAssembly

ng-wasm loads a .wasm file and exposes its WasmResource on the scope under a configurable name. Await resource.ready before reading resource.exports.

<div
  ng-wasm
  src="/wasm/image-processor.wasm"
  as="imageProcessor"
></div>
const resource = $scope.imageProcessor;
await resource.ready;
const result = resource.exports.grayscale(pixelBuffer, width, height);

WebAssembly loading is asynchronous. Use the resource status in templates and await ready before calling exports from application code.

Choosing A Transport

NeedUse
Server pushes one-way updates$sse
Client and server both send messages$websocket
HTTP/3 datagrams or streams$webTransport
CPU-heavy JavaScriptng-worker
Compiled compute moduleng-wasm