Add complete Tryton RPC client implementation with examples and tests

This commit is contained in:
2025-09-26 14:04:07 -05:00
parent 6543e80525
commit 0771466433
13 changed files with 3333 additions and 0 deletions

254
src/cache.js Normal file
View File

@@ -0,0 +1,254 @@
/**
* Cache system similar to Python's CacheDict from Tryton
* Implements LRU (Least Recently Used) cache using JavaScript Map
*/
class CacheDict extends Map {
/**
* Create a new CacheDict
* @param {number} cacheLen - Maximum number of items to cache
* @param {Function} defaultFactory - Factory function for missing keys
*/
constructor(cacheLen = 10, defaultFactory = null) {
super();
this.cacheLen = cacheLen;
this.defaultFactory = defaultFactory;
}
/**
* Set a key-value pair and maintain LRU order
* @param {*} key - The key
* @param {*} value - The value
* @returns {CacheDict} - This instance for chaining
*/
set(key, value) {
// If key exists, delete it first to move to end
if (this.has(key)) {
this.delete(key);
}
super.set(key, value);
// Remove oldest entries if cache is full
while (this.size > this.cacheLen) {
const firstKey = this.keys().next().value;
this.delete(firstKey);
}
return this;
}
/**
* Get a value and move it to end (most recently used)
* @param {*} key - The key to retrieve
* @returns {*} - The value
*/
get(key) {
if (this.has(key)) {
const value = super.get(key);
// Move to end by re-setting
this.delete(key);
super.set(key, value);
return value;
}
// Handle missing key with default factory
if (this.defaultFactory) {
const value = this.defaultFactory();
this.set(key, value);
return value;
}
return undefined;
}
/**
* Override has() to update LRU order on access
* @param {*} key - The key to check
* @returns {boolean} - Whether the key exists
*/
has(key) {
const exists = super.has(key);
if (exists) {
// Move to end on access
const value = super.get(key);
this.delete(key);
super.set(key, value);
}
return exists;
}
/**
* Get current cache size
* @returns {number} - Number of items in cache
*/
get length() {
return this.size;
}
/**
* Clear all items from cache
*/
clear() {
super.clear();
}
/**
* Convert cache to array for debugging
* @returns {Array} - Array of [key, value] pairs
*/
toArray() {
return Array.from(this.entries());
}
}
/**
* Advanced cache for Tryton RPC with expiration support
*/
class TrytonCache {
constructor(cacheLen = 1024) {
this.store = new CacheDict(cacheLen, () => new CacheDict(cacheLen));
}
/**
* Check if a prefix is cached
* @param {string} prefix - Method prefix
* @returns {boolean} - Whether prefix exists
*/
cached(prefix) {
return this.store.has(prefix);
}
/**
* Set cache entry with expiration
* @param {string} prefix - Method prefix
* @param {string} key - Cache key
* @param {number|Date} expire - Expiration time
* @param {*} value - Value to cache
*/
set(prefix, key, expire, value) {
let expiration;
if (typeof expire === "number") {
// Assume seconds, convert to Date
expiration = new Date(Date.now() + expire * 1000);
} else if (expire instanceof Date) {
expiration = expire;
} else {
throw new Error("Invalid expiration type");
}
// Deep copy value to avoid mutations
const cachedValue = this._deepCopy(value);
this.store.get(prefix).set(key, {
expire: expiration,
value: cachedValue,
});
}
/**
* Get cached value if not expired
* @param {string} prefix - Method prefix
* @param {string} key - Cache key
* @returns {*} - Cached value
* @throws {Error} - If key not found or expired
*/
get(prefix, key) {
const now = new Date();
if (!this.store.has(prefix)) {
throw new Error("Key not found");
}
const prefixCache = this.store.get(prefix);
if (!prefixCache.has(key)) {
throw new Error("Key not found");
}
const entry = prefixCache.get(key);
if (entry.expire < now) {
prefixCache.delete(key);
throw new Error("Key expired");
}
console.log(`(cached) ${prefix} ${key}`);
return this._deepCopy(entry.value);
}
/**
* Clear cache for a specific prefix or all
* @param {string} [prefix] - Optional prefix to clear
*/
clear(prefix = null) {
if (prefix) {
if (this.store.has(prefix)) {
this.store.get(prefix).clear();
}
} else {
this.store.clear();
}
}
/**
* Deep copy objects to prevent mutations
* @param {*} obj - Object to copy
* @returns {*} - Deep copied object
* @private
*/
_deepCopy(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map((item) => this._deepCopy(item));
}
if (obj instanceof Buffer) {
return Buffer.from(obj);
}
if (typeof obj === "object") {
const copy = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = this._deepCopy(obj[key]);
}
}
return copy;
}
return obj;
}
/**
* Get cache statistics
* @returns {Object} - Cache statistics
*/
getStats() {
const stats = {
totalPrefixes: this.store.size,
totalEntries: 0,
prefixes: {},
};
for (const [prefix, prefixCache] of this.store.entries()) {
const count = prefixCache.size;
stats.totalEntries += count;
stats.prefixes[prefix] = count;
}
return stats;
}
}
module.exports = {
CacheDict,
TrytonCache,
};

