From c5306116b0dde6d27b6010ddece53564af33dbad Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Thu, 9 Apr 2026 18:03:40 -0700 Subject: [PATCH 1/4] basic plugin system --- index.js | 10 ++++++++++ lib/constants.js | 29 +++++++++++++++++++---------- lib/messages.js | 20 ++++++++++++++++++++ lib/persistent.js | 14 ++++++++++++++ test/all.js | 1 + 5 files changed, 64 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index ad8160cb..b9187d44 100644 --- a/index.js +++ b/index.js @@ -47,6 +47,7 @@ class HyperDHT extends DHT { ...this.stats } this.rawStreams = new RawStreamSet(this) + this.plugins = new Map() this._router = new Router(this, router) this._socketPool = new SocketPool(this, opts.host || '0.0.0.0') @@ -127,6 +128,7 @@ class HyperDHT extends DHT { } this._router.destroy() if (this._persistent) this._persistent.destroy() + for (const [_, plugin] of this.plugins) plugin.destroy() await this.rawStreams.clear() await this._socketPool.destroy() await super.destroy() @@ -436,6 +438,10 @@ class HyperDHT extends DHT { this._persistent.onimmutableget(req) return true } + case COMMANDS.PLUGIN_PERSISTENT: { + this._persistent.onplugin(req) + return true + } } return false @@ -510,6 +516,10 @@ class HyperDHT extends DHT { from ) } + + register(name, plugin) { + this.plugins.set(name, plugin) + } } HyperDHT.BOOTSTRAP = BOOTSTRAP_NODES diff --git a/lib/constants.js b/lib/constants.js index 0d43cd5e..de8dcb26 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -10,7 +10,8 @@ const COMMANDS = (exports.COMMANDS = { MUTABLE_PUT: 6, MUTABLE_GET: 7, IMMUTABLE_PUT: 8, - IMMUTABLE_GET: 9 + IMMUTABLE_GET: 9, + PLUGIN_PERSISTENT: 10 }) exports.BOOTSTRAP_NODES = global.Pear?.config.dht?.bootstrap || [ @@ -39,19 +40,27 @@ exports.ERROR = { SEQ_TOO_LOW: 17 } -const [NS_ANNOUNCE, NS_UNANNOUNCE, NS_MUTABLE_PUT, NS_PEER_HANDSHAKE, NS_PEER_HOLEPUNCH] = - crypto.namespace('hyperswarm/dht', [ - COMMANDS.ANNOUNCE, - COMMANDS.UNANNOUNCE, - COMMANDS.MUTABLE_PUT, - COMMANDS.PEER_HANDSHAKE, - COMMANDS.PEER_HOLEPUNCH - ]) +const [ + NS_ANNOUNCE, + NS_UNANNOUNCE, + NS_MUTABLE_PUT, + NS_PEER_HANDSHAKE, + NS_PEER_HOLEPUNCH, + NS_PLUGIN_PERSISTENT +] = crypto.namespace('hyperswarm/dht', [ + COMMANDS.ANNOUNCE, + COMMANDS.UNANNOUNCE, + COMMANDS.MUTABLE_PUT, + COMMANDS.PEER_HANDSHAKE, + COMMANDS.PEER_HOLEPUNCH, + COMMANDS.PLUGIN_PERSISTENT +]) exports.NS = { ANNOUNCE: NS_ANNOUNCE, UNANNOUNCE: NS_UNANNOUNCE, MUTABLE_PUT: NS_MUTABLE_PUT, PEER_HANDSHAKE: NS_PEER_HANDSHAKE, - PEER_HOLEPUNCH: NS_PEER_HOLEPUNCH + PEER_HOLEPUNCH: NS_PEER_HOLEPUNCH, + PLUGIN_PERSISTENT: NS_PLUGIN_PERSISTENT } diff --git a/lib/messages.js b/lib/messages.js index 9b1bdf6e..d11d834b 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -419,3 +419,23 @@ exports.mutableGetResponse = { } } } + +exports.pluginRequest = { + preencode(state, m) { + c.string.preencode(state, m.plugin) + c.int.preencode(state, m.command) + c.buffer.preencode(state, m.payload) + }, + encode(state, m) { + c.string.encode(state, m.plugin) + c.int.encode(state, m.command) + c.buffer.encode(state, m.payload) + }, + decode(state) { + return { + plugin: c.string.decode(state), + command: c.int.decode(state), + payload: c.buffer.decode(state) + } + } +} diff --git a/lib/persistent.js b/lib/persistent.js index c45c1656..dee0342d 100644 --- a/lib/persistent.js +++ b/lib/persistent.js @@ -227,6 +227,20 @@ module.exports = class Persistent { req.reply(null) } + onplugin(req) { + if (!req.value) return + + const plugreq = decode(m.pluginRequest, req.value) + if (plugreq === null) return + + const { plugin, command, payload } = plugreq + + const p = this.dht.plugins.get(plugin) + if (!p) return + + p.onrequest(req) + } + destroy() { this.records.destroy() this.refreshes.destroy() diff --git a/test/all.js b/test/all.js index 6c5c24f7..349b254d 100644 --- a/test/all.js +++ b/test/all.js @@ -17,6 +17,7 @@ async function runTests() { await import('./pool.js') await import('./relaying.js') await import('./storing.js') + await import('./plugins.js') test.resume() } From 523a93333101ec7e833c6ba61163a4e8424b7dde Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Thu, 9 Apr 2026 18:05:09 -0700 Subject: [PATCH 2/4] add tests and missing plugin base class --- lib/plugin.js | 13 ++++ test/plugins.js | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 lib/plugin.js create mode 100644 test/plugins.js diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100644 index 00000000..b225d5b6 --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,13 @@ +module.exports = class Plugin { + constructor(name) { + this.name = name + } + + onrequest(req) { + throw new Error('onrequest() must be implemented') + } + + destroy() { + throw new Error('destroy() must be implemented') + } +} diff --git a/test/plugins.js b/test/plugins.js new file mode 100644 index 00000000..01803e9c --- /dev/null +++ b/test/plugins.js @@ -0,0 +1,174 @@ +const test = require('brittle') +const c = require('compact-encoding') +const b4a = require('b4a') +const sodium = require('sodium-universal') +const HyperDHT = require('../') +const { swarm } = require('./helpers') +const m = require('../lib/messages') +const { COMMANDS: HYPERDHT_COMMANDS } = require('../lib/constants') +const DHTPlugin = require('../lib/plugin') + +test('plugin put - get', async function (t) { + const PLUGIN_COMMANDS = { + PUT: 0, + GET: 1 + } + + function mapTest(node) { + return node + } + + class TestPlugin extends DHTPlugin { + constructor(dht) { + super('testplugin') + + this.dht = dht + this.data = new Map() + } + + onrequest(req) { + if (!req.value) return + + let plugreq + try { + plugreq = c.decode(m.pluginRequest, req.value) + } catch { + return + } + + const { plugin, command, payload } = plugreq + + switch (command) { + case PLUGIN_COMMANDS.PUT: { + this.onput(req) + return true + } + case PLUGIN_COMMANDS.GET: { + this.onget(req) + return true + } + } + + return false + } + + destroy() { + // Do nothing + } + + onput(req) { + if (!req.target || !req.token || !req.value) return + + let val + try { + const { plugin, command, payload } = c.decode(m.pluginRequest, req.value) + val = payload + } catch { + return req.reply(null) + } + + const k = b4a.toString(req.target, 'hex') + this.data.set(k, val) + req.reply(null) + } + + onget(req) { + if (!req.target) return + const k = b4a.toString(req.target, 'hex') + req.reply(this.data.get(k) || null) + } + + async put(val) { + const putReq = c.encode(m.pluginRequest, { + plugin: this.name, + command: PLUGIN_COMMANDS.PUT, + payload: b4a.from(val, 'utf8') + }) + + const opts = { + map: mapTest, + commit(reply, dht) { + return dht.request( + { + token: reply.token, + target, + command: HYPERDHT_COMMANDS.PLUGIN_PERSISTENT, + value: putReq + }, + reply.from + ) + } + } + + const target = b4a.allocUnsafe(32) + sodium.crypto_generichash(target, b4a.from(val, 'utf8')) + + const getReq = c.encode(m.pluginRequest, { + plugin: this.name, + command: PLUGIN_COMMANDS.GET, + payload: null + }) + + const query = this.dht.query( + { + target, + command: HYPERDHT_COMMANDS.PLUGIN_PERSISTENT, + value: getReq + }, + opts + ) + + await query.finished() + return { target, closestNodes: query.closestNodes } + } + + async get(target) { + const opts = { map: mapTest } + + const req = c.encode(m.pluginRequest, { + plugin: this.name, + command: PLUGIN_COMMANDS.GET, + payload: null + }) + + const query = this.dht.query( + { + target, + command: HYPERDHT_COMMANDS.PLUGIN_PERSISTENT, + value: req + }, + opts + ) + + for await (const node of query) { + return node + } + + return null + } + } + + const { nodes } = await swarm(t, 100) + const pluginClients = [] + + for (const node of nodes) { + const p = new TestPlugin(node) + pluginClients.push(p) + node.register(p.name, p) + } + + const put = await pluginClients[30].put('myTestValue') + + t.is(put.target.length, 32) + + const res = await pluginClients[30].get(put.target) + const { value } = res + + t.is(b4a.toString(value, 'utf8'), 'myTestValue') + t.is(typeof res.from, 'object') + t.is(typeof res.from.host, 'string') + t.is(typeof res.from.port, 'number') + t.is(typeof res.to, 'object') + t.is(typeof res.to.host, 'string') + t.is(typeof res.to.port, 'number') +}) From 22a95220966a64c0f317c34064e7ec2dc405b6af Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Thu, 9 Apr 2026 18:13:08 -0700 Subject: [PATCH 3/4] don't use b4a in tests --- test/plugins.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/plugins.js b/test/plugins.js index 01803e9c..5ac68957 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -1,6 +1,5 @@ const test = require('brittle') const c = require('compact-encoding') -const b4a = require('b4a') const sodium = require('sodium-universal') const HyperDHT = require('../') const { swarm } = require('./helpers') @@ -67,14 +66,14 @@ test('plugin put - get', async function (t) { return req.reply(null) } - const k = b4a.toString(req.target, 'hex') + const k = req.target.toString('hex') this.data.set(k, val) req.reply(null) } onget(req) { if (!req.target) return - const k = b4a.toString(req.target, 'hex') + const k = req.target.toString('hex') req.reply(this.data.get(k) || null) } @@ -82,7 +81,7 @@ test('plugin put - get', async function (t) { const putReq = c.encode(m.pluginRequest, { plugin: this.name, command: PLUGIN_COMMANDS.PUT, - payload: b4a.from(val, 'utf8') + payload: Buffer.from(val) }) const opts = { @@ -100,8 +99,8 @@ test('plugin put - get', async function (t) { } } - const target = b4a.allocUnsafe(32) - sodium.crypto_generichash(target, b4a.from(val, 'utf8')) + const target = Buffer.alloc(32) + sodium.crypto_generichash(target, Buffer.from(val)) const getReq = c.encode(m.pluginRequest, { plugin: this.name, @@ -164,7 +163,7 @@ test('plugin put - get', async function (t) { const res = await pluginClients[30].get(put.target) const { value } = res - t.is(b4a.toString(value, 'utf8'), 'myTestValue') + t.is(value.toString(), 'myTestValue') t.is(typeof res.from, 'object') t.is(typeof res.from.host, 'string') t.is(typeof res.from.port, 'number') From 628add4a20b9f94dbc503507fdb24d990c8d2bac Mon Sep 17 00:00:00 2001 From: Noah Levenson Date: Fri, 10 Apr 2026 10:24:05 -0700 Subject: [PATCH 4/4] fix test --- test/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins.js b/test/plugins.js index 5ac68957..b6ecbd8b 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -140,7 +140,7 @@ test('plugin put - get', async function (t) { ) for await (const node of query) { - return node + if (node.value !== null) return node } return null