2025-09-11 16:23:32 -07:00
/ * *
* Streaming Pipeline System for Brainy
*
* Real implementation of streaming data pipelines with :
* - Async iterators for streaming
* - Backpressure handling
* - Auto - scaling workers
* - Checkpointing for recovery
* - Error boundaries
* /
import { Brainy } from '../brainy.js'
import { NounType } from '../types/graphTypes.js'
/ * *
* Pipeline stage types
* /
export type StageType = 'source' | 'transform' | 'filter' | 'batch' | 'sink' | 'branch' | 'merge' | 'window' | 'reduce'
/ * *
* Pipeline execution options
* /
export interface PipelineOptions {
workers? : number | 'auto'
checkpoint? : boolean | string
monitoring? : boolean
maxThroughput? : number
backpressure ? : 'drop' | 'buffer' | 'pause'
retries? : number
errorHandler ? : ( error : Error , item : any ) = > void
bufferSize? : number
}
/ * *
* Base interface for pipeline stages
* /
export interface PipelineStage < T = any , R = any > {
type : StageType
name : string
process ( input : AsyncIterable < T > ) : AsyncIterable < R >
}
/ * *
* Streaming Pipeline Builder
* /
export class Pipeline < T = any > {
private stages : PipelineStage [ ] = [ ]
private running = false
private abortController? : AbortController
private metrics = {
processed : 0 ,
errors : 0 ,
startTime : 0 ,
throughput : 0
}
constructor ( private brainyInstance? : Brainy | Brainy < any > ) { }
2026-06-11 14:51:00 -07:00
/ * *
* Re - brand this pipeline ' s element type after a stage that changes the
* stream ' s item type has been pushed . The runtime object is unchanged —
* the fluent builder mutates ` stages ` on the same instance rather than
* allocating a new pipeline , and ` Pipeline<T> ` ' s type parameter exists
* only at compile time . ( Typed boundary : ` Pipeline<T> ` and ` Pipeline<R> `
* are not structurally comparable generics , so this is the one sanctioned
* ` unknown ` bridge in this class . )
* /
private retype < R > ( ) : Pipeline < R > {
return this as unknown as Pipeline < R >
}
2025-09-11 16:23:32 -07:00
/ * *
* Add a data source
* /
source < S > ( generator : AsyncIterable < S > | ( ( ) = > AsyncIterable < S > ) | AsyncGeneratorFunction ) : Pipeline < S > {
const stage : PipelineStage < any , S > = {
type : 'source' ,
name : 'source' ,
async * process ( ) : AsyncIterable < S > {
const source = typeof generator === 'function' ? generator ( ) : generator
for await ( const item of source ) {
yield item as S
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < S > ( )
2025-09-11 16:23:32 -07:00
}
2026-06-11 14:51:00 -07:00
2025-09-11 16:23:32 -07:00
/ * *
* Transform data
* /
map < R > ( fn : ( item : T ) = > R | Promise < R > ) : Pipeline < R > {
const stage : PipelineStage < T , R > = {
type : 'transform' ,
name : 'map' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < R > {
for await ( const item of input ) {
yield await fn ( item )
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < R > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Filter data
* /
filter ( predicate : ( item : T ) = > boolean | Promise < boolean > ) : Pipeline < T > {
const stage : PipelineStage < T , T > = {
type : 'filter' ,
name : 'filter' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < T > {
for await ( const item of input ) {
if ( await predicate ( item ) ) {
yield item
}
}
}
}
this . stages . push ( stage )
return this
}
/ * *
* Batch items for efficiency
* /
batch ( size : number , timeoutMs? : number ) : Pipeline < T [ ] > {
const stage : PipelineStage < T , T [ ] > = {
type : 'batch' ,
name : 'batch' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < T [ ] > {
let batch : T [ ] = [ ]
let timer : NodeJS.Timeout | null = null
const flush = ( ) = > {
if ( batch . length > 0 ) {
const result = [ . . . batch ]
batch = [ ]
return result
}
return null
}
for await ( const item of input ) {
batch . push ( item )
if ( batch . length >= size ) {
const result = flush ( )
if ( result ) yield result
} else if ( timeoutMs && ! timer ) {
timer = setTimeout ( ( ) = > {
timer = null
const result = flush ( )
if ( result ) {
// Note: This won't work perfectly in async iterator
// In production, use a proper queue
2026-06-11 14:51:00 -07:00
// Typed boundary preserving a pre-existing quirk verbatim:
// re-stashing the flushed batch as a single element nests it
// (a T[] stored where T is expected), so a later size-flush
// can yield a nested array. Documented, not fixed — fixing it
// (batch = result) would change runtime output.
batch = [ result ] as unknown as T [ ]
2025-09-11 16:23:32 -07:00
}
} , timeoutMs )
}
}
// Flush remaining
const result = flush ( )
if ( result ) yield result
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < T [ ] > ( )
2025-09-11 16:23:32 -07:00
}
2026-06-11 14:51:00 -07:00
2025-09-11 16:23:32 -07:00
/ * *
* Sink data to a destination
* /
sink ( handler : ( item : T ) = > Promise < void > | void ) : Pipeline < void > {
const stage : PipelineStage < T , void > = {
type : 'sink' ,
name : 'sink' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < void > {
for await ( const item of input ) {
await handler ( item )
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < void > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Sink data to Brainy
* /
toBrainy ( options ? : {
type ? : string
metadata? : any
batchSize? : number
} ) : Pipeline < void > {
if ( ! this . brainyInstance ) {
throw new Error ( 'Brainy instance required for toBrainy sink' )
}
const brain = this . brainyInstance
const batchSize = options ? . batchSize || 100
return this . batch ( batchSize ) . sink ( async ( batch : T [ ] ) = > {
// Handle both Brainy 3.0 and Brainy APIs
if ( 'add' in brain ) {
// Brainy 3.0 API
for ( const item of batch ) {
await ( brain as Brainy < any > ) . add ( {
data : item ,
2026-06-11 14:51:00 -07:00
// Type coercion since pipeline accepts string
type : ( options ? . type as NounType | undefined ) || NounType . Document ,
2025-09-11 16:23:32 -07:00
metadata : options?.metadata
} )
}
} else {
// Brainy API - use add method
for ( const item of batch ) {
await ( brain as Brainy ) . add ( {
data : item ,
2026-06-11 14:51:00 -07:00
type : ( options ? . type || 'document' ) as NounType , // Type coercion since pipeline accepts string
2025-09-11 16:23:32 -07:00
metadata : options?.metadata
} )
}
}
2026-06-11 14:51:00 -07:00
} )
2025-09-11 16:23:32 -07:00
}
/ * *
* Sink with rate limiting
* /
throttledSink (
handler : ( item : T ) = > Promise < void > | void ,
rateLimit : number
) : Pipeline < void > {
let lastTime = Date . now ( )
const minInterval = 1000 / rateLimit
const stage : PipelineStage < T , void > = {
type : 'sink' ,
name : 'throttledSink' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < void > {
for await ( const item of input ) {
const now = Date . now ( )
const elapsed = now - lastTime
if ( elapsed < minInterval ) {
await new Promise ( resolve = >
setTimeout ( resolve , minInterval - elapsed )
)
}
await handler ( item )
lastTime = Date . now ( )
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < void > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Parallel sink with worker pool
* /
parallelSink (
handler : ( item : T ) = > Promise < void > | void ,
workers = 4
) : Pipeline < void > {
const stage : PipelineStage < T , void > = {
type : 'sink' ,
name : 'parallelSink' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < void > {
const queue : Promise < void > [ ] = [ ]
for await ( const item of input ) {
// Add to queue
const promise = Promise . resolve ( handler ( item ) )
queue . push ( promise )
// Maintain worker pool size
if ( queue . length >= workers ) {
await Promise . race ( queue )
// Remove completed promises
for ( let i = queue . length - 1 ; i >= 0 ; i -- ) {
if ( await Promise . race ( [ queue [ i ] , Promise . resolve ( 'pending' ) ] ) !== 'pending' ) {
queue . splice ( i , 1 )
}
}
}
}
// Wait for remaining work
await Promise . all ( queue )
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < void > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Collect all results
* /
async collect ( ) : Promise < T [ ] > {
const results : T [ ] = [ ]
await this . sink ( async item = > {
results . push ( item )
} ) . run ( )
return results
}
/ * *
* Window operations for time - based processing
* /
window ( size : number , type : 'tumbling' | 'sliding' = 'tumbling' ) : Pipeline < T [ ] > {
const stage : PipelineStage < T , T [ ] > = {
type : 'window' ,
name : 'window' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < T [ ] > {
const window : T [ ] = [ ]
for await ( const item of input ) {
window . push ( item )
if ( type === 'sliding' ) {
if ( window . length > size ) {
window . shift ( )
}
if ( window . length === size ) {
yield [ . . . window ]
}
} else {
// Tumbling window
if ( window . length >= size ) {
yield [ . . . window ]
window . length = 0
}
}
}
// Emit remaining items
if ( window . length > 0 && type === 'tumbling' ) {
yield window
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < T [ ] > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Flatmap operation - map and flatten results
* /
flatMap < R > ( fn : ( item : T ) = > R [ ] | Promise < R [ ] > ) : Pipeline < R > {
const stage : PipelineStage < T , R > = {
type : 'transform' ,
name : 'flatMap' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < R > {
for await ( const item of input ) {
const results = await fn ( item )
for ( const result of results ) {
yield result
}
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < R > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Tap into the pipeline for side effects without modifying data
* /
tap ( fn : ( item : T ) = > void | Promise < void > ) : Pipeline < T > {
const stage : PipelineStage < T , T > = {
type : 'transform' ,
name : 'tap' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < T > {
for await ( const item of input ) {
await fn ( item )
yield item
}
}
}
this . stages . push ( stage )
return this
}
/ * *
* Retry failed operations
* /
retry < R > (
fn : ( item : T ) = > R | Promise < R > ,
maxRetries = 3 ,
backoff = 1000
) : Pipeline < R > {
const stage : PipelineStage < T , R > = {
type : 'transform' ,
name : 'retry' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < R > {
for await ( const item of input ) {
let retries = 0
let lastError : Error | undefined
while ( retries <= maxRetries ) {
try {
yield await fn ( item )
break
} catch ( error ) {
lastError = error as Error
retries ++
if ( retries <= maxRetries ) {
await new Promise ( resolve = >
setTimeout ( resolve , backoff * Math . pow ( 2 , retries - 1 ) )
)
}
}
}
if ( retries > maxRetries && lastError ) {
throw lastError
}
}
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < R > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Buffer with backpressure handling
* /
buffer ( size : number , strategy : 'drop' | 'block' = 'block' ) : Pipeline < T > {
const stage : PipelineStage < T , T > = {
type : 'transform' ,
name : 'buffer' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < T > {
const buffer : T [ ] = [ ]
let consuming = false
const consume = async function * ( ) {
while ( buffer . length > 0 ) {
yield buffer . shift ( ) !
}
}
for await ( const item of input ) {
if ( buffer . length >= size ) {
if ( strategy === 'drop' ) {
// Drop oldest item
buffer . shift ( )
} else {
// Block until buffer has space
if ( ! consuming ) {
consuming = true
for await ( const buffered of consume ( ) ) {
yield buffered
if ( buffer . length < size / 2 ) break
}
consuming = false
}
}
}
buffer . push ( item )
}
// Flush remaining buffer
for ( const item of buffer ) {
yield item
}
}
}
this . stages . push ( stage )
return this
}
/ * *
* Fork the pipeline into multiple branches
* /
fork ( . . . branches : Array < ( pipeline : Pipeline < T > ) = > Pipeline < any > > ) : Pipeline < T > {
const brainyRef = this . brainyInstance
const stage : PipelineStage < T , T > = {
type : 'branch' ,
name : 'fork' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < T > {
const buffers : T [ ] [ ] = branches . map ( ( ) = > [ ] )
for await ( const item of input ) {
// Distribute items to all branches
for ( let i = 0 ; i < branches . length ; i ++ ) {
buffers [ i ] . push ( item )
}
yield item
}
// Process branches in parallel
await Promise . all ( branches . map ( async ( branch , i ) = > {
const branchPipeline = new Pipeline < T > ( brainyRef )
const configured = branch ( branchPipeline )
// Create async iterable from buffer
const source = async function * ( ) {
for ( const item of buffers [ i ] ) {
yield item
}
}
await configured . source ( source ( ) ) . run ( )
} ) )
}
}
this . stages . push ( stage )
return this
}
/ * *
* Reduce operation
* /
reduce < R > (
reducer : ( acc : R , item : T ) = > R ,
initial : R
) : Pipeline < R > {
const stage : PipelineStage < T , R > = {
type : 'reduce' ,
name : 'reduce' ,
async * process ( input : AsyncIterable < T > ) : AsyncIterable < R > {
let accumulator = initial
for await ( const item of input ) {
accumulator = reducer ( accumulator , item )
}
yield accumulator
}
}
this . stages . push ( stage )
2026-06-11 14:51:00 -07:00
return this . retype < R > ( )
2025-09-11 16:23:32 -07:00
}
/ * *
* Run the pipeline with metrics tracking
* /
async run ( options : PipelineOptions = { } ) : Promise < void > {
if ( this . running ) {
throw new Error ( 'Pipeline is already running' )
}
this . running = true
this . abortController = new AbortController ( )
this . metrics . startTime = Date . now ( )
this . metrics . processed = 0
this . metrics . errors = 0
const { errorHandler , bufferSize = 1000 } = options
try {
2026-06-11 14:51:00 -07:00
// Build the pipeline chain. The head of the chain has no upstream:
// source stages declare process() without parameters and never read
// their input, so the undefined fed to them (and to a malformed
// pipeline whose first stage isn't a source — preserved legacy
// behavior) is typed at this boundary as the AsyncIterable it never is.
const noUpstream = undefined as unknown as AsyncIterable < unknown >
let stream : AsyncIterable < unknown > = noUpstream
2025-09-11 16:23:32 -07:00
for ( const stage of this . stages ) {
if ( stage . type === 'source' ) {
2026-06-11 14:51:00 -07:00
stream = stage . process ( noUpstream )
2025-09-11 16:23:32 -07:00
} else {
stream = stage . process ( stream )
}
}
// Execute the pipeline with error handling
if ( stream ) {
for await ( const item of stream ) {
try {
this . metrics . processed ++
// Calculate throughput
const elapsed = ( Date . now ( ) - this . metrics . startTime ) / 1000
this . metrics . throughput = this . metrics . processed / elapsed
// Check abort signal
if ( this . abortController . signal . aborted ) {
break
}
// Backpressure handling
if ( options . maxThroughput && this . metrics . throughput > options . maxThroughput ) {
const delay = 1000 / options . maxThroughput
await new Promise ( resolve = > setTimeout ( resolve , delay ) )
}
} catch ( error ) {
this . metrics . errors ++
if ( errorHandler ) {
errorHandler ( error as Error , item )
} else {
throw error
}
}
}
}
} finally {
this . running = false
this . abortController = undefined
// Log final metrics
if ( options . monitoring ) {
const elapsed = ( Date . now ( ) - this . metrics . startTime ) / 1000
console . log ( ` Pipeline completed: ${ this . metrics . processed } items in ${ elapsed . toFixed ( 2 ) } s ` )
console . log ( ` Throughput: ${ this . metrics . throughput . toFixed ( 2 ) } items/sec ` )
if ( this . metrics . errors > 0 ) {
console . log ( ` Errors: ${ this . metrics . errors } ` )
}
}
}
}
/ * *
* Start the pipeline ( alias for run )
* /
async start ( options : PipelineOptions = { } ) : Promise < void > {
return this . run ( options )
}
/ * *
* Stop the pipeline
* /
stop ( ) : void {
if ( this . abortController ) {
this . abortController . abort ( )
}
}
/ * *
* Monitor pipeline metrics
* /
monitor ( dashboard? : string ) : Pipeline < T > {
// In production, this would connect to monitoring service
console . log ( ` Monitoring enabled ${ dashboard ? ` with dashboard: ${ dashboard } ` : '' } ` )
return this
}
}
/ * *
* Pipeline factory function
* /
export function createPipeline ( brain? : Brainy ) : Pipeline {
return new Pipeline ( brain )
}
/ * *
* Backward compatibility exports
* /
export const pipeline = createPipeline ( )
// Execution modes for backward compatibility (deprecated)
export enum ExecutionMode {
SEQUENTIAL = 'sequential' ,
PARALLEL = 'parallel' ,
FIRST_SUCCESS = 'firstSuccess' ,
FIRST_RESULT = 'firstResult' ,
THREADED = 'threaded'
}
// Type exports for backward compatibility
export type PipelineResult < T > = { success : boolean ; data : T ; error? : string }
export type StreamlinedPipelineOptions = PipelineOptions
export type StreamlinedPipelineResult < T > = PipelineResult < T >
export { ExecutionMode as StreamlinedExecutionMode }