GithubHelp home page GithubHelp logo

caf's People

Contributors

chrisregnier avatar danielruf avatar getify avatar vkrol avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

caf's Issues

How can I dynamically detect a CAF-wrapped generator?

Thank you for this amazing library. I love being able to cancel async activity.

  • Background: Legacy parts of my codebase use traditional async functions that can't be canceled. Modern parts are using CAF.
  • Question: Is there a way I can introspect a CAF-wrapped generator to ensure that it's a CAF-wrapped function?

Example of a legacy and modern function in my codebase:

const syncOrgsModern = CAF(function *syncOrgs(signal) {
  yield doTheWork();
});

async function syncOrgsLegacy() {
  await doTheWork();
}

As we migrate more code to use CAF, I'd like to be able to dynamically inspect values such as these to ensure they're CAF-wrapped functions and not traditional sync functions. I want to do this so that my code can provide better error messages and more intelligence if something is configured wrong between the legacy way (traditional sync) and the modern way (CAF-wrapped generators).

Thanks again for the time you spend maintaining this library. It's really, really awesome. πŸ™

Cannot set property reason of #<AbortSignal> which has only a getter

First of all, thanks for your amazing framework!

Unfortunately recently I started to get error "Cannot set property reason of AbortSignal which has only a getter" from this code line while trying to discard a cancellation token. This was working in previous Chrome versions, but stopped at some point.

My browser is Google Chrome 98.0.4758.80 (arm on M1) and apparently AbortSignal.reason is a readonly field and can't be assigned.

Any help would be appreciated as now I have to use some ugly code workarounds to propagate cancellation reason through my entire flow.

Improve error when `signal` isn't passed down correctly to a sub-CAF

Thank you (again!) for this great project. It's a lifesaver if you need to timeout Promise-based code. πŸ™

I just wanted to document a potential improvement. We have code that uses CAF to add timeouts around async code. Within one CAF function we pass signal down to sub-CAF functions. This lets us separate async logic but still have it elegantly timeout.

But! If you forget to pass signal down to a sub-CAF function that expects it, a fairly unhelpful error is show:
image

Code example

// within a CAF function, call a sub-CAF function, but we forgot to pass `signal`
yield this.getAndSyncUsers(syncRequest, conversationMembers.members, seenUsers);

// signature for the sub-CAF function, but we forgot to accept `signal`
static getAndSyncUsers = CAF(function *getAndSyncUsers(
  syncRequest: SyncRequest,
  users: Array,
  seenUsers: Object,
) {
  // ...implementation...
});

explore: should CAF support "async generators"?

For the new async function* async generator functions that just landed in ES2018... should CAF support wrapping them (for cancelation) the same way we wrap a regular function * generators?

The implementation would be fairly straightforward... when you call it.next(..) in _runner(..), you just have to test if that result is itself a promise, and if so, wait for the actual iterator result from that promise before handing off to processResult(..) for processing.

The problem is... just like with regular async functions, if an async function* is currently awaiting a promise, doing it.return(..) doesn't immediately abort. It schedules an abort of the function, but only takes effect after the current await on a promise finishes.

In other words, using CAF with an async function* will give the appearance of the ability to do cancelations, but it will perhaps be a surprising detail that they can't necessarily be immediately canceled the way function * generators can, depending on what the async function* is currently doing.

That kind of surprising inconsistency might be more harmful to CAF supporting these, and maybe that means CAF shouldn't handle them. OTOH, handling them for some notion of cancelation might be better than nothing.

Anyone have any thoughts?


Illustration code:

function delay(ms) { return new Promise(res => setTimeout(res,ms)); }

async function *foo() {
   try {
      await delay(2000);
      return 42;
   }
   finally {
      return 50;
   }
}

var it = foo();
var res = it.next();
res.then(console.log);

var other = it.return(10);   // runs now, but doesn't cancel immediately, only schedules it
other.then(console.log);

// 2 seconds go by
// {value: 50, done: true}
// {value: 10, done: true}

Possible improvement to memory cleanup in signal race/all combinators

This twitter conversation gets me wondering about these lines, where we add an event listener to each parent for a child signal, but those event listeners are not removed (only the one that fires removes itself).

It might be useful for getSignalPr(..) to expose a trigger to call removeEventListener(..), so that lines 155 and 164 can remove the event handlers for all parent signals. That should ensure the promises for each child signal are GC'd, even though the child signals should already be able to be GC'd.

Also, it might be worth exploring if a WeakMap could possibly be more useful here or not, from the GC perspective.

Switch cancelToken to AbortSignal

As in fetch:

const controller = new AbortController;
const signal = controller.signal;

// pass it around
async function cancelable(signal, ...rest) {
  signal.addEventListener('abort', () => {
    /* stop listening */
    throw new AbortError('was canceled');
  });
  // rest of the implementation
}

controller.abort();

See also https://developer.mozilla.org/en-US/docs/Web/API/AbortController

So in practice, we should probably accept both an AbortSignal (possibly polyfilled: https://yarn.pm/abortcontroller-polyfill) and the cancelToken

Import Problems β€” TypeError: (0 , _caf.CAF) is not a function

What's the most correct way to import CAF in a modern Babel+Webpack+Jest environment?

I upgraded from Webpack 4->5 and it seems I have to change how I'm importing CAF.

It's in a bad situation where I have to choose between code failing and tests working, or code working and tests failing. For a while now I've been using CAF like this:

import CAF from 'caf';

// in some files, using like this
let timeoutToken = CAF.timeout(timeoutMs, timeoutMessage);

// in other files, using like this
const syncWorkspace = CAF(function *syncWorkspace(
  signal: any,
  syncRequest: SyncRequest,
  workspace: any,
) {
  this.syncStatsBreakdown[`workspace-${workspace.gid}`] = {};

  log.info(`workspace ${workspace.gid} - syncing tasks`);
  yield this.syncUserTaskList(signal, syncRequest, workspace);

  log.info(`workspace ${workspace.gid} - syncing projects`);
  yield this.syncWorkspaceProjects(signal, syncRequest, workspace);

  log.info(`workspace ${workspace.gid} - syncing users`);
  yield this.syncUsers(signal, syncRequest, workspace);

  log.info(`workspace ${workspace.gid} - syncing complete`);
});

My tests and builds were both happy with that when I was using Webpack 4.

Now, with Webpack 5, it seems I need to alter how I import CAF. But it's somehow in a situation where I can get the code and the tests both working. Here are the 2 main ways I'm trying. I've tried variations on these but can't seem to get CAF imported in a way that makes Webpack/Babel/Jest happy.

import CAF from 'caf';

  • Build compiles, but with warnings
    image

  • When build runs, it fails
    image

  • When tests run, works fine
    image

import { CAF } from 'caf';

  • Build compiles, with no warnings
    image

  • When build runs, works fine
    image

  • But when tests run, the tests fail.
    image

My previous environment worked with the first style:

  • Webpack 4
  • Node 12
  • Jest and Babel for tests
  • Webpack and Babel for builds

My previous environment seems to require a switch to the second style, but then tests fail:

  • Webpack 5
  • Node 14
  • Jest and Babel for test
  • Webpack and Babel for builds

It seems that CAF is using this trick to export itself as both a function and namespace. Any recommendations on different things to try? Thank you.

AbortController (in Node), integration with CAF?

Hey Kyle πŸ‘‹

Node recently(ish) shipped AbortController/AbortSignal (I believe I pinged you on one of the issues).

I just wanted to ask if there is anything Node.js should be doing differently with regards to our AbortController/AbortSignal or our usage of them in APIs or if you have an opinion regarding anything else we should do to improve the cancellation story.

Unexpected try/catch/finally semantics

After my first run at creating a couple of CAF runnables and playing with cancellation I've realized adding a finally changes the semantics in an unexpected way (to be fair this is documented but I didn't quite realize the implications until I ran into it).
Example running the following should log 'try'

