2025-08-26 12:32:21 -07:00
/ * *
* Custom error types for Brainy operations
* Provides better error classification and handling
* /
2026-05-15 12:31:28 -07:00
export type BrainyErrorType =
| 'TIMEOUT'
| 'NETWORK'
| 'STORAGE'
| 'NOT_FOUND'
| 'RETRY_EXHAUSTED'
| 'VALIDATION'
| 'FIELD_NOT_INDEXED'
fix(8.0): never serve a silent [] from find({connected}) on a cold-loaded graph
8.0 shipped with no cold-graph guard (the 7.33.4 fix was never ported), so on a
cold open of a large brain where the native adjacency reports membership but its
source->target edges did not load, find({connected})/neighbors()/related() could
silently return [] for persisted edges.
Add the converged isReady() contract: GraphIndexProvider.isReady?(): boolean is
the honest cold-load readiness signal (true ONLY when edges are loaded), so brainy
gates on it instead of the lying membership-size() proxy. verifyGraphAdjacencyLive()
checks it — Strategy 1: false -> hydrate the id-mapper, rebuild from storage, re-check,
throw GraphIndexNotReadyError if still not ready; providers without isReady() fall
back to a global known-edge-sample probe (Strategy 2, not the queried anchor, so a
genuinely edgeless node still returns []). rebuildIndexesIfNeeded gates the graph
rebuild on it too, with the id-mapper hydrated before the adjacency rebuild on the
lazy cold-open path (the CTX-BR-RESTORE-REBUILD ordering, shared with restore()).
executeGraphSearch re-verifies before trusting an empty connected result and
re-collects on a heal. 6-case integration test + 1718 unit green.
2026-06-30 09:30:11 -07:00
| 'GRAPH_INDEX_NOT_READY'
2025-08-26 12:32:21 -07:00
/ * *
* Custom error class for Brainy operations
* Provides error type classification and retry information
* /
export class BrainyError extends Error {
public readonly type : BrainyErrorType
public readonly retryable : boolean
public readonly originalError? : Error
public readonly attemptNumber? : number
public readonly maxRetries? : number
constructor (
message : string ,
type : BrainyErrorType ,
retryable : boolean = false ,
originalError? : Error ,
attemptNumber? : number ,
maxRetries? : number
) {
super ( message )
this . name = 'BrainyError'
this . type = type
this . retryable = retryable
this . originalError = originalError
this . attemptNumber = attemptNumber
this . maxRetries = maxRetries
// Maintain proper stack trace for where our error was thrown (only available on V8)
if ( Error . captureStackTrace ) {
Error . captureStackTrace ( this , BrainyError )
}
}
/ * *
* Create a timeout error
* /
static timeout ( operation : string , timeoutMs : number , originalError? : Error ) : BrainyError {
return new BrainyError (
` Operation ' ${ operation } ' timed out after ${ timeoutMs } ms ` ,
'TIMEOUT' ,
true ,
originalError
)
}
/ * *
* Create a network error
* /
static network ( message : string , originalError? : Error ) : BrainyError {
return new BrainyError (
` Network error: ${ message } ` ,
'NETWORK' ,
true ,
originalError
)
}
/ * *
* Create a storage error
* /
static storage ( message : string , originalError? : Error ) : BrainyError {
return new BrainyError (
` Storage error: ${ message } ` ,
'STORAGE' ,
true ,
originalError
)
}
/ * *
* Create a not found error
* /
static notFound ( resource : string ) : BrainyError {
return new BrainyError (
` Resource not found: ${ resource } ` ,
'NOT_FOUND' ,
false
)
}
/ * *
* Create a retry exhausted error
* /
static retryExhausted ( operation : string , maxRetries : number , lastError? : Error ) : BrainyError {
return new BrainyError (
` Operation ' ${ operation } ' failed after ${ maxRetries } retry attempts ` ,
'RETRY_EXHAUSTED' ,
false ,
lastError ,
maxRetries ,
maxRetries
)
}
2026-05-15 12:31:28 -07:00
/ * *
* Create a "field is not indexed" error . Thrown by metadata - index reads
* when a ` where ` clause names a field that has neither a column - store
* entry nor a sparse - index entry . Callers in ` find() ` evaluation catch
* this , translate the offending clause to an empty result , and log so
* the silent - empty behavior is replaced with a loud one . Use
* ` brain.explain({ where: {...} }) ` to discover this before running .
* /
static fieldNotIndexed ( field : string ) : BrainyError {
return new BrainyError (
` Field " ${ field } " is not indexed. find()/where will not match any entities. ` +
` Likely causes: (1) the writer registered the field in memory but has not flushed; ` +
` (2) the field name is mistyped; (3) no entity has ever held this field. ` +
` Run brain.explain({ where: { ${ field } : ... } }) for the diagnostic. ` ,
'FIELD_NOT_INDEXED' ,
false
)
}
2025-09-11 16:23:32 -07:00
/ * *
* Create a validation error
* /
static validation ( parameter : string , constraint : string , value? : any ) : BrainyError {
return new BrainyError (
` Invalid ${ parameter } : ${ constraint } ` ,
'VALIDATION' ,
false
)
}
2025-08-26 12:32:21 -07:00
/ * *
* Check if an error is retryable
* /
static isRetryable ( error : Error ) : boolean {
if ( error instanceof BrainyError ) {
return error . retryable
}
// Check for common retryable error patterns
const message = error . message . toLowerCase ( )
const name = error . name . toLowerCase ( )
// Network-related errors that are typically retryable
if (
message . includes ( 'timeout' ) ||
message . includes ( 'network' ) ||
message . includes ( 'connection' ) ||
message . includes ( 'econnreset' ) ||
message . includes ( 'enotfound' ) ||
message . includes ( 'etimedout' ) ||
name . includes ( 'timeout' )
) {
return true
}
// AWS SDK specific retryable errors
if (
message . includes ( 'throttling' ) ||
message . includes ( 'rate limit' ) ||
message . includes ( 'service unavailable' ) ||
message . includes ( 'internal server error' ) ||
message . includes ( 'bad gateway' ) ||
message . includes ( 'gateway timeout' )
) {
return true
}
return false
}
/ * *
* Convert a generic error to a BrainyError with appropriate classification
* /
static fromError ( error : Error , operation? : string ) : BrainyError {
if ( error instanceof BrainyError ) {
return error
}
const message = error . message . toLowerCase ( )
const name = error . name . toLowerCase ( )
// Classify the error based on common patterns
if ( message . includes ( 'timeout' ) || name . includes ( 'timeout' ) ) {
return BrainyError . timeout ( operation || 'unknown' , 0 , error )
}
if (
message . includes ( 'network' ) ||
message . includes ( 'connection' ) ||
message . includes ( 'econnreset' ) ||
message . includes ( 'enotfound' ) ||
message . includes ( 'etimedout' )
) {
return BrainyError . network ( error . message , error )
}
if (
message . includes ( 'nosuchkey' ) ||
message . includes ( 'not found' ) ||
message . includes ( 'does not exist' )
) {
return BrainyError . notFound ( operation || 'resource' )
}
2025-09-11 16:23:32 -07:00
if (
message . includes ( 'invalid' ) ||
message . includes ( 'validation' ) ||
message . includes ( 'cannot be null' ) ||
message . includes ( 'must be' )
) {
return new BrainyError ( error . message , 'VALIDATION' , false , error )
}
2025-08-26 12:32:21 -07:00
// Default to storage error for unclassified errors
return BrainyError . storage ( error . message , error )
}
}
fix(8.0): never serve a silent [] from find({connected}) on a cold-loaded graph
8.0 shipped with no cold-graph guard (the 7.33.4 fix was never ported), so on a
cold open of a large brain where the native adjacency reports membership but its
source->target edges did not load, find({connected})/neighbors()/related() could
silently return [] for persisted edges.
Add the converged isReady() contract: GraphIndexProvider.isReady?(): boolean is
the honest cold-load readiness signal (true ONLY when edges are loaded), so brainy
gates on it instead of the lying membership-size() proxy. verifyGraphAdjacencyLive()
checks it — Strategy 1: false -> hydrate the id-mapper, rebuild from storage, re-check,
throw GraphIndexNotReadyError if still not ready; providers without isReady() fall
back to a global known-edge-sample probe (Strategy 2, not the queried anchor, so a
genuinely edgeless node still returns []). rebuildIndexesIfNeeded gates the graph
rebuild on it too, with the id-mapper hydrated before the adjacency rebuild on the
lazy cold-open path (the CTX-BR-RESTORE-REBUILD ordering, shared with restore()).
executeGraphSearch re-verifies before trusting an empty connected result and
re-collects on a heal. 6-case integration test + 1718 unit green.
2026-06-30 09:30:11 -07:00
/ * *
* Thrown when the graph adjacency index reports that relationships exist ( its
* persisted manifest / count loaded , or its readiness signal says otherwise ) but
* the source → target adjacency itself did NOT load — so graph traversals
* ( ` find({ connected }) ` , ` neighbors() ` , ` related() ` ) would otherwise return an
* EMPTY array indistinguishable from "no edges" .
*
* On 8.0 brainy detects this on the first graph read via the provider ' s honest
* sync ` isReady() ` signal ( true ONLY when the edges are loaded ; see
* { @link import ( '../plugin.js' ) . GraphIndexProvider . isReady } ) ; for older providers
* that do not expose it , it falls back to a known - edge - sample probe ( one persisted
* verb + one neighbor lookup ) . Either way it attempts a rebuild from storage and
* raises this LOUD , catchable error only if even that cannot make the adjacency
* ready — replacing silent data - invisibility with a clear failure .
*
* Observed with a native graph provider whose cold - open adjacency load is
* swallowed on certain storage adapters ; the fix is upstream in the provider ,
* but Brainy refuses to serve ` [] ` as if it were truth .
* /
export class GraphIndexNotReadyError extends BrainyError {
constructor ( message : string , originalError? : Error ) {
super ( message , 'GRAPH_INDEX_NOT_READY' , false , originalError )
this . name = 'GraphIndexNotReadyError'
if ( Error . captureStackTrace ) {
Error . captureStackTrace ( this , GraphIndexNotReadyError )
}
}
}