GitHub

Global Store

The global store is a single shared data layer. Declare server fetchers once in pulse.store.js — user profiles, settings, feature flags — and any page can access them by name. No prop drilling, no repeated fetches.

When to use the store

The store is the right tool when the same server data is needed on multiple pages and it would be wasteful to redeclare the same fetcher in every spec:

  • Current user / session — store.user
  • App settings or feature flags — store.settings
  • Navigation items that come from a CMS — store.nav
  • Subscription or plan level — store.plan
The store has no client-side reactivity. Data is fetched on the server per request and is available to the view at mount time. For page-specific data, use spec.server instead.

Defining the store

Create a pulse.store.js file at the root of your project. Export a plain object with an optional state (default values) and a server map of async fetchers:

// pulse.store.js
export default {
  // Default / fallback values used on the server before fetchers resolve
  state: {
    user:     null,
    settings: { theme: 'dark', lang: 'en' },
    nav:      [],
  },

  // Server fetchers — run once per request, results override state defaults
  server: {
    user:     async (ctx) => db.users.findByCookie(ctx.cookies.session),
    settings: async (ctx) => db.settings.forUser(ctx.cookies.userId),
    nav:      async ()    => cms.getNavItems(),
  },
}

Registering the store

Pass your store to createServer via the store option. The store is validated at startup — bad fetchers throw before the server accepts connections:

import { createServer } from '@invisibleloop/pulse'
import store from './pulse.store.js'
import { dashboardSpec } from './src/pages/dashboard.js'
import { settingsSpec }  from './src/pages/settings.js'

createServer([dashboardSpec, settingsSpec], {
  port:      3000,
  staticDir: 'public',
  store,                 // ← register the global store
})

Using store data in a page

Declare which store keys a page needs using the store field. Those keys are merged into the server argument of the view — alongside any page-level server data:

// src/pages/dashboard.js
export default {
  route:    '/dashboard',
  store:    ['user', 'settings'],   // declare which store keys this page uses

  // Page-level server data still works alongside store data
  server: {
    stats: async (ctx) => db.stats.forUser(ctx.store.user?.id),
  },

  state: { filter: 'week' },

  view: (state, server) => `
    <main>
      <h1>Hello, ${server.user?.name ?? 'there'}</h1>
      <p>Theme: ${server.settings.theme}</p>
      <p>Stats: ${server.stats.total} requests this ${state.filter}</p>
    </main>
  `,
}

Only the keys listed in spec.store are available in the view — nothing leaks from the store to pages that do not declare a dependency on it. Page-level server keys always win if there is a name collision with the store.

Accessing the store in server fetchers

Store data is resolved before page server fetchers run. The full resolved store state is available as ctx.store in any page's server fetcher, guard, and meta functions:

export default {
  route: '/account',
  store: ['user'],

  // Guard can use ctx.store to check auth before fetching page data
  guard: async (ctx) => {
    if (!ctx.store.user) return { redirect: '/login' }
  },

  // Server fetchers receive ctx.store with the resolved store state
  server: {
    orders: async (ctx) => db.orders.forUser(ctx.store.user.id),
  },

  view: (state, server) => `
    <h1>Orders for ${server.user.name}</h1>
  `,
}

Store field reference

FieldTypeDescription
stateobjectDefault values. Used as fallbacks when a server fetcher returns undefined or the server key is absent.
serverobject of functionsAsync fetchers — async (ctx) => value. Receive the same ctx as page server fetchers. Results override state defaults.

spec.store field

FieldTypeDescription
storestring[]Array of store key strings to make available in the view's server argument. e.g. ['user', 'settings']
Pages that do not declare store receive no store data — the store never leaks to pages that do not ask for it.

Reactive updates — no refresh needed

When a page action changes store data, all other mounted pages that subscribe to the affected keys re-render immediately — no page refresh, no polling.

Return _storeUpdate from a page action's onSuccess to push a change into the global store:

// src/pages/settings.js
export default {
  route:    '/settings',
  store:    ['settings'],
  state:    { saved: false },

  actions: {
    saveTheme: {
      run: async (state, server, payload) => {
        const theme = payload.get('theme')
        await fetch('/api/settings', { method: 'PATCH', body: payload })
        return theme
      },
      onSuccess: (state, theme) => ({
        saved: true,
        _storeUpdate: { settings: { theme } },  // ← push to global store
      }),
      onError: (state, err) => ({ error: err.message }),
    },
  },

  view: (state, server) => `
    <form data-action="saveTheme">
      <select name="theme">
        <option value="dark"  ${server.settings.theme === 'dark'  ? 'selected' : ''}>Dark</option>
        <option value="light" ${server.settings.theme === 'light' ? 'selected' : ''}>Light</option>
      </select>
      <button type="submit">Save</button>
      ${state.saved ? '<p>Saved!</p>' : ''}
    </form>
  `,
}

Any other page that has store: ['settings'] will re-render with the new theme value the moment _storeUpdate is dispatched — without navigating away or refreshing.

_storeUpdate is stripped from the local page state — it is only forwarded to the store. The rest of the onSuccess return is merged into the page's own state as usual.

Caching and performance

Store fetchers run once per request, in parallel. They share the same request context as page server fetchers, so they can read cookies, params, and headers to scope data to the current user.

If your store data changes infrequently (nav items from a CMS, feature flags), consider adding a serverTtl to the relevant page spec to cache the full rendered HTML — or caching inside the fetcher itself:

// pulse.store.js — cache nav items in-process for 60 seconds
import { createCache } from './src/lib/cache.js'

const navCache = createCache(60)

export default {
  server: {
    nav: async () => navCache.getOrFetch('nav', () => cms.getNavItems()),
  },
}