@shadow-js/realtime 1.0.0 → 1.0.2
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 +26 -0
- package/README.md +35 -0
- package/dist/dev/adapter.d.ts +0 -2
- package/dist/dev/client/core.d.ts +0 -2
- package/dist/dev/client/core.js +105 -108
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/presence.js +1 -2
- package/dist/dev/client/sse.js +34 -26
- package/dist/dev/client/sse.js.map +1 -1
- package/dist/dev/client/types.d.ts +6 -2
- package/dist/dev/client/util.js +3 -4
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/client/ws.js +1 -2
- package/dist/dev/cloudflare/adapter.d.ts +0 -2
- package/dist/dev/cloudflare/adapter.js +1 -2
- package/dist/dev/cloudflare/core.js +1 -2
- package/dist/dev/cloudflare/link.js +70 -10
- package/dist/dev/cloudflare/link.js.map +1 -1
- package/dist/dev/cloudflare/object.d.ts +0 -2
- package/dist/dev/cloudflare/object.js +1 -2
- package/dist/dev/cloudflare/types.d.ts +0 -2
- package/dist/dev/cloudflare/wire.js +1 -2
- package/dist/dev/errors.d.ts +0 -2
- package/dist/dev/errors.js +1 -2
- package/dist/dev/hooks.d.ts +0 -2
- package/dist/dev/hooks.js +1 -2
- package/dist/dev/memory.d.ts +11 -2
- package/dist/dev/memory.js +39 -12
- package/dist/dev/memory.js.map +1 -1
- package/dist/dev/protocol.d.ts +0 -2
- package/dist/dev/protocol.js +14 -14
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/redis/client.d.ts +0 -2
- package/dist/dev/redis/client.js +1 -2
- package/dist/dev/redis/scripts.js +1 -2
- package/dist/dev/redis/topics.d.ts +6 -3
- package/dist/dev/redis/topics.js +5 -3
- package/dist/dev/redis/topics.js.map +1 -1
- package/dist/dev/redis.d.ts +0 -2
- package/dist/dev/redis.js +1 -2
- package/dist/dev/server/http.d.ts +0 -2
- package/dist/dev/server/http.js +38 -26
- package/dist/dev/server/http.js.map +1 -1
- package/dist/dev/server/hub.d.ts +0 -2
- package/dist/dev/server/hub.js +67 -41
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +2 -4
- package/dist/dev/server/index.js +1 -2
- package/dist/dev/server/index.js.map +1 -1
- package/dist/dev/server/limits.d.ts +4 -3
- package/dist/dev/server/limits.js +1 -2
- package/dist/dev/server/limits.js.map +1 -1
- package/dist/dev/server/rules.d.ts +0 -2
- package/dist/dev/server/rules.js +1 -2
- package/dist/dev/server/rules.js.map +1 -1
- package/dist/dev/testing.d.ts +0 -2
- package/dist/dev/testing.js +1 -2
- 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
package/dist/dev/protocol.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/protocol.ts
|
|
2
2
|
/**
|
|
3
3
|
* The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).
|
|
4
4
|
* @example
|
|
@@ -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,7 +395,6 @@ function decodeServerFrame(text) {
|
|
|
394
395
|
default: return;
|
|
395
396
|
}
|
|
396
397
|
}
|
|
397
|
-
|
|
398
|
-
export { PROTOCOL_VERSION, byteLength, decodeClientFrame, decodeServerFrame, isChannelName, isEventName, isIdempotencyKey };
|
|
398
|
+
export { PROTOCOL_VERSION, byteLength, decodeClientFrame, decodeServerFrame, isChannelName, isEventName, isIdempotencyKey, serverFrameOf };
|
|
399
399
|
|
|
400
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":";;;;;;;;;AAUA,MAAa,mBAAsB;AA2FnC,MAAM,UAAU;AAEhB,MAAM,QAAQ;AACd,MAAM,UAAU,IAAI,YAAY;;;;;;;;;;;;AAahC,SAAgB,cAAc,OAAiC;CAC7D,OAAO,OAAO,UAAU,YAAY,QAAQ,KAAK,KAAK;AACxD;;;;;;;;;;;;AAaA,SAAgB,YAAY,OAAiC;CAC3D,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,KAAK;AACtD;;AAGA,SAAgB,iBAAiB,OAAiC;CAChE,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,KAAK;AACtD;;AAGA,SAAgB,WAAW,MAAsB;CAE/C,IAAI,KAAK,SAAS,KAAK,YAAY;EACjC,IAAI,QAAQ;EACZ,KAAK,IAAI,KAAK,GAAG,KAAK,KAAK,QAAQ,MAAM,GACvC,IAAI,KAAK,WAAW,EAAE,IAAI,KAAM;GAC9B,QAAQ;GACR;EACF;EAEF,IAAI,OAAO,OAAO,KAAK;CACzB;CACA,OAAO,QAAQ,OAAO,IAAI,CAAC,CAAC;AAC9B;AAEA,SAAS,QAAQ,OAAiC;CAChD,OAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS;AAC9E;AAIA,SAAS,IAAI,KAAa,GAAsB;CAC9C,OAAO;EAAE,IAAI;EAAO,MAAM;EAAa;EAAK,GAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC;CAAG;AAC3E;;;;;;;;;;;;;;AAeA,SAAgB,kBAAkB,MAAc,UAAsC;CACpF,IAAI,WAAW,IAAI,IAAI,UACrB,OAAO;EACL,IAAI;EACJ,MAAM;EACN,KAAe;CACjB;CACF,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,IAAI;CACzB,QAAQ;EACN,OAAO,IAAc,uBAA4C;CACnE;CACA,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GACpE,OAAO,IAAc,4BAAkD;CACzE,MAAM,MAAM;CACZ,MAAM,OAAO,IAAI;CACjB,MAAM,IAAI,IAAI;CACd,IAAI,MAAM,KAAA,KAAa,CAAC,QAAQ,CAAC,GAC/B,OAAO,IAAc,kCAAuD;CAC9E,MAAM,IAAI,IAAI;CACd,MAAM,oBACJ,cAAc,CAAC,IACX,KAAA,IACA;EACE,GAAG,IAAc,iCAAsD,CAAC;EACxE,MAAM;CACR;CACN,QAAQ,MAAR;EACE,KAAK,SAAS;GACZ,MAAM,IAAI,IAAI;GACd,IAAI,OAAO,MAAM,YAAY,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,GACvD,OAAO,IAAc,qBAA2C;GAClE,OAAO;IAAE,IAAI;IAAM,OAAO;KAAE,GAAG;KAAS;IAAE;GAAE;EAC9C;EACA,KAAK,QACH,OAAO;GAAE,IAAI;GAAM,OAAO,MAAM,KAAA,IAAY,EAAE,GAAG,OAAO,IAAI;IAAE,GAAG;IAAQ;GAAE;EAAE;EAC/E,KAAK,OAAO;GACV,MAAM,UAAU,YAAY;GAC5B,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,IAAI,MAAM,KAAA,GAAW,OAAO,IAAc,aAAkC;GAC5E,MAAM,QAAQ,IAAI;GAClB,IAAI,UAAU,KAAA,KAAa,CAAC,QAAQ,KAAK,GACvC,OAAO,IAAc,wCAA6D,CAAC;GACrF,MAAM,OAAO,IAAI;GACjB,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,WACxC,OAAO,IAAc,0BAA+C,CAAC;GACvE,MAAM,QAAQ,IAAI;GAClB,IAAI,UAAU,KAAA,KAAa,OAAO,UAAU,WAC1C,OAAO,IAAc,2BAAiD,CAAC;GACzE,MAAM,cAAc,cAAc;GAClC,OAAO;IACL,IAAI;IACJ,OAAO;KACL,GAAG;KACH;KACG;KACH;KACA,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;KACvC,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;KACrC,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;KACvC,GAAI,cAAc,EAAE,UAAU,IAAI,YAAoB,IAAI,CAAC;IAC7D;GACF;EACF;EACA,KAAK,SAAS;GACZ,MAAM,UAAU,YAAY;GAC5B,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,OAAO;IACL,IAAI;IACJ,OAAO,MAAM,KAAA,IAAY;KAAE,GAAG;KAAY;IAAY,IAAI;KAAE,GAAG;KAAS;KAAM;IAAY;GAC5F;EACF;EACA,KAAK,OAAO;GACV,MAAM,UAAU,YAAY;GAC5B,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,MAAM,IAAI,IAAI;GACd,IAAI,CAAC,YAAY,CAAC,GAChB,OAAO,IAAc,+BAAoD,CAAC;GAC5E,MAAM,IAAI,IAAI;GACd,IAAI,MAAM,KAAA,KAAa,CAAC,iBAAiB,CAAC,GACxC,OAAO,IAAc,wBAA6C,CAAC;GAErE,OAAO;IACL,IAAI;IACJ,OAAO;KACL,GAAG;KACA;KACH;KACA,GAPS,KAAK,UAAU,IAAI,QAAQ,IAO9B;KACN,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,EAAE;KAC/B,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,EAAE;IACjC;GACF;EACF;EACA,KAAK,OAAO;GACV,MAAM,UAAU,YAAY;GAC5B,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,IAAI,MAAM,KAAA,GAAW,OAAO,IAAc,aAAkC;GAC5E,MAAM,IAAI,IAAI;GACd,IAAI,CAAC,YAAY,CAAC,GAChB,OAAO,IAAc,+BAAoD,CAAC;GAC5E,MAAM,KAAK,IAAI;GACf,IAAI,OAAO,KAAA,KAAa,EAAE,OAAO,OAAO,YAAY,OAAO,SAAS,EAAE,KAAK,KAAK,IAC9E,OAAO,IAAc,gCAAqD,CAAC;GAC7E,OAAO;IACL,IAAI;IACJ,OAAO;KACL,GAAG;KACH;KACG;KACH;KACA,GAAI,IAAI,QAAQ;KAChB,GAAI,OAAO,KAAA,IAAY,CAAC,IAAI,EAAE,GAAG;IACnC;GACF;EACF;EACA,KAAK,QAAQ;GACX,MAAM,UAAU,YAAY;GAC5B,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,OAAO;IAAE,IAAI;IAAM,OAAO;KAAE,GAAG;KAAW;KAAa,GAAI,IAAI,QAAQ;IAAc;GAAE;EACzF;EACA,SACE,OAAO,IAAc,sBAA4C,CAAC;CACtE;AACF;;;;;;;;;;;;;;AA6DA,SAAgB,kBAAkB,MAAuC;CACvE,IAAI;EACF,OAAO,cAAc,KAAK,MAAM,IAAI,CAAC;CACvC,QAAQ;EACN;CACF;AACF;;;;;;AAOA,SAAgB,cAAc,OAAyC;CACrE,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO,KAAA;CAChF,MAAM,MAAM;CACZ,MAAM,OAAO,QAAoC;EAC/C,MAAM,OAAO,IAAI;EACjB,OAAO,OAAO,SAAS,WAAW,OAAO,KAAA;CAC3C;CACA,MAAM,OAAO,QAAoC;EAC/C,MAAM,OAAO,IAAI;EACjB,OAAO,QAAQ,IAAI,IAAI,OAAO,KAAA;CAChC;CACA,QAAQ,IAAI,MAAZ;EACE,KAAK,SAAS;GACZ,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,KAAK,IAAI,IAAI;GACnB,IAAI,MAAM,KAAA,KAAa,OAAO,KAAA,GAAW,OAAO,KAAA;GAChD,OAAO;IAAE,GAAG;IAAS;IAAG;IAAI,IAAI,IAAI,IAAI,KAAK;IAAQ,KAAK,IAAI,KAAK,KAAK;GAAO;EACjF;EACA,KAAK,UAAU;GACb,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,OAAO,IAAI,MAAM;GACvB,IAAI,MAAM,KAAA,KAAa,MAAM,KAAA,KAAa,SAAS,KAAA,GAAW,OAAO,KAAA;GACrE,OAAO;IACL,GAAG;IACH;IACA;IACA;IACA,GAAI,IAAI,WAAW,OAAO,EAAE,KAAK,KAAK,IAAI,CAAC;IAC3C,GAAI,IAAI,aAAa,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;GACjD;EACF;EACA,KAAK,YAAY;GACf,MAAM,IAAI,IAAI,GAAG;GACjB,IAAI,MAAM,KAAA,GAAW,OAAO,KAAA;GAC5B,MAAM,MAAM,IAAI,KAAK;GACrB,OAAO,QAAQ,KAAA,IAAY;IAAE,GAAG;IAAY;GAAE,IAAI;IAAE,GAAG;IAAY;IAAG;GAAI;EAC5E;EACA,KAAK,OAAO;GACV,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,IAAI,IAAI,GAAG;GACjB,IAAI,MAAM,KAAA,KAAa,MAAM,KAAA,KAAa,MAAM,KAAA,KAAa,IAAI,GAAG,OAAO,KAAA;GAC3E,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,IAAI,IAAI,GAAG;GACjB,OAAO;IACL,GAAG;IACH;IACA;IACA;IACA,GAAG,IAAI;IACP,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,EAAE;IAC/B,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,EAAE;IAC/B,GAAI,IAAI,SAAS,IAAI,EAAE,GAAG,EAAW,IAAI,CAAC;GAC5C;EACF;EACA,KAAK,OAAO;GACV,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,IAAI,IAAI,GAAG;GACjB,IAAI,MAAM,KAAA,KAAa,MAAM,KAAA,GAAW,OAAO,KAAA;GAC/C,OAAO;IAAE,GAAG;IAAO;IAAG;IAAG,GAAI,IAAI,WAAW,OAAO,EAAE,KAAK,KAAK,IAAI,CAAC;GAAG;EACzE;EACA,KAAK,OAAO;GACV,MAAM,IAAI,IAAI,GAAG;GACjB,IAAI,MAAM,KAAA,GAAW,OAAO,KAAA;GAC5B,OAAO;IAAE,GAAG;IAAO;IAAG,GAAG,IAAI;GAAa;EAC5C;EACA,KAAK,OAAO;GACV,MAAM,OAAO,IAAI,MAAM;GACvB,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,MAAM,IAAI,KAAK;GACrB,OAAO;IACL,GAAG;IACG;IACN,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,EAAE;IAC/B,GAAI,MAAM,KAAA,IAAY,CAAC,IAAI,EAAE,EAAE;IAC/B,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;GACrC;EACF;EACA,KAAK,QAAQ;GACX,MAAM,IAAI,IAAI,GAAG;GACjB,MAAM,KAAK,IAAI;GACf,MAAM,IAAI,IAAI;GACd,IAAI,MAAM,KAAA,KAAc,OAAO,UAAU,OAAO,UAAU,OAAO,SAAS,OAAO,SAC/E,OAAO,KAAA;GACT,MAAM,UAAU,SAAsC;IACpD,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO,KAAA;IACtD,MAAM,QAAQ;IACd,IAAI,OAAO,MAAM,UAAU,YAAY,CAAC,QAAQ,MAAM,IAAI,GAAG,OAAO,KAAA;IACpE,OAAO;KACL,IAAI,MAAM;KACV,GAAG,MAAM;KACT,GAAI,MAAM,QAAQ;KAClB,GAAI,OAAO,MAAM,SAAS,WAAW,EAAE,GAAG,MAAM,KAAK,IAAI,CAAC;IAC5D;GACF;GACA,IAAI,OAAO,QAAQ;IACjB,IAAI,CAAC,MAAM,QAAQ,CAAC,GAAG,OAAO,KAAA;IAC9B,MAAM,OAAO,EAAE,IAAI,MAAM;IACzB,IAAI,KAAK,MAAM,SAAS,SAAS,KAAA,CAAS,GAAG,OAAO,KAAA;IACpD,OAAO;KAAE,GAAG;KAAQ;KAAG;KAAI,GAAG;IAAiB;GACjD;GACA,MAAM,MAAM,OAAO,CAAC;GACpB,OAAO,QAAQ,KAAA,IAAY,KAAA,IAAY;IAAE,GAAG;IAAQ;IAAG;IAAI,GAAG;GAAI;EACpE;EACA,KAAK,QAAQ;GACX,MAAM,IAAI,IAAI,GAAG;GACjB,OAAO,MAAM,KAAA,IAAY,EAAE,GAAG,OAAO,IAAI;IAAE,GAAG;IAAQ;GAAE;EAC1D;EACA,KAAK,OAAO;GACV,MAAM,OAAO,IAAI,MAAM;GACvB,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY;IAAE,GAAG;IAAO;IAAM,KAAK,IAAI,KAAK,KAAK;GAAG;EAClF;EACA,SACE;CACJ;AACF"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/redis/client.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* What the Redis adapter needs from a Redis library, and the two wrappers that adapt the common ones. The adapter has no
|
|
4
3
|
* dependency on a Redis package: it is written against these small interfaces, which ioredis and node-redis satisfy through
|
|
@@ -135,5 +134,4 @@ export declare function fromIoredis(client: IoredisLike): RedisClient;
|
|
|
135
134
|
* @since 1.0.0
|
|
136
135
|
*/
|
|
137
136
|
export declare function fromNodeRedis(client: NodeRedisLike): RedisClient;
|
|
138
|
-
//#endregion
|
|
139
137
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/dev/redis/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/redis/client.ts
|
|
2
2
|
/** A set of handlers with add-and-remove, shared by the two wrappers. */
|
|
3
3
|
function handlers() {
|
|
4
4
|
const set = /* @__PURE__ */ new Set();
|
|
@@ -140,7 +140,6 @@ function fromNodeRedis(client) {
|
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
-
//#endregion
|
|
144
143
|
export { fromIoredis, fromNodeRedis };
|
|
145
144
|
|
|
146
145
|
//# sourceMappingURL=client.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/redis/scripts.ts
|
|
2
2
|
function rotateLeft(value, bits) {
|
|
3
3
|
return (value << bits | value >>> 32 - bits) >>> 0;
|
|
4
4
|
}
|
|
@@ -269,7 +269,6 @@ for from = 1, #ids, step do
|
|
|
269
269
|
end
|
|
270
270
|
return out
|
|
271
271
|
`);
|
|
272
|
-
//#endregion
|
|
273
272
|
export { HISTORY, KEEPALIVE, PRESENCE_LIST, PRESENCE_REAP, PRESENCE_REMOVE, PRESENCE_SET, PRESENCE_TOUCH, PUBLISH, sha1 };
|
|
274
273
|
|
|
275
274
|
//# sourceMappingURL=scripts.js.map
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Where a failure the adapter handled came from.
|
|
3
|
+
* @since 1.0.0
|
|
4
|
+
*/
|
|
3
5
|
export interface RedisErrorContext {
|
|
6
|
+
/** The step that failed, for example `"parse"`, `"listener"`, `"reconnect"` or `"subscriber"`. */
|
|
4
7
|
readonly where: string;
|
|
8
|
+
/** The channel it concerned, when it concerned one. */
|
|
5
9
|
readonly channel?: string;
|
|
6
10
|
}
|
|
7
|
-
//#endregion
|
|
8
11
|
//# sourceMappingURL=topics.d.ts.map
|
package/dist/dev/redis/topics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/redis/topics.ts
|
|
2
2
|
/** Live messages held while a recovery reads history; a larger burst falls back to reading history again. */
|
|
3
3
|
const CATCH_UP_BUFFER = 1e3;
|
|
4
4
|
/** Messages read per history page during recovery. */
|
|
@@ -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: [],
|
|
@@ -371,7 +374,6 @@ function createTopics(deps) {
|
|
|
371
374
|
}
|
|
372
375
|
};
|
|
373
376
|
}
|
|
374
|
-
//#endregion
|
|
375
377
|
export { createTopics };
|
|
376
378
|
|
|
377
379
|
//# sourceMappingURL=topics.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topics.js","names":[],"sources":["../../../src/redis/topics.ts"],"sourcesContent":["import type { Unsubscribe } from \"../adapter\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { RedisSubscriber } from \"./client\";\n\n/** Where a failure the adapter handled came from. */\nexport interface RedisErrorContext {\n readonly where: string;\n readonly channel?: string;\n}\n\n/** What the subscription manager needs from the adapter that owns it. */\nexport interface TopicsDeps {\n /** The shared subscriber connection (opened on first use). */\n subscriber(): Promise<RedisSubscriber>;\n /** The retained messages after `after`, at most `limit`, oldest first. */\n history(channel: string, after: number, limit: number): Promise<readonly StoredMessage[]>;\n /** The live presence members of a channel. */\n members(channel: string): Promise<readonly Member[]>;\n /** Extends the lifetime of a channel's numbering. */\n keepAlive(channel: string): Promise<void>;\n /** Parses a pub/sub payload; undefined (after reporting) when it is malformed. */\n parseMessage(text: string, channel: string): StoredMessage | undefined;\n parsePresence(text: string, channel: string): PresenceEvent | undefined;\n messageTopic(channel: string): string;\n presenceTopic(channel: string): string;\n report(error: unknown, context: RedisErrorContext): void;\n /** Turns a failure into the error the adapter's callers see. */\n fail(error: unknown): Error;\n /** Milliseconds between keep-alive rounds. */\n keepAliveMs: number;\n}\n\n/** What the manager returns. */\nexport interface Topics {\n messages(channel: string, listener: (message: StoredMessage) => void): Promise<Unsubscribe>;\n presence(channel: string, listener: (event: PresenceEvent) => void): Promise<Unsubscribe>;\n /** Unsubscribes everything and stops the timers. */\n close(): Promise<void>;\n /** The number of topics currently subscribed (for tests and diagnostics). */\n readonly size: number;\n}\n\n/** Live messages held while a recovery reads history; a larger burst falls back to reading history again. */\nconst CATCH_UP_BUFFER = 1000;\n/** Messages read per history page during recovery. */\nconst CATCH_UP_PAGE = 500;\n/** Delays between attempts to recover after a reconnect. */\nconst RETRY_DELAYS_MS: readonly number[] = [50, 250, 1000];\n/** Topics subscribed again at once after a reconnect. */\nconst RESUBSCRIBE_BATCH = 64;\n/** Members tracked per presence channel; a larger channel is delivered unfiltered and not reconciled after a reconnect. */\nconst MAX_TRACKED_MEMBERS = 5000;\n\ntype MessageListener = (message: StoredMessage) => void;\ntype PresenceListener = (event: PresenceEvent) => void;\n\ninterface TopicBase {\n readonly channel: string;\n readonly topic: string;\n readonly handler: (text: string) => void;\n ready: Promise<void>;\n dead: boolean;\n}\n\ninterface MessageTopic extends TopicBase {\n readonly kind: \"message\";\n readonly listeners: Set<MessageListener>;\n /** The highest seq handed to the listeners: nothing at or below it is delivered again. */\n last: number;\n /** True while history is being read to fill a gap: live messages wait in `buffer`. */\n catching: boolean;\n /** Set when a reconnect happened while a recovery was running: it reads once more. */\n rerun: boolean;\n buffer: StoredMessage[];\n overflow: boolean;\n}\n\ninterface PresenceTopic extends TopicBase {\n readonly kind: \"presence\";\n readonly listeners: Set<PresenceListener>;\n /** The members as the listeners know them, by id. */\n members: Map<string, Member>;\n /** False when the channel is too large to track: events are then delivered unfiltered and not reconciled. */\n tracked: boolean;\n /** Events received while the state is being read; applied on top of it when it arrives. */\n pending: PresenceEvent[] | undefined;\n}\n\ntype Topic = MessageTopic | PresenceTopic;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Applies an event to a member map; true when it changed the map (a stale or repeated event does not). */\nfunction apply(members: Map<string, Member>, event: PresenceEvent): boolean {\n const known = members.get(event.member.id);\n if (event.op === \"leave\") return known !== undefined && members.delete(event.member.id);\n if (known !== undefined && known.v >= event.member.v) return false;\n members.set(event.member.id, event.member);\n return true;\n}\n\n/**\n * The shared subscriber connection of one adapter: topics are refcounted (one SUBSCRIBE per topic however many listeners), a\n * listener sees strictly increasing seqs and never a repeat, and after a reconnect the topics are subscribed again and what the\n * connection missed is read back from history (messages) or from the member list (presence).\n */\nexport function createTopics(deps: TopicsDeps): Topics {\n const topics = new Map<string, Topic>();\n let closed = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n let reconnecting = false;\n let again = false;\n let connected: RedisSubscriber | undefined;\n /** Removes the hooks this manager put on the subscriber (a subscriber the caller passed outlives the adapter). */\n const unhook: (() => void)[] = [];\n\n function reportTo(error: unknown, topic: Topic, where: string): void {\n deps.report(error, { where, channel: topic.channel });\n }\n\n // --- delivery ------------------------------------------------------------------------------------------------------\n\n function emit(topic: MessageTopic, message: StoredMessage): void {\n if (topic.dead || message.seq <= topic.last) return;\n topic.last = message.seq;\n for (const listener of [...topic.listeners]) {\n try {\n listener(message);\n } catch (error) {\n reportTo(error, topic, \"listener\");\n }\n }\n }\n\n function onMessage(topic: MessageTopic, text: string): void {\n const message = deps.parseMessage(text, topic.channel);\n if (message === undefined || topic.dead) return;\n if (topic.catching || (topic.last > 0 && message.seq > topic.last + 1)) {\n // Either history is being read, or this message jumped ahead of the cursor (a publish this connection missed): hold it\n // back and fill the gap from history first, so the listeners see every retained message in order.\n if (topic.buffer.length >= CATCH_UP_BUFFER) topic.overflow = true;\n else topic.buffer.push(message);\n if (!topic.catching) void heal(topic);\n return;\n }\n emit(topic, message);\n }\n\n function dispatchPresence(topic: PresenceTopic, event: PresenceEvent): void {\n for (const listener of [...topic.listeners]) {\n try {\n listener(event);\n } catch (error) {\n reportTo(error, topic, \"listener\");\n }\n }\n }\n\n function onPresence(topic: PresenceTopic, text: string): void {\n const event = deps.parsePresence(text, topic.channel);\n if (event === undefined || topic.dead) return;\n if (topic.pending !== undefined) {\n // The state is being read: deliver raw now, and fold the event into the state when it arrives.\n if (topic.pending.length < MAX_TRACKED_MEMBERS) topic.pending.push(event);\n else topic.tracked = false;\n dispatchPresence(topic, event);\n return;\n }\n if (topic.tracked && !apply(topic.members, event)) return;\n dispatchPresence(topic, event);\n }\n\n // --- recovery after a reconnect ------------------------------------------------------------------------------------\n\n /** Reads history from the cursor until it reaches the present, then lets the held live messages through. */\n async function readToPresent(topic: MessageTopic): Promise<void> {\n for (;;) {\n let more = true;\n while (more) {\n const page = await deps.history(topic.channel, topic.last, CATCH_UP_PAGE);\n if (topic.dead) return;\n for (const message of page) emit(topic, message);\n more = page.length >= CATCH_UP_PAGE;\n }\n if (!topic.overflow) return;\n // More live messages arrived than the buffer holds: read history again from the cursor.\n topic.overflow = false;\n topic.buffer.length = 0;\n }\n }\n\n /**\n * Fills a gap of the cursor from history. One at a time per topic; a request that arrives while one runs makes it read once\n * more. Gives up after a few failed attempts (the hub's own recovery is the next net) and lets the held messages through.\n */\n async function heal(topic: MessageTopic): Promise<void> {\n if (topic.catching) {\n topic.rerun = true;\n return;\n }\n topic.catching = true;\n try {\n let attempt = 0;\n while (!topic.dead && !closed) {\n topic.rerun = false;\n try {\n await readToPresent(topic);\n if (!topic.rerun) return;\n } catch (error) {\n reportTo(error, topic, \"recover\");\n const delay = RETRY_DELAYS_MS[attempt];\n attempt += 1;\n if (delay === undefined) return;\n await sleep(delay);\n }\n }\n } finally {\n topic.catching = false;\n topic.rerun = false;\n const held = topic.buffer.splice(0);\n for (const message of held) emit(topic, message);\n }\n }\n\n async function resync(topic: PresenceTopic): Promise<void> {\n if (!topic.tracked) return;\n const before = topic.members;\n topic.pending = [];\n let list: readonly Member[];\n try {\n list = await deps.members(topic.channel);\n } catch (error) {\n const held = topic.pending;\n topic.pending = undefined;\n for (const event of held) if (apply(before, event)) dispatchPresence(topic, event);\n throw error;\n }\n const held = topic.pending;\n topic.pending = undefined;\n if (topic.dead) return;\n const next = new Map<string, Member>(list.map((member) => [member.id, member]));\n for (const event of held) apply(next, event);\n const events: PresenceEvent[] = [];\n for (const [id, member] of before)\n if (!next.has(id)) events.push({ channel: topic.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const known = before.get(id);\n if (known === undefined) events.push({ channel: topic.channel, op: \"join\", member });\n else if (member.v > known.v) events.push({ channel: topic.channel, op: \"set\", member });\n }\n topic.members = next;\n if (next.size > MAX_TRACKED_MEMBERS) topic.tracked = false;\n for (const event of events) dispatchPresence(topic, event);\n }\n\n async function recover(topic: Topic): Promise<void> {\n for (let attempt = 0; ; attempt += 1) {\n try {\n const subscriber = await deps.subscriber();\n await subscriber.subscribe(topic.topic, topic.handler);\n if (topic.kind === \"message\") {\n if (topic.last > 0) await heal(topic);\n } else {\n await resync(topic);\n }\n return;\n } catch (error) {\n reportTo(error, topic, \"reconnect\");\n const delay = RETRY_DELAYS_MS[attempt];\n if (delay === undefined || topic.dead || closed) return;\n await sleep(delay);\n if (topic.dead || closed) return;\n }\n }\n }\n\n async function reconnect(): Promise<void> {\n if (reconnecting) {\n again = true;\n return;\n }\n reconnecting = true;\n try {\n do {\n again = false;\n const live = [...topics.values()].filter((topic) => !topic.dead);\n for (let from = 0; from < live.length && !closed; from += RESUBSCRIBE_BATCH)\n await Promise.all(live.slice(from, from + RESUBSCRIBE_BATCH).map(recover));\n } while (again && !closed);\n } finally {\n reconnecting = false;\n }\n }\n\n async function hook(): Promise<RedisSubscriber> {\n const subscriber = await deps.subscriber();\n if (connected !== subscriber) {\n connected = subscriber;\n unhook.push(\n subscriber.onReconnect(() => {\n void reconnect();\n }),\n );\n const stopErrors = subscriber.onError?.((error) =>\n deps.report(error, { where: \"subscriber\" }),\n );\n if (stopErrors !== undefined) unhook.push(stopErrors);\n }\n return subscriber;\n }\n\n // --- keeping numbering alive ---------------------------------------------------------------------------------------\n\n async function keepAliveRound(): Promise<void> {\n const channels = [...topics.values()].filter(\n (topic): topic is MessageTopic => topic.kind === \"message\" && !topic.dead,\n );\n for (let from = 0; from < channels.length; from += RESUBSCRIBE_BATCH)\n await Promise.all(\n channels.slice(from, from + RESUBSCRIBE_BATCH).map(async (topic) => {\n try {\n await deps.keepAlive(topic.channel);\n } catch (error) {\n reportTo(error, topic, \"keepAlive\");\n }\n }),\n );\n }\n\n function syncTimer(): void {\n const needed = [...topics.values()].some((topic) => topic.kind === \"message\");\n if (needed && timer === undefined && !closed) {\n timer = setInterval(() => void keepAliveRound(), deps.keepAliveMs);\n (timer as { unref?: () => void }).unref?.();\n } else if (!needed && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n // --- subscribing and releasing -------------------------------------------------------------------------------------\n\n async function open<T extends Topic>(topic: T): Promise<void> {\n topics.set(topic.topic, topic);\n syncTimer();\n topic.ready = (async () => {\n const subscriber = await hook();\n await subscriber.subscribe(topic.topic, topic.handler);\n if (topic.kind === \"presence\") await seed(topic);\n })();\n topic.ready.catch(() => {\n // The first caller's await reports it; here the half-open topic is dropped.\n topic.dead = true;\n if (topics.get(topic.topic) === topic) topics.delete(topic.topic);\n syncTimer();\n connected?.unsubscribe(topic.topic).catch(() => undefined);\n });\n await topic.ready;\n }\n\n async function seed(topic: PresenceTopic): Promise<void> {\n topic.pending = [];\n let list: readonly Member[];\n try {\n list = await deps.members(topic.channel);\n } catch (error) {\n topic.pending = undefined;\n throw error;\n }\n const held = topic.pending;\n topic.pending = undefined;\n const members = new Map<string, Member>(list.map((member) => [member.id, member]));\n for (const event of held) apply(members, event);\n topic.members = members;\n if (members.size > MAX_TRACKED_MEMBERS) topic.tracked = false;\n }\n\n /**\n * Returns the function that removes one listener. The last listener out drops the topic and sends UNSUBSCRIBE at once (not\n * after an await), so a subscribe of the same topic issued next is sent after it and wins.\n */\n function release(topic: Topic, remove: () => void): Unsubscribe {\n let done = false;\n return async () => {\n if (done) return;\n done = true;\n remove();\n if (topic.listeners.size > 0 || topic.dead) return;\n topic.dead = true;\n if (topics.get(topic.topic) === topic) topics.delete(topic.topic);\n syncTimer();\n if (closed || connected === undefined) return;\n try {\n await connected.unsubscribe(topic.topic);\n } catch (error) {\n reportTo(error, topic, \"unsubscribe\");\n }\n };\n }\n\n async function attach<T extends Topic>(\n topic: T | undefined,\n create: () => T,\n add: (topic: T) => Unsubscribe,\n ): Promise<Unsubscribe> {\n if (closed) throw deps.fail(new Error(\"the adapter is closed.\"));\n const found = topic ?? create();\n const stop = add(found);\n try {\n if (topic === undefined) await open(found);\n else await found.ready;\n } catch (error) {\n void stop();\n throw deps.fail(error);\n }\n if (closed || found.dead) {\n void stop();\n throw deps.fail(new Error(\"the adapter is closed.\"));\n }\n return stop;\n }\n\n return {\n messages(channel, listener) {\n const key = deps.messageTopic(channel);\n const existing = topics.get(key);\n return attach<MessageTopic>(\n existing?.kind === \"message\" ? existing : undefined,\n () => {\n const topic: MessageTopic = {\n kind: \"message\",\n channel,\n topic: key,\n listeners: new Set(),\n last: 0,\n catching: false,\n rerun: false,\n buffer: [],\n overflow: false,\n dead: false,\n ready: Promise.resolve(),\n handler: (text) => onMessage(topic, text),\n };\n return topic;\n },\n (topic) => {\n topic.listeners.add(listener);\n return release(topic, () => topic.listeners.delete(listener));\n },\n );\n },\n presence(channel, listener) {\n const key = deps.presenceTopic(channel);\n const existing = topics.get(key);\n return attach<PresenceTopic>(\n existing?.kind === \"presence\" ? existing : undefined,\n () => {\n const topic: PresenceTopic = {\n kind: \"presence\",\n channel,\n topic: key,\n listeners: new Set(),\n members: new Map(),\n tracked: true,\n pending: undefined,\n dead: false,\n ready: Promise.resolve(),\n handler: (text) => onPresence(topic, text),\n };\n return topic;\n },\n (topic) => {\n topic.listeners.add(listener);\n return release(topic, () => topic.listeners.delete(listener));\n },\n );\n },\n async close() {\n closed = true;\n for (const stop of unhook.splice(0)) stop();\n if (timer !== undefined) clearInterval(timer);\n timer = undefined;\n const open = [...topics.values()];\n topics.clear();\n for (const topic of open) topic.dead = true;\n if (open.length === 0) return;\n const subscriber = connected;\n if (subscriber === undefined) return;\n await Promise.all(\n open.map((topic) =>\n subscriber.unsubscribe(topic.topic).catch((error: unknown) => {\n reportTo(error, topic, \"unsubscribe\");\n }),\n ),\n );\n },\n get size() {\n return topics.size;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"topics.js","names":[],"sources":["../../../src/redis/topics.ts"],"sourcesContent":["import type { Unsubscribe } from \"../adapter\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { RedisSubscriber } from \"./client\";\n\n/**\n * Where a failure the adapter handled came from.\n * @since 1.0.0\n */\nexport interface RedisErrorContext {\n /** The step that failed, for example `\"parse\"`, `\"listener\"`, `\"reconnect\"` or `\"subscriber\"`. */\n readonly where: string;\n /** The channel it concerned, when it concerned one. */\n readonly channel?: string;\n}\n\n/** What the subscription manager needs from the adapter that owns it. */\nexport interface TopicsDeps {\n /** The shared subscriber connection (opened on first use). */\n subscriber(): Promise<RedisSubscriber>;\n /** The retained messages after `after`, at most `limit`, oldest first. */\n history(channel: string, after: number, limit: number): Promise<readonly StoredMessage[]>;\n /** The live presence members of a channel. */\n members(channel: string): Promise<readonly Member[]>;\n /** Extends the lifetime of a channel's numbering. */\n keepAlive(channel: string): Promise<void>;\n /** Parses a pub/sub payload; undefined (after reporting) when it is malformed. */\n parseMessage(text: string, channel: string): StoredMessage | undefined;\n parsePresence(text: string, channel: string): PresenceEvent | undefined;\n messageTopic(channel: string): string;\n presenceTopic(channel: string): string;\n report(error: unknown, context: RedisErrorContext): void;\n /** Turns a failure into the error the adapter's callers see. */\n fail(error: unknown): Error;\n /** Milliseconds between keep-alive rounds. */\n keepAliveMs: number;\n}\n\n/** What the manager returns. */\nexport interface Topics {\n messages(channel: string, listener: (message: StoredMessage) => void): Promise<Unsubscribe>;\n presence(channel: string, listener: (event: PresenceEvent) => void): Promise<Unsubscribe>;\n /** Unsubscribes everything and stops the timers. */\n close(): Promise<void>;\n /** The number of topics currently subscribed (for tests and diagnostics). */\n readonly size: number;\n}\n\n/** Live messages held while a recovery reads history; a larger burst falls back to reading history again. */\nconst CATCH_UP_BUFFER = 1000;\n/** Messages read per history page during recovery. */\nconst CATCH_UP_PAGE = 500;\n/** Delays between attempts to recover after a reconnect. */\nconst RETRY_DELAYS_MS: readonly number[] = [50, 250, 1000];\n/** Topics subscribed again at once after a reconnect. */\nconst RESUBSCRIBE_BATCH = 64;\n/** Members tracked per presence channel; a larger channel is delivered unfiltered and not reconciled after a reconnect. */\nconst MAX_TRACKED_MEMBERS = 5000;\n\ntype MessageListener = (message: StoredMessage) => void;\ntype PresenceListener = (event: PresenceEvent) => void;\n\ninterface TopicBase {\n readonly channel: string;\n readonly topic: string;\n readonly handler: (text: string) => void;\n ready: Promise<void>;\n dead: boolean;\n}\n\ninterface MessageTopic extends TopicBase {\n readonly kind: \"message\";\n readonly listeners: Set<MessageListener>;\n /** The highest seq handed to the listeners: nothing at or below it is delivered again. */\n last: number;\n /** When the message at `last` was accepted: a lower seq accepted later is a restarted numbering, not a repeat. */\n lastAt: number;\n /** True while history is being read to fill a gap: live messages wait in `buffer`. */\n catching: boolean;\n /** Set when a reconnect happened while a recovery was running: it reads once more. */\n rerun: boolean;\n buffer: StoredMessage[];\n overflow: boolean;\n}\n\ninterface PresenceTopic extends TopicBase {\n readonly kind: \"presence\";\n readonly listeners: Set<PresenceListener>;\n /** The members as the listeners know them, by id. */\n members: Map<string, Member>;\n /** False when the channel is too large to track: events are then delivered unfiltered and not reconciled. */\n tracked: boolean;\n /** Events received while the state is being read; applied on top of it when it arrives. */\n pending: PresenceEvent[] | undefined;\n}\n\ntype Topic = MessageTopic | PresenceTopic;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Applies an event to a member map; true when it changed the map (a stale or repeated event does not). */\nfunction apply(members: Map<string, Member>, event: PresenceEvent): boolean {\n const known = members.get(event.member.id);\n if (event.op === \"leave\") return known !== undefined && members.delete(event.member.id);\n if (known !== undefined && known.v >= event.member.v) return false;\n members.set(event.member.id, event.member);\n return true;\n}\n\n/**\n * The shared subscriber connection of one adapter: topics are refcounted (one SUBSCRIBE per topic however many listeners), a\n * listener sees strictly increasing seqs and never a repeat, and after a reconnect the topics are subscribed again and what the\n * connection missed is read back from history (messages) or from the member list (presence).\n */\nexport function createTopics(deps: TopicsDeps): Topics {\n const topics = new Map<string, Topic>();\n let closed = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n let reconnecting = false;\n let again = false;\n let connected: RedisSubscriber | undefined;\n /** Removes the hooks this manager put on the subscriber (a subscriber the caller passed outlives the adapter). */\n const unhook: (() => void)[] = [];\n\n function reportTo(error: unknown, topic: Topic, where: string): void {\n deps.report(error, { where, channel: topic.channel });\n }\n\n // --- delivery ------------------------------------------------------------------------------------------------------\n\n function emit(topic: MessageTopic, message: StoredMessage): void {\n if (topic.dead) return;\n // Redis stamps `at` itself, so a repeat carries the time it first had. A lower seq with a later time means the keys were\n // lost (a flush, a failover to a replica that lacked them) and numbering began again: dropping it would silence the\n // channel until the new numbers pass the old ones. The hub tells its subscribers `reset` when it sees one.\n if (message.seq <= topic.last && message.at <= topic.lastAt) return;\n topic.last = message.seq;\n topic.lastAt = Math.max(topic.lastAt, message.at);\n for (const listener of [...topic.listeners]) {\n try {\n listener(message);\n } catch (error) {\n reportTo(error, topic, \"listener\");\n }\n }\n }\n\n function onMessage(topic: MessageTopic, text: string): void {\n const message = deps.parseMessage(text, topic.channel);\n if (message === undefined || topic.dead) return;\n if (topic.catching || (topic.last > 0 && message.seq > topic.last + 1)) {\n // Either history is being read, or this message jumped ahead of the cursor (a publish this connection missed): hold it\n // back and fill the gap from history first, so the listeners see every retained message in order.\n if (topic.buffer.length >= CATCH_UP_BUFFER) topic.overflow = true;\n else topic.buffer.push(message);\n if (!topic.catching) void heal(topic);\n return;\n }\n emit(topic, message);\n }\n\n function dispatchPresence(topic: PresenceTopic, event: PresenceEvent): void {\n for (const listener of [...topic.listeners]) {\n try {\n listener(event);\n } catch (error) {\n reportTo(error, topic, \"listener\");\n }\n }\n }\n\n function onPresence(topic: PresenceTopic, text: string): void {\n const event = deps.parsePresence(text, topic.channel);\n if (event === undefined || topic.dead) return;\n if (topic.pending !== undefined) {\n // The state is being read: deliver raw now, and fold the event into the state when it arrives.\n if (topic.pending.length < MAX_TRACKED_MEMBERS) topic.pending.push(event);\n else topic.tracked = false;\n dispatchPresence(topic, event);\n return;\n }\n if (topic.tracked && !apply(topic.members, event)) return;\n dispatchPresence(topic, event);\n }\n\n // --- recovery after a reconnect ------------------------------------------------------------------------------------\n\n /** Reads history from the cursor until it reaches the present, then lets the held live messages through. */\n async function readToPresent(topic: MessageTopic): Promise<void> {\n for (;;) {\n let more = true;\n while (more) {\n const page = await deps.history(topic.channel, topic.last, CATCH_UP_PAGE);\n if (topic.dead) return;\n for (const message of page) emit(topic, message);\n more = page.length >= CATCH_UP_PAGE;\n }\n if (!topic.overflow) return;\n // More live messages arrived than the buffer holds: read history again from the cursor.\n topic.overflow = false;\n topic.buffer.length = 0;\n }\n }\n\n /**\n * Fills a gap of the cursor from history. One at a time per topic; a request that arrives while one runs makes it read once\n * more. Gives up after a few failed attempts (the hub's own recovery is the next net) and lets the held messages through.\n */\n async function heal(topic: MessageTopic): Promise<void> {\n if (topic.catching) {\n topic.rerun = true;\n return;\n }\n topic.catching = true;\n try {\n let attempt = 0;\n while (!topic.dead && !closed) {\n topic.rerun = false;\n try {\n await readToPresent(topic);\n if (!topic.rerun) return;\n } catch (error) {\n reportTo(error, topic, \"recover\");\n const delay = RETRY_DELAYS_MS[attempt];\n attempt += 1;\n if (delay === undefined) return;\n await sleep(delay);\n }\n }\n } finally {\n topic.catching = false;\n topic.rerun = false;\n const held = topic.buffer.splice(0);\n for (const message of held) emit(topic, message);\n }\n }\n\n async function resync(topic: PresenceTopic): Promise<void> {\n if (!topic.tracked) return;\n const before = topic.members;\n topic.pending = [];\n let list: readonly Member[];\n try {\n list = await deps.members(topic.channel);\n } catch (error) {\n const held = topic.pending;\n topic.pending = undefined;\n for (const event of held) if (apply(before, event)) dispatchPresence(topic, event);\n throw error;\n }\n const held = topic.pending;\n topic.pending = undefined;\n if (topic.dead) return;\n const next = new Map<string, Member>(list.map((member) => [member.id, member]));\n for (const event of held) apply(next, event);\n const events: PresenceEvent[] = [];\n for (const [id, member] of before)\n if (!next.has(id)) events.push({ channel: topic.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const known = before.get(id);\n if (known === undefined) events.push({ channel: topic.channel, op: \"join\", member });\n else if (member.v > known.v) events.push({ channel: topic.channel, op: \"set\", member });\n }\n topic.members = next;\n if (next.size > MAX_TRACKED_MEMBERS) topic.tracked = false;\n for (const event of events) dispatchPresence(topic, event);\n }\n\n async function recover(topic: Topic): Promise<void> {\n for (let attempt = 0; ; attempt += 1) {\n try {\n const subscriber = await deps.subscriber();\n await subscriber.subscribe(topic.topic, topic.handler);\n if (topic.kind === \"message\") {\n if (topic.last > 0) await heal(topic);\n } else {\n await resync(topic);\n }\n return;\n } catch (error) {\n reportTo(error, topic, \"reconnect\");\n const delay = RETRY_DELAYS_MS[attempt];\n if (delay === undefined || topic.dead || closed) return;\n await sleep(delay);\n if (topic.dead || closed) return;\n }\n }\n }\n\n async function reconnect(): Promise<void> {\n if (reconnecting) {\n again = true;\n return;\n }\n reconnecting = true;\n try {\n do {\n again = false;\n const live = [...topics.values()].filter((topic) => !topic.dead);\n for (let from = 0; from < live.length && !closed; from += RESUBSCRIBE_BATCH)\n await Promise.all(live.slice(from, from + RESUBSCRIBE_BATCH).map(recover));\n } while (again && !closed);\n } finally {\n reconnecting = false;\n }\n }\n\n async function hook(): Promise<RedisSubscriber> {\n const subscriber = await deps.subscriber();\n if (connected !== subscriber) {\n connected = subscriber;\n unhook.push(\n subscriber.onReconnect(() => {\n void reconnect();\n }),\n );\n const stopErrors = subscriber.onError?.((error) =>\n deps.report(error, { where: \"subscriber\" }),\n );\n if (stopErrors !== undefined) unhook.push(stopErrors);\n }\n return subscriber;\n }\n\n // --- keeping numbering alive ---------------------------------------------------------------------------------------\n\n async function keepAliveRound(): Promise<void> {\n const channels = [...topics.values()].filter(\n (topic): topic is MessageTopic => topic.kind === \"message\" && !topic.dead,\n );\n for (let from = 0; from < channels.length; from += RESUBSCRIBE_BATCH)\n await Promise.all(\n channels.slice(from, from + RESUBSCRIBE_BATCH).map(async (topic) => {\n try {\n await deps.keepAlive(topic.channel);\n } catch (error) {\n reportTo(error, topic, \"keepAlive\");\n }\n }),\n );\n }\n\n function syncTimer(): void {\n const needed = [...topics.values()].some((topic) => topic.kind === \"message\");\n if (needed && timer === undefined && !closed) {\n timer = setInterval(() => void keepAliveRound(), deps.keepAliveMs);\n (timer as { unref?: () => void }).unref?.();\n } else if (!needed && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n // --- subscribing and releasing -------------------------------------------------------------------------------------\n\n async function open<T extends Topic>(topic: T): Promise<void> {\n topics.set(topic.topic, topic);\n syncTimer();\n topic.ready = (async () => {\n const subscriber = await hook();\n await subscriber.subscribe(topic.topic, topic.handler);\n if (topic.kind === \"presence\") await seed(topic);\n })();\n topic.ready.catch(() => {\n // The first caller's await reports it; here the half-open topic is dropped.\n topic.dead = true;\n if (topics.get(topic.topic) === topic) topics.delete(topic.topic);\n syncTimer();\n connected?.unsubscribe(topic.topic).catch(() => undefined);\n });\n await topic.ready;\n }\n\n async function seed(topic: PresenceTopic): Promise<void> {\n topic.pending = [];\n let list: readonly Member[];\n try {\n list = await deps.members(topic.channel);\n } catch (error) {\n topic.pending = undefined;\n throw error;\n }\n const held = topic.pending;\n topic.pending = undefined;\n const members = new Map<string, Member>(list.map((member) => [member.id, member]));\n for (const event of held) apply(members, event);\n topic.members = members;\n if (members.size > MAX_TRACKED_MEMBERS) topic.tracked = false;\n }\n\n /**\n * Returns the function that removes one listener. The last listener out drops the topic and sends UNSUBSCRIBE at once (not\n * after an await), so a subscribe of the same topic issued next is sent after it and wins.\n */\n function release(topic: Topic, remove: () => void): Unsubscribe {\n let done = false;\n return async () => {\n if (done) return;\n done = true;\n remove();\n if (topic.listeners.size > 0 || topic.dead) return;\n topic.dead = true;\n if (topics.get(topic.topic) === topic) topics.delete(topic.topic);\n syncTimer();\n if (closed || connected === undefined) return;\n try {\n await connected.unsubscribe(topic.topic);\n } catch (error) {\n reportTo(error, topic, \"unsubscribe\");\n }\n };\n }\n\n async function attach<T extends Topic>(\n topic: T | undefined,\n create: () => T,\n add: (topic: T) => Unsubscribe,\n ): Promise<Unsubscribe> {\n if (closed) throw deps.fail(new Error(\"the adapter is closed.\"));\n const found = topic ?? create();\n const stop = add(found);\n try {\n if (topic === undefined) await open(found);\n else await found.ready;\n } catch (error) {\n void stop();\n throw deps.fail(error);\n }\n if (closed || found.dead) {\n void stop();\n throw deps.fail(new Error(\"the adapter is closed.\"));\n }\n return stop;\n }\n\n return {\n messages(channel, listener) {\n const key = deps.messageTopic(channel);\n const existing = topics.get(key);\n return attach<MessageTopic>(\n existing?.kind === \"message\" ? existing : undefined,\n () => {\n const topic: MessageTopic = {\n kind: \"message\",\n channel,\n topic: key,\n listeners: new Set(),\n last: 0,\n lastAt: 0,\n catching: false,\n rerun: false,\n buffer: [],\n overflow: false,\n dead: false,\n ready: Promise.resolve(),\n handler: (text) => onMessage(topic, text),\n };\n return topic;\n },\n (topic) => {\n topic.listeners.add(listener);\n return release(topic, () => topic.listeners.delete(listener));\n },\n );\n },\n presence(channel, listener) {\n const key = deps.presenceTopic(channel);\n const existing = topics.get(key);\n return attach<PresenceTopic>(\n existing?.kind === \"presence\" ? existing : undefined,\n () => {\n const topic: PresenceTopic = {\n kind: \"presence\",\n channel,\n topic: key,\n listeners: new Set(),\n members: new Map(),\n tracked: true,\n pending: undefined,\n dead: false,\n ready: Promise.resolve(),\n handler: (text) => onPresence(topic, text),\n };\n return topic;\n },\n (topic) => {\n topic.listeners.add(listener);\n return release(topic, () => topic.listeners.delete(listener));\n },\n );\n },\n async close() {\n closed = true;\n for (const stop of unhook.splice(0)) stop();\n if (timer !== undefined) clearInterval(timer);\n timer = undefined;\n const open = [...topics.values()];\n topics.clear();\n for (const topic of open) topic.dead = true;\n if (open.length === 0) return;\n const subscriber = connected;\n if (subscriber === undefined) return;\n await Promise.all(\n open.map((topic) =>\n subscriber.unsubscribe(topic.topic).catch((error: unknown) => {\n reportTo(error, topic, \"unsubscribe\");\n }),\n ),\n );\n },\n get size() {\n return topics.size;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/dev/redis.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Adapter } from "./adapter.js";
|
|
2
2
|
import { IoredisLike, IoredisSubscriberLike, NodeRedisLike, NodeRedisSubscriberLike, RedisClient, RedisSubscriber, fromIoredis, fromNodeRedis } from "./redis/client.js";
|
|
3
3
|
import { RedisErrorContext } from "./redis/topics.js";
|
|
4
|
-
//#region src/redis.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Options of {@link redisAdapter}.
|
|
7
6
|
* @since 1.0.0
|
|
@@ -63,6 +62,5 @@ export interface RedisAdapterOptions {
|
|
|
63
62
|
* @since 1.0.0
|
|
64
63
|
*/
|
|
65
64
|
export declare function redisAdapter(options: RedisAdapterOptions): Adapter;
|
|
66
|
-
//#endregion
|
|
67
65
|
export { type IoredisLike, type IoredisSubscriberLike, type NodeRedisLike, type NodeRedisSubscriberLike, type RedisClient, type RedisErrorContext, type RedisSubscriber, fromIoredis, fromNodeRedis };
|
|
68
66
|
//# sourceMappingURL=redis.d.ts.map
|
package/dist/dev/redis.js
CHANGED
|
@@ -2,7 +2,7 @@ import { RealtimeError } from "./errors.js";
|
|
|
2
2
|
import { HISTORY, KEEPALIVE, PRESENCE_LIST, PRESENCE_REAP, PRESENCE_REMOVE, PRESENCE_SET, PRESENCE_TOUCH, PUBLISH } from "./redis/scripts.js";
|
|
3
3
|
import { createTopics } from "./redis/topics.js";
|
|
4
4
|
import { fromIoredis, fromNodeRedis } from "./redis/client.js";
|
|
5
|
-
|
|
5
|
+
// src/redis.ts
|
|
6
6
|
const DAY_MS = 864e5;
|
|
7
7
|
const MAX_LIMIT = 1e4;
|
|
8
8
|
const MAX_KEY_BYTES = 1024;
|
|
@@ -416,7 +416,6 @@ function redisAdapter(options) {
|
|
|
416
416
|
}
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
|
-
//#endregion
|
|
420
419
|
export { fromIoredis, fromNodeRedis, redisAdapter };
|
|
421
420
|
|
|
422
421
|
//# sourceMappingURL=redis.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Identity } from "./rules.js";
|
|
2
|
-
//#region src/server/http.d.ts
|
|
3
2
|
/** What a hub needs from the framework's request event. @since 1.0.0 */
|
|
4
3
|
export interface RequestLike {
|
|
5
4
|
readonly request: Request;
|
|
@@ -44,5 +43,4 @@ export interface SseLike {
|
|
|
44
43
|
readonly signal: AbortSignal;
|
|
45
44
|
readonly closed: boolean;
|
|
46
45
|
}
|
|
47
|
-
//#endregion
|
|
48
46
|
//# sourceMappingURL=http.d.ts.map
|
package/dist/dev/server/http.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { RealtimeError } from "../errors.js";
|
|
2
|
-
import { byteLength } from "../protocol.js";
|
|
3
2
|
import { TokenBucket } from "./limits.js";
|
|
4
|
-
|
|
3
|
+
// src/server/http.ts
|
|
5
4
|
const KEY = "$realtime";
|
|
5
|
+
/** Marks a socket that closed before its connection existed. */
|
|
6
|
+
const GONE = "$realtime:gone";
|
|
6
7
|
const MAX_FRAMES_PER_POST = 64;
|
|
7
8
|
const MAX_SUBSCRIPTIONS_IN_QUERY = 64;
|
|
8
9
|
const MAX_BUCKETS = 1e4;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
message
|
|
13
|
-
} }), {
|
|
10
|
+
/** A JSON answer that no cache may keep. */
|
|
11
|
+
function jsonResponse(status, body) {
|
|
12
|
+
return new Response(body, {
|
|
14
13
|
status,
|
|
15
14
|
headers: {
|
|
16
15
|
"content-type": "application/json",
|
|
@@ -18,6 +17,27 @@ function refuse(status, code, message) {
|
|
|
18
17
|
}
|
|
19
18
|
});
|
|
20
19
|
}
|
|
20
|
+
function refuse(status, code, message) {
|
|
21
|
+
return jsonResponse(status, JSON.stringify({ error: {
|
|
22
|
+
code,
|
|
23
|
+
message
|
|
24
|
+
} }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reads a request body as text, stopping at `max` bytes: a body that never ends must not be held in memory before it is
|
|
28
|
+
* refused. Undefined when the body is larger than `max`.
|
|
29
|
+
*/
|
|
30
|
+
async function readBounded(request, max) {
|
|
31
|
+
const decoder = new TextDecoder();
|
|
32
|
+
let text = "";
|
|
33
|
+
let bytes = 0;
|
|
34
|
+
for await (const chunk of request.body ?? []) {
|
|
35
|
+
bytes += chunk.byteLength;
|
|
36
|
+
if (bytes > max) return void 0;
|
|
37
|
+
text += decoder.decode(chunk, { stream: true });
|
|
38
|
+
}
|
|
39
|
+
return text + decoder.decode();
|
|
40
|
+
}
|
|
21
41
|
/** Whether the request's `Origin` header is acceptable. A missing header is allowed (see `HubRouteOptions.allowedOrigins`). */
|
|
22
42
|
function originAllowed(request, allowed) {
|
|
23
43
|
const origin = request.headers.get("origin");
|
|
@@ -57,6 +77,7 @@ function websocketRoute(hub, options = {}) {
|
|
|
57
77
|
},
|
|
58
78
|
async open(ws, event) {
|
|
59
79
|
const result = await identify(options, event);
|
|
80
|
+
if (ws.signal.aborted || ws.data[GONE] === true) return;
|
|
60
81
|
if ("refusal" in result) {
|
|
61
82
|
ws.close(1008, "unauthorized");
|
|
62
83
|
return;
|
|
@@ -77,6 +98,7 @@ function websocketRoute(hub, options = {}) {
|
|
|
77
98
|
connection.receive(message);
|
|
78
99
|
},
|
|
79
100
|
close(ws) {
|
|
101
|
+
ws.data[GONE] = true;
|
|
80
102
|
ws.data[KEY]?.closed();
|
|
81
103
|
},
|
|
82
104
|
error(ws, error) {
|
|
@@ -137,13 +159,9 @@ function sseRoute(hub, options = {}) {
|
|
|
137
159
|
return;
|
|
138
160
|
}
|
|
139
161
|
const result = await identify(options, event);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
const entries = declared(new URL(event.request.url));
|
|
145
|
-
if (entries === void 0) {
|
|
146
|
-
await refuseStream(stream, "bad subscription list");
|
|
162
|
+
const entries = "refusal" in result ? void 0 : declared(new URL(event.request.url));
|
|
163
|
+
if ("refusal" in result || entries === void 0) {
|
|
164
|
+
await refuseStream(stream, "refusal" in result ? "unauthorized" : "bad subscription list");
|
|
147
165
|
return;
|
|
148
166
|
}
|
|
149
167
|
let pendingBytes = 0;
|
|
@@ -167,9 +185,9 @@ function sseRoute(hub, options = {}) {
|
|
|
167
185
|
}, result.identity);
|
|
168
186
|
entries.forEach((entry, at) => {
|
|
169
187
|
connection.receive(JSON.stringify({
|
|
188
|
+
...entry,
|
|
170
189
|
t: "sub",
|
|
171
|
-
n: at + 1
|
|
172
|
-
...entry
|
|
190
|
+
n: at + 1
|
|
173
191
|
}));
|
|
174
192
|
});
|
|
175
193
|
if (!stream.signal.aborted) await new Promise((resolve) => {
|
|
@@ -216,8 +234,8 @@ function postRoute(hub, options = {}) {
|
|
|
216
234
|
if (!originAllowed(request, options.allowedOrigins)) return refuse(403, "forbidden", "origin not allowed");
|
|
217
235
|
const result = await identify(options, event);
|
|
218
236
|
if ("refusal" in result) return result.refusal;
|
|
219
|
-
const text = await request.
|
|
220
|
-
if (
|
|
237
|
+
const text = await readBounded(request, hub.limits.maxMessageBytes * 8);
|
|
238
|
+
if (text === void 0) return refuse(413, "too_large", "the request is too large");
|
|
221
239
|
let body;
|
|
222
240
|
try {
|
|
223
241
|
body = JSON.parse(text);
|
|
@@ -247,6 +265,7 @@ function postRoute(hub, options = {}) {
|
|
|
247
265
|
const item = frame;
|
|
248
266
|
try {
|
|
249
267
|
if (typeof item.c !== "string" || typeof cid !== "string") throw new RealtimeError("bad_frame", "pres needs c and the connection id");
|
|
268
|
+
if (!bucketFor(key).take(Date.now())) throw new RealtimeError("rate_limited");
|
|
250
269
|
await hub.setMemberState(result.identity, item.c, cid, item.s ?? null);
|
|
251
270
|
} catch (error) {
|
|
252
271
|
const code = error instanceof RealtimeError ? error.code : "unavailable";
|
|
@@ -271,16 +290,9 @@ function postRoute(hub, options = {}) {
|
|
|
271
290
|
} finally {
|
|
272
291
|
connection.closed();
|
|
273
292
|
}
|
|
274
|
-
return
|
|
275
|
-
status: 200,
|
|
276
|
-
headers: {
|
|
277
|
-
"content-type": "application/json",
|
|
278
|
-
"cache-control": "no-store"
|
|
279
|
-
}
|
|
280
|
-
});
|
|
293
|
+
return jsonResponse(200, `{"frames":[${answers.join(",")}]}`);
|
|
281
294
|
};
|
|
282
295
|
}
|
|
283
|
-
//#endregion
|
|
284
296
|
export { postRoute, sseRoute, websocketRoute };
|
|
285
297
|
|
|
286
298
|
//# sourceMappingURL=http.js.map
|