Skip to content

Commit 22c975c

Browse files
committed
Update based on feedback
1 parent 6acefd1 commit 22c975c

File tree

4 files changed

+81
-2
lines changed

4 files changed

+81
-2
lines changed

packages/code-link-cli/src/helpers/watcher.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,38 @@ describe("rename detection", () => {
435435
await fs.rm(tmpDir, { recursive: true, force: true })
436436
})
437437

438+
it("replaces a buffered add on the same path without leaking the old timer", async () => {
439+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "framer-watcher-"))
440+
441+
const events: WatcherEvent[] = []
442+
const watcher: Watcher = initWatcher(tmpDir)
443+
watcher.on("change", event => events.push(event))
444+
const rawWatcher = createdWatchers.at(-1)
445+
if (!rawWatcher) throw new Error("No watcher created")
446+
447+
const filePath = path.join(tmpDir, "Component.tsx")
448+
await fs.writeFile(filePath, "export const Version = 1", "utf-8")
449+
await rawWatcher.__emit("add", filePath)
450+
451+
await fs.writeFile(filePath, "export const Version = 2", "utf-8")
452+
await rawWatcher.__emit("add", filePath)
453+
454+
expect(events).toHaveLength(0)
455+
456+
await waitForBuffer()
457+
458+
expect(events).toEqual([
459+
{
460+
kind: "add",
461+
relativePath: "Component.tsx",
462+
content: "export const Version = 2",
463+
},
464+
])
465+
466+
await watcher.close()
467+
await fs.rm(tmpDir, { recursive: true, force: true })
468+
})
469+
438470
it("emits add and delete separately when content differs", async () => {
439471
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "framer-watcher-"))
440472

packages/code-link-cli/src/helpers/watcher.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ export function initWatcher(filesDir: string): Watcher {
298298
}
299299

300300
// No pending delete match — buffer this add in case a delete arrives soon
301+
const existingPendingAdd = pendingAdds.get(relativePath)
302+
if (existingPendingAdd) {
303+
clearTimeout(existingPendingAdd.timer)
304+
}
305+
const retainedPreviousContentHash = existingPendingAdd?.previousContentHash ?? previousContentHash
301306
const timer = setTimeout(() => {
302307
pendingAdds.delete(relativePath)
303308
dispatchEvent({ kind: "add", relativePath, content })
@@ -308,7 +313,7 @@ export function initWatcher(filesDir: string): Watcher {
308313
contentHash,
309314
content,
310315
timer,
311-
previousContentHash,
316+
previousContentHash: retainedPreviousContentHash,
312317
})
313318
return
314319
}

plugins/code-link/src/api.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,33 @@ describe("CodeFilesAPI", () => {
221221
expect(trackerRemember).not.toHaveBeenCalled()
222222
})
223223

224+
it("seeds snapshot before a remote write finishes to avoid echoing subscription updates", async () => {
225+
const { api, socket, tracker, trackerRemember } = setup()
226+
const oldContent = "export const Race = 1"
227+
const newContent = "export const Race = 2"
228+
const existing = createCodeFile({ name: "Race.tsx", content: oldContent })
229+
230+
await publishSnapshotAndClear({
231+
api,
232+
socket,
233+
files: [createCodeFile({ name: "Race.tsx", content: oldContent })],
234+
})
235+
236+
framerMock.getCodeFiles.mockResolvedValueOnce([existing])
237+
existing.setFileContent.mockImplementation(async (content: string) => {
238+
existing.content = content
239+
framerMock.getCodeFiles.mockResolvedValue([existing])
240+
await api.handleFramerFilesChanged(socket as unknown as WebSocket, tracker)
241+
})
242+
243+
await api.applyRemoteChange("Race.tsx", newContent, socket as unknown as WebSocket)
244+
245+
const sentMessages = getSentMessages(socket)
246+
expect(sentMessages).toHaveLength(1)
247+
expectFileSyncedMessage(sentMessages[0], "Race.tsx")
248+
expect(trackerRemember).not.toHaveBeenCalled()
249+
})
250+
224251
it("does not update snapshot state when a remote write fails", async () => {
225252
const { api, socket, tracker, trackerRemember } = setup()
226253
const oldContent = "export const Broken = 1"

plugins/code-link/src/api.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,23 @@ export class CodeFilesAPI {
7878

7979
async applyRemoteChange(fileName: string, content: string, socket: WebSocket) {
8080
const normalizedName = normalizeCodeFileName(fileName)
81-
const updatedAt = await upsertFramerFile(normalizedName, content)
81+
const previousSnapshot = this.lastSnapshot.get(normalizedName)
82+
83+
// Update snapshot BEFORE upsert to prevent race with file subscription.
8284
this.lastSnapshot.set(normalizedName, content)
85+
86+
let updatedAt: number | undefined
87+
try {
88+
updatedAt = await upsertFramerFile(normalizedName, content)
89+
} catch (error) {
90+
if (previousSnapshot !== undefined) {
91+
this.lastSnapshot.set(normalizedName, previousSnapshot)
92+
} else {
93+
this.lastSnapshot.delete(normalizedName)
94+
}
95+
throw error
96+
}
97+
8398
// Send file-synced message with timestamp
8499
const syncTimestamp = updatedAt ?? Date.now()
85100
log.debug(

0 commit comments

Comments
 (0)