UNPKG

@ton3/liteclient

Version:
152 lines (115 loc) 4.11 kB
import EventEmitter from 'events' import { ADNLClientTCP, ADNLClientWS } from 'adnl' import { BlockchainClient } from './blockchain' import { LiteApi } from './liteapi' import { hrtime } from 'process' const PING_INTERVAL = 1000 const SYNC_INTERVAL = 2000 const MEASUREMENT_WINDOW = 60 interface LiteClientStats { roundTimeTrip: number outOfSync: number head: number } class LiteClient { private _isReady = false public adnl: ADNLClientTCP public api: BlockchainClient public roundTimeTrips: number[] = [] public outOfSyncs: number[] = [] public utime: number public seqno: number public readonly events = new EventEmitter(); private constructor( private readonly _url: string, private readonly _publicKey: string | Uint8Array ) { const adnlClient = this._url.toLowerCase().startsWith('tcp://') ? new ADNLClientTCP(this._url, this.publicKey) : new ADNLClientWS(this._url, this.publicKey) adnlClient.on('error', this.onError.bind(this)) adnlClient.on('ready', this.onReady.bind(this)) this.adnl = adnlClient this.api = new BlockchainClient(new LiteApi(adnlClient)) adnlClient.connect() } private onReady (): void { this._isReady = true this.loop(this.ping.bind(this), PING_INTERVAL) this.loop(this.sync.bind(this)) } private onError (err: any): void { // ... } public static create (url: string, publicKey: string | Uint8Array): LiteClient { const liteServer = new LiteClient(url, publicKey) return liteServer } private async ping (): Promise<void> { if (!this.isReady) return Promise.resolve() const start = hrtime() await this.api.getTime() const [ sec, nano ] = hrtime(start) const rtt = Math.round((sec * 1e9 + nano) / 1e6) if (this.roundTimeTrips.unshift(rtt) > MEASUREMENT_WINDOW) { this.roundTimeTrips.pop() } } private async sync (): Promise<void> { if (!this.isReady) return Promise.resolve() const wait = this.seqno ? { seqno: this.seqno + 1 } : undefined const last = await this.api.getMasterchainInfo(wait) const { info } = await this.api.getBlock(last) const outOfSync = Math.floor(Date.now() / 1000) - info.gen_utime this.events.emit('block', last) this.seqno = info.seq_no this.utime = info.gen_utime if (this.outOfSyncs.unshift(outOfSync) > MEASUREMENT_WINDOW) { this.outOfSyncs.pop() } } private async delay (timeout: number): Promise<null> { return new Promise(res => setTimeout(() => res(null), timeout)) } private async loop (action: () => Promise<void>, sleep?: number): Promise<void> { while (this.isReady) { try { await action() if (!sleep) { continue } await this.delay(sleep) } catch (err) { // TODO: handle error } } } public get url (): string { return this._url } public get publicKey (): string | Uint8Array { return this._publicKey } public get stats (): LiteClientStats { const roundTimeTrip = this.roundTimeTrips.reduce((acc, el) => acc + el, 0) / this.roundTimeTrips.length const outOfSync = this.outOfSyncs.reduce((acc, el) => acc + el, 0) / this.outOfSyncs.length return { roundTimeTrip: Math.round(roundTimeTrip), outOfSync: Math.round(outOfSync), head: this.seqno } } public get isReady (): boolean { return this._isReady } public get isSynced (): boolean { if (!this.isReady || !this.utime) return false const now = Math.round(Date.now() / 1000) const diff = now - this.utime return diff <= 30 } public get isAlive (): boolean { return this.isReady && this.isSynced } } export { LiteClient }