global-slots-plan.md 5.0 KB

Global Slots Plan — Deterministic-key broadcast state with explicit opt-in

Overview

Global slots let the server broadcast shared state (e.g. a live score) to all connected clients. They use a deterministic key ("global:" + type_name) so every client references the same slot. The existing slot machinery (signing, SSE push, stale-as_at auto-refresh) handles everything else — no new transport, no new channel protocol.

Global slots are explicit opt-in: each entrypoint/action that needs a global calls .set_global(type) (current value) and/or .subscribe_global(type) (live SSE updates). Pages that don't opt in never receive the global.

Design

Deterministic key

A global slot's key is "global:" + type_name (e.g. "global:score"). This is set as the slot's id at creation, so all existing methods (get_slot, sign_slot, update, clear_slot) work with it unchanged.

Registration (startup)

statum.global<ScorePublic>("score", new ScorePublic { home = 0, away = 0 });

Creates the slot with the deterministic key and the initial model (mapped via GObjectMapping). Scope is PAGE (the server always has the authoritative copy; the client doesn't need to persist it).

Explicit opt-in (entrypoints / actions)

// Entrypoint: include the current value + subscribe for live updates
return directives()
    .set_global("score")
    .subscribe_global("score")
    .set_typed<CounterPublic>("counter", Scope.PAGE, counter);

// Action: update via update_held (client holds it from the entrypoint)
return directives().update_held<ScorePublic>("score", s => s.home += 1);

update_held resolves the held slot's key (the deterministic "global:score"), mutates, signs, and pushes to all subscribed channels. The requesting client gets a set in the response; other clients get the SSE push. Clients that miss the push refresh on their next request via the stale-as_at auto-set.

Background updates (no client request)

// From a timer, webhook, or any non-request context
yield state_service.update_global_typed<ScorePublic>("score", s => s.home += 1);

Pushes via SSE to all subscribers. Stale clients refresh on next request.

What reuses existing machinery

Mechanism How it works for globals
Signing sign_slot("global:score") — same as any slot
SSE push update("global:score", state)push_set to all channels subscribed to that key
Stale as_at auto-set HeldSlotResolver compares the client's as_at with last_touched; stale → auto set appended
Subscribe/unsubscribe subscribe directive with key "global:score"; cleanup on page close as normal

Tasks

V-1: StateService global slot support — src/StateService.vala

  • new_global_slot(string type_name, Scope scope, State state) — creates a slot with id = "global:" + type_name, caches it. If the key already exists (e.g. on restart), updates the current state.
  • update_global(string type_name, State state) async — delegates to existing update("global:" + type_name, state) (sign + push).
  • update_global_typed<TPublic>(string type_name, owned UpdateMutator<TPublic> mutator) async — typed read-mutate-write (maps via GObjectMapping, preserves private).
  • clear_global(string type_name) async — delegates to clear_slot.

V-2: DirectiveBuilder global methods — src/DirectiveBuilder.vala

  • .set_global(string type_name) — derives "global:" + type_name, calls state_service.sign_slot(key), emits set. Signs synchronously at call time (sign_slot is sync).
  • .subscribe_global(string type_name) — emits subscribe with the derived key.

V-3: StatumConfigurator.global() — src/Statum.vala
  • global<TPublic>(string type_name, TPublic initial) — injects StateService, maps the initial model via GObjectMapping.to_properties, calls state_service.new_global_slot(type_name, Scope.PAGE, state).

V-4: Example — example/

  • Add a global "announcement" slot to the example (simple string) to demonstrate the flow: registered at startup, set_global + subscribe_global in the home entrypoint, updated by a background timer.
  • No new page needed; the home page binds stm-text="announcement.text".

D-1: Documentation

  • model.md: add a "Global slots" section describing the deterministic-key model, explicit opt-in via set_global / subscribe_global, background updates via update_global_typed, and the SSE/stale-refresh fan-out.
  • getting-started.md: add set_global / subscribe_global / update_global_typed to the DirectiveBuilder and StateService tables; add a "Global state" quick-start example.

T-1: Runtime test

  • Register a global at startup.
  • Verify the entrypoint (with set_global + subscribe_global) returns the global's set + subscribe.
  • Verify update_held<T>("global_type", ...) in an action pushes the update (the requesting client gets set; a second client's stale as_at gets an auto-set on its next request).