Skip to content

Commit 2539e37

Browse files
committed
feat: add 30ms batching for file changes to prevent race conditions
Rapid typing was causing edits to arrive out-of-order on the backend due to concurrent request processing. Now accumulate changes per file and send them as a single batch after 30ms of inactivity. Changes: - Add PendingBatch type to types.ts - Add BATCH_DELAY_MS constant to constants.ts - Implement flushChanges() to send batched edits - Update handleChange() to batch regular edits with debounce - Handle undo/redo immediately with flush of pending changes - Flush pending changes on file close, save, and disconnect This reduces network overhead, LSP calls, and guarantees edit order.
1 parent 0024204 commit 2539e37

3 files changed

Lines changed: 75 additions & 5 deletions

File tree

anycode/App.tsx

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React, { useState, useEffect, useRef, useCallback } from 'react';
22
import { io, Socket } from 'socket.io-client';
33
import { AnycodeEditorReact, AnycodeEditor } from 'anycode-react';
4-
import type { Change, Position } from '../anycode-base/src/code';
4+
import type { Change, Position, Edit } from '../anycode-base/src/code';
55
import { WatcherCreate, WatcherEdits, WatcherRemove,
66
type CursorHistory, type Terminal, type AcpSession,
77
type AcpMessage, type AcpPromptStateMessage, type AcpToolCallMessage,
8-
type AcpOpenFileMessage, type SearchResult, type SearchEnd, type SearchMatch
8+
type AcpOpenFileMessage, type SearchResult, type SearchEnd, type SearchMatch,
9+
type PendingBatch
910
} from './types';
1011
import { loadTerminals, loadTerminalSelected, loadBottomVisible,
1112
loadLeftPanelVisible, loadRightPanelVisible, loadCenterPaneVisible,
@@ -23,7 +24,7 @@ import { getAllAgents, getDefaultAgent, updateAgents, getDefaultAgentId,
2324
ensureDefaultAgents
2425
} from './agents';
2526
import { AcpAgent } from './types';
26-
import { DEFAULT_FILE, DEFAULT_FILE_CONTENT, BACKEND_URL } from './constants';
27+
import { DEFAULT_FILE, DEFAULT_FILE_CONTENT, BACKEND_URL, BATCH_DELAY_MS } from './constants';
2728
import './App.css';
2829
import {
2930
Completion, CompletionRequest, Diagnostic, DiagnosticResponse,
@@ -85,6 +86,9 @@ const App: React.FC = () => {
8586
const reconnectAttemptsRef = useRef<number>(0);
8687
const reconnectDelay = 1000;
8788

89+
// Batching for file changes
90+
const pendingChangesRef = useRef<Map<string, PendingBatch>>(new Map());
91+
8892
const handleLeftPanelVisibleChange = (index: number, visible: boolean) => {
8993
console.log('handleLeftPanelVisibleChange', index, visible);
9094
if (index === 0) {
@@ -221,11 +225,55 @@ const App: React.FC = () => {
221225
}
222226
}, [activeFileId]);
223227

228+
const flushChanges = (filename: string) => {
229+
const batch = pendingChangesRef.current.get(filename);
230+
if (!batch || batch.changes.length === 0) return;
231+
232+
// Merge all edits from batched changes
233+
const allEdits = batch.changes.flatMap(c => c.edits);
234+
235+
// Send all batched edits in one message
236+
if (wsRef.current && isConnected) {
237+
wsRef.current.emit("file:change", {
238+
file: filename,
239+
edits: allEdits
240+
});
241+
}
242+
243+
// Clear the batch
244+
batch.changes = [];
245+
batch.timerId = null;
246+
};
247+
224248
const handleChange = (filename: string, change: Change) => {
225249
console.log('handleChange', filename, change);
226250

227-
if (wsRef.current && isConnected) {
228-
wsRef.current.emit("file:change", { file: filename, ...change});
251+
// Handle undo/redo immediately (flush pending changes first)
252+
if (change.isUndo || change.isRedo) {
253+
flushChanges(filename);
254+
if (wsRef.current && isConnected) {
255+
wsRef.current.emit("file:change", { file: filename, ...change });
256+
}
257+
} else {
258+
// Batch regular edits
259+
let batch = pendingChangesRef.current.get(filename);
260+
if (!batch) {
261+
batch = { changes: [], timerId: null };
262+
pendingChangesRef.current.set(filename, batch);
263+
}
264+
265+
// Add change to batch
266+
batch.changes.push(change);
267+
268+
// Clear old timer
269+
if (batch.timerId) {
270+
clearTimeout(batch.timerId);
271+
}
272+
273+
// Set new timer to flush changes
274+
batch.timerId = setTimeout(() => {
275+
flushChanges(filename);
276+
}, BATCH_DELAY_MS);
229277
}
230278

231279
const file = files.find(f => f.id === filename);
@@ -269,6 +317,9 @@ const App: React.FC = () => {
269317
};
270318

271319
const closeFile = (fileId: string) => {
320+
// Flush any pending changes before closing
321+
flushChanges(fileId);
322+
272323
if (wsRef.current && isConnected) {
273324
wsRef.current.emit("file:close", { file: fileId });
274325
}
@@ -320,6 +371,9 @@ const App: React.FC = () => {
320371
};
321372

322373
const saveFile = (fileId: string) => {
374+
// Flush any pending changes before saving
375+
flushChanges(fileId);
376+
323377
const editor = editorRefs.current.get(fileId);
324378
if (!editor) return;
325379

@@ -452,6 +506,14 @@ const App: React.FC = () => {
452506
};
453507

454508
const disconnectFromBackend = () => {
509+
// Flush all pending changes before disconnecting
510+
pendingChangesRef.current.forEach((batch, filename) => {
511+
if (batch.timerId) {
512+
clearTimeout(batch.timerId);
513+
}
514+
});
515+
pendingChangesRef.current.clear();
516+
455517
if (reconnectTimeoutRef.current) {
456518
clearTimeout(reconnectTimeoutRef.current);
457519
reconnectTimeoutRef.current = null;

anycode/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ function hello() {
3333
// if curent port is 5173 then use 3000 else use current port
3434
const port = window.location.port === '5173' ? '3000' : window.location.port;
3535
export const BACKEND_URL = `${window.location.protocol}//${window.location.hostname}:${port}`;
36+
37+
// File change batching delay in milliseconds
38+
export const BATCH_DELAY_MS = 30;

anycode/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ export interface WatcherRemove {
9393
isFile: boolean;
9494
}
9595

96+
export interface PendingBatch {
97+
changes: Change[];
98+
timerId: number | null;
99+
}
100+
96101
// ACP protocol types
97102
export interface AcpAgent {
98103
id: string;

0 commit comments

Comments
 (0)