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); + }); + }); +});
