@esportsplus/web-storage 0.3.5 → 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/constants.d.ts +3 -1
- package/build/constants.js +2 -0
- package/build/drivers/localstorage.d.ts +1 -0
- package/build/drivers/localstorage.js +10 -2
- package/build/drivers/memory.d.ts +16 -0
- package/build/drivers/memory.js +64 -0
- package/build/drivers/sessionstorage.d.ts +20 -0
- package/build/drivers/sessionstorage.js +102 -0
- package/build/index.d.ts +12 -2
- package/build/index.js +271 -31
- package/build/lz.d.ts +3 -0
- package/build/lz.js +134 -0
- package/build/types.d.ts +16 -2
- package/package.json +6 -2
- package/src/constants.ts +3 -1
- package/src/drivers/localstorage.ts +13 -2
- package/src/drivers/memory.ts +92 -0
- package/src/drivers/sessionstorage.ts +142 -0
- package/src/index.ts +420 -39
- package/src/lz.ts +192 -0
- package/src/types.ts +23 -2
- package/storage/test-audit-web-storage.md +74 -0
- package/tests/drivers/indexeddb.ts +297 -0
- package/tests/drivers/localstorage.ts +376 -0
- package/tests/drivers/memory.ts +257 -0
- package/tests/drivers/sessionstorage.ts +375 -0
- package/tests/index.ts +1871 -0
- package/tests/lz.ts +324 -0
- package/vitest.config.ts +16 -0
package/src/lz.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
function emitLiteral(ctx: CompressCtx, ch: string) {
|
|
6
|
+
let code = ch.charCodeAt(0);
|
|
7
|
+
|
|
8
|
+
if (code < 256) {
|
|
9
|
+
writeBits(ctx, ctx.numBits, 0);
|
|
10
|
+
writeBits(ctx, 8, code);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
writeBits(ctx, ctx.numBits, 1);
|
|
14
|
+
writeBits(ctx, 16, code);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readBits(ctx: DecompressCtx, n: number): number {
|
|
19
|
+
let result = 0;
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
if (ctx.bitPos > 15) {
|
|
23
|
+
ctx.currentValue = ctx.compressed.charCodeAt(ctx.pos++) - 1;
|
|
24
|
+
ctx.bitPos = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
result = (result << 1) | ((ctx.currentValue >> (15 - ctx.bitPos)) & 1);
|
|
28
|
+
ctx.bitPos++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeBits(ctx: CompressCtx, n: number, value: number) {
|
|
35
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
36
|
+
ctx.buffer = (ctx.buffer << 1) | ((value >> i) & 1);
|
|
37
|
+
ctx.bitsInBuffer++;
|
|
38
|
+
|
|
39
|
+
if (ctx.bitsInBuffer === 16) {
|
|
40
|
+
ctx.output.push(ctx.buffer + 1);
|
|
41
|
+
ctx.buffer = 0;
|
|
42
|
+
ctx.bitsInBuffer = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
const compress = (input: string): string => {
|
|
49
|
+
if (!input) {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let ctx: CompressCtx = { bitsInBuffer: 0, buffer: 0, numBits: 2, output: [] },
|
|
54
|
+
dictSize = 3,
|
|
55
|
+
dictionary = new Map<string, number>(),
|
|
56
|
+
w = '';
|
|
57
|
+
|
|
58
|
+
for (let i = 0, n = input.length; i < n; i++) {
|
|
59
|
+
let c = input[i],
|
|
60
|
+
wc = w + c;
|
|
61
|
+
|
|
62
|
+
if (dictionary.has(wc)) {
|
|
63
|
+
w = wc;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (w.length > 0) {
|
|
68
|
+
if (dictionary.has(w)) {
|
|
69
|
+
writeBits(ctx, ctx.numBits, dictionary.get(w)!);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
emitLiteral(ctx, w);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dictionary.set(wc, dictSize++);
|
|
76
|
+
|
|
77
|
+
if (dictSize > (1 << ctx.numBits)) {
|
|
78
|
+
ctx.numBits++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
w = c;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (w.length > 0) {
|
|
86
|
+
if (dictionary.has(w)) {
|
|
87
|
+
writeBits(ctx, ctx.numBits, dictionary.get(w)!);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
emitLiteral(ctx, w);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Trailing dict advance: ensures the decompressor's last placeholder growth
|
|
95
|
+
// matches (the decompressor will push a placeholder before reading EOF)
|
|
96
|
+
dictSize++;
|
|
97
|
+
|
|
98
|
+
if (dictSize > (1 << ctx.numBits)) {
|
|
99
|
+
ctx.numBits++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
writeBits(ctx, ctx.numBits, 2);
|
|
103
|
+
|
|
104
|
+
if (ctx.bitsInBuffer > 0) {
|
|
105
|
+
ctx.output.push(((ctx.buffer << (16 - ctx.bitsInBuffer)) & 0xFFFF) + 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ctx.output.push((ctx.bitsInBuffer === 0 ? 16 : ctx.bitsInBuffer) + 1);
|
|
109
|
+
|
|
110
|
+
let chars: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (let i = 0, n = ctx.output.length; i < n; i++) {
|
|
113
|
+
chars.push(String.fromCharCode(ctx.output[i]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return chars.join('');
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const decompress = (compressed: string): string => {
|
|
120
|
+
if (!compressed) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let ctx: DecompressCtx = { bitPos: 16, compressed: '', currentValue: 0, pos: 0 },
|
|
125
|
+
dictSize = 3,
|
|
126
|
+
dictionary: string[] = [],
|
|
127
|
+
numBits = 2;
|
|
128
|
+
|
|
129
|
+
ctx.compressed = compressed.substring(0, compressed.length - 1);
|
|
130
|
+
|
|
131
|
+
let code = readBits(ctx, numBits),
|
|
132
|
+
entry: string;
|
|
133
|
+
|
|
134
|
+
if (code === 0) {
|
|
135
|
+
entry = String.fromCharCode(readBits(ctx, 8));
|
|
136
|
+
}
|
|
137
|
+
else if (code === 1) {
|
|
138
|
+
entry = String.fromCharCode(readBits(ctx, 16));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let result: string[] = [entry],
|
|
145
|
+
w = entry;
|
|
146
|
+
|
|
147
|
+
while (true) {
|
|
148
|
+
// Reserve dict slot BEFORE reading (matches compressor's add-before-next-emit timing)
|
|
149
|
+
let slotIdx = dictionary.length;
|
|
150
|
+
|
|
151
|
+
dictionary.push('');
|
|
152
|
+
dictSize++;
|
|
153
|
+
|
|
154
|
+
if (dictSize > (1 << numBits)) {
|
|
155
|
+
numBits++;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
code = readBits(ctx, numBits);
|
|
159
|
+
|
|
160
|
+
if (code === 2) {
|
|
161
|
+
dictionary.pop();
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let slotCode = slotIdx + 3;
|
|
166
|
+
|
|
167
|
+
if (code === 0) {
|
|
168
|
+
entry = String.fromCharCode(readBits(ctx, 8));
|
|
169
|
+
}
|
|
170
|
+
else if (code === 1) {
|
|
171
|
+
entry = String.fromCharCode(readBits(ctx, 16));
|
|
172
|
+
}
|
|
173
|
+
else if (code === slotCode) {
|
|
174
|
+
entry = w + w[0];
|
|
175
|
+
}
|
|
176
|
+
else if (code >= 3 && code < slotCode) {
|
|
177
|
+
entry = dictionary[code - 3];
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
throw new Error('LZ: invalid decompression code');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
dictionary[slotIdx] = w + entry[0];
|
|
184
|
+
result.push(entry);
|
|
185
|
+
w = entry;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result.join('');
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
export { compress, decompress };
|
package/src/types.ts
CHANGED
|
@@ -16,11 +16,32 @@ interface Driver<T> {
|
|
|
16
16
|
|
|
17
17
|
type Filter<T> = (data: { i: number; key: keyof T; stop: VoidFunction; value: T[keyof T] }) => boolean | Promise<boolean>;
|
|
18
18
|
|
|
19
|
+
type MigrationContext = {
|
|
20
|
+
all(): Promise<Record<string, unknown>>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type MigrationFn = (old: MigrationContext) => Promise<Record<string, unknown>>;
|
|
24
|
+
|
|
19
25
|
type Options = {
|
|
20
|
-
driver?: DriverType.IndexedDB | DriverType.LocalStorage;
|
|
26
|
+
driver?: DriverType.IndexedDB | DriverType.LocalStorage | DriverType.Memory | DriverType.SessionStorage;
|
|
27
|
+
migrations?: Record<number, MigrationFn>;
|
|
21
28
|
name: string;
|
|
22
29
|
version: number;
|
|
23
30
|
};
|
|
24
31
|
|
|
32
|
+
type SetOptions = {
|
|
33
|
+
ttl?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TTLEnvelope<V> = {
|
|
37
|
+
__e: number;
|
|
38
|
+
__v: V;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
type GlobalCallback<T> = (key: keyof T, newValue: T[keyof T] | undefined, oldValue: T[keyof T] | undefined) => void;
|
|
43
|
+
|
|
44
|
+
type KeyCallback<T, K extends keyof T = keyof T> = (newValue: T[K] | undefined, oldValue: T[K] | undefined) => void;
|
|
45
|
+
|
|
25
46
|
|
|
26
|
-
export type { Driver, Filter, Options };
|
|
47
|
+
export type { Driver, Filter, GlobalCallback, KeyCallback, MigrationContext, MigrationFn, Options, SetOptions, TTLEnvelope };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Test Audit: @esportsplus/web-storage
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
- Source modules: 8
|
|
5
|
+
- Tested modules: 8 (100%)
|
|
6
|
+
- Benchmarked modules: 0 (0%)
|
|
7
|
+
- Total tests: 253
|
|
8
|
+
- Total gaps found: 23
|
|
9
|
+
|
|
10
|
+
## Missing Tests (Priority Order)
|
|
11
|
+
|
|
12
|
+
| Module | Export / Path | Type | Risk |
|
|
13
|
+
|--------|-------------|------|------|
|
|
14
|
+
| Local<T> + SessionStorage | All Local<T> methods with SS driver | integration | HIGH — only 1 factory test exists for SS; zero method coverage at Local<T> layer |
|
|
15
|
+
| Local<T> + Memory | TTL methods (ttl, persist, cleanup) | integration | HIGH — TTL logic is driver-agnostic but untested against Memory driver |
|
|
16
|
+
| Local<T> + Memory | get(key, factory) | integration | MED — factory tested for IDB/LS but not Memory |
|
|
17
|
+
| Local<T> + Memory | encryption (with secret) | integration | MED — no encryption round-trip tests for Memory driver |
|
|
18
|
+
| Local<T> + Memory | filter, only, map, length | integration | MED — bulk read operations untested at Local<T> layer for Memory |
|
|
19
|
+
| Local<T> | persist() on non-existent key | edge case | MED — returns false by code inspection, no test |
|
|
20
|
+
| Local<T> | persist() on already-permanent key | edge case | LOW — returns true, no test |
|
|
21
|
+
| Local<T> | cleanup() subscription notifications | integration | MED — validator flagged: cleanup fires notify but no test covers it |
|
|
22
|
+
| Local<T> | get() TTL expiry + subscription | edge case | MED — expired get does fire-and-forget delete but bypasses subscription-aware delete path; behavior unclear |
|
|
23
|
+
| Local<T> | count() with VERSION_KEY present | edge case | MED — should return count-1 when migrations active; no test |
|
|
24
|
+
| Local<T> | keys() with VERSION_KEY present | edge case | MED — should exclude __version__; no test |
|
|
25
|
+
| Local<T> + encryption | get(key, factory) + encryption | integration | LOW — factory + encrypt combo untested |
|
|
26
|
+
| Local<T> + LS | compression + encryption combined | integration | MED — encrypted ciphertext stored via driver which may attempt compression; round-trip untested |
|
|
27
|
+
|
|
28
|
+
## Shallow Tests
|
|
29
|
+
|
|
30
|
+
| Module | Export | Covered | Missing |
|
|
31
|
+
|--------|--------|---------|---------|
|
|
32
|
+
| Local<T>.persist() | IDB, LS | happy path (has TTL, removes it) | non-existent key, already-permanent key, expired key |
|
|
33
|
+
| Local<T>.cleanup() | IDB, LS | removes expired entries | empty store, no expired entries, subscription notifications |
|
|
34
|
+
| Local<T>.clear() | IDB, LS, Memory | clears all + notifies | VERSION_KEY preserved after clear (migration stores re-init) |
|
|
35
|
+
| Local<T>.subscribe() | Memory | set, delete, replace, clear, unsubscribe | cleanup notifications, factory-triggered notifications, TTL expiry notifications |
|
|
36
|
+
| Local<T>.map() | IDB, LS, Memory | iterates entries | TTL filtering + VERSION_KEY filtering combined |
|
|
37
|
+
| LZ compress/decompress | round-trip | all string types | very large strings (>100KB), strings that produce larger output than input (random/high-entropy) |
|
|
38
|
+
| LocalStorageDriver.parse() | error handling | corrupted compressed data | corrupted non-compressed JSON, null byte in stored data |
|
|
39
|
+
|
|
40
|
+
## Missing Benchmarks
|
|
41
|
+
|
|
42
|
+
No benchmark infrastructure exists. For a storage library, benchmarks would be useful for:
|
|
43
|
+
|
|
44
|
+
| Module | Export | Reason |
|
|
45
|
+
|--------|--------|--------|
|
|
46
|
+
| LZ compress/decompress | compress() | Called on every localStorage write ≥100 bytes |
|
|
47
|
+
| LZ compress/decompress | decompress() | Called on every localStorage read of compressed data |
|
|
48
|
+
| LocalStorageDriver | set/get | Hot path for localStorage operations |
|
|
49
|
+
| IndexedDBDriver | set/get/all | Async I/O operations, would reveal contention |
|
|
50
|
+
| Local<T> | set with encryption | Encryption + serialization overhead |
|
|
51
|
+
|
|
52
|
+
## Stale Tests
|
|
53
|
+
|
|
54
|
+
None found. All test references match current exports.
|
|
55
|
+
|
|
56
|
+
## Recommendations
|
|
57
|
+
|
|
58
|
+
### Priority 1: SessionStorage Local<T> integration (HIGH)
|
|
59
|
+
The sessionStorage driver has full driver-level tests (36) but almost zero Local<T> integration tests (just 1 factory test). Add at minimum: set/get, all, delete, clear, count, keys — mirroring the existing Memory driver block. Encryption and TTL should also be tested since the driver shares serialization logic with localStorage but includes compression.
|
|
60
|
+
|
|
61
|
+
### Priority 2: Memory driver feature coverage (HIGH)
|
|
62
|
+
TTL, persist, cleanup, get(key, factory), encryption, and bulk read operations are untested at the Local<T> layer for the Memory driver. Since Memory is the recommended driver for unit testing, these gaps are ironic — users testing their own code with Memory may hit untested paths.
|
|
63
|
+
|
|
64
|
+
### Priority 3: Cross-feature edge cases (MED)
|
|
65
|
+
- cleanup() + subscription notifications
|
|
66
|
+
- get() TTL expiry + subscription side-effects
|
|
67
|
+
- count()/keys() with VERSION_KEY present (migrations active)
|
|
68
|
+
- Compression + encryption combined round-trip
|
|
69
|
+
- persist() on non-existent and already-permanent keys
|
|
70
|
+
|
|
71
|
+
### Priority 4: LZ compression boundaries (LOW)
|
|
72
|
+
- Very large strings (100KB+)
|
|
73
|
+
- High-entropy strings that don't compress
|
|
74
|
+
- Explicit test that compression never increases size by more than a bounded amount
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { IndexedDBDriver } from '~/drivers/indexeddb';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type TestData = { age: number; name: string; tags: string[] };
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
let id = 0;
|
|
12
|
+
|
|
13
|
+
function uid() {
|
|
14
|
+
return `test-db-${++id}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
describe('IndexedDBDriver', () => {
|
|
19
|
+
|
|
20
|
+
describe('all', () => {
|
|
21
|
+
it('returns all stored key-value pairs', async () => {
|
|
22
|
+
let db = uid(),
|
|
23
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
24
|
+
|
|
25
|
+
await driver.set('name', 'alice');
|
|
26
|
+
await driver.set('age', 30);
|
|
27
|
+
|
|
28
|
+
let result = await driver.all();
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ age: 30, name: 'alice' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns empty object when storage is empty', async () => {
|
|
34
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
35
|
+
|
|
36
|
+
expect(await driver.all()).toEqual({});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
describe('clear', () => {
|
|
42
|
+
it('removes all entries', async () => {
|
|
43
|
+
let db = uid(),
|
|
44
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
45
|
+
|
|
46
|
+
await driver.set('name', 'alice');
|
|
47
|
+
await driver.set('age', 25);
|
|
48
|
+
await driver.clear();
|
|
49
|
+
|
|
50
|
+
expect(await driver.count()).toBe(0);
|
|
51
|
+
expect(await driver.all()).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
describe('constructor / connect', () => {
|
|
57
|
+
it('creates database and object store', async () => {
|
|
58
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
59
|
+
|
|
60
|
+
expect(await driver.count()).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reuses connection for same name+version', async () => {
|
|
64
|
+
let db = uid(),
|
|
65
|
+
a = new IndexedDBDriver<TestData>(db, 1),
|
|
66
|
+
b = new IndexedDBDriver<TestData>(db, 1);
|
|
67
|
+
|
|
68
|
+
await a.set('name', 'alice');
|
|
69
|
+
|
|
70
|
+
expect(await b.get('name')).toBe('alice');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
describe('count', () => {
|
|
76
|
+
it('returns 0 when empty', async () => {
|
|
77
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
78
|
+
|
|
79
|
+
expect(await driver.count()).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns correct count of stored items', async () => {
|
|
83
|
+
let db = uid(),
|
|
84
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
85
|
+
|
|
86
|
+
await driver.set('name', 'alice');
|
|
87
|
+
await driver.set('age', 30);
|
|
88
|
+
await driver.set('tags', ['a', 'b']);
|
|
89
|
+
|
|
90
|
+
expect(await driver.count()).toBe(3);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
describe('delete', () => {
|
|
96
|
+
it('handles deleting non-existent keys without error', async () => {
|
|
97
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
98
|
+
|
|
99
|
+
await expect(driver.delete(['name', 'age'])).resolves.toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('removes specified keys', async () => {
|
|
103
|
+
let db = uid(),
|
|
104
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
105
|
+
|
|
106
|
+
await driver.set('name', 'alice');
|
|
107
|
+
await driver.set('age', 30);
|
|
108
|
+
await driver.set('tags', ['x']);
|
|
109
|
+
|
|
110
|
+
await driver.delete(['name', 'tags']);
|
|
111
|
+
|
|
112
|
+
expect(await driver.get('name')).toBeUndefined();
|
|
113
|
+
expect(await driver.get('tags')).toBeUndefined();
|
|
114
|
+
expect(await driver.get('age')).toBe(30);
|
|
115
|
+
expect(await driver.count()).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
describe('keys', () => {
|
|
121
|
+
it('returns all keys', async () => {
|
|
122
|
+
let db = uid(),
|
|
123
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
124
|
+
|
|
125
|
+
await driver.set('name', 'alice');
|
|
126
|
+
await driver.set('age', 30);
|
|
127
|
+
|
|
128
|
+
let keys = await driver.keys();
|
|
129
|
+
|
|
130
|
+
expect(keys.sort()).toEqual(['age', 'name']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns empty array when empty', async () => {
|
|
134
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
135
|
+
|
|
136
|
+
expect(await driver.keys()).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
describe('map', () => {
|
|
142
|
+
it('iterates over all entries with correct value, key, and index', async () => {
|
|
143
|
+
let db = uid(),
|
|
144
|
+
driver = new IndexedDBDriver<TestData>(db, 1),
|
|
145
|
+
entries: { i: number; key: keyof TestData; value: TestData[keyof TestData] }[] = [];
|
|
146
|
+
|
|
147
|
+
await driver.set('age', 25);
|
|
148
|
+
await driver.set('name', 'bob');
|
|
149
|
+
|
|
150
|
+
await driver.map((value, key, i) => {
|
|
151
|
+
entries.push({ i, key, value });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(entries).toHaveLength(2);
|
|
155
|
+
|
|
156
|
+
entries.sort((a, b) => (a.key as string).localeCompare(b.key as string));
|
|
157
|
+
|
|
158
|
+
expect(entries[0]).toEqual({ i: expect.any(Number), key: 'age', value: 25 });
|
|
159
|
+
expect(entries[1]).toEqual({ i: expect.any(Number), key: 'name', value: 'bob' });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('works on empty store with no callback invocations', async () => {
|
|
163
|
+
let called = false,
|
|
164
|
+
driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
165
|
+
|
|
166
|
+
await driver.map(() => {
|
|
167
|
+
called = true;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(called).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
describe('only', () => {
|
|
176
|
+
it('returns empty Map when no keys match', async () => {
|
|
177
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1),
|
|
178
|
+
result = await driver.only(['name', 'age']);
|
|
179
|
+
|
|
180
|
+
expect(result.size).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns Map with only requested keys', async () => {
|
|
184
|
+
let db = uid(),
|
|
185
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
186
|
+
|
|
187
|
+
await driver.set('name', 'alice');
|
|
188
|
+
await driver.set('age', 30);
|
|
189
|
+
await driver.set('tags', ['a']);
|
|
190
|
+
|
|
191
|
+
let result = await driver.only(['name', 'tags']);
|
|
192
|
+
|
|
193
|
+
expect(result.size).toBe(2);
|
|
194
|
+
expect(result.get('name')).toBe('alice');
|
|
195
|
+
expect(result.get('tags')).toEqual(['a']);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('skips keys that do not exist', async () => {
|
|
199
|
+
let db = uid(),
|
|
200
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
201
|
+
|
|
202
|
+
await driver.set('name', 'alice');
|
|
203
|
+
|
|
204
|
+
let result = await driver.only(['name', 'age']);
|
|
205
|
+
|
|
206
|
+
expect(result.size).toBe(1);
|
|
207
|
+
expect(result.get('name')).toBe('alice');
|
|
208
|
+
expect(result.has('age')).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
describe('replace', () => {
|
|
214
|
+
it('replaces multiple entries at once', async () => {
|
|
215
|
+
let db = uid(),
|
|
216
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
217
|
+
|
|
218
|
+
await driver.set('name', 'alice');
|
|
219
|
+
|
|
220
|
+
await driver.replace([
|
|
221
|
+
['name', 'bob'],
|
|
222
|
+
['age', 42],
|
|
223
|
+
['tags', ['x', 'y']]
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
expect(await driver.get('name')).toBe('bob');
|
|
227
|
+
expect(await driver.get('age')).toBe(42);
|
|
228
|
+
expect(await driver.get('tags')).toEqual(['x', 'y']);
|
|
229
|
+
expect(await driver.count()).toBe(3);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
describe('set / get', () => {
|
|
235
|
+
it('overwrites existing key', async () => {
|
|
236
|
+
let db = uid(),
|
|
237
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
238
|
+
|
|
239
|
+
await driver.set('name', 'alice');
|
|
240
|
+
await driver.set('name', 'bob');
|
|
241
|
+
|
|
242
|
+
expect(await driver.get('name')).toBe('bob');
|
|
243
|
+
expect(await driver.count()).toBe(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('returns true on successful set', async () => {
|
|
247
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
248
|
+
|
|
249
|
+
expect(await driver.set('name', 'alice')).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('returns undefined for non-existent key', async () => {
|
|
253
|
+
let driver = new IndexedDBDriver<TestData>(uid(), 1);
|
|
254
|
+
|
|
255
|
+
expect(await driver.get('name')).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('sets and retrieves a number value', async () => {
|
|
259
|
+
let db = uid(),
|
|
260
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
261
|
+
|
|
262
|
+
await driver.set('age', 25);
|
|
263
|
+
|
|
264
|
+
expect(await driver.get('age')).toBe(25);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('sets and retrieves a string value', async () => {
|
|
268
|
+
let db = uid(),
|
|
269
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
270
|
+
|
|
271
|
+
await driver.set('name', 'alice');
|
|
272
|
+
|
|
273
|
+
expect(await driver.get('name')).toBe('alice');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('sets and retrieves an array value', async () => {
|
|
277
|
+
let db = uid(),
|
|
278
|
+
driver = new IndexedDBDriver<TestData>(db, 1);
|
|
279
|
+
|
|
280
|
+
await driver.set('tags', ['a', 'b', 'c']);
|
|
281
|
+
|
|
282
|
+
expect(await driver.get('tags')).toEqual(['a', 'b', 'c']);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('sets and retrieves an object value', async () => {
|
|
286
|
+
type ObjData = { meta: { nested: boolean; value: number } };
|
|
287
|
+
|
|
288
|
+
let db = uid(),
|
|
289
|
+
driver = new IndexedDBDriver<ObjData>(db, 1),
|
|
290
|
+
obj = { nested: true, value: 42 };
|
|
291
|
+
|
|
292
|
+
await driver.set('meta', obj);
|
|
293
|
+
|
|
294
|
+
expect(await driver.get('meta')).toEqual(obj);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|