@api3/commons 0.10.0 → 0.11.0

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/README.md CHANGED
@@ -25,6 +25,8 @@ Read the documentation and sources of each module how to use it in the project.
25
25
  - [http](./src/http/README.md)
26
26
  - [logger](./src/logger/README.md)
27
27
  - [processing](./src/processing/README.md)
28
+ - [run-in-loop](./src/run-in-loop/README.md)
29
+ - [utils](./src/utils/README.md)
28
30
 
29
31
  ## Using the package in universal context
30
32
 
@@ -0,0 +1,44 @@
1
+ import { type Logger } from '../logger';
2
+ export interface RunInLoopOptions {
3
+ /** An API3 logger instance required to execute the callback with context. */
4
+ logger: Logger;
5
+ /** Part of every message logged by the logger. */
6
+ logLabel?: Lowercase<string>;
7
+ /**
8
+ * Minimum time to wait between executions. E.g. value 500 means that the next execution will be started only after
9
+ * 500ms from the start of the previous one (even if the previous one ended after 150ms). Default is 0.
10
+ */
11
+ frequencyMs?: number;
12
+ /**
13
+ * Minimum time to wait between executions. E.g. value 50 means that next execution will start 50ms after the previous
14
+ * one ended. Default is 0.
15
+ */
16
+ minWaitTimeMs?: number;
17
+ /**
18
+ * Maximum time to wait between executions. E.g. value 100 means that the next execution will be started at most after
19
+ * 100ms from the end of the previous one. The maxWaitTime has higher precedence than minWaitTime and frequencyMs.
20
+ */
21
+ maxWaitTimeMs?: number;
22
+ /**
23
+ * Maximum time to wait for the execution to finish. If the execution exceeds this time a warning is logged.
24
+ */
25
+ softTimeoutMs?: number;
26
+ /**
27
+ * Maximum time to wait for the callback execution to finish. If the execution exceeds this time an error is logged and the
28
+ * execution is force-stopped.
29
+ */
30
+ hardTimeoutMs?: number;
31
+ /**
32
+ * If false, the execution will not run. This is useful for temporarily disabling the execution (e.g. change
33
+ * environment variable and redeploy). Note that, there is no way to stop the loop execution once started. Default is
34
+ * true.
35
+ */
36
+ enabled?: boolean;
37
+ /**
38
+ * The initial delay to to wait before executing the callback for the first time. Default is 0, which means the
39
+ * callback is executed immediately.
40
+ */
41
+ initialDelayMs?: number;
42
+ }
43
+ export declare const runInLoop: (fn: () => Promise<void>, options: RunInLoopOptions) => Promise<never>;
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/run-in-loop/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,QAAQ,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,SAAS,OAAc,MAAM,OAAO,CAAC,IAAI,CAAC,WAAW,gBAAgB,mBAoDjF,CAAC"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runInLoop = void 0;
4
+ const promise_utils_1 = require("@api3/promise-utils");
5
+ const utils_1 = require("../utils");
6
+ const runInLoop = async (fn, options) => {
7
+ const { logger, logLabel, frequencyMs = 0, minWaitTimeMs = 0, maxWaitTimeMs, softTimeoutMs, hardTimeoutMs, enabled = true, initialDelayMs, } = options;
8
+ if (softTimeoutMs && hardTimeoutMs && hardTimeoutMs < softTimeoutMs) {
9
+ throw new Error('hardTimeoutMs must not be smaller than softTimeoutMs');
10
+ }
11
+ if (minWaitTimeMs && maxWaitTimeMs && maxWaitTimeMs < minWaitTimeMs) {
12
+ throw new Error('maxWaitTimeMs must not be smaller than minWaitTimeMs');
13
+ }
14
+ if (initialDelayMs)
15
+ await (0, utils_1.sleep)(initialDelayMs);
16
+ while (true) {
17
+ const executionStart = performance.now();
18
+ const executionId = (0, utils_1.generateRandomBytes32)();
19
+ if (enabled) {
20
+ const context = logLabel ? { executionId, label: logLabel } : { executionId };
21
+ await logger.runWithContext(context, async () => {
22
+ const goRes = await (0, promise_utils_1.go)(fn, hardTimeoutMs ? { totalTimeoutMs: hardTimeoutMs } : {}); // NOTE: This is a safety net to prevent the loop from hanging
23
+ if (!goRes.success) {
24
+ logger.error(`Unexpected runInLoop error`, goRes.error);
25
+ }
26
+ const executionTimeMs = performance.now() - executionStart;
27
+ if (executionTimeMs >= softTimeoutMs) {
28
+ logger.warn(`Execution took longer than the interval`, { executionTimeMs });
29
+ }
30
+ else {
31
+ logger.info(`Execution finished`, { executionTimeMs });
32
+ }
33
+ });
34
+ }
35
+ else {
36
+ // If the bot is disabled, we still want to run the loop to prevent the process from hanging. We also want to
37
+ // sleep according to the wait time logic.
38
+ logger.info('Loop execution is disabled.');
39
+ }
40
+ const remainingWaitTime = Math.max(frequencyMs - (performance.now() - executionStart), 0);
41
+ const waitTime = Math.max(minWaitTimeMs, remainingWaitTime);
42
+ const actualWaitTime = maxWaitTimeMs ? Math.min(waitTime, maxWaitTimeMs) : waitTime;
43
+ if (actualWaitTime > 0)
44
+ await (0, utils_1.sleep)(actualWaitTime);
45
+ }
46
+ };
47
+ exports.runInLoop = runInLoop;
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/run-in-loop/index.ts"],"names":[],"mappings":";;;AAAA,uDAAyC;AAGzC,oCAAwD;AA4CjD,MAAM,SAAS,GAAG,KAAK,EAAE,EAAuB,EAAE,OAAyB,EAAE,EAAE;IACpF,MAAM,EACJ,MAAM,EACN,QAAQ,EACR,WAAW,GAAG,CAAC,EACf,aAAa,GAAG,CAAC,EACjB,aAAa,EACb,aAAa,EACb,aAAa,EACb,OAAO,GAAG,IAAI,EACd,cAAc,GACf,GAAG,OAAO,CAAC;IAEZ,IAAI,aAAa,IAAI,aAAa,IAAI,aAAa,GAAG,aAAa,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,aAAa,IAAI,aAAa,IAAI,aAAa,GAAG,aAAa,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,cAAc;QAAE,MAAM,IAAA,aAAK,EAAC,cAAc,CAAC,CAAC;IAEhD,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QACzC,MAAM,WAAW,GAAG,IAAA,6BAAqB,GAAE,CAAC;QAE5C,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;YAC9E,MAAM,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBAC9C,MAAM,KAAK,GAAG,MAAM,IAAA,kBAAE,EAAC,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,8DAA8D;gBAClJ,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;oBACnB,MAAM,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC1D,CAAC;gBAED,MAAM,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC;gBAC3D,IAAI,eAAe,IAAI,aAAc,EAAE,CAAC;oBACtC,MAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;gBAC9E,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;gBACzD,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,6GAA6G;YAC7G,0CAA0C;YAC1C,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1F,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAC;QAC5D,MAAM,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACpF,IAAI,cAAc,GAAG,CAAC;YAAE,MAAM,IAAA,aAAK,EAAC,cAAc,CAAC,CAAC;IACtD,CAAC;AACH,CAAC,CAAC;AApDW,QAAA,SAAS,aAoDpB"}
@@ -1,3 +1,5 @@
1
1
  export * from './blockchain-utilities';
2
2
  export * from './http';
3
+ export * from './run-in-loop';
4
+ export * from './utils';
3
5
  //# sourceMappingURL=universal-index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"universal-index.d.ts","sourceRoot":"","sources":["../src/universal-index.ts"],"names":[],"mappings":"AACA,cAAc,wBAAwB,CAAC;AACvC,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"universal-index.d.ts","sourceRoot":"","sources":["../src/universal-index.ts"],"names":[],"mappings":"AACA,cAAc,wBAAwB,CAAC;AACvC,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC"}
@@ -17,4 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  // NOTE: Only export modules which work in both Node.js and browser environments.
18
18
  __exportStar(require("./blockchain-utilities"), exports);
19
19
  __exportStar(require("./http"), exports);
20
+ __exportStar(require("./run-in-loop"), exports);
21
+ __exportStar(require("./utils"), exports);
20
22
  //# sourceMappingURL=universal-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"universal-index.js","sourceRoot":"","sources":["../src/universal-index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iFAAiF;AACjF,yDAAuC;AACvC,yCAAuB"}
1
+ {"version":3,"file":"universal-index.js","sourceRoot":"","sources":["../src/universal-index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iFAAiF;AACjF,yDAAuC;AACvC,yCAAuB;AACvB,gDAA8B;AAC9B,0CAAwB"}
@@ -0,0 +1,3 @@
1
+ export declare const sleep: (ms: number) => Promise<unknown>;
2
+ export declare const generateRandomBytes32: () => string;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,KAAK,OAAc,MAAM,qBAAsD,CAAC;AAE7F,eAAO,MAAM,qBAAqB,cAEjC,CAAC"}
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRandomBytes32 = exports.sleep = void 0;
4
+ const ethers_1 = require("ethers");
5
+ const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6
+ exports.sleep = sleep;
7
+ const generateRandomBytes32 = () => {
8
+ return ethers_1.ethers.utils.hexlify(ethers_1.ethers.utils.randomBytes(32));
9
+ };
10
+ exports.generateRandomBytes32 = generateRandomBytes32;
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;AAAA,mCAAgC;AAEzB,MAAM,KAAK,GAAG,KAAK,EAAE,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAAhF,QAAA,KAAK,SAA2E;AAEtF,MAAM,qBAAqB,GAAG,GAAG,EAAE;IACxC,OAAO,eAAM,CAAC,KAAK,CAAC,OAAO,CAAC,eAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5D,CAAC,CAAC;AAFW,QAAA,qBAAqB,yBAEhC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api3/commons",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "keywords": [],
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -30,21 +30,21 @@
30
30
  "dotenv": "^16.4.5",
31
31
  "ethers": "^5.7.2",
32
32
  "lodash": "^4.17.21",
33
- "winston": "^3.13.0",
33
+ "winston": "^3.13.1",
34
34
  "winston-console-format": "^1.0.8",
35
35
  "zod": "^3.23.8"
36
36
  },
37
37
  "devDependencies": {
38
- "@api3/eslint-plugin-commons": "^1.0.1",
38
+ "@api3/eslint-plugin-commons": "^2.0.1",
39
39
  "@types/jest": "^29.5.12",
40
- "@types/lodash": "^4.17.5",
41
- "@types/node": "^20.14.8",
40
+ "@types/lodash": "^4.17.7",
41
+ "@types/node": "^20.14.13",
42
42
  "eslint": "^8.57.0",
43
- "husky": "^9.0.11",
43
+ "husky": "^9.1.3",
44
44
  "jest": "^29.7.0",
45
- "prettier": "^3.3.2",
46
- "ts-jest": "^29.1.5",
47
- "typescript": "^5.5.2"
45
+ "prettier": "^3.3.3",
46
+ "ts-jest": "^29.2.3",
47
+ "typescript": "^5.5.4"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc --project tsconfig.build.json",
@@ -0,0 +1,3 @@
1
+ # Run in loop
2
+
3
+ A helper function to call a callback at a given frequency indefinitely.
@@ -0,0 +1,100 @@
1
+ import { go } from '@api3/promise-utils';
2
+
3
+ import { type Logger } from '../logger';
4
+ import { generateRandomBytes32, sleep } from '../utils';
5
+
6
+ export interface RunInLoopOptions {
7
+ /** An API3 logger instance required to execute the callback with context. */
8
+ logger: Logger;
9
+ /** Part of every message logged by the logger. */
10
+ logLabel?: Lowercase<string>;
11
+ /**
12
+ * Minimum time to wait between executions. E.g. value 500 means that the next execution will be started only after
13
+ * 500ms from the start of the previous one (even if the previous one ended after 150ms). Default is 0.
14
+ */
15
+ frequencyMs?: number;
16
+ /**
17
+ * Minimum time to wait between executions. E.g. value 50 means that next execution will start 50ms after the previous
18
+ * one ended. Default is 0.
19
+ */
20
+ minWaitTimeMs?: number;
21
+ /**
22
+ * Maximum time to wait between executions. E.g. value 100 means that the next execution will be started at most after
23
+ * 100ms from the end of the previous one. The maxWaitTime has higher precedence than minWaitTime and frequencyMs.
24
+ */
25
+ maxWaitTimeMs?: number;
26
+ /**
27
+ * Maximum time to wait for the execution to finish. If the execution exceeds this time a warning is logged.
28
+ */
29
+ softTimeoutMs?: number;
30
+ /**
31
+ * Maximum time to wait for the callback execution to finish. If the execution exceeds this time an error is logged and the
32
+ * execution is force-stopped.
33
+ */
34
+ hardTimeoutMs?: number;
35
+ /**
36
+ * If false, the execution will not run. This is useful for temporarily disabling the execution (e.g. change
37
+ * environment variable and redeploy). Note that, there is no way to stop the loop execution once started. Default is
38
+ * true.
39
+ */
40
+ enabled?: boolean;
41
+ /**
42
+ * The initial delay to to wait before executing the callback for the first time. Default is 0, which means the
43
+ * callback is executed immediately.
44
+ */
45
+ initialDelayMs?: number;
46
+ }
47
+
48
+ export const runInLoop = async (fn: () => Promise<void>, options: RunInLoopOptions) => {
49
+ const {
50
+ logger,
51
+ logLabel,
52
+ frequencyMs = 0,
53
+ minWaitTimeMs = 0,
54
+ maxWaitTimeMs,
55
+ softTimeoutMs,
56
+ hardTimeoutMs,
57
+ enabled = true,
58
+ initialDelayMs,
59
+ } = options;
60
+
61
+ if (softTimeoutMs && hardTimeoutMs && hardTimeoutMs < softTimeoutMs) {
62
+ throw new Error('hardTimeoutMs must not be smaller than softTimeoutMs');
63
+ }
64
+ if (minWaitTimeMs && maxWaitTimeMs && maxWaitTimeMs < minWaitTimeMs) {
65
+ throw new Error('maxWaitTimeMs must not be smaller than minWaitTimeMs');
66
+ }
67
+
68
+ if (initialDelayMs) await sleep(initialDelayMs);
69
+
70
+ while (true) {
71
+ const executionStart = performance.now();
72
+ const executionId = generateRandomBytes32();
73
+
74
+ if (enabled) {
75
+ const context = logLabel ? { executionId, label: logLabel } : { executionId };
76
+ await logger.runWithContext(context, async () => {
77
+ const goRes = await go(fn, hardTimeoutMs ? { totalTimeoutMs: hardTimeoutMs } : {}); // NOTE: This is a safety net to prevent the loop from hanging
78
+ if (!goRes.success) {
79
+ logger.error(`Unexpected runInLoop error`, goRes.error);
80
+ }
81
+
82
+ const executionTimeMs = performance.now() - executionStart;
83
+ if (executionTimeMs >= softTimeoutMs!) {
84
+ logger.warn(`Execution took longer than the interval`, { executionTimeMs });
85
+ } else {
86
+ logger.info(`Execution finished`, { executionTimeMs });
87
+ }
88
+ });
89
+ } else {
90
+ // If the bot is disabled, we still want to run the loop to prevent the process from hanging. We also want to
91
+ // sleep according to the wait time logic.
92
+ logger.info('Loop execution is disabled.');
93
+ }
94
+
95
+ const remainingWaitTime = Math.max(frequencyMs - (performance.now() - executionStart), 0);
96
+ const waitTime = Math.max(minWaitTimeMs, remainingWaitTime);
97
+ const actualWaitTime = maxWaitTimeMs ? Math.min(waitTime, maxWaitTimeMs) : waitTime;
98
+ if (actualWaitTime > 0) await sleep(actualWaitTime);
99
+ }
100
+ };
@@ -1,3 +1,5 @@
1
1
  // NOTE: Only export modules which work in both Node.js and browser environments.
2
2
  export * from './blockchain-utilities';
3
3
  export * from './http';
4
+ export * from './run-in-loop';
5
+ export * from './utils';
@@ -0,0 +1,3 @@
1
+ # Utils
2
+
3
+ A set of helpers common for API3 applications.
@@ -0,0 +1,7 @@
1
+ import { ethers } from 'ethers';
2
+
3
+ export const sleep = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
4
+
5
+ export const generateRandomBytes32 = () => {
6
+ return ethers.utils.hexlify(ethers.utils.randomBytes(32));
7
+ };