| … | … | @@ -3,13 +3,17 @@ |
| 3 | 3 | // We own only the socket setup (native binding: create the abstract socket, hand |
| 4 | 4 | // back fds). Stock, unmodified `bare-pipe` does all stream I/O via `new Pipe(fd)`. |
| 5 | 5 | // |
| 6 | // const { AbstractServer, connect } = require('bare-abstract-socket') | |
| 6 | // Two surfaces: | |
| 7 | 7 | // |
| 8 | // const server = new AbstractServer() | |
| 9 | // server.on('connection', (socket) => socket.pipe(socket)) // echo | |
| 10 | // server.listen('my-abstract-name') | |
| 8 | // Low level: | |
| 9 | // const { AbstractServer, connect } = require('bare-abstract-socket') | |
| 11 | 10 | // |
| 12 | // const socket = connect('my-abstract-name') // a bare-pipe stream | |
| 11 | // net-compatible drop-in (so code using #node:net over a UNIX path can switch | |
| 12 | // to an abstract name with a minimal diff): | |
| 13 | // const abstract = require('bare-abstract-socket') | |
| 14 | // const server = abstract.createServer({ allowHalfOpen: false }, onConnection) | |
| 15 | // server.listen('my-name', () => {}) | |
| 16 | // const socket = abstract.createConnection('my-name') | |
| 13 | 17 | // |
| 14 | 18 | // The name is the abstract-namespace name WITHOUT the leading NUL — the binding |
| 15 | 19 | // adds it. Linux only; abstract sockets are a Linux extension. |
| … | … | @@ -21,15 +25,22 @@ const binding = require('./binding') |
| 21 | 25 | const DEFAULT_BACKLOG = 511 |
| 22 | 26 | |
| 23 | 27 | class AbstractServer extends EventEmitter { |
| 24 | constructor() { | |
| 28 | constructor(opts = {}) { | |
| 25 | 29 | super() |
| 26 | 30 | |
| 27 | 31 | this._handle = null |
| 28 | 32 | this._name = null |
| 33 | // Match bare-pipe's default (true); hyper-svc passes false to avoid the | |
| 34 | // half-open fd leak, so this MUST be honoured on accepted sockets. | |
| 35 | this._allowHalfOpen = opts.allowHalfOpen !== false | |
| 29 | 36 | this.listening = false |
| 30 | 37 | } |
| 31 | 38 | |
| 32 | listen(name, backlog = DEFAULT_BACKLOG) { | |
| 39 | listen(name, backlog = DEFAULT_BACKLOG, onListening) { | |
| 40 | if (typeof backlog === 'function') { | |
| 41 | onListening = backlog | |
| 42 | backlog = DEFAULT_BACKLOG | |
| 43 | } | |
| 33 | 44 | if (this._handle !== null) throw new Error('Server is already listening') |
| 34 | 45 | |
| 35 | 46 | // `this` is the ctx; _onConnection is invoked as (err, fd) per accepted conn. |
| … | … | @@ -37,6 +48,7 @@ class AbstractServer extends EventEmitter { |
| 37 | 48 | this._name = name |
| 38 | 49 | this.listening = true |
| 39 | 50 | |
| 51 | if (onListening) this.once('listening', onListening) | |
| 40 | 52 | queueMicrotask(() => this.emit('listening')) |
| 41 | 53 | |
| 42 | 54 | return this |
| … | … | @@ -50,24 +62,78 @@ class AbstractServer extends EventEmitter { |
| 50 | 62 | } |
| 51 | 63 | |
| 52 | 64 | // Wrap the accepted fd with stock bare-pipe — it does the stream I/O. |
| 53 | const socket = new Pipe(fd) | |
| 65 | const socket = new Pipe(fd, { allowHalfOpen: this._allowHalfOpen }) | |
| 54 | 66 | this.emit('connection', socket) |
| 55 | 67 | } |
| 56 | 68 | |
| 57 | close() { | |
| 58 | if (this._handle === null) return | |
| 69 | address() { | |
| 70 | return this._name | |
| 71 | } | |
| 72 | ||
| 73 | close(cb) { | |
| 74 | if (this._handle === null) { | |
| 75 | if (cb) queueMicrotask(cb) | |
| 76 | return this | |
| 77 | } | |
| 59 | 78 | |
| 60 | 79 | binding.close(this._handle) |
| 61 | 80 | this._handle = null |
| 62 | 81 | this.listening = false |
| 63 | 82 | |
| 64 | this.emit('close') | |
| 83 | if (cb) this.once('close', cb) | |
| 84 | queueMicrotask(() => this.emit('close')) | |
| 85 | ||
| 86 | return this | |
| 65 | 87 | } |
| 66 | 88 | } |
| 67 | 89 | |
| 68 | function connect(name) { | |
| 90 | // Low-level connect: returns a connected bare-pipe stream. Throws synchronously | |
| 91 | // if the peer is absent (ECONNREFUSED) or the name is too long. | |
| 92 | function connect(name, opts = {}) { | |
| 69 | 93 | const fd = binding.connect(name) |
| 70 | return new Pipe(fd) | |
| 94 | return new Pipe(fd, { allowHalfOpen: opts.allowHalfOpen !== false }) | |
| 95 | } | |
| 96 | ||
| 97 | // net-compatible surface ----------------------------------------------------- | |
| 98 | ||
| 99 | // Mirrors net.createServer(opts?, onConnection?): returns an AbstractServer whose | |
| 100 | // listen(name[, cb]) / close([cb]) / 'connection' / 'error' match net's. | |
| 101 | function createServer(opts, onConnection) { | |
| 102 | if (typeof opts === 'function') { | |
| 103 | onConnection = opts | |
| 104 | opts = {} | |
| 105 | } | |
| 106 | const server = new AbstractServer(opts || {}) | |
| 107 | if (onConnection) server.on('connection', onConnection) | |
| 108 | return server | |
| 109 | } | |
| 110 | ||
| 111 | // Mirrors net.createConnection(name[, opts][, onConnect]): NEVER throws | |
| 112 | // synchronously — connection errors surface as an async 'error' event, like net. | |
| 113 | // On failure a lightweight errored stub is returned so callers can still attach | |
| 114 | // 'error' and call destroy(). | |
| 115 | function createConnection(name, opts, onConnect) { | |
| 116 | if (typeof opts === 'function') { | |
| 117 | onConnect = opts | |
| 118 | opts = {} | |
| 119 | } | |
| 120 | opts = opts || {} | |
| 121 | ||
| 122 | let fd | |
| 123 | try { | |
| 124 | fd = binding.connect(name) | |
| 125 | } catch (err) { | |
| 126 | const stub = new EventEmitter() | |
| 127 | stub.destroy = () => {} | |
| 128 | queueMicrotask(() => stub.emit('error', err)) | |
| 129 | return stub | |
| 130 | } | |
| 131 | ||
| 132 | const socket = new Pipe(fd, { allowHalfOpen: opts.allowHalfOpen !== false }) | |
| 133 | if (onConnect) socket.once('connect', onConnect) | |
| 134 | // bare-pipe opens synchronously for an fd; signal readiness like net does. | |
| 135 | queueMicrotask(() => socket.emit('connect')) | |
| 136 | return socket | |
| 71 | 137 | } |
| 72 | 138 | |
| 73 | module.exports = { AbstractServer, connect } | |
| 139 | module.exports = { AbstractServer, connect, createServer, createConnection } |
| … | … | @@ -0,0 +1,152 @@ |
| 1 | { | |
| 2 | "name": "bare-abstract-socket", | |
| 3 | "version": "1.0.0", | |
| 4 | "lockfileVersion": 3, | |
| 5 | "requires": true, | |
| 6 | "packages": { | |
| 7 | "": { | |
| 8 | "name": "bare-abstract-socket", | |
| 9 | "version": "1.0.0", | |
| 10 | "license": "Apache-2.0", | |
| 11 | "dependencies": { | |
| 12 | "bare-events": "^2.0.0", | |
| 13 | "bare-pipe": "^4.2.2" | |
| 14 | }, | |
| 15 | "devDependencies": { | |
| 16 | "cmake-bare": "^1.1.6" | |
| 17 | }, | |
| 18 | "engines": { | |
| 19 | "bare": ">=1.16.0" | |
| 20 | } | |
| 21 | }, | |
| 22 | "node_modules/b4a": { | |
| 23 | "version": "1.8.1", | |
| 24 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", | |
| 25 | "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", | |
| 26 | "license": "Apache-2.0", | |
| 27 | "peerDependencies": { | |
| 28 | "react-native-b4a": "*" | |
| 29 | }, | |
| 30 | "peerDependenciesMeta": { | |
| 31 | "react-native-b4a": { | |
| 32 | "optional": true | |
| 33 | } | |
| 34 | } | |
| 35 | }, | |
| 36 | "node_modules/bare-events": { | |
| 37 | "version": "2.9.1", | |
| 38 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", | |
| 39 | "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", | |
| 40 | "license": "Apache-2.0", | |
| 41 | "peerDependencies": { | |
| 42 | "bare-abort-controller": "*" | |
| 43 | }, | |
| 44 | "peerDependenciesMeta": { | |
| 45 | "bare-abort-controller": { | |
| 46 | "optional": true | |
| 47 | } | |
| 48 | } | |
| 49 | }, | |
| 50 | "node_modules/bare-pipe": { | |
| 51 | "version": "4.2.2", | |
| 52 | "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.2.2.tgz", | |
| 53 | "integrity": "sha512-Wkf6//CmKS8inHrkgi3X/xNMeaGfs2I0ayLM0B8cbPB4DxYHcoes6Ng+muCBfXNIJ9qmEjwDDJjAUz7nRxxjgQ==", | |
| 54 | "license": "Apache-2.0", | |
| 55 | "dependencies": { | |
| 56 | "bare-events": "^2.0.0", | |
| 57 | "bare-stream": "^2.0.0" | |
| 58 | }, | |
| 59 | "engines": { | |
| 60 | "bare": ">=1.16.0" | |
| 61 | } | |
| 62 | }, | |
| 63 | "node_modules/bare-stream": { | |
| 64 | "version": "2.13.3", | |
| 65 | "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", | |
| 66 | "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", | |
| 67 | "license": "Apache-2.0", | |
| 68 | "dependencies": { | |
| 69 | "b4a": "^1.8.1", | |
| 70 | "streamx": "^2.25.0", | |
| 71 | "teex": "^1.0.1" | |
| 72 | }, | |
| 73 | "peerDependencies": { | |
| 74 | "bare-abort-controller": "*", | |
| 75 | "bare-buffer": "*", | |
| 76 | "bare-events": "*" | |
| 77 | }, | |
| 78 | "peerDependenciesMeta": { | |
| 79 | "bare-abort-controller": { | |
| 80 | "optional": true | |
| 81 | }, | |
| 82 | "bare-buffer": { | |
| 83 | "optional": true | |
| 84 | }, | |
| 85 | "bare-events": { | |
| 86 | "optional": true | |
| 87 | } | |
| 88 | } | |
| 89 | }, | |
| 90 | "node_modules/cmake-bare": { | |
| 91 | "version": "1.8.0", | |
| 92 | "resolved": "https://registry.npmjs.org/cmake-bare/-/cmake-bare-1.8.0.tgz", | |
| 93 | "integrity": "sha512-dUE5i9Drhp8ZVnQ57RiHXqVpZ5iBondpIsgAmQ/VBmgkCy1g5F+mFX8e/FM7kwSKgKxtra+pREzhDGundZKxvw==", | |
| 94 | "dev": true, | |
| 95 | "license": "Apache-2.0", | |
| 96 | "dependencies": { | |
| 97 | "cmake-npm": "^1.1.0" | |
| 98 | } | |
| 99 | }, | |
| 100 | "node_modules/cmake-npm": { | |
| 101 | "version": "1.1.2", | |
| 102 | "resolved": "https://registry.npmjs.org/cmake-npm/-/cmake-npm-1.1.2.tgz", | |
| 103 | "integrity": "sha512-CfN53Ov9usNBeaIsr8TtEQwwXeRZkJWZSoGe0qybr9bojxGlwTdxHk8LQwJFNypsSgFoYyOWUMjKGuahsiGifA==", | |
| 104 | "dev": true, | |
| 105 | "license": "Apache-2.0" | |
| 106 | }, | |
| 107 | "node_modules/events-universal": { | |
| 108 | "version": "1.0.1", | |
| 109 | "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", | |
| 110 | "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", | |
| 111 | "license": "Apache-2.0", | |
| 112 | "dependencies": { | |
| 113 | "bare-events": "^2.7.0" | |
| 114 | } | |
| 115 | }, | |
| 116 | "node_modules/fast-fifo": { | |
| 117 | "version": "1.3.2", | |
| 118 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", | |
| 119 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", | |
| 120 | "license": "MIT" | |
| 121 | }, | |
| 122 | "node_modules/streamx": { | |
| 123 | "version": "2.28.0", | |
| 124 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", | |
| 125 | "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", | |
| 126 | "license": "MIT", | |
| 127 | "dependencies": { | |
| 128 | "events-universal": "^1.0.0", | |
| 129 | "fast-fifo": "^1.3.2", | |
| 130 | "text-decoder": "^1.1.0" | |
| 131 | } | |
| 132 | }, | |
| 133 | "node_modules/teex": { | |
| 134 | "version": "1.0.1", | |
| 135 | "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", | |
| 136 | "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", | |
| 137 | "license": "MIT", | |
| 138 | "dependencies": { | |
| 139 | "streamx": "^2.12.5" | |
| 140 | } | |
| 141 | }, | |
| 142 | "node_modules/text-decoder": { | |
| 143 | "version": "1.2.7", | |
| 144 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", | |
| 145 | "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", | |
| 146 | "license": "Apache-2.0", | |
| 147 | "dependencies": { | |
| 148 | "b4a": "^1.6.4" | |
| 149 | } | |
| 150 | } | |
| 151 | } | |
| 152 | } |
| … | … | @@ -1,45 +1,56 @@ |
| 1 | // Isolated proof: abstract-namespace server + client over our companion, with | |
| 2 | // ALL stream I/O done by stock bare-pipe (new Pipe(fd)). Echo round-trip + a | |
| 3 | // check that no filesystem path was created (true abstract socket). | |
| 4 | const { AbstractServer, connect } = require('./index.js') | |
| 1 | // Proof + net-compat coverage. All stream I/O is done by stock bare-pipe | |
| 2 | // (new Pipe(fd)); the companion only creates the abstract sockets. | |
| 3 | // | |
| 4 | // 1. net-compat round-trip: createServer(allowHalfOpen:false) echo + | |
| 5 | // createConnection('connect' event) — the surface hyper-svc consumes. | |
| 6 | // 2. async-error path: createConnection to an absent name must NOT throw | |
| 7 | // synchronously; it emits 'error' (ECONNREFUSED) — hyper-svc's _probe | |
| 8 | // depends on this. | |
| 9 | const abstract = require('./index.js') | |
| 5 | 10 | |
| 6 | 11 | const NAME = 'bare-abstract-companion-proof' |
| 7 | 12 | |
| 8 | let done = false | |
| 9 | function finish(code, msg) { | |
| 10 | if (done) return | |
| 11 | done = true | |
| 12 | if (msg) console.log(msg) | |
| 13 | try { server.close() } catch {} | |
| 14 | Bare.exit(code) | |
| 15 | } | |
| 16 | ||
| 17 | const server = new AbstractServer() | |
| 13 | let step = 0 | |
| 14 | function fail(msg) { console.log('FAIL: ' + msg); Bare.exit(1) } | |
| 18 | 15 | |
| 19 | server.on('error', (err) => finish(1, 'SERVER ERROR: ' + err.message)) | |
| 20 | ||
| 21 | server.on('connection', (socket) => { | |
| 22 | console.log('server: accepted a connection (fd wrapped by stock bare-pipe)') | |
| 23 | socket.on('data', (data) => { | |
| 24 | socket.write(Buffer.concat([Buffer.from('echo:'), data])) | |
| 25 | }) | |
| 26 | socket.on('error', () => {}) | |
| 16 | const server = abstract.createServer({ allowHalfOpen: false }, (sock) => { | |
| 17 | sock.on('data', (data) => sock.write(Buffer.concat([Buffer.from('echo:'), data]))) | |
| 18 | sock.on('error', () => {}) | |
| 27 | 19 | }) |
| 28 | ||
| 29 | server.on('listening', () => { | |
| 30 | console.log('server: listening on abstract name "' + NAME + '"') | |
| 31 | const socket = connect(NAME) | |
| 32 | socket.on('error', (err) => finish(1, 'CLIENT ERROR: ' + err.message)) | |
| 20 | server.on('error', (err) => fail('server error: ' + err.message)) | |
| 21 | ||
| 22 | server.listen(NAME, () => { | |
| 23 | // Test 1 — round-trip via the net-compat surface. | |
| 24 | const socket = abstract.createConnection(NAME) | |
| 25 | let connected = false | |
| 26 | socket.on('connect', () => { connected = true }) | |
| 27 | socket.on('error', (err) => fail('client error: ' + err.message)) | |
| 33 | 28 | socket.on('data', (data) => { |
| 34 | const s = data.toString() | |
| 35 | console.log('client: received "' + s + '"') | |
| 36 | if (s === 'echo:hello') finish(0, 'PASS: abstract round-trip works via stock bare-pipe I/O') | |
| 37 | else finish(1, 'FAIL: unexpected "' + s + '"') | |
| 29 | if (data.toString() !== 'echo:hello') return fail('unexpected "' + data + '"') | |
| 30 | if (!connected) return fail("'connect' event never fired") | |
| 31 | console.log('PASS 1: net-compat round-trip (createServer/createConnection, connect event)') | |
| 32 | socket.destroy() | |
| 33 | runTest2() | |
| 38 | 34 | }) |
| 39 | 35 | socket.write(Buffer.from('hello')) |
| 40 | console.log('client: connected + sent "hello"') | |
| 41 | 36 | }) |
| 42 | 37 | |
| 43 | server.listen(NAME) | |
| 38 | // Test 2 — connecting to a non-existent name emits 'error', never throws sync. | |
| 39 | function runTest2() { | |
| 40 | let threw = false | |
| 41 | try { | |
| 42 | const dead = abstract.createConnection('bare-abstract-does-not-exist-zzz') | |
| 43 | dead.on('error', (err) => { | |
| 44 | const code = err.code || err.message | |
| 45 | console.log('PASS 2: absent name -> async error (' + code + '), no sync throw') | |
| 46 | server.close(() => Bare.exit(0)) | |
| 47 | }) | |
| 48 | dead.on('connect', () => fail('connected to a non-existent socket')) | |
| 49 | } catch (err) { | |
| 50 | threw = true | |
| 51 | fail('createConnection threw synchronously: ' + err.message) | |
| 52 | } | |
| 53 | if (threw) return | |
| 54 | } | |
| 44 | 55 | |
| 45 | setTimeout(() => finish(1, 'TIMEOUT — no round-trip'), 3000) | |
| 56 | setTimeout(() => fail('TIMEOUT'), 4000) |