Skip to content

Commit

Permalink
Merge pull request #61 from mainmatter/link-cancel-only-instance
Browse files Browse the repository at this point in the history
feat: link only cancel the instance of the task that was linked
  • Loading branch information
paoloricciuti authored May 10, 2024
2 parents 0f23a6a + ab37b29 commit 1e42410
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 25 deletions.
57 changes: 34 additions & 23 deletions src/lib/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type TaskOptions = {

export type SvelteConcurrencyUtils = {
signal: AbortSignal;
link: <T extends { cancelAll: () => void }>(task: T) => T;
link: <TArgs, TReturn>(task: Task<TArgs, TReturn>) => Task<TArgs, TReturn>;
};

export type Task<TArgs = unknown, TReturn = unknown> = ReturnType<typeof task<TArgs, TReturn>>;
Expand All @@ -51,40 +51,35 @@ function _task<TArgs = undefined, TReturn = unknown>(
results,
});

const abort_controllers = new Set<AbortController>();
const abort_controllers = new Set<{ controller: AbortController; listener: () => void }>();

onDestroy(() => {
abort_controllers.forEach((abort_controller) => {
abort_controller.abort();
abort_controller.signal.removeEventListener('abort', cancel_linked_and_update_store);
abort_controller.controller.abort();
abort_controller.controller.signal.removeEventListener('abort', abort_controller.listener);
});
});

const child_tasks = new Set<{ cancelAll: () => void }>();

function link<T extends { cancelAll: () => void }>(task: T) {
child_tasks.add(task);
return task;
}

function cancel_linked_and_update_store() {
for (const child_task of child_tasks) {
child_task.cancelAll();
}
result.update((old) => {
old.is_loading = false;
return old;
});
}

return {
subscribe,
cancelAll() {
abort_controllers.forEach((abort_controller) => {
abort_controller.abort();
abort_controller.controller.abort();
});
},
perform(...args: undefined extends TArgs ? [] : [TArgs]) {
const child_tasks = new Set<ReturnType<Task<any, any>['perform']>>();

function cancel_linked_and_update_store() {
for (const child_task of child_tasks) {
child_task.cancel();
}
result.update((old) => {
old.is_loading = false;
return old;
});
}

let resolve: (value: TReturn) => unknown;
let reject: (cause: unknown) => unknown;
const promise = new Promise<TReturn>((resolver, rejecter) => {
Expand All @@ -93,7 +88,23 @@ function _task<TArgs = undefined, TReturn = unknown>(
});
const abort_controller = new AbortController();
abort_controller.signal.addEventListener('abort', cancel_linked_and_update_store);
abort_controllers.add(abort_controller);
abort_controllers.add({
controller: abort_controller,
listener: cancel_linked_and_update_store,
});
function link<TLinkArgs, TLinkReturn>(
task: Task<TLinkArgs, TLinkReturn>,
): Task<TLinkArgs, TLinkReturn> {
const old_perform = task.perform;
return {
...task,
perform(...args) {
const instance = old_perform(...args);
child_tasks.add(instance);
return instance;
},
};
}
handler(
() => {
result.update((old) => {
Expand Down
28 changes: 28 additions & 0 deletions src/lib/tests/components/link/child.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import { task, type Task } from '../../../task';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let parent: Task<number, any>;
export let kind: string;
const default_task = task.default(async (_, { link }) => {
await link(parent).perform(0);
});
const options_task = task(
async (_, { link }) => {
await link(parent).perform(0);
},
{ kind: 'default' },
);
</script>

<button
data-testid="child-component-perform-{kind}"
on:click={async () => {
if (kind === 'default') {
default_task.perform();
} else {
options_task.perform();
}
}}>perform</button
>
135 changes: 135 additions & 0 deletions src/lib/tests/components/link/parent.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script lang="ts">
import { task, type SvelteConcurrencyUtils } from '../../../task';
import Child from './child.svelte';
export let fn: (
args: number,
utils: SvelteConcurrencyUtils,
) => Promise<unknown> | AsyncGenerator<unknown, unknown, unknown>;
export let return_value: (value: unknown) => void = () => {};
export let argument = 0;
const default_task = task.default(fn);
const options_task = task(fn, { kind: 'default' });
const default_task_child = task.default(async (_, { link }) => {
await link(default_task).perform(argument);
});
const default_options_task_child = task.default(async (_, { link }) => {
await link(options_task).perform(argument);
});
let latest_task_instance: ReturnType<typeof default_task.perform>;
let latest_task_child_instance: ReturnType<typeof default_task_child.perform>;
let latest_options_task_child_instance: ReturnType<typeof default_options_task_child.perform>;
let latest_options_task_instance: ReturnType<typeof options_task.perform>;
let mounted = true;
</script>

<button
data-testid="perform-default"
on:click={async () => {
latest_task_instance = default_task.perform(argument);
return_value(await latest_task_instance);
}}>perform</button
>

<button
data-testid="perform-options"
on:click={async () => {
latest_options_task_instance = options_task.perform(argument);
return_value(await latest_options_task_instance);
}}>perform options</button
>

<button
data-testid="perform-child-default"
on:click={async () => {
latest_task_child_instance = default_task_child.perform();
}}>perform child</button
>

<button
data-testid="perform-child-options"
on:click={async () => {
latest_options_task_child_instance = default_options_task_child.perform();
}}>perform child options</button
>

<button
data-testid="cancel-default"
on:click={() => {
default_task.cancelAll();
}}>cancel</button
>

<button
data-testid="cancel-options"
on:click={() => {
options_task.cancelAll();
}}>cancel options</button
>

<button
data-testid="cancel-child-default"
on:click={() => {
default_task_child.cancelAll();
}}>cancel child</button
>

<button
data-testid="cancel-child-options"
on:click={() => {
default_options_task_child.cancelAll();
}}>cancel child options</button
>

<button
data-testid="cancel-default-last"
on:click={() => {
if (latest_task_instance) {
latest_task_instance.cancel();
}
}}>cancel last instance</button
>

<button
data-testid="cancel-options-last"
on:click={() => {
if (latest_options_task_instance) {
latest_options_task_instance.cancel();
}
}}>cancel last options instance</button
>

<button
data-testid="cancel-child-default-last"
on:click={() => {
if (latest_task_child_instance) {
latest_task_child_instance.cancel();
}
}}>cancel last child instance</button
>

<button
data-testid="cancel-child-options-last"
on:click={() => {
if (latest_options_task_child_instance) {
latest_options_task_child_instance.cancel();
}
}}>cancel last child options instance</button
>

<button
data-testid="unmount-child-component"
on:click={() => {
mounted = !mounted;
}}>unmount child component</button
>
{#if mounted}
<Child parent={default_task} kind="default" />
<Child parent={options_task} kind="options" />
{/if}
Loading

0 comments on commit 1e42410

Please sign in to comment.