WakaStore

WakaStore is a shared reactive state plugin for wakaPAC. Multiple components read and write the same store object, and any mutation automatically propagates to all of them.

explanation

Getting Started

Include the script after wakaPAC, register the plugin, then create a store:

<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakastore.min.js"></script>

<script>
    wakaPAC.use(wakaStore); // must be called before any wakaPAC() calls

    const store = wakaStore.createStore({
        user: { name: 'Floris', role: 'admin' },
        notifications: []
    });
</script>

Mount the store on components under any property name. Mutate directly — no special setter syntax required. Changes propagate immediately to all mounted components:

wakaPAC('#header',    { session: store });
wakaPAC('#sidebar',   { session: store });
wakaPAC('#dashboard', { currentUser: store });

store.user.name = 'Jan'; // all three components update

In templates, reference store properties through the mounted key name:

<span>{{session.user.name}}</span>      <!-- in #header -->
<span>{{currentUser.user.name}}</span>  <!-- in #dashboard -->

You can also mutate through this inside a component's methods or event handlers, using the mounted key name directly:

wakaPAC('#header', {
    session: store,

    logout() {
        this.session.user.name = '';
        this.session.user.role = 'guest';
    }
});

Deletions are reactive too:

delete store.user.role; // all components that reference user.role update

Arrays

Arrays in the store are fully reactive. The supported mutating methods are push, pop, shift, unshift, splice, sort, and reverse — all trigger DOM updates and foreach re-rendering across every mounted component:

store.notifications.push({ id: 1, text: 'New message' });
store.notifications.splice(0, 1);

Non-Reactive Properties

Properties whose names start with _ or $ are non-reactive. Mutations are applied to the underlying object but do not trigger DOM updates:

store._internalFlag = true;   // silent — no DOM update
store.$debug = { trace: [] }; // silent — no DOM update
store.user.name = 'Jan';      // reactive — all components update

Multiple Stores

A single component can mount multiple stores simultaneously. Each store is tracked independently:

const userStore = wakaStore.createStore({ name: 'Floris', role: 'admin' });
const cartStore = wakaStore.createStore({ items: [], total: 0 });

wakaPAC('#checkout', {
    user: userStore,
    cart: cartStore
});

cartStore.items.push({ sku: 'ABC', qty: 2 }); // only cart bindings update
userStore.name = 'Jan';                        // only user bindings update

Local Persistence

Stores can persist their state to localStorage and rehydrate on page load. Pass a persist key to createStore to opt in — this unlocks save(), load(), and clearPersist() on the store proxy.

const store = wakaStore.createStore({ draft: { title: '', body: '' } }, {
    persist: 'article-draft'
});

Automatic persistence

Pass autoLoad: true to rehydrate on creation and autoSave: true to write to localStorage on every mutation (debounced). The two options are independent — enable either or both. The restored state is available before any component mounts.

const store = wakaStore.createStore({
    theme: 'light',
    sidebarCollapsed: false,
    tablePageSize: 25
}, {
    persist: 'user-prefs',
    autoLoad: true,
    autoSave: true
});

Manual persistence

For full control over when state is loaded and saved, call store.load() and store.save() explicitly. This is useful when you want to save only at specific points — such as on form submit rather than on every keystroke — or when you want to discard unsaved changes on cancel.

// Save when the user pauses typing
store.save();

// On successful submit, clear the saved draft
store.push('/api/articles')
    .then(() => store.clearPersist())
    .catch(error => console.error('submit failed', error));

Merge strategy

On load(), stored values overwrite matching keys in the current state. Keys present in initialState but absent from storage keep their defaults. This means adding new properties to your store in a later version of your app will not leave them undefined for returning users who have older data in storage.

Server Sync — HTTP

Stores can poll a server endpoint and push local state back. All writes go through the store proxy, so DOM updates happen automatically.

Polling

Call poll() to start polling at a fixed interval. The endpoint should return a plain JSON object. The store shallow-merges the response via Object.assign and all mounted components update:

