Skip to content

Commit a48276f

Browse files
committed
Split out JsProtobufUtils (#DH-20578)
1 parent 4d2a11d commit a48276f

File tree

2 files changed

+180
-162
lines changed

2 files changed

+180
-162
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//
2+
// Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending
3+
//
4+
package io.deephaven.web.client.api;
5+
6+
import elemental2.core.Uint8Array;
7+
import elemental2.dom.TextEncoder;
8+
9+
/**
10+
* Utility methods for working with protobuf messages in the client-side JavaScript environment.
11+
*/
12+
public class JsProtobufUtils {
13+
14+
private JsProtobufUtils() {
15+
// Utility class, no instantiation
16+
}
17+
18+
/**
19+
* Wraps a protobuf message in a google.protobuf.Any message.
20+
* <p>
21+
* The google.protobuf.Any message has two fields:
22+
* <ul>
23+
* <li>Field 1: type_url (string) - identifies the type of message contained</li>
24+
* <li>Field 2: value (bytes) - the actual serialized message</li>
25+
* </ul>
26+
* <p>
27+
* This method manually encodes the Any message in protobuf binary format since the client-side
28+
* JavaScript protobuf library doesn't provide Any.pack() like the server-side Java library does.
29+
*
30+
* @param typeUrl the type URL for the message (e.g., "type.googleapis.com/package.MessageName")
31+
* @param messageBytes the serialized protobuf message bytes
32+
* @return the serialized Any message containing the wrapped message
33+
*/
34+
public static Uint8Array wrapInAny(String typeUrl, Uint8Array messageBytes) {
35+
// Protobuf tag constants for google.protobuf.Any message fields
36+
// Tag format: (field_number << 3) | wire_type
37+
// wire_type=2 means length-delimited (for strings/bytes)
38+
final int TYPE_URL_TAG = 10; // (1 << 3) | 2 = field 1, wire type 2
39+
final int VALUE_TAG = 18; // (2 << 3) | 2 = field 2, wire type 2
40+
41+
// Encode the type_url string to UTF-8 bytes
42+
TextEncoder textEncoder = new TextEncoder();
43+
Uint8Array typeUrlBytes = textEncoder.encode(typeUrl);
44+
45+
// Calculate sizes for protobuf binary encoding
46+
int typeUrlFieldSize = calculateFieldSize(TYPE_URL_TAG, typeUrlBytes.length);
47+
int valueFieldSize = calculateFieldSize(VALUE_TAG, messageBytes.length);
48+
49+
// Allocate buffer for the complete Any message
50+
int totalSize = typeUrlFieldSize + valueFieldSize;
51+
Uint8Array result = new Uint8Array(totalSize);
52+
int pos = 0;
53+
54+
// Write field 1 (type_url) in protobuf binary format
55+
pos = writeField(result, pos, TYPE_URL_TAG, typeUrlBytes);
56+
57+
// Write field 2 (value) in protobuf binary format
58+
writeField(result, pos, VALUE_TAG, messageBytes);
59+
60+
return result;
61+
}
62+
63+
/**
64+
* Calculates the total size needed for a protobuf length-delimited field.
65+
* <p>
66+
* A length-delimited field consists of:
67+
* <ul>
68+
* <li>Tag (field number + wire type) encoded as a varint</li>
69+
* <li>Length of the data encoded as a varint</li>
70+
* <li>The actual data bytes</li>
71+
* </ul>
72+
*
73+
* @param tag the protobuf field tag (field number << 3 | wire type)
74+
* @param dataLength the length of the data in bytes
75+
* @return the total number of bytes needed for this field
76+
*/
77+
private static int calculateFieldSize(int tag, int dataLength) {
78+
return sizeOfVarint(tag) + sizeOfVarint(dataLength) + dataLength;
79+
}
80+
81+
/**
82+
* Calculates how many bytes a varint encoding will require for the given value.
83+
* <p>
84+
* Protobuf uses varint encoding where each byte stores 7 bits of data (the 8th bit is
85+
* a continuation flag). This means:
86+
* <ul>
87+
* <li>1 byte: 0 to 127 (2^7 - 1)</li>
88+
* <li>2 bytes: 128 to 16,383 (2^14 - 1)</li>
89+
* <li>3 bytes: 16,384 to 2,097,151 (2^21 - 1)</li>
90+
* <li>4 bytes: 2,097,152 to 268,435,455 (2^28 - 1)</li>
91+
* <li>5 bytes: 268,435,456 to 4,294,967,295 (2^35 - 1, max unsigned 32-bit)</li>
92+
* <li>10 bytes: negative numbers (due to sign extension)</li>
93+
* </ul>
94+
*
95+
* @param value the integer value to encode
96+
* @return the number of bytes required to encode the value as a varint
97+
*/
98+
private static int sizeOfVarint(int value) {
99+
if (value < 0)
100+
return 10; // Negative numbers use sign extension, always 10 bytes
101+
if (value < 128) // 2^7
102+
return 1;
103+
if (value < 16384) // 2^14
104+
return 2;
105+
if (value < 2097152) // 2^21
106+
return 3;
107+
if (value < 268435456) // 2^28
108+
return 4;
109+
return 5; // 2^35 (max for positive 32-bit int)
110+
}
111+
112+
/**
113+
* Writes a complete protobuf length-delimited field to the buffer.
114+
* <p>
115+
* A length-delimited field consists of:
116+
* <ul>
117+
* <li>Tag (field number + wire type) encoded as a varint</li>
118+
* <li>Length of the data encoded as a varint</li>
119+
* <li>The actual data bytes</li>
120+
* </ul>
121+
*
122+
* @param buffer the buffer to write to
123+
* @param pos the starting position in the buffer
124+
* @param tag the protobuf field tag
125+
* @param data the data bytes to write
126+
* @return the new position after writing the complete field
127+
*/
128+
private static int writeField(Uint8Array buffer, int pos, int tag, Uint8Array data) {
129+
// Write tag and length
130+
pos = writeVarint(buffer, pos, tag);
131+
pos = writeVarint(buffer, pos, data.length);
132+
// Write data bytes
133+
for (int i = 0; i < data.length; i++) {
134+
buffer.setAt(pos++, data.getAt(i));
135+
}
136+
return pos;
137+
}
138+
139+
/**
140+
* Writes a value to the buffer as a protobuf varint (variable-length integer).
141+
* <p>
142+
* Varint encoding works by:
143+
* <ol>
144+
* <li>Taking the lowest 7 bits of the value</li>
145+
* <li>Setting the 8th bit to 1 if more bytes follow (continuation flag)</li>
146+
* <li>Writing the byte to the buffer</li>
147+
* <li>Shifting the value right by 7 bits</li>
148+
* <li>Repeating until the value is less than 128</li>
149+
* <li>Writing the final byte without the continuation flag (8th bit = 0)</li>
150+
* </ol>
151+
* <p>
152+
* Example: encoding 300
153+
* <ul>
154+
* <li>300 in binary: 100101100</li>
155+
* <li>First byte: (300 & 0x7F) | 0x80 = 0b00101100 | 0b10000000 = 172 (0xAC)</li>
156+
* <li>Shift: 300 >>> 7 = 2</li>
157+
* <li>Second byte: 2 (no continuation flag)</li>
158+
* <li>Result: [172, 2]</li>
159+
* </ul>
160+
*
161+
* @param buffer the buffer to write to
162+
* @param pos the starting position in the buffer
163+
* @param value the value to encode
164+
* @return the new position after writing
165+
*/
166+
private static int writeVarint(Uint8Array buffer, int pos, int value) {
167+
while (value >= 128) {
168+
// Extract lowest 7 bits and set continuation flag (8th bit = 1)
169+
buffer.setAt(pos++, (double) ((value & 0x7F) | 0x80));
170+
// Shift right by 7 to process next chunk
171+
value >>>= 7; // Unsigned right shift to handle large positive values
172+
}
173+
// Write final byte (no continuation flag, 8th bit = 0)
174+
buffer.setAt(pos++, (double) value);
175+
return pos;
176+
}
177+
}
178+

web/client-api/src/main/java/io/deephaven/web/client/api/remotefilesource/JsRemoteFileSourceService.java

Lines changed: 2 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket;
2323
import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.TypedTicket;
2424
import io.deephaven.web.client.api.Callbacks;
25+
import io.deephaven.web.client.api.JsProtobufUtils;
2526
import io.deephaven.web.client.api.event.Event;
2627
import io.deephaven.web.client.api.WorkerConnection;
2728
import io.deephaven.web.client.api.event.HasEventHandling;
@@ -81,7 +82,7 @@ private static Promise<FlightInfo> fetchPluginFlightInfo(WorkerConnection connec
8182
Uint8Array innerRequestBytes = fetchRequest.serializeBinary();
8283

8384
// Wrap in google.protobuf.Any with the proper typeUrl
84-
Uint8Array anyWrappedBytes = wrapInAny(
85+
Uint8Array anyWrappedBytes = JsProtobufUtils.wrapInAny(
8586
"type.googleapis.com/io.deephaven.proto.backplane.grpc.RemoteFileSourcePluginFetchRequest",
8687
innerRequestBytes);
8788

@@ -317,165 +318,4 @@ public void respond(@JsNullable Object content) {
317318
sendClientRequest(clientRequest);
318319
}
319320
}
320-
321-
/**
322-
* Calculates the total size needed for a protobuf length-delimited field.
323-
* <p>
324-
* A length-delimited field consists of:
325-
* <ul>
326-
* <li>Tag (field number + wire type) encoded as a varint</li>
327-
* <li>Length of the data encoded as a varint</li>
328-
* <li>The actual data bytes</li>
329-
* </ul>
330-
*
331-
* @param tag the protobuf field tag (field number << 3 | wire type)
332-
* @param dataLength the length of the data in bytes
333-
* @return the total number of bytes needed for this field
334-
*/
335-
private static int calculateFieldSize(int tag, int dataLength) {
336-
return sizeOfVarint(tag) + sizeOfVarint(dataLength) + dataLength;
337-
}
338-
339-
/**
340-
* Calculates how many bytes a varint encoding will require for the given value.
341-
* <p>
342-
* Protobuf uses varint encoding where each byte stores 7 bits of data (the 8th bit is
343-
* a continuation flag). This means:
344-
* <ul>
345-
* <li>1 byte: 0 to 127 (2^7 - 1)</li>
346-
* <li>2 bytes: 128 to 16,383 (2^14 - 1)</li>
347-
* <li>3 bytes: 16,384 to 2,097,151 (2^21 - 1)</li>
348-
* <li>4 bytes: 2,097,152 to 268,435,455 (2^28 - 1)</li>
349-
* <li>5 bytes: 268,435,456 to 4,294,967,295 (2^35 - 1, max unsigned 32-bit)</li>
350-
* <li>10 bytes: negative numbers (due to sign extension)</li>
351-
* </ul>
352-
*
353-
* @param value the integer value to encode
354-
* @return the number of bytes required to encode the value as a varint
355-
*/
356-
private static int sizeOfVarint(int value) {
357-
if (value < 0)
358-
return 10; // Negative numbers use sign extension, always 10 bytes
359-
if (value < 128) // 2^7
360-
return 1;
361-
if (value < 16384) // 2^14
362-
return 2;
363-
if (value < 2097152) // 2^21
364-
return 3;
365-
if (value < 268435456) // 2^28
366-
return 4;
367-
return 5; // 2^35 (max for positive 32-bit int)
368-
}
369-
370-
/**
371-
* Wraps a protobuf message in a google.protobuf.Any message.
372-
* <p>
373-
* The google.protobuf.Any message has two fields:
374-
* <ul>
375-
* <li>Field 1: type_url (string) - identifies the type of message contained</li>
376-
* <li>Field 2: value (bytes) - the actual serialized message</li>
377-
* </ul>
378-
* <p>
379-
* This method manually encodes the Any message in protobuf binary format since the client-side
380-
* JavaScript protobuf library doesn't provide Any.pack() like the server-side Java library does.
381-
*
382-
* @param typeUrl the type URL for the message (e.g., "type.googleapis.com/package.MessageName")
383-
* @param messageBytes the serialized protobuf message bytes
384-
* @return the serialized Any message containing the wrapped message
385-
*/
386-
private static Uint8Array wrapInAny(String typeUrl, Uint8Array messageBytes) {
387-
// Protobuf tag constants for google.protobuf.Any message fields
388-
// Tag format: (field_number << 3) | wire_type
389-
// wire_type=2 means length-delimited (for strings/bytes)
390-
final int TYPE_URL_TAG = 10; // (1 << 3) | 2 = field 1, wire type 2
391-
final int VALUE_TAG = 18; // (2 << 3) | 2 = field 2, wire type 2
392-
393-
// Encode the type_url string to UTF-8 bytes
394-
TextEncoder textEncoder = new TextEncoder();
395-
Uint8Array typeUrlBytes = textEncoder.encode(typeUrl);
396-
397-
// Calculate sizes for protobuf binary encoding
398-
int typeUrlFieldSize = calculateFieldSize(TYPE_URL_TAG, typeUrlBytes.length);
399-
int valueFieldSize = calculateFieldSize(VALUE_TAG, messageBytes.length);
400-
401-
// Allocate buffer for the complete Any message
402-
int totalSize = typeUrlFieldSize + valueFieldSize;
403-
Uint8Array result = new Uint8Array(totalSize);
404-
int pos = 0;
405-
406-
// Write field 1 (type_url) in protobuf binary format
407-
pos = writeField(result, pos, TYPE_URL_TAG, typeUrlBytes);
408-
409-
// Write field 2 (value) in protobuf binary format
410-
writeField(result, pos, VALUE_TAG, messageBytes);
411-
412-
return result;
413-
}
414-
415-
/**
416-
* Writes a complete protobuf length-delimited field to the buffer.
417-
* <p>
418-
* A length-delimited field consists of:
419-
* <ul>
420-
* <li>Tag (field number + wire type) encoded as a varint</li>
421-
* <li>Length of the data encoded as a varint</li>
422-
* <li>The actual data bytes</li>
423-
* </ul>
424-
*
425-
* @param buffer the buffer to write to
426-
* @param pos the starting position in the buffer
427-
* @param tag the protobuf field tag
428-
* @param data the data bytes to write
429-
* @return the new position after writing the complete field
430-
*/
431-
private static int writeField(Uint8Array buffer, int pos, int tag, Uint8Array data) {
432-
// Write tag and length
433-
pos = writeVarint(buffer, pos, tag);
434-
pos = writeVarint(buffer, pos, data.length);
435-
// Write data bytes
436-
for (int i = 0; i < data.length; i++) {
437-
buffer.setAt(pos++, data.getAt(i));
438-
}
439-
return pos;
440-
}
441-
442-
443-
/**
444-
* Writes a value to the buffer as a protobuf varint (variable-length integer).
445-
* <p>
446-
* Varint encoding works by:
447-
* <ol>
448-
* <li>Taking the lowest 7 bits of the value</li>
449-
* <li>Setting the 8th bit to 1 if more bytes follow (continuation flag)</li>
450-
* <li>Writing the byte to the buffer</li>
451-
* <li>Shifting the value right by 7 bits</li>
452-
* <li>Repeating until the value is less than 128</li>
453-
* <li>Writing the final byte without the continuation flag (8th bit = 0)</li>
454-
* </ol>
455-
* <p>
456-
* Example: encoding 300
457-
* <ul>
458-
* <li>300 in binary: 100101100</li>
459-
* <li>First byte: (300 & 0x7F) | 0x80 = 0b00101100 | 0b10000000 = 172 (0xAC)</li>
460-
* <li>Shift: 300 >>> 7 = 2</li>
461-
* <li>Second byte: 2 (no continuation flag)</li>
462-
* <li>Result: [172, 2]</li>
463-
* </ul>
464-
*
465-
* @param buffer the buffer to write to
466-
* @param pos the starting position in the buffer
467-
* @param value the value to encode
468-
* @return the new position after writing
469-
*/
470-
private static int writeVarint(Uint8Array buffer, int pos, int value) {
471-
while (value >= 128) {
472-
// Extract lowest 7 bits and set continuation flag (8th bit = 1)
473-
buffer.setAt(pos++, (double) ((value & 0x7F) | 0x80));
474-
// Shift right by 7 to process next chunk
475-
value >>>= 7; // Unsigned right shift to handle large positive values
476-
}
477-
// Write final byte (no continuation flag, 8th bit = 0)
478-
buffer.setAt(pos++, (double) value);
479-
return pos;
480-
}
481321
}

0 commit comments

Comments
 (0)