@dr.pogodin/js-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 +22 -0
- package/README.md +27 -0
- package/babel.config.js +6 -0
- package/bin/release.sh +6 -0
- package/jest.config.js +4 -0
- package/js/Barrier.d.ts +24 -0
- package/js/Barrier.js +53 -0
- package/js/Emitter.d.ts +30 -0
- package/js/Emitter.js +49 -0
- package/js/Semaphore.d.ts +25 -0
- package/js/Semaphore.js +90 -0
- package/js/index.d.ts +4 -0
- package/js/index.js +27 -0
- package/js/time.d.ts +13 -0
- package/js/time.js +53 -0
- package/package.json +48 -0
- package/ts/Barrier.ts +73 -0
- package/ts/Emitter.ts +50 -0
- package/ts/Semaphore.ts +78 -0
- package/ts/index.ts +4 -0
- package/ts/time.ts +44 -0
- package/tsconfig.json +8 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
_Copyright © 2023, Dr. Sergey Pogodin_
|
|
4
|
+
— <doc@pogodin.studio> (https://dr.pogodin.studio) \
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[React Utils]: https://github.com/birdofpreyru/react-utils
|
|
2
|
+
|
|
3
|
+
# JS Utils
|
|
4
|
+
|
|
5
|
+
[_TODO: Latest NPM version / Link to NPM package page_]
|
|
6
|
+
[_TODO: NPM monthly downloads count / Link to NPM package page_]
|
|
7
|
+
[_TODO: CircleCI status of primary branch build / Link to CircleCI project page_]
|
|
8
|
+
[](https://github.com/birdofpreyru/js-utils)
|
|
9
|
+
|
|
10
|
+
The aim for this repo/package is to move in from the [React Utils] the pieces
|
|
11
|
+
which are not React-specific, thus are also useful cross non-React projects,
|
|
12
|
+
and thus having them in a dedicated package will faciliate their re-use
|
|
13
|
+
in generic JavaScript (and TypeScript) projects.
|
|
14
|
+
|
|
15
|
+
At least for the first time, all stuff moved in here will still be exposed from
|
|
16
|
+
[React Utils] the same way as before, and the documentation for these pieces
|
|
17
|
+
will be still kept at https://dr.pogodin.studio/docs/react-utils/index.html.
|
|
18
|
+
Maybe later, time permitting, this will be documented as a stand-alone library,
|
|
19
|
+
but prior to that it will be maintained and used as a stand-alone lib, but not
|
|
20
|
+
very well documented as such.
|
|
21
|
+
|
|
22
|
+
Yeah, the source code will be written in TypeScript, and for the library
|
|
23
|
+
version released to NPM it will be also compiled into plain JavaScript.
|
|
24
|
+
Consumers of that NPM package thus will have access to both TS (`/ts` folder)
|
|
25
|
+
and JS (`/js` folder) version of the library.
|
|
26
|
+
|
|
27
|
+
[](https://github.com/sponsors/birdofpreyru)
|
package/babel.config.js
ADDED
package/bin/release.sh
ADDED
package/jest.config.js
ADDED
package/js/Barrier.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type Resolver<T> = (value: T | PromiseLike<T>) => void;
|
|
2
|
+
type Rejecter = (reason?: any) => void;
|
|
3
|
+
export type Executor<T> = (resolve: Resolver<T>, reject: Rejecter) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Barrier is just a Promise which has resolve and reject exposed as instance
|
|
6
|
+
* methods.
|
|
7
|
+
*
|
|
8
|
+
* Docs: https://dr.pogodin.studio/docs/react-utils/docs/api/classes/Barrier
|
|
9
|
+
*/
|
|
10
|
+
export default class Barrier<T, TR = T> extends Promise<TR> {
|
|
11
|
+
private p_resolve;
|
|
12
|
+
private p_reject;
|
|
13
|
+
private p_state;
|
|
14
|
+
constructor(executor?: Executor<TR>);
|
|
15
|
+
get resolve(): Resolver<T>;
|
|
16
|
+
get reject(): Rejecter;
|
|
17
|
+
get resolved(): boolean;
|
|
18
|
+
get rejected(): boolean;
|
|
19
|
+
get settled(): boolean;
|
|
20
|
+
catch<TR1>(onRejected?: ((reason: any) => TR1 | PromiseLike<TR1>) | null): Barrier<T, TR1>;
|
|
21
|
+
finally(onFinally?: (() => void) | null): Barrier<TR>;
|
|
22
|
+
then<TR1, TR2>(onFulfilled?: ((value: TR) => TR1 | PromiseLike<TR1>) | null, onRejected?: ((reason: any) => TR2 | PromiseLike<TR2>) | null): Barrier<T, TR1 | TR2>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
package/js/Barrier.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
var STATE;
|
|
4
|
+
(function (STATE) {
|
|
5
|
+
STATE["PENDING"] = "PENDING";
|
|
6
|
+
STATE["REJECTED"] = "REJECTED";
|
|
7
|
+
STATE["RESOLVED"] = "RESOLVED";
|
|
8
|
+
})(STATE || (STATE = {}));
|
|
9
|
+
/**
|
|
10
|
+
* Barrier is just a Promise which has resolve and reject exposed as instance
|
|
11
|
+
* methods.
|
|
12
|
+
*
|
|
13
|
+
* Docs: https://dr.pogodin.studio/docs/react-utils/docs/api/classes/Barrier
|
|
14
|
+
*/
|
|
15
|
+
class Barrier extends Promise {
|
|
16
|
+
constructor(executor) {
|
|
17
|
+
let resolveRef;
|
|
18
|
+
let rejectRef;
|
|
19
|
+
super((resolve, reject) => {
|
|
20
|
+
resolveRef = (value) => {
|
|
21
|
+
resolve(value);
|
|
22
|
+
this.p_state = STATE.RESOLVED;
|
|
23
|
+
};
|
|
24
|
+
rejectRef = (reason) => {
|
|
25
|
+
reject(reason);
|
|
26
|
+
this.p_state = STATE.REJECTED;
|
|
27
|
+
};
|
|
28
|
+
if (executor)
|
|
29
|
+
executor(resolveRef, rejectRef);
|
|
30
|
+
});
|
|
31
|
+
this.p_state = STATE.PENDING;
|
|
32
|
+
this.p_resolve = resolveRef;
|
|
33
|
+
this.p_reject = rejectRef;
|
|
34
|
+
}
|
|
35
|
+
get resolve() { return this.p_resolve; }
|
|
36
|
+
get reject() { return this.p_reject; }
|
|
37
|
+
get resolved() { return this.p_state === STATE.RESOLVED; }
|
|
38
|
+
get rejected() { return this.p_state === STATE.REJECTED; }
|
|
39
|
+
get settled() { return this.p_state !== STATE.PENDING; }
|
|
40
|
+
catch(onRejected) {
|
|
41
|
+
return super.catch(onRejected);
|
|
42
|
+
}
|
|
43
|
+
finally(onFinally) {
|
|
44
|
+
return super.finally(onFinally);
|
|
45
|
+
}
|
|
46
|
+
then(onFulfilled, onRejected) {
|
|
47
|
+
const res = super.then(onFulfilled, onRejected);
|
|
48
|
+
res.p_resolve = this.resolve;
|
|
49
|
+
res.p_reject = this.reject;
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.default = Barrier;
|
package/js/Emitter.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type Listener = (...args: any[]) => void;
|
|
2
|
+
/**
|
|
3
|
+
* Simple listeneable data Emitter.
|
|
4
|
+
*/
|
|
5
|
+
export default class Emitter {
|
|
6
|
+
private p_listeners;
|
|
7
|
+
/**
|
|
8
|
+
* Returns "true" if any listener is connected; "false" otherwise.
|
|
9
|
+
* @return {boolean}
|
|
10
|
+
*/
|
|
11
|
+
get hasListeners(): boolean;
|
|
12
|
+
get listeners(): ReadonlyArray<Listener>;
|
|
13
|
+
/**
|
|
14
|
+
* Adds `listener` if it is not already connected.
|
|
15
|
+
* @param {function} listener
|
|
16
|
+
* @return {function} Unsubscribe function.
|
|
17
|
+
*/
|
|
18
|
+
addListener(listener: Listener): () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Calls every connected listener with the given arguments.
|
|
21
|
+
* @param {...any} args
|
|
22
|
+
*/
|
|
23
|
+
emit(...args: any[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Removes specified `listener`, if connected.
|
|
26
|
+
* @param {function} listener
|
|
27
|
+
*/
|
|
28
|
+
removeListener(listener: Listener): void;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
package/js/Emitter.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Simple listeneable data Emitter.
|
|
5
|
+
*/
|
|
6
|
+
class Emitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.p_listeners = [];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns "true" if any listener is connected; "false" otherwise.
|
|
12
|
+
* @return {boolean}
|
|
13
|
+
*/
|
|
14
|
+
get hasListeners() {
|
|
15
|
+
return !!this.p_listeners.length;
|
|
16
|
+
}
|
|
17
|
+
get listeners() { return this.p_listeners; }
|
|
18
|
+
/**
|
|
19
|
+
* Adds `listener` if it is not already connected.
|
|
20
|
+
* @param {function} listener
|
|
21
|
+
* @return {function} Unsubscribe function.
|
|
22
|
+
*/
|
|
23
|
+
addListener(listener) {
|
|
24
|
+
if (!this.p_listeners.includes(listener)) {
|
|
25
|
+
this.p_listeners.push(listener);
|
|
26
|
+
}
|
|
27
|
+
return () => this.removeListener(listener);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Calls every connected listener with the given arguments.
|
|
31
|
+
* @param {...any} args
|
|
32
|
+
*/
|
|
33
|
+
emit(...args) {
|
|
34
|
+
const { p_listeners: listeners } = this;
|
|
35
|
+
for (let i = 0; i < listeners.length; ++i) {
|
|
36
|
+
listeners[i](...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Removes specified `listener`, if connected.
|
|
41
|
+
* @param {function} listener
|
|
42
|
+
*/
|
|
43
|
+
removeListener(listener) {
|
|
44
|
+
const idx = this.p_listeners.indexOf(listener);
|
|
45
|
+
if (idx >= 0)
|
|
46
|
+
this.p_listeners.splice(idx, 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.default = Emitter;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implements a simple semaphore for async code logic.
|
|
3
|
+
*/
|
|
4
|
+
export default class Semaphore {
|
|
5
|
+
constructor(ready?: boolean);
|
|
6
|
+
get ready(): boolean;
|
|
7
|
+
setReady(ready: boolean): void;
|
|
8
|
+
/**
|
|
9
|
+
* Waits until the semaphore is ready, and marks it as non-ready (seizes it).
|
|
10
|
+
* @return {Promise}
|
|
11
|
+
*/
|
|
12
|
+
seize(): Promise<void>;
|
|
13
|
+
waitReady(seize?: boolean): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* If semaphore is ready, it releases the next barrier in the queue, if any,
|
|
16
|
+
* and reschedules itself for a call in the next event loop iteration.
|
|
17
|
+
* Otherwise, it breaks the queue draining loop, which will be restarted
|
|
18
|
+
* the next time the semaphore is set ready.
|
|
19
|
+
*/
|
|
20
|
+
p_drainQueue(): Promise<void>;
|
|
21
|
+
private p_draining;
|
|
22
|
+
private p_drainLock;
|
|
23
|
+
private p_queue;
|
|
24
|
+
private p_ready;
|
|
25
|
+
}
|
package/js/Semaphore.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const Barrier_1 = __importDefault(require("./Barrier"));
|
|
16
|
+
/**
|
|
17
|
+
* Implements a simple semaphore for async code logic.
|
|
18
|
+
*/
|
|
19
|
+
class Semaphore {
|
|
20
|
+
constructor(ready = false) {
|
|
21
|
+
// "true" when the drain queue process is running (and thus no need to start
|
|
22
|
+
// a new one).
|
|
23
|
+
this.p_draining = false;
|
|
24
|
+
// Each time a Promise from drain queue is resolved this drainLock is set
|
|
25
|
+
// to block further queue draining until the promise resolution handler
|
|
26
|
+
// (.seize() or .waitReady()) unlocks it, thus confirming it is fine
|
|
27
|
+
// to continue the draining. This is specifically important for .seize(),
|
|
28
|
+
// which should have a chance to switch semaphore state to non-ready prior
|
|
29
|
+
// to next Promise in the queue being unlocked.
|
|
30
|
+
this.p_drainLock = null;
|
|
31
|
+
// The array of barriers set for each async code flow awaiting for
|
|
32
|
+
// the Semaphore to become ready.
|
|
33
|
+
this.p_queue = [];
|
|
34
|
+
this.p_ready = !!ready;
|
|
35
|
+
}
|
|
36
|
+
get ready() { return this.p_ready; }
|
|
37
|
+
setReady(ready) {
|
|
38
|
+
const bool = !!ready;
|
|
39
|
+
if (this.p_ready !== bool) {
|
|
40
|
+
this.p_ready = bool;
|
|
41
|
+
if (bool && !this.p_draining && this.p_queue.length) {
|
|
42
|
+
this.p_drainQueue();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Waits until the semaphore is ready, and marks it as non-ready (seizes it).
|
|
48
|
+
* @return {Promise}
|
|
49
|
+
*/
|
|
50
|
+
seize() {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
return this.waitReady(true);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
waitReady(seize = false) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
if (!this.p_ready || this.p_queue.length) {
|
|
58
|
+
const barrier = new Barrier_1.default();
|
|
59
|
+
this.p_queue.push(barrier);
|
|
60
|
+
yield barrier;
|
|
61
|
+
if (seize)
|
|
62
|
+
this.p_ready = false;
|
|
63
|
+
this.p_drainLock.resolve();
|
|
64
|
+
}
|
|
65
|
+
else if (seize)
|
|
66
|
+
this.p_ready = false;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Private members below this point.
|
|
70
|
+
/**
|
|
71
|
+
* If semaphore is ready, it releases the next barrier in the queue, if any,
|
|
72
|
+
* and reschedules itself for a call in the next event loop iteration.
|
|
73
|
+
* Otherwise, it breaks the queue draining loop, which will be restarted
|
|
74
|
+
* the next time the semaphore is set ready.
|
|
75
|
+
*/
|
|
76
|
+
p_drainQueue() {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
this.p_draining = true;
|
|
79
|
+
while (this.p_ready && this.p_queue.length) {
|
|
80
|
+
this.p_drainLock = new Barrier_1.default();
|
|
81
|
+
this.p_queue[0].resolve();
|
|
82
|
+
yield this.p_drainLock; // eslint-disable-line no-await-in-loop
|
|
83
|
+
this.p_queue.shift();
|
|
84
|
+
}
|
|
85
|
+
this.p_draining = false;
|
|
86
|
+
this.p_drainLock = null;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.default = Semaphore;
|
package/js/index.d.ts
ADDED
package/js/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.Semaphore = exports.Emitter = exports.Barrier = void 0;
|
|
21
|
+
var Barrier_1 = require("./Barrier");
|
|
22
|
+
Object.defineProperty(exports, "Barrier", { enumerable: true, get: function () { return __importDefault(Barrier_1).default; } });
|
|
23
|
+
var Emitter_1 = require("./Emitter");
|
|
24
|
+
Object.defineProperty(exports, "Emitter", { enumerable: true, get: function () { return __importDefault(Emitter_1).default; } });
|
|
25
|
+
var Semaphore_1 = require("./Semaphore");
|
|
26
|
+
Object.defineProperty(exports, "Semaphore", { enumerable: true, get: function () { return __importDefault(Semaphore_1).default; } });
|
|
27
|
+
__exportStar(require("./time"), exports);
|
package/js/time.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const SEC_MS = 1000;
|
|
2
|
+
export declare const MIN_MS: number;
|
|
3
|
+
export declare const HOUR_MS: number;
|
|
4
|
+
export declare const DAY_MS: number;
|
|
5
|
+
export declare const YEAR_MS: number;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Promise, which resolves after the given timeout.
|
|
8
|
+
* @param {number} timeout Timeout [ms].
|
|
9
|
+
* @return {Barrier} Resolves after the timeout. It has additional
|
|
10
|
+
* .abort() method attached, which cancels the pending timer resolution
|
|
11
|
+
* (without resolving or rejecting the barrier).
|
|
12
|
+
*/
|
|
13
|
+
export declare function timer(timeout: number): Promise<void>;
|
package/js/time.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.timer = exports.YEAR_MS = exports.DAY_MS = exports.HOUR_MS = exports.MIN_MS = exports.SEC_MS = void 0;
|
|
16
|
+
const Barrier_1 = __importDefault(require("./Barrier"));
|
|
17
|
+
exports.SEC_MS = 1000;
|
|
18
|
+
exports.MIN_MS = 60 * exports.SEC_MS;
|
|
19
|
+
exports.HOUR_MS = 60 * exports.MIN_MS;
|
|
20
|
+
exports.DAY_MS = 24 * exports.HOUR_MS;
|
|
21
|
+
exports.YEAR_MS = 365 * exports.DAY_MS;
|
|
22
|
+
class Timer extends Barrier_1.default {
|
|
23
|
+
get abort() { return this.p_abort; }
|
|
24
|
+
constructor(executor, timeout = 0) {
|
|
25
|
+
super(executor);
|
|
26
|
+
if (timeout > 0) {
|
|
27
|
+
const id = setTimeout(super.resolve.bind(this), timeout);
|
|
28
|
+
this.p_abort = () => clearTimeout(id);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.p_abort = () => { };
|
|
32
|
+
super.resolve();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
then(onFulfilled, onRejected) {
|
|
36
|
+
const res = super.then(onFulfilled, onRejected);
|
|
37
|
+
res.p_abort = this.p_abort;
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a Promise, which resolves after the given timeout.
|
|
43
|
+
* @param {number} timeout Timeout [ms].
|
|
44
|
+
* @return {Barrier} Resolves after the timeout. It has additional
|
|
45
|
+
* .abort() method attached, which cancels the pending timer resolution
|
|
46
|
+
* (without resolving or rejecting the barrier).
|
|
47
|
+
*/
|
|
48
|
+
function timer(timeout) {
|
|
49
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
return new Timer(undefined, timeout);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
exports.timer = timer;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dr.pogodin/js-utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Collection of JavaScript (TypeScript) utilities.",
|
|
5
|
+
"main": "js/index",
|
|
6
|
+
"react-native": "ts/index",
|
|
7
|
+
"source": "ts/index",
|
|
8
|
+
"types": "js/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"jest": "jest --config jest.config.js",
|
|
12
|
+
"lint": "eslint . --ext .js,.ts",
|
|
13
|
+
"test": "npm run lint && npm run typecheck && npm run jest",
|
|
14
|
+
"typecheck": "tsc --noEmit && tsc --project __tests__/tsconfig.json"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/birdofpreyru/js-utils.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"javascript",
|
|
22
|
+
"util",
|
|
23
|
+
"utility"
|
|
24
|
+
],
|
|
25
|
+
"author": "Dr. Sergey Pogodin <doc@pogodin.studio> (https://dr.pogodin.studio)",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/birdofpreyru/js-utils/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/birdofpreyru/js-utils#readme",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@babel/core": "^7.21.4",
|
|
33
|
+
"@babel/preset-env": "^7.21.4",
|
|
34
|
+
"@babel/preset-typescript": "^7.21.4",
|
|
35
|
+
"@tsconfig/recommended": "^1.0.2",
|
|
36
|
+
"@types/jest": "^29.5.1",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
|
38
|
+
"@typescript-eslint/parser": "^5.59.1",
|
|
39
|
+
"babel-jest": "^29.5.0",
|
|
40
|
+
"eslint": "^8.39.0",
|
|
41
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
42
|
+
"eslint-config-airbnb-typescript": "^17.0.0",
|
|
43
|
+
"eslint-import-resolver-typescript": "^3.5.5",
|
|
44
|
+
"eslint-plugin-import": "^2.27.5",
|
|
45
|
+
"jest": "^29.5.0",
|
|
46
|
+
"typescript": "^5.0.4"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/ts/Barrier.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
type Resolver<T> = (value: T | PromiseLike<T>) => void;
|
|
2
|
+
type Rejecter = (reason?: any) => void;
|
|
3
|
+
export type Executor<T> = (resolve: Resolver<T>, reject: Rejecter) => void;
|
|
4
|
+
|
|
5
|
+
enum STATE {
|
|
6
|
+
PENDING = 'PENDING',
|
|
7
|
+
REJECTED = 'REJECTED',
|
|
8
|
+
RESOLVED = 'RESOLVED',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Barrier is just a Promise which has resolve and reject exposed as instance
|
|
13
|
+
* methods.
|
|
14
|
+
*
|
|
15
|
+
* Docs: https://dr.pogodin.studio/docs/react-utils/docs/api/classes/Barrier
|
|
16
|
+
*/
|
|
17
|
+
export default class Barrier<T, TR = T> extends Promise<TR> {
|
|
18
|
+
private p_resolve: Resolver<T> | Resolver<TR>;
|
|
19
|
+
|
|
20
|
+
private p_reject: Rejecter;
|
|
21
|
+
|
|
22
|
+
private p_state = STATE.PENDING;
|
|
23
|
+
|
|
24
|
+
constructor(executor?: Executor<TR>) {
|
|
25
|
+
let resolveRef: Resolver<TR>;
|
|
26
|
+
let rejectRef: Rejecter;
|
|
27
|
+
|
|
28
|
+
super((resolve, reject) => {
|
|
29
|
+
resolveRef = (value: TR | PromiseLike<TR>) => {
|
|
30
|
+
resolve(value);
|
|
31
|
+
this.p_state = STATE.RESOLVED;
|
|
32
|
+
};
|
|
33
|
+
rejectRef = (reason?: any) => {
|
|
34
|
+
reject(reason);
|
|
35
|
+
this.p_state = STATE.REJECTED;
|
|
36
|
+
};
|
|
37
|
+
if (executor) executor(resolveRef, rejectRef);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.p_resolve = resolveRef!;
|
|
41
|
+
this.p_reject = rejectRef!;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get resolve() { return <Resolver<T>> this.p_resolve; }
|
|
45
|
+
|
|
46
|
+
get reject() { return this.p_reject; }
|
|
47
|
+
|
|
48
|
+
get resolved() { return this.p_state === STATE.RESOLVED; }
|
|
49
|
+
|
|
50
|
+
get rejected() { return this.p_state === STATE.REJECTED; }
|
|
51
|
+
|
|
52
|
+
get settled() { return this.p_state !== STATE.PENDING; }
|
|
53
|
+
|
|
54
|
+
catch<TR1>(
|
|
55
|
+
onRejected?: ((reason: any) => TR1 | PromiseLike<TR1>) | null,
|
|
56
|
+
): Barrier<T, TR1> {
|
|
57
|
+
return <Barrier<T, TR1>> super.catch(onRejected);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
finally(onFinally?: (() => void) | null): Barrier<TR> {
|
|
61
|
+
return <Barrier<TR>> super.finally(onFinally);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
then<TR1, TR2>(
|
|
65
|
+
onFulfilled?: ((value: TR) => TR1 | PromiseLike<TR1>) | null,
|
|
66
|
+
onRejected?: ((reason: any) => TR2 | PromiseLike<TR2>) | null,
|
|
67
|
+
): Barrier<T, TR1 | TR2> {
|
|
68
|
+
const res = <Barrier<T, TR1 | TR2>> super.then(onFulfilled, onRejected);
|
|
69
|
+
res.p_resolve = this.resolve;
|
|
70
|
+
res.p_reject = this.reject;
|
|
71
|
+
return res;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/ts/Emitter.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type Listener = (...args: any[]) => void;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple listeneable data Emitter.
|
|
5
|
+
*/
|
|
6
|
+
export default class Emitter {
|
|
7
|
+
private p_listeners: Listener[] = [];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns "true" if any listener is connected; "false" otherwise.
|
|
11
|
+
* @return {boolean}
|
|
12
|
+
*/
|
|
13
|
+
get hasListeners(): boolean {
|
|
14
|
+
return !!this.p_listeners.length;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get listeners(): ReadonlyArray<Listener> { return this.p_listeners; }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Adds `listener` if it is not already connected.
|
|
21
|
+
* @param {function} listener
|
|
22
|
+
* @return {function} Unsubscribe function.
|
|
23
|
+
*/
|
|
24
|
+
addListener(listener: Listener): () => void {
|
|
25
|
+
if (!this.p_listeners.includes(listener)) {
|
|
26
|
+
this.p_listeners.push(listener);
|
|
27
|
+
}
|
|
28
|
+
return () => this.removeListener(listener);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calls every connected listener with the given arguments.
|
|
33
|
+
* @param {...any} args
|
|
34
|
+
*/
|
|
35
|
+
emit(...args: any[]) {
|
|
36
|
+
const { p_listeners: listeners } = this;
|
|
37
|
+
for (let i = 0; i < listeners.length; ++i) {
|
|
38
|
+
listeners[i](...args);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Removes specified `listener`, if connected.
|
|
44
|
+
* @param {function} listener
|
|
45
|
+
*/
|
|
46
|
+
removeListener(listener: Listener) {
|
|
47
|
+
const idx = this.p_listeners.indexOf(listener);
|
|
48
|
+
if (idx >= 0) this.p_listeners.splice(idx, 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/ts/Semaphore.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Barrier from './Barrier';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implements a simple semaphore for async code logic.
|
|
5
|
+
*/
|
|
6
|
+
export default class Semaphore {
|
|
7
|
+
constructor(ready = false) {
|
|
8
|
+
this.p_ready = !!ready;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get ready() { return this.p_ready; }
|
|
12
|
+
|
|
13
|
+
setReady(ready: boolean) {
|
|
14
|
+
const bool = !!ready;
|
|
15
|
+
if (this.p_ready !== bool) {
|
|
16
|
+
this.p_ready = bool;
|
|
17
|
+
if (bool && !this.p_draining && this.p_queue.length) {
|
|
18
|
+
this.p_drainQueue();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Waits until the semaphore is ready, and marks it as non-ready (seizes it).
|
|
25
|
+
* @return {Promise}
|
|
26
|
+
*/
|
|
27
|
+
async seize() {
|
|
28
|
+
return this.waitReady(true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async waitReady(seize = false) {
|
|
32
|
+
if (!this.p_ready || this.p_queue.length) {
|
|
33
|
+
const barrier = new Barrier<void>();
|
|
34
|
+
this.p_queue.push(barrier);
|
|
35
|
+
await barrier;
|
|
36
|
+
if (seize) this.p_ready = false;
|
|
37
|
+
this.p_drainLock!.resolve();
|
|
38
|
+
} else if (seize) this.p_ready = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Private members below this point.
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* If semaphore is ready, it releases the next barrier in the queue, if any,
|
|
45
|
+
* and reschedules itself for a call in the next event loop iteration.
|
|
46
|
+
* Otherwise, it breaks the queue draining loop, which will be restarted
|
|
47
|
+
* the next time the semaphore is set ready.
|
|
48
|
+
*/
|
|
49
|
+
async p_drainQueue() {
|
|
50
|
+
this.p_draining = true;
|
|
51
|
+
while (this.p_ready && this.p_queue.length) {
|
|
52
|
+
this.p_drainLock = new Barrier();
|
|
53
|
+
this.p_queue[0].resolve();
|
|
54
|
+
await this.p_drainLock; // eslint-disable-line no-await-in-loop
|
|
55
|
+
this.p_queue.shift();
|
|
56
|
+
}
|
|
57
|
+
this.p_draining = false;
|
|
58
|
+
this.p_drainLock = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// "true" when the drain queue process is running (and thus no need to start
|
|
62
|
+
// a new one).
|
|
63
|
+
private p_draining = false;
|
|
64
|
+
|
|
65
|
+
// Each time a Promise from drain queue is resolved this drainLock is set
|
|
66
|
+
// to block further queue draining until the promise resolution handler
|
|
67
|
+
// (.seize() or .waitReady()) unlocks it, thus confirming it is fine
|
|
68
|
+
// to continue the draining. This is specifically important for .seize(),
|
|
69
|
+
// which should have a chance to switch semaphore state to non-ready prior
|
|
70
|
+
// to next Promise in the queue being unlocked.
|
|
71
|
+
private p_drainLock: Barrier<void> | null = null;
|
|
72
|
+
|
|
73
|
+
// The array of barriers set for each async code flow awaiting for
|
|
74
|
+
// the Semaphore to become ready.
|
|
75
|
+
private p_queue: Barrier<void>[] = [];
|
|
76
|
+
|
|
77
|
+
private p_ready: boolean;
|
|
78
|
+
}
|
package/ts/index.ts
ADDED
package/ts/time.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import Barrier, { type Executor } from './Barrier';
|
|
2
|
+
|
|
3
|
+
export const SEC_MS = 1000;
|
|
4
|
+
export const MIN_MS = 60 * SEC_MS;
|
|
5
|
+
export const HOUR_MS = 60 * MIN_MS;
|
|
6
|
+
export const DAY_MS = 24 * HOUR_MS;
|
|
7
|
+
export const YEAR_MS = 365 * DAY_MS;
|
|
8
|
+
|
|
9
|
+
class Timer<T> extends Barrier<void, T> {
|
|
10
|
+
private p_abort: () => void;
|
|
11
|
+
|
|
12
|
+
get abort(): () => void { return this.p_abort; }
|
|
13
|
+
|
|
14
|
+
constructor(executor?: Executor<T>, timeout: number = 0) {
|
|
15
|
+
super(executor);
|
|
16
|
+
if (timeout > 0) {
|
|
17
|
+
const id = setTimeout(super.resolve.bind(this), timeout);
|
|
18
|
+
this.p_abort = () => clearTimeout(id);
|
|
19
|
+
} else {
|
|
20
|
+
this.p_abort = () => {};
|
|
21
|
+
super.resolve();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
then<TR1, TR2>(
|
|
26
|
+
onFulfilled?: ((value: T) => TR1 | PromiseLike<TR1>) | null,
|
|
27
|
+
onRejected?: ((reason: any) => TR2 | PromiseLike<TR2>) | null,
|
|
28
|
+
): Timer<TR1 | TR2> {
|
|
29
|
+
const res = <Timer<TR1 | TR2>> super.then(onFulfilled, onRejected);
|
|
30
|
+
res.p_abort = this.p_abort;
|
|
31
|
+
return res;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a Promise, which resolves after the given timeout.
|
|
37
|
+
* @param {number} timeout Timeout [ms].
|
|
38
|
+
* @return {Barrier} Resolves after the timeout. It has additional
|
|
39
|
+
* .abort() method attached, which cancels the pending timer resolution
|
|
40
|
+
* (without resolving or rejecting the barrier).
|
|
41
|
+
*/
|
|
42
|
+
export async function timer(timeout: number) {
|
|
43
|
+
return new Timer<void>(undefined, timeout);
|
|
44
|
+
}
|