import { Socket } from "node:net";
import { nextTick } from "ext:deno_node/_next_tick.ts";
import { codeMap, UV_ECANCELED } from "ext:deno_node/internal_binding/uv.ts";
import { setImmediate } from "node:timers";
import {
kBytesWritten,
kLastWriteWasAsync,
streamBaseState,
} from "ext:deno_node/internal_binding/stream_wrap.ts";
import { Buffer } from "node:buffer";
const kCurrentWriteRequest = Symbol("kCurrentWriteRequest");
const kCurrentShutdownRequest = Symbol("kCurrentShutdownRequest");
const kPendingShutdownRequest = Symbol("kPendingShutdownRequest");
const kPendingClose = Symbol("kPendingClose");
const kJSStreamHandle = Symbol.for("kJSStreamHandle");
function isClosing() {
return this[kOwner].isClosing();
}
function onreadstart() {
return this[kOwner].readStart();
}
function onreadstop() {
return this[kOwner].readStop();
}
const kOwner = Symbol.for("kJSStreamOwner");
class JSStreamSocket extends Socket {
constructor(stream) {
const handle = {
[kJSStreamHandle]: true,
[kOwner]: null, close(cb) {
handle[kOwner].doClose(cb);
},
isClosing,
onreadstart,
onreadstop,
readStart() {
return handle[kOwner].readStart();
},
readStop() {
return handle[kOwner].readStop();
},
writeBuffer(req, data) {
const len = data.byteLength ?? data.length ?? 0;
streamBaseState[kBytesWritten] = len;
streamBaseState[kLastWriteWasAsync] = 1;
return handle[kOwner].doWrite(req, [data]);
},
writev(req, chunks, allBuffers) {
let bufs;
let total = 0;
if (allBuffers) {
bufs = chunks;
for (let i = 0; i < bufs.length; i++) {
total += bufs[i].byteLength ?? bufs[i].length ?? 0;
}
} else {
bufs = new Array(chunks.length >> 1);
for (let i = 0; i < chunks.length; i += 2) {
const chunk = chunks[i];
const enc = chunks[i + 1];
const buf = typeof chunk === "string"
? Buffer.from(chunk, enc)
: chunk;
bufs[i >> 1] = buf;
total += buf.byteLength ?? buf.length ?? 0;
}
}
streamBaseState[kBytesWritten] = total;
streamBaseState[kLastWriteWasAsync] = 1;
return handle[kOwner].doWrite(req, bufs);
},
writeAsciiString(req, data) {
return this.writeBuffer(req, Buffer.from(data, "ascii"));
},
writeUtf8String(req, data) {
return this.writeBuffer(req, Buffer.from(data, "utf8"));
},
writeLatin1String(req, data) {
return this.writeBuffer(req, Buffer.from(data, "latin1"));
},
writeUcs2String(req, data) {
return this.writeBuffer(req, Buffer.from(data, "utf16le"));
},
shutdown(req) {
return handle[kOwner].doShutdown(req);
},
readBuffer: null,
emitEOF: null,
reading: false,
};
stream.pause();
stream.on("error", (err) => this.emit("error", err));
const ondata = (chunk) => {
if (
typeof chunk === "string" ||
stream.readableObjectMode === true
) {
stream.pause();
stream.removeListener("data", ondata);
this.emit("error", new Error("Stream is not in binary mode"));
return;
}
if (this._handle && this._handle.readBuffer) {
this._handle.readBuffer(chunk);
}
};
stream.on("data", ondata);
stream.once("end", () => {
if (this._handle && this._handle.emitEOF) {
this._handle.emitEOF();
}
});
stream.once("close", () => {
this.destroy();
});
super({ handle, manualStart: true });
handle[kOwner] = this;
this.stream = stream;
this[kCurrentWriteRequest] = null;
this[kCurrentShutdownRequest] = null;
this[kPendingShutdownRequest] = null;
this[kPendingClose] = false;
this.readable = stream.readable;
this.writable = stream.writable;
this.read(0);
}
isClosing() {
return !this.readable || !this.writable;
}
readStart() {
this.stream.resume();
return 0;
}
readStop() {
this.stream.pause();
return 0;
}
doShutdown(req) {
if (this[kCurrentWriteRequest] !== null) {
this[kPendingShutdownRequest] = req;
return 0;
}
this[kCurrentShutdownRequest] = req;
if (this[kPendingClose]) {
return 0;
}
const handle = this._handle;
nextTick(() => {
this.stream.end(() => {
this.finishShutdown(handle, 0);
});
});
return 0;
}
finishShutdown(_handle, errCode) {
const req = this[kCurrentShutdownRequest];
if (req === null) return;
this[kCurrentShutdownRequest] = null;
if (typeof req.oncomplete === "function") {
req.oncomplete(errCode | 0);
}
}
doWrite(req, bufs) {
if (this[kPendingClose]) {
this[kCurrentWriteRequest] = req;
return 0;
} else if (this._handle === null) {
return 0;
}
const handle = this._handle;
const self = this;
let pending = bufs.length;
this.stream.cork();
for (let i = 0; i < bufs.length; ++i) {
this.stream.write(bufs[i], done);
}
this.stream.uncork();
this[kCurrentWriteRequest] = req;
function done(err) {
if (!err && --pending !== 0) return;
pending = 0;
let errCode = 0;
if (err) {
errCode = codeMap.get(err.code) || codeMap.get("EPIPE");
}
setImmediate(() => {
self.finishWrite(handle, errCode);
});
}
return 0;
}
finishWrite(_handle, errCode) {
const req = this[kCurrentWriteRequest];
if (req === null) return;
this[kCurrentWriteRequest] = null;
if (typeof req.oncomplete === "function") {
req.oncomplete(errCode | 0);
}
if (this[kPendingShutdownRequest]) {
const sreq = this[kPendingShutdownRequest];
this[kPendingShutdownRequest] = null;
this.doShutdown(sreq);
}
}
doClose(cb) {
this[kPendingClose] = true;
const handle = this._handle;
this.stream.destroy();
setImmediate(() => {
this.finishWrite(handle, UV_ECANCELED);
this.finishShutdown(handle, UV_ECANCELED);
this[kPendingClose] = false;
if (cb) cb();
});
}
}
export { JSStreamSocket, kJSStreamHandle, kOwner };
export default JSStreamSocket;