@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":"redis.js","names":["positive","value","name","isRecord","text","reply","integer","list","isNoScript","error","inBatches","from","items","work","redisAdapter","options","client","prefix","channelTtlMs","graceMs","onError","closed","ownedSubscriber","report","context","fail","RealtimeError","open","channel","keysOf","base","key","run","tail","keys","args","script","parseMessage","source","seq","\"s\"","at","\"a\"","event","\"e\"","data","\"d\"","publisher","\"p\"","user","\"u\"","memberOf","id","\"id\"","version","\"v\"","parseStoredMember","member","\"m\"","parsePresence","op","\"o\"","history","after","count","limit","HISTORY","messages","row","message","members","PRESENCE_LIST","out","subscriber","create","topics","createTopics","KEEPALIVE","entry","publish","historyTtl","idempotencyTtl","body","PUBLISH","listener","json","PRESENCE_SET","ttlMs","PRESENCE_REMOVE","ttl","byChannel","ref","entries","ids","jobs","missing","PRESENCE_TOUCH","removed","PRESENCE_REAP","owned","fromIoredis","fromNodeRedis"],"sources":["../../src/redis.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport { RealtimeError } from \"./errors\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\nimport type { RedisClient, RedisSubscriber } from \"./redis/client\";\nimport {\n HISTORY,\n KEEPALIVE,\n PRESENCE_LIST,\n PRESENCE_REAP,\n PRESENCE_REMOVE,\n PRESENCE_SET,\n PRESENCE_TOUCH,\n PUBLISH,\n type Script,\n} from \"./redis/scripts\";\nimport { createTopics, type RedisErrorContext } from \"./redis/topics\";\n\nexport {\n fromIoredis,\n fromNodeRedis,\n type IoredisLike,\n type IoredisSubscriberLike,\n type NodeRedisLike,\n type NodeRedisSubscriberLike,\n type RedisClient,\n type RedisSubscriber,\n} from \"./redis/client\";\nexport type { RedisErrorContext } from \"./redis/topics\";\n\n/**\n * Options of {@link redisAdapter}.\n * @since 1.0.0\n */\nexport interface RedisAdapterOptions {\n /** The connection that runs commands (see {@link fromIoredis} and {@link fromNodeRedis}). The adapter never closes it. */\n readonly client: RedisClient;\n /**\n * A connection in subscriber mode, shared by every subscription of this adapter. Without it the adapter asks the client for one\n * (`client.createSubscriber`) when the first subscription is made, and closes that one with the adapter. A subscriber you pass\n * is yours to close; give it to one adapter only (a topic has one listener on a subscriber).\n */\n readonly subscriber?: RedisSubscriber;\n /**\n * Prefix of every key and topic. Servers that share channels use the same one; apps (or environments) that must not share\n * use different ones. No braces or whitespace. Default `\"rt\"`.\n */\n readonly prefix?: string;\n /**\n * Milliseconds a channel's numbering outlives its last publish (and its last listener, which keeps it alive). After an idle\n * channel expired, its seq restarts at 1, which a client that resumes sees as `reset`. Default 24 hours.\n */\n readonly channelTtlMs?: number;\n /**\n * Milliseconds the presence keys of a channel outlive the last refresh of any member, so a channel nobody reaps anymore does\n * not stay in Redis forever. Default 60 seconds.\n */\n readonly presenceGraceMs?: number;\n /**\n * Receives every failure the adapter handled without surfacing it: a malformed stored entry that was skipped, a listener that\n * threw, a subscriber connection error, a recovery that gave up. Default: ignored.\n */\n readonly onError?: (error: unknown, context: RedisErrorContext) => void;\n}\n\nconst DAY_MS = 86_400_000;\nconst MAX_LIMIT = 10_000;\nconst MAX_KEY_BYTES = 1024;\nconst REAP_PAGE = 500;\nconst TOUCH_PAGE = 500;\nconst PIPELINE = 64;\n\nfunction positive(name: string, value: number): number {\n if (!Number.isFinite(value) || value < 1)\n throw new RangeError(`${name} must be a positive number of milliseconds.`);\n return Math.ceil(value);\n}\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction text(reply: unknown): string {\n if (typeof reply === \"string\") return reply;\n if (reply instanceof Uint8Array) return new TextDecoder().decode(reply);\n if (typeof reply === \"number\") return String(reply);\n throw new TypeError(\"Redis answered with an unexpected reply.\");\n}\n\nfunction integer(reply: unknown): number {\n const value = typeof reply === \"number\" ? reply : Number(text(reply));\n if (!Number.isSafeInteger(value))\n throw new TypeError(\"Redis answered with an unexpected number.\");\n return value;\n}\n\nfunction list(reply: unknown): readonly unknown[] {\n if (!Array.isArray(reply)) throw new TypeError(\"Redis answered with an unexpected reply.\");\n return reply;\n}\n\nfunction isNoScript(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(\"NOSCRIPT\");\n}\n\n/** Walks pages of `size` items and runs `work` on up to `PIPELINE` of them at once. */\nasync function inBatches<T>(items: readonly T[], work: (item: T) => Promise<void>): Promise<void> {\n for (let from = 0; from < items.length; from += PIPELINE)\n await Promise.all(items.slice(from, from + PIPELINE).map(work));\n}\n\n/**\n * The adapter that makes several servers one: messages, history, idempotency and presence live in Redis (6.2 or later, or\n * Valkey), and fan-out between servers goes through pub/sub. A hub created with it behaves exactly like one over the in-memory\n * adapter, on as many servers as you run.\n *\n * What is stored, per channel `c` (`p` is the prefix; the braces make every key of one channel hash to one Redis Cluster slot):\n * `p:{c}:m` (hash: head seq, history lifetime), `p:{c}:l` and `p:{c}:t` (sorted sets: the retained messages by seq, and when\n * each was accepted), `p:{c}:k:<key>` (idempotency keys), `p:{c}:p` and `p:{c}:x` (presence members, and when each expires),\n * and the topics `p:{c}:c` (messages) and `p:{c}:e` (presence events). A publish is one Lua script: it checks the key,\n * numbers, stores, trims by count and age, and publishes, atomically, so seq order is delivery order. Every method rejects with\n * `RealtimeError(\"unavailable\")` when Redis cannot be reached, and leaves no half-applied state.\n *\n * @param options The connections and the key prefix.\n * @returns The adapter. Close it with the hub (`createHub` closes an adapter it created; close one you passed yourself).\n * @throws RangeError for an option that is not valid; TypeError when no subscriber is given and the client cannot create one.\n * @example\n * import { Redis } from \"ioredis\";\n * import { createHub } from \"@shadow-js/realtime/server\";\n * import { fromIoredis, redisAdapter } from \"@shadow-js/realtime/redis\";\n *\n * const adapter = redisAdapter({ client: fromIoredis(new Redis(process.env.REDIS_URL ?? \"\")), prefix: \"myapp\" });\n * const hub = createHub({ adapter, channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * await hub.close();\n * await adapter.close();\n * @since 1.0.0\n */\nexport function redisAdapter(options: RedisAdapterOptions): Adapter {\n const { client } = options;\n if (typeof client?.sendCommand !== \"function\")\n throw new TypeError(\n \"redisAdapter needs a client with sendCommand (see fromIoredis, fromNodeRedis).\",\n );\n if (options.subscriber === undefined && client.createSubscriber === undefined)\n throw new TypeError(\n \"redisAdapter needs a subscriber: pass one, or a client that can create one (fromIoredis, fromNodeRedis).\",\n );\n const prefix = options.prefix ?? \"rt\";\n if (!/^[^\\s{}]{1,128}$/.test(prefix))\n throw new RangeError(\"prefix must be 1 to 128 characters without braces or whitespace.\");\n const channelTtlMs = positive(\"channelTtlMs\", options.channelTtlMs ?? DAY_MS);\n const graceMs = positive(\"presenceGraceMs\", options.presenceGraceMs ?? 60_000);\n const onError = options.onError;\n let closed = false;\n let ownedSubscriber: Promise<RedisSubscriber> | undefined;\n\n function report(error: unknown, context: RedisErrorContext): void {\n try {\n onError?.(error, context);\n } catch {\n // The reporter's failure must not become the adapter's.\n }\n }\n\n function fail(error: unknown): RealtimeError {\n if (error instanceof RealtimeError) return error;\n return new RealtimeError(\n \"unavailable\",\n closed ? \"the Redis adapter is closed.\" : \"the Redis adapter could not complete the command.\",\n { cause: error },\n );\n }\n\n function open(): void {\n if (closed) throw new RealtimeError(\"unavailable\", \"the Redis adapter is closed.\");\n }\n\n function name(channel: string): string {\n if (channel.length === 0 || channel.includes(\"{\") || channel.includes(\"}\"))\n throw new RealtimeError(\"bad_channel\", \"a channel name is empty or contains a brace.\");\n return `${prefix}:{${channel}}`;\n }\n\n const keysOf = (channel: string) => {\n const base = name(channel);\n return {\n meta: `${base}:m`,\n log: `${base}:l`,\n time: `${base}:t`,\n idem: (key: string): string => `${base}:k:${key}`,\n members: `${base}:p`,\n expiry: `${base}:x`,\n messages: `${base}:c`,\n events: `${base}:e`,\n };\n };\n\n /** Runs a script by hash and, when the server does not know it (first use, a flush, a failover), by source. */\n async function run(\n script: Script,\n keys: readonly string[],\n args: readonly string[],\n ): Promise<unknown> {\n open();\n const tail = [String(keys.length), ...keys, ...args];\n try {\n try {\n return await client.sendCommand([\"EVALSHA\", script.sha, ...tail]);\n } catch (error) {\n if (!isNoScript(error)) throw error;\n return await client.sendCommand([\"EVAL\", script.source, ...tail]);\n }\n } catch (error) {\n throw fail(error);\n }\n }\n\n // --- parsing what Redis holds --------------------------------------------------------------------------------------\n\n function parseMessage(source: string, channel: string): StoredMessage | undefined {\n try {\n const value: unknown = JSON.parse(source);\n if (isRecord(value)) {\n const seq = value[\"s\"];\n const at = value[\"a\"];\n const event = value[\"e\"];\n const data = value[\"d\"];\n const publisher = value[\"p\"];\n const user = value[\"u\"];\n if (\n typeof seq === \"number\" &&\n Number.isSafeInteger(seq) &&\n seq >= 1 &&\n typeof at === \"number\" &&\n typeof event === \"string\" &&\n typeof data === \"string\" &&\n (publisher === undefined || typeof publisher === \"string\") &&\n (user === undefined || typeof user === \"string\")\n )\n return {\n seq,\n at,\n event,\n data,\n ...(publisher === undefined ? {} : { publisher }),\n ...(user === undefined ? {} : { user }),\n };\n }\n } catch (error) {\n report(error, { where: \"parse\", channel });\n return undefined;\n }\n report(new TypeError(\"a stored message has an unexpected shape.\"), { where: \"parse\", channel });\n return undefined;\n }\n\n function memberOf(value: unknown): Member | undefined {\n if (!isRecord(value)) return undefined;\n const id = value[\"id\"];\n const user = value[\"u\"];\n const version = value[\"v\"];\n if (\n typeof id !== \"string\" ||\n typeof version !== \"number\" ||\n !Number.isSafeInteger(version) ||\n !(\"s\" in value) ||\n (user !== undefined && typeof user !== \"string\")\n )\n return undefined;\n return {\n id,\n ...(user === undefined ? {} : { u: user }),\n s: value[\"s\"] as Member[\"s\"],\n v: version,\n };\n }\n\n function parseStoredMember(source: unknown, channel: string): Member | undefined {\n try {\n const value: unknown = JSON.parse(text(source));\n const member = isRecord(value) ? memberOf(value[\"m\"]) : undefined;\n if (member !== undefined) return member;\n } catch (error) {\n report(error, { where: \"parse\", channel });\n return undefined;\n }\n report(new TypeError(\"a stored presence member has an unexpected shape.\"), {\n where: \"parse\",\n channel,\n });\n return undefined;\n }\n\n function parsePresence(source: string, channel: string): PresenceEvent | undefined {\n try {\n const value: unknown = JSON.parse(source);\n if (isRecord(value)) {\n const op = value[\"o\"];\n const member = memberOf(value[\"m\"]);\n if ((op === \"join\" || op === \"set\" || op === \"leave\") && member !== undefined)\n return { channel, op, member };\n }\n } catch (error) {\n report(error, { where: \"parse\", channel });\n return undefined;\n }\n report(new TypeError(\"a presence event has an unexpected shape.\"), { where: \"parse\", channel });\n return undefined;\n }\n\n // --- reads shared with the subscription recovery -------------------------------------------------------------------\n\n async function history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n const keys = keysOf(channel);\n const from = Number.isFinite(after)\n ? Math.min(Math.max(0, Math.floor(after)), Number.MAX_SAFE_INTEGER)\n : 0;\n const count = Number.isFinite(limit) ? Math.min(Math.max(0, Math.floor(limit)), MAX_LIMIT) : 0;\n const reply = list(\n await run(HISTORY, [keys.meta, keys.log, keys.time], [String(from), String(count)]),\n );\n const messages: StoredMessage[] = [];\n try {\n for (const row of reply.slice(2)) {\n const message = parseMessage(text(row), channel);\n if (message !== undefined) messages.push(message);\n }\n return { messages, head: integer(reply[0]), oldest: integer(reply[1]) };\n } catch (error) {\n throw fail(error);\n }\n }\n\n async function members(channel: string): Promise<readonly Member[]> {\n const keys = keysOf(channel);\n const reply = list(await run(PRESENCE_LIST, [keys.members, keys.expiry], []));\n const out: Member[] = [];\n for (const row of reply) {\n const member = parseStoredMember(row, channel);\n if (member !== undefined) out.push(member);\n }\n return out;\n }\n\n // --- the shared subscriber -----------------------------------------------------------------------------------------\n\n function subscriber(): Promise<RedisSubscriber> {\n open();\n if (options.subscriber !== undefined) return Promise.resolve(options.subscriber);\n if (ownedSubscriber === undefined) {\n const create = client.createSubscriber;\n if (create === undefined) return Promise.reject(fail(new TypeError(\"no subscriber.\")));\n ownedSubscriber = (async () => create.call(client))();\n ownedSubscriber.catch(() => {\n ownedSubscriber = undefined;\n });\n }\n return ownedSubscriber.catch((error: unknown) => {\n throw fail(error);\n });\n }\n\n const topics = createTopics({\n subscriber,\n history: async (channel, after, limit) => (await history(channel, after, limit)).messages,\n members,\n keepAlive: async (channel) => {\n await run(KEEPALIVE, [keysOf(channel).meta], [String(channelTtlMs)]);\n },\n parseMessage,\n parsePresence,\n messageTopic: (channel) => keysOf(channel).messages,\n presenceTopic: (channel) => keysOf(channel).events,\n report,\n fail,\n keepAliveMs: Math.max(100, Math.floor(channelTtlMs / 3)),\n });\n\n // --- the adapter ---------------------------------------------------------------------------------------------------\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n const keys = keysOf(channel);\n if (typeof entry.data !== \"string\")\n throw new TypeError(\"a stored entry's data is the JSON text of the value.\");\n const history = Math.max(0, Math.floor(publish.history));\n const historyTtl = positive(\"historyTtlMs\", publish.historyTtlMs);\n const idempotencyTtl = positive(\"idempotencyTtlMs\", publish.idempotencyTtlMs);\n const key = publish.key;\n if (key !== undefined && (key.length === 0 || key.length > MAX_KEY_BYTES))\n throw new RealtimeError(\"bad_frame\", \"an idempotency key is empty or too long.\");\n const body = JSON.stringify({\n e: entry.event,\n d: entry.data,\n ...(entry.publisher === undefined ? {} : { p: entry.publisher }),\n ...(entry.user === undefined ? {} : { u: entry.user }),\n }).slice(1);\n const reply = list(\n await run(\n PUBLISH,\n [keys.meta, keys.log, keys.time, ...(key === undefined ? [] : [keys.idem(key)])],\n [\n String(history),\n String(historyTtl),\n String(idempotencyTtl),\n String(channelTtlMs),\n keys.messages,\n body,\n ],\n ),\n );\n try {\n return { seq: integer(reply[0]), duplicate: integer(reply[1]) === 1 };\n } catch (error) {\n throw fail(error);\n }\n },\n\n subscribe(channel: string, listener: (message: StoredMessage) => void): Promise<Unsubscribe> {\n try {\n open();\n name(channel);\n } catch (error) {\n return Promise.reject(fail(error));\n }\n return topics.messages(channel, listener);\n },\n\n history,\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n const keys = keysOf(channel);\n if (!Number.isSafeInteger(member.v) || member.v < 0)\n throw new RangeError(\"a member version must be a non-negative integer.\");\n const json = JSON.stringify({\n id: member.id,\n ...(member.u === undefined ? {} : { u: member.u }),\n s: member.s === undefined ? null : member.s,\n v: member.v,\n });\n await run(\n PRESENCE_SET,\n [keys.members, keys.expiry],\n [\n member.id,\n String(member.v),\n String(positive(\"ttlMs\", ttlMs)),\n keys.events,\n `{\"v\":${member.v},\"m\":${json}}`,\n json,\n String(graceMs),\n ],\n );\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n const keys = keysOf(channel);\n return (\n integer(await run(PRESENCE_REMOVE, [keys.members, keys.expiry], [id, keys.events])) === 1\n );\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n return members(channel);\n },\n\n async presenceTouch(\n entries: readonly PresenceRef[],\n ttlMs: number,\n ): Promise<readonly PresenceRef[]> {\n const ttl = String(positive(\"ttlMs\", ttlMs));\n const byChannel = new Map<string, string[]>();\n for (const ref of entries) {\n const ids = byChannel.get(ref.channel);\n if (ids === undefined) byChannel.set(ref.channel, [ref.id]);\n else ids.push(ref.id);\n }\n const jobs: { readonly channel: string; readonly ids: readonly string[] }[] = [];\n for (const [channel, ids] of byChannel)\n for (let from = 0; from < ids.length; from += TOUCH_PAGE)\n jobs.push({ channel, ids: ids.slice(from, from + TOUCH_PAGE) });\n const missing: PresenceRef[] = [];\n await inBatches(jobs, async ({ channel, ids }) => {\n const keys = keysOf(channel);\n const reply = list(\n await run(PRESENCE_TOUCH, [keys.members, keys.expiry], [ttl, String(graceMs), ...ids]),\n );\n for (const id of reply) missing.push({ channel, id: text(id) });\n });\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n const keys = keysOf(channel);\n const removed: Member[] = [];\n for (;;) {\n const reply = list(\n await run(PRESENCE_REAP, [keys.members, keys.expiry], [keys.events, String(REAP_PAGE)]),\n );\n for (const row of reply) {\n const member = parseStoredMember(row, channel);\n if (member !== undefined) removed.push(member);\n }\n if (reply.length < REAP_PAGE) return removed;\n }\n },\n\n presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n try {\n open();\n name(channel);\n } catch (error) {\n return Promise.reject(fail(error));\n }\n return topics.presence(channel, listener);\n },\n\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n await topics.close();\n const owned = ownedSubscriber;\n ownedSubscriber = undefined;\n if (owned !== undefined) {\n try {\n await (await owned).close?.();\n } catch (error) {\n report(error, { where: \"close\" });\n }\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{RealtimeError as e}from"../errors.js";import{byteLength as t}from"../protocol.js";import{TokenBucket as n}from"./limits.js";const r=`$realtime`;function i(e,t,n){return new Response(JSON.stringify({error:{code:t,message:n}}),{status:e,headers:{"content-type":`application/json`,"cache-control":`no-store`}})}function a(e,t){let n=e.headers.get(`origin`);if(n===null)return!0;if(t!==void 0)return t.includes(n);try{return n===new URL(e.url).origin}catch{return!1}}async function o(e,t){let n=await e.authenticate?.(t);return n instanceof Response?{refusal:n}:{identity:n??{}}}function s(e,t={}){return{async upgrade(e){let n=await o(t,e);return`refusal`in n?n.refusal:void 0},async open(n,i){let a=await o(t,i);if(`refusal`in a){n.close(1008,`unauthorized`);return}let s={send:e=>n.send(e),close:(e,t)=>n.close(e,t)};n.data[r]=e.connect(s,a.identity)},message(e,t){let n=e.data[r];n!==void 0&&(typeof t==`string`?n.receive(t):e.close(1003,`binary frames are not supported`))},close(e){e.data[r]?.closed()},error(t,n){e.report(n,{where:`websocket`}),t.data[r]?.closed()}}}async function c(e,t){try{await e.send({event:`rt`,data:JSON.stringify({t:`bye`,code:1008,why:t})})}catch{}e.close()}function l(e){let t=e.searchParams.get(`s`);if(t===null)return[];try{let e=JSON.parse(t);return!Array.isArray(e)||e.length>64?void 0:e.map(e=>typeof e==`object`&&e?e:{})}catch{return}}function u(t,n={}){return async(r,i)=>{if(!a(r.request,n.allowedOrigins)){t.report(new e(`forbidden`,`origin refused`),{where:`sse.origin`}),await c(i,`origin not allowed`);return}let s=await o(n,r);if(`refusal`in s){await c(i,`unauthorized`);return}let u=l(new URL(r.request.url));if(u===void 0){await c(i,`bad subscription list`);return}let d=0,f=t.connect({send(e){return!i.closed&&(d+=e.length,i.send({event:`rt`,data:e}).then(()=>{d-=e.length},()=>{d=2**53-1,f.closed()}),!0)},close:()=>i.close(),buffered:()=>d},s.identity);u.forEach((e,t)=>{f.receive(JSON.stringify({t:`sub`,n:t+1,...e}))}),i.signal.aborted||await new Promise(e=>{i.signal.addEventListener(`abort`,()=>e(),{once:!0})}),f.closed(),await f.done}}function d(r,s={}){let c=/* @__PURE__ */ new Map,l=e=>{let t=c.get(e);if(t===void 0){if(c.size>=1e4){let e=c.keys().next().value;e!==void 0&&c.delete(e)}t=new n(r.limits.publishesPerSecond,r.limits.publishBurst,Date.now()),c.set(e,t)}return t};return async n=>{let{request:c}=n;if(!a(c,s.allowedOrigins))return i(403,`forbidden`,`origin not allowed`);let u=await o(s,n);if(`refusal`in u)return u.refusal;let d=await c.text();if(t(d)>r.limits.maxMessageBytes*8)return i(413,`too_large`,`the request is too large`);let f;try{f=JSON.parse(d)}catch{return i(400,`bad_frame`,`the body is not JSON`)}let p=f?.frames,m=f?.cid;if(!Array.isArray(p)||p.length===0||p.length>64)return i(400,`bad_frame`,`frames must hold 1..64 frames`);let h=s.key?.(n,u.identity)??u.identity.user??`anonymous`,g=[],_=r.connect({send(e){return e.startsWith(`{"t":"ready"`)||g.push(e),!0},close:()=>void 0},u.identity,{stateless:!0,publishBucket:l(h),...typeof m==`string`?{actingFor:m}:{}});try{for(let t of p){let n=t?.t;if(n===`pres`){let n=t;try{if(typeof n.c!=`string`||typeof m!=`string`)throw new e(`bad_frame`,`pres needs c and the connection id`);await r.setMemberState(u.identity,n.c,m,n.s??null)}catch(t){let n=t instanceof e?t.code:`unavailable`;g.push(JSON.stringify({t:`err`,code:n}))}}else n!==`pub`&&n!==`req`?g.push(JSON.stringify({t:`err`,code:`bad_frame`,msg:`only pub, req and pres`})):_.receive(JSON.stringify(t))}await _.settled()}finally{_.closed()}return new Response(`{"frames":[${g.join(`,`)}]}`,{status:200,headers:{"content-type":`application/json`,"cache-control":`no-store`}})}}export{d as postRoute,u as sseRoute,s as websocketRoute};
|
|
2
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","names":["KEY","refuse","originAllowed","origin","request","allowed","identify","result","options","event","websocketRoute","ws","transport","frame","code","reason","hub","connection","message","error","refuseStream","stream","declared","text","url","value","item","sseRoute","RealtimeError","entries","pendingBytes","at","entry","resolve","postRoute","buckets","bucketFor","bucket","key","oldest","TokenBucket","byteLength","body","frames","cid","answers","type"],"sources":["../../../src/server/http.ts"],"sourcesContent":["import { RealtimeError } from \"../errors\";\nimport { byteLength, type Json } from \"../protocol\";\nimport type { HubConnection, HubCore, Transport } from \"./hub\";\nimport { TokenBucket } from \"./limits\";\nimport type { Identity } from \"./rules\";\n\n/** What a hub needs from the framework's request event. @since 1.0.0 */\nexport interface RequestLike {\n readonly request: Request;\n}\n\n/** How a connection is identified: an identity, or a response that refuses it. @since 1.0.0 */\nexport type AuthenticateResult = Identity | undefined | Response;\n\n/** Options shared by the transport routes. @since 1.0.0 */\nexport interface HubRouteOptions<E extends RequestLike> {\n /**\n * Reads the identity from the request (the session cookie, a bearer token). Return a `Response` to refuse (a 401). Return\n * nothing for an anonymous connection. Default: everyone is anonymous, and channel rules decide.\n */\n readonly authenticate?: (event: E) => AuthenticateResult | Promise<AuthenticateResult>;\n /**\n * Origins allowed to open a stream or post (full origins such as `https://app.example.com`). Default: the request's own\n * origin only. A request with no `Origin` header (same-origin GET, a non-browser client) is allowed.\n */\n readonly allowedOrigins?: readonly string[];\n}\n\n/** The WebSocket connection the framework hands to handlers (the part this package uses). @since 1.0.0 */\nexport interface WebSocketLike {\n readonly data: Record<string, unknown>;\n readonly signal: AbortSignal;\n send(message: string | Uint8Array): boolean;\n close(code?: number, reason?: string): void;\n}\n\n/** The handlers of a WebSocket route, shaped like Dusk's `WS` export. @since 1.0.0 */\nexport interface WebSocketRoute<E extends RequestLike> {\n upgrade(event: E): Promise<Response | undefined>;\n open(ws: WebSocketLike, event: E): Promise<void>;\n message(ws: WebSocketLike, message: string | Uint8Array, event: E): void;\n close(ws: WebSocketLike): void;\n error(ws: WebSocketLike, error: unknown, event: E): void;\n}\n\n/** The stream a server-sent events route writes to (the part this package uses). @since 1.0.0 */\nexport interface SseLike {\n send(message: { readonly event: string; readonly data: string }): Promise<void>;\n close(): void;\n readonly signal: AbortSignal;\n readonly closed: boolean;\n}\n\nconst KEY = \"$realtime\";\nconst MAX_FRAMES_PER_POST = 64;\nconst MAX_SUBSCRIPTIONS_IN_QUERY = 64;\nconst MAX_BUCKETS = 10_000;\n\nfunction refuse(status: number, code: RealtimeError[\"code\"], message: string): Response {\n return new Response(JSON.stringify({ error: { code, message } }), {\n status,\n headers: { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" },\n });\n}\n\n/** Whether the request's `Origin` header is acceptable. A missing header is allowed (see `HubRouteOptions.allowedOrigins`). */\nfunction originAllowed(request: Request, allowed: readonly string[] | undefined): boolean {\n const origin = request.headers.get(\"origin\");\n if (origin === null) return true;\n if (allowed !== undefined) return allowed.includes(origin);\n try {\n return origin === new URL(request.url).origin;\n } catch {\n return false;\n }\n}\n\nasync function identify<E extends RequestLike>(\n options: HubRouteOptions<E>,\n event: E,\n): Promise<{ readonly identity: Identity } | { readonly refusal: Response }> {\n const result = await options.authenticate?.(event);\n if (result instanceof Response) return { refusal: result };\n return { identity: result ?? {} };\n}\n\n/**\n * The WebSocket route of a hub: `export const WS = hub.websocket({ authenticate })` in a Dusk stream file. The upgrade is decided\n * by `authenticate` (a `Response` refuses it) and Dusk's own checks (origin, caps, size); each text frame goes to the hub.\n * Binary frames close the connection with 1003.\n *\n * @param hub The hub.\n * @param options How to identify the connection.\n * @returns Handlers shaped like Dusk's `WS` export.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const WS = hub.websocket();\n * @since 1.0.0\n */\nexport function websocketRoute<E extends RequestLike>(\n hub: HubCore,\n options: Pick<HubRouteOptions<E>, \"authenticate\"> = {},\n): WebSocketRoute<E> {\n return {\n async upgrade(event) {\n const result = await identify(options, event);\n return \"refusal\" in result ? result.refusal : undefined;\n },\n async open(ws, event) {\n const result = await identify(options, event);\n if (\"refusal\" in result) {\n ws.close(1008, \"unauthorized\");\n return;\n }\n const transport: Transport = {\n send: (frame) => ws.send(frame),\n close: (code, reason) => ws.close(code, reason),\n };\n ws.data[KEY] = hub.connect(transport, result.identity);\n },\n message(ws, message) {\n const connection = ws.data[KEY] as HubConnection | undefined;\n if (connection === undefined) return;\n if (typeof message !== \"string\") {\n ws.close(1003, \"binary frames are not supported\");\n return;\n }\n connection.receive(message);\n },\n close(ws) {\n (ws.data[KEY] as HubConnection | undefined)?.closed();\n },\n error(ws, error) {\n hub.report(error, { where: \"websocket\" });\n (ws.data[KEY] as HubConnection | undefined)?.closed();\n },\n };\n}\n\n/**\n * Refuses a stream that has already started (the status 200 is sent before the handler runs): a `bye` with the policy code the\n * client treats as fatal, so it gives up instead of reconnecting for ever, then the end of the stream.\n */\nasync function refuseStream(stream: SseLike, why: string): Promise<void> {\n try {\n await stream.send({ event: \"rt\", data: JSON.stringify({ t: \"bye\", code: 1008, why }) });\n } catch {\n // A client that already left needs no answer.\n }\n stream.close();\n}\n\n/** Reads the subscriptions a stream request declares: `?s=[{\"c\":\"chat:lobby\",\"after\":12}]`. */\nfunction declared(url: URL): readonly Record<string, unknown>[] | undefined {\n const text = url.searchParams.get(\"s\");\n if (text === null) return [];\n try {\n const value: unknown = JSON.parse(text);\n if (!Array.isArray(value) || value.length > MAX_SUBSCRIPTIONS_IN_QUERY) return undefined;\n return value.map((item) =>\n item !== null && typeof item === \"object\" ? (item as Record<string, unknown>) : {},\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * The server-sent events route of a hub, for hosts that cannot hold a WebSocket: `export const SSE = hub.sse()`. The channels\n * are declared in the request (`?s=[{\"c\":\"chat:lobby\",\"after\":12,\"echo\":false,\"presence\":{\"typing\":false}}]`); to change them the\n * client opens a new stream. Frames go out as events named `rt`; everything the client sends goes to {@link postRoute}.\n *\n * @param hub The hub.\n * @param options How to identify the connection and which origins may connect.\n * @returns A handler shaped like Dusk's `SSE` export.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const SSE = hub.sse();\n * @since 1.0.0\n */\nexport function sseRoute<E extends RequestLike>(\n hub: HubCore,\n options: HubRouteOptions<E> = {},\n): (event: E, stream: SseLike) => Promise<void> {\n return async (event, stream) => {\n if (!originAllowed(event.request, options.allowedOrigins)) {\n hub.report(new RealtimeError(\"forbidden\", \"origin refused\"), { where: \"sse.origin\" });\n await refuseStream(stream, \"origin not allowed\");\n return;\n }\n const result = await identify(options, event);\n if (\"refusal\" in result) {\n await refuseStream(stream, \"unauthorized\");\n return;\n }\n const entries = declared(new URL(event.request.url));\n if (entries === undefined) {\n await refuseStream(stream, \"bad subscription list\");\n return;\n }\n let pendingBytes = 0;\n const transport: Transport = {\n send(frame) {\n if (stream.closed) return false;\n pendingBytes += frame.length;\n stream.send({ event: \"rt\", data: frame }).then(\n () => {\n pendingBytes -= frame.length;\n },\n () => {\n pendingBytes = Number.MAX_SAFE_INTEGER;\n connection.closed();\n },\n );\n return true;\n },\n close: () => stream.close(),\n buffered: () => pendingBytes,\n };\n const connection = hub.connect(transport, result.identity);\n entries.forEach((entry, at) => {\n connection.receive(JSON.stringify({ t: \"sub\", n: at + 1, ...entry }));\n });\n if (!stream.signal.aborted)\n await new Promise<void>((resolve) => {\n stream.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n });\n connection.closed();\n await connection.done;\n };\n}\n\n/**\n * The HTTP route that carries what an SSE client sends: `export const POST = hub.post()`. The body is\n * `{\"cid\": \"<connection id>\", \"frames\": [ ... ]}` (at most 64 frames of the types `pub`, `req`, `pres`); the answer is\n * `{\"frames\": [ ... ]}` with the acks, replies and errors. It keeps nothing between requests, so any server can answer it.\n *\n * The request rate of one caller is limited by `key` (default: the user, else all anonymous callers together) with the hub's\n * publish limits; put Dusk's rate limiting in front of the route for per-address limits.\n *\n * @param hub The hub.\n * @param options Identity, origins and the rate-limit key.\n * @returns A handler for a Dusk API route.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const POST = hub.post();\n * @since 1.0.0\n */\nexport function postRoute<E extends RequestLike>(\n hub: HubCore,\n options: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string } = {},\n): (event: E) => Promise<Response> {\n const buckets = new Map<string, TokenBucket>();\n const bucketFor = (key: string): TokenBucket => {\n let bucket = buckets.get(key);\n if (bucket === undefined) {\n if (buckets.size >= MAX_BUCKETS) {\n const oldest = buckets.keys().next().value;\n if (oldest !== undefined) buckets.delete(oldest);\n }\n bucket = new TokenBucket(hub.limits.publishesPerSecond, hub.limits.publishBurst, Date.now());\n buckets.set(key, bucket);\n }\n return bucket;\n };\n return async (event) => {\n const { request } = event;\n if (!originAllowed(request, options.allowedOrigins))\n return refuse(403, \"forbidden\", \"origin not allowed\");\n const result = await identify(options, event);\n if (\"refusal\" in result) return result.refusal;\n const text = await request.text();\n if (byteLength(text) > hub.limits.maxMessageBytes * 8)\n return refuse(413, \"too_large\", \"the request is too large\");\n let body: unknown;\n try {\n body = JSON.parse(text);\n } catch {\n return refuse(400, \"bad_frame\", \"the body is not JSON\");\n }\n const frames = (body as { frames?: unknown } | null)?.frames;\n const cid = (body as { cid?: unknown } | null)?.cid;\n if (!Array.isArray(frames) || frames.length === 0 || frames.length > MAX_FRAMES_PER_POST)\n return refuse(400, \"bad_frame\", `frames must hold 1..${MAX_FRAMES_PER_POST} frames`);\n const key = options.key?.(event, result.identity) ?? result.identity.user ?? \"anonymous\";\n const answers: string[] = [];\n const transport: Transport = {\n send(frame) {\n if (!frame.startsWith('{\"t\":\"ready\"')) answers.push(frame);\n return true;\n },\n close: () => undefined,\n };\n const connection = hub.connect(transport, result.identity, {\n stateless: true,\n publishBucket: bucketFor(key),\n ...(typeof cid === \"string\" ? { actingFor: cid } : {}),\n });\n try {\n for (const frame of frames) {\n const type = (frame as { t?: unknown } | null)?.t;\n if (type === \"pres\") {\n const item = frame as { c?: unknown; s?: unknown };\n try {\n if (typeof item.c !== \"string\" || typeof cid !== \"string\")\n throw new RealtimeError(\"bad_frame\", \"pres needs c and the connection id\");\n await hub.setMemberState(result.identity, item.c, cid, (item.s ?? null) as Json);\n } catch (error) {\n const code = error instanceof RealtimeError ? error.code : \"unavailable\";\n answers.push(JSON.stringify({ t: \"err\", code }));\n }\n continue;\n }\n if (type !== \"pub\" && type !== \"req\") {\n answers.push(\n JSON.stringify({ t: \"err\", code: \"bad_frame\", msg: \"only pub, req and pres\" }),\n );\n continue;\n }\n connection.receive(JSON.stringify(frame));\n }\n await connection.settled();\n } finally {\n connection.closed();\n }\n return new Response(`{\"frames\":[${answers.join(\",\")}]}`, {\n status: 200,\n headers: { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" },\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{RealtimeError as e}from"../errors.js";import{byteLength as t,decodeClientFrame as n,isChannelName as r,isEventName as i,isIdempotencyKey as a}from"../protocol.js";import{TokenBucket as o,WindowCounter as s,resolveLimits as c}from"./limits.js";import{memoryAdapter as l}from"../memory.js";import{compileRule as u,matchRule as d}from"./rules.js";function f(){let e=/* @__PURE__ */ new Uint8Array(16);crypto.getRandomValues(e);let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function p(e){return JSON.stringify(e)}function m(e,t,n){return`{"t":"msg","c":${JSON.stringify(e)},"s":${t.seq},"e":${JSON.stringify(t.event)},"d":${t.data}`+(t.publisher===void 0?``:`,"p":${JSON.stringify(t.publisher)}`)+(t.user===void 0?``:`,"u":${JSON.stringify(t.user)}`)+(n?`,"r":1}`:`}`)}function h(h={}){let g=c(h.limits),_=h.now??Date.now,v=h.adapter===void 0,y=h.adapter??l({now:_}),b=[],x=/* @__PURE__ */ new Map,S=/* @__PURE__ */ new Map,C,w=!1,T=!1,E={published:0,delivered:0,refused:0,slowConsumers:0},D=(e,t)=>{if(h.onError!==void 0)try{h.onError(e,t)}catch{}else console.error(`[realtime] ${t.where}`,e)};function O(e){for(let t of b){let n=d(t,e);if(n!==void 0)return{rule:t.rule,params:n}}}async function k(t,n,r,i,a){let o=t[n];if(o===void 0||o===!1)return!1;if(o===!0)return!0;try{return await o({channel:r,params:i,identity:a,op:n})===!0}catch(t){throw D(t,{where:`rule.${n}`,channel:r}),new e(`unavailable`,`the channel rule could not be evaluated`)}}function A(e,t){if(!e.closed){try{if((e.transport.buffered?.()??0)>g.maxBufferedBytes){E.slowConsumers+=1,N(e,1013,`slow consumer`);return}if(e.transport.send(t))return}catch(t){D(t,{where:`transport.send`,connection:e.id})}Z(e)}}function j(e,t){A(e,p(t))}function M(e,t,n,r,i){E.refused+=1,j(e,{t:`err`,...t===void 0?{}:{n:t},...r===void 0?{}:{c:r},code:n,...i===void 0?{}:{msg:i}})}function N(e,t,n){if(!e.closed){try{e.transport.send(p({t:`bye`,code:t,why:n}))}catch{}try{e.transport.close(t,n)}catch{}Z(e)}}function P(e){let t=S.get(e);return(t===void 0||t.closing)&&(t={name:e,subs:/* @__PURE__ */ new Set,bucket:new o(g.channelPublishesPerSecond,g.channelPublishesPerSecond,_()),adapterSub:void 0,presenceSub:void 0,closing:!1,frame:void 0},S.set(e,t)),t}async function F(e){e.adapterSub===void 0&&(e.adapterSub=y.subscribe(e.name,t=>L(e,t)),e.adapterSub.catch(()=>{e.adapterSub=void 0})),await e.adapterSub}async function I(e){e.presenceSub===void 0&&(e.presenceSub=y.presenceSubscribe(e.name,t=>B(e,t)),e.presenceSub.catch(()=>{e.presenceSub=void 0})),await e.presenceSub}function L(e,t){for(let n of e.subs)n.active&&(n.pumping?n.buffer.length>=1e4?H(n,`gap`):n.buffer.push(t):t.seq===n.lastSeq+1?R(n,t,!1):t.seq>n.lastSeq+1?(n.buffer.push(t),z(n,void 0)):t.at>n.lastAt&&H(n,`reset`))}function R(e,t,n){if(e.lastSeq=t.seq,e.lastAt=Math.max(e.lastAt,t.at),!e.echo&&t.publisher===e.conn.id)return;if(E.delivered+=1,n){A(e.conn,m(e.channel.name,t,!0));return}let r=e.channel.frame,i=r!==void 0&&r.seq===t.seq?r.text:m(e.channel.name,t,!1);(r===void 0||r.seq!==t.seq)&&(e.channel.frame={seq:t.seq,text:i}),A(e.conn,i)}async function z(e,t){if(!(e.pumping&&t===void 0)){e.pumping=!0;try{for(;e.active;){e.buffer=e.buffer.filter(t=>t.seq>e.lastSeq);let n=e.buffer[0];if(n!==void 0&&n.seq===e.lastSeq+1){R(e,n,!1),e.buffer.shift();continue}let r=n===void 0?t??0:n.seq-1;if(e.lastSeq>=r)break;let i=await y.history(e.channel.name,e.lastSeq,Math.min(500,r-e.lastSeq));if(!e.active)return;let a=i.messages[0];if(a===void 0||a.seq!==e.lastSeq+1){if(!e.gapOk){H(e,`gap`);return}e.lastSeq=(a?.seq??r+1)-1}else for(let t of i.messages){if(t.seq>r)break;R(e,t,!0)}}}catch(t){D(t,{where:`pump`,channel:e.channel.name,connection:e.conn.id}),e.active&&H(e,`unavailable`)}finally{e.pumping=!1}e.active&&e.buffer.length>0&&z(e,void 0)}}function B(e,t){for(let n of e.subs)n.active&&(n.joined||n.watch)&&(n.presenceQueue===void 0?V(n,t):n.presenceQueue.push(t))}function V(e,t){j(e.conn,{t:`pres`,c:t.channel,op:t.op,m:t.member})}function H(e,t){e.active&&(e.active=!1,e.conn.subs.delete(e.channel.name),e.channel.subs.delete(e),t!==void 0&&!e.conn.closed&&j(e.conn,{t:`unsubbed`,c:e.channel.name,...t===``?{}:{why:t}}),W(e))}let U=/* @__PURE__ */ new Set;function W(e){let t=G(e);return U.add(t),t.finally(()=>U.delete(t)),t}async function G(e){let t=e.channel;try{await e.joining?.catch(()=>void 0),e.joined&&(e.joined=!1,await y.presenceRemove(t.name,e.conn.id))}catch(n){D(n,{where:`presenceRemove`,channel:t.name,connection:e.conn.id})}if(t.subs.size>0||t.closing)return;t.closing=!0,S.get(t.name)===t&&S.delete(t.name);let n=[t.adapterSub,t.presenceSub].filter(e=>e!==void 0);await Promise.all(n.map(async e=>{try{await(await e)()}catch(e){D(e,{where:`unsubscribe`,channel:t.name})}}))}function K(e){return e.identity.user===void 0?`unauthorized`:`forbidden`}async function q(n,r){let{c:i,n:a}=r;if(n.subs.has(i))return M(n,a,`conflict`,i,`already subscribed`);if(n.subs.size>=g.maxChannelsPerConnection)return M(n,a,`limit`,i,`too many channels on this connection`);let o=O(i);if(o===void 0||!await k(o.rule,`subscribe`,i,o.params,n.identity))return M(n,a,K(n),i);let s=r.hasPresence||r.watch===!0;if(s&&!await k(o.rule,`presence`,i,o.params,n.identity))return M(n,a,K(n),i);if(r.hasPresence&&t(JSON.stringify(r.presence??null))>g.maxStateBytes)return M(n,a,`too_large`,i,`the presence state is too large`);if(n.closed)return;let c=P(i);if(c.subs.size>=g.maxSubscribersPerChannel)return M(n,a,`limit`,i,`the channel is full`);let l={conn:n,channel:c,rule:o.rule,params:o.params,echo:r.echo!==!1,watch:r.watch===!0,joined:!1,version:0,lastSeq:0,lastAt:_(),pumping:!0,gapOk:!1,buffer:[],presenceQueue:s?[]:void 0,joining:void 0,active:!0};n.subs.set(i,l),c.subs.add(l);try{await F(c),s&&(await I(c),Q());let e=await y.history(i,2**53-1,0);if(!l.active)return;let t=e.head,o=r.after,u=!1,d=!1;if(o===void 0?l.lastSeq=t:o>t?(d=!0,l.lastSeq=t):(l.lastSeq=o,u=e.oldest===0?t>o:e.oldest>o+1,l.gapOk=u),j(n,{t:`subbed`,n:a,c:i,head:t,...u?{gap:!0}:{},...d?{reset:!0}:{}}),r.hasPresence&&(l.joining=J(l,r.presence??null),await l.joining),s&&await ee(l),!l.active)return;await z(l,t),l.gapOk=!1}catch(t){l.active&&(H(l),D(t,{where:`subscribe`,channel:i,connection:n.id}),M(n,a,t instanceof e?t.code:`unavailable`,i))}}async function J(t,n){if((await y.presenceList(t.channel.name)).length>=g.maxPresenceMembers)throw new e(`limit`,`the presence set is full`);t.version=1,t.joined=!0,Q();let r={id:t.conn.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:n,v:1};await y.presenceSet(t.channel.name,r,g.presenceTtlMs)}async function ee(e){let t=await y.presenceList(e.channel.name);if(!e.active)return;j(e.conn,{t:`pres`,c:e.channel.name,op:`sync`,m:t});let n=e.presenceQueue??[];e.presenceQueue=void 0;let r=new Map(t.map(e=>[e.id,e.v]));for(let t of n){let n=r.get(t.member.id);(t.op===`leave`?n===void 0:n!==void 0&&n>=t.member.v)||V(e,t)}}async function te(e,n){let r=e.subs.get(n.c);if(r===void 0||!r.joined)return M(e,void 0,`not_subscribed`,n.c);if(t(JSON.stringify(n.s))>g.maxStateBytes)return M(e,void 0,`too_large`,n.c,`the presence state is too large`);if(!e.publishBucket.take(_()))return Y(e,void 0,n.c);r.version+=1;let i={id:e.id,...e.identity.user===void 0?{}:{u:e.identity.user},s:n.s,v:r.version};try{await y.presenceSet(n.c,i,g.presenceTtlMs)}catch(t){D(t,{where:`presenceSet`,channel:n.c,connection:e.id}),M(e,void 0,`unavailable`,n.c)}}function Y(e,t,n){M(e,t,`rate_limited`,n),e.limited.hit(_())&&N(e,1008,`rate limit exceeded`)}async function ne(e,t){let{c:n,n:r}=t,i=O(n);if(i===void 0||!await k(i.rule,`publish`,n,i.params,e.identity))return M(e,r,K(e),n);if(t.d.length>g.maxMessageBytes)return M(e,r,`too_large`,n,`the message is too large`);if(!e.publishBucket.take(_())||S.get(n)?.bucket.take(_())===!1)return Y(e,r,n);try{let a=await y.publish(n,{event:t.e,data:t.d,publisher:e.id,...e.identity.user===void 0?{}:{user:e.identity.user}},{history:i.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...t.k===void 0?{}:{key:t.k}});E.published+=+!a.duplicate,r!==void 0&&j(e,{t:`ack`,n:r,s:a.seq,...a.duplicate?{dup:!0}:{}})}catch(t){D(t,{where:`publish`,channel:n,connection:e.id}),M(e,r,`unavailable`,n)}}async function re(n,r){let{c:i,n:a}=r,o=n.subs.get(i);if(o===void 0&&n.stateless){let e=O(i);e!==void 0&&await k(e.rule,`subscribe`,i,e.params,n.identity)&&(o=e)}if(o===void 0)return M(n,a,K(n)===`unauthorized`&&n.stateless?`unauthorized`:`not_subscribed`,i);let s=o,c=s.rule.handlers?.[r.e];if(c===void 0)return M(n,a,`no_handler`,i);if(n.pending.size>=g.maxPendingRequests)return M(n,a,`limit`,i,`too many requests in flight`);if(!n.publishBucket.take(_()))return Y(n,a,i);let l=new AbortController;n.pending.add(l);let u=Math.min(r.ms??g.requestTimeoutMs,g.requestTimeoutMs),d=setTimeout(()=>l.abort(new e(`timeout`)),u);(async()=>{try{let e=Promise.resolve(c({channel:i,params:s.params,identity:n.identity,event:r.e,data:r.d,signal:l.signal})),o=new Promise((e,t)=>{l.signal.addEventListener(`abort`,()=>t(l.signal.reason),{once:!0})}),u=await Promise.race([e,o])??null,d=JSON.stringify(u);if(d===void 0||t(d)>g.maxMessageBytes)return M(n,a,`too_large`,i,`the reply is too large`);A(n,`{"t":"rep","n":${a},"d":${d}}`)}catch(t){t instanceof e?M(n,a,t.code,i,t.code===`timeout`||t.code===`closed`?void 0:t.message):(D(t,{where:`request`,channel:i,connection:n.id}),M(n,a,`handler_error`,i))}finally{clearTimeout(d),n.pending.delete(l),X(n)}})()}async function ie(t,r){let i=n(r,g.maxMessageBytes);if(!i.ok){M(t,i.n,i.code,void 0,i.why),t.invalid.hit(_())&&N(t,1008,`too many invalid frames`);return}let a=i.frame;try{switch(a.t){case`hello`:return;case`ping`:j(t,{t:`pong`,...a.n===void 0?{}:{n:a.n}});return;case`sub`:return await q(t,a);case`unsub`:{let e=t.subs.get(a.c);if(e===void 0)return M(t,a.n,`not_subscribed`,a.c);H(e);return}case`pub`:return await ne(t,a);case`req`:return await re(t,a);case`pres`:return await te(t,a)}}catch(n){let r=`n`in a?a.n:void 0,i=`c`in a?a.c:void 0;n instanceof e?M(t,r,n.code,i):(D(n,{where:`frame`,connection:t.id,...i===void 0?{}:{channel:i}}),M(t,r,`unavailable`,i))}}async function ae(t){if(!t.running){t.running=!0;try{for(let n=t.queue.shift();n!==void 0&&!t.closed;n=t.queue.shift())try{await ie(t,n)}catch(n){n instanceof e?M(t,void 0,n.code):(D(n,{where:`frame`,connection:t.id}),M(t,void 0,`unavailable`))}}finally{t.running=!1,X(t)}}}function X(e){if(!(e.running||e.queue.length>0||e.pending.size>0))for(let t of e.waiters.splice(0))t()}async function Z(t){if(t.closed)return t.done;t.closed=!0,x.get(t.id)===t&&x.delete(t.id);for(let n of t.pending)n.abort(new e(`closed`));t.pending.clear(),t.queue.length=0;for(let e of t.waiters.splice(0))e();let n=[...t.subs.values()];for(let e of n)e.active=!1,e.channel.subs.delete(e);t.subs.clear(),await Promise.allSettled(n.map(e=>W(e))),t.release?.()}function Q(){if(C!==void 0||w)return;let e=setInterval(()=>void oe(),g.presenceHeartbeatMs);e.unref?.(),C=e}async function oe(){if(!T){T=!0;try{await se()}finally{T=!1}}}async function se(){let e=[],t=/* @__PURE__ */ new Set;for(let n of x.values())for(let r of n.subs.values())r.joined&&e.push({channel:r.channel.name,id:n.id}),(r.joined||r.watch)&&t.add(r.channel.name);if(e.length===0&&t.size===0)clearInterval(C),C=void 0;else try{let n=await y.presenceTouch(e,g.presenceTtlMs);for(let e of n){let t=x.get(e.id)?.subs.get(e.channel);t!==void 0&&t.joined&&await y.presenceSet(e.channel,{id:e.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:null,v:t.version},g.presenceTtlMs)}for(let e of t)await y.presenceReap(e)}catch(e){D(e,{where:`presenceHeartbeat`})}}for(let[e,t]of Object.entries(h.channels??{}))b.push(u(e,t));let $={limits:g,adapter:y,report:D,match:O,allowed:k,channel(e,t){return b.push(u(e,t)),$},connect(t,n={},r={}){if(w)throw new e(`closed`,`the hub is closed`);let i=()=>void 0,a=new Promise(e=>{i=e}),c=r.actingFor===void 0?void 0:x.get(r.actingFor),l=c!==void 0&&n.user!==void 0&&c.identity.user===n.user,u={id:l?r.actingFor:f(),identity:n,transport:t,subs:/* @__PURE__ */ new Map,queue:[],publishBucket:r.publishBucket??new o(g.publishesPerSecond,g.publishBurst,_()),stateless:r.stateless===!0,waiters:[],invalid:new s(10,6e4),limited:new s(50,1e4),pending:/* @__PURE__ */ new Set,running:!1,closed:!1,release:i,done:a};return l||x.set(u.id,u),j(u,{t:`ready`,v:1,id:u.id,hb:g.heartbeatMs,max:g.maxMessageBytes}),{id:u.id,identity:n,done:a,receive(e){u.closed||(u.queue.length>=g.maxQueuedFrames?N(u,1013,`too many frames waiting`):(u.queue.push(e),ae(u)))},closed(){Z(u)},settled(){return new Promise(e=>{u.waiters.push(e),X(u)})}}},async publish(n,o,s,c={}){if(!r(n))throw new e(`bad_channel`,`"${n}" is not a valid channel name`);if(!i(o))throw new e(`bad_frame`,`"${o}" is not a valid event name`);if(c.key!==void 0&&!a(c.key))throw new e(`bad_frame`,`the idempotency key is not valid`);let l;try{l=JSON.stringify(s===void 0?null:s)}catch(e){throw TypeError(`the data is not JSON`,{cause:e})}if(l===void 0)throw TypeError(`the data is not JSON`);if(t(l)>g.maxMessageBytes)throw new e(`too_large`,`the message is too large`);let u=O(n),d=await y.publish(n,{event:o,data:l},{history:u?.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...c.key===void 0?{}:{key:c.key}});return E.published+=+!d.duplicate,d},async setMemberState(n,r,i,a){if(n.user===void 0)throw new e(`unauthorized`,`presence changes over HTTP need a signed-in user`);let o=O(r);if(o===void 0||!await k(o.rule,`presence`,r,o.params,n))throw new e(`forbidden`);if(t(JSON.stringify(a))>g.maxStateBytes)throw new e(`too_large`,`the presence state is too large`);let s=(await y.presenceList(r)).find(e=>e.id===i);if(s===void 0||s.u!==n.user)throw new e(`forbidden`,`that connection is not yours`);await y.presenceSet(r,{id:s.id,u:s.u,s:a,v:s.v+1},g.presenceTtlMs)},revoke(e){if(e.user===void 0&&e.connection===void 0&&e.channel===void 0)throw TypeError(`revoke needs a user, a connection or a channel.`);let t=0;for(let n of[...x.values()])if((e.connection===void 0||n.id===e.connection)&&(e.user===void 0||n.identity.user===e.user))for(let r of[...n.subs.values()])(e.channel===void 0||r.channel.name===e.channel)&&(H(r,`revoked`),t+=1);return t},stats(){let e=0;for(let t of x.values())e+=t.subs.size;return{connections:x.size,channels:S.size,subscriptions:e,...E}},async close(){if(w)return;w=!0,C!==void 0&&clearInterval(C),C=void 0;let e=[...x.values()];for(let t of e)N(t,1001,`going away`);await Promise.all(e.map(e=>e.done)),await Promise.allSettled([...U]),v&&await y.close()}};return $}export{h as createHubCore};
|
|
2
|
+
//# sourceMappingURL=hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub.js","names":["randomId","bytes","text","byte","encodeFrame","frame","messageFrame","channel","message","replayed","createHubCore","limits","resolveLimits","options","now","ownsAdapter","adapter","memoryAdapter","rules","conns","chans","heartbeat","closing","beating","counters","report","error","context","match","compiled","params","matchRule","allowed","permission","rule","op","RealtimeError","send","conn","drop","closeConnection","sendFrame","fail","n","reason","code","channelState","chan","name","TokenBucket","ensureAdapterSub","onMessage","ensurePresenceSub","onPresence","event","sub","removeSub","deliver","pump","cached","upTo","next","target","page","first","sendPresence","why","releaseSub","releasing","running","release","stops","item","stop","identityDenied","subscribe","found","wantsPresence","byteLength","startHeartbeat","probe","head","after","gap","reset","joinPresence","syncPresence","member","state","members","queued","known","version","setPresence","limit","publishFrame","result","request","handler","controller","timeout","timer","run","aborted","reject","reply","settle","handle","decoded","decodeClientFrame","wake","subs","beat","beatOnce","refs","watched","missing","ref","pattern","compileRule","core","done","resolve","owner","connectOptions","acting","identity","WindowCounter","isChannelName","isEventName","publishOptions","isIdempotencyKey","data","connectionId","count","subscriptions","all"],"sources":["../../../src/server/hub.ts"],"sourcesContent":["import type { Adapter, PublishResult, Unsubscribe } from \"../adapter\";\nimport { RealtimeError } from \"../errors\";\nimport { memoryAdapter } from \"../memory\";\nimport {\n byteLength,\n type ClientFrame,\n decodeClientFrame,\n isChannelName,\n isEventName,\n isIdempotencyKey,\n type Json,\n type Member,\n PROTOCOL_VERSION,\n type PresenceEvent,\n type StoredMessage,\n} from \"../protocol\";\nimport { type HubLimits, resolveLimits, TokenBucket, WindowCounter } from \"./limits\";\nimport {\n type ChannelRule,\n type CompiledRule,\n compileRule,\n type Identity,\n matchRule,\n type RuleContext,\n} from \"./rules\";\n\n/**\n * What a hub needs from a connection's transport. A WebSocket, an SSE stream and an in-memory test client all fit.\n * @since 1.0.0\n */\nexport interface Transport {\n /** Sends one frame (text). False when the transport is closed. */\n send(frame: string): boolean;\n /** Closes the transport with a WebSocket close code and a reason of at most 123 bytes. */\n close(code: number, reason: string): void;\n /** Bytes accepted by `send` that the peer has not yet taken. When present, a connection over the limit is closed. */\n buffered?(): number;\n}\n\n/** Where an error reported to `onError` came from. @since 1.0.0 */\nexport interface ErrorContext {\n readonly where: string;\n readonly channel?: string;\n readonly connection?: string;\n}\n\n/** Options of {@link createHub}. @since 1.0.0 */\nexport interface HubOptions {\n /** Storage and fan-out. Default: a fresh `memoryAdapter()`, closed with the hub. */\n readonly adapter?: Adapter;\n /** Channel rules by pattern, in order. The first matching pattern wins. */\n readonly channels?: Readonly<Record<string, ChannelRule>>;\n /** Limits (see {@link HubLimits}). */\n readonly limits?: Partial<HubLimits>;\n /** Receives failures the hub handled (an adapter outage, a rule predicate that threw). Default: `console.error`. */\n readonly onError?: (error: unknown, context: ErrorContext) => void;\n /** The clock, in epoch milliseconds. Default `Date.now`. */\n readonly now?: () => number;\n}\n\n/** Counters of a running hub. @since 1.0.0 */\nexport interface HubStats {\n readonly connections: number;\n readonly channels: number;\n readonly subscriptions: number;\n readonly published: number;\n readonly delivered: number;\n readonly refused: number;\n readonly slowConsumers: number;\n}\n\n/** One connection, as the transport glue drives it. @since 1.0.0 */\nexport interface HubConnection {\n readonly id: string;\n readonly identity: Identity;\n /** Hands the hub one text frame from the peer. Frames are handled in order. */\n receive(text: string): void;\n /** Tells the hub the transport closed. Safe to call twice. */\n closed(): void;\n /** Resolves once every frame received so far was handled and every request answered. */\n settled(): Promise<void>;\n /** Resolves when the hub finished releasing what the connection held. */\n readonly done: Promise<void>;\n}\n\n/** How a connection differs from a long-lived one. @since 1.0.0 */\nexport interface ConnectOptions {\n /**\n * A connection that exists for one HTTP request: a request is answered for any identity the channel's `subscribe` rule\n * accepts (there is no subscription to look up), and nothing is kept after it closes.\n */\n readonly stateless?: boolean;\n /** Replaces the connection's own publish bucket, so a caller can share one across requests. */\n readonly publishBucket?: TokenBucket;\n /**\n * The id of a long-lived connection of the same user this request acts for (SSE clients post under their stream's id, so\n * `echo:false` and `publisher` work). Honored only when that connection is open on this server and has the same user.\n */\n readonly actingFor?: string;\n}\n\n/** Who to remove subscriptions of. At least one field. @since 1.0.0 */\nexport interface RevokeTarget {\n readonly user?: string;\n readonly connection?: string;\n readonly channel?: string;\n}\n\n/** The server object. @since 1.0.0 */\nexport interface HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: ChannelRule): HubCore;\n /** Starts handling a connection. Sends `ready`. */\n connect(transport: Transport, identity?: Identity, options?: ConnectOptions): HubConnection;\n /** Publishes from the server (no rule applies). */\n publish(\n channel: string,\n event: string,\n data: unknown,\n options?: { readonly key?: string },\n ): Promise<PublishResult>;\n /** Removes matching subscriptions; each subscriber gets `unsubbed why:\"revoked\"`. Returns how many were removed. */\n revoke(target: RevokeTarget): number;\n /** Counters. */\n stats(): HubStats;\n /** Closes every connection with 1001, stops timers, removes presence, settles requests. */\n close(): Promise<void>;\n /** @internal Changes the state of a presence member from outside its connection (SSE clients). */\n setMemberState(\n identity: Identity,\n channel: string,\n connectionId: string,\n state: Json,\n ): Promise<void>;\n /** @internal The resolved limits. */\n readonly limits: HubLimits;\n /** @internal The adapter. */\n readonly adapter: Adapter;\n /** @internal Reports an error. */\n report(error: unknown, context: ErrorContext): void;\n /** @internal Matches a channel against the rules. */\n match(\n channel: string,\n ): { readonly rule: ChannelRule; readonly params: Record<string, string> } | undefined;\n /** @internal Evaluates a rule permission for an identity. */\n allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean>;\n}\n\ninterface SubState {\n readonly conn: ConnState;\n readonly channel: ChanState;\n readonly rule: ChannelRule;\n readonly params: Record<string, string>;\n readonly echo: boolean;\n readonly watch: boolean;\n joined: boolean;\n version: number;\n lastSeq: number;\n /** The `at` of the last message delivered: a message with an old seq but a newer `at` means the numbering restarted. */\n lastAt: number;\n /** While true, live messages wait in `buffer` and `pump` delivers in order. */\n pumping: boolean;\n gapOk: boolean;\n buffer: StoredMessage[];\n presenceQueue: PresenceEvent[] | undefined;\n /** The join in progress, so that releasing the subscription waits for it and never leaves a member behind. */\n joining: Promise<void> | undefined;\n active: boolean;\n}\n\ninterface ChanState {\n readonly name: string;\n readonly subs: Set<SubState>;\n readonly bucket: TokenBucket;\n adapterSub: Promise<Unsubscribe> | undefined;\n presenceSub: Promise<Unsubscribe> | undefined;\n closing: boolean;\n /** Cache of the encoded frame of the message being fanned out. */\n frame: { readonly seq: number; readonly text: string } | undefined;\n}\n\ninterface ConnState {\n readonly id: string;\n readonly identity: Identity;\n readonly transport: Transport;\n readonly subs: Map<string, SubState>;\n readonly queue: string[];\n readonly publishBucket: TokenBucket;\n readonly invalid: WindowCounter;\n readonly limited: WindowCounter;\n readonly pending: Set<AbortController>;\n readonly stateless: boolean;\n readonly waiters: (() => void)[];\n running: boolean;\n closed: boolean;\n release: (() => void) | undefined;\n readonly done: Promise<void>;\n}\n\nconst REPLAY_PAGE = 500;\nconst MAX_PUMP_BUFFER = 10_000;\nconst ID_BYTES = 16;\n\n/** A random id: 128 bits, base64url. */\nfunction randomId(): string {\n const bytes = new Uint8Array(ID_BYTES);\n crypto.getRandomValues(bytes);\n let text = \"\";\n for (const byte of bytes) text += String.fromCharCode(byte);\n return btoa(text).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction encodeFrame(frame: Record<string, unknown>): string {\n return JSON.stringify(frame);\n}\n\nfunction messageFrame(channel: string, message: StoredMessage, replayed: boolean): string {\n return (\n `{\"t\":\"msg\",\"c\":${JSON.stringify(channel)},\"s\":${message.seq},\"e\":${JSON.stringify(message.event)},\"d\":${message.data}` +\n (message.publisher === undefined ? \"\" : `,\"p\":${JSON.stringify(message.publisher)}`) +\n (message.user === undefined ? \"\" : `,\"u\":${JSON.stringify(message.user)}`) +\n (replayed ? ',\"r\":1}' : \"}\")\n );\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules, enforces limits and talks to\n * one adapter. Wire it to a transport with `hub.websocket()`, `hub.sse()` and `hub.post()` (Dusk routes) or with `hub.connect`.\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: { \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined } },\n * });\n * const sent: string[] = [];\n * const connection = hub.connect({ send: (frame) => (sent.push(frame), true), close: () => {} });\n * connection.receive('{\"t\":\"sub\",\"n\":1,\"c\":\"chat:lobby\"}');\n * @since 1.0.0\n */\nexport function createHubCore(options: HubOptions = {}): HubCore {\n const limits = resolveLimits(options.limits);\n const now = options.now ?? Date.now;\n const ownsAdapter = options.adapter === undefined;\n const adapter = options.adapter ?? memoryAdapter({ now });\n const rules: CompiledRule[] = [];\n const conns = new Map<string, ConnState>();\n const chans = new Map<string, ChanState>();\n let heartbeat: ReturnType<typeof setInterval> | undefined;\n let closing = false;\n let beating = false;\n const counters = { published: 0, delivered: 0, refused: 0, slowConsumers: 0 };\n\n const report = (error: unknown, context: ErrorContext): void => {\n if (options.onError !== undefined) {\n try {\n options.onError(error, context);\n } catch {\n // A failing reporter must not break the hub.\n }\n } else {\n console.error(`[realtime] ${context.where}`, error);\n }\n };\n\n function match(\n channel: string,\n ): { rule: ChannelRule; params: Record<string, string> } | undefined {\n for (const compiled of rules) {\n const params = matchRule(compiled, channel);\n if (params !== undefined) return { rule: compiled.rule, params };\n }\n return undefined;\n }\n\n async function allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean> {\n const permission = rule[op];\n if (permission === undefined || permission === false) return false;\n if (permission === true) return true;\n try {\n return (await permission({ channel, params, identity, op })) === true;\n } catch (error) {\n report(error, { where: `rule.${op}`, channel });\n throw new RealtimeError(\"unavailable\", \"the channel rule could not be evaluated\");\n }\n }\n\n function send(conn: ConnState, text: string): void {\n if (conn.closed) return;\n try {\n const buffered = conn.transport.buffered?.() ?? 0;\n if (buffered > limits.maxBufferedBytes) {\n counters.slowConsumers += 1;\n drop(conn, 1013, \"slow consumer\");\n return;\n }\n if (conn.transport.send(text)) return;\n } catch (error) {\n // A transport that throws is a broken socket: this connection goes, the others are not affected (guarantee G13).\n report(error, { where: \"transport.send\", connection: conn.id });\n }\n void closeConnection(conn);\n }\n\n function sendFrame(conn: ConnState, frame: Record<string, unknown>): void {\n send(conn, encodeFrame(frame));\n }\n\n function fail(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n channel?: string,\n message?: string,\n ): void {\n counters.refused += 1;\n sendFrame(conn, {\n t: \"err\",\n ...(n === undefined ? {} : { n }),\n ...(channel === undefined ? {} : { c: channel }),\n code,\n ...(message === undefined ? {} : { msg: message }),\n });\n }\n\n /** Sends a final frame, closes the transport and releases the connection. */\n function drop(conn: ConnState, code: number, reason: string): void {\n if (conn.closed) return;\n try {\n conn.transport.send(encodeFrame({ t: \"bye\", code, why: reason }));\n } catch {\n // The transport may already be gone.\n }\n try {\n conn.transport.close(code, reason);\n } catch {\n // See above.\n }\n void closeConnection(conn);\n }\n\n // --- channels ------------------------------------------------------------------------------------------------------\n\n function channelState(name: string): ChanState {\n let chan = chans.get(name);\n if (chan === undefined || chan.closing) {\n chan = {\n name,\n subs: new Set(),\n bucket: new TokenBucket(\n limits.channelPublishesPerSecond,\n limits.channelPublishesPerSecond,\n now(),\n ),\n adapterSub: undefined,\n presenceSub: undefined,\n closing: false,\n frame: undefined,\n };\n chans.set(name, chan);\n }\n return chan;\n }\n\n async function ensureAdapterSub(chan: ChanState): Promise<void> {\n if (chan.adapterSub === undefined) {\n chan.adapterSub = adapter.subscribe(chan.name, (message) => onMessage(chan, message));\n chan.adapterSub.catch(() => {\n chan.adapterSub = undefined;\n });\n }\n await chan.adapterSub;\n }\n\n async function ensurePresenceSub(chan: ChanState): Promise<void> {\n if (chan.presenceSub === undefined) {\n chan.presenceSub = adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event));\n chan.presenceSub.catch(() => {\n chan.presenceSub = undefined;\n });\n }\n await chan.presenceSub;\n }\n\n function onMessage(chan: ChanState, message: StoredMessage): void {\n for (const sub of chan.subs) {\n if (!sub.active) continue;\n if (sub.pumping) {\n // A consumer so far behind that it cannot catch up is resubscribed from its last seq instead of holding memory.\n if (sub.buffer.length >= MAX_PUMP_BUFFER) removeSub(sub, \"gap\");\n else sub.buffer.push(message);\n continue;\n }\n if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);\n else if (message.seq > sub.lastSeq + 1) {\n sub.buffer.push(message);\n void pump(sub, undefined);\n } else if (message.at > sub.lastAt) removeSub(sub, \"reset\");\n // Any other seq at or below the cursor was already delivered: dropped (guarantee G2).\n }\n }\n\n function deliver(sub: SubState, message: StoredMessage, replayed: boolean): void {\n sub.lastSeq = message.seq;\n sub.lastAt = Math.max(sub.lastAt, message.at);\n if (!sub.echo && message.publisher === sub.conn.id) return;\n counters.delivered += 1;\n if (replayed) {\n send(sub.conn, messageFrame(sub.channel.name, message, true));\n return;\n }\n const cached = sub.channel.frame;\n const text =\n cached !== undefined && cached.seq === message.seq\n ? cached.text\n : messageFrame(sub.channel.name, message, false);\n if (cached === undefined || cached.seq !== message.seq)\n sub.channel.frame = { seq: message.seq, text };\n send(sub.conn, text);\n }\n\n /**\n * Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the\n * missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one\n * is removed with `unsubbed why:\"gap\"`.\n */\n async function pump(sub: SubState, upTo: number | undefined): Promise<void> {\n if (sub.pumping && upTo === undefined) return;\n sub.pumping = true;\n try {\n while (sub.active) {\n sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);\n const next = sub.buffer[0];\n if (next !== undefined && next.seq === sub.lastSeq + 1) {\n deliver(sub, next, false);\n sub.buffer.shift();\n continue;\n }\n const target = next === undefined ? (upTo ?? 0) : next.seq - 1;\n if (sub.lastSeq >= target) break;\n const page = await adapter.history(\n sub.channel.name,\n sub.lastSeq,\n Math.min(REPLAY_PAGE, target - sub.lastSeq),\n );\n if (!sub.active) return;\n const first = page.messages[0];\n if (first === undefined || first.seq !== sub.lastSeq + 1) {\n if (!sub.gapOk) {\n removeSub(sub, \"gap\");\n return;\n }\n // The gap was reported at subscribe time: continue from what is retained.\n sub.lastSeq = (first?.seq ?? target + 1) - 1;\n continue;\n }\n for (const message of page.messages) {\n if (message.seq > target) break;\n deliver(sub, message, true);\n }\n }\n } catch (error) {\n report(error, { where: \"pump\", channel: sub.channel.name, connection: sub.conn.id });\n if (sub.active) removeSub(sub, \"unavailable\");\n } finally {\n sub.pumping = false;\n }\n // A message that arrived while the last history page was awaited is in the buffer; one more pass delivers it.\n if (sub.active && sub.buffer.length > 0) void pump(sub, undefined);\n }\n\n function onPresence(chan: ChanState, event: PresenceEvent): void {\n for (const sub of chan.subs) {\n if (!sub.active || (!sub.joined && !sub.watch)) continue;\n if (sub.presenceQueue !== undefined) sub.presenceQueue.push(event);\n else sendPresence(sub, event);\n }\n }\n\n function sendPresence(sub: SubState, event: PresenceEvent): void {\n sendFrame(sub.conn, { t: \"pres\", c: event.channel, op: event.op, m: event.member });\n }\n\n /** Removes a subscription from its connection and channel; tells the peer when `why` is given. */\n function removeSub(sub: SubState, why?: string): void {\n if (!sub.active) return;\n sub.active = false;\n sub.conn.subs.delete(sub.channel.name);\n sub.channel.subs.delete(sub);\n if (why !== undefined && !sub.conn.closed)\n sendFrame(sub.conn, { t: \"unsubbed\", c: sub.channel.name, ...(why === \"\" ? {} : { why }) });\n void releaseSub(sub);\n }\n\n /** The releases still running, so that `close` finishes them before it closes the adapter. */\n const releasing = new Set<Promise<void>>();\n\n function releaseSub(sub: SubState): Promise<void> {\n const running = release(sub);\n releasing.add(running);\n void running.finally(() => releasing.delete(running));\n return running;\n }\n\n async function release(sub: SubState): Promise<void> {\n const chan = sub.channel;\n try {\n // A join that is still being written must finish first, or its member would outlive the connection.\n await sub.joining?.catch(() => undefined);\n if (sub.joined) {\n sub.joined = false;\n await adapter.presenceRemove(chan.name, sub.conn.id);\n }\n } catch (error) {\n report(error, { where: \"presenceRemove\", channel: chan.name, connection: sub.conn.id });\n }\n if (chan.subs.size > 0 || chan.closing) return;\n chan.closing = true;\n if (chans.get(chan.name) === chan) chans.delete(chan.name);\n const stops = [chan.adapterSub, chan.presenceSub].filter(\n (item): item is Promise<Unsubscribe> => item !== undefined,\n );\n await Promise.all(\n stops.map(async (stop) => {\n try {\n await (await stop)();\n } catch (error) {\n report(error, { where: \"unsubscribe\", channel: chan.name });\n }\n }),\n );\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n function identityDenied(conn: ConnState): RealtimeError[\"code\"] {\n return conn.identity.user === undefined ? \"unauthorized\" : \"forbidden\";\n }\n\n async function subscribe(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"sub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n if (conn.subs.has(name)) return fail(conn, n, \"conflict\", name, \"already subscribed\");\n if (conn.subs.size >= limits.maxChannelsPerConnection)\n return fail(conn, n, \"limit\", name, \"too many channels on this connection\");\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n const wantsPresence = frame.hasPresence || frame.watch === true;\n if (\n wantsPresence &&\n !(await allowed(found.rule, \"presence\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (\n frame.hasPresence &&\n byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes\n )\n return fail(conn, n, \"too_large\", name, \"the presence state is too large\");\n if (conn.closed) return;\n const chan = channelState(name);\n if (chan.subs.size >= limits.maxSubscribersPerChannel)\n return fail(conn, n, \"limit\", name, \"the channel is full\");\n const sub: SubState = {\n conn,\n channel: chan,\n rule: found.rule,\n params: found.params,\n echo: frame.echo !== false,\n watch: frame.watch === true,\n joined: false,\n version: 0,\n lastSeq: 0,\n lastAt: now(),\n pumping: true,\n gapOk: false,\n buffer: [],\n presenceQueue: wantsPresence ? [] : undefined,\n joining: undefined,\n active: true,\n };\n conn.subs.set(name, sub);\n chan.subs.add(sub);\n try {\n await ensureAdapterSub(chan);\n if (wantsPresence) {\n await ensurePresenceSub(chan);\n startHeartbeat();\n }\n const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);\n if (!sub.active) return;\n const head = probe.head;\n const after = frame.after;\n let gap = false;\n let reset = false;\n if (after === undefined) {\n sub.lastSeq = head;\n } else if (after > head) {\n reset = true;\n sub.lastSeq = head;\n } else {\n sub.lastSeq = after;\n gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;\n sub.gapOk = gap;\n }\n sendFrame(conn, {\n t: \"subbed\",\n n,\n c: name,\n head,\n ...(gap ? { gap: true } : {}),\n ...(reset ? { reset: true } : {}),\n });\n if (frame.hasPresence) {\n sub.joining = joinPresence(sub, frame.presence ?? null);\n await sub.joining;\n }\n if (wantsPresence) await syncPresence(sub);\n if (!sub.active) return;\n await pump(sub, head);\n sub.gapOk = false;\n } catch (error) {\n if (sub.active) {\n removeSub(sub);\n report(error, { where: \"subscribe\", channel: name, connection: conn.id });\n fail(conn, n, error instanceof RealtimeError ? error.code : \"unavailable\", name);\n }\n }\n }\n\n async function joinPresence(sub: SubState, state: Json): Promise<void> {\n const existing = await adapter.presenceList(sub.channel.name);\n if (existing.length >= limits.maxPresenceMembers)\n throw new RealtimeError(\"limit\", \"the presence set is full\");\n sub.version = 1;\n sub.joined = true;\n startHeartbeat();\n const member: Member = {\n id: sub.conn.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: state,\n v: 1,\n };\n await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);\n }\n\n async function syncPresence(sub: SubState): Promise<void> {\n const members = await adapter.presenceList(sub.channel.name);\n if (!sub.active) return;\n sendFrame(sub.conn, { t: \"pres\", c: sub.channel.name, op: \"sync\", m: members });\n const queued = sub.presenceQueue ?? [];\n sub.presenceQueue = undefined;\n const known = new Map(members.map((member) => [member.id, member.v]));\n for (const event of queued) {\n // What the snapshot already shows is not announced again.\n const version = known.get(event.member.id);\n if (\n event.op === \"leave\"\n ? version === undefined\n : version !== undefined && version >= event.member.v\n )\n continue;\n sendPresence(sub, event);\n }\n }\n\n async function setPresence(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pres\" }>,\n ): Promise<void> {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined || !sub.joined) return fail(conn, undefined, \"not_subscribed\", frame.c);\n if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes)\n return fail(conn, undefined, \"too_large\", frame.c, \"the presence state is too large\");\n if (!conn.publishBucket.take(now())) return limit(conn, undefined, frame.c);\n sub.version += 1;\n const member: Member = {\n id: conn.id,\n ...(conn.identity.user === undefined ? {} : { u: conn.identity.user }),\n s: frame.s,\n v: sub.version,\n };\n try {\n await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);\n } catch (error) {\n report(error, { where: \"presenceSet\", channel: frame.c, connection: conn.id });\n fail(conn, undefined, \"unavailable\", frame.c);\n }\n }\n\n function limit(conn: ConnState, n: number | undefined, channel: string): void {\n fail(conn, n, \"rate_limited\", channel);\n if (conn.limited.hit(now())) drop(conn, 1008, \"rate limit exceeded\");\n }\n\n async function publishFrame(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"publish\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (frame.d.length > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, \"the message is too large\");\n // A channel with no subscriber on this server has no bucket: only the connection's own rate applies to it.\n if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false)\n return limit(conn, n, name);\n try {\n const result = await adapter.publish(\n name,\n {\n event: frame.e,\n data: frame.d,\n publisher: conn.id,\n ...(conn.identity.user === undefined ? {} : { user: conn.identity.user }),\n },\n {\n history: found.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(frame.k === undefined ? {} : { key: frame.k }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n if (n !== undefined)\n sendFrame(conn, { t: \"ack\", n, s: result.seq, ...(result.duplicate ? { dup: true } : {}) });\n } catch (error) {\n report(error, { where: \"publish\", channel: name, connection: conn.id });\n fail(conn, n, \"unavailable\", name);\n }\n }\n\n async function request(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"req\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n let target: { rule: ChannelRule; params: Record<string, string> } | undefined =\n conn.subs.get(name);\n if (target === undefined && conn.stateless) {\n const found = match(name);\n if (\n found !== undefined &&\n (await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n target = found;\n }\n if (target === undefined)\n return fail(\n conn,\n n,\n identityDenied(conn) === \"unauthorized\" && conn.stateless\n ? \"unauthorized\"\n : \"not_subscribed\",\n name,\n );\n const sub = target;\n const handler = sub.rule.handlers?.[frame.e];\n if (handler === undefined) return fail(conn, n, \"no_handler\", name);\n if (conn.pending.size >= limits.maxPendingRequests)\n return fail(conn, n, \"limit\", name, \"too many requests in flight\");\n if (!conn.publishBucket.take(now())) return limit(conn, n, name);\n const controller = new AbortController();\n conn.pending.add(controller);\n const timeout = Math.min(frame.ms ?? limits.requestTimeoutMs, limits.requestTimeoutMs);\n const timer = setTimeout(() => controller.abort(new RealtimeError(\"timeout\")), timeout);\n void (async () => {\n try {\n const run = Promise.resolve(\n handler({\n channel: name,\n params: sub.params,\n identity: conn.identity,\n event: frame.e,\n data: frame.d,\n signal: controller.signal,\n }),\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n controller.signal.addEventListener(\"abort\", () => reject(controller.signal.reason), {\n once: true,\n });\n });\n const reply = (await Promise.race([run, aborted])) ?? null;\n const text = JSON.stringify(reply);\n if (text === undefined || byteLength(text) > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, \"the reply is too large\");\n send(conn, `{\"t\":\"rep\",\"n\":${n},\"d\":${text}}`);\n } catch (error) {\n if (error instanceof RealtimeError)\n fail(\n conn,\n n,\n error.code,\n name,\n error.code === \"timeout\" || error.code === \"closed\" ? undefined : error.message,\n );\n else {\n report(error, { where: \"request\", channel: name, connection: conn.id });\n fail(conn, n, \"handler_error\", name);\n }\n } finally {\n clearTimeout(timer);\n conn.pending.delete(controller);\n settle(conn);\n }\n })();\n }\n\n async function handle(conn: ConnState, text: string): Promise<void> {\n const decoded = decodeClientFrame(text, limits.maxMessageBytes);\n if (!decoded.ok) {\n fail(conn, decoded.n, decoded.code, undefined, decoded.why);\n if (conn.invalid.hit(now())) drop(conn, 1008, \"too many invalid frames\");\n return;\n }\n const frame = decoded.frame;\n try {\n switch (frame.t) {\n case \"hello\":\n return;\n case \"ping\":\n sendFrame(conn, { t: \"pong\", ...(frame.n === undefined ? {} : { n: frame.n }) });\n return;\n case \"sub\":\n return await subscribe(conn, frame);\n case \"unsub\": {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined) return fail(conn, frame.n, \"not_subscribed\", frame.c);\n removeSub(sub);\n return;\n }\n case \"pub\":\n return await publishFrame(conn, frame);\n case \"req\":\n return await request(conn, frame);\n case \"pres\":\n return await setPresence(conn, frame);\n }\n } catch (error) {\n const n = \"n\" in frame ? frame.n : undefined;\n const channel = \"c\" in frame ? frame.c : undefined;\n if (error instanceof RealtimeError) fail(conn, n, error.code, channel);\n else {\n report(error, {\n where: \"frame\",\n connection: conn.id,\n ...(channel === undefined ? {} : { channel }),\n });\n fail(conn, n, \"unavailable\", channel);\n }\n }\n }\n\n async function run(conn: ConnState): Promise<void> {\n if (conn.running) return;\n conn.running = true;\n try {\n for (let text = conn.queue.shift(); text !== undefined; text = conn.queue.shift()) {\n if (conn.closed) break;\n try {\n await handle(conn, text);\n } catch (error) {\n if (error instanceof RealtimeError) fail(conn, undefined, error.code);\n else {\n report(error, { where: \"frame\", connection: conn.id });\n fail(conn, undefined, \"unavailable\");\n }\n }\n }\n } finally {\n conn.running = false;\n settle(conn);\n }\n }\n\n /** Wakes the callers of `settled` when the connection has nothing left to do. */\n function settle(conn: ConnState): void {\n if (conn.running || conn.queue.length > 0 || conn.pending.size > 0) return;\n for (const wake of conn.waiters.splice(0)) wake();\n }\n\n async function closeConnection(conn: ConnState): Promise<void> {\n if (conn.closed) return conn.done;\n conn.closed = true;\n if (conns.get(conn.id) === conn) conns.delete(conn.id);\n for (const controller of conn.pending) controller.abort(new RealtimeError(\"closed\"));\n conn.pending.clear();\n conn.queue.length = 0;\n for (const wake of conn.waiters.splice(0)) wake();\n const subs = [...conn.subs.values()];\n for (const sub of subs) {\n sub.active = false;\n sub.channel.subs.delete(sub);\n }\n conn.subs.clear();\n await Promise.allSettled(subs.map((sub) => releaseSub(sub)));\n conn.release?.();\n }\n\n // --- presence heartbeat --------------------------------------------------------------------------------------------\n\n function startHeartbeat(): void {\n if (heartbeat !== undefined || closing) return;\n const timer = setInterval(() => void beat(), limits.presenceHeartbeatMs);\n // Node's timer can be unref'd so an idle hub does not keep the process alive; other runtimes return a number.\n (timer as unknown as { unref?: () => void }).unref?.();\n heartbeat = timer;\n }\n\n async function beat(): Promise<void> {\n if (beating) return;\n beating = true;\n try {\n await beatOnce();\n } finally {\n beating = false;\n }\n }\n\n async function beatOnce(): Promise<void> {\n const refs: { channel: string; id: string }[] = [];\n const watched = new Set<string>();\n for (const conn of conns.values())\n for (const sub of conn.subs.values()) {\n if (sub.joined) refs.push({ channel: sub.channel.name, id: conn.id });\n if (sub.joined || sub.watch) watched.add(sub.channel.name);\n }\n if (refs.length === 0 && watched.size === 0) {\n clearInterval(heartbeat);\n heartbeat = undefined;\n return;\n }\n try {\n const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);\n for (const ref of missing) {\n const sub = conns.get(ref.id)?.subs.get(ref.channel);\n if (sub === undefined || !sub.joined) continue;\n await adapter.presenceSet(\n ref.channel,\n {\n id: ref.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: null,\n v: sub.version,\n },\n limits.presenceTtlMs,\n );\n }\n for (const channel of watched) await adapter.presenceReap(channel);\n } catch (error) {\n report(error, { where: \"presenceHeartbeat\" });\n }\n }\n\n // --- the hub -------------------------------------------------------------------------------------------------------\n\n for (const [pattern, rule] of Object.entries(options.channels ?? {}))\n rules.push(compileRule(pattern, rule));\n\n const core: HubCore = {\n limits,\n adapter,\n report,\n match,\n allowed,\n\n channel(pattern, rule) {\n rules.push(compileRule(pattern, rule));\n return core;\n },\n\n connect(transport, identity = {}, connectOptions = {}) {\n if (closing) throw new RealtimeError(\"closed\", \"the hub is closed\");\n let release: () => void = () => undefined;\n const done = new Promise<void>((resolve) => {\n release = resolve;\n });\n const owner =\n connectOptions.actingFor === undefined ? undefined : conns.get(connectOptions.actingFor);\n const acting =\n owner !== undefined && identity.user !== undefined && owner.identity.user === identity.user;\n const conn: ConnState = {\n id: acting ? (connectOptions.actingFor as string) : randomId(),\n identity,\n transport,\n subs: new Map(),\n queue: [],\n publishBucket:\n connectOptions.publishBucket ??\n new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),\n stateless: connectOptions.stateless === true,\n waiters: [],\n invalid: new WindowCounter(10, 60_000),\n limited: new WindowCounter(50, 10_000),\n pending: new Set(),\n running: false,\n closed: false,\n release,\n done,\n };\n // An acting connection shares its owner's id: it must not replace the owner in the table.\n if (!acting) conns.set(conn.id, conn);\n sendFrame(conn, {\n t: \"ready\",\n v: PROTOCOL_VERSION,\n id: conn.id,\n hb: limits.heartbeatMs,\n max: limits.maxMessageBytes,\n });\n return {\n id: conn.id,\n identity,\n done,\n receive(text) {\n if (conn.closed) return;\n if (conn.queue.length >= limits.maxQueuedFrames) {\n drop(conn, 1013, \"too many frames waiting\");\n return;\n }\n conn.queue.push(text);\n void run(conn);\n },\n closed() {\n void closeConnection(conn);\n },\n settled() {\n return new Promise<void>((resolve) => {\n conn.waiters.push(resolve);\n settle(conn);\n });\n },\n };\n },\n\n async publish(channel, event, data, publishOptions = {}) {\n if (!isChannelName(channel))\n throw new RealtimeError(\"bad_channel\", `\"${channel}\" is not a valid channel name`);\n if (!isEventName(event))\n throw new RealtimeError(\"bad_frame\", `\"${event}\" is not a valid event name`);\n if (publishOptions.key !== undefined && !isIdempotencyKey(publishOptions.key))\n throw new RealtimeError(\"bad_frame\", \"the idempotency key is not valid\");\n let text: string | undefined;\n try {\n text = JSON.stringify(data === undefined ? null : data);\n } catch (error) {\n throw new TypeError(\"the data is not JSON\", { cause: error });\n }\n if (text === undefined) throw new TypeError(\"the data is not JSON\");\n if (byteLength(text) > limits.maxMessageBytes)\n throw new RealtimeError(\"too_large\", \"the message is too large\");\n const found = match(channel);\n const result = await adapter.publish(\n channel,\n { event, data: text },\n {\n history: found?.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(publishOptions.key === undefined ? {} : { key: publishOptions.key }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n return result;\n },\n\n async setMemberState(identity, channel, connectionId, state) {\n if (identity.user === undefined)\n throw new RealtimeError(\"unauthorized\", \"presence changes over HTTP need a signed-in user\");\n const found = match(channel);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"presence\", channel, found.params, identity))\n )\n throw new RealtimeError(\"forbidden\");\n if (byteLength(JSON.stringify(state)) > limits.maxStateBytes)\n throw new RealtimeError(\"too_large\", \"the presence state is too large\");\n const members = await adapter.presenceList(channel);\n const member = members.find((item) => item.id === connectionId);\n if (member === undefined || member.u !== identity.user)\n throw new RealtimeError(\"forbidden\", \"that connection is not yours\");\n await adapter.presenceSet(\n channel,\n { id: member.id, u: member.u, s: state, v: member.v + 1 },\n limits.presenceTtlMs,\n );\n },\n\n revoke(target) {\n if (\n target.user === undefined &&\n target.connection === undefined &&\n target.channel === undefined\n )\n throw new TypeError(\"revoke needs a user, a connection or a channel.\");\n let count = 0;\n for (const conn of [...conns.values()]) {\n if (target.connection !== undefined && conn.id !== target.connection) continue;\n if (target.user !== undefined && conn.identity.user !== target.user) continue;\n for (const sub of [...conn.subs.values()]) {\n if (target.channel !== undefined && sub.channel.name !== target.channel) continue;\n removeSub(sub, \"revoked\");\n count += 1;\n }\n }\n return count;\n },\n\n stats() {\n let subscriptions = 0;\n for (const conn of conns.values()) subscriptions += conn.subs.size;\n return {\n connections: conns.size,\n channels: chans.size,\n subscriptions,\n ...counters,\n };\n },\n\n async close() {\n if (closing) return;\n closing = true;\n if (heartbeat !== undefined) clearInterval(heartbeat);\n heartbeat = undefined;\n const all = [...conns.values()];\n for (const conn of all) drop(conn, 1001, \"going away\");\n await Promise.all(all.map((conn) => conn.done));\n await Promise.allSettled([...releasing]);\n if (ownsAdapter) await adapter.close();\n },\n };\n return core;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DEFAULT_HUB_LIMITS as e}from"./limits.js";import{postRoute as t,sseRoute as n,websocketRoute as r}from"./http.js";import{memoryAdapter as i}from"../memory.js";import{createHubCore as a}from"./hub.js";function o(e={}){let i=a(e),o=Object.assign(i,{channel(e,t){return i.channel(e,t),o},websocket:e=>r(i,e),sse:e=>n(i,e),post:e=>t(i,e)});return o}export{e as DEFAULT_HUB_LIMITS,o as createHub,i as memoryAdapter};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["createHub","core","createHubCore","options","hub","pattern","rule","websocketRoute","routeOptions","sseRoute","postRoute","DEFAULT_HUB_LIMITS","memoryAdapter"],"sources":["../../../src/server/index.ts"],"sourcesContent":["import {\n type HubRouteOptions,\n postRoute,\n type RequestLike,\n type SseLike,\n sseRoute,\n type WebSocketRoute,\n websocketRoute,\n} from \"./http\";\nimport { createHubCore, type HubCore, type HubOptions } from \"./hub\";\nimport type { Identity } from \"./rules\";\n\nexport type { Adapter } from \"../adapter\";\nexport { memoryAdapter } from \"../memory\";\nexport type {\n AuthenticateResult,\n HubRouteOptions,\n RequestLike,\n SseLike,\n WebSocketLike,\n WebSocketRoute,\n} from \"./http\";\nexport type {\n ConnectOptions,\n ErrorContext,\n HubConnection,\n HubOptions,\n HubStats,\n RevokeTarget,\n Transport,\n} from \"./hub\";\nexport type { HubLimits } from \"./limits\";\nexport { DEFAULT_HUB_LIMITS } from \"./limits\";\nexport type {\n ChannelRule,\n Identity,\n Permission,\n RequestContext,\n RequestHandler,\n RuleContext,\n} from \"./rules\";\n\n/**\n * The server object of `@shadow-js/realtime`: connections, channel rules, limits and an adapter, with the routes that\n * connect it to a transport.\n * @since 1.0.0\n */\nexport interface Hub extends HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub;\n /** The WebSocket route: `export const WS = hub.websocket()`. */\n websocket<E extends RequestLike = RequestLike>(\n options?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E>;\n /** The server-sent events route: `export const SSE = hub.sse()`. */\n sse<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E>,\n ): (event: E, stream: SseLike) => Promise<void>;\n /** The HTTP route for what SSE clients send: `export const POST = hub.post()`. */\n post<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string },\n ): (event: E) => Promise<Response>;\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules (default deny), enforces the\n * limits, and talks to one adapter (in memory by default; Redis or Cloudflare when several servers share channels).\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: {\n * \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },\n * },\n * });\n * await hub.publish(\"chat:lobby\", \"message\", { text: \"hello\" });\n * await hub.close();\n * @since 1.0.0\n */\nexport function createHub(options: HubOptions = {}): Hub {\n const core = createHubCore(options);\n const hub: Hub = Object.assign(core, {\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub {\n core.channel(pattern, rule);\n return hub;\n },\n websocket: <E extends RequestLike = RequestLike>(\n routeOptions?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E> => websocketRoute<E>(core, routeOptions),\n sse: <E extends RequestLike = RequestLike>(routeOptions?: HubRouteOptions<E>) =>\n sseRoute<E>(core, routeOptions),\n post: <E extends RequestLike = RequestLike>(\n routeOptions?: HubRouteOptions<E> & {\n readonly key?: (event: E, identity: Identity) => string;\n },\n ) => postRoute<E>(core, routeOptions),\n });\n return hub;\n}\n"],"mappings":"A,+MAmFA,SAAgBA,EAAU,EAAsB,CAAC,EAAQ,CACvD,IAAMC,EAAOC,EAAcC,CAAO,EAC5BC,EAAW,OAAO,OAAOH,EAAM,CACnC,QAAQ,EAAiB,EAA0C,CAEjE,OADAA,EAAK,QAAQI,EAASC,CAAI,EACnBF,CACT,EACA,UACE,GACsBG,EAAkBN,EAAMO,CAAY,EAC5D,IAA2C,GACzCC,EAAYR,EAAMO,CAAY,EAChC,KACE,GAGGE,EAAaT,EAAMO,CAAY,CACtC,CAAC,EACD,OAAOJ,CACT,C"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=Object.freeze({maxMessageBytes:65536,maxChannelsPerConnection:64,maxSubscribersPerChannel:5e4,maxPresenceMembers:1e3,maxStateBytes:1024,maxPendingRequests:32,maxQueuedFrames:256,maxBufferedBytes:1048576,publishesPerSecond:20,publishBurst:40,channelPublishesPerSecond:500,requestTimeoutMs:1e4,history:100,historyTtlMs:3e5,idempotencyTtlMs:6e4,presenceTtlMs:45e3,presenceHeartbeatMs:15e3,heartbeatMs:3e4}),t=1e4;function n(n){let r={...e};for(let[t,i]of Object.entries(n??{})){if(!(t in e))throw TypeError(`unknown limit "${t}".`);if(i===void 0)continue;let n=t===`history`?0:1;if(typeof i!=`number`||!Number.isInteger(i)||i<n)throw RangeError(`limits.${t} must be ${n===0?`a non-negative`:`a positive`} integer.`);r[t]=i}if(r.history>1e4)throw RangeError(`limits.history must be at most ${t}.`);if(r.presenceTtlMs<=r.presenceHeartbeatMs)throw RangeError(`limits.presenceTtlMs must be larger than limits.presenceHeartbeatMs.`);return r}var r=class{#e;#t;#n;#r;constructor(e,t,n){this.#n=e,this.#r=t,this.#e=t,this.#t=n}take(e){let t=Math.max(0,e-this.#t);return this.#t=e,this.#e=Math.min(this.#r,this.#e+t*this.#n/1e3),this.#e<1?!1:(--this.#e,!0)}},i=class{#e=[];#t;#n;constructor(e,t){this.#t=e,this.#n=t}hit(e){this.#e.push(e);let t=e-this.#n;for(;this.#e.length>0&&this.#e[0]<=t;)this.#e.shift();return this.#e.length>=this.#t}};export{e as DEFAULT_HUB_LIMITS,t as MAX_HISTORY,r as TokenBucket,i as WindowCounter,n as resolveLimits};
|
|
2
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","names":["DEFAULT_HUB_LIMITS","MAX_HISTORY","resolveLimits","out","name","value","given","floor","\"history\"","\"presenceTtlMs\"","\"presenceHeartbeatMs\"","TokenBucket","#rate","rate","#burst","burst","#tokens","#at","now","elapsed","WindowCounter","#limit","limit","#windowMs","windowMs","#times","cutoff"],"sources":["../../../src/server/limits.ts"],"sourcesContent":["/**\n * The limits of a hub. Every one is bounded, so a hub cannot be made to hold unbounded state (see the realtime protocol reference).\n * @since 1.0.0\n */\nexport interface HubLimits {\n /** Largest frame, in UTF-8 bytes, either way. Default 65,536. */\n readonly maxMessageBytes: number;\n /** Channels one connection may subscribe to. Default 64. */\n readonly maxChannelsPerConnection: number;\n /** Subscribers one channel may have on this server. Default 50,000. */\n readonly maxSubscribersPerChannel: number;\n /** Presence members one channel may have, across servers. Default 1,000. */\n readonly maxPresenceMembers: number;\n /** Largest presence state, in bytes of JSON. Default 1,024. */\n readonly maxStateBytes: number;\n /** Requests one connection may have in flight. Default 32. */\n readonly maxPendingRequests: number;\n /** Frames one connection may have waiting to be handled. Default 256. */\n readonly maxQueuedFrames: number;\n /** Bytes the transport may hold unsent before the connection is closed as a slow consumer. Default 1 MiB. */\n readonly maxBufferedBytes: number;\n /** Publishes per second per connection. Default 20. */\n readonly publishesPerSecond: number;\n /** Largest burst of publishes per connection. Default 40. */\n readonly publishBurst: number;\n /** Publishes per second per channel on this server. Default 500. */\n readonly channelPublishesPerSecond: number;\n /** Milliseconds a request may run. Default 10,000. */\n readonly requestTimeoutMs: number;\n /** Messages kept per channel unless a rule says otherwise. Default 100. */\n readonly history: number;\n /** Milliseconds a message is kept. Default 300,000. */\n readonly historyTtlMs: number;\n /** Milliseconds an idempotency key is remembered. Default 60,000. */\n readonly idempotencyTtlMs: number;\n /** Milliseconds a presence member lives without a heartbeat. Default 45,000. */\n readonly presenceTtlMs: number;\n /** Milliseconds between the heartbeats a client is asked to send (`ready.hb`). Default 30,000. */\n readonly heartbeatMs: number;\n /** Milliseconds between presence heartbeats (and reaping). Default 15,000. */\n readonly presenceHeartbeatMs: number;\n}\n\n/** The defaults of {@link HubLimits}. @since 1.0.0 */\nexport const DEFAULT_HUB_LIMITS: HubLimits = Object.freeze({\n maxMessageBytes: 65_536,\n maxChannelsPerConnection: 64,\n maxSubscribersPerChannel: 50_000,\n maxPresenceMembers: 1_000,\n maxStateBytes: 1_024,\n maxPendingRequests: 32,\n maxQueuedFrames: 256,\n maxBufferedBytes: 1_048_576,\n publishesPerSecond: 20,\n publishBurst: 40,\n channelPublishesPerSecond: 500,\n requestTimeoutMs: 10_000,\n history: 100,\n historyTtlMs: 300_000,\n idempotencyTtlMs: 60_000,\n presenceTtlMs: 45_000,\n presenceHeartbeatMs: 15_000,\n heartbeatMs: 30_000,\n});\n\n/** The most messages a channel may retain. */\nexport const MAX_HISTORY = 10_000;\n\n/**\n * Validates and fills in the limits.\n * @param given The limits the app set.\n * @returns The complete limits.\n * @throws RangeError for a limit that is not a positive integer (a history of 0 is allowed).\n */\nexport function resolveLimits(given: Partial<HubLimits> | undefined): HubLimits {\n const out: Record<string, number> = { ...DEFAULT_HUB_LIMITS };\n for (const [name, value] of Object.entries(given ?? {})) {\n if (!(name in DEFAULT_HUB_LIMITS)) throw new TypeError(`unknown limit \"${name}\".`);\n if (value === undefined) continue;\n const floor = name === \"history\" ? 0 : 1;\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < floor)\n throw new RangeError(\n `limits.${name} must be ${floor === 0 ? \"a non-negative\" : \"a positive\"} integer.`,\n );\n out[name] = value;\n }\n if ((out[\"history\"] as number) > MAX_HISTORY)\n throw new RangeError(`limits.history must be at most ${MAX_HISTORY}.`);\n if ((out[\"presenceTtlMs\"] as number) <= (out[\"presenceHeartbeatMs\"] as number))\n throw new RangeError(\"limits.presenceTtlMs must be larger than limits.presenceHeartbeatMs.\");\n return out as unknown as HubLimits;\n}\n\n/** A token bucket: `rate` tokens per second up to `burst`. */\nexport class TokenBucket {\n #tokens: number;\n #at: number;\n readonly #rate: number;\n readonly #burst: number;\n\n constructor(rate: number, burst: number, now: number) {\n this.#rate = rate;\n this.#burst = burst;\n this.#tokens = burst;\n this.#at = now;\n }\n\n /** Takes one token; false when the bucket is empty. */\n take(now: number): boolean {\n const elapsed = Math.max(0, now - this.#at);\n this.#at = now;\n this.#tokens = Math.min(this.#burst, this.#tokens + (elapsed * this.#rate) / 1000);\n if (this.#tokens < 1) return false;\n this.#tokens -= 1;\n return true;\n }\n}\n\n/** Counts events inside a sliding window; `hit` returns true once the count reaches the limit. */\nexport class WindowCounter {\n readonly #times: number[] = [];\n readonly #limit: number;\n readonly #windowMs: number;\n\n constructor(limit: number, windowMs: number) {\n this.#limit = limit;\n this.#windowMs = windowMs;\n }\n\n hit(now: number): boolean {\n this.#times.push(now);\n const cutoff = now - this.#windowMs;\n while (this.#times.length > 0 && (this.#times[0] as number) <= cutoff) this.#times.shift();\n return this.#times.length >= this.#limit;\n }\n}\n"],"mappings":"AA4CA,MAAaA,EAAgC,OAAO,OAAO,CACzD,gBAAiB,MACjB,yBAA0B,GAC1B,yBAA0B,IAC1B,mBAAoB,IACpB,cAAe,KACf,mBAAoB,GACpB,gBAAiB,IACjB,iBAAkB,QAClB,mBAAoB,GACpB,aAAc,GACd,0BAA2B,IAC3B,iBAAkB,IAClB,QAAS,IACT,aAAc,IACd,iBAAkB,IAClB,cAAe,KACf,oBAAqB,KACrB,YAAa,GACf,CAAC,EAGYC,EAAc,IAQ3B,SAAgBC,EAAc,EAAkD,CAC9E,IAAMC,EAA8B,CAAE,GAAGH,CAAmB,EAC5D,IAAK,GAAM,CAACI,EAAMC,KAAU,OAAO,QAAQC,GAAS,CAAC,CAAC,EAAG,CACvD,GAAI,EAAEF,KAAQJ,GAAqB,MAAU,UAAU,kBAAkBI,EAAK,GAAG,EACjF,GAAIC,IAAU,IAAA,GAAW,SACzB,IAAME,EAAQH,IAAS,UAAY,EAAI,EACvC,GAAI,OAAOC,GAAU,UAAY,CAAC,OAAO,UAAUA,CAAK,GAAKA,EAAQE,EACnE,MAAU,WACR,UAAUH,EAAK,WAAWG,IAAU,EAAI,iBAAmB,aAAa,UAC1E,EACF,EAAIH,GAAQC,CACd,CACA,GAAKF,EAAIK,QAAA,IACP,MAAU,WAAW,kCAAkCP,EAAY,EAAE,EACvE,GAAKE,EAAIM,eAAgCN,EAAIO,oBAC3C,MAAU,WAAW,sEAAsE,EAC7F,OAAOP,CACT,CAGA,IAAaQ,EAAb,KAAyB,CACvB,GACA,GACA,GACA,GAEA,YAAY,EAAc,EAAe,EAAa,CACpD,KAAKC,GAAQC,EACb,KAAKC,GAASC,EACd,KAAKC,GAAUD,EACf,KAAKE,GAAMC,CACb,CAGA,KAAK,EAAsB,CACzB,IAAMC,EAAU,KAAK,IAAI,EAAGD,EAAM,KAAKD,EAAG,EAK1C,MAJA,MAAKA,GAAMC,EACX,KAAKF,GAAU,KAAK,IAAI,KAAKF,GAAQ,KAAKE,GAAWG,EAAU,KAAKP,GAAS,GAAI,EAC7E,KAAKI,GAAU,EAAU,IAC7B,OAAKA,GACE,GACT,CACF,EAGaI,EAAb,KAA2B,CACzB,GAA4B,CAAC,EAC7B,GACA,GAEA,YAAY,EAAe,EAAkB,CAC3C,KAAKC,GAASC,EACd,KAAKC,GAAYC,CACnB,CAEA,IAAI,EAAsB,CACxB,KAAKC,GAAO,KAAKP,CAAG,EACpB,IAAMQ,EAASR,EAAM,KAAKK,GAC1B,KAAO,KAAKE,GAAO,OAAS,GAAM,KAAKA,GAAO,IAAiBC,GAAQ,KAAKD,GAAO,MAAM,EACzF,OAAO,KAAKA,GAAO,QAAU,KAAKJ,EACpC,CACF,E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/,t=/^\{\.\.\.([A-Za-z_][A-Za-z0-9_]*)\}$/,n=/^[A-Za-z0-9_.@/-]+$/;function r(r,i){if(typeof r!=`string`||r.length===0||r.length>128)throw TypeError(`the channel pattern "${String(r)}" must be 1..128 characters.`);let a=r.split(`:`),o=/* @__PURE__ */ new Set,s=a.map((i,s)=>{let c=t.exec(i);if(c!==null){if(s!==a.length-1)throw TypeError(`the channel pattern "${r}": {...name} must be the last segment.`);let e=c[1];if(o.has(e))throw TypeError(`the channel pattern "${r}" repeats "${e}".`);return o.add(e),{kind:`rest`,name:e}}let l=e.exec(i);if(l!==null){let e=l[1];if(o.has(e))throw TypeError(`the channel pattern "${r}" repeats "${e}".`);return o.add(e),{kind:`param`,name:e}}if(!n.test(i)||i.startsWith(`$`))throw TypeError(`the channel pattern "${r}" has an invalid segment "${i}".`);return{kind:`literal`,text:i}});if(i.history!==void 0&&(!Number.isInteger(i.history)||i.history<0))throw RangeError(`the rule for "${r}": history must be a non-negative integer.`);return{pattern:r,parts:s,rule:i}}function i(e,t){let n=t.split(`:`);if(n.includes(``))return;let{parts:r}=e;if(r[r.length-1]?.kind===`rest`?n.length<r.length:n.length!==r.length)return;let i={};for(let e=0;e<r.length;e+=1){let t=r[e],a=n[e];if(t.kind===`rest`)return i[t.name]=n.slice(e).join(`:`),i;if(t.kind===`literal`){if(a!==t.text)return}else i[t.name]=a}return i}export{r as compileRule,i as matchRule};
|
|
2
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","names":["PARAM","REST","LITERAL","compileRule","pattern","segments","seen","parts","rest","segment","at","name","param","rule","matchRule","channel","compiled","params","part"],"sources":["../../../src/server/rules.ts"],"sourcesContent":["import type { Json } from \"../protocol\";\n\n/** Who a connection is. `user` identifies a person; other claims are the app's. @since 1.0.0 */\nexport interface Identity {\n readonly user?: string;\n readonly [claim: string]: unknown;\n}\n\n/** What a rule predicate is asked. @since 1.0.0 */\nexport interface RuleContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly op: \"subscribe\" | \"publish\" | \"presence\";\n}\n\n/** `true`, `false`, or a predicate that decides per connection. @since 1.0.0 */\nexport type Permission<Params extends Readonly<Record<string, string>> = Record<string, string>> =\n | boolean\n | ((context: RuleContext<Params>) => boolean | Promise<boolean>);\n\n/** What a request handler is given. @since 1.0.0 */\nexport interface RequestContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly event: string;\n readonly data: Json;\n /** Aborted when the connection closes, the request times out or the hub closes. */\n readonly signal: AbortSignal;\n}\n\n/** Answers a request. The returned value is the reply (JSON). @since 1.0.0 */\nexport type RequestHandler<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> = (context: RequestContext<Params>) => Json | undefined | Promise<Json | undefined>;\n\n/**\n * What a channel pattern allows. Every permission defaults to `false`: a channel nobody wrote a rule for is closed.\n * @since 1.0.0\n */\nexport interface ChannelRule<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly subscribe?: Permission<Params>;\n readonly publish?: Permission<Params>;\n readonly presence?: Permission<Params>;\n /** Messages kept for resume (default: the hub's `history` limit; 0 turns resume off for these channels). */\n readonly history?: number;\n /** Request handlers by event name. */\n readonly handlers?: Readonly<Record<string, RequestHandler<Params>>>;\n}\n\ntype Part =\n | { readonly kind: \"literal\"; readonly text: string }\n | { readonly kind: \"param\"; readonly name: string }\n | { readonly kind: \"rest\"; readonly name: string };\n\n/** A compiled rule. @internal */\nexport interface CompiledRule {\n readonly pattern: string;\n readonly parts: readonly Part[];\n readonly rule: ChannelRule;\n}\n\nconst PARAM = /^\\{([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst REST = /^\\{\\.\\.\\.([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst LITERAL = /^[A-Za-z0-9_.@/-]+$/;\n\n/**\n * Compiles a channel pattern: `:`-separated segments that are literals, `{name}` (one segment) or a final `{...name}` (one or\n * more segments).\n * @throws TypeError for an invalid pattern.\n */\nexport function compileRule(pattern: string, rule: ChannelRule): CompiledRule {\n if (typeof pattern !== \"string\" || pattern.length === 0 || pattern.length > 128)\n throw new TypeError(`the channel pattern \"${String(pattern)}\" must be 1..128 characters.`);\n const segments = pattern.split(\":\");\n const seen = new Set<string>();\n const parts = segments.map((segment, at): Part => {\n const rest = REST.exec(segment);\n if (rest !== null) {\n if (at !== segments.length - 1)\n throw new TypeError(\n `the channel pattern \"${pattern}\": {...name} must be the last segment.`,\n );\n const name = rest[1] as string;\n if (seen.has(name))\n throw new TypeError(`the channel pattern \"${pattern}\" repeats \"${name}\".`);\n seen.add(name);\n return { kind: \"rest\", name };\n }\n const param = PARAM.exec(segment);\n if (param !== null) {\n const name = param[1] as string;\n if (seen.has(name))\n throw new TypeError(`the channel pattern \"${pattern}\" repeats \"${name}\".`);\n seen.add(name);\n return { kind: \"param\", name };\n }\n if (!LITERAL.test(segment) || segment.startsWith(\"$\"))\n throw new TypeError(`the channel pattern \"${pattern}\" has an invalid segment \"${segment}\".`);\n return { kind: \"literal\", text: segment };\n });\n if (rule.history !== undefined && (!Number.isInteger(rule.history) || rule.history < 0))\n throw new RangeError(`the rule for \"${pattern}\": history must be a non-negative integer.`);\n return { pattern, parts, rule };\n}\n\n/** The params a name captures against a rule, or undefined when it does not match. */\nexport function matchRule(\n compiled: CompiledRule,\n channel: string,\n): Record<string, string> | undefined {\n const segments = channel.split(\":\");\n if (segments.includes(\"\")) return undefined;\n const { parts } = compiled;\n const last = parts[parts.length - 1];\n if (last?.kind === \"rest\" ? segments.length < parts.length : segments.length !== parts.length)\n return undefined;\n const params: Record<string, string> = {};\n for (let at = 0; at < parts.length; at += 1) {\n const part = parts[at] as Part;\n const segment = segments[at] as string;\n if (part.kind === \"rest\") {\n params[part.name] = segments.slice(at).join(\":\");\n return params;\n }\n if (part.kind === \"literal\") {\n if (segment !== part.text) return undefined;\n } else {\n params[part.name] = segment;\n }\n }\n return params;\n}\n"],"mappings":"AAqEA,MAAMA,EAAQ,iCACRC,EAAO,uCACPC,EAAU,sBAOhB,SAAgBC,EAAY,EAAiB,EAAiC,CAC5E,GAAI,OAAOC,GAAY,UAAYA,EAAQ,SAAW,GAAKA,EAAQ,OAAS,IAC1E,MAAU,UAAU,wBAAwB,OAAOA,CAAO,EAAE,6BAA6B,EAC3F,IAAMC,EAAWD,EAAQ,MAAM,GAAG,EAC5BE,kBAAO,IAAI,IACXC,EAAQF,EAAS,KAAK,EAAS,IAAa,CAChD,IAAMG,EAAOP,EAAK,KAAKQ,CAAO,EAC9B,GAAID,IAAS,KAAM,CACjB,GAAIE,IAAOL,EAAS,OAAS,EAC3B,MAAU,UACR,wBAAwBD,EAAQ,uCAClC,EACF,IAAMO,EAAOH,EAAK,GAClB,GAAIF,EAAK,IAAIK,CAAI,EACf,MAAU,UAAU,wBAAwBP,EAAQ,aAAaO,EAAK,GAAG,EAE3E,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,OAAQ,MAAK,CAC9B,CACA,IAAMC,EAAQZ,EAAM,KAAKS,CAAO,EAChC,GAAIG,IAAU,KAAM,CAClB,IAAMD,EAAOC,EAAM,GACnB,GAAIN,EAAK,IAAIK,CAAI,EACf,MAAU,UAAU,wBAAwBP,EAAQ,aAAaO,EAAK,GAAG,EAE3E,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,QAAS,MAAK,CAC/B,CACA,GAAI,CAACT,EAAQ,KAAKO,CAAO,GAAKA,EAAQ,WAAW,GAAG,EAClD,MAAU,UAAU,wBAAwBL,EAAQ,4BAA4BK,EAAQ,GAAG,EAC7F,MAAO,CAAE,KAAM,UAAW,KAAMA,CAAQ,CAC1C,CAAC,EACD,GAAII,EAAK,UAAY,IAAA,KAAc,CAAC,OAAO,UAAUA,EAAK,OAAO,GAAKA,EAAK,QAAU,GACnF,MAAU,WAAW,iBAAiBT,EAAQ,2CAA2C,EAC3F,MAAO,CAAE,UAAS,QAAO,MAAK,CAChC,CAGA,SAAgBU,EACd,EACA,EACoC,CACpC,IAAMT,EAAWU,EAAQ,MAAM,GAAG,EAClC,GAAIV,EAAS,SAAS,EAAE,EAAG,OAC3B,GAAM,CAAE,SAAUW,EAElB,GADaT,EAAMA,EAAM,OAAS,EAC1B,EAAE,OAAS,OAASF,EAAS,OAASE,EAAM,OAASF,EAAS,SAAWE,EAAM,OACrF,OACF,IAAMU,EAAiC,CAAC,EACxC,IAAK,IAAIP,EAAK,EAAGA,EAAKH,EAAM,OAAQ,GAAM,EAAG,CAC3C,IAAMW,EAAOX,EAAMG,GACbD,EAAUJ,EAASK,GACzB,GAAIQ,EAAK,OAAS,OAEhB,MADA,GAAOA,EAAK,MAAQb,EAAS,MAAMK,CAAE,CAAC,CAAC,KAAK,GAAG,EACxCO,EAET,GAAIC,EAAK,OAAS,UACZ,IAAAT,IAAYS,EAAK,KAAM,MAAO,KAElC,GAAOA,EAAK,MAAQT,CAExB,CACA,OAAOQ,CACT,C"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{decodeServerFrame as e}from"./protocol.js";function t(t,n={}){let r=[],i=[],a,o=t.connect({send(t){if(a!==void 0)return!1;let n=e(t);if(n===void 0)return!0;r.push(n);for(let e of[...i])e(n);return!0},close(e,t){a={code:e,reason:t}}},n);return{id:o.id,frames:r,get closedWith(){return a},send(e){o.receive(typeof e==`string`?e:JSON.stringify(e))},until(e,t=2e3){let n=r.find(e);return n===void 0?new Promise((n,r)=>{let a=setTimeout(()=>{i.splice(i.indexOf(o),1),r(/* @__PURE__ */ Error(`no matching frame arrived in time`))},t),o=t=>{e(t)&&(clearTimeout(a),i.splice(i.indexOf(o),1),n(t))};i.push(o)}):Promise.resolve(n)},settled:()=>o.settled(),async close(){o.closed(),await o.done}}}function n(e,t={}){class n{onopen=null;onmessage=null;onclose=null;onerror=null;readyState=0;url;#e;constructor(n){this.url=n,queueMicrotask(()=>{this.readyState===0&&(this.readyState=1,this.#e=e.connect({send:e=>this.readyState===1&&(queueMicrotask(()=>{this.readyState===1&&this.onmessage?.(new MessageEvent(`message`,{data:e}))}),!0),close:(e,t)=>this.#t(e,t)},t),this.onopen?.(new Event(`open`)))})}send(e){if(this.readyState!==1)throw new DOMException(`the socket is not open`,`InvalidStateError`);let t=this.#e;queueMicrotask(()=>t?.receive(e))}close(e=1e3){this.readyState<2&&this.#t(e,``)}#t(e,t){if(this.readyState===3)return;this.readyState=3,this.#e?.closed();let n={type:`close`,code:e,reason:t,wasClean:e!==1006};queueMicrotask(()=>this.onclose?.(n))}}return n}async function r(e,t=3e3){let n=Date.now()+t;for(;!e();){if(Date.now()>n)throw Error(`the condition did not become true in time`);await new Promise(e=>setTimeout(e,5))}}function i(e){let{runner:t}=e,{describe:n,it:i,expect:a}=t,o=0,s=[],c=()=>(o+=1,`conformance-${Date.now().toString(36)}-${o}`),l=async t=>{let n=await e.connect(t);return s.push(n),n};t.afterEach(async()=>{await Promise.all(s.splice(0).map(e=>e.close().catch(()=>void 0)))});let u={history:100,historyTtlMs:6e4,idempotencyTtlMs:6e4},d=e=>({event:`e`,data:String(e)});n(`adapter: messages`,()=>{i(`[RT-AD-01] numbers messages 1, 2, 3 per channel, independently`,async()=>{let e=await l(c()),t=[];for(let n=0;n<3;n+=1)t.push((await e.publish(`a`,d(n),u)).seq);a(t).toEqual([1,2,3]),a((await e.publish(`b`,d(0),u)).seq).toBe(1)}),i(`[RT-AD-02] delivers to listeners in seq order, and stops after unsubscribe`,async()=>{let e=await l(c()),t=[],n=await e.subscribe(`a`,e=>t.push(e.seq));for(let t=0;t<20;t+=1)await e.publish(`a`,d(t),u);await r(()=>t.length===20),a(t).toEqual(Array.from({length:20},(e,t)=>t+1)),await n(),await e.publish(`a`,d(99),u),await new Promise(e=>setTimeout(e,50)),a(t).toHaveLength(20)}),i(`[RT-AD-03] numbers concurrent publishes from two servers without a duplicate or a gap, seen in order on both`,async()=>{let e=c(),t=await l(e),n=await l(e),i=[],o=[];await t.subscribe(`a`,e=>i.push(e.seq)),await n.subscribe(`a`,e=>o.push(e.seq));let s=await Promise.all(Array.from({length:100},(e,r)=>(r%2==0?t:n).publish(`a`,d(r),u)));a(s.map(e=>e.seq).sort((e,t)=>e-t)).toEqual(Array.from({length:100},(e,t)=>t+1)),await r(()=>i.length===100&&o.length===100),a(i).toEqual(Array.from({length:100},(e,t)=>t+1)),a(o).toEqual(Array.from({length:100},(e,t)=>t+1))}),i(`[RT-AD-04] reads history after a seq with a limit, and reports head and oldest`,async()=>{let e=await l(c());for(let t=1;t<=10;t+=1)await e.publish(`a`,d(t),u);let t=await e.history(`a`,3,4);a(t.messages.map(e=>e.seq)).toEqual([4,5,6,7]),a(t.head).toBe(10),a(t.oldest).toBe(1),a((await e.history(`nothing`,0,10)).head).toBe(0),a((await e.history(`a`,2**53-1,0)).head).toBe(10)}),i("[RT-AD-05] keeps only the newest `history` messages",async()=>{let e=await l(c()),t={...u,history:3};for(let n=1;n<=8;n+=1)await e.publish(`a`,d(n),t);let n=await e.history(`a`,0,100);a(n.messages.map(e=>e.seq)).toEqual([6,7,8]),a(n.oldest).toBe(6),a(n.head).toBe(8)}),i(`[RT-AD-06] forgets messages older than historyTtlMs but keeps numbering`,async()=>{let e=await l(c()),t={...u,historyTtlMs:400};await e.publish(`a`,d(1),t),await new Promise(e=>setTimeout(e,700)),await e.publish(`a`,d(2),t);let n=await e.history(`a`,0,100);a(n.messages.map(e=>e.seq)).toEqual([2]),a(n.head).toBe(2)}),i(`[RT-AD-07] with history 0 retains nothing but still numbers`,async()=>{let e=await l(c()),t={...u,history:0};a((await e.publish(`a`,d(1),t)).seq).toBe(1),a((await e.publish(`a`,d(2),t)).seq).toBe(2);let n=await e.history(`a`,0,10);a(n.messages).toHaveLength(0),a(n.head).toBe(2),a(n.oldest).toBe(0)}),i(`[RT-AD-08] accepts a publish with a known idempotency key once, across servers, until the key expires`,async()=>{let e=c(),t=await l(e),n=await l(e),r={...u,key:`k1`,idempotencyTtlMs:400},i=await t.publish(`a`,d(1),r),o=await n.publish(`a`,d(2),r);a(i).toEqual({seq:1,duplicate:!1}),a(o).toEqual({seq:1,duplicate:!0}),a((await t.history(`a`,0,10)).messages).toHaveLength(1),await new Promise(e=>setTimeout(e,700)),a((await t.publish(`a`,d(3),r)).duplicate).toBe(!1)}),i(`[RT-AD-09] gives two racing publishes with one key a single seq`,async()=>{let e=c(),t=await l(e),n=await l(e),r={...u,key:`race`},i=await Promise.all([t.publish(`a`,d(1),r),n.publish(`a`,d(2),r)]);a(i.filter(e=>e.duplicate)).toHaveLength(1),a(i[0]?.seq).toBe(i[1]?.seq)}),i(`[RT-AD-10] isolates namespaces`,async()=>{let e=await l(c()),t=await l(c());await e.publish(`a`,d(1),u),a((await t.history(`a`,0,10)).head).toBe(0)})}),n(`adapter: presence`,()=>{let e=(e,t=1,n=null)=>({id:e,v:t,s:n});i(`[RT-AD-20] lists, updates and removes members and announces join, set and leave to every server`,async()=>{let t=c(),n=await l(t),i=await l(t),o=[];await i.presenceSubscribe(`room`,e=>o.push(`${e.op}:${e.member.id}:${e.member.v}`)),await n.presenceSet(`room`,e(`m1`),5e3),await n.presenceSet(`room`,e(`m1`,2,{x:1}),5e3),await n.presenceSet(`room`,e(`m2`),5e3),a((await i.presenceList(`room`)).map(e=>e.id).sort()).toEqual([`m1`,`m2`]),a(await n.presenceRemove(`room`,`m1`)).toBe(!0),await r(()=>o.length===4),a(o).toEqual([`join:m1:1`,`set:m1:2`,`join:m2:1`,`leave:m1:2`])}),i(`[RT-AD-21] ignores an older version of a member`,async()=>{let t=await l(c());await t.presenceSet(`room`,e(`m1`,5,`new`),5e3),await t.presenceSet(`room`,e(`m1`,3,`old`),5e3),a((await t.presenceList(`room`))[0]).toEqual(e(`m1`,5,`new`))}),i(`[RT-AD-22] removes a member once even when two servers race to remove it`,async()=>{let t=c(),n=await l(t),i=await l(t),o=[];await n.presenceSubscribe(`room`,e=>e.op===`leave`&&o.push(e.member.id)),await n.presenceSet(`room`,e(`m1`),5e3);let s=await Promise.all([n.presenceRemove(`room`,`m1`),i.presenceRemove(`room`,`m1`)]);a(s.filter(Boolean)).toHaveLength(1),await r(()=>o.length>=1),await new Promise(e=>setTimeout(e,40)),a(o).toEqual([`m1`])}),i(`[RT-AD-23] expires members without a heartbeat, and reap announces each leave once across servers`,async()=>{let t=c(),n=await l(t),i=await l(t),o=[];await n.presenceSubscribe(`room`,e=>e.op===`leave`&&o.push(e.member.id)),await n.presenceSet(`room`,e(`gone`),60),await n.presenceSet(`room`,e(`stays`),5e3),await new Promise(e=>setTimeout(e,120)),a((await n.presenceList(`room`)).map(e=>e.id)).toEqual([`stays`]);let s=await Promise.all([n.presenceReap(`room`),i.presenceReap(`room`)]);a(s.flat().map(e=>e.id)).toEqual([`gone`]),await r(()=>o.length>=1),await new Promise(e=>setTimeout(e,40)),a(o).toEqual([`gone`])}),i(`[RT-AD-24] touch extends the life of members and reports the ones that no longer exist`,async()=>{let t=await l(c());await t.presenceSet(`room`,e(`m1`),80);let n=await t.presenceTouch([{channel:`room`,id:`m1`},{channel:`room`,id:`ghost`}],5e3);a(n).toEqual([{channel:`room`,id:`ghost`}]),await new Promise(e=>setTimeout(e,150)),a((await t.presenceList(`room`)).map(e=>e.id)).toEqual([`m1`])}),i(`[RT-AD-25] stops presence events after unsubscribe`,async()=>{let t=await l(c()),n=[],i=await t.presenceSubscribe(`room`,e=>n.push(e.op));await t.presenceSet(`room`,e(`m1`),5e3),await r(()=>n.length===1),await i(),await t.presenceSet(`room`,e(`m2`),5e3),await new Promise(e=>setTimeout(e,50)),a(n).toEqual([`join`])})})}export{i as adapterConformance,t as connectTest,n as memoryWebSocket};
|
|
2
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":["connectTest","frames","watchers","closedWith","connection","hub","frame","decodeServerFrame","text","watcher","identity","found","match","timer","reject","timeoutMs","resolve","memoryWebSocket","MemorySocket","url","#connection","#finish","code","reason","event","until","stop","ms","condition","adapterConformance","options","runner","counter","opened","fresh","open","adapter","namespace","rules","entry","n","describe","it","seqs","at","expect","seen","message","first","second","seenFirst","seenSecond","results","result","x","y","page","small","short","none","keyed","one","two","member","events","item","leaves","reaped","missing"],"sources":["../../src/testing.ts"],"sourcesContent":["import type { Adapter, PublishOptions } from \"./adapter\";\nimport {\n decodeServerFrame,\n type Json,\n type Member,\n type ServerFrame,\n type StoredEntry,\n} from \"./protocol\";\nimport type { Hub } from \"./server/index\";\nimport type { Identity } from \"./server/rules\";\n\n/**\n * A connection to a hub with no network: frames go in as objects and come out decoded. For tests of an app's channel rules and\n * request handlers.\n * @since 1.0.0\n */\nexport interface TestConnection {\n /** The connection id the hub assigned. */\n readonly id: string;\n /** Every frame the hub sent, decoded, in order. */\n readonly frames: readonly ServerFrame[];\n /** The code and reason the hub closed the connection with, once it did. */\n readonly closedWith: { readonly code: number; readonly reason: string } | undefined;\n /** Sends one frame (an object, or raw text to test malformed input). */\n send(frame: Readonly<Record<string, unknown>> | string): void;\n /** Resolves with the first frame (already received or yet to come) that satisfies `match`; rejects after `timeoutMs`. */\n until<T extends ServerFrame>(\n match: (frame: ServerFrame) => frame is T,\n timeoutMs?: number,\n ): Promise<T>;\n /** Resolves when the hub handled every frame sent so far. */\n settled(): Promise<void>;\n /** Closes the connection as a network drop would. */\n close(): Promise<void>;\n}\n\n/**\n * Opens a {@link TestConnection} to `hub`.\n *\n * @param hub The hub.\n * @param identity Who the connection is.\n * @returns The connection (its `ready` frame has been received).\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n * import { connectTest } from \"@shadow-js/realtime/testing\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true } } });\n * const client = connectTest(hub);\n * client.send({ t: \"sub\", n: 1, c: \"chat:lobby\" });\n * await client.settled();\n * client.frames.some((frame) => frame.t === \"subbed\"); // true\n * await hub.close();\n * @since 1.0.0\n */\nexport function connectTest(hub: Hub, identity: Identity = {}): TestConnection {\n const frames: ServerFrame[] = [];\n const watchers: ((frame: ServerFrame) => void)[] = [];\n let closedWith: { code: number; reason: string } | undefined;\n const connection = hub.connect(\n {\n send(text) {\n if (closedWith !== undefined) return false;\n const frame = decodeServerFrame(text);\n if (frame === undefined) return true;\n frames.push(frame);\n for (const watcher of [...watchers]) watcher(frame);\n return true;\n },\n close(code, reason) {\n closedWith = { code, reason };\n },\n },\n identity,\n );\n return {\n id: connection.id,\n frames,\n get closedWith() {\n return closedWith;\n },\n send(frame) {\n connection.receive(typeof frame === \"string\" ? frame : JSON.stringify(frame));\n },\n until<T extends ServerFrame>(match: (frame: ServerFrame) => frame is T, timeoutMs = 2000) {\n const found = frames.find(match);\n if (found !== undefined) return Promise.resolve(found);\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n watchers.splice(watchers.indexOf(watcher), 1);\n reject(new Error(\"no matching frame arrived in time\"));\n }, timeoutMs);\n const watcher = (frame: ServerFrame): void => {\n if (!match(frame)) return;\n clearTimeout(timer);\n watchers.splice(watchers.indexOf(watcher), 1);\n resolve(frame);\n };\n watchers.push(watcher);\n });\n },\n settled: () => connection.settled(),\n async close() {\n connection.closed();\n await connection.done;\n },\n };\n}\n\n/**\n * A `WebSocket` constructor whose other end is `hub` in the same process: pass it as the `WebSocket` option of `createRealtime` to\n * test client code and server rules together with no network and no server. The hub is real; only the socket is replaced.\n *\n * @param hub The hub the sockets connect to.\n * @param identity Who the connections are.\n * @returns A drop-in for the global `WebSocket`.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { createHub } from \"@shadow-js/realtime/server\";\n * import { memoryWebSocket } from \"@shadow-js/realtime/testing\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * const rt = createRealtime({ url: \"/rt\", transport: \"ws\", WebSocket: memoryWebSocket(hub) });\n * rt.close();\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryWebSocket(hub: Hub, identity: Identity = {}): typeof WebSocket {\n class MemorySocket {\n onopen: ((event: Event) => void) | null = null;\n onmessage: ((event: MessageEvent) => void) | null = null;\n onclose: ((event: CloseEvent) => void) | null = null;\n onerror: ((event: Event) => void) | null = null;\n readyState = 0;\n readonly url: string;\n #connection: ReturnType<Hub[\"connect\"]> | undefined;\n\n constructor(url: string) {\n this.url = url;\n queueMicrotask(() => {\n if (this.readyState !== 0) return;\n this.readyState = 1;\n this.#connection = hub.connect(\n {\n send: (frame) => {\n if (this.readyState !== 1) return false;\n queueMicrotask(() => {\n if (this.readyState === 1)\n this.onmessage?.(new MessageEvent(\"message\", { data: frame }));\n });\n return true;\n },\n close: (code, reason) => this.#finish(code, reason),\n },\n identity,\n );\n this.onopen?.(new Event(\"open\"));\n });\n }\n\n send(text: string): void {\n if (this.readyState !== 1)\n throw new DOMException(\"the socket is not open\", \"InvalidStateError\");\n const connection = this.#connection;\n queueMicrotask(() => connection?.receive(text));\n }\n\n close(code = 1000): void {\n if (this.readyState < 2) this.#finish(code, \"\");\n }\n\n #finish(code: number, reason: string): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n this.#connection?.closed();\n const event = { type: \"close\", code, reason, wasClean: code !== 1006 } as CloseEvent;\n queueMicrotask(() => this.onclose?.(event));\n }\n }\n return MemorySocket as unknown as typeof WebSocket;\n}\n\n/** The test runner the conformance suite registers its tests with (Vitest's `describe`, `it`, `expect` fit). @since 1.0.0 */\nexport interface ConformanceRunner {\n describe(name: string, body: () => void): void;\n it(name: string, body: () => Promise<void>, timeoutMs?: number): void;\n expect(value: unknown): {\n toBe(expected: unknown): void;\n toEqual(expected: unknown): void;\n toBeGreaterThan(expected: number): void;\n toHaveLength(expected: number): void;\n toBeLessThanOrEqual(expected: number): void;\n };\n /** Runs after each test (registers cleanup). */\n afterEach(body: () => Promise<void>): void;\n}\n\n/** Options of {@link adapterConformance}. @since 1.0.0 */\nexport interface ConformanceOptions {\n readonly runner: ConformanceRunner;\n /**\n * Returns an adapter attached to `namespace`. Two calls with the same namespace are two servers sharing one store (for a\n * Redis adapter: two clients on one key prefix); different namespaces share nothing. `now` is not passed: the suite uses\n * short real timeouts.\n */\n readonly connect: (namespace: string) => Adapter | Promise<Adapter>;\n}\n\nconst WAIT_STEP_MS = 5;\n\nasync function until(condition: () => boolean, ms = 3000): Promise<void> {\n const stop = Date.now() + ms;\n while (!condition()) {\n if (Date.now() > stop) throw new Error(\"the condition did not become true in time\");\n await new Promise((resolve) => setTimeout(resolve, WAIT_STEP_MS));\n }\n}\n\n/**\n * Registers the tests every adapter must pass (https://realtime.shadowjs.net/guides/realtime-protocol/): atomic gapless numbering, ordered fan-out\n * between servers, retention, idempotency, and presence with exactly-once leave. Run it against your own adapter.\n *\n * @param options The test runner and a way to open adapters on a shared store.\n * @example\n * import { describe, expect, it, afterEach } from \"vitest\";\n * import { memoryAdapter } from \"@shadow-js/realtime/server\";\n * import { adapterConformance } from \"@shadow-js/realtime/testing\";\n *\n * const stores = new Map<string, ReturnType<typeof memoryAdapter>>();\n * adapterConformance({\n * runner: { describe, it, expect, afterEach },\n * connect: (namespace) => {\n * const found = stores.get(namespace) ?? memoryAdapter();\n * stores.set(namespace, found);\n * return found;\n * },\n * });\n * @since 1.0.0\n */\nexport function adapterConformance(options: ConformanceOptions): void {\n const { runner } = options;\n const { describe, it, expect } = runner;\n let counter = 0;\n const opened: Adapter[] = [];\n const fresh = (): string => {\n counter += 1;\n return `conformance-${Date.now().toString(36)}-${counter}`;\n };\n const open = async (namespace: string): Promise<Adapter> => {\n const adapter = await options.connect(namespace);\n opened.push(adapter);\n return adapter;\n };\n runner.afterEach(async () => {\n await Promise.all(opened.splice(0).map((adapter) => adapter.close().catch(() => undefined)));\n });\n const rules: PublishOptions = { history: 100, historyTtlMs: 60_000, idempotencyTtlMs: 60_000 };\n const entry = (n: number): StoredEntry => ({ event: \"e\", data: String(n) });\n\n describe(\"adapter: messages\", () => {\n it(\"[RT-AD-01] numbers messages 1, 2, 3 per channel, independently\", async () => {\n const adapter = await open(fresh());\n const seqs: number[] = [];\n for (let at = 0; at < 3; at += 1)\n seqs.push((await adapter.publish(\"a\", entry(at), rules)).seq);\n expect(seqs).toEqual([1, 2, 3]);\n expect((await adapter.publish(\"b\", entry(0), rules)).seq).toBe(1);\n });\n\n it(\"[RT-AD-02] delivers to listeners in seq order, and stops after unsubscribe\", async () => {\n const adapter = await open(fresh());\n const seen: number[] = [];\n const stop = await adapter.subscribe(\"a\", (message) => seen.push(message.seq));\n for (let at = 0; at < 20; at += 1) await adapter.publish(\"a\", entry(at), rules);\n await until(() => seen.length === 20);\n expect(seen).toEqual(Array.from({ length: 20 }, (_, at) => at + 1));\n await stop();\n await adapter.publish(\"a\", entry(99), rules);\n await new Promise((resolve) => setTimeout(resolve, 50));\n expect(seen).toHaveLength(20);\n });\n\n it(\"[RT-AD-03] numbers concurrent publishes from two servers without a duplicate or a gap, seen in order on both\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const seenFirst: number[] = [];\n const seenSecond: number[] = [];\n await first.subscribe(\"a\", (message) => seenFirst.push(message.seq));\n await second.subscribe(\"a\", (message) => seenSecond.push(message.seq));\n const results = await Promise.all(\n Array.from({ length: 100 }, (_, at) =>\n (at % 2 === 0 ? first : second).publish(\"a\", entry(at), rules),\n ),\n );\n expect(results.map((result) => result.seq).sort((x, y) => x - y)).toEqual(\n Array.from({ length: 100 }, (_, at) => at + 1),\n );\n await until(() => seenFirst.length === 100 && seenSecond.length === 100);\n expect(seenFirst).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));\n expect(seenSecond).toEqual(Array.from({ length: 100 }, (_, at) => at + 1));\n });\n\n it(\"[RT-AD-04] reads history after a seq with a limit, and reports head and oldest\", async () => {\n const adapter = await open(fresh());\n for (let at = 1; at <= 10; at += 1) await adapter.publish(\"a\", entry(at), rules);\n const page = await adapter.history(\"a\", 3, 4);\n expect(page.messages.map((message) => message.seq)).toEqual([4, 5, 6, 7]);\n expect(page.head).toBe(10);\n expect(page.oldest).toBe(1);\n expect((await adapter.history(\"nothing\", 0, 10)).head).toBe(0);\n expect((await adapter.history(\"a\", Number.MAX_SAFE_INTEGER, 0)).head).toBe(10);\n });\n\n it(\"[RT-AD-05] keeps only the newest `history` messages\", async () => {\n const adapter = await open(fresh());\n const small: PublishOptions = { ...rules, history: 3 };\n for (let at = 1; at <= 8; at += 1) await adapter.publish(\"a\", entry(at), small);\n const page = await adapter.history(\"a\", 0, 100);\n expect(page.messages.map((message) => message.seq)).toEqual([6, 7, 8]);\n expect(page.oldest).toBe(6);\n expect(page.head).toBe(8);\n });\n\n it(\"[RT-AD-06] forgets messages older than historyTtlMs but keeps numbering\", async () => {\n const adapter = await open(fresh());\n // 400 ms: an adapter over a network (Redis, a Durable Object in another location) takes milliseconds per call, which a shorter lifetime\n // would count against the message it has just stored.\n const short: PublishOptions = { ...rules, historyTtlMs: 400 };\n await adapter.publish(\"a\", entry(1), short);\n await new Promise((resolve) => setTimeout(resolve, 700));\n await adapter.publish(\"a\", entry(2), short);\n const page = await adapter.history(\"a\", 0, 100);\n expect(page.messages.map((message) => message.seq)).toEqual([2]);\n expect(page.head).toBe(2);\n });\n\n it(\"[RT-AD-07] with history 0 retains nothing but still numbers\", async () => {\n const adapter = await open(fresh());\n const none: PublishOptions = { ...rules, history: 0 };\n expect((await adapter.publish(\"a\", entry(1), none)).seq).toBe(1);\n expect((await adapter.publish(\"a\", entry(2), none)).seq).toBe(2);\n const page = await adapter.history(\"a\", 0, 10);\n expect(page.messages).toHaveLength(0);\n expect(page.head).toBe(2);\n expect(page.oldest).toBe(0);\n });\n\n it(\"[RT-AD-08] accepts a publish with a known idempotency key once, across servers, until the key expires\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const keyed: PublishOptions = { ...rules, key: \"k1\", idempotencyTtlMs: 400 };\n const one = await first.publish(\"a\", entry(1), keyed);\n const two = await second.publish(\"a\", entry(2), keyed);\n expect(one).toEqual({ seq: 1, duplicate: false });\n expect(two).toEqual({ seq: 1, duplicate: true });\n expect((await first.history(\"a\", 0, 10)).messages).toHaveLength(1);\n await new Promise((resolve) => setTimeout(resolve, 700));\n expect((await first.publish(\"a\", entry(3), keyed)).duplicate).toBe(false);\n });\n\n it(\"[RT-AD-09] gives two racing publishes with one key a single seq\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const keyed: PublishOptions = { ...rules, key: \"race\" };\n const results = await Promise.all([\n first.publish(\"a\", entry(1), keyed),\n second.publish(\"a\", entry(2), keyed),\n ]);\n expect(results.filter((result) => result.duplicate)).toHaveLength(1);\n expect(results[0]?.seq).toBe(results[1]?.seq);\n });\n\n it(\"[RT-AD-10] isolates namespaces\", async () => {\n const one = await open(fresh());\n const two = await open(fresh());\n await one.publish(\"a\", entry(1), rules);\n expect((await two.history(\"a\", 0, 10)).head).toBe(0);\n });\n });\n\n describe(\"adapter: presence\", () => {\n const member = (id: string, v = 1, s: Json = null): Member => ({ id, v, s });\n\n it(\"[RT-AD-20] lists, updates and removes members and announces join, set and leave to every server\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const events: string[] = [];\n await second.presenceSubscribe(\"room\", (event) =>\n events.push(`${event.op}:${event.member.id}:${event.member.v}`),\n );\n await first.presenceSet(\"room\", member(\"m1\"), 5000);\n await first.presenceSet(\"room\", member(\"m1\", 2, { x: 1 }), 5000);\n await first.presenceSet(\"room\", member(\"m2\"), 5000);\n expect((await second.presenceList(\"room\")).map((item) => item.id).sort()).toEqual([\n \"m1\",\n \"m2\",\n ]);\n expect(await first.presenceRemove(\"room\", \"m1\")).toBe(true);\n await until(() => events.length === 4);\n expect(events).toEqual([\"join:m1:1\", \"set:m1:2\", \"join:m2:1\", \"leave:m1:2\"]);\n });\n\n it(\"[RT-AD-21] ignores an older version of a member\", async () => {\n const adapter = await open(fresh());\n await adapter.presenceSet(\"room\", member(\"m1\", 5, \"new\"), 5000);\n await adapter.presenceSet(\"room\", member(\"m1\", 3, \"old\"), 5000);\n expect((await adapter.presenceList(\"room\"))[0]).toEqual(member(\"m1\", 5, \"new\"));\n });\n\n it(\"[RT-AD-22] removes a member once even when two servers race to remove it\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const leaves: string[] = [];\n await first.presenceSubscribe(\n \"room\",\n (event) => event.op === \"leave\" && leaves.push(event.member.id),\n );\n await first.presenceSet(\"room\", member(\"m1\"), 5000);\n const results = await Promise.all([\n first.presenceRemove(\"room\", \"m1\"),\n second.presenceRemove(\"room\", \"m1\"),\n ]);\n expect(results.filter(Boolean)).toHaveLength(1);\n await until(() => leaves.length >= 1);\n await new Promise((resolve) => setTimeout(resolve, 40));\n expect(leaves).toEqual([\"m1\"]);\n });\n\n it(\"[RT-AD-23] expires members without a heartbeat, and reap announces each leave once across servers\", async () => {\n const namespace = fresh();\n const first = await open(namespace);\n const second = await open(namespace);\n const leaves: string[] = [];\n await first.presenceSubscribe(\n \"room\",\n (event) => event.op === \"leave\" && leaves.push(event.member.id),\n );\n await first.presenceSet(\"room\", member(\"gone\"), 60);\n await first.presenceSet(\"room\", member(\"stays\"), 5000);\n await new Promise((resolve) => setTimeout(resolve, 120));\n expect((await first.presenceList(\"room\")).map((item) => item.id)).toEqual([\"stays\"]);\n const reaped = await Promise.all([first.presenceReap(\"room\"), second.presenceReap(\"room\")]);\n expect(reaped.flat().map((item) => item.id)).toEqual([\"gone\"]);\n await until(() => leaves.length >= 1);\n await new Promise((resolve) => setTimeout(resolve, 40));\n expect(leaves).toEqual([\"gone\"]);\n });\n\n it(\"[RT-AD-24] touch extends the life of members and reports the ones that no longer exist\", async () => {\n const adapter = await open(fresh());\n await adapter.presenceSet(\"room\", member(\"m1\"), 80);\n const missing = await adapter.presenceTouch(\n [\n { channel: \"room\", id: \"m1\" },\n { channel: \"room\", id: \"ghost\" },\n ],\n 5000,\n );\n expect(missing).toEqual([{ channel: \"room\", id: \"ghost\" }]);\n await new Promise((resolve) => setTimeout(resolve, 150));\n expect((await adapter.presenceList(\"room\")).map((item) => item.id)).toEqual([\"m1\"]);\n });\n\n it(\"[RT-AD-25] stops presence events after unsubscribe\", async () => {\n const adapter = await open(fresh());\n const events: string[] = [];\n const stop = await adapter.presenceSubscribe(\"room\", (event) => events.push(event.op));\n await adapter.presenceSet(\"room\", member(\"m1\"), 5000);\n await until(() => events.length === 1);\n await stop();\n await adapter.presenceSet(\"room\", member(\"m2\"), 5000);\n await new Promise((resolve) => setTimeout(resolve, 50));\n expect(events).toEqual([\"join\"]);\n });\n });\n}\n"],"mappings":"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"}
|