@iobroker/db-base 4.0.0-alpha.8-20210909-001a711c → 4.0.3

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2014-2020 bluefox<dogafox@gmail.com>,
3
+ Copyright (c) 2014-2022 bluefox<dogafox@gmail.com>,
4
4
  Copyright (c) 2014 hobbyquaker
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
package/README.md CHANGED
@@ -4,5 +4,5 @@ This Library contains base classes that are used by the database classes for ioB
4
4
  ## License
5
5
  The MIT License (MIT)
6
6
 
7
- Copyright (c) 2014-2020 bluefox <dogafox@gmail.com>,
7
+ Copyright (c) 2014-2022 bluefox <dogafox@gmail.com>,
8
8
  Copyright (c) 2014 hobbyquaker
@@ -0,0 +1,6 @@
1
+ const Resp = require('respjs');
2
+
3
+ module.exports = {
4
+ QUEUED_STR_BUF: Resp.encodeString('QUEUED'),
5
+ OK_STR_BUF: Resp.encodeString('OK')
6
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * States DB in memory - Server
3
3
  *
4
- * Copyright 2013-2021 bluefox <dogafox@gmail.com>
4
+ * Copyright 2013-2022 bluefox <dogafox@gmail.com>
5
5
  *
6
6
  * MIT License
7
7
  *
@@ -14,9 +14,9 @@
14
14
  /* jslint node: true */
15
15
  'use strict';
16
16
 
17
- const fs = require('fs-extra');
18
- const path = require('path');
19
- const tools = require('./tools.js');
17
+ const fs = require('fs-extra');
18
+ const path = require('path');
19
+ const tools = require('./tools.js');
20
20
 
21
21
  // settings = {
22
22
  // change: function (id, state) {},
@@ -44,7 +44,6 @@ const tools = require('./tools.js');
44
44
  * and general subscription and publish functionality
45
45
  **/
46
46
  class InMemoryFileDB {
47
-
48
47
  constructor(settings) {
49
48
  this.settings = settings || {};
50
49
 
@@ -57,15 +56,15 @@ class InMemoryFileDB {
57
56
  this.zlib = null;
58
57
  this.callbackSubscriptionClient = {};
59
58
 
60
- this.settings.backup = this.settings.backup || {
61
- disabled: false, // deactivates
62
- files: 24, // minimum number of files
63
- hours: 48, // hours
64
- period: 120, // minutes
65
- path: '' // use default path
59
+ this.settings.backup = this.settings.connection.backup || {
60
+ disabled: false, // deactivates
61
+ files: 24, // minimum number of files
62
+ hours: 48, // hours
63
+ period: 120, // minutes
64
+ path: '' // use default path
66
65
  };
67
66
 
68
- this.dataDir = (this.settings.connection.dataDir || tools.getDefaultDataDir());
67
+ this.dataDir = this.settings.connection.dataDir || tools.getDefaultDataDir();
69
68
  if (!path.isAbsolute(this.dataDir)) {
70
69
  this.dataDir = path.normalize(path.join(tools.getControllerDir(), this.dataDir));
71
70
  }
@@ -77,7 +76,7 @@ class InMemoryFileDB {
77
76
 
78
77
  this.stateTimer = null;
79
78
 
80
- this.backupDir = this.settings.backup.path || (path.join(this.dataDir, this.settings.fileDB.backupDirName));
79
+ this.backupDir = this.settings.backup.path || path.join(this.dataDir, this.settings.fileDB.backupDirName);
81
80
 
82
81
  if (!this.settings.backup.disabled) {
83
82
  this.initBackupDir();
@@ -117,32 +116,55 @@ class InMemoryFileDB {
117
116
  let ret = {};
118
117
  try {
119
118
  ret = await this.loadDatasetFile(datasetName);
119
+
120
+ // loading worked, make sure that "bak" File is not broken
121
+ try {
122
+ await fs.readJSON(`${datasetName}.bak`);
123
+ } catch (e) {
124
+ this.log.info(
125
+ `${this.namespace} Rewrite bak file, because error on verify ${datasetName}.bak: ${e.message}`
126
+ );
127
+ try {
128
+ const jsonString = JSON.stringify(ret);
129
+ await fs.writeFile(`${datasetName}.bak`, jsonString);
130
+ } catch (e) {
131
+ this.log.error(`${this.namespace} Cannot save ${datasetName}.bak: ${e.message}`);
132
+ }
133
+ }
120
134
  } catch (err) {
121
135
  this.log.error(`${this.namespace} Cannot load ${datasetName}: ${err.message}. We try last Backup!`);
122
136
 
123
137
  try {
124
- ret = await this.loadDatasetFile(datasetName + '.bak');
138
+ ret = await this.loadDatasetFile(`${datasetName}.bak`);
125
139
 
126
140
  // it worked, lets overwrite old file and store the broken one for pot. forensic check
127
141
  try {
128
142
  if (await fs.pathExists(datasetName)) {
129
143
  try {
130
- await fs.move(datasetName, `${datasetName}.broken`, {overwrite: true});
144
+ await fs.move(datasetName, `${datasetName}.broken`, { overwrite: true });
131
145
  } catch (e) {
132
- this.log.error(`${this.namespace} Cannot copy the broken file ${datasetName} to ${datasetName}.broken ${e.message}`);
146
+ this.log.error(
147
+ `${this.namespace} Cannot copy the broken file ${datasetName} to ${datasetName}.broken ${e.message}`
148
+ );
133
149
  }
134
150
  try {
135
151
  await fs.writeFile(datasetName, JSON.stringify(ret));
136
152
  } catch (e) {
137
- this.log.error(`${this.namespace} Cannot restore backup file as new main ${datasetName}: ${e.message}`);
153
+ this.log.error(
154
+ `${this.namespace} Cannot restore backup file as new main ${datasetName}: ${e.message}`
155
+ );
138
156
  }
139
157
  }
140
158
  } catch {
141
159
  // ignore, file does not exist
142
160
  }
143
161
  } catch (err) {
144
- this.log.error(`${this.namespace} Cannot load ${datasetName}.bak: ${err.message}. Continue with empty dataset!`);
145
- this.log.error(`${this.namespace} If this is no Migration or initial start please restore the last backup from ${this.backupDir}`);
162
+ this.log.error(
163
+ `${this.namespace} Cannot load ${datasetName}.bak: ${err.message}. Continue with empty dataset!`
164
+ );
165
+ this.log.error(
166
+ `${this.namespace} If this is no Migration or initial start please restore the last backup from ${this.backupDir}`
167
+ );
146
168
  }
147
169
  }
148
170
  return ret;
@@ -151,18 +173,21 @@ class InMemoryFileDB {
151
173
  initBackupDir() {
152
174
  this.zlib = this.zlib || require('zlib');
153
175
  // Interval in minutes => to milliseconds
154
- this.settings.backup.period = this.settings.backup.period === undefined ? 120 : parseInt(this.settings.backup.period);
176
+ this.settings.backup.period =
177
+ this.settings.backup.period === undefined ? 120 : parseInt(this.settings.backup.period);
155
178
  if (isNaN(this.settings.backup.period)) {
156
179
  this.settings.backup.period = 120;
157
180
  }
158
181
  this.settings.backup.period *= 60000;
159
182
 
160
- this.settings.backup.files = this.settings.backup.files === undefined ? 24 : parseInt(this.settings.backup.files);
183
+ this.settings.backup.files =
184
+ this.settings.backup.files === undefined ? 24 : parseInt(this.settings.backup.files);
161
185
  if (isNaN(this.settings.backup.files)) {
162
186
  this.settings.backup.files = 24;
163
187
  }
164
188
 
165
- this.settings.backup.hours = this.settings.backup.hours === undefined ? 48 : parseInt(this.settings.backup.hours);
189
+ this.settings.backup.hours =
190
+ this.settings.backup.hours === undefined ? 48 : parseInt(this.settings.backup.hours);
166
191
  if (isNaN(this.settings.backup.hours)) {
167
192
  this.settings.backup.hours = 48;
168
193
  }
@@ -186,11 +211,11 @@ class InMemoryFileDB {
186
211
  return;
187
212
  }
188
213
 
189
- s.push({pattern: pattern, regex: new RegExp(tools.pattern2RegEx(pattern)), options: options});
214
+ s.push({ pattern: pattern, regex: new RegExp(tools.pattern2RegEx(pattern)), options: options });
190
215
  });
191
216
  } else {
192
217
  if (!s.find(sub => sub.pattern === pattern)) {
193
- s.push({pattern: pattern, regex: new RegExp(tools.pattern2RegEx(pattern)), options: options});
218
+ s.push({ pattern: pattern, regex: new RegExp(tools.pattern2RegEx(pattern)), options: options });
194
219
  }
195
220
  }
196
221
  typeof cb === 'function' && cb();
@@ -226,13 +251,13 @@ class InMemoryFileDB {
226
251
  throw new Error('no communication handling implemented');
227
252
  }
228
253
 
229
- deleteOldBackupFiles() {
254
+ deleteOldBackupFiles(baseFilename) {
230
255
  // delete files only if settings.backupNumber is not 0
231
256
  let files = fs.readdirSync(this.backupDir);
232
257
  files.sort();
233
258
  const limit = Date.now() - this.settings.backup.hours * 3600000;
234
259
 
235
- files = files.filter(f => f.endsWith(this.settings.fileDB.fileName + '.gz'));
260
+ files = files.filter(f => f.endsWith(baseFilename + '.gz'));
236
261
 
237
262
  while (files.length > this.settings.backup.files) {
238
263
  const file = files.shift();
@@ -245,7 +270,9 @@ class InMemoryFileDB {
245
270
  try {
246
271
  fs.unlinkSync(path.join(this.backupDir, file));
247
272
  } catch (e) {
248
- this.log.error(`${this.namespace} Cannot delete file "${path.join(this.backupDir, file)}: ${e.message}`);
273
+ this.log.error(
274
+ `${this.namespace} Cannot delete file "${path.join(this.backupDir, file)}: ${e.message}`
275
+ );
249
276
  }
250
277
  }
251
278
  }
@@ -319,7 +346,7 @@ class InMemoryFileDB {
319
346
  try {
320
347
  if (await fs.pathExists(this.datasetName)) {
321
348
  try {
322
- await fs.move(this.datasetName, `${this.datasetName}.bak`, {overwrite: true});
349
+ await fs.move(this.datasetName, `${this.datasetName}.bak`, { overwrite: true });
323
350
  } catch (e) {
324
351
  bakOk = false;
325
352
  this.log.error(`${this.namespace} Cannot backup file ${this.datasetName}.bak: ${e.message}`);
@@ -333,19 +360,21 @@ class InMemoryFileDB {
333
360
  }
334
361
 
335
362
  try {
336
- await fs.move(`${this.datasetName}.new`, this.datasetName, {overwrite: true});
363
+ await fs.move(`${this.datasetName}.new`, this.datasetName, { overwrite: true });
337
364
  } catch (e) {
338
- this.log.error(`${this.namespace} Cannot move ${this.datasetName}.new to ${this.datasetName}: ${e.message}. Try direct write as fallback`);
365
+ this.log.error(
366
+ `${this.namespace} Cannot move ${this.datasetName}.new to ${this.datasetName}: ${e.message}. Try direct write as fallback`
367
+ );
339
368
  try {
340
369
  await fs.writeFile(this.datasetName, jsonString);
341
370
  } catch (e) {
342
371
  this.log.error(`${this.namespace} Cannot directly write Dataset to ${this.datasetName}: ${e.message}`);
343
372
  return jsonString;
344
373
  }
345
-
346
374
  }
347
375
 
348
- if (!bakOk) { // it seems the bak File is not successfully there, write current content again
376
+ if (!bakOk) {
377
+ // it seems the bak File is not successfully there, write current content again
349
378
  try {
350
379
  await fs.writeFile(`${this.datasetName}.bak`, jsonString);
351
380
  } catch (e) {
@@ -368,7 +397,10 @@ class InMemoryFileDB {
368
397
  // makes backups only if settings.backupInterval is not 0
369
398
  if (this.settings.backup.period && (!this.lastSave || now - this.lastSave > this.settings.backup.period)) {
370
399
  this.lastSave = now;
371
- const backFileName = path.join(this.backupDir, this.getTimeStr(now) + '_' + this.settings.fileDB.fileName + '.gz');
400
+ const backFileName = path.join(
401
+ this.backupDir,
402
+ this.getTimeStr(now) + '_' + this.settings.fileDB.fileName + '.gz'
403
+ );
372
404
 
373
405
  try {
374
406
  if (!fs.existsSync(backFileName)) {
@@ -386,7 +418,7 @@ class InMemoryFileDB {
386
418
  compress.end();
387
419
 
388
420
  // analyse older files
389
- this.deleteOldBackupFiles();
421
+ this.deleteOldBackupFiles(this.settings.fileDB.fileName);
390
422
  }
391
423
  } catch (e) {
392
424
  this.log.error(`${this.namespace} Cannot save backup ${backFileName}: ${e.message}`);
@@ -395,7 +427,7 @@ class InMemoryFileDB {
395
427
  }
396
428
 
397
429
  getStatus() {
398
- return {type: 'file', server: true};
430
+ return { type: 'file', server: true };
399
431
  }
400
432
 
401
433
  /** @returns {object} */
@@ -419,11 +451,14 @@ class InMemoryFileDB {
419
451
  }
420
452
 
421
453
  // local subscriptions
422
- if (this.change && this.callbackSubscriptionClient._subscribe && this.callbackSubscriptionClient._subscribe[type]) {
454
+ if (
455
+ this.change &&
456
+ this.callbackSubscriptionClient._subscribe &&
457
+ this.callbackSubscriptionClient._subscribe[type]
458
+ ) {
423
459
  for (let j = 0; j < this.callbackSubscriptionClient._subscribe[type].length; j++) {
424
460
  if (this.callbackSubscriptionClient._subscribe[type][j].regex.test(id)) {
425
- setImmediate(() =>
426
- this.change(id, obj));
461
+ setImmediate(() => this.change(id, obj));
427
462
  break;
428
463
  }
429
464
  }
@@ -1,5 +1,6 @@
1
1
  const Resp = require('respjs');
2
2
  const { EventEmitter } = require('events');
3
+ const { QUEUED_STR_BUF, OK_STR_BUF } = require('./constants');
3
4
 
4
5
  /**
5
6
  * Class to handle a redis connection and provide events to react on for
@@ -23,10 +24,11 @@ class RedisHandler extends EventEmitter {
23
24
  }
24
25
  this.socket = socket;
25
26
 
26
- this.socketId = this.logScope + socket.remoteAddress + ':' + socket.remotePort;
27
+ this.socketId = `${this.logScope + socket.remoteAddress}:${socket.remotePort}`;
27
28
  this.initialized = false;
28
29
  this.stop = false;
29
30
 
31
+ this.activeMultiCalls = [];
30
32
  this.writeQueue = [];
31
33
 
32
34
  this.handleBuffers = false;
@@ -40,7 +42,7 @@ class RedisHandler extends EventEmitter {
40
42
  this.resp.on('error', err => {
41
43
  this.log.error(`${this.socketId} (Init=${this.initialized}) Redis error:${err}`);
42
44
  if (this.initialized) {
43
- this.sendError(null,new Error('PARSER ERROR ' + err)); // TODO
45
+ this.sendError(null, new Error(`PARSER ERROR ${err}`)); // TODO
44
46
  } else {
45
47
  this.close();
46
48
  }
@@ -50,7 +52,16 @@ class RedisHandler extends EventEmitter {
50
52
 
51
53
  socket.on('data', data => {
52
54
  if (this.options.enhancedLogging) {
53
- this.log.silly(`${this.socketId} New Redis request: ${(data.length > 1024) ? data.toString().replace(/[\r\n]+/g, '').substring(0, 100) + ' -- ' + data.length + ' bytes' : data.toString().replace(/[\r\n]+/g, '')}`);
55
+ this.log.silly(
56
+ `${this.socketId} New Redis request: ${
57
+ data.length > 1024
58
+ ? `${data
59
+ .toString()
60
+ .replace(/[\r\n]+/g, '')
61
+ .substring(0, 100)} -- ${data.length} bytes`
62
+ : data.toString().replace(/[\r\n]+/g, '')
63
+ }`
64
+ );
54
65
  }
55
66
  this.resp.write(data);
56
67
  });
@@ -87,19 +98,53 @@ class RedisHandler extends EventEmitter {
87
98
  }
88
99
  }
89
100
  }
90
- if (command === 'info') {
91
- this.initialized = true;
92
- }
101
+
93
102
  const t = process.hrtime();
94
- const responseId = (t[0] * 1e3) + (t[1] / 1e6);
103
+ const responseId = t[0] * 1e3 + t[1] / 1e6;
104
+
95
105
  if (this.options.enhancedLogging) {
96
- this.log.silly(`${this.socketId} Parser result: id=${responseId}, command=${command}, data=${(JSON.stringify(data).length > 1024) ? JSON.stringify(data).substring(0, 100) + ' -- ' + JSON.stringify(data).length + ' bytes' : JSON.stringify(data)}`);
106
+ this.log.silly(
107
+ `${this.socketId} Parser result: id=${responseId}, command=${command}, data=${
108
+ JSON.stringify(data).length > 1024
109
+ ? `${JSON.stringify(data).substring(0, 100)} -- ${JSON.stringify(data).length} bytes`
110
+ : JSON.stringify(data)
111
+ }`
112
+ );
113
+ }
114
+
115
+ if (command === 'multi') {
116
+ if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled) {
117
+ // should never happen
118
+ this.log.warn(`${this.socketId} Conflicting multi call`);
119
+ }
120
+ this._handleMulti();
121
+ return;
122
+ }
123
+
124
+ // multi active and exec not called yet
125
+ if (this.activeMultiCalls.length && !this.activeMultiCalls[0].execCalled && command !== 'exec') {
126
+ // store all response ids so we know which need to be in the multi call
127
+ this.activeMultiCalls[0].responseIds.push(responseId);
128
+ // add it for the correct order will be overwritten with correct response
129
+ this.activeMultiCalls[0].responseMap.set(responseId, null);
130
+ } else {
131
+ // multi response ids should not be pushed - we will answer combined
132
+ this.writeQueue.push({ id: responseId, data: false });
97
133
  }
98
- this.writeQueue.push({id: responseId, data: false});
134
+
135
+ if (command === 'exec') {
136
+ this._handleExec(responseId);
137
+ return;
138
+ }
139
+
140
+ if (command === 'info') {
141
+ this.initialized = true;
142
+ }
143
+
99
144
  if (this.listenerCount(command) !== 0) {
100
145
  setImmediate(() => this.emit(command, data, responseId));
101
146
  } else {
102
- this.sendError(responseId, new Error(command + ' NOT SUPPORTED'));
147
+ this.sendError(responseId, new Error(`${command} NOT SUPPORTED`));
103
148
  }
104
149
  }
105
150
 
@@ -112,6 +157,7 @@ class RedisHandler extends EventEmitter {
112
157
  */
113
158
  _sendQueued(responseId, data) {
114
159
  let idx = 0;
160
+
115
161
  while (this.writeQueue.length && idx < this.writeQueue.length) {
116
162
  // we found the queue entry that matches with the responseId, so store the data so be sent out
117
163
  if (this.writeQueue[idx].id === responseId) {
@@ -127,8 +173,15 @@ class RedisHandler extends EventEmitter {
127
173
  if (idx === 0 && this.writeQueue[idx].data !== false) {
128
174
  const response = this.writeQueue.shift();
129
175
  if (this.options.enhancedLogging) {
130
- this.log.silly(`${this.socketId} Redis response (${response.id}): ${(response.data.length > 1024) ? data.length + ' bytes' : response.data.toString().replace(/[\r\n]+/g, '')}`);
176
+ this.log.silly(
177
+ `${this.socketId} Redis response (${response.id}): ${
178
+ response.data.length > 1024
179
+ ? `${data.length} bytes`
180
+ : response.data.toString().replace(/[\r\n]+/g, '')
181
+ }`
182
+ );
131
183
  }
184
+
132
185
  this._write(response.data);
133
186
  // We sended out first queue entry but no further response is ready
134
187
  // and we do not need to check the whole queue, so we are done here
@@ -175,8 +228,9 @@ class RedisHandler extends EventEmitter {
175
228
  }
176
229
  if (!data) {
177
230
  this.log.warn(`${this.socketId} Not able to write ${JSON.stringify(data)}`);
178
- data = Resp.encodeError(new Error('INVALID RESPONSE: ' + JSON.stringify(data)));
231
+ data = Resp.encodeError(new Error(`INVALID RESPONSE: ${JSON.stringify(data)}`));
179
232
  }
233
+
180
234
  setImmediate(() => this._sendQueued(responseId, data));
181
235
  }
182
236
 
@@ -202,6 +256,13 @@ class RedisHandler extends EventEmitter {
202
256
  * @param responseId ID of the response
203
257
  */
204
258
  sendNull(responseId) {
259
+ for (const i in this.activeMultiCalls) {
260
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
261
+ this._handleMultiResponse(responseId, i, Resp.encodeNull());
262
+ return;
263
+ }
264
+ }
265
+
205
266
  this.sendResponse(responseId, Resp.encodeNull());
206
267
  }
207
268
 
@@ -210,6 +271,13 @@ class RedisHandler extends EventEmitter {
210
271
  * @param responseId ID of the response
211
272
  */
212
273
  sendNullArray(responseId) {
274
+ for (const i in this.activeMultiCalls) {
275
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
276
+ this._handleMultiResponse(responseId, i, Resp.encodeNullArray());
277
+ return;
278
+ }
279
+ }
280
+
213
281
  this.sendResponse(responseId, Resp.encodeNullArray());
214
282
  }
215
283
 
@@ -219,6 +287,13 @@ class RedisHandler extends EventEmitter {
219
287
  * @param str String to encode
220
288
  */
221
289
  sendString(responseId, str) {
290
+ for (const i in this.activeMultiCalls) {
291
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
292
+ this._handleMultiResponse(responseId, i, Resp.encodeString(str));
293
+ return;
294
+ }
295
+ }
296
+
222
297
  this.sendResponse(responseId, Resp.encodeString(str));
223
298
  }
224
299
 
@@ -229,6 +304,14 @@ class RedisHandler extends EventEmitter {
229
304
  */
230
305
  sendError(responseId, error) {
231
306
  this.log.warn(`${this.socketId} Error from InMemDB: ${error}`);
307
+
308
+ for (const i in this.activeMultiCalls) {
309
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
310
+ this._handleMultiResponse(responseId, i, Resp.encodeError(error));
311
+ return;
312
+ }
313
+ }
314
+
232
315
  this.sendResponse(responseId, Resp.encodeError(error));
233
316
  }
234
317
 
@@ -238,6 +321,13 @@ class RedisHandler extends EventEmitter {
238
321
  * @param num Integer to send out
239
322
  */
240
323
  sendInteger(responseId, num) {
324
+ for (const i in this.activeMultiCalls) {
325
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
326
+ this._handleMultiResponse(responseId, i, Resp.encodeInteger(num));
327
+ return;
328
+ }
329
+ }
330
+
241
331
  this.sendResponse(responseId, Resp.encodeInteger(num));
242
332
  }
243
333
 
@@ -247,6 +337,13 @@ class RedisHandler extends EventEmitter {
247
337
  * @param str String to send out
248
338
  */
249
339
  sendBulk(responseId, str) {
340
+ for (const i in this.activeMultiCalls) {
341
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
342
+ this._handleMultiResponse(responseId, i, Resp.encodeBulk(str));
343
+ return;
344
+ }
345
+ }
346
+
250
347
  this.sendResponse(responseId, Resp.encodeBulk(str));
251
348
  }
252
349
 
@@ -256,6 +353,13 @@ class RedisHandler extends EventEmitter {
256
353
  * @param buf Buffer to send out
257
354
  */
258
355
  sendBufBulk(responseId, buf) {
356
+ for (const i in this.activeMultiCalls) {
357
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
358
+ this._handleMultiResponse(responseId, i, Resp.encodeBufBulk(buf));
359
+ return;
360
+ }
361
+ }
362
+
259
363
  this.sendResponse(responseId, Resp.encodeBufBulk(buf));
260
364
  }
261
365
 
@@ -283,12 +387,83 @@ class RedisHandler extends EventEmitter {
283
387
 
284
388
  /**
285
389
  * Encode a array values to buffers and send out
286
- * @param responseId ID of the response
287
- * @param arr Array to send out
390
+ * @param {number} responseId ID of the response
391
+ * @param {any[]} arr Array to send out
288
392
  */
289
393
  sendArray(responseId, arr) {
394
+ for (const i in this.activeMultiCalls) {
395
+ if (this.activeMultiCalls[i].responseIds.includes(responseId)) {
396
+ this._handleMultiResponse(responseId, i, Resp.encodeArray(this.encodeRespArray(arr)));
397
+ return;
398
+ }
399
+ }
400
+
290
401
  this.sendResponse(responseId, Resp.encodeArray(this.encodeRespArray(arr)));
291
402
  }
403
+
404
+ /**
405
+ * Handles a 'multi' command
406
+ *
407
+ * @private
408
+ */
409
+ _handleMulti() {
410
+ this.activeMultiCalls.unshift({
411
+ responseIds: [],
412
+ execCalled: false,
413
+ responseCount: 0,
414
+ responseMap: new Map()
415
+ });
416
+ }
417
+
418
+ /**
419
+ * Handles an 'exec' command
420
+ *
421
+ * @param {number} responseId ID of the response
422
+ * @private
423
+ */
424
+ _handleExec(responseId) {
425
+ this.activeMultiCalls[0].execId = responseId;
426
+ this.activeMultiCalls[0].execCalled = true;
427
+
428
+ // maybe we have all fullfilled yet
429
+ if (this.activeMultiCalls[0].responseCount === this.activeMultiCalls[0].responseIds.length) {
430
+ const multiRespObj = this.activeMultiCalls.shift();
431
+ this._sendExecResponse(multiRespObj);
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Builds up the exec response and sends it
437
+ * @param {Record<string, any>} multiObj the multi object to send out
438
+ *
439
+ * @private
440
+ */
441
+ _sendExecResponse(multiObj) {
442
+ // collect all 'QUEUED' answers
443
+ const queuedStrArr = new Array(multiObj.responseCount).fill(QUEUED_STR_BUF);
444
+
445
+ this._sendQueued(
446
+ multiObj.execId,
447
+ Buffer.concat([OK_STR_BUF, ...queuedStrArr, Resp.encodeArray(Array.from(multiObj.responseMap.values()))])
448
+ );
449
+ }
450
+
451
+ /**
452
+ * Handles a multi response
453
+ *
454
+ * @param {number} responseId ID of the response
455
+ * @param {number} index index of the multi call
456
+ * @param {Buffer} buf buffer to include in response
457
+ * @private
458
+ */
459
+ _handleMultiResponse(responseId, index, buf) {
460
+ this.activeMultiCalls[index].responseMap.set(responseId, buf);
461
+ this.activeMultiCalls[index].responseCount++;
462
+ if (this.activeMultiCalls[index].responseCount === this.activeMultiCalls[index].responseIds.length) {
463
+ const multiRespObj = this.activeMultiCalls.splice(index, 1)[0];
464
+ this._sendExecResponse(multiRespObj);
465
+ }
466
+ }
292
467
  }
293
468
 
294
469
  module.exports = RedisHandler;
package/lib/tools.js CHANGED
@@ -18,4 +18,4 @@ module.exports.maybeCallbackWithRedisError = (callback, error, ...args) => {
18
18
  error.message = module.exports.ERRORS.ERROR_DB_CLOSED;
19
19
  }
20
20
  return module.exports.maybeCallbackWithError(callback, error, ...args);
21
- };
21
+ };
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@iobroker/db-base",
3
- "version": "4.0.0-alpha.8-20210909-001a711c",
3
+ "version": "4.0.3",
4
4
  "engines": {
5
5
  "node": ">=12.0.0"
6
6
  },
7
7
  "dependencies": {
8
- "@iobroker/js-controller-common": "4.0.0-alpha.8-20210909-001a711c",
8
+ "@iobroker/js-controller-common": "4.0.3",
9
9
  "deep-clone": "^3.0.3",
10
10
  "fs-extra": "^10.0.0",
11
- "node.extend": "^2.0.2",
12
11
  "respjs": "^4.2.0"
13
12
  },
14
13
  "keywords": [
@@ -36,5 +35,5 @@
36
35
  "lib/",
37
36
  "index.js"
38
37
  ],
39
- "gitHead": "4c022b8b6845fdd4792fc0003dbfc740fef4a942"
38
+ "gitHead": "79a76a082db91399d0e432ef02bf2981a6739107"
40
39
  }