From faba6f3c127cd95403951b34af299d46d3dabd1a Mon Sep 17 00:00:00 2001 From: "qikang.yuan" Date: Sun, 7 Apr 2024 17:10:30 +0800 Subject: [PATCH 1/2] a simpler implementation of socket interface for different platform --- packages/base/src/event-emitter.ts | 43 ++++++++++ packages/base/src/socket-base.ts | 81 ++++++------------- packages/base/tests/network/base.test.ts | 25 ++++-- packages/mp-base/src/helpers/socket.ts | 66 +++++++++------ packages/mp-base/src/index.ts | 6 +- .../page-spy-browser/src/helpers/socket.ts | 47 ++--------- packages/page-spy-browser/tests/init.test.ts | 4 +- packages/page-spy-uniapp/src/index.ts | 4 +- 8 files changed, 139 insertions(+), 137 deletions(-) create mode 100644 packages/base/src/event-emitter.ts diff --git a/packages/base/src/event-emitter.ts b/packages/base/src/event-emitter.ts new file mode 100644 index 00000000..49584b3d --- /dev/null +++ b/packages/base/src/event-emitter.ts @@ -0,0 +1,43 @@ +// There are many where use event emitter, but the platform may not +// natively support it, so we made our own. +export class EventEmitter { + private events: Map = new Map(); + + on(eventName: E, callback: C) { + let list = this.events.get(eventName); + if (!list) { + list = []; + this.events.set(eventName, list); + } + if (list.indexOf(callback) < 0) { + list.push(callback); + } + } + + off(eventName: E, callback: C) { + if (this.events.has(eventName)) { + const list = this.events.get(eventName); + const i = list!.indexOf(callback); + if (i >= 0) { + list?.splice(i, 1); + } + } + } + + emit(eventName: E, ...args: any[]) { + const list = this.events.get(eventName); + if (list) { + list.forEach((cb) => { + cb(...args); + }); + } + } + + clearListeners(eventName: E) { + this.events.delete(eventName); + } + + clearAllListeners() { + this.events.clear(); + } +} diff --git a/packages/base/src/socket-base.ts b/packages/base/src/socket-base.ts index e8450ad4..078800e6 100644 --- a/packages/base/src/socket-base.ts +++ b/packages/base/src/socket-base.ts @@ -26,7 +26,7 @@ interface GetterMember { export type WebSocketEvents = keyof WebSocketEventMap; type CallbackType = (data?: any) => void; -// fork WebSocket state +// WebSocket state. Enum defs for WebSocket readyState export enum SocketState { CONNECTING = 0, OPEN = 1, @@ -36,55 +36,17 @@ export enum SocketState { const HEARTBEAT_INTERVAL = 5000; -// 封装不同平台的 socket -export abstract class SocketWrapper { - abstract init(url: string): void; - abstract send(data: string): void; - abstract close(data?: {}): void; - abstract getState(): SocketState; - protected events: Record = { - open: [], - close: [], - error: [], - message: [], - }; - - protected emit(event: WebSocketEvents, data: any) { - this.events[event].forEach((fun) => { - fun(data); - }); - // for close and error, clear all listeners or they will be called on next socket instance. - if (event === 'close' || event === 'error') { - this.clearListeners(); - } - } - - onOpen(fun: (res: { header?: Record }) => void) { - this.events.open.push(fun); - } - - onClose(fun: (res: { code: number; reason: string }) => void) { - this.events.close.push(fun); - } - - onError(fun: (msg: string) => void) { - this.events.error.push(fun); - } - - onMessage(fun: (data: string | ArrayBuffer) => void) { - this.events.message.push(fun); - } - - protected clearListeners() { - // clear listeners - Object.entries(this.events).forEach(([, funs]) => { - funs.splice(0); - }); - } +// socket interface, be consistent with websocket. +export interface ISocket { + send(data: string): void; + close(): void; + addEventListener(event: WebSocketEvents, callback: CallbackType): void; + removeEventListener(event: WebSocketEvents, callback: CallbackType): void; + readonly readyState: SocketState; } export abstract class SocketStoreBase { - protected abstract socketWrapper: SocketWrapper; + protected socket: ISocket | null = null; private socketUrl: string = ''; @@ -133,35 +95,42 @@ export abstract class SocketStoreBase { this.addListener('debugger-online', this.handleFlushBuffer); } + public getSocket() { + return this.socket; + } + // Simple offline listener abstract onOffline(): void; + // Create socket instance of different platform implementations + abstract createSocket(url: string): ISocket; + public init(url: string) { try { if (!url) { throw Error('WebSocket url cannot be empty'); } // close existing connection - if (this.socketWrapper.getState() === SocketState.OPEN) { - this.socketWrapper.close(); + if (this.socket?.readyState === SocketState.OPEN) { + this.socket.close(); } - this.socketWrapper?.onOpen(() => { + this.socket = this.createSocket(url); + this.socket.addEventListener('open', () => { this.connectOnline(); }); // Strictly, the onMessage should be called after onOpen. But for some platform(alipay,) // this may cause some message losing. - this.socketWrapper?.onMessage((evt) => { + this.socket.addEventListener('message', (evt) => { this.handleMessage(evt); }); - this.socketWrapper?.onClose(() => { + this.socket.addEventListener('close', () => { this.connectOffline(); }); - this.socketWrapper?.onError(() => { + this.socket.addEventListener('error', () => { // we treat on error the same with on close. this.connectOffline(); }); this.socketUrl = url; - this.socketWrapper?.init(url); } catch (e: any) { psLog.error(e.message); } @@ -195,7 +164,7 @@ export abstract class SocketStoreBase { this.clearPing(); this.reconnectTimes = 0; this.reconnectable = false; - this.socketWrapper?.close(); + this.socket?.close(); this.messages = []; Object.entries(this.events).forEach(([, fns]) => { fns.splice(0); @@ -433,7 +402,7 @@ export abstract class SocketStoreBase { pkMsg.createdAt = Date.now(); pkMsg.requestId = getRandomId(); const dataString = stringifyData(pkMsg); - this.socketWrapper?.send(dataString); + this.socket?.send(dataString); } catch (e) { throw Error(`Incompatible: ${(e as Error).message}`); } diff --git a/packages/base/tests/network/base.test.ts b/packages/base/tests/network/base.test.ts index e34dacee..86b63f17 100644 --- a/packages/base/tests/network/base.test.ts +++ b/packages/base/tests/network/base.test.ts @@ -1,12 +1,12 @@ -import { - SocketState, - SocketStoreBase, - SocketWrapper, -} from 'base/src/socket-base'; +import { SocketState, SocketStoreBase, ISocket } from 'base/src/socket-base'; import NetworkProxyBase from 'base/src/network/base'; import RequestItem from 'base/src/request-item'; -class PlatformSocketWrapper extends SocketWrapper { +class PlatformSocketImpl implements ISocket { + readyState: SocketState = 0; + constructor(url: string) { + this.init(url); + } init(url: string): void { throw new Error('Method not implemented.'); } @@ -22,10 +22,19 @@ class PlatformSocketWrapper extends SocketWrapper { getState(): SocketState { throw new Error('Method not implemented.'); } + addEventListener( + event: keyof WebSocketEventMap, + callback: (data?: any) => void, + ): void {} + removeEventListener( + event: keyof WebSocketEventMap, + callback: (data?: any) => void, + ): void {} } - class PlatformSocket extends SocketStoreBase { - protected socketWrapper: SocketWrapper = new PlatformSocketWrapper(); + createSocket(url: string): ISocket { + return new PlatformSocketImpl(url); + } onOffline(): void { throw new Error('Method not implemented.'); } diff --git a/packages/mp-base/src/helpers/socket.ts b/packages/mp-base/src/helpers/socket.ts index 7cf91dae..e88835e0 100644 --- a/packages/mp-base/src/helpers/socket.ts +++ b/packages/mp-base/src/helpers/socket.ts @@ -1,39 +1,44 @@ import { ROOM_SESSION_KEY } from 'base/src/constants'; -import { - SocketStoreBase, - SocketState, - SocketWrapper, -} from 'base/src/socket-base'; +import { SocketStoreBase, SocketState, ISocket } from 'base/src/socket-base'; import { getMPSDK, utilAPI } from '../utils'; +import { EventEmitter } from 'base/src/event-emitter'; -export class MPSocketWrapper extends SocketWrapper { +// Mini program implementation of ISocket +export class MPSocketImpl + extends EventEmitter<'open' | 'close' | 'error' | 'message', Function> + implements ISocket +{ private socketInstance: MPSocket | null = null; - private state: SocketState = 0; + private _state: SocketState = SocketState.CONNECTING; + + get readyState() { + return this._state; + } // some ali-family app only support single socket connection... public static isSingleSocket = false; - init(url: string) { - this.state = SocketState.CONNECTING; + constructor(url: string) { + super(); + this._state = SocketState.CONNECTING; const mp = getMPSDK(); const closeHandler: SocketOnCloseHandler = (data) => { - this.state = SocketState.CLOSED; + this._state = SocketState.CLOSED; this.emit('close', data); }; const openHandler: SocketOnOpenHandler = (data) => { - this.state = SocketState.OPEN; + this._state = SocketState.OPEN; this.emit('open', data); }; const errorHandler: SocketOnErrorHandler = (data) => { - this.state = SocketState.CLOSED; + this._state = SocketState.CLOSED; this.emit('error', data); }; const messageHandler: SocketOnMessageHandler = (data) => { this.emit('message', data); }; - - if (!MPSocketWrapper.isSingleSocket) { + if (!MPSocketImpl.isSingleSocket) { this.socketInstance = mp.connectSocket({ url, multiple: true, // for alipay mp to return a task @@ -53,7 +58,7 @@ export class MPSocketWrapper extends SocketWrapper { } send(data: string) { - if (MPSocketWrapper.isSingleSocket) { + if (MPSocketImpl.isSingleSocket) { getMPSDK().sendSocketMessage({ data }); } else { this.socketInstance?.send({ @@ -63,27 +68,36 @@ export class MPSocketWrapper extends SocketWrapper { } close() { - if (MPSocketWrapper.isSingleSocket) { + if (MPSocketImpl.isSingleSocket) { getMPSDK().closeSocket({}); } else { this.socketInstance?.close({}); } - this.state = SocketState.CLOSED; - this.clearListeners(); + this._state = SocketState.CLOSED; + this.clearAllListeners(); } getState(): SocketState { - return this.state; + return this._state; + } + + addEventListener( + event: keyof WebSocketEventMap, + callback: (data?: any) => void, + ): void { + this.on(event, callback); + } + + removeEventListener( + event: keyof WebSocketEventMap, + callback: (data?: any) => void, + ): void { + this.off(event, callback); } } export class MPSocketStore extends SocketStoreBase { // websocket socketInstance - protected socketWrapper = new MPSocketWrapper(); - - public getSocket() { - return this.socketWrapper; - } constructor() { super(); @@ -94,6 +108,10 @@ export class MPSocketStore extends SocketStoreBase { onOffline() { utilAPI.setStorage(ROOM_SESSION_KEY, JSON.stringify({ usable: false })); } + + createSocket(url: string): ISocket { + return new MPSocketImpl(url); + } } export default new MPSocketStore(); diff --git a/packages/mp-base/src/index.ts b/packages/mp-base/src/index.ts index acaa5253..c069959b 100644 --- a/packages/mp-base/src/index.ts +++ b/packages/mp-base/src/index.ts @@ -15,7 +15,7 @@ import NetworkPlugin from './plugins/network'; import SystemPlugin from './plugins/system'; import StoragePlugin from './plugins/storage'; -import socketStore, { MPSocketWrapper } from './helpers/socket'; +import socketStore, { MPSocketImpl } from './helpers/socket'; import Request from './api'; // import './index.less'; @@ -100,7 +100,7 @@ class PageSpy { const config = this.config.mergeConfig(init); if (config.singletonSocket) { - MPSocketWrapper.isSingleSocket = true; + MPSocketImpl.isSingleSocket = true; } const mp = getMPSDK(); @@ -174,7 +174,7 @@ class PageSpy { mp.onAppShow(() => { // Mini programe can not detect ws disconnect (before we add heart beat ping pong). // So we need to refresh the connection. - const state = socketStore.getSocket().getState(); + const state = socketStore.getSocket()?.readyState; if (state === SocketState.CLOSED || state === SocketState.CLOSING) { this.useOldConnection(); } diff --git a/packages/page-spy-browser/src/helpers/socket.ts b/packages/page-spy-browser/src/helpers/socket.ts index f7131617..8888fdde 100644 --- a/packages/page-spy-browser/src/helpers/socket.ts +++ b/packages/page-spy-browser/src/helpers/socket.ts @@ -2,50 +2,9 @@ import { getRandomId, stringifyData } from 'base/src'; import atom from 'base/src/atom'; import { ROOM_SESSION_KEY } from 'base/src/constants'; import { makeMessage } from 'base/src/message'; -import { - SocketStoreBase, - SocketState, - SocketWrapper, - WebSocketEvents, -} from 'base/src/socket-base'; +import { ISocket, SocketStoreBase } from 'base/src/socket-base'; import { SpyBase } from 'packages/page-spy-types'; - -export class WebSocketWrapper extends SocketWrapper { - private socketInstance: WebSocket | null = null; - - init(url: string) { - this.socketInstance = new WebSocket(url); - const eventNames: WebSocketEvents[] = ['open', 'close', 'error', 'message']; - eventNames.forEach((eventName) => { - this.socketInstance!.addEventListener(eventName, (data) => { - this.events[eventName].forEach((cb) => { - cb(data); - }); - }); - }); - } - - send(data: string) { - this.socketInstance?.send(stringifyData(data)); - } - - close() { - this.socketInstance?.close(); - } - - getState(): SocketState { - return this.socketInstance?.readyState as SocketState; - } -} - export class WebSocketStore extends SocketStoreBase { - // websocket instance - protected socketWrapper: WebSocketWrapper = new WebSocketWrapper(); - - public getSocket() { - return this.socketWrapper; - } - // disable lint: this is an abstract method of parent class, so it cannot be static // eslint-disable-next-line class-methods-use-this onOffline(): void { @@ -53,6 +12,10 @@ export class WebSocketStore extends SocketStoreBase { sessionStorage.setItem(ROOM_SESSION_KEY, JSON.stringify({ usable: false })); } + createSocket(url: string): ISocket { + return new WebSocket(url); + } + // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor() { super(); diff --git a/packages/page-spy-browser/tests/init.test.ts b/packages/page-spy-browser/tests/init.test.ts index 850f2495..243a781b 100644 --- a/packages/page-spy-browser/tests/init.test.ts +++ b/packages/page-spy-browser/tests/init.test.ts @@ -192,7 +192,7 @@ describe('new PageSpy([config])', () => { JSON.stringify({ name: '', address: '', - roomUrl: '', + roomUrl: 'demo-url', // need a non-empty value to avoid exception usable: true, project: 'default', }), @@ -212,7 +212,7 @@ describe('new PageSpy([config])', () => { JSON.stringify({ name: '', address: '', - roomUrl: '', + roomUrl: 'demo-url', // need a non-empty value to avoid exception usable: false, project: 'default', }), diff --git a/packages/page-spy-uniapp/src/index.ts b/packages/page-spy-uniapp/src/index.ts index bc01ceb9..498fb40d 100644 --- a/packages/page-spy-uniapp/src/index.ts +++ b/packages/page-spy-uniapp/src/index.ts @@ -4,7 +4,7 @@ import Device from 'mp-base/src/device'; import { SpyDevice } from 'packages/page-spy-types'; import { SocketStoreBase } from 'base/src/socket-base'; import { psLog } from 'base/src'; -import { MPSocketWrapper } from 'mp-base/src/helpers/socket'; +import { MPSocketImpl } from 'mp-base/src/helpers/socket'; declare const uni: any; @@ -60,7 +60,7 @@ if ( info.uniPlatform === 'mp-alipay' && (info.hostName === 'DingTalk' || info.hostName === 'mPaaS') ) { - MPSocketWrapper.isSingleSocket = true; + MPSocketImpl.isSingleSocket = true; } // Really disgusting... alipay mp has different message format even in uniapp... From 9ca784b5e6a44750c336e3ba2a82609fd12f7a4b Mon Sep 17 00:00:00 2001 From: "qikang.yuan" Date: Sun, 7 Apr 2024 17:32:42 +0800 Subject: [PATCH 2/2] more simplified code --- package.json | 1 - packages/base/src/event-emitter.ts | 4 ++-- packages/mp-base/src/helpers/socket.ts | 14 -------------- packages/mp-base/tests/mock/mp.ts | 10 +++++----- yarn.lock | 10 ---------- 5 files changed, 7 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index f469bd60..5eb26961 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-plugin-import": "^2.29.1", - "events": "^3.3.0", "express": "^4.18.2", "fake-indexeddb": "^5.0.2", "jest": "^29.7.0", diff --git a/packages/base/src/event-emitter.ts b/packages/base/src/event-emitter.ts index 49584b3d..5a0d20db 100644 --- a/packages/base/src/event-emitter.ts +++ b/packages/base/src/event-emitter.ts @@ -3,7 +3,7 @@ export class EventEmitter { private events: Map = new Map(); - on(eventName: E, callback: C) { + addEventListener(eventName: E, callback: C) { let list = this.events.get(eventName); if (!list) { list = []; @@ -14,7 +14,7 @@ export class EventEmitter { } } - off(eventName: E, callback: C) { + removeEventListener(eventName: E, callback: C) { if (this.events.has(eventName)) { const list = this.events.get(eventName); const i = list!.indexOf(callback); diff --git a/packages/mp-base/src/helpers/socket.ts b/packages/mp-base/src/helpers/socket.ts index e88835e0..c8ff9e9b 100644 --- a/packages/mp-base/src/helpers/socket.ts +++ b/packages/mp-base/src/helpers/socket.ts @@ -80,20 +80,6 @@ export class MPSocketImpl getState(): SocketState { return this._state; } - - addEventListener( - event: keyof WebSocketEventMap, - callback: (data?: any) => void, - ): void { - this.on(event, callback); - } - - removeEventListener( - event: keyof WebSocketEventMap, - callback: (data?: any) => void, - ): void { - this.off(event, callback); - } } export class MPSocketStore extends SocketStoreBase { diff --git a/packages/mp-base/tests/mock/mp.ts b/packages/mp-base/tests/mock/mp.ts index eaf4644a..6ac4d46f 100644 --- a/packages/mp-base/tests/mock/mp.ts +++ b/packages/mp-base/tests/mock/mp.ts @@ -1,6 +1,6 @@ import { SocketState } from 'base/src/socket-base'; import { mockRequest } from './request'; -import EventEmitter from 'events'; +import { EventEmitter } from 'base/src/event-emitter'; type CBType = (event?: any) => any; export class MockSocket { @@ -9,16 +9,16 @@ export class MockSocket { send(params: { data: string | Buffer }) {} onOpen(handler: (res: any) => void) { this.status = SocketState.OPEN; - this.ee.addListener('open', handler); + this.ee.addEventListener('open', handler); } onClose(handler: (res: any) => void) { - this.ee.addListener('close', handler); + this.ee.addEventListener('close', handler); } onError(handler: (msg: string) => void) { - this.ee.addListener('error', handler); + this.ee.addEventListener('error', handler); } onMessage(handler: (data: string | Buffer) => void) { - this.ee.addListener('message', handler); + this.ee.addEventListener('message', handler); } close() { if (this.status !== SocketState.CLOSED) { diff --git a/yarn.lock b/yarn.lock index aea35247..4e48ddc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1078,11 +1078,6 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== -"@huolala-tech/page-spy-types@^1.7.6": - version "1.7.6" - resolved "https://registry.npmmirror.com/@huolala-tech/page-spy-types/-/page-spy-types-1.7.6.tgz#cfee6b2241838fba6b21a198ebb85a7349ada513" - integrity sha512-9tgxKu8DbcHobS8poM7QxrhoKH6L6/77SfymnSvKRJsd9CctDn11IrNXKHdve78HVnCz65PkXVdhXDwNnWnhOg== - "@hutson/parse-repository-url@^3.0.0": version "3.0.2" resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" @@ -4223,11 +4218,6 @@ eventemitter3@^5.0.1: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - execa@5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"