/** * `@syncular/server-workers` — the Cloudflare Workers entry. * * This is deliberately thin. `createSyncularHono` (server-hono) is already * Workers-native: it routes with Hono (which runs unmodified on `workerd`) * and speaks only Web `Request`/`Response`/`fetch`/Web-Crypto — nothing * Bun- or Node-specific. So the Workers lane is *not* a second adapter; it * is the same HTTP handler wired to Workers bindings: * * - **D1** → `D1ServerStorage` (the sqlite-family storage over the D1 * binding, §4.2); * - **R2 / secrets** → the segment store, blob store, and signed-URL * config the host assembles from `env` (R2-as-S3 via `S3SegmentStore` + * `s3PresignedUrls`, or a memory store for tests); * - **secrets** → whatever `authenticate` needs. * * ## Realtime (§8): the Durable Object * * The realtime channel (`GET /realtime`, §1.1 second binding) needs a * durable, stateful WebSocket host. On Workers that is a **Durable Object** — * `SyncularRealtimeDO` (`realtime-do.ts`): one DO per partition hosting the * `RealtimeHub`, WebSocket hibernation driving the existing `RealtimeSession`, * in-DO commit fan-out, storage over the same D1 binding. Pass a * `realtime` option to mount `/realtime`; its namespace also coordinates D1 * `/sync`. An HTTP-only D1 deployment uses the `coordinator` option instead: * WebSockets are optional, the per-partition push queue is not. */ import { type D1Database, D1ServerStorage, type StoredCommit, type SyncServerConfig } from '@syncular/server'; import { type SyncularHonoOptions } from '@syncular/server-hono'; import { type RealtimeUpgradeIdentity } from './realtime-do.js'; export { type D1Database, D1ServerStorage } from '@syncular/server'; export * from './realtime-do.js'; /** * Build the request-scoped config + auth for one Worker invocation from the * Worker's `env` (and `ctx`, e.g. for `waitUntil`). Runs per request so * bindings resolved from `env` (D1, R2, secrets) are always the live ones. * Return the `SyncServerConfig` the core handler needs plus the host * `authenticate` callback (§1.1). */ export type WorkersConfigFactory = (env: Env, ctx: ExecutionContextLike) => SyncularHonoOptions | Promise; /** The subset of `ExecutionContext` this entry passes through. */ export interface ExecutionContextLike { waitUntil(promise: Promise): void; passThroughOnException?(): void; } /** A DO stub — the callable handle to one Durable Object instance. */ export interface DurableObjectStubLike { fetch(request: Request): Promise; } /** A DO namespace binding: `idFromName` → `get(id)` → a stub. */ export interface DurableObjectNamespaceLike { idFromName(name: string): DurableObjectIdLike; get(id: DurableObjectIdLike): DurableObjectStubLike; } export interface DurableObjectIdLike { toString(): string; } /** * Realtime wiring for `createWorkersFetchHandler`. Supplying it mounts the * `GET /realtime` upgrade route and uses the same namespace as the D1 * `/sync` coordinator. */ export interface WorkersRealtimeOptions { /** The DO namespace binding (wrangler `[[durable_objects.bindings]]`). */ readonly namespace: DurableObjectNamespaceLike; /** * Resolve the §8 upgrade identity from the incoming `GET /realtime` request. * This is the realtime-channel authentication seam — the analogue of the * HTTP handler's `authenticate`. Return `undefined` to reject the upgrade * (a 401). The `partition` selects the DO (one DO per partition). */ readonly authenticate: (request: Request) => RealtimeUpgradeIdentity | undefined | Promise; /** The mount path segment for the upgrade route; default `/realtime`. */ readonly path?: string; } /** Resolve the per-env realtime wiring for one Worker invocation. */ export type WorkersRealtimeFactory = (env: Env, ctx: ExecutionContextLike) => WorkersRealtimeOptions | Promise; /** Per-partition Durable Object boundary for D1 sync rounds without WS. */ export interface WorkersCoordinatorOptions { readonly namespace: DurableObjectNamespaceLike; } export type WorkersCoordinatorFactory = (env: Env, ctx: ExecutionContextLike) => WorkersCoordinatorOptions | Promise; export interface WorkersFetchHandlerOptions { /** Build the HTTP handler config + auth per request (see the type doc). */ readonly config: WorkersConfigFactory; /** * Serialize D1 `/sync` rounds through one Durable Object per partition. * Required for D1 pushes when `realtime` is omitted. If `realtime` is * present its namespace is the coordinator automatically. */ readonly coordinator?: WorkersCoordinatorFactory; /** * Realtime (§8) over a Durable Object. Omit for an HTTP-only deployment * (still fully conformant — clients sync over `POST /sync`). */ readonly realtime?: WorkersRealtimeFactory; } /** * Wrap a config factory (or a `{ config, realtime }` options object) into a * Workers module `fetch` handler: * * ```ts * export default { * fetch: createWorkersFetchHandler((env: Env) => ({ * config: syncConfig(env), * authenticate: (req) => authenticate(req, env), * })), * }; * ``` * * With realtime over a Durable Object, pass the options form and thread the * `durableObjectRealtimeNotifier` into the config's `realtime` so HTTP pushes * wake the partition's DO: * * ```ts * export default { * fetch: createWorkersFetchHandler({ * config: (env) => ({ * config: { * ...syncConfig(env), * realtime: durableObjectRealtimeNotifier(env.REALTIME), * }, * authenticate: (req) => authenticate(req, env), * }), * realtime: (env) => ({ * namespace: env.REALTIME, * authenticate: (req) => authenticateRealtime(req, env), * }), * }), * }; * export { SyncularRealtimeDO } from './realtime-do-class.js'; * ``` * * The returned handler builds the Hono app once per request from the factory * and delegates to it. Hono is cheap to construct; building per request keeps * the handler stateless (no module-global mutable server), which is the * Workers-correct posture — each invocation may run on a fresh isolate. */ export declare function createWorkersFetchHandler(factoryOrOptions: WorkersConfigFactory | WorkersFetchHandlerOptions): (request: Request, env: Env, ctx: ExecutionContextLike) => Promise; /** * Forward an authenticated HTTP sync round to the partition's Durable Object. * Pulls and pushes share this path so client-record updates and push apply use * one ordered partition boundary. Other HTTP routes remain direct. */ export declare function forwardSyncRequest(request: Request, namespace: DurableObjectNamespaceLike, identity: { readonly partition: string; readonly actorId: string; }): Promise; /** * Forward a `/realtime` upgrade to the partition's DO stub. The identity is * carried on internal headers to the DO's upgrade endpoint (the DO trusts the * Worker to have authenticated — the DO namespace is private to the Worker). * The DO is selected by `idFromName(partition)`: one DO per partition (§8.2). */ export declare function forwardRealtimeUpgrade(request: Request, namespace: DurableObjectNamespaceLike, identity: RealtimeUpgradeIdentity): Promise; /** * A `RealtimeNotifier` (§8.2) for an external authoritative command host that * already serializes its D1 writes and must wake the partition's DO. The DO * calls `hub.wake(partition, 'catchup-required')` and its sockets re-pull the * delta from the shared D1 (§8.3) — the Workers in-platform analogue of the * Postgres LISTEN/NOTIFY fan-out. A wake, not a byte re-broadcast. * * Ordinary Workers `/sync` does not need this: it already lands on the DO and * fans out in-process. This wake is fire-and-forget: * a DO fetch failure never fails the push (the commit is already durable in * D1; the client's next pull or reconnect self-heals). */ export declare function durableObjectRealtimeNotifier(namespace: DurableObjectNamespaceLike): { notifyCommit: (partition: string, commit: StoredCommit) => Promise; }; /** * Convenience: a `D1ServerStorage` over a Worker's D1 binding. `migrate` is * NOT called here — apply the schema with `wrangler d1 migrations` (see the * README + `wrangler.toml` example) so cold requests never race a DDL apply. */ export declare function d1Storage(binding: D1Database): D1ServerStorage; /** Re-export the shared config type for host `configFactory` signatures. */ export type { SyncServerConfig, SyncularHonoOptions };