@node-red/nodes 2.2.2 → 3.0.0-beta.1

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.
Files changed (51) hide show
  1. package/core/common/05-junction.html +5 -0
  2. package/core/common/05-junction.js +12 -0
  3. package/core/common/20-inject.html +25 -13
  4. package/core/common/21-debug.html +60 -6
  5. package/core/common/21-debug.js +57 -27
  6. package/core/common/60-link.html +66 -29
  7. package/core/common/60-link.js +169 -20
  8. package/core/common/lib/debug/debug-utils.js +34 -1
  9. package/core/function/10-function.html +57 -21
  10. package/core/function/10-switch.html +3 -1
  11. package/core/function/10-switch.js +1 -0
  12. package/core/function/15-change.html +40 -12
  13. package/core/function/16-range.html +14 -5
  14. package/core/function/80-template.html +16 -12
  15. package/core/function/89-delay.html +46 -6
  16. package/core/function/89-trigger.html +12 -4
  17. package/core/function/rbe.html +7 -3
  18. package/core/network/05-tls.html +10 -4
  19. package/core/network/06-httpproxy.html +10 -1
  20. package/core/network/10-mqtt.html +73 -17
  21. package/core/network/10-mqtt.js +191 -80
  22. package/core/network/21-httpin.html +6 -2
  23. package/core/network/21-httprequest.html +217 -12
  24. package/core/network/21-httprequest.js +98 -17
  25. package/core/network/22-websocket.html +19 -5
  26. package/core/network/22-websocket.js +16 -13
  27. package/core/network/31-tcpin.html +47 -10
  28. package/core/network/31-tcpin.js +8 -3
  29. package/core/network/32-udp.html +14 -2
  30. package/core/parsers/70-CSV.html +4 -1
  31. package/core/parsers/70-JSON.html +3 -2
  32. package/core/parsers/70-XML.html +2 -1
  33. package/core/parsers/70-YAML.html +2 -1
  34. package/core/sequence/17-split.html +5 -1
  35. package/core/sequence/19-batch.html +28 -4
  36. package/core/storage/10-file.html +68 -8
  37. package/core/storage/10-file.js +46 -3
  38. package/core/storage/23-watch.html +2 -1
  39. package/core/storage/23-watch.js +21 -43
  40. package/locales/de/messages.json +1 -0
  41. package/locales/en-US/common/60-link.html +18 -3
  42. package/locales/en-US/messages.json +68 -17
  43. package/locales/en-US/network/21-httprequest.html +1 -1
  44. package/locales/en-US/storage/10-file.html +6 -2
  45. package/locales/ja/common/60-link.html +12 -0
  46. package/locales/ja/messages.json +63 -16
  47. package/locales/ko/messages.json +1 -0
  48. package/locales/ru/messages.json +1 -0
  49. package/locales/zh-CN/messages.json +1 -0
  50. package/locales/zh-TW/messages.json +1 -0
  51. package/package.json +12 -12
@@ -100,14 +100,107 @@
100
100
  <option value="obj" data-i18n="httpin.json"></option>
101
101
  </select>
102
102
  </div>
103
+
104
+ <div class="form-row form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
105
+
106
+ <div class="form-row" style="margin-bottom:0;">
107
+ <label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
108
+ </div>
109
+ <div class="form-row node-input-headers-container-row">
110
+ <ol id="node-input-headers-container"></ol>
111
+ </div>
112
+
103
113
  <div class="form-row">
104
114
  <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
105
115
  <input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
106
116
  </div>
107
- <div class="form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
108
117
  </script>
109
118
 
110
119
  <script type="text/javascript">
