npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@signal-tree/kernel

v15.3.0

Published

SignalTree: framework-neutral reactive application state with causal semantics.

Readme

@signal-tree/kernel

Framework-neutral SignalTree state: fields and entity collections, transactions and rollback, undo, external updates through link(), batching, and DevTools.

This is the package framework adapters are built on, so it also documents the advanced vocabulary (causal turns, restoration designation, the observation seam). Application developers usually need only the terms in the glossary's first table.

Angular applications should construct trees through @signal-tree/angular (requires Angular 20, 21, or 22 — see peerDependencies in packages/angular/package.json). React applications should construct and observe trees through @signal-tree/react. Use the kernel directly for framework-neutral runtimes, libraries, and tests. Framework packages forward this neutral surface by identity, so an application should use its framework package as its one SignalTree import root.

Semantic Guidance

The canonical v15 model and composition guidance ships with this package as llms.txt. It explains the facade rule, link() relationships, persistence composition, and why human-readable causal explanations are projections rather than retained kernel facts.

Install

npm install @signal-tree/kernel

Create A Tree

import { signalTree } from '@signal-tree/kernel';

const tree = signalTree({
  count: 0,
  user: {
    name: 'Ada',
    active: true,
  },
});

$ is the state facade. Root, branch, and terminal locations support whole-value reads, replacements, and updater functions. A value call is never a patch; derive a patched value with an updater.

tree.$();
tree.$.user();
tree.$.user({ name: 'Grace', active: true });
tree.$.user((user) => ({ ...user, active: false }));

tree.$.count();
tree.$.count(1);
tree.$.count((count) => count + 1);

The state literal defines the public paths. There are no actions, reducers, or selectors required for ordinary reads and writes.

Terminal Values

A plain object normally becomes a branch whose properties are locations. Use leaf(value) when an object should remain one atomic value, or when a callable is state rather than topology:

import { leaf, signalTree } from '@signal-tree/kernel';

const tree = signalTree({
  user: { name: 'Ada' },
  bounds: leaf({ min: 0, max: 100 }),
  handler: leaf((value: number) => console.log(value)),
});

tree.$.user.name('Grace');
tree.$.bounds({ min: 10, max: 90 });
tree.$.handler(leaf((value) => persist(value)));

At construction, leaf(object) stops dot-path expansion. At invocation, location(leaf(callable)) distinguishes callable data from the updater grammar. The wrapper never enters canonical state, snapshots, persistence, restoration, or links; reads return the original raw value by identity.

Construction

State, capabilities, and derived values are declared in one construction call:

const tree = signalTree(initialState, {
  enhancers: [batching(), restoration(), devTools()],
  derived: ($) => ({
    // Return realization-native computed values here.
  }),
});

There is no late .with() phase or fluent .derived() chain. SignalTree validates the complete enhancer set before construction and resolves declared capability requirements from that set.

The neutral kernel does not own framework lifecycle, rendering, dependency injection, or effect scheduling. Framework packages provide those realizations.

EntityMap

entityMap() creates a normalized collection at any state path.

import { entityMap, signalTree } from '@signal-tree/kernel';

type Product = {
  id: number;
  name: string;
  inStock: boolean;
};

const tree = signalTree({
  catalog: {
    products: entityMap<Product, number>({
      selectId: (product) => product.id,
    }),
  },
});

const products = tree.$.catalog.products;

products.addMany([
  { id: 1, name: 'Laptop', inStock: true },
  { id: 2, name: 'Chair', inStock: false },
]);

products.all();
products.ids();
products.count();
products.empty();
products.asMap();
products.has(1)();
products.where((product) => product.inStock)();
products.find((product) => product.name === 'Laptop')();

products.byId(1)?.();
products.byIdOrFail(1)();
products.updateOne(1, { inStock: false });
products.replaceOne(1, { id: 1, name: 'Laptop', inStock: true });
products.upsertOne({ id: 3, name: 'Desk', inStock: true });
products.removeOne(2);

EntityMap preserves collection order and stable entity handles across ordinary updates. changeId(from, to) adopts a new key without remove-and-add identity loss. Keep each entity fact under one EntityMap authority and derive selections rather than duplicating entity objects elsewhere.

