File download
Beta — Telefunc 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.lastModifiedare available as soon asawait 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().
| Approach | Server memory | Client memory | Best for |
|---|---|---|---|
Native File / Blob | Full payload | Full payload | Bytes already in memory |
download(file) / download(blob) | Full payload | Depends on consumer† | Same + progress / cancel / save-to-disk |
download(stream, opts) | Constant (chunk-sized) | Depends on consumer† | Bytes 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.)
| Member | Type | Description |
|---|---|---|
dl.onProgress(cb) | (loaded, total) => void | Register 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.loaded | number | Bytes received so far. |
dl.cancel() | void | Abort 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:
| Method | Returns | Memory | Best for |
|---|---|---|---|
dl.stream() | ReadableStream | Constant (chunk-sized) | Pipe to disk, S3, or any writable stream |
dl.bytes() | Uint8Array | Full file in memory | Process raw binary |
dl.arrayBuffer() | ArrayBuffer | Full file in memory | Process entire file at once |
dl.text() | string | Full file in memory | Read text content |
dl.saveToMemory() | File / Blob‡ | Full file in memory | Use 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 realFile/Blob, so callawait dl.saveToMemory()/saveToDisk()/saveToOpfs()first. These APIs check for an internal marker that only realFile/Blobinstances have.
saveToDisk({ mode })
mode | Behavior |
|---|---|
| (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()(orsaveToOpfs()for large files) and reuse the returnedFile/Blob.