409
src/client.js Normal file
View File

@@ -0,0 +1,409 @@
/**
* Main Tryton RPC Client
* JavaScript implementation of sabatron-tryton-rpc-client
*/
const { ServerProxy, ServerPool } = require("./jsonrpc");
/**
* Main client class for connecting to Tryton server via RPC
*/
class TrytonClient {
/**
* Create a new Tryton client
* @param {Object} config - Configuration object
* @param {string} config.hostname - Server hostname
* @param {string} config.database - Database name
* @param {string} config.username - Username
* @param {string} config.password - Password
* @param {number} [config.port=8000] - Server port
* @param {string} [config.language='en'] - Language code
* @param {Object} [config.options={}] - Additional options
*/
constructor({
hostname,
database,
username,
password,
port = 8000,
language = "en",
options = {},
}) {
// Extract protocol from hostname if present
if (hostname.startsWith("https://")) {
this.hostname = hostname.replace("https://", "");
this.useHttps = true;
} else if (hostname.startsWith("http://")) {
this.hostname = hostname.replace("http://", "");
this.useHttps = false;
} else {
this.hostname = hostname;
this.useHttps = port === 443 || port === 8443;
}
this.database = database;
this.username = username;
this.password = password;
this.port = port;
this.language = language;
this.options = options;
this.connection = null;
this.session = null;
}
/**
* Alternative constructor for backward compatibility
* @param {string} hostname - Server hostname
* @param {string} database - Database name
* @param {string} username - Username
* @param {string} password - Password
* @param {number} [port=8000] - Server port
* @param {string} [language='en'] - Language code
* @returns {TrytonClient} - New client instance
*/
static create(
hostname,
database,
username,
password,
port = 8000,
language = "en"
) {
return new TrytonClient({
hostname,
database,
username,
password,
port,
language,
});
}
/**
* Connect to Tryton server and authenticate
* @returns {Promise<boolean>} - True if connection successful
* @throws {Error} - If connection or authentication fails
*/
async connect() {
try {
// Create proxy for login
const proxy = new ServerProxy(
this.hostname,
this.port,
this.database,
{
verbose: this.options.verbose || false,
connectTimeout: this.options.connectTimeout,
timeout: this.options.timeout,
useHttps: this.useHttps,
}
);
// Perform login
const parameters = {
password: this.password,
};
const result = await proxy.request("common.db.login", [
this.username,
parameters,
this.language,
]);
// Close temporary proxy
proxy.close();
// Create session string
this.session = [this.username, ...result].join(":");
// Create connection pool with session
this.connection = new ServerPool(
this.hostname,
this.port,
this.database,
{
session: this.session,
cache: this.options.cache !== false ? [] : null, // Enable cache by default
verbose: this.options.verbose || false,
connectTimeout: this.options.connectTimeout,
timeout: this.options.timeout,
keepMax: this.options.keepMax || 4,
useHttps: this.useHttps,
}
);
return true;
} catch (error) {
throw new Error(`Connection failed: ${error.message}`);
}
}
/**
* Call RPC method on server
* @param {string} methodName - RPC method name (e.g., 'model.party.party.read')
* @param {Array} args - Method arguments
* @returns {Promise<*>} - Method result
* @throws {Error} - If not connected or method call fails
*/
async call(methodName, args = []) {
if (!this.connection) {
throw new Error("Not connected. Call connect() first.");
}
return this.connection.withConnection(async (conn) => {
return conn.request(methodName, args);
});
}
/**
* Call multiple RPC methods in sequence
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callMultiple(calls) {
const results = [];
for (const call of calls) {
const result = await this.call(call.method, call.args);
results.push(result);
}
return results;
}
/**
* Call multiple RPC methods in parallel
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callParallel(calls) {
const promises = calls.map((call) => this.call(call.method, call.args));
return Promise.all(promises);
}
/**
* Helper method to read records from a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<number>} ids - Record IDs to read
* @param {Array<string>} fields - Fields to read
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async read(model, ids, fields, context = {}) {
return this.call(`model.${model}.read`, [ids, fields, context]);
}
/**
* Helper method to create records in a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<Object>} records - Records to create
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of created record IDs
*/
async create(model, records, context = {}) {
return this.call(`model.${model}.create`, [records, context]);
}
/**
* Helper method to write/update records in a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<number>} ids - Record IDs to update
* @param {Object} values - Values to update
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async write(model, ids, values, context = {}) {
return this.call(`model.${model}.write`, [ids, values, context]);
}
/**
* Helper method to delete records from a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<number>} ids - Record IDs to delete
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async delete(model, ids, context = {}) {
return this.call(`model.${model}.delete`, [ids, context]);
}
/**
* Helper method to search for records
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array} domain - Search domain
* @param {number} [offset=0] - Offset for pagination
* @param {number} [limit=null] - Limit for pagination
* @param {Array<string>} [order=null] - Order specification
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of record IDs
*/
async search(
model,
domain,
offset = 0,
limit = null,
order = null,
context = {}
) {
return this.call(`model.${model}.search`, [
domain,
offset,
limit,
order,
context,
]);
}
/**
* Helper method to search and read records in one call
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array} domain - Search domain
* @param {Array<string>} fields - Fields to read
* @param {number} [offset=0] - Offset for pagination
* @param {number} [limit=null] - Limit for pagination
* @param {Array<string>} [order=null] - Order specification
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async searchRead(
model,
domain,
fields,
offset = 0,
limit = null,
order = null,
context = {}
) {
return this.call(`model.${model}.search_read`, [
domain,
offset,
limit,
order,
fields,
context,
]);
}
/**
* Helper method to count records
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array} domain - Search domain
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<number>} - Number of records
*/
async searchCount(model, domain, context = {}) {
return this.call(`model.${model}.search_count`, [domain, context]);
}
/**
* Get database information
* @returns {Promise<Object>} - Database info
*/
async getDatabaseInfo() {
return this.call("common.db.get_info", []);
}
/**
* List available databases
* @returns {Promise<Array<string>>} - Database names
*/
async listDatabases() {
return this.call("common.db.list", []);
}
/**
* Get server version
* @returns {Promise<string>} - Server version
*/
async getVersion() {
return this.call("common.version", []);
}
/**
* Clear cache for specific prefix or all
* @param {string} [prefix] - Optional prefix to clear
*/
clearCache(prefix = null) {
if (this.connection) {
this.connection.clearCache(prefix);
}
}
/**
* Get connection SSL status
* @returns {boolean|null} - SSL status or null if not connected
*/
get ssl() {
return this.connection ? this.connection.ssl : null;
}
/**
* Get connection URL
* @returns {string|null} - Connection URL or null if not connected
*/
get url() {
return this.connection ? this.connection.url : null;
}
/**
* Check if client is connected
* @returns {boolean} - True if connected
*/
get isConnected() {
return this.connection !== null;
}
/**
* Get current session
* @returns {string|null} - Session string or null if not connected
*/
getSession() {
return this.session;
}
/**
* Close connection and cleanup resources
*/
close() {
if (this.connection) {
this.connection.close();
this.connection = null;
this.session = null;
}
}
/**
* Create a new client instance with the same configuration
* @returns {TrytonClient} - New client instance
*/
clone() {
return new TrytonClient({
hostname: this.hostname,
database: this.database,
username: this.username,
password: this.password,
port: this.port,
language: this.language,
options: { ...this.options },
});
}
/**
* Get client configuration (without sensitive data)
* @returns {Object} - Client configuration
*/
getConfig() {
return {
hostname: this.hostname,
database: this.database,
username: this.username,
port: this.port,
language: this.language,
isConnected: this.isConnected,
ssl: this.ssl,
url: this.url,
};
}
}
module.exports = {
TrytonClient,
};

