@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 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
@@ -5,6 +5,7 @@ declare class LocalStorageDriver<T> implements Driver<T> {
5
5
  private getKeys;
6
6
  private key;
7
7
  private parse;
8
+ private serialize;
8
9
  all(): Promise<T>;
9
10
  clear(): Promise<void>;
10
11
  count(): Promise<number>;
@@ -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++) {
@@ -44,7 +52,13 @@ class LocalStorageDriver {
44
52
  }
45
53
  }
46
54
  async count() {
47
- return this.getKeys().length;
55
+ let count = 0;
56
+ for (let i = 0, n = localStorage.length; i < n; i++) {
57
+ if (localStorage.key(i)?.startsWith(this.prefix)) {
58
+ count++;
59
+ }
60
+ }
61
+ return count;
48
62
  }
49
63
  async delete(keys) {
50
64
  for (let i = 0, n = keys.length; i < n; i++) {
@@ -78,12 +92,12 @@ class LocalStorageDriver {
78
92
  }
79
93
  async replace(entries) {
80
94
  for (let i = 0, n = entries.length; i < n; i++) {
81
- localStorage.setItem(this.key(entries[i][0]), JSON.stringify(entries[i][1]));
95
+ localStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
82
96
  }
83
97
  }
84
98
  async set(key, value) {
85
99
  try {
86
- localStorage.setItem(this.key(key), JSON.stringify(value));
100
+ localStorage.setItem(this.key(key), this.serialize(value));
87
101
  return true;
88
102
  }
89
103
  catch {
@@ -5,6 +5,7 @@ declare class SessionStorageDriver<T> implements Driver<T> {
5
5
  private getKeys;
6
6
  private key;
7
7
  private parse;
8
+ private serialize;
8
9
  all(): Promise<T>;
9
10
  clear(): Promise<void>;
10
11
  count(): Promise<number>;
@@ -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++) {
@@ -44,7 +52,13 @@ class SessionStorageDriver {
44
52
  }
45
53
  }
46
54
  async count() {
47
- return this.getKeys().length;
55
+ let count = 0;
56
+ for (let i = 0, n = sessionStorage.length; i < n; i++) {
57
+ if (sessionStorage.key(i)?.startsWith(this.prefix)) {
58
+ count++;
59
+ }
60
+ }
61
+ return count;
48
62
  }
49
63
  async delete(keys) {
50
64
  for (let i = 0, n = keys.length; i < n; i++) {
@@ -78,12 +92,12 @@ class SessionStorageDriver {
78
92
  }
79
93
  async replace(entries) {
80
94
  for (let i = 0, n = entries.length; i < n; i++) {
81
- sessionStorage.setItem(this.key(entries[i][0]), JSON.stringify(entries[i][1]));
95
+ sessionStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
82
96
  }
83
97
  }
84
98
  async set(key, value) {
85
99
  try {
86
- sessionStorage.setItem(this.key(key), JSON.stringify(value));
100
+ sessionStorage.setItem(this.key(key), this.serialize(value));
87
101
  return true;
88
102
  }
89
103
  catch {
package/build/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { Filter, GlobalCallback, KeyCallback, Options, SetOptions } from './types.js';
2
2
  declare class Local<T> {
3
+ private cipher;
3
4
  private driver;
4
5
  private globals;
5
6
  private listeners;
6
7
  private ready;
7
- private secret;
8
8
  private version;
9
9
  constructor(options: Options, secret?: string);
10
10
  all(): Promise<T>;