tests
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const btnSend = document.getElementById('btnSend');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const publicKeyEl = document.getElementById('publicKey');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
@@ -72,6 +73,8 @@
|
||||
conn.on('error', (err) => log('Peer error: ' + err.message, 'err'));
|
||||
});
|
||||
await swarm.join(topic);
|
||||
const pubKey = await swarm.getPublicKey();
|
||||
publicKeyEl.textContent = 'Your public key: ' + pubKey;
|
||||
setStatus('Joined "' + topic + '". Send a message or open this page in another tab.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="peers" id="publicKey">Your public key: (join to generate)</div>
|
||||
<div class="row">
|
||||
<input type="text" id="message" placeholder="Type a message..." disabled>
|
||||
<button class="primary" id="btnSend" disabled>Send</button>
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
window.BridgeSwarm.ready().then(async function () {
|
||||
const swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-data' });
|
||||
const pubKey = await swarm.getPublicKey();
|
||||
document.getElementById('publicKey').textContent = 'Your public key: ' + pubKey;
|
||||
await swarm.destroy();
|
||||
log('Data API ready. Use the buttons above.');
|
||||
|
||||
document.getElementById('coreInfo').onclick = async function () {
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<body>
|
||||
<h1>BridgeSwarm – Data API Demo</h1>
|
||||
<p>Uses <code>BridgeSwarm.request(type, payload)</code> to call Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb in the native host.</p>
|
||||
<div class="section">
|
||||
<div class="row">
|
||||
<span id="publicKey">Your public key: (loading...)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hypercore</h2>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const publicKeyEl = document.getElementById('publicKey');
|
||||
const connSelectRow = document.getElementById('connSelectRow');
|
||||
const connSelect = document.getElementById('connSelect');
|
||||
const btnEnableHrpc = document.getElementById('btnEnableHrpc');
|
||||
@@ -150,6 +151,8 @@
|
||||
});
|
||||
|
||||
await swarm.join(topic);
|
||||
const pubKey = await swarm.getPublicKey();
|
||||
publicKeyEl.textContent = 'Your public key: ' + pubKey;
|
||||
setStatus('Joined "' + topic + '". Open another tab and join the same topic; HRPC enables automatically.', 'ok');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<div class="section">
|
||||
<h2>2. Connection & HRPC <span class="badge">attachHrpc, hrpcInvoke</span></h2>
|
||||
<p id="peers">Peers: 0</p>
|
||||
<p id="publicKey">Your public key: (join to generate)</p>
|
||||
<div class="row" id="connSelectRow" style="display:none;">
|
||||
<label for="connSelect" style="font-size:0.85rem; color:#a9b1d6;">Use connection:</label>
|
||||
<select id="connSelect" style="min-width:180px; padding:0.35rem 0.5rem; border-radius:6px; border:1px solid #3b4261; background:#24283b; color:#c0caf5; font-size:0.85rem;"></select>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const btnShare = document.getElementById('btnShare');
|
||||
const btnStopShare = document.getElementById('btnStopShare');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const publicKeyEl = document.getElementById('publicKey');
|
||||
const remoteVideo = document.getElementById('remoteVideo');
|
||||
const videoPlaceholder = document.getElementById('videoPlaceholder');
|
||||
const placeholderBlock = document.getElementById('placeholderBlock');
|
||||
@@ -267,6 +268,8 @@
|
||||
conn.on('error', function (err) { console.warn('Peer error:', err); });
|
||||
});
|
||||
await swarm.join(topic);
|
||||
const pubKey = await swarm.getPublicKey();
|
||||
publicKeyEl.textContent = 'Your public key: ' + pubKey;
|
||||
setStatus('Joined. Share your screen now or wait for viewers.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="peers" id="publicKey">Your public key: (join to generate)</div>
|
||||
<div class="row">
|
||||
<button class="primary" id="btnShare" disabled>Share my screen</button>
|
||||
<button class="secondary" id="btnStopShare" disabled style="display: none;">Stop sharing</button>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const publicKeyEl = document.getElementById('publicKey');
|
||||
const connStatusEl = document.getElementById('connStatus');
|
||||
const rawMsgEl = document.getElementById('rawMsg');
|
||||
const btnRawSend = document.getElementById('btnRawSend');
|
||||
@@ -125,6 +126,8 @@
|
||||
});
|
||||
|
||||
await swarm.join(topic);
|
||||
const pubKey = await swarm.getPublicKey();
|
||||
publicKeyEl.textContent = 'Your public key: ' + pubKey;
|
||||
setStatus('Joined "' + topic + '". Connect another tab to try raw and Protomux messages.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<div class="section">
|
||||
<h2>2. Connections <span class="badge">peerInfo, connection events</span></h2>
|
||||
<p id="peers">Peers: 0</p>
|
||||
<p id="publicKey">Your public key: (join to generate)</p>
|
||||
<p class="status ok" id="connStatus" style="display:none;">On connection you get (conn, peerInfo). peerInfo: publicKey (hex), topics (hex[]).</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const btnClear = document.getElementById('btnClear');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const publicKeyEl = document.getElementById('publicKey');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
@@ -202,6 +203,8 @@
|
||||
conn.on('error', () => {});
|
||||
});
|
||||
await swarm.join(topic);
|
||||
const pubKey = await swarm.getPublicKey();
|
||||
publicKeyEl.textContent = 'Your public key: ' + pubKey;
|
||||
setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<button class="secondary" id="btnClear" disabled>Clear board</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="peers" id="publicKey">Your public key: (join to generate)</div>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="canvas" width="800" height="500"></canvas>
|
||||
</div>
|
||||
|
||||
+25
-1
@@ -113,7 +113,8 @@
|
||||
defaults.maxPeers != null && defaults.maxPeers > 0 && opts.maxPeers == null && { maxPeers: defaults.maxPeers },
|
||||
opts
|
||||
);
|
||||
this.swarmId = 'swarm_' + generateId();
|
||||
// Use appName as swarmId so multiple tabs share the same swarm
|
||||
this.swarmId = 'swarm_' + (this.options.appName || 'default');
|
||||
this._initPromise = null;
|
||||
this._eventUnsub = null;
|
||||
this._boundOnEvent = this._onEvent.bind(this);
|
||||
@@ -129,6 +130,11 @@
|
||||
payload: { swarmId: this.swarmId, options: this.options },
|
||||
}).then((res) => {
|
||||
if (res && !res.ok) throw new Error(res.error || 'Init failed');
|
||||
sendToBridge({
|
||||
id: generateId(),
|
||||
type: 'registerSwarm',
|
||||
payload: { swarmId: this.swarmId },
|
||||
}).catch(() => {});
|
||||
this._listenEvents();
|
||||
return res;
|
||||
});
|
||||
@@ -147,6 +153,7 @@
|
||||
const msg = e.detail;
|
||||
if (!msg || msg.type !== 'event') return;
|
||||
const payload = msg.payload || {};
|
||||
console.log('[BridgeSwarm-api] Event received:', msg.event, 'connId:', payload.connId, 'swarmId:', payload.swarmId);
|
||||
if (payload.swarmId !== this.swarmId) return;
|
||||
|
||||
if (msg.event === 'connection') {
|
||||
@@ -155,6 +162,7 @@
|
||||
this._connections.set(payload.connId, conn);
|
||||
conn.on('end', () => { if (this._connections) this._connections.delete(payload.connId); });
|
||||
const peerInfo = payload.peerInfo || {};
|
||||
console.log('[BridgeSwarm-api] Emitting connection event for connId:', payload.connId);
|
||||
this.emit('connection', conn, peerInfo);
|
||||
return;
|
||||
}
|
||||
@@ -167,6 +175,7 @@
|
||||
return;
|
||||
}
|
||||
if (msg.event === 'end' || msg.event === 'error') {
|
||||
console.log('[BridgeSwarm-api] END event for connId:', payload.connId);
|
||||
const conn = this._connections && this._connections.get(payload.connId);
|
||||
if (conn) {
|
||||
conn.destroyed = true;
|
||||
@@ -213,6 +222,21 @@
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the public key (hex) of this swarm's key pair.
|
||||
* @returns {Promise<string>} Public key as 64-character hex string.
|
||||
*/
|
||||
BridgeSwarm.prototype.getPublicKey = function () {
|
||||
return sendToBridge({
|
||||
id: generateId(),
|
||||
type: 'swarmKey',
|
||||
payload: { swarmId: this.swarmId },
|
||||
}).then((res) => {
|
||||
if (res && !res.ok) throw new Error(res.error || 'Failed to get public key');
|
||||
return res.publicKey;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Snapshot of current connection objects. After destroy() returns [].
|
||||
* @returns {BridgeSwarmConnection[]}
|
||||
|
||||
+200
-1
@@ -7,6 +7,29 @@ const HOST_NAME = 'com.bridgeswarm';
|
||||
const MAX_RECONNECT_DELAY = 30000;
|
||||
const INITIAL_RECONNECT_DELAY = 100;
|
||||
|
||||
function log(...args) {
|
||||
console.log('[BridgeSwarm-bg]', ...args);
|
||||
// Store logs for dashboard
|
||||
const timestamp = Date.now();
|
||||
const message = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
|
||||
logs.push({ timestamp, message, level: 'info' });
|
||||
if (logs.length > MAX_LOGS) logs.shift();
|
||||
broadcastLogs();
|
||||
}
|
||||
|
||||
const logs = [];
|
||||
const MAX_LOGS = 500;
|
||||
|
||||
function broadcastLogs() {
|
||||
// Send recent logs to any dashboard that's open
|
||||
for (const tabId of dashboardTabs) {
|
||||
browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-logs', logs: logs.slice(-50) }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Track dashboard tabs
|
||||
const dashboardTabs = new Set();
|
||||
|
||||
const browser = typeof chrome !== 'undefined' && chrome.runtime?.connectNative
|
||||
? chrome
|
||||
: typeof browser !== 'undefined'
|
||||
@@ -23,9 +46,52 @@ const pending = new Map();
|
||||
// Tabs that have subscribed to native host events (content script registered)
|
||||
const subscribedTabs = new Set();
|
||||
|
||||
// Track which swarmIds belong to which tab: tabId -> Set<swarmId>
|
||||
const tabSwarms = new Map();
|
||||
// Track how many tabs are using each swarm: swarmId -> count
|
||||
const swarmRefCount = new Map();
|
||||
|
||||
// Track active connections from native host: connId -> { swarmId, peerKey, createdAt }
|
||||
const activeConnections = new Map();
|
||||
|
||||
const SETTINGS_KEY = 'bridgeSwarmSettings';
|
||||
let notifyOnDisconnect = false;
|
||||
|
||||
// Extension state for the UI
|
||||
const extensionState = {
|
||||
hostConnected: false,
|
||||
swarms: new Map(), // swarmId -> { appName, tabCount, connections: [], createdAt }
|
||||
stats: {
|
||||
totalConnections: 0,
|
||||
totalSwarms: 0,
|
||||
uptime: Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
function updateExtensionState() {
|
||||
extensionState.hostConnected = !!port;
|
||||
extensionState.stats.totalConnections = activeConnections.size;
|
||||
|
||||
// Update swarms from tabSwarms
|
||||
extensionState.swarms.clear();
|
||||
for (const [swarmId, count] of swarmRefCount) {
|
||||
const connections = [];
|
||||
for (const [connId, conn] of activeConnections) {
|
||||
if (conn.swarmId === swarmId) {
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
extensionState.swarms.set(swarmId, {
|
||||
swarmId,
|
||||
appName: swarmId.replace('swarm_', ''),
|
||||
tabCount: count,
|
||||
connections,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
extensionState.stats.totalSwarms = extensionState.swarms.size;
|
||||
}
|
||||
|
||||
function loadNotifySetting() {
|
||||
browser.storage.local.get(SETTINGS_KEY, (result) => {
|
||||
const s = result[SETTINGS_KEY] || {};
|
||||
@@ -43,8 +109,9 @@ browser.storage.onChanged.addListener((changes, areaName) => {
|
||||
function connect() {
|
||||
try {
|
||||
port = browser.runtime.connectNative(HOST_NAME);
|
||||
log('Connected to native host');
|
||||
} catch (e) {
|
||||
console.error('[BridgeSwarm] connectNative failed:', e);
|
||||
log('connectNative failed:', e);
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
@@ -52,6 +119,37 @@ function connect() {
|
||||
reconnectDelay = INITIAL_RECONNECT_DELAY;
|
||||
|
||||
port.onMessage.addListener((msg) => {
|
||||
log('Received from native:', msg.type, msg.event || '');
|
||||
|
||||
// Track connection events
|
||||
if (msg.type === 'event') {
|
||||
const payload = msg.payload || {};
|
||||
if (msg.event === 'connection') {
|
||||
const connId = payload.connId;
|
||||
const swarmId = payload.swarmId;
|
||||
const peerInfo = payload.peerInfo || {};
|
||||
log('Connection established:', connId, 'peer:', peerInfo.publicKey?.slice(0, 8));
|
||||
activeConnections.set(connId, {
|
||||
connId,
|
||||
swarmId,
|
||||
peerKey: peerInfo.publicKey || '',
|
||||
createdAt: Date.now()
|
||||
});
|
||||
updateExtensionState();
|
||||
} else if (msg.event === 'error') {
|
||||
const connId = payload.connId;
|
||||
const errorMsg = payload.message || 'Unknown error';
|
||||
log('Connection ERROR:', connId, errorMsg);
|
||||
activeConnections.delete(connId);
|
||||
updateExtensionState();
|
||||
} else if (msg.event === 'end') {
|
||||
const connId = payload.connId;
|
||||
log('Connection ended:', connId);
|
||||
activeConnections.delete(connId);
|
||||
updateExtensionState();
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type === 'response' && msg.id != null) {
|
||||
const p = pending.get(msg.id);
|
||||
if (p) {
|
||||
@@ -65,6 +163,7 @@ function connect() {
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'event') {
|
||||
log('Broadcasting event to tabs:', msg.event, 'connId:', msg.payload?.connId);
|
||||
for (const tabId of subscribedTabs) {
|
||||
browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-event', payload: msg }).catch(() => {});
|
||||
}
|
||||
@@ -72,6 +171,10 @@ function connect() {
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
log('Native host disconnected');
|
||||
extensionState.hostConnected = false;
|
||||
activeConnections.clear();
|
||||
updateExtensionState();
|
||||
port = null;
|
||||
if (notifyOnDisconnect && browser.notifications) {
|
||||
browser.notifications.create('bridgeswarm-host-disconnect', {
|
||||
@@ -103,6 +206,7 @@ function scheduleReconnect() {
|
||||
}
|
||||
|
||||
function send(msg) {
|
||||
log('Sending to native:', msg.type, msg.payload?.swarmId || '', msg.payload?.connId || '');
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!port) {
|
||||
reject(new Error('Native host not connected'));
|
||||
@@ -123,11 +227,50 @@ function send(msg) {
|
||||
loadNotifySetting();
|
||||
connect();
|
||||
|
||||
// Handle tab closure - destroy swarms only when last tab using them closes
|
||||
browser.tabs.onRemoved.addListener((tabId) => {
|
||||
log('Tab closed:', tabId);
|
||||
const swarmIds = tabSwarms.get(tabId);
|
||||
if (swarmIds) {
|
||||
for (const swarmId of swarmIds) {
|
||||
const count = (swarmRefCount.get(swarmId) || 1) - 1;
|
||||
swarmRefCount.set(swarmId, count);
|
||||
log('Swarm', swarmId, 'refcount now:', count);
|
||||
// Only destroy when last tab closes
|
||||
if (count <= 0) {
|
||||
log('Last tab for swarm', swarmId, '- destroying');
|
||||
send({ type: 'destroy', payload: { swarmId } }).catch(() => {});
|
||||
swarmRefCount.delete(swarmId);
|
||||
}
|
||||
}
|
||||
tabSwarms.delete(tabId);
|
||||
}
|
||||
subscribedTabs.delete(tabId);
|
||||
});
|
||||
|
||||
// Content script talks to background via runtime.sendMessage / onMessage
|
||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.target !== 'bridge-swarm-native') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle registerSwarm locally - don't forward to native host
|
||||
if (message.action === 'send' && message.payload?.type === 'registerSwarm') {
|
||||
const swarmId = message.payload?.payload?.swarmId;
|
||||
if (swarmId && sender.tab?.id != null) {
|
||||
if (!tabSwarms.has(sender.tab.id)) {
|
||||
tabSwarms.set(sender.tab.id, new Set());
|
||||
}
|
||||
tabSwarms.get(sender.tab.id).add(swarmId);
|
||||
// Increment reference count
|
||||
const count = swarmRefCount.get(swarmId) || 0;
|
||||
swarmRefCount.set(swarmId, count + 1);
|
||||
log('Registered swarm', swarmId, 'for tab', sender.tab.id, '- refcount:', count + 1);
|
||||
}
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.action === 'send') {
|
||||
send(message.payload)
|
||||
.then(sendResponse)
|
||||
@@ -144,4 +287,60 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle dashboard registration
|
||||
if (message.action === 'registerDashboard' && sender.tab && sender.tab.id != null) {
|
||||
dashboardTabs.add(sender.tab.id);
|
||||
log('Dashboard registered for tab', sender.tab.id);
|
||||
// Send current logs immediately
|
||||
sendResponse({ ok: true, logs: logs.slice(-50) });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.action === 'unregisterDashboard' && sender.tab && sender.tab.id != null) {
|
||||
dashboardTabs.delete(sender.tab.id);
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle getState request from UI
|
||||
if (message.action === 'getState') {
|
||||
updateExtensionState();
|
||||
// Build tabs info
|
||||
const tabsInfo = [];
|
||||
for (const [tabId, swarmIds] of tabSwarms) {
|
||||
tabsInfo.push({
|
||||
tabId,
|
||||
swarmIds: Array.from(swarmIds)
|
||||
});
|
||||
}
|
||||
sendResponse({
|
||||
ok: true,
|
||||
state: {
|
||||
hostConnected: extensionState.hostConnected,
|
||||
swarms: Array.from(extensionState.swarms.values()),
|
||||
tabs: tabsInfo,
|
||||
stats: {
|
||||
totalConnections: extensionState.stats.totalConnections,
|
||||
totalSwarms: extensionState.stats.totalSwarms,
|
||||
uptime: extensionState.stats.uptime
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle destroySwarm request from UI
|
||||
if (message.action === 'destroySwarm' && message.payload?.swarmId) {
|
||||
const swarmId = message.payload.swarmId;
|
||||
send({ type: 'destroy', payload: { swarmId } })
|
||||
.then(sendResponse)
|
||||
.catch((err) => sendResponse({ ok: false, error: err.message }));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Open dashboard when extension icon is clicked
|
||||
browser.action?.onClicked?.addListener((tab) => {
|
||||
browser.tabs.create({ url: 'dashboard.html' });
|
||||
});
|
||||
|
||||
+17
-10
@@ -7,9 +7,13 @@
|
||||
const browser = typeof chrome !== 'undefined' && chrome.runtime?.sendMessage ? chrome : typeof browser !== 'undefined' ? browser : chrome;
|
||||
|
||||
const SETTINGS_KEY = 'bridgeSwarmSettings';
|
||||
let debugMode = false;
|
||||
let debugMode = true; // Always on for debugging
|
||||
const DEFAULTS = { defaultAppName: 'bridge-swarm', defaultRequestTimeoutMs: 0, defaultMaxPeers: 0, readyTimeoutMs: 0, disableOnFileUrls: false, debug: false };
|
||||
|
||||
function log(...args) {
|
||||
console.log('[BridgeSwarm-content]', ...args);
|
||||
}
|
||||
|
||||
function injectApi() {
|
||||
function inject(src, next) {
|
||||
const script = document.createElement('script');
|
||||
@@ -32,12 +36,14 @@ function injectApi() {
|
||||
const readyTimeoutMs = typeof s.readyTimeoutMs === 'number' ? s.readyTimeoutMs : DEFAULTS.readyTimeoutMs;
|
||||
const debug = s.debug === true;
|
||||
debugMode = debug;
|
||||
const defaultsScript = document.createElement('script');
|
||||
defaultsScript.textContent =
|
||||
'window.__BRIDGESWARM_DEFAULTS__={appName:' + JSON.stringify(appName) + ',requestTimeoutMs:' + requestTimeoutMs + ',maxPeers:' + maxPeers + ',readyTimeoutMs:' + readyTimeoutMs + '};' +
|
||||
'window.__BRIDGESWARM_DEBUG__=' + (debug ? 'true' : 'false') + ';';
|
||||
(document.head || document.documentElement).appendChild(defaultsScript);
|
||||
defaultsScript.remove();
|
||||
const defaults = {
|
||||
appName: appName,
|
||||
requestTimeoutMs: requestTimeoutMs,
|
||||
maxPeers: maxPeers,
|
||||
readyTimeoutMs: readyTimeoutMs
|
||||
};
|
||||
window.__BRIDGESWARM_DEFAULTS__ = defaults;
|
||||
window.__BRIDGESWARM_DEBUG__ = debug;
|
||||
inject('api.js', () => inject('framed-stream.js', () => inject('protomux-bundle.js')));
|
||||
});
|
||||
}
|
||||
@@ -50,9 +56,11 @@ window.addEventListener('message', (event) => {
|
||||
if (event.data?.type !== 'bridge-swarm-bridge') return;
|
||||
|
||||
const { id, action, payload } = event.data;
|
||||
log('Page -> Background:', action, payload?.type || '');
|
||||
browser.runtime.sendMessage(
|
||||
{ target: 'bridge-swarm-native', action, payload },
|
||||
(response) => {
|
||||
log('Background -> Page response:', id, response?.error || 'ok');
|
||||
const err = response?.error;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('bridge-swarm-bridge-response', {
|
||||
@@ -65,16 +73,15 @@ window.addEventListener('message', (event) => {
|
||||
|
||||
// Subscribe to native host events once (from content script context)
|
||||
browser.runtime.sendMessage({ target: 'bridge-swarm-native', action: 'subscribe' }, () => {
|
||||
if (debugMode) console.log('[BridgeSwarm] Subscribed to native host events');
|
||||
log('Subscribed to native host events');
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
log('Received from background:', message.type, message.payload?.event || '', message.payload?.connId || '');
|
||||
if (message.type === 'bridge-swarm-event') {
|
||||
if (debugMode) console.log('[BridgeSwarm] event', message.payload);
|
||||
window.dispatchEvent(new CustomEvent('bridge-swarm-event', { detail: message.payload }));
|
||||
}
|
||||
if (message.type === 'bridge-swarm-host-disconnect') {
|
||||
if (debugMode) console.log('[BridgeSwarm] host disconnected');
|
||||
window.dispatchEvent(new CustomEvent('bridge-swarm-host-disconnect'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,677 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Dashboard</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f0f14;
|
||||
color: #e4e4e7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 240px;
|
||||
background: #18181b;
|
||||
border-right: 1px solid #27272a;
|
||||
padding: 1.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.logo {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid #27272a;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.logo h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.logo h1 span {
|
||||
color: #22d3ee;
|
||||
}
|
||||
.logo .version {
|
||||
font-size: 0.75rem;
|
||||
color: #71717a;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex: 1;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
color: #a1a1aa;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background: #27272a;
|
||||
color: #e4e4e7;
|
||||
}
|
||||
.nav-item.active {
|
||||
background: #22d3ee15;
|
||||
color: #22d3ee;
|
||||
border-right: 2px solid #22d3ee;
|
||||
}
|
||||
.nav-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-item .badge {
|
||||
margin-left: auto;
|
||||
background: #27272a;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.nav-item.active .badge {
|
||||
background: #22d3ee25;
|
||||
color: #22d3ee;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.main {
|
||||
margin-left: 240px;
|
||||
padding: 2rem;
|
||||
}
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
.page.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Status banner */
|
||||
.status-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.status-banner.connected {
|
||||
background: #22d3ee15;
|
||||
border: 1px solid #22d3ee35;
|
||||
}
|
||||
.status-banner.disconnected {
|
||||
background: #f43f5e15;
|
||||
border: 1px solid #f43f5e35;
|
||||
}
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
.connected .status-dot { background: #22d3ee; }
|
||||
.disconnected .status-dot { background: #f43f5e; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card-header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid #27272a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #fafafa;
|
||||
}
|
||||
.card-body {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
/* Stats grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stat-card {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: #71717a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.stat-value.accent { color: #22d3ee; }
|
||||
.stat-value.warning { color: #fbbf24; }
|
||||
.stat-value.danger { color: #f43f5e; }
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
.table th {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #71717a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background: #1f1f23;
|
||||
}
|
||||
.table td {
|
||||
font-size: 0.875rem;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
.table tr:hover td {
|
||||
background: #27272a;
|
||||
}
|
||||
.table .mono {
|
||||
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #22d3ee;
|
||||
}
|
||||
.table .empty {
|
||||
text-align: center;
|
||||
color: #52525b;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #22d3ee;
|
||||
color: #0f0f14;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #06b6d4;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #f43f5e20;
|
||||
color: #f43f5e;
|
||||
border: 1px solid #f43f5e35;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: #f43f5e35;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tag-success {
|
||||
background: #22d3ee20;
|
||||
color: #22d3ee;
|
||||
}
|
||||
.tag-warning {
|
||||
background: #fbbf2420;
|
||||
color: #fbbf24;
|
||||
}
|
||||
.tag-error {
|
||||
background: #f43f5e20;
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
/* Time ago */
|
||||
.time-ago {
|
||||
color: #52525b;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #18181b;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3f3f46;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #52525b;
|
||||
}
|
||||
|
||||
/* Refresh button */
|
||||
.refresh-btn {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #22d3ee;
|
||||
color: #0f0f14;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.refresh-btn:hover {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.refresh-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Settings section */
|
||||
.settings-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.settings-group h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #fafafa;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #27272a;
|
||||
}
|
||||
.setting-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
}
|
||||
.setting-label {
|
||||
font-size: 0.9rem;
|
||||
color: #e4e4e7;
|
||||
}
|
||||
.setting-desc {
|
||||
font-size: 0.8rem;
|
||||
color: #71717a;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.toggle {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
background: #3f3f46;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.toggle.active {
|
||||
background: #22d3ee;
|
||||
}
|
||||
.toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.toggle.active::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Logs console */
|
||||
.logs-container {
|
||||
background: #0d0d0f;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 8px;
|
||||
height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.log-entry {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-bottom: 1px solid #1f1f23;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.log-entry:hover {
|
||||
background: #18181b;
|
||||
}
|
||||
.log-time {
|
||||
color: #52525b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.log-msg {
|
||||
color: #a1a1aa;
|
||||
word-break: break-all;
|
||||
}
|
||||
.logs-toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.logs-toolbar .btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
.log-level-info { color: #22d3ee; }
|
||||
.log-level-warn { color: #fbbf24; }
|
||||
.log-level-error { color: #f43f5e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">
|
||||
<h1>🦅 <span>BridgeSwarm</span></h1>
|
||||
<div class="version">v1.0.0</div>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<div class="nav-item active" data-page="dashboard">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</div>
|
||||
<div class="nav-item" data-page="swarms">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/><circle cx="19" cy="5" r="2"/><circle cx="5" cy="19" r="2"/>
|
||||
<circle cx="5" cy="5" r="2"/><circle cx="19" cy="19" r="2"/>
|
||||
<line x1="12" y1="9" x2="12" y2="5"/><line x1="9.5" y1="14.5" x2="6" y2="17"/>
|
||||
<line x1="14.5" y1="14.5" x2="18" y2="17"/>
|
||||
</svg>
|
||||
Swarms
|
||||
<span class="badge" id="swarmCount">0</span>
|
||||
</div>
|
||||
<div class="nav-item" data-page="connections">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Connections
|
||||
<span class="badge" id="connCount">0</span>
|
||||
</div>
|
||||
<div class="nav-item" data-page="tabs">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/>
|
||||
</svg>
|
||||
Tabs
|
||||
</div>
|
||||
<div class="nav-item" data-page="logs">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/><polyline points="10,9 9,9 8,9"/>
|
||||
</svg>
|
||||
Logs
|
||||
<span class="badge" id="logCount">0</span>
|
||||
</div>
|
||||
<div class="nav-item" data-page="settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main">
|
||||
<!-- Dashboard Page -->
|
||||
<div class="page active" id="page-dashboard">
|
||||
<div class="status-banner" id="statusBanner">
|
||||
<div class="status-dot"></div>
|
||||
<span id="statusText">Connecting...</span>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Active Swarms</div>
|
||||
<div class="stat-value accent" id="dashSwarms">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Active Connections</div>
|
||||
<div class="stat-value" id="dashConnections">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Active Tabs</div>
|
||||
<div class="stat-value" id="dashTabs">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Uptime</div>
|
||||
<div class="stat-value" id="dashUptime">0s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Recent Connections</span>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Connection ID</th>
|
||||
<th>Swarm</th>
|
||||
<th>Peer Key</th>
|
||||
<th>Connected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentConnections">
|
||||
<tr><td colspan="4" class="empty">No active connections</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Swarms Page -->
|
||||
<div class="page" id="page-swarms">
|
||||
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Active Swarms</h2>
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Swarm ID</th>
|
||||
<th>App Name</th>
|
||||
<th>Connections</th>
|
||||
<th>Tab Count</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="swarmsTable">
|
||||
<tr><td colspan="5" class="empty">No active swarms</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connections Page -->
|
||||
<div class="page" id="page-connections">
|
||||
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Active Connections</h2>
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Connection ID</th>
|
||||
<th>Swarm</th>
|
||||
<th>Peer Key</th>
|
||||
<th>Connected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="connectionsTable">
|
||||
<tr><td colspan="4" class="empty">No active connections</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Page -->
|
||||
<div class="page" id="page-tabs">
|
||||
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Active Tabs</h2>
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tab ID</th>
|
||||
<th>Swarm IDs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tabsTable">
|
||||
<tr><td colspan="2" class="empty">No active tabs</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Page -->
|
||||
<div class="page" id="page-settings">
|
||||
<h2 style="margin-bottom: 1.5rem; font-size: 1.25rem;">Extension Settings</h2>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="settings-group">
|
||||
<h3>Notifications</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Host Disconnect</div>
|
||||
<div class="setting-desc">Show notification when native host disconnects</div>
|
||||
</div>
|
||||
<div class="toggle" id="toggleNotify" data-setting="notifyOnDisconnect"></div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Debug Mode</div>
|
||||
<div class="setting-desc">Log events to console for troubleshooting</div>
|
||||
</div>
|
||||
<div class="toggle" id="toggleDebug" data-setting="debug"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>Defaults</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Default App Name</div>
|
||||
<div class="setting-desc">Used when page doesn't specify appName</div>
|
||||
</div>
|
||||
<input type="text" id="defaultAppName" style="width: 200px; padding: 0.5rem; background: #27272a; border: 1px solid #3f3f46; border-radius: 6px; color: #e4e4e7;">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Default Max Peers</div>
|
||||
<div class="setting-desc">Maximum peer connections per swarm</div>
|
||||
</div>
|
||||
<input type="number" id="defaultMaxPeers" style="width: 100px; padding: 0.5rem; background: #27272a; border: 1px solid #3f3f46; border-radius: 6px; color: #e4e4e7;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<button class="btn btn-primary" id="btnSaveSettings">Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Page -->
|
||||
<div class="page" id="page-logs">
|
||||
<h2 style="margin-bottom: 1rem; font-size: 1.25rem;">System Logs</h2>
|
||||
<div class="logs-toolbar">
|
||||
<button class="btn btn-primary" id="btnClearLogs">Clear Logs</button>
|
||||
<button class="btn btn-secondary" id="btnAutoScroll">Auto-scroll: ON</button>
|
||||
</div>
|
||||
<div class="logs-container" id="logsContainer">
|
||||
<div class="empty" style="padding: 2rem; text-align: center; color: #52525b;">No logs yet</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<button class="refresh-btn" id="refreshBtn" title="Refresh">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23,4 23,10 17,10"/><polyline points="1,20 1,14 7,14"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* BridgeSwarm Dashboard - UI logic
|
||||
*/
|
||||
|
||||
const SETTINGS_KEY = 'bridgeSwarmSettings';
|
||||
let currentState = null;
|
||||
let settings = {};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
console.log('[BridgeSwarm-dashboard]', ...args);
|
||||
}
|
||||
|
||||
// Format time ago
|
||||
function timeAgo(timestamp) {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||
if (seconds < 60) return seconds + 's ago';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return minutes + 'm ago';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return hours + 'h ago';
|
||||
return Math.floor(hours / 24) + 'd ago';
|
||||
}
|
||||
|
||||
// Format uptime
|
||||
function formatUptime(ms) {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return seconds + 's';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return minutes + 'm ' + (seconds % 60) + 's';
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return hours + 'h ' + mins + 'm';
|
||||
}
|
||||
|
||||
// Truncate string
|
||||
function truncate(str, len = 16) {
|
||||
if (!str) return '';
|
||||
return str.length > len ? str.slice(0, len) + '…' : str;
|
||||
}
|
||||
|
||||
// Fetch state from background
|
||||
async function fetchState() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'bridge-swarm-native', action: 'getState' },
|
||||
(response) => {
|
||||
if (response && response.ok) {
|
||||
resolve(response.state);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Load settings
|
||||
function loadSettings() {
|
||||
chrome.storage.local.get(SETTINGS_KEY, (result) => {
|
||||
settings = result[SETTINGS_KEY] || {};
|
||||
updateSettingsUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Update settings UI
|
||||
function updateSettingsUI() {
|
||||
$('toggleNotify').classList.toggle('active', settings.notifyOnDisconnect === true);
|
||||
$('toggleDebug').classList.toggle('active', settings.debug === true);
|
||||
$('defaultAppName').value = settings.defaultAppName || 'bridge-swarm';
|
||||
$('defaultMaxPeers').value = settings.defaultMaxPeers || '';
|
||||
}
|
||||
|
||||
// Save settings
|
||||
function saveSettings() {
|
||||
settings.notifyOnDisconnect = $('toggleNotify').classList.contains('active');
|
||||
settings.debug = $('toggleDebug').classList.contains('active');
|
||||
settings.defaultAppName = $('defaultAppName').value || 'bridge-swarm';
|
||||
settings.defaultMaxPeers = parseInt($('defaultMaxPeers').value) || 0;
|
||||
|
||||
chrome.storage.local.set({ [SETTINGS_KEY]: settings }, () => {
|
||||
log('Settings saved');
|
||||
});
|
||||
}
|
||||
|
||||
// Update dashboard
|
||||
function updateDashboard(state) {
|
||||
currentState = state;
|
||||
|
||||
// Status banner
|
||||
const banner = $('statusBanner');
|
||||
const statusText = $('statusText');
|
||||
if (state.hostConnected) {
|
||||
banner.className = 'status-banner connected';
|
||||
statusText.textContent = 'Native host connected';
|
||||
} else {
|
||||
banner.className = 'status-banner disconnected';
|
||||
statusText.textContent = 'Native host disconnected';
|
||||
}
|
||||
|
||||
// Stats
|
||||
$('dashSwarms').textContent = state.stats.totalSwarms;
|
||||
$('dashConnections').textContent = state.stats.totalConnections;
|
||||
$('dashTabs').textContent = state.tabs.length;
|
||||
$('dashUptime').textContent = formatUptime(Date.now() - state.stats.uptime);
|
||||
|
||||
// Badges
|
||||
$('swarmCount').textContent = state.stats.totalSwarms;
|
||||
$('connCount').textContent = state.stats.totalConnections;
|
||||
|
||||
// Recent connections
|
||||
const connections = [];
|
||||
for (const swarm of state.swarms) {
|
||||
for (const conn of swarm.connections) {
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
|
||||
const recentConnBody = $('recentConnections');
|
||||
if (connections.length === 0) {
|
||||
recentConnBody.innerHTML = '<tr><td colspan="4" class="empty">No active connections</td></tr>';
|
||||
} else {
|
||||
recentConnBody.innerHTML = connections.slice(0, 10).map(conn => `
|
||||
<tr>
|
||||
<td class="mono">${truncate(conn.connId)}</td>
|
||||
<td>${truncate(conn.swarmId, 12)}</td>
|
||||
<td class="mono">${truncate(conn.peerKey, 12)}</td>
|
||||
<td class="time-ago">${timeAgo(conn.createdAt)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Update swarms table
|
||||
function updateSwarmsTable(state) {
|
||||
const tbody = $('swarmsTable');
|
||||
if (state.swarms.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty">No active swarms</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = state.swarms.map(swarm => `
|
||||
<tr>
|
||||
<td class="mono">${truncate(swarm.swarmId, 20)}</td>
|
||||
<td>${swarm.appName}</td>
|
||||
<td>${swarm.connections.length}</td>
|
||||
<td>${swarm.tabCount}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm" onclick="destroySwarm('${swarm.swarmId}')">Destroy</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update connections table
|
||||
function updateConnectionsTable(state) {
|
||||
const connections = [];
|
||||
for (const swarm of state.swarms) {
|
||||
for (const conn of swarm.connections) {
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
|
||||
const tbody = $('connectionsTable');
|
||||
if (connections.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty">No active connections</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = connections.map(conn => `
|
||||
<tr>
|
||||
<td class="mono">${truncate(conn.connId, 20)}</td>
|
||||
<td>${truncate(conn.swarmId, 16)}</td>
|
||||
<td class="mono">${truncate(conn.peerKey, 16)}</td>
|
||||
<td class="time-ago">${timeAgo(conn.createdAt)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update tabs table
|
||||
function updateTabsTable(state) {
|
||||
const tbody = $('tabsTable');
|
||||
if (state.tabs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="2" class="empty">No active tabs</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = state.tabs.map(tab => `
|
||||
<tr>
|
||||
<td>${tab.tabId}</td>
|
||||
<td>${tab.swarmIds.map(id => `<span class="mono">${truncate(id, 12)}</span>`).join(', ') || 'none'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Destroy swarm
|
||||
async function destroySwarm(swarmId) {
|
||||
if (!confirm(`Destroy swarm "${swarmId}"? This will disconnect all peers.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'bridge-swarm-native', action: 'destroySwarm', payload: { swarmId } },
|
||||
(response) => {
|
||||
if (response && response.ok) {
|
||||
log('Swarm destroyed:', swarmId);
|
||||
refresh();
|
||||
} else {
|
||||
log('Failed to destroy swarm:', response?.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Make destroySwarm global
|
||||
window.destroySwarm = destroySwarm;
|
||||
|
||||
// Refresh all data
|
||||
async function refresh() {
|
||||
const state = await fetchState();
|
||||
if (state) {
|
||||
updateDashboard(state);
|
||||
updateSwarmsTable(state);
|
||||
updateConnectionsTable(state);
|
||||
updateTabsTable(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
function setupNavigation() {
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
const pages = document.querySelectorAll('.page');
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const page = item.dataset.page;
|
||||
|
||||
// Update nav
|
||||
navItems.forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// Update page
|
||||
pages.forEach(p => p.classList.remove('active'));
|
||||
$(`page-${page}`).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEvents() {
|
||||
// Refresh button
|
||||
$('refreshBtn').addEventListener('click', refresh);
|
||||
|
||||
// Toggle switches
|
||||
document.querySelectorAll('.toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Save settings
|
||||
$('btnSaveSettings').addEventListener('click', saveSettings);
|
||||
|
||||
// Logs functionality
|
||||
let logs = [];
|
||||
let autoScroll = true;
|
||||
|
||||
// Register as dashboard
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'bridge-swarm-native', action: 'registerDashboard' },
|
||||
(response) => {
|
||||
if (response && response.logs) {
|
||||
logs = response.logs;
|
||||
updateLogsDisplay();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for log updates
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === 'bridge-swarm-logs' && message.logs) {
|
||||
logs = message.logs;
|
||||
updateLogsDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
function updateLogsDisplay() {
|
||||
const container = $('logsContainer');
|
||||
if (!container) return;
|
||||
|
||||
$('logCount').textContent = logs.length;
|
||||
|
||||
if (logs.length === 0) {
|
||||
container.innerHTML = '<div class="empty" style="padding: 2rem; text-align: center; color: #52525b;">No logs yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = logs.map(entry => {
|
||||
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||
return `<div class="log-entry">
|
||||
<span class="log-time">${time}</span>
|
||||
<span class="log-msg">${escapeHtml(entry.message)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
if (autoScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Clear logs button
|
||||
$('btnClearLogs')?.addEventListener('click', () => {
|
||||
logs = [];
|
||||
updateLogsDisplay();
|
||||
});
|
||||
|
||||
// Auto-scroll toggle
|
||||
$('btnAutoScroll')?.addEventListener('click', () => {
|
||||
autoScroll = !autoScroll;
|
||||
$('btnAutoScroll').textContent = 'Auto-scroll: ' + (autoScroll ? 'ON' : 'OFF');
|
||||
});
|
||||
|
||||
// Unregister dashboard on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'bridge-swarm-native', action: 'unregisterDashboard' },
|
||||
() => {}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
log('Dashboard initializing...');
|
||||
setupNavigation();
|
||||
setupEvents();
|
||||
loadSettings();
|
||||
await refresh();
|
||||
|
||||
// Auto-refresh every 2 seconds
|
||||
setInterval(refresh, 2000);
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Default settings injected into the page before api.js loads.
|
||||
*/
|
||||
(function () {
|
||||
var key = '__BRIDGESWARM_DEFAULTS__';
|
||||
if (typeof window[key] === 'undefined') {
|
||||
window[key] = {};
|
||||
}
|
||||
})();
|
||||
@@ -8,8 +8,12 @@
|
||||
"nativeMessaging",
|
||||
"storage",
|
||||
"scripting",
|
||||
"notifications"
|
||||
"notifications",
|
||||
"tabs"
|
||||
],
|
||||
"action": {
|
||||
"default_title": "BridgeSwarm Dashboard"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
@@ -29,7 +33,10 @@
|
||||
"resources": [
|
||||
"api.js",
|
||||
"framed-stream.js",
|
||||
"protomux-bundle.js"
|
||||
"protomux-bundle.js",
|
||||
"defaults.js",
|
||||
"dashboard.html",
|
||||
"dashboard.js"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
|
||||
@@ -80,6 +80,16 @@
|
||||
<body>
|
||||
<h1>BridgeSwarm</h1>
|
||||
<p class="sub">Extension settings. Changes apply to new swarms and requests.</p>
|
||||
|
||||
<p style="margin-bottom: 1.5rem;">
|
||||
<a href="dashboard.html" target="_blank" style="color: #7aa2f7; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||
</svg>
|
||||
Open Dashboard →
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>Swarm defaults</h2>
|
||||
|
||||
+54
-1
@@ -12,12 +12,40 @@ const Hyperdrive = require('hyperdrive');
|
||||
const Autobase = require('autobase');
|
||||
const HyperDB = require('hyperdb');
|
||||
const path = require('bare-path');
|
||||
const fs = require('bare-fs');
|
||||
const { Duplex } = require('bare-stream');
|
||||
const c = require('compact-encoding');
|
||||
const def = require(path.join(path.dirname(require.resolve('hyperdb')), 'lib', 'definition.js'));
|
||||
const minimalDefinition = require('./hyperdb-minimal-definition.js');
|
||||
const b4a = require('b4a');
|
||||
|
||||
// File-based logging
|
||||
const LOG_FILE = path.join(__dirname, 'bridge-swarm.log');
|
||||
let logStream = null;
|
||||
|
||||
function getLogStream() {
|
||||
if (!logStream) {
|
||||
try {
|
||||
const logPath = process.env.BRIDGE_SWARM_LOG || LOG_FILE;
|
||||
logStream = fs.createWriteStream(logPath, { flags: 'a' });
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return logStream;
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
const stream = getLogStream();
|
||||
if (stream) {
|
||||
const msg = '[' + new Date().toISOString() + '] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + '\n';
|
||||
stream.write(msg);
|
||||
}
|
||||
if (process.stderr) {
|
||||
process.stderr.write('[' + new Date().toISOString() + '] ' + args.join(' ') + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Load generated HRPC lazily so a missing/broken spec does not crash the host at startup
|
||||
let HRPC = null;
|
||||
let hrpcLoadError = null;
|
||||
@@ -176,21 +204,26 @@ async function handleMessageAsync(send, msg) {
|
||||
}
|
||||
function closeConnection() {
|
||||
if (entry._closed) return;
|
||||
log('closeConnection called for', connId);
|
||||
entry._closed = true;
|
||||
entry.socket.removeAllListeners();
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
if (peerMap.get(peerKeyHex) === connId) peerMap.delete(peerKeyHex);
|
||||
log('EMIT end event for conn', connId, 'swarm', swarmId);
|
||||
emit('end', { connId, swarmId });
|
||||
}
|
||||
function onEnd() {
|
||||
log('socket end for conn', connId, 'peer', peerKeyHex.slice(0, 8));
|
||||
closeConnection();
|
||||
}
|
||||
function onError(err) {
|
||||
log('socket ERROR for conn', connId, ':', err.message, err.stack);
|
||||
if (!entry._closed) emit('error', { connId, swarmId, message: err.message });
|
||||
closeConnection();
|
||||
}
|
||||
function onClose() {
|
||||
function onClose(hadError) {
|
||||
log('socket close for conn', connId, 'peer', peerKeyHex.slice(0, 8), 'hadError:', hadError);
|
||||
closeConnection();
|
||||
}
|
||||
entry._listeners = { onData, onEnd, onError, onClose };
|
||||
@@ -201,6 +234,7 @@ async function handleMessageAsync(send, msg) {
|
||||
socket.on('end', onEnd);
|
||||
socket.on('error', onError);
|
||||
socket.on('close', onClose);
|
||||
log('new connection', connId, 'from peer', peerKeyHex.slice(0, 8));
|
||||
emit('connection', {
|
||||
connId,
|
||||
swarmId,
|
||||
@@ -229,6 +263,22 @@ async function handleMessageAsync(send, msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'swarmKey': {
|
||||
const { swarmId } = payload;
|
||||
const swarm = swarms.get(swarmId);
|
||||
if (!swarm) {
|
||||
reply({ ok: false, error: 'Swarm not found' });
|
||||
return;
|
||||
}
|
||||
const keyPair = swarm.keyPair;
|
||||
if (!keyPair || !keyPair.publicKey) {
|
||||
reply({ ok: false, error: 'Key pair not available yet' });
|
||||
return;
|
||||
}
|
||||
reply({ ok: true, publicKey: b4a.toString(keyPair.publicKey, 'hex') });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'leave': {
|
||||
const { swarmId, topic: topicHex } = payload;
|
||||
const swarm = swarms.get(swarmId);
|
||||
@@ -467,15 +517,18 @@ async function handleMessageAsync(send, msg) {
|
||||
|
||||
case 'destroy': {
|
||||
const { swarmId } = payload;
|
||||
log('destroy swarm', swarmId);
|
||||
const swarm = swarms.get(swarmId);
|
||||
if (swarm) {
|
||||
const toDestroy = [...connToSwarm.entries()].filter(([, sid]) => sid === swarmId);
|
||||
for (const [cid] of toDestroy) {
|
||||
const entry = connections.get(cid);
|
||||
if (entry) {
|
||||
log('closing conn', cid, 'peer', entry.peerKeyHex?.slice(0, 8));
|
||||
entry._closed = true;
|
||||
entry.socket.removeAllListeners();
|
||||
peerConnections.get(swarmId)?.delete(entry.peerKeyHex);
|
||||
emit('end', { connId: cid, swarmId });
|
||||
try { entry.socket.destroy(); } catch (_) {}
|
||||
}
|
||||
connections.delete(cid);
|
||||
|
||||
Reference in New Issue
Block a user