blob: 57b77022b27b9449db131a0827dce9cd228f77ad [file]
/*
* 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 { RemoteStrategy } from '../driver/remote-connection.js';
import DriverRemoteConnection from '../driver/driver-remote-connection.js';
import Client from '../driver/client.js';
import GremlinLang from './gremlin-lang.js';
import { GraphTraversalSource } from './graph-traversal.js';
import { TraversalStrategies } from './traversal-strategy.js';
/**
* Controls an explicit remote transaction. A thin wrapper around a Client that
* adds transaction lifecycle (begin/commit/rollback/close) and attaches a
* transactionId to every request.
*
* Created via Client.transact() or g.tx(). The traversal source (g alias) is
* inherited from the Client and cannot be changed.
*
* Transactions are short-lived and single-use. After commit or rollback, the
* transaction ID is invalid and the object cannot be reused.
*
* Each traversal on the transaction-bound source must be awaited before
* submitting the next, to ensure the server receives requests in order.
*
* This class is NOT thread-safe. Do not share a Transaction across concurrent
* async contexts without external synchronization.
*/
export class Transaction {
private _transactionId?: string;
private _isOpen = false;
private _failed = false;
private readonly _client: Client;
/**
* Creates a Transaction. Accepts a Client which provides the connection
* and traversal source for all transaction requests.
*/
constructor(client: Client) {
if (!client) {
throw new Error('Client is required for transactions');
}
this._client = client;
}
/**
* Spawns a GraphTraversalSource that is bound to a remote transaction.
*
* begin() is idempotent: calling it while a transaction is already open does not send a
* second begin to the server and does not throw - it reuses the existing transaction ID and
* returns a source bound to the same transaction. A transaction is single-use, so calling
* begin() after it has been closed (commit/rollback/failed begin) throws.
* @returns {Promise<GraphTraversalSource>}
*/
async begin(): Promise<GraphTraversalSource> {
if (this._failed) {
throw new Error('Transaction is closed and cannot be reused; begin a new transaction');
}
// idempotent: if a transaction is already open, reuse the existing transactionId without
// sending a second begin to the server, and return a source bound to the same transaction
if (!this._isOpen) {
let result;
try {
result = await this._client.submit('g.tx().begin()', null);
} catch (e) {
this._failed = true;
throw e;
}
const resultArray = result.toArray();
if (!resultArray || resultArray.length === 0) {
this._failed = true;
throw new Error('Server did not return transaction ID');
}
const resultMap = resultArray[0];
if (!resultMap || !(resultMap instanceof Map) || !resultMap.get('transactionId')) {
this._failed = true;
throw new Error('Server did not return transaction ID in expected format');
}
this._transactionId = resultMap.get('transactionId');
this._isOpen = true;
this._client.trackTransaction(this);
}
// Create a DriverRemoteConnection bound to this transaction. The DRC
// will automatically attach the transactionId to all requests.
const txConnection = new DriverRemoteConnection(
this._client.url,
this._client.options,
this._transactionId,
);
const strategies = new TraversalStrategies();
strategies.addStrategy(new RemoteStrategy(txConnection));
const gtx = new GraphTraversalSource(null as any, strategies, new GremlinLang());
gtx._boundTransaction = this;
return gtx;
}
/**
* Commits the transaction.
* @returns {Promise<void>}
*/
async commit(): Promise<void> {
await this.#closeTransaction('g.tx().commit()');
}
/**
* Rolls back the transaction.
* @returns {Promise<void>}
*/
async rollback(): Promise<void> {
await this.#closeTransaction('g.tx().rollback()');
}
/**
* Returns true if transaction is open.
*/
get isOpen(): boolean {
return this._isOpen;
}
/**
* Returns the server-generated transaction ID, or undefined if not yet begun.
*/
get transactionId(): string | undefined {
return this._transactionId;
}
/**
* Closes the transaction. Default behavior is rollback: partial work is
* discarded rather than accidentally persisted.
* @returns {Promise<void>}
*/
async close(): Promise<void> {
if (this._isOpen) {
await this.rollback();
}
}
/**
* Submits a gremlin-lang string within this transaction. The transactionId
* is automatically attached. Has the same signature as Client.submit().
*/
async submit(gremlin: string, bindings?: any, requestOptions?: any): Promise<any> {
if (!this._isOpen) {
throw new Error('Transaction is not open');
}
const opts = {
...requestOptions,
transactionId: this._transactionId,
};
return this._client.submit(gremlin, bindings || null, opts);
}
async #closeTransaction(script: string): Promise<void> {
if (!this._isOpen) {
throw new Error('Transaction is not open');
}
await this._client.submit(script, null, {
transactionId: this._transactionId
});
this._isOpen = false;
this._failed = true; // Terminal state: transaction cannot be reused
this._client.untrackTransaction(this);
}
}