@node-red/nodes 2.2.2 → 3.0.0-beta.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.
Files changed (55) hide show
  1. package/99-sample.html.demo +1 -1
  2. package/core/common/05-junction.html +5 -0
  3. package/core/common/05-junction.js +12 -0
  4. package/core/common/20-inject.html +25 -13
  5. package/core/common/20-inject.js +3 -2
  6. package/core/common/21-debug.html +58 -5
  7. package/core/common/21-debug.js +57 -27
  8. package/core/common/60-link.html +65 -29
  9. package/core/common/60-link.js +169 -20
  10. package/core/common/lib/debug/debug-utils.js +34 -1
  11. package/core/function/10-function.html +58 -22
  12. package/core/function/10-switch.html +19 -12
  13. package/core/function/10-switch.js +1 -0
  14. package/core/function/15-change.html +40 -12
  15. package/core/function/16-range.html +14 -5
  16. package/core/function/80-template.html +16 -12
  17. package/core/function/89-delay.html +46 -6
  18. package/core/function/89-trigger.html +12 -4
  19. package/core/function/rbe.html +7 -3
  20. package/core/network/05-tls.html +10 -4
  21. package/core/network/06-httpproxy.html +10 -1
  22. package/core/network/10-mqtt.html +73 -17
  23. package/core/network/10-mqtt.js +239 -91
  24. package/core/network/21-httpin.html +10 -6
  25. package/core/network/21-httprequest.html +219 -12
  26. package/core/network/21-httprequest.js +98 -17
  27. package/core/network/22-websocket.html +19 -5
  28. package/core/network/22-websocket.js +16 -13
  29. package/core/network/31-tcpin.html +47 -10
  30. package/core/network/31-tcpin.js +23 -20
  31. package/core/network/32-udp.html +14 -2
  32. package/core/parsers/70-CSV.html +4 -1
  33. package/core/parsers/70-JSON.html +3 -2
  34. package/core/parsers/70-XML.html +2 -1
  35. package/core/parsers/70-YAML.html +2 -1
  36. package/core/sequence/17-split.html +6 -2
  37. package/core/sequence/19-batch.html +28 -4
  38. package/core/storage/10-file.html +66 -6
  39. package/core/storage/10-file.js +46 -3
  40. package/core/storage/23-watch.html +2 -1
  41. package/core/storage/23-watch.js +21 -43
  42. package/locales/de/messages.json +1 -0
  43. package/locales/en-US/common/60-link.html +19 -3
  44. package/locales/en-US/messages.json +136 -83
  45. package/locales/en-US/network/21-httprequest.html +1 -1
  46. package/locales/en-US/storage/10-file.html +6 -2
  47. package/locales/ja/common/60-link.html +13 -0
  48. package/locales/ja/messages.json +85 -32
  49. package/locales/ja/network/21-httprequest.html +1 -1
  50. package/locales/ja/storage/10-file.html +8 -6
  51. package/locales/ko/messages.json +1 -0
  52. package/locales/ru/messages.json +1 -0
  53. package/locales/zh-CN/messages.json +1 -0
  54. package/locales/zh-TW/messages.json +1 -0
  55. package/package.json +13 -13
