@giveback007/util-lib 2.1.3 → 2.3.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/dist/@state.types.d.ts +16 -0
- package/dist/@state.types.js +3 -0
- package/dist/@state.types.js.map +1 -0
- package/dist/@types.d.ts +10 -11
- package/dist/general.d.ts +0 -2
- package/dist/general.js +1 -4
- package/dist/general.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/number.d.ts +1 -8
- package/dist/number.js +3 -11
- package/dist/number.js.map +1 -1
- package/dist/state-manager.d.ts +74 -0
- package/dist/state-manager.js +227 -0
- package/dist/state-manager.js.map +1 -0
- package/dist/time.d.ts +134 -3
- package/dist/time.js +48 -8
- package/dist/time.js.map +1 -1
- package/package.json +1 -1
- package/src/@state.types.ts +19 -0
- package/src/@types.ts +28 -18
- package/src/general.ts +0 -4
- package/src/index.ts +4 -2
- package/src/number.ts +2 -11
- package/src/state-manager.ts +301 -0
- package/src/time.ts +50 -8
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Action, actSubFct, lsOptions, stateSubFct
|
|
3
|
+
} from './@state.types';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Dict, KeysOfValueType
|
|
7
|
+
} from '.'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
wait, equal, objExtract,
|
|
11
|
+
isType, objKeys, clone
|
|
12
|
+
} from '.';
|
|
13
|
+
|
|
14
|
+
export class StateManager<
|
|
15
|
+
State extends {},
|
|
16
|
+
Act extends Action<any, any> = Action<any, any>,
|
|
17
|
+
Key extends Extract<keyof State, string> = Extract<keyof State, string>
|
|
18
|
+
> {
|
|
19
|
+
|
|
20
|
+
private prevState: State | null = null;
|
|
21
|
+
private emittedState: State | null = null;
|
|
22
|
+
private state: State;
|
|
23
|
+
|
|
24
|
+
private readonly useLS: lsOptions<Key> | false = false;
|
|
25
|
+
|
|
26
|
+
private stateSubDict: Dict<stateSubFct<State>> = {};
|
|
27
|
+
private actionSubDict: Dict<actSubFct<Act>> = {};
|
|
28
|
+
|
|
29
|
+
private stateWasUpdated = true;
|
|
30
|
+
private keysChanged: { [K in Key]?: true } = {};
|
|
31
|
+
|
|
32
|
+
private throttledState: Dict<Partial<State>> = {};
|
|
33
|
+
private throttlersRunning: Dict<boolean> = {};
|
|
34
|
+
/**
|
|
35
|
+
* The local storage takes an id, this id
|
|
36
|
+
* should be unique in order to ensure that the
|
|
37
|
+
* storage is unique to the given state object
|
|
38
|
+
*/
|
|
39
|
+
constructor(
|
|
40
|
+
initialState: State,
|
|
41
|
+
useLocalStorage?: lsOptions<Key>
|
|
42
|
+
) {
|
|
43
|
+
let state = {} as State;
|
|
44
|
+
|
|
45
|
+
if (useLocalStorage) {
|
|
46
|
+
const { useKeys, ignoreKeys, id } = useLocalStorage;
|
|
47
|
+
|
|
48
|
+
if (useKeys && ignoreKeys) throw Error(
|
|
49
|
+
'"useKeys" & "ignoreKeys" are mutually '
|
|
50
|
+
+ 'exclusive, only use one or the other.'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
this.useLS = useLocalStorage;
|
|
54
|
+
const lsId = this.useLS.id = id + '-utilStateManager';
|
|
55
|
+
|
|
56
|
+
state = {
|
|
57
|
+
...initialState,
|
|
58
|
+
...this.stateFromLS(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
addEventListener('storage', (e: StorageEvent) => {
|
|
62
|
+
if (e.key !== lsId) return;
|
|
63
|
+
|
|
64
|
+
let fromLS = this.stateFromLS();
|
|
65
|
+
|
|
66
|
+
if (useKeys)
|
|
67
|
+
fromLS = objExtract(fromLS, useKeys);
|
|
68
|
+
else if (ignoreKeys)
|
|
69
|
+
ignoreKeys.forEach((key) => delete fromLS[key]);
|
|
70
|
+
|
|
71
|
+
if (equal(this.state, { ...this.state, ...fromLS })) return;
|
|
72
|
+
|
|
73
|
+
this.setState(fromLS);
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
state = initialState;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.state = state;
|
|
80
|
+
this.setState(state);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getState = () => this.state;
|
|
84
|
+
|
|
85
|
+
setState = async (updateState: Partial<State>) => {
|
|
86
|
+
// Do this, otherwise you are mutating the value.
|
|
87
|
+
// (Would make bugs in this case)
|
|
88
|
+
this.state = { ...this.state, ...updateState };
|
|
89
|
+
|
|
90
|
+
this.stateWasUpdated = true;
|
|
91
|
+
await this.stateChanged();
|
|
92
|
+
return this.getState();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
action = <A extends Act = Act>(action: A | A['type']) => {
|
|
96
|
+
if (isType(action, 'string')) action = { type: action } as A;
|
|
97
|
+
const state = this.getState();
|
|
98
|
+
|
|
99
|
+
for (const k in this.actionSubDict)
|
|
100
|
+
this.actionSubDict[k]?.(action, state);
|
|
101
|
+
|
|
102
|
+
return action;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// -- State Set Throttler -- //
|
|
106
|
+
/**
|
|
107
|
+
* Aggregates state updates by this method over the course of
|
|
108
|
+
* `msCycle` time and sets the state only once per `msCycle` time.
|
|
109
|
+
*
|
|
110
|
+
* Different `msCycle` timings run on separate loops, therefore can
|
|
111
|
+
* run multiple `msCycle` at the same time.
|
|
112
|
+
*
|
|
113
|
+
* To keep state consistent and to prevent bugs, any key run-ins set
|
|
114
|
+
* in previous `msCycle`(s) will be overwritten by latest
|
|
115
|
+
* `throttledSetState()` call.
|
|
116
|
+
*
|
|
117
|
+
* Will wait the full designated time in `msCycle` on first run.
|
|
118
|
+
*/
|
|
119
|
+
throttledSetState = async (
|
|
120
|
+
msCycle: number,
|
|
121
|
+
updateState: Partial<State>
|
|
122
|
+
) => {
|
|
123
|
+
if (!this.throttledState[msCycle])
|
|
124
|
+
this.throttledState[msCycle] = {};
|
|
125
|
+
|
|
126
|
+
const tsKeys = objKeys(this.throttledState);
|
|
127
|
+
for (const k in updateState) {
|
|
128
|
+
this.throttledState[msCycle][k] = updateState[k];
|
|
129
|
+
|
|
130
|
+
tsKeys.forEach((tsKey) => // To keep state consistent
|
|
131
|
+
this.throttledState[tsKey]![k] = updateState[k]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.throttledStateSetter(msCycle);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private throttledStateSetter = async (msCycle: number) => {
|
|
138
|
+
if (this.throttlersRunning[msCycle]) return;
|
|
139
|
+
this.throttlersRunning[msCycle] = true;
|
|
140
|
+
|
|
141
|
+
await wait(msCycle);
|
|
142
|
+
while (this.throttledState[msCycle]) {
|
|
143
|
+
this.setState(this.throttledState[msCycle]);
|
|
144
|
+
delete this.throttledState[msCycle];
|
|
145
|
+
await wait(msCycle);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.throttlersRunning[msCycle] = false;
|
|
149
|
+
}
|
|
150
|
+
// -- State Set Throttler -- //
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Will execute the given function on state change. Subscribe to
|
|
154
|
+
* specific key(s) changes in state by setting keys to the desired
|
|
155
|
+
* key(s) to sub to. Set `keys: true` to sub to all state changes.
|
|
156
|
+
*/
|
|
157
|
+
stateSub = <K extends Key = Key>(
|
|
158
|
+
keys: true | K[] | K,
|
|
159
|
+
fct: stateSubFct<State>,
|
|
160
|
+
fireOnInitSub = false
|
|
161
|
+
) => {
|
|
162
|
+
if (isType(keys, 'array') && keys.length === 1)
|
|
163
|
+
keys = keys[0]!;
|
|
164
|
+
|
|
165
|
+
let f = fct;
|
|
166
|
+
|
|
167
|
+
if (isType(keys, 'string')) f = (s, prev) => {
|
|
168
|
+
if (this.keysChanged[keys as K]) return fct(s, prev);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
else if (isType(keys, 'array')) f = (s, prev) => {
|
|
172
|
+
for (const k of keys as K[])
|
|
173
|
+
if (this.keysChanged[k]) return fct(s, prev);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (fireOnInitSub)
|
|
177
|
+
wait(0).then(() => fct(this.state, this.prevState));
|
|
178
|
+
|
|
179
|
+
const id = Math.random();
|
|
180
|
+
this.stateSubDict[id] = f as any;
|
|
181
|
+
return { unsubscribe: () => delete this.stateSubDict[id] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** set `true` if to subscribe to all actions */
|
|
185
|
+
actionSub = <
|
|
186
|
+
T extends Act['type'] = Act['type'],
|
|
187
|
+
A extends Extract<Act, { type: T }> = Extract<Act, { type: T }>
|
|
188
|
+
>(
|
|
189
|
+
actions: true | T | T[],
|
|
190
|
+
fct: actSubFct<A, State>
|
|
191
|
+
) => {
|
|
192
|
+
if (isType(actions, 'array') && actions.length === 1)
|
|
193
|
+
actions = actions[0]!;
|
|
194
|
+
|
|
195
|
+
let f = fct;
|
|
196
|
+
|
|
197
|
+
if (isType(actions, 'string')) f = (a, s) => {
|
|
198
|
+
if (a.type === actions) return fct(a, s);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
else if (isType(actions, 'array')) f = (a, s) => {
|
|
202
|
+
for (const act of actions as T['type'][])
|
|
203
|
+
if (a.type === act) return fct(a, s);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const id = Math.random();
|
|
207
|
+
this.actionSubDict[id] = f as any;
|
|
208
|
+
return { unsubscribe: () => delete this.actionSubDict[id] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Allows you to toggle any key with a boolean value true/false.
|
|
213
|
+
*/
|
|
214
|
+
toggle = (key: KeysOfValueType<State, boolean>) =>
|
|
215
|
+
this.setState({ [key]: (!this.getState()[key]) } as any);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Erases local storage managed by this instance of StateManager,
|
|
219
|
+
* & removes all properties/methods on the object. (This way any
|
|
220
|
+
* attempts of accessing the object should return an error);
|
|
221
|
+
*
|
|
222
|
+
* (For debugging purposes):
|
|
223
|
+
* Object will have this appearance afterwards:
|
|
224
|
+
* ```js
|
|
225
|
+
* { type: 'StateManager', destroyed: true }
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
destroy = () => {
|
|
229
|
+
if (this.useLS)
|
|
230
|
+
localStorage.removeItem(this.useLS.id);
|
|
231
|
+
|
|
232
|
+
objKeys(this).forEach(k => delete this[k]);
|
|
233
|
+
(this as any).type = 'StateManager';
|
|
234
|
+
(this as any).destroyed = true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
cloneKey =
|
|
238
|
+
<K extends Key>(key: K): State[K] => clone(this.state[key]);
|
|
239
|
+
|
|
240
|
+
private stateChanged = async () => {
|
|
241
|
+
// Ensures to run only after all sync code updates the state.
|
|
242
|
+
await wait(0);
|
|
243
|
+
if (!this.stateWasUpdated) return false;
|
|
244
|
+
|
|
245
|
+
let stateDidNotChange = true;
|
|
246
|
+
|
|
247
|
+
objKeys(this.state).forEach((k) => {
|
|
248
|
+
const em = this.emittedState || {} as Partial<State>;
|
|
249
|
+
|
|
250
|
+
if (// only check equality if not already changed.
|
|
251
|
+
!this.keysChanged[k as Key] && equal(this.state[k], em[k])
|
|
252
|
+
) return;
|
|
253
|
+
|
|
254
|
+
stateDidNotChange = false;
|
|
255
|
+
this.keysChanged[k as Key] = true;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (stateDidNotChange)
|
|
259
|
+
return this.stateWasUpdated = false;
|
|
260
|
+
|
|
261
|
+
this.updateLocalStorage();
|
|
262
|
+
|
|
263
|
+
// these 3 need to be set before iterating over subs
|
|
264
|
+
// else prevState wont be accurately emitted
|
|
265
|
+
this.prevState = this.emittedState;
|
|
266
|
+
this.emittedState = this.state;
|
|
267
|
+
this.state = { ...this.state }; // use obj spread (or get bugs)!
|
|
268
|
+
|
|
269
|
+
for (const k in this.stateSubDict)
|
|
270
|
+
this.stateSubDict[k]?.(this.state, this.prevState as State);
|
|
271
|
+
|
|
272
|
+
this.keysChanged = {};
|
|
273
|
+
this.stateWasUpdated = false;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private stateFromLS = () => {
|
|
278
|
+
if (!this.useLS) return;
|
|
279
|
+
|
|
280
|
+
const { id } = this.useLS;
|
|
281
|
+
const strState = localStorage.getItem(id);
|
|
282
|
+
if (!strState) return {};
|
|
283
|
+
|
|
284
|
+
return JSON.parse(strState);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private updateLocalStorage = () => {
|
|
288
|
+
if (!this.useLS) return;
|
|
289
|
+
|
|
290
|
+
const { id, ignoreKeys, useKeys } = this.useLS;
|
|
291
|
+
|
|
292
|
+
let state = { ...this.state };
|
|
293
|
+
|
|
294
|
+
if (ignoreKeys)
|
|
295
|
+
ignoreKeys.forEach((key) => delete state[key]);
|
|
296
|
+
else if (useKeys)
|
|
297
|
+
state = objExtract(state, useKeys);
|
|
298
|
+
|
|
299
|
+
localStorage.setItem(id, JSON.stringify(state));
|
|
300
|
+
}
|
|
301
|
+
}
|
package/src/time.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Temporal } from '@js-temporal/polyfill';
|
|
2
2
|
import { AnyDate, isType, MsTime, num, str, TimeArr, TimeObj } from '.';
|
|
3
3
|
|
|
4
|
+
/** A promise that waits `ms` amount of milliseconds to execute */
|
|
5
|
+
export const wait = (ms: number): Promise<void> =>
|
|
6
|
+
new Promise((res) => setTimeout(() => res(), ms));
|
|
7
|
+
|
|
8
|
+
/** Resolves after a given msEpoch passes. `msEpoch - Date.now()` */
|
|
9
|
+
export const waitUntil = (msEpoch: number): Promise<void> =>
|
|
10
|
+
new Promise(res => setTimeout(res, msEpoch - Date.now()))
|
|
11
|
+
|
|
4
12
|
export const msTime: MsTime = {
|
|
5
13
|
s: 1000,
|
|
6
14
|
m: 60000,
|
|
@@ -9,8 +17,6 @@ export const msTime: MsTime = {
|
|
|
9
17
|
w: 604800000,
|
|
10
18
|
}
|
|
11
19
|
|
|
12
|
-
export const weekTuple = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const;
|
|
13
|
-
|
|
14
20
|
/**
|
|
15
21
|
* Converts Date to time of day.
|
|
16
22
|
* Good for use in logging.
|
|
@@ -56,6 +62,7 @@ export const time = {
|
|
|
56
62
|
min: (n: num) => Date.now() + n * msTime.m,
|
|
57
63
|
},
|
|
58
64
|
|
|
65
|
+
/** Convert ms into: */
|
|
59
66
|
msTo: {
|
|
60
67
|
/** fnc(n) -> from ms to num of seconds */
|
|
61
68
|
sec: (ms: num) => ms / msTime.s,
|
|
@@ -97,13 +104,17 @@ export const humanizedTime = (date: AnyDate) => {
|
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
// Intl.supportedValuesOf('timeZone');
|
|
101
107
|
/** A Date substitute, to make working with time easier and more versatile */
|
|
102
108
|
export function getTime(
|
|
103
|
-
t
|
|
109
|
+
t?: Temporal.ZonedDateTime | TimeArr | num | str | 'now',
|
|
110
|
+
/** For a list of available timeZone values run:
|
|
111
|
+
* `Intl.supportedValuesOf('timeZone');` */
|
|
104
112
|
timeZone?: str
|
|
105
113
|
) {
|
|
106
|
-
|
|
114
|
+
if (t === undefined || t === 'now') t = Date.now();
|
|
115
|
+
if (!timeZone) timeZone = t instanceof Temporal.ZonedDateTime ?
|
|
116
|
+
t.timeZoneId : Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
117
|
+
|
|
107
118
|
let zonedTemporal: Temporal.ZonedDateTime;
|
|
108
119
|
let date: Date;
|
|
109
120
|
let isoStr: str;
|
|
@@ -114,6 +125,8 @@ export function getTime(
|
|
|
114
125
|
zonedTemporal = Temporal.ZonedDateTime.from(isoStr + `[${timeZone}]`);
|
|
115
126
|
} else if (isType(t, 'string')) {
|
|
116
127
|
zonedTemporal = Temporal.ZonedDateTime.from(t + `[${timeZone}]`)
|
|
128
|
+
} else if (t instanceof Temporal.ZonedDateTime) {
|
|
129
|
+
zonedTemporal = t.withTimeZone(timeZone)
|
|
117
130
|
} else {
|
|
118
131
|
zonedTemporal = Temporal.ZonedDateTime.from({
|
|
119
132
|
year: t[0],
|
|
@@ -134,10 +147,39 @@ export function getTime(
|
|
|
134
147
|
date,
|
|
135
148
|
tzOffsetMin: zonedTemporal.offsetNanoseconds / 60_000_000_000,
|
|
136
149
|
localISO: zonedTemporal.toJSON(),
|
|
137
|
-
|
|
150
|
+
obj: timeObj(zonedTemporal),
|
|
138
151
|
isoStr: isoStr! || date.toISOString(),
|
|
139
152
|
timeZone: timeZone,
|
|
140
153
|
epochMs: zonedTemporal.epochMilliseconds,
|
|
154
|
+
startOf: {
|
|
155
|
+
day: () => getTime(zonedTemporal.startOfDay(), timeZone),
|
|
156
|
+
|
|
157
|
+
month: () => {
|
|
158
|
+
const zT = zonedTemporal;
|
|
159
|
+
return getTime([zT.year, zT.month, 1, 0, 0, 0, 0], timeZone)
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
year: () => {
|
|
163
|
+
const zT = zonedTemporal;
|
|
164
|
+
return getTime([zT.year, 1, 1, 0, 0, 0, 0], timeZone)
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
endOf: {
|
|
168
|
+
day: () => {
|
|
169
|
+
const zT = zonedTemporal;
|
|
170
|
+
return getTime([zT.year, zT.month, zT.day, 23, 59, 59, 999], timeZone)
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
month: () => {
|
|
174
|
+
const zT = zonedTemporal;
|
|
175
|
+
return getTime([zT.year, zT.month, zT.daysInMonth, 23, 59, 59, 999], timeZone)
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
year: () => {
|
|
179
|
+
const zT = zonedTemporal;
|
|
180
|
+
return getTime([zT.year, 12, 31, 23, 59, 59, 999], timeZone)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
141
183
|
}
|
|
142
184
|
}
|
|
143
185
|
|
|
@@ -149,7 +191,7 @@ export const timeObj = (dt: Date | Temporal.ZonedDateTime): TimeObj => dt instan
|
|
|
149
191
|
min: dt.getMinutes(),
|
|
150
192
|
sec: dt.getSeconds(),
|
|
151
193
|
ms: dt.getMilliseconds(),
|
|
152
|
-
wDay:
|
|
194
|
+
wDay: (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const)[dt.getDay()]!
|
|
153
195
|
}) : ({
|
|
154
196
|
y: dt.year,
|
|
155
197
|
m: dt.month,
|
|
@@ -158,7 +200,7 @@ export const timeObj = (dt: Date | Temporal.ZonedDateTime): TimeObj => dt instan
|
|
|
158
200
|
min: dt.minute,
|
|
159
201
|
sec: dt.second,
|
|
160
202
|
ms: dt.millisecond,
|
|
161
|
-
wDay:
|
|
203
|
+
wDay: (['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const)[dt.dayOfWeek - 1]!
|
|
162
204
|
});
|
|
163
205
|
|
|
164
206
|
export function parseDate(d: AnyDate) {
|