chore: directory migration - gateway→server, web→client/electron
This commit is contained in:
308
client/harmony/entry/src/main/ets/api/SseService.ets
Normal file
308
client/harmony/entry/src/main/ets/api/SseService.ets
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* AgentMail 鸿蒙客户端 — SSE 多账号实时推送服务
|
||||
* 每个账号各建一条 SSE 连接(各带自己的 user_key)
|
||||
* AccountManager 维护连接集合,按 accountId 分发事件
|
||||
*/
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { AccountManager, AccountInfo } from './AccountManager';
|
||||
|
||||
const DOMAIN = 0x0001;
|
||||
const TAG = 'SseService';
|
||||
|
||||
/** SSE 事件数据 */
|
||||
export class SseEvent {
|
||||
type: string = '';
|
||||
data: string = '';
|
||||
accountId: string = '';
|
||||
}
|
||||
|
||||
/** SSE 回调 */
|
||||
export type SseListener = (event: SseEvent) => void;
|
||||
|
||||
/** SSE 连接状态 */
|
||||
export type SseStatus = 'disconnected' | 'connecting' | 'connected';
|
||||
|
||||
/** SSE 状态回调 */
|
||||
export type SseStatusListener = (status: SseStatus) => void;
|
||||
|
||||
/** 单个账号的 SSE 连接状态(纯数据) */
|
||||
export class SseConnection {
|
||||
accountId: string = '';
|
||||
server: string = '';
|
||||
token: string = '';
|
||||
httpRequest: http.HttpRequest | null = null;
|
||||
listeners: SseListener[] = [];
|
||||
status: SseStatus = 'disconnected';
|
||||
reconnectTimer: number = 0;
|
||||
buffer: string = '';
|
||||
connected: boolean = false;
|
||||
}
|
||||
|
||||
export class SseService {
|
||||
private static instance: SseService | null = null;
|
||||
private connections: Map<string, SseConnection> = new Map();
|
||||
private globalListeners: SseListener[] = [];
|
||||
private globalStatusListeners: SseStatusListener[] = [];
|
||||
|
||||
static getInstance(): SseService {
|
||||
if (SseService.instance === null) {
|
||||
SseService.instance = new SseService();
|
||||
}
|
||||
return SseService.instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/** 添加全局事件监听(所有账号的事件都会收到) */
|
||||
addListener(listener: SseListener): void {
|
||||
this.globalListeners.push(listener);
|
||||
}
|
||||
|
||||
/** 移除全局事件监听 */
|
||||
removeListener(listener: SseListener): void {
|
||||
const idx: number = this.globalListeners.indexOf(listener);
|
||||
if (idx >= 0) {
|
||||
this.globalListeners.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加全局状态监听 */
|
||||
addStatusListener(listener: SseStatusListener): void {
|
||||
this.globalStatusListeners.push(listener);
|
||||
}
|
||||
|
||||
/** 移除全局状态监听 */
|
||||
removeStatusListener(listener: SseStatusListener): void {
|
||||
const idx: number = this.globalStatusListeners.indexOf(listener);
|
||||
if (idx >= 0) {
|
||||
this.globalStatusListeners.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** 为指定账号建立 SSE 连接 */
|
||||
connectForAccount(accountId: string, server: string, token: string): void {
|
||||
let conn: SseConnection | undefined = this.connections.get(accountId);
|
||||
if (conn !== undefined && conn.connected) {
|
||||
hilog.info(DOMAIN, TAG, 'account %{public}s already connected', accountId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (conn === undefined) {
|
||||
conn = new SseConnection();
|
||||
conn.accountId = accountId;
|
||||
this.connections.set(accountId, conn);
|
||||
}
|
||||
|
||||
conn.server = server;
|
||||
conn.token = token;
|
||||
conn.connected = true;
|
||||
this.setConnStatus(conn, 'connecting');
|
||||
this.doConnect(conn);
|
||||
}
|
||||
|
||||
/** 断开指定账号的 SSE 连接 */
|
||||
disconnectAccount(accountId: string): void {
|
||||
const conn: SseConnection | undefined = this.connections.get(accountId);
|
||||
if (conn === undefined) {
|
||||
return;
|
||||
}
|
||||
conn.connected = false;
|
||||
if (conn.reconnectTimer !== 0) {
|
||||
clearTimeout(conn.reconnectTimer);
|
||||
conn.reconnectTimer = 0;
|
||||
}
|
||||
if (conn.httpRequest !== null) {
|
||||
conn.httpRequest.destroy();
|
||||
conn.httpRequest = null;
|
||||
}
|
||||
this.setConnStatus(conn, 'disconnected');
|
||||
this.connections.delete(accountId);
|
||||
}
|
||||
|
||||
/** 断开所有连接 */
|
||||
disconnectAll(): void {
|
||||
const keys: string[] = [];
|
||||
this.connections.forEach((_conn: SseConnection, key: string) => {
|
||||
keys.push(key);
|
||||
});
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.disconnectAccount(keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据 AccountManager 连接所有账号 */
|
||||
async connectAll(acctMgr: AccountManager): Promise<void> {
|
||||
await acctMgr.load();
|
||||
const accounts: AccountInfo[] = acctMgr.getAccounts();
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const acct: AccountInfo = accounts[i];
|
||||
this.connectForAccount(acct.id, acct.server, acct.token);
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取指定账号的连接状态 */
|
||||
getStatusForAccount(accountId: string): SseStatus {
|
||||
const conn: SseConnection | undefined = this.connections.get(accountId);
|
||||
if (conn === undefined) {
|
||||
return 'disconnected';
|
||||
}
|
||||
return conn.status;
|
||||
}
|
||||
|
||||
/** 给指定账号添加事件监听 */
|
||||
addListenerForAccount(accountId: string, listener: SseListener): void {
|
||||
let conn: SseConnection | undefined = this.connections.get(accountId);
|
||||
if (conn === undefined) {
|
||||
conn = new SseConnection();
|
||||
conn.accountId = accountId;
|
||||
this.connections.set(accountId, conn);
|
||||
}
|
||||
conn.listeners.push(listener);
|
||||
}
|
||||
|
||||
/** 移除指定账号的事件监听 */
|
||||
removeListenerForAccount(accountId: string, listener: SseListener): void {
|
||||
const conn: SseConnection | undefined = this.connections.get(accountId);
|
||||
if (conn !== undefined) {
|
||||
const idx: number = conn.listeners.indexOf(listener);
|
||||
if (idx >= 0) {
|
||||
conn.listeners.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setConnStatus(conn: SseConnection, status: SseStatus): void {
|
||||
if (conn.status !== status) {
|
||||
conn.status = status;
|
||||
hilog.info(DOMAIN, TAG, 'status[%{public}s]: %{public}s', conn.accountId, status);
|
||||
for (let i = 0; i < this.globalStatusListeners.length; i++) {
|
||||
this.globalStatusListeners[i](status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doConnect(conn: SseConnection): void {
|
||||
if (!conn.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpRequest = http.createHttp();
|
||||
conn.httpRequest = httpRequest;
|
||||
|
||||
const url: string = conn.server + '/events/stream';
|
||||
const header: Record<string, string> = {};
|
||||
if (conn.token.length > 0) {
|
||||
header['Authorization'] = 'Bearer ' + conn.token;
|
||||
}
|
||||
|
||||
hilog.info(DOMAIN, TAG, 'connecting account %{public}s to %{public}s', conn.accountId, url);
|
||||
|
||||
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
|
||||
const text: string = this.arrayBufferToString(data);
|
||||
conn.buffer += text;
|
||||
this.processBuffer(conn);
|
||||
});
|
||||
|
||||
httpRequest.on('dataEnd', () => {
|
||||
hilog.info(DOMAIN, TAG, 'dataEnd for account %{public}s', conn.accountId);
|
||||
this.setConnStatus(conn, 'disconnected');
|
||||
this.scheduleReconnect(conn);
|
||||
});
|
||||
|
||||
httpRequest.on('headersReceive', (_headers: Object) => {
|
||||
hilog.info(DOMAIN, TAG, 'headers received for account %{public}s', conn.accountId);
|
||||
});
|
||||
|
||||
const options: http.HttpRequestOptions = {
|
||||
method: http.RequestMethod.GET,
|
||||
header: header,
|
||||
connectTimeout: 10000,
|
||||
readTimeout: 0
|
||||
};
|
||||
|
||||
httpRequest.requestInStream(url, options, (err: BusinessError, code: number) => {
|
||||
if (err !== undefined && err !== null) {
|
||||
hilog.error(DOMAIN, TAG, 'requestInStream error account %{public}s: %{public}s', conn.accountId, err.message);
|
||||
this.setConnStatus(conn, 'disconnected');
|
||||
this.scheduleReconnect(conn);
|
||||
return;
|
||||
}
|
||||
hilog.info(DOMAIN, TAG, 'requestInStream code=%{public}d account=%{public}s', code, conn.accountId);
|
||||
if (code === 200) {
|
||||
this.setConnStatus(conn, 'connected');
|
||||
} else {
|
||||
hilog.error(DOMAIN, TAG, 'SSE failed code=%{public}d account=%{public}s', code, conn.accountId);
|
||||
this.setConnStatus(conn, 'disconnected');
|
||||
this.scheduleReconnect(conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private processBuffer(conn: SseConnection): void {
|
||||
const lines: string[] = conn.buffer.split('\n');
|
||||
conn.buffer = lines.pop() ?? '';
|
||||
|
||||
let eventType: string = '';
|
||||
let eventData: string = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line: string = lines[i];
|
||||
|
||||
if (line.length === 0) {
|
||||
if (eventType.length > 0 || eventData.length > 0) {
|
||||
const event: SseEvent = new SseEvent();
|
||||
event.type = eventType.length > 0 ? eventType : 'message';
|
||||
event.data = eventData;
|
||||
event.accountId = conn.accountId;
|
||||
this.dispatchEvent(conn, event);
|
||||
}
|
||||
eventType = '';
|
||||
eventData = '';
|
||||
} else if (line.startsWith('event:')) {
|
||||
eventType = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:')) {
|
||||
const newData: string = line.substring(5).trim();
|
||||
if (eventData.length > 0) {
|
||||
eventData += '\n' + newData;
|
||||
} else {
|
||||
eventData = newData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchEvent(conn: SseConnection, event: SseEvent): void {
|
||||
hilog.info(DOMAIN, TAG, 'event[%{public}s]: %{public}s data: %{public}s', conn.accountId, event.type,
|
||||
event.data.substring(0, 100));
|
||||
// 分发给账号级监听
|
||||
for (let i = 0; i < conn.listeners.length; i++) {
|
||||
conn.listeners[i](event);
|
||||
}
|
||||
// 分发给全局监听
|
||||
for (let i = 0; i < this.globalListeners.length; i++) {
|
||||
this.globalListeners[i](event);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(conn: SseConnection): void {
|
||||
if (!conn.connected) {
|
||||
return;
|
||||
}
|
||||
const timer: number | undefined = setTimeout(() => {
|
||||
conn.reconnectTimer = 0;
|
||||
this.doConnect(conn);
|
||||
}, 3000);
|
||||
conn.reconnectTimer = timer ?? 0;
|
||||
}
|
||||
|
||||
private arrayBufferToString(buffer: ArrayBuffer): string {
|
||||
const uint8Array: Uint8Array = new Uint8Array(buffer);
|
||||
let result: string = '';
|
||||
for (let i = 0; i < uint8Array.length; i++) {
|
||||
result += String.fromCharCode(uint8Array[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user