Skip to content

Commit 77050d4

Browse files
committed
fix(files): cap how long a room delivery waits for its refetches
The coalescing in #9419 awaits each delivery's refetches before the next one starts, so a refetch still in flight is neither cancelled nor overlapped. A fetch has no timeout of its own, so one that hung held the room's deliveries forever and the tab stopped picking up remote changes. Each delivery now waits at most DELIVERY_SETTLE_CAP_MS; past it, the next delivery's invalidation cancels and restarts the hung fetch, as every broadcast did before coalescing. Autopsy: the coalescing tests covered a refetch that settles late, never one that does not settle.
1 parent 17f35ef commit 77050d4

2 files changed

Lines changed: 27 additions & 3 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/hooks/use-workspace-invalidation-room.test.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ vi.mock('@/app/workspace/providers/socket-provider', () => ({
1414
}))
1515

1616
import {
17+
DELIVERY_SETTLE_CAP_MS,
1718
INVALIDATION_COALESCE_MS,
1819
useWorkspaceInvalidationRoom,
1920
} from '@/app/workspace/[workspaceId]/hooks/use-workspace-invalidation-room'
@@ -152,5 +153,19 @@ describe('useWorkspaceInvalidationRoom', () => {
152153
await vi.advanceTimersByTimeAsync(INVALIDATION_COALESCE_MS)
153154
expect(onChanged).toHaveBeenCalledTimes(2)
154155
})
156+
157+
it('delivers again once a refetch that never settles outlasts the cap', async () => {
158+
const onChanged = vi.fn(() => new Promise<void>(() => {}))
159+
mount('ws-1', 'workspace-files', onChanged)
160+
161+
broadcast('workspace-files-changed', 'ws-1')
162+
await vi.advanceTimersByTimeAsync(0)
163+
broadcast('workspace-files-changed', 'ws-1')
164+
await vi.advanceTimersByTimeAsync(DELIVERY_SETTLE_CAP_MS - 1)
165+
expect(onChanged).toHaveBeenCalledTimes(1)
166+
167+
await vi.advanceTimersByTimeAsync(1 + INVALIDATION_COALESCE_MS)
168+
expect(onChanged).toHaveBeenCalledTimes(2)
169+
})
155170
})
156171
})

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
55
import type { RoomType } from '@sim/realtime-protocol/rooms'
66
import { sleep } from '@sim/utils/helpers'
77
import type { Socket } from 'socket.io-client'
8+
import { withinDeadline } from '@/lib/core/utils/deadline'
89
import { useSocket } from '@/app/workspace/providers/socket-provider'
910

1011
const logger = createLogger('WorkspaceInvalidationRoom')
@@ -17,6 +18,12 @@ const JOIN_RETRY_BASE_MS = 1000
1718
* uploading files one by one) otherwise refetches every viewer's list once per broadcast.
1819
*/
1920
export const INVALIDATION_COALESCE_MS = 1000
21+
/**
22+
* The longest a delivery waits for its refetches. A fetch has no timeout of its own, so one that
23+
* hangs would otherwise hold the room's deliveries forever; past this, the next delivery's
24+
* invalidation cancels and restarts it.
25+
*/
26+
export const DELIVERY_SETTLE_CAP_MS = 30_000
2027

2128
interface JoinErrorPayload {
2229
workspaceId: string
@@ -26,8 +33,8 @@ interface JoinErrorPayload {
2633
}
2734

2835
/**
29-
* Runs when the room's resources changed. A returned promise is awaited before the next delivery,
30-
* so a refetch still in flight is neither cancelled nor overlapped by the next one.
36+
* Runs when the room's resources changed. A returned promise is awaited, up to
37+
* {@link DELIVERY_SETTLE_CAP_MS}, before the next delivery starts.
3138
*/
3239
type RoomChangedCallback = () => unknown
3340

@@ -85,9 +92,11 @@ function createSharedRoomSubscription(
8592
try {
8693
do {
8794
changedSinceDelivery = false
88-
await Promise.allSettled(
95+
const settled = Promise.allSettled(
8996
[...callbacks.values()].map(async (group) => group.values().next().value?.())
9097
)
98+
// `allSettled` never rejects, so the only rejection here is the cap expiring.
99+
await withinDeadline(() => settled, Date.now() + DELIVERY_SETTLE_CAP_MS).catch(() => {})
91100
await sleep(INVALIDATION_COALESCE_MS)
92101
} while (changedSinceDelivery && !disposed)
93102
} finally {

0 commit comments

Comments
 (0)