chore(8.0)!: drop browser support, cloud SDKs, legacy pipeline, dead threading
Brainy 8.0 is server-only. This commit takes the consequences seriously and
removes everything that was only there to keep browser/cloud/threading
surfaces alive.
Browser support drop (per the @deprecated notes in environment.ts):
- isBrowser, isWebWorker, areWebWorkersAvailable, navigator.deviceMemory
paths, window/document/self.onmessage code.
- browser console.log in unified.ts, the 'browser' branch in
autoConfiguration.ts (env enum + scaleUp cases), 'browser-cache' model
path, MCP service environment value.
- package.json browser field.
- src/worker.ts (Web Worker entrypoint) deleted.
Cloud SDK removal (the four adapters were dropped in Phase 7; the SDKs
were the lingering tax):
- @aws-sdk/client-s3, @azure/identity, @azure/storage-blob, and
@google-cloud/storage removed from package.json. Lockfile drops the
entire @aws/@azure/@google-cloud/@smithy transitive tree.
- EnhancedS3Clear class deleted from enhancedClearOperations.ts (the
only @aws-sdk/client-s3 consumer; the dynamic import sites went with
it). EnhancedFileSystemClear stays.
- src/utils/adaptiveSocketManager.ts deleted entirely (474 LOC of HTTPS
socket-pool management for the dropped cloud HTTP handler).
performanceMonitor.ts no longer reports a socketConfig; socket
utilization is fixed at 0.
Dead threading subsystem:
- executeInThread was imported by distance.ts and hnswIndex.ts but
never called. It was scaffolding for a future "off-main-thread
distance batch" optimization that never shipped.
- src/utils/workerUtils.ts deleted (Web Worker code path + an
unreachable Node Worker Threads code path).
- environment.ts loses isThreadingAvailable, isThreadingAvailableAsync,
areWorkerThreadsAvailable, areWorkerThreadsAvailableSync. All exports
purged from index.ts and unified.ts.
- autoConfiguration.ts drops AutoConfigResult.threadingAvailable.
Legacy plugin/augmentation pipeline:
- src/pipeline.ts deleted. The whole file was a no-op stub for
backwards compat — Pipeline class had no methods, no lifecycle hooks,
no before/after callbacks. AugmentationPipeline, augmentationPipeline,
createPipeline, createStreamingPipeline, StreamlinedPipelineOptions,
StreamlinedPipelineResult, StreamlinedExecutionMode were all aliases
for the same stub.
- src/mcp/mcpAugmentationToolset.ts deleted. executePipeline always
threw "deprecated", isValidAugmentationType always returned false,
getAvailableTools always returned []. Dead surface.
- BrainyMCPService no longer instantiates a toolset. TOOL_EXECUTION
requests now return the standard UNSUPPORTED_REQUEST_TYPE error.
'availableTools' system-info returns [] (was the same in practice).
Net: 22 files changed, ~6400 LOC deleted (including legacy code +
mechanical lockfile churn). Build clean, 1409/1409 tests pass.
This commit is contained in:
parent
adda1570f3
commit
266715aeee
22 changed files with 134 additions and 4648 deletions
|
|
@ -1,474 +0,0 @@
|
|||
/**
|
||||
* Adaptive Socket Manager
|
||||
* Automatically manages socket pools and connection settings based on load patterns
|
||||
* Zero-configuration approach that learns and adapts to workload characteristics
|
||||
*/
|
||||
|
||||
import { Agent as HttpsAgent } from 'node:https'
|
||||
import { NodeHttpHandler } from '@smithy/node-http-handler'
|
||||
import { createModuleLogger } from './logger.js'
|
||||
|
||||
interface LoadMetrics {
|
||||
requestsPerSecond: number
|
||||
pendingRequests: number
|
||||
socketUtilization: number
|
||||
errorRate: number
|
||||
latencyP50: number
|
||||
latencyP95: number
|
||||
memoryUsage: number
|
||||
}
|
||||
|
||||
interface AdaptiveConfig {
|
||||
maxSockets: number
|
||||
maxFreeSockets: number
|
||||
keepAliveTimeout: number
|
||||
connectionTimeout: number
|
||||
socketTimeout: number
|
||||
batchSize: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Adaptive Socket Manager that automatically scales based on load patterns
|
||||
*/
|
||||
export class AdaptiveSocketManager {
|
||||
private logger = createModuleLogger('AdaptiveSocketManager')
|
||||
|
||||
// Current configuration
|
||||
private config: AdaptiveConfig = {
|
||||
maxSockets: 100, // Start conservative
|
||||
maxFreeSockets: 20,
|
||||
keepAliveTimeout: 60000,
|
||||
connectionTimeout: 10000,
|
||||
socketTimeout: 60000,
|
||||
batchSize: 10
|
||||
}
|
||||
|
||||
// Performance tracking
|
||||
private metrics: LoadMetrics = {
|
||||
requestsPerSecond: 0,
|
||||
pendingRequests: 0,
|
||||
socketUtilization: 0,
|
||||
errorRate: 0,
|
||||
latencyP50: 0,
|
||||
latencyP95: 0,
|
||||
memoryUsage: 0
|
||||
}
|
||||
|
||||
// Historical data for learning
|
||||
private history: LoadMetrics[] = []
|
||||
private maxHistorySize = 100
|
||||
|
||||
// Adaptation state
|
||||
private lastAdaptationTime = 0
|
||||
private adaptationInterval = 5000 // Check every 5 seconds
|
||||
private consecutiveHighLoad = 0
|
||||
private consecutiveLowLoad = 0
|
||||
|
||||
// Request tracking
|
||||
private requestStartTimes = new Map<string, number>()
|
||||
private requestLatencies: number[] = []
|
||||
private errorCount = 0
|
||||
private successCount = 0
|
||||
private lastMetricReset = Date.now()
|
||||
|
||||
// Socket pool instances
|
||||
private currentAgent: HttpsAgent | null = null
|
||||
private currentHandler: NodeHttpHandler | null = null
|
||||
|
||||
/**
|
||||
* Get or create an optimized HTTP handler
|
||||
*/
|
||||
public getHttpHandler(): NodeHttpHandler {
|
||||
// Adapt configuration if needed
|
||||
this.adaptIfNeeded()
|
||||
|
||||
// Create new handler if configuration changed
|
||||
if (!this.currentHandler || this.shouldRecreateHandler()) {
|
||||
this.currentAgent = new HttpsAgent({
|
||||
keepAlive: true,
|
||||
maxSockets: this.config.maxSockets,
|
||||
maxFreeSockets: this.config.maxFreeSockets,
|
||||
timeout: this.config.keepAliveTimeout,
|
||||
scheduling: 'fifo' // Fair scheduling for high-volume scenarios
|
||||
})
|
||||
|
||||
this.currentHandler = new NodeHttpHandler({
|
||||
httpsAgent: this.currentAgent,
|
||||
connectionTimeout: this.config.connectionTimeout,
|
||||
socketTimeout: this.config.socketTimeout
|
||||
})
|
||||
|
||||
this.logger.debug('Created new HTTP handler with config:', this.config)
|
||||
}
|
||||
|
||||
return this.currentHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current batch size recommendation
|
||||
*/
|
||||
public getBatchSize(): number {
|
||||
this.adaptIfNeeded()
|
||||
return this.config.batchSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Track request start
|
||||
*/
|
||||
public trackRequestStart(requestId: string): void {
|
||||
this.requestStartTimes.set(requestId, Date.now())
|
||||
this.metrics.pendingRequests++
|
||||
}
|
||||
|
||||
/**
|
||||
* Track request completion
|
||||
*/
|
||||
public trackRequestComplete(requestId: string, success: boolean): void {
|
||||
const startTime = this.requestStartTimes.get(requestId)
|
||||
if (startTime) {
|
||||
const latency = Date.now() - startTime
|
||||
this.requestLatencies.push(latency)
|
||||
this.requestStartTimes.delete(requestId)
|
||||
|
||||
// Keep latency array bounded
|
||||
if (this.requestLatencies.length > 1000) {
|
||||
this.requestLatencies = this.requestLatencies.slice(-500)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
this.successCount++
|
||||
} else {
|
||||
this.errorCount++
|
||||
}
|
||||
|
||||
this.metrics.pendingRequests = Math.max(0, this.metrics.pendingRequests - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should adapt configuration
|
||||
*/
|
||||
private adaptIfNeeded(): void {
|
||||
const now = Date.now()
|
||||
if (now - this.lastAdaptationTime < this.adaptationInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastAdaptationTime = now
|
||||
this.updateMetrics()
|
||||
this.analyzeAndAdapt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current metrics
|
||||
*/
|
||||
private updateMetrics(): void {
|
||||
const now = Date.now()
|
||||
const timeSinceReset = (now - this.lastMetricReset) / 1000
|
||||
|
||||
// Calculate requests per second
|
||||
const totalRequests = this.successCount + this.errorCount
|
||||
this.metrics.requestsPerSecond = timeSinceReset > 0
|
||||
? totalRequests / timeSinceReset
|
||||
: 0
|
||||
|
||||
// Calculate error rate
|
||||
this.metrics.errorRate = totalRequests > 0
|
||||
? this.errorCount / totalRequests
|
||||
: 0
|
||||
|
||||
// Calculate latency percentiles
|
||||
if (this.requestLatencies.length > 0) {
|
||||
const sorted = [...this.requestLatencies].sort((a, b) => a - b)
|
||||
const p50Index = Math.floor(sorted.length * 0.5)
|
||||
const p95Index = Math.floor(sorted.length * 0.95)
|
||||
this.metrics.latencyP50 = sorted[p50Index] || 0
|
||||
this.metrics.latencyP95 = sorted[p95Index] || 0
|
||||
}
|
||||
|
||||
// Calculate socket utilization
|
||||
this.metrics.socketUtilization = this.metrics.pendingRequests / this.config.maxSockets
|
||||
|
||||
// Memory usage
|
||||
if (typeof process !== 'undefined' && process.memoryUsage) {
|
||||
const memUsage = process.memoryUsage()
|
||||
this.metrics.memoryUsage = memUsage.heapUsed / memUsage.heapTotal
|
||||
}
|
||||
|
||||
// Add to history
|
||||
this.history.push({ ...this.metrics })
|
||||
if (this.history.length > this.maxHistorySize) {
|
||||
this.history.shift()
|
||||
}
|
||||
|
||||
// Reset counters periodically
|
||||
if (timeSinceReset > 60) {
|
||||
this.lastMetricReset = now
|
||||
this.successCount = 0
|
||||
this.errorCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze metrics and adapt configuration
|
||||
*/
|
||||
private analyzeAndAdapt(): void {
|
||||
const wasConfig = { ...this.config }
|
||||
|
||||
// Detect high load conditions
|
||||
const isHighLoad = this.detectHighLoad()
|
||||
const isLowLoad = this.detectLowLoad()
|
||||
const hasErrors = this.metrics.errorRate > 0.01 // More than 1% errors
|
||||
|
||||
if (isHighLoad) {
|
||||
this.consecutiveHighLoad++
|
||||
this.consecutiveLowLoad = 0
|
||||
|
||||
if (this.consecutiveHighLoad >= 2) { // Wait for 2 consecutive high load readings
|
||||
this.scaleUp()
|
||||
}
|
||||
} else if (isLowLoad) {
|
||||
this.consecutiveLowLoad++
|
||||
this.consecutiveHighLoad = 0
|
||||
|
||||
if (this.consecutiveLowLoad >= 6) { // Wait longer before scaling down
|
||||
this.scaleDown()
|
||||
}
|
||||
} else {
|
||||
// Reset counters if load is normal
|
||||
this.consecutiveHighLoad = Math.max(0, this.consecutiveHighLoad - 1)
|
||||
this.consecutiveLowLoad = Math.max(0, this.consecutiveLowLoad - 1)
|
||||
}
|
||||
|
||||
// Handle error conditions
|
||||
if (hasErrors) {
|
||||
this.handleErrors()
|
||||
}
|
||||
|
||||
// Log significant changes
|
||||
if (JSON.stringify(wasConfig) !== JSON.stringify(this.config)) {
|
||||
this.logger.info('Adapted configuration', {
|
||||
from: wasConfig,
|
||||
to: this.config,
|
||||
metrics: this.metrics
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect high load conditions
|
||||
*/
|
||||
private detectHighLoad(): boolean {
|
||||
return (
|
||||
this.metrics.socketUtilization > 0.7 || // Sockets heavily used
|
||||
this.metrics.pendingRequests > this.config.maxSockets * 0.8 || // Many pending requests
|
||||
this.metrics.latencyP95 > 5000 || // High latency
|
||||
this.metrics.requestsPerSecond > 100 // High request rate
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect low load conditions
|
||||
*/
|
||||
private detectLowLoad(): boolean {
|
||||
return (
|
||||
this.metrics.socketUtilization < 0.2 && // Sockets barely used
|
||||
this.metrics.pendingRequests < 5 && // Few pending requests
|
||||
this.metrics.latencyP95 < 1000 && // Low latency
|
||||
this.metrics.requestsPerSecond < 10 && // Low request rate
|
||||
this.metrics.memoryUsage < 0.5 // Low memory usage
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale up resources for high load
|
||||
*/
|
||||
private scaleUp(): void {
|
||||
// Increase socket limits progressively
|
||||
const scaleFactor = this.metrics.errorRate > 0.05 ? 1.5 : 2.0 // Scale more aggressively if no errors
|
||||
|
||||
this.config.maxSockets = Math.min(
|
||||
2000, // Hard limit to prevent resource exhaustion
|
||||
Math.ceil(this.config.maxSockets * scaleFactor)
|
||||
)
|
||||
|
||||
this.config.maxFreeSockets = Math.min(
|
||||
200,
|
||||
Math.ceil(this.config.maxSockets * 0.1) // Keep 10% as free sockets
|
||||
)
|
||||
|
||||
// Increase batch size for better throughput
|
||||
this.config.batchSize = Math.min(
|
||||
100,
|
||||
Math.ceil(this.config.batchSize * 1.5)
|
||||
)
|
||||
|
||||
// Adjust timeouts for high load
|
||||
this.config.keepAliveTimeout = 120000 // Keep connections alive longer
|
||||
this.config.connectionTimeout = 15000 // Allow more time for connections
|
||||
this.config.socketTimeout = 90000 // Allow more time for responses
|
||||
|
||||
this.logger.debug('Scaled up for high load', {
|
||||
sockets: this.config.maxSockets,
|
||||
batchSize: this.config.batchSize
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale down resources for low load
|
||||
*/
|
||||
private scaleDown(): void {
|
||||
// Only scale down if memory pressure is low
|
||||
if (this.metrics.memoryUsage > 0.7) {
|
||||
return
|
||||
}
|
||||
|
||||
// Decrease socket limits conservatively
|
||||
this.config.maxSockets = Math.max(
|
||||
50, // Minimum sockets
|
||||
Math.floor(this.config.maxSockets * 0.7)
|
||||
)
|
||||
|
||||
this.config.maxFreeSockets = Math.max(
|
||||
10,
|
||||
Math.floor(this.config.maxSockets * 0.2) // Keep 20% as free sockets
|
||||
)
|
||||
|
||||
// Decrease batch size
|
||||
this.config.batchSize = Math.max(
|
||||
5,
|
||||
Math.floor(this.config.batchSize * 0.7)
|
||||
)
|
||||
|
||||
// Adjust timeouts for low load
|
||||
this.config.keepAliveTimeout = 60000
|
||||
this.config.connectionTimeout = 10000
|
||||
this.config.socketTimeout = 60000
|
||||
|
||||
this.logger.debug('Scaled down for low load', {
|
||||
sockets: this.config.maxSockets,
|
||||
batchSize: this.config.batchSize
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error conditions by adjusting configuration
|
||||
*/
|
||||
private handleErrors(): void {
|
||||
const errorRate = this.metrics.errorRate
|
||||
|
||||
if (errorRate > 0.1) { // More than 10% errors
|
||||
// Severe errors - back off aggressively
|
||||
this.config.maxSockets = Math.max(50, Math.floor(this.config.maxSockets * 0.5))
|
||||
this.config.batchSize = Math.max(1, Math.floor(this.config.batchSize * 0.3))
|
||||
this.config.connectionTimeout = Math.min(30000, this.config.connectionTimeout * 2)
|
||||
this.config.socketTimeout = Math.min(120000, this.config.socketTimeout * 2)
|
||||
|
||||
this.logger.warn('High error rate detected, backing off', {
|
||||
errorRate,
|
||||
newConfig: this.config
|
||||
})
|
||||
} else if (errorRate > 0.05) { // More than 5% errors
|
||||
// Moderate errors - reduce load slightly
|
||||
this.config.batchSize = Math.max(1, Math.floor(this.config.batchSize * 0.7))
|
||||
this.config.connectionTimeout = Math.min(20000, this.config.connectionTimeout * 1.2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should recreate the handler
|
||||
*/
|
||||
private shouldRecreateHandler(): boolean {
|
||||
if (!this.currentAgent) return true
|
||||
|
||||
// Recreate if socket configuration changed significantly
|
||||
const currentMaxSockets = (this.currentAgent as any).maxSockets
|
||||
const socketsDiff = Math.abs(currentMaxSockets - this.config.maxSockets)
|
||||
|
||||
return socketsDiff > currentMaxSockets * 0.5 // 50% change threshold
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration (for monitoring)
|
||||
*/
|
||||
public getConfig(): Readonly<AdaptiveConfig> {
|
||||
return { ...this.config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current metrics (for monitoring)
|
||||
*/
|
||||
public getMetrics(): Readonly<LoadMetrics> {
|
||||
return { ...this.metrics }
|
||||
}
|
||||
|
||||
/**
|
||||
* Predict optimal configuration based on historical data
|
||||
*/
|
||||
public predictOptimalConfig(): AdaptiveConfig {
|
||||
if (this.history.length < 10) {
|
||||
return this.config // Not enough data to predict
|
||||
}
|
||||
|
||||
// Analyze recent history
|
||||
const recentHistory = this.history.slice(-20)
|
||||
const avgRPS = recentHistory.reduce((sum, m) => sum + m.requestsPerSecond, 0) / recentHistory.length
|
||||
const maxRPS = Math.max(...recentHistory.map(m => m.requestsPerSecond))
|
||||
const avgLatency = recentHistory.reduce((sum, m) => sum + m.latencyP95, 0) / recentHistory.length
|
||||
|
||||
// Predict optimal socket count based on request patterns
|
||||
const optimalSockets = Math.min(2000, Math.max(50, Math.ceil(maxRPS * 2)))
|
||||
|
||||
// Predict optimal batch size based on latency
|
||||
const optimalBatchSize = avgLatency < 1000 ? 50 : avgLatency < 3000 ? 20 : 10
|
||||
|
||||
return {
|
||||
maxSockets: optimalSockets,
|
||||
maxFreeSockets: Math.ceil(optimalSockets * 0.15),
|
||||
keepAliveTimeout: avgRPS > 50 ? 120000 : 60000,
|
||||
connectionTimeout: avgLatency > 3000 ? 20000 : 10000,
|
||||
socketTimeout: avgLatency > 3000 ? 90000 : 60000,
|
||||
batchSize: optimalBatchSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default configuration
|
||||
*/
|
||||
public reset(): void {
|
||||
this.config = {
|
||||
maxSockets: 100,
|
||||
maxFreeSockets: 20,
|
||||
keepAliveTimeout: 60000,
|
||||
connectionTimeout: 10000,
|
||||
socketTimeout: 60000,
|
||||
batchSize: 10
|
||||
}
|
||||
|
||||
this.consecutiveHighLoad = 0
|
||||
this.consecutiveLowLoad = 0
|
||||
this.history = []
|
||||
this.requestLatencies = []
|
||||
this.errorCount = 0
|
||||
this.successCount = 0
|
||||
|
||||
// Force recreation of handler
|
||||
this.currentAgent = null
|
||||
this.currentHandler = null
|
||||
|
||||
this.logger.info('Reset to default configuration')
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton instance
|
||||
let globalSocketManager: AdaptiveSocketManager | null = null
|
||||
|
||||
/**
|
||||
* Get the global socket manager instance
|
||||
*/
|
||||
export function getGlobalSocketManager(): AdaptiveSocketManager {
|
||||
if (!globalSocketManager) {
|
||||
globalSocketManager = new AdaptiveSocketManager()
|
||||
}
|
||||
return globalSocketManager
|
||||
}
|
||||
|
|
@ -3,17 +3,17 @@
|
|||
* Detects environment, resources, and data patterns to provide optimal settings
|
||||
*/
|
||||
|
||||
import { isBrowser, isNode, isThreadingAvailable } from './environment.js'
|
||||
import { isNode } from './environment.js'
|
||||
|
||||
export interface AutoConfigResult {
|
||||
// Environment details
|
||||
environment: 'browser' | 'nodejs' | 'serverless' | 'unknown'
|
||||
|
||||
environment: 'nodejs' | 'serverless' | 'unknown'
|
||||
|
||||
// Resource detection
|
||||
availableMemory: number // bytes
|
||||
cpuCores: number
|
||||
threadingAvailable: boolean
|
||||
|
||||
|
||||
|
||||
// Storage capabilities
|
||||
persistentStorageAvailable: boolean
|
||||
s3StorageDetected: boolean
|
||||
|
|
@ -201,22 +201,18 @@ export class AutoConfiguration {
|
|||
/**
|
||||
* Detect the current runtime environment
|
||||
*/
|
||||
private detectEnvironment(): 'browser' | 'nodejs' | 'serverless' | 'unknown' {
|
||||
if (isBrowser()) {
|
||||
return 'browser'
|
||||
}
|
||||
|
||||
private detectEnvironment(): 'nodejs' | 'serverless' | 'unknown' {
|
||||
if (isNode()) {
|
||||
// Check for serverless environment indicators
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME ||
|
||||
process.env.VERCEL ||
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME ||
|
||||
process.env.VERCEL ||
|
||||
process.env.NETLIFY ||
|
||||
process.env.CLOUDFLARE_WORKERS) {
|
||||
return 'serverless'
|
||||
}
|
||||
return 'nodejs'
|
||||
}
|
||||
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
|
|
@ -226,40 +222,21 @@ export class AutoConfiguration {
|
|||
private async detectResources(): Promise<{
|
||||
availableMemory: number
|
||||
cpuCores: number
|
||||
threadingAvailable: boolean
|
||||
}> {
|
||||
let availableMemory = 2 * 1024 * 1024 * 1024 // Default 2GB
|
||||
let cpuCores = 4 // Default 4 cores
|
||||
|
||||
// Browser memory detection
|
||||
if (isBrowser()) {
|
||||
// @ts-ignore - navigator.deviceMemory is experimental
|
||||
if (navigator.deviceMemory) {
|
||||
// @ts-ignore
|
||||
availableMemory = navigator.deviceMemory * 1024 * 1024 * 1024 * 0.3 // Use 30% of device memory
|
||||
} else {
|
||||
availableMemory = 512 * 1024 * 1024 // Conservative 512MB for browsers
|
||||
}
|
||||
|
||||
cpuCores = navigator.hardwareConcurrency || 4
|
||||
}
|
||||
|
||||
// Node.js memory detection
|
||||
|
||||
if (isNode()) {
|
||||
try {
|
||||
const os = await import('node:os')
|
||||
availableMemory = os.totalmem() * 0.7 // Use 70% of total memory
|
||||
cpuCores = os.cpus().length
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Fallback to defaults
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
availableMemory,
|
||||
cpuCores,
|
||||
threadingAvailable: isThreadingAvailable()
|
||||
}
|
||||
|
||||
return { availableMemory, cpuCores }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -271,21 +248,14 @@ export class AutoConfiguration {
|
|||
}> {
|
||||
let persistentStorageAvailable = false
|
||||
let s3StorageDetected = s3Hint || false
|
||||
|
||||
if (isBrowser()) {
|
||||
// Check for OPFS support
|
||||
persistentStorageAvailable = 'navigator' in globalThis &&
|
||||
'storage' in navigator &&
|
||||
'getDirectory' in navigator.storage
|
||||
}
|
||||
|
||||
|
||||
if (isNode()) {
|
||||
persistentStorageAvailable = true // Always available in Node.js
|
||||
|
||||
// Check for AWS SDK or S3 environment variables
|
||||
s3StorageDetected = s3Hint ||
|
||||
!!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||
|
||||
!!(process.env.S3_BUCKET_NAME)
|
||||
persistentStorageAvailable = true
|
||||
// 8.0 dropped cloud storage adapters; the s3StorageDetected flag is
|
||||
// preserved as a hint for operator backup tooling, not adapter wiring.
|
||||
s3StorageDetected = s3Hint ||
|
||||
!!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||
|
||||
!!(process.env.S3_BUCKET_NAME)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -311,7 +281,7 @@ export class AutoConfiguration {
|
|||
maxMemoryUsage: memoryBudget,
|
||||
targetSearchLatency: 150,
|
||||
enablePartitioning: datasetSize > 25000,
|
||||
enableCompression: environment === 'browser' || memoryBudget < 2 * 1024 * 1024 * 1024,
|
||||
enableCompression: memoryBudget < 2 * 1024 * 1024 * 1024,
|
||||
enableDistributedSearch: resources.cpuCores > 2 && datasetSize > 50000,
|
||||
enablePredictiveCaching: true,
|
||||
partitionStrategy: 'semantic' as const,
|
||||
|
|
@ -321,17 +291,6 @@ export class AutoConfiguration {
|
|||
|
||||
// Environment-specific adjustments
|
||||
switch (environment) {
|
||||
case 'browser':
|
||||
config = {
|
||||
...config,
|
||||
maxMemoryUsage: Math.min(memoryBudget, 1024 * 1024 * 1024), // Cap at 1GB
|
||||
targetSearchLatency: 200, // More lenient for browsers
|
||||
enableCompression: true, // Always enable for browsers
|
||||
maxNodesPerPartition: 25000, // Smaller partitions
|
||||
semanticClusters: 4 // Fewer clusters to save memory
|
||||
}
|
||||
break
|
||||
|
||||
case 'serverless':
|
||||
config = {
|
||||
...config,
|
||||
|
|
@ -437,7 +396,6 @@ export class AutoConfiguration {
|
|||
const environment = this.detectEnvironment()
|
||||
|
||||
switch (environment) {
|
||||
case 'browser': return 10000
|
||||
case 'serverless': return 50000
|
||||
case 'nodejs': return 100000
|
||||
default: return 25000
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
*/
|
||||
|
||||
import { DistanceFunction, Vector } from '../coreTypes.js'
|
||||
import { executeInThread } from './workerUtils.js'
|
||||
import { isThreadingAvailable } from './environment.js'
|
||||
|
||||
/**
|
||||
* Calculates the Euclidean distance between two vectors
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
/**
|
||||
* Utility functions for environment detection
|
||||
* @module utils/environment
|
||||
* @description Runtime environment detection helpers. 8.0 supports Node.js,
|
||||
* Bun, and Deno on the server side only — browsers were dropped from the
|
||||
* support matrix in 8.0, so the helpers here assume a Node-like runtime.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if code is running in a browser environment
|
||||
* @deprecated Browser support is deprecated and will be removed in v8.0. Use Node.js or Bun instead.
|
||||
*/
|
||||
export function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if code is running in a Node.js environment
|
||||
* @description True when running inside a Node-compatible runtime
|
||||
* (Node.js, Bun, Deno node-compat). 8.0 has no browser path, so this is
|
||||
* effectively always true in a deployed brain — the function is kept for
|
||||
* defensive checks at runtime boundaries.
|
||||
*/
|
||||
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 &&
|
||||
|
|
@ -28,161 +20,64 @@ export function isNode(): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if code is running in a Web Worker environment
|
||||
*/
|
||||
export function isWebWorker(): boolean {
|
||||
return (
|
||||
typeof self === 'object' &&
|
||||
self.constructor &&
|
||||
self.constructor.name === 'DedicatedWorkerGlobalScope'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Web Workers are available in the current environment
|
||||
* @deprecated Browser support is deprecated and will be removed in v8.0. Use Node.js Worker Threads instead.
|
||||
*/
|
||||
export function areWebWorkersAvailable(): boolean {
|
||||
return isBrowser() && typeof Worker !== 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker Threads are available in the current environment (Node.js)
|
||||
*/
|
||||
export async function areWorkerThreadsAvailable(): Promise<boolean> {
|
||||
if (!isNode()) return false
|
||||
|
||||
try {
|
||||
// Use dynamic import to avoid errors in browser environments
|
||||
await import('worker_threads')
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version that doesn't actually try to load the module
|
||||
* This is safer in ES module environments
|
||||
*/
|
||||
export function areWorkerThreadsAvailableSync(): boolean {
|
||||
if (!isNode()) return false
|
||||
|
||||
// In Node.js 22+, worker_threads is always available
|
||||
return parseInt(process.versions.node.split('.')[0]) >= 22
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if threading is available in the current environment
|
||||
* Returns true if either Web Workers (browser) or Worker Threads (Node.js) are available
|
||||
*/
|
||||
export function isThreadingAvailable(): boolean {
|
||||
return areWebWorkersAvailable() || areWorkerThreadsAvailableSync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of isThreadingAvailable
|
||||
*/
|
||||
export async function isThreadingAvailableAsync(): Promise<boolean> {
|
||||
return areWebWorkersAvailable() || (await areWorkerThreadsAvailable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect production environment to minimize logging costs
|
||||
* @description Auto-detect a production deployment. Returns true when any
|
||||
* common managed-runtime indicator is present (Cloud Run, Lambda, Azure
|
||||
* Functions, Vercel, Netlify, Heroku, Railway, Fly.io, Docker prod), or
|
||||
* when `NODE_ENV=production`. Used to silence verbose logging in prod.
|
||||
*/
|
||||
export function isProductionEnvironment(): boolean {
|
||||
// Node.js environment detection
|
||||
if (isNode()) {
|
||||
// Check common production environment indicators
|
||||
const nodeEnv = process.env.NODE_ENV?.toLowerCase()
|
||||
if (nodeEnv === 'production' || nodeEnv === 'prod') return true
|
||||
|
||||
// Google Cloud Run detection
|
||||
if (process.env.K_SERVICE || process.env.GOOGLE_CLOUD_PROJECT) return true
|
||||
|
||||
// AWS Lambda detection
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV) return true
|
||||
|
||||
// Azure Functions detection
|
||||
if (process.env.AZURE_FUNCTIONS_ENVIRONMENT || process.env.WEBSITE_SITE_NAME) return true
|
||||
|
||||
// Vercel detection
|
||||
if (process.env.VERCEL || process.env.VERCEL_ENV === 'production') return true
|
||||
|
||||
// Netlify detection
|
||||
if (process.env.NETLIFY && process.env.CONTEXT === 'production') return true
|
||||
|
||||
// Heroku detection
|
||||
if (process.env.DYNO && process.env.NODE_ENV !== 'development') return true
|
||||
|
||||
// Railway detection
|
||||
if (process.env.RAILWAY_ENVIRONMENT === 'production') return true
|
||||
|
||||
// Fly.io detection
|
||||
if (process.env.FLY_APP_NAME && process.env.FLY_REGION) return true
|
||||
|
||||
// Docker in production (common patterns)
|
||||
if (process.env.DOCKER_ENV === 'production' || process.env.ENVIRONMENT === 'production') return true
|
||||
|
||||
// Generic production indicators
|
||||
if (process.env.PROD === 'true' || process.env.PRODUCTION === 'true') return true
|
||||
}
|
||||
|
||||
// Browser environment - assume development unless explicitly production
|
||||
if (isBrowser()) {
|
||||
// Check for production domain patterns
|
||||
const hostname = window?.location?.hostname
|
||||
if (hostname) {
|
||||
// Avoid logging on production domains
|
||||
if (hostname.includes('.com') || hostname.includes('.org') || hostname.includes('.net')) {
|
||||
return !hostname.includes('localhost') && !hostname.includes('127.0.0.1') && !hostname.includes('dev')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNode()) return false
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV?.toLowerCase()
|
||||
if (nodeEnv === 'production' || nodeEnv === 'prod') return true
|
||||
|
||||
if (process.env.K_SERVICE || process.env.GOOGLE_CLOUD_PROJECT) return true
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV) return true
|
||||
if (process.env.AZURE_FUNCTIONS_ENVIRONMENT || process.env.WEBSITE_SITE_NAME) return true
|
||||
if (process.env.VERCEL || process.env.VERCEL_ENV === 'production') return true
|
||||
if (process.env.NETLIFY && process.env.CONTEXT === 'production') return true
|
||||
if (process.env.DYNO && process.env.NODE_ENV !== 'development') return true
|
||||
if (process.env.RAILWAY_ENVIRONMENT === 'production') return true
|
||||
if (process.env.FLY_APP_NAME && process.env.FLY_REGION) return true
|
||||
if (process.env.DOCKER_ENV === 'production' || process.env.ENVIRONMENT === 'production') return true
|
||||
if (process.env.PROD === 'true' || process.env.PRODUCTION === 'true') return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate log level based on environment
|
||||
* @description Resolve a log level from BRAINY_LOG_LEVEL, NODE_ENV, and
|
||||
* deployment-environment heuristics. Production defaults to 'error' so a
|
||||
* busy app doesn't pay for verbose logging.
|
||||
*/
|
||||
export function getLogLevel(): 'silent' | 'error' | 'warn' | 'info' | 'verbose' {
|
||||
// Explicit log level override
|
||||
const explicitLevel = process.env.BRAINY_LOG_LEVEL?.toLowerCase()
|
||||
if (explicitLevel && ['silent', 'error', 'warn', 'info', 'verbose'].includes(explicitLevel)) {
|
||||
return explicitLevel as 'silent' | 'error' | 'warn' | 'info' | 'verbose'
|
||||
}
|
||||
|
||||
// Auto-detect based on environment
|
||||
if (isProductionEnvironment()) {
|
||||
return 'error' // Only log errors in production to minimize costs
|
||||
}
|
||||
|
||||
// Development environments get more verbose logging
|
||||
|
||||
if (isProductionEnvironment()) return 'error'
|
||||
|
||||
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev') {
|
||||
return 'verbose'
|
||||
}
|
||||
|
||||
// Test environments should be quieter
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return 'warn'
|
||||
}
|
||||
|
||||
// Default to info level
|
||||
|
||||
if (process.env.NODE_ENV === 'test') return 'warn'
|
||||
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if logging should be enabled for a given level
|
||||
* @description True when a log message at the requested level should be
|
||||
* emitted given the current configured log level. Returns false in 'silent'.
|
||||
*/
|
||||
export function shouldLog(level: 'error' | 'warn' | 'info' | 'verbose'): boolean {
|
||||
const currentLevel = getLogLevel()
|
||||
|
||||
if (currentLevel === 'silent') return false
|
||||
|
||||
|
||||
const levels = ['error', 'warn', 'info', 'verbose']
|
||||
const currentIndex = levels.indexOf(currentLevel)
|
||||
const messageIndex = levels.indexOf(level)
|
||||
|
||||
|
||||
return messageIndex <= currentIndex
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export * from './distance.js'
|
||||
export * from './embedding.js'
|
||||
export * from './workerUtils.js'
|
||||
export * from './statisticsCollector.js'
|
||||
export * from './jsonProcessing.js'
|
||||
export * from './fieldNameTracking.js'
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { createModuleLogger } from './logger.js'
|
||||
import { getGlobalSocketManager } from './adaptiveSocketManager.js'
|
||||
import { getGlobalBackpressure } from './adaptiveBackpressure.js'
|
||||
|
||||
interface PerformanceMetrics {
|
||||
|
|
@ -215,10 +214,11 @@ export class PerformanceMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
// Get metrics from socket manager
|
||||
const socketMetrics = getGlobalSocketManager().getMetrics()
|
||||
this.metrics.socketUtilization = socketMetrics.socketUtilization
|
||||
|
||||
// Socket-pool metrics were tied to the dropped cloud HTTP handler; not
|
||||
// applicable to filesystem-only 8.0 deployments.
|
||||
this.metrics.socketUtilization = 0
|
||||
|
||||
|
||||
// Get metrics from backpressure system
|
||||
const backpressureStatus = getGlobalBackpressure().getStatus()
|
||||
this.metrics.queueDepth = backpressureStatus.queueLength
|
||||
|
|
@ -431,14 +431,12 @@ export class PerformanceMonitor {
|
|||
metrics: PerformanceMetrics
|
||||
trends: PerformanceTrend[]
|
||||
recommendations: string[]
|
||||
socketConfig: any
|
||||
backpressureStatus: any
|
||||
} {
|
||||
return {
|
||||
metrics: this.getMetrics(),
|
||||
trends: this.getTrends(),
|
||||
recommendations: this.getRecommendations(),
|
||||
socketConfig: getGlobalSocketManager().getConfig(),
|
||||
backpressureStatus: getGlobalBackpressure().getStatus()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,15 +13,7 @@ let patchApplied = false
|
|||
* Simplified version for Transformers.js/ONNX Runtime
|
||||
*/
|
||||
export async function applyTensorFlowPatch(): Promise<void> {
|
||||
// Apply patches for all non-browser environments that might need TextEncoder/TextDecoder
|
||||
const isBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
if (isBrowserEnv || patchApplied) {
|
||||
return // Browser environments don't need these patches, and don't patch twice
|
||||
}
|
||||
|
||||
if (!isNode()) {
|
||||
return // Only patch Node.js environments
|
||||
}
|
||||
if (patchApplied || !isNode()) return
|
||||
|
||||
try {
|
||||
console.log('Brainy: Applying TextEncoder/TextDecoder patch for Node.js')
|
||||
|
|
|
|||
|
|
@ -1,512 +0,0 @@
|
|||
/**
|
||||
* Utility functions for executing functions in Worker Threads (Node.js) or Web Workers (Browser)
|
||||
* This implementation leverages Node.js 22's stable Worker Threads API for maximum reliability
|
||||
*/
|
||||
|
||||
import { isBrowser, isNode } from './environment.js'
|
||||
import { prodLog } from './logger.js'
|
||||
|
||||
// Worker pool to reuse workers
|
||||
const workerPool: Map<string, any> = new Map()
|
||||
const MAX_POOL_SIZE = 4 // Adjust based on system capabilities
|
||||
|
||||
/**
|
||||
* Execute a function in a separate thread
|
||||
*
|
||||
* @param fnString The function to execute as a string
|
||||
* @param args The arguments to pass to the function
|
||||
* @returns A promise that resolves with the result of the function
|
||||
*/
|
||||
export function executeInThread<T>(fnString: string, args: any): Promise<T> {
|
||||
if (isNode()) {
|
||||
return executeInNodeWorker<T>(fnString, args)
|
||||
} else if (isBrowser() && typeof window !== 'undefined' && window.Worker) {
|
||||
return executeInWebWorker<T>(fnString, args)
|
||||
} else {
|
||||
// Fallback to main thread execution
|
||||
try {
|
||||
// Try different approaches to create a function from string
|
||||
let fn
|
||||
try {
|
||||
// First try with 'return' prefix
|
||||
fn = new Function('return ' + fnString)()
|
||||
} catch (functionError) {
|
||||
console.warn(
|
||||
'Fallback: Error creating function with return syntax, trying alternative approaches',
|
||||
functionError
|
||||
)
|
||||
|
||||
try {
|
||||
// Try wrapping in parentheses for function expressions
|
||||
fn = new Function('return (' + fnString + ')')()
|
||||
} catch (wrapError) {
|
||||
console.warn(
|
||||
'Fallback: Error creating function with parentheses wrapping',
|
||||
wrapError
|
||||
)
|
||||
|
||||
try {
|
||||
// Try direct approach for named functions
|
||||
fn = new Function(fnString)()
|
||||
} catch (directError) {
|
||||
console.warn(
|
||||
'Fallback: Direct approach failed, trying with function wrapper',
|
||||
directError
|
||||
)
|
||||
|
||||
try {
|
||||
// Try wrapping in a function that returns the function expression
|
||||
fn = new Function(
|
||||
'return function(args) { return (' + fnString + ')(args); }'
|
||||
)()
|
||||
} catch (wrapperError) {
|
||||
console.error(
|
||||
'Fallback: All approaches to create function failed',
|
||||
wrapperError
|
||||
)
|
||||
throw new Error(
|
||||
'Failed to create function from string: ' +
|
||||
(functionError as Error).message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(fn(args) as T)
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function in a Node.js Worker Thread
|
||||
* Optimized for Node.js 22 LTS with stable Worker Threads performance
|
||||
*/
|
||||
function executeInNodeWorker<T>(fnString: string, args: any): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
try {
|
||||
// Dynamically import worker_threads (Node.js only)
|
||||
import('node:worker_threads')
|
||||
.then(({ Worker, isMainThread, parentPort, workerData }) => {
|
||||
if (!isMainThread && parentPort) {
|
||||
// We're inside a worker, execute the function
|
||||
const fn = new Function('return ' + workerData.fnString)()
|
||||
const result = fn(workerData.args)
|
||||
parentPort.postMessage({ result })
|
||||
return
|
||||
}
|
||||
|
||||
// Get a worker from the pool or create a new one
|
||||
const workerId = `worker-${Math.random().toString(36).substring(2, 9)}`
|
||||
let worker: any
|
||||
|
||||
if (workerPool.size < MAX_POOL_SIZE) {
|
||||
// Create a new worker
|
||||
worker = new Worker(
|
||||
`
|
||||
import { parentPort, workerData } from 'node:worker_threads';
|
||||
|
||||
// Add TensorFlow.js platform patch for Node.js
|
||||
if (typeof global !== 'undefined') {
|
||||
try {
|
||||
// Define a custom PlatformNode class
|
||||
class PlatformNode {
|
||||
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));
|
||||
},
|
||||
// Use native TextEncoder and TextDecoder
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder
|
||||
};
|
||||
|
||||
// Initialize encoders using native constructors
|
||||
this.textEncoder = new TextEncoder();
|
||||
this.textDecoder = new TextDecoder();
|
||||
}
|
||||
|
||||
// Define isFloat32Array directly on the instance
|
||||
isFloat32Array(arr) {
|
||||
return !!(
|
||||
arr instanceof Float32Array ||
|
||||
(arr && Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
);
|
||||
}
|
||||
|
||||
// Define isTypedArray directly on the instance
|
||||
isTypedArray(arr) {
|
||||
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView));
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the PlatformNode class to the global object
|
||||
global.PlatformNode = PlatformNode;
|
||||
|
||||
// Also create an instance and assign it to global.platformNode
|
||||
global.platformNode = new PlatformNode();
|
||||
|
||||
// Ensure global.util exists and has the necessary methods
|
||||
if (!global.util) {
|
||||
global.util = {};
|
||||
}
|
||||
|
||||
// Add isFloat32Array method if it doesn't exist
|
||||
if (!global.util.isFloat32Array) {
|
||||
global.util.isFloat32Array = (arr) => {
|
||||
return !!(
|
||||
arr instanceof Float32Array ||
|
||||
(arr && Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Add isTypedArray method if it doesn't exist
|
||||
if (!global.util.isTypedArray) {
|
||||
global.util.isTypedArray = (arr) => {
|
||||
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView));
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to apply TensorFlow.js platform patch:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fn = new Function('return ' + workerData.fnString)();
|
||||
const result = fn(workerData.args);
|
||||
parentPort.postMessage({ result });
|
||||
`,
|
||||
{
|
||||
eval: true,
|
||||
workerData: { fnString, args }
|
||||
}
|
||||
)
|
||||
|
||||
workerPool.set(workerId, worker)
|
||||
} else {
|
||||
// Reuse an existing worker
|
||||
const poolKeys = Array.from(workerPool.keys())
|
||||
const randomKey =
|
||||
poolKeys[Math.floor(Math.random() * poolKeys.length)]
|
||||
worker = workerPool.get(randomKey)
|
||||
|
||||
// Terminate and recreate if the worker is busy
|
||||
if (worker._busy) {
|
||||
worker.terminate()
|
||||
worker = new Worker(
|
||||
`
|
||||
import { parentPort, workerData } from 'node:worker_threads';
|
||||
|
||||
// Add TensorFlow.js platform patch for Node.js
|
||||
if (typeof global !== 'undefined') {
|
||||
try {
|
||||
// Define a custom PlatformNode class
|
||||
class PlatformNode {
|
||||
constructor() {
|
||||
// Create a util object with necessary methods
|
||||
this.util = {
|
||||
// Use native TextEncoder and TextDecoder
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder
|
||||
};
|
||||
|
||||
// Initialize encoders using native constructors
|
||||
this.textEncoder = new TextEncoder();
|
||||
this.textDecoder = new TextDecoder();
|
||||
}
|
||||
|
||||
// Define isFloat32Array directly on the instance
|
||||
isFloat32Array(arr) {
|
||||
return !!(
|
||||
arr instanceof Float32Array ||
|
||||
(arr && Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
);
|
||||
}
|
||||
|
||||
// Define isTypedArray directly on the instance
|
||||
isTypedArray(arr) {
|
||||
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView));
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the PlatformNode class to the global object
|
||||
global.PlatformNode = PlatformNode;
|
||||
|
||||
// Also create an instance and assign it to global.platformNode
|
||||
global.platformNode = new PlatformNode();
|
||||
|
||||
// Ensure global.util exists and has the necessary methods
|
||||
if (!global.util) {
|
||||
global.util = {};
|
||||
}
|
||||
|
||||
// Add isFloat32Array method if it doesn't exist
|
||||
if (!global.util.isFloat32Array) {
|
||||
global.util.isFloat32Array = (arr) => {
|
||||
return !!(
|
||||
arr instanceof Float32Array ||
|
||||
(arr && Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Add isTypedArray method if it doesn't exist
|
||||
if (!global.util.isTypedArray) {
|
||||
global.util.isTypedArray = (arr) => {
|
||||
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView));
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to apply TensorFlow.js platform patch:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fn = new Function('return ' + workerData.fnString)();
|
||||
const result = fn(workerData.args);
|
||||
parentPort.postMessage({ result });
|
||||
`,
|
||||
{
|
||||
eval: true,
|
||||
workerData: { fnString, args }
|
||||
}
|
||||
)
|
||||
workerPool.set(randomKey, worker)
|
||||
}
|
||||
|
||||
worker._busy = true
|
||||
}
|
||||
|
||||
worker.on('message', (message: any) => {
|
||||
worker._busy = false
|
||||
resolve(message.result as T)
|
||||
})
|
||||
|
||||
worker.on('error', (err: any) => {
|
||||
worker._busy = false
|
||||
reject(err)
|
||||
})
|
||||
|
||||
worker.on('exit', (code: number) => {
|
||||
if (code !== 0) {
|
||||
worker._busy = false
|
||||
reject(new Error(`Worker stopped with exit code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(reject)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function in a Web Worker (Browser environment)
|
||||
*/
|
||||
function executeInWebWorker<T>(fnString: string, args: any): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
try {
|
||||
// Use the dedicated worker.js file instead of creating a blob
|
||||
// Try different approaches to locate the worker.js file
|
||||
let workerPath = './worker.js'
|
||||
|
||||
try {
|
||||
// First try to use the import.meta.url if available (modern browsers)
|
||||
if (typeof import.meta !== 'undefined' && import.meta.url) {
|
||||
const baseUrl = import.meta.url.substring(
|
||||
0,
|
||||
import.meta.url.lastIndexOf('/') + 1
|
||||
)
|
||||
workerPath = `${baseUrl}worker.js`
|
||||
}
|
||||
// Fallback to a relative path based on the unified.js location
|
||||
else if (typeof document !== 'undefined') {
|
||||
// Find the script tag that loaded unified.js
|
||||
const scripts = document.getElementsByTagName('script')
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const src = scripts[i].src
|
||||
if (src && src.includes('unified.js')) {
|
||||
// Get the directory path
|
||||
workerPath =
|
||||
src.substring(0, src.lastIndexOf('/') + 1) + 'worker.js'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Could not determine worker path from import.meta.url, using relative path',
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
// If we couldn't determine the path, try some common locations
|
||||
if (workerPath === './worker.js' && typeof window !== 'undefined') {
|
||||
// Try to find the worker.js in the same directory as the current page
|
||||
const pageUrl = window.location.href
|
||||
const pageDir = pageUrl.substring(0, pageUrl.lastIndexOf('/') + 1)
|
||||
workerPath = `${pageDir}worker.js`
|
||||
|
||||
// Also check for dist/worker.js
|
||||
if (typeof document !== 'undefined') {
|
||||
const distWorkerPath = `${pageDir}dist/worker.js`
|
||||
// Create a test request to see if the file exists
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('HEAD', distWorkerPath, false)
|
||||
try {
|
||||
xhr.send()
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
workerPath = distWorkerPath
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors, we'll use the default path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Using worker path:', workerPath)
|
||||
|
||||
// Try to create a worker, but fall back to inline worker or main thread execution if it fails
|
||||
let worker: Worker
|
||||
try {
|
||||
worker = new Worker(workerPath)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to create Web Worker from file, trying inline worker:',
|
||||
error
|
||||
)
|
||||
|
||||
try {
|
||||
// Create an inline worker using a Blob
|
||||
const workerCode = `
|
||||
// Brainy Inline Worker Script
|
||||
console.log('Brainy Inline Worker: Started');
|
||||
|
||||
self.onmessage = function (e) {
|
||||
try {
|
||||
console.log('Brainy Inline Worker: Received message', e.data ? 'with data' : 'without data');
|
||||
|
||||
if (!e.data || !e.data.fnString) {
|
||||
throw new Error('Invalid message: missing function string');
|
||||
}
|
||||
|
||||
console.log('Brainy Inline Worker: Creating function from string');
|
||||
const fn = new Function('return ' + e.data.fnString)();
|
||||
|
||||
console.log('Brainy Inline Worker: Executing function with args');
|
||||
const result = fn(e.data.args);
|
||||
|
||||
console.log('Brainy Inline Worker: Function executed successfully, posting result');
|
||||
self.postMessage({ result: result });
|
||||
} catch (error) {
|
||||
console.error('Brainy Inline Worker: Error executing function', error);
|
||||
self.postMessage({
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
};
|
||||
`
|
||||
|
||||
const blob = new Blob([workerCode], {
|
||||
type: 'application/javascript'
|
||||
})
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
worker = new Worker(blobUrl)
|
||||
|
||||
console.log('Created inline worker using Blob URL')
|
||||
} catch (inlineWorkerError) {
|
||||
console.warn(
|
||||
'Failed to create inline Web Worker, falling back to main thread execution:',
|
||||
inlineWorkerError
|
||||
)
|
||||
// Execute in main thread as fallback
|
||||
try {
|
||||
const fn = new Function('return ' + fnString)()
|
||||
resolve(fn(args) as T)
|
||||
return
|
||||
} catch (mainThreadError) {
|
||||
reject(mainThreadError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set a timeout to prevent hanging
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn(
|
||||
'Web Worker execution timed out, falling back to main thread'
|
||||
)
|
||||
worker.terminate()
|
||||
|
||||
// Execute in main thread as fallback
|
||||
try {
|
||||
const fn = new Function('return ' + fnString)()
|
||||
resolve(fn(args) as T)
|
||||
} catch (mainThreadError) {
|
||||
reject(mainThreadError)
|
||||
}
|
||||
}, 25000) // 25 second timeout (less than the 30 second test timeout)
|
||||
|
||||
worker.onmessage = function (e) {
|
||||
clearTimeout(timeoutId)
|
||||
if (e.data.error) {
|
||||
reject(new Error(e.data.error))
|
||||
} else {
|
||||
resolve(e.data.result as T)
|
||||
}
|
||||
worker.terminate()
|
||||
}
|
||||
|
||||
worker.onerror = function (e) {
|
||||
clearTimeout(timeoutId)
|
||||
console.warn(
|
||||
'Web Worker error, falling back to main thread execution:',
|
||||
e.message
|
||||
)
|
||||
worker.terminate()
|
||||
|
||||
// Execute in main thread as fallback
|
||||
try {
|
||||
const fn = new Function('return ' + fnString)()
|
||||
resolve(fn(args) as T)
|
||||
} catch (mainThreadError) {
|
||||
reject(mainThreadError)
|
||||
}
|
||||
}
|
||||
|
||||
worker.postMessage({ fnString, args })
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all worker pools
|
||||
* This should be called when the application is shutting down
|
||||
*/
|
||||
export function cleanupWorkerPools(): void {
|
||||
if (isNode()) {
|
||||
import('node:worker_threads')
|
||||
.then(({ Worker }) => {
|
||||
for (const worker of workerPool.values()) {
|
||||
worker.terminate()
|
||||
}
|
||||
workerPool.clear()
|
||||
console.log('Worker pools cleaned up')
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue