@pistonite/pure 0.0.17 → 0.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pistonite/pure",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Pure TypeScript libraries for my projects",
5
5
  "homepage": "https://github.com/Pistonite/pure",
6
6
  "bugs": {
package/src/sync/index.ts CHANGED
@@ -10,6 +10,7 @@ export { debounce, type DebounceConstructor } from "./debounce.ts";
10
10
  export { batch, type BatchConstructor } from "./batch.ts";
11
11
  export { cell, type CellConstructor, type Cell } from "./cell.ts";
12
12
  export { persist, type PersistConstructor, type Persist } from "./persist.ts";
13
+ export { once, type OnceConstructor } from "./once.ts";
13
14
 
14
15
  // unstable
15
16
  export { RwLock } from "./RwLock.ts";
@@ -0,0 +1,107 @@
1
+ import { makePromise } from "./util";
2
+
3
+ /**
4
+ * An async event wrapper that ensures an async initialization is only ran once.
5
+ * Any subsequent calls after the first call will return a promise that resolves/rejects
6
+ * with the result of the first call.
7
+ *
8
+ * ## Example
9
+ * ```typescript
10
+ * import { once } from "@pistonite/pure/sync";
11
+ *
12
+ * const getLuckyNumber = once({
13
+ * fn: async () => {
14
+ * console.log("running expensive initialization...")
15
+ * await new Promise((resolve) => setTimeout(resolve, 100));
16
+ * console.log("done")
17
+ * return 42;
18
+ * }
19
+ * });
20
+ *
21
+ * const result1 = getLuckyNumber();
22
+ * const result2 = getLuckyNumber();
23
+ * console.log(await result1);
24
+ * console.log(await result2);
25
+ * // logs:
26
+ * // running expensive initialization...
27
+ * // done
28
+ * // 42
29
+ * // 42
30
+ * ```
31
+ *
32
+ * ## Caveat with HMR
33
+ * Some initialization might require clean up, such as unregister
34
+ * event handlers and/or timers. In this case, a production build might
35
+ * work fine but a HMR (Hot Module Reload) development server might not
36
+ * do this for you automatically.
37
+ *
38
+ * One way to work around this during development is to store the cleanup
39
+ * as a global object
40
+ * ```typescript
41
+ * const getResourceThatNeedsCleanup = once({
42
+ * fn: async () => {
43
+ * if (__DEV__) { // Configure your bundler to inject this
44
+ * // await if you need async clean up
45
+ * await (window as any).cleanupMyResource?.();
46
+ * }
47
+ *
48
+ * let resource: MyResource;
49
+ * if (__DEV__) {
50
+ * (window as any).cleanupMyResource = async () => {
51
+ * await resource?.cleanup();
52
+ * };
53
+ * }
54
+ *
55
+ * resource = await initResource();
56
+ * return resource;
57
+ * }
58
+ * });
59
+ * ```
60
+ *
61
+ * An alternative solution is to not use `once` but instead tie the initialization
62
+ * of the resource to some other lifecycle event that gets cleaned up during HMR.
63
+ * For example, A framework that supports HMR for React components might unmount
64
+ * the component before reloading, which gives you a chance to clean up the resource.
65
+ *
66
+ * This is not an issue if the resource doesn't leak other resources,
67
+ * since it will eventually be GC'd.
68
+ */
69
+ export function once<TFn extends (...args: any[]) => any>({
70
+ fn,
71
+ }: OnceConstructor<TFn>) {
72
+ const impl = new OnceImpl(fn);
73
+ return (...args: Parameters<TFn>): Promise<Awaited<ReturnType<TFn>>> =>
74
+ impl.invoke(...args);
75
+ }
76
+
77
+ export type OnceConstructor<TFn> = {
78
+ /** Function to be called only once */
79
+ fn: TFn;
80
+ };
81
+
82
+ export class OnceImpl<TFn extends (...args: any[]) => any> {
83
+ private promise: Promise<Awaited<ReturnType<TFn>>> | undefined;
84
+
85
+ constructor(private fn: TFn) {
86
+ this.fn = fn;
87
+ }
88
+
89
+ public async invoke(
90
+ ...args: Parameters<TFn>
91
+ ): Promise<Awaited<ReturnType<TFn>>> {
92
+ if (this.promise) {
93
+ return this.promise;
94
+ }
95
+ const { promise, resolve, reject } =
96
+ makePromise<Awaited<ReturnType<TFn>>>();
97
+ this.promise = promise;
98
+ try {
99
+ const result = await this.fn(...args);
100
+ resolve(result);
101
+ return result;
102
+ } catch (e) {
103
+ reject(e);
104
+ throw e;
105
+ }
106
+ }
107
+ }
package/src/sync/util.ts CHANGED
@@ -1,4 +1,8 @@
1
- export const makePromise = <T>() => {
1
+ export const makePromise = <T>(): {
2
+ promise: Promise<T>;
3
+ resolve: (value: T | PromiseLike<T>) => void;
4
+ reject: (reason?: any) => void;
5
+ } => {
2
6
  let resolve;
3
7
  let reject;
4
8
  const promise = new Promise<T>((res, rej) => {