A small TypeScript Petri net engine. Define places and transitions, fire them, analyse reachable states, or wire up a load/fire/save dispatcher for production use.
For background on Petri nets and the ideas behind this library, see the blog post.
npm install petri-tsimport { type PetriNet, fire, reachableStates, terminalStates } from "petri-ts";
type Place = "idle" | "running" | "done";
const net: PetriNet<Place> = {
transitions: [
{ name: "start", inputs: ["idle"], outputs: ["running"] },
{ name: "finish", inputs: ["running"], outputs: ["done"] },
],
initialMarking: { idle: 1, running: 0, done: 0 },
};
let m = net.initialMarking;
m = fire(m, net.transitions[0]); // { idle: 0, running: 1, done: 0 }
reachableStates(net); // all reachable markings from initialMarking
terminalStates(net); // markings where nothing can fireRun all analysis in one call — reachable states, terminal states, deadlock check, invariant verification, and optional DOT output:
import { analyse } from "petri-ts";
const result = analyse(net, {
invariants: [
{ weights: { idle: 1, running: 1, done: 1 } },
],
dot: true,
});
result.reachableStateCount; // 3
result.terminalStates; // [{ idle: 0, running: 0, done: 1 }]
result.isDeadlockFree; // false (done is terminal)
result.invariants[0].holds; // true (tokens conserved)
result.dot; // Graphviz DOT stringFor the "marking as a JSON column" pattern — one net definition, one row per instance, load/fire/save inside a transaction:
import { createDispatcher, memoryAdapter } from "petri-ts";
const d = createDispatcher(net, memoryAdapter());
await d.create("order-1");
await d.dispatch("order-1", "start");
const { marking } = await d.inspect("order-1");memoryAdapter() is a Map-backed store for tests. In production, write your own PersistenceAdapter that wraps your database — the lib doesn't handle locking, your transaction does.
| Function | Description |
|---|---|
canFire(marking, transition) |
Check if a transition is enabled |
fire(marking, transition) |
Fire a transition, returns new marking |
reachableStates(net, limit?) |
BFS all reachable markings (default limit: 10,000) |
terminalStates(net, states?) |
Reachable markings with no enabled transitions |
isDeadlockFree(net, states?) |
True if no terminal states exist |
enabledTransitions(net, marking) |
Which transitions can fire |
checkInvariant(net, weights, states?) |
Verify a weighted token sum is constant |
analyse(net, options?) |
All-in-one analysis: states, deadlocks, invariants, DOT |
toDot(net, marking?) |
Graphviz DOT output |
createDispatcher(net, adapter, options?) |
Load/fire/save dispatcher |
memoryAdapter() |
In-memory persistence for tests |
npm test # vitest
npm run build # JS bundle + .d.ts
npm run check # tsc --noEmit
npm run lint # oxlint
npm run fmt # oxfmt