**feat: enhance threading compatibility and refactor module loading**

- Refactored `TextEncoder` and `TextDecoder` in `src/utils/tensorflowUtils.ts` to handle environments without global availability, logging warnings and providing fallback implementations.
- Enhanced threading utilities in `src/utils/environment.ts`:
  - Introduced `areWorkerThreadsAvailableSync()` and `isThreadingAvailableAsync()` for synchronous and asynchronous checks.
  - Improved compatibility across browser and Node.js environments.
- Migrated `fileSystemStorage.ts` to use dynamic imports for Node.js modules, removing synchronous loading mechanisms to align with ES module standards.
- Updated `cli-package/package.json` keywords to include `browser` and `container`, improving discoverability.
- Added `worker_threads` as an external dependency in `cli-package/rollup.config.js` and `rollup.config.js`.
- Updated `.gitignore` to ignore all `tgz` files dynamically under main and CLI package.

This update improves threading compatibility, enhances environment-specific support, and aligns with modern module loading practices.
This commit is contained in:
David Snelling 2025-07-02 16:52:44 -07:00
parent cf18cd99c4
commit 703fc9eda3
8 changed files with 89 additions and 104 deletions

4
.gitignore vendored
View file

@ -54,6 +54,6 @@ Thumbs.db
/cli-package/node_modules/ /cli-package/node_modules/
/npm /npm
/rollup /rollup
/soulcraft-brainy-0.9.23.tgz /soulcraft-brainy-*.tgz
/cli-package/soulcraft-brainy-cli-0.9.23.tgz
/data/ /data/
/cli-package/soulcraft-brainy-cli-*.tgz

View file

@ -1,16 +1,16 @@
{ {
"name": "@soulcraft/brainy-cli", "name": "@soulcraft/brainy-cli",
"version": "0.9.29", "version": "0.9.30",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@soulcraft/brainy-cli", "name": "@soulcraft/brainy-cli",
"version": "0.9.29", "version": "0.9.30",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@soulcraft/brainy": "0.9.29", "@soulcraft/brainy": "0.9.30",
"commander": "^14.0.0", "commander": "^14.0.0",
"omelette": "^0.4.17" "omelette": "^0.4.17"
}, },
@ -2086,9 +2086,9 @@
} }
}, },
"node_modules/@soulcraft/brainy": { "node_modules/@soulcraft/brainy": {
"version": "0.9.29", "version": "0.9.30",
"resolved": "https://registry.npmjs.org/@soulcraft/brainy/-/brainy-0.9.29.tgz", "resolved": "https://registry.npmjs.org/@soulcraft/brainy/-/brainy-0.9.30.tgz",
"integrity": "sha512-t1kECxPqO7Y8dC6Vh2BoWQKYtiHCUPombz6ZGNfiyhADMEf8T3E1dd7XGmAX8qUver9/aXKk1I1pfIXA7RP5Cw==", "integrity": "sha512-CPI0XQLwp2KIy31dK6mzJfbW+F36dx1pG0ORmoY6tDRtRe7w39PaW+fj0Q/p8RpaBxAK5PaBgl/0rN+8bmsrsA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View file

@ -21,7 +21,8 @@
"vector-database", "vector-database",
"hnsw", "hnsw",
"cli", "cli",
"command-line", "browser",
"container",
"graph-database" "graph-database"
], ],
"author": "David Snelling (david@soulcraft.com)", "author": "David Snelling (david@soulcraft.com)",

View file

@ -37,6 +37,7 @@ export default {
'fs', 'fs',
'path', 'path',
'url', 'url',
'child_process' 'child_process',
'worker_threads'
] ]
} }

View file

@ -176,8 +176,11 @@ const mainConfig = {
'@aws-crypto/crc32c', '@aws-crypto/crc32c',
'node:stream/web', 'node:stream/web',
'node:worker_threads', 'node:worker_threads',
'worker_threads',
'node:fs', 'node:fs',
'node:path' 'node:path',
'fs',
'path'
] ]
} }

View file

@ -1,51 +1,12 @@
import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js' import { GraphVerb, HNSWNoun, StorageAdapter } from '../coreTypes.js'
// Import Node.js built-in modules // Import Node.js built-in modules
// Using require for compatibility with Node.js // Using dynamic imports for compatibility with ES modules
let fs: any let fs: any
let path: any let path: any
// Initialize these modules immediately if in Node.js environment // We'll initialize these modules in the init() method
if ( // No synchronous loading here to avoid issues with ES modules
typeof process !== 'undefined' &&
process.versions &&
process.versions.node
) {
try {
// Use require for Node.js built-in modules
fs = require('fs')
path = require('path')
// Verify that path has the required methods
if (!path || typeof path.resolve !== 'function') {
throw new Error('path module is missing required methods')
}
} catch (e) {
console.warn('Failed to load Node.js modules with require:', e)
// Try using dynamic import as a fallback
try {
// Use a synchronous approach to ensure modules are loaded before continuing
const pathModule = require('node:path')
const fsModule = require('node:fs')
path = pathModule
fs = fsModule
// Verify that path has the required methods
if (!path || typeof path.resolve !== 'function') {
throw new Error(
'path module from node:path is missing required methods'
)
}
} catch (nodeImportError) {
console.warn(
'Failed to load Node.js modules with node: prefix:',
nodeImportError
)
}
}
}
// Constants for directory and file names // Constants for directory and file names
const ROOT_DIR = 'brainy-data' const ROOT_DIR = 'brainy-data'
@ -111,37 +72,7 @@ export class FileSystemStorage implements StorageAdapter {
// Try multiple approaches to load the modules // Try multiple approaches to load the modules
const loadAttempts = [ const loadAttempts = [
// Attempt 1: Use require // Attempt 1: Use dynamic import
async () => {
console.log('Attempting to load Node.js modules with require()')
const fsModule = require('fs')
const pathModule = require('path')
if (!pathModule || typeof pathModule.resolve !== 'function') {
throw new Error('path.resolve is not a function after require()')
}
return { fs: fsModule, path: pathModule }
},
// Attempt 2: Use require with node: prefix
async () => {
console.log(
'Attempting to load Node.js modules with require("node:...")'
)
const fsModule = require('node:fs')
const pathModule = require('node:path')
if (!pathModule || typeof pathModule.resolve !== 'function') {
throw new Error(
'path.resolve is not a function after require("node:path")'
)
}
return { fs: fsModule, path: pathModule }
},
// Attempt 3: Use dynamic import
async () => { async () => {
console.log( console.log(
'Attempting to load Node.js modules with dynamic import' 'Attempting to load Node.js modules with dynamic import'
@ -161,7 +92,7 @@ export class FileSystemStorage implements StorageAdapter {
return { fs: fsResolved, path: pathResolved } return { fs: fsResolved, path: pathResolved }
}, },
// Attempt 4: Use dynamic import with node: prefix // Attempt 2: Use dynamic import with node: prefix
async () => { async () => {
console.log( console.log(
'Attempting to load Node.js modules with dynamic import("node:...")' 'Attempting to load Node.js modules with dynamic import("node:...")'

View file

@ -6,53 +6,75 @@
* Check if code is running in a browser environment * Check if code is running in a browser environment
*/ */
export function isBrowser(): boolean { export function isBrowser(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined'; return typeof window !== 'undefined' && typeof document !== 'undefined'
} }
/** /**
* Check if code is running in a Node.js environment * Check if code is running in a Node.js environment
*/ */
export function isNode(): boolean { export function isNode(): boolean {
return typeof process !== 'undefined' && return (
typeof process !== 'undefined' &&
process.versions != null && process.versions != null &&
process.versions.node != null; process.versions.node != null
)
} }
/** /**
* Check if code is running in a Web Worker environment * Check if code is running in a Web Worker environment
*/ */
export function isWebWorker(): boolean { export function isWebWorker(): boolean {
return typeof self === 'object' && return (
typeof self === 'object' &&
self.constructor && self.constructor &&
self.constructor.name === 'DedicatedWorkerGlobalScope'; self.constructor.name === 'DedicatedWorkerGlobalScope'
)
} }
/** /**
* Check if Web Workers are available in the current environment * Check if Web Workers are available in the current environment
*/ */
export function areWebWorkersAvailable(): boolean { export function areWebWorkersAvailable(): boolean {
return isBrowser() && typeof Worker !== 'undefined'; return isBrowser() && typeof Worker !== 'undefined'
} }
/** /**
* Check if Worker Threads are available in the current environment (Node.js) * Check if Worker Threads are available in the current environment (Node.js)
*/ */
export function areWorkerThreadsAvailable(): boolean { export async function areWorkerThreadsAvailable(): Promise<boolean> {
if (!isNode()) return false; if (!isNode()) return false
try { try {
// Dynamic import to avoid errors in browser environments // Use dynamic import to avoid errors in browser environments
require('worker_threads'); await import('worker_threads')
return true; return true
} catch (e) { } catch (e) {
return false; 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 12+, worker_threads is available without requiring a flag
return parseInt(process.versions.node.split('.')[0]) >= 12
}
/** /**
* Determine if threading is available in the current environment * Determine if threading is available in the current environment
* Returns true if either Web Workers (browser) or Worker Threads (Node.js) are available * Returns true if either Web Workers (browser) or Worker Threads (Node.js) are available
*/ */
export function isThreadingAvailable(): boolean { export function isThreadingAvailable(): boolean {
return areWebWorkersAvailable() || areWorkerThreadsAvailable(); return areWebWorkersAvailable() || areWorkerThreadsAvailableSync()
}
/**
* Async version of isThreadingAvailable
*/
export async function isThreadingAvailableAsync(): Promise<boolean> {
return areWebWorkersAvailable() || (await areWorkerThreadsAvailable())
} }

View file

@ -33,13 +33,40 @@ if (
isFloat32Array, isFloat32Array,
isTypedArray, isTypedArray,
// Use the global TextEncoder/TextDecoder directly // Use the global TextEncoder/TextDecoder directly
TextEncoder: typeof TextEncoder !== 'undefined' ? TextEncoder : null, TextEncoder:
TextDecoder: typeof TextDecoder !== 'undefined' ? TextDecoder : null typeof TextEncoder !== 'undefined'
? TextEncoder
: function () {
console.warn(
'TextEncoder constructor called but not available'
)
return { encode: (str: string) => new Uint8Array([]) }
},
TextDecoder:
typeof TextDecoder !== 'undefined'
? TextDecoder
: function () {
console.warn(
'TextDecoder constructor called but not available'
)
return { decode: (bytes: Uint8Array) => '' }
}
} }
// Initialize TextEncoder/TextDecoder directly from globals // Initialize TextEncoder/TextDecoder directly from globals
if (typeof TextEncoder !== 'undefined') {
this.textEncoder = new TextEncoder() this.textEncoder = new TextEncoder()
} else {
console.warn('TextEncoder is not available in this environment')
this.textEncoder = { encode: (str: string) => new Uint8Array([]) }
}
if (typeof TextDecoder !== 'undefined') {
this.textDecoder = new TextDecoder() this.textDecoder = new TextDecoder()
} else {
console.warn('TextDecoder is not available in this environment')
this.textDecoder = { decode: (bytes: Uint8Array) => '' }
}
} }
} }