deckide 3.5.35 → 3.5.36

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.
@@ -10,6 +10,10 @@ const DEFAULT_PROFILE_DIR = path.join(os.homedir(), '.local', 'share', 'agent-br
10
10
  const DEFAULT_OUTPUT_DIR = path.join(os.homedir(), '.local', 'share', 'agent-browser', 'output');
11
11
  const START_TIMEOUT_MS = 30_000;
12
12
  const MAX_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
13
+ // Above this many bytes already queued on a client socket we hold the newest
14
+ // frame instead of piling on, then flush it once the socket drains.
15
+ const FRAME_COALESCE_THRESHOLD = 256 * 1024;
16
+ const FRAME_FLUSH_INTERVAL_MS = 16;
13
17
  const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
14
18
  const MIN_VIEWPORT = { width: 320, height: 240 };
15
19
  const MAX_VIEWPORT = { width: 3840, height: 2160 };
@@ -248,6 +252,9 @@ export class AgentBrowserService {
248
252
  // pushed to clients as binary WebSocket messages (no base64) to cut ~33% size
249
253
  // and avoid per-frame string allocation.
250
254
  lastFrameBuffer = null;
255
+ // Newest frame held back per slow client, flushed when its socket drains.
256
+ pendingFrames = new Map();
257
+ frameFlushTimer = null;
251
258
  clients = new Set();
252
259
  viewport = { ...DEFAULT_VIEWPORT };
253
260
  pageUrl = null;
@@ -310,6 +317,11 @@ export class AgentBrowserService {
310
317
  this.pageTitle = null;
311
318
  this.screencastStarted = false;
312
319
  this.lastFrameBuffer = null;
320
+ this.pendingFrames.clear();
321
+ if (this.frameFlushTimer) {
322
+ clearInterval(this.frameFlushTimer);
323
+ this.frameFlushTimer = null;
324
+ }
313
325
  if (proc && proc.exitCode == null && proc.signalCode == null) {
314
326
  await new Promise((resolve) => {
315
327
  const killTimer = setTimeout(() => {
@@ -347,6 +359,7 @@ export class AgentBrowserService {
347
359
  });
348
360
  socket.on('close', () => {
349
361
  this.clients.delete(socket);
362
+ this.pendingFrames.delete(socket);
350
363
  if (this.clients.size === 0) {
351
364
  void this.stopScreencast();
352
365
  }
@@ -449,7 +462,7 @@ export class AgentBrowserService {
449
462
  // change, so without this a client attaching to a static page would stay
450
463
  // blank until the page next repaints.
451
464
  if (this.lastFrameBuffer) {
452
- this.sendBinaryRaw(socket, this.lastFrameBuffer);
465
+ this.writeFrame(socket, this.lastFrameBuffer);
453
466
  }
454
467
  if (this.clients.size > 0) {
455
468
  await this.startScreencast();
@@ -732,14 +745,56 @@ export class AgentBrowserService {
732
745
  }
733
746
  broadcastBinary(buffer) {
734
747
  for (const socket of this.clients) {
735
- this.sendBinaryRaw(socket, buffer);
748
+ this.queueFrameForClient(socket, buffer);
749
+ }
750
+ }
751
+ // Per-client, backpressure-aware frame delivery. A fast client (e.g. on
752
+ // localhost) drains instantly and receives every frame; a slow client (remote
753
+ // or mobile) has only the *newest* frame held and gets it once its send buffer
754
+ // clears. This trades frame rate for latency so motion stays smooth instead of
755
+ // accumulating a backlog that stutters and then bursts.
756
+ queueFrameForClient(socket, buffer) {
757
+ if (!isOpen(socket)) {
758
+ this.pendingFrames.delete(socket);
759
+ return;
760
+ }
761
+ if (socket.bufferedAmount > FRAME_COALESCE_THRESHOLD) {
762
+ this.pendingFrames.set(socket, buffer);
763
+ this.ensureFrameFlushTimer();
764
+ return;
765
+ }
766
+ this.pendingFrames.delete(socket);
767
+ this.writeFrame(socket, buffer);
768
+ }
769
+ ensureFrameFlushTimer() {
770
+ if (this.frameFlushTimer) {
771
+ return;
772
+ }
773
+ this.frameFlushTimer = setInterval(() => this.flushPendingFrames(), FRAME_FLUSH_INTERVAL_MS);
774
+ this.frameFlushTimer.unref?.();
775
+ }
776
+ flushPendingFrames() {
777
+ for (const [socket, buffer] of this.pendingFrames) {
778
+ if (!isOpen(socket)) {
779
+ this.pendingFrames.delete(socket);
780
+ continue;
781
+ }
782
+ if (socket.bufferedAmount > FRAME_COALESCE_THRESHOLD) {
783
+ continue; // still draining; keep holding the newest frame
784
+ }
785
+ this.pendingFrames.delete(socket);
786
+ this.writeFrame(socket, buffer);
787
+ }
788
+ if (this.pendingFrames.size === 0 && this.frameFlushTimer) {
789
+ clearInterval(this.frameFlushTimer);
790
+ this.frameFlushTimer = null;
736
791
  }
737
792
  }
738
- sendBinaryRaw(socket, buffer) {
793
+ writeFrame(socket, buffer) {
739
794
  if (!isOpen(socket)) {
740
795
  return;
741
796
  }
742
- // Drop the slow client rather than letting frames pile up in memory.
797
+ // Hard ceiling: a client this far behind is unrecoverable, so drop it.
743
798
  if (socket.bufferedAmount > MAX_CLIENT_BUFFERED_AMOUNT) {
744
799
  try {
745
800
  socket.close(1009, 'Browser stream overflow');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.5.35",
3
+ "version": "3.5.36",
4
4
  "description": "Deck IDE - Browser-based IDE with terminal, file explorer, and git integration",
5
5
  "type": "module",
6
6
  "bin": {