New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

@encody/vue

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@encody/vue

Encody client SDK — resumable upload queue with Vue composables

latest
npmnpm
Version
0.13.0
Version published
Weekly downloads
17
-22.73%
Maintainers
1
Weekly downloads
 
Created
Source

@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/vue

Usage

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

OptionDefaultDescription
baseUrl'https://app.encody.io'API origin. Override for on-prem or enterprise installs.
endpoint{baseUrl}/media/uploadTUS upload endpoint. Derived from baseUrl if omitted.
sseUrl{baseUrl}/api/upload-eventsSSE endpoint for real-time status updates. Derived from baseUrl if omitted.
tokenasync () => 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.
concurrency3Max parallel uploads.
retryDelays[0, 3000, 10000, 30000]Backoff delays in ms between TUS retries. Pass [] to disable.
maxSizenullMax 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.
storageMemoryAdapterQueue storage adapter. Pass a persistent adapter to survive page reloads — see Persistent offline queue.
queueTTL86400000 (24 h)Max age in ms of a queued/interrupted record before startup recovery discards it. Only relevant with persistent storage. null = keep forever.
keepCompletedfalseKeep completed (ready/cancelled) records in persistent storage across reloads. By default they are purged during startup recovery.
pollInterval5000Watchdog: while files are processing and no SSE event arrived within this window (ms), the SDK polls /api/upload-status to catch missed events. null/0 disables.
scanTimeoutInfinityMax time in ms a file may sit in processing waiting for the virus-scan result. After the deadline the file resolves to ready anyway; 0 means the scan never gates readiness. Infinity (default) keeps the current behavior: ready only once the scan reported. Note: a late infected verdict still flips an already-ready file to failed, and with pollInterval disabled an elapsed deadline only resolves on the next SSE event.

Persistent offline queue

By default the queue lives in memory and is lost on reload. Plug in @encody/storage-idb to persist queued files — including the file bytes — in IndexedDB:

import { IndexedDBAdapter } from '@encody/storage-idb'

const { add, files, on } = useEncody({
  baseUrl: 'https://...',
  token: async () => getJwt(),
  storage: new IndexedDBAdapter()
})

On startup the SDK scans the persisted queue: interrupted uploads restart from byte 0, and a queue:recovered event reports what was resumed. Coming back online or returning to a visible tab re-drains anything still queued — this works on every browser including iOS Safari (no Background Sync needed).

If a record's file blob did not survive (storage eviction), a file:reprompt event asks your app to let the user re-select the file:

on('file:reprompt', async ({ fileId, name, size }) => {
  // show your own UI, then:
  await resolveReprompt(fileId, newlySelectedFile)
})

Records queued longer than queueTTL (default 24 h) are discarded during recovery. A storage:quota-exceeded event fires when the browser rejects a write for lack of space (add() also rejects).

Multiple tabs work out of the box: the adapter broadcasts queue changes via BroadcastChannel, so every tab sees live status and progress. A leader tab (elected via the Web Locks API) owns recovery and drain, preventing duplicate uploads. Each active upload additionally holds a per-file Web Lock as an exact liveness signal — when a tab closes or crashes mid-upload, the browser releases its locks instantly and another tab takes the upload over immediately (a ~10 s staleness heuristic only applies on browsers without Web Locks).

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.mjs

Then 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 allowedTypes return value.
  • Precedence: a non-empty project list overrides the allowedTypes you pass to useEncody(...); if the project defines none, your init allowedTypes applies; 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:

OptionDefaultDescription
(none)autoIn-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-origin

The 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?, { version? })

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)">

Cache busting (?v=): file contents can change under the same URL (server-side replace). If your page knows a content version — the upload's md5 from your own backend or webhook data — pass it as version; the URL gets a ?v= query parameter, so a new version bypasses the year-long browser cache immediately. The server ignores the parameter. Without version the URL is unchanged.

file(upload.fileId).imageUrl(400, 300, undefined, { version: upload.md5 })
// → '/media/image/{id}/800-600/image.webp?v=<md5>'

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 allowedTypes

Use 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 spiegel@uscreen.de — published and supported by u|screen

FAQs

Package last updated on 10 Aug 2026

Related posts