localspace 1.0.1 → 1.1.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
@@ -8,101 +8,96 @@ localspace — modern storage toolkit that keeps localForage compatibility while
8
8
 
9
9
  ## Motivation
10
10
 
11
- The industry still leans on localForages familiar API, yet modern apps crave stronger typing, async ergonomics, and multi-platform reliability without a painful rewrite. localspace exists to bridge that gap: it honors the old contract while delivering the capabilities teams have been asking for since 2021.
11
+ The industry still leans on localForage's familiar API, yet modern apps crave stronger typing, async ergonomics, and multi-platform reliability without a painful rewrite. localspace exists to bridge that gap: it honors the old contract while delivering first-class TypeScript types, native async/await, reliable IndexedDB cleanup, and a clean driver architecture.
12
12
 
13
- ### What needed to change
13
+ **Why rebuild instead of fork?** Starting fresh let us eliminate technical debt while maintaining API compatibility. Teams can migrate from localForage without changing application code, then unlock better developer experience and future extensibility.
14
14
 
15
- localForage’s storage layer stopped evolving while real-world needs kept growing. Long-standing requests—first-class TypeScript types, native async/await, reliable IndexedDB cleanup, consistency across Node and React Native, batch operations, TTL, and encryption—remain unresolved. Teams want those upgrades without abandoning the API that already powers their products.
15
+ ## Quick Start
16
16
 
17
- ### How localspace responds
17
+ Get started in 5 minutes:
18
18
 
19
- We stay 100% compatible with localForage on the surface, but rebuild the internals with modern JavaScript, a TypeScript-first type system, native Promises, and a clean driver architecture. That drop-in approach delivers predictable behavior (including a complete IndexedDB `dropInstance`), clearer diagnostics, and room to grow with new drivers (Cache API, SQLite, OPFS) and optional plugins (TTL, encryption, compression) across browsers, Node, React Native, and Electron. Our goal is a storage toolkit that preserves your investment in the localForage mental model while finally addressing the community’s accumulated pain points.
19
+ ### 1. Install
20
20
 
21
- ### Why rebuild instead of fork?
21
+ ```bash
22
+ npm install localspace
23
+ # or: yarn add localspace / pnpm add localspace
24
+ ```
22
25
 
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.
26
+ ### 2. Basic Usage
24
27
 
