From 687aec7464290ffabde4bbc724d2ec81047145d5 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 15 Jan 2024 15:47:41 -0500 Subject: [PATCH] Added a way to stop the operation --- src/App.tsx | 17 +++++++++++++++- src/components/LoadingSpinner.tsx | 6 ++++++ src/hooks/useLoader.ts | 32 +++++++++++++++++++++++++------ src/operations/UpdateContext.ts | 15 +++++++++------ src/operations/createLoader.ts | 20 +++++++++++++++++-- 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b4c4f65..5f15a72 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import "./App.css"; -import { sleep, Operation } from "effection"; +import { sleep, Operation, run } from "effection"; import { Player } from "./components/Player"; import { useState } from "react"; @@ -174,6 +174,21 @@ export const App = () => { {...options} /> +
  • + +
  • ); diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx index 667749a..5c2fa8e 100644 --- a/src/components/LoadingSpinner.tsx +++ b/src/components/LoadingSpinner.tsx @@ -39,6 +39,12 @@ export function LoadingSpinner({ loader }: { loader: LoaderState }): JS Failed after 3 attempts. Please contact support.

    ) + case "stopped": + return ( +

    + {loader.reason} +

    + ) } return <> diff --git a/src/hooks/useLoader.ts b/src/hooks/useLoader.ts index 5df5ff7..b6e4d09 100644 --- a/src/hooks/useLoader.ts +++ b/src/hooks/useLoader.ts @@ -1,7 +1,7 @@ import { useMemo, useEffect, useState, useCallback } from "react"; -import { run, type Callable } from "effection"; +import { createChannel, run, spawn, type Callable, each, race, Operation } from "effection"; import { CreateLoaderOptions, createLoader } from "../operations/createLoader"; -import { setUpdateContext, update } from "../operations/UpdateContext"; +import { setUpdateContext } from "../operations/UpdateContext"; export type LoaderState = | { @@ -33,11 +33,16 @@ export type LoaderState = | { type: "failed"; error: Error; - }; + } + | { + type: "stopped"; + reason: string; + }; export type LoaderFn = (params: { attempt: number; signal: AbortSignal; + stop: (reason: string) => Operation; }) => Callable; export type UseLoaderOptions = Partial, "load">>; @@ -86,9 +91,24 @@ export function useLoader( useEffect(() => { const task = run(function* () { - yield* setUpdateContext(setState); - yield* update({ type: "initial" }) - yield* loader(); + const channel = createChannel>(); + + yield* setUpdateContext(channel.send); + + const updates = yield* spawn(function*() { + for (const value of yield* each(channel)) { + setState(value); + if (value.type === "stopped") { + throw new Error("Forced interrupt") + } + yield* each.next(); + } + }); + + const driver = yield* spawn(loader); + + yield* race([updates, driver]); + }); return () => { diff --git a/src/operations/UpdateContext.ts b/src/operations/UpdateContext.ts index 942ce05..e82a6ee 100644 --- a/src/operations/UpdateContext.ts +++ b/src/operations/UpdateContext.ts @@ -1,13 +1,16 @@ -import { Operation, createContext, lift } from "effection"; +import { Operation, createContext, suspend } from "effection"; import { LoaderState } from "../hooks/useLoader"; -export const UpdateContext = createContext("update"); +export const UpdateContext = createContext<(value: LoaderState) => Operation>("update"); export const update = function* update(value: LoaderState): Operation { - const setState = yield* UpdateContext; - yield* setState(value); + const send = yield* UpdateContext; + yield* send(value); + if (value.type === "stopped") { + yield* suspend(); + } } -export function* setUpdateContext(setState: (value: LoaderState) => void): Operation { - yield* UpdateContext.set(lift(setState)); +export function* setUpdateContext(send: (value: LoaderState) => Operation): Operation { + yield* UpdateContext.set(send); } \ No newline at end of file diff --git a/src/operations/createLoader.ts b/src/operations/createLoader.ts index 5ef1707..4995911 100644 --- a/src/operations/createLoader.ts +++ b/src/operations/createLoader.ts @@ -1,5 +1,12 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import { Operation, sleep, spawn, call, useAbortSignal } from "effection"; +import { + Operation, + sleep, + spawn, + call, + useAbortSignal, + useScope, +} from "effection"; import { CreateSpinnerOptions, createSpinner } from "./createSpinner"; import { update } from "./UpdateContext"; import { LoaderFn } from "../hooks/useLoader"; @@ -26,6 +33,8 @@ export function createLoader({ type: "started", }); + const scope = yield* useScope(); + for (let attempt = 0; attempt <= retryAttempts; attempt++) { const spinner = yield* spawn(function* () { if (attempt === 0) { @@ -40,7 +49,14 @@ export function createLoader({ const signal = yield* useAbortSignal(); try { - const result = yield* call(() => load({ attempt, signal })); + const result = yield* call(() => + load({ + attempt, + signal, + stop: (reason: string) => + scope.run(() => update({ type: "stopped", reason })), + }) + ); yield* update({ type: "success",