@pixagram/lacerta-db 0.0.3 → 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/readme.md CHANGED
@@ -1,998 +1,207 @@
1
- # LacertaDB Technical Documentation
2
- *Version 1.0.0 - Architecturally Refined Implementation*
3
-
4
- ## Table of Contents
5
- 1. [Installation & Prerequisites](#installation--prerequisites)
6
- 2. [Architecture Overview](#architecture-overview)
7
- 3. [API Reference](#api-reference)
8
- 4. [Data Schemas](#data-schemas)
9
- 5. [Error Handling](#error-handling)
10
- 6. [Edge Cases & Behaviors](#edge-cases--behaviors)
11
- 7. [Performance Optimization](#performance-optimization)
12
- 8. [Security Model](#security-model)
13
- 9. [Code Examples](#code-examples)
14
-
15
- ---
16
-
17
- ## Installation & Prerequisites
18
-
19
- ### Critical Dependencies
20
- ```javascript
21
- // REQUIRED: JOYSON library for binary serialization
22
- npm install joyson
23
- // OR include via CDN
24
- <script src="https://cdn.jsdelivr.net/npm/joyson/dist/joyson.min.js"></script>
25
- ```
26
-
27
- ### Browser Requirements
28
- | API | Minimum Version | Required |
29
- |-----|-----------------|----------|
30
- | IndexedDB | Chrome 24+ | ✅ |
31
- | localStorage | All modern | ✅ |
32
- | Web Crypto API | Chrome 37+ | ✅ |
33
- | CompressionStream | Chrome 86+ | ✅ |
34
- | Origin Private File System | Chrome 86+ | ✅ |
35
-
36
- ### Module Import
37
- ```javascript
38
- import { Database, Document, Collection } from './lacertadb.js';
39
- ```
40
-
41
- ---
42
-
43
- ## Architecture Overview
44
-
45
- ### Storage Hierarchy
46
- ```
47
- ┌─────────────────────────────────────────┐
48
- │ Application Layer │
49
- └─────────────┬───────────────────────────┘
50
-
51
- ┌─────────────▼───────────────────────────┐
52
- │ Database Instance │
53
- │ • Collections Management │
54
- │ • Global Settings │
55
- │ • Metadata Coordination │
56
- └─────────────┬───────────────────────────┘
57
-
58
- ┌─────────────▼───────────────────────────┐
59
- │ Storage Layers │
60
- ├─────────────────────────────────────────┤
61
- │ IndexedDB │ Primary document storage │
62
- │ localStorage │ Metadata & settings │
63
- │ OPFS │ Binary attachments │
64
- │ QuickStore │ Sync localStorage cache │
65
- └─────────────────────────────────────────┘
66
- ```
67
-
68
- ### Transaction Flow
69
- ```
70
- User Request → TransactionManager → Queue → IndexedDB
71
-
72
- Metadata Update
73
-
74
- Observer Events
75
- ```
76
-
77
- ---
78
-
79
- ## API Reference
80
-
81
- ### Database Class
82
-
83
- #### Constructor
84
- ```javascript
85
- new Database(dbName: string, settings?: DatabaseSettings)
86
- ```
87
-
88
- **Parameters:**
89
- - `dbName` (string): Unique database identifier
90
- - `settings` (object, optional): Configuration object
91
-
92
- **Settings Schema:**
93
- ```typescript
94
- interface DatabaseSettings {
95
- sizeLimitKB?: number; // Max size before cleanup (default: Infinity)
96
- bufferLimitKB?: number; // Buffer before trigger (default: -20% of sizeLimitKB)
97
- freeSpaceEvery?: number; // Ms between cleanups (default: 10000, min: 1000)
98
- }
99
- ```
1
+ LacertaDB (@pixagram/lacerta-db)
2
+ LacertaDB is a Javascript IndexedDB Database for Web Browsers. Simple, Fast, Secure.
100
3
 
101
- #### Methods
4
+ LacertaDB offers a powerful, modern, and feature-rich interface for working with IndexedDB in the browser. It's designed as a document-oriented database, providing a developer-friendly API similar to MongoDB. It prioritizes security, performance, and advanced data handling capabilities right out of the box.
102
5
 
103
- ##### `async init(): Promise<void>`
104
- Initializes database connection and loads collections.
6
+ Key Features
7
+ Simple API: An intuitive and modern API that makes database operations straightforward.
105
8
 
106
- **Throws:** `LacertaDBError` if database cannot be opened
9
+ Strong Encryption: Built-in AES-GCM encryption to secure sensitive documents with a password.
107
10
 
108
- **Example:**
109
- ```javascript
110
- const db = new Database('myapp');
111
- await db.init();
112
- ```
11
+ Efficient Compression: Automatic data compression using the Compression Streams API to save storage space.
113
12
 
114
- ---
13
+ OPFS Attachments: Store large files like images or videos efficiently using the Origin Private File System (OPFS), linking them directly to your documents.
115
14
 
116
- ##### `async createCollection(collectionName: string): Promise<LacertaDBResult>`
117
- Creates a new collection or returns existing one.
15
+ Advanced Queries: A powerful MongoDB-like query engine with support for complex operators ($gt, $in, $regex, etc.) and logical combinations ($and, $or).
118
16
 
119
- **Returns:** `LacertaDBResult` object
120
- ```typescript
121
- {
122
- success: boolean,
123
- data: Collection | null,
124
- error: LacertaDBError | null
125
- }
126
- ```
127
-
128
- **Behavior Matrix:**
129
- | Scenario | `success` | `data` | `error` |
130
- |----------|-----------|---------|----------|
131
- | New collection created | `true` | Collection instance | `null` |
132
- | Collection already exists | `false` | Existing Collection | `COLLECTION_EXISTS` error |
133
-
134
- **Example:**
135
- ```javascript
136
- const result = await db.createCollection('users');
137
- if (result.success) {
138
- const collection = result.data;
139
- } else {
140
- console.log('Collection exists:', result.data);
141
- }
142
- ```
17
+ Aggregation Pipeline: Perform complex data analysis and transformations directly in the database with a multi-stage aggregation pipeline ($match, $group, $sort, etc.).
143
18
 
144
- ---
19
+ High-Performance Serialization: Uses turboserial and turbobase64 instead of JSON for faster serialization and deserialization of data.
145
20
 
146
- ##### `async getCollection(collectionName: string): Promise<LacertaDBResult>`
147
- Retrieves a collection by name.
21
+ Automatic Cleanup: Set storage limits and let the database automatically manage space by removing the oldest, non-permanent documents.
148
22
 
149
- **Returns:** `LacertaDBResult` object
23
+ Full-Featured: Includes batch operations, query caching, metadata management, and a migration system to handle schema changes over time.
150
24
 
151
- **Behavior Matrix:**
152
- | Scenario | `success` | `data` | `error` |
153
- |----------|-----------|---------|----------|
154
- | Collection exists in memory | `true` | Collection instance | `null` |
155
- | Collection exists in DB only | `true` | New Collection instance | `null` |
156
- | Collection doesn't exist | `false` | `null` | `COLLECTION_NOT_FOUND` error |
25
+ 🚀 Installation
26
+ Install the package using your favorite package manager.
157
27
 
158
- **Example:**
159
- ```javascript
160
- const result = await db.getCollection('users');
161
- if (result.success) {
162
- const collection = result.data;
163
- } else {
164
- console.error('Collection not found:', result.error.message);
165
- }
166
- ```
28
+ npm install @pixagram/lacerta-db
167
29
 
168
- ---
30
+ ⚡ Quick Start
31
+ Here's how to get up and running with LacertaDB in just a few lines of code.
169
32
 
170
- ##### `async deleteCollection(collectionName: string): Promise<LacertaDBResult>`
171
- Deletes a collection and all its documents.
33
+ // Import the default instance
34
+ import LacertaDB from '@pixagram/lacerta-db';
172
35
 
173
- **Returns:** `LacertaDBResult` object
36
+ async function main() {
37
+ try {
38
+ // 1. Get a database instance
39
+ const db = await LacertaDB.getDatabase('my-app-db');
174
40
 
175
- **Behavior Matrix:**
176
- | Scenario | `success` | `data` | `error` |
177
- |----------|-----------|---------|----------|
178
- | Collection deleted | `true` | `null` | `null` |
179
- | Collection doesn't exist | `false` | `null` | `COLLECTION_NOT_FOUND` error |
41
+ // 2. Get or create a collection
42
+ // Collections are created automatically on first access
43
+ const users = await db.getCollection('users');
180
44
 
181
- **Side Effects:**
182
- - Clears all documents from IndexedDB
183
- - Removes collection metadata
184
- - Cleans up event observers
185
- - Stops free space timers
45
+ // 3. Add a new document
46
+ const newUserId = await users.add({
47
+ name: 'Alex',
48
+ level: 10,
49
+ joined: new Date()
50
+ });
51
+ console.log(`User created with ID: ${newUserId}`);
186
52
 
187
- ---
53
+ // 4. Get a document by its ID
54
+ const user = await users.get(newUserId);
55
+ console.log('Retrieved user:', user);
188
56
 
189
- ##### `async close(): Promise<void>`
190
- Closes all database connections.
57
+ // 5. Query for documents
58
+ const highLevelUsers = await users.query({ level: { '$gt': 5 } });
59
+ console.log('High-level users:', highLevelUsers);
191
60
 
192
- **Important:** Always call before application termination.
61
+ } catch (error) {
62
+ console.error('Database operation failed:', error);
63
+ }
64
+ }
193
65
 
194
- ---
66
+ main();
195
67
 
196
- ##### `async deleteDatabase(): Promise<void>`
197
- Permanently deletes entire database.
68
+ 📖 API Reference
69
+ Database
70
+ The Database object is your main entry point for managing collections.
198
71
 
199
- **Warning:** This operation is irreversible!
72
+ getDatabase(name)
73
+ Retrieves a database instance. If it doesn't exist, it's created.
200
74
 
201
- **Side Effects:**
202
- - Deletes IndexedDB database
203
- - Removes all localStorage metadata
204
- - Clears all settings
205
- - Invalidates all collection references
75
+ const db = await LacertaDB.getDatabase('my-app-db');
206
76
 
207
- ---
77
+ getCollection(name)
78
+ Retrieves a collection from the database. If it doesn't exist, it's created.
208
79
 
209
- ### Collection Class
80
+ const usersCollection = await db.getCollection('users');
210
81
 
211
- #### Properties
82
+ dropCollection(name)
83
+ Deletes a collection and all of its documents and attachments.
212
84
 
213
- | Property | Type | Description |
214
- |----------|------|-------------|
215
- | `name` | string | Collection identifier |
216
- | `sizeKB` | number | Total size in kilobytes |
217
- | `length` | number | Document count |
218
- | `keys` | string[] | All document IDs |
219
- | `observer` | Observer | Event emitter instance |
220
- | `documentsMetadata` | Array | Document metadata array |
85
+ await db.dropCollection('users');
221
86
 
222
- #### Methods
87
+ Collection
88
+ The Collection object provides methods to interact with documents.
223
89
 
224
- ##### `async addDocument(documentData: DocumentInput, encryptionKey?: string): Promise<boolean>`
90
+ add(data, [options])
91
+ Adds a new document to the collection.
225
92
 
226
- **Parameters:**
227
- ```typescript
228
- interface DocumentInput {
229
- _id?: string; // Auto-generated if omitted
230
- _permanent?: boolean; // Prevents auto-deletion (default: false)
231
- _compressed?: boolean; // Enable compression (default: false)
232
- data: object; // Document content
233
- attachments?: Array<{data: Blob}>; // File attachments
234
- }
235
- ```
236
-
237
- **Returns:** `boolean` - `true` if new document, `false` if updated
238
-
239
- **Behavior:**
240
- - Auto-generates ID if not provided
241
- - Updates existing document if ID exists
242
- - Saves attachments to OPFS
243
- - Triggers space management
244
- - Emits `beforeAdd` and `afterAdd` events
245
-
246
- **Example:**
247
- ```javascript
248
- const isNew = await collection.addDocument({
249
- data: { name: 'Alice', age: 30 },
250
- _permanent: true,
251
- attachments: [{ data: imageBlob }]
252
- }, 'encryption-password');
253
- ```
254
-
255
- ---
256
-
257
- ##### `async getDocument(docId: string, encryptionKey?: string, includeAttachments?: boolean): Promise<Document | false>`
258
-
259
- **Returns:** Document object or `false`
260
-
261
- **Behavior Matrix:**
262
- | Scenario | Return Value |
263
- |----------|--------------|
264
- | Document exists, no encryption | Document object |
265
- | Document exists, correct key | Document object |
266
- | Document exists, wrong/missing key | `false` |
267
- | Document doesn't exist | `false` |
268
-
269
- ---
270
-
271
- ##### `async deleteDocument(docId: string, force?: boolean): Promise<boolean>`
272
-
273
- **Parameters:**
274
- - `docId`: Document identifier
275
- - `force`: Override permanent flag (default: false)
276
-
277
- **Returns:** `boolean` - Success status
278
-
279
- **Behavior Matrix:**
280
- | Scenario | `force` | Result |
281
- |----------|---------|---------|
282
- | Non-permanent document | any | Deleted |
283
- | Permanent document | `false` | Not deleted |
284
- | Permanent document | `true` | Deleted |
285
- | Document doesn't exist | any | Returns `false` |
286
-
287
- ---
288
-
289
- ##### `async query(filter?: object, options?: QueryOptions): Promise<Document[]>`
290
-
291
- **Parameters:**
292
- ```typescript
293
- interface QueryOptions {
294
- encryptionKey?: string;
295
- limit?: number; // Max results (default: Infinity)
296
- offset?: number; // Skip documents (default: 0)
297
- orderBy?: 'asc' | 'desc';
298
- index?: string; // Index name for optimization
299
- }
300
- ```
93
+ data (Object): The document data.
301
94
 
302
- **Filter Examples:**
303
- ```javascript
304
- // Simple equality
305
- { status: 'active' }
95
+ options (Object, optional):
306
96
 
307
- // Nested property
308
- { 'address.city': 'New York' }
309
- ```
97
+ encrypted (boolean): Set to true to encrypt the document.
310
98
 
311
- ---
99
+ password (string): Required if encrypted is true.
312
100
 
313
- ##### `async createIndex(fieldPath: string, options?: IndexOptions): Promise<void>`
101
+ permanent (boolean): If true, the document won't be deleted by the auto-cleanup process.
314
102
 
315
- **Parameters:**
316
- ```typescript
317
- interface IndexOptions {
318
- name?: string; // Custom index name
319
- unique?: boolean; // Enforce uniqueness
320
- multiEntry?: boolean; // Index array values
321
- }
322
- ```
103
+ attachments (Array): An array of file attachments.
323
104
 
324
- **Example:**
325
- ```javascript
326
- await collection.createIndex('data.email', {
327
- name: 'email_idx',
328
- unique: true
329
- });
330
- ```
105
+ const userId = await users.add({ name: 'Zoe' }, { permanent: true });
331
106
 
332
- ---
107
+ get(id, [options])
108
+ Retrieves a single document by its _id.
333
109
 
334
- ##### `async freeSpace(size: number): Promise<number>`
110
+ id (string): The document ID.
335
111
 
336
- **Parameters:**
337
- - `size`: Target size in KB (positive) or amount to free (negative)
112
+ options (Object, optional):
338
113
 
339
- **Returns:** Amount freed in KB
114
+ password (string): Required to decrypt an encrypted document.
340
115
 
341
- **Algorithm:**
342
- 1. Sorts non-permanent documents by modification time
343
- 2. Deletes oldest first
344
- 3. Stops when target reached
116
+ includeAttachments (boolean): If true, retrieves file data from OPFS.
345
117
 
346
- ---
118
+ const user = await users.get(userId);
347
119
 
348
- ### Document Class
120
+ query(filter, [options])
121
+ Finds documents matching the filter object.
349
122
 
350
- #### Constructor
351
- ```javascript
352
- new Document(data: DocumentData, encryptionKey?: string)
353
- ```
123
+ filter (Object): A MongoDB-style query object.
354
124
 
355
- #### Schema
356
- ```typescript
357
- interface DocumentData {
358
- _id?: string;
359
- _created?: number;
360
- _modified?: number;
361
- _permanent?: boolean;
362
- _encrypted?: boolean;
363
- _compressed?: boolean;
364
- data?: object;
365
- packedData?: Uint8Array;
366
- attachments?: Array;
367
- }
368
- ```
369
-
370
- #### Methods
371
-
372
- ##### `async pack(): Promise<Uint8Array>`
373
- Serializes, compresses, and encrypts document data.
374
-
375
- ##### `async unpack(): Promise<object>`
376
- Deserializes, decompresses, and decrypts document data.
377
-
378
- ##### `packSync(): Uint8Array`
379
- Synchronous packing (no encryption/compression).
125
+ options (Object, optional):
380
126
 
381
- **Throws:** Error if document has encryption or compression enabled
127
+ sort (Object): Sort order (e.g., { level: -1 }).
382
128
 
383
- ##### `unpackSync(): object`
384
- Synchronous unpacking (no encryption/compression).
129
+ limit (number): Max number of documents to return.
385
130
 
386
- ---
131
+ skip (number): Number of documents to skip.
387
132
 
388
- ### QuickStore Class
133
+ const results = await users.query({ name: 'Alex' });
389
134
 
390
- Provides synchronous localStorage-based storage.
135
+ update(id, updates)
136
+ Updates the data of a specific document.
391
137
 
392
- #### Methods
138
+ await users.update(userId, { level: 11 });
393
139
 
394
- ##### `setDocumentSync(documentData: object, encryptionKey?: string): boolean`
395
- **Note:** Cannot use encryption or compression (throws error).
140
+ delete(id)
141
+ Removes a document from the collection.
396
142
 
397
- ##### `getDocumentSync(docId: string, encryptionKey?: string): object | null`
143
+ await users.delete(userId);
398
144
 
399
- ##### `deleteDocumentSync(docId: string, force?: boolean): boolean`
145
+ aggregate(pipeline)
146
+ Processes documents through an aggregation pipeline.
400
147
 
401
- ##### `getAllKeys(): string[]`
148
+ const results = await users.aggregate([
149
+ { '$match': { level: { '$gt': 5 } } },
150
+ { '$group': { _id: null, averageLevel: { '$avg': '$level' } } }
151
+ ]);
402
152
 
403
- ---
153
+ 🛠️ Advanced Usage
154
+ Encryption 🔒
155
+ To store sensitive data, simply set the encrypted flag and provide a password. LacertaDB handles the rest.
404
156
 
405
- ## Data Schemas
157
+ const secretData = { account: '123-456', secret: 'my-secret-key' };
406
158
 
407
- ### Document Schema (Stored)
408
- ```javascript
409
- {
410
- "_id": "xxxx-xxxx-xxxx",
411
- "_created": 1234567890,
412
- "_modified": 1234567890,
413
- "_permanent": false,
414
- "_encrypted": false,
415
- "_compressed": false,
416
- "packedData": Uint8Array,
417
- "attachments": ["db/collection/doc/0", "db/collection/doc/1"]
418
- }
419
- ```
420
-
421
- ### Document Schema (Retrieved)
422
- ```javascript
423
- {
424
- "_id": "xxxx-xxxx-xxxx",
425
- "_created": 1234567890,
426
- "_modified": 1234567890,
427
- "_permanent": false,
428
- "_encrypted": false,
429
- "_compressed": false,
430
- "data": { /* user data */ },
431
- "attachments": [
432
- { "path": "db/collection/doc/0", "data": File },
433
- { "path": "db/collection/doc/1", "data": File }
434
- ]
435
- }
436
- ```
437
-
438
- ### Metadata Schema
439
- ```javascript
440
- {
441
- "name": "database_name",
442
- "collections": {
443
- "collection_name": {
444
- "name": "collection_name",
445
- "sizeKB": 1024.5,
446
- "length": 100,
447
- "createdAt": 1234567890,
448
- "modifiedAt": 1234567890,
449
- "documentSizes": { "docId": 10.5 },
450
- "documentModifiedAt": { "docId": 1234567890 },
451
- "documentPermanent": { "docId": 1 },
452
- "documentAttachments": { "docId": 2 }
453
- }
454
- },
455
- "totalSizeKB": 2048.5,
456
- "totalLength": 200,
457
- "modifiedAt": 1234567890
458
- }
459
- ```
460
-
461
- ---
462
-
463
- ## Error Handling
464
-
465
- ### Error Codes
466
- | Code | Description | Recovery Strategy |
467
- |------|-------------|-------------------|
468
- | `COLLECTION_EXISTS` | Collection already exists | Use existing collection |
469
- | `COLLECTION_NOT_FOUND` | Collection doesn't exist | Create collection first |
470
- | `DOCUMENT_NOT_FOUND` | Document doesn't exist | Check document ID |
471
- | `ENCRYPTION_FAILED` | Encryption/decryption failed | Verify encryption key |
472
- | `TRANSACTION_FAILED` | Database transaction failed | Retry operation |
473
- | `QUOTA_EXCEEDED` | Storage quota exceeded | Free space or increase quota |
474
- | `ATTACHMENT_DELETE_FAILED` | Attachment cleanup failed | Manual OPFS cleanup |
475
- | `METADATA_SYNC_FAILED` | Metadata save failed | Check localStorage quota |
476
-
477
- ### Error Handling Pattern
478
- ```javascript
479
- try {
480
- const result = await db.createCollection('users');
481
- if (result.success) {
482
- // Handle success
483
- } else {
484
- switch (result.error.code) {
485
- case ErrorCodes.COLLECTION_EXISTS:
486
- // Use existing collection
487
- break;
488
- default:
489
- throw result.error;
490
- }
491
- }
492
- } catch (error) {
493
- if (error instanceof LacertaDBError) {
494
- console.error(`Database error ${error.code}: ${error.message}`);
495
- }
496
- }
497
- ```
498
-
499
- ---
500
-
501
- ## Edge Cases & Behaviors
502
-
503
- ### Collection Operations
504
-
505
- #### Creating Existing Collection
506
- ```javascript
507
- const result1 = await db.createCollection('users');
508
- // result1.success = true, result1.data = Collection
509
-
510
- const result2 = await db.createCollection('users');
511
- // result2.success = false
512
- // result2.data = existing Collection instance
513
- // result2.error.code = 'COLLECTION_EXISTS'
514
- ```
515
-
516
- #### Getting Non-Existent Collection
517
- ```javascript
518
- const result = await db.getCollection('nonexistent');
519
- // result.success = false
520
- // result.data = null
521
- // result.error.code = 'COLLECTION_NOT_FOUND'
522
- ```
523
-
524
- #### Deleting Non-Existent Collection
525
- ```javascript
526
- const result = await db.deleteCollection('nonexistent');
527
- // result.success = false
528
- // result.data = null
529
- // result.error.code = 'COLLECTION_NOT_FOUND'
530
- ```
531
-
532
- ### Document Operations
533
-
534
- #### Adding Document with Existing ID
535
- ```javascript
536
- // First add
537
- await collection.addDocument({ _id: 'doc1', data: { v: 1 } });
538
- // Returns: true (new document)
539
-
540
- // Second add with same ID
541
- await collection.addDocument({ _id: 'doc1', data: { v: 2 } });
542
- // Returns: false (updated existing)
543
- // Document is updated, not duplicated
544
- ```
545
-
546
- #### Getting Encrypted Document Without Key
547
- ```javascript
548
- // Add encrypted document
549
- await collection.addDocument(
550
- { data: { secret: 'data' } },
551
- 'encryption-key'
552
- );
553
-
554
- // Try to get without key
555
- const doc = await collection.getDocument(docId);
556
- // Returns: false (cannot decrypt)
557
-
558
- // Try with wrong key
559
- const doc2 = await collection.getDocument(docId, 'wrong-key');
560
- // Returns: false (decryption fails)
561
- ```
562
-
563
- #### Deleting Permanent Document
564
- ```javascript
565
- // Add permanent document
566
- await collection.addDocument({
567
- _permanent: true,
568
- data: { important: 'data' }
159
+ const docId = await db.collection('secrets').add(secretData, {
160
+ encrypted: true,
161
+ password: 'a-very-strong-password'
569
162
  });
570
163
 
571
- // Try normal delete
572
- const deleted = await collection.deleteDocument(docId);
573
- // Returns: false (protected)
574
-
575
- // Force delete
576
- const forceDeleted = await collection.deleteDocument(docId, true);
577
- // Returns: true (deleted)
578
- ```
579
-
580
- ### Space Management Behaviors
581
-
582
- #### Automatic Cleanup Trigger
583
- ```javascript
584
- const db = new Database('myapp', {
585
- sizeLimitKB: 1000, // 1MB limit
586
- bufferLimitKB: -200, // Start at 800KB
587
- freeSpaceEvery: 5000 // Check every 5 seconds
164
+ // You MUST provide the same password to retrieve it
165
+ const retrieved = await db.collection('secrets').get(docId, {
166
+ password: 'a-very-strong-password'
588
167
  });
589
168
 
590
- // When collection.sizeKB > 800KB:
591
- // - Automatic cleanup triggers
592
- // - Deletes oldest non-permanent documents
593
- // - Stops at 1000KB or when all deletable removed
594
- ```
595
-
596
- #### Manual Space Freeing
597
- ```javascript
598
- // Keep only 500KB
599
- const freed = await collection.freeSpace(500);
600
-
601
- // Free 200KB
602
- const freed = await collection.freeSpace(-200);
603
- ```
604
-
605
- ### Transaction Behaviors
606
-
607
- #### Retry Logic
608
- - Transactions retry up to 3 times on failure
609
- - Exponential backoff: 100ms, 200ms, 400ms
610
- - Includes idempotency tracking to prevent duplicates
611
-
612
- #### Concurrent Operations
613
- ```javascript
614
- // Operations are queued and executed serially
615
- const promises = [
616
- collection.addDocument({ data: { id: 1 } }),
617
- collection.addDocument({ data: { id: 2 } }),
618
- collection.addDocument({ data: { id: 3 } })
619
- ];
620
- await Promise.all(promises);
621
- // Executed in order, prevents race conditions
622
- ```
623
-
624
- ---
625
-
626
- ## Performance Optimization
627
-
628
- ### Indexing Strategy
629
- ```javascript
630
- // Create indexes for frequently queried fields
631
- await collection.createIndex('data.status');
632
- await collection.createIndex('data.userId');
633
-
634
- // Use indexes in queries
635
- const results = await collection.query(
636
- { status: 'active' },
637
- { index: 'data_status' } // Index name is fieldPath with dots replaced
638
- );
639
- ```
640
-
641
- ### Batch Operations Pattern
642
- ```javascript
643
- // Efficient batch insert
644
- const documents = generateDocuments(1000);
645
- const promises = documents.map(doc =>
646
- collection.addDocument(doc)
647
- );
648
- await Promise.all(promises);
649
- ```
650
-
651
- ### Memory Management
652
- ```javascript
653
- // Process large datasets in chunks
654
- async function* queryInChunks(collection, filter, chunkSize = 100) {
655
- let offset = 0;
656
- while (true) {
657
- const chunk = await collection.query(filter, {
658
- limit: chunkSize,
659
- offset: offset
660
- });
661
- if (chunk.length === 0) break;
662
- yield chunk;
663
- offset += chunkSize;
664
- }
665
- }
666
-
667
- // Usage
668
- for await (const chunk of queryInChunks(collection, {})) {
669
- processChunk(chunk);
670
- }
671
- ```
672
-
673
- ---
674
-
675
- ## Security Model
169
+ File Attachments 📎
170
+ You can attach files (like File objects or Uint8Array data) to a document. They are stored efficiently in the browser's Origin Private File System.
676
171
 
677
- ### Encryption Specifications
678
- - **Algorithm**: AES-GCM 256-bit
679
- - **Key Derivation**: PBKDF2-SHA-512, 600,000 iterations
680
- - **Salt**: 16 bytes random
681
- - **IV**: 12 bytes random
682
- - **Checksum**: SHA-256 for integrity
683
-
684
- ### Security Best Practices
685
- ```javascript
686
- // Use strong passwords
687
- const strongKey = crypto.randomUUID() + crypto.randomUUID();
688
-
689
- // Encrypt sensitive collections
690
- class SecureCollection {
691
- constructor(collection, masterKey) {
692
- this.collection = collection;
693
- this.masterKey = masterKey;
694
- }
695
-
696
- async add(data) {
697
- return this.collection.addDocument(data, this.masterKey);
698
- }
699
-
700
- async get(id) {
701
- return this.collection.getDocument(id, this.masterKey);
702
- }
703
- }
704
-
705
- // Field-level encryption pattern
706
- function encryptField(field, key) {
707
- // Implement field-specific encryption
708
- return encryptedValue;
709
- }
710
-
711
- const doc = {
712
- data: {
713
- public: 'visible',
714
- ssn: encryptField('123-45-6789', fieldKey)
715
- }
172
+ // Create a sample file
173
+ const fileContent = new TextEncoder().encode('This is the content of my file.');
174
+ const attachment = {
175
+ name: 'readme.txt',
176
+ type: 'text/plain',
177
+ data: fileContent
716
178
  };
717
- ```
718
-
719
- ---
720
-
721
- ## Code Examples
722
-
723
- ### Complete Application Example
724
- ```javascript
725
- import { Database } from './lacertadb.js';
726
-
727
- class TodoApp {
728
- async init() {
729
- // Initialize database
730
- this.db = new Database('todos', {
731
- sizeLimitKB: 5000,
732
- bufferLimitKB: -1000,
733
- freeSpaceEvery: 60000
734
- });
735
- await this.db.init();
736
-
737
- // Get or create collection
738
- const result = await this.db.getCollection('tasks');
739
- if (!result.success) {
740
- const createResult = await this.db.createCollection('tasks');
741
- this.tasks = createResult.data;
742
- } else {
743
- this.tasks = result.data;
744
- }
745
-
746
- // Create indexes
747
- await this.tasks.createIndex('data.priority');
748
- await this.tasks.createIndex('data.dueDate');
749
-
750
- // Setup observers
751
- this.tasks.observer.on('afterAdd', (doc) => {
752
- this.updateUI('added', doc);
753
- });
754
-
755
- this.tasks.observer.on('afterDelete', (id) => {
756
- this.updateUI('deleted', id);
757
- });
758
- }
759
-
760
- async addTask(title, priority, dueDate) {
761
- return await this.tasks.addDocument({
762
- data: {
763
- title,
764
- priority,
765
- dueDate,
766
- completed: false,
767
- createdAt: Date.now()
768
- }
769
- });
770
- }
771
-
772
- async getHighPriorityTasks() {
773
- return await this.tasks.query(
774
- { priority: 'high' },
775
- {
776
- index: 'data_priority',
777
- orderBy: 'asc'
778
- }
779
- );
780
- }
781
-
782
- async getOverdueTasks() {
783
- const now = Date.now();
784
- const allTasks = await this.tasks.query({}, {
785
- index: 'data_dueDate',
786
- orderBy: 'asc'
787
- });
788
-
789
- return allTasks.filter(task =>
790
- task.data.dueDate < now && !task.data.completed
791
- );
792
- }
793
-
794
- async completeTask(taskId) {
795
- const task = await this.tasks.getDocument(taskId);
796
- if (task) {
797
- task.data.completed = true;
798
- task.data.completedAt = Date.now();
799
- await this.tasks.addDocument(task);
800
- }
801
- }
802
-
803
- async cleanup() {
804
- await this.db.close();
805
- }
806
-
807
- updateUI(action, data) {
808
- console.log(`UI Update: ${action}`, data);
809
- }
810
- }
811
-
812
- // Usage
813
- const app = new TodoApp();
814
- await app.init();
815
- await app.addTask('Review PRs', 'high', Date.now() + 86400000);
816
- const urgent = await app.getHighPriorityTasks();
817
- ```
818
-
819
- ### Encrypted Notes Application
820
- ```javascript
821
- class SecureNotes {
822
- constructor(password) {
823
- this.password = password;
824
- }
825
-
826
- async init() {
827
- this.db = new Database('secure_notes');
828
- await this.db.init();
829
-
830
- const result = await this.db.createCollection('notes');
831
- this.notes = result.success ? result.data : result.data;
832
- }
833
-
834
- async addNote(title, content, attachments = []) {
835
- return await this.notes.addDocument({
836
- data: {
837
- title,
838
- content,
839
- tags: [],
840
- createdAt: Date.now()
841
- },
842
- attachments,
843
- _permanent: true,
844
- _encrypted: true
845
- }, this.password);
846
- }
847
-
848
- async getNote(id) {
849
- return await this.notes.getDocument(
850
- id,
851
- this.password,
852
- true // Include attachments
853
- );
854
- }
855
-
856
- async searchNotes(searchTerm) {
857
- // Get all notes (encrypted)
858
- const allNotes = await this.notes.query(
859
- {},
860
- { encryptionKey: this.password }
861
- );
862
-
863
- // Search in decrypted content
864
- return allNotes.filter(note =>
865
- note.data.title.includes(searchTerm) ||
866
- note.data.content.includes(searchTerm)
867
- );
868
- }
869
- }
870
- ```
871
-
872
- ### Data Migration Pattern
873
- ```javascript
874
- class DatabaseMigration {
875
- static async migrate(fromDb, toDb) {
876
- const sourceDb = new Database(fromDb);
877
- const targetDb = new Database(toDb);
878
-
879
- await sourceDb.init();
880
- await targetDb.init();
881
-
882
- // Get all collections
883
- for (const collectionName of sourceDb.metadata.getCollectionNames()) {
884
- const sourceResult = await sourceDb.getCollection(collectionName);
885
- if (!sourceResult.success) continue;
886
-
887
- const source = sourceResult.data;
888
- const targetResult = await targetDb.createCollection(collectionName);
889
- const target = targetResult.success ?
890
- targetResult.data : targetResult.data;
891
-
892
- // Copy all documents
893
- const docs = await source.query({});
894
- for (const doc of docs) {
895
- await target.addDocument(doc);
896
- }
897
- }
898
-
899
- await sourceDb.close();
900
- await targetDb.close();
901
- }
902
- }
903
- ```
904
-
905
- ---
906
-
907
- ## Troubleshooting
908
179
 
909
- ### Common Issues
180
+ // Add a document with the attachment
181
+ const reportId = await db.collection('reports').add(
182
+ { title: 'Q3 Report' },
183
+ { attachments: [attachment] }
184
+ );
910
185
 
911
- #### QuotaExceededError
912
- ```javascript
913
- // Solution 1: Enable auto-cleanup
914
- const db = new Database('app', {
915
- sizeLimitKB: 5000,
916
- freeSpaceEvery: 30000
186
+ // Retrieve the document and its attachments
187
+ const report = await db.collection('reports').get(reportId, {
188
+ includeAttachments: true
917
189
  });
918
190
 
919
- // Solution 2: Manual cleanup
920
- await collection.freeSpace(1000);
191
+ console.log(report.data._attachments[0].name); // "readme.txt"
192
+ const content = new TextDecoder().decode(report.data._attachments[0].data);
193
+ console.log(content); // "This is the content of my file."
921
194
 
922
- // Solution 3: Request more quota
923
- if (navigator.storage?.persist) {
924
- await navigator.storage.persist();
925
- }
926
- ```
927
-
928
- #### Encryption Key Lost
929
- ```javascript
930
- // Implement key recovery
931
- class KeyManager {
932
- static async deriveFromPassword(password, salt) {
933
- const encoder = new TextEncoder();
934
- const keyMaterial = await crypto.subtle.importKey(
935
- 'raw',
936
- encoder.encode(password),
937
- 'PBKDF2',
938
- false,
939
- ['deriveBits']
940
- );
941
-
942
- const keyBits = await crypto.subtle.deriveBits(
943
- {
944
- name: 'PBKDF2',
945
- salt: encoder.encode(salt),
946
- iterations: 100000,
947
- hash: 'SHA-256'
948
- },
949
- keyMaterial,
950
- 256
951
- );
952
-
953
- return btoa(String.fromCharCode(...new Uint8Array(keyBits)));
954
- }
955
- }
956
- ```
957
-
958
- #### Performance Degradation
959
- ```javascript
960
- // Monitor and optimize
961
- class PerformanceMonitor {
962
- static async analyzeCollection(collection) {
963
- return {
964
- size: collection.sizeKB,
965
- documents: collection.length,
966
- avgDocSize: collection.sizeKB / collection.length,
967
- metadata: collection.documentsMetadata,
968
- recommendation: this.getRecommendation(collection)
969
- };
970
- }
971
-
972
- static getRecommendation(collection) {
973
- if (collection.length > 10000) {
974
- return 'Consider sharding';
975
- }
976
- if (collection.sizeKB / collection.length > 100) {
977
- return 'Consider compression';
978
- }
979
- return 'Optimal';
980
- }
981
- }
982
- ```
195
+ ⚙️ Serialization: TurboSerial
196
+ A key performance feature of LacertaDB is its use of turboserial instead of JSON. This provides several advantages:
983
197
 
984
- ---
198
+ ⚡ Speed: turboserial is significantly faster at serializing and deserializing complex JavaScript objects.
985
199
 
986
- ## Version History
200
+ 📦 Efficiency: It produces a more compact binary output, especially when compression is enabled, saving storage space.
987
201
 
988
- ### v1.0.0 - Architecturally Refined
989
- - Fixed metadata synchronization issues
990
- - Improved transaction retry logic with idempotency
991
- - Added observer cleanup to prevent memory leaks
992
- - Enhanced error handling with LacertaDBResult
993
- - Added comprehensive edge case handling
202
+ 🔬 Type Support: It correctly handles more data types than JSON, including Date, Map, Set, BigInt, and typed arrays.
994
203
 
995
- ---
204
+ All data stored in localStorage (like metadata) or passed to IndexedDB is processed through turboserial and turbobase64, ensuring top-tier performance.
996
205
 
997
- ## License
998
- MIT License - Copyright (c) 2024 Matias Affolter
206
+ 📜 License
207
+ LacertaDB is licensed under the MIT License.