@esportsplus/web-storage 0.4.0 → 0.5.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 +248 -0
- package/build/drivers/localstorage.d.ts +1 -0
- package/build/drivers/localstorage.js +10 -2
- package/build/drivers/sessionstorage.d.ts +1 -0
- package/build/drivers/sessionstorage.js +10 -2
- package/build/lz.d.ts +3 -0
- package/build/lz.js +134 -0
- package/package.json +1 -1
- package/src/drivers/localstorage.ts +13 -2
- package/src/drivers/sessionstorage.ts +13 -2
- package/src/lz.ts +192 -0
- package/storage/test-audit-web-storage.md +74 -0
- package/tests/drivers/localstorage.ts +86 -0
- package/tests/drivers/sessionstorage.ts +85 -0
- package/tests/index.ts +409 -0
- package/tests/lz.ts +324 -0
- package/storage/feature-research.md +0 -173
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# @esportsplus/web-storage
|
|
2
|
+
|
|
3
|
+
Typed async storage with multiple backends, TTL, encryption, compression, subscriptions, and migrations.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import storage, { DriverType } from '@esportsplus/web-storage';
|
|
7
|
+
|
|
8
|
+
type UserData = { name: string; preferences: { theme: string } };
|
|
9
|
+
|
|
10
|
+
let store = storage<UserData>({ name: 'app', version: 1 });
|
|
11
|
+
|
|
12
|
+
await store.set('name', 'alice');
|
|
13
|
+
await store.get('name'); // 'alice'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @esportsplus/web-storage
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Drivers
|
|
25
|
+
|
|
26
|
+
| Driver | Persistence | Compression | Use Case |
|
|
27
|
+
|--------|------------|-------------|----------|
|
|
28
|
+
| IndexedDB | Permanent | No | Default. Large data, no quota pressure |
|
|
29
|
+
| localStorage | Permanent | Yes (LZ) | Small data, 5MB limit |
|
|
30
|
+
| sessionStorage | Per-tab | Yes (LZ) | Tab-scoped state |
|
|
31
|
+
| Memory | None | No | Testing, SSR, fallback |
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// IndexedDB (default)
|
|
35
|
+
let store = storage<T>({ name: 'app', version: 1 });
|
|
36
|
+
|
|
37
|
+
// localStorage
|
|
38
|
+
let store = storage<T>({ driver: DriverType.LocalStorage, name: 'app', version: 1 });
|
|
39
|
+
|
|
40
|
+
// sessionStorage
|
|
41
|
+
let store = storage<T>({ driver: DriverType.SessionStorage, name: 'app', version: 1 });
|
|
42
|
+
|
|
43
|
+
// Memory (non-persistent)
|
|
44
|
+
let store = storage<T>({ driver: DriverType.Memory, name: 'app', version: 1 });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
All methods are async and fully typed via `Local<T>`.
|
|
51
|
+
|
|
52
|
+
### Core CRUD
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// Set a value
|
|
56
|
+
await store.set('name', 'alice');
|
|
57
|
+
|
|
58
|
+
// Get a value
|
|
59
|
+
let name = await store.get('name'); // string | undefined
|
|
60
|
+
|
|
61
|
+
// Get with factory (lazy init — never returns undefined)
|
|
62
|
+
let name = await store.get('name', () => 'default');
|
|
63
|
+
let user = await store.get('name', async () => await fetchUser());
|
|
64
|
+
|
|
65
|
+
// Delete keys
|
|
66
|
+
await store.delete('name', 'preferences');
|
|
67
|
+
|
|
68
|
+
// Replace multiple values
|
|
69
|
+
let failed = await store.replace({ name: 'bob', preferences: { theme: 'dark' } });
|
|
70
|
+
|
|
71
|
+
// Get all entries
|
|
72
|
+
let all = await store.all();
|
|
73
|
+
|
|
74
|
+
// Get specific keys
|
|
75
|
+
let subset = await store.only('name', 'preferences');
|
|
76
|
+
|
|
77
|
+
// Count entries
|
|
78
|
+
let count = await store.count();
|
|
79
|
+
|
|
80
|
+
// List keys
|
|
81
|
+
let keys = await store.keys();
|
|
82
|
+
|
|
83
|
+
// Clear everything
|
|
84
|
+
await store.clear();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Iteration
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Map over all entries
|
|
91
|
+
await store.map((value, key, i) => {
|
|
92
|
+
console.log(key, value);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Filter entries (with early stop)
|
|
96
|
+
let result = await store.filter(({ key, value, stop }) => {
|
|
97
|
+
if (key === 'name') {
|
|
98
|
+
stop(); // halt iteration
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return typeof value === 'string';
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## TTL / Expiration
|
|
107
|
+
|
|
108
|
+
Per-key time-to-live in milliseconds. Expired entries return `undefined` and are lazily deleted.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// Set with 1 hour TTL
|
|
112
|
+
await store.set('session', token, { ttl: 3600000 });
|
|
113
|
+
|
|
114
|
+
// Check remaining time (-1 if no TTL or expired)
|
|
115
|
+
await store.ttl('session'); // ms remaining
|
|
116
|
+
|
|
117
|
+
// Remove TTL (make permanent)
|
|
118
|
+
await store.persist('session');
|
|
119
|
+
|
|
120
|
+
// Proactively sweep all expired entries
|
|
121
|
+
await store.cleanup();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Encryption
|
|
126
|
+
|
|
127
|
+
Optional AES-GCM encryption via a secret string.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
let store = storage<T>({ name: 'secure', version: 1 }, 'my-secret-key');
|
|
131
|
+
|
|
132
|
+
await store.set('token', 'sensitive-data'); // encrypted at rest
|
|
133
|
+
await store.get('token'); // 'sensitive-data' (decrypted)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## Change Subscriptions
|
|
138
|
+
|
|
139
|
+
Subscribe to value changes. Returns an unsubscribe function.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Per-key subscription
|
|
143
|
+
let unsubscribe = store.subscribe('name', (newValue, oldValue) => {
|
|
144
|
+
console.log(`name: ${oldValue} -> ${newValue}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Global subscription (all keys)
|
|
148
|
+
let unsubscribe = store.subscribe((key, newValue, oldValue) => {
|
|
149
|
+
console.log(`${String(key)} changed`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Stop listening
|
|
153
|
+
unsubscribe();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Fires after: `set`, `delete`, `replace`, `clear`, `cleanup`.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Migrations
|
|
160
|
+
|
|
161
|
+
Run transform functions when the version number changes.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
type V1 = { name: string };
|
|
165
|
+
type V2 = { displayName: string; name: string };
|
|
166
|
+
|
|
167
|
+
let store = storage<V2>({
|
|
168
|
+
name: 'app',
|
|
169
|
+
version: 2,
|
|
170
|
+
migrations: {
|
|
171
|
+
2: async (old) => {
|
|
172
|
+
let data = await old.all();
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
...data,
|
|
176
|
+
displayName: (data.name as string) || 'Anonymous'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Migrations run sequentially. Version 1 to 3 runs migration 2 then migration 3. Each migration receives the current store data and returns the transformed data.
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## Compression
|
|
187
|
+
|
|
188
|
+
localStorage and sessionStorage drivers automatically compress values >= 100 bytes using an inlined LZW compressor. No configuration needed.
|
|
189
|
+
|
|
190
|
+
- Values < 100 bytes: stored as JSON (LZ overhead not worth it)
|
|
191
|
+
- Values >= 100 bytes: LZ compressed (2-10x capacity gain on JSON)
|
|
192
|
+
- Backward compatible: existing uncompressed values read normally
|
|
193
|
+
- Runs before encryption on write, after decryption on read
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
## Factory Pattern (`get` with default)
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Sync factory
|
|
200
|
+
let count = await store.get('count', () => 0);
|
|
201
|
+
|
|
202
|
+
// Async factory
|
|
203
|
+
let user = await store.get('user', async () => {
|
|
204
|
+
return await fetchUser(id);
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The factory is called only when the key is missing or expired. The produced value is persisted via a fire-and-forget `set` (caller isn't blocked by the write).
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## Types
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import type { Local } from '@esportsplus/web-storage';
|
|
215
|
+
import { DriverType } from '@esportsplus/web-storage';
|
|
216
|
+
|
|
217
|
+
type Options = {
|
|
218
|
+
driver?: DriverType;
|
|
219
|
+
migrations?: Record<number, MigrationFn>;
|
|
220
|
+
name: string;
|
|
221
|
+
version: number;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
type SetOptions = {
|
|
225
|
+
ttl?: number;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
type MigrationFn = (old: {
|
|
229
|
+
all(): Promise<Record<string, unknown>>;
|
|
230
|
+
}) => Promise<Record<string, unknown>>;
|
|
231
|
+
|
|
232
|
+
// Subscription callbacks
|
|
233
|
+
type KeyCallback<T, K extends keyof T> = (
|
|
234
|
+
newValue: T[K] | undefined,
|
|
235
|
+
oldValue: T[K] | undefined
|
|
236
|
+
) => void;
|
|
237
|
+
|
|
238
|
+
type GlobalCallback<T> = (
|
|
239
|
+
key: keyof T,
|
|
240
|
+
newValue: T[keyof T] | undefined,
|
|
241
|
+
oldValue: T[keyof T] | undefined
|
|
242
|
+
) => void;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compress, decompress } from '../lz.js';
|
|
1
2
|
class LocalStorageDriver {
|
|
2
3
|
prefix;
|
|
3
4
|
constructor(name, version) {
|
|
@@ -21,12 +22,19 @@ class LocalStorageDriver {
|
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
24
|
try {
|
|
25
|
+
if (value.charCodeAt(0) === 1) {
|
|
26
|
+
return JSON.parse(decompress(value.slice(1)));
|
|
27
|
+
}
|
|
24
28
|
return JSON.parse(value);
|
|
25
29
|
}
|
|
26
30
|
catch {
|
|
27
31
|
return undefined;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
serialize(value) {
|
|
35
|
+
let json = JSON.stringify(value);
|
|
36
|
+
return json.length >= 100 ? '\x01' + compress(json) : json;
|
|
37
|
+
}
|
|
30
38
|
async all() {
|
|
31
39
|
let keys = this.getKeys(), result = {};
|
|
32
40
|
for (let i = 0, n = keys.length; i < n; i++) {
|
|
@@ -78,12 +86,12 @@ class LocalStorageDriver {
|
|
|
78
86
|
}
|
|
79
87
|
async replace(entries) {
|
|
80
88
|
for (let i = 0, n = entries.length; i < n; i++) {
|
|
81
|
-
localStorage.setItem(this.key(entries[i][0]),
|
|
89
|
+
localStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
async set(key, value) {
|
|
85
93
|
try {
|
|
86
|
-
localStorage.setItem(this.key(key),
|
|
94
|
+
localStorage.setItem(this.key(key), this.serialize(value));
|
|
87
95
|
return true;
|
|
88
96
|
}
|
|
89
97
|
catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compress, decompress } from '../lz.js';
|
|
1
2
|
class SessionStorageDriver {
|
|
2
3
|
prefix;
|
|
3
4
|
constructor(name, version) {
|
|
@@ -21,12 +22,19 @@ class SessionStorageDriver {
|
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
24
|
try {
|
|
25
|
+
if (value.charCodeAt(0) === 1) {
|
|
26
|
+
return JSON.parse(decompress(value.slice(1)));
|
|
27
|
+
}
|
|
24
28
|
return JSON.parse(value);
|
|
25
29
|
}
|
|
26
30
|
catch {
|
|
27
31
|
return undefined;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
serialize(value) {
|
|
35
|
+
let json = JSON.stringify(value);
|
|
36
|
+
return json.length >= 100 ? '\x01' + compress(json) : json;
|
|
37
|
+
}
|
|
30
38
|
async all() {
|
|
31
39
|
let keys = this.getKeys(), result = {};
|
|
32
40
|
for (let i = 0, n = keys.length; i < n; i++) {
|
|
@@ -78,12 +86,12 @@ class SessionStorageDriver {
|
|
|
78
86
|
}
|
|
79
87
|
async replace(entries) {
|
|
80
88
|
for (let i = 0, n = entries.length; i < n; i++) {
|
|
81
|
-
sessionStorage.setItem(this.key(entries[i][0]),
|
|
89
|
+
sessionStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
async set(key, value) {
|
|
85
93
|
try {
|
|
86
|
-
sessionStorage.setItem(this.key(key),
|
|
94
|
+
sessionStorage.setItem(this.key(key), this.serialize(value));
|
|
87
95
|
return true;
|
|
88
96
|
}
|
|
89
97
|
catch {
|
package/build/lz.d.ts
ADDED
package/build/lz.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function emitLiteral(ctx, ch) {
|
|
2
|
+
let code = ch.charCodeAt(0);
|
|
3
|
+
if (code < 256) {
|
|
4
|
+
writeBits(ctx, ctx.numBits, 0);
|
|
5
|
+
writeBits(ctx, 8, code);
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
writeBits(ctx, ctx.numBits, 1);
|
|
9
|
+
writeBits(ctx, 16, code);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function readBits(ctx, n) {
|
|
13
|
+
let result = 0;
|
|
14
|
+
for (let i = 0; i < n; i++) {
|
|
15
|
+
if (ctx.bitPos > 15) {
|
|
16
|
+
ctx.currentValue = ctx.compressed.charCodeAt(ctx.pos++) - 1;
|
|
17
|
+
ctx.bitPos = 0;
|
|
18
|
+
}
|
|
19
|
+
result = (result << 1) | ((ctx.currentValue >> (15 - ctx.bitPos)) & 1);
|
|
20
|
+
ctx.bitPos++;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function writeBits(ctx, n, value) {
|
|
25
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
26
|
+
ctx.buffer = (ctx.buffer << 1) | ((value >> i) & 1);
|
|
27
|
+
ctx.bitsInBuffer++;
|
|
28
|
+
if (ctx.bitsInBuffer === 16) {
|
|
29
|
+
ctx.output.push(ctx.buffer + 1);
|
|
30
|
+
ctx.buffer = 0;
|
|
31
|
+
ctx.bitsInBuffer = 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const compress = (input) => {
|
|
36
|
+
if (!input) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
let ctx = { bitsInBuffer: 0, buffer: 0, numBits: 2, output: [] }, dictSize = 3, dictionary = new Map(), w = '';
|
|
40
|
+
for (let i = 0, n = input.length; i < n; i++) {
|
|
41
|
+
let c = input[i], wc = w + c;
|
|
42
|
+
if (dictionary.has(wc)) {
|
|
43
|
+
w = wc;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (w.length > 0) {
|
|
47
|
+
if (dictionary.has(w)) {
|
|
48
|
+
writeBits(ctx, ctx.numBits, dictionary.get(w));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
emitLiteral(ctx, w);
|
|
52
|
+
}
|
|
53
|
+
dictionary.set(wc, dictSize++);
|
|
54
|
+
if (dictSize > (1 << ctx.numBits)) {
|
|
55
|
+
ctx.numBits++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
w = c;
|
|
59
|
+
}
|
|
60
|
+
if (w.length > 0) {
|
|
61
|
+
if (dictionary.has(w)) {
|
|
62
|
+
writeBits(ctx, ctx.numBits, dictionary.get(w));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
emitLiteral(ctx, w);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
dictSize++;
|
|
69
|
+
if (dictSize > (1 << ctx.numBits)) {
|
|
70
|
+
ctx.numBits++;
|
|
71
|
+
}
|
|
72
|
+
writeBits(ctx, ctx.numBits, 2);
|
|
73
|
+
if (ctx.bitsInBuffer > 0) {
|
|
74
|
+
ctx.output.push(((ctx.buffer << (16 - ctx.bitsInBuffer)) & 0xFFFF) + 1);
|
|
75
|
+
}
|
|
76
|
+
ctx.output.push((ctx.bitsInBuffer === 0 ? 16 : ctx.bitsInBuffer) + 1);
|
|
77
|
+
let chars = [];
|
|
78
|
+
for (let i = 0, n = ctx.output.length; i < n; i++) {
|
|
79
|
+
chars.push(String.fromCharCode(ctx.output[i]));
|
|
80
|
+
}
|
|
81
|
+
return chars.join('');
|
|
82
|
+
};
|
|
83
|
+
const decompress = (compressed) => {
|
|
84
|
+
if (!compressed) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
let ctx = { bitPos: 16, compressed: '', currentValue: 0, pos: 0 }, dictSize = 3, dictionary = [], numBits = 2;
|
|
88
|
+
ctx.compressed = compressed.substring(0, compressed.length - 1);
|
|
89
|
+
let code = readBits(ctx, numBits), entry;
|
|
90
|
+
if (code === 0) {
|
|
91
|
+
entry = String.fromCharCode(readBits(ctx, 8));
|
|
92
|
+
}
|
|
93
|
+
else if (code === 1) {
|
|
94
|
+
entry = String.fromCharCode(readBits(ctx, 16));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
let result = [entry], w = entry;
|
|
100
|
+
while (true) {
|
|
101
|
+
let slotIdx = dictionary.length;
|
|
102
|
+
dictionary.push('');
|
|
103
|
+
dictSize++;
|
|
104
|
+
if (dictSize > (1 << numBits)) {
|
|
105
|
+
numBits++;
|
|
106
|
+
}
|
|
107
|
+
code = readBits(ctx, numBits);
|
|
108
|
+
if (code === 2) {
|
|
109
|
+
dictionary.pop();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
let slotCode = slotIdx + 3;
|
|
113
|
+
if (code === 0) {
|
|
114
|
+
entry = String.fromCharCode(readBits(ctx, 8));
|
|
115
|
+
}
|
|
116
|
+
else if (code === 1) {
|
|
117
|
+
entry = String.fromCharCode(readBits(ctx, 16));
|
|
118
|
+
}
|
|
119
|
+
else if (code === slotCode) {
|
|
120
|
+
entry = w + w[0];
|
|
121
|
+
}
|
|
122
|
+
else if (code >= 3 && code < slotCode) {
|
|
123
|
+
entry = dictionary[code - 3];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
throw new Error('LZ: invalid decompression code');
|
|
127
|
+
}
|
|
128
|
+
dictionary[slotIdx] = w + entry[0];
|
|
129
|
+
result.push(entry);
|
|
130
|
+
w = entry;
|
|
131
|
+
}
|
|
132
|
+
return result.join('');
|
|
133
|
+
};
|
|
134
|
+
export { compress, decompress };
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compress, decompress } from '~/lz';
|
|
1
2
|
import type { Driver } from '~/types';
|
|
2
3
|
|
|
3
4
|
|
|
@@ -35,6 +36,10 @@ class LocalStorageDriver<T> implements Driver<T> {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
try {
|
|
39
|
+
if (value.charCodeAt(0) === 1) {
|
|
40
|
+
return JSON.parse(decompress(value.slice(1)));
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
return JSON.parse(value);
|
|
39
44
|
}
|
|
40
45
|
catch {
|
|
@@ -42,6 +47,12 @@ class LocalStorageDriver<T> implements Driver<T> {
|
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
private serialize(value: T[keyof T]): string {
|
|
51
|
+
let json = JSON.stringify(value);
|
|
52
|
+
|
|
53
|
+
return json.length >= 100 ? '\x01' + compress(json) : json;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
|
|
46
57
|
async all(): Promise<T> {
|
|
47
58
|
let keys = this.getKeys(),
|
|
@@ -112,13 +123,13 @@ class LocalStorageDriver<T> implements Driver<T> {
|
|
|
112
123
|
|
|
113
124
|
async replace(entries: [keyof T, T[keyof T]][]): Promise<void> {
|
|
114
125
|
for (let i = 0, n = entries.length; i < n; i++) {
|
|
115
|
-
localStorage.setItem(this.key(entries[i][0]),
|
|
126
|
+
localStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
async set(key: keyof T, value: T[keyof T]): Promise<boolean> {
|
|
120
131
|
try {
|
|
121
|
-
localStorage.setItem(this.key(key),
|
|
132
|
+
localStorage.setItem(this.key(key), this.serialize(value));
|
|
122
133
|
return true;
|
|
123
134
|
}
|
|
124
135
|
catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compress, decompress } from '~/lz';
|
|
1
2
|
import type { Driver } from '~/types';
|
|
2
3
|
|
|
3
4
|
|
|
@@ -35,6 +36,10 @@ class SessionStorageDriver<T> implements Driver<T> {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
try {
|
|
39
|
+
if (value.charCodeAt(0) === 1) {
|
|
40
|
+
return JSON.parse(decompress(value.slice(1)));
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
return JSON.parse(value);
|
|
39
44
|
}
|
|
40
45
|
catch {
|
|
@@ -42,6 +47,12 @@ class SessionStorageDriver<T> implements Driver<T> {
|
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
private serialize(value: T[keyof T]): string {
|
|
51
|
+
let json = JSON.stringify(value);
|
|
52
|
+
|
|
53
|
+
return json.length >= 100 ? '\x01' + compress(json) : json;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
|
|
46
57
|
async all(): Promise<T> {
|
|
47
58
|
let keys = this.getKeys(),
|
|
@@ -112,13 +123,13 @@ class SessionStorageDriver<T> implements Driver<T> {
|
|
|
112
123
|
|
|
113
124
|
async replace(entries: [keyof T, T[keyof T]][]): Promise<void> {
|
|
114
125
|
for (let i = 0, n = entries.length; i < n; i++) {
|
|
115
|
-
sessionStorage.setItem(this.key(entries[i][0]),
|
|
126
|
+
sessionStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
async set(key: keyof T, value: T[keyof T]): Promise<boolean> {
|
|
120
131
|
try {
|
|
121
|
-
sessionStorage.setItem(this.key(key),
|
|
132
|
+
sessionStorage.setItem(this.key(key), this.serialize(value));
|
|
122
133
|
return true;
|
|
123
134
|
}
|
|
124
135
|
catch {
|