verso-db 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +13 -7
- package/dist/BinaryHeap.d.ts +11 -1
- package/dist/BinaryHeap.d.ts.map +1 -1
- package/dist/BinaryHeap.js +138 -0
- package/dist/BinaryHeap.js.map +1 -0
- package/dist/Collection.d.ts +30 -4
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +1186 -0
- package/dist/Collection.js.map +1 -0
- package/dist/HNSWIndex.d.ts +59 -0
- package/dist/HNSWIndex.d.ts.map +1 -1
- package/dist/HNSWIndex.js +2818 -0
- package/dist/HNSWIndex.js.map +1 -0
- package/dist/MaxBinaryHeap.d.ts +2 -64
- package/dist/MaxBinaryHeap.d.ts.map +1 -1
- package/dist/MaxBinaryHeap.js +5 -0
- package/dist/MaxBinaryHeap.js.map +1 -0
- package/dist/SearchWorker.d.ts +57 -4
- package/dist/SearchWorker.d.ts.map +1 -1
- package/dist/SearchWorker.js +573 -0
- package/dist/SearchWorker.js.map +1 -0
- package/dist/VectorDB.d.ts.map +1 -1
- package/dist/VectorDB.js +246 -0
- package/dist/VectorDB.js.map +1 -0
- package/dist/WorkerPool.d.ts +32 -2
- package/dist/WorkerPool.d.ts.map +1 -1
- package/dist/WorkerPool.js +266 -0
- package/dist/WorkerPool.js.map +1 -0
- package/dist/backends/JsDistanceBackend.d.ts.map +1 -1
- package/dist/backends/JsDistanceBackend.js +163 -0
- package/dist/backends/JsDistanceBackend.js.map +1 -0
- package/dist/encoding/DeltaEncoder.d.ts +2 -2
- package/dist/encoding/DeltaEncoder.d.ts.map +1 -1
- package/dist/encoding/DeltaEncoder.js +199 -0
- package/dist/encoding/DeltaEncoder.js.map +1 -0
- package/dist/errors.js +97 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -42
- package/dist/index.js.map +1 -9
- package/dist/presets.js +205 -0
- package/dist/presets.js.map +1 -0
- package/dist/quantization/ScalarQuantizer.d.ts +0 -34
- package/dist/quantization/ScalarQuantizer.d.ts.map +1 -1
- package/dist/quantization/ScalarQuantizer.js +346 -0
- package/dist/quantization/ScalarQuantizer.js.map +1 -0
- package/dist/storage/BatchWriter.js +351 -0
- package/dist/storage/BatchWriter.js.map +1 -0
- package/dist/storage/BunStorageBackend.d.ts +7 -3
- package/dist/storage/BunStorageBackend.d.ts.map +1 -1
- package/dist/storage/BunStorageBackend.js +182 -0
- package/dist/storage/BunStorageBackend.js.map +1 -0
- package/dist/storage/MemoryBackend.js +109 -0
- package/dist/storage/MemoryBackend.js.map +1 -0
- package/dist/storage/OPFSBackend.d.ts.map +1 -1
- package/dist/storage/OPFSBackend.js +325 -0
- package/dist/storage/OPFSBackend.js.map +1 -0
- package/dist/storage/StorageBackend.js +12 -0
- package/dist/storage/StorageBackend.js.map +1 -0
- package/dist/storage/WriteAheadLog.js +321 -0
- package/dist/storage/WriteAheadLog.js.map +1 -0
- package/dist/storage/createStorageBackend.d.ts +4 -0
- package/dist/storage/createStorageBackend.d.ts.map +1 -1
- package/dist/storage/createStorageBackend.js +119 -0
- package/dist/storage/createStorageBackend.js.map +1 -0
- package/{src/storage/index.ts → dist/storage/index.js} +7 -27
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/nodeFsRuntime.d.ts +14 -0
- package/dist/storage/nodeFsRuntime.d.ts.map +1 -0
- package/dist/storage/nodeFsRuntime.js +105 -0
- package/dist/storage/nodeFsRuntime.js.map +1 -0
- package/package.json +9 -7
- package/src/BinaryHeap.ts +0 -136
- package/src/Collection.ts +0 -1262
- package/src/HNSWIndex.ts +0 -2894
- package/src/MaxBinaryHeap.ts +0 -181
- package/src/SearchWorker.ts +0 -264
- package/src/VectorDB.ts +0 -319
- package/src/WorkerPool.ts +0 -222
- package/src/backends/JsDistanceBackend.ts +0 -171
- package/src/encoding/DeltaEncoder.ts +0 -236
- package/src/errors.ts +0 -110
- package/src/index.ts +0 -106
- package/src/presets.ts +0 -229
- package/src/quantization/ScalarQuantizer.ts +0 -487
- package/src/storage/BatchWriter.ts +0 -420
- package/src/storage/BunStorageBackend.ts +0 -199
- package/src/storage/MemoryBackend.ts +0 -122
- package/src/storage/OPFSBackend.ts +0 -348
- package/src/storage/StorageBackend.ts +0 -74
- package/src/storage/WriteAheadLog.ts +0 -379
- package/src/storage/createStorageBackend.ts +0 -137
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-Memory Storage Backend
|
|
3
|
-
*
|
|
4
|
-
* Stores all data in memory using a Map. Useful for:
|
|
5
|
-
* - Testing without file system
|
|
6
|
-
* - Small datasets that fit in memory
|
|
7
|
-
* - Browser environments without OPFS/IndexedDB
|
|
8
|
-
* - Temporary caches
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { StorageBackend } from './StorageBackend';
|
|
12
|
-
|
|
13
|
-
export class MemoryBackend implements StorageBackend {
|
|
14
|
-
readonly type = 'memory';
|
|
15
|
-
private storage: Map<string, ArrayBuffer> = new Map();
|
|
16
|
-
private directories: Set<string> = new Set();
|
|
17
|
-
|
|
18
|
-
constructor() {
|
|
19
|
-
this.directories.add(''); // Root directory always exists
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async read(key: string): Promise<ArrayBuffer | null> {
|
|
23
|
-
const data = this.storage.get(key);
|
|
24
|
-
if (!data) return null;
|
|
25
|
-
// Return a copy to prevent mutation
|
|
26
|
-
return data.slice(0);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async write(key: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
30
|
-
// Store a copy to prevent external mutation
|
|
31
|
-
const buffer = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
|
32
|
-
this.storage.set(key, buffer.slice(0));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async append(key: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
36
|
-
const appendData = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
37
|
-
|
|
38
|
-
const existing = this.storage.get(key);
|
|
39
|
-
if (existing) {
|
|
40
|
-
const existingArray = new Uint8Array(existing);
|
|
41
|
-
const combined = new Uint8Array(existingArray.length + appendData.length);
|
|
42
|
-
combined.set(existingArray, 0);
|
|
43
|
-
combined.set(appendData, existingArray.length);
|
|
44
|
-
this.storage.set(key, combined.buffer.slice(0) as ArrayBuffer);
|
|
45
|
-
} else {
|
|
46
|
-
this.storage.set(key, appendData.buffer.slice(appendData.byteOffset, appendData.byteOffset + appendData.byteLength) as ArrayBuffer);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async delete(key: string): Promise<void> {
|
|
51
|
-
this.storage.delete(key);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async exists(key: string): Promise<boolean> {
|
|
55
|
-
return this.storage.has(key);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async list(prefix?: string): Promise<string[]> {
|
|
59
|
-
const keys: string[] = [];
|
|
60
|
-
// Match BunStorageBackend/OPFSBackend semantics: prefix is a directory path
|
|
61
|
-
const dirPrefix = prefix ? (prefix.endsWith('/') ? prefix : prefix + '/') : '';
|
|
62
|
-
for (const key of this.storage.keys()) {
|
|
63
|
-
if (!dirPrefix || key.startsWith(dirPrefix)) {
|
|
64
|
-
keys.push(key);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return keys;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async mkdir(path: string): Promise<void> {
|
|
71
|
-
this.directories.add(path);
|
|
72
|
-
// Also add parent directories
|
|
73
|
-
const parts = path.split('/');
|
|
74
|
-
for (let i = 1; i <= parts.length; i++) {
|
|
75
|
-
this.directories.add(parts.slice(0, i).join('/'));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Clear all data
|
|
81
|
-
*/
|
|
82
|
-
clear(): void {
|
|
83
|
-
this.storage.clear();
|
|
84
|
-
this.directories.clear();
|
|
85
|
-
this.directories.add('');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get memory usage statistics
|
|
90
|
-
*/
|
|
91
|
-
getStats(): { keyCount: number; totalBytes: number } {
|
|
92
|
-
let totalBytes = 0;
|
|
93
|
-
for (const value of this.storage.values()) {
|
|
94
|
-
totalBytes += value.byteLength;
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
keyCount: this.storage.size,
|
|
98
|
-
totalBytes,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Export all data as a serializable object
|
|
104
|
-
* Useful for debugging or saving state
|
|
105
|
-
*/
|
|
106
|
-
export(): Record<string, ArrayBuffer> {
|
|
107
|
-
const result: Record<string, ArrayBuffer> = {};
|
|
108
|
-
for (const [key, value] of this.storage.entries()) {
|
|
109
|
-
result[key] = value.slice(0);
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Import data from a serializable object
|
|
116
|
-
*/
|
|
117
|
-
import(data: Record<string, ArrayBuffer>): void {
|
|
118
|
-
for (const [key, value] of Object.entries(data)) {
|
|
119
|
-
this.storage.set(key, value.slice(0));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Origin Private File System (OPFS) Storage Backend
|
|
3
|
-
*
|
|
4
|
-
* High-performance browser storage using the File System Access API.
|
|
5
|
-
* OPFS provides file-like semantics with significantly better performance
|
|
6
|
-
* than IndexedDB for large binary data (up to 10x faster).
|
|
7
|
-
*
|
|
8
|
-
* Features:
|
|
9
|
-
* - Auto-initializing (no manual init() required)
|
|
10
|
-
* - High performance for large binary data
|
|
11
|
-
*
|
|
12
|
-
* Browser Support:
|
|
13
|
-
* - Chrome 86+
|
|
14
|
-
* - Safari 15.2+
|
|
15
|
-
* - Firefox 111+
|
|
16
|
-
* - Edge 86+
|
|
17
|
-
*
|
|
18
|
-
* Note: This module is designed for browser environments only.
|
|
19
|
-
* It will not work in Node.js/Bun server environments.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import type { StorageBackend } from './StorageBackend';
|
|
23
|
-
|
|
24
|
-
export class OPFSBackend implements StorageBackend {
|
|
25
|
-
readonly type = 'opfs';
|
|
26
|
-
private root: FileSystemDirectoryHandle | null = null;
|
|
27
|
-
private initialized: boolean = false;
|
|
28
|
-
private initPromise: Promise<void> | null = null;
|
|
29
|
-
private appendLocks: Map<string, Promise<void>> = new Map();
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Initialize the OPFS backend (optional - auto-initializes on first operation)
|
|
33
|
-
* @deprecated No longer required - operations auto-initialize
|
|
34
|
-
*/
|
|
35
|
-
async init(): Promise<void> {
|
|
36
|
-
return this.doInit();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private async doInit(): Promise<void> {
|
|
40
|
-
if (this.initialized) return;
|
|
41
|
-
|
|
42
|
-
if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory) {
|
|
43
|
-
throw new Error('OPFS not available in this environment. Use MemoryBackend or IndexedDBBackend instead.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
this.root = await navigator.storage.getDirectory();
|
|
47
|
-
this.initialized = true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Ensure backend is initialized (lazy init on first use).
|
|
52
|
-
* Safe to call concurrently — only runs initialization once.
|
|
53
|
-
*/
|
|
54
|
-
private async ensureInitialized(): Promise<void> {
|
|
55
|
-
if (this.initialized) return;
|
|
56
|
-
if (!this.initPromise) {
|
|
57
|
-
this.initPromise = this.doInit().catch((err) => {
|
|
58
|
-
// Allow retry after transient initialization failures
|
|
59
|
-
this.initPromise = null;
|
|
60
|
-
throw err;
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
return this.initPromise;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private isNotFoundError(err: unknown): boolean {
|
|
67
|
-
return (
|
|
68
|
-
!!err &&
|
|
69
|
-
typeof err === 'object' &&
|
|
70
|
-
'name' in err &&
|
|
71
|
-
(err as any).name === 'NotFoundError'
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private formatError(err: unknown): string {
|
|
76
|
-
if (err instanceof Error) {
|
|
77
|
-
return err.message;
|
|
78
|
-
}
|
|
79
|
-
return String(err);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private validateAndSplitPath(path: string, allowEmpty: boolean): string[] {
|
|
83
|
-
if (typeof path !== 'string') {
|
|
84
|
-
throw new Error('Path must be a string');
|
|
85
|
-
}
|
|
86
|
-
if (path.includes('\0')) {
|
|
87
|
-
throw new Error(`Invalid path '${path}': contains null byte`);
|
|
88
|
-
}
|
|
89
|
-
if (path.length === 0) {
|
|
90
|
-
if (allowEmpty) return [];
|
|
91
|
-
throw new Error('Invalid path: empty path is not allowed');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const parts: string[] = [];
|
|
95
|
-
for (const part of path.split('/')) {
|
|
96
|
-
if (part === '') continue;
|
|
97
|
-
if (part === '.' || part === '..') {
|
|
98
|
-
throw new Error(`Path traversal detected: '${path}' contains '${part}' segment`);
|
|
99
|
-
}
|
|
100
|
-
if (part.includes('\\')) {
|
|
101
|
-
throw new Error(`Invalid path '${path}': backslashes are not allowed`);
|
|
102
|
-
}
|
|
103
|
-
parts.push(part);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!allowEmpty && parts.length === 0) {
|
|
107
|
-
throw new Error(`Invalid path '${path}': no file name`);
|
|
108
|
-
}
|
|
109
|
-
return parts;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get a file handle, creating parent directories as needed
|
|
114
|
-
*/
|
|
115
|
-
private async getFileHandle(key: string, create: boolean = false): Promise<FileSystemFileHandle | null> {
|
|
116
|
-
await this.ensureInitialized();
|
|
117
|
-
|
|
118
|
-
const parts = this.validateAndSplitPath(key, false);
|
|
119
|
-
const fileName = parts.pop()!;
|
|
120
|
-
|
|
121
|
-
// Navigate/create directories
|
|
122
|
-
let currentDir = this.root!;
|
|
123
|
-
for (const part of parts) {
|
|
124
|
-
try {
|
|
125
|
-
currentDir = await currentDir.getDirectoryHandle(part, { create });
|
|
126
|
-
} catch (err) {
|
|
127
|
-
if (!create && this.isNotFoundError(err)) return null;
|
|
128
|
-
throw new Error(`Failed to access directory '${part}' for '${key}': ${this.formatError(err)}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Get file handle
|
|
133
|
-
try {
|
|
134
|
-
return await currentDir.getFileHandle(fileName, { create });
|
|
135
|
-
} catch (err) {
|
|
136
|
-
if (!create && this.isNotFoundError(err)) return null;
|
|
137
|
-
throw new Error(`Failed to access file '${key}': ${this.formatError(err)}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get a directory handle, creating if needed
|
|
143
|
-
*/
|
|
144
|
-
private async getDirectoryHandle(path: string, create: boolean = false): Promise<FileSystemDirectoryHandle | null> {
|
|
145
|
-
await this.ensureInitialized();
|
|
146
|
-
|
|
147
|
-
const parts = this.validateAndSplitPath(path, true);
|
|
148
|
-
|
|
149
|
-
let currentDir = this.root!;
|
|
150
|
-
for (const part of parts) {
|
|
151
|
-
try {
|
|
152
|
-
currentDir = await currentDir.getDirectoryHandle(part, { create });
|
|
153
|
-
} catch (err) {
|
|
154
|
-
if (!create && this.isNotFoundError(err)) return null;
|
|
155
|
-
throw new Error(`Failed to access directory '${path}': ${this.formatError(err)}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return currentDir;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async read(key: string): Promise<ArrayBuffer | null> {
|
|
163
|
-
await this.ensureInitialized();
|
|
164
|
-
|
|
165
|
-
const handle = await this.getFileHandle(key, false);
|
|
166
|
-
if (!handle) return null;
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const file = await handle.getFile();
|
|
170
|
-
return file.arrayBuffer();
|
|
171
|
-
} catch (err) {
|
|
172
|
-
if (this.isNotFoundError(err)) return null;
|
|
173
|
-
throw new Error(`Failed to read file '${key}': ${this.formatError(err)}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async write(key: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
178
|
-
await this.ensureInitialized();
|
|
179
|
-
|
|
180
|
-
const handle = await this.getFileHandle(key, true);
|
|
181
|
-
if (!handle) {
|
|
182
|
-
throw new Error(`Failed to create file: ${key}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const writable = await handle.createWritable();
|
|
186
|
-
try {
|
|
187
|
-
// Convert to ArrayBuffer for FileSystemWriteChunkType compatibility
|
|
188
|
-
const writeData = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
|
189
|
-
await writable.write(writeData);
|
|
190
|
-
await writable.close();
|
|
191
|
-
} catch (err) {
|
|
192
|
-
// Abort on error to discard the temp file — close() would commit partial data
|
|
193
|
-
try { await writable.abort(); } catch { /* abort best-effort */ }
|
|
194
|
-
throw err;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async append(key: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
199
|
-
// Serialize appends per-key to prevent TOCTOU race condition
|
|
200
|
-
// (concurrent appends reading the same file size would overwrite each other)
|
|
201
|
-
const prev = this.appendLocks.get(key) ?? Promise.resolve();
|
|
202
|
-
let resolve: () => void;
|
|
203
|
-
const lock = new Promise<void>(r => { resolve = r; });
|
|
204
|
-
this.appendLocks.set(key, lock);
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
await prev;
|
|
208
|
-
await this.doAppend(key, data);
|
|
209
|
-
} finally {
|
|
210
|
-
resolve!();
|
|
211
|
-
if (this.appendLocks.get(key) === lock) {
|
|
212
|
-
this.appendLocks.delete(key);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private async doAppend(key: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
218
|
-
await this.ensureInitialized();
|
|
219
|
-
|
|
220
|
-
const handle = await this.getFileHandle(key, true);
|
|
221
|
-
if (!handle) {
|
|
222
|
-
throw new Error(`Failed to create file: ${key}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const appendData = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
226
|
-
|
|
227
|
-
// Get current file size to seek to end
|
|
228
|
-
let existingSize = 0;
|
|
229
|
-
try {
|
|
230
|
-
const file = await handle.getFile();
|
|
231
|
-
existingSize = file.size;
|
|
232
|
-
} catch {
|
|
233
|
-
existingSize = 0;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Use keepExistingData: true for O(1) append (no full read required)
|
|
237
|
-
// Then seek to end and write only new data
|
|
238
|
-
const writable = await handle.createWritable({ keepExistingData: true });
|
|
239
|
-
try {
|
|
240
|
-
// Seek to end of file
|
|
241
|
-
await writable.seek(existingSize);
|
|
242
|
-
// Write only the new data - O(data.length) not O(existingSize + data.length)
|
|
243
|
-
// Convert to ArrayBuffer for FileSystemWriteChunkType compatibility
|
|
244
|
-
const writeData = appendData.buffer.slice(appendData.byteOffset, appendData.byteOffset + appendData.byteLength) as ArrayBuffer;
|
|
245
|
-
await writable.write(writeData);
|
|
246
|
-
await writable.close();
|
|
247
|
-
} catch (err) {
|
|
248
|
-
// Abort on error to discard the temp file — close() would commit partial/corrupt data
|
|
249
|
-
try { await writable.abort(); } catch { /* abort best-effort */ }
|
|
250
|
-
throw err;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async delete(key: string): Promise<void> {
|
|
255
|
-
await this.ensureInitialized();
|
|
256
|
-
|
|
257
|
-
const parts = this.validateAndSplitPath(key, false);
|
|
258
|
-
const fileName = parts.pop()!;
|
|
259
|
-
|
|
260
|
-
// Navigate to parent directory
|
|
261
|
-
let currentDir = this.root!;
|
|
262
|
-
for (const part of parts) {
|
|
263
|
-
try {
|
|
264
|
-
currentDir = await currentDir.getDirectoryHandle(part, { create: false });
|
|
265
|
-
} catch (err) {
|
|
266
|
-
if (this.isNotFoundError(err)) {
|
|
267
|
-
return; // Parent doesn't exist, nothing to delete
|
|
268
|
-
}
|
|
269
|
-
throw new Error(`Failed to access directory for '${key}': ${this.formatError(err)}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Remove the file
|
|
274
|
-
try {
|
|
275
|
-
await currentDir.removeEntry(fileName);
|
|
276
|
-
} catch (err) {
|
|
277
|
-
if (this.isNotFoundError(err)) {
|
|
278
|
-
return; // Parent doesn't exist, nothing to delete
|
|
279
|
-
}
|
|
280
|
-
throw new Error(`Failed to delete '${key}': ${this.formatError(err)}`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async exists(key: string): Promise<boolean> {
|
|
285
|
-
await this.ensureInitialized();
|
|
286
|
-
|
|
287
|
-
const handle = await this.getFileHandle(key, false);
|
|
288
|
-
return handle !== null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async list(prefix?: string): Promise<string[]> {
|
|
292
|
-
await this.ensureInitialized();
|
|
293
|
-
|
|
294
|
-
const results: string[] = [];
|
|
295
|
-
const basePath = prefix ?? '';
|
|
296
|
-
if (basePath.length > 0) {
|
|
297
|
-
this.validateAndSplitPath(basePath, true);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const dir = basePath ? await this.getDirectoryHandle(basePath, false) : this.root!;
|
|
301
|
-
if (!dir) return results;
|
|
302
|
-
|
|
303
|
-
// Recursive list
|
|
304
|
-
const listDir = async (dirHandle: FileSystemDirectoryHandle, pathPrefix: string): Promise<void> => {
|
|
305
|
-
for await (const entry of (dirHandle as any).values()) {
|
|
306
|
-
const entryPath = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name;
|
|
307
|
-
|
|
308
|
-
if (entry.kind === 'file') {
|
|
309
|
-
results.push(entryPath);
|
|
310
|
-
} else if (entry.kind === 'directory') {
|
|
311
|
-
await listDir(entry, entryPath);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
await listDir(dir, basePath);
|
|
317
|
-
return results;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async mkdir(path: string): Promise<void> {
|
|
321
|
-
await this.ensureInitialized();
|
|
322
|
-
this.validateAndSplitPath(path, true);
|
|
323
|
-
await this.getDirectoryHandle(path, true);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Clear all data in OPFS
|
|
328
|
-
*/
|
|
329
|
-
async clear(): Promise<void> {
|
|
330
|
-
await this.ensureInitialized();
|
|
331
|
-
|
|
332
|
-
// Collect entry names first to avoid modifying directory during iteration
|
|
333
|
-
const names: string[] = [];
|
|
334
|
-
for await (const entry of (this.root as any).values()) {
|
|
335
|
-
names.push(entry.name);
|
|
336
|
-
}
|
|
337
|
-
for (const name of names) {
|
|
338
|
-
await this.root!.removeEntry(name, { recursive: true });
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Check if OPFS is available in the current environment
|
|
344
|
-
*/
|
|
345
|
-
static isAvailable(): boolean {
|
|
346
|
-
return typeof navigator !== 'undefined' && !!navigator.storage?.getDirectory;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage Backend Interface
|
|
3
|
-
*
|
|
4
|
-
* Provides a unified interface for persisting vector index data across different platforms:
|
|
5
|
-
* - Bun/Node.js: File system storage via BunStorageBackend
|
|
6
|
-
* - Browser: OPFS (Origin Private File System) or IndexedDB
|
|
7
|
-
* - Testing: In-memory storage
|
|
8
|
-
*
|
|
9
|
-
* All operations are async to support both sync and async underlying storage.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface StorageBackend {
|
|
13
|
-
/**
|
|
14
|
-
* Read data from storage
|
|
15
|
-
* @param key Unique key/path for the data
|
|
16
|
-
* @returns ArrayBuffer of data or null if not found
|
|
17
|
-
*/
|
|
18
|
-
read(key: string): Promise<ArrayBuffer | null>;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Write data to storage (overwrites existing)
|
|
22
|
-
* @param key Unique key/path for the data
|
|
23
|
-
* @param data Data to write
|
|
24
|
-
*/
|
|
25
|
-
write(key: string, data: ArrayBuffer | Uint8Array): Promise<void>;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Append data to existing file (for WAL)
|
|
29
|
-
* @param key Unique key/path for the data
|
|
30
|
-
* @param data Data to append
|
|
31
|
-
*/
|
|
32
|
-
append(key: string, data: ArrayBuffer | Uint8Array): Promise<void>;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Delete data from storage
|
|
36
|
-
* @param key Unique key/path to delete
|
|
37
|
-
*/
|
|
38
|
-
delete(key: string): Promise<void>;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Check if key exists in storage
|
|
42
|
-
* @param key Unique key/path to check
|
|
43
|
-
*/
|
|
44
|
-
exists(key: string): Promise<boolean>;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* List all keys with given prefix
|
|
48
|
-
* @param prefix Optional prefix to filter keys
|
|
49
|
-
*/
|
|
50
|
-
list(prefix?: string): Promise<string[]>;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create a directory (for file-based backends)
|
|
54
|
-
* @param path Directory path to create
|
|
55
|
-
*/
|
|
56
|
-
mkdir(path: string): Promise<void>;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get storage type identifier
|
|
60
|
-
*/
|
|
61
|
-
readonly type: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Options for creating a storage backend
|
|
66
|
-
*/
|
|
67
|
-
export interface StorageOptions {
|
|
68
|
-
/** Base path for file-based storage */
|
|
69
|
-
path?: string;
|
|
70
|
-
/** Database name for IndexedDB */
|
|
71
|
-
dbName?: string;
|
|
72
|
-
/** Store name for IndexedDB */
|
|
73
|
-
storeName?: string;
|
|
74
|
-
}
|