Strange no one mentioned `Promise.race` which would do the job (ex: https://github.com/mo/abortcontroller-polyfill/blob/master/src/abortableFetch.js#L77 )
2018-04-24 22:40 GMT+02:00 kai zhu <[email protected]>: > here's a simple (60 sloc), zero-dependency/zero-config, high-performance > express-middleware for serving files, that can broadcast a single > fs.readFile() operation to multiple server-requests. the entire standalone > program (including emulated express-server and client-side stress-testing) > is under 200 sloc, and written entirely with glue-code and > recursive-callbacks (similar to how one would go about writing an awk > program). > > honestly, there's a surprising amount of high-level stuff you can get done > in javascript using just glue-code, instead of resorting to complicated > “reusable” classes / promises / generators > > > > > ```js > /* > * example.js > * > * this zero-dependency example will demo a simple (60 sloc), > high-performance express-middleware > * that can broadcast a single fs.readFile() operation to multiple > server-requests > * > * > * > * example usage: > * $ node example.js > * > * example output: > * [server] - listening on port 1337 > * [client] - hammering file-server for 1000 ms with client-request " > http://localhost:1337/example.js" > * [server] - broadcast 10083 bytes of data from task > fs.readFile("example.js") to 352 server-requests in 229 ms > * [server] - broadcast 10083 bytes of data from task > fs.readFile("example.js") to 459 server-requests in 296 ms > * [server] - broadcast 10083 bytes of data from task > fs.readFile("example.js") to 642 server-requests in 299 ms > * [server] - broadcast 10083 bytes of data from task > fs.readFile("example.js") to 353 server-requests in 166 ms > * [server] - broadcast 10083 bytes of data from task > fs.readFile("example.js") to 1 server-requests in 1 ms > * [server] - handled 1807 server-file-requests total > * [client] - made 2100 client-requests total in 1022 ms > * [client] - (1807 client-requests passed) > * [client] - (293 client-requests failed) > * > * finished running stress-test > * feel free to continue playing with this file-server > * (e.g. $ curl http://localhost:1337/example.js) > * or press "ctrl-c" to exit > */ > > /*jslint > bitwise: true, > browser: true, > maxerr: 4, > maxlen: 200, > node: true, > nomen: true, > regexp: true, > stupid: true > */ > (function () { > 'use strict'; > var local; > local = {}; > > local.middlewareFileServer = function (request, response, > nextMiddleware) { > /* > * this express-middleware will serve files in an optimized manner by > * piggybacking multiple requests for the same file onto a single > readFileTask > * 1. if readFileTask with the given filename exist, then skip to step > 4. > * 2. if requested filename does not exist, then goto nextMiddleware > * 3. if readFileTask with the given filename does not exist, then > create it > * 4. piggyback server-request onto readFileTask with the given > filename > * 5. broadcast data from readFileTask to all piggybacked > server-requests > * 6. cleanup readFileTask > */ > var fileExists, filename, modeNext, onNext, timeStart; > onNext = function (error, data) { > modeNext += 1; > switch (modeNext) { > case 1: > // init timeStart > timeStart = Date.now(); > // init readFileTaskDict > local.readFileTaskDict = local.readFileTaskDict || {}; > // init serverRequestsTotal > local.serverRequestsTotal = local.serverRequestsTotal || 0 > // get filename from request > filename = require('url').parse(request.url).pathname; > // security - prevent access to parent directory, e.g. > ../users/john/.ssh/id_rsa > filename = require('path').resolve('/', filename).slice(1); > // init fileExists > fileExists = true; > // 1. if readFileTask with the given filename exist, then > skip to step 4. > if (local.readFileTaskDict[filename]) { > modeNext += 2; > onNext(); > return; > } > local.readFileTaskDict[filename] = []; > require('fs').exists(filename, onNext); > break; > case 2: > fileExists = error; > // 2. if requested filename does not exist, then goto > nextMiddleware > if (!fileExists) { > modeNext = Infinity; > } > onNext(); > break; > case 3: > // 3. if readFileTask with the given filename does not > exist, then create it > require('fs').readFile(filename, function (error, data) { > modeNext = Infinity; > onNext(error, data); > }); > onNext(); > break; > case 4: > // 4. piggyback server-request onto readFileTask with the > given filename > local.readFileTaskDict[filename].push(response); > break; > default: > // 5. broadcast data from readFileTask to all piggybacked > server-requests > local.readFileTaskDict[filename].forEach(function > (response) { > local.serverRequestsTotal += 1; > if (error) { > console.error(error); > response.statusCode = 500; > response.end('500 Internal Server Error'); > return; > } > response.end(data); > }); > console.error('[server] - broadcast ' + (data || > '').length + > ' bytes of data from task fs.readFile(' + > JSON.stringify(filename) + ') to ' + > local.readFileTaskDict[filename].length + ' > server-requests in ' + > (Date.now() - timeStart) + ' ms'); > // 6. cleanup readFileTask > delete local.readFileTaskDict[filename]; > if (!fileExists) { > nextMiddleware(); > } > } > }; > modeNext = 0; > onNext(); > }; > > local.middlewareError = function (error, request, response) { > /* > * this express-error-middleware will handle all unhandled requests > */ > var message; > // jslint-hack - prevent jslint from complaining about unused > 'request' argument > local.nop(request); > message = '404 Not Found'; > response.statusCode = 404; > if (error) { > console.error(error); > message = '500 Internal Server Error'; > response.statusCode = 500; > } > try { > response.end(message); > } catch (errorCaught) { > console.error(errorCaught); > } > }; > > local.nop = function () { > /* > * this function will do nothing > */ > return; > }; > > local.runClientTest = function () { > /* > * this function will hammer the file-server with as many [client] > file-request as possible, > * in 1000 ms and print the results afterwards > */ > var clientHttpRequest, > ii, > modeNext, > onNext, > requestsPassed, > requestsTotal, > timeElapsed, > timeStart, > timerInterval, > url; > onNext = function () { > modeNext += 1; > switch (modeNext) { > case 1: > // [client] - run initialization code > timeStart = Date.now(); > clientHttpRequest = function (url) { > require('http').request(require('url').parse(url), > function (response) { > response > .on('data', local.nop) > .on('end', function () { > requestsPassed += 1; > }); > }) > .on('error', local.nop) > .end(); > }; > requestsPassed = 0; > requestsTotal = 0; > url = 'http://localhost:1337/' + > require('path').basename(__filename); > // [client] - hammer server with as manny client-requests > as possible in 1000 ms > // [client] - with setInterval > console.error('[client] - hammering file-server for 1000 > ms with client-request ' + > JSON.stringify(url)); > onNext(); > break; > case 2: > setTimeout(function () { > for (ii = 0; ii < 100; ii += 1) { > clientHttpRequest(url); > requestsTotal += 1; > } > timeElapsed = Date.now() - timeStart; > // recurse / repeat this step for 1000 ms > if (timeElapsed < 1000) { > modeNext -= 1; > } > onNext(); > }); > break; > case 3: > // [client] - stop stress-test and wait 2000 ms > // [client] - for server to finish processing requests > timeElapsed = Date.now() - timeStart; > clearInterval(timerInterval); > setTimeout(onNext, 2000); > break; > case 4: > // [server] - print result > console.error('[server] - handled ' + > local.serverRequestsTotal + > ' server-file-requests total'); > // [client] - print result > console.error('[client] - made ' + requestsTotal + ' > client-requests total in ' + > timeElapsed + ' ms'); > console.error('[client] - (' + requestsPassed + ' > client-requests passed)'); > console.error('[client] - (' + (requestsTotal - > requestsPassed) + > ' client-requests failed)'); > console.error('\nfinished running stress-test\n' + > 'feel free to continue playing with this > file-server\n' + > '(e.g. $ curl ' + url + ')\n' + > 'or press "ctrl-c" to exit'); > break; > } > }; > modeNext = 0; > onNext(); > }; > > // [server] - create file-server with express-middlewares > local.server = require('http').createServer(function (request, > response) { > var modeNextMiddleware, onNextMiddleware; > onNextMiddleware = function (error) { > if (error) { > modeNextMiddleware = Infinity; > } > modeNextMiddleware += 1; > switch (modeNextMiddleware) { > case 1: > local.middlewareFileServer(request, response, > onNextMiddleware); > break; > default: > local.middlewareError(error, request, response); > break; > } > }; > modeNextMiddleware = 0; > onNextMiddleware(); > }); > // [server] - listen on port 1337 > console.error('[server] - listening on port 1337'); > // [client] - run client test after server has started > local.server.listen(1337, local.runClientTest); > }()); > ``` > > On 24 Apr 2018, at 8:46 PM, Andrea Giammarchi <[email protected]> > wrote: > > to be honest, I have solved already these cases through named promises and > the broadcast micro library I've mentioned. > > ```js > let shouldAsk = true; > function askForExpensiveTask() { > if (shouldAsk) { > shouldAsk = false; > doExpensiveThing().then(r => broadcast.that('expensive-task', r)); > } > return broadcast.when('expensive-task'); > } > ``` > > That gives me the ability to name any task I want and resolve those asking > for such tasks whenever these are available. > > A further call to `broadcast.that('expensive-task', other)` would update > the resolved value and notify those that setup > `broadcast.all('expensive-task', > callback)`, which you can also `.drop()` at any time. > > Yet, having a way to hook into these kind of flows in core would be great. > > Regards > > > On Tue, Apr 24, 2018 at 12:40 PM, kai zhu <[email protected]> wrote: > >> I see a simple scenario like the following one: >> >> - user asks for a very expensive task clicking section A >> - while it's waiting for it, user changes idea clicking section B >> - both section A and section B needs that very expensive async call >> - drop "going to section A" info and put "go to section B" to that >> very same promise >> - whenever resolved, do that action >> >> A caching mechanism to trigger only once such expensive operation would >> also work, yet it's not possible to drop "go into A" and put "go into B” >> >> >> for your scenario, what you want is a cacheable background-task, where >> you can piggyback B onto the task initiated by A (e.g. common-but-expensive >> database-queries that might take 10-60 seconds to execute). >> >> its generally more trouble than its worth to micromanage such tasks with >> removeListeners or make them cancellable (maybe later on C wants to >> piggyback, even tho A and B are no longer interested). its easier >> implementation-wise to have the background-task run its course and save it >> to cache, and just have A ignore the results. the logic is that because >> this common-but-expensive task was recently called, it will likely be >> called again in the near-future, so let it run its course and cache the >> result. >> >> here's a real-world cacheable-task implementation for such a scenario, >> but it piggybacks the expensive gzipping of commonly-requested files, >> instead of database-queries [1] [2] >> >> [1] https://github.com/kaizhu256/node-utility2/blob/2018.1.13/li >> b.utility2.js#L4372 - piggyback gzipping of files onto a cacheable-task >> [2] https://github.com/kaizhu256/node-utility2/blob/2018.1.13/li >> b.utility2.js#L5872 - cacheable-task source-code >> >> <Screen Shot 2018-04-24 at 5.31.58 PM copy.jpg> >> >> >> ```js >> /*jslint >> bitwise: true, >> browser: true, >> maxerr: 4, >> maxlen: 100, >> node: true, >> nomen: true, >> regexp: true, >> stupid: true >> */ >> >> local.middlewareAssetsCached = function (request, response, >> nextMiddleware) { >> /* >> * this function will run the middleware that will serve cached >> gzipped-assets >> * 1. if cache-hit for the gzipped-asset, then immediately serve it to >> response >> * 2. run background-task (if not already) to re-gzip the asset and >> update cache >> * 3. save re-gzipped-asset to cache >> * 4. if cache-miss, then piggy-back onto the background-task >> */ >> var options; >> options = {}; >> local.onNext(options, function (error, data) { >> options.result = options.result || local.assetsDict[request.urlPa >> rsed.pathname]; >> if (options.result === undefined) { >> nextMiddleware(error); >> return; >> } >> switch (options.modeNext) { >> case 1: >> // skip gzip >> if (response.headersSent || >> !(/\bgzip\b/).test(request.headers['accept-encoding'])) >> { >> options.modeNext += 1; >> options.onNext(); >> return; >> } >> // gzip and cache result >> local.taskCreateCached({ >> cacheDict: 'middlewareAssetsCachedGzip', >> key: request.urlParsed.pathname >> }, function (onError) { >> local.zlib.gzip(options.result, function (error, data) { >> onError(error, !error && data.toString('base64')); >> }); >> }, options.onNext); >> break; >> case 2: >> // set gzip header >> options.result = local.base64ToBuffer(data); >> response.setHeader('Content-Encoding', 'gzip'); >> response.setHeader('Content-Length', options.result.length); >> options.onNext(); >> break; >> case 3: >> local.middlewareCacheControlLastModified(request, response, >> options.onNext); >> break; >> case 4: >> response.end(options.result); >> break; >> } >> }); >> options.modeNext = 0; >> options.onNext(); >> }; >> >> ... >> >> local.taskCreateCached = function (options, onTask, onError) { >> /* >> * this function will >> * 1. if cache-hit, then call onError with cacheValue >> * 2. run onTask in background to update cache >> * 3. save onTask's result to cache >> * 4. if cache-miss, then call onError with onTask's result >> */ >> local.onNext(options, function (error, data) { >> switch (options.modeNext) { >> // 1. if cache-hit, then call onError with cacheValue >> case 1: >> // read cacheValue from memory-cache >> local.cacheDict[options.cacheDict] = >> local.cacheDict[options.cacheDict] || >> {}; >> options.cacheValue = local.cacheDict[options.cacheD >> ict][options.key]; >> if (options.cacheValue) { >> // call onError with cacheValue >> options.modeCacheHit = true; >> onError(null, JSON.parse(options.cacheValue)); >> if (!options.modeCacheUpdate) { >> break; >> } >> } >> // run background-task with lower priority for cache-hit >> setTimeout(options.onNext, options.modeCacheHit && >> options.cacheTtl); >> break; >> // 2. run onTask in background to update cache >> case 2: >> local.taskCreate(options, onTask, options.onNext); >> break; >> default: >> // 3. save onTask's result to cache >> // JSON.stringify data to prevent side-effects on cache >> options.cacheValue = JSON.stringify(data); >> if (!error && options.cacheValue) { >> local.cacheDict[options.cacheDict][options.key] = >> options.cacheValue; >> } >> // 4. if cache-miss, then call onError with onTask's result >> if (!options.modeCacheHit) { >> onError(error, options.cacheValue && >> JSON.parse(options.cacheValue)); >> } >> (options.onCacheWrite || local.nop)(); >> break; >> } >> }); >> options.modeNext = 0; >> options.onNext(); >> }; >> ``` >> >> On 24 Apr 2018, at 6:06 PM, Oliver Dunk <[email protected]> wrote: >> >> Based on feedback, I agree that a blanket `Promise.prototype.clear()` is >> a bad idea. I don’t think that is worth pursuing. >> >> I still think that there is value in this, especially the adding and >> removing of listeners you have reference to as Andrea’s PoC shows. >> Listeners would prevent the chaining issue or alternatively I think it >> would definitely be possible to decide on intuitive behaviour with the >> clear mechanic. The benefit of `clear(reference)` over listeners is that it >> adds less to the semantics. >> >> I think the proposed userland solutions are bigger than I would want for >> something that I believe should be available by default, but I respect that >> a lot of the people in this conversation are in a better position to make a >> judgement about that than me. >> _______________________________________________ >> es-discuss mailing list >> [email protected] >> https://mail.mozilla.org/listinfo/es-discuss >> >> >> >> _______________________________________________ >> es-discuss mailing list >> [email protected] >> https://mail.mozilla.org/listinfo/es-discuss >> >> > > > _______________________________________________ > es-discuss mailing list > [email protected] > https://mail.mozilla.org/listinfo/es-discuss > >
_______________________________________________ es-discuss mailing list [email protected] https://mail.mozilla.org/listinfo/es-discuss

