diff --git a/doc/api/fetch.md b/doc/api/fetch.md new file mode 100644 index 00000000000000..b03c40972172e1 --- /dev/null +++ b/doc/api/fetch.md @@ -0,0 +1,243 @@ +# Fetch + + + +> Stability: 1 - Experimental + +## Class: `fetch.Headers` + +Represents a WHATWG Fetch Spec +[Headers Class](https://fetch.spec.whatwg.org/#headers-class). + +### `new Headers([init])` + +* `init` {Headers | Iterable | string\[] | string\[]\[] | Object} Initial header list to be cloned into the new instance. + +Per spec, other JS primordials can be passed and will fail silently. + +```js +new Headers(); + +new Headers([['name', 'value']]); + +new Headers(['name', 'value']); + +const headers = new Headers({ + name: 'value', +}); + +new Headers(headers); +``` + +### `headers.append(name, value)` + +* `name` {string} +* `value` {string} + +Non-destructive operation for adding header entries. When called multiple times +with the same _name_, the values will be collected in a list and returned +together when retrieved using [Headers.get](#headersgetname). + +```js +const headers = new Headers(); + +headers.append('undici', 'fetch'); +headers.get('undici'); // -> 'fetch' + +headers.append('foobar', 'fuzz'); +headers.append('foobar', 'buzz'); +headers.get('foobar'); // -> 'fuzz, buzz' +``` + +### `headers.delete(name)` + +* `name` {string} + +Removes a header entry. This operation is destructive and cannot be restored. +Does **not** throw an error if the given _name_ does not exist. Reminder that +[Headers.get](#headersgetname) will return `null` if the _name_ does not exist. + +```js +const headers = new Headers(); + +headers.append('undici', 'fetch'); + +headers.get('undici'); // -> 'fetch' + +headers.delete('undici'); + +headers.get('undici'); // -> null +``` + +### `headers.get(name)` + +* `name` {string} +* Returns: {string | null} + +Retrieves a header entry. If the entry _name_ has multiple values, they are +returned as a string joined by `','` characters. If the _name_ does not exist, +this method returns `null`. + +```js +const headers = new Headers(); + +headers.append('undici', 'fetch'); +headers.get('undici'); // -> 'fetch' + +headers.append('foobar', 'fuzz'); +headers.append('foobar', 'buzz'); +headers.get('foobar'); // -> 'fuzz, buzz' + +headers.get('nodejs'); // -> null +``` + +### `headers.has(name)` + +* `name` {string} +* Returns: {boolean} + +Checks for the existence of a given entry _name_. + +```js +const headers = new Headers(); + +headers.append('undici', 'fetch'); +headers.has('undici'); // -> true +``` + +### `headers.set(name, value)` + +* `name` {string} +* `value` {string} + +Destructive operation that will override any existing values for the given entry +_name_. For a non-destructive alternative see +[Headers.append](#headersappendname-value). + +```js +const headers = new Headers(); + +headers.set('foobar', 'fuzz'); +headers.get('foobar'); // -> 'fuzz' + +headers.set('foobar', 'buzz'); +headers.get('foobar'); // -> 'buzz' +``` + +### `headers.values()` + +* Returns: {Iterator} + +Yields a list of header values combined and sorted by their respective keys. + +```js +const headers = new Headers(); + +headers.set('abc', '123'); +headers.set('def', '456'); +headers.set('ghi', '789'); +headers.append('ghi', '012'); + +for (const value of headers.values()) { + console.log(value); +} + +// -> '123' +// -> '456' +// -> '789, 012' +``` + +### `headers.keys()` + +Returns: {Iterator} + +Yields a sorted list of header keys. + +```js +const headers = new Headers(); + +headers.set('abc', '123'); +headers.set('def', '456'); +headers.set('ghi', '789'); +headers.append('ghi', '012'); + +for (const name of headers.keys()) { + console.log(name); +} + +// -> 'abc' +// -> 'def' +// -> 'ghi' +``` + +### `headers.forEach(callback, [thisArg])` + +* `callback` {Function} +* `thisArg` {any} (optional) + +A Headers class can be iterated using `.forEach(callback, [thisArg])`. + +The callback function is passed three arguments, `value: string`, `key: string`, and `iterable: Headers`. + +Optionally a `thisArg` can be passed which will be assigned to the `this` +context of callback. + +The headers are returned in a sorted order, and values are combined on similar +keys. + +```js +const headers = new Headers([['abc', '123']]); + +headers.forEach(function(value, key, headers) { + console.log(key, value); +}); +// -> 'abc', '123' +``` + +### `headers[Symbol.iterator]` + +* Returns: {Iterator} + +A Headers class instance is iterable. It yields each of its entries as a pair +where the first value is the entry _name_ and the second value is the header +_value_. They are sorted by _name_ or otherwise referred to as the header key. + +```js +const headers = new Headers(); + +headers.set('abc', '123'); +headers.set('def', '456'); +headers.set('ghi', '789'); +headers.append('ghi', '012'); + +for (const [name, value] of headers) { + console.log(name, value); +} + +// -> 'abc', '123' +// -> 'def', '456' +// -> 'ghi', '789, 012' +``` + +### `headers.entries()` + +* Returns: {Iterator} + +Yields a list of headers sorted and combined by key. + +```js +const headers = new Headers(); + +headers.set('abc', '123'); +headers.set('def', '456'); +headers.set('ghi', '789'); +headers.append('ghi', '012'); + +for (const entry of headers.entries()) { + console.log(entry); +} + +// -> 'abc', '123' +// -> 'def', '456' +// -> 'ghi', '789, 012' +``` diff --git a/doc/api/index.md b/doc/api/index.md index 448f6d599fc8f5..325983fcf221b9 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -29,6 +29,7 @@ * [Domain](domain.md) * [Errors](errors.md) * [Events](events.md) +* [Fetch](fetch.md) * [File system](fs.md) * [Globals](globals.md) * [HTTP](http.md) diff --git a/lib/fetch.js b/lib/fetch.js new file mode 100644 index 00000000000000..77d8d1497f413c --- /dev/null +++ b/lib/fetch.js @@ -0,0 +1,11 @@ +'use strict'; + +const { + emitExperimentalWarning, +} = require('internal/util'); + +emitExperimentalWarning('fetch/headers'); + +const { Headers } = require('internal/fetch/headers'); + +module.exports = { Headers }; diff --git a/lib/internal/fetch/headers.js b/lib/internal/fetch/headers.js new file mode 100644 index 00000000000000..e6b0a8eb9515e1 --- /dev/null +++ b/lib/internal/fetch/headers.js @@ -0,0 +1,289 @@ +'use strict'; + +const { + codes: { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_HTTP_TOKEN, + ERR_INVALID_THIS, + ERR_HTTP_INVALID_HEADER_VALUE, + }, +} = require('internal/errors'); + +const { + ArrayIsArray, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + FunctionPrototypeCall, + MathFloor, + ObjectDefineProperties, + ObjectEntries, + RegExpPrototypeSymbolReplace, + StringPrototypeLocaleCompare, + StringPrototypeToLocaleLowerCase, + Symbol, + SymbolIterator, +} = primordials; + +const { validateObject } = require('internal/validators'); +const { isBoxedPrimitive } = require('internal/util/types'); +const { + customInspectSymbol, + kEnumerableProperty, +} = require('internal/util'); + +const { validateHeaderName, validateHeaderValue } = require('_http_outgoing'); + +const { Buffer } = require('buffer'); + +const kHeadersList = Symbol('headers list'); + +/** + * This algorithm is based off of + * https://www.tbray.org/ongoing/When/200x/2003/03/22/Binary + * It only operates on the even indexes of the array (the header names) by only + * iterating at most half the length of the input array. The search also + * assumes all entries are strings and uses String.prototype.localeCompare for + * comparison. + */ +function binarySearch(arr, val) { + let low = 0; + let high = MathFloor(arr.length / 2); + + while (high > low) { + const mid = (high + low) >>> 1; + + if (StringPrototypeLocaleCompare(val, arr[mid * 2]) > 0) { + low = mid + 1; + } else { + high = mid; + } + } + + return low * 2; +} + +function normalizeAndValidateHeaderName(name) { + if (name === undefined) { + throw new ERR_INVALID_HTTP_TOKEN('Header name', name); + } + const normalizedHeaderName = StringPrototypeToLocaleLowerCase(name); + validateHeaderName(normalizedHeaderName); + return normalizedHeaderName; +} + +function normalizeAndValidateHeaderValue(name, value) { + if (value === undefined) { + throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); + } + // https://fetch.spec.whatwg.org/#concept-header-value-normalize + const normalizedHeaderValue = RegExpPrototypeSymbolReplace( + /^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, + value, + '' + ); + validateHeaderValue(name, normalizedHeaderValue); + return normalizedHeaderValue; +} + +function isHeaders(object) { + return kHeadersList in object; +} + +function fill(headers, object) { + if (isHeaders(object)) { + // Object is instance of Headers + headers[kHeadersList] = ArrayPrototypeSlice(object[kHeadersList]); + } else if (ArrayIsArray(object)) { + // Support both 1D and 2D arrays of header entries + if (ArrayIsArray(object[0])) { + // Array of arrays + for (let i = 0; i < object.length; i++) { + if (object[i].length !== 2) { + throw new ERR_INVALID_ARG_VALUE('init', object[i], + 'is not of length 2'); + } + headers.append(object[i][0], object[i][1]); + } + } else if (typeof object[0] === 'string' || Buffer.isBuffer(object[0])) { + // Flat array of strings or Buffers + if (object.length % 2 !== 0) { + throw new ERR_INVALID_ARG_VALUE('init', object, + 'is not even in length'); + } + for (let i = 0; i < object.length; i += 2) { + headers.append( + object[i].toString('utf-8'), + object[i + 1].toString('utf-8') + ); + } + } else { + // All other array based entries + throw new ERR_INVALID_ARG_VALUE( + 'init', + object, + 'is not a valid array entry' + ); + } + } else if (!isBoxedPrimitive(object)) { + // Object of key/value entries + const entries = ObjectEntries(object); + for (let i = 0; i < entries.length; i++) { + headers.append(entries[i][0], entries[i][1]); + } + } +} + +class Headers { + constructor(init = {}) { + validateObject(init, 'init', { allowArray: true }); + this[kHeadersList] = []; + fill(this, init); + } + + append(name, value) { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + const normalizedName = normalizeAndValidateHeaderName(name); + const normalizedValue = normalizeAndValidateHeaderValue(name, value); + + const index = binarySearch(this[kHeadersList], normalizedName); + + if (this[kHeadersList][index] === normalizedName) { + this[kHeadersList][index + 1] += `, ${normalizedValue}`; + } else { + ArrayPrototypeSplice( + this[kHeadersList], index, 0, normalizedName, normalizedValue); + } + } + + delete(name) { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + const normalizedName = normalizeAndValidateHeaderName(name); + + const index = binarySearch(this[kHeadersList], normalizedName); + + if (this[kHeadersList][index] === normalizedName) { + ArrayPrototypeSplice(this[kHeadersList], index, 2); + } + } + + get(name) { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + const normalizedName = normalizeAndValidateHeaderName(name); + + const index = binarySearch(this[kHeadersList], normalizedName); + + if (this[kHeadersList][index] === normalizedName) { + return this[kHeadersList][index + 1]; + } + + return null; + } + + has(name) { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + const normalizedName = normalizeAndValidateHeaderName(name); + + const index = binarySearch(this[kHeadersList], normalizedName); + + return this[kHeadersList][index] === normalizedName; + } + + set(name, value) { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + const normalizedName = normalizeAndValidateHeaderName(name); + const normalizedValue = normalizeAndValidateHeaderValue(name, value); + + const index = binarySearch(this[kHeadersList], normalizedName); + if (this[kHeadersList][index] === normalizedName) { + this[kHeadersList][index + 1] = normalizedValue; + } else { + ArrayPrototypeSplice( + this[kHeadersList], index, 0, normalizedName, normalizedValue); + } + } + + * keys() { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + for (let index = 0; index < this[kHeadersList].length; index += 2) { + yield this[kHeadersList][index]; + } + } + + * values() { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + for (let index = 1; index < this[kHeadersList].length; index += 2) { + yield this[kHeadersList][index]; + } + } + + * entries() { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + for (let index = 0; index < this[kHeadersList].length; index += 2) { + yield [this[kHeadersList][index], this[kHeadersList][index + 1]]; + } + } + + forEach(callback, thisArg) { + if (!isHeaders(this)) { + throw new ERR_INVALID_THIS('Headers'); + } + + for (let index = 0; index < this[kHeadersList].length; index += 2) { + FunctionPrototypeCall( + callback, + thisArg, + this[kHeadersList][index + 1], + this[kHeadersList][index], + this + ); + } + } + + [customInspectSymbol]() { + return this[kHeadersList]; + } +} + +Headers.prototype[SymbolIterator] = Headers.prototype.entries; + +ObjectDefineProperties(Headers.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + keys: kEnumerableProperty, + values: kEnumerableProperty, + entries: kEnumerableProperty, + forEach: kEnumerableProperty, +}); + +module.exports = { + Headers, + binarySearch, + kHeadersList, +}; diff --git a/lib/internal/util.js b/lib/internal/util.js index 9158fc8e52431e..65104634365409 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -464,6 +464,9 @@ function structuredClone(value) { return des.readValue(); } +const kEnumerableProperty = ObjectCreate(null); +kEnumerableProperty.enumerable = true; + module.exports = { assertCrypto, cachedResult, @@ -499,5 +502,6 @@ module.exports = { // Used by the buffer module to capture an internal reference to the // default isEncoding implementation, just in case userland overrides it. kIsEncodingSymbol: Symbol('kIsEncodingSymbol'), - kVmBreakFirstLineSymbol: Symbol('kVmBreakFirstLineSymbol') + kVmBreakFirstLineSymbol: Symbol('kVmBreakFirstLineSymbol'), + kEnumerableProperty, }; diff --git a/lib/internal/webstreams/compression.js b/lib/internal/webstreams/compression.js index 692f64af005af9..2ba8e53fa28e73 100644 --- a/lib/internal/webstreams/compression.js +++ b/lib/internal/webstreams/compression.js @@ -16,13 +16,11 @@ const { newReadableWritablePairFromDuplex, } = require('internal/webstreams/adapters'); -const { - customInspect, - kEnumerableProperty, -} = require('internal/webstreams/util'); +const { customInspect } = require('internal/webstreams/util'); const { customInspectSymbol: kInspect, + kEnumerableProperty, } = require('internal/util'); let zlib; diff --git a/lib/internal/webstreams/encoding.js b/lib/internal/webstreams/encoding.js index 5af59bc9f4a502..9cc76fae2ce10d 100644 --- a/lib/internal/webstreams/encoding.js +++ b/lib/internal/webstreams/encoding.js @@ -14,10 +14,7 @@ const { TransformStream, } = require('internal/webstreams/transformstream'); -const { - customInspect, - kEnumerableProperty, -} = require('internal/webstreams/util'); +const { customInspect } = require('internal/webstreams/util'); const { codes: { @@ -26,7 +23,8 @@ const { } = require('internal/errors'); const { - customInspectSymbol: kInspect + customInspectSymbol: kInspect, + kEnumerableProperty, } = require('internal/util'); const kHandle = Symbol('kHandle'); diff --git a/lib/internal/webstreams/queuingstrategies.js b/lib/internal/webstreams/queuingstrategies.js index 2db51b92a7f502..b739d87d8b73b0 100644 --- a/lib/internal/webstreams/queuingstrategies.js +++ b/lib/internal/webstreams/queuingstrategies.js @@ -14,6 +14,7 @@ const { const { customInspectSymbol: kInspect, + kEnumerableProperty, } = require('internal/util'); const { @@ -21,7 +22,6 @@ const { isBrandCheck, kType, kState, - kEnumerableProperty, } = require('internal/webstreams/util'); const { diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 62da68ff28086f..46dcbb844fefb8 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -50,6 +50,7 @@ const { const { createDeferredPromise, customInspectSymbol: kInspect, + kEnumerableProperty, } = require('internal/util'); const { @@ -103,7 +104,6 @@ const { nonOpStart, kType, kState, - kEnumerableProperty, } = require('internal/webstreams/util'); const { diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js index b4e690daa98c4a..cea127515d4f97 100644 --- a/lib/internal/webstreams/transformstream.js +++ b/lib/internal/webstreams/transformstream.js @@ -27,6 +27,7 @@ const { const { createDeferredPromise, customInspectSymbol: kInspect, + kEnumerableProperty, } = require('internal/util'); const { @@ -45,7 +46,6 @@ const { nonOpFlush, kType, kState, - kEnumerableProperty, } = require('internal/webstreams/util'); const { diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index 58f191cf07ff5a..305064ba05a490 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -207,9 +207,6 @@ function lazyTransfer() { return transfer; } -const kEnumerableProperty = ObjectCreate(null); -kEnumerableProperty.enumerable = true; - module.exports = { ArrayBufferViewGetBuffer, ArrayBufferViewGetByteLength, @@ -237,5 +234,4 @@ module.exports = { nonOpWrite, kType, kState, - kEnumerableProperty, }; diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js index dba7560c549a9a..d8a3d1d6cb14cc 100644 --- a/lib/internal/webstreams/writablestream.js +++ b/lib/internal/webstreams/writablestream.js @@ -33,6 +33,7 @@ const { const { createDeferredPromise, customInspectSymbol: kInspect, + kEnumerableProperty, } = require('internal/util'); const { @@ -64,7 +65,6 @@ const { nonOpWrite, kType, kState, - kEnumerableProperty, } = require('internal/webstreams/util'); const { diff --git a/test/parallel/test-headers.js b/test/parallel/test-headers.js new file mode 100644 index 00000000000000..0ebd34675fc95d --- /dev/null +++ b/test/parallel/test-headers.js @@ -0,0 +1,472 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// Flags: --expose-internals + +const { + binarySearch, + kHeadersList, + Headers, +} = require('internal/fetch/headers'); + +{ + // init is undefined + assert.deepStrictEqual(new Headers()[kHeadersList], []); + // Init is flat array with one entry + assert.deepStrictEqual( + new Headers(['test-name', 'test-value'])[kHeadersList], + ['test-name', 'test-value'] + ); + // Init is flat array with multiple entries + assert.deepStrictEqual( + new Headers(['test-name-1', 'test-value-1', 'test-name-2', 'test-value-2'])[ + kHeadersList + ], + ['test-name-1', 'test-value-1', 'test-name-2', 'test-value-2'] + ); + // Init is multidimensional array with one entry + assert.deepStrictEqual( + new Headers([['test-name-1', 'test-value-1']])[kHeadersList], + ['test-name-1', 'test-value-1'] + ); + // Init is multidimensional array with multiple entries + assert.deepStrictEqual( + new Headers([ + ['test-name-1', 'test-value-1'], + ['test-name-2', 'test-value-2'], + ])[kHeadersList], + ['test-name-1', 'test-value-1', 'test-name-2', 'test-value-2'] + ); + // Throws when init length is odd + assert.throws(() => { + new Headers(['test-name-1', 'test-value', 'test-name-2']); + }, { + name: 'TypeError', + message: "The argument 'init' is not even in length. " + + "Received [ 'test-name-1', 'test-value', 'test-name-2' ]" + }); + // Throws when multidimensional init entry length is not 2 + assert.throws(() => { + new Headers([['test-name-1', 'test-value-1'], ['test-name-2']]); + }, { + name: 'TypeError', + message: "The argument 'init' is not of length 2. " + + "Received [ 'test-name-2' ]" + }); + // Throws when init is not valid array input + assert.throws(() => { + new Headers([0, 1]); + }, { + name: 'TypeError', + message: "The argument 'init' is not a valid array entry. " + + 'Received [ 0, 1 ]' + }); +} + +{ + // Init is object with single entry + const headers = new Headers({ + 'test-name-1': 'test-value-1' + }); + assert.strictEqual(headers[kHeadersList].length, 2); +} + +{ + // Init is object with multiple entries + const headers = new Headers({ + 'test-name-1': 'test-value-1', + 'test-name-2': 'test-value-2' + }); + assert.strictEqual(headers[kHeadersList].length, 4); +} + +{ + // append + const headers = new Headers(); + headers.append('test-name-1', 'test-value-1'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + ]); + headers.append('test-name-2', 'test-value-2'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + 'test-name-2', + 'test-value-2', + ]); + headers.append('test-name-1', 'test-value-3'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1, test-value-3', + 'test-name-2', + 'test-value-2', + ]); + + assert.throws(() => { + headers.append(); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["undefined"]' + }); + + assert.throws(() => { + headers.append('test-name'); + }, { + name: 'TypeError', + message: 'Invalid value "undefined" for header "test-name"' + }); + + assert.throws(() => { + headers.append('invalid @ header ? name', 'test-value'); + }, { + name: 'TypeError', + message: + 'Header name must be a valid HTTP token ["invalid @ header ? name"]' + }); +} + +{ + // delete + const headers = new Headers(); + headers.append('test-name-1', 'test-value-1'); + headers.append('test-name-2', 'test-value-2'); + headers.append('test-name-3', 'test-value-3'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + 'test-name-2', + 'test-value-2', + 'test-name-3', + 'test-value-3', + ]); + headers.delete('test-name-2'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + 'test-name-3', + 'test-value-3', + ]); + headers.delete('does-not-exist'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + 'test-name-3', + 'test-value-3', + ]); + + assert.throws(() => { + headers.delete(); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["undefined"]' + }); + + assert.throws(() => { + headers.delete('invalid @ header ?'); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["invalid @ header ?"]' + }); +} + +{ + // get + const headers = new Headers(); + headers.append('test-name-1', 'test-value-1'); + headers.append('test-name-2', 'test-value-2'); + headers.append('test-name-3', 'test-value-3'); + assert.deepStrictEqual(headers.get('test-name-1'), 'test-value-1'); + assert.deepStrictEqual(headers.get('does-not-exist'), null); + headers.append('test-name-2', 'test-value-4'); + assert.deepStrictEqual(headers.get('test-name-2'), + 'test-value-2, test-value-4'); + + assert.throws(() => { + headers.get(); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["undefined"]' + }); + + assert.throws(() => { + headers.get('invalid @ header ?'); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["invalid @ header ?"]' + }); +} + +{ + // has + const headers = new Headers(); + headers.append('test-name-1', 'test-value-1'); + headers.append('test-name-2', 'test-value-2'); + headers.append('test-name-3', 'test-value-3'); + assert.strictEqual(headers.has('test-name-1'), true); + assert.strictEqual(headers.has('does-not-exist'), false); + + assert.throws(() => { + headers.has(); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["undefined"]' + }); + + assert.throws(() => { + headers.has('invalid @ header ?'); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["invalid @ header ?"]' + }); +} + +{ + // set + const headers = new Headers(); + headers.set('test-name-1', 'test-value-1'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + ]); + headers.set('test-name-2', 'test-value-2'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-1', + 'test-name-2', + 'test-value-2', + ]); + headers.set('test-name-1', 'test-value-3'); + assert.deepStrictEqual(headers[kHeadersList], [ + 'test-name-1', + 'test-value-3', + 'test-name-2', + 'test-value-2', + ]); + + assert.throws(() => { + headers.set(); + }, { + name: 'TypeError', + message: 'Header name must be a valid HTTP token ["undefined"]' + }); + + assert.throws(() => { + headers.set('test-name'); + }, { + name: 'TypeError', + message: 'Invalid value "undefined" for header "test-name"' + }); + + assert.throws(() => { + headers.set('invalid @ header ? name', 'test-value'); + }, { + name: 'TypeError', + message: + 'Header name must be a valid HTTP token ["invalid @ header ? name"]' + }); +} + +{ + // for each + const init = [ + ['a', '1'], + ['b', '2'], + ['c', '3'], + ['abc', '4'], + ['b', '5'], + ]; + const expected = [ + ['a', '1'], + ['abc', '4'], + ['b', '2, 5'], + ['c', '3'], + ]; + + const headers = new Headers(init); + const that = {}; + let i = 0; + headers.forEach(function(value, key, _headers) { + assert.deepStrictEqual(expected[i++], [key, value]); + assert.strictEqual(headers, _headers); + assert.strictEqual(this, that); + }, that); +} + +{ + // entries + const init = [ + ['a', '1'], + ['b', '2'], + ['c', '3'], + ['abc', '4'], + ['b', '5'], + ]; + const expected = [ + ['a', '1'], + ['abc', '4'], + ['b', '2, 5'], + ['c', '3'], + ]; + const headers = new Headers(init); + let i = 0; + for (const header of headers.entries()) { + assert.deepStrictEqual(header, expected[i++]); + } +} + +{ + // keys + const init = [ + ['a', '1'], + ['b', '2'], + ['c', '3'], + ['abc', '4'], + ['b', '5'], + ]; + const expected = ['a', 'abc', 'b', 'c']; + const headers = new Headers(init); + let i = 0; + for (const key of headers.keys()) { + assert.deepStrictEqual(key, expected[i++]); + } +} + +{ + // values + const init = [ + ['a', '1'], + ['b', '2'], + ['c', '3'], + ['abc', '4'], + ['b', '5'], + ]; + const expected = ['1', '4', '2, 5', '3']; + const headers = new Headers(init); + let i = 0; + for (const value of headers.values()) { + assert.deepStrictEqual(value, expected[i++]); + } +} + +{ + // for of + const init = [ + ['a', '1'], + ['b', '2'], + ['c', '3'], + ['abc', '4'], + ['b', '5'], + ]; + const expected = [ + ['a', '1'], + ['abc', '4'], + ['b', '2, 5'], + ['c', '3'], + ]; + let i = 0; + const headers = new Headers(init); + for (const header of headers) { + assert.deepStrictEqual(header, expected[i++]); + } +} + +{ + // 0 1 2 3 4 5 6 7 + const l1 = ['b', 1, 'c', 2, 'd', 3, 'f', 4]; + // 0 1 2 3 4 5 6 7 8 9 + const l2 = ['b', 1, 'c', 2, 'd', 3, 'e', 4, 'g', 5]; + // 0 1 2 3 4 5 6 7 + const l3 = ['a', 1, 'b', 2, 'bcd', 3, 'c', 4]; + // 0 1 2 3 4 5 6 7 8 9 + const l4 = ['a', 1, 'b', 2, 'c', 3, 'cde', 4, 'f', 5]; + + const tests = [ + { + input: [l1, 'c'], + expected: 2, + message: 'find item in n=even array' + }, + { + input: [l1, 'f'], + expected: 6, + message: 'find item at end of n=even array' + }, + { + input: [l1, 'b'], + expected: 0, + message: 'find item at beg of n=even array' + }, + { + input: [l1, 'e'], + expected: 6, + message: 'find new item position in n=even array' + }, + { + input: [l1, 'g'], + expected: 8, + message: 'find new item position at end of n=even array' + }, + { + input: [l1, 'a'], + expected: 0, + message: 'find new item position at beg of n=even array' + }, + { + input: [l2, 'c'], + expected: 2, + message: 'find item in n=odd array' + }, + { + input: [l2, 'g'], + expected: 8, + message: 'find item at end of n=odd array' + }, + { + input: [l2, 'b'], + expected: 0, + message: 'find item at beg of n=odd array' + }, + { + input: [l2, 'f'], + expected: 8, + message: 'find new item position in n=odd array' + }, + { + input: [l2, 'h'], + expected: 10, + message: 'find new item position at end of n=odd array' + }, + { + input: [l2, 'a'], + expected: 0, + message: 'find new item position at beg of n=odd array' + }, + { + input: [l3, 'b'], + expected: 2, + message: 'find item with similarity in n=odd array' + }, + { + input: [l3, 'bcd'], + expected: 4, + message: 'find item with similarity in n=odd array' + }, + { + input: [l4, 'c'], + expected: 4, + message: 'find item with similarity in n=odd array' + }, + { + input: [l4, 'cde'], + expected: 6, + message: 'find item with similarity in n=odd array' + }, + ]; + + tests.forEach(({ input: [list, target], expected, message }) => { + assert.deepStrictEqual(expected, binarySearch(list, target), message); + }); +} diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 481a35ca330c80..47cd9162731035 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -132,6 +132,8 @@ const customTypesMap = { 'Event': 'events.html#events_class_event', 'EventListener': 'events.html#events_event_listener', + 'Headers': 'fetch.html#fetch_class_headers', + 'FileHandle': 'fs.html#fs_class_filehandle', 'fs.Dir': 'fs.html#fs_class_fs_dir', 'fs.Dirent': 'fs.html#fs_class_fs_dirent',