From f749813bc41f055992e85cd21bfd0199dc3acaf2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 05:58:41 -0700 Subject: [PATCH] fix: bound editor self-write stamps (#4145) --- .../editor/editor-self-write-registry.test.ts | 22 ++++++++++++ .../editor/editor-self-write-registry.ts | 34 +++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/editor/editor-self-write-registry.test.ts b/src/renderer/src/components/editor/editor-self-write-registry.test.ts index 0b30bda1b14..8bc90df02b1 100644 --- a/src/renderer/src/components/editor/editor-self-write-registry.test.ts +++ b/src/renderer/src/components/editor/editor-self-write-registry.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { __clearSelfWriteRegistryForTests, + __getSelfWriteRegistrySizeForTests, clearSelfWrite, hasRecentSelfWrite, recordSelfWrite @@ -36,4 +37,25 @@ describe('editor self-write registry', () => { expect(hasRecentSelfWrite('/repo/a.md')).toBe(false) }) + + it('prunes expired stamps when recording later writes', () => { + recordSelfWrite('/repo/old.md') + + vi.advanceTimersByTime(751) + recordSelfWrite('/repo/new.md') + + expect(__getSelfWriteRegistrySizeForTests()).toBe(1) + expect(hasRecentSelfWrite('/repo/old.md')).toBe(false) + expect(hasRecentSelfWrite('/repo/new.md')).toBe(true) + }) + + it('caps retained stamps', () => { + for (let i = 0; i < 260; i++) { + recordSelfWrite(`/repo/${i}.md`) + } + + expect(__getSelfWriteRegistrySizeForTests()).toBe(256) + expect(hasRecentSelfWrite('/repo/0.md')).toBe(false) + expect(hasRecentSelfWrite('/repo/259.md')).toBe(true) + }) }) diff --git a/src/renderer/src/components/editor/editor-self-write-registry.ts b/src/renderer/src/components/editor/editor-self-write-registry.ts index e1a2bb1f199..12829069e85 100644 --- a/src/renderer/src/components/editor/editor-self-write-registry.ts +++ b/src/renderer/src/components/editor/editor-self-write-registry.ts @@ -12,6 +12,7 @@ import { normalizeAbsolutePathForComparison } from '@/components/right-sidebar/f // a short TTL so a genuinely external edit that lands after the window still // gets picked up. const SELF_WRITE_TTL_MS = 750 +const SELF_WRITE_MAX_STAMPS = 256 export type RecentSelfWrite = { content: string | null @@ -23,11 +24,36 @@ type SelfWriteStamp = RecentSelfWrite & { const stamps = new Map() +function pruneExpiredSelfWrites(now = Date.now()): void { + for (const [key, stamp] of stamps) { + if (now > stamp.expiresAt) { + stamps.delete(key) + } + } +} + +function enforceSelfWriteStampLimit(): void { + while (stamps.size > SELF_WRITE_MAX_STAMPS) { + const oldest = stamps.keys().next().value + if (oldest === undefined) { + break + } + stamps.delete(oldest) + } +} + export function recordSelfWrite(absolutePath: string, content?: string): void { - stamps.set(normalizeAbsolutePathForComparison(absolutePath), { + const now = Date.now() + pruneExpiredSelfWrites(now) + const key = normalizeAbsolutePathForComparison(absolutePath) + // Why: a missing watcher echo should not leave stale path/content stamps in + // memory for the whole renderer session. + stamps.delete(key) + stamps.set(key, { content: content ?? null, - expiresAt: Date.now() + SELF_WRITE_TTL_MS + expiresAt: now + SELF_WRITE_TTL_MS }) + enforceSelfWriteStampLimit() } export function clearSelfWrite(absolutePath: string): void { @@ -54,3 +80,7 @@ export function hasRecentSelfWrite(absolutePath: string): boolean { export function __clearSelfWriteRegistryForTests(): void { stamps.clear() } + +export function __getSelfWriteRegistrySizeForTests(): number { + return stamps.size +}