Edit

File download

BetaTelefunc Stream is in beta: breaking changes may occur in any version update.

You can return a File or Blob from a telefunction like any other value.

A single file, multiple files, or files mixed with other return values — all are supported.

The client receives a standard File / Blob ready for URL.createObjectURL, <img src>, fetch({ body }), FormData, etc.

There are multiple streaming and reading strategies, see:

Example: basic

Return a native File or Blob like any other value.

// Invoice.telefunc.js
// Environment: server
 
import { renderPdf } from 'some-pdf-lib'
 
export async function onRenderInvoice(form) {
  const bytes = await renderPdf(form)
  return new File([bytes], `invoice-${form.id}.pdf`, { type: 'application/pdf' })
}
// Invoice.telefunc.ts
// Environment: server
 
import { renderPdf } from 'some-pdf-lib'
 
export async function onRenderInvoice(form: InvoiceForm) {
  const bytes = await renderPdf(form)
  return new File([bytes], `invoice-${form.id}.pdf`, { type: 'application/pdf' })
}
// Invoice.jsx
// Environment: client
 
import { onRenderInvoice } from './Invoice.telefunc'
 
const pdf = await onRenderInvoice(form)
window.open(URL.createObjectURL(pdf))
// Invoice.tsx
// Environment: client
 
import { onRenderInvoice } from './Invoice.telefunc'
 
const pdf = await onRenderInvoice(form)
window.open(URL.createObjectURL(pdf))

Example: progress + cancel

// Download.jsx
// Environment: client
 
import { useState } from 'react'
import { onGetImage } from './Image.telefunc'
 
function Download({ s3Key }) {
  const [progress, setProgress] = useState(0)
  const [src, setSrc] = useState(null)
  const [cancel, setCancel] = useState(null)
 
  async function handleDownload() {
    const dl = await onGetImage(s3Key)
    setCancel(() => () => dl.cancel())
 
    dl.onProgress((loaded, total) => {
      setProgress(total ? Math.round((loaded / total) * 100) : 0)
    })
 
    try {
      const file = await dl.saveToMemory()
      setSrc(URL.createObjectURL(file))
    } finally {
      setCancel(null)
    }
  }
 
  return (
    <div>
      <button onClick={handleDownload}>Download</button>
      {cancel && <button onClick={cancel}>Cancel</button>}
      <div style={{ width: `${progress}%`, height: 4, background: '#3b82f6' }} />
      {src && <img src={src} />}
    </div>
  )
}
// Download.tsx
// Environment: client
 
import { useState } from 'react'
import { onGetImage } from './Image.telefunc'
 
function Download({ s3Key }: { s3Key: string }) {
  const [progress, setProgress] = useState(0)
  const [src, setSrc] = useState<string | null>(null)
  const [cancel, setCancel] = useState<(() => void) | null>(null)
 
  async function handleDownload() {
    const dl = await onGetImage(s3Key)
    setCancel(() => () => dl.cancel())
 
    dl.onProgress((loaded, total) => {
      setProgress(total ? Math.round((loaded / total) * 100) : 0)
    })
 
    try {
      const file = await dl.saveToMemory()
      setSrc(URL.createObjectURL(file))
    } finally {
      setCancel(null)
    }
  }
 
  return (
    <div>
      <button onClick={handleDownload}>Download</button>
      {cancel && <button onClick={cancel}>Cancel</button>}
      <div style={{ width: `${progress}%`, height: 4, background: '#3b82f6' }} />
      {src && <img src={src} />}
    </div>
  )
}

Calling dl.cancel() aborts the download — the pending saveToMemory() call then rejects.

Example: multiple concurrent downloads

Return any number of File, Blob, or download() values anywhere in the response — in arrays, in nested objects, or mixed with regular data. Each one exposes its own onProgress / cancel / saveTo* methods on the client, independently of the others.

// Document.telefunc.js
// Environment: server
 
import fs from 'node:fs'
import { Readable } from 'node:stream'
import { download } from 'telefunc'
 
export async function onLoadDocuments(id) {
  return {
    title: 'My Document',
    pdf: download(Readable.toWeb(fs.createReadStream(`./docs/${id}.pdf`)), {
      name: 'doc.pdf',
      type: 'application/pdf'
    }),
    thumbnail: new File([await fs.promises.readFile(`./docs/${id}-thumb.png`)], 'thumb.png', {
      type: 'image/png'
    })
  }
}
// Document.telefunc.ts
// Environment: server
 
import fs from 'node:fs'
import { Readable } from 'node:stream'
import { download } from 'telefunc'
 
export async function onLoadDocuments(id: string) {
  return {
    title: 'My Document',
    pdf: download(Readable.toWeb(fs.createReadStream(`./docs/${id}.pdf`)), {
      name: 'doc.pdf',
      type: 'application/pdf'
    }),
    thumbnail: new File([await fs.promises.readFile(`./docs/${id}-thumb.png`)], 'thumb.png', {
      type: 'image/png',
    }),
  }
}
// Document.jsx
// Environment: client
 
const { title, pdf, thumbnail } = await onLoadDocuments(id)
 
pdfEl.src = URL.createObjectURL(await pdf.saveToMemory())
thumbEl.src = URL.createObjectURL(thumbnail)
// Document.tsx
// Environment: client
 
const { title, pdf, thumbnail } = await onLoadDocuments(id)
 
pdfEl.src = URL.createObjectURL(await pdf.saveToMemory())
thumbEl.src = URL.createObjectURL(thumbnail)

Example: passthrough with download()

Wrap a ReadableStream with download() to stream bytes from an upstream source through your server without buffering.

// Image.telefunc.js
// Environment: server
 
