Signals system for Ajeeb, based on ES6 Generators. This package is intended to unite and supersede @ajeeb/coroutines and @ajeeb/input.
This package is a work in progress. Use at your own risk.
A signal is an object that represents a stream of events, a suspension point for coroutines, and a store of value. The four things you can do with a signal are:
These operations turn out to be really effective building blocks for the kind of asynchronous code that makes up the playful systems Ajeeb is designed for.
const signal = new Signal()
// run coroutine up to its first yield then await the signal
go(function* () {
// ...
// *await* signal's emission, coroutine will not run
// until signal.emit is called
const x = yield signal
})
// ... elsewhere
// *emit* the signal to unblock any awaiting coroutines
signal.emit(9)
// *read* the most recent emitted value any any time
signal.value // => 9
// subscribe to a signal with a conventional callback
signal.on(v => ...)
const frame = new Signal()
// Ajeeb is agnostic to the graphics frameworks you use
const object = framework.makeObject(...)
go(function*(){
while(true) {
delta = yield frame
object.rotation.x += delta
}
})
// emit frame signal once per frame
let lastTime = 0
function tick(time) {
requestAnimationFrame(tick)
frame.emit((time - lastTime) / 1000)
lastTime = time
}
const mouse = Signal.event(document.body, "mousepressed")
go(function* () {
const eventData = yield mouse
console.log('clicked', eventData.clientX, eventData.clientY)
})
const timeout = new Signal()
setTimeout(timeout.emit, 5000)
const click = new Signal()
const button = document.createElement('button')
button.value = "five seconds to click me!"
document.body.appendChild(button)
button.addEventListener('click', click.emit)
go(function* () {
yield* first(timeout, click)
// this code will not run until one of timeout or click has emitted
if(timeout.emitted) {
console.log("too slow!")
} else if(click.emitted) {
console.log("you clicked me!")
}
document.body.removeChild(button)
})
Historically the Ajeeb system had a coroutine system which provided a Schedule to contain and run coroutines and an input system to manage input. This was used to great effect in the many places Ajeeb was deployed but had its limitations. The semantics of the schedule were that yield
would pause coroutines until the next tick
, which meant to await long running asynchronous operations you were forced to poll a value once per frame. Also, managing and passing around a Schedule instance was messy.
Ajeeb Signals can seamlessly consume callback and Promise-based APIs, which Ajeeb Coroutines could not. The lack of a schedule makes client code cleaner and certain edge cases much easier to implement.
Signals could conceivably be built on JavaScript's Promises but they are limited in two ways
Little control over execution order
We need rock solid guarantees of determinism and code execution order for these systems to be usable as foundational abstractions. Promises are scheduled by the JavaScript host event loop and the exact order of evaluation between scheduled promises is not defined. This is OK when you're waiting on things like network requests but intolerable for many of the interesting use cases Signals can address, namely waiting on coroutines and other asynchronous code.
No way to cancel a Promise
Being able to cancel a waiting coroutine and allow it to clean up after itself turns out to be a critical feature of asynchronous systems -- and is often missing. Ajeeb Signals leverage JavaScript's try
/finally
machinery to enable this with minimal ceremony, but Promises simply cannot be cancelled.
JavaScript is built on the notion of "events" and C# even has them built into the language. They are similar so Ajeeb Signals in that you can emit values to through them but, critically, they cannot be awaited, making them less useful in coroutines or for the construction of state machines.
Godot has signals and since 4.0 they are awaitable in coroutines, making them very similar to Ajeeb's signals. A difference is that in Godot signals are part of a larger engine, with object-oriented semantics and other organizational ideas, where in Ajeeb signals and coroutines are meant to be the main organizational principles that everything else is built on.
Ajeeb is free and unencumbered software released into the public domain under the terms of the CC0 1.0 UNIVERSAL license.
Attribution is appreciated but not required. When attributing a mention to the effect of "This project makes use of Ajeeb, originally by Ramsey Nasser" and/or a link to https://ajeeb.games/
is sufficient.
While this software may legally be used by anyone, it is not for everyone.
This software is the result of a great deal of joyful labor on the part of the author, and it is shared freely in the hope that individuals, students, artists, and worker cooperatives will benefit from its use and study in any capacity, commercial and otherwise. The author made this for you.
It is absolutely not intended for use in service of capital, in pursuit of profit for a minority off the labor of others, or by organizations that differentiate their owners and their workers.
If you own such an organization, know that the author believes that you are a bad person, and that you should step aside and allow your enterprise to be run democratically by your employees with equal stake and say in its management. The author did not made this for you, and holds you in contempt.
If you work for such an organization, know that the author believes that you are being exploited, and that you should start organizing towards converting your workplace into a cooperative or unionizing at the very least. The author is on your side, hopes this software lightens the burden of your labor.
We have nothing to lose but our chains and a better world is possible.
1 There are plans for a fourth operation -- to subscribe to a signal using a conventional callback