store.poll('/api/user', { interval: 5000 });
// Server response — plain JSON object
{ "name": "Floris", "role": "admin" }

// Becomes: store.name = "Floris", store.role = "admin"

The poll pauses automatically when the browser tab is hidden and resumes when it becomes visible. Call stopPoll() to stop it permanently.

each poll cycle overwrites the entire store with the server response, discarding any local mutations made since the last cycle. Call push() before the next cycle fires if you need to persist local changes.

Pushing

Call push() to send the current store state to a server endpoint as a PATCH request. If the server returns a plain JSON object, it is merged back into the store automatically — useful for server-generated fields like timestamps or IDs. Returns a Promise that resolves with the parsed response body:

store.user.name = 'Jan';

store.push('/api/user/1')
    .then(response => console.log('saved'))
    .catch(error => console.error('failed', error));

To send a partial payload, pass a body option:

store.push('/api/user/1', { body: { name: store.user.name } });
push() always attempts to parse the response as JSON. Endpoints returning 204 No Content will cause the promise to reject — supply an onError callback or a merge callback that ignores the response to handle this gracefully.

Custom merge

Supply a merge callback when the response shape requires custom handling — envelope responses, arrays, or nested structures. It receives the raw parsed response and is called with the store as this. Works for both poll() and push():

store.poll('/api/data', {
    interval: 5000,
    merge: function(response) {
        this.items = response.data.items;
        this.total = response.meta.total;
    }
});

Options

Option Method Default Description
interval poll 5000 Milliseconds between poll cycles, measured from when the previous response settles.
merge poll, push Custom merge callback function(response), called with the store as this. Use for envelope responses, arrays, or any shape that isn't a plain JSON object.
onError poll, push Error callback (error) => void. For poll, the loop continues after errors unless you call stopPoll() inside. For push, the error is re-thrown after the callback so your .catch() still fires.
method push 'PATCH' HTTP method to use for the push request.
body push full store state Payload to send. Pass a partial object to send only specific fields.
fetchOptions poll, push Additional options forwarded directly to fetch(), such as custom headers or credentials.

Server Sync — WebSocket

For real-time updates pushed from the server, use connect(). It opens a WebSocket connection and merges each incoming message into the store automatically. All mounted components update as normal.

store.connect('wss://example.com/updates');
store.disconnect();

Each message must be a JSON string. The default merge is Object.assign(store, data), which expects a plain JSON object. Supply a merge callback for any other shape:

store.connect('wss://example.com/updates', {
    merge: function(data) {
        this.items = data.items;
        this.total = data.meta.total;
    }
});

Reconnection

Reconnect is enabled by default. On close, WakaStore waits reconnectDelay ms before retrying, doubling the delay on each attempt up to reconnectDelayMax. The counter resets when a connection is established successfully.

To suppress reconnect for a specific close event, return false from the onClose callback — useful for codes like 4001 (unauthorised) where retrying is pointless:

store.connect('wss://example.com/updates', {
    reconnectDelay:    1000,   // base delay in ms (default)
    reconnectDelayMax: 30000,  // cap in ms (default)
    onClose: function(event) {
        if (event.code === 4001) return false; // don't reconnect
    },
    onError: function(event) {
        console.error('WebSocket error', event);
    }
});

Calling connect() while a connection is already active replaces it — the existing socket is closed cleanly before the new one opens.

Running poll() and connect() simultaneously is not recommended — concurrent updates from both sources produce unpredictable merge order. Use one or the other.

Options

Option Default Description
protocols Subprotocol string or array passed to the WebSocket constructor.
merge Custom merge callback function(data), called with the store as this. Use for non-plain-object messages.
onError Error callback (event) => void. Receives the native ErrorEvent.
onClose Close callback (CloseEvent) => void|false. Return false to suppress reconnect for this close event.
reconnect true Whether to reconnect automatically on close.
reconnectDelay 1000 Base reconnect delay in ms. Doubles on each attempt.
reconnectDelayMax 30000 Maximum reconnect delay in ms.

Cleanup

