feat(8.0): zero-config finalize + cut JS quantization (config.vector = recall + persistMode)
This commit is contained in:
parent
f4c5d9749f
commit
f8e0079d3f
12 changed files with 348 additions and 1739 deletions
359
src/brainy.ts
359
src/brainy.ts
|
|
@ -116,7 +116,7 @@ import type { MigrationPreview, MigrationResult, MigrateOptions } from './migrat
|
|||
import { AggregationIndex } from './aggregation/AggregationIndex.js'
|
||||
import { AggregateMaterializer } from './aggregation/materializer.js'
|
||||
import type { AggregateDefinition, AggregateQueryParams, AggregateResult } from './types/brainy.types.js'
|
||||
import { resolveJsHnswConfig } from './utils/recallPreset.js'
|
||||
import { resolveJsHnswConfig, DEFAULT_RECALL } from './utils/recallPreset.js'
|
||||
import * as fs from 'node:fs'
|
||||
import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js'
|
||||
import { GenerationStore } from './db/generationStore.js'
|
||||
|
|
@ -205,6 +205,8 @@ type ResolvedBrainyConfig = Required<
|
|||
| 'vector'
|
||||
| 'plugins'
|
||||
| 'integrations'
|
||||
| 'history'
|
||||
| 'eagerEmbeddings'
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
|
|
@ -215,6 +217,8 @@ type ResolvedBrainyConfig = Required<
|
|||
| 'vector'
|
||||
| 'plugins'
|
||||
| 'integrations'
|
||||
| 'history'
|
||||
| 'eagerEmbeddings'
|
||||
>
|
||||
|
||||
/**
|
||||
|
|
@ -387,6 +391,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
private _readyResolve: (() => void) | null = null
|
||||
private _readyReject: ((error: Error) => void) | null = null
|
||||
|
||||
// In-flight init() promise. Guards against concurrent initialization when
|
||||
// multiple operations race to lazily initialize the same instance — all
|
||||
// racers await the single in-flight run. Reset on failure so init() can
|
||||
// be retried after the underlying cause (e.g. a held writer lock) clears.
|
||||
private initPromise: Promise<void> | null = null
|
||||
|
||||
// Set once close() has torn the instance down. close() is terminal: a closed
|
||||
// instance must NOT silently lazy-re-initialize on the next operation (that
|
||||
// would resurrect released indexes and timers). The lazy-init convenience
|
||||
// applies only to instances that were never closed.
|
||||
private closed = false
|
||||
|
||||
// Lazy rebuild state (Production-scale lazy loading)
|
||||
// Prevents race conditions when multiple queries trigger rebuild simultaneously
|
||||
private lazyRebuildInProgress = false
|
||||
|
|
@ -542,13 +558,42 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Initialize Brainy - MUST be called before use
|
||||
* Initialize Brainy.
|
||||
*
|
||||
* **Calling this explicitly is optional.** Every public operation lazily
|
||||
* initializes the instance on first use (`new Brainy()` followed directly
|
||||
* by `add()` / `find()` / `transact()` just works). Call `init()` yourself
|
||||
* when you want to control *when* the startup cost is paid (e.g. during
|
||||
* server boot rather than on the first request) or to pass init-time
|
||||
* configuration overrides.
|
||||
*
|
||||
* Idempotent and concurrency-safe: repeat calls after success return
|
||||
* immediately; calls racing an in-flight initialization await that single
|
||||
* run (overrides are only applied by the run that starts initialization).
|
||||
*
|
||||
* @param overrides Optional configuration overrides for init
|
||||
*/
|
||||
async init(overrides?: Partial<BrainyConfig & { dimensions?: number }>): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
if (this.initPromise) {
|
||||
return this.initPromise
|
||||
}
|
||||
const run = this.performInit(overrides)
|
||||
this.initPromise = run
|
||||
try {
|
||||
await run
|
||||
} finally {
|
||||
this.initPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The single initialization run guarded by `init()`. Never call directly —
|
||||
* always go through `init()` (or any public method, which lazily inits).
|
||||
*/
|
||||
private async performInit(overrides?: Partial<BrainyConfig & { dimensions?: number }>): Promise<void> {
|
||||
|
||||
// Apply any init-time configuration overrides
|
||||
if (overrides) {
|
||||
|
|
@ -566,7 +611,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
...this.config,
|
||||
...configOverrides,
|
||||
storage: mergedStorage,
|
||||
index: { ...this.config.index, ...configOverrides.index },
|
||||
verbose: configOverrides.verbose ?? this.config.verbose,
|
||||
silent: configOverrides.silent ?? this.config.silent
|
||||
}
|
||||
|
|
@ -702,32 +746,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
setMsgpackImplementation(msgpackProvider)
|
||||
}
|
||||
|
||||
// Provider: native SQ8 approximate distance (e.g. cortex's Rust SIMD) — swaps
|
||||
// the JS quantized-distance used in HNSW SQ8 reranking. Signature is
|
||||
// byte-compatible with the JS distanceSQ8; falls back to JS when absent.
|
||||
const sq8DistanceProvider = this.pluginRegistry.getProvider<
|
||||
(a: Uint8Array, aMin: number, aMax: number, b: Uint8Array, bMin: number, bMax: number) => number
|
||||
>('distance:sq8')
|
||||
if (sq8DistanceProvider) {
|
||||
const { setSQ8DistanceImplementation } = await import('./utils/vectorQuantization.js')
|
||||
setSQ8DistanceImplementation(sq8DistanceProvider)
|
||||
}
|
||||
|
||||
// Provider: native SQ4 approximate distance (cortex's Rust). Same swap
|
||||
// pattern as SQ8; signature is byte-compatible with the JS distanceSQ4
|
||||
// (4-bit quantization range, packed nibbles). Used in HNSW SQ4 reranking
|
||||
// when config.hnsw.quantization.bits === 4. Falls back to JS when absent.
|
||||
const sq4DistanceProvider = this.pluginRegistry.getProvider<
|
||||
(
|
||||
a: Uint8Array, aMin: number, aMax: number, aDim: number,
|
||||
b: Uint8Array, bMin: number, bMax: number, bDim: number
|
||||
) => number
|
||||
>('distance:sq4')
|
||||
if (sq4DistanceProvider) {
|
||||
const { setSQ4DistanceImplementation } = await import('./utils/vectorQuantization.js')
|
||||
setSQ4DistanceImplementation(sq4DistanceProvider)
|
||||
}
|
||||
|
||||
// Provider: sort:topK (e.g. cortex's native partial-sort / heap-select) — swaps the
|
||||
// JS result-ranking used by find() to pick the top `offset + limit` rows. The provider
|
||||
// returns indices into a scores array, ordered descending with stable ties, identical
|
||||
|
|
@ -831,11 +849,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
// Connect distributed components to storage
|
||||
await this.connectDistributedStorage()
|
||||
|
||||
// Warm up if configured
|
||||
if (this.config.warmup) {
|
||||
await this.warmup()
|
||||
}
|
||||
|
||||
// Register shutdown hooks for graceful count flushing (once globally)
|
||||
if (!Brainy.shutdownHooksRegisteredGlobally) {
|
||||
this.registerShutdownHooks()
|
||||
|
|
@ -875,12 +888,34 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
await this._vfs.init()
|
||||
this._vfsInitialized = true // Mark VFS as fully initialized
|
||||
|
||||
// Eager embedding initialization for cloud deployments
|
||||
// When eagerEmbeddings is true, initialize the WASM embedding engine now
|
||||
// instead of lazily on first embed() call. This moves the 90-140 second
|
||||
// WASM compilation to container startup rather than first request.
|
||||
// Recommended for: Cloud Run, Lambda, Fargate, Kubernetes
|
||||
if (this.config.eagerEmbeddings && !this.pluginRegistry.hasProvider('embeddings')) {
|
||||
// Eager embedding initialization.
|
||||
//
|
||||
// Adaptive default (8.0): the WASM embedding engine eagerly initializes
|
||||
// during init() WHENEVER it is the active embedder — i.e. no native
|
||||
// 'embeddings' provider has taken over — and the instance is a writer
|
||||
// (not reader-mode) outside of unit tests. The WASM module (≈93MB with
|
||||
// the embedded model) takes 90-140s to compile on throttled CPUs; paying
|
||||
// that during boot rather than on the first embed()-driven call is the
|
||||
// right default for the overwhelmingly common single-process server.
|
||||
//
|
||||
// Skipped automatically when:
|
||||
// - a native 'embeddings' provider is registered (it owns embeddings;
|
||||
// the WASM engine is dead weight),
|
||||
// - reader-mode (readers don't embed — they query existing vectors),
|
||||
// - unit-test mode (tests must stay fast and use the mock embedder).
|
||||
//
|
||||
// `eagerEmbeddings: false` is the explicit override to force lazy init
|
||||
// (first-embed) even when this instance is the active embedder.
|
||||
const isUnitTestMode =
|
||||
process.env.BRAINY_UNIT_TEST === 'true' ||
|
||||
(globalThis as { __BRAINY_UNIT_TEST__?: boolean }).__BRAINY_UNIT_TEST__ === true
|
||||
const eager = this.config.eagerEmbeddings ?? true
|
||||
if (
|
||||
eager &&
|
||||
!this.pluginRegistry.hasProvider('embeddings') &&
|
||||
this.config.mode !== 'reader' &&
|
||||
!isUnitTestMode
|
||||
) {
|
||||
console.log('Eager embedding initialization enabled...')
|
||||
await embeddingManager.init()
|
||||
console.log('Embedding engine ready')
|
||||
|
|
@ -1026,11 +1061,24 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Ensure Brainy is initialized
|
||||
* Ensure Brainy is initialized, lazily initializing on first use.
|
||||
*
|
||||
* Called at the top of every public operation, so `new Brainy()` followed
|
||||
* directly by `add()` / `find()` / any other call just works — forgetting
|
||||
* `init()` is not an error. Concurrent first operations share a single
|
||||
* initialization run (see `init()`); if initialization fails, the error
|
||||
* propagates to the operation that triggered it.
|
||||
*
|
||||
* `close()` is terminal: a closed instance throws here instead of silently
|
||||
* re-initializing — using a closed Brainy is a consumer bug, not a lazy-init
|
||||
* opportunity.
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (this.closed) {
|
||||
throw new Error('Brainy instance is not initialized: it was closed via close(). Create a new instance.')
|
||||
}
|
||||
if (!this.initialized) {
|
||||
throw new Error('Brainy not initialized. Call init() first.')
|
||||
await this.init()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5143,6 +5191,68 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
return this.generationStore.compact(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Resolve the effective generational-history retention policy
|
||||
* from `config.history`, filling per-field defaults so a partial object
|
||||
* inherits the rest. This is the single read site for the policy referenced
|
||||
* by {@link autoCompactHistory}.
|
||||
*
|
||||
* Defaults (zero-config): keep the 100 most recent committed generations
|
||||
* AND everything committed within the last 7 days; auto-compact on
|
||||
* `flush()` / `close()`. A generation is reclaimed only when it is older
|
||||
* than BOTH bounds and no live `Db` pin protects it.
|
||||
*
|
||||
* @returns The resolved policy: retention bounds plus the `autoCompact` flag.
|
||||
*/
|
||||
private resolveHistoryPolicy(): {
|
||||
retainGenerations: number
|
||||
retainMs: number
|
||||
autoCompact: boolean
|
||||
} {
|
||||
const history = this.config.history
|
||||
return {
|
||||
retainGenerations: history?.retainGenerations ?? 100,
|
||||
retainMs: history?.retainMs ?? 604_800_000, // 7 days
|
||||
autoCompact: history?.autoCompact ?? true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Run history compaction with the resolved retention policy if
|
||||
* `config.history.autoCompact` is on (the default). Invoked from `flush()`
|
||||
* and `close()` so generational record-sets cannot accumulate unbounded
|
||||
* across a long-lived writer's lifetime.
|
||||
*
|
||||
* Read-only instances and an explicit `autoCompact: false` skip silently.
|
||||
* Pinned generations (live `Db` values) are never reclaimed — the safety
|
||||
* invariant lives in {@link GenerationStore.compact}. Failures are logged
|
||||
* and swallowed: compaction is housekeeping and must never fail a flush or
|
||||
* a clean shutdown.
|
||||
*/
|
||||
private async autoCompactHistory(): Promise<void> {
|
||||
// Nothing to compact on a read-only instance or before init wired up the
|
||||
// generation store (close() can run defensively on an uninitialized brain).
|
||||
if (this.isReadOnly || !this.generationStore) {
|
||||
return
|
||||
}
|
||||
const policy = this.resolveHistoryPolicy()
|
||||
if (!policy.autoCompact) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.generationStore.compact({
|
||||
retainGenerations: policy.retainGenerations,
|
||||
retainMs: policy.retainMs
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Auto-compaction of generational history failed (non-fatal): ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Replace this store's ENTIRE state from a snapshot directory
|
||||
* previously produced by `db.persist(path)`. Destructive: current
|
||||
|
|
@ -6858,6 +6968,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
this.generationStore.persistCounterNow()
|
||||
])
|
||||
|
||||
// 6. Auto-compact generational history per config.history (default on).
|
||||
// Runs after the flush so durable state is in place; respects live
|
||||
// Db pins and an explicit autoCompact: false.
|
||||
await this.autoCompactHistory()
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
|
||||
console.log(`All indexes flushed to disk in ${elapsed}ms`)
|
||||
|
|
@ -10167,14 +10282,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
return this.embedder(textToEmbed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm up the system
|
||||
*/
|
||||
private async warmup(): Promise<void> {
|
||||
// Warm up embedder
|
||||
await this.embed('warmup')
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly warm up the embedding engine
|
||||
*
|
||||
|
|
@ -10300,26 +10407,22 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
/**
|
||||
* Setup index — single unified vector graph.
|
||||
*
|
||||
* Brainy 8.0 ships filesystem-only storage (no cloud adapters), so persistence
|
||||
* mode defaults to `'immediate'` everywhere. Operators who want bulk-ingest
|
||||
* speed can set `config.vector.persistMode = 'deferred'`.
|
||||
* Persistence mode is auto-selected from the storage adapter (see
|
||||
* {@link resolveHNSWPersistMode}): `'immediate'` on filesystem storage,
|
||||
* `'deferred'` on memory storage. Operators can override with an explicit
|
||||
* `config.vector.persistMode` (e.g. `'deferred'` for bulk-ingest speed on
|
||||
* filesystem storage).
|
||||
*/
|
||||
private setupIndex(): JsHnswVectorIndex {
|
||||
// 8.0 config surface: config.vector.{recall, quantization, persistMode}.
|
||||
// 8.0 config surface: config.vector.{recall, persistMode}.
|
||||
// The recall preset translates to HNSW knobs (M / efConstruction / efSearch)
|
||||
// via resolveJsHnswConfig. No algorithm-internal knobs are exposed at the
|
||||
// public surface.
|
||||
// public surface. The JS index computes exact float32 distances throughout —
|
||||
// there is no quantization path.
|
||||
const recallKnobs = resolveJsHnswConfig(this.config.vector)
|
||||
const vectorCfg = this.config.vector
|
||||
const indexConfig = {
|
||||
...this.config.index,
|
||||
...recallKnobs,
|
||||
distanceFunction: this.distance,
|
||||
quantization: vectorCfg?.quantization ? {
|
||||
enabled: vectorCfg.quantization.enabled ?? false,
|
||||
bits: vectorCfg.quantization.bits ?? 8,
|
||||
rerankMultiplier: 3
|
||||
} : undefined
|
||||
distanceFunction: this.distance
|
||||
}
|
||||
|
||||
const persistMode = this.resolveHNSWPersistMode()
|
||||
|
|
@ -10346,8 +10449,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
const persistMode = this.resolveHNSWPersistMode()
|
||||
const vectorFactory = this.pluginRegistry.getProvider<(config: any, distance: DistanceFunction, options: any) => any>('vector')
|
||||
if (vectorFactory) {
|
||||
// Native providers receive the algorithm-neutral surface: the resolved
|
||||
// recall preset name (per the 8.0 provider contract they translate it
|
||||
// to their own internal knobs), plus the JS HNSW knob tuple for
|
||||
// providers that mirror the open-core implementation. Quantization at
|
||||
// scale is the native provider's own concern (e.g. DiskANN PQ) — Brainy
|
||||
// exposes no quantization knob.
|
||||
return vectorFactory(
|
||||
{ ...this.config.index, distanceFunction: this.distance },
|
||||
{
|
||||
...resolveJsHnswConfig(this.config.vector),
|
||||
recall: this.config.vector?.recall ?? DEFAULT_RECALL,
|
||||
distanceFunction: this.distance
|
||||
},
|
||||
this.distance,
|
||||
{ storage: this.storage, persistMode }
|
||||
)
|
||||
|
|
@ -10356,15 +10469,23 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Resolve HNSW persistence mode.
|
||||
* Extracted so both setupIndex() and the HNSW plugin factory path can use it.
|
||||
* Resolve the vector persistence mode from the storage adapter.
|
||||
*
|
||||
* Brainy 8.0 ships filesystem-only; the cloud-storage smart-default
|
||||
* (deferred for cloud, immediate for local) collapses to "always
|
||||
* immediate unless the user overrides via config.vector.persistMode".
|
||||
* Auto-selected when `config.vector.persistMode` is omitted:
|
||||
* - **filesystem** (and any persistent adapter): `'immediate'` — per-add
|
||||
* durability is the point of a persistent backend.
|
||||
* - **memory**: `'deferred'` — nothing survives the process anyway, so
|
||||
* per-add persistence writes are pure overhead; deferred batches them
|
||||
* into `flush()` / `close()`.
|
||||
*
|
||||
* An explicit `config.vector.persistMode` always wins (the operator
|
||||
* escape hatch for bulk-ingest pipelines on filesystem storage).
|
||||
*/
|
||||
private resolveHNSWPersistMode(): 'immediate' | 'deferred' {
|
||||
return this.config.vector?.persistMode ?? 'immediate'
|
||||
if (this.config.vector?.persistMode) {
|
||||
return this.config.vector.persistMode
|
||||
}
|
||||
return this.getStorageType() === 'memory' ? 'deferred' : 'immediate'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -10390,59 +10511,40 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
)
|
||||
}
|
||||
|
||||
// Validate numeric configurations
|
||||
if (config?.index?.m && (config.index.m < 1 || config.index.m > 128)) {
|
||||
throw new Error(`Invalid index m parameter: ${config.index.m}. Must be between 1 and 128`)
|
||||
}
|
||||
|
||||
if (config?.index?.efConstruction && (config.index.efConstruction < 1 || config.index.efConstruction > 1000)) {
|
||||
throw new Error(`Invalid index efConstruction: ${config.index.efConstruction}. Must be between 1 and 1000`)
|
||||
}
|
||||
|
||||
if (config?.index?.efSearch && (config.index.efSearch < 1 || config.index.efSearch > 1000)) {
|
||||
throw new Error(`Invalid index efSearch: ${config.index.efSearch}. Must be between 1 and 1000`)
|
||||
}
|
||||
|
||||
// Auto-detect distributed mode based on environment and configuration
|
||||
const distributedConfig = this.autoDetectDistributed(config?.distributed)
|
||||
// Distributed mode is explicit opt-in only (config or BRAINY_DISTRIBUTED
|
||||
// env var) — never inferred from deployment heuristics.
|
||||
const distributedConfig = this.resolveDistributedConfig(config?.distributed)
|
||||
|
||||
return {
|
||||
storage: config?.storage || { type: 'auto' },
|
||||
index: config?.index || {},
|
||||
cache: config?.cache ?? true,
|
||||
distributed: distributedConfig,
|
||||
warmup: config?.warmup ?? false,
|
||||
realtime: config?.realtime ?? false,
|
||||
multiTenancy: config?.multiTenancy ?? false,
|
||||
telemetry: config?.telemetry ?? false,
|
||||
verbose: config?.verbose ?? false,
|
||||
silent: config?.silent ?? false,
|
||||
// New performance options with smart defaults
|
||||
disableAutoRebuild: config?.disableAutoRebuild ?? false, // false = auto-decide based on size
|
||||
disableMetrics: config?.disableMetrics ?? false,
|
||||
disableAutoOptimize: config?.disableAutoOptimize ?? false,
|
||||
batchWrites: config?.batchWrites ?? true,
|
||||
maxConcurrentOperations: config?.maxConcurrentOperations ?? 10,
|
||||
// Memory management options
|
||||
// false = auto-decide based on dataset size (inline vs lazy rebuild)
|
||||
disableAutoRebuild: config?.disableAutoRebuild ?? false,
|
||||
// Memory management escape hatches over the RAM-derived auto limits
|
||||
maxQueryLimit: config?.maxQueryLimit ?? undefined,
|
||||
reservedQueryMemory: config?.reservedQueryMemory ?? undefined,
|
||||
// Vector index configuration (8.0) — algorithm-neutral surface with
|
||||
// `recall` preset + `persistMode` (folded in from 7.x's hnswPersistMode).
|
||||
// See BRAINY-8.0-RENAME-COORDINATION § A.2 + § G.2.
|
||||
vector: config?.vector ?? undefined,
|
||||
// Embedding initialization - false = lazy init on first embed()
|
||||
eagerEmbeddings: config?.eagerEmbeddings ?? false,
|
||||
// Generational-history retention — defaults applied at the read site
|
||||
// (resolveHistoryPolicy) so a partial object inherits per-field defaults.
|
||||
history: config?.history ?? undefined,
|
||||
// Embedding initialization — left undefined when omitted so the adaptive
|
||||
// default resolves at the init() read site (eager when the WASM embedder
|
||||
// is the active one on a non-reader writer outside unit tests). Explicit
|
||||
// true/false always wins. See the eagerEmbeddings JSDoc in brainy.types.ts.
|
||||
eagerEmbeddings: config?.eagerEmbeddings ?? undefined,
|
||||
// Plugin configuration - undefined = auto-detect
|
||||
plugins: config?.plugins ?? undefined,
|
||||
// Integration Hub - undefined/false = disabled
|
||||
integrations: config?.integrations ?? undefined,
|
||||
// Migration — disabled by default, opt-in for automatic migration
|
||||
autoMigrate: config?.autoMigrate ?? false,
|
||||
// Subtype pairing enforcement (7.30.0) — opt-in.
|
||||
// false: only per-type rules registered via brain.requireSubtype() apply.
|
||||
// true: every public write path requires subtype on every type.
|
||||
// { except: [...] }: strict, but listed types may omit subtype.
|
||||
// Becomes the default in 8.0.0.
|
||||
// Migration — auto-run by default. Pending data migrations apply inline
|
||||
// during init() for small datasets (<10K entities); larger datasets
|
||||
// defer with a notice so the operator can schedule brain.migrate().
|
||||
autoMigrate: config?.autoMigrate ?? true,
|
||||
// Subtype required-by-default (8.0 — per BRAINY-8.0-SUBTYPE-CONTRACT § C-1).
|
||||
// Every write path now requires `subtype` on every type unless the consumer
|
||||
// opts out explicitly. Opt-out: `requireSubtype: false` (last-resort migration
|
||||
|
|
@ -10926,6 +11028,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
* This ensures deferred persistence mode data is saved
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// Phase 0: Auto-compact generational history per config.history (default
|
||||
// on) BEFORE the generation store closes below. Respects live Db pins and
|
||||
// an explicit autoCompact: false; no-op on read-only instances.
|
||||
await this.autoCompactHistory()
|
||||
|
||||
// Phase 1: Flush ALL components in parallel to persist buffered data
|
||||
// This is critical when cortex native providers buffer data in Rust memory
|
||||
await Promise.all([
|
||||
|
|
@ -11033,26 +11140,34 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
|
||||
this.initialized = false
|
||||
// close() is terminal: block lazy re-initialization on any subsequent
|
||||
// operation (ensureInitialized() throws once this is set).
|
||||
this.closed = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligently auto-detect distributed configuration
|
||||
* Zero-config: Automatically determines best distributed settings
|
||||
*/
|
||||
private autoDetectDistributed(config?: BrainyConfig['distributed']): BrainyConfig['distributed'] {
|
||||
// If explicitly disabled, respect that
|
||||
if (config?.enabled === false) {
|
||||
return config
|
||||
}
|
||||
|
||||
// Auto-detect based on environment variables (common in production)
|
||||
const envEnabled = process.env.BRAINY_DISTRIBUTED === 'true' ||
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.CLUSTER_SIZE ||
|
||||
process.env.KUBERNETES_SERVICE_HOST // Running in K8s
|
||||
|
||||
// If not explicitly configured but environment suggests distributed
|
||||
if (!config && envEnabled) {
|
||||
/**
|
||||
* Resolve distributed-cluster configuration. **Explicit opt-in only**:
|
||||
* either `config.distributed.enabled === true` or the
|
||||
* `BRAINY_DISTRIBUTED=true` environment variable. Once opted in, the
|
||||
* remaining fields default sensibly (64 shards, 3 replicas, raft, http,
|
||||
* hostname-derived nodeId).
|
||||
*
|
||||
* Earlier versions auto-enabled cluster mode from deployment heuristics
|
||||
* (`NODE_ENV=production`, a Kubernetes service host, `CLUSTER_SIZE`).
|
||||
* That violated zero-config safety: a single-process production
|
||||
* deployment — the overwhelmingly common case — would silently start a
|
||||
* coordinator, bind a port, and run raft against itself. Deployment
|
||||
* environment does not imply a multi-node Brainy cluster, so the
|
||||
* heuristics are gone.
|
||||
*/
|
||||
private resolveDistributedConfig(config?: BrainyConfig['distributed']): BrainyConfig['distributed'] {
|
||||
// Env-var opt-in for deployments that can't touch code (the ONLY
|
||||
// environment signal honoured — it names brainy explicitly).
|
||||
if (!config && process.env.BRAINY_DISTRIBUTED === 'true') {
|
||||
return {
|
||||
enabled: true,
|
||||
nodeId: process.env.HOSTNAME || process.env.NODE_ID || `node-${Date.now()}`,
|
||||
|
|
@ -11065,7 +11180,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
// Merge with provided config, applying intelligent defaults
|
||||
// Explicit config: fill the per-field defaults.
|
||||
return config ? {
|
||||
...config,
|
||||
nodeId: config.nodeId || process.env.HOSTNAME || `node-${Date.now()}`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue