This is an automated email from the ASF dual-hosted git repository.

colegreer pushed a commit to branch graph-binary-4-JS-HTTP
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 88686fa2507c0d3da7cb9e6a3eaa23a8c80da5ed
Author: Kirill Stepanishin <[email protected]>
AuthorDate: Wed Mar 4 15:01:30 2026 -0800

    Updated GraphBinary reader and writer to v4
---
 .../io/binary/internals/GraphBinaryReader.js       |  61 +++--
 .../io/binary/internals/GraphBinaryWriter.js       |  16 +-
 .../unit/graphbinary/GraphBinaryReader-test.js     | 278 +++++++++++++++++++++
 .../unit/graphbinary/GraphBinaryWriter-test.js     | 122 +++++++++
 4 files changed, 448 insertions(+), 29 deletions(-)

diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js
index deca362305..004fb8befc 100644
--- 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js
@@ -42,40 +42,63 @@ export default class GraphBinaryReader {
       throw new Error('Buffer is empty.');
     }
 
-    const response = { status: {}, result: {} };
     let cursor = buffer;
     let len;
 
     // {version} is a Byte representing the protocol version
     const version = cursor[0];
-    if (version !== 0x81) {
+    if (version !== 0x84) {
       throw new Error(`Unsupported version '${version}'.`);
     }
     cursor = cursor.slice(1); // skip version
 
-    // {request_id} is a nullable UUID
-    ({ v: response.requestId, len } = 
this.ioc.uuidSerializer.deserialize(cursor, false, true));
-    cursor = cursor.slice(len);
+    // {bulked} is a Byte: 0x00 = not bulked, 0x01 = bulked
+    const bulked = cursor[0] === 0x01;
+    cursor = cursor.slice(1);
 
-    // {status_code} is an Int
-    ({ v: response.status.code, len } = 
this.ioc.intSerializer.deserialize(cursor, false));
-    cursor = cursor.slice(len);
+    // {result_data} stream - read values until marker
+    const data = [];
+    while (cursor[0] !== 0xfd) {
+      const { v, len: valueLen } = this.ioc.anySerializer.deserialize(cursor);
+      cursor = cursor.slice(valueLen);
 
-    // {status_message} is a nullable String
-    ({ v: response.status.message, len } = 
this.ioc.stringSerializer.deserialize(cursor, false, true));
-    cursor = cursor.slice(len);
+      if (bulked) {
+        const { v: bulk, len: bulkLen } = 
this.ioc.longSerializer.deserialize(cursor, false);
+        cursor = cursor.slice(bulkLen);
+        data.push({ v, bulk: Number(bulk) });
+      } else {
+        data.push(v);
+      }
+    }
 
-    // {status_attributes} is a Map
-    ({ v: response.status.attributes, len } = 
this.ioc.mapSerializer.deserialize(cursor, false));
-    cursor = cursor.slice(len);
+    // Skip marker [0xFD, 0x00, 0x00]
+    cursor = cursor.slice(3);
 
-    // {result_meta} is a Map
-    ({ v: response.result.meta, len } = 
this.ioc.mapSerializer.deserialize(cursor, false));
+    // {status_code} is an Int bare
+    let code;
+    ({ v: code, len } = this.ioc.intSerializer.deserialize(cursor, false));
     cursor = cursor.slice(len);
 
-    // {result_data} is a fully qualified typed value composed of 
{type_code}{type_info}{value_flag}{value}
-    ({ v: response.result.data } = this.ioc.anySerializer.deserialize(cursor));
+    // {status_message} is nullable
+    let message = null;
+    if (cursor[0] === 0x00) {
+      cursor = cursor.slice(1);
+      ({ v: message, len } = this.ioc.stringSerializer.deserialize(cursor, 
false));
+      cursor = cursor.slice(len);
+    } else {
+      cursor = cursor.slice(1); // skip 0x01 null flag
+    }
+
+    // {exception} is nullable
+    let exception = null;
+    if (cursor[0] === 0x00) {
+      cursor = cursor.slice(1);
+      ({ v: exception } = this.ioc.stringSerializer.deserialize(cursor, 
false));
+    }
 
-    return response;
+    return {
+      status: { code, message, exception },
+      result: { data, bulked },
+    };
   }
 }
diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryWriter.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryWriter.js
index 856582d0ad..af427e504d 100644
--- 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryWriter.js
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryWriter.js
@@ -31,18 +31,14 @@ export default class GraphBinaryWriter {
     this.ioc = ioc;
   }
 
-  writeRequest({ requestId, op, processor, args }) {
+  writeRequest({ gremlin, fields }) {
     const bufs = [
       // {version} 1 byte
-      Buffer.from([0x81]),
-      // {request_id} UUID
-      this.ioc.uuidSerializer.serialize(requestId, false),
-      // {op} String
-      this.ioc.stringSerializer.serialize(op, false),
-      // {processor} String
-      this.ioc.stringSerializer.serialize(processor, false),
-      // {args} Map
-      this.ioc.mapSerializer.serialize(args, false),
+      Buffer.from([0x84]),
+      // {fields} Map bare
+      this.ioc.mapSerializer.serialize(fields || new Map(), false),
+      // {gremlin} String bare
+      this.ioc.stringSerializer.serialize(gremlin, false),
     ];
     return Buffer.concat(bufs);
   }
diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js
new file mode 100644
index 0000000000..bb006690b3
--- /dev/null
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js
@@ -0,0 +1,278 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/*
+ * GraphBinaryReader v4 response format tests.
+ * Tests the reader's ability to parse v4 response format:
+ * {version:0x84}{bulked:Byte}{result_data stream}{marker:0xFD 0x00 
0x00}{status_code:Int bare}{status_message:nullable}{exception:nullable}
+ */
+
+import { assert } from 'chai';
+import { Buffer } from 'buffer';
+import GraphBinaryReader from 
'../../../lib/structure/io/binary/internals/GraphBinaryReader.js';
+import ioc from '../../../lib/structure/io/binary/GraphBinary.js';
+
+describe('GraphBinaryReader', () => {
+  const reader = new GraphBinaryReader(ioc);
+
+  describe('input validation', () => {
+    it('undefined buffer throws error', () => {
+      assert.throws(() => reader.readResponse(undefined), /Buffer is missing/);
+    });
+
+    it('null buffer throws error', () => {
+      assert.throws(() => reader.readResponse(null), /Buffer is missing/);
+    });
+
+    it('non-Buffer throws error', () => {
+      assert.throws(() => reader.readResponse('not a buffer'), /Not an 
instance of Buffer/);
+    });
+
+    it('empty buffer throws error', () => {
+      assert.throws(() => reader.readResponse(Buffer.alloc(0)), /Buffer is 
empty/);
+    });
+  });
+
+  describe('version validation', () => {
+    it('rejects version 0x00', () => {
+      const buffer = Buffer.from([0x00]);
+      assert.throws(() => reader.readResponse(buffer), /Unsupported version 
'0'/);
+    });
+
+    it('rejects version 0x81', () => {
+      const buffer = Buffer.from([0x81]);
+      assert.throws(() => reader.readResponse(buffer), /Unsupported version 
'129'/);
+    });
+
+    it('rejects version 0xFF', () => {
+      const buffer = Buffer.from([0xFF]);
+      assert.throws(() => reader.readResponse(buffer), /Unsupported version 
'255'/);
+    });
+  });
+
+  describe('non-bulked responses', () => {
+    it('single value', () => {
+      const buffer = Buffer.from([
+        0x84, // version
+        0x00, // bulked=false
+        0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int: type_code=0x01, 
value_flag=0x00, value=67
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, // status_message null flag
+        0x01  // exception null flag
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.deepEqual(result, {
+        status: { code: 200, message: null, exception: null },
+        result: { data: [67], bulked: false },
+      });
+    });
+
+    it('multiple values', () => {
+      const buffer = Buffer.from([
+        0x84, // version
+        0x00, // bulked=false
+        0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int: 67
+        0x03, 0x00, 0x00, 0x00, 0x00, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // 
fq String: "hello"
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, // status_message null
+        0x01  // exception null
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.deepEqual(result, {
+        status: { code: 200, message: null, exception: null },
+        result: { data: [67, 'hello'], bulked: false }
+      });
+    });
+
+    it('empty result', () => {
+      const buffer = Buffer.from([
+        0x84, // version
+        0x00, // bulked=false
+        0xFD, 0x00, 0x00, // marker (no data)
+        0x00, 0x00, 0x00, 0xCC, // status_code=204
+        0x01, // status_message null
+        0x01  // exception null
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.deepEqual(result, {
+        status: { code: 204, message: null, exception: null },
+        result: { data: [], bulked: false }
+      });
+    });
+  });
+
+  describe('bulked responses', () => {
+    it('single item with bulk count', () => {
+      const buffer = Buffer.from([
+        0x84, // version
+        0x01, // bulked=true
+        0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int: 67
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // bare Long bulk=3
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, // status_message null
+        0x01  // exception null
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.deepEqual(result, {
+        status: { code: 200, message: null, exception: null },
+        result: { data: [{ v: 67, bulk: 3 }], bulked: true }
+      });
+    });
+
+    it('multiple items with bulk counts', () => {
+      const buffer = Buffer.from([
+        0x84, // version
+        0x01, // bulked=true
+        0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int: 67
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // bare Long bulk=2
+        0x03, 0x00, 0x00, 0x00, 0x00, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // 
fq String: "hello"
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // bare Long bulk=1
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, // status_message null
+        0x01  // exception null
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.deepEqual(result, {
+        status: { code: 200, message: null, exception: null },
+        result: { data: [{ v: 67, bulk: 2 }, { v: 'hello', bulk: 1 }], bulked: 
true }
+      });
+    });
+  });
+
+  describe('status codes', () => {
+    it('status 403', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x01, 0x93, // status_code=403
+        0x01, 0x01 // null message, null exception
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.status.code, 403);
+    });
+
+    it('status 500', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x01, 0xF4, // status_code=500
+        0x01, 0x01 // null message, null exception
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.status.code, 500);
+    });
+  });
+
+  describe('nullable status_message', () => {
+    it('present message', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x00, // message present flag
+        0x00, 0x00, 0x00, 0x07, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, // 
bare String: "Success"
+        0x01 // exception null
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.status.message, 'Success');
+    });
+
+    it('null message', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, // message null flag
+        0x01 // exception null
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.status.message, null);
+    });
+  });
+
+  describe('nullable exception', () => {
+    it('present exception', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x01, 0xF4, // status_code=500
+        0x01, // message null
+        0x00, // exception present flag
+        0x00, 0x00, 0x00, 0x05, 0x45, 0x72, 0x72, 0x6F, 0x72 // bare String: 
"Error"
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.status.exception, 'Error');
+    });
+
+    it('null exception', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, // message null
+        0x01 // exception null flag
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.status.exception, null);
+    });
+  });
+
+  describe('error response', () => {
+    it('no result data with error status', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0xFD, 0x00, 0x00, // marker (no data)
+        0x00, 0x00, 0x01, 0xF4, // status_code=500
+        0x00, // message present
+        0x00, 0x00, 0x00, 0x0E, 0x49, 0x6E, 0x74, 0x65, 0x72, 0x6E, 0x61, 
0x6C, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, // "Internal error" (14 chars)
+        0x00, // exception present
+        0x00, 0x00, 0x00, 0x09, 0x45, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 
0x6F, 0x6E // "Exception"
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.deepEqual(result, {
+        status: { code: 500, message: 'Internal error', exception: 'Exception' 
},
+        result: { data: [], bulked: false }
+      });
+    });
+  });
+
+  describe('complex result values', () => {
+    it('vertex in result data', () => {
+      const buffer = Buffer.from([
+        0x84, 0x00, // version, bulked=false
+        0x11, 0x00, // fq Vertex: type_code=0x11, value_flag=0x00
+        0x01, 0x00, 0x00, 0x00, 0x00, 0x01, // id: fq Int=1
+        0x00, 0x00, 0x00, 0x01, // label: bare List length=1
+        0x03, 0x00, 0x00, 0x00, 0x00, 0x06, 0x70, 0x65, 0x72, 0x73, 0x6F, 
0x6E, // fq String="person"
+        0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, // properties: fq List=empty
+        0xFD, 0x00, 0x00, // marker
+        0x00, 0x00, 0x00, 0xC8, // status_code=200
+        0x01, 0x01 // null message, null exception
+      ]);
+      const result = reader.readResponse(buffer);
+      assert.equal(result.result.data.length, 1);
+      assert.equal(result.result.data[0].id, 1);
+      assert.equal(result.result.data[0].label, 'person');
+    });
+  });
+});
diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/GraphBinaryWriter-test.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/GraphBinaryWriter-test.js
new file mode 100644
index 0000000000..d034915f54
--- /dev/null
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/GraphBinaryWriter-test.js
@@ -0,0 +1,122 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/*
+ * GraphBinaryWriter v4 request format tests.
+ * Tests the writer's ability to generate v4 request format:
+ * {version:0x84}{fields:Map bare}{gremlin:String bare}
+ */
+
+import { assert } from 'chai';
+import { Buffer } from 'buffer';
+import GraphBinaryWriter from 
'../../../lib/structure/io/binary/internals/GraphBinaryWriter.js';
+import ioc from '../../../lib/structure/io/binary/GraphBinary.js';
+
+describe('GraphBinaryWriter', () => {
+  const writer = new GraphBinaryWriter(ioc);
+
+  describe('version byte', () => {
+    it('first byte is 0x84', () => {
+      const result = writer.writeRequest({ gremlin: 'g.V()', fields: new Map() 
});
+      assert.equal(result[0], 0x84);
+    });
+  });
+
+  describe('empty fields + gremlin', () => {
+    it('empty map + bare string', () => {
+      const result = writer.writeRequest({ gremlin: 'g.V()', fields: new Map() 
});
+      const expected = Buffer.from([
+        0x84, // version
+        0x00, 0x00, 0x00, 0x00, // empty map bare (length=0)
+        0x00, 0x00, 0x00, 0x05, // string length=5
+        0x67, 0x2E, 0x56, 0x28, 0x29 // "g.V()"
+      ]);
+      assert.deepEqual(result, expected);
+    });
+  });
+
+  describe('fields with entries', () => {
+    it('map with evaluationTimeout entry', () => {
+      const fields = new Map();
+      fields.set('evaluationTimeout', 1000);
+      const result = writer.writeRequest({ gremlin: 'g.V()', fields });
+      const expected = Buffer.from([
+        0x84, // version
+        0x00, 0x00, 0x00, 0x01, // map length=1
+        0x03, 0x00, // key type_code=STRING, value_flag=0x00
+        0x00, 0x00, 0x00, 0x11, // key string length=17
+        0x65, 0x76, 0x61, 0x6C, 0x75, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x54, 
0x69, 0x6D, 0x65, 0x6F, 0x75, 0x74, // "evaluationTimeout"
+        0x01, 0x00, // value type_code=INT, value_flag=0x00
+        0x00, 0x00, 0x03, 0xE8, // value int=1000
+        0x00, 0x00, 0x00, 0x05, // gremlin string length=5
+        0x67, 0x2E, 0x56, 0x28, 0x29 // "g.V()"
+      ]);
+      assert.deepEqual(result, expected);
+    });
+  });
+
+  describe('default fields', () => {
+    it('undefined fields defaults to empty map', () => {
+      const result = writer.writeRequest({ gremlin: 'g.V()', fields: undefined 
});
+      const expected = Buffer.from([
+        0x84, // version
+        0x00, 0x00, 0x00, 0x00, // empty map bare (length=0)
+        0x00, 0x00, 0x00, 0x05, // string length=5
+        0x67, 0x2E, 0x56, 0x28, 0x29 // "g.V()"
+      ]);
+      assert.deepEqual(result, expected);
+    });
+
+    it('null fields defaults to empty map', () => {
+      const result = writer.writeRequest({ gremlin: 'g.V()', fields: null });
+      const expected = Buffer.from([
+        0x84, // version
+        0x00, 0x00, 0x00, 0x00, // empty map bare (length=0)
+        0x00, 0x00, 0x00, 0x05, // string length=5
+        0x67, 0x2E, 0x56, 0x28, 0x29 // "g.V()"
+      ]);
+      assert.deepEqual(result, expected);
+    });
+  });
+
+  describe('gremlin as bare string', () => {
+    it('no type_code/value_flag prefix on gremlin', () => {
+      const result = writer.writeRequest({ gremlin: 'g.V().count()', fields: 
new Map() });
+      // Verify gremlin portion starts with length, not type_code
+      const gremlinStart = 5; // after version + empty map
+      assert.equal(result[gremlinStart], 0x00); // length byte 1
+      assert.equal(result[gremlinStart + 1], 0x00); // length byte 2
+      assert.equal(result[gremlinStart + 2], 0x00); // length byte 3
+      assert.equal(result[gremlinStart + 3], 0x0D); // length=13
+      assert.equal(result[gremlinStart + 4], 0x67); // 'g'
+    });
+  });
+
+  describe('empty gremlin', () => {
+    it('empty string has length 0', () => {
+      const result = writer.writeRequest({ gremlin: '', fields: new Map() });
+      const expected = Buffer.from([
+        0x84, // version
+        0x00, 0x00, 0x00, 0x00, // empty map bare (length=0)
+        0x00, 0x00, 0x00, 0x00 // empty string bare (length=0)
+      ]);
+      assert.deepEqual(result, expected);
+    });
+  });
+});

Reply via email to