GithubHelp home page GithubHelp logo

henriqueinonhe / promises-training Goto Github PK

View Code? Open in Web Editor NEW
721.0 4.0 29.0 1.57 MB

Practice working with promises through a curated collection of interactive challenges. This repository provides a platform to refine your skills, complete with automated tests to to give you instant feedback and validate your progress.

License: Other

TypeScript 96.63% JavaScript 2.50% HTML 0.14% SCSS 0.73%
async async-await promises exercises practice

promises-training's Introduction

Promises Training

Warning

DO NOT INSTALL THIS PACKAGE AS A DEPENDENCY! (See: Getting Started)

Currently, promises are the de-facto way of handling asynchronous tasks in Javascript, and because of that, they're a fundamental part of any Javascript developer's knowledge.

However, when we're learning promises for the first time, we only learn enough to get by, that is, we learn a little bit of how to use promises, (nowadays, most likely with async/await only), the Promise.all method and that's it.

While this approach makes sense for beginners because it is enough to solve most problems in their daily lives, a recurring issue is that they stop there, that is, they never go past this initial knowledge.

And it's precisely this "learn just enough to get by" posture that holds many developers at their current level, as solving more complex problems requires a deeper understanding.

So, if you want to take your developer skills to the next level, swimming in shallow waters won't cut it, you must go deeper, you need to fully understand promises, and how they work, you need to be proficient in both async/await and then/catch styles of promise handling, and be able to orchestrate asynchronous tasks in the most efficient way possible.

Also, as promises are ultimately an abstraction to handle asynchronous tasks, being able to tackle common problems related to asynchronous programming is a must.

With that in mind, we created this project precisely to help you do this deep dive into promises and asynchronous programming.

By providing both explanations and practical exercises surrounding these topics, this project aims to be your companion in this journey to master them.

Even if you're already a seasoned developer, you might learn a thing or two, like, for example, you might want to try solving concrete/parallelMaxConcurrency, concrete/concurrencyOverride, concrete/extractingResolvers and /foundation/promise as they present some interesting challenges.

Important

This project is not intended for people who are learning promises for the first time, as it assumes that you have at least some basic knowledge of promises, what they represent and how to use them both with async/await and then/catch.

Table of Contents

Getting Started

Warning

ATTENTION: THIS REPO IS NOT MEANT TO BE CLONED UNLESS YOU'RE CONTRIBUTING IF YOU'RE AN END USER, PLEASE FOLLOW THE INSTRUCTIONS BELOW

First, to install the project, run:

npm create promises-training@latest

Note

This project is exercise-driven, so the main goal is to solve them.

Occasionally, there will be explanations along with the exercises to help you understand what needs to be done, and also some context about the problem being solved.

Exercises are divided into three categories:

  • Graph -> Exercises that involve orchestrating tasks according to dependency graphs.
  • Concrete -> Exercises that simulate real-world problems.
  • Foundation -> Exercises based on the foundations of promises and their implementation, and common helpers.

Important

There isn't an specific order for categories, you can start from any of them and switch to another one even before finishing the other one completely. However, the exercises have different levels that will be discussed next.

Exercises are located within src/exercises/<category> folders, where <category> is one of the categories mentioned above.

For graph exercises, the base explanations are located in this README, in the graph section, and for each exercise, there is a graph.png that depicts the dependency graph for that specific exercise.

For concrete and foundation exercises, the explanations are located in the README.md inside the exercise's folder (e.g. src/exercises/concrete/parallelChunks/README.md).

To solve an exercise, you need to edit the src/exercises/<category>/<exercise>/exercise.ts file.

After solving an exercise, you may check your solution by running:

npm run check <category>/<exercise>

Tests are located within src/tests.

Generally, you will only work inside the exercises folder as tests are devised in a way that they tell you exactly what went wrong without having to look at their implementation, but if for any reason you get stuck or curious, you may peek at them.

The src/lib folder is intended for internal use only, so don't bother with it.

Also, to keep your installation forwards compatible with future versions, DO NOT modify any file outside the src/exercises folder.

Levels

