@colyseus/sdk 0.17.40 → 0.17.42

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 (52) hide show
  1. package/build/3rd_party/discord.cjs +1 -1
  2. package/build/3rd_party/discord.mjs +1 -1
  3. package/build/Auth.cjs +1 -1
  4. package/build/Auth.mjs +1 -1
  5. package/build/Client.cjs +1 -1
  6. package/build/Client.mjs +1 -1
  7. package/build/Connection.cjs +1 -1
  8. package/build/Connection.mjs +1 -1
  9. package/build/HTTP.cjs +1 -1
  10. package/build/HTTP.mjs +1 -1
  11. package/build/Room.cjs +1 -1
  12. package/build/Room.mjs +1 -1
  13. package/build/Storage.cjs +1 -1
  14. package/build/Storage.mjs +1 -1
  15. package/build/core/nanoevents.cjs +1 -1
  16. package/build/core/nanoevents.mjs +1 -1
  17. package/build/core/signal.cjs +1 -1
  18. package/build/core/signal.mjs +1 -1
  19. package/build/core/utils.cjs +1 -1
  20. package/build/core/utils.mjs +1 -1
  21. package/build/debug.cjs +70 -44
  22. package/build/debug.cjs.map +1 -1
  23. package/build/debug.mjs +70 -44
  24. package/build/debug.mjs.map +1 -1
  25. package/build/errors/Errors.cjs +1 -1
  26. package/build/errors/Errors.mjs +1 -1
  27. package/build/fetchXHR.cjs +1 -1
  28. package/build/fetchXHR.mjs +1 -1
  29. package/build/index.cjs +1 -1
  30. package/build/index.mjs +1 -1
  31. package/build/legacy.cjs +1 -1
  32. package/build/legacy.mjs +1 -1
  33. package/build/serializer/NoneSerializer.cjs +1 -1
  34. package/build/serializer/NoneSerializer.mjs +1 -1
  35. package/build/serializer/SchemaSerializer.cjs +1 -1
  36. package/build/serializer/SchemaSerializer.mjs +1 -1
  37. package/build/serializer/Serializer.cjs +1 -1
  38. package/build/serializer/Serializer.mjs +1 -1
  39. package/build/transport/H3Transport.cjs +70 -21
  40. package/build/transport/H3Transport.cjs.map +1 -1
  41. package/build/transport/H3Transport.d.ts +16 -0
  42. package/build/transport/H3Transport.mjs +69 -23
  43. package/build/transport/H3Transport.mjs.map +1 -1
  44. package/build/transport/WebSocketTransport.cjs +1 -1
  45. package/build/transport/WebSocketTransport.mjs +1 -1
  46. package/dist/colyseus.js +69 -21
  47. package/dist/colyseus.js.map +1 -1
  48. package/dist/debug.js +70 -44
  49. package/dist/debug.js.map +1 -1
  50. package/package.json +4 -4
  51. package/src/debug.ts +69 -43
  52. package/src/transport/H3Transport.ts +74 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/sdk",
3
- "version": "0.17.40",
3
+ "version": "0.17.42",
4
4
  "description": "Colyseus Multiplayer SDK for JavaScript/TypeScript",
5
5
  "author": "Endel Dreyer",
6
6
  "license": "MIT",
@@ -55,8 +55,8 @@
55
55
  "@colyseus/schema": "^4.0.7",
56
56
  "tslib": "^2.1.0",
57
57
  "ws": "^8.13.0",
58
- "@colyseus/better-call": "^1.3.1",
59
- "@colyseus/shared-types": "^0.17.6"
58
+ "@colyseus/shared-types": "^0.17.6",
59
+ "@colyseus/better-call": "^1.3.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@rollup/plugin-alias": "^5.1.1",
@@ -82,7 +82,7 @@
82
82
  "typescript": "^5.9.3",
83
83
  "vite": "^5.0.11",
84
84
  "vitest": "^2.1.1",
85
- "@colyseus/core": "^0.17.41"
85
+ "@colyseus/core": "^0.17.42"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "@colyseus/core": "0.17.x"
package/src/debug.ts CHANGED
@@ -92,6 +92,19 @@ const BASE_MODAL_ZINDEX = 10000;
92
92
  // Load preferences on script load
93
93
  loadPreferences();
94
94
 
