@cripty2001/utils 0.0.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/LICENSE.md ADDED
@@ -0,0 +1,2 @@
1
+ MIT License
2
+ Fabio Mauri (cripty2001@outlook.com)
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@cripty2001/utils",
3
+ "version": "0.0.1",
4
+ "description": "Internal Set of utils. If you need them use them, otherwise go to the next package ;)",
5
+ "homepage": "https://github.com/cripty2001/utils#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/cripty2001/utils/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/cripty2001/utils.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Fabio Mauri <cripty2001@outlook.com>",
15
+ "type": "commonjs",
16
+ "main": "/dist/index.js",
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1",
19
+ "build": "tsc"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "dependencies": {
25
+ "@cripty2001/whispr": "^0.1.0",
26
+ "@types/lodash": "^4.17.20",
27
+ "lodash": "^4.17.21"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.js"
33
+ },
34
+ "./dispatcher": {
35
+ "import": "./dist/Dispatcher.js",
36
+ "require": "./dist/Dispatcher.js"
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,163 @@
1
+ import { Whispr, WhisprSetter } from "@cripty2001/whispr";
2
+ import { sleep } from ".";
3
+ import { isEqual } from "lodash";
4
+
5
+ export type DispatcherStatePayload<T> =
6
+ {
7
+ loading: true,
8
+ progress: number,
9
+ } | (
10
+ { loading: false } & (
11
+ {
12
+ ok: true;
13
+ data: T
14
+ } | {
15
+ ok: false;
16
+ error: Error
17
+ }
18
+ )
19
+ )
20
+
21
+ type DispatcherState<T> = {
22
+ controller: AbortController;
23
+ payload: DispatcherStatePayload<T>;
24
+ }
25
+
26
+ type DispatcherFunction<I, O> = (data: I, setProgress: (p: number) => void, signal: AbortSignal) => Promise<O>
27
+ export class Dispatcher<I, O> {
28
+ private state: Whispr<DispatcherState<O>>;
29
+ private setState: WhisprSetter<DispatcherState<O>>;
30
+
31
+ public data: Whispr<DispatcherStatePayload<O>>;
32
+ public filtered: Whispr<O | null>;
33
+
34
+ public readonly DEBOUNCE_INTERVAL;
35
+ private readonly f: DispatcherFunction<I, O>;
36
+
37
+ private value: Whispr<I>; // Value is a whispr that we are subscribed to. We must keep a reference to it to avoid the subscription being automatically canceled
38
+ private lastValue: I | null = null; // Last value, to avoid useless dispatches
39
+
40
+ /**
41
+ * Create a new dispatcher
42
+ * @param value The whispr value that will trigger f call when changed. Using this pattern instead of exposing a dispatch method allow to return the full dispatcher to anyone, without having to worry about them messing it
43
+ * @param f The async function to call. It should return a promise that resolves to the data.
44
+ * @param DEBOUNCE_INTERVAL
45
+ *
46
+ * @remarks The value is deep checked for equality. The function will be called only if the value changed deeply
47
+ */
48
+ constructor(value: Whispr<I>, f: DispatcherFunction<I, O>, DEBOUNCE_INTERVAL: number = 200) {
49
+ // Initing state
50
+ this.f = f;
51
+ this.DEBOUNCE_INTERVAL = DEBOUNCE_INTERVAL;
52
+ this.value = value;
53
+
54
+ [this.state, this.setState] = Whispr.create<DispatcherState<O>>({
55
+ controller: new AbortController(),
56
+ payload: {
57
+ loading: true,
58
+ progress: 0,
59
+ }
60
+ });
61
+
62
+ // Subscribing to input changes
63
+ this.value.subscribe((v) => {
64
+ if (this.lastValue !== null && isEqual(this.lastValue, v))
65
+ return;
66
+
67
+ this.lastValue = v;
68
+ this.dispatch(v);
69
+ });
70
+
71
+ // Initing public derived whisprs
72
+ this.data = Whispr
73
+ .from({ state: this.state }, ({ state }) => state.payload);
74
+
75
+ this.filtered = Whispr.from(
76
+ {
77
+ data: this.data
78
+ },
79
+ ({ data }) => {
80
+ if (data.loading)
81
+ return null;
82
+ if (!data.ok)
83
+ return null;
84
+ return data.data;
85
+ }
86
+ )
87
+ }
88
+
89
+ private reset() {
90
+ // Aborting previous request
91
+ this.state.value.controller.abort();
92
+
93
+ // Initing new abort controller
94
+ const controller = new AbortController();
95
+
96
+ // Resetting response state
97
+ this.setState({
98
+ controller,
99
+ payload: {
100
+ loading: true,
101
+ progress: 0,
102
+ }
103
+ });
104
+
105
+ // Creating generic state update function
106
+ const updateState = (value: DispatcherStatePayload<O>) => {
107
+ if (controller.signal.aborted) // Working on local controller, not global one. Old controller will change and be aborted on reset, global one will always be running
108
+ return;
109
+
110
+ this.setState({
111
+ controller: this.state.value.controller, // Keeping the effective controller, not the internal old one (even if, in practice, they should be the same, if everything worked well),
112
+ payload: value,
113
+ });
114
+ }
115
+
116
+ // Returning state update function
117
+ return {
118
+ commit: (data: O) => {
119
+ updateState({
120
+ loading: false,
121
+ ok: true,
122
+ data,
123
+ });
124
+ },
125
+ raise: (error: Error) => {
126
+ updateState({
127
+ loading: false,
128
+ ok: false,
129
+ error,
130
+ });
131
+ },
132
+ progress: (p: number) => {
133
+ updateState({
134
+ loading: true,
135
+ progress: p,
136
+ });
137
+ },
138
+ controller
139
+ };
140
+ };
141
+
142
+ private dispatch(data: I): Promise<void> {
143
+ const signals = this.reset();
144
+
145
+ const toReturn = (async () => {
146
+ // Debouncing rapid changes
147
+ await sleep(this.DEBOUNCE_INTERVAL);
148
+ if (signals.controller.signal.aborted)
149
+ throw new DOMException('Debounced', 'AbortError');
150
+
151
+ // Scheduling function execution
152
+ return await this.f(data, signals.progress, signals.controller.signal)
153
+ })()
154
+ .then((res) => {
155
+ signals.commit(res);
156
+ })
157
+ .catch((e) => {
158
+ signals.raise(e instanceof Error ? e : new Error(JSON.stringify(e)));
159
+ });
160
+
161
+ return toReturn;
162
+ }
163
+ }
package/src/index.ts ADDED
@@ -0,0 +1,73 @@
1
+ export type JSONEncodable = number | string | boolean | JSONEncodable[] | { [key: string]: JSONEncodable };
2
+
3
+ export type TypeofArray<T extends any[]> = T extends (infer U)[] ? U : never;
4
+ export type TypeofRecord<T extends Record<string, any>> = T extends Record<
5
+ string,
6
+ infer U
7
+ >
8
+ ? U
9
+ : never;
10
+
11
+ export function getRandom(_alphabeth: string, length: number): string {
12
+ const alphabeth = _alphabeth.split("");
13
+ const toReturn: string[] = [];
14
+ while (toReturn.length < length) {
15
+ toReturn.push(alphabeth[Math.floor(Math.random() * alphabeth.length)]);
16
+ }
17
+ return toReturn.join("");
18
+ }
19
+
20
+ export function getRandomId(length: number = 20): string {
21
+ const ALPHABET =
22
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
23
+ return getRandom(ALPHABET, length);
24
+ }
25
+
26
+ export function getRandomOtp(
27
+ length: number = 6,
28
+ char: boolean = false
29
+ ): string {
30
+ const ALPHABET = "0123456789" + (char ? "ABCDEFGHIJKLMNOPQRSTUVWXYZ" : "");
31
+ return getRandom(ALPHABET, length);
32
+ }
33
+
34
+ export function sleep(ms: number): Promise<void> {
35
+ return new Promise((resolve) => setTimeout(resolve, ms));
36
+ }
37
+
38
+ export function parseHash(fields: string[]): URLSearchParams {
39
+ // Checking empty hash
40
+ if (window.location.hash === "") return new URLSearchParams();
41
+
42
+ // Parsing hash
43
+ const data = new URLSearchParams(window.location.hash.replace(/^#/, "?"));
44
+
45
+ // Extracting fields
46
+ const toReturn: URLSearchParams = new URLSearchParams();
47
+ for (const field of fields) {
48
+ if (data.has(field)) {
49
+ toReturn.set(field, data.get(field) as string);
50
+ data.delete(field);
51
+ }
52
+ }
53
+
54
+ // Reencoding hash without extracted fields
55
+ window.location.hash = `#${data.toString()}`;
56
+
57
+ // Returning extracted fields
58
+ return toReturn;
59
+ }
60
+
61
+ export async function loop(cb: () => Promise<void>, interval: number, onError: (e: any) => Promise<void> = async (e) => { console.error(e) }): Promise<void> {
62
+ while (true) {
63
+ try {
64
+ await cb();
65
+ } catch (e) {
66
+ await onError(e);
67
+ }
68
+ finally {
69
+ await sleep(interval);
70
+ }
71
+ }
72
+ }
73
+
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "rootDir": "./src",
6
+ "moduleResolution": "NodeNext",
7
+ "declaration": true,
8
+ "outDir": "./dist",
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "strict": true,
12
+ "skipLibCheck": true
13
+ }
14
+ }