Besides categories, exercises are also divided into levels, where exercises increase in difficulty as you progress through the levels.

There are three levels:

  1. Beginner
  2. Intermediate
  3. Advanced

Keep in mind that this classification is somewhat subjective, so YMMV and also you don't necessarily need to complete all exercises in a level to move to the next one.

Beginner

  • Graph -> 1 to 8
  • Concrete
    • serial
    • parallelChain
    • retry
    • serialCollectErrors
    • parallelCollectErrors
    • parallelChunks

Intermediate

  • Graph -> 9 to 17
  • Concrete
    • retryWithTimeout
    • retryWithBackoff
    • parallelCollectRetry
    • parallelMaxConcurrency
    • concurrencyAbort
    • concurrencyEnqueue
    • concurrencyOverride
    • extractingResolvers
  • Foundation
    • promiseAll
    • promiseAllSettled
    • promiseAny
    • promiseRace
    • promiseReject
    • promiseResolve
    • readFile
    • wait

Advanced

  • Graph -> 18 and beyond
  • Foundation
    • promise

Note

As you can see, currently, there aren't that many advanced exercises, but the idea is that new exercises will be added over time.

Tests

Each exercise is accompanied by automated tests so that you can check your solution.

To run a single exercise's tests, run:

npm run check <category>/<exercise>

For example, to run the tests for the parallelChunks exercise, run:

npm run check concrete/parallelChunks

Or, to run the graph exercise number 2, run:

npm run check graph/2/test.test.ts

Note

In the previous example, we needed to append /test.test.ts to the exercise's file otherwise it would also run for other graph exercises starting with 2, for example: exercises from 2 to 29.

We use Vitest as the test runner, so all of its CLI options are available.

Also, it's important to mention that graph exercises have some peculiarities, in the sense that they are generated automatically from the graph itself, and because of that, some exercises have a HUGE number of tests (some exercises have over 100k tests).

Of course, we don't run all of them as it would be prohibitively slow, so we only run a subset of them and it's possible to tweak the number of tests that are run and the also the subset.

You may read more in the graph exercises section.

Exercises

Currently, there are three exercise categories:

  1. Graph
  2. Concrete
  3. Foundation

Graph Exercises

A big part of dealing with asynchronous tasks is orchestrating them so that each task starts as soon as possible, and to properly orchestrate these tasks we need to understand the dependency relations between them.

In this category, you'll be presented with a dependency graph in each exercise and then you'll orchestrate the tasks in the graph in the most efficient way possible.

As the exercise is focused on the orchestration itself, tasks are created by calling createPromise(label), where label is a string that identifies the task.

Take this graph, for example:

There are two tasks in this graph, A and B, and B depends on A, which is represented by the arrow that comes out from B and points to A.

This means that B can only start after A has finished and A, as it doesn't depend on any other task, can start right away.

Thus, the most efficient implementation for this graph would be:

await createPromise("A");
await createPromise("B");

Tasks can also depend on more than one task:

In this graph, C depends on both A and B, so it can only start after both A and B have finished.

However, both A and B don't depend on any other task, so they can start right away.

The most efficient implementation for this graph would be:

await Promise.all([createPromise("A"), createPromise("B")]);
await createPromise("C");

Tasks can also have multiple different sets of dependencies where, if any of the sets is satisfied, then the task can start:

In this graph, C depends either on A or on B, which is represented by using different colors for each dependency set. The colors themselves don't have any specific meaning, they are used like this just so the dependencies are distinguished from one another.

Therefore, C can start as soon as either A or B has finished.

await Promise.any([createPromise("A"), createPromise("B")]);
await createPromise("C");

Last but not least, promises have two possible outcomes: they can either be fulfilled or rejected.

In this graph, we have task B which depends on A's fulfillment and task C which depends on A's rejection.

Important

Dashed edges are used to represent promise rejections.

This means that B can only start after A has been fulfilled and C can only start after A has been rejected.

As only one of these outcomes is possible, either B or C will be carried out.

Corresponding implementation:

try {
  await createPromise("A");

  try {
    await createPromise("B");
  } catch {}
} catch {
  await createPromise("C");
}

