diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
new file mode 100644
index 0000000..ef67730
--- /dev/null
+++ b/.github/workflows/documentation.yml
@@ -0,0 +1,10 @@
+name: Documentation
+on:
+ push:
+ branches: [main]
+permissions:
+ contents: write
+jobs:
+ deploy:
+ uses: innmind/github-workflows/.github/workflows/documentation.yml@main
+ secrets: inherit
diff --git a/.gitignore b/.gitignore
index ff72e2d..fe80fd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/composer.lock
/vendor
+/.cache
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6b5c6f1
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,6 @@
+# This command is intended to be run on your computer
+serve-doc:
+ docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material
+
+build-doc:
+ docker run --rm -it -v ${PWD}:/docs squidfunk/mkdocs-material build
diff --git a/README.md b/README.md
index add340f..bfb1165 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
Abstraction on top of `Fiber`s to coordinate multiple tasks asynchronously.
-The goal is to easily move the execution of any code built using [`innmind/operating-system`](https://packagist.org/packages/innmind/operating-system) from a synchronous context to an async one. This means that it's easier to experiment running a piece of code asynchronously and then move back if the experiment is not successful. This also means that you can test each part of an asynchronous system synchronously.
+The goal is to easily move the execution of any code built using [`innmind/operating-system`](https://innmind.org/OperatingSystem/) from a synchronous context to an async one. This means that it's easier to experiment running a piece of code asynchronously and then move back if the experiment is not successful. This also means that you can test each part of an asynchronous system synchronously.
## Installation
@@ -22,116 +22,23 @@ use Innmind\Async\{
Scope\Continuation,
};
use Innmind\OperatingSystem\{
- Factory,
OperatingSystem,
-};
-use Innmind\Filesystem\Name;
-use Innmind\HttpTransport\Success;
-use Innmind\Http\{
- Request,
- Method,
- ProtocolVersion,
-};
-use Innmind\Url\{
- Url,
- Path,
+ Factory,
};
use Innmind\Immutable\Sequence;
-[$users] = Scheduler::of(Factory::build());
- ->sink([0, 0, false])
+Scheduler::of(Factory::build())
+ ->sink(null)
->with(
- static function(array $carry, OperatingSystem $os, Continuation $continuation, Sequence $results): Continuation {
- [$users, $finished, $launched] = $carry;
-
- if (!$launched) {
- return $continuation
- ->carryWith([$users, $finished, true])
- ->schedule(Sequence::of(
- static fn(OperatingSystem $os): int => $os
- ->remote()
- ->http()(Request::of(
- Url::of('http://some-service.tld/users/count'),
- Method::get,
- ProtocolVersion::v11,
- ))
- ->map(static fn(Success $success): string => $success->response()->body()->toString())
- ->match(
- static fn(string $response): int => (int) $response,
- static fn() => throw new \RuntimeException('Failed to count the users'),
- ),
- static fn(OperatingSystem $os): int => $os
- ->filesystem()
- ->mount(Path::of('some/directory/'))
- ->unwrap()
- ->get(Name::of('users.csv'))
- ->map(static fn($file) => $file->content()->lines())
- ->match(
- static fn(Sequence $lines) => $lines->reduce(
- 0,
- static fn(int $total): int => $total + 1,
- ),
- static fn() => throw new \RuntimeException('Users file not found'),
- ),
- ));
- }
-
- $finished += $results->size();
- $users = $results->reduce(
- $users,
- static fn(int $total, int $result): int => $total + $result,
- );
- $continuation = $continuation->carryWith([$users, $finished, $launched]);
-
- if ($finished === 2) {
- $continuation = $continuation->finish();
- }
-
- return $continuation->wakeOnResult();
- },
+ static fn(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ) => $continuation
+ ->schedule(Sequence::of(
+ static fn(OperatingSystem $os) => importUsers($os),
+ static fn(OperatingSystem $os) => importProducts($os),
+ ))
+ ->finish(),
);
```
-
-This example counts a number of `$users` coming from 2 sources.
-
-The `Scheduler` object behaves as a _reduce_ operation, that's why it has 2 arguments: a carried value and a reducer (called a scope in this package).
-
-The carried value here is an array that holds the number of fetched users, the number of finished tasks and whether it already launched the tasks or not.
-
-The scope will launch 2 tasks if not already done; the first one does an HTTP call and the second one counts the number of lines in a file. The scope will be called again once a task finishes and their results will be available inside the fourth argument `$results`, it will add the number of finished tasks and the number of users to the carried value array. If both tasks are finished then the scope calls `$continuation->terminate()` to instruct the loop to stop.
-
-When the scope calls `->terminate()` and that all tasks are finished then `->with()` returns the carried value. Here it will assign the aggregation of both tasks results to the value `$users`.
-
-> [!NOTE]
-> As long as you use the `$os` abstraction passed as arguments the system will automatically suspend your code when necessary. This means that you don't even need to think about it.
-
-> [!NOTE]
-> The scope `callable` is also run asynchronously. This means that you can use it to build a socket server and wait indefinitely for new connections without impacting the execution of already started tasks.
-
-> [!WARNING]
-> Do NOT return the `$os` variable outside of the tasks or the scope as it may break your code.
-
-> [!NOTE]
-> Since this package has been designed by only passing arguments (no global state) it means that you can compose the use of `Scheduler`, this means that you can run a new instance of `Scheduler` inside a task and it will behave transparently. (Although this feature as not been tested yet!)
-
-## Limitations
-
-### HTTP calls
-
-Currently HTTP calls are done via `curl` but it can't be integrated in the same loop as other streams. To allow the coordination of multiple tasks when doing HTTP calls the system use a timeout of `10ms` and switches between tasks at this max rate.
-
-To fix this limitation a new implementation entirely based on PHP streams needs to be created.
-
-Meanwhile if your goal is to make multiple concurrent HTTP calls you don't need this package. [`innmind/http-transport`](https://innmind.org/documentation/getting-started/concurrency/http/) already support concurrent calls on it's own (without the limitation mentionned above).
-
-### SQL queries
-
-SQL queries executed via `$os->remote()->sql()` are still executed synchronously.
-
-To fix this limitation a new implementation entirely based on PHP streams needs to be created.
-
-### Number of tasks
-
-It seems that the current implementation of this package has a [limit of around 100K concurrent tasks](https://bsky.app/profile/baptouuuu.bsky.social/post/3lwr7pei2ek2f) before it starts slowing down drastically.
-
-A simple script scheduling 100k tasks that each halts the process for 10 second will take ~13s.
diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png
new file mode 100644
index 0000000..08dee3a
Binary files /dev/null and b/docs/assets/favicon.png differ
diff --git a/docs/assets/fonts/MonaspaceNeon-Regular.woff b/docs/assets/fonts/MonaspaceNeon-Regular.woff
new file mode 100644
index 0000000..ce0168b
Binary files /dev/null and b/docs/assets/fonts/MonaspaceNeon-Regular.woff differ
diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg
new file mode 100644
index 0000000..6a5d322
--- /dev/null
+++ b/docs/assets/logo.svg
@@ -0,0 +1,24 @@
+
diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css
new file mode 100644
index 0000000..e4aa2d4
--- /dev/null
+++ b/docs/assets/stylesheets/extra.css
@@ -0,0 +1,113 @@
+@font-face {
+ font-family: "Monaspace Neon";
+ font-weight: normal;
+ font-style: normal;
+ src: url("../fonts/MonaspaceNeon-Regular.woff");
+}
+
+:root {
+ --md-code-font: "Monaspace Neon";
+}
+
+:root {
+ --light-md-code-hl-number-color: #f76d47;
+ --light-md-code-hl-function-color: #6384b9;
+ --light-md-code-hl-operator-color: #39adb5;
+ --light-md-code-hl-constant-color: #7c4dff;
+ --light-md-code-hl-string-color: #9fc06f;
+ --light-md-code-hl-punctuation-color: #39adb5;
+ --light-md-code-hl-keyword-color: #7c4dff;
+ --light-md-code-hl-variable-color: #80cbc4;
+ --light-md-code-hl-comment-color: #ccd7da;
+ --light-md-code-bg-color: #fafafa;
+ --light-md-code-fg-color: #ffb62c;
+ --light-md-code-hl-variable-color: #6384b9;
+ --dark-md-code-hl-number-color: #f78c6c;
+ --dark-md-code-hl-function-color: #82aaff;
+ --dark-md-code-hl-operator-color: #89ddff;
+ --dark-md-code-hl-constant-color: #c792ea;
+ --dark-md-code-hl-string-color: #c3e88d;
+ --dark-md-code-hl-punctuation-color: #89ddff;
+ --dark-md-code-hl-keyword-color: #c792ea;
+ --dark-md-code-hl-variable-color: #e8f9f9;
+ --dark-md-code-hl-comment-color: #546e7a;
+ --dark-md-code-bg-color: #263238;
+ --dark-md-code-fg-color: #ffcb6b;
+ --dark-md-code-hl-variable-color: #82aaff;
+}
+
+@media (prefers-color-scheme: light) {
+ .language-php > * {
+ --md-code-hl-number-color: var(--light-md-code-hl-number-color);
+ --md-code-hl-function-color: var(--light-md-code-hl-function-color);
+ --md-code-hl-operator-color: var(--light-md-code-hl-operator-color);
+ --md-code-hl-constant-color: var(--light-md-code-hl-constant-color);
+ --md-code-hl-string-color: var(--light-md-code-hl-string-color);
+ --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color);
+ --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color);
+ --md-code-hl-variable-color: var(--light-md-code-hl-variable-color);
+ --md-code-hl-comment-color: var(--light-md-code-hl-comment-color);
+ --md-code-bg-color: var(--light-md-code-bg-color);
+ --md-code-fg-color: var(--light-md-code-fg-color);
+ }
+
+ .language-php .na {
+ --md-code-hl-variable-color: var(--light-md-code-hl-variable-color);
+ }
+}
+
+[data-md-color-media="(prefers-color-scheme: light)"] .language-php > * {
+ --md-code-hl-number-color: var(--light-md-code-hl-number-color);
+ --md-code-hl-function-color: var(--light-md-code-hl-function-color);
+ --md-code-hl-operator-color: var(--light-md-code-hl-operator-color);
+ --md-code-hl-constant-color: var(--light-md-code-hl-constant-color);
+ --md-code-hl-string-color: var(--light-md-code-hl-string-color);
+ --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color);
+ --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color);
+ --md-code-hl-variable-color: var(--light-md-code-hl-variable-color);
+ --md-code-hl-comment-color: var(--light-md-code-hl-comment-color);
+ --md-code-bg-color: var(--light-md-code-bg-color);
+ --md-code-fg-color: var(--light-md-code-fg-color);
+}
+
+[data-md-color-media="(prefers-color-scheme: light)"] .language-php .na {
+ --md-code-hl-variable-color: var(--light-md-code-hl-variable-color);
+}
+
+@media (prefers-color-scheme: dark) {
+ .language-php > * {
+ --md-code-hl-number-color: var(--dark-md-code-hl-number-color);
+ --md-code-hl-function-color: var(--dark-md-code-hl-function-color);
+ --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color);
+ --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color);
+ --md-code-hl-string-color: var(--dark-md-code-hl-string-color);
+ --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color);
+ --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color);
+ --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color);
+ --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color);
+ --md-code-bg-color: var(--dark-md-code-bg-color);
+ --md-code-fg-color: var(--dark-md-code-fg-color);
+ }
+
+ .language-php .na {
+ --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color);
+ }
+}
+
+[data-md-color-media="(prefers-color-scheme: dark)"] .language-php > * {
+ --md-code-hl-number-color: var(--dark-md-code-hl-number-color);
+ --md-code-hl-function-color: var(--dark-md-code-hl-function-color);
+ --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color);
+ --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color);
+ --md-code-hl-string-color: var(--dark-md-code-hl-string-color);
+ --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color);
+ --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color);
+ --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color);
+ --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color);
+ --md-code-bg-color: var(--dark-md-code-bg-color);
+ --md-code-fg-color: var(--dark-md-code-fg-color);
+}
+
+[data-md-color-media="(prefers-color-scheme: dark)"] .language-php .na {
+ --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color);
+}
diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
new file mode 100644
index 0000000..0055fea
--- /dev/null
+++ b/docs/getting-started/index.md
@@ -0,0 +1,45 @@
+# Getting started
+
+## Installation
+
+```sh
+composer require innmind/async
+```
+
+## Setup
+
+```php title="async.php"
+sink(null) #(1)
+ ->with(function(
+ $_, #(2)
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ // this is the scope that will schedule tasks
+
+ return $continuation;
+ });
+```
+
+1. You'll learn in a [later chapter](../scopes/lifecycle.md#carry-a-value) what this value is. For now leave it like this.
+2. You'll learn in a [later chapter](../scopes/lifecycle.md#carry-a-value) what this value is. For now leave it like this.
+
+You can run this script via `php async.php` in your terminal. For now it executes an infinite loop that does nothing.
+
+You'll see in the next chapter what you can inside the loop.
diff --git a/docs/getting-started/scope.md b/docs/getting-started/scope.md
new file mode 100644
index 0000000..2ef4f38
--- /dev/null
+++ b/docs/getting-started/scope.md
@@ -0,0 +1,204 @@
+# Create a scope
+
+As mentionned in the [preface](../preface/terminology.md#scope) a Scope is a function. In fact, it can be any `callable` that accept the following arguments:
+
+
+
+- a carried value (1)
+- an instance of the operating system
+- a continuation
+- a list of results
+
+
+
+1. You'll learn in a [later chapter](../scopes/lifecycle.md#carry-a-value) how to use this value.
+
+To keep things simple for now, we'll only talk about the second and third arguments. And to feel a bit more at home we'll use a class with the `__invoke` method instead of an anonymous function.
+
+If we re-implement the example from the previous page we get:
+
+=== "Scope"
+ ```php title="Scope.php"
+ use Innmind\Async\Scope\Continuation;
+ use Innmind\OperatingSystem\OperatingSystem;
+
+ final class Scope
+ {
+ public function __invoke(
+ mixed $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ return $continuation;
+ }
+ }
+ ```
+
+=== "Scheduler"
+ ```php title="async.php"
+ sink(null)
+ ->with(new Scope);
+ ```
+
+Once again, if you run `php async.php` in your terminal it will execute an infinite loop that does nothing.
+
+However by defining the scope with an object we see that the `__invoke` method will always be called on the same object. This means you can keep state inside properties if you want to!
+
+Now let's actually do something in this scope.
+
+The most basic thing you can do is halting the process:
+
+```php title="Scope.php" hl_lines="3 12-15"
+use Innmind\Async\Scope\Continuation;
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\TimeContinuum\Period;
+
+final class Scope
+{
+ public function __invoke(
+ mixed $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ $os
+ ->process()
+ ->halt(Period::second(1))
+ ->unwrap();
+
+ return $continuation;
+ }
+}
+```
+
+This will pause the scope for a second before returning the continuation. And since by default a continuation instruct the system to call the scope once again, this means that the `__invoke` method is called every second.
+
+Since we only work with the scope for now, this is the same as doing:
+
+```php
+do {
+ sleep(1);
+} while (true);
+```
+
+Now that we have a timer, we can do something else every second. For example we can fetch data via an HTTP call:
+
+```php title="Scope.php" hl_lines="3-9 23-38"
+use Innmind\Async\Scope\Continuation;
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\HttpTransport\Success;
+use Innmind\Http\{
+ Request,
+ Method,
+ ProtocolVersion,
+};
+use Innmind\Url\Url;
+use Innmind\TimeContinuum\Period;
+
+final class Scope
+{
+ public function __invoke(
+ mixed $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ $os
+ ->process()
+ ->halt(Period::second(1))
+ ->unwrap();
+ $users = $os
+ ->remote()
+ ->http()(
+ Request::of(
+ Url::of('https://somewhere.tld/api/users'),
+ Method::get,
+ ProtocolVersion::v11,
+ ),
+ )
+ ->match(
+ static fn(Success $success) => \json_decode(
+ $success->response()->body()->toString(),
+ true,
+ ),
+ static fn(object $error) => throw new \RuntimeException('An error occured'),
+ );
+
+ return $continuation;
+ }
+}
+```
+
+Now every second we call an api to fetch users and decode the response content. Putting everything inside the `__invoke` method can become quite verbose. But since we're in a class we can create other methods:
+
+```php title="Scope.php" hl_lines="19 24-46"
+use Innmind\Async\Scope\Continuation;
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\HttpTransport\Success;
+use Innmind\Http\{
+ Request,
+ Method,
+ ProtocolVersion,
+};
+use Innmind\Url\Url;
+use Innmind\TimeContinuum\Period;
+
+final class Scope
+{
+ public function __invoke(
+ mixed $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ $users = $this->fetch($os);
+
+ return $continuation;
+ }
+
+ /**
+ * @return list
+ */
+ private function fetch(OperatingSystem $os): array
+ {
+ $os
+ ->process()
+ ->halt(Period::second(1))
+ ->unwrap();
+ $users = $os
+ ->remote()
+ ->http()(
+ Request::of(
+ Url::of('https://somewhere.tld/api/users'),
+ Method::get,
+ ProtocolVersion::v11,
+ ),
+ )
+ ->match(
+ static fn(Success $success) => \json_decode(
+ $success->response()->body()->toString(),
+ true,
+ ),
+ static fn(object $error) => throw new \RuntimeException('An error occured'),
+ );
+ }
+}
+```
+
+We'll see in the next chapter how to run tasks for each of these users.
+
+!!! tip
+ You should explore the other APIs provided by the [operating system](https://innmind.org/OperatingSystem/).
diff --git a/docs/getting-started/task.md b/docs/getting-started/task.md
new file mode 100644
index 0000000..e8977b8
--- /dev/null
+++ b/docs/getting-started/task.md
@@ -0,0 +1,77 @@
+# Create a task
+
+Let's build a simple task that looks in a database if a user exists and if not inserts it.
+
+```php title="Task.php"
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\Url\Url;
+use Formal\AccessLayer\Query\{
+ SQL,
+ Parameter,
+};
+
+final class Task
+{
+ public function __construct(
+ private string $user,
+ ) {
+ }
+
+ public function __invoke(OperatingSystem $os): void
+ {
+ $database = $os
+ ->remote()
+ ->sql(Url::of('mysql://user:password@127.0.0.1/database'));
+
+ $existing = $database(
+ SQL::of('SELECT * FROM users WHERE name = ?')->with(Parameter::of(
+ $this->user,
+ )),
+ );
+
+ if (!$existing->empty()) {
+ return;
+ }
+
+ $database(
+ SQL::of('INSERT INTO users (name) VALUES (?)')->with(Parameter::of(
+ $this->user,
+ )),
+ );
+ }
+}
+```
+
+You can now schedule your tasks:
+
+```php title="Scope.php"
+use Innmind\Async\Scope\Continuation;
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\Immutable\Sequence;
+
+final class Scope
+{
+ public function __invoke(
+ mixed $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ $users = $this->fetch($os);
+
+ return $continuation
+ ->schedule(
+ Sequence::of(...$users)
+ ->map(static fn(string $user) => new Task($user)),
+ )
+ ->finish();
+ }
+
+ /**
+ * @return list
+ */
+ private function fetch(OperatingSystem $os): array
+ {
+ // see previous chapter
+ }
+}
+```
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..c2ea703
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,40 @@
+---
+hide:
+ - navigation
+ - toc
+---
+
+# Welcome to `innmind/async`
+
+This package is an abstraction on top of `Fiber`s to coordinate multiple tasks asynchronously.
+
+The goal is to easily move the execution of any code built using [`innmind/operating-system`](https://innmind.org/OperatingSystem/) from a synchronous context to an async one. This means that it's easier to experiment running a piece of code asynchronously and then move back if the experiment is not successful. This also means that you can test each part of an asynchronous system synchronously.
+
+
+??? example "Sneak peek"
+ ```php
+ use Innmind\Async\{
+ Scheduler,
+ Scope\Continuation,
+ };
+ use Innmind\OperatingSystem\{
+ OperatingSystem,
+ Factory,
+ };
+ use Innmind\Immutable\Sequence;
+
+ Scheduler::of(Factory::build())
+ ->sink(null)
+ ->with(
+ static fn(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ) => $continuation
+ ->schedule(Sequence::of(
+ static fn(OperatingSystem $os) => importUsers($os),
+ static fn(OperatingSystem $os) => importProducts($os),
+ ))
+ ->finish(),
+ );
+ ```
diff --git a/docs/limitations.md b/docs/limitations.md
new file mode 100644
index 0000000..0c15087
--- /dev/null
+++ b/docs/limitations.md
@@ -0,0 +1,29 @@
+---
+hide:
+ - navigation
+---
+
+# Limitations
+
+!!! warning ""
+ The limitations mentionned below exists for now as fixing them will take time. But they can be overcome, and will be in due time!
+
+## HTTP calls
+
+Currently HTTP calls are done via `curl` but it can't be integrated in the same loop as other streams. To allow the coordination of multiple tasks when doing HTTP calls the system use a timeout of `10ms` and switches between tasks at this max rate.
+
+To fix this limitation a new implementation entirely based on PHP streams needs to be created.
+
+Meanwhile if your goal is to make multiple concurrent HTTP calls you don't need this package. [`innmind/http-transport`](https://innmind.org/documentation/getting-started/concurrency/http/) already supports concurrent calls on it's own (without the limitation mentionned above).
+
+## SQL queries
+
+SQL queries executed via `$os->remote()->sql()` are still executed synchronously (as it uses `PDO`).
+
+To fix this limitation a new implementation entirely based on PHP streams needs to be created.
+
+## Scaling
+
+It seems that the current implementation of this package has a [limit of around 100K concurrent tasks](https://bsky.app/profile/baptouuuu.bsky.social/post/3lwr7pei2ek2f) before it starts slowing down.
+
+A simple script scheduling 100k tasks that each halts the process for 10 second will take ~13s.
diff --git a/docs/preface/philosophy.md b/docs/preface/philosophy.md
new file mode 100644
index 0000000..7428c91
--- /dev/null
+++ b/docs/preface/philosophy.md
@@ -0,0 +1,74 @@
+# Philosophy
+
+Usually you'll want to use asynchronous code to improve the performance of your application. Async allows to fix IO performance. When doing IO (network calls and such) a good amount of time is spent waiting for the network to respond. Instead of waiting, async allows to do other things.
+
+This means you'll try to fix problems in an existing codebase.
+
+The goal of this package is to allow you to try running your existing synchronous code asynchronously without changing your implementation. This has 3 big advantages:
+
+
+
+- you can experiment with async with your _real_ code (instead of a proof of concept)
+- you can go back if the async experiment isn't conclusive (1)
+- you can run your code asynchronously and test it synchronously (2)
+
+
+
+1. Thus being more cost effective for your company.
+2. Which is usually a major pain point of async code.
+
+Unlike other async packages, instead of trying to duplicate the PHP functions in order to make then run asynchronously, this package rely on higher level abstractions. This helps reduce the amount of code necessary to make this package possible and consequently reduce the maintainability cost.
+
+## Abstraction
+
+This package relies on the [`innmind/operating-system`](https://innmind.org/OperatingSystem/) abstraction. It offers all the APIs that could benefit from being run asynchronously.
+
+Since all these APIs are accessed through a single object it offers a simple way to move a code from synchronous to asynchronous.
+
+The APIs concerned are:
+
+- halting the process (aka an abstraction on top of `#!php sleep`)
+- sockets (HTTP, SQL, etc...)
+- files
+- processes
+
+By focusing on this abstraction as the central point to make a code run asynchronously brings another advantage. Any abstraction built on top of it makes it automatically async compatible. No need to use differents ecosystems (sync vs async).
+
+All this is completely transparent thanks to a lower level abstraction: Monads.
+
+## Monads
+
+Monads are data structures coming from functional programming. They help solve different use cases. But the common point between all of them is that you describe what you want to do and not how to do it. It's this particular point that allows a system to inject logic on the _how_ part to make the code run asynchronously. Without you being aware of it.
+
+The other big advantage of monads is their great composability. Because you only control _what_ you want to do you can safely build abstractions upon abstractions without breaking the sync (or async) nature of the code.
+
+All the Innmind ecosystem rely of the monads provided by [`innmind/immutable`](https://innmind.org/Immutable/).
+
+## Pooling suspensions
+
+The use of the operating system abstraction in the end describe only 2 ways a code should be suspended: (1)
+{.annotate}
+
+1. aka instruct to do something else while waiting.
+
+- waiting X amount of time
+- watching for `resource`s to be ready (to read/write)
+
+The goal of _pooling_ these suspensions is to determine the shortest amount of time the process really need to wait before it can again do something.
+
+## MapReduce
+
+[MapReduce](https://en.wikipedia.org/wiki/MapReduce) is a pattern with 2 components: Map and Reduce.
+
+- A Map describes tasks that can be safely done concurrently to produce a result.
+- A Reduce describes the way to aggregate the result of multiple tasks to a new result.
+
+This pattern is great because it's simple to grasp and is composable. Indeed a Map operation could itself use the MapReduce pattern to compute its value.
+
+This package is designed around this pattern, even though it uses a different terminology. It's composed of:
+
+- a [Scope](terminology.md#scope) acting as a Reduce
+ - it creates tasks
+ - it computes a [carried value](terminology.md#carried-value)
+- a [Task](terminology.md#task) acting as a Map
+ - it computes a value that is fed back to the scope
diff --git a/docs/preface/terminology.md b/docs/preface/terminology.md
new file mode 100644
index 0000000..763ef06
--- /dev/null
+++ b/docs/preface/terminology.md
@@ -0,0 +1,33 @@
+# Terminology
+
+## Scheduler
+
+A `Scheduler` is the object responsible to coordonate the execution of a [Scope](#scope) and the [Tasks](#task) it schedule.
+
+When trying to execute the scope and the tasks it will look for why them have been suspended. A scope or the tasks can be suspended when asking for the process to halt or watching for IO (files or sockets). These suspensions happen through the use of [`innmind/operating-system`](https://innmind.org/OperatingSystem/).
+
+In order to create a scheduler you need an instance of this operating system. By default this abstraction is synchronous. It's the job of the scheduler to create copies of this operating system object and pass them to the scope and tasks.
+
+## Scope
+
+A Scope is a function, run asynchronously, responsible to scheduling new asynchronous [Tasks](#task). It can do so indefinitively, the default behaviour, or choose to either stop and let the tasks finish or ask to be called again once a task result is available.
+
+The scope is also responsible to [carry a value](#carried-value). Each time the scope is called it has access to the last carried value and has the possibility to change its value for the next call.
+
+The scope is run asynchronously because it will usually be the place you'll watch for a socket server to accept new connections and schedule tasks to handle these connections. Or a more simpler case it to build it as a timer that will shedule tasks every X amount of time.
+
+## Task
+
+A Task is a function that must accept an instance of the [operating system](https://innmind.org/OperatingSystem/). As described above, it's thanks to this object that the function can run asynchronously.
+
+If a function never uses this operating system object, then it **cannot** run asynchronously.
+
+Like any other function it can return any value. When it does, the value will be made available to the scope the next time it's called.
+
+## Carried value
+
+A carried value can be any PHP variable. This value is passed to the [Scope](#scope) each time this function is called, and can change it for the next call.
+
+When the scope decides to finish running, the [Scheduler](#scheduler) will return the last value specified by the scope.
+
+You can use this value to gather the tasks results, compute a new value or print to [the console](https://innmind.org/CLI/).
diff --git a/docs/scheduler/config.md b/docs/scheduler/config.md
new file mode 100644
index 0000000..f8ab9f8
--- /dev/null
+++ b/docs/scheduler/config.md
@@ -0,0 +1,17 @@
+# Config
+
+## Limit tasks concurrency
+
+By default all the tasks scheduled by a scope will be started immediately. But depending on the number you'll schedule this can take a lot of resources. To avoid the process taking too much RAM you can limit the number of tasks being run at a point in time
+
+```php hl_lines="2"
+Scheduler::of($os)
+ ->limitConcurrencyTo($size)
+ ->sink(null)
+ ->with(new Scope);
+```
+
+`#!php $size` can be any int above `#!php 2`. As soon a task finished it will pick a new one from the previously scheduled ones.
+
+??? info
+ As a point of reference, a simple script sheduling 100k tasks that halt the process for 10s will take 1.9Go of RAM (1M tasks will use 19Go).
diff --git a/docs/scopes/lifecycle.md b/docs/scopes/lifecycle.md
new file mode 100644
index 0000000..e481f1a
--- /dev/null
+++ b/docs/scopes/lifecycle.md
@@ -0,0 +1,153 @@
+# Lifecycle
+
+## Restart the scope
+
+By default the `$continuation` object that the scope needs to return is configured to tell the scheduler to call the scope once again. This is why be default the scheduler runs an infinite loop.
+
+When you return a continuation with scheduled tasks, the scheduler will start these tasks immediately and call the scope once again.
+
+!!! warning ""
+ This means that if your scope never waits on anything (1) before scheduling tasks you'll enter in a runaway situation. New tasks will keep piling on and the scheduler will never resume the suspended tasks.
+ {.annotate}
+
+ 1. Either by halting the process or watching for sockets/files.
+
+ You **must** always wait before scheduling new tasks and restarting the scope, even if it's halting the process for 1 microsend.
+
+## Carry a value
+
+Since a scope acts as a reducer, it can keep track of a carried value that will be returned by the scheduler once the scope finishes or terminate.
+
+```php title="Scope.php" hl_lines="4"
+final class Scope
+{
+ public function __invoke(
+ mixed $carriedValue, // <-- this is the value
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ return $continuation;
+ }
+}
+```
+
+Each time the scope is called it receives the carried value from the previous call. On the first call it will receive the value passed as argument to `#!php $scheduler->sink($carriedValue)`.
+
+You can change the carried value for the next call via `#!php $continuation->carryWith($newValue)`;
+
+## Run tasks in the background
+
+If you want to run multiple tasks asynchronously but you don't care about there results, you need to tell the scheduler to not call the scope after scheduling the tasks.
+
+```php title="Scope.php" hl_lines="10"
+final class Scope
+{
+ public function __invoke(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ return $continuation
+ ->schedule($tasks)
+ ->finish();
+ }
+}
+```
+
+## Wait to start tasks
+
+In most cases your scope will wait for some external event before scheduling a new task. Such event can be receiving an incoming connection on a socket:
+
+```php
+use Innmind\Url\Authority\Port;
+use Innmind\IO\Sockets\{
+ Servers\Server,
+ Internet\Transport,
+};
+use Innmind\IP\IPv4;
+use Innmind\TimeContinuum\Period;
+
+final class Scope
+{
+ private ?Server $server = null;
+
+ public function __invoke(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ $server = $this->server ??= $os
+ ->ports()
+ ->open(Transport::tcp(), IPv4::localhost(), Port::of(8080))
+ ->unwrap();
+ $tasks = $server
+ ->timeoutAfter(Period::second(1))
+ ->accept()
+ ->map(static fn($client) => new Task($client))
+ ->maybe()
+ ->toSequence();
+
+ return $continuation->schedule($tasks);
+ }
+}
+```
+
+This will look for an incoming connection every second.
+
+Even though we wait for an incoming connection it doesn't block other tasks because the scope itself is run asynchronously.
+
+## Wait for tasks results
+
+Once you've scheduled all your tasks, you can tell the scheduler to call the scope only when tasks results are available. You can do that with the `#!php $continuation->wakeOnResult()` method.
+
+```php title="Scope.php"
+use Innmind\Immutable\Sequence;
+
+final class Scope
+{
+ private bool $scheduled;
+
+ /**
+ * @param Sequence $results
+ */
+ public function __invoke(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ Sequence $results,
+ ): Continuation {
+ if (!$this->scheduled) {
+ $this->scheduled = true,
+
+ return $continuation
+ ->schedule($tasks)
+ ->wakeOnResult();
+ }
+
+ doSomething($results);
+
+ return $continuation->wakeOnResult();
+ }
+}
+```
+
+Beware, not all results will be available at once. The scope may be called multiple times.
+
+## Terminate the tasks
+
+If for some reason you need to cancel all the scheduled tasks you can do it with `#!php $continuation->terminate()`. This will make sure the scope is never called again, [send a signal](../tasks/graceful-shutdown.md) to each task and wait for them to stop.
+
+When all tasks finished, the scheduler will return the last carried value.
+
+```php title="Scope.php"
+final class Scope
+{
+ public function __invoke(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ return $continuation->terminate();
+ }
+}
+```
diff --git a/docs/scopes/strategies/race.md b/docs/scopes/strategies/race.md
new file mode 100644
index 0000000..359c93e
--- /dev/null
+++ b/docs/scopes/strategies/race.md
@@ -0,0 +1,61 @@
+# Race for a result
+
+=== "Scheduler"
+ ```php
+ use Innmind\Async\Scheduler;
+ use Innmind\OperatingSystem\Factory;
+
+ $result = Scheduler::of(Factory::build())
+ ->sink(null)
+ ->with(new Scope);
+ $result === 'foo'; // true
+ ```
+
+ This is always `foo` because it's the task that waits the less.
+
+=== "Scope"
+ ```php
+ use Innmind\Async\Scope\Continuation;
+ use Innmind\OperatingSystem\OperatingSystem;
+ use Innmind\TimeContinuum\Period;
+ use Innmind\Immutable\Sequence;
+
+ final class Scope
+ {
+ private bool $scheduled;
+
+ public function __invoke(
+ array $results,
+ OperatingSystem $os,
+ Continuation $continuation,
+ Sequence $newResults,
+ ): Continuation {
+ if (!$this->scheduled) {
+ $this->scheduled = true;
+
+ return $continuation
+ ->schedule(Sequence::of(
+ static fn($os) => $os
+ ->process()
+ ->halt(Period::second(2))
+ ->map(static fn() => 'bar')
+ ->unwrap(),
+ static fn($os) => $os
+ ->process()
+ ->halt(Period::second(1))
+ ->map(static fn() => 'foo')
+ ->unwrap(),
+ ))
+ ->wakeOnResult();
+ }
+
+ return $results->first()->match(
+ static fn($value) => $continuation
+ ->carryWith($value)
+ ->finish(),
+ static fn() => $continuation->wakeOnResult(),
+ );
+ }
+ }
+ ```
+
diff --git a/docs/scopes/strategies/results.md b/docs/scopes/strategies/results.md
new file mode 100644
index 0000000..85ea06f
--- /dev/null
+++ b/docs/scopes/strategies/results.md
@@ -0,0 +1,62 @@
+# Gather results
+
+=== "Scheduler"
+ ```php
+ use Innmind\Async\Scheduler;
+ use Innmind\OperatingSystem\Factory;
+
+ $results = Scheduler::of(Factory::build())
+ ->sink([])
+ ->with(new Scope);
+ $results === ['foo' => 'bar', 'bar' => 'baz']; // true
+ ```
+
+=== "Scope"
+ ```php
+ use Innmind\Async\Scope\Continuation;
+ use Innmind\OperatingSystem\OperatingSystem;
+ use Innmind\TimeContinuum\Period;
+ use Innmind\Immutable\Sequence;
+
+ final class Scope
+ {
+ private bool $scheduled;
+
+ public function __invoke(
+ array $results,
+ OperatingSystem $os,
+ Continuation $continuation,
+ Sequence $newResults,
+ ): Continuation {
+ if (!$this->scheduled) {
+ $this->scheduled = true;
+
+ return $continuation
+ ->schedule(Sequence::of(
+ static fn($os) => [
+ 'foo' => $os
+ ->process()
+ ->halt(Period::second(1))
+ ->map(static fn() => 'bar')
+ ->unwrap(),
+ ],
+ static fn($os) => [
+ 'bar' => $os
+ ->process()
+ ->halt(Period::second(2))
+ ->map(static fn() => 'baz')
+ ->unwrap(),
+ ],
+ ))
+ ->wakeOnResult();
+ }
+
+ return $continuation
+ ->carryWith(\array_merge(
+ $results,
+ ...$newResults->toList(),
+ ))
+ ->wakeOnResult();
+ }
+ }
+ ```
diff --git a/docs/tasks/discard-result.md b/docs/tasks/discard-result.md
new file mode 100644
index 0000000..3adf1c7
--- /dev/null
+++ b/docs/tasks/discard-result.md
@@ -0,0 +1,47 @@
+# Discard result
+
+When a task finishes the returned value (1) will be sent to the scope the next time it's called. But if you don't need to handle the result value this adds an overhead as the results are collected and the scope needs to be called.
+{.annotate}
+
+1. Even if it's `#!php null`.
+
+You can avoid this overhead by returning a special object that tells the scheduler to ignore the value.
+
+```php title="Task.php"
+use Innmind\Async\Task\Discard;
+use Innmind\OperatingSystem\OperatingSystem;
+
+final class Task
+{
+ public function __invoke(OperatingSystem $os): Discard
+ {
+ // do something
+
+ return Discard::result;
+ }
+}
+```
+
+Or if you don't want your task to be aware of that but want this logic to be held by your scope it's as simple as:
+
+```php
+use Innmind\Async\{
+ Scope\Continuation,
+ Task\Discard,
+};
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\Immutable\Sequence;
+
+final class Scope
+{
+ public function __invoke(
+ $_,
+ OperatingSystem $os,
+ Continuation $continuation,
+ ): Continuation {
+ return $continuation->schedule(Sequence::of(
+ Discard::result(new Task),
+ ));
+ }
+}
+```
diff --git a/docs/tasks/graceful-shutdown.md b/docs/tasks/graceful-shutdown.md
new file mode 100644
index 0000000..0b139f8
--- /dev/null
+++ b/docs/tasks/graceful-shutdown.md
@@ -0,0 +1,35 @@
+# Gracefully shutdown
+
+Since a task is a function that can be run synchronously or asynchronously, it uses the same mechanism to know that it needs to gracefully stop what it's doing.
+
+This mechanism is [process signals](https://innmind.org/OperatingSystem/use_cases/signals/).
+
+```php title="Task.php"
+use Innmind\OperatingSystem\OperatingSystem;
+use Innmind\Signals\Signal;
+
+final class Task
+{
+ public function __invoke(OperatingSystem $os)
+ {
+ $signaled = false;
+ $os
+ ->process()
+ ->signals()
+ ->listen(Signal::terminate, function() use (&$signaled) {
+ $signaled = true;
+ });
+
+ while (!$signaled) {
+ // do something
+ }
+ }
+}
+```
+
+The `#!php $signaled` variable will be flipped in 2 cases:
+
+- the PHP process receives the signal (that will be dispatched to all tasks that listened to it)
+- the scope calls `#!php $continuation->terminate()`
+
+If the scope asks to terminate then it will send the signal to all tasks. This means that if you call this method in your scope you should add a listener to all your tasks otherwise the system may never terminate.
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..785e0d4
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,112 @@
+site_name: Innmind/async
+repo_name: Innmind/async
+
+nav:
+ - Home: index.md
+ - Preface:
+ - preface/philosophy.md
+ - preface/terminology.md
+ - Getting started:
+ - getting-started/index.md
+ - Create a scope: getting-started/scope.md
+ - Create a task: getting-started/task.md
+ - Scheduler:
+ - scheduler/config.md
+ - Scopes:
+ - scopes/lifecycle.md
+ - Strategies:
+ - scopes/strategies/results.md
+ - scopes/strategies/race.md
+ - Tasks:
+ - tasks/graceful-shutdown.md
+ - tasks/discard-result.md
+ - limitations.md
+
+theme:
+ name: material
+ logo: assets/logo.svg
+ favicon: assets/favicon.png
+ font: false
+ features:
+ - content.code.copy
+ - content.code.annotate
+ - navigation.tracking
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.sections
+ - navigation.expand
+ - navigation.indexes
+ - navigation.top
+ - navigation.footer
+ - search.suggest
+ - search.highlight
+ - content.action.edit
+ palette:
+ # Palette toggle for automatic mode
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+ primary: blue
+ accent: deep orange
+ # Palette toggle for light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ primary: blue
+ accent: deep orange
+ # Palette toggle for dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ toggle:
+ icon: material/brightness-4
+ name: Switch to system preference
+ primary: blue
+ accent: deep orange
+
+markdown_extensions:
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ extend_pygments_lang:
+ - name: php
+ lang: php
+ options:
+ startinline: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - attr_list
+ - md_in_html
+ - pymdownx.superfences
+ - abbr
+ - admonition
+ - pymdownx.details:
+ - pymdownx.tabbed:
+ alternate_style: true
+ - toc:
+ permalink: true
+ - footnotes
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+
+extra_css:
+ - assets/stylesheets/extra.css
+
+plugins:
+ - search
+ - privacy
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/Innmind/async
+ - icon: fontawesome/brands/x-twitter
+ link: https://twitter.com/Baptouuuu
+ - icon: fontawesome/brands/mastodon
+ link: https://phpc.social/@baptouuuu
+ - icon: fontawesome/brands/bluesky
+ link: https://bsky.app/profile/baptouuuu.bsky.social
diff --git a/test.php b/test.php
new file mode 100644
index 0000000..cd8f3f4
--- /dev/null
+++ b/test.php
@@ -0,0 +1,32 @@
+sink(null)
+ ->with(
+ static fn($_, $__, $cont) => $cont
+ ->schedule(Sequence::of()->pad(
+ (int) ($argv[1] ?? 100),
+ static fn($os) => $os->process()->halt(Period::second(10)),
+ ))
+ ->finish(),
+ );
+
+\printf(
+ "%.2f Mo\n",
+ memory_get_peak_usage(true)/1024/1024,
+);