UNPKG

dimension-js-sdk

Version:
184 lines (160 loc) 5.11 kB
import { Result, Ok, Err } from 'ts-results'; import http from 'http'; interface DeploySubscription { deployHash: string; eventHandlerFn: EventHandlerFn; } enum StreamErrors { NotAnEvent, EarlyEndOfStream, MissingDataHeader, MissingDataHeaderAndId, MissingId } export class DeployWatcher { es: EventStream; watchList: DeploySubscription[] = []; constructor(public eventStreamUrl: string) { this.es = new EventStream(eventStreamUrl); } subscribe(val: DeploySubscription[]): void { this.watchList = [...this.watchList, ...val]; } unsubscribe(deployHash: string): void { this.watchList = this.watchList.filter(d => d.deployHash !== deployHash); } start() { this.es.subscribe(EventName.DeployProcessed, result => { const deployHash = result.body.DeployProcessed.deploy_hash; const pendingDeploy = this.watchList.find( d => d.deployHash === deployHash ); if (pendingDeploy) { pendingDeploy.eventHandlerFn(result); this.unsubscribe(deployHash); } }); this.es.start(); } stop() { this.es.stop(); } } type EventHandlerFn = (result: any) => void; export enum EventName { BlockAdded = 'BlockAdded', BlockFinalized = 'BlockFinalized', FinalitySignature = 'FinalitySignature', Fault = 'Fault', DeployProcessed = 'DeployProcessed' } interface EventSubscription { eventName: EventName; eventHandlerFn: EventHandlerFn; } interface EventParseResult { id: string | null; err: StreamErrors | null; body: any | null; } export class EventStream { subscribedTo: EventSubscription[] = []; pendingDeploysParts: EventParseResult[] = []; pendingDeployString = ''; stream: any; constructor(public eventStreamUrl: string) {} subscribe( eventName: EventName, eventHandlerFn: EventHandlerFn ): Result<boolean, string> { if (this.subscribedTo.some(e => e.eventName === eventName)) { return Err('Already subscribed to this event'); } this.subscribedTo.push({ eventName, eventHandlerFn }); return Ok(true); } unsubscribe(eventName: EventName): Result<boolean, string> { if (!this.subscribedTo.some(e => e.eventName === eventName)) { return Err('Cannot find provided subscription'); } this.subscribedTo = this.subscribedTo.filter( d => d.eventName !== eventName ); return Ok(true); } runEventsLoop(result: EventParseResult) { this.subscribedTo.forEach((sub: EventSubscription) => { if (result.body && result.body.hasOwnProperty(sub.eventName)) { sub.eventHandlerFn(result); } }); } start(eventId = 0): void { http.get(`${this.eventStreamUrl}?start_from=${eventId}`, res => { this.stream = res; this.stream.on('data', (buf: Uint8Array) => { const result = parseEvent(Buffer.from(buf).toString()); if (result && !result.err) { this.runEventsLoop(result); } if (result.err === StreamErrors.EarlyEndOfStream) { this.pendingDeployString = result.body; } if (result.err === StreamErrors.MissingDataHeaderAndId) { this.pendingDeployString += result.body; } if (result.err === StreamErrors.MissingDataHeader) { this.pendingDeployString += result.body; this.pendingDeployString += `\nid:${result.id}`; const newResult = parseEvent(this.pendingDeployString); if (newResult.err === null) { this.pendingDeployString = ''; } this.runEventsLoop(newResult); } }); }); } stop(): void { this.stream.destroy(); } } export const parseEvent = (eventString: string): any => { if (eventString.startsWith('data')) { const splitted = eventString.split('\n'); const id = splitted[1] && splitted[1].startsWith('id:') ? splitted[1].substr(3) : null; try { const body = JSON.parse(splitted[0].substr(5)); if (id) { // Note: This is case where there is proper object with JSON body and id in one chunk. return { id, body, err: null }; } else { // Note: This is case where there is proper object with JSON body but without ID. return { id, body, err: StreamErrors.MissingId }; } } catch { // Note: This is case where there is invalid JSON because of early end of stream. const body = splitted[0]; return { id, body, err: StreamErrors.EarlyEndOfStream }; } } else { // Note: This is in case where there data chunk which isn't the first one. const splitted = eventString.split('\n'); const body = splitted[0]; const id = splitted[1] && splitted[1].startsWith('id:') ? splitted[1].substr(3) : null; if (splitted[0] === ':' && splitted[1] === '' && splitted[2] === '') { return { id: null, body: null, err: StreamErrors.NotAnEvent }; } if (id) { return { id, body, err: StreamErrors.MissingDataHeader }; } else { return { id: null, body, err: StreamErrors.MissingDataHeaderAndId }; } } };