Layer 1

@syncframe/core

Minimum protocol for deterministic state extrapolation.

Overview

Core provides the foundational sync protocol: clock synchronization, anchors, evaluators, smoother, and pluggable storage/transport.

pnpm add @syncframe/core

Anchor

An anchor describes deterministic state at a server time. Given an anchor and server time, any client can evaluate the current value.

// Anchor<T, M>
interface Anchor<T, M> {
  at: number;        // server timestamp
  value: T;          // value at time 'at'
  motion: M;         // motion descriptor
}

Evaluator

Pure function: (anchor, serverTime) → current value. Same inputs always produce the same output. Deterministic across all clients.

// evaluateScalar
function evaluateScalar(
  anchor: Anchor<number, ScalarMotion>,
  serverTimeMs: number
): number
Returns: value + (serverTimeMs - anchor.at) × motion.ratePerMs

Scalar motion

Core ships with scalar motion: a number that changes at a constant rate per millisecond. Consumers define their own motion shapes for complex trajectories.

interface ScalarMotion {
  kind: 'scalar';
  ratePerMs: number;  // units per millisecond
}
ratePerMs: 0.001Video at 1x speed
ratePerMs: 0Paused state
ratePerMs: -0.001Countdown timer

Smoother

Exponential chase interpolation hides network jitter. Tracks evaluated value smoothly, snaps on large discontinuities (seek, pause).

function smoothStep(
  current: number,
  target: number,
  dt: number,
  options?: SmootherOptions
): number

SyncServer

Server-side entry point: anchor CRUD, meta patches, snapshot build, and pub/sub fan-out. Each instance is bound to one namespace at construction (default default). Need multiple isolated scopes? Run multiple SyncServer instances over a shared store.

import { SyncServer, InMemoryStore, EventEmitterTransport } from '@syncframe/core/server';

const server = new SyncServer({
  store: new InMemoryStore(),
  transport: new EventEmitterTransport(),
  namespace: 'timer',
});

await server.setAnchor('timer', {
  at: server.clockProbe(),
  value: 60,
  motion: { kind: 'scalar', ratePerMs: -0.001 },
});
await server.publishUpdate();
// SSE routes: repair channel registry before buildSnapshot()
await server.ensureAnchor('timer', () => defaultAnchor(Date.now()));
const snapshot = await server.buildSnapshot();
ensureAnchor re-registers the channel set so listAnchors matches getAnchor — important for snapshot streams.

React hooks

Client entry point. Hooks subscribe to an SSE endpoint that emits CoreSnapshot JSON. Multiple hooks on the same page share one EventSource per stream URL.

import { useServerClock, useAnchor } from '@syncframe/core/react';
import { evaluateScalar } from '@syncframe/core/server';

const clock = useServerClock('/api/clock');
const anchor = useAnchor<number>('timer', '/api/timer/stream');

// In a rAF loop:
const value = anchor
  ? evaluateScalar(anchor, clock.serverNow())
  : null;
No roomId on hooks — partition scope is chosen server-side when the stream route wires SyncServer.

Pluggable storage

Core is backend-agnostic. Ships with in-memory defaults; @syncframe/redis provides production store + transport. Implement SyncStore for other backends — the low-level interface still takes an explicit namespace partition key if you use the store directly.

Next steps