@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 +2 -0
- package/package.json +39 -0
- package/src/Dispatcher.ts +163 -0
- package/src/index.ts +73 -0
- package/tsconfig.json +14 -0
package/LICENSE.md
ADDED
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
|
+
}
|