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:

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

  1. Start - Initialize with metadata
  2. Push - Send one or more chunks of data
  3. 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.