Skip to content

Commit 7de77ea

Browse files
authored
Merge pull request #6043 from simstudioai/collab-review-fixes
improvement(realtime): post-review fixes for collab dirty-state + relay lifecycle
2 parents 320e18c + d3ca016 commit 7de77ea

4 files changed

Lines changed: 37 additions & 11 deletions

File tree

apps/realtime/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,13 @@ async function main() {
112112
logger.info(`Health check available at: http://localhost:${PORT}/health`)
113113
})
114114

115+
let shuttingDown = false
115116
const shutdown = async () => {
117+
// SIGINT and SIGTERM both bind this; a double signal (or SIGTERM then SIGINT during the drain)
118+
// must not run the whole teardown twice — that means a second forced-exit timer and a second
119+
// Redis quit (which throws "The client is closed").
120+
if (shuttingDown) return
121+
shuttingDown = true
116122
logger.info('Shutting down Socket.IO server...')
117123

118124
accessRevalidation.stop()
@@ -145,6 +151,15 @@ async function main() {
145151
logger.error('Error during FileDocStore shutdown:', error)
146152
}
147153

154+
// Close local client connections so `httpServer.close()` can complete its callback and exit
155+
// gracefully — otherwise open websockets keep it hanging until the forced-exit timer below.
156+
// Local-only: a rolling deploy must not disconnect clients pinned to other pods.
157+
try {
158+
io.local.disconnectSockets(true)
159+
} catch (error) {
160+
logger.error('Error disconnecting sockets on shutdown:', error)
161+
}
162+
148163
httpServer.close(() => {
149164
logger.info('Socket.IO server closed')
150165
process.exit(0)

apps/realtime/src/routes/http.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,12 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) {
169169
try {
170170
const body = await readRequestBody(req)
171171
const { workspaceId } = JSON.parse(body)
172-
if (typeof workspaceId === 'string' && workspaceId.length > 0) {
173-
roomManager.emitToRoom(
174-
{ type: ROOM_TYPES.WORKSPACE_FILES, id: workspaceId },
175-
'workspace-files-changed',
176-
{ workspaceId, timestamp: Date.now() }
177-
)
178-
}
172+
if (!isNonEmptyString(workspaceId)) return sendError(res, 'Invalid workspaceId', 400)
173+
roomManager.emitToRoom(
174+
{ type: ROOM_TYPES.WORKSPACE_FILES, id: workspaceId },
175+
'workspace-files-changed',
176+
{ workspaceId, timestamp: Date.now() }
177+
)
179178
sendSuccess(res)
180179
} catch (error) {
181180
logger.error('Error handling workspace files changed notification:', error)

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,16 @@ export function useEditableFileContent({
257257
),
258258
})
259259

260+
// When the client can't autosave it isn't the durability owner: the collaborative editor holds
261+
// `canAutosave` permanently false because the relay persists the doc server-side (debounced + on
262+
// last-disconnect), so `savedContent` never advances and raw `isDirty` would latch true after any
263+
// local OR remote keystroke — surfacing a spurious "Unsaved changes" navigation prompt whose
264+
// "Discard" discards nothing real. With nothing the user can save, there is nothing to warn about.
265+
const isDirtyForCaller = canAutosave && isDirty
266+
260267
useEffect(() => {
261-
onDirtyChangeRef.current?.(isDirty)
262-
}, [isDirty])
268+
onDirtyChangeRef.current?.(isDirtyForCaller)
269+
}, [isDirtyForCaller])
263270

264271
useEffect(() => {
265272
onSaveStatusChangeRef.current?.(
@@ -307,6 +314,6 @@ export function useEditableFileContent({
307314
hasContentError: streamingContent === undefined && Boolean(error) && !isInitialized,
308315
saveStatus,
309316
saveImmediately,
310-
isDirty,
317+
isDirty: isDirtyForCaller,
311318
}
312319
}

apps/sim/app/workspace/[workspaceId]/files/hooks/use-workspace-files-room.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ export function useWorkspaceFilesRoom(workspaceId: string): void {
4141
const join = () => socket.emit('join-workspace-files', { workspaceId })
4242

4343
// A fresh (re)connect gets a fresh retry budget, so a prior full exhaustion doesn't leave the
44-
// socket unable to retry a failed re-join until the next success.
44+
// socket unable to retry a failed re-join until the next success. Cancel any retry still pending
45+
// from before the reconnect so it can't fire a duplicate join after this immediate one.
4546
const handleConnect = () => {
4647
retries = 0
48+
if (retryTimer) {
49+
clearTimeout(retryTimer)
50+
retryTimer = null
51+
}
4752
join()
4853
}
4954

0 commit comments

Comments
 (0)