@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/coreAnchor
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
): numberScalar 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 speedratePerMs: 0Paused stateratePerMs: -0.001Countdown timerSmoother
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
): numberSyncServer
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();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;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.