@schukai/monster 3.0.0 → 3.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schukai/monster",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Monster is a simple library for creating fast, robust and lightweight websites.",
5
5
  "keywords": [
6
6
  "framework",
@@ -22,7 +22,7 @@ export {RestAPI}
22
22
  * @since 1.22.0
23
23
  * @copyright schukai GmbH
24
24
  * @memberOf Monster.Data.Datasource
25
- * @summary The LocalStorage class encapsulates the access to data objects.
25
+ * @summary The RestAPI is a class that binds a REST API server.
26
26
  */
27
27
  class RestAPI extends Datasource {
28
28
 
@@ -30,7 +30,6 @@ class RestAPI extends Datasource {
30
30
  *
31
31
  * @param {Object} [readDefinition] An options object containing any custom settings that you want to apply to the read request.
32
32
  * @param {Object} [writeDefinition] An options object containing any custom settings that you want to apply to the write request.
33
- * @throws {TypeError} value is not a string
34
33
  */
35
34
  constructor(readDefinition, writeDefinition) {
36
35
  super();
@@ -54,17 +53,21 @@ class RestAPI extends Datasource {
54
53
  }
55
54
 
56
55
  /**
57
- * @property {string} url=undefined Defines the resource that you wish to fetch.
58
56
  * @property {Object} write={} Options
59
57
  * @property {Object} write.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor}
60
58
  * @property {string} write.init.method=POST
59
+ * @property {Object} write.init.headers Object containing any custom headers that you want to apply to the request.
61
60
  * @property {string} write.acceptedStatus=[200,201]
62
61
  * @property {string} write.url URL
63
62
  * @property {Object} write.mapping the mapping is applied before writing.
64
63
  * @property {String} write.mapping.transformer Transformer to select the appropriate entries
64
+ * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
65
65
  * @property {Object} write.report
66
66
  * @property {String} write.report.path Path to validations
67
- * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
67
+ * @property {Object} write.sheathing
68
+ * @property {Object} write.sheathing.object Object to be wrapped
69
+ * @property {string} write.sheathing.path Path to the data
70
+ * @property {Object} read={} Options
68
71
  * @property {Object} read.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor}
69
72
  * @property {string} read.init.method=GET
70
73
  * @property {string} read.acceptedStatus=[200]
@@ -85,6 +88,10 @@ class RestAPI extends Datasource {
85
88
  transformer: undefined,
86
89
  callbacks: []
87
90
  },
91
+ sheathing: {
92
+ object: undefined,
93
+ path: undefined,
94
+ },
88
95
  report: {
89
96
  path: undefined
90
97
  }
@@ -146,6 +153,11 @@ class RestAPI extends Datasource {
146
153
  let transformation = self.getOption('read.mapping.transformer');
147
154
  if (transformation !== undefined) {
148
155
  const pipe = new Pipe(transformation);
156
+
157
+ for (const callback of self.getOption('read.mapping.callbacks')) {
158
+ pipe.setCallback(callback.constructor.name, callback);
159
+ }
160
+
149
161
  obj = pipe.run(obj);
150
162
  }
151
163
 
@@ -174,6 +186,11 @@ class RestAPI extends Datasource {
174
186
  let transformation = self.getOption('write.mapping.transformer');
175
187
  if (transformation !== undefined) {
176
188
  const pipe = new Pipe(transformation);
189
+
190
+ for (const callback of self.getOption('write.mapping.callbacks')) {
191
+ pipe.setCallback(callback.constructor.name, callback);
192
+ }
193
+
177
194
  obj = pipe.run(obj);
178
195
  }
179
196
 
@@ -234,12 +251,3 @@ class RestAPI extends Datasource {
234
251
  }
235
252
 
236
253
 
237
- /**
238
- * This callback can be passed to a datasource and is used to adapt data structures.
239
- *
240
- * @callback Monster.Data.Datasource~exampleCallback
241
- * @param {*} value Value
242
- * @param {string} key Key
243
- * @memberOf Monster.Data
244
- * @see Monster.Data.Datasource
245
- */
@@ -0,0 +1,337 @@
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 {internalSymbol, instanceSymbol} from "../../constants.mjs";
9
+ import {isInteger, isString, isObject} from "../../types/is.mjs";
10
+ import {Queue} from "../../types/queue.mjs";
11
+ import {Datasource} from "../datasource.mjs";
12
+ import {Pathfinder} from "../pathfinder.mjs";
13
+ import {Pipe} from "../pipe.mjs";
14
+
15
+ export {WebSocketDatasource}
16
+
17
+ /**
18
+ * @private
19
+ * @type {symbol}
20
+ */
21
+ const receiveQueueSymbol = Symbol("queue");
22
+
23
+
24
+ /**
25
+ * @private
26
+ * @type {symbol}
27
+ *
28
+ * hint: this name is used in the tests. if you want to change it, please change it in the tests as well.
29
+ */
30
+ const connectionSymbol = Symbol("connection");
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
+ };
57
+
58
+ /**
59
+ * The RestAPI is a class that enables a REST API server.
60
+ *
61
+ * @externalExample ../../../example/data/storage/restapi.mjs
62
+ * @license AGPLv3
63
+ * @since 3.1.0
64
+ * @copyright schukai GmbH
65
+ * @memberOf Monster.Data.Datasource
66
+ * @summary The LocalStorage class encapsulates the access to data objects.
67
+ */
68
+ class WebSocketDatasource extends Datasource {
69
+
70
+ /**
71
+ *
72
+ * @param {Object} [options] options contains definitions for the datasource.
73
+ */
74
+ constructor(options) {
75
+ super();
76
+
77
+ if (isString(options)) {
78
+ options = {url: options};
79
+ }
80
+
81
+ if (!isObject(options)) options = {};
82
+ this.setOptions(options);
83
+ this[receiveQueueSymbol] = new Queue();
84
+
85
+ this[connectionSymbol] = {};
86
+ this[connectionSymbol].socket = null;
87
+ this[connectionSymbol].reconnectCounter = 0;
88
+ this[manualCloseSymbol]=false;
89
+ }
90
+
91
+ /**
92
+ *
93
+ * @returns {Websocketdatasource}
94
+ * @throws {Error} No url defined for websocket datasource.
95
+ */
96
+ connect() {
97
+ const self = this;
98
+
99
+ let connected = false;
100
+ let reconnectTimeout = self.getOption('reconnect.timeout');
101
+ if (!isInteger(reconnectTimeout) || reconnectTimeout < 1000) reconnectTimeout = 1000;
102
+ let reconnectAttempts = self.getOption('reconnect.attempts');
103
+ if (!isInteger(reconnectAttempts) || reconnectAttempts < 1) reconnectAttempts = 1;
104
+ let reconnectEnabled = self.getOption('reconnect.enabled');
105
+ if (reconnectEnabled !== true) reconnectEnabled = false;
106
+
107
+ self[manualCloseSymbol] = false;
108
+ self[connectionSymbol].reconnectCounter++;
109
+
110
+ if (self[connectionSymbol].socket && self[connectionSymbol].socket.readyState < 2) {
111
+ self[connectionSymbol].socket.close();
112
+ }
113
+ self[connectionSymbol].socket = null;
114
+
115
+ const url = self.getOption('url');
116
+ if (!url) throw new Error('No url defined for websocket datasource.');
117
+
118
+ self[connectionSymbol].socket = new WebSocket(url);
119
+
120
+ self[connectionSymbol].socket.onmessage = function (event) {
121
+ self[receiveQueueSymbol].add(event);
122
+ setTimeout(function () {
123
+ self.read();
124
+ }, 0);
125
+ };
126
+
127
+ self[connectionSymbol].socket.onopen = function () {
128
+ connected = true;
129
+ self[connectionSymbol].reconnectCounter = 0;
130
+ };
131
+
132
+ self[connectionSymbol].socket.close = function (event) {
133
+
134
+ if (self[manualCloseSymbol]) {
135
+ self[manualCloseSymbol] = false;
136
+ return;
137
+ }
138
+
139
+ if (reconnectEnabled && this[connectionSymbol].reconnectCounter < reconnectAttempts) {
140
+ setTimeout(() => {
141
+ self.connect();
142
+ }, reconnectTimeout * this[connectionSymbol].reconnectCounter);
143
+ }
144
+
145
+ };
146
+
147
+ self[connectionSymbol].socket.onerror = (error) => {
148
+
149
+ if (reconnectEnabled && self[connectionSymbol].reconnectCounter < reconnectAttempts) {
150
+ setTimeout(() => {
151
+ self.connect();
152
+ }, reconnectTimeout * this[connectionSymbol].reconnectCounter);
153
+ }
154
+
155
+ };
156
+ }
157
+
158
+ isConnected() {
159
+ return this[connectionSymbol].socket && this[connectionSymbol].socket.readyState === 1;
160
+ }
161
+
162
+ /**
163
+ * This method is called by the `instanceof` operator.
164
+ * @returns {symbol}
165
+ */
166
+ static get [instanceSymbol]() {
167
+ return Symbol.for("@schukai/monster/data/datasource/websocket");
168
+ }
169
+
170
+ /**
171
+ * @property {string} url=undefined Defines the resource that you wish to fetch.
172
+ * @property {Number} reconnect.timeout The timeout in milliseconds for the reconnect.
173
+ * @property {Number} reconnect.attempts The maximum number of reconnects.
174
+ * @property {Bool} reconnect.enabled If the reconnect is enabled.
175
+ * @property {Object} write={} Options
176
+ * @property {Object} write.mapping the mapping is applied before writing.
177
+ * @property {String} write.mapping.transformer Transformer to select the appropriate entries
178
+ * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
179
+ * @property {Object} write.report
180
+ * @property {String} write.report.path Path to validations
181
+ * @property {Object} write.sheathing
182
+ * @property {Object} write.sheathing.object Object to be wrapped
183
+ * @property {string} write.sheathing.path Path to the data
184
+ * @property {Object} read={} Options
185
+ * @property {Object} read.mapping the mapping is applied after reading.
186
+ * @property {String} read.mapping.transformer Transformer to select the appropriate entries
187
+ * @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading.
188
+ */
189
+ get defaults() {
190
+ return Object.assign({}, super.defaults, {
191
+ url: undefined,
192
+ write: {
193
+ mapping: {
194
+ transformer: undefined,
195
+ callbacks: []
196
+ },
197
+ report: {
198
+ path: undefined
199
+ },
200
+ sheathing: {
201
+ object: undefined,
202
+ path: undefined,
203
+ },
204
+ },
205
+ read: {
206
+ mapping: {
207
+ transformer: undefined,
208
+ callbacks: []
209
+ },
210
+ },
211
+ reconnect: {
212
+ timeout: 1000,
213
+ attempts: 10,
214
+ enabled: true
215
+ }
216
+ });
217
+ }
218
+
219
+ /**
220
+ * This method closes the connection.
221
+ *
222
+ * @returns {Promise}
223
+ */
224
+ close() {
225
+ this[manualCloseSymbol]=true;
226
+ if (this[connectionSymbol].socket) {
227
+ this[connectionSymbol].socket.close();
228
+ }
229
+ return this;
230
+ }
231
+
232
+ /**
233
+ * @return {Promise}
234
+ * @throws {Error} the options does not contain a valid json definition
235
+ * @throws {Error} the data cannot be read
236
+ * @throws {TypeError} value is not an object
237
+ */
238
+ read() {
239
+ const self = this;
240
+ let response;
241
+
242
+ return new Promise((resolve, reject) => {
243
+ if (self[receiveQueueSymbol].isEmpty()) {
244
+ resolve();
245
+ }
246
+
247
+ while (!self[receiveQueueSymbol].isEmpty()) {
248
+
249
+ const event = self[receiveQueueSymbol].poll();
250
+ const body = event?.data;
251
+ if (!body) continue;
252
+
253
+ let obj;
254
+ try {
255
+ obj = JSON.parse(body);
256
+ } catch (e) {
257
+
258
+ let msg = 'the response does not contain a valid json (actual: ';
259
+
260
+ if (body.length > 100) {
261
+ msg += body.substring(0, 97) + '...';
262
+ } else {
263
+ msg += body;
264
+ }
265
+
266
+ msg += "; " + e.message + ')';
267
+
268
+ reject(msg);
269
+ }
270
+
271
+ let transformation = self.getOption('read.mapping.transformer');
272
+ if (transformation !== undefined) {
273
+ const pipe = new Pipe(transformation);
274
+
275
+ for (const callback of self.getOption('read.mapping.callbacks')) {
276
+ pipe.setCallback(callback.constructor.name, callback);
277
+ }
278
+
279
+ obj = pipe.run(obj);
280
+ }
281
+
282
+ self.set(obj);
283
+ return response;
284
+ }
285
+ })
286
+ }
287
+
288
+ /**
289
+ * @return {Promise}
290
+ */
291
+ write() {
292
+ const self = this;
293
+
294
+ let obj = self.get();
295
+ let transformation = self.getOption('write.mapping.transformer');
296
+ if (transformation !== undefined) {
297
+ const pipe = new Pipe(transformation);
298
+
299
+ for (const callback of self.getOption('write.mapping.callbacks')) {
300
+ pipe.setCallback(callback.constructor.name, callback);
301
+ }
302
+
303
+ obj = pipe.run(obj);
304
+ }
305
+
306
+ let sheathingObject = self.getOption('write.sheathing.object');
307
+ let sheathingPath = self.getOption('write.sheathing.path');
308
+ let reportPath = self.getOption('write.report.path');
309
+
310
+ if (sheathingObject && sheathingPath) {
311
+ const sub = obj;
312
+ obj = sheathingObject;
313
+ (new Pathfinder(obj)).setVia(sheathingPath, sub);
314
+ }
315
+
316
+ return new Promise((resolve, reject) => {
317
+
318
+ if (self[connectionSymbol].socket.readyState !== 1) {
319
+ reject('the socket is not ready');
320
+ }
321
+
322
+ self[connectionSymbol].socket.send(JSON.stringify(obj))
323
+ resolve();
324
+ });
325
+ }
326
+
327
+
328
+ /**
329
+ * @return {RestAPI}
330
+ */
331
+ getClone() {
332
+ const self = this;
333
+ return new Websocketdatasource(self[internalSymbol].getRealSubject()['options']);
334
+ }
335
+
336
+ }
337
+
@@ -17,6 +17,18 @@ import {Pathfinder} from "./pathfinder.mjs";
17
17
 
18
18
  export {Datasource}
19
19
 
20
+
21
+ /**
22
+ * This callback can be passed to a datasource and is used to adapt data structures.
23
+ *
24
+ * @callback Monster.Data.Datasource~exampleCallback
25
+ * @param {*} value Value
26
+ * @param {string} key Key
27
+ * @memberOf Monster.Data
28
+ * @see Monster.Data.Datasource
29
+ */
30
+
31
+
20
32
  /**
21
33
  * @private
22
34
  * @type {symbol}
@@ -16,7 +16,7 @@ import {Observer} from "../types/observer.mjs";
16
16
  import {ProxyObserver} from "../types/proxyobserver.mjs";
17
17
  import {validateFunction, validateInstance, validateObject, validateString} from "../types/validate.mjs";
18
18
  import {clone} from "../util/clone.mjs";
19
- import {addAttributeToken, addToObjectLink, getLinkedObjects, hasObjectLink} from "./attributes.mjs";
19
+ import {addAttributeToken, getLinkedObjects, hasObjectLink} from "./attributes.mjs";
20
20
  import {
21
21
  ATTRIBUTE_DISABLED,
22
22
  ATTRIBUTE_ERRORMESSAGE,
@@ -468,7 +468,6 @@ class CustomElement extends HTMLElement {
468
468
  }
469
469
 
470
470
  addObjectWithUpdaterToElement.call(self, nodeList, customElementUpdaterLinkSymbol, clone(self[internalSymbol].getRealSubject()['options']))
471
- //assignUpdaterToElement.call(self, nodeList, clone(self[internalSymbol].getRealSubject()['options']));
472
471
  return self;
473
472
  }
474
473
 
@@ -7,11 +7,18 @@
7
7
 
8
8
  import {Base} from './base.mjs';
9
9
  import {instanceSymbol} from '../constants.mjs';
10
+
10
11
  export {Queue}
11
12
 
12
13
  /**
13
- * You can create the instance via the monster namespace `new Monster.Types.Queue()`.
14
- *
14
+ * A queue is a list of items that are processed one after another (first in, first out).
15
+ *
16
+ * With a queue you can add items to the end of the list `Queue.add()` and remove items from the beginning of the list `Queue.pop()`.
17
+ *
18
+ * With `Queue.peek()` you can get the first item without removing it from the list.
19
+ *
20
+ * You can create the instance via `new Queue()`.
21
+ *
15
22
  * @externalExample ../../example/types/queue.mjs
16
23
  * @license AGPLv3
17
24
  * @since 1.4.0
@@ -149,7 +149,7 @@ function getMonsterVersion() {
149
149
  }
150
150
 
151
151
  /** don't touch, replaced by make with package.json version */
152
- monsterVersion = new Version('3.0.0')
152
+ monsterVersion = new Version('3.1.0')
153
153
 
154
154
  return monsterVersion;
155
155
 
@@ -0,0 +1,96 @@
1
+ import {expect} from "chai"
2
+ import {WebSocketDatasource} from "../../../../../application/source/data/datasource/websocket.mjs";
3
+ import {initWebSocket} from "../../../util/websocket.mjs";
4
+
5
+ const testUrl = "wss://ws.postman-echo.com/raw"
6
+
7
+ // const g = getGlobal();
8
+ // g['WebSocket'] = WS;
9
+
10
+
11
+ describe('Websocket', function () {
12
+
13
+ let ds = undefined
14
+
15
+ before(function (done) {
16
+ initWebSocket().then(() => {
17
+ done()
18
+ }).catch((e) => {
19
+ done(e)
20
+ })
21
+ });
22
+
23
+ afterEach(function (done) {
24
+ if (ds) {
25
+ ds.close()
26
+ }
27
+
28
+ // without this, the node test will hang
29
+ for (const sym of Object.getOwnPropertySymbols(ds)) {
30
+ if (sym.toString() ==='Symbol(connection)') {
31
+ if(ds[sym]?.socket?.['terminate']) {
32
+ ds[sym]?.socket?.['terminate']()
33
+ }
34
+ }
35
+ }
36
+
37
+ done()
38
+ });
39
+
40
+ it('should connect', function (done) {
41
+ ds = new WebSocketDatasource({
42
+ url: testUrl,
43
+ reconnect: {
44
+ enabled: false
45
+ }
46
+ });
47
+ ds.connect()
48
+ setTimeout(() => {
49
+ expect(ds.isConnected()).to.be.true;
50
+ done();
51
+ }, 500);
52
+
53
+
54
+ })
55
+
56
+ it('should send message', function (done) {
57
+ ds = new WebSocketDatasource({
58
+ url: testUrl,
59
+ reconnect: {
60
+ enabled: false
61
+ }
62
+ });
63
+ ds.connect()
64
+
65
+ ds.set({
66
+ data: {
67
+ message: "Hello World"
68
+ }
69
+ })
70
+
71
+ setTimeout(() => {
72
+
73
+ ds.write().then(() => {
74
+
75
+ ds.set({})
76
+ expect(ds.get()).to.be.deep.equal({});
77
+
78
+
79
+ setTimeout(() => {
80
+
81
+ expect(ds.get()).to.be.deep.equal({
82
+ data: {
83
+ message: "Hello World"
84
+ }
85
+ });
86
+ done();
87
+ }, 1000);
88
+ }).catch((err) => {
89
+ done(new Error(err));
90
+ })
91
+ }, 1000)
92
+
93
+
94
+ }).timeout(10000);
95
+
96
+ });
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version('3.0.0')
10
+ monsterVersion = new Version('3.1.0')
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -9,7 +9,7 @@ describe('DeadMansSwitch', function () {
9
9
 
10
10
  const ms1 = Date.now();
11
11
 
12
- const deadmansswitch = new DeadMansSwitch(100, () => {
12
+ new DeadMansSwitch(100, () => {
13
13
 
14
14
  const ms2 = Date.now();
15
15
 
@@ -38,12 +38,12 @@ describe('DeadMansSwitch', function () {
38
38
  const diff = ms2 - ms1;
39
39
 
40
40
  if (ms1 > ms2) {
41
- done('timing error');
41
+ done(new Error('timing error'));
42
42
  return;
43
43
  }
44
44
 
45
45
  if (diff < 600) {
46
- done('to short ' + diff);
46
+ done(new Error('to short ' + diff));
47
47
  return;
48
48
  }
49
49
  done();
@@ -0,0 +1,22 @@
1
+ import {getGlobal} from "../../../application/source/types/global.mjs";
2
+
3
+ function initWebSocket() {
4
+ if (typeof window === "object" && window['WebSocket']) return Promise.resolve();
5
+
6
+ return import("ws").then((ws) => {
7
+ getGlobal().WebSocket = class extends ws['WebSocket'] {
8
+ constructor(url, protocols) {
9
+ super(url, protocols, {
10
+ handshakeTimeout: 1000,
11
+ maxPayload: 1024 * 1024 * 1024,
12
+ });
13
+
14
+ }
15
+ };
16
+
17
+ });
18
+
19
+
20
+ }
21
+
22
+ export {initWebSocket}
@@ -78,4 +78,5 @@ import "../cases/data/diff.mjs";
78
78
  import "../cases/data/datasource/restapi.mjs";
79
79
  import "../cases/data/datasource/storage/sessionstorage.mjs";
80
80
  import "../cases/data/datasource/storage/localstorage.mjs";
81
+ import "../cases/data/datasource/webservice.mjs";
81
82
  import "../cases/math/random.mjs";
@@ -14,8 +14,8 @@
14
14
  </head>
15
15
  <body>
16
16
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
17
- <h1 style='margin-bottom: 0.1em;'>Monster 2.2.1</h1>
18
- <div id="lastupdate" style='font-size:0.7em'>last update Mi 4. Jan 15:51:44 CET 2023</div>
17
+ <h1 style='margin-bottom: 0.1em;'>Monster 3.0.0</h1>
18
+ <div id="lastupdate" style='font-size:0.7em'>last update Fr 6. Jan 12:54:47 CET 2023</div>
19
19
  </div>
20
20
  <div id="mocks"></div>
21
21
  <div id="mocha"></div>