feat: ws client & new backups frontend (#4813)

* feat: ws client

* feat: v1 backups endpoints

* feat: migrate backups page to api-client and new DI ctx

* feat: switch to ws client via api-client

* fix: disgust

* fix: stats

* fix: console

* feat: v0 backups api

* feat: migrate backups.vue to page system w/ components to ui pkgs

* feat: polish backups frontend

* feat: pending refactor for ws handling of backups

* fix: vue shit

* fix: cancel logic fix

* fix: qa issues

* fix: alignment issues for backups page

* fix: bar positioning

* feat: finish QA

* fix: icons

* fix: lint & i18n

* fix: clear comment

* lint

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2025-12-04 02:32:03 +00:00
committed by GitHub
parent e3444a3456
commit 8eff939039
43 changed files with 2466 additions and 1177 deletions

View File

@@ -4,6 +4,7 @@ import type { ClientConfig } from '../types/client'
import type { RequestContext, RequestOptions } from '../types/request'
import type { AbstractFeature } from './abstract-feature'
import type { AbstractModule } from './abstract-module'
import type { AbstractWebSocketClient } from './abstract-websocket'
import { ModrinthApiError, ModrinthServerError } from './errors'
/**
@@ -24,7 +25,7 @@ export abstract class AbstractModrinthClient {
private _moduleNamespaces: Map<string, Record<string, AbstractModule>> = new Map()
public readonly labrinth!: InferredClientModules['labrinth']
public readonly archon!: InferredClientModules['archon']
public readonly archon!: InferredClientModules['archon'] & { sockets: AbstractWebSocketClient }
public readonly kyros!: InferredClientModules['kyros']
public readonly iso3166!: InferredClientModules['iso3166']

View File

@@ -0,0 +1,106 @@
import type mitt from 'mitt'
import type { Archon } from '../modules/archon/types'
export type WebSocketEventHandler<
E extends Archon.Websocket.v0.WSEvent = Archon.Websocket.v0.WSEvent,
> = (event: E) => void
export interface WebSocketConnection {
serverId: string
socket: WebSocket
reconnectAttempts: number
reconnectTimer?: ReturnType<typeof setTimeout>
isReconnecting: boolean
}
export interface WebSocketStatus {
connected: boolean
reconnecting: boolean
reconnectAttempts: number
}
type WSEventMap = {
[K in Archon.Websocket.v0.WSEvent as `${string}:${K['event']}`]: K
}
export abstract class AbstractWebSocketClient {
protected connections = new Map<string, WebSocketConnection>()
protected abstract emitter: ReturnType<typeof mitt<WSEventMap>>
protected readonly MAX_RECONNECT_ATTEMPTS = 10
protected readonly RECONNECT_BASE_DELAY = 1000
protected readonly RECONNECT_MAX_DELAY = 30000
constructor(
protected client: {
archon: {
servers_v0: {
getWebSocketAuth: (serverId: string) => Promise<Archon.Websocket.v0.WSAuth>
}
}
},
) {}
abstract connect(serverId: string, auth: Archon.Websocket.v0.WSAuth): Promise<void>
abstract disconnect(serverId: string): void
abstract disconnectAll(): void
abstract send(serverId: string, message: Archon.Websocket.v0.WSOutgoingMessage): void
async safeConnect(serverId: string, options?: { force?: boolean }): Promise<void> {
const status = this.getStatus(serverId)
if (status?.connected && !options?.force) {
return
}
if (status && !status.connected && !options?.force) {
return
}
if (options?.force && status) {
this.disconnect(serverId)
}
const auth = await this.client.archon.servers_v0.getWebSocketAuth(serverId)
await this.connect(serverId, auth)
}
on<E extends Archon.Websocket.v0.WSEventType>(
serverId: string,
eventType: E,
handler: WebSocketEventHandler<Extract<Archon.Websocket.v0.WSEvent, { event: E }>>,
): () => void {
const eventKey = `${serverId}:${eventType}` as keyof WSEventMap
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.emitter.on(eventKey, handler as any)
return () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.emitter.off(eventKey, handler as any)
}
}
getStatus(serverId: string): WebSocketStatus | null {
const connection = this.connections.get(serverId)
if (!connection) return null
return {
connected: connection.socket.readyState === WebSocket.OPEN,
reconnecting: connection.isReconnecting,
reconnectAttempts: connection.reconnectAttempts,
}
}
protected getReconnectDelay(attempt: number): number {
const delay = Math.min(
this.RECONNECT_BASE_DELAY * Math.pow(2, attempt),
this.RECONNECT_MAX_DELAY,
)
return delay + Math.random() * 1000
}
}

View File

@@ -1,5 +1,11 @@
export { AbstractModrinthClient } from './core/abstract-client'
export { AbstractFeature, type FeatureConfig } from './core/abstract-feature'
export {
AbstractWebSocketClient,
type WebSocketConnection,
type WebSocketEventHandler,
type WebSocketStatus,
} from './core/abstract-websocket'
export { ModrinthApiError, ModrinthServerError } from './core/errors'
export { type AuthConfig, AuthFeature } from './features/auth'
export {

View File

@@ -0,0 +1,95 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonBackupsV0Module extends AbstractModule {
public getModuleID(): string {
return 'archon_backups_v0'
}
/** GET /modrinth/v0/servers/:server_id/backups */
public async list(serverId: string): Promise<Archon.Backups.v1.Backup[]> {
return this.client.request<Archon.Backups.v1.Backup[]>(`/servers/${serverId}/backups`, {
api: 'archon',
version: 'modrinth/v0',
method: 'GET',
})
}
/** GET /modrinth/v0/servers/:server_id/backups/:backup_id */
public async get(serverId: string, backupId: string): Promise<Archon.Backups.v1.Backup> {
return this.client.request<Archon.Backups.v1.Backup>(
`/servers/${serverId}/backups/${backupId}`,
{ api: 'archon', version: 'modrinth/v0', method: 'GET' },
)
}
/** POST /modrinth/v0/servers/:server_id/backups */
public async create(
serverId: string,
request: Archon.Backups.v1.BackupRequest,
): Promise<Archon.Backups.v1.PostBackupResponse> {
return this.client.request<Archon.Backups.v1.PostBackupResponse>(
`/servers/${serverId}/backups`,
{ api: 'archon', version: 'modrinth/v0', method: 'POST', body: request },
)
}
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/restore */
public async restore(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/restore`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
})
}
/** DELETE /modrinth/v0/servers/:server_id/backups/:backup_id */
public async delete(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}`, {
api: 'archon',
version: 'modrinth/v0',
method: 'DELETE',
})
}
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/lock */
public async lock(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/lock`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
})
}
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/unlock */
public async unlock(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/unlock`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
})
}
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/retry */
public async retry(serverId: string, backupId: string): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/retry`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
})
}
/** PATCH /modrinth/v0/servers/:server_id/backups/:backup_id */
public async rename(
serverId: string,
backupId: string,
request: Archon.Backups.v1.PatchBackup,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}`, {
api: 'archon',
version: 'modrinth/v0',
method: 'PATCH',
body: request,
})
}
}

