OverviewFilesCommitsIssuesDocsReleasesPeers

bare-abstract-socket commits / c7b3c83

index: add net-compatible createServer/createConnection + allowHalfOpen
Lets #node:net UDS code switch to an abstract name with a minimal diff. createConnection mirrors net (async 'error', never throws sync) so probe-style callers work unchanged. AbstractServer honours allowHalfOpen:false (hyper-svc's fd-leak fix). Test covers the round-trip + the absent-name async-error path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c7b3c83df7d05e41a8eb96fc2ac6edbe317a3dd8
by plankatron <fleeky@gmail.com> · 2026-07-13T23:27:51.000Z

Changed files (3) +277 -48

index.js +80 -14
……@@ -3,13 +3,17 @@
33// We own only the socket setup (native binding: create the abstract socket, hand
44// back fds). Stock, unmodified `bare-pipe` does all stream I/O via `new Pipe(fd)`.
55//
6// const { AbstractServer, connect } = require('bare-abstract-socket')
6// Two surfaces:
77//
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')
1110//
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')
1317//
1418// The name is the abstract-namespace name WITHOUT the leading NUL — the binding
1519// adds it. Linux only; abstract sockets are a Linux extension.
……@@ -21,15 +25,22 @@ const binding = require('./binding')
2125const DEFAULT_BACKLOG = 511
2226
2327class AbstractServer extends EventEmitter {
24 constructor() {
28 constructor(opts = {}) {
2529 super()
2630
2731 this._handle = null
2832 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
2936 this.listening = false
3037 }
3138
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 }
3344 if (this._handle !== null) throw new Error('Server is already listening')
3445
3546 // `this` is the ctx; _onConnection is invoked as (err, fd) per accepted conn.
……@@ -37,6 +48,7 @@ class AbstractServer extends EventEmitter {
3748 this._name = name
3849 this.listening = true
3950
51 if (onListening) this.once('listening', onListening)
4052 queueMicrotask(() => this.emit('listening'))
4153
4254 return this
……@@ -50,24 +62,78 @@ class AbstractServer extends EventEmitter {
5062 }
5163
5264 // 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 })
5466 this.emit('connection', socket)
5567 }
5668
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 }
5978
6079 binding.close(this._handle)
6180 this._handle = null
6281 this.listening = false
6382
64 this.emit('close')
83 if (cb) this.once('close', cb)
84 queueMicrotask(() => this.emit('close'))
85
86 return this
6587 }
6688}
6789
68function 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.
92function connect(name, opts = {}) {
6993 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.
101function 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().
115function 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
71137}
72138
73module.exports = { AbstractServer, connect }
139module.exports = { AbstractServer, connect, createServer, createConnection }
package-lock.json +152 -0
……@@ -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}
test.cjs +45 -34
……@@ -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).
4const { 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.
9const abstract = require('./index.js')
510
611const NAME = 'bare-abstract-companion-proof'
712
8let done = false
9function 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
17const server = new AbstractServer()
13let step = 0
14function fail(msg) { console.log('FAIL: ' + msg); Bare.exit(1) }
1815
19server.on('error', (err) => finish(1, 'SERVER ERROR: ' + err.message))
20
21server.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', () => {})
16const server = abstract.createServer({ allowHalfOpen: false }, (sock) => {
17 sock.on('data', (data) => sock.write(Buffer.concat([Buffer.from('echo:'), data])))
18 sock.on('error', () => {})
2719})
28
29server.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))
20server.on('error', (err) => fail('server error: ' + err.message))
21
22server.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))
3328 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()
3834 })
3935 socket.write(Buffer.from('hello'))
40 console.log('client: connected + sent "hello"')
4136})
4237
43server.listen(NAME)
38// Test 2 — connecting to a non-existent name emits 'error', never throws sync.
39function 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}
4455
45setTimeout(() => finish(1, 'TIMEOUT — no round-trip'), 3000)
56setTimeout(() => fail('TIMEOUT'), 4000)