# Architecture

Source: https://signy.dev/documentation/1/overview/architecture/

What runs when, from a signal write to the DOM.

This page is for contributors, library authors and anyone chasing a timing problem. [How Signy works](https://signy.dev/documentation/1/overview/how-signy-works/index.html.md) is the short version. File names refer to [`src/` in the signy repo](https://github.com/alopia/signy/tree/main/src).

## Layers

Each layer only uses the ones above it.

| Layer | Entry point | Folder | What it holds |
| --- | --- | --- | --- |
| Reactivity | `signy/core` | `reactivity/` | Signals, computeds, effects, owners, the scheduler |
| DOM and views | `signy/web` | `dom/`, `views/` | `html`, `h()`, bindings, events, control flow |
| Components | `signy/web` | `component/`, `context/` | `Component`, lifecycle, context, Shadow DOM |
| Application | `signy/app` | `app/` | `application()`, plugins, app level values, config |
| Router and store | `signy/router`, `signy/store` | `router/`, `reactivity/store.ts` | Built on the layers above |
| Dev features | all | `core/features.ts`, `log/` | Hooks for `signy-dev`, log ids |

## Signals and computeds

A `Signal` (`reactivity/signal/state.ts`) is a class close to TC39 `Signal.State`: a value, a set of subscribers and a version number that goes up on every change. Reading `.value` while a computation runs adds that computation to the subscribers. One object per signal, no closures, which keeps memory low for lists of thousands of rows.

A `Computed` (`reactivity/signal/computed.ts`) pulls. A write marks it stale, and the next read checks the versions of its sources and recomputes only when one moved. It caches errors like values and detects cycles. A computed nobody watches does not stay subscribed to its sources, so it can be garbage collected.

## The scheduler

Writes are synchronous: the value changes and computeds are marked at once, so the next line reads fresh values. The rest is queued (`reactivity/signal/scheduler.ts`):

| Work | Created by | First run | Re-runs |
| --- | --- | --- | --- |
| User effect | `effect()` | Microtask | Effect queue, microtask |
| Render effect | Template bindings, `createRenderEffect()` | Right away, so templates return finished DOM | DOM task queue, microtask |
| Sync effect | `createSyncEffect()` | Right away | Right after the write, or at the end of a `batch()` |

A click handler that writes two signals runs like this:

```
click handler
  a.value = 1        value changes, computeds marked, effects queued once
  b.value = 2        value changes, the same effects are already queued
handler returns
microtask
  effects run once, reading a = 1 and b = 2
  bindings re-run, the DOM changes once
browser paints
```

- Each effect runs at most once per flush, however many of its sources changed.
- `flushAll()` drains notifications, effects and DOM tasks in a loop until nothing is left. `flushSync()` only delivers notifications.
- An effect that keeps triggering itself is stopped after 100 rounds, with an error in the log.
- An effect that throws is logged, or handed to the nearest `ErrorBoundary`, and the flush goes on.
- Transitions use the same microtask, so they run before the paint too. See [Transitions](https://signy.dev/documentation/1/reactivity/transitions/index.html.md).

## Ownership

Every effect belongs to the owner that was current when it was created: a component, a root from `createRoot()`, a control flow branch or a list row (`reactivity/effect.ts`). Owners form a tree, with the children of each owner in a linked list, so removing one takes constant time.

- Disposing an owner disposes its children, runs their cleanups in reverse order and unsubscribes them from their sources.
- Before an effect re-runs, it disposes what its previous run created. That is why a binding that returns markup rebuilds it, and why `onCleanup()` runs before each re-run.
- An effect created with no owner, at the top of a module, lives until you dispose it.
- An error thrown by an effect goes up the tree to the nearest error handler. `ErrorBoundary` installs one.

## Templates

`html` (`views/html.ts`) parses a template with [htm](https://github.com/developit/htm) the first time it runs and builds its static elements once. Each later call clones them and fills in the values, so a list row costs one `cloneNode()`. A template with a component tag, a spread, or a tag or `xmlns` from a value goes through `h()` (`views/hyperscript.ts`) instead.

- A plain value becomes a text node or an attribute, set once.
- A function becomes a render effect bound to one text node or one attribute.
- Control flow blocks sit between comment markers, so they can swap their nodes without a wrapper element.

## Events

For 22 common bubbling events, `onClick` and its kind store the handler on the element, and one listener per event type on the document finds it by walking `event.composedPath()` (`views/events.ts`). That path crosses open shadow roots, and a closed shadow root gets a listener of its own. Every other event, and every `on:` attribute, gets a listener on the element.

A consequence: an element that is not in the document receives no delegated events, since the document never sees them.

## Control flow and lists

- `Show` and `Switch` render each branch in its own root and dispose it when another branch wins. The children function runs untracked.
- `For` keeps one root per row, keyed by `keyBy`, `key` or the item. On change it diffs the keys (`views/reconcile.ts`): appends, prepends, single removals and swaps take a fast path, and a big reorder moves the rows outside the longest run that is already in order.
- `Index` keeps one row per position and pushes the new item into that row's signal.

## Components

For each component, Signy (`component/component.ts`):

1. creates the instance, which runs the field initializers,
2. links it to its parent and its app, so `inject()` can walk up,
3. runs `setup()`, then `render()`, inside the component as owner,
4. queues its `onMount` callbacks, and runs the queue once the DOM is in place, children before parents.

Unmounting runs `onUnmount`, then the `onMount` cleanups, then unmounts the children and disposes the owner. `inject()` walks the parents and then falls back to the app's values.

## Application and plugins

`application()` (`app/application.ts`) holds the root component, the app level values and a registry of installed plugins. `use()` calls the plugin's `install()` and keeps the cleanup it returns. `unmount()` unmounts the root, runs the cleanups in reverse order and clears the values.

## Router

The router (`router/`) is a plugin. It provides itself and its signals to the app, and `Outlet` reads them.

- **History.** One adapter per mode: browser history, hash or memory (`history.ts`).
- **Matching.** Each path is compiled to a regular expression with a score. Static segments score highest, wildcards lowest, and the best score wins (`route.ts`).
- **Navigation.** Resolve the target, save the scroll position, run `beforeEach` guards and the route's guards, follow a redirect, run the loader of the deepest route, update the history, set the route signals, scroll, then run `afterEach` (`router.ts`). A newer navigation aborts an older one at any step.
- **Rendering.** Each `Outlet` knows its depth and renders the matched route of that depth.

## Store

A store (`reactivity/store.ts`) wraps each object in one proxy, cached so the same object always gives the same proxy. A read creates a node for that key of that object, which works like a signal. A write notifies the node of the written key only, which is why parents do not re-run. `Object.keys()` and `in` read a separate node for the key list.

Known limit: a node stays as long as its object, even after its key is deleted. A store used as a map of ids grows with every id that was read.

## Dev features

The framework calls `features.emit` at component mounts, signal writes, effect runs and the like (`core/features.ts`). With no tools installed, a single check skips it. [signy-dev](https://signy.dev/documentation/1/application/dev-tools/index.html.md) installs the profiler, the inspector and the event logger as features. Logs and errors carry ids, and `signy-dev` registers the sentences for them, so production bundles do not ship the texts.

## Builds

- The npm build is ES2022 modules, split into shared chunks, so `signy`, `signy/web` and `signy/router` share one runtime and the same signals.
- It starts with `const __SIGNY_DEV__ = process.env.NODE_ENV !== 'production'`, so your bundler decides whether the dev checks stay.
- The global builds bundle everything into one file each, with ES2017 and ES5 variants of the full build. See [Installation](https://signy.dev/documentation/1/overview/installation/index.html.md#other-builds).