When doing graph exercises, you'll notice that three functions are being exported: mixed, asyncAwait, thenCatch.

The idea is for you to provide 3 different implementations:

  • mixed: this one is completely free, you can mix both async/await and then/catch,
  • asyncAwait: in this one you should only use async/await
  • thenCatch: in this one you should only use then/catch.

This way you'll be proficient in both styles of promise handling.

Also, at the end of the file, you'll notice that exports are being wrapped in a skipExercise, which skips tests for that specific implementation so that it doesn't litter the output.

As you implement a solution for each of those three, remove the skipExercise call for the implementation you want the tests to run. For example: if you already implemented the mixed solution, remove the skipExercise from it but keep the ones for asyncAwait and thenCatch until you implement them.

UI

To aid you in debugging your implementation for the graph exercises, we created a UI that allows you to simulate different execution "paths".

To open the UI, run:

npm run graph:ui

The UI is served as a web app and it looks like this.

Now let's explore each section:

The sidebar on the left allows you to select the exercise you want to debug.

This top section allows you to select the implementation you want to debug.

The right sidebar lets you control the exercise's execution flow by resolving/rejecting promises.

As promises are created, new entries are added to the sidebar.

This section at the center shows the records of the promises that were created and resolved/rejected at each step.

This section at the bottom shows a summary of the promises that were resolved/rejected at each step, in order.

Tests

As graph exercises are based on graphs (duh), it's possible to generate all possible tests for a given exercise automatically, which is what we do.

As one might imagine, the number of generated tests is sometimes HUGE, so we have a cap on the maximum number of tests that are run.

Also, to prevent biases, we don't run tests in the order they were generated, instead, we shuffle them.

This shuffling happens right after the tests are first generated, so that tests are deterministic, that is, every time you run graph exercises tests, you'll be running the same subset of tests.

However, it's possible to tweak both the cap and the subset of tests that are run.

To tweak the cap, you can run npm run graph:setGraphTestsCap <number>.

For example, to set the cap to 10000, run:

npm run graph:setGraphTestsCap 10000

To tweak the subset of tests that are run, you can run npm run graph:shuffleGraphTestData <graph-exercise-number>, which will reshuffle the tests for the specified graph exercise, which will then result in a different subset of tests.

For example, to reshuffle the tests for graph exercise number 2, run:

npm run graph:shuffleGraphTestData 2

Concrete Exercises

Graph exercises are great for understanding the dependency relations between tasks, however, they don't cover the full spectrum of possible scenarios, as only tasks whose dependencies are known at compile time and fixed can be represented by a graph.

Therefore we have this category of concrete exercises, where you'll be presented with concrete scenarios that you'll have to implement.

As each exercise in this category is unique, their description is colocated with their folder.

Foundation Exercises

Foundation exercises are designed to help you reinforce your understanding of the foundations of promises, by reimplementing promise-related functions and, eventually, the promise itself.

Descriptions are colocated with exercises.

Solutions

Solutions to the exercises can be found in this repo, e.g. https://github.com/henriqueinonhe/promises-training/blob/master/src/exercises/concrete/concurrencyAbort/exercise.ts .

However, we recommend that you only check the solutions after you've solved the exercise yourself, as the goal is for you to learn by solving the exercises.

Also, keep in mind that currently the solutions presented are not necessarily the best ones, which means that even if your solutions don't resemble the ones you'll find here at all, it doesn't mean that they're bad.

Upgrading

To ease upgrading to newer versions we created a migration script that automatically migrates your installation to the latest version while preserving your solutions.

To run the migration script, run:

npm create promises-training@latest -- --migrate

License

This project is licensed under CC-BY-NC-ND 4.0.

The goal behind this project is to be a FREE learning resource and for it to remain free and accessible forever.

Here is a Q&A of some common questions regarding the license:

Can I use this project for self or group study?

Yes, please do.

Can I use this project in an internal company training?

Yes, as long as you credit the project and make it clear that the project is freely accessible independently of the training.

Can I use this project for my paid mentoring/workshop sessions?

