@esportsplus/web-storage 0.3.3 → 0.4.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/.github/workflows/bump.yml +2 -2
- package/.github/workflows/dependabot.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/build/constants.d.ts +3 -1
- package/build/constants.js +2 -0
- package/build/drivers/memory.d.ts +16 -0
- package/build/drivers/memory.js +64 -0
- package/build/drivers/sessionstorage.d.ts +19 -0
- package/build/drivers/sessionstorage.js +94 -0
- package/build/index.d.ts +12 -2
- package/build/index.js +271 -31
- package/build/types.d.ts +16 -2
- package/package.json +6 -2
- package/src/constants.ts +3 -1
- package/src/drivers/memory.ts +92 -0
- package/src/drivers/sessionstorage.ts +131 -0
- package/src/index.ts +420 -39
- package/src/types.ts +23 -2
- package/storage/feature-research.md +173 -0
- package/tests/drivers/indexeddb.ts +297 -0
- package/tests/drivers/localstorage.ts +290 -0
- package/tests/drivers/memory.ts +257 -0
- package/tests/drivers/sessionstorage.ts +290 -0
- package/tests/index.ts +1462 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Feature Research: @esportsplus/web-storage
|
|
2
|
+
|
|
3
|
+
Research date: 2026-03-24
|
|
4
|
+
Sources: localForage, Dexie.js, idb-keyval, store2, RxDB, unstorage, lz-string, lscache, expired-storage, proxy-storage, Valtio, Podda
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## What We Already Have
|
|
8
|
+
|
|
9
|
+
- Two drivers: IndexedDB (default), localStorage
|
|
10
|
+
- Unified async API: get, set, delete, all, clear, count, keys, map, filter, only, replace
|
|
11
|
+
- AES-GCM encryption with optional secret
|
|
12
|
+
- TypeScript generics (`Local<T>` with per-key type inference)
|
|
13
|
+
- Namespace isolation via `name:version:` prefix (LS) / separate databases (IDB)
|
|
14
|
+
- Connection pooling for IndexedDB
|
|
15
|
+
- Factory pattern for independent instances
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Recommended Features (Priority Order)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### 1. TTL / Expiration (HIGH)
|
|
22
|
+
|
|
23
|
+
The most universally requested missing feature across storage libraries.
|
|
24
|
+
|
|
25
|
+
**What**: Per-key time-to-live. `set(key, value, { ttl: 60000 })` stores an expiry timestamp alongside the value. On `get`, expired entries return `undefined` and are lazily deleted.
|
|
26
|
+
|
|
27
|
+
**Why**: Without TTL, consumers reimplement expiry logic on every project. Cache use cases are impossible without it. lscache, expired-storage, and dozens of wrappers exist solely for this.
|
|
28
|
+
|
|
29
|
+
**Approach**: Envelope wrapping — store `{ value, expiry }` instead of raw value. Lazy deletion on read (check expiry on `get`). Optional proactive sweep via `cleanup()` method.
|
|
30
|
+
|
|
31
|
+
**API surface**:
|
|
32
|
+
```typescript
|
|
33
|
+
await store.set('session', token, { ttl: 3600000 }); // 1 hour
|
|
34
|
+
await store.get('session'); // undefined after expiry
|
|
35
|
+
await store.ttl('session'); // remaining ms, or -1 if no TTL
|
|
36
|
+
await store.persist('session'); // remove TTL, make permanent
|
|
37
|
+
await store.cleanup(); // proactively sweep all expired entries
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Implemented by**: lscache, expired-storage, ttl-db, localstorage-slim, Redis
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### 2. Memory Driver (HIGH)
|
|
44
|
+
|
|
45
|
+
**What**: In-memory `Driver<T>` backed by a `Map`. Non-persistent — data lost on page reload.
|
|
46
|
+
|
|
47
|
+
**Why**: Essential for unit testing (no browser APIs needed), SSR environments (no `localStorage`/`indexedDB`), and as a fallback when persistent storage is unavailable (private browsing, quota exceeded). Every major library offers this.
|
|
48
|
+
|
|
49
|
+
**Approach**: Implement `Driver<T>` interface with a `Map<keyof T, T[keyof T]>`. Trivial — the interface already exists.
|
|
50
|
+
|
|
51
|
+
**API surface**:
|
|
52
|
+
```typescript
|
|
53
|
+
import storage, { DriverType } from '@esportsplus/web-storage';
|
|
54
|
+
|
|
55
|
+
let store = storage({ driver: DriverType.Memory, name: 'test', version: 1 });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Implemented by**: localForage (plugin), unstorage (default), proxy-storage
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### 4. `get(key, factory)` — Lazy Init (HIGH)
|
|
62
|
+
|
|
63
|
+
**What**: Optional second parameter on `get`. If the key is missing and a factory is provided, call the factory to produce the value, fire-and-forget the `set` (do not await it), and return the value immediately.
|
|
64
|
+
|
|
65
|
+
**Why**: Extremely common pattern that every consumer reimplements. Especially useful for expensive computations or API calls that should be cached. The fire-and-forget write means the caller is never blocked by storage I/O — the value is returned as soon as the factory resolves.
|
|
66
|
+
|
|
67
|
+
**Behavior**:
|
|
68
|
+
1. `get(key)` — existing behavior, returns `T[keyof T] | undefined`
|
|
69
|
+
2. `get(key, factory)` — if value exists, return it. If missing, call `factory()`, persist the result via `set` without awaiting, return the factory value. Factory can be sync or async.
|
|
70
|
+
|
|
71
|
+
**API surface**:
|
|
72
|
+
```typescript
|
|
73
|
+
// Without factory — unchanged, returns T[keyof T] | undefined
|
|
74
|
+
let user = await store.get('user');
|
|
75
|
+
|
|
76
|
+
// With factory — returns T[keyof T] (never undefined)
|
|
77
|
+
let user = await store.get('user', async () => {
|
|
78
|
+
return await fetchUser(id);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Sync factory works too
|
|
82
|
+
let count = await store.get('count', () => 0);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Implementation note**: The `set` call is intentionally not awaited. The returned value comes from the factory, not from a subsequent read. This means the caller gets the value immediately while the write happens in the background. If the write fails silently, the next `get` with a factory will simply re-invoke it.
|
|
86
|
+
|
|
87
|
+
**Implemented by**: Common pattern, no library does it particularly well
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
### 5. Compression (HIGH for localStorage)
|
|
91
|
+
|
|
92
|
+
**What**: Always-on LZ-based compression for the localStorage driver. Multiplies effective capacity by 2-10x.
|
|
93
|
+
|
|
94
|
+
**Why**: localStorage has a hard 5MB limit. Users consistently hit this wall. IndexedDB has no meaningful quota pressure (60% of disk), so compression is localStorage-only.
|
|
95
|
+
|
|
96
|
+
**Approach**: Built into the localStorage driver's serialize/deserialize pipeline. No configuration — always active, but with a 100-byte threshold: values under 100 bytes are stored as-is (LZ framing overhead would make them larger), values at or above 100 bytes are compressed via `lz-string` `compressToUTF16`. On read, detect whether the stored value is compressed and decompress accordingly. Compression runs before encryption when a secret is present (compress → encrypt on write, decrypt → decompress on read).
|
|
97
|
+
|
|
98
|
+
**No API surface** — this is an internal optimization. No user-facing option or configuration. The localStorage driver always compresses large values transparently.
|
|
99
|
+
|
|
100
|
+
**Implemented by**: lz-string, localstorage-slim, locally (locallyjs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
### 6. Change Subscriptions (MEDIUM-HIGH)
|
|
104
|
+
|
|
105
|
+
**What**: `subscribe(key, callback)` fires when a value changes. Returns an unsubscribe function.
|
|
106
|
+
|
|
107
|
+
**Why**: Essential for UI framework integration. Dexie's `liveQuery` and RxDB's reactive queries are headline features. A minimal event emitter per key fills the gap between the storage library and the UI layer.
|
|
108
|
+
|
|
109
|
+
**Approach**: Internal `Map<keyof T, Set<Callback>>` in `Local<T>`. Fire callbacks after `set`, `delete`, `replace`, `clear` complete. No cross-tab sync (keep it simple).
|
|
110
|
+
|
|
111
|
+
**API surface**:
|
|
112
|
+
```typescript
|
|
113
|
+
let unsubscribe = store.subscribe('theme', (newValue, oldValue) => {
|
|
114
|
+
applyTheme(newValue);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// or subscribe to all changes
|
|
118
|
+
let unsubscribe = store.subscribe((key, newValue, oldValue) => {
|
|
119
|
+
console.log(`${key} changed`);
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Implemented by**: Podda, Valtio, unstorage, local-storage-proxy
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
### 7. Migration Callbacks (MEDIUM-HIGH)
|
|
127
|
+
|
|
128
|
+
**What**: Run transform functions when the version number changes. The `version` parameter already exists but has no migration path.
|
|
129
|
+
|
|
130
|
+
**Why**: Critical for production apps that evolve their schema. Without migrations, a version bump silently loses or corrupts data. Dexie and RxDB consider this a core feature.
|
|
131
|
+
|
|
132
|
+
**Approach**: Accept a `migrations` map in options. On construction, detect version change and run the appropriate migration function before the store becomes usable.
|
|
133
|
+
|
|
134
|
+
**API surface**:
|
|
135
|
+
```typescript
|
|
136
|
+
let store = storage<AppDataV2>({
|
|
137
|
+
name: 'app',
|
|
138
|
+
version: 2,
|
|
139
|
+
migrations: {
|
|
140
|
+
2: async (old) => {
|
|
141
|
+
// Transform v1 data to v2 shape
|
|
142
|
+
let all = await old.all();
|
|
143
|
+
return { ...all, newField: defaultValue };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Implemented by**: Dexie (`upgrade()`), RxDB (`migrationStrategies`)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### 9. sessionStorage Driver (MEDIUM)
|
|
153
|
+
|
|
154
|
+
**What**: Per-tab ephemeral storage via `sessionStorage`.
|
|
155
|
+
|
|
156
|
+
**Why**: Fills a real gap for per-tab state (form drafts, wizard progress, auth tokens). Trivial to implement — copy `LocalStorageDriver` and swap `localStorage` for `sessionStorage`.
|
|
157
|
+
|
|
158
|
+
**API surface**:
|
|
159
|
+
```typescript
|
|
160
|
+
let store = storage({ driver: DriverType.SessionStorage, name: 'tab', version: 1 });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Implemented by**: store2 (`store.session`)
|
|
164
|
+
|
|
165
|
+
### 11. OPFS Driver (LOW — emerging)
|
|
166
|
+
|
|
167
|
+
**What**: Origin Private File System — 3-4x faster than IndexedDB for large binary data. Accessed via Web Worker using `FileSystemSyncAccessHandle`.
|
|
168
|
+
|
|
169
|
+
**Why**: Emerging as the fastest browser storage for performance-sensitive apps. RxDB reports significant speed gains. However, browser support is still maturing and the API is complex.
|
|
170
|
+
|
|
171
|
+
**Approach**: Defer until OPFS stabilizes further. Monitor adoption.
|
|
172
|
+
|
|
173
|
+
**Implemented by**: RxDB, opfs-tools
|
|
@@ -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
|
+
});
|