#webhook #callback #api-client #non-blocking #imperative #request #received

fish

An imperative webhook/callback client API based on async/await

3 releases

0.0.3 Oct 26, 2021
0.0.2 Nov 22, 2020
0.0.1 Nov 11, 2020

#3 in #received

22 downloads per month

MIT/Apache

20KB
264 lines

Fish

Using an event based interaction pattern for callback communication is often praised as ideal, but, for many cases we've found this results in complexity, bugs, and unnecessary work. Fish provides imperative non-blocking runtime interaction for callback and webhook based APIs.

Let's contrive an example to see how this works: imagine you are the developer of some plugin for a delivery app.

The app will notify you (... via a webhook) when a new delivery region opens. There is an endpoint that allows you to register a webhook for new orders in that region.

Whenever an order is received, you can send a request to accept it. The delivery app will then send you a callback if your request is granted. Oh, I forgot to mention, you are a broker, so, whenever the order is received, you need to pass it along to your fulfillment client, who will likewise send a callback if they desire to fulfill the order.

Along each step, you have domain-specific logic and state that is often used throughout the entire lifecycle.

In Fish, this interaction looks something like:

// Step 1: Register webhook for new delivery regions
let orders = server.spawn();

app.send("region", RegisterRegion {
    webhook_url: orders.url(),
    region_id
}).await;

// orders is a Stream. You could concatenate multiple regions' orders together,
// or maybe handle them separately
while let Ok(order) = orders.next().await {
   // Step 2: Let's see if our fulfillment partner is interested in the order
   let fulfillment = server.spawn();

    partner.send("order", NotifyOrder {
        callback: fulfillment.url(),
        ..order
    }).await;

    // The partner will send us back a POST request if they want to fulfill the order,
    // and do-nothing otherwise. We have a time limit to adhere to, so we'll give them
    // 5 seconds to respond
   if let Ok(_) = timeout(Duration::from_secs(5), fulfillment.next().await) {
     // Step 3: OK, we're set, lets let the app know we are interested!
     let granted = server.spawn();
     app.send("order", AcceptOrder {
          callback_url: url,
          ..
     }).await;

     // granted.next() and so on!
   }
}

Ok, we could've drawn this out a lot further. And sorely missing is the domain-specific logic, state updates, caching, and so on that happens during this orchestration. We've found doing this in an event-handler-based manner takes an enormous effort.

What about you? Have you found this to be challenging? Any suggestions?

License: MIT OR Apache-2.0

To think about:

How does this pattern mesh well with CI/CD, hardware faults, and so on? For example, let's say there are 10 callbacks outstanding. If the server is restarted, the receivers for that will cease. In an "event-based" system, the server would boot back up and everything would be hunky-dory, the 3rd party might see a 504 while down and do a retry.

There is unrecoverable state.

Dependencies

~9–19MB
~271K SLoC