-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #48 from ainblockchain/feature/csh/subscribe
Feature/csh/subscribe
- Loading branch information
Showing
12 changed files
with
2,070 additions
and
2,940 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Ain from '../src/ain'; | ||
|
||
const { test_event_handler_node } = require('./test_data'); | ||
const delayMs = (time) => new Promise(resolve => setTimeout(resolve, time)); | ||
|
||
jest.setTimeout(60000); | ||
|
||
describe('Event Handler', function() { | ||
let ain = new Ain(test_event_handler_node); | ||
|
||
beforeAll(async () => { | ||
await ain.em.connect(); | ||
}); | ||
|
||
afterAll(() => { | ||
ain.em.disconnect(); | ||
}); | ||
|
||
it('Subscribe to BLOCK_FINALIZED', async () => { | ||
const callback = jest.fn(); | ||
ain.em.subscribe('BLOCK_FINALIZED', {}, (data) => { | ||
callback(data); | ||
}); | ||
await delayMs(10000); | ||
expect(callback).toBeCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import EventFilter from './event-filter'; | ||
import Subscription from './subscription'; | ||
import { BlockchainEventTypes, EventConfigType } from '../types'; | ||
import { PushId } from '../ain-db/push-id'; | ||
|
||
export default class EventCallbackManager { | ||
private readonly _filters: Map<string, EventFilter>; | ||
private readonly _filterIdToSubscription: Map<string, Subscription>; | ||
|
||
constructor() { | ||
this._filters = new Map<string, EventFilter>(); | ||
this._filterIdToSubscription = new Map<string, Subscription>(); | ||
} | ||
|
||
buildFilterId() { | ||
return PushId.generate(); | ||
} | ||
|
||
buildSubscriptionId() { | ||
return PushId.generate(); | ||
} | ||
|
||
emitEvent(filterId: string, payload: any) { | ||
const subscription = this._filterIdToSubscription.get(filterId); | ||
if (!subscription) { | ||
throw Error(`Can't find subscription by filter id (${filterId})`); | ||
} | ||
subscription.emit('data', payload); | ||
} | ||
|
||
emitError(filterId: string, errorMessage: string) { | ||
const subscription = this._filterIdToSubscription.get(filterId); | ||
if (!subscription) { | ||
throw Error(`Can't find subscription by filter id (${filterId})`); | ||
} | ||
subscription.emit('error', errorMessage); | ||
} | ||
|
||
createFilter(eventTypeStr: string, config: EventConfigType): EventFilter { | ||
const eventType = eventTypeStr as BlockchainEventTypes; | ||
switch (eventType) { | ||
case BlockchainEventTypes.BLOCK_FINALIZED: | ||
const filterId = this.buildFilterId(); | ||
if (this._filters.get(filterId)) { // TODO(cshcomcom): Retry logic | ||
throw Error(`Already existing filter id in filters (${filterId})`); | ||
} | ||
const filter = new EventFilter(filterId, eventType, config); | ||
this._filters.set(filterId, filter); | ||
return filter; | ||
case BlockchainEventTypes.VALUE_CHANGED: // TODO(cshcomcom): Implement | ||
throw Error(`Not implemented`); | ||
case BlockchainEventTypes.TX_STATE_CHANGED: // TODO(cshcomcom): Implement | ||
throw Error(`Not implemented`); | ||
default: | ||
throw Error(`Invalid event type (${eventType})`); | ||
} | ||
} | ||
|
||
createSubscription(filter: EventFilter, dataCallback?: (data: any) => void, | ||
errorCallback?: (error: any) => void) { | ||
const subscription = new Subscription(filter); | ||
if (dataCallback) { | ||
subscription.on('data', dataCallback); | ||
} | ||
if (errorCallback) { | ||
subscription.on('error', errorCallback); | ||
} | ||
this._filterIdToSubscription.set(filter.id, subscription); | ||
return subscription; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import Ain from '../ain'; | ||
import { WebSocket } from 'ws'; | ||
import { | ||
EventChannelMessageTypes, | ||
EventChannelMessage, | ||
BlockchainEventTypes, | ||
EventChannelConnectionOption, | ||
} from '../types'; | ||
import EventFilter from './event-filter'; | ||
import EventCallbackManager from './event-callback-manager'; | ||
|
||
export default class EventChannelClient { | ||
private readonly _ain: Ain; | ||
private readonly _eventCallbackManager: EventCallbackManager; | ||
private _wsClient?: WebSocket; | ||
private _endpointUrl?: string; | ||
private _isConnected: boolean; | ||
|
||
constructor(ain: Ain, eventCallbackManager: EventCallbackManager) { | ||
this._ain = ain; | ||
this._eventCallbackManager = eventCallbackManager; | ||
this._wsClient = undefined; | ||
this._endpointUrl = undefined; | ||
this._isConnected = false; | ||
} | ||
|
||
get isConnected(): boolean { | ||
return this._isConnected; | ||
} | ||
|
||
connect(connectionOption: EventChannelConnectionOption) { | ||
return new Promise(async (resolve, reject) => { | ||
const eventHandlerNetworkInfo = await this._ain.net.getEventHandlerNetworkInfo(); | ||
const url = eventHandlerNetworkInfo.url; | ||
if (!url) { | ||
reject(new Error(`Can't get url from eventHandlerNetworkInfo ` + | ||
`(${JSON.stringify(eventHandlerNetworkInfo, null, 2)}`)); | ||
} | ||
this._endpointUrl = url; | ||
this._wsClient = new WebSocket(url, [], { handshakeTimeout: connectionOption.handshakeTimeout || 30000 }); | ||
this._wsClient.on('message', (message) => { | ||
this.handleMessage(message); | ||
}); | ||
this._wsClient.on('error', (err) => { | ||
reject(err); | ||
}); | ||
this._wsClient.on('open', () => { | ||
this._isConnected = true; | ||
resolve(); | ||
}); | ||
// TODO(cshcomcom): Handle close connection (w/ ping-pong) | ||
}) | ||
} | ||
|
||
disconnect() { | ||
this._isConnected = false; | ||
this._wsClient.close(); | ||
} | ||
|
||
handleEmitEventMessage(messageData) { | ||
const filterId = messageData.filter_id; | ||
if (!filterId) { | ||
throw Error(`Can't find filter ID from message data (${JSON.stringify(messageData, null, 2)})`); | ||
} | ||
const eventTypeStr = messageData.type; | ||
const eventType = eventTypeStr as BlockchainEventTypes; | ||
if (!Object.values(BlockchainEventTypes).includes(eventType)) { | ||
throw Error(`Invalid event type (${eventTypeStr})`); | ||
} | ||
const payload = messageData.payload; | ||
if (!payload) { | ||
throw Error(`Can't find payload from message data (${JSON.stringify(messageData, null, 2)})`); | ||
} | ||
this._eventCallbackManager.emitEvent(filterId, payload); | ||
} | ||
|
||
handleEmitErrorMessage(messageData) { | ||
const filterId = messageData.filter_id; | ||
if (!filterId) { | ||
throw Error(`Can't find filter ID from message data (${JSON.stringify(messageData, null, 2)})`); | ||
} | ||
// TODO(cshcomcom): error codes | ||
const errorMessage = messageData.error_message; | ||
if (!errorMessage) { | ||
throw Error(`Can't find error message from message data (${JSON.stringify(messageData, null, 2)})`); | ||
} | ||
this._eventCallbackManager.emitError(filterId, errorMessage); | ||
} | ||
|
||
handleMessage(message: string) { | ||
try { | ||
const parsedMessage = JSON.parse(message); | ||
const messageType = parsedMessage.type; | ||
if (!messageType) { | ||
throw Error(`Can't find type from message (${message})`); | ||
} | ||
const messageData = parsedMessage.data; | ||
if (!messageData) { | ||
throw Error(`Can't find data from message (${message})`); | ||
} | ||
switch (messageType) { | ||
case EventChannelMessageTypes.EMIT_EVENT: | ||
this.handleEmitEventMessage(messageData); | ||
break; | ||
case EventChannelMessageTypes.EMIT_ERROR: | ||
this.handleEmitErrorMessage(messageData); | ||
break; | ||
default: | ||
break; | ||
} | ||
} catch (err) { | ||
console.error(err); | ||
// TODO(cshcomcom): Error handling | ||
} | ||
} | ||
|
||
buildMessage(messageType: EventChannelMessageTypes, data: any): EventChannelMessage { | ||
return { | ||
type: messageType, | ||
data: data, | ||
}; | ||
} | ||
|
||
registerFilter(filter: EventFilter) { | ||
const filterObj = filter.toObject(); | ||
const registerMessage = this.buildMessage(EventChannelMessageTypes.REGISTER_FILTER, filterObj); | ||
this._wsClient.send(JSON.stringify(registerMessage)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { BlockchainEventTypes, EventConfigType } from '../types'; | ||
|
||
export default class EventFilter { | ||
public readonly id: string; | ||
public readonly type: BlockchainEventTypes; | ||
public readonly config: EventConfigType; | ||
|
||
constructor(id: string, type: BlockchainEventTypes, config: EventConfigType) { | ||
this.id = id; | ||
this.type = type; | ||
this.config = config; | ||
} | ||
|
||
toObject() { | ||
return { | ||
id: this.id, | ||
type: this.type, | ||
config: this.config, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import Ain from '../ain'; | ||
import { | ||
BlockFinalizedEventConfig, | ||
ErrorFirstCallback, | ||
EventChannelConnectionOption, | ||
EventConfigType, | ||
TxStateChangedEventConfig, | ||
ValueChangedEventConfig, | ||
} from '../types'; | ||
import EventChannelClient from './event-channel-client'; | ||
import EventCallbackManager from './event-callback-manager'; | ||
import Subscription from './subscription'; | ||
|
||
export default class EventManager { | ||
private _ain: Ain; | ||
private readonly _eventCallbackManager: EventCallbackManager; | ||
private readonly _eventChannelClient: EventChannelClient; | ||
|
||
constructor(ain: Ain) { | ||
this._ain = ain; | ||
this._eventCallbackManager = new EventCallbackManager(); | ||
this._eventChannelClient = new EventChannelClient(ain, this._eventCallbackManager); | ||
} | ||
|
||
async connect(connectionOption?: EventChannelConnectionOption) { | ||
await this._eventChannelClient.connect(connectionOption || {}); | ||
} | ||
|
||
disconnect() { | ||
this._eventChannelClient.disconnect(); | ||
} | ||
|
||
subscribe( | ||
eventType: 'BLOCK_FINALIZED', config: BlockFinalizedEventConfig, | ||
dataCallback?: (data: any) => void, errorCallback?: (error: any) => void): string; | ||
subscribe( | ||
eventType: 'VALUE_CHANGED', config: ValueChangedEventConfig, | ||
dataCallback?: (data: any) => void, errorCallback?: (error: any) => void): string; | ||
subscribe( | ||
eventType: 'TX_STATE_CHANGED', config: TxStateChangedEventConfig, | ||
dataCallback?: (data: any) => void, errorCallback?: (error: any) => void): string; | ||
subscribe( | ||
eventTypeStr: string, config: EventConfigType, | ||
dataCallback?: (data: any) => void, errorCallback?: (error: any) => void): string { | ||
if (!this._eventChannelClient.isConnected) { | ||
throw Error(`Event channel is not connected! You must call ain.eh.connect() before using subscribe()`); | ||
} | ||
const filter = this._eventCallbackManager.createFilter(eventTypeStr, config); | ||
this._eventChannelClient.registerFilter(filter); | ||
this._eventCallbackManager.createSubscription(filter, dataCallback, errorCallback); | ||
return filter.id; | ||
} | ||
|
||
unsubscribe(filterId: string, callback: ErrorFirstCallback<boolean>) { | ||
// TODO(cshcomcom): Implement logic | ||
callback(new Error(`Not implemented!`)); | ||
} | ||
} |
Oops, something went wrong.