Coastline
Deprecated. Async/await has infinitely better support than when this project was started and restarted and we've switched to promises in all new projects some time ago.
Some of the features are not implementable with promises (env, deadlocks, etc) but that's not a reason to be incompatible with the rest of the world.
A proper call stack for asynchronous JavaScript.
Please note that Coastline only works on Node 4.0.0 and up. It relies heavily on generators (but will work with Babel/Traceur in old browsers).
Features
-
"Synchronous" asynchronous operations via ES6 generators:
var value = yield (function* () { ... })(); yield CL.sleep(1000);
-
Single-line asynchronous method calls and waiting for events:
var buffer = yield fs.readFile('file.txt', CL.cb()); var event = yield $('button').on('click', CL.done());
-
Full two-way support for promises:
var result = yield new Promise(...); CL.fun(function* () {})().then(...);
-
Parallel tasks via
bg
:var a = CL.bg(fs.readFile('header.html', CL.cb())); var b = CL.bg(fs.readFile('footer.html', CL.cb())); yield fs.writeFile('index.html', (yield a) + 'Welcome!' + (yield b), CL.cb());
-
Race condition control via host contexts:
var obj = { work: CL.fun(function* () { ... }) }; obj.work(); obj.work(); // executes one after the other, even when called from outside
-
Easy and powerful error handling:
CL.try(function* () { fs.readFile('never.existed', CL.cb()); }, { ENOENT: function* (e) { console.log('No surprise here.'); }});
-
Deadlock detection, cleanup and retrying:
CL.fun(function* () { something++; yield potentialDeadlock(); }, { retry: function* () { something--; } });
-
Storing information on stack
var query = function* () { db.query(CL.get('transaction'), ...); }; CL.set('transaction', db.begin()); yield query('SELECT...');
-
Fast enough - up to 10 million operations per second per core
Usage
In Node.js
Just npm install coastline
and then var CL = require('coastline');
.
In browser
You will probably want to compile Coastline and your code with Babel, then include browser-polyfill.js
from babel-core
package before any other code. Or if you only care about Chrome and Firefox, just import coastline.js
with a synchronous <script>
tag before your code.
In Node.js/Browserify modules
Currently it is required for valid operation that there is only one Coastline object per process. Therefore Coastline will set window.CL
or global.CL
to the first module that is loaded and use that instead of any subsequent module loads. In later versions, an upgrade mechanism will be added to always use the latest version. Currently your only option is to keep your modules up to date manually.
Interactive examples
Recommended in order. Note that JSBin Babel support seems to be broken, so these don't use it, so please view in Chrome or Firefox.
- Simple example
- CL.bg() example
- CL.each() & CL.mapParallel() example
- Host objects and error handling example
- Deadlock example
- Retry example
API documentation
Note: there are many ways to pass code to Coastline. In this documentation:
- Function specifically means unwrapped function or generator function.
- Method means one of:
- a function
- a generator function (
function* () {}
) - a
CL.fun()
wrapped function - an array
[object, 'methodName']
(since 3.0.0) (cannot be yielded) - an array
[object, Method]
(since 3.0.0) (cannot be yielded)
- Task means one of:
- a Method
- a generator (result of running a
function*
) - a Promises/A+ object.
- Context is the object returned by CL.* methods which can be passed to other methods or
yield
In this documentation, methods that have yield
before their name should be accordingly used with yield
before their call, and the ones without shouldn't.
CL.run(Method, arguments...)
Starts a new caller chain if the currently running code is not run by Coastline already. Can also be used to explicitly run something in current context, in a case when code executing is not a generator.
First argument is the Method. All the subsequent arguments are passed to the function.
var fun = function* (arg1, arg2) { };
CL.run(fun, arg1, arg2);
yield Task
- Starts the Task if it's not started yet and waits for it to finish, returning the result.
- Waits for the result of
CL.bg()
or unrelated Contexts. - If
CL.cb()
and similar were called, returns the result of the created context, regardless of what's actually yielded. - Otherwise passes anything that is not a Task or a Context through without modification.
Example:
123 === yield function* () { return 123 };
123 === yield (function* () { return 123 })();
123 === yield CL.bg(function* () { return 123 });
123 === yield CL.top(function* () { return 123 });
123 === yield 123;
// etc
return Context|Value|result
Returns the result of the function. If a Context is returned, waits on it and returns the value of that Context instead. Use CL.val()
if you really want to return a context.
Like yield
, if CL.cb()
was called, will wait and return the result of the callback. This can't be overridden with CL.val()
.
return 123;
return CL.parallel(...);
return fs.readFile('filename', CL.cb());
CL.immediate(Context)
Try to execute and return the result of the context immediately (out-of-band). If needed, will also fast-forward caller and host and any contexts the task yields.
Will also return a value of an already finished context. If the Context has encountered an error previously, will throw it via native throw
.
If immediate execution is not possible, will return the passed Context.
Useful e.g. to transparently wrap synchronous functions in a Context
123 === CL.immediate(CL.env({ num: 123 }, function () { return CL.get(123); }));
CL.val(value)
Creates a Value object that can be returned or yielded without evaluation. The actual received return value will be unwrapped, so you have to wrap it again if you need to pass it further. CL.wait()
or CL.resolve()
will also ignore anything wrapped in a Value.
var a = function* () {
return CL.val(CL.top(function* () { return 123; }));
};
var v = yield a();
v instanceof CL.Context == true;
yield v == 123;
CL.fun(Function, [options])
Makes a Method that creates a context around the passed function and runs it. Basically, CL.fun(...)
similar to function () { return CL.run(...) }
.
The difference is that when used as an object's method this function will create and use a host context, as explained above.
Options is an optional object. Available options currently are:
retry
- can be a Method ortrue
and will cause the task to re-run in case of a deadlock. If it's a Method, it will be run immediately after the deadlock is detected and can be usedloose
- if set totrue
, will not wait for or lock the host Context. Useful for static methods, where when callinglib.fun()
you don't want to locklib
.
Example:
Class.prototype = {
method: CL.fun(function* () {
something++;
yield potentialDeadlock();
}, {
retry: function* () {
something--;
}
});
}
The wrapped function can be accessed as the .method
property of the returned function.
CL.top(Method, arguments...)
Starts a new caller chain, regardless of when it's called. The arguments are the same as for CL.run()
.
CL.run(function* () {
CL.top(unrelatedMethod, arg1, arg2);
});
Note that deadlocks will not be detected if you yield
an unrelated context. This will be fixed in eventually.
CL.spawn(Method, arguments...)
Starts a new caller chain, same as CL.top()
, but retains the environment created by CL.set()
or CL.env()
.
yield CL.visit(Object, Method, arguments...)
Appends the task to the object's host context. The remaining arguments are the same as for CL.run()
CL.bg(Task | Context, args...)
Starts the Task in the background or marks the Context as a background context. Background task will not block the caller but it will wait on it before finishing.
To wait manually and receive the return value, just save the result of CL.bg()
to a variable and yield
it when you need it.
var a = CL.bg(fs.readFile('header.html', CL.cb()));
var b = CL.bg(fs.readFile('footer.html', CL.cb()));
yield fs.writeFile('index.html', (yield a) + 'Welcome!' + (yield b), CL.cb());
You can use CL.wait()
or CL.resolve()
to wait on arrays or objects containing Contexts returned by CL.bg()
.
Note: anything that involves you literally writing yield CL.bg()
will not work in parallel since it will immediately wait on the Context. So instead of func(yield CL.bg(), yield CL.bg())
you will have to use e.g. the spread operator func(...(yield CL.wait([CL.bg(), CL.bg()])))
. There may be a better way coming soon.
CL.cb(errorType)
Creates a callback of form callback(error, result)
that can be passed to any external function. The task must yield immediately after said function is run so that this callback is bound to a correct object.
yield fs.writeFile(
'otherfile',
yield fs.readFile('filename', CL.cb()),
CL.cb()
);
If an error is received, it will be thrown as via CL.throw()
and the task will not continue. If errorType
is given, Error's type will be set to it.
Otherwise the the return value of yield
will be the result passed to the callback.
CL.done()
Same as CL.cb()
but creates a callback(result)
and does not handle errors.
yield setTimeout(CL.done(), 1000);
CL.error(errorType)
Same as CL.cb()
but creates a callback(error)
.
img.onload = CL.done();
img.onerror = CL.error();
img.src = 'img.jpg';
yield;
CL.throw(type, error)
Throw an error up the stack. This error can be caught using CL.try()
.
If it's not caught, Coastline will terminate and CL.onError
will be run.
Can be run with 1 or 2 arguments:
- string type, string error message (
type
will be assigned to Error's.type
) - string type, standard Error object
- string type, any Object (will be assigned to
.data
) - string - will be both
.type
and.message
- Error object
- any Object
Example:
CL.throw('CustomError', 'A custom error has occured');
CL.throw('CustomError', { useful: 'information' });
yield CL.try(..., function* (e) { CL.throw(e); });
yield CL.try(Method, Method | { errorType: Method })
Starts a task defined by first argument and catches any errors occurring anywhere down the call chain, passing them to the corresponding handler.
Handlers can be:
- A catch-all Method
- An object where key is error type and value is handler. Key
*
matches everything.
Example:
yield CL.try(function* () {
fs.readFile('never.existed', CL.cb());
}, { ENOENT: function* (e) {
console.log('No surprise here.');
}});
Type will be either set by CL.throw()
or, if the error message contains a colon, it will be the part before the first colon. For example "ENOENT: no such file or directory" sets type to ENOENT
. If Error object contains .name
, e.g. TypeError
, that will be used instead.
CL.try()
will return the return value of the first Method on success or the return value of the second Method on failure.
Neither passing the Method, nor *
will catch Coastline errors (both internal and caused by your actions). Define handlers for CLError
and CLInternalError
if you really want to catch them. But you really don't.
CL.onError = function (e) { ... }
A callback that is run when an unhandled error reaches the top of the stack.
By default will print the Coastline stack and throw the error, therefore crashing the process in case of Node.js. If you override it, be sure to crash the process too since the behavior of Coastline after an unhandled error is currently undefined. This may change in later versions.
The printed trace consists of tag (if present) full method code and arguments - it's the only useful information there is. Previous version would create an Error object for every Context to get a proper stack, but that's way too slow.
Refer to the code of the default handler to learn how you can use the passed Error object.
CL.tag(tag)
Set a custom tag for the Context that will be shown in the trace in case of unhandled error. There are no limitations on its contents.
CL.tag('Got arg ' + arg + ', calling some other function now');
CL.priority(prio)
Set the priority of current Context, Valid values are -5 to 5 (inclusive). Priorities are inherited from the caller. The default priority is 0.
Note that contexts with a higher priority are always executed first, regardless of how many are in the lower priority queues.
CL.set(key, value)
Attach data to current context that can be accessed by any child contexts. Can be used as kind of a global variable for session information etc. As it is with global variables, must be used with care.
var query = function* () { db.query(CL.get('transaction'), ...); };
CL.set('transaction', db.begin());
yield query('SELECT...');
CL.get(key)
Get a value by key previously defined by CL.set()
on current context or one of its callers.
CL.env(data, Method|Context, args...)
Creates a new context with its data set to data
and optionally runs Method with args.
If passed a Context instead of Method, will set its data to data
.
Returns the created or modified Context.
yield CL.wait(Array | Object)
Waits on multiple bg
tasks (passed as array or object) and returns said array/object with values filled in.
var results = _.each(array, function (el) {
return CL.bg(process(el));
});
results = yield CL.wait(results);
Note: CL.wait()
modifies the object it is passed. Make a copy if you need the original for some weird reason.
yield CL.resolve(Array | Object)
Deep version of CL.wait()
. Recursively waits on any Contexts found in an array or object.
yield CL.sleep(ms);
Suspend the context for a said number of milliseconds.
console.log('Your call is very important to us, please hold.');
yield CL.sleep(10000);
CL.expect()
Creates a context object that will wait until .done()
or .throw()
is called on it to finish. Yielding this is same as yielding a promise, but a bit more internal to Coastline. This can be used e.g. for request-response system:
requests[id] = CL.expect();
sendRequest(id);
var response = yield requests[id];
// elsewhere
requests[id].done(response);
yield CL.each(Array | Object, Method)
Run Method(element, i)
over all elements of array or Method(value, key)
over all properties of an object, one after the other.
yield CL.each([1000, 2000, 3000], CL.sleep);
console.log('Slept for six seconds');
For non-generator functions you should use something faster (e.g. Underscore)
yield CL.map(Array | Object, Method)
Run Method(element, i)
over all elements of array or Method(value, key)
over all properties of an object, one after the other and returns array or object with function's result values.
Example:
var sum = 0;
var result = yield CL.map([1, 2, 3], function* (v, i) {
return sum += v;
});
//result == [ 1, 3, 6 ]
yield CL.parallel(Array | Object, Method, limit)
- run
Method(element, i)
over all elements of array in parallel. - run
Method(value, key)
over all properties of an object in parallel.
Example:
yield CL.parallel([1000, 2000, 3000], CL.sleep);
console.log('Slept for three seconds');
Execution order is not guaranteed. At most limit
tasks will be running in parallel. The default limit is 1000.
yield CL.mapParallel(Array | Object, Method, limit)
Same as CL.parallel()
but returns an Array or Object with function's return values.
CL.queue(options)
Constructs a Queue
object that allows limiting parallel execution of unrelated tasks.
Available options are:
limit
maximum number of tasks to run in parallel, 1 by defaultsleep
number of milliseconds to sleep between tasks, 0 by default
Queue.push(Task, args...)
Pushes a task onto the queue. If task is a method, remaining arguments will be passed to it. Returns the pushed context.
Note that tasks are not implicitly marked as bg
- tasks added from the same Context must either be yield
ed and will execute in order or must explicitly be wrapped in CL.bg()
.
var q = CL.queue({ limit: 2 });
q.push(CL.bg(CL.sleep(1000)));
yield q.push(function* (str) {
console.log(str);
}, 'hello');
Potentially private methods
The information about these methods is currently only available in the code documentation and I'd rather you didn't use them until we decide it's safe to do so. But if you find a good use case for them, please do tell me.
new CL.Context()
CL.push(Context c)
CL.obj(Object)
CL.cur()
CL.work()
Debugging
CL.debug(true|false)
Turn the debugging features on or off. Note that this may significantly degrade performance.
This currently enables
- a list of active contexts
- stack traces
CL.getActive()
Get a list of Contexts that have started but haven't finished yet. Requires CL.debug(true)
.
Context.trace()
Get a stack trace of all contexts leading to the current one.
console.log(CL.cur().trace());
CLEnableDebugging
A window
or global
field that can be set to initialize CL.debug()
to said value when Coastline loads.
Changelog
5.1.2
- Handle case when our error callback is called before the buildin Context in yield
- all commits for this version
5.1.1
- Now including a version precompiled with Regenerator (as
compat.js
) - all commits for this version
5.0.1
- Fixed stack traces for when Coastline is not loaded from a file named "coastline.js"
- all commits for this version
5.0.0
- Host contexts will now remove themselves from objects upon finishing the queue to release resources. This means that there now can be more than one host context in object's lifetime. A very low possibility of things breaking exists.
- all commits for this version
4.4.1
- Make CL.immediate() caller wait on failed immediate only when it's its caller
- all commits for this version
4.4.0
CL.priority()
added- all commits for this version
4.3.0
CL.spawn()
added- all commits for this version
4.2.3
- Don't crash on error, then success case in CL.parallel.
- all commits for this version
4.2.2
- Fixed obscure error where multiple CL.immediate calls + host context would result in a crash.
- all commits for this version
4.2.1
- Fixed breaking change in 4.2.0 when CL.get() would crash without current context.
- all commits for this version
4.2.0
- Moved get/set methods to Context so they can now be called on any context, not just current one.
- Not assuming there was no value in Context before method return - allows to preset return value manually.
- all commits for this version
4.1.0
- Implemented asynchronous stack traces. These will be printed automatically on error and can be obtained manually by
.trace()
. Note thatCL.debug(true)
now significantly decreases performance (up to 10x). - all commits for this version
4.0.0
- breaking: when returning
undefined
(or not returning), the return value will now beundefined
(as expected) and not the value of the lastyield
. - all commits for this version
3.3.0
- minimal debugging tools introduced
- don't crash after waiting on host contexts
- don't crash after babel recompilation
- all commits for this version
3.2.3
Multiple bugfix and performance releases.
- use setImmediate where available
- fixed get-over-10 benchmark
- CL.sleep(0) benchmark
- do not setTimeout all the time
- do not consider yielded arrays Methods - this can lead to too confusing bugs
- all commits for this version
3.2.0
- CL.visit()
- all commits for this version
3.1.0
- CL.immediate()
- all commits for this version
3.0.0
- breaking:
CL.wait()
will now wait on host context if passed aCL.obj()
(or a host ofCL.fun()
) - breaking: Method can now be an array with object as the first element and method as the second. This includes
yield
-ed values, so may break things. - CL.env() method added
- optimizations for very long queues
- all commits for this version
2.2.0
Queue
class implemented.then()
calls now chainable as per Promises/A+ spec- all commits for this version
2.1.0
- CL.fun()
loose
option added - Coastline now uses symbols to attach host Contexts to objects, where available
- all commits for this version
2.0.0
- breaking:
return
now behaves as expected for functions, generators and promises (returns them unmodified) - expose wrapped function as .method in CL.fun()
CL.val()
added- all commits for this version
1.1.0
- added
CL.map()
andCL.mapParallel()
- yielding unrelated contexts now fully supported
CL.expect()
- various fixes
- all commits for this version
Want to talk or have questions?
- come to
#coastline
on Freenode - follow @very_lv on Twitter
- drop a mail to danko@very.lv
- report bugs and ask questions here