@snps/streaming
Version:
Zero-dependency streaming utilities for SSE, async streams, and AI integration
459 lines • 10.9 kB
JavaScript
/**
* Streaming API - Modern streaming support for Server-Sent Events, async iteration, and real-time data
* Perfect for AI streaming responses, live updates, and data processing pipelines
* @packageDocumentation
*/
/**
* Server-Sent Events (SSE) Stream
* Enables real-time updates from server to client
*/
export class SSEStream {
controller;
closed = false;
/**
* Create a new SSE stream
*/
constructor() {
// Stream is created on demand
}
/**
* Get the ReadableStream for this SSE connection
*/
getStream() {
return new ReadableStream({
start: (controller) => {
this.controller = controller;
},
cancel: () => {
this.closed = true;
}
});
}
/**
* Send a message to the client
*/
send(message) {
if (this.closed || !this.controller)
return;
let data = '';
if (message.event) {
data += `event: ${message.event}\n`;
}
if (message.id) {
data += `id: ${message.id}\n`;
}
if (message.retry !== undefined) {
data += `retry: ${message.retry}\n`;
}
// Handle multi-line data
const lines = message.data.split('\n');
for (const line of lines) {
data += `data: ${line}\n`;
}
data += '\n';
try {
this.controller.enqueue(data);
}
catch (error) {
console.error('SSE send error:', error);
this.closed = true;
}
}
/**
* Send a simple message (shorthand)
*/
message(data) {
this.send({ data });
}
/**
* Send an event with data
*/
event(event, data) {
this.send({ event, data });
}
/**
* Send JSON data
*/
json(data) {
this.send({ data: JSON.stringify(data) });
}
/**
* Close the stream
*/
close() {
if (this.closed)
return;
this.closed = true;
if (this.controller) {
this.controller.close();
}
}
/**
* Check if stream is closed
*/
isClosed() {
return this.closed;
}
}
/**
* Create a Server-Sent Events response
*/
export function createSSEResponse(stream) {
return new Response(stream.getStream(), {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}
/**
* Async Stream - Modern async iterator-based streaming
*/
export class AsyncStream {
queue = [];
resolvers = [];
done = false;
error;
/**
* Push a value to the stream
*/
push(value) {
if (this.done) {
throw new Error('Cannot push to closed stream');
}
const resolver = this.resolvers.shift();
if (resolver) {
resolver({ value, done: false });
}
else {
this.queue.push(value);
}
}
/**
* End the stream
*/
end() {
this.done = true;
this.resolvers.forEach(resolve => {
resolve({ value: undefined, done: true });
});
this.resolvers = [];
}
/**
* Error the stream
*/
throw(error) {
this.error = error;
this.done = true;
this.resolvers.forEach(resolve => {
resolve({ value: undefined, done: true });
});
this.resolvers = [];
}
/**
* Async iterator interface
*/
async *[Symbol.asyncIterator]() {
while (true) {
if (this.error) {
throw this.error;
}
if (this.queue.length > 0) {
const value = this.queue.shift();
if (value !== undefined) {
yield value;
}
continue;
}
if (this.done) {
break;
}
// Wait for next value
const result = await new Promise((resolve) => {
this.resolvers.push(resolve);
});
if (result.done) {
break;
}
yield result.value;
}
}
/**
* Transform the stream
*/
map(fn) {
const output = new AsyncStream();
(async () => {
try {
for await (const chunk of this) {
const transformed = await fn(chunk);
output.push(transformed);
}
output.end();
}
catch (error) {
output.throw(error);
}
})();
return output;
}
/**
* Filter the stream
*/
filter(predicate) {
const output = new AsyncStream();
(async () => {
try {
for await (const chunk of this) {
if (await predicate(chunk)) {
output.push(chunk);
}
}
output.end();
}
catch (error) {
output.throw(error);
}
})();
return output;
}
/**
* Take only n items
*/
take(n) {
const output = new AsyncStream();
(async () => {
try {
let count = 0;
for await (const chunk of this) {
if (count >= n)
break;
output.push(chunk);
count++;
}
output.end();
}
catch (error) {
output.throw(error);
}
})();
return output;
}
/**
* Collect all values into an array
*/
async collect() {
const result = [];
for await (const chunk of this) {
result.push(chunk);
}
return result;
}
/**
* Reduce the stream to a single value
*/
async reduce(fn, initial) {
let acc = initial;
for await (const chunk of this) {
acc = await fn(acc, chunk);
}
return acc;
}
/**
* Pipe to another stream
*/
pipeTo(destination) {
(async () => {
try {
for await (const chunk of this) {
destination.push(chunk);
}
destination.end();
}
catch (error) {
destination.throw(error);
}
})();
}
}
/**
* Create a stream from an array
*/
export function fromArray(array) {
const stream = new AsyncStream();
(async () => {
for (const item of array) {
stream.push(item);
}
stream.end();
})();
return stream;
}
/**
* Create a stream from an async iterable
*/
export function fromAsyncIterable(iterable) {
const stream = new AsyncStream();
(async () => {
try {
for await (const item of iterable) {
stream.push(item);
}
stream.end();
}
catch (error) {
stream.throw(error);
}
})();
return stream;
}
/**
* Merge multiple streams into one
*/
export function merge(...streams) {
const output = new AsyncStream();
let active = streams.length;
for (const stream of streams) {
(async () => {
try {
for await (const chunk of stream) {
output.push(chunk);
}
}
catch (error) {
output.throw(error);
return;
}
active--;
if (active === 0) {
output.end();
}
})();
}
return output;
}
/**
* Create a stream from a generator function
*/
export function fromGenerator(generator) {
return fromAsyncIterable(generator());
}
/**
* Create an interval stream that emits values periodically
*/
export function interval(ms, count) {
const stream = new AsyncStream();
let counter = 0;
const timer = setInterval(() => {
stream.push(counter++);
if (count !== undefined && counter >= count) {
clearInterval(timer);
stream.end();
}
}, ms);
return stream;
}
/**
* Stream for AI/LLM responses
*/
export class AIStream extends AsyncStream {
/**
* Send to SSE stream
*/
toSSE(sse) {
(async () => {
for await (const chunk of this) {
sse.message(chunk);
}
sse.close();
})();
}
/**
* Concatenate all chunks
*/
async text() {
const chunks = [];
for await (const chunk of this) {
chunks.push(chunk);
}
return chunks.join('');
}
/**
* Stream JSON objects
*/
async *json() {
let buffer = '';
for await (const chunk of this) {
buffer += chunk;
// Try to parse complete JSON objects
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
try {
yield JSON.parse(line);
}
catch {
// Not valid JSON, skip
}
}
}
}
// Try to parse remaining buffer
if (buffer.trim()) {
try {
yield JSON.parse(buffer);
}
catch {
// Ignore parse errors
}
}
}
}
/**
* Create an AI stream for streaming AI responses
*/
export function createAIStream() {
return new AIStream();
}
/**
* Transform stream - processes chunks through a transform function
*/
export class TransformStream {
input = new AsyncStream();
output = new AsyncStream();
constructor(transform) {
(async () => {
try {
for await (const chunk of this.input) {
const transformed = await transform(chunk);
this.output.push(transformed);
}
this.output.end();
}
catch (error) {
this.output.throw(error);
}
})();
}
/**
* Get the writable side
*/
writable() {
return this.input;
}
/**
* Get the readable side
*/
readable() {
return this.output;
}
}
/**
* Create a transform stream
*/
export function transform(fn) {
return new TransformStream(fn);
}
//# sourceMappingURL=index.js.map