hacker-lobby 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/index.js CHANGED
@@ -89,6 +89,9 @@ function initChat() {
89
89
  addSystemMessage(`Stream disconnected: ${err.message}`);
90
90
  });
91
91
 
92
+ // Post join message to the server
93
+ sendMessage(getAlias(), 'joined the chat').catch(() => {});
94
+
92
95
  rl.on('line', (line) => {
93
96
  // Disable newline muting once readline has finished processing the line
94
97
  muteNewline = false;
@@ -119,6 +122,11 @@ function initChat() {
119
122
  }
120
123
  });
121
124
 
125
+ // Handle Ctrl+C gracefully
126
+ rl.on('SIGINT', () => {
127
+ cleanupAndExit();
128
+ });
129
+
122
130
  // Initial prompt display
123
131
  rl.prompt(true);
124
132
  }
@@ -143,7 +151,7 @@ function drawLayout() {
143
151
 
144
152
  // 2. Draw static divider line just above the input prompt
145
153
  moveCursor(rows - 1, 1);
146
- process.stdout.write(COLORS.GRAY + '─'.repeat(cols) + COLORS.RESET);
154
+ process.stdout.write(COLORS.GRAY + '-'.repeat(cols) + COLORS.RESET);
147
155
 
148
156
  // 3. Set the scrolling region for messages
149
157
  setScrollRegion(topMargin, bottomMargin);
@@ -207,10 +215,14 @@ function addSystemMessage(text) {
207
215
  moveCursor(rows, col);
208
216
  }
209
217
 
210
- function cleanupAndExit() {
218
+ async function cleanupAndExit() {
211
219
  if (abortController) {
212
220
  abortController.abort();
213
221
  }
222
+ try {
223
+ // Send leave notification to server before exiting
224
+ await sendMessage(getAlias(), 'left the chat');
225
+ } catch (_) {}
214
226
  resetScrollRegion();
215
227
  clearScreen();
216
228
  console.log(formatSystem('Goodbye!'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hacker-lobby",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Multiplayer terminal chat application",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -17,4 +17,4 @@
17
17
  "engines": {
18
18
  "node": ">=16.0.0"
19
19
  }
20
- }
20
+ }
package/src/ui.js CHANGED
@@ -54,6 +54,12 @@ ${COLORS.CYAN} ██╗ ██╗ █████╗ ██████╗█
54
54
  * @returns {string}
55
55
  */
56
56
  export function formatMessage(sender, text) {
57
+ if (text === 'joined the chat') {
58
+ return `${COLORS.GREEN}${COLORS.BOLD}[+]${COLORS.RESET} ${COLORS.CYAN}${COLORS.BOLD}${sender}${COLORS.RESET} ${COLORS.NEON_GREEN}joined the chat${COLORS.RESET}`;
59
+ }
60
+ if (text === 'left the chat') {
61
+ return `${COLORS.RED}${COLORS.BOLD}[-]${COLORS.RESET} ${COLORS.CYAN}${COLORS.BOLD}${sender}${COLORS.RESET} ${COLORS.RED}left the chat${COLORS.RESET}`;
62
+ }
57
63
  return `${COLORS.CYAN}${COLORS.BOLD}${sender}${COLORS.RESET}: ${text}`;
58
64
  }
59
65
 
package/src/worker.js CHANGED
@@ -1,6 +1,3 @@
1
- // Active SSE connection writers
2
- const activeConnections = new Set();
3
-
4
1
  // Regex to match ANSI escape sequences (control codes, color formatting, etc.)
5
2
  const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
6
3
 
@@ -14,6 +11,52 @@ function sanitizeAnsi(str) {
14
11
  return str.replace(ansiRegex, '');
15
12
  }
16
13
 
14
+ /**
15
+ * Periodically polls the D1 database for new messages and writes them to the SSE stream.
16
+ * This ensures multi-isolate reliability since Cloudflare Workers are stateless.
17
+ * @param {WritableStreamDefaultWriter} writer
18
+ * @param {Object} env
19
+ * @param {Request} request
20
+ */
21
+ async function pollAndStream(writer, env, request) {
22
+ const encoder = new TextEncoder();
23
+ try {
24
+ const initialResult = await env.DB.prepare('SELECT MAX(id) as maxId FROM messages').first();
25
+ let lastSeenId = initialResult?.maxId || 0;
26
+
27
+ // Send initial handshake
28
+ await writer.write(encoder.encode(': ok\n\n'));
29
+
30
+ while (!request.signal.aborted) {
31
+ const { results } = await env.DB.prepare(
32
+ 'SELECT id, username, content, created_at FROM messages WHERE id > ? ORDER BY id ASC'
33
+ )
34
+ .bind(lastSeenId)
35
+ .all();
36
+
37
+ if (results && results.length > 0) {
38
+ for (const msg of results) {
39
+ const payload = JSON.stringify({
40
+ username: msg.username,
41
+ content: msg.content,
42
+ created_at: msg.created_at,
43
+ });
44
+ await writer.write(encoder.encode(`data: ${payload}\n\n`));
45
+ lastSeenId = msg.id;
46
+ }
47
+ }
48
+
49
+ await new Promise((resolve) => setTimeout(resolve, 1000));
50
+ }
51
+ } catch (err) {
52
+ console.error('SSE Stream error:', err);
53
+ } finally {
54
+ try {
55
+ await writer.close();
56
+ } catch (_) {}
57
+ }
58
+ }
59
+
17
60
  export default {
18
61
  /**
19
62
  * Fetch handler for Cloudflare Worker.
@@ -59,20 +102,8 @@ export default {
59
102
  const { readable, writable } = new TransformStream();
60
103
  const writer = writable.getWriter();
61
104
 
62
- // Add writer to active connections tracking
63
- activeConnections.add(writer);
64
-
65
- // Handle connection disconnect to prevent memory leaks
66
- request.signal.addEventListener('abort', () => {
67
- activeConnections.delete(writer);
68
- try {
69
- writer.close();
70
- } catch (_) {}
71
- });
72
-
73
- // Keep-alive or immediate connection handshake
74
- const encoder = new TextEncoder();
75
- writer.write(encoder.encode(': ok\n\n')).catch(() => {});
105
+ // Start the D1 polling loop in the background, keeping it alive with ctx.waitUntil
106
+ ctx.waitUntil(pollAndStream(writer, env, request));
76
107
 
77
108
  return new Response(readable, {
78
109
  headers: {
@@ -121,29 +152,6 @@ export default {
121
152
  .bind(sanitizedUser, sanitizedText)
122
153
  .run();
123
154
 
124
- // Broadcast the message payload to all active SSE subscribers
125
- const encoder = new TextEncoder();
126
- const payload = JSON.stringify({
127
- username: sanitizedUser,
128
- content: sanitizedText,
129
- created_at: new Date().toISOString(),
130
- });
131
- const messageChunk = encoder.encode(`data: ${payload}\n\n`);
132
-
133
- const broadcastPromises = Array.from(activeConnections).map(async (w) => {
134
- try {
135
- await w.write(messageChunk);
136
- } catch (_) {
137
- activeConnections.delete(w);
138
- try {
139
- w.close();
140
- } catch (__) {}
141
- }
142
- });
143
-
144
- // Wait for all active broadcasts
145
- await Promise.all(broadcastPromises);
146
-
147
155
  return jsonResponse({
148
156
  success: true,
149
157
  message: 'Message sent successfully',