You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by co...@apache.org on 2026/03/17 00:53:04 UTC
(tinkerpop) 04/09: Updated GraphBinary reader and writer to v4
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 <ki...@improving.com>
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);
+ });
+ });
+});