Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Brings cancellation and testability to your async flows.
It is a tiny (core is < 200 lines of code), zero dependencies generator runner.
Just grep and replace:
async
-> function*
await
-> yield
Promise.(all|race|allSettled|any)
-> Conclude.(all|race|allSettled|any)
import { conclude } from 'conclure';
import * as Conclude from 'conclure/combinators';
// An example of a multi-step async flow that a user might want to cancel at any time
function* fetchItem(item) {
const { contentsUrl } = yield item.fetchMetaData();
const res = yield fetch(contentsUrl);
return res.text();
};
const loadAll = items => Conclude.all(items.map(fetchItem));
const cancel = conclude(loadAll(myDocs), (err, contents) => {
if (err) console.error(err);
else console.log(contents);
});
/// later...
cancel();
You can yield/conclude iterators, promises, and effects interchangeably, so you can gradually introduce cancellation and testability to your async flows.
You should avoid Promises for two major reasons:
await promise
always inserts a tick into your async flow, even if the promise is already resolved or can be resolved synchronously.You can see a Promise
as a particular type of an iterator for which the JS VM provides a built-in runner, a quite poorly designed one nonetheless.
⤕ Conclure JS is a custom generator runner that
An async flow may be represented by any of the three base concepts:
You can yield
or return
a flow from a generator function. Conclure's runner will conclude the flow that will either
The runner returns the concluded value to the generator function via .next(result)
or .throw(error)
The return value of the generator function yielding the flow - an iterator - becomes the flow's parent.
A flow may have multiple parents - different generators yielding the same flow. Conclure ensures that in this case the flow only runs once, but the results are delivered to all parents once concluded.
The root flow may be concluded by calling conclude
explicitly, which itself is a CPS function, in the same vein as you would attach a then
handler to a Promise outside of an async function. You may have multiple root flows.
conclude
returns a cancel
function that cancels the top-level flow. A child flow would then be cancelled if all of its parents are cancelled.
Unlike redux-saga, Conclure does not call .return
with some "magic" value on the iterator. It simply attempts to cancel the currently pending operation and stops iterating the iterator.
A flow is considered finished when it is either concluded (with a result or an error) or cancelled.
You can also attach weak watchers to a flow using whenFinished(flow, callback)
. The callback will be called with { cancelled, error, result }
when the flow has finished.
In case the flow concludes with a result or an error, the weak watchers are called before the result is delivered to the flow's parents, so the callback passed to whenFinished
is roughly equivalent to the finally
block of a redux-saga generator. However, it can be attached to promises and effects as well, and enables perfectly valid edge cases, when a flow is cancelled synchronously while the generator is running.
Check out some examples in the Recipes section below.
import { call, cps, cps_no_cancel, delay } from 'conclure/effects';
An effect is simply an abstracted declarative (lazy) function call: it is a simple object { [TYPE], context, fn, args }
which may come in two flavors: CALL
or CPS
.
A CALL
effect, when concluded, will call fn.apply(context, args)
and conclude the result. Create a CALL
effect using call(fn, ...args)
. If fn
requires this
, you can pass the context as call([context, fn], ...args)
.
A CPS
effect, when concluded, will call fn.call(context, ...args, callback)
, and resolve or reject when the callback is called. fn
must return a cancellation. Create a CPS
effect using cps(fn, ...args)
. If fn
requires this
, you can pass the context as cps([context, fn], ...args)
.
To call third-party CPS functions that do not return a cancellation, use the cps_no_cancel
effect instead.
delay(ms)
delay
is a CPS function. However, when called without the second callback argument it returns a cps effect on itself. When concluded, it introduces a delay of ms
milliseconds into the flow.
import * as Conclude from 'conclure/combinators';
Conclude.[all|any|race|allSettled]
combinators would do the same thing as their Promise
counterparts, except that they operate on all types of flows supported by ConclureJS: promises, iterators, or effects. All other values are concluded as themselves. The payload argument may be an Iterable
or an object.
Combinator conclude behavior summary:
Combinator | Flow k produces result | Flow k fails with error | All flows conclude |
---|---|---|---|
all([]) | continue | Fail with error | Return all results |
all({}) | continue | Fail with {[k]: error} | Return { [k in payload]: results[k] } |
any([]) | Return result | continue | Fail with all errors |
any({}) | Return {[k]: result} | continue | Fail with { [k in payload]: errors[k] } |
race([]) | Return result | Fail with error | noop |
race({}) | Return {[k]: result} | Fail with {[k]: error} | noop |
allSettled([]) | continue | continue | Return [{ result: results[k], error: errors[k] }] for all k |
allSettled({}) | continue | continue | Return { [k in payload]: { result: results[k], error: errors[k] } } |
All the combinators are implemented as CPS functions. Same as delay
, when called without the callback argument, each combinator returns a cps effect on itself.
IMPORTANT
Refer to the API reference for more details.
export function* abortableFetch(url, options) {
const controller = new AbortController();
const promise = fetch(url, { ...options, signal: controller.signal });
whenFinished(promise, ({ cancelled }) => cancelled && controller.abort());
const res = yield promise;
if (!res.ok) throw new Error(res.statusText);
const contentType = res.headers.get('Content-Type');
return contentType && contentType.indexOf('application/json') !== -1
? res.json()
: res.text();
}
const withCache = (fn, expiry = 0, cache = new Map()) => function(key, ...args) {
if (cache.has(key)) {
return cache.get(key);
}
const it = fn(key, ...args);
cache.set(key, it);
whenFinished(it, ({ cancelled, error, result }) => {
if (cancelled || error || !expiry) cache.delete(key);
else setTimeout(() => cache.delete(key), expiry);
});
return it;
}
const cachedFetch = withCache(abortableFetch, 10000);
function withSpinner(flow) {
const it = call(() => {
showSpinner();
return flow;
});
whenFinished(it, () => hideSpinner());
return it;
}
conclude(withSpinner(cachedFetch(FILE_URL)), (err, res) => console.log({ err, res }));
FAQs
Generator runner
The npm package conclure receives a total of 9 weekly downloads. As such, conclure popularity was classified as not popular.
We found that conclure demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.