2025-10-13 16:39:06 -07:00
/ * *
* EntityIdMapper - Bidirectional mapping between UUID strings and integer IDs for roaring bitmaps
*
* Roaring bitmaps require 32 - bit unsigned integers , but Brainy uses UUID strings as entity IDs .
* This class provides efficient bidirectional mapping with persistence support .
*
* Features :
* - O ( 1 ) lookup in both directions
* - Persistent storage via storage adapter
* - Atomic counter for next ID
* - Serialization / deserialization support
*
* @module utils / entityIdMapper
* /
2025-10-13 16:42:45 -07:00
import type { StorageAdapter } from '../coreTypes.js'
2025-10-13 16:39:06 -07:00
export interface EntityIdMapperOptions {
2025-10-13 16:42:45 -07:00
storage : StorageAdapter
2025-10-13 16:39:06 -07:00
storageKey? : string
}
export interface EntityIdMapperData {
nextId : number
uuidToInt : Record < string , number >
intToUuid : Record < number , string >
}
/ * *
* Maps entity UUIDs to integer IDs for use with Roaring Bitmaps
* /
export class EntityIdMapper {
2025-10-13 16:42:45 -07:00
private storage : StorageAdapter
2025-10-13 16:39:06 -07:00
private storageKey : string
// Bidirectional maps
private uuidToInt = new Map < string , number > ( )
private intToUuid = new Map < number , string > ( )
// Atomic counter for next ID
private nextId = 1
// Dirty flag for persistence
private dirty = false
constructor ( options : EntityIdMapperOptions ) {
this . storage = options . storage
this . storageKey = options . storageKey || 'brainy:entityIdMapper'
}
/ * *
* Initialize the mapper by loading from storage
* /
async init ( ) : Promise < void > {
try {
2025-10-17 12:29:27 -07:00
const metadata = await this . storage . getMetadata ( this . storageKey )
2026-01-27 15:38:21 -08:00
// metadata IS the data (no nested 'data' property)
2025-10-27 17:01:37 -07:00
if ( metadata && ( metadata as any ) . nextId !== undefined ) {
const data = metadata as any as EntityIdMapperData
2025-10-13 16:39:06 -07:00
this . nextId = data . nextId
// Rebuild maps from serialized data
this . uuidToInt = new Map ( Object . entries ( data . uuidToInt ) . map ( ( [ k , v ] ) = > [ k , Number ( v ) ] ) )
this . intToUuid = new Map ( Object . entries ( data . intToUuid ) . map ( ( [ k , v ] ) = > [ Number ( k ) , v ] ) )
}
} catch ( error ) {
// First time initialization - maps are empty, nextId = 1
}
}
/ * *
* Get integer ID for UUID , assigning a new ID if not exists
* /
getOrAssign ( uuid : string ) : number {
const existing = this . uuidToInt . get ( uuid )
if ( existing !== undefined ) {
return existing
}
// Assign new ID
const newId = this . nextId ++
this . uuidToInt . set ( uuid , newId )
this . intToUuid . set ( newId , uuid )
this . dirty = true
return newId
}
2026-01-26 12:12:11 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* Get integer ID for UUID with immediate persistence guarantee
2026-01-26 12:12:11 -08:00
* Unlike getOrAssign ( ) , this method flushes to storage immediately after assigning
* a new ID . This prevents UUID → int mapping divergence if the process crashes
* before a normal flush ( ) occurs .
*
* Use this for critical operations where data integrity is paramount .
* Normal operations can use getOrAssign ( ) with batched flushing for better performance .
* /
async getOrAssignSync ( uuid : string ) : Promise < number > {
const id = this . getOrAssign ( uuid )
// If a new ID was assigned, immediately persist to storage
if ( this . dirty ) {
await this . flush ( )
}
return id
}
2025-10-13 16:39:06 -07:00
/ * *
* Get UUID for integer ID
* /
getUuid ( intId : number ) : string | undefined {
return this . intToUuid . get ( intId )
}
/ * *
* Get integer ID for UUID ( without assigning if not exists )
* /
getInt ( uuid : string ) : number | undefined {
return this . uuidToInt . get ( uuid )
}
/ * *
* Check if UUID has been assigned an integer ID
* /
has ( uuid : string ) : boolean {
return this . uuidToInt . has ( uuid )
}
/ * *
* Remove mapping for UUID
* /
remove ( uuid : string ) : boolean {
const intId = this . uuidToInt . get ( uuid )
if ( intId === undefined ) {
return false
}
this . uuidToInt . delete ( uuid )
this . intToUuid . delete ( intId )
this . dirty = true
return true
}
/ * *
* Get total number of mappings
* /
get size ( ) : number {
return this . uuidToInt . size
}
2026-02-01 16:23:49 -08:00
/ * *
* Get all mapped integer IDs without storage reads .
* Used for bitmap negation operations ( ne , exists :false , missing :true )
* to avoid full - table getAllIds ( ) scans .
* /
getAllIntIds ( ) : number [ ] {
return Array . from ( this . intToUuid . keys ( ) )
}
2025-10-13 16:39:06 -07:00
/ * *
* Convert array of UUIDs to array of integers
* /
uuidsToInts ( uuids : string [ ] ) : number [ ] {
return uuids . map ( uuid = > this . getOrAssign ( uuid ) )
}
/ * *
* Convert array of integers to array of UUIDs
* /
intsToUuids ( ints : number [ ] ) : string [ ] {
const result : string [ ] = [ ]
for ( const intId of ints ) {
const uuid = this . intToUuid . get ( intId )
if ( uuid ) {
result . push ( uuid )
}
}
return result
}
/ * *
* Convert iterable of integers to array of UUIDs ( for roaring bitmap iteration )
* /
intsIterableToUuids ( ints : Iterable < number > ) : string [ ] {
const result : string [ ] = [ ]
for ( const intId of ints ) {
const uuid = this . intToUuid . get ( intId )
if ( uuid ) {
result . push ( uuid )
}
}
return result
}
/ * *
* Flush mappings to storage
* /
async flush ( ) : Promise < void > {
if ( ! this . dirty ) {
return
}
// Convert maps to plain objects for serialization
2026-01-27 15:38:21 -08:00
// Add required 'noun' property for NounMetadata
2025-10-17 12:29:27 -07:00
const data = {
noun : 'EntityIdMapper' ,
2025-10-13 16:39:06 -07:00
nextId : this.nextId ,
uuidToInt : Object.fromEntries ( this . uuidToInt ) ,
intToUuid : Object.fromEntries ( this . intToUuid )
}
2025-10-17 12:29:27 -07:00
await this . storage . saveMetadata ( this . storageKey , data as any )
2025-10-13 16:39:06 -07:00
this . dirty = false
}
/ * *
* Clear all mappings
* /
async clear ( ) : Promise < void > {
this . uuidToInt . clear ( )
this . intToUuid . clear ( )
this . nextId = 1
this . dirty = true
await this . flush ( )
}
/ * *
* Get statistics about the mapper
* /
getStats() {
return {
mappings : this.uuidToInt.size ,
nextId : this.nextId ,
dirty : this.dirty ,
memoryEstimate : this.uuidToInt.size * ( 36 + 8 + 4 + 8 ) // uuid string + map overhead + int + map overhead
}
}
}