diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 55c77bb7269..ca8f6151ec0 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -26,6 +26,9 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima === TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET) * Added Gremlator, a single page web application, that translates Gremlin into various programming languages like Javascript and Python. +* Connected HTTP streaming response deserialization to the traversal API in `gremlin-javascript`, enabling `next()` to return the first result without waiting for the full response. +* Changed `Client.stream()` in `gremlin-javascript` to return an `AsyncGenerator` for direct incremental consumption. +* Removed `readable-stream` dependency from `gremlin-javascript`. [[release-4-0-0-beta-2]] === TinkerPop 4.0.0-beta.2 (April 1, 2026) diff --git a/docs/src/upgrade/release-4.x.x.asciidoc b/docs/src/upgrade/release-4.x.x.asciidoc index 385df2c45d9..dc16f92de5a 100644 --- a/docs/src/upgrade/release-4.x.x.asciidoc +++ b/docs/src/upgrade/release-4.x.x.asciidoc @@ -41,6 +41,39 @@ anonymized form. The original gremlator.com was a prototype built by TinkerPop c previous implementation required Java and a running Gremlin Server, whereas the new version runs entirely in the browser with no server infrastructure needed. +==== JS HTTP Streaming Response Support + +The JavaScript driver now supports incremental HTTP streaming. Results are deserialized from the server response as +they arrive, rather than buffering the entire response before processing. + +Traversal API terminal steps (`next()`, `toList()`, `hasNext()`) are now truly incremental. `next()` +returns the first result as soon as it is deserialized from the wire, without waiting for the full response. +In 3.x, `next()` waited for all WebSocket frames before returning. + +`Client.stream()` now returns an `AsyncGenerator` for direct incremental consumption. This is a breaking change +from 3.x where `stream()` returned a Node.js `Readable`. The new return type works in both Node.js and browsers: + +[source,javascript] +---- +// 3.x — Readable stream (no longer supported) +// const stream = client.stream('g.V()'); +// stream.on('data', (resultSet) => { ... }); + +// 4.0 — AsyncGenerator +for await (const item of client.stream('g.V()', null)) { + console.log(item); + if (someCondition) break; // stops reading from the HTTP stream +} +---- + +`Client.submit()` remains unchanged. It still buffers the full response and returns `Promise`. + +=== Upgrading for Providers + +==== Graph System Providers + +==== Graph Driver Providers + == TinkerPop 4.0.0-beta.2 *Release Date: April 1, 2026* diff --git a/gremlin-js/gremlin-javascript/lib/driver/client.ts b/gremlin-js/gremlin-javascript/lib/driver/client.ts index 35cffc28f65..9283a9421c6 100644 --- a/gremlin-js/gremlin-javascript/lib/driver/client.ts +++ b/gremlin-js/gremlin-javascript/lib/driver/client.ts @@ -18,7 +18,6 @@ */ import Connection, { ConnectionOptions } from './connection.js'; -import { Readable } from 'stream'; import {RequestMessage} from "./request-message.js"; export type RequestOptions = { @@ -70,67 +69,57 @@ export default class Client { } /** - * Configuration specific to the current request. - * @typedef {Object} RequestOptions - * @property {any} bindings - The parameter bindings to apply to the script. - * @property {String} language - The language of the script to execute. Defaults to 'gremlin-lang'. - * @property {String} accept - The MIME type expected in the response. - * @property {Boolean} bulkResults - Indicates whether results should be returned in bulk format. - * @property {Object} params - Additional parameters to include with the request. - * @property {Number} batchSize - The size in which the result of a request is to be 'batched' back to the client. - * @property {String} userAgent - The user agent string to send with the request. - * @property {Number} evaluationTimeout - The timeout for the evaluation of the request. - * @property {String} materializeProperties - Indicates whether element properties should be returned or not. - */ - - /** - * Send a request to the Gremlin Server. + * Send a request to the Gremlin Server and buffer the entire response. * @param {string} message The script to send * @param {Object|null} [bindings] The script bindings, if any. * @param {RequestOptions} [requestOptions] Configuration specific to the current request. - * @returns {Promise} - */ //TODO:: tighten return type to Promise + * @returns {Promise} + */ submit(message: string, bindings: any | null, requestOptions?: RequestOptions): Promise { - const requestBuilder = RequestMessage.build(message) - .addG(this.options.traversalSource || 'g') - - if (requestOptions?.language) { - requestBuilder.addLanguage(requestOptions.language); - } - if (requestOptions?.bindings) { - requestBuilder.addBindings(requestOptions.bindings); - } - if (bindings) { - requestBuilder.addBindings(bindings); - } - if (requestOptions?.materializeProperties) { - requestBuilder.addMaterializeProperties(requestOptions.materializeProperties); - } - if (requestOptions?.evaluationTimeout) { - requestBuilder.addTimeoutMillis(requestOptions.evaluationTimeout); - } - if (requestOptions?.bulkResults) { - requestBuilder.addBulkResults(requestOptions.bulkResults); - } - - return this._connection.submit(requestBuilder.create()); + return this._connection.submit(this.#buildRequest(message, bindings, requestOptions)); } /** - * Send a request to the Gremlin Server and receive a stream for the results. + * Send a request to the Gremlin Server and stream results incrementally. + * Returns an AsyncGenerator that yields individual result items as they are + * deserialized from the response. For bulked responses, yields Traverser objects. * @param {string} message The script to send - * @param {Object} [bindings] The script bindings, if any. + * @param {Object|null} [bindings] The script bindings, if any. * @param {RequestOptions} [requestOptions] Configuration specific to the current request. - * @returns {ReadableStream} + * @returns {AsyncGenerator} */ - //TODO:: Update stream() to mirror submit() - stream(message: string, bindings: any, requestOptions?: RequestOptions): Readable { - throw new Error("Stream not yet implemented"); + async *stream(message: string, bindings: any | null, requestOptions?: RequestOptions): AsyncGenerator { + return yield* this._connection.stream(this.#buildRequest(message, bindings, requestOptions)); + } + + #buildRequest(message: string, bindings: any | null, requestOptions?: RequestOptions): RequestMessage { + const requestBuilder = RequestMessage.build(message) + .addG(this.options.traversalSource || 'g'); + + if (requestOptions?.language) { + requestBuilder.addLanguage(requestOptions.language); + } + if (requestOptions?.bindings) { + requestBuilder.addBindings(requestOptions.bindings); + } + if (bindings) { + requestBuilder.addBindings(bindings); + } + if (requestOptions?.materializeProperties) { + requestBuilder.addMaterializeProperties(requestOptions.materializeProperties); + } + if (requestOptions?.evaluationTimeout) { + requestBuilder.addTimeoutMillis(requestOptions.evaluationTimeout); + } + if (requestOptions?.bulkResults) { + requestBuilder.addBulkResults(requestOptions.bulkResults); + } + + return requestBuilder.create(); } /** * Closes the underlying connection - * send session close request before connection close if session mode * @returns {Promise} */ close(): Promise { @@ -147,7 +136,7 @@ export default class Client { } /** - * Removes a previowsly added event listener to the connection + * Removes a previously added event listener to the connection * @param {String} event The event name that you want to listen to. * @param {Function} handler The event handler to be removed. */ diff --git a/gremlin-js/gremlin-javascript/lib/driver/connection.ts b/gremlin-js/gremlin-javascript/lib/driver/connection.ts index 35e7e092858..14ae76a5aeb 100644 --- a/gremlin-js/gremlin-javascript/lib/driver/connection.ts +++ b/gremlin-js/gremlin-javascript/lib/driver/connection.ts @@ -25,21 +25,19 @@ import { Buffer } from 'buffer'; import { EventEmitter } from 'eventemitter3'; import type { Agent } from 'node:http'; import ioc from '../structure/io/binary/GraphBinary.js'; +import StreamReader from '../structure/io/binary/internals/StreamReader.js'; import * as utils from '../utils.js'; import ResultSet from './result-set.js'; import {RequestMessage} from "./request-message.js"; -import {Readable} from "stream"; import ResponseError from './response-error.js'; import { Traverser } from '../process/traversal.js'; -const { DeferredPromise } = utils; const { graphBinaryReader, graphBinaryWriter } = ioc; const responseStatusCode = { success: 200, noContent: 204, partialContent: 206, - authenticationChallenge: 407, }; export type HttpRequest = { @@ -115,20 +113,87 @@ export default class Connection extends EventEmitter { return Promise.resolve(); } - /** @override */ - submit(request: RequestMessage) { - // The user may not want the body to be serialized if they are using an interceptor. + /** + * Send a request and buffer the entire response. Returns a Promise. + */ + async submit(request: RequestMessage) { const body = this._writer ? this._writer.writeRequest(request) : request; - - return this.#makeHttpRequest(body) - .then((response) => { - return this.#handleResponse(response); - }); + const response = await this.#makeHttpRequest(body); + return this.#handleResponse(response); } - /** @override */ - stream(request: RequestMessage): Readable { - throw new Error('stream() is not yet implemented'); + /** + * Send a request and stream the response incrementally. + * Returns an AsyncGenerator that yields deserialized result items. + * For bulked responses, yields Traverser objects. + * + * In the GraphBinary v4 streaming protocol, the server sends the status after all + * result data. If the server encounters an error mid-traversal, values yielded before + * the error are valid partial results. A ResponseError is thrown after the last value + * has been yielded. + * + * @param {RequestMessage} request + * @returns {AsyncGenerator} + */ + async *stream(request: RequestMessage): AsyncGenerator { + const body = this._writer ? this._writer.writeRequest(request) : request; + const abortController = new AbortController(); + + let response: Response; + try { + response = await this.#makeHttpRequest(body, abortController.signal); + } catch (e: any) { + throw new Error(`Stream request failed: ${e.message}`, { cause: e }); + } + + if (!response.ok) { + // For error responses, buffer and parse the error body + const buffer = Buffer.from(await response.arrayBuffer()); + const errorMessage = `Server returned HTTP ${response.status}: ${response.statusText}`; + const reader = this.#getReaderForContentType(response.headers.get("Content-Type")); + + if (reader) { + try { + const deserialized = await reader.readResponse(buffer); + const attributes = new Map(); + if (deserialized.status.exception) { + attributes.set('exceptions', deserialized.status.exception); + attributes.set('stackTrace', deserialized.status.exception); + } + throw new ResponseError(errorMessage, { + code: deserialized.status.code, + message: deserialized.status.message || response.statusText, + attributes, + }); + } catch (err) { + if (err instanceof ResponseError) throw err; + } + } + + throw new ResponseError(errorMessage, { + code: response.status, + message: response.statusText, + attributes: new Map(), + }); + } + + if (!response.body) { + // 204 No Content — nothing to yield + return; + } + + const streamReader = StreamReader.fromReadableStream(response.body); + + let completed = false; + try { + yield* this._reader.readResponseStream(streamReader); + completed = true; + } finally { + if (!completed) { + // Consumer broke out early or an error occurred — abort to release the connection + abortController.abort(); + } + } } #getReaderForContentType(contentType: string | null) { @@ -143,7 +208,7 @@ export default class Connection extends EventEmitter { return null; } - async #makeHttpRequest(body: any): Promise { + async #makeHttpRequest(body: any, signal?: AbortSignal): Promise { const headers: Record = { 'Accept': this._reader.mimeType }; @@ -164,6 +229,7 @@ export default class Connection extends EventEmitter { headers[key] = Array.isArray(value) ? value.join(', ') : value; }); } + let httpRequest: HttpRequest = { url: this.url, method: 'POST', @@ -183,6 +249,7 @@ export default class Connection extends EventEmitter { method: httpRequest.method, headers: httpRequest.headers, body: httpRequest.body, + signal, }); } @@ -196,7 +263,7 @@ export default class Connection extends EventEmitter { try { if (reader) { - const deserialized = reader.readResponse(buffer); + const deserialized = await reader.readResponse(buffer); const attributes = new Map(); if (deserialized.status.exception) { attributes.set('exceptions', deserialized.status.exception); @@ -232,9 +299,9 @@ export default class Connection extends EventEmitter { throw new Error(`Response Content-Type '${contentType}' does not match the configured reader (expected '${this._reader.mimeType}')`); } - const deserialized = reader.readResponse(buffer); + const deserialized = await reader.readResponse(buffer); - if (deserialized.status.code && deserialized.status.code !== 200 && deserialized.status.code !== 204 && deserialized.status.code !== 206) { + if (deserialized.status.code && deserialized.status.code !== responseStatusCode.success && deserialized.status.code !== responseStatusCode.noContent && deserialized.status.code !== responseStatusCode.partialContent) { const attributes = new Map(); if (deserialized.status.exception) { attributes.set('exceptions', deserialized.status.exception); diff --git a/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts index b6624b69c77..94ca4b30286 100644 --- a/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts +++ b/gremlin-js/gremlin-javascript/lib/driver/driver-remote-connection.ts @@ -24,7 +24,6 @@ import * as rcModule from './remote-connection.js'; const RemoteConnection = rcModule.RemoteConnection; const RemoteTraversal = rcModule.RemoteTraversal; -import * as utils from '../utils.js'; import Client, { RequestOptions } from './client.js'; import GremlinLang from '../process/gremlin-lang.js'; import { ConnectionOptions } from './connection.js'; @@ -57,6 +56,14 @@ export default class DriverRemoteConnection extends RemoteConnection { /** @override */ submit(gremlinLang: GremlinLang) { + const { gremlin, requestOptions } = this.#buildRequestArgs(gremlinLang); + + // Use streaming internally — returns an AsyncGenerator backed RemoteTraversal + const generator = this._client.stream(gremlin, null, requestOptions); + return Promise.resolve(new RemoteTraversal(generator)); + } + + #buildRequestArgs(gremlinLang: GremlinLang) { gremlinLang.addG(this.options.traversalSource || 'g'); let requestOptions: RequestOptions | undefined = undefined; @@ -93,8 +100,7 @@ export default class DriverRemoteConnection extends RemoteConnection { requestOptions.params = Object.fromEntries(params); } - return this._client.submit(gremlinLang.getGremlin(), null, requestOptions) - .then((result) => new RemoteTraversal(result.toArray())); + return { gremlin: gremlinLang.getGremlin(), requestOptions }; } override commit() { diff --git a/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts index c619bd08800..44007e9bd4b 100644 --- a/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts +++ b/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts @@ -85,10 +85,11 @@ export abstract class RemoteConnection { */ export class RemoteTraversal extends Traversal { constructor( - public results: Traverser[], - public sideEffects?: any[], + source: AsyncGenerator, + sideEffects?: any[], ) { super(null, null); + this._resultsStream = source; } } @@ -103,13 +104,13 @@ export class RemoteStrategy extends TraversalStrategy { /** @override */ apply(traversal: Traversal) { - if (traversal.results) { + if (traversal._resultsStream) { return Promise.resolve(); } return this.connection.submit(traversal.getGremlinLang()).then(function (remoteTraversal: RemoteTraversal) { traversal.sideEffects = remoteTraversal.sideEffects; - traversal.results = remoteTraversal.results; + traversal._resultsStream = remoteTraversal._resultsStream; }); } } diff --git a/gremlin-js/gremlin-javascript/lib/process/traversal.ts b/gremlin-js/gremlin-javascript/lib/process/traversal.ts index 5b4b6157be3..12404b80b39 100644 --- a/gremlin-js/gremlin-javascript/lib/process/traversal.ts +++ b/gremlin-js/gremlin-javascript/lib/process/traversal.ts @@ -25,14 +25,19 @@ import { Graph } from '../structure/graph.js'; import { TraversalStrategies } from './traversal-strategy.js'; import GremlinLang from './gremlin-lang.js'; -const itemDone = Object.freeze({ value: null, done: true }); +const itemDone: IteratorResult = Object.freeze({ value: null, done: true }) as any; const asyncIteratorSymbol = Symbol.asyncIterator || Symbol('@@asyncIterator'); export class Traversal { - results: any[] | null = null; + /** @internal Async results stream set by RemoteStrategy */ + _resultsStream: AsyncGenerator | null = null; + /** @internal Buffered traverser for bulk expansion and hasNext peek */ + private _currentTraverser: Traverser | null = null; + /** Trailing response status from the server, populated after iteration completes. */ + private _status: { code: number; message: string | null; exception: string | null } | null = null; sideEffects?: any = null; private _traversalStrategiesPromise: Promise | null = null; - private _resultsIteratorIndex = 0; + private _done = false; constructor( public graph: Graph | null, @@ -52,52 +57,71 @@ export class Traversal { return this.gremlinLang; } + /** + * Gets the trailing response status from the server. + * Available after the traversal has been fully iterated (via toList(), iterate(), or manual iteration). + * Returns null if the traversal has not completed. + * @returns {{ code: number, message: string | null, exception: string | null } | null} + */ + getStatus() { + return this._status; + } + /** * Returns an Array containing the traverser objects. * @returns {Promise.} */ - toList(): Promise { - return this._applyStrategies().then(() => { - const result: T[] = []; - let it; - while ((it = this._getNext()) && !it.done) { - result.push(it.value as T); - } - return result; - }); + async toList(): Promise { + await this._applyStrategies(); + const result: T[] = []; + while (true) { + const it = await this._getNext(); + if (it.done) break; + result.push(it.value); + } + return result; } /** * Determines if there are any more items to iterate from the traversal. * @returns {Promise.} */ - hasNext() { - return this._applyStrategies().then( - () => { - if (!this.results || this.results.length <= 0 || this._resultsIteratorIndex >= this.results.length) { - return false; - } - if (this.results[this._resultsIteratorIndex] instanceof Traverser) { - return this.results[this._resultsIteratorIndex].bulk > 0 || this._resultsIteratorIndex + 1 < this.results.length; - } else { - return true; - } + async hasNext(): Promise { + await this._applyStrategies(); + + // If we have a current traverser with remaining bulk, there's more + if (this._currentTraverser && this._currentTraverser.bulk > 0) { + return true; + } + + if (this._done) return false; + + // Try to pull the next item from the source to check + if (this._resultsStream) { + const { value, done } = await this._resultsStream.next(); + if (done) { + this._done = true; + return false; } - ); + // Buffer it as a traverser for the next _getNext() call + this._currentTraverser = value instanceof Traverser ? value : new Traverser(value, 1); + return true; + } + + return false; } /** * Iterates all Traverser instances in the traversal. * @returns {Promise} */ - iterate() { + async iterate(): Promise { this.gremlinLang.addStep('discard'); - return this._applyStrategies().then(() => { - let it; - while ((it = this._getNext()) && !it.done) { - // - } - }); + await this._applyStrategies(); + while (true) { + const it = await this._getNext(); + if (it.done) break; + } } /** @@ -105,31 +129,55 @@ export class Traversal { * Returns a promise containing an iterator item. * @returns {Promise.<{value, done}>} */ - next(): Promise> { - return this._applyStrategies().then(() => this._getNext()); + async next(): Promise> { + await this._applyStrategies(); + return this._getNext(); } /** - * Synchronous iterator of traversers including + * Pull the next value, handling Traverser bulk expansion. * @private */ - _getNext(): IteratorResult { - while (this.results && this._resultsIteratorIndex < this.results.length) { - const next = this.results[this._resultsIteratorIndex]; - - if (next instanceof Traverser) { - if (next.bulk > 0) { - next.bulk--; - return { value: next.object, done: false }; + async _getNext(): Promise> { + // Drain current traverser's bulk first + if (this._currentTraverser) { + if (this._currentTraverser.bulk > 0) { + this._currentTraverser.bulk--; + const value = this._currentTraverser.object; + if (this._currentTraverser.bulk <= 0) { + this._currentTraverser = null; } + return { value, done: false }; } + this._currentTraverser = null; + } - this._resultsIteratorIndex++; + // Streaming source path + if (this._resultsStream) { + if (this._done) return itemDone; - if (!(next instanceof Traverser)) { - return { value: next, done: false }; + const { value, done } = await this._resultsStream.next(); + if (done) { + this._done = true; + this._status = value ?? null; + return itemDone; } + + if (value instanceof Traverser) { + if (value.bulk > 0) { + value.bulk--; + const result = value.object; + if (value.bulk > 0) { + this._currentTraverser = value; + } + return { value: result, done: false }; + } + return itemDone; + } + + return { value, done: false }; } + return itemDone; } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js index f446a37cee7..c3bd93bf1de 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js @@ -21,8 +21,6 @@ * @author Igor Ostapenko */ -import { Buffer } from 'buffer'; - export default class AnySerializer { constructor(ioc) { this.ioc = ioc; @@ -64,25 +62,33 @@ export default class AnySerializer { return this.getSerializerCanBeUsedFor(item).serialize(item, fullyQualifiedFormat); } - deserialize(buffer) { - // obviously, fullyQualifiedFormat always is true - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } + /** + * Async deserialization from a StreamReader. + * Reads type_code + value_flag, then dispatches to the appropriate serializer's deserializeValue(). + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const pos = reader.position; + const type_code = await reader.readUInt8(); + const serializer = this.ioc.serializers[type_code]; + if (!serializer) { + throw new Error(`AnySerializer: unknown {type_code}=0x${type_code.toString(16)} at position ${pos}`); + } - const type_code = buffer.readUInt8(); - const serializer = this.ioc.serializers[type_code]; - if (!serializer) { - throw new Error('unknown {type_code}'); - } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00 && value_flag !== 0x02) { + throw new Error(`AnySerializer: unexpected {value_flag}=0x${value_flag.toString(16)} at position ${pos}`); + } - return serializer.deserialize(buffer); + try { + return await serializer.deserializeValue(reader, value_flag, type_code); } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, err }); + err.message = `${serializer.constructor.name}.deserializeValue() at position ${pos}: ${err.message}`; + throw err; } } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ArraySerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ArraySerializer.js index 5aaae1dcfe6..658df133948 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ArraySerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ArraySerializer.js @@ -67,86 +67,54 @@ export default class ArraySerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - let isBulked = false; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ID) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0 && value_flag !== 2) { - throw new Error('unexpected {value_flag}'); - } - isBulked = value_flag === 2; - cursor = cursor.slice(1); - } - - let length, length_len; - try { - ({ v: length, len: length_len } = this.ioc.intSerializer.deserialize(cursor, false)); - len += length_len; - } catch (err) { - err.message = '{length}: ' + err.message; - throw err; - } - if (length < 0) { - throw new Error('{length} is less than zero'); - } - cursor = cursor.slice(length_len); - - const v = []; - for (let i = 0; i < length; i++) { - let value, value_len; - try { - ({ v: value, len: value_len } = this.ioc.anySerializer.deserialize(cursor)); - len += value_len; - } catch (err) { - err.message = `{item_${i}}: ` + err.message; - throw err; - } - cursor = cursor.slice(value_len); + /** + * Async deserialization of array value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag - 0x00 for normal, 0x02 for bulked + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const isBulked = valueFlag === 0x02; + const length = await this.ioc.intSerializer.deserializeBare(reader); + if (length < 0) { + throw new Error(`ArraySerializer: {length}=${length} is less than zero`); + } - if (isBulked) { - if (cursor.length < 8) { - throw new Error(`{item_${i}}: bulk count is missing`); - } - const bulkCount = cursor.readBigInt64BE(); - len += 8; - cursor = cursor.slice(8); + const v = []; + for (let i = 0; i < length; i++) { + const value = await this.ioc.anySerializer.deserialize(reader); - for (let j = 0n; j < bulkCount; j++) { - v.push(value); - } - } else { + if (isBulked) { + const bulkCount = await reader.readBigInt64BE(); + for (let j = 0n; j < bulkCount; j++) { v.push(value); } + } else { + v.push(value); } + } + + return v; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ID) { + throw new Error(`ArraySerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00 && value_flag !== 0x02) { + throw new Error(`ArraySerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BigIntegerSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BigIntegerSerializer.js index 436f02a95b3..cd265680e00 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BigIntegerSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BigIntegerSerializer.js @@ -88,65 +88,42 @@ export default class BigIntegerSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.BIGINTEGER) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - // {length} - let length, length_len; - try { - ({ v: length, len: length_len } = this.ioc.intSerializer.deserialize(cursor, false)); - len += length_len; - } catch (err) { - err.message = '{length}: ' + err.message; - throw err; - } - if (length < 1) { - throw new Error(`{length}=${length} is less than one`); - } - cursor = cursor.slice(length_len); - - len += length; - cursor = cursor.slice(0, length); - let v = BigInt(`0x${cursor.toString('hex')}`); - const is_sign_bit_set = (cursor[0] & 0x80) === 0x80; - if (is_sign_bit_set) { - v = BigInt.asIntN(length * 8, v); // now we get expected negative number - } + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const length = await this.ioc.intSerializer.deserializeBare(reader); + if (length < 1) { + throw new Error(`BigIntegerSerializer: {length}=${length} is less than one`); + } + const bytes = await reader.readBytes(length); + let v = BigInt(`0x${bytes.toString('hex')}`); + const is_sign_bit_set = (bytes[0] & 0x80) === 0x80; + if (is_sign_bit_set) { + v = BigInt.asIntN(length * 8, v); + } + return v; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.BIGINTEGER) { + throw new Error(`BigIntegerSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`BigIntegerSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BinarySerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BinarySerializer.js index 4d51e29f3c9..3b748855637 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BinarySerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BinarySerializer.js @@ -64,62 +64,39 @@ export default class BinarySerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.BINARY) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let length, length_len; - try { - ({ v: length, len: length_len } = this.ioc.intSerializer.deserialize(cursor, false)); - len += length_len; - } catch (err) { - err.message = '{length}: ' + err.message; - throw err; - } - if (length < 0) { - throw new Error('{length} is less than zero'); - } - cursor = cursor.slice(length_len); - - if (cursor.length < length) { - throw new Error(`{value}: unexpected actual {value} length=${cursor.length} when {length}=${length}`); - } - const v = cursor.slice(0, length); - len += length; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const length = await this.ioc.intSerializer.deserializeBare(reader); + if (length < 0) { + throw new Error(`BinarySerializer: {length}=${length} is less than zero`); + } + if (length === 0) { + return Buffer.alloc(0); + } + return reader.readBytes(length); + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.BINARY) { + throw new Error(`BinarySerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`BinarySerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BooleanSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BooleanSerializer.js index 5dc38f39100..046b5684237 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BooleanSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/BooleanSerializer.js @@ -51,54 +51,36 @@ export default class BooleanSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.BOOLEAN) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 1) { - throw new Error('unexpected {value} length'); - } - len += 1; - - let v = cursor.readUInt8(); - if (v !== 0x00 && v !== 0x01) { - throw new Error(`unexpected boolean byte=${v}`); - } - v = v === 0x01; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const v = await reader.readUInt8(); + if (v !== 0x00 && v !== 0x01) { + throw new Error(`BooleanSerializer: unexpected boolean byte=0x${v.toString(16)}`); + } + return v === 0x01; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.BOOLEAN) { + throw new Error(`BooleanSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`BooleanSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ByteSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ByteSerializer.js index aeed10dbd27..662f9363ccd 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ByteSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ByteSerializer.js @@ -48,49 +48,32 @@ export default class ByteSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.BYTE) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 1) { - throw new Error('unexpected {value} length'); - } - len += 1; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + return await reader.readByte(); + } - const v = cursor.readInt8(); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.BYTE) { + throw new Error(`ByteSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`ByteSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DateTimeSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DateTimeSerializer.js index a0b3ddd787f..bc2555d77e3 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DateTimeSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DateTimeSerializer.js @@ -74,88 +74,64 @@ export default class DateTimeSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + // 18 bytes: year(4) + month(1) + day(1) + nanos(8) + utcOffset(4) + const buf = await reader.readBytes(18); - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ID) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } + let offset = 0; + const year = buf.readInt32BE(offset); + offset += 4; + const month = buf.readUInt8(offset); + offset += 1; + const day = buf.readUInt8(offset); + offset += 1; + const nanos = buf.readBigInt64BE(offset); + offset += 8; + const utcOffset = buf.readInt32BE(offset); + + // Convert nanos to time components + const hours = Number(nanos / 3_600_000_000_000n); + const remainingNanos = nanos % 3_600_000_000_000n; + const minutes = Number(remainingNanos / 60_000_000_000n); + const remainingNanos2 = remainingNanos % 60_000_000_000n; + const seconds = Number(remainingNanos2 / 1_000_000_000n); + const millis = Number((remainingNanos2 % 1_000_000_000n) / 1_000_000n); + + const v = new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds, millis)); + // Date.UTC treats years 0-99 as 1900-1999, correct it + if (year >= 0 && year <= 99) { + v.setUTCFullYear(year); + } + // Adjust for non-zero UTC offset (JS Date is always UTC internally) + if (utcOffset !== 0) { + v.setTime(v.getTime() - utcOffset * 1000); + } - if (cursor.length < 18) { - throw new Error('unexpected {value} length'); - } - len += 18; - - let offset = 0; - - // Read year (Int32BE) - const year = cursor.readInt32BE(offset); - offset += 4; - - // Read month (UInt8, 1-based) - const month = cursor.readUInt8(offset); - offset += 1; - - // Read day (UInt8, 1-based) - const day = cursor.readUInt8(offset); - offset += 1; - - // Read nanoseconds since midnight (BigInt64BE) - const nanos = cursor.readBigInt64BE(offset); - offset += 8; - - // Read UTC offset in seconds (Int32BE) - JS Date cannot represent non-zero offsets - const utcOffset = cursor.readInt32BE(offset); - offset += 4; - - // Convert nanos to time components - const hours = Number(nanos / 3_600_000_000_000n); - const remainingNanos = nanos % 3_600_000_000_000n; - const minutes = Number(remainingNanos / 60_000_000_000n); - const remainingNanos2 = remainingNanos % 60_000_000_000n; - const seconds = Number(remainingNanos2 / 1_000_000_000n); - const millis = Number((remainingNanos2 % 1_000_000_000n) / 1_000_000n); - - const v = new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds, millis)); - // Date.UTC treats years 0-99 as 1900-1999, correct it - if (year >= 0 && year <= 99) { - v.setUTCFullYear(year); - } - // Adjust for non-zero UTC offset (JS Date is always UTC internally) - if (utcOffset !== 0) { - v.setTime(v.getTime() - utcOffset * 1000); - } + return v; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ID) { + throw new Error(`DateTimeSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`DateTimeSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DoubleSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DoubleSerializer.js index 4e7019e12a9..4f577fb8bb3 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DoubleSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/DoubleSerializer.js @@ -52,49 +52,32 @@ export default class DoubleSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.DOUBLE) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 8) { - throw new Error('unexpected {value} length'); - } - len += 8; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + return await reader.readDoubleBE(); + } - const v = cursor.readDoubleBE(); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.DOUBLE) { + throw new Error(`DoubleSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`DoubleSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js index 339c8b692ec..5ec2307b018 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EdgeSerializer.js @@ -89,130 +89,67 @@ export default class EdgeSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.EDGE) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let id, id_len; - try { - ({ v: id, len: id_len } = this.ioc.anySerializer.deserialize(cursor)); - len += id_len; - } catch (err) { - err.message = '{id}: ' + err.message; - throw err; - } - cursor = cursor.slice(id_len); - - let label, label_len; - try { - ({ v: label, len: label_len } = this.ioc.listSerializer.deserialize(cursor, false)); - label = Array.isArray(label) && label.length > 0 ? label[0] : label; - len += label_len; - } catch (err) { - err.message = '{label}: ' + err.message; - throw err; - } - cursor = cursor.slice(label_len); - - let inVId, inVId_len; - try { - ({ v: inVId, len: inVId_len } = this.ioc.anySerializer.deserialize(cursor)); - len += inVId_len; - } catch (err) { - err.message = '{inVId}: ' + err.message; - throw err; - } - cursor = cursor.slice(inVId_len); - - let inVLabel, inVLabel_len; - try { - ({ v: inVLabel, len: inVLabel_len } = this.ioc.listSerializer.deserialize(cursor, false)); - inVLabel = Array.isArray(inVLabel) && inVLabel.length > 0 ? inVLabel[0] : inVLabel; - len += inVLabel_len; - } catch (err) { - err.message = '{inVLabel}: ' + err.message; - throw err; - } - cursor = cursor.slice(inVLabel_len); - - let outVId, outVId_len; - try { - ({ v: outVId, len: outVId_len } = this.ioc.anySerializer.deserialize(cursor)); - len += outVId_len; - } catch (err) { - err.message = '{outVId}: ' + err.message; - throw err; - } - cursor = cursor.slice(outVId_len); - - let outVLabel, outVLabel_len; - try { - ({ v: outVLabel, len: outVLabel_len } = this.ioc.listSerializer.deserialize(cursor, false)); - outVLabel = Array.isArray(outVLabel) && outVLabel.length > 0 ? outVLabel[0] : outVLabel; - len += outVLabel_len; - } catch (err) { - err.message = '{outVLabel}: ' + err.message; - throw err; - } - cursor = cursor.slice(outVLabel_len); - - let parent_len; - try { - ({ len: parent_len } = this.ioc.anySerializer.deserialize(cursor)); - len += parent_len; - } catch (err) { - err.message = '{parent}: ' + err.message; - throw err; - } - cursor = cursor.slice(parent_len); - - let properties, properties_len; - try { - ({ v: properties, len: properties_len } = this.ioc.anySerializer.deserialize(cursor)); - len += properties_len; - } catch (err) { - err.message = '{properties}: ' + err.message; - throw err; - } - cursor = cursor.slice(properties_len); - - // null properties are deserialized into empty lists - const edge_props = properties ? properties : []; + /** + * Async deserialization of edge value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + // {id} fully qualified + const id = await this.ioc.anySerializer.deserialize(reader); + + // {label} bare list, extract first element + const labelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); + const label = Array.isArray(labelList) && labelList.length > 0 ? labelList[0] : labelList; + + // {inVId} fully qualified + const inVId = await this.ioc.anySerializer.deserialize(reader); + + // {inVLabel} bare list, extract first element + const inVLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); + const inVLabel = Array.isArray(inVLabelList) && inVLabelList.length > 0 ? inVLabelList[0] : inVLabelList; + + // {outVId} fully qualified + const outVId = await this.ioc.anySerializer.deserialize(reader); + + // {outVLabel} bare list, extract first element + const outVLabelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); + const outVLabel = Array.isArray(outVLabelList) && outVLabelList.length > 0 ? outVLabelList[0] : outVLabelList; + + // {parent} fully qualified (always null in current TinkerPop) + await this.ioc.anySerializer.deserialize(reader); + + // {properties} fully qualified + const properties = await this.ioc.anySerializer.deserialize(reader); + + return new Edge( + id, + new Vertex(outVId, outVLabel, null), + label, + new Vertex(inVId, inVLabel, null), + properties || [], + ); + } - const v = new Edge(id, new Vertex(outVId, outVLabel, null), label, new Vertex(inVId, inVLabel, null), edge_props); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.EDGE) { + throw new Error(`EdgeSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`EdgeSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EnumSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EnumSerializer.js index f8c27dfd56b..b898a4a4d9d 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EnumSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/EnumSerializer.js @@ -72,66 +72,57 @@ export default class EnumSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - let typeName; - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code === this.ioc.DataType.DIRECTION) { - typeName = 'Direction'; - } else if (type_code === this.ioc.DataType.MERGE) { - typeName = 'Merge'; - } else if (type_code === this.ioc.DataType.T) { - typeName = 'T'; - } else { - throw new Error(`unexpected {type_code}=${type_code}`); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let elementName, elementName_len; - try { - ({ v: elementName, len: elementName_len } = this.ioc.stringSerializer.deserialize(cursor, true)); - len += elementName_len; - } catch (err) { - err.message = 'elementName: ' + err.message; - throw err; - } + /** + * Resolve the type_code (already read by AnySerializer) to a typeName. + * Called by AnySerializer before dispatching. + */ + _typeNameForCode(type_code) { + if (type_code === this.ioc.DataType.DIRECTION) { + return 'Direction'; + } + if (type_code === this.ioc.DataType.MERGE) { + return 'Merge'; + } + if (type_code === this.ioc.DataType.T) { + return 'T'; + } + return undefined; + } - let v; - if (typeName) { - v = this.types[typeName].enum[elementName]; - } else { - v = new EnumValue(undefined, elementName); - } + /** + * @param {StreamReader} reader + * @param {number} valueFlag - already consumed by AnySerializer + * @param {number} typeCode - the type_code byte already read by AnySerializer + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const typeName = this._typeNameForCode(typeCode); + // elementName is a fully-qualified String (type_code + value_flag + length + text) + const elementName = await this.ioc.stringSerializer.deserialize(reader); + + if (typeName) { + return this.types[typeName].enum[elementName]; + } + return new EnumValue(undefined, elementName); + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (!this._typeNameForCode(type_code)) { + throw new Error(`EnumSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`EnumSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/FloatSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/FloatSerializer.js index 62ba0cd94bb..696ae8f5a54 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/FloatSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/FloatSerializer.js @@ -52,49 +52,32 @@ export default class FloatSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.FLOAT) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 4) { - throw new Error('unexpected {value} length'); - } - len += 4; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + return await reader.readFloatBE(); + } - const v = cursor.readFloatBE(); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.FLOAT) { + throw new Error(`FloatSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`FloatSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js index e1aa316970e..1cb7a798c00 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/GraphBinaryReader.js @@ -22,6 +22,17 @@ */ import { Buffer } from 'buffer'; +import StreamReader from './StreamReader.js'; +import { END_OF_STREAM } from './MarkerSerializer.js'; +import { Traverser } from '../../../../process/traversal.js'; +import ResponseError from '../../../../driver/response-error.js'; + +/** GraphBinary response status codes. */ +const StatusCode = { + SUCCESS: 200, + NO_CONTENT: 204, + PARTIAL_CONTENT: 206, +}; /** * GraphBinary reader. @@ -35,7 +46,13 @@ export default class GraphBinaryReader { return 'application/vnd.graphbinary-v4.0'; } - readResponse(buffer) { + /** + * Read a complete response from a Buffer. Used by the non-streaming submit() path. + * Returns the full { status, result } object after reading all data. + * @param {Buffer} buffer + * @returns {Promise<{status: {code, message, exception}, result: {data: any[], bulked: boolean}}>} + */ + async readResponse(buffer) { if (buffer === undefined || buffer === null) { throw new Error('Buffer is missing.'); } @@ -46,62 +63,130 @@ export default class GraphBinaryReader { throw new Error('Buffer is empty.'); } - let cursor = buffer; - let len; + const reader = StreamReader.fromBuffer(buffer); + return await this.#readFromReader(reader); + } - // {version} is a Byte representing the protocol version - const version = cursor[0]; + /** + * Stream results from a StreamReader, yielding each value as it's deserialized. + * Used by the streaming Connection.stream() path. + * + * Note: In the GraphBinary v4 streaming protocol, the status (including error codes) + * is sent *after* all result data. This means values are yielded to the consumer as + * they arrive, and a server error is only thrown after all values have been yielded. + * Consumers should be aware that partial results may have been processed before a + * ResponseError is thrown. + * + * @param {StreamReader} reader + * @returns {AsyncGenerator} + */ + async *readResponseStream(reader) { + // {version} + const version = await reader.readUInt8(); if (version !== 0x84) { throw new Error(`Unsupported version '${version}'.`); } - cursor = cursor.slice(1); // skip version - // {bulked} is a Byte: 0x00 = not bulked, 0x01 = bulked - const bulked = cursor[0] === 0x01; - cursor = cursor.slice(1); + // {bulked} + const bulked = (await reader.readUInt8()) === 0x01; - // {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); + // {result_data} stream — yield values until EndOfStream marker + while (true) { + const value = await this.ioc.anySerializer.deserialize(reader); + + if (value === END_OF_STREAM) { + break; + } if (bulked) { - const { v: bulk, len: bulkLen } = this.ioc.longSerializer.deserialize(cursor, true); - cursor = cursor.slice(bulkLen); - data.push({ v, bulk: Number(bulk) }); + const bulk = await this.ioc.longSerializer.deserialize(reader); + yield new Traverser(value, Number(bulk)); } else { - data.push(v); + yield value; } } - // Skip marker [0xFD, 0x00, 0x00] - cursor = cursor.slice(3); + // {status_code} {status_message} {exception} + const status = await this.#readStatus(reader); + if ( + status.code && + status.code !== StatusCode.SUCCESS && + status.code !== StatusCode.NO_CONTENT && + status.code !== StatusCode.PARTIAL_CONTENT + ) { + const attributes = new Map(); + if (status.exception) { + attributes.set('exceptions', status.exception); + attributes.set('stackTrace', status.exception); + } + throw new ResponseError(`Server error: ${status.message || 'Unknown error'} (${status.code})`, { + code: status.code, + message: status.message || '', + attributes, + }); + } - // {status_code} is an Int bare - const { v: code, len: codeLen } = this.ioc.intSerializer.deserialize(cursor, false); - cursor = cursor.slice(codeLen); + // Attach status to the generator's return value + return status; + } - // {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 + /** + * Internal: read the full response into a collected result (non-streaming). + */ + async #readFromReader(reader) { + // {version} + const version = await reader.readUInt8(); + if (version !== 0x84) { + throw new Error(`Unsupported version '${version}'.`); } - // {exception} is nullable - let exception = null; - if (cursor[0] === 0x00) { - cursor = cursor.slice(1); - ({ v: exception } = this.ioc.stringSerializer.deserialize(cursor, false)); + // {bulked} + const bulked = (await reader.readUInt8()) === 0x01; + + // {result_data} — collect all values + const data = []; + while (true) { + const value = await this.ioc.anySerializer.deserialize(reader); + + if (value === END_OF_STREAM) { + break; + } + + if (bulked) { + const bulk = await this.ioc.longSerializer.deserialize(reader); + data.push({ v: value, bulk: Number(bulk) }); + } else { + data.push(value); + } } + // {status} + const status = await this.#readStatus(reader); + return { - status: { code, message, exception }, + status, result: { data, bulked }, }; } + + /** + * Read the status block: {code:Int bare}{message:nullable String}{exception:nullable String} + */ + async #readStatus(reader) { + const code = await reader.readInt32BE(); + + let message = null; + const msgFlag = await reader.readUInt8(); + if (msgFlag === 0x00) { + message = await this.ioc.stringSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.STRING); + } + + let exception = null; + const excFlag = await reader.readUInt8(); + if (excFlag === 0x00) { + exception = await this.ioc.stringSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.STRING); + } + + return { code, message, exception }; + } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/IntSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/IntSerializer.js index 49a0aed2d99..297d5b26571 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/IntSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/IntSerializer.js @@ -65,49 +65,42 @@ export default class IntSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.INT) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } + /** + * Read a bare int32 value from the StreamReader (no type_code or value_flag). + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserializeBare(reader) { + return await reader.readInt32BE(); + } - if (cursor.length < 4) { - throw new Error('unexpected {value} length'); - } - len += 4; + /** + * @param {StreamReader} reader + * @param {number} valueFlag - already consumed by AnySerializer + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + return await reader.readInt32BE(); + } - const v = cursor.readInt32BE(); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Read a fully-qualified int from the StreamReader (type_code + value_flag + value). + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.INT) { + throw new Error(`IntSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`IntSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js index 4476e481493..8ef2781573c 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js @@ -52,54 +52,37 @@ export default class LongSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.LONG) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 8) { - throw new Error('unexpected {value} length'); - } - len += 8; - - let v = cursor.readBigInt64BE(); - if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) { - v = Number(v); - } - // Values outside safe integer range stay as BigInt to preserve precision. + /** + * @param {StreamReader} reader + * @param {number} valueFlag - already consumed by AnySerializer + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + let v = await reader.readBigInt64BE(); + if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) { + v = Number(v); + } + return v; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Read a fully-qualified long from the StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.LONG) { + throw new Error(`LongSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`LongSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MapSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MapSerializer.js index b9d0b38eaa8..b3db2853c92 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MapSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MapSerializer.js @@ -74,81 +74,46 @@ export default class MapSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.MAP) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 0x01) { - return { v: null, len }; - } - if (value_flag !== 0x00 && value_flag !== 0x02) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let length, length_len; - try { - ({ v: length, len: length_len } = this.ioc.intSerializer.deserialize(cursor, false)); - len += length_len; - } catch (err) { - err.message = '{length}: ' + err.message; - throw err; - } - if (length < 0) { - throw new Error('{length} is less than zero'); - } - cursor = cursor.slice(length_len); - - const v = new Map(); - for (let i = 0; i < length; i++) { - let key, key_len; - try { - ({ v: key, len: key_len } = this.ioc.anySerializer.deserialize(cursor)); - len += key_len; - } catch (err) { - err.message = `{item_${i}} key: ` + err.message; - throw err; - } - cursor = cursor.slice(key_len); + /** + * Async deserialization of map value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const length = await this.ioc.intSerializer.deserializeBare(reader); + if (length < 0) { + throw new Error(`MapSerializer: {length}=${length} is less than zero`); + } - let value, value_len; - try { - ({ v: value, len: value_len } = this.ioc.anySerializer.deserialize(cursor)); - len += value_len; - } catch (err) { - err.message = `{item_${i}} value: ` + err.message; - throw err; - } - cursor = cursor.slice(value_len); + const v = new Map(); + for (let i = 0; i < length; i++) { + const key = await this.ioc.anySerializer.deserialize(reader); + const value = await this.ioc.anySerializer.deserialize(reader); + v.set(key, value); + } - v.set(key, value); - } + return v; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.MAP) { + throw new Error(`MapSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`MapSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MarkerSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MarkerSerializer.js index 2b0a785ab78..a4da4ef0495 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MarkerSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/MarkerSerializer.js @@ -17,7 +17,7 @@ * under the License. */ -import { Buffer } from 'buffer'; +export const END_OF_STREAM = Symbol('EndOfStream'); export default class MarkerSerializer { constructor(ioc) { @@ -25,43 +25,36 @@ export default class MarkerSerializer { this.ioc.serializers[ioc.DataType.MARKER] = this; } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < (fullyQualifiedFormat ? 3 : 1)) { - throw new Error('buffer is too short for marker'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.MARKER) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - const value_flag = cursor.readUInt8(); - len++; - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - const value = cursor.readUInt8(); - len++; - if (value !== 0) { - throw new Error('unexpected marker value'); - } + /** + * @param {StreamReader} reader + * @param {number} valueFlag - already consumed by AnySerializer + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag) { + const value = await reader.readUInt8(); + if (value !== 0) { + throw new Error(`unexpected marker value: ${value}`); + } + return END_OF_STREAM; + } - return { v: 'marker', len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.MARKER) { + throw new Error(`MarkerSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`MarkerSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PathSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PathSerializer.js index 11530d6057a..3f43478b2b8 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PathSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PathSerializer.js @@ -59,65 +59,40 @@ export default class PathSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.PATH) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let labels, labels_len; - try { - ({ v: labels, len: labels_len } = this.ioc.listSerializer.deserialize(cursor)); - len += labels_len; - } catch (err) { - err.message = '{labels}: ' + err.message; - throw err; - } - // TODO: should we check content of labels to make sure it's List< Set > ? - cursor = cursor.slice(labels_len); - - let objects, objects_len; - try { - ({ v: objects, len: objects_len } = this.ioc.listSerializer.deserialize(cursor)); - len += objects_len; - } catch (err) { - err.message = '{objects}: ' + err.message; - throw err; - } - cursor = cursor.slice(objects_len); + /** + * Async deserialization of path value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + // {labels} fully qualified list + const labels = await this.ioc.anySerializer.deserialize(reader); + + // {objects} fully qualified list + const objects = await this.ioc.anySerializer.deserialize(reader); + + return new Path(labels, objects); + } - const v = new Path(labels, objects); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.PATH) { + throw new Error(`PathSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`PathSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PropertySerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PropertySerializer.js index dd63941e8f5..f075c799edb 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PropertySerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PropertySerializer.js @@ -63,79 +63,43 @@ export default class PropertySerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.PROPERTY) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - // {key} is a String value - let key, key_len; - try { - ({ v: key, len: key_len } = this.ioc.stringSerializer.deserialize(cursor, false)); - len += key_len; - } catch (err) { - err.message = '{key}: ' + err.message; - throw err; - } - cursor = cursor.slice(key_len); - - // {value} is a fully qualified typed value composed of {type_code}{type_info}{value_flag}{value} - let value, value_len; - try { - ({ v: value, len: value_len } = this.ioc.anySerializer.deserialize(cursor)); - len += value_len; - } catch (err) { - err.message = '{value}: ' + err.message; - throw err; - } - cursor = cursor.slice(value_len); - - // {parent} is a fully qualified typed value composed of {type_code}{type_info}{value_flag}{value} which is either an Edge or VertexProperty. - // Note that as TinkerPop currently sends "references" only this value will always be null. - let parent_len; - try { - ({ len: parent_len } = this.ioc.unspecifiedNullSerializer.deserialize(cursor)); - len += parent_len; - } catch (err) { - err.message = '{parent}: ' + err.message; - throw err; - } - // TODO: should we verify that parent is null? - cursor = cursor.slice(parent_len); + /** + * Async deserialization of property value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + // {key} bare string (length + text_value) + const key = await this.ioc.stringSerializer.deserializeValue(reader, 0x00, typeCode); + + // {value} fully qualified + const value = await this.ioc.anySerializer.deserialize(reader); + + // {parent} fully qualified (always null in current TinkerPop) + await this.ioc.anySerializer.deserialize(reader); + + return new Property(key, value); + } - const v = new Property(key, value); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.PROPERTY) { + throw new Error(`PropertySerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`PropertySerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/SetSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/SetSerializer.js index 0b7f5f275bd..d250b65f602 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/SetSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/SetSerializer.js @@ -64,85 +64,52 @@ export default class SetSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - let isBulked = false; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } + /** + * Async deserialization of set value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag - 0x00 for normal, 0x02 for bulked + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const isBulked = valueFlag === 0x02; + const length = await this.ioc.intSerializer.deserializeBare(reader); + if (length < 0) { + throw new Error(`SetSerializer: {length}=${length} is less than zero`); + } - if (fullyQualifiedFormat) { - const typeCode = cursor.readUInt8(); - len++; - if (typeCode !== this.ID) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); + const v = new Set(); + for (let i = 0; i < length; i++) { + const value = await this.ioc.anySerializer.deserialize(reader); - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const valueFlag = cursor.readUInt8(); - len++; - if (valueFlag === 1) { - return { v: null, len }; - } - if (valueFlag !== 0 && valueFlag !== 2) { - throw new Error('unexpected {value_flag}'); - } - isBulked = valueFlag === 2; - cursor = cursor.slice(1); + if (isBulked) { + // consume the bulk count; Set.add is idempotent so count doesn't matter + await reader.readBigInt64BE(); } - let length, lengthLen; - try { - ({ v: length, len: lengthLen } = this.ioc.intSerializer.deserialize(cursor, false)); - len += lengthLen; - } catch (err) { - err.message = '{length}: ' + err.message; - throw err; - } - if (length < 0) { - throw new Error('{length} is less than zero'); - } - cursor = cursor.slice(lengthLen); - - const v = new Set(); - for (let i = 0; i < length; i++) { - let value, valueLen; - try { - ({ v: value, len: valueLen } = this.ioc.anySerializer.deserialize(cursor)); - len += valueLen; - } catch (err) { - err.message = `{item_${i}}: ` + err.message; - throw err; - } - cursor = cursor.slice(valueLen); - - if (isBulked) { - if (cursor.length < 8) { - throw new Error(`{item_${i}}: bulk count is missing`); - } - cursor.readBigInt64BE(); - len += 8; - cursor = cursor.slice(8); + v.add(value); + } - // Set.add is idempotent; bulk count only affects cardinality, not Set membership - v.add(value); - } else { - v.add(value); - } - } + return v; + } - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ID) { + throw new Error(`SetSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00 && value_flag !== 0x02) { + throw new Error(`SetSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ShortSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ShortSerializer.js index 497378fef9a..58d0913c58a 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ShortSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/ShortSerializer.js @@ -52,49 +52,32 @@ export default class ShortSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.SHORT) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 2) { - throw new Error('unexpected {value} length'); - } - len += 2; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + return await reader.readInt16BE(); + } - const v = cursor.readInt16BE(); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.SHORT) { + throw new Error(`ShortSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`ShortSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StreamReader.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StreamReader.js new file mode 100644 index 00000000000..b1f12748124 --- /dev/null +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StreamReader.js @@ -0,0 +1,203 @@ +/* + * 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. + */ + +import { Buffer } from 'buffer'; + +/** + * Async byte reader that provides a uniform interface over both a complete Buffer + * (for non-streaming submit()) and a ReadableStream (for streaming HTTP responses). + * + * Handles chunk boundaries transparently and blocks (awaits) until the requested bytes are available. + */ +export default class StreamReader { + /** @type {Buffer} */ + #buffer; + /** @type {number} */ + #offset; + /** @type {ReadableStreamDefaultReader|null} */ + #reader; + /** @type {number} Total bytes consumed (monotonically increasing, survives chunk reassembly) */ + #position; + + /** + * @param {Buffer} initialBuffer + * @param {ReadableStreamDefaultReader|null} reader + */ + constructor(initialBuffer, reader) { + this.#buffer = initialBuffer; + this.#offset = 0; + this.#reader = reader; + this.#position = 0; + } + + /** + * Create a StreamReader backed by a complete Buffer. + * All reads are satisfied from the buffer; no async I/O occurs. + * @param {Buffer} buffer + * @returns {StreamReader} + */ + static fromBuffer(buffer) { + return new StreamReader(buffer, null); + } + + /** + * Create a StreamReader backed by a ReadableStream (e.g. fetch response.body). + * Reads pull chunks from the stream as needed. + * @param {ReadableStream} readableStream + * @returns {StreamReader} + */ + static fromReadableStream(readableStream) { + return new StreamReader(Buffer.alloc(0), readableStream.getReader()); + } + + /** + * Ensure at least `n` bytes are available in the buffer from the current offset. + * For buffer-backed readers this is a bounds check. For stream-backed readers + * this pulls chunks until enough data is buffered. + * @param {number} n + */ + async #ensure(n) { + const available = this.#buffer.length - this.#offset; + if (available >= n) { + return; + } + + if (this.#reader === null) { + throw new Error( + `Unexpected end of buffer at position ${this.#position}: needed ${n} bytes, ${available} available`, + ); + } + + // Collect chunks until we have enough + const chunks = [this.#buffer.subarray(this.#offset)]; + let total = available; + + while (total < n) { + const { value, done } = await this.#reader.read(); + if (done) { + break; + } + const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value); + chunks.push(chunk); + total += chunk.length; + } + + if (total < n) { + throw new Error(`Unexpected end of stream at position ${this.#position}: needed ${n} bytes, ${total} available`); + } + + this.#buffer = Buffer.concat(chunks); + this.#offset = 0; + } + + /** + * Total number of bytes consumed so far (monotonically increasing). + * Useful for error diagnostics. + * @returns {number} + */ + get position() { + return this.#position; + } + + /** + * Read exactly `n` bytes and return them as a Buffer. + * @param {number} n + * @returns {Promise} + */ + async readBytes(n) { + await this.#ensure(n); + const result = this.#buffer.subarray(this.#offset, this.#offset + n); + this.#offset += n; + this.#position += n; + return result; + } + + /** + * @returns {Promise} unsigned 8-bit integer + */ + async readUInt8() { + await this.#ensure(1); + this.#position++; + return this.#buffer[this.#offset++]; + } + + /** + * @returns {Promise} signed 8-bit integer + */ + async readByte() { + await this.#ensure(1); + this.#position++; + return this.#buffer.readInt8(this.#offset++); + } + + /** + * @returns {Promise} signed 16-bit big-endian integer + */ + async readInt16BE() { + await this.#ensure(2); + const v = this.#buffer.readInt16BE(this.#offset); + this.#offset += 2; + this.#position += 2; + return v; + } + + /** + * @returns {Promise} signed 32-bit big-endian integer + */ + async readInt32BE() { + await this.#ensure(4); + const v = this.#buffer.readInt32BE(this.#offset); + this.#offset += 4; + this.#position += 4; + return v; + } + + /** + * @returns {Promise} signed 64-bit big-endian integer + */ + async readBigInt64BE() { + await this.#ensure(8); + const v = this.#buffer.readBigInt64BE(this.#offset); + this.#offset += 8; + this.#position += 8; + return v; + } + + /** + * @returns {Promise} 32-bit big-endian float + */ + async readFloatBE() { + await this.#ensure(4); + const v = this.#buffer.readFloatBE(this.#offset); + this.#offset += 4; + this.#position += 4; + return v; + } + + /** + * @returns {Promise} 64-bit big-endian double + */ + async readDoubleBE() { + await this.#ensure(8); + const v = this.#buffer.readDoubleBE(this.#offset); + this.#offset += 8; + this.#position += 8; + return v; + } +} diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StringSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StringSerializer.js index 49349c28ac4..ec919ceac99 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StringSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StringSerializer.js @@ -53,63 +53,42 @@ export default class StringSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true, nullable = false) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ID) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - } - if (fullyQualifiedFormat || nullable) { - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let length, length_len; - try { - ({ v: length, len: length_len } = this.ioc.intSerializer.deserialize(cursor, false)); - len += length_len; - } catch (err) { - err.message = '{length}: ' + err.message; - throw err; - } - if (length < 0) { - throw new Error('{length} is less than zero'); - } - cursor = cursor.slice(length_len); - - if (cursor.length < length) { - throw new Error('unexpected {text_value} length'); - } - len += length; + /** + * Read the string value bytes from the StreamReader (length + text_value). + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const length = await this.ioc.intSerializer.deserializeBare(reader); + if (length < 0) { + throw new Error(`StringSerializer: {length}=${length} is less than zero`); + } + if (length === 0) { + return ''; + } + const bytes = await reader.readBytes(length); + return bytes.toString('utf8'); + } - const v = cursor.toString('utf8', 0, length); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Read a fully-qualified string from the StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ID) { + throw new Error(`StringSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`StringSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StubSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StubSerializer.js index ccdddfb1399..51696b0e4a4 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StubSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/StubSerializer.js @@ -24,7 +24,9 @@ export default class StubSerializer { this.typeName = typeName; this.ioc.serializers[typeCode] = this; } - deserialize() { + + // eslint-disable-next-line require-await + async deserializeValue(reader, valueFlag, typeCode) { throw new Error(`${this.typeName} deserialization is not yet implemented`); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UnspecifiedNullSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UnspecifiedNullSerializer.js index 8fd1601ad7e..2d0c00ec254 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UnspecifiedNullSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UnspecifiedNullSerializer.js @@ -38,39 +38,13 @@ export default class UnspecifiedNullSerializer { return Buffer.from([this.ioc.DataType.UNSPECIFIED_NULL, 0x01]); } - deserialize(buffer) { - // fullyQualifiedFormat always is true - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.UNSPECIFIED_NULL) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag !== 1) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - - return { v: null, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); - } + /** + * @param {StreamReader} reader + * @param {number} valueFlag - already consumed by AnySerializer (always 0x01 for null) + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async deserializeValue(reader, valueFlag) { + throw new Error('UnspecifiedNull should always have value_flag=0x01'); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UuidSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UuidSerializer.js index 9bc346c6e5f..66231e3bc97 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UuidSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/UuidSerializer.js @@ -22,6 +22,7 @@ */ import { Buffer } from 'buffer'; +import { stringify as uuidStringify } from 'uuid'; export default class UuidSerializer { constructor(ioc) { @@ -61,60 +62,33 @@ export default class UuidSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true, nullable = false) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.UUID) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - } - if (fullyQualifiedFormat || nullable) { - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - if (cursor.length < 16) { - throw new Error('unexpected {value} length'); - } - len += 16; + /** + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + const bytes = await reader.readBytes(16); + return uuidStringify(bytes); + } - // Example: 2075278D-F624-4B2B-960D-25D374D57C04 - const v = - cursor.slice(0, 4).toString('hex') + - '-' + - cursor.slice(4, 6).toString('hex') + - '-' + - cursor.slice(6, 8).toString('hex') + - '-' + - cursor.slice(8, 10).toString('hex') + - '-' + - cursor.slice(10, 16).toString('hex'); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.UUID) { + throw new Error(`UuidSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`UuidSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexPropertySerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexPropertySerializer.js index 4e86d5b94b2..292b777f951 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexPropertySerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexPropertySerializer.js @@ -72,105 +72,50 @@ export default class VertexPropertySerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.VERTEXPROPERTY) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } + /** + * Async deserialization of vertex property value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + // {id} fully qualified + const id = await this.ioc.anySerializer.deserialize(reader); + + // {label} bare list, extract first element + const labelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); + const label = Array.isArray(labelList) && labelList.length > 0 ? labelList[0] : labelList; + + // {value} fully qualified + const value = await this.ioc.anySerializer.deserialize(reader); + + // {parent} fully qualified (always null in current TinkerPop) + await this.ioc.anySerializer.deserialize(reader); + + // {properties} fully qualified + const properties = await this.ioc.anySerializer.deserialize(reader); + + return new VertexProperty(id, label, value, properties || []); + } - // {id} is a fully qualified typed value composed of {type_code}{type_info}{value_flag}{value} - let id, id_len; - try { - ({ v: id, len: id_len } = this.ioc.anySerializer.deserialize(cursor)); - len += id_len; - } catch (err) { - err.message = '{id}: ' + err.message; - throw err; - } - cursor = cursor.slice(id_len); - - // {label} is a List value - let label, label_len; - try { - ({ v: label, len: label_len } = this.ioc.listSerializer.deserialize(cursor, false)); - label = Array.isArray(label) && label.length > 0 ? label[0] : label; - len += label_len; - } catch (err) { - err.message = '{label}: ' + err.message; - throw err; - } - cursor = cursor.slice(label_len); - - // {value} is a fully qualified typed value composed of {type_code}{type_info}{value_flag}{value} - let value, value_len; - try { - ({ v: value, len: value_len } = this.ioc.anySerializer.deserialize(cursor)); - len += value_len; - } catch (err) { - err.message = '{value}: ' + err.message; - throw err; - } - cursor = cursor.slice(value_len); - - // {parent} is a fully qualified typed value composed of {type_code}{type_info}{value_flag}{value} which contains the parent Vertex. - // Note that as TinkerPop currently send "references" only, this value will always be null. - let parent_len; - try { - ({ len: parent_len } = this.ioc.unspecifiedNullSerializer.deserialize(cursor)); - len += parent_len; - } catch (err) { - err.message = '{parent}: ' + err.message; - throw err; - } - // TODO: should we verify that parent is null? - cursor = cursor.slice(parent_len); - - // {properties} is a fully qualified typed value composed of {type_code}{type_info}{value_flag}{value} which contains properties. Note that as TinkerPop currently send "references" only, this value will always be null. - let properties, properties_len; - try { - ({ v: properties, len: properties_len } = this.ioc.anySerializer.deserialize(cursor)); - len += properties_len; - } catch (err) { - err.message = '{properties}: ' + err.message; - throw err; - } - // TODO: should we verify that properties is null? - cursor = cursor.slice(properties_len); - - // null properties are deserialized into empty lists - const vp_props = properties ? properties : []; - const v = new VertexProperty(id, label, value, vp_props); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.VERTEXPROPERTY) { + throw new Error(`VertexPropertySerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`VertexPropertySerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js index 5cd18ae6c0f..49771247d0d 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/VertexSerializer.js @@ -63,78 +63,44 @@ export default class VertexSerializer { return Buffer.concat(bufs); } - deserialize(buffer, fullyQualifiedFormat = true) { - let len = 0; - let cursor = buffer; - - try { - if (buffer === undefined || buffer === null || !(buffer instanceof Buffer)) { - throw new Error('buffer is missing'); - } - if (buffer.length < 1) { - throw new Error('buffer is empty'); - } - - if (fullyQualifiedFormat) { - const type_code = cursor.readUInt8(); - len++; - if (type_code !== this.ioc.DataType.VERTEX) { - throw new Error('unexpected {type_code}'); - } - cursor = cursor.slice(1); - - if (cursor.length < 1) { - throw new Error('{value_flag} is missing'); - } - const value_flag = cursor.readUInt8(); - len++; - if (value_flag === 1) { - return { v: null, len }; - } - if (value_flag !== 0) { - throw new Error('unexpected {value_flag}'); - } - cursor = cursor.slice(1); - } - - let id, id_len; - try { - ({ v: id, len: id_len } = this.ioc.anySerializer.deserialize(cursor)); - len += id_len; - } catch (err) { - err.message = '{id}: ' + err.message; - throw err; - } - cursor = cursor.slice(id_len); - - let label, label_len; - try { - ({ v: label, len: label_len } = this.ioc.listSerializer.deserialize(cursor, false)); - label = Array.isArray(label) && label.length > 0 ? label[0] : label; - len += label_len; - } catch (err) { - err.message = '{label}: ' + err.message; - throw err; - } - cursor = cursor.slice(label_len); - - let properties, properties_len; - try { - ({ v: properties, len: properties_len } = this.ioc.anySerializer.deserialize(cursor)); - len += properties_len; - } catch (err) { - err.message = '{properties}: ' + err.message; - throw err; - } - cursor = cursor.slice(properties_len); - - // null properties are deserialized into empty lists - const vertex_props = properties ? properties : []; + /** + * Async deserialization of vertex value bytes from a StreamReader. + * @param {StreamReader} reader + * @param {number} valueFlag + * @param {number} typeCode + * @returns {Promise} + */ + async deserializeValue(reader, valueFlag, typeCode) { + // {id} fully qualified + const id = await this.ioc.anySerializer.deserialize(reader); + + // {label} bare list, extract first element + const labelList = await this.ioc.listSerializer.deserializeValue(reader, 0x00, this.ioc.DataType.LIST); + const label = Array.isArray(labelList) && labelList.length > 0 ? labelList[0] : labelList; + + // {properties} fully qualified + const properties = await this.ioc.anySerializer.deserialize(reader); + + return new Vertex(id, label, properties || []); + } - const v = new Vertex(id, label, vertex_props); - return { v, len }; - } catch (err) { - throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); + /** + * Async fully-qualified deserialization from a StreamReader. + * @param {StreamReader} reader + * @returns {Promise} + */ + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.VERTEX) { + throw new Error(`VertexSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`VertexSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); } + return this.deserializeValue(reader, value_flag, type_code); } } diff --git a/gremlin-js/gremlin-javascript/package.json b/gremlin-js/gremlin-javascript/package.json index 9f87678562f..49ecba346b2 100644 --- a/gremlin-js/gremlin-javascript/package.json +++ b/gremlin-js/gremlin-javascript/package.json @@ -33,7 +33,9 @@ }, "typesVersions": { "*": { - "language": ["./build/esm/language/index.d.ts"] + "language": [ + "./build/esm/language/index.d.ts" + ] } }, "files": [ @@ -43,7 +45,6 @@ "antlr4ng": "3.0.16", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", - "readable-stream": "^4.5.2", "uuid": "^9.0.1" }, "optionalDependencies": { @@ -56,7 +57,6 @@ "@eslint/js": "^9.16.0", "@knighted/duel": "^4.0.2", "@tsconfig/node18": "^18.2.2", - "@types/readable-stream": "^4.0.10", "@types/uuid": "^9.0.8", "antlr-ng": "^1.0.10", "chai": "~4.5.0", diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js index a686188fb6f..2a9c48b5fd9 100644 --- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/GraphBinaryReader-test.js @@ -26,48 +26,65 @@ import { assert } from 'chai'; import { Buffer } from 'buffer'; import GraphBinaryReader from '../../../lib/structure/io/binary/internals/GraphBinaryReader.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; import ioc from '../../../lib/structure/io/binary/GraphBinary.js'; +import { Traverser } from '../../../lib/process/traversal.js'; +import ResponseError from '../../../lib/driver/response-error.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('undefined buffer throws error', async () => { + try { + await reader.readResponse(undefined); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /Buffer is missing/); + } }); - it('null buffer throws error', () => { - assert.throws(() => reader.readResponse(null), /Buffer is missing/); + it('null buffer throws error', async () => { + try { + await reader.readResponse(null); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /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/); + it('empty buffer throws error', async () => { + try { + await reader.readResponse(Buffer.alloc(0)); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /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 0x00', async () => { + try { + await reader.readResponse(Buffer.from([0x00, 0x00])); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /Unsupported version/); + } }); - it('rejects version 0xFF', () => { - const buffer = Buffer.from([0xFF]); - assert.throws(() => reader.readResponse(buffer), /Unsupported version '255'/); + it('rejects version 0x81', async () => { + try { + await reader.readResponse(Buffer.from([0x81, 0x00])); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /Unsupported version/); + } }); }); describe('non-bulked responses', () => { - it('single value', () => { + it('single value', async () => { const buffer = Buffer.from([ 0x84, // version 0x00, // bulked=false @@ -77,14 +94,14 @@ describe('GraphBinaryReader', () => { 0x01, // status_message null flag 0x01 // exception null flag ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.deepEqual(result, { status: { code: 200, message: null, exception: null }, result: { data: [67], bulked: false }, }); }); - it('multiple values', () => { + it('multiple values', async () => { const buffer = Buffer.from([ 0x84, // version 0x00, // bulked=false @@ -95,14 +112,14 @@ describe('GraphBinaryReader', () => { 0x01, // status_message null 0x01 // exception null ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.deepEqual(result, { status: { code: 200, message: null, exception: null }, result: { data: [67, 'hello'], bulked: false } }); }); - it('empty result', () => { + it('empty result', async () => { const buffer = Buffer.from([ 0x84, // version 0x00, // bulked=false @@ -111,7 +128,7 @@ describe('GraphBinaryReader', () => { 0x01, // status_message null 0x01 // exception null ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.deepEqual(result, { status: { code: 204, message: null, exception: null }, result: { data: [], bulked: false } @@ -120,7 +137,7 @@ describe('GraphBinaryReader', () => { }); describe('bulked responses', () => { - it('single item with bulk count', () => { + it('single item with bulk count', async () => { const buffer = Buffer.from([ 0x84, // version 0x01, // bulked=true @@ -131,14 +148,14 @@ describe('GraphBinaryReader', () => { 0x01, // status_message null 0x01 // exception null ]); - const result = reader.readResponse(buffer); + const result = await 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', () => { + it('multiple items with bulk counts', async () => { const buffer = Buffer.from([ 0x84, // version 0x01, // bulked=true @@ -151,7 +168,7 @@ describe('GraphBinaryReader', () => { 0x01, // status_message null 0x01 // exception null ]); - const result = reader.readResponse(buffer); + const result = await 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 } @@ -160,31 +177,31 @@ describe('GraphBinaryReader', () => { }); describe('status codes', () => { - it('status 403', () => { + it('status 403', async () => { 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); + const result = await reader.readResponse(buffer); assert.equal(result.status.code, 403); }); - it('status 500', () => { + it('status 500', async () => { 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); + const result = await reader.readResponse(buffer); assert.equal(result.status.code, 500); }); }); describe('nullable status_message', () => { - it('present message', () => { + it('present message', async () => { const buffer = Buffer.from([ 0x84, 0x00, // version, bulked=false 0xFD, 0x00, 0x00, // marker @@ -193,11 +210,11 @@ describe('GraphBinaryReader', () => { 0x00, 0x00, 0x00, 0x07, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, // bare String: "Success" 0x01 // exception null ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.equal(result.status.message, 'Success'); }); - it('null message', () => { + it('null message', async () => { const buffer = Buffer.from([ 0x84, 0x00, // version, bulked=false 0xFD, 0x00, 0x00, // marker @@ -205,13 +222,13 @@ describe('GraphBinaryReader', () => { 0x01, // message null flag 0x01 // exception null ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.equal(result.status.message, null); }); }); describe('nullable exception', () => { - it('present exception', () => { + it('present exception', async () => { const buffer = Buffer.from([ 0x84, 0x00, // version, bulked=false 0xFD, 0x00, 0x00, // marker @@ -220,11 +237,11 @@ describe('GraphBinaryReader', () => { 0x00, // exception present flag 0x00, 0x00, 0x00, 0x05, 0x45, 0x72, 0x72, 0x6F, 0x72 // bare String: "Error" ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.equal(result.status.exception, 'Error'); }); - it('null exception', () => { + it('null exception', async () => { const buffer = Buffer.from([ 0x84, 0x00, // version, bulked=false 0xFD, 0x00, 0x00, // marker @@ -232,13 +249,13 @@ describe('GraphBinaryReader', () => { 0x01, // message null 0x01 // exception null flag ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.equal(result.status.exception, null); }); }); describe('error response', () => { - it('no result data with error status', () => { + it('no result data with error status', async () => { const buffer = Buffer.from([ 0x84, 0x00, // version, bulked=false 0xFD, 0x00, 0x00, // marker (no data) @@ -248,7 +265,7 @@ describe('GraphBinaryReader', () => { 0x00, // exception present 0x00, 0x00, 0x00, 0x09, 0x45, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E // "Exception" ]); - const result = reader.readResponse(buffer); + const result = await reader.readResponse(buffer); assert.deepEqual(result, { status: { code: 500, message: 'Internal error', exception: 'Exception' }, result: { data: [], bulked: false } @@ -257,7 +274,7 @@ describe('GraphBinaryReader', () => { }); describe('complex result values', () => { - it('vertex in result data', () => { + it('vertex in result data', async () => { const buffer = Buffer.from([ 0x84, 0x00, // version, bulked=false 0x11, 0x00, // fq Vertex: type_code=0x11, value_flag=0x00 @@ -269,10 +286,156 @@ describe('GraphBinaryReader', () => { 0x00, 0x00, 0x00, 0xC8, // status_code=200 0x01, 0x01 // null message, null exception ]); - const result = reader.readResponse(buffer); + const result = await 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'); }); }); + + describe('readResponseStream', () => { + it('non-bulked stream yields raw values', async () => { + const buffer = Buffer.from([ + 0x84, // version + 0x00, // bulked=false + 0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int=67 + 0xFD, 0x00, 0x00, // marker + 0x00, 0x00, 0x00, 0xC8, // status_code=200 + 0x01, // status_message null + 0x01 // exception null + ]); + const streamReader = StreamReader.fromBuffer(buffer); + const results = []; + for await (const item of reader.readResponseStream(streamReader)) { + results.push(item); + } + assert.deepEqual(results, [67]); + }); + + it('bulked stream yields Traverser objects', async () => { + const buffer = Buffer.from([ + 0x84, // version + 0x01, // bulked=true + 0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int=67 + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // fq Long=3 + 0xFD, 0x00, 0x00, // marker + 0x00, 0x00, 0x00, 0xC8, // status_code=200 + 0x01, // status_message null + 0x01 // exception null + ]); + const streamReader = StreamReader.fromBuffer(buffer); + const results = []; + for await (const item of reader.readResponseStream(streamReader)) { + results.push(item); + } + assert.equal(results.length, 1); + assert.instanceOf(results[0], Traverser); + assert.equal(results[0].object, 67); + assert.equal(results[0].bulk, 3); + }); + + it('bulked stream Traversers expand correctly via Traversal', async () => { + const { Traversal } = await import('../../../lib/process/traversal.js'); + const buffer = Buffer.from([ + 0x84, // version + 0x01, // bulked=true + 0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int=67 + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // fq Long=3 + 0xFD, 0x00, 0x00, // marker + 0x00, 0x00, 0x00, 0xC8, // status_code=200 + 0x01, 0x01 + ]); + const streamReader = StreamReader.fromBuffer(buffer); + const gen = reader.readResponseStream(streamReader); + const traversal = new Traversal(null, null); + traversal._resultsStream = gen; + const list = await traversal.toList(); + // bulk=3 should expand to 3 copies of 67 + assert.deepEqual(list, [67, 67, 67]); + }); + + it('trailing status is accessible via Traversal.getStatus()', async () => { + const { Traversal } = await import('../../../lib/process/traversal.js'); + const buffer = Buffer.from([ + 0x84, // version + 0x00, // bulked=false + 0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int=67 + 0xFD, 0x00, 0x00, // marker + 0x00, 0x00, 0x00, 0xC8, // status_code=200 + 0x00, // message present + 0x00, 0x00, 0x00, 0x02, 0x4F, 0x4B, // "OK" + 0x01 // exception null + ]); + const streamReader = StreamReader.fromBuffer(buffer); + const gen = reader.readResponseStream(streamReader); + const traversal = new Traversal(null, null); + traversal._resultsStream = gen; + + assert.isNull(traversal.getStatus()); + await traversal.toList(); + const status = traversal.getStatus(); + assert.isNotNull(status); + assert.equal(status.code, 200); + assert.equal(status.message, 'OK'); + assert.isNull(status.exception); + }); + + it('throws ResponseError on server error status after yielding values', async () => { + const buffer = Buffer.from([ + 0x84, // version + 0x00, // bulked=false + 0x01, 0x00, 0x00, 0x00, 0x00, 0x43, // fq Int=67 + 0xFD, 0x00, 0x00, // marker + 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" + 0x00, // exception present + 0x00, 0x00, 0x00, 0x09, 0x45, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E // "Exception" + ]); + const streamReader = StreamReader.fromBuffer(buffer); + const results = []; + try { + for await (const item of reader.readResponseStream(streamReader)) { + results.push(item); + } + assert.fail('should have thrown'); + } catch (e) { + assert.instanceOf(e, ResponseError); + assert.equal(e.statusCode, 500); + assert.match(e.message, /Internal error/); + } + // Verify partial results were yielded before the error + assert.deepEqual(results, [67]); + }); + + it('hasNext() peeks without consuming from streaming source', async () => { + const { Traversal } = await import('../../../lib/process/traversal.js'); + const buffer = Buffer.from([ + 0x84, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, // fq Int=1 + 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, // fq Int=2 + 0xFD, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC8, + 0x01, 0x01 + ]); + const streamReader = StreamReader.fromBuffer(buffer); + const gen = reader.readResponseStream(streamReader); + const traversal = new Traversal(null, null); + traversal._resultsStream = gen; + + // hasNext should return true without consuming + assert.isTrue(await traversal.hasNext()); + assert.isTrue(await traversal.hasNext()); // calling again should still be true + + // next() should return the peeked value + const first = await traversal.next(); + assert.equal(first.value, 1); + + assert.isTrue(await traversal.hasNext()); + const second = await traversal.next(); + assert.equal(second.value, 2); + + assert.isFalse(await traversal.hasNext()); + }); + }); }); diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/StreamReader-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/StreamReader-test.js new file mode 100644 index 00000000000..d14c0ae5959 --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/StreamReader-test.js @@ -0,0 +1,215 @@ +/* + * 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. + */ + +import { assert } from 'chai'; +import { Buffer } from 'buffer'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; + +/** + * Helper: create a ReadableStream that yields the given chunks with optional delays. + * @param {Buffer[]} chunks + * @returns {ReadableStream} + */ +function chunkedStream(chunks) { + let i = 0; + return new ReadableStream({ + pull(controller) { + if (i < chunks.length) { + controller.enqueue(new Uint8Array(chunks[i++])); + } else { + controller.close(); + } + }, + }); +} + +describe('StreamReader', () => { + describe('fromBuffer', () => { + it('readUInt8 reads single bytes', async () => { + const reader = StreamReader.fromBuffer(Buffer.from([0x01, 0xff, 0x00])); + assert.equal(await reader.readUInt8(), 0x01); + assert.equal(await reader.readUInt8(), 0xff); + assert.equal(await reader.readUInt8(), 0x00); + }); + + it('readByte reads signed bytes', async () => { + const reader = StreamReader.fromBuffer(Buffer.from([0x7f, 0x80])); + assert.equal(await reader.readByte(), 127); + assert.equal(await reader.readByte(), -128); + }); + + it('readInt16BE reads signed 16-bit', async () => { + const buf = Buffer.alloc(4); + buf.writeInt16BE(12345, 0); + buf.writeInt16BE(-1, 2); + const reader = StreamReader.fromBuffer(buf); + assert.equal(await reader.readInt16BE(), 12345); + assert.equal(await reader.readInt16BE(), -1); + }); + + it('readInt32BE reads signed 32-bit', async () => { + const buf = Buffer.alloc(8); + buf.writeInt32BE(2147483647, 0); + buf.writeInt32BE(-2147483648, 4); + const reader = StreamReader.fromBuffer(buf); + assert.equal(await reader.readInt32BE(), 2147483647); + assert.equal(await reader.readInt32BE(), -2147483648); + }); + + it('readBigInt64BE reads signed 64-bit', async () => { + const buf = Buffer.alloc(8); + buf.writeBigInt64BE(9223372036854775807n, 0); + const reader = StreamReader.fromBuffer(buf); + assert.equal(await reader.readBigInt64BE(), 9223372036854775807n); + }); + + it('readFloatBE reads 32-bit float', async () => { + const buf = Buffer.alloc(4); + buf.writeFloatBE(3.14, 0); + const reader = StreamReader.fromBuffer(buf); + assert.closeTo(await reader.readFloatBE(), 3.14, 0.001); + }); + + it('readDoubleBE reads 64-bit double', async () => { + const buf = Buffer.alloc(8); + buf.writeDoubleBE(3.141592653589793, 0); + const reader = StreamReader.fromBuffer(buf); + assert.equal(await reader.readDoubleBE(), 3.141592653589793); + }); + + it('readBytes returns exact slice', async () => { + const reader = StreamReader.fromBuffer(Buffer.from([0x01, 0x02, 0x03, 0x04])); + const bytes = await reader.readBytes(2); + assert.deepEqual([...bytes], [0x01, 0x02]); + const rest = await reader.readBytes(2); + assert.deepEqual([...rest], [0x03, 0x04]); + }); + + it('throws on read past end of buffer', async () => { + const reader = StreamReader.fromBuffer(Buffer.from([0x01])); + await reader.readUInt8(); + try { + await reader.readUInt8(); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /Unexpected end of buffer/); + } + }); + + it('mixed reads advance offset correctly', async () => { + const buf = Buffer.alloc(13); + buf.writeUInt8(0xAB, 0); + buf.writeInt32BE(42, 1); + buf.writeDoubleBE(1.5, 5); + const reader = StreamReader.fromBuffer(buf); + assert.equal(await reader.readUInt8(), 0xAB); + assert.equal(await reader.readInt32BE(), 42); + assert.equal(await reader.readDoubleBE(), 1.5); + }); + }); + + describe('fromReadableStream', () => { + it('reads from a single chunk', async () => { + const stream = chunkedStream([Buffer.from([0x01, 0x02, 0x03])]); + const reader = StreamReader.fromReadableStream(stream); + assert.equal(await reader.readUInt8(), 0x01); + assert.equal(await reader.readUInt8(), 0x02); + assert.equal(await reader.readUInt8(), 0x03); + }); + + it('reads across chunk boundaries', async () => { + // Int32 (4 bytes) split across two 2-byte chunks + const buf = Buffer.alloc(4); + buf.writeInt32BE(305419896, 0); // 0x12345678 + const stream = chunkedStream([buf.subarray(0, 2), buf.subarray(2, 4)]); + const reader = StreamReader.fromReadableStream(stream); + assert.equal(await reader.readInt32BE(), 305419896); + }); + + it('reads across many small chunks', async () => { + // 8-byte double split into 1-byte chunks + const buf = Buffer.alloc(8); + buf.writeDoubleBE(2.718281828, 0); + const chunks = []; + for (let i = 0; i < 8; i++) { + chunks.push(buf.subarray(i, i + 1)); + } + const stream = chunkedStream(chunks); + const reader = StreamReader.fromReadableStream(stream); + assert.closeTo(await reader.readDoubleBE(), 2.718281828, 1e-9); + }); + + it('handles readBytes spanning chunks', async () => { + const stream = chunkedStream([Buffer.from([0x01, 0x02]), Buffer.from([0x03, 0x04, 0x05])]); + const reader = StreamReader.fromReadableStream(stream); + const bytes = await reader.readBytes(4); + assert.deepEqual([...bytes], [0x01, 0x02, 0x03, 0x04]); + assert.equal(await reader.readUInt8(), 0x05); + }); + + it('throws on premature end of stream', async () => { + const stream = chunkedStream([Buffer.from([0x01])]); + const reader = StreamReader.fromReadableStream(stream); + try { + await reader.readInt32BE(); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /Unexpected end of stream/); + } + }); + + it('handles empty stream', async () => { + const stream = chunkedStream([]); + const reader = StreamReader.fromReadableStream(stream); + try { + await reader.readUInt8(); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /Unexpected end of stream/); + } + }); + + it('sequential reads across multiple chunks', async () => { + const buf = Buffer.alloc(9); + buf.writeUInt8(0x84, 0); // version byte + buf.writeInt32BE(200, 1); // status code + buf.writeInt32BE(-1, 5); // another int + // Split: [version + 1 byte of int] [3 bytes of int + 2 bytes] [2 bytes] + const stream = chunkedStream([buf.subarray(0, 2), buf.subarray(2, 7), buf.subarray(7, 9)]); + const reader = StreamReader.fromReadableStream(stream); + assert.equal(await reader.readUInt8(), 0x84); + assert.equal(await reader.readInt32BE(), 200); + assert.equal(await reader.readInt32BE(), -1); + }); + + it('handles Uint8Array chunks from fetch', async () => { + // fetch response.body yields Uint8Array, not Buffer + const data = new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x2A]); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(data); + controller.close(); + }, + }); + const reader = StreamReader.fromReadableStream(stream); + assert.equal(await reader.readUInt8(), 0x01); + assert.equal(await reader.readInt32BE(), 42); + }); + }); +}); diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/async-deserialize-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/async-deserialize-test.js new file mode 100644 index 00000000000..f73a29086bb --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/async-deserialize-test.js @@ -0,0 +1,285 @@ +/* + * 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. + */ + +import { assert } from 'chai'; +import { Buffer } from 'buffer'; +import ioc from '../../../lib/structure/io/binary/GraphBinary.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; +import { END_OF_STREAM } from '../../../lib/structure/io/binary/internals/MarkerSerializer.js'; + +/** + * Round-trip tests: serialize a value with the existing sync serializer, + * then deserialize it with the new async StreamReader-based deserializer. + */ +describe('Async deserialization round-trip', () => { + async function roundTrip(serializer, value) { + const buf = serializer.serialize(value); + const reader = StreamReader.fromBuffer(buf); + return ioc.anySerializer.deserialize(reader); + } + + describe('primitives via AnySerializer', () => { + it('Int', async () => { + assert.equal(await roundTrip(ioc.intSerializer, 42), 42); + }); + + it('Int negative', async () => { + assert.equal(await roundTrip(ioc.intSerializer, -2147483648), -2147483648); + }); + + it('Int null', async () => { + assert.isNull(await roundTrip(ioc.intSerializer, null)); + }); + + it('Long', async () => { + assert.equal(await roundTrip(ioc.longSerializer, 9007199254740991), 9007199254740991); + }); + + it('Short', async () => { + assert.equal(await roundTrip(ioc.shortSerializer, 32767), 32767); + }); + + it('Byte', async () => { + assert.equal(await roundTrip(ioc.byteSerializer, -128), -128); + }); + + it('Float', async () => { + const result = await roundTrip(ioc.floatSerializer, 3.14); + assert.closeTo(result, 3.14, 0.001); + }); + + it('Double', async () => { + assert.equal(await roundTrip(ioc.doubleSerializer, 3.141592653589793), 3.141592653589793); + }); + + it('Double NaN', async () => { + assert.isNaN(await roundTrip(ioc.doubleSerializer, NaN)); + }); + + it('Double Infinity', async () => { + assert.equal(await roundTrip(ioc.doubleSerializer, Infinity), Infinity); + }); + + it('Boolean true', async () => { + assert.equal(await roundTrip(ioc.booleanSerializer, true), true); + }); + + it('Boolean false', async () => { + assert.equal(await roundTrip(ioc.booleanSerializer, false), false); + }); + + it('String', async () => { + assert.equal(await roundTrip(ioc.stringSerializer, 'hello world'), 'hello world'); + }); + + it('String empty', async () => { + assert.equal(await roundTrip(ioc.stringSerializer, ''), ''); + }); + + it('String null', async () => { + assert.isNull(await roundTrip(ioc.stringSerializer, null)); + }); + + it('UUID', async () => { + const uuid = '41d2e28a-20a4-4ab0-b379-d810dede3786'; + assert.equal(await roundTrip(ioc.uuidSerializer, uuid), uuid); + }); + + it('DateTime', async () => { + const date = new Date('2023-06-15T10:30:00.000Z'); + const result = await roundTrip(ioc.dateTimeSerializer, date); + assert.equal(result.getTime(), date.getTime()); + }); + + it('BigInteger', async () => { + assert.equal(await roundTrip(ioc.bigIntegerSerializer, 123456789012345678901234567890n), 123456789012345678901234567890n); + }); + + it('BigInteger negative', async () => { + assert.equal(await roundTrip(ioc.bigIntegerSerializer, -42n), -42n); + }); + + it('Binary', async () => { + const buf = Buffer.from([0x01, 0x02, 0x03]); + const result = await roundTrip(ioc.binarySerializer, buf); + assert.deepEqual([...result], [0x01, 0x02, 0x03]); + }); + + it('null (UnspecifiedNull)', async () => { + assert.isNull(await roundTrip(ioc.unspecifiedNullSerializer, null)); + }); + }); + + describe('Marker', () => { + it('deserializes EndOfStream marker', async () => { + // Marker wire format: type_code=0xFD, value_flag=0x00, value=0x00 + const buf = Buffer.from([0xFD, 0x00, 0x00]); + const reader = StreamReader.fromBuffer(buf); + const result = await ioc.anySerializer.deserialize(reader); + assert.equal(result, END_OF_STREAM); + }); + }); + + describe('Enum', () => { + it('Direction.OUT', async () => { + const { direction } = await import('../../../lib/process/traversal.js'); + const result = await roundTrip(ioc.enumSerializer, direction.out); + assert.equal(result.typeName, 'Direction'); + assert.equal(result.elementName, 'OUT'); + }); + + it('T.id', async () => { + const { t } = await import('../../../lib/process/traversal.js'); + const result = await roundTrip(ioc.enumSerializer, t.id); + assert.equal(result.typeName, 'T'); + assert.equal(result.elementName, 'id'); + }); + }); + + describe('direct serializer.deserialize(reader)', () => { + it('IntSerializer.deserialize reads fully-qualified', async () => { + const buf = ioc.intSerializer.serialize(99); + const reader = StreamReader.fromBuffer(buf); + const result = await ioc.intSerializer.deserialize(reader); + assert.equal(result, 99); + }); + + it('StringSerializer.deserialize reads fully-qualified', async () => { + const buf = ioc.stringSerializer.serialize('test'); + const reader = StreamReader.fromBuffer(buf); + const result = await ioc.stringSerializer.deserialize(reader); + assert.equal(result, 'test'); + }); + }); + + describe('streaming (chunked) deserialization', () => { + it('deserializes Int from 1-byte chunks', async () => { + const buf = ioc.intSerializer.serialize(42); + const chunks = []; + for (let i = 0; i < buf.length; i++) { + chunks.push(buf.subarray(i, i + 1)); + } + const stream = new ReadableStream({ + pull(controller) { + if (chunks.length > 0) { + controller.enqueue(new Uint8Array(chunks.shift())); + } else { + controller.close(); + } + }, + }); + const reader = StreamReader.fromReadableStream(stream); + const result = await ioc.anySerializer.deserialize(reader); + assert.equal(result, 42); + }); + + it('deserializes String from 2-byte chunks', async () => { + const buf = ioc.stringSerializer.serialize('hello'); + const chunks = []; + for (let i = 0; i < buf.length; i += 2) { + chunks.push(buf.subarray(i, Math.min(i + 2, buf.length))); + } + const stream = new ReadableStream({ + pull(controller) { + if (chunks.length > 0) { + controller.enqueue(new Uint8Array(chunks.shift())); + } else { + controller.close(); + } + }, + }); + const reader = StreamReader.fromReadableStream(stream); + const result = await ioc.anySerializer.deserialize(reader); + assert.equal(result, 'hello'); + }); + }); +}); + +describe('Async deserialization round-trip — compound types', () => { + async function roundTrip(serializer, value) { + const buf = serializer.serialize(value); + const reader = (await import('../../../lib/structure/io/binary/internals/StreamReader.js')).default.fromBuffer(buf); + return ioc.anySerializer.deserialize(reader); + } + + it('List of ints', async () => { + const result = await roundTrip(ioc.listSerializer, [1, 2, 3]); + assert.deepEqual(result, [1, 2, 3]); + }); + + it('List empty', async () => { + const result = await roundTrip(ioc.listSerializer, []); + assert.deepEqual(result, []); + }); + + it('List null', async () => { + assert.isNull(await roundTrip(ioc.listSerializer, null)); + }); + + it('Map', async () => { + const map = new Map([['a', 1], ['b', 2]]); + const result = await roundTrip(ioc.mapSerializer, map); + assert.equal(result.get('a'), 1); + assert.equal(result.get('b'), 2); + }); + + it('Map empty', async () => { + const result = await roundTrip(ioc.mapSerializer, new Map()); + assert.equal(result.size, 0); + }); + + it('Nested list of strings', async () => { + const result = await roundTrip(ioc.listSerializer, ['hello', 'world']); + assert.deepEqual(result, ['hello', 'world']); + }); + + it('Vertex', async () => { + const { Vertex } = await import('../../../lib/structure/graph.js'); + const v = new Vertex(1, 'person', []); + const result = await roundTrip(ioc.vertexSerializer, v); + assert.equal(result.id, 1); + assert.equal(result.label, 'person'); + }); + + it('Edge', async () => { + const { Vertex, Edge } = await import('../../../lib/structure/graph.js'); + const outV = new Vertex(1, 'person', []); + const inV = new Vertex(2, 'person', []); + const e = new Edge(10, outV, 'knows', inV, []); + const result = await roundTrip(ioc.edgeSerializer, e); + assert.equal(result.id, 10); + assert.equal(result.label, 'knows'); + }); + + it('Path', async () => { + const { Path } = await import('../../../lib/structure/graph.js'); + const p = new Path([['a'], ['b']], [1, 2]); + const result = await roundTrip(ioc.pathSerializer, p); + assert.deepEqual(result.labels, [['a'], ['b']]); + assert.deepEqual(result.objects, [1, 2]); + }); + + it('Property', async () => { + const { Property } = await import('../../../lib/structure/graph.js'); + const prop = new Property('name', 'marko'); + const result = await roundTrip(ioc.propertySerializer, prop); + assert.equal(result.key, 'name'); + assert.equal(result.value, 'marko'); + }); +}); diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/error-cases-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/error-cases-test.js index 1d4616beaef..8837ecc5959 100644 --- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/error-cases-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/error-cases-test.js @@ -26,365 +26,380 @@ import { assert } from 'chai'; import { Buffer } from 'buffer'; import ioc from '../../../lib/structure/io/binary/GraphBinary.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; import { P, TextP, Traverser } from '../../../lib/process/traversal.js'; import { OptionsStrategy } from '../../../lib/process/traversal-strategy.js'; const { anySerializer, intSerializer, longSerializer, stringSerializer, listSerializer, mapSerializer, uuidSerializer, dateTimeSerializer, floatSerializer, shortSerializer, byteSerializer, bigIntegerSerializer, binarySerializer, setSerializer, enumSerializer } = ioc; +/** Helper: assert that an async call rejects with a message matching the pattern */ +async function assertRejects(fn, pattern) { + try { + await fn(); + assert.fail('Expected an error to be thrown'); + } catch (e) { + if (pattern) assert.match(e.message, pattern); + } +} + describe('GraphBinary v4 Error Cases', () => { describe('Buffer validation', () => { - it('undefined buffer throws error', () => { - assert.throws(() => anySerializer.deserialize(undefined), /buffer is missing/); + it('undefined buffer throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer(undefined))); }); - it('null buffer throws error', () => { - assert.throws(() => anySerializer.deserialize(null), /buffer is missing/); + it('null buffer throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer(null))); }); - it('non-Buffer object throws error', () => { - assert.throws(() => anySerializer.deserialize('not a buffer'), /buffer is missing/); + it('non-Buffer object throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer('not a buffer'))); }); - it('empty buffer throws error', () => { - assert.throws(() => anySerializer.deserialize(Buffer.alloc(0)), /buffer is empty/); + it('empty buffer throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer(Buffer.alloc(0))), /Unexpected end of buffer/); }); - it('buffer with only type_code, no value_flag throws error', () => { - assert.throws(() => anySerializer.deserialize(Buffer.from([0x01])), /value_flag.*missing/); + it('buffer with only type_code, no value_flag throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01]))), /Unexpected end of buffer/); }); - it('individual serializer with undefined buffer throws error', () => { - assert.throws(() => intSerializer.deserialize(undefined), /buffer is missing/); + it('individual serializer with undefined buffer throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(undefined))); }); - it('individual serializer with null buffer throws error', () => { - assert.throws(() => intSerializer.deserialize(null), /buffer is missing/); + it('individual serializer with null buffer throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(null))); }); - it('individual serializer with empty buffer throws error', () => { - assert.throws(() => intSerializer.deserialize(Buffer.alloc(0)), /buffer is empty/); + it('individual serializer with empty buffer throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(Buffer.alloc(0))), /Unexpected end of buffer/); }); }); describe('Type code errors', () => { - it('unknown type code throws error', () => { - assert.throws(() => anySerializer.deserialize(Buffer.from([0x99, 0x00])), /unknown.*type_code/); + it('unknown type code throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x99, 0x00]))), /unknown.*type_code/); }); - it('wrong type code for INT throws error', () => { - assert.throws(() => intSerializer.deserialize(Buffer.from([0x02, 0x00, 0x00, 0x00, 0x00, 0x00])), /unexpected.*type_code/); + it('wrong type code for INT throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x02, 0x00, 0x00, 0x00, 0x00, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for LONG throws error', () => { - assert.throws(() => longSerializer.deserialize(Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x00])), /unexpected.*type_code/); + it('wrong type code for LONG throws error', async () => { + await assertRejects(() => longSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for STRING throws error', () => { - assert.throws(() => stringSerializer.deserialize(Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x00])), /unexpected.*type_code/); + it('wrong type code for STRING throws error', async () => { + await assertRejects(() => stringSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for DOUBLE throws error', () => { - assert.throws(() => ioc.doubleSerializer.deserialize(Buffer.from([0x01, 0x00])), /unexpected.*type_code/); + it('wrong type code for DOUBLE throws error', async () => { + await assertRejects(() => ioc.doubleSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for BOOLEAN throws error', () => { - assert.throws(() => ioc.booleanSerializer.deserialize(Buffer.from([0x01, 0x00])), /unexpected.*type_code/); + it('wrong type code for BOOLEAN throws error', async () => { + await assertRejects(() => ioc.booleanSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for UUID throws error', () => { - assert.throws(() => uuidSerializer.deserialize(Buffer.from([0x01, 0x00])), /unexpected.*type_code/); + it('wrong type code for UUID throws error', async () => { + await assertRejects(() => uuidSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for LIST throws error', () => { - assert.throws(() => listSerializer.deserialize(Buffer.from([0x01, 0x00])), /unexpected.*type_code/); + it('wrong type code for LIST throws error', async () => { + await assertRejects(() => listSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for MAP throws error', () => { - assert.throws(() => mapSerializer.deserialize(Buffer.from([0x01, 0x00])), /unexpected.*type_code/); + it('wrong type code for MAP throws error', async () => { + await assertRejects(() => mapSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for FLOAT throws error', () => { - assert.throws(() => floatSerializer.deserialize(Buffer.from([0x09, 0x00])), /unexpected.*type_code/); + it('wrong type code for FLOAT throws error', async () => { + await assertRejects(() => floatSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x09, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for SHORT throws error', () => { - assert.throws(() => shortSerializer.deserialize(Buffer.from([0x27, 0x00])), /unexpected.*type_code/); + it('wrong type code for SHORT throws error', async () => { + await assertRejects(() => shortSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x27, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for BYTE throws error', () => { - assert.throws(() => byteSerializer.deserialize(Buffer.from([0x25, 0x00])), /unexpected.*type_code/); + it('wrong type code for BYTE throws error', async () => { + await assertRejects(() => byteSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x25, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for BIGINTEGER throws error', () => { - assert.throws(() => bigIntegerSerializer.deserialize(Buffer.from([0x24, 0x00])), /unexpected.*type_code/); + it('wrong type code for BIGINTEGER throws error', async () => { + await assertRejects(() => bigIntegerSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x24, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for BINARY throws error', () => { - assert.throws(() => binarySerializer.deserialize(Buffer.from([0x26, 0x00])), /unexpected.*type_code/); + it('wrong type code for BINARY throws error', async () => { + await assertRejects(() => binarySerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x26, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for SET throws error', () => { - assert.throws(() => setSerializer.deserialize(Buffer.from([0x0C, 0x00])), /unexpected.*type_code/); + it('wrong type code for SET throws error', async () => { + await assertRejects(() => setSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x0C, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for DATETIME throws error', () => { - assert.throws(() => dateTimeSerializer.deserialize(Buffer.from([0x05, 0x00])), /unexpected.*type_code/); + it('wrong type code for DATETIME throws error', async () => { + await assertRejects(() => dateTimeSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x05, 0x00]))), /unexpected.*type_code/); }); - it('wrong type code for ENUM throws error', () => { - assert.throws(() => enumSerializer.deserialize(Buffer.from([0x19, 0x00])), /unexpected.*type_code/); + it('wrong type code for ENUM throws error', async () => { + await assertRejects(() => enumSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x19, 0x00]))), /unexpected.*type_code/); }); }); describe('Value flag errors', () => { - it('invalid value_flag 0x03 for INT throws error', () => { - assert.throws(() => intSerializer.deserialize(Buffer.from([0x01, 0x03])), /unexpected.*value_flag/); + it('invalid value_flag 0x03 for INT throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x03]))), /unexpected.*value_flag/); + }); + + it('invalid value_flag 0x0F for STRING throws error', async () => { + await assertRejects(() => stringSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x03, 0x0F]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x0F for STRING throws error', () => { - assert.throws(() => stringSerializer.deserialize(Buffer.from([0x03, 0x0F])), /unexpected.*value_flag/); + it('invalid value_flag 0xFF for LIST throws error', async () => { + await assertRejects(() => listSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x09, 0xFF]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0xFF for LIST throws error', () => { - assert.throws(() => listSerializer.deserialize(Buffer.from([0x09, 0xFF])), /unexpected.*value_flag/); + it('invalid value_flag 0x05 for MAP throws error', async () => { + await assertRejects(() => mapSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x0A, 0x05]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x05 for MAP throws error', () => { - assert.throws(() => mapSerializer.deserialize(Buffer.from([0x0A, 0x05])), /unexpected.*value_flag/); + it('invalid value_flag 0x10 for DOUBLE throws error', async () => { + await assertRejects(() => ioc.doubleSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x07, 0x10]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x10 for DOUBLE throws error', () => { - assert.throws(() => ioc.doubleSerializer.deserialize(Buffer.from([0x07, 0x10])), /unexpected.*value_flag/); + it('invalid value_flag 0x04 for FLOAT throws error', async () => { + await assertRejects(() => floatSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x08, 0x04]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x04 for FLOAT throws error', () => { - assert.throws(() => floatSerializer.deserialize(Buffer.from([0x08, 0x04])), /unexpected.*value_flag/); + it('invalid value_flag 0x06 for SHORT throws error', async () => { + await assertRejects(() => shortSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x26, 0x06]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x06 for SHORT throws error', () => { - assert.throws(() => shortSerializer.deserialize(Buffer.from([0x26, 0x06])), /unexpected.*value_flag/); + it('invalid value_flag 0x07 for BYTE throws error', async () => { + await assertRejects(() => byteSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x24, 0x07]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x07 for BYTE throws error', () => { - assert.throws(() => byteSerializer.deserialize(Buffer.from([0x24, 0x07])), /unexpected.*value_flag/); + it('invalid value_flag 0x08 for BIGINTEGER throws error', async () => { + await assertRejects(() => bigIntegerSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x23, 0x08]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x08 for BIGINTEGER throws error', () => { - assert.throws(() => bigIntegerSerializer.deserialize(Buffer.from([0x23, 0x08])), /unexpected.*value_flag/); + it('invalid value_flag 0x09 for BINARY throws error', async () => { + await assertRejects(() => binarySerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x25, 0x09]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x09 for BINARY throws error', () => { - assert.throws(() => binarySerializer.deserialize(Buffer.from([0x25, 0x09])), /unexpected.*value_flag/); + it('invalid value_flag 0x0A for SET throws error', async () => { + await assertRejects(() => setSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x0B, 0x0A]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x0A for SET throws error', () => { - assert.throws(() => setSerializer.deserialize(Buffer.from([0x0B, 0x0A])), /unexpected.*value_flag/); + it('invalid value_flag 0x0B for DATETIME throws error', async () => { + await assertRejects(() => dateTimeSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x04, 0x0B]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x0B for DATETIME throws error', () => { - assert.throws(() => dateTimeSerializer.deserialize(Buffer.from([0x04, 0x0B])), /unexpected.*value_flag/); + it('invalid value_flag 0x0C for UUID throws error', async () => { + await assertRejects(() => uuidSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x0C, 0x0C]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x0C for UUID throws error', () => { - assert.throws(() => uuidSerializer.deserialize(Buffer.from([0x0C, 0x0C])), /unexpected.*value_flag/); + it('invalid value_flag 0x0D for ENUM throws error', async () => { + await assertRejects(() => enumSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x18, 0x0D]))), /unexpected.*value_flag/); }); - it('invalid value_flag 0x0D for ENUM throws error', () => { - assert.throws(() => enumSerializer.deserialize(Buffer.from([0x18, 0x0D])), /unexpected.*value_flag/); + it('invalid value_flag dispatched via anySerializer throws error', async () => { + await assertRejects(() => anySerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x0D]))), /unexpected.*value_flag/); }); - it('missing value_flag for INT throws error', () => { - assert.throws(() => intSerializer.deserialize(Buffer.from([0x01])), /value_flag.*missing/); + it('missing value_flag for INT throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01]))), /Unexpected end of buffer/); }); - it('missing value_flag for STRING throws error', () => { - assert.throws(() => stringSerializer.deserialize(Buffer.from([0x03])), /value_flag.*missing/); + it('missing value_flag for STRING throws error', async () => { + await assertRejects(() => stringSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x03]))), /Unexpected end of buffer/); }); - it('missing value_flag for LIST throws error', () => { - assert.throws(() => listSerializer.deserialize(Buffer.from([0x09])), /value_flag.*missing/); + it('missing value_flag for LIST throws error', async () => { + await assertRejects(() => listSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x09]))), /Unexpected end of buffer/); }); - it('missing value_flag for MAP throws error', () => { - assert.throws(() => mapSerializer.deserialize(Buffer.from([0x0A])), /value_flag.*missing/); + it('missing value_flag for MAP throws error', async () => { + await assertRejects(() => mapSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x0A]))), /Unexpected end of buffer/); }); }); describe('Truncated data', () => { - it('INT with only 2 of 4 value bytes throws error', () => { - assert.throws(() => intSerializer.deserialize(Buffer.from([0x01, 0x00, 0x00, 0x01])), /unexpected.*value.*length/); + it('INT with only 2 of 4 value bytes throws error', async () => { + await assertRejects(() => intSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x01, 0x00, 0x00, 0x01]))), /Unexpected end of buffer/); }); - it('LONG with only 4 of 8 value bytes throws error', () => { - assert.throws(() => longSerializer.deserialize(Buffer.from([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])), /unexpected.*value.*length/); + it('LONG with only 4 of 8 value bytes throws error', async () => { + await assertRejects(() => longSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]))), /Unexpected end of buffer/); }); - it('DOUBLE with only 4 of 8 value bytes throws error', () => { - assert.throws(() => ioc.doubleSerializer.deserialize(Buffer.from([0x07, 0x00, 0x00, 0x00, 0x00, 0x01])), /unexpected.*value.*length/); + it('DOUBLE with only 4 of 8 value bytes throws error', async () => { + await assertRejects(() => ioc.doubleSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x07, 0x00, 0x00, 0x00, 0x00, 0x01]))), /Unexpected end of buffer/); }); - it('FLOAT with only 2 of 4 value bytes throws error', () => { - assert.throws(() => ioc.floatSerializer.deserialize(Buffer.from([0x08, 0x00, 0x00, 0x01])), /unexpected.*value.*length/); + it('FLOAT with only 2 of 4 value bytes throws error', async () => { + await assertRejects(() => ioc.floatSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x08, 0x00, 0x00, 0x01]))), /Unexpected end of buffer/); }); - it('SHORT with only 1 of 2 value bytes throws error', () => { - assert.throws(() => ioc.shortSerializer.deserialize(Buffer.from([0x26, 0x00, 0x01])), /unexpected.*value.*length/); + it('SHORT with only 1 of 2 value bytes throws error', async () => { + await assertRejects(() => ioc.shortSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x26, 0x00, 0x01]))), /Unexpected end of buffer/); }); - it('STRING with length 10 but only 3 bytes throws error', () => { + it('STRING with length 10 but only 3 bytes throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x03, 0x00]), // STRING, value_flag=0x00 Buffer.from([0x00, 0x00, 0x00, 0x0A]), // length=10 Buffer.from([0x41, 0x42, 0x43]) // only 3 bytes: "ABC" ]); - assert.throws(() => stringSerializer.deserialize(buffer), /unexpected.*text_value.*length/); + await assertRejects(() => stringSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('LIST with length 5 but only 1 item throws error', () => { + it('LIST with length 5 but only 1 item throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x09, 0x00]), // LIST, value_flag=0x00 Buffer.from([0x00, 0x00, 0x00, 0x05]), // length=5 Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x01]) // only 1 INT item ]); - assert.throws(() => listSerializer.deserialize(buffer), /item_1.*buffer is empty/); + await assertRejects(() => listSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('MAP with length 3 but only 1 entry throws error', () => { + it('MAP with length 3 but only 1 entry throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x0A, 0x00]), // MAP, value_flag=0x00 Buffer.from([0x00, 0x00, 0x00, 0x03]), // length=3 Buffer.from([0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x6B, 0x65, 0x79]), // key: "key" Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x01]) // value: 1 ]); - assert.throws(() => mapSerializer.deserialize(buffer), /{item_1}.*buffer is empty/); + await assertRejects(() => mapSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('UUID with only 8 of 16 bytes throws error', () => { + it('UUID with only 8 of 16 bytes throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x0C, 0x00]), // UUID, value_flag=0x00 Buffer.from([0x41, 0xD2, 0xE2, 0x8A, 0x20, 0x13, 0x4E, 0x35]) // only 8 bytes ]); - assert.throws(() => uuidSerializer.deserialize(buffer), /unexpected.*value.*length/); + await assertRejects(() => uuidSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('DATETIME with only 10 of 18 bytes throws error', () => { + it('DATETIME with only 10 of 18 bytes throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x04, 0x00]), // DATETIME, value_flag=0x00 Buffer.from([0x07, 0xB2, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) // only 10 bytes ]); - assert.throws(() => dateTimeSerializer.deserialize(buffer), /unexpected.*value.*length/); + await assertRejects(() => dateTimeSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('BOOLEAN with no value byte throws error', () => { - assert.throws(() => ioc.booleanSerializer.deserialize(Buffer.from([0x27, 0x00])), /unexpected.*value.*length/); + it('BOOLEAN with no value byte throws error', async () => { + await assertRejects(() => ioc.booleanSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x27, 0x00]))), /Unexpected end of buffer/); }); - it('BYTE with no value byte throws error', () => { - assert.throws(() => ioc.byteSerializer.deserialize(Buffer.from([0x24, 0x00])), /unexpected.*value.*length/); + it('BYTE with no value byte throws error', async () => { + await assertRejects(() => ioc.byteSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0x24, 0x00]))), /Unexpected end of buffer/); }); - it('BIGINTEGER with truncated length throws error', () => { + it('BIGINTEGER with truncated length throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x23, 0x00]), // BIGINTEGER, value_flag=0x00 Buffer.from([0x00, 0x00]) // only 2 bytes of length instead of 4 ]); - assert.throws(() => bigIntegerSerializer.deserialize(buffer), /{length}.*unexpected.*value.*length/); + await assertRejects(() => bigIntegerSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('BIGINTEGER with length=0 throws error', () => { + it('BIGINTEGER with length=0 throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x23, 0x00]), // BIGINTEGER, value_flag=0x00 Buffer.from([0x00, 0x00, 0x00, 0x00]) // length=0 ]); - assert.throws(() => bigIntegerSerializer.deserialize(buffer), /{length}=0 is less than one/); + await assertRejects(() => bigIntegerSerializer.deserialize(StreamReader.fromBuffer(buffer)), /length.*less than one|length.*0/); }); - it('BINARY with truncated value data throws error', () => { + it('BINARY with truncated value data throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x25, 0x00]), // BINARY, value_flag=0x00 Buffer.from([0x00, 0x00, 0x00, 0x05]), // length=5 Buffer.from([0x01, 0x02]) // only 2 bytes instead of 5 ]); - assert.throws(() => binarySerializer.deserialize(buffer), /{value}.*unexpected.*value.*length/); + await assertRejects(() => binarySerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('SET with length 5 but only 1 item throws error', () => { + it('SET with length 5 but only 1 item throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x0B, 0x00]), // SET, value_flag=0x00 Buffer.from([0x00, 0x00, 0x00, 0x05]), // length=5 Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x01]) // only 1 INT item ]); - assert.throws(() => setSerializer.deserialize(buffer), /{item_1}.*buffer is empty/); + await assertRejects(() => setSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('ENUM with truncated elementName throws error', () => { + it('ENUM with truncated elementName throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x18, 0x00]), // DIRECTION, value_flag=0x00 Buffer.from([0x03, 0x00, 0x00, 0x00, 0x00, 0x05]) // STRING with length=5 but no data ]); - assert.throws(() => enumSerializer.deserialize(buffer), /elementName.*unexpected.*text_value.*length/); + await assertRejects(() => enumSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); }); describe('Negative lengths', () => { - it('STRING with negative length throws error', () => { + it('STRING with negative length throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x03, 0x00]), // STRING, value_flag=0x00 Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) // length=-1 ]); - assert.throws(() => stringSerializer.deserialize(buffer), /length.*less than zero/); + await assertRejects(() => stringSerializer.deserialize(StreamReader.fromBuffer(buffer)), /length.*less than zero/); }); - it('LIST with negative length throws error', () => { + it('LIST with negative length throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x09, 0x00]), // LIST, value_flag=0x00 Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) // length=-1 ]); - assert.throws(() => listSerializer.deserialize(buffer), /length.*less than zero/); + await assertRejects(() => listSerializer.deserialize(StreamReader.fromBuffer(buffer)), /length.*less than zero/); }); - it('MAP with negative length throws error', () => { + it('MAP with negative length throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x0A, 0x00]), // MAP, value_flag=0x00 Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) // length=-1 ]); - assert.throws(() => mapSerializer.deserialize(buffer), /length.*less than zero/); + await assertRejects(() => mapSerializer.deserialize(StreamReader.fromBuffer(buffer)), /length.*less than zero/); }); - it('SET with negative length throws error', () => { + it('SET with negative length throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x0B, 0x00]), // SET, value_flag=0x00 Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) // length=-1 ]); - assert.throws(() => ioc.setSerializer.deserialize(buffer), /length.*less than zero/); + await assertRejects(() => setSerializer.deserialize(StreamReader.fromBuffer(buffer)), /length.*less than zero/); }); - it('BINARY with negative length throws error', () => { + it('BINARY with negative length throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x25, 0x00]), // BINARY, value_flag=0x00 Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) // length=-1 ]); - assert.throws(() => ioc.binarySerializer.deserialize(buffer), /length.*less than zero/); + await assertRejects(() => binarySerializer.deserialize(StreamReader.fromBuffer(buffer)), /length.*less than zero/); }); }); describe('Bulk flag errors', () => { - it('LIST with bulk flag but missing bulk count data throws error', () => { + it('LIST with bulk flag but missing bulk count data throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x09, 0x02]), // LIST, value_flag=0x02 (bulk) Buffer.from([0x00, 0x00, 0x00, 0x01]), // length=1 Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x01]) // 1 INT item but no bulk count ]); - assert.throws(() => listSerializer.deserialize(buffer), /{item_0}.*bulk count is missing/); + await assertRejects(() => listSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); - it('SET with bulk flag but missing bulk count data throws error', () => { + it('SET with bulk flag but missing bulk count data throws error', async () => { const buffer = Buffer.concat([ Buffer.from([0x0B, 0x02]), // SET, value_flag=0x02 (bulk) Buffer.from([0x00, 0x00, 0x00, 0x01]), // length=1 Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x01]) // 1 INT item but no bulk count ]); - assert.throws(() => setSerializer.deserialize(buffer), /{item_0}.*bulk count is missing/); + await assertRejects(() => setSerializer.deserialize(StreamReader.fromBuffer(buffer)), /Unexpected end of buffer/); }); }); diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js index 4f9beaeaa5f..28be83f8862 100644 --- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js @@ -29,10 +29,11 @@ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { model } from './model.js'; import ioc from '../../../lib/structure/io/binary/GraphBinary.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; const { anySerializer } = ioc; -const gbinDir = 'gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/'; +const gbinDir = '../../gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/'; const searchPattern = 'gremlin-javascript/src/main'; const thisFile = fileURLToPath(import.meta.url); const defaultDir = thisFile.substring(0, thisFile.indexOf(searchPattern)); @@ -48,9 +49,9 @@ function run(name, comparator = assertEqual) { const fileBytes = readGbinFile(name); const modelValue = model[name]; - it('deserialize .gbin matches model', () => { - const result = anySerializer.deserialize(fileBytes); - comparator(result.v, modelValue); + it('deserialize .gbin matches model', async () => { + const result = await anySerializer.deserialize(StreamReader.fromBuffer(fileBytes)); + comparator(result, modelValue); }); it('serialize model matches .gbin bytes', () => { @@ -58,16 +59,17 @@ function run(name, comparator = assertEqual) { assert.deepStrictEqual(serialized, fileBytes); }); - it('round-trip serialize(deserialize(fileBytes)) matches .gbin', () => { - const deserialized = anySerializer.deserialize(fileBytes); - const reserialized = anySerializer.serialize(deserialized.v); + it('round-trip serialize(deserialize(fileBytes)) matches .gbin', async () => { + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(fileBytes)); + const reserialized = anySerializer.serialize(deserialized); assert.deepStrictEqual(reserialized, fileBytes); }); - it('length tracking', () => { + it('length tracking', async () => { const garbageBytes = Buffer.concat([fileBytes, Buffer.from([0xFF])]); - const result = anySerializer.deserialize(garbageBytes); - assert.strictEqual(result.len, fileBytes.length); + const reader = StreamReader.fromBuffer(garbageBytes); + await anySerializer.deserialize(reader); + assert.strictEqual(reader.position, fileBytes.length); }); }); } @@ -78,21 +80,26 @@ function runWriteRead(name, comparator = assertEqual) { const fileBytes = readGbinFile(name); const modelValue = model[name]; - it('deserialize .gbin matches model', () => { - const result = anySerializer.deserialize(fileBytes); - comparator(result.v, modelValue); + it('deserialize .gbin matches model', async () => { + const result = await anySerializer.deserialize(StreamReader.fromBuffer(fileBytes)); + comparator(result, modelValue); }); - it('double round-trip idempotency', () => { - const firstRoundTrip = anySerializer.deserialize(anySerializer.serialize(modelValue)); - const secondRoundTrip = anySerializer.deserialize(anySerializer.serialize(firstRoundTrip.v)); - comparator(firstRoundTrip.v, secondRoundTrip.v); + it('double round-trip idempotency', async () => { + const firstRoundTrip = await anySerializer.deserialize( + StreamReader.fromBuffer(anySerializer.serialize(modelValue)), + ); + const secondRoundTrip = await anySerializer.deserialize( + StreamReader.fromBuffer(anySerializer.serialize(firstRoundTrip)), + ); + comparator(firstRoundTrip, secondRoundTrip); }); - it('length tracking', () => { + it('length tracking', async () => { const garbageBytes = Buffer.concat([fileBytes, Buffer.from([0xFF])]); - const result = anySerializer.deserialize(garbageBytes); - assert.strictEqual(result.len, fileBytes.length); + const reader = StreamReader.fromBuffer(garbageBytes); + await anySerializer.deserialize(reader); + assert.strictEqual(reader.position, fileBytes.length); }); }); } @@ -103,15 +110,16 @@ function runRead(name, comparator = assertEqual) { const fileBytes = readGbinFile(name); const modelValue = model[name]; - it('deserialize .gbin matches model', () => { - const result = anySerializer.deserialize(fileBytes); - comparator(result.v, modelValue); + it('deserialize .gbin matches model', async () => { + const result = await anySerializer.deserialize(StreamReader.fromBuffer(fileBytes)); + comparator(result, modelValue); }); - it('length tracking', () => { + it('length tracking', async () => { const garbageBytes = Buffer.concat([fileBytes, Buffer.from([0xFF])]); - const result = anySerializer.deserialize(garbageBytes); - assert.strictEqual(result.len, fileBytes.length); + const reader = StreamReader.fromBuffer(garbageBytes); + await anySerializer.deserialize(reader); + assert.strictEqual(reader.position, fileBytes.length); }); }); } diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/null-handling-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/null-handling-test.js index fc9f8bf2aef..9a206dc2fd0 100644 --- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/null-handling-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/null-handling-test.js @@ -24,7 +24,8 @@ */ import { assert } from 'chai'; -import ioc from '../../../lib/structure/io/binary/GraphBinary.js'; +import ioc, {enumSerializer} from '../../../lib/structure/io/binary/GraphBinary.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; const { anySerializer, DataType } = ioc; describe('GraphBinary v4 Null Handling Tests', () => { @@ -39,10 +40,9 @@ describe('GraphBinary v4 Null Handling Tests', () => { assert.deepStrictEqual(result, Buffer.from([0xFE, 0x01])); }); - it('deserialize unspecified null', () => { - const result = anySerializer.deserialize(Buffer.from([0xFE, 0x01])); - assert.strictEqual(result.v, null); - assert.strictEqual(result.len, 2); + it('deserialize unspecified null', async () => { + const result = await anySerializer.deserialize(StreamReader.fromBuffer(Buffer.from([0xFE, 0x01]))); + assert.strictEqual(result, null); }); }); @@ -82,57 +82,53 @@ describe('GraphBinary v4 Null Handling Tests', () => { assert.deepStrictEqual(result, Buffer.from([code, 0x01])); }); - it('deserialize fully-qualified null', () => { - const result = ioc[serializer].deserialize(Buffer.from([code, 0x01]), true); - assert.strictEqual(result.v, null); - assert.strictEqual(result.len, 2); + it('deserialize fully-qualified null', async () => { + const result = await ioc[serializer].deserialize(StreamReader.fromBuffer(Buffer.from([code, 0x01]))); + assert.strictEqual(result, null); }); }); }); // EnumSerializer has special null handling - it doesn't support null/undefined directly describe('DIRECTION', () => { - it('deserialize fully-qualified null', () => { - const result = ioc.enumSerializer.deserialize(Buffer.from([DataType.DIRECTION, 0x01]), true); - assert.strictEqual(result.v, null); - assert.strictEqual(result.len, 2); + it('deserialize fully-qualified null', async () => { + const result = await ioc.enumSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([DataType.DIRECTION, 0x01]))); + assert.strictEqual(result, null); }); }); describe('T', () => { - it('deserialize fully-qualified null', () => { - const result = ioc.enumSerializer.deserialize(Buffer.from([DataType.T, 0x01]), true); - assert.strictEqual(result.v, null); - assert.strictEqual(result.len, 2); + it('deserialize fully-qualified null', async () => { + const result = await ioc.enumSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([DataType.T, 0x01]))); + assert.strictEqual(result, null); }); }); describe('MERGE', () => { - it('deserialize fully-qualified null', () => { - const result = ioc.enumSerializer.deserialize(Buffer.from([DataType.MERGE, 0x01]), true); - assert.strictEqual(result.v, null); - assert.strictEqual(result.len, 2); + it('deserialize fully-qualified null', async () => { + const result = await ioc.enumSerializer.deserialize(StreamReader.fromBuffer(Buffer.from([DataType.MERGE, 0x01]))); + assert.strictEqual(result, null); }); }); }); describe('per-type bare null', () => { const bareNullTests = [ - { name: 'INT', serializer: 'intSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: 0 }, - { name: 'LONG', serializer: 'longSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), defaultValue: 0 }, - { name: 'STRING', serializer: 'stringSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: '' }, - { name: 'DOUBLE', serializer: 'doubleSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), defaultValue: 0.0 }, - { name: 'FLOAT', serializer: 'floatSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: 0.0 }, - { name: 'BOOLEAN', serializer: 'booleanSerializer', expected: Buffer.from([0x00]), defaultValue: false }, - { name: 'SHORT', serializer: 'shortSerializer', expected: Buffer.from([0x00, 0x00]), defaultValue: 0 }, - { name: 'BYTE', serializer: 'byteSerializer', expected: Buffer.from([0x00]), defaultValue: 0 }, - { name: 'UUID', serializer: 'uuidSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), defaultValue: '00000000-0000-0000-0000-000000000000' }, - { name: 'BIGINTEGER', serializer: 'bigIntegerSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x01, 0x00]), defaultValue: BigInt(0) }, - { name: 'DATETIME', serializer: 'dateTimeSerializer', expected: Buffer.alloc(18), defaultValue: null }, - { name: 'BINARY', serializer: 'binarySerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: Buffer.alloc(0) } + { name: 'INT', code: DataType.INT, serializer: 'intSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: 0 }, + { name: 'LONG', code: DataType.LONG, serializer: 'longSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), defaultValue: 0 }, + { name: 'STRING', code: DataType.STRING, serializer: 'stringSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: '' }, + { name: 'DOUBLE', code: DataType.DOUBLE, serializer: 'doubleSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), defaultValue: 0.0 }, + { name: 'FLOAT', code: DataType.FLOAT, serializer: 'floatSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: 0.0 }, + { name: 'BOOLEAN', code: DataType.BOOLEAN, serializer: 'booleanSerializer', expected: Buffer.from([0x00]), defaultValue: false }, + { name: 'SHORT', code: DataType.SHORT, serializer: 'shortSerializer', expected: Buffer.from([0x00, 0x00]), defaultValue: 0 }, + { name: 'BYTE', code: DataType.BYTE, serializer: 'byteSerializer', expected: Buffer.from([0x00]), defaultValue: 0 }, + { name: 'UUID', code: DataType.UUID, serializer: 'uuidSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), defaultValue: '00000000-0000-0000-0000-000000000000' }, + { name: 'BIGINTEGER', code: DataType.BIGINTEGER, serializer: 'bigIntegerSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x01, 0x00]), defaultValue: BigInt(0) }, + { name: 'DATETIME', code: DataType.DATETIME, serializer: 'dateTimeSerializer', expected: Buffer.alloc(18), defaultValue: null }, + { name: 'BINARY', code: DataType.BINARY, serializer: 'binarySerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: Buffer.alloc(0) } ]; - bareNullTests.forEach(({ name, serializer, expected, defaultValue }) => { + bareNullTests.forEach(({ name, code, serializer, expected, defaultValue }) => { describe(name, () => { it('serialize null bare', () => { const result = ioc[serializer].serialize(null, false); @@ -144,34 +140,35 @@ describe('GraphBinary v4 Null Handling Tests', () => { assert.deepStrictEqual(result, expected); }); - it('deserialize bare null', () => { - const result = ioc[serializer].deserialize(expected, false); + it('deserialize bare null', async () => { + const reader = StreamReader.fromBuffer(expected); + const result = await ioc[serializer].deserializeValue(reader, 0x00, code); if (defaultValue === null) { // DATETIME: 18 zero bytes produce a valid Date object but with implementation-defined value - assert.instanceOf(result.v, Date); + assert.instanceOf(result, Date); } else if (typeof defaultValue === 'bigint') { - assert.strictEqual(result.v, defaultValue); + assert.strictEqual(result, defaultValue); } else if (Buffer.isBuffer(defaultValue)) { - assert.isTrue(Buffer.isBuffer(result.v)); - assert.isTrue(result.v.equals(defaultValue)); + assert.isTrue(Buffer.isBuffer(result)); + assert.isTrue(result.equals(defaultValue)); } else if (defaultValue instanceof Date) { - assert.instanceOf(result.v, Date); + assert.instanceOf(result, Date); } else { - assert.strictEqual(result.v, defaultValue); + assert.strictEqual(result, defaultValue); } - assert.strictEqual(result.len, expected.length); + assert.strictEqual(reader.position, expected.length); }); }); }); // Collection types have different bare null behavior const collectionTests = [ - { name: 'LIST', serializer: 'listSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: [] }, - { name: 'SET', serializer: 'setSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: new Set() }, - { name: 'MAP', serializer: 'mapSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: new Map() } + { name: 'LIST', code: DataType.LIST, serializer: 'listSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: [] }, + { name: 'SET', code: DataType.SET, serializer: 'setSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: new Set() }, + { name: 'MAP', code: DataType.MAP, serializer: 'mapSerializer', expected: Buffer.from([0x00, 0x00, 0x00, 0x00]), defaultValue: new Map() } ]; - collectionTests.forEach(({ name, serializer, expected, defaultValue }) => { + collectionTests.forEach(({ name, code, serializer, expected, defaultValue }) => { describe(name, () => { it('serialize null bare', () => { const result = ioc[serializer].serialize(null, false); @@ -183,33 +180,34 @@ describe('GraphBinary v4 Null Handling Tests', () => { assert.deepStrictEqual(result, expected); }); - it('deserialize bare null', () => { - const result = ioc[serializer].deserialize(expected, false); + it('deserialize bare null', async () => { + const reader = StreamReader.fromBuffer(expected); + const result = await ioc[serializer].deserializeValue(reader, 0x00, code); if (defaultValue instanceof Set) { - assert.instanceOf(result.v, Set); - assert.strictEqual(result.v.size, 0); + assert.instanceOf(result, Set); + assert.strictEqual(result.size, 0); } else if (defaultValue instanceof Map) { - assert.instanceOf(result.v, Map); - assert.strictEqual(result.v.size, 0); + assert.instanceOf(result, Map); + assert.strictEqual(result.size, 0); } else if (Array.isArray(defaultValue)) { - assert.isArray(result.v); - assert.strictEqual(result.v.length, 0); + assert.isArray(result); + assert.strictEqual(result.length, 0); } - assert.strictEqual(result.len, expected.length); + assert.strictEqual(reader.position, expected.length); }); }); }); // Graph types serialize to empty structures const graphTests = [ - { name: 'VERTEX', serializer: 'vertexSerializer' }, - { name: 'EDGE', serializer: 'edgeSerializer' }, - { name: 'PROPERTY', serializer: 'propertySerializer' }, - { name: 'VERTEXPROPERTY', serializer: 'vertexPropertySerializer' }, - { name: 'PATH', serializer: 'pathSerializer' } + { name: 'VERTEX', code: DataType.VERTEX, serializer: 'vertexSerializer' }, + { name: 'EDGE', code: DataType.EDGE, serializer: 'edgeSerializer' }, + { name: 'PROPERTY', code: DataType.PROPERTY, serializer: 'propertySerializer' }, + { name: 'VERTEXPROPERTY', code: DataType.VERTEXPROPERTY, serializer: 'vertexPropertySerializer' }, + { name: 'PATH', code: DataType.PATH, serializer: 'pathSerializer' } ]; - graphTests.forEach(({ name, serializer }) => { + graphTests.forEach(({ name, code, serializer }) => { describe(name, () => { it('serialize null bare produces non-empty bytes', () => { const result = ioc[serializer].serialize(null, false); @@ -221,11 +219,12 @@ describe('GraphBinary v4 Null Handling Tests', () => { assert.isTrue(result.length > 0); }); - it('deserialize bare null produces default object', () => { + it('deserialize bare null produces default object', async () => { const nullBytes = ioc[serializer].serialize(null, false); - const result = ioc[serializer].deserialize(nullBytes, false); - assert.isNotNull(result.v); - assert.strictEqual(result.len, nullBytes.length); + const reader = StreamReader.fromBuffer(nullBytes); + const result = await ioc[serializer].deserializeValue(reader, 0x00, code); + assert.isNotNull(result); + assert.strictEqual(reader.position, nullBytes.length); }); }); }); diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/type-detection-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/type-detection-test.js index 966784d5788..309b0dff2fb 100644 --- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/type-detection-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/type-detection-test.js @@ -28,6 +28,7 @@ import { assert } from 'chai'; import { Vertex, Edge, Property, VertexProperty, Path } from '../../../lib/structure/graph.js'; import { direction, t, merge } from '../../../lib/process/traversal.js'; import ioc, { DataType } from '../../../lib/structure/io/binary/GraphBinary.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; const { anySerializer } = ioc; @@ -73,11 +74,11 @@ describe('Type Detection Tests', () => { assert.strictEqual(anySerializer.serialize(-Infinity)[0], DataType.DOUBLE); }); - it('-0 → DOUBLE (0x07) preserving sign', () => { + it('-0 → DOUBLE (0x07) preserving sign', async () => { const result = anySerializer.serialize(-0); assert.strictEqual(result[0], DataType.DOUBLE); - const deserialized = anySerializer.deserialize(result); - assert.isTrue(Object.is(deserialized.v, -0)); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(result)); + assert.isTrue(Object.is(deserialized, -0)); }); it('BigInt within Int64 range → BIGINTEGER (0x23)', () => { @@ -280,52 +281,52 @@ describe('Type Detection Tests', () => { }); describe('Round-trip extras', () => { - it('nested collections', () => { + it('nested collections', async () => { const value = [1, [2, 3], new Map([['a', [4]]])]; const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.deepStrictEqual(deserialized.v, value); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.deepStrictEqual(deserialized, value); }); - it('empty containers', () => { + it('empty containers', async () => { const values = [[], new Set(), new Map(), '']; - values.forEach(value => { + for (const value of values) { const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.deepStrictEqual(deserialized.v, value); - }); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.deepStrictEqual(deserialized, value); + } }); - it('JS-specific label construction survives round-trip', () => { + it('JS-specific label construction survives round-trip', async () => { const vertex = new Vertex(123, 'person'); const serialized = anySerializer.serialize(vertex); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v.label, 'person'); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized.label, 'person'); }); - it('Unicode strings', () => { + it('Unicode strings', async () => { const value = '🚀 Hello 世界'; const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v, value); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized, value); }); - it('boundary integers', () => { + it('boundary integers', async () => { const values = [0, -1, 1]; - values.forEach(value => { + for (const value of values) { const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v, value); - }); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized, value); + } }); - it('NaN round-trip', () => { + it('NaN round-trip', async () => { const serialized = anySerializer.serialize(NaN); - const deserialized = anySerializer.deserialize(serialized); - assert.isTrue(Number.isNaN(deserialized.v)); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.isTrue(Number.isNaN(deserialized)); }); - it('complex nested structure', () => { + it('complex nested structure', async () => { const value = new Map([ ['numbers', [1, 2.5, BigInt(123)]], ['sets', new Set(['a', 'b'])], @@ -333,65 +334,65 @@ describe('Type Detection Tests', () => { ['nested', new Map([['inner', [true, false, null]]])] ]); const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.deepStrictEqual(deserialized.v, value); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.deepStrictEqual(deserialized, value); }); - it('mixed type array', () => { + it('mixed type array', async () => { const value = [1, 'string', true, null, new Date(0), Buffer.from('test')]; const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v.length, value.length); - assert.strictEqual(deserialized.v[0], 1); - assert.strictEqual(deserialized.v[1], 'string'); - assert.strictEqual(deserialized.v[2], true); - assert.strictEqual(deserialized.v[3], null); - assert.deepStrictEqual(deserialized.v[4], new Date(0)); - assert.isTrue(Buffer.isBuffer(deserialized.v[5])); - assert.isTrue(deserialized.v[5].equals(Buffer.from('test'))); - }); - - it('graph elements with properties', () => { + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized.length, value.length); + assert.strictEqual(deserialized[0], 1); + assert.strictEqual(deserialized[1], 'string'); + assert.strictEqual(deserialized[2], true); + assert.strictEqual(deserialized[3], null); + assert.deepStrictEqual(deserialized[4], new Date(0)); + assert.isTrue(Buffer.isBuffer(deserialized[5])); + assert.isTrue(deserialized[5].equals(Buffer.from('test'))); + }); + + it('graph elements with properties', async () => { const vertex = new Vertex(1, ['person'], [new VertexProperty(1, ['name'], 'marko')]); const serialized = anySerializer.serialize(vertex); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v.id, 1); - assert.strictEqual(deserialized.v.label, 'person'); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized.id, 1); + assert.strictEqual(deserialized.label, 'person'); }); - it('path with multiple labels', () => { + it('path with multiple labels', async () => { const path = new Path([new Set(['a']), new Set(['b', 'c'])], [1, 2]); const serialized = anySerializer.serialize(path); - const deserialized = anySerializer.deserialize(serialized); - assert.deepStrictEqual(deserialized.v.labels, path.labels); - assert.deepStrictEqual(deserialized.v.objects, path.objects); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.deepStrictEqual(deserialized.labels, path.labels); + assert.deepStrictEqual(deserialized.objects, path.objects); }); - it('large numbers', () => { + it('large numbers', async () => { const values = [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 2n**100n]; - values.forEach(value => { + for (const value of values) { const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v, value); - }); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized, value); + } }); - it('special float values', () => { + it('special float values', async () => { const values = [Infinity, -Infinity]; - values.forEach(value => { + for (const value of values) { const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v, value); - }); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized, value); + } }); - it('enum values', () => { + it('enum values', async () => { const values = [direction.out, direction.in, direction.both, t.id, t.key, t.label, t.value, merge.onCreate, merge.onMatch, merge.outV, merge.inV]; - values.forEach(value => { + for (const value of values) { const serialized = anySerializer.serialize(value); - const deserialized = anySerializer.deserialize(serialized); - assert.strictEqual(deserialized.v, value); - }); + const deserialized = await anySerializer.deserialize(StreamReader.fromBuffer(serialized)); + assert.strictEqual(deserialized, value); + } }); }); -}); \ No newline at end of file +}); diff --git a/gremlin-js/gremlin-javascript/test/unit/traversal-test.js b/gremlin-js/gremlin-javascript/test/unit/traversal-test.js index 264f5196555..3fd1857ea9a 100644 --- a/gremlin-js/gremlin-javascript/test/unit/traversal-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/traversal-test.js @@ -32,6 +32,12 @@ const V = statics.V; import { TraversalStrategies } from '../../lib/process/traversal-strategy.js'; import { RemoteConnection } from '../../lib/driver/remote-connection.js'; +async function* traversersToResultStream(traversers) { + for (const t of traversers) { + yield t; + } +} + describe('Traversal', function () { @@ -39,7 +45,7 @@ describe('Traversal', function () { it('should apply the strategies and return a Promise with the iterator item', function () { const strategyMock = { apply: function (traversal) { - traversal.results = [ new Traverser(1, 1), new Traverser(2, 1) ]; + traversal._resultsStream = traversersToResultStream([ new Traverser(1, 1), new Traverser(2, 1) ]); return Promise.resolve(); } }; @@ -67,7 +73,7 @@ describe('Traversal', function () { it('should support bulk', function () { const strategyMock = { apply: function (traversal) { - traversal.results = [ new Traverser(1, 2), new Traverser(2, 1) ]; + traversal._resultsStream = traversersToResultStream([ new Traverser(1, 2), new Traverser(2, 1) ]); return Promise.resolve(); } }; @@ -112,7 +118,7 @@ describe('Traversal', function () { it('should apply the strategies and return a Promise with an array', function () { const strategyMock = { apply: function (traversal) { - traversal.results = [ new Traverser('a', 1), new Traverser('b', 1) ]; + traversal._resultsStream = traversersToResultStream([ new Traverser('a', 1), new Traverser('b', 1) ]); return Promise.resolve(); } }; @@ -128,7 +134,7 @@ describe('Traversal', function () { it('should return an empty array when traversers is empty', function () { const strategyMock = { apply: function (traversal) { - traversal.results = []; + traversal._resultsStream = traversersToResultStream([]); return Promise.resolve(); } }; @@ -144,8 +150,8 @@ describe('Traversal', function () { it('should support bulk', function () { const strategyMock = { apply: function (traversal) { - traversal.results = [ new Traverser(1, 1), new Traverser(2, 3), new Traverser(3, 2), - new Traverser(4, 1) ]; + traversal._resultsStream = traversersToResultStream([ new Traverser(1, 1), new Traverser(2, 3), new Traverser(3, 2), + new Traverser(4, 1) ]); return Promise.resolve(); } }; @@ -165,7 +171,7 @@ describe('Traversal', function () { const strategyMock = { apply: function (traversal) { applied = true; - traversal.results = [ new Traverser('a', 1), new Traverser('b', 1) ]; + traversal._resultsStream = traversersToResultStream([ new Traverser('a', 1), new Traverser('b', 1) ]); return Promise.resolve(); } }; diff --git a/gremlin-js/package-lock.json b/gremlin-js/package-lock.json index 41246e16e36..42d6d6eeea4 100644 --- a/gremlin-js/package-lock.json +++ b/gremlin-js/package-lock.json @@ -19,7 +19,6 @@ "antlr4ng": "3.0.16", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", - "readable-stream": "^4.5.2", "uuid": "^9.0.1" }, "devDependencies": { @@ -27,7 +26,6 @@ "@eslint/js": "^9.16.0", "@knighted/duel": "^4.0.2", "@tsconfig/node18": "^18.2.2", - "@types/readable-stream": "^4.0.10", "@types/uuid": "^9.0.8", "antlr-ng": "^1.0.10", "chai": "~4.5.0", @@ -5410,16 +5408,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/readable-stream": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", - "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -6035,18 +6023,6 @@ "win32" ] }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -7827,30 +7803,12 @@ "through": "^2.3.8" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -11403,15 +11361,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -11647,22 +11596,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",