- App.tsx: full navigation (Auth stack + Main tabs with 5 screens) - Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen - HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors - MapScreen: interactive map with layer toggles (6 layers) - MarketplaceScreen: categories (6), products (5), search - ChatScreen: AI chat with quick prompts (4), bot responses - ProfileScreen: user info, stats, menu (9 items), logout - AlertsScreen: alert list with severity, acknowledge - SensorsScreen: sensor list with type filters (6 types), search - ZonesScreen: zone cards with stats - SettingsScreen: language picker (FR/EN/ES/DE), privacy, about - Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n - Hooks: useSensors, useAlerts, useNotifications, useLocation - Components: Card, Button, LoadingSpinner, ErrorBoundary, Header - Services: iotService, notificationService (with axios API client) - Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN) - Theme: colors.ts with full design system (Blue Ocean palette) - Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
208 lines
6.7 KiB
JavaScript
208 lines
6.7 KiB
JavaScript
import { EventEmitter } from 'fbemitter';
|
|
export class WebSocketWithReconnect {
|
|
url;
|
|
retriesInterval;
|
|
maxRetries;
|
|
connectTimeout;
|
|
onError;
|
|
onReconnect;
|
|
ws = null;
|
|
retries = 0;
|
|
connectTimeoutHandle = null;
|
|
isClosed = false;
|
|
sendQueue = [];
|
|
lastCloseEvent = null;
|
|
emitter = new EventEmitter();
|
|
eventSubscriptions = [];
|
|
wsBinaryType;
|
|
constructor(url, options) {
|
|
this.url = url;
|
|
this.retriesInterval = options?.retriesInterval ?? 1500;
|
|
this.maxRetries = options?.maxRetries ?? 200;
|
|
this.connectTimeout = options?.connectTimeout ?? 5000;
|
|
this.onError =
|
|
options?.onError ??
|
|
((error) => {
|
|
throw error;
|
|
});
|
|
this.onReconnect = options?.onReconnect ?? (() => { });
|
|
this.wsBinaryType = options?.binaryType;
|
|
this.connect();
|
|
}
|
|
close(code, reason) {
|
|
this.clearConnectTimeoutIfNeeded();
|
|
this.emitter.emit('close', this.lastCloseEvent ?? {
|
|
code: code ?? 1000,
|
|
reason: reason ?? 'Explicit closing',
|
|
message: 'Explicit closing',
|
|
});
|
|
this.lastCloseEvent = null;
|
|
this.isClosed = true;
|
|
this.emitter.removeAllListeners();
|
|
this.sendQueue = [];
|
|
if (this.ws != null) {
|
|
const ws = this.ws;
|
|
this.ws = null;
|
|
this.wsClose(ws);
|
|
}
|
|
}
|
|
addEventListener(event, listener) {
|
|
this.eventSubscriptions.push(this.emitter.addListener(event, listener));
|
|
}
|
|
removeEventListener(event, listener) {
|
|
const index = this.eventSubscriptions.findIndex((subscription) => subscription.listener === listener);
|
|
if (index >= 0) {
|
|
this.eventSubscriptions[index].remove();
|
|
this.eventSubscriptions.splice(index, 1);
|
|
}
|
|
}
|
|
//#region Internals
|
|
connect() {
|
|
if (this.ws != null) {
|
|
return;
|
|
}
|
|
this.connectTimeoutHandle = setTimeout(this.handleConnectTimeout, this.connectTimeout);
|
|
this.ws = new WebSocket(this.url.toString());
|
|
if (this.wsBinaryType != null) {
|
|
this.ws.binaryType = this.wsBinaryType;
|
|
}
|
|
this.ws.addEventListener('message', this.handleMessage);
|
|
this.ws.addEventListener('open', this.handleOpen);
|
|
// @ts-ignore TypeScript expects (e: Event) => any, but we want (e: WebSocketErrorEvent) => any
|
|
this.ws.addEventListener('error', this.handleError);
|
|
this.ws.addEventListener('close', this.handleClose);
|
|
}
|
|
send(data) {
|
|
if (this.isClosed) {
|
|
this.onError(new Error('Unable to send data: WebSocket is closed'));
|
|
return;
|
|
}
|
|
if (this.retries >= this.maxRetries) {
|
|
this.onError(new Error(`Unable to send data: Exceeded max retries - retries[${this.retries}]`));
|
|
return;
|
|
}
|
|
const ws = this.ws;
|
|
if (ws != null && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(data);
|
|
}
|
|
else {
|
|
this.sendQueue.push(data);
|
|
}
|
|
}
|
|
handleOpen = () => {
|
|
this.clearConnectTimeoutIfNeeded();
|
|
this.lastCloseEvent = null;
|
|
this.emitter.emit('open');
|
|
const sendQueue = this.sendQueue;
|
|
this.sendQueue = [];
|
|
for (const data of sendQueue) {
|
|
this.send(data);
|
|
}
|
|
};
|
|
handleMessage = (event) => {
|
|
this.emitter.emit('message', event);
|
|
};
|
|
handleError = (event) => {
|
|
this.clearConnectTimeoutIfNeeded();
|
|
this.emitter.emit('error', event);
|
|
this.reconnectIfNeeded(`WebSocket error - ${event.message}`);
|
|
};
|
|
handleClose = (event) => {
|
|
this.clearConnectTimeoutIfNeeded();
|
|
this.lastCloseEvent = {
|
|
code: event.code,
|
|
reason: event.reason,
|
|
message: event.message,
|
|
};
|
|
this.reconnectIfNeeded(`WebSocket closed - code[${event.code}] reason[${event.reason}]`);
|
|
};
|
|
handleConnectTimeout = () => {
|
|
this.reconnectIfNeeded('Timeout from connecting to the WebSocket');
|
|
};
|
|
clearConnectTimeoutIfNeeded() {
|
|
if (this.connectTimeoutHandle != null) {
|
|
clearTimeout(this.connectTimeoutHandle);
|
|
this.connectTimeoutHandle = null;
|
|
}
|
|
}
|
|
reconnectIfNeeded(reason) {
|
|
if (this.ws != null) {
|
|
this.wsClose(this.ws);
|
|
this.ws = null;
|
|
}
|
|
if (this.isClosed) {
|
|
return;
|
|
}
|
|
if (this.retries >= this.maxRetries) {
|
|
this.onError(new Error('Exceeded max retries'));
|
|
this.close();
|
|
return;
|
|
}
|
|
setTimeout(() => {
|
|
this.retries += 1;
|
|
this.connect();
|
|
this.onReconnect(reason);
|
|
}, this.retriesInterval);
|
|
}
|
|
wsClose(ws) {
|
|
try {
|
|
ws.removeEventListener('message', this.handleMessage);
|
|
ws.removeEventListener('open', this.handleOpen);
|
|
// @ts-ignore: TypeScript expects (e: Event) => any, but we want (e: WebSocketErrorEvent) => any
|
|
ws.removeEventListener('error', this.handleError);
|
|
ws.removeEventListener('close', this.handleClose);
|
|
ws.close();
|
|
}
|
|
catch { }
|
|
}
|
|
get readyState() {
|
|
// Only return closed if the WebSocket is explicitly closed or exceeds max retries.
|
|
if (this.isClosed) {
|
|
return WebSocket.CLOSED;
|
|
}
|
|
const readyState = this.ws?.readyState;
|
|
if (readyState === WebSocket.CLOSED) {
|
|
return WebSocket.CONNECTING;
|
|
}
|
|
return readyState ?? WebSocket.CONNECTING;
|
|
}
|
|
//#endregion
|
|
//#region WebSocket API proxy
|
|
CONNECTING = 0;
|
|
OPEN = 1;
|
|
CLOSING = 2;
|
|
CLOSED = 3;
|
|
get binaryType() {
|
|
return this.ws?.binaryType ?? 'blob';
|
|
}
|
|
get bufferedAmount() {
|
|
return this.ws?.bufferedAmount ?? 0;
|
|
}
|
|
get extensions() {
|
|
return this.ws?.extensions ?? '';
|
|
}
|
|
get protocol() {
|
|
return this.ws?.protocol ?? '';
|
|
}
|
|
ping() {
|
|
return this.ws?.ping();
|
|
}
|
|
dispatchEvent(event) {
|
|
return this.ws?.dispatchEvent(event) ?? false;
|
|
}
|
|
//#endregion
|
|
//#regions Unsupported legacy properties
|
|
set onclose(value) {
|
|
throw new Error('Unsupported legacy property, use addEventListener instead');
|
|
}
|
|
set onerror(value) {
|
|
throw new Error('Unsupported legacy property, use addEventListener instead');
|
|
}
|
|
set onmessage(value) {
|
|
throw new Error('Unsupported legacy property, use addEventListener instead');
|
|
}
|
|
set onopen(value) {
|
|
throw new Error('Unsupported legacy property, use addEventListener instead');
|
|
}
|
|
}
|
|
//# sourceMappingURL=WebSocketWithReconnect.js.map
|