let cancelToken = new CAF.cancelToken();
CAF(*main(signal) {
  try {
    console.log('try');
    yield new Promise(() => {});   // yield forever
  }
  catch (err) {
    console.log('catch');
  }
  console.log('after');
})( cancelToken.signal);
wait(5000).then(() => cancelToken.abort('hello abort'))

Meanwhile add the finally block and now this will print 'try', 'finally' (Note it does not print 'after')

let cancelToken = new CAF.cancelToken();
CAF(*main(signal) {
  try {
    console.log('try');
    yield new Promise(() => {});   // yield forever
  }
  catch (err) {
    console.log('catch');
  }
  finally {
    console.log('finally');
  }
  console.log('after');
})( cancelToken.signal);
wait(5000).then(() => cancelToken.abort('hello abort'))

This was not quite what I was expecting, and now finally blocks get triggered from 3 paths instead of the normal 2 (3rd path is the hidden cancel signal). I was actually expecting the abort to throw a special Cancel Error or even just the value passed into .abort('hello abort') and then be able to catch it and check for the special cancel error or a cancel Symbol. Obviously if you don't have a try/catch around your yields then you're not going to catch any errors and the whole runner is finished.

I can understand the behaviour makes sense since you're not really wrapping the signal promise in the try catch, but it seems weird that it acts like a hidden error and triggers the finally only. So I'm wondering if there would be value in adding a new abortWithThrow(reason) that calls the generator's throw with some kind of cancel error/symbol as part of aborting the AbortSignal.
Then this would print 'try', 'cancelled: hello abort', 'after'

let cancelToken = new CAF.cancelToken();
CAF(*main(signal) {
  try {
    console.log('try');
    yield new Promise(() => {});   // yield forever
  }
  catch (err) {
    if (err instanceof CAF.CancelError) {
      console.log('cancelled: ' + err.message)
    }
    else {
      console.log('catch');
    }
  }
  console.log('after');
})( cancelToken.signal);
wait(5000).then(() => cancelToken.abort('hello abort'))

ReferenceError: AbortController is not defined

file:///home/ubuntu/dwhelper/Digging%20into%20Nodejs/Learning%20Digging%20into%20Nodejs/node_modules/caf/dist/esm/shared.mjs:5
const CLEANUP_FN=Symbol("Cleanup Function"),TIMEOUT_TOKEN=Symbol("Timeout Token");class cancelToken{constructor(n=new AbortController){var s;this.controller=n,this.signal=n.signal;var handleReject=(n,i)=>{var doRej=()=>{if(i){var n=this.signal&&this.signal.reason?this.signal.reason:void 0;i(n),i=null}};this.signal.addEventListener("abort",doRej,!1),s=()=>{this.signal&&(this.signal.removeEventListener("abort",doRej,!1),this.signal.pr&&(this.signal.pr[CLEANUP_FN]=null)),doRej=null}};this.signal.pr=new Promise(handleReject),this.signal.pr[CLEANUP_FN]=s,this.signal.pr.catch(s),handleReject=s=null}abort(n){this.signal&&!("reason"in this.signal)&&(this.signal.reason=n),this.controller&&this.controller.abort()}discard(){this.signal&&(this.signal.pr&&(this.signal.pr[CLEANUP_FN]&&this.signal.prCLEANUP_FN,this.signal.pr=null),this.signal=this.signal.reason=null),this.controller=null}}export default{CLEANUP_FN:CLEANUP_FN,TIMEOUT_TOKEN:TIMEOUT_TOKEN,cancelToken:cancelToken,signalPromise:signalPromise,processTokenOrSignal:processTokenOrSignal};export{CLEANUP_FN};export{TIMEOUT_TOKEN};export{cancelToken};export{signalPromise};export{processTokenOrSignal};function signalPromise(n){if(n.pr)return n.pr;var s,i=new Promise((function c(i,r){s=()=>r(),n.addEventListener("abort",s,!1)}));return i[CLEANUP_FN]=function cleanup(){n&&(n.removeEventListener("abort",s,!1),n=null),i&&(i=i[CLEANUP_FN]=s=null)},i.catch(i[CLEANUP_FN]),i}function processTokenOrSignal(n){n instanceof AbortController&&(n=new cancelToken(n));var s=n&&n instanceof cancelToken?n.signal:n;return{tokenOrSignal:n,signal:s,signalPr:signalPromise(s)}}
^

