<!-- generated-by: gsd-doc-writer -->

# @xmachines/play-signals

TC39 Signals polyfill for XMachines — fine-grained reactive state primitives that enable glitch-free, subscription-free state propagation in the Play Architecture.

Part of the [xmachines-js monorepo](../../README.md).

## Installation

```bash
pnpm add @xmachines/play-signals
```

## Overview

This package wraps the [`signal-polyfill`](https://github.com/nicolo-ribaudo/tc39-proposal-signals-polyfill) reference implementation of the [TC39 Signals proposal](https://github.com/tc39/proposal-signals) (Stage 1). It re-exports the full `Signal` namespace and adds a memory-safe `watchSignal` utility, isolating the rest of the codebase from potential Stage 1 API churn.

**All signal imports in the XMachines ecosystem should come from this package**, not directly from `signal-polyfill`, so that polyfill version bumps or API adaptations can be made in one place.

## Usage

### `Signal.State` — writable reactive state

```typescript
import { Signal } from "@xmachines/play-signals";

const count = new Signal.State(0);

console.log(count.get()); // 0
count.set(5);
console.log(count.get()); // 5
```

### `Signal.Computed` — lazy memoized derived values

```typescript
import { Signal } from "@xmachines/play-signals";

const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);

console.log(doubled.get()); // 0  (computed on first access)
count.set(5);
console.log(doubled.get()); // 10 (recomputed because dependency changed)
console.log(doubled.get()); // 10 (memoized — no recomputation)
```

Computations automatically track every signal accessed inside them. Dynamic branching is fully supported — only signals read in the _current_ execution path are tracked as dependencies.

### `watchSignal` — memory-safe one-shot effect

Use `watchSignal` to subscribe to a `Signal.State` or `Signal.Computed` and receive its value after each change. Updates are coalesced into a single microtask per synchronous batch.

```typescript
import { Signal, watchSignal } from "@xmachines/play-signals";

const count = new Signal.State(0);

const cleanup = watchSignal(count, (value) => {
	console.log("count changed:", value);
});

count.set(1); // → logs "count changed: 1" (via microtask)
count.set(2); // coalesced with any rapid synchronous changes
count.set(3); // → logs "count changed: 3" once

// Stop watching
cleanup();
```

The returned cleanup function is idempotent — calling it multiple times is safe and will not throw.

### `Signal.subtle.Watcher` — low-level multi-signal observation

For advanced use cases such as framework integrations, the full `Signal.subtle.Watcher` API is available:

```typescript
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();
		console.log("signals changed:", pending.length);
		watcher.watch(...pending); // re-arm for future changes
	});
});

watcher.watch(count);
watcher.watch(doubled);

count.set(5); // schedules microtask notification
```

### Custom equality

Both `Signal.State` and `Signal.Computed` accept an `equals` option to control when dependents are notified:

```typescript
import { Signal } from "@xmachines/play-signals";
import type { SignalOptions } from "@xmachines/play-signals";

const options: SignalOptions<{ name: string; age: number }> = {
	equals: (a, b) => a.name === b.name && a.age === b.age,
};

const person = new Signal.State({ name: "Alice", age: 30 }, options);
// Setting structurally identical value will not notify dependents
person.set({ name: "Alice", age: 30 });
```

## API Summary

| Export                         | Kind      | Description                                                                                            |
| ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------ |
| `Signal`                       | namespace | Full TC39 Signals namespace (`State`, `Computed`, `subtle.Watcher`) re-exported from `signal-polyfill` |
| `watchSignal(signal, onValue)` | function  | Memory-safe subscription helper; returns a cleanup function                                            |
| `SignalState<T>`               | interface | Shape of `Signal.State<T>` (`.get()`, `.set()`)                                                        |
| `SignalComputed<T>`            | interface | Shape of `Signal.Computed<T>` (`.get()`)                                                               |
| `SignalWatcher`                | interface | Shape of `Signal.subtle.Watcher` (`.watch()`, `.unwatch()`, `.getPending()`)                           |
| `SignalOptions<T>`             | interface | Options bag for `Signal.State` constructor (`equals?`)                                                 |
| `ComputedOptions<T>`           | interface | Options bag for `Signal.Computed` constructor (`equals?`)                                              |
| `WatcherNotify`                | type      | Callback signature for `Signal.subtle.Watcher` notify function                                         |

## Testing

Run tests for this package in isolation:

```bash
# From this package directory
pnpm test

# Watch mode
pnpm test -- --watch
```

From the monorepo root:

```bash
# Run tests for this package
pnpm --filter @xmachines/play-signals test

# Run with coverage (lines ≥ 90 %, functions ≥ 90 %, branches ≥ 85 %, statements ≥ 90 %)
pnpm run test:coverage
```

## Requirements

- **Node.js** `>= 22.0.0`
- **TypeScript** `5.7+` (for consumers using TypeScript)

## License

MIT — see [LICENSE](LICENSE).
