push.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. // tiktok-push.js
  2. // 抖音小程序 WebSocket 推送模块(完整修复版)
  3. // 支持频道订阅、心跳保活、自动重连、私有频道认证
  4. // ==================== 辅助函数 ====================
  5. function getGlobalObject() {
  6. if (typeof global !== 'undefined') return global;
  7. if (typeof window !== 'undefined') return window;
  8. return this;
  9. }
  10. // ==================== CallbackRegistry ====================
  11. function CallbackRegistry() {
  12. this._callbacks = {};
  13. }
  14. function prefix(name) {
  15. return '_' + name;
  16. }
  17. CallbackRegistry.prototype.get = function (name) {
  18. var key = prefix(name);
  19. return this._callbacks[key] || [];
  20. };
  21. CallbackRegistry.prototype.add = function (name, callback, context) {
  22. var key = prefix(name);
  23. if (!this._callbacks[key]) {
  24. this._callbacks[key] = [];
  25. }
  26. this._callbacks[key].push({
  27. fn: callback,
  28. context: context
  29. });
  30. };
  31. CallbackRegistry.prototype.remove = function (name, callback, context) {
  32. if (!name && !callback && !context) {
  33. this._callbacks = {};
  34. return;
  35. }
  36. var names = name ? [prefix(name)] : Object.keys(this._callbacks);
  37. for (var i = 0; i < names.length; i++) {
  38. var key = names[i];
  39. if (!this._callbacks[key]) continue;
  40. if (callback || context) {
  41. var filtered = [];
  42. for (var j = 0; j < this._callbacks[key].length; j++) {
  43. var cb = this._callbacks[key][j];
  44. if ((callback && callback !== cb.fn) || (context && context !== cb.context)) {
  45. filtered.push(cb);
  46. }
  47. }
  48. if (filtered.length === 0) {
  49. delete this._callbacks[key];
  50. } else {
  51. this._callbacks[key] = filtered;
  52. }
  53. } else {
  54. delete this._callbacks[key];
  55. }
  56. }
  57. };
  58. // ==================== Dispatcher ====================
  59. function Dispatcher(failThrough) {
  60. this.callbacks = new CallbackRegistry();
  61. this.global_callbacks = [];
  62. this.failThrough = failThrough;
  63. }
  64. Dispatcher.prototype.on = function (eventName, callback, context) {
  65. this.callbacks.add(eventName, callback, context);
  66. return this;
  67. };
  68. Dispatcher.prototype.on_global = function (callback) {
  69. if (typeof callback === 'function') {
  70. this.global_callbacks.push(callback);
  71. }
  72. return this;
  73. };
  74. Dispatcher.prototype.off = function (eventName, callback, context) {
  75. this.callbacks.remove(eventName, callback, context);
  76. return this;
  77. };
  78. Dispatcher.prototype.emit = function (eventName, data) {
  79. // 全局回调
  80. if (Array.isArray(this.global_callbacks)) {
  81. for (var i = 0; i < this.global_callbacks.length; i++) {
  82. var gc = this.global_callbacks[i];
  83. if (typeof gc === 'function') {
  84. try {
  85. gc(eventName, data);
  86. } catch (e) {
  87. console.error('global callback error:', e);
  88. }
  89. }
  90. }
  91. }
  92. // 获取事件回调列表
  93. var callbacks = null;
  94. try {
  95. callbacks = this.callbacks && this.callbacks.get ? this.callbacks.get(eventName) : null;
  96. } catch (e) {
  97. console.warn('get callbacks error:', e);
  98. }
  99. if (callbacks && Array.isArray(callbacks) && callbacks.length > 0) {
  100. for (var j = 0; j < callbacks.length; j++) {
  101. var item = callbacks[j];
  102. if (item && typeof item.fn === 'function') {
  103. var ctx = item.context || getGlobalObject();
  104. try {
  105. item.fn.call(ctx, data);
  106. } catch (e) {
  107. console.error('callback error for event "' + eventName + '":', e);
  108. }
  109. }
  110. }
  111. } else if (this.failThrough && typeof this.failThrough === 'function') {
  112. this.failThrough(eventName, data);
  113. }
  114. return this;
  115. };
  116. // ==================== Channel ====================
  117. function Channel(connection, channel_name) {
  118. this.subscribed = false;
  119. this.dispatcher = new Dispatcher();
  120. this.connection = connection;
  121. this.channelName = channel_name;
  122. this.subscribeCb = null;
  123. this.queue = [];
  124. // 绑定 dispatcher 方法,确保 this 指向 dispatcher
  125. this.on = this.dispatcher.on.bind(this.dispatcher);
  126. this.off = this.dispatcher.off.bind(this.dispatcher);
  127. this.emit = this.dispatcher.emit.bind(this.dispatcher);
  128. this.on_global = this.dispatcher.on_global.bind(this.dispatcher);
  129. }
  130. Channel.prototype.processSubscribe = function () {
  131. if (this.connection.state !== 'connected') {
  132. return;
  133. }
  134. if (this.subscribeCb) {
  135. this.subscribeCb();
  136. }
  137. };
  138. Channel.prototype.processQueue = function () {
  139. if (this.connection.state !== 'connected' || !this.subscribed) {
  140. return;
  141. }
  142. for (var i = 0; i < this.queue.length; i++) {
  143. try {
  144. this.queue[i]();
  145. } catch (e) {
  146. console.error('processQueue error:', e);
  147. }
  148. }
  149. this.queue = [];
  150. };
  151. Channel.prototype.trigger = function (event, data) {
  152. if (event.indexOf('client-') !== 0) {
  153. throw new Error("Event '" + event + "' should start with 'client-'");
  154. }
  155. var self = this;
  156. this.queue.push(function () {
  157. self.connection.send(JSON.stringify({ event: event, data: data, channel: self.channelName }));
  158. });
  159. this.processQueue();
  160. };
  161. // ==================== Connection ====================
  162. function Connection(options) {
  163. this.dispatcher = new Dispatcher();
  164. // 绑定 dispatcher 方法
  165. this.on = this.dispatcher.on.bind(this.dispatcher);
  166. this.off = this.dispatcher.off.bind(this.dispatcher);
  167. this.emit = this.dispatcher.emit.bind(this.dispatcher);
  168. this.on_global = this.dispatcher.on_global.bind(this.dispatcher);
  169. this.options = options;
  170. this.state = 'initialized';
  171. this.doNotConnect = 0;
  172. this.reconnectInterval = 1000;
  173. this.socketTask = null;
  174. this.reconnectTimer = 0;
  175. this.socket_id = null;
  176. this.connect();
  177. }
  178. Connection.prototype.updateNetworkState = function (state) {
  179. var old_state = this.state;
  180. this.state = state;
  181. if (old_state !== state) {
  182. this.emit('state_change', { previous: old_state, current: state });
  183. }
  184. };
  185. Connection.prototype.connect = function () {
  186. var self = this;
  187. this.doNotConnect = 0;
  188. if (this.state === 'connected') {
  189. console.log('networkState is "' + this.state + '" , no need to connect');
  190. return;
  191. }
  192. if (this.reconnectTimer) {
  193. clearTimeout(this.reconnectTimer);
  194. this.reconnectTimer = 0;
  195. }
  196. this.closeAndClean();
  197. var url = this.options.url;
  198. var app_key = this.options.app_key;
  199. var fullUrl = url + '/app/' + app_key;
  200. this.socketTask = tt.connectSocket({
  201. url: fullUrl,
  202. success: function (res) {
  203. console.log('WebSocket connect success, socketTaskId:', res.socketTaskId);
  204. },
  205. fail: function (err) {
  206. console.error('WebSocket connect fail:', err);
  207. self.updateNetworkState('disconnected');
  208. if (!self.doNotConnect) {
  209. self.waitReconnect();
  210. }
  211. if (self.options.onError) {
  212. self.options.onError(err);
  213. }
  214. }
  215. });
  216. this.socketTask.onOpen(function (res) {
  217. self.reconnectInterval = 1000;
  218. if (self.doNotConnect) {
  219. self.updateNetworkState('disconnected');
  220. self.socketTask.close();
  221. return;
  222. }
  223. self.updateNetworkState('connected');
  224. if (self.options.onOpen) {
  225. self.options.onOpen(res);
  226. }
  227. });
  228. this.socketTask.onMessage(function (res) {
  229. if (self.options.onMessage) {
  230. self.options.onMessage(res);
  231. }
  232. });
  233. this.socketTask.onClose(function (res) {
  234. console.log('WebSocket closed, code:', res);
  235. self.updateNetworkState('disconnected');
  236. if (!self.doNotConnect) {
  237. self.waitReconnect();
  238. }
  239. if (self.options.onClose) {
  240. self.options.onClose(res);
  241. }
  242. });
  243. this.socketTask.onError(function (res) {
  244. console.log('WebSocket error, code:', res);
  245. self.updateNetworkState('disconnected');
  246. if (!self.doNotConnect) {
  247. self.waitReconnect();
  248. }
  249. if (self.options.onError) {
  250. self.options.onError(res);
  251. }
  252. });
  253. this.updateNetworkState('connecting');
  254. };
  255. Connection.prototype.closeAndClean = function () {
  256. if (this.socketTask) {
  257. try {
  258. this.socketTask.close();
  259. } catch (e) {
  260. console.warn('closeAndClean error:', e);
  261. }
  262. this.socketTask = null;
  263. }
  264. this.updateNetworkState('disconnected');
  265. };
  266. Connection.prototype.waitReconnect = function () {
  267. if (this.state === 'connected' || this.state === 'connecting') {
  268. return;
  269. }
  270. if (!this.doNotConnect) {
  271. this.updateNetworkState('connecting');
  272. var self = this;
  273. if (this.reconnectTimer) {
  274. clearTimeout(this.reconnectTimer);
  275. }
  276. this.reconnectTimer = setTimeout(function () {
  277. self.connect();
  278. }, this.reconnectInterval);
  279. if (this.reconnectInterval < 1000) {
  280. this.reconnectInterval = 1000;
  281. } else {
  282. this.reconnectInterval = Math.min(this.reconnectInterval * 2, 2000);
  283. }
  284. }
  285. };
  286. Connection.prototype.send = function (data) {
  287. if (this.state !== 'connected') {
  288. console.warn('state is "' + this.state + '", cannot send:', data);
  289. return;
  290. }
  291. this.socketTask.send({
  292. data: data,
  293. fail: function (err) {
  294. console.error('send fail:', err);
  295. }
  296. });
  297. };
  298. Connection.prototype.close = function () {
  299. this.doNotConnect = 1;
  300. this.updateNetworkState('disconnected');
  301. if (this.socketTask) {
  302. this.socketTask.close();
  303. }
  304. };
  305. // ==================== Push 客户端 ====================
  306. function Push(options) {
  307. this.doNotConnect = 0;
  308. options = options || {};
  309. options.heartbeat = options.heartbeat || 25000;
  310. options.pingTimeout = options.pingTimeout || 10000;
  311. this.config = options;
  312. this.uid = 0;
  313. this.channels = {};
  314. this.connection = null;
  315. this.pingTimeoutTimer = 0;
  316. Push.instances.push(this);
  317. this.createConnection();
  318. }
  319. Push.instances = [];
  320. Push.prototype.checkoutPing = function () {
  321. var self = this;
  322. setTimeout(function () {
  323. if (self.connection.state === 'connected') {
  324. self.connection.send('hi');
  325. if (self.pingTimeoutTimer) {
  326. clearTimeout(self.pingTimeoutTimer);
  327. self.pingTimeoutTimer = 0;
  328. }
  329. self.pingTimeoutTimer = setTimeout(function () {
  330. self.connection.closeAndClean();
  331. if (!self.connection.doNotConnect) {
  332. self.connection.waitReconnect();
  333. }
  334. }, self.config.pingTimeout);
  335. }
  336. }, this.config.heartbeat);
  337. };
  338. Push.prototype.channel = function (name) {
  339. return this.channels[name];
  340. };
  341. Push.prototype.allChannels = function () {
  342. return this.channels;
  343. };
  344. Push.prototype.createConnection = function () {
  345. if (this.connection) {
  346. throw new Error('Connection already exists');
  347. }
  348. var self = this;
  349. var url = this.config.url;
  350. var app_key = this.config.app_key;
  351. function updateSubscribed() {
  352. for (var i in self.channels) {
  353. if (self.channels[i]) {
  354. self.channels[i].subscribed = false;
  355. }
  356. }
  357. }
  358. this.connection = new Connection({
  359. url: url,
  360. app_key: app_key,
  361. onOpen: function () {
  362. self.connection.state = 'connected';
  363. self.checkoutPing();
  364. },
  365. onMessage: function (res) {
  366. if (self.pingTimeoutTimer) {
  367. clearTimeout(self.pingTimeoutTimer);
  368. self.pingTimeoutTimer = 0;
  369. }
  370. var params;
  371. try {
  372. params = JSON.parse(res.data);
  373. } catch (e) {
  374. console.warn('parse message error:', e, res.data);
  375. return;
  376. }
  377. var event = params.event;
  378. var channel_name = params.channel;
  379. if (event === 'pusher:pong') {
  380. self.checkoutPing();
  381. return;
  382. }
  383. if (event === 'pusher:error') {
  384. throw new Error(params.data && params.data.message);
  385. }
  386. var data = params.data;
  387. var channel;
  388. if (event === 'pusher_internal:subscription_succeeded') {
  389. channel = self.channels[channel_name];
  390. if (channel) {
  391. channel.subscribed = true;
  392. channel.processQueue();
  393. channel.emit('pusher:subscription_succeeded');
  394. }
  395. return;
  396. }
  397. if (event === 'pusher:connection_established') {
  398. self.connection.socket_id = data.socket_id;
  399. self.connection.state = 'connected';
  400. self.subscribeAll();
  401. return;
  402. }
  403. if (event && event.indexOf('pusher_internal') !== -1) {
  404. console.log('Event "' + event + '" not implemented');
  405. return;
  406. }
  407. channel = self.channels[channel_name];
  408. if (channel) {
  409. if (typeof data === 'string') {
  410. try {
  411. data = JSON.parse(data);
  412. } catch (e) {}
  413. }
  414. channel.emit(event, data);
  415. }
  416. },
  417. onClose: function () {
  418. updateSubscribed();
  419. },
  420. onError: function () {
  421. updateSubscribed();
  422. }
  423. });
  424. };
  425. Push.prototype.disconnect = function () {
  426. this.connection.doNotConnect = 1;
  427. this.connection.close();
  428. };
  429. Push.prototype.subscribeAll = function () {
  430. if (this.connection.state !== 'connected') {
  431. return;
  432. }
  433. for (var channel_name in this.channels) {
  434. if (this.channels[channel_name]) {
  435. this.channels[channel_name].processSubscribe();
  436. }
  437. }
  438. };
  439. Push.prototype.unsubscribe = function (channel_name) {
  440. if (this.channels[channel_name]) {
  441. delete this.channels[channel_name];
  442. if (this.connection.state === 'connected') {
  443. this.connection.send(JSON.stringify({ event: 'pusher:unsubscribe', data: { channel: channel_name } }));
  444. }
  445. }
  446. };
  447. Push.prototype.unsubscribeAll = function () {
  448. for (var channel_name in this.channels) {
  449. this.unsubscribe(channel_name);
  450. }
  451. this.channels = {};
  452. };
  453. Push.prototype.subscribe = function (channel_name) {
  454. if (this.channels[channel_name]) {
  455. return this.channels[channel_name];
  456. }
  457. if (channel_name.indexOf('private-') === 0) {
  458. return createPrivateChannel(channel_name, this);
  459. }
  460. if (channel_name.indexOf('presence-') === 0) {
  461. return createPresenceChannel(channel_name, this);
  462. }
  463. return createChannel(channel_name, this);
  464. };
  465. // ==================== 频道创建辅助函数 ====================
  466. function createChannel(channel_name, push) {
  467. var channel = new Channel(push.connection, channel_name);
  468. push.channels[channel_name] = channel;
  469. channel.subscribeCb = function () {
  470. push.connection.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: channel_name } }));
  471. };
  472. channel.processSubscribe();
  473. return channel;
  474. }
  475. function createPrivateChannel(channel_name, push) {
  476. var channel = new Channel(push.connection, channel_name);
  477. push.channels[channel_name] = channel;
  478. channel.subscribeCb = function () {
  479. tt.request({
  480. url: push.config.auth,
  481. method: 'POST',
  482. data: { channel_name: channel_name, socket_id: push.connection.socket_id },
  483. success: function (res) {
  484. var data;
  485. try {
  486. data = res.data;
  487. } catch (e) {
  488. console.error('parse auth response error:', e);
  489. return;
  490. }
  491. data.channel = channel_name;
  492. push.connection.send(JSON.stringify({ event: 'pusher:subscribe', data: data }));
  493. },
  494. fail: function (err) {
  495. console.error('auth request fail:', err);
  496. throw new Error(err.errMsg);
  497. }
  498. });
  499. };
  500. channel.processSubscribe();
  501. return channel;
  502. }
  503. function createPresenceChannel(channel_name, push) {
  504. return createPrivateChannel(channel_name, push);
  505. }
  506. // ==================== 导出 ====================
  507. module.exports = {
  508. Push: Push
  509. };