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

tilia

v5.2.0

Published

๐Ÿƒ State management library, domain-driven.

Readme

Tilia

Tilia is a simple, powerful state management library for TypeScript and ReScript, designed for data-intensive and highly interactive apps. Built with best practices in mind, Tilia emphasizes readability and minimal API surface, making state management nearly invisible in your code.

Why Tilia for Domain-Driven Design ?

Tiliaโ€™s minimal, expressive API lets you model application state and business logic in the language of your domainโ€”without boilerplate or framework jargon. Features like carve encourage modular, feature-focused state that maps naturally to DDDโ€™s bounded contexts. Computed properties and derived actions keep business logic close to your data, making code more readable, maintainable, and easier to evolve as your domain grows.

In short: Tilia helps you write code that matches your business, not your framework.

For more information, check out the DDD section of the website.

Check the website for full documentation and more examples for both TypeScript and ReScript.

Note on exceptions

If a computed or observe callback throws an exception, the exception is caught, logged to console.error and re-thrown at the end of the next flush. This is done to avoid breaking the application in case of a bug in the callback but still bubbling the error to the user.

API for versin 3.x (in case the website is not available)

(TypeScript version below)

ReScript

This is taken directly from Tilia.resi file.

type observer

type signal<'a> = {mutable value: 'a}
type readonly<'a> = {data: 'a}
type setter<'a> = 'a => unit
type deriver<'p> = {
  /** 
   * Return a derived value to be inserted into a tilia object. This is like
   * a computed but with the tilia object as parameter.
   * 
   * @param f The computation function that takes the tilia object as parameter.
   */
  derived: 'a. ('p => 'a) => 'a,
}

type tilia = {
  /** 
   * Transform a regular object or array into a tilia proxy value.
   * 
   * The returned value is reactive: any nested fields or elements are tracked
   * for changes.
   */
  tilia: 'a. 'a => 'a,
  /** 
   * Transform a regular object or array into a tilia proxy value, with the
   * possibility to derive state from the object itself.
   * 
   * The returned value is reactive: any nested fields or elements are tracked
   * for changes.
   */
  carve: 'a. (deriver<'a> => 'a) => 'a,
  /** 
   * Register a callback to be re-run whenever any observed value changes in the
   * default context.
   * 
   * This uses a PUSH model: changes "push" the callback to run.
   * 
   * For a PULL model (run only when a value is read), see `computed`.
   */
  observe: (unit => unit) => unit,
  /** 
   * React to value changes.
  * 
  * The first function captures the values to observe and passes them to the
  * second function. The second function is called whenever any of the observed
  * values changes.
  * 
  * @param f The capture function.
  * @param m The effect function.
  */
  watch: 'a. (unit => 'a, 'a => unit) => unit,
  /** 
   * Run a series of operations in a batch, blocking notifications until the
   * batch is complete.
   * 
   * Useful for updating multiple reactive values efficiently.
   */
  batch: (unit => unit) => unit,
  /**
   * Wrap a primitive value in a reactive signal. Use this to quickly create a
   * tilia object with a single `value` field and a setter.
   *
   */
  signal: 'a. 'a => (signal<'a>, setter<'a>),
  /**
   * Derive a signal from other signals.
   *
   */
  derived: 'a. (unit => 'a) => signal<'a>,
  /** 
   * Internal: Register an observer callback.
   */
  _observe: (unit => unit) => observer,
}

/** 
 * Create a new tilia context and return the `tilia`, `observe`, `batch` and
 * `signal` functions.
 *
 * The `gc` parameter controls how many cleared watchers are kept before
 * triggering garbage collection (default: 50).
 * 
 * @param ~gc Maximum cleared watchers before GC (default: 50).
 */
let make: (~gc: int=?) => tilia

/** 
 * Transform a regular object or array into a tilia proxy value.
 * 
 * The returned value is reactive, tracking changes to nested fields or
 * elements.
 * 
 * @param a The object or array to wrap.
 */
let tilia: 'a => 'a

/** 
 * Transform a regular object or array into a tilia proxy value, with the
 * possibility to derive state from the object itself.
 * 
 * The returned value is reactive: any nested fields or elements are tracked for
 * changes.
 */
let carve: (deriver<'a> => 'a) => 'a

/** 
 * Register a callback to be re-run whenever any of the observed values changes.
 * 
 * This uses a PUSH model: changes "push" the callback to run.  For a PULL model
 * (run only when a value is read), see `computed`.
 * 
 * @param f The callback to run on changes.
 */
