feat:移除了弹窗,服务器添加sls
This commit is contained in:
commit
6a295f9a7a
4082 files changed
+1322534
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import type { RequestOptions } from '../types/request'
|
||||
|
||||
export function appendRequestParams(url: string, params?: RequestOptions['params']): string {
|
||||
if (!params) return url
|
||||
|
||||
const filteredParams: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
filteredParams[key] = String(value)
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = new URLSearchParams(filteredParams).toString()
|
||||
if (!queryString) return url
|
||||
|
||||
return `${url}${url.includes('?') ? '&' : '?'}${queryString}`
|
||||
}
|
||||
|
||||
export function toFetchBody(body: unknown): BodyInit | null | undefined {
|
||||
if (!body) return undefined
|
||||
|
||||
if (
|
||||
typeof body === 'object' &&
|
||||
!(body instanceof FormData) &&
|
||||
!(body instanceof URLSearchParams) &&
|
||||
!(body instanceof Blob) &&
|
||||
!(body instanceof ArrayBuffer) &&
|
||||
!ArrayBuffer.isView(body as ArrayBufferView)
|
||||
) {
|
||||
return JSON.stringify(body)
|
||||
}
|
||||
|
||||
return body as BodyInit
|
||||
}
|
||||
|
||||
export async function parseResponseErrorData(response: Response): Promise<unknown> {
|
||||
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
|
||||
|
||||
try {
|
||||
if (contentType.includes('application/json') || contentType.includes('+json')) {
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (!text) return undefined
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ModrinthApiError } from '../core/errors'
|
||||
|
||||
/**
|
||||
* Wrap a function with JWT retry logic.
|
||||
* On 401, calls refreshToken() and retries once.
|
||||
*/
|
||||
export async function withJWTRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
refreshToken: () => Promise<void>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthApiError && error.statusCode === 401) {
|
||||
await refreshToken()
|
||||
return await fn()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const NODE_FS_PATH_REGEX = /\/modrinth\/v\d+\/fs\/?$/
|
||||
const HTTP_SCHEME_REGEX = /^https?:\/\//i
|
||||
const WS_SCHEME_REGEX = /^wss?:\/\//i
|
||||
const HTTP_SECURE_SCHEME_REGEX = /^https:\/\//i
|
||||
const HTTP_INSECURE_SCHEME_REGEX = /^http:\/\//i
|
||||
|
||||
export function getNodeBaseUrl(url: string): string {
|
||||
const baseUrl = url.replace(NODE_FS_PATH_REGEX, '')
|
||||
return HTTP_SCHEME_REGEX.test(baseUrl) ? baseUrl : `https://${baseUrl}`
|
||||
}
|
||||
|
||||
export function getNodeWebSocketUrl(url: string): string {
|
||||
if (WS_SCHEME_REGEX.test(url)) return url
|
||||
if (HTTP_SECURE_SCHEME_REGEX.test(url)) return url.replace(HTTP_SECURE_SCHEME_REGEX, 'wss://')
|
||||
if (HTTP_INSECURE_SCHEME_REGEX.test(url)) return url.replace(HTTP_INSECURE_SCHEME_REGEX, 'ws://')
|
||||
|
||||
return `wss://${url}`
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
export interface WebSocketPingOptions {
|
||||
count?: number
|
||||
intervalMs?: number
|
||||
settleDelayMs?: number
|
||||
timeoutMs?: number
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export async function pingWebSocketUrl(
|
||||
url: string,
|
||||
options: WebSocketPingOptions = {},
|
||||
): Promise<number> {
|
||||
const count = options.count ?? 5
|
||||
const intervalMs = options.intervalMs ?? 200
|
||||
const settleDelayMs = options.settleDelayMs ?? 1000
|
||||
const timeoutMs = options.timeoutMs ?? count * intervalMs + settleDelayMs + 1000
|
||||
|
||||
if (options.signal?.aborted) return -1
|
||||
|
||||
return await new Promise<number>((resolve) => {
|
||||
const samples: number[] = []
|
||||
const timers = new Set<ReturnType<typeof setTimeout>>()
|
||||
let socket: WebSocket | undefined
|
||||
let settled = false
|
||||
|
||||
const setTrackedTimeout = (callback: () => void, ms: number) => {
|
||||
const timer = setTimeout(() => {
|
||||
timers.delete(timer)
|
||||
callback()
|
||||
}, ms)
|
||||
timers.add(timer)
|
||||
return timer
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
for (const timer of timers) clearTimeout(timer)
|
||||
timers.clear()
|
||||
options.signal?.removeEventListener('abort', abort)
|
||||
|
||||
if (
|
||||
socket &&
|
||||
(socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.OPEN)
|
||||
) {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
const finish = (ping: number) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
resolve(ping)
|
||||
}
|
||||
|
||||
const abort = () => finish(-1)
|
||||
options.signal?.addEventListener('abort', abort, { once: true })
|
||||
|
||||
try {
|
||||
socket = new WebSocket(url)
|
||||
} catch {
|
||||
finish(-1)
|
||||
return
|
||||
}
|
||||
|
||||
setTrackedTimeout(() => finish(-1), timeoutMs)
|
||||
|
||||
socket.onopen = () => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
setTrackedTimeout(() => {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
socket.send(String(performance.now()))
|
||||
}
|
||||
}, i * intervalMs)
|
||||
}
|
||||
|
||||
setTrackedTimeout(
|
||||
() => {
|
||||
const ping =
|
||||
samples.length > 0
|
||||
? Math.round([...samples].sort((a, b) => a - b)[Math.floor(samples.length / 2)])
|
||||
: -1
|
||||
finish(ping)
|
||||
},
|
||||
count * intervalMs + settleDelayMs,
|
||||
)
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
samples.push(performance.now() - Number(event.data))
|
||||
}
|
||||
|
||||
socket.onerror = () => finish(-1)
|
||||
socket.onclose = () => {
|
||||
if (!settled && samples.length === 0) finish(-1)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Archon } from '../modules/archon/types'
|
||||
|
||||
export type ParsedSseEvent = {
|
||||
kind: 'event'
|
||||
id?: string
|
||||
event?: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export type ParsedSseRetry = {
|
||||
kind: 'retry'
|
||||
retry: number
|
||||
}
|
||||
|
||||
export type ParsedSseItem = ParsedSseEvent | ParsedSseRetry
|
||||
|
||||
export class SseParser {
|
||||
private buffer = ''
|
||||
private eventName = ''
|
||||
private data = ''
|
||||
private id: string | undefined
|
||||
|
||||
feed(chunk: string): ParsedSseItem[] {
|
||||
this.buffer += chunk
|
||||
const items: ParsedSseItem[] = []
|
||||
|
||||
while (true) {
|
||||
const lineEnd = this.findLineEnd()
|
||||
if (!lineEnd) break
|
||||
|
||||
const { line, length } = lineEnd
|
||||
this.buffer = this.buffer.slice(length)
|
||||
this.processLine(line, items)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
end(): ParsedSseItem[] {
|
||||
const items: ParsedSseItem[] = []
|
||||
|
||||
if (this.buffer.length > 0) {
|
||||
this.processLine(this.buffer.endsWith('\r') ? this.buffer.slice(0, -1) : this.buffer, items)
|
||||
this.buffer = ''
|
||||
}
|
||||
|
||||
this.dispatch(items)
|
||||
return items
|
||||
}
|
||||
|
||||
private findLineEnd(): { line: string; length: number } | null {
|
||||
const lf = this.buffer.indexOf('\n')
|
||||
const cr = this.buffer.indexOf('\r')
|
||||
|
||||
if (lf === -1 && cr === -1) return null
|
||||
|
||||
if (cr !== -1 && (lf === -1 || cr < lf)) {
|
||||
if (cr === this.buffer.length - 1) return null
|
||||
const length = this.buffer[cr + 1] === '\n' ? cr + 2 : cr + 1
|
||||
return {
|
||||
line: this.buffer.slice(0, cr),
|
||||
length,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
line: this.buffer.slice(0, lf),
|
||||
length: lf + 1,
|
||||
}
|
||||
}
|
||||
|
||||
private processLine(line: string, items: ParsedSseItem[]): void {
|
||||
if (line === '') {
|
||||
this.dispatch(items)
|
||||
return
|
||||
}
|
||||
|
||||
if (line.startsWith(':')) return
|
||||
|
||||
const colon = line.indexOf(':')
|
||||
const field = colon === -1 ? line : line.slice(0, colon)
|
||||
let value = colon === -1 ? '' : line.slice(colon + 1)
|
||||
if (value.startsWith(' ')) value = value.slice(1)
|
||||
|
||||
switch (field) {
|
||||
case 'event':
|
||||
this.eventName = value
|
||||
break
|
||||
case 'data':
|
||||
this.data += `${value}\n`
|
||||
break
|
||||
case 'id':
|
||||
this.id = value
|
||||
break
|
||||
case 'retry': {
|
||||
const retry = Number(value)
|
||||
if (Number.isInteger(retry) && retry >= 0) {
|
||||
items.push({ kind: 'retry', retry })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private dispatch(items: ParsedSseItem[]): void {
|
||||
if (!this.data) {
|
||||
this.eventName = ''
|
||||
this.id = undefined
|
||||
return
|
||||
}
|
||||
|
||||
items.push({
|
||||
kind: 'event',
|
||||
id: this.id,
|
||||
event: this.eventName || undefined,
|
||||
data: this.data.endsWith('\n') ? this.data.slice(0, -1) : this.data,
|
||||
})
|
||||
|
||||
this.eventName = ''
|
||||
this.data = ''
|
||||
this.id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSyncEventData(data: string): Archon.Sync.v1.SyncEvent | null {
|
||||
let parsed: unknown
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(data)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
const event = parsed as { type?: unknown }
|
||||
if (typeof event.type !== 'string') return null
|
||||
|
||||
return parsed as Archon.Sync.v1.SyncEvent
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type Override<T, R> = Omit<T, keyof R> & R
|
||||
export type RawDecimal = string | number
|
||||
Reference in new issue
Block a user