Call store.destroy() when discarding a store — on SPA navigation, for example — to stop any running poll, close any active WebSocket connection, and detach the autoSave listener in one call. Without this, listeners and timers accumulate if stores are created and discarded repeatedly.

store.destroy();

API

wakaPAC.use(wakaStore)

Register the plugin. Must be called before any components are created.

wakaStore.createStore(initialState, opts?)

Create a new reactive store from a plain object. Throws if initialState is not a plain object (arrays, Date, Map, Set, and other non-plain objects are rejected).

ParameterTypeDescription
initialStateobjectA plain object used as the initial store state.
opts.persiststringOptional. A localStorage key. When set, save(), load(), and clearPersist() become available on the store.
opts.autoLoadbooleanOptional. When true and persist is set, the store rehydrates from localStorage on creation. The restored state is available before any component mounts. Defaults to false.
opts.autoSavebooleanOptional. When true and persist is set, the store writes to localStorage on every mutation (debounced). Defaults to false.
Returns ProxyReactive store proxy for direct mutation.

store.poll(url, opts?)

Start polling a JSON endpoint and merging responses into the store. Default merge: Object.assign(store, response). Replaces any currently active poll on this store.

ParameterTypeDescription
urlstringThe endpoint URL to poll.
opts.intervalnumberOptional. Milliseconds between poll cycles, measured from when the previous response settles. Defaults to 5000.
opts.mergefunctionOptional. Custom merge callback function(response), called with the store as this. Use for envelope responses or non-plain-object shapes.
opts.onErrorfunctionOptional. Error callback (error) => void. The poll loop continues after errors unless you call stopPoll() inside.
opts.fetchOptionsobjectOptional. Additional options forwarded directly to fetch().

store.stopPoll()

Stop the active poll permanently. Safe to call when no poll is running.

store.push(url, opts?)

Send store state to a server endpoint as a PATCH request. If the server returns a plain JSON object it is merged back into the store via Object.assign.

ParameterTypeDescription
urlstringThe endpoint URL to send the request to.
opts.methodstringOptional. HTTP method to use. Defaults to 'PATCH'.
opts.bodyobjectOptional. Payload to send. Defaults to the full store state.
opts.mergefunctionOptional. Custom merge callback function(response), called with the store as this. Use for envelope responses or non-plain-object shapes.
opts.onErrorfunctionOptional. Error callback (error) => void. The error is re-thrown after the callback so your .catch() still fires.
opts.fetchOptionsobjectOptional. Additional options forwarded directly to fetch().
Returns Promise<object>Resolves with the parsed response body.

store.connect(url, opts?)

Open a WebSocket connection and merge incoming JSON messages into the store. Default merge: Object.assign(store, data). Auto-reconnects on close with exponential backoff. Replaces any active connection on this store.

