2025-08-29 15:39:07 -07:00
/ * *
* Storage Configuration Auto - Detection
* Intelligently selects storage based on environment and available services
* /
import { isBrowser , isNode } from '../utils/environment.js'
/ * *
* Low - level storage implementation types
* /
export enum StorageType {
MEMORY = 'memory' ,
FILESYSTEM = 'filesystem' ,
OPFS = 'opfs' ,
S3 = 's3' ,
GCS = 'gcs' ,
R2 = 'r2'
}
/ * *
* High - level storage presets ( maps to StorageType )
* /
export enum StoragePreset {
AUTO = 'auto' ,
MEMORY = 'memory' ,
DISK = 'disk' ,
CLOUD = 'cloud'
}
// Backward compatibility type aliases
export type StorageTypeString = 'memory' | 'filesystem' | 'opfs' | 's3' | 'gcs' | 'r2'
export type StoragePresetString = 'auto' | 'memory' | 'disk' | 'cloud'
export interface StorageConfigResult {
type : StorageType | StorageTypeString // Support both enum and string for compatibility
config : any
reason : string
autoSelected : boolean
}
/ * *
* Auto - detect the best storage configuration
* @param override - Manual override : specific type or preset
* /
export async function autoDetectStorage (
override? : StorageType | StoragePreset | StorageTypeString | StoragePresetString | any
) : Promise < StorageConfigResult > {
// Handle direct storage config object
if ( override && typeof override === 'object' ) {
return {
type : override . type || 'memory' ,
config : override ,
reason : 'Manually configured storage' ,
autoSelected : false
}
}
// Handle storage type override (enum values or strings)
if ( override && Object . values ( StorageType ) . includes ( override as StorageType ) ) {
return {
type : override as StorageType ,
config : override ,
reason : ` Manually specified: ${ override } ` ,
autoSelected : false
}
}
// Handle presets (both enum and string values)
if ( override === StoragePreset . MEMORY || override === 'memory' ) {
return {
type : StorageType . MEMORY ,
config : StorageType.MEMORY ,
reason : 'Preset: memory storage' ,
autoSelected : false
}
}
if ( override === StoragePreset . DISK || override === 'disk' ) {
const diskType = isBrowser ( ) ? StorageType.OPFS : StorageType.FILESYSTEM
return {
type : diskType ,
config : diskType ,
reason : ` Preset: disk storage ( ${ diskType } ) ` ,
autoSelected : false
}
}
if ( override === StoragePreset . CLOUD || override === 'cloud' ) {
const cloudStorage = await detectCloudStorage ( )
if ( cloudStorage ) {
return {
. . . cloudStorage ,
reason : ` Preset: cloud storage ( ${ cloudStorage . type } ) ` ,
autoSelected : false
}
}
// Fallback to disk if no cloud storage detected
const diskType = isBrowser ( ) ? StorageType.OPFS : StorageType.FILESYSTEM
return {
type : diskType ,
config : diskType ,
reason : 'Preset: cloud (none detected, using disk)' ,
autoSelected : false
}
}
// Auto-detection logic
return await autoDetectBestStorage ( )
}
/ * *
* Automatically detect the best storage option
* /
async function autoDetectBestStorage ( ) : Promise < StorageConfigResult > {
// Check for cloud storage first (highest priority in production)
const cloudStorage = await detectCloudStorage ( )
if ( cloudStorage && process . env . NODE_ENV === 'production' ) {
return {
. . . cloudStorage ,
reason : ` Auto-detected ${ cloudStorage . type } in production ` ,
autoSelected : true
}
}
// Browser environment
if ( isBrowser ( ) ) {
// Check for OPFS support
if ( await hasOPFSSupport ( ) ) {
return {
type : StorageType . OPFS ,
config : { requestPersistentStorage : true } ,
reason : 'Browser with OPFS support detected' ,
autoSelected : true
}
}
// Fallback to memory for browsers without OPFS
return {
type : StorageType . MEMORY ,
config : StorageType.MEMORY ,
reason : 'Browser without OPFS - using memory' ,
autoSelected : true
}
}
// Serverless environment - prefer memory or cloud
if ( isServerlessEnvironment ( ) ) {
if ( cloudStorage ) {
return {
. . . cloudStorage ,
reason : ` Serverless with ${ cloudStorage . type } detected ` ,
autoSelected : true
}
}
return {
type : StorageType . MEMORY ,
config : StorageType.MEMORY ,
reason : 'Serverless environment - using memory' ,
autoSelected : true
}
}
// Node.js environment - use filesystem
if ( isNode ( ) ) {
const dataPath = await findBestDataPath ( )
return {
type : StorageType . FILESYSTEM ,
config : StorageType.FILESYSTEM ,
reason : ` Node.js environment - using filesystem at ${ dataPath } ` ,
autoSelected : true
}
}
// Fallback to memory
return {
type : StorageType . MEMORY ,
config : StorageType.MEMORY ,
reason : 'Default fallback - using memory' ,
autoSelected : true
}
}
/ * *
* Detect cloud storage from environment variables
* /
async function detectCloudStorage ( ) : Promise < { type : StorageType ; config : any } | null > {
// AWS S3 Detection
if ( hasAWSConfig ( ) ) {
return {
type : StorageType . S3 ,
config : {
s3Storage : {
bucketName : process.env.AWS_BUCKET || process . env . S3_BUCKET_NAME || 'brainy-data' ,
region : process.env.AWS_REGION || process . env . AWS_DEFAULT_REGION || 'us-east-1' ,
// Credentials will be picked up by AWS SDK automatically
}
}
}
}
// Google Cloud Storage Detection
if ( hasGCPConfig ( ) ) {
return {
type : StorageType . GCS ,
config : {
gcsStorage : {
bucketName : process.env.GCS_BUCKET || process . env . GOOGLE_STORAGE_BUCKET || 'brainy-data' ,
// Credentials will be picked up by GCP SDK automatically
}
}
}
}
// Cloudflare R2 Detection
if ( hasR2Config ( ) ) {
return {
type : StorageType . R2 ,
config : {
r2Storage : {
bucketName : process.env.R2_BUCKET || 'brainy-data' ,
accountId : process.env.CLOUDFLARE_ACCOUNT_ID ,
accessKeyId : process.env.R2_ACCESS_KEY_ID ,
secretAccessKey : process.env.R2_SECRET_ACCESS_KEY
}
}
}
}
return null
}
/ * *
* Check if AWS S3 is configured
* /
function hasAWSConfig ( ) : boolean {
return ! ! (
// Explicit S3 bucket
process . env . AWS_BUCKET ||
process . env . S3_BUCKET_NAME ||
// AWS credentials (SDK will find them)
process . env . AWS_ACCESS_KEY_ID ||
process . env . AWS_PROFILE ||
// AWS environment indicators
process . env . AWS_EXECUTION_ENV ||
process . env . AWS_LAMBDA_FUNCTION_NAME ||
process . env . ECS_CONTAINER_METADATA_URI
)
}
/ * *
* Check if Google Cloud Storage is configured
* /
function hasGCPConfig ( ) : boolean {
return ! ! (
// Explicit GCS bucket
process . env . GCS_BUCKET ||
process . env . GOOGLE_STORAGE_BUCKET ||
// GCP credentials
process . env . GOOGLE_APPLICATION_CREDENTIALS ||
// GCP environment indicators
process . env . GOOGLE_CLOUD_PROJECT ||
process . env . K_SERVICE ||
process . env . GAE_SERVICE
)
}
/ * *
* Check if Cloudflare R2 is configured
* /
function hasR2Config ( ) : boolean {
return ! ! (
process . env . R2_BUCKET ||
( process . env . CLOUDFLARE_ACCOUNT_ID && process . env . R2_ACCESS_KEY_ID )
)
}
/ * *
* Check if running in serverless environment
* /
function isServerlessEnvironment ( ) : boolean {
if ( ! isNode ( ) ) return false
return ! ! (
process . env . AWS_LAMBDA_FUNCTION_NAME ||
process . env . VERCEL ||
process . env . NETLIFY ||
process . env . CLOUDFLARE_WORKERS ||
process . env . FUNCTIONS_WORKER_RUNTIME ||
process . env . K_SERVICE ||
process . env . RAILWAY_ENVIRONMENT ||
process . env . FLY_APP_NAME
)
}
/ * *
* Check for OPFS support in browser
* /
async function hasOPFSSupport ( ) : Promise < boolean > {
if ( ! isBrowser ( ) ) return false
try {
return 'storage' in navigator &&
'getDirectory' in navigator . storage
} catch {
return false
}
}
/ * *
* Find the best path for filesystem storage
* /
async function findBestDataPath ( ) : Promise < string > {
if ( ! isNode ( ) ) return './brainy-data'
const homeDir = process . env . HOME || process . env . USERPROFILE || '~'
const tempDir = process . env . TMPDIR || process . env . TEMP || '/tmp'
const candidates = [
// User-specified path
process . env . BRAINY_DATA_PATH ,
// Current directory
'./brainy-data' ,
// Home directory
` ${ homeDir } /.brainy/data ` ,
// Temp directory (last resort)
` ${ tempDir } /brainy-data `
] . filter ( Boolean ) as string [ ]
// Find first writable directory
for ( const candidate of candidates ) {
if ( await isWritable ( candidate ) ) {
return candidate
}
}
// Default fallback
return candidates [ 1 ] // ./brainy-data
}
/ * *
* Check if a directory is writable
* /
async function isWritable ( dirPath : string ) : Promise < boolean > {
if ( ! isNode ( ) ) return false
try {
// Dynamic import fs for Node.js
feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00
const { promises : fs } = await import ( 'node:fs' )
const path = await import ( 'node:path' )
2025-08-29 15:39:07 -07:00
// Try to create directory if it doesn't exist
await fs . mkdir ( dirPath , { recursive : true } )
// Try to write a test file
const testFile = path . join ( dirPath , '.write-test' )
await fs . writeFile ( testFile , 'test' )
await fs . unlink ( testFile )
return true
} catch {
return false
}
}
// Legacy getStorageConfig function removed - now using simple string types
/ * *
* Log storage configuration decision
* /
export function logStorageConfig ( config : StorageConfigResult , verbose : boolean = false ) : void {
if ( ! verbose && process . env . NODE_ENV === 'production' ) {
return // Silent in production unless verbose
}
const icon = config . autoSelected ? '🤖' : '👤'
console . log ( ` ${ icon } Storage: ${ config . type . toUpperCase ( ) } - ${ config . reason } ` )
}