Read-Only Views

asReadonly(tree) narrows the same runtime object to a read-only type. It does not allocate a second store or create a runtime security boundary.

import { asReadonly } from '@signal-tree/kernel';

const reader = asReadonly(tree);
reader.$.count();
reader.$.catalog.products.byId(1)?.();

Location write overloads and EntityMap mutation methods are absent from the read-only type. Use this for consumers that should receive reads while an application-owned Ops service retains the writable tree.

Built-In Enhancers

batching()

Coalesces notifications for grouped writes and adds the batching capability.

const tree = signalTree(state, {
  enhancers: [batching()],
});

restoration()

Retains designated causal turns for undo and redo.

import { restoration, signalTree, undoable } from '@signal-tree/kernel';

const tree = signalTree(
  { count: 0 },
  {
    enhancers: [restoration({ maxHistorySize: 50 })],
  }
);

// Connect these handlers to separate user actions.
const actions = {
  increment: () => undoable(() => tree.$.count((count) => count + 1)),
  undo: () => tree.undo(),
  redo: () => tree.redo(),
};

undoable() designates the current synchronous authored turn. It is not an async scope and does not create a separate state authority. History is recorded when that turn settles: call undo from a later user action, not immediately after undoable() in the same synchronous function. The tree owner calls destroy() when this store is no longer needed.

transactions()

Adds an explicit pending operation that can be confirmed or rolled back. Use it for pending authority, not as a synonym for retained undo history.

Confirmed records are retained only while a live obligation needs them — a confirmed turn is released once no older pending turn could still consult it. Diagnostic history is a separate, explicitly bounded facility:

transactions({ history: { retain: 100 } });

Without it, confirmedTurnReader reports retention.truncated === true with no turns. That is deliberately distinguishable from "nothing happened", which reports truncated === false.

devTools()

Connects the tree to Redux DevTools and adds the typed debug-session surface.

const tree = signalTree(state, {
  enhancers: [devTools({ name: 'Application' })],
});

const session = tree.exportDebugSession();

External Truth

external() classifies synchronous writes whose authoritative decision came from outside the current authored operation. Restoration observes those writes but does not claim them as undoable authored work.

import { external } from '@signal-tree/kernel';

const rows = await api.list();
external(() => tree.$.rows.setAll(rows));

Acquire data first. Passing an async callback to external() is invalid because the classification scope ends when the callback returns.

Links

link() expresses a live relationship between state locations while preserving the kernel's authority and causal-turn semantics. Use it for genuine ongoing synchronization, not as a request wrapper or migration bridge.

Errors

Observe library-reported diagnostics through onTreeError():

import { onTreeError } from '@signal-tree/kernel';

const stop = onTreeError((event) => {
  console.error(event.operation, event.treeId, event.path, event.error);
});

stop();

Applications observe errors; reporting remains owned by the library.

Persistence, Async Work, And Forms

SignalTree 15 does not publish persistence, serialization, async-request, or forms capabilities. Applications own storage formats, migrations, fetching, cancellation, retries, validation, and form control behavior.

Write resolved external data through ordinary paths or EntityMap, using external() when restoration must not claim the write. Use framework effects or application services for storage synchronization.

Lifetime

A tree owns runtime resources until destroy() releases them.

const tree = signalTree({ value: 1 });

try {
  tree.$.value(2);
} finally {
  tree.destroy();
}

Application-root stores may live for the process lifetime. Component, route, SSR-request, test, and temporary-workflow trees have bounded owners and must be destroyed at that boundary. Dropping the last local reference is not prompt resource reclamation.

Angular's defineStore binds tree destruction to DestroyRef. Direct kernel construction remains the caller's responsibility.

Failed pending-transaction rollback throws SignalTreeRollbackError, whose stable code and structured cause distinguish refusal from application errors.

A refusal is atomic: it changes no state, retires nothing, and leaves the transaction pending, so confirm() and a retried rollback() both remain available. Reversing an older transaction while a newer overlapping one is still open refuses (cause.kind === 'later-pending-dependency'); settle the newer one first.

Exports

The package publishes three code entry points:

  • @signal-tree/kernel
  • @signal-tree/kernel/adapter
  • @signal-tree/kernel/internals — supported tooling observation seam

The adapter entry point is the framework-neutral observation SDK. It is not a compatibility layer or an application convenience surface.

  • createSignalTreeFactory(observation) binds framework observation to tree construction.
  • isConstructionBranch(value) identifies recursively traversed construction definitions, excluding explicit leaves, markers and terminal containers. Framework facades use this boundary for their own input validation; native reactive identity checks remain framework-owned.
  • isNodeAccessor(value) distinguishes a root or branch accessor from a terminal location when a realization must route framework integration.
  • replaceLocation(location, value) applies raw replacement ingress when the caller already knows the operation semantics, including callable state; it does not re-enter the authored updater grammar.
  • observeOwnerInvalidation(owner, callback) wakes a framework observer so it can reread canonical truth.
  • readCanonicalSnapshot(owner) reads the owner-qualified whole-tree snapshot.
  • withRestorationDesignation(callback) identifies framework-originated user writes that are eligible for restoration.

An ObservationAdapter supplies dependency tokens and runInvalidationGroup(run) so transactions and restoration can apply all changes before framework observers are notified. It never owns or mirrors location state.

An adapter may also supply an epoch, which lets a framework invalidate a whole subject through one native primitive instead of one carrier per field:

  • createEpoch() returns an EpochHandle — an opaque callable the adapter owns. The kernel stores it, passes it back, and never writes through it or inspects what is inside.
  • advanceEpoch(epoch) marks that handle stale. The kernel calls this; the adapter decides what the framework does about it.

The two are a pair. An adapter that supplies one without the other does not receive an epoch at all, because a handle the kernel cannot advance would silently stop invalidating. EpochHandle is exported from @signal-tree/kernel/adapter alongside ObservationAdapter and ObservationToken.

Redux DevTools collection display

The optional entityKeyedView setting adds an id-keyed byId view alongside an EntityMap's all array in Redux DevTools snapshots. It is off by default because it increases payload size. Keys come from id, key, or uuid; the keyed view is omitted if any included entity lacks a key. This is display data: the existing all representation and time-travel hydration remain unchanged.

Tooling observation

@signal-tree/kernel/internals is a supported observation seam for tools such as Studio. Application code continues to use its framework facade; these exports are not additions to the kernel root API.

  • treeRuntimeId exposes runtime identity for equality and map keys, never a persisted identity.
  • treeCapabilities reports construction capabilities; an empty list is a bare tree.
  • confirmedTurnReader reads retained committed consequences without installing history. Its ConfirmedTurnReader, ConfirmedTurnSnapshot, ConfirmedTurnView, ConfirmedTurnEffectView, ConfirmedTurnEffectKind and ConfirmedTurnRetention types describe that window, including retention limits. A tree that has not asked for diagnostic history retains nothing for the reader, and ConfirmedTurnRetention.truncated says so rather than presenting an empty window as a complete one — it is asserted by the transaction authority, never inferred from gaps in turn ids. Reads after destruction throw StudioTreeDestroyedError.
  • observeWrites subscribes to ObservedWriteFrame observation. A notification does not establish a causal relationship, intermediate attempted write, or complete history.
  • activeTransactionContext returns the synchronous transaction callback's owner and local ID, or undefined outside that scope. It does not report confirmation or propagate across await.
  • withWriteObservationScope associates writes with a bounded, owner-qualified tooling token during a synchronous callback. ObservedWriteFrame.declaredScopes uses DeclaredWriteScopes to preserve retained tokens, includesUnscoped contributions and omitted overflow when notifications coalesce. Declarations describe scope membership, not proven input dependencies or exclusive causes. Observer delivery does not inherit the scope.

Internally, getConfirmedTurnRecords supplies retained records to the tooling projection. Tools consume confirmedTurnReader; the raw internal transaction runtime accessor is not the supported inspection contract.

Studio is a separate private product with an explicit development-only attachment. Its adapter and query engine are not public npm packages. Tooling must retain unknown/unsupported distinctions and must not infer causality from event timing.

License

Apache-2.0. See LICENSE and NOTICE.