@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/src/index.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  import { decrypt, encrypt } from '@esportsplus/utilities';
2
- import type { Driver, Filter, Options } from './types';
2
+ import type { Driver, Filter, GlobalCallback, KeyCallback, MigrationFn, Options, SetOptions, TTLEnvelope } from './types';
3
3
  import { DriverType } from './constants';
4
4
  import { IndexedDBDriver } from '~/drivers/indexeddb';
5
5
  import { LocalStorageDriver } from '~/drivers/localstorage';
6
+ import { MemoryDriver } from '~/drivers/memory';
7
+ import { SessionStorageDriver } from '~/drivers/sessionstorage';
6
8
 
7
9
 
10
+ const VERSION_KEY = '__version__';
11
+
12
+
13
+ function isEnvelope<V>(value: unknown): value is TTLEnvelope<V> {
14
+ return value !== null
15
+ && typeof value === 'object'
16
+ && '__e' in (value as Record<string, unknown>)
17
+ && '__v' in (value as Record<string, unknown>);
18
+ }
19
+
8
20
  async function deserialize<V>(value: unknown, secret: string | null): Promise<V | undefined> {
9
21
  if (value === undefined || value === null) {
10
22
  return undefined;
@@ -35,125 +47,418 @@ async function serialize<V>(value: V, secret: string | null): Promise<string | V
35
47
  return value;
36
48
  }
37
49
 
50
+ function notify<T>(
51
+ globals: Set<GlobalCallback<T>>,
52
+ listeners: Map<keyof T, Set<KeyCallback<T>>>,
53
+ key: keyof T,
54
+ newValue: T[keyof T] | undefined,
55
+ oldValue: T[keyof T] | undefined
56
+ ): void {
57
+ let set = listeners.get(key);
58
+
59
+ if (set) {
60
+ for (let cb of set) {
61
+ cb(newValue, oldValue);
62
+ }
63
+ }
64
+
65
+ for (let cb of globals) {
66
+ cb(key, newValue, oldValue);
67
+ }
68
+ }
69
+
70
+ function unwrap<V>(value: unknown): { expired: boolean; hasTTL: boolean; value: V } {
71
+ if (isEnvelope<V>(value)) {
72
+ return {
73
+ expired: Date.now() > value.__e,
74
+ hasTTL: true,
75
+ value: value.__v
76
+ };
77
+ }
78
+
79
+ return { expired: false, hasTTL: false, value: value as V };
80
+ }
81
+
82
+ async function migrate<T>(driver: Driver<T>, migrations: Record<number, MigrationFn>, version: number): Promise<void> {
83
+ let raw = await driver.get(VERSION_KEY as keyof T),
84
+ stored = typeof raw === 'number' ? raw : 0;
85
+
86
+ if (stored >= version) {
87
+ return;
88
+ }
89
+
90
+ let keys = Object.keys(migrations).map(Number).filter((v) => v > stored && v <= version).sort((a, b) => a - b);
91
+
92
+ for (let i = 0, n = keys.length; i < n; i++) {
93
+ let all = await driver.all(),
94
+ data: Record<string, unknown> = {};
95
+
96
+ for (let key in all) {
97
+ if (key !== VERSION_KEY) {
98
+ data[key] = all[key as keyof T];
99
+ }
100
+ }
101
+
102
+ let transformed = await migrations[keys[i]]({ all: () => Promise.resolve(data) });
103
+
104
+ await driver.clear();
105
+
106
+ let entries: [keyof T, T[keyof T]][] = [];
107
+
108
+ for (let key in transformed) {
109
+ entries.push([key as keyof T, transformed[key] as T[keyof T]]);
110
+ }
111
+
112
+ if (entries.length > 0) {
113
+ await driver.replace(entries);
114
+ }
115
+ }
116
+
117
+ await driver.set(VERSION_KEY as keyof T, version as T[keyof T]);
118
+ }
119
+
38
120
 
39
121
  class Local<T> {
40
122
 
41
123
  private driver: Driver<T>;
42
124
 
125
+ private globals: Set<GlobalCallback<T>>;
126
+
127
+ private listeners: Map<keyof T, Set<KeyCallback<T>>>;
128
+
129
+ private ready: Promise<void>;
130
+
43
131
  private secret: string | null;
44
132
 
133
+ private version: number;
134
+
45
135
 
46
136
  constructor(options: Options, secret?: string) {
137
+ this.globals = new Set();
138
+ this.listeners = new Map();
47
139
  this.secret = secret || null;
48
140
 
49
- let { name, version = 1 } = options;
141
+ let { migrations, name, version = 1 } = options;
142
+
143
+ this.version = version;
50
144
 
51
145
  if (options.driver === DriverType.LocalStorage) {
52
146
  this.driver = new LocalStorageDriver<T>(name, version);
53
147
  }
148
+ else if (options.driver === DriverType.Memory) {
149
+ this.driver = new MemoryDriver<T>(name, version);
150
+ }
151
+ else if (options.driver === DriverType.SessionStorage) {
152
+ this.driver = new SessionStorageDriver<T>(name, version);
153
+ }
54
154
  else {
55
155
  this.driver = new IndexedDBDriver<T>(name, version);
56
156
  }
157
+
158
+ if (migrations) {
159
+ this.ready = migrate(this.driver, migrations, version);
160
+ }
161
+ else {
162
+ this.ready = Promise.resolve();
163
+ }
57
164
  }
58
165
 
59
166
 
60
167
  async all(): Promise<T> {
61
- let raw = await this.driver.all(),
168
+ await this.ready;
169
+
170
+ let expired: (keyof T)[] = [],
171
+ raw = await this.driver.all(),
62
172
  result = {} as T;
63
173
 
64
174
  for (let key in raw) {
65
- let value = await deserialize<T[keyof T]>(raw[key], this.secret);
175
+ if (key === VERSION_KEY) {
176
+ continue;
177
+ }
178
+
179
+ let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw[key], this.secret),
180
+ unwrapped = unwrap<T[keyof T]>(deserialized);
66
181
 
67
- if (value !== undefined) {
68
- (result as Record<string, unknown>)[key] = value;
182
+ if (deserialized === undefined) {
183
+ continue;
69
184
  }
185
+
186
+ if (unwrapped.expired) {
187
+ expired.push(key as keyof T);
188
+ continue;
189
+ }
190
+
191
+ (result as Record<string, unknown>)[key] = unwrapped.value;
192
+ }
193
+
194
+ if (expired.length > 0) {
195
+ this.driver.delete(expired);
70
196
  }
71
197
 
72
198
  return result;
73
199
  }
74
200
 
75
201
  async clear(): Promise<void> {
76
- return this.driver.clear();
202
+ await this.ready;
203
+
204
+ let allData = await this.all(),
205
+ keys = Object.keys(allData as Record<string, unknown>) as (keyof T)[];
206
+
207
+ await this.driver.clear();
208
+ await this.driver.set(VERSION_KEY as keyof T, this.version as T[keyof T]);
209
+
210
+ for (let i = 0, n = keys.length; i < n; i++) {
211
+ notify(this.globals, this.listeners, keys[i], undefined, allData[keys[i]]);
212
+ }
213
+ }
214
+
215
+ async cleanup(): Promise<void> {
216
+ await this.ready;
217
+
218
+ let expired: (keyof T)[] = [],
219
+ oldValues = new Map<keyof T, T[keyof T]>();
220
+
221
+ await this.driver.map(async (raw, key) => {
222
+ if (key as string === VERSION_KEY) {
223
+ return;
224
+ }
225
+
226
+ let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret),
227
+ unwrapped = unwrap<T[keyof T]>(deserialized);
228
+
229
+ if (deserialized !== undefined && unwrapped.expired) {
230
+ expired.push(key);
231
+ oldValues.set(key, unwrapped.value);
232
+ }
233
+ });
234
+
235
+ if (expired.length > 0) {
236
+ await this.driver.delete(expired);
237
+
238
+ for (let i = 0, n = expired.length; i < n; i++) {
239
+ notify(this.globals, this.listeners, expired[i], undefined, oldValues.get(expired[i]));
240
+ }
241
+ }
77
242
  }
78
243
 
79
244
  async count(): Promise<number> {
80
- return this.driver.count();
245
+ await this.ready;
246
+
247
+ let total = await this.driver.count(),
248
+ raw = await this.driver.get(VERSION_KEY as keyof T);
249
+
250
+ return raw !== undefined ? total - 1 : total;
81
251
  }
82
252
 
83
253
  async delete(...keys: (keyof T)[]): Promise<void> {
84
- return this.driver.delete(keys);
254
+ await this.ready;
255
+
256
+ let oldValues = new Map<keyof T, T[keyof T] | undefined>();
257
+
258
+ for (let i = 0, n = keys.length; i < n; i++) {
259
+ let raw = await this.driver.get(keys[i]),
260
+ deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret),
261
+ unwrapped = unwrap<T[keyof T]>(deserialized);
262
+
263
+ oldValues.set(keys[i], deserialized === undefined ? undefined : unwrapped.value);
264
+ }
265
+
266
+ await this.driver.delete(keys);
267
+
268
+ for (let i = 0, n = keys.length; i < n; i++) {
269
+ notify(this.globals, this.listeners, keys[i], undefined, oldValues.get(keys[i]));
270
+ }
85
271
  }
