posipaki 0.35.0 → 0.35.2
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/dist/{client-DHI-6XPF.d.ts → client-C4LZE65N.d.ts} +2 -2
- package/dist/{client-DHI-6XPF.d.ts.map → client-C4LZE65N.d.ts.map} +1 -1
- package/dist/hooks-CgXhmY4r.js.map +1 -1
- package/dist/{hooks-CfQ8EhKW.d.ts → hooks-Dj59y_jN.d.ts} +28 -15
- package/dist/hooks-Dj59y_jN.d.ts.map +1 -0
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{client-D23TlXPf.js → json1-DzPx9oX3.js} +47 -3
- package/dist/json1-DzPx9oX3.js.map +1 -0
- package/dist/plugins/debug-logger.d.ts +1 -1
- package/dist/plugins/tree-introspection.d.ts +1 -1
- package/dist/remote/gateway-cli.js +409 -1
- package/dist/remote/gateway-cli.js.map +1 -1
- package/dist/remote/index.d.ts +2 -2
- package/dist/remote/index.js +1 -2
- package/dist/remote/index.js.map +1 -1
- package/dist/remote/node.d.ts +2 -2
- package/dist/remote/node.js +534 -7
- package/dist/remote/node.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/{version-DDAdqTTr.js → version-0mlM8dbC.js} +2 -2
- package/dist/{version-DDAdqTTr.js.map → version-0mlM8dbC.js.map} +1 -1
- package/package.json +1 -1
- package/dist/client-D23TlXPf.js.map +0 -1
- package/dist/gateway-Cj3HSrl3.js +0 -537
- package/dist/gateway-Cj3HSrl3.js.map +0 -1
- package/dist/hooks-CfQ8EhKW.d.ts.map +0 -1
- package/dist/json1-DS3xxg13.js +0 -48
- package/dist/json1-DS3xxg13.js.map +0 -1
- package/dist/remote/gateway-cli.d.ts +0 -1
|
@@ -1,4 +1,412 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, open, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import * as readline from "node:readline";
|
|
7
|
+
//#region src/remote/stdio.ts
|
|
8
|
+
const OUTPUT_KEY = "$fd";
|
|
9
|
+
const ERROR_KEY = "$error";
|
|
10
|
+
/** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */
|
|
11
|
+
var StdioWireError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "StdioWireError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function outputFrame(fd, data) {
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
[OUTPUT_KEY]: fd,
|
|
20
|
+
data
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function errorFrame(reason) {
|
|
24
|
+
return JSON.stringify({ [ERROR_KEY]: reason });
|
|
25
|
+
}
|
|
26
|
+
/** Is this write error just a peer that already left? */
|
|
27
|
+
function peerGone(code) {
|
|
28
|
+
return code === "EPIPE" || code === "ERR_STREAM_DESTROYED" || code === "ERR_STREAM_WRITE_AFTER_END" || code === "ERR_STREAM_ALREADY_FINISHED";
|
|
29
|
+
}
|
|
30
|
+
function errorText$1(err) {
|
|
31
|
+
return err instanceof Error ? err.message : String(err);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A `StringTransport` over a read/write stream pair, one JSON frame per line.
|
|
35
|
+
*
|
|
36
|
+
* `close()` is idempotent and never throws: teardown races are normal here —
|
|
37
|
+
* the peer exits, the read side ends, and a write arrives a moment later.
|
|
38
|
+
*/
|
|
39
|
+
var LineTransport = class {
|
|
40
|
+
constructor(streams) {
|
|
41
|
+
this.pvtOnMessage = null;
|
|
42
|
+
this.pvtOnClose = null;
|
|
43
|
+
this.pvtClosed = false;
|
|
44
|
+
this.pvtClosing = null;
|
|
45
|
+
this.streams = streams;
|
|
46
|
+
const lines = readline.createInterface({ input: streams.read });
|
|
47
|
+
lines.on("line", (line) => {
|
|
48
|
+
if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);
|
|
49
|
+
});
|
|
50
|
+
lines.on("close", () => {
|
|
51
|
+
this.pvtOnClose?.();
|
|
52
|
+
this.close();
|
|
53
|
+
});
|
|
54
|
+
streams.read.on("error", () => {
|
|
55
|
+
this.close();
|
|
56
|
+
});
|
|
57
|
+
streams.write.on("error", () => {
|
|
58
|
+
this.close();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
get closed() {
|
|
62
|
+
return this.pvtClosed;
|
|
63
|
+
}
|
|
64
|
+
get hasHandler() {
|
|
65
|
+
return this.pvtOnMessage !== null;
|
|
66
|
+
}
|
|
67
|
+
onMessage(handler) {
|
|
68
|
+
if (this.pvtClosed) throw new StdioWireError("stdio transport: closed");
|
|
69
|
+
if (this.pvtOnMessage !== null) throw new StdioWireError("stdio transport: handler already set — call removeHandler()");
|
|
70
|
+
this.pvtOnMessage = handler;
|
|
71
|
+
}
|
|
72
|
+
removeHandler() {
|
|
73
|
+
const previous = this.pvtOnMessage;
|
|
74
|
+
this.pvtOnMessage = null;
|
|
75
|
+
return previous;
|
|
76
|
+
}
|
|
77
|
+
onClose(handler) {
|
|
78
|
+
this.pvtOnClose = handler;
|
|
79
|
+
}
|
|
80
|
+
send(frame) {
|
|
81
|
+
if (this.pvtClosed) return Promise.resolve();
|
|
82
|
+
const line = frame.endsWith("\n") ? frame : `${frame}\n`;
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
try {
|
|
85
|
+
this.streams.write.write(line, (err) => {
|
|
86
|
+
if (!err) return resolve();
|
|
87
|
+
const code = err.code;
|
|
88
|
+
if (peerGone(code)) return resolve();
|
|
89
|
+
reject(new StdioWireError(`stdio transport: ${err.message}`));
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
reject(new StdioWireError(`stdio transport: ${errorText$1(err)}`));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async close() {
|
|
97
|
+
this.pvtClosing ??= this.pvtClose();
|
|
98
|
+
await this.pvtClosing;
|
|
99
|
+
}
|
|
100
|
+
async pvtClose() {
|
|
101
|
+
await Promise.resolve();
|
|
102
|
+
this.pvtClosed = true;
|
|
103
|
+
try {
|
|
104
|
+
this.streams.read.destroy?.();
|
|
105
|
+
} catch {}
|
|
106
|
+
try {
|
|
107
|
+
if (!this.streams.write.writableEnded) this.streams.write.end();
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/remote/transports/fifo.ts
|
|
113
|
+
var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
|
|
114
|
+
constructor(opts) {
|
|
115
|
+
this.pvtOnMessage = null;
|
|
116
|
+
this.pvtOnClose = null;
|
|
117
|
+
this.closed = false;
|
|
118
|
+
this.closingPromise = null;
|
|
119
|
+
this.pvtError = null;
|
|
120
|
+
this.readFd = opts.readFd ?? null;
|
|
121
|
+
this.writeFd = opts.writeFd ?? null;
|
|
122
|
+
if (this.readFd) {
|
|
123
|
+
this.rs = this.readFd.createReadStream({ encoding: "utf-8" });
|
|
124
|
+
this.rl = readline.createInterface({ input: this.rs });
|
|
125
|
+
this.rl.on("line", (line) => {
|
|
126
|
+
if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);
|
|
127
|
+
});
|
|
128
|
+
this.rl.on("close", () => {
|
|
129
|
+
this.pvtOnClose?.();
|
|
130
|
+
this.close();
|
|
131
|
+
});
|
|
132
|
+
this.rs.on("error", (err) => {
|
|
133
|
+
this.pvtError = err;
|
|
134
|
+
this.close();
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
this.rs = null;
|
|
138
|
+
this.rl = null;
|
|
139
|
+
}
|
|
140
|
+
if (this.writeFd) {
|
|
141
|
+
this.ws = this.writeFd.createWriteStream({ encoding: "utf-8" });
|
|
142
|
+
this.ws.on("error", (err) => {
|
|
143
|
+
this.pvtError = err;
|
|
144
|
+
this.close();
|
|
145
|
+
});
|
|
146
|
+
} else this.ws = null;
|
|
147
|
+
}
|
|
148
|
+
static openReaderFd(readFd) {
|
|
149
|
+
return new FifoUtf8NlineTransport({ readFd });
|
|
150
|
+
}
|
|
151
|
+
static openWriterFd(writeFd) {
|
|
152
|
+
return new FifoUtf8NlineTransport({ writeFd });
|
|
153
|
+
}
|
|
154
|
+
static fromFds(readFd, writeFd) {
|
|
155
|
+
return new FifoUtf8NlineTransport({
|
|
156
|
+
readFd,
|
|
157
|
+
writeFd
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Start opening a bidirectional connection. Opens readPath for reading
|
|
162
|
+
* in the background, returns a promise. The caller should do whatever
|
|
163
|
+
* setup is needed to unblock the read (e.g. spawn a process that opens
|
|
164
|
+
* readPath for writing), then await the returned transport.
|
|
165
|
+
*
|
|
166
|
+
* The transport itself is built only once both directions are open. A read
|
|
167
|
+
* stream created while no writer exists on its fifo is handed an immediate
|
|
168
|
+
* end-of-stream by some runtimes (bun), which closes a channel that has not
|
|
169
|
+
* carried a byte yet — so the writes side is opened before the reader exists.
|
|
170
|
+
*/
|
|
171
|
+
static beginConnect(readPath, writePath) {
|
|
172
|
+
return { transport: open(readPath, "r").then(async (readFd) => {
|
|
173
|
+
const writeFd = await open(writePath, "w");
|
|
174
|
+
return FifoUtf8NlineTransport.fromFds(readFd, writeFd);
|
|
175
|
+
}) };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Open a bidirectional connection. Opens writePath for writing first
|
|
179
|
+
* (unblocking the peer's read), starts reading readPath in the
|
|
180
|
+
* background, then awaits both.
|
|
181
|
+
*
|
|
182
|
+
* Use this when you are the side that responds to the peer's beginConnect
|
|
183
|
+
* (i.e. you don't need to interleave any setup between the read and write
|
|
184
|
+
* opens). Built like `beginConnect`: the transport exists only after both
|
|
185
|
+
* directions do.
|
|
186
|
+
*/
|
|
187
|
+
static async connect(readPath, writePath) {
|
|
188
|
+
const readFdPromise = open(readPath, "r");
|
|
189
|
+
const writeFd = await open(writePath, "w");
|
|
190
|
+
const readFd = await readFdPromise;
|
|
191
|
+
return FifoUtf8NlineTransport.fromFds(readFd, writeFd);
|
|
192
|
+
}
|
|
193
|
+
get canSend() {
|
|
194
|
+
return this.writeFd !== null && !this.closed;
|
|
195
|
+
}
|
|
196
|
+
onMessage(handler) {
|
|
197
|
+
if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
|
|
198
|
+
if (this.readFd === null) throw new Error("FifoUtf8NlineTransport: not a reader");
|
|
199
|
+
if (this.pvtOnMessage !== null) throw new Error("FifoUtf8NlineTransport: handler already set — call removeHandler() first");
|
|
200
|
+
this.pvtOnMessage = handler;
|
|
201
|
+
}
|
|
202
|
+
removeHandler() {
|
|
203
|
+
const prev = this.pvtOnMessage;
|
|
204
|
+
this.pvtOnMessage = null;
|
|
205
|
+
return prev;
|
|
206
|
+
}
|
|
207
|
+
onClose(handler) {
|
|
208
|
+
this.pvtOnClose = handler;
|
|
209
|
+
}
|
|
210
|
+
get hasHandler() {
|
|
211
|
+
return this.pvtOnMessage !== null;
|
|
212
|
+
}
|
|
213
|
+
get lastError() {
|
|
214
|
+
return this.pvtError;
|
|
215
|
+
}
|
|
216
|
+
async send(line) {
|
|
217
|
+
if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
|
|
218
|
+
if (this.writeFd === null) throw new Error("FifoUtf8NlineTransport: not a writer");
|
|
219
|
+
if (!line.endsWith("\n")) line += "\n";
|
|
220
|
+
await this.ws?.write(line);
|
|
221
|
+
}
|
|
222
|
+
async close() {
|
|
223
|
+
this.closingPromise = this.closingPromise || this.pvtClose();
|
|
224
|
+
await this.closingPromise;
|
|
225
|
+
this.closed = true;
|
|
226
|
+
}
|
|
227
|
+
async pvtClose() {
|
|
228
|
+
await Promise.resolve();
|
|
229
|
+
if (this.rl) {
|
|
230
|
+
this.rl.close();
|
|
231
|
+
this.rl = null;
|
|
232
|
+
}
|
|
233
|
+
if (this.rs) {
|
|
234
|
+
this.rs.destroy();
|
|
235
|
+
this.rs = null;
|
|
236
|
+
}
|
|
237
|
+
if (this.ws) {
|
|
238
|
+
await new Promise((resolve) => {
|
|
239
|
+
try {
|
|
240
|
+
this.ws?.end("", resolve);
|
|
241
|
+
} catch {
|
|
242
|
+
resolve();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
this.ws = null;
|
|
246
|
+
}
|
|
247
|
+
if (this.readFd) {
|
|
248
|
+
try {
|
|
249
|
+
await this.readFd.close();
|
|
250
|
+
} catch {}
|
|
251
|
+
this.readFd = null;
|
|
252
|
+
}
|
|
253
|
+
if (this.writeFd) {
|
|
254
|
+
try {
|
|
255
|
+
await this.writeFd.close();
|
|
256
|
+
} catch {}
|
|
257
|
+
this.writeFd = null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/remote/gateway.ts
|
|
263
|
+
const run = promisify(execFile);
|
|
264
|
+
function errorText(err) {
|
|
265
|
+
return err instanceof Error ? err.message : String(err);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A label for the gateway's own temp directory: the program this environment was made for.
|
|
269
|
+
* It is the gateway's business alone — the payload is not told which label it is.
|
|
270
|
+
*/
|
|
271
|
+
function label(worker) {
|
|
272
|
+
const name = basename(worker).replace(/[^A-Za-z0-9.+@_-]/g, "-").slice(0, 60);
|
|
273
|
+
return name === "" ? "payload" : name;
|
|
274
|
+
}
|
|
275
|
+
/** The boot a gateway was started with, or the reason it cannot start. */
|
|
276
|
+
function gatewayBoot(argv) {
|
|
277
|
+
const [worker, ...passthrough] = argv;
|
|
278
|
+
if (worker === void 0 || worker.startsWith("--")) throw new Error("no <payload>: the gateway relays to a payload, named as its first argument");
|
|
279
|
+
return {
|
|
280
|
+
passthrough,
|
|
281
|
+
worker
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* End a start that failed. The reason has already gone out on the wire, and
|
|
286
|
+
* returning is not enough: the `open()` on a fifo no payload will ever write sits
|
|
287
|
+
* in the threadpool forever, so the loop never drains and the gateway would linger
|
|
288
|
+
* with a dead payload on its books.
|
|
289
|
+
*/
|
|
290
|
+
function abandon() {
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
/** Carry a failed start to the client, then let the caller exit non-zero. */
|
|
294
|
+
async function fail(wire, reason) {
|
|
295
|
+
process.stderr.write(`gateway: ${reason}\n`);
|
|
296
|
+
try {
|
|
297
|
+
await wire.send(errorFrame(reason));
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
/** Copy everything the payload prints onto the wire as output frames. */
|
|
301
|
+
function forwardOutput(stream, wire, fd) {
|
|
302
|
+
stream.setEncoding("utf-8");
|
|
303
|
+
stream.on("data", (chunk) => {
|
|
304
|
+
wire.send(outputFrame(fd, chunk)).catch(() => {});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Serve one environment over stdin/stdout. Returns the exit code; the caller
|
|
309
|
+
* turns it into `process.exitCode`.
|
|
310
|
+
*/
|
|
311
|
+
async function runGateway(argv = process.argv.slice(2)) {
|
|
312
|
+
const wire = new LineTransport({
|
|
313
|
+
read: process.stdin,
|
|
314
|
+
write: process.stdout
|
|
315
|
+
});
|
|
316
|
+
let booted;
|
|
317
|
+
try {
|
|
318
|
+
booted = gatewayBoot(argv);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
await fail(wire, errorText(err));
|
|
321
|
+
abandon();
|
|
322
|
+
}
|
|
323
|
+
const { passthrough, worker } = booted;
|
|
324
|
+
const dir = await mkdtemp(join(tmpdir(), `posipaki-${label(worker)}-`));
|
|
325
|
+
const fifoIn = join(dir, "in");
|
|
326
|
+
const fifoOut = join(dir, "out");
|
|
327
|
+
let child = null;
|
|
328
|
+
let fifo = null;
|
|
329
|
+
/**
|
|
330
|
+
* Everything this gateway owns, undone. All of it is idempotent, because the
|
|
331
|
+
* exit path and the signal path can both arrive.
|
|
332
|
+
*/
|
|
333
|
+
const teardown = async () => {
|
|
334
|
+
child?.kill();
|
|
335
|
+
await fifo?.close().catch(() => {});
|
|
336
|
+
await rm(dir, {
|
|
337
|
+
recursive: true,
|
|
338
|
+
force: true
|
|
339
|
+
}).catch(() => {});
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* A client that gives up stops us with a signal, and a gateway that died on the
|
|
343
|
+
* spot would leave its payload behind in the environment — a process holding a
|
|
344
|
+
* fifo nobody will ever write, which is a leak the environment cannot clean up
|
|
345
|
+
* for us. So the signals do what the exit path does.
|
|
346
|
+
*/
|
|
347
|
+
const onTerminate = () => {
|
|
348
|
+
teardown().catch(() => {}).then(() => process.exit(1));
|
|
349
|
+
};
|
|
350
|
+
for (const signal of [
|
|
351
|
+
"SIGTERM",
|
|
352
|
+
"SIGINT",
|
|
353
|
+
"SIGHUP"
|
|
354
|
+
]) process.on(signal, onTerminate);
|
|
355
|
+
try {
|
|
356
|
+
await run("mkfifo", [
|
|
357
|
+
"-m",
|
|
358
|
+
"600",
|
|
359
|
+
fifoIn,
|
|
360
|
+
fifoOut
|
|
361
|
+
]);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
await fail(wire, `cannot create the environment's fifos: ${errorText(err)}`);
|
|
364
|
+
await teardown();
|
|
365
|
+
abandon();
|
|
366
|
+
}
|
|
367
|
+
const connection = FifoUtf8NlineTransport.beginConnect(fifoIn, fifoOut);
|
|
368
|
+
const workerArgs = [
|
|
369
|
+
worker,
|
|
370
|
+
...passthrough,
|
|
371
|
+
`--fifo-in=${fifoIn}`,
|
|
372
|
+
`--fifo-out=${fifoOut}`
|
|
373
|
+
];
|
|
374
|
+
const workerProc = spawn(workerArgs[0], workerArgs.slice(1), { stdio: [
|
|
375
|
+
"ignore",
|
|
376
|
+
"pipe",
|
|
377
|
+
"pipe"
|
|
378
|
+
] });
|
|
379
|
+
child = workerProc;
|
|
380
|
+
forwardOutput(workerProc.stdout, wire, 1);
|
|
381
|
+
forwardOutput(workerProc.stderr, wire, 2);
|
|
382
|
+
const exited = new Promise((settle) => {
|
|
383
|
+
workerProc.once("exit", (code) => settle(code ?? 0));
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
fifo = await Promise.race([connection.transport, exited.then((code) => {
|
|
387
|
+
throw new Error(`the payload exited with code ${code} before it opened its channel`);
|
|
388
|
+
})]);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
await fail(wire, errorText(err));
|
|
391
|
+
await teardown();
|
|
392
|
+
abandon();
|
|
393
|
+
}
|
|
394
|
+
const relay = fifo;
|
|
395
|
+
relay.onMessage((line) => {
|
|
396
|
+
wire.send(line).catch(() => {});
|
|
397
|
+
});
|
|
398
|
+
wire.onMessage((line) => {
|
|
399
|
+
relay.send(line).catch(() => {});
|
|
400
|
+
});
|
|
401
|
+
wire.onClose(() => {
|
|
402
|
+
child?.kill();
|
|
403
|
+
});
|
|
404
|
+
const code = await exited;
|
|
405
|
+
await teardown();
|
|
406
|
+
await wire.close();
|
|
407
|
+
return code;
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
2
410
|
//#region src/remote/gateway-cli.ts
|
|
3
411
|
(async () => {
|
|
4
412
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-cli.js","names":[],"sources":["../../src/remote/gateway-cli.ts"],"sourcesContent":["// ── The gateway, as a program ──────────────────────────────────────────────\n//\n// A gateway is something a client runs: the first-stage command that makes a fifo\n// inside a foreign environment and relays frames between its own stdin/stdout and\n// the payload. That is this file's whole job — it always serves, and it exits with\n// the code the run ended in.\n//\n// Serving lives in ./gateway.js and starts nothing by itself. A file cannot decide\n// \"am I the program?\" from where it is: a consumer that bundles the node surface\n// into its own program carries this code along, and there the bundle *is* the\n// program — a payload would find a gateway booting on its wire. So the decision is\n// in the layout instead: importing a module never starts anything, and running this\n// one always does.\n//\n// The client names it either way: `posipaki/remote/gateway-cli.js` is a published\n// specifier, so `import.meta.resolve` answers with the built file, and a consumer\n// with a program of its own stages that instead. Nothing here works out its own\n// location — a path derived from `import.meta.url` is a guess about a layout the\n// bundler owns, and the guess is wrong the moment the module moves into a chunk.\n\nimport { GATEWAY_FAILED, runGateway } from \"./gateway.js\";\n\nvoid (async () => {\n try {\n process.exitCode = await runGateway();\n } catch (err) {\n process.stderr.write(`gateway: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = GATEWAY_FAILED;\n }\n})();\n"],"mappings":";;CAsBM,YAAY;CAChB,IAAI;EACF,QAAQ,WAAW,MAAM,WAAW;CACtC,SAAS,KAAK;EACZ,QAAQ,OAAO,MAAM,YAAY,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAAG;EACrF,QAAQ,WAAA;CACV;AACF,EAAA,CAAG"}
|
|
1
|
+
{"version":3,"file":"gateway-cli.js","names":["errorText","Lines"],"sources":["../../src/remote/stdio.ts","../../src/remote/transports/fifo.ts","../../src/remote/gateway.ts","../../src/remote/gateway-cli.ts"],"sourcesContent":["// ── stdio wire ─────────────────────────────────────────────────────────────\n//\n// A newline-delimited JSON channel over a pair of streams: the one wire every\n// way into a foreign execution context can carry. Two callers rely on it, and\n// they are two halves of the same thing:\n//\n// * the client of a spawn we make ourselves, on fds handed to the child (see\n// `fdStreams`) — private fds, so the child's own stdout stays free;\n// * a first-stage command on its own stdin/stdout, over ssh, under `sudo`, in\n// a container — where stdio is the only channel there is.\n//\n// Frames are one JSON object per line. Whatever the far end prints is carried\n// as an *output frame* (`$fd`), so it can never be mistaken for a frame of the\n// protocol itself; a failed start is carried as an `$error` frame.\n//\n// Why the client filters output frames before handing the transport to the\n// seam: `json1Channel` decodes every line it sees, and the first frame of a\n// session is the peer's `$proto`. Stripping output frames in the transport\n// keeps the protocol view clean without the seam knowing anything about them.\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { isProto, type Channel, type StringTransport } from \"./channel.js\";\nimport { VERSION, json1Channel } from \"./protocols/json1.js\";\n\n/** The stream pair a line transport talks over. */\nexport interface LineStreams {\n read: Readable;\n write: Writable;\n}\n\n/** A stream of the far end's process: its stdout or its stderr. */\nexport type OutputFd = 1 | 2;\n\n/** Where the far end's own output goes. */\nexport type OutputSink = (fd: OutputFd, data: string) => void;\n\n/** How long to wait for the far end's first protocol frame. */\nexport const HANDSHAKE_TIMEOUT_MS = 15_000;\n\nconst OUTPUT_KEY = \"$fd\";\n\nconst ERROR_KEY = \"$error\";\n\n/** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */\nexport class StdioWireError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"StdioWireError\";\n }\n}\n\n// ── output frames ──────────────────────────────────────────────────────────\n\n/** One frame of the far end's own output. */\nexport interface OutputFrame {\n fd: OutputFd;\n data: string;\n}\n\nexport function outputFrame(fd: OutputFd, data: string): string {\n return JSON.stringify({ [OUTPUT_KEY]: fd, data });\n}\n\n/** The output frame a line carries, or null when it carries something else. */\nexport function parseOutputFrame(line: string): OutputFrame | null {\n // Cheap reject first: almost every line of a session is protocol traffic.\n if (!line.startsWith('{\"$fd\"')) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n const frame = parsed as Record<string, unknown>;\n const fd = frame[OUTPUT_KEY];\n const data = frame.data;\n if (fd !== 1 && fd !== 2) return null;\n return { fd, data: typeof data === \"string\" ? data : JSON.stringify(data) };\n}\n\nexport function errorFrame(reason: string): string {\n return JSON.stringify({ [ERROR_KEY]: reason });\n}\n\n/** The reason an `$error` frame carries, or null when it is not one. */\nexport function errorReason(frame: Record<string, unknown>): string | null {\n const reason = frame[ERROR_KEY];\n return typeof reason === \"string\" ? reason : null;\n}\n\n// ── transports ─────────────────────────────────────────────────────────────\n\n/** Is this write error just a peer that already left? */\nfunction peerGone(code: unknown): boolean {\n return (\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_DESTROYED\" ||\n code === \"ERR_STREAM_WRITE_AFTER_END\" ||\n code === \"ERR_STREAM_ALREADY_FINISHED\"\n );\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A `StringTransport` over a read/write stream pair, one JSON frame per line.\n *\n * `close()` is idempotent and never throws: teardown races are normal here —\n * the peer exits, the read side ends, and a write arrives a moment later.\n */\nexport class LineTransport implements StringTransport {\n private readonly streams: LineStreams;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private pvtClosed = false;\n private pvtClosing: Promise<void> | null = null;\n\n constructor(streams: LineStreams) {\n this.streams = streams;\n const lines = readline.createInterface({ input: streams.read });\n lines.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);\n });\n lines.on(\"close\", () => {\n this.pvtOnClose?.();\n void this.close();\n });\n // A stream error is a peer that went away, not a reason to take the process\n // down with an unhandled 'error' event.\n streams.read.on(\"error\", () => {\n void this.close();\n });\n streams.write.on(\"error\", () => {\n void this.close();\n });\n }\n\n get closed(): boolean {\n return this.pvtClosed;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.pvtClosed) throw new StdioWireError(\"stdio transport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new StdioWireError(\"stdio transport: handler already set — call removeHandler()\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const previous = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return previous;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n send(frame: string): Promise<void> {\n if (this.pvtClosed) {\n return Promise.resolve();\n }\n const line = frame.endsWith(\"\\n\") ? frame : `${frame}\\n`;\n return new Promise<void>((resolve, reject) => {\n try {\n this.streams.write.write(line, (err?: Error | null) => {\n if (!err) return resolve();\n const code = (err as { code?: unknown }).code;\n // A peer that has hung up cannot be told anything; the write that\n // races its exit is not an error in this transport.\n if (peerGone(code)) {\n return resolve();\n }\n reject(new StdioWireError(`stdio transport: ${err.message}`));\n });\n } catch (err) {\n reject(new StdioWireError(`stdio transport: ${errorText(err)}`));\n }\n });\n }\n\n async close(): Promise<void> {\n this.pvtClosing ??= this.pvtClose();\n await this.pvtClosing;\n }\n\n private async pvtClose(): Promise<void> {\n await Promise.resolve();\n\n this.pvtClosed = true;\n // Best effort: a half-open stream (the peer is already gone) must not turn\n // teardown into an exception.\n try {\n this.streams.read.destroy?.();\n } catch {\n /* already gone */\n }\n try {\n if (!this.streams.write.writableEnded) this.streams.write.end();\n } catch {\n /* already gone */\n }\n }\n}\n\n/** Streams over two fds of *our own* process — the child side of an fd wire. */\nexport function fdStreams(readFd: number, writeFd: number): LineStreams {\n const read = createReadStream(\"\", { fd: readFd, encoding: \"utf-8\", autoClose: false });\n const write = createWriteStream(\"\", { fd: writeFd, encoding: \"utf-8\", autoClose: false });\n return { read, write };\n}\n\n/**\n * A transport that lifts output frames out of the stream: everything else is\n * passed through untouched, so the protocol sees frames and only frames.\n */\nexport class OutputFilter implements StringTransport {\n private readonly inner: StringTransport;\n private readonly sink: OutputSink;\n\n constructor(inner: StringTransport, sink: OutputSink) {\n this.inner = inner;\n this.sink = sink;\n }\n\n send(frame: string): void | Promise<void> {\n return this.inner.send(frame);\n }\n\n onMessage(handler: (frame: string) => void): void {\n this.inner.onMessage((line) => {\n const output = parseOutputFrame(line);\n if (output) this.sink(output.fd, output.data);\n else handler(line);\n });\n }\n\n removeHandler(): void {\n this.inner.removeHandler();\n }\n\n onClose(handler: () => void): void {\n this.inner.onClose(handler);\n }\n\n close(): Promise<void> {\n return this.inner.close();\n }\n}\n\n// ── sinks ──────────────────────────────────────────────────────────────────\n\n/** Write the far end's output to our stderr, tagged with its name. */\nexport function stderrSink(name = \"\"): OutputSink {\n const tag = name ? `[${name}] ` : \"\";\n return (fd, data) => {\n const lead = fd === 1 ? tag : `${tag}err: `;\n for (const line of data.split(\"\\n\")) {\n if (line.length > 0) process.stderr.write(`${lead}${line}\\n`);\n }\n };\n}\n\n// ── handshakes ─────────────────────────────────────────────────────────────\n\n/** The next frame that arrives, or a timeout. */\nasync function nextFrame(channel: Channel, timeoutMs: number): Promise<Record<string, unknown>> {\n return await new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new StdioWireError(`no protocol frame within ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref?.();\n channel.onMessage((frame) => {\n clearTimeout(timer);\n resolve(frame);\n });\n });\n}\n\nexport interface ClientChannelOptions {\n /** Where the far end's own output goes. Defaults to our stderr. */\n onOutput?: OutputSink;\n /** How long to wait for the peer's protocol frame. Defaults to {@link HANDSHAKE_TIMEOUT_MS}. */\n timeoutMs?: number;\n}\n\n/**\n * The client end of the wire: hand the streams to the seam (minus output\n * frames) and wait for the peer's `$proto`. A first stage reports a failed\n * start as an `$error` frame instead.\n */\nexport async function clientChannel(\n streams: LineStreams,\n opts: ClientChannelOptions = {},\n): Promise<Channel> {\n const transport = new LineTransport(streams);\n const channel = json1Channel(new OutputFilter(transport, opts.onOutput ?? stderrSink()));\n const frame = await nextFrame(channel, opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS);\n channel.removeHandler();\n\n const reason = errorReason(frame);\n if (reason) throw new StdioWireError(reason);\n if (!isProto(frame)) {\n throw new StdioWireError(`unexpected first frame: ${JSON.stringify(frame).slice(0, 120)}`);\n }\n if (frame.$proto !== VERSION) {\n throw new StdioWireError(`unsupported protocol ${String(frame.$proto)}, expected ${VERSION}`);\n }\n return channel;\n}\n\n/** The server end of the wire — a payload speaking stdio itself. */\nexport async function serverChannel(streams: LineStreams): Promise<Channel> {\n const channel = json1Channel(new LineTransport(streams));\n await channel.send({ $proto: VERSION });\n return channel;\n}\n","// ── FIFO transport ─────────────────────────────────────────────────────────\n//\n// Newline-delimited UTF-8 over named fifos. Strict handler lifecycle:\n// onMessage() throws if a handler is already set; call removeHandler() first.\n\nimport { open, type FileHandle } from \"node:fs/promises\";\nimport type { ReadStream, WriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { StringTransport } from \"../channel.js\";\n\nexport class FifoUtf8NlineTransport implements StringTransport {\n private readFd: FileHandle | null;\n private writeFd: FileHandle | null;\n private rl: readline.Interface | null;\n private rs: ReadStream | null;\n private ws: WriteStream | null;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n private closingPromise: Promise<void> | null = null;\n private pvtError: Error | null = null;\n\n private constructor(opts: { readFd?: FileHandle; writeFd?: FileHandle }) {\n this.readFd = opts.readFd ?? null;\n this.writeFd = opts.writeFd ?? null;\n\n if (this.readFd) {\n this.rs = this.readFd.createReadStream({ encoding: \"utf-8\" });\n this.rl = readline.createInterface({ input: this.rs });\n\n this.rl.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);\n });\n\n this.rl.on(\"close\", () => {\n this.pvtOnClose?.();\n this.close();\n });\n\n this.rs.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.rs = null;\n this.rl = null;\n }\n\n if (this.writeFd) {\n this.ws = this.writeFd.createWriteStream({ encoding: \"utf-8\" });\n this.ws.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.ws = null;\n }\n }\n\n // ── factories ──────────────────────────────────────────────────────────\n\n static openReaderFd(readFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd });\n }\n\n static openWriterFd(writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ writeFd });\n }\n\n static fromFds(readFd: FileHandle, writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd, writeFd });\n }\n\n // ── bidirectional connection ───────────────────────────────────────────\n\n /**\n * Start opening a bidirectional connection. Opens readPath for reading\n * in the background, returns a promise. The caller should do whatever\n * setup is needed to unblock the read (e.g. spawn a process that opens\n * readPath for writing), then await the returned transport.\n *\n * The transport itself is built only once both directions are open. A read\n * stream created while no writer exists on its fifo is handed an immediate\n * end-of-stream by some runtimes (bun), which closes a channel that has not\n * carried a byte yet — so the writes side is opened before the reader exists.\n */\n static beginConnect(\n readPath: string,\n writePath: string,\n ): { transport: Promise<FifoUtf8NlineTransport> } {\n const readFdPromise = open(readPath, \"r\");\n\n const transport = readFdPromise.then(async (readFd) => {\n const writeFd = await open(writePath, \"w\");\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n });\n\n return { transport };\n }\n\n /**\n * Open a bidirectional connection. Opens writePath for writing first\n * (unblocking the peer's read), starts reading readPath in the\n * background, then awaits both.\n *\n * Use this when you are the side that responds to the peer's beginConnect\n * (i.e. you don't need to interleave any setup between the read and write\n * opens). Built like `beginConnect`: the transport exists only after both\n * directions do.\n */\n static async connect(readPath: string, writePath: string): Promise<FifoUtf8NlineTransport> {\n const readFdPromise = open(readPath, \"r\");\n const writeFd = await open(writePath, \"w\");\n const readFd = await readFdPromise;\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n }\n\n // ── public API ─────────────────────────────────────────────────────────\n\n get canSend(): boolean {\n return this.writeFd !== null && !this.closed;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.readFd === null) throw new Error(\"FifoUtf8NlineTransport: not a reader\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"FifoUtf8NlineTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n get lastError(): Error | null {\n return this.pvtError;\n }\n\n async send(line: string): Promise<void> {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.writeFd === null) throw new Error(\"FifoUtf8NlineTransport: not a writer\");\n if (!line.endsWith(\"\\n\")) line += \"\\n\";\n await this.ws?.write(line);\n }\n\n async close(): Promise<void> {\n this.closingPromise = this.closingPromise || this.pvtClose();\n await this.closingPromise;\n this.closed = true;\n }\n\n private async pvtClose(): Promise<void> {\n // tranfser control back to caller\n // so they can capture the promise reference\n // and prevent reentry\n await Promise.resolve();\n\n if (this.rl) {\n this.rl.close();\n this.rl = null;\n }\n if (this.rs) {\n this.rs.destroy();\n this.rs = null;\n }\n if (this.ws) {\n await new Promise<void>((resolve) => {\n try {\n this.ws?.end(\"\", resolve);\n } catch {\n resolve();\n }\n });\n this.ws = null;\n }\n\n if (this.readFd) {\n try {\n await this.readFd.close();\n } catch {}\n this.readFd = null;\n }\n if (this.writeFd) {\n try {\n await this.writeFd.close();\n } catch {}\n this.writeFd = null;\n }\n }\n}\n","// ── Gateway ────────────────────────────────────────────────────────────────\n//\n// The first-stage command. Run this *inside* a foreign environment — directly,\n// under `sudo`, over ssh, in a container — and it:\n//\n// 1. creates the channel to the payload (fifo paths it makes itself, in a\n// private temp dir, so they belong to the uid that will open them),\n// 2. starts the payload — the first argument, required — hands it everything else it was\n// given (the host version among it) and relays frames between stdin/stdout,\n// 3. carries whatever the payload prints to the client as output frames\n// (`$fd`), so the payload's own stdout can never be mistaken for protocol.\n//\n// Why a gateway at all: a fifo path created on the client side is useless when the\n// payload runs as another uid (it cannot open it) or on another host (the path is\n// not there). stdio is the one channel every way into an environment gives us, so\n// the wire is stdio and the fifo stays inside the environment.\n//\n// The gateway reads nothing out of what it carries and owns no flag: the payload's path and\n// every argument after it go through verbatim, in order. The payload is the one who refuses a\n// host version it is not, and a relay that judged would be a second place for that decision.\n//\n// Directories and processes are cleaned up on every exit path, which is also why\n// the client never sees a transport being closed twice.\n//\n// It imports nothing but node builtins and this package, so the payload behind it\n// can be anything the runtime there can start.\n\nimport { execFile, spawn } from \"node:child_process\";\nimport type { ChildProcess } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { errorFrame, outputFrame } from \"./stdio.js\";\nimport type { LineTransport, OutputFd } from \"./stdio.js\";\nimport { LineTransport as Lines } from \"./stdio.js\";\nimport { FifoUtf8NlineTransport } from \"./transports/fifo.js\";\n\nconst run = promisify(execFile);\n\n/** Exit code for \"this environment could not be set up\". */\nexport const GATEWAY_FAILED = 1;\n\nexport interface GatewayBoot {\n /**\n * The program to start for the payload: the first word of the command the client composed —\n * a runtime, or a program that runs itself. Required, and first, and the gateway has no\n * opinion of its own about which it is.\n */\n worker: string;\n /**\n * Everything after the payload, carried to it **as is** and in order: the consumer's own\n * arguments, and posipaki's own facts among them (the host version travels this way). The\n * gateway reads nothing by name and owns no flag, so nothing of the caller's is interpreted,\n * eaten, re-ordered or re-spelled — and the next fact added to that line needs no new\n * vocabulary here.\n */\n passthrough: string[];\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A label for the gateway's own temp directory: the program this environment was made for.\n * It is the gateway's business alone — the payload is not told which label it is.\n */\nfunction label(worker: string): string {\n const name = basename(worker).replace(/[^A-Za-z0-9.+@_-]/g, \"-\").slice(0, 60);\n return name === \"\" ? \"payload\" : name;\n}\n\n/**\n * The argv that boots this gateway — built by the client, read by\n * {@link gatewayBoot}, so the two halves cannot drift apart.\n *\n * The payload comes first and its arguments after it, so a positional argument of the\n * payload's own can never be mistaken for the payload.\n */\nexport function gatewayArgs(boot: GatewayBoot): string[] {\n return [boot.worker, ...boot.passthrough];\n}\n\n/** The boot a gateway was started with, or the reason it cannot start. */\nexport function gatewayBoot(argv: string[]): GatewayBoot {\n const [worker, ...passthrough] = argv;\n if (worker === undefined || worker.startsWith(\"--\")) {\n throw new Error(\"no <payload>: the gateway relays to a payload, named as its first argument\");\n }\n return { passthrough, worker };\n}\n\n/**\n * End a start that failed. The reason has already gone out on the wire, and\n * returning is not enough: the `open()` on a fifo no payload will ever write sits\n * in the threadpool forever, so the loop never drains and the gateway would linger\n * with a dead payload on its books.\n */\nfunction abandon(): never {\n process.exit(GATEWAY_FAILED);\n}\n\n/** Carry a failed start to the client, then let the caller exit non-zero. */\nasync function fail(wire: LineTransport, reason: string): Promise<void> {\n process.stderr.write(`gateway: ${reason}\\n`);\n try {\n await wire.send(errorFrame(reason));\n } catch {\n /* the client is gone too — the exit code still says what happened */\n }\n}\n\n/** Copy everything the payload prints onto the wire as output frames. */\nfunction forwardOutput(stream: NodeJS.ReadableStream, wire: LineTransport, fd: OutputFd): void {\n stream.setEncoding(\"utf-8\");\n stream.on(\"data\", (chunk: string) => {\n void wire.send(outputFrame(fd, chunk)).catch(() => {\n /* the client hung up: the payload's exit is what matters now */\n });\n });\n}\n\n/**\n * Serve one environment over stdin/stdout. Returns the exit code; the caller\n * turns it into `process.exitCode`.\n */\nexport async function runGateway(argv: string[] = process.argv.slice(2)): Promise<number> {\n const wire = new Lines({ read: process.stdin, write: process.stdout });\n\n let booted: GatewayBoot;\n try {\n booted = gatewayBoot(argv);\n } catch (err) {\n // Nothing has been started yet, so there is nothing to tear down — but the\n // client still deserves the reason rather than a channel that closes on it,\n // and the loop can outlive us: an open stdin nobody will close keeps the\n // process alive, so this is an exit, not a return.\n await fail(wire, errorText(err));\n abandon();\n }\n const { passthrough, worker } = booted;\n\n const dir = await mkdtemp(join(tmpdir(), `posipaki-${label(worker)}-`));\n const fifoIn = join(dir, \"in\"); // the payload writes, we read\n const fifoOut = join(dir, \"out\"); // we write, the payload reads\n\n let child: ChildProcess | null = null;\n let fifo: FifoUtf8NlineTransport | null = null;\n\n /**\n * Everything this gateway owns, undone. All of it is idempotent, because the\n * exit path and the signal path can both arrive.\n */\n const teardown = async (): Promise<void> => {\n child?.kill();\n await fifo?.close().catch(() => {});\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n /**\n * A client that gives up stops us with a signal, and a gateway that died on the\n * spot would leave its payload behind in the environment — a process holding a\n * fifo nobody will ever write, which is a leak the environment cannot clean up\n * for us. So the signals do what the exit path does.\n */\n const onTerminate = () => {\n void teardown()\n .catch(() => {})\n .then(() => process.exit(GATEWAY_FAILED));\n };\n for (const signal of [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const) process.on(signal, onTerminate);\n\n try {\n await run(\"mkfifo\", [\"-m\", \"600\", fifoIn, fifoOut]);\n } catch (err) {\n await fail(wire, `cannot create the environment's fifos: ${errorText(err)}`);\n await teardown();\n abandon();\n }\n\n // Open the read side in the background, then start the payload that unblocks\n // it — the order the fifo handshake requires.\n const connection = FifoUtf8NlineTransport.beginConnect(fifoIn, fifoOut);\n const workerArgs = [worker, ...passthrough, `--fifo-in=${fifoIn}`, `--fifo-out=${fifoOut}`];\n // Whatever the client said starts the payload, run as it said: a runtime with a script of\n // ours, or a program of the caller's own. The gateway does not know which it is, and does\n // not choose a runtime on the far end's behalf.\n const workerProc = spawn(workerArgs[0]!, workerArgs.slice(1), {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child = workerProc;\n forwardOutput(workerProc.stdout, wire, 1);\n forwardOutput(workerProc.stderr, wire, 2);\n\n const exited = new Promise<number>((settle) => {\n // A payload killed by a signal (the client hung up, and we killed it) has no\n // code — for us that is a clean end, not a failure.\n workerProc.once(\"exit\", (code) => settle(code ?? 0));\n });\n\n try {\n fifo = await Promise.race([\n connection.transport,\n exited.then((code) => {\n throw new Error(`the payload exited with code ${code} before it opened its channel`);\n }),\n ]);\n } catch (err) {\n await fail(wire, errorText(err));\n await teardown();\n abandon();\n }\n\n // Relay: the protocol is the client's and the payload's business, not ours.\n const relay = fifo;\n relay.onMessage((line) => {\n void wire.send(line).catch(() => {\n /* client gone */\n });\n });\n wire.onMessage((line) => {\n void relay.send(line).catch(() => {\n /* the payload is gone; its exit path cleans up */\n });\n });\n wire.onClose(() => {\n child?.kill();\n });\n\n const code = await exited;\n await teardown();\n await wire.close();\n return code;\n}\n","// ── The gateway, as a program ──────────────────────────────────────────────\n//\n// A gateway is something a client runs: the first-stage command that makes a fifo\n// inside a foreign environment and relays frames between its own stdin/stdout and\n// the payload. That is this file's whole job — it always serves, and it exits with\n// the code the run ended in.\n//\n// Serving lives in ./gateway.js and starts nothing by itself. A file cannot decide\n// \"am I the program?\" from where it is: a consumer that bundles the node surface\n// into its own program carries this code along, and there the bundle *is* the\n// program — a payload would find a gateway booting on its wire. So the decision is\n// in the layout instead: importing a module never starts anything, and running this\n// one always does.\n//\n// The client names it either way: `posipaki/remote/gateway-cli.js` is a published\n// specifier, so `import.meta.resolve` answers with the built file, and a consumer\n// with a program of its own stages that instead. Nothing here works out its own\n// location — a path derived from `import.meta.url` is a guess about a layout the\n// bundler owns, and the guess is wrong the moment the module moves into a chunk.\n\nimport { GATEWAY_FAILED, runGateway } from \"./gateway.js\";\n\nvoid (async () => {\n try {\n process.exitCode = await runGateway();\n } catch (err) {\n process.stderr.write(`gateway: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = GATEWAY_FAILED;\n }\n})();\n"],"mappings":";;;;;;;AAyCA,MAAM,aAAa;AAEnB,MAAM,YAAY;;AAGlB,IAAa,iBAAb,cAAoC,MAAM;CACxC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAUA,SAAgB,YAAY,IAAc,MAAsB;CAC9D,OAAO,KAAK,UAAU;GAAG,aAAa;EAAI;CAAK,CAAC;AAClD;AAoBA,SAAgB,WAAW,QAAwB;CACjD,OAAO,KAAK,UAAU,GAAG,YAAY,OAAO,CAAC;AAC/C;;AAWA,SAAS,SAAS,MAAwB;CACxC,OACE,SAAS,WACT,SAAS,0BACT,SAAS,gCACT,SAAS;AAEb;AAEA,SAASA,YAAU,KAAsB;CACvC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;;;;;;;AAQA,IAAa,gBAAb,MAAsD;CAOpD,YAAY,SAAsB;EALsB,KAAA,eAAA;EACd,KAAA,aAAA;EACtB,KAAA,YAAA;EACuB,KAAA,aAAA;EAGzC,KAAK,UAAU;EACf,MAAM,QAAQ,SAAS,gBAAgB,EAAE,OAAO,QAAQ,KAAK,CAAC;EAC9D,MAAM,GAAG,SAAS,SAAS;GACzB,IAAI,KAAK,gBAAgB,CAAC,KAAK,WAAW,KAAK,aAAa,IAAI;EAClE,CAAC;EACD,MAAM,GAAG,eAAe;GACtB,KAAK,aAAa;GAClB,KAAU,MAAM;EAClB,CAAC;EAGD,QAAQ,KAAK,GAAG,eAAe;GAC7B,KAAU,MAAM;EAClB,CAAC;EACD,QAAQ,MAAM,GAAG,eAAe;GAC9B,KAAU,MAAM;EAClB,CAAC;CACH;CAEA,IAAI,SAAkB;EACpB,OAAO,KAAK;CACd;CAEA,IAAI,aAAsB;EACxB,OAAO,KAAK,iBAAiB;CAC/B;CAEA,UAAU,SAAuC;EAC/C,IAAI,KAAK,WAAW,MAAM,IAAI,eAAe,yBAAyB;EACtE,IAAI,KAAK,iBAAiB,MACxB,MAAM,IAAI,eAAe,6DAA6D;EAExF,KAAK,eAAe;CACtB;CAEA,gBAAiD;EAC/C,MAAM,WAAW,KAAK;EACtB,KAAK,eAAe;EACpB,OAAO;CACT;CAEA,QAAQ,SAA2B;EACjC,KAAK,aAAa;CACpB;CAEA,KAAK,OAA8B;EACjC,IAAI,KAAK,WACP,OAAO,QAAQ,QAAQ;EAEzB,MAAM,OAAO,MAAM,SAAS,IAAI,IAAI,QAAQ,GAAG,MAAM;EACrD,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,IAAI;IACF,KAAK,QAAQ,MAAM,MAAM,OAAO,QAAuB;KACrD,IAAI,CAAC,KAAK,OAAO,QAAQ;KACzB,MAAM,OAAQ,IAA2B;KAGzC,IAAI,SAAS,IAAI,GACf,OAAO,QAAQ;KAEjB,OAAO,IAAI,eAAe,oBAAoB,IAAI,SAAS,CAAC;IAC9D,CAAC;GACH,SAAS,KAAK;IACZ,OAAO,IAAI,eAAe,oBAAoBA,YAAU,GAAG,GAAG,CAAC;GACjE;EACF,CAAC;CACH;CAEA,MAAM,QAAuB;EAC3B,KAAK,eAAe,KAAK,SAAS;EAClC,MAAM,KAAK;CACb;CAEA,MAAc,WAA0B;EACtC,MAAM,QAAQ,QAAQ;EAEtB,KAAK,YAAY;EAGjB,IAAI;GACF,KAAK,QAAQ,KAAK,UAAU;EAC9B,QAAQ,CAER;EACA,IAAI;GACF,IAAI,CAAC,KAAK,QAAQ,MAAM,eAAe,KAAK,QAAQ,MAAM,IAAI;EAChE,QAAQ,CAER;CACF;AACF;;;AC3MA,IAAa,yBAAb,MAAa,uBAAkD;CAY7D,YAAoB,MAAqD;EANjB,KAAA,eAAA;EACd,KAAA,aAAA;EACzB,KAAA,SAAA;EAC8B,KAAA,iBAAA;EACd,KAAA,WAAA;EAG/B,KAAK,SAAS,KAAK,UAAU;EAC7B,KAAK,UAAU,KAAK,WAAW;EAE/B,IAAI,KAAK,QAAQ;GACf,KAAK,KAAK,KAAK,OAAO,iBAAiB,EAAE,UAAU,QAAQ,CAAC;GAC5D,KAAK,KAAK,SAAS,gBAAgB,EAAE,OAAO,KAAK,GAAG,CAAC;GAErD,KAAK,GAAG,GAAG,SAAS,SAAS;IAC3B,IAAI,KAAK,gBAAgB,CAAC,KAAK,QAAQ,KAAK,aAAa,IAAI;GAC/D,CAAC;GAED,KAAK,GAAG,GAAG,eAAe;IACxB,KAAK,aAAa;IAClB,KAAK,MAAM;GACb,CAAC;GAED,KAAK,GAAG,GAAG,UAAU,QAAe;IAClC,KAAK,WAAW;IAChB,KAAK,MAAM;GACb,CAAC;EACH,OAAO;GACL,KAAK,KAAK;GACV,KAAK,KAAK;EACZ;EAEA,IAAI,KAAK,SAAS;GAChB,KAAK,KAAK,KAAK,QAAQ,kBAAkB,EAAE,UAAU,QAAQ,CAAC;GAC9D,KAAK,GAAG,GAAG,UAAU,QAAe;IAClC,KAAK,WAAW;IAChB,KAAK,MAAM;GACb,CAAC;EACH,OACE,KAAK,KAAK;CAEd;CAIA,OAAO,aAAa,QAA4C;EAC9D,OAAO,IAAI,uBAAuB,EAAE,OAAO,CAAC;CAC9C;CAEA,OAAO,aAAa,SAA6C;EAC/D,OAAO,IAAI,uBAAuB,EAAE,QAAQ,CAAC;CAC/C;CAEA,OAAO,QAAQ,QAAoB,SAA6C;EAC9E,OAAO,IAAI,uBAAuB;GAAE;GAAQ;EAAQ,CAAC;CACvD;;;;;;;;;;;;CAeA,OAAO,aACL,UACA,WACgD;EAQhD,OAAO,EAAE,WAPa,KAAK,UAAU,GAEP,CAAC,CAAC,KAAK,OAAO,WAAW;GACrD,MAAM,UAAU,MAAM,KAAK,WAAW,GAAG;GACzC,OAAO,uBAAuB,QAAQ,QAAQ,OAAO;EACvD,CAEiB,EAAE;CACrB;;;;;;;;;;;CAYA,aAAa,QAAQ,UAAkB,WAAoD;EACzF,MAAM,gBAAgB,KAAK,UAAU,GAAG;EACxC,MAAM,UAAU,MAAM,KAAK,WAAW,GAAG;EACzC,MAAM,SAAS,MAAM;EACrB,OAAO,uBAAuB,QAAQ,QAAQ,OAAO;CACvD;CAIA,IAAI,UAAmB;EACrB,OAAO,KAAK,YAAY,QAAQ,CAAC,KAAK;CACxC;CAEA,UAAU,SAAuC;EAC/C,IAAI,KAAK,QAAQ,MAAM,IAAI,MAAM,gCAAgC;EACjE,IAAI,KAAK,WAAW,MAAM,MAAM,IAAI,MAAM,sCAAsC;EAChF,IAAI,KAAK,iBAAiB,MACxB,MAAM,IAAI,MAAM,0EAA0E;EAE5F,KAAK,eAAe;CACtB;CAEA,gBAAiD;EAC/C,MAAM,OAAO,KAAK;EAClB,KAAK,eAAe;EACpB,OAAO;CACT;CAEA,QAAQ,SAA2B;EACjC,KAAK,aAAa;CACpB;CAEA,IAAI,aAAsB;EACxB,OAAO,KAAK,iBAAiB;CAC/B;CAEA,IAAI,YAA0B;EAC5B,OAAO,KAAK;CACd;CAEA,MAAM,KAAK,MAA6B;EACtC,IAAI,KAAK,QAAQ,MAAM,IAAI,MAAM,gCAAgC;EACjE,IAAI,KAAK,YAAY,MAAM,MAAM,IAAI,MAAM,sCAAsC;EACjF,IAAI,CAAC,KAAK,SAAS,IAAI,GAAG,QAAQ;EAClC,MAAM,KAAK,IAAI,MAAM,IAAI;CAC3B;CAEA,MAAM,QAAuB;EAC3B,KAAK,iBAAiB,KAAK,kBAAkB,KAAK,SAAS;EAC3D,MAAM,KAAK;EACX,KAAK,SAAS;CAChB;CAEA,MAAc,WAA0B;EAItC,MAAM,QAAQ,QAAQ;EAEtB,IAAI,KAAK,IAAI;GACX,KAAK,GAAG,MAAM;GACd,KAAK,KAAK;EACZ;EACA,IAAI,KAAK,IAAI;GACX,KAAK,GAAG,QAAQ;GAChB,KAAK,KAAK;EACZ;EACA,IAAI,KAAK,IAAI;GACX,MAAM,IAAI,SAAe,YAAY;IACnC,IAAI;KACF,KAAK,IAAI,IAAI,IAAI,OAAO;IAC1B,QAAQ;KACN,QAAQ;IACV;GACF,CAAC;GACD,KAAK,KAAK;EACZ;EAEA,IAAI,KAAK,QAAQ;GACf,IAAI;IACF,MAAM,KAAK,OAAO,MAAM;GAC1B,QAAQ,CAAC;GACT,KAAK,SAAS;EAChB;EACA,IAAI,KAAK,SAAS;GAChB,IAAI;IACF,MAAM,KAAK,QAAQ,MAAM;GAC3B,QAAQ,CAAC;GACT,KAAK,UAAU;EACjB;CACF;AACF;;;ACnKA,MAAM,MAAM,UAAU,QAAQ;AAsB9B,SAAS,UAAU,KAAsB;CACvC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;;;;;AAMA,SAAS,MAAM,QAAwB;CACrC,MAAM,OAAO,SAAS,MAAM,CAAC,CAAC,QAAQ,sBAAsB,GAAG,CAAC,CAAC,MAAM,GAAG,EAAE;CAC5E,OAAO,SAAS,KAAK,YAAY;AACnC;;AAcA,SAAgB,YAAY,MAA6B;CACvD,MAAM,CAAC,QAAQ,GAAG,eAAe;CACjC,IAAI,WAAW,KAAA,KAAa,OAAO,WAAW,IAAI,GAChD,MAAM,IAAI,MAAM,4EAA4E;CAE9F,OAAO;EAAE;EAAa;CAAO;AAC/B;;;;;;;AAQA,SAAS,UAAiB;CACxB,QAAQ,KAAA,CAAmB;AAC7B;;AAGA,eAAe,KAAK,MAAqB,QAA+B;CACtE,QAAQ,OAAO,MAAM,YAAY,OAAO,GAAG;CAC3C,IAAI;EACF,MAAM,KAAK,KAAK,WAAW,MAAM,CAAC;CACpC,QAAQ,CAER;AACF;;AAGA,SAAS,cAAc,QAA+B,MAAqB,IAAoB;CAC7F,OAAO,YAAY,OAAO;CAC1B,OAAO,GAAG,SAAS,UAAkB;EACnC,KAAU,KAAK,YAAY,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,CAEnD,CAAC;CACH,CAAC;AACH;;;;;AAMA,eAAsB,WAAW,OAAiB,QAAQ,KAAK,MAAM,CAAC,GAAoB;CACxF,MAAM,OAAO,IAAIC,cAAM;EAAE,MAAM,QAAQ;EAAO,OAAO,QAAQ;CAAO,CAAC;CAErE,IAAI;CACJ,IAAI;EACF,SAAS,YAAY,IAAI;CAC3B,SAAS,KAAK;EAKZ,MAAM,KAAK,MAAM,UAAU,GAAG,CAAC;EAC/B,QAAQ;CACV;CACA,MAAM,EAAE,aAAa,WAAW;CAEhC,MAAM,MAAM,MAAM,QAAQ,KAAK,OAAO,GAAG,YAAY,MAAM,MAAM,EAAE,EAAE,CAAC;CACtE,MAAM,SAAS,KAAK,KAAK,IAAI;CAC7B,MAAM,UAAU,KAAK,KAAK,KAAK;CAE/B,IAAI,QAA6B;CACjC,IAAI,OAAsC;;;;;CAM1C,MAAM,WAAW,YAA2B;EAC1C,OAAO,KAAK;EACZ,MAAM,MAAM,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;EAClC,MAAM,GAAG,KAAK;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CAChE;;;;;;;CAQA,MAAM,oBAAoB;EACxB,SAAc,CAAC,CACZ,YAAY,CAAC,CAAC,CAAC,CACf,WAAW,QAAQ,KAAA,CAAmB,CAAC;CAC5C;CACA,KAAK,MAAM,UAAU;EAAC;EAAW;EAAU;CAAQ,GAAY,QAAQ,GAAG,QAAQ,WAAW;CAE7F,IAAI;EACF,MAAM,IAAI,UAAU;GAAC;GAAM;GAAO;GAAQ;EAAO,CAAC;CACpD,SAAS,KAAK;EACZ,MAAM,KAAK,MAAM,0CAA0C,UAAU,GAAG,GAAG;EAC3E,MAAM,SAAS;EACf,QAAQ;CACV;CAIA,MAAM,aAAa,uBAAuB,aAAa,QAAQ,OAAO;CACtE,MAAM,aAAa;EAAC;EAAQ,GAAG;EAAa,aAAa;EAAU,cAAc;CAAS;CAI1F,MAAM,aAAa,MAAM,WAAW,IAAK,WAAW,MAAM,CAAC,GAAG,EAC5D,OAAO;EAAC;EAAU;EAAQ;CAAM,EAClC,CAAC;CACD,QAAQ;CACR,cAAc,WAAW,QAAQ,MAAM,CAAC;CACxC,cAAc,WAAW,QAAQ,MAAM,CAAC;CAExC,MAAM,SAAS,IAAI,SAAiB,WAAW;EAG7C,WAAW,KAAK,SAAS,SAAS,OAAO,QAAQ,CAAC,CAAC;CACrD,CAAC;CAED,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CACxB,WAAW,WACX,OAAO,MAAM,SAAS;GACpB,MAAM,IAAI,MAAM,gCAAgC,KAAK,8BAA8B;EACrF,CAAC,CACH,CAAC;CACH,SAAS,KAAK;EACZ,MAAM,KAAK,MAAM,UAAU,GAAG,CAAC;EAC/B,MAAM,SAAS;EACf,QAAQ;CACV;CAGA,MAAM,QAAQ;CACd,MAAM,WAAW,SAAS;EACxB,KAAU,KAAK,IAAI,CAAC,CAAC,YAAY,CAEjC,CAAC;CACH,CAAC;CACD,KAAK,WAAW,SAAS;EACvB,MAAW,KAAK,IAAI,CAAC,CAAC,YAAY,CAElC,CAAC;CACH,CAAC;CACD,KAAK,cAAc;EACjB,OAAO,KAAK;CACd,CAAC;CAED,MAAM,OAAO,MAAM;CACnB,MAAM,SAAS;CACf,MAAM,KAAK,MAAM;CACjB,OAAO;AACT;;;CCpNM,YAAY;CAChB,IAAI;EACF,QAAQ,WAAW,MAAM,WAAW;CACtC,SAAS,KAAK;EACZ,QAAQ,OAAO,MAAM,YAAY,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAAG;EACrF,QAAQ,WAAA;CACV;AACF,EAAA,CAAG"}
|
package/dist/remote/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as Message } from "../types-CsE8xK3a.js";
|
|
2
|
-
import { l as ActorDefinition, v as ReflectionOptions } from "../hooks-
|
|
3
|
-
import { a as isExit, c as isProto, i as StringTransport, l as isState, n as remoteClient, o as isInit, r as Channel, s as isMsg, t as ClientSpawner } from "../client-
|
|
2
|
+
import { l as ActorDefinition, v as ReflectionOptions } from "../hooks-Dj59y_jN.js";
|
|
3
|
+
import { a as isExit, c as isProto, i as StringTransport, l as isState, n as remoteClient, o as isInit, r as Channel, s as isMsg, t as ClientSpawner } from "../client-C4LZE65N.js";
|
|
4
4
|
//#region src/remote/server.d.ts
|
|
5
5
|
type Spawner = () => Promise<Channel>;
|
|
6
6
|
declare function makeSender(fromName: string, parentName: string | null, parentId: symbol | null): {
|
package/dist/remote/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { n as makeSender, r as serveRemoteActor, t as remoteClient } from "../client-D23TlXPf.js";
|
|
1
|
+
import { a as remoteClient, c as isExit, d as isProto, f as isState, i as json1Channel, l as isInit, n as decode, o as makeSender, r as encode, s as serveRemoteActor, t as VERSION, u as isMsg } from "../json1-DzPx9oX3.js";
|
|
3
2
|
//#region src/remote/transports/websocket.ts
|
|
4
3
|
/** WHATWG WebSocket readyState: OPEN. */
|
|
5
4
|
const WS_OPEN = 1;
|
package/dist/remote/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/remote/transports/websocket.ts","../../src/remote/transports/worker.ts","../../src/remote/spawners/ws-client.ts","../../src/remote/spawners/ws-server.ts","../../src/remote/spawners/worker-client.ts","../../src/remote/spawners/worker-self.ts"],"sourcesContent":["/* eslint-disable unicorn/prefer-add-event-listener */\n// ── WebSocket transport ─────────────────────────────────────────────────────────\n//\n// Message-boundary transport over a WebSocket. The socket's native framing means\n// one `send` is one frame — there is no delimiter to add or strip. It moves\n// encoded frames (strings); json1Channel sits above it and handles JSON.\n\nimport type { StringTransport } from \"../channel.js\";\n\n/**\n * The subset of a WebSocket the transport needs. The WHATWG client WebSocket\n * (Node, Bun, browsers) satisfies this directly; a Bun server socket is adapted\n * by the consumer (see spawners/ws-server.ts).\n */\nexport interface WebSocketLike {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly readyState: number;\n onmessage: ((event: { data: unknown }) => void) | null;\n onclose: (() => void) | null;\n}\n\n/** WHATWG WebSocket readyState: OPEN. */\nexport const WS_OPEN = 1;\n\nexport class WebSocketTransport implements StringTransport {\n private ws: WebSocketLike;\n private pvtOnMessage: ((frame: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n\n constructor(ws: WebSocketLike) {\n this.ws = ws;\n ws.onmessage = (event) => {\n if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(String(event.data));\n };\n ws.onclose = () => {\n this.closed = true;\n this.pvtOnClose?.();\n };\n }\n\n get canSend(): boolean {\n return !this.closed && this.ws.readyState === WS_OPEN;\n }\n\n onMessage(handler: (frame: string) => void): void {\n if (this.closed) throw new Error(\"WebSocketTransport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"WebSocketTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((frame: string) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n async send(frame: string): Promise<void> {\n if (this.closed) throw new Error(\"WebSocketTransport: closed\");\n if (this.ws.readyState !== WS_OPEN) throw new Error(\"WebSocketTransport: not open\");\n this.ws.send(frame);\n }\n\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n try {\n this.ws.close();\n } catch {\n // already closing / closed — ignore\n }\n }\n}\n","/* eslint-disable unicorn/prefer-add-event-listener, unicorn/require-post-message-target-origin */\n// ── Worker transport ────────────────────────────────────────────────────────────\n//\n// Moves frame objects directly over postMessage. postMessage structured-clones\n// the frame object, so there is no JSON encode/decode — this transport implements\n// Channel directly (not StringTransport). Structured clone is the worker's\n// native encoding, named by WORKER_VERSION.\n\nimport type { Channel } from \"../channel.js\";\n\n/** The worker encoding: native structured clone (no JSON). */\nexport const WORKER_VERSION = \"clone.v1\";\n\n/**\n * The subset of a worker the transport needs. The main-thread `Worker` and the\n * worker-global `self` both satisfy it; `terminate` is present only on the\n * main-thread side.\n */\nexport interface WorkerLike {\n postMessage(data: unknown): void;\n onmessage: ((event: { data: unknown }) => void) | null;\n onerror?: ((event: unknown) => void) | null;\n terminate?(): void;\n}\n\nexport class WorkerTransport implements Channel {\n private worker: WorkerLike;\n private pvtOnMessage: ((frame: Record<string, unknown>) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n\n constructor(worker: WorkerLike) {\n this.worker = worker;\n worker.onmessage = (event) => {\n if (this.pvtOnMessage && !this.closed) {\n this.pvtOnMessage(event.data as Record<string, unknown>);\n }\n };\n // An uncaught error in the worker is the closest thing to an abrupt drop.\n if (\"onerror\" in worker) {\n worker.onerror = () => {\n this.closed = true;\n this.pvtOnClose?.();\n };\n }\n }\n\n get canSend(): boolean {\n return !this.closed;\n }\n\n onMessage(handler: (frame: Record<string, unknown>) => void): void {\n if (this.closed) throw new Error(\"WorkerTransport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"WorkerTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((frame: Record<string, unknown>) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n async send(frame: Record<string, unknown>): Promise<void> {\n if (this.closed) throw new Error(\"WorkerTransport: closed\");\n this.worker.postMessage(frame);\n }\n\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n this.worker.terminate?.();\n }\n}\n","/* eslint-disable unicorn/prefer-add-event-listener */\n// ── WebSocket client spawner ────────────────────────────────────────────────────\n//\n// Environment-specific: connects to a `ws://` URL with the WHATWG WebSocket,\n// wraps it in a WebSocketTransport, and returns a frame Channel with the $proto\n// handshake already validated.\n\nimport { WebSocketTransport } from \"../transports/websocket.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport { isProto } from \"../channel.js\";\nimport type { ClientSpawner } from \"../client.js\";\n\nexport type WebSocketCtor = new (url: string) => {\n readonly readyState: number;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n onopen: (() => void) | null;\n onerror: ((err: unknown) => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n onclose: (() => void) | null;\n};\n\nconst defaultWebSocket = (globalThis as { WebSocket?: WebSocketCtor }).WebSocket;\n\nfunction openWebSocket(ws: { readonly readyState: number; onopen: (() => void) | null; onerror: ((err: unknown) => void) | null }): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState === 1 /* OPEN */) return resolve();\n ws.onopen = () => resolve();\n ws.onerror = (err) => reject(new Error(`websocket failed to open: ${String(err)}`));\n });\n}\n\nexport function wsClientSpawner<Args = unknown>(\n url: string | ((args: Args) => string),\n WebSocketImpl: WebSocketCtor = defaultWebSocket as WebSocketCtor,\n): ClientSpawner<Args> {\n return async (args) => {\n const target = typeof url === \"function\" ? url(args) : url;\n const ws = new WebSocketImpl(target);\n const transport = new WebSocketTransport(ws);\n const channel = json1Channel(transport);\n\n // Set the $proto waiter before awaiting open so a fast server's handshake\n // frame is not dropped.\n const protoPromise = new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n await openWebSocket(ws);\n const protoFrame = await protoPromise;\n channel.removeHandler();\n\n if (!isProto(protoFrame) || protoFrame.$proto !== VERSION) {\n throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);\n }\n return channel;\n };\n}\n","// ── WebSocket server spawner ────────────────────────────────────────────────────\n//\n// Environment-specific: serves an already-upgraded WebSocket connection. The\n// consumer's HTTP server does the upgrade and hands us the socket; we wrap it in\n// a WebSocketTransport, send $proto, and return the frame Channel.\n\nimport { WebSocketTransport, type WebSocketLike } from \"../transports/websocket.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport type { Spawner } from \"../server.js\";\n\nexport function wsServerSpawner(ws: WebSocketLike): Spawner {\n return async () => {\n const channel = json1Channel(new WebSocketTransport(ws));\n await channel.send({ $proto: VERSION });\n return channel;\n };\n}\n\n/**\n * A minimal Bun `ServerWebSocket` — `send`/`close`/`readyState` plus the data\n * slot the server uses to stash per-connection state. Bun delivers message and\n * close events through the `Bun.serve` websocket config callbacks, not through\n * properties on the socket, so the consumer routes them onto the adapter.\n */\nexport interface BunServerWebSocketLike {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly readyState: number;\n data?: unknown;\n}\n\n/**\n * Adapt a Bun server socket to the `WebSocketLike` the transport consumes. The\n * returned adapter owns settable `onmessage`/`onclose` slots; the consumer\n * routes Bun's `message`/`close` config callbacks into them, e.g.:\n *\n * ```ts\n * Bun.serve({\n * websocket: {\n * open(ws) {\n * const adapter = bunServerWebSocket(ws);\n * ws.data = adapter;\n * serveRemoteActor(actor, wsServerSpawner(adapter));\n * },\n * message(ws, msg) { (ws.data as WebSocketLike).onmessage?.({ data: msg }); },\n * close(ws) { (ws.data as WebSocketLike).onclose?.(); },\n * },\n * });\n * ```\n */\nexport function bunServerWebSocket(ws: BunServerWebSocketLike): WebSocketLike {\n return {\n send: (data) => ws.send(data),\n close: (code, reason) => ws.close(code, reason),\n readyState: ws.readyState,\n onmessage: null,\n onclose: null,\n };\n}\n","// ── Worker client spawner (command) ─────────────────────────────────────────────\n//\n// Environment-specific: spawns a Worker with the WHATWG `new Worker(url)` API,\n// wraps it in a WorkerTransport, and returns a frame Channel with the $proto\n// handshake already validated.\n\nimport { WorkerTransport, WORKER_VERSION, type WorkerLike } from \"../transports/worker.js\";\nimport { isProto } from \"../channel.js\";\nimport type { ClientSpawner } from \"../client.js\";\n\nexport type WorkerCtor = new (url: string) => WorkerLike;\n\nconst defaultWorker = (globalThis as { Worker?: WorkerCtor }).Worker;\n\nexport function workerClientSpawner<Args = unknown>(\n url: string | ((args: Args) => string),\n WorkerImpl: WorkerCtor = defaultWorker as WorkerCtor,\n): ClientSpawner<Args> {\n return async (args) => {\n const target = typeof url === \"function\" ? url(args) : url;\n const channel = new WorkerTransport(new WorkerImpl(target));\n\n const protoFrame = await new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n channel.removeHandler();\n\n if (!isProto(protoFrame) || protoFrame.$proto !== WORKER_VERSION) {\n throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);\n }\n return channel;\n };\n}\n","// ── Worker server spawner (self) ────────────────────────────────────────────────\n//\n// Environment-specific: serves the worker-global scope (`self`) as a frame\n// Channel with the $proto handshake already sent. The worker entry script calls\n// `serveRemoteActor(actor, workerSelfSpawner())`.\n\nimport { WorkerTransport, WORKER_VERSION, type WorkerLike } from \"../transports/worker.js\";\nimport type { Spawner } from \"../server.js\";\n\nexport function workerSelfSpawner(selfScope: WorkerLike = globalThis as unknown as WorkerLike): Spawner {\n return async () => {\n const channel = new WorkerTransport(selfScope);\n await channel.send({ $proto: WORKER_VERSION });\n return channel;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/remote/transports/websocket.ts","../../src/remote/transports/worker.ts","../../src/remote/spawners/ws-client.ts","../../src/remote/spawners/ws-server.ts","../../src/remote/spawners/worker-client.ts","../../src/remote/spawners/worker-self.ts"],"sourcesContent":["/* eslint-disable unicorn/prefer-add-event-listener */\n// ── WebSocket transport ─────────────────────────────────────────────────────────\n//\n// Message-boundary transport over a WebSocket. The socket's native framing means\n// one `send` is one frame — there is no delimiter to add or strip. It moves\n// encoded frames (strings); json1Channel sits above it and handles JSON.\n\nimport type { StringTransport } from \"../channel.js\";\n\n/**\n * The subset of a WebSocket the transport needs. The WHATWG client WebSocket\n * (Node, Bun, browsers) satisfies this directly; a Bun server socket is adapted\n * by the consumer (see spawners/ws-server.ts).\n */\nexport interface WebSocketLike {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly readyState: number;\n onmessage: ((event: { data: unknown }) => void) | null;\n onclose: (() => void) | null;\n}\n\n/** WHATWG WebSocket readyState: OPEN. */\nexport const WS_OPEN = 1;\n\nexport class WebSocketTransport implements StringTransport {\n private ws: WebSocketLike;\n private pvtOnMessage: ((frame: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n\n constructor(ws: WebSocketLike) {\n this.ws = ws;\n ws.onmessage = (event) => {\n if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(String(event.data));\n };\n ws.onclose = () => {\n this.closed = true;\n this.pvtOnClose?.();\n };\n }\n\n get canSend(): boolean {\n return !this.closed && this.ws.readyState === WS_OPEN;\n }\n\n onMessage(handler: (frame: string) => void): void {\n if (this.closed) throw new Error(\"WebSocketTransport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"WebSocketTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((frame: string) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n async send(frame: string): Promise<void> {\n if (this.closed) throw new Error(\"WebSocketTransport: closed\");\n if (this.ws.readyState !== WS_OPEN) throw new Error(\"WebSocketTransport: not open\");\n this.ws.send(frame);\n }\n\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n try {\n this.ws.close();\n } catch {\n // already closing / closed — ignore\n }\n }\n}\n","/* eslint-disable unicorn/prefer-add-event-listener, unicorn/require-post-message-target-origin */\n// ── Worker transport ────────────────────────────────────────────────────────────\n//\n// Moves frame objects directly over postMessage. postMessage structured-clones\n// the frame object, so there is no JSON encode/decode — this transport implements\n// Channel directly (not StringTransport). Structured clone is the worker's\n// native encoding, named by WORKER_VERSION.\n\nimport type { Channel } from \"../channel.js\";\n\n/** The worker encoding: native structured clone (no JSON). */\nexport const WORKER_VERSION = \"clone.v1\";\n\n/**\n * The subset of a worker the transport needs. The main-thread `Worker` and the\n * worker-global `self` both satisfy it; `terminate` is present only on the\n * main-thread side.\n */\nexport interface WorkerLike {\n postMessage(data: unknown): void;\n onmessage: ((event: { data: unknown }) => void) | null;\n onerror?: ((event: unknown) => void) | null;\n terminate?(): void;\n}\n\nexport class WorkerTransport implements Channel {\n private worker: WorkerLike;\n private pvtOnMessage: ((frame: Record<string, unknown>) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n\n constructor(worker: WorkerLike) {\n this.worker = worker;\n worker.onmessage = (event) => {\n if (this.pvtOnMessage && !this.closed) {\n this.pvtOnMessage(event.data as Record<string, unknown>);\n }\n };\n // An uncaught error in the worker is the closest thing to an abrupt drop.\n if (\"onerror\" in worker) {\n worker.onerror = () => {\n this.closed = true;\n this.pvtOnClose?.();\n };\n }\n }\n\n get canSend(): boolean {\n return !this.closed;\n }\n\n onMessage(handler: (frame: Record<string, unknown>) => void): void {\n if (this.closed) throw new Error(\"WorkerTransport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"WorkerTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((frame: Record<string, unknown>) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n async send(frame: Record<string, unknown>): Promise<void> {\n if (this.closed) throw new Error(\"WorkerTransport: closed\");\n this.worker.postMessage(frame);\n }\n\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n this.worker.terminate?.();\n }\n}\n","/* eslint-disable unicorn/prefer-add-event-listener */\n// ── WebSocket client spawner ────────────────────────────────────────────────────\n//\n// Environment-specific: connects to a `ws://` URL with the WHATWG WebSocket,\n// wraps it in a WebSocketTransport, and returns a frame Channel with the $proto\n// handshake already validated.\n\nimport { WebSocketTransport } from \"../transports/websocket.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport { isProto } from \"../channel.js\";\nimport type { ClientSpawner } from \"../client.js\";\n\nexport type WebSocketCtor = new (url: string) => {\n readonly readyState: number;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n onopen: (() => void) | null;\n onerror: ((err: unknown) => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n onclose: (() => void) | null;\n};\n\nconst defaultWebSocket = (globalThis as { WebSocket?: WebSocketCtor }).WebSocket;\n\nfunction openWebSocket(ws: { readonly readyState: number; onopen: (() => void) | null; onerror: ((err: unknown) => void) | null }): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState === 1 /* OPEN */) return resolve();\n ws.onopen = () => resolve();\n ws.onerror = (err) => reject(new Error(`websocket failed to open: ${String(err)}`));\n });\n}\n\nexport function wsClientSpawner<Args = unknown>(\n url: string | ((args: Args) => string),\n WebSocketImpl: WebSocketCtor = defaultWebSocket as WebSocketCtor,\n): ClientSpawner<Args> {\n return async (args) => {\n const target = typeof url === \"function\" ? url(args) : url;\n const ws = new WebSocketImpl(target);\n const transport = new WebSocketTransport(ws);\n const channel = json1Channel(transport);\n\n // Set the $proto waiter before awaiting open so a fast server's handshake\n // frame is not dropped.\n const protoPromise = new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n await openWebSocket(ws);\n const protoFrame = await protoPromise;\n channel.removeHandler();\n\n if (!isProto(protoFrame) || protoFrame.$proto !== VERSION) {\n throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);\n }\n return channel;\n };\n}\n","// ── WebSocket server spawner ────────────────────────────────────────────────────\n//\n// Environment-specific: serves an already-upgraded WebSocket connection. The\n// consumer's HTTP server does the upgrade and hands us the socket; we wrap it in\n// a WebSocketTransport, send $proto, and return the frame Channel.\n\nimport { WebSocketTransport, type WebSocketLike } from \"../transports/websocket.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport type { Spawner } from \"../server.js\";\n\nexport function wsServerSpawner(ws: WebSocketLike): Spawner {\n return async () => {\n const channel = json1Channel(new WebSocketTransport(ws));\n await channel.send({ $proto: VERSION });\n return channel;\n };\n}\n\n/**\n * A minimal Bun `ServerWebSocket` — `send`/`close`/`readyState` plus the data\n * slot the server uses to stash per-connection state. Bun delivers message and\n * close events through the `Bun.serve` websocket config callbacks, not through\n * properties on the socket, so the consumer routes them onto the adapter.\n */\nexport interface BunServerWebSocketLike {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly readyState: number;\n data?: unknown;\n}\n\n/**\n * Adapt a Bun server socket to the `WebSocketLike` the transport consumes. The\n * returned adapter owns settable `onmessage`/`onclose` slots; the consumer\n * routes Bun's `message`/`close` config callbacks into them, e.g.:\n *\n * ```ts\n * Bun.serve({\n * websocket: {\n * open(ws) {\n * const adapter = bunServerWebSocket(ws);\n * ws.data = adapter;\n * serveRemoteActor(actor, wsServerSpawner(adapter));\n * },\n * message(ws, msg) { (ws.data as WebSocketLike).onmessage?.({ data: msg }); },\n * close(ws) { (ws.data as WebSocketLike).onclose?.(); },\n * },\n * });\n * ```\n */\nexport function bunServerWebSocket(ws: BunServerWebSocketLike): WebSocketLike {\n return {\n send: (data) => ws.send(data),\n close: (code, reason) => ws.close(code, reason),\n readyState: ws.readyState,\n onmessage: null,\n onclose: null,\n };\n}\n","// ── Worker client spawner (command) ─────────────────────────────────────────────\n//\n// Environment-specific: spawns a Worker with the WHATWG `new Worker(url)` API,\n// wraps it in a WorkerTransport, and returns a frame Channel with the $proto\n// handshake already validated.\n\nimport { WorkerTransport, WORKER_VERSION, type WorkerLike } from \"../transports/worker.js\";\nimport { isProto } from \"../channel.js\";\nimport type { ClientSpawner } from \"../client.js\";\n\nexport type WorkerCtor = new (url: string) => WorkerLike;\n\nconst defaultWorker = (globalThis as { Worker?: WorkerCtor }).Worker;\n\nexport function workerClientSpawner<Args = unknown>(\n url: string | ((args: Args) => string),\n WorkerImpl: WorkerCtor = defaultWorker as WorkerCtor,\n): ClientSpawner<Args> {\n return async (args) => {\n const target = typeof url === \"function\" ? url(args) : url;\n const channel = new WorkerTransport(new WorkerImpl(target));\n\n const protoFrame = await new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n channel.removeHandler();\n\n if (!isProto(protoFrame) || protoFrame.$proto !== WORKER_VERSION) {\n throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);\n }\n return channel;\n };\n}\n","// ── Worker server spawner (self) ────────────────────────────────────────────────\n//\n// Environment-specific: serves the worker-global scope (`self`) as a frame\n// Channel with the $proto handshake already sent. The worker entry script calls\n// `serveRemoteActor(actor, workerSelfSpawner())`.\n\nimport { WorkerTransport, WORKER_VERSION, type WorkerLike } from \"../transports/worker.js\";\nimport type { Spawner } from \"../server.js\";\n\nexport function workerSelfSpawner(selfScope: WorkerLike = globalThis as unknown as WorkerLike): Spawner {\n return async () => {\n const channel = new WorkerTransport(selfScope);\n await channel.send({ $proto: WORKER_VERSION });\n return channel;\n };\n}\n"],"mappings":"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"}
|
package/dist/remote/node.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as Message } from "../types-CsE8xK3a.js";
|
|
2
|
-
import { h as MethodOptions, l as ActorDefinition, p as HandlerOptions, v as ReflectionOptions } from "../hooks-
|
|
3
|
-
import { i as StringTransport, r as Channel, t as ClientSpawner } from "../client-
|
|
2
|
+
import { h as MethodOptions, l as ActorDefinition, p as HandlerOptions, v as ReflectionOptions } from "../hooks-Dj59y_jN.js";
|
|
3
|
+
import { i as StringTransport, r as Channel, t as ClientSpawner } from "../client-C4LZE65N.js";
|
|
4
4
|
import { FileHandle } from "node:fs/promises";
|
|
5
5
|
import { ChildProcess, StdioOptions } from "node:child_process";
|
|
6
6
|
import { Readable, Writable } from "node:stream";
|