120
+ (function() {
121
+ const headerTypes = [
122
+ { value: "Accept", label: "Accept", hasValue: false },
123
+ { value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
124
+ { value: "Accept-Language", label: "Accept-Language", hasValue: false },
125
+ { value: "Authorization", label: "Authorization", hasValue: false },
126
+ { value: "Content-Type", label: "Content-Type", hasValue: false },
127
+ { value: "Cache-Control", label: "Cache-Control", hasValue: false },
128
+ { value: "User-Agent", label: "User-Agent", hasValue: false },
129
+ { value: "Location", label: "Location", hasValue: false },
130
+ { value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
131
+ { value: "msg", label: "msg.", hasValue: true },
132
+ ]
133
+ const headerOptions = {};
134
+ const defaultOptions = [
135
+ { value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
136
+ { value: "msg", label: "msg.", hasValue: true },
137
+ ];
138
+ headerOptions["accept"] = [
139
+ { value: "text/plain", label: "text/plain", hasValue: false },
140
+ { value: "text/html", label: "text/html", hasValue: false },
141
+ { value: "application/json", label: "application/json", hasValue: false },
142
+ { value: "application/xml", label: "application/xml", hasValue: false },
143
+ ...defaultOptions,
144
+ ];
145
+ headerOptions["accept-encoding"] = [
146
+ { value: "gzip", label: "gzip", hasValue: false },
147
+ { value: "deflate", label: "deflate", hasValue: false },
148
+ { value: "compress", label: "compress", hasValue: false },
149
+ { value: "br", label: "br", hasValue: false },
150
+ { value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
151
+ { value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
152
+ ...defaultOptions,
153
+ ];
154
+ headerOptions["accept-language"] = [
155
+ { value: "*", label: "*", hasValue: false },
156
+ { value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
157
+ { value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
158
+ { value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
159
+ { value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
160
+ { value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
161
+ { value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
162
+ ...defaultOptions,
163
+ ];
164
+ headerOptions["content-type"] = [
165
+ { value: "text/css", label: "text/css", hasValue: false },
166
+ { value: "text/plain", label: "text/plain", hasValue: false },
167
+ { value: "text/html", label: "text/html", hasValue: false },
168
+ { value: "application/json", label: "application/json", hasValue: false },
169
+ { value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
170
+ { value: "application/pdf", label: "application/pdf", hasValue: false },
171
+ { value: "application/xml", label: "application/xml", hasValue: false },
172
+ { value: "application/zip", label: "application/zip", hasValue: false },
173
+ { value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
174
+ { value: "audio/aac", label: "audio/aac", hasValue: false },
175
+ { value: "audio/ac3", label: "audio/ac3", hasValue: false },
176
+ { value: "audio/basic", label: "audio/basic", hasValue: false },
177
+ { value: "audio/mp4", label: "audio/mp4", hasValue: false },
178
+ { value: "audio/ogg", label: "audio/ogg", hasValue: false },
179
+ { value: "image/bmp", label: "image/bmp", hasValue: false },
180
+ { value: "image/gif", label: "image/gif", hasValue: false },
181
+ { value: "image/jpeg", label: "image/jpeg", hasValue: false },
182
+ { value: "image/png", label: "image/png", hasValue: false },
183
+ { value: "image/tiff", label: "image/tiff", hasValue: false },
184
+ ...defaultOptions,
185
+ ];
186
+ headerOptions["cache-control"] = [
187
+ { value: "max-age=0", label: "max-age=0", hasValue: false },
188
+ { value: "max-age=86400", label: "max-age=86400", hasValue: false },
189
+ { value: "no-cache", label: "no-cache", hasValue: false },
190
+ ...defaultOptions,
191
+ ];
192
+
193
+ headerOptions["user-agent"] = [
194
+ { value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
195
+ ...defaultOptions,
196
+ ];
197
+
198
+ function getHeaderOptions(headerName) {
199
+ const lc = (headerName || "").toLowerCase();
200
+ let opts = headerOptions[lc];
201
+ return opts || defaultOptions;
202
+ }
203
+
111
204
  RED.nodes.registerType('http request',{
112
205
  category: 'network',
113
206
  color:"rgb(231, 231, 174)",
@@ -116,12 +209,25 @@
116
209
  method:{value:"GET"},
117
210
  ret: {value:"txt"},
118
211
  paytoqs: {value: false},
119
- url:{value:"",validate:function(v) { return (v.trim().length === 0) || (v.indexOf("://") === -1) || (v.trim().indexOf("http") === 0)} },
120
- tls: {type:"tls-config",required: false},
212
+ url:{
213
+ value:"",
214
+ validate: function(v, opt) {
215
+ if ((v.trim().length === 0) ||
216
+ (v.indexOf("://") === -1) ||
217
+ (v.trim().indexOf("http") === 0)) {
218
+ return true;
219
+ }
220
+ return RED._("node-red:httpin.errors.invalid-url");
221
+ }
222
+ },
223
+ tls: {type:"tls-config",required: false,
224
+ label:RED._("node-red:httpin.tls-config") },
121
225
  persist: {value:false},
122
- proxy: {type:"http proxy",required: false},
226
+ proxy: {type:"http proxy",required: false,
227
+ label:RED._("node-red:httpin.proxy-config") },
123
228
  authType: {value: ""},
124
- senderr: {value: false}
229
+ senderr: {value: false},
230
+ headers: { value: [] }
125
231
  },
126
232
  credentials: {
127
233
  user: {type:"text"},
@@ -144,6 +250,7 @@
144
250
  return this.name?"node_label_italic":"";
145
251
  },
146
252
  oneditprepare: function() {
253
+ const node = this;
147
254
  $("#node-input-useAuth").on("change", function() {
148
255
  if ($(this).is(":checked")) {
149
256
  $(".node-input-useAuth-row").show();
@@ -157,9 +264,10 @@
157
264
  $('#node-input-user').val('');
158
265
  $('#node-input-password').val('');
159
266
  }
267
+ RED.tray.resize();
160
268
  });
161
269
  $("#node-input-authType-select").on("change", function() {
162
- var val = $(this).val();
270
+ const val = $(this).val();
163
271
  $("#node-input-authType").val(val);
164
272
  if (val === "basic" || val === "digest") {
165
273
  $(".node-input-basic-row").show();
@@ -171,6 +279,7 @@
171
279
  $('#node-span-token').show();
172
280
  $('#node-input-user').val('');
173
281
  }
282
+ RED.tray.resize();
174
283
  });
175
284
  $("#node-input-method").on("change", function() {
176
285
  if ($(this).val() == "GET") {
@@ -178,17 +287,18 @@
178
287
  } else {
179
288
  $(".node-input-paytoqs-row").hide();
180
289
  }
290
+ RED.tray.resize();
181
291
  });
182
- if (this.paytoqs === true || this.paytoqs == "query") {
292
+ if (node.paytoqs === true || node.paytoqs == "query") {
183
293
  $("#node-input-paytoqs").val("query");
184
- } else if (this.paytoqs === "body") {
294
+ } else if (node.paytoqs === "body") {
185
295
  $("#node-input-paytoqs").val("body");
186
296
  } else {
187
297
  $("#node-input-paytoqs").val("ignore");
188
298
  }
189
- if (this.authType) {
299
+ if (node.authType) {
190
300
  $('#node-input-useAuth').prop('checked', true);
191
- $("#node-input-authType-select").val(this.authType);
301
+ $("#node-input-authType-select").val(node.authType);
192
302
  $("#node-input-authType-select").change();
193
303
  } else {
194
304
  $('#node-input-useAuth').prop('checked', false);
@@ -201,8 +311,9 @@
201
311
  } else {
202
312
  $("#node-row-tls").hide();
203
313
  }
314
+ RED.tray.resize();
204
315
  }
205
- if (this.tls) {
316
+ if (node.tls) {
206
317
  $('#node-input-usetls').prop('checked', true);
207
318
  } else {
208
319
  $('#node-input-usetls').prop('checked', false);
@@ -218,8 +329,9 @@
218
329
  } else {
219
330
  $("#node-input-useProxy-row").hide();
220
331
  }
332
+ RED.tray.resize();
221
333
  }
222
- if (this.proxy) {
334
+ if (node.proxy) {
223
335
  $("#node-input-useProxy").prop("checked", true);
224
336
  } else {
225
337
  $("#node-input-useProxy").prop("checked", false);
@@ -235,7 +347,70 @@
235
347
  } else {
236
348
  $("#tip-json").hide();
237
349
  }
350
+ RED.tray.resize();
238
351
  });
352
+ const hasMatch = function (arr, value) {
353
+ return arr.some(function (ht) {
354
+ return ht.value === value
355
+ });
356
+ }
357
+ const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
358
+ addItem: function (container, i, header) {
359
+ const row = $('<div/>').css({
360
+ overflow: 'hidden',
361
+ whiteSpace: 'nowrap',
362
+ display: 'flex'
363
+ }).appendTo(container);
364
+ const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
365
+ const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
366
+ .appendTo(propertNameCell)
367
+ .typedInput({ types: headerTypes });
368
+
369
+ const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
370
+ const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
371
+ .appendTo(propertyValueCell)
372
+ .typedInput({
373
+ types: getHeaderOptions(header.keyType)
374
+ });
375
+
376
+ const setup = function(_header) {
377
+ const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
378
+ const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
379
+ const {keyType, keyValue, valueType, valueValue} = header;
380
+ if(keyType == "msg" || keyType == "other") {
381
+ propertyName.typedInput('type', keyType);
382
+ propertyName.typedInput('value', keyValue);
383
+ } else if (headerTypeIsAPreset(keyType)) {
384
+ propertyName.typedInput('type', keyType);
385
+ } else {
386
+ propertyName.typedInput('type', "other");
387
+ propertyName.typedInput('value', keyValue);
388
+ }
389
+ if(valueType == "msg" || valueType == "other") {
390
+ propertyValue.typedInput('type', valueType);
391
+ propertyValue.typedInput('value', valueValue);
392
+ } else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
393
+ propertyValue.typedInput('type', valueType);
394
+ } else {
395
+ propertyValue.typedInput('type', "other");
396
+ propertyValue.typedInput('value', valueValue);
397
+ }
398
+ }
399
+ setup(header);
400
+
401
+ propertyName.on('change', function (event) {
402
+ propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
403
+ });
404
+
405
+ },
406
+ removable: true
407
+ });
408
+ if (node.headers) {
409
+ for (let index = 0; index < node.headers.length; index++) {
410
+ const element = node.headers[index];
411
+ headerList.editableList('addItem', node.headers[index]);
412
+ }
413
+ }
239
414
  },
240
415
  oneditsave: function() {
241
416
  if (!$("#node-input-usetls").is(':checked')) {
@@ -244,6 +419,36 @@
244
419
  if (!$("#node-input-useProxy").is(":checked")) {
245
420
  $("#node-input-proxy").val("_ADD_");
246
421
  }
422
+ const headers = $("#node-input-headers-container").editableList('items');
423
+ const node = this;
424
+ node.headers = [];
425
+ headers.each(function(i) {
426
+ const header = $(this);
427
+ const keyType = header.find(".node-input-header-name").typedInput('type');
428
+ const keyValue = header.find(".node-input-header-name").typedInput('value');
429
+ const valueType = header.find(".node-input-header-value").typedInput('type');
430
+ const valueValue = header.find(".node-input-header-value").typedInput('value');
431
+ if (keyType !== '' || keyType === 'other' || keyType === 'msg') {
432
+ node.headers.push({
433
+ keyType, keyValue, valueType, valueValue
434
+ })
435
+ }
436
+ });
437
+ },
438
+ oneditresize: function(size) {
439
+ const dlg = $("#dialog-form");
440
+ const expandRow = dlg.find('.node-input-headers-container-row');
441
+ let height = dlg.height() - 5;
442
+ if(expandRow && expandRow.length){
443
+ const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
444
+ for (let i = 0; i < siblingRows.size(); i++) {
445
+ const cr = $(siblingRows[i]);
446
+ if(cr.is(":visible"))
447
+ height -= cr.outerHeight(true);
448
+ }
449
+ $("#node-input-headers-container").editableList('height',height);
450
+ }
247
451
  }
248
452
  });
453
+ })();
249
454
  </script>
@@ -73,7 +73,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
73
73
  var paytobody = false;
74
74
  var redirectList = [];
75
75
  var sendErrorsToCatch = n.senderr;
76
-
76
+ node.headers = n.headers || [];
77
77
  var nodeHTTPPersistent = n["persist"];
78
78
  if (n.tls) {
79
79
  var tlsNode = RED.nodes.getNode(n.tls);
@@ -105,6 +105,37 @@ in your Node-RED user directory (${RED.settings.userDir}).
105
105
  timingLog = RED.settings.httpRequestTimingLog;
106
106
  }
107
107
 
108
+ /**
109
+ * Case insensitive header value update util function
110
+ * @param {object} headersObject The opt.headers object to update
111
+ * @param {string} name The header name
112
+ * @param {string} value The header value to set (if blank, header is removed)
113
+ */
114
+ const updateHeader = function(headersObject, name, value ) {
115
+ const hn = name.toLowerCase();
116
+ const keys = Object.keys(headersObject);
117
+ const matchingKeys = keys.filter(e => e.toLowerCase() == hn)
118
+ const updateKey = (k,v) => {
119
+ delete headersObject[k]; //delete incase of case change
120
+ if(v) { headersObject[name] = v } //re-add with requested name & value
121
+ }
122
+ if(matchingKeys.length == 0) {
123
+ updateKey(name, value)
124
+ } else {
125
+ matchingKeys.forEach(k => {
126
+ updateKey(k, value);
127
+ });
128
+ }
129
+ }
130
+ /**
131
+ * @param {Object} headersObject
132
+ * @param {string} name
133
+ * @return {any} value
134
+ */
135
+ const getHeaderValue = (headersObject, name) => {
136
+ const asLowercase = name.toLowercase();
137
+ return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
138
+ }
108
139
  this.on("input",function(msg,nodeSend,nodeDone) {
109
140
  checkNodeAgentPatch();
110
141
  //reset redirectList on each request
@@ -183,7 +214,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
183
214
  // TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
184
215
  opts.decompress = false;
185
216
  opts.method = method;
186
- opts.headers = {};
187
217
  opts.retry = 0;
188
218
  opts.responseType = 'buffer';
189
219
  opts.maxRedirects = 21;
@@ -229,34 +259,85 @@ in your Node-RED user directory (${RED.settings.userDir}).
229
259
  ]
230
260
  }
231
261
 
232
- var ctSet = "Content-Type"; // set default camel case
233
- var clSet = "Content-Length";
262
+ let ctSet = "Content-Type"; // set default camel case
263
+ let clSet = "Content-Length";
264
+ const normaliseKnownHeader = function (name) {
265
+ const _name = name.toLowerCase();
266
+ // only normalise the known headers used later in this
267
+ // function. Otherwise leave them alone.
268
+ switch (_name) {
269
+ case "content-type":
270
+ ctSet = name;
271
+ name = _name;
272
+ break;
273
+ case "content-length":
274
+ clSet = name;
275
+ name = _name;
276
+ break;
277
+ }
278
+ return name;
279
+ }
280
+
281
+ opts.headers = {};
282
+ //add msg.headers
283
+ //NOTE: ui headers will take precidence over msg.headers
234
284
  if (msg.headers) {
235
285
  if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
236
- var headerHash = msg.headers['x-node-red-request-node'];
286
+ const headerHash = msg.headers['x-node-red-request-node'];
237
287
  delete msg.headers['x-node-red-request-node'];
238
- var hash = hashSum(msg.headers);
288
+ const hash = hashSum(msg.headers);
239
289
  if (hash === headerHash) {
240
290
  delete msg.headers;
241
291
  }
242
292
  }
243
293
  if (msg.headers) {
244
- for (var v in msg.headers) {
245
- if (msg.headers.hasOwnProperty(v)) {
246
- var name = v.toLowerCase();
247
- if (name !== "content-type" && name !== "content-length") {
248
- // only normalise the known headers used later in this
249
- // function. Otherwise leave them alone.
250
- name = v;
294
+ for (let hn in msg.headers) {
295
+ const name = normaliseKnownHeader(hn);
296
+ updateHeader(opts.headers, name, msg.headers[hn]);
297
+ }
298
+ }
299
+ }
300
+
301
+ //add/remove/update headers from UI.
302
+ if (node.headers.length) {
303
+ for (let index = 0; index < node.headers.length; index++) {
304
+ const header = node.headers[index];
305
+ let headerName, headerValue;
306
+ if (header.keyType === "other") {
307
+ headerName = header.keyValue
308
+ } else if (header.keyType === "msg") {
309
+ RED.util.evaluateNodeProperty(header.keyValue, header.keyType, node, msg, (err, value) => {
310
+ if (err) {
311
+ //ignore header
312
+ } else {
313
+ headerName = value;
251
314
  }
252
- else if (name === 'content-type') { ctSet = v; }
253
- else { clSet = v; }
254
- opts.headers[name] = msg.headers[v];
255
- }
315
+ });
316
+ } else {
317
+ headerName = header.keyType
318
+ }
319
+ if (!headerName) {
320
+ continue; //skip if header name is empyy
256
321
  }
322
+ if (header.valueType === "other") {
323
+ headerValue = header.valueValue
324
+ } else if (header.valueType === "msg") {
325
+ RED.util.evaluateNodeProperty(header.valueValue, header.valueType, node, msg, (err, value) => {
326
+ if (err) {
327
+ //ignore header
328
+ } else {
329
+ headerValue = value;
330
+ }
331
+ });
332
+ } else {
333
+ headerValue = header.valueType
334
+ }
335
+ const hn = normaliseKnownHeader(headerName);
336
+ updateHeader(opts.headers, hn, headerValue);
257
337
  }
258
338
  }
259
339
 
340
+
260
341
  if (msg.hasOwnProperty('followRedirects')) {
261
342
  opts.followRedirect = !!msg.followRedirects;
262
343
  }
@@ -83,13 +83,19 @@
83
83
  return true;
84
84
  }
85
85
  else {
86
- return RED.nodes.node(this.server) != null;
86
+ if (RED.nodes.node(this.server) != null) {
87
+ return true;
88
+ }
89
+ return RED._("node-red:websocket.errors.missing-server");
87
90
  }
88
91
  }
89
92
 
90
93
  function ws_validateclient() {
91
94
  if ($("#node-input-mode").val() === 'client' || (this.client && !this.server)) {
92
- return RED.nodes.node(this.client) != null;
95
+ if (RED.nodes.node(this.client) != null) {
96
+ return true;
97
+ }
98
+ return RED._("node-red:websocket.errors.missing-client");
93
99
  }
94
100
  else {
95
101
  return true;
@@ -138,7 +144,9 @@
138
144
  RED.nodes.registerType('websocket-listener',{
139
145
  category: 'config',
140
146
  defaults: {
141
- path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
147
+ path: {value:"",required:true,
148
+ label:RED._("node-red:websocket.label.path"),
149
+ validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
142
150
  wholemsg: {value:"false"}
143
151
  },
144
152
  inputs:0,
@@ -174,10 +182,16 @@
174
182
  RED.nodes.registerType('websocket-client',{
175
183
  category: 'config',
176
184
  defaults: {
177
- path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
185
+ path: {
186
+ value:"",required:true,
187
+ label:RED._("node-red:websocket.label.path"),
188
+ validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
178
189
  tls: {type:"tls-config",required: false},
179
190
  wholemsg: {value:"false"},
180
- hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) },
191
+ hb: {
192
+ value: "",
193
+ label:RED._("node-red:websocket.sendheartbeat"),
194
+ validate: RED.validators.number(/*blank allowed*/true) },
181
195
  subprotocol: {value:"",required: false}
182
196
  },
183
197
  inputs:0,
@@ -35,8 +35,6 @@ module.exports = function(RED) {
35
35
  }
36
36
  }
37
37
  var listenerNodes = {};
38
- var activeListenerNodes = 0;
39
-
40
38
 
41
39
  // A node red node that sets up a local websocket server
42
40
  function WebSocketListenerNode(n) {
@@ -166,7 +164,6 @@ module.exports = function(RED) {
166
164
  }
167
165
 
168
166
  if (node.isServer) {
169
- activeListenerNodes++;
170
167
  if (!serverUpgradeAdded) {
171
168
  RED.server.on('upgrade', handleServerUpgrade);
172
169
  serverUpgradeAdded = true
@@ -210,7 +207,7 @@ module.exports = function(RED) {
210
207
  startconn(); // start outbound connection
211
208
  }
212
209
 
213
- node.on("close", function() {
210
+ node.on("close", function(done) {
214
211
  if (node.heartbeatInterval) {
215
212
  clearInterval(node.heartbeatInterval);
216
213
  }
@@ -218,19 +215,25 @@ module.exports = function(RED) {
218
215
  delete listenerNodes[node.fullPath];
219
216
  node.server.close();
220
217
  node._inputNodes = [];
221
- activeListenerNodes--;
222
- // if (activeListenerNodes === 0 && serverUpgradeAdded) {
223
- // RED.server.removeListener('upgrade', handleServerUpgrade);
224
- // serverUpgradeAdded = false;
225
- // }
226
218
  }
227
219
  else {
228
220
  node.closing = true;
229
221
  node.server.close();
230
- if (node.tout) {
231
- clearTimeout(node.tout);
232
- node.tout = null;
233
- }
222
+ //wait 20*50 (1000ms max) for ws to close.
223
+ //call done when readyState === ws.CLOSED (or 1000ms, whichever comes fist)
224
+ const closeMonitorInterval = 20;
225
+ let closeMonitorCount = 50;
226
+ let si = setInterval(() => {
227
+ if(node.server.readyState === ws.CLOSED || closeMonitorCount <= 0) {
228
+ if (node.tout) {
229
+ clearTimeout(node.tout);
230
+ node.tout = null;
231
+ }
232
+ clearInterval(si);
233
+ return done();
234
+ }
235
+ closeMonitorCount--;
236
+ }, closeMonitorInterval);
234
237
  }
235
238
  });
236
239
  }