86
272
 
87
273
  async filter(fn: Filter<T>): Promise<T> {
88
- let i = 0,
274
+ await this.ready;
275
+
276
+ let expired: (keyof T)[] = [],
277
+ i = 0,
89
278
  result = {} as T,
90
279
  stop = () => { stopped = true; },
91
280
  stopped = false;
92
281
 
93
282
  await this.driver.map(async (raw, key) => {
94
- if (stopped) {
283
+ if (stopped || key as string === VERSION_KEY) {
95
284
  return;
96
285
  }
97
286
 
98
- let value = await deserialize<T[keyof T]>(raw, this.secret);
287
+ let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret),
288
+ unwrapped = unwrap<T[keyof T]>(deserialized);
289
+
290
+ if (deserialized === undefined) {
291
+ return;
292
+ }
99
293
 
100
- if (value === undefined) {
294
+ if (unwrapped.expired) {
295
+ expired.push(key);
101
296
  return;
102
297
  }
103
298
 
104
- if (await fn({ i: i++, key, stop, value })) {
105
- result[key] = value;
299
+ if (await fn({ i: i++, key, stop, value: unwrapped.value })) {
300
+ result[key] = unwrapped.value;
106
301
  }
107
302
  });
108
303
 
304
+ if (expired.length > 0) {
305
+ this.driver.delete(expired);
306
+ }
307
+
109
308
  return result;
110
309
  }
111
310
 
112
- async get(key: keyof T): Promise<T[keyof T] | undefined> {
113
- return deserialize<T[keyof T]>(
114
- await this.driver.get(key),
115
- this.secret
116
- );
311
+ async get(key: keyof T): Promise<T[keyof T] | undefined>;
312
+ async get(key: keyof T, factory: () => T[keyof T] | Promise<T[keyof T]>): Promise<T[keyof T]>;
313
+ async get(key: keyof T, factory?: () => T[keyof T] | Promise<T[keyof T]>): Promise<T[keyof T] | undefined> {
314
+ await this.ready;
315
+
316
+ let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(
317
+ await this.driver.get(key),
318
+ this.secret
319
+ ),
320
+ missing = false,
321
+ unwrapped = unwrap<T[keyof T]>(deserialized);
322
+
323
+ if (deserialized === undefined) {
324
+ missing = true;
325
+ }
326
+ else if (unwrapped.expired) {
327
+ this.driver.delete([key]);
328
+ missing = true;
329
+ }
330
+
331
+ if (missing) {
332
+ if (factory) {
333
+ let value = await factory();
334
+
335
+ this.set(key, value);
336
+
337
+ return value;
338
+ }
339
+
340
+ return undefined;
341
+ }
342
+
343
+ return unwrapped.value;
117
344
  }
118
345
 
119
346
  async keys(): Promise<(keyof T)[]> {
120
- return this.driver.keys();
347
+ await this.ready;
348
+
349
+ let all = await this.driver.keys();
350
+
351
+ return all.filter((k) => k as string !== VERSION_KEY);
121
352
  }
122
353
 
123
- length(): Promise<number> {
124
- return this.driver.count();
354
+ async length(): Promise<number> {
355
+ await this.ready;
356
+
357
+ return this.count();
125
358
  }
126
359
 
127
- map(fn: (value: T[keyof T], key: keyof T, i: number) => void | Promise<void>): Promise<void> {
128
- return this.driver.map(async (raw, key, i) => {
129
- let value = await deserialize<T[keyof T]>(raw, this.secret);
360
+ async map(fn: (value: T[keyof T], key: keyof T, i: number) => void | Promise<void>): Promise<void> {
361
+ await this.ready;
362
+
363
+ let expired: (keyof T)[] = [],
364
+ j = 0;
365
+
366
+ await this.driver.map(async (raw, key) => {
367
+ if (key as string === VERSION_KEY) {
368
+ return;
369
+ }
370
+
371
+ let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret),
372
+ unwrapped = unwrap<T[keyof T]>(deserialized);
130
373
 
131
- if (value !== undefined) {
132
- await fn(value, key, i);
374
+ if (deserialized === undefined) {
375
+ return;
133
376
  }
377
+
378
+ if (unwrapped.expired) {
379
+ expired.push(key);
380
+ return;
381
+ }
382
+
383
+ await fn(unwrapped.value, key, j++);
134
384
  });
385
+
386
+ if (expired.length > 0) {
387
+ this.driver.delete(expired);
388
+ }
135
389
  }
136
390
 
137
391
  async only(...keys: (keyof T)[]): Promise<T> {
138
- let raw = await this.driver.only(keys),
392
+ await this.ready;
393
+
394
+ let expired: (keyof T)[] = [],
395
+ raw = await this.driver.only(keys),
139
396
  result = {} as T;
140
397
 
141
398
  for (let [key, value] of raw) {
142
- let deserialized = await deserialize<T[keyof T]>(value, this.secret);
399
+ let deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(value, this.secret),
400
+ unwrapped = unwrap<T[keyof T]>(deserialized);
143
401
 
144
- if (deserialized !== undefined) {
145
- result[key] = deserialized;
402
+ if (deserialized === undefined) {
403
+ continue;
146
404
  }
405
+
406
+ if (unwrapped.expired) {
407
+ expired.push(key);
408
+ continue;
409
+ }
410
+
411
+ result[key] = unwrapped.value;
412
+ }
413
+
414
+ if (expired.length > 0) {
415
+ this.driver.delete(expired);
147
416
  }
148
417
 
149
418
  return result;
150
419
  }
151
420
 
421
+ async persist(key: keyof T): Promise<boolean> {
422
+ await this.ready;
423
+
424
+ let raw = await this.driver.get(key),
425
+ deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret);
426
+
427
+ if (deserialized === undefined) {
428
+ return false;
429
+ }
430
+
431
+ let unwrapped = unwrap<T[keyof T]>(deserialized);
432
+
433
+ if (unwrapped.expired) {
434
+ this.driver.delete([key]);
435
+ return false;
436
+ }
437
+
438
+ if (!unwrapped.hasTTL) {
439
+ return true;
440
+ }
441
+
442
+ return this.driver.set(
443
+ key,
444
+ await serialize(unwrapped.value, this.secret) as T[keyof T]
445
+ );
446
+ }
447
+
152
448
  async replace(values: Partial<T>): Promise<string[]> {
449
+ await this.ready;
450
+
153
451
  let entries: [keyof T, unknown][] = [],
154
- failed: string[] = [];
452
+ failed: string[] = [],
453
+ oldValues = new Map<keyof T, T[keyof T] | undefined>();
155
454
 
156
455
  for (let key in values) {
456
+ let raw = await this.driver.get(key),
457
+ deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret),
458
+ unwrapped = unwrap<T[keyof T]>(deserialized);
459
+
460
+ oldValues.set(key, deserialized === undefined ? undefined : unwrapped.value);
461
+
157
462
  try {
158
463
  entries.push([
159
464
  key,
@@ -169,20 +474,96 @@ class Local<T> {
169
474
  await this.driver.replace(entries as [keyof T, T[keyof T]][]);
170
475
  }
171
476
 
477
+ for (let i = 0, n = entries.length; i < n; i++) {
478
+ let key = entries[i][0];
479
+
480
+ notify(this.globals, this.listeners, key, values[key] as T[keyof T], oldValues.get(key));
481
+ }
482
+
172
483
  return failed;
173
484
  }
174
485
 
175
- async set(key: keyof T, value: T[keyof T]): Promise<boolean> {
486
+ async set(key: keyof T, value: T[keyof T], options?: SetOptions): Promise<boolean> {
487
+ await this.ready;
488
+
176
489
  try {
177
- return this.driver.set(
178
- key,
179
- await serialize(value, this.secret) as T[keyof T]
180
- );
490
+ let oldRaw = await this.driver.get(key),
491
+ oldDeserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(oldRaw, this.secret),
492
+ oldUnwrapped = unwrap<T[keyof T]>(oldDeserialized),
493
+ oldValue = oldDeserialized === undefined ? undefined : oldUnwrapped.value,
494
+ stored: T[keyof T] | string;
495
+
496
+ if (options?.ttl != null && options.ttl > 0) {
497
+ let envelope: TTLEnvelope<T[keyof T]> = {
498
+ __e: Date.now() + options.ttl,
499
+ __v: value
500
+ };
501
+
502
+ stored = await serialize(envelope, this.secret) as T[keyof T];
503
+ }
504
+ else {
505
+ stored = await serialize(value, this.secret) as T[keyof T];
506
+ }
507
+
508
+ let result = await this.driver.set(key, stored as T[keyof T]);
509
+
510
+ notify(this.globals, this.listeners, key, value, oldValue);
511
+
512
+ return result;
181
513
  }
182
514
  catch {
183
515
  return false;
184
516
  }
185
517
  }
518
+
519
+ subscribe(callback: GlobalCallback<T>): () => void;
520
+ subscribe<K extends keyof T>(key: K, callback: KeyCallback<T, K>): () => void;
521
+ subscribe<K extends keyof T>(keyOrCallback: K | GlobalCallback<T>, callback?: KeyCallback<T, K>): () => void {
522
+ if (typeof keyOrCallback === 'function') {
523
+ let cb = keyOrCallback as GlobalCallback<T>;
524
+
525
+ this.globals.add(cb);
526
+
527
+ return () => { this.globals.delete(cb); };
528
+ }
529
+
530
+ let cb = callback as KeyCallback<T, K>,
531
+ key = keyOrCallback as K,
532
+ set = this.listeners.get(key);
533
+
534
+ if (!set) {
535
+ set = new Set();
536
+ this.listeners.set(key, set);
537
+ }
538
+
539
+ set.add(cb as KeyCallback<T>);
540
+
541
+ return () => { set.delete(cb as KeyCallback<T>); };
542
+ }
543
+
544
+ async ttl(key: keyof T): Promise<number> {
545
+ await this.ready;
546
+
547
+ let raw = await this.driver.get(key),
548
+ deserialized = await deserialize<T[keyof T] | TTLEnvelope<T[keyof T]>>(raw, this.secret);
549
+
550
+ if (deserialized === undefined) {
551
+ return -1;
552
+ }
553
+
554
+ if (!isEnvelope(deserialized)) {
555
+ return -1;
556
+ }
557
+
558
+ let remaining = deserialized.__e - Date.now();
559
+
560
+ if (remaining <= 0) {
561
+ this.driver.delete([key]);
562
+ return -1;
563
+ }
564
+
565
+ return remaining;
566
+ }
186
567
  }
187
568
 
188
569
 
@@ -190,4 +571,4 @@ export default <T>(options: Options, secret?: string) => {
190
571
  return new Local<T>(options, secret);
191
572
  };
192
573
  export { DriverType } from './constants';
193
- export type { Local };
574
+ export type { Local };
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 };