Yes, as long as you credit the project, make it clear that the project is freely accessible independently of the mentoring/workshop, make it clear that you're charging for your time and not for the project itself, make it clear that the project is not part of your own material and make it clear that we do not endorse you or your services.

Can I use this project for my paid online course?

Yes, as long as you credit the project, make it clear that the project is freely accessible independently of the online course, make it clear that you're charging for your time and not for the project itself, make it clear that the project is not part of your own material and make it clear that we do not endorse you or your services.

Can I create a fork of this project and use it for my own purposes?

No, you can't. You can only use this project as is, without any modifications. This is necessary to prevent people from creating forks and then charging for them.

Can I create an online course based on this project?

No, you can't, because we don't want people creating "wrappers" around this project and then charging for them.

If you have any questions regarding the license, or want to talk about a specific use case, feel free to reach me at [email protected].

promises-training's People

Contributors

agerard57 avatar akatquas avatar cwrichardkim avatar henriqueinonhe avatar jprask avatar povilas-dev avatar talyssonoc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

promises-training's Issues

Error initializing graph exercises tests data

Fresh install. Windows 10, node v18.15.0. For some reason script tried to navigate to 'D:\D:\work\promises\.data\graph which is obviously an incorrect path.

Initializing graph exercises tests data...

> [email protected] graph:generateTests
> ts-node ./scripts/generateGraphTestsData.ts

[Error: ENOENT: no such file or directory, mkdir 'D:\D:\work\promises\.data\graph'] {
  errno: -4058,
  code: 'ENOENT',
  syscall: 'mkdir',
  path: 'D:\\D:\\work\\promises\\.data\\graph'
}

Error initializing graph exercises tests data
Error: Command failed: npm run graph:generateTests
    at checkExecSyncError (node:child_process:885:11)
    at execSync (node:child_process:957:15)
    at run (C:\Users\Uliss\AppData\Local\npm-cache\_npx\117e15c697a42bb1\node_modules\create-promises-training\bin.cjs:19:3)
    at initializeGraphTestsData (C:\Users\Uliss\AppData\Local\npm-cache\_npx\117e15c697a42bb1\node_modules\create-promises-training\bin.cjs:155:5)
    at setup (C:\Users\Uliss\AppData\Local\npm-cache\_npx\117e15c697a42bb1\node_modules\create-promises-training\bin.cjs:45:9)
    at async main (C:\Users\Uliss\AppData\Local\npm-cache\_npx\117e15c697a42bb1\node_modules\create-promises-training\bin.cjs:32:3) {
  status: 1,
  output: [ null, null, null ],
  pid: 12484,
  stderr: null
}
npm notice
npm notice New major version of npm available! 9.5.0 -> 10.2.4
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.2.4
npm notice Run npm install -g [email protected] to update!
npm notice
npm ERR! code 1
npm ERR! path D:\work\promises
npm ERR! command failed
npm ERR! command C:\Windows\system32\cmd.exe /d /s /c create-promises-training

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\Uliss\AppData\Local\npm-cache\_logs\2023-11-24T10_54_16_816Z-debug-0.log

2023-11-24T10_54_16_816Z-debug-0.log

Make It Clear That Injected Stuff Must Be Used (e.g. `now`)

Some exercises require the use of things like setTimeout and now, and, in order for the tests to work correctly, users MUST use the implementations that are provided as arguments in the exercise's implementations, this way we can inject an implementation of these services that we control during tests.

We need to make it clear for users that this is the case, otherwise they might be tempted to use stuff like Date instead of now, etc, which would not be compatible with tests.

Question: best practises

Thank you for this awesome project! Just a question: Will best practices or solutions be added eventually? Discussions might be a perfect place for such a thing.

Improve Graph Exercises UI

The graph exercises UI (npm run graph:ui) has a very basic styling and looks very ugly.

It would be nice to have it look better and be easier to work with.

Improve `promiseAll` and `promiseAllSettled` tests

Currently, tests for both promiseAll and promiseAllSettled only check for a specific resolution order for promises and for a single promise list length.

One of the problems that this causes, for example, for the promiseAll exercise is that there is a false negative (test passes but implementation is faulty) for the following implementation:

