Skip to content

Commit 30413e9

Browse files
committed
fix(sync): debounce unsubscriptions
1 parent 59e1bf7 commit 30413e9

File tree

1 file changed

+41
-6
lines changed

1 file changed

+41
-6
lines changed

packages/plugin-sync/src/core/server.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const _connections: Array<Connection> = []
2222
const _state: Record<string, unknown> = {}
2323
const _keySubscribers: Record<string, string[]> = {} // key -> [connectionId...] in subscription order
2424
const _keyHosts: Record<string, string> = {} // key -> hostConnectionId
25+
const _pendingUnsubscribes: Record<string, NodeJS.Timeout> = {} // "connectionId:cleanKey" -> timeout
26+
const UNSUBSCRIBE_DELAY_MS = 150 // Debounce delay to handle React StrictMode double-firing
2527
let _wss: WebSocketServer | undefined
2628

2729
function getSocketServer() {
@@ -195,6 +197,21 @@ function handleMessage(connection: Connection, message: string) {
195197
}
196198

197199
case 'on': {
200+
// Cancel any pending unsubscribe for this connection+key (handles StrictMode remount)
201+
const pendingKey = `${connection.id}:${cleanKey}`
202+
if (_pendingUnsubscribes[pendingKey]) {
203+
clearTimeout(_pendingUnsubscribes[pendingKey])
204+
delete _pendingUnsubscribes[pendingKey]
205+
syncLogger.debug(`Cancelled pending unsubscribe for ${connection.id} on ${cleanKey}`)
206+
207+
// Already subscribed, just send current state if exists
208+
if (_state[cleanKey]) {
209+
const response: MessagePayload = { data: _state[cleanKey], key, type: 'update' }
210+
connection.ws.send(JSON.stringify(response))
211+
}
212+
break
213+
}
214+
198215
const isNewSubscriber = !connection.watch.includes(cleanKey)
199216

200217
if (isNewSubscriber) {
@@ -231,14 +248,24 @@ function handleMessage(connection: Connection, message: string) {
231248
}
232249

233250
case 'off': {
234-
const index = connection.watch.indexOf(cleanKey)
235-
if (index > -1) {
236-
connection.watch.splice(index, 1)
237-
syncLogger.debug(`Connection ${connection.id} stopped watching:`, cleanKey)
251+
const pendingKey = `${connection.id}:${cleanKey}`
238252

239-
// Handle unsubscription with host migration
240-
handleUnsubscribe(connection, cleanKey, key)
253+
// Clear any existing pending unsubscribe for this connection+key
254+
if (_pendingUnsubscribes[pendingKey]) {
255+
clearTimeout(_pendingUnsubscribes[pendingKey])
241256
}
257+
258+
// Schedule delayed unsubscribe (handles React StrictMode double-firing)
259+
_pendingUnsubscribes[pendingKey] = setTimeout(() => {
260+
delete _pendingUnsubscribes[pendingKey]
261+
262+
const index = connection.watch.indexOf(cleanKey)
263+
if (index > -1) {
264+
connection.watch.splice(index, 1)
265+
syncLogger.debug(`Connection ${connection.id} stopped watching:`, cleanKey)
266+
handleUnsubscribe(connection, cleanKey, key)
267+
}
268+
}, UNSUBSCRIBE_DELAY_MS)
242269
break
243270
}
244271

@@ -349,6 +376,14 @@ function handleConnection(ws: WebSocket) {
349376
ws.on('close', () => {
350377
syncLogger.debug(`Connection ${connection.id} closed. Removing...`)
351378

379+
// Clear any pending unsubscribes for this connection (they're closing anyway)
380+
Object.keys(_pendingUnsubscribes).forEach((pendingKey) => {
381+
if (pendingKey.startsWith(`${connection.id}:`)) {
382+
clearTimeout(_pendingUnsubscribes[pendingKey])
383+
delete _pendingUnsubscribes[pendingKey]
384+
}
385+
})
386+
352387
// Handle unsubscription for all watched keys with host migration
353388
connection.watch.forEach((cleanKey) => {
354389
handleUnsubscribe(connection, cleanKey)

0 commit comments

Comments
 (0)