@amityco/ts-sdk-react-native
Version:
Amity Social Cloud Typescript SDK
204 lines (179 loc) • 6.09 kB
text/typescript
/* eslint-disable no-console */
/* eslint-disable no-promise-executor-return */
import mqtt, { IClientOptions, ISubscriptionGrant, MqttClient } from 'mqtt/dist/mqtt';
import { subscribeGlobalTopic } from '~/client/utils/subscribeGlobalTopic';
import { ASCError, ASCUnknownError } from '~/core/errors';
import { getMQTTClientId } from '~/core/device';
const QOS_FAILURE_CODE = 128;
const RETRY_BASE_TIMEOUT = 1000;
const RETRY_MAX_TIMEOUT = 8000;
const enum MqttError {
IDENTIFIER_REJECTED = 2,
BAD_USERNAME_OR_PASSWORD = 134,
NOT_AUTHORIZED = 135,
}
export function getMqttOptions(params: {
username: string;
password: string;
clientId?: string;
}): IClientOptions {
return {
clean: false, // keep subscriptions
clientId: `mqttjs_ + ${Math.random().toString(16).substring(2, 10)}`,
protocolId: 'MQTT',
protocolVersion: 4,
reconnectPeriod: RETRY_BASE_TIMEOUT,
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false,
},
resubscribe: true,
...params,
};
}
/**
* Creates a pre-configured socket.io instance
*
* @param endpoint The mqtt server's URL
* @returns A pre-configured (non-connected) mqtt client instance
*
* @category Transport
* @hidden
*/
export const createMqttTransport = (endpoint: string): Amity.MqttClient => {
let mqttClient: MqttClient;
let currentState: Amity.RTEConnectionState = 'disconnected';
const connectionStateListeners: Set<(state: Amity.RTEConnectionState) => void> = new Set();
const updateConnectionState = (state: Amity.RTEConnectionState) => {
if (currentState === state) return;
currentState = state;
connectionStateListeners.forEach(listener => {
listener(state);
});
};
async function connect(params: { accessToken: string; userId: string }): Promise<void> {
const clientId = await getMQTTClientId(params.userId);
updateConnectionState('connecting');
if (mqttClient) {
updateConnectionState('disconnecting');
mqttClient.removeAllListeners();
mqttClient.end(true);
}
mqttClient = mqtt.connect(
endpoint,
getMqttOptions({
username: params.userId,
password: params.accessToken,
clientId,
}),
);
mqttClient.on('connect', () => {
mqttClient.options.reconnectPeriod = RETRY_BASE_TIMEOUT;
updateConnectionState('connected');
subscribeGlobalTopic();
});
mqttClient.on('error', (error: Error & { code: number }) => {
// eslint-disable-next-line default-case
switch (error.code) {
case MqttError.IDENTIFIER_REJECTED:
case MqttError.BAD_USERNAME_OR_PASSWORD:
case MqttError.NOT_AUTHORIZED:
mqttClient.end();
updateConnectionState('disconnected');
}
});
mqttClient.on('reconnect', () => {
updateConnectionState('reconnecting');
// Double the reconnect period for each attempt
mqttClient.options.reconnectPeriod = Math.min(
(mqttClient.options.reconnectPeriod || RETRY_BASE_TIMEOUT) * 2,
RETRY_MAX_TIMEOUT,
);
});
mqttClient.on('close', () => {
updateConnectionState('disconnected');
});
mqttClient.on('disconnect', () => {
updateConnectionState('disconnected');
});
return new Promise(resolve => mqttClient!.once('connect', () => resolve()));
}
return {
connect,
async disconnect(): Promise<void> {
if (this.connected) {
return new Promise(resolve => mqttClient?.end(true, undefined, () => resolve()));
}
},
async reconnect(): Promise<void> {
return new Promise(resolve => {
mqttClient?.reconnect();
resolve();
});
},
listen(callback: (state: Amity.RTEConnectionState) => void): () => void {
connectionStateListeners.add(callback);
if (currentState) callback(currentState);
return () => {
connectionStateListeners.delete(callback);
};
},
get connected() {
return !!mqttClient?.connected;
},
on<T extends keyof Amity.MqttEvents>(
event: T,
handler: (payload: Amity.MqttEvents[T]) => void,
) {
mqttClient?.on(event, handler);
},
once<T extends keyof Amity.MqttEvents>(
event: T,
handler: (payload: Amity.MqttEvents[T]) => void,
) {
mqttClient?.once(event, handler);
},
off<T extends keyof Amity.MqttEvents>(
event: T,
handler?: (payload: Amity.MqttEvents[T]) => void,
) {
if (handler !== undefined) {
mqttClient?.off(event, handler);
} else {
mqttClient?.removeAllListeners(event);
}
},
removeAllListeners() {
mqttClient?.removeAllListeners();
},
subscribe(topic: string, callback?: Amity.Listener<ASCError | void>): Amity.Unsubscriber {
const callbackWrapper = (error: Error, granted: ISubscriptionGrant[]) => {
// In MQTT.js, when you subscribe to a topic with QoS 0, the granted parameter
// in the callback will typically be empty or undefined
if (error || granted[0]?.qos === QOS_FAILURE_CODE) {
const ascError = error
? new ASCError(error.message, Amity.ClientError.UNKNOWN_ERROR, Amity.ErrorLevel.ERROR)
: // TODO throw the actual error, once BE can tell us the actual error code
new ASCUnknownError(Amity.ClientError.UNKNOWN_ERROR, Amity.ErrorLevel.ERROR);
// Use warning lv instead of error lv to prevent misunderstanding of user
console.warn(`Failed to subscribe to topic ${topic}`, ascError);
callback?.(ascError);
} else {
console.log(`Subscribed to topic ${topic}`);
callback?.();
}
};
if (mqttClient) {
mqttClient.subscribe(topic, { qos: 0 }, callbackWrapper);
} else {
callbackWrapper(new Error('No connection to broker'), []);
}
return () => mqttClient?.unsubscribe(topic);
},
unsubscribe(topic: string) {
mqttClient?.unsubscribe(topic);
},
};
};