View File

@@ -0,0 +1,132 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
/**
* Default world ID - Uuid::nil() which the backend treats as "first/active world"
* See: apps/archon/src/routes/v1/servers/worlds/mod.rs - world_id_nullish()
* TODO:
* - Make sure world ID is being passed before we ship worlds.
* - The schema will change when Backups v4 (routes stay as v1) so remember to do that.
*/
const DEFAULT_WORLD_ID: string = '00000000-0000-0000-0000-000000000000' as const
export class ArchonBackupsV1Module extends AbstractModule {
public getModuleID(): string {
return 'archon_backups_v1'
}
/** GET /v1/:server_id/worlds/:world_id/backups */
public async list(
serverId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<Archon.Backups.v1.Backup[]> {
return this.client.request<Archon.Backups.v1.Backup[]>(
`/${serverId}/worlds/${worldId}/backups`,
{ api: 'archon', version: 1, method: 'GET' },
)
}
/** GET /v1/:server_id/worlds/:world_id/backups/:backup_id */
public async get(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<Archon.Backups.v1.Backup> {
return this.client.request<Archon.Backups.v1.Backup>(
`/${serverId}/worlds/${worldId}/backups/${backupId}`,
{ api: 'archon', version: 1, method: 'GET' },
)
}
/** POST /v1/:server_id/worlds/:world_id/backups */
public async create(
serverId: string,
request: Archon.Backups.v1.BackupRequest,
worldId: string = DEFAULT_WORLD_ID,
): Promise<Archon.Backups.v1.PostBackupResponse> {
return this.client.request<Archon.Backups.v1.PostBackupResponse>(
`/${serverId}/worlds/${worldId}/backups`,
{ api: 'archon', version: 1, method: 'POST', body: request },
)
}
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/restore */
public async restore(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/restore`, {
api: 'archon',
version: 1,
method: 'POST',
})
}
/** DELETE /v1/:server_id/worlds/:world_id/backups/:backup_id */
public async delete(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}`, {
api: 'archon',
version: 1,
method: 'DELETE',
})
}
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/lock */
public async lock(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/lock`, {
api: 'archon',
version: 1,
method: 'POST',
})
}
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/unlock */
public async unlock(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/unlock`, {
api: 'archon',
version: 1,
method: 'POST',
})
}
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/retry */
public async retry(
serverId: string,
backupId: string,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/retry`, {
api: 'archon',
version: 1,
method: 'POST',
})
}
/** PATCH /v1/:server_id/worlds/:world_id/backups/:backup_id */
public async rename(
serverId: string,
backupId: string,
request: Archon.Backups.v1.PatchBackup,
worldId: string = DEFAULT_WORLD_ID,
): Promise<void> {
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}`, {
api: 'archon',
version: 1,
method: 'PATCH',
body: request,
})
}
}