export default async <T>(promises: Array<Promise<T>>): Promise<Array<T>> => {
  const promiseCount = promises.length;
  const resolvedValues: Array<T> = [];

  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      promise.then((result) => {
        resolvedValues[index] = result;

        if (resolvedValues.length === promisesCount) {
          resolve(resolvedValues);
        }
      }, reject);
    });
  });
};

In this case, if the last promise is not the last one to be resolved, the outer promise will resolve because when you do array[index] = something, the array is considered to have length === index + 1 for it backfills all elements before the one you're setting with undefined.

Moving forward, we want to do two things:

  1. Create tests for different lengths of promise lists, including an empty list.
  2. For each promise list, we want to exercise all possible orders (permutations) of promise resolution.

Error during installation: `TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /.../generateGraphTestsData.ts`

Tried install using npm create promises-training@latest

got the following error:

Initializing graph exercises tests data...

> [email protected] graph:generateTests
> ts-node ./scripts/generateGraphTestsData.ts

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/sxxxxxa/Code/promise-trainer/scripts/generateGraphTestsData.ts
    at new NodeError (node:internal/errors:405:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:99:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:142:36)
    at defaultLoad (node:internal/modules/esm/load:91:20)
    at nextLoad (node:internal/modules/esm/hooks:733:28)
    at load (/Users/shxxxxa/Code/promise-trainer/node_modules/ts-node/dist/child/child-loader.js:19:122)
    at nextLoad (node:internal/modules/esm/hooks:733:28)
    at Hooks.load (node:internal/modules/esm/hooks:377:26)
    at MessagePort.handleMessage (node:internal/modules/esm/worker:168:24)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:778:20) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Error initializing graph exercises tests data
Error: Command failed: npm run graph:generateTests
    at checkExecSyncError (node:child_process:887:11)
    at execSync (node:child_process:959:15)
    at run (/Users/shxxxxa/.npm/_npx/117e15c697a42bb1/node_modules/create-promises-training/bin.cjs:19:3)
    at initializeGraphTestsData (/Users/shxxxxa/.npm/_npx/117e15c697a42bb1/node_modules/create-promises-training/bin.cjs:155:5)
    at setup (/Users/shxxxxa/.npm/_npx/117e15c697a42bb1/node_modules/create-promises-training/bin.cjs:45:9)
    at async main (/Users/shxxxxxa/.npm/_npx/117e15c697a42bb1/node_modules/create-promises-training/bin.cjs:32:3) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 30204,
  stdout: null,
  stderr: null
}

Make Migration Process More Robust

We want to make migrating to newer versions of this project as easy as possible, and to do that we have a migration script so that users can just run a command and have their projects automatically migrated to the most recent version.

Currently, the migration script (implemented in bin.cjs) is deceptively simple, and does the following:

  • Checks whether the current folder is actually a promises-training repo
  • Copies new files
  • Reinstalls dependencies

The "copies new files" step is the most sensitive one, as we want to update all "internal" files, like the ones inside src/lib, src/tests, but we want to leave existing files under src/execises untouched, as we don't want to erase users's solutions.

This migration script works in a very naive way as it doesn't consider that different versions might need different migration strategies.

Moving forward we want to use a migration approach that's similar to how database migrations work, where every time we have a new version we'll also have a migration that takes care of the specific migration between the previous and the current version, this way we only need to apply migrations in sequence and each migration can be independent of previous ones.

To do that we need:

  • A set of helpers to address common migration tasks, like copying specific files, merging old and new dependencies (because users might have installed their own dependencies so we can't just overwrite them), etc
  • An "orchestrator" that will be responsible for identifying which migrations need to be run according to the current version of the user's repo and the existing migrations

E2E Tests

This package has a lot of setup involved, a lot of things can go wrong during this setup.

We want to have some way of running E2E tests before publishing a new version so that we can make sure that:

  • Installation/setup works properly when using npm create promises-training@latest
  • Migrations work properly

It's very easy to mess up setup/installation/migrations in a way that you'll only notice it after you have published, and then you have to do a bunch of commits to master to get things right and you end up with a bunch of unusable versions, really annoying.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.