Private
Public Access
Trigger update checks off WS reconnect, not just the hourly poll (#42)
A backend restart during a deploy kills every open WebSocket, and the chat socket's existing reconnect-with-backoff already re-fires onopen within seconds -- reuse that as a reliable "the server just restarted" signal to check for a new service worker version, instead of waiting up to an hour for UpdateBanner's poll. The hourly poll stays as a fallback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||||
|
import { checkForUpdate, setSwRegistration } from '../lib/swUpdate'
|
||||||
import './UpdateBanner.css'
|
import './UpdateBanner.css'
|
||||||
|
|
||||||
// The service worker (registerType: 'prompt', sw.ts) already installs and
|
// The service worker (registerType: 'prompt', sw.ts) already installs and
|
||||||
@@ -6,7 +7,11 @@ import './UpdateBanner.css'
|
|||||||
// never navigates, and a page only checks for a new SW on navigation by
|
// never navigates, and a page only checks for a new SW on navigation by
|
||||||
// default, so a tab left open for hours could sit on a stale check
|
// default, so a tab left open for hours could sit on a stale check
|
||||||
// indefinitely. This polls explicitly so "reload available" shows up
|
// indefinitely. This polls explicitly so "reload available" shows up
|
||||||
// without the user having to close and reopen the app first.
|
// without the user having to close and reopen the app first. It's now a
|
||||||
|
// fallback, not the primary trigger -- useChatSocket.ts also calls
|
||||||
|
// checkForUpdate() on every WS reconnect, which reliably fires within
|
||||||
|
// seconds of a deploy (the backend restart that ships a new version also
|
||||||
|
// kills every open WS connection) rather than waiting up to an hour.
|
||||||
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000
|
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000
|
||||||
|
|
||||||
export function UpdateBanner() {
|
export function UpdateBanner() {
|
||||||
@@ -16,7 +21,8 @@ export function UpdateBanner() {
|
|||||||
} = useRegisterSW({
|
} = useRegisterSW({
|
||||||
onRegisteredSW(_url, registration) {
|
onRegisteredSW(_url, registration) {
|
||||||
if (!registration) return
|
if (!registration) return
|
||||||
setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS)
|
setSwRegistration(registration)
|
||||||
|
setInterval(checkForUpdate, UPDATE_CHECK_INTERVAL_MS)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Bridges the service worker registration (owned by UpdateBanner, which
|
||||||
|
// mounts outside ChatSocketProvider -- see App.tsx) out to code that has no
|
||||||
|
// other way to reach it, namely useChatSocket.ts's reconnect handler, which
|
||||||
|
// wants to trigger an update check whenever the WS reconnects (a reliable
|
||||||
|
// signal the backend just restarted, i.e. a deploy happened).
|
||||||
|
let registration: ServiceWorkerRegistration | null = null
|
||||||
|
|
||||||
|
export function setSwRegistration(reg: ServiceWorkerRegistration): void {
|
||||||
|
registration = reg
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkForUpdate(): void {
|
||||||
|
void registration?.update()
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { checkForUpdate } from '../lib/swUpdate'
|
||||||
import type { ServerEnvelope } from '../types'
|
import type { ServerEnvelope } from '../types'
|
||||||
|
|
||||||
interface UseChatSocketOptions {
|
interface UseChatSocketOptions {
|
||||||
@@ -42,6 +43,13 @@ export function useChatSocket({ onUnauthenticated }: UseChatSocketOptions) {
|
|||||||
let stopped = false
|
let stopped = false
|
||||||
let reconnectDelay = RECONNECT_BASE_DELAY_MS
|
let reconnectDelay = RECONNECT_BASE_DELAY_MS
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
// False only for the very first connect() of this effect's lifetime --
|
||||||
|
// every connect() after that was triggered by onclose's retry logic,
|
||||||
|
// i.e. this is a genuine reconnect. A reconnect reliably means the
|
||||||
|
// backend process just restarted (a deploy kills every open WS), so
|
||||||
|
// it's used as the trigger for an out-of-band SW update check instead
|
||||||
|
// of waiting on UpdateBanner's hourly poll -- see lib/swUpdate.ts.
|
||||||
|
let hasConnectedBefore = false
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
|
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
@@ -56,6 +64,8 @@ export function useChatSocket({ onUnauthenticated }: UseChatSocketOptions) {
|
|||||||
// socket's callbacks can stomp on state that the second (real)
|
// socket's callbacks can stomp on state that the second (real)
|
||||||
// socket already owns.
|
// socket already owns.
|
||||||
if (socketRef.current !== ws) return
|
if (socketRef.current !== ws) return
|
||||||
|
if (hasConnectedBefore) checkForUpdate()
|
||||||
|
hasConnectedBefore = true
|
||||||
reconnectDelay = RECONNECT_BASE_DELAY_MS
|
reconnectDelay = RECONNECT_BASE_DELAY_MS
|
||||||
setConnected(true)
|
setConnected(true)
|
||||||
// Re-join whatever rooms were joined before a reconnect -- the
|
// Re-join whatever rooms were joined before a reconnect -- the
|
||||||
|
|||||||
Reference in New Issue
Block a user