From a3c34485b26d52f1b40de79b611101e101149068 Mon Sep 17 00:00:00 2001 From: larryrider Date: Tue, 28 Jul 2026 17:18:26 +0200 Subject: [PATCH 1/5] feat: add environment metadata to logger defaultMeta --- src/utils/logger.utils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils/logger.utils.ts b/src/utils/logger.utils.ts index 47cc98b0..480a9830 100644 --- a/src/utils/logger.utils.ts +++ b/src/utils/logger.utils.ts @@ -1,13 +1,21 @@ +import os from 'os'; import winston from 'winston'; import { INTERNXT_CLI_LOGS_DIR } from '../constants/configs'; +import packageJson from '../../package.json'; const maxLogSize = 40 * 1024 * 1024; const maxLogsFiles = 5; +const environmentMeta = { + version: packageJson.version, + os: `${os.platform()} ${os.release()} (${os.arch()})`, + node: process.version, +}; + export const logger = winston.createLogger({ level: 'info', format: winston.format.combine(winston.format.timestamp(), winston.format.json()), - defaultMeta: { service: 'internxt-cli' }, + defaultMeta: { service: 'internxt-cli', ...environmentMeta }, transports: [ new winston.transports.File({ filename: 'internxt-cli-error.log', @@ -30,7 +38,7 @@ export const logger = winston.createLogger({ export const webdavLogger = winston.createLogger({ level: 'info', format: winston.format.combine(winston.format.timestamp(), winston.format.json()), - defaultMeta: { service: 'internxt-webdav' }, + defaultMeta: { service: 'internxt-webdav', ...environmentMeta }, transports: [ new winston.transports.File({ filename: 'internxt-webdav-error.log', From f304fd03aa18f68231117503d9bb54c604da264d Mon Sep 17 00:00:00 2001 From: larryrider Date: Tue, 28 Jul 2026 17:38:58 +0200 Subject: [PATCH 2/5] feat: implement concurrency mapping utility and update local filesystem scanning --- .../local-filesystem.service.ts | 7 +- .../local-filesystem.types.ts | 2 + src/utils/async.utils.ts | 30 +++++++ test/utils/async.utils.test.ts | 90 +++++++++++++++++++ 4 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 test/utils/async.utils.test.ts diff --git a/src/services/local-filesystem/local-filesystem.service.ts b/src/services/local-filesystem/local-filesystem.service.ts index b326f282..0d2b80e0 100644 --- a/src/services/local-filesystem/local-filesystem.service.ts +++ b/src/services/local-filesystem/local-filesystem.service.ts @@ -1,7 +1,8 @@ import { promises } from 'node:fs'; import { basename, dirname, join, relative, parse } from 'node:path'; -import { FileSystemNode, ScanResult } from './local-filesystem.types'; +import { FileSystemNode, MAX_CONCURRENT_SCANS, ScanResult } from './local-filesystem.types'; import { logger } from '../../utils/logger.utils'; +import { AsyncUtils } from '../../utils/async.utils'; export class LocalFilesystemService { static readonly instance = new LocalFilesystemService(); @@ -52,8 +53,8 @@ export class LocalFilesystemService { }); const entries = await promises.readdir(currentPath, { withFileTypes: true }); const validEntries = entries.filter((e) => !e.isSymbolicLink()); - const bytesArray = await Promise.all( - validEntries.map((e) => this.scanRecursive(join(currentPath, e.name), parentPath, folders, files)), + const bytesArray = await AsyncUtils.mapWithConcurrency(validEntries, MAX_CONCURRENT_SCANS, (e) => + this.scanRecursive(join(currentPath, e.name), parentPath, folders, files), ); return bytesArray.reduce((sum, bytes) => sum + bytes, 0); diff --git a/src/services/local-filesystem/local-filesystem.types.ts b/src/services/local-filesystem/local-filesystem.types.ts index 01c4da67..62d197cf 100644 --- a/src/services/local-filesystem/local-filesystem.types.ts +++ b/src/services/local-filesystem/local-filesystem.types.ts @@ -12,3 +12,5 @@ export interface ScanResult { totalItems: number; totalBytes: number; } + +export const MAX_CONCURRENT_SCANS = 100; diff --git a/src/utils/async.utils.ts b/src/utils/async.utils.ts index ec1f9bad..10ba5a0e 100644 --- a/src/utils/async.utils.ts +++ b/src/utils/async.utils.ts @@ -9,6 +9,36 @@ export class AsyncUtils { return new Promise((resolve) => setTimeout(resolve, ms)); }; + /** + * Maps an array with a bounded number of concurrent in-flight operations, + * preserving the original order of the results. + * + * @param {T[]} items - The items to map over. + * @param {number} limit - The maximum number of operations running at the same time. + * @param {(item: T, index: number) => Promise} mapper - The async operation to run for each item. + * @return {Promise} A promise that resolves to the mapped results, in the same order as `items`. + */ + static readonly mapWithConcurrency = async ( + items: T[], + limit: number, + mapper: (item: T, index: number) => Promise, + ): Promise => { + const results: R[] = new Array(items.length); + let nextIndex = 0; + + const worker = async () => { + while (nextIndex < items.length) { + const currentIndex = nextIndex++; + results[currentIndex] = await mapper(items[currentIndex], currentIndex); + } + }; + + const workers = Array.from({ length: Math.min(limit, items.length) }, worker); + await Promise.all(workers); + + return results; + }; + static readonly withTimeout = async ( promise: Promise, timeoutMs: number, diff --git a/test/utils/async.utils.test.ts b/test/utils/async.utils.test.ts new file mode 100644 index 00000000..98efe63d --- /dev/null +++ b/test/utils/async.utils.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, test } from 'vitest'; +import { AsyncUtils } from '../../src/utils/async.utils'; + +describe('Async utils', () => { + describe('mapWithConcurrency', () => { + test('when items are mapped, then results preserve the original order', async () => { + const items = [1, 2, 3, 4, 5]; + + const results = await AsyncUtils.mapWithConcurrency(items, 2, async (item) => { + await AsyncUtils.sleep(item % 2 === 0 ? 1 : 10); + return item * 2; + }); + + expect(results).toEqual([2, 4, 6, 8, 10]); + }); + + test('when the limit is lower than the amount of items, then no more than "limit" run at the same time', async () => { + const items = Array.from({ length: 10 }, (_, i) => i); + const limit = 3; + let inFlight = 0; + let maxInFlight = 0; + + await AsyncUtils.mapWithConcurrency(items, limit, async (item) => { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + await AsyncUtils.sleep(5); + inFlight--; + return item; + }); + + expect(maxInFlight).toBe(limit); + }); + + test('when the limit is higher than the amount of items, then all items run concurrently', async () => { + const items = Array.from({ length: 3 }, (_, i) => i); + let inFlight = 0; + let maxInFlight = 0; + + await AsyncUtils.mapWithConcurrency(items, 100, async (item) => { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + await AsyncUtils.sleep(5); + inFlight--; + return item; + }); + + expect(maxInFlight).toBe(items.length); + }); + + test('when the items array is empty, then it resolves to an empty array without calling the mapper', async () => { + let callCount = 0; + + const results = await AsyncUtils.mapWithConcurrency([], 5, async (item) => { + callCount++; + return item; + }); + + expect(results).toEqual([]); + expect(callCount).toBe(0); + }); + + test('when the mapper is called, then it receives the correct item and index', async () => { + const items = ['a', 'b', 'c']; + const calls: Array<[string, number]> = []; + + await AsyncUtils.mapWithConcurrency(items, 2, async (item, index) => { + calls.push([item, index]); + return item; + }); + + expect(calls.sort((a, b) => a[1] - b[1])).toEqual([ + ['a', 0], + ['b', 1], + ['c', 2], + ]); + }); + + test('when a mapper call rejects, then mapWithConcurrency rejects with the same error', async () => { + const items = [1, 2, 3]; + const error = new Error('mapper failed'); + + await expect( + AsyncUtils.mapWithConcurrency(items, 2, async (item) => { + if (item === 2) throw error; + return item; + }), + ).rejects.toThrow(error); + }); + }); +}); From 6101ea4a097c3d843c601f2cee7ac253e74df2ed Mon Sep 17 00:00:00 2001 From: larryrider Date: Tue, 28 Jul 2026 17:46:52 +0200 Subject: [PATCH 3/5] fix: update autocomplete plugin version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d08d1cab..964caf6b 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ EXAMPLES $ internxt autocomplete --refresh-cache ``` -_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.2.53/src/commands/autocomplete/index.ts)_ +_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.2.54/src/commands/autocomplete/index.ts)_ ## `internxt config` From b40398625ae360f0df21876e9d745c6ac4dcf907 Mon Sep 17 00:00:00 2001 From: larryrider Date: Wed, 29 Jul 2026 20:31:42 +0200 Subject: [PATCH 4/5] Revert "feat: implement concurrency mapping utility and update local filesystem scanning" This reverts commit f304fd03aa18f68231117503d9bb54c604da264d. --- .../local-filesystem.service.ts | 7 +- .../local-filesystem.types.ts | 2 - src/utils/async.utils.ts | 30 ------- test/utils/async.utils.test.ts | 90 ------------------- 4 files changed, 3 insertions(+), 126 deletions(-) delete mode 100644 test/utils/async.utils.test.ts diff --git a/src/services/local-filesystem/local-filesystem.service.ts b/src/services/local-filesystem/local-filesystem.service.ts index 0d2b80e0..b326f282 100644 --- a/src/services/local-filesystem/local-filesystem.service.ts +++ b/src/services/local-filesystem/local-filesystem.service.ts @@ -1,8 +1,7 @@ import { promises } from 'node:fs'; import { basename, dirname, join, relative, parse } from 'node:path'; -import { FileSystemNode, MAX_CONCURRENT_SCANS, ScanResult } from './local-filesystem.types'; +import { FileSystemNode, ScanResult } from './local-filesystem.types'; import { logger } from '../../utils/logger.utils'; -import { AsyncUtils } from '../../utils/async.utils'; export class LocalFilesystemService { static readonly instance = new LocalFilesystemService(); @@ -53,8 +52,8 @@ export class LocalFilesystemService { }); const entries = await promises.readdir(currentPath, { withFileTypes: true }); const validEntries = entries.filter((e) => !e.isSymbolicLink()); - const bytesArray = await AsyncUtils.mapWithConcurrency(validEntries, MAX_CONCURRENT_SCANS, (e) => - this.scanRecursive(join(currentPath, e.name), parentPath, folders, files), + const bytesArray = await Promise.all( + validEntries.map((e) => this.scanRecursive(join(currentPath, e.name), parentPath, folders, files)), ); return bytesArray.reduce((sum, bytes) => sum + bytes, 0); diff --git a/src/services/local-filesystem/local-filesystem.types.ts b/src/services/local-filesystem/local-filesystem.types.ts index 62d197cf..01c4da67 100644 --- a/src/services/local-filesystem/local-filesystem.types.ts +++ b/src/services/local-filesystem/local-filesystem.types.ts @@ -12,5 +12,3 @@ export interface ScanResult { totalItems: number; totalBytes: number; } - -export const MAX_CONCURRENT_SCANS = 100; diff --git a/src/utils/async.utils.ts b/src/utils/async.utils.ts index 10ba5a0e..ec1f9bad 100644 --- a/src/utils/async.utils.ts +++ b/src/utils/async.utils.ts @@ -9,36 +9,6 @@ export class AsyncUtils { return new Promise((resolve) => setTimeout(resolve, ms)); }; - /** - * Maps an array with a bounded number of concurrent in-flight operations, - * preserving the original order of the results. - * - * @param {T[]} items - The items to map over. - * @param {number} limit - The maximum number of operations running at the same time. - * @param {(item: T, index: number) => Promise} mapper - The async operation to run for each item. - * @return {Promise} A promise that resolves to the mapped results, in the same order as `items`. - */ - static readonly mapWithConcurrency = async ( - items: T[], - limit: number, - mapper: (item: T, index: number) => Promise, - ): Promise => { - const results: R[] = new Array(items.length); - let nextIndex = 0; - - const worker = async () => { - while (nextIndex < items.length) { - const currentIndex = nextIndex++; - results[currentIndex] = await mapper(items[currentIndex], currentIndex); - } - }; - - const workers = Array.from({ length: Math.min(limit, items.length) }, worker); - await Promise.all(workers); - - return results; - }; - static readonly withTimeout = async ( promise: Promise, timeoutMs: number, diff --git a/test/utils/async.utils.test.ts b/test/utils/async.utils.test.ts deleted file mode 100644 index 98efe63d..00000000 --- a/test/utils/async.utils.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { AsyncUtils } from '../../src/utils/async.utils'; - -describe('Async utils', () => { - describe('mapWithConcurrency', () => { - test('when items are mapped, then results preserve the original order', async () => { - const items = [1, 2, 3, 4, 5]; - - const results = await AsyncUtils.mapWithConcurrency(items, 2, async (item) => { - await AsyncUtils.sleep(item % 2 === 0 ? 1 : 10); - return item * 2; - }); - - expect(results).toEqual([2, 4, 6, 8, 10]); - }); - - test('when the limit is lower than the amount of items, then no more than "limit" run at the same time', async () => { - const items = Array.from({ length: 10 }, (_, i) => i); - const limit = 3; - let inFlight = 0; - let maxInFlight = 0; - - await AsyncUtils.mapWithConcurrency(items, limit, async (item) => { - inFlight++; - maxInFlight = Math.max(maxInFlight, inFlight); - await AsyncUtils.sleep(5); - inFlight--; - return item; - }); - - expect(maxInFlight).toBe(limit); - }); - - test('when the limit is higher than the amount of items, then all items run concurrently', async () => { - const items = Array.from({ length: 3 }, (_, i) => i); - let inFlight = 0; - let maxInFlight = 0; - - await AsyncUtils.mapWithConcurrency(items, 100, async (item) => { - inFlight++; - maxInFlight = Math.max(maxInFlight, inFlight); - await AsyncUtils.sleep(5); - inFlight--; - return item; - }); - - expect(maxInFlight).toBe(items.length); - }); - - test('when the items array is empty, then it resolves to an empty array without calling the mapper', async () => { - let callCount = 0; - - const results = await AsyncUtils.mapWithConcurrency([], 5, async (item) => { - callCount++; - return item; - }); - - expect(results).toEqual([]); - expect(callCount).toBe(0); - }); - - test('when the mapper is called, then it receives the correct item and index', async () => { - const items = ['a', 'b', 'c']; - const calls: Array<[string, number]> = []; - - await AsyncUtils.mapWithConcurrency(items, 2, async (item, index) => { - calls.push([item, index]); - return item; - }); - - expect(calls.sort((a, b) => a[1] - b[1])).toEqual([ - ['a', 0], - ['b', 1], - ['c', 2], - ]); - }); - - test('when a mapper call rejects, then mapWithConcurrency rejects with the same error', async () => { - const items = [1, 2, 3]; - const error = new Error('mapper failed'); - - await expect( - AsyncUtils.mapWithConcurrency(items, 2, async (item) => { - if (item === 2) throw error; - return item; - }), - ).rejects.toThrow(error); - }); - }); -}); From 554f7851a1f771065fe5f6f933f5d5a82c95bc96 Mon Sep 17 00:00:00 2001 From: larryrider Date: Wed, 29 Jul 2026 20:37:37 +0200 Subject: [PATCH 5/5] fix: update import statement for 'os' module to use 'node:os' --- src/utils/logger.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/logger.utils.ts b/src/utils/logger.utils.ts index 480a9830..12647c46 100644 --- a/src/utils/logger.utils.ts +++ b/src/utils/logger.utils.ts @@ -1,4 +1,4 @@ -import os from 'os'; +import os from 'node:os'; import winston from 'winston'; import { INTERNXT_CLI_LOGS_DIR } from '../constants/configs'; import packageJson from '../../package.json';