@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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.46.7] - 2025-11-22
6
+
7
+ ### Bug Fixes
8
+
9
+ - performance update updater
10
+
11
+
12
+
13
+ ## [4.46.6] - 2025-11-22
14
+
15
+ ### Bug Fixes
16
+
17
+ - some NaN Issues
18
+
19
+
20
+
5
21
  ## [4.46.5] - 2025-11-22
6
22
 
7
23
  ### Bug Fixes
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.5"}
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"}
@@ -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
- return this.#processQueue();
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
- * updater.enableEventProcessing();
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
- * updater.enableEventProcessing();
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"].indexOf(this.type) !== -1) {
312
- return `${this.value}` === `${current}` ? "true" : undefined;
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
- if (this[timerElementEventHandlerSymbol] instanceof DeadMansSwitch) {
400
+ const switches = this[timerElementEventHandlerSymbol];
401
+ let dms = switches.get(element);
402
+
403
+ if (dms instanceof DeadMansSwitch) {
353
404
  try {
354
- this[timerElementEventHandlerSymbol].touch();
405
+ dms.touch();
355
406
  return;
356
407
  } catch (e) {
357
- delete this[timerElementEventHandlerSymbol];
408
+ switches.delete(element);
358
409
  }
359
410
  }
360
411
 
361
- this[timerElementEventHandlerSymbol] = new DeadMansSwitch(50, () => {
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
- value = Number(value);
451
- if (isNaN(value)) {
452
- value = 0;
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
- value =
459
- value === "true" || value === "1" || value === "on" || value === true;
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
- value = JSON.parse(value);
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 (isString(val)) {
539
- return val.trim() === ""
540
- ? []
541
- : val
542
- .split(",")
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 = new Pipe(cmd);
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
- const iterator = new Set([...e]);
882
+ // Performance optimization: avoid new Set([...NodeList])
883
+ const elements = container.querySelectorAll(`${query}`);
836
884
 
837
- if (container.matches(query)) {
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 = new Pipe(cmd);
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
- const e = container.querySelectorAll(query);
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
- for (const [element] of iterator.entries()) {
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
- continue;
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 = new Pipe(cmd);
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
- for (const [index, opt] of Object.entries(element.options)) {
988
- opt.selected = value.indexOf(opt.value) !== -1;
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 === 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
- element.value = value === undefined ? "" : value;
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
- element.value = value === undefined ? "" : value;
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, isObject } from "../types/is.mjs";
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
- 1000: "Normal closure",
56
- 1001: "Going away",
57
- 1002: "Protocol error",
58
- 1003: "Unsupported data",
59
- 1004: "Reserved",
60
- 1005: "No status code",
61
- 1006: "Connection closed abnormally",
62
- 1007: "Invalid frame payload data",
63
- 1008: "Policy violation",
64
- 1009: "The Message is too big",
65
- 1010: "Mandatory extension",
66
- 1011: "Internal server error",
67
- 1015: "TLS handshake",
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
- const self = this;
77
-
78
- const url = self.getOption("url");
79
- if (!url) {
80
- reject("No url defined for web connect.");
81
- return;
82
- }
83
-
84
- let promiseAllredyResolved = false;
85
-
86
- let connectionTimeout = self.getOption("connection.timeout");
87
- if (!isInteger(connectionTimeout) || connectionTimeout < 100) {
88
- connectionTimeout = 5000;
89
- }
90
-
91
- setTimeout(() => {
92
- if (promiseAllredyResolved) {
93
- return;
94
- }
95
- reject(new Error("Connection timeout"));
96
- }, connectionTimeout);
97
-
98
- let reconnectTimeout = self.getOption("connection.reconnect.timeout");
99
- if (!isInteger(reconnectTimeout) || reconnectTimeout < 1000)
100
- reconnectTimeout = 1000;
101
- let reconnectAttempts = self.getOption("connection.reconnect.attempts");
102
- if (!isInteger(reconnectAttempts) || reconnectAttempts < 1)
103
- reconnectAttempts = 1;
104
- let reconnectEnabled = self.getOption("connection.reconnect.enabled");
105
- if (reconnectEnabled !== true) reconnectEnabled = false;
106
-
107
- self[manualCloseSymbol] = false;
108
- self[connectionSymbol].reconnectCounter++;
109
-
110
- if (
111
- self[connectionSymbol].socket &&
112
- self[connectionSymbol].socket.readyState < 2
113
- ) {
114
- self[connectionSymbol].socket.close();
115
- }
116
- self[connectionSymbol].socket = null;
117
-
118
- const WebSocket = getGlobalFunction("WebSocket");
119
- if (!WebSocket) {
120
- reject(new Error("WebSocket is not available"));
121
- return;
122
- }
123
-
124
- self[connectionSymbol].socket = new WebSocket(url);
125
-
126
- self[connectionSymbol].socket.onmessage = function (event) {
127
- if (event.data instanceof Blob) {
128
- const reader = new FileReader();
129
- reader.addEventListener("loadend", function () {
130
- self[receiveQueueSymbol].add(new Message(reader.result));
131
- });
132
- reader.readAsText(new Message(event.data));
133
- } else {
134
- self[receiveQueueSymbol].add(Message.fromJSON(event.data));
135
- }
136
- };
137
-
138
- self[connectionSymbol].socket.onopen = function () {
139
- self[connectionSymbol].reconnectCounter = 0;
140
- if (typeof resolve === "function" && !promiseAllredyResolved) {
141
- promiseAllredyResolved = true;
142
- resolve();
143
- }
144
- };
145
-
146
- self[connectionSymbol].socket.close = function (event) {
147
- if (self[manualCloseSymbol]) {
148
- self[manualCloseSymbol] = false;
149
- return;
150
- }
151
-
152
- if (
153
- reconnectEnabled &&
154
- this[connectionSymbol].reconnectCounter < reconnectAttempts
155
- ) {
156
- setTimeout(() => {
157
- self.connect();
158
- }, reconnectTimeout * this[connectionSymbol].reconnectCounter);
159
- }
160
- };
161
-
162
- self[connectionSymbol].socket.onerror = (error) => {
163
- if (
164
- reconnectEnabled &&
165
- self[connectionSymbol].reconnectCounter < reconnectAttempts
166
- ) {
167
- setTimeout(() => {
168
- self.connect();
169
- }, reconnectTimeout * this[connectionSymbol].reconnectCounter);
170
- } else {
171
- if (typeof reject === "function" && !promiseAllredyResolved) {
172
- promiseAllredyResolved = true;
173
- reject(error);
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
- * @param {Object} [options] options contains definitions for the webconnect.
192
- */
193
- constructor(options) {
194
- if (isString(options)) {
195
- options = { url: options };
196
- }
197
-
198
- super(options);
199
-
200
- this[receiveQueueSymbol] = new ObservableQueue();
201
- this[sendQueueSymbol] = new ObservableQueue();
202
-
203
- this[connectionSymbol] = {};
204
- this[connectionSymbol].socket = null;
205
- this[connectionSymbol].reconnectCounter = 0;
206
- this[manualCloseSymbol] = false;
207
- }
208
-
209
- /**
210
- *
211
- * @return {Promise}
212
- */
213
- connect() {
214
- return new Promise((resolve, reject) => {
215
- connectServer.call(this, resolve, reject);
216
- });
217
- }
218
-
219
- /**
220
- * @return {boolean}
221
- */
222
- isConnected() {
223
- return this[connectionSymbol]?.socket?.readyState === 1;
224
- }
225
-
226
- /**
227
- * This method is called by the `instanceof` operator.
228
- * @return {symbol}
229
- */
230
- static get [instanceSymbol]() {
231
- return Symbol.for("@schukai/monster/net/webconnect");
232
- }
233
-
234
- /**
235
- * @property {string} url=undefined Defines the resource that you wish to fetch.
236
- * @property {Object} connection
237
- * @property {Object} connection.timeout=5000 Defines the timeout for the connection.
238
- * @property {Number} connection.reconnect.timeout The timeout in milliseconds for the reconnect.
239
- * @property {Number} connection.reconnect.attempts The maximum number of reconnects.
240
- * @property {Bool} connection.reconnect.enabled If the reconnect is enabled.
241
- */
242
- get defaults() {
243
- return Object.assign({}, super.defaults, {
244
- url: undefined,
245
- connection: {
246
- timeout: 5000,
247
- reconnect: {
248
- timeout: 1000,
249
- attempts: 1,
250
- enabled: false,
251
- },
252
- },
253
- });
254
- }
255
-
256
- /**
257
- * This method closes the connection.
258
- *
259
- * @param {Number} [code=1000] The close code.
260
- * @param {String} [reason=""] The close reason.
261
- * @return {Promise}
262
- * @see https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
263
- */
264
- close(statusCode, reason) {
265
- if (!isInteger(statusCode) || statusCode < 1000 || statusCode > 4999) {
266
- statusCode = 1000;
267
- }
268
- if (!isString(reason)) {
269
- reason = "";
270
- }
271
-
272
- return new Promise((resolve, reject) => {
273
- try {
274
- this[manualCloseSymbol] = true;
275
- if (this[connectionSymbol].socket) {
276
- this[connectionSymbol].socket.close(statusCode, reason);
277
- }
278
- } catch (error) {
279
- reject(error);
280
- }
281
- resolve();
282
- });
283
- }
284
-
285
- /**
286
- * Polls the receive queue for new messages.
287
- *
288
- * @return {Message}
289
- */
290
- poll() {
291
- return this[receiveQueueSymbol].poll();
292
- }
293
-
294
- /**
295
- * Are there any messages in the receive queue?
296
- *
297
- * @return {boolean}
298
- */
299
- dataReceived() {
300
- return !this[receiveQueueSymbol].isEmpty();
301
- }
302
-
303
- /**
304
- * Get Message from the receive queue, but do not remove it.
305
- *
306
- * @return {Object}
307
- */
308
- peek() {
309
- return this[receiveQueueSymbol].peek();
310
- }
311
-
312
- /**
313
- * Attach a new observer
314
- *
315
- * @param {Observer} observer
316
- * @return {ProxyObserver}
317
- */
318
- attachObserver(observer) {
319
- this[receiveQueueSymbol].attachObserver(observer);
320
- return this;
321
- }
322
-
323
- /**
324
- * Detach a observer
325
- *
326
- * @param {Observer} observer
327
- * @return {ProxyObserver}
328
- */
329
- detachObserver(observer) {
330
- this[receiveQueueSymbol].detachObserver(observer);
331
- return this;
332
- }
333
-
334
- /**
335
- * @param {Observer} observer
336
- * @return {boolean}
337
- */
338
- containsObserver(observer) {
339
- return this[receiveQueueSymbol].containsObserver(observer);
340
- }
341
-
342
- /**
343
- * @param {Message|Object} message
344
- * @return {Promise}
345
- */
346
- send(message) {
347
- return new Promise((resolve, reject) => {
348
- if (this[connectionSymbol].socket.readyState !== 1) {
349
- reject("the socket is not ready");
350
- }
351
-
352
- this[connectionSymbol].socket.send(JSON.stringify(message));
353
- resolve();
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
  }