askexperts
Version:
AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol
201 lines • 7.86 kB
JavaScript
import { SimplePool, getPublicKey } from "nostr-tools";
import { StreamWriter } from "../../../stream/StreamWriter.js";
import { debugError, debugStream, enableAllDebug } from "../../../common/debug.js";
import { parseStreamMetadataEvent } from "../../../stream/metadata.js";
/**
* A simple async queue that processes tasks sequentially
*/
class AsyncQueue {
constructor() {
this.queue = [];
this.processing = false;
}
/**
* Add a task to the queue and start processing if not already processing
*
* @param task Function that returns a promise
*/
async add(task) {
// Add the task to the queue
this.queue.push(task);
// Start processing if not already processing
if (!this.processing) {
await this.process();
}
}
/**
* Process tasks in the queue sequentially
*/
async process() {
if (this.processing)
return;
this.processing = true;
try {
// Process tasks until the queue is empty
while (this.queue.length > 0) {
const task = this.queue.shift();
if (task) {
try {
await task();
}
catch (err) {
debugError("Error processing task in queue:", err);
// Continue processing other tasks even if one fails
}
}
}
}
finally {
this.processing = false;
}
}
}
/**
* Execute the stream send command
*
* @param options Command line options
*/
export async function executeStreamSendCommand(options) {
try {
// Require metadata option
if (!options.metadata) {
throw new Error("Missing --metadata option. Stream metadata event is required.");
}
// Parse metadata event
let metadataEvent;
try {
metadataEvent = JSON.parse(options.metadata);
}
catch (err) {
throw new Error(`Invalid metadata event JSON: ${err instanceof Error ? err.message : String(err)}`);
}
// Parse and validate the metadata event
const metadata = parseStreamMetadataEvent(metadataEvent);
// Get sender private key from options
if (!options.privateKey) {
throw new Error("Missing --private-key option. The sender's private key is required.");
}
// Parse the private key
const senderPrivkey = Buffer.from(options.privateKey, 'hex');
// Verify that the private key corresponds to the stream ID
const derivedPublicKey = getPublicKey(Buffer.from(options.privateKey, 'hex'));
if (derivedPublicKey !== metadata.streamId) {
throw new Error("The provided private key does not match the stream ID in the metadata.");
}
// Create a SimplePool for relay communication
const pool = new SimplePool();
// Parse chunk size and interval options
const maxChunkSize = options.chunkSize ? parseInt(options.chunkSize, 10) : undefined;
const minChunkInterval = options.chunkInterval ? parseInt(options.chunkInterval, 10) : undefined;
// Create writer config
const writerConfig = {
maxChunkSize,
minChunkInterval,
};
// Validate encryption requirements
if (metadata.encryption === "nip44") {
if (!metadata.receiver_pubkey) {
throw new Error("Missing receiver_pubkey in metadata for NIP-44 encryption");
}
}
debugStream("Stream metadata received. Starting to stream from stdin...");
// Create stream writer
const writer = new StreamWriter(metadata, pool, senderPrivkey, writerConfig);
// Set up stdin to read data
if (!metadata.binary)
process.stdin.setEncoding('utf8');
// Create an async queue to process stdin events sequentially
const taskQueue = new AsyncQueue();
// Read data from stdin and write to stream
let index = 0;
process.stdin.on('data', (chunk) => {
const i = index;
index++;
// Add the task to the queue
taskQueue.add(async () => {
// Drop the chunk if we're already failing
if (writer.status === 'error')
return;
debugStream(`Read chunk ${i} of ${chunk.length} bytes`);
try {
await writer.write(metadata.binary ? chunk : chunk.toString("utf8"));
debugStream(`Sent chunk ${i}`);
}
catch (err) {
debugError("Error writing to stream:", err);
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
});
});
// Handle end of input
process.stdin.on('end', () => {
// Add the end task to the queue
taskQueue.add(async () => {
// Drop the chunk if we're already failing
if (writer.status === 'error')
return;
debugStream(`Read end`);
try {
// Write final chunk with done flag
await writer.write("", true);
debugStream("Stream completed successfully.");
// Clean up resources
writer[Symbol.dispose]();
pool.destroy();
process.exit(0);
}
catch (err) {
debugError("Error completing stream:", err);
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
});
});
// Handle errors
process.stdin.on('error', (err) => {
// Add the error task to the queue
taskQueue.add(async () => {
debugError("Error reading from stdin:", err);
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
try {
// Send error status
await writer.error("stdin_error", err.message || "Error reading from stdin");
}
catch (writeErr) {
debugError("Error sending error status:", writeErr);
}
// Clean up resources
writer[Symbol.dispose]();
pool.destroy();
process.exit(1);
});
});
}
catch (err) {
debugError("Failed to execute stream send command:", err);
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}
/**
* Register the send command
*
* @param streamCommand The parent stream command
* @param addCommonOptions Function to add common options to command
*/
export function registerSendCommand(streamCommand, addCommonOptions) {
const sendCommand = streamCommand
.command("send")
.description("Send data from stdin using an existing stream")
.requiredOption("-m, --metadata <json>", "Stream metadata JSON")
.requiredOption("-k, --private-key <hex>", "Sender's private key in hex format")
.option("--chunk-size <bytes>", "Maximum chunk size in bytes", "65536")
.option("--chunk-interval <ms>", "Minimum interval between chunks in milliseconds", "0")
.action((options) => {
if (options.debug)
enableAllDebug();
executeStreamSendCommand(options);
});
addCommonOptions(sendCommand);
}
//# sourceMappingURL=send.js.map