UNPKG

@whatwg-node/node-fetch

Version:

Fetch API implementation for Node

346 lines (345 loc) • 13.1 kB
"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;