refactor: update dependencies and remove unused code
- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`. - Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27. - Removed the `backup.rs` file as it is no longer needed. - Introduced tests for configuration and credential management. - Enhanced the `identity` module to generate W3C compliant DID documents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2a867b32a8
commit
6fee6befed
@@ -3,8 +3,10 @@
|
||||
import type { Update, PatchOperation } from '../types/api'
|
||||
import { applyPatch, type Operation } from 'fast-json-patch'
|
||||
|
||||
export type ConnectionState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected'
|
||||
|
||||
type WebSocketCallback = (update: Update) => void
|
||||
type ConnectionStateCallback = (connected: boolean) => void
|
||||
type ConnectionStateCallback = (state: ConnectionState) => void
|
||||
|
||||
export class WebSocketClient {
|
||||
private ws: WebSocket | null = null
|
||||
@@ -13,14 +15,18 @@ export class WebSocketClient {
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 10
|
||||
private reconnectDelay = 1000
|
||||
private maxReconnectDelay = 30000
|
||||
private shouldReconnect = true
|
||||
private url: string
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private visibilityChangeHandler: (() => void) | null = null
|
||||
private onlineHandler: (() => void) | null = null
|
||||
private heartbeatTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private pingTimer: ReturnType<typeof setInterval> | null = null
|
||||
private lastMessageTime: number = Date.now()
|
||||
private heartbeatInterval = 10000 // Check connection every 10 seconds
|
||||
private pingInterval = 30000 // Send ping every 30 seconds
|
||||
private _state: ConnectionState = 'disconnected'
|
||||
|
||||
constructor(url: string = '/ws/db') {
|
||||
this.url = url
|
||||
@@ -33,13 +39,13 @@ export class WebSocketClient {
|
||||
// Handle page visibility changes (tab switching, browser minimizing)
|
||||
this.visibilityChangeHandler = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('[WebSocket] Page became visible, checking connection...')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Page became visible, checking connection...')
|
||||
// Only reconnect if we haven't been explicitly disconnected
|
||||
if (this.shouldReconnect && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) {
|
||||
console.log('[WebSocket] Connection lost while hidden, reconnecting...')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Connection lost while hidden, reconnecting...')
|
||||
this.reconnectAttempts = 0
|
||||
this.connect().catch(err => {
|
||||
console.error('[WebSocket] Failed to reconnect on visibility change:', err)
|
||||
if (import.meta.env.DEV) console.error('[WebSocket] Failed to reconnect on visibility change:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -50,11 +56,11 @@ export class WebSocketClient {
|
||||
this.onlineHandler = () => {
|
||||
// Only reconnect if we haven't been explicitly disconnected
|
||||
if (!this.shouldReconnect) return
|
||||
console.log('[WebSocket] Network came online, reconnecting...')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Network came online, reconnecting...')
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
this.reconnectAttempts = 0
|
||||
this.connect().catch(err => {
|
||||
console.error('[WebSocket] Failed to reconnect when network came online:', err)
|
||||
if (import.meta.env.DEV) console.error('[WebSocket] Failed to reconnect when network came online:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,14 +71,14 @@ export class WebSocketClient {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If already connected, resolve immediately
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
console.log('[WebSocket] Already connected, skipping')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Already connected, skipping')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// If connecting, wait for it
|
||||
if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
|
||||
console.log('[WebSocket] Already connecting, waiting...')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Already connecting, waiting...')
|
||||
const checkInterval = setInterval(() => {
|
||||
if (this.ws) {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
@@ -107,7 +113,7 @@ export class WebSocketClient {
|
||||
|
||||
// If we have an active WebSocket, don't create a new one
|
||||
if (this.ws) {
|
||||
console.log('[WebSocket] Connection exists, reusing it')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Connection exists, reusing it')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
@@ -124,14 +130,15 @@ export class WebSocketClient {
|
||||
const host = window.location.host
|
||||
const wsUrl = `${protocol}//${host}${this.url}`
|
||||
|
||||
console.log('[WebSocket] Connecting to:', wsUrl)
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Connecting to:', wsUrl)
|
||||
|
||||
this.setConnectionState('connecting')
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
// Timeout handler in case connection hangs
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
|
||||
console.warn('WebSocket connection timeout, retrying...')
|
||||
if (import.meta.env.DEV) console.warn('WebSocket connection timeout, retrying...')
|
||||
this.ws.close()
|
||||
reject(new Error('Connection timeout'))
|
||||
}
|
||||
@@ -141,15 +148,15 @@ export class WebSocketClient {
|
||||
clearTimeout(connectionTimeout)
|
||||
this.reconnectAttempts = 0
|
||||
this.lastMessageTime = Date.now()
|
||||
console.log('[WebSocket] Connected successfully')
|
||||
this.notifyConnectionState(true)
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Connected successfully')
|
||||
this.setConnectionState('connected')
|
||||
this.startHeartbeat()
|
||||
resolve()
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
clearTimeout(connectionTimeout)
|
||||
console.error('[WebSocket] Connection error:', error)
|
||||
if (import.meta.env.DEV) console.error('[WebSocket] Connection error:', error)
|
||||
// Don't reject immediately - let onclose handle reconnection
|
||||
// This prevents errors from blocking reconnection
|
||||
}
|
||||
@@ -160,24 +167,24 @@ export class WebSocketClient {
|
||||
const update: Update = JSON.parse(event.data)
|
||||
this.callbacks.forEach((callback) => callback(update))
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error)
|
||||
if (import.meta.env.DEV) console.error('Failed to parse WebSocket message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
clearTimeout(connectionTimeout)
|
||||
this.stopHeartbeat()
|
||||
console.log('[WebSocket] Closed', { code: event.code, reason: event.reason, wasClean: event.wasClean })
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Closed', { code: event.code, reason: event.reason, wasClean: event.wasClean })
|
||||
|
||||
// Notify connection state changed
|
||||
this.notifyConnectionState(false)
|
||||
this.setConnectionState('disconnected')
|
||||
|
||||
// Clear the WebSocket reference
|
||||
this.ws = null
|
||||
|
||||
// Don't reconnect if we explicitly disconnected
|
||||
if (!this.shouldReconnect) {
|
||||
console.log('[WebSocket] Reconnection disabled')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Reconnection disabled')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,11 +197,11 @@ export class WebSocketClient {
|
||||
// Immediate reconnection for HMR, service restarts, and first attempt after abnormal closure
|
||||
const needsImmediateReconnect = isHMR || isServiceRestart || (event.code === 1006 && this.reconnectAttempts === 0)
|
||||
|
||||
const delay = needsImmediateReconnect ? 0 :
|
||||
(this.reconnectAttempts === 0 ? 100 :
|
||||
Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 5000))
|
||||
const delay = needsImmediateReconnect ? 0 :
|
||||
(this.reconnectAttempts === 0 ? 100 :
|
||||
Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay))
|
||||
|
||||
console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts}, code: ${event.code})`)
|
||||
if (import.meta.env.DEV) console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts}, code: ${event.code})`)
|
||||
|
||||
// Clear any existing reconnect timer
|
||||
if (this.reconnectTimer) {
|
||||
@@ -213,9 +220,9 @@ export class WebSocketClient {
|
||||
this.reconnectAttempts++
|
||||
}
|
||||
|
||||
console.log('[WebSocket] Attempting reconnection...')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Attempting reconnection...')
|
||||
this.connect().catch((err) => {
|
||||
console.error('[WebSocket] Reconnection failed:', err)
|
||||
if (import.meta.env.DEV) console.error('[WebSocket] Reconnection failed:', err)
|
||||
// onclose will be called again and will retry
|
||||
})
|
||||
}
|
||||
@@ -230,7 +237,7 @@ export class WebSocketClient {
|
||||
}, delay)
|
||||
}
|
||||
} else {
|
||||
console.warn('[WebSocket] Max reconnection attempts reached')
|
||||
if (import.meta.env.DEV) console.warn('[WebSocket] Max reconnection attempts reached')
|
||||
this.shouldReconnect = false
|
||||
}
|
||||
}
|
||||
@@ -244,6 +251,10 @@ export class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
get state(): ConnectionState {
|
||||
return this._state
|
||||
}
|
||||
|
||||
onConnectionStateChange(callback: ConnectionStateCallback): () => void {
|
||||
this.connectionStateCallbacks.add(callback)
|
||||
return () => {
|
||||
@@ -251,26 +262,38 @@ export class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
private notifyConnectionState(connected: boolean): void {
|
||||
this.connectionStateCallbacks.forEach((callback) => callback(connected))
|
||||
private setConnectionState(state: ConnectionState): void {
|
||||
this._state = state
|
||||
this.connectionStateCallbacks.forEach((callback) => callback(state))
|
||||
}
|
||||
|
||||
private startHeartbeat(): void {
|
||||
this.stopHeartbeat()
|
||||
|
||||
|
||||
// Send ping messages every 30s
|
||||
this.pingTimer = setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'ping' }))
|
||||
} catch {
|
||||
// Send failed, connection likely broken
|
||||
}
|
||||
}
|
||||
}, this.pingInterval)
|
||||
|
||||
// Check connection health every 10s
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[WebSocket] Heartbeat detected closed connection')
|
||||
if (import.meta.env.DEV) console.warn('[WebSocket] Heartbeat detected closed connection')
|
||||
this.stopHeartbeat()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we've received a message recently
|
||||
|
||||
const timeSinceLastMessage = Date.now() - this.lastMessageTime
|
||||
|
||||
// If no message for more than 5 minutes, assume connection is stale
|
||||
if (timeSinceLastMessage > 300000) {
|
||||
console.warn('[WebSocket] No messages for 5m, reconnecting...')
|
||||
if (import.meta.env.DEV) console.warn('[WebSocket] No messages for 5m, reconnecting...')
|
||||
this.ws.close()
|
||||
return
|
||||
}
|
||||
@@ -282,11 +305,16 @@ export class WebSocketClient {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
if (this.pingTimer) {
|
||||
clearInterval(this.pingTimer)
|
||||
this.pingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.shouldReconnect = false
|
||||
this.reconnectAttempts = 0
|
||||
this.setConnectionState('disconnecting')
|
||||
this.stopHeartbeat()
|
||||
|
||||
// Clear reconnect timer
|
||||
@@ -345,7 +373,7 @@ function getWebSocketClient(): WebSocketClient {
|
||||
if (existing && existing instanceof WebSocketClient) {
|
||||
// Check if the WebSocket is still valid
|
||||
if (existing.isConnected()) {
|
||||
console.log('[WebSocket] Using existing connected client from HMR')
|
||||
if (import.meta.env.DEV) console.log('[WebSocket] Using existing connected client from HMR')
|
||||
wsClientInstance = existing
|
||||
return existing
|
||||
}
|
||||
@@ -374,7 +402,7 @@ export const wsClient: WebSocketClient = (() => {
|
||||
_wsClient = getWebSocketClient()
|
||||
return _wsClient
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] Error initializing client:', error)
|
||||
if (import.meta.env.DEV) console.error('[WebSocket] Error initializing client:', error)
|
||||
// Fallback to new instance
|
||||
_wsClient = new WebSocketClient()
|
||||
return _wsClient
|
||||
@@ -385,7 +413,7 @@ export const wsClient: WebSocketClient = (() => {
|
||||
export function applyDataPatch<T>(data: T, patch: PatchOperation[]): T {
|
||||
// Validate patch is an array before applying
|
||||
if (!Array.isArray(patch) || patch.length === 0) {
|
||||
console.warn('Invalid or empty patch received, returning original data')
|
||||
if (import.meta.env.DEV) console.warn('Invalid or empty patch received, returning original data')
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -393,7 +421,7 @@ export function applyDataPatch<T>(data: T, patch: PatchOperation[]): T {
|
||||
const result = applyPatch(data, patch as Operation[], false, false)
|
||||
return result.newDocument as T
|
||||
} catch (error) {
|
||||
console.error('Failed to apply patch:', error, 'Patch:', patch)
|
||||
if (import.meta.env.DEV) console.error('Failed to apply patch:', error, 'Patch:', patch)
|
||||
return data // Return original data on error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user