@shadow-js/realtime 1.0.1 → 1.0.3
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 +29 -0
- package/README.md +35 -0
- package/dist/dev/client/core.js +107 -107
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/sse.js +33 -24
- package/dist/dev/client/sse.js.map +1 -1
- package/dist/dev/client/types.d.ts +6 -0
- package/dist/dev/client/util.js +2 -2
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/cloudflare/link.js +69 -8
- package/dist/dev/cloudflare/link.js.map +1 -1
- package/dist/dev/memory.d.ts +11 -0
- package/dist/dev/memory.js +38 -10
- package/dist/dev/memory.js.map +1 -1
- package/dist/dev/protocol.js +13 -12
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/redis/topics.d.ts +6 -1
- package/dist/dev/redis/topics.js +4 -1
- package/dist/dev/redis/topics.js.map +1 -1
- package/dist/dev/server/http.js +37 -24
- package/dist/dev/server/http.js.map +1 -1
- package/dist/dev/server/hub.d.ts +1 -1
- package/dist/dev/server/hub.js +117 -69
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +2 -2
- package/dist/dev/server/index.js.map +1 -1
- package/dist/dev/server/limits.d.ts +4 -1
- package/dist/dev/server/limits.js.map +1 -1
- package/dist/dev/server/rules.js.map +1 -1
- package/dist/prod/client/core.js +1 -1
- package/dist/prod/client/core.js.map +1 -1
- package/dist/prod/client/sse.js +1 -4
- package/dist/prod/client/sse.js.map +1 -1
- package/dist/prod/client/util.js +1 -1
- package/dist/prod/client/util.js.map +1 -1
- package/dist/prod/cloudflare/link.js +1 -1
- package/dist/prod/cloudflare/link.js.map +1 -1
- package/dist/prod/memory.js +1 -1
- package/dist/prod/memory.js.map +1 -1
- package/dist/prod/protocol.js +1 -1
- package/dist/prod/protocol.js.map +1 -1
- package/dist/prod/redis/topics.js +1 -1
- package/dist/prod/redis/topics.js.map +1 -1
- package/dist/prod/server/http.js +1 -1
- package/dist/prod/server/http.js.map +1 -1
- package/dist/prod/server/hub.js +1 -1
- package/dist/prod/server/hub.js.map +1 -1
- package/dist/prod/server/index.js.map +1 -1
- package/dist/prod/server/limits.js +1 -1
- package/dist/prod/server/limits.js.map +1 -1
- package/dist/prod/server/rules.js +1 -1
- package/dist/prod/server/rules.js.map +1 -1
- package/package.json +6 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.js","names":[],"sources":["../../../src/cloudflare/link.ts"],"sourcesContent":["/**\n * The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept\n * open with backoff, resumed with `after`, and feeding each listener through its own queue.\n *\n * A Worker may use an I/O object (a socket, a stream, a timer) only inside the request that created it. A listener runs in the\n * request that subscribed, so it is called by a loop that was started there and woken by a plain promise: the socket's\n * handler only moves data into queues and never calls a listener itself.\n * @packageDocumentation\n */\nimport { RealtimeError } from \"../errors\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { ClientSocket } from \"./types\";\nimport { decodeLinkFrame } from \"./wire\";\n\n/** What a link needs from the adapter that owns it. @internal */\nexport interface LinkHost {\n readonly reconnectMinMs: number;\n readonly reconnectMaxMs: number;\n /** Ping period; 0 turns pings off. */\n readonly keepAliveMs: number;\n readonly connectTimeoutMs: number;\n /** The most events queued for one listener. */\n readonly maxQueued: number;\n now(): number;\n random(): number;\n /** Opens the subscribe socket (not yet accepted). */\n open(channel: string, after: number | undefined): Promise<ClientSocket>;\n /** Runs `task` once after `ms`; returns the way to cancel. Everything scheduled is canceled when the adapter closes. */\n after(ms: number, task: () => void): () => void;\n /** Runs `task` every `ms`; returns the way to cancel. */\n every(ms: number, task: () => void): () => void;\n report(error: unknown, where: string, channel: string): void;\n /** Called once when the link has nothing left to do. */\n forget(link: ChannelLink): void;\n}\n\ninterface Deferred {\n readonly promise: Promise<void>;\n resolve(): void;\n reject(error: unknown): void;\n}\n\nfunction deferred(): Deferred {\n let resolve: () => void = () => undefined;\n let reject: (error: unknown) => void = () => undefined;\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve;\n reject = onReject;\n });\n // A waiter may give up first; the rejection must not become an unhandled one.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A queue and the loop that empties it, in the request that created it. */\nclass Pump<T> {\n readonly #queue: T[] = [];\n readonly #max: number;\n #wake: (() => void) | undefined;\n #stopped = false;\n\n constructor(max: number) {\n this.#max = max;\n }\n\n push(item: T): void {\n if (this.#stopped || this.#queue.length >= this.#max) return;\n this.#queue.push(item);\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n stop(): void {\n this.#stopped = true;\n this.#queue.length = 0;\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n async run(\n deliver: (item: T) => void,\n report: (error: unknown) => void,\n every: (() => () => void) | undefined,\n ): Promise<void> {\n const cancel = every?.();\n try {\n while (!this.#stopped) {\n const item = this.#queue.shift();\n if (item === undefined) {\n await new Promise<void>((resolve) => {\n this.#wake = resolve;\n });\n continue;\n }\n try {\n deliver(item);\n } catch (error) {\n report(error);\n }\n }\n } finally {\n cancel?.();\n }\n }\n}\n\ninterface MessageSub {\n readonly listener: (message: StoredMessage) => void;\n readonly pump: Pump<StoredMessage>;\n /** The highest seq queued for this listener; -1 until the link tells the head. */\n last: number;\n}\n\ninterface PresenceSub {\n readonly listener: (event: PresenceEvent) => void;\n readonly pump: Pump<PresenceEvent>;\n}\n\n/** A way to stop one listener. @internal */\nexport type LinkSubscription = MessageSub | PresenceSub;\n\ntype LinkState = \"idle\" | \"connecting\" | \"open\" | \"backoff\" | \"disposed\";\n\n/** @internal */\nexport class ChannelLink {\n readonly channel: string;\n readonly #host: LinkHost;\n readonly #messages = new Set<MessageSub>();\n readonly #presence = new Set<PresenceSub>();\n #state: LinkState = \"idle\";\n #socket: ClientSocket | undefined;\n #closeOwner: Deferred | undefined;\n #attempt = 0;\n /** The highest seq received on this link: where a reconnect resumes. */\n #last = 0;\n #known = false;\n #members: Map<string, Member> | undefined;\n #lastReceived = 0;\n #failures = 0;\n #retryAt = 0;\n #connectedAt = 0;\n #ready: Deferred = deferred();\n #cancelRetry: (() => void) | undefined;\n #cancelConnect: (() => void) | undefined;\n #cancelKeep: (() => void) | undefined;\n\n constructor(channel: string, host: LinkHost) {\n this.channel = channel;\n this.#host = host;\n }\n\n get state(): LinkState {\n return this.#state;\n }\n\n get size(): number {\n return this.#messages.size + this.#presence.size;\n }\n\n // --- listeners -----------------------------------------------------------------------------------------------------\n\n /** Adds a message listener (call it in the request that will receive messages). */\n addMessages(listener: (message: StoredMessage) => void): LinkSubscription {\n const sub: MessageSub = {\n listener,\n pump: new Pump(this.#host.maxQueued),\n last: this.#state === \"open\" ? this.#last : -1,\n };\n this.#messages.add(sub);\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n );\n this.#start();\n return sub;\n }\n\n /** Adds a presence listener (call it in the request that will receive events). */\n addPresence(listener: (event: PresenceEvent) => void): LinkSubscription {\n const sub: PresenceSub = { listener, pump: new Pump(this.#host.maxQueued) };\n this.#presence.add(sub);\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n );\n this.#start();\n return sub;\n }\n\n /** Removes a listener; the last one closes the link. */\n remove(sub: LinkSubscription): void {\n sub.pump.stop();\n this.#messages.delete(sub as MessageSub);\n this.#presence.delete(sub as PresenceSub);\n if (this.size === 0) this.dispose(new RealtimeError(\"closed\", \"the subscription ended\"));\n }\n\n /** Resolves when the socket is open and has told the channel's head; rejects after `ms`. */\n whenReady(ms: number): Promise<void> {\n if (this.#state === \"open\") return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const cancel = this.#host.after(ms, () =>\n reject(new RealtimeError(\"unavailable\", \"timed out connecting to the channel object\")),\n );\n this.#ready.promise.then(\n () => {\n cancel();\n resolve();\n },\n (error: unknown) => {\n cancel();\n reject(error);\n },\n );\n });\n }\n\n // --- connecting ----------------------------------------------------------------------------------------------------\n\n /** The pump's periodic check, if pings are on. */\n #nudger(): (() => () => void) | undefined {\n const every = this.#host.keepAliveMs;\n if (every <= 0) return undefined;\n return () => this.#host.every(every, () => this.#check());\n }\n\n #start(): void {\n if (this.#state === \"idle\") this.#connect();\n }\n\n #connect(): void {\n if (this.#state === \"disposed\") return;\n this.#cancelRetry?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect?.();\n this.#state = \"connecting\";\n const attempt = ++this.#attempt;\n this.#connectedAt = this.#host.now();\n this.#cancelConnect = this.#host.after(this.#host.connectTimeoutMs, () => {\n if (attempt === this.#attempt && this.#state === \"connecting\")\n this.#down(attempt, new RealtimeError(\"unavailable\", \"the channel object did not answer\"));\n });\n this.#host.open(this.channel, this.#known ? this.#last : undefined).then(\n (socket) => this.#opened(attempt, socket),\n (error: unknown) => {\n if (attempt === this.#attempt) this.#down(attempt, error);\n },\n );\n }\n\n #opened(attempt: number, socket: ClientSocket): void {\n if (attempt !== this.#attempt || this.#state !== \"connecting\") {\n this.#closeSocket(socket, undefined);\n return;\n }\n const owner = deferred();\n this.#closeOwner = owner;\n this.#socket = socket;\n // Waits in the request that opened the socket, which is the one place guaranteed to be allowed to close it.\n void owner.promise.then(\n () => this.#tryClose(socket),\n () => undefined,\n );\n socket.addEventListener(\"message\", (event) => {\n if (attempt !== this.#attempt || typeof event.data !== \"string\") return;\n this.#received(attempt, event.data);\n });\n socket.addEventListener(\"close\", () => {\n if (attempt === this.#attempt)\n this.#down(attempt, new RealtimeError(\"unavailable\", \"closed\"));\n });\n socket.addEventListener(\"error\", () => {\n if (attempt === this.#attempt) this.#down(attempt, new RealtimeError(\"unavailable\", \"error\"));\n });\n try {\n socket.accept();\n } catch (error) {\n this.#down(attempt, error);\n }\n }\n\n #tryClose(socket: ClientSocket): boolean {\n try {\n socket.close(1000, \"done\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Closes a socket from wherever this runs; when the runtime refuses, the request that opened it does it. */\n #closeSocket(socket: ClientSocket | undefined, owner: Deferred | undefined): void {\n if (socket === undefined) return;\n if (!this.#tryClose(socket)) owner?.resolve();\n }\n\n /** The socket failed or closed: schedule a new attempt, or end the link when nobody listens. */\n #down(attempt: number, error: unknown): void {\n if (attempt !== this.#attempt || this.#state === \"disposed\") return;\n this.#attempt += 1;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n // Waiters keep the promise they hold until the link is open; only an opened link needs a new one.\n if (this.#state === \"open\") this.#ready = deferred();\n if (this.size === 0) {\n this.dispose(error);\n return;\n }\n this.#state = \"backoff\";\n this.#failures += 1;\n const ceiling = Math.min(\n this.#host.reconnectMaxMs,\n this.#host.reconnectMinMs * 2 ** Math.min(this.#failures - 1, 20),\n );\n const delay = Math.floor(this.#host.random() * ceiling);\n this.#retryAt = this.#host.now() + delay;\n this.#cancelRetry?.();\n this.#cancelRetry = this.#host.after(delay, () => this.#connect());\n }\n\n /** Reconnects now, without waiting: the object asked for it (a long replay is continued with a new socket). */\n #resume(attempt: number): void {\n if (attempt !== this.#attempt) return;\n this.#attempt += 1;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n this.#closeSocket(socket, owner);\n this.#state = \"idle\";\n this.#connect();\n }\n\n /**\n * A health check run by every listener's loop (each in its own request) and by the ping timer: whichever request is alive\n * can repair a link whose own request is gone, because a timer or a socket handler of a finished request never runs again.\n */\n #check(): void {\n const now = this.#host.now();\n const grace = Math.max(1000, this.#host.keepAliveMs);\n switch (this.#state) {\n case \"backoff\":\n if (now > this.#retryAt + grace) this.#connect();\n return;\n case \"connecting\":\n if (now > this.#connectedAt + this.#host.connectTimeoutMs * 2) this.#connect();\n return;\n case \"open\": {\n const keep = this.#host.keepAliveMs;\n if (keep > 0 && now - this.#lastReceived > keep * 3) {\n this.#down(\n this.#attempt,\n new RealtimeError(\"unavailable\", \"the channel object went silent\"),\n );\n return;\n }\n try {\n this.#socket?.send(\"ping\");\n } catch {\n // Sending from another request is refused; the owner's own ping covers it.\n }\n return;\n }\n default:\n return;\n }\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n #received(attempt: number, text: string): void {\n this.#lastReceived = this.#host.now();\n if (text === \"pong\") return;\n const frame = decodeLinkFrame(text);\n if (frame === undefined) return;\n switch (frame.t) {\n case \"ready\": {\n if (this.#known && frame.head < this.#last) {\n // The channel's numbering restarted (it was forgotten): follow it.\n this.#last = frame.head;\n for (const sub of this.#messages) sub.last = Math.min(sub.last, frame.head);\n }\n if (!this.#known) {\n this.#last = frame.head;\n this.#known = true;\n }\n for (const sub of this.#messages) if (sub.last < 0) sub.last = frame.head;\n this.#state = \"open\";\n this.#failures = 0;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#lastReceived = this.#host.now();\n if (this.#host.keepAliveMs > 0) {\n this.#cancelKeep?.();\n this.#cancelKeep = this.#host.every(this.#host.keepAliveMs, () => this.#check());\n }\n this.#ready.resolve();\n return;\n }\n case \"snap\": {\n const next = new Map<string, Member>();\n for (const member of frame.members) next.set(member.id, member);\n const before = this.#members;\n this.#members = next;\n if (before === undefined) return;\n for (const [id, member] of before)\n if (!next.has(id)) this.#fanPresence({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const old = before.get(id);\n if (old === undefined) this.#fanPresence({ channel: this.channel, op: \"join\", member });\n else if (member.v > old.v)\n this.#fanPresence({ channel: this.channel, op: \"set\", member });\n }\n return;\n }\n case \"msg\": {\n const message = frame.m;\n if (message.seq > this.#last) this.#last = message.seq;\n for (const sub of this.#messages)\n if (message.seq > sub.last) {\n sub.last = message.seq;\n sub.pump.push(message);\n }\n return;\n }\n case \"pres\": {\n const members = this.#members ?? new Map<string, Member>();\n this.#members = members;\n const known = members.get(frame.member.id);\n if (frame.op === \"leave\") {\n if (known === undefined) return;\n members.delete(frame.member.id);\n } else {\n if (known !== undefined && known.v >= frame.member.v && frame.op === \"join\") return;\n if (known !== undefined && known.v > frame.member.v) return;\n members.set(frame.member.id, frame.member);\n }\n this.#fanPresence({ channel: this.channel, op: frame.op, member: frame.member });\n return;\n }\n case \"more\":\n this.#resume(attempt);\n return;\n case \"bye\":\n this.#down(attempt, new RealtimeError(\"unavailable\", frame.why));\n return;\n }\n }\n\n #fanPresence(event: PresenceEvent): void {\n for (const sub of this.#presence) sub.pump.push(event);\n }\n\n // --- ending --------------------------------------------------------------------------------------------------------\n\n /** Ends the link: stops every listener, cancels timers and closes the socket. */\n dispose(reason: unknown): void {\n if (this.#state === \"disposed\") return;\n this.#state = \"disposed\";\n this.#attempt += 1;\n this.#cancelRetry?.();\n this.#cancelConnect?.();\n this.#cancelKeep?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect = undefined;\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n this.#ready.reject(reason);\n for (const sub of this.#messages) sub.pump.stop();\n for (const sub of this.#presence) sub.pump.stop();\n this.#messages.clear();\n this.#presence.clear();\n this.#host.forget(this);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"link.js","names":[],"sources":["../../../src/cloudflare/link.ts"],"sourcesContent":["/**\n * The Worker side of a channel's subscribe socket: one socket per channel and adapter, shared by every local listener, kept\n * open with backoff, resumed with `after`, and feeding each listener through its own queue.\n *\n * A Worker may use an I/O object (a socket, a stream, a timer) only inside the request that created it. A listener runs in the\n * request that subscribed, so it is called by a loop that was started there and woken by a plain promise: the socket's\n * handler only moves data into queues and never calls a listener itself.\n * @packageDocumentation\n */\nimport { RealtimeError } from \"../errors\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { ClientSocket } from \"./types\";\nimport { decodeLinkFrame } from \"./wire\";\n\n/** What a link needs from the adapter that owns it. @internal */\nexport interface LinkHost {\n readonly reconnectMinMs: number;\n readonly reconnectMaxMs: number;\n /** Ping period; 0 turns pings off. */\n readonly keepAliveMs: number;\n readonly connectTimeoutMs: number;\n /** The most events queued for one listener. */\n readonly maxQueued: number;\n now(): number;\n random(): number;\n /** Opens the subscribe socket (not yet accepted). */\n open(channel: string, after: number | undefined): Promise<ClientSocket>;\n /** Runs `task` once after `ms`; returns the way to cancel. Everything scheduled is canceled when the adapter closes. */\n after(ms: number, task: () => void): () => void;\n /** Runs `task` every `ms`; returns the way to cancel. */\n every(ms: number, task: () => void): () => void;\n report(error: unknown, where: string, channel: string): void;\n /** Called once when the link has nothing left to do. */\n forget(link: ChannelLink): void;\n}\n\ninterface Deferred {\n readonly promise: Promise<void>;\n resolve(): void;\n reject(error: unknown): void;\n}\n\nfunction deferred(): Deferred {\n let resolve: () => void = () => undefined;\n let reject: (error: unknown) => void = () => undefined;\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve;\n reject = onReject;\n });\n // A waiter may give up first; the rejection must not become an unhandled one.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A queue and the loop that empties it, in the request that created it. */\nclass Pump<T> {\n readonly #queue: T[] = [];\n readonly #max: number;\n readonly #onDrop: () => void;\n #wake: (() => void) | undefined;\n #stopped = false;\n\n /**\n * @param max The most items waiting for the loop.\n * @param onDrop Called for every item refused because the queue is full, so the owner can report it and repair what the\n * listener missed. It runs inside `push`, so it must not call back into this pump.\n */\n constructor(max: number, onDrop: () => void) {\n this.#max = max;\n this.#onDrop = onDrop;\n }\n\n push(item: T): void {\n if (this.#stopped) return;\n if (this.#queue.length >= this.#max) {\n this.#onDrop();\n return;\n }\n this.#queue.push(item);\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n stop(): void {\n this.#stopped = true;\n this.#queue.length = 0;\n const wake = this.#wake;\n this.#wake = undefined;\n wake?.();\n }\n\n async run(\n deliver: (item: T) => void,\n report: (error: unknown) => void,\n every: (() => () => void) | undefined,\n idle?: () => void,\n ): Promise<void> {\n const cancel = every?.();\n try {\n while (!this.#stopped) {\n const item = this.#queue.shift();\n if (item === undefined) {\n // The queue just emptied: the owner may refill it (to repair what a full queue refused) before the loop sleeps.\n idle?.();\n if (this.#queue.length > 0) continue;\n await new Promise<void>((resolve) => {\n this.#wake = resolve;\n });\n continue;\n }\n try {\n deliver(item);\n } catch (error) {\n report(error);\n }\n }\n } finally {\n cancel?.();\n }\n }\n}\n\ninterface MessageSub {\n readonly listener: (message: StoredMessage) => void;\n readonly pump: Pump<StoredMessage>;\n /** The highest seq queued for this listener; -1 until the link tells the head. */\n last: number;\n /** True from the first message refused by a full queue until the queue has emptied (one report per overflow). */\n overflowed: boolean;\n}\n\ninterface PresenceSub {\n readonly listener: (event: PresenceEvent) => void;\n readonly pump: Pump<PresenceEvent>;\n /** The members this listener has been given: what a resync is computed against. */\n readonly view: Map<string, Member>;\n /** True after an event was refused by a full queue and until the resync has been queued. */\n stale: boolean;\n}\n\n/** A way to stop one listener. @internal */\nexport type LinkSubscription = MessageSub | PresenceSub;\n\ntype LinkState = \"idle\" | \"connecting\" | \"open\" | \"backoff\" | \"disposed\";\n\n/** @internal */\nexport class ChannelLink {\n readonly channel: string;\n readonly #host: LinkHost;\n readonly #messages = new Set<MessageSub>();\n readonly #presence = new Set<PresenceSub>();\n #state: LinkState = \"idle\";\n #socket: ClientSocket | undefined;\n #closeOwner: Deferred | undefined;\n #attempt = 0;\n /** The highest seq received on this link: where a reconnect resumes. */\n #last = 0;\n #known = false;\n #members: Map<string, Member> | undefined;\n #lastReceived = 0;\n #failures = 0;\n #retryAt = 0;\n #connectedAt = 0;\n #ready: Deferred = deferred();\n #cancelRetry: (() => void) | undefined;\n #cancelConnect: (() => void) | undefined;\n #cancelKeep: (() => void) | undefined;\n\n constructor(channel: string, host: LinkHost) {\n this.channel = channel;\n this.#host = host;\n }\n\n get state(): LinkState {\n return this.#state;\n }\n\n get size(): number {\n return this.#messages.size + this.#presence.size;\n }\n\n // --- listeners -----------------------------------------------------------------------------------------------------\n\n /** Adds a message listener (call it in the request that will receive messages). */\n addMessages(listener: (message: StoredMessage) => void): LinkSubscription {\n const sub: MessageSub = {\n listener,\n pump: new Pump(this.#host.maxQueued, () => this.#dropped(sub)),\n last: this.#state === \"open\" ? this.#last : -1,\n overflowed: false,\n };\n this.#messages.add(sub);\n // A refused message is not retried here: the listener's next message has a gap before it, which the hub fills from history.\n void sub.pump.run(\n listener,\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n () => {\n sub.overflowed = false;\n },\n );\n this.#start();\n return sub;\n }\n\n /** Adds a presence listener (call it in the request that will receive events). */\n addPresence(listener: (event: PresenceEvent) => void): LinkSubscription {\n const sub: PresenceSub = {\n listener,\n pump: new Pump(this.#host.maxQueued, () => this.#dropped(sub)),\n view: new Map(),\n stale: false,\n };\n this.#presence.add(sub);\n void sub.pump.run(\n (event) => {\n // The view follows what the listener was given, even when it throws on it.\n if (event.op === \"leave\") sub.view.delete(event.member.id);\n else sub.view.set(event.member.id, event.member);\n listener(event);\n },\n (error) => this.#host.report(error, \"listener\", this.channel),\n this.#nudger(),\n () => this.#resync(sub),\n );\n this.#start();\n return sub;\n }\n\n /** A listener's queue refused an item: reported once per overflow, and a presence listener is marked for a resync. */\n #dropped(sub: MessageSub | PresenceSub): void {\n if (\"view\" in sub) {\n if (sub.stale) return;\n sub.stale = true;\n } else {\n if (sub.overflowed) return;\n sub.overflowed = true;\n }\n this.#host.report(\n new RealtimeError(\n \"limit\",\n \"a listener fell behind: its queue is full and events were dropped\",\n ),\n \"queue\",\n this.channel,\n );\n }\n\n /**\n * Called when a presence listener has caught up: after a drop it is sent the difference between what it was given and the\n * member set the link holds (leaves, joins, changes), so one overflow cannot leave its picture of the room wrong for good.\n * When the difference is larger than the queue, the rest follows at the next catch-up.\n */\n #resync(sub: PresenceSub): void {\n if (!sub.stale) return;\n sub.stale = false;\n const current = this.#members ?? new Map<string, Member>();\n for (const [id, member] of sub.view)\n if (!current.has(id)) sub.pump.push({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of current) {\n const seen = sub.view.get(id);\n if (seen === undefined) sub.pump.push({ channel: this.channel, op: \"join\", member });\n else if (member.v > seen.v) sub.pump.push({ channel: this.channel, op: \"set\", member });\n }\n }\n\n /** Removes a listener; the last one closes the link. */\n remove(sub: LinkSubscription): void {\n sub.pump.stop();\n this.#messages.delete(sub as MessageSub);\n this.#presence.delete(sub as PresenceSub);\n if (this.size === 0) this.dispose(new RealtimeError(\"closed\", \"the subscription ended\"));\n }\n\n /** Resolves when the socket is open and has told the channel's head; rejects after `ms`. */\n whenReady(ms: number): Promise<void> {\n if (this.#state === \"open\") return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const cancel = this.#host.after(ms, () =>\n reject(new RealtimeError(\"unavailable\", \"timed out connecting to the channel object\")),\n );\n this.#ready.promise.then(\n () => {\n cancel();\n resolve();\n },\n (error: unknown) => {\n cancel();\n reject(error);\n },\n );\n });\n }\n\n // --- connecting ----------------------------------------------------------------------------------------------------\n\n /** The pump's periodic check, if pings are on. */\n #nudger(): (() => () => void) | undefined {\n const every = this.#host.keepAliveMs;\n if (every <= 0) return undefined;\n return () => this.#host.every(every, () => this.#check());\n }\n\n #start(): void {\n if (this.#state === \"idle\") this.#connect();\n }\n\n #connect(): void {\n if (this.#state === \"disposed\") return;\n this.#cancelRetry?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect?.();\n this.#state = \"connecting\";\n const attempt = ++this.#attempt;\n this.#connectedAt = this.#host.now();\n this.#cancelConnect = this.#host.after(this.#host.connectTimeoutMs, () => {\n if (attempt === this.#attempt && this.#state === \"connecting\")\n this.#down(attempt, new RealtimeError(\"unavailable\", \"the channel object did not answer\"));\n });\n this.#host.open(this.channel, this.#known ? this.#last : undefined).then(\n (socket) => this.#opened(attempt, socket),\n (error: unknown) => {\n if (attempt === this.#attempt) this.#down(attempt, error);\n },\n );\n }\n\n #opened(attempt: number, socket: ClientSocket): void {\n if (attempt !== this.#attempt || this.#state !== \"connecting\") {\n this.#closeSocket(socket, undefined);\n return;\n }\n const owner = deferred();\n this.#closeOwner = owner;\n this.#socket = socket;\n // Waits in the request that opened the socket, which is the one place guaranteed to be allowed to close it.\n void owner.promise.then(\n () => this.#tryClose(socket),\n () => undefined,\n );\n socket.addEventListener(\"message\", (event) => {\n if (attempt !== this.#attempt || typeof event.data !== \"string\") return;\n this.#received(attempt, event.data);\n });\n socket.addEventListener(\"close\", () => {\n if (attempt === this.#attempt)\n this.#down(attempt, new RealtimeError(\"unavailable\", \"closed\"));\n });\n socket.addEventListener(\"error\", () => {\n if (attempt === this.#attempt) this.#down(attempt, new RealtimeError(\"unavailable\", \"error\"));\n });\n try {\n socket.accept();\n } catch (error) {\n this.#down(attempt, error);\n }\n }\n\n #tryClose(socket: ClientSocket): boolean {\n try {\n socket.close(1000, \"done\");\n return true;\n } catch {\n return false;\n }\n }\n\n /** Closes a socket from wherever this runs; when the runtime refuses, the request that opened it does it. */\n #closeSocket(socket: ClientSocket | undefined, owner: Deferred | undefined): void {\n if (socket === undefined) return;\n if (!this.#tryClose(socket)) owner?.resolve();\n }\n\n /** The socket failed or closed: schedule a new attempt, or end the link when nobody listens. */\n #down(attempt: number, error: unknown): void {\n if (attempt !== this.#attempt || this.#state === \"disposed\") return;\n this.#attempt += 1;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n // Waiters keep the promise they hold until the link is open; only an opened link needs a new one.\n if (this.#state === \"open\") this.#ready = deferred();\n if (this.size === 0) {\n this.dispose(error);\n return;\n }\n this.#state = \"backoff\";\n this.#failures += 1;\n const ceiling = Math.min(\n this.#host.reconnectMaxMs,\n this.#host.reconnectMinMs * 2 ** Math.min(this.#failures - 1, 20),\n );\n const delay = Math.floor(this.#host.random() * ceiling);\n this.#retryAt = this.#host.now() + delay;\n this.#cancelRetry?.();\n this.#cancelRetry = this.#host.after(delay, () => this.#connect());\n }\n\n /** Reconnects now, without waiting: the object asked for it (a long replay is continued with a new socket). */\n #resume(attempt: number): void {\n if (attempt !== this.#attempt) return;\n this.#attempt += 1;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#cancelKeep?.();\n this.#cancelKeep = undefined;\n this.#closeSocket(socket, owner);\n this.#state = \"idle\";\n this.#connect();\n }\n\n /**\n * A health check run by every listener's loop (each in its own request) and by the ping timer: whichever request is alive\n * can repair a link whose own request is gone, because a timer or a socket handler of a finished request never runs again.\n */\n #check(): void {\n const now = this.#host.now();\n const grace = Math.max(1000, this.#host.keepAliveMs);\n switch (this.#state) {\n case \"backoff\":\n if (now > this.#retryAt + grace) this.#connect();\n return;\n case \"connecting\":\n if (now > this.#connectedAt + this.#host.connectTimeoutMs * 2) this.#connect();\n return;\n case \"open\": {\n const keep = this.#host.keepAliveMs;\n if (keep > 0 && now - this.#lastReceived > keep * 3) {\n this.#down(\n this.#attempt,\n new RealtimeError(\"unavailable\", \"the channel object went silent\"),\n );\n return;\n }\n try {\n this.#socket?.send(\"ping\");\n } catch {\n // Sending from another request is refused; the owner's own ping covers it.\n }\n return;\n }\n default:\n return;\n }\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n #received(attempt: number, text: string): void {\n this.#lastReceived = this.#host.now();\n if (text === \"pong\") return;\n const frame = decodeLinkFrame(text);\n if (frame === undefined) return;\n switch (frame.t) {\n case \"ready\": {\n if (this.#known && frame.head < this.#last) {\n // The channel's numbering restarted (it was forgotten): follow it.\n this.#last = frame.head;\n for (const sub of this.#messages) sub.last = Math.min(sub.last, frame.head);\n }\n if (!this.#known) {\n this.#last = frame.head;\n this.#known = true;\n }\n for (const sub of this.#messages) if (sub.last < 0) sub.last = frame.head;\n this.#state = \"open\";\n this.#failures = 0;\n this.#cancelConnect?.();\n this.#cancelConnect = undefined;\n this.#lastReceived = this.#host.now();\n if (this.#host.keepAliveMs > 0) {\n this.#cancelKeep?.();\n this.#cancelKeep = this.#host.every(this.#host.keepAliveMs, () => this.#check());\n }\n this.#ready.resolve();\n return;\n }\n case \"snap\": {\n const next = new Map<string, Member>();\n for (const member of frame.members) next.set(member.id, member);\n const before = this.#members;\n this.#members = next;\n if (before === undefined) return;\n for (const [id, member] of before)\n if (!next.has(id)) this.#fanPresence({ channel: this.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const old = before.get(id);\n if (old === undefined) this.#fanPresence({ channel: this.channel, op: \"join\", member });\n else if (member.v > old.v)\n this.#fanPresence({ channel: this.channel, op: \"set\", member });\n }\n return;\n }\n case \"msg\": {\n const message = frame.m;\n if (message.seq > this.#last) this.#last = message.seq;\n for (const sub of this.#messages)\n if (message.seq > sub.last) {\n sub.last = message.seq;\n sub.pump.push(message);\n }\n return;\n }\n case \"pres\": {\n const members = this.#members ?? new Map<string, Member>();\n this.#members = members;\n const known = members.get(frame.member.id);\n if (frame.op === \"leave\") {\n if (known === undefined) return;\n members.delete(frame.member.id);\n } else {\n if (known !== undefined && known.v >= frame.member.v && frame.op === \"join\") return;\n if (known !== undefined && known.v > frame.member.v) return;\n members.set(frame.member.id, frame.member);\n }\n this.#fanPresence({ channel: this.channel, op: frame.op, member: frame.member });\n return;\n }\n case \"more\":\n this.#resume(attempt);\n return;\n case \"bye\":\n this.#down(attempt, new RealtimeError(\"unavailable\", frame.why));\n return;\n }\n }\n\n #fanPresence(event: PresenceEvent): void {\n for (const sub of this.#presence) sub.pump.push(event);\n }\n\n // --- ending --------------------------------------------------------------------------------------------------------\n\n /** Ends the link: stops every listener, cancels timers and closes the socket. */\n dispose(reason: unknown): void {\n if (this.#state === \"disposed\") return;\n this.#state = \"disposed\";\n this.#attempt += 1;\n this.#cancelRetry?.();\n this.#cancelConnect?.();\n this.#cancelKeep?.();\n this.#cancelRetry = undefined;\n this.#cancelConnect = undefined;\n this.#cancelKeep = undefined;\n const socket = this.#socket;\n const owner = this.#closeOwner;\n this.#socket = undefined;\n this.#closeOwner = undefined;\n this.#closeSocket(socket, owner);\n this.#ready.reject(reason);\n for (const sub of this.#messages) sub.pump.stop();\n for (const sub of this.#presence) sub.pump.stop();\n this.#messages.clear();\n this.#presence.clear();\n this.#host.forget(this);\n }\n}\n"],"mappings":"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"}
|
package/dist/dev/memory.d.ts
CHANGED
|
@@ -11,6 +11,17 @@ export interface MemoryAdapterOptions {
|
|
|
11
11
|
* numbering restarts, which a resuming client sees as `reset`. Default: one hour.
|
|
12
12
|
*/
|
|
13
13
|
readonly idleMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Receives a listener's failure (a message or presence listener that threw). Delivery to the other listeners goes on either
|
|
16
|
+
* way. `createHub` passes its own `onError` here. Default: `console.error`, so a bug in a listener is never silent.
|
|
17
|
+
*/
|
|
18
|
+
readonly onError?: (error: unknown, context: MemoryErrorContext) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Where an error reported by the memory adapter came from. @since 1.0.0 */
|
|
21
|
+
export interface MemoryErrorContext {
|
|
22
|
+
/** `"listener"` (a message listener) or `"presenceListener"`. */
|
|
23
|
+
readonly where: "listener" | "presenceListener";
|
|
24
|
+
readonly channel: string;
|
|
14
25
|
}
|
|
15
26
|
/**
|
|
16
27
|
* The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory
|
package/dist/dev/memory.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// src/memory.ts
|
|
2
|
+
/** The most idempotency keys one channel remembers; past it the oldest are forgotten, which only lets a very old resend through. */
|
|
3
|
+
const MAX_KEYS = 65536;
|
|
4
|
+
const KEYS_SWEEP_FROM = 4096;
|
|
2
5
|
/**
|
|
3
6
|
* The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory
|
|
4
7
|
* of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis
|
|
@@ -17,6 +20,14 @@
|
|
|
17
20
|
function memoryAdapter(options = {}) {
|
|
18
21
|
const now = options.now ?? Date.now;
|
|
19
22
|
const idleMs = options.idleMs ?? 36e5;
|
|
23
|
+
const report = (error, context) => {
|
|
24
|
+
try {
|
|
25
|
+
if (options.onError === void 0) console.error("[realtime] memory adapter", context, error);
|
|
26
|
+
else options.onError(error, context);
|
|
27
|
+
} catch (reporter) {
|
|
28
|
+
console.error("[realtime] memory adapter onError", reporter);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
20
31
|
let sweepAt = 1024;
|
|
21
32
|
const channels = /* @__PURE__ */ new Map();
|
|
22
33
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -42,6 +53,7 @@ function memoryAdapter(options = {}) {
|
|
|
42
53
|
seq: 0,
|
|
43
54
|
log: [],
|
|
44
55
|
keys: /* @__PURE__ */ new Map(),
|
|
56
|
+
keysSweepAt: KEYS_SWEEP_FROM,
|
|
45
57
|
listeners: /* @__PURE__ */ new Set(),
|
|
46
58
|
pending: [],
|
|
47
59
|
draining: false
|
|
@@ -51,14 +63,23 @@ function memoryAdapter(options = {}) {
|
|
|
51
63
|
state.lastActive = now();
|
|
52
64
|
return state;
|
|
53
65
|
}
|
|
66
|
+
/** Calls each listener with `value`. A listener's failure is its own: delivery to the others goes on (guarantee G13). */
|
|
67
|
+
function deliver(listeners, value, context) {
|
|
68
|
+
for (const listener of [...listeners]) try {
|
|
69
|
+
listener(value);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
report(error, context);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
54
74
|
/** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */
|
|
55
|
-
function drain(state) {
|
|
75
|
+
function drain(state, channel) {
|
|
56
76
|
if (state.draining) return;
|
|
57
77
|
state.draining = true;
|
|
58
78
|
try {
|
|
59
|
-
for (let next = state.pending.shift(); next !== void 0; next = state.pending.shift())
|
|
60
|
-
listener
|
|
61
|
-
|
|
79
|
+
for (let next = state.pending.shift(); next !== void 0; next = state.pending.shift()) deliver(state.listeners, next, {
|
|
80
|
+
where: "listener",
|
|
81
|
+
channel
|
|
82
|
+
});
|
|
62
83
|
} finally {
|
|
63
84
|
state.draining = false;
|
|
64
85
|
}
|
|
@@ -68,9 +89,10 @@ function memoryAdapter(options = {}) {
|
|
|
68
89
|
if (presenceDraining) return;
|
|
69
90
|
presenceDraining = true;
|
|
70
91
|
try {
|
|
71
|
-
for (let next = presenceQueue.shift(); next !== void 0; next = presenceQueue.shift())
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
for (let next = presenceQueue.shift(); next !== void 0; next = presenceQueue.shift()) deliver(presenceListeners.get(next.channel) ?? [], next, {
|
|
93
|
+
where: "presenceListener",
|
|
94
|
+
channel: next.channel
|
|
95
|
+
});
|
|
74
96
|
} finally {
|
|
75
97
|
presenceDraining = false;
|
|
76
98
|
}
|
|
@@ -103,8 +125,14 @@ function memoryAdapter(options = {}) {
|
|
|
103
125
|
seq: known.seq,
|
|
104
126
|
duplicate: true
|
|
105
127
|
};
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
const { keys } = state;
|
|
129
|
+
if (keys.size >= state.keysSweepAt) {
|
|
130
|
+
for (const [key, value] of keys) if (value.expires <= at) keys.delete(key);
|
|
131
|
+
state.keysSweepAt = Math.max(KEYS_SWEEP_FROM, keys.size * 2);
|
|
132
|
+
}
|
|
133
|
+
for (const key of keys.keys()) {
|
|
134
|
+
if (keys.size < MAX_KEYS) break;
|
|
135
|
+
keys.delete(key);
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
state.seq += 1;
|
|
@@ -122,7 +150,7 @@ function memoryAdapter(options = {}) {
|
|
|
122
150
|
trim(state, publish.history, publish.historyTtlMs);
|
|
123
151
|
} else state.log = [];
|
|
124
152
|
state.pending.push(message);
|
|
125
|
-
drain(state);
|
|
153
|
+
drain(state, channel);
|
|
126
154
|
return {
|
|
127
155
|
seq: message.seq,
|
|
128
156
|
duplicate: false
|
package/dist/dev/memory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.js","names":[],"sources":["../../src/memory.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\n\ninterface ChannelState {\n seq: number;\n log: StoredMessage[];\n readonly keys: Map<string, { readonly seq: number; readonly expires: number }>;\n readonly listeners: Set<(message: StoredMessage) => void>;\n readonly pending: StoredMessage[];\n draining: boolean;\n lastActive: number;\n}\n\ninterface PresenceEntry {\n member: Member;\n expires: number;\n}\n\n/**\n * Options of {@link memoryAdapter}.\n * @since 1.0.0\n */\nexport interface MemoryAdapterOptions {\n /** The clock, in epoch milliseconds. Default: `Date.now`. */\n readonly now?: () => number;\n /**\n * Milliseconds a channel with no listener and no retained message is remembered. After that its head is forgotten and its\n * numbering restarts, which a resuming client sees as `reset`. Default: one hour.\n */\n readonly idleMs?: number;\n}\n\n/**\n * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory\n * of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis\n * or Cloudflare adapter when more than one process serves clients.\n *\n * @param options The clock.\n * @returns The adapter.\n * @example\n * import { createHub, memoryAdapter } from \"@shadow-js/realtime/server\";\n *\n * const adapter = memoryAdapter();\n * const hub = createHub({ adapter });\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): Adapter {\n const now = options.now ?? Date.now;\n const idleMs = options.idleMs ?? 3_600_000;\n let sweepAt = 1024;\n const channels = new Map<string, ChannelState>();\n const presence = new Map<string, Map<string, PresenceEntry>>();\n const presenceListeners = new Map<string, Set<(event: PresenceEvent) => void>>();\n const presenceQueue: PresenceEvent[] = [];\n let presenceDraining = false;\n let closed = false;\n\n /** Forgets channels nobody listens to and nothing retains; runs when the table has doubled since the last sweep. */\n function sweep(): void {\n const cutoff = now() - idleMs;\n for (const [name, state] of channels)\n if (state.listeners.size === 0 && state.lastActive < cutoff) {\n trim(state, state.log.length, idleMs);\n if (state.log.length === 0) channels.delete(name);\n }\n sweepAt = Math.max(1024, channels.size * 2);\n }\n\n function channelOf(name: string): ChannelState {\n let state = channels.get(name);\n if (state === undefined) {\n if (channels.size >= sweepAt) sweep();\n state = {\n lastActive: now(),\n seq: 0,\n log: [],\n keys: new Map(),\n listeners: new Set(),\n pending: [],\n draining: false,\n };\n channels.set(name, state);\n }\n state.lastActive = now();\n return state;\n }\n\n /** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */\n function drain(state: ChannelState): void {\n if (state.draining) return;\n state.draining = true;\n try {\n for (let next = state.pending.shift(); next !== undefined; next = state.pending.shift()) {\n for (const listener of [...state.listeners]) {\n try {\n listener(next);\n } catch {\n // A listener's failure is its own: delivery to the others goes on (guarantee G13).\n }\n }\n }\n } finally {\n state.draining = false;\n }\n }\n\n function emitPresence(event: PresenceEvent): void {\n presenceQueue.push(event);\n if (presenceDraining) return;\n presenceDraining = true;\n try {\n for (let next = presenceQueue.shift(); next !== undefined; next = presenceQueue.shift()) {\n for (const listener of [...(presenceListeners.get(next.channel) ?? [])]) {\n try {\n listener(next);\n } catch {\n // See `drain`.\n }\n }\n }\n } finally {\n presenceDraining = false;\n }\n }\n\n function trim(state: ChannelState, history: number, ttl: number): void {\n const cutoff = now() - ttl;\n let drop = Math.max(0, state.log.length - history);\n while (drop < state.log.length && (state.log[drop]?.at ?? Infinity) < cutoff) drop += 1;\n if (drop > 0) state.log = state.log.slice(drop);\n }\n\n function live(name: string): Map<string, PresenceEntry> {\n let set = presence.get(name);\n if (set === undefined) {\n set = new Map();\n presence.set(name, set);\n }\n return set;\n }\n\n function check(): void {\n if (closed) throw new Error(\"the adapter is closed.\");\n }\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n check();\n const state = channelOf(channel);\n const at = now();\n if (publish.key !== undefined) {\n const known = state.keys.get(publish.key);\n if (known !== undefined && known.expires > at) return { seq: known.seq, duplicate: true };\n // Expired keys are swept when a new one is added, so the map stays bounded by the rate of publishes.\n if (state.keys.size >= 4096)\n for (const [key, value] of state.keys) if (value.expires <= at) state.keys.delete(key);\n }\n state.seq += 1;\n const message: StoredMessage = { ...entry, seq: state.seq, at };\n if (publish.key !== undefined)\n state.keys.set(publish.key, {\n seq: state.seq,\n expires: at + publish.idempotencyTtlMs,\n });\n if (publish.history > 0) {\n state.log.push(message);\n trim(state, publish.history, publish.historyTtlMs);\n } else {\n state.log = [];\n }\n state.pending.push(message);\n drain(state);\n return { seq: message.seq, duplicate: false };\n },\n\n async subscribe(\n channel: string,\n listener: (message: StoredMessage) => void,\n ): Promise<Unsubscribe> {\n check();\n const state = channelOf(channel);\n state.listeners.add(listener);\n return async () => {\n state.listeners.delete(listener);\n };\n },\n\n async history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n check();\n const state = channels.get(channel);\n if (state === undefined) return { messages: [], head: 0, oldest: 0 };\n const retained = state.log;\n const messages: StoredMessage[] = [];\n for (const message of retained) {\n if (messages.length >= limit) break;\n if (message.seq > after) messages.push(message);\n }\n return { messages, head: state.seq, oldest: retained[0]?.seq ?? 0 };\n },\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n check();\n const set = live(channel);\n const known = set.get(member.id);\n if (known !== undefined && known.member.v > member.v) return;\n set.set(member.id, { member, expires: now() + ttlMs });\n if (known === undefined) emitPresence({ channel, op: \"join\", member });\n else if (known.member.v < member.v) emitPresence({ channel, op: \"set\", member });\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n check();\n const set = presence.get(channel);\n const known = set?.get(id);\n if (set === undefined || known === undefined) return false;\n set.delete(id);\n if (set.size === 0) presence.delete(channel);\n emitPresence({ channel, op: \"leave\", member: known.member });\n return true;\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n check();\n const at = now();\n const out: Member[] = [];\n for (const entry of presence.get(channel)?.values() ?? [])\n if (entry.expires > at) out.push(entry.member);\n return out;\n },\n\n async presenceTouch(entries, ttlMs): Promise<readonly PresenceRef[]> {\n check();\n const expires = now() + ttlMs;\n const missing: PresenceRef[] = [];\n for (const ref of entries) {\n const entry = presence.get(ref.channel)?.get(ref.id);\n if (entry === undefined) missing.push(ref);\n else entry.expires = expires;\n }\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n check();\n const set = presence.get(channel);\n if (set === undefined) return [];\n const at = now();\n const removed: Member[] = [];\n for (const [id, entry] of [...set]) {\n if (entry.expires > at) continue;\n set.delete(id);\n removed.push(entry.member);\n emitPresence({ channel, op: \"leave\", member: entry.member });\n }\n if (set.size === 0) presence.delete(channel);\n return removed;\n },\n\n async presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n check();\n let set = presenceListeners.get(channel);\n if (set === undefined) {\n set = new Set();\n presenceListeners.set(channel, set);\n }\n set.add(listener);\n const owner = set;\n return async () => {\n owner.delete(listener);\n if (owner.size === 0 && presenceListeners.get(channel) === owner)\n presenceListeners.delete(channel);\n };\n },\n\n async close(): Promise<void> {\n closed = true;\n channels.clear();\n presence.clear();\n presenceListeners.clear();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"memory.js","names":[],"sources":["../../src/memory.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\n\ninterface ChannelState {\n seq: number;\n log: StoredMessage[];\n readonly keys: Map<string, { readonly seq: number; readonly expires: number }>;\n /** The size of `keys` at which expired entries are swept next. */\n keysSweepAt: number;\n readonly listeners: Set<(message: StoredMessage) => void>;\n readonly pending: StoredMessage[];\n draining: boolean;\n lastActive: number;\n}\n\n/** The most idempotency keys one channel remembers; past it the oldest are forgotten, which only lets a very old resend through. */\nconst MAX_KEYS = 65_536;\nconst KEYS_SWEEP_FROM = 4096;\n\ninterface PresenceEntry {\n member: Member;\n expires: number;\n}\n\n/**\n * Options of {@link memoryAdapter}.\n * @since 1.0.0\n */\nexport interface MemoryAdapterOptions {\n /** The clock, in epoch milliseconds. Default: `Date.now`. */\n readonly now?: () => number;\n /**\n * Milliseconds a channel with no listener and no retained message is remembered. After that its head is forgotten and its\n * numbering restarts, which a resuming client sees as `reset`. Default: one hour.\n */\n readonly idleMs?: number;\n /**\n * Receives a listener's failure (a message or presence listener that threw). Delivery to the other listeners goes on either\n * way. `createHub` passes its own `onError` here. Default: `console.error`, so a bug in a listener is never silent.\n */\n readonly onError?: (error: unknown, context: MemoryErrorContext) => void;\n}\n\n/** Where an error reported by the memory adapter came from. @since 1.0.0 */\nexport interface MemoryErrorContext {\n /** `\"listener\"` (a message listener) or `\"presenceListener\"`. */\n readonly where: \"listener\" | \"presenceListener\";\n readonly channel: string;\n}\n\n/**\n * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory\n * of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis\n * or Cloudflare adapter when more than one process serves clients.\n *\n * @param options The clock.\n * @returns The adapter.\n * @example\n * import { createHub, memoryAdapter } from \"@shadow-js/realtime/server\";\n *\n * const adapter = memoryAdapter();\n * const hub = createHub({ adapter });\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): Adapter {\n const now = options.now ?? Date.now;\n const idleMs = options.idleMs ?? 3_600_000;\n const report = (error: unknown, context: MemoryErrorContext): void => {\n try {\n if (options.onError === undefined) console.error(\"[realtime] memory adapter\", context, error);\n else options.onError(error, context);\n } catch (reporter) {\n // A reporter that throws must not stop delivery; its failure is the last resort for the console.\n console.error(\"[realtime] memory adapter onError\", reporter);\n }\n };\n let sweepAt = 1024;\n const channels = new Map<string, ChannelState>();\n const presence = new Map<string, Map<string, PresenceEntry>>();\n const presenceListeners = new Map<string, Set<(event: PresenceEvent) => void>>();\n const presenceQueue: PresenceEvent[] = [];\n let presenceDraining = false;\n let closed = false;\n\n /** Forgets channels nobody listens to and nothing retains; runs when the table has doubled since the last sweep. */\n function sweep(): void {\n const cutoff = now() - idleMs;\n for (const [name, state] of channels)\n if (state.listeners.size === 0 && state.lastActive < cutoff) {\n trim(state, state.log.length, idleMs);\n if (state.log.length === 0) channels.delete(name);\n }\n sweepAt = Math.max(1024, channels.size * 2);\n }\n\n function channelOf(name: string): ChannelState {\n let state = channels.get(name);\n if (state === undefined) {\n if (channels.size >= sweepAt) sweep();\n state = {\n lastActive: now(),\n seq: 0,\n log: [],\n keys: new Map(),\n keysSweepAt: KEYS_SWEEP_FROM,\n listeners: new Set(),\n pending: [],\n draining: false,\n };\n channels.set(name, state);\n }\n state.lastActive = now();\n return state;\n }\n\n /** Calls each listener with `value`. A listener's failure is its own: delivery to the others goes on (guarantee G13). */\n function deliver<T>(\n listeners: Iterable<(value: T) => void>,\n value: T,\n context: MemoryErrorContext,\n ): void {\n for (const listener of [...listeners]) {\n try {\n listener(value);\n } catch (error) {\n report(error, context);\n }\n }\n }\n\n /** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */\n function drain(state: ChannelState, channel: string): void {\n if (state.draining) return;\n state.draining = true;\n try {\n for (let next = state.pending.shift(); next !== undefined; next = state.pending.shift()) {\n deliver(state.listeners, next, { where: \"listener\", channel });\n }\n } finally {\n state.draining = false;\n }\n }\n\n function emitPresence(event: PresenceEvent): void {\n presenceQueue.push(event);\n if (presenceDraining) return;\n presenceDraining = true;\n try {\n for (let next = presenceQueue.shift(); next !== undefined; next = presenceQueue.shift()) {\n deliver(presenceListeners.get(next.channel) ?? [], next, {\n where: \"presenceListener\",\n channel: next.channel,\n });\n }\n } finally {\n presenceDraining = false;\n }\n }\n\n function trim(state: ChannelState, history: number, ttl: number): void {\n const cutoff = now() - ttl;\n let drop = Math.max(0, state.log.length - history);\n while (drop < state.log.length && (state.log[drop]?.at ?? Infinity) < cutoff) drop += 1;\n if (drop > 0) state.log = state.log.slice(drop);\n }\n\n function live(name: string): Map<string, PresenceEntry> {\n let set = presence.get(name);\n if (set === undefined) {\n set = new Map();\n presence.set(name, set);\n }\n return set;\n }\n\n function check(): void {\n if (closed) throw new Error(__DEV__ ? \"the adapter is closed.\" : \"REALTIME_CLOSED\");\n }\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n check();\n const state = channelOf(channel);\n const at = now();\n if (publish.key !== undefined) {\n const known = state.keys.get(publish.key);\n if (known !== undefined && known.expires > at) return { seq: known.seq, duplicate: true };\n // Expired keys are swept each time the map has doubled, so a sweep costs a publish little; the cap holds when none expired.\n const { keys } = state;\n if (keys.size >= state.keysSweepAt) {\n for (const [key, value] of keys) if (value.expires <= at) keys.delete(key);\n state.keysSweepAt = Math.max(KEYS_SWEEP_FROM, keys.size * 2);\n }\n for (const key of keys.keys()) {\n if (keys.size < MAX_KEYS) break;\n keys.delete(key);\n }\n }\n state.seq += 1;\n const message: StoredMessage = { ...entry, seq: state.seq, at };\n if (publish.key !== undefined)\n state.keys.set(publish.key, {\n seq: state.seq,\n expires: at + publish.idempotencyTtlMs,\n });\n if (publish.history > 0) {\n state.log.push(message);\n trim(state, publish.history, publish.historyTtlMs);\n } else {\n state.log = [];\n }\n state.pending.push(message);\n drain(state, channel);\n return { seq: message.seq, duplicate: false };\n },\n\n async subscribe(\n channel: string,\n listener: (message: StoredMessage) => void,\n ): Promise<Unsubscribe> {\n check();\n const state = channelOf(channel);\n state.listeners.add(listener);\n return async () => {\n state.listeners.delete(listener);\n };\n },\n\n async history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n check();\n const state = channels.get(channel);\n if (state === undefined) return { messages: [], head: 0, oldest: 0 };\n const retained = state.log;\n const messages: StoredMessage[] = [];\n for (const message of retained) {\n if (messages.length >= limit) break;\n if (message.seq > after) messages.push(message);\n }\n return { messages, head: state.seq, oldest: retained[0]?.seq ?? 0 };\n },\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n check();\n const set = live(channel);\n const known = set.get(member.id);\n if (known !== undefined && known.member.v > member.v) return;\n set.set(member.id, { member, expires: now() + ttlMs });\n if (known === undefined) emitPresence({ channel, op: \"join\", member });\n else if (known.member.v < member.v) emitPresence({ channel, op: \"set\", member });\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n check();\n const set = presence.get(channel);\n const known = set?.get(id);\n if (set === undefined || known === undefined) return false;\n set.delete(id);\n if (set.size === 0) presence.delete(channel);\n emitPresence({ channel, op: \"leave\", member: known.member });\n return true;\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n check();\n const at = now();\n const out: Member[] = [];\n for (const entry of presence.get(channel)?.values() ?? [])\n if (entry.expires > at) out.push(entry.member);\n return out;\n },\n\n async presenceTouch(entries, ttlMs): Promise<readonly PresenceRef[]> {\n check();\n const expires = now() + ttlMs;\n const missing: PresenceRef[] = [];\n for (const ref of entries) {\n const entry = presence.get(ref.channel)?.get(ref.id);\n if (entry === undefined) missing.push(ref);\n else entry.expires = expires;\n }\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n check();\n const set = presence.get(channel);\n if (set === undefined) return [];\n const at = now();\n const removed: Member[] = [];\n for (const [id, entry] of [...set]) {\n if (entry.expires > at) continue;\n set.delete(id);\n removed.push(entry.member);\n emitPresence({ channel, op: \"leave\", member: entry.member });\n }\n if (set.size === 0) presence.delete(channel);\n return removed;\n },\n\n async presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n check();\n let set = presenceListeners.get(channel);\n if (set === undefined) {\n set = new Set();\n presenceListeners.set(channel, set);\n }\n set.add(listener);\n const owner = set;\n return async () => {\n owner.delete(listener);\n if (owner.size === 0 && presenceListeners.get(channel) === owner)\n presenceListeners.delete(channel);\n };\n },\n\n async close(): Promise<void> {\n closed = true;\n channels.clear();\n presence.clear();\n presenceListeners.clear();\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/dev/protocol.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
const PROTOCOL_VERSION = 1;
|
|
11
11
|
const CHANNEL = /^[A-Za-z0-9_.:/@-]{1,128}$/;
|
|
12
12
|
const EVENT = /^[A-Za-z0-9_.:@-]{1,64}$/;
|
|
13
|
-
const KEY = /^[A-Za-z0-9_.:@-]{1,64}$/;
|
|
14
13
|
const encoder = new TextEncoder();
|
|
15
14
|
/**
|
|
16
15
|
* Whether `value` is a valid channel name: 1..128 characters of `A-Za-z0-9_.:/@-`, not starting with the reserved `$`.
|
|
@@ -42,7 +41,7 @@ function isEventName(value) {
|
|
|
42
41
|
}
|
|
43
42
|
/** Whether `value` is a valid idempotency key. @internal */
|
|
44
43
|
function isIdempotencyKey(value) {
|
|
45
|
-
return typeof value === "string" &&
|
|
44
|
+
return typeof value === "string" && EVENT.test(value);
|
|
46
45
|
}
|
|
47
46
|
/** The number of bytes of `text` in UTF-8, without encoding the common cases. @internal */
|
|
48
47
|
function byteLength(text) {
|
|
@@ -60,15 +59,11 @@ function isCount(value) {
|
|
|
60
59
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
61
60
|
}
|
|
62
61
|
function bad(why, n) {
|
|
63
|
-
return
|
|
64
|
-
ok: false,
|
|
65
|
-
code: "bad_frame",
|
|
66
|
-
n,
|
|
67
|
-
why
|
|
68
|
-
} : {
|
|
62
|
+
return {
|
|
69
63
|
ok: false,
|
|
70
64
|
code: "bad_frame",
|
|
71
|
-
why
|
|
65
|
+
why,
|
|
66
|
+
...isCount(n) ? { n } : {}
|
|
72
67
|
};
|
|
73
68
|
}
|
|
74
69
|
/**
|
|
@@ -233,12 +228,18 @@ function decodeClientFrame(text, maxBytes) {
|
|
|
233
228
|
* @since 1.0.0
|
|
234
229
|
*/
|
|
235
230
|
function decodeServerFrame(text) {
|
|
236
|
-
let value;
|
|
237
231
|
try {
|
|
238
|
-
|
|
232
|
+
return serverFrameOf(JSON.parse(text));
|
|
239
233
|
} catch {
|
|
240
234
|
return;
|
|
241
235
|
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Decodes a server frame that is already parsed (the frames of an HTTP answer arrive inside one JSON document), with the
|
|
239
|
+
* rules of {@link decodeServerFrame}.
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
function serverFrameOf(value) {
|
|
242
243
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
243
244
|
const raw = value;
|
|
244
245
|
const str = (key) => {
|
|
@@ -394,6 +395,6 @@ function decodeServerFrame(text) {
|
|
|
394
395
|
default: return;
|
|
395
396
|
}
|
|
396
397
|
}
|
|
397
|
-
export { PROTOCOL_VERSION, byteLength, decodeClientFrame, decodeServerFrame, isChannelName, isEventName, isIdempotencyKey };
|
|
398
|
+
export { PROTOCOL_VERSION, byteLength, decodeClientFrame, decodeServerFrame, isChannelName, isEventName, isIdempotencyKey, serverFrameOf };
|
|
398
399
|
|
|
399
400
|
//# sourceMappingURL=protocol.js.map
|
package/dist/dev/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","names":[],"sources":["../../src/protocol.ts"],"sourcesContent":["import type { ErrorCode } from \"./errors\";\n\n/**\n * The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).\n * @example\n * import { PROTOCOL_VERSION } from \"@shadow-js/realtime\";\n *\n * const hello = JSON.stringify({ t: \"hello\", v: PROTOCOL_VERSION });\n * @since 1.0.0\n */\nexport const PROTOCOL_VERSION: 1 = 1;\n\n/**\n * Any JSON value.\n * @since 1.0.0\n */\nexport type Json =\n | null\n | boolean\n | number\n | string\n | readonly Json[]\n | { readonly [key: string]: Json };\n\n/**\n * One member of a channel's presence set: the connection that joined, its user, its state, and the version of that state.\n * @since 1.0.0\n */\nexport interface Member<State = Json> {\n /** The connection id. */\n readonly id: string;\n /** The user of the connection's identity, when it has one. */\n readonly u?: string;\n /** The member's state. */\n readonly s: State;\n /** A counter that rises with every state change of this member, from 1. */\n readonly v: number;\n}\n\n/** A message as stored by an adapter: the data is its JSON text. @since 1.0.0 */\nexport interface StoredMessage {\n readonly seq: number;\n readonly event: string;\n /** The JSON text of the data. */\n readonly data: string;\n /** The publishing connection id, when a connection published it. */\n readonly publisher?: string;\n /** The publishing user, when it has one. */\n readonly user?: string;\n /** When the adapter accepted it, in epoch milliseconds. */\n readonly at: number;\n}\n\n/** What a publisher supplies; the adapter adds `seq` and `at`. @since 1.0.0 */\nexport type StoredEntry = Omit<StoredMessage, \"seq\" | \"at\">;\n\n/** A change of a presence set, as adapters deliver it. @since 1.0.0 */\nexport interface PresenceEvent {\n readonly channel: string;\n readonly op: \"join\" | \"set\" | \"leave\";\n readonly member: Member;\n}\n\n/** Frames a client sends. @since 1.0.0 */\nexport type ClientFrame =\n | { readonly t: \"hello\"; readonly v: number }\n | {\n readonly t: \"sub\";\n readonly n: number;\n readonly c: string;\n readonly after?: number;\n readonly echo?: boolean;\n readonly presence?: Json;\n readonly hasPresence: boolean;\n readonly watch?: boolean;\n }\n | { readonly t: \"unsub\"; readonly n?: number; readonly c: string }\n | {\n readonly t: \"pub\";\n readonly c: string;\n readonly e: string;\n readonly d: string;\n readonly n?: number;\n readonly k?: string;\n }\n | {\n readonly t: \"req\";\n readonly n: number;\n readonly c: string;\n readonly e: string;\n readonly d: Json;\n readonly ms?: number;\n }\n | { readonly t: \"pres\"; readonly c: string; readonly s: Json }\n | { readonly t: \"ping\"; readonly n?: number };\n\n/** What decoding a client frame yields. @since 1.0.0 */\nexport type DecodedClientFrame =\n | { readonly ok: true; readonly frame: ClientFrame }\n | { readonly ok: false; readonly code: ErrorCode; readonly n?: number; readonly why: string };\n\nconst CHANNEL = /^[A-Za-z0-9_.:/@-]{1,128}$/;\nconst EVENT = /^[A-Za-z0-9_.:@-]{1,64}$/;\nconst KEY = /^[A-Za-z0-9_.:@-]{1,64}$/;\nconst encoder = new TextEncoder();\n\n/**\n * Whether `value` is a valid channel name: 1..128 characters of `A-Za-z0-9_.:/@-`, not starting with the reserved `$`.\n * @param value The candidate.\n * @returns True for a valid name.\n * @example\n * import { isChannelName } from \"@shadow-js/realtime\";\n *\n * isChannelName(\"chat:lobby\"); // true\n * isChannelName(\"chat lobby\"); // false\n * @since 1.0.0\n */\nexport function isChannelName(value: unknown): value is string {\n return typeof value === \"string\" && CHANNEL.test(value);\n}\n\n/**\n * Whether `value` is a valid event name: 1..64 characters of `A-Za-z0-9_.:@-`.\n * @param value The candidate.\n * @returns True for a valid name.\n * @example\n * import { isEventName } from \"@shadow-js/realtime\";\n *\n * isEventName(\"message\"); // true\n * isEventName(\"a/b\"); // false\n * @since 1.0.0\n */\nexport function isEventName(value: unknown): value is string {\n return typeof value === \"string\" && EVENT.test(value);\n}\n\n/** Whether `value` is a valid idempotency key. @internal */\nexport function isIdempotencyKey(value: unknown): value is string {\n return typeof value === \"string\" && KEY.test(value);\n}\n\n/** The number of bytes of `text` in UTF-8, without encoding the common cases. @internal */\nexport function byteLength(text: string): number {\n // Every UTF-16 unit is at most 3 bytes in UTF-8, and at least 1.\n if (text.length * 3 <= 0x7fffffff) {\n let ascii = true;\n for (let at = 0; at < text.length; at += 1) {\n if (text.charCodeAt(at) > 0x7f) {\n ascii = false;\n break;\n }\n }\n if (ascii) return text.length;\n }\n return encoder.encode(text).length;\n}\n\nfunction isCount(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\ntype Refusal = Extract<DecodedClientFrame, { ok: false }>;\n\nfunction bad(why: string, n?: unknown): Refusal {\n return isCount(n)\n ? { ok: false, code: \"bad_frame\", n, why }\n : { ok: false, code: \"bad_frame\", why };\n}\n\n/**\n * Decodes and validates one client frame (server side). Never throws: any text yields a frame or a refusal.\n *\n * @param text The frame text.\n * @param maxBytes The largest accepted frame, in UTF-8 bytes.\n * @returns The frame, or the code and reason to refuse it with.\n * @example\n * import { decodeClientFrame } from \"@shadow-js/realtime\";\n *\n * const result = decodeClientFrame('{\"t\":\"ping\"}', 65_536);\n * result.ok; // true\n * @since 1.0.0\n */\nexport function decodeClientFrame(text: string, maxBytes: number): DecodedClientFrame {\n if (byteLength(text) > maxBytes)\n return { ok: false, code: \"too_large\", why: \"the frame is larger than the limit\" };\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return bad(\"the frame is not JSON\");\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value))\n return bad(\"the frame is not an object\");\n const raw = value as Record<string, unknown>;\n const type = raw[\"t\"];\n const n = raw[\"n\"];\n if (n !== undefined && !isCount(n)) return bad(\"n must be a non-negative integer\");\n const c = raw[\"c\"];\n const needChannel = (): Refusal | undefined =>\n isChannelName(c)\n ? undefined\n : { ...bad(\"c is not a valid channel name\", n), code: \"bad_channel\" };\n switch (type) {\n case \"hello\": {\n const v = raw[\"v\"];\n if (typeof v !== \"number\" || !Number.isInteger(v) || v < 1) return bad(\"v must be a version\");\n return { ok: true, frame: { t: \"hello\", v } };\n }\n case \"ping\":\n return { ok: true, frame: n === undefined ? { t: \"ping\" } : { t: \"ping\", n } };\n case \"sub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n if (n === undefined) return bad(\"sub needs n\");\n const after = raw[\"after\"];\n if (after !== undefined && !isCount(after))\n return bad(\"after must be a non-negative integer\", n);\n const echo = raw[\"echo\"];\n if (echo !== undefined && typeof echo !== \"boolean\") return bad(\"echo must be a boolean\", n);\n const watch = raw[\"watch\"];\n if (watch !== undefined && typeof watch !== \"boolean\")\n return bad(\"watch must be a boolean\", n);\n const hasPresence = \"presence\" in raw;\n return {\n ok: true,\n frame: {\n t: \"sub\",\n n,\n c: c as string,\n hasPresence,\n ...(after === undefined ? {} : { after }),\n ...(echo === undefined ? {} : { echo }),\n ...(watch === undefined ? {} : { watch }),\n ...(hasPresence ? { presence: raw[\"presence\"] as Json } : {}),\n },\n };\n }\n case \"unsub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n return {\n ok: true,\n frame: n === undefined ? { t: \"unsub\", c: c as string } : { t: \"unsub\", n, c: c as string },\n };\n }\n case \"pub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n const e = raw[\"e\"];\n if (!isEventName(e)) return bad(\"e is not a valid event name\", n);\n const k = raw[\"k\"];\n if (k !== undefined && !isIdempotencyKey(k)) return bad(\"k is not a valid key\", n);\n const data = JSON.stringify(raw[\"d\"] ?? null);\n return {\n ok: true,\n frame: {\n t: \"pub\",\n c: c as string,\n e,\n d: data,\n ...(n === undefined ? {} : { n }),\n ...(k === undefined ? {} : { k }),\n },\n };\n }\n case \"req\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n if (n === undefined) return bad(\"req needs n\");\n const e = raw[\"e\"];\n if (!isEventName(e)) return bad(\"e is not a valid event name\", n);\n const ms = raw[\"ms\"];\n if (ms !== undefined && !(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0))\n return bad(\"ms must be a positive number\", n);\n return {\n ok: true,\n frame: {\n t: \"req\",\n n,\n c: c as string,\n e,\n d: (raw[\"d\"] ?? null) as Json,\n ...(ms === undefined ? {} : { ms }),\n },\n };\n }\n case \"pres\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n return { ok: true, frame: { t: \"pres\", c: c as string, s: (raw[\"s\"] ?? null) as Json } };\n }\n default:\n return bad(\"unknown frame type\", n);\n }\n}\n\n/** Frames a server sends, as a client reads them. @since 1.0.0 */\nexport type ServerFrame =\n | {\n readonly t: \"ready\";\n readonly v: number;\n readonly id: string;\n readonly hb: number;\n readonly max: number;\n }\n | {\n readonly t: \"subbed\";\n readonly n: number;\n readonly c: string;\n readonly head: number;\n readonly gap?: boolean;\n readonly reset?: boolean;\n }\n | { readonly t: \"unsubbed\"; readonly c: string; readonly why?: string }\n | {\n readonly t: \"msg\";\n readonly c: string;\n readonly s: number;\n readonly e: string;\n readonly d: Json;\n readonly p?: string;\n readonly u?: string;\n readonly r?: 1;\n }\n | { readonly t: \"ack\"; readonly n: number; readonly s: number; readonly dup?: boolean }\n | { readonly t: \"rep\"; readonly n: number; readonly d: Json }\n | {\n readonly t: \"err\";\n readonly n?: number;\n readonly c?: string;\n readonly code: ErrorCode;\n readonly msg?: string;\n }\n | {\n readonly t: \"pres\";\n readonly c: string;\n readonly op: \"sync\" | \"join\" | \"set\" | \"leave\";\n readonly m: Member | readonly Member[];\n }\n | { readonly t: \"pong\"; readonly n?: number }\n | { readonly t: \"bye\"; readonly code: number; readonly why: string };\n\n/**\n * Decodes one server frame (client side). Returns `undefined` for anything that is not a known, well-formed frame, so a\n * client ignores what a newer server may send and never acts on a malformed frame.\n *\n * @param text The frame text.\n * @returns The frame, or undefined.\n * @example\n * import { decodeServerFrame } from \"@shadow-js/realtime\";\n *\n * decodeServerFrame('{\"t\":\"pong\"}'); // { t: \"pong\" }\n * decodeServerFrame(\"not json\"); // undefined\n * @since 1.0.0\n */\nexport function decodeServerFrame(text: string): ServerFrame | undefined {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const str = (key: string): string | undefined => {\n const item = raw[key];\n return typeof item === \"string\" ? item : undefined;\n };\n const num = (key: string): number | undefined => {\n const item = raw[key];\n return isCount(item) ? item : undefined;\n };\n switch (raw[\"t\"]) {\n case \"ready\": {\n const v = num(\"v\");\n const id = str(\"id\");\n if (v === undefined || id === undefined) return undefined;\n return { t: \"ready\", v, id, hb: num(\"hb\") ?? 30_000, max: num(\"max\") ?? 65_536 };\n }\n case \"subbed\": {\n const n = num(\"n\");\n const c = str(\"c\");\n const head = num(\"head\");\n if (n === undefined || c === undefined || head === undefined) return undefined;\n return {\n t: \"subbed\",\n n,\n c,\n head,\n ...(raw[\"gap\"] === true ? { gap: true } : {}),\n ...(raw[\"reset\"] === true ? { reset: true } : {}),\n };\n }\n case \"unsubbed\": {\n const c = str(\"c\");\n if (c === undefined) return undefined;\n const why = str(\"why\");\n return why === undefined ? { t: \"unsubbed\", c } : { t: \"unsubbed\", c, why };\n }\n case \"msg\": {\n const c = str(\"c\");\n const s = num(\"s\");\n const e = str(\"e\");\n if (c === undefined || s === undefined || e === undefined || s < 1) return undefined;\n const p = str(\"p\");\n const u = str(\"u\");\n return {\n t: \"msg\",\n c,\n s,\n e,\n d: raw[\"d\"] as Json,\n ...(p === undefined ? {} : { p }),\n ...(u === undefined ? {} : { u }),\n ...(raw[\"r\"] === 1 ? { r: 1 as const } : {}),\n };\n }\n case \"ack\": {\n const n = num(\"n\");\n const s = num(\"s\");\n if (n === undefined || s === undefined) return undefined;\n return { t: \"ack\", n, s, ...(raw[\"dup\"] === true ? { dup: true } : {}) };\n }\n case \"rep\": {\n const n = num(\"n\");\n if (n === undefined) return undefined;\n return { t: \"rep\", n, d: raw[\"d\"] as Json };\n }\n case \"err\": {\n const code = str(\"code\");\n if (code === undefined) return undefined;\n const n = num(\"n\");\n const c = str(\"c\");\n const msg = str(\"msg\");\n return {\n t: \"err\",\n code: code as ErrorCode,\n ...(n === undefined ? {} : { n }),\n ...(c === undefined ? {} : { c }),\n ...(msg === undefined ? {} : { msg }),\n };\n }\n case \"pres\": {\n const c = str(\"c\");\n const op = raw[\"op\"];\n const m = raw[\"m\"];\n if (c === undefined || (op !== \"sync\" && op !== \"join\" && op !== \"set\" && op !== \"leave\"))\n return undefined;\n const member = (item: unknown): Member | undefined => {\n if (item === null || typeof item !== \"object\") return undefined;\n const entry = item as Record<string, unknown>;\n if (typeof entry[\"id\"] !== \"string\" || !isCount(entry[\"v\"])) return undefined;\n return {\n id: entry[\"id\"],\n v: entry[\"v\"],\n s: (entry[\"s\"] ?? null) as Json,\n ...(typeof entry[\"u\"] === \"string\" ? { u: entry[\"u\"] } : {}),\n };\n };\n if (op === \"sync\") {\n if (!Array.isArray(m)) return undefined;\n const list = m.map(member);\n if (list.some((item) => item === undefined)) return undefined;\n return { t: \"pres\", c, op, m: list as Member[] };\n }\n const one = member(m);\n return one === undefined ? undefined : { t: \"pres\", c, op, m: one };\n }\n case \"pong\": {\n const n = num(\"n\");\n return n === undefined ? { t: \"pong\" } : { t: \"pong\", n };\n }\n case \"bye\": {\n const code = num(\"code\");\n return code === undefined ? undefined : { t: \"bye\", code, why: str(\"why\") ?? \"\" };\n }\n default:\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"protocol.js","names":[],"sources":["../../src/protocol.ts"],"sourcesContent":["import type { ErrorCode } from \"./errors\";\n\n/**\n * The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).\n * @example\n * import { PROTOCOL_VERSION } from \"@shadow-js/realtime\";\n *\n * const hello = JSON.stringify({ t: \"hello\", v: PROTOCOL_VERSION });\n * @since 1.0.0\n */\nexport const PROTOCOL_VERSION: 1 = 1;\n\n/**\n * Any JSON value.\n * @since 1.0.0\n */\nexport type Json =\n | null\n | boolean\n | number\n | string\n | readonly Json[]\n | { readonly [key: string]: Json };\n\n/**\n * One member of a channel's presence set: the connection that joined, its user, its state, and the version of that state.\n * @since 1.0.0\n */\nexport interface Member<State = Json> {\n /** The connection id. */\n readonly id: string;\n /** The user of the connection's identity, when it has one. */\n readonly u?: string;\n /** The member's state. */\n readonly s: State;\n /** A counter that rises with every state change of this member, from 1. */\n readonly v: number;\n}\n\n/** A message as stored by an adapter: the data is its JSON text. @since 1.0.0 */\nexport interface StoredMessage {\n readonly seq: number;\n readonly event: string;\n /** The JSON text of the data. */\n readonly data: string;\n /** The publishing connection id, when a connection published it. */\n readonly publisher?: string;\n /** The publishing user, when it has one. */\n readonly user?: string;\n /** When the adapter accepted it, in epoch milliseconds. */\n readonly at: number;\n}\n\n/** What a publisher supplies; the adapter adds `seq` and `at`. @since 1.0.0 */\nexport type StoredEntry = Omit<StoredMessage, \"seq\" | \"at\">;\n\n/** A change of a presence set, as adapters deliver it. @since 1.0.0 */\nexport interface PresenceEvent {\n readonly channel: string;\n readonly op: \"join\" | \"set\" | \"leave\";\n readonly member: Member;\n}\n\n/** Frames a client sends. @since 1.0.0 */\nexport type ClientFrame =\n | { readonly t: \"hello\"; readonly v: number }\n | {\n readonly t: \"sub\";\n readonly n: number;\n readonly c: string;\n readonly after?: number;\n readonly echo?: boolean;\n readonly presence?: Json;\n readonly hasPresence: boolean;\n readonly watch?: boolean;\n }\n | { readonly t: \"unsub\"; readonly n?: number; readonly c: string }\n | {\n readonly t: \"pub\";\n readonly c: string;\n readonly e: string;\n readonly d: string;\n readonly n?: number;\n readonly k?: string;\n }\n | {\n readonly t: \"req\";\n readonly n: number;\n readonly c: string;\n readonly e: string;\n readonly d: Json;\n readonly ms?: number;\n }\n | { readonly t: \"pres\"; readonly c: string; readonly s: Json }\n | { readonly t: \"ping\"; readonly n?: number };\n\n/** What decoding a client frame yields. @since 1.0.0 */\nexport type DecodedClientFrame =\n | { readonly ok: true; readonly frame: ClientFrame }\n | { readonly ok: false; readonly code: ErrorCode; readonly n?: number; readonly why: string };\n\nconst CHANNEL = /^[A-Za-z0-9_.:/@-]{1,128}$/;\n// Event names and idempotency keys share one shape.\nconst EVENT = /^[A-Za-z0-9_.:@-]{1,64}$/;\nconst encoder = new TextEncoder();\n\n/**\n * Whether `value` is a valid channel name: 1..128 characters of `A-Za-z0-9_.:/@-`, not starting with the reserved `$`.\n * @param value The candidate.\n * @returns True for a valid name.\n * @example\n * import { isChannelName } from \"@shadow-js/realtime\";\n *\n * isChannelName(\"chat:lobby\"); // true\n * isChannelName(\"chat lobby\"); // false\n * @since 1.0.0\n */\nexport function isChannelName(value: unknown): value is string {\n return typeof value === \"string\" && CHANNEL.test(value);\n}\n\n/**\n * Whether `value` is a valid event name: 1..64 characters of `A-Za-z0-9_.:@-`.\n * @param value The candidate.\n * @returns True for a valid name.\n * @example\n * import { isEventName } from \"@shadow-js/realtime\";\n *\n * isEventName(\"message\"); // true\n * isEventName(\"a/b\"); // false\n * @since 1.0.0\n */\nexport function isEventName(value: unknown): value is string {\n return typeof value === \"string\" && EVENT.test(value);\n}\n\n/** Whether `value` is a valid idempotency key. @internal */\nexport function isIdempotencyKey(value: unknown): value is string {\n return typeof value === \"string\" && EVENT.test(value);\n}\n\n/** The number of bytes of `text` in UTF-8, without encoding the common cases. @internal */\nexport function byteLength(text: string): number {\n // Every UTF-16 unit is at most 3 bytes in UTF-8, and at least 1.\n if (text.length * 3 <= 0x7fffffff) {\n let ascii = true;\n for (let at = 0; at < text.length; at += 1) {\n if (text.charCodeAt(at) > 0x7f) {\n ascii = false;\n break;\n }\n }\n if (ascii) return text.length;\n }\n return encoder.encode(text).length;\n}\n\nfunction isCount(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\ntype Refusal = Extract<DecodedClientFrame, { ok: false }>;\n\nfunction bad(why: string, n?: unknown): Refusal {\n return { ok: false, code: \"bad_frame\", why, ...(isCount(n) ? { n } : {}) };\n}\n\n/**\n * Decodes and validates one client frame (server side). Never throws: any text yields a frame or a refusal.\n *\n * @param text The frame text.\n * @param maxBytes The largest accepted frame, in UTF-8 bytes.\n * @returns The frame, or the code and reason to refuse it with.\n * @example\n * import { decodeClientFrame } from \"@shadow-js/realtime\";\n *\n * const result = decodeClientFrame('{\"t\":\"ping\"}', 65_536);\n * result.ok; // true\n * @since 1.0.0\n */\nexport function decodeClientFrame(text: string, maxBytes: number): DecodedClientFrame {\n if (byteLength(text) > maxBytes)\n return {\n ok: false,\n code: \"too_large\",\n why: __DEV__ ? \"the frame is larger than the limit\" : \"REALTIME_FRAME_11\",\n };\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return bad(__DEV__ ? \"the frame is not JSON\" : \"REALTIME_FRAME_5\");\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value))\n return bad(__DEV__ ? \"the frame is not an object\" : \"REALTIME_FRAME_10\");\n const raw = value as Record<string, unknown>;\n const type = raw[\"t\"];\n const n = raw[\"n\"];\n if (n !== undefined && !isCount(n))\n return bad(__DEV__ ? \"n must be a non-negative integer\" : \"REALTIME_FRAME_8\");\n const c = raw[\"c\"];\n const needChannel = (): Refusal | undefined =>\n isChannelName(c)\n ? undefined\n : {\n ...bad(__DEV__ ? \"c is not a valid channel name\" : \"REALTIME_FRAME_2\", n),\n code: \"bad_channel\",\n };\n switch (type) {\n case \"hello\": {\n const v = raw[\"v\"];\n if (typeof v !== \"number\" || !Number.isInteger(v) || v < 1)\n return bad(__DEV__ ? \"v must be a version\" : \"REALTIME_FRAME_13\");\n return { ok: true, frame: { t: \"hello\", v } };\n }\n case \"ping\":\n return { ok: true, frame: n === undefined ? { t: \"ping\" } : { t: \"ping\", n } };\n case \"sub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n if (n === undefined) return bad(__DEV__ ? \"sub needs n\" : \"REALTIME_FRAME_9\");\n const after = raw[\"after\"];\n if (after !== undefined && !isCount(after))\n return bad(__DEV__ ? \"after must be a non-negative integer\" : \"REALTIME_FRAME_1\", n);\n const echo = raw[\"echo\"];\n if (echo !== undefined && typeof echo !== \"boolean\")\n return bad(__DEV__ ? \"echo must be a boolean\" : \"REALTIME_FRAME_3\", n);\n const watch = raw[\"watch\"];\n if (watch !== undefined && typeof watch !== \"boolean\")\n return bad(__DEV__ ? \"watch must be a boolean\" : \"REALTIME_FRAME_14\", n);\n const hasPresence = \"presence\" in raw;\n return {\n ok: true,\n frame: {\n t: \"sub\",\n n,\n c: c as string,\n hasPresence,\n ...(after === undefined ? {} : { after }),\n ...(echo === undefined ? {} : { echo }),\n ...(watch === undefined ? {} : { watch }),\n ...(hasPresence ? { presence: raw[\"presence\"] as Json } : {}),\n },\n };\n }\n case \"unsub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n return {\n ok: true,\n frame: n === undefined ? { t: \"unsub\", c: c as string } : { t: \"unsub\", n, c: c as string },\n };\n }\n case \"pub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n const e = raw[\"e\"];\n if (!isEventName(e))\n return bad(__DEV__ ? \"e is not a valid event name\" : \"REALTIME_FRAME_4\", n);\n const k = raw[\"k\"];\n if (k !== undefined && !isIdempotencyKey(k))\n return bad(__DEV__ ? \"k is not a valid key\" : \"REALTIME_FRAME_6\", n);\n const data = JSON.stringify(raw[\"d\"] ?? null);\n return {\n ok: true,\n frame: {\n t: \"pub\",\n c: c as string,\n e,\n d: data,\n ...(n === undefined ? {} : { n }),\n ...(k === undefined ? {} : { k }),\n },\n };\n }\n case \"req\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n if (n === undefined) return bad(__DEV__ ? \"req needs n\" : \"REALTIME_FRAME_9\");\n const e = raw[\"e\"];\n if (!isEventName(e))\n return bad(__DEV__ ? \"e is not a valid event name\" : \"REALTIME_FRAME_4\", n);\n const ms = raw[\"ms\"];\n if (ms !== undefined && !(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0))\n return bad(__DEV__ ? \"ms must be a positive number\" : \"REALTIME_FRAME_7\", n);\n return {\n ok: true,\n frame: {\n t: \"req\",\n n,\n c: c as string,\n e,\n d: (raw[\"d\"] ?? null) as Json,\n ...(ms === undefined ? {} : { ms }),\n },\n };\n }\n case \"pres\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n return { ok: true, frame: { t: \"pres\", c: c as string, s: (raw[\"s\"] ?? null) as Json } };\n }\n default:\n return bad(__DEV__ ? \"unknown frame type\" : \"REALTIME_FRAME_12\", n);\n }\n}\n\n/** Frames a server sends, as a client reads them. @since 1.0.0 */\nexport type ServerFrame =\n | {\n readonly t: \"ready\";\n readonly v: number;\n readonly id: string;\n readonly hb: number;\n readonly max: number;\n }\n | {\n readonly t: \"subbed\";\n readonly n: number;\n readonly c: string;\n readonly head: number;\n readonly gap?: boolean;\n readonly reset?: boolean;\n }\n | { readonly t: \"unsubbed\"; readonly c: string; readonly why?: string }\n | {\n readonly t: \"msg\";\n readonly c: string;\n readonly s: number;\n readonly e: string;\n readonly d: Json;\n readonly p?: string;\n readonly u?: string;\n readonly r?: 1;\n }\n | { readonly t: \"ack\"; readonly n: number; readonly s: number; readonly dup?: boolean }\n | { readonly t: \"rep\"; readonly n: number; readonly d: Json }\n | {\n readonly t: \"err\";\n readonly n?: number;\n readonly c?: string;\n readonly code: ErrorCode;\n readonly msg?: string;\n }\n | {\n readonly t: \"pres\";\n readonly c: string;\n readonly op: \"sync\" | \"join\" | \"set\" | \"leave\";\n readonly m: Member | readonly Member[];\n }\n | { readonly t: \"pong\"; readonly n?: number }\n | { readonly t: \"bye\"; readonly code: number; readonly why: string };\n\n/**\n * Decodes one server frame (client side). Returns `undefined` for anything that is not a known, well-formed frame, so a\n * client ignores what a newer server may send and never acts on a malformed frame.\n *\n * @param text The frame text.\n * @returns The frame, or undefined.\n * @example\n * import { decodeServerFrame } from \"@shadow-js/realtime\";\n *\n * decodeServerFrame('{\"t\":\"pong\"}'); // { t: \"pong\" }\n * decodeServerFrame(\"not json\"); // undefined\n * @since 1.0.0\n */\nexport function decodeServerFrame(text: string): ServerFrame | undefined {\n try {\n return serverFrameOf(JSON.parse(text));\n } catch {\n return undefined;\n }\n}\n\n/**\n * Decodes a server frame that is already parsed (the frames of an HTTP answer arrive inside one JSON document), with the\n * rules of {@link decodeServerFrame}.\n * @internal\n */\nexport function serverFrameOf(value: unknown): ServerFrame | undefined {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const str = (key: string): string | undefined => {\n const item = raw[key];\n return typeof item === \"string\" ? item : undefined;\n };\n const num = (key: string): number | undefined => {\n const item = raw[key];\n return isCount(item) ? item : undefined;\n };\n switch (raw[\"t\"]) {\n case \"ready\": {\n const v = num(\"v\");\n const id = str(\"id\");\n if (v === undefined || id === undefined) return undefined;\n return { t: \"ready\", v, id, hb: num(\"hb\") ?? 30_000, max: num(\"max\") ?? 65_536 };\n }\n case \"subbed\": {\n const n = num(\"n\");\n const c = str(\"c\");\n const head = num(\"head\");\n if (n === undefined || c === undefined || head === undefined) return undefined;\n return {\n t: \"subbed\",\n n,\n c,\n head,\n ...(raw[\"gap\"] === true ? { gap: true } : {}),\n ...(raw[\"reset\"] === true ? { reset: true } : {}),\n };\n }\n case \"unsubbed\": {\n const c = str(\"c\");\n if (c === undefined) return undefined;\n const why = str(\"why\");\n return why === undefined ? { t: \"unsubbed\", c } : { t: \"unsubbed\", c, why };\n }\n case \"msg\": {\n const c = str(\"c\");\n const s = num(\"s\");\n const e = str(\"e\");\n if (c === undefined || s === undefined || e === undefined || s < 1) return undefined;\n const p = str(\"p\");\n const u = str(\"u\");\n return {\n t: \"msg\",\n c,\n s,\n e,\n d: raw[\"d\"] as Json,\n ...(p === undefined ? {} : { p }),\n ...(u === undefined ? {} : { u }),\n ...(raw[\"r\"] === 1 ? { r: 1 as const } : {}),\n };\n }\n case \"ack\": {\n const n = num(\"n\");\n const s = num(\"s\");\n if (n === undefined || s === undefined) return undefined;\n return { t: \"ack\", n, s, ...(raw[\"dup\"] === true ? { dup: true } : {}) };\n }\n case \"rep\": {\n const n = num(\"n\");\n if (n === undefined) return undefined;\n return { t: \"rep\", n, d: raw[\"d\"] as Json };\n }\n case \"err\": {\n const code = str(\"code\");\n if (code === undefined) return undefined;\n const n = num(\"n\");\n const c = str(\"c\");\n const msg = str(\"msg\");\n return {\n t: \"err\",\n code: code as ErrorCode,\n ...(n === undefined ? {} : { n }),\n ...(c === undefined ? {} : { c }),\n ...(msg === undefined ? {} : { msg }),\n };\n }\n case \"pres\": {\n const c = str(\"c\");\n const op = raw[\"op\"];\n const m = raw[\"m\"];\n if (c === undefined || (op !== \"sync\" && op !== \"join\" && op !== \"set\" && op !== \"leave\"))\n return undefined;\n const member = (item: unknown): Member | undefined => {\n if (item === null || typeof item !== \"object\") return undefined;\n const entry = item as Record<string, unknown>;\n if (typeof entry[\"id\"] !== \"string\" || !isCount(entry[\"v\"])) return undefined;\n return {\n id: entry[\"id\"],\n v: entry[\"v\"],\n s: (entry[\"s\"] ?? null) as Json,\n ...(typeof entry[\"u\"] === \"string\" ? { u: entry[\"u\"] } : {}),\n };\n };\n if (op === \"sync\") {\n if (!Array.isArray(m)) return undefined;\n const list = m.map(member);\n if (list.some((item) => item === undefined)) return undefined;\n return { t: \"pres\", c, op, m: list as Member[] };\n }\n const one = member(m);\n return one === undefined ? undefined : { t: \"pres\", c, op, m: one };\n }\n case \"pong\": {\n const n = num(\"n\");\n return n === undefined ? { t: \"pong\" } : { t: \"pong\", n };\n }\n case \"bye\": {\n const code = num(\"code\");\n return code === undefined ? undefined : { t: \"bye\", code, why: str(\"why\") ?? \"\" };\n }\n default:\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Where a failure the adapter handled came from.
|
|
3
|
+
* @since 1.0.0
|
|
4
|
+
*/
|
|
2
5
|
export interface RedisErrorContext {
|
|
6
|
+
/** The step that failed, for example `"parse"`, `"listener"`, `"reconnect"` or `"subscriber"`. */
|
|
3
7
|
readonly where: string;
|
|
8
|
+
/** The channel it concerned, when it concerned one. */
|
|
4
9
|
readonly channel?: string;
|
|
5
10
|
}
|
|
6
11
|
//# sourceMappingURL=topics.d.ts.map
|
package/dist/dev/redis/topics.js
CHANGED
|
@@ -45,8 +45,10 @@ function createTopics(deps) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
function emit(topic, message) {
|
|
48
|
-
if (topic.dead
|
|
48
|
+
if (topic.dead) return;
|
|
49
|
+
if (message.seq <= topic.last && message.at <= topic.lastAt) return;
|
|
49
50
|
topic.last = message.seq;
|
|
51
|
+
topic.lastAt = Math.max(topic.lastAt, message.at);
|
|
50
52
|
for (const listener of [...topic.listeners]) try {
|
|
51
53
|
listener(message);
|
|
52
54
|
} catch (error) {
|
|
@@ -315,6 +317,7 @@ function createTopics(deps) {
|
|
|
315
317
|
topic: key,
|
|
316
318
|
listeners: /* @__PURE__ */ new Set(),
|
|
317
319
|
last: 0,
|
|
320
|
+
lastAt: 0,
|
|
318
321
|
catching: false,
|
|
319
322
|
rerun: false,
|
|
320
323
|
buffer: [],
|