node-red-contrib-yandex-station-management 0.3.10 → 0.3.12

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.
@@ -21,6 +21,14 @@
21
21
  },
22
22
  payload: {
23
23
  value: 'payload',
24
+ validate: function(value, opt) {
25
+ var command = $('#node-input-input').val() || this.input;
26
+ // Only TTS uses this field; other commands always read msg.payload.
27
+ if (command !== 'tts') {
28
+ return true;
29
+ }
30
+ return RED.validators.typedInput('payloadType').call(this, value, opt);
31
+ },
24
32
  },
25
33
  payloadType: {
26
34
  value: 'msg',
@@ -31,7 +31,7 @@
31
31
  } else if (this.network.mode == "auto") {
32
32
  return true
33
33
  } else if (this.network.mode == "manual"){
34
- return address.match(/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/)
34
+ return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)
35
35
  }
36
36
  }
37
37
 
@@ -3,6 +3,20 @@ var mDnsSd = require('node-dns-sd');
3
3
  var WebSocket = require("ws");
4
4
  const { parse } = require('node-dns-sd/lib/dns-sd-parser');
5
5
 
6
+ // node-dns-sd owns one UDP socket. Share a pending search across all login
7
+ // nodes, including overlapping polls and retries, until it has fully settled.
8
+ let discoveryPromise = null;
9
+ function discoverStations() {
10
+ if (!discoveryPromise) {
11
+ discoveryPromise = Promise.resolve().then(() => mDnsSd.discover({
12
+ name: '_yandexio._tcp.local'
13
+ })).finally(() => {
14
+ discoveryPromise = null;
15
+ });
16
+ }
17
+ return discoveryPromise;
18
+ }
19
+
6
20
  module.exports = function(RED) {
7
21
 
8
22
  function YandexLoginNode(config) {
@@ -129,9 +143,7 @@ module.exports = function(RED) {
129
143
  });
130
144
  }
