accelr8 3.1.1 → 3.3.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.
@@ -6,7 +6,7 @@ import {
6
6
  LIMITS,
7
7
  PORT,
8
8
  PROTO
9
- } from "./chunk-AY3UXM6A.js";
9
+ } from "./chunk-TW4QNNNT.js";
10
10
 
11
11
  // src/server.ts
12
12
  import { WebSocketServer, WebSocket } from "ws";
@@ -109,6 +109,7 @@ function sanitizeText(v, maxCp) {
109
109
  return out;
110
110
  }
111
111
  var RoomCore = class {
112
+ // per-sid last voiceStats (2s)
112
113
  constructor(host) {
113
114
  this.host = host;
114
115
  }
@@ -137,8 +138,13 @@ var RoomCore = class {
137
138
  // userId -> trigger times
138
139
  // voice channel (realtime, loss-tolerant): sids currently in voice, and a
139
140
  // per-sid rate window (voiceFrame is exempt from the general frame flood cap)
140
- voiceSids = /* @__PURE__ */ new Set();
141
+ // voice membership is AUTHORITATIVE on the session attachment (SessionState.voice),
142
+ // not here — an in-memory Set was wiped by every DO hibernation/eviction/redeploy.
141
143
  voiceWindow = /* @__PURE__ */ new Map();
144
+ // per-sid frame rate window
145
+ voiceNackAt = /* @__PURE__ */ new Map();
146
+ // per-sid last voiceDrop NACK (1/s)
147
+ voiceStatsAt = /* @__PURE__ */ new Map();
142
148
  // ---------- lifecycle ----------
143
149
  async onMessage(conn, raw) {
144
150
  let msg;
@@ -330,28 +336,52 @@ var RoomCore = class {
330
336
  break;
331
337
  }
332
338
  case "voiceJoin": {
333
- if (!s.projectId || this.voiceSids.has(s.sid)) break;
334
- this.voiceSids.add(s.sid);
339
+ if (!s.projectId) break;
340
+ const already = s.voice === true;
341
+ if (!already) {
342
+ s.voice = true;
343
+ this.host.setSession(conn, s);
344
+ await this.systemLine(s.projectId, `${s.name} joined voice`);
345
+ }
346
+ this.host.send(conn, { t: "voiceJoined", peers: this.voicePeersOf(s.projectId) });
335
347
  this.broadcastVoice(s.projectId);
336
- await this.systemLine(s.projectId, `${s.name} joined voice`);
337
348
  break;
338
349
  }
339
350
  case "voiceLeave": {
340
- if (!s.projectId || !this.voiceSids.has(s.sid)) break;
341
- this.voiceSids.delete(s.sid);
351
+ if (!s.projectId || s.voice !== true) break;
352
+ s.voice = false;
353
+ this.host.setSession(conn, s);
342
354
  this.voiceWindow.delete(s.sid);
355
+ this.voiceStatsAt.delete(s.sid);
343
356
  this.broadcastVoice(s.projectId);
344
357
  await this.systemLine(s.projectId, `${s.name} left voice`);
345
358
  break;
346
359
  }
347
360
  case "voiceFrame": {
348
- if (!s.projectId || !this.voiceSids.has(s.sid)) break;
361
+ if (!s.projectId) break;
362
+ const vnow = this.host.now();
363
+ if (s.voice !== true) {
364
+ this.voiceNack(conn, s.sid, vnow, "notInVoice");
365
+ break;
366
+ }
349
367
  const data = typeof msg.data === "string" ? msg.data : "";
350
- if (!data || data.length > LIMITS.voiceFrameMaxB64) break;
351
- if (!this.rateOk(this.voiceWindow, s.sid, LIMITS.voiceFramesPerSec, 1e3)) break;
368
+ if (!data) break;
369
+ if (data.length > LIMITS.voiceFrameMaxB64) {
370
+ this.voiceNack(conn, s.sid, vnow, "tooBig");
371
+ break;
372
+ }
373
+ if (!this.rateOk(this.voiceWindow, s.sid, LIMITS.voiceFramesPerSec, 1e3)) {
374
+ this.voiceNack(conn, s.sid, vnow, "rateCapped");
375
+ break;
376
+ }
377
+ const lastStat = this.voiceStatsAt.get(s.sid) ?? 0;
378
+ if (vnow - lastStat >= 2e3) {
379
+ this.voiceStatsAt.set(s.sid, vnow);
380
+ this.host.send(conn, { t: "voiceStats", heardFromYou: (this.voiceWindow.get(s.sid) ?? []).length });
381
+ }
352
382
  for (const c of this.roomConns(s.projectId)) {
353
383
  const peer = this.host.getSession(c);
354
- if (peer && peer.sid !== s.sid && this.voiceSids.has(peer.sid)) {
384
+ if (peer && peer.sid !== s.sid && peer.voice === true) {
355
385
  this.host.send(c, { t: "voiceFrame", sid: s.sid, data });
356
386
  }
357
387
  }
@@ -526,8 +556,10 @@ var RoomCore = class {
526
556
  agents: this.agentPresence(project),
527
557
  chatTail: await this.tail(project.id, latest),
528
558
  chatLatestSeq: latest,
529
- secrets: []
559
+ secrets: [],
530
560
  // P4
561
+ voicePeers: this.voicePeersOf(project.id)
562
+ // a (re)joiner learns who's in voice
531
563
  });
532
564
  this.broadcastSessions(project.id);
533
565
  for (const c of this.roomPeers(project.id, conn))
@@ -554,8 +586,11 @@ var RoomCore = class {
554
586
  if (inv && inv.projectId === pid) void this.finalizeInvocation(inv, "partial");
555
587
  this.queues.delete(agentId);
556
588
  }
557
- const wasInVoice = this.voiceSids.delete(s.sid);
589
+ const wasInVoice = s.voice === true;
590
+ s.voice = false;
558
591
  this.voiceWindow.delete(s.sid);
592
+ this.voiceStatsAt.delete(s.sid);
593
+ this.voiceNackAt.delete(s.sid);
559
594
  s.projectId = null;
560
595
  this.host.setSession(conn, s);
561
596
  for (const c of this.roomPeers(pid, conn)) {
@@ -798,11 +833,24 @@ var RoomCore = class {
798
833
  const sessions = this.sessionsFor(projectId);
799
834
  for (const c of this.roomConns(projectId)) this.host.send(c, { t: "sessions", sessions });
800
835
  }
836
+ /** Current voice roster for a project, read off each session's attachment
837
+ * (authoritative, hibernation-proof — no in-memory Set to go stale). */
838
+ voicePeersOf(projectId) {
839
+ return this.roomConns(projectId).map((c) => this.host.getSession(c)).filter((s) => !!s && s.voice === true).map((s) => ({ sid: s.sid, userId: s.userId, name: s.name, color: s.color }));
840
+ }
801
841
  /** who is in voice for this project (join/leave presence, not per-frame). */
802
842
  broadcastVoice(projectId) {
803
- const peers = this.roomConns(projectId).map((c) => this.host.getSession(c)).filter((s) => !!s && this.voiceSids.has(s.sid)).map((s) => ({ sid: s.sid, userId: s.userId, name: s.name, color: s.color }));
843
+ const peers = this.voicePeersOf(projectId);
804
844
  for (const c of this.roomConns(projectId)) this.host.send(c, { t: "voice", peers });
805
845
  }
846
+ /** NACK a dropped voice frame back to its sender, rate-limited to 1/s/sid so a
847
+ * wedged mic can't spam the sender. Turns silent drops into a fixable signal. */
848
+ voiceNack(conn, sid, now, reason) {
849
+ const last = this.voiceNackAt.get(sid) ?? 0;
850
+ if (now - last < 1e3) return;
851
+ this.voiceNackAt.set(sid, now);
852
+ this.host.send(conn, { t: "voiceDrop", reason });
853
+ }
806
854
  onlineIds(projectId) {
807
855
  return [...new Set(this.sessionsFor(projectId).map((s) => s.userId))];
808
856
  }
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DEFAULT_SERVER,
4
4
  PORT,
5
5
  VERSION
6
- } from "./chunks/chunk-AY3UXM6A.js";
6
+ } from "./chunks/chunk-TW4QNNNT.js";
7
7
 
8
8
  // src/cli.ts
9
9
  var HELP = `
@@ -13,7 +13,7 @@ USAGE
13
13
  accelr8 open the app
14
14
  accelr8 join <CODE> open straight into a project
15
15
  accelr8 serve run your own sync server
16
- accelr8 --intro replay the full boot animation
16
+ accelr8 --intro replay the opening cinematic
17
17
 
18
18
  OPTIONS
19
19
  --name <name> display name (remembered for next time)
@@ -52,7 +52,7 @@ async function main() {
52
52
  console.error(`invalid port: ${argVal(args, "--port")}`);
53
53
  process.exit(1);
54
54
  }
55
- const { runServer } = await import("./chunks/server-EFKIKFUG.js");
55
+ const { runServer } = await import("./chunks/server-VWQ2NYRN.js");
56
56
  runServer(port);
57
57
  return;
58
58
  }
@@ -61,6 +61,19 @@ async function main() {
61
61
  await runAgentCli(args.slice(1));
62
62
  return;
63
63
  }
64
+ if (args[0] === "radio") {
65
+ if (!process.stdout.isTTY) {
66
+ console.error("accelr8 radio needs an interactive terminal (TTY).");
67
+ process.exit(1);
68
+ }
69
+ const { runRadio } = await import("./chunks/app-VQ33JVDI.js");
70
+ runRadio({
71
+ name: argVal(args, "--name"),
72
+ server: argVal(args, "--server"),
73
+ clip: argVal(args, "--clip")
74
+ });
75
+ return;
76
+ }
64
77
  let joinCode;
65
78
  if (args[0] === "join") {
66
79
  joinCode = args[1];
@@ -76,7 +89,7 @@ async function main() {
76
89
  const { cachedUpdate, refreshUpdateCache } = await import("./chunks/update-UEPNGB6Y.js");
77
90
  refreshUpdateCache();
78
91
  const update = cachedUpdate(VERSION);
79
- const { runClient } = await import("./chunks/client-J62VQCBE.js");
92
+ const { runClient } = await import("./chunks/client-6SYGGJ7L.js");
80
93
  runClient({
81
94
  name: argVal(args, "--name"),
82
95
  server: argVal(args, "--server"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accelr8",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "Collaborative ascii canvas in your terminal. Real multiplayer, live cursors, mouse-driven TUI.",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -42,7 +42,8 @@
42
42
  "chattest": "tsx src/chattest.ts",
43
43
  "inputtest": "tsx src/inputtest.ts",
44
44
  "torturetest": "tsx src/torturetest.ts",
45
- "test": "for t in core chat input ladder boot orb crypto accounts executor agentcontext map voice glyph materialize update install geo map contrast; do tsx src/${t}test.ts || exit 1; done && tsx src/rendertest.ts > /dev/null && echo RENDER OK",
45
+ "test": "for t in core chat input ladder boot orb crypto accounts executor agentcontext map voice voiceroom chain glyph materialize update install geo map contrast; do tsx src/${t}test.ts || exit 1; done && for t in station asciivideo audio funfacts launch overlay; do tsx src/radio/${t}test.ts || exit 1; done && tsx src/rendertest.ts > /dev/null && echo RENDER OK",
46
+ "teste2e": "tsx src/voicetransporttest.ts",
46
47
  "laddertest": "tsx src/laddertest.ts",
47
48
  "boottest": "tsx src/boottest.ts",
48
49
  "reconnecttest": "tsx src/reconnecttest.ts",
@@ -54,7 +55,8 @@
54
55
  "executortest": "tsx src/executortest.ts",
55
56
  "agentcontexttest": "tsx src/agentcontexttest.ts",
56
57
  "chaintest": "tsx src/chaintest.ts",
57
- "voicetransporttest": "tsx src/voicetransporttest.ts"
58
+ "voicetransporttest": "tsx src/voicetransporttest.ts",
59
+ "voiceroomtest": "tsx src/voiceroomtest.ts"
58
60
  },
59
61
  "dependencies": {
60
62
  "terminal-kit": "3.1.2",