Skip to content

Instantly share code, notes, and snippets.

@forresto
Created September 8, 2025 10:12
Show Gist options
  • Save forresto/b43744e3f91c8db5f2782ac46a43ba27 to your computer and use it in GitHub Desktop.
Save forresto/b43744e3f91c8db5f2782ac46a43ba27 to your computer and use it in GitHub Desktop.
Create a "blob stream" that can be used with PDFKit in the browser.
/**
* Minimal browser-native replacement for blob-stream
* Provides the same interface as [blob-stream](https://github.com/devongovett/blob-stream/issues/6) but uses browser-native APIs
*/
interface BlobStreamInterface {
// EventEmitter interface methods that PDFKit expects
on(event: string, callback: (...args: any[]) => void): this;
once(event: string, callback: (...args: any[]) => void): this;
emit(event: string, ...args: any[]): boolean;
addListener(event: string, callback: (...args: any[]) => void): this;
removeListener(event: string, callback: (...args: any[]) => void): this;
removeAllListeners(event?: string): this;
off(event: string, callback: (...args: any[]) => void): this;
listeners(event: string): ((...args: any[]) => void)[];
listenerCount(event: string): number;
setMaxListeners(n: number): this;
getMaxListeners(): number;
// Other EventEmitter methods
rawListeners(event: string): ((...args: any[]) => void)[];
prependListener(event: string, callback: (...args: any[]) => void): this;
prependOnceListener(event: string, callback: (...args: any[]) => void): this;
eventNames(): (string | symbol)[];
// blob-stream specific interface
toBlob(type?: string): Blob;
toBlobURL(type?: string): string;
// Minimal writable stream interface for PDFKit
writable: boolean;
write(chunk: any): boolean;
end(): this;
}
/**
* Creates a blob stream that can be used with PDFKit in the browser
*
* Usage: `const stream = doc.pipe(blobStream());`
*
* Only tested with pdfkit/js/pdfkit.standalone
*
* Vibe coded (🤪) 2025-09 with help from Gemini 2.5 Pro.
*/
function blobStream(): BlobStreamInterface {
let chunks: Uint8Array[] = [];
const eventListeners: { [key: string]: ((...args: any[]) => void)[] } = {};
let isFinished = false;
let blob: Blob | null = null;
let maxListeners = 10;
const instance: BlobStreamInterface = {
writable: true,
on(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
if (!eventListeners[event]) {
eventListeners[event] = [];
}
eventListeners[event].push(callback);
if (event === "finish" && isFinished) {
setTimeout(() => callback(), 0);
}
return instance;
},
once(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
const wrappedCallback = (...args: any[]) => {
instance.removeListener(event, wrappedCallback);
callback(...args);
};
return instance.on(event, wrappedCallback);
},
addListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
return instance.on(event, callback);
},
emit(event: string, ...args: any[]): boolean {
const listeners = eventListeners[event];
if (!listeners || listeners.length === 0) {
return false;
}
// Create a copy of the listeners array in case a listener modifies the array
[...listeners].forEach((callback) => {
try {
callback(...args);
} catch (error) {
console.error("Error in event listener:", error);
// We should still emit an 'error' event on the stream itself
instance.emit("error", error);
}
});
return true;
},
removeListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
const listeners = eventListeners[event];
if (listeners) {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
return instance;
},
off(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
return instance.removeListener(event, callback);
},
removeAllListeners(event?: string): BlobStreamInterface {
if (event) {
delete eventListeners[event];
} else {
Object.keys(eventListeners).forEach((key) => {
delete eventListeners[key];
});
}
return instance;
},
listeners(event: string): ((...args: any[]) => void)[] {
return eventListeners[event] ? [...eventListeners[event]] : [];
},
rawListeners(event: string): ((...args: any[]) => void)[] {
return instance.listeners(event);
},
prependListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
if (!eventListeners[event]) {
eventListeners[event] = [];
}
eventListeners[event].unshift(callback);
return instance;
},
prependOnceListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface {
const wrappedCallback = (...args: any[]) => {
instance.removeListener(event, wrappedCallback);
callback(...args);
};
return instance.prependListener(event, wrappedCallback);
},
eventNames(): (string | symbol)[] {
return Object.keys(eventListeners);
},
listenerCount(event: string): number {
return eventListeners[event] ? eventListeners[event].length : 0;
},
setMaxListeners(n: number): BlobStreamInterface {
maxListeners = n;
return instance;
},
getMaxListeners(): number {
return maxListeners;
},
write(chunk: any): boolean {
if (isFinished) return false;
let uint8Chunk: Uint8Array;
if (chunk instanceof Uint8Array) {
uint8Chunk = chunk;
} else if (typeof chunk === "string") {
const encoder = new TextEncoder();
uint8Chunk = encoder.encode(chunk);
} else if (chunk instanceof ArrayBuffer) {
uint8Chunk = new Uint8Array(chunk);
} else if (Array.isArray(chunk)) {
uint8Chunk = new Uint8Array(chunk);
} else {
try {
uint8Chunk = new Uint8Array(chunk);
} catch (e) {
instance.emit("error", new Error("Failed to convert chunk to Uint8Array"));
return false;
}
}
chunks.push(uint8Chunk);
return true;
},
end(): BlobStreamInterface {
if (isFinished) {
return instance;
}
isFinished = true;
try {
blob = new Blob(chunks as BlobPart[], { type: "application/pdf" });
instance.emit("finish");
} catch (error) {
instance.emit("error", error);
}
chunks = []; // free memory
return instance;
},
toBlob(type: string = "application/pdf"): Blob {
if (!blob) {
return new Blob(chunks as BlobPart[], { type });
}
if (type !== blob.type) {
return blob.slice(0, blob.size, type);
}
return blob;
},
toBlobURL(type: string = "application/pdf"): string {
const blobObj = instance.toBlob(type);
return URL.createObjectURL(blobObj);
},
};
return instance;
}
export default blobStream;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment