@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.
@@ -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: TimeArr | num | str,
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
- timeZone = timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
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
- timeObj: timeObj(zonedTemporal),
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: weekTuple[dt.getDay()]!
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: weekTuple[dt.dayOfWeek - 1]!
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) {