localspace 0.3.0 → 1.0.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.
Files changed (41) hide show
  1. package/README.md +394 -31
  2. package/dist/core/plugin-manager.d.ts +5 -0
  3. package/dist/core/plugin-manager.d.ts.map +1 -1
  4. package/dist/core/plugin-manager.js +82 -5
  5. package/dist/drivers/indexeddb.d.ts.map +1 -1
  6. package/dist/drivers/indexeddb.js +138 -74
  7. package/dist/drivers/localstorage.d.ts.map +1 -1
  8. package/dist/drivers/localstorage.js +27 -3
  9. package/dist/index.cjs.js +1 -1
  10. package/dist/index.cjs.js.map +1 -1
  11. package/dist/index.esm.js +1 -1
  12. package/dist/index.esm.js.map +1 -1
  13. package/dist/index.umd.js +1 -1
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/localspace.d.ts.map +1 -1
  16. package/dist/localspace.js +4 -2
  17. package/dist/plugins/compression.d.ts.map +1 -1
  18. package/dist/plugins/compression.js +76 -9
  19. package/dist/plugins/encryption.d.ts.map +1 -1
  20. package/dist/plugins/encryption.js +67 -5
  21. package/dist/plugins/quota.d.ts.map +1 -1
  22. package/dist/plugins/quota.js +109 -1
  23. package/dist/plugins/sync.d.ts.map +1 -1
  24. package/dist/plugins/sync.js +70 -1
  25. package/dist/plugins/ttl.d.ts +5 -0
  26. package/dist/plugins/ttl.d.ts.map +1 -1
  27. package/dist/plugins/ttl.js +88 -12
  28. package/dist/types.d.ts +18 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js +1 -1
  31. package/package.json +3 -1
  32. package/src/core/plugin-manager.ts +95 -4
  33. package/src/drivers/indexeddb.ts +196 -106
  34. package/src/drivers/localstorage.ts +41 -2
  35. package/src/localspace.ts +3 -1
  36. package/src/plugins/compression.ts +125 -10
  37. package/src/plugins/encryption.ts +122 -6
  38. package/src/plugins/quota.ts +146 -1
  39. package/src/plugins/sync.ts +78 -0
  40. package/src/plugins/ttl.ts +111 -13
  41. package/src/types.ts +21 -0
package/README.md CHANGED
@@ -23,6 +23,7 @@ We stay 100% compatible with localForage on the surface, but rebuild the interna
23
23
  Starting fresh let us eliminate technical debt while maintaining API compatibility. The codebase is written in modern TypeScript, uses contemporary patterns, and has a clear structure that makes it straightforward to add new capabilities. Teams can migrate from localForage without changing application code, then unlock better developer experience and future extensibility.
24
24
 
25
25
  ## Table of Contents