95
+ // Shadow DOM root — isolates all debug UI from page-level CSS.
96
+ // Every SDK element is appended here so page rules like `canvas { width: 100vw }`
97
+ // can't reach (or stretch) the debug panel's sparklines, logo, menu, or modals.
98
+ let _debugShadowRoot: ShadowRoot | null = null;
99
+ function getDebugRoot(): ShadowRoot {
100
+ if (_debugShadowRoot) return _debugShadowRoot;
101
+ const host = document.createElement('div');
102
+ host.id = 'colyseus-debug-shadow-host';
103
+ _debugShadowRoot = host.attachShadow({ mode: 'open' });
104
+ document.body.appendChild(host);
105
+ return _debugShadowRoot;
106
+ }
107
+
95
108
  // Function to select a modal (bring to front)
96
109
  function selectModal(modal) {
97
110
  if (!modal) return;
@@ -106,8 +119,9 @@ function selectModal(modal) {
106
119
  modalStack.push(modal);
107
120
 
108
121
  // Update z-indexes for all modals based on their position in stack
122
+ var root = getDebugRoot();
109
123
  modalStack.forEach((m, i) => {
110
- if (document.body.contains(m)) {
124
+ if (root.contains(m)) {
111
125
  m.style.zIndex = (BASE_MODAL_ZINDEX + i).toString();
112
126
  }
113
127
  });
@@ -126,7 +140,7 @@ document.addEventListener('keydown', function(e) {
126
140
  if (e.key === 'Escape' && modalStack.length > 0) {
127
141
  // Get the most recent modal (top of stack)
128
142
  const topModal = modalStack[modalStack.length - 1];
129
- if (topModal && document.body.contains(topModal)) {
143
+ if (topModal && getDebugRoot().contains(topModal)) {
130
144
  topModal.remove();
131
145
  }
132
146
  }
@@ -405,7 +419,7 @@ function initialize() {
405
419
  icon.insertAdjacentHTML('beforeend', logoIcon);
406
420
 
407
421
  container.appendChild(icon);
408
- document.body.appendChild(container);
422
+ getDebugRoot().appendChild(container);
409
423
 
410
424
  // Create menu first
411
425
  createMenu(container);
@@ -549,7 +563,8 @@ function createMenu(logoContainer) {
549
563
  }
550
564
 
551
565
  var styleId = 'latency-slider-style';
552
- var existingStyle = document.getElementById(styleId);
566
+ var root = getDebugRoot();
567
+ var existingStyle = root.getElementById(styleId);
553
568
  if (existingStyle) {
554
569
  existingStyle.remove();
555
570
  }
@@ -569,7 +584,7 @@ function createMenu(logoContainer) {
569
584
  border: none;
570
585
  }
571
586
  `;
572
- document.head.appendChild(style);
587
+ root.appendChild(style);
573
588
  }
574
589
 
575
590
  // Initialize slider color
@@ -615,11 +630,11 @@ function createMenu(logoContainer) {
615
630
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
616
631
  }
617
632
  `;
618
- document.head.appendChild(style);
633
+ getDebugRoot().appendChild(style);
619
634
 
620
635
  // Function to update container border color
621
636
  function updateContainerBackgroundColor() {
622
- var container = document.getElementById('debug-logo-container');
637
+ var container = getDebugRoot().getElementById('debug-logo-container');
623
638
  if (container) {
624
639
  // Update to normal state (hover handlers will update on hover)
625
640
  container.style.borderColor = getBorderColor(preferences.latencySimulation.delay, 0.7);
@@ -688,7 +703,7 @@ function createMenu(logoContainer) {
688
703
  });
689
704
  menu.appendChild(settingsOption);
690
705
 
691
- document.body.appendChild(menu);
706
+ getDebugRoot().appendChild(menu);
692
707
 
693
708
  // Toggle menu on logo click
694
709
  var menuVisible = false;
@@ -710,9 +725,15 @@ function createMenu(logoContainer) {
710
725
  }
711
726
  });
712
727
 
713
- // Close menu when clicking outside
728
+ // Close menu when clicking outside.
729
+ // Because menu/logo live inside a shadow root, event.target is retargeted
730
+ // to the shadow host for document-level listeners, so we walk the composed
731
+ // path to see whether the real click target was inside the menu or logo.
714
732
  document.addEventListener('click', function(e) {
715
- if (menuVisible && !menu.contains(e.target as Node) && !logoContainer.contains(e.target as Node)) {
733
+ var path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target as EventTarget];
734
+ var clickedInsideMenu = path.indexOf(menu) !== -1;
735
+ var clickedInsideLogo = path.indexOf(logoContainer) !== -1;
736
+ if (menuVisible && !clickedInsideMenu && !clickedInsideLogo) {
716
737
  menuVisible = false;
717
738
  menu.style.display = 'none';
718
739
  if (hostUpdateInterval) {
@@ -726,7 +747,7 @@ function createMenu(logoContainer) {
726
747
  // Create and open Settings modal
727
748
  function openSettingsModal() {
728
749
  // Remove existing modal if present
729
- var existingModal = document.getElementById('debug-settings-modal');
750
+ var existingModal = getDebugRoot().getElementById('debug-settings-modal');
730
751
  if (existingModal) {
731
752
  existingModal.remove();
732
753
  }
@@ -935,7 +956,7 @@ function openSettingsModal() {
935
956
  modal.appendChild(hideContainer);
936
957
 
937
958
  overlay.appendChild(modal);
938
- document.body.appendChild(overlay);
959
+ getDebugRoot().appendChild(overlay);
939
960
 
940
961
  // Mark as selected modal when opened
941
962
  selectModal(overlay);
@@ -977,7 +998,7 @@ function openSendMessagesModal(uniquePanelId) {
977
998
  }
978
999
 
979
1000
  // Remove existing modal if present
980
- var existingModal = document.getElementById('debug-send-messages-modal');
1001
+ var existingModal = getDebugRoot().getElementById('debug-send-messages-modal');
981
1002
  if (existingModal) {
982
1003
  existingModal.remove();
983
1004
  }
@@ -1422,7 +1443,7 @@ function openSendMessagesModal(uniquePanelId) {
1422
1443
  formContainer.appendChild(sendButton);
1423
1444
 
1424
1445
  modal.appendChild(formContainer);
1425
- document.body.appendChild(modal);
1446
+ getDebugRoot().appendChild(modal);
1426
1447
  }
1427
1448
 
1428
1449
  // Create and open State Inspector modal
@@ -1436,7 +1457,7 @@ function openStateInspectorModal(uniquePanelId) {
1436
1457
  var room = debugInfo.room;
1437
1458
 
1438
1459
  // Remove existing modal if present
1439
- var existingModal = document.getElementById('debug-state-inspector-modal');
1460
+ var existingModal = getDebugRoot().getElementById('debug-state-inspector-modal');
1440
1461
  if (existingModal) {
1441
1462
  existingModal.remove();
1442
1463
  }
@@ -1873,7 +1894,7 @@ function openStateInspectorModal(uniquePanelId) {
1873
1894
  }
1874
1895
 
1875
1896
  modal.appendChild(contentContainer);
1876
- document.body.appendChild(modal);
1897
+ getDebugRoot().appendChild(modal);
1877
1898
 
1878
1899
  // Drag and resize state variables
1879
1900
  var isDragging = false;
@@ -2095,9 +2116,10 @@ function openStateInspectorModal(uniquePanelId) {
2095
2116
 
2096
2117
  // Apply panel position based on current setting
2097
2118
  function applyPanelPosition() {
2098
- var logoContainer = document.getElementById('debug-logo-container');
2099
- var menu = document.getElementById('debug-menu');
2100
- var panels = document.querySelectorAll('[id^="debug-panel-"]');
2119
+ var root = getDebugRoot();
2120
+ var logoContainer = root.getElementById('debug-logo-container');
2121
+ var menu = root.getElementById('debug-menu');
2122
+ var panels = root.querySelectorAll('[id^="debug-panel-"]');
2101
2123
 
2102
2124
  var positions = {
2103
2125
  'bottom-right': { bottom: '14px', right: '14px', top: 'auto', left: 'auto' },
@@ -2139,9 +2161,10 @@ function hidePanelsForSession() {
2139
2161
  panelsHidden = true;
2140
2162
  savePreferences(); // Save the hidden state
2141
2163
 
2142
- var logoContainer = document.getElementById('debug-logo-container');
2143
- var menu = document.getElementById('debug-menu');
2144
- var panels = document.querySelectorAll('[id^="debug-panel-"]') as NodeListOf<HTMLElement>;
2164
+ var root = getDebugRoot();
2165
+ var logoContainer = root.getElementById('debug-logo-container');
2166
+ var menu = root.getElementById('debug-menu');
2167
+ var panels = root.querySelectorAll('[id^="debug-panel-"]') as NodeListOf<HTMLElement>;
2145
2168
 
2146
2169
  if (logoContainer) {
2147
2170
  logoContainer.style.display = 'none';
@@ -2170,7 +2193,7 @@ function formatBytes(bytes) {
2170
2193
  // Helper function to create debug panel for a room
2171
2194
  function createDebugPanel(uniquePanelId, debugInfo) {
2172
2195
  // Check if panel already exists
2173
- var existingPanel = document.getElementById('debug-panel-' + uniquePanelId);
2196
+ var existingPanel = getDebugRoot().getElementById('debug-panel-' + uniquePanelId);
2174
2197
  if (existingPanel) {
2175
2198
  return existingPanel;
2176
2199
  }
@@ -2327,11 +2350,12 @@ function createDebugPanel(uniquePanelId, debugInfo) {
2327
2350
  }
2328
2351
 
2329
2352
  // Inject CSS animation if not already present
2330
- if (!document.getElementById('debug-pulse-animation')) {
2353
+ var pulseRoot = getDebugRoot();
2354
+ if (!pulseRoot.getElementById('debug-pulse-animation')) {
2331
2355
  var style = document.createElement('style');
2332
2356
  style.id = 'debug-pulse-animation';
2333
2357
  style.textContent = '@keyframes debug-pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }';
2334
- document.head.appendChild(style);
2358
+ pulseRoot.appendChild(style);
2335
2359
  }
2336
2360
 
2337
2361
  // Apply initial style
@@ -2394,11 +2418,12 @@ function createDebugPanel(uniquePanelId, debugInfo) {
2394
2418
  panel.appendChild(content);
2395
2419
  panel.appendChild(actionsContainer);
2396
2420
 
2397
- // Prepend panel to body so new panels appear first
2398
- if (document.body.firstChild) {
2399
- document.body.insertBefore(panel, document.body.firstChild);
2421
+ // Prepend panel inside the shadow root so new panels appear first
2422
+ var root = getDebugRoot();
2423
+ if (root.firstChild) {
2424
+ root.insertBefore(panel, root.firstChild);
2400
2425
  } else {
2401
- document.body.appendChild(panel);
2426
+ root.appendChild(panel);
2402
2427
  }
2403
2428
 
2404
2429
  return panel;
@@ -2408,7 +2433,7 @@ function createDebugPanel(uniquePanelId, debugInfo) {
2408
2433
  function repositionDebugPanels() {
2409
2434
  if (panelsHidden) return;
2410
2435
 
2411
- var panels = Array.from(document.querySelectorAll('[id^="debug-panel-"]') as NodeListOf<HTMLElement>)
2436
+ var panels = Array.from(getDebugRoot().querySelectorAll('[id^="debug-panel-"]') as NodeListOf<HTMLElement>)
2412
2437
  .filter(function(panel: HTMLElement) { return panel.style.display !== 'none'; })
2413
2438
  .reverse(); // Reverse to get oldest first (since new panels are prepended)
2414
2439
 
@@ -2457,36 +2482,37 @@ function repositionDebugPanels() {
2457
2482
 
2458
2483
  // Update debug panel content
2459
2484
  function updateDebugPanel(uniquePanelId, debugInfo) {
2485
+ var root = getDebugRoot();
2460
2486
  var contentId = 'debug-content-' + uniquePanelId;
2461
2487
  var panelId = 'debug-panel-' + uniquePanelId;
2462
2488
  var titleId = 'debug-title-' + uniquePanelId;
2463
- var content = document.getElementById(contentId);
2464
- var panel = document.getElementById(panelId);
2465
- var title = document.getElementById(titleId);
2489
+ var content = root.getElementById(contentId);
2490
+ var panel = root.getElementById(panelId);
2491
+ var title = root.getElementById(titleId);
2466
2492
 
2467
2493
  if (!content || !panel) {
2468
2494
  // Only create if panel doesn't exist
2469
2495
  if (!panel) {
2470
2496
  createDebugPanel(uniquePanelId, debugInfo);
2471
- content = document.getElementById(contentId);
2472
- title = document.getElementById(titleId);
2497
+ content = root.getElementById(contentId);
2498
+ title = root.getElementById(titleId);
2473
2499
  repositionDebugPanels();
2474
2500
  } else {
2475
- content = document.getElementById(contentId);
2476
- title = document.getElementById(titleId);
2501
+ content = root.getElementById(contentId);
2502
+ title = root.getElementById(titleId);
2477
2503
  }
2478
2504
  }
2479
2505
 
2480
2506
  // Update title with room name only (roomId and sessionId are in tooltip)
2481
- var titleTextEl = document.getElementById('debug-title-text-' + uniquePanelId);
2507
+ var titleTextEl = root.getElementById('debug-title-text-' + uniquePanelId);
2482
2508
  var roomNameEl = titleTextEl?.querySelector('.debug-room-name');
2483
2509
  if (roomNameEl) roomNameEl.textContent = debugInfo.roomName;
2484
- document.getElementById('debug-tooltip-' + uniquePanelId).innerHTML = '<div><strong>Room ID:</strong> ' + debugInfo.roomId + '</div><div><strong>Session ID:</strong> ' + debugInfo.sessionId + '</div>';
2510
+ root.getElementById('debug-tooltip-' + uniquePanelId).innerHTML = '<div><strong>Room ID:</strong> ' + debugInfo.roomId + '</div><div><strong>Session ID:</strong> ' + debugInfo.sessionId + '</div>';
2485
2511
 
2486
2512
  // Update ping in header
2487
2513
  var pingDisplay = debugInfo.pingMs !== null ? debugInfo.pingMs + 'ms' : '--';
2488
2514
  var pingColor = debugInfo.pingMs !== null ? (debugInfo.pingMs < 100 ? '#22c55e' : debugInfo.pingMs < 200 ? '#eab308' : '#ef4444') : '#888';
2489
- var pingElement = document.getElementById('debug-ping-' + uniquePanelId);
2515
+ var pingElement = root.getElementById('debug-ping-' + uniquePanelId);
2490
2516
  if (pingElement) {
2491
2517
  pingElement.textContent = pingDisplay;
2492
2518
  pingElement.style.color = pingColor;
@@ -2516,7 +2542,7 @@ function updateDebugPanel(uniquePanelId, debugInfo) {
2516
2542
 
2517
2543
  // Draw graph on canvas
2518
2544
  function drawGraph(canvasId, data, color) {
2519
- var canvas = document.getElementById(canvasId) as HTMLCanvasElement;
2545
+ var canvas = getDebugRoot().getElementById(canvasId) as HTMLCanvasElement;
2520
2546
  if (!canvas) return;
2521
2547
 
2522
2548
  var ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
@@ -2702,7 +2728,7 @@ function applyMonkeyPatches() {
2702
2728
  debugInfo.messageTypes = messageTypes;
2703
2729
 
2704
2730
  // Show/hide message button based on message types availability
2705
- var messageBtnElement = document.getElementById('debug-message-btn-' + uniquePanelId);
2731
+ var messageBtnElement = getDebugRoot().getElementById('debug-message-btn-' + uniquePanelId);
2706
2732
  if (messageBtnElement) {
2707
2733
  messageBtnElement.style.display = messageTypes ? 'flex' : 'none';
2708
2734
  }
@@ -2834,7 +2860,7 @@ function applyMonkeyPatches() {
2834
2860
  debugInfo.pingInterval = null;
2835
2861
  }
2836
2862
  roomDebugInfo.delete(uniquePanelId);
2837
- var panel = document.getElementById('debug-panel-' + uniquePanelId);
2863
+ var panel = getDebugRoot().getElementById('debug-panel-' + uniquePanelId);
2838
2864
  if (panel) {
2839
2865
  panel.remove();
2840
2866
  repositionDebugPanels();
@@ -1,6 +1,69 @@
1
1
  import { encode, decode, type Iterator } from '@colyseus/schema';
2
2
  import type { ITransport, ITransportEventMap } from "./ITransport.ts";
3
3
 
4
+ // 9 bytes is the maximum length of a variable-length integer prefix
5
+ const MAX_LENGTH_PREFIX_BYTES = 9;
6
+
7
+ /**
8
+ * Reassembles length-prefixed frames from arbitrary byte chunks.
9
+ *
10
+ * A single WebTransport `reader.read()` may:
11
+ * - deliver multiple whole frames in one chunk
12
+ * - split a frame (or its length prefix) across multiple chunks
13
+ *
14
+ * This reassembler buffers partial data across reads so each dispatched
15
+ * frame is exactly one complete message.
16
+ */
17
+ export class FrameReassembler {
18
+ private pending: Uint8Array = new Uint8Array(0);
19
+
20
+ push(chunk: Uint8Array | undefined): Uint8Array[] {
21
+ if (!chunk || chunk.byteLength === 0) { return []; }
22
+
23
+ const bytes = (this.pending.byteLength === 0)
24
+ ? chunk
25
+ : concatBytes(this.pending, chunk);
26
+
27
+ const frames: Uint8Array[] = [];
28
+ let offset = 0;
29
+
30
+ while (offset < bytes.byteLength) {
31
+ const it: Iterator = { offset };
32
+ let length: number;
33
+
34
+ try {
35
+ length = decode.number(bytes as any, it);
36
+ } catch (e) {
37
+ // length prefix is incomplete — wait for more bytes
38
+ if (bytes.byteLength - offset <= MAX_LENGTH_PREFIX_BYTES) { break; }
39
+ throw e;
40
+ }
41
+
42
+ const frameEnd = it.offset + length;
43
+ if (frameEnd > bytes.byteLength) {
44
+ // payload is incomplete — wait for more bytes
45
+ break;
46
+ }
47
+
48
+ frames.push(bytes.subarray(it.offset, frameEnd));
49
+ offset = frameEnd;
50
+ }
51
+
52
+ this.pending = (offset < bytes.byteLength)
53
+ ? bytes.slice(offset)
54
+ : new Uint8Array(0);
55
+
56
+ return frames;
57
+ }
58
+ }
59
+
60
+ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
61
+ const out = new Uint8Array(a.byteLength + b.byteLength);
62
+ out.set(a, 0);
63
+ out.set(b, a.byteLength);
64
+ return out;
65
+ }
66
+
4
67
  export class H3TransportTransport implements ITransport {
5
68
  wt: WebTransport;
6
69
  isOpen: boolean = false;
@@ -14,6 +77,9 @@ export class H3TransportTransport implements ITransport {
14
77
 
15
78
  private lengthPrefixBuffer = new Uint8Array(9); // 9 bytes is the maximum length of a length prefix
16
79
 
80
+ private reliableReassembler = new FrameReassembler();
81
+ private unreliableReassembler = new FrameReassembler();
82
+
17
83
  constructor(events: ITransportEventMap) {
18
84
  this.events = events;
19
85
  }
@@ -110,19 +176,11 @@ export class H3TransportTransport implements ITransport {
110
176
  //
111
177
  // a single read may contain multiple messages
112
178
  // each message is prefixed with its length
179
+ // a read may also deliver a partial frame; buffer across reads
113
180
  //
114
-
115
- const messages = result.value;
116
- const it: Iterator = { offset: 0 };
117
- do {
118
- //
119
- // QUESTION: should we buffer the message in case it's not fully read?
120
- //
121
-
122
- const length = decode.number(messages as any, it);
123
- this.events.onmessage({ data: messages.subarray(it.offset, it.offset + length) });
124
- it.offset += length;
125
- } while (it.offset < messages.length);
181
+ for (const frame of this.reliableReassembler.push(result.value)) {
182
+ this.events.onmessage({ data: frame });
183
+ }
126
184
 
127
185
  } catch (e) {
128
186
  if (e.message.indexOf("session is closed") === -1) {
@@ -147,19 +205,11 @@ export class H3TransportTransport implements ITransport {
147
205
  //
148
206
  // a single read may contain multiple messages
149
207
  // each message is prefixed with its length
208
+ // a read may also deliver a partial frame; buffer across reads
150
209
  //
151
-
152
- const messages = result.value;
153
- const it: Iterator = { offset: 0 };
154
- do {
155
- //
156
- // QUESTION: should we buffer the message in case it's not fully read?
157
- //
158
-
159
- const length = decode.number(messages as any, it);
160
- this.events.onmessage({ data: messages.subarray(it.offset, it.offset + length) });
161
- it.offset += length;
162
- } while (it.offset < messages.length);
210
+ for (const frame of this.unreliableReassembler.push(result.value)) {
211
+ this.events.onmessage({ data: frame });
212
+ }
163
213
 
164
214
  } catch (e) {
165
215
  if (e.message.indexOf("session is closed") === -1) {