diff --git a/src/brainyData.ts b/src/brainyData.ts index ac8d1a86..b2860b38 100644 --- a/src/brainyData.ts +++ b/src/brainyData.ts @@ -37,6 +37,11 @@ import { WebSocketConnection } from './types/augmentations.js' import { BrainyDataInterface } from './types/brainyDataInterface.js' export interface BrainyDataConfig { + /** + * Vector dimensions (required if not using an embedding function that auto-detects dimensions) + */ + dimensions?: number + /** * HNSW index configuration */ @@ -136,16 +141,48 @@ export class BrainyData implements BrainyDataInterface { private readOnly: boolean private storageConfig: BrainyDataConfig['storage'] = {} private useOptimizedIndex: boolean = false + private _dimensions: number // Remote server properties private remoteServerConfig: BrainyDataConfig['remoteServer'] | null = null private serverSearchConduit: ServerSearchConduitAugmentation | null = null private serverConnection: WebSocketConnection | null = null + /** + * Get the vector dimensions + */ + public get dimensions(): number { + return this._dimensions + } + + /** + * Get the maximum connections parameter from HNSW configuration + */ + public get maxConnections(): number { + const config = this.index.getConfig() + return config.M || 16 + } + + /** + * Get the efConstruction parameter from HNSW configuration + */ + public get efConstruction(): number { + const config = this.index.getConfig() + return config.efConstruction || 200 + } + /** * Create a new vector database */ constructor(config: BrainyDataConfig = {}) { + // Validate dimensions + if (config.dimensions !== undefined && config.dimensions <= 0) { + throw new Error('Dimensions must be a positive number') + } + + // Set dimensions (default to 512 for embedding functions, or require explicit config) + this._dimensions = config.dimensions || 512 + // Set distance function this.distanceFunction = config.distanceFunction || cosineDistance @@ -284,6 +321,16 @@ export class BrainyData implements BrainyDataInterface { // Clear the index and add all nouns this.index.clear() for (const noun of nouns) { + // Check if the vector dimensions match the expected dimensions + if (noun.vector.length !== this._dimensions) { + console.warn( + `Skipping noun ${noun.id} due to dimension mismatch: expected ${this._dimensions}, got ${noun.vector.length}` + ) + // Optionally, you could delete the mismatched noun from storage + // await this.storage!.deleteNoun(noun.id) + continue + } + // Add to index await this.index.addItem({ id: noun.id, @@ -393,6 +440,11 @@ export class BrainyData implements BrainyDataInterface { throw new Error('Vector is undefined or null') } + // Validate vector dimensions + if (vector.length !== this._dimensions) { + throw new Error(`Vector dimension mismatch: expected ${this._dimensions}, got ${vector.length}`) + } + // Use ID from options if it exists, otherwise from metadata, otherwise generate a new UUID const id = options.id || @@ -431,7 +483,13 @@ export class BrainyData implements BrainyDataInterface { } } - await this.storage!.saveMetadata(id, metadata) + // Ensure metadata has the correct id field + let metadataToSave = metadata + if (metadata && typeof metadata === 'object') { + metadataToSave = { ...metadata, id } + } + + await this.storage!.saveMetadata(id, metadataToSave) } // If addToRemote is true and we're connected to a remote server, add to remote as well @@ -452,6 +510,26 @@ export class BrainyData implements BrainyDataInterface { } } + /** + * Add a text item to the database with automatic embedding + * This is a convenience method for adding text data with metadata + * @param text Text data to add + * @param metadata Metadata to associate with the text + * @param options Additional options + * @returns The ID of the added item + */ + public async addItem( + text: string, + metadata?: T, + options: { + addToRemote?: boolean // Whether to also add to the remote server if connected + id?: string // Optional ID to use instead of generating a new one + } = {} + ): Promise { + // Use the existing add method with forceEmbed to ensure text is embedded + return this.add(text, metadata, { ...options, forceEmbed: true }) + } + /** * Add data to both local and remote Brainy instances * @param vectorOrData Vector or data to add @@ -685,7 +763,9 @@ export class BrainyData implements BrainyDataInterface { forceEmbed?: boolean // Force using the embedding function even if input is a vector } = {} ): Promise[]> { - await this.ensureInitialized() + if (!this.isInitialized) { + throw new Error('BrainyData must be initialized before searching. Call init() first.') + } try { let queryVector: Vector @@ -814,6 +894,9 @@ export class BrainyData implements BrainyDataInterface { verbDirection?: 'outgoing' | 'incoming' | 'both' // Direction of verbs to consider when searching connected nouns } = {} ): Promise[]> { + if (!this.isInitialized) { + throw new Error('BrainyData must be initialized before searching. Call init() first.') + } // If searching for verbs directly if (options.searchVerbs) { const verbResults = await this.searchVerbs(queryVectorOrData, k, { @@ -873,6 +956,9 @@ export class BrainyData implements BrainyDataInterface { includeVerbs?: boolean // Whether to include associated GraphVerbs in the results } = {} ): Promise[]> { + if (!this.isInitialized) { + throw new Error('BrainyData must be initialized before searching. Call init() first.') + } // If input is a string and not a vector, automatically vectorize it let queryToUse = queryVectorOrData if (typeof queryVectorOrData === 'string' && !options.forceEmbed) { diff --git a/src/patched-platform-node.ts b/src/patched-platform-node.ts new file mode 100644 index 00000000..77e51340 --- /dev/null +++ b/src/patched-platform-node.ts @@ -0,0 +1,52 @@ +/** + * Custom patched implementation of TensorFlow.js PlatformNode class + * This resolves the TextEncoder/TextDecoder constructor issues + */ + +export class PatchedPlatformNode { + util: { + TextEncoder: typeof TextEncoder + TextDecoder: typeof TextDecoder + isFloat32Array: (arr: any) => boolean + isTypedArray: (arr: any) => boolean + } + textEncoder: TextEncoder + textDecoder: TextDecoder + + constructor() { + // Create a util object with necessary methods + this.util = { + // Add isFloat32Array and isTypedArray directly to util + isFloat32Array: (arr) => { + return !!( + arr instanceof Float32Array || + (arr && + Object.prototype.toString.call(arr) === '[object Float32Array]') + ) + }, + isTypedArray: (arr) => { + return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView)) + }, + // Provide constructor references directly + TextEncoder, + TextDecoder + } + + // Initialize TextEncoder/TextDecoder instances + this.textEncoder = new TextEncoder() + this.textDecoder = new TextDecoder() + } + + // Define isFloat32Array directly on the instance + isFloat32Array(arr: any) { + return !!( + arr instanceof Float32Array || + (arr && Object.prototype.toString.call(arr) === '[object Float32Array]') + ) + } + + // Define isTypedArray directly on the instance + isTypedArray(arr: any) { + return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView)) + } +} diff --git a/src/setup.ts b/src/setup.ts index c89b1459..6868bbe7 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -15,40 +15,32 @@ * tree-shaking by bundlers, ensuring the patch is always applied. */ -// CRITICAL: Apply the TensorFlow.js patch immediately at the top level -// This ensures it runs as early as possible in the module loading process -// before any imports are processed -if ( - typeof process !== 'undefined' && - process.versions && - process.versions.node -) { - try { - // For CommonJS environments, use require to ensure synchronous loading - if (typeof require === 'function') { - const textEncoding = require('./utils/textEncoding.js') - if ( - textEncoding && - typeof textEncoding.applyTensorFlowPatch === 'function' - ) { - textEncoding.applyTensorFlowPatch() - console.log( - 'Applied TensorFlow.js patch via CommonJS require in setup.ts' - ) - } - } - } catch (e) { - console.warn('Failed to apply TensorFlow.js patch via require:', e) - // Continue to the import-based approach +// Get the appropriate global object for the current environment +const globalObj = (() => { + if (typeof globalThis !== 'undefined') return globalThis + if (typeof global !== 'undefined') return global + if (typeof self !== 'undefined') return self + return null // No global object available +})() + +// Define TextEncoder and TextDecoder globally to make sure they're available +// Now works across all environments: Node.js, serverless, and other server environments +if (globalObj) { + if (!globalObj.TextEncoder) { + globalObj.TextEncoder = TextEncoder } + if (!globalObj.TextDecoder) { + globalObj.TextDecoder = TextDecoder + } + + // Create a special global constructor that TensorFlow can use safely + ;(globalObj as any).__TextEncoder__ = TextEncoder + ;(globalObj as any).__TextDecoder__ = TextDecoder } // Also import normally for ES modules environments import { applyTensorFlowPatch } from './utils/textEncoding.js' -// Apply the TensorFlow.js platform patch if needed -// This will be a no-op if the patch was already applied via require above +// Apply the TensorFlow.js platform patch applyTensorFlowPatch() -console.log( - 'Applied or verified TensorFlow.js patch via ES modules in setup.ts' -) +console.log('Applied TensorFlow.js patch via ES modules in setup.ts') diff --git a/src/storage/fileSystemStorage.ts b/src/storage/fileSystemStorage.ts index 8a448f98..83493f67 100644 --- a/src/storage/fileSystemStorage.ts +++ b/src/storage/fileSystemStorage.ts @@ -413,6 +413,7 @@ export class FileSystemStorage implements StorageAdapter { public async saveMetadata(id: string, metadata: any): Promise { if (!this.isInitialized) await this.init() const filePath = path.join(this.metadataDir, `${id}.json`) + await this.ensureDirectoryExists(path.dirname(filePath)) await fs.promises.writeFile(filePath, JSON.stringify(metadata, null, 2)) } @@ -437,7 +438,60 @@ export class FileSystemStorage implements StorageAdapter { public async clear(): Promise { if (!this.isInitialized) await this.init() - await fs.promises.rm(this.rootDir, { recursive: true, force: true }) + + // Helper function to recursively remove directory contents + const removeDirectoryContents = async (dirPath: string): Promise => { + try { + const files = await fs.promises.readdir(dirPath, { withFileTypes: true }) + + for (const file of files) { + const fullPath = path.join(dirPath, file.name) + + if (file.isDirectory()) { + await removeDirectoryContents(fullPath) + // Use fs.promises.rm with recursive option instead of rmdir + try { + await fs.promises.rm(fullPath, { recursive: true, force: true }) + } catch (rmError: any) { + // Fallback to rmdir if rm fails + await fs.promises.rmdir(fullPath) + } + } else { + await fs.promises.unlink(fullPath) + } + } + } catch (error: any) { + if (error.code !== 'ENOENT') { + console.error(`Error removing directory contents ${dirPath}:`, error) + throw error + } + } + } + + try { + // First try the modern approach + await fs.promises.rm(this.rootDir, { recursive: true, force: true }) + } catch (error: any) { + console.warn('Modern rm failed, falling back to manual cleanup:', error) + + // Fallback: manually remove contents then directory + try { + await removeDirectoryContents(this.rootDir) + // Use fs.promises.rm with recursive option instead of rmdir + try { + await fs.promises.rm(this.rootDir, { recursive: true, force: true }) + } catch (rmError: any) { + // Final fallback to rmdir if rm fails + await fs.promises.rmdir(this.rootDir) + } + } catch (fallbackError: any) { + if (fallbackError.code !== 'ENOENT') { + console.error('Manual cleanup also failed:', fallbackError) + throw fallbackError + } + } + } + this.isInitialized = false // Reset state await this.init() // Re-create directories } diff --git a/src/unified.ts b/src/unified.ts index be00581f..188a3e69 100644 --- a/src/unified.ts +++ b/src/unified.ts @@ -16,16 +16,20 @@ // available to TensorFlow.js before it initializes its platform detection import './setup.js' -// Export environment information +// Import environment detection functions +import { isBrowser, isNode } from './utils/environment.js' + +// Export environment information with lazy evaluation export const environment = { - isBrowser: typeof window !== 'undefined', - isNode: - typeof process !== 'undefined' && process.versions && process.versions.node, - isServerless: - typeof window === 'undefined' && - (typeof process === 'undefined' || - !process.versions || - !process.versions.node) + get isBrowser() { + return isBrowser() + }, + get isNode() { + return isNode() + }, + get isServerless() { + return !isBrowser() && !isNode() + } } // Make environment information available globally @@ -46,3 +50,6 @@ console.log( // Re-export everything from index.ts export * from './index.js' + +// Export the TensorFlow patch function for testing and manual use +export { applyTensorFlowPatch } from './utils/textEncoding.js' diff --git a/src/utils/distance.ts b/src/utils/distance.ts index dc3a8e2c..4cbcd096 100644 --- a/src/utils/distance.ts +++ b/src/utils/distance.ts @@ -145,32 +145,28 @@ export async function calculateDistancesBatch( // In worker context, use the importTensorFlow function tf = await self.importTensorFlow() } else { - // CRITICAL: First, directly import the setup module to ensure the TensorFlow.js patch is applied - // This is the most reliable way to ensure the patch is applied before TensorFlow.js is loaded + // CRITICAL: Ensure TextEncoder/TextDecoder are available before TensorFlow.js loads try { - // In Node.js environment, use require() which is synchronous - if (typeof require !== 'undefined') { - // First, require the setup module to apply the patch - require('../setup.js') - - // Now load TensorFlow.js core module - tf = require('@tensorflow/tfjs-core') - - // Load CPU backend - require('@tensorflow/tfjs-backend-cpu') - - // Set CPU as the backend - tf.setBackend('cpu') - } else { - // In browser or other environments without require(), use dynamic imports - // First, dynamically import the setup module to apply the patch - await import('../setup.js') - - // Now load TensorFlow.js core module - tf = await import('@tensorflow/tfjs-core') - await import('@tensorflow/tfjs-backend-cpu') - await tf.setBackend('cpu') + // Use dynamic imports for all environments to ensure TensorFlow loads after patch + if (typeof process !== 'undefined' && process.versions && process.versions.node) { + // Ensure TextEncoder/TextDecoder are globally available in Node.js + const util = await import('util') + if (typeof global.TextEncoder === 'undefined') { + global.TextEncoder = util.TextEncoder + } + if (typeof global.TextDecoder === 'undefined') { + global.TextDecoder = util.TextDecoder + } } + + // Apply the TensorFlow.js patch + const { applyTensorFlowPatch } = await import('./textEncoding.js') + await applyTensorFlowPatch() + + // Now load TensorFlow.js core module using dynamic imports + tf = await import('@tensorflow/tfjs-core') + await import('@tensorflow/tfjs-backend-cpu') + await tf.setBackend('cpu') } catch (error) { console.error('Failed to initialize TensorFlow.js:', error) throw error diff --git a/src/utils/embedding.ts b/src/utils/embedding.ts index 4648d35a..6b4a2cd0 100644 --- a/src/utils/embedding.ts +++ b/src/utils/embedding.ts @@ -22,52 +22,163 @@ export class UniversalSentenceEncoder implements EmbeddingModel { /** * Add polyfills and patches for TensorFlow.js compatibility - * This addresses issues with TensorFlow.js in Node.js environments - * + * This addresses issues with TensorFlow.js across all server environments + * (Node.js, serverless, and other server environments) + * * Note: The main TensorFlow.js patching is now centralized in textEncoding.ts * and applied through setup.ts. This method only adds additional utility functions * that might be needed by TensorFlow.js. */ - private addNodeCompatibilityPolyfills(): void { - // Only apply in Node.js environment - if ( - typeof process === 'undefined' || - !process.versions || - !process.versions.node - ) { - return + private addServerCompatibilityPolyfills(): void { + // Apply in all non-browser environments (Node.js, serverless, server environments) + const isBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined' + if (isBrowserEnv) { + return // Browser environments don't need these polyfills } - // Add polyfill for isFloat32Array in Node.js 24.4.0 - // This fixes the "Cannot read properties of undefined (reading 'isFloat32Array')" error - if (typeof global !== 'undefined') { + // Get the appropriate global object for the current environment + const globalObj = (() => { + if (typeof globalThis !== 'undefined') return globalThis + if (typeof global !== 'undefined') return global + if (typeof self !== 'undefined') return self + return {} as any // Fallback for unknown environments + })() + + // Add polyfill for utility functions across all server environments + // This fixes issues like "Cannot read properties of undefined (reading 'isFloat32Array')" + try { + // Ensure the util object exists + if (!globalObj.util) { + globalObj.util = {} + } + + // Add isFloat32Array method if it doesn't exist + if (!globalObj.util.isFloat32Array) { + globalObj.util.isFloat32Array = (obj: any) => { + return !!( + obj instanceof Float32Array || + (obj && + Object.prototype.toString.call(obj) === '[object Float32Array]') + ) + } + } + + // Add isTypedArray method if it doesn't exist + if (!globalObj.util.isTypedArray) { + globalObj.util.isTypedArray = (obj: any) => { + return !!(ArrayBuffer.isView(obj) && !(obj instanceof DataView)) + } + } + } catch (error) { + console.warn('Failed to add utility polyfills:', error) + } + } + + /** + * Check if we're running in a test environment + */ + private isTestEnvironment(): boolean { + // Safely check for Node.js environment first + if (typeof process === 'undefined') { + return false + } + + return ( + process.env.NODE_ENV === 'test' || + process.env.VITEST === 'true' || + (typeof global !== 'undefined' && global.__vitest__) || + process.argv.some((arg) => arg.includes('vitest')) + ) + } + + /** + * Log message only if not in test environment + */ + private logIfNotTest( + level: 'log' | 'warn' | 'error', + message: string, + ...args: any[] + ): void { + if (!this.isTestEnvironment()) { + console[level](message, ...args) + } + } + + /** + * Load the Universal Sentence Encoder model with retry logic + * This helps handle network failures and JSON parsing errors from TensorFlow Hub + * @param loadFunction The function to load the model + * @param maxRetries Maximum number of retry attempts + * @param baseDelay Base delay in milliseconds for exponential backoff + */ + private async loadModelWithRetry( + loadFunction: () => Promise, + maxRetries: number = 3, + baseDelay: number = 1000 + ): Promise { + let lastError: Error | null = null + + for (let attempt = 0; attempt <= maxRetries; attempt++) { try { - // Ensure the util object exists - if (!global.util) { - global.util = {} + this.logIfNotTest( + 'log', + attempt === 0 + ? 'Loading Universal Sentence Encoder model...' + : `Retrying Universal Sentence Encoder model loading (attempt ${attempt + 1}/${maxRetries + 1})...` + ) + + const model = await loadFunction() + + if (attempt > 0) { + this.logIfNotTest( + 'log', + 'Universal Sentence Encoder model loaded successfully after retry' + ) } - // Add isFloat32Array method if it doesn't exist - if (!global.util.isFloat32Array) { - global.util.isFloat32Array = (obj: any) => { - return !!( - obj instanceof Float32Array || - (obj && - Object.prototype.toString.call(obj) === '[object Float32Array]') + return model + } catch (error) { + lastError = error as Error + const errorMessage = lastError.message || String(lastError) + + // Check if this is a network-related error that might benefit from retry + const isRetryableError = + errorMessage.includes('Failed to parse model JSON') || + errorMessage.includes('Failed to fetch') || + errorMessage.includes('Network error') || + errorMessage.includes('ENOTFOUND') || + errorMessage.includes('ECONNRESET') || + errorMessage.includes('ETIMEDOUT') || + errorMessage.includes('JSON') || + errorMessage.includes('model.json') + + if (attempt < maxRetries && isRetryableError) { + const delay = baseDelay * Math.pow(2, attempt) // Exponential backoff + this.logIfNotTest( + 'warn', + `Universal Sentence Encoder model loading failed (attempt ${attempt + 1}): ${errorMessage}. Retrying in ${delay}ms...` + ) + await new Promise((resolve) => setTimeout(resolve, delay)) + } else { + // Either we've exhausted retries or this is not a retryable error + if (attempt >= maxRetries) { + this.logIfNotTest( + 'error', + `Universal Sentence Encoder model loading failed after ${maxRetries + 1} attempts. Last error: ${errorMessage}` + ) + } else { + this.logIfNotTest( + 'error', + `Universal Sentence Encoder model loading failed with non-retryable error: ${errorMessage}` ) } + throw lastError } - - // Add isTypedArray method if it doesn't exist - if (!global.util.isTypedArray) { - global.util.isTypedArray = (obj: any) => { - return !!(ArrayBuffer.isView(obj) && !(obj instanceof DataView)) - } - } - } catch (error) { - console.warn('Failed to add utility polyfills:', error) } } + + // This should never be reached, but just in case + throw lastError || new Error('Unknown error during model loading') } /** @@ -93,31 +204,62 @@ export class UniversalSentenceEncoder implements EmbeddingModel { } // Add polyfills for TensorFlow.js compatibility - this.addNodeCompatibilityPolyfills() + this.addServerCompatibilityPolyfills() // TensorFlow.js will use its default EPSILON value - // CRITICAL: First, directly import the setup module to ensure the TensorFlow.js patch is applied - // This is the most reliable way to ensure the patch is applied before TensorFlow.js is loaded + // CRITICAL: Ensure TextEncoder/TextDecoder are available before TensorFlow.js loads try { - // In Node.js environment, use require() which is synchronous - if (typeof require !== 'undefined') { - // First, require the setup module to apply the patch - require('../setup.js') + // Get the appropriate global object for the current environment + const globalObj = (() => { + if (typeof globalThis !== 'undefined') return globalThis + if (typeof global !== 'undefined') return global + if (typeof self !== 'undefined') return self + return null + })() - // Now load TensorFlow.js core module - this.tf = require('@tensorflow/tfjs-core') + // Ensure TextEncoder/TextDecoder are globally available in server environments + if (globalObj) { + // Try to use Node.js util module if available (Node.js environments) + try { + if (typeof process !== 'undefined' && process.versions && process.versions.node) { + const util = await import('util') + if (!globalObj.TextEncoder) { + globalObj.TextEncoder = util.TextEncoder + } + if (!globalObj.TextDecoder) { + globalObj.TextDecoder = util.TextDecoder + } + } + } catch (utilError) { + // Fallback to standard TextEncoder/TextDecoder for non-Node.js server environments + if (!globalObj.TextEncoder) { + globalObj.TextEncoder = TextEncoder + } + if (!globalObj.TextDecoder) { + globalObj.TextDecoder = TextDecoder + } + } + } - // Load CPU backend (always needed as fallback) - require('@tensorflow/tfjs-backend-cpu') + // Apply the TensorFlow.js patch + const { applyTensorFlowPatch } = await import('./textEncoding.js') + await applyTensorFlowPatch() - // Try to load WebGL backend for GPU acceleration in browser environments + // Now load TensorFlow.js core module using dynamic imports + this.tf = await import('@tensorflow/tfjs-core') + + // Import CPU backend (always needed as fallback) + await import('@tensorflow/tfjs-backend-cpu') + + // Try to import WebGL backend for GPU acceleration in browser environments + try { if (typeof window !== 'undefined') { + await import('@tensorflow/tfjs-backend-webgl') + // Check if WebGL is available try { - require('@tensorflow/tfjs-backend-webgl') - // Check if WebGL is available if (this.tf.setBackend) { - this.tf.setBackend('webgl') + await this.tf.setBackend('webgl') this.backend = 'webgl' console.log('Using WebGL backend for TensorFlow.js') } else { @@ -133,58 +275,18 @@ export class UniversalSentenceEncoder implements EmbeddingModel { this.backend = 'cpu' } } - - // Load Universal Sentence Encoder - this.use = require('@tensorflow-models/universal-sentence-encoder') - } else { - // In browser or other environments without require(), use dynamic imports - // First, dynamically import the setup module to apply the patch - await import('../setup.js') - - // Now load TensorFlow.js core module - this.tf = await import('@tensorflow/tfjs-core') - - // Import CPU backend (always needed as fallback) - await import('@tensorflow/tfjs-backend-cpu') - - // Try to import WebGL backend for GPU acceleration in browser environments - try { - if (typeof window !== 'undefined') { - await import('@tensorflow/tfjs-backend-webgl') - // Check if WebGL is available - try { - if (this.tf.setBackend) { - await this.tf.setBackend('webgl') - this.backend = 'webgl' - console.log('Using WebGL backend for TensorFlow.js') - } else { - console.warn( - 'tf.setBackend is not available, falling back to CPU' - ) - } - } catch (e) { - console.warn( - 'WebGL backend not available, falling back to CPU:', - e - ) - this.backend = 'cpu' - } - } - } catch (error) { - console.warn( - 'WebGL backend not available, falling back to CPU:', - error - ) - this.backend = 'cpu' - } - - // Load Universal Sentence Encoder - this.use = await import( - '@tensorflow-models/universal-sentence-encoder' + } catch (error) { + console.warn( + 'WebGL backend not available, falling back to CPU:', + error ) + this.backend = 'cpu' } + + // Load Universal Sentence Encoder using dynamic import + this.use = await import('@tensorflow-models/universal-sentence-encoder') } catch (error) { - console.error('Failed to initialize TensorFlow.js:', error) + this.logIfNotTest('error', 'Failed to initialize TensorFlow.js:', error) throw error } @@ -209,14 +311,18 @@ export class UniversalSentenceEncoder implements EmbeddingModel { ) } - // Load the model - this.model = await loadFunction() + // Load the model with retry logic for network failures + this.model = await this.loadModelWithRetry(loadFunction) this.initialized = true // Restore original console.warn console.warn = originalWarn } catch (error) { - console.error('Failed to initialize Universal Sentence Encoder:', error) + this.logIfNotTest( + 'error', + 'Failed to initialize Universal Sentence Encoder:', + error + ) throw new Error( `Failed to initialize Universal Sentence Encoder: ${error}` ) @@ -272,7 +378,8 @@ export class UniversalSentenceEncoder implements EmbeddingModel { return embeddingArray[0] } catch (error) { - console.error( + this.logIfNotTest( + 'error', 'Failed to embed text with Universal Sentence Encoder:', error ) @@ -336,7 +443,8 @@ export class UniversalSentenceEncoder implements EmbeddingModel { return results } catch (error) { - console.error( + this.logIfNotTest( + 'error', 'Failed to batch embed text with Universal Sentence Encoder:', error ) @@ -357,7 +465,11 @@ export class UniversalSentenceEncoder implements EmbeddingModel { this.tf.disposeVariables() this.initialized = false } catch (error) { - console.error('Failed to dispose Universal Sentence Encoder:', error) + this.logIfNotTest( + 'error', + 'Failed to dispose Universal Sentence Encoder:', + error + ) } } return Promise.resolve() @@ -471,13 +583,48 @@ function findUSELoadFunction( return loadFunction } +/** + * Check if we're running in a test environment (standalone version) + */ +function isTestEnvironment(): boolean { + // Safely check for Node.js environment first + if (typeof process === 'undefined') { + return false + } + + return ( + process.env.NODE_ENV === 'test' || + process.env.VITEST === 'true' || + (typeof global !== 'undefined' && global.__vitest__) || + process.argv.some((arg) => arg.includes('vitest')) + ) +} + +/** + * Log message only if not in test environment (standalone version) + */ +function logIfNotTest( + level: 'log' | 'warn' | 'error', + message: string, + ...args: any[] +): void { + if (!isTestEnvironment()) { + console[level](message, ...args) + } +} + /** * Create an embedding function from an embedding model - * @param model Embedding model to use + * @param model Embedding model to use (optional, defaults to UniversalSentenceEncoder) */ export function createEmbeddingFunction( - model: EmbeddingModel + model?: EmbeddingModel ): EmbeddingFunction { + // If no model is provided, use the default TensorFlow embedding function + if (!model) { + return createTensorFlowEmbeddingFunction() + } + return async (data: any): Promise => { return await model.embed(data) } @@ -497,13 +644,19 @@ export function createTensorFlowEmbeddingFunction(): EmbeddingFunction { try { // Initialize the model if it hasn't been initialized yet if (!sharedModelInitialized) { - await sharedModel.init() - sharedModelInitialized = true + try { + await sharedModel.init() + sharedModelInitialized = true + } catch (initError) { + // Reset the flag so we can retry initialization on the next call + sharedModelInitialized = false + throw initError + } } return await sharedModel.embed(data) } catch (error) { - console.error('Failed to use TensorFlow embedding:', error) + logIfNotTest('error', 'Failed to use TensorFlow embedding:', error) throw new Error( `Universal Sentence Encoder is required but failed: ${error}` ) @@ -543,7 +696,7 @@ export const defaultBatchEmbeddingFunction: ( return await sharedBatchModel.embedBatch(dataArray) } catch (error) { - console.error('Failed to use TensorFlow batch embedding:', error) + logIfNotTest('error', 'Failed to use TensorFlow batch embedding:', error) throw new Error( `Universal Sentence Encoder batch embedding failed: ${error}` ) diff --git a/src/utils/environment.ts b/src/utils/environment.ts index 4be9d23d..88bc9302 100644 --- a/src/utils/environment.ts +++ b/src/utils/environment.ts @@ -13,6 +13,12 @@ export function isBrowser(): boolean { * Check if code is running in a Node.js environment */ export function isNode(): boolean { + // If browser environment is detected, prioritize it over Node.js + // This handles cases like jsdom where both window and process exist + if (isBrowser()) { + return false + } + return ( typeof process !== 'undefined' && process.versions != null && diff --git a/src/utils/textEncoding.ts b/src/utils/textEncoding.ts index ff13d633..641e2fc4 100644 --- a/src/utils/textEncoding.ts +++ b/src/utils/textEncoding.ts @@ -1,224 +1,341 @@ -// In: @soulcraft/brainy/src/utils/textEncoding.ts +import { isNode } from './environment.js' -/** - * Checks if the code is running in a Node.js environment. - */ -function isNode(): boolean { - return ( - typeof process !== 'undefined' && - process.versions != null && - process.versions.node != null - ) +// This module must be run BEFORE any TensorFlow.js code initializes +// It directly patches the global environment to fix TextEncoder/TextDecoder issues + +// Extend the global type definitions to include our custom properties +declare global { + let _utilShim: any + let __TextEncoder__: typeof TextEncoder + let __TextDecoder__: typeof TextDecoder + let __brainy_util__: any + let __utilShim: any +} + +// Also extend the globalThis interface +interface GlobalThis { + _utilShim?: any + __TextEncoder__?: typeof TextEncoder + __TextDecoder__?: typeof TextDecoder + __brainy_util__?: any + __utilShim?: any } -/** - * Global flag to track if TensorFlow.js has been initialized - * This helps prevent multiple registrations of the same kernels - */ -const TENSORFLOW_INITIALIZED = Symbol('TENSORFLOW_INITIALIZED') - /** * Flag to track if the patch has been applied - * This prevents multiple applications of the patch */ let patchApplied = false /** - * CRITICAL: Applies a compatibility patch for TensorFlow.js when running in a modern - * Node.js ES Module environment. This must be called before any TensorFlow.js - * modules are imported. - * - * This function prevents the "TextEncoder is not a constructor" error by preemptively - * creating a compliant PlatformNode class with proper TextEncoder/TextDecoder support - * and placing it on the global object where TensorFlow.js expects to find it. - * - * The race condition occurs because TensorFlow.js's platform detection might run - * before the necessary global objects are properly initialized in certain Node.js - * environments, particularly when the package is being used by other applications. - * - * This function is called from setup.ts, which must be the first import in unified.ts - * to ensure the patch is applied before any TensorFlow.js code is executed. - * - * It also applies a patch to prevent duplicate kernel registrations when TensorFlow.js - * is imported multiple times. + * Monkeypatch TensorFlow.js's PlatformNode class to fix TextEncoder/TextDecoder issues + * CRITICAL: This runs immediately at the top level when this module is imported */ -export function applyTensorFlowPatch(): void { - // Prevent multiple applications of the patch - if (patchApplied) { - return - } - - if (!isNode()) { - return // Patch is only for Node.js - } - - // In modern Node.js with ES Modules, TensorFlow.js can fail during its - // initial platform detection. This patch preempts that logic by creating - // a compliant "Platform" class that uses the standard global TextEncoder - // and placing it on the global object where TensorFlow.js expects to find it. +if (typeof globalThis !== 'undefined' && isNode()) { try { - // Ensure TextEncoder and TextDecoder are available - const nodeUtil = require('util') - const TextEncoderPolyfill = nodeUtil.TextEncoder || global.TextEncoder - const TextDecoderPolyfill = nodeUtil.TextDecoder || global.TextDecoder - - if (!TextEncoderPolyfill || !TextDecoderPolyfill) { - console.warn( - 'Brainy: TextEncoder or TextDecoder not available, attempting to polyfill' - ) - - // If still not available, try to use a simple polyfill - if (!TextEncoderPolyfill) { - class SimpleTextEncoder { - encode(input: string): Uint8Array { - const buf = Buffer.from(input, 'utf8') - return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) - } - } - - global.TextEncoder = SimpleTextEncoder - } - - if (!TextDecoderPolyfill) { - class SimpleTextDecoder { - decode(input?: Uint8Array): string { - if (!input) return '' - return Buffer.from( - input.buffer, - input.byteOffset, - input.byteLength - ).toString('utf8') - } - } - - global.TextDecoder = SimpleTextDecoder - } - } else { - // Ensure they're available globally - global.TextEncoder = TextEncoderPolyfill - global.TextDecoder = TextDecoderPolyfill + // Ensure TextEncoder/TextDecoder are globally available + if (typeof globalThis.TextEncoder === 'undefined') { + globalThis.TextEncoder = TextEncoder + } + if (typeof globalThis.TextDecoder === 'undefined') { + globalThis.TextDecoder = TextDecoder } - // Create a PlatformNode implementation that uses the polyfilled TextEncoder/TextDecoder - class BrainyPlatformNode { - // Use the polyfilled TextEncoder/TextDecoder - readonly util = { - TextEncoder: global.TextEncoder, - TextDecoder: global.TextDecoder, - - // Add utility functions that TensorFlow.js might need - isTypedArray: (arr: any): boolean => { - return ArrayBuffer.isView(arr) && !(arr instanceof DataView) - }, - - isFloat32Array: (arr: any): boolean => { - return ( - arr instanceof Float32Array || - (arr && - Object.prototype.toString.call(arr) === '[object Float32Array]') - ) - } + // Patch global objects to handle the TensorFlow.js constructor issue + // This is needed because TF accesses TextEncoder/TextDecoder as constructors via this.util + if (typeof global !== 'undefined') { + if (!global.TextEncoder) { + global.TextEncoder = TextEncoder } - - // Create instances of the encoder/decoder - readonly textEncoder: any - readonly textDecoder: any - - constructor() { - try { - // Initialize encoders using constructors - this.textEncoder = new global.TextEncoder() - this.textDecoder = new global.TextDecoder() - } catch (e) { - console.warn( - 'Brainy: Error creating TextEncoder/TextDecoder instances:', - e - ) - // Provide fallback implementations if instantiation fails - this.textEncoder = { - encode: (input: string): Uint8Array => { - const buf = Buffer.from(input, 'utf8') - return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) - } - } - this.textDecoder = { - decode: (input?: Uint8Array): string => { - if (!input) return '' - return Buffer.from( - input.buffer, - input.byteOffset, - input.byteLength - ).toString('utf8') - } - } - } + if (!global.TextDecoder) { + global.TextDecoder = TextDecoder } + // Also set the special global constructors that TensorFlow can use safely + global.__TextEncoder__ = TextEncoder + global.__TextDecoder__ = TextDecoder + } - isTypedArray(arr: any): arr is Float32Array | Int32Array | Uint8Array { - return ArrayBuffer.isView(arr) && !(arr instanceof DataView) - } - - isFloat32Array(arr: any): arr is Float32Array { - return ( - arr instanceof Float32Array || - (arr && - Object.prototype.toString.call(arr) === '[object Float32Array]') - ) + // CRITICAL FIX: Create a custom util object that TensorFlow.js can use + // We'll make this available globally so TensorFlow.js can find it + const customUtil = { + TextEncoder: TextEncoder, + TextDecoder: TextDecoder, + types: { + isFloat32Array: (arr: any) => arr instanceof Float32Array, + isInt32Array: (arr: any) => arr instanceof Int32Array, + isUint8Array: (arr: any) => arr instanceof Uint8Array, + isUint8ClampedArray: (arr: any) => arr instanceof Uint8ClampedArray } } - // Assign the custom platform class to the global scope. - // TensorFlow.js specifically looks for `PlatformNode`. - global.PlatformNode = BrainyPlatformNode + // Make the custom util available globally + if (typeof global !== 'undefined') { + global.__brainy_util__ = customUtil + } - // Also create an instance and assign it to global.platformNode (lowercase p) - // This is needed for some TensorFlow.js versions - global.platformNode = new BrainyPlatformNode() + // Try to patch the global require cache if possible + if ( + typeof global !== 'undefined' && + global.require && + global.require.cache + ) { + // Find the util module in the cache and patch it + for (const key in global.require.cache) { + if (key.endsWith('/util.js') || key === 'util') { + const utilModule = global.require.cache[key] + if (utilModule && utilModule.exports) { + Object.assign(utilModule.exports, customUtil) + } + } + } + } - // Set up a global flag to track TensorFlow.js initialization - global[TENSORFLOW_INITIALIZED] = false + // CRITICAL: Patch the Node.js util module directly + try { + const util = require('util') + // Ensure TextEncoder and TextDecoder are available as constructors + util.TextEncoder = TextEncoder as typeof util.TextEncoder + util.TextDecoder = TextDecoder as typeof util.TextDecoder + } catch (error) { + // Ignore if util module is not available + } - // Monkey patch the registerKernel function to prevent duplicate registrations - // This will be applied when TensorFlow.js is imported - const originalRegisterKernel = global.registerKernel - if (!originalRegisterKernel) { - // Set up a handler to intercept the registerKernel function when it's defined - Object.defineProperty(global, 'registerKernel', { - set: function (newRegisterKernel) { - // Replace the setter with our patched version - Object.defineProperty(global, 'registerKernel', { - value: function (kernel: any) { - // Check if this kernel is already registered - const kernelName = kernel.kernelName - const backendName = kernel.backendName - const key = `${kernelName}_${backendName}` + // CRITICAL: Patch Float32Array to handle buffer alignment issues + // This fixes the "byte length of Float32Array should be a multiple of 4" error + if (typeof global !== 'undefined') { + const originalFloat32Array = global.Float32Array - // Use a global registry to track registered kernels - if (!global.__REGISTERED_KERNELS__) { - global.__REGISTERED_KERNELS__ = new Set() - } + global.Float32Array = class extends originalFloat32Array { + constructor(arg?: any, byteOffset?: number, length?: number) { + if (arg instanceof ArrayBuffer) { + // Ensure buffer is properly aligned for Float32Array (multiple of 4 bytes) + const alignedByteOffset = byteOffset || 0 + const alignedLength = + length !== undefined + ? length + : (arg.byteLength - alignedByteOffset) / 4 - // If this kernel is already registered, skip it - if (global.__REGISTERED_KERNELS__.has(key)) { - return - } + // Check if the buffer slice is properly aligned + if ( + (arg.byteLength - alignedByteOffset) % 4 !== 0 && + length === undefined + ) { + // Create a new aligned buffer if the original isn't properly aligned + const alignedByteLength = + Math.floor((arg.byteLength - alignedByteOffset) / 4) * 4 + const alignedBuffer = new ArrayBuffer(alignedByteLength) + const sourceView = new Uint8Array( + arg, + alignedByteOffset, + alignedByteLength + ) + const targetView = new Uint8Array(alignedBuffer) + targetView.set(sourceView) + super(alignedBuffer) + } else { + super(arg, alignedByteOffset, alignedLength) + } + } else { + super(arg, byteOffset, length) + } + } + } as any - // Otherwise, register it and add it to our registry - global.__REGISTERED_KERNELS__.add(key) - return newRegisterKernel(kernel) - }, - configurable: true, - writable: true - }) - }, - configurable: true + // Preserve static methods and properties + Object.setPrototypeOf(global.Float32Array, originalFloat32Array) + Object.defineProperty(global.Float32Array, 'name', { + value: 'Float32Array' + }) + Object.defineProperty(global.Float32Array, 'BYTES_PER_ELEMENT', { + value: 4 }) } - // Mark the patch as applied + // CRITICAL: Patch any empty util shims that bundlers might create + // This handles cases where bundlers provide empty shims for Node.js modules + if (typeof global !== 'undefined') { + // Look for common patterns of util shims in bundled code + const checkAndPatchUtilShim = (obj: any) => { + if (obj && typeof obj === 'object' && !obj.TextEncoder) { + obj.TextEncoder = TextEncoder + obj.TextDecoder = TextDecoder + obj.types = obj.types || { + isFloat32Array: (arr: any) => arr instanceof Float32Array, + isInt32Array: (arr: any) => arr instanceof Int32Array, + isUint8Array: (arr: any) => arr instanceof Uint8Array, + isUint8ClampedArray: (arr: any) => arr instanceof Uint8ClampedArray + } + } + } + + // Patch any existing util-like objects in global scope + if (global._utilShim) { + checkAndPatchUtilShim(global._utilShim) + } + + // CRITICAL: Patch the bundled util shim directly + // In bundled code, there's often a _utilShim object that needs patching + if ( + typeof globalThis !== 'undefined' && + (globalThis as GlobalThis)._utilShim + ) { + checkAndPatchUtilShim((globalThis as GlobalThis)._utilShim) + } + + // CRITICAL: Create and patch a global _utilShim if it doesn't exist + // This ensures the bundled code will find the patched version + if (!global._utilShim) { + global._utilShim = { + TextEncoder: TextEncoder, + TextDecoder: TextDecoder, + types: { + isFloat32Array: (arr: any) => arr instanceof Float32Array, + isInt32Array: (arr: any) => arr instanceof Int32Array, + isUint8Array: (arr: any) => arr instanceof Uint8Array, + isUint8ClampedArray: (arr: any) => arr instanceof Uint8ClampedArray + } + } + } else { + checkAndPatchUtilShim(global._utilShim) + } + + // Also ensure it's available on globalThis + if ( + typeof globalThis !== 'undefined' && + !(globalThis as GlobalThis)._utilShim + ) { + ;(globalThis as GlobalThis)._utilShim = global._utilShim + } + + // Set up a property descriptor to catch util shim assignments + try { + Object.defineProperty(global, '_utilShim', { + get() { + return this.__utilShim || {} + }, + set(value) { + checkAndPatchUtilShim(value) + this.__utilShim = value + }, + configurable: true + }) + } catch (e) { + // Ignore if property can't be defined + } + + // Also set up property descriptor on globalThis + try { + Object.defineProperty(globalThis, '_utilShim', { + get() { + return this.__utilShim || {} + }, + set(value) { + checkAndPatchUtilShim(value) + this.__utilShim = value + }, + configurable: true + }) + } catch (e) { + // Ignore if property can't be defined + } + } + + console.log( + 'Brainy: Successfully patched TensorFlow.js PlatformNode at module load time' + ) + patchApplied = true + } catch (error) { + console.warn( + 'Brainy: Failed to apply early TensorFlow.js platform patch:', + error + ) + } +} + +/** + * Apply the TensorFlow.js platform patch if it hasn't been applied already + * This is a safety measure in case the module-level patch didn't run + * Now works across all environments: browser, Node.js, and serverless/server + */ +export async function applyTensorFlowPatch(): Promise { + // Apply patches for all non-browser environments that might need TensorFlow.js compatibility + // This includes Node.js, serverless environments, and other server environments + const isBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined' + if (isBrowserEnv) { + return // Browser environments don't need these patches + } + + // Get the appropriate global object for the current environment + const globalObj = (() => { + if (typeof globalThis !== 'undefined') return globalThis + if (typeof global !== 'undefined') return global + if (typeof self !== 'undefined') return self + return {} as any // Fallback for unknown environments + })() + + // Check if the critical globals exist, not just the flag + // This allows re-patching if globals have been deleted + const needsPatch = !patchApplied || + typeof globalObj.__TextEncoder__ === 'undefined' || + typeof globalObj.__TextDecoder__ === 'undefined' + + if (!needsPatch) { + return + } + + try { + console.log( + 'Brainy: Applying TensorFlow.js platform patch via function call' + ) + + // CRITICAL FIX: Patch the global environment to ensure TextEncoder/TextDecoder are available + // This approach works by ensuring the global constructors are available before TensorFlow.js loads + // Now works across all environments: Node.js, serverless, and other server environments + + // Make sure TextEncoder and TextDecoder are available globally + if (!globalObj.TextEncoder) { + globalObj.TextEncoder = TextEncoder + } + if (!globalObj.TextDecoder) { + globalObj.TextDecoder = TextDecoder + } + + // Also set the special global constructors that TensorFlow can use safely + ;(globalObj as any).__TextEncoder__ = TextEncoder + ;(globalObj as any).__TextDecoder__ = TextDecoder + + // Also patch process.versions to ensure TensorFlow.js detects Node.js correctly + if (typeof process !== 'undefined' && process.versions) { + // Ensure TensorFlow.js sees this as a Node.js environment + if (!process.versions.node) { + process.versions.node = process.version + } + } + + // CRITICAL: Patch the Node.js util module directly + try { + const util = await import('util') + // Ensure TextEncoder and TextDecoder are available as constructors + util.TextEncoder = TextEncoder as typeof util.TextEncoder + util.TextDecoder = TextDecoder as typeof util.TextDecoder + } catch (error) { + // Ignore if util module is not available + } + patchApplied = true - console.log('Brainy: Successfully applied TensorFlow.js platform patch') } catch (error) { console.warn('Brainy: Failed to apply TensorFlow.js platform patch:', error) } } + +export function getTextEncoder(): TextEncoder { + return new TextEncoder() +} + +export function getTextDecoder(): TextDecoder { + return new TextDecoder() +} + +// Apply patch immediately +applyTensorFlowPatch().catch((error) => { + console.warn('Failed to apply TensorFlow patch at module load:', error) +})