@ymnuk/chunker-upload-client (0.0.11)
Installation
@ymnuk:registry=npm install @ymnuk/chunker-upload-client@0.0.11"@ymnuk/chunker-upload-client": "0.0.11"About this package
@ymnuk/chunker-upload-client
Клиентская библиотека для чанковой загрузки файлов с адаптацией к сети.
Установка
npm install @ymnuk/chunker-upload-client
Реестр — Forgejo:
# .npmrc
@ymnuk:registry=https://git.ymnuktech.ru/api/packages/ymnuk/npm/
Быстрый старт
import { UploadManager, HttpTransport } from '@ymnuk/chunker-upload-client'
const transport = new HttpTransport({
baseUrl: 'https://fs.example.com/api/uploads',
})
const manager = new UploadManager(transport, {
initialChunkSize: 256 * 1024, // 256 КБ
maxConcurrency: 5,
})
const file = fileInput.files[0]!
manager.upload({
uploadId: crypto.randomUUID(),
url: '/uploads',
file,
})
.then(result => {
console.log('Готово:', result.hash)
})
.catch(err => {
console.error('Ошибка:', err)
})
Колбеки для UI
Подключите колбеки на уровне менеджера, чтобы показывать прогресс, список загрузок и ошибки пользователю:
manager.setCallbacks({
onUploadStart(uploadId, fileName, fileSize) {
addUpload({ id: uploadId, name: fileName, size: fileSize })
},
onUploadProgress(uploadId, percent, loaded, total) {
updateProgress(uploadId, percent, loaded, total)
},
onChunkComplete({ uploadId, chunkId, offset, size, attempt }) {
console.log(`Чанк ${chunkId} (${size} байт) — попытка ${attempt}`)
},
onChunkRetry({ uploadId, chunkId, offset, size, attempt, error }) {
console.warn(`Ретрай чанка ${chunkId}: ${error.message}`)
},
onUploadComplete(uploadId, success, hash) {
if (success) {
console.log(`Файл загружен: ${hash}`)
}
},
onUploadError(uploadId, error) {
showToast(`Ошибка загрузки: ${error.message}`)
},
})
Колбеки получают uploadId — через него можно отслеживать прогресс
по каждому файлу отдельно:
const uploads = new Map<string, { name: string; loaded: number; total: number }>()
manager.setCallbacks({
onUploadStart(uploadId, fileName, fileSize) {
uploads.set(uploadId, { name: fileName, loaded: 0, total: fileSize })
},
onUploadProgress(uploadId, percent, loaded, total) {
const entry = uploads.get(uploadId)
if (entry) entry.loaded = loaded
},
onUploadComplete(uploadId, success, hash) {
uploads.delete(uploadId)
},
onUploadError(uploadId, error) {
uploads.delete(uploadId)
},
})
Можно комбинировать с per-upload onProgress из UploadOptions:
manager.upload({
uploadId: '...',
url: '/uploads',
file,
onProgress(percent, loaded, total) {
uploadCard.progress = percent
},
})
Как это работает
- Init — клиент получает uploadId от сервера.
- Resume — сначала запрашивается
/status, чтобы пропустить уже загруженные чанки. - Upload — чанки отправляются параллельно с контролем конкарренси.
- Adapt — каждые 5 чанков ансамбль алгоритмов анализирует RTT/loss/ошибки и корректирует размер чанка и параллельность.
- Split — при 413/507 чанк делится на 4 равные части рекурсивно.
discoveredMaxSizeуменьшается, последующие чанки рождаются меньшего размера. - Finish — после всех чанков вычисляется SHA-256 файла и отправляется на сервер.
Важно: retry, resume и адаптация размера чанка — в
UploadManager.
HttpTransport— это тонкая прослойка надfetch()без логики восстановления.
Если вызыватьHttpTransport.uploadChunk()напрямую (минуяUploadManager),
ошибочный чанк не будет переотправлен автоматически.
Диапазон чанков
Размер чанка автоматически подбирается от 32 КБ до 4 МБ с шагом 32 КБ (128 значений). Алгоритмы поднимают размер на хорошей сети и снижают на плохой или при ошибках.
Обработка ошибок
| HTTP-статус | Действие |
|---|---|
| 200 | чанк принят |
| 409 | чанк уже есть (дубль) |
| 422 | CRC32 не совпал — PermanentError (без retry) |
| 413/507 | Split на 4 части рекурсивно. discoveredMaxSize уменьшается, следующие чанки рождаются меньшего размера. |
| 429 | конкарренси снижается на 2 |
| 502/503 | конкарренси снижается на 1 |
| таймаут/сеть | retry до 5 раз с exponential backoff |
Пример с Angular
import { HttpClient } from '@angular/common/http'
import { HttpTransport, UploadManager, type ChunkData, type ChunkResult } from '@ymnuk/chunker-upload-client'
class AngularTransport implements Transport {
constructor(private http: HttpClient, private baseUrl: string) {}
async uploadChunk(baseUrl: string, uploadId: string, chunk: ChunkData, signal?: AbortSignal): Promise<ChunkResult> {
return firstValueFrom(this.http.put<ChunkResult>(
`${baseUrl}/upload/${uploadId}`,
chunk.data,
{
headers: {
'X-Chunk-Offset': String(chunk.offset),
'X-Chunk-Size': String(chunk.size),
'X-Chunk-Crc32': chunk.crc32,
'X-Chunk-Id': chunk.chunkId,
},
reportProgress: false,
}
))
}
// finishUpload, getStatus — аналогично
}
// В компоненте:
@Component({ ... })
export class UploadComponent {
private manager = new UploadManager(new AngularTransport(this.http, '/api/uploads'))
upload(file: File) {
this.manager.upload({ uploadId: crypto.randomUUID(), url: '/uploads', file })
}
}
API
UploadManager
Один экземпляр может загружать сколько угодно файлов. Статистика сети
накапливается между загрузками — ансамбль быстрее адаптируется.
При необходимости состояние сбрасывается через reset().
class UploadManager {
constructor(transport: Transport, options?: UploadManagerOptions)
upload(options: UploadOptions): Promise<UploadResult>
setCallbacks(callbacks: UploadCallbacks): void
reset(): void // сброс монитора сети, ансамбля, счётчика
}
UploadOptions
| Поле | Тип | Описание |
|---|---|---|
uploadId |
string |
ID сессии (с сервера) |
url |
string |
Базовый URL (/uploads) |
file |
File |
Файл для загрузки |
signal? |
AbortSignal |
Отмена загрузки |
onProgress? |
(percent, loaded, total) => void |
Прогресс текущей загрузки |
minChunkSize? |
number |
Минимальный размер чанка (32 КБ) |
maxChunkSize? |
number |
Максимальный размер чанка (4 МБ) |
maxConcurrency? |
number |
Максимум параллельных чанков (5) |
Transport
Интерфейс для реализации своего транспорта:
interface Transport {
uploadChunk(baseUrl: string, uploadId: string, chunk: ChunkData, signal?: AbortSignal): Promise<ChunkResult>
finishUpload(baseUrl: string, uploadId: string, hash: string, signal?: AbortSignal): Promise<FinishResult>
getStatus(baseUrl: string, uploadId: string, signal?: AbortSignal): Promise<UploadStatus>
}
Прямой вызов Transport (без UploadManager)
Если по какой-то причине UploadManager не подходит, retry придётся делать вручную:
import { HttpTransport, type ChunkData } from '@ymnuk/chunker-upload-client'
const transport = new HttpTransport({ baseUrl: 'https://fs.example.com' })
async function uploadWithRetry(basePath: string, uploadId: string, chunk: ChunkData): Promise<void> {
const maxAttempts = 5
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const result = await transport.uploadChunk(basePath, uploadId, chunk)
if (result.status === 200) return
if (result.status === 409) return // дубль — не ошибка
} catch {
// сетевая ошибка — продолжаем
}
if (attempt < maxAttempts - 1) {
const delay = Math.min(1000 * 2 ** attempt, 30000) + Math.random() * 1000
await new Promise(r => setTimeout(r, delay))
}
}
throw new Error(`chunk ${chunk.chunkId} failed after ${maxAttempts} attempts`)
}
Разработка
npm run test # vitest run
npm run test:watch # vitest
npm run lint # tsc --noEmit
npm run build # tsup — ESM + CJS + d.ts
Тесты находятся рядом с исходниками: src/**/*.test.ts.
Dependencies
Development dependencies
| ID | Version |
|---|---|
| @angular/common | ^22.0.3 |
| @types/node | ^26.0.1 |
| @vitest/coverage-v8 | ^2.0.0 |
| rxjs | ^7.8.0 |
| tsup | ^8.0.0 |
| typescript | ^5.5.0 |
| vitest | ^2.0.0 |
Peer dependencies
| ID | Version |
|---|---|
| @angular/common | >=18.0.0 |
| rxjs | ^7.0.0 |