localspace 0.3.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -22
- package/dist/core/plugin-manager.d.ts +5 -0
- package/dist/core/plugin-manager.d.ts.map +1 -1
- package/dist/core/plugin-manager.js +82 -5
- package/dist/drivers/indexeddb.d.ts.map +1 -1
- package/dist/drivers/indexeddb.js +13 -1
- package/dist/drivers/localstorage.d.ts.map +1 -1
- package/dist/drivers/localstorage.js +27 -3
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/plugins/compression.d.ts.map +1 -1
- package/dist/plugins/compression.js +68 -8
- package/dist/plugins/encryption.d.ts.map +1 -1
- package/dist/plugins/encryption.js +67 -5
- package/dist/plugins/quota.d.ts.map +1 -1
- package/dist/plugins/quota.js +109 -1
- package/dist/plugins/sync.d.ts.map +1 -1
- package/dist/plugins/sync.js +70 -1
- package/dist/plugins/ttl.d.ts +5 -0
- package/dist/plugins/ttl.d.ts.map +1 -1
- package/dist/plugins/ttl.js +85 -7
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +2 -1
- package/src/core/plugin-manager.ts +95 -4
- package/src/drivers/indexeddb.ts +13 -0
- package/src/drivers/localstorage.ts +41 -2
- package/src/plugins/compression.ts +114 -10
- package/src/plugins/encryption.ts +122 -6
- package/src/plugins/quota.ts +146 -1
- package/src/plugins/sync.ts +78 -0
- package/src/plugins/ttl.ts +109 -7
- package/src/types.ts +7 -0
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ We stay 100% compatible with localForage on the surface, but rebuild the interna
|
|
|
23
23
|
Starting fresh let us eliminate technical debt while maintaining API compatibility. The codebase is written in modern TypeScript, uses contemporary patterns, and has a clear structure that makes it straightforward to add new capabilities. Teams can migrate from localForage without changing application code, then unlock better developer experience and future extensibility.
|
|
24
24
|
|
|
25
25
|
## Table of Contents
|
|
26
|
+
|
|
26
27
|
- [Motivation](#motivation)
|
|
27
28
|
- [What needed to change](#what-needed-to-change)
|
|
28
29
|
- [How localspace responds](#how-localspace-responds)
|
|
@@ -47,6 +48,7 @@ Starting fresh let us eliminate technical debt while maintaining API compatibili
|
|
|
47
48
|
localspace is built on a foundation designed for growth. Here's what's planned:
|
|
48
49
|
|
|
49
50
|
### Core Compatibility (Complete)
|
|
51
|
+
|
|
50
52
|
- [x] IndexedDB and localStorage drivers
|
|
51
53
|
- [x] Full localForage API parity
|
|
52
54
|
- [x] TypeScript-first implementation
|
|
@@ -58,6 +60,7 @@ localspace is built on a foundation designed for growth. Here's what's planned:
|
|
|
58
60
|
- [x] **Improved error handling** - Structured error types with detailed context
|
|
59
61
|
|
|
60
62
|
### TODO
|
|
63
|
+
|
|
61
64
|
- [x] **Plugin system** - Middleware architecture for cross-cutting concerns
|
|
62
65
|
- [ ] **OPFS driver** - Origin Private File System for high-performance file storage
|
|
63
66
|
- [ ] **Custom driver templates** - Documentation and examples for third-party drivers
|
|
@@ -80,6 +83,7 @@ We prioritize features based on community feedback. If you need a specific capab
|
|
|
80
83
|
3. **Contribute** - We welcome PRs for new drivers, plugins, or improvements
|
|
81
84
|
|
|
82
85
|
**Want to help?** The most impactful contributions right now:
|
|
86
|
+
|
|
83
87
|
- Testing in diverse environments (browsers, frameworks, edge cases)
|
|
84
88
|
- Documentation improvements and usage examples
|
|
85
89
|
- Performance benchmarks and optimization suggestions
|
|
@@ -88,6 +92,7 @@ We prioritize features based on community feedback. If you need a specific capab
|
|
|
88
92
|
## Installation and Usage
|
|
89
93
|
|
|
90
94
|
### localspace delivers modern storage compatibility
|
|
95
|
+
|
|
91
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.**
|
|
92
97
|
|
|
93
98
|
- Promise-first API with optional callbacks
|
|
@@ -96,6 +101,7 @@ localspace targets developers who need localForage's API surface without its his
|
|
|
96
101
|
- Drop-in TypeScript generics for value typing
|
|
97
102
|
|
|
98
103
|
### Install and import localspace
|
|
104
|
+
|
|
99
105
|
Install the package with your preferred package manager and import it once at the entry point where you manage storage.
|
|
100
106
|
|
|
101
107
|
```bash
|
|
@@ -111,6 +117,7 @@ import localspace from 'localspace';
|
|
|
111
117
|
```
|
|
112
118
|
|
|
113
119
|
### Store data with async flows or callbacks
|
|
120
|
+
|
|
114
121
|
Use async/await for the clearest flow. **Callbacks remain supported for parity with existing localForage codebases.**
|
|
115
122
|
|
|
116
123
|
```ts
|
|
@@ -124,6 +131,7 @@ localspace.getItem('user', (error, value) => {
|
|
|
124
131
|
```
|
|
125
132
|
|
|
126
133
|
### 🚀 Opt into automatic performance optimization (coalesced writes)
|
|
134
|
+
|
|
127
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.
|
|
128
136
|
|
|
129
137
|
```ts
|
|
@@ -141,16 +149,18 @@ await Promise.all([
|
|
|
141
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.
|
|
142
150
|
|
|
143
151
|
**Turn it on or tune it**
|
|
152
|
+
|
|
144
153
|
```ts
|
|
145
154
|
const instance = localspace.createInstance({
|
|
146
|
-
coalesceWrites: true,
|
|
147
|
-
coalesceWindowMs: 8,
|
|
155
|
+
coalesceWrites: true, // opt-in (default is false)
|
|
156
|
+
coalesceWindowMs: 8, // 8ms window (default)
|
|
148
157
|
});
|
|
149
158
|
```
|
|
150
159
|
|
|
151
160
|
For consistency modes, batch limits, and failure semantics, see **Advanced: Coalesced Writes** below.
|
|
152
161
|
|
|
153
162
|
**When is this useful?**
|
|
163
|
+
|
|
154
164
|
- Form auto-save that writes multiple fields rapidly
|
|
155
165
|
- Bulk state synchronization loops
|
|
156
166
|
- Real-time collaborative editing
|
|
@@ -159,6 +169,7 @@ For consistency modes, batch limits, and failure semantics, see **Advanced: Coal
|
|
|
159
169
|
**Performance impact**: Single infrequent writes are unaffected. Rapid sequential writes get 3-10x faster automatically.
|
|
160
170
|
|
|
161
171
|
**Want to see the actual performance gains?**
|
|
172
|
+
|
|
162
173
|
```ts
|
|
163
174
|
// Get statistics to see how much coalescing helped (IndexedDB only)
|
|
164
175
|
const stats = localspace.getPerformanceStats?.();
|
|
@@ -172,6 +183,7 @@ console.log(stats);
|
|
|
172
183
|
```
|
|
173
184
|
|
|
174
185
|
### Boost throughput with batch operations
|
|
186
|
+
|
|
175
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).
|
|
176
188
|
|
|
177
189
|
```ts
|
|
@@ -208,17 +220,21 @@ await Promise.all([
|
|
|
208
220
|
|
|
209
221
|
// These features work independently and can be combined
|
|
210
222
|
const optimized = localspace.createInstance({
|
|
211
|
-
coalesceWrites: true,
|
|
223
|
+
coalesceWrites: true, // optimizes single-item writes (setItem/removeItem)
|
|
212
224
|
coalesceWindowMs: 8,
|
|
213
|
-
maxBatchSize: 200,
|
|
225
|
+
maxBatchSize: 200, // limits batch API chunk size (setItems/removeItems)
|
|
214
226
|
});
|
|
215
227
|
await optimized.setDriver([optimized.INDEXEDDB]);
|
|
216
228
|
|
|
217
|
-
// Note: localStorage batches
|
|
218
|
-
//
|
|
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.
|
|
219
234
|
```
|
|
220
235
|
|
|
221
236
|
### Run your own transaction
|
|
237
|
+
|
|
222
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.
|
|
223
239
|
|
|
224
240
|
```ts
|
|
@@ -232,6 +248,7 @@ await localspace.runTransaction('readwrite', async (tx) => {
|
|
|
232
248
|
```
|
|
233
249
|
|
|
234
250
|
### Configure isolated stores for clear data boundaries
|
|
251
|
+
|
|
235
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.
|
|
236
253
|
|
|
237
254
|
```ts
|
|
@@ -244,6 +261,7 @@ await sessionCache.setItem('token', 'abc123');
|
|
|
244
261
|
```
|
|
245
262
|
|
|
246
263
|
### Choose drivers with predictable fallbacks
|
|
264
|
+
|
|
247
265
|
By default, localspace prefers IndexedDB (`INDEXEDDB`) and falls back to localStorage (`LOCALSTORAGE`). Configure alternative sequences as needed.
|
|
248
266
|
|
|
249
267
|
```ts
|
|
@@ -271,6 +289,7 @@ await bucketed.setDriver([bucketed.INDEXEDDB]);
|
|
|
271
289
|
**Tip:** Use `defineDriver()` and `getDriver()` to register custom drivers that match the localForage interface.
|
|
272
290
|
|
|
273
291
|
### Handle binary data across browsers
|
|
292
|
+
|
|
274
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.
|
|
275
294
|
|
|
276
295
|
```ts
|
|
@@ -288,6 +307,7 @@ localspace offers an opt-in, configurable coalesced write path to cut IndexedDB
|
|
|
288
307
|
### Why coalesce writes?
|
|
289
308
|
|
|
290
309
|
Each IndexedDB write opens a readwrite transaction. At high frequency, transaction startup overhead becomes a bottleneck. With coalescing enabled, `setItem` and `removeItem` calls that land within a short window (default 8 ms) are merged into fewer transactions:
|
|
310
|
+
|
|
291
311
|
- Multiple writes can share one transaction.
|
|
292
312
|
- `coalesceMaxBatchSize` caps how many ops each flush processes.
|
|
293
313
|
- `coalesceReadConsistency` controls when writes resolve and when reads see them.
|
|
@@ -329,12 +349,14 @@ interface LocalSpaceConfig {
|
|
|
329
349
|
### Consistency modes
|
|
330
350
|
|
|
331
351
|
#### `coalesceReadConsistency: 'strong'` (default)
|
|
352
|
+
|
|
332
353
|
- Writes (`setItem` / `removeItem`): Promises resolve after the data is persisted; flush errors reject.
|
|
333
354
|
- Reads (`getItem`, `iterate`, batch reads): call `drainCoalescedWrites` first so you read what you just wrote.
|
|
334
355
|
|
|
335
356
|
Use this for user settings, drafts, and any flow where you need read-your-writes.
|
|
336
357
|
|
|
337
358
|
#### `coalesceReadConsistency: 'eventual'`
|
|
359
|
+
|
|
338
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.
|
|
339
361
|
- Reads: do not flush pending writes, so you may briefly see stale values.
|
|
340
362
|
- Destructive operations still force a flush to avoid dropping queued writes: `removeItems`, `clear`, `dropInstance`.
|
|
@@ -360,7 +382,8 @@ const store = localspace.createInstance({
|
|
|
360
382
|
|
|
361
383
|
### Recommended recipes
|
|
362
384
|
|
|
363
|
-
1
|
|
385
|
+
1. Default: coalescing off
|
|
386
|
+
|
|
364
387
|
```ts
|
|
365
388
|
const store = localspace.createInstance({
|
|
366
389
|
name: 'app',
|
|
@@ -369,7 +392,8 @@ const store = localspace.createInstance({
|
|
|
369
392
|
});
|
|
370
393
|
```
|
|
371
394
|
|
|
372
|
-
2
|
|
395
|
+
2. High-frequency writes with eventual consistency
|
|
396
|
+
|
|
373
397
|
```ts
|
|
374
398
|
const logStore = localspace.createInstance({
|
|
375
399
|
name: 'analytics',
|
|
@@ -380,11 +404,13 @@ const logStore = localspace.createInstance({
|
|
|
380
404
|
coalesceReadConsistency: 'eventual',
|
|
381
405
|
});
|
|
382
406
|
```
|
|
407
|
+
|
|
383
408
|
- `setItem` resolves almost immediately.
|
|
384
409
|
- Short windows of stale reads are acceptable.
|
|
385
410
|
- `clear` and `dropInstance` force-flush so queued writes are not lost.
|
|
386
411
|
|
|
387
|
-
3
|
|
412
|
+
3. Strong consistency with bounded batches
|
|
413
|
+
|
|
388
414
|
```ts
|
|
389
415
|
const userStore = localspace.createInstance({
|
|
390
416
|
name: 'user-data',
|
|
@@ -395,6 +421,7 @@ const userStore = localspace.createInstance({
|
|
|
395
421
|
coalesceReadConsistency: 'strong',
|
|
396
422
|
});
|
|
397
423
|
```
|
|
424
|
+
|
|
398
425
|
- Writes resolve after persistence.
|
|
399
426
|
- Reads flush pending writes first.
|
|
400
427
|
- Batching still reduces transaction count.
|
|
@@ -415,8 +442,8 @@ const store = localspace.createInstance({
|
|
|
415
442
|
storeName: 'primary',
|
|
416
443
|
plugins: [
|
|
417
444
|
ttlPlugin({ defaultTTL: 60_000 }),
|
|
418
|
-
encryptionPlugin({ key: '0123456789abcdef0123456789abcdef' }),
|
|
419
445
|
compressionPlugin({ threshold: 1024 }),
|
|
446
|
+
encryptionPlugin({ key: '0123456789abcdef0123456789abcdef' }),
|
|
420
447
|
syncPlugin({ channelName: 'localspace-sync' }),
|
|
421
448
|
quotaPlugin({ maxSize: 5 * 1024 * 1024, evictionPolicy: 'lru' }),
|
|
422
449
|
],
|
|
@@ -427,69 +454,278 @@ const store = localspace.createInstance({
|
|
|
427
454
|
|
|
428
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.
|
|
429
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.
|
|
430
|
-
- **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 (
|
|
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.
|
|
431
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.
|
|
432
|
-
- **Error handling &
|
|
459
|
+
- **Error handling & policies** – unexpected exceptions are reported through `plugin.onError`. Throw a `LocalSpaceError` if you need to stop the pipeline (quota violations, failed decryptions, etc.). Init policy: default fail-fast; set `pluginInitPolicy: 'disable-and-continue'` to log and skip the failing plugin. Runtime policy: default `pluginErrorPolicy: 'strict'` propagates all plugin errors; only use `lenient` if you explicitly accept swallowed errors, and avoid lenient for encryption/compression/ttl or any correctness-critical plugin.
|
|
433
460
|
|
|
434
461
|
### Plugin execution order
|
|
435
462
|
|
|
436
463
|
Plugins are sorted by `priority` (higher runs first in `before*`, last in `after*`). Default priorities:
|
|
437
464
|
|
|
438
|
-
| Plugin
|
|
439
|
-
|
|
440
|
-
| sync
|
|
441
|
-
| quota
|
|
442
|
-
|
|
|
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 |
|
|
443
472
|
|
|
444
|
-
**Recommended order**: `[ttlPlugin,
|
|
473
|
+
**Recommended order**: `[ttlPlugin, compressionPlugin, encryptionPlugin, syncPlugin, quotaPlugin]`
|
|
445
474
|
|
|
446
475
|
### Built-in plugins
|
|
447
476
|
|
|
448
477
|
#### TTL plugin
|
|
478
|
+
|
|
449
479
|
Wraps values as `{ data, expiresAt }`, invalidates stale reads, and optionally runs background cleanup. Options:
|
|
450
480
|
|
|
451
481
|
- `defaultTTL` (ms) and `keyTTL` overrides
|
|
452
|
-
- `cleanupInterval` to periodically scan
|
|
482
|
+
- `cleanupInterval` to periodically scan expired entries
|
|
483
|
+
- `cleanupBatchSize` (default: 100) for efficient batch cleanup
|
|
453
484
|
- `onExpire(key, value)` callback before removal
|
|
454
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
|
+
|
|
455
514
|
#### Encryption plugin
|
|
515
|
+
|
|
456
516
|
Encrypts serialized payloads using the Web Crypto API (AES-GCM by default) and decrypts transparently on reads.
|
|
457
517
|
|
|
458
518
|
- Provide a `key` (CryptoKey/ArrayBuffer/string) or `keyDerivation` block (PBKDF2)
|
|
459
519
|
- Customize `algorithm`, `ivLength`, `ivGenerator`, or `randomSource`
|
|
460
520
|
- Works in browsers and modern Node runtimes (pass your own `subtle` when needed)
|
|
461
521
|
|
|
522
|
+
```ts
|
|
523
|
+
// Using a direct key
|
|
524
|
+
const secureStore = localspace.createInstance({
|
|
525
|
+
name: 'secure-store',
|
|
526
|
+
plugins: [
|
|
527
|
+
encryptionPlugin({
|
|
528
|
+
key: '0123456789abcdef0123456789abcdef', // 32 bytes for AES-256
|
|
529
|
+
}),
|
|
530
|
+
],
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Using PBKDF2 key derivation (recommended for password-based encryption)
|
|
534
|
+
const passwordStore = localspace.createInstance({
|
|
535
|
+
name: 'password-store',
|
|
536
|
+
plugins: [
|
|
537
|
+
encryptionPlugin({
|
|
538
|
+
keyDerivation: {
|
|
539
|
+
passphrase: userPassword,
|
|
540
|
+
salt: 'unique-per-user-salt',
|
|
541
|
+
iterations: 150000, // Higher = more secure but slower
|
|
542
|
+
hash: 'SHA-256',
|
|
543
|
+
length: 256,
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Batch operations are also encrypted
|
|
550
|
+
await secureStore.setItems([
|
|
551
|
+
{ key: 'card-number', value: '4111-1111-1111-1111' },
|
|
552
|
+
{ key: 'cvv', value: '123' },
|
|
553
|
+
]);
|
|
554
|
+
```
|
|
555
|
+
|
|
462
556
|
#### Compression plugin
|
|
557
|
+
|
|
463
558
|
Runs LZ-string compression (or a custom codec) when payloads exceed a `threshold` and restores them on read.
|
|
464
559
|
|
|
465
560
|
- `threshold` (bytes) controls when compression kicks in
|
|
466
561
|
- Supply a custom `{ compress, decompress }` codec if you prefer pako/Brotli
|
|
467
562
|
|
|
563
|
+
```ts
|
|
564
|
+
const compressedStore = localspace.createInstance({
|
|
565
|
+
name: 'compressed-store',
|
|
566
|
+
plugins: [
|
|
567
|
+
compressionPlugin({
|
|
568
|
+
threshold: 1024, // Only compress if > 1KB
|
|
569
|
+
algorithm: 'lz-string', // Label stored in metadata
|
|
570
|
+
}),
|
|
571
|
+
],
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Custom codec example (using pako)
|
|
575
|
+
import pako from 'pako';
|
|
576
|
+
|
|
577
|
+
const pakoStore = localspace.createInstance({
|
|
578
|
+
name: 'pako-store',
|
|
579
|
+
plugins: [
|
|
580
|
+
compressionPlugin({
|
|
581
|
+
threshold: 512,
|
|
582
|
+
algorithm: 'gzip',
|
|
583
|
+
codec: {
|
|
584
|
+
compress: (data) => pako.gzip(data),
|
|
585
|
+
decompress: (data) => pako.ungzip(data, { to: 'string' }),
|
|
586
|
+
},
|
|
587
|
+
}),
|
|
588
|
+
],
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
468
592
|
#### Sync plugin
|
|
593
|
+
|
|
469
594
|
Keeps multiple tabs/processes in sync via `BroadcastChannel` (with `storage`-event fallback).
|
|
470
595
|
|
|
471
596
|
- `channelName` separates logical buses
|
|
472
597
|
- `syncKeys` lets you scope which keys broadcast
|
|
473
598
|
- `conflictStrategy` defaults to `last-write-wins`; provide `onConflict` (return `false` to drop remote writes) for merge logic
|
|
474
599
|
|
|
600
|
+
```ts
|
|
601
|
+
const syncedStore = localspace.createInstance({
|
|
602
|
+
name: 'synced-store',
|
|
603
|
+
plugins: [
|
|
604
|
+
syncPlugin({
|
|
605
|
+
channelName: 'my-app-sync',
|
|
606
|
+
syncKeys: ['cart', 'preferences', 'theme'], // Only sync these keys
|
|
607
|
+
conflictStrategy: 'last-write-wins',
|
|
608
|
+
onConflict: ({ key, localTimestamp, incomingTimestamp, value }) => {
|
|
609
|
+
console.log(`Conflict on ${key}: local=${localTimestamp}, incoming=${incomingTimestamp}`);
|
|
610
|
+
// Return false to reject the incoming change
|
|
611
|
+
return localTimestamp < incomingTimestamp;
|
|
612
|
+
},
|
|
613
|
+
}),
|
|
614
|
+
],
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Changes sync across tabs automatically
|
|
618
|
+
await syncedStore.setItem('cart', { items: [...] });
|
|
619
|
+
await syncedStore.setItems([
|
|
620
|
+
{ key: 'preferences', value: { darkMode: true } },
|
|
621
|
+
{ key: 'theme', value: 'blue' },
|
|
622
|
+
]);
|
|
623
|
+
```
|
|
624
|
+
|
|
475
625
|
#### Quota plugin
|
|
626
|
+
|
|
476
627
|
Tracks approximate storage usage after every mutation and enforces limits.
|
|
477
628
|
|
|
478
|
-
- `maxSize` (bytes) and optional `useNavigatorEstimate` to read the browser
|
|
629
|
+
- `maxSize` (bytes) and optional `useNavigatorEstimate` to read the browser's quota
|
|
479
630
|
- `evictionPolicy: 'error' | 'lru'` (LRU removes least-recently-used keys automatically)
|
|
480
631
|
- `onQuotaExceeded(info)` fires before throwing so you can log/alert users
|
|
481
632
|
|
|
633
|
+
```ts
|
|
634
|
+
const quotaStore = localspace.createInstance({
|
|
635
|
+
name: 'quota-store',
|
|
636
|
+
plugins: [
|
|
637
|
+
quotaPlugin({
|
|
638
|
+
maxSize: 5 * 1024 * 1024, // 5 MB
|
|
639
|
+
evictionPolicy: 'lru', // Automatically evict least-recently-used items
|
|
640
|
+
useNavigatorEstimate: true, // Also respect browser quota
|
|
641
|
+
onQuotaExceeded: ({ key, attemptedSize, maxSize, currentUsage }) => {
|
|
642
|
+
console.warn(`Quota exceeded: tried to write ${attemptedSize} bytes`);
|
|
643
|
+
console.warn(`Current usage: ${currentUsage}/${maxSize} bytes`);
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
],
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Batch operations are also quota-checked
|
|
650
|
+
await quotaStore.setItems([
|
|
651
|
+
{ key: 'large-1', value: largeData1 },
|
|
652
|
+
{ key: 'large-2', value: largeData2 },
|
|
653
|
+
]); // Throws QUOTA_EXCEEDED if total exceeds limit
|
|
654
|
+
```
|
|
655
|
+
|
|
482
656
|
> Tip: place quota plugins last so they see the final payload size after other transformations (TTL, encryption, compression, etc.).
|
|
483
657
|
|
|
658
|
+
### Plugin combination best practices
|
|
659
|
+
|
|
660
|
+
1. **Recommended plugin order** (from highest to lowest priority):
|
|
661
|
+
```ts
|
|
662
|
+
plugins: [
|
|
663
|
+
ttlPlugin({ ... }), // priority: 10
|
|
664
|
+
compressionPlugin({ ... }), // priority: 5
|
|
665
|
+
encryptionPlugin({ ... }), // priority: 0
|
|
666
|
+
quotaPlugin({ ... }), // priority: -10
|
|
667
|
+
syncPlugin({ ... }), // priority: -100
|
|
668
|
+
]
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
2. **Always compress before encrypting**: Encrypted data has high entropy and compresses poorly. The default priorities handle this automatically.
|
|
672
|
+
|
|
673
|
+
3. **Use strict error policy with security-critical plugins**:
|
|
674
|
+
```ts
|
|
675
|
+
// DON'T do this - encryption failures will be silently swallowed
|
|
676
|
+
const bad = localspace.createInstance({
|
|
677
|
+
plugins: [encryptionPlugin({ key })],
|
|
678
|
+
pluginErrorPolicy: 'lenient', // Dangerous!
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// DO this - encryption failures will propagate
|
|
682
|
+
const good = localspace.createInstance({
|
|
683
|
+
plugins: [encryptionPlugin({ key })],
|
|
684
|
+
pluginErrorPolicy: 'strict', // Safe (default)
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
4. **Batch operations work with all plugins**: All built-in plugins support `setItems`, `getItems`, and `removeItems`.
|
|
689
|
+
|
|
690
|
+
### Plugin troubleshooting
|
|
691
|
+
|
|
692
|
+
| Issue | Solution |
|
|
693
|
+
|-------|----------|
|
|
694
|
+
| TTL items not expiring | Ensure `cleanupInterval` is set, or read items to trigger expiration |
|
|
695
|
+
| Encryption fails silently | Check `pluginErrorPolicy` is not 'lenient' |
|
|
696
|
+
| Compression not working | Verify payload exceeds `threshold` |
|
|
697
|
+
| Sync not updating other tabs | Check `channelName` matches and `syncKeys` includes your key |
|
|
698
|
+
| Quota errors on small writes | Other plugins (TTL, encryption) add overhead; account for wrapper size |
|
|
699
|
+
| Plugin order seems wrong | Check `priority` values; higher = runs first in `before*` hooks |
|
|
700
|
+
|
|
701
|
+
## Compatibility & environments
|
|
702
|
+
|
|
703
|
+
- Browsers: modern Chromium/Edge, Firefox, Safari (desktop & iOS). IndexedDB is required for the primary driver; localStorage is available as a fallback.
|
|
704
|
+
- Known differences: Safari private mode / low-quota environments may throw quota; IndexedDB durability hints may be ignored outside Chromium 121+. If you need strict durability, prefer explicit flush/transaction patterns.
|
|
705
|
+
- Node/SSR: browser storage APIs are not available by default; supply a custom driver or guard usage in non-browser contexts.
|
|
706
|
+
|
|
707
|
+
## Testing & CI
|
|
708
|
+
|
|
709
|
+
- Recommended pipeline: `yarn lint` (if configured) → `yarn vitest run` → `yarn build` → `playwright test`.
|
|
710
|
+
- Regression coverage includes: coalesced writes + pending queue + maxConcurrentTransactions + idle close, plugin error policies (strict/lenient) including batch hooks, compression/encryption/ttl ordering, sync version persistence, localStorage quota handling with rollback.
|
|
711
|
+
|
|
712
|
+
## Security & performance guidance
|
|
713
|
+
|
|
714
|
+
- Plugin order for correctness/performance: `ttl → compression → encryption → sync → quota`.
|
|
715
|
+
- The encryption plugin provides basic crypto; key management/rotation is your responsibility, and you should not swallow encryption/compression errors via a lenient policy.
|
|
716
|
+
- Run compression before encryption for effectiveness; place quota last to see final sizes; keep sync last in `after*` to broadcast original values.
|
|
717
|
+
|
|
484
718
|
## Migration Guide
|
|
485
719
|
|
|
486
720
|
### Note differences from localForage before upgrading
|
|
721
|
+
|
|
487
722
|
- `dropInstance()` throws a real `Error` when arguments are invalid. Examine `error.message` instead of comparing string literals.
|
|
488
723
|
- Blob capability checks run on each request instead of being cached. Cache the result in your application if repeated blob writes dominate your workload.
|
|
489
724
|
- **WebSQL is intentionally unsupported.** Migrate any WebSQL-only code to IndexedDB or localStorage before switching.
|
|
490
725
|
|
|
491
726
|
### Enable compatibility mode for driver setup methods
|
|
492
|
-
|
|
727
|
+
|
|
728
|
+
If you maintain older code that expects separate _success_ and _error_ callbacks for driver setup methods (`setDriver`, `defineDriver`), enable `compatibilityMode` when creating an instance. **Use this mode only for migrations; prefer native Promises going forward.**
|
|
493
729
|
|
|
494
730
|
```ts
|
|
495
731
|
const legacy = localspace.createInstance({
|
|
@@ -505,7 +741,7 @@ legacy.setDriver(
|
|
|
505
741
|
},
|
|
506
742
|
(error) => {
|
|
507
743
|
// Error callback receives the Error object only.
|
|
508
|
-
}
|
|
744
|
+
}
|
|
509
745
|
);
|
|
510
746
|
```
|
|
511
747
|
|
|
@@ -522,6 +758,7 @@ localspace.setItem('key', 'value', (err, value) => {
|
|
|
522
758
|
```
|
|
523
759
|
|
|
524
760
|
## Performance notes
|
|
761
|
+
|
|
525
762
|
- **Automatic write coalescing (opt-in):** localspace can merge rapid single writes (`setItem`/`removeItem`) within an 8ms window into one transaction for IndexedDB, delivering 3-10x speedups under bursty writes. Enable with `coalesceWrites: true` and see **Advanced: Coalesced Writes** for consistency modes.
|
|
526
763
|
- **Read-your-writes consistency with coalescing:** Pending coalesced writes are flushed before reads (`getItem`, `getItems`, `iterate`, `keys`, `length`, `key`) and destructive ops (`clear`, `dropInstance`), so immediate reads always observe the latest value. If you need eventual reads for speed, you can switch `coalesceReadConsistency` to `'eventual'`.
|
|
527
764
|
- **Batch APIs outperform loops:** Playwright benchmark (`test/playwright/benchmark.spec.ts`) on 500 items x 256B showed `setItems()` ~6x faster and `getItems()` ~7.7x faster than per-item loops, with `removeItems()` ~2.8x faster (Chromium, relaxed durability).
|
|
@@ -536,6 +773,7 @@ localspace.setItem('key', 'value', (err, value) => {
|
|
|
536
773
|
When `compatibilityMode` is off, driver setup methods also use Node-style callbacks. Promises are recommended for all new code.
|
|
537
774
|
|
|
538
775
|
## Troubleshooting
|
|
776
|
+
|
|
539
777
|
- **Wait for readiness:** Call `await localspace.ready()` before the first operation when you need to confirm driver selection.
|
|
540
778
|
- **Inspect drivers:** Use `localspace.driver()` to confirm which driver is active in different environments.
|
|
541
779
|
- **Read structured errors:** Rejections surface as `LocalSpaceError` with a `code`, contextual `details` (driver, operation, key, attemptedDrivers), and the original `cause`. Branch on `error.code` instead of parsing strings.
|
|
@@ -545,4 +783,5 @@ When `compatibilityMode` is off, driver setup methods also use Node-style callba
|
|
|
545
783
|
- **Collect combined Vitest + Playwright coverage:** Run `yarn coverage:full` to clean previous artifacts, run `vitest --coverage`, stash its Istanbul JSON into `.nyc_output`, then execute the coverage-enabled Playwright suite and emit merged `nyc` reports.
|
|
546
784
|
|
|
547
785
|
## License
|
|
786
|
+
|
|
548
787
|
[MIT](./LICENSE)
|
|
@@ -15,7 +15,12 @@ export declare class PluginManager {
|
|
|
15
15
|
private readonly destroyed;
|
|
16
16
|
private readonly disabled;
|
|
17
17
|
private orderCounter;
|
|
18
|
+
private warningsEmitted;
|
|
18
19
|
constructor(host: PluginHost, initialPlugins?: LocalSpacePlugin[]);
|
|
20
|
+
/**
|
|
21
|
+
* Validate plugin combinations and emit warnings for potential issues.
|
|
22
|
+
*/
|
|
23
|
+
private validatePluginCombinations;
|
|
19
24
|
hasPlugins(): boolean;
|
|
20
25
|
registerPlugins(plugins: LocalSpacePlugin[]): void;
|
|
21
26
|
private sortPlugins;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-manager.d.ts","sourceRoot":"","sources":["../../src/core/plugin-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,MAAM,EACN,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EAEb,eAAe,EAEhB,MAAM,UAAU,CAAC;AAIlB,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,SAAiC;CAIrD;AAED,KAAK,UAAU,GAAG,kBAAkB,GAAG;IACrC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin-manager.d.ts","sourceRoot":"","sources":["../../src/core/plugin-manager.ts"],"names":[],"mappings":"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"}
|