@schukai/monster 4.46.5 → 4.46.7
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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/source/dom/updater.mjs +128 -69
- package/source/net/webconnect.mjs +326 -287
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.46.
|
|
1
|
+
{"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.46.7"}
|
package/source/dom/updater.mjs
CHANGED
|
@@ -72,6 +72,24 @@ const pendingDiffsSymbol = Symbol("pendingDiffs");
|
|
|
72
72
|
*/
|
|
73
73
|
const processingSymbol = Symbol("processing");
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @private
|
|
77
|
+
* @type {symbol}
|
|
78
|
+
*/
|
|
79
|
+
const pipeCacheSymbol = Symbol("pipeCache");
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @private
|
|
83
|
+
* @type {symbol}
|
|
84
|
+
*/
|
|
85
|
+
const processingScheduledSymbol = Symbol("processingScheduled");
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @private
|
|
89
|
+
* Performance optimization: static Set for boolean checks
|
|
90
|
+
*/
|
|
91
|
+
const TRUE_VALUES = new Set(["true", "1", "on"]);
|
|
92
|
+
|
|
75
93
|
/**
|
|
76
94
|
* The updater class connects an object with the DOM. In this way, structures and contents in the DOM can be
|
|
77
95
|
* programmatically adapted via attributes.
|
|
@@ -131,6 +149,9 @@ class Updater extends Base {
|
|
|
131
149
|
|
|
132
150
|
this[pendingDiffsSymbol] = [];
|
|
133
151
|
this[processingSymbol] = false;
|
|
152
|
+
this[processingScheduledSymbol] = false;
|
|
153
|
+
this[pipeCacheSymbol] = new Map();
|
|
154
|
+
this[timerElementEventHandlerSymbol] = new WeakMap();
|
|
134
155
|
|
|
135
156
|
this[internalSymbol].subject.attachObserver(
|
|
136
157
|
new Observer(() => {
|
|
@@ -138,7 +159,19 @@ class Updater extends Base {
|
|
|
138
159
|
const diffResult = diff(this[internalSymbol].last, real);
|
|
139
160
|
this[internalSymbol].last = clone(real);
|
|
140
161
|
this[pendingDiffsSymbol].push(diffResult);
|
|
141
|
-
|
|
162
|
+
|
|
163
|
+
if (!this[processingScheduledSymbol]) {
|
|
164
|
+
this[processingScheduledSymbol] = true;
|
|
165
|
+
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
queueMicrotask(() => {
|
|
168
|
+
this[processingScheduledSymbol] = false;
|
|
169
|
+
this.#processQueue().finally(resolve);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return Promise.resolve();
|
|
142
175
|
}),
|
|
143
176
|
);
|
|
144
177
|
}
|
|
@@ -194,7 +227,7 @@ class Updater extends Base {
|
|
|
194
227
|
*
|
|
195
228
|
* ```js
|
|
196
229
|
* updater.run().then(() => {
|
|
197
|
-
*
|
|
230
|
+
* updater.enableEventProcessing();
|
|
198
231
|
* });
|
|
199
232
|
* ```
|
|
200
233
|
*
|
|
@@ -243,7 +276,7 @@ class Updater extends Base {
|
|
|
243
276
|
*
|
|
244
277
|
* ```js
|
|
245
278
|
* updater.run().then(() => {
|
|
246
|
-
*
|
|
279
|
+
* updater.enableEventProcessing();
|
|
247
280
|
* });
|
|
248
281
|
* ```
|
|
249
282
|
*
|
|
@@ -295,6 +328,20 @@ class Updater extends Base {
|
|
|
295
328
|
this[internalSymbol].callbacks.set(name, callback);
|
|
296
329
|
return this;
|
|
297
330
|
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @private
|
|
334
|
+
* @param {string} cmd
|
|
335
|
+
* @returns {Pipe}
|
|
336
|
+
*/
|
|
337
|
+
getPipe(cmd) {
|
|
338
|
+
let pipe = this[pipeCacheSymbol].get(cmd);
|
|
339
|
+
if (!pipe) {
|
|
340
|
+
pipe = new Pipe(cmd);
|
|
341
|
+
this[pipeCacheSymbol].set(cmd, pipe);
|
|
342
|
+
}
|
|
343
|
+
return pipe;
|
|
344
|
+
}
|
|
298
345
|
}
|
|
299
346
|
|
|
300
347
|
/**
|
|
@@ -308,8 +355,9 @@ function getCheckStateCallback() {
|
|
|
308
355
|
return function (current) {
|
|
309
356
|
// this is a reference to the current object (therefore no array function here)
|
|
310
357
|
if (this instanceof HTMLInputElement) {
|
|
311
|
-
if (["radio", "checkbox"].
|
|
312
|
-
|
|
358
|
+
if (["radio", "checkbox"].includes(this.type)) {
|
|
359
|
+
if (current == null) return undefined;
|
|
360
|
+
return String(this.value) === String(current) ? "true" : undefined;
|
|
313
361
|
}
|
|
314
362
|
} else if (this instanceof HTMLOptionElement) {
|
|
315
363
|
if (isArray(current) && current.indexOf(this.value) !== -1) {
|
|
@@ -349,22 +397,26 @@ function getControlEventHandler() {
|
|
|
349
397
|
return;
|
|
350
398
|
}
|
|
351
399
|
|
|
352
|
-
|
|
400
|
+
const switches = this[timerElementEventHandlerSymbol];
|
|
401
|
+
let dms = switches.get(element);
|
|
402
|
+
|
|
403
|
+
if (dms instanceof DeadMansSwitch) {
|
|
353
404
|
try {
|
|
354
|
-
|
|
405
|
+
dms.touch();
|
|
355
406
|
return;
|
|
356
407
|
} catch (e) {
|
|
357
|
-
delete
|
|
408
|
+
switches.delete(element);
|
|
358
409
|
}
|
|
359
410
|
}
|
|
360
411
|
|
|
361
|
-
|
|
412
|
+
dms = new DeadMansSwitch(50, () => {
|
|
362
413
|
try {
|
|
363
414
|
retrieveAndSetValue.call(this, element);
|
|
364
415
|
} catch (e) {
|
|
365
416
|
addErrorAttribute(element, e);
|
|
366
417
|
}
|
|
367
418
|
});
|
|
419
|
+
switches.set(element, dms);
|
|
368
420
|
};
|
|
369
421
|
|
|
370
422
|
return this[symbol];
|
|
@@ -447,18 +499,21 @@ function retrieveAndSetValue(element) {
|
|
|
447
499
|
case "int":
|
|
448
500
|
case "float":
|
|
449
501
|
case "integer":
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
value =
|
|
502
|
+
const num = Number(value);
|
|
503
|
+
if (value === "" || value === null || value === undefined) {
|
|
504
|
+
value = undefined;
|
|
505
|
+
} else if (Number.isNaN(num)) {
|
|
506
|
+
value = undefined;
|
|
507
|
+
} else {
|
|
508
|
+
value = num;
|
|
453
509
|
}
|
|
454
510
|
break;
|
|
455
511
|
case "boolean":
|
|
456
512
|
case "bool":
|
|
457
513
|
case "checkbox":
|
|
458
|
-
|
|
459
|
-
|
|
514
|
+
// Use static set (Performance fix)
|
|
515
|
+
value = TRUE_VALUES.has(String(value).toLowerCase()) || value === true;
|
|
460
516
|
break;
|
|
461
|
-
|
|
462
517
|
case "string[]":
|
|
463
518
|
case "[]string":
|
|
464
519
|
if (isString(value)) {
|
|
@@ -509,11 +564,14 @@ function retrieveAndSetValue(element) {
|
|
|
509
564
|
case "object":
|
|
510
565
|
case "json":
|
|
511
566
|
if (isString(value)) {
|
|
512
|
-
|
|
567
|
+
try {
|
|
568
|
+
value = JSON.parse(value);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
throw new Error("unsupported value for object");
|
|
571
|
+
}
|
|
513
572
|
} else {
|
|
514
573
|
throw new Error("unsupported value for object");
|
|
515
574
|
}
|
|
516
|
-
|
|
517
575
|
break;
|
|
518
576
|
default:
|
|
519
577
|
break;
|
|
@@ -535,21 +593,11 @@ function retrieveAndSetValue(element) {
|
|
|
535
593
|
* @private
|
|
536
594
|
*/
|
|
537
595
|
function parseIntArray(val) {
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
.map((v) => parseInt(v, 10))
|
|
544
|
-
.filter((v) => !isNaN(v));
|
|
545
|
-
} else if (isInteger(val)) {
|
|
546
|
-
return [val];
|
|
547
|
-
} else if (val === undefined || val === null) {
|
|
548
|
-
return [];
|
|
549
|
-
} else if (isArray(val)) {
|
|
550
|
-
return val.map((v) => parseInt(v, 10)).filter((v) => !isNaN(v));
|
|
551
|
-
}
|
|
552
|
-
throw new Error("unsupported value for int array");
|
|
596
|
+
if (val === undefined || val === null) return [];
|
|
597
|
+
|
|
598
|
+
const list = isArray(val) ? val : String(val).split(",");
|
|
599
|
+
|
|
600
|
+
return list.map((v) => parseInt(v, 10)).filter((v) => !Number.isNaN(v));
|
|
553
601
|
}
|
|
554
602
|
|
|
555
603
|
/**
|
|
@@ -650,7 +698,7 @@ function insertElement(change) {
|
|
|
650
698
|
throw new Error("pipes are not allowed when cloning a node.");
|
|
651
699
|
}
|
|
652
700
|
|
|
653
|
-
const pipe =
|
|
701
|
+
const pipe = this.getPipe(cmd);
|
|
654
702
|
this[internalSymbol].callbacks.forEach((f, n) => {
|
|
655
703
|
pipe.setCallback(n, f);
|
|
656
704
|
});
|
|
@@ -830,25 +878,18 @@ function runUpdateContent(container, parts, subject) {
|
|
|
830
878
|
|
|
831
879
|
// Unfortunately, static data is always changed as well, since it is not possible to react to changes here.
|
|
832
880
|
const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`;
|
|
833
|
-
const e = container.querySelectorAll(`${query}`);
|
|
834
881
|
|
|
835
|
-
|
|
882
|
+
// Performance optimization: avoid new Set([...NodeList])
|
|
883
|
+
const elements = container.querySelectorAll(`${query}`);
|
|
836
884
|
|
|
837
|
-
|
|
838
|
-
iterator.add(container);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* @type {HTMLElement}
|
|
843
|
-
*/
|
|
844
|
-
for (const [element] of iterator.entries()) {
|
|
885
|
+
const process = (element) => {
|
|
845
886
|
if (mem.has(element)) return;
|
|
846
887
|
mem.add(element);
|
|
847
888
|
|
|
848
889
|
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
|
|
849
890
|
const cmd = trimSpaces(attributes);
|
|
850
891
|
|
|
851
|
-
const pipe =
|
|
892
|
+
const pipe = this.getPipe(cmd);
|
|
852
893
|
this[internalSymbol].callbacks.forEach((f, n) => {
|
|
853
894
|
pipe.setCallback(n, f);
|
|
854
895
|
});
|
|
@@ -874,6 +915,16 @@ function runUpdateContent(container, parts, subject) {
|
|
|
874
915
|
} else {
|
|
875
916
|
element.innerHTML = value;
|
|
876
917
|
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// Iterate NodeList directly
|
|
921
|
+
for (const element of elements) {
|
|
922
|
+
process(element);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Check container
|
|
926
|
+
if (container.matches(query)) {
|
|
927
|
+
process(container);
|
|
877
928
|
}
|
|
878
929
|
}
|
|
879
930
|
}
|
|
@@ -908,27 +959,18 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
908
959
|
const current = parts.join(".");
|
|
909
960
|
parts.pop();
|
|
910
961
|
|
|
911
|
-
let iterator = new Set();
|
|
912
|
-
|
|
913
962
|
const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`;
|
|
914
963
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (e.length > 0) {
|
|
918
|
-
iterator = new Set([...e]);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if (container.matches(query)) {
|
|
922
|
-
iterator.add(container);
|
|
923
|
-
}
|
|
964
|
+
// Performance optimization: avoid new Set([...NodeList])
|
|
965
|
+
const elements = container.querySelectorAll(query);
|
|
924
966
|
|
|
925
|
-
|
|
967
|
+
const process = (element) => {
|
|
926
968
|
if (mem.has(element)) return;
|
|
927
969
|
mem.add(element);
|
|
928
970
|
|
|
929
971
|
// this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set
|
|
930
972
|
if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
|
|
931
|
-
|
|
973
|
+
return;
|
|
932
974
|
}
|
|
933
975
|
|
|
934
976
|
const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
|
|
@@ -939,7 +981,7 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
939
981
|
const name = trimSpaces(def.substr(0, i));
|
|
940
982
|
const cmd = trimSpaces(def.substr(i));
|
|
941
983
|
|
|
942
|
-
const pipe =
|
|
984
|
+
const pipe = this.getPipe(cmd);
|
|
943
985
|
|
|
944
986
|
this[internalSymbol].callbacks.forEach((f, n) => {
|
|
945
987
|
pipe.setCallback(n, f, element);
|
|
@@ -967,6 +1009,16 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
967
1009
|
}
|
|
968
1010
|
handleInputControlAttributeUpdate.call(this, element, name, value);
|
|
969
1011
|
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// Iterate NodeList directly
|
|
1015
|
+
for (const element of elements) {
|
|
1016
|
+
process(element);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Check container
|
|
1020
|
+
if (container.matches(query)) {
|
|
1021
|
+
process(container);
|
|
970
1022
|
}
|
|
971
1023
|
}
|
|
972
1024
|
}
|
|
@@ -979,52 +1031,59 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
979
1031
|
* @return {void}
|
|
980
1032
|
* @this Updater
|
|
981
1033
|
*/
|
|
982
|
-
|
|
983
1034
|
function handleInputControlAttributeUpdate(element, name, value) {
|
|
1035
|
+
// Prevent NaN warnings by normalizing invalid numbers to undefined
|
|
1036
|
+
if (typeof value === "number" && isNaN(value)) {
|
|
1037
|
+
value = undefined;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
984
1040
|
if (element instanceof HTMLSelectElement) {
|
|
985
1041
|
switch (element.type) {
|
|
986
1042
|
case "select-multiple":
|
|
987
|
-
|
|
988
|
-
|
|
1043
|
+
// Ensure value is an array before calling indexOf to avoid errors
|
|
1044
|
+
if (Array.isArray(value) || typeof value === "string") {
|
|
1045
|
+
for (const [index, opt] of Object.entries(element.options)) {
|
|
1046
|
+
opt.selected = value.indexOf(opt.value) !== -1;
|
|
1047
|
+
}
|
|
989
1048
|
}
|
|
990
|
-
|
|
991
1049
|
break;
|
|
992
1050
|
case "select-one":
|
|
993
1051
|
// Only one value may be selected
|
|
994
|
-
|
|
995
1052
|
for (const [index, opt] of Object.entries(element.options)) {
|
|
996
|
-
if (opt.value
|
|
1053
|
+
if (opt.value == value) {
|
|
1054
|
+
// Loose equality to match string numbers
|
|
997
1055
|
element.selectedIndex = index;
|
|
998
1056
|
break;
|
|
999
1057
|
}
|
|
1000
1058
|
}
|
|
1001
|
-
|
|
1002
1059
|
break;
|
|
1003
1060
|
}
|
|
1004
1061
|
} else if (element instanceof HTMLInputElement) {
|
|
1005
1062
|
switch (element.type) {
|
|
1006
1063
|
case "radio":
|
|
1007
1064
|
if (name === "checked") {
|
|
1008
|
-
element.checked = value !== undefined;
|
|
1065
|
+
element.checked = value !== undefined && value !== null;
|
|
1009
1066
|
}
|
|
1010
1067
|
break;
|
|
1011
1068
|
|
|
1012
1069
|
case "checkbox":
|
|
1013
1070
|
if (name === "checked") {
|
|
1014
|
-
element.checked = value !== undefined;
|
|
1071
|
+
element.checked = value !== undefined && value !== null;
|
|
1015
1072
|
}
|
|
1016
1073
|
break;
|
|
1017
1074
|
|
|
1018
1075
|
case "text":
|
|
1019
1076
|
default:
|
|
1020
1077
|
if (name === "value") {
|
|
1021
|
-
|
|
1078
|
+
// Check for undefined, null, or NaN
|
|
1079
|
+
element.value = value === undefined || value === null ? "" : value;
|
|
1022
1080
|
}
|
|
1023
1081
|
break;
|
|
1024
1082
|
}
|
|
1025
1083
|
} else if (element instanceof HTMLTextAreaElement) {
|
|
1026
1084
|
if (name === "value") {
|
|
1027
|
-
|
|
1085
|
+
// Check for undefined, null, or NaN
|
|
1086
|
+
element.value = value === undefined || value === null ? "" : value;
|
|
1028
1087
|
}
|
|
1029
1088
|
}
|
|
1030
1089
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { instanceSymbol } from "../constants.mjs";
|
|
16
|
-
import { isInteger, isString
|
|
16
|
+
import { isInteger, isString } from "../types/is.mjs";
|
|
17
17
|
import { BaseWithOptions } from "../types/basewithoptions.mjs";
|
|
18
18
|
import { ObservableQueue } from "../types/observablequeue.mjs";
|
|
19
19
|
import { Message } from "./webconnect/message.mjs";
|
|
@@ -26,11 +26,6 @@ export { WebConnect };
|
|
|
26
26
|
* @type {symbol}
|
|
27
27
|
*/
|
|
28
28
|
const receiveQueueSymbol = Symbol("receiveQueue");
|
|
29
|
-
/**
|
|
30
|
-
* @private
|
|
31
|
-
* @type {symbol}
|
|
32
|
-
*/
|
|
33
|
-
const sendQueueSymbol = Symbol("sendQueue");
|
|
34
29
|
|
|
35
30
|
/**
|
|
36
31
|
* @private
|
|
@@ -52,19 +47,19 @@ const manualCloseSymbol = Symbol("manualClose");
|
|
|
52
47
|
* @type {Object}
|
|
53
48
|
*/
|
|
54
49
|
const connectionStatusCode = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
1000: "Normal closure",
|
|
51
|
+
1001: "Going away",
|
|
52
|
+
1002: "Protocol error",
|
|
53
|
+
1003: "Unsupported data",
|
|
54
|
+
1004: "Reserved",
|
|
55
|
+
1005: "No status code",
|
|
56
|
+
1006: "Connection closed abnormally",
|
|
57
|
+
1007: "Invalid frame payload data",
|
|
58
|
+
1008: "Policy violation",
|
|
59
|
+
1009: "The Message is too big",
|
|
60
|
+
1010: "Mandatory extension",
|
|
61
|
+
1011: "Internal server error",
|
|
62
|
+
1015: "TLS handshake",
|
|
68
63
|
};
|
|
69
64
|
|
|
70
65
|
/**
|
|
@@ -73,107 +68,142 @@ const connectionStatusCode = {
|
|
|
73
68
|
* @throws {Error} No url defined for websocket datasource.
|
|
74
69
|
*/
|
|
75
70
|
function connectServer(resolve, reject) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
71
|
+
const self = this;
|
|
72
|
+
|
|
73
|
+
const url = self.getOption("url");
|
|
74
|
+
if (!url) {
|
|
75
|
+
reject(new Error("No url defined for web connect."));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let promiseAlreadyResolved = false;
|
|
80
|
+
|
|
81
|
+
let connectionTimeout = self.getOption("connection.timeout");
|
|
82
|
+
if (!isInteger(connectionTimeout) || connectionTimeout < 100) {
|
|
83
|
+
connectionTimeout = 5000;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Timeout Handling
|
|
87
|
+
const timeoutId = setTimeout(() => {
|
|
88
|
+
if (promiseAlreadyResolved) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
promiseAlreadyResolved = true;
|
|
92
|
+
|
|
93
|
+
// Clean up hanging socket attempt
|
|
94
|
+
if (self[connectionSymbol].socket) {
|
|
95
|
+
try {
|
|
96
|
+
self[connectionSymbol].socket.close();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
reject(new Error("Connection timeout"));
|
|
103
|
+
}, connectionTimeout);
|
|
104
|
+
|
|
105
|
+
let reconnectTimeout = self.getOption("connection.reconnect.timeout");
|
|
106
|
+
if (!isInteger(reconnectTimeout) || reconnectTimeout < 1000)
|
|
107
|
+
reconnectTimeout = 1000;
|
|
108
|
+
|
|
109
|
+
let reconnectAttempts = self.getOption("connection.reconnect.attempts");
|
|
110
|
+
if (!isInteger(reconnectAttempts) || reconnectAttempts < 1)
|
|
111
|
+
reconnectAttempts = 1;
|
|
112
|
+
|
|
113
|
+
let reconnectEnabled = self.getOption("connection.reconnect.enabled");
|
|
114
|
+
if (reconnectEnabled !== true) reconnectEnabled = false;
|
|
115
|
+
|
|
116
|
+
self[manualCloseSymbol] = false;
|
|
117
|
+
self[connectionSymbol].reconnectCounter++;
|
|
118
|
+
|
|
119
|
+
// Cleanup existing socket
|
|
120
|
+
if (
|
|
121
|
+
self[connectionSymbol].socket &&
|
|
122
|
+
self[connectionSymbol].socket.readyState < 2
|
|
123
|
+
) {
|
|
124
|
+
// Remove listeners to prevent side effects during close
|
|
125
|
+
self[connectionSymbol].socket.onclose = null;
|
|
126
|
+
self[connectionSymbol].socket.onerror = null;
|
|
127
|
+
self[connectionSymbol].socket.onmessage = null;
|
|
128
|
+
self[connectionSymbol].socket.onopen = null;
|
|
129
|
+
self[connectionSymbol].socket.close();
|
|
130
|
+
}
|
|
131
|
+
self[connectionSymbol].socket = null;
|
|
132
|
+
|
|
133
|
+
const WebSocket = getGlobalFunction("WebSocket");
|
|
134
|
+
if (!WebSocket) {
|
|
135
|
+
clearTimeout(timeoutId);
|
|
136
|
+
reject(new Error("WebSocket is not available"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
self[connectionSymbol].socket = new WebSocket(url);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
clearTimeout(timeoutId);
|
|
144
|
+
if (!promiseAlreadyResolved) {
|
|
145
|
+
promiseAlreadyResolved = true;
|
|
146
|
+
reject(error);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
self[connectionSymbol].socket.onmessage = function (event) {
|
|
152
|
+
if (event.data instanceof Blob) {
|
|
153
|
+
const reader = new FileReader();
|
|
154
|
+
reader.addEventListener("loadend", function () {
|
|
155
|
+
self[receiveQueueSymbol].add(new Message(reader.result));
|
|
156
|
+
});
|
|
157
|
+
// Correctly pass the Blob, not a Message object
|
|
158
|
+
reader.readAsText(event.data);
|
|
159
|
+
} else {
|
|
160
|
+
self[receiveQueueSymbol].add(Message.fromJSON(event.data));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
self[connectionSymbol].socket.onopen = function () {
|
|
165
|
+
clearTimeout(timeoutId);
|
|
166
|
+
self[connectionSymbol].reconnectCounter = 0;
|
|
167
|
+
if (typeof resolve === "function" && !promiseAlreadyResolved) {
|
|
168
|
+
promiseAlreadyResolved = true;
|
|
169
|
+
resolve();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Internal helper to handle reconnects
|
|
174
|
+
const handleReconnect = () => {
|
|
175
|
+
if (self[manualCloseSymbol]) {
|
|
176
|
+
self[manualCloseSymbol] = false;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
reconnectEnabled &&
|
|
182
|
+
self[connectionSymbol].reconnectCounter < reconnectAttempts
|
|
183
|
+
) {
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
// catch potential unhandled promise rejections from the recursive call
|
|
186
|
+
self.connect().catch(() => {});
|
|
187
|
+
}, reconnectTimeout * self[connectionSymbol].reconnectCounter);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Use onclose event instead of overriding the close method
|
|
192
|
+
self[connectionSymbol].socket.onclose = function (event) {
|
|
193
|
+
handleReconnect();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
self[connectionSymbol].socket.onerror = (error) => {
|
|
197
|
+
if (!promiseAlreadyResolved) {
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
promiseAlreadyResolved = true;
|
|
200
|
+
reject(error);
|
|
201
|
+
} else {
|
|
202
|
+
// If the connection was already established, treat error as potential disconnect
|
|
203
|
+
// Usually onclose follows onerror, but we ensure we don't double-handle logic
|
|
204
|
+
// typically we rely on onclose for reconnect logic.
|
|
205
|
+
}
|
|
206
|
+
};
|
|
177
207
|
}
|
|
178
208
|
|
|
179
209
|
/**
|
|
@@ -186,171 +216,180 @@ function connectServer(resolve, reject) {
|
|
|
186
216
|
* @summary The LocalStorage class encapsulates the access to data objects.
|
|
187
217
|
*/
|
|
188
218
|
class WebConnect extends BaseWithOptions {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
219
|
+
/**
|
|
220
|
+
*
|
|
221
|
+
* @param {Object} [options] options contains definitions for the webconnect.
|
|
222
|
+
*/
|
|
223
|
+
constructor(options) {
|
|
224
|
+
if (isString(options)) {
|
|
225
|
+
options = { url: options };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
super(options);
|
|
229
|
+
|
|
230
|
+
this[receiveQueueSymbol] = new ObservableQueue();
|
|
231
|
+
|
|
232
|
+
this[connectionSymbol] = {};
|
|
233
|
+
this[connectionSymbol].socket = null;
|
|
234
|
+
this[connectionSymbol].reconnectCounter = 0;
|
|
235
|
+
this[manualCloseSymbol] = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
*
|
|
240
|
+
* @return {Promise}
|
|
241
|
+
*/
|
|
242
|
+
connect() {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
connectServer.call(this, resolve, reject);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @return {boolean}
|
|
250
|
+
*/
|
|
251
|
+
isConnected() {
|
|
252
|
+
return this[connectionSymbol]?.socket?.readyState === 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* This method is called by the `instanceof` operator.
|
|
257
|
+
* @return {symbol}
|
|
258
|
+
*/
|
|
259
|
+
static get [instanceSymbol]() {
|
|
260
|
+
return Symbol.for("@schukai/monster/net/webconnect");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @property {string} url=undefined Defines the resource that you wish to fetch.
|
|
265
|
+
* @property {Object} connection
|
|
266
|
+
* @property {Object} connection.timeout=5000 Defines the timeout for the connection.
|
|
267
|
+
* @property {Number} connection.reconnect.timeout The timeout in milliseconds for the reconnect.
|
|
268
|
+
* @property {Number} connection.reconnect.attempts The maximum number of reconnects.
|
|
269
|
+
* @property {Bool} connection.reconnect.enabled If the reconnect is enabled.
|
|
270
|
+
*/
|
|
271
|
+
get defaults() {
|
|
272
|
+
return Object.assign({}, super.defaults, {
|
|
273
|
+
url: undefined,
|
|
274
|
+
connection: {
|
|
275
|
+
timeout: 5000,
|
|
276
|
+
reconnect: {
|
|
277
|
+
timeout: 1000,
|
|
278
|
+
attempts: 1,
|
|
279
|
+
enabled: false,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* This method closes the connection.
|
|
287
|
+
*
|
|
288
|
+
* @param {Number} [code=1000] The close code.
|
|
289
|
+
* @param {String} [reason=""] The close reason.
|
|
290
|
+
* @return {Promise}
|
|
291
|
+
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
|
|
292
|
+
*/
|
|
293
|
+
close(statusCode, reason) {
|
|
294
|
+
if (!isInteger(statusCode) || statusCode < 1000 || statusCode > 4999) {
|
|
295
|
+
statusCode = 1000;
|
|
296
|
+
}
|
|
297
|
+
if (!isString(reason)) {
|
|
298
|
+
reason = "";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
try {
|
|
303
|
+
// Set manual close flag BEFORE calling close() to prevent reconnect
|
|
304
|
+
this[manualCloseSymbol] = true;
|
|
305
|
+
if (this[connectionSymbol].socket) {
|
|
306
|
+
this[connectionSymbol].socket.close(statusCode, reason);
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
reject(error);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
resolve();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Polls the receive queue for new messages.
|
|
318
|
+
*
|
|
319
|
+
* @return {Message}
|
|
320
|
+
*/
|
|
321
|
+
poll() {
|
|
322
|
+
return this[receiveQueueSymbol].poll();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Are there any messages in the receive queue?
|
|
327
|
+
*
|
|
328
|
+
* @return {boolean}
|
|
329
|
+
*/
|
|
330
|
+
dataReceived() {
|
|
331
|
+
return !this[receiveQueueSymbol].isEmpty();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get Message from the receive queue, but do not remove it.
|
|
336
|
+
*
|
|
337
|
+
* @return {Object}
|
|
338
|
+
*/
|
|
339
|
+
peek() {
|
|
340
|
+
return this[receiveQueueSymbol].peek();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Attach a new observer
|
|
345
|
+
*
|
|
346
|
+
* @param {Observer} observer
|
|
347
|
+
* @return {ProxyObserver}
|
|
348
|
+
*/
|
|
349
|
+
attachObserver(observer) {
|
|
350
|
+
this[receiveQueueSymbol].attachObserver(observer);
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Detach a observer
|
|
356
|
+
*
|
|
357
|
+
* @param {Observer} observer
|
|
358
|
+
* @return {ProxyObserver}
|
|
359
|
+
*/
|
|
360
|
+
detachObserver(observer) {
|
|
361
|
+
this[receiveQueueSymbol].detachObserver(observer);
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @param {Observer} observer
|
|
367
|
+
* @return {boolean}
|
|
368
|
+
*/
|
|
369
|
+
containsObserver(observer) {
|
|
370
|
+
return this[receiveQueueSymbol].containsObserver(observer);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {Message|Object} message
|
|
375
|
+
* @return {Promise}
|
|
376
|
+
*/
|
|
377
|
+
send(message) {
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
if (
|
|
380
|
+
!this[connectionSymbol].socket ||
|
|
381
|
+
this[connectionSymbol].socket.readyState !== 1
|
|
382
|
+
) {
|
|
383
|
+
reject(new Error("The socket is not ready"));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
this[connectionSymbol].socket.send(JSON.stringify(message));
|
|
389
|
+
resolve();
|
|
390
|
+
} catch (e) {
|
|
391
|
+
reject(e);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
356
395
|
}
|