View File

@@ -1,3 +1,5 @@
export * from './servers/types'
export * from './backups/v0'
export * from './backups/v1'
export * from './servers/v0'
export * from './servers/v1'
export * from './types'

View File

@@ -6,6 +6,18 @@ export class ArchonServersV0Module extends AbstractModule {
return 'archon_servers_v0'
}
/**
* Get a specific server by ID
* GET /modrinth/v0/servers/:id
*/
public async get(serverId: string): Promise<Archon.Servers.v0.Server> {
return this.client.request<Archon.Servers.v0.Server>(`/servers/${serverId}`, {
api: 'archon',
method: 'GET',
version: 'modrinth/v0',
})
}
/**
* Get list of servers for the authenticated user
* GET /modrinth/v0/servers
@@ -54,4 +66,16 @@ export class ArchonServersV0Module extends AbstractModule {
method: 'GET',
})
}
/**
* Get WebSocket authentication credentials for a server
* GET /modrinth/v0/servers/:id/ws
*/
public async getWebSocketAuth(serverId: string): Promise<Archon.Websocket.v0.WSAuth> {
return this.client.request<Archon.Websocket.v0.WSAuth>(`/servers/${serverId}/ws`, {
api: 'archon',
version: 'modrinth/v0',
method: 'GET',
})
}
}

View File

@@ -125,4 +125,187 @@ export namespace Archon {
}
}
}
export namespace Backups {
export namespace v1 {
export type BackupState = 'ongoing' | 'done' | 'failed' | 'cancelled' | 'unchanged'
export type BackupTask = 'file' | 'create' | 'restore'
export type BackupTaskProgress = {
progress: number // 0.0 to 1.0
state: BackupState
}
export type Backup = {
id: string
name: string
created_at: string
locked: boolean
automated: boolean
interrupted: boolean
ongoing: boolean
task?: {
file?: BackupTaskProgress
create?: BackupTaskProgress
restore?: BackupTaskProgress
}
// TODO: Uncomment when API supports these fields
// size?: number // bytes
// creator_id?: string // user ID, or 'auto' for automated backups
}
export type BackupRequest = {
name: string
}
export type PatchBackup = {
name?: string
}
export type PostBackupResponse = {
id: string
}
}
}
export namespace Websocket {
export namespace v0 {
export type WSAuth = {
url: string
token: string
}
export type BackupState = 'ongoing' | 'done' | 'failed' | 'cancelled' | 'unchanged'
export type BackupTask = 'file' | 'create' | 'restore'
export type WSBackupProgressEvent = {
event: 'backup-progress'
id: string
task: BackupTask
state: BackupState
progress: number
}
export type WSLogEvent = {
event: 'log'
stream: 'stdout' | 'stderr'
message: string
}
export type WSStatsEvent = {
event: 'stats'
cpu_percent: number
ram_usage_bytes: number
ram_total_bytes: number
storage_usage_bytes: number
storage_total_bytes: number
net_tx_bytes: number
net_rx_bytes: number
}
export type PowerState = 'running' | 'stopped' | 'starting' | 'stopping' | 'crashed'
export type WSPowerStateEvent = {
event: 'power-state'
state: PowerState
oom_killed?: boolean
exit_code?: number
}
export type WSAuthExpiringEvent = {
event: 'auth-expiring'
}
export type WSAuthIncorrectEvent = {
event: 'auth-incorrect'
}
export type WSAuthOkEvent = {
event: 'auth-ok'
}
export type WSInstallationResultEvent =
| WSInstallationResultOkEvent
| WSInstallationResultErrEvent
export type WSInstallationResultOkEvent = {
event: 'installation-result'
result: 'ok'
}
export type WSInstallationResultErrEvent = {
event: 'installation-result'
result: 'err'
reason?: string
}
export type WSUptimeEvent = {
event: 'uptime'
uptime: number
}
export type WSNewModEvent = {
event: 'new-mod'
project_id: string
version_id: string
}
export type FilesystemOpKind = 'unarchive'
export type FilesystemOpState =
| 'queued'
| 'ongoing'
| 'done'
| 'cancelled'
| 'failure-corrupted'
| 'failure-invalid-path'
export type FilesystemOperation = {
op: FilesystemOpKind
id: string
progress: number
bytes_processed: number
files_processed: number
state: FilesystemOpState
mime: string
current_file?: string
invalid_path?: string
src: string
started: string
}
export type WSFilesystemOpsEvent = {
event: 'filesystem-ops'
all: FilesystemOperation[]
}
// Outgoing messages (client -> server)
export type WSOutgoingMessage = WSAuthMessage | WSCommandMessage
export type WSAuthMessage = {
event: 'auth'
jwt: string
}
export type WSCommandMessage = {
event: 'command'
cmd: string
}
export type WSEvent =
| WSBackupProgressEvent
| WSLogEvent
| WSStatsEvent
| WSPowerStateEvent
| WSAuthExpiringEvent
| WSAuthIncorrectEvent
| WSAuthOkEvent
| WSInstallationResultEvent
| WSUptimeEvent
| WSNewModEvent
| WSFilesystemOpsEvent
export type WSEventType = WSEvent['event']
}
}
}

