Core is the minimum protocol for deterministic state extrapolation. Consumers own all domain semantics.
Syncframe enables synchronized state across browsers using dead-reckoning. Instead of streaming state continuously, clients broadcast anchors: "at server time T, value was V, and from then it evolves at rate R." Any client with a synced server clock can evaluate the current value at any later moment using pure math.
Core provides exactly one primitive: Anchor<T, M> — deterministic state at a server time + motion descriptor. The only built-in motion shape is ScalarMotion (a number advancing at a constant rate).
interface ScalarMotion {
kind: 'scalar';
ratePerMs: number; // units per millisecond
}
interface Anchor<T, M> {
at: number; // server timestamp
value: T; // value at time `at`
motion: M; // how value evolves from `at`
}Consumers define their own motion shapes for 2D paths, color interpolation, bouncing, exponential ramps, or any domain-specific behavior. Core's store accepts them as opaque JSON without inspection.
- Video playback sync: Anchor at time T, value = playback position, rate = 1.0 (or 0 for paused, 2.0 for 2x speed)
- Multi-screen installations: Positional data synced across displays
- Countdown timers: Rate = -1.0 for reverse counting
- Progress bars: Rate = percentage per millisecond
With a browser and internet connection, clients extrapolate state deterministically from shared anchors. Clock sync typically lands within ~30ms of server time — enough for video, timers, and UI; not sample-accurate.
Layer 1 (core): Time-event sync protocol — clock synchronization, anchors, evaluators, storage, transport. Core knows nothing about XY coordinates, viewports, or screens.
Layer 2 (spatial): Built on core — world size, viewport management, screen poses, bounding boxes, multi-screen orchestration. Spatial owns all positional semantics.
| Package | Entry point | What it is |
|---|---|---|
@syncframe/core |
@syncframe/core/server |
React-free protocol + server surface: types, evaluateScalar, smoother, SyncStore/SyncTransport + defaults, SyncServer |
@syncframe/core |
@syncframe/core/react |
Client hooks: useServerClock, useAnchor, useScalarAnchor, useSmoothedValue |
@syncframe/core |
@syncframe/core |
Full barrel (everything) — convenience; pulls React in |
@syncframe/redis |
@syncframe/redis |
Redis-backed SyncStore + SyncTransport adapters (connection-injected) |
@syncframe/spatial |
@syncframe/spatial |
Layer 2 — screens, poses, world bbox (in progress) |
Import server code from @syncframe/core/server and client code from @syncframe/core/react so React hooks (and their DOM types) never leak into server bundles.
npm install @syncframe/core # protocol, server, hooks
npm install @syncframe/redis ioredis # optional: Redis backendCore exposes two focused entry points (plus the full @syncframe/core barrel for convenience): @syncframe/core/server for the React-free protocol/server surface, and @syncframe/core/react for the client hooks.
import { SyncServer, InMemoryStore, EventEmitterTransport } from '@syncframe/core/server';
const server = new SyncServer({
store: new InMemoryStore(),
transport: new EventEmitterTransport(),
});
// Broadcast a scalar anchor (e.g., video playback position)
await server.setAnchor('room1', 'video', {
at: server.clockProbe(),
value: 45.5, // 45.5 seconds
motion: { kind: 'scalar', ratePerMs: 0.001 }, // 1 second per second
});
// Publish update to subscribers
await server.publishUpdate('room1');On the client:
import { evaluateScalar } from '@syncframe/core/server';
// Given an anchor and synced server time, evaluate current value
const currentValue = evaluateScalar(anchor, serverNowMs);evaluateScalar(anchor, serverNowMs)— Pure function:value + ratePerMs * (serverNowMs - at)smoothStep(ideal, current, dtMs)— Exponential chase smoother to hide jitterSyncStore— Pluggable storage (InMemoryStore, Redis, Postgres, etc.)SyncTransport— Pluggable pub/sub (EventEmitter, WebSocket, Redis, etc.)SyncServer— Orchestrates store + transport- React hooks —
useServerClock,useAnchor,useScalarAnchor,useSmoothedValue
Core ships zero-dependency defaults:
InMemoryStore— Perfect for local dev, single-process demosEventEmitterTransport— Node.js EventEmitter for single-process pub/sub
For multi-process / serverless deployments, @syncframe/redis provides RedisStore + RedisTransport. They're connection-injected — you pass in your own ioredis clients, so auth and pooling stay in your app:
import { SyncServer } from '@syncframe/core/server';
import { RedisStore, RedisTransport } from '@syncframe/redis';
const server = new SyncServer({
store: new RedisStore({ redis }),
transport: new RedisTransport({ redis, createSubscriber }),
});Or implement the interfaces yourself for any other backend:
class PostgresStore implements SyncStore { /* ... */ }
class WebSocketTransport implements SyncTransport { /* ... */ }Same inputs always produce bit-identical outputs. No Date.now(), no Math.random(), no DOM probes — every input is in the anchor itself.
const anchor = { at: 1000, value: 10, motion: { kind: 'scalar', ratePerMs: 0.001 } };
evaluateScalar(anchor, 2000); // 11
evaluateScalar(anchor, 6000); // 15
evaluateScalar(anchor, 1000); // 10Clients probe the server repeatedly using NTP-style RTT minimization:
import { useServerClock } from '@syncframe/core/react';
const { serverNowMs, offsetMs, rttMs } = useServerClock('/api/clock');The hook estimates server time offset by keeping the min-RTT sample across multiple probes.
Core's store is opaque — it accepts any motion shape with a kind discriminator:
// 2D position (spatial's concern)
await server.setAnchor('room1', 'player', {
at: 1000,
value: { x: 100, y: 200 },
motion: { kind: 'linear2d', vx: 0.05, vy: 0.03 },
});
// Color interpolation
await server.setAnchor('room1', 'light', {
at: 2000,
value: [255, 100, 50],
motion: { kind: 'color-lerp', target: [0, 0, 0], durationMs: 1000 },
});
// Your own evaluator
function evaluateLinear2d(anchor, serverNowMs) {
const dt = serverNowMs - anchor.at;
return {
x: anchor.value.x + anchor.motion.vx * dt,
y: anchor.value.y + anchor.motion.vy * dt,
};
}- Minimal core — Only scalar time-progression is built-in
- Opaque storage — Core accepts any
motion.kindwithout inspection - Consumer ownership — Play/pause/speed UI, 2D paths, color interpolation are consumer responsibilities
- Deterministic evaluation — Pure functions, no side effects, bit-identical across clients
- Pluggable backends — Storage and transport are interfaces, not implementations
- Graceful degradation — If network drops, clients extrapolate from cached anchors
MIT
The monorepo includes a Next.js docs + demo app at apps/site (Vercel deploy root). Run it from the repo root:
pnpm dev # localhost:3000
pnpm --filter site build:check # isolated build while dev server runsLive pages: / (landing), /docs/core, /demo/timer (global synced timer), /demo/dot, /demo/ring. Site UI conventions: STYLE.md.