@necrolab/dashboard 0.5.28 → 0.5.30

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.
Files changed (39) hide show
  1. package/backend/api.js +7 -5
  2. package/backend/batching.js +59 -2
  3. package/backend/index.js +1 -1
  4. package/index.html +10 -23
  5. package/package.json +1 -1
  6. package/src/assets/css/base/scroll.scss +1 -1
  7. package/src/assets/css/main.scss +14 -14
  8. package/src/components/Console/ConsoleToolbar.vue +8 -8
  9. package/src/components/Editors/Account/Account.vue +9 -5
  10. package/src/components/Editors/Account/AccountView.vue +37 -18
  11. package/src/components/Editors/Account/CreateAccount.vue +38 -4
  12. package/src/components/Editors/Profile/CreateProfile.vue +29 -4
  13. package/src/components/Editors/Profile/Profile.vue +11 -6
  14. package/src/components/Editors/Profile/ProfileCountryChooser.vue +2 -2
  15. package/src/components/Editors/Profile/ProfileView.vue +37 -18
  16. package/src/components/Tasks/CreateTaskAXS.vue +16 -2
  17. package/src/components/Tasks/CreateTaskTM.vue +28 -5
  18. package/src/components/Tasks/QuickSettings.vue +77 -10
  19. package/src/components/Tasks/Task.vue +20 -7
  20. package/src/components/Tasks/TaskView.vue +144 -58
  21. package/src/components/Tasks/ViewTask.vue +17 -3
  22. package/src/components/ui/Modal.vue +1 -1
  23. package/src/components/ui/ReadonlyFieldsSection.vue +3 -3
  24. package/src/components/ui/StatusBadge.vue +1 -1
  25. package/src/components/ui/TaskToggle.vue +2 -3
  26. package/src/components/ui/controls/CountryChooser.vue +2 -2
  27. package/src/components/ui/controls/atomic/Dropdown.vue +2 -2
  28. package/src/components/ui/controls/atomic/MultiDropdown.vue +1 -1
  29. package/src/composables/useDynamicTableHeight.js +4 -4
  30. package/src/composables/useRowSelection.js +0 -1
  31. package/src/composables/useZoomPrevention.js +16 -55
  32. package/src/stores/connection.js +453 -68
  33. package/src/stores/sampleData.js +34 -24
  34. package/src/stores/ui.js +89 -100
  35. package/src/views/Accounts.vue +2 -5
  36. package/src/views/Console.vue +13 -14
  37. package/src/views/Profiles.vue +2 -5
  38. package/src/views/Tasks.vue +29 -1
  39. package/vite.config.js +4 -2
@@ -4,8 +4,356 @@ import { decode } from "@msgpack/msgpack";
4
4
  import router from "@/router/index";
5
5
  import { sortTaskIds } from "@/libs/utils/array";
6
6
 
