Fix: Modified files so the client works with TS fine
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
/**
|
||||
* Cache system similar to Python's CacheDict from Tryton
|
||||
* Implements LRU (Least Recently Used) cache using JavaScript Map
|
||||
* TypeScript version
|
||||
*/
|
||||
|
||||
class CacheDict extends Map {
|
||||
export type CacheFactory<T> = () => T;
|
||||
|
||||
export class CacheDict<K = any, V = any> extends Map<K, V> {
|
||||
private cacheLen: number;
|
||||
private defaultFactory: CacheFactory<V> | null;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
constructor(
|
||||
cacheLen: number = 10,
|
||||
defaultFactory: CacheFactory<V> | null = null
|
||||
) {
|
||||
super();
|
||||
this.cacheLen = cacheLen;
|
||||
this.defaultFactory = defaultFactory;
|
||||
@@ -17,11 +24,8 @@ class CacheDict extends Map {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
override set(key: K, value: V): this {
|
||||
// If key exists, delete it first to move to end
|
||||
if (this.has(key)) {
|
||||
this.delete(key);
|
||||
@@ -32,7 +36,9 @@ class CacheDict extends Map {
|
||||
// Remove oldest entries if cache is full
|
||||
while (this.size > this.cacheLen) {
|
||||
const firstKey = this.keys().next().value;
|
||||
this.delete(firstKey);
|
||||
if (firstKey !== undefined) {
|
||||
this.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -40,12 +46,10 @@ class CacheDict extends Map {
|
||||
|
||||
/**
|
||||
* Get a value and move it to end (most recently used)
|
||||
* @param {*} key - The key to retrieve
|
||||
* @returns {*} - The value
|
||||
*/
|
||||
get(key) {
|
||||
override get(key: K): V | undefined {
|
||||
if (this.has(key)) {
|
||||
const value = super.get(key);
|
||||
const value = super.get(key)!;
|
||||
// Move to end by re-setting
|
||||
this.delete(key);
|
||||
super.set(key, value);
|
||||
@@ -64,14 +68,12 @@ class CacheDict extends Map {
|
||||
|
||||
/**
|
||||
* Override has() to update LRU order on access
|
||||
* @param {*} key - The key to check
|
||||
* @returns {boolean} - Whether the key exists
|
||||
*/
|
||||
has(key) {
|
||||
override has(key: K): boolean {
|
||||
const exists = super.has(key);
|
||||
if (exists) {
|
||||
// Move to end on access
|
||||
const value = super.get(key);
|
||||
const value = super.get(key)!;
|
||||
this.delete(key);
|
||||
super.set(key, value);
|
||||
}
|
||||
@@ -80,54 +82,64 @@ class CacheDict extends Map {
|
||||
|
||||
/**
|
||||
* Get current cache size
|
||||
* @returns {number} - Number of items in cache
|
||||
*/
|
||||
get length() {
|
||||
get length(): number {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all items from cache
|
||||
*/
|
||||
clear() {
|
||||
override clear(): void {
|
||||
super.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert cache to array for debugging
|
||||
* @returns {Array} - Array of [key, value] pairs
|
||||
*/
|
||||
toArray() {
|
||||
toArray(): Array<[K, V]> {
|
||||
return Array.from(this.entries());
|
||||
}
|
||||
}
|
||||
|
||||
export interface CacheEntry<T = any> {
|
||||
expire: Date;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
totalPrefixes: number;
|
||||
totalEntries: number;
|
||||
prefixes: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced cache for Tryton RPC with expiration support
|
||||
*/
|
||||
class TrytonCache {
|
||||
constructor(cacheLen = 1024) {
|
||||
this.store = new CacheDict(cacheLen, () => new CacheDict(cacheLen));
|
||||
export class TrytonCache {
|
||||
private store: CacheDict<string, CacheDict<string, CacheEntry>>;
|
||||
private cacheLen: number;
|
||||
|
||||
constructor(cacheLen: number = 1024) {
|
||||
this.cacheLen = cacheLen;
|
||||
this.store = new CacheDict<string, CacheDict<string, CacheEntry>>(
|
||||
cacheLen,
|
||||
() => new CacheDict<string, CacheEntry>(cacheLen)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a prefix is cached
|
||||
* @param {string} prefix - Method prefix
|
||||
* @returns {boolean} - Whether prefix exists
|
||||
*/
|
||||
cached(prefix) {
|
||||
cached(prefix: string): boolean {
|
||||
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;
|
||||
set(prefix: string, key: string, expire: number | Date, value: any): void {
|
||||
let expiration: Date;
|
||||
|
||||
if (typeof expire === "number") {
|
||||
// Assume seconds, convert to Date
|
||||
@@ -141,7 +153,7 @@ class TrytonCache {
|
||||
// Deep copy value to avoid mutations
|
||||
const cachedValue = this._deepCopy(value);
|
||||
|
||||
this.store.get(prefix).set(key, {
|
||||
this.store.get(prefix)!.set(key, {
|
||||
expire: expiration,
|
||||
value: cachedValue,
|
||||
});
|
||||
@@ -149,24 +161,20 @@ class TrytonCache {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
get(prefix: string, key: string): any {
|
||||
const now = new Date();
|
||||
|
||||
if (!this.store.has(prefix)) {
|
||||
throw new Error("Key not found");
|
||||
}
|
||||
|
||||
const prefixCache = this.store.get(prefix);
|
||||
const prefixCache = this.store.get(prefix)!;
|
||||
if (!prefixCache.has(key)) {
|
||||
throw new Error("Key not found");
|
||||
}
|
||||
|
||||
const entry = prefixCache.get(key);
|
||||
const entry = prefixCache.get(key)!;
|
||||
|
||||
if (entry.expire < now) {
|
||||
prefixCache.delete(key);
|
||||
@@ -179,12 +187,11 @@ class TrytonCache {
|
||||
|
||||
/**
|
||||
* Clear cache for a specific prefix or all
|
||||
* @param {string} [prefix] - Optional prefix to clear
|
||||
*/
|
||||
clear(prefix = null) {
|
||||
clear(prefix?: string): void {
|
||||
if (prefix) {
|
||||
if (this.store.has(prefix)) {
|
||||
this.store.get(prefix).clear();
|
||||
this.store.get(prefix)!.clear();
|
||||
}
|
||||
} else {
|
||||
this.store.clear();
|
||||
@@ -193,35 +200,33 @@ class TrytonCache {
|
||||
|
||||
/**
|
||||
* Deep copy objects to prevent mutations
|
||||
* @param {*} obj - Object to copy
|
||||
* @returns {*} - Deep copied object
|
||||
* @private
|
||||
*/
|
||||
_deepCopy(obj) {
|
||||
private _deepCopy<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime());
|
||||
return new Date(obj.getTime()) as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map((item) => this._deepCopy(item));
|
||||
return obj.map((item) => this._deepCopy(item)) as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Buffer) {
|
||||
return Buffer.from(obj);
|
||||
// Handle Buffer in Node.js environment
|
||||
if (typeof Buffer !== "undefined" && obj instanceof Buffer) {
|
||||
return Buffer.from(obj) as T;
|
||||
}
|
||||
|
||||
if (typeof obj === "object") {
|
||||
const copy = {};
|
||||
const copy: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
copy[key] = this._deepCopy(obj[key]);
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
copy[key] = this._deepCopy((obj as any)[key]);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
return copy as T;
|
||||
}
|
||||
|
||||
return obj;
|
||||
@@ -229,10 +234,9 @@ class TrytonCache {
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* @returns {Object} - Cache statistics
|
||||
*/
|
||||
getStats() {
|
||||
const stats = {
|
||||
getStats(): CacheStats {
|
||||
const stats: CacheStats = {
|
||||
totalPrefixes: this.store.size,
|
||||
totalEntries: 0,
|
||||
prefixes: {},
|
||||
@@ -246,9 +250,48 @@ class TrytonCache {
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CacheDict,
|
||||
TrytonCache,
|
||||
};
|
||||
/**
|
||||
* Remove expired entries
|
||||
*/
|
||||
cleanupExpired(): number {
|
||||
const now = new Date();
|
||||
let removedCount = 0;
|
||||
|
||||
for (const [prefix, prefixCache] of this.store.entries()) {
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (const [key, entry] of prefixCache.entries()) {
|
||||
if (entry.expire < now) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach((key) => {
|
||||
prefixCache.delete(key);
|
||||
removedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache size in bytes (approximate)
|
||||
*/
|
||||
getSizeEstimate(): number {
|
||||
let size = 0;
|
||||
|
||||
for (const [prefix, prefixCache] of this.store.entries()) {
|
||||
size += prefix.length * 2; // UTF-16 encoding
|
||||
|
||||
for (const [key, entry] of prefixCache.entries()) {
|
||||
size += key.length * 2;
|
||||
size += JSON.stringify(entry.value).length * 2;
|
||||
size += 24; // Date object overhead
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,62 @@
|
||||
/**
|
||||
* Main Tryton RPC Client
|
||||
* JavaScript implementation of sabatron-tryton-rpc-client
|
||||
* Tryton RPC Client for Node.js
|
||||
* TypeScript implementation of sabatron-tryton-rpc-client
|
||||
*/
|
||||
|
||||
const { ServerProxy, ServerPool } = require("./jsonrpc");
|
||||
import { ServerProxy, ServerPool } from "./jsonrpc";
|
||||
|
||||
import { ServerProxyOptions, ServerPoolOptions } from "../types";
|
||||
|
||||
// Constants
|
||||
const CONNECT_TIMEOUT = 5000; // 5 seconds
|
||||
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
||||
import {
|
||||
TrytonClientConfig,
|
||||
TrytonClientOptions,
|
||||
SearchDomain,
|
||||
TrytonContext,
|
||||
TrytonRecord,
|
||||
TrytonUser,
|
||||
UserPreferences,
|
||||
LoginResult,
|
||||
RecordId,
|
||||
RecordIds,
|
||||
FieldName,
|
||||
ModelName,
|
||||
ClientConfig,
|
||||
TrytonMethodCall,
|
||||
TypedModelOperations,
|
||||
} from "../types";
|
||||
|
||||
/**
|
||||
* Main client class for connecting to Tryton server via RPC
|
||||
*/
|
||||
class TrytonClient {
|
||||
export class TrytonClient {
|
||||
private hostname: string;
|
||||
private database: string;
|
||||
private username: string;
|
||||
private password: string;
|
||||
private port: number;
|
||||
private language: string;
|
||||
private options: TrytonClientOptions;
|
||||
private useHttps: boolean;
|
||||
private connection: ServerPool | null;
|
||||
private session: string | null;
|
||||
|
||||
/**
|
||||
* 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 = {},
|
||||
}) {
|
||||
constructor(config: TrytonClientConfig) {
|
||||
const {
|
||||
hostname,
|
||||
database,
|
||||
username,
|
||||
password,
|
||||
port = 8000,
|
||||
language = "en",
|
||||
options = {},
|
||||
} = config;
|
||||
|
||||
// Extract protocol from hostname if present
|
||||
if (hostname.startsWith("https://")) {
|
||||
this.hostname = hostname.replace("https://", "");
|
||||
@@ -53,22 +81,15 @@ class TrytonClient {
|
||||
|
||||
/**
|
||||
* 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"
|
||||
) {
|
||||
hostname: string,
|
||||
database: string,
|
||||
username: string,
|
||||
password: string,
|
||||
port: number = 8000,
|
||||
language: string = "en"
|
||||
): TrytonClient {
|
||||
return new TrytonClient({
|
||||
hostname,
|
||||
database,
|
||||
@@ -84,17 +105,17 @@ class TrytonClient {
|
||||
* @returns {Promise<boolean>} - True if connection successful
|
||||
* @throws {Error} - If connection or authentication fails
|
||||
*/
|
||||
async connect() {
|
||||
async connect(): Promise<boolean> {
|
||||
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,
|
||||
connectTimeout:
|
||||
this.options.connectTimeout || CONNECT_TIMEOUT,
|
||||
timeout: this.options.timeout || DEFAULT_TIMEOUT,
|
||||
useHttps: this.useHttps,
|
||||
}
|
||||
);
|
||||
@@ -104,16 +125,14 @@ class TrytonClient {
|
||||
password: this.password,
|
||||
};
|
||||
|
||||
const result = await proxy.request("common.db.login", [
|
||||
const result = await proxy.request<LoginResult>("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
|
||||
@@ -125,8 +144,9 @@ class TrytonClient {
|
||||
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,
|
||||
connectTimeout:
|
||||
this.options.connectTimeout || CONNECT_TIMEOUT,
|
||||
timeout: this.options.timeout || DEFAULT_TIMEOUT,
|
||||
keepMax: this.options.keepMax || 4,
|
||||
useHttps: this.useHttps,
|
||||
}
|
||||
@@ -134,7 +154,7 @@ class TrytonClient {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`Connection failed: ${error.message}`);
|
||||
throw new Error(`Connection failed: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,13 +165,13 @@ class TrytonClient {
|
||||
* @returns {Promise<*>} - Method result
|
||||
* @throws {Error} - If not connected or method call fails
|
||||
*/
|
||||
async call(methodName, args = []) {
|
||||
async call<T = any>(methodName: string, args: any[] = []): Promise<T> {
|
||||
if (!this.connection) {
|
||||
throw new Error("Not connected. Call connect() first.");
|
||||
}
|
||||
|
||||
return this.connection.withConnection(async (conn) => {
|
||||
return conn.request(methodName, args);
|
||||
return conn.request<T>(methodName, args);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,8 +180,8 @@ class TrytonClient {
|
||||
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
||||
* @returns {Promise<Array>} - Array of results
|
||||
*/
|
||||
async callMultiple(calls) {
|
||||
const results = [];
|
||||
async callMultiple(calls: TrytonMethodCall[]): Promise<any[]> {
|
||||
const results: any[] = [];
|
||||
for (const call of calls) {
|
||||
const result = await this.call(call.method, call.args);
|
||||
results.push(result);
|
||||
@@ -174,7 +194,7 @@ class TrytonClient {
|
||||
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
||||
* @returns {Promise<Array>} - Array of results
|
||||
*/
|
||||
async callParallel(calls) {
|
||||
async callParallel(calls: TrytonMethodCall[]): Promise<any[]> {
|
||||
const promises = calls.map((call) => this.call(call.method, call.args));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
@@ -187,8 +207,13 @@ class TrytonClient {
|
||||
* @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]);
|
||||
async read<T extends TrytonRecord = TrytonRecord>(
|
||||
model: ModelName,
|
||||
ids: RecordIds,
|
||||
fields: FieldName[],
|
||||
context: TrytonContext = {}
|
||||
): Promise<T[]> {
|
||||
return this.call<T[]>(`model.${model}.read`, [ids, fields, context]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,8 +223,15 @@ class TrytonClient {
|
||||
* @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]);
|
||||
async create(
|
||||
model: ModelName,
|
||||
records: Record<string, any>[],
|
||||
context: TrytonContext = {}
|
||||
): Promise<RecordIds> {
|
||||
return this.call<RecordIds>(`model.${model}.create`, [
|
||||
records,
|
||||
context,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,8 +242,13 @@ class TrytonClient {
|
||||
* @param {Object} [context={}] - Context dictionary
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async write(model, ids, values, context = {}) {
|
||||
return this.call(`model.${model}.write`, [ids, values, context]);
|
||||
async write(
|
||||
model: ModelName,
|
||||
ids: RecordIds,
|
||||
values: Record<string, any>,
|
||||
context: TrytonContext = {}
|
||||
): Promise<void> {
|
||||
return this.call<void>(`model.${model}.write`, [ids, values, context]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,8 +258,12 @@ class TrytonClient {
|
||||
* @param {Object} [context={}] - Context dictionary
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async delete(model, ids, context = {}) {
|
||||
return this.call(`model.${model}.delete`, [ids, context]);
|
||||
async delete(
|
||||
model: ModelName,
|
||||
ids: RecordIds,
|
||||
context: TrytonContext = {}
|
||||
): Promise<void> {
|
||||
return this.call<void>(`model.${model}.delete`, [ids, context]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,14 +277,14 @@ class TrytonClient {
|
||||
* @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`, [
|
||||
model: ModelName,
|
||||
domain: SearchDomain,
|
||||
offset: number = 0,
|
||||
limit: number | null = null,
|
||||
order: string[] | null = null,
|
||||
context: TrytonContext = {}
|
||||
): Promise<RecordIds> {
|
||||
return this.call<RecordIds>(`model.${model}.search`, [
|
||||
domain,
|
||||
offset,
|
||||
limit,
|
||||
@@ -263,16 +304,16 @@ class TrytonClient {
|
||||
* @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`, [
|
||||
async searchRead<T extends TrytonRecord = TrytonRecord>(
|
||||
model: ModelName,
|
||||
domain: SearchDomain,
|
||||
fields: FieldName[],
|
||||
offset: number = 0,
|
||||
limit: number | null = null,
|
||||
order: string[] | null = null,
|
||||
context: TrytonContext = {}
|
||||
): Promise<T[]> {
|
||||
return this.call<T[]>(`model.${model}.search_read`, [
|
||||
domain,
|
||||
offset,
|
||||
limit,
|
||||
@@ -289,15 +330,22 @@ class TrytonClient {
|
||||
* @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]);
|
||||
async searchCount(
|
||||
model: ModelName,
|
||||
domain: SearchDomain,
|
||||
context: TrytonContext = {}
|
||||
): Promise<number> {
|
||||
return this.call<number>(`model.${model}.search_count`, [
|
||||
domain,
|
||||
context,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database information
|
||||
* @returns {Promise<Object>} - Database info
|
||||
*/
|
||||
async getDatabaseInfo() {
|
||||
async getDatabaseInfo(): Promise<any> {
|
||||
return this.call("common.db.get_info", []);
|
||||
}
|
||||
|
||||
@@ -305,23 +353,49 @@ class TrytonClient {
|
||||
* List available databases
|
||||
* @returns {Promise<Array<string>>} - Database names
|
||||
*/
|
||||
async listDatabases() {
|
||||
return this.call("common.db.list", []);
|
||||
async listDatabases(): Promise<string[]> {
|
||||
return this.call<string[]>("common.db.list", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server version
|
||||
* @returns {Promise<string>} - Server version
|
||||
*/
|
||||
async getVersion() {
|
||||
return this.call("common.version", []);
|
||||
async getVersion(): Promise<string> {
|
||||
return this.call<string>("common.version", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user preferences
|
||||
*/
|
||||
async getUserPreferences(): Promise<UserPreferences> {
|
||||
return this.call<UserPreferences>("model.res.user.get_preferences", [
|
||||
true,
|
||||
{},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user information
|
||||
*/
|
||||
async getCurrentUser(): Promise<TrytonUser> {
|
||||
const preferences = await this.getUserPreferences();
|
||||
const users = await this.read<TrytonUser>(
|
||||
"res.user",
|
||||
[preferences.id],
|
||||
["id", "name", "login", "language", "company", "email"]
|
||||
);
|
||||
if (!users || users.length === 0) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return users[0]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for specific prefix or all
|
||||
* @param {string} [prefix] - Optional prefix to clear
|
||||
*/
|
||||
clearCache(prefix = null) {
|
||||
clearCache(prefix?: string): void {
|
||||
if (this.connection) {
|
||||
this.connection.clearCache(prefix);
|
||||
}
|
||||
@@ -331,7 +405,7 @@ class TrytonClient {
|
||||
* Get connection SSL status
|
||||
* @returns {boolean|null} - SSL status or null if not connected
|
||||
*/
|
||||
get ssl() {
|
||||
get ssl(): boolean | null {
|
||||
return this.connection ? this.connection.ssl : null;
|
||||
}
|
||||
|
||||
@@ -339,7 +413,7 @@ class TrytonClient {
|
||||
* Get connection URL
|
||||
* @returns {string|null} - Connection URL or null if not connected
|
||||
*/
|
||||
get url() {
|
||||
get url(): string | null {
|
||||
return this.connection ? this.connection.url : null;
|
||||
}
|
||||
|
||||
@@ -347,7 +421,7 @@ class TrytonClient {
|
||||
* Check if client is connected
|
||||
* @returns {boolean} - True if connected
|
||||
*/
|
||||
get isConnected() {
|
||||
get isConnected(): boolean {
|
||||
return this.connection !== null;
|
||||
}
|
||||
|
||||
@@ -355,14 +429,39 @@ class TrytonClient {
|
||||
* Get current session
|
||||
* @returns {string|null} - Session string or null if not connected
|
||||
*/
|
||||
getSession() {
|
||||
getSession(): string | null {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user ID from session
|
||||
*/
|
||||
getUserId(): number | null {
|
||||
if (!this.session) return null;
|
||||
const parts = this.session.split(":");
|
||||
return parts.length >= 2 ? parseInt(parts[1], 10) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session token from session
|
||||
*/
|
||||
getSessionToken(): string | null {
|
||||
if (!this.session) return null;
|
||||
const parts = this.session.split(":");
|
||||
return parts.length >= 3 ? parts[2] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get username
|
||||
*/
|
||||
getUsername(): string {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connection and cleanup resources
|
||||
*/
|
||||
close() {
|
||||
close(): void {
|
||||
if (this.connection) {
|
||||
this.connection.close();
|
||||
this.connection = null;
|
||||
@@ -374,7 +473,7 @@ class TrytonClient {
|
||||
* Create a new client instance with the same configuration
|
||||
* @returns {TrytonClient} - New client instance
|
||||
*/
|
||||
clone() {
|
||||
clone(): TrytonClient {
|
||||
return new TrytonClient({
|
||||
hostname: this.hostname,
|
||||
database: this.database,
|
||||
@@ -390,7 +489,7 @@ class TrytonClient {
|
||||
* Get client configuration (without sensitive data)
|
||||
* @returns {Object} - Client configuration
|
||||
*/
|
||||
getConfig() {
|
||||
getConfig(): ClientConfig {
|
||||
return {
|
||||
hostname: this.hostname,
|
||||
database: this.database,
|
||||
@@ -402,8 +501,77 @@ class TrytonClient {
|
||||
url: this.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe model operations factory
|
||||
* Creates a typed interface for specific models
|
||||
*/
|
||||
model<T extends TrytonRecord = TrytonRecord>(
|
||||
modelName: ModelName
|
||||
): TypedModelOperations<T> {
|
||||
return {
|
||||
read: (
|
||||
ids: RecordIds,
|
||||
fields: FieldName[],
|
||||
context?: TrytonContext
|
||||
) => this.read<T>(modelName, ids, fields, context),
|
||||
|
||||
create: (
|
||||
records: Partial<Omit<T, "id">>[],
|
||||
context?: TrytonContext
|
||||
) =>
|
||||
this.create(
|
||||
modelName,
|
||||
records as Record<string, any>[],
|
||||
context
|
||||
),
|
||||
|
||||
write: (
|
||||
ids: RecordIds,
|
||||
values: Partial<Omit<T, "id">>,
|
||||
context?: TrytonContext
|
||||
) =>
|
||||
this.write(
|
||||
modelName,
|
||||
ids,
|
||||
values as Record<string, any>,
|
||||
context
|
||||
),
|
||||
|
||||
delete: (ids: RecordIds, context?: TrytonContext) =>
|
||||
this.delete(modelName, ids, context),
|
||||
|
||||
search: (
|
||||
domain: SearchDomain,
|
||||
offset?: number,
|
||||
limit?: number,
|
||||
order?: string[],
|
||||
context?: TrytonContext
|
||||
) => this.search(modelName, domain, offset, limit, order, context),
|
||||
|
||||
searchRead: (
|
||||
domain: SearchDomain,
|
||||
fields: FieldName[],
|
||||
offset?: number,
|
||||
limit?: number,
|
||||
order?: string[],
|
||||
context?: TrytonContext
|
||||
) =>
|
||||
this.searchRead<T>(
|
||||
modelName,
|
||||
domain,
|
||||
fields,
|
||||
offset,
|
||||
limit,
|
||||
order,
|
||||
context
|
||||
),
|
||||
|
||||
searchCount: (domain: SearchDomain, context?: TrytonContext) =>
|
||||
this.searchCount(modelName, domain, context),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TrytonClient,
|
||||
};
|
||||
// Export for CommonJS compatibility
|
||||
export default TrytonClient;
|
||||
37
src/index.js
37
src/index.js
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
170
src/index.ts
Normal file
170
src/index.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Tryton RPC Client for Node.js - TypeScript Entry Point
|
||||
* Main exports for the Tryton RPC Client library
|
||||
*/
|
||||
|
||||
// Import types and classes for internal use
|
||||
import { TrytonClient } from "./client";
|
||||
import type {
|
||||
TrytonClientConfig,
|
||||
TrytonClientOptions,
|
||||
TrytonRecord,
|
||||
TrytonUser,
|
||||
TrytonError,
|
||||
} from "../types";
|
||||
|
||||
// Main client class
|
||||
export { TrytonClient } from "./client";
|
||||
export { default as TrytonClientDefault } from "./client";
|
||||
|
||||
// RPC and transport classes (Node.js only)
|
||||
export {
|
||||
ServerProxy,
|
||||
ServerPool,
|
||||
Transport,
|
||||
TrytonJSONEncoder,
|
||||
ResponseError,
|
||||
Fault,
|
||||
ProtocolError,
|
||||
} from "./jsonrpc";
|
||||
|
||||
// Cache classes
|
||||
export {
|
||||
TrytonCache,
|
||||
CacheDict,
|
||||
type CacheEntry,
|
||||
type CacheStats,
|
||||
type CacheFactory,
|
||||
} from "./cache";
|
||||
|
||||
// All type definitions
|
||||
export type {
|
||||
// Configuration types
|
||||
TrytonClientConfig,
|
||||
TrytonClientOptions,
|
||||
ServerProxyOptions,
|
||||
ServerPoolOptions,
|
||||
|
||||
// Data types
|
||||
TrytonRecord,
|
||||
TrytonUser,
|
||||
TrytonCompany,
|
||||
TrytonParty,
|
||||
|
||||
// Search and domain types
|
||||
SearchDomain,
|
||||
DomainClause,
|
||||
DomainOperator,
|
||||
DomainLogicalOperator,
|
||||
|
||||
// RPC types
|
||||
TrytonMethodCall,
|
||||
TrytonBatchCall,
|
||||
|
||||
// Authentication types
|
||||
LoginParameters,
|
||||
LoginResult,
|
||||
UserPreferences,
|
||||
|
||||
// Database types
|
||||
DatabaseInfo,
|
||||
|
||||
// Error types
|
||||
RpcError,
|
||||
|
||||
// Context types
|
||||
TrytonContext,
|
||||
|
||||
// CRUD operation types
|
||||
CreateOptions,
|
||||
ReadOptions,
|
||||
WriteOptions,
|
||||
DeleteOptions,
|
||||
SearchOptions,
|
||||
SearchReadOptions,
|
||||
|
||||
// Client state types
|
||||
ClientConfig,
|
||||
|
||||
// Utility types
|
||||
CacheEntry as CacheEntryType,
|
||||
ModelName,
|
||||
FieldName,
|
||||
RecordId,
|
||||
RecordIds,
|
||||
TypedModelOperations,
|
||||
} from "../types";
|
||||
|
||||
// Re-export error class for convenience
|
||||
export { TrytonError } from "../types";
|
||||
|
||||
// Version information
|
||||
export const VERSION = "1.1.0";
|
||||
|
||||
// Default export is the main client
|
||||
export default TrytonClient;
|
||||
|
||||
// Convenience factory functions
|
||||
export function createClient(config: TrytonClientConfig): TrytonClient {
|
||||
return new TrytonClient(config);
|
||||
}
|
||||
|
||||
export function createTrytonClient(
|
||||
hostname: string,
|
||||
database: string,
|
||||
username: string,
|
||||
password: string,
|
||||
port?: number,
|
||||
language?: string
|
||||
): TrytonClient {
|
||||
return TrytonClient.create(
|
||||
hostname,
|
||||
database,
|
||||
username,
|
||||
password,
|
||||
port,
|
||||
language
|
||||
);
|
||||
}
|
||||
|
||||
// Type guard functions
|
||||
export function isTrytonRecord(obj: any): obj is TrytonRecord {
|
||||
return obj && typeof obj === "object" && typeof obj.id === "number";
|
||||
}
|
||||
|
||||
export function isTrytonUser(obj: any): obj is TrytonUser {
|
||||
return (
|
||||
isTrytonRecord(obj) &&
|
||||
typeof obj.login === "string" &&
|
||||
typeof obj.name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isTrytonError(error: any): error is TrytonError {
|
||||
return error instanceof Error && error.name === "TrytonError";
|
||||
}
|
||||
|
||||
// Helper functions for React Context
|
||||
export interface ReactTrytonContextConfig {
|
||||
hostname?: string;
|
||||
database?: string;
|
||||
port?: number;
|
||||
language?: string;
|
||||
options?: TrytonClientOptions;
|
||||
}
|
||||
|
||||
export function createReactTrytonConfig(
|
||||
baseConfig: ReactTrytonContextConfig,
|
||||
username: string,
|
||||
password: string
|
||||
): TrytonClientConfig {
|
||||
return {
|
||||
hostname: baseConfig.hostname || "localhost",
|
||||
database: baseConfig.database || "tryton",
|
||||
username,
|
||||
password,
|
||||
port: baseConfig.port || 8000,
|
||||
language: baseConfig.language || "en",
|
||||
options: baseConfig.options || {},
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* JSON-RPC implementation for Tryton server communication
|
||||
* Based on the Python implementation from sabatron-tryton-rpc-client
|
||||
* TypeScript version - Node.js only
|
||||
*/
|
||||
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const zlib = require("zlib");
|
||||
const { URL } = require("url");
|
||||
const { TrytonCache } = require("./cache");
|
||||
import https from "https";
|
||||
import http from "http";
|
||||
import zlib from "zlib";
|
||||
import { URL } from "url";
|
||||
import { TrytonCache } from "./cache";
|
||||
import type { ServerProxyOptions, ServerPoolOptions } from "../types";
|
||||
|
||||
// Constants
|
||||
const CONNECT_TIMEOUT = 5000; // 5 seconds
|
||||
@@ -16,29 +18,45 @@ const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
||||
/**
|
||||
* Custom error classes
|
||||
*/
|
||||
class ResponseError extends Error {
|
||||
constructor(message) {
|
||||
export class ResponseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ResponseError";
|
||||
}
|
||||
}
|
||||
|
||||
class Fault extends Error {
|
||||
constructor(faultCode, faultString = "", extra = {}) {
|
||||
export class Fault extends Error {
|
||||
public readonly faultCode: string | number;
|
||||
public readonly faultString: string;
|
||||
public readonly extra: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
faultCode: string | number,
|
||||
faultString: string = "",
|
||||
extra: Record<string, any> = {}
|
||||
) {
|
||||
super(faultString);
|
||||
this.name = "Fault";
|
||||
this.faultCode = faultCode;
|
||||
this.faultString = faultString;
|
||||
this.extra = extra;
|
||||
Object.assign(this, extra);
|
||||
}
|
||||
|
||||
toString() {
|
||||
override toString(): string {
|
||||
return String(this.faultCode);
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolError extends Error {
|
||||
constructor(message, errcode = null, errmsg = null) {
|
||||
export class ProtocolError extends Error {
|
||||
public readonly errcode: string | number | null;
|
||||
public readonly errmsg: string | null;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
errcode: string | number | null = null,
|
||||
errmsg: string | null = null
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ProtocolError";
|
||||
this.errcode = errcode;
|
||||
@@ -46,17 +64,66 @@ class ProtocolError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
interface TrytonDateTime {
|
||||
__class__: "datetime";
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour?: number;
|
||||
minute?: number;
|
||||
second?: number;
|
||||
microsecond?: number;
|
||||
}
|
||||
|
||||
interface TrytonDate {
|
||||
__class__: "date";
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}
|
||||
|
||||
interface TrytonTime {
|
||||
__class__: "time";
|
||||
hour?: number;
|
||||
minute?: number;
|
||||
second?: number;
|
||||
microsecond?: number;
|
||||
}
|
||||
|
||||
interface TrytonTimeDelta {
|
||||
__class__: "timedelta";
|
||||
seconds?: number;
|
||||
}
|
||||
|
||||
interface TrytonBytes {
|
||||
__class__: "bytes";
|
||||
base64: string;
|
||||
}
|
||||
|
||||
interface TrytonDecimal {
|
||||
__class__: "Decimal";
|
||||
decimal: string;
|
||||
}
|
||||
|
||||
type TrytonSpecialType =
|
||||
| TrytonDateTime
|
||||
| TrytonDate
|
||||
| TrytonTime
|
||||
| TrytonTimeDelta
|
||||
| TrytonBytes
|
||||
| TrytonDecimal;
|
||||
|
||||
/**
|
||||
* JSON encoder/decoder for Tryton specific types
|
||||
*/
|
||||
class TrytonJSONEncoder {
|
||||
export 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) => {
|
||||
static serialize(obj: any): string {
|
||||
return JSON.stringify(obj, (key: string, value: any) => {
|
||||
if (value instanceof Date) {
|
||||
return {
|
||||
__class__: "datetime",
|
||||
@@ -67,14 +134,14 @@ class TrytonJSONEncoder {
|
||||
minute: value.getMinutes(),
|
||||
second: value.getSeconds(),
|
||||
microsecond: value.getMilliseconds() * 1000,
|
||||
};
|
||||
} as TrytonDateTime;
|
||||
}
|
||||
|
||||
if (value instanceof Buffer) {
|
||||
if (typeof Buffer !== "undefined" && value instanceof Buffer) {
|
||||
return {
|
||||
__class__: "bytes",
|
||||
base64: value.toString("base64"),
|
||||
};
|
||||
} as TrytonBytes;
|
||||
}
|
||||
|
||||
// Handle BigInt as Decimal
|
||||
@@ -82,7 +149,7 @@ class TrytonJSONEncoder {
|
||||
return {
|
||||
__class__: "Decimal",
|
||||
decimal: value.toString(),
|
||||
};
|
||||
} as TrytonDecimal;
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -94,46 +161,67 @@ class TrytonJSONEncoder {
|
||||
* @param {string} str - JSON string
|
||||
* @returns {*} - Parsed object
|
||||
*/
|
||||
static deserialize(str) {
|
||||
return JSON.parse(str, (key, value) => {
|
||||
static deserialize(str: string): any {
|
||||
return JSON.parse(str, (key: string, value: any) => {
|
||||
if (value && typeof value === "object" && value.__class__) {
|
||||
switch (value.__class__) {
|
||||
case "datetime":
|
||||
const specialValue = value as TrytonSpecialType;
|
||||
switch (specialValue.__class__) {
|
||||
case "datetime": {
|
||||
const dt = specialValue as TrytonDateTime;
|
||||
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)
|
||||
dt.year,
|
||||
dt.month - 1,
|
||||
dt.day,
|
||||
dt.hour || 0,
|
||||
dt.minute || 0,
|
||||
dt.second || 0,
|
||||
Math.floor((dt.microsecond || 0) / 1000)
|
||||
);
|
||||
}
|
||||
|
||||
case "date":
|
||||
return new Date(value.year, value.month - 1, value.day);
|
||||
case "date": {
|
||||
const d = specialValue as TrytonDate;
|
||||
return new Date(d.year, d.month - 1, d.day);
|
||||
}
|
||||
|
||||
case "time":
|
||||
case "time": {
|
||||
const t = specialValue as TrytonTime;
|
||||
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)
|
||||
t.hour || 0,
|
||||
t.minute || 0,
|
||||
t.second || 0,
|
||||
Math.floor((t.microsecond || 0) / 1000)
|
||||
);
|
||||
}
|
||||
|
||||
case "timedelta":
|
||||
case "timedelta": {
|
||||
const td = specialValue as TrytonTimeDelta;
|
||||
// Return seconds as number
|
||||
return value.seconds || 0;
|
||||
return td.seconds || 0;
|
||||
}
|
||||
|
||||
case "bytes":
|
||||
return Buffer.from(value.base64, "base64");
|
||||
case "bytes": {
|
||||
const b = specialValue as TrytonBytes;
|
||||
if (typeof Buffer !== "undefined") {
|
||||
return Buffer.from(b.base64, "base64");
|
||||
}
|
||||
// Fallback for browser environment
|
||||
return new Uint8Array(
|
||||
atob(b.base64)
|
||||
.split("")
|
||||
.map((c) => c.charCodeAt(0))
|
||||
);
|
||||
}
|
||||
|
||||
case "Decimal":
|
||||
case "Decimal": {
|
||||
const dec = specialValue as TrytonDecimal;
|
||||
// Convert to number or keep as string for precision
|
||||
return parseFloat(value.decimal);
|
||||
return parseFloat(dec.decimal);
|
||||
}
|
||||
|
||||
default:
|
||||
return value;
|
||||
@@ -144,11 +232,41 @@ class TrytonJSONEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
interface TransportOptions {
|
||||
fingerprints?: string[] | null | undefined;
|
||||
caCerts?: string[] | null | undefined;
|
||||
session?: string | null | undefined;
|
||||
connectTimeout?: number | undefined;
|
||||
timeout?: number | undefined;
|
||||
useHttps?: boolean;
|
||||
}
|
||||
|
||||
interface JsonRpcRequest {
|
||||
id: number;
|
||||
method: string;
|
||||
params: any[];
|
||||
}
|
||||
|
||||
interface JsonRpcResponse {
|
||||
id: number;
|
||||
result?: any;
|
||||
error?: [string | number, string];
|
||||
cache?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Transport for JSON-RPC requests
|
||||
*/
|
||||
class Transport {
|
||||
constructor(options = {}) {
|
||||
export class Transport {
|
||||
private fingerprints: string[] | null;
|
||||
private caCerts: string[] | null;
|
||||
private session: string | null;
|
||||
private connection: http.ClientRequest | null;
|
||||
private connectTimeout: number;
|
||||
private timeout: number;
|
||||
private useHttps: boolean;
|
||||
|
||||
constructor(options: TransportOptions = {}) {
|
||||
this.fingerprints = options.fingerprints || null;
|
||||
this.caCerts = options.caCerts || null;
|
||||
this.session = options.session || null;
|
||||
@@ -166,7 +284,12 @@ class Transport {
|
||||
* @param {boolean} verbose - Enable verbose logging
|
||||
* @returns {Promise<Object>} - Response object
|
||||
*/
|
||||
async request(host, handler, requestData, verbose = false) {
|
||||
async request(
|
||||
host: string,
|
||||
handler: string,
|
||||
requestData: string,
|
||||
verbose: boolean = false
|
||||
): Promise<JsonRpcResponse> {
|
||||
// Detect protocol based on port or explicit protocol
|
||||
const hostParts = host.split(":");
|
||||
const port = hostParts[1] ? parseInt(hostParts[1]) : 80;
|
||||
@@ -183,14 +306,17 @@ class Transport {
|
||||
const url = new URL(`${protocol}://${host}${handler}`);
|
||||
const isHttps = url.protocol === "https:";
|
||||
|
||||
const options = {
|
||||
const options: https.RequestOptions = {
|
||||
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),
|
||||
"Content-Length":
|
||||
typeof Buffer !== "undefined"
|
||||
? Buffer.byteLength(requestData)
|
||||
: new TextEncoder().encode(requestData).length,
|
||||
Connection: "keep-alive",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
},
|
||||
@@ -201,25 +327,44 @@ class Transport {
|
||||
|
||||
// Add session authentication
|
||||
if (this.session) {
|
||||
const auth = Buffer.from(this.session).toString("base64");
|
||||
options.headers["Authorization"] = `Session ${auth}`;
|
||||
const auth =
|
||||
typeof Buffer !== "undefined"
|
||||
? Buffer.from(this.session).toString("base64")
|
||||
: btoa(this.session);
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Session ${auth}`,
|
||||
};
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<JsonRpcResponse>((resolve, reject) => {
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const req = client.request(options, (res) => {
|
||||
let data = Buffer.alloc(0);
|
||||
const req = client.request(options, (res: http.IncomingMessage) => {
|
||||
let data =
|
||||
typeof Buffer !== "undefined"
|
||||
? Buffer.alloc(0)
|
||||
: new Uint8Array(0);
|
||||
|
||||
res.on("data", (chunk) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
res.on("data", (chunk: any) => {
|
||||
if (typeof Buffer !== "undefined") {
|
||||
data = Buffer.concat([data as Buffer, chunk]);
|
||||
} else {
|
||||
// Browser fallback
|
||||
const newData = new Uint8Array(
|
||||
(data as Uint8Array).length + chunk.length
|
||||
);
|
||||
newData.set(data as Uint8Array);
|
||||
newData.set(chunk, (data as Uint8Array).length);
|
||||
data = newData;
|
||||
}
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
try {
|
||||
// Handle compression
|
||||
const encoding = res.headers["content-encoding"];
|
||||
let responseText;
|
||||
let responseText: string;
|
||||
|
||||
if (encoding === "gzip") {
|
||||
responseText = zlib
|
||||
@@ -237,12 +382,13 @@ class Transport {
|
||||
console.log("Response:", responseText);
|
||||
}
|
||||
|
||||
const response =
|
||||
TrytonJSONEncoder.deserialize(responseText);
|
||||
const response = TrytonJSONEncoder.deserialize(
|
||||
responseText
|
||||
) as JsonRpcResponse;
|
||||
|
||||
// Add cache header if present
|
||||
const cacheHeader = res.headers["x-tryton-cache"];
|
||||
if (cacheHeader) {
|
||||
if (cacheHeader && typeof cacheHeader === "string") {
|
||||
try {
|
||||
response.cache = parseInt(cacheHeader);
|
||||
} catch (e) {
|
||||
@@ -254,14 +400,16 @@ class Transport {
|
||||
} catch (error) {
|
||||
reject(
|
||||
new ResponseError(
|
||||
`Failed to parse response: ${error.message}`
|
||||
`Failed to parse response: ${
|
||||
(error as Error).message
|
||||
}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", (error) => {
|
||||
req.on("error", (error: any) => {
|
||||
if (error.code === "ECONNRESET" || error.code === "EPIPE") {
|
||||
// Retry once on connection reset
|
||||
reject(
|
||||
@@ -290,7 +438,7 @@ class Transport {
|
||||
/**
|
||||
* Close transport connection
|
||||
*/
|
||||
close() {
|
||||
close(): void {
|
||||
if (this.connection) {
|
||||
this.connection.destroy();
|
||||
this.connection = null;
|
||||
@@ -301,8 +449,24 @@ class Transport {
|
||||
/**
|
||||
* Server proxy for making RPC calls
|
||||
*/
|
||||
class ServerProxy {
|
||||
constructor(host, port, database = "", options = {}) {
|
||||
export class ServerProxy {
|
||||
private host: string;
|
||||
private port: number;
|
||||
private database: string;
|
||||
private verbose: boolean;
|
||||
private handler: string;
|
||||
private hostUrl: string;
|
||||
private requestId: number;
|
||||
private cache: TrytonCache | null;
|
||||
private useHttps: boolean;
|
||||
private transport: Transport;
|
||||
|
||||
constructor(
|
||||
host: string,
|
||||
port: number,
|
||||
database: string = "",
|
||||
options: ServerProxyOptions = {}
|
||||
) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.database = database;
|
||||
@@ -310,7 +474,10 @@ class ServerProxy {
|
||||
this.handler = database ? `/${encodeURIComponent(database)}/` : "/";
|
||||
this.hostUrl = `${host}:${port}`;
|
||||
this.requestId = 0;
|
||||
this.cache = options.cache || null;
|
||||
this.cache =
|
||||
options.cache && !Array.isArray(options.cache)
|
||||
? (options.cache as TrytonCache)
|
||||
: null;
|
||||
this.useHttps = options.useHttps || false;
|
||||
|
||||
this.transport = new Transport({
|
||||
@@ -325,11 +492,8 @@ class ServerProxy {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
async request<T = any>(methodName: string, params: any[]): Promise<T> {
|
||||
this.requestId += 1;
|
||||
const id = this.requestId;
|
||||
|
||||
@@ -337,7 +501,7 @@ class ServerProxy {
|
||||
id: id,
|
||||
method: methodName,
|
||||
params: params,
|
||||
});
|
||||
} as JsonRpcRequest);
|
||||
|
||||
// Check cache first
|
||||
if (this.cache && this.cache.cached(methodName)) {
|
||||
@@ -348,7 +512,7 @@ class ServerProxy {
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
// Retry logic (up to 5 attempts)
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
@@ -385,9 +549,9 @@ class ServerProxy {
|
||||
);
|
||||
}
|
||||
|
||||
return response.result;
|
||||
return response.result as T;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
lastError = error as Error;
|
||||
|
||||
// Check if we should retry
|
||||
if (error instanceof ProtocolError && error.errcode === 503) {
|
||||
@@ -400,8 +564,8 @@ class ServerProxy {
|
||||
// For connection errors, try once more
|
||||
if (
|
||||
attempt === 0 &&
|
||||
(error.code === "ECONNRESET" ||
|
||||
error.code === "EPIPE" ||
|
||||
((error as any).code === "ECONNRESET" ||
|
||||
(error as any).code === "EPIPE" ||
|
||||
error instanceof ProtocolError)
|
||||
) {
|
||||
this.transport.close();
|
||||
@@ -419,7 +583,7 @@ class ServerProxy {
|
||||
/**
|
||||
* Close server proxy
|
||||
*/
|
||||
close() {
|
||||
close(): void {
|
||||
this.transport.close();
|
||||
}
|
||||
|
||||
@@ -427,7 +591,7 @@ class ServerProxy {
|
||||
* Get SSL status
|
||||
* @returns {boolean} - Whether connection uses SSL
|
||||
*/
|
||||
get ssl() {
|
||||
get ssl(): boolean {
|
||||
return this.port === 443 || this.hostUrl.startsWith("https");
|
||||
}
|
||||
|
||||
@@ -435,7 +599,7 @@ class ServerProxy {
|
||||
* Get full URL
|
||||
* @returns {string} - Full server URL
|
||||
*/
|
||||
get url() {
|
||||
get url(): string {
|
||||
const scheme = this.ssl ? "https" : "http";
|
||||
return `${scheme}://${this.hostUrl}${this.handler}`;
|
||||
}
|
||||
@@ -444,8 +608,23 @@ class ServerProxy {
|
||||
/**
|
||||
* Connection pool for reusing ServerProxy instances
|
||||
*/
|
||||
class ServerPool {
|
||||
constructor(host, port, database, options = {}) {
|
||||
export class ServerPool {
|
||||
private host: string;
|
||||
private port: number;
|
||||
private database: string;
|
||||
private options: ServerPoolOptions;
|
||||
private keepMax: number;
|
||||
private session: string | null;
|
||||
private pool: ServerProxy[];
|
||||
private used: Set<ServerProxy>;
|
||||
private cache: TrytonCache | null;
|
||||
|
||||
constructor(
|
||||
host: string,
|
||||
port: number,
|
||||
database: string,
|
||||
options: ServerPoolOptions = {}
|
||||
) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.database = database;
|
||||
@@ -454,13 +633,16 @@ class ServerPool {
|
||||
this.session = options.session || null;
|
||||
|
||||
this.pool = [];
|
||||
this.used = new Set();
|
||||
this.used = new Set<ServerProxy>();
|
||||
this.cache = null;
|
||||
|
||||
// Initialize cache if requested
|
||||
if (options.cache) {
|
||||
this.cache = new TrytonCache();
|
||||
this.options.cache = this.cache;
|
||||
if (Array.isArray(options.cache)) {
|
||||
this.cache = new TrytonCache();
|
||||
} else {
|
||||
this.cache = options.cache as TrytonCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,15 +650,15 @@ class ServerPool {
|
||||
* Get connection from pool or create new one
|
||||
* @returns {ServerProxy} - Server proxy instance
|
||||
*/
|
||||
getConnection() {
|
||||
let conn;
|
||||
getConnection(): ServerProxy {
|
||||
let conn: ServerProxy;
|
||||
|
||||
if (this.pool.length > 0) {
|
||||
conn = this.pool.pop();
|
||||
conn = this.pool.pop()!;
|
||||
} else {
|
||||
conn = new ServerProxy(this.host, this.port, this.database, {
|
||||
...this.options,
|
||||
cache: this.cache,
|
||||
cache: this.cache as any,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -488,14 +670,16 @@ class ServerPool {
|
||||
* Return connection to pool
|
||||
* @param {ServerProxy} conn - Connection to return
|
||||
*/
|
||||
putConnection(conn) {
|
||||
putConnection(conn: ServerProxy): void {
|
||||
this.used.delete(conn);
|
||||
this.pool.push(conn);
|
||||
|
||||
// Remove excess connections
|
||||
while (this.pool.length > this.keepMax) {
|
||||
const oldConn = this.pool.shift();
|
||||
oldConn.close();
|
||||
if (oldConn) {
|
||||
oldConn.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +688,9 @@ class ServerPool {
|
||||
* @param {Function} callback - Async function to execute
|
||||
* @returns {Promise<*>} - Callback result
|
||||
*/
|
||||
async withConnection(callback) {
|
||||
async withConnection<T>(
|
||||
callback: (conn: ServerProxy) => Promise<T>
|
||||
): Promise<T> {
|
||||
const conn = this.getConnection();
|
||||
try {
|
||||
return await callback(conn);
|
||||
@@ -516,7 +702,7 @@ class ServerPool {
|
||||
/**
|
||||
* Close all connections in pool
|
||||
*/
|
||||
close() {
|
||||
close(): void {
|
||||
// Close all pooled connections
|
||||
for (const conn of this.pool) {
|
||||
conn.close();
|
||||
@@ -535,7 +721,7 @@ class ServerPool {
|
||||
* Clear cache
|
||||
* @param {string} [prefix] - Optional prefix to clear
|
||||
*/
|
||||
clearCache(prefix = null) {
|
||||
clearCache(prefix?: string): void {
|
||||
if (this.cache) {
|
||||
this.cache.clear(prefix);
|
||||
}
|
||||
@@ -545,10 +731,10 @@ class ServerPool {
|
||||
* Get SSL status from any connection
|
||||
* @returns {boolean|null} - SSL status or null if no connections
|
||||
*/
|
||||
get ssl() {
|
||||
get ssl(): boolean | null {
|
||||
const allConns = [...this.pool, ...this.used];
|
||||
if (allConns.length > 0) {
|
||||
return allConns[0].ssl;
|
||||
return allConns[0]?.ssl || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -557,21 +743,11 @@ class ServerPool {
|
||||
* Get URL from any connection
|
||||
* @returns {string|null} - URL or null if no connections
|
||||
*/
|
||||
get url() {
|
||||
get url(): string | null {
|
||||
const allConns = [...this.pool, ...this.used];
|
||||
if (allConns.length > 0) {
|
||||
return allConns[0].url;
|
||||
return allConns[0]?.url || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ResponseError,
|
||||
Fault,
|
||||
ProtocolError,
|
||||
TrytonJSONEncoder,
|
||||
Transport,
|
||||
ServerProxy,
|
||||
ServerPool,
|
||||
};
|
||||
Reference in New Issue
Block a user