37
src/index.js Normal file
View File

@@ -0,0 +1,37 @@
/**
* Main module exports
* Entry point for the Tryton RPC Client package
*/
const { TrytonClient } = require("./client");
const {
ServerProxy,
ServerPool,
ResponseError,
Fault,
ProtocolError,
TrytonJSONEncoder,
} = require("./jsonrpc");
const { CacheDict, TrytonCache } = require("./cache");
module.exports = {
// Main client class
TrytonClient,
// Low-level RPC classes
ServerProxy,
ServerPool,
// Error classes
ResponseError,
Fault,
ProtocolError,
// Utility classes
TrytonJSONEncoder,
CacheDict,
TrytonCache,
// Convenience export for backward compatibility
Client: TrytonClient,
};

577
src/jsonrpc.js Normal file
View File

@@ -0,0 +1,577 @@
/**
* JSON-RPC implementation for Tryton server communication
* Based on the Python implementation from sabatron-tryton-rpc-client
*/
const https = require("https");
const http = require("http");
const zlib = require("zlib");
const { URL } = require("url");
const { TrytonCache } = require("./cache");
// Constants
const CONNECT_TIMEOUT = 5000; // 5 seconds
const DEFAULT_TIMEOUT = 30000; // 30 seconds
/**
* Custom error classes
*/
class ResponseError extends Error {
constructor(message) {
super(message);
this.name = "ResponseError";
}
}
class Fault extends Error {
constructor(faultCode, faultString = "", extra = {}) {
super(faultString);
this.name = "Fault";
this.faultCode = faultCode;
this.faultString = faultString;
Object.assign(this, extra);
}
toString() {
return String(this.faultCode);
}
}
class ProtocolError extends Error {
constructor(message, errcode = null, errmsg = null) {
super(message);
this.name = "ProtocolError";
this.errcode = errcode;
this.errmsg = errmsg;
}
}
/**
* JSON encoder/decoder for Tryton specific types
*/
class TrytonJSONEncoder {
/**
* Serialize JavaScript objects to JSON with Tryton type handling
* @param {*} obj - Object to serialize
* @returns {string} - JSON string
*/
static serialize(obj) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof Date) {
return {
__class__: "datetime",
year: value.getFullYear(),
month: value.getMonth() + 1,
day: value.getDate(),
hour: value.getHours(),
minute: value.getMinutes(),
second: value.getSeconds(),
microsecond: value.getMilliseconds() * 1000,
};
}
if (value instanceof Buffer) {
return {
__class__: "bytes",
base64: value.toString("base64"),
};
}
// Handle BigInt as Decimal
if (typeof value === "bigint") {
return {
__class__: "Decimal",
decimal: value.toString(),
};
}
return value;
});
}
/**
* Deserialize JSON with Tryton type handling
* @param {string} str - JSON string
* @returns {*} - Parsed object
*/
static deserialize(str) {
return JSON.parse(str, (key, value) => {
if (value && typeof value === "object" && value.__class__) {
switch (value.__class__) {
case "datetime":
return new Date(
value.year,
value.month - 1,
value.day,
value.hour || 0,
value.minute || 0,
value.second || 0,
Math.floor((value.microsecond || 0) / 1000)
);
case "date":
return new Date(value.year, value.month - 1, value.day);
case "time":
const today = new Date();
return new Date(
today.getFullYear(),
today.getMonth(),
today.getDate(),
value.hour || 0,
value.minute || 0,
value.second || 0,
Math.floor((value.microsecond || 0) / 1000)
);
case "timedelta":
// Return seconds as number
return value.seconds || 0;
case "bytes":
return Buffer.from(value.base64, "base64");
case "Decimal":
// Convert to number or keep as string for precision
return parseFloat(value.decimal);
default:
return value;
}
}
return value;
});
}
}
/**
* HTTP Transport for JSON-RPC requests
*/
class Transport {
constructor(options = {}) {
this.fingerprints = options.fingerprints || null;
this.caCerts = options.caCerts || null;
this.session = options.session || null;
this.connection = null;
this.connectTimeout = options.connectTimeout || CONNECT_TIMEOUT;
this.timeout = options.timeout || DEFAULT_TIMEOUT;
this.useHttps = options.useHttps || false;
}
/**
* Make HTTP request to server
* @param {string} host - Server host
* @param {string} handler - URL path
* @param {string} requestData - JSON request data
* @param {boolean} verbose - Enable verbose logging
* @returns {Promise<Object>} - Response object
*/
async request(host, handler, requestData, verbose = false) {
// Detect protocol based on port or explicit protocol
const hostParts = host.split(":");
const port = hostParts[1] ? parseInt(hostParts[1]) : 80;
const hostname = hostParts[0];
// Use HTTPS if explicitly configured, or for standard HTTPS ports
const shouldUseHttps =
this.useHttps ||
port === 443 ||
port === 8443 ||
host.startsWith("https://");
const protocol = shouldUseHttps ? "https" : "http";
const url = new URL(`${protocol}://${host}${handler}`);
const isHttps = url.protocol === "https:";
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(requestData),
Connection: "keep-alive",
"Accept-Encoding": "gzip, deflate",
},
timeout: this.connectTimeout,
// Allow self-signed certificates for testing
rejectUnauthorized: false,
};
// Add session authentication
if (this.session) {
const auth = Buffer.from(this.session).toString("base64");
options.headers["Authorization"] = `Session ${auth}`;
}
return new Promise((resolve, reject) => {
const client = isHttps ? https : http;
const req = client.request(options, (res) => {
let data = Buffer.alloc(0);
res.on("data", (chunk) => {
data = Buffer.concat([data, chunk]);
});
res.on("end", () => {
try {
// Handle compression
const encoding = res.headers["content-encoding"];
let responseText;
if (encoding === "gzip") {
responseText = zlib
.gunzipSync(data)
.toString("utf-8");
} else if (encoding === "deflate") {
responseText = zlib
.inflateSync(data)
.toString("utf-8");
} else {
responseText = data.toString("utf-8");
}
if (verbose) {
console.log("Response:", responseText);
}
const response =
TrytonJSONEncoder.deserialize(responseText);
// Add cache header if present
const cacheHeader = res.headers["x-tryton-cache"];
if (cacheHeader) {
try {
response.cache = parseInt(cacheHeader);
} catch (e) {
// Ignore invalid cache header
}
}
resolve(response);
} catch (error) {
reject(
new ResponseError(
`Failed to parse response: ${error.message}`
)
);
}
});
});
req.on("error", (error) => {
if (error.code === "ECONNRESET" || error.code === "EPIPE") {
// Retry once on connection reset
reject(
new ProtocolError(
"Connection reset",
error.code,
error.message
)
);
} else {
reject(error);
}
});
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
req.setTimeout(this.timeout);
req.write(requestData);
req.end();
});
}
/**
* Close transport connection
*/
close() {
if (this.connection) {
this.connection.destroy();
this.connection = null;
}
}
}
/**
* Server proxy for making RPC calls
*/
class ServerProxy {
constructor(host, port, database = "", options = {}) {
this.host = host;
this.port = port;
this.database = database;
this.verbose = options.verbose || false;
this.handler = database ? `/${encodeURIComponent(database)}/` : "/";
this.hostUrl = `${host}:${port}`;
this.requestId = 0;
this.cache = options.cache || null;
this.useHttps = options.useHttps || false;
this.transport = new Transport({
fingerprints: options.fingerprints,
caCerts: options.caCerts,
session: options.session,
connectTimeout: options.connectTimeout,
timeout: options.timeout,
useHttps: this.useHttps,
});
}
/**
* Make RPC request with retry logic
* @param {string} methodName - RPC method name
* @param {Array} params - Method parameters
* @returns {Promise<*>} - Method result
*/
async request(methodName, params) {
this.requestId += 1;
const id = this.requestId;
const requestData = TrytonJSONEncoder.serialize({
id: id,
method: methodName,
params: params,
});
// Check cache first
if (this.cache && this.cache.cached(methodName)) {
try {
return this.cache.get(methodName, requestData);
} catch (error) {
// Cache miss or expired, continue with request
}
}
let lastError = null;
// Retry logic (up to 5 attempts)
for (let attempt = 0; attempt < 5; attempt++) {
try {
const response = await this.transport.request(
this.hostUrl,
this.handler,
requestData,
this.verbose
);
// Validate response
if (response.id !== id) {
throw new ResponseError(
`Invalid response id (${response.id}) expected ${id}`
);
}
// Handle RPC errors
if (response.error) {
if (this.verbose) {
console.error("RPC Error:", response);
}
throw new Fault(response.error[0], response.error[1] || "");
}
// Cache successful response
if (this.cache && response.cache) {
this.cache.set(
methodName,
requestData,
response.cache,
response.result
);
}
return response.result;
} catch (error) {
lastError = error;
// Check if we should retry
if (error instanceof ProtocolError && error.errcode === 503) {
// Service unavailable, wait and retry
const delay = Math.min(attempt + 1, 10) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
// For connection errors, try once more
if (
attempt === 0 &&
(error.code === "ECONNRESET" ||
error.code === "EPIPE" ||
error instanceof ProtocolError)
) {
this.transport.close();
continue;
}
// Don't retry other errors
break;
}
}
throw lastError;
}
/**
* Close server proxy
*/
close() {
this.transport.close();
}
/**
* Get SSL status
* @returns {boolean} - Whether connection uses SSL
*/
get ssl() {
return this.port === 443 || this.hostUrl.startsWith("https");
}
/**
* Get full URL
* @returns {string} - Full server URL
*/
get url() {
const scheme = this.ssl ? "https" : "http";
return `${scheme}://${this.hostUrl}${this.handler}`;
}
}
/**
* Connection pool for reusing ServerProxy instances
*/
class ServerPool {
constructor(host, port, database, options = {}) {
this.host = host;
this.port = port;
this.database = database;
this.options = options;
this.keepMax = options.keepMax || 4;
this.session = options.session || null;
this.pool = [];
this.used = new Set();
this.cache = null;
// Initialize cache if requested
if (options.cache) {
this.cache = new TrytonCache();
this.options.cache = this.cache;
}
}
/**
* Get connection from pool or create new one
* @returns {ServerProxy} - Server proxy instance
*/
getConnection() {
let conn;
if (this.pool.length > 0) {
conn = this.pool.pop();
} else {
conn = new ServerProxy(this.host, this.port, this.database, {
...this.options,
cache: this.cache,
});
}
this.used.add(conn);
return conn;
}
/**
* Return connection to pool
* @param {ServerProxy} conn - Connection to return
*/
putConnection(conn) {
this.used.delete(conn);
this.pool.push(conn);
// Remove excess connections
while (this.pool.length > this.keepMax) {
const oldConn = this.pool.shift();
oldConn.close();
}
}
/**
* Execute callback with a pooled connection
* @param {Function} callback - Async function to execute
* @returns {Promise<*>} - Callback result
*/
async withConnection(callback) {
const conn = this.getConnection();
try {
return await callback(conn);
} finally {
this.putConnection(conn);
}
}
/**
* Close all connections in pool
*/
close() {
// Close all pooled connections
for (const conn of this.pool) {
conn.close();
}
// Close all used connections
for (const conn of this.used) {
conn.close();
}
this.pool = [];
this.used.clear();
}
/**
* Clear cache
* @param {string} [prefix] - Optional prefix to clear
*/
clearCache(prefix = null) {
if (this.cache) {
this.cache.clear(prefix);
}
}
/**
* Get SSL status from any connection
* @returns {boolean|null} - SSL status or null if no connections
*/
get ssl() {
const allConns = [...this.pool, ...this.used];
if (allConns.length > 0) {
return allConns[0].ssl;
}
return null;
}
/**
* Get URL from any connection
* @returns {string|null} - URL or null if no connections
*/
get url() {
const allConns = [...this.pool, ...this.used];
if (allConns.length > 0) {
return allConns[0].url;
}
return null;
}
}
module.exports = {
ResponseError,
Fault,
ProtocolError,
TrytonJSONEncoder,
Transport,
ServerProxy,
ServerPool,
};