@choksheak/ts-utils 0.1.9 → 0.2.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/dateTimeStr.d.ts +74 -0
- package/dateTimeStr.js +29 -4
- package/dateTimeStr.js.map +1 -1
- package/duration.d.ts +18 -6
- package/duration.js +5 -0
- package/duration.js.map +1 -1
- package/kvStore.d.ts +33 -2
- package/kvStore.js +80 -12
- package/kvStore.js.map +1 -1
- package/localStorageCache.d.ts +19 -2
- package/localStorageCache.js +29 -16
- package/localStorageCache.js.map +1 -1
- package/nonEmpty.d.ts +3 -0
- package/nonEmpty.js.map +1 -1
- package/nonNil.d.ts +3 -0
- package/nonNil.js.map +1 -1
- package/package.json +1 -1
- package/safeParseInt.d.ts +3 -0
- package/safeParseInt.js.map +1 -1
- package/sleep.d.ts +4 -0
- package/sleep.js.map +1 -1
- package/src/dateTimeStr.ts +99 -16
- package/src/duration.ts +21 -6
- package/src/kvStore.ts +77 -15
- package/src/localStorageCache.ts +47 -26
- package/src/nonEmpty.ts +3 -0
- package/src/nonNil.ts +3 -0
- package/src/safeParseInt.ts +3 -0
- package/src/sleep.ts +4 -0
package/src/kvStore.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* Just use the `kvStore` global constant like the local storage.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { Duration, durationOrMsToMs } from "./duration";
|
|
15
|
+
|
|
14
16
|
// Updating the DB name will cause all old entries to be gone.
|
|
15
17
|
const DEFAULT_DB_NAME = "KVStore";
|
|
16
18
|
|
|
@@ -32,7 +34,8 @@ export const GC_INTERVAL_MS = MILLIS_PER_DAY;
|
|
|
32
34
|
type StoredObject<T> = {
|
|
33
35
|
key: string;
|
|
34
36
|
value: T;
|
|
35
|
-
|
|
37
|
+
storedMs: number;
|
|
38
|
+
expiryMs: number;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -48,10 +51,12 @@ function validateStoredObject<T>(
|
|
|
48
51
|
!("key" in obj) ||
|
|
49
52
|
typeof obj.key !== "string" ||
|
|
50
53
|
!("value" in obj) ||
|
|
54
|
+
!("storedMs" in obj) ||
|
|
55
|
+
typeof obj.storedMs !== "number" ||
|
|
51
56
|
obj.value === undefined ||
|
|
52
|
-
!("
|
|
53
|
-
typeof obj.
|
|
54
|
-
Date.now() >= obj.
|
|
57
|
+
!("expiryMs" in obj) ||
|
|
58
|
+
typeof obj.expiryMs !== "number" ||
|
|
59
|
+
Date.now() >= obj.expiryMs
|
|
55
60
|
) {
|
|
56
61
|
return undefined;
|
|
57
62
|
}
|
|
@@ -168,12 +173,14 @@ export class KVStore {
|
|
|
168
173
|
public async set<T>(
|
|
169
174
|
key: string,
|
|
170
175
|
value: T,
|
|
171
|
-
|
|
176
|
+
expiryDeltaMs: number | Duration = this.defaultExpiryDeltaMs,
|
|
172
177
|
): Promise<T> {
|
|
173
|
-
const
|
|
178
|
+
const nowMs = Date.now();
|
|
179
|
+
const obj: StoredObject<T> = {
|
|
174
180
|
key,
|
|
175
181
|
value,
|
|
176
|
-
|
|
182
|
+
storedMs: nowMs,
|
|
183
|
+
expiryMs: nowMs + durationOrMsToMs(expiryDeltaMs),
|
|
177
184
|
};
|
|
178
185
|
|
|
179
186
|
return await this.transact<T>(
|
|
@@ -210,7 +217,9 @@ export class KVStore {
|
|
|
210
217
|
);
|
|
211
218
|
}
|
|
212
219
|
|
|
213
|
-
public async
|
|
220
|
+
public async getStoredObject<T>(
|
|
221
|
+
key: string,
|
|
222
|
+
): Promise<StoredObject<T> | undefined> {
|
|
214
223
|
const stored = await this.transact<StoredObject<T> | undefined>(
|
|
215
224
|
"readonly",
|
|
216
225
|
(objectStore, resolve, reject) => {
|
|
@@ -236,7 +245,7 @@ export class KVStore {
|
|
|
236
245
|
return undefined;
|
|
237
246
|
}
|
|
238
247
|
|
|
239
|
-
return obj
|
|
248
|
+
return obj;
|
|
240
249
|
} catch (e) {
|
|
241
250
|
console.error(`Invalid kv value: ${key}=${JSON.stringify(stored)}:`, e);
|
|
242
251
|
await this.delete(key);
|
|
@@ -247,11 +256,17 @@ export class KVStore {
|
|
|
247
256
|
}
|
|
248
257
|
}
|
|
249
258
|
|
|
259
|
+
public async get<T>(key: string): Promise<T | undefined> {
|
|
260
|
+
const obj = await this.getStoredObject<T>(key);
|
|
261
|
+
|
|
262
|
+
return obj?.value;
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
public async forEach(
|
|
251
266
|
callback: (
|
|
252
267
|
key: string,
|
|
253
268
|
value: unknown,
|
|
254
|
-
|
|
269
|
+
expiryMs: number,
|
|
255
270
|
) => void | Promise<void>,
|
|
256
271
|
): Promise<void> {
|
|
257
272
|
await this.transact<void>("readonly", (objectStore, resolve, reject) => {
|
|
@@ -266,7 +281,7 @@ export class KVStore {
|
|
|
266
281
|
if (cursor.key) {
|
|
267
282
|
const obj = validateStoredObject(cursor.value);
|
|
268
283
|
if (obj) {
|
|
269
|
-
await callback(String(cursor.key), obj.value, obj.
|
|
284
|
+
await callback(String(cursor.key), obj.value, obj.expiryMs);
|
|
270
285
|
} else {
|
|
271
286
|
await callback(String(cursor.key), undefined, 0);
|
|
272
287
|
}
|
|
@@ -300,8 +315,8 @@ export class KVStore {
|
|
|
300
315
|
/** Mainly for debugging dumps. */
|
|
301
316
|
public async asMap(): Promise<Map<string, unknown>> {
|
|
302
317
|
const map = new Map<string, unknown>();
|
|
303
|
-
await this.forEach((key, value,
|
|
304
|
-
map.set(key, { value,
|
|
318
|
+
await this.forEach((key, value, expiryMs) => {
|
|
319
|
+
map.set(key, { value, expiryMs });
|
|
305
320
|
});
|
|
306
321
|
return map;
|
|
307
322
|
}
|
|
@@ -345,8 +360,8 @@ export class KVStore {
|
|
|
345
360
|
|
|
346
361
|
const keysToDelete: string[] = [];
|
|
347
362
|
await this.forEach(
|
|
348
|
-
async (key: string, value: unknown,
|
|
349
|
-
if (value === undefined || Date.now() >=
|
|
363
|
+
async (key: string, value: unknown, expiryMs: number) => {
|
|
364
|
+
if (value === undefined || Date.now() >= expiryMs) {
|
|
350
365
|
keysToDelete.push(key);
|
|
351
366
|
}
|
|
352
367
|
},
|
|
@@ -380,3 +395,50 @@ export const kvStore = new KVStore(
|
|
|
380
395
|
DEFAULT_DB_VERSION,
|
|
381
396
|
DEFAULT_EXPIRY_DELTA_MS,
|
|
382
397
|
);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Class to represent one key in the store with a default expiration.
|
|
401
|
+
*/
|
|
402
|
+
class KvStoreItem<T> {
|
|
403
|
+
public constructor(
|
|
404
|
+
public readonly key: string,
|
|
405
|
+
public readonly defaultExpiryDeltaMs: number,
|
|
406
|
+
public readonly store = kvStore,
|
|
407
|
+
) {}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Example usage:
|
|
411
|
+
*
|
|
412
|
+
* const { value, storedMs, expiryMs } = await myKvItem.getStoredObject();
|
|
413
|
+
*/
|
|
414
|
+
public async getStoredObject(): Promise<StoredObject<T> | undefined> {
|
|
415
|
+
return await this.store.getStoredObject(this.key);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
public async get(): Promise<T | undefined> {
|
|
419
|
+
return await this.store.get(this.key);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public async set(
|
|
423
|
+
value: T,
|
|
424
|
+
expiryDeltaMs: number = this.defaultExpiryDeltaMs,
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
await this.store.set(this.key, value, expiryDeltaMs);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
public async delete(): Promise<void> {
|
|
430
|
+
await this.store.delete(this.key);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Create a KV store item with a key and a default expiration.
|
|
436
|
+
*/
|
|
437
|
+
export function kvStoreItem<T>(
|
|
438
|
+
key: string,
|
|
439
|
+
defaultExpiration: number | Duration,
|
|
440
|
+
): KvStoreItem<T> {
|
|
441
|
+
const defaultExpiryDeltaMs = durationOrMsToMs(defaultExpiration);
|
|
442
|
+
|
|
443
|
+
return new KvStoreItem<T>(key, defaultExpiryDeltaMs);
|
|
444
|
+
}
|
package/src/localStorageCache.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Duration,
|
|
1
|
+
import { Duration, durationOrMsToMs } from "./duration";
|
|
2
|
+
|
|
3
|
+
export type StoredItem<T> = { value: T; storedMs: number; expiryMs: number };
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Simple local storage cache with support for auto-expiration.
|
|
@@ -12,7 +14,12 @@ import { Duration, durationToMs } from "./duration";
|
|
|
12
14
|
* In order to provide proper type-checking, please always specify the T
|
|
13
15
|
* type parameter. E.g. const item = storeItem<string>("name", 10_000);
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* @param key The store key in local storage.
|
|
18
|
+
* @param expires Either a number in milliseconds, or a Duration object
|
|
19
|
+
* @param logError Log an error if we found an invalid object in the store.
|
|
20
|
+
* The invalid object is usually a string that cannot be parsed as JSON.
|
|
21
|
+
* @param defaultValue Specify a default value to use for the object. Defaults
|
|
22
|
+
* to undefined.
|
|
16
23
|
*/
|
|
17
24
|
export function storeItem<T>(
|
|
18
25
|
key: string,
|
|
@@ -20,8 +27,7 @@ export function storeItem<T>(
|
|
|
20
27
|
logError = true,
|
|
21
28
|
defaultValue?: T,
|
|
22
29
|
) {
|
|
23
|
-
const expireDeltaMs =
|
|
24
|
-
typeof expires === "number" ? expires : durationToMs(expires);
|
|
30
|
+
const expireDeltaMs = durationOrMsToMs(expires);
|
|
25
31
|
|
|
26
32
|
return new CacheItem<T>(key, expireDeltaMs, logError, defaultValue);
|
|
27
33
|
}
|
|
@@ -34,51 +40,57 @@ class CacheItem<T> {
|
|
|
34
40
|
public readonly key: string,
|
|
35
41
|
public readonly expireDeltaMs: number,
|
|
36
42
|
public readonly logError: boolean,
|
|
37
|
-
defaultValue: T | undefined,
|
|
38
|
-
) {
|
|
39
|
-
if (defaultValue !== undefined) {
|
|
40
|
-
if (this.get() === undefined) {
|
|
41
|
-
this.set(defaultValue);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
43
|
+
public readonly defaultValue: T | undefined,
|
|
44
|
+
) {}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Set the value of this item with auto-expiration.
|
|
48
48
|
*/
|
|
49
|
-
public set(
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
public set(
|
|
50
|
+
value: T,
|
|
51
|
+
expiryDelta: number | Duration = this.expireDeltaMs,
|
|
52
|
+
): void {
|
|
53
|
+
const nowMs = Date.now();
|
|
54
|
+
const toStore: StoredItem<T> = {
|
|
55
|
+
value,
|
|
56
|
+
storedMs: nowMs,
|
|
57
|
+
expiryMs: nowMs + durationOrMsToMs(expiryDelta),
|
|
58
|
+
};
|
|
59
|
+
const valueStr = JSON.stringify(toStore);
|
|
52
60
|
|
|
53
61
|
globalThis.localStorage.setItem(this.key, valueStr);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
57
|
-
*
|
|
65
|
+
* Example usage:
|
|
66
|
+
*
|
|
67
|
+
* const { value, storedMs, expiryMs } = await myItem.getStoredItem();
|
|
58
68
|
*/
|
|
59
|
-
public
|
|
69
|
+
public getStoredItem(): StoredItem<T> | undefined {
|
|
60
70
|
const jsonStr = globalThis.localStorage.getItem(this.key);
|
|
61
71
|
|
|
62
|
-
if (!jsonStr
|
|
72
|
+
if (!jsonStr) {
|
|
63
73
|
return undefined;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
try {
|
|
67
|
-
const obj:
|
|
68
|
-
|
|
77
|
+
const obj: StoredItem<T> | undefined = JSON.parse(jsonStr);
|
|
78
|
+
|
|
69
79
|
if (
|
|
70
80
|
!obj ||
|
|
71
81
|
typeof obj !== "object" ||
|
|
72
82
|
!("value" in obj) ||
|
|
73
|
-
!("
|
|
74
|
-
typeof obj.
|
|
75
|
-
|
|
83
|
+
!("storedMs" in obj) ||
|
|
84
|
+
typeof obj.storedMs !== "number" ||
|
|
85
|
+
!("expiryMs" in obj) ||
|
|
86
|
+
typeof obj.expiryMs !== "number" ||
|
|
87
|
+
Date.now() >= obj.expiryMs
|
|
76
88
|
) {
|
|
77
|
-
|
|
89
|
+
this.remove();
|
|
78
90
|
return undefined;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
return obj
|
|
93
|
+
return obj;
|
|
82
94
|
} catch (e) {
|
|
83
95
|
if (this.logError) {
|
|
84
96
|
console.error(
|
|
@@ -86,11 +98,20 @@ class CacheItem<T> {
|
|
|
86
98
|
e,
|
|
87
99
|
);
|
|
88
100
|
}
|
|
89
|
-
|
|
101
|
+
this.remove();
|
|
90
102
|
return undefined;
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Get the value of this item, or undefined if value is not set or expired.
|
|
108
|
+
*/
|
|
109
|
+
public get(): T | undefined {
|
|
110
|
+
const stored = this.getStoredItem();
|
|
111
|
+
|
|
112
|
+
return stored !== undefined ? stored.value : this.defaultValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
/**
|
|
95
116
|
* Remove the value of this item.
|
|
96
117
|
*/
|
package/src/nonEmpty.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { isEmpty } from "./isEmpty";
|
|
|
3
3
|
/**
|
|
4
4
|
* Type asserts that `t` is truthy.
|
|
5
5
|
* Throws an error if `t` is null or undefined.
|
|
6
|
+
*
|
|
7
|
+
* @param varName The variable name to include in the error to throw when t is
|
|
8
|
+
* empty. Defaults to 'value'.
|
|
6
9
|
*/
|
|
7
10
|
export function nonEmpty<T>(
|
|
8
11
|
t: T | null | undefined | "" | 0 | -0 | 0n | false | typeof NaN,
|
package/src/nonNil.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type asserts that `t` is neither null nor undefined.
|
|
3
3
|
* Throws an error if `t` is null or undefined.
|
|
4
|
+
*
|
|
5
|
+
* @param varName The variable name to include in the error to throw when t is
|
|
6
|
+
* nil. Defaults to 'value'.
|
|
4
7
|
*/
|
|
5
8
|
export function nonNil<T>(t: T | null | undefined, varName = "value"): T {
|
|
6
9
|
if (t === null || t === undefined) {
|
package/src/safeParseInt.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns 0 if the string is not a valid number.
|
|
3
|
+
*
|
|
4
|
+
* @param logError Log a console error if the given string is not a valid
|
|
5
|
+
* number. Defaults to false (don't log anything).
|
|
3
6
|
*/
|
|
4
7
|
export function safeParseInt(s: string, logError = false): number {
|
|
5
8
|
const i = Number(s);
|
package/src/sleep.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep for a given number of milliseconds. Note that this method is async,
|
|
3
|
+
* so please remember to call it with await, like `await sleep(1000);`.
|
|
4
|
+
*/
|
|
1
5
|
export function sleep(ms: number): Promise<void> {
|
|
2
6
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
7
|
}
|