Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions libs/vectoriadb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
"types": "./dist/src/worker.d.ts",
"import": "./dist/src/worker.js",
"default": "./dist/src/worker.js"
},
"./browser": {
"development": "./src/worker.ts",
"types": "./dist/src/worker.d.ts",
"import": "./dist/src/worker.js",
"default": "./dist/src/worker.js"
}
},
"peerDependencies": {
Expand Down
26 changes: 25 additions & 1 deletion libs/vectoriadb/src/storage/file.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import type { DocumentMetadata } from '../interfaces';
import type { StorageAdapterConfig, StoredData } from './adapter.interface';
Expand Down Expand Up @@ -28,9 +27,30 @@ export interface FileStorageConfig extends StorageAdapterConfig {
* Perfect for local development to avoid recalculating embeddings
*/
export class FileStorageAdapter<T extends DocumentMetadata = DocumentMetadata> extends BaseStorageAdapter<T> {
// Cached `fs/promises` module. Loaded lazily (see `getFs`) so that importing
// the package root does not force browser/worker bundlers to resolve
// `node:fs`. This is a Node-only adapter; `path` stays a static import because
// it is pure JS, tree-shakeable, and needed synchronously in the constructor.
private static _fs: typeof import('fs/promises') | null = null;

private fileConfig: Required<Pick<FileStorageConfig, 'cacheDir' | 'fileName'>>;
private filePath: string;

/**
* Lazily import `fs/promises`. Keeping this off the top-level import graph
* means importing the package root never forces a bundler to statically
* resolve `node:fs`; the module is only loaded when this Node-only adapter is
* actually used. The adapter throws naturally if `fs` is unavailable at runtime.
*/
private async getFs(): Promise<typeof import('fs/promises')> {
if (FileStorageAdapter._fs) {
return FileStorageAdapter._fs;
}
const mod = await import('fs/promises');
FileStorageAdapter._fs = mod;
return mod;
}

constructor(config: FileStorageConfig = {}) {
super(config);

Expand Down Expand Up @@ -86,6 +106,7 @@ export class FileStorageAdapter<T extends DocumentMetadata = DocumentMetadata> e

override async initialize(): Promise<void> {
// Ensure cache directory exists
const fs = await this.getFs();
const dir = path.dirname(this.filePath);
try {
await fs.mkdir(dir, { recursive: true });
Expand All @@ -101,6 +122,7 @@ export class FileStorageAdapter<T extends DocumentMetadata = DocumentMetadata> e

override async load(): Promise<StoredData<T> | null> {
try {
const fs = await this.getFs();
const content = await fs.readFile(this.filePath, 'utf-8');
return this.safeJsonParse<StoredData<T>>(content);
} catch {
Expand All @@ -115,6 +137,7 @@ export class FileStorageAdapter<T extends DocumentMetadata = DocumentMetadata> e
if (!content) {
throw new StorageError('Failed to serialize embeddings data');
}
const fs = await this.getFs();
await fs.writeFile(this.filePath, content, 'utf-8');
} catch (error) {
if (error instanceof StorageError) {
Expand All @@ -129,6 +152,7 @@ export class FileStorageAdapter<T extends DocumentMetadata = DocumentMetadata> e

override async clear(): Promise<void> {
try {
const fs = await this.getFs();
await fs.unlink(this.filePath);
} catch {
// File doesn't exist, ignore
Expand Down
Loading