Skip to content

Commit e05cf89

Browse files
authored
fix(files): cap how long a room delivery waits for its refetches (#9427)
Each room delivery waits at most DELIVERY_SETTLE_CAP_MS (one minute, above the slowest list read) for its refetches via withinDeadline, so a refetch that never settles no longer stops every later delivery; past the cap, the next delivery's invalidation cancels and restarts a hung refetch of a query with data. Settling refetches are still waited for and never overlapped.
1 parent 17f35ef commit e05cf89

2 files changed

Lines changed: 30 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: 15 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,15 @@ 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, later deliveries run again:
24+
* their invalidation cancels and restarts a hung refetch of a query that has data, while a hung
25+
* first load is joined, as it always was, until it settles or its last observer unmounts. A refetch
26+
* still progressing past the cap is restarted the same way, so the cap sits well above the
27+
* slowest list read seen.
28+
*/
29+
export const DELIVERY_SETTLE_CAP_MS = 60_000
2030

2131
interface JoinErrorPayload {
2232
workspaceId: string
@@ -26,8 +36,8 @@ interface JoinErrorPayload {
2636
}
2737

2838
/**
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.
39+
* Runs when the room's resources changed. A returned promise is awaited, up to
40+
* {@link DELIVERY_SETTLE_CAP_MS}, before the next delivery starts.
3141
*/
3242
type RoomChangedCallback = () => unknown
3343

@@ -85,9 +95,11 @@ function createSharedRoomSubscription(
8595
try {
8696
do {
8797
changedSinceDelivery = false
88-
await Promise.allSettled(
98+
const settled = Promise.allSettled(
8999
[...callbacks.values()].map(async (group) => group.values().next().value?.())
90100
)
101+
// `allSettled` never rejects, so the only rejection here is the cap expiring.
102+
await withinDeadline(() => settled, Date.now() + DELIVERY_SETTLE_CAP_MS).catch(() => {})
91103
await sleep(INVALIDATION_COALESCE_MS)
92104
} while (changedSinceDelivery && !disposed)
93105
} finally {

0 commit comments

Comments
 (0)