Skip to content

Context ​

Context is a free-form Record<string, unknown> that the client keeps in sync with the server. The autopilot reads it every turn — use it for everything that describes "where the user is right now": route, selection, app mode, theme, IDs.

Read ​

ts
const ctx = client.getContext(); // returns a deep copy

Write — three flavors ​

ts
client.setContext({ a: 1, b: 2 });            // replaces the entire context
client.updateContext({ b: 3, c: 4 });         // shallow merge → { a:1, b:3, c:4 }
client.mergeContext({ user: { name: 'Mo' } }); // deep merge

Which one to use:

  • setContext when you compute the full context from scratch on every render.
  • updateContext for the common case — "this one key changed".
  • mergeContext when you want to update a nested object without overwriting siblings.

Clear ​

ts
client.clearContext();

Owning a key ​

When a key belongs to something with a lifecycle (a page, a component), deleting it on teardown has the same remount race as unregistering functions by name: the new instance can write the key before the old one cleans up. Claim the key instead, and use the returned release:

ts
const release = client.claimContext('page');
client.updateContext({ page: { id: 42 } });

// on teardown
release(); // → true if it deleted the key

The release only deletes the key while the claim is still the latest. Claiming a key that someone else holds takes it over and deletes their value, so a new owner never inherits stale state. A claim ends (its release does nothing and returns false) when:

  • the key is claimed again,
  • a setContext() snapshot drops the key,
  • clearContext() runs.

Server-driven changes leave claims alone. hasContextClaim(key) tells you whether a key is claimed.

Sync model ​

All three writers feed the same pipeline:

your write  ──►  diff against last-synced snapshot  ──►  debounce 100 ms  ──►  send JSON Patch

Two consequences:

  • Bursty writers (a watch that fires on every keystroke, say) collapse into a single network message.
  • The server only sees the net change, not your intermediate states.

Size and depth limits ​

Every context sync is pre-flighted against the relay's limits before anything goes on the wire:

  • 64 KB cap. If the projected stored state (functions catalog + context) would exceed MAX_STATE_SIZE_BYTES (65536 bytes), the sync is aborted and a stateError event fires — e.g. context sync rejected: projected state size X bytes exceeds relay cap of 65536 bytes. The pending change is not discarded; shrink the context and it retries.
  • 10-level depth. The relay silently drops JSON-Patch ops whose path is deeper than 10 segments. The client catches this upfront and fires a stateError naming the offending path(s), so deep nesting fails loudly instead of diverging unnoticed.
ts
client.on('stateError', ({ error }) => {
  console.error('state rejected:', error.message); // shrink / flatten, then retry
});

Server snapshot reconciliation ​

When a server context snapshot arrives (the first connect, a relay echo of your own write, or a state_change from another client), the client reconciles it with any not-yet-synced local edits:

  • Unsent local edits are replayed on top of the server view — a write made between a flush and its echo is never lost.
  • If a local edit cannot replay onto the server view (the server retyped or removed a parent it touches), the server view is adopted wholesale and that one conflicting edit is dropped. This is rare and only happens after a structural change from elsewhere.

Forcing an immediate sync ​

ts
client.flushContextSync();

Use this before navigating away or closing the tab, when you cannot afford to wait the 100 ms.

ts
window.addEventListener('beforeunload', () => {
  client.flushContextSync();
});

Observing context changes ​

ts
client.on('contextUpdated', ({ context, patches }) => {
  console.log('patches sent:', patches);
});

The patches array is the RFC 6902 ops that were actually sent — useful for debugging "why did the server not see my update?".

With the Pinia store ​

If you are using useAutopilotStore(), its context property is a reactive object you can mutate directly: store.context.foo = 'bar'. A deep watcher auto-syncs mutations to the client (same 100 ms debounce). The convenience actions — store.setContextProperty('foo', 'bar') — still work and do the same thing. See the store reference.