Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/6894-dgram-node-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Expand `node:dgram` parity coverage and align socket lifecycle, validation,
send, multicast, metrics, and export behavior with Node.js 26.5.
4 changes: 1 addition & 3 deletions crates/perry-codegen/src/loop_purity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,7 @@ fn expr_alloc_free(e: &Expr) -> bool {
// Element READS never allocate — they return an existing element / a
// number. Recurse so the object and index are themselves alloc-free.
Expr::IndexGet { object, index } => expr_alloc_free(object) && expr_alloc_free(index),
Expr::BufferIndexGet { buffer, index } => {
expr_alloc_free(buffer) && expr_alloc_free(index)
}
Expr::BufferIndexGet { buffer, index } => expr_alloc_free(buffer) && expr_alloc_free(index),
Expr::Uint8ArrayGet { array, index } => expr_alloc_free(array) && expr_alloc_free(index),
// `arr[i]++` / `--`: read-modify-write of an existing numeric slot, no
// growth, no allocation.
Expand Down
297 changes: 231 additions & 66 deletions test-parity/node-suite/dgram/STATUS.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions test-parity/node-suite/dgram/api/export-shape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Upstream: Node v26.5.0 lib/dgram.js module.exports and ESM named-export bridge.
// Coverage added: exact enumerable exports beyond callable members.
import * as dgram from "node:dgram";

console.log("exports:", Object.keys(dgram).sort().join(","));
console.log("named types:", typeof dgram.Socket, typeof dgram.createSocket);
24 changes: 24 additions & 0 deletions test-parity/node-suite/dgram/api/receiver-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Upstream: Node v26.5.0 lib/dgram.js Socket methods read receiver-owned state.
// Coverage added: invalid call/apply receivers on stateful methods.
import * as dgram from "node:dgram";

function codeOf(fn: () => unknown): string {
try {
fn();
return "none";
} catch (error: unknown) {
const value = error as { code?: string; name?: string };
return value.code ?? value.name ?? "Error";
}
}

const socket = dgram.createSocket("udp4");

try {
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", resolve));
const other = {} as dgram.Socket;
console.log("address receiver:", codeOf(() => socket.address.call(other)));
console.log("ref receiver:", codeOf(() => socket.ref.call(other)));
} finally {
await new Promise<void>((resolve) => socket.close(resolve));
}
20 changes: 20 additions & 0 deletions test-parity/node-suite/dgram/api/socket-aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Upstream: Node v26.5.0 lib/dgram.js Socket inheritance and public surface.
// Coverage added: EventEmitter aliases and the absent hasRef method.
import * as dgram from "node:dgram";

const socket = dgram.createSocket("udp4");

try {
console.log(
"aliases:",
socket.on === socket.addListener,
socket.off === socket.removeListener,
);
console.log(
"hasRef:",
typeof (socket as dgram.Socket & { hasRef?: unknown }).hasRef,
);
} finally {
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => socket.close(resolve));
}
33 changes: 33 additions & 0 deletions test-parity/node-suite/dgram/api/socket-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Upstream: Node v26.5.0 lib/dgram.js Socket constructor and prototype setup.
// Coverage added: construction and prototype identity.
import * as dgram from "node:dgram";
import { EventEmitter } from "node:events";

async function bindAndClose(socket: dgram.Socket): Promise<void> {
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => socket.close(resolve));
}

const created = dgram.createSocket("udp4");
const constructed = new dgram.Socket("udp4");

try {
console.log(
"created class:",
created instanceof dgram.Socket,
created instanceof EventEmitter,
Object.getPrototypeOf(created) === dgram.Socket.prototype,
);
console.log(
"constructed class:",
constructed instanceof dgram.Socket,
constructed.type,
);
console.log(
"constructors:",
created.constructor === dgram.Socket,
dgram.Socket.prototype.constructor === dgram.Socket,
);
} finally {
await Promise.all([bindAndClose(created), bindAndClose(constructed)]);
}
24 changes: 24 additions & 0 deletions test-parity/node-suite/dgram/api/socket-descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Upstream: Node v26.5.0 lib/dgram.js Socket prototype and type setup.
// Coverage added: public method and socket type descriptors.
import * as dgram from "node:dgram";