25
- ## Table of Contents
26
-
27
- - [Motivation](#motivation)
28
- - [What needed to change](#what-needed-to-change)
29
- - [How localspace responds](#how-localspace-responds)
30
- - [Why rebuild instead of fork?](#why-rebuild-instead-of-fork)
31
- - [Roadmap](#roadmap)
32
- - [Installation and Usage](#installation-and-usage)
33
- - [localspace delivers modern storage compatibility](#localspace-delivers-modern-storage-compatibility)
34
- - [Install and import localspace](#install-and-import-localspace)
35
- - [Store data with async flows or callbacks](#store-data-with-async-flows-or-callbacks)
36
- - [Configure isolated stores for clear data boundaries](#configure-isolated-stores-for-clear-data-boundaries)
37
- - [Choose drivers with predictable fallbacks](#choose-drivers-with-predictable-fallbacks)
38
- - [Handle binary data across browsers](#handle-binary-data-across-browsers)
39
- - [Advanced: Coalesced Writes (IndexedDB only)](#advanced-coalesced-writes-indexeddb-only)
40
- - [Migration Guide](#migration-guide)
41
- - [Note differences from localForage before upgrading](#note-differences-from-localforage-before-upgrading)
42
- - [Enable compatibility mode for legacy callbacks](#enable-compatibility-mode-for-legacy-callbacks)
43
- - [Troubleshooting](#troubleshooting)
44
- - [License](#license)
28
+ ```ts
29
+ import localspace from 'localspace';
45
30
 
46
- ## Roadmap
31
+ // Store and retrieve data
32
+ await localspace.setItem('user', { name: 'Ada', role: 'admin' });
33
+ const user = await localspace.getItem<{ name: string; role: string }>('user');
47
34
 
48
- localspace is built on a foundation designed for growth. Here's what's planned:
35
+ // TypeScript generics for type safety
36
+ interface User {
37
+ name: string;
38
+ role: string;
39
+ }
40
+ const typedUser = await localspace.getItem<User>('user');
41
+ ```
49
42
 
50
- ### Core Compatibility (Complete)
43
+ ### 3. Create Isolated Instances
51
44
 
52
- - [x] IndexedDB and localStorage drivers
53
- - [x] Full localForage API parity
54
- - [x] TypeScript-first implementation
55
- - [x] Comprehensive test coverage
56
- - [x] Modern build pipeline (ES modules, CommonJS, UMD)
57
- - [x] Batch operations (`setItems()`, `getItems()`, `removeItems()`) for higher throughput
58
- - [x] Automatic write coalescing (3-10x faster rapid writes, opt-in for IndexedDB)
59
- - [x] Connection pooling, transaction batching, and warmup
60
- - [x] **Improved error handling** - Structured error types with detailed context
45
+ ```ts
46
+ const cache = localspace.createInstance({
47
+ name: 'my-app',
48
+ storeName: 'cache',
49
+ });
61
50
 
62
- ### TODO
51
+ await cache.setItem('token', 'abc123');
52
+ ```
63
53
 
64
- - [x] **Plugin system** - Middleware architecture for cross-cutting concerns
65
- - [ ] **OPFS driver** - Origin Private File System for high-performance file storage
66
- - [ ] **Custom driver templates** - Documentation and examples for third-party drivers
67
- - [ ] **Node.js** - File system and SQLite adapters
68
- - [ ] **React Native** - AsyncStorage and SQLite drivers
69
- - [ ] **Electron** - Main and renderer process coordination
70
- - [ ] **Deno** - Native KV store integration
71
- - [x] **TTL plugin** - Time-to-live expiration with automatic cleanup
72
- - [x] **Encryption plugin** - Transparent encryption/decryption with Web Crypto API
73
- - [x] **Compression plugin** - LZ-string or Brotli compression for large values
74
- - [x] **Sync plugin** - Multi-tab synchronization with BroadcastChannel
75
- - [x] **Quota plugin** - Automatic quota management and cleanup strategies
54
+ ### 4. Batch Operations
76
55
 
77
- ### 📊 Community Priorities
56
+ ```ts
57
+ // Write multiple items in one transaction (IndexedDB)
58
+ await localspace.setItems([
59
+ { key: 'user:1', value: { name: 'Ada' } },
60
+ { key: 'user:2', value: { name: 'Grace' } },
61
+ ]);
78
62
 
79
- We prioritize features based on community feedback. If you need a specific capability:
63
+ // Read multiple items
64
+ const users = await localspace.getItems(['user:1', 'user:2']);
65
+ ```
80
66
 
81
- 1. **Check existing issues** to see if it's already requested
82
- 2. **Open a feature request** with your use case and requirements
83
- 3. **Contribute** - We welcome PRs for new drivers, plugins, or improvements
67
+ ### 5. Use Plugins
84
68
 
85
- **Want to help?** The most impactful contributions right now:
69
+ ```ts
70
+ import localspace, { ttlPlugin, encryptionPlugin } from 'localspace';
86
71
 
87
- - Testing in diverse environments (browsers, frameworks, edge cases)
88
- - Documentation improvements and usage examples
89
- - Performance benchmarks and optimization suggestions
90
- - New driver implementations (especially Cache API and OPFS)
72
+ const secureStore = localspace.createInstance({
73
+ name: 'secure',
74
+ plugins: [
75
+ ttlPlugin({ defaultTTL: 60_000 }), // Auto-expire after 1 minute
76
+ encryptionPlugin({ key: 'your-32-byte-key' }), // Encrypt data
77
+ ],
78
+ });
79
+ ```
91
80
 
92
- ## Installation and Usage
81
+ That's it! For more details, see the sections below.
93
82
 
94
- ### localspace delivers modern storage compatibility
83
+ ---
95
84
 
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.**
85
+ ## Table of Contents
97
86
 
98
- - Promise-first API with optional callbacks
99
- - IndexedDB and localStorage drivers included out of the box
100
- - ES module, CommonJS, and UMD bundles plus `.d.ts` files
101
- - Drop-in TypeScript generics for value typing
87
+ - [Installation](#installation)
88
+ - [Core API](#core-api)
89
+ - [Batch Operations](#batch-operations)
90
+ - [Coalesced Writes](#coalesced-writes)
91
+ - [Plugin System](#plugin-system)
92
+ - [Configuration](#configuration)
93
+ - [Performance Notes](#performance-notes)
94
+ - [Troubleshooting](#troubleshooting)
95
+ - [Documentation](#documentation)
96
+ - [License](#license)
102
97
 
103
- ### Install and import localspace
98
+ ---
104
99
 
105
- Install the package with your preferred package manager and import it once at the entry point where you manage storage.
100
+ ## Installation
106
101
 
107
102
  ```bash
108
103
  npm install localspace
@@ -116,673 +111,307 @@ pnpm add localspace
116
111
  import localspace from 'localspace';
117
112
  ```
118
113
 
119
- ### Store data with async flows or callbacks
120
-
121
- Use async/await for the clearest flow. **Callbacks remain supported for parity with existing localForage codebases.**
122
-
123
- ```ts
124
- await localspace.setItem('user', { name: 'Ada', role: 'admin' });
125
- const user = await localspace.getItem<{ name: string; role: string }>('user');
114
+ **Bundles included:** ES modules, CommonJS, UMD, plus `.d.ts` files.
126
115
 
127
- localspace.getItem('user', (error, value) => {
128
- if (error) return console.error(error);
129
- console.log(value?.name);
130
- });
131
- ```
116
+ ---
132
117
 
133
- ### 🚀 Opt into automatic performance optimization (coalesced writes)
118
+ ## Core API
134
119
 
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.
120
+ ### Storage Methods
136
121
 
137
122
  ```ts
138
- // Your existing code - unchanged
139
- await Promise.all([
140
- localspace.setItem('setting1', value1),
141
- localspace.setItem('setting2', value2),
142
- localspace.setItem('setting3', value3),
143
- ]);
144
- // ✅ Automatically batched into one transaction!
145
- // ✅ 3-10x faster than individual commits
146
- // ✅ Zero code changes required
147
- ```
123
+ // Set and get items
124
+ await localspace.setItem('key', value);
125
+ const value = await localspace.getItem<T>('key');
148
126
 
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.
127
+ // Remove items
128
+ await localspace.removeItem('key');
129
+ await localspace.clear();
150
130
 
151
- **Turn it on or tune it**
131
+ // Query
132
+ const count = await localspace.length();
133
+ const keys = await localspace.keys();
152
134
 
153
- ```ts
154
- const instance = localspace.createInstance({
155
- coalesceWrites: true, // opt-in (default is false)
156
- coalesceWindowMs: 8, // 8ms window (default)
135
+ // Iterate
136
+ await localspace.iterate<T, void>((value, key, index) => {
137
+ console.log(key, value);
157
138
  });
158
139
  ```
159
140
 
160
- For consistency modes, batch limits, and failure semantics, see **Advanced: Coalesced Writes** below.
161
-
162
- **When is this useful?**
163
-
164
- - Form auto-save that writes multiple fields rapidly
165
- - Bulk state synchronization loops
166
- - Real-time collaborative editing
167
- - Any code with multiple sequential `setItem()` calls
168
-
169
- **Performance impact**: Single infrequent writes are unaffected. Rapid sequential writes get 3-10x faster automatically.
170
-
171
- **Want to see the actual performance gains?**
141
+ ### Callbacks (Legacy Support)
172
142
 
173
143
  ```ts
174
- // Get statistics to see how much coalescing helped (IndexedDB only)
175
- const stats = localspace.getPerformanceStats?.();
176
- console.log(stats);
177
- // {
178
- // totalWrites: 150, // Total write operations
179
- // coalescedWrites: 120, // Operations that were merged
180
- // transactionsSaved: 100, // Transactions saved by coalescing
181
- // avgCoalesceSize: 4.8 // Average batch size
182
- // }
144
+ localspace.getItem('user', (error, value) => {
145
+ if (error) return console.error(error);
146
+ console.log(value);
147
+ });
183
148
  ```
184
149
 
185
- ### Boost throughput with batch operations
186
-
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).
150
+ ### Driver Selection
188
151
 
189
152
  ```ts
190
- const items = [
191
- { key: 'user:1', value: { name: 'Ada' } },
192
- { key: 'user:2', value: { name: 'Lin' } },
193
- ];
194
-
195
- // Single transaction write
196
- await localspace.setItems(items);
197
-
198
- // Ordered bulk read
199
- const result = await localspace.getItems(items.map((item) => item.key));
200
- console.log(result); // [{ key: 'user:1', value: {…} }, { key: 'user:2', value: {…} }]
201
-
202
- // Single transaction delete
203
- await localspace.removeItems(items.map((item) => item.key));
204
-
205
- // For very large batches, set a chunk size to avoid huge transactions
206
- const limited = localspace.createInstance({ maxBatchSize: 200 });
207
- await limited.setDriver([limited.INDEXEDDB]);
208
- await limited.setItems(items); // will split into 200-item chunks
209
-
210
- // Optional: coalesce rapid single writes into one transaction (IndexedDB)
211
- const coalesced = localspace.createInstance({
212
- coalesceWrites: true,
213
- coalesceWindowMs: 8,
214
- });
215
- await coalesced.setDriver([coalesced.INDEXEDDB]);
216
- await Promise.all([
217
- coalesced.setItem('fast-1', 'a'),
218
- coalesced.setItem('fast-2', 'b'),
219
- ]); // batched into one tx within the window
220
-
221
- // These features work independently and can be combined
222
- const optimized = localspace.createInstance({
223
- coalesceWrites: true, // optimizes single-item writes (setItem/removeItem)
224
- coalesceWindowMs: 8,
225
- maxBatchSize: 200, // limits batch API chunk size (setItems/removeItems)
226
- });
227
- await optimized.setDriver([optimized.INDEXEDDB]);
153
+ // Web fallback order (default bundled drivers)
154
+ await localspace.setDriver([localspace.INDEXEDDB, localspace.LOCALSTORAGE]);
228
155
 
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.
156
+ // Check current driver
157
+ console.log(localspace.driver());
158
+ // 'asyncStorage' | 'localStorageWrapper'
234
159
  ```
235
160
 
236
- ### Run your own transaction
237
-
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.
161
+ ### React Native AsyncStorage
239
162
 
240
163
  ```ts
241
- await localspace.setDriver([localspace.INDEXEDDB]);
242
- await localspace.runTransaction('readwrite', async (tx) => {
243
- const current = await tx.get<number>('counter');
244
- const next = (current ?? 0) + 1;
245
- await tx.set('counter', next);
246
- await tx.set('lastUpdated', Date.now());
247
- });
248
- ```
249
-
250
- ### Configure isolated stores for clear data boundaries
251
-
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.
164
+ import AsyncStorage from '@react-native-async-storage/async-storage';
165
+ import localspace from 'localspace';
166
+ import { createReactNativeInstance } from 'localspace/react-native';
253
167
 
254
- ```ts
255
- const sessionCache = localspace.createInstance({
256
- name: 'session',
257
- storeName: 'volatile-items',
168
+ const mobileStore = await createReactNativeInstance(localspace, {
169
+ name: 'myapp',
170
+ storeName: 'kv',
171
+ reactNativeAsyncStorage: AsyncStorage,
258
172
  });
259
-
260
- await sessionCache.setItem('token', 'abc123');
261
173
  ```
262
174
 
263
- ### Choose drivers with predictable fallbacks
175
+ The default `localspace` entry does not bundle the React Native driver; it is included only when importing `localspace/react-native`. Explicit `reactNativeAsyncStorage` injection is recommended.
264
176
 
265
- By default, localspace prefers IndexedDB (`INDEXEDDB`) and falls back to localStorage (`LOCALSTORAGE`). Configure alternative sequences as needed.
177
+ Advanced usage is still available:
266
178
 
267
179
  ```ts
268
- await localspace.setDriver([localspace.INDEXEDDB, localspace.LOCALSTORAGE]);
269
-
270
- if (!localspace.supports(localspace.INDEXEDDB)) {
271
- console.warn('IndexedDB unavailable, using localStorage wrapper.');
272
- }
180
+ import localspace from 'localspace';
181
+ import { installReactNativeAsyncStorageDriver } from 'localspace/react-native';
273
182
 
274
- // Hint IndexedDB durability (Chrome defaults to "relaxed" from 121+)
275
- await localspace.setDriver([localspace.INDEXEDDB]);
276
- await localspace.ready();
277
- // Global durability hint for this instance
278
- localspace.config({ durability: 'strict' }); // or omit to stay relaxed for speed
279
-
280
- // Use Storage Buckets (Chromium 122+) to isolate data and hints
281
- const bucketed = localspace.createInstance({
282
- name: 'mail-cache',
283
- storeName: 'drafts',
284
- bucket: { name: 'drafts', durability: 'strict', persisted: true },
285
- });
286
- await bucketed.setDriver([bucketed.INDEXEDDB]);
183
+ await installReactNativeAsyncStorageDriver(localspace);
184
+ await localspace.setDriver(localspace.REACTNATIVEASYNCSTORAGE);
287
185
  ```
288
186
 
289
- **Tip:** Use `defineDriver()` and `getDriver()` to register custom drivers that match the localForage interface.
187
+ Integration smoke (official AsyncStorage Jest mock):
290
188
 
291
- ### Handle binary data across browsers
292
-
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.
294
-
295
- ```ts
296
- const file = new Blob(['hello'], { type: 'text/plain' });
297
- await localspace.setItem('file', file);
298
- const restored = await localspace.getItem<Blob>('file');
189
+ ```bash
190
+ yarn test:rn:integration
299
191
  ```
300
192
 
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.
193
+ See `integration/react-native-jest/README.md` for details.
304
194
 
305
- > `coalesceWrites` defaults to `false` so behavior stays predictable. Turn it on when you expect high-frequency writes.
195
+ GitHub Actions Detox workflow template (real simulator/emulator runtime):
306
196
 
307
- ### Why coalesce writes?
197
+ - Workflow: `.github/workflows/detox-mobile.yml`
198
+ - Fixture app folder: `integration/react-native-detox/`
199
+ - Fixture README: `integration/react-native-detox/README.md`
308
200
 
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:
201
+ 📖 **Full API Reference:** [docs/api-reference.md](./docs/api-reference.md)
310
202
 
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.
203
+ ---
314
204
 
315
- ### Configuration
205
+ ## Batch Operations
316
206
 
317
- Relevant `LocalSpaceConfig` fields:
207
+ Use batch APIs for better performance with IndexedDB:
318
208
 
319
209
  ```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.
210
+ // Single transaction write
211
+ await localspace.setItems([
212
+ { key: 'user:1', value: { name: 'Ada' } },
213
+ { key: 'user:2', value: { name: 'Lin' } },
214
+ ]);
357
215
 
358
- #### `coalesceReadConsistency: 'eventual'`
216
+ // Ordered bulk read
217
+ const result = await localspace.getItems(['user:1', 'user:2']);
218
+ // [{ key: 'user:1', value: {...} }, { key: 'user:2', value: {...} }]
359
219
 
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`.
220
+ // Single transaction delete
221
+ await localspace.removeItems(['user:1', 'user:2']);
222
+ ```
363
223
 
364
- Use this for logs/analytics or workloads that can tolerate short windows of staleness in exchange for the lightest write path.
224
+ ### Transactions
365
225
 
366
- ### Bounding batch size
226
+ For atomic multi-step operations:
367
227
 
368
228
  ```ts
369
- const store = localspace.createInstance({
370
- name: 'logs',
371
- storeName: 'events',
372
- coalesceWrites: true,
373
- coalesceWindowMs: 8,
374
- coalesceMaxBatchSize: 64,
375
- coalesceReadConsistency: 'eventual',
229
+ await localspace.runTransaction('readwrite', async (tx) => {
230
+ const current = (await tx.get<number>('counter')) ?? 0;
231
+ await tx.set('counter', current + 1);
232
+ await tx.set('lastUpdated', Date.now());
376
233
  });
377
234
  ```
378
235
 
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.
236
+ ---
382
237
 
383
- ### Recommended recipes
238
+ ## Coalesced Writes
384
239
 
385
- 1. Default: coalescing off
240
+ Opt-in automatic batching of rapid writes for **3-10x performance improvement**:
386
241
 
387
242
  ```ts
388
243
  const store = localspace.createInstance({
389
- name: 'app',
390
- storeName: 'keyvaluepairs',
391
- // coalesceWrites is false by default
244
+ coalesceWrites: true, // Enable (default: false)
245
+ coalesceWindowMs: 8, // 8ms merge window
392
246
  });
393
- ```
394
247
 
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
- });
248
+ // These are automatically batched into one transaction
249
+ await Promise.all([
250
+ store.setItem('a', 1),
251
+ store.setItem('b', 2),
252
+ store.setItem('c', 3),
253
+ ]);
406
254
  ```
407
255
 
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.
256
+ **Consistency modes:**
411
257
 
412
- 3. Strong consistency with bounded batches
258
+ - `'strong'` (default): Reads flush pending writes first
259
+ - `'eventual'`: Reads may see stale values briefly
413
260
 
414
261
  ```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
- });
262
+ // Get performance stats
263
+ const stats = localspace.getPerformanceStats?.();
264
+ // { totalWrites: 150, coalescedWrites: 120, transactionsSaved: 100 }
423
265
  ```
424
266
 
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.
267
+ ---
434
268
 
435
269
  ## Plugin System
436
270
 
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.
271
+ localspace ships with a powerful plugin engine:
438
272
 
439
273
  ```ts
274
+ import localspace, {
275
+ ttlPlugin,
276
+ compressionPlugin,
277
+ encryptionPlugin,
278
+ syncPlugin,
279
+ quotaPlugin,
280
+ } from 'localspace';
281
+
440
282
  const store = localspace.createInstance({
441
283
  name: 'secure-store',
442
- storeName: 'primary',
443
284
  plugins: [
444
- ttlPlugin({ defaultTTL: 60_000 }),
445
- compressionPlugin({ threshold: 1024 }),
446
- encryptionPlugin({ key: '0123456789abcdef0123456789abcdef' }),
447
- syncPlugin({ channelName: 'localspace-sync' }),
448
- quotaPlugin({ maxSize: 5 * 1024 * 1024, evictionPolicy: 'lru' }),
285
+ ttlPlugin({ defaultTTL: 60_000 }), // Auto-expire
286
+ compressionPlugin({ threshold: 1024 }), // Compress > 1KB
287
+ encryptionPlugin({ key: '32-byte-key-here' }), // Encrypt
288
+ syncPlugin({ channelName: 'my-app' }), // Multi-tab sync
289
+ quotaPlugin({ maxSize: 5 * 1024 * 1024 }), // 5MB limit
449
290
  ],
291
+ pluginErrorPolicy: 'strict', // Recommended for encryption
450
292
  });
451
293
  ```
452
294
 
453
- ### Lifecycle and hooks
454
-
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.
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.
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 (TTL → compression → encryption) remain invertible. Returning a value passes it to the next plugin, while throwing a `LocalSpaceError` aborts the operation.
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.
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: 'lenient'` reports and continues; use `strict` for encryption/compression/ttl or any correctness-critical plugin.
460
-
461
- ### Plugin execution order
462
-
463
- Plugins are sorted by `priority` (higher runs first in `before*`, last in `after*`). Default priorities:
464
-
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 |
295
+ ### Built-in Plugins
472
296
 
473
- **Recommended order**: `[ttlPlugin, compressionPlugin, encryptionPlugin, syncPlugin, quotaPlugin]`
297
+ | Plugin | Purpose |
298
+ | --------------- | ---------------------------------------------------- |
299
+ | **TTL** | Auto-expire items with `{ data, expiresAt }` wrapper |
300
+ | **Encryption** | AES-GCM encryption via Web Crypto API |
301
+ | **Compression** | LZ-string compression for large values |
302
+ | **Sync** | Multi-tab synchronization via BroadcastChannel |
303
+ | **Quota** | Storage limit enforcement with LRU eviction |
474
304
 
475
- ### Built-in plugins
305
+ 📖 **Full Plugin Documentation:** [docs/plugins.md](./docs/plugins.md)
306
+ 📖 **Real-World Examples:** [docs/examples.md](./docs/examples.md)
476
307
 
477
- #### TTL plugin
308
+ ---
478
309
 
479
- Wraps values as `{ data, expiresAt }`, invalidates stale reads, and optionally runs background cleanup. Options:
480
-
481
- - `defaultTTL` (ms) and `keyTTL` overrides
482
- - `cleanupInterval` to periodically scan expired entries
483
- - `cleanupBatchSize` (default: 100) for efficient batch cleanup
484
- - `onExpire(key, value)` callback before removal
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
-
514
- #### Encryption plugin
515
-
516
- Encrypts serialized payloads using the Web Crypto API (AES-GCM by default) and decrypts transparently on reads.
517
-
518
- - Provide a `key` (CryptoKey/ArrayBuffer/string) or `keyDerivation` block (PBKDF2)
519
- - Customize `algorithm`, `ivLength`, `ivGenerator`, or `randomSource`
520
- - Works in browsers and modern Node runtimes (pass your own `subtle` when needed)
310
+ ## Configuration
521
311
 
522
312
  ```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
-
556
- #### Compression plugin
557
-
558
- Runs LZ-string compression (or a custom codec) when payloads exceed a `threshold` and restores them on read.
313
+ const store = localspace.createInstance({
314
+ // Database
315
+ name: 'myapp', // Database name
316
+ storeName: 'data', // Store name
317
+ version: 1, // Schema version
559
318
 
560
- - `threshold` (bytes) controls when compression kicks in
561
- - Supply a custom `{ compress, decompress }` codec if you prefer pako/Brotli
319
+ // Driver
320
+ driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE],
562
321
 
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
- });
322
+ // IndexedDB performance
323
+ durability: 'relaxed', // 'relaxed' (fast) or 'strict'
324
+ prewarmTransactions: true, // Pre-warm connection
573
325
 
574
- // Custom codec example (using pako)
575
- import pako from 'pako';
326
+ // Batching
327
+ maxBatchSize: 200, // Split large batches
328
+ coalesceWrites: false, // Merge rapid writes
576
329
 
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
- ],
330
+ // Plugins
331
+ plugins: [],
332
+ pluginErrorPolicy: 'lenient', // 'strict' for encryption
589
333
  });
590
334
  ```
591
335
 
592
- #### Sync plugin
593
-
594
- Keeps multiple tabs/processes in sync via `BroadcastChannel` (with `storage`-event fallback).
595
-
596
- - `channelName` separates logical buses
597
- - `syncKeys` lets you scope which keys broadcast
598
- - `conflictStrategy` defaults to `last-write-wins`; provide `onConflict` (return `false` to drop remote writes) for merge logic
599
-
600
336
  ```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
-
625
- #### Quota plugin
626
-
627
- Tracks approximate storage usage after every mutation and enforces limits.
628
-
629
- - `maxSize` (bytes) and optional `useNavigatorEstimate` to read the browser's quota
630
- - `evictionPolicy: 'error' | 'lru'` (LRU removes least-recently-used keys automatically)
631
- - `onQuotaExceeded(info)` fires before throwing so you can log/alert users
337
+ // React Native one-step instance
338
+ import localspace from 'localspace';
339
+ import { createReactNativeInstance } from 'localspace/react-native';
632
340
 
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
- ],
341
+ const mobileStore = await createReactNativeInstance(localspace, {
342
+ name: 'myapp',
343
+ storeName: 'data',
344
+ reactNativeAsyncStorage: AsyncStorage,
647
345
  });
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
346
  ```
655
347
 
656
- > Tip: place quota plugins last so they see the final payload size after other transformations (TTL, encryption, compression, etc.).
657
-
658
- ### Plugin combination best practices
659
-
660
- 1. **Recommended plugin order** (from highest to lowest priority):
661
-
662
- ```ts
663
- plugins: [
664
- ttlPlugin({ ... }), // priority: 10
665
- compressionPlugin({ ... }), // priority: 5
666
- encryptionPlugin({ ... }), // priority: 0
667
- quotaPlugin({ ... }), // priority: -10
668
- syncPlugin({ ... }), // priority: -100
669
- ]
670
- ```
671
-
672
- 2. **Always compress before encrypting**: Encrypted data has high entropy and compresses poorly. The default priorities handle this automatically.
673
-
674
- 3. **Use strict error policy with security-critical plugins** (default is lenient):
675
-
676
- ```ts
677
- // DON'T do this - encryption failures will be silently swallowed
678
- const bad = localspace.createInstance({
679
- plugins: [encryptionPlugin({ key })],
680
- pluginErrorPolicy: 'lenient', // Dangerous!
681
- });
682
-
683
- // DO this - encryption failures will propagate
684
- const good = localspace.createInstance({
685
- plugins: [encryptionPlugin({ key })],
686
- pluginErrorPolicy: 'strict', // Safe (recommended)
687
- });
688
- ```
689
-
690
- 4. **Batch operations work with all plugins**: All built-in plugins support `setItems`, `getItems`, and `removeItems`.
691
-
692
- ### Plugin troubleshooting
693
-
694
- | Issue | Solution |
695
- | ---------------------------- | ---------------------------------------------------------------------- |
696
- | TTL items not expiring | Ensure `cleanupInterval` is set, or read items to trigger expiration |
697
- | Encryption fails silently | Set `pluginErrorPolicy: 'strict'` for encryption/compression/ttl |
698
- | Compression not working | Verify payload exceeds `threshold` |
699
- | Sync not updating other tabs | Check `channelName` matches and `syncKeys` includes your key |
700
- | Quota errors on small writes | Other plugins (TTL, encryption) add overhead; account for wrapper size |
701
- | Plugin order seems wrong | Check `priority` values; higher = runs first in `before*` hooks |
348
+ 📖 **Full Configuration Options:** [docs/api-reference.md#configuration-options](./docs/api-reference.md#configuration-options)
702
349
 
703
- ## Compatibility & environments
350
+ ---
704
351
 
705
- - Browsers: modern Chromium/Edge, Firefox, Safari (desktop & iOS). IndexedDB is required for the primary driver; localStorage is available as a fallback.
706
- - 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.
707
- - Node/SSR: browser storage APIs are not available by default; supply a custom driver or guard usage in non-browser contexts.
352
+ ## Performance Notes
708
353
 
709
- ## Testing & CI
354
+ - **Batch APIs outperform loops:** `setItems()` ~6x faster, `getItems()` ~7.7x faster than per-item loops
355
+ - **Coalesced writes:** 3-10x faster under write bursts (opt-in)
356
+ - **Transaction helpers:** `runTransaction()` for atomic migrations
357
+ - **IndexedDB durability:** Chrome 121+ uses relaxed durability by default
358
+ - **localStorage batches are non-atomic:** Prefer IndexedDB for atomic operations
710
359
 
711
- - Recommended pipeline: `yarn lint` (if configured) → `yarn vitest run` → `yarn build` → `playwright test`.
712
- - 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.
360
+ ---
713
361
 
714
- ## Security & performance guidance
362
+ ## Troubleshooting
715
363
 
716
- - Plugin order for correctness/performance: `ttl → compression → encryption → sync → quota`.
717
- - The encryption plugin provides basic crypto; key management/rotation is your responsibility, and you should not swallow encryption/compression errors via a lenient policy.
718
- - Run compression before encryption for effectiveness; place quota last to see final sizes; keep sync last in `after*` to broadcast original values.
364
+ | Issue | Solution |
365
+ | --------------------------- | ------------------------------------------------------ |
366
+ | Driver not ready | Call `await localspace.ready()` before first operation |
367
+ | Quota errors | Check `error.code === 'QUOTA_EXCEEDED'` |
368
+ | Plugin errors swallowed | Set `pluginErrorPolicy: 'strict'` |
369
+ | Stale reads with coalescing | Use `coalesceReadConsistency: 'strong'` (default) |
719
370
 
720
- ## Migration Guide
371
+ **Errors** are `LocalSpaceError` with `code`, `details`, and `cause` properties.
721
372
 
722
- ### Note differences from localForage before upgrading
373
+ ---
723
374
 
724
- - `dropInstance()` throws a real `Error` when arguments are invalid. Examine `error.message` instead of comparing string literals.
725
- - Blob capability checks run on each request instead of being cached. Cache the result in your application if repeated blob writes dominate your workload.
726
- - **WebSQL is intentionally unsupported.** Migrate any WebSQL-only code to IndexedDB or localStorage before switching.
375
+ ## Compatibility
727
376
 
728
- ### Enable compatibility mode for driver setup methods
377
+ - **Browsers:** Modern Chromium/Edge, Firefox, Safari
378
+ - **Drivers:** IndexedDB (primary), localStorage
379
+ - **React Native:** AsyncStorage driver available via `localspace/react-native` opt-in entry
380
+ - **WebSQL:** Not supported (migrate to IndexedDB)
381
+ - **Node/SSR:** Custom driver required
729
382
 
730
- 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.**
383
+ ---
731
384
 
732
- ```ts
733
- const legacy = localspace.createInstance({
734
- name: 'legacy-store',
735
- storeName: 'pairs',
736
- compatibilityMode: true,
737
- });
385
+ ## Documentation
738
386
 
739
- legacy.setDriver(
740
- [legacy.LOCALSTORAGE],
741
- () => {
742
- // Success callback receives no arguments.
743
- },
744
- (error) => {
745
- // Error callback receives the Error object only.
746
- }
747
- );
748
- ```
387
+ | Document | Description |
388
+ | -------------------------------------------- | ------------------------------------- |
389
+ | [API Reference](./docs/api-reference.md) | Complete method documentation |
390
+ | [Plugin System](./docs/plugins.md) | Built-in plugins & custom development |
391
+ | [Real-World Examples](./docs/examples.md) | Production-ready code patterns |
392
+ | [Migration Guide](./docs/migration-guide.md) | Upgrading from localForage |
749
393
 
750
- **Note:** Storage methods like `setItem`, `getItem`, `removeItem`, etc. always use Node-style `(error, value)` callbacks regardless of `compatibilityMode`. This matches localForage's original behavior. For example:
394
+ ---
751
395
 
752
- ```ts
753
- localspace.setItem('key', 'value', (err, value) => {
754
- if (err) {
755
- console.error('Error:', err);
756
- } else {
757
- console.log('Saved:', value);
758
- }
759
- });
760
- ```
396
+ ## Roadmap
761
397
 
762
- ## Performance notes
398
+ ### Complete
763
399
 
764
- - **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.
765
- - **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'`.
766
- - **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).
767
- - **Transaction helpers:** `runTransaction()` lets you co-locate reads/writes in a single transaction for atomic migrations and to shorten lock time.
768
- - **Batch sizing:** Use `maxBatchSize` to split very large batch operations (`setItems`/`removeItems`/`getItems`) and keep transaction size in check. This works independently from `coalesceWrites`, which optimizes single-item operations.
769
- - **IndexedDB durability defaults:** Chrome 121+ uses relaxed durability by default; keep it for speed or set `durability: 'strict'` in `config` for migration-style writes.
770
- - **Storage Buckets (Chromium 122+):** supply a `bucket` option to isolate critical data and hint durability/persistence per bucket.
771
- - **Connection warmup:** IndexedDB instances pre-warm a transaction after init to reduce first-op latency (`prewarmTransactions` enabled by default; set to `false` to skip).
772
- - **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.
773
- - **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.
400
+ - [x] IndexedDB and localStorage drivers
401
+ - [x] React Native AsyncStorage driver
402
+ - [x] Full localForage API parity
403
+ - [x] TypeScript-first implementation
404
+ - [x] Batch operations & write coalescing
405
+ - [x] Plugin system (TTL, Encryption, Compression, Sync, Quota)
774
406
 
775
- When `compatibilityMode` is off, driver setup methods also use Node-style callbacks. Promises are recommended for all new code.
407
+ ### Coming Soon
776
408
 
777
- ## Troubleshooting
409
+ - [ ] OPFS driver (Origin Private File System)
410
+ - [ ] Node.js (File system, SQLite)
411
+ - [ ] React Native SQLite driver
412
+ - [ ] Deno (Native KV store)
778
413
 
779
- - **Wait for readiness:** Call `await localspace.ready()` before the first operation when you need to confirm driver selection.
780
- - **Inspect drivers:** Use `localspace.driver()` to confirm which driver is active in different environments.
781
- - **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.
782
- - **Handle quota errors:** Check for `error.code === 'QUOTA_EXCEEDED'` (or inspect `error.cause`) from `setItem` to inform users about storage limits.
783
- - **Run unit tests:** The project ships with Vitest and Playwright suites covering API behavior; run `yarn test` to verify changes.
784
- - **Collect Playwright coverage:** Run `yarn test:e2e:coverage` to re-build the bundle, execute the Playwright suite with Chromium V8 coverage enabled, and emit both text + HTML reports via `nyc` (open `coverage/index.html` after the run; raw JSON sits in `.nyc_output`).
785
- - **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.
414
+ ---
786
415
 
787
416
  ## License
788
417