@api3/commons 1.1.2 → 1.2.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/dist/run-in-loop/index.d.ts +19 -0
- package/dist/run-in-loop/index.d.ts.map +1 -1
- package/dist/run-in-loop/index.js +4 -2
- package/dist/run-in-loop/index.js.map +1 -1
- package/package.json +4 -3
- package/src/release-scripts/README.md +41 -13
- package/src/run-in-loop/index.test.ts +72 -13
- package/src/run-in-loop/index.ts +29 -1
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { type Logger } from '../logger';
|
|
2
|
+
export type RunInLoopExecutionIdOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Generate a random 32-byte execution ID for each iteration.
|
|
5
|
+
*/
|
|
6
|
+
type: 'random';
|
|
7
|
+
} | {
|
|
8
|
+
/**
|
|
9
|
+
* Generate execution IDs as incrementing numbers starting from 0.
|
|
10
|
+
*/
|
|
11
|
+
type: 'incremental';
|
|
12
|
+
/**
|
|
13
|
+
* Optional prefix prepended to the incrementing number (e.g. "my-prefix-0").
|
|
14
|
+
*/
|
|
15
|
+
prefix?: string;
|
|
16
|
+
};
|
|
2
17
|
export interface RunInLoopOptions {
|
|
3
18
|
/** An API3 logger instance required to execute the callback with context. */
|
|
4
19
|
logger: Logger;
|
|
@@ -40,6 +55,10 @@ export interface RunInLoopOptions {
|
|
|
40
55
|
* callback is executed immediately.
|
|
41
56
|
*/
|
|
42
57
|
initialDelayMs?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Configures how execution IDs are generated. Defaults to random IDs.
|
|
60
|
+
*/
|
|
61
|
+
executionIdOptions?: RunInLoopExecutionIdOptions;
|
|
43
62
|
}
|
|
44
63
|
export declare const runInLoop: (fn: () => Promise<{
|
|
45
64
|
shouldContinueRunning: boolean;
|
|
@@ -1 +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;;;;OAIG;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;
|
|
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,MAAM,2BAA2B,GACnC;IACE;;OAEG;IACH,IAAI,EAAE,QAAQ,CAAC;CAChB,GACD;IACE;;OAEG;IACH,IAAI,EAAE,aAAa,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,QAAQ,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7B;;;;OAIG;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;IAExB;;OAEG;IACH,kBAAkB,CAAC,EAAE,2BAA2B,CAAC;CAClD;AAKD,eAAO,MAAM,SAAS,GACpB,IAAI,MAAM,OAAO,CAAC;IAAE,qBAAqB,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,EAC5D,SAAS,gBAAgB,kBA+D1B,CAAC"}
|
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runInLoop = void 0;
|
|
4
4
|
const promise_utils_1 = require("@api3/promise-utils");
|
|
5
5
|
const utils_1 = require("../utils");
|
|
6
|
+
const getExecutionId = (iteration, options) => options.type === 'random' ? (0, utils_1.generateRandomBytes32)() : `${options.prefix ?? ''}${iteration}`;
|
|
6
7
|
const runInLoop = async (fn, options) => {
|
|
7
|
-
const { logger, logLabel, frequencyMs = 0, minWaitTimeMs = 0, maxWaitTimeMs, softTimeoutMs = frequencyMs, hardTimeoutMs, enabled = true, initialDelayMs, } = options;
|
|
8
|
+
const { logger, logLabel, frequencyMs = 0, minWaitTimeMs = 0, maxWaitTimeMs, softTimeoutMs = frequencyMs, hardTimeoutMs, enabled = true, initialDelayMs, executionIdOptions = { type: 'random' }, } = options;
|
|
8
9
|
if (hardTimeoutMs && hardTimeoutMs < softTimeoutMs) {
|
|
9
10
|
throw new Error('hardTimeoutMs must not be smaller than softTimeoutMs');
|
|
10
11
|
}
|
|
@@ -13,9 +14,10 @@ const runInLoop = async (fn, options) => {
|
|
|
13
14
|
}
|
|
14
15
|
if (initialDelayMs)
|
|
15
16
|
await (0, utils_1.sleep)(initialDelayMs);
|
|
17
|
+
let iteration = 0;
|
|
16
18
|
while (true) {
|
|
17
19
|
const executionStart = performance.now();
|
|
18
|
-
const executionId = (
|
|
20
|
+
const executionId = getExecutionId(iteration++, executionIdOptions);
|
|
19
21
|
if (enabled) {
|
|
20
22
|
const context = logLabel ? { executionId, label: logLabel } : { executionId };
|
|
21
23
|
const shouldContinueRunning = await logger.runWithContext(context, async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/run-in-loop/index.ts"],"names":[],"mappings":";;;AAAA,uDAAyC;AAGzC,oCAAwD;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/run-in-loop/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@api3/commons",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -30,16 +30,17 @@
|
|
|
30
30
|
"axios": "^1.13.2",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"ethers": "^5.8.0",
|
|
33
|
-
"lodash": "^4.17.
|
|
33
|
+
"lodash": "^4.17.23",
|
|
34
34
|
"winston": "^3.19.0",
|
|
35
35
|
"winston-console-format": "^1.0.8",
|
|
36
|
-
"zod": "^4.3.
|
|
36
|
+
"zod": "^4.3.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@api3/eslint-plugin-commons": "^3.0.0",
|
|
40
40
|
"@types/jest": "^30.0.0",
|
|
41
41
|
"@types/lodash": "^4.17.21",
|
|
42
42
|
"@types/node": "^24.10.1",
|
|
43
|
+
"eslint": "^8.57.1",
|
|
43
44
|
"husky": "^9.1.7",
|
|
44
45
|
"jest": "^30.2.0",
|
|
45
46
|
"prettier": "^3.7.4",
|
|
@@ -12,14 +12,13 @@ defined in `package.json`.
|
|
|
12
12
|
|
|
13
13
|
### Usage
|
|
14
14
|
|
|
15
|
-
It is recommended to
|
|
16
|
-
define a script in `package.json`, and then 3) call that script as part of the CI process.
|
|
15
|
+
It is recommended to:
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// GH_ACCESS_TOKEN - created through the Github UI with relevant permissions to the repo. See the tag-and-release source for more information
|
|
17
|
+
1. Create a script that imports and uses the `tagAndRelease` function as demonstrated below.
|
|
18
|
+
2. Define a `package.json` script that triggers the release.
|
|
19
|
+
3. Call that script as part of the CI process.
|
|
22
20
|
|
|
21
|
+
```ts
|
|
23
22
|
// scripts/tag-and-release.ts
|
|
24
23
|
import { join } from 'node:path';
|
|
25
24
|
|
|
@@ -48,34 +47,63 @@ main()
|
|
|
48
47
|
It's also recommended to setup a step in CI that checks if the Git tag already exists before executing.
|
|
49
48
|
|
|
50
49
|
```yml
|
|
51
|
-
|
|
50
|
+
########################################################################################
|
|
51
|
+
# The following secrets are required:
|
|
52
|
+
#
|
|
53
|
+
# 1. GH_ACCESS_TOKEN - A "fine-grained personal access token" generated through the
|
|
54
|
+
# Github UI. It seems like these tokens are scoped to a user, rather than an
|
|
55
|
+
# organisation.
|
|
56
|
+
#
|
|
57
|
+
# The following minimum permissions are required:
|
|
58
|
+
# Read - access to metadata
|
|
59
|
+
# Read & write - access to actions and code
|
|
60
|
+
# 2. GH_USER_NAME - The name (not username) associated with the Git user. e.g. John Smith
|
|
61
|
+
# 3. GH_USER_EMAIL - The email associated with the Git user
|
|
62
|
+
########################################################################################
|
|
52
63
|
tag-and-release:
|
|
53
|
-
|
|
64
|
+
name: Tag and release
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
needs: required-checks-passed
|
|
67
|
+
# Only tag and release on pushes to main
|
|
54
68
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
69
|
+
permissions:
|
|
70
|
+
id-token: write # Required for https://docs.npmjs.com/trusted-publishers
|
|
71
|
+
contents: write # Required for pushing tags and making the GitHub releases
|
|
55
72
|
steps:
|
|
56
73
|
- name: Clone repo
|
|
74
|
+
uses: actions/checkout@v6
|
|
75
|
+
with:
|
|
76
|
+
fetch-depth: 0
|
|
57
77
|
- name: Install pnpm
|
|
78
|
+
uses: pnpm/action-setup@v3
|
|
58
79
|
- name: Setup Node
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
uses: actions/setup-node@v6
|
|
81
|
+
with:
|
|
82
|
+
node-version: 24
|
|
83
|
+
registry-url: 'https://registry.npmjs.org'
|
|
84
|
+
cache: 'pnpm'
|
|
61
85
|
- name: Configure Git credentials
|
|
62
86
|
run: |
|
|
63
87
|
git config --global user.name '${{ secrets.GH_USER_NAME }}'
|
|
64
88
|
git config --global user.email '${{ secrets.GH_USER_EMAIL }}'
|
|
65
|
-
|
|
89
|
+
- name: Install Dependencies
|
|
90
|
+
run: pnpm install --frozen-lockfile
|
|
91
|
+
- name: Build
|
|
92
|
+
run: pnpm run build
|
|
66
93
|
- name: Get package.json version
|
|
67
94
|
id: get-version
|
|
68
95
|
run: echo "version=$(cat package.json | jq -r '.version' | sed 's/^/v/')" >> $GITHUB_OUTPUT
|
|
69
|
-
# Check if a Git tag already exists with the pattern: `v{version}`
|
|
70
96
|
- name: Validate tag
|
|
71
97
|
id: validate-tag
|
|
72
98
|
run:
|
|
73
99
|
test "$(git tag -l '${{ steps.get-version.outputs.version }}' | awk '{print $NF}')" = "${{
|
|
74
100
|
steps.get-version.outputs.version }}" || echo "new-tag=true" >> $GITHUB_OUTPUT
|
|
75
|
-
# Run the tag-and-release script only if the tag does *not* already exist
|
|
76
101
|
- name: Tag and release on Github
|
|
77
102
|
if: ${{ steps.validate-tag.outputs.new-tag }}
|
|
78
103
|
run: pnpm run release:tag
|
|
79
104
|
env:
|
|
80
105
|
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
106
|
+
- name: Publish to npm
|
|
107
|
+
if: ${{ steps.validate-tag.outputs.new-tag }}
|
|
108
|
+
run: pnpm publish --access public
|
|
81
109
|
```
|
|
@@ -1,23 +1,82 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Logger } from '../logger';
|
|
2
|
+
import * as utils from '../utils';
|
|
2
3
|
|
|
3
4
|
import { runInLoop } from './index';
|
|
4
5
|
|
|
6
|
+
const createMockLogger = (): Logger => ({
|
|
7
|
+
runWithContext: jest.fn((_: Record<string, any>, fn: () => any) => fn()) as Logger['runWithContext'],
|
|
8
|
+
debug: jest.fn(),
|
|
9
|
+
info: jest.fn(),
|
|
10
|
+
warn: jest.fn(),
|
|
11
|
+
error: jest.fn() as Logger['error'],
|
|
12
|
+
child: jest.fn() as Logger['child'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const createTestRunInLoopFunction = (executions: number) => {
|
|
16
|
+
let callCount = 0;
|
|
17
|
+
|
|
18
|
+
return jest.fn(async () => {
|
|
19
|
+
callCount += 1;
|
|
20
|
+
return { shouldContinueRunning: callCount < executions };
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const getRunContexts = (mockedLogger: Logger) => {
|
|
25
|
+
const runWithContextMock = jest.mocked(mockedLogger.runWithContext);
|
|
26
|
+
return runWithContextMock.mock.calls.map(([context]) => context);
|
|
27
|
+
};
|
|
28
|
+
|
|
5
29
|
describe(runInLoop.name, () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
enabled: true,
|
|
9
|
-
minLevel: 'info',
|
|
10
|
-
format: 'json',
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.restoreAllMocks();
|
|
11
32
|
});
|
|
12
33
|
|
|
13
34
|
it('stops the loop after getting the stop signal', async () => {
|
|
14
|
-
const
|
|
15
|
-
const fnSpy =
|
|
16
|
-
|
|
17
|
-
.mockImplementationOnce(async () => ({ shouldContinueRunning: true }))
|
|
18
|
-
.mockImplementationOnce(async () => ({ shouldContinueRunning: true }))
|
|
19
|
-
.mockImplementationOnce(async () => ({ shouldContinueRunning: false }));
|
|
20
|
-
await runInLoop(fnSpy as any, { logger });
|
|
35
|
+
const mockedLogger = createMockLogger();
|
|
36
|
+
const fnSpy = createTestRunInLoopFunction(3);
|
|
37
|
+
await runInLoop(fnSpy, { logger: mockedLogger });
|
|
21
38
|
expect(fnSpy).toHaveBeenCalledTimes(3);
|
|
22
39
|
});
|
|
40
|
+
|
|
41
|
+
it('uses random execution IDs by default', async () => {
|
|
42
|
+
const mockedLogger = createMockLogger();
|
|
43
|
+
const fnSpy = createTestRunInLoopFunction(1);
|
|
44
|
+
jest.spyOn(utils, 'generateRandomBytes32').mockReturnValue('0xrandom');
|
|
45
|
+
|
|
46
|
+
await runInLoop(fnSpy, { logger: mockedLogger });
|
|
47
|
+
|
|
48
|
+
expect(getRunContexts(mockedLogger)).toStrictEqual([{ executionId: '0xrandom' }]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('uses incremental execution IDs', async () => {
|
|
52
|
+
const mockedLogger = createMockLogger();
|
|
53
|
+
const fnSpy = createTestRunInLoopFunction(3);
|
|
54
|
+
|
|
55
|
+
await runInLoop(fnSpy, {
|
|
56
|
+
logger: mockedLogger,
|
|
57
|
+
executionIdOptions: { type: 'incremental' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(getRunContexts(mockedLogger)).toStrictEqual([
|
|
61
|
+
{ executionId: '0' },
|
|
62
|
+
{ executionId: '1' },
|
|
63
|
+
{ executionId: '2' },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('uses incremental execution IDs with prefix', async () => {
|
|
68
|
+
const mockedLogger = createMockLogger();
|
|
69
|
+
const fnSpy = createTestRunInLoopFunction(3);
|
|
70
|
+
|
|
71
|
+
await runInLoop(fnSpy, {
|
|
72
|
+
logger: mockedLogger,
|
|
73
|
+
executionIdOptions: { type: 'incremental', prefix: 'worker-' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(getRunContexts(mockedLogger)).toStrictEqual([
|
|
77
|
+
{ executionId: 'worker-0' },
|
|
78
|
+
{ executionId: 'worker-1' },
|
|
79
|
+
{ executionId: 'worker-2' },
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
23
82
|
});
|
package/src/run-in-loop/index.ts
CHANGED
|
@@ -3,6 +3,24 @@ import { go } from '@api3/promise-utils';
|
|
|
3
3
|
import { type Logger } from '../logger';
|
|
4
4
|
import { generateRandomBytes32, sleep } from '../utils';
|
|
5
5
|
|
|
6
|
+
export type RunInLoopExecutionIdOptions =
|
|
7
|
+
| {
|
|
8
|
+
/**
|
|
9
|
+
* Generate a random 32-byte execution ID for each iteration.
|
|
10
|
+
*/
|
|
11
|
+
type: 'random';
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
/**
|
|
15
|
+
* Generate execution IDs as incrementing numbers starting from 0.
|
|
16
|
+
*/
|
|
17
|
+
type: 'incremental';
|
|
18
|
+
/**
|
|
19
|
+
* Optional prefix prepended to the incrementing number (e.g. "my-prefix-0").
|
|
20
|
+
*/
|
|
21
|
+
prefix?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
6
24
|
export interface RunInLoopOptions {
|
|
7
25
|
/** An API3 logger instance required to execute the callback with context. */
|
|
8
26
|
logger: Logger;
|
|
@@ -44,8 +62,16 @@ export interface RunInLoopOptions {
|
|
|
44
62
|
* callback is executed immediately.
|
|
45
63
|
*/
|
|
46
64
|
initialDelayMs?: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Configures how execution IDs are generated. Defaults to random IDs.
|
|
68
|
+
*/
|
|
69
|
+
executionIdOptions?: RunInLoopExecutionIdOptions;
|
|
47
70
|
}
|
|
48
71
|
|
|
72
|
+
const getExecutionId = (iteration: number, options: RunInLoopExecutionIdOptions) =>
|
|
73
|
+
options.type === 'random' ? generateRandomBytes32() : `${options.prefix ?? ''}${iteration}`;
|
|
74
|
+
|
|
49
75
|
export const runInLoop = async (
|
|
50
76
|
fn: () => Promise<{ shouldContinueRunning: boolean } | void>,
|
|
51
77
|
options: RunInLoopOptions
|
|
@@ -60,6 +86,7 @@ export const runInLoop = async (
|
|
|
60
86
|
hardTimeoutMs,
|
|
61
87
|
enabled = true,
|
|
62
88
|
initialDelayMs,
|
|
89
|
+
executionIdOptions = { type: 'random' },
|
|
63
90
|
} = options;
|
|
64
91
|
|
|
65
92
|
if (hardTimeoutMs && hardTimeoutMs < softTimeoutMs) {
|
|
@@ -71,9 +98,10 @@ export const runInLoop = async (
|
|
|
71
98
|
|
|
72
99
|
if (initialDelayMs) await sleep(initialDelayMs);
|
|
73
100
|
|
|
101
|
+
let iteration = 0;
|
|
74
102
|
while (true) {
|
|
75
103
|
const executionStart = performance.now();
|
|
76
|
-
const executionId =
|
|
104
|
+
const executionId = getExecutionId(iteration++, executionIdOptions);
|
|
77
105
|
|
|
78
106
|
if (enabled) {
|
|
79
107
|
const context = logLabel ? { executionId, label: logLabel } : { executionId };
|