import { download } from 'telefunc'
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
 
const s3 = new S3Client({})
 
export async function onGetImage(key) {
  const res = await s3.send(new GetObjectCommand({ Bucket: 'my-bucket', Key: key }))
  return download(res.Body.transformToWebStream(), {
    name: key,
    size: res.ContentLength,
    type: res.ContentType
  })
}
// Image.telefunc.ts
// Environment: server
 
import { download } from 'telefunc'
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
 
const s3 = new S3Client({})
 
export async function onGetImage(key: string) {
  const res = await s3.send(new GetObjectCommand({ Bucket: 'my-bucket', Key: key }))
  return download(res.Body!.transformToWebStream(), {
    name: key,
    size: res.ContentLength,
    type: res.ContentType
  })
}
// ImageView.jsx
// Environment: client
 
import { onGetImage } from './Image.telefunc'
 
const dl = await onGetImage(s3Key)
imgEl.src = URL.createObjectURL(await dl.saveToMemory())
// ImageView.tsx
// Environment: client
 
import { onGetImage } from './Image.telefunc'
 
const dl = await onGetImage(s3Key)
imgEl.src = URL.createObjectURL(await dl.saveToMemory())

dl.name, dl.type, dl.size, dl.lastModified are available as soon as await onGetImage(...) settles — bytes stream in the background.

Streaming strategies

Environment: server.

You can choose between download() and a native File/Blob:

  • Bytes already in memory → return a native File/Blob.
  • Large files, bytes streamed from an upstream source, or anything needing progress / cancel / save-to-disk → wrap them with download().
ApproachServer memoryClient memoryBest for
Native File / BlobFull payloadFull payloadBytes already in memory
download(file) / download(blob)Full payloadDepends on consumerSame + progress / cancel / save-to-disk
download(stream, opts)Constant (chunk-sized)Depends on consumerBytes streamed from upstream (S3, CDN, …)

Constant when consumed via dl.stream(), dl.saveToOpfs(), or dl.saveToDisk() (except its 'memory' fallback); full payload when consumed via dl.saveToMemory() / dl.text() / dl.arrayBuffer().

download()

A download() value exposes progress and cancellation on the client. (Native File / Blob returns don't — wrap your bytes with download() to opt in.)

MemberTypeDescription
dl.onProgress(cb)(loaded, total) => voidRegister a callback fired as bytes arrive. loaded is the bytes received so far; total is the full size (from download()'s size / the upstream Content-Length), or undefined if unknown. Fires immediately with the current progress if bytes already arrived; can be called more than once to register multiple callbacks.
dl.loadednumberBytes received so far.
dl.cancel()voidAbort the download. Any pending read (saveToMemory(), text(), …) then rejects.

Reading strategies

Environment: client.

The streaming download is a standard File / Blob object — dl.stream(), dl.text(), dl.arrayBuffer(), dl.bytes(), and dl.slice() all work and pull from the streaming source as you read:

MethodReturnsMemoryBest for
dl.stream()ReadableStreamConstant (chunk-sized)Pipe to disk, S3, or any writable stream
dl.bytes()Uint8ArrayFull file in memoryProcess raw binary
dl.arrayBuffer()ArrayBufferFull file in memoryProcess entire file at once
dl.text()stringFull file in memoryRead text content
dl.saveToMemory()File / BlobFull file in memoryUse with Web APIs (URL.createObjectURL, <img src>, ...)
dl.saveToDisk(opts?)File (disk-backed)Constant§Save to user's filesystem (picker or Downloads folder)
dl.saveToOpfs(path?)File (disk-backed)Constant (chunk-sized)Stash in browser-private storage (OPFS)

File for FileDownload, Blob for BlobDownload.

§ Constant in 'picker' / 'opfs' mode; the 'memory' fallback (default when showSaveFilePicker is unavailable) buffers the full file in RAM.

For large files, prefer dl.stream() / dl.saveToDisk() / dl.saveToOpfs() — memory stays constant regardless of file size.

// Pass to a Web API:
const f = await dl.saveToMemory()
imgEl.src = URL.createObjectURL(f)
 
// Save to user's filesystem — picker if available, else Downloads folder:
await dl.saveToDisk()
 
// Stash in browser-private storage; URL.createObjectURL on the result has no RAM cost:
const opfsFile = await dl.saveToOpfs()
imgEl.src = URL.createObjectURL(opfsFile)

Some Web APIs — URL.createObjectURL, <img src>, fetch({body}), FormData.append — require a real File / Blob, so call await dl.saveToMemory() / saveToDisk() / saveToOpfs() first. These APIs check for an internal marker that only real File / Blob instances have.

saveToDisk({ mode })

modeBehavior
(omitted)'picker' if showSaveFilePicker is available, else 'memory'.
'picker'Opens the save-file picker so the user chooses the location. Throws if not supported.
'memory'Buffers bytes in RAM, then triggers a native browser download to the Downloads folder. Cross-browser.
'opfs'Streams bytes through browser-private storage (OPFS), then triggers a native browser download. For large files where in-memory buffering isn't viable. Cross-browser.

Error handling

You handle errors the same way you do for Telefunc streams: Guides > Stream > Error handling.

Limitations

The client receives the File / Blob as a lazy, streaming object: its bytes flow straight from the HTTP response as you read them, without being buffered in memory. This keeps downloads memory-efficient — but comes with one trade-off:

One-shot reads

The streaming download can only be consumed once — calling .stream(), .bytes(), .text(), .arrayBuffer(), .slice(), or any saveTo* method a second time throws.

If you need the data more than once, materialize it once via dl.saveToMemory() (or saveToOpfs() for large files) and reuse the returned File / Blob.

See also