You've already forked AstralRinth
forked from didirus/AstralRinth
feat: modrinth hosting - files tab refactor (#4912)
* feat: api-client module for content v0 * feat: delete unused components + modules + setting * feat: xhr uploading * feat: fs module -> api-client * feat: migrate files.vue to use tanstack * fix: mem leak + other issues * fix: build * feat: switch to monaco * fix: go back to using ace, but improve preloading + theme * fix: styling + dead attrs * feat: match figma * fix: padding * feat: files-new for ui page structure * feat: finalize files.vue * fix: lint * fix: qa * fix: dep * fix: lint * fix: lockfile merge * feat: icons on navtab * fix: surface alternating on table * fix: hover surface color --------- Signed-off-by: Calum H. <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
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'
|
||||
import { XHRUploadClient } from './xhr-upload-client'
|
||||
|
||||
/**
|
||||
* Generic platform client using ofetch
|
||||
@@ -24,7 +24,7 @@ import { GenericWebSocketClient } from './websocket-generic'
|
||||
* const project = await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
* ```
|
||||
*/
|
||||
export class GenericModrinthClient extends AbstractModrinthClient {
|
||||
export class GenericModrinthClient extends XHRUploadClient {
|
||||
constructor(config: ClientConfig) {
|
||||
super(config)
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { FetchError } from 'ofetch'
|
||||
|
||||
import { AbstractModrinthClient } from '../core/abstract-client'
|
||||
import type { ModrinthApiError } from '../core/errors'
|
||||
import { 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 type { UploadHandle, UploadRequestOptions } from '../types/upload'
|
||||
import { GenericWebSocketClient } from './websocket-generic'
|
||||
import { XHRUploadClient } from './xhr-upload-client'
|
||||
|
||||
/**
|
||||
* Circuit breaker storage using Nuxt's useState
|
||||
@@ -53,6 +54,8 @@ export interface NuxtClientConfig extends ClientConfig {
|
||||
*
|
||||
* This client is optimized for Nuxt applications and handles SSR/CSR automatically.
|
||||
*
|
||||
* Note: upload() is only available in browser context (CSR). It will throw during SSR.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In a Nuxt composable
|
||||
@@ -70,7 +73,7 @@ export interface NuxtClientConfig extends ClientConfig {
|
||||
* const project = await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
* ```
|
||||
*/
|
||||
export class NuxtModrinthClient extends AbstractModrinthClient {
|
||||
export class NuxtModrinthClient extends XHRUploadClient {
|
||||
declare protected config: NuxtClientConfig
|
||||
|
||||
constructor(config: NuxtClientConfig) {
|
||||
@@ -84,6 +87,20 @@ export class NuxtModrinthClient extends AbstractModrinthClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file with progress tracking
|
||||
*
|
||||
* Note: This method is only available in browser context (CSR).
|
||||
* Calling during SSR will throw an error.
|
||||
*/
|
||||
upload<T = void>(path: string, options: UploadRequestOptions): UploadHandle<T> {
|
||||
// @ts-expect-error - import.meta is provided by Nuxt
|
||||
if (import.meta.server) {
|
||||
throw new ModrinthApiError('upload() is not supported during SSR')
|
||||
}
|
||||
return super.upload(path, options)
|
||||
}
|
||||
|
||||
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
|
||||
try {
|
||||
// @ts-expect-error - $fetch is provided by Nuxt runtime
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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'
|
||||
import { XHRUploadClient } from './xhr-upload-client'
|
||||
|
||||
/**
|
||||
* Tauri-specific configuration
|
||||
@@ -20,7 +20,9 @@ interface HttpError extends Error {
|
||||
|
||||
/**
|
||||
* Tauri platform client using Tauri v2 HTTP plugin
|
||||
|
||||
*
|
||||
* Extends XHRUploadClient to provide upload with progress tracking.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getVersion } from '@tauri-apps/api/app'
|
||||
@@ -36,7 +38,7 @@ interface HttpError extends Error {
|
||||
* const project = await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
* ```
|
||||
*/
|
||||
export class TauriModrinthClient extends AbstractModrinthClient {
|
||||
export class TauriModrinthClient extends XHRUploadClient {
|
||||
declare protected config: TauriClientConfig
|
||||
|
||||
constructor(config: TauriClientConfig) {
|
||||
|
||||
142
packages/api-client/src/platform/xhr-upload-client.ts
Normal file
142
packages/api-client/src/platform/xhr-upload-client.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { AbstractModrinthClient } from '../core/abstract-client'
|
||||
import { ModrinthApiError } from '../core/errors'
|
||||
import type { RequestContext } from '../types/request'
|
||||
import type {
|
||||
UploadHandle,
|
||||
UploadMetadata,
|
||||
UploadProgress,
|
||||
UploadRequestOptions,
|
||||
} from '../types/upload'
|
||||
|
||||
/**
|
||||
* Abstract client with XHR-based upload implementation
|
||||
*
|
||||
* Platform-specific clients should extend this instead of AbstractModrinthClient
|
||||
* to inherit the XHR upload implementation.
|
||||
*/
|
||||
export abstract class XHRUploadClient extends AbstractModrinthClient {
|
||||
upload<T = void>(path: string, options: UploadRequestOptions): UploadHandle<T> {
|
||||
let baseUrl: string
|
||||
if (options.api === 'labrinth') {
|
||||
baseUrl = this.config.labrinthBaseUrl!
|
||||
} else if (options.api === 'archon') {
|
||||
baseUrl = this.config.archonBaseUrl!
|
||||
} else {
|
||||
baseUrl = options.api
|
||||
}
|
||||
|
||||
const url = this.buildUrl(path, baseUrl, options.version)
|
||||
|
||||
const mergedOptions: UploadRequestOptions = {
|
||||
retry: false, // default: don't retry uploads
|
||||
...options,
|
||||
headers: {
|
||||
...this.buildDefaultHeaders(),
|
||||
'Content-Type': 'application/octet-stream',
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
|
||||
const context = this.buildUploadContext(url, path, mergedOptions)
|
||||
|
||||
const progressCallbacks: Array<(p: UploadProgress) => void> = []
|
||||
if (mergedOptions.onProgress) {
|
||||
progressCallbacks.push(mergedOptions.onProgress)
|
||||
}
|
||||
const abortController = new AbortController()
|
||||
|
||||
if (mergedOptions.signal) {
|
||||
mergedOptions.signal.addEventListener('abort', () => abortController.abort())
|
||||
}
|
||||
|
||||
const handle: UploadHandle<T> = {
|
||||
promise: this.executeUploadFeatureChain<T>(context, progressCallbacks, abortController)
|
||||
.then(async (result) => {
|
||||
await this.config.hooks?.onResponse?.(result, context)
|
||||
return result
|
||||
})
|
||||
.catch(async (error) => {
|
||||
const apiError = this.normalizeError(error, context)
|
||||
await this.config.hooks?.onError?.(apiError, context)
|
||||
throw apiError
|
||||
}),
|
||||
onProgress: (callback) => {
|
||||
progressCallbacks.push(callback)
|
||||
return handle
|
||||
},
|
||||
cancel: () => abortController.abort(),
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
protected executeXHRUpload<T>(
|
||||
context: RequestContext,
|
||||
progressCallbacks: Array<(p: UploadProgress) => void>,
|
||||
abortController: AbortController,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const metadata = context.metadata as UploadMetadata
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const progress: UploadProgress = {
|
||||
loaded: e.loaded,
|
||||
total: e.total,
|
||||
progress: e.loaded / e.total,
|
||||
}
|
||||
progressCallbacks.forEach((cb) => cb(progress))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
resolve(xhr.response ? JSON.parse(xhr.response) : (undefined as T))
|
||||
} catch {
|
||||
resolve(undefined as T)
|
||||
}
|
||||
} else {
|
||||
reject(this.createUploadError(xhr))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.addEventListener('error', () => reject(new ModrinthApiError('Upload failed')))
|
||||
xhr.addEventListener('abort', () => reject(new ModrinthApiError('Upload cancelled')))
|
||||
|
||||
// build URL with params (unlike $fetch, XHR doesn't handle params automatically)
|
||||
let url = context.url
|
||||
if (context.options.params) {
|
||||
const queryString = new URLSearchParams(
|
||||
Object.entries(context.options.params).map(([k, v]) => [k, String(v)]),
|
||||
).toString()
|
||||
url += (url.includes('?') ? '&' : '?') + queryString
|
||||
}
|
||||
|
||||
xhr.open('POST', url)
|
||||
|
||||
// apply headers from context (features may have modified them)
|
||||
for (const [key, value] of Object.entries(context.options.headers ?? {})) {
|
||||
xhr.setRequestHeader(key, value)
|
||||
}
|
||||
|
||||
xhr.send(metadata.file)
|
||||
abortController.signal.addEventListener('abort', () => xhr.abort())
|
||||
})
|
||||
}
|
||||
|
||||
protected createUploadError(xhr: XMLHttpRequest): ModrinthApiError {
|
||||
let responseData: unknown
|
||||
try {
|
||||
responseData = xhr.response ? JSON.parse(xhr.response) : undefined
|
||||
} catch {
|
||||
responseData = xhr.responseText
|
||||
}
|
||||
return this.createNormalizedError(
|
||||
new Error(`Upload failed with status ${xhr.status}`),
|
||||
xhr.status,
|
||||
responseData,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user