PHP Classes

File: public_html/js/binkstream-worker-v2.js

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   public_html/js/binkstream-worker-v2.js   Download  
File: public_html/js/binkstream-worker-v2.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 18,304 bytes
 

Contents

Class file image Download
/** * binkstream-worker-v2.js - SharedWorker transport layer for BinkStream. * * Maintains one realtime connection per browser profile and fans events out to * all connected tabs. Supports: * - WebSocket transport (preferred when configured) * - SSE transport (including auto mode) * - POST /api/stream command fallback for non-WebSocket transports */ 'use strict'; const STREAM_URL = '/api/stream'; const MIN_BACKOFF = 1000; const MAX_BACKOFF = 30000; const INTENTIONAL_RECONNECT_DELAY_MS = 2500; const WS_HANDSHAKE_TIMEOUT_MS = 3500; const MIN_WS_RETRY_PROBE_DELAY_MS = 3000; const MAX_WS_RETRY_PROBE_DELAY_MS = 30000; const ports = new Set(); const subscribedTypes = new Set(); const requestPorts = new Map(); let transportMode = 'sse'; let preferredTransportMode = 'sse'; let wsUrl = ''; let csrfToken = ''; let es = null; let ws = null; let backoff = MIN_BACKOFF; let reconnectTimer = null; let wsConnectTimer = null; let wsRetryProbeTimer = null; let wsRetryProbeDelay = MIN_WS_RETRY_PROBE_DELAY_MS; let lastCursor = ''; let isInitialized = false; let activeTransportMode = 'poll'; function debugLog() { if (typeof console === 'undefined' || typeof console.log !== 'function') { return; } console.log.apply(console, arguments); } self.onconnect = function (e) { const port = e.ports[0]; ports.add(port); port.onmessage = function (msg) { const data = msg.data || {}; switch (data.action) { case 'init': initializeConfig(data.config || {}); isInitialized = true; ensureTransport(); break; case 'subscribe': subscribeType(String(data.type || '').trim()); break; case 'unsubscribe': unsubscribeType(String(data.type || '').trim()); break; case 'command': handleCommand(port, data); break; case 'disconnect': forgetPort(port); break; } }; port.start(); // Push the current cursor to this port immediately so that UserStorage is // up-to-date as soon as the page connects, even if no new events arrive // before the next reconnect cycle. Without this, a refresh where the // SharedWorker survives but no events fire leaves UserStorage pointing at // a stale position, causing replay if the worker is later restarted. if (lastCursor) { try { port.postMessage({ type: '__cursor', data: { cursor: lastCursor } }); } catch (_) {} } if (activeTransportMode) { try { port.postMessage({ type: '__transport', data: { mode: activeTransportMode } }); } catch (_) {} } }; function initializeConfig(config) { const rawTransportMode = String(config.transportMode || 'sse'); const rawPreferredTransportMode = String(config.preferredTransportMode || 'sse'); const mode = String(config.transportMode || 'sse').toLowerCase(); transportMode = ['auto', 'sse', 'ws'].includes(mode) ? mode : 'sse'; const preferred = String(config.preferredTransportMode || 'sse').toLowerCase(); preferredTransportMode = ['sse', 'ws'].includes(preferred) ? preferred : 'sse'; wsUrl = typeof config.wsUrl === 'string' ? config.wsUrl : ''; csrfToken = typeof config.csrfToken === 'string' ? config.csrfToken : ''; // Seed the cursor from the client's persisted value if the worker doesn't // already have one (e.g. first tab after a worker restart). if (!lastCursor && typeof config.cursor === 'string' && config.cursor) { lastCursor = config.cursor; } if (preferredTransportMode === 'ws') { wsRetryProbeDelay = MIN_WS_RETRY_PROBE_DELAY_MS; } debugLog('[BinkStream worker] init', { rawTransportMode: rawTransportMode, rawPreferredTransportMode: rawPreferredTransportMode, rawWsUrl: typeof config.wsUrl === 'string' ? config.wsUrl : '(missing)', configuredTransportMode: transportMode, preferredTransportMode: preferredTransportMode, wsUrl: wsUrl || '(default)', cursor: lastCursor || '(none)' }); } function effectiveTransportMode() { return transportMode === 'auto' ? preferredTransportMode : transportMode; } function ensureTransport() { if (!isInitialized) { return; } if (ports.size === 0) { activeTransportMode = 'poll'; closeTransport(); return; } debugLog('[BinkStream worker] ensureTransport', { configuredTransportMode: transportMode, activeTransportMode: effectiveTransportMode(), ports: ports.size }); if (effectiveTransportMode() === 'ws') { if (!ws || ws.readyState === WebSocket.CLOSED) { connectWebSocket(); } return; } if (!es || es.readyState === EventSource.CLOSED) { connectSse(); } } function closeTransport() { clearReconnectTimer(); clearWsConnectTimer(); clearWsRetryProbeTimer(); if (es) { es.close(); es = null; } if (ws) { try { ws.close(); } catch (_) {} ws = null; } } function clearReconnectTimer() { if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } } function clearWsConnectTimer() { if (wsConnectTimer) { clearTimeout(wsConnectTimer); wsConnectTimer = null; } } function clearWsRetryProbeTimer() { if (wsRetryProbeTimer) { clearTimeout(wsRetryProbeTimer); wsRetryProbeTimer = null; } } function scheduleReconnect() { if (ports.size === 0) { activeTransportMode = 'poll'; closeTransport(); return; } clearReconnectTimer(); reconnectTimer = setTimeout(function () { reconnectTimer = null; ensureTransport(); }, backoff); backoff = Math.min(backoff * 2, MAX_BACKOFF); } function scheduleWsRetryProbe() { if (transportMode !== 'auto' || preferredTransportMode !== 'sse' || ports.size === 0) { return; } if (wsRetryProbeTimer) { return; } const delay = wsRetryProbeDelay; wsRetryProbeTimer = setTimeout(function () { wsRetryProbeTimer = null; if (transportMode !== 'auto' || preferredTransportMode !== 'sse' || ports.size === 0) { return; } debugLog('[BinkStream worker] probing WebSocket transport again from SSE fallback'); preferredTransportMode = 'ws'; ensureTransport(); }, delay); wsRetryProbeDelay = Math.min(wsRetryProbeDelay * 2, MAX_WS_RETRY_PROBE_DELAY_MS); } function subscribeType(type) { if (!type || subscribedTypes.has(type)) { return; } subscribedTypes.add(type); if (es && es.readyState !== EventSource.CLOSED) { addSseListener(type, es); } if (ws && ws.readyState === WebSocket.OPEN) { sendWsJson({ action: 'subscribe', type: type }); } } function unsubscribeType(type) { if (!type) { return; } subscribedTypes.delete(type); if (ws && ws.readyState === WebSocket.OPEN) { sendWsJson({ action: 'unsubscribe', type: type }); } } function connectSse() { debugLog('[BinkStream worker] trying SSE transport', { cursor: lastCursor || '(none)' }); clearReconnectTimer(); if (ws) { try { ws.close(); } catch (_) {} ws = null; } if (es) { es.close(); } const url = lastCursor ? STREAM_URL + '?cursor=' + encodeURIComponent(lastCursor) : STREAM_URL; es = new EventSource(url); const current = es; current.addEventListener('connected', function (e) { if (current !== es) { return; } backoff = MIN_BACKOFF; if (e.lastEventId) { lastCursor = e.lastEventId; broadcastCursor(lastCursor); } debugLog('[BinkStream worker] using SSE transport', { cursor: lastCursor || '(none)' }); broadcastTransportMode('sse'); scheduleWsRetryProbe(); broadcast('connected', tryParse(e.data)); }); current.addEventListener('reconnect', function (e) { if (current !== es) { return; } if (e.lastEventId) { lastCursor = e.lastEventId; broadcastCursor(lastCursor); } current.close(); es = null; broadcastTransportMode('sse-reconnecting'); scheduleWsRetryProbe(); clearReconnectTimer(); reconnectTimer = setTimeout(function () { reconnectTimer = null; ensureTransport(); }, INTENTIONAL_RECONNECT_DELAY_MS); }); current.addEventListener('error', function () { if (current !== es) { return; } // Sync cursor from the browser's native lastEventId before closing. // The browser advances lastEventId for every SSE id: field regardless // of whether a named-event listener is registered, so this ensures the // cursor advances past events whose types are not currently subscribed // (e.g. admin events on a non-admin page, or events the page hasn't // subscribed to yet). Without this, unsubscribed events are replayed // on every reconnect because lastCursor never moves past them. const nativeId = current.lastEventId; if (nativeId && nativeId !== lastCursor) { lastCursor = nativeId; broadcastCursor(lastCursor); } current.close(); es = null; broadcastTransportMode('sse-reconnecting'); scheduleWsRetryProbe(); scheduleReconnect(); }); subscribedTypes.forEach(function (type) { addSseListener(type, current); }); } function addSseListener(type, targetEs) { targetEs.addEventListener(type, function (e) { if (targetEs !== es) { return; } if (e.lastEventId) { lastCursor = e.lastEventId; broadcastCursor(lastCursor); } broadcast(type, tryParse(e.data)); }); } function connectWebSocket() { debugLog('[BinkStream worker] trying WebSocket transport', { url: wsUrl || defaultWsUrl(), cursor: lastCursor || '(none)' }); clearReconnectTimer(); clearWsConnectTimer(); clearWsRetryProbeTimer(); if (es) { es.close(); es = null; } if (ws) { try { ws.close(); } catch (_) {} } const baseUrl = wsUrl || defaultWsUrl(); const socketUrl = lastCursor ? appendCursor(baseUrl, lastCursor) : baseUrl; ws = new WebSocket(socketUrl); const current = ws; let handshakeComplete = false; if (transportMode === 'auto') { wsConnectTimer = setTimeout(function () { if (current !== ws || handshakeComplete) { return; } debugLog('[BinkStream worker] websocket handshake timed out, falling back to SSE'); try { current.close(); } catch (_) {} ws = null; preferredTransportMode = 'sse'; connectSse(); }, WS_HANDSHAKE_TIMEOUT_MS); } current.onopen = function () { if (current !== ws) { return; } backoff = MIN_BACKOFF; debugLog('[BinkStream worker] websocket open'); subscribedTypes.forEach(function (type) { sendWsJson({ action: 'subscribe', type: type }); }); }; current.onmessage = function (event) { if (current !== ws) { return; } let payload = null; try { payload = JSON.parse(event.data); } catch (_) { return; } if (!payload || !payload.type) { return; } if (payload.id) { lastCursor = String(payload.id); broadcastCursor(lastCursor); } if (payload.type === 'connected') { handshakeComplete = true; clearWsConnectTimer(); clearWsRetryProbeTimer(); wsRetryProbeDelay = MIN_WS_RETRY_PROBE_DELAY_MS; debugLog('[BinkStream worker] using WebSocket transport', { cursor: lastCursor || '(none)' }); broadcastTransportMode('ws'); } if (payload.type === 'command_result' && payload.requestId) { const port = requestPorts.get(payload.requestId); if (port) { requestPorts.delete(payload.requestId); try { port.postMessage(payload); } catch (_) {} } return; } broadcast(payload.type, payload.data); }; current.onerror = function () { if (current !== ws) { return; } clearWsConnectTimer(); debugLog('[BinkStream worker] websocket error'); try { current.close(); } catch (_) {} }; current.onclose = function () { if (current !== ws) { return; } clearWsConnectTimer(); ws = null; if (transportMode === 'auto' && !handshakeComplete) { debugLog('[BinkStream worker] websocket unavailable, switching to SSE'); preferredTransportMode = 'sse'; connectSse(); return; } // WS was working (or explicit WS mode) and just closed ? signal reconnecting. broadcastTransportMode('reconnecting'); debugLog('[BinkStream worker] websocket closed, scheduling reconnect'); scheduleReconnect(); }; } function handleCommand(port, data) { const requestId = String(data.requestId || '').trim(); const command = String(data.command || '').trim(); const payload = data.payload && typeof data.payload === 'object' ? data.payload : {}; if (!requestId || !command) { respondToPort(port, { type: 'command_result', requestId: requestId, success: false, error: 'Invalid realtime command payload', errorCode: 'errors.realtime.invalid_payload' }); return; } if (effectiveTransportMode() === 'ws') { if (!ws || ws.readyState !== WebSocket.OPEN) { respondToPort(port, { type: 'command_result', requestId: requestId, success: false, error: 'Realtime websocket is not connected' }); return; } requestPorts.set(requestId, port); sendWsJson({ action: 'command', requestId: requestId, command: command, payload: payload }); return; } sendHttpCommand(requestId, command, payload).then(function (result) { respondToPort(port, { type: 'command_result', requestId: requestId, success: true, result: result }); }).catch(function (error) { const payload = error && error.payload ? error.payload : {}; const response = { type: 'command_result', requestId: requestId, success: false, error: payload.error || error.message || 'Realtime command failed' }; if (payload.error_code) { response.errorCode = payload.error_code; } respondToPort(port, { type: response.type, requestId: response.requestId, success: response.success, error: response.error, errorCode: response.errorCode }); }); } function sendHttpCommand(requestId, command, payload) { const headers = { 'Content-Type': 'application/json' }; if (csrfToken) { headers['X-CSRF-Token'] = csrfToken; } return fetch(STREAM_URL, { method: 'POST', headers: headers, credentials: 'same-origin', body: JSON.stringify({ command: command, payload: payload }) }).then(function (response) { return response.json().then(function (body) { if (!response.ok || body.success === false) { const error = new Error(body.error || 'Realtime command failed'); error.payload = body; throw error; } return body.result; }); }); } function sendWsJson(payload) { if (!ws || ws.readyState !== WebSocket.OPEN) { return; } try { ws.send(JSON.stringify(payload)); } catch (_) {} } function respondToPort(port, payload) { try { port.postMessage(payload); } catch (_) {} } function broadcast(type, data) { const deadPorts = []; ports.forEach(function (port) { try { port.postMessage({ type: type, data: data }); } catch (_) { deadPorts.push(port); } }); deadPorts.forEach(function (port) { ports.delete(port); }); if (ports.size === 0) { activeTransportMode = 'poll'; closeTransport(); } } function broadcastTransportMode(mode) { activeTransportMode = mode; broadcast('__transport', { mode: mode }); } function forgetPort(port) { ports.delete(port); requestPorts.forEach(function (requestPort, requestId) { if (requestPort === port) { requestPorts.delete(requestId); } }); if (ports.size === 0) { activeTransportMode = 'poll'; closeTransport(); } } function broadcastCursor(cursor) { broadcast('__cursor', { cursor: cursor }); } function tryParse(str) { try { return JSON.parse(str); } catch (_) { return str; } } function defaultWsUrl() { const protocol = self.location.protocol === 'https:' ? 'wss:' : 'ws:'; return protocol + '//' + self.location.host + '/ws'; } function appendCursor(url, cursor) { const separator = url.indexOf('?') === -1 ? '?' : '&'; return url + separator + 'cursor=' + encodeURIComponent(cursor); }