@encody/vue
v0.9.0
Published
Encody client SDK — resumable upload queue with Vue composables
Downloads
471
Readme
@encody/vue
TUS-based resumable upload queue with a Vue 3 composable. Handles concurrency, retries, SSE status updates, file validation, and error codes out of the box.
Install
npm install @encody/vueUsage
import { useEncody } from '@encody/vue'
const { add, clear, files, isActive } = useEncody({
token: () => getTokenFromBackend()
})All options accept a raw value or a Vue ref — the composable stays reactive either way.
Live demo and playground: demo.encody.io
Options
| Option | Default | Description |
|--------|---------|-------------|
| baseUrl | 'https://app.encody.io' | API origin. Override for on-prem or enterprise installs. |
| endpoint | {baseUrl}/media/upload | TUS upload endpoint. Derived from baseUrl if omitted. |
| sseUrl | {baseUrl}/api/upload-events | SSE endpoint for real-time status updates. Derived from baseUrl if omitted. |
| token | — | async () => string — called before each upload and on SSE reconnect. Must return a short-lived JWT. Never expose your API key to the browser — request the token from your own backend, which exchanges the API key for a JWT against the Encody API. |
| concurrency | 3 | Max parallel uploads. |
| retryDelays | [0, 3000, 10000, 30000] | Backoff delays in ms between TUS retries. Pass [] to disable. |
| maxSize | null | Max file size in bytes. null = unlimited. |
| allowedTypes | [] | Allowed MIME types. Supports wildcards (image/*). Empty = all types allowed. A project's allowed types (configured in the Encody dashboard) are inherited automatically and override this — see Allowed file types. |
Token flow
The token function is called by the SDK before every upload and on SSE reconnect. It must return a short-lived JWT issued by the Encody API.
Your API key must never leave your server. The recommended flow is:
Browser → POST /token → Your backend → POST /api/token → Encody API
(Authorization: Bearer ek_...)Minimal Node.js token endpoint (no dependencies):
// token-server.mjs
import { createServer } from 'node:http'
const {
PORT = 3000,
ENCODY_API_KEY,
ENCODY_BASE_URL = 'https://app.encody.io'
} = process.env
createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/token') {
res.writeHead(404).end()
return
}
const { token } = await fetch(`${ENCODY_BASE_URL}/api/token`, {
method: 'POST',
headers: { Authorization: `Bearer ${ENCODY_API_KEY}` }
}).then(r => r.json())
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ token }))
}).listen(PORT)ENCODY_API_KEY=ek_... node token-server.mjsThen pass it to useEncody:
const { add, files } = useEncody({
token: async () => {
const res = await fetch('/token', { method: 'POST' })
return (await res.json()).token
}
})Allowed file types
A project can define which MIME types it accepts in the Encody dashboard. The SDK inherits that list automatically — you don't have to re-declare it in the browser:
- It rides along in the upload-token JWT (refreshed ~every 15 min) and is pushed live over the SSE stream, so changing it in the dashboard updates connected clients without a reload.
- The effective list is exposed as a reactive
allowedTypesreturn value. - Precedence: a non-empty project list overrides the
allowedTypesyou pass touseEncody(...); if the project defines none, your initallowedTypesapplies; if that is also empty, all types are allowed.
Every add() is validated against the effective list — disallowed files are rejected with
ErrorCode.FILE_TYPE_NOT_ALLOWED (see Error codes).
Filtering a native <input type="file">
The effective list maps directly onto the input's accept attribute — MIME wildcards like
image/* are valid accept tokens, so a comma-joined string works as-is. This filters the
operating-system file dialog so users only see allowed types:
<script setup>
import { useEncody } from '@encody/vue'
import { computed } from 'vue'
const { add, allowedTypes } = useEncody({ token: getTokenFromBackend })
// undefined when the list is empty → attribute omitted → all types allowed
const accept = computed(() => allowedTypes.value.length ? allowedTypes.value.join(',') : undefined)
</script>
<template>
<input type="file" multiple :accept="accept" @change="e => add([...e.target.files])">
</template>allowedTypes is reactive, so the accept filter updates live when the project's types
change in the dashboard. Note that accept is a UX hint only — it doesn't enforce
anything; the SDK's add() validation (and the Encody server) still reject disallowed files.
Composable return value
const {
add, // add files to the queue
clear, // clear the queue
files, // ComputedRef<FileRecord[]> — reactive queue state
isActive, // ComputedRef<boolean> — true while any upload is in progress
allowedTypes, // Ref<string[]> — effective allowed MIME types (project override, else init), reactive
file, // (id) => { retry, cancel, pause, resume, updateMeta }
useFile, // (id | Ref<id>) => ComputedRef<FileRecord> — reactive single file
useBatch, // (batchId) => { files, progress, isComplete, hasFailed }
on, // (event, handler) => void
off, // (event, handler) => void
initSession // () => Promise<void> — establish /media/* browser session cookie
} = useEncody(options)add(input, options?)
// single file
add(file)
// multiple files — auto-grouped as a batch
add([file1, file2])
// with custom meta (stored to DB, accessible server-side)
add(files, { meta: { folderId: '123', tag: 'avatar' } })
// per-file meta
add([
{ file: fileA, meta: { tag: 'cover' } },
{ file: fileB, meta: { tag: 'thumb' } }
])file(id)
file(id).retry()
file(id).cancel()
file(id).pause()
file(id).resume()
file(id).updateMeta({ tag: 'updated' })Events
on('file:uploading', ({ fileId, filename, size, meta }) => {})
on('file:ready', ({ fileId, url, meta }) => {})
on('file:failed', ({ fileId, code, error, meta }) => {})
on('file:progress', ({ fileId, progress, meta }) => {})
on('batch:complete', ({ batchId, files }) => {})
on('batch:failed', ({ batchId, failed, succeeded }) => {})
on('sse:error', () => {})
on('allowedTypes:change', ({ allowedTypes }) => {}) // project's allowed types changed (JWT claim or live SSE)file(id).raw()
Returns the file as a File object. Accepts an optional options object to control the source:
| Option | Default | Description |
|--------|---------|-------------|
| (none) | auto | In-memory File if available in this session, fetches from API otherwise |
| { remote: true } | — | Always fetch from API — bypasses the in-memory cache |
| { local: true } | — | Only return in-memory File; returns null if not in queue |
// Auto — local if available, remote fallback
const f = await file(record.id).raw()
// Force remote fetch (e.g. to measure download time or get a fresh copy)
const f = await file(record.id).raw({ remote: true })
// Local only — null if the file isn't in the current session queue
const f = await file(record.id).raw({ local: true })
// Local image preview
img.src = URL.createObjectURL(f)
// Pass to another API (e.g. canvas, custom processor)
processLocally(f)file(id).downloadUrl()
Returns a Promise resolving to the authenticated download URL. Works with queue IDs (resolves the server file ID automatically) or raw server file IDs fetched from a REST listing — the caller doesn't need to know which kind of ID it has.
// from the upload queue
const url = await file(record.id).downloadUrl()
// from a REST listing — works identically
const url = await file(upload.fileId).downloadUrl()
const url = await file(upload.id).downloadUrl()
// in a Vue template
<a :href="await file(record.id).downloadUrl()">Download</a>file(id).download(filename?)
Fetches the file with auth and triggers a browser download dialog — no signed URLs needed. Uses the JWT from your token getter if configured, otherwise falls back to credentials: 'include' for session auth.
// from the upload queue
await file(record.id).download()
await file(record.id).download('custom-name.jpg')
// from a REST listing — works identically
await file(upload.fileId).download()
// in a template
<button @click="file(record.id).download(record.name)">Download</button>initSession()
Exchanges the current JWT for a browser session cookie covering all /media/* routes (/media/image, /media/download, etc.). Once established, cross-origin <img src> and <video src> requests authenticate via the cookie automatically — no Authorization header needed.
Called automatically on each add() call. Invoke explicitly at startup for image-only use cases (no uploads):
const { initSession, file } = useEncody({ token: getTokenFromBackend })
await initSession()
// <img :src="file(id).imageUrl(400, 300)"> now works cross-originThe session is valid for 15 minutes (matching the JWT TTL). The SDK tracks expiry via a companion readable cookie (encody_media_exp) set by the server, so redundant re-init calls are skipped across page reloads.
file(id).imageUrl(width?, height?, format?)
Returns an image URL string — sync, safe to use inline in templates. Passes id through as-is (accepts a fileId or any server-side id). Browser HTTP caching (1 year, public + ETag) is fully preserved since no blob fetch is involved.
// Auto-detects WebP support and scales for device pixel ratio.
// On a 2× Retina display with WebP support:
file(upload.fileId).imageUrl(400, 300) // → '/media/image/{id}/800-600/image.webp'
// On a 1× display without WebP:
file(upload.fileId).imageUrl(400, 300) // → '/media/image/{id}/400-300/image.jpg'
// Override format explicitly (DPR still applied)
file(upload.fileId).imageUrl(400, 300, 'jpeg') // → '/media/image/{id}/800-600/image.jpeg' on 2× display
// Pass 0 for either dimension to preserve aspect ratio (no letterboxing, no cropping):
file(upload.fileId).imageUrl(400, 0) // → '/media/image/{id}/800-0/image.webp' — fit to width
file(upload.fileId).imageUrl(0, 300) // → '/media/image/{id}/0-600/image.webp' — fit to height
// Original file, no transformation
file(upload.fileId).imageUrl() // → '/media/image/{id}'
// Directly in a Vue template — no computed or await needed
// <img :src="file(upload.fileId).imageUrl(400, 300)">Both methods accept any server-side id — the server resolves either format automatically.
FileRecord shape
{
id: string,
status: 'queued' | 'uploading' | 'processing' | 'ready' | 'failed' | 'cancelled',
progress: number, // 0–99 during upload, 99 during processing, 100 when ready
name: string,
size: number,
mimeType: string,
meta: object,
batchId: string | null,
error: string | null,
errorCode: ErrorCode | null,
url: string | null, // populated when status === 'ready'
serverFileId: string | null, // server-side file ID, populated when status === 'ready' — use for download()
}Error codes
import { defaultMessages, EncodyError, ErrorCode } from '@encody/vue'
ErrorCode.TOKEN_EMPTY // token() returned falsy
ErrorCode.UPLOAD_FAILED // network or server error
ErrorCode.VIRUS_DETECTED // antivirus flagged the file
ErrorCode.SSE_ERROR // SSE connection failed repeatedly
ErrorCode.FILE_TOO_LARGE // exceeds maxSize
ErrorCode.FILE_TYPE_NOT_ALLOWED // not in allowedTypesUse defaultMessages[code] as fallbacks, override per code for i18n:
const messages = {
[ErrorCode.FILE_TOO_LARGE]: 'Die Datei ist zu groß'
}
function errorMessage(file) {
return messages[file.errorCode] ?? file.error
}Framework-agnostic core
import { createEncody } from '@encody/vue/core'
const instance = createEncody({ token, baseUrl })
instance.add(files)
instance.on('file:ready', handler)
instance.destroy()License
MIT
Author
Marcus Spiegel [email protected] — published and supported by u|screen
