2025-08-26 12:32:21 -07:00
|
|
|
/**
|
|
|
|
|
* Base Storage Adapter
|
|
|
|
|
* Provides common functionality for all storage adapters, including statistics tracking
|
|
|
|
|
*/
|
|
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
import {
|
|
|
|
|
StatisticsData,
|
|
|
|
|
StorageAdapter,
|
|
|
|
|
HNSWNoun,
|
|
|
|
|
HNSWVerb,
|
|
|
|
|
GraphVerb,
|
|
|
|
|
HNSWNounWithMetadata,
|
|
|
|
|
HNSWVerbWithMetadata,
|
|
|
|
|
NounMetadata,
|
|
|
|
|
VerbMetadata
|
|
|
|
|
} from '../../coreTypes.js'
|
2025-10-30 08:54:04 -07:00
|
|
|
import { StorageBatchConfig } from '../baseStorage.js'
|
2025-08-26 12:32:21 -07:00
|
|
|
import { extractFieldNamesFromJson, mapToStandardField } from '../../utils/fieldNameTracking.js'
|
2025-09-22 15:45:35 -07:00
|
|
|
import { getGlobalMutex, cleanupMutexes } from '../../utils/mutex.js'
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Base class for storage adapters that implements statistics tracking
|
|
|
|
|
*/
|
|
|
|
|
export abstract class BaseStorageAdapter implements StorageAdapter {
|
|
|
|
|
// Abstract methods that must be implemented by subclasses
|
|
|
|
|
abstract init(): Promise<void>
|
|
|
|
|
|
fix: metadata explosion bug - 69K files reduced to ~1K
Critical fix for metadata indexing that was creating 60+ chunk files per entity.
Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).
Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)
Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading
Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing
Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:10:31 -07:00
|
|
|
abstract saveNoun(noun: HNSWNoun): Promise<void>
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract saveNounMetadata(id: string, metadata: NounMetadata): Promise<void>
|
|
|
|
|
abstract deleteNounMetadata(id: string): Promise<void>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract getNoun(id: string): Promise<HNSWNounWithMetadata | null>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract getNounsByNounType(nounType: string): Promise<HNSWNounWithMetadata[]>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
abstract deleteNoun(id: string): Promise<void>
|
|
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract saveVerb(verb: HNSWVerb): Promise<void>
|
|
|
|
|
abstract saveVerbMetadata(id: string, metadata: VerbMetadata): Promise<void>
|
|
|
|
|
abstract deleteVerbMetadata(id: string): Promise<void>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract getVerb(id: string): Promise<HNSWVerbWithMetadata | null>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract getVerbsBySource(sourceId: string): Promise<HNSWVerbWithMetadata[]>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract getVerbsByTarget(targetId: string): Promise<HNSWVerbWithMetadata[]>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
2025-10-17 12:29:27 -07:00
|
|
|
abstract getVerbsByType(type: string): Promise<HNSWVerbWithMetadata[]>
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
abstract deleteVerb(id: string): Promise<void>
|
|
|
|
|
|
|
|
|
|
abstract saveMetadata(id: string, metadata: any): Promise<void>
|
|
|
|
|
|
|
|
|
|
abstract getMetadata(id: string): Promise<any | null>
|
|
|
|
|
|
2025-10-06 15:43:45 -07:00
|
|
|
abstract getNounMetadata(id: string): Promise<any | null>
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
abstract getVerbMetadata(id: string): Promise<any | null>
|
|
|
|
|
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
// Vector Index Persistence (Brainy 8.0 — algorithm-neutral surface).
|
|
|
|
|
// Concrete adapters persist the per-entity HNSW graph node (level + connections)
|
|
|
|
|
// under these methods. Cortex's DiskANN-style native vector index doesn't use
|
|
|
|
|
// them (it persists its own single mmap'd `.dkann` file under
|
|
|
|
|
// `_system/vector-index/<shard>.dkann` per BRAINY-8.0-RENAME-COORDINATION § B.2).
|
2025-10-10 11:15:17 -07:00
|
|
|
|
|
|
|
|
abstract getNounVector(id: string): Promise<number[] | null>
|
|
|
|
|
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
abstract saveVectorIndexData(nounId: string, data: {
|
2025-10-10 11:15:17 -07:00
|
|
|
level: number
|
|
|
|
|
connections: Record<string, string[]>
|
|
|
|
|
}): Promise<void>
|
|
|
|
|
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
abstract getVectorIndexData(nounId: string): Promise<{
|
2025-10-10 11:15:17 -07:00
|
|
|
level: number
|
|
|
|
|
connections: Record<string, string[]>
|
|
|
|
|
} | null>
|
|
|
|
|
|
|
|
|
|
abstract saveHNSWSystem(systemData: {
|
|
|
|
|
entryPointId: string | null
|
|
|
|
|
maxLevel: number
|
|
|
|
|
}): Promise<void>
|
|
|
|
|
|
|
|
|
|
abstract getHNSWSystem(): Promise<{
|
|
|
|
|
entryPointId: string | null
|
|
|
|
|
maxLevel: number
|
|
|
|
|
} | null>
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
abstract clear(): Promise<void>
|
|
|
|
|
|
|
|
|
|
abstract getStorageStatus(): Promise<{
|
|
|
|
|
type: string
|
|
|
|
|
used: number
|
|
|
|
|
quota: number | null
|
|
|
|
|
details?: Record<string, any>
|
|
|
|
|
}>
|
|
|
|
|
|
feat(storage): add raw binary-blob primitive to every storage adapter
Introduce a first-class binary-blob storage primitive on the StorageAdapter
contract and implement it across all storage backends. This stores opaque byte
payloads verbatim instead of base64-in-JSON, eliminating the ~33% inflation and
full-materialization cost of the JSON envelope. It unblocks zero-copy,
mmap-able column-store segments and batch vector I/O at billion scale.
New methods (declared abstract on BaseStorageAdapter, the class that implements
StorageAdapter, and added to the StorageAdapter interface):
saveBinaryBlob(key, data) raw write, atomic on real filesystems
loadBinaryBlob(key) exact bytes, or null if absent
deleteBinaryBlob(key) idempotent (missing is ignored)
getBinaryBlobPath(key) real local fs path where one exists, else null
Shared key -> location convention across every adapter: the key's
"/"-separated segments nest under a `_blobs/` prefix and are suffixed with
`.bin`, e.g. "graph-lsm/source/sstable-123" ->
"<root>/_blobs/graph-lsm/source/sstable-123.bin". Blobs are not branch-scoped
(COW): they are immutable producer-managed segments.
Per-adapter behavior:
- FileSystemStorage: writes under <rootDir>/_blobs via tmp+rename; returns the
real on-disk path so native code can mmap it directly. Path convention matches
the existing MmapFileSystemStorage subclass byte-for-byte.
- S3CompatibleStorage / R2Storage / GcsStorage / AzureBlobStorage: put/get/delete
raw octet-stream objects; getBinaryBlobPath returns null (remote stores have no
local path).
- MemoryStorage: defensive-copied Map<string, Buffer>; null path; cleared on
clear().
- OPFSStorage: stores raw bytes in the OPFS tree; null path.
- HistoricalStorageAdapter: read-only — save/delete throw; load resolves the
blob from the historical commit tree; null path.
Tests: tests/unit/storage/binaryBlob.test.ts exercises save/load round-trip
(byte-identical, incl. non-UTF8 bytes), overwrite, delete-then-load, load-missing,
and getBinaryBlobPath behavior for all eight adapters. Cloud adapters run against
in-memory client fakes that drive the real adapter code; OPFS runs against an
in-memory FileSystem Access API mock; the historical adapter commits a blob into
a real COW tree. 59 new tests; full unit suite (1398 tests) green.
2026-05-27 11:49:49 -07:00
|
|
|
// ===========================================================================
|
|
|
|
|
// Raw binary-blob primitive
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
//
|
|
|
|
|
// A first-class storage primitive for opaque byte payloads that must NOT be
|
|
|
|
|
// wrapped in a JSON envelope. The JSON object path base64-encodes binary data,
|
|
|
|
|
// inflating it ~33% and forcing full in-memory materialization on every read.
|
|
|
|
|
// Blobs side-step that: bytes are written and read verbatim.
|
|
|
|
|
//
|
|
|
|
|
// This unblocks zero-copy, mmap-able column-store segments and batch vector
|
|
|
|
|
// I/O at billion scale. On filesystem-backed adapters `getBinaryBlobPath`
|
|
|
|
|
// returns a real local path so native code (Rust) can mmap the file directly.
|
|
|
|
|
//
|
|
|
|
|
// Blob key → location convention (shared by every adapter so cross-language
|
|
|
|
|
// and cross-adapter consumers agree on exactly where a blob lives):
|
|
|
|
|
//
|
|
|
|
|
// key location
|
|
|
|
|
// "graph-lsm/source/sstable-123" "<root>/_blobs/graph-lsm/source/sstable-123.bin"
|
|
|
|
|
//
|
|
|
|
|
// i.e. the key's "/"-separated segments are nested under a `_blobs/` prefix and
|
feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API
The COW version-control surface (fork, branches, checkout, commit,
getHistory/streamHistory, asOfCommit, brain.versions) is gone, along with
its subsystems: src/versioning/, the COW object store (CommitLog,
CommitObject, RefManager, TreeObject), HistoricalStorageAdapter, and the
TypeAwareHNSWIndex path it kept alive. The Db API (now/transact/asOf/
with/persist/restore) is the one versioning model in 8.0.
Survivors and replacements:
- BlobStorage survives (the VFS stores file content through it), relocated
to src/storage/blobStorage.ts with binaryDataCodec.ts; its adapter
interface is now BlobStoreAdapter, slimmed to the consumed surface
(write/read/has/delete/getMetadata + MIME-aware compression policy).
- brain.migrate() backup branches are replaced by persist-before-migrate:
MigrateOptions.backupTo persists a hard-link snapshot of the current
generation before any transform runs; MigrationResult.backupPath reports
it, and brain.restore(path) brings it back wholesale.
- CLI: cow.ts (fork/branch/checkout/history/migrate) is replaced by
snapshot.ts — snapshot <path>, restore <path>, history (tx-log),
generation.
- New public read API: brain.transactionLog({limit}) exposes the reified
tx-log (generation/timestamp/meta, newest first) that backs the CLI
history command; TxLogEntry is exported.
Tests: superseded suites deleted; fork/commit blocks excised from shared
suites; BlobStorage tests relocated + reworked against the slimmed store;
migration tests now prove the backupTo snapshot/restore round trip; new
transactionLog coverage in db-mvcc.
2026-06-10 15:22:47 -07:00
|
|
|
// suffixed with `.bin`. Blobs are immutable, content-addressed segments
|
|
|
|
|
// managed by their producer.
|
feat(storage): add raw binary-blob primitive to every storage adapter
Introduce a first-class binary-blob storage primitive on the StorageAdapter
contract and implement it across all storage backends. This stores opaque byte
payloads verbatim instead of base64-in-JSON, eliminating the ~33% inflation and
full-materialization cost of the JSON envelope. It unblocks zero-copy,
mmap-able column-store segments and batch vector I/O at billion scale.
New methods (declared abstract on BaseStorageAdapter, the class that implements
StorageAdapter, and added to the StorageAdapter interface):
saveBinaryBlob(key, data) raw write, atomic on real filesystems
loadBinaryBlob(key) exact bytes, or null if absent
deleteBinaryBlob(key) idempotent (missing is ignored)
getBinaryBlobPath(key) real local fs path where one exists, else null
Shared key -> location convention across every adapter: the key's
"/"-separated segments nest under a `_blobs/` prefix and are suffixed with
`.bin`, e.g. "graph-lsm/source/sstable-123" ->
"<root>/_blobs/graph-lsm/source/sstable-123.bin". Blobs are not branch-scoped
(COW): they are immutable producer-managed segments.
Per-adapter behavior:
- FileSystemStorage: writes under <rootDir>/_blobs via tmp+rename; returns the
real on-disk path so native code can mmap it directly. Path convention matches
the existing MmapFileSystemStorage subclass byte-for-byte.
- S3CompatibleStorage / R2Storage / GcsStorage / AzureBlobStorage: put/get/delete
raw octet-stream objects; getBinaryBlobPath returns null (remote stores have no
local path).
- MemoryStorage: defensive-copied Map<string, Buffer>; null path; cleared on
clear().
- OPFSStorage: stores raw bytes in the OPFS tree; null path.
- HistoricalStorageAdapter: read-only — save/delete throw; load resolves the
blob from the historical commit tree; null path.
Tests: tests/unit/storage/binaryBlob.test.ts exercises save/load round-trip
(byte-identical, incl. non-UTF8 bytes), overwrite, delete-then-load, load-missing,
and getBinaryBlobPath behavior for all eight adapters. Cloud adapters run against
in-memory client fakes that drive the real adapter code; OPFS runs against an
in-memory FileSystem Access API mock; the historical adapter commits a blob into
a real COW tree. 59 new tests; full unit suite (1398 tests) green.
2026-05-27 11:49:49 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Persist a raw binary blob under `key`, writing the bytes verbatim (no JSON
|
|
|
|
|
* envelope, no base64). Overwrites any existing blob at the same key. Where the
|
|
|
|
|
* backend is a real filesystem the write is atomic (temp file + rename) so a
|
|
|
|
|
* concurrent reader never observes a torn write.
|
|
|
|
|
*
|
|
|
|
|
* @param key - Logical blob key. "/"-separated segments map to nested
|
|
|
|
|
* directories under the adapter's `_blobs/` prefix (e.g.
|
|
|
|
|
* `"graph-lsm/source/sstable-123"`).
|
|
|
|
|
* @param data - The exact bytes to store.
|
|
|
|
|
* @returns Resolves once the blob is durably written.
|
|
|
|
|
* @example
|
|
|
|
|
* await storage.saveBinaryBlob('graph-lsm/source/sstable-7', segmentBytes)
|
|
|
|
|
*/
|
|
|
|
|
abstract saveBinaryBlob(key: string, data: Buffer): Promise<void>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load the raw bytes previously stored under `key`, or `null` if no blob
|
|
|
|
|
* exists at that key. The returned buffer is byte-identical to what was passed
|
|
|
|
|
* to {@link saveBinaryBlob}.
|
|
|
|
|
*
|
|
|
|
|
* @param key - The blob key used when saving.
|
|
|
|
|
* @returns The blob bytes, or `null` if absent.
|
|
|
|
|
* @example
|
|
|
|
|
* const bytes = await storage.loadBinaryBlob('graph-lsm/source/sstable-7')
|
|
|
|
|
* if (bytes) decodeSegment(bytes)
|
|
|
|
|
*/
|
|
|
|
|
abstract loadBinaryBlob(key: string): Promise<Buffer | null>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete the blob stored under `key`. Missing blobs are ignored (no error) so
|
|
|
|
|
* delete is idempotent.
|
|
|
|
|
*
|
|
|
|
|
* @param key - The blob key to delete.
|
|
|
|
|
* @returns Resolves once the blob is gone (or was already absent).
|
|
|
|
|
*/
|
|
|
|
|
abstract deleteBinaryBlob(key: string): Promise<void>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve `key` to a real local filesystem path that native code can `mmap`
|
|
|
|
|
* directly, or `null` when this backend has no local file for the blob.
|
|
|
|
|
*
|
|
|
|
|
* Filesystem-backed adapters return the on-disk path (whether or not the file
|
|
|
|
|
* currently exists — the caller is expected to write before mapping). Remote
|
|
|
|
|
* object stores (S3, R2, GCS, Azure), in-memory storage, browser storage
|
|
|
|
|
* (OPFS), and the read-only historical adapter genuinely have no local path
|
|
|
|
|
* and therefore return `null`. A `null` here is correct behavior, not a
|
|
|
|
|
* fallback: callers must use {@link loadBinaryBlob} when no path is available.
|
|
|
|
|
*
|
|
|
|
|
* @param key - The blob key.
|
|
|
|
|
* @returns An absolute local filesystem path, or `null` if none exists.
|
|
|
|
|
*/
|
|
|
|
|
abstract getBinaryBlobPath(key: string): string | null
|
|
|
|
|
|
2025-10-30 08:54:04 -07:00
|
|
|
/**
|
|
|
|
|
* Get optimal batch configuration for this storage adapter
|
|
|
|
|
* Override in subclasses to provide storage-specific optimization
|
|
|
|
|
*
|
|
|
|
|
* This method allows each storage adapter to declare its optimal batch behavior
|
|
|
|
|
* for rate limiting and performance. The configuration is used by addMany(),
|
|
|
|
|
* relateMany(), and import operations to automatically adapt to storage capabilities.
|
|
|
|
|
*
|
|
|
|
|
* @returns Batch configuration optimized for this storage type
|
|
|
|
|
*/
|
|
|
|
|
public getBatchConfig(): StorageBatchConfig {
|
|
|
|
|
// Conservative defaults that work safely across all storage types
|
|
|
|
|
// Cloud storage adapters should override with higher throughput values
|
|
|
|
|
// Local storage adapters should override with no delays
|
|
|
|
|
return {
|
|
|
|
|
maxBatchSize: 50,
|
|
|
|
|
batchDelayMs: 100,
|
|
|
|
|
maxConcurrent: 50,
|
|
|
|
|
supportsParallelWrites: false,
|
|
|
|
|
rateLimit: {
|
|
|
|
|
operationsPerSecond: 100,
|
|
|
|
|
burstCapacity: 200
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
// NOTE: getAllNouns and getAllVerbs have been removed to prevent expensive full scans.
|
|
|
|
|
// Use getNouns() and getVerbs() with pagination instead.
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get nouns with pagination and filtering
|
|
|
|
|
* @param options Pagination and filtering options
|
|
|
|
|
* @returns Promise that resolves to a paginated result of nouns
|
|
|
|
|
*/
|
|
|
|
|
abstract getNouns(options?: {
|
|
|
|
|
pagination?: {
|
|
|
|
|
offset?: number
|
|
|
|
|
limit?: number
|
|
|
|
|
cursor?: string
|
|
|
|
|
}
|
|
|
|
|
filter?: {
|
|
|
|
|
nounType?: string | string[]
|
|
|
|
|
service?: string | string[]
|
|
|
|
|
metadata?: Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
}): Promise<{
|
2025-10-17 12:29:27 -07:00
|
|
|
items: HNSWNounWithMetadata[]
|
2025-08-26 12:32:21 -07:00
|
|
|
totalCount?: number
|
|
|
|
|
hasMore: boolean
|
|
|
|
|
nextCursor?: string
|
|
|
|
|
}>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get verbs with pagination and filtering
|
|
|
|
|
* @param options Pagination and filtering options
|
|
|
|
|
* @returns Promise that resolves to a paginated result of verbs
|
|
|
|
|
*/
|
|
|
|
|
abstract getVerbs(options?: {
|
|
|
|
|
pagination?: {
|
|
|
|
|
offset?: number
|
|
|
|
|
limit?: number
|
|
|
|
|
cursor?: string
|
|
|
|
|
}
|
|
|
|
|
filter?: {
|
|
|
|
|
verbType?: string | string[]
|
|
|
|
|
sourceId?: string | string[]
|
|
|
|
|
targetId?: string | string[]
|
|
|
|
|
service?: string | string[]
|
|
|
|
|
metadata?: Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
}): Promise<{
|
2025-10-17 12:29:27 -07:00
|
|
|
items: HNSWVerbWithMetadata[]
|
2025-08-26 12:32:21 -07:00
|
|
|
totalCount?: number
|
|
|
|
|
hasMore: boolean
|
|
|
|
|
nextCursor?: string
|
|
|
|
|
}>
|
|
|
|
|
|
2025-09-02 14:55:15 -07:00
|
|
|
/**
|
|
|
|
|
* Get nouns with pagination (internal implementation)
|
|
|
|
|
* This method should be implemented by storage adapters to support efficient pagination
|
|
|
|
|
* @param options Pagination options
|
|
|
|
|
* @returns Promise that resolves to a paginated result of nouns
|
|
|
|
|
*/
|
|
|
|
|
getNounsWithPagination?(options: {
|
|
|
|
|
limit?: number
|
|
|
|
|
cursor?: string
|
|
|
|
|
filter?: {
|
|
|
|
|
nounType?: string | string[]
|
|
|
|
|
service?: string | string[]
|
|
|
|
|
metadata?: Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
}): Promise<{
|
2025-10-17 12:29:27 -07:00
|
|
|
items: HNSWNounWithMetadata[]
|
2025-09-02 14:55:15 -07:00
|
|
|
totalCount?: number
|
|
|
|
|
hasMore: boolean
|
|
|
|
|
nextCursor?: string
|
|
|
|
|
}>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get verbs with pagination (internal implementation)
|
|
|
|
|
* This method should be implemented by storage adapters to support efficient pagination
|
|
|
|
|
* @param options Pagination options
|
|
|
|
|
* @returns Promise that resolves to a paginated result of verbs
|
|
|
|
|
*/
|
|
|
|
|
getVerbsWithPagination?(options: {
|
|
|
|
|
limit?: number
|
|
|
|
|
cursor?: string
|
|
|
|
|
filter?: {
|
|
|
|
|
verbType?: string | string[]
|
|
|
|
|
sourceId?: string | string[]
|
|
|
|
|
targetId?: string | string[]
|
|
|
|
|
service?: string | string[]
|
|
|
|
|
metadata?: Record<string, any>
|
|
|
|
|
}
|
|
|
|
|
}): Promise<{
|
2025-10-17 12:29:27 -07:00
|
|
|
items: HNSWVerbWithMetadata[]
|
2025-09-02 14:55:15 -07:00
|
|
|
totalCount?: number
|
|
|
|
|
hasMore: boolean
|
|
|
|
|
nextCursor?: string
|
|
|
|
|
}>
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
// Statistics cache
|
|
|
|
|
protected statisticsCache: StatisticsData | null = null
|
|
|
|
|
|
|
|
|
|
// Batch update timer ID
|
|
|
|
|
protected statisticsBatchUpdateTimerId: NodeJS.Timeout | null = null
|
|
|
|
|
|
|
|
|
|
// Flag to indicate if statistics have been modified since last save
|
|
|
|
|
protected statisticsModified = false
|
|
|
|
|
|
|
|
|
|
// Time of last statistics flush to storage
|
|
|
|
|
protected lastStatisticsFlushTime = 0
|
|
|
|
|
|
|
|
|
|
// Minimum time between statistics flushes (5 seconds)
|
|
|
|
|
protected readonly MIN_FLUSH_INTERVAL_MS = 5000
|
|
|
|
|
|
|
|
|
|
// Maximum time to wait before flushing statistics (30 seconds)
|
|
|
|
|
protected readonly MAX_FLUSH_DELAY_MS = 30000
|
|
|
|
|
|
|
|
|
|
// Throttling tracking properties
|
|
|
|
|
protected throttlingDetected = false
|
|
|
|
|
protected throttlingBackoffMs = 1000 // Start with 1 second
|
|
|
|
|
protected maxBackoffMs = 30000 // Max 30 seconds
|
|
|
|
|
protected consecutiveThrottleEvents = 0
|
|
|
|
|
protected lastThrottleTime = 0
|
|
|
|
|
protected totalThrottleEvents = 0
|
|
|
|
|
protected throttleEventsByHour: number[] = new Array(24).fill(0)
|
|
|
|
|
protected throttleReasons: Record<string, number> = {}
|
|
|
|
|
protected lastThrottleHourIndex = -1
|
|
|
|
|
|
|
|
|
|
// Operation impact tracking
|
|
|
|
|
protected delayedOperations = 0
|
|
|
|
|
protected retriedOperations = 0
|
|
|
|
|
protected failedDueToThrottling = 0
|
|
|
|
|
protected totalDelayMs = 0
|
|
|
|
|
|
|
|
|
|
// Service-level throttling
|
|
|
|
|
protected serviceThrottling: Map<string, {
|
|
|
|
|
throttleCount: number
|
|
|
|
|
lastThrottle: number
|
|
|
|
|
status: 'normal' | 'throttled' | 'recovering'
|
|
|
|
|
}> = new Map()
|
|
|
|
|
|
|
|
|
|
// Statistics-specific methods that must be implemented by subclasses
|
|
|
|
|
protected abstract saveStatisticsData(
|
|
|
|
|
statistics: StatisticsData
|
|
|
|
|
): Promise<void>
|
|
|
|
|
|
|
|
|
|
protected abstract getStatisticsData(): Promise<StatisticsData | null>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save statistics data
|
|
|
|
|
* @param statistics The statistics data to save
|
|
|
|
|
*/
|
|
|
|
|
async saveStatistics(statistics: StatisticsData): Promise<void> {
|
|
|
|
|
// Update the cache with a deep copy to avoid reference issues
|
|
|
|
|
this.statisticsCache = {
|
|
|
|
|
nounCount: { ...statistics.nounCount },
|
|
|
|
|
verbCount: { ...statistics.verbCount },
|
|
|
|
|
metadataCount: { ...statistics.metadataCount },
|
|
|
|
|
hnswIndexSize: statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: statistics.lastUpdated,
|
|
|
|
|
// Include serviceActivity if present
|
|
|
|
|
...(statistics.serviceActivity && {
|
|
|
|
|
serviceActivity: Object.fromEntries(
|
|
|
|
|
Object.entries(statistics.serviceActivity).map(([k, v]) => [k, {...v}])
|
|
|
|
|
)
|
|
|
|
|
}),
|
|
|
|
|
// Include services if present
|
|
|
|
|
...(statistics.services && {
|
|
|
|
|
services: statistics.services.map(s => ({...s}))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Schedule a batch update instead of saving immediately
|
|
|
|
|
this.scheduleBatchUpdate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get statistics data
|
|
|
|
|
* @returns Promise that resolves to the statistics data
|
|
|
|
|
*/
|
|
|
|
|
async getStatistics(): Promise<StatisticsData | null> {
|
|
|
|
|
// If we have cached statistics, return a deep copy
|
|
|
|
|
if (this.statisticsCache) {
|
|
|
|
|
return {
|
|
|
|
|
nounCount: { ...this.statisticsCache.nounCount },
|
|
|
|
|
verbCount: { ...this.statisticsCache.verbCount },
|
|
|
|
|
metadataCount: { ...this.statisticsCache.metadataCount },
|
|
|
|
|
hnswIndexSize: this.statisticsCache.hnswIndexSize,
|
|
|
|
|
lastUpdated: this.statisticsCache.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, get from storage
|
|
|
|
|
const statistics = await this.getStatisticsData()
|
|
|
|
|
|
|
|
|
|
// If we found statistics, update the cache
|
|
|
|
|
if (statistics) {
|
|
|
|
|
// Update the cache with a deep copy
|
|
|
|
|
this.statisticsCache = {
|
|
|
|
|
nounCount: { ...statistics.nounCount },
|
|
|
|
|
verbCount: { ...statistics.verbCount },
|
|
|
|
|
metadataCount: { ...statistics.metadataCount },
|
|
|
|
|
hnswIndexSize: statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: statistics.lastUpdated
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statistics
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Schedule a batch update of statistics
|
|
|
|
|
*/
|
|
|
|
|
protected scheduleBatchUpdate(): void {
|
|
|
|
|
// Mark statistics as modified
|
|
|
|
|
this.statisticsModified = true
|
|
|
|
|
|
|
|
|
|
// If a timer is already set, don't set another one
|
|
|
|
|
if (this.statisticsBatchUpdateTimerId !== null) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate time since last flush
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
const timeSinceLastFlush = now - this.lastStatisticsFlushTime
|
|
|
|
|
|
|
|
|
|
// If we've recently flushed, wait longer before the next flush
|
|
|
|
|
const delayMs =
|
|
|
|
|
timeSinceLastFlush < this.MIN_FLUSH_INTERVAL_MS
|
|
|
|
|
? this.MAX_FLUSH_DELAY_MS
|
|
|
|
|
: this.MIN_FLUSH_INTERVAL_MS
|
|
|
|
|
|
|
|
|
|
// Schedule the batch update
|
|
|
|
|
this.statisticsBatchUpdateTimerId = setTimeout(() => {
|
|
|
|
|
this.flushStatistics()
|
|
|
|
|
}, delayMs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Flush statistics to storage
|
|
|
|
|
*/
|
|
|
|
|
protected async flushStatistics(): Promise<void> {
|
|
|
|
|
// Clear the timer
|
|
|
|
|
if (this.statisticsBatchUpdateTimerId !== null) {
|
|
|
|
|
clearTimeout(this.statisticsBatchUpdateTimerId)
|
|
|
|
|
this.statisticsBatchUpdateTimerId = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If statistics haven't been modified, no need to flush
|
|
|
|
|
if (!this.statisticsModified || !this.statisticsCache) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Save the statistics to storage
|
|
|
|
|
await this.saveStatisticsData(this.statisticsCache)
|
|
|
|
|
|
|
|
|
|
// Update the last flush time
|
|
|
|
|
this.lastStatisticsFlushTime = Date.now()
|
|
|
|
|
// Reset the modified flag
|
|
|
|
|
this.statisticsModified = false
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to flush statistics data:', error)
|
|
|
|
|
// Mark as still modified so we'll try again later
|
|
|
|
|
this.statisticsModified = true
|
|
|
|
|
// Don't throw the error to avoid disrupting the application
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Increment a statistic counter
|
|
|
|
|
* @param type The type of statistic to increment ('noun', 'verb', 'metadata')
|
|
|
|
|
* @param service The service that inserted the data
|
|
|
|
|
* @param amount The amount to increment by (default: 1)
|
|
|
|
|
*/
|
|
|
|
|
async incrementStatistic(
|
|
|
|
|
type: 'noun' | 'verb' | 'metadata',
|
|
|
|
|
service: string,
|
|
|
|
|
amount: number = 1
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
// Get current statistics from cache or storage
|
|
|
|
|
let statistics = this.statisticsCache
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = await this.getStatisticsData()
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = this.createDefaultStatistics()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the cache
|
|
|
|
|
this.statisticsCache = {
|
|
|
|
|
nounCount: { ...statistics.nounCount },
|
|
|
|
|
verbCount: { ...statistics.verbCount },
|
|
|
|
|
metadataCount: { ...statistics.metadataCount },
|
|
|
|
|
hnswIndexSize: statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: statistics.lastUpdated,
|
|
|
|
|
// Include serviceActivity if present
|
|
|
|
|
...(statistics.serviceActivity && {
|
|
|
|
|
serviceActivity: Object.fromEntries(
|
|
|
|
|
Object.entries(statistics.serviceActivity).map(([k, v]) => [k, {...v}])
|
|
|
|
|
)
|
|
|
|
|
}),
|
|
|
|
|
// Include services if present
|
|
|
|
|
...(statistics.services && {
|
|
|
|
|
services: statistics.services.map(s => ({...s}))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Increment the appropriate counter
|
|
|
|
|
const counterMap = {
|
|
|
|
|
noun: this.statisticsCache!.nounCount,
|
|
|
|
|
verb: this.statisticsCache!.verbCount,
|
|
|
|
|
metadata: this.statisticsCache!.metadataCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const counter = counterMap[type]
|
|
|
|
|
counter[service] = (counter[service] || 0) + amount
|
|
|
|
|
|
|
|
|
|
// Track service activity
|
|
|
|
|
this.trackServiceActivity(service, 'add')
|
|
|
|
|
|
|
|
|
|
// Update timestamp
|
|
|
|
|
this.statisticsCache!.lastUpdated = new Date().toISOString()
|
|
|
|
|
|
|
|
|
|
// Schedule a batch update instead of saving immediately
|
|
|
|
|
this.scheduleBatchUpdate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Track service activity (first/last activity, operation counts)
|
|
|
|
|
* @param service The service name
|
|
|
|
|
* @param operation The operation type
|
|
|
|
|
*/
|
|
|
|
|
protected trackServiceActivity(
|
|
|
|
|
service: string,
|
|
|
|
|
operation: 'add' | 'update' | 'delete'
|
|
|
|
|
): void {
|
|
|
|
|
if (!this.statisticsCache) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize serviceActivity if it doesn't exist
|
|
|
|
|
if (!this.statisticsCache.serviceActivity) {
|
|
|
|
|
this.statisticsCache.serviceActivity = {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = new Date().toISOString()
|
|
|
|
|
const activity = this.statisticsCache.serviceActivity[service]
|
|
|
|
|
|
|
|
|
|
if (!activity) {
|
|
|
|
|
// First activity for this service
|
|
|
|
|
this.statisticsCache.serviceActivity[service] = {
|
|
|
|
|
firstActivity: now,
|
|
|
|
|
lastActivity: now,
|
|
|
|
|
totalOperations: 1
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Update existing activity
|
|
|
|
|
activity.lastActivity = now
|
|
|
|
|
activity.totalOperations++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrement a statistic counter
|
|
|
|
|
* @param type The type of statistic to decrement ('noun', 'verb', 'metadata')
|
|
|
|
|
* @param service The service that inserted the data
|
|
|
|
|
* @param amount The amount to decrement by (default: 1)
|
|
|
|
|
*/
|
|
|
|
|
async decrementStatistic(
|
|
|
|
|
type: 'noun' | 'verb' | 'metadata',
|
|
|
|
|
service: string,
|
|
|
|
|
amount: number = 1
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
// Get current statistics from cache or storage
|
|
|
|
|
let statistics = this.statisticsCache
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = await this.getStatisticsData()
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = this.createDefaultStatistics()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the cache
|
|
|
|
|
this.statisticsCache = {
|
|
|
|
|
nounCount: { ...statistics.nounCount },
|
|
|
|
|
verbCount: { ...statistics.verbCount },
|
|
|
|
|
metadataCount: { ...statistics.metadataCount },
|
|
|
|
|
hnswIndexSize: statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: statistics.lastUpdated,
|
|
|
|
|
// Include serviceActivity if present
|
|
|
|
|
...(statistics.serviceActivity && {
|
|
|
|
|
serviceActivity: Object.fromEntries(
|
|
|
|
|
Object.entries(statistics.serviceActivity).map(([k, v]) => [k, {...v}])
|
|
|
|
|
)
|
|
|
|
|
}),
|
|
|
|
|
// Include services if present
|
|
|
|
|
...(statistics.services && {
|
|
|
|
|
services: statistics.services.map(s => ({...s}))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decrement the appropriate counter
|
|
|
|
|
const counterMap = {
|
|
|
|
|
noun: this.statisticsCache!.nounCount,
|
|
|
|
|
verb: this.statisticsCache!.verbCount,
|
|
|
|
|
metadata: this.statisticsCache!.metadataCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const counter = counterMap[type]
|
|
|
|
|
counter[service] = Math.max(0, (counter[service] || 0) - amount)
|
|
|
|
|
|
|
|
|
|
// Track service activity
|
|
|
|
|
this.trackServiceActivity(service, 'delete')
|
|
|
|
|
|
|
|
|
|
// Update timestamp
|
|
|
|
|
this.statisticsCache!.lastUpdated = new Date().toISOString()
|
|
|
|
|
|
|
|
|
|
// Schedule a batch update instead of saving immediately
|
|
|
|
|
this.scheduleBatchUpdate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the HNSW index size statistic
|
|
|
|
|
* @param size The new size of the HNSW index
|
|
|
|
|
*/
|
|
|
|
|
async updateHnswIndexSize(size: number): Promise<void> {
|
|
|
|
|
// Get current statistics from cache or storage
|
|
|
|
|
let statistics = this.statisticsCache
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = await this.getStatisticsData()
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = this.createDefaultStatistics()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the cache
|
|
|
|
|
this.statisticsCache = {
|
|
|
|
|
nounCount: { ...statistics.nounCount },
|
|
|
|
|
verbCount: { ...statistics.verbCount },
|
|
|
|
|
metadataCount: { ...statistics.metadataCount },
|
|
|
|
|
hnswIndexSize: statistics.hnswIndexSize,
|
|
|
|
|
lastUpdated: statistics.lastUpdated,
|
|
|
|
|
// Include serviceActivity if present
|
|
|
|
|
...(statistics.serviceActivity && {
|
|
|
|
|
serviceActivity: Object.fromEntries(
|
|
|
|
|
Object.entries(statistics.serviceActivity).map(([k, v]) => [k, {...v}])
|
|
|
|
|
)
|
|
|
|
|
}),
|
|
|
|
|
// Include services if present
|
|
|
|
|
...(statistics.services && {
|
|
|
|
|
services: statistics.services.map(s => ({...s}))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update HNSW index size
|
|
|
|
|
this.statisticsCache!.hnswIndexSize = size
|
|
|
|
|
|
|
|
|
|
// Update timestamp
|
|
|
|
|
this.statisticsCache!.lastUpdated = new Date().toISOString()
|
|
|
|
|
|
|
|
|
|
// Schedule a batch update instead of saving immediately
|
|
|
|
|
this.scheduleBatchUpdate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Force an immediate flush of statistics to storage
|
|
|
|
|
* This ensures that any pending statistics updates are written to persistent storage
|
|
|
|
|
*/
|
|
|
|
|
async flushStatisticsToStorage(): Promise<void> {
|
|
|
|
|
// If there are no statistics in cache or they haven't been modified, nothing to flush
|
|
|
|
|
if (!this.statisticsCache || !this.statisticsModified) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call the protected flushStatistics method to immediately write to storage
|
|
|
|
|
await this.flushStatistics()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Track field names from a JSON document
|
|
|
|
|
* @param jsonDocument The JSON document to extract field names from
|
|
|
|
|
* @param service The service that inserted the data
|
|
|
|
|
*/
|
|
|
|
|
async trackFieldNames(jsonDocument: any, service: string): Promise<void> {
|
|
|
|
|
// Skip if not a JSON object
|
|
|
|
|
if (typeof jsonDocument !== 'object' || jsonDocument === null || Array.isArray(jsonDocument)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current statistics from cache or storage
|
|
|
|
|
let statistics = this.statisticsCache
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = await this.getStatisticsData()
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = this.createDefaultStatistics()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the cache
|
|
|
|
|
this.statisticsCache = {
|
|
|
|
|
...statistics,
|
|
|
|
|
nounCount: { ...statistics.nounCount },
|
|
|
|
|
verbCount: { ...statistics.verbCount },
|
|
|
|
|
metadataCount: { ...statistics.metadataCount },
|
|
|
|
|
fieldNames: { ...statistics.fieldNames },
|
|
|
|
|
standardFieldMappings: { ...statistics.standardFieldMappings }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure fieldNames exists
|
|
|
|
|
if (!this.statisticsCache!.fieldNames) {
|
|
|
|
|
this.statisticsCache!.fieldNames = {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure standardFieldMappings exists
|
|
|
|
|
if (!this.statisticsCache!.standardFieldMappings) {
|
|
|
|
|
this.statisticsCache!.standardFieldMappings = {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract field names from the JSON document
|
|
|
|
|
const fieldNames = extractFieldNamesFromJson(jsonDocument)
|
|
|
|
|
|
|
|
|
|
// Initialize service entry if it doesn't exist
|
|
|
|
|
if (!this.statisticsCache!.fieldNames[service]) {
|
|
|
|
|
this.statisticsCache!.fieldNames[service] = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add new field names to the service's list
|
|
|
|
|
for (const fieldName of fieldNames) {
|
|
|
|
|
if (!this.statisticsCache!.fieldNames[service].includes(fieldName)) {
|
|
|
|
|
this.statisticsCache!.fieldNames[service].push(fieldName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map to standard field if possible
|
|
|
|
|
const standardField = mapToStandardField(fieldName)
|
|
|
|
|
if (standardField) {
|
|
|
|
|
// Initialize standard field entry if it doesn't exist
|
|
|
|
|
if (!this.statisticsCache!.standardFieldMappings[standardField]) {
|
|
|
|
|
this.statisticsCache!.standardFieldMappings[standardField] = {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize service entry if it doesn't exist
|
|
|
|
|
if (!this.statisticsCache!.standardFieldMappings[standardField][service]) {
|
|
|
|
|
this.statisticsCache!.standardFieldMappings[standardField][service] = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add field name to standard field mapping if not already there
|
|
|
|
|
if (!this.statisticsCache!.standardFieldMappings[standardField][service].includes(fieldName)) {
|
|
|
|
|
this.statisticsCache!.standardFieldMappings[standardField][service].push(fieldName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update timestamp
|
|
|
|
|
this.statisticsCache!.lastUpdated = new Date().toISOString()
|
|
|
|
|
|
|
|
|
|
// Schedule a batch update
|
|
|
|
|
this.statisticsModified = true
|
|
|
|
|
this.scheduleBatchUpdate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get available field names by service
|
|
|
|
|
* @returns Record of field names by service
|
|
|
|
|
*/
|
|
|
|
|
async getAvailableFieldNames(): Promise<Record<string, string[]>> {
|
|
|
|
|
// Get current statistics from cache or storage
|
|
|
|
|
let statistics = this.statisticsCache
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = await this.getStatisticsData()
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
return {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return field names by service
|
|
|
|
|
return statistics.fieldNames || {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get standard field mappings
|
|
|
|
|
* @returns Record of standard field mappings
|
|
|
|
|
*/
|
|
|
|
|
async getStandardFieldMappings(): Promise<Record<string, Record<string, string[]>>> {
|
|
|
|
|
// Get current statistics from cache or storage
|
|
|
|
|
let statistics = this.statisticsCache
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
statistics = await this.getStatisticsData()
|
|
|
|
|
if (!statistics) {
|
|
|
|
|
return {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return standard field mappings
|
|
|
|
|
return statistics.standardFieldMappings || {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create default statistics data
|
|
|
|
|
* @returns Default statistics data
|
|
|
|
|
*/
|
|
|
|
|
protected createDefaultStatistics(): StatisticsData {
|
|
|
|
|
return {
|
|
|
|
|
nounCount: {},
|
|
|
|
|
verbCount: {},
|
|
|
|
|
metadataCount: {},
|
|
|
|
|
hnswIndexSize: 0,
|
|
|
|
|
fieldNames: {},
|
|
|
|
|
standardFieldMappings: {},
|
|
|
|
|
lastUpdated: new Date().toISOString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Detect if an error is a throttling error
|
|
|
|
|
* Override this method in specific adapters for custom detection
|
|
|
|
|
*/
|
|
|
|
|
protected isThrottlingError(error: any): boolean {
|
|
|
|
|
const statusCode = error.$metadata?.httpStatusCode || error.statusCode || error.code
|
|
|
|
|
const message = error.message?.toLowerCase() || ''
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
statusCode === 429 || // Too Many Requests
|
|
|
|
|
statusCode === 503 || // Service Unavailable / Slow Down
|
|
|
|
|
statusCode === 'ECONNRESET' || // Connection reset
|
|
|
|
|
statusCode === 'ETIMEDOUT' || // Timeout
|
|
|
|
|
message.includes('throttl') ||
|
|
|
|
|
message.includes('slow down') ||
|
|
|
|
|
message.includes('rate limit') ||
|
|
|
|
|
message.includes('too many requests') ||
|
|
|
|
|
message.includes('quota exceeded')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Track a throttling event
|
|
|
|
|
* @param error The error that caused throttling
|
|
|
|
|
* @param service Optional service that was throttled
|
|
|
|
|
*/
|
|
|
|
|
protected trackThrottlingEvent(error: any, service?: string): void {
|
|
|
|
|
this.throttlingDetected = true
|
|
|
|
|
this.consecutiveThrottleEvents++
|
|
|
|
|
this.lastThrottleTime = Date.now()
|
|
|
|
|
this.totalThrottleEvents++
|
|
|
|
|
|
|
|
|
|
// Track by hour
|
|
|
|
|
const hourIndex = new Date().getHours()
|
|
|
|
|
if (hourIndex !== this.lastThrottleHourIndex) {
|
|
|
|
|
// Reset hour tracking if we've moved to a new hour
|
|
|
|
|
this.throttleEventsByHour = new Array(24).fill(0)
|
|
|
|
|
this.lastThrottleHourIndex = hourIndex
|
|
|
|
|
}
|
|
|
|
|
this.throttleEventsByHour[hourIndex]++
|
|
|
|
|
|
|
|
|
|
// Track throttle reason
|
|
|
|
|
const reason = this.getThrottleReason(error)
|
|
|
|
|
this.throttleReasons[reason] = (this.throttleReasons[reason] || 0) + 1
|
|
|
|
|
|
|
|
|
|
// Track service-level throttling
|
|
|
|
|
if (service) {
|
|
|
|
|
const serviceInfo = this.serviceThrottling.get(service) || {
|
|
|
|
|
throttleCount: 0,
|
|
|
|
|
lastThrottle: 0,
|
|
|
|
|
status: 'normal' as const
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceInfo.throttleCount++
|
|
|
|
|
serviceInfo.lastThrottle = Date.now()
|
|
|
|
|
serviceInfo.status = 'throttled'
|
|
|
|
|
|
|
|
|
|
this.serviceThrottling.set(service, serviceInfo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exponential backoff
|
|
|
|
|
this.throttlingBackoffMs = Math.min(
|
|
|
|
|
this.throttlingBackoffMs * 2,
|
|
|
|
|
this.maxBackoffMs
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the reason for throttling from an error
|
|
|
|
|
*/
|
|
|
|
|
protected getThrottleReason(error: any): string {
|
|
|
|
|
const statusCode = error.$metadata?.httpStatusCode || error.statusCode || error.code
|
|
|
|
|
|
|
|
|
|
if (statusCode === 429) return '429_TooManyRequests'
|
|
|
|
|
if (statusCode === 503) return '503_ServiceUnavailable'
|
|
|
|
|
if (statusCode === 'ECONNRESET') return 'ConnectionReset'
|
|
|
|
|
if (statusCode === 'ETIMEDOUT') return 'Timeout'
|
|
|
|
|
|
|
|
|
|
const message = error.message?.toLowerCase() || ''
|
|
|
|
|
if (message.includes('throttl')) return 'Throttled'
|
|
|
|
|
if (message.includes('slow down')) return 'SlowDown'
|
|
|
|
|
if (message.includes('rate limit')) return 'RateLimit'
|
|
|
|
|
if (message.includes('quota exceeded')) return 'QuotaExceeded'
|
|
|
|
|
|
|
|
|
|
return 'Unknown'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear throttling state after successful operations
|
|
|
|
|
*/
|
|
|
|
|
protected clearThrottlingState(): void {
|
|
|
|
|
if (this.consecutiveThrottleEvents > 0) {
|
|
|
|
|
this.consecutiveThrottleEvents = 0
|
|
|
|
|
this.throttlingBackoffMs = 1000 // Reset to initial backoff
|
|
|
|
|
|
|
|
|
|
if (this.throttlingDetected) {
|
|
|
|
|
this.throttlingDetected = false
|
|
|
|
|
|
|
|
|
|
// Update service statuses
|
|
|
|
|
for (const [service, info] of this.serviceThrottling) {
|
|
|
|
|
if (info.status === 'throttled') {
|
|
|
|
|
info.status = 'recovering'
|
|
|
|
|
} else if (info.status === 'recovering') {
|
|
|
|
|
const timeSinceThrottle = Date.now() - info.lastThrottle
|
|
|
|
|
if (timeSinceThrottle > 60000) { // 1 minute recovery period
|
|
|
|
|
info.status = 'normal'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle throttling by implementing exponential backoff
|
|
|
|
|
* @param error The error that triggered throttling
|
|
|
|
|
* @param service Optional service that was throttled
|
|
|
|
|
*/
|
|
|
|
|
async handleThrottling(error: any, service?: string): Promise<void> {
|
|
|
|
|
if (this.isThrottlingError(error)) {
|
|
|
|
|
this.trackThrottlingEvent(error, service)
|
|
|
|
|
|
|
|
|
|
// Add delay for retry
|
|
|
|
|
const delayMs = this.throttlingBackoffMs
|
|
|
|
|
this.totalDelayMs += delayMs
|
|
|
|
|
this.delayedOperations++
|
|
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
|
|
|
} else {
|
|
|
|
|
// Clear throttling state on non-throttling errors
|
|
|
|
|
this.clearThrottlingState()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Track a retried operation
|
|
|
|
|
*/
|
|
|
|
|
protected trackRetriedOperation(): void {
|
|
|
|
|
this.retriedOperations++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Track an operation that failed due to throttling
|
|
|
|
|
*/
|
|
|
|
|
protected trackFailedDueToThrottling(): void {
|
|
|
|
|
this.failedDueToThrottling++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get current throttling metrics
|
|
|
|
|
*/
|
|
|
|
|
protected getThrottlingMetrics(): StatisticsData['throttlingMetrics'] {
|
|
|
|
|
const averageDelayMs = this.delayedOperations > 0
|
|
|
|
|
? this.totalDelayMs / this.delayedOperations
|
|
|
|
|
: 0
|
|
|
|
|
|
|
|
|
|
// Convert service throttling map to record
|
|
|
|
|
const serviceThrottlingRecord: Record<string, {
|
|
|
|
|
throttleCount: number
|
|
|
|
|
lastThrottle: string
|
|
|
|
|
status: 'normal' | 'throttled' | 'recovering'
|
|
|
|
|
}> = {}
|
|
|
|
|
|
|
|
|
|
for (const [service, info] of this.serviceThrottling) {
|
|
|
|
|
serviceThrottlingRecord[service] = {
|
|
|
|
|
throttleCount: info.throttleCount,
|
|
|
|
|
lastThrottle: new Date(info.lastThrottle).toISOString(),
|
|
|
|
|
status: info.status
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
storage: {
|
|
|
|
|
currentlyThrottled: this.throttlingDetected,
|
|
|
|
|
lastThrottleTime: this.lastThrottleTime > 0
|
|
|
|
|
? new Date(this.lastThrottleTime).toISOString()
|
|
|
|
|
: undefined,
|
|
|
|
|
consecutiveThrottleEvents: this.consecutiveThrottleEvents,
|
|
|
|
|
currentBackoffMs: this.throttlingBackoffMs,
|
|
|
|
|
totalThrottleEvents: this.totalThrottleEvents,
|
|
|
|
|
throttleEventsByHour: [...this.throttleEventsByHour],
|
|
|
|
|
throttleReasons: { ...this.throttleReasons }
|
|
|
|
|
},
|
|
|
|
|
operationImpact: {
|
|
|
|
|
delayedOperations: this.delayedOperations,
|
|
|
|
|
retriedOperations: this.retriedOperations,
|
|
|
|
|
failedDueToThrottling: this.failedDueToThrottling,
|
|
|
|
|
averageDelayMs,
|
|
|
|
|
totalDelayMs: this.totalDelayMs
|
|
|
|
|
},
|
|
|
|
|
serviceThrottling: Object.keys(serviceThrottlingRecord).length > 0
|
|
|
|
|
? serviceThrottlingRecord
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Include throttling metrics in statistics
|
|
|
|
|
*/
|
|
|
|
|
async getStatisticsWithThrottling(): Promise<StatisticsData | null> {
|
|
|
|
|
const stats = await this.getStatistics()
|
|
|
|
|
if (stats) {
|
|
|
|
|
stats.throttlingMetrics = this.getThrottlingMetrics()
|
|
|
|
|
}
|
|
|
|
|
return stats
|
|
|
|
|
}
|
2025-09-22 15:45:35 -07:00
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// Universal O(1) Count Management
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
// Universal count tracking - O(1) operations
|
|
|
|
|
protected totalNounCount = 0
|
|
|
|
|
protected totalVerbCount = 0
|
|
|
|
|
protected entityCounts: Map<string, number> = new Map() // type -> count
|
|
|
|
|
protected verbCounts: Map<string, number> = new Map() // verb type -> count
|
|
|
|
|
protected countCache: Map<string, { count: number; timestamp: number }> = new Map()
|
|
|
|
|
protected readonly COUNT_CACHE_TTL = 60000 // 1 minute cache TTL
|
|
|
|
|
|
2025-10-09 17:35:01 -07:00
|
|
|
// =============================================
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
// Count Persistence
|
2025-10-09 17:35:01 -07:00
|
|
|
// =============================================
|
|
|
|
|
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
// Counts changed since the last persist? Drives the write-through flush.
|
|
|
|
|
protected pendingCountPersist = false
|
2025-10-09 17:35:01 -07:00
|
|
|
|
2025-09-22 15:45:35 -07:00
|
|
|
/**
|
|
|
|
|
* Get total noun count - O(1) operation
|
|
|
|
|
* @returns Promise that resolves to the total number of nouns
|
|
|
|
|
*/
|
|
|
|
|
async getNounCount(): Promise<number> {
|
|
|
|
|
return this.totalNounCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get total verb count - O(1) operation
|
|
|
|
|
* @returns Promise that resolves to the total number of verbs
|
|
|
|
|
*/
|
|
|
|
|
async getVerbCount(): Promise<number> {
|
|
|
|
|
return this.totalVerbCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
refactor(8.0)!: remove distributed clustering subsystem — inert/orphaned, scale is single-process + native provider
The distributed-clustering subsystem never ran in production: it was inert,
orphaned dead code (faked consensus, stub replication, no live wiring, and it
did not interoperate with the 8.0 Db API). Brainy 8.0 is a single-process
library. Scale is single-process + the optional native provider
(@soulcraft/cortex, on-disk DiskANN to 10B+ vectors) + per-tenant pools +
horizontal read scaling (many reader processes, one writer).
Removed:
- src/distributed/ entirely (coordinator, shardManager, cacheSync,
readWriteSeparation, queryPlanner, healthMonitor, configManager,
hashPartitioner, shardMigration, domainDetector, storageDiscovery, http/network
transports). ReaderMode/HybridMode relocated to src/storage/operationalModes.ts
(slimmed to the live surface).
- src/types/distributedTypes.ts; config.distributed field + JSDoc;
coreTypes distributedConfig; memoryStorage distributedConfig persistence.
- DistributedRole enum + src/config/distributedPresets.ts and the orphaned
src/config/extensibleConfig.ts (config/augmentation registry built on removed
cloud adapters + distributed presets), plus their src/index.ts re-exports.
- 13 BRAINY_* cluster env vars; the storage setDistributedComponents hook;
enableDistributedSearch (dead config flag); the metadata partition field;
the distributed_ reserved key prefix.
- Orphaned src/storage/readOnlyOptimizations.ts (zero importers).
- Tests targeting the subsystem: distributed-demo, distributed-cluster helper,
distributed-transactions, sharding-transactions.
- Docs: EXTENDING_STORAGE.md (deleted); scrubbed distributed/cluster/Raft/
shard-manager/multi-node prose from v3-features, enterprise-for-everyone,
augmentations-actual, complete-feature-list, vfs/README, vfs/ROADMAP,
vfs/VFS_CORE, capacity-planning, transactions, MIGRATION-V3-TO-V4,
storage-architecture; reframed scale prose to the 8.0 model.
Kept: src/storage/sharding.ts (local-disk 256-bucket directory sharding via
getShardIdFromUuid — used live by baseStorage, unrelated to clustering);
the multi-process mode: 'reader' | 'writer' roles; semantic/HNSW clustering.
RELEASES.md: added a removed-surfaces row documenting the cut and the 8.0
scale model.
2026-06-15 10:37:39 -07:00
|
|
|
* Increment count for entity type - O(1) operation.
|
|
|
|
|
* Concurrency is handled by the process-global mutex
|
|
|
|
|
* ({@link incrementEntityCountSafe}); this raw form is for callers that
|
|
|
|
|
* already hold it.
|
2025-09-22 15:45:35 -07:00
|
|
|
* @param type The entity type
|
|
|
|
|
*/
|
|
|
|
|
protected incrementEntityCount(type: string): void {
|
|
|
|
|
this.entityCounts.set(type, (this.entityCounts.get(type) || 0) + 1)
|
|
|
|
|
this.totalNounCount++
|
|
|
|
|
// Update cache
|
|
|
|
|
this.countCache.set('nouns_count', {
|
|
|
|
|
count: this.totalNounCount,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
refactor(8.0)!: remove distributed clustering subsystem — inert/orphaned, scale is single-process + native provider
The distributed-clustering subsystem never ran in production: it was inert,
orphaned dead code (faked consensus, stub replication, no live wiring, and it
did not interoperate with the 8.0 Db API). Brainy 8.0 is a single-process
library. Scale is single-process + the optional native provider
(@soulcraft/cortex, on-disk DiskANN to 10B+ vectors) + per-tenant pools +
horizontal read scaling (many reader processes, one writer).
Removed:
- src/distributed/ entirely (coordinator, shardManager, cacheSync,
readWriteSeparation, queryPlanner, healthMonitor, configManager,
hashPartitioner, shardMigration, domainDetector, storageDiscovery, http/network
transports). ReaderMode/HybridMode relocated to src/storage/operationalModes.ts
(slimmed to the live surface).
- src/types/distributedTypes.ts; config.distributed field + JSDoc;
coreTypes distributedConfig; memoryStorage distributedConfig persistence.
- DistributedRole enum + src/config/distributedPresets.ts and the orphaned
src/config/extensibleConfig.ts (config/augmentation registry built on removed
cloud adapters + distributed presets), plus their src/index.ts re-exports.
- 13 BRAINY_* cluster env vars; the storage setDistributedComponents hook;
enableDistributedSearch (dead config flag); the metadata partition field;
the distributed_ reserved key prefix.
- Orphaned src/storage/readOnlyOptimizations.ts (zero importers).
- Tests targeting the subsystem: distributed-demo, distributed-cluster helper,
distributed-transactions, sharding-transactions.
- Docs: EXTENDING_STORAGE.md (deleted); scrubbed distributed/cluster/Raft/
shard-manager/multi-node prose from v3-features, enterprise-for-everyone,
augmentations-actual, complete-feature-list, vfs/README, vfs/ROADMAP,
vfs/VFS_CORE, capacity-planning, transactions, MIGRATION-V3-TO-V4,
storage-architecture; reframed scale prose to the 8.0 model.
Kept: src/storage/sharding.ts (local-disk 256-bucket directory sharding via
getShardIdFromUuid — used live by baseStorage, unrelated to clustering);
the multi-process mode: 'reader' | 'writer' roles; semantic/HNSW clustering.
RELEASES.md: added a removed-surfaces row documenting the cut and the 8.0
scale model.
2026-06-15 10:37:39 -07:00
|
|
|
* Thread-safe increment for concurrent scenarios.
|
|
|
|
|
* A process-global mutex serialises the read-modify-write so concurrent
|
|
|
|
|
* writers in the same process cannot lose updates.
|
2025-09-22 15:45:35 -07:00
|
|
|
*/
|
|
|
|
|
protected async incrementEntityCountSafe(type: string): Promise<void> {
|
|
|
|
|
const mutex = getGlobalMutex()
|
|
|
|
|
await mutex.runExclusive(`count-entity-${type}`, async () => {
|
|
|
|
|
this.incrementEntityCount(type)
|
2026-01-27 15:38:21 -08:00
|
|
|
// Smart batching: Adapts to storage type
|
2025-10-09 17:35:01 -07:00
|
|
|
// - Cloud storage (GCS, S3): Batches 10 ops OR 5 seconds
|
|
|
|
|
// - Local storage (File, Memory): Persists immediately
|
|
|
|
|
await this.scheduleCountPersist()
|
2025-09-22 15:45:35 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrement count for entity type - O(1) operation
|
|
|
|
|
* @param type The entity type
|
|
|
|
|
*/
|
|
|
|
|
protected decrementEntityCount(type: string): void {
|
|
|
|
|
const current = this.entityCounts.get(type) || 0
|
|
|
|
|
if (current > 1) {
|
|
|
|
|
this.entityCounts.set(type, current - 1)
|
|
|
|
|
} else {
|
|
|
|
|
this.entityCounts.delete(type)
|
|
|
|
|
}
|
|
|
|
|
if (this.totalNounCount > 0) {
|
|
|
|
|
this.totalNounCount--
|
|
|
|
|
}
|
|
|
|
|
// Update cache
|
|
|
|
|
this.countCache.set('nouns_count', {
|
|
|
|
|
count: this.totalNounCount,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Thread-safe decrement for concurrent scenarios
|
|
|
|
|
*/
|
|
|
|
|
protected async decrementEntityCountSafe(type: string): Promise<void> {
|
|
|
|
|
const mutex = getGlobalMutex()
|
|
|
|
|
await mutex.runExclusive(`count-entity-${type}`, async () => {
|
|
|
|
|
this.decrementEntityCount(type)
|
2026-01-27 15:38:21 -08:00
|
|
|
// Smart batching: Adapts to storage type
|
2025-10-09 17:35:01 -07:00
|
|
|
await this.scheduleCountPersist()
|
2025-09-22 15:45:35 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
refactor(8.0)!: remove distributed clustering subsystem — inert/orphaned, scale is single-process + native provider
The distributed-clustering subsystem never ran in production: it was inert,
orphaned dead code (faked consensus, stub replication, no live wiring, and it
did not interoperate with the 8.0 Db API). Brainy 8.0 is a single-process
library. Scale is single-process + the optional native provider
(@soulcraft/cortex, on-disk DiskANN to 10B+ vectors) + per-tenant pools +
horizontal read scaling (many reader processes, one writer).
Removed:
- src/distributed/ entirely (coordinator, shardManager, cacheSync,
readWriteSeparation, queryPlanner, healthMonitor, configManager,
hashPartitioner, shardMigration, domainDetector, storageDiscovery, http/network
transports). ReaderMode/HybridMode relocated to src/storage/operationalModes.ts
(slimmed to the live surface).
- src/types/distributedTypes.ts; config.distributed field + JSDoc;
coreTypes distributedConfig; memoryStorage distributedConfig persistence.
- DistributedRole enum + src/config/distributedPresets.ts and the orphaned
src/config/extensibleConfig.ts (config/augmentation registry built on removed
cloud adapters + distributed presets), plus their src/index.ts re-exports.
- 13 BRAINY_* cluster env vars; the storage setDistributedComponents hook;
enableDistributedSearch (dead config flag); the metadata partition field;
the distributed_ reserved key prefix.
- Orphaned src/storage/readOnlyOptimizations.ts (zero importers).
- Tests targeting the subsystem: distributed-demo, distributed-cluster helper,
distributed-transactions, sharding-transactions.
- Docs: EXTENDING_STORAGE.md (deleted); scrubbed distributed/cluster/Raft/
shard-manager/multi-node prose from v3-features, enterprise-for-everyone,
augmentations-actual, complete-feature-list, vfs/README, vfs/ROADMAP,
vfs/VFS_CORE, capacity-planning, transactions, MIGRATION-V3-TO-V4,
storage-architecture; reframed scale prose to the 8.0 model.
Kept: src/storage/sharding.ts (local-disk 256-bucket directory sharding via
getShardIdFromUuid — used live by baseStorage, unrelated to clustering);
the multi-process mode: 'reader' | 'writer' roles; semantic/HNSW clustering.
RELEASES.md: added a removed-surfaces row documenting the cut and the 8.0
scale model.
2026-06-15 10:37:39 -07:00
|
|
|
* Increment verb count - O(1) operation (now synchronous).
|
|
|
|
|
* Concurrency is handled by the process-global mutex
|
|
|
|
|
* ({@link incrementVerbCountSafe}); this raw form is for callers that already
|
|
|
|
|
* hold it.
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
* @param type The verb type
|
|
|
|
|
*/
|
|
|
|
|
protected incrementVerbCount(type: string): void {
|
|
|
|
|
this.verbCounts.set(type, (this.verbCounts.get(type) || 0) + 1)
|
|
|
|
|
this.totalVerbCount++
|
|
|
|
|
// Update cache
|
|
|
|
|
this.countCache.set('verbs_count', {
|
|
|
|
|
count: this.totalVerbCount,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
refactor(8.0)!: remove distributed clustering subsystem — inert/orphaned, scale is single-process + native provider
The distributed-clustering subsystem never ran in production: it was inert,
orphaned dead code (faked consensus, stub replication, no live wiring, and it
did not interoperate with the 8.0 Db API). Brainy 8.0 is a single-process
library. Scale is single-process + the optional native provider
(@soulcraft/cortex, on-disk DiskANN to 10B+ vectors) + per-tenant pools +
horizontal read scaling (many reader processes, one writer).
Removed:
- src/distributed/ entirely (coordinator, shardManager, cacheSync,
readWriteSeparation, queryPlanner, healthMonitor, configManager,
hashPartitioner, shardMigration, domainDetector, storageDiscovery, http/network
transports). ReaderMode/HybridMode relocated to src/storage/operationalModes.ts
(slimmed to the live surface).
- src/types/distributedTypes.ts; config.distributed field + JSDoc;
coreTypes distributedConfig; memoryStorage distributedConfig persistence.
- DistributedRole enum + src/config/distributedPresets.ts and the orphaned
src/config/extensibleConfig.ts (config/augmentation registry built on removed
cloud adapters + distributed presets), plus their src/index.ts re-exports.
- 13 BRAINY_* cluster env vars; the storage setDistributedComponents hook;
enableDistributedSearch (dead config flag); the metadata partition field;
the distributed_ reserved key prefix.
- Orphaned src/storage/readOnlyOptimizations.ts (zero importers).
- Tests targeting the subsystem: distributed-demo, distributed-cluster helper,
distributed-transactions, sharding-transactions.
- Docs: EXTENDING_STORAGE.md (deleted); scrubbed distributed/cluster/Raft/
shard-manager/multi-node prose from v3-features, enterprise-for-everyone,
augmentations-actual, complete-feature-list, vfs/README, vfs/ROADMAP,
vfs/VFS_CORE, capacity-planning, transactions, MIGRATION-V3-TO-V4,
storage-architecture; reframed scale prose to the 8.0 model.
Kept: src/storage/sharding.ts (local-disk 256-bucket directory sharding via
getShardIdFromUuid — used live by baseStorage, unrelated to clustering);
the multi-process mode: 'reader' | 'writer' roles; semantic/HNSW clustering.
RELEASES.md: added a removed-surfaces row documenting the cut and the 8.0
scale model.
2026-06-15 10:37:39 -07:00
|
|
|
* Thread-safe increment for verb counts.
|
|
|
|
|
* A process-global mutex serialises the read-modify-write so concurrent
|
|
|
|
|
* writers in the same process cannot lose updates.
|
2025-09-22 15:45:35 -07:00
|
|
|
* @param type The verb type
|
|
|
|
|
*/
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
protected async incrementVerbCountSafe(type: string): Promise<void> {
|
2025-09-22 15:45:35 -07:00
|
|
|
const mutex = getGlobalMutex()
|
|
|
|
|
await mutex.runExclusive(`count-verb-${type}`, async () => {
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
this.incrementVerbCount(type)
|
2026-01-27 15:38:21 -08:00
|
|
|
// Smart batching: Adapts to storage type
|
2025-10-09 17:35:01 -07:00
|
|
|
await this.scheduleCountPersist()
|
2025-09-22 15:45:35 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-27 15:38:21 -08:00
|
|
|
* Decrement verb count - O(1) operation (now synchronous)
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
* @param type The verb type
|
|
|
|
|
*/
|
|
|
|
|
protected decrementVerbCount(type: string): void {
|
|
|
|
|
const current = this.verbCounts.get(type) || 0
|
|
|
|
|
if (current > 1) {
|
|
|
|
|
this.verbCounts.set(type, current - 1)
|
|
|
|
|
} else {
|
|
|
|
|
this.verbCounts.delete(type)
|
|
|
|
|
}
|
|
|
|
|
if (this.totalVerbCount > 0) {
|
|
|
|
|
this.totalVerbCount--
|
|
|
|
|
}
|
|
|
|
|
// Update cache
|
|
|
|
|
this.countCache.set('verbs_count', {
|
|
|
|
|
count: this.totalVerbCount,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-27 15:38:21 -08:00
|
|
|
* Thread-safe decrement for verb counts
|
2025-09-22 15:45:35 -07:00
|
|
|
* @param type The verb type
|
|
|
|
|
*/
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
protected async decrementVerbCountSafe(type: string): Promise<void> {
|
2025-09-22 15:45:35 -07:00
|
|
|
const mutex = getGlobalMutex()
|
|
|
|
|
await mutex.runExclusive(`count-verb-${type}`, async () => {
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
this.decrementVerbCount(type)
|
2026-01-27 15:38:21 -08:00
|
|
|
// Smart batching: Adapts to storage type
|
2025-10-09 17:35:01 -07:00
|
|
|
await this.scheduleCountPersist()
|
2025-09-22 15:45:35 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 17:35:01 -07:00
|
|
|
// =============================================
|
2026-01-27 15:38:21 -08:00
|
|
|
// Smart Batching Methods
|
2025-10-09 17:35:01 -07:00
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
/**
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
* Persist counts immediately.
|
2026-01-07 12:51:05 -08:00
|
|
|
*
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
* Filesystem and memory storage have no network latency, so counts are
|
|
|
|
|
* written through on every change rather than batched.
|
2025-10-09 17:35:01 -07:00
|
|
|
*/
|
|
|
|
|
protected async scheduleCountPersist(): Promise<void> {
|
|
|
|
|
this.pendingCountPersist = true
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
await this.flushCounts()
|
2025-10-09 17:35:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
* Flush pending counts to storage.
|
2025-10-09 17:35:01 -07:00
|
|
|
*
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
* Used for graceful shutdown (SIGTERM handler) and the immediate
|
|
|
|
|
* write-through path. This is the public API that shutdown hooks can call.
|
2025-10-09 17:35:01 -07:00
|
|
|
*/
|
|
|
|
|
async flushCounts(): Promise<void> {
|
|
|
|
|
// Nothing to flush?
|
|
|
|
|
if (!this.pendingCountPersist) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Persist to storage (implemented by subclass)
|
|
|
|
|
await this.persistCounts()
|
|
|
|
|
this.pendingCountPersist = false
|
|
|
|
|
} catch (error) {
|
refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
2026-06-15 11:11:21 -07:00
|
|
|
console.error('CRITICAL: Failed to flush counts to storage:', error)
|
2025-10-09 17:35:01 -07:00
|
|
|
// Keep pending flag set so we retry on next operation
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-22 15:45:35 -07:00
|
|
|
/**
|
|
|
|
|
* Initialize counts from storage - must be implemented by each adapter
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
protected abstract initializeCounts(): Promise<void>
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Persist counts to storage - must be implemented by each adapter
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
|
|
|
|
protected abstract persistCounts(): Promise<void>
|
2025-08-26 12:32:21 -07:00
|
|
|
}
|