mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
257 lines
8.4 KiB
JavaScript
257 lines
8.4 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
const LOG = (...args) => console.log('[KefinTweaks WebSocketHelper]', ...args);
|
|
const WARN = (...args) => console.warn('[KefinTweaks WebSocketHelper]', ...args);
|
|
const ERR = (...args) => console.error('[KefinTweaks WebSocketHelper]', ...args);
|
|
|
|
// Registry of event listeners: MessageType -> array of callbacks
|
|
const listeners = {};
|
|
// messageType -> unsubscribe from ApiClient.subscribe
|
|
const subscribeUnsubs = {};
|
|
// messageType -> whether we already attached a subscribe fan-out
|
|
const subscribeAttached = {};
|
|
|
|
let hookedSocket = null;
|
|
let hookedOriginalOnMessage = null;
|
|
let pollingIntervalId = null;
|
|
let deliveryMode = null; // 'subscribe' | 'onmessage' | null
|
|
|
|
const POLL_INTERVAL_MS = 1500;
|
|
|
|
function getApiClient() {
|
|
return window.ApiClient || null;
|
|
}
|
|
|
|
function getSocket() {
|
|
const api = getApiClient();
|
|
if (!api) return null;
|
|
return api.webSocket || api._webSocket || null;
|
|
}
|
|
|
|
function canSubscribe() {
|
|
const api = getApiClient();
|
|
return !!(api && typeof api.subscribe === 'function');
|
|
}
|
|
|
|
function dispatchMessage(data) {
|
|
if (!data || typeof data !== 'object') return;
|
|
const messageType = data.MessageType;
|
|
if (!messageType || !listeners[messageType]) return;
|
|
listeners[messageType].forEach((callback) => {
|
|
try {
|
|
callback(data);
|
|
} catch (err) {
|
|
ERR(`Error in WebSocket listener for ${messageType}:`, err);
|
|
}
|
|
});
|
|
}
|
|
|
|
function ensureSubscribeForType(messageType) {
|
|
if (subscribeAttached[messageType]) return true;
|
|
if (!canSubscribe()) return false;
|
|
|
|
try {
|
|
const api = getApiClient();
|
|
const unsub = api.subscribe([messageType], (msg) => {
|
|
// SDK/subscribe may pass the outbound message object directly
|
|
const data = msg?.MessageType ? msg : { MessageType: messageType, Data: msg?.Data ?? msg };
|
|
if (!data.MessageType) data.MessageType = messageType;
|
|
dispatchMessage(data);
|
|
});
|
|
subscribeAttached[messageType] = true;
|
|
subscribeUnsubs[messageType] = typeof unsub === 'function' ? unsub : null;
|
|
deliveryMode = 'subscribe';
|
|
LOG(`Subscribed via ApiClient.subscribe for ${messageType}`);
|
|
return true;
|
|
} catch (err) {
|
|
ERR(`ApiClient.subscribe failed for ${messageType}:`, err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function ensureSubscribeForAllRegisteredTypes() {
|
|
if (!canSubscribe()) return false;
|
|
let any = false;
|
|
Object.keys(listeners).forEach((messageType) => {
|
|
if (listeners[messageType]?.length) {
|
|
if (ensureSubscribeForType(messageType)) any = true;
|
|
}
|
|
});
|
|
return any;
|
|
}
|
|
|
|
function unhookSocket() {
|
|
if (hookedSocket && hookedOriginalOnMessage !== undefined) {
|
|
try {
|
|
hookedSocket.onmessage = hookedOriginalOnMessage;
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
hookedSocket = null;
|
|
hookedOriginalOnMessage = null;
|
|
}
|
|
|
|
/**
|
|
* Legacy path: wrap socket.onmessage, re-bind when the socket instance changes.
|
|
*/
|
|
function hookSocketIfNeeded() {
|
|
const socket = getSocket();
|
|
if (!socket) return false;
|
|
|
|
if (hookedSocket === socket) return true;
|
|
|
|
unhookSocket();
|
|
|
|
try {
|
|
const originalHandler = socket.onmessage;
|
|
hookedOriginalOnMessage = originalHandler;
|
|
hookedSocket = socket;
|
|
|
|
socket.onmessage = function(event) {
|
|
// Keep JF handler isolated so its throws do not look like parse failures
|
|
// and do not block KefinTweaks listener delivery.
|
|
if (originalHandler) {
|
|
try {
|
|
originalHandler.call(this, event);
|
|
} catch (err) {
|
|
WARN('Original WebSocket onmessage handler error:', err);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const messageData = event.Data || event.data;
|
|
const data = typeof messageData === 'string' ? JSON.parse(messageData) : messageData;
|
|
dispatchMessage(data);
|
|
} catch (err) {
|
|
WARN('WebSocket message parse error:', err);
|
|
}
|
|
};
|
|
|
|
deliveryMode = 'onmessage';
|
|
LOG('WebSocket onmessage hook attached (legacy fallback)', {
|
|
readyState: socket.readyState,
|
|
});
|
|
return true;
|
|
} catch (err) {
|
|
ERR('Error hooking WebSocket onmessage:', err);
|
|
hookedSocket = null;
|
|
hookedOriginalOnMessage = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function tryInitializeDelivery() {
|
|
// Prefer JF-web style subscribe (reconnect-safe)
|
|
if (canSubscribe()) {
|
|
if (ensureSubscribeForAllRegisteredTypes()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Fallback: raw socket onmessage with rebind support
|
|
return hookSocketIfNeeded();
|
|
}
|
|
|
|
function startPolling() {
|
|
if (pollingIntervalId != null) return;
|
|
if (tryInitializeDelivery()) {
|
|
// Still poll in legacy mode so we rebind if socket is replaced
|
|
if (deliveryMode === 'subscribe') return;
|
|
}
|
|
|
|
pollingIntervalId = setInterval(() => {
|
|
if (canSubscribe()) {
|
|
ensureSubscribeForAllRegisteredTypes();
|
|
if (deliveryMode === 'subscribe') {
|
|
// Subscribe path owns delivery; drop legacy hook if any
|
|
unhookSocket();
|
|
clearInterval(pollingIntervalId);
|
|
pollingIntervalId = null;
|
|
LOG('Delivery mode: ApiClient.subscribe');
|
|
return;
|
|
}
|
|
}
|
|
|
|
const socket = getSocket();
|
|
if (!socket) return;
|
|
|
|
if (hookedSocket !== socket) {
|
|
hookSocketIfNeeded();
|
|
}
|
|
}, POLL_INTERVAL_MS);
|
|
}
|
|
|
|
/**
|
|
* Register a callback to listen for a specific WebSocket message type
|
|
* @param {string} messageType - e.g. 'UserDataChanged', 'LibraryChanged'
|
|
* @param {Function} callback - Receives the parsed message object
|
|
*/
|
|
function listen(messageType, callback) {
|
|
if (typeof messageType !== 'string' || !messageType) {
|
|
ERR('listen() requires a non-empty string messageType');
|
|
return;
|
|
}
|
|
|
|
if (typeof callback !== 'function') {
|
|
ERR('listen() requires a callback function');
|
|
return;
|
|
}
|
|
|
|
if (!listeners[messageType]) {
|
|
listeners[messageType] = [];
|
|
}
|
|
|
|
if (listeners[messageType].includes(callback)) {
|
|
LOG(`Listener already registered for message type: ${messageType}`);
|
|
startPolling();
|
|
return;
|
|
}
|
|
|
|
listeners[messageType].push(callback);
|
|
LOG(`Registered listener for message type: ${messageType} (${listeners[messageType].length} total)`, {
|
|
deliveryMode: deliveryMode || 'pending',
|
|
canSubscribe: canSubscribe(),
|
|
});
|
|
|
|
if (canSubscribe()) {
|
|
ensureSubscribeForType(messageType);
|
|
}
|
|
|
|
startPolling();
|
|
}
|
|
|
|
/**
|
|
* Remove a callback registered with listen().
|
|
* @param {string} messageType
|
|
* @param {Function} callback
|
|
*/
|
|
function unlisten(messageType, callback) {
|
|
if (!listeners[messageType]) return;
|
|
listeners[messageType] = listeners[messageType].filter((cb) => cb !== callback);
|
|
LOG(`Unregistered listener for message type: ${messageType} (${listeners[messageType].length} remaining)`);
|
|
|
|
if (!listeners[messageType].length && subscribeUnsubs[messageType]) {
|
|
try {
|
|
subscribeUnsubs[messageType]();
|
|
} catch (e) {
|
|
WARN(`Failed to unsubscribe ${messageType}:`, e);
|
|
}
|
|
delete subscribeUnsubs[messageType];
|
|
delete subscribeAttached[messageType];
|
|
}
|
|
}
|
|
|
|
window.websocketHelper = {
|
|
listen,
|
|
unlisten,
|
|
/** @returns {'subscribe'|'onmessage'|null} */
|
|
getDeliveryMode() {
|
|
return deliveryMode;
|
|
},
|
|
};
|
|
|
|
startPolling();
|
|
})();
|