@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.
@@ -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;CACzB;AAED,eAAO,MAAM,SAAS,GACpB,IAAI,MAAM,OAAO,CAAC;IAAE,qBAAqB,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,EAC5D,SAAS,gBAAgB,kBA6D1B,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 = (0, utils_1.generateRandomBytes32)();
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":"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"}
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.1.2",
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.21",
33
+ "lodash": "^4.17.23",
34
34
  "winston": "^3.19.0",
35
35
  "winston-console-format": "^1.0.8",
36
- "zod": "^4.3.4"
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 1) create a script that imports and uses the `tagAndRelease` function as demonstrated below, 2)
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
- ```ts
19
- // The following environment variable is expected. See the script itself for more details
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
- # NOTE: irrelevant names and steps have been omitted such cloning, installing dependencies etc.
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
- # Only tag and release on pushes to main (or the release branch)
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
- - name: Install Dependencies
60
- # Configure the Git user
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
- # Get the version as defined in package.json
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 { createLogger } from '../logger';
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
- const logger = createLogger({
7
- colorize: true,
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 fn = async () => ({ shouldContinueRunning: false });
15
- const fnSpy = jest
16
- .spyOn({ fn }, 'fn')
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
  });
@@ -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 = generateRandomBytes32();
104
+ const executionId = getExecutionId(iteration++, executionIdOptions);
77
105
 
78
106
  if (enabled) {
79
107
  const context = logLabel ? { executionId, label: logLabel } : { executionId };