@snakechat/commskit-node 0.0.0-stage → 0.1.1

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/adapters-8ztjEYtz.d.ts +83 -0
  4. package/dist/call.d.ts +34 -0
  5. package/dist/call.js +51 -0
  6. package/dist/call.js.map +1 -0
  7. package/dist/chat.d.ts +64 -0
  8. package/dist/chat.js +30 -0
  9. package/dist/chat.js.map +1 -0
  10. package/dist/chunk-5BJ2SNM5.js +26 -0
  11. package/dist/chunk-5BJ2SNM5.js.map +1 -0
  12. package/dist/chunk-6I27CI32.js +586 -0
  13. package/dist/chunk-6I27CI32.js.map +1 -0
  14. package/dist/chunk-BBFVG7QU.js +52 -0
  15. package/dist/chunk-BBFVG7QU.js.map +1 -0
  16. package/dist/chunk-GR4PMGCS.js +82 -0
  17. package/dist/chunk-GR4PMGCS.js.map +1 -0
  18. package/dist/chunk-KL4WRZKZ.js +799 -0
  19. package/dist/chunk-KL4WRZKZ.js.map +1 -0
  20. package/dist/chunk-KLKRK6OG.js +107 -0
  21. package/dist/chunk-KLKRK6OG.js.map +1 -0
  22. package/dist/chunk-LY2GJWN7.js +708 -0
  23. package/dist/chunk-LY2GJWN7.js.map +1 -0
  24. package/dist/chunk-SEDRF62Q.js +39 -0
  25. package/dist/chunk-SEDRF62Q.js.map +1 -0
  26. package/dist/chunk-U4Z5INIW.js +1192 -0
  27. package/dist/chunk-U4Z5INIW.js.map +1 -0
  28. package/dist/chunk-XOXZZYSW.js +247 -0
  29. package/dist/chunk-XOXZZYSW.js.map +1 -0
  30. package/dist/chunk-XQ6FLRX7.js +179 -0
  31. package/dist/chunk-XQ6FLRX7.js.map +1 -0
  32. package/dist/chunk-XQFQN73U.js +38 -0
  33. package/dist/chunk-XQFQN73U.js.map +1 -0
  34. package/dist/connections-CGEPiZPF.d.ts +128 -0
  35. package/dist/core.d.ts +26 -0
  36. package/dist/core.js +89 -0
  37. package/dist/core.js.map +1 -0
  38. package/dist/devices-oLIB_M19.d.ts +97 -0
  39. package/dist/express.d.ts +19 -0
  40. package/dist/express.js +34 -0
  41. package/dist/express.js.map +1 -0
  42. package/dist/fastify.d.ts +22 -0
  43. package/dist/fastify.js +42 -0
  44. package/dist/fastify.js.map +1 -0
  45. package/dist/feature-B2bEXLZG.d.ts +233 -0
  46. package/dist/feature-DQUbnYE-.d.ts +284 -0
  47. package/dist/feature-DwKkuOBh.d.ts +195 -0
  48. package/dist/hub-CPrWmIvB.d.ts +174 -0
  49. package/dist/index.d.ts +33 -0
  50. package/dist/index.js +138 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/integration.d.ts +97 -0
  53. package/dist/integration.js +151 -0
  54. package/dist/integration.js.map +1 -0
  55. package/dist/models-D1lehXIu.d.ts +31 -0
  56. package/dist/models-DTXEX1Q8.d.ts +34 -0
  57. package/dist/moderation.d.ts +9 -0
  58. package/dist/moderation.js +48 -0
  59. package/dist/moderation.js.map +1 -0
  60. package/dist/push.d.ts +198 -0
  61. package/dist/push.js +475 -0
  62. package/dist/push.js.map +1 -0
  63. package/dist/redis.d.ts +103 -0
  64. package/dist/redis.js +296 -0
  65. package/dist/redis.js.map +1 -0
  66. package/dist/s3.d.ts +75 -0
  67. package/dist/s3.js +315 -0
  68. package/dist/s3.js.map +1 -0
  69. package/dist/util-B32rIBVA.d.ts +22 -0
  70. package/package.json +146 -4
