smartstream/ts/smartstream.classes.smartduplex.ts

241 lines
6.9 KiB
TypeScript
Raw Normal View History

2023-11-01 13:16:58 +00:00
import * as plugins from './smartstream.plugins.js';
import { Duplex, type DuplexOptions } from 'stream';
2023-11-03 20:32:24 +00:00
export interface IStreamTools {
truncate: () => void;
2024-03-16 17:29:44 +00:00
push: (pipeObject: any) => Promise<boolean>;
2023-11-03 12:55:56 +00:00
}
2023-11-01 13:16:58 +00:00
2023-11-07 20:46:46 +00:00
export interface IStreamWriteFunction<T, rT> {
2023-11-03 20:32:24 +00:00
(chunkArg: T, toolsArg: IStreamTools): Promise<rT>;
}
2023-11-01 13:16:58 +00:00
2023-11-07 20:46:46 +00:00
export interface IStreamFinalFunction<rT> {
2023-11-03 20:32:24 +00:00
(toolsArg: IStreamTools): Promise<rT>;
}
2023-11-01 13:16:58 +00:00
2023-11-11 19:30:42 +00:00
export interface ISmartDuplexOptions<TInput, TOutput> extends DuplexOptions {
2024-02-25 19:14:19 +00:00
/**
* wether to print debug logs
*/
2023-11-13 16:43:15 +00:00
debug?: boolean;
2024-02-25 19:14:19 +00:00
/**
* the name of the stream
*/
2023-11-13 16:43:15 +00:00
name?: string;
2024-02-25 19:14:19 +00:00
/**
* a function that is being called to read more stuff from whereever to be processed by the stream
* @returns
*/
2023-11-03 20:32:24 +00:00
readFunction?: () => Promise<void>;
2024-02-25 19:14:19 +00:00
/**
* the write function is called for every chunk that is being written to the stream
* it can push or return chunks (but does not have to) to be written to the readable side of the stream
*/
2023-11-07 20:46:46 +00:00
writeFunction?: IStreamWriteFunction<TInput, TOutput>;
2024-02-25 19:14:19 +00:00
/**
* a final function that is run at the end of the stream
2024-02-25 19:14:19 +00:00
*/
2023-11-07 20:46:46 +00:00
finalFunction?: IStreamFinalFunction<TOutput>;
2023-11-03 20:32:24 +00:00
}
2023-11-01 13:16:58 +00:00
2023-11-03 20:32:24 +00:00
export class SmartDuplex<TInput = any, TOutput = any> extends Duplex {
// STATIC
2023-11-11 19:30:42 +00:00
static fromBuffer(buffer: Buffer, options?: ISmartDuplexOptions<any, any>): SmartDuplex {
const smartDuplex = new SmartDuplex(options);
2023-11-01 13:16:58 +00:00
process.nextTick(() => {
2023-11-11 19:30:42 +00:00
smartDuplex.push(buffer);
smartDuplex.push(null); // Signal the end of the data
2023-11-01 13:16:58 +00:00
});
2023-11-11 19:30:42 +00:00
return smartDuplex;
2023-11-01 13:16:58 +00:00
}
public static fromWebReadableStream<T = any>(
readableStream: ReadableStream<T>
): SmartDuplex<T, T> {
const smartDuplex = new SmartDuplex<T, T>({
/**
* this function is called whenever the stream is being read from and at the same time if nothing is enqueued
* therefor it is important to always unlock the reader after reading
*/
readFunction: async () => {
const reader = readableStream.getReader();
const { value, done } = await reader.read();
if (value !== undefined) {
smartDuplex.push(value);
}
reader.releaseLock();
if (done) {
smartDuplex.push(null);
}
},
});
return smartDuplex;
}
2023-11-03 20:32:24 +00:00
// INSTANCE
private backpressuredArray: plugins.lik.BackpressuredArray<TOutput>; // an array that only takes a defined amount of items
2023-11-13 16:43:15 +00:00
public options: ISmartDuplexOptions<TInput, TOutput>;
2023-11-03 20:32:24 +00:00
private observableSubscription?: plugins.smartrx.rxjs.Subscription;
2023-11-13 16:43:15 +00:00
private debugLog(messageArg: string) {
// optional debug log
2023-11-13 16:43:15 +00:00
if (this.options.debug) {
console.log(messageArg);
}
}
2023-11-03 20:32:24 +00:00
2023-11-11 19:30:42 +00:00
constructor(optionsArg?: ISmartDuplexOptions<TInput, TOutput>) {
super(
Object.assign(
{
highWaterMark: 1,
},
optionsArg
)
);
2023-11-13 16:43:15 +00:00
this.options = optionsArg;
this.backpressuredArray = new plugins.lik.BackpressuredArray<TOutput>(
this.options.highWaterMark || 1
);
2023-11-03 20:32:24 +00:00
}
public async _read(size: number): Promise<void> {
2023-11-13 16:43:15 +00:00
this.debugLog(`${this.options.name}: read was called`);
if (this.options.readFunction) {
await this.options.readFunction();
}
await this.backpressuredArray.waitForItems();
this.debugLog(`${this.options.name}: successfully waited for items.`);
let canPushMore = true;
while (this.backpressuredArray.data.length > 0 && canPushMore) {
2023-11-13 16:43:15 +00:00
const nextChunk = this.backpressuredArray.shift();
2023-11-13 18:12:23 +00:00
canPushMore = this.push(nextChunk);
2023-11-03 20:32:24 +00:00
}
}
public async backpressuredPush(pushArg: TOutput) {
2023-11-14 09:29:44 +00:00
const canPushMore = this.backpressuredArray.push(pushArg);
if (!canPushMore) {
this.debugLog(`${this.options.name}: cannot push more`);
await this.backpressuredArray.waitForSpace();
this.debugLog(`${this.options.name}: can push more again`);
}
2024-03-16 17:29:44 +00:00
return canPushMore;
}
2023-11-14 09:29:44 +00:00
2023-11-12 21:34:55 +00:00
private asyncWritePromiseObjectmap = new plugins.lik.ObjectMap<Promise<any>>();
2023-11-03 20:32:24 +00:00
// Ensure the _write method types the chunk as TInput and encodes TOutput
public async _write(chunk: TInput, encoding: string, callback: (error?: Error | null) => void) {
2023-11-13 16:43:15 +00:00
if (!this.options.writeFunction) {
2023-11-03 20:32:24 +00:00
return callback(new Error('No stream function provided'));
}
2023-11-13 16:43:15 +00:00
let isTruncated = false;
2023-11-03 20:32:24 +00:00
const tools: IStreamTools = {
truncate: () => {
this.push(null);
2023-11-13 16:43:15 +00:00
isTruncated = true;
2023-11-03 20:32:24 +00:00
callback();
},
2023-11-13 17:19:11 +00:00
push: async (pushArg: TOutput) => {
2024-03-16 17:29:44 +00:00
return await this.backpressuredPush(pushArg);
},
2023-11-03 20:32:24 +00:00
};
try {
2023-11-12 21:34:55 +00:00
const writeDeferred = plugins.smartpromise.defer();
this.asyncWritePromiseObjectmap.add(writeDeferred.promise);
2023-11-13 16:43:15 +00:00
const modifiedChunk = await this.options.writeFunction(chunk, tools);
if (isTruncated) {
return;
}
2023-11-03 20:32:24 +00:00
if (modifiedChunk) {
2023-11-13 17:19:11 +00:00
await tools.push(modifiedChunk);
2023-11-03 20:32:24 +00:00
}
2023-11-13 16:43:15 +00:00
callback();
2023-11-12 21:34:55 +00:00
writeDeferred.resolve();
writeDeferred.promise.then(() => {
this.asyncWritePromiseObjectmap.remove(writeDeferred.promise);
});
2023-11-03 20:32:24 +00:00
} catch (err) {
callback(err);
}
}
public async _final(callback: (error?: Error | null) => void) {
2023-11-12 21:34:55 +00:00
await Promise.all(this.asyncWritePromiseObjectmap.getArray());
2023-11-13 16:43:15 +00:00
if (this.options.finalFunction) {
2023-11-03 20:32:24 +00:00
const tools: IStreamTools = {
truncate: () => callback(),
2023-11-13 17:19:11 +00:00
push: async (pipeObject) => {
2024-03-16 17:29:44 +00:00
return this.backpressuredArray.push(pipeObject);
2023-11-13 17:19:11 +00:00
},
2023-11-03 20:32:24 +00:00
};
try {
2023-11-13 16:43:15 +00:00
const finalChunk = await this.options.finalFunction(tools);
2023-11-03 20:32:24 +00:00
if (finalChunk) {
2023-11-13 18:06:02 +00:00
this.backpressuredArray.push(finalChunk);
2023-11-03 20:32:24 +00:00
}
} catch (err) {
2023-11-13 18:06:02 +00:00
this.backpressuredArray.push(null);
2023-11-03 20:32:24 +00:00
callback(err);
2023-11-12 21:34:55 +00:00
return;
2023-11-03 20:32:24 +00:00
}
}
2023-11-13 18:06:02 +00:00
this.backpressuredArray.push(null);
2023-11-06 21:10:20 +00:00
callback();
2023-11-03 20:32:24 +00:00
}
public async getWebStreams(): Promise<{ readable: ReadableStream; writable: WritableStream }> {
const duplex = this;
const readable = new ReadableStream({
start(controller) {
duplex.on('readable', () => {
let chunk;
while (null !== (chunk = duplex.read())) {
controller.enqueue(chunk);
}
});
duplex.on('end', () => {
controller.close();
});
},
cancel(reason) {
duplex.destroy(new Error(reason));
},
});
const writable = new WritableStream({
write(chunk) {
return new Promise<void>((resolve, reject) => {
const isBackpressured = !duplex.write(chunk, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
if (isBackpressured) {
duplex.once('drain', resolve);
}
});
},
close() {
return new Promise<void>((resolve, reject) => {
duplex.end(resolve);
});
},
abort(reason) {
duplex.destroy(new Error(reason));
},
});
return { readable, writable };
}
2023-11-01 13:16:58 +00:00
}