Skip to content

[webchannel] Add JavaScript client implementation#1

Closed
sampajano wants to merge 23 commits into
mainfrom
webchannel-js-export
Closed

[webchannel] Add JavaScript client implementation#1
sampajano wants to merge 23 commits into
mainfrom
webchannel-js-export

Conversation

@sampajano

Copy link
Copy Markdown
Owner

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request imports the WebChannel JavaScript client implementation based on the Google Closure Library, introducing bidirectional communication over HTTP. The reviewer provided several critical code-quality and bug-fixing suggestions: resolving a potential TypeError in ChannelRequest when handling string responses or nullified XMLHTTP objects, fixing a duplicate callback bug in netUtils.testPingServer during timeouts, preventing 414 Request-URI Too Large errors in WebChannelBase by safely handling undefined map sizes, and optimizing the message queue encoding loop in WireV8.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +772 to +778
const responseChunks =
/** @type {!Array<!Uint8Array>|string} */ (this.xmlHttp_.getResponse());
if (responseChunks === '') {
// in case of net::ERR_INTERNET_DISCONNECTED, this.xmlHttp_.getResponse()
// returns '' (empty string)
return '';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If responseChunks is a non-empty string, calling textDecoder.decode(responseChunks[i]) will throw a TypeError because responseChunks[i] is a string character, not an ArrayBuffer or ArrayBufferView. It is safer to check if responseChunks is a string and handle it directly without decoding.

Suggested change
const responseChunks =
/** @type {!Array<!Uint8Array>|string} */ (this.xmlHttp_.getResponse());
if (responseChunks === '') {
// in case of net::ERR_INTERNET_DISCONNECTED, this.xmlHttp_.getResponse()
// returns '' (empty string)
return '';
}
const responseChunks =
/** @type {!Array<!Uint8Array>|string} */ (this.xmlHttp_.getResponse());
if (typeof responseChunks === 'string') {
if (responseChunks !== '') {
this.fetchResponseState_.responseArrivedForFetch = true;
}
this.fetchResponseState_.responseBuffer += responseChunks;
this.xmlHttpChunkStart_ = 0;
return this.fetchResponseState_.responseBuffer;
}

Comment on lines +94 to +119
function testPingServer(url, timeout, callback) {
const channelDebug = new WebChannelDebug();
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
networkTestCallback(
channelDebug, 'TestPingServer: timeout', false, callback);
}, timeout);

fetch(url, {signal: controller.signal})
.then((response) => {
clearTimeout(timeoutId);
if (response.ok) {
networkTestCallback(
channelDebug, 'TestPingServer: ok', true, callback);
} else {
networkTestCallback(
channelDebug, 'TestPingServer: server error', false, callback);
}
})
.catch((error) => {
clearTimeout(timeoutId);
networkTestCallback(
channelDebug, 'TestPingServer: error', false, callback);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In testPingServer, calling networkTestCallback inside the setTimeout handler and then calling controller.abort() causes a duplicate callback invocation. Aborting the controller rejects the fetch promise, which triggers the .catch block and invokes networkTestCallback a second time. Letting the .catch block handle the timeout abort (by checking error.name === 'AbortError') avoids this duplicate callback bug.

function testPingServer(url, timeout, callback) {
  const channelDebug = new WebChannelDebug();
  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);

  fetch(url, {signal: controller.signal})
      .then((response) => {
        clearTimeout(timeoutId);
        if (response.ok) {
          networkTestCallback(
              channelDebug, 'TestPingServer: ok', true, callback);
        } else {
          networkTestCallback(
              channelDebug, 'TestPingServer: server error', false, callback);
        }
      })
      .catch((error) => {
        clearTimeout(timeoutId);
        const isTimeout = error.name === 'AbortError';
        networkTestCallback(
            channelDebug,
            isTimeout ? 'TestPingServer: timeout' : 'TestPingServer: error',
            false,
            callback);
      });
}

Comment on lines +578 to +582
if (this.hasResponseBody_()) {
const channelRequest = this;
this.channelDebug_.dumpException(ex, function() {
return 'ResponseText: ' + channelRequest.xmlHttp_.getResponseText();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Evaluating channelRequest.xmlHttp_.getResponseText() inside the lazy log callback can throw a TypeError if this.xmlHttp_ has been nullified during cleanup_ (which is common on request failures). Using the local xmlhttp parameter is much safer as it is guaranteed to remain non-null.

    if (this.hasResponseBody_()) {
      this.channelDebug_.dumpException(ex, function() {
        return 'ResponseText: ' + xmlhttp.getResponseText();
      });
    } else {

Comment on lines +1365 to +1372
*/
WebChannelBase.prototype.getMaxNumMessagesForFastHandshake_ = function() {
let total = 0;
for (let i = 0; i < this.outgoingMaps_.length; i++) {
const map = this.outgoingMaps_[i];
const size = map.getRawDataSize();
if (size === undefined) {
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In getMaxNumMessagesForFastHandshake_, if a map has an undefined size, the loop breaks and returns this.maxMapsPerRequest_ (1000). This can result in bundling up to 1000 maps in the GET request URL, leading to 414 Request-URI Too Large errors. Returning i instead of breaking ensures we only bundle maps whose sizes we can verify.

Suggested change
*/
WebChannelBase.prototype.getMaxNumMessagesForFastHandshake_ = function() {
let total = 0;
for (let i = 0; i < this.outgoingMaps_.length; i++) {
const map = this.outgoingMaps_[i];
const size = map.getRawDataSize();
if (size === undefined) {
break;
WebChannelBase.prototype.getMaxNumMessagesForFastHandshake_ = function() {
let total = 0;
for (let i = 0; i < this.outgoingMaps_.length; i++) {
const map = this.outgoingMaps_[i];
const size = map.getRawDataSize();
if (size === undefined) {
return i;
}

Comment on lines +102 to +107
if (mapId < 0) {
// redo the encoding in case of retry/reordering, plus extra space
offset = Math.max(0, messageQueue[i].mapId - 100);
done = false;
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In encodeMessageQueue, when mapId < 0, continuing the for loop is highly inefficient because the rest of the messages are still encoded even though the buffer will be discarded and the while loop restarted. Breaking the for loop immediately avoids wasting CPU cycles.

Suggested change
if (mapId < 0) {
// redo the encoding in case of retry/reordering, plus extra space
offset = Math.max(0, messageQueue[i].mapId - 100);
done = false;
continue;
}
if (mapId < 0) {
// redo the encoding in case of retry/reordering, plus extra space
offset = Math.max(0, messageQueue[i].mapId - 100);
done = false;
break;
}

@sampajano sampajano force-pushed the webchannel-js-export branch from c98cd0a to 9ec52e5 Compare June 8, 2026 23:29

@wenbozhu wenbozhu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@sampajano

sampajano commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

Thanks!

@wenbozhu Hi wenbo thanks for review!

Although, this is not really being merged to the main repo — just a test change for you to see.

If this looks good now, I'll send this against the main repo :)

@wenbozhu

wenbozhu commented Jun 9, 2026

Copy link
Copy Markdown

Yes. Thanks.

@sampajano

Copy link
Copy Markdown
Owner Author

Yes. Thanks.

Sure! Opened google#69

@sampajano sampajano force-pushed the webchannel-js-export branch from 9ec52e5 to d71e0af Compare June 10, 2026 17:29
@sampajano sampajano closed this Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants