Skip to content

Commit

Permalink
Add optional "extra Node options."
Browse files Browse the repository at this point in the history
  • Loading branch information
danfuzz committed Dec 5, 2024
1 parent 7f17113 commit 12d0478
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 6 deletions.
92 changes: 87 additions & 5 deletions src/net-util/export/InterfaceAddress.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export class InterfaceAddress extends IntfDeconstructable {
*/
#fd;

/**
* Extra options to use when interacting with Node's {@link Server} API.
*
* @type {object}
*/
#nodeOptions;

/**
* Result for {@link #toString}, or `null` if not yet calculated.
*
Expand All @@ -64,10 +71,17 @@ export class InterfaceAddress extends IntfDeconstructable {
* be informational only) when passing `fd`. When non-`null`, it must be an
* integer in the range `1..65535`.
*
* **Note:** With regards to `nodeServerOptions`, `allowHalfOpen: true` is
* included by default, even though that isn't Node's default, because it is
* arguably a better default to have.
*
* @param {string|object} fullAddress The full address, in one of the forms
* mentioned above.
* @param {?object} nodeServerOptions Extra options to use when constructing
* a Node {@link Server} object or calling `listen()` on one; or `null` not
* to have extra options beyond the defaults.
*/
constructor(fullAddress) {
constructor(fullAddress, nodeServerOptions = null) {
super();

let needCanonicalization;
Expand Down Expand Up @@ -111,9 +125,10 @@ export class InterfaceAddress extends IntfDeconstructable {
}
}

this.#address = address;
this.#portNumber = portNumber;
this.#fd = fd;
this.#address = address;
this.#portNumber = portNumber;
this.#fd = fd;
this.#nodeOptions = InterfaceAddress.#fixNodeOptions(nodeServerOptions);
}

/**
Expand All @@ -133,6 +148,14 @@ export class InterfaceAddress extends IntfDeconstructable {
return this.#fd;
}

/**
* @returns {object} Frozen plain object with any extra options that are to be
* used when configuring a Node {@link Server} object.
*/
get nodeServerOptions() {
return this.#nodeOptions;
}

/**
* @returns {?number} The port, or `null` if this instance has an {@link #fd}
* and no known port.
Expand Down Expand Up @@ -170,7 +193,26 @@ export class InterfaceAddress extends IntfDeconstructable {
const { address: a1, fd: fd1, portNumber: pn1 } = this;
const { address: a2, fd: fd2, portNumber: pn2 } = other;

return (a1 === a2) && (fd1 === fd2) && (pn1 === pn2);
if (!((a1 === a2) && (fd1 === fd2) && (pn1 === pn2))) {
return false;
}

const ns1 = this.nodeServerOptions;
const ns2 = other.nodeServerOptions;
const keys1 = Object.getOwnPropertyNames(ns1);
const keys2 = Object.getOwnPropertyNames(ns2);

if (keys1.length !== keys2.length) {
return false;
}

for (const k of keys1) {
if (ns1[k] !== ns2[k]) {
return false;
}
}

return true;
}

/**
Expand Down Expand Up @@ -305,4 +347,44 @@ export class InterfaceAddress extends IntfDeconstructable {

throw new Error(`Not a port number: ${portNumber}`);
}

/**
* Validates and "Fixes" a `nodeServerOptions` argument.
*
* @param {*} nodeServerOptions Argument to fix.
* @returns {object} The fixed version.
* @throws {Error} Thrown if there was trouble.
*/
static #fixNodeOptions(nodeServerOptions) {
const result = { allowHalfOpen: true };

for (const [k, v] of Object.entries(nodeServerOptions ?? {})) {
switch (k) {
case 'allowHalfOpen':
case 'exclusive':
case 'keepAlive':
case 'noDelay':
case 'pauseOnConnect': {
result[k] = MustBe.boolean(v);
break;
}

case 'backlog': {
result[k] = MustBe.number(v, { safeInteger: true, minInclusive: 0 });
break;
}

case 'keepAliveInitialDelay': {
result[k] = MustBe.number(v, { minInclusive: 0 });
break;
}

default: {
throw new Error(`Unrecognized option: ${k}`);
}
}
}

return Object.freeze(result);
}
}
71 changes: 70 additions & 1 deletion src/net-util/tests/InterfaceAddress.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,31 @@ describe('constructor', () => {
expect(() => new InterfaceAddress('/dev/fd/777:654')).not.toThrow();
expect(() => new InterfaceAddress({ fd: 777, portNumber: 654 })).not.toThrow();
});

test('accepts all valid extra Node options', () => {
const opts = {
allowHalfOpen: true,
backlog: 100,
exclusive: true,
keepAlive: true,
keepAliveInitialDelay: 1234.56,
noDelay: true,
pauseOnConnect: true
};

expect(() => new InterfaceAddress('1.2.3.4:56', opts)).not.toThrow();
});

test.each`
opts
${{ address: '10.20.30.40' }}
${{ fd: 99 }}
${{ port: 123 }}
${{ portNumber: 123 }}
${{ zorp: false }}
`('rejects extra node options `$opts`', ({ opts }) => {
expect(() => new InterfaceAddress('1.2.3.4:56', opts)).toThrow();
});
});

describe('.address', () => {
Expand Down Expand Up @@ -163,6 +188,36 @@ describe('.fd', () => {
});
});

describe('.nodeServerOptions', () => {
test('has `allowHalfOpen: true` by default', () => {
const ia = new InterfaceAddress('a.b:3');

expect(ia.nodeServerOptions).toStrictEqual({ allowHalfOpen: true });
});

test('is frozen', () => {
const ia = new InterfaceAddress('a.b:3');

expect(ia.nodeServerOptions).toBeFrozen();
});

test('is equal to but not the same object as the one passed in the constructor', () => {
const opts = { allowHalfOpen: false, backlog: 123 };
const ia = new InterfaceAddress('a.b:3', opts);

expect(ia.nodeServerOptions).toStrictEqual(opts);
expect(ia.nodeServerOptions).not.toBe(opts);
});

test('is `null` if constructed with no `fd`', () => {
const ia1 = new InterfaceAddress('1.3.4.1:99');
const ia2 = new InterfaceAddress({ address: '[99::aa]', portNumber: 986 });

expect(ia1.fd).toBeNull();
expect(ia2.fd).toBeNull();
});
});

describe('.portNumber', () => {
test('is the number passed in the constructor', () => {
const ia1 = new InterfaceAddress('12.34.56.78:999');
Expand Down Expand Up @@ -199,12 +254,26 @@ describe('equals()', () => {
expect(ia1.equals(ia2)).toBeTrue();
});

test('returns `false` when compared to a differently-constructed instance', () => {
test('returns `false` when compared to a differently-constructed main instance', () => {
const ia1 = new InterfaceAddress('1.2.3.4:567');
const ia2 = new InterfaceAddress('4.3.2.1:987');
expect(ia1.equals(ia2)).toBeFalse();
});

test('returns `true` when extra Node options match', () => {
const opts = { allowHalfOpen: true, keepAlive: true, keepAliveInitialDelay: 99 };
const ia1 = new InterfaceAddress('x:9', opts);
const ia2 = new InterfaceAddress('x:9', opts);
expect(ia1.equals(ia2)).toBeTrue();
});

test('returns `false` when extra Node options do not match', () => {
const opts = { allowHalfOpen: true, keepAlive: true, keepAliveInitialDelay: 99 };
const ia1 = new InterfaceAddress('x:9', opts);
const ia2 = new InterfaceAddress('x:9', { ...opts, allowHalfOpen: false });
expect(ia1.equals(ia2)).toBeFalse();
});

test.each`
arg
${null}
Expand Down

0 comments on commit 12d0478

Please sign in to comment.