fix(8.0): gate native graph analytics on the provider readiness flag
The optional native graph-acceleration provider exposes `isInitialized`, which is false during its cold-start / rebuild window (engine + cursor not yet loaded). The accessor cached the resolved provider INSTANCE but never re-checked readiness, so `brain.graph.*` could dispatch to a not-ready provider and throw instead of answering. Resolve and cache the instance once, but check `isInitialized` LIVE on every call: route to the pure-TS analytics path while the provider is not ready, and re-engage the native path automatically the moment it flips true. This gates every native route at once — subgraph, export, rank, communities, path, and the query→expand fusion. Adds a test asserting the provider is never called (and the TS fallback returns real answers) while not ready. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bf0afe8563
commit
d321cf5f33
2 changed files with 65 additions and 19 deletions
|
|
@ -3299,25 +3299,30 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
|
|
||||||
/** Resolve the optional native graph-acceleration provider (feature-detected). */
|
/** Resolve the optional native graph-acceleration provider (feature-detected). */
|
||||||
private graphAccelerationProvider(): GraphAccelerationProvider | undefined {
|
private graphAccelerationProvider(): GraphAccelerationProvider | undefined {
|
||||||
if (this._graphAccelProvider !== undefined) {
|
// Resolve + cache the provider INSTANCE once. Accept EITHER a ready instance OR
|
||||||
return this._graphAccelProvider ?? undefined
|
// a `(storage) => provider` factory (the convention the graphIndex/metadataIndex/
|
||||||
}
|
// vector providers use) — a registered factory would otherwise fail the
|
||||||
// Resolve the optional 'graphAcceleration' provider once and cache it. Accept
|
// isGraphAccelerationProvider duck-test and the native path would silently never engage.
|
||||||
// EITHER a ready instance OR a `(storage) => provider` factory (the convention
|
if (this._graphAccelProvider === undefined) {
|
||||||
// the graphIndex/metadataIndex/vector providers use) — a registered factory
|
const raw = this.pluginRegistry.getProvider<unknown>('graphAcceleration')
|
||||||
// would otherwise fail the isGraphAccelerationProvider duck-test and the native
|
let resolved: unknown = raw
|
||||||
// path would silently never engage.
|
if (typeof raw === 'function' && !isGraphAccelerationProvider(raw)) {
|
||||||
const raw = this.pluginRegistry.getProvider<unknown>('graphAcceleration')
|
try {
|
||||||
let resolved: unknown = raw
|
resolved = (raw as (storage: StorageAdapter) => unknown)(this.storage)
|
||||||
if (typeof raw === 'function' && !isGraphAccelerationProvider(raw)) {
|
} catch {
|
||||||
try {
|
resolved = undefined
|
||||||
resolved = (raw as (storage: StorageAdapter) => unknown)(this.storage)
|
}
|
||||||
} catch {
|
|
||||||
resolved = undefined
|
|
||||||
}
|
}
|
||||||
|
this._graphAccelProvider = isGraphAccelerationProvider(resolved) ? resolved : null
|
||||||
}
|
}
|
||||||
this._graphAccelProvider = isGraphAccelerationProvider(resolved) ? resolved : null
|
const provider = this._graphAccelProvider ?? undefined
|
||||||
return this._graphAccelProvider ?? undefined
|
// Readiness gate — checked LIVE each call, never cached: the native engine sets
|
||||||
|
// `isInitialized` false until its engine + cursor have loaded (the cold-start /
|
||||||
|
// rebuild window). Until then, route to the pure-TS path rather than calling a
|
||||||
|
// not-ready provider (which throws) — and re-engage the native path automatically
|
||||||
|
// the moment `isInitialized` flips true. Gates EVERY `brain.graph.*` route
|
||||||
|
// (subgraph/export/rank/communities/path + the query→expand fusion) at once.
|
||||||
|
return provider && provider.isInitialized ? provider : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
||||||
import { createTestConfig } from '../../helpers/test-factory.js'
|
import { createTestConfig } from '../../helpers/test-factory.js'
|
||||||
|
|
||||||
/** A stateful mock GraphAccelerationProvider; the test sets the columnar payloads. */
|
/** A stateful mock GraphAccelerationProvider; the test sets the columnar payloads. */
|
||||||
function makeMockAccel() {
|
function makeMockAccel(opts: { isInitialized?: boolean } = {}) {
|
||||||
const calls = { traverse: 0, cursorOpen: 0, cursorNext: 0, cursorClose: 0, rank: 0, communities: 0, path: 0 }
|
const calls = { traverse: 0, cursorOpen: 0, cursorNext: 0, cursorClose: 0, rank: 0, communities: 0, path: 0 }
|
||||||
const state: {
|
const state: {
|
||||||
calls: typeof calls
|
calls: typeof calls
|
||||||
|
|
@ -44,7 +44,7 @@ function makeMockAccel() {
|
||||||
communityCount: 0
|
communityCount: 0
|
||||||
}
|
}
|
||||||
const provider = {
|
const provider = {
|
||||||
isInitialized: true,
|
isInitialized: opts.isInitialized ?? true,
|
||||||
traverse: async (seeds: any) => {
|
traverse: async (seeds: any) => {
|
||||||
calls.traverse++
|
calls.traverse++
|
||||||
state.lastTraverseSeeds = seeds
|
state.lastTraverseSeeds = seeds
|
||||||
|
|
@ -290,4 +290,45 @@ describe('brain.graph.* native routing + columnar hydration (native seam)', () =
|
||||||
expect(Array.isArray(mock.state.lastTraverseSeeds)).toBe(true) // bigint[], not a Buffer
|
expect(Array.isArray(mock.state.lastTraverseSeeds)).toBe(true) // bigint[], not a Buffer
|
||||||
expect(typeof (mock.state.lastTraverseSeeds as unknown[])[0]).toBe('bigint')
|
expect(typeof (mock.state.lastTraverseSeeds as unknown[])[0]).toBe('bigint')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Readiness gate (cor boundary-audit item #1): the native engine reports
|
||||||
|
// isInitialized=false during its cold-start/rebuild window. Brainy must route to the
|
||||||
|
// pure-TS path then — NOT call the not-ready provider (which would throw) — across the
|
||||||
|
// whole brain.graph.* surface, and re-engage the native path once it flips true.
|
||||||
|
it('routes to the TS fallback (never calls the provider) while accel.isInitialized is false', async () => {
|
||||||
|
const notReady = makeMockAccel({ isInitialized: false })
|
||||||
|
const b = new Brainy(createTestConfig())
|
||||||
|
b.use({
|
||||||
|
name: 'mock-graph-accel-not-ready',
|
||||||
|
activate: async (ctx: any) => {
|
||||||
|
ctx.registerProvider('graphAcceleration', notReady.provider)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} as any)
|
||||||
|
await b.init()
|
||||||
|
const p = await b.add({ type: NounType.Person, subtype: 'employee', data: 'P' })
|
||||||
|
const q = await b.add({ type: NounType.Person, subtype: 'employee', data: 'Q' })
|
||||||
|
await b.relate({ from: p, to: q, type: VerbType.RelatedTo })
|
||||||
|
|
||||||
|
// Every brain.graph.* route + the query→expand fusion must NOT throw and must serve
|
||||||
|
// from the TS fallback while the engine is cold.
|
||||||
|
const sub = await b.graph.subgraph(p, { depth: 1 })
|
||||||
|
const ranked = await b.graph.rank()
|
||||||
|
const comm = await b.graph.communities()
|
||||||
|
const route = await b.graph.path(p, q)
|
||||||
|
const fused = await b.graph.subgraph({ type: NounType.Person }, { depth: 1 })
|
||||||
|
|
||||||
|
// The provider was never touched — all served by the JS fallback.
|
||||||
|
expect(notReady.state.calls.traverse).toBe(0)
|
||||||
|
expect(notReady.state.calls.rank).toBe(0)
|
||||||
|
expect(notReady.state.calls.communities).toBe(0)
|
||||||
|
expect(notReady.state.calls.path).toBe(0)
|
||||||
|
// And the TS fallback returned real answers (not throws / empties from a cold engine).
|
||||||
|
expect(sub.nodes.some((n) => n.id === p)).toBe(true)
|
||||||
|
expect(ranked.length).toBeGreaterThan(0)
|
||||||
|
expect(comm.count).toBeGreaterThan(0)
|
||||||
|
expect(route?.nodes).toEqual([p, q])
|
||||||
|
expect(fused.nodes.some((n) => n.id === p)).toBe(true)
|
||||||
|
await b.close()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue