@yjsync/snapshot-server
v0.1.0
Published
Runtime-agnostic Web Request/Response handler that mirrors yjsync JSON snapshots into a pluggable SnapshotStore.
Maintainers
Readme
@yjsync/snapshot-server
Runtime-agnostic Web Request/Response handler that mirrors yjsync JSON snapshots into a pluggable SnapshotStore. Serves authenticated reads with an optional fallback to a live source (e.g. the Cloudflare DO /__snapshot endpoint).
Built on Web standards (Request/Response/fetch) — works on Cloudflare Workers, Bun, Deno, and Node 20+.
Install
bun add @yjsync/snapshot-serverUsage
import {
createSnapshotHandler,
MemorySnapshotStore,
} from '@yjsync/snapshot-server'
const store = new MemorySnapshotStore()
const handle = createSnapshotHandler({
store,
internalSecret: env.INTERNAL_SNAPSHOT_SECRET,
authorize: async (request, roomId) => {
return await myAuthCheck(request, roomId)
},
})
export default {
async fetch(request: Request): Promise<Response> {
const handled = await handle(request)
if (handled) return handled
// …your other routes…
return new Response('Not Found', { status: 404 })
},
}Routes (defaults; both overrideable via routes)
POST /api/internal/snapshot— internal upsert. HeaderX-Internal-Secretrequired (constant-time compare). Body:{ roomId, revision, exportedAt?, data }. Stale revisions return200 { accepted: false, reason: 'stale' }.GET /api/rooms/:roomId/snapshot— read; runsauthorize(request, roomId). Returns the stored snapshot, falls back toliveFetcher.fetch(roomId)if provided, otherwise{ data: null, source: 'none' }.
For paths it does not own, the handler returns null so it composes with your existing router.
SnapshotStore contract
interface SnapshotStore {
get(roomId: string): Promise<Snapshot | null>
put(roomId: string, snap: Snapshot): Promise<PutResult>
}Implementations MUST guarantee monotonicity: put for a given room never regresses the stored revision. Older revisions return { accepted: false, reason: 'stale' }; equal revisions return 'duplicate'. This is what fixes the latent bug where a late retry of an old export overwrote newer state.
Available stores
MemorySnapshotStore— in this package. For tests / local dev.@yjsync/snapshot-store-d1— Cloudflare D1.- Bring your own — implement
SnapshotStorefor Postgres, KV, R2, etc.
With Cloudflare Durable Objects
import { createSnapshotHandler } from '@yjsync/snapshot-server'
import { D1SnapshotStore } from '@yjsync/snapshot-store-d1'
import { createDoLiveFetcher } from '@yjsync/cloudflare'
const handle = createSnapshotHandler({
store: new D1SnapshotStore(env.DB),
liveFetcher: createDoLiveFetcher({
binding: env.YJS_ROOM,
secret: env.INTERNAL_SNAPSHOT_SECRET,
}),
internalSecret: env.INTERNAL_SNAPSHOT_SECRET,
authorize: async (req, roomId) => isAllowed(req, roomId),
})