package/dist/redis.js ADDED
@@ -0,0 +1,296 @@
1
+ import {
2
+ RateLimiter
3
+ } from "./chunk-BBFVG7QU.js";
4
+ import "./chunk-KLKRK6OG.js";
5
+ import {
6
+ logger
7
+ } from "./chunk-5BJ2SNM5.js";
8
+
9
+ // src/core/redis.ts
10
+ var USER_CHANNEL_PREFIX = "user_channel:";
11
+ var USER_CHANNEL_PATTERN = "user_channel:*";
12
+ var PRESENCE_KEY_PREFIX = "presence:";
13
+ var AWAY_KEY_PREFIX = "presence_away:";
14
+ var ROOM_CHANNEL_PREFIX = "room_channel:";
15
+ var ROOM_CHANNEL_PATTERN = "room_channel:*";
16
+ var CAME_ONLINE_KEY_PREFIX = "came_online:";
17
+ async function loadIoRedis() {
18
+ const name = "ioredis";
19
+ try {
20
+ const mod = await import(
21
+ /* @vite-ignore */
22
+ name
23
+ );
24
+ return mod.default ?? mod.Redis ?? mod;
25
+ } catch {
26
+ throw new Error("RedisBridge needs ioredis: npm install ioredis");
27
+ }
28
+ }
29
+ var RedisBridge = class {
30
+ client;
31
+ heartbeatSeconds;
32
+ staleSeconds;
33
+ presenceTtlSeconds;
34
+ awayTtlSeconds;
35
+ url;
36
+ sub = null;
37
+ deliver = null;
38
+ roomDeliver = null;
39
+ constructor(opts = {}) {
40
+ if (!opts.url && !opts.client) throw new Error("RedisBridge needs a url or a client");
41
+ this.url = opts.url;
42
+ this.client = opts.client ?? null;
43
+ this.heartbeatSeconds = opts.heartbeatSeconds ?? 30;
44
+ this.staleSeconds = opts.staleSeconds ?? 90;
45
+ this.presenceTtlSeconds = opts.presenceTtlSeconds ?? 120;
46
+ this.awayTtlSeconds = opts.awayTtlSeconds ?? 12 * 3600;
47
+ }
48
+ setRoomDeliver(fn) {
49
+ this.roomDeliver = fn;
50
+ }
51
+ async start(deliver, _serverId) {
52
+ this.deliver = deliver;
53
+ let ready = false;
54
+ try {
55
+ if (!this.client) {
56
+ const Redis = await loadIoRedis();
57
+ const c = new Redis(this.url, { lazyConnect: true, maxRetriesPerRequest: 1, connectTimeout: 5e3, retryStrategy: (n) => ready ? Math.min(n * 200, 2e3) : null });
58
+ c.on("error", () => void 0);
59
+ this.client = c;
60
+ await c.connect();
61
+ }
62
+ await this.client.ping();
63
+ ready = true;
64
+ const sub = this.client.duplicate();
65
+ sub.on("error", () => void 0);
66
+ if (sub.connect) {
67
+ try {
68
+ await sub.connect();
69
+ } catch (err) {
70
+ if (!/already/i.test(String(err?.message))) throw err;
71
+ }
72
+ }
73
+ sub.on("pmessage", (_pattern, channel, message) => {
74
+ void this.consume(channel, message);
75
+ });
76
+ await sub.psubscribe(USER_CHANNEL_PATTERN, ROOM_CHANNEL_PATTERN);
77
+ this.sub = sub;
78
+ } catch (err) {
79
+ logger.warn(`Redis unavailable (${err?.message ?? err}); running single-worker`);
80
+ const c = this.client;
81
+ this.client = null;
82
+ this.sub = null;
83
+ try {
84
+ c?.disconnect?.();
85
+ } catch {
86
+ }
87
+ }
88
+ }
89
+ async stop() {
90
+ for (const obj of [this.sub, this.client]) {
91
+ if (!obj) continue;
92
+ try {
93
+ if (obj.quit) await obj.quit();
94
+ else obj.disconnect?.();
95
+ } catch {
96
+ try {
97
+ obj.disconnect?.();
98
+ } catch {
99
+ }
100
+ }
101
+ }
102
+ this.sub = null;
103
+ this.client = null;
104
+ }
105
+ async consume(channel, raw) {
106
+ if (typeof channel !== "string" || !raw) return;
107
+ if (channel.startsWith(ROOM_CHANNEL_PREFIX)) {
108
+ const room = channel.slice(ROOM_CHANNEL_PREFIX.length);
109
+ if (!this.roomDeliver) return;
110
+ try {
111
+ await this.roomDeliver(room, JSON.parse(raw));
112
+ } catch (err) {
113
+ logger.warn(`Cross-worker room delivery to ${room} failed: ${err?.message ?? err}`);
114
+ }
115
+ return;
116
+ }
117
+ if (!this.deliver) return;
118
+ const userId = channel.slice(channel.lastIndexOf(":") + 1);
119
+ try {
120
+ await this.deliver(userId, JSON.parse(raw));
121
+ } catch (err) {
122
+ logger.warn(`Cross-worker delivery to ${userId} failed: ${err?.message ?? err}`);
123
+ }
124
+ }
125
+ // ------------------------------------------------------------ presence
126
+ async setOnline(userId, serverId) {
127
+ const c = this.client;
128
+ if (!c) return;
129
+ const key = PRESENCE_KEY_PREFIX + userId;
130
+ try {
131
+ await c.hset(key, serverId, Math.floor(Date.now() / 1e3));
132
+ await c.expire(key, this.presenceTtlSeconds);
133
+ } catch (err) {
134
+ logger.warn(`setOnline(${userId}) failed: ${err?.message ?? err}`);
135
+ }
136
+ }
137
+ async setOffline(userId, serverId) {
138
+ const c = this.client;
139
+ if (!c) return;
140
+ try {
141
+ await c.hdel(PRESENCE_KEY_PREFIX + userId, serverId);
142
+ } catch (err) {
143
+ logger.warn(`setOffline(${userId}) failed: ${err?.message ?? err}`);
144
+ }
145
+ }
146
+ async setAway(userId, away) {
147
+ const c = this.client;
148
+ if (!c) return;
149
+ const key = AWAY_KEY_PREFIX + userId;
150
+ try {
151
+ if (away) await c.set(key, 1, "EX", this.awayTtlSeconds);
152
+ else await c.del(key);
153
+ } catch (err) {
154
+ logger.warn(`setAway(${userId}) failed: ${err?.message ?? err}`);
155
+ }
156
+ }
157
+ async getUserServers(userId) {
158
+ const c = this.client;
159
+ if (!c) return [];
160
+ let entries;
161
+ try {
162
+ entries = await c.hgetall(PRESENCE_KEY_PREFIX + userId);
163
+ } catch (err) {
164
+ logger.warn(`getUserServers(${userId}) failed: ${err?.message ?? err}`);
165
+ return [];
166
+ }
167
+ const cutoff = Date.now() / 1e3 - this.staleSeconds;
168
+ return Object.entries(entries ?? {}).filter(([, beat]) => Number(beat) >= cutoff).map(([server]) => server);
169
+ }
170
+ async isOnline(userId) {
171
+ if (!(await this.getUserServers(userId)).length) return false;
172
+ try {
173
+ return !await this.client.exists(AWAY_KEY_PREFIX + userId);
174
+ } catch {
175
+ return true;
176
+ }
177
+ }
178
+ async getOnlineUsers(userIds) {
179
+ const c = this.client;
180
+ if (!c || !userIds.length) return /* @__PURE__ */ new Set();
181
+ let results;
182
+ try {
183
+ results = await Promise.all(
184
+ userIds.map(async (uid) => [await c.hgetall(PRESENCE_KEY_PREFIX + uid), await c.exists(AWAY_KEY_PREFIX + uid)])
185
+ );
186
+ } catch (err) {
187
+ logger.warn(`getOnlineUsers failed: ${err?.message ?? err}`);
188
+ return /* @__PURE__ */ new Set();
189
+ }
190
+ const cutoff = Date.now() / 1e3 - this.staleSeconds;
191
+ const online = /* @__PURE__ */ new Set();
192
+ userIds.forEach((uid, i) => {
193
+ const [servers, away] = results[i];
194
+ if (!away && Object.values(servers ?? {}).some((b) => Number(b) >= cutoff)) online.add(uid);
195
+ });
196
+ return online;
197
+ }
198
+ async publish(userId, payload, origin) {
199
+ const c = this.client;
200
+ if (!c) return;
201
+ try {
202
+ await c.publish(USER_CHANNEL_PREFIX + userId, JSON.stringify({ origin, data: payload }));
203
+ } catch (err) {
204
+ logger.warn(`publish(${userId}) failed: ${err?.message ?? err}`);
205
+ }
206
+ }
207
+ async publishRoom(room, payload, origin) {
208
+ const c = this.client;
209
+ if (!c) return;
210
+ try {
211
+ await c.publish(ROOM_CHANNEL_PREFIX + room, JSON.stringify({ origin, data: payload }));
212
+ } catch (err) {
213
+ logger.warn(`publishRoom(${room}) failed: ${err?.message ?? err}`);
214
+ }
215
+ }
216
+ // ------------------------------------------------------- user.came_online
217
+ /** True for exactly one caller until the key expires or is released (`SET NX PX`). Without Redis every caller wins. */
218
+ async claimCameOnline(userId, ttlSeconds) {
219
+ const c = this.client;
220
+ if (!c) return true;
221
+ try {
222
+ return Boolean(await c.set(CAME_ONLINE_KEY_PREFIX + userId, 1, "PX", Math.max(1, Math.floor(ttlSeconds * 1e3)), "NX"));
223
+ } catch (err) {
224
+ logger.warn(`claimCameOnline(${userId}) failed: ${err?.message ?? err}`);
225
+ return true;
226
+ }
227
+ }
228
+ async releaseCameOnline(userId) {
229
+ const c = this.client;
230
+ if (!c) return;
231
+ try {
232
+ await c.del(CAME_ONLINE_KEY_PREFIX + userId);
233
+ } catch (err) {
234
+ logger.warn(`releaseCameOnline(${userId}) failed: ${err?.message ?? err}`);
235
+ }
236
+ }
237
+ };
238
+ var TOKEN_BUCKET_LUA = `
239
+ local cap = tonumber(ARGV[1])
240
+ local rate = tonumber(ARGV[2])
241
+ local now = tonumber(ARGV[3])
242
+ local cost = tonumber(ARGV[4])
243
+ local v = redis.call('HMGET', KEYS[1], 't', 'ts')
244
+ local tokens = tonumber(v[1])
245
+ local ts = tonumber(v[2])
246
+ if tokens == nil or ts == nil then tokens = cap; ts = now end
247
+ if now > ts then tokens = math.min(cap, tokens + (now - ts) * rate) end
248
+ local wait = 0
249
+ if tokens >= cost then tokens = tokens - cost else wait = (cost - tokens) / rate end
250
+ redis.call('HSET', KEYS[1], 't', tostring(tokens), 'ts', tostring(math.max(now, ts)))
251
+ redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[5]))
252
+ return tostring(wait)
253
+ `;
254
+ var RedisRateLimiter = class {
255
+ constructor(source, prefix = "ratelimit:", fallback) {
256
+ this.source = source;
257
+ this.prefix = prefix;
258
+ this.fallback = fallback ?? new RateLimiter();
259
+ }
260
+ source;
261
+ prefix;
262
+ fallback;
263
+ client() {
264
+ const s = this.source;
265
+ if (typeof s.eval === "function") return s;
266
+ return s.client ?? null;
267
+ }
268
+ async hit(key, limit, cost = 1) {
269
+ if (!limit) return 0;
270
+ const client = this.client();
271
+ if (!client) return this.fallback.hit(key, limit, cost);
272
+ const ttlMs = Math.floor(limit.seconds * 1e3 * 2) + 1e3;
273
+ try {
274
+ const raw = await client.eval(TOKEN_BUCKET_LUA, 1, this.prefix + key, limit.count, String(limit.rate), String(Date.now() / 1e3), String(cost), ttlMs);
275
+ const wait = Number(Buffer.isBuffer(raw) ? raw.toString() : raw);
276
+ if (Number.isNaN(wait)) throw new Error(`unexpected reply ${String(raw)}`);
277
+ return Math.max(0, wait);
278
+ } catch (err) {
279
+ logger.warn(`Redis rate limit failed (${err?.message ?? err}); using the in-memory limiter`);
280
+ return this.fallback.hit(key, limit, cost);
281
+ }
282
+ }
283
+ };
284
+ export {
285
+ AWAY_KEY_PREFIX,
286
+ CAME_ONLINE_KEY_PREFIX,
287
+ PRESENCE_KEY_PREFIX,
288
+ ROOM_CHANNEL_PATTERN,
289
+ ROOM_CHANNEL_PREFIX,
290
+ RedisBridge,
291
+ RedisRateLimiter,
292
+ TOKEN_BUCKET_LUA,
293
+ USER_CHANNEL_PATTERN,
294
+ USER_CHANNEL_PREFIX
295
+ };
296
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/redis.ts"],"sourcesContent":["/**\n * Optional Redis presence + cross-worker delivery (port of commskit.core.redis_bridge) and the shared\n * token-bucket limiter (port of commskit.core.ratelimit.RedisRateLimiter). Needs `ioredis` (optional peer\n * dependency, imported lazily) unless a client is injected.\n *\n * Key names match the Python kit and matrimonial-backend-v2, so Python and Node workers interoperate:\n * `presence:{uid}` (hash serverId -> heartbeat epoch seconds), `presence_away:{uid}`, pub/sub `user_channel:{uid}`\n * carrying `{\"origin\",\"data\"}`, `room_channel:{room}` (same envelope) and `came_online:{uid}` (SET NX PX).\n *\n * Every Redis failure is logged and degrades to \"not connected\" (local-only); it never throws.\n */\nimport type { Deliver, PresenceBridge } from \"./connections.js\";\nimport { logger } from \"./logger.js\";\nimport { type Limiter, RateLimit, RateLimiter } from \"./ratelimit.js\";\n\nexport const USER_CHANNEL_PREFIX = \"user_channel:\";\nexport const USER_CHANNEL_PATTERN = \"user_channel:*\";\nexport const PRESENCE_KEY_PREFIX = \"presence:\";\nexport const AWAY_KEY_PREFIX = \"presence_away:\";\nexport const ROOM_CHANNEL_PREFIX = \"room_channel:\";\nexport const ROOM_CHANNEL_PATTERN = \"room_channel:*\";\nexport const CAME_ONLINE_KEY_PREFIX = \"came_online:\";\n\n/** The slice of an `ioredis` client the bridge uses. */\nexport interface RedisClientLike {\n ping(): Promise<unknown>;\n hset(key: string, field: string, value: string | number): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n hdel(key: string, field: string): Promise<unknown>;\n hgetall(key: string): Promise<Record<string, string>>;\n set(key: string, value: string | number, ...args: Array<string | number>): Promise<unknown>;\n del(key: string): Promise<unknown>;\n exists(key: string): Promise<number>;\n publish(channel: string, message: string): Promise<unknown>;\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n /** A second connection for the subscriber. */\n duplicate(): RedisSubscriberLike;\n connect?(): Promise<unknown>;\n quit?(): Promise<unknown>;\n disconnect?(): void;\n on?(event: string, listener: (...args: any[]) => void): unknown;\n}\n\nexport interface RedisSubscriberLike {\n psubscribe(...patterns: string[]): Promise<unknown>;\n on(event: \"pmessage\", listener: (pattern: string, channel: string, message: string) => void): unknown;\n on(event: string, listener: (...args: any[]) => void): unknown;\n connect?(): Promise<unknown>;\n quit?(): Promise<unknown>;\n disconnect?(): void;\n}\n\nexport type RoomDeliver = (room: string, envelope: any) => Promise<void>;\n\nexport interface RedisBridgeOptions {\n url?: string;\n /** An already-created ioredis-compatible client (tests, shared connections). */\n client?: RedisClientLike;\n heartbeatSeconds?: number;\n staleSeconds?: number;\n presenceTtlSeconds?: number;\n awayTtlSeconds?: number;\n}\n\nasync function loadIoRedis(): Promise<any> {\n const name = \"ioredis\"; // variable specifier: no compile-time dependency\n try {\n const mod: any = await import(/* @vite-ignore */ name);\n return mod.default ?? mod.Redis ?? mod;\n } catch {\n throw new Error('RedisBridge needs ioredis: npm install ioredis');\n }\n}\n\nexport class RedisBridge implements PresenceBridge {\n client: RedisClientLike | null;\n heartbeatSeconds: number;\n staleSeconds: number;\n presenceTtlSeconds: number;\n awayTtlSeconds: number;\n private url?: string;\n private sub: RedisSubscriberLike | null = null;\n private deliver: Deliver | null = null;\n private roomDeliver: RoomDeliver | null = null;\n\n constructor(opts: RedisBridgeOptions = {}) {\n if (!opts.url && !opts.client) throw new Error(\"RedisBridge needs a url or a client\");\n this.url = opts.url;\n this.client = opts.client ?? null;\n this.heartbeatSeconds = opts.heartbeatSeconds ?? 30;\n this.staleSeconds = opts.staleSeconds ?? 90;\n this.presenceTtlSeconds = opts.presenceTtlSeconds ?? 120;\n this.awayTtlSeconds = opts.awayTtlSeconds ?? 12 * 3600;\n }\n\n setRoomDeliver(fn: RoomDeliver): void {\n this.roomDeliver = fn;\n }\n\n async start(deliver: Deliver, _serverId: string): Promise<void> {\n this.deliver = deliver;\n let ready = false; // fail fast on the first connect; reconnect with backoff once it worked\n try {\n if (!this.client) {\n const Redis = await loadIoRedis();\n const c = new Redis(this.url, { lazyConnect: true, maxRetriesPerRequest: 1, connectTimeout: 5000, retryStrategy: (n: number) => (ready ? Math.min(n * 200, 2000) : null) });\n c.on(\"error\", () => undefined);\n this.client = c as RedisClientLike;\n await c.connect();\n }\n await this.client.ping();\n ready = true;\n const sub = this.client.duplicate();\n sub.on(\"error\", () => undefined);\n if (sub.connect) {\n try {\n await sub.connect();\n } catch (err) {\n if (!/already/i.test(String((err as Error)?.message))) throw err;\n }\n }\n sub.on(\"pmessage\", (_pattern: string, channel: string, message: string) => {\n void this.consume(channel, message);\n });\n await sub.psubscribe(USER_CHANNEL_PATTERN, ROOM_CHANNEL_PATTERN);\n this.sub = sub;\n } catch (err) {\n logger.warn(`Redis unavailable (${(err as Error)?.message ?? err}); running single-worker`);\n const c = this.client;\n this.client = null;\n this.sub = null;\n try {\n c?.disconnect?.();\n } catch {\n /* ignore */\n }\n }\n }\n\n async stop(): Promise<void> {\n for (const obj of [this.sub, this.client]) {\n if (!obj) continue;\n try {\n if (obj.quit) await obj.quit();\n else obj.disconnect?.();\n } catch {\n try {\n obj.disconnect?.();\n } catch {\n /* ignore */\n }\n }\n }\n this.sub = null;\n this.client = null;\n }\n\n private async consume(channel: string, raw: string): Promise<void> {\n if (typeof channel !== \"string\" || !raw) return;\n if (channel.startsWith(ROOM_CHANNEL_PREFIX)) {\n const room = channel.slice(ROOM_CHANNEL_PREFIX.length);\n if (!this.roomDeliver) return;\n try {\n await this.roomDeliver(room, JSON.parse(raw));\n } catch (err) {\n logger.warn(`Cross-worker room delivery to ${room} failed: ${(err as Error)?.message ?? err}`);\n }\n return;\n }\n if (!this.deliver) return;\n const userId = channel.slice(channel.lastIndexOf(\":\") + 1);\n try {\n await this.deliver(userId, JSON.parse(raw));\n } catch (err) {\n logger.warn(`Cross-worker delivery to ${userId} failed: ${(err as Error)?.message ?? err}`);\n }\n }\n\n // ------------------------------------------------------------ presence\n\n async setOnline(userId: string, serverId: string): Promise<void> {\n const c = this.client;\n if (!c) return;\n const key = PRESENCE_KEY_PREFIX + userId;\n try {\n await c.hset(key, serverId, Math.floor(Date.now() / 1000));\n await c.expire(key, this.presenceTtlSeconds);\n } catch (err) {\n logger.warn(`setOnline(${userId}) failed: ${(err as Error)?.message ?? err}`);\n }\n }\n\n async setOffline(userId: string, serverId: string): Promise<void> {\n const c = this.client;\n if (!c) return;\n try {\n await c.hdel(PRESENCE_KEY_PREFIX + userId, serverId);\n } catch (err) {\n logger.warn(`setOffline(${userId}) failed: ${(err as Error)?.message ?? err}`);\n }\n }\n\n async setAway(userId: string, away: boolean): Promise<void> {\n const c = this.client;\n if (!c) return;\n const key = AWAY_KEY_PREFIX + userId;\n try {\n if (away) await c.set(key, 1, \"EX\", this.awayTtlSeconds);\n else await c.del(key);\n } catch (err) {\n logger.warn(`setAway(${userId}) failed: ${(err as Error)?.message ?? err}`);\n }\n }\n\n async getUserServers(userId: string): Promise<string[]> {\n const c = this.client;\n if (!c) return [];\n let entries: Record<string, string>;\n try {\n entries = await c.hgetall(PRESENCE_KEY_PREFIX + userId);\n } catch (err) {\n logger.warn(`getUserServers(${userId}) failed: ${(err as Error)?.message ?? err}`);\n return [];\n }\n const cutoff = Date.now() / 1000 - this.staleSeconds;\n return Object.entries(entries ?? {}).filter(([, beat]) => Number(beat) >= cutoff).map(([server]) => server);\n }\n\n async isOnline(userId: string): Promise<boolean> {\n if (!(await this.getUserServers(userId)).length) return false;\n try {\n return !(await this.client!.exists(AWAY_KEY_PREFIX + userId));\n } catch {\n return true;\n }\n }\n\n async getOnlineUsers(userIds: string[]): Promise<Set<string>> {\n const c = this.client;\n if (!c || !userIds.length) return new Set();\n let results: Array<[Record<string, string>, number]>;\n try {\n results = await Promise.all(\n userIds.map(async (uid) => [await c.hgetall(PRESENCE_KEY_PREFIX + uid), await c.exists(AWAY_KEY_PREFIX + uid)] as [Record<string, string>, number]),\n );\n } catch (err) {\n logger.warn(`getOnlineUsers failed: ${(err as Error)?.message ?? err}`);\n return new Set();\n }\n const cutoff = Date.now() / 1000 - this.staleSeconds;\n const online = new Set<string>();\n userIds.forEach((uid, i) => {\n const [servers, away] = results[i]!;\n if (!away && Object.values(servers ?? {}).some((b) => Number(b) >= cutoff)) online.add(uid);\n });\n return online;\n }\n\n async publish(userId: string, payload: unknown, origin: string): Promise<void> {\n const c = this.client;\n if (!c) return;\n try {\n await c.publish(USER_CHANNEL_PREFIX + userId, JSON.stringify({ origin, data: payload }));\n } catch (err) {\n logger.warn(`publish(${userId}) failed: ${(err as Error)?.message ?? err}`);\n }\n }\n\n async publishRoom(room: string, payload: unknown, origin: string): Promise<void> {\n const c = this.client;\n if (!c) return;\n try {\n await c.publish(ROOM_CHANNEL_PREFIX + room, JSON.stringify({ origin, data: payload }));\n } catch (err) {\n logger.warn(`publishRoom(${room}) failed: ${(err as Error)?.message ?? err}`);\n }\n }\n\n // ------------------------------------------------------- user.came_online\n\n /** True for exactly one caller until the key expires or is released (`SET NX PX`). Without Redis every caller wins. */\n async claimCameOnline(userId: string, ttlSeconds: number): Promise<boolean> {\n const c = this.client;\n if (!c) return true;\n try {\n return Boolean(await c.set(CAME_ONLINE_KEY_PREFIX + userId, 1, \"PX\", Math.max(1, Math.floor(ttlSeconds * 1000)), \"NX\"));\n } catch (err) {\n logger.warn(`claimCameOnline(${userId}) failed: ${(err as Error)?.message ?? err}`);\n return true;\n }\n }\n\n async releaseCameOnline(userId: string): Promise<void> {\n const c = this.client;\n if (!c) return;\n try {\n await c.del(CAME_ONLINE_KEY_PREFIX + userId);\n } catch (err) {\n logger.warn(`releaseCameOnline(${userId}) failed: ${(err as Error)?.message ?? err}`);\n }\n }\n}\n\n// ------------------------------------------------------------ rate limiter\n\n// KEYS[1] bucket; ARGV: capacity, rate (tokens/s), now (s), cost, ttl (ms). Returns the wait as a string.\n// Verbatim from commskit.core.ratelimit.\nexport const TOKEN_BUCKET_LUA = `\nlocal cap = tonumber(ARGV[1])\nlocal rate = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal cost = tonumber(ARGV[4])\nlocal v = redis.call('HMGET', KEYS[1], 't', 'ts')\nlocal tokens = tonumber(v[1])\nlocal ts = tonumber(v[2])\nif tokens == nil or ts == nil then tokens = cap; ts = now end\nif now > ts then tokens = math.min(cap, tokens + (now - ts) * rate) end\nlocal wait = 0\nif tokens >= cost then tokens = tokens - cost else wait = (cost - tokens) / rate end\nredis.call('HSET', KEYS[1], 't', tostring(tokens), 'ts', tostring(math.max(now, ts)))\nredis.call('PEXPIRE', KEYS[1], tonumber(ARGV[5]))\nreturn tostring(wait)\n`;\n\n/**\n * Token buckets in Redis, shared by all workers. `source` is a `RedisBridge` (its `client` is read on every call, so the\n * limiter can be built before `hub.connections.start()`) or an ioredis-compatible client. Any Redis error, or no\n * client yet, falls back to `fallback` (in-memory by default).\n */\nexport class RedisRateLimiter implements Limiter {\n fallback: Limiter;\n constructor(\n private source: { client?: RedisClientLike | null } | RedisClientLike,\n public prefix = \"ratelimit:\",\n fallback?: Limiter,\n ) {\n this.fallback = fallback ?? new RateLimiter();\n }\n\n private client(): RedisClientLike | null {\n const s: any = this.source;\n if (typeof s.eval === \"function\") return s;\n return s.client ?? null;\n }\n\n async hit(key: string, limit: RateLimit | null | undefined, cost = 1): Promise<number> {\n if (!limit) return 0;\n const client = this.client();\n if (!client) return this.fallback.hit(key, limit, cost);\n const ttlMs = Math.floor(limit.seconds * 1000 * 2) + 1000;\n try {\n const raw = await client.eval(TOKEN_BUCKET_LUA, 1, this.prefix + key, limit.count, String(limit.rate), String(Date.now() / 1000), String(cost), ttlMs);\n const wait = Number(Buffer.isBuffer(raw) ? raw.toString() : raw);\n if (Number.isNaN(wait)) throw new Error(`unexpected reply ${String(raw)}`);\n return Math.max(0, wait);\n } catch (err) {\n logger.warn(`Redis rate limit failed (${(err as Error)?.message ?? err}); using the in-memory limiter`);\n return this.fallback.hit(key, limit, cost);\n }\n }\n}\n"],"mappings":"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","names":[]}
package/dist/s3.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { b as MediaStorage, M as MediaInfo } from './adapters-8ztjEYtz.js';
2
+
3
+ declare const PRESIGN_MAX_SECONDS: number;
4
+ /** The S3 operations the storage needs; the default implementation wraps @aws-sdk/client-s3 (inject a fake in tests). */
5
+ interface S3Operations {
6
+ putObject(params: {
7
+ Bucket: string;
8
+ Key: string;
9
+ Body: Buffer;
10
+ ContentType: string;
11
+ [k: string]: unknown;
12
+ }): Promise<unknown>;
13
+ deleteObject(params: {
14
+ Bucket: string;
15
+ Key: string;
16
+ }): Promise<unknown>;
17
+ presignGet(params: {
18
+ Bucket: string;
19
+ Key: string;
20
+ }, expiresIn: number): Promise<string>;
21
+ }
22
+ declare function thumbnailKey(key: string): string;
23
+ interface S3StorageOptions {
24
+ bucket: string;
25
+ region?: string;
26
+ /** An S3Operations implementation (default: built lazily from @aws-sdk/client-s3). */
27
+ client?: S3Operations;
28
+ prefix?: string;
29
+ publicBaseUrl?: string;
30
+ private?: boolean;
31
+ presignExpires?: number;
32
+ thumbnails?: boolean;
33
+ durations?: boolean;
34
+ /** Extra PutObject parameters (e.g. ServerSideEncryption, CacheControl). */
35
+ extraPutArgs?: Record<string, unknown>;
36
+ }
37
+ declare class S3Storage implements MediaStorage {
38
+ readonly bucket: string;
39
+ readonly region?: string;
40
+ readonly prefix: string;
41
+ readonly publicBaseUrl: string;
42
+ readonly private: boolean;
43
+ readonly presignExpires: number;
44
+ readonly thumbnails: boolean;
45
+ readonly durations: boolean;
46
+ readonly extraPutArgs: Record<string, unknown>;
47
+ private injected?;
48
+ private lazyOps;
49
+ constructor(opts: S3StorageOptions);
50
+ private ops;
51
+ makeKey(chatId: number, mediaType: string, fileName: string): string;
52
+ urlFor(key: string): Promise<string>;
53
+ /** Object key of a URL this storage produced (public or presigned); null otherwise. */
54
+ keyFor(fileUrl: string): string | null;
55
+ /** Fresh presigned GET URL for a stored file; null for URLs this storage did not produce. */
56
+ presign(fileUrl: string, expires?: number): Promise<string | null>;
57
+ private put;
58
+ upload(chatId: number, fileName: string, data: Buffer, mediaType: string): Promise<MediaInfo>;
59
+ /** Delete the object (and its thumbnail). Unknown URLs are ignored; S3 deletes are idempotent. */
60
+ delete(fileUrl: string): Promise<void>;
61
+ }
62
+
63
+ declare const THUMBNAIL_SIZE: readonly [number, number];
64
+ declare const DURATION_MEDIA_TYPES: ReadonlySet<string>;
65
+ declare const VIDEO_MEDIA_TYPES: ReadonlySet<string>;
66
+ /** `<NAME>_PATH` env var if it points at a file, else the first match on PATH; null when not found. */
67
+ declare function findTool(name: string, env?: NodeJS.ProcessEnv): string | null;
68
+ /** JPEG thumbnail that fits in `size` (aspect kept, never enlarged); null without sharp or for a bad image. */
69
+ declare function makeImageThumbnail(data: Buffer, size?: readonly [number, number], quality?: number): Promise<Buffer | null>;
70
+ /** Duration in whole seconds via `ffprobe`; null when ffprobe is missing or fails. */
71
+ declare function probeDuration(data: Buffer, suffix?: string, timeoutSeconds?: number): Promise<number | null>;
72
+ /** JPEG of a frame near the start via `ffmpeg`; null when ffmpeg is missing or fails. */
73
+ declare function makeVideoThumbnail(data: Buffer, suffix?: string, width?: number, timeoutSeconds?: number): Promise<Buffer | null>;
74
+
75
+ export { DURATION_MEDIA_TYPES, PRESIGN_MAX_SECONDS, type S3Operations, S3Storage, type S3StorageOptions, THUMBNAIL_SIZE, VIDEO_MEDIA_TYPES, findTool, makeImageThumbnail, makeVideoThumbnail, probeDuration, thumbnailKey };