A TypeScript library for crafting interaction nets, with abstractions for reactive programming, state management, distributed systems, and more.
📖 API Documentation | 📚 Examples | 📦 npm package
Annette is a TypeScript library that models applications as networks of interacting agents. Instead of traditional object-oriented or functional approaches, it uses a graph-based model where:
- Agents are like tiny programs with specific roles
- Ports are connection points on agents
- Rules define what happens when agents connect
- Networks manage the connections and execution
This approach can help with:
- Understanding data flow in complex applications
- Building reactive user interfaces
- Managing state across distributed systems
- Debugging with time travel capabilities
npm install annetteLet's start with something simple - a counter that increments when clicked:
import { Agent, Network, ActionRule } from 'annette';
// 1. Create a network (like a container for your agents)
const net = Network("counter-app");
// 2. Create agents (like tiny programs with specific roles)
const counter = Agent("Counter", 0);
const incrementer = Agent("Incrementer", 1);
// 3. Add agents to the network
net.addAgent(counter);
net.addAgent(incrementer);
// 4. Define what happens when agents connect
const incrementRule = ActionRule(
counter.ports.main,
incrementer.ports.main,
(counter, incrementer) => {
// This runs when the agents connect
counter.value += incrementer.value;
return [counter, incrementer]; // Return agents to keep them in the network
}
);
// 5. Add the rule to the network
net.addRule(incrementRule);
// 6. Connect the agents
net.connectPorts(counter.ports.main, incrementer.ports.main);
// 7. Execute the interaction
console.log("Before:", counter.value); // 0
net.step(); // Execute one interaction
console.log("After:", counter.value); // 1That's it! You've just created your first Annette application. The counter and incrementer are agents that interact through a rule when connected.
The scoped API creates a bound set of helpers for a single network instance:
import { createNetwork } from 'annette';
const { Agent, withConnections, scope } = createNetwork('app');
const Counter = withConnections(Agent.factory<'Counter', number>('Counter'), {
add: (counter) => {
counter.value += 1;
}
}, { autoDisconnectMain: true });
scope.reduce(() => {
const counter = Counter(0);
counter.add();
counter.add();
console.log(counter.value); // 2
});Scope helpers:
scope.step()queues work inside the callback, then runs one step.scope.reduce()queues work inside the callback, then reduces until quiescent.scope.manual()runs without auto-stepping and returns your callback value.
The scoped helpers include factory-based Agent, Port, Rule, and Connection utilities so you can stay inside the same network context. Use autoDisconnectMain if you plan to queue multiple method calls before stepping.
batch() groups method calls and runs a single step (or batch.reduce() to exhaust all pending work). untrack() runs a callback without tracking, so no agents, connections, or rules are registered.
const { batch, untrack } = createNetwork('app');
batch(() => {
counter.add();
counter.add();
});
batch.reduce(() => {
counter.add();
counter.add();
});
untrack(() => {
Counter(1); // not added to the network
});Operational notes:
rules.list()includes symmetric rules by default. Pass{ includeSymmetric: false }to hide mirrored entries.asNestedNetwork()wraps a scoped network when embedding it inside another agent value.
import { createNetwork, consume } from 'annette';
const { Agent, rules, connect, step, fnAgent } = createNetwork('app');
const Counter = Agent.factory<'Counter', number>('Counter');
const Incrementer = Agent.factory<'Incrementer', number>('Incrementer');
rules.when(Counter, Incrementer)
.consume((counter, incrementer) => {
counter.value += incrementer.value;
});
const ruleList = rules.list({ includeSymmetric: false });
const FnIncrementer = fnAgent(Counter, 'FnIncrementer', consume((counter, incrementer) => {
counter.value += incrementer.value;
}));
const counter = Counter(1);
const incrementer = Incrementer(2);
connect(counter, incrementer);
step();Fluent rule chains let you refine matching and outcomes:
rules.when()returns a builder with.on(),.where(),.consume(),.spawn(),.transform(), and.mutate().- Wrapper helpers like
consume()andspawn()adapt handlers forfnAgent()orwithConnections().
You can also attach pair interactions directly on an agent factory:
import { pair } from 'annette';
withConnections(Counter, {
applyIncrement: pair(Incrementer, (counter, incrementer) => {
counter.value += incrementer.value;
})
});renderToGraph builds a DOM-backed agent graph from a template tree. Each element becomes an Element agent, text becomes a TextNode, and attributes become Attribute agents connected to the element.
import { Network, renderToGraph } from 'annette';
const network = Network('dom');
const template = {
tag: 'div',
attrs: { class: 'card' },
children: [
{ tag: 'h1', children: [{ tag: 'text', text: 'Hello Graph' }] },
{ tag: 'p', children: [{ tag: 'text', text: 'Mounted with renderToGraph.' }] }
]
};
const root = renderToGraph(network, template);
if (root.name === 'Element' && root.value.ref) {
document.body.appendChild(root.value.ref);
}Notes:
- Requires a browser
documentfor DOM creation. - Attribute agents are created and connected; syncing them to actual DOM attributes is left to rules or effects.
createOptimizedDomSystem provides Million-style block updates, map-style list diffing, and Solid-like selection. Blocks hold a map of edit nodes, and updates are applied in O(1) via Update agents.
import { createNetwork, createOptimizedDomSystem } from 'annette';
const scope = createNetwork('dom');
const dom = createOptimizedDomSystem(scope);
const Row = dom.createBlockTemplate<{ label: string }>(
`<div class="row"><span class="label"></span></div>`,
(root) => ({ label: root.querySelector('.label') as HTMLElement })
);
const selectionManager = dom.SelectionManager({ selectedClass: 'selected' });
const listManager = dom.createListManager({
container: document.body,
blockFactory: Row,
activeBlocks: new Map(),
selectionManager,
getUpdates: (item) => [{ key: 'label', value: item.data.label, type: 'text' }]
});
dom.updateList(listManager, [
{ id: '1', data: { label: 'Buy Milk' } },
{ id: '2', data: { label: 'Walk Dog' } }
]);Notes:
- Blocks are agents with an
editsmap used for O(1) patching. selectBlockconnects the block to the selection manager’s active port.
The zero module exposes a functional, zero-cost topology runtime where ports are direct function references. It keeps type inference by modeling ports as typed callables.
import { zero } from 'annette';
const zeroNetwork = zero.createNetwork();
const Counter = zeroNetwork.Agent('Counter', (initial: number) => {
let value = initial;
const main = zeroNetwork.createPort<number>((delta) => {
value += delta;
});
return { main };
});
zeroNetwork.run(() => {
const counter = Counter(0);
const increment = zeroNetwork.createPort<number>((amount) => {
counter.main(amount);
});
zeroNetwork.connect(counter.main, increment);
increment(5);
});Notes:
zeroNetwork.run()scopes hierarchy and cleanup tracking.zeroNetwork.connectrewires ports by swapping function pointers.- There is no scheduler; ports execute immediately on call.
- Use
zero.middlewarehelpers for recording, sync, and debugging. - Use
zero.createFanout/zero.createRouterfor high-performance fanout and rewiring.
Topology-based state machines express state as a connected agent rather than a string. The createTopologyStateMachine helper wires ActionRules that replace the active state agent while keeping the machine connection intact.
import { createNetwork, createTopologyStateMachine } from 'annette';
const scope = createNetwork('machine');
const { Agent, Port, connect } = scope;
const Machine = Agent.factory<{ name: string }>('Machine', {
ports: { main: Port.main(), aux: Port.aux('aux') }
});
const Idle = Agent.factory<null>('Idle', {
ports: { main: Port.main(), aux: Port.aux('aux') }
});
const Working = Agent.factory<{ attempt: number }>('Working', {
ports: { main: Port.main(), aux: Port.aux('aux') }
});
const Start = Agent.factory<null>('Start');
const topology = createTopologyStateMachine(scope, {
machinePort: 'aux',
statePort: 'aux',
stateEventPort: 'main',
eventPort: 'main'
});
topology.transition(Idle, Start, Working, {
mapValue: () => ({ attempt: 1 })
});
const machine = Machine({ name: 'Process' });
const idle = Idle(null);
connect(machine.ports.aux, idle.ports.aux);
topology.dispatch(machine, Start(null));
console.log(topology.getState(machine)?.name); // WorkingNotes:
- The machine holds the active state via a dedicated port (default
aux). - Events are temporary agents removed by the transition rule.
createEventSystem models events as a linked list of listener agents. Emitting an event spawns a pulse agent that traverses the chain and triggers callbacks.
import { createNetwork, createEventSystem } from 'annette';
const scope = createNetwork('events');
const events = createEventSystem(scope);
const onLogin = events.createEvent<{ username: string }>('UserLogin');
events.listen(onLogin, (data) => {
console.log(`Email welcome: ${data.username}`);
});
events.listen(onLogin, (data) => {
console.log(`Analytics login: ${data.username}`);
});
events.emit(onLogin, { username: 'alice' });Notes:
- Listener insertion happens at the head of the chain.
- Pulses are removed after reaching the terminator.
createReifiedEffectSystem models async work as agents. An Effect connects to a Handler, which runs the async function and later injects a Result or ErrorResult back to the requester.
import { createNetwork, createReifiedEffectSystem } from 'annette';
const scope = createNetwork('effects');
const { Agent, Port, connect, rules } = scope;
const effects = createReifiedEffectSystem(scope);
const Client = Agent.factory<{ data: string | null }>('Client', {
ports: { main: Port.main('main'), io: Port.aux('io') }
});
rules.when(Client, effects.Result).consume((client, result) => {
const value = result.value as { data: string };
client.value.data = value.data;
});
const handler = effects.Handler({
topic: 'FETCH',
fn: async () => 'ok'
});
const client = Client({ data: null });
connect(client.ports.io, handler.ports.capability);
effects.requestFrom(client.ports.io, client.ports.main, 'FETCH', { url: '/api' });Notes:
requestremoves the effect after it reaches the handler.- Results are injected asynchronously via
scoped.step().
Storylines let you record method calls and replay or serialize them later:
import { createNetwork } from 'annette';
const { Agent, withConnections, storyline } = createNetwork('app');
const Counter = withConnections(Agent.factory<number>('Counter'), {
add: (counter) => {
counter.value += 1;
}
}, { autoDisconnectMain: true });
const story = storyline(Counter, function* (counter) {
yield* counter.add();
yield* counter.add();
return counter;
});
const counter = Counter(0);
story.apply(counter);
const serialized = story.serialize({ format: 'json' });
const replay = story.deserialize(serialized, { format: 'json' });
replay.apply(counter);Agents can hold scoped networks as values and expose explicit stepping rules:
import { createNetwork, asNestedNetwork } from 'annette';
const child = createNetwork('child');
const { Agent: ChildAgent, withConnections: childConnections } = child;
const Counter = childConnections(ChildAgent.factory<'Counter', number>('Counter'), {
add: (counter) => {
counter.value += 1;
}
});
const parent = createNetwork('parent');
const { Agent, withConnections, scope } = parent;
type ChildNetwork = typeof child;
const nested = asNestedNetwork(child);
const Host = withConnections(Agent.factory<'Host', ChildNetwork>('Host'), {
stepInner: (host) => {
nested.step();
},
reduceInner: (host) => {
nested.reduce();
}
}, { autoDisconnectMain: true });
scope.reduce(() => {
const host = Host(child);
const counter = Counter(0);
counter.add();
host.stepInner();
});Think of agents as tiny programs with a specific role. Each agent has:
- A name (like "Counter", "User", "Database") that defines its type
- A value that can be any TypeScript type
- Named ports for connecting to other agents
// Basic agent with a number value
const counter = Agent<"Counter", number>("Counter", 0);
// Agent with complex data
const user = Agent<"User", { name: string; age: number }>("User", {
name: "Alice",
age: 30
});
// Agent with custom ports
const processor = Agent("Processor", { status: "idle" }, {
input: Port("input", "main"),
output: Port("output", "aux"),
control: Port("control", "aux")
});
// Factory from existing agent
const ProcessorFactory = Agent.factoryFrom(processor);
const cloned = ProcessorFactory({ status: "ready" });Agent.factory() infers the name from the string literal, so you can provide only the value type:
const Counter = Agent.factory<number>('Counter');Key Insight: Agents are like actors in a play - they have a role (name), state (value), and ways to communicate (ports).
Ports are connection points on agents. They're like electrical sockets - you can plug things into them.
// Create different types of ports
const mainPort = Port("main", "main"); // Main interaction point
const auxPort = Port("output", "aux"); // Auxiliary connection point
// Convenience helpers
const mainShortcut = Port.main();
const auxShortcut = Port.aux("output");
// Factory from existing port
const PortCopy = Port.factoryFrom(auxPort);
const cloned = PortCopy();Each port has:
- A name (like "main", "input", "output")
- A type ("main" or "aux")
Key Insight: Main ports are for primary interactions, aux ports are for secondary connections (like passing results to other agents).
Networks are containers that hold agents and manage their interactions.
// Create a network
const net = Network("my-app");
// Add agents
net.addAgent(counter);
net.addAgent(incrementer);
// Connect agents
net.connectPorts(counter.ports.main, incrementer.ports.main);
// Execute interactions
net.step(); // One interaction
net.reduce(); // All possible interactionsKey Insight: The network is like a circuit board - it provides the structure for agents to interact.
Rules define what happens when agents connect. There are two types:
For custom logic when agents interact:
const incrementRule = ActionRule(
counter.ports.main,
incrementer.ports.main,
(counter, incrementer) => {
// Your custom logic here
counter.value += incrementer.value;
return [counter, incrementer]; // Return agents to keep them
}
);For transforming the network structure:
const addRule = RewriteRule(
number1.ports.main,
number2.ports.main,
(n1, n2) => ({
newAgents: [
{
name: "Result",
initialValue: n1.value + n2.value,
_templateId: "sum"
}
],
internalConnections: [],
portMapAgent1: { result: { newAgentTemplateId: "sum", newPortName: "main" } },
portMapAgent2: { result: null }
})
);Key Insight: ActionRules are for "what to do", RewriteRules are for "how to transform the graph".
Connections link ports between agents:
// Connect two ports
net.connectPorts(agent1.ports.main, agent2.ports.input);
// Or create a connection object first
const conn = Connection(agent1.ports.output, agent2.ports.input);
net.addConnection(conn);Key Insight: Connections are like wires in a circuit - they define the flow of information.
Let's build your understanding step by step with practical examples.
Let's create a counter that can be incremented and decremented:
import { Agent, Network, ActionRule } from 'annette';
const net = Network("counter-example");
// Create our agents
const counter = Agent<"Counter", number>("Counter", 0);
const incrementer = Agent<"Incrementer", number>("Incrementer", 1);
const decrementer = Agent<"Decrementer", number>("Decrementer", -1);
// Add them to the network
net.addAgent(counter);
net.addAgent(incrementer);
net.addAgent(decrementer);
// Create rules for incrementing and decrementing
const incrementRule = ActionRule(
counter.ports.main,
incrementer.ports.main,
(counter, incrementer) => {
counter.value += incrementer.value;
return [counter, incrementer];
}
);
const decrementRule = ActionRule(
counter.ports.main,
decrementer.ports.main,
(counter, decrementer) => {
counter.value += decrementer.value; // Adding a negative number
return [counter, decrementer];
}
);
// Add rules to network
net.addRule(incrementRule);
net.addRule(decrementRule);
// Test incrementing
console.log("Initial:", counter.value); // 0
net.connectPorts(counter.ports.main, incrementer.ports.main);
net.step();
console.log("After increment:", counter.value); // 1
// Test decrementing
net.connectPorts(counter.ports.main, decrementer.ports.main);
net.step();
console.log("After decrement:", counter.value); // 0What you learned:
- Multiple agents can interact with the same counter
- Each rule defines a specific interaction pattern
- Agents maintain their state between interactions
Let's build a simple state machine:
import { Agent, Network, ActionRule } from 'annette';
const net = Network('state-machine');
// Create state and event agents
const state = Agent<'State', string>('State', 'idle');
const startEvent = Agent<'Event', string>('Event', 'start');
const stopEvent = Agent<'Event', string>('Event', 'stop');
// Add to network
net.addAgent(state);
net.addAgent(startEvent);
net.addAgent(stopEvent);
// Create transition rules
const startRule = ActionRule(
state.ports.main,
startEvent.ports.main,
(state, event) => {
if (state.value === 'idle' && event.value === 'start') {
state.value = 'running';
}
return [state, event];
}
);
const stopRule = ActionRule(
state.ports.main,
stopEvent.ports.main,
(state, event) => {
if (state.value === 'running' && event.value === 'stop') {
state.value = 'idle';
}
return [state, event];
}
);
net.addRule(startRule);
net.addRule(stopRule);
// Test the state machine
console.log("Initial state:", state.value); // 'idle'
net.connectPorts(state.ports.main, startEvent.ports.main);
net.step();
console.log("After start:", state.value); // 'running'
net.connectPorts(state.ports.main, stopEvent.ports.main);
net.step();
console.log("After stop:", state.value); // 'idle'What you learned:
- Agents can represent both data (state) and actions (events)
- Rules can have conditions (if statements)
- The same agents can be reused for different interactions
Let's create a data processing pipeline:
import { Agent, Network, ActionRule, Port } from 'annette';
const net = Network("data-pipeline");
// Create data processing agents
const input = Agent("Input", { data: [1, 2, 3, 4, 5] }, {
main: Port("main", "main"),
output: Port("output", "aux")
});
const filter = Agent("Filter", { condition: (x: number) => x > 3 }, {
input: Port("input", "main"),
output: Port("output", "aux")
});
const output = Agent("Output", { results: [] }, {
input: Port("input", "main")
});
// Add to network
net.addAgent(input);
net.addAgent(filter);
net.addAgent(output);
// Create processing rules
const inputToFilterRule = ActionRule(
input.ports.main,
filter.ports.input,
(input, filter) => {
// Process data and pass results to output port
const filtered = input.value.data.filter(filter.value.condition);
input.value.processed = filtered;
return [input, filter];
}
);
const filterToOutputRule = ActionRule(
filter.ports.output,
output.ports.input,
(filter, output) => {
// This would receive the processed data
// In a real scenario, you'd pass data through aux ports
return [filter, output];
}
);
net.addRule(inputToFilterRule);
net.addRule(filterToOutputRule);
// Connect the pipeline
net.connectPorts(input.ports.main, filter.ports.input);
net.connectPorts(filter.ports.output, output.ports.input);
// Process the data
net.step();
console.log("Filtered data:", input.value.processed); // [4, 5]What you learned:
- Agents can have multiple ports for different types of connections
- Data can flow through a pipeline of transformations
- Aux ports can be used for passing results to other agents
Annette is based on interaction nets, a theoretical model of computation that naturally embodies several powerful properties:
The graph structure inherently maintains cause-and-effect relationships. When agents interact, the connections between them explicitly represent data dependencies:
// Each interaction preserves causality through explicit connections
const incrementRule = ActionRule(
counter.ports.main,
incrementer.ports.main,
(counter, incrementer) => {
// The counter's new state is directly caused by the incrementer
counter.value += incrementer.value;
return [counter, incrementer];
}
);Why this matters: Unlike traditional approaches where causality can be implicit and hard to trace, Annette makes data flow explicit through the network structure. This makes debugging, testing, and reasoning about your application much easier.
Each agent maintains its own scope and state boundaries. Agents can only communicate through explicit port connections, preventing unintended side effects:
// Agents maintain clean boundaries
const user = Agent("User", { name: "Alice", preferences: { theme: "dark" } });
const settings = Agent("Settings", { theme: "light" });
// They can only interact through explicit rules
const updateThemeRule = ActionRule(
user.ports.main,
settings.ports.main,
(user, settings) => {
// Controlled interaction - no accidental state leakage
user.value.preferences.theme = settings.value.theme;
return [user, settings];
}
);Why this matters: This prevents the "spooky action at a distance" problem common in traditional state management, where changing one piece of state unexpectedly affects others.
The agent-based model naturally supports distribution because agents are self-contained units that communicate through well-defined interfaces:
// Agents can be distributed across different processes/machines
const clientDoc = Agent("Document", { content: "Hello" });
const serverDoc = Agent("Document", { content: "" });
// Synchronization happens through explicit operations
const syncRule = ActionRule(
clientDoc.ports.sync,
serverDoc.ports.sync,
(client, server) => {
// Merge changes through defined conflict resolution
server.value.content = resolveConflicts(client.value.content, server.value.content);
return [client, server];
}
);Why this matters: Traditional approaches often retrofit distribution onto centralized models, leading to complex synchronization issues. Annette's model is inherently distributed-friendly.
The interaction model naturally supports concurrent execution because rules only affect the agents they explicitly reference:
// Multiple independent interactions can run concurrently
const rule1 = ActionRule(counter1.ports.main, incrementer1.ports.main, /*...*/);
const rule2 = ActionRule(counter2.ports.main, incrementer2.ports.main, /*...*/);
// These can run in parallel since they don't interfere with each other
network.step(); // May execute both rules concurrently if possibleWhy this matters: Unlike traditional approaches where shared mutable state requires complex locking mechanisms, Annette's model makes it easier to reason about and implement concurrent operations safely.
Unlike traditional libraries where these properties are add-ons or afterthoughts, they're fundamental to Annette's design:
- No additional libraries needed for causality tracking or time travel
- No complex setup for distributed synchronization
- No external tools required for debugging data flow
- No special configuration for concurrent execution
The interaction nets foundation means these capabilities are naturally present in the model, not bolted on as features.
Annette can track all changes and let you go back in time:
import { TimeTravelNetwork, Agent, ActionRule } from 'annette';
const net = TimeTravelNetwork("time-travel-counter");
const counter = Agent("Counter", { count: 0 });
const incrementer = Agent("Incrementer", { by: 1 });
net.addAgent(counter);
net.addAgent(incrementer);
net.addRule(ActionRule(
counter.ports.main,
incrementer.ports.main,
(counter, incrementer) => {
counter.value.count += incrementer.value.by;
return [counter, incrementer];
}
));
// Take a snapshot
const snapshot1 = net.takeSnapshot("Initial state");
console.log("Initial:", counter.value.count); // 0
// Make some changes
net.connectPorts(counter.ports.main, incrementer.ports.main);
net.step();
console.log("After increment 1:", counter.value.count); // 1
net.step();
console.log("After increment 2:", counter.value.count); // 2
const snapshot2 = net.takeSnapshot("After two increments");
// Go back in time
net.rollbackTo(snapshot1.id);
console.log("After rollback:", counter.value.count); // 0Annette has built-in reactive programming:
import { createReactive, createComputed, createEffect } from 'annette';
// Create reactive values
const count = createReactive(0);
const multiplier = createReactive(2);
// Create computed values (automatically update when dependencies change)
const doubled = createComputed(() => count() * multiplier());
// Create effects (run when dependencies change)
createEffect(() => {
console.log(`Count: ${count()}, Doubled: ${doubled()}`);
});
// Update values - effects run automatically
count(1); // Logs: "Count: 1, Doubled: 2"
multiplier(3); // Logs: "Count: 1, Doubled: 3"
count(2); // Logs: "Count: 2, Doubled: 6"Share state across multiple clients/servers:
import { SyncNetwork, Agent, registerSyncRules } from 'annette';
// Create a sync network
const clientNet = SyncNetwork("client-app", "client-1");
const serverNet = SyncNetwork("server-app", "server-1");
// Register sync rules
registerSyncRules(clientNet);
registerSyncRules(serverNet);
// Create shared document
const clientDoc = Agent("Document", {
id: "doc-123",
content: "Hello from client",
version: 1
});
// Add to client network
clientNet.addAgent(clientDoc);
// In real usage, operations would be sent over network
// This is simplified for the example
const operations = clientNet.collectOperations(0);
serverNet.applyOperations(operations);Convert Annette structures to/from strings for storage or network transfer:
import { serializeValue, deserializeValue } from 'annette';
// Complex object with circular references
const user = {
name: "Alice",
profile: { theme: "dark" }
};
user.self = user; // Circular reference
// Serialize (handles circular references automatically)
const serialized = serializeValue(user);
// Deserialize
const deserialized = deserializeValue(serialized);
console.log(deserialized.self === deserialized); // trueFor the full API (types, examples, and module breakdowns), see the standalone docs:
Quick pointers:
- Core primitives:
Agent,Network,ActionRule,RewriteRule,Connection - Scoped helpers:
createNetwork,scope,batch,untrack,storyline - Reactive APIs:
createReactive,createComputed,createSignal,createEffect - Serialization:
serializeValue,deserializeValue,deepClone
Annette's interaction nets have confluence — the same result regardless of operation order. This means:
- No conflict resolution code needed
- Distributed sync "just works"
- Time travel is free (replay the interaction log)
You don't build CRDTs on Annette. The network already is one.
Check out the examples directory for more comprehensive examples:
- Simple Counter - Basic counter implementation
- State Machine - Finite state machine
- Time Travel - Undo/redo functionality
- Distributed Sync - Multi-client synchronization
- Reactive Todo App - Full reactive application
Learn Annette step by step with our comprehensive tutorials:
- Getting Started Tutorial - Learn the core concepts with hands-on examples
- Time Travel Tutorial - Master undo/redo and debugging with time travel
- Reactive Programming Tutorial - Build reactive user interfaces and applications
Annette can be helpful for:
- Complex State Management - Applications with interconnected state
- Real-time Collaboration - Multi-user applications needing conflict resolution
- Offline-First Apps - Applications that must work offline and sync later
- State Machines - Applications with complex state transitions
- Time Travel Debugging - Applications where debugging state changes is critical
- Cross-Context Communication - Applications sharing state between workers/iframes
- TypeScript Projects - Teams valuing strong typing and compile-time safety
| Feature | Annette | Redux | MobX | XState | Recoil |
|---|---|---|---|---|---|
| State Management | ✅ | ✅ | ✅ | ✅ | ✅ |
| Immutable Updates | ✅ | ✅ | ❌ | ✅ | ✅ |
| Mutable API | ✅ | ❌ | ✅ | ❌ | ❌ |
| Time Travel | ✅ | ✅* | ❌ | ✅* | ❌ |
| Reactivity | ✅ | ❌ | ✅ | ❌ | ✅ |
| Fine-grained Updates | ✅ | ❌ | ✅ | ❌ | ✅ |
| State Machines | ✅ | ❌ | ❌ | ✅ | ❌ |
| Distributed Sync | ✅ | ❌ | ❌ | ❌ | ❌ |
| Algebraic Effects | ✅ | ❌ | ❌ | ❌ | ❌ |
| Type Safety | ✅ | ✅* | ✅ | ✅ | ✅ |
| Serialization | ✅ | ✅* | ❌ | ✅* | ❌ |
*With additional libraries or configuration
npm install annetteThen import what you need:
import {
Agent,
Network,
ActionRule,
createReactive,
TimeTravelNetwork
} from 'annette';You can also import from specific layers based on your needs:
// Core layer only
import { Core } from 'annette';
const network = Core.createNetwork('minimal');
// Standard library
import { StdLib } from 'annette';
const enhancedNetwork = StdLib.createEnhancedNetwork('full-featured');
// Specific features
import {
Agent, Network, ActionRule, // Core
TimeTravelNetwork, // Standard Library
serializeValue, // Application Layer
} from 'annette';We welcome contributions! See our contributing guide for details.
MIT License - see LICENSE for details.
Ready to build something amazing? Annette provides the foundation for creating robust, scalable applications with clear data flow and powerful abstractions. Start with the Quick Start guide and explore the examples to see what's possible! 🚀