ParameterTypeDescription
urlstringWebSocket endpoint (ws:// or wss://).
opts.protocolsstring|string[]Optional. Subprotocol(s) passed to the WebSocket constructor.
opts.mergefunctionOptional. Custom merge callback function(data), called with the store as this.
opts.onErrorfunctionOptional. Error callback (event) => void. Receives the native ErrorEvent.
opts.onClosefunctionOptional. Close callback (CloseEvent) => void|false. Return false to suppress reconnect for this close event.
opts.reconnectbooleanOptional. Auto-reconnect on close. Defaults to true.
opts.reconnectDelaynumberOptional. Base reconnect delay in ms. Doubles on each attempt. Defaults to 1000.
opts.reconnectDelayMaxnumberOptional. Maximum reconnect delay in ms. Defaults to 30000.

store.disconnect()

Close the active WebSocket connection and cancel any pending reconnect. Safe to call when no connection is active. The store remains fully usable afterwards.

store.save()

Write the current store state to localStorage under the key set in opts.persist. Failures are swallowed silently — private browsing and storage quota errors do not throw.

ParameterTypeDescription
Returns booleantrue on success, false if the write failed or no persist key was set.

store.load()

Rehydrate the store from localStorage. Stored values overwrite matching keys in the current state. Keys present in initialState but absent from storage keep their defaults.

ParameterTypeDescription
Returns booleantrue if stored data was found and applied, false otherwise.

store.clearPersist()

Remove this store's entry from localStorage. Does not affect in-memory state.

ParameterTypeDescription
Returns booleanfalse if no persist key was set, true otherwise.

store.stopAutoSave()

Detach the autoSave mutation listener and cancel any pending debounced write. The persist key and save() / load() methods remain available — only the automatic save-on-mutation behaviour stops. Safe to call when autoSave was not enabled.

store.destroy()

Tear down all active background activity in one call: stopPoll() + disconnect() + stopAutoSave(). The store proxy remains usable afterwards — in-memory state and the persistence methods are unaffected. Call this when discarding a store on SPA navigation to prevent listener accumulation.

// On route change or component teardown
store.destroy();

Events

These events are dispatched internally by WakaStore to drive reactivity. You do not need to handle them yourself — wakaPAC subscribes to them automatically. They are documented here in case you need to observe or debug store activity.

pac:store-changed

Fired on document by the store proxy on every mutation. Useful for observing or logging store changes globally across all components.

Event Detail

Property Type Description
storeId string Internal identifier of the store that was mutated.
path string Dot-separated path of the mutated property (e.g. user.name).
oldValue any The value before the mutation.
newValue any The value after the mutation. For array mutations, this is the post-mutation snapshot of the array.

pac:change

Dispatched on each subscribed container element after a store mutation. The path is translated to the component's mounted key name rather than the raw store path. wakaPAC uses this event to update bindings within that component.

Event Detail

Property Type Description
path string Dot-separated path of the mutated property, prefixed with the component's mounted key name (e.g. session.user.name).

pac:array-change

Dispatched on each subscribed container element instead of pac:change when the mutation is an array operation (e.g. push, splice). Carrying array-specific detail lets wakaPAC's foreach diffing perform minimal DOM updates rather than re-rendering the entire list.

Event Detail

Property Type Description
path string Dot-separated path to the mutated array, prefixed with the component's mounted key name.

Notes

Store references must be placed at the top level of the abstraction passed to wakaPAC(). Nested references are not scanned and will not be subscribed:

// correct — store is a top-level key
wakaPAC('#header', { session: store });

// incorrect — store is nested and will not be found
wakaPAC('#header', { state: { session: store } });

Store references are static after a component is mounted — replacing a store reference on the component after creation is not detected. Destroy and recreate the component instead.

Re-entrant mutations (where a change handler writes back to the same store) are silently dropped to prevent infinite update loops. If you need to respond to a store change by writing a derived value, use a different store or a plain (non-reactive) property:

// Avoid this pattern — the mutation is silently dropped
document.addEventListener('pac:store-changed', function() {
    store.counter++; // re-entrant write to the same store; no-op
});

Best Practices

  • Register before creating components — call wakaPAC.use(wakaStore) before any wakaPAC() calls.
  • Keep stores focused — one store for session state, one for cart state, etc. Mixing unrelated concerns makes changes harder to reason about.
  • Mount under descriptive names{ session: store } reads better in templates than { store: store }.
  • Push before the next poll — if you mutate locally while polling is active, call push() before the next cycle or those changes will be overwritten.
  • Use onError to control poll fate — by default the loop continues after errors. Call store.stopPoll() inside onError if repeated failures should stop polling.
  • Suppress reconnect on fatal close codes — return false from onClose for codes like 4001 (unauthorised) where reconnecting is pointless.
  • Call destroy() on navigation — in SPAs, call store.destroy() when a store is no longer needed to stop background activity and prevent listener accumulation.
  • Use autoLoad + autoSave for preferences, manual load() / save() for drafts — auto-load and auto-save suit persistent UI state that should always reflect the latest value; manual control suits form drafts where the user should decide when to load and commit.
  • Clear storage on logout — if your store contains user-specific data, call store.clearPersist() when the user logs out to avoid leaking state to the next session.