chore(8.0): final pre-RC1 sweep — API consistency, named errors, orphans, zero-cast codebase
This commit is contained in:
parent
970e08c466
commit
1f7e365a4e
237 changed files with 1951 additions and 49413 deletions
|
|
@ -193,6 +193,27 @@ function getVerbMetadataPath(id: string): string {
|
|||
return `entities/verbs/${shard}/${id}/metadata.json`
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional count capabilities probed via duck typing by getNouns()/getVerbs().
|
||||
* Adapters with a native O(1) count API may implement these; they are not part
|
||||
* of the BaseStorageAdapter contract, so BaseStorage feature-detects them at
|
||||
* runtime before falling back to scan-based counting.
|
||||
*/
|
||||
interface OptionalCountCapabilities {
|
||||
countNouns?: (filter?: {
|
||||
nounType?: string | string[]
|
||||
service?: string | string[]
|
||||
metadata?: Record<string, unknown>
|
||||
}) => Promise<number>
|
||||
countVerbs?: (filter?: {
|
||||
verbType?: string | string[]
|
||||
sourceId?: string | string[]
|
||||
targetId?: string | string[]
|
||||
service?: string | string[]
|
||||
metadata?: Record<string, unknown>
|
||||
}) => Promise<number>
|
||||
}
|
||||
|
||||
/**
|
||||
* Base storage adapter that implements common functionality
|
||||
* This is an abstract class that should be extended by specific storage adapters
|
||||
|
|
@ -454,9 +475,14 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
public invalidateGraphIndex(): void {
|
||||
if (this.graphIndex) {
|
||||
prodLog.info('[BaseStorage] Invalidating GraphAdjacencyIndex for clear()')
|
||||
// Stop any pending operations
|
||||
if (typeof (this.graphIndex as any).stopAutoFlush === 'function') {
|
||||
(this.graphIndex as any).stopAutoFlush()
|
||||
// Stop any pending operations. stopAutoFlush is an optional capability
|
||||
// of plugin-provided graph indexes (duck-typed; the built-in
|
||||
// GraphAdjacencyIndex does not implement it).
|
||||
const flushable = this.graphIndex as GraphAdjacencyIndex & {
|
||||
stopAutoFlush?: () => void
|
||||
}
|
||||
if (typeof flushable.stopAutoFlush === 'function') {
|
||||
flushable.stopAutoFlush()
|
||||
}
|
||||
this.graphIndex = undefined
|
||||
this.graphIndexPromise = undefined
|
||||
|
|
@ -738,7 +764,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
*
|
||||
* The hook fires once per entity-visible metadata mutation — noun/verb
|
||||
* metadata saves and deletes, the one storage write every logical Brainy
|
||||
* mutation (`add`, `update`, `delete`, `relate`, `updateRelation`,
|
||||
* mutation (`add`, `update`, `remove`, `relate`, `updateRelation`,
|
||||
* `unrelate`) performs exactly once per entity it touches. It does NOT fire
|
||||
* for derived-index writes (HNSW node data, metadata-index chunks, LSM
|
||||
* segments, statistics), so the generation counter tracks *data* mutations,
|
||||
|
|
@ -1380,9 +1406,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// First, try to get a count of total nouns (if the adapter supports it)
|
||||
let totalCount: number | undefined = undefined
|
||||
try {
|
||||
// This is an optional method that adapters may implement
|
||||
if (typeof (this as any).countNouns === 'function') {
|
||||
totalCount = await (this as any).countNouns(options?.filter)
|
||||
// This is an optional method that adapters may implement (duck-typed —
|
||||
// see OptionalCountCapabilities)
|
||||
const adapter = this as BaseStorage & OptionalCountCapabilities
|
||||
if (typeof adapter.countNouns === 'function') {
|
||||
totalCount = await adapter.countNouns(options?.filter)
|
||||
}
|
||||
} catch (countError) {
|
||||
// Ignore errors from count method, it's optional
|
||||
|
|
@ -1390,9 +1418,16 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
|
||||
// Check if the adapter has a paginated method for getting nouns
|
||||
if (typeof (this as any).getNounsWithPagination === 'function') {
|
||||
// Use the adapter's paginated method - pass offset directly to adapter
|
||||
const result = await (this as any).getNounsWithPagination({
|
||||
if (typeof this.getNounsWithPagination === 'function') {
|
||||
// Use the adapter's paginated method - pass offset directly to adapter.
|
||||
// The annotation widens totalCount to optional: adapter overrides
|
||||
// follow BaseStorageAdapter's contract, where totalCount may be absent.
|
||||
const result: {
|
||||
items: HNSWNounWithMetadata[]
|
||||
totalCount?: number
|
||||
hasMore: boolean
|
||||
nextCursor?: string
|
||||
} = await this.getNounsWithPagination({
|
||||
limit,
|
||||
offset, // Let the adapter handle offset for O(1) operation
|
||||
cursor,
|
||||
|
|
@ -1556,7 +1591,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
/**
|
||||
* Get verbs with pagination (Type-first implementation with billion-scale optimizations)
|
||||
*
|
||||
* CRITICAL: This method is required for brain.getRelations() to work!
|
||||
* CRITICAL: This method is required for brain.related() to work!
|
||||
* Iterates through verb types with the same optimizations as nouns.
|
||||
*
|
||||
* ARCHITECTURE: Reads storage directly (not indexes) to avoid circular dependencies.
|
||||
|
|
@ -1605,11 +1640,17 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
const filterTargetIds = filter?.targetId
|
||||
? new Set(Array.isArray(filter.targetId) ? filter.targetId : [filter.targetId])
|
||||
: null
|
||||
const filterSubtypes = (filter as any)?.subtype
|
||||
// `subtype` rides alongside the declared filter fields (Brainy's
|
||||
// related() path passes it through); it's applied after metadata
|
||||
// loads below, since subtype lives in verb metadata, not on the raw verb.
|
||||
const subtypeFilterValue = (
|
||||
filter as { verbType?: string | string[]; subtype?: string | string[] } | undefined
|
||||
)?.subtype
|
||||
const filterSubtypes = subtypeFilterValue
|
||||
? new Set(
|
||||
Array.isArray((filter as any).subtype)
|
||||
? (filter as any).subtype
|
||||
: [(filter as any).subtype]
|
||||
Array.isArray(subtypeFilterValue)
|
||||
? subtypeFilterValue
|
||||
: [subtypeFilterValue]
|
||||
)
|
||||
: null
|
||||
|
||||
|
|
@ -1652,7 +1693,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
|
||||
// Apply subtype filter (requires metadata — checked AFTER load)
|
||||
if (filterSubtypes) {
|
||||
const subtype = (metadata as any)?.subtype as string | undefined
|
||||
const subtype = metadata?.subtype as string | undefined
|
||||
if (!subtype || !filterSubtypes.has(subtype)) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1725,9 +1766,12 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// subtype check after the load). Subtype is not stored on the raw HNSWVerb;
|
||||
// it's a metadata field. The graph-index fast paths return verbs without
|
||||
// metadata, so they can't apply subtype filtering correctly.
|
||||
if (options?.filter && !(options.filter as any).subtype) {
|
||||
if (
|
||||
options?.filter &&
|
||||
!(options.filter as { verbType?: string | string[]; subtype?: string | string[] }).subtype
|
||||
) {
|
||||
// CRITICAL VFS FIX: If filtering by sourceId + verbType (most common VFS pattern!)
|
||||
// This is the query PathResolver.getChildren() uses: getRelations({ from: dirId, type: VerbType.Contains })
|
||||
// This is the query PathResolver.getChildren() uses: related({ from: dirId, type: VerbType.Contains })
|
||||
if (
|
||||
options.filter.sourceId &&
|
||||
options.filter.verbType &&
|
||||
|
|
@ -1921,9 +1965,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// First, try to get a count of total verbs (if the adapter supports it)
|
||||
let totalCount: number | undefined = undefined
|
||||
try {
|
||||
// This is an optional method that adapters may implement
|
||||
if (typeof (this as any).countVerbs === 'function') {
|
||||
totalCount = await (this as any).countVerbs(options?.filter)
|
||||
// This is an optional method that adapters may implement (duck-typed —
|
||||
// see OptionalCountCapabilities)
|
||||
const adapter = this as BaseStorage & OptionalCountCapabilities
|
||||
if (typeof adapter.countVerbs === 'function') {
|
||||
totalCount = await adapter.countVerbs(options?.filter)
|
||||
}
|
||||
} catch (countError) {
|
||||
// Ignore errors from count method, it's optional
|
||||
|
|
@ -1931,13 +1977,24 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
|
||||
// Check if the adapter has a paginated method for getting verbs
|
||||
if (typeof (this as any).getVerbsWithPagination === 'function') {
|
||||
if (typeof this.getVerbsWithPagination === 'function') {
|
||||
// Use the adapter's paginated method
|
||||
// Convert offset to cursor if no cursor provided (adapters use cursor for offset)
|
||||
const effectiveCursor = cursor || (offset > 0 ? offset.toString() : undefined)
|
||||
|
||||
const result = await (this as any).getVerbsWithPagination({
|
||||
// offset is carried via effectiveCursor for adapters with cursor-based
|
||||
// pagination; 0 here matches the long-standing destructure default in
|
||||
// getVerbsWithPagination (this call never passed offset). The
|
||||
// annotation widens totalCount to optional: adapter overrides follow
|
||||
// BaseStorageAdapter's contract, where totalCount may be absent.
|
||||
const result: {
|
||||
items: HNSWVerbWithMetadata[]
|
||||
totalCount?: number
|
||||
hasMore: boolean
|
||||
nextCursor?: string
|
||||
} = await this.getVerbsWithPagination({
|
||||
limit,
|
||||
offset: 0,
|
||||
cursor: effectiveCursor,
|
||||
filter: options?.filter
|
||||
})
|
||||
|
|
@ -2398,7 +2455,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
* **Use cases:**
|
||||
* - VFS tree traversal (fetch all children at once)
|
||||
* - brain.find() result hydration (batch load entities)
|
||||
* - brain.getRelations() target entities (eliminate N+1)
|
||||
* - brain.related() target entities (eliminate N+1)
|
||||
* - Import operations (batch existence checks)
|
||||
*
|
||||
* @param ids Array of entity IDs to fetch
|
||||
|
|
@ -2571,8 +2628,12 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
private async readBatchFromAdapter(paths: string[]): Promise<Map<string, any>> {
|
||||
if (paths.length === 0) return new Map()
|
||||
|
||||
// Check if this class implements batch operations (will be added to cloud adapters)
|
||||
const selfWithBatch = this as any
|
||||
// Check if this class implements batch operations (will be added to cloud
|
||||
// adapters). Duck-typed optional capability — readBatch is not part of the
|
||||
// BaseStorageAdapter contract.
|
||||
const selfWithBatch = this as BaseStorage & {
|
||||
readBatch?: (paths: string[]) => Promise<Map<string, unknown>>
|
||||
}
|
||||
|
||||
if (typeof selfWithBatch.readBatch === 'function') {
|
||||
// Adapter has native batch support - use it
|
||||
|
|
@ -2693,7 +2754,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
await this.ensureInitialized()
|
||||
|
||||
// Extract verb type from metadata for ID-first path
|
||||
const verbType = (metadata as any).verb as VerbType | undefined
|
||||
const verbType = metadata.verb as VerbType | undefined
|
||||
|
||||
if (!verbType) {
|
||||
// Backward compatibility: fallback to old path if no verb type
|
||||
|
|
@ -2729,7 +2790,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
this.decrementVerbSubtypeCount(priorEntry.verb, priorEntry.subtype)
|
||||
} else if (!priorEntry && priorVerbForSubtype && !isNew) {
|
||||
// Edge case: cache miss but metadata existed with a subtype (e.g. reader process startup).
|
||||
const priorSubFromMeta = (existingMetadata as any)?.subtype as string | undefined
|
||||
const priorSubFromMeta = existingMetadata?.subtype as string | undefined
|
||||
if (priorSubFromMeta && (priorSubFromMeta !== newSubtype || priorVerbForSubtype !== verbType)) {
|
||||
this.decrementVerbSubtypeCount(priorVerbForSubtype, priorSubFromMeta)
|
||||
}
|
||||
|
|
@ -3706,7 +3767,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
*
|
||||
* **Use cases:**
|
||||
* - VFS tree traversal (get Contains edges for multiple directories)
|
||||
* - brain.getRelations() for multiple entities
|
||||
* - brain.related() for multiple entities
|
||||
* - Graph traversal (fetch neighbors of multiple nodes)
|
||||
*
|
||||
* @param sourceIds Array of source entity IDs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue