| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- // tiktok-push.js
- // 抖音小程序 WebSocket 推送模块(完整修复版)
- // 支持频道订阅、心跳保活、自动重连、私有频道认证
- // ==================== 辅助函数 ====================
- function getGlobalObject() {
- if (typeof global !== 'undefined') return global;
- if (typeof window !== 'undefined') return window;
- return this;
- }
- // ==================== CallbackRegistry ====================
- function CallbackRegistry() {
- this._callbacks = {};
- }
- function prefix(name) {
- return '_' + name;
- }
- CallbackRegistry.prototype.get = function (name) {
- var key = prefix(name);
- return this._callbacks[key] || [];
- };
- CallbackRegistry.prototype.add = function (name, callback, context) {
- var key = prefix(name);
- if (!this._callbacks[key]) {
- this._callbacks[key] = [];
- }
- this._callbacks[key].push({
- fn: callback,
- context: context
- });
- };
- CallbackRegistry.prototype.remove = function (name, callback, context) {
- if (!name && !callback && !context) {
- this._callbacks = {};
- return;
- }
- var names = name ? [prefix(name)] : Object.keys(this._callbacks);
- for (var i = 0; i < names.length; i++) {
- var key = names[i];
- if (!this._callbacks[key]) continue;
- if (callback || context) {
- var filtered = [];
- for (var j = 0; j < this._callbacks[key].length; j++) {
- var cb = this._callbacks[key][j];
- if ((callback && callback !== cb.fn) || (context && context !== cb.context)) {
- filtered.push(cb);
- }
- }
- if (filtered.length === 0) {
- delete this._callbacks[key];
- } else {
- this._callbacks[key] = filtered;
- }
- } else {
- delete this._callbacks[key];
- }
- }
- };
- // ==================== Dispatcher ====================
- function Dispatcher(failThrough) {
- this.callbacks = new CallbackRegistry();
- this.global_callbacks = [];
- this.failThrough = failThrough;
- }
- Dispatcher.prototype.on = function (eventName, callback, context) {
- this.callbacks.add(eventName, callback, context);
- return this;
- };
- Dispatcher.prototype.on_global = function (callback) {
- if (typeof callback === 'function') {
- this.global_callbacks.push(callback);
- }
- return this;
- };
- Dispatcher.prototype.off = function (eventName, callback, context) {
- this.callbacks.remove(eventName, callback, context);
- return this;
- };
- Dispatcher.prototype.emit = function (eventName, data) {
- // 全局回调
- if (Array.isArray(this.global_callbacks)) {
- for (var i = 0; i < this.global_callbacks.length; i++) {
- var gc = this.global_callbacks[i];
- if (typeof gc === 'function') {
- try {
- gc(eventName, data);
- } catch (e) {
- console.error('global callback error:', e);
- }
- }
- }
- }
- // 获取事件回调列表
- var callbacks = null;
- try {
- callbacks = this.callbacks && this.callbacks.get ? this.callbacks.get(eventName) : null;
- } catch (e) {
- console.warn('get callbacks error:', e);
- }
- if (callbacks && Array.isArray(callbacks) && callbacks.length > 0) {
- for (var j = 0; j < callbacks.length; j++) {
- var item = callbacks[j];
- if (item && typeof item.fn === 'function') {
- var ctx = item.context || getGlobalObject();
- try {
- item.fn.call(ctx, data);
- } catch (e) {
- console.error('callback error for event "' + eventName + '":', e);
- }
- }
- }
- } else if (this.failThrough && typeof this.failThrough === 'function') {
- this.failThrough(eventName, data);
- }
- return this;
- };
- // ==================== Channel ====================
- function Channel(connection, channel_name) {
- this.subscribed = false;
- this.dispatcher = new Dispatcher();
- this.connection = connection;
- this.channelName = channel_name;
- this.subscribeCb = null;
- this.queue = [];
- // 绑定 dispatcher 方法,确保 this 指向 dispatcher
- this.on = this.dispatcher.on.bind(this.dispatcher);
- this.off = this.dispatcher.off.bind(this.dispatcher);
- this.emit = this.dispatcher.emit.bind(this.dispatcher);
- this.on_global = this.dispatcher.on_global.bind(this.dispatcher);
- }
- Channel.prototype.processSubscribe = function () {
- if (this.connection.state !== 'connected') {
- return;
- }
- if (this.subscribeCb) {
- this.subscribeCb();
- }
- };
- Channel.prototype.processQueue = function () {
- if (this.connection.state !== 'connected' || !this.subscribed) {
- return;
- }
- for (var i = 0; i < this.queue.length; i++) {
- try {
- this.queue[i]();
- } catch (e) {
- console.error('processQueue error:', e);
- }
- }
- this.queue = [];
- };
- Channel.prototype.trigger = function (event, data) {
- if (event.indexOf('client-') !== 0) {
- throw new Error("Event '" + event + "' should start with 'client-'");
- }
- var self = this;
- this.queue.push(function () {
- self.connection.send(JSON.stringify({ event: event, data: data, channel: self.channelName }));
- });
- this.processQueue();
- };
- // ==================== Connection ====================
- function Connection(options) {
- this.dispatcher = new Dispatcher();
- // 绑定 dispatcher 方法
- this.on = this.dispatcher.on.bind(this.dispatcher);
- this.off = this.dispatcher.off.bind(this.dispatcher);
- this.emit = this.dispatcher.emit.bind(this.dispatcher);
- this.on_global = this.dispatcher.on_global.bind(this.dispatcher);
- this.options = options;
- this.state = 'initialized';
- this.doNotConnect = 0;
- this.reconnectInterval = 1000;
- this.socketTask = null;
- this.reconnectTimer = 0;
- this.socket_id = null;
- this.connect();
- }
- Connection.prototype.updateNetworkState = function (state) {
- var old_state = this.state;
- this.state = state;
- if (old_state !== state) {
- this.emit('state_change', { previous: old_state, current: state });
- }
- };
- Connection.prototype.connect = function () {
- var self = this;
- this.doNotConnect = 0;
- if (this.state === 'connected') {
- console.log('networkState is "' + this.state + '" , no need to connect');
- return;
- }
- if (this.reconnectTimer) {
- clearTimeout(this.reconnectTimer);
- this.reconnectTimer = 0;
- }
- this.closeAndClean();
- var url = this.options.url;
- var app_key = this.options.app_key;
- var fullUrl = url + '/app/' + app_key;
- this.socketTask = tt.connectSocket({
- url: fullUrl,
- success: function (res) {
- console.log('WebSocket connect success, socketTaskId:', res.socketTaskId);
- },
- fail: function (err) {
- console.error('WebSocket connect fail:', err);
- self.updateNetworkState('disconnected');
- if (!self.doNotConnect) {
- self.waitReconnect();
- }
- if (self.options.onError) {
- self.options.onError(err);
- }
- }
- });
- this.socketTask.onOpen(function (res) {
- self.reconnectInterval = 1000;
- if (self.doNotConnect) {
- self.updateNetworkState('disconnected');
- self.socketTask.close();
- return;
- }
- self.updateNetworkState('connected');
- if (self.options.onOpen) {
- self.options.onOpen(res);
- }
- });
- this.socketTask.onMessage(function (res) {
- if (self.options.onMessage) {
- self.options.onMessage(res);
- }
- });
- this.socketTask.onClose(function (res) {
- console.log('WebSocket closed, code:', res);
- self.updateNetworkState('disconnected');
- if (!self.doNotConnect) {
- self.waitReconnect();
- }
- if (self.options.onClose) {
- self.options.onClose(res);
- }
- });
- this.socketTask.onError(function (res) {
- console.log('WebSocket error, code:', res);
- self.updateNetworkState('disconnected');
- if (!self.doNotConnect) {
- self.waitReconnect();
- }
- if (self.options.onError) {
- self.options.onError(res);
- }
- });
- this.updateNetworkState('connecting');
- };
- Connection.prototype.closeAndClean = function () {
- if (this.socketTask) {
- try {
- this.socketTask.close();
- } catch (e) {
- console.warn('closeAndClean error:', e);
- }
- this.socketTask = null;
- }
- this.updateNetworkState('disconnected');
- };
- Connection.prototype.waitReconnect = function () {
- if (this.state === 'connected' || this.state === 'connecting') {
- return;
- }
- if (!this.doNotConnect) {
- this.updateNetworkState('connecting');
- var self = this;
- if (this.reconnectTimer) {
- clearTimeout(this.reconnectTimer);
- }
- this.reconnectTimer = setTimeout(function () {
- self.connect();
- }, this.reconnectInterval);
- if (this.reconnectInterval < 1000) {
- this.reconnectInterval = 1000;
- } else {
- this.reconnectInterval = Math.min(this.reconnectInterval * 2, 2000);
- }
- }
- };
- Connection.prototype.send = function (data) {
- if (this.state !== 'connected') {
- console.warn('state is "' + this.state + '", cannot send:', data);
- return;
- }
- this.socketTask.send({
- data: data,
- fail: function (err) {
- console.error('send fail:', err);
- }
- });
- };
- Connection.prototype.close = function () {
- this.doNotConnect = 1;
- this.updateNetworkState('disconnected');
- if (this.socketTask) {
- this.socketTask.close();
- }
- };
- // ==================== Push 客户端 ====================
- function Push(options) {
- this.doNotConnect = 0;
- options = options || {};
- options.heartbeat = options.heartbeat || 25000;
- options.pingTimeout = options.pingTimeout || 10000;
- this.config = options;
- this.uid = 0;
- this.channels = {};
- this.connection = null;
- this.pingTimeoutTimer = 0;
- Push.instances.push(this);
- this.createConnection();
- }
- Push.instances = [];
- Push.prototype.checkoutPing = function () {
- var self = this;
- setTimeout(function () {
- if (self.connection.state === 'connected') {
- self.connection.send('hi');
- if (self.pingTimeoutTimer) {
- clearTimeout(self.pingTimeoutTimer);
- self.pingTimeoutTimer = 0;
- }
- self.pingTimeoutTimer = setTimeout(function () {
- self.connection.closeAndClean();
- if (!self.connection.doNotConnect) {
- self.connection.waitReconnect();
- }
- }, self.config.pingTimeout);
- }
- }, this.config.heartbeat);
- };
- Push.prototype.channel = function (name) {
- return this.channels[name];
- };
- Push.prototype.allChannels = function () {
- return this.channels;
- };
- Push.prototype.createConnection = function () {
- if (this.connection) {
- throw new Error('Connection already exists');
- }
- var self = this;
- var url = this.config.url;
- var app_key = this.config.app_key;
- function updateSubscribed() {
- for (var i in self.channels) {
- if (self.channels[i]) {
- self.channels[i].subscribed = false;
- }
- }
- }
- this.connection = new Connection({
- url: url,
- app_key: app_key,
- onOpen: function () {
- self.connection.state = 'connected';
- self.checkoutPing();
- },
- onMessage: function (res) {
- if (self.pingTimeoutTimer) {
- clearTimeout(self.pingTimeoutTimer);
- self.pingTimeoutTimer = 0;
- }
- var params;
- try {
- params = JSON.parse(res.data);
- } catch (e) {
- console.warn('parse message error:', e, res.data);
- return;
- }
- var event = params.event;
- var channel_name = params.channel;
- if (event === 'pusher:pong') {
- self.checkoutPing();
- return;
- }
- if (event === 'pusher:error') {
- throw new Error(params.data && params.data.message);
- }
- var data = params.data;
- var channel;
- if (event === 'pusher_internal:subscription_succeeded') {
- channel = self.channels[channel_name];
- if (channel) {
- channel.subscribed = true;
- channel.processQueue();
- channel.emit('pusher:subscription_succeeded');
- }
- return;
- }
- if (event === 'pusher:connection_established') {
- self.connection.socket_id = data.socket_id;
- self.connection.state = 'connected';
- self.subscribeAll();
- return;
- }
- if (event && event.indexOf('pusher_internal') !== -1) {
- console.log('Event "' + event + '" not implemented');
- return;
- }
- channel = self.channels[channel_name];
- if (channel) {
- if (typeof data === 'string') {
- try {
- data = JSON.parse(data);
- } catch (e) {}
- }
- channel.emit(event, data);
- }
- },
- onClose: function () {
- updateSubscribed();
- },
- onError: function () {
- updateSubscribed();
- }
- });
- };
- Push.prototype.disconnect = function () {
- this.connection.doNotConnect = 1;
- this.connection.close();
- };
- Push.prototype.subscribeAll = function () {
- if (this.connection.state !== 'connected') {
- return;
- }
- for (var channel_name in this.channels) {
- if (this.channels[channel_name]) {
- this.channels[channel_name].processSubscribe();
- }
- }
- };
- Push.prototype.unsubscribe = function (channel_name) {
- if (this.channels[channel_name]) {
- delete this.channels[channel_name];
- if (this.connection.state === 'connected') {
- this.connection.send(JSON.stringify({ event: 'pusher:unsubscribe', data: { channel: channel_name } }));
- }
- }
- };
- Push.prototype.unsubscribeAll = function () {
- for (var channel_name in this.channels) {
- this.unsubscribe(channel_name);
- }
- this.channels = {};
- };
- Push.prototype.subscribe = function (channel_name) {
- if (this.channels[channel_name]) {
- return this.channels[channel_name];
- }
- if (channel_name.indexOf('private-') === 0) {
- return createPrivateChannel(channel_name, this);
- }
- if (channel_name.indexOf('presence-') === 0) {
- return createPresenceChannel(channel_name, this);
- }
- return createChannel(channel_name, this);
- };
- // ==================== 频道创建辅助函数 ====================
- function createChannel(channel_name, push) {
- var channel = new Channel(push.connection, channel_name);
- push.channels[channel_name] = channel;
- channel.subscribeCb = function () {
- push.connection.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: channel_name } }));
- };
- channel.processSubscribe();
- return channel;
- }
- function createPrivateChannel(channel_name, push) {
- var channel = new Channel(push.connection, channel_name);
- push.channels[channel_name] = channel;
- channel.subscribeCb = function () {
- tt.request({
- url: push.config.auth,
- method: 'POST',
- data: { channel_name: channel_name, socket_id: push.connection.socket_id },
- success: function (res) {
- var data;
- try {
- data = res.data;
- } catch (e) {
- console.error('parse auth response error:', e);
- return;
- }
- data.channel = channel_name;
- push.connection.send(JSON.stringify({ event: 'pusher:subscribe', data: data }));
- },
- fail: function (err) {
- console.error('auth request fail:', err);
- throw new Error(err.errMsg);
- }
- });
- };
- channel.processSubscribe();
- return channel;
- }
- function createPresenceChannel(channel_name, push) {
- return createPrivateChannel(channel_name, push);
- }
- // ==================== 导出 ====================
- module.exports = {
- Push: Push
- };
|