Skip to content

Write a tutorial about "Build an Autocomplete with Structured Concurrency using Effection in React" #880

Open

Description

@taras

The autocomplete example's logic is ready. We should now write a blog post about it. The focus should be on describing the pattern of doing autocomplete with SC. The main takeaway is that SC organizes operations into a tree where an operation can have children. One of the rules of SC is that a child can not outlive its parent, we can use this rule to design out autocomplete. We'll build from nothing to a working autocomplete in React.

It's going to be broken up into 3 steps

  1. Write autocomplete operation in Effection in Node
  2. Hook it up into React
  3. Add loading and error states

Activity

  1. changed the title [-]Write tutorial about "Build an Autocomplete with Structured Concurrency using Effection in React"[/-] [+]Write blog post about "Build an Autocomplete with Structured Concurrency using Effection in React"[/+] on Jan 6, 2024
  2. changed the title [-]Write blog post about "Build an Autocomplete with Structured Concurrency using Effection in React"[/-] [+]Write a tutorial about "Build an Autocomplete with Structured Concurrency using Effection in React"[/+] on Jan 6, 2024
  3. minkimcello commented on Jan 31, 2024

    @minkimcello
    Collaborator

    I think we'll start with a more introductory blog post first - Retrying in Javascript with Structured Concurrency using Effection.

    We can begin the blog post with an example of 2 retries of a fetch call written in node and from there we can sprinkle in two more requirements involving a timer and the retry limit changing based on the response status code? The additional requirements will make it clear that it would be a pain in the butt to implement correctly and we'll say it's easy with effection and show how it's done.

  4. taras commented on Jan 31, 2024

    @taras
    MemberAuthor

    Sounds good. Let's do it!

  5. minkimcello commented on Feb 1, 2024

    @minkimcello
    Collaborator

    @cowboyd I wrote the javascript examples of fetching with additional requirements (without effection)

    1. Are the examples below how an average developer would write these implementations in javascript?
    2. Are the two additional requirement examples complex enough that we can drive the point across about this all being easier/better with effection?

    Simple Retries

    let retries = 2;
    
    while (retries > 0) {
      const response = await fetch(url);
      if (response.ok) {
        return;
      }
      retries--;
    }

    More Requirements

    Retry twice but up to 5 times for 5xx error codes

    let retries = 2;
    let retries_500 = 5;
    
    while (retries > 0 && retries_500 > 0) {
      const response = await fetch(url);
      if (response.ok) {
        return;
      }
      if (response.statusCode > 499) {
        retries_500--;
      }
      if (response.statusCode < 500) {
        retries--;
      }
    }

    Retry twice but up to 5 times for 5xx error codes, cancel after 5 seconds

    let retries = 2;
    let retries_500 = 5;
    
    const controller = new AbortController();
    
    setTimeout(() => {
      retries = 0;
      retries_500 = 0;
      controller.abort
    }, 5_000);
    
    while (retries > 0 && retries_500 > 0) {
      const response = await fetch(url, { signal: controller.signal });
      if (response.ok) {
        return;
      }
      if (response.statusCode > 499) {
        retries_500--;
      }
      if (response.statusCode < 500) {
        retries--;
      }
    }
  6. taras commented on Feb 2, 2024

    @taras
    MemberAuthor

    Are the examples below how an average developer would write these implementations in javascript?

    I don't think so. It would look close to that in Effection because we can easily interrupt any operation; without Effection, there would be no way to stop this code. Even if we use an abort controller, it would run while loop. Maybe if you checked the state of the abort controller on every cycle.

    Let me do some research.

  7. taras commented on Feb 2, 2024

    @taras
    MemberAuthor

    retry-fetch is perhaps the closest to what we're trying to show here. Here is the main code from this library.

        return new Promise(function (resolve, reject) {
          var wrappedFetch = function (attempt) {
            // As of node 18, this is no longer needed since node comes with native support for fetch:
            /* istanbul ignore next */
            var _input =
              typeof Request !== 'undefined' && input instanceof Request
                ? input.clone()
                : input;
            fetch(_input, init)
              .then(function (response) {
                if (Array.isArray(retryOn) && retryOn.indexOf(response.status) === -1) {
                  resolve(response);
                } else if (typeof retryOn === 'function') {
                  try {
                    // eslint-disable-next-line no-undef
                    return Promise.resolve(retryOn(attempt, null, response))
                      .then(function (retryOnResponse) {
                        if(retryOnResponse) {
                          retry(attempt, null, response);
                        } else {
                          resolve(response);
                        }
                      }).catch(reject);
                  } catch (error) {
                    reject(error);
                  }
                } else {
                  if (attempt < retries) {
                    retry(attempt, null, response);
                  } else {
                    resolve(response);
                  }
                }
              })
              .catch(function (error) {
                if (typeof retryOn === 'function') {
                  try {
                    // eslint-disable-next-line no-undef
                    Promise.resolve(retryOn(attempt, error, null))
                      .then(function (retryOnResponse) {
                        if(retryOnResponse) {
                          retry(attempt, error, null);
                        } else {
                          reject(error);
                        }
                      })
                      .catch(function(error) {
                        reject(error);
                      });
                  } catch(error) {
                    reject(error);
                  }
                } else if (attempt < retries) {
                  retry(attempt, error, null);
                } else {
                  reject(error);
                }
              });
          };

    This is probably the best example of how someone would do this without Effection, Observables or Effect.ts.

  8. minkimcello commented on Feb 2, 2024

    @minkimcello
    Collaborator

    @taras - not sure if @ notifications get sent if it's added in an edit 🤷

    there would be no way to stop this code. Even if we use an abort controller, it would run while loop. Maybe if you checked the state of the abort controller on every cycle.

    Are we talking about just dropping/stopping everything at any point in the code? Isn't the abort controller (in my last example) saying, after 5 seconds, we're going to stop fetching and let it run the rest of the while loop but it won't do another cycle?

    retry-fetch is perhaps the closest to what we're trying to show here. Here is the main code from this library.

    So then would the structure of the blogpost be:

    1. Here's a quick (but full of compromises) way of doing it (my examples)
    2. But here's a better way of doing it (with retry-fetch) because the examples from [1] is missing {x}
    3. And here's what it looks like with effection.
  9. taras commented on Feb 2, 2024

    @taras
    MemberAuthor

    saying, after 5 seconds, we will stop fetching and let it run the rest of the while loop, but it won't do another cycle?

    It's worth a try. The libraries I listed above don't seem to support AbortController explicitly. fetch-retry tests don't have a mention of abort. What do you think about making a small test harness to test the behavior of these different libraries? We can figure out the narrative once we have a better understanding of what the status quo is.

  10. minkimcello commented on Feb 3, 2024

    @minkimcello
    Collaborator

    Updated outline

    • Intro: Retry/back-off = common pattern
    • Common pattern but it's complicated
      • JS doesn't give guarantees
      • Need to thread abort controllers to each layer
    • Most developers would look for libraries
      • [Example libraries]
      • Nobody wants to write code like this
    • We wrote our own async/await implementation for retry/back-off
    • Our implementations and the other libraries are still missing {x}
      • Link event horizon blog post
    • Solution using structured concurrency
      • Easy to write from scratch and also addresses {x}
  11. minkimcello commented on Feb 5, 2024

    @minkimcello
    Collaborator

    @taras ☝️

  12. taras commented on Feb 5, 2024

    @taras
    MemberAuthor

    Yeah, that looks good. I'm going to put together a small test to see how it behaves

  13. taras commented on Feb 6, 2024

    @taras
    MemberAuthor

    @minkimcello I found an interesting library called abort-controller-x for composing abort controller aware asyncronous functions. Interestingly, they wrote the code the same way you did in your example. The point of this library is that it makes it easy to thread the abort controller through many async operations. We could mention it in our "Need to thread abort controllers to each layer or use something like abort-controller-x to compose abort controller aware async operations"

    https://github.com/deeplay-io/abort-controller-x/blob/master/src/retry.ts#L44-L93

    This is the best example of writing a retry/backoff using an abort controller.

    export async function retry<T>(
      signal: AbortSignal,
      fn: (signal: AbortSignal, attempt: number, reset: () => void) => Promise<T>,
      options: RetryOptions = {},
    ): Promise<T> {
      const {
        baseMs = 1000,
        maxDelayMs = 30000,
        onError,
        maxAttempts = Infinity,
      } = options;
    
      let attempt = 0;
    
      const reset = () => {
        attempt = -1;
      };
    
      while (true) {
        try {
          return await fn(signal, attempt, reset);
        } catch (error) {
          rethrowAbortError(error);
    
          if (attempt >= maxAttempts) {
            throw error;
          }
    
          let delayMs: number;
    
          if (attempt === -1) {
            delayMs = 0;
          } else {
            // https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/
            const backoff = Math.min(maxDelayMs, Math.pow(2, attempt) * baseMs);
            delayMs = Math.round((backoff * (1 + Math.random())) / 2);
          }
    
          if (onError) {
            onError(error, attempt, delayMs);
          }
    
          if (delayMs !== 0) {
            await delay(signal, delayMs);
          }
    
          attempt += 1;
        }
      }
    }

    It looks almost identical to how you'd implement it in Effection except you don't need to manage abort controller manually.

  14. cowboyd commented on Feb 6, 2024

    @cowboyd
    Member

    I just want to make sure that we don't drown out the simplicity of the Effection example by showing too many ways to do it.

  15. minkimcello commented on Feb 7, 2024

    @minkimcello
    Collaborator

    I just want to make sure that we don't drown out the simplicity of the Effection example by showing too many ways to do it.

    We'll just jump right into the Effection implementation and highlight all the advantages without referencing other libraries. That should simplify the blogpost quite a bit.

  16. minkimcello commented on Feb 7, 2024

    @minkimcello
    Collaborator

    @taras @cowboyd How does this look? https://github.com/minkimcello/effection-retries/blob/main/blog.ts - this will be for the code snippet for the blog post.

    There's also this file that I was running locally https://github.com/minkimcello/effection-retries/blob/main/index.ts to run a fake fetch.

    I noticed I couldn't resolve from inside the while loop. Is that by design?

  17. taras commented on Feb 7, 2024

    @taras
    MemberAuthor

    @minkimcello I think we can make that example much simpler.

    1. we should use run here instead of main
    2. the action is not needed
    3. instead of setTimeout, we should use sleep from effection
    4. Let's use the backoff logic from abort-controller-x
    5. we don't need both retries and retries_500
    6. let's start at retries -1 and go up to maxTimeout
  18. minkimcello commented on Feb 9, 2024

    @minkimcello
    Collaborator

    @taras I was trying to replace the fetch with a fake fetch and test out the script. but I noticed none of the console logs from operations inside race() aren't being printed. are we calling the operations incorrectly inside race? https://github.com/minkimcello/effection-retries/blob/main/index.ts#L60-L64

  19. cowboyd commented on Feb 10, 2024

    @cowboyd
    Member

    That is very odd. I would expect it to log... unless there was an error or someone else won the race.

  20. taras commented on Feb 10, 2024

    @taras
    MemberAuthor

    We figured it out. It's a missing yield in front of race

  21. minkimcello commented on Feb 13, 2024

    @minkimcello
    Collaborator

    wrote the code examples (and not much of the text (yet)) for the blogpost: thefrontside/frontside.com#374

    Is that too long? Maybe we can cut out the last part? the Reusable section

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions