npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

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

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 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 [email protected] — published and supported by u|screen