mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
Bound runtime file-watch event batches
This commit is contained in:
@@ -1,38 +1,44 @@
|
||||
import { tmpdir } from 'os'
|
||||
import * as path from 'path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { FsChangeEvent } from '../../../../shared/types'
|
||||
import { createFileWatchEventBatcher } from './file-watch-event-batcher'
|
||||
|
||||
const LARGE_EVENT_COUNT = 150_000
|
||||
|
||||
function buildFileWatchEvents(count: number): FsChangeEvent[] {
|
||||
const events: FsChangeEvent[] = []
|
||||
const root = path.join(tmpdir(), 'orca-file-watch-batch')
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
events.push({
|
||||
kind: 'update',
|
||||
absolutePath: path.join(root, `file-${index}.txt`)
|
||||
})
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
describe('createFileWatchEventBatcher', () => {
|
||||
it('accepts large watcher event bursts', () => {
|
||||
it('keeps precise watcher events while the batch is under the overflow limit', () => {
|
||||
const emit = vi.fn()
|
||||
const batcher = createFileWatchEventBatcher(path.join(tmpdir(), 'worktree'), emit)
|
||||
const batcher = createFileWatchEventBatcher('worktree-1', emit)
|
||||
const events: FsChangeEvent[] = [
|
||||
{ kind: 'update', absolutePath: '/repo/file-a.ts' },
|
||||
{ kind: 'delete', absolutePath: '/repo/file-b.ts' }
|
||||
]
|
||||
|
||||
batcher.push(buildFileWatchEvents(LARGE_EVENT_COUNT))
|
||||
batcher.push(events)
|
||||
batcher.flush()
|
||||
|
||||
const payload = emit.mock.calls[0]?.[0] as
|
||||
| { type: string; worktree: string; events: FsChangeEvent[] }
|
||||
| undefined
|
||||
expect(payload?.type).toBe('changed')
|
||||
expect(payload?.events).toHaveLength(LARGE_EVENT_COUNT)
|
||||
expect(payload?.events.at(-1)?.absolutePath).toBe(
|
||||
path.join(tmpdir(), 'orca-file-watch-batch', `file-${LARGE_EVENT_COUNT - 1}.txt`)
|
||||
)
|
||||
expect(emit).toHaveBeenCalledWith({
|
||||
type: 'changed',
|
||||
worktree: 'worktree-1',
|
||||
events
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses very large watcher bursts without spreading them onto the stack', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const emit = vi.fn()
|
||||
const batcher = createFileWatchEventBatcher('worktree-1', emit)
|
||||
const event: FsChangeEvent = { kind: 'update', absolutePath: '/repo/file.ts' }
|
||||
const burst = Array.from({ length: 200_000 }, () => event)
|
||||
|
||||
expect(() => batcher.push(burst)).not.toThrow()
|
||||
batcher.flush()
|
||||
|
||||
expect(emit).toHaveBeenCalledWith({
|
||||
type: 'changed',
|
||||
worktree: 'worktree-1',
|
||||
events: [{ kind: 'overflow', absolutePath: '/repo/file.ts' }]
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FsChangeEvent } from '../../../../shared/types'
|
||||
|
||||
const FILE_WATCH_FLUSH_MS = 150
|
||||
const FILE_WATCH_MAX_WAIT_MS = 500
|
||||
const MAX_FILE_WATCH_BATCH_EVENTS = 5_000
|
||||
|
||||
export function createFileWatchEventBatcher(
|
||||
worktree: string,
|
||||
@@ -12,6 +13,7 @@ export function createFileWatchEventBatcher(
|
||||
dispose: () => void
|
||||
} {
|
||||
let events: FsChangeEvent[] = []
|
||||
let overflowed = false
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
let firstEventAt = 0
|
||||
|
||||
@@ -26,6 +28,7 @@ export function createFileWatchEventBatcher(
|
||||
const flush = (): void => {
|
||||
clearTimer()
|
||||
const nextEvents = events.splice(0)
|
||||
overflowed = false
|
||||
firstEventAt = 0
|
||||
if (nextEvents.length === 0) {
|
||||
return
|
||||
@@ -38,10 +41,26 @@ export function createFileWatchEventBatcher(
|
||||
if (nextEvents.length === 0) {
|
||||
return
|
||||
}
|
||||
// Why: file watchers can report huge bursts; spreading them into push can
|
||||
// exceed JavaScript's argument limit before the batch has a chance to flush.
|
||||
for (const event of nextEvents) {
|
||||
events.push(event)
|
||||
if (!overflowed) {
|
||||
const incomingOverflow = nextEvents.find((event) => event.kind === 'overflow')
|
||||
if (incomingOverflow) {
|
||||
events = [incomingOverflow]
|
||||
overflowed = true
|
||||
} else if (events.length + nextEvents.length > MAX_FILE_WATCH_BATCH_EVENTS) {
|
||||
// Why: once precision is too expensive to retain and stream, one
|
||||
// overflow asks clients to refresh safely without a huge payload.
|
||||
events = [
|
||||
{
|
||||
kind: 'overflow',
|
||||
absolutePath: nextEvents[0]?.absolutePath ?? events[0]?.absolutePath ?? ''
|
||||
}
|
||||
]
|
||||
overflowed = true
|
||||
} else {
|
||||
for (const event of nextEvents) {
|
||||
events.push(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
const now = Date.now()
|
||||
if (firstEventAt === 0) {
|
||||
@@ -63,6 +82,7 @@ export function createFileWatchEventBatcher(
|
||||
dispose(): void {
|
||||
clearTimer()
|
||||
events = []
|
||||
overflowed = false
|
||||
firstEventAt = 0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user