-
Notifications
You must be signed in to change notification settings - Fork 41
example shared store
Stable.
If your plugin ships more than one JS bundle (e.g. an always-on
shell + a lazy-loaded UI bundle, or two unrelated features that
share a settings cache), each bundle compiles its own copy of every
imported source file. A state object defined at module scope in
one bundle is invisible to the other bundle — same source, different
runtime objects. Mutations don't propagate. Subscribers don't fire.
The chat window opens on the placeholder. The badge stays at zero.
wp.os.createSharedStore() is the framework primitive that
solves this. One window-level slot, keyed by your string;
mutate-then-notify; subscribers from any bundle fire on any
mutation.
A typical multi-bundle plugin: an always-on shell.js bundle owns
the inbound delivery / heartbeat plumbing; a lazy window.js bundle
renders the UI when the user opens the window.
-
shell.jscallssetFocusedItem(X)and mutatesstate.focusedItemId. -
window.jsreadsstate.focusedItemIdto decide what to render. - Each bundle has its own compiled copy of
state.ts. The shell's mutation is invisible to the window's render callback. The window opens on the placeholder.
Plugin authors who tried to roll their own dedupe (window.__myShared = window.__myShared || ...) hit subtle issues: stranded subscribers
after one bundle throws, stale captures after a test reset, no
type story, no consistent slot naming.
const store = wp.os.createSharedStore< MyState >(
'my-plugin/state', // any unique string
() => ( { // thunk: runs once per key
selectedId: null,
items: [],
} ),
);
// Subscribe — returns an unsubscribe.
const off = store.subscribe( ( s ) => {
console.log( 'state changed:', s.selectedId, s.items.length );
} );
// Mutate then notify.
store.state.selectedId = 7;
store.state.items.push( newItem );
store.notify();
// Read-only snapshot (same reference, just narrower type).
const current = store.getState();
// Stop listening.
off();The same key from any bundle — including a future bundle that
doesn't exist yet — returns the same store. The thunked
initialState only runs the first time the key is seen.
src/my-plugin/state.ts (imported by both bundles):
import type { SharedStore } from 'openstation';
interface MyState {
selectedId: number | null;
items: { id: number; label: string }[];
}
const store: SharedStore< MyState > = wp.os.createSharedStore(
'my-plugin/state',
() => ( {
selectedId: null,
items: [],
} ),
);
export function getState() { return store.getState(); }
export function subscribe( cb ) { return store.subscribe( cb ); }
export function selectItem( id: number | null ) {
store.state.selectedId = id;
store.notify();
}
export function setItems( items: MyState[ 'items' ] ) {
store.state.items = items;
store.notify();
}src/my-plugin/shell-entry.ts (always-on bundle):
import { setItems, selectItem } from './state';
// Hydrate from REST on boot — runs once per page load.
wp.os.fetch( '/wp-json/my-plugin/v1/items', undefined, { source: 'my-plugin/items' } )
.then( ( r ) => r.json() )
.then( ( items ) => setItems( items ) );
// React to a global keyboard shortcut by mutating shared state.
document.addEventListener( 'keydown', ( e ) => {
if ( e.key === 'Escape' ) {
selectItem( null );
}
} );src/my-plugin/window.ts (lazy chat-window bundle):
import { getState, subscribe } from './state';
export function mount( body: HTMLElement ): () => void {
function repaint(): void {
const s = getState();
body.querySelector( '.title' )!.textContent =
s.selectedId !== null ? `Item #${ s.selectedId }` : 'Nothing selected';
}
repaint();
return subscribe( repaint );
}The shell's selectItem(null) mutates the same store the window's
subscribe is listening on, so the title repaints — even though
the two files were compiled into separate IIFE bundles.
If your plugin ships a single JS bundle, plain module-level
state works fine. Don't reach for the primitive just because it
exists. The cost of the dedupe lookup is microscopic but the
abstraction overhead — keys, thunks, notify() calls — earns its
keep only when you have multiple bundles.
interface SharedStore< T > {
state: T; // mutable
getState(): Readonly< T >; // same ref, narrower type
notify(): void; // wake subscribers
subscribe( cb: ( s: Readonly< T > ) => void ): () => void;
setState( patch: Partial< T > ): void; // patch + notify in one call
// (object-shaped
// state only)
reset(): void; // tests only — preserves
// outer object identity
// for object state
}
wp.os.createSharedStore< T >(
key: string,
initialState: () => T,
): SharedStore< T >;setState() collapses the mutate-then-notify pair into one call
for flat patches; on a primitive-shaped store it warns and no-ops —
use the state setter there instead.
-
Namespace your key:
'<plugin>/<purpose>'keeps two unrelated plugins from colliding. - Document the shape near the call site: the runtime is type-erased; the FIRST bundle to call wins on shape if two bundles pass incompatible types.
-
Don't call
reset()in production: it tears state down for every consumer of the key, not just yours. It's there for tests. - One key per store: if you find yourself wanting two stores with related shapes, consider whether they should be one store with two top-level fields.
-
docs/javascript-reference.md#createSharedStore— full API doc.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config