@rljson/bs 0.0.18 → 0.0.19

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.
@@ -8,8 +8,480 @@ found in the LICENSE file in the root of this package.
8
8
 
9
9
  # @rljson/bs
10
10
 
11
- Blob storage interface and implementations for rljson.
11
+ Content-addressable blob storage interface and implementations for rljson.
12
12
 
13
- ## Example
13
+ ## Overview
14
14
 
15
- [src/example.ts](src/example.ts)
15
+ `@rljson/bs` provides a unified interface for blob storage with content-addressable semantics. All blobs are identified by their SHA256 hash, ensuring automatic deduplication and data integrity.
16
+
17
+ ### Key Features
18
+
19
+ - **Content-Addressable Storage**: Blobs are identified by SHA256 hash of their content
20
+ - **Automatic Deduplication**: Identical content is stored only once
21
+ - **Multiple Implementations**: In-memory, peer-to-peer, server-based, and multi-tier
22
+ - **Type-Safe**: Full TypeScript support with comprehensive type definitions
23
+ - **Stream Support**: Efficient handling of large blobs via ReadableStreams
24
+ - **100% Test Coverage**: Fully tested with comprehensive test suite
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install @rljson/bs
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### In-Memory Storage
35
+
36
+ The simplest implementation for testing or temporary storage:
37
+
38
+ ```typescript
39
+ import { BsMem } from '@rljson/bs';
40
+
41
+ // Create an in-memory blob storage
42
+ const bs = new BsMem();
43
+
44
+ // Store a blob - returns SHA256 hash as blobId
45
+ const { blobId } = await bs.setBlob('Hello, World!');
46
+ console.log(blobId); // e.g., "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"
47
+
48
+ // Retrieve the blob
49
+ const { content } = await bs.getBlob(blobId);
50
+ console.log(content.toString()); // "Hello, World!"
51
+
52
+ // Check if blob exists
53
+ const exists = await bs.blobExists(blobId);
54
+ console.log(exists); // true
55
+
56
+ // List all blobs
57
+ const { blobs } = await bs.listBlobs();
58
+ console.log(blobs.length); // 1
59
+ ```
60
+
61
+ ### Multi-Tier Storage (Cache + Remote)
62
+
63
+ Combine multiple storage backends with automatic caching:
64
+
65
+ ```typescript
66
+ import { BsMulti, BsMem, BsPeer, PeerSocketMock } from '@rljson/bs';
67
+
68
+ // Setup remote storage (simulated)
69
+ const remoteStore = new BsMem();
70
+ const remoteSocket = new PeerSocketMock(remoteStore);
71
+ const remotePeer = new BsPeer(remoteSocket);
72
+ await remotePeer.init();
73
+
74
+ // Setup local cache
75
+ const localCache = new BsMem();
76
+
77
+ // Create multi-tier storage with cache-first strategy
78
+ const bs = new BsMulti([
79
+ { bs: localCache, priority: 0, read: true, write: true }, // Cache first
80
+ { bs: remotePeer, priority: 1, read: true, write: false }, // Remote fallback
81
+ ]);
82
+ await bs.init();
83
+
84
+ // Store blob - writes to cache only (writable stores)
85
+ const { blobId } = await bs.setBlob('Cached content');
86
+
87
+ // Read from cache first, falls back to remote
88
+ // Automatically hot-swaps remote blobs to cache
89
+ const { content } = await bs.getBlob(blobId);
90
+ ```
91
+
92
+ ## Core Concepts
93
+
94
+ ### Content-Addressable Storage
95
+
96
+ Every blob is identified by the SHA256 hash of its content. This means:
97
+
98
+ - **Automatic Deduplication**: Storing the same content twice returns the same `blobId`
99
+ - **Data Integrity**: The `blobId` serves as a cryptographic checksum
100
+ - **Location Independence**: Blobs can be identified and verified anywhere
101
+
102
+ ```typescript
103
+ const bs = new BsMem();
104
+
105
+ const result1 = await bs.setBlob('Same content');
106
+ const result2 = await bs.setBlob('Same content');
107
+
108
+ // Both return the same blobId
109
+ console.log(result1.blobId === result2.blobId); // true
110
+ ```
111
+
112
+ ### Blob Properties
113
+
114
+ All blobs have associated metadata:
115
+
116
+ ```typescript
117
+ interface BlobProperties {
118
+ blobId: string; // SHA256 hash of content
119
+ size: number; // Size in bytes
120
+ contentType: string; // MIME type (default: 'application/octet-stream')
121
+ createdAt: Date; // Creation timestamp
122
+ metadata?: Record<string, string>; // Optional custom metadata
123
+ }
124
+ ```
125
+
126
+ ## Implementations
127
+
128
+ ### BsMem - In-Memory Storage
129
+
130
+ Fast, ephemeral storage for testing and temporary data:
131
+
132
+ ```typescript
133
+ import { BsMem } from '@rljson/bs';
134
+
135
+ const bs = new BsMem();
136
+ const { blobId } = await bs.setBlob('Temporary data');
137
+ ```
138
+
139
+ **Use Cases:**
140
+
141
+ - Unit testing
142
+ - Temporary caching
143
+ - Development and prototyping
144
+
145
+ **Limitations:**
146
+
147
+ - Data lost when process ends
148
+ - Limited by available RAM
149
+
150
+ ### BsPeer - Peer-to-Peer Storage
151
+
152
+ Access remote blob storage over a socket connection:
153
+
154
+ ```typescript
155
+ import { BsPeer, PeerSocketMock } from '@rljson/bs';
156
+
157
+ // Create a peer connected to a remote storage
158
+ const remoteStorage = new BsMem();
159
+ const socket = new PeerSocketMock(remoteStorage);
160
+ const peer = new BsPeer(socket);
161
+ await peer.init();
162
+
163
+ // Use like any other Bs implementation
164
+ const { blobId } = await peer.setBlob('Remote data');
165
+ const { content } = await peer.getBlob(blobId);
166
+
167
+ // Close connection when done
168
+ await peer.close();
169
+ ```
170
+
171
+ **Use Cases:**
172
+
173
+ - Distributed systems
174
+ - Client-server architectures
175
+ - Remote backup
176
+
177
+ ### BsServer - Server-Side Handler
178
+
179
+ Handle blob storage requests from remote peers:
180
+
181
+ ```typescript
182
+ import { BsServer, BsMem, SocketMock } from '@rljson/bs';
183
+
184
+ // Server-side setup
185
+ const storage = new BsMem();
186
+ const server = new BsServer(storage);
187
+
188
+ // Handle incoming connection
189
+ const clientSocket = new SocketMock();
190
+ const serverSocket = clientSocket.createPeer();
191
+ server.handleConnection(serverSocket);
192
+
193
+ // Client can now access storage through clientSocket
194
+ ```
195
+
196
+ **Use Cases:**
197
+
198
+ - Building blob storage services
199
+ - Network protocol implementation
200
+ - API backends
201
+
202
+ ### BsMulti - Multi-Tier Storage
203
+
204
+ Combine multiple storage backends with configurable priorities:
205
+
206
+ ```typescript
207
+ import { BsMulti, BsMem } from '@rljson/bs';
208
+
209
+ const fastCache = new BsMem();
210
+ const mainStorage = new BsMem();
211
+ const backup = new BsMem();
212
+
213
+ const bs = new BsMulti([
214
+ { bs: fastCache, priority: 0, read: true, write: true }, // L1 cache
215
+ { bs: mainStorage, priority: 1, read: true, write: true }, // Main storage
216
+ { bs: backup, priority: 2, read: true, write: false }, // Read-only backup
217
+ ]);
218
+ await bs.init();
219
+ ```
220
+
221
+ **Features:**
222
+
223
+ - **Priority-Based Reads**: Reads from lowest priority number first
224
+ - **Hot-Swapping**: Automatically caches blobs from remote to local
225
+ - **Parallel Writes**: Writes to all writable stores simultaneously
226
+ - **Deduplication**: Merges results from all readable stores
227
+
228
+ **Use Cases:**
229
+
230
+ - Local cache + remote storage
231
+ - Local network storage infrastructure
232
+ - Backup and archival systems
233
+ - Distributed blob storage across network nodes
234
+
235
+ ## API Reference
236
+
237
+ ### Bs Interface
238
+
239
+ All implementations conform to the `Bs` interface:
240
+
241
+ #### `setBlob(content: Buffer | string | ReadableStream): Promise<BlobProperties>`
242
+
243
+ Stores a blob and returns its properties including the SHA256 `blobId`.
244
+
245
+ ```typescript
246
+ // From string
247
+ const { blobId } = await bs.setBlob('Hello');
248
+
249
+ // From Buffer
250
+ const buffer = Buffer.from('World');
251
+ await bs.setBlob(buffer);
252
+
253
+ // From ReadableStream
254
+ const stream = new ReadableStream({
255
+ start(controller) {
256
+ controller.enqueue(new TextEncoder().encode('Stream data'));
257
+ controller.close();
258
+ }
259
+ });
260
+ await bs.setBlob(stream);
261
+ ```
262
+
263
+ #### `getBlob(blobId: string, options?: DownloadBlobOptions): Promise<{ content: Buffer; properties: BlobProperties }>`
264
+
265
+ Retrieves a blob by its ID.
266
+
267
+ ```typescript
268
+ const { content, properties } = await bs.getBlob(blobId);
269
+ console.log(content.toString());
270
+ console.log(properties.size);
271
+
272
+ // With range request (partial content)
273
+ const { content: partial } = await bs.getBlob(blobId, {
274
+ range: { start: 0, end: 99 } // First 100 bytes
275
+ });
276
+ ```
277
+
278
+ #### `getBlobStream(blobId: string): Promise<ReadableStream<Uint8Array>>`
279
+
280
+ Retrieves a blob as a stream for efficient handling of large files.
281
+
282
+ ```typescript
283
+ const stream = await bs.getBlobStream(blobId);
284
+ const reader = stream.getReader();
285
+
286
+ while (true) {
287
+ const { done, value } = await reader.read();
288
+ if (done) break;
289
+ // Process chunk
290
+ console.log('Chunk size:', value.length);
291
+ }
292
+ ```
293
+
294
+ #### `deleteBlob(blobId: string): Promise<void>`
295
+
296
+ Deletes a blob from storage.
297
+
298
+ ```typescript
299
+ await bs.deleteBlob(blobId);
300
+ ```
301
+
302
+ **Note:** In production systems with content-addressable storage, consider implementing reference counting before deletion.
303
+
304
+ #### `blobExists(blobId: string): Promise<boolean>`
305
+
306
+ Checks if a blob exists.
307
+
308
+ ```typescript
309
+ if (await bs.blobExists(blobId)) {
310
+ console.log('Blob found');
311
+ }
312
+ ```
313
+
314
+ #### `getBlobProperties(blobId: string): Promise<BlobProperties>`
315
+
316
+ Gets blob metadata without downloading content.
317
+
318
+ ```typescript
319
+ const props = await bs.getBlobProperties(blobId);
320
+ console.log(`Blob size: ${props.size} bytes`);
321
+ console.log(`Created: ${props.createdAt}`);
322
+ ```
323
+
324
+ #### `listBlobs(options?: ListBlobsOptions): Promise<ListBlobsResult>`
325
+
326
+ Lists all blobs with optional filtering and pagination.
327
+
328
+ ```typescript
329
+ // List all blobs
330
+ const { blobs } = await bs.listBlobs();
331
+
332
+ // With prefix filter
333
+ const result = await bs.listBlobs({ prefix: 'abc' });
334
+
335
+ // Paginated listing
336
+ let continuationToken: string | undefined;
337
+ do {
338
+ const result = await bs.listBlobs({
339
+ maxResults: 100,
340
+ continuationToken
341
+ });
342
+
343
+ console.log(`Got ${result.blobs.length} blobs`);
344
+ continuationToken = result.continuationToken;
345
+ } while (continuationToken);
346
+ ```
347
+
348
+ #### `generateSignedUrl(blobId: string, expiresIn: number, permissions?: 'read' | 'delete'): Promise<string>`
349
+
350
+ Generates a signed URL for temporary access to a blob.
351
+
352
+ ```typescript
353
+ // Read-only URL valid for 1 hour
354
+ const url = await bs.generateSignedUrl(blobId, 3600);
355
+
356
+ // Delete permission URL
357
+ const deleteUrl = await bs.generateSignedUrl(blobId, 300, 'delete');
358
+ ```
359
+
360
+ ## Advanced Usage
361
+
362
+ ### Custom Storage Implementation
363
+
364
+ Implement the `Bs` interface to create custom storage backends:
365
+
366
+ ```typescript
367
+ import { Bs, BlobProperties } from '@rljson/bs';
368
+
369
+ class MyCustomStorage implements Bs {
370
+ async setBlob(content: Buffer | string | ReadableStream): Promise<BlobProperties> {
371
+ // Your implementation
372
+ }
373
+
374
+ async getBlob(blobId: string) {
375
+ // Your implementation
376
+ }
377
+
378
+ // Implement other methods...
379
+ }
380
+ ```
381
+
382
+ ### Multi-Tier Patterns
383
+
384
+ **Local Cache + Remote Storage:**
385
+
386
+ ```typescript
387
+ const bs = new BsMulti([
388
+ { bs: localCache, priority: 0, read: true, write: true },
389
+ { bs: remoteStorage, priority: 1, read: true, write: false },
390
+ ]);
391
+ ```
392
+
393
+ **Write-Through Cache:**
394
+
395
+ ```typescript
396
+ const bs = new BsMulti([
397
+ { bs: localCache, priority: 0, read: true, write: true },
398
+ { bs: remoteStorage, priority: 1, read: true, write: true }, // Also writable
399
+ ]);
400
+ ```
401
+
402
+ **Multi-Region Replication:**
403
+
404
+ ```typescript
405
+ const bs = new BsMulti([
406
+ { bs: regionUs, priority: 0, read: true, write: true },
407
+ { bs: regionEu, priority: 1, read: true, write: true },
408
+ { bs: regionAsia, priority: 2, read: true, write: true },
409
+ ]);
410
+ ```
411
+
412
+ ### Error Handling
413
+
414
+ All methods throw errors for invalid operations:
415
+
416
+ ```typescript
417
+ try {
418
+ await bs.getBlob('nonexistent-id');
419
+ } catch (error) {
420
+ console.error('Blob not found:', error.message);
421
+ }
422
+
423
+ // BsMulti gracefully handles partial failures
424
+ const multi = new BsMulti([
425
+ { bs: failingStore, priority: 0, read: true, write: false },
426
+ { bs: workingStore, priority: 1, read: true, write: false },
427
+ ]);
428
+
429
+ // Falls back to workingStore if failingStore errors
430
+ const { content } = await multi.getBlob(blobId);
431
+ ```
432
+
433
+ ## Testing
434
+
435
+ The package includes comprehensive test utilities:
436
+
437
+ ```typescript
438
+ import { BsMem } from '@rljson/bs';
439
+
440
+ describe('My Tests', () => {
441
+ let bs: BsMem;
442
+
443
+ beforeEach(() => {
444
+ bs = new BsMem();
445
+ });
446
+
447
+ it('should store and retrieve blobs', async () => {
448
+ const { blobId } = await bs.setBlob('test data');
449
+ const { content } = await bs.getBlob(blobId);
450
+ expect(content.toString()).toBe('test data');
451
+ });
452
+ });
453
+ ```
454
+
455
+ ## Performance Considerations
456
+
457
+ ### Memory Usage
458
+
459
+ - `BsMem` stores all data in RAM - suitable for small to medium datasets
460
+ - Use streams (`getBlobStream`) for large blobs to avoid loading entire content into memory
461
+ - `BsMulti` with local cache reduces network overhead
462
+
463
+ ### Network Efficiency
464
+
465
+ - Use `BsPeer` for remote access with minimal protocol overhead
466
+ - `BsMulti` automatically caches frequently accessed blobs
467
+ - Content-addressable nature prevents redundant transfers
468
+
469
+ ### Deduplication
470
+
471
+ - Identical content stored multiple times occupies space only once
472
+ - Particularly effective for:
473
+ - Version control systems
474
+ - Backup solutions
475
+ - Build artifact storage
476
+
477
+ ## License
478
+
479
+ MIT
480
+
481
+ ## Contributing
482
+
483
+ Issues and pull requests welcome at <https://github.com/rljson/bs>
484
+
485
+ ## Related Packages
486
+
487
+ - `@rljson/io` - Data table storage interface and implementations
@@ -0,0 +1,96 @@
1
+ import { BlobProperties, Bs, DownloadBlobOptions, ListBlobsOptions, ListBlobsResult } from './bs.js';
2
+ /**
3
+ * Type representing a Bs instance along with its capabilities and priority.
4
+ */
5
+ export type BsMultiBs = {
6
+ bs: Bs;
7
+ id?: string;
8
+ priority: number;
9
+ read: boolean;
10
+ write: boolean;
11
+ };
12
+ /**
13
+ * Multi-tier Bs implementation that combines multiple underlying Bs instances
14
+ * with different capabilities (read, write) and priorities.
15
+ *
16
+ * Pattern: Local cache + remote server fallback
17
+ * - Lower priority number = checked first
18
+ * - Reads from highest priority readable, with hot-swapping to cache
19
+ * - Writes to all writable instances in parallel
20
+ */
21
+ export declare class BsMulti implements Bs {
22
+ private _stores;
23
+ constructor(_stores: Array<BsMultiBs>);
24
+ /**
25
+ * Initializes the BsMulti by assigning IDs to all underlying Bs instances.
26
+ * All underlying Bs instances must already be initialized.
27
+ */
28
+ init(): Promise<void>;
29
+ /**
30
+ * Stores a blob in all writable Bs instances in parallel.
31
+ * @param content - The blob content to store
32
+ * @returns Promise resolving to blob properties from the first successful write
33
+ */
34
+ setBlob(content: Buffer | string | ReadableStream<Uint8Array>): Promise<BlobProperties>;
35
+ /**
36
+ * Retrieves a blob from the highest priority readable Bs instance.
37
+ * Hot-swaps the blob to all writable instances for caching.
38
+ * @param blobId - The blob identifier
39
+ * @param options - Download options
40
+ * @returns Promise resolving to blob content and properties
41
+ */
42
+ getBlob(blobId: string, options?: DownloadBlobOptions): Promise<{
43
+ content: Buffer;
44
+ properties: BlobProperties;
45
+ }>;
46
+ /**
47
+ * Retrieves a blob as a ReadableStream from the highest priority readable Bs instance.
48
+ * @param blobId - The blob identifier
49
+ * @returns Promise resolving to a ReadableStream
50
+ */
51
+ getBlobStream(blobId: string): Promise<ReadableStream<Uint8Array>>;
52
+ /**
53
+ * Deletes a blob from all writable Bs instances in parallel.
54
+ * @param blobId - The blob identifier
55
+ */
56
+ deleteBlob(blobId: string): Promise<void>;
57
+ /**
58
+ * Checks if a blob exists in any readable Bs instance.
59
+ * @param blobId - The blob identifier
60
+ * @returns Promise resolving to true if blob exists in any readable
61
+ */
62
+ blobExists(blobId: string): Promise<boolean>;
63
+ /**
64
+ * Gets blob properties from the highest priority readable Bs instance.
65
+ * @param blobId - The blob identifier
66
+ * @returns Promise resolving to blob properties
67
+ */
68
+ getBlobProperties(blobId: string): Promise<BlobProperties>;
69
+ /**
70
+ * Lists blobs by merging results from all readable Bs instances.
71
+ * Deduplicates by blobId (content-addressable).
72
+ * @param options - Listing options
73
+ * @returns Promise resolving to list of blobs
74
+ */
75
+ listBlobs(options?: ListBlobsOptions): Promise<ListBlobsResult>;
76
+ /**
77
+ * Generates a signed URL from the highest priority readable Bs instance.
78
+ * @param blobId - The blob identifier
79
+ * @param expiresIn - Expiration time in seconds
80
+ * @param permissions - Access permissions
81
+ * @returns Promise resolving to signed URL
82
+ */
83
+ generateSignedUrl(blobId: string, expiresIn: number, permissions?: 'read' | 'delete'): Promise<string>;
84
+ /**
85
+ * Gets the list of underlying readable Bs instances, sorted by priority.
86
+ */
87
+ get readables(): Array<BsMultiBs>;
88
+ /**
89
+ * Gets the list of underlying writable Bs instances, sorted by priority.
90
+ */
91
+ get writables(): Array<BsMultiBs>;
92
+ /**
93
+ * Example: Local cache (BsMem) + Remote server (BsPeer)
94
+ */
95
+ static example: () => Promise<BsMulti>;
96
+ }
@@ -0,0 +1,70 @@
1
+ import { Bs } from './bs.js';
2
+ import { Socket } from './socket.js';
3
+ /**
4
+ * Bridges Socket events to Bs method calls.
5
+ *
6
+ * This class listens to socket events and translates them into corresponding
7
+ * Bs method calls, automatically registering all Bs interface methods.
8
+ */
9
+ export declare class BsPeerBridge {
10
+ private _bs;
11
+ private _socket;
12
+ private _eventHandlers;
13
+ constructor(_bs: Bs, _socket: Socket);
14
+ /**
15
+ * Starts the bridge by setting up connection event handlers and
16
+ * automatically registering all Bs methods.
17
+ */
18
+ start(): void;
19
+ /**
20
+ * Stops the bridge by removing all event handlers.
21
+ */
22
+ stop(): void;
23
+ /**
24
+ * Automatically registers all Bs interface methods as socket event handlers.
25
+ */
26
+ private _registerBsMethods;
27
+ /**
28
+ * Registers a socket event to be translated to a Bs method call.
29
+ * @param eventName - The socket event name (should match a Bs method name)
30
+ * @param bsMethodName - (Optional) The Bs method name if different from eventName
31
+ */
32
+ registerEvent(eventName: string, bsMethodName?: string): void;
33
+ /**
34
+ * Registers multiple socket events at once.
35
+ * @param eventNames - Array of event names to register
36
+ */
37
+ registerEvents(eventNames: string[]): void;
38
+ /**
39
+ * Unregisters a socket event handler.
40
+ * @param eventName - The event name to unregister
41
+ */
42
+ unregisterEvent(eventName: string | symbol): void;
43
+ /**
44
+ * Emits a result back through the socket.
45
+ * @param eventName - The event name to emit
46
+ * @param data - The data to send
47
+ */
48
+ emitToSocket(eventName: string | symbol, ...data: any[]): void;
49
+ /**
50
+ * Calls a Bs method directly and emits the result through the socket.
51
+ * @param bsMethodName - The Bs method to call
52
+ * @param socketEventName - The socket event to emit with the result
53
+ * @param args - Arguments to pass to the Bs method
54
+ */
55
+ callBsAndEmit(bsMethodName: string, socketEventName: string | symbol, ...args: any[]): Promise<void>;
56
+ private _handleConnect;
57
+ private _handleDisconnect;
58
+ /**
59
+ * Gets the current socket instance.
60
+ */
61
+ get socket(): Socket;
62
+ /**
63
+ * Gets the current Bs instance.
64
+ */
65
+ get bs(): Bs;
66
+ /**
67
+ * Returns whether the socket is currently connected.
68
+ */
69
+ get isConnected(): boolean;
70
+ }