Proposal: Allow Promise callbacks to be removed
Cyril Auburtin
cyril.auburtin at gmail.com
Tue Apr 24 22:34:57 UTC 2018
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 <kaizhu256 at gmail.com>:
> 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 <andrea.giammarchi at gmail.com>
> 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 <kaizhu256 at gmail.com> 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 <oliver at oliverdunk.com> 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
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>>
>>
>>
>> _______________________________________________
>> es-discuss mailing list
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>>
>>
>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180425/dea16965/attachment-0001.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Screen Shot 2018-04-25 at 3.10.19 AM copy.jpg
Type: image/jpeg
Size: 43642 bytes
Desc: not available
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180425/dea16965/attachment-0001.jpg>
More information about the es-discuss
mailing list