You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -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']
|
||||
|
||||
|
||||
106
packages/api-client/src/core/abstract-websocket.ts
Normal file
106
packages/api-client/src/core/abstract-websocket.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user