@parlor/sync 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/sync-client.d.ts +2 -0
- package/dist/client/sync-client.d.ts.map +1 -1
- package/dist/client/sync-client.js +4 -2
- package/dist/client/sync-client.js.map +1 -1
- package/dist/server/event-stream.d.ts +27 -0
- package/dist/server/event-stream.d.ts.map +1 -0
- package/dist/server/event-stream.js +82 -0
- package/dist/server/event-stream.js.map +1 -0
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +80 -34
- package/dist/server/routes.js.map +1 -1
- package/dist/server/store.d.ts +1 -0
- package/dist/server/store.d.ts.map +1 -1
- package/dist/server/store.js +1 -1
- package/dist/server/store.js.map +1 -1
- package/package.json +3 -3
|
@@ -15,6 +15,8 @@ export interface SyncClientOptions {
|
|
|
15
15
|
readonly isHidden?: (() => boolean) | undefined;
|
|
16
16
|
readonly setTimeout?: typeof globalThis.setTimeout | undefined;
|
|
17
17
|
readonly clearTimeout?: typeof globalThis.clearTimeout | undefined;
|
|
18
|
+
/** Picks each reconnect delay below its backoff ceiling. Defaults to `Math.random`. */
|
|
19
|
+
readonly random?: (() => number) | undefined;
|
|
18
20
|
readonly channels?: readonly ClientChannel[] | undefined;
|
|
19
21
|
/**
|
|
20
22
|
* Read each time the next poll is scheduled; the interval is the larger of this and the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../../src/client/sync-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../../src/client/sync-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -56,6 +56,7 @@ export function createSyncClient(options) {
|
|
|
56
56
|
};
|
|
57
57
|
const schedule = options.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));
|
|
58
58
|
const cancel = options.clearTimeout ?? ((id) => globalThis.clearTimeout(id));
|
|
59
|
+
const random = options.random ?? Math.random;
|
|
59
60
|
const isHidden = options.isHidden ?? (() => false);
|
|
60
61
|
const channels = new Map((options.channels ?? []).map((c) => [c.event, c]));
|
|
61
62
|
const guarded = (run) => {
|
|
@@ -296,10 +297,11 @@ export function createSyncClient(options) {
|
|
|
296
297
|
pollTimer = undefined;
|
|
297
298
|
(options.onUnauthorized ?? settings.onError)?.(error);
|
|
298
299
|
};
|
|
300
|
+
// Full jitter, so clients that lost one server together do not return in lockstep.
|
|
299
301
|
const backoff = () => {
|
|
300
|
-
const
|
|
302
|
+
const ceiling = reconnectDelay;
|
|
301
303
|
reconnectDelay = Math.min(reconnectDelay * 2, settings.reconnectMaxMs);
|
|
302
|
-
return
|
|
304
|
+
return Math.round(ceiling * random());
|
|
303
305
|
};
|
|
304
306
|
let connecting = false;
|
|
305
307
|
const connect = async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-client.js","sourceRoot":"","sources":["../../src/client/sync-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientChannel } from \"../channel.js\";\nimport {\n compareCursors,\n cursorOf,\n decodeCursor,\n encodeCursor,\n INITIAL_CURSOR,\n LEGACY_KEEP_ALIVE_MS,\n type SyncBatch,\n type SyncCursor,\n type SyncEvent,\n type SyncNegotiation,\n streamLivenessMs,\n} from \"../protocol.js\";\nimport { readSse } from \"./sse.js\";\n\nexport type SyncRequest = (path: string, init?: RequestInit) => Promise<Response>;\n\nexport interface SyncClientOptions {\n readonly request: SyncRequest;\n readonly basePath?: string | undefined;\n readonly clientId?: string | undefined;\n readonly onEvents: (events: SyncEvent[]) => void;\n readonly onError?: ((error: unknown) => void) | undefined;\n readonly coalesceMs?: number | undefined;\n readonly safetyPollMs?: number | undefined;\n readonly hiddenPollMs?: number | undefined;\n readonly reconnectBaseMs?: number | undefined;\n readonly reconnectMaxMs?: number | undefined;\n readonly isHidden?: (() => boolean) | undefined;\n readonly setTimeout?: typeof globalThis.setTimeout | undefined;\n readonly clearTimeout?: typeof globalThis.clearTimeout | undefined;\n readonly channels?: readonly ClientChannel[] | undefined;\n /**\n * Read each time the next poll is scheduled; the interval is the larger of this and the\n * negotiated one, so a caller can only slow polling down.\n */\n readonly pollEveryMs?: (() => number) | undefined;\n /**\n * Where to read from while the client has no cursor of its own: the encoded cursor of the\n * oldest data the page holds, so the first stream does not replay what that data already\n * shows. Defaults to the start of the retained log.\n */\n readonly startAt?: (() => string) | undefined;\n /**\n * The server could not replay every event since the client's cursor: the client fell\n * more than one batch behind, or behind what the server keeps. Sync continues from\n * `cursor`, where the client's streams stand now; reread whatever predates it. Without\n * it, the client is never reset and a poll pages through the backlog.\n */\n readonly onReset?: ((cursor: string) => void) | undefined;\n /** Bounds a negotiation or a poll, not the SSE stream. Defaults to 10 s. */\n readonly requestTimeoutMs?: number | undefined;\n /**\n * What the server negotiated, each time it does; `SyncClient.transport` reads `poll`\n * during the fallback poll of a failed negotiation, this never does.\n */\n readonly onNegotiated?: ((transport: \"sse\" | \"poll\") => void) | undefined;\n /**\n * The negotiated transport's connection came back after a failure: a stream reopened\n * after it dropped, or a poll succeeded after a failed one. A fallback poll on an SSE\n * deployment does not count.\n */\n readonly onResume?: (() => void) | undefined;\n /**\n * The server refused the credential (401 or 403). The client never degrades to polling\n * on it; it keeps retrying the negotiation with backoff, unless this calls `stop`.\n * Defaults to `onError`.\n */\n readonly onUnauthorized?: ((error: SyncUnauthorized) => void) | undefined;\n}\n\nexport class SyncUnauthorized extends Error {\n override readonly name = \"SyncUnauthorized\";\n constructor(readonly status: number) {\n super(`sync was refused with HTTP ${status}`);\n }\n}\n\nexport class SyncRequestTimeout extends Error {\n override readonly name = \"SyncRequestTimeout\";\n constructor(readonly ms: number) {\n super(`sync request timed out after ${ms} ms`);\n }\n}\n\nclass StaleRequest extends Error {\n override readonly name = \"StaleRequest\";\n}\n\nconst refused = (response: Response) =>\n response.status === 401 || response.status === 403;\n\nexport interface SyncClient {\n start(): void;\n stop(): void;\n /**\n * Registers a channel after `start`, for features loaded lazily. It opens at once when\n * the live stream's handshake lists it, else on the next stream. Returns the detach.\n */\n addChannel(channel: ClientChannel): () => void;\n /**\n * Skips the remaining reconnect backoff, for `online` and visibility events. While\n * polling it polls at once; with a stream open or a request in flight it does nothing.\n */\n reconnectNow(): void;\n readonly cursor: string;\n readonly transport: \"sse\" | \"poll\" | undefined;\n}\n\nconst DEFAULTS = {\n coalesceMs: 250,\n safetyPollMs: 60_000,\n hiddenPollMs: 10_000,\n reconnectBaseMs: 1000,\n reconnectMaxMs: 30_000,\n requestTimeoutMs: 10_000,\n};\n\ninterface Handshake {\n readonly capabilities: readonly string[];\n readonly keepAliveMs: number | undefined;\n}\n\nfunction parseHandshake(handshake: string): Handshake {\n try {\n const { capabilities, keepAliveMs } = JSON.parse(handshake) as {\n capabilities?: unknown;\n keepAliveMs?: unknown;\n };\n return {\n capabilities: Array.isArray(capabilities)\n ? capabilities.filter((c): c is string => typeof c === \"string\")\n : [],\n keepAliveMs:\n typeof keepAliveMs === \"number\" && keepAliveMs > 0 ? keepAliveMs : undefined,\n };\n } catch {\n return { capabilities: [], keepAliveMs: undefined };\n }\n}\n\nexport function createSyncClient(options: SyncClientOptions): SyncClient {\n const basePath = options.basePath ?? \"/_parlor\";\n const settings = {\n coalesceMs: options.coalesceMs ?? DEFAULTS.coalesceMs,\n safetyPollMs: options.safetyPollMs ?? DEFAULTS.safetyPollMs,\n hiddenPollMs: options.hiddenPollMs ?? DEFAULTS.hiddenPollMs,\n reconnectBaseMs: options.reconnectBaseMs ?? DEFAULTS.reconnectBaseMs,\n reconnectMaxMs: options.reconnectMaxMs ?? DEFAULTS.reconnectMaxMs,\n requestTimeoutMs: options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs,\n clientId: options.clientId,\n onError: options.onError,\n };\n const schedule: (fn: () => void, ms: number) => ReturnType<typeof setTimeout> =\n options.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));\n const cancel: (id: ReturnType<typeof setTimeout>) => void =\n options.clearTimeout ?? ((id) => globalThis.clearTimeout(id));\n const isHidden = options.isHidden ?? (() => false);\n const channels = new Map((options.channels ?? []).map((c) => [c.event, c]));\n const guarded = (run: () => void) => {\n try {\n run();\n } catch (error) {\n settings.onError?.(error);\n }\n };\n\n let cursor: SyncCursor = INITIAL_CURSOR;\n let transport: \"sse\" | \"poll\" | undefined;\n let negotiated: \"sse\" | \"poll\" | undefined;\n let interrupted = false;\n let running = false;\n let controller: AbortController | undefined;\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setTimeout> | undefined;\n let pending: SyncEvent[] = [];\n let reconnectDelay = settings.reconnectBaseMs;\n let endReconnectWait: (() => void) | undefined;\n let connectTimer: ReturnType<typeof setTimeout> | undefined;\n let epoch = 0;\n const inflight = new Set<AbortController>();\n\n const withTimeout = async <T>(run: (signal: AbortSignal) => Promise<T>): Promise<T> => {\n const mine = epoch;\n const own = new AbortController();\n inflight.add(own);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const expired = new Promise<never>((_, reject) => {\n timer = schedule(() => {\n own.abort();\n reject(new SyncRequestTimeout(settings.requestTimeoutMs));\n }, settings.requestTimeoutMs);\n });\n try {\n const result = await Promise.race([run(own.signal), expired]);\n if (mine !== epoch) throw new StaleRequest();\n return result;\n } catch (error) {\n if (mine !== epoch) throw new StaleRequest();\n throw error;\n } finally {\n inflight.delete(own);\n if (timer !== undefined) cancel(timer);\n }\n };\n\n const resumed = () => {\n if (!interrupted) return;\n interrupted = false;\n guarded(() => options.onResume?.());\n };\n\n const accept = (events: readonly SyncEvent[]) => {\n for (const event of events) {\n const at = cursorOf(event);\n if (compareCursors(at, cursor) <= 0) continue;\n cursor = at;\n if (event.source && event.source === settings.clientId) continue;\n pending.push(event);\n }\n if (pending.length > 0 && flushTimer === undefined) {\n flushTimer = schedule(() => {\n flushTimer = undefined;\n const batch = pending;\n pending = [];\n options.onEvents(batch);\n }, settings.coalesceMs);\n }\n };\n\n const held = () => (cursor === INITIAL_CURSOR ? \"\" : encodeCursor(cursor));\n const since = () =>\n cursor === INITIAL_CURSOR ? (options.startAt?.() ?? \"\") : encodeCursor(cursor);\n const missedSince = () =>\n `since=${encodeURIComponent(since())}${options.onReset ? \"&reset=1\" : \"\"}`;\n\n const take = (batch: SyncBatch) => {\n if (!batch.reset) {\n accept(batch.events);\n return;\n }\n cursor = decodeCursor(batch.cursor);\n guarded(() => options.onReset?.(batch.cursor));\n };\n\n const poll = async (mine: number) => {\n const batch = await withTimeout(async (signal) => {\n const response = await options.request(`${basePath}/sync?${missedSince()}`, {\n signal,\n });\n if (refused(response)) throw new SyncUnauthorized(response.status);\n if (!response.ok) throw new Error(`sync poll failed with HTTP ${response.status}`);\n return (await response.json()) as SyncBatch;\n });\n if (mine !== epoch) throw new StaleRequest();\n take(batch);\n if (running && negotiated === \"poll\") resumed();\n };\n\n const schedulePoll = (ms: number) => {\n if (!running) return;\n if (pollTimer !== undefined) cancel(pollTimer);\n pollTimer = schedule(() => {\n pollTimer = undefined;\n let halted = false;\n const mine = epoch;\n void poll(mine)\n .catch((error: unknown) => {\n if (error instanceof StaleRequest || mine !== epoch) {\n halted = true;\n return;\n }\n if (negotiated === \"poll\") interrupted = true;\n if (error instanceof SyncUnauthorized) {\n halted = true;\n unauthorized(error);\n if (transport === \"poll\") {\n transport = undefined;\n connectTimer = schedule(() => {\n connectTimer = undefined;\n void connect();\n }, backoff());\n }\n return;\n }\n settings.onError?.(error);\n })\n .finally(() => {\n if (!halted) schedulePoll(nextPollDelay());\n });\n }, ms);\n };\n\n const nextPollDelay = () => {\n if (transport === \"sse\") return settings.safetyPollMs;\n const base = Math.max(negotiatedPollMs, options.pollEveryMs?.() ?? 0);\n return isHidden() ? Math.max(base, settings.hiddenPollMs) : base;\n };\n let negotiatedPollMs = 2000;\n\n const negotiate = async (): Promise<SyncNegotiation> => {\n return withTimeout(async (signal) => {\n const response = await options.request(`${basePath}/sync/token`, {\n method: \"POST\",\n signal,\n });\n if (refused(response)) throw new SyncUnauthorized(response.status);\n if (!response.ok)\n throw new Error(`sync negotiation failed with HTTP ${response.status}`);\n return (await response.json()) as SyncNegotiation;\n });\n };\n\n interface LiveStream {\n listed: ReadonlySet<string>;\n readonly open: Set<ClientChannel>;\n }\n let live: LiveStream | undefined;\n\n const stream = async (endpoint: string) => {\n const own = new AbortController();\n controller = own;\n let keepAliveMs = LEGACY_KEEP_ALIVE_MS;\n let silent = false;\n let watchdog: ReturnType<typeof setTimeout> | undefined;\n const rearm = () => {\n if (watchdog !== undefined) cancel(watchdog);\n watchdog = schedule(() => {\n silent = true;\n own.abort();\n }, streamLivenessMs(keepAliveMs));\n };\n const open = new Set<ClientChannel>();\n const current: LiveStream = { listed: new Set(), open };\n live = current;\n rearm();\n try {\n const response = await options.request(`${endpoint}?${missedSince()}`, {\n headers: { accept: \"text/event-stream\" },\n signal: own.signal,\n });\n if (refused(response)) throw new SyncUnauthorized(response.status);\n if (!response.ok || !response.body) {\n throw new Error(`sync stream failed with HTTP ${response.status}`);\n }\n reconnectDelay = settings.reconnectBaseMs;\n resumed();\n await readSse(\n response.body,\n (frame) => {\n if (frame.event === \"message\") {\n const batch = JSON.parse(frame.data) as SyncBatch;\n take(batch);\n const parsed = decodeCursor(batch.cursor);\n if (compareCursors(parsed, cursor) > 0) cursor = parsed;\n return;\n }\n if (frame.event === \"handshake\") {\n const handshake = parseHandshake(frame.data);\n keepAliveMs = handshake.keepAliveMs ?? LEGACY_KEEP_ALIVE_MS;\n rearm();\n current.listed = new Set(handshake.capabilities);\n for (const event of handshake.capabilities) {\n const channel = channels.get(event);\n if (!channel || open.has(channel)) continue;\n open.add(channel);\n guarded(() => channel.opened());\n }\n return;\n }\n const channel = channels.get(frame.event);\n if (channel && open.has(channel)) guarded(() => channel.receive(frame.data));\n },\n own.signal,\n rearm,\n );\n } catch (error) {\n if (!silent) throw error;\n } finally {\n if (watchdog !== undefined) cancel(watchdog);\n if (live === current) live = undefined;\n for (const channel of open) guarded(() => channel.closed());\n }\n };\n\n const unauthorized = (error: SyncUnauthorized) => {\n if (pollTimer !== undefined) cancel(pollTimer);\n pollTimer = undefined;\n (options.onUnauthorized ?? settings.onError)?.(error);\n };\n\n const backoff = () => {\n const delay = reconnectDelay;\n reconnectDelay = Math.min(reconnectDelay * 2, settings.reconnectMaxMs);\n return delay;\n };\n\n let connecting = false;\n const connect = async () => {\n if (connecting) return;\n connecting = true;\n const mine = epoch;\n try {\n await connectLoop(mine);\n } finally {\n if (mine === epoch) connecting = false;\n }\n };\n\n const connectLoop = async (mine: number) => {\n const active = () => running && mine === epoch;\n for (;;) {\n if (!active()) return;\n let endpoint: string | undefined;\n try {\n const negotiation = await negotiate();\n if (!active()) return;\n negotiatedPollMs = negotiation.pollIntervalMs;\n negotiated = negotiation.transport;\n guarded(() => options.onNegotiated?.(negotiation.transport));\n if (negotiation.transport === \"poll\") {\n transport = \"poll\";\n schedulePoll(0);\n return;\n }\n endpoint = negotiation.endpoint;\n } catch (error) {\n if (!active()) return;\n interrupted = true;\n if (error instanceof SyncUnauthorized) {\n transport = undefined;\n unauthorized(error);\n } else {\n settings.onError?.(error);\n if (transport !== \"poll\") {\n transport = \"poll\";\n schedulePoll(negotiatedPollMs);\n }\n }\n }\n if (endpoint !== undefined) {\n transport = \"sse\";\n schedulePoll(settings.safetyPollMs);\n try {\n await stream(endpoint);\n } catch (error) {\n if (!active()) return;\n if (error instanceof SyncUnauthorized) unauthorized(error);\n else settings.onError?.(error);\n }\n interrupted = true;\n }\n if (!active()) return;\n const delay = backoff();\n await new Promise<void>((resolve) => {\n const timer = schedule(() => {\n endReconnectWait = undefined;\n resolve();\n }, delay);\n endReconnectWait = () => {\n cancel(timer);\n endReconnectWait = undefined;\n resolve();\n };\n });\n }\n };\n\n const detachChannel = (channel: ClientChannel) => {\n if (channels.get(channel.event) !== channel) return;\n channels.delete(channel.event);\n if (live?.open.delete(channel)) guarded(() => channel.closed());\n };\n\n const start = () => {\n if (running) return;\n running = true;\n void connect();\n };\n\n const reconnectNow = () => {\n if (!running) return;\n if (endReconnectWait) {\n reconnectDelay = settings.reconnectBaseMs;\n endReconnectWait();\n } else if (connectTimer !== undefined) {\n cancel(connectTimer);\n connectTimer = undefined;\n reconnectDelay = settings.reconnectBaseMs;\n void connect();\n }\n if (transport === \"poll\" && pollTimer !== undefined) schedulePoll(0);\n };\n\n const stop = () => {\n running = false;\n epoch += 1;\n for (const request of inflight) request.abort();\n inflight.clear();\n controller?.abort();\n controller = undefined;\n connecting = false;\n endReconnectWait?.();\n if (pollTimer !== undefined) cancel(pollTimer);\n if (flushTimer !== undefined) cancel(flushTimer);\n if (connectTimer !== undefined) cancel(connectTimer);\n pollTimer = flushTimer = connectTimer = undefined;\n pending = [];\n };\n\n return {\n start,\n stop,\n addChannel(channel) {\n const replaced = channels.get(channel.event);\n if (replaced === channel) return () => detachChannel(channel);\n if (replaced) detachChannel(replaced);\n channels.set(channel.event, channel);\n if (live?.listed.has(channel.event)) {\n live.open.add(channel);\n guarded(() => channel.opened());\n }\n return () => detachChannel(channel);\n },\n reconnectNow,\n get cursor() {\n return held();\n },\n get transport() {\n return transport;\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sync-client.js","sourceRoot":"","sources":["../../src/client/sync-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientChannel } from \"../channel.js\";\nimport {\n compareCursors,\n cursorOf,\n decodeCursor,\n encodeCursor,\n INITIAL_CURSOR,\n LEGACY_KEEP_ALIVE_MS,\n type SyncBatch,\n type SyncCursor,\n type SyncEvent,\n type SyncNegotiation,\n streamLivenessMs,\n} from \"../protocol.js\";\nimport { readSse } from \"./sse.js\";\n\nexport type SyncRequest = (path: string, init?: RequestInit) => Promise<Response>;\n\nexport interface SyncClientOptions {\n readonly request: SyncRequest;\n readonly basePath?: string | undefined;\n readonly clientId?: string | undefined;\n readonly onEvents: (events: SyncEvent[]) => void;\n readonly onError?: ((error: unknown) => void) | undefined;\n readonly coalesceMs?: number | undefined;\n readonly safetyPollMs?: number | undefined;\n readonly hiddenPollMs?: number | undefined;\n readonly reconnectBaseMs?: number | undefined;\n readonly reconnectMaxMs?: number | undefined;\n readonly isHidden?: (() => boolean) | undefined;\n readonly setTimeout?: typeof globalThis.setTimeout | undefined;\n readonly clearTimeout?: typeof globalThis.clearTimeout | undefined;\n /** Picks each reconnect delay below its backoff ceiling. Defaults to `Math.random`. */\n readonly random?: (() => number) | undefined;\n readonly channels?: readonly ClientChannel[] | undefined;\n /**\n * Read each time the next poll is scheduled; the interval is the larger of this and the\n * negotiated one, so a caller can only slow polling down.\n */\n readonly pollEveryMs?: (() => number) | undefined;\n /**\n * Where to read from while the client has no cursor of its own: the encoded cursor of the\n * oldest data the page holds, so the first stream does not replay what that data already\n * shows. Defaults to the start of the retained log.\n */\n readonly startAt?: (() => string) | undefined;\n /**\n * The server could not replay every event since the client's cursor: the client fell\n * more than one batch behind, or behind what the server keeps. Sync continues from\n * `cursor`, where the client's streams stand now; reread whatever predates it. Without\n * it, the client is never reset and a poll pages through the backlog.\n */\n readonly onReset?: ((cursor: string) => void) | undefined;\n /** Bounds a negotiation or a poll, not the SSE stream. Defaults to 10 s. */\n readonly requestTimeoutMs?: number | undefined;\n /**\n * What the server negotiated, each time it does; `SyncClient.transport` reads `poll`\n * during the fallback poll of a failed negotiation, this never does.\n */\n readonly onNegotiated?: ((transport: \"sse\" | \"poll\") => void) | undefined;\n /**\n * The negotiated transport's connection came back after a failure: a stream reopened\n * after it dropped, or a poll succeeded after a failed one. A fallback poll on an SSE\n * deployment does not count.\n */\n readonly onResume?: (() => void) | undefined;\n /**\n * The server refused the credential (401 or 403). The client never degrades to polling\n * on it; it keeps retrying the negotiation with backoff, unless this calls `stop`.\n * Defaults to `onError`.\n */\n readonly onUnauthorized?: ((error: SyncUnauthorized) => void) | undefined;\n}\n\nexport class SyncUnauthorized extends Error {\n override readonly name = \"SyncUnauthorized\";\n constructor(readonly status: number) {\n super(`sync was refused with HTTP ${status}`);\n }\n}\n\nexport class SyncRequestTimeout extends Error {\n override readonly name = \"SyncRequestTimeout\";\n constructor(readonly ms: number) {\n super(`sync request timed out after ${ms} ms`);\n }\n}\n\nclass StaleRequest extends Error {\n override readonly name = \"StaleRequest\";\n}\n\nconst refused = (response: Response) =>\n response.status === 401 || response.status === 403;\n\nexport interface SyncClient {\n start(): void;\n stop(): void;\n /**\n * Registers a channel after `start`, for features loaded lazily. It opens at once when\n * the live stream's handshake lists it, else on the next stream. Returns the detach.\n */\n addChannel(channel: ClientChannel): () => void;\n /**\n * Skips the remaining reconnect backoff, for `online` and visibility events. While\n * polling it polls at once; with a stream open or a request in flight it does nothing.\n */\n reconnectNow(): void;\n readonly cursor: string;\n readonly transport: \"sse\" | \"poll\" | undefined;\n}\n\nconst DEFAULTS = {\n coalesceMs: 250,\n safetyPollMs: 60_000,\n hiddenPollMs: 10_000,\n reconnectBaseMs: 1000,\n reconnectMaxMs: 30_000,\n requestTimeoutMs: 10_000,\n};\n\ninterface Handshake {\n readonly capabilities: readonly string[];\n readonly keepAliveMs: number | undefined;\n}\n\nfunction parseHandshake(handshake: string): Handshake {\n try {\n const { capabilities, keepAliveMs } = JSON.parse(handshake) as {\n capabilities?: unknown;\n keepAliveMs?: unknown;\n };\n return {\n capabilities: Array.isArray(capabilities)\n ? capabilities.filter((c): c is string => typeof c === \"string\")\n : [],\n keepAliveMs:\n typeof keepAliveMs === \"number\" && keepAliveMs > 0 ? keepAliveMs : undefined,\n };\n } catch {\n return { capabilities: [], keepAliveMs: undefined };\n }\n}\n\nexport function createSyncClient(options: SyncClientOptions): SyncClient {\n const basePath = options.basePath ?? \"/_parlor\";\n const settings = {\n coalesceMs: options.coalesceMs ?? DEFAULTS.coalesceMs,\n safetyPollMs: options.safetyPollMs ?? DEFAULTS.safetyPollMs,\n hiddenPollMs: options.hiddenPollMs ?? DEFAULTS.hiddenPollMs,\n reconnectBaseMs: options.reconnectBaseMs ?? DEFAULTS.reconnectBaseMs,\n reconnectMaxMs: options.reconnectMaxMs ?? DEFAULTS.reconnectMaxMs,\n requestTimeoutMs: options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs,\n clientId: options.clientId,\n onError: options.onError,\n };\n const schedule: (fn: () => void, ms: number) => ReturnType<typeof setTimeout> =\n options.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));\n const cancel: (id: ReturnType<typeof setTimeout>) => void =\n options.clearTimeout ?? ((id) => globalThis.clearTimeout(id));\n const random = options.random ?? Math.random;\n const isHidden = options.isHidden ?? (() => false);\n const channels = new Map((options.channels ?? []).map((c) => [c.event, c]));\n const guarded = (run: () => void) => {\n try {\n run();\n } catch (error) {\n settings.onError?.(error);\n }\n };\n\n let cursor: SyncCursor = INITIAL_CURSOR;\n let transport: \"sse\" | \"poll\" | undefined;\n let negotiated: \"sse\" | \"poll\" | undefined;\n let interrupted = false;\n let running = false;\n let controller: AbortController | undefined;\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setTimeout> | undefined;\n let pending: SyncEvent[] = [];\n let reconnectDelay = settings.reconnectBaseMs;\n let endReconnectWait: (() => void) | undefined;\n let connectTimer: ReturnType<typeof setTimeout> | undefined;\n let epoch = 0;\n const inflight = new Set<AbortController>();\n\n const withTimeout = async <T>(run: (signal: AbortSignal) => Promise<T>): Promise<T> => {\n const mine = epoch;\n const own = new AbortController();\n inflight.add(own);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const expired = new Promise<never>((_, reject) => {\n timer = schedule(() => {\n own.abort();\n reject(new SyncRequestTimeout(settings.requestTimeoutMs));\n }, settings.requestTimeoutMs);\n });\n try {\n const result = await Promise.race([run(own.signal), expired]);\n if (mine !== epoch) throw new StaleRequest();\n return result;\n } catch (error) {\n if (mine !== epoch) throw new StaleRequest();\n throw error;\n } finally {\n inflight.delete(own);\n if (timer !== undefined) cancel(timer);\n }\n };\n\n const resumed = () => {\n if (!interrupted) return;\n interrupted = false;\n guarded(() => options.onResume?.());\n };\n\n const accept = (events: readonly SyncEvent[]) => {\n for (const event of events) {\n const at = cursorOf(event);\n if (compareCursors(at, cursor) <= 0) continue;\n cursor = at;\n if (event.source && event.source === settings.clientId) continue;\n pending.push(event);\n }\n if (pending.length > 0 && flushTimer === undefined) {\n flushTimer = schedule(() => {\n flushTimer = undefined;\n const batch = pending;\n pending = [];\n options.onEvents(batch);\n }, settings.coalesceMs);\n }\n };\n\n const held = () => (cursor === INITIAL_CURSOR ? \"\" : encodeCursor(cursor));\n const since = () =>\n cursor === INITIAL_CURSOR ? (options.startAt?.() ?? \"\") : encodeCursor(cursor);\n const missedSince = () =>\n `since=${encodeURIComponent(since())}${options.onReset ? \"&reset=1\" : \"\"}`;\n\n const take = (batch: SyncBatch) => {\n if (!batch.reset) {\n accept(batch.events);\n return;\n }\n cursor = decodeCursor(batch.cursor);\n guarded(() => options.onReset?.(batch.cursor));\n };\n\n const poll = async (mine: number) => {\n const batch = await withTimeout(async (signal) => {\n const response = await options.request(`${basePath}/sync?${missedSince()}`, {\n signal,\n });\n if (refused(response)) throw new SyncUnauthorized(response.status);\n if (!response.ok) throw new Error(`sync poll failed with HTTP ${response.status}`);\n return (await response.json()) as SyncBatch;\n });\n if (mine !== epoch) throw new StaleRequest();\n take(batch);\n if (running && negotiated === \"poll\") resumed();\n };\n\n const schedulePoll = (ms: number) => {\n if (!running) return;\n if (pollTimer !== undefined) cancel(pollTimer);\n pollTimer = schedule(() => {\n pollTimer = undefined;\n let halted = false;\n const mine = epoch;\n void poll(mine)\n .catch((error: unknown) => {\n if (error instanceof StaleRequest || mine !== epoch) {\n halted = true;\n return;\n }\n if (negotiated === \"poll\") interrupted = true;\n if (error instanceof SyncUnauthorized) {\n halted = true;\n unauthorized(error);\n if (transport === \"poll\") {\n transport = undefined;\n connectTimer = schedule(() => {\n connectTimer = undefined;\n void connect();\n }, backoff());\n }\n return;\n }\n settings.onError?.(error);\n })\n .finally(() => {\n if (!halted) schedulePoll(nextPollDelay());\n });\n }, ms);\n };\n\n const nextPollDelay = () => {\n if (transport === \"sse\") return settings.safetyPollMs;\n const base = Math.max(negotiatedPollMs, options.pollEveryMs?.() ?? 0);\n return isHidden() ? Math.max(base, settings.hiddenPollMs) : base;\n };\n let negotiatedPollMs = 2000;\n\n const negotiate = async (): Promise<SyncNegotiation> => {\n return withTimeout(async (signal) => {\n const response = await options.request(`${basePath}/sync/token`, {\n method: \"POST\",\n signal,\n });\n if (refused(response)) throw new SyncUnauthorized(response.status);\n if (!response.ok)\n throw new Error(`sync negotiation failed with HTTP ${response.status}`);\n return (await response.json()) as SyncNegotiation;\n });\n };\n\n interface LiveStream {\n listed: ReadonlySet<string>;\n readonly open: Set<ClientChannel>;\n }\n let live: LiveStream | undefined;\n\n const stream = async (endpoint: string) => {\n const own = new AbortController();\n controller = own;\n let keepAliveMs = LEGACY_KEEP_ALIVE_MS;\n let silent = false;\n let watchdog: ReturnType<typeof setTimeout> | undefined;\n const rearm = () => {\n if (watchdog !== undefined) cancel(watchdog);\n watchdog = schedule(() => {\n silent = true;\n own.abort();\n }, streamLivenessMs(keepAliveMs));\n };\n const open = new Set<ClientChannel>();\n const current: LiveStream = { listed: new Set(), open };\n live = current;\n rearm();\n try {\n const response = await options.request(`${endpoint}?${missedSince()}`, {\n headers: { accept: \"text/event-stream\" },\n signal: own.signal,\n });\n if (refused(response)) throw new SyncUnauthorized(response.status);\n if (!response.ok || !response.body) {\n throw new Error(`sync stream failed with HTTP ${response.status}`);\n }\n reconnectDelay = settings.reconnectBaseMs;\n resumed();\n await readSse(\n response.body,\n (frame) => {\n if (frame.event === \"message\") {\n const batch = JSON.parse(frame.data) as SyncBatch;\n take(batch);\n const parsed = decodeCursor(batch.cursor);\n if (compareCursors(parsed, cursor) > 0) cursor = parsed;\n return;\n }\n if (frame.event === \"handshake\") {\n const handshake = parseHandshake(frame.data);\n keepAliveMs = handshake.keepAliveMs ?? LEGACY_KEEP_ALIVE_MS;\n rearm();\n current.listed = new Set(handshake.capabilities);\n for (const event of handshake.capabilities) {\n const channel = channels.get(event);\n if (!channel || open.has(channel)) continue;\n open.add(channel);\n guarded(() => channel.opened());\n }\n return;\n }\n const channel = channels.get(frame.event);\n if (channel && open.has(channel)) guarded(() => channel.receive(frame.data));\n },\n own.signal,\n rearm,\n );\n } catch (error) {\n if (!silent) throw error;\n } finally {\n if (watchdog !== undefined) cancel(watchdog);\n if (live === current) live = undefined;\n for (const channel of open) guarded(() => channel.closed());\n }\n };\n\n const unauthorized = (error: SyncUnauthorized) => {\n if (pollTimer !== undefined) cancel(pollTimer);\n pollTimer = undefined;\n (options.onUnauthorized ?? settings.onError)?.(error);\n };\n\n // Full jitter, so clients that lost one server together do not return in lockstep.\n const backoff = () => {\n const ceiling = reconnectDelay;\n reconnectDelay = Math.min(reconnectDelay * 2, settings.reconnectMaxMs);\n return Math.round(ceiling * random());\n };\n\n let connecting = false;\n const connect = async () => {\n if (connecting) return;\n connecting = true;\n const mine = epoch;\n try {\n await connectLoop(mine);\n } finally {\n if (mine === epoch) connecting = false;\n }\n };\n\n const connectLoop = async (mine: number) => {\n const active = () => running && mine === epoch;\n for (;;) {\n if (!active()) return;\n let endpoint: string | undefined;\n try {\n const negotiation = await negotiate();\n if (!active()) return;\n negotiatedPollMs = negotiation.pollIntervalMs;\n negotiated = negotiation.transport;\n guarded(() => options.onNegotiated?.(negotiation.transport));\n if (negotiation.transport === \"poll\") {\n transport = \"poll\";\n schedulePoll(0);\n return;\n }\n endpoint = negotiation.endpoint;\n } catch (error) {\n if (!active()) return;\n interrupted = true;\n if (error instanceof SyncUnauthorized) {\n transport = undefined;\n unauthorized(error);\n } else {\n settings.onError?.(error);\n if (transport !== \"poll\") {\n transport = \"poll\";\n schedulePoll(negotiatedPollMs);\n }\n }\n }\n if (endpoint !== undefined) {\n transport = \"sse\";\n schedulePoll(settings.safetyPollMs);\n try {\n await stream(endpoint);\n } catch (error) {\n if (!active()) return;\n if (error instanceof SyncUnauthorized) unauthorized(error);\n else settings.onError?.(error);\n }\n interrupted = true;\n }\n if (!active()) return;\n const delay = backoff();\n await new Promise<void>((resolve) => {\n const timer = schedule(() => {\n endReconnectWait = undefined;\n resolve();\n }, delay);\n endReconnectWait = () => {\n cancel(timer);\n endReconnectWait = undefined;\n resolve();\n };\n });\n }\n };\n\n const detachChannel = (channel: ClientChannel) => {\n if (channels.get(channel.event) !== channel) return;\n channels.delete(channel.event);\n if (live?.open.delete(channel)) guarded(() => channel.closed());\n };\n\n const start = () => {\n if (running) return;\n running = true;\n void connect();\n };\n\n const reconnectNow = () => {\n if (!running) return;\n if (endReconnectWait) {\n reconnectDelay = settings.reconnectBaseMs;\n endReconnectWait();\n } else if (connectTimer !== undefined) {\n cancel(connectTimer);\n connectTimer = undefined;\n reconnectDelay = settings.reconnectBaseMs;\n void connect();\n }\n if (transport === \"poll\" && pollTimer !== undefined) schedulePoll(0);\n };\n\n const stop = () => {\n running = false;\n epoch += 1;\n for (const request of inflight) request.abort();\n inflight.clear();\n controller?.abort();\n controller = undefined;\n connecting = false;\n endReconnectWait?.();\n if (pollTimer !== undefined) cancel(pollTimer);\n if (flushTimer !== undefined) cancel(flushTimer);\n if (connectTimer !== undefined) cancel(connectTimer);\n pollTimer = flushTimer = connectTimer = undefined;\n pending = [];\n };\n\n return {\n start,\n stop,\n addChannel(channel) {\n const replaced = channels.get(channel.event);\n if (replaced === channel) return () => detachChannel(channel);\n if (replaced) detachChannel(replaced);\n channels.set(channel.event, channel);\n if (live?.listed.has(channel.event)) {\n live.open.add(channel);\n guarded(() => channel.opened());\n }\n return () => detachChannel(channel);\n },\n reconnectNow,\n get cursor() {\n return held();\n },\n get transport() {\n return transport;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const DEFAULT_MAX_QUEUED_BYTES: number;
|
|
2
|
+
export declare class SlowConsumer extends Error {
|
|
3
|
+
readonly maxQueuedBytes: number;
|
|
4
|
+
readonly name = "SlowConsumer";
|
|
5
|
+
constructor(maxQueuedBytes: number);
|
|
6
|
+
}
|
|
7
|
+
export interface EventStream {
|
|
8
|
+
readonly body: ReadableStream<Uint8Array>;
|
|
9
|
+
push(frame: {
|
|
10
|
+
readonly event?: string | undefined;
|
|
11
|
+
readonly data: string;
|
|
12
|
+
}): void;
|
|
13
|
+
comment(text: string): void;
|
|
14
|
+
/** Resolves once the reader has taken everything queued, or the stream ended. */
|
|
15
|
+
drained(): Promise<void>;
|
|
16
|
+
/** Ends the stream after the reader takes what is queued. */
|
|
17
|
+
close(): void;
|
|
18
|
+
/** Runs once, when the stream closes, its reader cancels, or the reader falls too far behind. */
|
|
19
|
+
onClosed(listener: () => void): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A server-sent event stream that drops its connection, rather than buffering without
|
|
23
|
+
* bound, when the reader falls more than `maxQueuedBytes` behind. A frame that enters an
|
|
24
|
+
* empty queue is always taken, so one large frame cannot fail a reader that keeps up.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createEventStream(maxQueuedBytes?: number): EventStream;
|
|
27
|
+
//# sourceMappingURL=event-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-stream.d.ts","sourceRoot":"","sources":["../../src/server/event-stream.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,QAAc,CAAC;AAEpD,qBAAa,YAAa,SAAQ,KAAK;IAEzB,QAAQ,CAAC,cAAc,EAAE,MAAM;IAD3C,SAAkB,IAAI,kBAAkB;IACxC,YAAqB,cAAc,EAAE,MAAM,EAE1C;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iFAAiF;IACjF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,6DAA6D;IAC7D,KAAK,IAAI,IAAI,CAAC;IACd,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,cAAc,GAAE,MAAiC,GAChD,WAAW,CA4Db"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const DEFAULT_MAX_QUEUED_BYTES = 1024 * 1024;
|
|
2
|
+
export class SlowConsumer extends Error {
|
|
3
|
+
maxQueuedBytes;
|
|
4
|
+
name = "SlowConsumer";
|
|
5
|
+
constructor(maxQueuedBytes) {
|
|
6
|
+
super(`the stream's reader fell more than ${maxQueuedBytes} bytes behind`);
|
|
7
|
+
this.maxQueuedBytes = maxQueuedBytes;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A server-sent event stream that drops its connection, rather than buffering without
|
|
12
|
+
* bound, when the reader falls more than `maxQueuedBytes` behind. A frame that enters an
|
|
13
|
+
* empty queue is always taken, so one large frame cannot fail a reader that keeps up.
|
|
14
|
+
*/
|
|
15
|
+
export function createEventStream(maxQueuedBytes = DEFAULT_MAX_QUEUED_BYTES) {
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
let controller;
|
|
18
|
+
let ended = false;
|
|
19
|
+
let listeners = [];
|
|
20
|
+
let waiters = [];
|
|
21
|
+
const queued = () => maxQueuedBytes - (controller.desiredSize ?? 0);
|
|
22
|
+
const wake = () => {
|
|
23
|
+
for (const resolve of waiters.splice(0))
|
|
24
|
+
resolve();
|
|
25
|
+
};
|
|
26
|
+
const end = () => {
|
|
27
|
+
if (ended)
|
|
28
|
+
return;
|
|
29
|
+
ended = true;
|
|
30
|
+
wake();
|
|
31
|
+
for (const listener of listeners.splice(0))
|
|
32
|
+
listener();
|
|
33
|
+
};
|
|
34
|
+
const body = new ReadableStream({
|
|
35
|
+
start(own) {
|
|
36
|
+
controller = own;
|
|
37
|
+
},
|
|
38
|
+
pull() {
|
|
39
|
+
if (queued() === 0)
|
|
40
|
+
wake();
|
|
41
|
+
},
|
|
42
|
+
cancel: end,
|
|
43
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: maxQueuedBytes }));
|
|
44
|
+
const write = (text) => {
|
|
45
|
+
if (ended)
|
|
46
|
+
return;
|
|
47
|
+
const wasEmpty = queued() === 0;
|
|
48
|
+
controller.enqueue(encoder.encode(text));
|
|
49
|
+
if (!wasEmpty && queued() > maxQueuedBytes) {
|
|
50
|
+
controller.error(new SlowConsumer(maxQueuedBytes));
|
|
51
|
+
end();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
body,
|
|
56
|
+
push({ event, data }) {
|
|
57
|
+
const lines = data.split(/\r\n|\r|\n/).map((line) => `data: ${line}\n`);
|
|
58
|
+
write(`${event ? `event: ${event}\n` : ""}${lines.join("")}\n`);
|
|
59
|
+
},
|
|
60
|
+
comment(text) {
|
|
61
|
+
write(`: ${text}\n\n`);
|
|
62
|
+
},
|
|
63
|
+
drained() {
|
|
64
|
+
if (ended || queued() === 0)
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
67
|
+
},
|
|
68
|
+
close() {
|
|
69
|
+
if (ended)
|
|
70
|
+
return;
|
|
71
|
+
controller.close();
|
|
72
|
+
end();
|
|
73
|
+
},
|
|
74
|
+
onClosed(listener) {
|
|
75
|
+
if (ended)
|
|
76
|
+
queueMicrotask(listener);
|
|
77
|
+
else
|
|
78
|
+
listeners.push(listener);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=event-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-stream.js","sourceRoot":"","sources":["../../src/server/event-stream.ts"],"names":[],"mappings":"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","sourcesContent":["export const DEFAULT_MAX_QUEUED_BYTES = 1024 * 1024;\n\nexport class SlowConsumer extends Error {\n override readonly name = \"SlowConsumer\";\n constructor(readonly maxQueuedBytes: number) {\n super(`the stream's reader fell more than ${maxQueuedBytes} bytes behind`);\n }\n}\n\nexport interface EventStream {\n readonly body: ReadableStream<Uint8Array>;\n push(frame: { readonly event?: string | undefined; readonly data: string }): void;\n comment(text: string): void;\n /** Resolves once the reader has taken everything queued, or the stream ended. */\n drained(): Promise<void>;\n /** Ends the stream after the reader takes what is queued. */\n close(): void;\n /** Runs once, when the stream closes, its reader cancels, or the reader falls too far behind. */\n onClosed(listener: () => void): void;\n}\n\n/**\n * A server-sent event stream that drops its connection, rather than buffering without\n * bound, when the reader falls more than `maxQueuedBytes` behind. A frame that enters an\n * empty queue is always taken, so one large frame cannot fail a reader that keeps up.\n */\nexport function createEventStream(\n maxQueuedBytes: number = DEFAULT_MAX_QUEUED_BYTES,\n): EventStream {\n const encoder = new TextEncoder();\n let controller!: ReadableStreamDefaultController<Uint8Array>;\n let ended = false;\n let listeners: (() => void)[] = [];\n let waiters: (() => void)[] = [];\n const queued = () => maxQueuedBytes - (controller.desiredSize ?? 0);\n const wake = () => {\n for (const resolve of waiters.splice(0)) resolve();\n };\n const end = () => {\n if (ended) return;\n ended = true;\n wake();\n for (const listener of listeners.splice(0)) listener();\n };\n const body = new ReadableStream<Uint8Array>(\n {\n start(own) {\n controller = own;\n },\n pull() {\n if (queued() === 0) wake();\n },\n cancel: end,\n },\n new ByteLengthQueuingStrategy({ highWaterMark: maxQueuedBytes }),\n );\n const write = (text: string) => {\n if (ended) return;\n const wasEmpty = queued() === 0;\n controller.enqueue(encoder.encode(text));\n if (!wasEmpty && queued() > maxQueuedBytes) {\n controller.error(new SlowConsumer(maxQueuedBytes));\n end();\n }\n };\n return {\n body,\n push({ event, data }) {\n const lines = data.split(/\\r\\n|\\r|\\n/).map((line) => `data: ${line}\\n`);\n write(`${event ? `event: ${event}\\n` : \"\"}${lines.join(\"\")}\\n`);\n },\n comment(text) {\n write(`: ${text}\\n\\n`);\n },\n drained() {\n if (ended || queued() === 0) return Promise.resolve();\n return new Promise((resolve) => waiters.push(resolve));\n },\n close() {\n if (ended) return;\n controller.close();\n end();\n },\n onClosed(listener) {\n if (ended) queueMicrotask(listener);\n else listeners.push(listener);\n },\n };\n}\n"]}
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface SyncRouteOptions {
|
|
|
16
16
|
readonly transport?: SyncTransport | undefined;
|
|
17
17
|
readonly pollIntervalMs?: number | undefined;
|
|
18
18
|
readonly keepAliveMs?: number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* How far a stream's reader may fall behind before the server drops its connection; the
|
|
21
|
+
* client then reconnects and catches up from its cursor. Defaults to 1 MiB.
|
|
22
|
+
*/
|
|
23
|
+
readonly maxQueuedBytes?: number | undefined;
|
|
19
24
|
readonly channels?: readonly ServerChannel[] | undefined;
|
|
20
25
|
readonly clientId?: ((request: Request) => string | undefined) | undefined;
|
|
21
26
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/server/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/server/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAY,KAAK,EAAE,EAAgB,MAAM,IAAI,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAanD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAQxC,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC;IACzD,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IAC3E;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG,SAAS,CAAC;CACnF;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC;CACpE;AAED,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C,wBAAgB,gBAAgB,CAC9B,KAAK,GAAE,MAAM,GAAG,SAA6C,GAC5D,aAAa,GAAG,SAAS,CAE3B;AAqDD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA2I3E"}
|
package/dist/server/routes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getQuery } from "h3";
|
|
2
2
|
import { compareCursors, cursorOf, decodeCursor, DEFAULT_KEEP_ALIVE_MS, encodeCursor, SYNC_PROTOCOL_VERSION, } from "../protocol.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createEventStream, DEFAULT_MAX_QUEUED_BYTES } from "./event-stream.js";
|
|
4
|
+
import { MAX_BATCH, readSyncBacklog, readSyncEvents, } from "./store.js";
|
|
4
5
|
export const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
5
6
|
export function transportFromEnv(value = process.env.PARLOR_SYNC_TRANSPORT) {
|
|
6
7
|
return value === "sse" || value === "poll" ? value : undefined;
|
|
@@ -9,10 +10,33 @@ function batch(events, since) {
|
|
|
9
10
|
const last = events.at(-1);
|
|
10
11
|
return { events, cursor: last ? encodeCursor(cursorOf(last)) : since };
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
const CATCH_UP_FRAME_CHARS = 64 * 1024;
|
|
14
|
+
function catchUpFrames(missed) {
|
|
15
|
+
if (missed.reset)
|
|
16
|
+
return [missed];
|
|
17
|
+
const frames = [];
|
|
18
|
+
let events = [];
|
|
19
|
+
let size = 0;
|
|
20
|
+
for (const event of missed.events) {
|
|
21
|
+
const eventSize = JSON.stringify(event).length;
|
|
22
|
+
if (events.length > 0 && size + eventSize > CATCH_UP_FRAME_CHARS) {
|
|
23
|
+
frames.push(batch(events, missed.cursor));
|
|
24
|
+
events = [];
|
|
25
|
+
size = 0;
|
|
26
|
+
}
|
|
27
|
+
events.push(event);
|
|
28
|
+
size += eventSize;
|
|
29
|
+
}
|
|
30
|
+
if (events.length > 0)
|
|
31
|
+
frames.push(batch(events, missed.cursor));
|
|
32
|
+
return frames;
|
|
33
|
+
}
|
|
34
|
+
function missedQuery(event) {
|
|
13
35
|
const query = getQuery(event);
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
return { since: String(query.since ?? ""), reset: query.reset === "1" };
|
|
37
|
+
}
|
|
38
|
+
async function missedSince(db, streams, { since, reset }) {
|
|
39
|
+
if (!reset) {
|
|
16
40
|
return batch(await readSyncEvents(db, streams, decodeCursor(since)), since);
|
|
17
41
|
}
|
|
18
42
|
const backlog = await readSyncBacklog(db, streams, decodeCursor(since));
|
|
@@ -31,6 +55,7 @@ export function registerSyncRoutes(app, options) {
|
|
|
31
55
|
const transport = options.transport ?? transportFromEnv() ?? (hub.canPush ? "sse" : "poll");
|
|
32
56
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
33
57
|
const keepAliveMs = options.keepAliveMs ?? DEFAULT_KEEP_ALIVE_MS;
|
|
58
|
+
const maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;
|
|
34
59
|
const db = () => data.read();
|
|
35
60
|
const channels = options.channels ?? [];
|
|
36
61
|
app.post(`${basePath}/sync/token`, () => ({
|
|
@@ -40,7 +65,7 @@ export function registerSyncRoutes(app, options) {
|
|
|
40
65
|
pollIntervalMs,
|
|
41
66
|
}));
|
|
42
67
|
app.get(`${basePath}/sync`, async (event) => {
|
|
43
|
-
const missed = await missedSince(db(), await streamsOf(event.req), event);
|
|
68
|
+
const missed = await missedSince(db(), await streamsOf(event.req), missedQuery(event));
|
|
44
69
|
event.res.headers.set("cache-control", "no-store");
|
|
45
70
|
return missed;
|
|
46
71
|
});
|
|
@@ -51,26 +76,29 @@ export function registerSyncRoutes(app, options) {
|
|
|
51
76
|
const admitted = authority?.channels ?? true;
|
|
52
77
|
const carried = channels.filter((channel) => typeof admitted === "function" ? admitted(channel) : admitted);
|
|
53
78
|
await hub.start();
|
|
54
|
-
const stream = createEventStream(
|
|
55
|
-
const send = (payload) =>
|
|
79
|
+
const stream = createEventStream(maxQueuedBytes);
|
|
80
|
+
const send = (payload) => stream.push({ data: JSON.stringify(payload) });
|
|
56
81
|
const sendLive = (syncEvent) => send({ events: [syncEvent], cursor: encodeCursor(cursorOf(syncEvent)) });
|
|
57
82
|
let arrivedDuringCatchUp = [];
|
|
83
|
+
let droppedDuringCatchUp = false;
|
|
58
84
|
const unsubscribes = streams.map((name) => hub.subscribe(name, (syncEvent) => {
|
|
59
|
-
if (arrivedDuringCatchUp)
|
|
60
|
-
arrivedDuringCatchUp.push(syncEvent);
|
|
61
|
-
else
|
|
85
|
+
if (!arrivedDuringCatchUp)
|
|
62
86
|
sendLive(syncEvent);
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
else if (arrivedDuringCatchUp.length < MAX_BATCH)
|
|
88
|
+
arrivedDuringCatchUp.push(syncEvent);
|
|
89
|
+
else {
|
|
90
|
+
arrivedDuringCatchUp = [];
|
|
91
|
+
droppedDuringCatchUp = true;
|
|
92
|
+
}
|
|
93
|
+
}, () => stream.close()));
|
|
94
|
+
const keepAlive = setInterval(() => stream.comment("keep-alive"), keepAliveMs);
|
|
65
95
|
const disconnects = [];
|
|
66
96
|
const ended = new AbortController();
|
|
67
|
-
let detached = false;
|
|
68
97
|
const revoked = authority?.revoked;
|
|
69
|
-
const closeRevoked = () =>
|
|
98
|
+
const closeRevoked = () => stream.close();
|
|
70
99
|
const detach = () => {
|
|
71
|
-
if (
|
|
100
|
+
if (ended.signal.aborted)
|
|
72
101
|
return;
|
|
73
|
-
detached = true;
|
|
74
102
|
ended.abort();
|
|
75
103
|
clearInterval(keepAlive);
|
|
76
104
|
revoked?.removeEventListener("abort", closeRevoked);
|
|
@@ -84,7 +112,7 @@ export function registerSyncRoutes(app, options) {
|
|
|
84
112
|
closeRevoked();
|
|
85
113
|
else
|
|
86
114
|
revoked?.addEventListener("abort", closeRevoked, { once: true });
|
|
87
|
-
|
|
115
|
+
stream.push({
|
|
88
116
|
event: "handshake",
|
|
89
117
|
data: JSON.stringify({
|
|
90
118
|
protocol: SYNC_PROTOCOL_VERSION,
|
|
@@ -99,30 +127,48 @@ export function registerSyncRoutes(app, options) {
|
|
|
99
127
|
scope: currentScope,
|
|
100
128
|
...(authority ? { principal: authority.principal } : {}),
|
|
101
129
|
closed: ended.signal,
|
|
102
|
-
send: (frame) => {
|
|
103
|
-
if (!detached)
|
|
104
|
-
void stream.push({ event: channel.event, data: frame });
|
|
105
|
-
},
|
|
130
|
+
send: (frame) => stream.push({ event: channel.event, data: frame }),
|
|
106
131
|
}));
|
|
107
132
|
}
|
|
108
|
-
const
|
|
133
|
+
const query = missedQuery(event);
|
|
134
|
+
const catchUp = async (first) => {
|
|
135
|
+
let missed = first;
|
|
136
|
+
for (;;) {
|
|
137
|
+
for (const frame of catchUpFrames(missed)) {
|
|
138
|
+
await stream.drained();
|
|
139
|
+
if (ended.signal.aborted)
|
|
140
|
+
return;
|
|
141
|
+
send(frame);
|
|
142
|
+
}
|
|
143
|
+
const truncated = !missed.reset && missed.events.length >= MAX_BATCH;
|
|
144
|
+
if (!truncated && !droppedDuringCatchUp)
|
|
145
|
+
break;
|
|
146
|
+
droppedDuringCatchUp = false;
|
|
147
|
+
missed = await missedSince(db(), streams, { ...query, since: missed.cursor });
|
|
148
|
+
}
|
|
149
|
+
const caughtUpTo = decodeCursor(missed.cursor);
|
|
150
|
+
const pending = arrivedDuringCatchUp
|
|
151
|
+
.filter((syncEvent) => compareCursors(cursorOf(syncEvent), caughtUpTo) > 0)
|
|
152
|
+
.sort((a, b) => compareCursors(cursorOf(a), cursorOf(b)));
|
|
153
|
+
arrivedDuringCatchUp = undefined;
|
|
154
|
+
for (const syncEvent of pending)
|
|
155
|
+
sendLive(syncEvent);
|
|
156
|
+
};
|
|
157
|
+
const first = await missedSince(db(), streams, query).catch((error) => {
|
|
109
158
|
detach();
|
|
110
159
|
throw error;
|
|
111
160
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const syncEvent of pending)
|
|
120
|
-
sendLive(syncEvent);
|
|
121
|
-
const body = await stream.send();
|
|
161
|
+
void catchUp(first).catch((error) => {
|
|
162
|
+
console.error("[parlor] sync catch-up failed:", error);
|
|
163
|
+
stream.close();
|
|
164
|
+
});
|
|
165
|
+
event.res.headers.set("content-type", "text/event-stream");
|
|
166
|
+
event.res.headers.set("cache-control", "no-cache, no-store, no-transform");
|
|
167
|
+
event.res.headers.set("x-accel-buffering", "no");
|
|
122
168
|
if (event.req.headers.get("connection") === "keep-alive") {
|
|
123
169
|
event.res.headers.set("connection", "close");
|
|
124
170
|
}
|
|
125
|
-
return body;
|
|
171
|
+
return stream.body;
|
|
126
172
|
});
|
|
127
173
|
}
|
|
128
174
|
//# sourceMappingURL=routes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/server/routes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DataProvider, Principal } from \"@parlor/core\";\nimport { createEventStream, getQuery, type H3, type H3Event } from \"h3\";\n\nimport type { ServerChannel } from \"../channel.js\";\nimport {\n compareCursors,\n cursorOf,\n decodeCursor,\n DEFAULT_KEEP_ALIVE_MS,\n encodeCursor,\n type SyncBatch,\n type SyncEvent,\n type SyncNegotiation,\n SYNC_PROTOCOL_VERSION,\n} from \"../protocol.js\";\nimport type { SyncHub } from \"./hub.js\";\nimport { readSyncBacklog, readSyncEvents, type SyncDatabase } from \"./store.js\";\n\nexport type SyncTransport = \"sse\" | \"poll\";\n\nexport interface SyncRouteOptions {\n readonly basePath: string;\n readonly data: DataProvider;\n readonly hub: SyncHub;\n readonly scope: (request: Request) => string | Promise<string>;\n /**\n * The caller's own stream inside its scope, for events only its user should hear (an\n * action with `notifies: \"user\"`); read and pushed alongside the scope's.\n */\n readonly audience?: ((request: Request) => string | Promise<string>) | undefined;\n readonly transport?: SyncTransport | undefined;\n readonly pollIntervalMs?: number | undefined;\n readonly keepAliveMs?: number | undefined;\n readonly channels?: readonly ServerChannel[] | undefined;\n readonly clientId?: ((request: Request) => string | undefined) | undefined;\n /**\n * Who holds a stream and until when. A stream closes when `revoked` aborts, and a\n * stream without `channels` carries sync events only. Refusing a caller happens before\n * these routes run.\n */\n readonly authority?: ((request: Request) => Promise<StreamAuthority>) | undefined;\n}\n\nexport interface StreamAuthority {\n readonly principal: Principal;\n readonly revoked?: AbortSignal | undefined;\n /** Which channels the stream carries: all, none, or those the predicate admits. */\n readonly channels: boolean | ((channel: ServerChannel) => boolean);\n}\n\nexport const DEFAULT_POLL_INTERVAL_MS = 2000;\n\nexport function transportFromEnv(\n value: string | undefined = process.env.PARLOR_SYNC_TRANSPORT,\n): SyncTransport | undefined {\n return value === \"sse\" || value === \"poll\" ? value : undefined;\n}\n\nfunction batch(events: SyncEvent[], since: string): SyncBatch {\n const last = events.at(-1);\n return { events, cursor: last ? encodeCursor(cursorOf(last)) : since };\n}\n\nasync function missedSince(\n db: SyncDatabase,\n streams: readonly string[],\n event: H3Event,\n): Promise<SyncBatch> {\n const query = getQuery(event);\n const since = String(query.since ?? \"\");\n if (query.reset !== \"1\") {\n return batch(await readSyncEvents(db, streams, decodeCursor(since)), since);\n }\n const backlog = await readSyncBacklog(db, streams, decodeCursor(since));\n if (backlog.resetTo) {\n return { events: [], cursor: encodeCursor(backlog.resetTo), reset: true };\n }\n return batch(backlog.events, since);\n}\n\nexport function registerSyncRoutes(app: H3, options: SyncRouteOptions): void {\n const { basePath, data, hub, scope } = options;\n const streamsOf = async (request: Request): Promise<readonly string[]> => {\n const own = await scope(request);\n const audience = await options.audience?.(request);\n return audience === undefined || audience === own ? [own] : [own, audience];\n };\n const transport: SyncTransport =\n options.transport ?? transportFromEnv() ?? (hub.canPush ? \"sse\" : \"poll\");\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const keepAliveMs = options.keepAliveMs ?? DEFAULT_KEEP_ALIVE_MS;\n const db = () => data.read() as SyncDatabase;\n const channels = options.channels ?? [];\n\n app.post(`${basePath}/sync/token`, (): SyncNegotiation => ({\n protocol: SYNC_PROTOCOL_VERSION,\n transport,\n endpoint: `${basePath}/sync/stream`,\n pollIntervalMs,\n }));\n\n app.get(`${basePath}/sync`, async (event: H3Event) => {\n const missed = await missedSince(db(), await streamsOf(event.req), event);\n event.res.headers.set(\"cache-control\", \"no-store\");\n return missed;\n });\n\n app.get(`${basePath}/sync/stream`, async (event: H3Event) => {\n const streams = await streamsOf(event.req);\n const currentScope = streams[0]!;\n const authority = await options.authority?.(event.req);\n const admitted = authority?.channels ?? true;\n const carried = channels.filter((channel) =>\n typeof admitted === \"function\" ? admitted(channel) : admitted,\n );\n await hub.start();\n const stream = createEventStream(event);\n const send = (payload: SyncBatch) => void stream.push(JSON.stringify(payload));\n const sendLive = (syncEvent: SyncEvent) =>\n send({ events: [syncEvent], cursor: encodeCursor(cursorOf(syncEvent)) });\n\n let arrivedDuringCatchUp: SyncEvent[] | undefined = [];\n const unsubscribes = streams.map((name) =>\n hub.subscribe(\n name,\n (syncEvent) => {\n if (arrivedDuringCatchUp) arrivedDuringCatchUp.push(syncEvent);\n else sendLive(syncEvent);\n },\n () => void stream.close(),\n ),\n );\n const keepAlive = setInterval(\n () => void stream.pushComment(\"keep-alive\"),\n keepAliveMs,\n );\n const disconnects: (() => void)[] = [];\n const ended = new AbortController();\n let detached = false;\n const revoked = authority?.revoked;\n const closeRevoked = () => void stream.close();\n const detach = () => {\n if (detached) return;\n detached = true;\n ended.abort();\n clearInterval(keepAlive);\n revoked?.removeEventListener(\"abort\", closeRevoked);\n for (const unsubscribe of unsubscribes) unsubscribe();\n for (const disconnect of disconnects) disconnect();\n };\n stream.onClosed(detach);\n if (revoked?.aborted) closeRevoked();\n else revoked?.addEventListener(\"abort\", closeRevoked, { once: true });\n\n void stream.push({\n event: \"handshake\",\n data: JSON.stringify({\n protocol: SYNC_PROTOCOL_VERSION,\n capabilities: carried.map((channel) => channel.event),\n keepAliveMs,\n }),\n });\n const clientId = options.clientId?.(event.req);\n for (const channel of carried) {\n disconnects.push(\n channel.connect({\n clientId,\n scope: currentScope,\n ...(authority ? { principal: authority.principal } : {}),\n closed: ended.signal,\n send: (frame) => {\n if (!detached) void stream.push({ event: channel.event, data: frame });\n },\n }),\n );\n }\n const missed = await missedSince(db(), streams, event).catch((error: unknown) => {\n detach();\n throw error;\n });\n if (missed.events.length > 0 || missed.reset) send(missed);\n const caughtUpTo = decodeCursor(missed.cursor);\n const pending = arrivedDuringCatchUp\n .filter((syncEvent) => compareCursors(cursorOf(syncEvent), caughtUpTo) > 0)\n .sort((a, b) => compareCursors(cursorOf(a), cursorOf(b)));\n arrivedDuringCatchUp = undefined;\n for (const syncEvent of pending) sendLive(syncEvent);\n const body = await stream.send();\n if (event.req.headers.get(\"connection\") === \"keep-alive\") {\n event.res.headers.set(\"connection\", \"close\");\n }\n return body;\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/server/routes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DataProvider, Principal } from \"@parlor/core\";\nimport { getQuery, type H3, type H3Event } from \"h3\";\n\nimport type { ServerChannel } from \"../channel.js\";\nimport {\n compareCursors,\n cursorOf,\n decodeCursor,\n DEFAULT_KEEP_ALIVE_MS,\n encodeCursor,\n type SyncBatch,\n type SyncEvent,\n type SyncNegotiation,\n SYNC_PROTOCOL_VERSION,\n} from \"../protocol.js\";\nimport { createEventStream, DEFAULT_MAX_QUEUED_BYTES } from \"./event-stream.js\";\nimport type { SyncHub } from \"./hub.js\";\nimport {\n MAX_BATCH,\n readSyncBacklog,\n readSyncEvents,\n type SyncDatabase,\n} from \"./store.js\";\n\nexport type SyncTransport = \"sse\" | \"poll\";\n\nexport interface SyncRouteOptions {\n readonly basePath: string;\n readonly data: DataProvider;\n readonly hub: SyncHub;\n readonly scope: (request: Request) => string | Promise<string>;\n /**\n * The caller's own stream inside its scope, for events only its user should hear (an\n * action with `notifies: \"user\"`); read and pushed alongside the scope's.\n */\n readonly audience?: ((request: Request) => string | Promise<string>) | undefined;\n readonly transport?: SyncTransport | undefined;\n readonly pollIntervalMs?: number | undefined;\n readonly keepAliveMs?: number | undefined;\n /**\n * How far a stream's reader may fall behind before the server drops its connection; the\n * client then reconnects and catches up from its cursor. Defaults to 1 MiB.\n */\n readonly maxQueuedBytes?: number | undefined;\n readonly channels?: readonly ServerChannel[] | undefined;\n readonly clientId?: ((request: Request) => string | undefined) | undefined;\n /**\n * Who holds a stream and until when. A stream closes when `revoked` aborts, and a\n * stream without `channels` carries sync events only. Refusing a caller happens before\n * these routes run.\n */\n readonly authority?: ((request: Request) => Promise<StreamAuthority>) | undefined;\n}\n\nexport interface StreamAuthority {\n readonly principal: Principal;\n readonly revoked?: AbortSignal | undefined;\n /** Which channels the stream carries: all, none, or those the predicate admits. */\n readonly channels: boolean | ((channel: ServerChannel) => boolean);\n}\n\nexport const DEFAULT_POLL_INTERVAL_MS = 2000;\n\nexport function transportFromEnv(\n value: string | undefined = process.env.PARLOR_SYNC_TRANSPORT,\n): SyncTransport | undefined {\n return value === \"sse\" || value === \"poll\" ? value : undefined;\n}\n\nfunction batch(events: SyncEvent[], since: string): SyncBatch {\n const last = events.at(-1);\n return { events, cursor: last ? encodeCursor(cursorOf(last)) : since };\n}\n\nconst CATCH_UP_FRAME_CHARS = 64 * 1024;\n\nfunction catchUpFrames(missed: SyncBatch): SyncBatch[] {\n if (missed.reset) return [missed];\n const frames: SyncBatch[] = [];\n let events: SyncEvent[] = [];\n let size = 0;\n for (const event of missed.events) {\n const eventSize = JSON.stringify(event).length;\n if (events.length > 0 && size + eventSize > CATCH_UP_FRAME_CHARS) {\n frames.push(batch(events, missed.cursor));\n events = [];\n size = 0;\n }\n events.push(event);\n size += eventSize;\n }\n if (events.length > 0) frames.push(batch(events, missed.cursor));\n return frames;\n}\n\ninterface MissedQuery {\n readonly since: string;\n readonly reset: boolean;\n}\n\nfunction missedQuery(event: H3Event): MissedQuery {\n const query = getQuery(event);\n return { since: String(query.since ?? \"\"), reset: query.reset === \"1\" };\n}\n\nasync function missedSince(\n db: SyncDatabase,\n streams: readonly string[],\n { since, reset }: MissedQuery,\n): Promise<SyncBatch> {\n if (!reset) {\n return batch(await readSyncEvents(db, streams, decodeCursor(since)), since);\n }\n const backlog = await readSyncBacklog(db, streams, decodeCursor(since));\n if (backlog.resetTo) {\n return { events: [], cursor: encodeCursor(backlog.resetTo), reset: true };\n }\n return batch(backlog.events, since);\n}\n\nexport function registerSyncRoutes(app: H3, options: SyncRouteOptions): void {\n const { basePath, data, hub, scope } = options;\n const streamsOf = async (request: Request): Promise<readonly string[]> => {\n const own = await scope(request);\n const audience = await options.audience?.(request);\n return audience === undefined || audience === own ? [own] : [own, audience];\n };\n const transport: SyncTransport =\n options.transport ?? transportFromEnv() ?? (hub.canPush ? \"sse\" : \"poll\");\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const keepAliveMs = options.keepAliveMs ?? DEFAULT_KEEP_ALIVE_MS;\n const maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;\n const db = () => data.read() as SyncDatabase;\n const channels = options.channels ?? [];\n\n app.post(`${basePath}/sync/token`, (): SyncNegotiation => ({\n protocol: SYNC_PROTOCOL_VERSION,\n transport,\n endpoint: `${basePath}/sync/stream`,\n pollIntervalMs,\n }));\n\n app.get(`${basePath}/sync`, async (event: H3Event) => {\n const missed = await missedSince(\n db(),\n await streamsOf(event.req),\n missedQuery(event),\n );\n event.res.headers.set(\"cache-control\", \"no-store\");\n return missed;\n });\n\n app.get(`${basePath}/sync/stream`, async (event: H3Event) => {\n const streams = await streamsOf(event.req);\n const currentScope = streams[0]!;\n const authority = await options.authority?.(event.req);\n const admitted = authority?.channels ?? true;\n const carried = channels.filter((channel) =>\n typeof admitted === \"function\" ? admitted(channel) : admitted,\n );\n await hub.start();\n const stream = createEventStream(maxQueuedBytes);\n const send = (payload: SyncBatch) => stream.push({ data: JSON.stringify(payload) });\n const sendLive = (syncEvent: SyncEvent) =>\n send({ events: [syncEvent], cursor: encodeCursor(cursorOf(syncEvent)) });\n\n let arrivedDuringCatchUp: SyncEvent[] | undefined = [];\n let droppedDuringCatchUp = false;\n const unsubscribes = streams.map((name) =>\n hub.subscribe(\n name,\n (syncEvent) => {\n if (!arrivedDuringCatchUp) sendLive(syncEvent);\n else if (arrivedDuringCatchUp.length < MAX_BATCH)\n arrivedDuringCatchUp.push(syncEvent);\n else {\n arrivedDuringCatchUp = [];\n droppedDuringCatchUp = true;\n }\n },\n () => stream.close(),\n ),\n );\n const keepAlive = setInterval(() => stream.comment(\"keep-alive\"), keepAliveMs);\n const disconnects: (() => void)[] = [];\n const ended = new AbortController();\n const revoked = authority?.revoked;\n const closeRevoked = () => stream.close();\n const detach = () => {\n if (ended.signal.aborted) return;\n ended.abort();\n clearInterval(keepAlive);\n revoked?.removeEventListener(\"abort\", closeRevoked);\n for (const unsubscribe of unsubscribes) unsubscribe();\n for (const disconnect of disconnects) disconnect();\n };\n stream.onClosed(detach);\n if (revoked?.aborted) closeRevoked();\n else revoked?.addEventListener(\"abort\", closeRevoked, { once: true });\n\n stream.push({\n event: \"handshake\",\n data: JSON.stringify({\n protocol: SYNC_PROTOCOL_VERSION,\n capabilities: carried.map((channel) => channel.event),\n keepAliveMs,\n }),\n });\n const clientId = options.clientId?.(event.req);\n for (const channel of carried) {\n disconnects.push(\n channel.connect({\n clientId,\n scope: currentScope,\n ...(authority ? { principal: authority.principal } : {}),\n closed: ended.signal,\n send: (frame) => stream.push({ event: channel.event, data: frame }),\n }),\n );\n }\n\n const query = missedQuery(event);\n const catchUp = async (first: SyncBatch) => {\n let missed = first;\n for (;;) {\n for (const frame of catchUpFrames(missed)) {\n await stream.drained();\n if (ended.signal.aborted) return;\n send(frame);\n }\n const truncated = !missed.reset && missed.events.length >= MAX_BATCH;\n if (!truncated && !droppedDuringCatchUp) break;\n droppedDuringCatchUp = false;\n missed = await missedSince(db(), streams, { ...query, since: missed.cursor });\n }\n const caughtUpTo = decodeCursor(missed.cursor);\n const pending = arrivedDuringCatchUp!\n .filter((syncEvent) => compareCursors(cursorOf(syncEvent), caughtUpTo) > 0)\n .sort((a, b) => compareCursors(cursorOf(a), cursorOf(b)));\n arrivedDuringCatchUp = undefined;\n for (const syncEvent of pending) sendLive(syncEvent);\n };\n const first = await missedSince(db(), streams, query).catch((error: unknown) => {\n detach();\n throw error;\n });\n void catchUp(first).catch((error: unknown) => {\n console.error(\"[parlor] sync catch-up failed:\", error);\n stream.close();\n });\n\n event.res.headers.set(\"content-type\", \"text/event-stream\");\n event.res.headers.set(\"cache-control\", \"no-cache, no-store, no-transform\");\n event.res.headers.set(\"x-accel-buffering\", \"no\");\n if (event.req.headers.get(\"connection\") === \"keep-alive\") {\n event.res.headers.set(\"connection\", \"close\");\n }\n return stream.body;\n });\n}\n"]}
|
package/dist/server/store.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type SyncDatabase = PgDatabase<PgQueryResultHKT>;
|
|
|
5
5
|
export declare const RETENTION_MS: number;
|
|
6
6
|
export declare const PRUNE_LOCK_KEY = 7311207;
|
|
7
7
|
export declare const NOTIFY_PAYLOAD_LIMIT_BYTES = 8000;
|
|
8
|
+
export declare const MAX_BATCH = 500;
|
|
8
9
|
export declare function ensureSyncTables(db: SyncDatabase): Promise<void>;
|
|
9
10
|
export interface RecordSyncEventInput {
|
|
10
11
|
readonly scope: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/server/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAExE,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,SAAS,EAEf,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;AAExD,eAAO,MAAM,YAAY,QAAsB,CAAC;AAChD,eAAO,MAAM,cAAc,UAAY,CAAC;AACxC,eAAO,MAAM,0BAA0B,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/server/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAExE,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,SAAS,EAEf,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;AAExD,eAAO,MAAM,YAAY,QAAsB,CAAC;AAChD,eAAO,MAAM,cAAc,UAAY,CAAC;AACxC,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,eAAO,MAAM,SAAS,MAAM,CAAC;AAO7B,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtE;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,SAAS,CAAC,CA6BpB;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAIrE;AAeD,kFAAkF;AAClF,wBAAsB,cAAc,CAClC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACjC,KAAK,EAAE,UAAU,EACjB,KAAK,GAAE,MAAkB,GACxB,OAAO,CAAC,SAAS,EAAE,CAAC,CAsBtB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAC9D;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAErC;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACjC,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,WAAW,CAAC,CAUtB;AASD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAChC,OAAO,CAAC,UAAU,CAAC,CAerB;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,WAAW,GAAE,MAAqB,GACjC,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAQ7E"}
|
package/dist/server/store.js
CHANGED
|
@@ -3,7 +3,7 @@ import { compareCursors, INITIAL_CURSOR, SYNC_CHANNEL, } from "../protocol.js";
|
|
|
3
3
|
export const RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
4
4
|
export const PRUNE_LOCK_KEY = 7_311_207;
|
|
5
5
|
export const NOTIFY_PAYLOAD_LIMIT_BYTES = 8000;
|
|
6
|
-
const MAX_BATCH = 500;
|
|
6
|
+
export const MAX_BATCH = 500;
|
|
7
7
|
async function rows(query) {
|
|
8
8
|
const result = (await query);
|
|
9
9
|
return Array.isArray(result) ? result : (result.rows ?? []);
|
package/dist/server/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/server/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Tag } from \"@parlor/core\";\nimport { sql } from \"drizzle-orm\";\nimport type { PgDatabase, PgQueryResultHKT } from \"drizzle-orm/pg-core\";\n\nimport {\n compareCursors,\n INITIAL_CURSOR,\n type SyncCursor,\n type SyncEvent,\n SYNC_CHANNEL,\n} from \"../protocol.js\";\n\nexport type SyncDatabase = PgDatabase<PgQueryResultHKT>;\n\nexport const RETENTION_MS = 24 * 60 * 60 * 1000;\nexport const PRUNE_LOCK_KEY = 7_311_207;\nexport const NOTIFY_PAYLOAD_LIMIT_BYTES = 8000;\nconst MAX_BATCH = 500;\n\nasync function rows<T>(query: Promise<unknown>): Promise<T[]> {\n const result = (await query) as { rows?: T[] } | T[];\n return Array.isArray(result) ? result : (result.rows ?? []);\n}\n\nexport async function ensureSyncTables(db: SyncDatabase): Promise<void> {\n await db.execute(sql`\n create table if not exists sync_events (\n id text primary key,\n version bigint not null,\n scope text not null,\n tags jsonb not null,\n source text,\n created_at timestamptz not null default clock_timestamp()\n )`);\n await db.execute(\n sql`create index if not exists sync_events_scope_version on sync_events (scope, version, id)`,\n );\n await db.execute(\n sql`create table if not exists sync_version (id int primary key, v bigint not null)`,\n );\n await db.execute(sql`\n insert into sync_version (id, v)\n select 1, greatest(coalesce((select max(version) from sync_events), 0), 0)\n on conflict (id) do nothing`);\n await db.execute(\n sql`create table if not exists sync_pruned (id int primary key, version bigint not null)`,\n );\n await db.execute(sql`\n insert into sync_pruned (id, version)\n select 1, coalesce((select min(version) - 1 from sync_events), (select v from sync_version where id = 1))\n on conflict (id) do nothing`);\n}\n\nexport interface RecordSyncEventInput {\n readonly scope: string;\n readonly tags: readonly Tag[];\n readonly source?: string | undefined;\n}\n\nexport async function recordSyncEvent(\n tx: SyncDatabase,\n input: RecordSyncEventInput,\n): Promise<SyncEvent> {\n if (input.tags.length === 0) throw new Error(\"A sync event needs at least one tag\");\n const id = crypto.randomUUID();\n const tags = JSON.stringify(input.tags);\n const inserted = await rows<{ version: number | string }>(\n tx.execute(sql`\n with alloc as (\n update sync_version\n set v = greatest(v + 1, (extract(epoch from clock_timestamp()) * 1000)::bigint)\n where id = 1\n returning v\n )\n insert into sync_events (id, version, scope, tags, source)\n select ${id}, alloc.v, ${input.scope}, ${tags}::jsonb, ${input.source ?? null}\n from alloc\n returning version`),\n );\n const row = inserted[0];\n if (!row) throw new Error(\"sync_version row is missing; run ensureSyncTables first\");\n const event: SyncEvent = {\n version: Number(row.version),\n id,\n tags: input.tags,\n source: input.source,\n };\n await tx.execute(\n sql`select pg_notify(${SYNC_CHANNEL}, ${notifyPayload(input.scope, event)})`,\n );\n return event;\n}\n\n// pg_notify rejects payloads of 8000 bytes or more; the row keeps the full tags.\nexport function notifyPayload(scope: string, event: SyncEvent): string {\n const full = JSON.stringify({ scope, event });\n if (new TextEncoder().encode(full).length < NOTIFY_PAYLOAD_LIMIT_BYTES) return full;\n return JSON.stringify({ scope, event: { ...event, tags: tableTags(event.tags) } });\n}\n\nfunction tableTags(tags: readonly Tag[]): Tag[] {\n const tables = new Set<string | number>();\n for (const tag of tags) if (tag[0] !== undefined) tables.add(tag[0]);\n return [...tables].map((table) => [table]);\n}\n\ninterface EventRow {\n id: string;\n version: number | string;\n tags: Tag[];\n source: string | null;\n}\n\n/** One scope's events, or several streams' merged on the shared version order. */\nexport async function readSyncEvents(\n db: SyncDatabase,\n scope: string | readonly string[],\n since: SyncCursor,\n limit: number = MAX_BATCH,\n): Promise<SyncEvent[]> {\n const scopes = typeof scope === \"string\" ? [scope] : scope;\n const found = await rows<EventRow>(\n db.execute(sql`\n select id, version, tags, source\n from sync_events\n where scope in (${sql.join(\n scopes.map((one) => sql`${one}`),\n sql`, `,\n )})\n and (version > ${since.version} or (version = ${since.version} and id > ${since.id}))\n order by version asc, id asc\n limit ${limit}`),\n );\n return found\n .map((row) => ({\n id: row.id,\n version: Number(row.version),\n tags: row.tags,\n source: row.source ?? undefined,\n }))\n .sort((a, b) => compareCursors(a, b));\n}\n\nexport type SyncBacklog =\n | { readonly events: SyncEvent[]; readonly resetTo?: undefined }\n | { readonly resetTo: SyncCursor };\n\n/**\n * Every event of these streams after `since`, or, when they cannot all be replayed, the\n * cursor to reset to: there are more than one batch of them, or pruning removed events\n * after `since`. The initial cursor asks for the retained log, so pruning never resets it.\n */\nexport async function readSyncBacklog(\n db: SyncDatabase,\n scope: string | readonly string[],\n since: SyncCursor,\n): Promise<SyncBacklog> {\n const events = await readSyncEvents(db, scope, since, MAX_BATCH + 1);\n const pruned = await readPrunedVersion(db);\n const fromStart = compareCursors(since, INITIAL_CURSOR) === 0;\n if (events.length <= MAX_BATCH && (fromStart || since.version >= pruned)) {\n return { events };\n }\n const head = await readSyncCursor(db, scope);\n const floor: SyncCursor = { version: pruned, id: \"\" };\n return { resetTo: compareCursors(head, floor) > 0 ? head : floor };\n}\n\nasync function readPrunedVersion(db: SyncDatabase): Promise<number> {\n const [row] = await rows<{ version: number | string }>(\n db.execute(sql`select version from sync_pruned where id = 1`),\n );\n return Number(row?.version ?? 0);\n}\n\n/**\n * The last event of these streams, or the initial cursor when they have none. Read before\n * a query, it marks what the query's answer already reflects: versions are allocated under\n * one row lock, so any event committed later in these streams sorts after it.\n */\nexport async function readSyncCursor(\n db: SyncDatabase,\n scope: string | readonly string[],\n): Promise<SyncCursor> {\n const scopes = typeof scope === \"string\" ? [scope] : scope;\n const lasts = scopes.map(\n (one) =>\n sql`(select version, id from sync_events where scope = ${one} order by version desc, id desc limit 1)`,\n );\n const found = await rows<{ id: string; version: number | string }>(\n db.execute(sql.join(lasts, sql` union all `)),\n );\n return found\n .map((row) => ({ version: Number(row.version), id: row.id }))\n .reduce<SyncCursor>(\n (latest, cursor) => (compareCursors(cursor, latest) > 0 ? cursor : latest),\n INITIAL_CURSOR,\n );\n}\n\nexport async function pruneSyncEvents(\n db: SyncDatabase,\n retentionMs: number = RETENTION_MS,\n): Promise<number> {\n return db.transaction(async (tx) => {\n const [lock] = await rows<{ held: boolean }>(\n tx.execute(sql`select pg_try_advisory_xact_lock(${PRUNE_LOCK_KEY}) as held`),\n );\n if (!lock?.held) return 0;\n const cutoff = new Date(Date.now() - retentionMs);\n const [deleted] = await rows<{ count: number }>(\n tx.execute(sql`\n with deleted as (\n delete from sync_events where created_at < ${cutoff} returning version\n ), marked as (\n update sync_pruned set version = greatest(version, (select max(version) from deleted))\n where id = 1\n )\n select count(*)::int as count from deleted`),\n );\n return deleted?.count ?? 0;\n });\n}\n\nexport interface NotifyPayload {\n readonly scope: string;\n readonly event: SyncEvent;\n}\n\nexport function parseNotifyPayload(payload: string): NotifyPayload | undefined {\n try {\n const parsed = JSON.parse(payload) as Partial<NotifyPayload>;\n if (typeof parsed.scope !== \"string\" || !parsed.event) return undefined;\n return { scope: parsed.scope, event: parsed.event };\n } catch {\n return undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/server/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Tag } from \"@parlor/core\";\nimport { sql } from \"drizzle-orm\";\nimport type { PgDatabase, PgQueryResultHKT } from \"drizzle-orm/pg-core\";\n\nimport {\n compareCursors,\n INITIAL_CURSOR,\n type SyncCursor,\n type SyncEvent,\n SYNC_CHANNEL,\n} from \"../protocol.js\";\n\nexport type SyncDatabase = PgDatabase<PgQueryResultHKT>;\n\nexport const RETENTION_MS = 24 * 60 * 60 * 1000;\nexport const PRUNE_LOCK_KEY = 7_311_207;\nexport const NOTIFY_PAYLOAD_LIMIT_BYTES = 8000;\nexport const MAX_BATCH = 500;\n\nasync function rows<T>(query: Promise<unknown>): Promise<T[]> {\n const result = (await query) as { rows?: T[] } | T[];\n return Array.isArray(result) ? result : (result.rows ?? []);\n}\n\nexport async function ensureSyncTables(db: SyncDatabase): Promise<void> {\n await db.execute(sql`\n create table if not exists sync_events (\n id text primary key,\n version bigint not null,\n scope text not null,\n tags jsonb not null,\n source text,\n created_at timestamptz not null default clock_timestamp()\n )`);\n await db.execute(\n sql`create index if not exists sync_events_scope_version on sync_events (scope, version, id)`,\n );\n await db.execute(\n sql`create table if not exists sync_version (id int primary key, v bigint not null)`,\n );\n await db.execute(sql`\n insert into sync_version (id, v)\n select 1, greatest(coalesce((select max(version) from sync_events), 0), 0)\n on conflict (id) do nothing`);\n await db.execute(\n sql`create table if not exists sync_pruned (id int primary key, version bigint not null)`,\n );\n await db.execute(sql`\n insert into sync_pruned (id, version)\n select 1, coalesce((select min(version) - 1 from sync_events), (select v from sync_version where id = 1))\n on conflict (id) do nothing`);\n}\n\nexport interface RecordSyncEventInput {\n readonly scope: string;\n readonly tags: readonly Tag[];\n readonly source?: string | undefined;\n}\n\nexport async function recordSyncEvent(\n tx: SyncDatabase,\n input: RecordSyncEventInput,\n): Promise<SyncEvent> {\n if (input.tags.length === 0) throw new Error(\"A sync event needs at least one tag\");\n const id = crypto.randomUUID();\n const tags = JSON.stringify(input.tags);\n const inserted = await rows<{ version: number | string }>(\n tx.execute(sql`\n with alloc as (\n update sync_version\n set v = greatest(v + 1, (extract(epoch from clock_timestamp()) * 1000)::bigint)\n where id = 1\n returning v\n )\n insert into sync_events (id, version, scope, tags, source)\n select ${id}, alloc.v, ${input.scope}, ${tags}::jsonb, ${input.source ?? null}\n from alloc\n returning version`),\n );\n const row = inserted[0];\n if (!row) throw new Error(\"sync_version row is missing; run ensureSyncTables first\");\n const event: SyncEvent = {\n version: Number(row.version),\n id,\n tags: input.tags,\n source: input.source,\n };\n await tx.execute(\n sql`select pg_notify(${SYNC_CHANNEL}, ${notifyPayload(input.scope, event)})`,\n );\n return event;\n}\n\n// pg_notify rejects payloads of 8000 bytes or more; the row keeps the full tags.\nexport function notifyPayload(scope: string, event: SyncEvent): string {\n const full = JSON.stringify({ scope, event });\n if (new TextEncoder().encode(full).length < NOTIFY_PAYLOAD_LIMIT_BYTES) return full;\n return JSON.stringify({ scope, event: { ...event, tags: tableTags(event.tags) } });\n}\n\nfunction tableTags(tags: readonly Tag[]): Tag[] {\n const tables = new Set<string | number>();\n for (const tag of tags) if (tag[0] !== undefined) tables.add(tag[0]);\n return [...tables].map((table) => [table]);\n}\n\ninterface EventRow {\n id: string;\n version: number | string;\n tags: Tag[];\n source: string | null;\n}\n\n/** One scope's events, or several streams' merged on the shared version order. */\nexport async function readSyncEvents(\n db: SyncDatabase,\n scope: string | readonly string[],\n since: SyncCursor,\n limit: number = MAX_BATCH,\n): Promise<SyncEvent[]> {\n const scopes = typeof scope === \"string\" ? [scope] : scope;\n const found = await rows<EventRow>(\n db.execute(sql`\n select id, version, tags, source\n from sync_events\n where scope in (${sql.join(\n scopes.map((one) => sql`${one}`),\n sql`, `,\n )})\n and (version > ${since.version} or (version = ${since.version} and id > ${since.id}))\n order by version asc, id asc\n limit ${limit}`),\n );\n return found\n .map((row) => ({\n id: row.id,\n version: Number(row.version),\n tags: row.tags,\n source: row.source ?? undefined,\n }))\n .sort((a, b) => compareCursors(a, b));\n}\n\nexport type SyncBacklog =\n | { readonly events: SyncEvent[]; readonly resetTo?: undefined }\n | { readonly resetTo: SyncCursor };\n\n/**\n * Every event of these streams after `since`, or, when they cannot all be replayed, the\n * cursor to reset to: there are more than one batch of them, or pruning removed events\n * after `since`. The initial cursor asks for the retained log, so pruning never resets it.\n */\nexport async function readSyncBacklog(\n db: SyncDatabase,\n scope: string | readonly string[],\n since: SyncCursor,\n): Promise<SyncBacklog> {\n const events = await readSyncEvents(db, scope, since, MAX_BATCH + 1);\n const pruned = await readPrunedVersion(db);\n const fromStart = compareCursors(since, INITIAL_CURSOR) === 0;\n if (events.length <= MAX_BATCH && (fromStart || since.version >= pruned)) {\n return { events };\n }\n const head = await readSyncCursor(db, scope);\n const floor: SyncCursor = { version: pruned, id: \"\" };\n return { resetTo: compareCursors(head, floor) > 0 ? head : floor };\n}\n\nasync function readPrunedVersion(db: SyncDatabase): Promise<number> {\n const [row] = await rows<{ version: number | string }>(\n db.execute(sql`select version from sync_pruned where id = 1`),\n );\n return Number(row?.version ?? 0);\n}\n\n/**\n * The last event of these streams, or the initial cursor when they have none. Read before\n * a query, it marks what the query's answer already reflects: versions are allocated under\n * one row lock, so any event committed later in these streams sorts after it.\n */\nexport async function readSyncCursor(\n db: SyncDatabase,\n scope: string | readonly string[],\n): Promise<SyncCursor> {\n const scopes = typeof scope === \"string\" ? [scope] : scope;\n const lasts = scopes.map(\n (one) =>\n sql`(select version, id from sync_events where scope = ${one} order by version desc, id desc limit 1)`,\n );\n const found = await rows<{ id: string; version: number | string }>(\n db.execute(sql.join(lasts, sql` union all `)),\n );\n return found\n .map((row) => ({ version: Number(row.version), id: row.id }))\n .reduce<SyncCursor>(\n (latest, cursor) => (compareCursors(cursor, latest) > 0 ? cursor : latest),\n INITIAL_CURSOR,\n );\n}\n\nexport async function pruneSyncEvents(\n db: SyncDatabase,\n retentionMs: number = RETENTION_MS,\n): Promise<number> {\n return db.transaction(async (tx) => {\n const [lock] = await rows<{ held: boolean }>(\n tx.execute(sql`select pg_try_advisory_xact_lock(${PRUNE_LOCK_KEY}) as held`),\n );\n if (!lock?.held) return 0;\n const cutoff = new Date(Date.now() - retentionMs);\n const [deleted] = await rows<{ count: number }>(\n tx.execute(sql`\n with deleted as (\n delete from sync_events where created_at < ${cutoff} returning version\n ), marked as (\n update sync_pruned set version = greatest(version, (select max(version) from deleted))\n where id = 1\n )\n select count(*)::int as count from deleted`),\n );\n return deleted?.count ?? 0;\n });\n}\n\nexport interface NotifyPayload {\n readonly scope: string;\n readonly event: SyncEvent;\n}\n\nexport function parseNotifyPayload(payload: string): NotifyPayload | undefined {\n try {\n const parsed = JSON.parse(payload) as Partial<NotifyPayload>;\n if (typeof parsed.scope !== \"string\" || !parsed.event) return undefined;\n return { scope: parsed.scope, event: parsed.event };\n } catch {\n return undefined;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parlor/sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"homepage": "https://cychien.github.io/parlor",
|
|
5
5
|
"bugs": "https://github.com/cychien/parlor/issues",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"drizzle-orm": "^0.45.4",
|
|
32
32
|
"h3": "^2.0.1",
|
|
33
|
-
"@parlor/core": "0.
|
|
33
|
+
"@parlor/core": "0.4.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^24.14.0",
|
|
37
37
|
"typescript": "^7.0.2",
|
|
38
38
|
"vitest": "^5.0.3",
|
|
39
|
-
"@parlor/data": "0.
|
|
39
|
+
"@parlor/data": "0.4.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|