State Management Without a UI - Running SDuX Vault on Deno
Why State Management Isn't Just a UI Problem
The moment you have a value that changes over time, that multiple operations read and write, and that must stay consistent while async work is in flight, you have a state management problem - whether or not a screen is involved. A CLI that fetches records in batches, a job that hydrates a cache, an edge function that tracks a counter: all of them coordinate mutable state under concurrency. Most libraries answer that problem only for the browser. They wire their store to a component tree and assume a render loop exists.
A FeatureCell makes no such assumption. It is a headless runtime primitive: you create it, you commit to it, and you read confirmed snapshots back - no DOM, no framework, no render cycle required.
A FeatureCell Is Plain TypeScript
A FeatureCell is not a UI widget with state attached. It is a typed state pipeline. On Deno, you import @sdux-vault/core through an npm: specifier, initialize the Vault runtime once, create a cell, and activate it. That is the entire bootstrap - the same runtime you would use inside a browser app.
import { FeatureCell, Vault } from 'npm:@sdux-vault/core';
// Initialize the Vault runtime once, before any cell is created
Vault({ logLevel: 'off', devMode: false });
// A counter cell with a zeroed initial state - no framework bootstrap
const cell = FeatureCell<CounterState>({
key: 'counter',
initialState: {
count: 0,
label: 'Counter Example',
lastUpdate: new Date().toISOString()
}
});
// Explicit activation starts the pipeline
cell.initialize();
Key takeaway: This snippet is the @sdux-vault/core headless runtime - it is identical whether it runs in a browser tab or a Deno process. There is no framework-specific variant because there is no framework involved.
How State Interaction Maps From Redux
If you reach for Redux the instant you hear "shared state," the headless model is a small shift. Redux positions its store as application UI state wired to a component tree. A FeatureCell commits deterministic state through the same pipeline regardless of where it runs - the interaction surface does not change when the UI disappears.
| Concern | Redux | SDuX Vault |
|---|---|---|
| Where state lives | One global store wired to the component tree | The owning FeatureCell instance - no tree required |
| Reading state | Selectors project from a global tree | state property / state$ stream on the cell |
| Updating state | dispatch broadcasts an action to every reducer |
mergeState() / replaceState() target the cell directly |
| Runtime | Assumes a browser render loop | Runs anywhere a JavaScript runtime does |
Use Case - Collection Orchestration in a CLI or Script
A script that accumulates records - log lines, imported rows, discovered files - needs append semantics, not replacement. Register withArrayAppendMergeBehavior at cell creation and every mergeState() call concatenates the incoming array onto the committed collection instead of overwriting it. Previous entries are never discarded, only extended.
import { withArrayAppendMergeBehavior } from 'npm:@sdux-vault/addons';
import { FeatureCell, Vault } from 'npm:@sdux-vault/core';
Vault({ logLevel: 'off', devMode: false });
const cell = FeatureCell<Example[]>(
// FeatureCell descriptor (identity + initial state)
{ key: 'examples', initialState: [{ id: 66, name: 'Darth', lastName: 'Vader' }] },
// Definition-time behaviors - configure the merge stage of the pipeline
[withArrayAppendMergeBehavior],
// Controllers - none used in this example
[]
);
cell.initialize();
// Each merge concatenates, it does not replace
cell.mergeState({ loading: false, value: input, error: null });
The behavior array is a definition-time argument, so the merge rule is fixed for the life of the cell. Your script never has to remember to concatenate manually - the pipeline does it on every write.
Live demo: StackBlitz - deno/array-append-example .
Use Case - Async Data Loading With Derived State in a Job
A background job that loads users from a remote API has two recurring needs: track each entry through its loading lifecycle, and keep a derived total consistent with the committed collection. A reducer registered before initialize() recomputes the derived value after every commit, so there is no manual counting scattered across call sites.
const cell = FeatureCell<UsersState>({
key: 'users',
initialState: { users: [], totalLoaded: 0, lastRefresh: new Date().toISOString() }
});
// Pure reducer: recompute totalLoaded from the committed collection.
// Runs after every pipeline commit - no manual counting in call sites.
cell
.reducers([
(current) => ({
...current,
totalLoaded: current.users.filter((u) => u.status === 'loaded').length
})
])
.initialize();
Each write commits a loading placeholder first, then the resolved or errored entry - and a failed fetch is isolated to its own entry without disturbing the rest of the collection. Because the reducer runs inside the pipeline, totalLoaded can never drift out of sync with the users array.
Live demo: StackBlitz - deno/promise-example .
Use Case - Deterministic State for Edge Functions and Prototyping
When you need a single value replaced atomically - a counter, a config object, a session record - replaceState() swaps the entire committed value in one write. Readers always see a fully consistent snapshot, never a half-updated object. That determinism makes a FeatureCell a clean fit for edge functions and for prototyping state logic before any UI exists.
cell.replaceState({
loading: false,
error: null,
value: { count, label, lastUpdate: new Date().toISOString() }
});
// Clear the value without destroying the cell or its pipeline
cell.reset();
Live demo: StackBlitz - deno/replace-example .
Honest Limits - These Are Demos, Not Production Servers
The Deno examples are teaching scripts. They prove that the runtime is genuinely headless and that the pipeline behaves the same off the browser - they are not a blueprint for a production service. Each script exits explicitly once its sequence completes, because an open state$ subscription keeps the Deno event loop alive.
Warning: Treat these as proofs of concept. A server that runs long-lived cells needs its own lifecycle management - unsubscribing from state$, deciding cell ownership per request or per process, and handling shutdown. The examples demonstrate the runtime, not deployment architecture.
Deeper Dive
The headless runtime is the same one the framework bindings wrap - so everything you learn writing a Deno script transfers directly to a browser app. Explore the FeatureCell API to see the full interaction surface, or read the migration guide to map your existing Redux store, selectors, and reducers onto a FeatureCell.
Comments
No comments yet. Start the discussion.