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-29 11:15:00 +00:00
|
|
|
|
2024-02-25 19:14:19 +00:00
|
|
|
/**
|
2024-02-29 11:15:00 +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
|
|
|
}
|
|
|
|
|
2023-11-03 20:32:24 +00:00
|
|
|
// INSTANCE
|
2023-11-13 19:34:21 +00:00
|
|
|
private backpressuredArray: plugins.lik.BackpressuredArray<TOutput>;
|
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) {
|
|
|
|
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>) {
|
2023-11-13 19:34:21 +00:00
|
|
|
super(Object.assign({
|
2023-11-14 09:51:23 +00:00
|
|
|
highWaterMark: 1,
|
2023-11-13 19:34:21 +00:00
|
|
|
}, optionsArg));
|
2023-11-13 16:43:15 +00:00
|
|
|
this.options = optionsArg;
|
2023-11-14 09:51:23 +00:00
|
|
|
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`);
|
2023-11-13 16:52:11 +00:00
|
|
|
await this.backpressuredArray.waitForItems();
|
|
|
|
this.debugLog(`${this.options.name}: successfully waited for items.`);
|
2023-11-13 16:43:15 +00:00
|
|
|
if (this.options.readFunction) {
|
|
|
|
await this.options.readFunction();
|
|
|
|
}
|
|
|
|
let canPushMore = true;
|
|
|
|
while(this.backpressuredArray.data.length > 0 && canPushMore) {
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 09:29:44 +00:00
|
|
|
public async backpressuredPush (pushArg: TOutput) {
|
|
|
|
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-14 09:29:44 +00:00
|
|
|
}
|
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
|
|
|
}
|
2024-06-02 21:40:52 +00:00
|
|
|
|
2024-06-03 08:27:07 +00:00
|
|
|
public async getWebStreams(): Promise<{ readable: ReadableStream, writable: WritableStream }> {
|
2024-06-02 21:40:52 +00:00
|
|
|
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
|
|
|
}
|