131
145
  async function discoverDevices(deviceList) {
132
- await mDnsSd.discover({
133
- name: '_yandexio._tcp.local'
134
- }).then((result) => {
146
+ await discoverStations().then((result) => {
135
147
  node.emit('refreshHttpDNS', result);
136
148
  if (result.length != 0){
137
149
  for (const device of deviceList) {
@@ -855,4 +867,4 @@ module.exports = function(RED) {
855
867
  }
856
868
  });
857
869
 
858
- }
870
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "node-red-contrib-yandex-station-management",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Local management of YandexStation using API on websockets",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"no test specified\""
7
+ "test": "node --test test/*.test.js"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -13,22 +13,23 @@
13
13
  "author": "n0name",
14
14
  "license": "ISC",
15
15
  "homepage": "https://github.com/n0name45/node-red-contrib-yandex-station-management",
16
- "node-red": {
17
- "nodes": {
18
- "alice-local-in": "nodes/in.js",
19
- "alice-local-get": "nodes/get.js",
20
- "alice-local-out": "nodes/local-out.js",
21
- "yandex-login": "nodes/yandex-login.js",
22
- "alice-local-station": "nodes/station.js"
23
-
24
- }
25
- },
26
- "keywords": [ "node-red" ],
27
- "dependencies": {
28
- "request": "^2.88.2",
29
- "request-promise": "^4.2.5",
30
- "request-promise-native": "^1.0.8",
31
- "node-dns-sd": "^0.4.2",
32
- "ws": "^7.4.0"
16
+ "node-red": {
17
+ "nodes": {
18
+ "alice-local-in": "nodes/in.js",
19
+ "alice-local-get": "nodes/get.js",
20
+ "alice-local-out": "nodes/local-out.js",
21
+ "yandex-login": "nodes/yandex-login.js",
22
+ "alice-local-station": "nodes/station.js"
33
23
  }
24
+ },
25
+ "keywords": [
26
+ "node-red"
27
+ ],
28
+ "dependencies": {
29
+ "request": "^2.88.2",
30
+ "request-promise": "^4.2.5",
31
+ "request-promise-native": "^1.0.8",
32
+ "node-dns-sd": "^1.0.1",
33
+ "ws": "^7.4.0"
34
+ }
34
35
  }
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const { EventEmitter } = require('node:events');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const test = require('node:test');
7
+ const vm = require('node:vm');
8
+
9
+ function deferred() {
10
+ let resolve, reject;
11
+ const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
12
+ return { promise, resolve, reject };
13
+ }
14
+
15
+ const tick = () => new Promise(resolve => setImmediate(resolve));
16
+
17
+ // Run the real node registration and polling code without Node-RED, cloud
18
+ // requests, UDP sockets, or real timers. Each fixture has a fresh module scope.
19
+ function fixture() {
20
+ let constructor;
21
+ let discover = () => Promise.resolve([]);
22
+ const calls = [];
23
+ const polls = [];
24
+ const logs = [];
25
+ const module = { exports: {} };
26
+ const source = process.env.YANDEX_SOURCE || path.resolve(__dirname, '../nodes/yandex-login.js');
27
+ const mocks = {
28
+ 'request-promise': () => Promise.resolve(JSON.stringify({ devices: [] })),
29
+ 'node-dns-sd': { discover: options => { calls.push(options); return discover(); } },
30
+ 'node-dns-sd/lib/dns-sd-parser': {},
31
+ ws: class WebSocket {}
32
+ };
33
+ vm.runInNewContext(fs.readFileSync(source, 'utf8'), {
34
+ module,
35
+ require: name => {
36
+ assert.ok(Object.hasOwn(mocks, name), 'unexpected dependency: ' + name);
37
+ return mocks[name];
38
+ },
39
+ setInterval: (fn, delay, ...args) => {
40
+ const poll = () => fn(...args);
41
+ polls.push(poll);
42
+ return poll;
43
+ },
44
+ clearInterval: () => {},
45
+ setTimeout: () => { throw new Error('Unexpected device connection'); },
46
+ clearTimeout: () => {},
47
+ console
48
+ }, { filename: source });
49
+ module.exports({
50
+ nodes: {
51
+ registerType: (name, node) => { constructor = node; },
52
+ createNode: (node, config) => {
53
+ const events = new EventEmitter();
54
+ for (const method of ['on', 'emit', 'removeListener', 'setMaxListeners']) {
55
+ node[method] = events[method].bind(events);
56
+ }
57
+ node.id = config.id;
58
+ node.credentials = { token: 'test-only-token' };
59
+ node.log = message => logs.push(message);
60
+ node.error = message => logs.push(message);
61
+ }
62
+ },
63
+ httpAdmin: { get: () => {} },
64
+ auth: { needsPermission: () => () => {} }
65
+ });
66
+ return {
67
+ calls, polls, logs,
68
+ create: id => new constructor({ id, debugFlag: true }),
69
+ discover: fn => { discover = fn; }
70
+ };
71
+ }
72
+
73
+ test('two login nodes share one discovery and both receive its result', async () => {
74
+ const f = fixture();
75
+ const pending = deferred();
76
+ f.discover(() => pending.promise);
77
+ const received = [];
78
+ f.create('first').on('refreshHttpDNS', result => received.push(result));
79
+ f.create('second').on('refreshHttpDNS', result => received.push(result));
80
+ await tick();
81
+ const count = f.calls.length;
82
+ const result = [];
83
+ pending.resolve(result);
84
+ await tick();
85
+ assert.equal(count, 1);
86
+ assert.equal(f.calls[0].name, '_yandexio._tcp.local');
87
+ assert.equal(received.length, 2);
88
+ assert.ok(received.every(value => value === result));
89
+ });
90
+
91
+ test('repeated polling during a slow search shares the pending operation', async () => {
92
+ const f = fixture();
93
+ const pending = deferred();
94
+ f.discover(() => pending.promise);
95
+ f.create('first');
96
+ await tick();
97
+ f.polls[0]();
98
+ f.polls[0]();
99
+ await tick();
100
+ const count = f.calls.length;
101
+ pending.resolve([]);
102
+ await tick();
103
+ assert.equal(count, 1);
104
+ });
105
+
106
+ test('a later poll starts a fresh discovery after success', async () => {
107
+ const f = fixture();
108
+ f.create('first');
109
+ await tick();
110
+ assert.equal(f.calls.length, 1);
111
+ f.polls[0]();
112
+ await tick();
113
+ assert.equal(f.calls.length, 2);
114
+ });
115
+
116
+ test('shared discovery rejection reaches both callers and allows retry', async () => {
117
+ const f = fixture();
118
+ const pending = deferred();
119
+ f.discover(() => pending.promise);
120
+ f.create('first');
121
+ f.create('second');
122
+ await tick();
123
+ const count = f.calls.length;
124
+ const error = new Error('socket bind failed');
125
+ pending.reject(error);
126
+ await tick();
127
+ assert.equal(count, 1);
128
+ assert.equal(f.logs.filter(value => value === error).length, 2);
129
+ f.discover(() => Promise.resolve([]));
130
+ f.polls[0]();
131
+ await tick();
132
+ assert.equal(f.calls.length, 2);
133
+ });
134
+
135
+ test('synchronous discovery errors are handled and do not prevent retry', async () => {
136
+ const f = fixture();
137
+ const error = new Error('socket setup failed');
138
+ f.discover(() => { throw error; });
139
+ f.create('first');
140
+ await tick();
141
+ assert.ok(f.logs.includes(error));
142
+ f.discover(() => Promise.resolve([]));
143
+ f.polls[0]();
144
+ await tick();
145
+ assert.equal(f.calls.length, 2);
146
+ });
147
+
148
+ test('a login node created during discovery joins the existing search', async () => {
149
+ const f = fixture();
150
+ const pending = deferred();
151
+ f.discover(() => pending.promise);
152
+ f.create('first');
153
+ await tick();
154
+ const received = [];
155
+ f.create('second').on('refreshHttpDNS', value => received.push(value));
156
+ await tick();
157
+ const count = f.calls.length;
158
+ pending.resolve([]);
159
+ await tick();
160
+ assert.equal(count, 1);
161
+ assert.equal(received.length, 1);
162
+ });