@esportsplus/web-storage 0.4.0 → 0.5.1
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 +17 -3
- package/build/drivers/sessionstorage.d.ts +1 -0
- package/build/drivers/sessionstorage.js +17 -3
- package/build/index.d.ts +1 -1
- package/build/index.js +82 -44
- package/build/lz.d.ts +3 -0
- package/build/lz.js +138 -0
- package/build/types.d.ts +1 -1
- package/package.json +5 -5
- package/src/drivers/localstorage.ts +22 -3
- package/src/drivers/sessionstorage.ts +22 -3
- package/src/index.ts +110 -52
- package/src/lz.ts +200 -0
- package/src/types.ts +1 -1
- package/tests/drivers/localstorage.ts +86 -0
- package/tests/drivers/sessionstorage.ts +85 -0
- package/tests/index.ts +564 -10
- package/tests/lz.ts +371 -0
- package/storage/feature-research.md +0 -173
package/src/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Driver, Filter, GlobalCallback, KeyCallback, MigrationFn, Options, SetOptions, TTLEnvelope } from './types';
|
|
1
|
+
import { encryption } from '@esportsplus/utilities';
|
|
3
2
|
import { DriverType } from './constants';
|
|
4
3
|
import { IndexedDBDriver } from '~/drivers/indexeddb';
|
|
5
4
|
import { LocalStorageDriver } from '~/drivers/localstorage';
|
|
6
5
|
import { MemoryDriver } from '~/drivers/memory';
|
|
7
6
|
import { SessionStorageDriver } from '~/drivers/sessionstorage';
|
|
8
7
|
|
|
8
|
+
import type { Cipher } from '@esportsplus/utilities';
|
|
9
|
+
import type { Driver, Filter, GlobalCallback, KeyCallback, MigrationFn, Options, SetOptions, TTLEnvelope } from './types';
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
const VERSION_KEY = '__version__';
|
|
11
13
|
|
|
@@ -17,15 +19,14 @@ function isEnvelope<V>(value: unknown): value is TTLEnvelope<V> {
|
|
|
17
19
|
&& '__v' in (value as Record<string, unknown>);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
async function deserialize<V>(value: unknown,
|
|
22
|
+
async function deserialize<V>(value: unknown, cipher: Cipher | null): Promise<V | undefined> {
|
|
21
23
|
if (value === undefined || value === null) {
|
|
22
24
|
return undefined;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
if (
|
|
27
|
+
if (cipher && typeof value === 'string') {
|
|
26
28
|
try {
|
|
27
|
-
value = await decrypt(value
|
|
28
|
-
value = JSON.parse(value as string);
|
|
29
|
+
value = await cipher.decrypt(value);
|
|
29
30
|
}
|
|
30
31
|
catch {
|
|
31
32
|
return undefined;
|
|
@@ -35,13 +36,13 @@ async function deserialize<V>(value: unknown, secret: string | null): Promise<V
|
|
|
35
36
|
return value as V;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
async function serialize<V>(value: V,
|
|
39
|
+
async function serialize<V>(value: V, cipher: Cipher | null): Promise<string | V> {
|
|
39
40
|
if (value === undefined || value === null) {
|
|
40
41
|
return value;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
if (
|
|
44
|
-
return encrypt(
|
|
44
|
+
if (cipher) {
|
|
45
|
+
return cipher.encrypt(value);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
return value;
|
|
@@ -101,14 +102,14 @@ async function migrate<T>(driver: Driver<T>, migrations: Record<number, Migratio
|
|
|
101
102
|
|
|
102
103
|
let transformed = await migrations[keys[i]]({ all: () => Promise.resolve(data) });
|
|
103
104
|
|
|
104
|
-
await driver.clear();
|
|
105
|
-
|
|
106
105
|
let entries: [keyof T, T[keyof T]][] = [];
|
|
107
106
|
|
|
108
107
|
for (let key in transformed) {
|
|
109
108
|
entries.push([key as keyof T, transformed[key] as T[keyof T]]);
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
await driver.clear();
|
|
112
|
+
|
|
112
113
|
if (entries.length > 0) {
|
|
113
114
|
await driver.replace(entries);
|
|
114
115
|
}
|
|
@@ -120,6 +121,8 @@ async function migrate<T>(driver: Driver<T>, migrations: Record<number, Migratio
|
|
|
120
121
|
|
|
121
122
|
class Local<T> {
|
|
122
123
|
|
|
124
|
+
private cipher: Cipher | null;
|
|
125
|
+
|
|
123
126
|
private driver: Driver<T>;
|
|
124
127
|
|
|
125
128
|
private globals: Set<GlobalCallback<T>>;
|
|
@@ -128,15 +131,13 @@ class Local<T> {
|
|
|
128
131
|
|
|
129
132
|
private ready: Promise<void>;
|
|
130
133
|
|
|
131
|
-
private secret: string | null;
|
|
132
|
-
|
|
133
134
|
private version: number;
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
constructor(options: Options, secret?: string) {
|
|
138
|
+
this.cipher = null;
|
|
137
139
|
this.globals = new Set();
|
|
138
140
|
this.listeners = new Map();
|
|
139
|
-
this.secret = secret || null;
|
|
140
141
|
|
|
141
142
|
let { migrations, name, version = 1 } = options;
|
|
142
143
|
|
|
@@ -155,11 +156,15 @@ class Local<T> {
|
|
|
155
156
|
this.driver = new IndexedDBDriver<T>(name, version);
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
let init = secret
|
|
160
|
+
? encryption(secret).then((c) => { this.cipher = c; })
|
|
161
|
+
: Promise.resolve();
|
|
162
|
+
|
|
158
163
|
if (migrations) {
|
|
159
|
-
this.ready = migrate(this.driver, migrations, version);
|
|
164
|
+
this.ready = init.then(() => migrate(this.driver, migrations, version));
|
|
160
165
|
}
|
|
161
166
|
else {
|
|
162
|
-
this.ready =
|
|
167
|
+
this.ready = init;
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -176,7 +181,7 @@ class Local<T> {
|
|
|
176
181
|
continue;
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw[key], this.
|
|
184
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw[key], this.cipher),
|
|
180
185
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
181
186
|
|
|
182
187
|
if (deserialized === undefined) {
|
|
@@ -192,7 +197,7 @@ class Local<T> {
|
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
if (expired.length > 0) {
|
|
195
|
-
this.driver.delete(expired);
|
|
200
|
+
await this.driver.delete(expired);
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
return result;
|
|
@@ -223,7 +228,7 @@ class Local<T> {
|
|
|
223
228
|
return;
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.
|
|
231
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher),
|
|
227
232
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
228
233
|
|
|
229
234
|
if (deserialized !== undefined && unwrapped.expired) {
|
|
@@ -244,23 +249,47 @@ class Local<T> {
|
|
|
244
249
|
async count(): Promise<number> {
|
|
245
250
|
await this.ready;
|
|
246
251
|
|
|
247
|
-
let
|
|
248
|
-
|
|
252
|
+
let expired: (keyof T)[] = [],
|
|
253
|
+
total = 0;
|
|
254
|
+
|
|
255
|
+
await this.driver.map(async (raw, key) => {
|
|
256
|
+
if (key as string === VERSION_KEY) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
249
259
|
|
|
250
|
-
|
|
260
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher),
|
|
261
|
+
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
262
|
+
|
|
263
|
+
if (deserialized === undefined) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (unwrapped.expired) {
|
|
268
|
+
expired.push(key);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
total++;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (expired.length > 0) {
|
|
276
|
+
await this.driver.delete(expired);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return total;
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
async delete(...keys: (keyof T)[]): Promise<void> {
|
|
254
283
|
await this.ready;
|
|
255
284
|
|
|
256
|
-
let oldValues = new Map<keyof T, T[keyof T] | undefined>()
|
|
285
|
+
let oldValues = new Map<keyof T, T[keyof T] | undefined>(),
|
|
286
|
+
raw = await this.driver.only(keys);
|
|
257
287
|
|
|
258
|
-
for (let
|
|
259
|
-
let
|
|
260
|
-
deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret),
|
|
288
|
+
for (let [key, value] of raw) {
|
|
289
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(value, this.cipher),
|
|
261
290
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
262
291
|
|
|
263
|
-
oldValues.set(
|
|
292
|
+
oldValues.set(key, deserialized === undefined ? undefined : unwrapped.value);
|
|
264
293
|
}
|
|
265
294
|
|
|
266
295
|
await this.driver.delete(keys);
|
|
@@ -284,7 +313,7 @@ class Local<T> {
|
|
|
284
313
|
return;
|
|
285
314
|
}
|
|
286
315
|
|
|
287
|
-
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.
|
|
316
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher),
|
|
288
317
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
289
318
|
|
|
290
319
|
if (deserialized === undefined) {
|
|
@@ -302,7 +331,7 @@ class Local<T> {
|
|
|
302
331
|
});
|
|
303
332
|
|
|
304
333
|
if (expired.length > 0) {
|
|
305
|
-
this.driver.delete(expired);
|
|
334
|
+
await this.driver.delete(expired);
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
return result;
|
|
@@ -315,7 +344,7 @@ class Local<T> {
|
|
|
315
344
|
|
|
316
345
|
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(
|
|
317
346
|
await this.driver.get(key),
|
|
318
|
-
this.
|
|
347
|
+
this.cipher
|
|
319
348
|
),
|
|
320
349
|
missing = false,
|
|
321
350
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
@@ -324,7 +353,7 @@ class Local<T> {
|
|
|
324
353
|
missing = true;
|
|
325
354
|
}
|
|
326
355
|
else if (unwrapped.expired) {
|
|
327
|
-
this.driver.delete([key]);
|
|
356
|
+
await this.driver.delete([key]);
|
|
328
357
|
missing = true;
|
|
329
358
|
}
|
|
330
359
|
|
|
@@ -346,9 +375,34 @@ class Local<T> {
|
|
|
346
375
|
async keys(): Promise<(keyof T)[]> {
|
|
347
376
|
await this.ready;
|
|
348
377
|
|
|
349
|
-
let
|
|
378
|
+
let expired: (keyof T)[] = [],
|
|
379
|
+
result: (keyof T)[] = [];
|
|
380
|
+
|
|
381
|
+
await this.driver.map(async (raw, key) => {
|
|
382
|
+
if (key as string === VERSION_KEY) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher),
|
|
387
|
+
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
388
|
+
|
|
389
|
+
if (deserialized === undefined) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (unwrapped.expired) {
|
|
394
|
+
expired.push(key);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
result.push(key);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (expired.length > 0) {
|
|
402
|
+
await this.driver.delete(expired);
|
|
403
|
+
}
|
|
350
404
|
|
|
351
|
-
return
|
|
405
|
+
return result;
|
|
352
406
|
}
|
|
353
407
|
|
|
354
408
|
async length(): Promise<number> {
|
|
@@ -368,7 +422,7 @@ class Local<T> {
|
|
|
368
422
|
return;
|
|
369
423
|
}
|
|
370
424
|
|
|
371
|
-
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.
|
|
425
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher),
|
|
372
426
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
373
427
|
|
|
374
428
|
if (deserialized === undefined) {
|
|
@@ -384,7 +438,7 @@ class Local<T> {
|
|
|
384
438
|
});
|
|
385
439
|
|
|
386
440
|
if (expired.length > 0) {
|
|
387
|
-
this.driver.delete(expired);
|
|
441
|
+
await this.driver.delete(expired);
|
|
388
442
|
}
|
|
389
443
|
}
|
|
390
444
|
|
|
@@ -396,7 +450,7 @@ class Local<T> {
|
|
|
396
450
|
result = {} as T;
|
|
397
451
|
|
|
398
452
|
for (let [key, value] of raw) {
|
|
399
|
-
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(value, this.
|
|
453
|
+
let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(value, this.cipher),
|
|
400
454
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
401
455
|
|
|
402
456
|
if (deserialized === undefined) {
|
|
@@ -412,7 +466,7 @@ class Local<T> {
|
|
|
412
466
|
}
|
|
413
467
|
|
|
414
468
|
if (expired.length > 0) {
|
|
415
|
-
this.driver.delete(expired);
|
|
469
|
+
await this.driver.delete(expired);
|
|
416
470
|
}
|
|
417
471
|
|
|
418
472
|
return result;
|
|
@@ -422,7 +476,7 @@ class Local<T> {
|
|
|
422
476
|
await this.ready;
|
|
423
477
|
|
|
424
478
|
let raw = await this.driver.get(key),
|
|
425
|
-
deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.
|
|
479
|
+
deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher);
|
|
426
480
|
|
|
427
481
|
if (deserialized === undefined) {
|
|
428
482
|
return false;
|
|
@@ -431,7 +485,7 @@ class Local<T> {
|
|
|
431
485
|
let unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
432
486
|
|
|
433
487
|
if (unwrapped.expired) {
|
|
434
|
-
this.driver.delete([key]);
|
|
488
|
+
await this.driver.delete([key]);
|
|
435
489
|
return false;
|
|
436
490
|
}
|
|
437
491
|
|
|
@@ -441,7 +495,7 @@ class Local<T> {
|
|
|
441
495
|
|
|
442
496
|
return this.driver.set(
|
|
443
497
|
key,
|
|
444
|
-
await serialize(unwrapped.value, this.
|
|
498
|
+
await serialize(unwrapped.value, this.cipher) as T[keyof T]
|
|
445
499
|
);
|
|
446
500
|
}
|
|
447
501
|
|
|
@@ -450,11 +504,15 @@ class Local<T> {
|
|
|
450
504
|
|
|
451
505
|
let entries: [keyof T, unknown][] = [],
|
|
452
506
|
failed: string[] = [],
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
507
|
+
fetchKeys = Object.keys(values) as (keyof T)[],
|
|
508
|
+
oldValues = new Map<keyof T, T[keyof T] | undefined>(),
|
|
509
|
+
raw = await this.driver.only(fetchKeys);
|
|
510
|
+
|
|
511
|
+
for (let key of fetchKeys) {
|
|
512
|
+
let value = raw.get(key),
|
|
513
|
+
deserialized = value !== undefined
|
|
514
|
+
? await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(value, this.cipher)
|
|
515
|
+
: undefined,
|
|
458
516
|
unwrapped = unwrap<T[keyof T]>(deserialized);
|
|
459
517
|
|
|
460
518
|
oldValues.set(key, deserialized === undefined ? undefined : unwrapped.value);
|
|
@@ -462,11 +520,11 @@ class Local<T> {
|
|
|
462
520
|
try {
|
|
463
521
|
entries.push([
|
|
464
522
|
key,
|
|
465
|
-
await serialize(values[key], this.
|
|
523
|
+
await serialize(values[key], this.cipher)
|
|
466
524
|
]);
|
|
467
525
|
}
|
|
468
526
|
catch {
|
|
469
|
-
failed.push(key);
|
|
527
|
+
failed.push(key as string);
|
|
470
528
|
}
|
|
471
529
|
}
|
|
472
530
|
|
|
@@ -488,7 +546,7 @@ class Local<T> {
|
|
|
488
546
|
|
|
489
547
|
try {
|
|
490
548
|
let oldRaw = await this.driver.get(key),
|
|
491
|
-
oldDeserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(oldRaw, this.
|
|
549
|
+
oldDeserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(oldRaw, this.cipher),
|
|
492
550
|
oldUnwrapped = unwrap<T[keyof T]>(oldDeserialized),
|
|
493
551
|
oldValue = oldDeserialized === undefined ? undefined : oldUnwrapped.value,
|
|
494
552
|
stored: T[keyof T] | string;
|
|
@@ -499,10 +557,10 @@ class Local<T> {
|
|
|
499
557
|
__v: value
|
|
500
558
|
};
|
|
501
559
|
|
|
502
|
-
stored = await serialize(envelope, this.
|
|
560
|
+
stored = await serialize(envelope, this.cipher) as T[keyof T];
|
|
503
561
|
}
|
|
504
562
|
else {
|
|
505
|
-
stored = await serialize(value, this.
|
|
563
|
+
stored = await serialize(value, this.cipher) as T[keyof T];
|
|
506
564
|
}
|
|
507
565
|
|
|
508
566
|
let result = await this.driver.set(key, stored as T[keyof T]);
|
|
@@ -545,7 +603,7 @@ class Local<T> {
|
|
|
545
603
|
await this.ready;
|
|
546
604
|
|
|
547
605
|
let raw = await this.driver.get(key),
|
|
548
|
-
deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.
|
|
606
|
+
deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.cipher);
|
|
549
607
|
|
|
550
608
|
if (deserialized === undefined) {
|
|
551
609
|
return -1;
|
|
@@ -558,7 +616,7 @@ class Local<T> {
|
|
|
558
616
|
let remaining = deserialized.__e - Date.now();
|
|
559
617
|
|
|
560
618
|
if (remaining <= 0) {
|
|
561
|
-
this.driver.delete([key]);
|
|
619
|
+
await this.driver.delete([key]);
|
|
562
620
|
return -1;
|
|
563
621
|
}
|
|
564
622
|
|
package/src/lz.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
type CompressCtx = { bitsInBuffer: number; buffer: number; numBits: number; output: number[] };
|
|
2
|
+
type DecompressCtx = { bitPos: number; compressed: string; currentValue: number; pos: number };
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
let MAX_DECOMPRESSED_SIZE = 10_485_760;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function emitLiteral(ctx: CompressCtx, ch: string) {
|
|
9
|
+
let code = ch.charCodeAt(0);
|
|
10
|
+
|
|
11
|
+
if (code < 256) {
|
|
12
|
+
writeBits(ctx, ctx.numBits, 0);
|
|
13
|
+
writeBits(ctx, 8, code);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
writeBits(ctx, ctx.numBits, 1);
|
|
17
|
+
writeBits(ctx, 16, code);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readBits(ctx: DecompressCtx, n: number): number {
|
|
22
|
+
let result = 0;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < n; i++) {
|
|
25
|
+
if (ctx.bitPos > 15) {
|
|
26
|
+
if (ctx.pos >= ctx.compressed.length) {
|
|
27
|
+
throw new Error('LZ: unexpected end of compressed data');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ctx.currentValue = ctx.compressed.charCodeAt(ctx.pos++) - 1;
|
|
31
|
+
ctx.bitPos = 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
result = (result << 1) | ((ctx.currentValue >> (15 - ctx.bitPos)) & 1);
|
|
35
|
+
ctx.bitPos++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeBits(ctx: CompressCtx, n: number, value: number) {
|
|
42
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
43
|
+
ctx.buffer = (ctx.buffer << 1) | ((value >> i) & 1);
|
|
44
|
+
ctx.bitsInBuffer++;
|
|
45
|
+
|
|
46
|
+
if (ctx.bitsInBuffer === 16) {
|
|
47
|
+
ctx.output.push(ctx.buffer + 1);
|
|
48
|
+
ctx.buffer = 0;
|
|
49
|
+
ctx.bitsInBuffer = 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const compress = (input: string): string => {
|
|
56
|
+
if (!input) {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let ctx: CompressCtx = { bitsInBuffer: 0, buffer: 0, numBits: 2, output: [] },
|
|
61
|
+
dictSize = 3,
|
|
62
|
+
dictionary = new Map<string, number>(),
|
|
63
|
+
w = '';
|
|
64
|
+
|
|
65
|
+
for (let i = 0, n = input.length; i < n; i++) {
|
|
66
|
+
let c = input[i],
|
|
67
|
+
wc = w + c;
|
|
68
|
+
|
|
69
|
+
if (dictionary.has(wc)) {
|
|
70
|
+
w = wc;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (w.length > 0) {
|
|
75
|
+
if (dictionary.has(w)) {
|
|
76
|
+
writeBits(ctx, ctx.numBits, dictionary.get(w)!);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
emitLiteral(ctx, w);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
dictionary.set(wc, dictSize++);
|
|
83
|
+
|
|
84
|
+
if (dictSize > (1 << ctx.numBits)) {
|
|
85
|
+
ctx.numBits++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
w = c;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (w.length > 0) {
|
|
93
|
+
if (dictionary.has(w)) {
|
|
94
|
+
writeBits(ctx, ctx.numBits, dictionary.get(w)!);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
emitLiteral(ctx, w);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Trailing dict advance: ensures the decompressor's last placeholder growth
|
|
102
|
+
// matches (the decompressor will push a placeholder before reading EOF)
|
|
103
|
+
dictSize++;
|
|
104
|
+
|
|
105
|
+
if (dictSize > (1 << ctx.numBits)) {
|
|
106
|
+
ctx.numBits++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
writeBits(ctx, ctx.numBits, 2);
|
|
110
|
+
|
|
111
|
+
if (ctx.bitsInBuffer > 0) {
|
|
112
|
+
ctx.output.push(((ctx.buffer << (16 - ctx.bitsInBuffer)) & 0xFFFF) + 1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ctx.output.push((ctx.bitsInBuffer === 0 ? 16 : ctx.bitsInBuffer) + 1);
|
|
116
|
+
|
|
117
|
+
return String.fromCharCode(...ctx.output);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const decompress = (compressed: string): string => {
|
|
121
|
+
if (!compressed) {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let ctx: DecompressCtx = { bitPos: 16, compressed: '', currentValue: 0, pos: 0 },
|
|
126
|
+
dictSize = 3,
|
|
127
|
+
dictionary: string[] = [],
|
|
128
|
+
numBits = 2;
|
|
129
|
+
|
|
130
|
+
ctx.compressed = compressed.substring(0, compressed.length - 1);
|
|
131
|
+
|
|
132
|
+
let code = readBits(ctx, numBits),
|
|
133
|
+
entry: string;
|
|
134
|
+
|
|
135
|
+
if (code === 0) {
|
|
136
|
+
entry = String.fromCharCode(readBits(ctx, 8));
|
|
137
|
+
}
|
|
138
|
+
else if (code === 1) {
|
|
139
|
+
entry = String.fromCharCode(readBits(ctx, 16));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let parts: string[] = [entry],
|
|
146
|
+
totalLength = entry.length,
|
|
147
|
+
w = entry;
|
|
148
|
+
|
|
149
|
+
while (true) {
|
|
150
|
+
// Reserve dict slot BEFORE reading (matches compressor's add-before-next-emit timing)
|
|
151
|
+
let slotIdx = dictionary.length;
|
|
152
|
+
|
|
153
|
+
dictionary.push('');
|
|
154
|
+
dictSize++;
|
|
155
|
+
|
|
156
|
+
if (dictSize > (1 << numBits)) {
|
|
157
|
+
numBits++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
code = readBits(ctx, numBits);
|
|
161
|
+
|
|
162
|
+
if (code === 2) {
|
|
163
|
+
dictionary.pop();
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let slotCode = slotIdx + 3;
|
|
168
|
+
|
|
169
|
+
if (code === 0) {
|
|
170
|
+
entry = String.fromCharCode(readBits(ctx, 8));
|
|
171
|
+
}
|
|
172
|
+
else if (code === 1) {
|
|
173
|
+
entry = String.fromCharCode(readBits(ctx, 16));
|
|
174
|
+
}
|
|
175
|
+
else if (code === slotCode) {
|
|
176
|
+
entry = w + w[0];
|
|
177
|
+
}
|
|
178
|
+
else if (code >= 3 && code < slotCode) {
|
|
179
|
+
entry = dictionary[code - 3];
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw new Error('LZ: invalid decompression code');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
dictionary[slotIdx] = w + entry[0];
|
|
186
|
+
parts.push(entry);
|
|
187
|
+
totalLength += entry.length;
|
|
188
|
+
|
|
189
|
+
if (totalLength > MAX_DECOMPRESSED_SIZE) {
|
|
190
|
+
throw new Error('LZ: decompressed output exceeds size limit');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
w = entry;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return parts.join('');
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
export { compress, decompress };
|
package/src/types.ts
CHANGED
|
@@ -44,4 +44,4 @@ type GlobalCallback<T> = (key: keyof T, newValue: T[keyof T] | undefined, oldVal
|
|
|
44
44
|
type KeyCallback<T, K extends keyof T = keyof T> = (newValue: T[K] | undefined, oldValue: T[K] | undefined) => void;
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
export type { Driver, Filter, GlobalCallback, KeyCallback,
|
|
47
|
+
export type { Driver, Filter, GlobalCallback, KeyCallback, MigrationFn, Options, SetOptions, TTLEnvelope };
|
|
@@ -232,6 +232,92 @@ describe('LocalStorageDriver', () => {
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
describe('compression', () => {
|
|
236
|
+
type LargeData = { bio: string };
|
|
237
|
+
|
|
238
|
+
let largeDriver: LocalStorageDriver<LargeData>,
|
|
239
|
+
largeValue: string;
|
|
240
|
+
|
|
241
|
+
beforeEach(() => {
|
|
242
|
+
largeDriver = new LocalStorageDriver<LargeData>('lz', 1);
|
|
243
|
+
largeValue = 'a'.repeat(200);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('stores small values without compression prefix', async () => {
|
|
247
|
+
await driver.set('name', 'alice');
|
|
248
|
+
|
|
249
|
+
let raw = localStorage.getItem('test:1:name')!;
|
|
250
|
+
|
|
251
|
+
expect(raw.charCodeAt(0)).not.toBe(1);
|
|
252
|
+
expect(raw).toBe('"alice"');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('stores large values with \\x01 prefix', async () => {
|
|
256
|
+
await largeDriver.set('bio', largeValue);
|
|
257
|
+
|
|
258
|
+
let raw = localStorage.getItem('lz:1:bio')!;
|
|
259
|
+
|
|
260
|
+
expect(raw.charCodeAt(0)).toBe(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('round-trips large values through set/get', async () => {
|
|
264
|
+
await largeDriver.set('bio', largeValue);
|
|
265
|
+
|
|
266
|
+
expect(await largeDriver.get('bio')).toBe(largeValue);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('round-trips large values through replace/all', async () => {
|
|
270
|
+
await largeDriver.replace([['bio', largeValue]]);
|
|
271
|
+
|
|
272
|
+
let all = await largeDriver.all();
|
|
273
|
+
|
|
274
|
+
expect(all.bio).toBe(largeValue);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('reads existing uncompressed values (backward compat)', async () => {
|
|
278
|
+
localStorage.setItem('lz:1:bio', JSON.stringify(largeValue));
|
|
279
|
+
|
|
280
|
+
expect(await largeDriver.get('bio')).toBe(largeValue);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('compressed output is smaller than raw JSON', async () => {
|
|
284
|
+
await largeDriver.set('bio', largeValue);
|
|
285
|
+
|
|
286
|
+
let compressed = localStorage.getItem('lz:1:bio')!,
|
|
287
|
+
raw = JSON.stringify(largeValue);
|
|
288
|
+
|
|
289
|
+
expect(compressed.length).toBeLessThan(raw.length);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('handles 100-byte boundary correctly', async () => {
|
|
293
|
+
type BoundaryData = { val: string };
|
|
294
|
+
|
|
295
|
+
let boundaryDriver = new LocalStorageDriver<BoundaryData>('bound', 1);
|
|
296
|
+
|
|
297
|
+
// JSON.stringify('"' + 'x'.repeat(97) + '"') = 97 chars + 2 quotes = "xxx...x" = 99 chars inside quotes, total 99+2=101? No.
|
|
298
|
+
// JSON.stringify('x'.repeat(96)) = '"' + 'x'*96 + '"' = 98 bytes < 100 => no compress
|
|
299
|
+
await boundaryDriver.set('val', 'x'.repeat(96));
|
|
300
|
+
|
|
301
|
+
let rawSmall = localStorage.getItem('bound:1:val')!;
|
|
302
|
+
|
|
303
|
+
expect(rawSmall.charCodeAt(0)).not.toBe(1);
|
|
304
|
+
|
|
305
|
+
// JSON.stringify('x'.repeat(98)) = '"' + 'x'*98 + '"' = 100 bytes >= 100 => compress
|
|
306
|
+
await boundaryDriver.set('val', 'x'.repeat(98));
|
|
307
|
+
|
|
308
|
+
let rawLarge = localStorage.getItem('bound:1:val')!;
|
|
309
|
+
|
|
310
|
+
expect(rawLarge.charCodeAt(0)).toBe(1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('parse returns undefined for corrupted compressed data', async () => {
|
|
314
|
+
localStorage.setItem('lz:1:bio', '\x01corrupted-data');
|
|
315
|
+
|
|
316
|
+
expect(await largeDriver.get('bio')).toBeUndefined();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
|
|
235
321
|
describe('set / get', () => {
|
|
236
322
|
it('overwrites existing key', async () => {
|
|
237
323
|
await driver.set('name', 'alice');
|