let observe: (unit => unit) => unit

/** 
 * React to changes of captured values.
 * 
 * The first function captures values to observe. The second function
 * is called with the returned value from the first function whenever
 * any of the observed values changes.
 * 
 * The effect callback should avoid synchronously mutating captured signals
 * to prevent unexpected recursive updates. Use `observe` for more complex
 * reactive behaviors involving such mutations.
 * 
 * @param f1 The function that captures values to observe.
 * @param f2 The function called when the captured values change.
 */
let watch: (unit => 'a, 'a => unit) => unit

/** 
 * Run a series of operations in a batch, blocking notifications until the batch
 * is complete.
 * 
 * Useful for updating multiple reactive values efficiently (not needed within a
 * tilia callback such as in `computed` or `observe`)
 * 
 * @param f The function to execute in a batch.
 */
let batch: (unit => unit) => unit

/**
 * Wrap a primitive value in a reactive signal. Use this to quickly create a
 * tilia object with a single `value` field and a setter.
 *
 * @param v The initial value.
 */
let signal: 'a => (signal<'a>, setter<'a>)

/**
 * Derive a signal from other signals.
 *
 */
let derived: (unit => 'a) => signal<'a>

/**
 * Wrap a value in a readonly holder with a non-writable `value` field.
 *
 * Use to insert immutable data into a tilia object and avoid tracking.
 *
 * @param v The initial value.
 */
let readonly: 'a => readonly<'a>

/** 
 * Return a computed value to be inserted into a tilia object.
 *
 * The cached value is computed when the key is read and is destroyed 
 * (invalidated) when any observed value changes.
 *
 * The callback should return the current value.
 * 
 * @param f The computation function.
 */
let computed: (unit => 'a) => 'a

/**
 * Create a computed value that reflects the current value of a signal.
 *
 * This function takes a reactive `signal` and returns a computed value that
 * "lifts" and tracks the inner `value` field of the signal.
 * 
 * @param s The signal to lift as a computed.
 */
let lift: signal<'a> => 'a

/**
 * Return a reactive source value to be inserted into a tilia object.
 *
 * The setup callback is called once on first value read and whenever any 
 * observed value changes. The callback receives a setter function, which 
 * can be used to imperatively update the value. The initial value is used 
 * before the first update.
 *
 * This is useful for implementing resource loaders, state machines or any state
 * that depends on external or asynchronous events.
 * 
 * @param f The setup function, receives a setter.
 * @param v The initial value.
 */
let source: (('a => unit) => 'ignored, 'a) => 'a

/**
 * Return a managed value to be inserted into a tilia object.
 *
 * The setup callback runs once when the value is first accessed, and again
 * whenever any observed dependency changes. The callback receives a setter
 * function to imperatively update the value, and should return the initial
 * value.
 *
 * This is useful for implementing event based machines with a simple initial
 * setup.
 * 
 * @param f The setup function, receives a setter and returns the current value.
 */
let store: (('a => unit) => 'a) => 'a

/**
 * Track key-level writes on a tilia-proxied dict.
 *
 * Takes an accessor `() => dict<'a>` so the tracker can follow source swaps.
 *
 * Returns `{ changes, mute }`:
 * - `changes`: capture function for `watch`. Drains accumulated changes into
 *   `{ upsert, remove }`. `upsert` contains objects captured at write time.
 *   `remove` contains keys of deleted entries. Last write wins per key.
 * - `mute`: run a callback with this tracker's dirty tracking temporarily
 *   removed. Writes inside `mute` are still reactive but not tracked.
 *
 * When a `guard` is provided and returns false, changes accumulate silently
 * without triggering the watcher. When the guard becomes true, the
 * effect fires with all accumulated changes.
 *
 * @param accessor Function returning the tilia-proxied dict to track.
 * @param ~guard Optional reactive guard function.
 */
type changes<'a> = {upsert: array<'a>, remove: array<string>}
type changing<'a> = {changes: unit => changes<'a>, mute: (unit => unit) => unit}
let changing: (unit => dict<'a>, ~guard: unit => bool=?) => changing<'a>

/** ---------- Internal types and functions for library developers ---------- */
/** 
 * Internal: Register an observer callback.
 */
let _observe: (unit => unit) => observer

/** 
 * Internal: Stop observing.
 */
let _done: observer => unit

/** 
 * Internal: Stop observing and mark an observer as ready to respond. 
 * 
 * If `bool` is true, notify if changed.
 */
let _ready: (observer, bool) => unit

/** 
 * Internal: Dispose of an observer that wasn't notified (notification disposes of observers automatically).
 */
let _clear: observer => unit

/** 
 * Internal: Get meta information on the proxy (raw tree, etc).
 */
let _meta: 'a => nullable<'b>

/** 
 * Internal: The default tilia context.
 */
let _ctx: tilia

TypeScript

declare const o: unique symbol;
declare const r: unique symbol;
export type Observer = { readonly [o]: true };
export type Signal<T> = { value: T };
export type Readonly<T> = { readonly data: T };
export type Setter<T> = (v: T) => void;
export type Deriver<U> = { derived: <T>(fn: (p: U) => T) => T };
export type Tilia = {
  tilia: <T>(branch: T) => T;
  carve: <T>(fn: (deriver: Deriver<T>) => T) => T;
  observe: (fn: () => void) => void;
  watch: <T>(fn: () => T, effect: (v: T) => void) => void;
  batch: (fn: () => void) => void;
  signal: <T>(value: T) => Signal<T>;
  derived: <T>(fn: () => T) => Signal<T>;
  source: <T>(initialValue: T, fn: (previous: T, set: Setter<T>) => unknown) => T;
  store: <T>(fn: (set: Setter<T>) => T) => T;
  changing: <T>(accessor: () => Record<string, T>, guard?: () => boolean) => Changing<T>;
  // Internal
  _observe(callback: () => void): Observer;
};
export function make(flush?: (fn: () => void) => void, gc?: number): Tilia;

// Default global context
export function tilia<T>(branch: T): T;
export function carve<T>(fn: (deriver: Deriver<T>) => T): T;
export function observe(fn: () => void): void;
export function watch<T>(fn: () => T, effect: (v: T) => void): void;
export function batch(fn: () => void): void;

// Functional reactive programming
export function computed<T>(fn: () => T): T;
export function source<T>(
  initialValue: T,
  fn: (previous: T, set: Setter<T>) => unknown
): T;
export function store<T>(fn: (set: Setter<T>) => T): T;
export function readonly<T>(data: T): Readonly<T>;
export function signal<T>(value: T): [Signal<T>, Setter<T>];
export function derived<T>(fn: () => T): Signal<T>;
export function lift<T>(s: Signal<T>): T;
export interface Changes<T> { upsert: T[]; remove: string[] }
export interface Changing<T> {
  changes: () => Changes<T>;
  mute: (fn: () => void) => void;
}
export function changing<T>(accessor: () => Record<string, T>, guard?: () => boolean): Changing<T>;

// Internal
export function _observe(callback: () => void): Observer;
export function _done(observer: Observer): void;
export function _ready(observer: Observer, notifyIfChanged?: boolean): void;
export function _clear(observer: Observer): void;
export function _meta<T>(tree: T): unknown;
export const _ctx: Tilia;

Basic Example

import { tilia, observe } from "tilia";

const alice = tilia({
  name: "Alice",
  age: 0,
  birthday: dayjs("2015-05-24"),
});

const globals = tilia({ now: dayjs() });

setInterval(() => (globals.now = dayjs()), 1000 * 60);

// The cached computed value is reset if now_.value or alice.birthday changes.
alice.age = computed(() => globals.now.diff(alice.birthday, "year"));

// This will be called every time alice.age changes.
observe(() => {
  console.log("Alice is now", alice.age, "years old !!");
});

Advanced Example

Demonstrates how to use carve for features where methods and properties depend on each other.

export function makeTodos(remote: Remote, data: Todo[]) {
  const todos = carve<Todos>(({ derived }) => ({
    // State
    filter: source(fetchFilter(remote), "all"),
    selected: newTodo(),

    // Computed state
    list: derived(list),
    remaining: derived(remaining),

    // Actions
    clear: derived(clear),
    edit: derived(edit),
    remove: derived(remove),
    save: derived(save),
    setFilter: derived(setFilter),
    setTitle: derived(setTitle),
    toggle: derived(toggle),

    // Private state
    data,
  }));

  // Sync writes to remote
  const { changes, mute } = changing(() => todos);
  watch(changes, ({ upsert, remove }) => remote.sync(upsert, remove));

  return todos;
}