View File

@@ -1,5 +1,7 @@
import type { AbstractModrinthClient } from '../core/abstract-client'
import type { AbstractModule } from '../core/abstract-module'
import { ArchonBackupsV0Module } from './archon/backups/v0'
import { ArchonBackupsV1Module } from './archon/backups/v1'
import { ArchonServersV0Module } from './archon/servers/v0'
import { ArchonServersV1Module } from './archon/servers/v1'
import { ISO3166Module } from './iso3166'
@@ -21,6 +23,8 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
* TODO: Better way? Probably not
*/
export const MODULE_REGISTRY = {
archon_backups_v0: ArchonBackupsV0Module,
archon_backups_v1: ArchonBackupsV1Module,
archon_servers_v0: ArchonServersV0Module,
archon_servers_v1: ArchonServersV1Module,
iso3166_data: ISO3166Module,

View File

@@ -2,7 +2,9 @@ import { $fetch, FetchError } from 'ofetch'
import { AbstractModrinthClient } from '../core/abstract-client'
import type { ModrinthApiError } from '../core/errors'
import type { ClientConfig } from '../types/client'
import type { RequestOptions } from '../types/request'
import { GenericWebSocketClient } from './websocket-generic'
/**
* Generic platform client using ofetch
@@ -23,6 +25,17 @@ import type { RequestOptions } from '../types/request'
* ```
*/
export class GenericModrinthClient extends AbstractModrinthClient {
constructor(config: ClientConfig) {
super(config)
Object.defineProperty(this.archon, 'sockets', {
value: new GenericWebSocketClient(this),
writable: false,
enumerable: true,
configurable: false,
})
}
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
try {
const response = await $fetch<T>(url, {

View File

@@ -5,6 +5,7 @@ import type { ModrinthApiError } from '../core/errors'
import type { CircuitBreakerState, CircuitBreakerStorage } from '../features/circuit-breaker'
import type { ClientConfig } from '../types/client'
import type { RequestOptions } from '../types/request'
import { GenericWebSocketClient } from './websocket-generic'
/**
* Circuit breaker storage using Nuxt's useState
@@ -72,6 +73,17 @@ export interface NuxtClientConfig extends ClientConfig {
export class NuxtModrinthClient extends AbstractModrinthClient {
protected declare config: NuxtClientConfig
constructor(config: NuxtClientConfig) {
super(config)
Object.defineProperty(this.archon, 'sockets', {
value: new GenericWebSocketClient(this),
writable: false,
enumerable: true,
configurable: false,
})
}
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
try {
// @ts-expect-error - $fetch is provided by Nuxt runtime

View File

@@ -2,6 +2,7 @@ import { AbstractModrinthClient } from '../core/abstract-client'
import type { ModrinthApiError } from '../core/errors'
import type { ClientConfig } from '../types/client'
import type { RequestOptions } from '../types/request'
import { GenericWebSocketClient } from './websocket-generic'
/**
* Tauri-specific configuration
@@ -38,6 +39,17 @@ interface HttpError extends Error {
export class TauriModrinthClient extends AbstractModrinthClient {
protected declare config: TauriClientConfig
constructor(config: TauriClientConfig) {
super(config)
Object.defineProperty(this.archon, 'sockets', {
value: new GenericWebSocketClient(this),
writable: false,
enumerable: true,
configurable: false,
})
}
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
try {
// Dynamically import Tauri HTTP plugin

View File

@@ -0,0 +1,147 @@
import mitt from 'mitt'
import { AbstractWebSocketClient, type WebSocketConnection } from '../core/abstract-websocket'
import type { Archon } from '../modules/archon/types'
type WSEventMap = {
[K in Archon.Websocket.v0.WSEvent as `${string}:${K['event']}`]: K
}
export class GenericWebSocketClient extends AbstractWebSocketClient {
protected emitter = mitt<WSEventMap>()
async connect(serverId: string, auth: Archon.Websocket.v0.WSAuth): Promise<void> {
if (this.connections.has(serverId)) {
this.disconnect(serverId)
}
return new Promise((resolve, reject) => {
try {
const ws = new WebSocket(`wss://${auth.url}`)
const connection: WebSocketConnection = {
serverId,
socket: ws,
reconnectAttempts: 0,
reconnectTimer: undefined,
isReconnecting: false,
}
this.connections.set(serverId, connection)
ws.onopen = () => {
ws.send(JSON.stringify({ event: 'auth', jwt: auth.token }))
connection.reconnectAttempts = 0
connection.isReconnecting = false
resolve()
}
ws.onmessage = (messageEvent) => {
try {
const data = JSON.parse(messageEvent.data) as Archon.Websocket.v0.WSEvent
const eventKey = `${serverId}:${data.event}` as keyof WSEventMap
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.emitter.emit(eventKey, data as any)
if (data.event === 'auth-expiring' || data.event === 'auth-incorrect') {
this.handleAuthExpiring(serverId).catch(console.error)
}
} catch (error) {
console.error('[WebSocket] Failed to parse message:', error)
}
}
ws.onclose = (event) => {
if (event.code !== 1000) {
this.scheduleReconnect(serverId, auth)
}
}
ws.onerror = (error) => {
console.error(`[WebSocket] Error for server ${serverId}:`, error)
reject(new Error(`WebSocket connection failed for server ${serverId}`))
}
} catch (error) {
reject(error)
}
})
}
disconnect(serverId: string): void {
const connection = this.connections.get(serverId)
if (!connection) return
if (connection.reconnectTimer) {
clearTimeout(connection.reconnectTimer)
connection.reconnectTimer = undefined
}
if (
connection.socket.readyState === WebSocket.OPEN ||
connection.socket.readyState === WebSocket.CONNECTING
) {
connection.socket.close(1000, 'Client disconnecting')
}
this.emitter.all.forEach((_handlers, type) => {
if (type.toString().startsWith(`${serverId}:`)) {
this.emitter.all.delete(type)
}
})
this.connections.delete(serverId)
}
disconnectAll(): void {
for (const serverId of this.connections.keys()) {
this.disconnect(serverId)
}
}
send(serverId: string, message: Archon.Websocket.v0.WSOutgoingMessage): void {
const connection = this.connections.get(serverId)
if (!connection || connection.socket.readyState !== WebSocket.OPEN) {
console.warn(`Cannot send message: WebSocket not connected for server ${serverId}`)
return
}
connection.socket.send(JSON.stringify(message))
}
private scheduleReconnect(serverId: string, auth: Archon.Websocket.v0.WSAuth): void {
const connection = this.connections.get(serverId)
if (!connection) return
if (connection.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
this.disconnect(serverId)
return
}
connection.isReconnecting = true
connection.reconnectAttempts++
const delay = this.getReconnectDelay(connection.reconnectAttempts)
connection.reconnectTimer = setTimeout(() => {
this.connect(serverId, auth).catch((error) => {
console.error(`[WebSocket] Reconnection failed for server ${serverId}:`, error)
})
}, delay)
}
private async handleAuthExpiring(serverId: string): Promise<void> {
try {
const newAuth = await this.client.archon.servers_v0.getWebSocketAuth(serverId)
const connection = this.connections.get(serverId)
if (connection && connection.socket.readyState === WebSocket.OPEN) {
connection.socket.send(JSON.stringify({ event: 'auth', jwt: newAuth.token }))
}
} catch (error) {
console.error(`[WebSocket] Failed to refresh auth for server ${serverId}:`, error)
this.disconnect(serverId)
}
}
}

View File

@@ -6,6 +6,7 @@ export type {
CircuitBreakerStorage,
} from '../features/circuit-breaker'
export type { BackoffStrategy, RetryConfig } from '../features/retry'
export type { Archon } from '../modules/archon/types'
export type { ClientConfig, RequestHooks } from './client'
export type { ApiErrorData, ModrinthErrorResponse } from './errors'
export { isModrinthErrorResponse } from './errors'