Skip to content

Web Worker Usage ​

Use Web Workers to prevent main thread blocking when processing large .cloupe files.

Why Use Web Workers? ​

.cloupe files can range from hundreds of megabytes to several gigabytes. Processing large data on the main thread causes:

  • UI freezing (unresponsive)
  • User interaction blocked
  • Browser may suggest terminating the script

Web Workers run in a separate thread, preventing these issues.

Basic Setup ​

Vite ​

typescript
// main.ts
import { CloupeWorkerClient } from "cloupe.js";

const worker = new Worker(new URL("cloupe/worker", import.meta.url), { type: "module" });

const client = new CloupeWorkerClient(worker);

Webpack ​

javascript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.worker\.ts$/,
        use: { loader: "worker-loader" },
      },
    ],
  },
};
typescript
// main.ts
import CloupeWorker from "cloupe.js/worker";
import { CloupeWorkerClient } from "cloupe.js";

const worker = new CloupeWorker();
const client = new CloupeWorkerClient(worker);

API Usage ​

CloupeWorkerClient provides the same API as CloupeReader:

typescript
import { CloupeWorkerClient } from "cloupe.js";

// Create Worker
const worker = new Worker(new URL("cloupe/worker", import.meta.url), { type: "module" });
const client = new CloupeWorkerClient(worker);

// Open file
await client.open(file);

// Read data (same API as CloupeReader)
const summary = await client.getSummary();
const barcodes = await client.getBarcodes({ limit: 100 });
const projection = await client.getProjection("UMAP");
const expression = await client.getExpressionByFeatureName("CD3D");

// Cleanup
await client.close();
client.terminate(); // Terminate worker

Example: Progress Display ​

typescript
import { CloupeWorkerClient } from "cloupe.js";

async function loadWithProgress(file: File, onProgress: (msg: string) => void) {
  const worker = new Worker(new URL("cloupe/worker", import.meta.url), { type: "module" });
  const client = new CloupeWorkerClient(worker);

  try {
    onProgress("Opening file...");
    await client.open(file);

    onProgress("Loading summary...");
    const summary = await client.getSummary();

    onProgress("Loading projection...");
    const projection = await client.getProjection("UMAP");

    onProgress("Loading features...");
    const features = await client.getFeatures();

    onProgress("Complete!");

    return { summary, projection, features };
  } finally {
    await client.close();
    client.terminate();
  }
}

// Usage
const result = await loadWithProgress(file, (msg) => {
  document.getElementById("status").textContent = msg;
});

Example: Large Data Processing ​

typescript
import { CloupeWorkerClient } from "cloupe.js";

async function analyzeExpression(
  file: File,
  geneNames: string[],
  onProgress: (progress: number) => void
) {
  const worker = new Worker(new URL("cloupe/worker", import.meta.url), { type: "module" });
  const client = new CloupeWorkerClient(worker);

  try {
    await client.open(file);

    const results: Map<string, number> = new Map();

    for (let i = 0; i < geneNames.length; i++) {
      const expression = await client.getExpressionByFeatureName(geneNames[i]);

      if (expression) {
        const mean = expression.values.reduce((a, b) => a + b, 0) / expression.values.length;
        results.set(geneNames[i], mean);
      }

      onProgress((i + 1) / geneNames.length);
    }

    return results;
  } finally {
    await client.close();
    client.terminate();
  }
}

// Usage
const genes = ["CD3D", "CD4", "CD8A", "MS4A1", "CD14"];
const results = await analyzeExpression(file, genes, (progress) => {
  progressBar.value = progress * 100;
});

Example: React Hook ​

tsx
import { useState, useEffect, useCallback } from "react";
import { CloupeWorkerClient } from "cloupe.js";

interface UseCloupeWorkerResult {
  client: CloupeWorkerClient | null;
  isLoading: boolean;
  error: Error | null;
  open: (file: File) => Promise<void>;
  close: () => Promise<void>;
}

export function useCloupeWorker(): UseCloupeWorkerResult {
  const [client, setClient] = useState<CloupeWorkerClient | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const worker = new Worker(new URL("cloupe/worker", import.meta.url), { type: "module" });
    const newClient = new CloupeWorkerClient(worker);
    setClient(newClient);

    return () => {
      newClient.terminate();
    };
  }, []);

  const open = useCallback(
    async (file: File) => {
      if (!client) return;

      setIsLoading(true);
      setError(null);

      try {
        await client.open(file);
      } catch (e) {
        setError(e instanceof Error ? e : new Error("Unknown error"));
      } finally {
        setIsLoading(false);
      }
    },
    [client]
  );

  const close = useCallback(async () => {
    if (!client) return;
    await client.close();
  }, [client]);

  return { client, isLoading, error, open, close };
}

// Usage
function MyComponent() {
  const { client, isLoading, error, open, close } = useCloupeWorker();
  const [summary, setSummary] = useState(null);

  const handleFileSelect = async (file: File) => {
    await open(file);
    if (client) {
      const sum = await client.getSummary();
      setSummary(sum);
    }
  };

  if (error) return <div>Error: {error.message}</div>;
  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <input type="file" onChange={(e) => handleFileSelect(e.target.files[0])} />
      {summary && <pre>{JSON.stringify(summary, null, 2)}</pre>}
    </div>
  );
}

Important Notes ​

Data Transfer ​

Data transfer between worker and main thread uses the structured clone algorithm:

  • TypedArrays (Float64Array, etc.) are transferred efficiently
  • Large data may take time to transfer
  • Original data is detached after transfer (transferable objects)

Memory ​

  • Workers use separate memory space
  • Be aware of browser memory limits when loading large data
  • Call clearCache() for unnecessary data

Browser Support ​

  • All modern browsers supported
  • Module Workers require ES module-capable browsers
  • SharedArrayBuffer usage requires CORS headers

Released under the MIT License.