**feat: enhance WebStreams API compatibility and refactor BrainyData integration**

### Changes:
- Introduced environment-independent WebStreams API compatibility:
  - Added `initializeStreamClasses` for dynamic detection of WebStreams API support in browsers and Node.js.
  - Implemented fallback mechanism with detailed error messages for unsupported environments.
  - Ensured asynchronous initialization of stream classes without blocking module execution.
- Updated `SequentialPipeline`:
  - Added `ensureStreamClassesInitialized` method to streamline WebStreams setup across operations.
  - Enhanced `initialize` to parallelize stream classes and `BrainyData` initialization.
  - Refactored WebSocket handlers (`createWebSocketHandler` and `createWebSocketStreams`) to support async stream initialization.
  - Improved TypeScript typings for streams (`TransformStreamDefaultController`, `PipelineResult<unknown>`).
- Adjusted `ServerSearchConduitAugmentation`:
  - Introduced dependency on `BrainyDataInterface` instead of direct `BrainyData` usage.
  - Ensured `localDb` is set prior to initialization with an appropriate error message.
  - Updated methods (`setLocalDb`, `getLocalDb`, and vector operations) to rely on `BrainyDataInterface`.
- Enhanced error handling and logging throughout streaming and augmentation processes.

### Purpose:
Improved cross-environment compatibility of WebStreams API, ensuring seamless usage in both browsers and Node.js. Refactored `ServerSearchConduitAugmentation` to decouple `BrainyData` dependency,
This commit is contained in:
David Snelling 2025-06-19 15:03:17 -07:00
parent 5f0d9f4b0a
commit 25f7ab73b4
2 changed files with 69 additions and 20 deletions

View file

