JSPM

@yjsync/snapshot-server

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 4
  • Score
    100M100P100Q61497F
  • License MIT

Runtime-agnostic Web Request/Response handler that mirrors yjsync JSON snapshots into a pluggable SnapshotStore.

Package Exports

  • @yjsync/snapshot-server

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-server

Usage

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. Header X-Internal-Secret required (constant-time compare). Body: { roomId, revision, exportedAt?, data }. Stale revisions return 200 { accepted: false, reason: 'stale' }.
  • GET /api/rooms/:roomId/snapshot — read; runs authorize(request, roomId). Returns the stored snapshot, falls back to liveFetcher.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 SnapshotStore for 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),
})