tiinyrpc
Version:
An small rpc server base on tinyrpc protocol
158 lines (131 loc) • 5.02 kB
text/typescript
import http from 'http';
import https from 'https';
import beson from "beson";
import {TrimId} from "./trimid.js";
import { ErrorCode } from './consts.js';
const DEFAULT_TIMEOUT = 5_000;
export interface ClientInitOptions {
url:string;
use_beson?:boolean;
timeout?:number;
headers?:http.OutgoingHttpHeaders;
};
interface ClientPrivates {use_beson:boolean; timeout:number; url:string; headers:http.OutgoingHttpHeaders;};
const _Client:WeakMap<Client, ClientPrivates> = new WeakMap();
export class Client {
static init(options:ClientInitOptions):Client { return new Client(options); }
constructor(options:ClientInitOptions) {
_Client.set(this, {
use_beson: !!options.use_beson,
url:options.url,
timeout:typeof options.timeout === "number" ? options.timeout : DEFAULT_TIMEOUT,
headers: Object.assign({}, options.headers)
});
}
get use_beson() { return _Client.get(this)!.use_beson; }
set use_beson(v:boolean) { _Client.get(this)!.use_beson = !!v; }
get timeout() { return _Client.get(this)!.timeout; }
set timeout(v:number) {
if ( typeof v !== "number")
throw new TypeError("Property timeout only accept integers!");
_Client.get(this)!.timeout = v;
}
get headers() { return _Client.get(this)!.headers; }
mutate(overwrites:Omit<ClientInitOptions, 'url'>):Client {
const curr = _Client.get(this)!;
const new_client = new Client({url:''});
Object.assign(_Client.get(new_client)!, overwrites, {url:curr.url});
return new_client;
}
invoke<ReturnType=any>(call:string, ...args:any[]):Promise<ReturnType> {
const {use_beson, headers, timeout, url} = _Client.get(this)!;
return TRPCCall<ReturnType>({use_beson, headers, timeout, url}, call, args);
}
}
interface ReqInfo { use_beson?:boolean; url?:string; headers?:http.OutgoingHttpHeaders; timeout?:number; }
function TRPCCall<ReturnType=any>(url:string, call:string, args:any[]):Promise<ReturnType>;
function TRPCCall<ReturnType=any>(info:ReqInfo, call:string, args:any[]):Promise<ReturnType>
function TRPCCall<ReturnType=any>(arg1:string|ReqInfo, call:string, args:any[]):Promise<ReturnType> {
let use_beson:boolean, timeout:number, headers:http.OutgoingHttpHeaders, url:string;
if ( typeof arg1 === "string" ) {
url = arg1;
headers = {};
timeout = 0;
use_beson = false;
}
else {
if ( typeof arg1.url !== "string" || !arg1.url ) { throw new Error("Field url is required!"); }
url = arg1.url;
timeout = arg1.timeout||0;
headers = arg1.headers||{};
use_beson = !!arg1.use_beson;
}
return new Promise<ReturnType>((resolve, reject)=>{
const _http = url.substring(0, 6) === 'https:' ? https : http;
const _headers = {...headers};
delete _headers['content-type'];
delete _headers['Content-Type'];
let body:any;
if ( use_beson ) {
_headers['Content-Type'] = 'application/beson';
body = Buffer.from(beson.Serialize({rpc:"1.0", id:TrimId(), call, args}));
}
else {
_headers['Content-Type'] = 'application/json';
body = Buffer.from(JSON.stringify({rpc:"1.0", id:TrimId(), call, args}));
}
const req = _http.request(url, {method:'POST', headers:_headers}, (res)=>{
const chunks:Buffer[] = [];
const status_code = res.statusCode!;
res
.on('data', c=>chunks.push(c))
.on('error', err=>reject(err))
.on('end', ()=>{
const raw_data = Buffer.concat(chunks);
let utfdata:string|undefined = undefined;
let parsed_data:any|undefined = undefined;
const result_ctnt_type = res.headers['content-type']||'';
const divider = result_ctnt_type.indexOf(';');
const content_type = result_ctnt_type.substring(0, divider < 0 ? result_ctnt_type.length : divider);
if ( content_type === "application/json" ) {
try { utfdata = raw_data.toString('utf8'); } catch(e) {}
if ( utfdata !== undefined ) {
try { parsed_data = JSON.parse(utfdata); } catch(e) {}
}
}
else
if ( content_type === "application/beson" ) {
parsed_data = beson.Deserialize(raw_data);
}
else {
return reject(Object.assign(new Error("Unsupported mime type returned from remote server!"), {
code: ErrorCode.UNSUPPORTED_RESPONSE_TYPE,
detail: { mime:content_type }
}))
}
if ( parsed_data === undefined || Object(parsed_data) !== parsed_data ) {
return reject(Object.assign(new Error("Unable to resolve response body content!"), {
code: ErrorCode.INCORRECT_RESPONSE_FORMAT,
status: status_code,
data: parsed_data||utfdata||raw_data
}));
}
if ( parsed_data.error !== undefined ) {
return reject(Object.assign(new Error(parsed_data.error.message),
{code:ErrorCode.UNKOWN_ERROR},
parsed_data.error,
{
status: status_code,
is_remote: true
}
));
}
return resolve(parsed_data.ret);
});
})
.on('error', (err)=>reject(err))
.on('timeout', function(this:http.ClientRequest) { this.destroy(); });
if ( timeout > 0 ) { req.setTimeout(timeout); }
req.end(body);
});
}