agora-react-native-rtm
Version:
React Native around the Agora RTM SDKs for Android and iOS agora
383 lines (330 loc) • 9.45 kB
text/typescript
import { Buffer } from 'buffer';
import base64 from 'base64-js';
import EventEmitter from 'eventemitter3';
import { NativeEventEmitter } from 'react-native';
import { BaseResponse, ErrorInfo } from '../api/RTMClient';
import {
RTMClientEventMap,
cleanIrisExtraData,
processRTMClientEventMap,
} from '../api/RTMEvents';
import AgoraRtmNg from '../specs';
import { RtmClientInternal } from './RtmClientInternal';
import { StreamChannelInternal } from './StreamChannelInternal';
// Request queue for handling async operations
type RequestItem = {
resolve: Function;
reject: Function;
callbackName: string;
timeout?: ReturnType<typeof setTimeout>;
};
export class RequestQueue {
private static _instance: RequestQueue;
private _requestMap: Map<number, RequestItem> = new Map();
private constructor() {}
public static get instance(): RequestQueue {
if (!RequestQueue._instance) {
RequestQueue._instance = new RequestQueue();
}
return RequestQueue._instance;
}
public addRequest(
callbackName: string,
timeoutMs: number,
externalRequestId: number
): Promise<any> & { requestId: number } {
const requestId = externalRequestId;
let promiseResolve: Function;
let promiseReject: Function;
const promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
}) as Promise<any> & { requestId: number };
promise.requestId = requestId;
let timeout: ReturnType<typeof setTimeout> | undefined;
if (timeoutMs > 0) {
timeout = setTimeout(() => {
const item = this._requestMap.get(requestId);
if (item) {
this._requestMap.delete(requestId);
console.log(`Request timeout: ${requestId}`);
item.reject(
new Error(`Request ${requestId} timed out after ${timeoutMs}ms`)
);
}
}, timeoutMs);
}
this._requestMap.set(requestId, {
resolve: promiseResolve!,
reject: promiseReject!,
callbackName,
timeout,
});
return promise;
}
public resolveRequest(
requestId: number,
errorCode: number,
callbackName: string,
...args: any[]
): boolean {
const request = this._requestMap.get(requestId);
if (!request) {
console.log(`Request not found: ${requestId}`);
return false;
}
// Check if callback name matches
if (request.callbackName !== callbackName) {
console.log(
`Callback name mismatch: expected ${request.callbackName}, actual ${callbackName}`
);
return false;
}
console.log(
`Resolving request: ${requestId}, callback: ${callbackName}, error code: ${errorCode}`
);
if (request.timeout) {
clearTimeout(request.timeout);
}
this._requestMap.delete(requestId);
if (errorCode === 0) {
request.resolve(...args);
} else {
request.reject(...args);
}
return true;
}
}
export type IrisApiParam = {
funcName: string;
params: string;
buffers?: string[];
};
// @ts-ignore
export const DeviceEventEmitter: EventEmitter = new EventEmitter();
const AgoraEventEmitter = new NativeEventEmitter(AgoraRtmNg);
AgoraEventEmitter.addListener('AgoraRtmNg:onEvent', handleEvent);
let debuggable = false;
/**
* @internal
*/
export function setDebuggable(flag: boolean) {
debuggable = flag;
}
/**
* @internal
*/
export function isDebuggable() {
return debuggable && __DEV__;
}
/**
* @internal
*/
export type EventProcessor<T extends ProcessorType> = {
suffix: string;
type: (data: any) => EVENT_TYPE;
func: Function[];
preprocess?: (event: string, data: any, buffers: Uint8Array[]) => void;
handlers: (data: any) => (T | undefined)[] | undefined;
};
export enum EVENT_TYPE {
RTMEvent,
}
type ProcessorType = RTMClientEventMap;
type EventProcessors = {
RTMClientEventMap: EventProcessor<RTMClientEventMap>;
};
/**
* @internal
*/
export const EVENT_PROCESSORS: EventProcessors = {
RTMClientEventMap: {
suffix: 'RtmEventHandler_',
type: () => EVENT_TYPE.RTMEvent,
func: [processRTMClientEventMap],
handlers: () => RtmClientInternal._event_handlers,
preprocess: (event: string, data: any, buffers: Uint8Array[]) => {
switch (event) {
case 'onMessageEvent':
console.log('onMessageEvent', data.event.message, buffers);
data.event.message = buffers[0]?.toString();
break;
}
return { event, data, buffers };
},
},
};
function handleEvent({ event, data, buffers }: any) {
if (debuggable) {
console.info('onEvent', event, data, buffers);
}
let _data: any;
try {
_data = JSON.parse(data) ?? {};
} catch (e) {
_data = {};
}
let _event: string = event;
let processor: EventProcessor<any> = EVENT_PROCESSORS.RTMClientEventMap;
Object.values(EVENT_PROCESSORS).some((it) => {
const p = it as EventProcessor<any>;
if (
_event.startsWith(p.suffix) &&
processor.handlers(_data) !== undefined
) {
processor = p;
const reg = new RegExp(`^${processor.suffix}`, 'g');
_event = _event.replace(reg, '');
return true;
}
return false;
});
// for new IrisType, but this is temporary
if (_event.includes('_')) {
_event = _event.substring(0, _event.indexOf('_'));
}
const _buffers: Uint8Array[] = (buffers as string[])?.map((value) => {
return Buffer.from(value, 'base64') as unknown as Uint8Array;
});
if (processor.preprocess) {
processor.preprocess(_event, _data, _buffers);
}
if (processor.handlers) {
processor.handlers(_data)?.map((value) => {
if (value) {
processor.func.map((it) => {
it(value, _event, _data);
});
}
});
}
const requestId = _data.requestId;
if (requestId !== undefined && _data.errorCode !== undefined) {
const requestQueue = RequestQueue.instance;
requestQueue.resolveRequest(requestId, _data.errorCode, _event, _data);
}
emitEvent(_event, processor, _data);
}
/**
* @internal
*/
export function callIrisApi(this: any, funcName: string, params: any): any {
try {
const buffers: string[] = [];
if (funcName.startsWith('StreamChannel_')) {
params.channelName = (this as StreamChannelInternal).channelName;
const json = params.toJSON?.call();
params.toJSON = function () {
return { ...json, channelName: params.channelName };
};
}
if (
funcName === 'RtmClient_publish_2d36e93' ||
funcName === 'StreamChannel_publishTopicMessage_a31773e'
) {
if (typeof params.message === 'string') {
let uint8Array = new Uint8Array(Buffer.from(params.message));
let buffer = base64.fromByteArray(uint8Array);
console.log(uint8Array, buffer);
buffers.push(buffer);
params.length = base64.byteLength(buffer);
} else {
let buffer = base64.fromByteArray(params.message);
console.log(params.message, buffer);
buffers.push(buffer);
params.length = base64.byteLength(buffer);
}
delete params.message;
const json = params.toJSON?.call();
delete json.message;
params.toJSON = function () {
return { ...json, length: params.length };
};
}
// RTM_ERROR_DUPLICATE_OPERATION
if (funcName === 'RtmClient_create') {
AgoraRtmNg.newIrisRtmEngine();
}
let ret = AgoraRtmNg.callApi({
funcName,
params: JSON.stringify(params),
buffers,
});
if (funcName === 'RtmClient_release') {
AgoraRtmNg.destroyIrisRtmEngine();
}
if (ret !== undefined && ret !== null && ret !== '' && ret !== 'null') {
const retObj = JSON.parse(ret);
if (isDebuggable()) {
if (typeof retObj.result === 'number' && retObj.result < 0) {
console.error('callApi', funcName, JSON.stringify(params), ret);
} else {
console.log('callApi', funcName, JSON.stringify(params), ret);
}
}
return retObj;
}
} catch (e) {
if (isDebuggable()) {
console.error('callApi', funcName, JSON.stringify(params), e);
} else {
console.warn('callApi', funcName, JSON.stringify(params), e);
}
}
return {};
}
/**
* @internal
*/
export function emitEvent<EventType extends keyof T, T extends ProcessorType>(
eventType: EventType,
eventProcessor: EventProcessor<T>,
data: any
): void {
DeviceEventEmitter.emit(eventType as string, eventProcessor, data);
}
type WrapRtmResultResult = BaseResponse & {
callBackResult?: any;
};
/**
* @internal
*/
export async function wrapRtmResult(
data: any,
operation: string,
callbackName: string,
withCallbackResult: boolean = false
): Promise<WrapRtmResultResult> {
if (data.result < 0) {
throw {
error: true,
reason: 'iris call failed',
operation,
errorCode: data.result,
};
} else {
let result = await RequestQueue.instance.addRequest(
callbackName,
60000,
data.requestId
);
if (withCallbackResult) {
result = cleanIrisExtraData(result);
}
return {
timestamp: 0,
...(withCallbackResult ? { callBackResult: result } : {}),
};
}
}
/**
* @internal
*/
export function handleError(data: any, operation: string): ErrorInfo {
return {
error: true,
reason: data,
operation: operation,
errorCode: data?.errorCode,
};
}