UNPKG

@zkochan/pnpm

Version:

A fast implementation of npm install

293 lines (242 loc) 8.69 kB
// Copyright 2013 LinkedIn Corp. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var fs = require('fs'); var sepiaUtil = require('./util'); var EventEmitter = require('events').EventEmitter; var http = require('http'); var playbackHits = true; var recordMisses = true; module.exports.configure = function(mode) { switch (mode) { case 'record': playbackHits = false; recordMisses = true; break; case 'playback': playbackHits = true; recordMisses = false; break; case 'cache': playbackHits = true; recordMisses = true; break; default: throw new Error('Unrecognized mode: ' + mode); } }; ['http', 'https'].forEach(function(protocol) { var protocolModule = require(protocol); var oldRequest = protocolModule.request; // Ensure there are enough sockets to handle timeout issues that arise due to // slow servers or unaccessible servers when recording. protocolModule.globalAgent.maxSockets = 1000; protocolModule.request = function(options, callback) { var reqUrl = sepiaUtil.urlFromHttpRequestOptions(options, protocol); var reqBody = []; var debug = sepiaUtil.shouldFindMatchingFixtures(); var req = new EventEmitter(); req.setTimeout = req.abort = function() {}; req.write = function(chunk) { reqBody.push(chunk); }; req.end = function(lastChunk) { if (lastChunk) { reqBody.push(lastChunk); } reqBody = reqBody.map(function(chunk) { if (!Buffer.isBuffer(chunk)) { return new Buffer(chunk); } else { return chunk; } }); reqBody = Buffer.concat(reqBody); var filename = sepiaUtil.constructFilename(options.method, reqUrl, reqBody.toString(), options.headers); options.headers = sepiaUtil.removeInternalHeaders(options.headers); var forceLive = sepiaUtil.shouldForceLive(reqUrl); // Only called if either the fixture with the constructed filename // exists, or we're playing back passed in data. function playback(resHeaders, resBody) { if (!forceLive) { var headerContent = fs.readFileSync(filename + '.headers'); resHeaders = JSON.parse(headerContent); } var socket = new EventEmitter(); socket.setTimeout = socket.setEncoding = function() {}; // Needed for node 0.8.x socket.destroy = socket.pause = socket.resume = function() {}; req.socket = socket; req.emit('socket', socket); if (resHeaders.timeout) { socket.emit('timeout'); req.emit('error', new Error('Timeout')); return; } if (resHeaders.error) { req.emit('error', resHeaders.error); return; } var res = new http.IncomingMessage(socket); res.headers = resHeaders.headers || {}; res.statusCode = resHeaders.statusCode; if (callback) { callback(res); } if (!forceLive) { resBody = fs.readFileSync(filename); } req.emit('response', res); if (res.push) { // node 0.10.x res.push(resBody); res.push(null); } else { // node 0.8.x res.emit('data', resBody); res.emit('end'); } } // If the file exists and we allow playback (e.g. we are not in // record-only mode), then simply play back the call. if (playbackHits && !forceLive && fs.existsSync(filename + '.headers')) { playback(); return; } // If we are not recording, and the fixtures file does not exist, then // throw an exception. if (!recordMisses && !forceLive) { // But, create a .missing file before throwing the exception. var requestData = { url: reqUrl, method: options.method, headers: options.headers, body: reqBody.toString() }; var missingFileName = filename + '.missing'; fs.writeFileSync(missingFileName, JSON.stringify(requestData, null, 2)); if (debug) { var bestMatchFileName = sepiaUtil.findTheBestMatchingFixture(missingFileName); if (bestMatchFileName) { throw new Error('Fixture ' + filename + ' not found, Expected ' + missingFileName + ' , but the best match is ' + bestMatchFileName); } else { throw new Error('Fixture ' + filename + ' not found and could not compute the best matching fixture'); } } throw new Error('Fixture ' + filename + ' not found.'); } // Remember how long it took to perform this action. var startTime = Date.now(); var timedOut = false; function writeHeaderFile(headers) { var timeLength = Date.now() - startTime; headers.url = reqUrl; headers.time = timeLength; headers.request = { method: options.method, headers: options.headers }; fs.writeFileSync(filename + '.headers', JSON.stringify(headers, null, 2)); } // Suppose the request times out while recording. We don't want the // fixtures file to be missing; we want to send back a timeout on // playback. To accomplish this, we write a timeout to the .header file // pre-emptively, then overwrite it with the server response if the // request doesn't time out. if (!forceLive) { writeHeaderFile({ timeout: true, time: 30000 }); } var realReq = oldRequest(options, function(res) { // It's important that we don't respect the encoding set by // application because we want to treat the incoming data as a // buffer. When body data is treated as a string, there are issues // writing it to a file. With non-ASCII messages, the string's length // (in characters) is not necessarily the same as the buffer's length // (in bytes). Thus, the solution is to treat the data as a buffer // without allowing conversion into a string. res.setEncoding = function() {}; var resBodyChunks = []; res.on('data', function(chunk) { resBodyChunks.push(chunk); }); res.on('end', function() { var resBody = Buffer.concat(resBodyChunks); if (forceLive) { // Don't write the response to any files, and just send it back to // whoever issued the request. playback({ statusCode: res.statusCode, headers: res.headers }, resBody); } else { fs.writeFileSync(filename, resBody); // Store the request, if debug is true if (debug) { var requestData = { url: reqUrl, method: options.method, headers: options.headers, body: reqBody.toString() }; writeRequestFile(requestData, filename); } writeHeaderFile({ statusCode: res.statusCode, headers: res.headers }); playback(); } }); }); realReq.on('error', function(error) { var header = { error: error }; if (timedOut) { header.timeout = true; } if (forceLive) { // Don't write the error to a file, and just send it back to whoever // issued the request. playback(undefined, header); } else { writeHeaderFile(header); playback(); } }); realReq.on('socket', function(socket) { socket.on('timeout', function() { timedOut = true; }); }); realReq.end(reqBody); }; return req; }; }); function writeRequestFile(requestData, filename) { fs.writeFileSync(filename + '.request', JSON.stringify(requestData, null, 2)); } module.exports.internal = {}; module.exports.internal.writeRequestFile = writeRequestFile;