Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ember concurrency examples #2

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/lib/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { onDestroy } from 'svelte';
import { writable } from 'svelte/store';

type SvelteConcurrencyUtils = {
signal: AbortSignal;
abortController: AbortController;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I understand how signal is supposed to be used:

In my race case, I wanted a parent task that triggers 3 children tasks, and as soon as one of the children is done, everyone stops.

I couldn't figure out how / when onDestroy is supposed to be called, and I couldn't use cancel because it's kind of an "auto-cancellation", the parent task says in its own body "I am done, I stop all my children". I didn't see how to use the main branch's API to do that, I just wanted a way to run .abort() for the current function.

Considering everything you've already done in the task logic, what do you think is the cleanest way to achieve that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we didn't implement that kind of concurrency yet. Probably i would just do something like

child.perform().then(()=>{
    child2.cancel();
    child3.cancel();
});
child2.perform().then(()=>{
    child.cancel();
    child3.cancel();
});
child3.perform().then(()=>{
    child.cancel();
    child2.cancel();
});

link: <T extends { cancel: () => void }>(task: T) => T;
};

Expand Down Expand Up @@ -52,7 +52,7 @@ export function task<TArgs = undefined, TReturn = unknown>(
cancel() {
abort_controller.abort();
},
perform(...args: undefined extends TArgs ? [] : [TArgs]) {
perform(...args: unknown[]) {
Copy link
Collaborator Author

@BlueCutOfficial BlueCutOfficial Feb 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve been playing with svelte-concurrency and I encounter something a bit counter-intuitive with the task arguments. As it is on main, it seems that TArgs has to be an array. I didn’t follow all the threads about TypeScript constraints, but in the main page example, when you do:

const parent = task(async function* (param: number) {
   (...)
   return param * 2;
});

Let’s assume you think you have param === 4, you actually have param === [4] and do:

[4] * 2

Surprisingly it works because you can multiply values in arrays in JS, but in my "accelerating buttons" case I had 0 + [1] that resulted to a string "01".

...args: unknown[] is not a proposal, it's just my lazy fix to get it work =p

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok this is actually something else: basically this TS code is inferring the type of TArgs. If you don't pass any parameter it will actually be undefined and when you call perfom this type will disallow you to pass any values. But if you pass some argument then the arguments will be the same arguments that you expect in the task function. It doesn't need to be an array, you can pass a single value and ts will infer that you want that kind of value and force you to use the same value in perform. So basically you can do this

const my_task = task((value: number)=>{
    console.log(value);
});

my_task.perform(4);

abort_controller.signal.removeEventListener('abort', cancel_linked_and_update_store);
abort_controller = new AbortController();
abort_controller.signal.addEventListener('abort', cancel_linked_and_update_store);
Expand All @@ -65,7 +65,7 @@ export function task<TArgs = undefined, TReturn = unknown>(
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const gen_or_value = await gen_or_fun(args as any, {
signal: abort_controller.signal,
abortController: abort_controller,
link,
});
const is_generator =
Expand Down
41 changes: 41 additions & 0 deletions src/routes/accelerating-button/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { task } from '$lib/task.js';

let count: number = 0;

const incrementBy = task(async function* ([increment]: [increment: number]) {
let speed = 400;
while (true) {
count = count + increment;
await new Promise((r) => setTimeout(r, speed));
yield;
speed = Math.max(50, speed * 0.8);
}
});

</script>

<p>Hold down the buttons to accelerate:</p>
<p>Count: {count}</p>
<button
on:mousedown={async () => {
await incrementBy.perform(-1);
}}
on:mouseup={async () => {
await incrementBy.cancel();
}}
>-- Decrease</button>
<button
on:mousedown={async () => {
await incrementBy.perform(1);
}}
on:mouseup={async () => {
await incrementBy.cancel();
}}
>Increase ++</button>

<p>This example is inspired by
<a target="_blank" rel="noopener noreferrer" href="http://ember-concurrency.com/docs/examples/increment-buttons">
ember-concurrency example "Accelerating Increment / Decrement Buttons"
</a>
</p>
69 changes: 69 additions & 0 deletions src/routes/race/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script lang="ts">
import { task, type Task } from '$lib/task.js';

type Challenger = {
name: string;
progress: number;
};

let challengers: Challenger[] = [];
for (let i = 1; i <= 3; i++) {
challengers.push({
name: `Challenger ${i}`,
progress: 0,
})
}

let winner: Challenger|undefined;

const runTheRace = task(async function* (_, { abortController, link }) {
console.log('Run the race')
let challengerRaces = challengers.map(challenger => {
return link(race).perform(challenger);
})
let first = await Promise.race(challengerRaces);
console.log(`First arrived is ${first.name}`);
abortController.abort();
return first;
});

const race = task(async function* ([challenger]: [challenger: Challenger]) {
console.log(`${challenger.name} runs`);
while (challenger.progress < 100) {
await new Promise((r) => setTimeout(r, Math.random() * 100 + 100));
yield;
challenger.progress = Math.min(
100,
Math.floor(challenger.progress + Math.random() * 20),
);
console.log(`${challenger.name} progress: ${challenger.progress}`);
}
return challenger;
});

</script>

<button
on:click={async () => {
winner = await runTheRace.perform();
}}
>Start race</button>

<button
on:click={() => {
challengers[0].progress = 50;
challengers[1].progress = 20;
challengers[2].progress = 70;
}}
>Test</button>
Comment on lines +52 to +58
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this button to check the <progress> elements have their values updated on click, and that works. But my progress update in the different children's tasks has no effect, there might be something I need to learn about the render or for loop maybe?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you just need to call await tick(); after you update the value in the while loop.


{#each challengers as challenger}
<div>
<span>{challenger.name}</span>
<progress max="100" value={challenger.progress} />
</div>
{/each}

{#if winner}
<p>The winner is {winner.name}</p>
{/if}