postal-transport-uds 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/index.cjs +394 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +135 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +135 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +359 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-Present Jim Cowart
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# postal-transport-uds
|
|
2
|
+
|
|
3
|
+
Unix domain socket transport for [postal](https://github.com/postaljs/postal.js) — bridges pub/sub across independent Node.js processes via a UDS with NDJSON framing. Any process can connect to a known socket path; no parent/child relationship required.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install postal postal-transport-uds
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Server
|
|
14
|
+
|
|
15
|
+
One process listens on a socket path and accepts connections from any number of clients:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { getChannel } from "postal";
|
|
19
|
+
import { listenOnSocket } from "postal-transport-uds";
|
|
20
|
+
|
|
21
|
+
const { dispose } = await listenOnSocket("/tmp/postal.sock");
|
|
22
|
+
|
|
23
|
+
// Messages published here are forwarded to all connected clients
|
|
24
|
+
getChannel("jobs").publish("task.start", { id: 1 });
|
|
25
|
+
|
|
26
|
+
// Tear down when done
|
|
27
|
+
dispose();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Client
|
|
31
|
+
|
|
32
|
+
Other processes connect to the server's socket path:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { getChannel } from "postal";
|
|
36
|
+
import { connectToSocket } from "postal-transport-uds";
|
|
37
|
+
|
|
38
|
+
const removeTransport = await connectToSocket("/tmp/postal.sock", {
|
|
39
|
+
onDisconnect: () => {
|
|
40
|
+
console.log("Server went away");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
getChannel("jobs").subscribe("task.start", env => {
|
|
45
|
+
console.log("Got task:", env.payload);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Disconnect when done
|
|
49
|
+
removeTransport();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Filtering
|
|
53
|
+
|
|
54
|
+
Restrict which envelopes the server forwards:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const { dispose } = await listenOnSocket("/tmp/postal.sock", {
|
|
58
|
+
filter: {
|
|
59
|
+
channels: ["jobs"],
|
|
60
|
+
topics: ["task.#"],
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Low-level transport
|
|
66
|
+
|
|
67
|
+
If you manage the socket and handshake yourself, use the low-level factory:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createSocketTransport } from "postal-transport-uds";
|
|
71
|
+
import { addTransport } from "postal";
|
|
72
|
+
|
|
73
|
+
const transport = createSocketTransport(myConnectedSocket);
|
|
74
|
+
addTransport(transport);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### `listenOnSocket(socketPath, options?)`
|
|
80
|
+
|
|
81
|
+
Creates a `net.Server` on the given Unix domain socket path. For each connecting client, performs a SYN/ACK handshake and registers a transport with postal. Returns `Promise<{ dispose }>`.
|
|
82
|
+
|
|
83
|
+
### `connectToSocket(socketPath, options?)`
|
|
84
|
+
|
|
85
|
+
Connects to a postal UDS server. Performs a SYN/ACK handshake and registers a transport. Returns `Promise<() => void>` (the remove function).
|
|
86
|
+
|
|
87
|
+
### `createSocketTransport(socket, serializer?)`
|
|
88
|
+
|
|
89
|
+
Low-level factory. Wraps any object with `write()`, `on('data')`, and `removeListener('data')` in a postal `Transport`. Handles NDJSON framing, buffering, and dispatch. Does not own the socket lifecycle.
|
|
90
|
+
|
|
91
|
+
### Options
|
|
92
|
+
|
|
93
|
+
| Option | Type | Default | Used by | Description |
|
|
94
|
+
| -------------- | ----------------- | ------- | -------------- | -------------------------------------------- |
|
|
95
|
+
| `timeout` | `number` | `5000` | server, client | Handshake timeout in milliseconds |
|
|
96
|
+
| `filter` | `TransportFilter` | — | server | Restrict forwarded channels/topics |
|
|
97
|
+
| `unlinkStale` | `boolean` | `true` | server | Unlink existing socket file before listening |
|
|
98
|
+
| `onDisconnect` | `() => void` | — | client | Called on unexpected socket close |
|
|
99
|
+
|
|
100
|
+
## How It Works
|
|
101
|
+
|
|
102
|
+
- **Topology**: Hub-and-spoke. One server, N clients. The server registers each client as a separate transport; postal's core `broadcastToTransports()` handles fan-out and echo prevention via the `source` field.
|
|
103
|
+
- **Wire format**: NDJSON — `JSON.stringify(message) + "\n"`. Self-delimiting, debuggable with `socat`/`netcat`, zero dependencies.
|
|
104
|
+
- **Handshake**: Client sends `{ type: "postal:uds-syn" }`, server responds with `{ type: "postal:uds-ack" }`. Both happen over the same NDJSON stream as envelopes.
|
|
105
|
+
- **Serialization**: Abstracted behind a `Serializer` interface (`encode`/`decode`). The default is NDJSON; swap in MessagePack or similar without restructuring the transport.
|
|
106
|
+
|
|
107
|
+
## Known Limitations
|
|
108
|
+
|
|
109
|
+
- **Binary payloads**: NDJSON uses JSON serialization. `Buffer` and `ArrayBuffer` values would need base64 encoding. Acceptable for typical pub/sub payloads.
|
|
110
|
+
- **No auto-reconnection**: The transport exposes `onDisconnect`; the consumer decides what to do. Reconnection policy (backoff, retries, state reconciliation) is a userland concern.
|
|
111
|
+
- **Single-user socket**: The socket file inherits default permissions. All connecting processes must run as the same OS user (or adjust permissions manually).
|
|
112
|
+
- **Node.js only**: Uses `node:net` and `node:fs`. Not available in browsers.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let node_net = require("node:net");
|
|
30
|
+
node_net = __toESM(node_net);
|
|
31
|
+
let node_fs = require("node:fs");
|
|
32
|
+
node_fs = __toESM(node_fs);
|
|
33
|
+
let postal = require("postal");
|
|
34
|
+
|
|
35
|
+
//#region src/protocol.ts
|
|
36
|
+
/** Protocol version — bump if the handshake format changes. */
|
|
37
|
+
const PROTOCOL_VERSION = 1;
|
|
38
|
+
/** Default handshake timeout in milliseconds. */
|
|
39
|
+
const DEFAULT_TIMEOUT = 5e3;
|
|
40
|
+
const POSTAL_NAMESPACE = "postal:";
|
|
41
|
+
const isPostalMessage = (data) => {
|
|
42
|
+
return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string" && data.type.startsWith(POSTAL_NAMESPACE);
|
|
43
|
+
};
|
|
44
|
+
/** Narrows unknown data to a SYN message. Safe to call on any input. */
|
|
45
|
+
const isUdsSyn = (data) => {
|
|
46
|
+
return isPostalMessage(data) && data.type === "postal:uds-syn" && data.version === PROTOCOL_VERSION;
|
|
47
|
+
};
|
|
48
|
+
/** Narrows unknown data to an ACK message. Safe to call on any input. */
|
|
49
|
+
const isUdsAck = (data) => {
|
|
50
|
+
return isPostalMessage(data) && data.type === "postal:uds-ack" && data.version === PROTOCOL_VERSION;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Loose check — matches the SYN type string without version validation.
|
|
54
|
+
* Used to distinguish "wrong version" from "not a SYN at all".
|
|
55
|
+
*/
|
|
56
|
+
const looksLikeSyn = (data) => {
|
|
57
|
+
return isPostalMessage(data) && data.type === "postal:uds-syn";
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Loose check — matches the ACK type string without version validation.
|
|
61
|
+
* Used to distinguish "wrong version" from "not an ACK at all".
|
|
62
|
+
*/
|
|
63
|
+
const looksLikeAck = (data) => {
|
|
64
|
+
return isPostalMessage(data) && data.type === "postal:uds-ack";
|
|
65
|
+
};
|
|
66
|
+
/** Narrows unknown data to an envelope wrapper. Validates type, version, and that envelope is a non-null object. */
|
|
67
|
+
const isUdsEnvelopeMessage = (data) => {
|
|
68
|
+
return isPostalMessage(data) && data.type === "postal:envelope" && data.version === PROTOCOL_VERSION && "envelope" in data && typeof data.envelope === "object" && data.envelope !== null;
|
|
69
|
+
};
|
|
70
|
+
/** Creates a SYN message stamped with the current protocol version. */
|
|
71
|
+
const createUdsSyn = () => ({
|
|
72
|
+
type: "postal:uds-syn",
|
|
73
|
+
version: PROTOCOL_VERSION
|
|
74
|
+
});
|
|
75
|
+
/** Creates an ACK message stamped with the current protocol version. */
|
|
76
|
+
const createUdsAck = () => ({
|
|
77
|
+
type: "postal:uds-ack",
|
|
78
|
+
version: PROTOCOL_VERSION
|
|
79
|
+
});
|
|
80
|
+
/** Wraps a postal Envelope in a protocol message for wire transmission. */
|
|
81
|
+
const createUdsEnvelopeMessage = (envelope) => ({
|
|
82
|
+
type: "postal:envelope",
|
|
83
|
+
version: PROTOCOL_VERSION,
|
|
84
|
+
envelope
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/serialization.ts
|
|
89
|
+
/**
|
|
90
|
+
* NDJSON serializer — JSON.stringify + "\n" on encode, JSON.parse on decode.
|
|
91
|
+
*
|
|
92
|
+
* JSON's escaping rules guarantee no unescaped newlines in the output,
|
|
93
|
+
* so "\n" is an unambiguous delimiter.
|
|
94
|
+
*/
|
|
95
|
+
const ndjsonSerializer = {
|
|
96
|
+
encode: (msg) => JSON.stringify(msg) + "\n",
|
|
97
|
+
decode: (line) => JSON.parse(line)
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Creates a stateful line-parser that buffers incoming chunks, splits on
|
|
101
|
+
* newlines, decodes each complete line, and calls `onMessage` with the result.
|
|
102
|
+
*
|
|
103
|
+
* TCP delivers bytes, not messages — a single `data` event can contain
|
|
104
|
+
* half a line, multiple lines, or 1.5 lines. The parser keeps the trailing
|
|
105
|
+
* partial as its buffer for the next chunk.
|
|
106
|
+
*/
|
|
107
|
+
const createLineParser = (onMessage, serializer = ndjsonSerializer) => {
|
|
108
|
+
let buffer = "";
|
|
109
|
+
return (chunk) => {
|
|
110
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
111
|
+
const lines = buffer.split("\n");
|
|
112
|
+
buffer = lines.pop();
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
if (line.length === 0) continue;
|
|
115
|
+
try {
|
|
116
|
+
onMessage(serializer.decode(line));
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/socketTransport.ts
|
|
124
|
+
/**
|
|
125
|
+
* Creates a Transport from a socket with NDJSON framing.
|
|
126
|
+
*
|
|
127
|
+
* Buffers incoming data events, splits on newlines, parses each complete
|
|
128
|
+
* line as JSON, and dispatches postal envelope messages to subscribers.
|
|
129
|
+
*
|
|
130
|
+
* @param socket - A connected net.Socket (or compatible mock)
|
|
131
|
+
* @param serializer - Encode/decode strategy (defaults to NDJSON)
|
|
132
|
+
* @returns A Transport suitable for postal's addTransport()
|
|
133
|
+
*/
|
|
134
|
+
const createSocketTransport = (socket, serializer = ndjsonSerializer) => {
|
|
135
|
+
let disposed = false;
|
|
136
|
+
const listeners = [];
|
|
137
|
+
const onData = createLineParser((parsed) => {
|
|
138
|
+
if (disposed) return;
|
|
139
|
+
if (isUdsEnvelopeMessage(parsed)) {
|
|
140
|
+
const { envelope } = parsed;
|
|
141
|
+
for (const listener of [...listeners]) try {
|
|
142
|
+
listener(envelope);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
queueMicrotask(() => {
|
|
145
|
+
throw err;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}, serializer);
|
|
150
|
+
socket.on("data", onData);
|
|
151
|
+
const send = (envelope) => {
|
|
152
|
+
if (disposed) return;
|
|
153
|
+
socket.write(serializer.encode(createUdsEnvelopeMessage(envelope)));
|
|
154
|
+
};
|
|
155
|
+
const subscribe = (callback) => {
|
|
156
|
+
if (disposed) return () => {};
|
|
157
|
+
listeners.push(callback);
|
|
158
|
+
let removed = false;
|
|
159
|
+
return () => {
|
|
160
|
+
if (removed) return;
|
|
161
|
+
removed = true;
|
|
162
|
+
const index = listeners.indexOf(callback);
|
|
163
|
+
if (index !== -1) listeners.splice(index, 1);
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const dispose = () => {
|
|
167
|
+
if (disposed) return;
|
|
168
|
+
disposed = true;
|
|
169
|
+
socket.removeListener("data", onData);
|
|
170
|
+
listeners.splice(0, listeners.length);
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
send,
|
|
174
|
+
subscribe,
|
|
175
|
+
dispose
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/server.ts
|
|
181
|
+
/**
|
|
182
|
+
* UDS server — listens for incoming client connections on a Unix domain socket.
|
|
183
|
+
*
|
|
184
|
+
* For each connecting client, completes a SYN/ACK handshake, wraps the socket
|
|
185
|
+
* in a Transport, and registers it with postal via addTransport(). Postal's
|
|
186
|
+
* core handles fan-out and echo prevention automatically.
|
|
187
|
+
*
|
|
188
|
+
* @module
|
|
189
|
+
*/
|
|
190
|
+
/**
|
|
191
|
+
* Creates a net.Server, unlinks stale socket file, listens on the path,
|
|
192
|
+
* and handles incoming postal client connections.
|
|
193
|
+
*
|
|
194
|
+
* @param socketPath - Path for the Unix domain socket file
|
|
195
|
+
* @param options - Server configuration
|
|
196
|
+
* @returns Promise resolving with a dispose function for full teardown
|
|
197
|
+
*/
|
|
198
|
+
const listenOnSocket = (socketPath, options = {}) => {
|
|
199
|
+
const { filter, unlinkStale = true, timeout = DEFAULT_TIMEOUT } = options;
|
|
200
|
+
const connections = /* @__PURE__ */ new Map();
|
|
201
|
+
let disposed = false;
|
|
202
|
+
if (unlinkStale) try {
|
|
203
|
+
node_fs.unlinkSync(socketPath);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (err.code !== "ENOENT") throw err;
|
|
206
|
+
}
|
|
207
|
+
const server = node_net.createServer();
|
|
208
|
+
server.on("connection", (clientSocket) => {
|
|
209
|
+
if (disposed) {
|
|
210
|
+
clientSocket.destroy();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let handshakeComplete = false;
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
if (!handshakeComplete) clientSocket.destroy();
|
|
216
|
+
}, timeout);
|
|
217
|
+
const onData = createLineParser((parsed) => {
|
|
218
|
+
if (handshakeComplete) return;
|
|
219
|
+
if (!isUdsSyn(parsed) && looksLikeSyn(parsed)) {
|
|
220
|
+
handshakeComplete = true;
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
clientSocket.removeListener("data", onData);
|
|
223
|
+
clientSocket.destroy();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (isUdsSyn(parsed)) {
|
|
227
|
+
handshakeComplete = true;
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
clientSocket.removeListener("data", onData);
|
|
230
|
+
clientSocket.write(ndjsonSerializer.encode(createUdsAck()));
|
|
231
|
+
const removeTransport = (0, postal.addTransport)(createSocketTransport(clientSocket), { filter });
|
|
232
|
+
connections.set(clientSocket, removeTransport);
|
|
233
|
+
const onClose = () => {
|
|
234
|
+
const removeFn = connections.get(clientSocket);
|
|
235
|
+
if (removeFn) {
|
|
236
|
+
removeFn();
|
|
237
|
+
connections.delete(clientSocket);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
clientSocket.on("close", onClose);
|
|
241
|
+
clientSocket.on("error", onClose);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
clientSocket.on("data", onData);
|
|
245
|
+
});
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const onStartupError = (err) => {
|
|
248
|
+
reject(err);
|
|
249
|
+
};
|
|
250
|
+
server.on("error", onStartupError);
|
|
251
|
+
server.listen(socketPath, () => {
|
|
252
|
+
server.removeListener("error", onStartupError);
|
|
253
|
+
server.on("error", (err) => {
|
|
254
|
+
queueMicrotask(() => {
|
|
255
|
+
throw err;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
const dispose = () => {
|
|
259
|
+
if (disposed) return;
|
|
260
|
+
disposed = true;
|
|
261
|
+
server.close();
|
|
262
|
+
for (const [socket, removeTransport] of connections) {
|
|
263
|
+
removeTransport();
|
|
264
|
+
socket.destroy();
|
|
265
|
+
}
|
|
266
|
+
connections.clear();
|
|
267
|
+
};
|
|
268
|
+
resolve({ dispose });
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/errors.ts
|
|
275
|
+
/**
|
|
276
|
+
* Thrown when the UDS handshake does not complete within the configured timeout.
|
|
277
|
+
*
|
|
278
|
+
* On the client side, this means the server did not respond with an ACK.
|
|
279
|
+
* On the server side, this means a connecting client did not send a SYN.
|
|
280
|
+
* In either case, the socket is destroyed before the error is thrown.
|
|
281
|
+
*
|
|
282
|
+
* The {@link timeout} property preserves the configured value for diagnostics.
|
|
283
|
+
*/
|
|
284
|
+
var PostalUdsHandshakeTimeoutError = class extends Error {
|
|
285
|
+
/** The timeout duration (ms) that was exceeded. */
|
|
286
|
+
timeout;
|
|
287
|
+
constructor(timeout) {
|
|
288
|
+
super(`Postal UDS handshake timed out after ${timeout}ms`);
|
|
289
|
+
this.name = "PostalUdsHandshakeTimeoutError";
|
|
290
|
+
this.timeout = timeout;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Thrown when a SYN or ACK arrives with a protocol version that doesn't
|
|
295
|
+
* match the local PROTOCOL_VERSION. This is a clear signal that the
|
|
296
|
+
* remote side needs to be updated — not a transient network issue.
|
|
297
|
+
*/
|
|
298
|
+
var PostalUdsVersionMismatchError = class extends Error {
|
|
299
|
+
/** The version received from the remote side. */
|
|
300
|
+
received;
|
|
301
|
+
/** The version this side expected (i.e. PROTOCOL_VERSION). */
|
|
302
|
+
expected;
|
|
303
|
+
constructor(received, expected) {
|
|
304
|
+
super(`Postal UDS protocol version mismatch: received ${received}, expected ${expected}`);
|
|
305
|
+
this.name = "PostalUdsVersionMismatchError";
|
|
306
|
+
this.received = received;
|
|
307
|
+
this.expected = expected;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/client.ts
|
|
313
|
+
/**
|
|
314
|
+
* UDS client — connects to a postal server via Unix domain socket.
|
|
315
|
+
*
|
|
316
|
+
* Sends SYN, waits for ACK, wraps the socket in a Transport, and registers
|
|
317
|
+
* it with postal via addTransport(). Returns a function to remove the transport.
|
|
318
|
+
*
|
|
319
|
+
* @module
|
|
320
|
+
*/
|
|
321
|
+
/**
|
|
322
|
+
* Connects to a postal UDS server at the given socket path.
|
|
323
|
+
*
|
|
324
|
+
* Performs a SYN/ACK handshake, wraps the connection in a Transport,
|
|
325
|
+
* and registers it with postal. Returns a function that removes the
|
|
326
|
+
* transport and cleans up the socket.
|
|
327
|
+
*
|
|
328
|
+
* @param socketPath - Path to the server's Unix domain socket
|
|
329
|
+
* @param options - Client configuration
|
|
330
|
+
* @returns Promise resolving with a remove function
|
|
331
|
+
* @throws PostalUdsHandshakeTimeoutError if the server doesn't ACK in time
|
|
332
|
+
*/
|
|
333
|
+
const connectToSocket = (socketPath, options = {}) => {
|
|
334
|
+
const { timeout = DEFAULT_TIMEOUT, onDisconnect } = options;
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
const socket = node_net.connect(socketPath);
|
|
337
|
+
let settled = false;
|
|
338
|
+
const timer = setTimeout(() => {
|
|
339
|
+
if (!settled) {
|
|
340
|
+
settled = true;
|
|
341
|
+
socket.destroy();
|
|
342
|
+
reject(new PostalUdsHandshakeTimeoutError(timeout));
|
|
343
|
+
}
|
|
344
|
+
}, timeout);
|
|
345
|
+
const onError = (err) => {
|
|
346
|
+
if (!settled) {
|
|
347
|
+
settled = true;
|
|
348
|
+
clearTimeout(timer);
|
|
349
|
+
socket.destroy();
|
|
350
|
+
reject(err);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
socket.on("error", onError);
|
|
354
|
+
const onData = createLineParser((parsed) => {
|
|
355
|
+
if (settled) return;
|
|
356
|
+
if (!isUdsAck(parsed) && looksLikeAck(parsed)) {
|
|
357
|
+
settled = true;
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
socket.removeListener("data", onData);
|
|
360
|
+
socket.removeListener("error", onError);
|
|
361
|
+
socket.destroy();
|
|
362
|
+
reject(new PostalUdsVersionMismatchError(parsed.version, PROTOCOL_VERSION));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (isUdsAck(parsed)) {
|
|
366
|
+
settled = true;
|
|
367
|
+
clearTimeout(timer);
|
|
368
|
+
socket.removeListener("data", onData);
|
|
369
|
+
socket.removeListener("error", onError);
|
|
370
|
+
const removeTransport = (0, postal.addTransport)(createSocketTransport(socket));
|
|
371
|
+
const onClose = onDisconnect ? () => onDisconnect() : void 0;
|
|
372
|
+
if (onClose) socket.on("close", onClose);
|
|
373
|
+
resolve(() => {
|
|
374
|
+
if (onClose) socket.removeListener("close", onClose);
|
|
375
|
+
removeTransport();
|
|
376
|
+
socket.destroy();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
socket.on("data", onData);
|
|
381
|
+
socket.on("connect", () => {
|
|
382
|
+
if (!settled) socket.write(ndjsonSerializer.encode(createUdsSyn()));
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
//#endregion
|
|
388
|
+
exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
389
|
+
exports.PostalUdsHandshakeTimeoutError = PostalUdsHandshakeTimeoutError;
|
|
390
|
+
exports.PostalUdsVersionMismatchError = PostalUdsVersionMismatchError;
|
|
391
|
+
exports.connectToSocket = connectToSocket;
|
|
392
|
+
exports.createSocketTransport = createSocketTransport;
|
|
393
|
+
exports.listenOnSocket = listenOnSocket;
|
|
394
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["net","net"],"sources":["../src/protocol.ts","../src/serialization.ts","../src/socketTransport.ts","../src/server.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * Postal UDS handshake protocol.\n *\n * Namespaced to \"postal:uds-\" for handshake messages to avoid collisions\n * with other transport types sharing a process. Envelope messages use\n * the shared \"postal:envelope\" type.\n *\n * Sequence:\n * 1. Client connects to socket, sends { type: \"postal:uds-syn\" }\n * 2. Server receives SYN, responds with { type: \"postal:uds-ack\" }\n * 3. Both sides wrap the socket in createSocketTransport()\n *\n * @module\n */\n\nimport type { Envelope } from \"postal\";\n\n/** Protocol version — bump if the handshake format changes. */\nexport const PROTOCOL_VERSION = 1;\n\n/** Default handshake timeout in milliseconds. */\nexport const DEFAULT_TIMEOUT = 5000;\n\nconst POSTAL_NAMESPACE = \"postal:\";\n\n// --- Message shapes ---\n\n/** Client-to-server handshake initiation. */\nexport type UdsSynMessage = {\n type: \"postal:uds-syn\";\n version: number;\n};\n\n/** Server-to-client handshake acknowledgment. */\nexport type UdsAckMessage = {\n type: \"postal:uds-ack\";\n version: number;\n};\n\n/** Wraps a postal Envelope for transmission over the socket. */\nexport type UdsEnvelopeMessage = {\n type: \"postal:envelope\";\n version: number;\n envelope: Envelope;\n};\n\n/** Union of all messages that can appear on the wire. */\nexport type UdsProtocolMessage = UdsSynMessage | UdsAckMessage | UdsEnvelopeMessage;\n\n// --- Type guards ---\n// Three-step validation: check it's an object, has a string `type` field,\n// and the type starts with the postal namespace. This rejects nulls, arrays,\n// primitives, and non-postal traffic without throwing.\n\nconst isPostalMessage = (data: unknown): data is UdsProtocolMessage => {\n return (\n typeof data === \"object\" &&\n data !== null &&\n \"type\" in data &&\n typeof (data as UdsProtocolMessage).type === \"string\" &&\n (data as UdsProtocolMessage).type.startsWith(POSTAL_NAMESPACE)\n );\n};\n\n/** Narrows unknown data to a SYN message. Safe to call on any input. */\nexport const isUdsSyn = (data: unknown): data is UdsSynMessage => {\n return (\n isPostalMessage(data) &&\n (data as UdsSynMessage).type === \"postal:uds-syn\" &&\n (data as UdsSynMessage).version === PROTOCOL_VERSION\n );\n};\n\n/** Narrows unknown data to an ACK message. Safe to call on any input. */\nexport const isUdsAck = (data: unknown): data is UdsAckMessage => {\n return (\n isPostalMessage(data) &&\n (data as UdsAckMessage).type === \"postal:uds-ack\" &&\n (data as UdsAckMessage).version === PROTOCOL_VERSION\n );\n};\n\n/**\n * Loose check — matches the SYN type string without version validation.\n * Used to distinguish \"wrong version\" from \"not a SYN at all\".\n */\nexport const looksLikeSyn = (data: unknown): data is UdsSynMessage => {\n return isPostalMessage(data) && (data as UdsSynMessage).type === \"postal:uds-syn\";\n};\n\n/**\n * Loose check — matches the ACK type string without version validation.\n * Used to distinguish \"wrong version\" from \"not an ACK at all\".\n */\nexport const looksLikeAck = (data: unknown): data is UdsAckMessage => {\n return isPostalMessage(data) && (data as UdsAckMessage).type === \"postal:uds-ack\";\n};\n\n/** Narrows unknown data to an envelope wrapper. Validates type, version, and that envelope is a non-null object. */\nexport const isUdsEnvelopeMessage = (data: unknown): data is UdsEnvelopeMessage => {\n return (\n isPostalMessage(data) &&\n (data as UdsEnvelopeMessage).type === \"postal:envelope\" &&\n (data as UdsEnvelopeMessage).version === PROTOCOL_VERSION &&\n \"envelope\" in data &&\n typeof (data as UdsEnvelopeMessage).envelope === \"object\" &&\n (data as UdsEnvelopeMessage).envelope !== null\n );\n};\n\n// --- Message factories ---\n\n/** Creates a SYN message stamped with the current protocol version. */\nexport const createUdsSyn = (): UdsSynMessage => ({\n type: \"postal:uds-syn\",\n version: PROTOCOL_VERSION,\n});\n\n/** Creates an ACK message stamped with the current protocol version. */\nexport const createUdsAck = (): UdsAckMessage => ({\n type: \"postal:uds-ack\",\n version: PROTOCOL_VERSION,\n});\n\n/** Wraps a postal Envelope in a protocol message for wire transmission. */\nexport const createUdsEnvelopeMessage = (envelope: Envelope): UdsEnvelopeMessage => ({\n type: \"postal:envelope\",\n version: PROTOCOL_VERSION,\n envelope,\n});\n","/**\n * Serialization abstraction for the UDS transport.\n *\n * The default NDJSON implementation is the only built-in serializer.\n * The interface exists so MessagePack (or similar) can be swapped in\n * without restructuring the transport.\n *\n * @module\n */\n\n/** Encode/decode contract for wire serialization. */\nexport type Serializer = {\n /** Serialize a message to a newline-terminated string. */\n encode: (msg: unknown) => string;\n /** Deserialize a single line (without trailing newline) back to a value. */\n decode: (line: string) => unknown;\n};\n\n/**\n * NDJSON serializer — JSON.stringify + \"\\n\" on encode, JSON.parse on decode.\n *\n * JSON's escaping rules guarantee no unescaped newlines in the output,\n * so \"\\n\" is an unambiguous delimiter.\n */\nexport const ndjsonSerializer: Serializer = {\n encode: (msg: unknown): string => JSON.stringify(msg) + \"\\n\",\n decode: (line: string): unknown => JSON.parse(line),\n};\n\n/**\n * Creates a stateful line-parser that buffers incoming chunks, splits on\n * newlines, decodes each complete line, and calls `onMessage` with the result.\n *\n * TCP delivers bytes, not messages — a single `data` event can contain\n * half a line, multiple lines, or 1.5 lines. The parser keeps the trailing\n * partial as its buffer for the next chunk.\n */\nexport const createLineParser = (\n onMessage: (parsed: unknown) => void,\n serializer: Serializer = ndjsonSerializer\n): ((chunk: Buffer | string) => void) => {\n let buffer = \"\";\n return (chunk: Buffer | string): void => {\n buffer += typeof chunk === \"string\" ? chunk : chunk.toString(\"utf-8\");\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop()!;\n for (const line of lines) {\n if (line.length === 0) {\n continue;\n }\n try {\n onMessage(serializer.decode(line));\n } catch {\n // Malformed line — skip\n }\n }\n };\n};\n","/**\n * Low-level Transport backed by a net.Socket with NDJSON framing.\n *\n * Works with any object that satisfies the UdsSocket shape — real\n * net.Socket instances or test mocks based on EventEmitter.\n *\n * The transport does NOT own the socket lifecycle. dispose() removes\n * the data listener and clears subscribers, but does NOT call\n * socket.destroy(). The server/client modules own their sockets.\n *\n * @module\n */\n\nimport type { Transport, Envelope } from \"postal\";\nimport { isUdsEnvelopeMessage, createUdsEnvelopeMessage } from \"./protocol\";\nimport { createLineParser, ndjsonSerializer, type Serializer } from \"./serialization\";\n\n/**\n * Minimal socket surface — anything with write, on, and removeListener\n * for the \"data\" event. Typed loosely to avoid importing @types/node.\n */\nexport type UdsSocket = {\n write: (data: string) => void;\n on: (event: \"data\", handler: (chunk: string | Buffer) => void) => void;\n removeListener: (event: \"data\", handler: (chunk: string | Buffer) => void) => void;\n};\n\n/**\n * Creates a Transport from a socket with NDJSON framing.\n *\n * Buffers incoming data events, splits on newlines, parses each complete\n * line as JSON, and dispatches postal envelope messages to subscribers.\n *\n * @param socket - A connected net.Socket (or compatible mock)\n * @param serializer - Encode/decode strategy (defaults to NDJSON)\n * @returns A Transport suitable for postal's addTransport()\n */\nexport const createSocketTransport = (\n socket: UdsSocket,\n serializer: Serializer = ndjsonSerializer\n): Transport => {\n let disposed = false;\n const listeners: ((envelope: Envelope) => void)[] = [];\n\n const onData = createLineParser(parsed => {\n if (disposed) {\n return;\n }\n\n if (isUdsEnvelopeMessage(parsed)) {\n const { envelope } = parsed;\n // Snapshot — safe if a listener unsubscribes during iteration\n for (const listener of [...listeners]) {\n try {\n listener(envelope);\n } catch (err) {\n // Re-throw asynchronously so a single bad listener doesn't\n // kill delivery to the rest.\n queueMicrotask(() => {\n throw err;\n });\n }\n }\n }\n }, serializer);\n\n socket.on(\"data\", onData);\n\n const send = (envelope: Envelope): void => {\n if (disposed) {\n return;\n }\n socket.write(serializer.encode(createUdsEnvelopeMessage(envelope)));\n };\n\n const subscribe = (callback: (envelope: Envelope) => void): (() => void) => {\n if (disposed) {\n return () => {};\n }\n\n listeners.push(callback);\n\n let removed = false;\n return () => {\n if (removed) {\n return;\n }\n removed = true;\n const index = listeners.indexOf(callback);\n if (index !== -1) {\n listeners.splice(index, 1);\n }\n };\n };\n\n const dispose = (): void => {\n if (disposed) {\n return;\n }\n disposed = true;\n socket.removeListener(\"data\", onData);\n listeners.splice(0, listeners.length);\n // Intentionally NOT calling socket.destroy().\n // The transport is a consumer of the socket, not its owner.\n };\n\n return { send, subscribe, dispose };\n};\n","/**\n * UDS server — listens for incoming client connections on a Unix domain socket.\n *\n * For each connecting client, completes a SYN/ACK handshake, wraps the socket\n * in a Transport, and registers it with postal via addTransport(). Postal's\n * core handles fan-out and echo prevention automatically.\n *\n * @module\n */\n\nimport * as net from \"node:net\";\nimport * as fs from \"node:fs\";\nimport { addTransport } from \"postal\";\nimport { createSocketTransport } from \"./socketTransport\";\nimport { isUdsSyn, looksLikeSyn, createUdsAck, DEFAULT_TIMEOUT } from \"./protocol\";\nimport { createLineParser, ndjsonSerializer } from \"./serialization\";\nimport type { UdsServerOptions } from \"./types\";\n\n/** Maps each connected client socket to its transport-removal function. */\ntype ConnectionMap = Map<net.Socket, () => void>;\n\n/**\n * Creates a net.Server, unlinks stale socket file, listens on the path,\n * and handles incoming postal client connections.\n *\n * @param socketPath - Path for the Unix domain socket file\n * @param options - Server configuration\n * @returns Promise resolving with a dispose function for full teardown\n */\nexport const listenOnSocket = (\n socketPath: string,\n options: UdsServerOptions = {}\n): Promise<{ dispose: () => void }> => {\n const { filter, unlinkStale = true, timeout = DEFAULT_TIMEOUT } = options;\n const connections: ConnectionMap = new Map();\n let disposed = false;\n\n // Attempt to unlink a stale socket file left by a crashed process\n if (unlinkStale) {\n try {\n fs.unlinkSync(socketPath);\n } catch (err: unknown) {\n // ENOENT is fine — no stale file to clean up\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw err;\n }\n }\n }\n\n const server = net.createServer();\n\n server.on(\"connection\", (clientSocket: net.Socket) => {\n if (disposed) {\n clientSocket.destroy();\n return;\n }\n\n let handshakeComplete = false;\n\n // Timeout for slow/non-postal clients that never send SYN\n const timer = setTimeout(() => {\n if (!handshakeComplete) {\n clientSocket.destroy();\n }\n }, timeout);\n\n const onData = createLineParser(parsed => {\n if (handshakeComplete) {\n return;\n }\n\n // Detect version mismatch before it times out with an unhelpful error\n if (!isUdsSyn(parsed) && looksLikeSyn(parsed)) {\n handshakeComplete = true;\n clearTimeout(timer);\n clientSocket.removeListener(\"data\", onData);\n clientSocket.destroy();\n return;\n }\n\n if (isUdsSyn(parsed)) {\n handshakeComplete = true;\n clearTimeout(timer);\n clientSocket.removeListener(\"data\", onData);\n\n // Send ACK before creating the transport so the client's\n // ACK listener sees it before any envelope messages arrive\n clientSocket.write(ndjsonSerializer.encode(createUdsAck()));\n\n const transport = createSocketTransport(clientSocket);\n const removeTransport = addTransport(transport, { filter });\n\n connections.set(clientSocket, removeTransport);\n\n // Both close and error can fire for the same socket death,\n // so the handler is idempotent — the Map lookup guards against\n // double-removal.\n const onClose = (): void => {\n const removeFn = connections.get(clientSocket);\n if (removeFn) {\n removeFn();\n connections.delete(clientSocket);\n }\n };\n\n clientSocket.on(\"close\", onClose);\n clientSocket.on(\"error\", onClose);\n }\n });\n\n clientSocket.on(\"data\", onData);\n });\n\n return new Promise<{ dispose: () => void }>((resolve, reject) => {\n const onStartupError = (err: Error): void => {\n reject(err);\n };\n\n server.on(\"error\", onStartupError);\n\n server.listen(socketPath, () => {\n // Swap the pre-listen error handler for one that surfaces\n // post-startup errors instead of silently dropping them\n server.removeListener(\"error\", onStartupError);\n server.on(\"error\", (err: Error) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n\n const dispose = (): void => {\n if (disposed) {\n return;\n }\n disposed = true;\n\n server.close();\n\n for (const [socket, removeTransport] of connections) {\n removeTransport();\n socket.destroy();\n }\n connections.clear();\n };\n\n resolve({ dispose });\n });\n });\n};\n","/**\n * Thrown when the UDS handshake does not complete within the configured timeout.\n *\n * On the client side, this means the server did not respond with an ACK.\n * On the server side, this means a connecting client did not send a SYN.\n * In either case, the socket is destroyed before the error is thrown.\n *\n * The {@link timeout} property preserves the configured value for diagnostics.\n */\nexport class PostalUdsHandshakeTimeoutError extends Error {\n /** The timeout duration (ms) that was exceeded. */\n readonly timeout: number;\n\n constructor(timeout: number) {\n super(`Postal UDS handshake timed out after ${timeout}ms`);\n this.name = \"PostalUdsHandshakeTimeoutError\";\n this.timeout = timeout;\n }\n}\n\n/**\n * Thrown when a SYN or ACK arrives with a protocol version that doesn't\n * match the local PROTOCOL_VERSION. This is a clear signal that the\n * remote side needs to be updated — not a transient network issue.\n */\nexport class PostalUdsVersionMismatchError extends Error {\n /** The version received from the remote side. */\n readonly received: number;\n /** The version this side expected (i.e. PROTOCOL_VERSION). */\n readonly expected: number;\n\n constructor(received: number, expected: number) {\n super(`Postal UDS protocol version mismatch: received ${received}, expected ${expected}`);\n this.name = \"PostalUdsVersionMismatchError\";\n this.received = received;\n this.expected = expected;\n }\n}\n","/**\n * UDS client — connects to a postal server via Unix domain socket.\n *\n * Sends SYN, waits for ACK, wraps the socket in a Transport, and registers\n * it with postal via addTransport(). Returns a function to remove the transport.\n *\n * @module\n */\n\nimport * as net from \"node:net\";\nimport { addTransport } from \"postal\";\nimport { createSocketTransport } from \"./socketTransport\";\nimport {\n createUdsSyn,\n isUdsAck,\n looksLikeAck,\n PROTOCOL_VERSION,\n DEFAULT_TIMEOUT,\n} from \"./protocol\";\nimport { createLineParser, ndjsonSerializer } from \"./serialization\";\nimport { PostalUdsHandshakeTimeoutError, PostalUdsVersionMismatchError } from \"./errors\";\nimport type { UdsConnectOptions } from \"./types\";\n\n/**\n * Connects to a postal UDS server at the given socket path.\n *\n * Performs a SYN/ACK handshake, wraps the connection in a Transport,\n * and registers it with postal. Returns a function that removes the\n * transport and cleans up the socket.\n *\n * @param socketPath - Path to the server's Unix domain socket\n * @param options - Client configuration\n * @returns Promise resolving with a remove function\n * @throws PostalUdsHandshakeTimeoutError if the server doesn't ACK in time\n */\nexport const connectToSocket = (\n socketPath: string,\n options: UdsConnectOptions = {}\n): Promise<() => void> => {\n const { timeout = DEFAULT_TIMEOUT, onDisconnect } = options;\n\n return new Promise<() => void>((resolve, reject) => {\n const socket = net.connect(socketPath);\n // Guards the promise from double-settlement — timeout, error, and\n // successful ACK are all racing to resolve/reject.\n let settled = false;\n\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n socket.destroy();\n reject(new PostalUdsHandshakeTimeoutError(timeout));\n }\n }, timeout);\n\n const onError = (err: Error): void => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n socket.destroy();\n reject(err);\n }\n };\n\n socket.on(\"error\", onError);\n\n const onData = createLineParser(parsed => {\n if (settled) {\n return;\n }\n\n // Detect version mismatch before it times out with an unhelpful error\n if (!isUdsAck(parsed) && looksLikeAck(parsed)) {\n settled = true;\n clearTimeout(timer);\n socket.removeListener(\"data\", onData);\n socket.removeListener(\"error\", onError);\n socket.destroy();\n reject(new PostalUdsVersionMismatchError(parsed.version, PROTOCOL_VERSION));\n return;\n }\n\n if (isUdsAck(parsed)) {\n settled = true;\n clearTimeout(timer);\n socket.removeListener(\"data\", onData);\n socket.removeListener(\"error\", onError);\n\n const transport = createSocketTransport(socket);\n const removeTransport = addTransport(transport);\n\n // Wire up onDisconnect callback — remove the close listener\n // during cleanup so that calling the returned function doesn't\n // fire onDisconnect spuriously (socket.destroy() triggers close).\n const onClose = onDisconnect ? () => onDisconnect() : undefined;\n if (onClose) {\n socket.on(\"close\", onClose);\n }\n\n resolve(() => {\n if (onClose) {\n socket.removeListener(\"close\", onClose);\n }\n removeTransport();\n socket.destroy();\n });\n }\n });\n\n socket.on(\"data\", onData);\n\n // Send SYN once the connection is established\n socket.on(\"connect\", () => {\n if (!settled) {\n socket.write(ndjsonSerializer.encode(createUdsSyn()));\n }\n });\n });\n};\n"],"mappings":"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"}
|