aes70 2.0.15 → 2.0.16
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/dist/AES70.es5.js +274 -63
- package/package.json +1 -1
- package/src/OCP1/message_generator.js +9 -1
- package/src/connection.d.ts +17 -0
- package/src/connection.js +36 -16
- package/src/controller/abstract_udp_connection.js +58 -43
- package/src/controller/client_connection.js +46 -3
- package/src/controller/fetch_device_content.js +4 -0
- package/src/utils/timer.js +123 -0
package/dist/AES70.es5.js
CHANGED
|
@@ -939,6 +939,14 @@
|
|
|
939
939
|
};
|
|
940
940
|
}
|
|
941
941
|
|
|
942
|
+
get bufferedAmount() {
|
|
943
|
+
return this._currentSize;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
get batchSize() {
|
|
947
|
+
return this._batchSize;
|
|
948
|
+
}
|
|
949
|
+
|
|
942
950
|
add(pdu) {
|
|
943
951
|
const currentSize = this._currentSize;
|
|
944
952
|
const encodedLength = pdu.encoded_length();
|
|
@@ -971,7 +979,7 @@
|
|
|
971
979
|
/* Keepalive packets are never combined into one message. */
|
|
972
980
|
this._lastMessageType = messageType;
|
|
973
981
|
|
|
974
|
-
if (this._currentSize > this._batchSize) {
|
|
982
|
+
if (this._currentSize + additionalSize > this._batchSize) {
|
|
975
983
|
this.flush();
|
|
976
984
|
} else if (this._pdus.length === 1) {
|
|
977
985
|
this.scheduleFlush();
|
|
@@ -1036,6 +1044,130 @@
|
|
|
1036
1044
|
}
|
|
1037
1045
|
}
|
|
1038
1046
|
|
|
1047
|
+
function isItTime(target, now) {
|
|
1048
|
+
// We are ok with 1ms accuracy.
|
|
1049
|
+
return target - now < 1;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
class Timer {
|
|
1053
|
+
constructor(callback, getNow) {
|
|
1054
|
+
this._callback = callback;
|
|
1055
|
+
this._getNow = getNow;
|
|
1056
|
+
this._targetTime = undefined;
|
|
1057
|
+
this._timerId = undefined;
|
|
1058
|
+
this._timerAt = undefined;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
poll() {
|
|
1062
|
+
const now = this._getNow();
|
|
1063
|
+
|
|
1064
|
+
if (this._targetTime === undefined) return;
|
|
1065
|
+
|
|
1066
|
+
if (isItTime(this._targetTime, now)) {
|
|
1067
|
+
this._targetTime = undefined;
|
|
1068
|
+
try {
|
|
1069
|
+
this._callback();
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
console.error('Timer callback threw an exception', err);
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
this._reschedule();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
_reschedule() {
|
|
1079
|
+
const target = this._targetTime;
|
|
1080
|
+
const interval = target - this._getNow();
|
|
1081
|
+
|
|
1082
|
+
if (this._timerId !== undefined) {
|
|
1083
|
+
if (target >= this._timerAt) {
|
|
1084
|
+
// The timer will fire before target. We will then reschedule it.
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
clearTimeout(this._timerId);
|
|
1089
|
+
this._timerId = undefined;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this._timerAt = target;
|
|
1093
|
+
this._timerId = setTimeout(() => {
|
|
1094
|
+
this._timerId = undefined;
|
|
1095
|
+
this._timerAt = undefined;
|
|
1096
|
+
this.poll();
|
|
1097
|
+
}, Math.max(0, interval));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
*
|
|
1102
|
+
* @param {number} interval
|
|
1103
|
+
* Interval in milliseconds.
|
|
1104
|
+
*/
|
|
1105
|
+
scheduleIn(interval) {
|
|
1106
|
+
if (!(interval >= 0)) {
|
|
1107
|
+
throw new TypeError(`Expected positive interval.`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
this._targetTime = this._getNow() + interval;
|
|
1111
|
+
this._reschedule();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Schedule the timer in a given number of milliseconds. If the timer
|
|
1116
|
+
* is already running and scheduled to run before, do not modify it.
|
|
1117
|
+
*
|
|
1118
|
+
* @param {number} interval
|
|
1119
|
+
*/
|
|
1120
|
+
scheduleDeadlineIn(interval) {
|
|
1121
|
+
if (!(interval >= 0)) {
|
|
1122
|
+
throw new TypeError(`Expected positive interval.`);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const target = this._getNow() + interval;
|
|
1126
|
+
|
|
1127
|
+
if (this._targetTime !== undefined && this._targetTime <= target) {
|
|
1128
|
+
this.poll();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
this.scheduleAt(target);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
*
|
|
1137
|
+
* @param {number} target
|
|
1138
|
+
* Target time in milliseconds.
|
|
1139
|
+
*/
|
|
1140
|
+
scheduleAt(target) {
|
|
1141
|
+
if (!(target >= 0)) {
|
|
1142
|
+
throw new TypeError();
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
this._targetTime = target;
|
|
1146
|
+
this._reschedule();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
stop() {
|
|
1150
|
+
this._targetTime = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
cancel() {
|
|
1154
|
+
this.stop();
|
|
1155
|
+
this._clearTimeout();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
_clearTimeout() {
|
|
1159
|
+
if (this._timerId) {
|
|
1160
|
+
clearTimeout(this._timerId);
|
|
1161
|
+
this._timerId = undefined;
|
|
1162
|
+
this._timerAt = undefined;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
dispose() {
|
|
1167
|
+
this.cancel();
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1039
1171
|
/**
|
|
1040
1172
|
* Connection base class. It extends :class:`Events` and defines two events:
|
|
1041
1173
|
*
|
|
@@ -1072,8 +1204,11 @@
|
|
|
1072
1204
|
this.last_tx_time = now;
|
|
1073
1205
|
this.rx_bytes = 0;
|
|
1074
1206
|
this.tx_bytes = 0;
|
|
1207
|
+
this._keepalive_timer = new Timer(
|
|
1208
|
+
() => this._check_keepalive(),
|
|
1209
|
+
() => this._now()
|
|
1210
|
+
);
|
|
1075
1211
|
this.keepalive_interval = -1;
|
|
1076
|
-
this._keepalive_interval_id = null;
|
|
1077
1212
|
this._closed = false;
|
|
1078
1213
|
this.on('close', () => {
|
|
1079
1214
|
if (this._closed) return;
|
|
@@ -1092,6 +1227,18 @@
|
|
|
1092
1227
|
return true;
|
|
1093
1228
|
}
|
|
1094
1229
|
|
|
1230
|
+
get bufferedAmount() {
|
|
1231
|
+
return this._message_generator.bufferedAmount;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
get batchSize() {
|
|
1235
|
+
return this._message_generator.batchSize;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
get pendingWrites() {
|
|
1239
|
+
return this._message_generator.bufferedAmount > 0 ? 1 : 0;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1095
1242
|
send(pdu) {
|
|
1096
1243
|
if (this.is_closed()) throw new Error('Connection is closed.');
|
|
1097
1244
|
this.emit('send', pdu);
|
|
@@ -1149,7 +1296,7 @@
|
|
|
1149
1296
|
}
|
|
1150
1297
|
}
|
|
1151
1298
|
|
|
1152
|
-
this.
|
|
1299
|
+
this.poll();
|
|
1153
1300
|
}
|
|
1154
1301
|
|
|
1155
1302
|
incoming(a) {}
|
|
@@ -1180,16 +1327,18 @@
|
|
|
1180
1327
|
if (this.is_closed()) throw new Error('cleanup() called twice.');
|
|
1181
1328
|
|
|
1182
1329
|
// disable keepalive
|
|
1183
|
-
this.
|
|
1330
|
+
this._keepalive_timer.dispose();
|
|
1184
1331
|
this._message_generator.dispose();
|
|
1185
1332
|
this._message_generator = null;
|
|
1186
1333
|
this.removeAllEventListeners();
|
|
1187
1334
|
}
|
|
1188
1335
|
|
|
1189
1336
|
_check_keepalive() {
|
|
1190
|
-
if (
|
|
1191
|
-
|
|
1337
|
+
if (this.is_closed()) return;
|
|
1192
1338
|
const t = this.keepalive_interval;
|
|
1339
|
+
if (!(t > 0)) return;
|
|
1340
|
+
|
|
1341
|
+
this._keepalive_timer.scheduleIn(t / 2 + 10);
|
|
1193
1342
|
|
|
1194
1343
|
if (this.rx_idle_time() > t * 3) {
|
|
1195
1344
|
this.emit('timeout');
|
|
@@ -1201,6 +1350,13 @@
|
|
|
1201
1350
|
}
|
|
1202
1351
|
}
|
|
1203
1352
|
|
|
1353
|
+
/**
|
|
1354
|
+
* Check if some regular internal timers must run.
|
|
1355
|
+
*/
|
|
1356
|
+
poll() {
|
|
1357
|
+
this._keepalive_timer.poll();
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1204
1360
|
/**
|
|
1205
1361
|
* Flush write buffers. This are usually PDUs or may also be unwritten
|
|
1206
1362
|
* buffers.
|
|
@@ -1226,11 +1382,6 @@
|
|
|
1226
1382
|
|
|
1227
1383
|
const t = seconds * 1000;
|
|
1228
1384
|
|
|
1229
|
-
if (this._keepalive_interval_id !== null) {
|
|
1230
|
-
clearInterval(this._keepalive_interval_id);
|
|
1231
|
-
this._keepalive_interval_id = null;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1385
|
this.keepalive_interval = t;
|
|
1235
1386
|
|
|
1236
1387
|
// Notify the other side about our new keepalive
|
|
@@ -1238,12 +1389,12 @@
|
|
|
1238
1389
|
|
|
1239
1390
|
this.send(new KeepAlive(t));
|
|
1240
1391
|
|
|
1241
|
-
if (
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
this.
|
|
1246
|
-
}
|
|
1392
|
+
if (t > 0) {
|
|
1393
|
+
// we check twice as often to make sure we stay within the timers
|
|
1394
|
+
this._keepalive_timer.scheduleIn(t / 2 + 10);
|
|
1395
|
+
} else {
|
|
1396
|
+
this._keepalive_timer.stop();
|
|
1397
|
+
}
|
|
1247
1398
|
}
|
|
1248
1399
|
}
|
|
1249
1400
|
|
|
@@ -1729,17 +1880,57 @@
|
|
|
1729
1880
|
class ClientConnection extends Connection {
|
|
1730
1881
|
constructor(options) {
|
|
1731
1882
|
super(options);
|
|
1883
|
+
// All pending commands by id/handle
|
|
1732
1884
|
this._pendingCommands = new Map();
|
|
1885
|
+
// All pending commands scheduled to be sent.
|
|
1886
|
+
this._scheduledPendingCommands = new Set();
|
|
1887
|
+
// All pending commands wich have been sent.
|
|
1888
|
+
this._sentPendingCommands = new Set();
|
|
1733
1889
|
this._nextCommandHandle = 0;
|
|
1734
1890
|
this._subscribers = new Map();
|
|
1891
|
+
this._sendCommandsTimer = new Timer(
|
|
1892
|
+
() => {
|
|
1893
|
+
this.sendCommands();
|
|
1894
|
+
},
|
|
1895
|
+
() => this._now()
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
shouldSendMoreCommands() {
|
|
1900
|
+
return this.is_reliable;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
sendCommands() {
|
|
1904
|
+
const { _scheduledPendingCommands, _sentPendingCommands } = this;
|
|
1905
|
+
|
|
1906
|
+
for (const pendingCommand of _scheduledPendingCommands) {
|
|
1907
|
+
if (!this.shouldSendMoreCommands()) break;
|
|
1908
|
+
_scheduledPendingCommands.delete(pendingCommand);
|
|
1909
|
+
_sentPendingCommands.add(pendingCommand);
|
|
1910
|
+
this.send(pendingCommand.command);
|
|
1911
|
+
pendingCommand.lastSent = this._now();
|
|
1912
|
+
pendingCommand.retries++;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
scheduleSendCommands() {
|
|
1917
|
+
this._sendCommandsTimer.scheduleDeadlineIn(5);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
poll() {
|
|
1921
|
+
super.poll();
|
|
1922
|
+
this._sendCommandsTimer.poll();
|
|
1735
1923
|
}
|
|
1736
1924
|
|
|
1737
1925
|
cleanup(error) {
|
|
1738
1926
|
super.cleanup(error);
|
|
1927
|
+
this._sendCommandsTimer.dispose();
|
|
1739
1928
|
const subscribers = this._subscribers;
|
|
1740
1929
|
this._subscribers = null;
|
|
1741
1930
|
const pendingCommands = this._pendingCommands;
|
|
1742
1931
|
this._pendingCommands = null;
|
|
1932
|
+
this._scheduledPendingCommands.clear();
|
|
1933
|
+
this._sentPendingCommands.clear();
|
|
1743
1934
|
|
|
1744
1935
|
const e = new CloseError(error);
|
|
1745
1936
|
pendingCommands.forEach((pendingCommand, id) => {
|
|
@@ -1818,9 +2009,8 @@
|
|
|
1818
2009
|
);
|
|
1819
2010
|
|
|
1820
2011
|
this._pendingCommands.set(handle, pendingCommand);
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
this.send(command);
|
|
2012
|
+
this._scheduledPendingCommands.add(pendingCommand);
|
|
2013
|
+
this.scheduleSendCommands();
|
|
1824
2014
|
};
|
|
1825
2015
|
|
|
1826
2016
|
if (callback) {
|
|
@@ -1841,6 +2031,9 @@
|
|
|
1841
2031
|
|
|
1842
2032
|
pendingCommands.delete(handle);
|
|
1843
2033
|
|
|
2034
|
+
if (!this._sentPendingCommands.delete(pendingCommand))
|
|
2035
|
+
this._scheduledPendingCommands.delete(pendingCommand);
|
|
2036
|
+
|
|
1844
2037
|
return pendingCommand;
|
|
1845
2038
|
}
|
|
1846
2039
|
|
|
@@ -28806,15 +28999,20 @@
|
|
|
28806
28999
|
this.retry_interval =
|
|
28807
29000
|
options.retry_interval >= 0 ? options.retry_interval : 250;
|
|
28808
29001
|
this.retry_count = options.retry_count >= 0 ? options.retry_count : 3;
|
|
28809
|
-
this.
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
28816
|
-
|
|
28817
|
-
|
|
29002
|
+
this._write_out_timer = new Timer(
|
|
29003
|
+
() => {
|
|
29004
|
+
this._write_out();
|
|
29005
|
+
},
|
|
29006
|
+
() => this._now()
|
|
29007
|
+
);
|
|
29008
|
+
this._retry_timer = new Timer(
|
|
29009
|
+
() => {
|
|
29010
|
+
this._retryCommands();
|
|
29011
|
+
this._retry_timer.scheduleIn(this.retry_interval);
|
|
29012
|
+
},
|
|
29013
|
+
() => this._now()
|
|
29014
|
+
);
|
|
29015
|
+
this._retry_timer.scheduleIn(this.retry_interval);
|
|
28818
29016
|
this.q = [];
|
|
28819
29017
|
socket.onmessage = (buffer) => {
|
|
28820
29018
|
try {
|
|
@@ -28832,6 +29030,24 @@
|
|
|
28832
29030
|
return false;
|
|
28833
29031
|
}
|
|
28834
29032
|
|
|
29033
|
+
get bufferedAmount() {
|
|
29034
|
+
let amount = super.bufferedAmount;
|
|
29035
|
+
|
|
29036
|
+
for (const buf of this.q) {
|
|
29037
|
+
amount += buf.byteLength;
|
|
29038
|
+
}
|
|
29039
|
+
|
|
29040
|
+
return amount;
|
|
29041
|
+
}
|
|
29042
|
+
|
|
29043
|
+
get pendingWrites() {
|
|
29044
|
+
return super.pendingWrites + this.q.length;
|
|
29045
|
+
}
|
|
29046
|
+
|
|
29047
|
+
shouldSendMoreCommands() {
|
|
29048
|
+
return this.q.length < 3;
|
|
29049
|
+
}
|
|
29050
|
+
|
|
28835
29051
|
/**
|
|
28836
29052
|
* Connect to the given endpoint.
|
|
28837
29053
|
*
|
|
@@ -28901,14 +29117,8 @@
|
|
|
28901
29117
|
this.socket.close();
|
|
28902
29118
|
this.socket = null;
|
|
28903
29119
|
}
|
|
28904
|
-
|
|
28905
|
-
|
|
28906
|
-
this._write_out_id = -1;
|
|
28907
|
-
}
|
|
28908
|
-
if (this._retry_id !== -1) {
|
|
28909
|
-
clearInterval(this._retry_id);
|
|
28910
|
-
this._retry_id = -1;
|
|
28911
|
-
}
|
|
29120
|
+
this._write_out_timer.dispose();
|
|
29121
|
+
this._retry_timer.dispose();
|
|
28912
29122
|
}
|
|
28913
29123
|
|
|
28914
29124
|
_estimate_next_tx_time() {
|
|
@@ -28927,6 +29137,13 @@
|
|
|
28927
29137
|
super.write(buf);
|
|
28928
29138
|
|
|
28929
29139
|
if (q.length) this._schedule_write_out();
|
|
29140
|
+
this.scheduleSendCommands();
|
|
29141
|
+
}
|
|
29142
|
+
|
|
29143
|
+
poll() {
|
|
29144
|
+
super.poll();
|
|
29145
|
+
this._write_out_timer.poll();
|
|
29146
|
+
this._retry_timer.poll();
|
|
28930
29147
|
}
|
|
28931
29148
|
|
|
28932
29149
|
_schedule_write_out() {
|
|
@@ -28938,38 +29155,36 @@
|
|
|
28938
29155
|
return;
|
|
28939
29156
|
}
|
|
28940
29157
|
|
|
28941
|
-
|
|
28942
|
-
if (this._write_out_id !== -1) return;
|
|
28943
|
-
|
|
28944
|
-
this._write_out_id = setTimeout(
|
|
28945
|
-
this._write_out_callback,
|
|
28946
|
-
delay - tx_idle_time
|
|
28947
|
-
);
|
|
29158
|
+
this._write_out_timer.scheduleIn(delay - tx_idle_time);
|
|
28948
29159
|
}
|
|
28949
29160
|
|
|
28950
29161
|
_retryCommands() {
|
|
28951
29162
|
const now = this._now();
|
|
28952
29163
|
const retryTime = now - this.retry_interval;
|
|
28953
|
-
// This is an estimate for how many commands we would manage to send
|
|
28954
|
-
// off.
|
|
28955
|
-
const max = 5 * (this.retry_interval / this.delay) - this.q.length;
|
|
28956
29164
|
const pendingCommands = this._pendingCommands;
|
|
29165
|
+
const _sentPendingCommands = this._sentPendingCommands;
|
|
29166
|
+
const _scheduledPendingCommands = this._scheduledPendingCommands;
|
|
28957
29167
|
|
|
28958
|
-
|
|
29168
|
+
let scheduledCount = 0;
|
|
28959
29169
|
const failed = [];
|
|
28960
29170
|
|
|
28961
|
-
for (const
|
|
28962
|
-
const [, pendingCommand] = entry;
|
|
28963
|
-
|
|
28964
|
-
// All later commands are newer than the cutoff.
|
|
29171
|
+
for (const pendingCommand of _sentPendingCommands) {
|
|
28965
29172
|
if (pendingCommand.lastSent > retryTime) break;
|
|
29173
|
+
|
|
29174
|
+
_sentPendingCommands.delete(pendingCommand);
|
|
29175
|
+
|
|
28966
29176
|
if (pendingCommand.retries >= this.retry_count) {
|
|
28967
|
-
failed.push(
|
|
28968
|
-
} else
|
|
28969
|
-
|
|
29177
|
+
failed.push(pendingCommand);
|
|
29178
|
+
} else {
|
|
29179
|
+
_scheduledPendingCommands.add(pendingCommand);
|
|
29180
|
+
scheduledCount++;
|
|
28970
29181
|
}
|
|
28971
29182
|
}
|
|
28972
29183
|
|
|
29184
|
+
if (scheduledCount) {
|
|
29185
|
+
this.scheduleSendCommands();
|
|
29186
|
+
}
|
|
29187
|
+
|
|
28973
29188
|
if (failed.length) {
|
|
28974
29189
|
const timeoutError = new Error('Timeout.');
|
|
28975
29190
|
|
|
@@ -28978,14 +29193,6 @@
|
|
|
28978
29193
|
pendingCommand.reject(timeoutError);
|
|
28979
29194
|
});
|
|
28980
29195
|
}
|
|
28981
|
-
|
|
28982
|
-
retries.forEach(([handle, pendingCommand]) => {
|
|
28983
|
-
pendingCommands.delete(handle);
|
|
28984
|
-
pendingCommands.set(handle, pendingCommand);
|
|
28985
|
-
this.send(pendingCommand.command);
|
|
28986
|
-
pendingCommand.lastSent = now;
|
|
28987
|
-
pendingCommand.retries++;
|
|
28988
|
-
});
|
|
28989
29196
|
}
|
|
28990
29197
|
}
|
|
28991
29198
|
|
|
@@ -29114,6 +29321,10 @@
|
|
|
29114
29321
|
}
|
|
29115
29322
|
return value;
|
|
29116
29323
|
}
|
|
29324
|
+
} else if (typeof value === 'bigint') {
|
|
29325
|
+
return value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER
|
|
29326
|
+
? Number(value)
|
|
29327
|
+
: value.toString();
|
|
29117
29328
|
} else {
|
|
29118
29329
|
return value;
|
|
29119
29330
|
}
|
package/package.json
CHANGED
|
@@ -19,6 +19,14 @@ export class MessageGenerator {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
get bufferedAmount() {
|
|
23
|
+
return this._currentSize;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get batchSize() {
|
|
27
|
+
return this._batchSize;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
add(pdu) {
|
|
23
31
|
const currentSize = this._currentSize;
|
|
24
32
|
const encodedLength = pdu.encoded_length();
|
|
@@ -51,7 +59,7 @@ export class MessageGenerator {
|
|
|
51
59
|
/* Keepalive packets are never combined into one message. */
|
|
52
60
|
this._lastMessageType = messageType;
|
|
53
61
|
|
|
54
|
-
if (this._currentSize > this._batchSize) {
|
|
62
|
+
if (this._currentSize + additionalSize > this._batchSize) {
|
|
55
63
|
this.flush();
|
|
56
64
|
} else if (this._pdus.length === 1) {
|
|
57
65
|
this.scheduleFlush();
|
package/src/connection.d.ts
CHANGED
|
@@ -56,6 +56,23 @@ export declare class Connection extends Events {
|
|
|
56
56
|
*/
|
|
57
57
|
get rx_idle_time(): number;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Amount of bytes currently buffered to be sent out.
|
|
61
|
+
*/
|
|
62
|
+
get bufferedAmount(): number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Amount of bytes which will be batched into single
|
|
66
|
+
* aes70 messages. This can be configured using the
|
|
67
|
+
* `batch` option when creating a connection.
|
|
68
|
+
*/
|
|
69
|
+
get batchSize(): number;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The number of currently pending write operations.
|
|
73
|
+
*/
|
|
74
|
+
get pendingWrites(): number;
|
|
75
|
+
|
|
59
76
|
/**
|
|
60
77
|
* Closes this connection.
|
|
61
78
|
*/
|
package/src/connection.js
CHANGED
|
@@ -3,6 +3,7 @@ import { decodeMessage } from './OCP1/decode_message.js';
|
|
|
3
3
|
import { KeepAlive } from './OCP1/keepalive.js';
|
|
4
4
|
import { MessageGenerator } from './OCP1/message_generator.js';
|
|
5
5
|
import { TimeoutError } from './timeout_error.js';
|
|
6
|
+
import { Timer } from './utils/timer.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Connection base class. It extends :class:`Events` and defines two events:
|
|
@@ -40,8 +41,11 @@ export class Connection extends Events {
|
|
|
40
41
|
this.last_tx_time = now;
|
|
41
42
|
this.rx_bytes = 0;
|
|
42
43
|
this.tx_bytes = 0;
|
|
44
|
+
this._keepalive_timer = new Timer(
|
|
45
|
+
() => this._check_keepalive(),
|
|
46
|
+
() => this._now()
|
|
47
|
+
);
|
|
43
48
|
this.keepalive_interval = -1;
|
|
44
|
-
this._keepalive_interval_id = null;
|
|
45
49
|
this._closed = false;
|
|
46
50
|
this.on('close', () => {
|
|
47
51
|
if (this._closed) return;
|
|
@@ -60,6 +64,18 @@ export class Connection extends Events {
|
|
|
60
64
|
return true;
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
get bufferedAmount() {
|
|
68
|
+
return this._message_generator.bufferedAmount;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get batchSize() {
|
|
72
|
+
return this._message_generator.batchSize;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get pendingWrites() {
|
|
76
|
+
return this._message_generator.bufferedAmount > 0 ? 1 : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
send(pdu) {
|
|
64
80
|
if (this.is_closed()) throw new Error('Connection is closed.');
|
|
65
81
|
this.emit('send', pdu);
|
|
@@ -117,7 +133,7 @@ export class Connection extends Events {
|
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
|
|
120
|
-
this.
|
|
136
|
+
this.poll();
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
incoming(a) {}
|
|
@@ -148,16 +164,18 @@ export class Connection extends Events {
|
|
|
148
164
|
if (this.is_closed()) throw new Error('cleanup() called twice.');
|
|
149
165
|
|
|
150
166
|
// disable keepalive
|
|
151
|
-
this.
|
|
167
|
+
this._keepalive_timer.dispose();
|
|
152
168
|
this._message_generator.dispose();
|
|
153
169
|
this._message_generator = null;
|
|
154
170
|
this.removeAllEventListeners();
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
_check_keepalive() {
|
|
158
|
-
if (
|
|
159
|
-
|
|
174
|
+
if (this.is_closed()) return;
|
|
160
175
|
const t = this.keepalive_interval;
|
|
176
|
+
if (!(t > 0)) return;
|
|
177
|
+
|
|
178
|
+
this._keepalive_timer.scheduleIn(t / 2 + 10);
|
|
161
179
|
|
|
162
180
|
if (this.rx_idle_time() > t * 3) {
|
|
163
181
|
this.emit('timeout');
|
|
@@ -169,6 +187,13 @@ export class Connection extends Events {
|
|
|
169
187
|
}
|
|
170
188
|
}
|
|
171
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Check if some regular internal timers must run.
|
|
192
|
+
*/
|
|
193
|
+
poll() {
|
|
194
|
+
this._keepalive_timer.poll();
|
|
195
|
+
}
|
|
196
|
+
|
|
172
197
|
/**
|
|
173
198
|
* Flush write buffers. This are usually PDUs or may also be unwritten
|
|
174
199
|
* buffers.
|
|
@@ -194,11 +219,6 @@ export class Connection extends Events {
|
|
|
194
219
|
|
|
195
220
|
const t = seconds * 1000;
|
|
196
221
|
|
|
197
|
-
if (this._keepalive_interval_id !== null) {
|
|
198
|
-
clearInterval(this._keepalive_interval_id);
|
|
199
|
-
this._keepalive_interval_id = null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
222
|
this.keepalive_interval = t;
|
|
203
223
|
|
|
204
224
|
// Notify the other side about our new keepalive
|
|
@@ -206,11 +226,11 @@ export class Connection extends Events {
|
|
|
206
226
|
|
|
207
227
|
this.send(new KeepAlive(t));
|
|
208
228
|
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
}
|
|
229
|
+
if (t > 0) {
|
|
230
|
+
// we check twice as often to make sure we stay within the timers
|
|
231
|
+
this._keepalive_timer.scheduleIn(t / 2 + 10);
|
|
232
|
+
} else {
|
|
233
|
+
this._keepalive_timer.stop();
|
|
234
|
+
}
|
|
215
235
|
}
|
|
216
236
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-env node */
|
|
2
|
+
import { Timer } from '../utils/timer.js';
|
|
2
3
|
import { ClientConnection } from './client_connection.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -17,15 +18,20 @@ export class AbstractUDPConnection extends ClientConnection {
|
|
|
17
18
|
this.retry_interval =
|
|
18
19
|
options.retry_interval >= 0 ? options.retry_interval : 250;
|
|
19
20
|
this.retry_count = options.retry_count >= 0 ? options.retry_count : 3;
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
this._write_out_timer = new Timer(
|
|
22
|
+
() => {
|
|
23
|
+
this._write_out();
|
|
24
|
+
},
|
|
25
|
+
() => this._now()
|
|
26
|
+
);
|
|
27
|
+
this._retry_timer = new Timer(
|
|
28
|
+
() => {
|
|
29
|
+
this._retryCommands();
|
|
30
|
+
this._retry_timer.scheduleIn(this.retry_interval);
|
|
31
|
+
},
|
|
32
|
+
() => this._now()
|
|
33
|
+
);
|
|
34
|
+
this._retry_timer.scheduleIn(this.retry_interval);
|
|
29
35
|
this.q = [];
|
|
30
36
|
socket.onmessage = (buffer) => {
|
|
31
37
|
try {
|
|
@@ -43,6 +49,24 @@ export class AbstractUDPConnection extends ClientConnection {
|
|
|
43
49
|
return false;
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
get bufferedAmount() {
|
|
53
|
+
let amount = super.bufferedAmount;
|
|
54
|
+
|
|
55
|
+
for (const buf of this.q) {
|
|
56
|
+
amount += buf.byteLength;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return amount;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get pendingWrites() {
|
|
63
|
+
return super.pendingWrites + this.q.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
shouldSendMoreCommands() {
|
|
67
|
+
return this.q.length < 3;
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
/**
|
|
47
71
|
* Connect to the given endpoint.
|
|
48
72
|
*
|
|
@@ -112,14 +136,8 @@ export class AbstractUDPConnection extends ClientConnection {
|
|
|
112
136
|
this.socket.close();
|
|
113
137
|
this.socket = null;
|
|
114
138
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this._write_out_id = -1;
|
|
118
|
-
}
|
|
119
|
-
if (this._retry_id !== -1) {
|
|
120
|
-
clearInterval(this._retry_id);
|
|
121
|
-
this._retry_id = -1;
|
|
122
|
-
}
|
|
139
|
+
this._write_out_timer.dispose();
|
|
140
|
+
this._retry_timer.dispose();
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
_estimate_next_tx_time() {
|
|
@@ -138,6 +156,13 @@ export class AbstractUDPConnection extends ClientConnection {
|
|
|
138
156
|
super.write(buf);
|
|
139
157
|
|
|
140
158
|
if (q.length) this._schedule_write_out();
|
|
159
|
+
this.scheduleSendCommands();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
poll() {
|
|
163
|
+
super.poll();
|
|
164
|
+
this._write_out_timer.poll();
|
|
165
|
+
this._retry_timer.poll();
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
_schedule_write_out() {
|
|
@@ -149,38 +174,36 @@ export class AbstractUDPConnection extends ClientConnection {
|
|
|
149
174
|
return;
|
|
150
175
|
}
|
|
151
176
|
|
|
152
|
-
|
|
153
|
-
if (this._write_out_id !== -1) return;
|
|
154
|
-
|
|
155
|
-
this._write_out_id = setTimeout(
|
|
156
|
-
this._write_out_callback,
|
|
157
|
-
delay - tx_idle_time
|
|
158
|
-
);
|
|
177
|
+
this._write_out_timer.scheduleIn(delay - tx_idle_time);
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
_retryCommands() {
|
|
162
181
|
const now = this._now();
|
|
163
182
|
const retryTime = now - this.retry_interval;
|
|
164
|
-
// This is an estimate for how many commands we would manage to send
|
|
165
|
-
// off.
|
|
166
|
-
const max = 5 * (this.retry_interval / this.delay) - this.q.length;
|
|
167
183
|
const pendingCommands = this._pendingCommands;
|
|
184
|
+
const _sentPendingCommands = this._sentPendingCommands;
|
|
185
|
+
const _scheduledPendingCommands = this._scheduledPendingCommands;
|
|
168
186
|
|
|
169
|
-
|
|
187
|
+
let scheduledCount = 0;
|
|
170
188
|
const failed = [];
|
|
171
189
|
|
|
172
|
-
for (const
|
|
173
|
-
const [, pendingCommand] = entry;
|
|
174
|
-
|
|
175
|
-
// All later commands are newer than the cutoff.
|
|
190
|
+
for (const pendingCommand of _sentPendingCommands) {
|
|
176
191
|
if (pendingCommand.lastSent > retryTime) break;
|
|
192
|
+
|
|
193
|
+
_sentPendingCommands.delete(pendingCommand);
|
|
194
|
+
|
|
177
195
|
if (pendingCommand.retries >= this.retry_count) {
|
|
178
|
-
failed.push(
|
|
179
|
-
} else
|
|
180
|
-
|
|
196
|
+
failed.push(pendingCommand);
|
|
197
|
+
} else {
|
|
198
|
+
_scheduledPendingCommands.add(pendingCommand);
|
|
199
|
+
scheduledCount++;
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
|
|
203
|
+
if (scheduledCount) {
|
|
204
|
+
this.scheduleSendCommands();
|
|
205
|
+
}
|
|
206
|
+
|
|
184
207
|
if (failed.length) {
|
|
185
208
|
const timeoutError = new Error('Timeout.');
|
|
186
209
|
|
|
@@ -189,13 +212,5 @@ export class AbstractUDPConnection extends ClientConnection {
|
|
|
189
212
|
pendingCommand.reject(timeoutError);
|
|
190
213
|
});
|
|
191
214
|
}
|
|
192
|
-
|
|
193
|
-
retries.forEach(([handle, pendingCommand]) => {
|
|
194
|
-
pendingCommands.delete(handle);
|
|
195
|
-
pendingCommands.set(handle, pendingCommand);
|
|
196
|
-
this.send(pendingCommand.command);
|
|
197
|
-
pendingCommand.lastSent = now;
|
|
198
|
-
pendingCommand.retries++;
|
|
199
|
-
});
|
|
200
215
|
}
|
|
201
216
|
}
|
|
@@ -11,6 +11,7 @@ import { EncodedArguments } from '../OCP1/encoded_arguments.js';
|
|
|
11
11
|
import { CloseError } from '../close_error.js';
|
|
12
12
|
import { Subscriptions } from '../utils/subscriptions.js';
|
|
13
13
|
import { subscribeEvent } from '../utils/subscribeEvent.js';
|
|
14
|
+
import { Timer } from '../utils/timer.js';
|
|
14
15
|
|
|
15
16
|
class PendingCommand {
|
|
16
17
|
get handle() {
|
|
@@ -90,17 +91,57 @@ function eventToKey(event) {
|
|
|
90
91
|
export class ClientConnection extends Connection {
|
|
91
92
|
constructor(options) {
|
|
92
93
|
super(options);
|
|
94
|
+
// All pending commands by id/handle
|
|
93
95
|
this._pendingCommands = new Map();
|
|
96
|
+
// All pending commands scheduled to be sent.
|
|
97
|
+
this._scheduledPendingCommands = new Set();
|
|
98
|
+
// All pending commands wich have been sent.
|
|
99
|
+
this._sentPendingCommands = new Set();
|
|
94
100
|
this._nextCommandHandle = 0;
|
|
95
101
|
this._subscribers = new Map();
|
|
102
|
+
this._sendCommandsTimer = new Timer(
|
|
103
|
+
() => {
|
|
104
|
+
this.sendCommands();
|
|
105
|
+
},
|
|
106
|
+
() => this._now()
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
shouldSendMoreCommands() {
|
|
111
|
+
return this.is_reliable;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
sendCommands() {
|
|
115
|
+
const { _scheduledPendingCommands, _sentPendingCommands } = this;
|
|
116
|
+
|
|
117
|
+
for (const pendingCommand of _scheduledPendingCommands) {
|
|
118
|
+
if (!this.shouldSendMoreCommands()) break;
|
|
119
|
+
_scheduledPendingCommands.delete(pendingCommand);
|
|
120
|
+
_sentPendingCommands.add(pendingCommand);
|
|
121
|
+
this.send(pendingCommand.command);
|
|
122
|
+
pendingCommand.lastSent = this._now();
|
|
123
|
+
pendingCommand.retries++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
scheduleSendCommands() {
|
|
128
|
+
this._sendCommandsTimer.scheduleDeadlineIn(5);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
poll() {
|
|
132
|
+
super.poll();
|
|
133
|
+
this._sendCommandsTimer.poll();
|
|
96
134
|
}
|
|
97
135
|
|
|
98
136
|
cleanup(error) {
|
|
99
137
|
super.cleanup(error);
|
|
138
|
+
this._sendCommandsTimer.dispose();
|
|
100
139
|
const subscribers = this._subscribers;
|
|
101
140
|
this._subscribers = null;
|
|
102
141
|
const pendingCommands = this._pendingCommands;
|
|
103
142
|
this._pendingCommands = null;
|
|
143
|
+
this._scheduledPendingCommands.clear();
|
|
144
|
+
this._sentPendingCommands.clear();
|
|
104
145
|
|
|
105
146
|
const e = new CloseError(error);
|
|
106
147
|
pendingCommands.forEach((pendingCommand, id) => {
|
|
@@ -179,9 +220,8 @@ export class ClientConnection extends Connection {
|
|
|
179
220
|
);
|
|
180
221
|
|
|
181
222
|
this._pendingCommands.set(handle, pendingCommand);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.send(command);
|
|
223
|
+
this._scheduledPendingCommands.add(pendingCommand);
|
|
224
|
+
this.scheduleSendCommands();
|
|
185
225
|
};
|
|
186
226
|
|
|
187
227
|
if (callback) {
|
|
@@ -202,6 +242,9 @@ export class ClientConnection extends Connection {
|
|
|
202
242
|
|
|
203
243
|
pendingCommands.delete(handle);
|
|
204
244
|
|
|
245
|
+
if (!this._sentPendingCommands.delete(pendingCommand))
|
|
246
|
+
this._scheduledPendingCommands.delete(pendingCommand);
|
|
247
|
+
|
|
205
248
|
return pendingCommand;
|
|
206
249
|
}
|
|
207
250
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
function isItTime(target, now) {
|
|
2
|
+
// We are ok with 1ms accuracy.
|
|
3
|
+
return target - now < 1;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class Timer {
|
|
7
|
+
constructor(callback, getNow) {
|
|
8
|
+
this._callback = callback;
|
|
9
|
+
this._getNow = getNow;
|
|
10
|
+
this._targetTime = undefined;
|
|
11
|
+
this._timerId = undefined;
|
|
12
|
+
this._timerAt = undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
poll() {
|
|
16
|
+
const now = this._getNow();
|
|
17
|
+
|
|
18
|
+
if (this._targetTime === undefined) return;
|
|
19
|
+
|
|
20
|
+
if (isItTime(this._targetTime, now)) {
|
|
21
|
+
this._targetTime = undefined;
|
|
22
|
+
try {
|
|
23
|
+
this._callback();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Timer callback threw an exception', err);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
this._reschedule();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_reschedule() {
|
|
33
|
+
const target = this._targetTime;
|
|
34
|
+
const interval = target - this._getNow();
|
|
35
|
+
|
|
36
|
+
if (this._timerId !== undefined) {
|
|
37
|
+
if (target >= this._timerAt) {
|
|
38
|
+
// The timer will fire before target. We will then reschedule it.
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clearTimeout(this._timerId);
|
|
43
|
+
this._timerId = undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this._timerAt = target;
|
|
47
|
+
this._timerId = setTimeout(() => {
|
|
48
|
+
this._timerId = undefined;
|
|
49
|
+
this._timerAt = undefined;
|
|
50
|
+
this.poll();
|
|
51
|
+
}, Math.max(0, interval));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
* @param {number} interval
|
|
57
|
+
* Interval in milliseconds.
|
|
58
|
+
*/
|
|
59
|
+
scheduleIn(interval) {
|
|
60
|
+
if (!(interval >= 0)) {
|
|
61
|
+
throw new TypeError(`Expected positive interval.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this._targetTime = this._getNow() + interval;
|
|
65
|
+
this._reschedule();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Schedule the timer in a given number of milliseconds. If the timer
|
|
70
|
+
* is already running and scheduled to run before, do not modify it.
|
|
71
|
+
*
|
|
72
|
+
* @param {number} interval
|
|
73
|
+
*/
|
|
74
|
+
scheduleDeadlineIn(interval) {
|
|
75
|
+
if (!(interval >= 0)) {
|
|
76
|
+
throw new TypeError(`Expected positive interval.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const target = this._getNow() + interval;
|
|
80
|
+
|
|
81
|
+
if (this._targetTime !== undefined && this._targetTime <= target) {
|
|
82
|
+
this.poll();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.scheduleAt(target);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
*
|
|
91
|
+
* @param {number} target
|
|
92
|
+
* Target time in milliseconds.
|
|
93
|
+
*/
|
|
94
|
+
scheduleAt(target) {
|
|
95
|
+
if (!(target >= 0)) {
|
|
96
|
+
throw new TypeError();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._targetTime = target;
|
|
100
|
+
this._reschedule();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
stop() {
|
|
104
|
+
this._targetTime = undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
cancel() {
|
|
108
|
+
this.stop();
|
|
109
|
+
this._clearTimeout();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_clearTimeout() {
|
|
113
|
+
if (this._timerId) {
|
|
114
|
+
clearTimeout(this._timerId);
|
|
115
|
+
this._timerId = undefined;
|
|
116
|
+
this._timerAt = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
dispose() {
|
|
121
|
+
this.cancel();
|
|
122
|
+
}
|
|
123
|
+
}
|