Skip to content

Commit 214bf61

Browse files
committed
fix(files): publish a stable file-doc flush and trace its outcome
The published flush was bound to the provider's identity, so every socket churn republished it — and a churn ending on null left the file-detail header with nothing to call, silently degrading a retype back to a stale read. It now publishes once and resolves the provider at call time. Adds a log on both sides of the flush. Its outcome decides whether the caller may treat the durable bytes as current, and unchanged/skipped are both silent no-writes, so a stale read after a retype is otherwise indistinguishable from a rendering bug.
1 parent 3a06d38 commit 214bf61

3 files changed

Lines changed: 35 additions & 6 deletions

File tree

apps/realtime/src/handlers/file-doc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,12 @@ export function setupWorkspaceFileDocHandlers(
13041304
room.persistDeadline = null
13051305

13061306
const outcome = await flushPersist(name, room, 'requested')
1307+
// The caller's next step depends on this outcome, and `unchanged`/`skipped` are both silent
1308+
// no-writes — worth a line so a stale read after a retype can be traced without a repro.
1309+
logger.info(`Requested flush for file ${fileId}: ${outcome.status}`, {
1310+
edited: room.edited,
1311+
hasWorkspace: Boolean(room.workspaceId),
1312+
})
13071313
ack(outcome.status, outcome.status === 'persisted' ? outcome.version : undefined)
13081314
} catch (error) {
13091315
logger.error('Error flushing file-doc room:', error)

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/use-file-doc-collaboration.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useMemo, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { FILE_DOC_EVENTS, type FileDocPresence } from '@sim/realtime-protocol/file-doc'
55
import { Awareness } from 'y-protocols/awareness'
66
import * as Y from 'yjs'
@@ -110,10 +110,25 @@ export function useFileDocCollaboration({
110110
}
111111
}, [enabled, socket, fileId])
112112

113-
// Publish this document's flush so the file-detail header can force durability before it reads the
114-
// file's stored bytes back (changing the file's type unmounts this editor and swaps in one that
115-
// reads them). Cleared with the provider, so it can never outlive the socket it wraps.
116-
useReportFileDocFlush(useMemo(() => (provider ? () => provider.flush() : null), [provider]))
113+
/**
114+
* Publish this document's flush so the file-detail header can force durability before it reads the
115+
* file's stored bytes back (changing the file's type unmounts this editor and swaps in one that
116+
* reads them).
117+
*
118+
* The published function is STABLE and resolves the provider at call time through a ref. Binding it
119+
* to the provider's identity instead looked equivalent and was not: the provider is torn down and
120+
* rebuilt on every socket change, so each churn republished — and any churn ending on `null` left
121+
* the header with nothing to call, silently degrading the retype back to a stale read. A stable
122+
* identity publishes once and always sees the live provider.
123+
*/
124+
const providerRef = useRef<FileDocProvider | null>(null)
125+
providerRef.current = provider
126+
useReportFileDocFlush(
127+
useCallback(
128+
() => providerRef.current?.flush() ?? Promise.resolve({ fileId, status: 'skipped' as const }),
129+
[fileId]
130+
)
131+
)
117132

118133
const reportOthers = useReportFileDocOthers()
119134
const reportOthersRef = useRef(reportOthers)

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,15 @@ export function Files() {
12381238
if (type.mimeType === file.type && nextName === file.name) return
12391239

12401240
if (isDirtyRef.current) await saveRef.current?.()
1241-
await flushFileDocRef(fileDocFlushRef)
1241+
const flushed = await flushFileDocRef(fileDocFlushRef)
1242+
if (flushed.status !== 'persisted') {
1243+
// Not an error — `unchanged` means there was nothing to write, and `skipped` means the write
1244+
// did not land in time. The retype proceeds either way; this is the breadcrumb for a stale
1245+
// first paint, which is otherwise indistinguishable from a rendering bug.
1246+
logger.info('Changing file type without a confirmed durable flush', {
1247+
status: flushed.status,
1248+
})
1249+
}
12421250

12431251
const siblingNames = new Set(
12441252
filesRef.current

0 commit comments

Comments
 (0)