-
Notifications
You must be signed in to change notification settings - Fork 29
ScarpeEvents.md
Scarpe makes heavy use of the "Events" abstraction common to many GUI (and other) libraries. This is related to, but different from, Evented Programming a la Node.js or libuv.
This is a deep topic. I'll try to distinguish between Scarpe-specific information and more general information that's true everywhere.
If you're thinking, "wait, what does that mean?" then you should read the later sections of this document and then come back to this one. Most of the rest of the document is about things that are generally true everywhere. This section is about what Scarpe does, very specifically.
Scarpe has a singleton DisplayService object, which allows registering for Shoes events, by event name and by the target object of the event. They can also pass arguments. These are used for Shoes/Lacci objects to send messages to the display service, and vice-versa.
Display services are allowed to have internal event systems as well (e.g. Webview display service does this with its ControlInterface.) But that's an implementation detail, and your Shoes apps shouldn't need to be aware of it.
Scarpe does not guarantee what order events are received in if you have multiple bindings. If you bind to :any event and to a particular event too, you will get both... in some order, but no guarantees.
If a bound event handler gets an event and then sends a different event as a result... Some bindings may receive the first event, then everybody gets the second event, then the remainder of will get the first event.
Certain events, especially the "run" events, don't terminate. That means that this event, or events that would be sent in response to it, may be completely lost in some situations. For now, that probably means that you shouldn't send Shoes events in response to the "run" event or other events that never return/complete.
A "synchronous" or "sync" operation takes place when you tell it to, like an API call. You say "call this method," the method gets called, and it won't return control to you until it's done. If it makes another synchronous call (e.g. calls another method) then you'll be waiting until that finishes, too. A synchronous method can return a value, and as soon as it returns you can usually check whether it had any errors or other difficulties. After you've called a synchronous operation it's done and you can check its results and go on with your day.
An "asynchronous" or "async" operation doesn't necessarily happen when you tell it to, and you can't assume it did. You tell it to start an operation (function-call, message-send, something else) and it'll get to it when it's ready to get to it. Sometimes your asynchronous event will finish if you wait for it (e.g. making an API call from a background thread.) But some async operations can't even start until your current operations have returned control back to the main message loop (e.g. many kinds of RPC messages.) One way to handle async operations is to "poll" - to repeatedly check whether they have finished, usually waiting for a short time in between those checks. Another way is to register a "handler" or "callback" - code which will execute when the operation has finished. While your asynchronous operation can't usually return a value or an error (it hasn't happened yet!), they will often return some sort of ID or tracking information so that you can later poll them or register a handler. Promises (see below) are one form of tracking information for async operations.
Async operations are usually more complicated to deal with than sync operations, but they can be far more efficient in some cases. Mixing sync with async is often a recipe for headaches, and requires a very carefully designed interface between the two.
A "handler," "callback," or "hook" is a bit of code that runs in response to an operation. They can be sync or async. For this document, you can treat all three of those words as completely identical.
Large programs that live a long time have a lot of coupling (also see Jim Weirich's closely-related term "connascence.") You wind up with pairs of objects that reference each other and call each other's methods. This isn't a great thing, and it makes it hard to refactor later.
With libraries, you can reduce coupling and connascence by not having the called library functions know about the caller. They can be used across a variety of programs, and a variety of tasks, by not knowing who is calling them or how it will be done. They establish ground rules for how they get called, but you don't have to modify them in order to modify their callers (we hope.)
With inheritance you can reduce coupling and connascence by letting the parent classes not know about the child classes. They work a lot like libraries in most ways.
Events, like libraries and inheritance, are a way to reduce coupling by having one side (event sources) provide an interface and a "contract" for how it's used without knowing about its callers/consumers. Events are different from libraries because when an event is sent it may have no receiver, or one receiver, or many receivers. They're different from OO inheritance because they don't directly provide properties (state) that the receivers can modify. Event subscriptions require the subscriber to know about the sender, but not the other direction. Event sending doesn't require the sender to know anything about the receiver at all, or even whether there is a receiver.
The flip side is that, since an event sender can't even be sure anybody is listening, they can't easily say "was there an error in response to this?" or "did this return a value?" All that has to be handled on the receiving side... if there is one.
An Event can be sync or async. If an Event is sync, then when it happens, all its handlers will execute and then you'll get control back. If it's async, then firing the event will result in async operations -- a lot of handlers will happen, but you don't necessarily know when.
Sync and async events can both be complicated to reason through. If an event is sync and it causes other events to occur ("I clicked the mouse, which turned into selecting a list item, which ran a user callback, which created an alert, which...") then all of that has to happen before control returns to your code. If your event is async and spawns lots of other events and operations, you still have a lot going on, but you probably have much less control over what happens when.
The Shoes side of the Shoes/Display separation is mostly synchronous for simplicity. Async is much harder to debug and test. However, Webview and most UI libraries are basically async. That means the Shoes/Display interface has to be very carefully designed. Also it's not that easy to test.
A Promise is an object meant to track an asynchronous operation in progress. We model Scarpe Promises on JavaScript Promises, though there are some differences due to our different execution model and flow of control. They're also a lot like Concurrent Ruby's Promise objects... But Concurrent Ruby uses background threads for async operations, which isn't what we wanted for Scarpe. We have a simplified Promise implementation with no built-in concurrency (no threads, no fibers, no extra processes, etc.)
When Scarpe's Webview Display Service starts an asynchronous operation, such as creating a new drawable or changing the appearance of one, it will usually return a Promise. You can poll the Promise to see if it's complete, and whether it succeeded or failed, and what return value or error it received. But more usually, you would register a handler on that promise, which will run when it succeeds or fails.
Scarpe's Promises are basically synchronous. When they are fulfilled or rejected, their handlers happen immediately. If you want an async operation for that, you can schedule one from the (synchronous) handler. This means they have the ordering issues mentioned above -- if a Promise handler fulfills another Promise then that Promise's handlers will also happen immediately, before control returns from the first Promise being fulfilled. This can be complicated. You can think of it as being like a depth-first search, where your code will call handlers, and the handlers' handlers, and their handlers' handlers' handlers and so on before returning.