FileStreams
FileStreams handle binary data in Jazz applications - think documents, audio files, and other non-text content. They're essentially collaborative versions of Blobs that sync automatically across devices.
Use FileStreams when you need to:
- Distribute documents across devices
- Store audio or video files
- Sync any binary data between users
Note: For images specifically, Jazz provides the higher-level ImageDefinition abstraction which manages multiple image resolutions - see the ImageDefinition documentation for details.
FileStreams provide automatic chunking when using the createFromBlob method, track upload progress, and handle MIME types and metadata.
In your schema, reference FileStreams like any other CoValue:
import { co, z } from "jazz-tools"; const Document = co.map({ title: z.string(), file: co.fileStream(), // Store a document file });
Creating FileStreams
There are two main ways to create FileStreams: creating empty ones for manual data population or creating directly from existing files or blobs.
Creating from Blobs and Files
For files from input elements or drag-and-drop interfaces, use createFromBlob:
// From a file input const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; fileInput.addEventListener("change", async () => { const file = fileInput.files?.[0]; if (!file) return; // Create FileStream from user-selected file const fileStream = await co .fileStream() .createFromBlob(file, { owner: myGroup }); // Or with progress tracking for better UX const fileWithProgress = await co.fileStream().createFromBlob(file, { onProgress: (progress) => { // progress is a value between 0 and 1 const percent = Math.round(progress * 100); console.log(`Upload progress: ${percent}%`); progressBar.style.width = `${percent}%`; }, owner: myGroup, }); });
Creating Empty FileStreams
Create an empty FileStream when you want to manually add binary data in chunks:
const fileStream = co.fileStream().create({ owner: myGroup });
Ownership
Like other CoValues, you can specify ownership when creating FileStreams.
// Create a team group const teamGroup = Group.create(); teamGroup.addMember(colleagueAccount, "writer"); // Create a FileStream with shared ownership const teamFileStream = co.fileStream().create({ owner: teamGroup });
See Groups as permission scopes for more information on how to use groups to control access to FileStreams.
Reading from FileStreams
FileStreams provide several ways to access their binary content, from raw chunks to convenient Blob objects.
Getting Raw Data Chunks
To access the raw binary data and metadata:
// Get all chunks and metadata const fileData = fileStream.getChunks(); if (fileData) { console.log(`MIME type: ${fileData.mimeType}`); console.log(`Total size: ${fileData.totalSizeBytes} bytes`); console.log(`File name: ${fileData.fileName}`); console.log(`Is complete: ${fileData.finished}`); // Access raw binary chunks for (const chunk of fileData.chunks) { // Each chunk is a Uint8Array console.log(`Chunk size: ${chunk.length} bytes`); } }
By default, getChunks() only returns data for completely synced FileStreams. To start using chunks from a FileStream that's currently still being synced use the allowUnfinished option:
// Get data even if the stream isn't complete const partialData = fileStream.getChunks({ allowUnfinished: true });
Converting to Blobs
For easier integration with web APIs, convert to a Blob:
// Convert to a Blob const blob = fileStream.toBlob(); // Get the filename from the metadata const filename = fileStream.getChunks()?.fileName; if (blob) { // Use with URL.createObjectURL const url = URL.createObjectURL(blob); // Create a download link const link = document.createElement("a"); link.href = url; link.download = filename || "document.pdf"; link.click(); // Clean up when done URL.revokeObjectURL(url); }
Loading FileStreams as Blobs
You can directly load a FileStream as a Blob when you only have its ID:
// Load directly as a Blob when you have an ID const blobFromID = await co.fileStream().loadAsBlob(fileStreamId); // By default, waits for complete uploads // For in-progress uploads: const partialBlob = await co.fileStream().loadAsBlob(fileStreamId, { allowUnfinished: true, });
Checking Completion Status
Check if a FileStream is fully synced:
if (fileStream.isBinaryStreamEnded()) { console.log("File is completely synced"); } else { console.log("File upload is still in progress"); }
Writing to FileStreams
When creating a FileStream manually (not using createFromBlob), you need to manage the upload process yourself. This gives you more control over chunking and progress tracking.
The Upload Lifecycle
FileStream uploads follow a three-stage process:
- Start - Initialize with metadata
- Push - Send one or more chunks of data
- End - Mark the stream as complete
Starting a FileStream
Begin by providing metadata about the file:
// Create an empty FileStream const manualFileStream = co.fileStream().create({ owner: myGroup }); // Initialize with metadata manualFileStream.start({ mimeType: "application/pdf", // MIME type (required) totalSizeBytes: 1024 * 1024 * 2, // Size in bytes (if known) fileName: "document.pdf", // Original filename (optional) });
Pushing Data
Add binary data in chunks - this helps with large files and progress tracking:
const data = new Uint8Array(arrayBuffer); // For large files, break into chunks (e.g., 100KB each) const chunkSize = 1024 * 100; for (let i = 0; i < data.length; i += chunkSize) { // Create a slice of the data const chunk = data.slice(i, i + chunkSize); // Push chunk to the FileStream fileStream.push(chunk); // Track progress const progress = Math.min( 100, Math.round(((i + chunk.length) * 100) / data.length), ); console.log(`Upload progress: ${progress}%`); } // Finalise the upload fileStream.end(); console.log("Upload complete!");
Completing the Upload
Once all chunks are pushed, mark the FileStream as complete:
// Finalise the upload fileStream.end(); console.log("Upload complete!");
Subscribing to FileStreams
Like other CoValues, you can subscribe to FileStreams to get notified of changes as they happen. This is especially useful for tracking upload progress when someone else is uploading a file.
Loading by ID
Load a FileStream when you have its ID:
const fileStreamFromId = await co.fileStream().load(fileStreamId); if (fileStream.$isLoaded) { console.log("FileStream loaded successfully"); // Check if it's complete if (fileStream.isBinaryStreamEnded()) { // Process the completed file const blob = fileStream.toBlob(); } }
Subscribing to Changes
Subscribe to a FileStream to be notified when chunks are added or when the upload is complete:
const unsubscribe = co .fileStream() .subscribe(fileStreamId, (fileStream: FileStream) => { // Called whenever the FileStream changes console.log("FileStream updated"); // Get current status const chunks = fileStream.getChunks({ allowUnfinished: true }); if (chunks) { const uploadedBytes = chunks.chunks.reduce( (sum: number, chunk: Uint8Array) => sum + chunk.length, 0, ); const totalBytes = chunks.totalSizeBytes || 1; const progress = Math.min( 100, Math.round((uploadedBytes * 100) / totalBytes), ); console.log(`Upload progress: ${progress}%`); if (fileStream.isBinaryStreamEnded()) { console.log("Upload complete!"); // Now safe to use the file const blob = fileStream.toBlob(); // Clean up the subscription if we're done unsubscribe(); } } });
Waiting for Upload Completion
If you need to wait for a FileStream to be fully synchronized across devices:
// Wait for the FileStream to be fully synced await fileStream.$jazz.waitForSync({ timeout: 5000, // Optional timeout in ms }); console.log("FileStream is now synced to all connected devices");
This is useful when you need to ensure that a file is available to other users before proceeding with an operation.