@schukai/monster 3.2.0 → 3.3.0
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/package.json +1 -1
- package/source/data/datasource/websocket.mjs +139 -206
- package/source/net/webconnect/message.mjs +55 -0
- package/source/net/webconnect.mjs +346 -0
- package/source/types/observablequeue.mjs +110 -0
- package/source/types/proxyobserver.mjs +2 -2
- package/source/types/uniquequeue.mjs +9 -6
- package/source/types/version.mjs +1 -1
- package/test/cases/data/datasource/websocket.mjs +118 -33
- package/test/cases/monster.mjs +1 -1
- package/test/cases/net/webconnect/message.mjs +50 -0
- package/test/cases/net/webconnect.mjs +116 -0
- package/test/cases/types/observablequeue.mjs +17 -0
- package/test/cases/types/queue.mjs +4 -1
package/package.json
CHANGED
|
@@ -6,20 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {internalSymbol, instanceSymbol} from "../../constants.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import {isString, isObject} from "../../types/is.mjs";
|
|
10
|
+
import {WebConnect} from "../../net/webconnect.mjs";
|
|
11
|
+
import {Message} from "../../net/webconnect/message.mjs";
|
|
11
12
|
import {Datasource} from "../datasource.mjs";
|
|
12
13
|
import {Pathfinder} from "../pathfinder.mjs";
|
|
13
14
|
import {Pipe} from "../pipe.mjs";
|
|
14
15
|
|
|
15
16
|
export {WebSocketDatasource}
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
* @private
|
|
19
|
-
* @type {Symbol}
|
|
20
|
-
*/
|
|
21
|
-
const receiveQueueSymbol = Symbol("queue");
|
|
22
|
-
|
|
23
18
|
|
|
24
19
|
/**
|
|
25
20
|
* @private
|
|
@@ -27,122 +22,33 @@ const receiveQueueSymbol = Symbol("queue");
|
|
|
27
22
|
*
|
|
28
23
|
* hint: this name is used in the tests. if you want to change it, please change it in the tests as well.
|
|
29
24
|
*/
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @private
|
|
34
|
-
* @type {symbol}
|
|
35
|
-
*/
|
|
36
|
-
const manualCloseSymbol = Symbol("manualClose");
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
|
|
40
|
-
* @type {{"1000": string, "1011": string, "1010": string, "1008": string, "1007": string, "1006": string, "1005": string, "1004": string, "1015": string, "1003": string, "1002": string, "1001": string, "1009": string}}
|
|
41
|
-
*/
|
|
42
|
-
const connectionStatusCode = {
|
|
43
|
-
1000: "Normal closure",
|
|
44
|
-
1001: "Going away",
|
|
45
|
-
1002: "Protocol error",
|
|
46
|
-
1003: "Unsupported data",
|
|
47
|
-
1004: "Reserved",
|
|
48
|
-
1005: "No status code",
|
|
49
|
-
1006: "Connection closed abnormally",
|
|
50
|
-
1007: "Invalid frame payload data",
|
|
51
|
-
1008: "Policy violation",
|
|
52
|
-
1009: "Message too big",
|
|
53
|
-
1010: "Mandatory extension",
|
|
54
|
-
1011: "Internal server error",
|
|
55
|
-
1015: "TLS handshake"
|
|
56
|
-
};
|
|
25
|
+
const webConnectSymbol = Symbol("connection");
|
|
57
26
|
|
|
58
27
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
61
|
-
* @
|
|
28
|
+
*
|
|
29
|
+
* @param self
|
|
30
|
+
* @param obj
|
|
31
|
+
* @returns {*}
|
|
62
32
|
*/
|
|
63
|
-
function
|
|
33
|
+
function doTransform(type, obj) {
|
|
64
34
|
const self = this;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
let transformation = self.getOption(type + '.mapping.transformer');
|
|
36
|
+
if (transformation !== undefined) {
|
|
37
|
+
const pipe = new Pipe(transformation);
|
|
38
|
+
const callbacks = self.getOption(type + '.mapping.callbacks')
|
|
39
|
+
|
|
40
|
+
if (isObject(callbacks)) {
|
|
41
|
+
for (const key in callbacks) {
|
|
42
|
+
if (callbacks.hasOwnProperty(key) && typeof callbacks[key] === 'function') {
|
|
43
|
+
pipe.setCallback(key, callbacks[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
75
46
|
}
|
|
76
|
-
reject(new Error("Connection timeout"));
|
|
77
|
-
}, connectionTimeout);
|
|
78
|
-
|
|
79
|
-
let reconnectTimeout = self.getOption('connection.reconnect.timeout');
|
|
80
|
-
if (!isInteger(reconnectTimeout) || reconnectTimeout < 1000) reconnectTimeout = 1000;
|
|
81
|
-
let reconnectAttempts = self.getOption('connection.reconnect.attempts');
|
|
82
|
-
if (!isInteger(reconnectAttempts) || reconnectAttempts < 1) reconnectAttempts = 1;
|
|
83
|
-
let reconnectEnabled = self.getOption('connection.reconnect.enabled');
|
|
84
|
-
if (reconnectEnabled !== true) reconnectEnabled = false;
|
|
85
|
-
|
|
86
|
-
self[manualCloseSymbol] = false;
|
|
87
|
-
self[connectionSymbol].reconnectCounter++;
|
|
88
|
-
|
|
89
|
-
if (self[connectionSymbol].socket && self[connectionSymbol].socket.readyState < 2) {
|
|
90
|
-
self[connectionSymbol].socket.close();
|
|
91
|
-
}
|
|
92
|
-
self[connectionSymbol].socket = null;
|
|
93
47
|
|
|
94
|
-
|
|
95
|
-
if (!url) {
|
|
96
|
-
reject('No url defined for websocket datasource.');
|
|
97
|
-
return;
|
|
48
|
+
obj = pipe.run(obj);
|
|
98
49
|
}
|
|
99
50
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
self[connectionSymbol].socket.onmessage = function (event) {
|
|
103
|
-
self[receiveQueueSymbol].add(event);
|
|
104
|
-
setTimeout(function () {
|
|
105
|
-
self.read();
|
|
106
|
-
}, 1);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
self[connectionSymbol].socket.onopen = function () {
|
|
110
|
-
self[connectionSymbol].reconnectCounter = 0;
|
|
111
|
-
if (typeof resolve === 'function' && !promiseAllredyResolved) {
|
|
112
|
-
promiseAllredyResolved = true;
|
|
113
|
-
resolve();
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
self[connectionSymbol].socket.close = function (event) {
|
|
118
|
-
|
|
119
|
-
if (self[manualCloseSymbol]) {
|
|
120
|
-
self[manualCloseSymbol] = false;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (reconnectEnabled && this[connectionSymbol].reconnectCounter < reconnectAttempts) {
|
|
125
|
-
setTimeout(() => {
|
|
126
|
-
self.connect();
|
|
127
|
-
}, reconnectTimeout * this[connectionSymbol].reconnectCounter);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
self[connectionSymbol].socket.onerror = (error) => {
|
|
133
|
-
|
|
134
|
-
if (reconnectEnabled && self[connectionSymbol].reconnectCounter < reconnectAttempts) {
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
self.connect();
|
|
137
|
-
}, reconnectTimeout * this[connectionSymbol].reconnectCounter);
|
|
138
|
-
} else {
|
|
139
|
-
if (typeof reject === 'function' && !promiseAllredyResolved) {
|
|
140
|
-
promiseAllredyResolved = true;
|
|
141
|
-
reject(error);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
};
|
|
51
|
+
return obj;
|
|
146
52
|
}
|
|
147
53
|
|
|
148
54
|
/**
|
|
@@ -163,6 +69,8 @@ class WebSocketDatasource extends Datasource {
|
|
|
163
69
|
*/
|
|
164
70
|
constructor(options) {
|
|
165
71
|
super();
|
|
72
|
+
|
|
73
|
+
const self = this;
|
|
166
74
|
|
|
167
75
|
if (isString(options)) {
|
|
168
76
|
options = {url: options};
|
|
@@ -170,12 +78,17 @@ class WebSocketDatasource extends Datasource {
|
|
|
170
78
|
|
|
171
79
|
if (!isObject(options)) options = {};
|
|
172
80
|
this.setOptions(options);
|
|
173
|
-
this[
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
81
|
+
this[webConnectSymbol] = new WebConnect({
|
|
82
|
+
url: self.getOption('url'),
|
|
83
|
+
connection: {
|
|
84
|
+
timeout: self.getOption('connection.timeout'),
|
|
85
|
+
reconnect: {
|
|
86
|
+
timeout: self.getOption('connection.reconnect.timeout'),
|
|
87
|
+
attempts: self.getOption('connection.reconnect.attempts'),
|
|
88
|
+
enabled: self.getOption('connection.reconnect.enabled')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
179
92
|
}
|
|
180
93
|
|
|
181
94
|
/**
|
|
@@ -183,18 +96,14 @@ class WebSocketDatasource extends Datasource {
|
|
|
183
96
|
* @returns {Promise}
|
|
184
97
|
*/
|
|
185
98
|
connect() {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return new Promise((resolve, reject) => {
|
|
189
|
-
connectServer.call(this, resolve, reject);
|
|
190
|
-
});
|
|
99
|
+
return this[webConnectSymbol].connect();
|
|
191
100
|
}
|
|
192
101
|
|
|
193
102
|
/**
|
|
194
103
|
* @returns {boolean}
|
|
195
104
|
*/
|
|
196
105
|
isConnected() {
|
|
197
|
-
|
|
106
|
+
return this[webConnectSymbol].isConnected();
|
|
198
107
|
}
|
|
199
108
|
|
|
200
109
|
/**
|
|
@@ -216,12 +125,11 @@ class WebSocketDatasource extends Datasource {
|
|
|
216
125
|
* @property {Object} write.mapping the mapping is applied before writing.
|
|
217
126
|
* @property {String} write.mapping.transformer Transformer to select the appropriate entries
|
|
218
127
|
* @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
|
|
219
|
-
* @property {Object} write.report
|
|
220
|
-
* @property {String} write.report.path Path to validations
|
|
221
128
|
* @property {Object} write.sheathing
|
|
222
129
|
* @property {Object} write.sheathing.object Object to be wrapped
|
|
223
130
|
* @property {string} write.sheathing.path Path to the data
|
|
224
131
|
* @property {Object} read={} Options
|
|
132
|
+
* @property {String} read.path Path to data
|
|
225
133
|
* @property {Object} read.mapping the mapping is applied after reading.
|
|
226
134
|
* @property {String} read.mapping.transformer Transformer to select the appropriate entries
|
|
227
135
|
* @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading.
|
|
@@ -232,10 +140,7 @@ class WebSocketDatasource extends Datasource {
|
|
|
232
140
|
write: {
|
|
233
141
|
mapping: {
|
|
234
142
|
transformer: undefined,
|
|
235
|
-
callbacks:
|
|
236
|
-
},
|
|
237
|
-
report: {
|
|
238
|
-
path: undefined
|
|
143
|
+
callbacks: {}
|
|
239
144
|
},
|
|
240
145
|
sheathing: {
|
|
241
146
|
object: undefined,
|
|
@@ -245,8 +150,9 @@ class WebSocketDatasource extends Datasource {
|
|
|
245
150
|
read: {
|
|
246
151
|
mapping: {
|
|
247
152
|
transformer: undefined,
|
|
248
|
-
callbacks:
|
|
153
|
+
callbacks: {}
|
|
249
154
|
},
|
|
155
|
+
path: undefined,
|
|
250
156
|
},
|
|
251
157
|
connection: {
|
|
252
158
|
timeout: 5000,
|
|
@@ -265,11 +171,7 @@ class WebSocketDatasource extends Datasource {
|
|
|
265
171
|
* @returns {Promise}
|
|
266
172
|
*/
|
|
267
173
|
close() {
|
|
268
|
-
this[
|
|
269
|
-
if (this[connectionSymbol].socket) {
|
|
270
|
-
this[connectionSymbol].socket.close();
|
|
271
|
-
}
|
|
272
|
-
return this;
|
|
174
|
+
return this[webConnectSymbol].close();
|
|
273
175
|
}
|
|
274
176
|
|
|
275
177
|
/**
|
|
@@ -277,99 +179,130 @@ class WebSocketDatasource extends Datasource {
|
|
|
277
179
|
*/
|
|
278
180
|
read() {
|
|
279
181
|
const self = this;
|
|
280
|
-
let response;
|
|
281
|
-
|
|
282
|
-
if (self[connectionSymbol]?.socket?.readyState !== 1) {
|
|
283
|
-
return Promise.reject('The connection is not established.');
|
|
284
|
-
}
|
|
285
182
|
|
|
286
183
|
return new Promise((resolve, reject) => {
|
|
287
|
-
if (self[receiveQueueSymbol].isEmpty()) {
|
|
288
|
-
resolve();
|
|
289
|
-
}
|
|
290
184
|
|
|
291
|
-
while (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
let obj;
|
|
298
|
-
try {
|
|
299
|
-
obj = JSON.parse(body);
|
|
300
|
-
} catch (e) {
|
|
301
|
-
|
|
302
|
-
let msg = 'the response does not contain a valid json (actual: ';
|
|
303
|
-
|
|
304
|
-
if (body.length > 100) {
|
|
305
|
-
msg += body.substring(0, 97) + '...';
|
|
306
|
-
} else {
|
|
307
|
-
msg += body;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
msg += "; " + e.message + ')';
|
|
185
|
+
while (this[webConnectSymbol].dataReceived() === true) {
|
|
186
|
+
let obj = this[webConnectSymbol].poll();
|
|
187
|
+
if (!isObject(obj)) {
|
|
188
|
+
reject(new Error('The received data is not an object.'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
311
191
|
|
|
312
|
-
|
|
192
|
+
if (!(obj instanceof Message)) {
|
|
193
|
+
reject(new Error('The received data is not a Message.'));
|
|
194
|
+
return;
|
|
313
195
|
}
|
|
314
196
|
|
|
315
|
-
|
|
316
|
-
if (transformation !== undefined) {
|
|
317
|
-
const pipe = new Pipe(transformation);
|
|
197
|
+
obj = obj.getData();
|
|
318
198
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
199
|
+
obj = self.transformServerPayload.call(self, obj);
|
|
200
|
+
self.set( obj);
|
|
201
|
+
}
|
|
322
202
|
|
|
323
|
-
|
|
324
|
-
}
|
|
203
|
+
resolve(self.get());
|
|
325
204
|
|
|
326
|
-
self.set(obj);
|
|
327
|
-
return response;
|
|
328
|
-
}
|
|
329
205
|
})
|
|
330
|
-
|
|
206
|
+
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// const self = this;
|
|
210
|
+
// let response;
|
|
211
|
+
//
|
|
212
|
+
// if (self[webConnectSymbol]?.socket?.readyState !== 1) {
|
|
213
|
+
// return Promise.reject('The connection is not established.');
|
|
214
|
+
// }
|
|
215
|
+
//
|
|
216
|
+
// return new Promise((resolve, reject) => {
|
|
217
|
+
// if (self[receiveQueueSymbol].isEmpty()) {
|
|
218
|
+
// resolve();
|
|
219
|
+
// }
|
|
220
|
+
//
|
|
221
|
+
// while (!self[receiveQueueSymbol].isEmpty()) {
|
|
222
|
+
//
|
|
223
|
+
// const event = self[receiveQueueSymbol].poll();
|
|
224
|
+
// const body = event?.data;
|
|
225
|
+
// if (!body) continue;
|
|
226
|
+
//
|
|
227
|
+
// let obj;
|
|
228
|
+
// try {
|
|
229
|
+
// obj = JSON.parse(body);
|
|
230
|
+
// } catch (e) {
|
|
231
|
+
//
|
|
232
|
+
// let msg = 'the response does not contain a valid json (actual: ';
|
|
233
|
+
//
|
|
234
|
+
// if (body.length > 100) {
|
|
235
|
+
// msg += body.substring(0, 97) + '...';
|
|
236
|
+
// } else {
|
|
237
|
+
// msg += body;
|
|
238
|
+
// }
|
|
239
|
+
//
|
|
240
|
+
// msg += "; " + e.message + ')';
|
|
241
|
+
//
|
|
242
|
+
// reject(msg);
|
|
243
|
+
// return;
|
|
244
|
+
// }
|
|
245
|
+
//
|
|
246
|
+
// obj = self.transformServerPayload.call(self, obj);
|
|
247
|
+
//
|
|
248
|
+
//
|
|
249
|
+
// self.set(obj);
|
|
250
|
+
// return response;
|
|
251
|
+
// }
|
|
252
|
+
// })
|
|
253
|
+
//}
|
|
331
254
|
|
|
332
255
|
/**
|
|
333
|
-
*
|
|
256
|
+
* This prepares the data that comes from the server.
|
|
257
|
+
* Should not be called directly.
|
|
258
|
+
*
|
|
259
|
+
* @private
|
|
260
|
+
* @param {Object} payload
|
|
261
|
+
* @returns {Object}
|
|
334
262
|
*/
|
|
335
|
-
|
|
263
|
+
transformServerPayload(payload) {
|
|
336
264
|
const self = this;
|
|
265
|
+
payload = doTransform.call(self, 'read', payload);
|
|
337
266
|
|
|
338
|
-
|
|
339
|
-
|
|
267
|
+
const dataPath = self.getOption('read.path');
|
|
268
|
+
if (dataPath) {
|
|
269
|
+
payload = (new Pathfinder(payload)).getVia(dataPath);
|
|
340
270
|
}
|
|
341
271
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (transformation !== undefined) {
|
|
345
|
-
const pipe = new Pipe(transformation);
|
|
272
|
+
return payload;
|
|
273
|
+
}
|
|
346
274
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
275
|
+
/**
|
|
276
|
+
* This prepares the data for writing and should not be called directly.
|
|
277
|
+
*
|
|
278
|
+
* @private
|
|
279
|
+
* @param {Object} payload
|
|
280
|
+
* @returns {Object}
|
|
281
|
+
*/
|
|
282
|
+
prepareServerPayload(payload) {
|
|
283
|
+
const self = this;
|
|
350
284
|
|
|
351
|
-
|
|
352
|
-
}
|
|
285
|
+
payload = doTransform.call(self, 'write', payload);
|
|
353
286
|
|
|
354
287
|
let sheathingObject = self.getOption('write.sheathing.object');
|
|
355
288
|
let sheathingPath = self.getOption('write.sheathing.path');
|
|
356
|
-
let reportPath = self.getOption('write.report.path');
|
|
357
289
|
|
|
358
290
|
if (sheathingObject && sheathingPath) {
|
|
359
|
-
const sub =
|
|
360
|
-
|
|
361
|
-
(new Pathfinder(
|
|
291
|
+
const sub = payload;
|
|
292
|
+
payload = sheathingObject;
|
|
293
|
+
(new Pathfinder(payload)).setVia(sheathingPath, sub);
|
|
362
294
|
}
|
|
363
295
|
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
if (self[connectionSymbol].socket.readyState !== 1) {
|
|
367
|
-
reject('the socket is not ready');
|
|
368
|
-
}
|
|
296
|
+
return payload;
|
|
297
|
+
}
|
|
369
298
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
299
|
+
/**
|
|
300
|
+
* @return {Promise}
|
|
301
|
+
*/
|
|
302
|
+
write() {
|
|
303
|
+
const self = this;
|
|
304
|
+
let obj = self.prepareServerPayload(self.get());
|
|
305
|
+
return self[webConnectSymbol].send(obj)
|
|
373
306
|
}
|
|
374
307
|
|
|
375
308
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright schukai GmbH and contributors 2022. All Rights Reserved.
|
|
3
|
+
* Node module: @schukai/monster
|
|
4
|
+
* This file is licensed under the AGPLv3 License.
|
|
5
|
+
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {Base} from "../../types/base.mjs";
|
|
9
|
+
import {validateObject, validateString} from "../../types/validate.mjs";
|
|
10
|
+
|
|
11
|
+
export {Message}
|
|
12
|
+
|
|
13
|
+
const dataSymbol = Symbol("@@data");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This class represents a WebSocket message.
|
|
17
|
+
*/
|
|
18
|
+
class Message extends Base {
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} data
|
|
22
|
+
* @throws {TypeError} value is not a object
|
|
23
|
+
*/
|
|
24
|
+
constructor(data) {
|
|
25
|
+
super();
|
|
26
|
+
this[dataSymbol] = validateObject(data);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the raw message.
|
|
31
|
+
*
|
|
32
|
+
* @returns {object}
|
|
33
|
+
*/
|
|
34
|
+
getData() {
|
|
35
|
+
return this[dataSymbol];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @returns {*}
|
|
40
|
+
*/
|
|
41
|
+
toJSON() {
|
|
42
|
+
return this[dataSymbol];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} json
|
|
47
|
+
* @returns {Message}
|
|
48
|
+
* @throws {TypeError} value is not a string
|
|
49
|
+
*/
|
|
50
|
+
static fromJSON(json) {
|
|
51
|
+
validateString(json);
|
|
52
|
+
return new Message(JSON.parse(json));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
}
|