feat: add browser environment compatibility support
- Remove top-level Node.js imports that break bundlers - Use universal adapters for crypto operations - Add dynamic imports for Node.js-specific modules - Add browser field to package.json for bundler hints - Maintain full Node.js functionality while enabling browser usage This allows Brainy to be used with modern bundlers (Vite, Webpack, etc.) without requiring Node.js polyfills. Browser environments get core features while Node.js retains all capabilities including filesystem and networking.
This commit is contained in:
parent
2793cef197
commit
c303ead318
7 changed files with 122 additions and 41 deletions
|
|
@ -41,6 +41,11 @@
|
||||||
"types": "./dist/universal/index.d.ts"
|
"types": "./dist/universal/index.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"browser": {
|
||||||
|
"./dist/distributed": false,
|
||||||
|
"./dist/cli": false,
|
||||||
|
"./dist/scripts": false
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.x"
|
"node": "22.x"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { BaseAugmentation } from './brainyAugmentation.js'
|
import { BaseAugmentation } from './brainyAugmentation.js'
|
||||||
import { AugmentationManifest } from './manifest.js'
|
import { AugmentationManifest } from './manifest.js'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from '../universal/crypto.js'
|
||||||
|
|
||||||
export interface AuditLogConfig {
|
export interface AuditLogConfig {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,34 @@
|
||||||
/**
|
/**
|
||||||
* Local Augmentation Discovery
|
* Local Augmentation Discovery
|
||||||
*
|
*
|
||||||
* Discovers augmentations installed locally in node_modules
|
* Discovers augmentations installed locally in node_modules
|
||||||
* and built-in augmentations that ship with Brainy
|
* and built-in augmentations that ship with Brainy
|
||||||
|
*
|
||||||
|
* NOTE: This is a Node.js-only feature that requires filesystem access
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
||||||
import { join } from 'node:path'
|
|
||||||
import { AugmentationManifest } from '../manifest.js'
|
import { AugmentationManifest } from '../manifest.js'
|
||||||
|
|
||||||
|
// Node.js modules - dynamically imported to avoid bundler issues
|
||||||
|
let fs: any = null
|
||||||
|
let path: any = null
|
||||||
|
|
||||||
|
// Load Node.js modules if available
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
try {
|
||||||
|
fs = await import('node:fs')
|
||||||
|
path = await import('node:path')
|
||||||
|
} catch (e) {
|
||||||
|
// Will throw error in methods if not available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create compatibility layer for sync methods
|
||||||
|
const existsSync = fs?.existsSync || (() => { throw new Error('Filesystem not available') })
|
||||||
|
const readdirSync = fs?.readdirSync || (() => { throw new Error('Filesystem not available') })
|
||||||
|
const readFileSync = fs?.readFileSync || (() => { throw new Error('Filesystem not available') })
|
||||||
|
const join = path?.join || ((...args: string[]) => args.join('/'))
|
||||||
|
|
||||||
export interface LocalAugmentation {
|
export interface LocalAugmentation {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,8 @@
|
||||||
* 4. System MUST fail fast if model unavailable in production
|
* 4. System MUST fail fast if model unavailable in production
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync } from 'node:fs'
|
|
||||||
import { readFile, mkdir, writeFile, stat } from 'node:fs/promises'
|
|
||||||
import { join, dirname } from 'node:path'
|
|
||||||
import { createHash } from 'node:crypto'
|
|
||||||
import { env } from '@huggingface/transformers'
|
import { env } from '@huggingface/transformers'
|
||||||
|
import { createHash } from '../universal/crypto.js'
|
||||||
|
|
||||||
// CRITICAL: These values MUST NEVER CHANGE
|
// CRITICAL: These values MUST NEVER CHANGE
|
||||||
const CRITICAL_MODEL_CONFIG = {
|
const CRITICAL_MODEL_CONFIG = {
|
||||||
|
|
@ -102,8 +99,8 @@ export class ModelGuardian {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: In production, FAIL FAST
|
// Step 2: In production, FAIL FAST (Node.js only)
|
||||||
if (process.env.NODE_ENV === 'production' && !process.env.BRAINY_ALLOW_RUNTIME_DOWNLOAD) {
|
if (typeof window === 'undefined' && process.env.NODE_ENV === 'production' && !process.env.BRAINY_ALLOW_RUNTIME_DOWNLOAD) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'🚨 CRITICAL FAILURE: Transformer model not found in production!\n' +
|
'🚨 CRITICAL FAILURE: Transformer model not found in production!\n' +
|
||||||
'The model is REQUIRED for Brainy to function.\n' +
|
'The model is REQUIRED for Brainy to function.\n' +
|
||||||
|
|
@ -147,7 +144,18 @@ export class ModelGuardian {
|
||||||
* Verify the local model files exist and are correct
|
* Verify the local model files exist and are correct
|
||||||
*/
|
*/
|
||||||
private async verifyLocalModel(): Promise<boolean> {
|
private async verifyLocalModel(): Promise<boolean> {
|
||||||
const modelBasePath = join(this.modelPath, ...CRITICAL_MODEL_CONFIG.modelName.split('/'))
|
// Browser doesn't have local file access
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('⚠️ Model verification skipped in browser environment')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically import Node.js modules
|
||||||
|
const fs = await import('node:fs')
|
||||||
|
const fsPromises = await import('node:fs/promises')
|
||||||
|
const path = await import('node:path')
|
||||||
|
|
||||||
|
const modelBasePath = path.join(this.modelPath, ...CRITICAL_MODEL_CONFIG.modelName.split('/'))
|
||||||
console.log(`🔍 Debug: Checking model at path: ${modelBasePath}`)
|
console.log(`🔍 Debug: Checking model at path: ${modelBasePath}`)
|
||||||
console.log(`🔍 Debug: Model path components: ${this.modelPath} + ${CRITICAL_MODEL_CONFIG.modelName.split('/')}`)
|
console.log(`🔍 Debug: Model path components: ${this.modelPath} + ${CRITICAL_MODEL_CONFIG.modelName.split('/')}`)
|
||||||
|
|
||||||
|
|
@ -159,17 +167,17 @@ export class ModelGuardian {
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const file of criticalFiles) {
|
for (const file of criticalFiles) {
|
||||||
const filePath = join(modelBasePath, file)
|
const filePath = path.join(modelBasePath, file)
|
||||||
console.log(`🔍 Debug: Checking file: ${filePath}`)
|
console.log(`🔍 Debug: Checking file: ${filePath}`)
|
||||||
|
|
||||||
if (!existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.log(`❌ Missing critical file: ${file} at ${filePath}`)
|
console.log(`❌ Missing critical file: ${file} at ${filePath}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify size for critical files
|
// Verify size for critical files
|
||||||
if (CRITICAL_MODEL_CONFIG.modelSize[file]) {
|
if (CRITICAL_MODEL_CONFIG.modelSize[file]) {
|
||||||
const stats = await stat(filePath)
|
const stats = await fsPromises.stat(filePath)
|
||||||
const expectedSize = CRITICAL_MODEL_CONFIG.modelSize[file]
|
const expectedSize = CRITICAL_MODEL_CONFIG.modelSize[file]
|
||||||
|
|
||||||
if (Math.abs(stats.size - expectedSize) > 1000) { // Allow 1KB variance
|
if (Math.abs(stats.size - expectedSize) > 1000) { // Allow 1KB variance
|
||||||
|
|
@ -263,24 +271,37 @@ export class ModelGuardian {
|
||||||
* Detect where models should be stored
|
* Detect where models should be stored
|
||||||
*/
|
*/
|
||||||
private detectModelPath(): string {
|
private detectModelPath(): string {
|
||||||
const candidates = [
|
// Browser always uses default path
|
||||||
process.env.BRAINY_MODELS_PATH,
|
if (typeof window !== 'undefined') {
|
||||||
'./models',
|
return './models'
|
||||||
join(process.cwd(), 'models'),
|
}
|
||||||
join(process.env.HOME || '', '.brainy', 'models'),
|
|
||||||
'/opt/models', // Lambda/container path
|
// Use require for synchronous access in Node.js
|
||||||
env.cacheDir
|
try {
|
||||||
]
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
for (const path of candidates) {
|
|
||||||
if (path && existsSync(path)) {
|
const candidates = [
|
||||||
const modelPath = join(path, ...CRITICAL_MODEL_CONFIG.modelName.split('/'))
|
process.env.BRAINY_MODELS_PATH,
|
||||||
if (existsSync(join(modelPath, 'onnx', 'model.onnx'))) {
|
'./models',
|
||||||
return path // Return the models directory, not its parent
|
path.join(process.cwd(), 'models'),
|
||||||
|
path.join(process.env.HOME || '', '.brainy', 'models'),
|
||||||
|
'/opt/models', // Lambda/container path
|
||||||
|
env.cacheDir
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const candidatePath of candidates) {
|
||||||
|
if (candidatePath && fs.existsSync(candidatePath)) {
|
||||||
|
const modelPath = path.join(candidatePath, ...CRITICAL_MODEL_CONFIG.modelName.split('/'))
|
||||||
|
if (fs.existsSync(path.join(modelPath, 'onnx', 'model.onnx'))) {
|
||||||
|
return candidatePath // Return the models directory, not its parent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If Node.js modules not available, return default
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
return './models'
|
return './models'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,8 @@ export class TransformerEmbedding implements EmbeddingModel {
|
||||||
const { createRequire } = await import('module')
|
const { createRequire } = await import('module')
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
const path = require('path')
|
const path = require('node:path')
|
||||||
const fs = require('fs')
|
const fs = require('node:fs')
|
||||||
|
|
||||||
// Try to resolve the package location
|
// Try to resolve the package location
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,33 @@
|
||||||
|
|
||||||
import { FindParams, AddParams, UpdateParams, RelateParams } from '../types/brainy.types.js'
|
import { FindParams, AddParams, UpdateParams, RelateParams } from '../types/brainy.types.js'
|
||||||
import { NounType, VerbType } from '../types/graphTypes.js'
|
import { NounType, VerbType } from '../types/graphTypes.js'
|
||||||
import * as os from 'node:os'
|
|
||||||
|
// Dynamic import for Node.js os module
|
||||||
|
let os: any = null
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
try {
|
||||||
|
os = await import('node:os')
|
||||||
|
} catch (e) {
|
||||||
|
// OS module not available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser-safe memory detection
|
||||||
|
const getSystemMemory = (): number => {
|
||||||
|
if (os) {
|
||||||
|
return os.totalmem()
|
||||||
|
}
|
||||||
|
// Browser fallback: assume 4GB
|
||||||
|
return 4 * 1024 * 1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAvailableMemory = (): number => {
|
||||||
|
if (os) {
|
||||||
|
return os.freemem()
|
||||||
|
}
|
||||||
|
// Browser fallback: assume 2GB available
|
||||||
|
return 2 * 1024 * 1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-configured limits based on system resources
|
* Auto-configured limits based on system resources
|
||||||
|
|
@ -27,8 +53,8 @@ class ValidationConfig {
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// Auto-configure based on system resources
|
// Auto-configure based on system resources
|
||||||
const totalMemory = os.totalmem()
|
const totalMemory = getSystemMemory()
|
||||||
const availableMemory = os.freemem()
|
const availableMemory = getAvailableMemory()
|
||||||
|
|
||||||
// Scale limits based on available memory
|
// Scale limits based on available memory
|
||||||
// 1GB = 10K limit, 8GB = 80K limit, etc.
|
// 1GB = 10K limit, 8GB = 80K limit, etc.
|
||||||
|
|
@ -224,8 +250,8 @@ export function getValidationConfig() {
|
||||||
maxLimit: config.maxLimit,
|
maxLimit: config.maxLimit,
|
||||||
maxQueryLength: config.maxQueryLength,
|
maxQueryLength: config.maxQueryLength,
|
||||||
maxVectorDimensions: config.maxVectorDimensions,
|
maxVectorDimensions: config.maxVectorDimensions,
|
||||||
systemMemory: os.totalmem(),
|
systemMemory: getSystemMemory(),
|
||||||
availableMemory: os.freemem()
|
availableMemory: getAvailableMemory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { performance } from 'perf_hooks'
|
import { performance } from 'perf_hooks'
|
||||||
import { hostname } from 'node:os'
|
import { randomUUID } from '../universal/crypto.js'
|
||||||
import { randomUUID } from 'node:crypto'
|
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
SILENT = -1,
|
SILENT = -1,
|
||||||
|
|
@ -312,10 +311,20 @@ export class StructuredLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.includeHost) {
|
if (this.config.includeHost) {
|
||||||
entry.host = hostname()
|
// Use dynamic import for hostname (Node.js only)
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
try {
|
||||||
|
const os = require('node:os')
|
||||||
|
entry.host = os.hostname()
|
||||||
|
} catch {
|
||||||
|
entry.host = 'unknown'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.host = window.location?.hostname || 'browser'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.includeMemory) {
|
if (this.config.includeMemory && typeof process !== 'undefined' && process.memoryUsage) {
|
||||||
const mem = process.memoryUsage()
|
const mem = process.memoryUsage()
|
||||||
entry.performance = {
|
entry.performance = {
|
||||||
memory: {
|
memory: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue