@yarkivaev/source-to-sink 1.1.0 → 1.2.2

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.
@@ -9,4 +9,4 @@ jobs:
9
9
  uses: yarkivaev/npm-workflows/.github/workflows/ci.yml@main
10
10
  with:
11
11
  node-version: '22'
12
- lint: false
12
+ lint: true
@@ -0,0 +1,176 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+
4
+ export default [
5
+ js.configs.recommended,
6
+ {
7
+ languageOptions: {
8
+ ecmaVersion: 'latest',
9
+ sourceType: 'module',
10
+ globals: {
11
+ ...globals.node,
12
+ ...globals.mocha
13
+ }
14
+ },
15
+ rules: {
16
+ // Possible Problems
17
+ 'array-callback-return': 'error',
18
+ 'no-await-in-loop': 'error',
19
+ 'no-constant-binary-expression': 'error',
20
+ 'no-constructor-return': 'error',
21
+ 'no-duplicate-imports': 'error',
22
+ 'no-new-native-nonconstructor': 'error',
23
+ 'no-promise-executor-return': 'error',
24
+ 'no-self-compare': 'error',
25
+ 'no-template-curly-in-string': 'error',
26
+ 'no-unmodified-loop-condition': 'error',
27
+ 'no-unreachable-loop': 'error',
28
+ 'no-unused-private-class-members': 'error',
29
+ 'no-use-before-define': 'error',
30
+ 'require-atomic-updates': 'error',
31
+
32
+ // Suggestions
33
+ 'accessor-pairs': 'error',
34
+ 'arrow-body-style': ['error', 'always'],
35
+ 'block-scoped-var': 'error',
36
+ 'camelcase': 'error',
37
+ 'consistent-return': 'error',
38
+ 'consistent-this': ['error', 'self'],
39
+ 'curly': ['error', 'all'],
40
+ 'default-case': 'error',
41
+ 'default-case-last': 'error',
42
+ 'default-param-last': 'error',
43
+ 'dot-notation': 'error',
44
+ 'eqeqeq': ['error', 'always'],
45
+ 'func-name-matching': 'error',
46
+ 'func-names': ['error', 'always'],
47
+ 'func-style': ['error', 'declaration'],
48
+ 'grouped-accessor-pairs': ['error', 'getBeforeSet'],
49
+ 'guard-for-in': 'error',
50
+ 'id-denylist': 'off',
51
+ 'id-length': ['error', { 'min': 2, 'exceptions': ['i', 'j', 'k', 'x', 'y', 'z', 'a', 'b', 'p', 'w', 'e', 'r'] }],
52
+ 'max-classes-per-file': ['error', 1],
53
+ 'max-depth': ['error', 3],
54
+ 'max-lines': ['error', { 'max': 200, 'skipBlankLines': true, 'skipComments': true }],
55
+ 'max-lines-per-function': ['error', { 'max': 50, 'skipBlankLines': true, 'skipComments': true }],
56
+ 'max-nested-callbacks': ['error', 3],
57
+ 'max-params': ['error', 5],
58
+ 'max-statements': ['error', 15],
59
+ 'new-cap': 'error',
60
+ 'no-alert': 'error',
61
+ 'no-array-constructor': 'error',
62
+ 'no-bitwise': 'error',
63
+ 'no-caller': 'error',
64
+ 'no-console': 'error',
65
+ 'no-continue': 'error',
66
+ 'no-div-regex': 'error',
67
+ 'no-else-return': 'error',
68
+ 'no-empty-function': 'error',
69
+ 'no-eq-null': 'error',
70
+ 'no-eval': 'error',
71
+ 'no-extend-native': 'error',
72
+ 'no-extra-bind': 'error',
73
+ 'no-extra-label': 'error',
74
+ 'no-implicit-coercion': 'error',
75
+ 'no-implicit-globals': 'error',
76
+ 'no-implied-eval': 'error',
77
+ 'no-inline-comments': 'error',
78
+ 'no-invalid-this': 'error',
79
+ 'no-iterator': 'error',
80
+ 'no-label-var': 'error',
81
+ 'no-labels': 'error',
82
+ 'no-lone-blocks': 'error',
83
+ 'no-lonely-if': 'error',
84
+ 'no-loop-func': 'error',
85
+ 'no-magic-numbers': 'off',
86
+ 'no-multi-assign': 'error',
87
+ 'no-multi-str': 'error',
88
+ 'no-negated-condition': 'error',
89
+ 'no-nested-ternary': 'error',
90
+ 'no-new': 'error',
91
+ 'no-new-func': 'error',
92
+ 'no-new-wrappers': 'error',
93
+ 'no-object-constructor': 'error',
94
+ 'no-octal-escape': 'error',
95
+ 'no-param-reassign': 'error',
96
+ 'no-plusplus': 'error',
97
+ 'no-proto': 'error',
98
+ 'no-return-assign': 'error',
99
+ 'no-script-url': 'error',
100
+ 'no-sequences': 'error',
101
+ 'no-shadow': 'error',
102
+ 'no-throw-literal': 'error',
103
+ 'no-undef-init': 'error',
104
+ 'no-undefined': 'off',
105
+ 'no-underscore-dangle': 'error',
106
+ 'no-unneeded-ternary': 'error',
107
+ 'no-unused-expressions': 'error',
108
+ 'no-useless-call': 'error',
109
+ 'no-useless-computed-key': 'error',
110
+ 'no-useless-concat': 'error',
111
+ 'no-useless-constructor': 'error',
112
+ 'no-useless-rename': 'error',
113
+ 'no-useless-return': 'error',
114
+ 'no-var': 'error',
115
+ 'no-void': 'off',
116
+ 'no-warning-comments': 'warn',
117
+ 'object-shorthand': ['error', 'always'],
118
+ 'one-var': ['error', 'never'],
119
+ 'operator-assignment': ['error', 'always'],
120
+ 'prefer-arrow-callback': 'error',
121
+ 'prefer-const': 'error',
122
+ 'prefer-destructuring': ['error', { 'array': false, 'object': true }],
123
+ 'prefer-exponentiation-operator': 'error',
124
+ 'prefer-named-capture-group': 'error',
125
+ 'prefer-numeric-literals': 'error',
126
+ 'prefer-object-has-own': 'error',
127
+ 'prefer-object-spread': 'error',
128
+ 'prefer-promise-reject-errors': 'error',
129
+ 'prefer-regex-literals': 'error',
130
+ 'prefer-rest-params': 'error',
131
+ 'prefer-spread': 'error',
132
+ 'prefer-template': 'error',
133
+ 'radix': 'error',
134
+ 'require-await': 'error',
135
+ 'require-unicode-regexp': 'error',
136
+ 'sort-imports': ['error', { 'ignoreCase': true, 'ignoreDeclarationSort': true }],
137
+ 'sort-keys': 'off',
138
+ 'sort-vars': 'error',
139
+ 'strict': ['error', 'never'],
140
+ 'symbol-description': 'error',
141
+ 'vars-on-top': 'error',
142
+ 'yoda': 'error',
143
+
144
+ // Layout & Formatting
145
+ 'line-comment-position': ['error', { 'position': 'above' }],
146
+ 'unicode-bom': ['error', 'never']
147
+ }
148
+ },
149
+ {
150
+ files: ['src/modbusSource.js'],
151
+ rules: {
152
+ 'max-params': ['error', 8],
153
+ 'max-lines-per-function': ['error', { 'max': 60, 'skipBlankLines': true, 'skipComments': true }],
154
+ 'max-statements': ['error', 20]
155
+ }
156
+ },
157
+ {
158
+ files: ['test/**/*.js'],
159
+ rules: {
160
+ 'max-lines': 'off',
161
+ 'max-lines-per-function': 'off',
162
+ 'max-statements': 'off',
163
+ 'max-nested-callbacks': ['error', 5],
164
+ 'prefer-arrow-callback': 'off',
165
+ 'func-names': 'off',
166
+ 'id-denylist': 'off',
167
+ 'no-undefined': 'off',
168
+ 'no-empty-function': 'off',
169
+ 'func-style': 'off',
170
+ 'no-invalid-this': 'off',
171
+ 'no-await-in-loop': 'off',
172
+ 'camelcase': ['error', { 'properties': 'never' }],
173
+ 'id-length': ['error', { 'min': 2, 'exceptions': ['i', 'j', 'k', 'x', 'y', 'z', 'a', 'b', 'p', 'w', 'e', 'r', 'c', 't', 'v', '_'] }]
174
+ }
175
+ }
176
+ ];
package/index.js CHANGED
@@ -37,3 +37,4 @@ export { default as stompSource } from './src/stompSource.js';
37
37
  export { default as lokiSource } from './src/lokiSource.js';
38
38
  export { default as modbusSource } from './src/modbusSource.js';
39
39
  export { default as mqttSink } from './src/mqttSink.js';
40
+ export { default as sqliteSink } from './src/sqliteSink.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/source-to-sink",
3
- "version": "1.1.0",
3
+ "version": "1.2.2",
4
4
  "description": "Generic library for building data streaming pipelines",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,15 +13,21 @@
13
13
  "@clickhouse/client": "^1.0.0",
14
14
  "stompit": "^1.0.0",
15
15
  "pg": "^8.0.0",
16
- "modbus-serial": "^8.0.0"
16
+ "modbus-serial": "^8.0.0",
17
+ "better-sqlite3": "^11.0.0"
17
18
  },
18
19
  "devDependencies": {
19
- "mocha": "^10.0.0",
20
+ "@eslint/js": "^9.39.2",
20
21
  "c8": "^8.0.0",
22
+ "eslint": "^9.39.2",
23
+ "globals": "^17.0.0",
24
+ "mocha": "^10.0.0",
21
25
  "testcontainers": "^10.13.0"
22
26
  },
23
27
  "scripts": {
24
- "test": "mocha test/*.js",
25
- "coverage": "c8 mocha test/*.js"
28
+ "test": "mocha test/*.js --exit",
29
+ "coverage": "c8 mocha test/*.js",
30
+ "lint": "eslint .",
31
+ "lint:fix": "eslint . --fix"
26
32
  }
27
33
  }
package/src/batch.js CHANGED
@@ -30,7 +30,7 @@ export default function batch(sink, size, circuit) {
30
30
  throw new Error('Circuit must have an allowing() method');
31
31
  }
32
32
  let records = [];
33
- const perform = () => {
33
+ function perform() {
34
34
  if (records.length === 0) {
35
35
  return;
36
36
  }
@@ -46,7 +46,7 @@ export default function batch(sink, size, circuit) {
46
46
  circuit.fail();
47
47
  throw err;
48
48
  }
49
- };
49
+ }
50
50
  return {
51
51
  /**
52
52
  * Accepts a record into the batch.
package/src/lokiSource.js CHANGED
@@ -26,9 +26,9 @@ export default function lokiSource(url, query, interval, collector, clk) {
26
26
  if (typeof query !== 'string' || query.length === 0) {
27
27
  throw new Error('Query must be a non-empty string');
28
28
  }
29
- const fetch = async (since, until) => {
29
+ async function fetch(since, until) {
30
30
  const params = new URLSearchParams({
31
- query: query,
31
+ query,
32
32
  start: (since * 1000000).toString(),
33
33
  end: (until * 1000000).toString(),
34
34
  limit: '1000'
@@ -44,6 +44,6 @@ export default function lokiSource(url, query, interval, collector, clk) {
44
44
  }
45
45
  }
46
46
  return entries;
47
- };
47
+ }
48
48
  return pollingSource(fetch, interval, collector, clk);
49
49
  }
@@ -4,12 +4,35 @@ import pollingSource from './pollingSource.js';
4
4
  /**
5
5
  * Idle state for Modbus connection.
6
6
  *
7
- * @returns {object} State with connected() returning false
7
+ * @returns {object} State with connected() returning false and no retry handle
8
8
  */
9
9
  function idle() {
10
10
  return {
11
11
  connected() {
12
12
  return false;
13
+ },
14
+ retrying() {
15
+ return false;
16
+ }
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Retrying state for Modbus connection.
22
+ *
23
+ * @param {object} handle - Timer handle from setTimeout
24
+ * @returns {object} State with retrying() returning true
25
+ */
26
+ function retrying(handle) {
27
+ return {
28
+ connected() {
29
+ return false;
30
+ },
31
+ retrying() {
32
+ return true;
33
+ },
34
+ cancel() {
35
+ clearTimeout(handle);
13
36
  }
14
37
  };
15
38
  }
@@ -25,6 +48,9 @@ function connected(client) {
25
48
  connected() {
26
49
  return true;
27
50
  },
51
+ retrying() {
52
+ return false;
53
+ },
28
54
  disconnect() {
29
55
  client.close();
30
56
  }
@@ -51,9 +77,10 @@ function connected(client) {
51
77
  * @param {number} interval - Polling interval in seconds
52
78
  * @param {object} collector - Collector with accept() method
53
79
  * @param {object} clk - Clock with millis() method
80
+ * @param {object} [log] - Logger with error() method for connection failures
54
81
  * @returns {object} Source with start() and stop() methods
55
82
  */
56
- export default function modbusSource(host, port, address, count, interval, collector, clk) {
83
+ export default function modbusSource(host, port, address, count, interval, collector, clk, log = console) {
57
84
  if (typeof host !== 'string' || host.length === 0) {
58
85
  throw new Error('Host must be a non-empty string');
59
86
  }
@@ -68,28 +95,44 @@ export default function modbusSource(host, port, address, count, interval, colle
68
95
  }
69
96
  const client = new ModbusRTU();
70
97
  let state = idle();
71
- const fetch = async () => {
98
+ async function fetch() {
72
99
  const result = await client.readHoldingRegisters(address, count);
73
100
  return [result.data];
74
- };
101
+ }
102
+ const maxDelay = 300;
75
103
  const source = pollingSource(fetch, interval, collector, clk);
104
+ let delay = interval;
105
+ function attempt() {
106
+ client.connectTCP(host, { port }).then(() => {
107
+ delay = interval;
108
+ state = connected(client);
109
+ source.start();
110
+ }).catch((err) => {
111
+ log.error(`Connection to ${host}:${port} failed, retrying in ${delay}s: ${err.message}`);
112
+ const handle = setTimeout(attempt, delay * 1000);
113
+ state = retrying(handle);
114
+ delay = Math.min(delay * 2, maxDelay);
115
+ });
116
+ }
76
117
  return {
77
118
  /**
78
119
  * Connects to the Modbus device and starts polling.
79
120
  */
80
- async start() {
81
- if (state.connected()) {
121
+ start() {
122
+ if (state.connected() || state.retrying()) {
82
123
  return;
83
124
  }
84
- await client.connectTCP(host, { port });
85
- state = connected(client);
86
- source.start();
125
+ attempt();
87
126
  },
88
127
  /**
89
128
  * Stops polling and disconnects from the Modbus device.
90
129
  */
91
130
  stop() {
92
131
  source.stop();
132
+ if (state.retrying()) {
133
+ state.cancel();
134
+ state = idle();
135
+ }
93
136
  if (state.connected()) {
94
137
  state.disconnect();
95
138
  state = idle();
package/src/mqttSource.js CHANGED
@@ -65,7 +65,7 @@ export default function mqttSource(url, topics, collector, options = {}) {
65
65
  if (!collector || typeof collector.accept !== 'function') {
66
66
  throw new Error('Collector must have an accept() method');
67
67
  }
68
- const list = topics.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
68
+ const list = topics.split(',').map((tp) => {return tp.trim()}).filter((tp) => {return tp.length > 0});
69
69
  let state = idle();
70
70
  return {
71
71
  /**
@@ -77,15 +77,15 @@ export default function mqttSource(url, topics, collector, options = {}) {
77
77
  }
78
78
  const client = mqtt.connect(url, {
79
79
  clientId: options.clientId,
80
- clean: options.clientId ? false : true,
80
+ clean: !options.clientId,
81
81
  protocolVersion: 5,
82
82
  properties: options.clientId ? {
83
83
  sessionExpiryInterval: options.sessionExpiryInterval || 3600
84
84
  } : undefined
85
85
  });
86
- const handler = (t, message) => {
87
- collector.accept({ topic: t, payload: message.toString() });
88
- };
86
+ function handler(tp, message) {
87
+ collector.accept({ topic: tp, payload: message.toString() });
88
+ }
89
89
  client.on('message', handler);
90
90
  client.on('connect', () => {
91
91
  client.subscribe(list, { qos: options.clientId ? 1 : 0 });
@@ -64,14 +64,15 @@ export default function pollingSource(fetch, interval, collector, clk) {
64
64
  }
65
65
  let state = idle();
66
66
  let since = clk.millis();
67
- const poll = async () => {
67
+ async function poll() {
68
+ const from = since;
68
69
  const until = clk.millis();
69
- const result = await fetch(since, until);
70
70
  since = until;
71
+ const result = await fetch(from, until);
71
72
  for (const entry of result) {
72
73
  collector.accept(entry);
73
74
  }
74
- };
75
+ }
75
76
  return {
76
77
  /**
77
78
  * Starts polling.
@@ -45,10 +45,10 @@ import pg from 'pg';
45
45
  * @returns {Array} Deduplicated records
46
46
  */
47
47
  export function deduplicate(records, keys) {
48
- if (keys.length === 0) return records;
48
+ if (keys.length === 0) {return records;}
49
49
  const seen = new Map();
50
50
  for (const record of records) {
51
- const key = keys.map(k => record[k]).join('\0');
51
+ const key = keys.map(k => {return record[k]}).join('\0');
52
52
  seen.set(key, record);
53
53
  }
54
54
  return Array.from(seen.values());
@@ -61,10 +61,10 @@ export function deduplicate(records, keys) {
61
61
  * @returns {string} SQL suffix or empty string
62
62
  */
63
63
  function buildSuffix(options) {
64
- if (!Array.isArray(options.conflict) || options.conflict.length === 0) return '';
64
+ if (!Array.isArray(options.conflict) || options.conflict.length === 0) {return '';}
65
65
  const cols = options.conflict.join(', ');
66
66
  if (Array.isArray(options.update) && options.update.length > 0) {
67
- const sets = options.update.map(c => `${c} = EXCLUDED.${c}`).join(', ');
67
+ const sets = options.update.map((col) => {return `${col} = EXCLUDED.${col}`}).join(', ');
68
68
  return ` ON CONFLICT (${cols}) DO UPDATE SET ${sets}`;
69
69
  }
70
70
  return ` ON CONFLICT (${cols}) DO NOTHING`;
@@ -92,13 +92,13 @@ export default function postgresSink(url, table, columns, options = {}) {
92
92
  */
93
93
  write(records) {
94
94
  const unique = deduplicate(records, conflict);
95
- const placeholders = unique.map((_, i) => {
95
+ const placeholders = unique.map((record, i) => {
96
96
  const offset = i * columns.length;
97
- const row = columns.map((__, j) => `$${offset + j + 1}`).join(', ');
97
+ const row = columns.map((__, j) => {return `$${offset + j + 1}`}).join(', ');
98
98
  return `(${row})`;
99
99
  }).join(', ');
100
100
  const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${placeholders}${suffix}`;
101
- const values = unique.flatMap((record) => columns.map((col) => record[col]));
101
+ const values = unique.flatMap((record) => {return columns.map((col) => {return record[col]})});
102
102
  return pool.query(query, values);
103
103
  }
104
104
  };
@@ -0,0 +1,55 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ /**
4
+ * SQLite sink for batch record insertion.
5
+ *
6
+ * Opens a SQLite database in WAL mode and implements the Sink
7
+ * interface for use with batch collectors. Creates the target
8
+ * table with the given columns if it does not exist.
9
+ *
10
+ * @example
11
+ * const sink = sqliteSink('/tmp/metrics.db', 'metrics', ['topic', 'ts', 'value']);
12
+ * sink.write([{ topic: 'sensor/1', ts: 1700000000000, value: 42 }]);
13
+ *
14
+ * @param {string} path - Filesystem path to the SQLite database file
15
+ * @param {string} table - Target table name
16
+ * @param {Array<string>} columns - Column names for insertion
17
+ * @returns {object} Sink with write(records) and close() methods
18
+ */
19
+ export default function sqliteSink(path, table, columns) {
20
+ if (typeof path !== 'string' || path.length === 0) {
21
+ throw new Error('Path must be a non-empty string');
22
+ }
23
+ if (typeof table !== 'string' || table.length === 0) {
24
+ throw new Error('Table must be a non-empty string');
25
+ }
26
+ if (!Array.isArray(columns) || columns.length === 0) {
27
+ throw new Error('Columns must be a non-empty array');
28
+ }
29
+ const db = new Database(path);
30
+ db.pragma('journal_mode = WAL');
31
+ const placeholders = columns.map(() => {return '?'}).join(', ');
32
+ const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
33
+ const statement = db.prepare(sql);
34
+ const transaction = db.transaction((records) => {
35
+ for (const record of records) {
36
+ statement.run(...columns.map((col) => {return record[col]}));
37
+ }
38
+ });
39
+ return {
40
+ /**
41
+ * Writes records to SQLite table inside a transaction.
42
+ *
43
+ * @param {Array} records - Array of objects with keys matching columns
44
+ */
45
+ write(records) {
46
+ transaction(records);
47
+ },
48
+ /**
49
+ * Closes the database connection.
50
+ */
51
+ close() {
52
+ db.close();
53
+ }
54
+ };
55
+ }
@@ -54,6 +54,25 @@ function subscribed(channel) {
54
54
  * @param {string} [options.host] - Virtual host for STOMP connection
55
55
  * @returns {object} Source with start() and stop() methods
56
56
  */
57
+ /**
58
+ * Builds STOMP server configuration from a URL.
59
+ *
60
+ * @param {URL} parsed - Parsed URL object
61
+ * @param {object} options - Connection options
62
+ * @returns {object} Server config for ConnectFailover
63
+ */
64
+ function serverConfig(parsed, options) {
65
+ return {
66
+ host: parsed.hostname,
67
+ port: parseInt(parsed.port, 10) || 61613,
68
+ connectHeaders: {
69
+ host: options.host || parsed.hostname,
70
+ login: options.login || '',
71
+ passcode: options.passcode || ''
72
+ }
73
+ };
74
+ }
75
+
57
76
  export default function stompSource(url, destination, collector, options = {}) {
58
77
  if (typeof url !== 'string' || url.length === 0) {
59
78
  throw new Error('URL must be a non-empty string');
@@ -74,17 +93,7 @@ export default function stompSource(url, destination, collector, options = {}) {
74
93
  if (state.subscribed()) {
75
94
  return;
76
95
  }
77
- const headers = {
78
- host: options.host || parsed.hostname,
79
- login: options.login || '',
80
- passcode: options.passcode || ''
81
- };
82
- const server = {
83
- host: parsed.hostname,
84
- port: parseInt(parsed.port, 10) || 61613,
85
- connectHeaders: headers
86
- };
87
- const failover = new stompit.ConnectFailover([server], {
96
+ const failover = new stompit.ConnectFailover([serverConfig(parsed, options)], {
88
97
  initialReconnectDelay: 10,
89
98
  maxReconnectDelay: 30000,
90
99
  maxReconnects: -1
package/src/timedBatch.js CHANGED
@@ -63,7 +63,7 @@ export default function timedBatch(origin, interval) {
63
63
  throw new Error(`Interval must be a positive number, got: ${interval}`);
64
64
  }
65
65
  let timer = idle();
66
- const schedule = () => {
66
+ function schedule() {
67
67
  if (!timer.scheduled()) {
68
68
  const handle = setTimeout(() => {
69
69
  timer = idle();
@@ -71,11 +71,11 @@ export default function timedBatch(origin, interval) {
71
71
  }, interval * 1000);
72
72
  timer = scheduled(handle);
73
73
  }
74
- };
75
- const cancel = () => {
74
+ }
75
+ function cancel() {
76
76
  timer.cancel();
77
77
  timer = idle();
78
- };
78
+ }
79
79
  return {
80
80
  /**
81
81
  * Accepts a record and schedules flush timer.
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert';
2
- import { describe, it, before, after } from 'mocha';
2
+ import { after, before, describe, it } from 'mocha';
3
3
  import { GenericContainer } from 'testcontainers';
4
4
  import { createClient } from '@clickhouse/client';
5
5
  import clickhouseSink from '../src/clickhouseSink.js';
@@ -26,7 +26,7 @@ describe('clickhouseSink integration', function() {
26
26
  break;
27
27
  } catch {
28
28
  retries -= 1;
29
- await new Promise((resolve) => setTimeout(resolve, 1000));
29
+ await new Promise((resolve) => { setTimeout(resolve, 1000); });
30
30
  }
31
31
  }
32
32
  await client.command({ query: 'CREATE DATABASE IF NOT EXISTS test' });
@@ -55,7 +55,7 @@ describe('clickhouseSink integration', function() {
55
55
  const ts = Date.now();
56
56
  const sink = clickhouseSink(url, 'test.metrics');
57
57
  await sink.write([{ topic, ts, value }]);
58
- await new Promise((resolve) => setTimeout(resolve, 500));
58
+ await new Promise((resolve) => { setTimeout(resolve, 500); });
59
59
  const result = await client.query({
60
60
  query: `SELECT * FROM test.metrics WHERE topic = '${topic}'`,
61
61
  format: 'JSONEachRow'
@@ -73,7 +73,7 @@ describe('clickhouseSink integration', function() {
73
73
  ];
74
74
  const sink = clickhouseSink(url, 'test.metrics');
75
75
  await sink.write(records);
76
- await new Promise((resolve) => setTimeout(resolve, 500));
76
+ await new Promise((resolve) => { setTimeout(resolve, 500); });
77
77
  const result = await client.query({
78
78
  query: `SELECT * FROM test.metrics WHERE topic = '${topic}'`,
79
79
  format: 'JSONEachRow'
@@ -88,7 +88,7 @@ describe('clickhouseSink integration', function() {
88
88
  const ts = Date.now();
89
89
  const sink = clickhouseSink(url, 'test.metrics');
90
90
  await sink.write([{ topic, ts, value }]);
91
- await new Promise((resolve) => setTimeout(resolve, 500));
91
+ await new Promise((resolve) => { setTimeout(resolve, 500); });
92
92
  const result = await client.query({
93
93
  query: `SELECT * FROM test.metrics WHERE topic = '${topic}'`,
94
94
  format: 'JSONEachRow'