@ -15,7 +15,7 @@ import {
} from '../types/augmentations.js' } from '../types/augmentations.js'
import { WebSocketConduitAugmentation } from './conduitAugmentations.js' import { WebSocketConduitAugmentation } from './conduitAugmentations.js'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { BrainyData } from '../brainyData.js' import { BrainyDataInterface } from '../types/brainyDataInterface.js'
/** /**
* ServerSearchConduitAugmentation * ServerSearchConduitAugmentation
@ -24,7 +24,7 @@ import { BrainyData } from '../brainyData.js'
* a server-hosted Brainy instance and storing results locally. * a server-hosted Brainy instance and storing results locally.
*/ */
export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentation { export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentation {
private localDb: BrainyData | null = null private localDb: BrainyDataInterface | null = null
constructor(name: string = 'server-search-conduit') { constructor(name: string = 'server-search-conduit') {
super(name) super(name)
@ -43,10 +43,9 @@ export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentatio
// Initialize the base conduit // Initialize the base conduit
await super.initialize() await super.initialize()
// Initialize local Brainy instance if not provided // Local DB must be set before initialization
if (!this.localDb) { if (!this.localDb) {
this.localDb = new BrainyData() throw new Error('Local database not set. Call setLocalDb before initializing.')
await this.localDb.init()
} }
this.isInitialized = true this.isInitialized = true
@ -60,7 +59,7 @@ export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentatio
* Set the local Brainy instance * Set the local Brainy instance
* @param db The Brainy instance to use for local storage * @param db The Brainy instance to use for local storage
*/ */
setLocalDb(db: BrainyData): void { setLocalDb(db: BrainyDataInterface): void {
this.localDb = db this.localDb = db
} }
@ -68,7 +67,7 @@ export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentatio
* Get the local Brainy instance * Get the local Brainy instance
* @returns The local Brainy instance * @returns The local Brainy instance
*/ */
getLocalDb(): BrainyData | null { getLocalDb(): BrainyDataInterface | null {
return this.localDb return this.localDb
} }
@ -265,7 +264,7 @@ export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentatio
const id = await this.localDb.add(data, metadata) const id = await this.localDb.add(data, metadata)
// Get the vector and metadata // Get the vector and metadata
const noun = await this.localDb.get(id) const noun = await this.localDb.get(id) as import('../coreTypes.js').VectorDocument<unknown>
if (!noun) { if (!noun) {
return { return {
@ -620,7 +619,7 @@ export async function createServerSearchAugmentations(
conduitName?: string, conduitName?: string,
activationName?: string, activationName?: string,
protocols?: string | string[], protocols?: string | string[],
localDb?: BrainyData localDb?: BrainyDataInterface
} = {} } = {}
): Promise<{ ): Promise<{
conduit: ServerSearchConduitAugmentation, conduit: ServerSearchConduitAugmentation,

View file

@ -23,8 +23,39 @@ import {
} from './types/augmentations.js' } from './types/augmentations.js'
import { BrainyData } from './brainyData.js' import { BrainyData } from './brainyData.js'
import { augmentationPipeline } from './augmentationPipeline.js' import { augmentationPipeline } from './augmentationPipeline.js'
// Using Node.js 23.11+ native WebStreams API // Use the browser's built-in WebStreams API or Node.js native WebStreams API
import { TransformStream, ReadableStream, WritableStream } from 'node:stream/web' // This approach ensures compatibility with both environments
let TransformStream: any, ReadableStream: any, WritableStream: any;
// Function to initialize the stream classes
const initializeStreamClasses = () => {
// Try to use the browser's built-in WebStreams API first
if (typeof globalThis.TransformStream !== 'undefined' &&
typeof globalThis.ReadableStream !== 'undefined' &&
typeof globalThis.WritableStream !== 'undefined') {
TransformStream = globalThis.TransformStream;
ReadableStream = globalThis.ReadableStream;
WritableStream = globalThis.WritableStream;
return Promise.resolve();
} else {
// In Node.js environment, try to import from node:stream/web
// This will be executed in Node.js but not in browsers
return import('node:stream/web')
.then(streamWebModule => {
TransformStream = streamWebModule.TransformStream;
ReadableStream = streamWebModule.ReadableStream;
WritableStream = streamWebModule.WritableStream;
})
.catch(error => {
console.error('Failed to import WebStreams API:', error);
// Provide fallback implementations or throw a more helpful error
throw new Error('WebStreams API is not available in this environment. Please use a modern browser or Node.js 18+.');
});
}
};
// Initialize immediately but don't block module execution
const streamClassesPromise = initializeStreamClasses();
/** /**
* Options for sequential pipeline execution * Options for sequential pipeline execution
@ -104,13 +135,25 @@ export class SequentialPipeline {
this.brainyData = options.brainyData || new BrainyData(); this.brainyData = options.brainyData || new BrainyData();
} }
/**
* Ensure stream classes are initialized
* @private
*/
private async ensureStreamClassesInitialized(): Promise<void> {
await streamClassesPromise;
}
/** /**
* Initialize the pipeline * Initialize the pipeline
* *
* @returns A promise that resolves when initialization is complete * @returns A promise that resolves when initialization is complete
*/ */
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
await this.brainyData.init(); // Initialize stream classes and BrainyData in parallel
await Promise.all([
this.ensureStreamClassesInitialized(),
this.brainyData.init()
]);
} }
/** /**
@ -310,14 +353,17 @@ export class SequentialPipeline {
* @param options Options for pipeline execution * @param options Options for pipeline execution
* @returns A function to handle incoming WebSocket messages * @returns A function to handle incoming WebSocket messages
*/ */
public createWebSocketHandler( public async createWebSocketHandler(
connection: WebSocketConnection, connection: WebSocketConnection,
dataType: string, dataType: string,
options: SequentialPipelineOptions = {} options: SequentialPipelineOptions = {}
): (data: unknown) => void { ): Promise<(data: unknown) => void> {
// Ensure stream classes are initialized
await this.ensureStreamClassesInitialized();
// Create a transform stream for processing data // Create a transform stream for processing data
const transformStream = new TransformStream({ const transformStream = new TransformStream({
transform: async (chunk, controller) => { transform: async (chunk: unknown, controller: TransformStreamDefaultController) => {
try { try {
const data = typeof chunk === 'string' ? chunk : JSON.stringify(chunk); const data = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
const result = await this.processData(data, dataType, options); const result = await this.processData(data, dataType, options);
@ -334,7 +380,7 @@ export class SequentialPipeline {
// Create a writable stream that will be the sink for our data // Create a writable stream that will be the sink for our data
const writableStream = new WritableStream({ const writableStream = new WritableStream({
write: async (result) => { write: async (result: PipelineResult<unknown>) => {
// Handle the processed result if needed // Handle the processed result if needed
if (connection.send && typeof connection.send === 'function') { if (connection.send && typeof connection.send === 'function') {
try { try {
@ -384,6 +430,9 @@ export class SequentialPipeline {
readableStream?: ReadableStream<unknown>, readableStream?: ReadableStream<unknown>,
writableStream?: WritableStream<unknown> writableStream?: WritableStream<unknown>
}> { }> {
// Ensure stream classes are initialized
await this.ensureStreamClassesInitialized();
// Get WebSocket-supporting augmentations // Get WebSocket-supporting augmentations
const webSocketAugmentations = augmentationPipeline.getWebSocketAugmentations(); const webSocketAugmentations = augmentationPipeline.getWebSocketAugmentations();
@ -399,7 +448,7 @@ export class SequentialPipeline {
// Create a readable stream from the WebSocket messages // Create a readable stream from the WebSocket messages
const readableStream = new ReadableStream({ const readableStream = new ReadableStream({
start: (controller) => { start: (controller: ReadableStreamDefaultController) => {
// Define a message handler that writes to the stream // Define a message handler that writes to the stream
const messageHandler = (event: { data: unknown }) => { const messageHandler = (event: { data: unknown }) => {
try { try {
@ -461,11 +510,11 @@ export class SequentialPipeline {
}); });
// Create a handler for processing the data // Create a handler for processing the data
const handler = this.createWebSocketHandler(connection, dataType, options); const handlerPromise = this.createWebSocketHandler(connection, dataType, options);
// Create a writable stream that sends data to the WebSocket // Create a writable stream that sends data to the WebSocket
const writableStream = new WritableStream({ const writableStream = new WritableStream({
write: async (chunk) => { write: async (chunk: unknown) => {
if (connection.send && typeof connection.send === 'function') { if (connection.send && typeof connection.send === 'function') {
try { try {
const data = typeof chunk === 'string' ? chunk : JSON.stringify(chunk); const data = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
@ -491,8 +540,9 @@ export class SequentialPipeline {
// Pipe the readable stream through our processing pipeline // Pipe the readable stream through our processing pipeline
readableStream readableStream
.pipeThrough(new TransformStream({ .pipeThrough(new TransformStream({
transform: async (chunk, controller) => { transform: async (chunk: unknown, controller: TransformStreamDefaultController) => {
// Process each chunk through our handler // Process each chunk through our handler
const handler = await handlerPromise;
handler(chunk); handler(chunk);
// Pass through the original data // Pass through the original data
controller.enqueue(chunk); controller.enqueue(chunk);