function descriptor(value: object, key: PropertyKey): string {
const item = Object.getOwnPropertyDescriptor(value, key);
return item
? `${item.writable}:${item.enumerable}:${item.configurable}`
: "missing";
}

const socket = dgram.createSocket("udp4");

try {
console.log("type descriptor:", socket.type, descriptor(socket, "type"));
console.log(
"bind descriptor:",
descriptor(dgram.Socket.prototype, "bind"),
Object.hasOwn(socket, "bind"),
);
} finally {
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) => socket.close(resolve));
}
41 changes: 27 additions & 14 deletions test-parity/node-suite/dgram/connection/connect-ordering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,33 @@ await new Promise<void>((resolve) => receiver.bind(0, "127.0.0.1", resolve));

const sender = dgram.createSocket("udp4");
const order: string[] = [];
sender.once("connect", () => order.push("event"));

await new Promise<void>((resolve) => {
sender.connect(receiver.address().port, "127.0.0.1", () => {
order.push("callback");
resolve();
});
});
try {
sender.once("connect", () => order.push("event"));

const remote = sender.remoteAddress();
console.log("connect order:", order.join(","));
console.log("remote:", remote.address, remote.family, remote.port === receiver.address().port);
await new Promise<void>((resolve) => {
sender.connect(receiver.address().port, "127.0.0.1", function () {
order.push(`callback:${this === sender}:${arguments.length}`);
resolve();
});
});

await Promise.all([
new Promise<void>((resolve) => sender.close(resolve)),
new Promise<void>((resolve) => receiver.close(resolve)),
]);
const remote = sender.remoteAddress();
console.log("connect order:", order.join(","));
console.log(
"remote:",
remote.address,
remote.family,
remote.port === receiver.address().port,
);
} finally {
const senderClosed = new Promise<void>((resolve) =>
sender.once("close", resolve)
);
const receiverClosed = new Promise<void>((resolve) =>
receiver.once("close", resolve)
);
sender.close();
receiver.close();
await Promise.all([senderClosed, receiverClosed]);
}
46 changes: 46 additions & 0 deletions test-parity/node-suite/dgram/connection/connect-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Upstream: Node v26.5.0 test/parallel/test-dgram-connect-sync.js.
// Coverage added: Node 26's synchronous connect contract.
import * as dgram from "node:dgram";

type ConnectSync = (port: number, address?: string) => void;
const receiver = dgram.createSocket("udp4");
const sender = dgram.createSocket("udp4");
const connectSync =
(sender as dgram.Socket & { connectSync?: ConnectSync }).connectSync;

try {
await new Promise<void>((resolve) => receiver.bind(0, "127.0.0.1", resolve));
if (typeof connectSync !== "function") {
console.log("connectSync supported:", false);
} else {
const order: string[] = [];
const connected = new Promise<void>((resolve) =>
sender.once("connect", () => {
order.push("connect");
resolve();
})
);
const result = connectSync.call(
sender,
receiver.address().port,
"127.0.0.1",
);
order.push("returned");
const remote = sender.remoteAddress();
console.log("connectSync supported:", true);
console.log("connectSync result:", result);
console.log(
"connectSync remote:",
remote.address,
remote.family,
remote.port === receiver.address().port,
);
await connected;
console.log("connectSync order:", order.join(","));
}
} finally {
await Promise.all([
new Promise<void>((resolve) => sender.close(resolve)),
new Promise<void>((resolve) => receiver.close(resolve)),
]);
}
84 changes: 55 additions & 29 deletions test-parity/node-suite/dgram/connection/state-transitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,62 @@ function codeOf(fn: () => unknown): string {
const socket = dgram.createSocket("udp4");
const firstPeer = dgram.createSocket("udp4");
const secondPeer = dgram.createSocket("udp4");
await Promise.all([
new Promise<void>((resolve) => firstPeer.bind(0, "127.0.0.1", () => resolve())),
new Promise<void>((resolve) => secondPeer.bind(0, "127.0.0.1", () => resolve())),
]);
const firstPort = firstPeer.address().port;
const secondPort = secondPeer.address().port;
try {
await Promise.all([
new Promise<void>((resolve) =>
firstPeer.bind(0, "127.0.0.1", () => resolve())
),
new Promise<void>((resolve) =>
secondPeer.bind(0, "127.0.0.1", () => resolve())
),
]);
const firstPort = firstPeer.address().port;
const secondPort = secondPeer.address().port;

console.log("disconnect before connect:", codeOf(() => socket.disconnect()));
console.log("bad ports:", [0, -1, 65536].map((port) => codeOf(() => socket.connect(port))).join(","));
console.log("disconnect before connect:", codeOf(() => socket.disconnect()));
console.log(
"bad ports:",
[0, -1, 65536].map((port) => codeOf(() => socket.connect(port))).join(","),
);

const firstConnect = new Promise<void>((resolve) => {
socket.connect(firstPort, "127.0.0.1", () => resolve());
});
console.log("connect while pending:", codeOf(() => socket.connect(firstPort)));
await firstConnect;
const firstConnect = new Promise<void>((resolve) => {
socket.connect(firstPort, "127.0.0.1", () => resolve());
});
console.log(
"connect while pending:",
codeOf(() => socket.connect(firstPort)),
);
await firstConnect;

let remote = socket.remoteAddress();
console.log("first remote:", remote.address, remote.family, remote.port === firstPort);
console.log("connect while connected:", codeOf(() => socket.connect(secondPort)));
console.log("disconnect result:", socket.disconnect());
console.log("remote after disconnect:", codeOf(() => socket.remoteAddress()));
console.log("repeat disconnect:", codeOf(() => socket.disconnect()));
let remote = socket.remoteAddress();
console.log(
"first remote:",
remote.address,
remote.family,
remote.port === firstPort,
);
console.log(
"connect while connected:",
codeOf(() => socket.connect(secondPort)),
);
console.log("disconnect result:", socket.disconnect());
console.log("remote after disconnect:", codeOf(() => socket.remoteAddress()));
console.log("repeat disconnect:", codeOf(() => socket.disconnect()));

await new Promise<void>((resolve) => {
socket.connect(secondPort, "127.0.0.1", () => resolve());
});
remote = socket.remoteAddress();
console.log("reconnected remote:", remote.address, remote.family, remote.port === secondPort);
await Promise.all([
new Promise<void>((resolve) => socket.close(() => resolve())),
new Promise<void>((resolve) => firstPeer.close(() => resolve())),
new Promise<void>((resolve) => secondPeer.close(() => resolve())),
]);
await new Promise<void>((resolve) => {
socket.connect(secondPort, "127.0.0.1", () => resolve());
});
remote = socket.remoteAddress();
console.log(
"reconnected remote:",
remote.address,
remote.family,
remote.port === secondPort,
);
} finally {
await Promise.all([
new Promise<void>((resolve) => socket.close(resolve)),
new Promise<void>((resolve) => firstPeer.close(resolve)),
new Promise<void>((resolve) => secondPeer.close(resolve)),
]);
}
32 changes: 18 additions & 14 deletions test-parity/node-suite/dgram/control/ttl-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ function codeOf(fn: () => unknown): string {
}

const socket = dgram.createSocket("udp4");
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", () => resolve()));

console.log(
"ttl invalid:",
[0, 256, Infinity, "64"].map((value) => codeOf(() => socket.setTTL(value as never))).join(","),
);
console.log(
"multicast ttl invalid:",
[-1, 256, Infinity, "64"]
.map((value) => codeOf(() => socket.setMulticastTTL(value as never)))
.join(","),
);
console.log("ttl valid:", socket.setTTL(64), socket.setMulticastTTL(0));
await new Promise<void>((resolve) => socket.close(() => resolve()));
try {
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", resolve));
console.log(
"ttl invalid:",
[0, 256, Infinity, "64"].map((value) =>
codeOf(() => socket.setTTL(value as never))
).join(","),
);
console.log(
"multicast ttl invalid:",
[-1, 256, Infinity, "64"]
.map((value) => codeOf(() => socket.setMulticastTTL(value as never)))
.join(","),
);
console.log("ttl valid:", socket.setTTL(64), socket.setMulticastTTL(0));
} finally {
await new Promise<void>((resolve) => socket.close(resolve));
}
Loading
Loading