Create an isolated Tilia context with its own reactive world.
functionmake(gc?:number): Tilia
let make:(~gc:int=?, unit)=> tilia
make creates a context object containing the Tilia API (tilia, carve, observe, watch, batch, signal, derived, source, store, _observe).
Each context is isolated: observers and proxies from one context do not share tracking with another context. Use this to create independent reactive worlds.
gc sets the garbage-collection threshold for cleared watchers. The default is 50. See tilia, Tilia, and the guide chapter Mistakes stay small.
Example
import{ make }from"tilia";const a =make();const b =make();const left = a.tilia({ count:0});const right = b.tilia({ count:0});
left.count =1;
right.count =2;
openTilialet a =make()let b =make()let left = a.tilia({count:0})let right = b.tilia({count:0})
left.count=1
right.count=2
tilia converts a plain object or array into a proxy that tracks property reads and writes. The return value keeps the same shape and type as the input.
Nested plain objects and arrays are proxied lazily when read. Values with non-plain prototypes (for example, class instances) are returned as-is. Calling tilia on a value that is not an object or array throws. Calling tilia on an already proxied value returns the same proxy.
Writing the same value (or the same underlying target object) does not notify observers. See also observe, computed, and the guide chapter A Living Object.
Example
import{ tilia }from"tilia";const alice =tilia({
name:"Alice",
age:10,});
alice.age =11;
openTilialet alice =tilia({
name:"Alice",
age:10,})
alice.age=11
Build a reactive object where fields can derive from the full object.
functioncarve<T>(fn:(deriver: Deriver<T>)=>T):T
let carve:(deriver<'a>=>'a)=>'a
carve builds a proxied object from a factory function, providing a deriver argument with derived(self => ...).
derived is evaluated with the carved object as its input. This enables cross-field derivations and methods that depend on sibling fields. The returned object is then proxied like tilia.
Use carve when a derivation needs self; use computed when a standalone closure is enough. See the guide chapter Carving a feature.
Run a tracked callback immediately and on dependency changes.
functionobserve(fn:()=>void):()=>void
let observe:(unit => unit)=> unit => unit
observe runs fn once immediately, tracks reactive reads during that run, and re-runs fn whenever one of those tracked keys changes. Dependency tracking is rebuilt on each run.
Writes performed inside fn are deferred while fn is running. If fn writes to keys that it also tracks, it is scheduled to run again after the current run finishes. This makes observe suitable for state-machine-style transitions.
observe returns a function that cancels the observation. Once called, the callback never runs again. You can ignore this function for observers that should live as long as the context. For two-phase capture/effect behavior, use watch. For pull reactivity, use computed. See the guide chapters A living object and While Alice sleeps.
watch separates reactive work into two phases: fn captures dependencies and returns a value, and effect receives that value when the captured dependencies change.
A watch never retriggers itself through its own writes, whether they occur during capture or effect. The effect runs untracked, and notifications to other observers from its writes are deferred and flushed as a single batch. On initial registration, fn runs once to install dependencies, but effect is not called. watch returns a function that stops the watch; once called, neither phase runs again.
openTilialet(score, setScore)=signal(0)let(result, setResult)=signal("pending")watch(()=> result.value,
value =>{if value ==="pass"{setScore(score.value+1)}},)setResult("pass")
Group multiple writes and flush notifications once.
functionbatch(fn:()=>void):void
let batch:(unit => unit)=> unit
batch runs fn while notification flushing is locked, then flushes notifications once the outermost batch ends.
Nested batches are supported. Within a batch, writes update state immediately, but observers are notified only after the outermost batch ends. This prevents transient intermediate notifications.
observe, watch effects, and computed rebuilds already use deferred flushing. Use batch for grouped writes from non-reactive callbacks. See watch and the guide chapter While Alice sleeps.
Define a pull-based cached value for insertion into reactive objects.
functioncomputed<T>(fn:()=>T):T
let computed:(unit =>'a)=>'a
computed creates a dynamic value intended for direct assignment in a tilia/carve object. The value is computed when read, cached, and invalidated when tracked dependencies change.
If no observer depends on the computed key, Tilia can clear its internal observer and keep the dynamic definition for later reads. Replacing or deleting the property removes the previous computed behavior.
Using a computed value outside an insertion context raises an orphan-computation error. Define it directly where it is inserted. See tilia, carve, and the guide chapter Values that follow.
source creates a dynamic value for insertion into a reactive object. It starts with initialValue, then executes fn(previous, set) on the first read and whenever tracked dependencies in fn change.
previous is the latest value held by the source, and set updates it. If fn does asynchronous work, dependencies must still be read synchronously before awaiting; only synchronous reads are tracked.
If dependencies change, the current value remains available until set is called again. In the example below, the previous cards remain visible while the new deck loads. See store, carve, and the guide chapter Letting the world in.
When an empty result is valid, use a stable, empty tilia value to distinguish the initial loading state without changing the value's type. Compare by identity:
Example
const loading =tilia<Card[]>([]);const app =tilia({
cards:source(loading,(_previous, set)=>{fetchCards("spanish").then(set);}),});if(app.cards === loading){// No result has arrived yet.}
let loading =tilia([])let app =tilia({
cards:source(loading,(_previous, set)=>{fetchCards("spanish")->Promise.thenResolve(set)->ignore
}),})if app.cards=== loading {// No result has arrived yet.}
The sentinel marks only the initial load. When a dependency changes, source keeps its previous value until the next call to set. Call set(loading) synchronously if a reload should return to the loading state.
Define an inserted managed value from a setup function with setter.
functionstore<T>(fn:(set: Setter<T>)=>T):T
let store:(('a => unit)=>'a)=>'a
store creates a dynamic value for insertion into a reactive object. Its fn(set) setup returns the current value and receives set so that it can update the value later.
The setup runs on the first access and can run again when dependencies tracked during setup change. This is suitable for finite-state values whose transitions call set.
openTiliatypesession=Idle({start: unit => unit})|Running({stop: unit => unit})letrec idle = set =>Idle({start:()=>set(running(set))})and running = set =>Running({stop:()=>set(idle(set))})let app =tilia({session:store(idle)})
Create a single-value reactive signal and its setter.
functionsignal<T>(value:T):[Signal<T>, Setter<T>]
let signal:'a =>(signal<'a>, setter<'a>)
signal returns a pair: a reactive object { value } and a setter function.
The signal object is a Tilia proxy, so reads of .value are tracked, and writes through the setter notify dependents. This provides a compact form for individual mutable values.
Use derived to build a computed signal and lift to expose a signal in a tilia object.
Example
import{ signal }from"tilia";const[count, setCount]=signal(0);setCount(1);
count.value;
Lift a signal into a computed value for object insertion.
functionlift<T>(s: Signal<T>):T
let lift: signal<'a>=>'a
lift converts a signal into an inserted computed value by tracking s.value.
It is equivalent to computed(() => s.value). Use it when an object should expose a signal as a read-only field while retaining mutation through the signal setter.
Opaque observer handle used by low-level observer lifecycle helpers.
typeObserver={readonly[o]:true}
typeobserver
The Observer/observer type is an opaque handle representing a registered observer.
Application code that uses public APIs does not normally create or consume this type directly. It appears in low-level lifecycle helpers (_observe, _done, _ready, _clear).
For ordinary reactive effects, use observe or watch.