7
+ const TASK_UPDATE_EVENTS = new Set(["task-update", "task-setstatus", "task-setinfo"]);
8
+ const QUEUE_STAT_FIELDS = new Set(["siteId", "eventId", "inQueue", "status", "queuePosition"]);
9
+ const TASK_FLUSH_CHUNK_SIZE = 300;
10
+ const TASK_INIT_APPLY_CHUNK_SIZE = 900;
11
+ const QUEUE_STATS_REFRESH_DELAY = 600;
12
+
7
13
  export class ConnectionHandler {
14
+ constructor() {
15
+ this.pendingTaskMessages = new Map();
16
+ this.deferredTaskMessages = [];
17
+ this.flushTaskFrame = null;
18
+ this.inFlightFlushMessages = null;
19
+ this.inFlightFlushIndex = 0;
20
+ this.inFlightShouldSort = false;
21
+ this.inFlightQueueRelevantChanges = false;
22
+ this.queueStatsRefreshTimer = null;
23
+ this.taskMessageSeq = 0;
24
+ this.isTaskInitInProgress = false;
25
+ this.pendingInitTaskIds = [];
26
+ this.oldTaskOrder = [];
27
+ this.taskInitFrame = null;
28
+ this.profileInitBuffer = null;
29
+ this.tmAccountInitBuffer = null;
30
+ this.axsAccountInitBuffer = null;
31
+ this.ui = null;
32
+ this.socket = null;
33
+ }
34
+
35
+ scheduleQueueStatsRefresh(immediate = false) {
36
+ if (immediate) {
37
+ clearTimeout(this.queueStatsRefreshTimer);
38
+ this.queueStatsRefreshTimer = null;
39
+ this.ui.refreshQueueStats();
40
+ return;
41
+ }
42
+
43
+ if (!this.isTasksRouteActive()) return;
44
+ if (this.queueStatsRefreshTimer) return;
45
+ this.queueStatsRefreshTimer = setTimeout(() => {
46
+ this.queueStatsRefreshTimer = null;
47
+ this.ui.refreshQueueStats();
48
+ }, QUEUE_STATS_REFRESH_DELAY);
49
+ }
50
+
51
+ isTasksRouteActive() {
52
+ const path = router.currentRoute?.value?.path || window.location.pathname;
53
+ return path === "/" || path === "/tasks";
54
+ }
55
+
56
+ insertTaskId(taskId) {
57
+ if (this.ui.taskIdOrder.includes(taskId)) return false;
58
+
59
+ if (this.ui.sortData.sortBy) {
60
+ this.ui.taskIdOrder.push(taskId);
61
+ return true;
62
+ }
63
+
64
+ let left = 0;
65
+ let right = this.ui.taskIdOrder.length;
66
+
67
+ while (left < right) {
68
+ const mid = Math.floor((left + right) / 2);
69
+ if (sortTaskIds(taskId, this.ui.taskIdOrder[mid]) < 0) right = mid;
70
+ else left = mid + 1;
71
+ }
72
+
73
+ this.ui.taskIdOrder.splice(left, 0, taskId);
74
+ return true;
75
+ }
76
+
77
+ mergeSortedTaskIds(existingIds, newSortedIds) {
78
+ const out = [];
79
+ let i = 0;
80
+ let j = 0;
81
+
82
+ while (i < existingIds.length && j < newSortedIds.length) {
83
+ if (sortTaskIds(existingIds[i], newSortedIds[j]) <= 0) out.push(existingIds[i++]);
84
+ else out.push(newSortedIds[j++]);
85
+ }
86
+
87
+ while (i < existingIds.length) out.push(existingIds[i++]);
88
+ while (j < newSortedIds.length) out.push(newSortedIds[j++]);
89
+ return out;
90
+ }
91
+
92
+ mergeTaskMessages(existing, incoming, taskId) {
93
+ if (!existing) return { ...incoming, id: incoming.id || taskId };
94
+ if (incoming?.task?.removed) return { ...incoming, id: incoming.id || taskId };
95
+ if (existing?.task?.removed) return existing;
96
+
97
+ const merged = {
98
+ ...existing,
99
+ ...incoming,
100
+ id: incoming.id || existing.id || taskId
101
+ };
102
+
103
+ if (existing.task || incoming.task) {
104
+ merged.task = {
105
+ ...(existing.task || {}),
106
+ ...(incoming.task || {})
107
+ };
108
+ }
109
+
110
+ if (existing.update || incoming.update) {
111
+ merged.update = {
112
+ ...(existing.update || {}),
113
+ ...(incoming.update || {})
114
+ };
115
+ }
116
+
117
+ if (merged.task && merged.update) {
118
+ Object.assign(merged.task, merged.update);
119
+ delete merged.update;
120
+ }
121
+
122
+ return merged;
123
+ }
124
+
125
+ applyTaskMessage(msg) {
126
+ const taskId = msg.id || msg.task?.taskId;
127
+ if (!taskId) return { changed: false, added: false, queueRelevantChanged: false };
128
+
129
+ if (msg?.task?.removed) {
130
+ const existed = Boolean(this.ui.tasks[taskId]);
131
+ if (existed) delete this.ui.tasks[taskId];
132
+
133
+ if (this.ui.taskIdOrder.includes(taskId)) {
134
+ this.ui.taskIdOrder = this.ui.taskIdOrder.filter((id) => id !== taskId);
135
+ return { changed: true, added: false, queueRelevantChanged: true };
136
+ }
137
+
138
+ return { changed: existed, added: false, queueRelevantChanged: existed };
139
+ }
140
+
141
+ if (msg.update) {
142
+ const existingTask = this.ui.tasks[taskId];
143
+ if (!existingTask) return { changed: false, added: false, queueRelevantChanged: false };
144
+ const queueRelevantChanged = this.messageAffectsQueueStats(msg);
145
+ const changed = this.assignChangedFields(existingTask, msg.update);
146
+ if (!changed) return { changed: false, added: false, queueRelevantChanged: false };
147
+ return { changed: true, added: false, queueRelevantChanged };
148
+ }
149
+
150
+ if (!msg.task) return { changed: false, added: false, queueRelevantChanged: false };
151
+
152
+ if (!this.ui.tasks[taskId]) {
153
+ this.ui.tasks[taskId] = msg.task;
154
+ this.insertTaskId(taskId);
155
+ return { changed: true, added: true, queueRelevantChanged: true };
156
+ }
157
+
158
+ const queueRelevantChanged = this.messageAffectsQueueStats(msg);
159
+ const changed = this.assignChangedFields(this.ui.tasks[taskId], msg.task);
160
+ if (!changed) return { changed: false, added: false, queueRelevantChanged: false };
161
+ return { changed: true, added: false, queueRelevantChanged };
162
+ }
163
+
164
+ assignChangedFields(target, source) {
165
+ let changed = false;
166
+ for (const [key, nextValue] of Object.entries(source)) {
167
+ if (target[key] === nextValue) continue;
168
+ target[key] = nextValue;
169
+ changed = true;
170
+ }
171
+ return changed;
172
+ }
173
+
174
+ flushQueuedTaskMessages() {
175
+ if (!this.inFlightFlushMessages?.length) {
176
+ if (!this.pendingTaskMessages.size) return;
177
+ this.inFlightFlushMessages = Array.from(this.pendingTaskMessages.values())
178
+ .sort((a, b) => a.seq - b.seq)
179
+ .map((entry) => entry.message);
180
+ this.pendingTaskMessages.clear();
181
+ this.inFlightFlushIndex = 0;
182
+ this.inFlightShouldSort = false;
183
+ this.inFlightQueueRelevantChanges = false;
184
+ }
185
+
186
+ const end = Math.min(
187
+ this.inFlightFlushIndex + TASK_FLUSH_CHUNK_SIZE,
188
+ this.inFlightFlushMessages.length
189
+ );
190
+
191
+ for (let index = this.inFlightFlushIndex; index < end; index++) {
192
+ const msg = this.inFlightFlushMessages[index];
193
+ const { changed, added, queueRelevantChanged } = this.applyTaskMessage(msg);
194
+ if (!changed) continue;
195
+ if (added) this.inFlightShouldSort = true;
196
+ if (queueRelevantChanged) this.inFlightQueueRelevantChanges = true;
197
+ }
198
+
199
+ this.inFlightFlushIndex = end;
200
+ if (this.inFlightFlushIndex < this.inFlightFlushMessages.length) {
201
+ this.flushTaskFrame = requestAnimationFrame(() => {
202
+ this.flushTaskFrame = null;
203
+ this.flushQueuedTaskMessages();
204
+ });
205
+ return;
206
+ }
207
+
208
+ if (this.inFlightShouldSort && this.ui.sortData.sortBy) this.ui.toggleSort();
209
+ if (this.inFlightQueueRelevantChanges) this.scheduleQueueStatsRefresh();
210
+
211
+ this.inFlightFlushMessages = null;
212
+ this.inFlightFlushIndex = 0;
213
+ this.inFlightShouldSort = false;
214
+ this.inFlightQueueRelevantChanges = false;
215
+
216
+ if (!this.pendingTaskMessages.size) return;
217
+ this.flushTaskFrame = requestAnimationFrame(() => {
218
+ this.flushTaskFrame = null;
219
+ this.flushQueuedTaskMessages();
220
+ });
221
+ }
222
+
223
+ enqueueTaskMessage(msg) {
224
+ const taskId = msg.id || msg.task?.taskId;
225
+ if (!taskId) return;
226
+
227
+ const queueKey = `${msg.event}:${taskId}`;
228
+ const existingEntry = this.pendingTaskMessages.get(queueKey);
229
+ const merged = this.mergeTaskMessages(existingEntry?.message, msg, taskId);
230
+ this.pendingTaskMessages.set(queueKey, {
231
+ message: merged,
232
+ seq: ++this.taskMessageSeq
233
+ });
234
+
235
+ if (this.flushTaskFrame) return;
236
+ this.flushTaskFrame = requestAnimationFrame(() => {
237
+ this.flushTaskFrame = null;
238
+ this.flushQueuedTaskMessages();
239
+ });
240
+ }
241
+
242
+ handleIncomingBatch(messages) {
243
+ if (!Array.isArray(messages)) return;
244
+ messages.forEach((msg) => this.handleWebsocketMessages(msg));
245
+ }
246
+
247
+ clearTaskUpdateQueue() {
248
+ this.pendingTaskMessages.clear();
249
+ this.deferredTaskMessages = [];
250
+ this.inFlightFlushMessages = null;
251
+ this.inFlightFlushIndex = 0;
252
+ this.inFlightShouldSort = false;
253
+ this.inFlightQueueRelevantChanges = false;
254
+ if (this.flushTaskFrame) {
255
+ cancelAnimationFrame(this.flushTaskFrame);
256
+ this.flushTaskFrame = null;
257
+ }
258
+ if (this.taskInitFrame) {
259
+ cancelAnimationFrame(this.taskInitFrame);
260
+ this.taskInitFrame = null;
261
+ }
262
+ }
263
+
264
+ messageAffectsQueueStats(msg) {
265
+ if (msg?.task?.removed) return true;
266
+
267
+ const payload = msg.task || msg.update;
268
+ if (!payload || typeof payload !== "object") return false;
269
+ return Object.keys(payload).some((key) => QUEUE_STAT_FIELDS.has(key));
270
+ }
271
+
272
+ handleTaskInitStart() {
273
+ this.clearTaskUpdateQueue();
274
+ this.isTaskInitInProgress = true;
275
+ this.oldTaskOrder = [...this.ui.taskIdOrder];
276
+ this.pendingInitTaskIds = [];
277
+ this.ui.tasks = {};
278
+ this.ui.taskIdOrder = [];
279
+ }
280
+
281
+ applyInitTaskChunk(tasksChunk) {
282
+ const ids = Object.keys(tasksChunk || {});
283
+ if (!ids.length) return;
284
+
285
+ ids.forEach((id) => {
286
+ this.ui.tasks[id] = tasksChunk[id];
287
+ });
288
+ this.pendingInitTaskIds.push(...ids);
289
+ }
290
+
291
+ finalizeTaskInit() {
292
+ const newIdSet = new Set(this.pendingInitTaskIds);
293
+ const oldIdSet = new Set(this.oldTaskOrder);
294
+
295
+ const existingIds = this.oldTaskOrder.filter((id) => newIdSet.has(id));
296
+ const missingIds = this.pendingInitTaskIds.filter((id) => !oldIdSet.has(id));
297
+ missingIds.sort(sortTaskIds);
298
+
299
+ this.ui.taskIdOrder = [...existingIds, ...missingIds];
300
+
301
+ if (this.ui.sortData.sortBy) this.ui.toggleSort();
302
+
303
+ this.isTaskInitInProgress = false;
304
+ this.pendingInitTaskIds = [];
305
+ this.oldTaskOrder = [];
306
+ this.scheduleQueueStatsRefresh(true);
307
+ this.ui.hideSpinner();
308
+
309
+ if (!this.deferredTaskMessages.length) return;
310
+ const bufferedMessages = this.deferredTaskMessages;
311
+ this.deferredTaskMessages = [];
312
+ bufferedMessages.forEach((message) => this.enqueueTaskMessage(message));
313
+ }
314
+
315
+ applyTasksSnapshotInChunks(tasksSnapshot) {
316
+ this.handleTaskInitStart();
317
+
318
+ const entries = Object.entries(tasksSnapshot || {});
319
+ const total = entries.length;
320
+ if (!total) {
321
+ this.finalizeTaskInit();
322
+ return;
323
+ }
324
+
325
+ let cursor = 0;
326
+ const applyChunk = () => {
327
+ const end = Math.min(cursor + TASK_INIT_APPLY_CHUNK_SIZE, total);
328
+ for (let index = cursor; index < end; index++) {
329
+ const [taskId, task] = entries[index];
330
+ this.ui.tasks[taskId] = task;
331
+ this.pendingInitTaskIds.push(taskId);
332
+ }
333
+
334
+ cursor = end;
335
+ if (cursor < total) {
336
+ this.taskInitFrame = requestAnimationFrame(applyChunk);
337
+ return;
338
+ }
339
+
340
+ this.taskInitFrame = null;
341
+ this.finalizeTaskInit();
342
+ };
343
+
344
+ applyChunk();
345
+ }
346
+
8
347
  handleWebsocketMessages(msg) {
348
+ if (TASK_UPDATE_EVENTS.has(msg.event)) {
349
+ if (this.isTaskInitInProgress) {
350
+ this.deferredTaskMessages.push(msg);
351
+ return;
352
+ }
353
+ this.enqueueTaskMessage(msg);
354
+ return;
355
+ }
356
+
9
357
  switch (msg.event) {
10
358
  case "startup":
11
359
  if (msg.done) {
@@ -32,20 +380,23 @@ export class ConnectionHandler {
32
380
  this.ui.tasks = {};
33
381
  this.ui.taskIdOrder = [];
34
382
  this.ui.showSuccess("Deleted all tasks!");
383
+ this.scheduleQueueStatsRefresh(true);
35
384
  break;
36
385
 
37
- case "init-tasks":
38
- var oldOrder = [...this.ui.taskIdOrder];
39
- this.ui.tasks = msg.tasks;
40
- var newIds = Object.keys(msg.tasks);
386
+ case "init-tasks-start":
387
+ this.handleTaskInitStart();
388
+ break;
41
389
 
42
- var existingIds = oldOrder.filter((id) => newIds.includes(id));
43
- var missingTaskIds = newIds.filter((id) => !oldOrder.includes(id));
44
- missingTaskIds.sort(sortTaskIds);
390
+ case "init-tasks-chunk":
391
+ this.applyInitTaskChunk(msg.tasks);
392
+ break;
45
393
 
46
- this.ui.taskIdOrder = [...existingIds, ...missingTaskIds];
47
- this.ui.refreshQueueStats();
48
- this.ui.hideSpinner();
394
+ case "init-tasks-done":
395
+ this.finalizeTaskInit();
396
+ break;
397
+
398
+ case "init-tasks":
399
+ this.applyTasksSnapshotInChunks(msg.tasks);
49
400
  break;
50
401
 
51
402
  case "init-profiles":
@@ -53,86 +404,109 @@ export class ConnectionHandler {
53
404
  this.ui.hideSpinner();
54
405
  break;
55
406
 
407
+ case "init-profiles-start":
408
+ this.profileInitBuffer = [];
409
+ break;
410
+
411
+ case "init-profiles-chunk":
412
+ if (!this.profileInitBuffer) this.profileInitBuffer = [];
413
+ this.profileInitBuffer.push(...(msg.items || []));
414
+ break;
415
+
416
+ case "init-profiles-done":
417
+ this.ui.setProfiles(this.profileInitBuffer || []);
418
+ this.profileInitBuffer = null;
419
+ this.ui.hideSpinner();
420
+ break;
421
+
56
422
  case "init-proxies":
57
423
  this.ui.setProxies(msg.proxies);
58
424
  this.ui.hideSpinner();
59
425
  break;
60
426
 
61
427
  case "init-tm-accounts":
62
- this.ui.setAccounts(msg.accounts.map((acc) => ({ ...acc, module: "TM" })));
428
+ this.ui.setAccountsForModule(
429
+ "TM",
430
+ msg.accounts.map((acc) => ({ ...acc, module: "TM" }))
431
+ );
63
432
  this.ui.hideSpinner();
64
433
  break;
65
434
 
66
435
  case "init-axs-accounts":
67
- this.ui.setAccounts(msg.accounts.map((acc) => ({ ...acc, module: "AXS" })));
436
+ this.ui.setAccountsForModule(
437
+ "AXS",
438
+ msg.accounts.map((acc) => ({ ...acc, module: "AXS" }))
439
+ );
440
+ this.ui.hideSpinner();
441
+ break;
442
+
443
+ case "init-tm-accounts-start":
444
+ this.tmAccountInitBuffer = [];
445
+ break;
446
+
447
+ case "init-tm-accounts-chunk":
448
+ if (!this.tmAccountInitBuffer) this.tmAccountInitBuffer = [];
449
+ this.tmAccountInitBuffer.push(
450
+ ...(msg.items || []).map((account) => ({ ...account, module: "TM" }))
451
+ );
452
+ break;
453
+
454
+ case "init-tm-accounts-done":
455
+ this.ui.setAccountsForModule("TM", this.tmAccountInitBuffer || []);
456
+ this.tmAccountInitBuffer = null;
68
457
  this.ui.hideSpinner();
69
458
  break;
70
459
 
71
- case "add-task":
460
+ case "init-axs-accounts-start":
461
+ this.axsAccountInitBuffer = [];
462
+ break;
463
+
464
+ case "init-axs-accounts-chunk":
465
+ if (!this.axsAccountInitBuffer) this.axsAccountInitBuffer = [];
466
+ this.axsAccountInitBuffer.push(
467
+ ...(msg.items || []).map((account) => ({ ...account, module: "AXS" }))
468
+ );
469
+ break;
470
+
471
+ case "init-axs-accounts-done":
472
+ this.ui.setAccountsForModule("AXS", this.axsAccountInitBuffer || []);
473
+ this.axsAccountInitBuffer = null;
474
+ this.ui.hideSpinner();
475
+ break;
476
+
477
+ case "add-task": {
72
478
  this.ui.tasks[msg.task.taskId] = msg.task;
73
479
 
74
- if (!this.ui.taskIdOrder.includes(msg.task.taskId)) {
75
- let pos = this.ui.taskIdOrder.length;
76
- for (let i = 0; i < this.ui.taskIdOrder.length; i++) {
77
- if (sortTaskIds(msg.task.taskId, this.ui.taskIdOrder[i]) < 0) {
78
- pos = i;
79
- break;
80
- }
81
- }
82
- this.ui.taskIdOrder.splice(pos, 0, msg.task.taskId);
83
- }
480
+ const inserted = this.insertTaskId(msg.task.taskId);
84
481
 
85
- this.ui.toggleSort();
482
+ if (inserted && this.ui.sortData.sortBy) this.ui.toggleSort();
86
483
  this.ui.showSuccess("Created 1 task!");
87
- this.ui.refreshQueueStats();
484
+ this.scheduleQueueStatsRefresh();
88
485
  break;
486
+ }
89
487
 
90
- case "add-tasks":
488
+ case "add-tasks": {
91
489
  var batchTaskIds = Object.keys(msg.tasks).sort(sortTaskIds);
490
+ const existingOrderSet = new Set(this.ui.taskIdOrder);
491
+ const batchMissingTaskIds = batchTaskIds.filter((id) => !existingOrderSet.has(id));
492
+
493
+ Object.entries(msg.tasks).forEach(([id, task]) => {
494
+ this.ui.tasks[id] = task;
495
+ });
92
496
 
93
- for (const id of batchTaskIds) {
94
- const value = msg.tasks[id];
95
- this.ui.tasks[id] = value;
96
-
97
- if (!this.ui.taskIdOrder.includes(id)) {
98
- let pos = this.ui.taskIdOrder.length;
99
- for (let i = 0; i < this.ui.taskIdOrder.length; i++) {
100
- if (sortTaskIds(id, this.ui.taskIdOrder[i]) < 0) {
101
- pos = i;
102
- break;
103
- }
104
- }
105
- this.ui.taskIdOrder.splice(pos, 0, id);
497
+ if (batchMissingTaskIds.length) {
498
+ if (this.ui.sortData.sortBy) {
499
+ this.ui.taskIdOrder.push(...batchMissingTaskIds);
500
+ this.ui.toggleSort();
501
+ } else {
502
+ this.ui.taskIdOrder = this.mergeSortedTaskIds(this.ui.taskIdOrder, batchMissingTaskIds);
106
503
  }
107
504
  }
505
+
108
506
  this.ui.showSuccess(`Created ${batchTaskIds.length} tasks!`);
109
- this.ui.refreshQueueStats();
110
- break;
111
-
112
- case "task-update":
113
- case "task-setstatus":
114
- case "task-setinfo":
115
- if (msg?.task?.removed) {
116
- delete this.ui.tasks[msg.id];
117
- this.ui.taskIdOrder = this.ui.taskIdOrder.filter((i) => i !== msg.id);
118
- } else if (msg.update) {
119
- try {
120
- Object.entries(msg.update).forEach((change) => (this.ui.tasks[msg.id][change[0]] = change[1]));
121
- } catch {
122
- // Ignore update errors if task doesn't exist
123
- }
124
- } else {
125
- if (!this.ui.tasks[msg.id]) this.ui.tasks[msg.id] = msg.task;
126
- if (!this.ui.taskIdOrder.includes(msg.id)) {
127
- this.ui.taskIdOrder.push(msg.id);
128
- this.ui.sortTasks();
129
- } else
130
- Object.keys(msg.task).forEach((key) => {
131
- this.ui.tasks[msg.id][key] = msg.task[key];
132
- });
133
- }
134
- this.ui.refreshQueueStats();
507
+ this.scheduleQueueStatsRefresh();
135
508
  break;
509
+ }
136
510
 
137
511
  case "edit-presale-code":
138
512
  for (const value of Object.values(this.ui.tasks)) {
@@ -212,22 +586,33 @@ export class ConnectionHandler {
212
586
  }
213
587
 
214
588
  // if event is not json but msgpack encoded, decode it
215
- if (event.data instanceof Blob || event.data instanceof ArrayBuffer) {
589
+ if (event.data instanceof ArrayBuffer) {
590
+ const data = decode(new Uint8Array(event.data));
591
+ this.handleIncomingBatch(data);
592
+ return;
593
+ }
594
+
595
+ if (event.data instanceof Blob) {
216
596
  event.data.arrayBuffer().then((buffer) => {
217
597
  const data = decode(new Uint8Array(buffer));
218
- data.forEach((d) => this.handleWebsocketMessages(d));
598
+ this.handleIncomingBatch(data);
219
599
  });
220
- } else JSON.parse(event.data).forEach((e) => this.handleWebsocketMessages(e));
600
+ return;
601
+ }
602
+
603
+ this.handleIncomingBatch(JSON.parse(event.data));
221
604
  };
222
605
 
223
606
  this.socket.onclose = () => {
224
607
  this.ui.startSpinner("Reconnecting");
225
608
  hasShownDisconnect = true;
609
+ this.clearTaskUpdateQueue();
226
610
  };
227
611
 
228
612
  this.socket.onerror = () => {
229
613
  this.ui.startSpinner("Reconnecting");
230
614
  hasShownDisconnect = true;
615
+ this.clearTaskUpdateQueue();
231
616
  };
232
617
 
233
618
  }