@whatwg-node/node-fetch
Version:
Fetch API implementation for Node
346 lines (345 loc) • 13.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PonyfillHeaders = void 0;
exports.isHeadersLike = isHeadersLike;
const node_util_1 = require("node:util");
const IteratorObject_js_1 = require("./IteratorObject.js");
function isHeadersLike(headers) {
return headers?.get && headers?.forEach;
}
class PonyfillHeaders {
headersInit;
_map;
objectNormalizedKeysOfHeadersInit = [];
objectOriginalKeysOfHeadersInit = [];
_setCookies;
constructor(headersInit) {
this.headersInit = headersInit;
}
// perf: we don't need to build `this.map` for Requests, as we can access the headers directly
_get(key) {
const normalized = key.toLowerCase();
if (normalized === 'set-cookie' && this._setCookies?.length) {
return this._setCookies.join(', ');
}
// If the map is built, reuse it
if (this._map) {
const mapped = this._map.get(normalized);
return mapped === undefined ? null : mapped;
}
// If the map is not built, try to get the value from the this.headersInit
if (this.headersInit == null) {
return null;
}
if (Array.isArray(this.headersInit)) {
const found = this.headersInit.filter(([headerKey]) => headerKey.toLowerCase() === normalized);
if (found.length === 0) {
return null;
}
if (found.length === 1) {
return found[0][1];
}
return found.map(([, value]) => value).join(', ');
}
else if (isHeadersLike(this.headersInit)) {
return this.headersInit.get(normalized);
}
else {
const initValue = this.headersInit[key] || this.headersInit[normalized];
if (initValue != null) {
return initValue;
}
if (!this.objectNormalizedKeysOfHeadersInit.length) {
Object.keys(this.headersInit).forEach(k => {
this.objectOriginalKeysOfHeadersInit.push(k);
this.objectNormalizedKeysOfHeadersInit.push(k.toLowerCase());
});
}
const index = this.objectNormalizedKeysOfHeadersInit.indexOf(normalized);
if (index === -1) {
return null;
}
const originalKey = this.objectOriginalKeysOfHeadersInit[index];
return this.headersInit[originalKey];
}
}
// perf: Build the map of headers lazily, only when we need to access all headers or write to it.
// I could do a getter here, but I'm too lazy to type `getter`.
getMap() {
if (!this._map) {
this._setCookies ||= [];
if (this.headersInit != null) {
if (Array.isArray(this.headersInit)) {
this._map = new Map();
for (const [key, value] of this.headersInit) {
const normalizedKey = key.toLowerCase();
if (normalizedKey === 'set-cookie') {
if (Array.isArray(value)) {
this._setCookies.push(...value);
}
else if (value != null) {
this._setCookies.push(value);
}
continue;
}
const existingValue = this._map.get(normalizedKey);
this._map.set(normalizedKey, existingValue === undefined ? value : `${existingValue}, ${value}`);
}
}
else if (isHeadersLike(this.headersInit)) {
this._map = new Map();
this.headersInit.forEach((value, key) => {
if (key === 'set-cookie') {
this._setCookies ||= [];
if (Array.isArray(value)) {
this._setCookies.push(...value);
}
else if (value != null) {
this._setCookies.push(value);
}
return;
}
this._map.set(key, value);
});
}
else {
this._map = new Map();
for (const initKey in this.headersInit) {
const initValue = this.headersInit[initKey];
if (initValue != null) {
const normalizedKey = initKey.toLowerCase();
if (normalizedKey === 'set-cookie') {
this._setCookies ||= [];
if (Array.isArray(initValue)) {
this._setCookies.push(...initValue);
continue;
}
this._setCookies.push(initValue);
continue;
}
const existingValue = this._map.get(normalizedKey);
this._map.set(normalizedKey, existingValue === undefined ? initValue : `${existingValue}, ${initValue}`);
}
}
}
}
else {
this._map = new Map();
}
}
return this._map;
}
// A sequence can repeat a name, so it is iterated through the map, which combines the values
buildMapForSequence() {
if (!this._map && Array.isArray(this.headersInit)) {
this.getMap();
}
}
append(name, value) {
const key = name.toLowerCase();
if (key === 'set-cookie') {
// Materialize initializer cookies before mutating them.
this.getMap();
this._setCookies ||= [];
this._setCookies.push(value);
return;
}
const existingValue = this.getMap().get(key);
const finalValue = existingValue === undefined ? value : `${existingValue}, ${value}`;
this.getMap().set(key, finalValue);
}
get(name) {
const value = this._get(name);
if (value == null) {
return null;
}
return value.toString();
}
has(name) {
const key = name.toLowerCase();
if (key === 'set-cookie') {
return !!this._setCookies?.length;
}
return this._get(name) !== null;
}
set(name, value) {
const key = name.toLowerCase();
if (key === 'set-cookie') {
// Materialize initializer cookies before replacing them.
this.getMap();
this._setCookies = [value];
return;
}
// A sequence can repeat a name, so it goes through the map
if (!this._map && this.headersInit != null && !Array.isArray(this.headersInit)) {
if (isHeadersLike(this.headersInit)) {
this.headersInit.set(key, value);
return;
}
else {
this.headersInit[key] = value;
return;
}
}
this.getMap().set(key, value);
}
delete(name) {
const key = name.toLowerCase();
if (key === 'set-cookie') {
// Materialize initializer cookies before clearing them.
this.getMap();
this._setCookies = [];
return;
}
this.getMap().delete(key);
}
// Fetch iterates the result of "sort and combine": names ascending by byte order.
*sortedEntries() {
this.buildMapForSequence();
if (!this._map) {
if (this.headersInit) {
const names = [];
const valuesByName = new Map();
const addValue = (name, value) => {
let values = valuesByName.get(name);
if (!values) {
values = [];
valuesByName.set(name, values);
names.push(name);
}
values.push(value);
};
if (isHeadersLike(this.headersInit)) {
this.headersInit.forEach((value, key) => {
addValue(key.toLowerCase(), value);
});
}
else {
const record = this.headersInit;
for (const key of Object.keys(record)) {
const value = record[key];
if (value != null) {
addValue(key.toLowerCase(), value);
}
}
}
// Appended cookies can exist without building the map (keeps writeHead fast path).
if (this._setCookies?.length) {
for (const cookie of this._setCookies) {
addValue('set-cookie', cookie);
}
}
names.sort();
for (const name of names) {
// A forEach callback may build the map via delete/set; honor live state.
// Cast: this block started with !_map, but mutations can assign it.
const liveMap = this._map;
if (liveMap) {
if (name === 'set-cookie') {
if (!this._setCookies?.length) {
continue;
}
for (const cookie of this._setCookies) {
yield ['set-cookie', cookie];
}
continue;
}
const live = liveMap.get(name);
if (live === undefined) {
continue;
}
yield [name, live];
continue;
}
const values = valuesByName.get(name);
if (name === 'set-cookie') {
for (const value of values) {
yield [name, value];
}
}
else {
yield [name, values.join(', ')];
}
}
return;
}
// set-cookie may exist without a map (append/set on an empty Headers)
if (this._setCookies?.length) {
for (const cookie of this._setCookies) {
yield ['set-cookie', cookie];
}
}
return;
}
const names = [...this._map.keys()];
if (this._setCookies?.length) {
names.push('set-cookie');
}
names.sort();
for (const name of names) {
if (name === 'set-cookie') {
if (!this._setCookies?.length) {
continue;
}
for (const cookie of this._setCookies) {
yield ['set-cookie', cookie];
}
continue;
}
const value = this._map.get(name);
if (value === undefined) {
continue;
}
yield [name, value];
}
}
forEach(callback) {
for (const [key, value] of this.sortedEntries()) {
callback(value, key, this);
}
}
*_keys() {
for (const [key] of this.sortedEntries()) {
yield key;
}
}
keys() {
return new IteratorObject_js_1.PonyfillIteratorObject(this._keys(), 'HeadersIterator');
}
*_values() {
for (const [, value] of this.sortedEntries()) {
yield value;
}
}
values() {
return new IteratorObject_js_1.PonyfillIteratorObject(this._values(), 'HeadersIterator');
}
*_entries() {
yield* this.sortedEntries();
}
entries() {
return new IteratorObject_js_1.PonyfillIteratorObject(this._entries(), 'HeadersIterator');
}
getSetCookie() {
if (!this._setCookies) {
this.getMap();
}
return this._setCookies;
}
[Symbol.iterator]() {
return this.entries();
}
[Symbol.for('nodejs.util.inspect.custom')]() {
const record = {};
this.forEach((value, key) => {
if (key === 'set-cookie') {
record['set-cookie'] = this._setCookies || [];
}
else {
record[key] = value?.includes(',') ? value.split(',').map(el => el.trim()) : value;
}
});
return `Headers ${(0, node_util_1.inspect)(record)}`;
}
}
exports.PonyfillHeaders = PonyfillHeaders;