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.
This commit is contained in:
David Snelling 2026-05-27 11:49:49 -07:00
parent 547721ae14
commit 298b572671
12 changed files with 1552 additions and 0 deletions

View file

@ -878,6 +878,52 @@ export interface StorageAdapter {
details?: Record<string, any>
}>
/**
* Persist a raw binary blob under `key`, writing the bytes verbatim with no
* JSON envelope and no base64 inflation. Overwrites any existing blob at the
* same key. On filesystem-backed adapters the write is atomic (temp + rename).
*
* This is the zero-copy / mmap-friendly counterpart to the JSON object
* primitives intended for column-store segments and batch vector payloads
* where base64-in-JSON would inflate size ~33% and force full materialization.
*
* @param key - Logical blob key; "/"-separated segments nest under the
* adapter's `_blobs/` prefix (e.g. `"graph-lsm/source/sstable-123"`).
* @param data - The exact bytes to store.
*/
saveBinaryBlob(key: string, data: Buffer): Promise<void>
/**
* Load the raw bytes stored under `key`, byte-identical to what was saved, or
* `null` if no blob exists at that key.
*
* @param key - The blob key used when saving.
* @returns The blob bytes, or `null` if absent.
*/
loadBinaryBlob(key: string): Promise<Buffer | null>
/**
* Delete the blob stored under `key`. Missing blobs are ignored, so delete is
* idempotent.
*
* @param key - The blob key to delete.
*/
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 storage returns the on-disk path; remote object stores, in-memory
* storage, browser (OPFS) storage, and historical (read-only) storage return
* `null`. `null` is correct behavior for those backends, not a fallback
* callers must fall back to {@link loadBinaryBlob} when no path is available.
*
* @param key - The blob key.
* @returns An absolute local filesystem path, or `null` if none exists.
*/
getBinaryBlobPath(key: string): string | null
/**
* Save statistics data
* @param statistics The statistics data to save

View file

@ -1007,6 +1007,92 @@ export class AzureBlobStorage extends BaseStorage {
}
}
// ===========================================================================
// Raw binary-blob primitive
// ===========================================================================
/**
* Map a blob key to its Azure blob name under the shared `_blobs/` prefix, e.g.
* `"graph-lsm/source/sstable-123"` `"_blobs/graph-lsm/source/sstable-123.bin"`.
*
* @param key - The blob key.
* @returns The Azure blob name for the blob.
* @private
*/
private blobObjectKey(key: string): string {
return `_blobs/${key}.bin`
}
/**
* Store a raw binary blob as an Azure block blob, writing the bytes verbatim
* with `application/octet-stream` content type. Overwrites any existing blob at
* the same key.
*
* @param key - The blob key.
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
await this.ensureInitialized()
await this.ensureValidatedForWrite()
const blockBlobClient = this.containerClient!.getBlockBlobClient(this.blobObjectKey(key))
await blockBlobClient.upload(data, data.length, {
blobHTTPHeaders: { blobContentType: 'application/octet-stream' }
})
}
/**
* Load the raw bytes of the Azure blob for `key`, or `null` if it does not
* exist.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
await this.ensureInitialized()
try {
const blockBlobClient = this.containerClient!.getBlockBlobClient(this.blobObjectKey(key))
const downloadResponse = await blockBlobClient.download(0)
return await this.streamToBuffer(downloadResponse.readableStreamBody!)
} catch (error: any) {
if (error.statusCode === 404 || error.code === 'BlobNotFound') {
return null
}
throw BrainyError.fromError(error, `loadBinaryBlob(${key})`)
}
}
/**
* Delete the Azure blob for `key`. Missing blobs are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
try {
const blockBlobClient = this.containerClient!.getBlockBlobClient(this.blobObjectKey(key))
await blockBlobClient.delete()
} catch (error: any) {
if (error.statusCode === 404 || error.code === 'BlobNotFound') {
return
}
throw new Error(`Failed to delete blob ${key}: ${error}`)
}
}
/**
* Azure Blob Storage is remote with no local filesystem path to mmap, so this
* always returns `null`. Callers must use {@link loadBinaryBlob} instead.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* Batch delete multiple blobs from Azure Blob Storage
* Deletes up to 256 blobs per batch (Azure limit)

View file

@ -129,6 +129,84 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
details?: Record<string, any>
}>
// ===========================================================================
// 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
// suffixed with `.bin`. Blobs are deliberately NOT branch-scoped (COW): they
// are immutable, content-addressed segments managed by their producer,
// mirroring how COW metadata under `_cow/` is also kept global.
/**
* 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
/**
* Get optimal batch configuration for this storage adapter
* Override in subclasses to provide storage-specific optimization

View file

@ -89,6 +89,10 @@ export class FileSystemStorage extends BaseStorage {
private indexDir!: string // Legacy - for backward compatibility
private systemDir!: string
private lockDir!: string
// Root for raw binary blobs (`<rootDir>/_blobs`). Blobs are stored verbatim
// (no JSON envelope, no compression) so native code can mmap them directly via
// getBinaryBlobPath(). Set in init() once the path module is loaded.
private blobsDir!: string
private activeLocks: Set<string> = new Set()
private lockTimers: Map<string, NodeJS.Timeout> = new Map() // Track timers for cleanup
private allTimers: Set<NodeJS.Timeout> = new Set() // Track all timers for cleanup
@ -213,6 +217,7 @@ export class FileSystemStorage extends BaseStorage {
this.indexDir = path.join(this.rootDir, 'indexes')
this.systemDir = path.join(this.rootDir, SYSTEM_DIR)
this.lockDir = path.join(this.rootDir, 'locks')
this.blobsDir = path.join(this.rootDir, '_blobs')
// Create the root directory if it doesn't exist
await this.ensureDirectoryExists(this.rootDir)
@ -242,6 +247,9 @@ export class FileSystemStorage extends BaseStorage {
// Create the locks directory if it doesn't exist
await this.ensureDirectoryExists(this.lockDir)
// Create the binary blobs directory if it doesn't exist
await this.ensureDirectoryExists(this.blobsDir)
// Initialize count management
this.countsFilePath = path.join(this.systemDir, 'counts.json')
await this.initializeCounts()
@ -946,6 +954,91 @@ export class FileSystemStorage extends BaseStorage {
}
}
// ===========================================================================
// Raw binary-blob primitive (mmap-friendly)
// ===========================================================================
/**
* Resolve a blob key to its on-disk path under `<rootDir>/_blobs`.
*
* The key's "/"-separated segments become nested directories and the file is
* suffixed with `.bin`, e.g. `"graph-lsm/source/sstable-123"`
* `<rootDir>/_blobs/graph-lsm/source/sstable-123.bin`. This convention is
* shared with cortex's `MmapFileSystemStorage` so native code and the TS layer
* agree on exactly where each blob lives.
*
* @param key - The blob key.
* @returns The absolute on-disk path for the blob.
* @private
*/
private blobPath(key: string): string {
// `path` and `blobsDir` are populated in init(). If a caller resolves a blob
// path before init() (e.g. native code probing for a mmap target), fall back
// to a POSIX-style join off rootDir so this synchronous method never throws.
if (path && this.blobsDir) {
return path.join(this.blobsDir, ...key.split('/')) + '.bin'
}
return `${this.rootDir}/_blobs/${key}.bin`
}
/**
* Persist a raw binary blob verbatim under `key`, using an atomic
* temp-file + rename so concurrent readers never observe a torn write.
* Parent directories are created on demand.
*
* @param key - The blob key (see {@link getBinaryBlobPath} for the convention).
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
await this.ensureInitialized()
const filePath = this.blobPath(key)
await this.ensureDirectoryExists(path.dirname(filePath))
const tmpPath = filePath + '.tmp'
await fs.promises.writeFile(tmpPath, data)
await fs.promises.rename(tmpPath, filePath)
}
/**
* Load the raw bytes stored under `key`, or `null` if the blob does not exist.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
await this.ensureInitialized()
try {
return await fs.promises.readFile(this.blobPath(key))
} catch {
return null
}
}
/**
* Delete the blob stored under `key`. Missing blobs are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
try {
await fs.promises.unlink(this.blobPath(key))
} catch {
/* ignore missing files */
}
}
/**
* Return the real on-disk path for `key` so native code can mmap the file
* directly. The path is returned whether or not the file currently exists
* callers are expected to write before mapping.
*
* @param key - The blob key.
* @returns The absolute on-disk path (never `null` for filesystem storage).
*/
public getBinaryBlobPath(key: string): string | null {
return this.blobPath(key)
}
/**
* Get multiple metadata objects in batches (CRITICAL: Prevents socket exhaustion)
* FileSystem implementation uses controlled concurrency to prevent too many file reads

View file

@ -1036,6 +1036,93 @@ export class GcsStorage extends BaseStorage {
}
}
// ===========================================================================
// Raw binary-blob primitive
// ===========================================================================
/**
* Map a blob key to its GCS object name under the shared `_blobs/` prefix, e.g.
* `"graph-lsm/source/sstable-123"` `"_blobs/graph-lsm/source/sstable-123.bin"`.
*
* @param key - The blob key.
* @returns The GCS object name for the blob.
* @private
*/
private blobObjectKey(key: string): string {
return `_blobs/${key}.bin`
}
/**
* Store a raw binary blob as a GCS object, writing the bytes verbatim with
* `application/octet-stream` content type. Overwrites any existing blob at the
* same key.
*
* @param key - The blob key.
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
await this.ensureInitialized()
await this.ensureValidatedForWrite()
const file = this.bucket!.file(this.blobObjectKey(key))
await file.save(data, {
contentType: 'application/octet-stream',
resumable: false
})
}
/**
* Load the raw bytes of the GCS blob object for `key`, or `null` if it does not
* exist.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
await this.ensureInitialized()
try {
const file = this.bucket!.file(this.blobObjectKey(key))
const [contents] = await file.download()
return Buffer.from(contents)
} catch (error: any) {
if (error.code === 404) {
return null
}
throw BrainyError.fromError(error, `loadBinaryBlob(${key})`)
}
}
/**
* Delete the GCS blob object for `key`. Missing objects are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
try {
const file = this.bucket!.file(this.blobObjectKey(key))
await file.delete()
} catch (error: any) {
if (error.code === 404) {
return
}
throw new Error(`Failed to delete blob ${key}: ${error}`)
}
}
/**
* GCS is a remote object store with no local filesystem path to mmap, so this
* always returns `null`. Callers must use {@link loadBinaryBlob} instead.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* List all objects under a specific prefix in GCS
* Primitive operation required by base class

View file

@ -264,6 +264,81 @@ export class HistoricalStorageAdapter extends BaseStorage {
)
}
// ===========================================================================
// Raw binary-blob primitive (read-only)
// ===========================================================================
/**
* WRITE BLOCKED: Historical storage is read-only.
*
* @param key - The blob key (unused; included for the error message).
* @throws Always historical state is immutable.
*/
public async saveBinaryBlob(key: string, _data: Buffer): Promise<void> {
throw new Error(
`Historical storage is read-only. Cannot save binary blob: ${key}`
)
}
/**
* Load the raw bytes of a binary blob from the historical commit state by
* walking the commit tree for the blob entry at `_blobs/<key>.bin` and reading
* its content-addressed bytes verbatim (no JSON decode). Returns `null` if the
* blob was not present in this commit.
*
* @param key - The blob key (same convention as the live adapters).
* @returns The blob bytes as stored at this commit, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
try {
const { CommitObject } = await import('../cow/CommitObject.js')
const { TreeObject } = await import('../cow/TreeObject.js')
const { isNullHash } = await import('../cow/constants.js')
const commit = await CommitObject.read(this.blobStorage!, this.commitId)
if (isNullHash(commit.tree)) {
return null
}
const tree = await TreeObject.read(this.blobStorage!, commit.tree)
const blobName = `_blobs/${key}.bin`
for await (const entry of TreeObject.walk(this.blobStorage!, tree)) {
if (entry.type === 'blob' && entry.name === blobName) {
return await this.blobStorage!.read(entry.hash)
}
}
return null
} catch (error) {
// Blob not present in historical state
return null
}
}
/**
* DELETE BLOCKED: Historical storage is read-only.
*
* @param key - The blob key (unused; included for the error message).
* @throws Always historical state is immutable.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
throw new Error(
`Historical storage is read-only. Cannot delete binary blob: ${key}`
)
}
/**
* Historical state lives in the content-addressed commit store, not on the
* local filesystem, so there is no mmap-able path. Always returns `null`.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* Get storage statistics from historical commit
*/

View file

@ -30,6 +30,11 @@ export class MemoryStorage extends BaseStorage {
// Unified object store for primitive operations (replaces metadata, nounMetadata, verbMetadata)
private objectStore: Map<string, any> = new Map()
// Raw binary-blob store, keyed by blob key. Holds opaque byte payloads
// (column-store segments, batch vectors) verbatim — no JSON envelope. In-memory
// storage has no local file, so getBinaryBlobPath() returns null.
private blobStore: Map<string, Buffer> = new Map()
// Backward compatibility aliases
private get metadata(): Map<string, any> {
return this.objectStore
@ -136,6 +141,54 @@ export class MemoryStorage extends BaseStorage {
return paths.sort()
}
// ===========================================================================
// Raw binary-blob primitive
// ===========================================================================
/**
* Persist a raw binary blob in memory under `key`. A defensive copy of the
* bytes is stored so later mutations to the caller's buffer don't corrupt the
* stored blob. Overwrites any existing blob at the same key.
*
* @param key - The blob key.
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
this.blobStore.set(key, Buffer.from(data))
}
/**
* Load a copy of the bytes stored under `key`, or `null` if absent. A copy is
* returned so callers cannot mutate the stored blob in place.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
const data = this.blobStore.get(key)
return data ? Buffer.from(data) : null
}
/**
* Delete the blob stored under `key`. Missing blobs are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
this.blobStore.delete(key)
}
/**
* In-memory storage has no local filesystem path to mmap, so this always
* returns `null`. Callers must use {@link loadBinaryBlob} instead.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* Get multiple metadata objects in batches (CRITICAL: Prevents socket exhaustion)
* Memory storage implementation is simple since all data is already in memory
@ -163,6 +216,7 @@ export class MemoryStorage extends BaseStorage {
*/
public async clear(): Promise<void> {
this.objectStore.clear()
this.blobStore.clear()
this.statistics = null
this.totalNounCount = 0
this.totalVerbCount = 0

View file

@ -402,6 +402,122 @@ export class OPFSStorage extends BaseStorage {
}
}
// ===========================================================================
// Raw binary-blob primitive
// ===========================================================================
/**
* Resolve a blob key to its OPFS path under the shared `_blobs/` prefix, e.g.
* `"graph-lsm/source/sstable-123"`
* `["_blobs", "graph-lsm", "source", "sstable-123.bin"]` (returned as segments
* to navigate the OPFS directory tree). Mirrors the on-disk convention used by
* filesystem storage.
*
* @param key - The blob key.
* @returns Path segments for the blob, including the trailing `.bin` filename.
* @private
*/
private blobPathSegments(key: string): string[] {
const parts = key.split('/')
const filename = parts.pop()! + '.bin'
return ['_blobs', ...parts, filename]
}
/**
* Store a raw binary blob in OPFS under `key`, writing the bytes verbatim (no
* JSON envelope). Parent directories are created on demand. Overwrites any
* existing blob at the same key.
*
* @param key - The blob key.
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
await this.ensureInitialized()
const segments = this.blobPathSegments(key)
const filename = segments.pop()!
let currentDir = this.rootDir!
for (const dirName of segments) {
currentDir = await currentDir.getDirectoryHandle(dirName, { create: true })
}
const fileHandle = await currentDir.getFileHandle(filename, { create: true })
const writable = await fileHandle.createWritable()
// Write a copy of the exact bytes; Uint8Array view keeps it binary-clean.
await writable.write(new Uint8Array(data))
await writable.close()
}
/**
* Load the raw bytes of the OPFS blob for `key`, or `null` if it does not
* exist.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
await this.ensureInitialized()
try {
const segments = this.blobPathSegments(key)
const filename = segments.pop()!
let currentDir = this.rootDir!
for (const dirName of segments) {
currentDir = await currentDir.getDirectoryHandle(dirName)
}
const fileHandle = await currentDir.getFileHandle(filename)
const file = await fileHandle.getFile()
const arrayBuffer = await file.arrayBuffer()
return Buffer.from(arrayBuffer)
} catch (error: any) {
if (error.name === 'NotFoundError') {
return null
}
throw error
}
}
/**
* Delete the OPFS blob for `key`. Missing blobs are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
try {
const segments = this.blobPathSegments(key)
const filename = segments.pop()!
let currentDir = this.rootDir!
for (const dirName of segments) {
currentDir = await currentDir.getDirectoryHandle(dirName)
}
await currentDir.removeEntry(filename)
} catch (error: any) {
if (error.name === 'NotFoundError') {
return
}
throw error
}
}
/**
* OPFS is browser-sandboxed storage with no real local filesystem path to
* mmap, so this always returns `null`. Callers must use {@link loadBinaryBlob}
* instead.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* Get multiple metadata objects in batches (CRITICAL: Prevents socket exhaustion)
* OPFS implementation uses controlled concurrency for file operations

View file

@ -725,6 +725,110 @@ export class R2Storage extends BaseStorage {
}
}
// ===========================================================================
// Raw binary-blob primitive
// ===========================================================================
/**
* Map a blob key to its R2 object key under the shared `_blobs/` prefix, e.g.
* `"graph-lsm/source/sstable-123"` `"_blobs/graph-lsm/source/sstable-123.bin"`.
*
* @param key - The blob key.
* @returns The R2 object key for the blob.
* @private
*/
private blobObjectKey(key: string): string {
return `_blobs/${key}.bin`
}
/**
* Store a raw binary blob as an R2 object, writing the bytes verbatim with
* `application/octet-stream` content type. Overwrites any existing blob at the
* same key.
*
* @param key - The blob key.
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
await this.ensureInitialized()
const { PutObjectCommand } = await import('@aws-sdk/client-s3')
await this.s3Client!.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: this.blobObjectKey(key),
Body: data,
ContentType: 'application/octet-stream'
})
)
}
/**
* Load the raw bytes of the R2 blob object for `key`, or `null` if it does not
* exist.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
await this.ensureInitialized()
try {
const { GetObjectCommand } = await import('@aws-sdk/client-s3')
const response = await this.s3Client!.send(
new GetObjectCommand({
Bucket: this.bucketName,
Key: this.blobObjectKey(key)
})
)
if (!response || !response.Body) {
return null
}
const bytes = await response.Body.transformToByteArray()
return Buffer.from(bytes)
} catch (error: any) {
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
return null
}
throw BrainyError.fromError(error, `loadBinaryBlob(${key})`)
}
}
/**
* Delete the R2 blob object for `key`. Missing objects are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
try {
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3')
await this.s3Client!.send(
new DeleteObjectCommand({
Bucket: this.bucketName,
Key: this.blobObjectKey(key)
})
)
} catch (error: any) {
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
return
}
throw new Error(`Failed to delete blob ${key}: ${error}`)
}
}
/**
* R2 is a remote object store with no local filesystem path to mmap, so this
* always returns `null`. Callers must use {@link loadBinaryBlob} instead.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* List all objects under a specific prefix in R2
*/

View file

@ -1961,6 +1961,126 @@ export class S3CompatibleStorage extends BaseStorage {
}
}
// ===========================================================================
// Raw binary-blob primitive
// ===========================================================================
/**
* Map a blob key to its S3 object key under the shared `_blobs/` prefix, e.g.
* `"graph-lsm/source/sstable-123"` `"_blobs/graph-lsm/source/sstable-123.bin"`.
* Matches the on-disk convention used by filesystem storage so the same logical
* key resolves consistently across backends.
*
* @param key - The blob key.
* @returns The S3 object key for the blob.
* @private
*/
private blobObjectKey(key: string): string {
return `_blobs/${key}.bin`
}
/**
* Store a raw binary blob as an S3 object, writing the bytes verbatim with
* `application/octet-stream` content type (no JSON envelope, no base64).
* Overwrites any existing blob at the same key.
*
* @param key - The blob key.
* @param data - The exact bytes to store.
*/
public async saveBinaryBlob(key: string, data: Buffer): Promise<void> {
await this.ensureInitialized()
await this.ensureValidatedForWrite()
const { PutObjectCommand } = await import('@aws-sdk/client-s3')
await this.s3Client!.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: this.blobObjectKey(key),
Body: data,
ContentType: 'application/octet-stream'
})
)
}
/**
* Load the raw bytes of the S3 blob object for `key`, or `null` if it does not
* exist.
*
* @param key - The blob key.
* @returns The blob bytes, or `null` if absent.
*/
public async loadBinaryBlob(key: string): Promise<Buffer | null> {
await this.ensureInitialized()
try {
const { GetObjectCommand } = await import('@aws-sdk/client-s3')
const response = await this.s3Client!.send(
new GetObjectCommand({
Bucket: this.bucketName,
Key: this.blobObjectKey(key)
})
)
if (!response || !response.Body) {
return null
}
const bytes = await response.Body.transformToByteArray()
return Buffer.from(bytes)
} catch (error: any) {
if (
error.name === 'NoSuchKey' ||
(error.message &&
(error.message.includes('NoSuchKey') ||
error.message.includes('not found') ||
error.message.includes('does not exist')))
) {
return null
}
throw BrainyError.fromError(error, `loadBinaryBlob(${key})`)
}
}
/**
* Delete the S3 blob object for `key`. Missing objects are ignored.
*
* @param key - The blob key.
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
await this.ensureValidatedForWrite()
try {
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3')
await this.s3Client!.send(
new DeleteObjectCommand({
Bucket: this.bucketName,
Key: this.blobObjectKey(key)
})
)
} catch (error: any) {
if (
error.name === 'NoSuchKey' ||
(error.message &&
(error.message.includes('NoSuchKey') ||
error.message.includes('not found') ||
error.message.includes('does not exist')))
) {
return
}
throw new Error(`Failed to delete blob ${key}: ${error}`)
}
}
/**
* S3 is a remote object store with no local filesystem path to mmap, so this
* always returns `null`. Callers must use {@link loadBinaryBlob} instead.
*
* @param _key - The blob key (unused).
* @returns Always `null`.
*/
public getBinaryBlobPath(_key: string): string | null {
return null
}
/**
* Batch delete multiple objects from S3-compatible storage
* Deletes up to 1000 objects per batch (S3 limit)

View file

@ -2123,6 +2123,12 @@ export abstract class BaseStorage extends BaseStorageAdapter {
*/
protected abstract listObjectsUnderPath(prefix: string): Promise<string[]>
// Raw binary-blob primitive (saveBinaryBlob/loadBinaryBlob/deleteBinaryBlob/
// getBinaryBlobPath) is declared abstract on BaseStorageAdapter — the class
// that `implements StorageAdapter` — alongside the other public storage
// methods. Each concrete adapter implements it. See BaseStorageAdapter for the
// contract and the shared `_blobs/<key>.bin` key→location convention.
/**
* Save metadata to storage (now typed)
* Routes to correct location (system or entity) based on key format