ReferenceError: AbortController is not defined
at new cancelToken (file:///home/ubuntu/dwhelper/Digging%20into%20Nodejs/Learning%20Digging%20into%20Nodejs/node_modules/caf/dist/esm/shared.mjs:5:119)
at Function.timeout (file:///home/ubuntu/dwhelper/Digging%20into%20Nodejs/Learning%20Digging%20into%20Nodejs/node_modules/caf/dist/esm/caf.mjs:5:1158)
at file:///home/ubuntu/dwhelper/Digging%20into%20Nodejs/Learning%20Digging%20into%20Nodejs/cli3.js:43:28
at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
at async Loader.import (internal/modules/esm/loader.js:166:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5)

code ->

#!/usr/bin/env node

"use strict";

import util from 'util';
import path from 'path';
import fs from 'fs';
import zlib from 'zlib';

import minimist from 'minimist';
import { Transform } from 'stream';
import { CAF } from 'caf';

const __dirname = path.resolve();

var args = minimist(process.argv.slice(2),{
boolean: ['help', 'in', 'out', 'compress', 'uncompress'],
string: ['file']
});

processFile = CAF(processFile);

function streamComplete(stream){
return new Promise (function c(res){
stream.on("end",res);
})
}

var BASH_PATH = path.resolve( process.env.BASE_PATH || __dirname)

var OUTFILE = path.join(BASH_PATH, 'out.txt');

if(args.help){
printHelp();
}
else if(args.in || args._.includes('-')){
const timeoutToken = CAF.timeout(3, "Timeout!");
processFile(timeoutToken,process.stdin)
.catch(error);
}
else if(args.file){
const stream = fs.createReadStream(path.join(BASH_PATH,args.file));
const timeoutToken = CAF.timeout(3, "Timeout!");
processFile(timeoutToken,stream)
.then((data)=>{
console.log('Complete!');
})
.catch(error);
}
else{
error('Incorrect usage',true)
}

// **************

function *processFile (signal,inStream){
let outStream = inStream;

if(args.uncompress){
const gunzipStream = zlib.createGunzip();
outStream = outStream.pipe(gunzipStream);
}

const upperStream = new Transform({
transform(chunk, enc, cb){
this.push(chunk.toString().toUpperCase());
cb();
}
});
outStream = outStream.pipe(upperStream);
if(args.compress){
const gzipStream = zlib.createGzip();
outStream = outStream.pipe(gzipStream);
OUTFILE = ${OUTFILE}.gz;
}
let targetStream;
if(args.out){
targetStream = process.stdout;
}
else{
targetStream = fs.createWriteStream(OUTFILE);
}
outStream.pipe(targetStream);
signal.pr.catch(function f(){
outStream.unpipe(targetStream);
outStream.destroy();
})
yield streamComplete(outStream)
}

function error(msg, includeHelp= false){
console.log(msg);
if(includeHelp){
console.log('');
printHelp();
}
}

function printHelp(){
console.log("cli3 usage:");
console.log(" cli3.js --file={FILENAME}");
console.log("");
console.log(" --help print this help");
console.log(" --file process the file");
console.log(" --in, - process stdin");
console.log(" --out print the output");
console.log(" --compress gzip the output");
console.log(" --uncompress un-zip the input");
console.log("");
}

Feature request: configurable 'this' context

Thanks for the great little library!
I've been trying to integrate it into some of my work and found that I missing the ability to set 'this' on the generator passed to CAF. So far I've been just using bind on the generator to get around things, but I think things could be improved if CAF allowed a way of setting the bound context.

For an example on my use case:

class JobRunner {
  constructor(fooService) {
    this.fooService = fooService;

    // ***  pass in new options with context? ***
    this._runner = CAF(this.run, { context: this });
  }

  *run(signal, ...params ) {
    // do stuff in the generator but still allowed to use 'this' context of the owning class    
  }

  add(...params) {
    const token = new CAF.cancelToken();
    const result = this._runner(token.signal, ...params);
    return { token, result };
  }
}

AbortController not defined using ES Import statements

Hey Kyle
I was working on this library and encountered a bug that AbortController is not defined.
It seems that the issue is somewhat related with ES imports statements . I tried using the usual require statements and surprisingly it's working fine.
Here is the screenshot of the error
Screenshot from 2021-06-20 14-09-49

update "abortcontroller" to fully use new version of polyfill

The abortcontroller polyfill used by CAF is now able to run in Node without any modifications. As such, we should now be able to list it as a dependency and link to it separately instead of including a manually tweaked version of the file for our distribution build. Update accordingly.

How to use CAF with Axios' `cancelToken` argument?

Hi, first off, thanks for this library. I was in the middle of implementing my own async generator-runner when I realized that CAF does exactly what I want and has excellent documentation. It's really, really helpful that you've made this public.

I noticed in the README there are examples showing how to pass signal to fetch() in order to to abort mid-request. It's awesome that optimization is supported.

Are there any CAF users who are doing a similar thing with Axios? Axios supports a cancelToken argument, documented here, but the API seems like it might be slightly off from CAF's.

Here's what Axios does with the cancelToken argument: https://github.com/axios/axios/blob/f3cc053fb9feda2c3d5a27513f16e6722a0f9737/lib/adapters/xhr.js#L165

Is there a way I can pass CAF's signal.pr to Axios' cancelToken? Or wrapped version of signal.pr?

CAF and Axios seem to have independent implementations of cancellation, but given that AbstractController is a browser standard, I'm wondering if there's a way to connect the dots between CAF and Axios.

I've been reading the docs and code on both sides carefully, but it's not worth trying and getting it wrong because bailing from an Axios request early is an optimization in my case, not a core requirement. Thanks again for this fantastic library!

–Ben

Can't use with create-react-app

Hello, I tagged you on Twitter about this a few days ago, but I figured it was okay to open an issue here. I feel like create-react-app is used by enough people (80k stars on GitHub) that it's worth looking into this issue.

How to reproduce the error

Make a new create-react-app app:

npx create-react-app my-test-app
cd my-test-app

Install caf:

npm install caf

Include caf in any /src file -- say App.js -- as specified in the readme:

var CAF = require("caf");

Save the changes and run the app:

npm start

Observe the following error, when running locally:

Error: Cannot find module '/dist/abortcontroller-polyfill-only.js'

Or observe the following error when running on CodeSandbox:

ModuleNotFoundError
Could not find module in path: 'caf/dist/abortcontroller-polyfill-only.js' relative to '/node_modules/caf/index.js'

CodeSandbox for convenient testing

https://codesandbox.io/s/frosty-tree-cwe5z


I'll post comments here as I try to figure out how to fix the issue.

add `signalRace(..)` and `signalAll(..)` helpers

These helpers will correspond to race() and all() on promises, but will combine signals into a new signal.

var userCanceled = new CAF.cancelToken();
var tookTooLong = CAF.timeout(10000,"took too long");

send = CAF(send);
send( CAF.signalRace([userCanceled,tookTooLong]), "some text" );
// or
// send( CAF.signalAll([userCanceled,tookTooLong]), "some text" );

function *send(signal,data) {
   var res = yield fetch("/some/url",{
      body: data,
      signal
   });
   if (res.ok) {
      let resp = yield res.text();
      console.log(resp);
   }
}

Implementing debounce with CAF?

[EDIT - this comment is in the context of the original title: "CAF.delay does not proxy cancel reason"]

Consider this example:

const token = new CAF.cancelToken();
CAF.delay(token.signal, 5000).then(console.log, console.error);
token.abort('TEST');

I would expect that console would log the error 'TEST' but instead it logs the error 'delay (5000) interrupted'.

Am I misunderstanding this behavior?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.