Skip to content

@xmachines/play-signals

Documentation / @xmachines/play-signals

Canonical Signals substrate for XMachines with Stage 1 API isolation

@xmachines/play-signals re-exports Signal from signal-polyfill as the single import boundary for XMachines packages.

Why This Package Exists

  • Keep the raw Signal API as the canonical substrate surface.
  • Isolate Stage 1 proposal churn behind one package boundary.
  • Preserve Play invariants: Signal-only reactivity, passive infrastructure, and event-only mutation paths.

This package does not add business behavior to signals. Adapters and renderers observe signals and forward events; they do not mutate business state directly.

Installation

Terminal window
npm install @xmachines/play-signals

Current Exports

  • Signal (re-export from signal-polyfill)
  • Type exports from src/types.ts: SignalState, SignalComputed, SignalWatcher, SignalOptions, ComputedOptions, WatcherNotify

Quick Start

import { Signal } from "@xmachines/play-signals";
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);
const watcher = new Signal.subtle.Watcher(() => {
queueMicrotask(() => {
const pending = watcher.getPending();
for (const signal of pending) {
signal.get();
}
watcher.watch(...pending);
});
});
watcher.watch(doubled);
doubled.get();
count.set(2);
const dispose = () => {
watcher.unwatch(doubled);
};
void dispose;

Canonical Watcher Lifecycle

Use one lifecycle pattern everywhere (React, Vue, Solid, router bridges, helper wrappers):

  1. notify callback runs.
  2. Schedule work with queueMicrotask.
  3. Drain watcher.getPending().
  4. Perform reads/effects.
  5. Re-arm watcher with watch() or watch(...signals).

Watcher notifications are one-shot. If you do not re-arm, you will miss future updates.

Cleanup Contract

Always dispose explicitly. Do not rely on GC-only cleanup guidance.

  • If you called watch(...), call unwatch(...) in teardown.
  • Framework lifecycles (useEffect cleanup, onUnmounted, onCleanup) must unwatch.
  • Bridge lifecycles (disconnect, dispose) must unwatch and unsubscribe.

Optional Helper Direction

Raw Signal remains canonical. Helper APIs are optional, additive guidance for consistency:

  • watchSignals(signals, onChange, options)
  • createSignalEffect(effect, options)
  • toSubscribable(signal, options)

These helpers are intended to codify lifecycle-safe watcher scheduling and deterministic teardown. They do not replace direct Signal usage.

API Surface

  • Signal.State<T>: writable signal state (get, set)
  • Signal.Computed<T>: lazy memoized derivations
  • Signal.subtle.Watcher: low-level watcher (watch, unwatch, getPending)

Architecture Notes

  • Signal-Only Reactivity (INV-05): Signals are the reactive substrate.
  • Passive Infrastructure (INV-04): Adapters and frameworks only observe/forward.
  • Actor Authority (INV-01): Business validity and transitions stay in actors.
  • Event-only mutation path: Signals are not a business mutation channel.

Resources

License

Copyright (c) 2016 Mikael Karon. All rights reserved.

This work is licensed under the terms of the MIT license.
For a copy, see https://opensource.org/licenses/MIT.

TC39 Signals Polyfill for XMachines Play Architecture

Provides fine-grained reactive state primitives based on the TC39 Signals proposal (Stage 1). This package isolates the TC39 polyfill to protect the codebase from Stage 1 API changes.

Architectural Context: Implements Signal-Only Reactivity (INV-05) by providing the reactive primitives that enable Actor-to-Infrastructure communication without subscriptions or event emitters. All state propagation in Play Architecture uses TC39 Signals for automatic dependency tracking and glitch-free updates.

Example

Basic Signal usage

import { Signal } from "@xmachines/play-signals";
// Create state signal
const count = new Signal.State(0);
// Create computed signal
const doubled = new Signal.Computed(() => count.get() * 2);
// Observe changes
const watcher = new Signal.subtle.Watcher(() => {
console.log("Count:", count.get(), "Doubled:", doubled.get());
});
watcher.watch(count);
count.set(5); // Logs: Count: 5 Doubled: 10

See

Remarks

Stage 1 Status: TC39 Signals is currently Stage 1 in the TC39 process. This package uses the official signal-polyfill reference implementation to isolate the codebase from potential API changes as the proposal evolves. All signal imports should go through this package to maintain isolation.

Why Isolation: By re-exporting the polyfill through this dedicated package, we can update the polyfill version or adapt to API changes in one place without touching consuming packages. This architectural decision protects against Stage 1 API churn.

Namespaces

Interfaces

Type Aliases