26
+
26
27
  - [Motivation](#motivation)
27
28
  - [What needed to change](#what-needed-to-change)
28
29
  - [How localspace responds](#how-localspace-responds)
@@ -35,6 +36,7 @@ Starting fresh let us eliminate technical debt while maintaining API compatibili
35
36
  - [Configure isolated stores for clear data boundaries](#configure-isolated-stores-for-clear-data-boundaries)
36
37
  - [Choose drivers with predictable fallbacks](#choose-drivers-with-predictable-fallbacks)
37
38
  - [Handle binary data across browsers](#handle-binary-data-across-browsers)
39
+ - [Advanced: Coalesced Writes (IndexedDB only)](#advanced-coalesced-writes-indexeddb-only)
38
40
  - [Migration Guide](#migration-guide)
39
41
  - [Note differences from localForage before upgrading](#note-differences-from-localforage-before-upgrading)
40
42
  - [Enable compatibility mode for legacy callbacks](#enable-compatibility-mode-for-legacy-callbacks)
@@ -46,17 +48,19 @@ Starting fresh let us eliminate technical debt while maintaining API compatibili
46
48
  localspace is built on a foundation designed for growth. Here's what's planned:
47
49
 
48
50
  ### Core Compatibility (Complete)
51
+
49
52
  - [x] IndexedDB and localStorage drivers
50
53
  - [x] Full localForage API parity
51
54
  - [x] TypeScript-first implementation
52
55
  - [x] Comprehensive test coverage
53
56
  - [x] Modern build pipeline (ES modules, CommonJS, UMD)
54
57
  - [x] Batch operations (`setItems()`, `getItems()`, `removeItems()`) for higher throughput
55
- - [x] Automatic write coalescing (3-10x faster rapid writes, enabled by default)
58
+ - [x] Automatic write coalescing (3-10x faster rapid writes, opt-in for IndexedDB)
56
59
  - [x] Connection pooling, transaction batching, and warmup
57
60
  - [x] **Improved error handling** - Structured error types with detailed context
58
61
 
59
62
  ### TODO
63
+
60
64
  - [x] **Plugin system** - Middleware architecture for cross-cutting concerns
61
65
  - [ ] **OPFS driver** - Origin Private File System for high-performance file storage
62
66
  - [ ] **Custom driver templates** - Documentation and examples for third-party drivers
@@ -79,6 +83,7 @@ We prioritize features based on community feedback. If you need a specific capab
79
83
  3. **Contribute** - We welcome PRs for new drivers, plugins, or improvements
80
84
 
81
85
  **Want to help?** The most impactful contributions right now:
86
+
82
87
  - Testing in diverse environments (browsers, frameworks, edge cases)
83
88
  - Documentation improvements and usage examples
84
89
  - Performance benchmarks and optimization suggestions
@@ -87,6 +92,7 @@ We prioritize features based on community feedback. If you need a specific capab
87
92
  ## Installation and Usage
88
93
 
89
94
  ### localspace delivers modern storage compatibility
95
+
90
96
  localspace targets developers who need localForage's API surface without its historical baggage. **You get the same method names, configuration options, and driver constants, all implemented with modern JavaScript and TypeScript types.**
91
97
 
92
98
  - Promise-first API with optional callbacks
@@ -95,6 +101,7 @@ localspace targets developers who need localForage's API surface without its his
95
101
  - Drop-in TypeScript generics for value typing
96
102
 
97
103
  ### Install and import localspace
104
+
98
105
  Install the package with your preferred package manager and import it once at the entry point where you manage storage.
99
106
 
100
107
  ```bash
@@ -110,6 +117,7 @@ import localspace from 'localspace';
110
117
  ```
111
118
 
112
119
  ### Store data with async flows or callbacks
120
+
113
121
  Use async/await for the clearest flow. **Callbacks remain supported for parity with existing localForage codebases.**
114
122
 
115
123
  ```ts
@@ -122,8 +130,9 @@ localspace.getItem('user', (error, value) => {
122
130
  });
123
131
  ```
124
132
 
125
- ### 🚀 Get automatic performance optimization (enabled by default)
126
- localspace automatically merges rapid single writes into batched transactions, giving you **3-10x performance improvement** without changing your code. This feature is enabled by default and works transparently in the background.
133
+ ### 🚀 Opt into automatic performance optimization (coalesced writes)
134
+
135
+ localspace can merge rapid single writes into batched transactions for IndexedDB, giving you **3-10x performance improvement** under write-heavy bursts. This is opt-in so default behavior stays predictable; enable it when you know you have high write pressure.
127
136
 
128
137
  ```ts
129
138
  // Your existing code - unchanged
@@ -137,22 +146,21 @@ await Promise.all([
137
146
  // ✅ Zero code changes required
138
147
  ```
139
148
 
140
- **How it works**: When using IndexedDB, rapid writes within an 8ms window are automatically merged into a single transaction commit. This is transparent to your application and has no impact on single writes.
149
+ **How it works**: When using IndexedDB, rapid writes within an 8ms window are merged into a single transaction commit. This is transparent to your application and has no impact on single writes.
150
+
151
+ **Turn it on or tune it**
141
152
 
142
- **Want to customize or disable it?**
143
153
  ```ts
144
154
  const instance = localspace.createInstance({
145
- coalesceWrites: true, // enabled by default
146
- coalesceWindowMs: 8, // 8ms window (default)
147
- });
148
-
149
- // Or disable if you need strict per-operation durability
150
- const strict = localspace.createInstance({
151
- coalesceWrites: false,
155
+ coalesceWrites: true, // opt-in (default is false)
156
+ coalesceWindowMs: 8, // 8ms window (default)
152
157
  });
153
158
  ```
154
159
 
160
+ For consistency modes, batch limits, and failure semantics, see **Advanced: Coalesced Writes** below.
161
+
155
162
  **When is this useful?**
163
+
156
164
  - Form auto-save that writes multiple fields rapidly
157
165
  - Bulk state synchronization loops
158
166
  - Real-time collaborative editing
@@ -161,6 +169,7 @@ const strict = localspace.createInstance({
161
169
  **Performance impact**: Single infrequent writes are unaffected. Rapid sequential writes get 3-10x faster automatically.
162
170
 
163
171
  **Want to see the actual performance gains?**
172
+
164
173
  ```ts
165
174
  // Get statistics to see how much coalescing helped (IndexedDB only)
166
175
  const stats = localspace.getPerformanceStats?.();
@@ -174,6 +183,7 @@ console.log(stats);
174
183
  ```
175
184
 
176
185
  ### Boost throughput with batch operations
186
+
177
187
  Use the batch APIs to group writes and reads into single transactions for IndexedDB and localStorage. This reduces commit overhead and benefits from Chrome’s relaxed durability defaults (see below).
178
188
 
179
189
  ```ts
@@ -210,17 +220,21 @@ await Promise.all([
210
220
 
211
221
  // These features work independently and can be combined
212
222
  const optimized = localspace.createInstance({
213
- coalesceWrites: true, // optimizes single-item writes (setItem/removeItem)
223
+ coalesceWrites: true, // optimizes single-item writes (setItem/removeItem)
214
224
  coalesceWindowMs: 8,
215
- maxBatchSize: 200, // limits batch API chunk size (setItems/removeItems)
225
+ maxBatchSize: 200, // limits batch API chunk size (setItems/removeItems)
216
226
  });
217
227
  await optimized.setDriver([optimized.INDEXEDDB]);
218
228
 
219
- // Note: localStorage batches are not atomic—writes are applied one by one.
220
- // For critical flows, prefer IndexedDB or handle your own compensating logic.
229
+ // Note: localStorage batches attempt best-effort rollback on failure and map
230
+ // quota errors to QUOTA_EXCEEDED, but they still serialize per-item and are
231
+ // not truly atomic. For strict atomicity or durability, prefer IndexedDB or
232
+ // add your own compensating logic. If you need per-item success/failure, call
233
+ // setItems in smaller chunks or handle errors explicitly.
221
234
  ```
222
235
 
223
236
  ### Run your own transaction
237
+
224
238
  When you need atomic multi-step work (migrations, dependent writes), wrap operations in a single transaction. On IndexedDB this uses one `IDBTransaction`; on localStorage it executes sequentially.
225
239
 
226
240
  ```ts
@@ -234,6 +248,7 @@ await localspace.runTransaction('readwrite', async (tx) => {
234
248
  ```
235
249
 
236
250
  ### Configure isolated stores for clear data boundaries
251
+
237
252
  Create independent instances when you want to separate cache layers or product features. Each instance can override defaults like `name`, `storeName`, and driver order.
238
253
 
239
254
  ```ts
@@ -246,6 +261,7 @@ await sessionCache.setItem('token', 'abc123');
246
261
  ```
247
262
 
248
263
  ### Choose drivers with predictable fallbacks
264
+
249
265
  By default, localspace prefers IndexedDB (`INDEXEDDB`) and falls back to localStorage (`LOCALSTORAGE`). Configure alternative sequences as needed.
250
266
 
251
267
  ```ts
@@ -273,6 +289,7 @@ await bucketed.setDriver([bucketed.INDEXEDDB]);
273
289
  **Tip:** Use `defineDriver()` and `getDriver()` to register custom drivers that match the localForage interface.
274
290
 
275
291
  ### Handle binary data across browsers
292
+
276
293
  localspace serializes complex values transparently. It stores `Blob`, `ArrayBuffer`, and typed arrays in IndexedDB natively and in localStorage via Base64 encoding when necessary. You write the same code regardless of the driver.
277
294
 
278
295
  ```ts
@@ -281,6 +298,140 @@ await localspace.setItem('file', file);
281
298
  const restored = await localspace.getItem<Blob>('file');
282
299
  ```
283
300
 
301
+ ## Advanced: Coalesced Writes (IndexedDB only)
302
+
303
+ localspace offers an opt-in, configurable coalesced write path to cut IndexedDB transaction count and improve throughput under heavy write bursts.
304
+
305
+ > `coalesceWrites` defaults to `false` so behavior stays predictable. Turn it on when you expect high-frequency writes.
306
+
307
+ ### Why coalesce writes?
308
+
309
+ Each IndexedDB write opens a readwrite transaction. At high frequency, transaction startup overhead becomes a bottleneck. With coalescing enabled, `setItem` and `removeItem` calls that land within a short window (default 8 ms) are merged into fewer transactions:
310
+
311
+ - Multiple writes can share one transaction.
312
+ - `coalesceMaxBatchSize` caps how many ops each flush processes.
313
+ - `coalesceReadConsistency` controls when writes resolve and when reads see them.
314
+
315
+ ### Configuration
316
+
317
+ Relevant `LocalSpaceConfig` fields:
318
+
319
+ ```ts
320
+ interface LocalSpaceConfig {
321
+ /**
322
+ * Enable coalesced writes (IndexedDB only).
323
+ * Default: false
324
+ */
325
+ coalesceWrites?: boolean;
326
+
327
+ /**
328
+ * Time window (ms) for merging writes into the same batch.
329
+ * Default: 8
330
+ */
331
+ coalesceWindowMs?: number;
332
+
333
+ /**
334
+ * Maximum operations per flush batch. Beyond this, flush immediately
335
+ * and split into multiple transactions.
336
+ * Default: undefined (no limit)
337
+ */
338
+ coalesceMaxBatchSize?: number;
339
+
340
+ /**
341
+ * When coalesceWrites is on:
342
+ * - 'strong' (default): drain pending writes before reads
343
+ * - 'eventual': reads skip draining; writes only guarantee queueing
344
+ */
345
+ coalesceReadConsistency?: 'strong' | 'eventual';
346
+ }
347
+ ```
348
+
349
+ ### Consistency modes
350
+
351
+ #### `coalesceReadConsistency: 'strong'` (default)
352
+
353
+ - Writes (`setItem` / `removeItem`): Promises resolve after the data is persisted; flush errors reject.
354
+ - Reads (`getItem`, `iterate`, batch reads): call `drainCoalescedWrites` first so you read what you just wrote.
355
+
356
+ Use this for user settings, drafts, and any flow where you need read-your-writes.
357
+
358
+ #### `coalesceReadConsistency: 'eventual'`
359
+
360
+ - Writes: queued and resolve immediately once enqueued; flush happens in the background. Errors log `console.warn('[localspace] coalesced write failed (eventual mode)', error)` but do not reject the earlier Promise.
361
+ - Reads: do not flush pending writes, so you may briefly see stale values.
362
+ - Destructive operations still force a flush to avoid dropping queued writes: `removeItems`, `clear`, `dropInstance`.
363
+
364
+ Use this for logs/analytics or workloads that can tolerate short windows of staleness in exchange for the lightest write path.
365
+
366
+ ### Bounding batch size
367
+
368
+ ```ts
369
+ const store = localspace.createInstance({
370
+ name: 'logs',
371
+ storeName: 'events',
372
+ coalesceWrites: true,
373
+ coalesceWindowMs: 8,
374
+ coalesceMaxBatchSize: 64,
375
+ coalesceReadConsistency: 'eventual',
376
+ });
377
+ ```
378
+
379
+ - When the queue reaches `coalesceMaxBatchSize`, it flushes immediately.
380
+ - Flush splits work into batches of up to 64 ops, each in its own transaction.
381
+ - `getPerformanceStats()` reports `totalWrites`, `coalescedWrites`, and `transactionsSaved` so you can see the gains.
382
+
383
+ ### Recommended recipes
384
+
385
+ 1. Default: coalescing off
386
+
387
+ ```ts
388
+ const store = localspace.createInstance({
389
+ name: 'app',
390
+ storeName: 'keyvaluepairs',
391
+ // coalesceWrites is false by default
392
+ });
393
+ ```
394
+
395
+ 2. High-frequency writes with eventual consistency
396
+
397
+ ```ts
398
+ const logStore = localspace.createInstance({
399
+ name: 'analytics',
400
+ storeName: 'events',
401
+ coalesceWrites: true,
402
+ coalesceWindowMs: 8,
403
+ coalesceMaxBatchSize: 64,
404
+ coalesceReadConsistency: 'eventual',
405
+ });
406
+ ```
407
+
408
+ - `setItem` resolves almost immediately.
409
+ - Short windows of stale reads are acceptable.
410
+ - `clear` and `dropInstance` force-flush so queued writes are not lost.
411
+
412
+ 3. Strong consistency with bounded batches
413
+
414
+ ```ts
415
+ const userStore = localspace.createInstance({
416
+ name: 'user-data',
417
+ storeName: 'kv',
418
+ coalesceWrites: true,
419
+ coalesceWindowMs: 8,
420
+ coalesceMaxBatchSize: 32,
421
+ coalesceReadConsistency: 'strong',
422
+ });
423
+ ```
424
+
425
+ - Writes resolve after persistence.
426
+ - Reads flush pending writes first.
427
+ - Batching still reduces transaction count.
428
+
429
+ ### Caveats
430
+
431
+ - Coalesced writes apply to the IndexedDB driver only; localStorage always writes per operation.
432
+ - In `eventual` mode, writes can be lost if the page closes before flush completes, and errors surface only via `console.warn`.
433
+ - For critical durability (orders, payments, irreversible state), avoid `eventual` and consider leaving `coalesceWrites` off entirely.
434
+
284
435
  ## Plugin System
285
436
 
286
437
  localspace now ships with a first-class plugin engine. Attach middleware when creating an instance or call `use()` later; plugins can mutate payloads, observe driver context, and run async interceptors around every storage call.
@@ -291,8 +442,8 @@ const store = localspace.createInstance({
291
442
  storeName: 'primary',
292
443
  plugins: [
293
444
  ttlPlugin({ defaultTTL: 60_000 }),
294
- encryptionPlugin({ key: '0123456789abcdef0123456789abcdef' }),
295
445
  compressionPlugin({ threshold: 1024 }),
446
+ encryptionPlugin({ key: '0123456789abcdef0123456789abcdef' }),
296
447
  syncPlugin({ channelName: 'localspace-sync' }),
297
448
  quotaPlugin({ maxSize: 5 * 1024 * 1024, evictionPolicy: 'lru' }),
298
449
  ],
@@ -303,69 +454,278 @@ const store = localspace.createInstance({
303
454
 
304
455
  - **Registration** – supply `plugins` when calling `createInstance()` or chain `instance.use(plugin)` later. Each plugin can also expose `enabled` (boolean or function) and `priority` to control execution order.
305
456
  - **Lifecycle events** – `onInit(context)` is invoked after `ready()`, and `onDestroy` lets you tear down timers or channels. Call `await instance.destroy()` when disposing of an instance to run every `onDestroy` hook (executed in reverse priority order). Context exposes the active driver, db info, config, and a shared `metadata` bag for cross-plugin coordination.
306
- - **Interceptors** – hook into `beforeSet/afterSet`, `beforeGet/afterGet`, `beforeRemove/afterRemove`, plus batch-specific methods such as `beforeSetItems` or `beforeGetItems`. Hooks run sequentially: `before*` hooks execute from highest to lowest priority, while `after*` hooks unwind in reverse order so layered transformations (compressionencryptionTTL) remain invertible. Returning a value passes it to the next plugin, while throwing a `LocalSpaceError` aborts the operation.
457
+ - **Interceptors** – hook into `beforeSet/afterSet`, `beforeGet/afterGet`, `beforeRemove/afterRemove`, plus batch-specific methods such as `beforeSetItems` or `beforeGetItems`. Hooks run sequentially: `before*` hooks execute from highest to lowest priority, while `after*` hooks unwind in reverse order so layered transformations (TTLcompressionencryption) remain invertible. Returning a value passes it to the next plugin, while throwing a `LocalSpaceError` aborts the operation.
307
458
  - **Per-call state** – plugins can stash data on `context.operationState` (e.g., capture the original value in `beforeSet` and reuse it in `afterSet`). For batch operations, `context.operationState.isBatch` is `true` and `context.operationState.batchSize` provides the total count.
308
- - **Error handling & init policy** – unexpected exceptions are reported through `plugin.onError`. Throw a `LocalSpaceError` if you need to stop the pipeline (quota violations, failed decryptions, etc.). If a plugin `onInit` throws, the default policy is fail-fast (propagate and abort init). Set `pluginInitPolicy: 'disable-and-continue'` in config to log and skip the failing plugin instead (use with care for critical plugins like encryption).
459
+ - **Error handling & policies** – unexpected exceptions are reported through `plugin.onError`. Throw a `LocalSpaceError` if you need to stop the pipeline (quota violations, failed decryptions, etc.). Init policy: default fail-fast; set `pluginInitPolicy: 'disable-and-continue'` to log and skip the failing plugin. Runtime policy: default `pluginErrorPolicy: 'strict'` propagates all plugin errors; only use `lenient` if you explicitly accept swallowed errors, and avoid lenient for encryption/compression/ttl or any correctness-critical plugin.
309
460
 
310
461
  ### Plugin execution order
311
462
 
312
463
  Plugins are sorted by `priority` (higher runs first in `before*`, last in `after*`). Default priorities:
313
464
 
314
- | Plugin | Priority | Notes |
315
- |--------|----------|-------|
316
- | sync | -100 | Runs last in `afterSet` to broadcast original (untransformed) values |
317
- | quota | -10 | Runs late so it measures final payload sizes |
318
- | ttl, encryption, compression | 0 | Default; chain in registration order |
465
+ | Plugin | Priority | Notes |
466
+ | ----------- | -------- | -------------------------------------------------------------------- |
467
+ | sync | -100 | Runs last in `afterSet` to broadcast original (untransformed) values |
468
+ | quota | -10 | Runs late so it measures final payload sizes |
469
+ | encryption | 0 | Encrypts after compression so decrypt runs first in `after*` |
470
+ | compression | 5 | Runs before encryption so payload is compressible |
471
+ | ttl | 10 | Runs outermost so TTL wrapper is transformed by other plugins |
319
472
 
320
- **Recommended order**: `[ttlPlugin, encryptionPlugin, compressionPlugin, syncPlugin, quotaPlugin]`
473
+ **Recommended order**: `[ttlPlugin, compressionPlugin, encryptionPlugin, syncPlugin, quotaPlugin]`
321
474
 
322
475
  ### Built-in plugins
323
476
 
324
477
  #### TTL plugin
478
+
325
479
  Wraps values as `{ data, expiresAt }`, invalidates stale reads, and optionally runs background cleanup. Options:
326
480
 
327
481
  - `defaultTTL` (ms) and `keyTTL` overrides
328
- - `cleanupInterval` to periodically scan `iterate()` output
482
+ - `cleanupInterval` to periodically scan expired entries
483
+ - `cleanupBatchSize` (default: 100) for efficient batch cleanup
329
484
  - `onExpire(key, value)` callback before removal
330
485
 
486
+ ```ts
487
+ // Cache API responses for 5 minutes
488
+ const cacheStore = localspace.createInstance({
489
+ name: 'api-cache',
490
+ plugins: [
491
+ ttlPlugin({
492
+ defaultTTL: 5 * 60 * 1000, // 5 minutes
493
+ keyTTL: {
494
+ 'user-profile': 30 * 60 * 1000, // 30 minutes for user data
495
+ 'session-token': 60 * 60 * 1000, // 1 hour for session
496
+ },
497
+ cleanupInterval: 60 * 1000, // Cleanup every minute
498
+ cleanupBatchSize: 50, // Process 50 keys at a time
499
+ onExpire: (key, value) => {
500
+ console.log(`Cache expired: ${key}`);
501
+ },
502
+ }),
503
+ ],
504
+ });
505
+
506
+ // Single item and batch operations both respect TTL
507
+ await cacheStore.setItem('user-profile', userData);
508
+ await cacheStore.setItems([
509
+ { key: 'post-1', value: post1 },
510
+ { key: 'post-2', value: post2 },
511
+ ]);
512
+ ```
513
+
331
514
  #### Encryption plugin
515
+
332
516
  Encrypts serialized payloads using the Web Crypto API (AES-GCM by default) and decrypts transparently on reads.
333
517
 
334
518
  - Provide a `key` (CryptoKey/ArrayBuffer/string) or `keyDerivation` block (PBKDF2)
335
519
  - Customize `algorithm`, `ivLength`, `ivGenerator`, or `randomSource`
336
520
  - Works in browsers and modern Node runtimes (pass your own `subtle` when needed)
337
521
 
522
+ ```ts
523
+ // Using a direct key
524
+ const secureStore = localspace.createInstance({
525
+ name: 'secure-store',
526
+ plugins: [
527
+ encryptionPlugin({
528
+ key: '0123456789abcdef0123456789abcdef', // 32 bytes for AES-256
529
+ }),
530
+ ],
531
+ });
532
+
533
+ // Using PBKDF2 key derivation (recommended for password-based encryption)
534
+ const passwordStore = localspace.createInstance({
535
+ name: 'password-store',
536
+ plugins: [
537
+ encryptionPlugin({
538
+ keyDerivation: {
539
+ passphrase: userPassword,
540
+ salt: 'unique-per-user-salt',
541
+ iterations: 150000, // Higher = more secure but slower
542
+ hash: 'SHA-256',
543
+ length: 256,
544
+ },
545
+ }),
546
+ ],
547
+ });
548
+
549
+ // Batch operations are also encrypted
550
+ await secureStore.setItems([
551
+ { key: 'card-number', value: '4111-1111-1111-1111' },
552
+ { key: 'cvv', value: '123' },
553
+ ]);
554
+ ```
555
+
338
556
  #### Compression plugin
557
+
339
558
  Runs LZ-string compression (or a custom codec) when payloads exceed a `threshold` and restores them on read.
340
559
 
341
560
  - `threshold` (bytes) controls when compression kicks in
342
561
  - Supply a custom `{ compress, decompress }` codec if you prefer pako/Brotli
343
562
 
563
+ ```ts
564
+ const compressedStore = localspace.createInstance({
565
+ name: 'compressed-store',
566
+ plugins: [
567
+ compressionPlugin({
568
+ threshold: 1024, // Only compress if > 1KB
569
+ algorithm: 'lz-string', // Label stored in metadata
570
+ }),
571
+ ],
572
+ });
573
+
574
+ // Custom codec example (using pako)
575
+ import pako from 'pako';
576
+
577
+ const pakoStore = localspace.createInstance({
578
+ name: 'pako-store',
579
+ plugins: [
580
+ compressionPlugin({
581
+ threshold: 512,
582
+ algorithm: 'gzip',
583
+ codec: {
584
+ compress: (data) => pako.gzip(data),
585
+ decompress: (data) => pako.ungzip(data, { to: 'string' }),
586
+ },
587
+ }),
588
+ ],
589
+ });
590
+ ```
591
+
344
592
  #### Sync plugin
593
+
345
594
  Keeps multiple tabs/processes in sync via `BroadcastChannel` (with `storage`-event fallback).
346
595
 
347
596
  - `channelName` separates logical buses
348
597
  - `syncKeys` lets you scope which keys broadcast
349
598
  - `conflictStrategy` defaults to `last-write-wins`; provide `onConflict` (return `false` to drop remote writes) for merge logic
350
599
 
600
+ ```ts
601
+ const syncedStore = localspace.createInstance({
602
+ name: 'synced-store',
603
+ plugins: [
604
+ syncPlugin({
605
+ channelName: 'my-app-sync',
606
+ syncKeys: ['cart', 'preferences', 'theme'], // Only sync these keys
607
+ conflictStrategy: 'last-write-wins',
608
+ onConflict: ({ key, localTimestamp, incomingTimestamp, value }) => {
609
+ console.log(`Conflict on ${key}: local=${localTimestamp}, incoming=${incomingTimestamp}`);
610
+ // Return false to reject the incoming change
611
+ return localTimestamp < incomingTimestamp;
612
+ },
613
+ }),
614
+ ],
615
+ });
616
+
617
+ // Changes sync across tabs automatically
618
+ await syncedStore.setItem('cart', { items: [...] });
619
+ await syncedStore.setItems([
620
+ { key: 'preferences', value: { darkMode: true } },
621
+ { key: 'theme', value: 'blue' },
622
+ ]);
623
+ ```
624
+
351
625
  #### Quota plugin
626
+
352
627
  Tracks approximate storage usage after every mutation and enforces limits.
353
628
 
354
- - `maxSize` (bytes) and optional `useNavigatorEstimate` to read the browsers quota
629
+ - `maxSize` (bytes) and optional `useNavigatorEstimate` to read the browser's quota
355
630
  - `evictionPolicy: 'error' | 'lru'` (LRU removes least-recently-used keys automatically)
356
631
  - `onQuotaExceeded(info)` fires before throwing so you can log/alert users
357
632
 
633
+ ```ts
634
+ const quotaStore = localspace.createInstance({
635
+ name: 'quota-store',
636
+ plugins: [
637
+ quotaPlugin({
638
+ maxSize: 5 * 1024 * 1024, // 5 MB
639
+ evictionPolicy: 'lru', // Automatically evict least-recently-used items
640
+ useNavigatorEstimate: true, // Also respect browser quota
641
+ onQuotaExceeded: ({ key, attemptedSize, maxSize, currentUsage }) => {
642
+ console.warn(`Quota exceeded: tried to write ${attemptedSize} bytes`);
643
+ console.warn(`Current usage: ${currentUsage}/${maxSize} bytes`);
644
+ },
645
+ }),
646
+ ],
647
+ });
648
+
649
+ // Batch operations are also quota-checked
650
+ await quotaStore.setItems([
651
+ { key: 'large-1', value: largeData1 },
652
+ { key: 'large-2', value: largeData2 },
653
+ ]); // Throws QUOTA_EXCEEDED if total exceeds limit
654
+ ```
655
+
358
656
  > Tip: place quota plugins last so they see the final payload size after other transformations (TTL, encryption, compression, etc.).
359
657
 
658
+ ### Plugin combination best practices
659
+
660
+ 1. **Recommended plugin order** (from highest to lowest priority):
661
+ ```ts
662
+ plugins: [
663
+ ttlPlugin({ ... }), // priority: 10
664
+ compressionPlugin({ ... }), // priority: 5
665
+ encryptionPlugin({ ... }), // priority: 0
666
+ quotaPlugin({ ... }), // priority: -10
667
+ syncPlugin({ ... }), // priority: -100
668
+ ]
669
+ ```
670
+
671
+ 2. **Always compress before encrypting**: Encrypted data has high entropy and compresses poorly. The default priorities handle this automatically.
672
+
673
+ 3. **Use strict error policy with security-critical plugins**:
674
+ ```ts
675
+ // DON'T do this - encryption failures will be silently swallowed
676
+ const bad = localspace.createInstance({
677
+ plugins: [encryptionPlugin({ key })],
678
+ pluginErrorPolicy: 'lenient', // Dangerous!
679
+ });
680
+
681
+ // DO this - encryption failures will propagate
682
+ const good = localspace.createInstance({
683
+ plugins: [encryptionPlugin({ key })],
684
+ pluginErrorPolicy: 'strict', // Safe (default)
685
+ });
686
+ ```
687
+
688
+ 4. **Batch operations work with all plugins**: All built-in plugins support `setItems`, `getItems`, and `removeItems`.
689
+
690
+ ### Plugin troubleshooting
691
+
692
+ | Issue | Solution |
693
+ |-------|----------|
694
+ | TTL items not expiring | Ensure `cleanupInterval` is set, or read items to trigger expiration |
695
+ | Encryption fails silently | Check `pluginErrorPolicy` is not 'lenient' |
696
+ | Compression not working | Verify payload exceeds `threshold` |
697
+ | Sync not updating other tabs | Check `channelName` matches and `syncKeys` includes your key |
698
+ | Quota errors on small writes | Other plugins (TTL, encryption) add overhead; account for wrapper size |
699
+ | Plugin order seems wrong | Check `priority` values; higher = runs first in `before*` hooks |
700
+
701
+ ## Compatibility & environments
702
+
703
+ - Browsers: modern Chromium/Edge, Firefox, Safari (desktop & iOS). IndexedDB is required for the primary driver; localStorage is available as a fallback.
704
+ - Known differences: Safari private mode / low-quota environments may throw quota; IndexedDB durability hints may be ignored outside Chromium 121+. If you need strict durability, prefer explicit flush/transaction patterns.
705
+ - Node/SSR: browser storage APIs are not available by default; supply a custom driver or guard usage in non-browser contexts.
706
+
707
+ ## Testing & CI
708
+
709
+ - Recommended pipeline: `yarn lint` (if configured) → `yarn vitest run` → `yarn build` → `playwright test`.
710
+ - Regression coverage includes: coalesced writes + pending queue + maxConcurrentTransactions + idle close, plugin error policies (strict/lenient) including batch hooks, compression/encryption/ttl ordering, sync version persistence, localStorage quota handling with rollback.
711
+
712
+ ## Security & performance guidance
713
+
714
+ - Plugin order for correctness/performance: `ttl → compression → encryption → sync → quota`.
715
+ - The encryption plugin provides basic crypto; key management/rotation is your responsibility, and you should not swallow encryption/compression errors via a lenient policy.
716
+ - Run compression before encryption for effectiveness; place quota last to see final sizes; keep sync last in `after*` to broadcast original values.
717
+
360
718
  ## Migration Guide
361
719
 
362
720
  ### Note differences from localForage before upgrading
721
+
363
722
  - `dropInstance()` throws a real `Error` when arguments are invalid. Examine `error.message` instead of comparing string literals.
364
723
  - Blob capability checks run on each request instead of being cached. Cache the result in your application if repeated blob writes dominate your workload.
365
724
  - **WebSQL is intentionally unsupported.** Migrate any WebSQL-only code to IndexedDB or localStorage before switching.
366
725
 
367
726
  ### Enable compatibility mode for driver setup methods
368
- If you maintain older code that expects separate *success* and *error* callbacks for driver setup methods (`setDriver`, `defineDriver`), enable `compatibilityMode` when creating an instance. **Use this mode only for migrations; prefer native Promises going forward.**
727
+
728
+ If you maintain older code that expects separate _success_ and _error_ callbacks for driver setup methods (`setDriver`, `defineDriver`), enable `compatibilityMode` when creating an instance. **Use this mode only for migrations; prefer native Promises going forward.**
369
729
 
370
730
  ```ts
371
731
  const legacy = localspace.createInstance({
@@ -381,7 +741,7 @@ legacy.setDriver(
381
741
  },
382
742
  (error) => {
383
743
  // Error callback receives the Error object only.
384
- },
744
+ }
385
745
  );
386
746
  ```
387
747
 
@@ -398,7 +758,8 @@ localspace.setItem('key', 'value', (err, value) => {
398
758
  ```
399
759
 
400
760
  ## Performance notes
401
- - **Automatic write coalescing (enabled by default):** localspace automatically merges rapid single writes (`setItem`/`removeItem`) within an 8ms window into one transaction, giving you 3-10x performance improvement with zero code changes. This is enabled by default for IndexedDB. Set `coalesceWrites: false` if you need strict per-operation durability.
761
+
762
+ - **Automatic write coalescing (opt-in):** localspace can merge rapid single writes (`setItem`/`removeItem`) within an 8ms window into one transaction for IndexedDB, delivering 3-10x speedups under bursty writes. Enable with `coalesceWrites: true` and see **Advanced: Coalesced Writes** for consistency modes.
402
763
  - **Read-your-writes consistency with coalescing:** Pending coalesced writes are flushed before reads (`getItem`, `getItems`, `iterate`, `keys`, `length`, `key`) and destructive ops (`clear`, `dropInstance`), so immediate reads always observe the latest value. If you need eventual reads for speed, you can switch `coalesceReadConsistency` to `'eventual'`.
403
764
  - **Batch APIs outperform loops:** Playwright benchmark (`test/playwright/benchmark.spec.ts`) on 500 items x 256B showed `setItems()` ~6x faster and `getItems()` ~7.7x faster than per-item loops, with `removeItems()` ~2.8x faster (Chromium, relaxed durability).
404
765
  - **Transaction helpers:** `runTransaction()` lets you co-locate reads/writes in a single transaction for atomic migrations and to shorten lock time.
@@ -406,12 +767,13 @@ localspace.setItem('key', 'value', (err, value) => {
406
767
  - **IndexedDB durability defaults:** Chrome 121+ uses relaxed durability by default; keep it for speed or set `durability: 'strict'` in `config` for migration-style writes.
407
768
  - **Storage Buckets (Chromium 122+):** supply a `bucket` option to isolate critical data and hint durability/persistence per bucket.
408
769
  - **Connection warmup:** IndexedDB instances pre-warm a transaction after init to reduce first-op latency (`prewarmTransactions` enabled by default; set to `false` to skip).
409
- - **Recommended defaults:** keep `coalesceWrites` enabled (default), `durability` relaxed, and `prewarmTransactions` on. Set `connectionIdleMs` only if you want idle connections to auto-close, and `maxBatchSize` only for very large bulk writes. Prefer IndexedDB for atomic/bulk writes since localStorage batches are non-atomic. Use `maxConcurrentTransactions` to throttle heavy parallel workloads when needed.
770
+ - **Recommended defaults:** leave `coalesceWrites` off unless you know you need higher write throughput; if you enable it, prefer the default `strong` consistency. Keep `durability` relaxed and `prewarmTransactions` on. Set `connectionIdleMs` only if you want idle connections to auto-close, and `maxBatchSize` only for very large bulk writes. Prefer IndexedDB for atomic/bulk writes since localStorage batches are non-atomic. Use `maxConcurrentTransactions` to throttle heavy parallel workloads when needed.
410
771
  - **localStorage batch atomicity:** When using localStorage driver, batch operations (`setItems()`, `removeItems()`) are **not atomic**. If an error occurs mid-operation, some items may be written or removed while others are not. In contrast, IndexedDB batch operations use transactions and guarantee atomicity (all-or-nothing). If atomicity is critical for your use case, prefer IndexedDB driver or implement application-level rollback logic.
411
772
 
412
773
  When `compatibilityMode` is off, driver setup methods also use Node-style callbacks. Promises are recommended for all new code.
413
774
 
414
775
  ## Troubleshooting
776
+
415
777
  - **Wait for readiness:** Call `await localspace.ready()` before the first operation when you need to confirm driver selection.
416
778
  - **Inspect drivers:** Use `localspace.driver()` to confirm which driver is active in different environments.
417
779
  - **Read structured errors:** Rejections surface as `LocalSpaceError` with a `code`, contextual `details` (driver, operation, key, attemptedDrivers), and the original `cause`. Branch on `error.code` instead of parsing strings.
@@ -421,4 +783,5 @@ When `compatibilityMode` is off, driver setup methods also use Node-style callba
421
783
  - **Collect combined Vitest + Playwright coverage:** Run `yarn coverage:full` to clean previous artifacts, run `vitest --coverage`, stash its Istanbul JSON into `.nyc_output`, then execute the coverage-enabled Playwright suite and emit merged `nyc` reports.
422
784
 
423
785
  ## License
786
+
424
787
  [MIT](./LICENSE)
@@ -15,7 +15,12 @@ export declare class PluginManager {
15
15
  private readonly destroyed;
16
16
  private readonly disabled;
17
17
  private orderCounter;
18
+ private warningsEmitted;
18
19
  constructor(host: PluginHost, initialPlugins?: LocalSpacePlugin[]);
20
+ /**
21
+ * Validate plugin combinations and emit warnings for potential issues.
22
+ */
23
+ private validatePluginCombinations;
19
24
  hasPlugins(): boolean;
20
25
  registerPlugins(plugins: LocalSpacePlugin[]): void;
21
26
  private sortPlugins;