@shadow-js/realtime 0.0.0-stage → 1.0.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/dev/adapter.d.ts +75 -0
- package/dist/dev/client/core.d.ts +24 -0
- package/dist/dev/client/core.js +845 -0
- package/dist/dev/client/core.js.map +1 -0
- package/dist/dev/client/index.d.ts +5 -0
- package/dist/dev/client/index.js +3 -0
- package/dist/dev/client/presence.js +23 -0
- package/dist/dev/client/presence.js.map +1 -0
- package/dist/dev/client/sse.js +76 -0
- package/dist/dev/client/sse.js.map +1 -0
- package/dist/dev/client/types.d.ts +256 -0
- package/dist/dev/client/util.js +30 -0
- package/dist/dev/client/util.js.map +1 -0
- package/dist/dev/client/ws.js +39 -0
- package/dist/dev/client/ws.js.map +1 -0
- package/dist/dev/cloudflare/adapter.d.ts +80 -0
- package/dist/dev/cloudflare/adapter.js +347 -0
- package/dist/dev/cloudflare/adapter.js.map +1 -0
- package/dist/dev/cloudflare/core.js +701 -0
- package/dist/dev/cloudflare/core.js.map +1 -0
- package/dist/dev/cloudflare/link.js +397 -0
- package/dist/dev/cloudflare/link.js.map +1 -0
- package/dist/dev/cloudflare/object.d.ts +82 -0
- package/dist/dev/cloudflare/object.js +214 -0
- package/dist/dev/cloudflare/object.js.map +1 -0
- package/dist/dev/cloudflare/types.d.ts +100 -0
- package/dist/dev/cloudflare/wire.js +284 -0
- package/dist/dev/cloudflare/wire.js.map +1 -0
- package/dist/dev/cloudflare.d.ts +4 -0
- package/dist/dev/cloudflare.js +3 -0
- package/dist/dev/errors.d.ts +25 -0
- package/dist/dev/errors.js +24 -0
- package/dist/dev/errors.js.map +1 -0
- package/dist/dev/hooks.d.ts +110 -0
- package/dist/dev/hooks.js +162 -0
- package/dist/dev/hooks.js.map +1 -0
- package/dist/dev/index.d.ts +4 -0
- package/dist/dev/index.js +3 -0
- package/dist/dev/memory.d.ts +33 -0
- package/dist/dev/memory.js +255 -0
- package/dist/dev/memory.js.map +1 -0
- package/dist/dev/protocol.d.ts +211 -0
- package/dist/dev/protocol.js +400 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/redis/client.d.ts +139 -0
- package/dist/dev/redis/client.js +146 -0
- package/dist/dev/redis/client.js.map +1 -0
- package/dist/dev/redis/scripts.js +275 -0
- package/dist/dev/redis/scripts.js.map +1 -0
- package/dist/dev/redis/topics.d.ts +8 -0
- package/dist/dev/redis/topics.js +377 -0
- package/dist/dev/redis/topics.js.map +1 -0
- package/dist/dev/redis.d.ts +68 -0
- package/dist/dev/redis.js +422 -0
- package/dist/dev/redis.js.map +1 -0
- package/dist/dev/server/http.d.ts +48 -0
- package/dist/dev/server/http.js +286 -0
- package/dist/dev/server/http.js.map +1 -0
- package/dist/dev/server/hub.d.ts +114 -0
- package/dist/dev/server/hub.js +827 -0
- package/dist/dev/server/hub.js.map +1 -0
- package/dist/dev/server/index.d.ts +47 -0
- package/dist/dev/server/index.js +41 -0
- package/dist/dev/server/index.js.map +1 -0
- package/dist/dev/server/limits.d.ts +54 -0
- package/dist/dev/server/limits.js +85 -0
- package/dist/dev/server/limits.js.map +1 -0
- package/dist/dev/server/rules.d.ts +43 -0
- package/dist/dev/server/rules.js +72 -0
- package/dist/dev/server/rules.js.map +1 -0
- package/dist/dev/testing.d.ts +115 -0
- package/dist/dev/testing.js +427 -0
- package/dist/dev/testing.js.map +1 -0
- package/dist/prod/client/core.js +2 -0
- package/dist/prod/client/core.js.map +1 -0
- package/dist/prod/client/index.js +1 -0
- package/dist/prod/client/presence.js +2 -0
- package/dist/prod/client/presence.js.map +1 -0
- package/dist/prod/client/sse.js +5 -0
- package/dist/prod/client/sse.js.map +1 -0
- package/dist/prod/client/util.js +2 -0
- package/dist/prod/client/util.js.map +1 -0
- package/dist/prod/client/ws.js +2 -0
- package/dist/prod/client/ws.js.map +1 -0
- package/dist/prod/cloudflare/adapter.js +3 -0
- package/dist/prod/cloudflare/adapter.js.map +1 -0
- package/dist/prod/cloudflare/core.js +2 -0
- package/dist/prod/cloudflare/core.js.map +1 -0
- package/dist/prod/cloudflare/link.js +2 -0
- package/dist/prod/cloudflare/link.js.map +1 -0
- package/dist/prod/cloudflare/object.js +2 -0
- package/dist/prod/cloudflare/object.js.map +1 -0
- package/dist/prod/cloudflare/wire.js +2 -0
- package/dist/prod/cloudflare/wire.js.map +1 -0
- package/dist/prod/cloudflare.js +1 -0
- package/dist/prod/errors.js +2 -0
- package/dist/prod/errors.js.map +1 -0
- package/dist/prod/hooks.js +2 -0
- package/dist/prod/hooks.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/memory.js +2 -0
- package/dist/prod/memory.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/redis/client.js +2 -0
- package/dist/prod/redis/client.js.map +1 -0
- package/dist/prod/redis/scripts.js +157 -0
- package/dist/prod/redis/scripts.js.map +1 -0
- package/dist/prod/redis/topics.js +2 -0
- package/dist/prod/redis/topics.js.map +1 -0
- package/dist/prod/redis.js +2 -0
- package/dist/prod/redis.js.map +1 -0
- package/dist/prod/server/http.js +2 -0
- package/dist/prod/server/http.js.map +1 -0
- package/dist/prod/server/hub.js +2 -0
- package/dist/prod/server/hub.js.map +1 -0
- package/dist/prod/server/index.js +2 -0
- package/dist/prod/server/index.js.map +1 -0
- package/dist/prod/server/limits.js +2 -0
- package/dist/prod/server/limits.js.map +1 -0
- package/dist/prod/server/rules.js +2 -0
- package/dist/prod/server/rules.js.map +1 -0
- package/dist/prod/testing.js +2 -0
- package/dist/prod/testing.js.map +1 -0
- package/package.json +105 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","names":[],"sources":["../../../src/cloudflare/core.ts"],"sourcesContent":["/**\n * The logic of one channel, free of the Workers runtime: it takes the storage, the clock and the sockets as plain objects. The\n * Durable Object class wires it to the platform; the unit tests wire it to an in-memory storage. Everything that touches state\n * runs one at a time (a queue in front of every operation), so numbering is gapless by construction and a presence member\n * leaves exactly once, whether the object is awake, evicted and restarted, or woken by its alarm.\n *\n * Storage layout (keys sort as strings, numbers are zero padded):\n * - `meta`: head seq, oldest retained seq and its time, the retention of the last publish, what is due and when;\n * - `h:<seq>`: one retained message per key (a value stays far below the platform's limit);\n * - `p:<id>`: a presence member and its expiry;\n * - `k:<key>`: an idempotency key and the seq it produced; `x:<expires>:<key>`: the expiry index used to sweep keys.\n * @packageDocumentation\n */\nimport type { PublishOptions, PublishResult } from \"../adapter\";\nimport { RealtimeError } from \"../errors\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"../protocol\";\nimport type { ChannelSocket, ChannelStorage } from \"./types\";\nimport { type HistoryPage, historyKey, type LinkFrame, MAX_MEMBERS, seqOfKey } from \"./wire\";\n\nconst META = \"meta\";\nconst MESSAGES = \"h:\";\nconst MEMBERS = \"p:\";\nconst KEYS = \"k:\";\nconst KEY_INDEX = \"x:\";\n/** `x:` + 16 digits + `:`; what follows is the idempotency key. */\nconst KEY_INDEX_PREFIX = 19;\n/** The platform accepts at most 128 keys in one `delete`, `put` or `get`. */\nconst BATCH = 128;\n/** The most messages one operation deletes; a larger backlog is finished by the next operation or alarm. */\nconst TRIM_MAX = 1024;\n/** Waiting this long after the first idempotency key expires lets one alarm sweep many. */\nconst SWEEP_SLACK_MS = 5000;\n/** The same for retained messages. */\nconst HISTORY_SLACK_MS = 1000;\nconst PAGE_MESSAGES = 512;\nconst PAGE_BYTES = 4 * 1024 * 1024;\nconst REPLAY_MESSAGES = 10_000;\nconst REPLAY_BYTES = 8 * 1024 * 1024;\n\ninterface Meta {\n v: 1;\n channel: string;\n /** The highest seq assigned. */\n head: number;\n /** The oldest retained seq, 0 when nothing is retained. */\n oldest: number;\n /** When the oldest retained message was accepted. */\n oldestAt: number;\n /** The `history` count of the last publish. */\n keep: number;\n /** The `historyTtlMs` of the last publish. */\n ttlMs: number;\n /** When the earliest idempotency key can be swept, 0 for none. */\n keysDue: number;\n /** When the earliest presence member can be reaped by the alarm, 0 for none. */\n presenceDue: number;\n /** How long after its expiry the alarm reaps a member; the hub's own `presenceReap` runs inside that window. */\n graceMs: number;\n /** Forget the channel after this long without activity, 0 to keep it for good. */\n idleMs: number;\n /** The time of the last publish or presence change. */\n touched: number;\n}\n\ninterface KeyRecord {\n readonly seq: number;\n readonly expires: number;\n}\n\ninterface MemberRecord {\n readonly member: Member;\n expires: number;\n}\n\n/** A member in memory: `stored` is the expiry the storage holds, which lags behind `expires` to save writes. */\ninterface MemberState {\n member: Member;\n expires: number;\n stored: number;\n}\n\n/** What {@link ChannelCore} needs from its surroundings. @internal */\nexport interface CoreOptions {\n readonly storage: ChannelStorage;\n /** The clock in epoch milliseconds. Default: `Date.now`. */\n readonly now?: () => number;\n /** The most presence members one channel keeps. Default: {@link MAX_MEMBERS}. */\n readonly maxMembers?: number;\n}\n\nfunction pad(value: number): string {\n return String(value).padStart(16, \"0\");\n}\n\nfunction chunks<T>(items: readonly T[]): T[][] {\n const out: T[][] = [];\n for (let at = 0; at < items.length; at += BATCH) out.push(items.slice(at, at + BATCH));\n return out;\n}\n\nfunction sizeOf(message: StoredMessage): number {\n return message.data.length + message.event.length + 96;\n}\n\n/**\n * The single writer of one channel.\n * @internal\n */\nexport class ChannelCore {\n readonly #storage: ChannelStorage;\n readonly #now: () => number;\n readonly #maxMembers: number;\n readonly #sockets = new Set<ChannelSocket>();\n #tail: Promise<void> = Promise.resolve();\n #meta: Meta | undefined;\n #members: Map<string, MemberState> | undefined;\n /** The alarm time the storage holds: `undefined` unknown, `null` none. */\n #alarmAt: number | null | undefined;\n\n constructor(options: CoreOptions) {\n this.#storage = options.storage;\n this.#now = options.now ?? Date.now;\n this.#maxMembers = options.maxMembers ?? MAX_MEMBERS;\n }\n\n /** Registers sockets that were open before this instance was created (the runtime kept them while the object slept). */\n adopt(sockets: Iterable<ChannelSocket>): void {\n for (const socket of sockets) this.#sockets.add(socket);\n }\n\n /** Forgets a socket that closed. */\n detach(socket: ChannelSocket): void {\n this.#sockets.delete(socket);\n }\n\n /** The number of sockets attached. */\n get sockets(): number {\n return this.#sockets.size;\n }\n\n // --- the queue -----------------------------------------------------------------------------------------------------\n\n /** Runs `task` after every earlier task finished. A failure drops the caches, so the next task reads the storage afresh. */\n #run<T>(task: () => Promise<T>): Promise<T> {\n const result = this.#tail.then(task).catch((error: unknown) => {\n if (!(error instanceof RealtimeError)) {\n this.#meta = undefined;\n this.#members = undefined;\n this.#alarmAt = undefined;\n }\n throw error;\n });\n this.#tail = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n\n // --- state ---------------------------------------------------------------------------------------------------------\n\n async #metaFor(channel: string): Promise<Meta> {\n const meta = await this.#storedMeta();\n if (meta !== undefined) {\n if (meta.channel !== channel)\n throw new RealtimeError(\"conflict\", \"the object belongs to another channel\");\n return meta;\n }\n const fresh: Meta = {\n v: 1,\n channel,\n head: 0,\n oldest: 0,\n oldestAt: 0,\n keep: 0,\n ttlMs: 0,\n keysDue: 0,\n presenceDue: 0,\n graceMs: 0,\n idleMs: 0,\n touched: this.#now(),\n };\n this.#meta = fresh;\n return fresh;\n }\n\n async #storedMeta(): Promise<Meta | undefined> {\n if (this.#meta === undefined) this.#meta = await this.#storage.get<Meta>(META);\n return this.#meta;\n }\n\n async #loadMembers(): Promise<Map<string, MemberState>> {\n if (this.#members === undefined) {\n const rows = await this.#storage.list<MemberRecord>({ prefix: MEMBERS });\n const members = new Map<string, MemberState>();\n for (const [key, record] of rows)\n members.set(key.slice(MEMBERS.length), {\n member: record.member,\n expires: record.expires,\n stored: record.expires,\n });\n this.#members = members;\n }\n return this.#members;\n }\n\n /** Writes `puts` and `deletes` and the metadata in one transaction. */\n async #commit(\n meta: Meta,\n puts: Readonly<Record<string, unknown>>,\n deletes: readonly string[],\n ): Promise<void> {\n await this.#storage.transaction(async (transaction) => {\n await transaction.put({ ...puts, [META]: meta });\n for (const batch of chunks(deletes)) await transaction.delete(batch);\n });\n this.#meta = meta;\n }\n\n // --- fan-out -------------------------------------------------------------------------------------------------------\n\n #send(socket: ChannelSocket, text: string): boolean {\n try {\n socket.send(text);\n return true;\n } catch {\n this.#sockets.delete(socket);\n try {\n socket.close(1011, \"write failed\");\n } catch {\n // The socket is gone already.\n }\n return false;\n }\n }\n\n #broadcast(frame: LinkFrame): void {\n if (this.#sockets.size === 0) return;\n const text = JSON.stringify(frame);\n for (const socket of [...this.#sockets]) this.#send(socket, text);\n }\n\n #emit(op: PresenceEvent[\"op\"], member: Member): void {\n this.#broadcast({ t: \"pres\", op, member });\n }\n\n // --- messages ------------------------------------------------------------------------------------------------------\n\n /**\n * Assigns the next seq, retains the message, remembers the key and sends the message to every attached socket.\n *\n * @param channel The channel name.\n * @param entry The message.\n * @param options Retention and the idempotency key.\n * @param idleMs Forget the channel after this long without activity (0 keeps it).\n * @returns The seq and whether the key was already accepted.\n */\n publish(\n channel: string,\n entry: StoredEntry,\n options: PublishOptions,\n idleMs = 0,\n ): Promise<PublishResult> {\n return this.#run(async () => {\n const meta = await this.#metaFor(channel);\n const now = this.#now();\n if (options.key !== undefined) {\n const known = await this.#storage.get<KeyRecord>(KEYS + options.key);\n if (known !== undefined && known.expires > now) return { seq: known.seq, duplicate: true };\n }\n const seq = meta.head + 1;\n const message: StoredMessage = {\n seq,\n event: entry.event,\n data: entry.data,\n at: now,\n ...(entry.publisher === undefined ? {} : { publisher: entry.publisher }),\n ...(entry.user === undefined ? {} : { user: entry.user }),\n };\n const plan = await this.#planTrim(meta, now, options.history, options.historyTtlMs, 1);\n const next: Meta = {\n ...meta,\n head: seq,\n keep: options.history,\n ttlMs: options.historyTtlMs,\n idleMs,\n touched: now,\n oldest: plan.oldest,\n oldestAt: plan.oldestAt,\n };\n const puts: Record<string, unknown> = {};\n if (options.history > 0) {\n puts[historyKey(seq)] = message;\n if (next.oldest === 0) {\n next.oldest = seq;\n next.oldestAt = now;\n }\n }\n if (options.key !== undefined) {\n const expires = now + options.idempotencyTtlMs;\n puts[KEYS + options.key] = { seq, expires } satisfies KeyRecord;\n puts[`${KEY_INDEX}${pad(expires)}:${options.key}`] = 1;\n const due = expires + SWEEP_SLACK_MS;\n next.keysDue = next.keysDue === 0 ? due : Math.min(next.keysDue, due);\n }\n await this.#commit(next, puts, plan.deletes);\n this.#broadcast({ t: \"msg\", m: message });\n await this.#schedule(next);\n return { seq, duplicate: false };\n });\n }\n\n /**\n * Works out which retained messages to delete so that, after `extra` more messages are stored, at most `keep` remain and\n * none is older than `ttlMs`. It reads at most a few pages and deletes at most {@link TRIM_MAX} messages.\n */\n async #planTrim(\n meta: Meta,\n now: number,\n keep: number,\n ttlMs: number,\n extra: number,\n ): Promise<{ deletes: string[]; oldest: number; oldestAt: number }> {\n const deletes: string[] = [];\n let oldest = meta.oldest;\n let oldestAt = meta.oldestAt;\n if (oldest === 0) return { deletes, oldest: 0, oldestAt: 0 };\n const target = meta.head + 1 + extra - keep;\n if (oldest < target) {\n const stop = Math.min(target, meta.head + 1, oldest + TRIM_MAX);\n for (let seq = oldest; seq < stop; seq += 1) deletes.push(historyKey(seq));\n oldest = stop;\n if (oldest > meta.head) return { deletes, oldest: 0, oldestAt: 0 };\n const first = await this.#firstFrom(oldest);\n if (first === undefined) return { deletes, oldest: 0, oldestAt: 0 };\n oldest = first.seq;\n oldestAt = first.at;\n }\n const cutoff = now - ttlMs;\n while (oldestAt < cutoff && deletes.length < TRIM_MAX) {\n const page = await this.#storage.list<StoredMessage>({\n prefix: MESSAGES,\n start: historyKey(oldest),\n limit: BATCH,\n });\n let survivor: StoredMessage | undefined;\n for (const message of page.values()) {\n if (message.at >= cutoff || deletes.length >= TRIM_MAX) {\n survivor = message;\n break;\n }\n deletes.push(historyKey(message.seq));\n }\n if (survivor !== undefined) {\n oldest = survivor.seq;\n oldestAt = survivor.at;\n break;\n }\n if (page.size < BATCH) return { deletes, oldest: 0, oldestAt: 0 };\n const last = deletes.length === 0 ? undefined : deletes[deletes.length - 1];\n if (last === undefined) break;\n oldest = seqOfKey(last) + 1;\n const next = await this.#firstFrom(oldest);\n if (next === undefined) return { deletes, oldest: 0, oldestAt: 0 };\n oldest = next.seq;\n oldestAt = next.at;\n }\n return { deletes, oldest, oldestAt };\n }\n\n async #firstFrom(seq: number): Promise<StoredMessage | undefined> {\n const page = await this.#storage.list<StoredMessage>({\n prefix: MESSAGES,\n start: historyKey(seq),\n limit: 1,\n });\n for (const message of page.values()) return message;\n return undefined;\n }\n\n /** Whether retained messages are past their time or above the count of the last publish. */\n #retentionDue(meta: Meta, now: number): boolean {\n if (meta.oldest === 0) return false;\n return meta.head - meta.oldest + 1 > meta.keep || meta.oldestAt + meta.ttlMs < now;\n }\n\n async #retain(meta: Meta, now: number): Promise<boolean> {\n if (!this.#retentionDue(meta, now)) return false;\n const plan = await this.#planTrim(meta, now, meta.keep, meta.ttlMs, 0);\n if (plan.deletes.length === 0 && plan.oldest === meta.oldest) return false;\n await this.#commit({ ...meta, oldest: plan.oldest, oldestAt: plan.oldestAt }, {}, plan.deletes);\n return true;\n }\n\n /**\n * Reads retained messages after a seq.\n *\n * @param channel The channel name.\n * @param after The last seq the caller holds.\n * @param limit The most messages to return (0 asks for the head only).\n * @returns A page; `more` says retained messages remain after it.\n */\n history(channel: string, after: number, limit: number): Promise<HistoryPage> {\n return this.#run(async () => {\n let meta = await this.#metaFor(channel);\n if (await this.#retain(meta, this.#now())) meta = this.#meta ?? meta;\n if (limit <= 0 || after >= meta.head || meta.oldest === 0)\n return { messages: [], head: meta.head, oldest: meta.oldest, more: false };\n const cap = Math.min(limit, PAGE_MESSAGES);\n const rows = await this.#storage.list<StoredMessage>({\n prefix: MESSAGES,\n start: historyKey(Math.max(after + 1, meta.oldest)),\n limit: cap,\n });\n const messages: StoredMessage[] = [];\n let bytes = 0;\n for (const message of rows.values()) {\n if (messages.length > 0 && bytes + sizeOf(message) > PAGE_BYTES) break;\n messages.push(message);\n bytes += sizeOf(message);\n }\n const last = messages[messages.length - 1];\n // Retained messages are contiguous, so any seq above the last one returned (up to the head) is retained too.\n return {\n messages,\n head: meta.head,\n oldest: meta.oldest,\n more: last !== undefined && last.seq < meta.head,\n };\n });\n }\n\n // --- subscribers ---------------------------------------------------------------------------------------------------\n\n /**\n * Starts sending a socket the channel's messages and presence events: the head, the live members, the messages after\n * `after` that are retained, then every later event. Nothing can interleave, because it runs in the queue.\n *\n * @param socket The server end of the subscribe socket.\n * @param channel The channel name.\n * @param after The last seq the subscriber holds, when it is resuming.\n */\n attach(socket: ChannelSocket, channel: string, after: number | undefined): Promise<void> {\n return this.#run(async () => {\n let meta = await this.#metaFor(channel);\n const now = this.#now();\n if (await this.#retain(meta, now)) meta = this.#meta ?? meta;\n const members = await this.#loadMembers();\n const live: Member[] = [];\n for (const state of members.values()) if (state.expires > now) live.push(state.member);\n const frames: string[] = [\n JSON.stringify({ t: \"ready\", head: meta.head, oldest: meta.oldest } satisfies LinkFrame),\n JSON.stringify({ t: \"snap\", members: live } satisfies LinkFrame),\n ];\n let truncated = false;\n if (after !== undefined && after < meta.head && meta.oldest > 0) {\n let cursor = Math.max(after, meta.oldest - 1);\n let count = 0;\n let bytes = 0;\n replay: while (cursor < meta.head) {\n const rows = await this.#storage.list<StoredMessage>({\n prefix: MESSAGES,\n start: historyKey(cursor + 1),\n limit: BATCH,\n });\n if (rows.size === 0) break;\n for (const message of rows.values()) {\n frames.push(JSON.stringify({ t: \"msg\", m: message } satisfies LinkFrame));\n cursor = message.seq;\n count += 1;\n bytes += sizeOf(message);\n if (count >= REPLAY_MESSAGES || bytes >= REPLAY_BYTES) {\n truncated = cursor < meta.head;\n break replay;\n }\n }\n if (rows.size < BATCH) break;\n }\n }\n if (truncated) frames.push(JSON.stringify({ t: \"more\" } satisfies LinkFrame));\n for (const frame of frames) {\n try {\n socket.send(frame);\n } catch {\n return;\n }\n }\n this.#sockets.add(socket);\n });\n }\n\n // --- presence ------------------------------------------------------------------------------------------------------\n\n /**\n * Adds or updates a member. An older version is ignored; the first sighting is a `join`, a newer version a `set`.\n *\n * @param channel The channel name.\n * @param member The member.\n * @param ttlMs Milliseconds until it expires without a touch.\n * @param graceMs Milliseconds after expiry that the alarm waits before it reaps (so the hub's own reap comes first).\n */\n presenceSet(channel: string, member: Member, ttlMs: number, graceMs: number): Promise<void> {\n return this.#run(async () => {\n const meta = await this.#metaFor(channel);\n const members = await this.#loadMembers();\n const known = members.get(member.id);\n if (known !== undefined && known.member.v > member.v) return;\n if (known === undefined && members.size >= this.#maxMembers)\n throw new RealtimeError(\"limit\", \"the channel has too many presence members\");\n const now = this.#now();\n const expires = now + ttlMs;\n const changed = known === undefined || known.member.v < member.v;\n const due = expires + graceMs;\n const next: Meta = {\n ...meta,\n graceMs,\n touched: now,\n presenceDue: meta.presenceDue === 0 ? due : Math.min(meta.presenceDue, due),\n };\n const persist = changed || known === undefined || expires - known.stored >= ttlMs / 2;\n if (persist || next.presenceDue !== meta.presenceDue || next.graceMs !== meta.graceMs) {\n await this.#commit(\n next,\n persist ? { [MEMBERS + member.id]: { member, expires } satisfies MemberRecord } : {},\n [],\n );\n }\n members.set(member.id, {\n member,\n expires,\n stored: persist ? expires : (known?.stored ?? expires),\n });\n if (known === undefined) this.#emit(\"join\", member);\n else if (known.member.v < member.v) this.#emit(\"set\", member);\n await this.#schedule(next);\n });\n }\n\n /**\n * Removes a member.\n *\n * @param channel The channel name.\n * @param id The member id.\n * @returns True when the member existed (and a `leave` was sent); false otherwise.\n */\n presenceRemove(channel: string, id: string): Promise<boolean> {\n return this.#run(async () => {\n const meta = await this.#metaFor(channel);\n const members = await this.#loadMembers();\n const known = members.get(id);\n if (known === undefined) return false;\n const next: Meta = members.size === 1 ? { ...meta, presenceDue: 0 } : meta;\n await this.#commit(next, {}, [MEMBERS + id]);\n members.delete(id);\n this.#emit(\"leave\", known.member);\n await this.#schedule(next);\n return true;\n });\n }\n\n /**\n * The members that have not expired.\n *\n * @param channel The channel name.\n * @returns The members.\n */\n presenceList(channel: string): Promise<Member[]> {\n return this.#run(async () => {\n await this.#metaFor(channel);\n const members = await this.#loadMembers();\n const now = this.#now();\n const out: Member[] = [];\n for (const state of members.values()) if (state.expires > now) out.push(state.member);\n return out;\n });\n }\n\n /**\n * Extends the expiry of members. The storage is written only when the new expiry is at least half a time to live beyond\n * the stored one, so a heartbeat every few seconds costs a write every few beats; a restart in between loses at most\n * that half, which a heartbeat repairs long before the member would expire.\n *\n * @param channel The channel name.\n * @param ids The member ids.\n * @param ttlMs Milliseconds until they expire without another touch.\n * @returns The ids that no longer exist.\n */\n presenceTouch(channel: string, ids: readonly string[], ttlMs: number): Promise<string[]> {\n return this.#run(async () => {\n const meta = await this.#metaFor(channel);\n const members = await this.#loadMembers();\n const now = this.#now();\n const expires = now + ttlMs;\n const missing: string[] = [];\n const puts: Record<string, unknown> = {};\n const written: MemberState[] = [];\n let touched = 0;\n for (const id of ids) {\n const state = members.get(id);\n if (state === undefined) {\n missing.push(id);\n continue;\n }\n // Like the reference adapter, the expiry becomes exactly now + ttl: a touch with a shorter life shortens it.\n state.expires = expires;\n touched += 1;\n if (state.expires - state.stored >= ttlMs / 2) {\n puts[MEMBERS + id] = {\n member: state.member,\n expires: state.expires,\n } satisfies MemberRecord;\n written.push(state);\n }\n }\n const keys = Object.keys(puts);\n // `put` takes at most 128 entries.\n for (const batch of chunks(keys)) {\n const part: Record<string, unknown> = {};\n for (const key of batch) part[key] = puts[key];\n await this.#storage.put(part);\n }\n for (const state of written) state.stored = state.expires;\n // Touches usually push the expiry later, which the alarm finds out for itself; a shorter life needs an earlier alarm.\n const due = expires + meta.graceMs;\n if (touched > 0 && (meta.presenceDue === 0 || due < meta.presenceDue)) {\n const next: Meta = { ...meta, presenceDue: due };\n await this.#commit(next, {}, []);\n await this.#schedule(next);\n }\n return missing;\n });\n }\n\n /**\n * Removes every expired member and sends one `leave` for each.\n *\n * @param channel The channel name.\n * @returns The members removed.\n */\n presenceReap(channel: string): Promise<Member[]> {\n return this.#run(async () => {\n const meta = await this.#metaFor(channel);\n return this.#reap(meta, this.#now());\n });\n }\n\n async #reap(meta: Meta, now: number): Promise<Member[]> {\n const members = await this.#loadMembers();\n const gone: MemberState[] = [];\n for (const state of members.values()) if (state.expires <= now) gone.push(state);\n if (gone.length === 0) return [];\n let due = 0;\n for (const state of members.values())\n if (state.expires > now) {\n const at = state.expires + meta.graceMs;\n due = due === 0 ? at : Math.min(due, at);\n }\n const next: Meta = { ...meta, presenceDue: due };\n await this.#commit(\n next,\n {},\n gone.map((state) => MEMBERS + state.member.id),\n );\n for (const state of gone) {\n members.delete(state.member.id);\n this.#emit(\"leave\", state.member);\n }\n return gone.map((state) => state.member);\n }\n\n // --- the alarm -----------------------------------------------------------------------------------------------------\n\n /** The time the next alarm is due, `undefined` when nothing is pending. */\n #nextDue(meta: Meta, now: number): number | undefined {\n let due = 0;\n const consider = (at: number): void => {\n if (at > 0 && (due === 0 || at < due)) due = at;\n };\n consider(meta.presenceDue);\n consider(meta.keysDue);\n if (meta.oldest > 0)\n consider(\n meta.head - meta.oldest + 1 > meta.keep\n ? now\n : meta.oldestAt + meta.ttlMs + HISTORY_SLACK_MS,\n );\n if (meta.idleMs > 0) {\n // A channel with a subscriber is not idle: look again after another idle period instead of waking at once, every time.\n const idleAt = meta.touched + meta.idleMs;\n consider(this.#sockets.size > 0 && idleAt <= now ? now + meta.idleMs : idleAt);\n }\n return due === 0 ? undefined : Math.max(due, now);\n }\n\n /** Makes the storage's alarm fire no later than the next thing that is due. An earlier alarm is left alone: it wakes, finds nothing, and sets the right time. */\n async #schedule(meta: Meta): Promise<void> {\n const now = this.#now();\n const due = this.#nextDue(meta, now);\n if (this.#alarmAt === undefined) this.#alarmAt = await this.#storage.getAlarm();\n if (due === undefined) {\n if (this.#alarmAt !== null) {\n await this.#storage.deleteAlarm();\n this.#alarmAt = null;\n }\n return;\n }\n if (this.#alarmAt === null || this.#alarmAt > due) {\n await this.#storage.setAlarm(due);\n this.#alarmAt = due;\n }\n }\n\n /** Deletes idempotency keys that expired, in batches. */\n async #sweepKeys(meta: Meta, now: number): Promise<Meta> {\n for (let pass = 0; pass < 32; pass += 1) {\n const rows = await this.#storage.list<number>({\n prefix: KEY_INDEX,\n end: KEY_INDEX + pad(now + 1),\n limit: BATCH,\n });\n if (rows.size === 0) break;\n const indexKeys = [...rows.keys()];\n const names = indexKeys.map((key) => key.slice(KEY_INDEX_PREFIX));\n const records = await this.#storage.get<KeyRecord>(names.map((name) => KEYS + name));\n const deletes = [...indexKeys];\n for (const name of names) {\n const record = records.get(KEYS + name);\n if (record === undefined || record.expires <= now) deletes.push(KEYS + name);\n }\n await this.#storage.transaction(async (transaction) => {\n for (const batch of chunks(deletes)) await transaction.delete(batch);\n });\n if (rows.size < BATCH) break;\n }\n const first = await this.#storage.list<number>({ prefix: KEY_INDEX, limit: 1 });\n let keysDue = 0;\n for (const key of first.keys())\n keysDue = Number(key.slice(KEY_INDEX.length, KEY_INDEX.length + 16)) + SWEEP_SLACK_MS;\n return { ...meta, keysDue };\n }\n\n /**\n * The alarm handler: reaps expired presence members (one `leave` each), sweeps expired idempotency keys, trims retained\n * messages that expired, forgets a channel that has been idle for its `idleMs`, and sets the next alarm. It is safe to run\n * more often than needed, and to run again after a failure.\n */\n alarm(): Promise<void> {\n return this.#run(async () => {\n this.#alarmAt = null;\n let meta = await this.#storedMeta();\n if (meta === undefined) return;\n const now = this.#now();\n if (meta.presenceDue > 0 && meta.presenceDue <= now) {\n await this.#reap(meta, now);\n meta = this.#meta ?? meta;\n // An alarm earlier than the earliest expiry (members were touched) reaps nothing: move the due time instead.\n let due = 0;\n for (const state of (await this.#loadMembers()).values()) {\n const at = state.expires + meta.graceMs;\n due = due === 0 ? at : Math.min(due, at);\n }\n if (due !== meta.presenceDue) {\n meta = { ...meta, presenceDue: due };\n await this.#commit(meta, {}, []);\n }\n }\n if (meta.keysDue > 0 && meta.keysDue <= now) {\n meta = await this.#sweepKeys(meta, now);\n await this.#commit(meta, {}, []);\n }\n if (await this.#retain(meta, now)) meta = this.#meta ?? meta;\n if (\n meta.idleMs > 0 &&\n meta.touched + meta.idleMs <= now &&\n meta.oldest === 0 &&\n meta.presenceDue === 0 &&\n meta.keysDue === 0 &&\n this.#sockets.size === 0\n ) {\n await this.#storage.deleteAll();\n this.#meta = undefined;\n this.#members = undefined;\n this.#alarmAt = null;\n return;\n }\n await this.#schedule(meta);\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { RealtimeError } from "../errors.js";
|
|
2
|
+
import { decodeLinkFrame } from "./wire.js";
|
|
3
|
+
//#region src/cloudflare/link.ts
|
|
4
|
+
/**
|
|
5
|
+
* The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept
|
|
6
|
+
* open with backoff, resumed with `after`, and feeding each listener through its own queue.
|
|
7
|
+
*
|
|
8
|
+
* A Worker may use an I/O object (a socket, a stream, a timer) only inside the request that created it. A listener runs in the
|
|
9
|
+
* request that subscribed, so it is called by a loop that was started there and woken by a plain promise: the socket's
|
|
10
|
+
* handler only moves data into queues and never calls a listener itself.
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
function deferred() {
|
|
14
|
+
let resolve = () => void 0;
|
|
15
|
+
let reject = () => void 0;
|
|
16
|
+
const promise = new Promise((onResolve, onReject) => {
|
|
17
|
+
resolve = onResolve;
|
|
18
|
+
reject = onReject;
|
|
19
|
+
});
|
|
20
|
+
promise.catch(() => void 0);
|
|
21
|
+
return {
|
|
22
|
+
promise,
|
|
23
|
+
resolve,
|
|
24
|
+
reject
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A queue and the loop that empties it, in the request that created it. */
|
|
28
|
+
var Pump = class {
|
|
29
|
+
#queue = [];
|
|
30
|
+
#max;
|
|
31
|
+
#wake;
|
|
32
|
+
#stopped = false;
|
|
33
|
+
constructor(max) {
|
|
34
|
+
this.#max = max;
|
|
35
|
+
}
|
|
36
|
+
push(item) {
|
|
37
|
+
if (this.#stopped || this.#queue.length >= this.#max) return;
|
|
38
|
+
this.#queue.push(item);
|
|
39
|
+
const wake = this.#wake;
|
|
40
|
+
this.#wake = void 0;
|
|
41
|
+
wake?.();
|
|
42
|
+
}
|
|
43
|
+
stop() {
|
|
44
|
+
this.#stopped = true;
|
|
45
|
+
this.#queue.length = 0;
|
|
46
|
+
const wake = this.#wake;
|
|
47
|
+
this.#wake = void 0;
|
|
48
|
+
wake?.();
|
|
49
|
+
}
|
|
50
|
+
async run(deliver, report, every) {
|
|
51
|
+
const cancel = every?.();
|
|
52
|
+
try {
|
|
53
|
+
while (!this.#stopped) {
|
|
54
|
+
const item = this.#queue.shift();
|
|
55
|
+
if (item === void 0) {
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
this.#wake = resolve;
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
deliver(item);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
report(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
cancel?.();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** @internal */
|
|
73
|
+
var ChannelLink = class {
|
|
74
|
+
channel;
|
|
75
|
+
#host;
|
|
76
|
+
#messages = /* @__PURE__ */ new Set();
|
|
77
|
+
#presence = /* @__PURE__ */ new Set();
|
|
78
|
+
#state = "idle";
|
|
79
|
+
#socket;
|
|
80
|
+
#closeOwner;
|
|
81
|
+
#attempt = 0;
|
|
82
|
+
/** The highest seq received on this link: where a reconnect resumes. */
|
|
83
|
+
#last = 0;
|
|
84
|
+
#known = false;
|
|
85
|
+
#members;
|
|
86
|
+
#lastReceived = 0;
|
|
87
|
+
#failures = 0;
|
|
88
|
+
#retryAt = 0;
|
|
89
|
+
#connectedAt = 0;
|
|
90
|
+
#ready = deferred();
|
|
91
|
+
#cancelRetry;
|
|
92
|
+
#cancelConnect;
|
|
93
|
+
#cancelKeep;
|
|
94
|
+
constructor(channel, host) {
|
|
95
|
+
this.channel = channel;
|
|
96
|
+
this.#host = host;
|
|
97
|
+
}
|
|
98
|
+
get state() {
|
|
99
|
+
return this.#state;
|
|
100
|
+
}
|
|
101
|
+
get size() {
|
|
102
|
+
return this.#messages.size + this.#presence.size;
|
|
103
|
+
}
|
|
104
|
+
/** Adds a message listener (call it in the request that will receive messages). */
|
|
105
|
+
addMessages(listener) {
|
|
106
|
+
const sub = {
|
|
107
|
+
listener,
|
|
108
|
+
pump: new Pump(this.#host.maxQueued),
|
|
109
|
+
last: this.#state === "open" ? this.#last : -1
|
|
110
|
+
};
|
|
111
|
+
this.#messages.add(sub);
|
|
112
|
+
sub.pump.run(listener, (error) => this.#host.report(error, "listener", this.channel), this.#nudger());
|
|
113
|
+
this.#start();
|
|
114
|
+
return sub;
|
|
115
|
+
}
|
|
116
|
+
/** Adds a presence listener (call it in the request that will receive events). */
|
|
117
|
+
addPresence(listener) {
|
|
118
|
+
const sub = {
|
|
119
|
+
listener,
|
|
120
|
+
pump: new Pump(this.#host.maxQueued)
|
|
121
|
+
};
|
|
122
|
+
this.#presence.add(sub);
|
|
123
|
+
sub.pump.run(listener, (error) => this.#host.report(error, "listener", this.channel), this.#nudger());
|
|
124
|
+
this.#start();
|
|
125
|
+
return sub;
|
|
126
|
+
}
|
|
127
|
+
/** Removes a listener; the last one closes the link. */
|
|
128
|
+
remove(sub) {
|
|
129
|
+
sub.pump.stop();
|
|
130
|
+
this.#messages.delete(sub);
|
|
131
|
+
this.#presence.delete(sub);
|
|
132
|
+
if (this.size === 0) this.dispose(new RealtimeError("closed", "the subscription ended"));
|
|
133
|
+
}
|
|
134
|
+
/** Resolves when the socket is open and has told the channel's head; rejects after `ms`. */
|
|
135
|
+
whenReady(ms) {
|
|
136
|
+
if (this.#state === "open") return Promise.resolve();
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const cancel = this.#host.after(ms, () => reject(new RealtimeError("unavailable", "timed out connecting to the channel object")));
|
|
139
|
+
this.#ready.promise.then(() => {
|
|
140
|
+
cancel();
|
|
141
|
+
resolve();
|
|
142
|
+
}, (error) => {
|
|
143
|
+
cancel();
|
|
144
|
+
reject(error);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** The pump's periodic check, if pings are on. */
|
|
149
|
+
#nudger() {
|
|
150
|
+
const every = this.#host.keepAliveMs;
|
|
151
|
+
if (every <= 0) return void 0;
|
|
152
|
+
return () => this.#host.every(every, () => this.#check());
|
|
153
|
+
}
|
|
154
|
+
#start() {
|
|
155
|
+
if (this.#state === "idle") this.#connect();
|
|
156
|
+
}
|
|
157
|
+
#connect() {
|
|
158
|
+
if (this.#state === "disposed") return;
|
|
159
|
+
this.#cancelRetry?.();
|
|
160
|
+
this.#cancelRetry = void 0;
|
|
161
|
+
this.#cancelConnect?.();
|
|
162
|
+
this.#state = "connecting";
|
|
163
|
+
const attempt = ++this.#attempt;
|
|
164
|
+
this.#connectedAt = this.#host.now();
|
|
165
|
+
this.#cancelConnect = this.#host.after(this.#host.connectTimeoutMs, () => {
|
|
166
|
+
if (attempt === this.#attempt && this.#state === "connecting") this.#down(attempt, new RealtimeError("unavailable", "the channel object did not answer"));
|
|
167
|
+
});
|
|
168
|
+
this.#host.open(this.channel, this.#known ? this.#last : void 0).then((socket) => this.#opened(attempt, socket), (error) => {
|
|
169
|
+
if (attempt === this.#attempt) this.#down(attempt, error);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
#opened(attempt, socket) {
|
|
173
|
+
if (attempt !== this.#attempt || this.#state !== "connecting") {
|
|
174
|
+
this.#closeSocket(socket, void 0);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const owner = deferred();
|
|
178
|
+
this.#closeOwner = owner;
|
|
179
|
+
this.#socket = socket;
|
|
180
|
+
owner.promise.then(() => this.#tryClose(socket), () => void 0);
|
|
181
|
+
socket.addEventListener("message", (event) => {
|
|
182
|
+
if (attempt !== this.#attempt || typeof event.data !== "string") return;
|
|
183
|
+
this.#received(attempt, event.data);
|
|
184
|
+
});
|
|
185
|
+
socket.addEventListener("close", () => {
|
|
186
|
+
if (attempt === this.#attempt) this.#down(attempt, new RealtimeError("unavailable", "closed"));
|
|
187
|
+
});
|
|
188
|
+
socket.addEventListener("error", () => {
|
|
189
|
+
if (attempt === this.#attempt) this.#down(attempt, new RealtimeError("unavailable", "error"));
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
socket.accept();
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.#down(attempt, error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
#tryClose(socket) {
|
|
198
|
+
try {
|
|
199
|
+
socket.close(1e3, "done");
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Closes a socket from wherever this runs; when the runtime refuses, the request that opened it does it. */
|
|
206
|
+
#closeSocket(socket, owner) {
|
|
207
|
+
if (socket === void 0) return;
|
|
208
|
+
if (!this.#tryClose(socket)) owner?.resolve();
|
|
209
|
+
}
|
|
210
|
+
/** The socket failed or closed: schedule a new attempt, or end the link when nobody listens. */
|
|
211
|
+
#down(attempt, error) {
|
|
212
|
+
if (attempt !== this.#attempt || this.#state === "disposed") return;
|
|
213
|
+
this.#attempt += 1;
|
|
214
|
+
this.#cancelConnect?.();
|
|
215
|
+
this.#cancelConnect = void 0;
|
|
216
|
+
this.#cancelKeep?.();
|
|
217
|
+
this.#cancelKeep = void 0;
|
|
218
|
+
const socket = this.#socket;
|
|
219
|
+
const owner = this.#closeOwner;
|
|
220
|
+
this.#socket = void 0;
|
|
221
|
+
this.#closeOwner = void 0;
|
|
222
|
+
this.#closeSocket(socket, owner);
|
|
223
|
+
if (this.#state === "open") this.#ready = deferred();
|
|
224
|
+
if (this.size === 0) {
|
|
225
|
+
this.dispose(error);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.#state = "backoff";
|
|
229
|
+
this.#failures += 1;
|
|
230
|
+
const ceiling = Math.min(this.#host.reconnectMaxMs, this.#host.reconnectMinMs * 2 ** Math.min(this.#failures - 1, 20));
|
|
231
|
+
const delay = Math.floor(this.#host.random() * ceiling);
|
|
232
|
+
this.#retryAt = this.#host.now() + delay;
|
|
233
|
+
this.#cancelRetry?.();
|
|
234
|
+
this.#cancelRetry = this.#host.after(delay, () => this.#connect());
|
|
235
|
+
}
|
|
236
|
+
/** Reconnects now, without waiting: the object asked for it (a long replay is continued with a new socket). */
|
|
237
|
+
#resume(attempt) {
|
|
238
|
+
if (attempt !== this.#attempt) return;
|
|
239
|
+
this.#attempt += 1;
|
|
240
|
+
const socket = this.#socket;
|
|
241
|
+
const owner = this.#closeOwner;
|
|
242
|
+
this.#socket = void 0;
|
|
243
|
+
this.#closeOwner = void 0;
|
|
244
|
+
this.#cancelKeep?.();
|
|
245
|
+
this.#cancelKeep = void 0;
|
|
246
|
+
this.#closeSocket(socket, owner);
|
|
247
|
+
this.#state = "idle";
|
|
248
|
+
this.#connect();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A health check run by every listener's loop (each in its own request) and by the ping timer: whichever request is alive
|
|
252
|
+
* can repair a link whose own request is gone, because a timer or a socket handler of a finished request never runs again.
|
|
253
|
+
*/
|
|
254
|
+
#check() {
|
|
255
|
+
const now = this.#host.now();
|
|
256
|
+
const grace = Math.max(1e3, this.#host.keepAliveMs);
|
|
257
|
+
switch (this.#state) {
|
|
258
|
+
case "backoff":
|
|
259
|
+
if (now > this.#retryAt + grace) this.#connect();
|
|
260
|
+
return;
|
|
261
|
+
case "connecting":
|
|
262
|
+
if (now > this.#connectedAt + this.#host.connectTimeoutMs * 2) this.#connect();
|
|
263
|
+
return;
|
|
264
|
+
case "open": {
|
|
265
|
+
const keep = this.#host.keepAliveMs;
|
|
266
|
+
if (keep > 0 && now - this.#lastReceived > keep * 3) {
|
|
267
|
+
this.#down(this.#attempt, new RealtimeError("unavailable", "the channel object went silent"));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
this.#socket?.send("ping");
|
|
272
|
+
} catch {}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
default: return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
#received(attempt, text) {
|
|
279
|
+
this.#lastReceived = this.#host.now();
|
|
280
|
+
if (text === "pong") return;
|
|
281
|
+
const frame = decodeLinkFrame(text);
|
|
282
|
+
if (frame === void 0) return;
|
|
283
|
+
switch (frame.t) {
|
|
284
|
+
case "ready":
|
|
285
|
+
if (this.#known && frame.head < this.#last) {
|
|
286
|
+
this.#last = frame.head;
|
|
287
|
+
for (const sub of this.#messages) sub.last = Math.min(sub.last, frame.head);
|
|
288
|
+
}
|
|
289
|
+
if (!this.#known) {
|
|
290
|
+
this.#last = frame.head;
|
|
291
|
+
this.#known = true;
|
|
292
|
+
}
|
|
293
|
+
for (const sub of this.#messages) if (sub.last < 0) sub.last = frame.head;
|
|
294
|
+
this.#state = "open";
|
|
295
|
+
this.#failures = 0;
|
|
296
|
+
this.#cancelConnect?.();
|
|
297
|
+
this.#cancelConnect = void 0;
|
|
298
|
+
this.#lastReceived = this.#host.now();
|
|
299
|
+
if (this.#host.keepAliveMs > 0) {
|
|
300
|
+
this.#cancelKeep?.();
|
|
301
|
+
this.#cancelKeep = this.#host.every(this.#host.keepAliveMs, () => this.#check());
|
|
302
|
+
}
|
|
303
|
+
this.#ready.resolve();
|
|
304
|
+
return;
|
|
305
|
+
case "snap": {
|
|
306
|
+
const next = /* @__PURE__ */ new Map();
|
|
307
|
+
for (const member of frame.members) next.set(member.id, member);
|
|
308
|
+
const before = this.#members;
|
|
309
|
+
this.#members = next;
|
|
310
|
+
if (before === void 0) return;
|
|
311
|
+
for (const [id, member] of before) if (!next.has(id)) this.#fanPresence({
|
|
312
|
+
channel: this.channel,
|
|
313
|
+
op: "leave",
|
|
314
|
+
member
|
|
315
|
+
});
|
|
316
|
+
for (const [id, member] of next) {
|
|
317
|
+
const old = before.get(id);
|
|
318
|
+
if (old === void 0) this.#fanPresence({
|
|
319
|
+
channel: this.channel,
|
|
320
|
+
op: "join",
|
|
321
|
+
member
|
|
322
|
+
});
|
|
323
|
+
else if (member.v > old.v) this.#fanPresence({
|
|
324
|
+
channel: this.channel,
|
|
325
|
+
op: "set",
|
|
326
|
+
member
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
case "msg": {
|
|
332
|
+
const message = frame.m;
|
|
333
|
+
if (message.seq > this.#last) this.#last = message.seq;
|
|
334
|
+
for (const sub of this.#messages) if (message.seq > sub.last) {
|
|
335
|
+
sub.last = message.seq;
|
|
336
|
+
sub.pump.push(message);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
case "pres": {
|
|
341
|
+
const members = this.#members ?? /* @__PURE__ */ new Map();
|
|
342
|
+
this.#members = members;
|
|
343
|
+
const known = members.get(frame.member.id);
|
|
344
|
+
if (frame.op === "leave") {
|
|
345
|
+
if (known === void 0) return;
|
|
346
|
+
members.delete(frame.member.id);
|
|
347
|
+
} else {
|
|
348
|
+
if (known !== void 0 && known.v >= frame.member.v && frame.op === "join") return;
|
|
349
|
+
if (known !== void 0 && known.v > frame.member.v) return;
|
|
350
|
+
members.set(frame.member.id, frame.member);
|
|
351
|
+
}
|
|
352
|
+
this.#fanPresence({
|
|
353
|
+
channel: this.channel,
|
|
354
|
+
op: frame.op,
|
|
355
|
+
member: frame.member
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
case "more":
|
|
360
|
+
this.#resume(attempt);
|
|
361
|
+
return;
|
|
362
|
+
case "bye":
|
|
363
|
+
this.#down(attempt, new RealtimeError("unavailable", frame.why));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
#fanPresence(event) {
|
|
368
|
+
for (const sub of this.#presence) sub.pump.push(event);
|
|
369
|
+
}
|
|
370
|
+
/** Ends the link: stops every listener, cancels timers and closes the socket. */
|
|
371
|
+
dispose(reason) {
|
|
372
|
+
if (this.#state === "disposed") return;
|
|
373
|
+
this.#state = "disposed";
|
|
374
|
+
this.#attempt += 1;
|
|
375
|
+
this.#cancelRetry?.();
|
|
376
|
+
this.#cancelConnect?.();
|
|
377
|
+
this.#cancelKeep?.();
|
|
378
|
+
this.#cancelRetry = void 0;
|
|
379
|
+
this.#cancelConnect = void 0;
|
|
380
|
+
this.#cancelKeep = void 0;
|
|
381
|
+
const socket = this.#socket;
|
|
382
|
+
const owner = this.#closeOwner;
|
|
383
|
+
this.#socket = void 0;
|
|
384
|
+
this.#closeOwner = void 0;
|
|
385
|
+
this.#closeSocket(socket, owner);
|
|
386
|
+
this.#ready.reject(reason);
|
|
387
|
+
for (const sub of this.#messages) sub.pump.stop();
|
|
388
|
+
for (const sub of this.#presence) sub.pump.stop();
|
|
389
|
+
this.#messages.clear();
|
|
390
|
+
this.#presence.clear();
|
|
391
|
+
this.#host.forget(this);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
//#endregion
|
|
395
|
+
export { ChannelLink };
|
|
396
|
+
|
|
397
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","names":[],"sources":["../../../src/cloudflare/link.ts"],"sourcesContent":["/**\n * The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept\n * open with backoff, resumed with `after`, and feeding each listener through its own queue.\n *\n * A Worker may use an I/O object (a socket, a stream, a timer) only inside the request that created it. A listener runs in the\n * request that subscribed, so it is called by a loop that was started there and woken by a plain promise: the socket's\n * handler only moves data into queues and never calls a listener itself.\n * @packageDocumentation\n */\nimport { RealtimeError } from \"../errors\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { ClientSocket } from \"./types\";\nimport { decodeLinkFrame } from \"./wire\";\n\n/** What a link needs from the adapter that owns it. @internal */\nexport interface LinkHost {\n readonly reconnectMinMs: number;\n readonly reconnectMaxMs: number;\n /** Ping period; 0 turns pings off. */\n readonly keepAliveMs: number;\n readonly connectTimeoutMs: number;\n /** The most events queued for one listener. */\n readonly maxQueued: number;\n now(): number;\n random(): number;\n /** Opens the subscribe socket (not yet accepted). */\n open(channel: string, after: number | undefined): Promise<ClientSocket>;\n /** Runs `task` once after `ms`; returns the way to cancel. Everything scheduled is canceled when the adapter closes. */\n after(ms: number, task: () => void): () => void;\n /** Runs `task` every `ms`; returns the way to cancel. */\n every(ms: number, task: () => void): () => void;\n report(error: unknown, where: string, channel: string): void;\n /** Called once when the link has nothing left to do. */\n forget(link: ChannelLink): void;\n}\n\ninterface Deferred {\n readonly promise: Promise<void>;\n resolve(): void;\n reject(error: unknown): void;\n}\n\nfunction deferred(): Deferred {\n let resolve: () => void = () => undefined;\n let reject: (error: unknown) => void = () => undefined;\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve;\n reject = onReject;\n });\n // A waiter may give up first; the rejection must not become an unhandled one.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A queue and the loop that empties it, in the request that created it. */\nclass Pump<T> {\n readonly #queue: T[] = [];\n readonly #max: number;\n #wake: (() => void) | undefined;\n #stopped = false;\n\n constructor(max: number) {\n this.#max = max;\n }\n\n push(item: T): void {\n if (this.#stopped || this.#queue.length >= this.#max) return;\n this.#queue.push(item);\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n stop(): void {\n this.#stopped = true;\n this.#queue.length = 0;\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n async run(\n deliver: (item: T) => void,\n report: (error: unknown) => void,\n every: (() => () => void) | undefined,\n ): Promise<void> {\n const cancel = every?.();\n try {\n while (!this.#stopped) {\n const item = this.#queue.shift();\n if (item === undefined) {\n await new Promise<void>((resolve) => {\n this.#wake = resolve;\n });\n continue;\n }\n try {\n deliver(item);\n } catch (error) {\n report(error);\n }\n }\n } finally {\n cancel?.();\n }\n }\n}\n\ninterface MessageSub {\n readonly listener: (message: StoredMessage) => void;\n readonly pump: Pump<StoredMessage>;\n /** The highest seq queued for this listener; -1 until the link tells the head. */\n last: number;\n}\n\ninterface PresenceSub {\n readonly listener: (event: PresenceEvent) => void;\n readonly pump: Pump<PresenceEvent>;\n}\n\n/** A way to stop one listener. @internal */\nexport type LinkSubscription = MessageSub | PresenceSub;\n\ntype LinkState = \"idle\" | \"connecting\" | \"open\" | \"backoff\" | \"disposed\";\n\n/** @internal */\nexport class ChannelLink {\n readonly channel: string;\n readonly #host: LinkHost;\n readonly #messages = new Set<MessageSub>();\n readonly #presence = new Set<PresenceSub>();\n #state: LinkState = \"idle\";\n #socket: ClientSocket | undefined;\n #closeOwner: Deferred | undefined;\n #attempt = 0;\n /** The highest seq received on this link: where a reconnect resumes. */\n #last = 0;\n #known = false;\n #members: Map<string, Member> | undefined;\n #lastReceived = 0;\n #failures = 0;\n #retryAt = 0;\n #connectedAt = 0;\n #ready: Deferred = deferred();\n #cancelRetry: (() => void) | undefined;\n #cancelConnect: (() => void) | undefined;\n #cancelKeep: (() => void) | undefined;\n\n constructor(channel: string, host: LinkHost) {\n this.channel = channel;\n this.#host = host;\n }\n\n get state(): LinkState {\n return this.#state;\n }\n\n get size(): number {\n return this.#messages.size + this.#presence.size;\n }\n\n // --- listeners -----------------------------------------------------------------------------------------------------\n\n /** Adds a message listener (call it in the request that will receive messages). */\n addMessages(listener: (message: StoredMessage) => void): LinkSubscription {\n const sub: MessageSub = {\n listener,\n pump: new Pump(this.#host.maxQueued),\n last: this.#state === \"open\" ? this.#last : -1,\n };\n this.#messages.add(sub);\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n );\n this.#start();\n return sub;\n }\n\n /** Adds a presence listener (call it in the request that will receive events). */\n addPresence(listener: (event: PresenceEvent) => void): LinkSubscription {\n const sub: PresenceSub = { listener, pump: new Pump(this.#host.maxQueued) };\n this.#presence.add(sub);\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n );\n this.#start();\n return sub;\n }\n\n /** Removes a listener; the last one closes the link. */\n remove(sub: LinkSubscription): void {\n sub.pump.stop();\n this.#messages.delete(sub as MessageSub);\n this.#presence.delete(sub as PresenceSub);\n if (this.size === 0) this.dispose(new RealtimeError(\"closed\", \"the subscription ended\"));\n }\n\n /** Resolves when the socket is open and has told the channel's head; rejects after `ms`. */\n whenReady(ms: number): Promise<void> {\n if (this.#state === \"open\") return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const cancel = this.#host.after(ms, () =>\n reject(new RealtimeError(\"unavailable\", \"timed out connecting to the channel object\")),\n );\n this.#ready.promise.then(\n () => {\n cancel();\n resolve();\n },\n (error: unknown) => {\n cancel();\n reject(error);\n },\n );\n });\n }\n\n // --- connecting ----------------------------------------------------------------------------------------------------\n\n /** The pump's periodic check, if pings are on. */\n #nudger(): (() => () => void) | undefined {\n const every = this.#host.keepAliveMs;\n if (every <= 0) return undefined;\n return () => this.#host.every(every, () => this.#check());\n }\n\n #start(): void {\n if (this.#state === \"idle\") this.#connect();\n }\n\n #connect(): void {\n if (this.#state === \"disposed\") return;\n this.#cancelRetry?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect?.();\n this.#state = \"connecting\";\n const attempt = ++this.#attempt;\n this.#connectedAt = this.#host.now();\n this.#cancelConnect = this.#host.after(this.#host.connectTimeoutMs, () => {\n if (attempt === this.#attempt && this.#state === \"connecting\")\n this.#down(attempt, new RealtimeError(\"unavailable\", \"the channel object did not answer\"));\n });\n this.#host.open(this.channel, this.#known ? this.#last : undefined).then(\n (socket) => this.#opened(attempt, socket),\n (error: unknown) => {\n if (attempt === this.#attempt) this.#down(attempt, error);\n },\n );\n }\n\n #opened(attempt: number, socket: ClientSocket): void {\n if (attempt !== this.#attempt || this.#state !== \"connecting\") {\n this.#closeSocket(socket, undefined);\n return;\n }\n const owner = deferred();\n this.#closeOwner = owner;\n this.#socket = socket;\n // Waits in the request that opened the socket, which is the one place guaranteed to be allowed to close it.\n void owner.promise.then(\n () => this.#tryClose(socket),\n () => undefined,\n );\n socket.addEventListener(\"message\", (event) => {\n if (attempt !== this.#attempt || typeof event.data !== \"string\") return;\n this.#received(attempt, event.data);\n });\n socket.addEventListener(\"close\", () => {\n if (attempt === this.#attempt)\n this.#down(attempt, new RealtimeError(\"unavailable\", \"closed\"));\n });\n socket.addEventListener(\"error\", () => {\n if (attempt === this.#attempt) this.#down(attempt, new RealtimeError(\"unavailable\", \"error\"));\n });\n try {\n socket.accept();\n } catch (error) {\n this.#down(attempt, error);\n }\n }\n\n #tryClose(socket: ClientSocket): boolean {\n try {\n socket.close(1000, \"done\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Closes a socket from wherever this runs; when the runtime refuses, the request that opened it does it. */\n #closeSocket(socket: ClientSocket | undefined, owner: Deferred | undefined): void {\n if (socket === undefined) return;\n if (!this.#tryClose(socket)) owner?.resolve();\n }\n\n /** The socket failed or closed: schedule a new attempt, or end the link when nobody listens. */\n #down(attempt: number, error: unknown): void {\n if (attempt !== this.#attempt || this.#state === \"disposed\") return;\n this.#attempt += 1;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n // Waiters keep the promise they hold until the link is open; only an opened link needs a new one.\n if (this.#state === \"open\") this.#ready = deferred();\n if (this.size === 0) {\n this.dispose(error);\n return;\n }\n this.#state = \"backoff\";\n this.#failures += 1;\n const ceiling = Math.min(\n this.#host.reconnectMaxMs,\n this.#host.reconnectMinMs * 2 ** Math.min(this.#failures - 1, 20),\n );\n const delay = Math.floor(this.#host.random() * ceiling);\n this.#retryAt = this.#host.now() + delay;\n this.#cancelRetry?.();\n this.#cancelRetry = this.#host.after(delay, () => this.#connect());\n }\n\n /** Reconnects now, without waiting: the object asked for it (a long replay is continued with a new socket). */\n #resume(attempt: number): void {\n if (attempt !== this.#attempt) return;\n this.#attempt += 1;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n this.#closeSocket(socket, owner);\n this.#state = \"idle\";\n this.#connect();\n }\n\n /**\n * A health check run by every listener's loop (each in its own request) and by the ping timer: whichever request is alive\n * can repair a link whose own request is gone, because a timer or a socket handler of a finished request never runs again.\n */\n #check(): void {\n const now = this.#host.now();\n const grace = Math.max(1000, this.#host.keepAliveMs);\n switch (this.#state) {\n case \"backoff\":\n if (now > this.#retryAt + grace) this.#connect();\n return;\n case \"connecting\":\n if (now > this.#connectedAt + this.#host.connectTimeoutMs * 2) this.#connect();\n return;\n case \"open\": {\n const keep = this.#host.keepAliveMs;\n if (keep > 0 && now - this.#lastReceived > keep * 3) {\n this.#down(\n this.#attempt,\n new RealtimeError(\"unavailable\", \"the channel object went silent\"),\n );\n return;\n }\n try {\n this.#socket?.send(\"ping\");\n } catch {\n // Sending from another request is refused; the owner's own ping covers it.\n }\n return;\n }\n default:\n return;\n }\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n #received(attempt: number, text: string): void {\n this.#lastReceived = this.#host.now();\n if (text === \"pong\") return;\n const frame = decodeLinkFrame(text);\n if (frame === undefined) return;\n switch (frame.t) {\n case \"ready\": {\n if (this.#known && frame.head < this.#last) {\n // The channel's numbering restarted (it was forgotten): follow it.\n this.#last = frame.head;\n for (const sub of this.#messages) sub.last = Math.min(sub.last, frame.head);\n }\n if (!this.#known) {\n this.#last = frame.head;\n this.#known = true;\n }\n for (const sub of this.#messages) if (sub.last < 0) sub.last = frame.head;\n this.#state = \"open\";\n this.#failures = 0;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#lastReceived = this.#host.now();\n if (this.#host.keepAliveMs > 0) {\n this.#cancelKeep?.();\n this.#cancelKeep = this.#host.every(this.#host.keepAliveMs, () => this.#check());\n }\n this.#ready.resolve();\n return;\n }\n case \"snap\": {\n const next = new Map<string, Member>();\n for (const member of frame.members) next.set(member.id, member);\n const before = this.#members;\n this.#members = next;\n if (before === undefined) return;\n for (const [id, member] of before)\n if (!next.has(id)) this.#fanPresence({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const old = before.get(id);\n if (old === undefined) this.#fanPresence({ channel: this.channel, op: \"join\", member });\n else if (member.v > old.v)\n this.#fanPresence({ channel: this.channel, op: \"set\", member });\n }\n return;\n }\n case \"msg\": {\n const message = frame.m;\n if (message.seq > this.#last) this.#last = message.seq;\n for (const sub of this.#messages)\n if (message.seq > sub.last) {\n sub.last = message.seq;\n sub.pump.push(message);\n }\n return;\n }\n case \"pres\": {\n const members = this.#members ?? new Map<string, Member>();\n this.#members = members;\n const known = members.get(frame.member.id);\n if (frame.op === \"leave\") {\n if (known === undefined) return;\n members.delete(frame.member.id);\n } else {\n if (known !== undefined && known.v >= frame.member.v && frame.op === \"join\") return;\n if (known !== undefined && known.v > frame.member.v) return;\n members.set(frame.member.id, frame.member);\n }\n this.#fanPresence({ channel: this.channel, op: frame.op, member: frame.member });\n return;\n }\n case \"more\":\n this.#resume(attempt);\n return;\n case \"bye\":\n this.#down(attempt, new RealtimeError(\"unavailable\", frame.why));\n return;\n }\n }\n\n #fanPresence(event: PresenceEvent): void {\n for (const sub of this.#presence) sub.pump.push(event);\n }\n\n // --- ending --------------------------------------------------------------------------------------------------------\n\n /** Ends the link: stops every listener, cancels timers and closes the socket. */\n dispose(reason: unknown): void {\n if (this.#state === \"disposed\") return;\n this.#state = \"disposed\";\n this.#attempt += 1;\n this.#cancelRetry?.();\n this.#cancelConnect?.();\n this.#cancelKeep?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect = undefined;\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n this.#ready.reject(reason);\n for (const sub of this.#messages) sub.pump.stop();\n for (const sub of this.#presence) sub.pump.stop();\n this.#messages.clear();\n this.#presence.clear();\n this.#host.forget(this);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ChannelObjectState, ChannelSocket } from "./types.js";
|
|
2
|
+
//#region src/cloudflare/object.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The pieces of the platform the object creates: a WebSocket pair, the upgrade response and the auto-response pair. The default
|
|
5
|
+
* reads the Workers globals; the unit tests pass their own.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface ObjectPlatform {
|
|
9
|
+
createPair(): {
|
|
10
|
+
readonly client: unknown;
|
|
11
|
+
readonly server: ChannelSocket;
|
|
12
|
+
};
|
|
13
|
+
upgrade(client: unknown): Response;
|
|
14
|
+
autoResponse(): object | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The Durable Object that owns one channel: the single writer of its sequence numbers, its retained history, its idempotency
|
|
18
|
+
* keys and its presence set, and the fan-out to the Workers that subscribed. Re-export it from your Worker and bind it as a
|
|
19
|
+
* SQLite-backed Durable Object; `cloudflareAdapter` talks to it.
|
|
20
|
+
*
|
|
21
|
+
* It keeps everything in Durable Object storage, so it can be evicted and restart at any time, and it uses the WebSocket
|
|
22
|
+
* Hibernation API, so a channel nobody publishes to costs nothing while its subscribers stay connected. An alarm reaps expired
|
|
23
|
+
* presence members and expired retention.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // worker.ts
|
|
27
|
+
* export { RealtimeChannelObject } from "@shadow-js/realtime/cloudflare";
|
|
28
|
+
*
|
|
29
|
+
* // wrangler.toml
|
|
30
|
+
* // [[durable_objects.bindings]]
|
|
31
|
+
* // name = "CHANNELS"
|
|
32
|
+
* // class_name = "RealtimeChannelObject"
|
|
33
|
+
* // [[migrations]]
|
|
34
|
+
* // tag = "v1"
|
|
35
|
+
* // new_sqlite_classes = ["RealtimeChannelObject"]
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
*/
|
|
38
|
+
export declare class RealtimeChannelObject {
|
|
39
|
+
#private;
|
|
40
|
+
/**
|
|
41
|
+
* @param state The Durable Object state the runtime passes.
|
|
42
|
+
* @param env The Worker's bindings (unused).
|
|
43
|
+
* @param platform The platform seams (the runtime passes none).
|
|
44
|
+
*/
|
|
45
|
+
constructor(state: ChannelObjectState, env?: unknown, platform?: ObjectPlatform);
|
|
46
|
+
/**
|
|
47
|
+
* Handles a request from `cloudflareAdapter`: `POST /v1/<operation>` with a JSON body, or `GET /v1/ws` to subscribe.
|
|
48
|
+
*
|
|
49
|
+
* @param request The request.
|
|
50
|
+
* @returns The response.
|
|
51
|
+
*/
|
|
52
|
+
fetch(request: Request): Promise<Response>;
|
|
53
|
+
/**
|
|
54
|
+
* Runtime hook: a message on a subscribe socket. The only message is the keep-alive `ping`, which the runtime normally answers
|
|
55
|
+
* itself without waking the object.
|
|
56
|
+
*
|
|
57
|
+
* @param socket The socket.
|
|
58
|
+
* @param message The message.
|
|
59
|
+
*/
|
|
60
|
+
webSocketMessage(socket: ChannelSocket, message: string | ArrayBuffer): void;
|
|
61
|
+
/**
|
|
62
|
+
* Runtime hook: a subscribe socket closed.
|
|
63
|
+
*
|
|
64
|
+
* @param socket The socket.
|
|
65
|
+
* @param code The close code the peer sent.
|
|
66
|
+
* @param reason The reason the peer sent.
|
|
67
|
+
*/
|
|
68
|
+
webSocketClose(socket: ChannelSocket, code: number, reason: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Runtime hook: a subscribe socket failed.
|
|
71
|
+
*
|
|
72
|
+
* @param socket The socket.
|
|
73
|
+
*/
|
|
74
|
+
webSocketError(socket: ChannelSocket): void;
|
|
75
|
+
/**
|
|
76
|
+
* Runtime hook: the alarm. Reaps expired presence members (one `leave` each), expired idempotency keys and expired retained
|
|
77
|
+
* messages, and sets the next alarm.
|
|
78
|
+
*/
|
|
79
|
+
alarm(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//# sourceMappingURL=object.d.ts.map
|