I got tired of application state being split across a dozen tiny systems.
One store for settings.
One object for UI state.
One event bus for updates.
One validation layer for important values.
One undo stack.
One pile of helper functions for waiting until something is ready.
One separate system for debugging what changed.
That works for a while.
Then the app gets larger, and state stops being “just values.” It becomes application infrastructure.
That is the problem I built OmniTurbo around.
OmniTurbo is a framework-agnostic TypeScript state engine built around path-first, application-wide state management.
The core idea is:
One state graph. Loose by default. Reactive when needed. Governed when it matters.
Repo: https://github.com/r146023/OmniTurbo
Package: npm install @r146023/omniturbo
The problem I kept running into
Most state starts innocent:
theme = "dark";
zoom = 1.25;
selectedId = "node_1";
But real apps tend to grow in annoying ways.
Eventually, some state needs to notify the UI when it changes.
Some state needs validation.
Some state should support undo.
Some state should be loaded in a batch without firing a hundred subscriptions.
Some state should be watched as a subtree.
Some state should be private to the subsystem that owns it.
Some state should appear in a timeline so you can debug what happened.
And some state is casual enough that none of that should be required.
That last part matters.
I did not want a system where every value needed a schema, a model, a reducer, a class, or a bunch of ceremony.
I wanted simple paths to stay simple, while important paths could opt into stronger behavior.
Basic usage
OmniTurbo stores state at string paths:
import { Omni } from "@r146023/omniturbo";
const omni = new Omni();
omni.set("settings.theme", "dark");
omni.set("editor.zoom", 1.25);
omni.set("selection.activeId", "node_1");
omni.setObj("myApp.currentUser", {
id: "user_123",
name: "Alice"
});
omni.set("myApp.friends", [{name:"someNerd"},{name:"otherNerd"}]);
console.log(omni.get("settings.theme"));
// "dark"
omni.push("myApp.friends", {name:"AWholeNewNerd"});
omni.get("myApp.currentUser.name");
// "Alice"
omni.set("myApp.currentUser.bio", "The biggest Goober on earth.");
omni.getObj("myApp.currentUser");
// {
// id: "user_123",
// name: "Alice",
// bio: "The biggest Goober on earth."
// }
That part is intentionally boring.
The useful part is that the same application-wide state graph can also support subscriptions, tree subscriptions, schemas, datatype validation, coercion, structured results, history, alerts, waiters, aliases, batching, and privacy where those features are needed.
Example paths might look like this:
settings.theme
settings.zoom
viewport.pan.x
viewport.pan.y
selection.activeId
entities.node_1.meta.width
entities.node_1.meta.height
forms.profile.email
plugins.snap.enabled
app.ready
The app gets one shared state graph instead of scattered state islands and one-off glue code.
Loose by default
Not all state deserves a ceremony tax.
Sometimes you really do just want to store a value:
omni.set("ui.sidebar.open", true);
omni.set("debug.lastClickedThing", {
id: "abc",
time: Date.now()
});
No schema required.
No validation required.
No special setup required.
That is the “loose by default” part.
But when a path becomes important, you can add rules.
Governed when it matters
Imagine editor.zoom should always be a number between 0.1 and 4.
You can govern that path:
omni.schema("editor.zoom", {
type: "number",
min: 0.1,
max: 4,
coerce: true
});
Now writes are checked before they become application state:
omni.set("editor.zoom", "1.5");
// accepted, stores 1.5
omni.set("editor.zoom", "banana");
// rejected, old value remains
That is important because invalid state should not leak into the rest of the app.
If a write fails, it should not notify subscribers, resolve waiters, trigger alerts, or appear as a successful state change.
Reactive when needed
A path-first store is not very useful if the rest of the application cannot react to it.
OmniTurbo supports direct subscriptions:
const unsubscribe = omni.subscribe("settings.theme", (path, value, oldValue) => {
console.log(`${path} changed from ${oldValue} to ${value}`);
});
omni.set("settings.theme", "light");
unsubscribe();
That works for watching a specific path.
But a lot of application state is hierarchical, so OmniTurbo also supports tree subscriptions:
omni.subscribeTree("entities.node_1.meta", (root, changedPath, value, oldValue) => {
console.log(`${changedPath} changed under ${root}`);
});
omni.set("entities.node_1.meta.width", 250);
omni.set("entities.node_1.meta.height", 100);
This is useful when a panel, form section, inspector, editor object, dashboard widget, or generated UI wants to watch an entire subtree.
There are also global subscriptions for tooling, debugging, logging, or app-wide observers:
omni.subscribeGlobal((path, value, oldValue) => {
console.log("state changed:", path);
});
This is the part that makes OmniTurbo feel more like application infrastructure than a plain value bag.
The app can react to state changes without every subsystem inventing its own event bus.
Invalid writes should not notify the app
This is where subscriptions and guarantees fit together.
If a path is governed and a write fails, subscribers should not be told that state changed.
omni.schema("layout.width", {
type: "number",
min: 1,
max: 5000,
coerce: true
});
omni.subscribe("layout.width", () => {
console.log("width changed");
});
omni.set("layout.width", "250");
// subscriber fires
omni.set("layout.width", "banana");
// rejected, subscriber does not fire
That is the key relationship:
The app can be reactive because failed writes do not become fake state changes.
In a small app, this might not matter much.
In a larger app, one invalid value can trigger UI updates, downstream calculations, derived state, persistence, history, or plugin behavior.
If the write did not really commit, the app should not react as if it did.
Structured results instead of guessing
OmniTurbo write methods return structured result objects.
const result = omni.set("layout.width", "250", {
schema: {
type: "number",
min: 1,
max: 5000,
coerce: true
}
});
if (result.success) {
console.log("Committed value:", result.value);
} else {
console.log("Issues:", result.issues);
}
I wanted this because once a write can be accepted, rejected, coerced, blocked, ignored, or partially applied, a boolean is not enough.
Throwing is also not always right.
In a form engine, editor, settings panel, generated UI, or internal tool, failed writes may be expected. They should be inspectable.
A result object can answer questions like:
- Did the write succeed?
- What value was actually committed?
- Was the value coerced?
- What was the old value?
- What issues occurred?
- Was the write rejected?
- Was the path governed?
- Did privacy block the write?
That makes state changes easier to reason about.
Batch initialization without startup chaos
Large apps often need to load a lot of state at startup.
omni.batch(() => {
omni.set("settings.theme", "dark");
omni.set("settings.zoom", 1);
omni.set("viewport.pan.x", 0);
omni.set("viewport.pan.y", 0);
omni.set("app.ready", true);
});
Without batching, initialization can create a storm of subscriptions, alerts, waiters, and history entries while the app is still booting.
Batching lets you load or update a group of paths as one coordinated operation.
That matters for:
- restoring saved state
- loading documents
- initializing editor state
- hydrating local-first data
- setting up generated UIs
- loading settings
- importing project files
- preparing app state before rendering
Startup loading and user-driven mutation are not always the same thing.
They should not always produce the same side effects.
Built-in history and timeline
Some paths should support undo-like behavior.
For example:
omni.set("editor.zoom", 1);
omni.set("editor.zoom", 2);
omni.undo("editor.zoom");
console.log(omni.get("editor.zoom"));
// 1
History can be enabled where it makes sense:
omni.schema("editor.zoom", {
type: "number",
history: true,
historyLimit: 20
});
Not every path needs history.
A temporary hover state probably does not need it.
A user-edited property might.
OmniTurbo also has a timeline concept, which is different from undo history.
History is for moving a path backward.
Timeline is for inspecting what happened.
A timeline entry can help answer:
- What path changed?
- What was the old value?
- What is the new value?
- Was it created, updated, deleted, or undone?
- When did it happen?
That makes the state graph easier to debug.
Alerts and waiters
Subscriptions are for reacting to ongoing changes.
Sometimes you only care when a notable committed change happens:
omni.alert("settings.zoom", (value) => {
console.warn("zoom changed", value);
});
Or only when a condition becomes true:
omni.alert("settings.zoom", (value) => {
console.warn("zoom is very high", value);
}, {
condition: (value) => Number(value) > 3
});
Waiters are for asynchronous readiness:
await omni.waitFor("app.ready");
Or multiple paths:
const values = await omni.waitFor([
"settings.loaded",
"plugins.loaded"
]);
That is useful when one part of an app needs to wait for state produced by another part of the app.
Not everything needs to be an event listener.
Sometimes you only need to wait once.
Wildcard schemas for generated state
A lot of apps generate state dynamically.
For example:
entities.node_1.meta.width
entities.node_1.meta.height
entities.node_2.meta.width
entities.node_2.meta.height
entities.node_3.meta.width
entities.node_3.meta.height
You do not want to manually define every path.
So OmniTurbo supports wildcard-style governance:
omni.schema("entities.*.meta", {
type: "object",
children: {
width: {
type: "number",
min: 1,
max: 5000,
coerce: true
},
height: {
type: "number",
min: 1,
max: 5000,
coerce: true
},
label: {
type: "string",
maxLength: 120,
coerce: true
},
locked: {
type: "boolean",
coerce: true
}
}
});
Now generated paths can still behave consistently.
This is useful for visual editors, form builders, dashboards, diagram tools, generated UIs, and applications where the full state shape is not known upfront.
Privacy is useful, but it is not the whole pitch
OmniTurbo also supports private paths and private setters.
That is useful when a path should only be updated by the subsystem that owns it.
But privacy is only one piece of the larger model.
The bigger idea is that paths can have different levels of behavior:
- some paths are just values
- some paths are watched directly
- some paths are watched as subtrees
- some paths are schema-governed
- some paths coerce input
- some paths keep history
- some paths appear in the timeline
- some paths trigger alerts
- some paths are waited on
- some paths are private
- some paths are aliases
You pick the behavior that matches the path.
That is why I think of OmniTurbo as a path-first application state engine, not just a validation layer.
What this is not trying to replace
OmniTurbo is not trying to replace every state library.
If all you need is component-local state, use component-local state.
If all you need is a simple global UI store, there are already great tools for that.
OmniTurbo is aimed more at apps where state starts becoming infrastructure:
- visual editors
- diagram editors
- canvas tools
- schema-driven forms
- dashboard builders
- generated UIs
- internal tools
- local-first apps
- desktop-style web apps
- metadata-heavy apps
- developer tools
- workflow builders
In those apps, state is not just temporary UI memory.
State has structure, meaning, side effects, history, and consequences.
The mental model
The simplest way I can describe OmniTurbo is:
path -> value
path -> subscribers
path -> optional schema
path -> optional history
path -> optional aliases
path -> optional privacy
path -> timeline entries
Loose paths stay simple.
Reactive paths notify the app.
Important paths get rules.
Historical paths can be undone or inspected.
The application gets one shared state graph instead of scattered stores, event buses, validation helpers, readiness flags, and one-off undo logic.
Current status
OmniTurbo is pre-1.0.
The core direction is stable, but I expect the API to evolve as it gets tested in more real applications.
I am especially interested in feedback on:
- the path-first state model
- direct, tree, and global subscriptions
- structured result objects
- batch initialization behavior
- per-path history
- timeline inspection
- schema and datatype ergonomics
- whether “loose by default, governed by request” makes sense
- whether this feels useful outside of my own projects
If you build TypeScript tools, visual editors, form engines, schema-heavy UIs, internal tools, generated app systems, or state-heavy frontend applications, I would genuinely appreciate blunt design feedback.
Repo: https://github.com/r146023/OmniTurbo
Package: npm install @r146023/omniturbo
For further actions, you may consider blocking this person and/or reporting abuse
