providing password reset services for a long while: circa 2025

feat: use special slack rate limiting with 1 per sec per channel and 350 per workspace

+116 -74
+3
.gitignore
··· 173 173 174 174 # Finder (MacOS) folder config 175 175 .DS_Store 176 + 177 + # db 178 + data
data/slack-queue.db

This is a binary file and will not be displayed.

+110 -68
features/message-queue.ts
··· 2 2 import type { Block, SlackAPIClient } from "slack-edge"; 3 3 4 4 export interface SlackMessage { 5 - userId?: string; 6 - channelId?: string; 5 + channel: string; 7 6 blocks?: Block[]; 8 7 text: string; 9 8 timestamp?: number; ··· 15 14 private slack: SlackAPIClient; 16 15 private isProcessing = false; 17 16 private batchSize = 50; 17 + private rateLimitDelay = 1000; // 1 message per second per channel 18 + private channelLastMessageTime: Map<string, number> = new Map(); 19 + private totalMessageCount = 0; 20 + private messageCountResetTime = 0; 21 + private backoffDelay = 1000; 22 + private maxBackoff = 30000; // 30 seconds 18 23 19 24 constructor(slackClient: SlackAPIClient, dbPath = "slack-queue.db") { 20 25 this.slack = slackClient; 21 26 this.db = new Database(dbPath); 22 27 this.initDatabase(); 28 + this.processQueue(); 23 29 } 24 30 25 31 private initDatabase() { 26 32 this.db.run(` 27 33 CREATE TABLE IF NOT EXISTS messages ( 28 34 id INTEGER PRIMARY KEY AUTOINCREMENT, 29 - userId TEXT, 30 - channelId TEXT, 35 + channel TEXT NOT NULL, 31 36 blocks TEXT, 32 37 text TEXT NOT NULL, 33 38 timestamp INTEGER NOT NULL, ··· 39 44 40 45 async enqueue(message: SlackMessage): Promise<void> { 41 46 const stmt = this.db.prepare(` 42 - INSERT INTO messages (userId, channelId, blocks, text, timestamp, status) 43 - VALUES (?, ?, ?, ?, ?, ?) 47 + INSERT INTO messages (channel, blocks, text, timestamp, status) 48 + VALUES (?, ?, ?, ?, ?) 44 49 `); 45 50 46 51 stmt.run( 47 - message.userId ?? null, 48 - message.channelId ?? null, 52 + message.channel ?? null, 49 53 JSON.stringify(message.blocks) ?? null, 50 54 message.text, 51 55 Date.now(), ··· 57 61 } 58 62 } 59 63 64 + private async sleep(ms: number): Promise<void> { 65 + return new Promise((resolve) => setTimeout(resolve, ms)); 66 + } 67 + 68 + private async sendWithRateLimit( 69 + message: SlackMessage & { id: number }, 70 + ): Promise<void> { 71 + const now = Date.now(); 72 + 73 + // Check per-minute total limit 74 + if (now - this.messageCountResetTime >= 60000) { 75 + this.totalMessageCount = 0; 76 + this.messageCountResetTime = now; 77 + } 78 + 79 + if (this.totalMessageCount >= 350) { 80 + const waitTime = 60000 - (now - this.messageCountResetTime); 81 + await this.sleep(waitTime); 82 + this.totalMessageCount = 0; 83 + this.messageCountResetTime = Date.now(); 84 + } 85 + 86 + // Check per-channel rate limit 87 + const channelLastTime = 88 + this.channelLastMessageTime.get(message.channel) || 0; 89 + const timeSinceLastChannelMessage = now - channelLastTime; 90 + 91 + if (timeSinceLastChannelMessage < this.rateLimitDelay) { 92 + await this.sleep(this.rateLimitDelay - timeSinceLastChannelMessage); 93 + } 94 + 95 + let currentBackoff = this.backoffDelay; 96 + let attempts = 0; 97 + const maxAttempts = 3; 98 + 99 + while (attempts < maxAttempts) { 100 + try { 101 + await this.slack.chat.postMessage({ 102 + channel: message.channel, 103 + blocks: JSON.parse(message.blocks as unknown as string) ?? undefined, 104 + text: message.text, 105 + }); 106 + 107 + this.channelLastMessageTime.set(message.channel, Date.now()); 108 + this.totalMessageCount++; 109 + 110 + this.db 111 + .prepare( 112 + ` 113 + UPDATE messages 114 + SET status = 'sent' 115 + WHERE id = ? 116 + `, 117 + ) 118 + .run(message.id); 119 + 120 + return; 121 + } catch (error) { 122 + console.error( 123 + `Error sending message (attempt ${attempts + 1}/${maxAttempts})`, 124 + error, 125 + ); 126 + attempts++; 127 + 128 + if (attempts === maxAttempts) { 129 + this.db 130 + .prepare( 131 + ` 132 + UPDATE messages 133 + SET status = 'failed' 134 + WHERE id = ? 135 + `, 136 + ) 137 + .run(message.id); 138 + return; 139 + } 140 + 141 + await this.sleep(currentBackoff); 142 + currentBackoff = Math.min(currentBackoff * 2, this.maxBackoff); 143 + } 144 + } 145 + } 146 + 60 147 private async processQueue() { 61 148 if (this.isProcessing) return; 62 149 this.isProcessing = true; ··· 68 155 const messages = this.db 69 156 .prepare( 70 157 ` 71 - SELECT * FROM messages 72 - WHERE status = 'pending' 73 - LIMIT ? 74 - `, 158 + SELECT * FROM messages 159 + WHERE status = 'pending' 160 + LIMIT ? 161 + `, 75 162 ) 76 163 .all(this.batchSize) as (SlackMessage & { id: number })[]; 77 164 78 - console.log(messages); 79 165 if (messages.length === 0) break; 80 166 81 - await Promise.all( 82 - messages.map(async (message) => { 83 - try { 84 - if (message.channelId) { 85 - await this.slack.chat.postMessage({ 86 - channel: message.channelId, 87 - blocks: 88 - JSON.parse(message.blocks as unknown as string) ?? 89 - undefined, 90 - text: message.text, 91 - }); 92 - 93 - console.log(res); 94 - } else if (message.userId) { 95 - await this.slack.chat.postMessage({ 96 - channel: message.userId, 97 - blocks: 98 - JSON.parse(message.blocks as unknown as string) ?? 99 - undefined, 100 - text: message.text, 101 - }); 102 - } 103 - 104 - console.log("Message sent successfully"); 105 - 106 - this.db 107 - .prepare( 108 - ` 109 - UPDATE messages 110 - SET status = 'sent' 111 - WHERE id = ? 112 - `, 113 - ) 114 - .run(message.id); 115 - } catch (error) { 116 - console.error("Error sending message", error); 117 - this.db 118 - .prepare( 119 - ` 120 - UPDATE messages 121 - SET status = 'failed' 122 - WHERE id = ? 123 - `, 124 - ) 125 - .run(message.id); 126 - } 127 - }), 128 - ); 167 + // Process messages sequentially to maintain rate limiting 168 + for (const message of messages) { 169 + await this.sendWithRateLimit(message); 170 + } 129 171 } 130 172 } finally { 131 173 this.isProcessing = false; ··· 137 179 this.db 138 180 .prepare( 139 181 ` 140 - DELETE FROM messages 141 - WHERE timestamp < ? AND status != 'pending' 142 - `, 182 + DELETE FROM messages 183 + WHERE timestamp < ? AND status != 'pending' 184 + `, 143 185 ) 144 186 .run(cutoff); 145 187 } ··· 148 190 const result = this.db 149 191 .prepare( 150 192 ` 151 - SELECT COUNT(*) as count 152 - FROM messages 153 - WHERE status = 'pending' 154 - `, 193 + SELECT COUNT(*) as count 194 + FROM messages 195 + WHERE status = 'pending' 196 + `, 155 197 ) 156 198 .get(); 157 199 return (result as { count: number }).count;
+3 -6
index.ts
··· 67 67 } 68 68 69 69 const message: SlackMessage = await request.json(); 70 - const { userId, channelId, text } = message; 70 + const { channel, text } = message; 71 71 72 - if ((!userId && !channelId) || (userId && channelId) || !text) { 72 + if (!channel || !text) { 73 73 return new Response( 74 74 `Invalid fields: ${[ 75 - !userId && 76 - !channelId && 77 - "must provide either userId or channelId", 78 - userId && channelId && "cannot provide both userId and channelId", 75 + !channel && "channel is required", 79 76 !text && "text is required", 80 77 ] 81 78 .filter(Boolean)