@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/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
- expireMs: number;
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
- !("expireMs" in obj) ||
53
- typeof obj.expireMs !== "number" ||
54
- Date.now() >= obj.expireMs
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
- expireDeltaMs: number = this.defaultExpiryDeltaMs,
176
+ expiryDeltaMs: number | Duration = this.defaultExpiryDeltaMs,
172
177
  ): Promise<T> {
173
- const obj = {
178
+ const nowMs = Date.now();
179
+ const obj: StoredObject<T> = {
174
180
  key,
175
181
  value,
176
- expireMs: Date.now() + expireDeltaMs,
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 get<T>(key: string): Promise<T | undefined> {
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.value;
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
- expireMs: number,
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.expireMs);
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, expireMs) => {
304
- map.set(key, { value, expireMs });
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, expireMs: number) => {
349
- if (value === undefined || Date.now() >= expireMs) {
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
+ }
@@ -1,4 +1,6 @@
1
- import { Duration, durationToMs } from "./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
- * expires - Either a number in milliseconds, or a Duration object
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(value: T): void {
50
- const expireMs = Date.now() + this.expireDeltaMs;
51
- const valueStr = JSON.stringify({ value, expireMs });
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
- * Get the value of this item, or undefined if value is not set or expired.
65
+ * Example usage:
66
+ *
67
+ * const { value, storedMs, expiryMs } = await myItem.getStoredItem();
58
68
  */
59
- public get(): T | undefined {
69
+ public getStoredItem(): StoredItem<T> | undefined {
60
70
  const jsonStr = globalThis.localStorage.getItem(this.key);
61
71
 
62
- if (!jsonStr || typeof jsonStr !== "string") {
72
+ if (!jsonStr) {
63
73
  return undefined;
64
74
  }
65
75
 
66
76
  try {
67
- const obj: { value: T; expireMs: number } | undefined =
68
- JSON.parse(jsonStr);
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
- !("expireMs" in obj) ||
74
- typeof obj.expireMs !== "number" ||
75
- Date.now() >= obj.expireMs
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
- globalThis.localStorage.removeItem(this.key);
89
+ this.remove();
78
90
  return undefined;
79
91
  }
80
92
 
81
- return obj.value;
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
- globalThis.localStorage.removeItem(this.key);
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) {
@@ -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
  }