A library that makes it easier to work with Streams in the browser.
npx @tessl/cli install tessl/npm-workbox-streams@7.3.0Workbox Streams provides utilities for working with ReadableStreams in service workers and web applications. It offers functions to concatenate multiple stream sources into a single ReadableStream, create HTTP responses from concatenated streams, detect browser support for streaming APIs, and implement streaming strategies for Workbox routing.
npm install workbox-streamsimport {
concatenate,
concatenateToResponse,
isSupported,
strategy,
type StreamSource,
type StreamsHandlerCallback
} from "workbox-streams";For CommonJS:
const {
concatenate,
concatenateToResponse,
isSupported,
strategy
} = require("workbox-streams");import {
concatenate,
concatenateToResponse,
isSupported,
strategy
} from "workbox-streams";
// Check browser support for streaming
if (isSupported()) {
// Concatenate multiple stream sources
const sourcePromises = [
fetch('/api/header'),
fetch('/api/content'),
fetch('/api/footer')
];
const {done, stream} = concatenate(sourcePromises);
// Or create a Response directly
const {done: responseDone, response} = concatenateToResponse(
sourcePromises,
{'content-type': 'text/html'}
);
// Use in service worker
self.addEventListener('fetch', (event) => {
event.waitUntil(responseDone);
event.respondWith(response);
});
}Workbox Streams is built around streaming and fallback patterns:
Combines multiple stream sources into a single ReadableStream with sequential data flow.
/**
* Takes multiple source Promises and returns a ReadableStream with sequential data
* @param sourcePromises - Array of Promises resolving to StreamSource objects
* @returns Object with completion Promise and ReadableStream
*/
function concatenate(sourcePromises: Promise<StreamSource>[]): {
done: Promise<void>;
stream: ReadableStream;
};Usage Example:
import { concatenate } from "workbox-streams";
const sourcePromises = [
Promise.resolve(new Response('Hello ')),
Promise.resolve(new Response('World!')),
fetch('/api/data')
];
const {done, stream} = concatenate(sourcePromises);
// Use the stream
const response = new Response(stream, {
headers: {'content-type': 'text/plain'}
});
// Wait for completion
await done;Creates an HTTP Response with body consisting of concatenated stream data.
/**
* Takes multiple source Promises and returns a Response with concatenated data
* @param sourcePromises - Array of Promises resolving to StreamSource objects
* @param headersInit - Optional headers for the response (if no 'Content-Type', defaults to 'text/html')
* @returns Object with completion Promise and Response
*/
function concatenateToResponse(
sourcePromises: Promise<StreamSource>[],
headersInit?: HeadersInit
): {
done: Promise<void>;
response: Response;
};Usage Example:
import { concatenateToResponse } from "workbox-streams";
const sourcePromises = [
fetch('/template/header'),
fetch('/content/body'),
fetch('/template/footer')
];
// With headers
const {done, response} = concatenateToResponse(
sourcePromises,
{
'content-type': 'text/html',
'cache-control': 'max-age=3600'
}
);
// Without headers (defaults to 'text/html')
const {done: done2, response: response2} = concatenateToResponse(sourcePromises);
// Use in service worker
self.addEventListener('fetch', (event) => {
if (event.request.url.endsWith('/composite-page')) {
event.waitUntil(done);
event.respondWith(response);
}
});Determines whether the current browser supports streaming features.
/**
* Checks if browser supports ReadableStream construction for streaming responses
* @returns true if streaming is supported, false otherwise
*/
function isSupported(): boolean;Usage Example:
import { isSupported, concatenateToResponse } from "workbox-streams";
function createCompositeResponse(sources: Promise<StreamSource>[], headers: HeadersInit) {
if (isSupported()) {
// Use streaming concatenation
return concatenateToResponse(sources, headers);
} else {
// Fallback to waiting for all sources
return Promise.all(sources).then(resolvedSources => {
// Manual concatenation fallback
return new Response(/* combined content */, {headers});
});
}
}Creates a strategy function compatible with Workbox routing that handles streaming with automatic fallback.
/**
* Creates a Workbox-compatible strategy for streaming responses with fallback
* @param sourceFunctions - Array of handler functions returning StreamSource objects
* @param headersInit - Optional headers for responses (if no 'Content-Type', defaults to 'text/html')
* @returns RouteHandlerCallback compatible with Workbox routing
*/
function strategy(
sourceFunctions: StreamsHandlerCallback[],
headersInit?: HeadersInit
): RouteHandlerCallback;Usage Example:
import { strategy } from "workbox-streams";
import { registerRoute } from "workbox-routing";
// Define source functions
const sourceFunctions = [
// Header source
({url, request}) => fetch('/api/header'),
// Dynamic content based on URL
({url, request}) => fetch(`/api/content${url.pathname}`),
// Footer source
({url, request}) => fetch('/api/footer')
];
// Create streaming strategy with headers
const streamingStrategy = strategy(sourceFunctions, {
'content-type': 'text/html',
'cache-control': 'no-cache'
});
// Or without headers (defaults to 'text/html')
const basicStrategy = strategy(sourceFunctions);
// Register with Workbox routing
registerRoute(
({url}) => url.pathname.startsWith('/dynamic/'),
streamingStrategy
);Union type representing valid stream sources.
/**
* Valid source types for streaming operations
*/
type StreamSource = Response | ReadableStream | BodyInit;Where BodyInit includes: Blob | BufferSource | FormData | URLSearchParams | string
Interface for handler functions used in streaming strategies.
/**
* Handler function interface for streaming strategies
*/
interface StreamsHandlerCallback {
(options: RouteHandlerCallbackOptions): Promise<StreamSource> | StreamSource;
}
/**
* Generic object interface for key-value mapping (from workbox-core)
*/
interface MapLikeObject {
[key: string]: any;
}
/**
* Options passed to handler callbacks (from workbox-core)
*/
interface RouteHandlerCallbackOptions {
event: ExtendableEvent;
request: Request;
url: URL;
params?: string[] | MapLikeObject;
}
/**
* Route handler callback interface (from workbox-core)
*/
interface RouteHandlerCallback {
(options: RouteHandlerCallbackOptions): Promise<Response>;
}When stream processing fails, errors are propagated through the Promise chain:
import { concatenate } from "workbox-streams";
const {done, stream} = concatenate([
fetch('/api/source1'),
fetch('/api/source2')
]);
// Handle errors in the completion promise
done.catch(error => {
console.error('Stream concatenation failed:', error);
});
// Errors also propagate through the stream
const response = new Response(stream);
response.text().catch(error => {
console.error('Stream reading failed:', error);
});Workbox Streams throws a WorkboxError when attempting to stream opaque responses:
// This will throw WorkboxError: 'opaque-streams-source'
const opaqueResponse = await fetch('https://external-api.com/data', {
mode: 'no-cors' // Creates opaque response
});
const {stream} = concatenate([Promise.resolve(opaqueResponse)]);
// Error: Cannot read from opaque response