mockSimulatorDebugLogger.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. const DEFAULT_DEBUG_LOG_URL = 'wss://gs.gotomars.xyz/debug-log'
  2. const MAX_QUEUED_LOGS = 80
  3. export type MockSimulatorDebugLogLevel = 'info' | 'warn' | 'error'
  4. export interface MockSimulatorDebugLoggerState {
  5. enabled: boolean
  6. connected: boolean
  7. connecting: boolean
  8. url: string
  9. statusText: string
  10. }
  11. export interface MockSimulatorDebugLogEntry {
  12. type: 'debug-log'
  13. timestamp: number
  14. channelId?: string
  15. scope: string
  16. level: MockSimulatorDebugLogLevel
  17. message: string
  18. payload?: Record<string, unknown>
  19. }
  20. function normalizeMockSimulatorChannelId(rawChannelId: string | null | undefined): string {
  21. const trimmed = String(rawChannelId || '').trim()
  22. return trimmed || 'default'
  23. }
  24. function normalizeMockSimulatorLogUrl(rawUrl: string): string {
  25. const trimmed = String(rawUrl || '').trim()
  26. if (!trimmed) {
  27. return DEFAULT_DEBUG_LOG_URL
  28. }
  29. let normalized = trimmed
  30. if (!/^wss?:\/\//i.test(normalized)) {
  31. normalized = `ws://${normalized.replace(/^\/+/, '')}`
  32. }
  33. if (!/\/debug-log(?:\?.*)?$/i.test(normalized)) {
  34. normalized = normalized.replace(/\/+$/, '')
  35. normalized = `${normalized}/debug-log`
  36. }
  37. return normalized
  38. }
  39. export class MockSimulatorDebugLogger {
  40. socketTask: WechatMiniprogram.SocketTask | null
  41. enabled: boolean
  42. connected: boolean
  43. connecting: boolean
  44. url: string
  45. channelId: string
  46. queue: MockSimulatorDebugLogEntry[]
  47. onStateChange?: (state: MockSimulatorDebugLoggerState) => void
  48. constructor(onStateChange?: (state: MockSimulatorDebugLoggerState) => void) {
  49. this.socketTask = null
  50. this.enabled = false
  51. this.connected = false
  52. this.connecting = false
  53. this.url = DEFAULT_DEBUG_LOG_URL
  54. this.channelId = 'default'
  55. this.queue = []
  56. this.onStateChange = onStateChange
  57. }
  58. getState(): MockSimulatorDebugLoggerState {
  59. return {
  60. enabled: this.enabled,
  61. connected: this.connected,
  62. connecting: this.connecting,
  63. url: this.url,
  64. statusText: !this.enabled
  65. ? `已关闭 (${this.url})`
  66. : this.connected
  67. ? `已连接 (${this.url})`
  68. : this.connecting
  69. ? `连接中 (${this.url})`
  70. : `未连接 (${this.url})`,
  71. }
  72. }
  73. emitState(): void {
  74. if (this.onStateChange) {
  75. this.onStateChange(this.getState())
  76. }
  77. }
  78. setEnabled(enabled: boolean): void {
  79. if (this.enabled === enabled) {
  80. return
  81. }
  82. this.enabled = enabled
  83. if (!enabled) {
  84. this.disconnect()
  85. this.queue = []
  86. this.emitState()
  87. return
  88. }
  89. this.emitState()
  90. this.connect()
  91. }
  92. setUrl(url: string): void {
  93. const nextUrl = normalizeMockSimulatorLogUrl(url)
  94. if (this.url === nextUrl) {
  95. return
  96. }
  97. this.url = nextUrl
  98. if (!this.enabled) {
  99. this.emitState()
  100. return
  101. }
  102. this.disconnect()
  103. this.emitState()
  104. this.connect()
  105. }
  106. setChannelId(channelId: string): void {
  107. this.channelId = normalizeMockSimulatorChannelId(channelId)
  108. }
  109. log(
  110. scope: string,
  111. level: MockSimulatorDebugLogLevel,
  112. message: string,
  113. payload?: Record<string, unknown>,
  114. ): void {
  115. if (!this.enabled) {
  116. return
  117. }
  118. const entry: MockSimulatorDebugLogEntry = {
  119. type: 'debug-log',
  120. timestamp: Date.now(),
  121. channelId: this.channelId,
  122. scope,
  123. level,
  124. message,
  125. ...(payload ? { payload } : {}),
  126. }
  127. if (this.connected && this.socketTask) {
  128. this.send(entry)
  129. return
  130. }
  131. this.queue.push(entry)
  132. if (this.queue.length > MAX_QUEUED_LOGS) {
  133. this.queue.splice(0, this.queue.length - MAX_QUEUED_LOGS)
  134. }
  135. this.connect()
  136. }
  137. disconnect(): void {
  138. const socketTask = this.socketTask
  139. this.socketTask = null
  140. this.connected = false
  141. this.connecting = false
  142. if (socketTask) {
  143. try {
  144. socketTask.close({})
  145. } catch (_error) {
  146. // noop
  147. }
  148. }
  149. this.emitState()
  150. }
  151. destroy(): void {
  152. this.disconnect()
  153. this.queue = []
  154. }
  155. connect(): void {
  156. if (!this.enabled || this.connected || this.connecting) {
  157. return
  158. }
  159. this.connecting = true
  160. this.emitState()
  161. try {
  162. const socketTask = wx.connectSocket({
  163. url: this.url,
  164. })
  165. this.socketTask = socketTask
  166. socketTask.onOpen(() => {
  167. this.connected = true
  168. this.connecting = false
  169. this.emitState()
  170. this.send({
  171. type: 'debug-log',
  172. timestamp: Date.now(),
  173. channelId: this.channelId,
  174. scope: 'logger',
  175. level: 'info',
  176. message: 'logger channel connected',
  177. payload: {
  178. url: this.url,
  179. channelId: this.channelId,
  180. },
  181. })
  182. this.flush()
  183. })
  184. socketTask.onClose(() => {
  185. this.connected = false
  186. this.connecting = false
  187. this.socketTask = null
  188. this.emitState()
  189. })
  190. socketTask.onError(() => {
  191. this.connected = false
  192. this.connecting = false
  193. this.socketTask = null
  194. this.emitState()
  195. })
  196. socketTask.onMessage(() => {
  197. // 模拟器会广播所有消息,debug logger 不消费回包。
  198. })
  199. } catch (_error) {
  200. this.connected = false
  201. this.connecting = false
  202. this.socketTask = null
  203. this.emitState()
  204. }
  205. }
  206. flush(): void {
  207. if (!this.connected || !this.socketTask || !this.queue.length) {
  208. return
  209. }
  210. const pending = this.queue.splice(0, this.queue.length)
  211. pending.forEach((entry) => {
  212. this.send(entry)
  213. })
  214. }
  215. send(entry: MockSimulatorDebugLogEntry): void {
  216. if (!this.socketTask || !this.connected) {
  217. return
  218. }
  219. try {
  220. this.socketTask.send({
  221. data: JSON.stringify(entry),
  222. })
  223. } catch (_error) {
  224. this.connected = false
  225. this.connecting = false
  226. this.socketTask = null
  227. this.emitState()
  228. }
  229. }
  230. }