protoblast
Advanced tools
Comparing version 0.7.9 to 0.7.10
@@ -0,1 +1,12 @@ | ||
## 0.7.10 (2021-08-07) | ||
* Add `Request.lookup()` method which caches DNS lookups for 60 seconds | ||
* Add more `Request` methods so no external `xhr` access is required | ||
* Add `Request` caching | ||
* Add `Blast.mkdirp()` and `Blast.mkdirpSync()` methods | ||
* Add `Blast.rmrf()` and `Blast.rmrfSync()` methods | ||
* Add temporary files methods: `Blast.generateTempPath()`, `Blast.openTempFile()`, `Blast.cleanupTempPaths()`, ... | ||
* Fix the `String#replaceAll()` polyfill | ||
* Fix `TimeoutPledge` not actually running its executor | ||
## 0.7.9 (2021-06-11) | ||
@@ -2,0 +13,0 @@ |
@@ -81,3 +81,3 @@ var tokenMatches, | ||
comment1 : /\/\*[\s\S]*?\*\//, | ||
comment2 : /\/\/.*?\n/, | ||
comment2 : /\/\/.*?(?=\r\n|\r|\n|$)/, | ||
number : /\d+(?:\.\d+)?(?:e[+-]?\d+)?/, | ||
@@ -84,0 +84,0 @@ parens : /[\(\)]/, |
499
lib/init.js
@@ -300,5 +300,2 @@ module.exports = function BlastInitLoader(modifyPrototype) { | ||
Blast.ACTIVE_FILE = Symbol('active_file'); | ||
// We break this string up so the Blast.convertCoverage doesn't find this | ||
var source_map_url_prefix = '//# sourceMappingURL' + '=data:application/json;charset=utf-8;base64,'; | ||
} | ||
@@ -379,3 +376,3 @@ // PROTOBLAST END CUT | ||
// when unit testing | ||
if (module.exports.unit_test) { | ||
if (module.exports.unit_test || Blast.Globals.__is_protoblast_unit_test) { | ||
Blast.Shims = {}; | ||
@@ -676,3 +673,3 @@ } | ||
// In node, every global is an own property of the `global` object | ||
Globals[name] = constructor; | ||
Globals[name] = value; | ||
} else { | ||
@@ -953,464 +950,3 @@ // In the browser, it's mostly a property of the window prototype | ||
//PROTOBLAST START CUT | ||
let os = require('os'), | ||
tmpdir = fs.mkdtempSync(libpath.resolve(os.tmpdir(), 'protoblast')), | ||
cache = {}; | ||
/** | ||
* Server side: create client side file | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.1.1 | ||
* @version 0.7.2 | ||
* | ||
* @param {Object} options | ||
* | ||
* @return {Pledge} | ||
*/ | ||
Blast.getClientPath = function getClientPath(options) { | ||
var refresh = false, | ||
ua, | ||
id; | ||
if (!options) { | ||
options = {}; | ||
} else { | ||
if (options.ua) { | ||
ua = Blast.parseUseragent(options.ua); | ||
id = ua.family + '-' + ua.major + '.' + ua.minor; | ||
} | ||
if (options.refresh) { | ||
refresh = true; | ||
} | ||
} | ||
let create_source_map = options.create_source_map, | ||
enable_coverage = options.enable_coverage; | ||
// If the source-map module couldn't be loaded, ignore it | ||
if (Blast.sourceMap === false) { | ||
create_source_map = false; | ||
} | ||
// If we want to add coverage, the sourcemap is required! | ||
if (enable_coverage) { | ||
create_source_map = true; | ||
if (!Blast.instrumentSource) { | ||
require('./coverage.js'); | ||
} | ||
} | ||
// If we want to make a sourcemap, but the module hasn't been loaded | ||
// try to load it now | ||
if (create_source_map && Blast.sourceMap == null) { | ||
try { | ||
Blast.sourceMap = require('source-map'); | ||
} catch (err) { | ||
create_source_map = false; | ||
Blast.sourceMap = false; | ||
} | ||
} | ||
if (!id) { | ||
id = 'full'; | ||
} | ||
if (options.use_common) { | ||
id = 'common_' + id; | ||
} else if (options.modify_prototypes) { | ||
id = 'global_' + id; | ||
} | ||
if (enable_coverage) { | ||
id += '_cov'; | ||
} | ||
if (cache[id] && !refresh) { | ||
return cache[id]; | ||
} | ||
let extra_files = [], | ||
compose_id = '', | ||
extra, | ||
i; | ||
// Now iterate over the extras | ||
for (i = 0; i < extras.length; i++) { | ||
extra = extras[i]; | ||
if (!extra.client) { | ||
continue; | ||
} | ||
// See if we've been given a useragent | ||
if (ua && extra.versions && id != 'full' && id != 'full_common') { | ||
let entry = extra.versions[ua.family]; | ||
// If the user's browser version is higher than the required max, | ||
// it is also not needed | ||
if (entry && ua.version.float > entry.max) { | ||
continue; | ||
} | ||
} | ||
extra_files.push(extra); | ||
compose_id += i + '-'; | ||
} | ||
compose_id = Blast.Bound.Object.checksum(compose_id); | ||
if (enable_coverage) { | ||
compose_id += '_cov'; | ||
} | ||
if (cache[compose_id] && !refresh) { | ||
cache[id] = cache[compose_id]; | ||
return cache[id]; | ||
} | ||
let files = [ | ||
'init', | ||
'json-dry', | ||
]; | ||
let code = '', | ||
tasks = []; | ||
// The first file should be the template | ||
tasks.push(Blast.getCachedFile('client.js')); | ||
// Queue some basic, pre-wrapped files | ||
files.forEach(function eachFile(name, index) { | ||
var path; | ||
name = name.toLowerCase(); | ||
if (name == 'json-dry') { | ||
path = require.resolve('json-dry'); | ||
} else { | ||
path = libpath.resolve(__dirname, name + '.js'); | ||
} | ||
tasks.push(function getFile(next) { | ||
Blast.getCachedFile(path).then(function gotCode(code) { | ||
let filename = name + '.js'; | ||
var data = 'require.register("' + filename + '", function(module, exports, require){\n'; | ||
data += code; | ||
data += '});\n'; | ||
let result = { | ||
start : 1, // Starts at 1 for the `require` line | ||
code : data, | ||
filename : filename, | ||
name_id : name, | ||
name : name, | ||
path : path, | ||
source : null | ||
}; | ||
if (create_source_map) { | ||
result.source = code; | ||
} | ||
next(null, result); | ||
}).catch(next); | ||
}); | ||
}); | ||
extra_files.forEach(function eachExtraFile(options) { | ||
tasks.push(function getExtraFile(next) { | ||
Blast.getCachedFile(options.path).then(function gotCode(code) { | ||
let source, | ||
start = 0; | ||
if (create_source_map) { | ||
source = code; | ||
} | ||
if (options.add_wrapper !== false) { | ||
if (options.add_wrapper || code.slice(0, 14) != 'module.exports') { | ||
// Add 1 line for the `register` line | ||
start++; | ||
let data = 'module.exports = function('; | ||
if (options.arguments) { | ||
data += Blast.getArgumentConfiguration(options.arguments).names.join(','); | ||
} else { | ||
data += 'Blast, Collection, Bound, Obj, Fn'; | ||
} | ||
data += ') {\n'; | ||
code = data + code + '\n};'; | ||
} | ||
} | ||
let name = options.name_id || options.name, | ||
filename = libpath.basename(options.path); | ||
code = 'require.register("' + name + '", function(module, exports, require){\n' | ||
+ code | ||
+ '});\n'; | ||
// Add 1 line for the `require` line | ||
start++; | ||
let result = { | ||
start : start, | ||
code : code, | ||
filename : filename, | ||
name : options.name, | ||
name_id : options.name_id, | ||
path : options.path, | ||
source : null | ||
}; | ||
if (create_source_map) { | ||
result.source = source; | ||
} | ||
next(null, result); | ||
}).catch(next); | ||
}); | ||
}); | ||
cache[id] = new Blast.Classes.Pledge(); | ||
cache[compose_id] = cache[id]; | ||
Blast.Bound.Function.parallel(tasks, function gotFiles(err, files) { | ||
if (err) { | ||
return cache[id].reject(err); | ||
} | ||
let current_line, | ||
sourcemap, | ||
template = files.shift(), | ||
index = template.indexOf('//_REGISTER_//'), | ||
filename = libpath.resolve(tmpdir, compose_id + '.js'), | ||
code = '', | ||
file, | ||
i; | ||
let template_start = template.slice(0, index), | ||
template_offset = Blast.Bound.String.count(template_start, '\n'); | ||
if (create_source_map) { | ||
sourcemap = new Blast.sourceMap.SourceMapGenerator({ | ||
file : compose_id + '.js', | ||
sourceRoot : '' | ||
}); | ||
} | ||
for (i = 0; i < files.length; i++) { | ||
file = files[i]; | ||
if (code) { | ||
code += '\n'; | ||
} | ||
if (create_source_map) { | ||
// Count the current line we're on | ||
current_line = template_offset + Blast.Bound.String.count(code, '\n'); | ||
} | ||
if (typeof file == 'string') { | ||
let path = libpath.resolve(__dirname, 'client.js'); | ||
code += file; | ||
if (create_source_map) { | ||
// Ugly hack for the client.js file | ||
file = { | ||
start : 0, | ||
code : file, | ||
source : file, | ||
path : path, | ||
name : 'blast_template_client.js' | ||
}; | ||
} | ||
} else { | ||
code += file.code; | ||
} | ||
if (create_source_map) { | ||
let filename = file.name; | ||
if (filename.indexOf('.js') == -1) { | ||
filename += '.js'; | ||
} | ||
filename = file.path; | ||
sourcemap.setSourceContent(filename, file.source); | ||
let target_line = current_line + (file.start || 0), | ||
end_column, | ||
char_start, | ||
char_end, | ||
tokens, | ||
lines = file.source.split('\n'), | ||
token, | ||
i, | ||
j; | ||
for (i = 0; i < lines.length; i++) { | ||
line = lines[i]; | ||
tokens = Blast.Bound.Function.tokenize(line, false); | ||
char_start = 0; | ||
for (j = 0; j < tokens.length; j++) { | ||
char_end = char_start + tokens[j].length; | ||
sourcemap.addMapping({ | ||
source : filename, | ||
original : {line: 1 + i, column: char_start}, | ||
generated : {line: target_line + i + 1, column: char_start}, | ||
name : tokens[j] | ||
}); | ||
char_start = char_end; | ||
} | ||
} | ||
} | ||
} | ||
if (options.use_common) { | ||
code += '\nuse_common = true;\n'; | ||
} else if (options.modify_prototypes) { | ||
code += '\nmodify_prototypes = true;\n'; | ||
} | ||
let client_extras = []; | ||
extra_files.forEach(function eachExtraFile(options) { | ||
if (options.client === false || options.is_extra === false) { | ||
return; | ||
} | ||
client_extras.push([options.name_id, options.arguments]); | ||
}); | ||
code += '\nclient_extras = ' + JSON.stringify(client_extras) + ';\n'; | ||
template = template_start + code + template.slice(index); | ||
let cut_rx = /\/\/\s?PROTOBLAST\s?START\s?CUT([\s\S]*?)(\/\/\s?PROTOBLAST\s?END\s?CUT)/gm; | ||
// Remove everything between "PROTOBLAST START CUT" and "PROTOBLAST END CUT" (with slashes) | ||
if (create_source_map) { | ||
// Instead of actually cutting the code when making a sourcemap, | ||
// the code is commented | ||
template = template.replace(cut_rx, function doReplace(match) { | ||
let result = '', | ||
lines = match.split('\n'), | ||
line, | ||
i; | ||
for (i = 0; i < lines.length; i++) { | ||
if (i) { | ||
result += '\n'; | ||
} | ||
line = lines[i]; | ||
result += '// ' + line; | ||
} | ||
return result; | ||
}); | ||
let sourcemap_64 = Buffer.from(sourcemap.toString()).toString('base64'); | ||
let inline_source_map = source_map_url_prefix + sourcemap_64; | ||
template += '\n' + inline_source_map; | ||
} else { | ||
template = template.replace(cut_rx, ''); | ||
} | ||
if (enable_coverage) { | ||
template = Blast.instrumentSource(template, 'test_path.js', JSON.parse(sourcemap.toString())).code; | ||
} | ||
let retries = 0; | ||
function retryWithTempdir(filename, template) { | ||
retries++; | ||
fs.mkdtemp(libpath.resolve(os.tmpdir(), 'protoblast'), function madeDir(err, result) { | ||
if (err) { | ||
return cache[id].reject(err); | ||
} | ||
tmpdir = result; | ||
filename = libpath.resolve(tmpdir, compose_id + '.js') | ||
writeFile(filename, template); | ||
}); | ||
} | ||
function writeFile(filename, template) { | ||
fs.writeFile(filename, template, function written(err) { | ||
if (err) { | ||
if (retries == 0) { | ||
return retryWithTempdir(filename, template); | ||
} | ||
return cache[id].reject(err); | ||
} | ||
cache[id].resolve(filename); | ||
}); | ||
} | ||
writeFile(filename, template); | ||
}); | ||
return cache[id]; | ||
}; | ||
/** | ||
* Get a file and cache it | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.7.0 | ||
* @version 0.7.0 | ||
* | ||
* @param {String} path | ||
* | ||
* @return {Promise} | ||
*/ | ||
Blast.getCachedFile = function getCachedFile(path) { | ||
if (path[0] != '/') { | ||
path = libpath.resolve(__dirname, path); | ||
} | ||
return new Promise(function doReadFile(resolve, reject) { | ||
fs.readFile(path, 'utf8', function gotResult(err, data) { | ||
if (err) { | ||
return reject(err); | ||
} | ||
resolve(data); | ||
}); | ||
}); | ||
}; | ||
require('./server_functions.js')(Blast, extras); | ||
//PROTOBLAST END CUT | ||
@@ -1642,2 +1178,31 @@ | ||
/** | ||
* Do a synchronous sleep (blocking the eventloop!) | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @param {number} duration | ||
*/ | ||
Blast.sleepSync = function sleepSync(duration) { | ||
const valid = duration < Infinity; | ||
if (!valid) { | ||
throw new Error('Unable to sleep, duration is not valid: "' + duration + '"'); | ||
} | ||
if (typeof SharedArrayBuffer !== 'undefined' && typeof Atomics !== 'undefined') { | ||
const nil = new Int32Array(new SharedArrayBuffer(4)); | ||
Atomics.wait(nil, 0, 0, Number(duration)); | ||
} else { | ||
const target = Date.now() + Number(duration); | ||
while (target > Date.now()) { | ||
// Let's go CPU! | ||
} | ||
} | ||
}; | ||
//PROTOBLAST START CUT | ||
@@ -1644,0 +1209,0 @@ /** |
@@ -974,3 +974,3 @@ var PENDING = 0, | ||
* @since 0.7.1 | ||
* @version 0.7.1 | ||
* @version 0.7.10 | ||
* | ||
@@ -991,4 +991,10 @@ * @param {Function} executor | ||
this.executor = executor; | ||
if (typeof timeout != 'number') { | ||
timeout = +timeout; | ||
} | ||
if (!timeout) { | ||
timeout = 0; | ||
} | ||
setTimeout(function checkTimeout() { | ||
@@ -1009,2 +1015,4 @@ | ||
}, timeout); | ||
TimeoutPledge.super.call(this, executor); | ||
}); | ||
@@ -1011,0 +1019,0 @@ |
@@ -7,2 +7,74 @@ var Request = Blast.Classes.Develry.Request, | ||
/** | ||
* The XMLHttpRequest instance, | ||
* which is doing the actual browser-side request | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {XMLHttpRequest} | ||
*/ | ||
Request.setProperty('xhr', null); | ||
/** | ||
* The original XHR response instance | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {Response} | ||
*/ | ||
Request.setProperty('xhr_res', null); | ||
/** | ||
* Allow parsing responses as JSON-Dry by default | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {boolean} | ||
*/ | ||
Request.setProperty('allow_json_dry_response', true); | ||
/** | ||
* Get the current response status | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {number} | ||
*/ | ||
Request.setProperty(function status() { | ||
if (this.xhr) { | ||
return this.xhr.status; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.status; | ||
} | ||
}); | ||
/** | ||
* Get the current response status message | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {string} | ||
*/ | ||
Request.setProperty(function status_message() { | ||
if (this.xhr) { | ||
return this.xhr.statusText; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.status_message; | ||
} | ||
}); | ||
/** | ||
* Actually make the request | ||
@@ -50,2 +122,3 @@ * | ||
pledge.cancel = function cancel() { | ||
that.cancelled = true; | ||
xhr.abort(); | ||
@@ -155,4 +228,6 @@ }; | ||
response = xhr.response || xhr.responseText; | ||
that.response = response; | ||
that.xhr_res = response; | ||
type = xhr.getResponseHeader('content-type') || ''; | ||
that.status = xhr.status; | ||
that.statusText = xhr.statusText; | ||
@@ -284,23 +359,8 @@ if (type && type.indexOf(';') > -1) { | ||
if (!error && xhr.status > 399) { | ||
error = new Error(xhr.statusText); | ||
error.status = error.number = xhr.status; | ||
} | ||
let parsed = that._parseResponse(result, error); | ||
if (type && type.indexOf('json') > -1 && result) { | ||
try { | ||
result = Collection.JSON.undry(result); | ||
} catch (err) { | ||
console.error('Error parsing JSON string from "' + that.url.path + '":', result, err); | ||
return pledge.reject(err); | ||
} | ||
} | ||
if (error) { | ||
error.result = result; | ||
pledge.reject(error); | ||
that.error = error; | ||
if (parsed.error) { | ||
pledge.reject(parsed.error); | ||
} else { | ||
that.result = result; | ||
pledge.resolve(result); | ||
pledge.resolve(parsed.result); | ||
Blast.state.reportSuccess(); | ||
@@ -337,9 +397,36 @@ } | ||
* @since 0.7.0 | ||
* @version 0.7.0 | ||
* @version 0.7.10 | ||
*/ | ||
Request.setMethod(function getResponseHeader(name) { | ||
return this.xhr.getResponseHeader(name); | ||
if (this.xhr) { | ||
return this.xhr.getResponseHeader(name); | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.getResponseHeader(name); | ||
} | ||
}); | ||
/** | ||
* Get all the response header | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @return {object} | ||
*/ | ||
Request.setMethod(function getAllResponseHeaders() { | ||
if (this.xhr) { | ||
return this.xhr.getAllResponseHeaders(); | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.getAllResponseHeaders(); | ||
} | ||
}); | ||
/** | ||
* Hook into the original open method | ||
@@ -346,0 +433,0 @@ * |
@@ -1,5 +0,7 @@ | ||
var Request = Blast.Classes.Develry.Request, | ||
var dns_cache, | ||
Request = Blast.Classes.Develry.Request, | ||
https, | ||
http, | ||
zlib; | ||
zlib, | ||
dns; | ||
@@ -10,2 +12,3 @@ if (Blast.isNW) { | ||
zlib = nw.require('zlib'); | ||
dns = nw.require('dns'); | ||
} else { | ||
@@ -15,5 +18,135 @@ https = require('https'); | ||
zlib = require('zlib'); | ||
dns = require('dns'); | ||
} | ||
/** | ||
* Server-side DNS lookup with caching | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
*/ | ||
Request.setStatic(function lookup(hostname, options, callback) { | ||
let start = Date.now(); | ||
if (!dns_cache) { | ||
dns_cache = new Blast.Classes.Develry.Cache({ | ||
max_age : 60 * 1000, | ||
}); | ||
} | ||
if (typeof options == 'function') { | ||
callback = options; | ||
options = {}; | ||
} | ||
let key = Blast.Bound.Object.checksum([hostname, options]), | ||
result; | ||
if (dns_cache.has(key)) { | ||
result = dns_cache.get(key); | ||
return resolve(result); | ||
} | ||
let pledge = new Blast.Classes.Pledge(); | ||
dns_cache.set(key, pledge); | ||
dns.lookup(hostname, options, (...response) => { | ||
dns_cache.set(key, response); | ||
resolve(response); | ||
}); | ||
function resolve(result) { | ||
let dur = Date.now() - start; | ||
// If it's an array, we can callback | ||
if (Blast.Classes.Array.isArray(result)) { | ||
return Blast.setImmediate(() => { | ||
callback(...result); | ||
}); | ||
} | ||
// If not, it's thennable and we need to wait for it | ||
Blast.Classes.Pledge.done(result, (err, ...result) => { | ||
callback(...result); | ||
}); | ||
} | ||
}); | ||
/** | ||
* The ClientRequest instance: | ||
* the Node.js outgoing request | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {ClientRequest} | ||
*/ | ||
Request.setProperty('outgoing_req', null); | ||
/** | ||
* The IncomingMessage instance: | ||
* the Node.js response to the ClientRequest | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {IncomingMessage} | ||
*/ | ||
Request.setProperty('incoming_res', null); | ||
/** | ||
* Do not allow parsing responses as JSON-Dry by default | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {boolean} | ||
*/ | ||
Request.setProperty('allow_json_dry_response', false); | ||
/** | ||
* Get the current response status | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {number} | ||
*/ | ||
Request.setProperty(function status() { | ||
if (this.incoming_res) { | ||
return this.incoming_res.statusCode; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.status; | ||
} | ||
}); | ||
/** | ||
* Get the current response status message | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {string} | ||
*/ | ||
Request.setProperty(function status_message() { | ||
if (this.incoming_res) { | ||
return this.incoming_res.statusMessage; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.status_message; | ||
} | ||
}); | ||
/** | ||
* Actually make a request | ||
@@ -37,4 +170,3 @@ * | ||
body = this.body, | ||
url, | ||
req; | ||
url; | ||
@@ -57,3 +189,4 @@ if (options) { | ||
headers : this.headers, | ||
method : method.method | ||
method : method.method, | ||
lookup : Request.lookup | ||
}; | ||
@@ -91,4 +224,6 @@ | ||
// Create the request | ||
req = protocol.request(config, function gotResponse(res) { | ||
this.outgoing_req = protocol.request(config, (res) => { | ||
this.incoming_res = res; | ||
var output, | ||
@@ -102,3 +237,3 @@ gzip, | ||
// Follow redirects if there are any | ||
if (res.statusCode > 299 && res.statusCode < 400) { | ||
if (this.status > 299 && this.status < 400) { | ||
@@ -117,4 +252,2 @@ // Increase the redirect count | ||
that.response = res; | ||
// If an error occurs, call the callback with it | ||
@@ -144,33 +277,4 @@ res.on('error', function gotResponseError(err) { | ||
output.on('end', function ended() { | ||
var error_data, | ||
error; | ||
if (res.headers['content-type'] && (~res.headers['content-type'].indexOf('json'))) { | ||
body = Blast.Bound.JSON.safeParse(body); | ||
} | ||
if (res.statusCode >= 400) { | ||
error = res.statusCode + ' - ' + res.statusMessage + '\n'; | ||
if (body && typeof body == 'object') { | ||
if (body.code) { | ||
error += ' Body error code: ' + body.code + '\n'; | ||
} | ||
if (body.message) { | ||
error += ' Body error message: ' + body.message + '\n'; | ||
} | ||
} | ||
error += 'on ' + config.method + ' ' + String(url) + '\n'; | ||
error = new Error(error); | ||
error.request = that; | ||
error.result = body; | ||
} else { | ||
error = null; | ||
} | ||
done(error, body); | ||
let parsed = that._parseResponse(body); | ||
done(parsed.error, parsed.result); | ||
}); | ||
@@ -180,3 +284,3 @@ }); | ||
// Listen for request errors | ||
req.on('error', function onRequestError(err) { | ||
this.outgoing_req.on('error', function onRequestError(err) { | ||
done(err); | ||
@@ -187,5 +291,5 @@ }); | ||
if (is_form) { | ||
handleFormData(body, req); | ||
handleFormData(body, this.outgoing_req); | ||
} else { | ||
req.write(body); | ||
this.outgoing_req.write(body); | ||
} | ||
@@ -196,3 +300,3 @@ } | ||
if (!is_form) { | ||
req.end(); | ||
this.outgoing_req.end(); | ||
} | ||
@@ -220,2 +324,44 @@ | ||
/** | ||
* Get a response header | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @param {string} | ||
* | ||
* @return {string|undefined} | ||
*/ | ||
Request.setMethod(function getResponseHeader(name) { | ||
if (this.incoming_res) { | ||
return this.incoming_res.headers[name]; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.getResponseHeader(name); | ||
} | ||
}); | ||
/** | ||
* Get all the response header | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @return {object} | ||
*/ | ||
Request.setMethod(function getAllResponseHeaders() { | ||
if (this.incoming_res) { | ||
return this.incoming_res.headers; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.getAllResponseHeaders(); | ||
} | ||
}); | ||
/** | ||
* Handle server-side form data submission | ||
@@ -222,0 +368,0 @@ * |
@@ -1,3 +0,5 @@ | ||
var method_symbol = Symbol('method'), | ||
url_symbol = Symbol('url'); | ||
const ORIGINAL_REQUEST = Symbol('ori_req'), | ||
MAKE_REQUEST = Symbol('make_request'), | ||
METHOD = Symbol('method'), | ||
URL = Symbol('url'); | ||
@@ -52,2 +54,17 @@ /** | ||
/** | ||
* A cache instance used for caching GET requests | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @return {Cache} | ||
*/ | ||
Request.prepareStaticProperty(function cache() { | ||
return new Blast.Classes.Develry.Cache({ | ||
max_age : 60 * 1000, | ||
}); | ||
}); | ||
/** | ||
* Method info | ||
@@ -128,2 +145,35 @@ * | ||
/** | ||
* The original response data | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {*} | ||
*/ | ||
Request.setProperty('raw_response_body', null); | ||
/** | ||
* The parent request we're caching | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {Develry.Request} | ||
*/ | ||
Request.setProperty('cached_request', null); | ||
/** | ||
* Was this request cancelled? | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {boolean} | ||
*/ | ||
Request.setProperty('cancelled', false); | ||
/** | ||
* Default timeout in ms | ||
@@ -151,2 +201,13 @@ * | ||
/** | ||
* Allow cached responses? | ||
* | ||
* @author Jelle De Loecker <jelle@develry.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {boolean|number|null} | ||
*/ | ||
Request.setProperty('cache', null); | ||
/** | ||
* Refer to the time_started | ||
@@ -175,4 +236,4 @@ * | ||
if (this[method_symbol]) { | ||
return this[method_symbol]; | ||
if (this[METHOD]) { | ||
return this[METHOD]; | ||
} | ||
@@ -189,3 +250,3 @@ | ||
return this[method_symbol] = method; | ||
return this[METHOD] = method; | ||
}); | ||
@@ -216,3 +277,3 @@ | ||
Request.setProperty(function url() { | ||
return this[url_symbol]; | ||
return this[URL]; | ||
}, function setUrl(url) { | ||
@@ -223,2 +284,46 @@ return this.setUrl(url); | ||
/** | ||
* Get the response content type | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {string|undefined} | ||
*/ | ||
Request.setProperty(function content_type() { | ||
let content_type = this.getResponseHeader('content-type'); | ||
if (content_type) { | ||
if (content_type.indexOf(';') > -1) { | ||
content_type = Bound.String.before(content_type, ';'); | ||
} | ||
return content_type; | ||
} | ||
if (Blast.isBrowser && this.xhr_res) { | ||
return this.xhr_res.type; | ||
} | ||
if (this.cached_request) { | ||
return this.cached_request.content_type; | ||
} | ||
}); | ||
/** | ||
* Deprecated reference to the status number | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {number} | ||
*/ | ||
Request.setProperty(function statusCode() { | ||
return this.status; | ||
}); | ||
/** | ||
* Set options | ||
@@ -309,5 +414,5 @@ * | ||
this[url_symbol] = Blast.Classes.RURL.parse(url, origin); | ||
this[URL] = Blast.Classes.RURL.parse(url, origin); | ||
} else { | ||
this[url_symbol] = url; | ||
this[URL] = url; | ||
} | ||
@@ -355,3 +460,3 @@ }); | ||
* @since 0.2.0 | ||
* @version 0.7.0 | ||
* @version 0.7.10 | ||
* | ||
@@ -378,7 +483,196 @@ * @return {Pledge} | ||
let pledge, | ||
key; | ||
if (this.method == 'GET' && this.cache !== false && this.blast_cache !== false) { | ||
key = Blast.Classes.Object.checksum([this.url.href, this.headers, this.download_if_inline, this.get_stream]); | ||
pledge = Request.cache.get(key); | ||
if (pledge) { | ||
return this._resolveWithCache(pledge, key); | ||
} | ||
} | ||
// Do the response, follow redirects | ||
return this._make_request(); | ||
pledge = this[MAKE_REQUEST](key); | ||
return pledge; | ||
}); | ||
/** | ||
* Parse a response | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @param {string} response_body | ||
* | ||
* @return {Object} | ||
*/ | ||
Request.setMethod(function _parseResponse(response_body, error) { | ||
// Remember the raw response body | ||
this.raw_response_body = response_body; | ||
if (!error && this.status >= 400) { | ||
error = new Error(this.status_message); | ||
error.request = this; | ||
error.status = error.number = this.status; | ||
} | ||
let result; | ||
if (this.content_type && this.content_type.indexOf('json') > -1) { | ||
result = Bound.JSON.safeParse(response_body); | ||
if (this.allow_json_dry_response) { | ||
try { | ||
result = Bound.JSON.undry(result); | ||
} catch (parse_error) { | ||
if (!error) { | ||
error = parse_error; | ||
} | ||
} | ||
} | ||
} else { | ||
result = response_body; | ||
} | ||
if (error) { | ||
error.result = result; | ||
} | ||
if (!error) { | ||
error = null; | ||
} | ||
this.result = result; | ||
this.error = error; | ||
return { | ||
result : result, | ||
error : error, | ||
}; | ||
}); | ||
/** | ||
* Resolve with a cached response | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @param {Pledge} cached_pledge The Pledge gotten from the cache | ||
* @param {string} key The key that was used in the cache | ||
* | ||
* @return {Pledge} | ||
*/ | ||
Request.setMethod(function _resolveWithCache(cached_pledge, key) { | ||
let ori_req = cached_pledge[ORIGINAL_REQUEST]; | ||
// If no original request instance is found, or it had an error, | ||
// make the request anyway! | ||
if (!ori_req || ori_req.error || ori_req.cancelled) { | ||
return this[MAKE_REQUEST](key); | ||
} | ||
let result = new Blast.Classes.Pledge(); | ||
this.cached_request = ori_req; | ||
result.request = this; | ||
Blast.Classes.Pledge.done(cached_pledge, (err, res) => { | ||
if (err || ori_req.error) { | ||
this.cached_request = null; | ||
result.resolve(this[MAKE_REQUEST](key)); | ||
return; | ||
} | ||
this.time_ended = Date.now(); | ||
let parsed = this._parseResponse(ori_req.raw_response_body, ori_req.error || err); | ||
if (parsed.error) { | ||
result.reject(parsed.error); | ||
} else { | ||
result.resolve(parsed.result); | ||
} | ||
}); | ||
return result; | ||
}); | ||
/** | ||
* Make the request and potentially cache it | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @param {string} key The key that should be used in the cache | ||
* | ||
* @return {Pledge} | ||
*/ | ||
Request.setMethod(MAKE_REQUEST, function makeRequest(key) { | ||
let pledge = this._make_request(); | ||
if (key) { | ||
let max_age; | ||
if (typeof this.cache == 'number') { | ||
max_age = this.cache; | ||
} | ||
Request.cache.set(key, pledge, max_age); | ||
pledge[ORIGINAL_REQUEST] = this; | ||
} | ||
return pledge; | ||
}); | ||
/** | ||
* A small Response class | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @param {Develry.Request} request | ||
*/ | ||
const Response = Collection.Function.inherits('Informer', 'Develry', function Response(request) { | ||
this.request = request; | ||
}); | ||
/** | ||
* Reference to the response status number | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {number} | ||
*/ | ||
Response.setProperty(['status', 'statusCode'], function status() { | ||
return this.request.status; | ||
}); | ||
/** | ||
* Reference to all the response headers | ||
* | ||
* @author Jelle De Loecker <jelle@elevenways.be> | ||
* @since 0.7.10 | ||
* @version 0.7.10 | ||
* | ||
* @type {object} | ||
*/ | ||
Response.setProperty(function headers() { | ||
return this.request.getAllResponseHeaders(); | ||
}); | ||
/** | ||
* Fetch a simple resource | ||
@@ -388,3 +682,3 @@ * | ||
* @since 0.2.0 | ||
* @version 0.7.1 | ||
* @version 0.7.10 | ||
* | ||
@@ -419,9 +713,11 @@ * @param {Object} options | ||
if (callback) { | ||
pledge.done(function done(err, result) { | ||
pledge.done(function done(err, output) { | ||
let response = new Response(req); | ||
if (err) { | ||
return callback(err, req.response); | ||
return callback(err, response); | ||
} | ||
callback(null, req.response, result); | ||
callback(null, response, output); | ||
}); | ||
@@ -428,0 +724,0 @@ } |
@@ -1556,3 +1556,3 @@ let astral_rx = /\ud83c[\udffb-\udfff](?=\ud83c[\udffb-\udfff])|(?:[^\ud800-\udfff][\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]?|[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/g; | ||
* @since 0.1.2 | ||
* @version 0.1.2 | ||
* @version 0.7.10 | ||
* | ||
@@ -1566,21 +1566,16 @@ * @param {String} needle The string to look for | ||
var count, | ||
str, | ||
len, | ||
i; | ||
count = this.match(new RegExp(needle, 'g')); | ||
if (!count) { | ||
if (needle == null) { | ||
return this; | ||
} | ||
str = this; | ||
len = count.length; | ||
if (!Collection.RegExp.isRegExp(needle)) { | ||
let escaped = Collection.RegExp.escape(needle); | ||
needle = Collection.RegExp.interpret(escaped, 'g'); | ||
} | ||
for (i = 0; i < len; i++) { | ||
str = str.replace(needle, replacement); | ||
if (needle.flags.indexOf('g') == -1) { | ||
throw TypeError('String.prototype.replaceAll called with a non-global RegExp argument'); | ||
} | ||
return str; | ||
return this.replace(needle, replacement); | ||
}, true); | ||
@@ -1587,0 +1582,0 @@ |
{ | ||
"name": "protoblast", | ||
"description": "Native object expansion library", | ||
"version": "0.7.9", | ||
"version": "0.7.10", | ||
"author": "Jelle De Loecker <jelle@elevenways.be>", | ||
@@ -15,3 +15,3 @@ "keywords": [ | ||
"dependencies": { | ||
"json-dry" : "~1.1.0" | ||
"json-dry" : "~1.1.0" | ||
}, | ||
@@ -22,5 +22,5 @@ "repository": "11ways/protoblast", | ||
"scripts": { | ||
"test" : "mocha --exit --reporter spec --bail --timeout 5000 --file test/00-init.js", | ||
"coverage" : "nyc --reporter=text --reporter=lcov mocha --exit --timeout 20000 --bail --file test/00-init.js", | ||
"report-coverage" : "codecov" | ||
"report-coverage" : "codecov", | ||
"test" : "mocha --exit --reporter spec --bail --timeout 5000 --file test/00-init.js" | ||
}, | ||
@@ -30,3 +30,3 @@ "main": "lib/init.js", | ||
"browserify" : "~16.5.1", | ||
"codecov" : "~3.7.0", | ||
"codecov" : "~3.8.3", | ||
"git-rev" : "0.2.1", | ||
@@ -39,3 +39,3 @@ "istanbul-lib-instrument" : "~4.0.3", | ||
"promises-aplus-tests" : "~2.1.2", | ||
"puppeteer" : "~5.1.0", | ||
"puppeteer" : "~10.2.0", | ||
"source-map" : "~0.7.3", | ||
@@ -46,4 +46,4 @@ "uglify-js" : "3.2.0", | ||
"engines": { | ||
"node": ">=10.21.0" | ||
"node" : ">=10.21.0" | ||
} | ||
} |
@@ -6,12 +6,7 @@ <h1 align="center"> | ||
<div align="center"> | ||
<!-- CI - TravisCI --> | ||
<a href="https://travis-ci.org/11ways/protoblast"> | ||
<img src="https://travis-ci.org/11ways/protoblast.svg?branch=master" alt="Mac/Linux Build Status" /> | ||
<!-- CI - Github Actions --> | ||
<a href="https://github.com/11ways/protoblast/actions/workflows/unit_test.yaml"> | ||
<img src="https://github.com/11ways/protoblast/actions/workflows/unit_test.yaml/badge.svg" alt="Node.js CI (Linux, MacOS, Windows)" /> | ||
</a> | ||
<!-- CI - AppVeyor --> | ||
<a href="https://ci.appveyor.com/project/skerit/protoblast"> | ||
<img src="https://img.shields.io/appveyor/ci/skerit/protoblast/master.svg?label=Windows" alt="Windows Build status" /> | ||
</a> | ||
<!-- Coverage - Codecov --> | ||
@@ -18,0 +13,0 @@ <a href="https://codecov.io/gh/11ways/protoblast"> |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
730434
55
30298
97
14
3