@@ -100,14 +100,109 @@
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: RED._("node-red:httpin.label.other"),
131
+ hasValue: true, icon: "red/images/typedInput/az.svg" },
132
+ { value: "msg", label: "msg.", hasValue: true },
133
+ ]
134
+ const headerOptions = {};
135
+ const defaultOptions = [
136
+ { value: "other", label: RED._("node-red:httpin.label.other"),
137
+ hasValue: true, icon: "red/images/typedInput/az.svg" },
138
+ { value: "msg", label: "msg.", hasValue: true },
139
+ ];
140
+ headerOptions["accept"] = [
141
+ { value: "text/plain", label: "text/plain", hasValue: false },
142
+ { value: "text/html", label: "text/html", hasValue: false },
143
+ { value: "application/json", label: "application/json", hasValue: false },
144
+ { value: "application/xml", label: "application/xml", hasValue: false },
145
+ ...defaultOptions,
146
+ ];
147
+ headerOptions["accept-encoding"] = [
148
+ { value: "gzip", label: "gzip", hasValue: false },
149
+ { value: "deflate", label: "deflate", hasValue: false },
150
+ { value: "compress", label: "compress", hasValue: false },
151
+ { value: "br", label: "br", hasValue: false },
152
+ { value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
153
+ { value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
154
+ ...defaultOptions,
155
+ ];
156
+ headerOptions["accept-language"] = [
157
+ { value: "*", label: "*", hasValue: false },
158
+ { value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
159
+ { 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 },
160
+ { value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
161
+ { value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
162
+ { 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 },
163
+ { value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
164
+ ...defaultOptions,
165
+ ];
166
+ headerOptions["content-type"] = [
167
+ { value: "text/css", label: "text/css", hasValue: false },
168
+ { value: "text/plain", label: "text/plain", hasValue: false },
169
+ { value: "text/html", label: "text/html", hasValue: false },
170
+ { value: "application/json", label: "application/json", hasValue: false },
171
+ { value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
172
+ { value: "application/pdf", label: "application/pdf", hasValue: false },
173
+ { value: "application/xml", label: "application/xml", hasValue: false },
174
+ { value: "application/zip", label: "application/zip", hasValue: false },
175
+ { value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
176
+ { value: "audio/aac", label: "audio/aac", hasValue: false },
177
+ { value: "audio/ac3", label: "audio/ac3", hasValue: false },
178
+ { value: "audio/basic", label: "audio/basic", hasValue: false },
179
+ { value: "audio/mp4", label: "audio/mp4", hasValue: false },
180
+ { value: "audio/ogg", label: "audio/ogg", hasValue: false },
181
+ { value: "image/bmp", label: "image/bmp", hasValue: false },
182
+ { value: "image/gif", label: "image/gif", hasValue: false },
183
+ { value: "image/jpeg", label: "image/jpeg", hasValue: false },
184
+ { value: "image/png", label: "image/png", hasValue: false },
185
+ { value: "image/tiff", label: "image/tiff", hasValue: false },
186
+ ...defaultOptions,
187
+ ];
188
+ headerOptions["cache-control"] = [
189
+ { value: "max-age=0", label: "max-age=0", hasValue: false },
190
+ { value: "max-age=86400", label: "max-age=86400", hasValue: false },
191
+ { value: "no-cache", label: "no-cache", hasValue: false },
192
+ ...defaultOptions,
193
+ ];
194
+
195
+ headerOptions["user-agent"] = [
196
+ { value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
197
+ ...defaultOptions,
198
+ ];
199
+
200
+ function getHeaderOptions(headerName) {
201
+ const lc = (headerName || "").toLowerCase();
202
+ let opts = headerOptions[lc];
203
+ return opts || defaultOptions;
204
+ }
205
+
111
206
  RED.nodes.registerType('http request',{
112
207
  category: 'network',
113
208
  color:"rgb(231, 231, 174)",
@@ -116,12 +211,25 @@
116
211
  method:{value:"GET"},
117
212
  ret: {value:"txt"},
118
213
  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},
214
+ url:{
215
+ value:"",
216
+ validate: function(v, opt) {
217
+ if ((v.trim().length === 0) ||
218
+ (v.indexOf("://") === -1) ||
219
+ (v.trim().indexOf("http") === 0)) {
220
+ return true;
221
+ }
222
+ return RED._("node-red:httpin.errors.invalid-url");
223
+ }
224
+ },
225
+ tls: {type:"tls-config",required: false,
226
+ label:RED._("node-red:httpin.tls-config") },
121
227
  persist: {value:false},
122
- proxy: {type:"http proxy",required: false},
228
+ proxy: {type:"http proxy",required: false,
229
+ label:RED._("node-red:httpin.proxy-config") },
123
230
  authType: {value: ""},
124
- senderr: {value: false}
231
+ senderr: {value: false},
232
+ headers: { value: [] }
125
233
  },
126
234
  credentials: {
127
235
  user: {type:"text"},
@@ -144,6 +252,7 @@
144
252
  return this.name?"node_label_italic":"";
145
253
  },
146
254
  oneditprepare: function() {
255
+ const node = this;
147
256
  $("#node-input-useAuth").on("change", function() {
148
257
  if ($(this).is(":checked")) {
149
258
  $(".node-input-useAuth-row").show();
@@ -157,9 +266,10 @@
157
266
  $('#node-input-user').val('');
158
267
  $('#node-input-password').val('');
159
268
  }
269
+ RED.tray.resize();
160
270
  });
161
271
  $("#node-input-authType-select").on("change", function() {
162
- var val = $(this).val();
272
+ const val = $(this).val();
163
273
  $("#node-input-authType").val(val);
164
274
  if (val === "basic" || val === "digest") {
165
275
  $(".node-input-basic-row").show();
@@ -171,6 +281,7 @@
171
281
  $('#node-span-token').show();
172
282
  $('#node-input-user').val('');
173
283
  }
284
+ RED.tray.resize();
174
285
  });
175
286
  $("#node-input-method").on("change", function() {
176
287
  if ($(this).val() == "GET") {
@@ -178,17 +289,18 @@
178
289
  } else {
179
290
  $(".node-input-paytoqs-row").hide();
180
291
  }
292
+ RED.tray.resize();
181
293
  });
182
- if (this.paytoqs === true || this.paytoqs == "query") {
294
+ if (node.paytoqs === true || node.paytoqs == "query") {
183
295
  $("#node-input-paytoqs").val("query");
184
- } else if (this.paytoqs === "body") {
296
+ } else if (node.paytoqs === "body") {
185
297
  $("#node-input-paytoqs").val("body");
186
298
  } else {
187
299
  $("#node-input-paytoqs").val("ignore");
188
300
  }
189
- if (this.authType) {
301
+ if (node.authType) {
190
302
  $('#node-input-useAuth').prop('checked', true);
191
- $("#node-input-authType-select").val(this.authType);
303
+ $("#node-input-authType-select").val(node.authType);
192
304
  $("#node-input-authType-select").change();
193
305
  } else {
194
306
  $('#node-input-useAuth').prop('checked', false);
@@ -201,8 +313,9 @@
201
313
  } else {
202
314
  $("#node-row-tls").hide();
203
315
  }
316
+ RED.tray.resize();
204
317
  }
205
- if (this.tls) {
318
+ if (node.tls) {
206
319
  $('#node-input-usetls').prop('checked', true);
207
320
  } else {
208
321
  $('#node-input-usetls').prop('checked', false);
@@ -218,8 +331,9 @@
218
331
  } else {
219
332
  $("#node-input-useProxy-row").hide();
220
333
  }
334
+ RED.tray.resize();
221
335
  }
222
- if (this.proxy) {
336
+ if (node.proxy) {
223
337
  $("#node-input-useProxy").prop("checked", true);
224
338
  } else {
225
339
  $("#node-input-useProxy").prop("checked", false);
@@ -235,7 +349,70 @@
235
349
  } else {
236
350
  $("#tip-json").hide();
237
351
  }
352
+ RED.tray.resize();
238
353
  });
354
+ const hasMatch = function (arr, value) {
355
+ return arr.some(function (ht) {
356
+ return ht.value === value
357
+ });
358
+ }
359
+ const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
360
+ addItem: function (container, i, header) {
361
+ const row = $('<div/>').css({
362
+ overflow: 'hidden',
363
+ whiteSpace: 'nowrap',
364
+ display: 'flex'
365
+ }).appendTo(container);
366
+ const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
367
+ const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
368
+ .appendTo(propertNameCell)
369
+ .typedInput({ types: headerTypes });
370
+
371
+ const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
372
+ const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
373
+ .appendTo(propertyValueCell)
374
+ .typedInput({
375
+ types: getHeaderOptions(header.keyType)
376
+ });
377
+
378
+ const setup = function(_header) {
379
+ const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
380
+ const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
381
+ const {keyType, keyValue, valueType, valueValue} = header;
382
+ if(keyType == "msg" || keyType == "other") {
383
+ propertyName.typedInput('type', keyType);
384
+ propertyName.typedInput('value', keyValue);
385
+ } else if (headerTypeIsAPreset(keyType)) {
386
+ propertyName.typedInput('type', keyType);
387
+ } else {
388
+ propertyName.typedInput('type', "other");
389
+ propertyName.typedInput('value', keyValue);
390
+ }
391
+ if(valueType == "msg" || valueType == "other") {
392
+ propertyValue.typedInput('type', valueType);
393
+ propertyValue.typedInput('value', valueValue);
394
+ } else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
395
+ propertyValue.typedInput('type', valueType);
396
+ } else {
397
+ propertyValue.typedInput('type', "other");
398
+ propertyValue.typedInput('value', valueValue);
399
+ }
400
+ }
401
+ setup(header);
402
+
403
+ propertyName.on('change', function (event) {
404
+ propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
405
+ });
406
+
407
+ },
408
+ removable: true
409
+ });
410
+ if (node.headers) {
411
+ for (let index = 0; index < node.headers.length; index++) {
412
+ const element = node.headers[index];
413
+ headerList.editableList('addItem', node.headers[index]);
414
+ }
415
+ }
239
416
  },
240
417
  oneditsave: function() {
241
418
  if (!$("#node-input-usetls").is(':checked')) {
@@ -244,6 +421,36 @@
244
421
  if (!$("#node-input-useProxy").is(":checked")) {
245
422
  $("#node-input-proxy").val("_ADD_");
246
423
  }
424
+ const headers = $("#node-input-headers-container").editableList('items');
425
+ const node = this;
426
+ node.headers = [];
427
+ headers.each(function(i) {
428
+ const header = $(this);
429
+ const keyType = header.find(".node-input-header-name").typedInput('type');
430
+ const keyValue = header.find(".node-input-header-name").typedInput('value');
431
+ const valueType = header.find(".node-input-header-value").typedInput('type');
432
+ const valueValue = header.find(".node-input-header-value").typedInput('value');
433
+ if (keyType !== '' || keyType === 'other' || keyType === 'msg') {
434
+ node.headers.push({
435
+ keyType, keyValue, valueType, valueValue
436
+ })
437
+ }
438
+ });
439
+ },
440
+ oneditresize: function(size) {
441
+ const dlg = $("#dialog-form");
442
+ const expandRow = dlg.find('.node-input-headers-container-row');
443
+ let height = dlg.height() - 5;
444
+ if(expandRow && expandRow.length){
445
+ const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
446
+ for (let i = 0; i < siblingRows.size(); i++) {
447
+ const cr = $(siblingRows[i]);
448
+ if(cr.is(":visible"))
449
+ height -= cr.outerHeight(true);
450
+ }
451
+ $("#node-input-headers-container").editableList('height',height);
452
+ }
247
453
  }
248
454
  });
455
+ })();
249
456
  </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
  }