@shadow-js/realtime 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +114 -2
  4. package/dist/dev/adapter.d.ts +75 -0
  5. package/dist/dev/client/core.d.ts +24 -0
  6. package/dist/dev/client/core.js +845 -0
  7. package/dist/dev/client/core.js.map +1 -0
  8. package/dist/dev/client/index.d.ts +5 -0
  9. package/dist/dev/client/index.js +3 -0
  10. package/dist/dev/client/presence.js +23 -0
  11. package/dist/dev/client/presence.js.map +1 -0
  12. package/dist/dev/client/sse.js +76 -0
  13. package/dist/dev/client/sse.js.map +1 -0
  14. package/dist/dev/client/types.d.ts +256 -0
  15. package/dist/dev/client/util.js +30 -0
  16. package/dist/dev/client/util.js.map +1 -0
  17. package/dist/dev/client/ws.js +39 -0
  18. package/dist/dev/client/ws.js.map +1 -0
  19. package/dist/dev/cloudflare/adapter.d.ts +80 -0
  20. package/dist/dev/cloudflare/adapter.js +347 -0
  21. package/dist/dev/cloudflare/adapter.js.map +1 -0
  22. package/dist/dev/cloudflare/core.js +701 -0
  23. package/dist/dev/cloudflare/core.js.map +1 -0
  24. package/dist/dev/cloudflare/link.js +397 -0
  25. package/dist/dev/cloudflare/link.js.map +1 -0
  26. package/dist/dev/cloudflare/object.d.ts +82 -0
  27. package/dist/dev/cloudflare/object.js +214 -0
  28. package/dist/dev/cloudflare/object.js.map +1 -0
  29. package/dist/dev/cloudflare/types.d.ts +100 -0
  30. package/dist/dev/cloudflare/wire.js +284 -0
  31. package/dist/dev/cloudflare/wire.js.map +1 -0
  32. package/dist/dev/cloudflare.d.ts +4 -0
  33. package/dist/dev/cloudflare.js +3 -0
  34. package/dist/dev/errors.d.ts +25 -0
  35. package/dist/dev/errors.js +24 -0
  36. package/dist/dev/errors.js.map +1 -0
  37. package/dist/dev/hooks.d.ts +110 -0
  38. package/dist/dev/hooks.js +162 -0
  39. package/dist/dev/hooks.js.map +1 -0
  40. package/dist/dev/index.d.ts +4 -0
  41. package/dist/dev/index.js +3 -0
  42. package/dist/dev/memory.d.ts +33 -0
  43. package/dist/dev/memory.js +255 -0
  44. package/dist/dev/memory.js.map +1 -0
  45. package/dist/dev/protocol.d.ts +211 -0
  46. package/dist/dev/protocol.js +400 -0
  47. package/dist/dev/protocol.js.map +1 -0
  48. package/dist/dev/redis/client.d.ts +139 -0
  49. package/dist/dev/redis/client.js +146 -0
  50. package/dist/dev/redis/client.js.map +1 -0
  51. package/dist/dev/redis/scripts.js +275 -0
  52. package/dist/dev/redis/scripts.js.map +1 -0
  53. package/dist/dev/redis/topics.d.ts +8 -0
  54. package/dist/dev/redis/topics.js +377 -0
  55. package/dist/dev/redis/topics.js.map +1 -0
  56. package/dist/dev/redis.d.ts +68 -0
  57. package/dist/dev/redis.js +422 -0
  58. package/dist/dev/redis.js.map +1 -0
  59. package/dist/dev/server/http.d.ts +48 -0
  60. package/dist/dev/server/http.js +286 -0
  61. package/dist/dev/server/http.js.map +1 -0
  62. package/dist/dev/server/hub.d.ts +114 -0
  63. package/dist/dev/server/hub.js +827 -0
  64. package/dist/dev/server/hub.js.map +1 -0
  65. package/dist/dev/server/index.d.ts +47 -0
  66. package/dist/dev/server/index.js +41 -0
  67. package/dist/dev/server/index.js.map +1 -0
  68. package/dist/dev/server/limits.d.ts +54 -0
  69. package/dist/dev/server/limits.js +85 -0
  70. package/dist/dev/server/limits.js.map +1 -0
  71. package/dist/dev/server/rules.d.ts +43 -0
  72. package/dist/dev/server/rules.js +72 -0
  73. package/dist/dev/server/rules.js.map +1 -0
  74. package/dist/dev/testing.d.ts +115 -0
  75. package/dist/dev/testing.js +427 -0
  76. package/dist/dev/testing.js.map +1 -0
  77. package/dist/prod/client/core.js +2 -0
  78. package/dist/prod/client/core.js.map +1 -0
  79. package/dist/prod/client/index.js +1 -0
  80. package/dist/prod/client/presence.js +2 -0
  81. package/dist/prod/client/presence.js.map +1 -0
  82. package/dist/prod/client/sse.js +5 -0
  83. package/dist/prod/client/sse.js.map +1 -0
  84. package/dist/prod/client/util.js +2 -0
  85. package/dist/prod/client/util.js.map +1 -0
  86. package/dist/prod/client/ws.js +2 -0
  87. package/dist/prod/client/ws.js.map +1 -0
  88. package/dist/prod/cloudflare/adapter.js +3 -0
  89. package/dist/prod/cloudflare/adapter.js.map +1 -0
  90. package/dist/prod/cloudflare/core.js +2 -0
  91. package/dist/prod/cloudflare/core.js.map +1 -0
  92. package/dist/prod/cloudflare/link.js +2 -0
  93. package/dist/prod/cloudflare/link.js.map +1 -0
  94. package/dist/prod/cloudflare/object.js +2 -0
  95. package/dist/prod/cloudflare/object.js.map +1 -0
  96. package/dist/prod/cloudflare/wire.js +2 -0
  97. package/dist/prod/cloudflare/wire.js.map +1 -0
  98. package/dist/prod/cloudflare.js +1 -0
  99. package/dist/prod/errors.js +2 -0
  100. package/dist/prod/errors.js.map +1 -0
  101. package/dist/prod/hooks.js +2 -0
  102. package/dist/prod/hooks.js.map +1 -0
  103. package/dist/prod/index.js +1 -0
  104. package/dist/prod/memory.js +2 -0
  105. package/dist/prod/memory.js.map +1 -0
  106. package/dist/prod/protocol.js +2 -0
  107. package/dist/prod/protocol.js.map +1 -0
  108. package/dist/prod/redis/client.js +2 -0
  109. package/dist/prod/redis/client.js.map +1 -0
  110. package/dist/prod/redis/scripts.js +157 -0
  111. package/dist/prod/redis/scripts.js.map +1 -0
  112. package/dist/prod/redis/topics.js +2 -0
  113. package/dist/prod/redis/topics.js.map +1 -0
  114. package/dist/prod/redis.js +2 -0
  115. package/dist/prod/redis.js.map +1 -0
  116. package/dist/prod/server/http.js +2 -0
  117. package/dist/prod/server/http.js.map +1 -0
  118. package/dist/prod/server/hub.js +2 -0
  119. package/dist/prod/server/hub.js.map +1 -0
  120. package/dist/prod/server/index.js +2 -0
  121. package/dist/prod/server/index.js.map +1 -0
  122. package/dist/prod/server/limits.js +2 -0
  123. package/dist/prod/server/limits.js.map +1 -0
  124. package/dist/prod/server/rules.js +2 -0
  125. package/dist/prod/server/rules.js.map +1 -0
  126. package/dist/prod/testing.js +2 -0
  127. package/dist/prod/testing.js.map +1 -0
  128. package/package.json +105 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scripts.js","names":[],"sources":["../../../src/redis/scripts.ts"],"sourcesContent":["/**\n * The Lua scripts of the Redis adapter, and the SHA-1 that names them on the server. Every script touches only keys of one\n * channel (one hash slot) and reads the clock from the server (`TIME`), so servers with skewed clocks agree.\n */\n\n/** A script with its source and the SHA-1 `EVALSHA` uses. */\nexport interface Script {\n readonly source: string;\n readonly sha: string;\n}\n\nfunction rotateLeft(value: number, bits: number): number {\n return ((value << bits) | (value >>> (32 - bits))) >>> 0;\n}\n\n/**\n * SHA-1 of an ASCII or UTF-8 text, as lowercase hex. Pure TypeScript, so the adapter needs neither `node:crypto` nor a\n * round trip to `SCRIPT LOAD`.\n */\nexport function sha1(text: string): string {\n const input = new TextEncoder().encode(text);\n const total = ((input.length + 9 + 63) >> 6) << 6;\n const padded = new Uint8Array(total);\n padded.set(input);\n padded[input.length] = 0x80;\n const view = new DataView(padded.buffer);\n const bits = input.length * 8;\n view.setUint32(total - 8, Math.floor(bits / 2 ** 32));\n view.setUint32(total - 4, bits >>> 0);\n let h0 = 0x67452301;\n let h1 = 0xefcdab89;\n let h2 = 0x98badcfe;\n let h3 = 0x10325476;\n let h4 = 0xc3d2e1f0;\n const w = new Uint32Array(80);\n for (let offset = 0; offset < total; offset += 64) {\n for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4);\n for (let i = 16; i < 80; i += 1)\n w[i] = rotateLeft(\n (w[i - 3] as number) ^ (w[i - 8] as number) ^ (w[i - 14] as number) ^ (w[i - 16] as number),\n 1,\n );\n let a = h0;\n let b = h1;\n let c = h2;\n let d = h3;\n let e = h4;\n for (let i = 0; i < 80; i += 1) {\n let f: number;\n let k: number;\n if (i < 20) {\n f = (b & c) | (~b & d);\n k = 0x5a827999;\n } else if (i < 40) {\n f = b ^ c ^ d;\n k = 0x6ed9eba1;\n } else if (i < 60) {\n f = (b & c) | (b & d) | (c & d);\n k = 0x8f1bbcdc;\n } else {\n f = b ^ c ^ d;\n k = 0xca62c1d6;\n }\n const next = (rotateLeft(a, 5) + (f >>> 0) + e + k + (w[i] as number)) >>> 0;\n e = d;\n d = c;\n c = rotateLeft(b, 30);\n b = a;\n a = next;\n }\n h0 = (h0 + a) >>> 0;\n h1 = (h1 + b) >>> 0;\n h2 = (h2 + c) >>> 0;\n h3 = (h3 + d) >>> 0;\n h4 = (h4 + e) >>> 0;\n }\n return [h0, h1, h2, h3, h4].map((word) => word.toString(16).padStart(8, \"0\")).join(\"\");\n}\n\nfunction script(source: string): Script {\n return { source, sha: sha1(source) };\n}\n\n/** Lua helpers shared by the scripts that read the clock. */\nconst CLOCK = `\nlocal function now_ms()\n local t = redis.call('TIME')\n return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal function num(n) return string.format('%.0f', n) end\n`;\n\n/**\n * Publish. KEYS: meta hash, log zset (score = seq, member = the message), time zset (score = accepted-at, member = zero-padded\n * seq), optional idempotency key. ARGV: history, historyTtlMs, idempotencyTtlMs, channelTtlMs, topic, message tail.\n * Returns `{seq, duplicate}`.\n */\nexport const PUBLISH: Script = script(`${CLOCK}\nlocal meta, log, tix, idem = KEYS[1], KEYS[2], KEYS[3], KEYS[4]\nif idem then\n local prev = redis.call('GET', idem)\n if prev then return {tonumber(prev), 1} end\nend\nlocal history = tonumber(ARGV[1])\nlocal histTtl = tonumber(ARGV[2])\nlocal idemTtl = tonumber(ARGV[3])\nlocal chanTtl = tonumber(ARGV[4])\nlocal at = now_ms()\nlocal seq = redis.call('HINCRBY', meta, 's', 1)\nif history > 0 then\n local newest = redis.call('ZREVRANGE', tix, 0, 0, 'WITHSCORES')\n if newest[2] and tonumber(newest[2]) > at then at = tonumber(newest[2]) end\nend\nlocal msg = '{\"s\":' .. num(seq) .. ',\"a\":' .. num(at) .. ',' .. ARGV[6]\nif history > 0 then\n local pad = string.format('%016.0f', seq)\n redis.call('ZADD', log, seq, msg)\n redis.call('ZADD', tix, at, pad)\n local n = redis.call('ZCARD', log)\n if n ~= redis.call('ZCARD', tix) then\n redis.call('DEL', log, tix)\n redis.call('ZADD', log, seq, msg)\n redis.call('ZADD', tix, at, pad)\n else\n local drop = n - history\n local old = redis.call('ZCOUNT', tix, '-inf', '(' .. num(at - histTtl))\n if old > drop then drop = old end\n if drop > 0 then\n redis.call('ZREMRANGEBYRANK', log, 0, drop - 1)\n redis.call('ZREMRANGEBYRANK', tix, 0, drop - 1)\n end\n end\n redis.call('PEXPIRE', log, histTtl)\n redis.call('PEXPIRE', tix, histTtl)\nelse\n redis.call('DEL', log, tix)\nend\nredis.call('HSET', meta, 't', ARGV[2])\nlocal keep = chanTtl\nif histTtl > keep then keep = histTtl end\nif idem and idemTtl > keep then keep = idemTtl end\nredis.call('PEXPIRE', meta, keep)\nif idem then redis.call('SET', idem, num(seq), 'PX', idemTtl) end\nredis.call('PUBLISH', ARGV[5], msg)\nreturn {seq, 0}\n`);\n\n/**\n * History. KEYS: meta, log, time. ARGV: after, limit. Returns `{head, oldest, message...}`; a message older than the channel's\n * history lifetime is not returned even when no publish has trimmed it yet.\n */\nexport const HISTORY: Script = script(`${CLOCK}\nlocal meta, log, tix = KEYS[1], KEYS[2], KEYS[3]\nlocal head = tonumber(redis.call('HGET', meta, 's') or '0')\nlocal ttl = redis.call('HGET', meta, 't')\nlocal low = tonumber(ARGV[1])\nlocal limit = tonumber(ARGV[2])\nlocal skip = 0\nif ttl then\n skip = redis.call('ZCOUNT', tix, '-inf', '(' .. num(now_ms() - tonumber(ttl)))\nend\nlocal oldest = 0\nlocal first = redis.call('ZRANGE', tix, skip, skip)\nif first[1] then oldest = tonumber(first[1]) end\nif skip > 0 then\n local expired = tonumber(redis.call('ZRANGE', tix, skip - 1, skip - 1)[1])\n if expired and expired > low then low = expired end\nend\nlocal out = {head, oldest}\nif limit > 0 and oldest > 0 then\n local rows = redis.call('ZRANGEBYSCORE', log, '(' .. num(low), '+inf', 'LIMIT', 0, limit)\n for i = 1, #rows do out[#out + 1] = rows[i] end\nend\nreturn out\n`);\n\n/**\n * Keeps the numbering of a channel that has listeners alive. KEYS: meta. ARGV: channelTtlMs. Only ever extends the lifetime.\n */\nexport const KEEPALIVE: Script = script(`\nlocal ms = tonumber(ARGV[1])\nlocal left = redis.call('PTTL', KEYS[1])\nif left >= 0 and left < ms then redis.call('PEXPIRE', KEYS[1], ms) end\nreturn 1\n`);\n\n/** Lua: extends the lifetime of both presence keys to `keep` milliseconds, never shortens it. */\nconst EXTEND = `\nlocal function extend(keep)\n for i = 1, 2 do\n local left = redis.call('PTTL', KEYS[i])\n if left == -1 or (left >= 0 and left < keep) then redis.call('PEXPIRE', KEYS[i], keep) end\n end\nend\n`;\n\n/**\n * Presence set. KEYS: members hash (id -> stored), expiry zset (id -> expires). ARGV: id, version, ttlMs, topic, stored, member\n * JSON, graceMs. Returns 0 for an ignored older version, else 1. Publishes join (new) or set (higher version).\n */\nexport const PRESENCE_SET: Script = script(`${CLOCK}${EXTEND}\nlocal id, version, ttl = ARGV[1], tonumber(ARGV[2]), tonumber(ARGV[3])\nlocal cur = redis.call('HGET', KEYS[1], id)\nlocal op = nil\nif cur then\n local known = tonumber(string.match(cur, '^{\"v\":(%d+),'))\n if known and known > version then return 0 end\n if known == nil or known < version then op = 'set' end\nelse\n op = 'join'\nend\nredis.call('HSET', KEYS[1], id, ARGV[5])\nredis.call('ZADD', KEYS[2], num(now_ms() + ttl), id)\nextend(ttl + tonumber(ARGV[7]))\nif op then redis.call('PUBLISH', ARGV[4], '{\"o\":\"' .. op .. '\",\"m\":' .. ARGV[6] .. '}') end\nreturn 1\n`);\n\n/**\n * Presence remove. KEYS: hash, zset. ARGV: id, topic. Returns 1 only for the call that removed the member (which alone\n * publishes the leave), else 0.\n */\nexport const PRESENCE_REMOVE: Script = script(`\nlocal cur = redis.call('HGET', KEYS[1], ARGV[1])\nredis.call('ZREM', KEYS[2], ARGV[1])\nif not cur then return 0 end\nredis.call('HDEL', KEYS[1], ARGV[1])\nlocal member = string.match(cur, '^{\"v\":%d+,\"m\":(.*)}$')\nif member then redis.call('PUBLISH', ARGV[2], '{\"o\":\"leave\",\"m\":' .. member .. '}') end\nreturn 1\n`);\n\n/**\n * Presence touch. KEYS: hash, zset. ARGV: ttlMs, graceMs, id... Refreshes the expiry of the members that exist and returns the\n * ids that do not.\n */\nexport const PRESENCE_TOUCH: Script = script(`${CLOCK}${EXTEND}\nlocal ttl = tonumber(ARGV[1])\nlocal expires = num(now_ms() + ttl)\nlocal missing = {}\nlocal found = false\nfor i = 3, #ARGV do\n if redis.call('HEXISTS', KEYS[1], ARGV[i]) == 1 then\n redis.call('ZADD', KEYS[2], expires, ARGV[i])\n found = true\n else\n missing[#missing + 1] = ARGV[i]\n end\nend\nif found then extend(ttl + tonumber(ARGV[2])) end\nreturn missing\n`);\n\n/**\n * Presence reap. KEYS: hash, zset. ARGV: topic, limit. Removes up to `limit` expired members, publishing one leave for each,\n * and returns their stored values.\n */\nexport const PRESENCE_REAP: Script = script(`${CLOCK}\nlocal ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', num(now_ms()), 'LIMIT', 0, tonumber(ARGV[2]))\nlocal out = {}\nfor i = 1, #ids do\n local cur = redis.call('HGET', KEYS[1], ids[i])\n redis.call('ZREM', KEYS[2], ids[i])\n if cur then\n redis.call('HDEL', KEYS[1], ids[i])\n out[#out + 1] = cur\n local member = string.match(cur, '^{\"v\":%d+,\"m\":(.*)}$')\n if member then redis.call('PUBLISH', ARGV[1], '{\"o\":\"leave\",\"m\":' .. member .. '}') end\n end\nend\nreturn out\n`);\n\n/** Presence list. KEYS: hash, zset. Returns the stored values of the members that have not expired. */\nexport const PRESENCE_LIST: Script = script(`${CLOCK}\nlocal ids = redis.call('ZRANGEBYSCORE', KEYS[2], '(' .. num(now_ms()), '+inf')\nlocal out = {}\nlocal step = 400\nfor from = 1, #ids, step do\n local to = from + step - 1\n if to > #ids then to = #ids end\n local values = redis.call('HMGET', KEYS[1], unpack(ids, from, to))\n for i = 1, #values do\n if values[i] then out[#out + 1] = values[i] end\n end\nend\nreturn out\n`);\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ //#region src/redis/topics.d.ts
2
+ /** Where a failure the adapter handled came from. */
3
+ export interface RedisErrorContext {
4
+ readonly where: string;
5
+ readonly channel?: string;
6
+ }
7
+ //#endregion
8
+ //# sourceMappingURL=topics.d.ts.map
@@ -0,0 +1,377 @@
1
+ //#region src/redis/topics.ts
2
+ /** Live messages held while a recovery reads history; a larger burst falls back to reading history again. */
3
+ const CATCH_UP_BUFFER = 1e3;
4
+ /** Messages read per history page during recovery. */
5
+ const CATCH_UP_PAGE = 500;
6
+ /** Delays between attempts to recover after a reconnect. */
7
+ const RETRY_DELAYS_MS = [
8
+ 50,
9
+ 250,
10
+ 1e3
11
+ ];
12
+ /** Topics subscribed again at once after a reconnect. */
13
+ const RESUBSCRIBE_BATCH = 64;
14
+ /** Members tracked per presence channel; a larger channel is delivered unfiltered and not reconciled after a reconnect. */
15
+ const MAX_TRACKED_MEMBERS = 5e3;
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+ /** Applies an event to a member map; true when it changed the map (a stale or repeated event does not). */
20
+ function apply(members, event) {
21
+ const known = members.get(event.member.id);
22
+ if (event.op === "leave") return known !== void 0 && members.delete(event.member.id);
23
+ if (known !== void 0 && known.v >= event.member.v) return false;
24
+ members.set(event.member.id, event.member);
25
+ return true;
26
+ }
27
+ /**
28
+ * The shared subscriber connection of one adapter: topics are refcounted (one SUBSCRIBE per topic however many listeners), a
29
+ * listener sees strictly increasing seqs and never a repeat, and after a reconnect the topics are subscribed again and what the
30
+ * connection missed is read back from history (messages) or from the member list (presence).
31
+ */
32
+ function createTopics(deps) {
33
+ const topics = /* @__PURE__ */ new Map();
34
+ let closed = false;
35
+ let timer;
36
+ let reconnecting = false;
37
+ let again = false;
38
+ let connected;
39
+ /** Removes the hooks this manager put on the subscriber (a subscriber the caller passed outlives the adapter). */
40
+ const unhook = [];
41
+ function reportTo(error, topic, where) {
42
+ deps.report(error, {
43
+ where,
44
+ channel: topic.channel
45
+ });
46
+ }
47
+ function emit(topic, message) {
48
+ if (topic.dead || message.seq <= topic.last) return;
49
+ topic.last = message.seq;
50
+ for (const listener of [...topic.listeners]) try {
51
+ listener(message);
52
+ } catch (error) {
53
+ reportTo(error, topic, "listener");
54
+ }
55
+ }
56
+ function onMessage(topic, text) {
57
+ const message = deps.parseMessage(text, topic.channel);
58
+ if (message === void 0 || topic.dead) return;
59
+ if (topic.catching || topic.last > 0 && message.seq > topic.last + 1) {
60
+ if (topic.buffer.length >= CATCH_UP_BUFFER) topic.overflow = true;
61
+ else topic.buffer.push(message);
62
+ if (!topic.catching) heal(topic);
63
+ return;
64
+ }
65
+ emit(topic, message);
66
+ }
67
+ function dispatchPresence(topic, event) {
68
+ for (const listener of [...topic.listeners]) try {
69
+ listener(event);
70
+ } catch (error) {
71
+ reportTo(error, topic, "listener");
72
+ }
73
+ }
74
+ function onPresence(topic, text) {
75
+ const event = deps.parsePresence(text, topic.channel);
76
+ if (event === void 0 || topic.dead) return;
77
+ if (topic.pending !== void 0) {
78
+ if (topic.pending.length < MAX_TRACKED_MEMBERS) topic.pending.push(event);
79
+ else topic.tracked = false;
80
+ dispatchPresence(topic, event);
81
+ return;
82
+ }
83
+ if (topic.tracked && !apply(topic.members, event)) return;
84
+ dispatchPresence(topic, event);
85
+ }
86
+ /** Reads history from the cursor until it reaches the present, then lets the held live messages through. */
87
+ async function readToPresent(topic) {
88
+ for (;;) {
89
+ let more = true;
90
+ while (more) {
91
+ const page = await deps.history(topic.channel, topic.last, CATCH_UP_PAGE);
92
+ if (topic.dead) return;
93
+ for (const message of page) emit(topic, message);
94
+ more = page.length >= CATCH_UP_PAGE;
95
+ }
96
+ if (!topic.overflow) return;
97
+ topic.overflow = false;
98
+ topic.buffer.length = 0;
99
+ }
100
+ }
101
+ /**
102
+ * 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
103
+ * more. Gives up after a few failed attempts (the hub's own recovery is the next net) and lets the held messages through.
104
+ */
105
+ async function heal(topic) {
106
+ if (topic.catching) {
107
+ topic.rerun = true;
108
+ return;
109
+ }
110
+ topic.catching = true;
111
+ try {
112
+ let attempt = 0;
113
+ while (!topic.dead && !closed) {
114
+ topic.rerun = false;
115
+ try {
116
+ await readToPresent(topic);
117
+ if (!topic.rerun) return;
118
+ } catch (error) {
119
+ reportTo(error, topic, "recover");
120
+ const delay = RETRY_DELAYS_MS[attempt];
121
+ attempt += 1;
122
+ if (delay === void 0) return;
123
+ await sleep(delay);
124
+ }
125
+ }
126
+ } finally {
127
+ topic.catching = false;
128
+ topic.rerun = false;
129
+ const held = topic.buffer.splice(0);
130
+ for (const message of held) emit(topic, message);
131
+ }
132
+ }
133
+ async function resync(topic) {
134
+ if (!topic.tracked) return;
135
+ const before = topic.members;
136
+ topic.pending = [];
137
+ let list;
138
+ try {
139
+ list = await deps.members(topic.channel);
140
+ } catch (error) {
141
+ const held = topic.pending;
142
+ topic.pending = void 0;
143
+ for (const event of held) if (apply(before, event)) dispatchPresence(topic, event);
144
+ throw error;
145
+ }
146
+ const held = topic.pending;
147
+ topic.pending = void 0;
148
+ if (topic.dead) return;
149
+ const next = new Map(list.map((member) => [member.id, member]));
150
+ for (const event of held) apply(next, event);
151
+ const events = [];
152
+ for (const [id, member] of before) if (!next.has(id)) events.push({
153
+ channel: topic.channel,
154
+ op: "leave",
155
+ member
156
+ });
157
+ for (const [id, member] of next) {
158
+ const known = before.get(id);
159
+ if (known === void 0) events.push({
160
+ channel: topic.channel,
161
+ op: "join",
162
+ member
163
+ });
164
+ else if (member.v > known.v) events.push({
165
+ channel: topic.channel,
166
+ op: "set",
167
+ member
168
+ });
169
+ }
170
+ topic.members = next;
171
+ if (next.size > MAX_TRACKED_MEMBERS) topic.tracked = false;
172
+ for (const event of events) dispatchPresence(topic, event);
173
+ }
174
+ async function recover(topic) {
175
+ for (let attempt = 0;; attempt += 1) try {
176
+ await (await deps.subscriber()).subscribe(topic.topic, topic.handler);
177
+ if (topic.kind === "message") {
178
+ if (topic.last > 0) await heal(topic);
179
+ } else await resync(topic);
180
+ return;
181
+ } catch (error) {
182
+ reportTo(error, topic, "reconnect");
183
+ const delay = RETRY_DELAYS_MS[attempt];
184
+ if (delay === void 0 || topic.dead || closed) return;
185
+ await sleep(delay);
186
+ if (topic.dead || closed) return;
187
+ }
188
+ }
189
+ async function reconnect() {
190
+ if (reconnecting) {
191
+ again = true;
192
+ return;
193
+ }
194
+ reconnecting = true;
195
+ try {
196
+ do {
197
+ again = false;
198
+ const live = [...topics.values()].filter((topic) => !topic.dead);
199
+ for (let from = 0; from < live.length && !closed; from += RESUBSCRIBE_BATCH) await Promise.all(live.slice(from, from + RESUBSCRIBE_BATCH).map(recover));
200
+ } while (again && !closed);
201
+ } finally {
202
+ reconnecting = false;
203
+ }
204
+ }
205
+ async function hook() {
206
+ const subscriber = await deps.subscriber();
207
+ if (connected !== subscriber) {
208
+ connected = subscriber;
209
+ unhook.push(subscriber.onReconnect(() => {
210
+ reconnect();
211
+ }));
212
+ const stopErrors = subscriber.onError?.((error) => deps.report(error, { where: "subscriber" }));
213
+ if (stopErrors !== void 0) unhook.push(stopErrors);
214
+ }
215
+ return subscriber;
216
+ }
217
+ async function keepAliveRound() {
218
+ const channels = [...topics.values()].filter((topic) => topic.kind === "message" && !topic.dead);
219
+ for (let from = 0; from < channels.length; from += RESUBSCRIBE_BATCH) await Promise.all(channels.slice(from, from + RESUBSCRIBE_BATCH).map(async (topic) => {
220
+ try {
221
+ await deps.keepAlive(topic.channel);
222
+ } catch (error) {
223
+ reportTo(error, topic, "keepAlive");
224
+ }
225
+ }));
226
+ }
227
+ function syncTimer() {
228
+ const needed = [...topics.values()].some((topic) => topic.kind === "message");
229
+ if (needed && timer === void 0 && !closed) {
230
+ timer = setInterval(() => void keepAliveRound(), deps.keepAliveMs);
231
+ timer.unref?.();
232
+ } else if (!needed && timer !== void 0) {
233
+ clearInterval(timer);
234
+ timer = void 0;
235
+ }
236
+ }
237
+ async function open(topic) {
238
+ topics.set(topic.topic, topic);
239
+ syncTimer();
240
+ topic.ready = (async () => {
241
+ await (await hook()).subscribe(topic.topic, topic.handler);
242
+ if (topic.kind === "presence") await seed(topic);
243
+ })();
244
+ topic.ready.catch(() => {
245
+ topic.dead = true;
246
+ if (topics.get(topic.topic) === topic) topics.delete(topic.topic);
247
+ syncTimer();
248
+ connected?.unsubscribe(topic.topic).catch(() => void 0);
249
+ });
250
+ await topic.ready;
251
+ }
252
+ async function seed(topic) {
253
+ topic.pending = [];
254
+ let list;
255
+ try {
256
+ list = await deps.members(topic.channel);
257
+ } catch (error) {
258
+ topic.pending = void 0;
259
+ throw error;
260
+ }
261
+ const held = topic.pending;
262
+ topic.pending = void 0;
263
+ const members = new Map(list.map((member) => [member.id, member]));
264
+ for (const event of held) apply(members, event);
265
+ topic.members = members;
266
+ if (members.size > MAX_TRACKED_MEMBERS) topic.tracked = false;
267
+ }
268
+ /**
269
+ * Returns the function that removes one listener. The last listener out drops the topic and sends UNSUBSCRIBE at once (not
270
+ * after an await), so a subscribe of the same topic issued next is sent after it and wins.
271
+ */
272
+ function release(topic, remove) {
273
+ let done = false;
274
+ return async () => {
275
+ if (done) return;
276
+ done = true;
277
+ remove();
278
+ if (topic.listeners.size > 0 || topic.dead) return;
279
+ topic.dead = true;
280
+ if (topics.get(topic.topic) === topic) topics.delete(topic.topic);
281
+ syncTimer();
282
+ if (closed || connected === void 0) return;
283
+ try {
284
+ await connected.unsubscribe(topic.topic);
285
+ } catch (error) {
286
+ reportTo(error, topic, "unsubscribe");
287
+ }
288
+ };
289
+ }
290
+ async function attach(topic, create, add) {
291
+ if (closed) throw deps.fail(/* @__PURE__ */ new Error("the adapter is closed."));
292
+ const found = topic ?? create();
293
+ const stop = add(found);
294
+ try {
295
+ if (topic === void 0) await open(found);
296
+ else await found.ready;
297
+ } catch (error) {
298
+ stop();
299
+ throw deps.fail(error);
300
+ }
301
+ if (closed || found.dead) {
302
+ stop();
303
+ throw deps.fail(/* @__PURE__ */ new Error("the adapter is closed."));
304
+ }
305
+ return stop;
306
+ }
307
+ return {
308
+ messages(channel, listener) {
309
+ const key = deps.messageTopic(channel);
310
+ const existing = topics.get(key);
311
+ return attach(existing?.kind === "message" ? existing : void 0, () => {
312
+ const topic = {
313
+ kind: "message",
314
+ channel,
315
+ topic: key,
316
+ listeners: /* @__PURE__ */ new Set(),
317
+ last: 0,
318
+ catching: false,
319
+ rerun: false,
320
+ buffer: [],
321
+ overflow: false,
322
+ dead: false,
323
+ ready: Promise.resolve(),
324
+ handler: (text) => onMessage(topic, text)
325
+ };
326
+ return topic;
327
+ }, (topic) => {
328
+ topic.listeners.add(listener);
329
+ return release(topic, () => topic.listeners.delete(listener));
330
+ });
331
+ },
332
+ presence(channel, listener) {
333
+ const key = deps.presenceTopic(channel);
334
+ const existing = topics.get(key);
335
+ return attach(existing?.kind === "presence" ? existing : void 0, () => {
336
+ const topic = {
337
+ kind: "presence",
338
+ channel,
339
+ topic: key,
340
+ listeners: /* @__PURE__ */ new Set(),
341
+ members: /* @__PURE__ */ new Map(),
342
+ tracked: true,
343
+ pending: void 0,
344
+ dead: false,
345
+ ready: Promise.resolve(),
346
+ handler: (text) => onPresence(topic, text)
347
+ };
348
+ return topic;
349
+ }, (topic) => {
350
+ topic.listeners.add(listener);
351
+ return release(topic, () => topic.listeners.delete(listener));
352
+ });
353
+ },
354
+ async close() {
355
+ closed = true;
356
+ for (const stop of unhook.splice(0)) stop();
357
+ if (timer !== void 0) clearInterval(timer);
358
+ timer = void 0;
359
+ const open = [...topics.values()];
360
+ topics.clear();
361
+ for (const topic of open) topic.dead = true;
362
+ if (open.length === 0) return;
363
+ const subscriber = connected;
364
+ if (subscriber === void 0) return;
365
+ await Promise.all(open.map((topic) => subscriber.unsubscribe(topic.topic).catch((error) => {
366
+ reportTo(error, topic, "unsubscribe");
367
+ })));
368
+ },
369
+ get size() {
370
+ return topics.size;
371
+ }
372
+ };
373
+ }
374
+ //#endregion
375
+ export { createTopics };
376
+
377
+ //# sourceMappingURL=topics.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,68 @@
1
+ import { Adapter } from "./adapter.js";
2
+ import { IoredisLike, IoredisSubscriberLike, NodeRedisLike, NodeRedisSubscriberLike, RedisClient, RedisSubscriber, fromIoredis, fromNodeRedis } from "./redis/client.js";
3
+ import { RedisErrorContext } from "./redis/topics.js";
4
+ //#region src/redis.d.ts
5
+ /**
6
+ * Options of {@link redisAdapter}.
7
+ * @since 1.0.0
8
+ */
9
+ export interface RedisAdapterOptions {
10
+ /** The connection that runs commands (see {@link fromIoredis} and {@link fromNodeRedis}). The adapter never closes it. */
11
+ readonly client: RedisClient;
12
+ /**
13
+ * A connection in subscriber mode, shared by every subscription of this adapter. Without it the adapter asks the client for one
14
+ * (`client.createSubscriber`) when the first subscription is made, and closes that one with the adapter. A subscriber you pass
15
+ * is yours to close; give it to one adapter only (a topic has one listener on a subscriber).
16
+ */
17
+ readonly subscriber?: RedisSubscriber;
18
+ /**
19
+ * Prefix of every key and topic. Servers that share channels use the same one; apps (or environments) that must not share
20
+ * use different ones. No braces or whitespace. Default `"rt"`.
21
+ */
22
+ readonly prefix?: string;
23
+ /**
24
+ * Milliseconds a channel's numbering outlives its last publish (and its last listener, which keeps it alive). After an idle
25
+ * channel expired, its seq restarts at 1, which a client that resumes sees as `reset`. Default 24 hours.
26
+ */
27
+ readonly channelTtlMs?: number;
28
+ /**
29
+ * Milliseconds the presence keys of a channel outlive the last refresh of any member, so a channel nobody reaps anymore does
30
+ * not stay in Redis forever. Default 60 seconds.
31
+ */
32
+ readonly presenceGraceMs?: number;
33
+ /**
34
+ * Receives every failure the adapter handled without surfacing it: a malformed stored entry that was skipped, a listener that
35
+ * threw, a subscriber connection error, a recovery that gave up. Default: ignored.
36
+ */
37
+ readonly onError?: (error: unknown, context: RedisErrorContext) => void;
38
+ }
39
+ /**
40
+ * The adapter that makes several servers one: messages, history, idempotency and presence live in Redis (6.2 or later, or
41
+ * Valkey), and fan-out between servers goes through pub/sub. A hub created with it behaves exactly like one over the in-memory
42
+ * adapter, on as many servers as you run.
43
+ *
44
+ * What is stored, per channel `c` (`p` is the prefix; the braces make every key of one channel hash to one Redis Cluster slot):
45
+ * `p:{c}:m` (hash: head seq, history lifetime), `p:{c}:l` and `p:{c}:t` (sorted sets: the retained messages by seq, and when
46
+ * each was accepted), `p:{c}:k:<key>` (idempotency keys), `p:{c}:p` and `p:{c}:x` (presence members, and when each expires),
47
+ * and the topics `p:{c}:c` (messages) and `p:{c}:e` (presence events). A publish is one Lua script: it checks the key,
48
+ * numbers, stores, trims by count and age, and publishes, atomically, so seq order is delivery order. Every method rejects with
49
+ * `RealtimeError("unavailable")` when Redis cannot be reached, and leaves no half-applied state.
50
+ *
51
+ * @param options The connections and the key prefix.
52
+ * @returns The adapter. Close it with the hub (`createHub` closes an adapter it created; close one you passed yourself).
53
+ * @throws RangeError for an option that is not valid; TypeError when no subscriber is given and the client cannot create one.
54
+ * @example
55
+ * import { Redis } from "ioredis";
56
+ * import { createHub } from "@shadow-js/realtime/server";
57
+ * import { fromIoredis, redisAdapter } from "@shadow-js/realtime/redis";
58
+ *
59
+ * const adapter = redisAdapter({ client: fromIoredis(new Redis(process.env.REDIS_URL ?? "")), prefix: "myapp" });
60
+ * const hub = createHub({ adapter, channels: { "chat:{room}": { subscribe: true, publish: true } } });
61
+ * await hub.close();
62
+ * await adapter.close();
63
+ * @since 1.0.0
64
+ */
65
+ export declare function redisAdapter(options: RedisAdapterOptions): Adapter;
66
+ //#endregion
67
+ export { type IoredisLike, type IoredisSubscriberLike, type NodeRedisLike, type NodeRedisSubscriberLike, type RedisClient, type RedisErrorContext, type RedisSubscriber, fromIoredis, fromNodeRedis };
68
+ //# sourceMappingURL=redis.d.ts.map