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 +251 -622
- package/dist/drivers/react-native-async-storage.d.ts +4 -0
- package/dist/drivers/react-native-async-storage.d.ts.map +1 -0
- package/dist/drivers/react-native-async-storage.js +448 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/localspace.d.ts +2 -0
- package/dist/localspace.d.ts.map +1 -1
- package/dist/localspace.js +18 -5
- package/dist/react-native.cjs.js +2 -0
- package/dist/react-native.cjs.js.map +1 -0
- package/dist/react-native.d.ts +18 -0
- package/dist/react-native.d.ts.map +1 -0
- package/dist/react-native.esm.js +2 -0
- package/dist/react-native.esm.js.map +1 -0
- package/dist/react-native.js +47 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +13 -1
- package/src/drivers/react-native-async-storage.ts +770 -0
- package/src/index.ts +2 -0
- package/src/localspace.ts +21 -4
- package/src/react-native.ts +71 -0
- package/src/types.ts +22 -0
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 localForage
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
## Quick Start
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Get started in 5 minutes:
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### 1. Install
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
```bash
|
|
22
|
+
npm install localspace
|
|
23
|
+
# or: yarn add localspace / pnpm add localspace
|
|
24
|
+
```
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
### 2. Basic Usage
|
|
24
27
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
43
|
+
### 3. Create Isolated Instances
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
51
|
+
await cache.setItem('token', 'abc123');
|
|
52
|
+
```
|
|
63
53
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
// Read multiple items
|
|
64
|
+
const users = await localspace.getItems(['user:1', 'user:2']);
|
|
65
|
+
```
|
|
80
66
|
|
|
81
|
-
|
|
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
|
-
|
|
69
|
+
```ts
|
|
70
|
+
import localspace, { ttlPlugin, encryptionPlugin } from 'localspace';
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
81
|
+
That's it! For more details, see the sections below.
|
|
93
82
|
|
|
94
|
-
|
|
83
|
+
---
|
|
95
84
|
|
|
96
|
-
|
|
85
|
+
## Table of Contents
|
|
97
86
|
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
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
|
-
|
|
98
|
+
---
|
|
104
99
|
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
if (error) return console.error(error);
|
|
129
|
-
console.log(value?.name);
|
|
130
|
-
});
|
|
131
|
-
```
|
|
116
|
+
---
|
|
132
117
|
|
|
133
|
-
|
|
118
|
+
## Core API
|
|
134
119
|
|
|
135
|
-
|
|
120
|
+
### Storage Methods
|
|
136
121
|
|
|
137
122
|
```ts
|
|
138
|
-
//
|
|
139
|
-
await
|
|
140
|
-
|
|
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
|
-
|
|
127
|
+
// Remove items
|
|
128
|
+
await localspace.removeItem('key');
|
|
129
|
+
await localspace.clear();
|
|
150
130
|
|
|
151
|
-
|
|
131
|
+
// Query
|
|
132
|
+
const count = await localspace.length();
|
|
133
|
+
const keys = await localspace.keys();
|
|
152
134
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
console.log(
|
|
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
|
-
###
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
230
|
-
|
|
231
|
-
//
|
|
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
|
-
###
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
+
Advanced usage is still available:
|
|
266
178
|
|
|
267
179
|
```ts
|
|
268
|
-
|
|
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
|
-
|
|
275
|
-
await localspace.setDriver(
|
|
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
|
-
|
|
187
|
+
Integration smoke (official AsyncStorage Jest mock):
|
|
290
188
|
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
+
GitHub Actions Detox workflow template (real simulator/emulator runtime):
|
|
306
196
|
|
|
307
|
-
|
|
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
|
-
|
|
201
|
+
📖 **Full API Reference:** [docs/api-reference.md](./docs/api-reference.md)
|
|
310
202
|
|
|
311
|
-
|
|
312
|
-
- `coalesceMaxBatchSize` caps how many ops each flush processes.
|
|
313
|
-
- `coalesceReadConsistency` controls when writes resolve and when reads see them.
|
|
203
|
+
---
|
|
314
204
|
|
|
315
|
-
|
|
205
|
+
## Batch Operations
|
|
316
206
|
|
|
317
|
-
|
|
207
|
+
Use batch APIs for better performance with IndexedDB:
|
|
318
208
|
|
|
319
209
|
```ts
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
220
|
+
// Single transaction delete
|
|
221
|
+
await localspace.removeItems(['user:1', 'user:2']);
|
|
222
|
+
```
|
|
363
223
|
|
|
364
|
-
|
|
224
|
+
### Transactions
|
|
365
225
|
|
|
366
|
-
|
|
226
|
+
For atomic multi-step operations:
|
|
367
227
|
|
|
368
228
|
```ts
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
## Coalesced Writes
|
|
384
239
|
|
|
385
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
// coalesceWrites is false by default
|
|
244
|
+
coalesceWrites: true, // Enable (default: false)
|
|
245
|
+
coalesceWindowMs: 8, // 8ms merge window
|
|
392
246
|
});
|
|
393
|
-
```
|
|
394
247
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
+
- `'strong'` (default): Reads flush pending writes first
|
|
259
|
+
- `'eventual'`: Reads may see stale values briefly
|
|
413
260
|
|
|
414
261
|
```ts
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
447
|
-
syncPlugin({ channelName: '
|
|
448
|
-
quotaPlugin({ maxSize: 5 * 1024 * 1024,
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
📖 **Full Plugin Documentation:** [docs/plugins.md](./docs/plugins.md)
|
|
306
|
+
📖 **Real-World Examples:** [docs/examples.md](./docs/examples.md)
|
|
476
307
|
|
|
477
|
-
|
|
308
|
+
---
|
|
478
309
|
|
|
479
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
name: '
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
561
|
-
|
|
319
|
+
// Driver
|
|
320
|
+
driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE],
|
|
562
321
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
//
|
|
575
|
-
|
|
326
|
+
// Batching
|
|
327
|
+
maxBatchSize: 200, // Split large batches
|
|
328
|
+
coalesceWrites: false, // Merge rapid writes
|
|
576
329
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
+
---
|
|
704
351
|
|
|
705
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
362
|
+
## Troubleshooting
|
|
715
363
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
371
|
+
**Errors** are `LocalSpaceError` with `code`, `details`, and `cause` properties.
|
|
721
372
|
|
|
722
|
-
|
|
373
|
+
---
|
|
723
374
|
|
|
724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
+
---
|
|
731
384
|
|
|
732
|
-
|
|
733
|
-
const legacy = localspace.createInstance({
|
|
734
|
-
name: 'legacy-store',
|
|
735
|
-
storeName: 'pairs',
|
|
736
|
-
compatibilityMode: true,
|
|
737
|
-
});
|
|
385
|
+
## Documentation
|
|
738
386
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
394
|
+
---
|
|
751
395
|
|
|
752
|
-
|
|
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
|
-
|
|
398
|
+
### Complete ✅
|
|
763
399
|
|
|
764
|
-
-
|
|
765
|
-
-
|
|
766
|
-
-
|
|
767
|
-
-
|
|
768
|
-
-
|
|
769
|
-
-
|
|
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
|
-
|
|
407
|
+
### Coming Soon
|
|
776
408
|
|
|
777
|
-
|
|
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
|
-
|
|
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
|
|