Implement hrpc with streaming and wire into setup
- Add spec/hyperschema and spec/hrpc codegen (scripts/build-hrpc.js) with unary, request/response-stream, duplex, and send-only commands - Run build:hrpc and build:protomux from install.sh so setup is one-step - Native host: add attachHrpc (Protomux channel + generated HRPC), handlers for ping, streamSum, fetchStream, duplex, notify - Load HRPC lazily so missing spec does not crash host; use bare-stream for Duplex under Bare; add bare-stream, compact-encoding deps
This commit is contained in:
+129
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* BridgeSwarm instance manager for the native messaging host.
|
||||
* Maps swarmId -> Hyperswarm, connId -> { socket, protomux?, forwarding }; routes commands and emits events.
|
||||
* Supports Protomux and optional Hypercore replication (attachReplication).
|
||||
* Supports Protomux, optional Hypercore replication (attachReplication), and hrpc (attachHrpc).
|
||||
*/
|
||||
|
||||
const Hyperswarm = require('hyperswarm');
|
||||
@@ -12,12 +12,25 @@ const Hyperdrive = require('hyperdrive');
|
||||
const Autobase = require('autobase');
|
||||
const HyperDB = require('hyperdb');
|
||||
const path = require('path');
|
||||
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');
|
||||
|
||||
// Load generated HRPC lazily so a missing/broken spec does not crash the host at startup
|
||||
let HRPC = null;
|
||||
let hrpcLoadError = null;
|
||||
try {
|
||||
HRPC = require(path.join(__dirname, '..', 'spec', 'hrpc'));
|
||||
} catch (err) {
|
||||
hrpcLoadError = err;
|
||||
}
|
||||
|
||||
const HRPC_PROTOCOL = 'bridgeswarm-hrpc';
|
||||
|
||||
const swarms = new Map();
|
||||
/** @type {Map<string, { socket: import('stream').Duplex, protomux?: import('protomux'), forwarding: boolean }>} */
|
||||
/** @type {Map<string, { socket: import('stream').Duplex, protomux?: import('protomux'), hrpc?: InstanceType<typeof HRPC>, hrpcDuplex?: import('stream').Duplex, forwarding: boolean, _listeners: object }>} */
|
||||
const connections = new Map();
|
||||
const connToSwarm = new Map(); // connId -> swarmId
|
||||
let nextConnId = 0;
|
||||
@@ -222,6 +235,9 @@ async function handleMessageAsync(send, msg) {
|
||||
const { connId } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (entry) {
|
||||
if (entry.hrpcDuplex && !entry.hrpcDuplex.destroyed) {
|
||||
entry.hrpcDuplex.destroy();
|
||||
}
|
||||
entry.socket.destroy();
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
@@ -267,6 +283,114 @@ async function handleMessageAsync(send, msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'attachHrpc': {
|
||||
if (HRPC === null) {
|
||||
reply({
|
||||
ok: false,
|
||||
error: hrpcLoadError ? `hrpc not available: ${hrpcLoadError.message}` : 'hrpc spec not built (run: npm run build:hrpc)'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { connId } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (!entry) {
|
||||
reply({ ok: false, error: 'Connection not found' });
|
||||
return;
|
||||
}
|
||||
if (entry.hrpc) {
|
||||
reply({ ok: true });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
entry.forwarding = false;
|
||||
entry.socket.removeListener('data', entry._listeners.onData);
|
||||
entry.socket.removeListener('end', entry._listeners.onEnd);
|
||||
entry.socket.removeListener('error', entry._listeners.onError);
|
||||
entry.socket.removeListener('close', entry._listeners.onClose);
|
||||
entry.socket.once('close', () => {
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
});
|
||||
const mux = new Protomux(entry.socket);
|
||||
entry.protomux = mux;
|
||||
const channel = mux.createChannel({
|
||||
protocol: HRPC_PROTOCOL,
|
||||
id: Buffer.alloc(0),
|
||||
unique: false
|
||||
});
|
||||
let hrpcDuplex;
|
||||
channel.addMessage({
|
||||
encoding: c.buffer,
|
||||
onmessage(buf) {
|
||||
if (hrpcDuplex && !hrpcDuplex.destroyed) hrpcDuplex.push(buf);
|
||||
}
|
||||
});
|
||||
const frameMsg = channel.messages[0];
|
||||
hrpcDuplex = new Duplex({
|
||||
read() {},
|
||||
write(chunk, enc, cb) {
|
||||
if (frameMsg && frameMsg.send(chunk, channel)) {
|
||||
cb();
|
||||
} else {
|
||||
mux.stream.once('drain', () => cb());
|
||||
}
|
||||
}
|
||||
});
|
||||
channel.open();
|
||||
const rpc = new HRPC(hrpcDuplex);
|
||||
rpc.onPing((data) => {
|
||||
return { pong: (data && data.value) || 'pong' };
|
||||
});
|
||||
rpc.onStreamSum(async (requestStream) => {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
await new Promise((resolve, reject) => {
|
||||
requestStream.on('data', (data) => {
|
||||
if (data && typeof data.n === 'number') sum += data.n;
|
||||
count++;
|
||||
});
|
||||
requestStream.on('end', resolve);
|
||||
requestStream.on('error', reject);
|
||||
});
|
||||
return { sum, count };
|
||||
});
|
||||
rpc.onFetchStream((responseStream) => {
|
||||
const count = Math.min((responseStream.data && responseStream.data.count) || 0, 100);
|
||||
for (let i = 0; i < count; i++) {
|
||||
responseStream.write({ i, data: `chunk-${i}` });
|
||||
}
|
||||
responseStream.end();
|
||||
});
|
||||
rpc.onDuplex((stream) => {
|
||||
stream.on('data', (data) => {
|
||||
if (data && stream.writable) {
|
||||
stream.write({
|
||||
result: `got ${data.x} ${data.y || ''}`.trim(),
|
||||
n: (data.x || 0) + 1
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
rpc.onNotify((data) => {
|
||||
if (process.stderr && data) {
|
||||
process.stderr.write(`[bridge-swarm-hrpc] notify: ${data.event} ${data.payload}\n`);
|
||||
}
|
||||
});
|
||||
hrpcDuplex.on('error', () => {});
|
||||
hrpcDuplex.on('close', () => {
|
||||
try {
|
||||
channel.close();
|
||||
} catch (_) {}
|
||||
});
|
||||
entry.hrpc = rpc;
|
||||
entry.hrpcDuplex = hrpcDuplex;
|
||||
reply({ ok: true });
|
||||
} catch (err) {
|
||||
reply({ ok: false, error: err.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'destroy': {
|
||||
const { swarmId } = payload;
|
||||
const swarm = swarms.get(swarmId);
|
||||
@@ -524,6 +648,9 @@ async function handleMessageAsync(send, msg) {
|
||||
function cleanup() {
|
||||
for (const entry of connections.values()) {
|
||||
try {
|
||||
if (entry.hrpcDuplex && !entry.hrpcDuplex.destroyed) {
|
||||
entry.hrpcDuplex.destroy();
|
||||
}
|
||||
entry.socket.destroy();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user