LoginSignup
7
0

More than 1 year has passed since last update.

イ〇ゲーム「そのURLに〇〇はあるか?」

Last updated at Posted at 2021-12-10

イ〇ゲーム「そのURLに〇〇はあるか?」

今日はオンラインの忘年会で盛り上がるかもしれない簡単なゲームをするアプリを作ってみようと思う。そのゲームのルールは簡単だ。2つのチームに別れて、それぞれのチームはWeb上に存在するURLを同じ数だけ選ぶ。重要なのは、そのURLで必ず画像が表示されること。これから作るゲームを行うためのアプリは、ブラウザにURLを入力すると、そのURLから取得できる画像だけがブラウザに表示される。先手がブラウザにURLを入力して画像を表示させる前に、後手はそのURLで表示されると思う画像を一つだけ答える。これを交互に行い、相手のURLで表示される画像を当てた数の多いチームが勝ちとなる。URLがヒントになる場合もあるので、どんな画像が表示されるのかが推測されにくいURLを選ぶとよいだろう。
事前に自分のチームが選んだURLを表示して画像を確認してもルール違反ではないが、ゲーム中に相手チームのURLを表示して画像を確認することはできない。

Puppeteer

アプリでこのURLの画像を表示させる仕組みなのだが、今最も使われているヘッドレスChromeのAPIであるPuppeteerを1つのシンプルなプログラミングモデルから操作できるようにして実現しようと思う。ただし、一度に複数のチームがゲームを同時に行えるようにするには、複数のサーバーを使って一度にたくさんの処理を並列で行う必要があり、その一つ一つで行われる並行処理を正しく実装しなければならない。それには並行処理を正しく実装できるプログラミングモデルを使う必要がある。

アクタープログラミングモデル

本題に入る前に、並行処理とその問題について少し触れてみることにする。並行処理とは同時に複数の処理が行われることで、現代ではアプリケーションで同時実行される複数のスレッドからマルチコアのCPUを使うことで実現されている。並行処理には競合がつきもので、複数のスレッドの間で共有されたリソースへのアクセスで起こり得る問題を避けるために開発者はロック、ミューテックス、セマフォ、ラッチ、モニターといった同期機構を使ってプログラム上で同時実行されるスレッドを制御しなければならない。アプリケーション全体にわたって並行処理が複雑になるにつれて、その仕組みを正しく設計することは難しくなり、これらの過度の使用や誤った実装は、システム全体の実行速度の低下であったり、スレッドのデットロックによってプログラムが動作不能となる原因にもなる。

クラウドサービスが一般的になった現代においても、開発者がコーディングする上で直面する並行処理の制御の難しさは解消されないままなのだが、50年近く前に一人のコンピュータ科学者が提唱した1つのシンプルなプログラミングモデルを使うことで、これらの問題に悩まされることなくスケーラブルな分散システムを実装することができる。

このシンプルなプログラミングモデルの根幹にあるのが、非同期なメッセージングによって相互に連携するアクターと呼ばれるオブジェクトである。アクターはオブジェクト指向プログラミングのクラスとは違い、インターフェースを実装するという概念がない。それをオブジェクト指向プログラミングの観点で説明することは難しく、アクターが具体的に何者なのかは、”わからない”のだが、アクターにメッセージで要求すると、アクターがその応答をメッセージで返すこともあれば、将又メッセージの要求に対しては何も応えず、受信したメッセージの内容が正しければタスクの実行を非同期で開始するといった動きをする。計算という概念を抽象化したものに実行のための時間の概念が合わさった、物理の軸をもったプログラミング構造と呼べるかもしれない。

複数のサーバの実行環境において、このいくつものアクターの相互連携が非同期なメッセージングによって行われるソフトウェアアーキテクチャが1973年にMITのDr.Carl Hewittが提唱したアクタープログラミングモデルであり、開発者はアクターが次の4つの性質を持つことを前提に実装する処理を設計する必要がある。

  1. アクターはメッセージを受けることができる

  2. アクターは新たなアクターを生成することができる

  3. アクターはアクターにメッセージを送信することができ、メッセージの送信先は自分自身でもよい。

  4. アクターはFSM(Finite State Machine)である。メッセージによってその状態を変えることは
    アクターが次に受けられるメッセージが何であるかを決めることでもある。

上記を含めてアクタープログラミングモデルにまつわる話は、Carl Hewitt氏のホームページにある動画やドキュメントを参照されたい。

アクタープログラミングモデルによって解決される問題

オブジェクト指向プログラミングでは正確性を担保するのが難しい並行処理と比べて、アクタープログラミングモデルには優位性があると考える。1つのアクターの処理から参照されるリソースの領域が、そのアクターにプライベートであるため、他のアクターの処理から直接アクセスされることがない。リソースの競合状態が発生しないことで、開発者は前述のスレッド制御の実装を意識せずに安全な並行処理を実現できる。処理の結果をアクターが送信するメッセージの内容に反映させることができるため、デバッガーやコンソール出力を使って挙動の確認が必要になるマルチスレッドプログラミングと比べると、メッセージの内容を使うことでアクターの正確性を容易に検証することができる。

また、アクターは1度に1つのメッセージしか処理しないため、マルチスレッドで処理を実行する場合と比べて、実行順序の予測がつきやすい。オブジェクト指向プログラミングで安全な並行処理を実現するためには、メソッドを実行するスレッドの制御は開発者の技量に頼らざるを得ないため、私見ではあるが、いささか浅薄な印象を受けるプログラミングモデルのようにも思う。

開発者が、アクターの並行処理を意識せずにそのロジックを実装できること以外に、1つのアクターは完全に独立したステートマシンとして考える必要があり、設計の時点でアクターが担う役割を明確にせざるをえないため、用途が曖昧な設計になってしまうことが極めて少ないのではないかと考える。
それから、いわゆるオブジェクト指向プログラミングにあるインターフェースという概念がないため、アクターが行う処理の改善やリファクタリングはアプリケーションの他の部分に影響を与えることなく容易に行うことができるメリットもある。アクター同士が非同期なメッセージングによって連携されることもあって、アプリケーションの構成要素は疎結合になる。

アクターはマイクロサービスのようでもある

以上を踏まえ、アプリケーションがアクタープログラミングモデルによって実装されているものとした場合、そのソフトウェアアーキテクチャはここ何年かで一般的になったマイクロサービスを彷彿とさせるかもしれない。だが、アクタープログラミングモデルでアプリケーションを開発する場合、その工程にモノリスの分割はない。0からマイクロサービスの枠組に合わせてアプリケーションを構築するという発想が近いかもしれないが、アクタープログラミングモデルで構成されたアプリケーションの全体像はマイクロサービスとは異なる。

一般的なマイクロサービスの同期的な連携ではRESTやgRPCなどが使われ、非同期で行われるバッチ処理との連携には、KafkaやAMQPを使ったミドルウェアを介したメッセージングが使われる。アクター同士のコミュニケーションにおいても非同期なメッセージングが使われるため、アクターの同士の連携をマイクロサービスのそれと同様に考えてしまうかもしれないが、アクターで使われるメッセージングは非同期に処理を行うことを目的としているのではなく、前述したようなスレッドの競合を解消するための同期機構を使わずして並行処理を効率的に実現するための不可避的な選択だと考えることができる。

メッセージの送受信に使われるメッセージキューだが、概念的には個々のアクターに内包されアクターの一部として存在し、それは複数のスレッドからアクセスされることになるメッセージングミドルウェアのチャネルやトピックとは異なる。同時にアクターの処理を実行するスレッドの数が1つに制限されていることで、リソース競合がないその状態は言わばスレッドセーフなコードブロックである。それはスレッドセーフではないPuppeteerのようなAPIを動かすためには、うってつけの”場所”だとも言える。

Akka.jsでPuppeteerを操る

Akka.js

アクタープログラミングモデルを使ってアプリケーションを実装するためのOSSのフレームワークやライブラリはいくつか存在する。Scala/Java向けに開発されているもので、とりわけユーザの評価が高いのが、知る人ぞ知るLightbend社のAkka ToolkitなのだがPuppeteerがnode APIであるため、Scala/Java向けに開発されているAkkaからでは直接そのAPIを実行することができない。ScalaのコードをJavaScriptにコンパイルすることができるScala.jsの存在は以前から知っていたので、AkkaをJavaScriptにコンパイルしてnodeで動かすことができないものかと調べてみたところ、Akkaをブラウザやnodeで動かせるように移植したAkka.jsの存在を知ることになった。

Akka.jsのソースディレクトリに、いくつかのソースファイルはあるのだが、驚いたことにそのほとんどが空っぽなのである。それがどういうことか説明すると、Akka.jsはScala/Java向けに開発されているオリジナルのAkkaのリポジトリから取得したソースファイルをビルド時にその空のソースディレクトリにコピーしている。そして、Scala.jsによってコンパイルされたプログラムがJavaScriptの実行環境で動作するために必要な変更のみが、そのいくつかのソースファイルの内容で行われるようになっているだけで、Akka.jsというのは、Scala/JavaのAkkaのソースコードがScala.jsによってコンパイルされたものなのである。それがnodeでScala/JavaのAkkaと同じように動くことを目の当たりにしたとき、Scala.jsの移植性の高さで二度驚くことになったのは、想像に難くないだろう。

Akkaのコードがnode環境でそのまま動くのは、なんとも不思議な感覚ではあるのだが、Akka.jsの着想と実現方法などに関しての論文にはそれが動く理由が記されている。残念ながら、Akka.jsはJVMのAkkaと機能拡張部分まで同等というわけではないため、Scala/JavaのAkkaのようにOOTBでクラスタ構成が組めるという訳ではなく、これを複数台のサーバーで実行するには、現時点ではインフラでカバーする必要がある。

SBT

Scala.jsを使うには、sbtのプラグインが必要になる。
plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1”)

Akka.jsをScala3でビルドするプロジェクトで使う必要があるため、.cross(CrossVersion.for3Use2_13)を指定する。Scala3でビルドをしないと後述するexpress.jsのTypeScriptの型情報からScala.jsの型を抽出する際に失敗するため、今回はやむを得ずScala3でビルドすることにした。

build.sbt
lazy val root = (project in file("."))
  .enablePlugins(ScalablyTypedConverterPlugin)
  .configure(baseSettings, nodeProject)
  .settings(
    useYarn := true,
    name := "squid-game",
    scalaJSUseMainModuleInitializer := true,
    libraryDependencies += ("org.akka-js" %%% "akkajsactortyped" % "2.2.6.14").cross(CrossVersion.for3Use2_13),
    libraryDependencies += ("org.akka-js" %%% "akkajstypedtestkit" % "2.2.6.14" % "test").cross(CrossVersion.for3Use2_13),
    libraryDependencies += ("org.akka-js" %%% "akkajsactorstreamtyped" % "2.2.6.14").cross(CrossVersion.for3Use2_13),
    Compile / npmDependencies ++= Seq(
      "puppeteer" -> "11.0.0",
      ...
  )
ScalablyTyped

ScalablyTypedを使いPuppeteerのTypeScriptの型定義からScala.jsの型を抽出し、それをAkka.jsから使えるようにする。Akka.jsでPuppeteerを操作して画像データを取得するには、Akka.jsのActorSystemに処理を要求し、その結果をアクターが要求元に返せる必要がある。

今回は、HTTPでAkka.jsのアクターにPuppeteerの操作を要求し、結果の画像データを一つにまとめてtext/htmlで取得できるようにexpress.jsを使うことにした。また、アクターの処理でDOM操作を行うためにcheerioもインストールする。

plugins.sbt
addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta36)
build.sbt
...
"@types/express" -> "4.17.13",
      "express" -> "4.17.1",
      "express-async-handler" -> "1.2.0",
      "cheerio" -> "1.0.0-rc.10"
      )
lazy val baseSettings: Project => Project =
  _.enablePlugins(ScalaJSPlugin)
    .settings(scalaVersion := "3.1.0",
      version := "0.1.0-SNAPSHOT",
      scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked"),
      scalaJSUseMainModuleInitializer := true,
      scalaJSLinkerConfig ~= (_
        /* disabled because it somehow triggers many warnings */
        .withSourceMap(false)
        .withModuleKind(ModuleKind.CommonJSModule))
    )
val nodeProject: Project => Project =
  _.settings(
    jsEnv := new org.scalajs.jsenv.nodejs.NodeJSEnv,
    stStdlib := List("esnext"),
    stUseScalaJsDom := false,
    Compile / npmDependencies ++= Seq(
      "@types/node" -> "16.11.7"
    )
  )

Akka.jsによるアクターの実装

Puppeteerを操作するための具体的な振る舞いをAkka.jsのアクターで実装したものがこちらだ。
PuppeteerBrowserPuppeteerPageの二つのアクターが連携し一度に複数のリクエストを並列に処理できる実装になっている。Puppeteerがどのようにリクエストを処理しているのかを具体的には説明しないが、ここで紹介する実装パターンはいずれ参考にしてもらえると思う。

アクターの実装方法は、Lightbend社のドキュメントを参照してもらうとして、この2つのアクターで、前述した4つの性質が実装されていることに気づいただろうか? 例えば、PuppeteerPageアクターは15分間メッセージを受けることがなかった場合に停止するようになっているのだが、それは

 3. アクターはアクターにメッセージを送信することができ、メッセージの送信先は自分自身でもよい。

性質が使われていると言える。 もう一箇所、この性質を使った実装があるのだが是非探してみてほしい。

どちらのアクターも常にactiveの状態に戻るように実装している。idleからactiveに状態遷移するとき、必ずbufferに滞留するメッセージが次にアクターが実行されるタイミングで処理される。

どちらもactiveの状態から、コードを読み進めてもらうと全体の処理の流れをつかむことができると思うのだが、経験を積んだプログラマの読者であれば、アクターが行う処理の追加と変更は比較的容易であることが分かると思う。

PuppeteerBrowser.scala
package example

import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.pattern.StatusReply
import akka.util.Timeout
import typings.node.bufferMod.global.Buffer
import typings.puppeteer.mod.Browser
import scala.concurrent.duration.{DurationDouble, FiniteDuration}
import scala.scalajs.js.Thenable.Implicits.*
import java.util.UUID.randomUUID
import scala.concurrent.ExecutionContext
import scala.reflect.classTag
import scala.util.{Failure, Success}

object PuppeteerBrowser {

  sealed trait Command

  case class CapturePageContent(id: String, url: String, replyTo: ActorRef[StatusReply[Buffer]]) extends Command

  case class CommandFailed(throwable: Throwable, replyTo: ActorRef[StatusReply[String]]) extends Command

  class Actor(context: ActorContext[Command], buffer: StashBuffer[Command])(implicit ex: ExecutionContext) {
    implicit val timeout: Timeout = 30.seconds

    private def initializing(pages: Map[String, ActorRef[PuppeteerPage.Command]] = Map.empty): Behavior[Command] = Behaviors.receiveMessage {
      case BrowserCreated(browser) =>
        context.log.info("Browser Created")
        idle(browser, pages)
      case InitializationFailed(throwable) =>
        context.log.error("Failed to initialize Browser")
        throw throwable
      case other =>
        buffer.stash(other)
        Behaviors.same
    }

    private def idle(browser: Browser, pages: Map[String, ActorRef[PuppeteerPage.Command]]): Behavior[Command] =
      buffer.unstashAll(active(browser, pages))

    private def active(browser: Browser, pages: Map[String, ActorRef[PuppeteerPage.Command]]): Behavior[Command] =
      Behaviors.receiveMessagePartial {
        case command@CapturePageContent(id, url, replyTo) =>
          pages.get(id).fold(create(browser, pages, id, command)) {
            page =>
              context.log.info(s"capturing page content from url for $id")
              page ! PuppeteerPage.CapturePageContent(url, replyTo)
              Behaviors.same
          }
        case PageTerminated(id) =>
          active(browser, pages - id)
      }

    private def create(browser: Browser,
                     pages: Map[String, ActorRef[PuppeteerPage.Command]],
                     id: String, request: Command): Behavior[Command] = {
      context.self ! request
      active(browser, pages + (id -> newPage(browser, id)))
    }

    private def newPage(browser: Browser, id: String): ActorRef[PuppeteerPage.Command] = {
      context.log.info(s"creating Page for $id")
      val page = context.spawnAnonymous(PuppeteerPage.Actor(browser))
      context.watchWith(page, PageTerminated(id))
      page
    }
  }

  private case class BrowserCreated(browser: Browser) extends Command

  private case class PageTerminated(id: String) extends Command

  private case class InitializationFailed(throwable: Throwable) extends Command

  object Actor {
    def apply()(implicit ec: ExecutionContext): Behavior[Command] =
      Behaviors.supervise[Command](
        Behaviors.withStash(100) { buffer =>
          Behaviors.setup {
            context =>
              context.pipeToSelf(typings.puppeteer.mod.launch()) {
                case Success(browser) => BrowserCreated(browser)
                case Failure(exception) =>
                  InitializationFailed(exception)
              }
              new Actor(context, buffer).initializing()
          }
        }).onFailure(SupervisorStrategy.stop)(classTag[Throwable])
  }
}
PuppeteerPage.scala
package example

import akka.Done
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer, TimerScheduler}
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.pattern.StatusReply
import org.scalablytyped.runtime.StringDictionary
import typings.cheerio.cheerioMod.Cheerio
import typings.cheerio.mod.Node
import typings.devtoolsProtocol.mod.Protocol.Network.ResourceType
import typings.node.bufferMod.global.{Buffer, BufferEncoding}
import typings.node.nodeStrings.undefined
import typings.node.nodeUrlMod.URL
import typings.puppeteer.anon.WaitForOptionsrefererstriTimeout
import typings.puppeteer.mod.*
import typings.puppeteer.mod.global.{Document, Element, NodeListOf}
import typings.puppeteer.puppeteerStrings.{request, response}
import wvlet.airframe.log
import typings.cheerio.{loadMod, mod as cheerio}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.{DurationDouble, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.classTag
import scala.scalajs.js
import scala.scalajs.js.Object.keys
import scala.scalajs.js.Promise
import scala.scalajs.js.Thenable.Implicits.*
import scala.util.chaining.scalaUtilChainingOps
import scala.util.{Failure, Success, Try}

object PuppeteerPage {

  trait Command

  case class CapturePageContent(url: String, replyTo: ActorRef[StatusReply[Buffer]]) extends Command

  case class PageContentCaptured(buffer: Buffer) extends Command

  case class CommandFailed(throwable: Throwable) extends Command

  private case class Created(page: typings.puppeteer.mod.Page) extends Command

  private case class InitializationFailed(throwable: Throwable) extends Command

  private class Actor(context: ActorContext[Command],
                      buffer: StashBuffer[Command],
                      timers: TimerScheduler[Command]) {
    private val resources = scala.collection.mutable.Set[String]()
    val handler: js.Function1[HTTPResponse, Unit] = res => {
      res.headers().get("content-type").fold(Future.successful(resources)) { value =>
        value match {
          case "image/jpeg" | "image/png" | "image/gif" =>
            res.buffer().map(buffer => resources += s"data:$value;charset=utf-8;base64,"
              .concat(buffer.toString(BufferEncoding.base64)))
          case other => resources
        }
      }
    }
    def initialize(): Behavior[Command] =
      Behaviors.receiveMessage {
        case Created(page) =>
          page.on_response(response, handler)
          idle(page)
        case InitializationFailed(throwable) =>
          context.log.error("Initialization failed")
          Behaviors.stopped
        case other =>
          buffer.stash(other)
          Behaviors.same
      }

    def idle(page: Page): Behavior[Command] = {
      if (timers.isTimerActive(TimeoutKey)) timers.cancel(TimeoutKey)
      timers.startSingleTimer(TimeoutKey, ClosePage, 15.minutes)
      buffer.unstashAll(active(page))
    }

    def terminating: Behavior[Command] =
      Behaviors.receiveMessagePartial {
        case CapturePageContent(url, replyTo) =>
          replyTo ! StatusReply.Error("Actor is terminating")
          Behaviors.same
        case PageClosed =>
          context.log.info("Page closed")
          Behaviors.stopped
        case CommandFailed(throwable) =>
          context.log.error(s"failed to close Page ${throwable.getCause.getLocalizedMessage}", throwable.getCause)
          Behaviors.same
      }

    def capturingPageContent(page: Page, replyTo: ActorRef[StatusReply[Buffer]]): Behavior[Command] =
      Behaviors.receiveMessage {
        case PageContentCaptured(value) =>
          replyTo ! StatusReply.Success(value)
          idle(page)
        case CommandFailed(throwable) =>
          replyTo ! StatusReply.Error(throwable)
          idle(page)
        case other =>
          buffer.stash(other)
          Behaviors.same
      }

    def active(page: Page): Behavior[Command] = Behaviors.receiveMessage {
      case CapturePageContent(url, replyTo) =>
        resources.clear()
        context.pipeToSelf(capturePageContent(page, url)) {
          case Success(value) => PageContentCaptured(value)
          case Failure(throwable) => CommandFailed(throwable)
        }
        capturingPageContent(page, replyTo)
      case ClosePage =>
        context.pipeToSelf(page.close()) {
          case Success(_) => PageClosed
          case Failure(throwable) => CommandFailed(throwable)
        }
        terminating
    }

    def capturePageContent(page: Page, url: String): Future[Buffer] = {
      val option = ScreenshotOptions()
      option.fullPage = true
      option.captureBeyondViewport = true
      for {_ <- page.setViewport(Viewport(1024, 768))
           _ <- page.goto(url, WaitForOptionsrefererstriTimeout().setWaitUntil(PuppeteerLifeCycleEvent.domcontentloaded))
           pageContent <- page.content().map(content => render(content, url))
           } yield Buffer.from(pageContent)
    }

    def render(content: String, url: String): String = {
      val $ = cheerio.load(s"<table id=\"images\"><thead>$url</thead><tbody></tbody></table>")
      resources.foldLeft($("#images > tbody")) {(images, value) => images.append(js.Array(s"<tr><td><image src=\"$value\"></tr>"))}
      $.html()
    }
  }

  case object ClosePage extends Command

  object Actor {
    def apply(browser: Browser)(implicit ec: ExecutionContext): Behavior[Command] =
      Behaviors.supervise[Command](
        Behaviors.withStash(100) { buffer =>
          Behaviors.setup {
            context =>
              Behaviors.withTimers { timers =>
                context.pipeToSelf(browser.newPage()) {
                  case Success(page) => Created(page)
                  case Failure(exception) =>
                    InitializationFailed(exception)
                }
                new Actor(context, buffer, timers).initialize()
              }
          }
        }).onFailure(SupervisorStrategy.stop)(classTag[Throwable])
  }

  private case object PageClosed extends Command

  private case object TimeoutKey

}

Akka.jsとexpress.js

こちらが2つのアクターを使ったアプリケーションを起動するApp.scalaなのだが、express.jsで受け取ったリクエストをメッセージに変換してアクターに送信している。

package.scala
import typings.express.mod as express
import typings.express.mod.{RequestHandler, request_=}
import typings.expressServeStaticCore.mod.*
import typings.node.bufferMod.global.Buffer
import typings.node.processMod as process
import wvlet.airframe.log

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ExecutionContext, Future}
import scala.language.postfixOps
import scala.scalajs.js
import scala.scalajs.js.JSConverters.*
import scala.scalajs.js.Object.{entries, keys}
import scala.scalajs.js.Thenable.Implicits.*
import scala.scalajs.js.UndefOr

package object example {
  trait ReqBody extends js.Object {
    val payload: js.UndefOr[js.Any]
  }

  object asyncHandler {
    type Req = Request[ParamsDictionary, Buffer, ReqBody, Query, typings.std.Record[String, js.Any]]
    type Res = Response[Buffer, typings.std.Record[String, js.Any], Double]
    type Handler = RequestHandler[ParamsDictionary, Buffer, ReqBody, Query, typings.std.Record[String, js.Any]]

    def apply(fn: js.Function3[Req, Res, NextFunction, Future[Unit]]): Handler =
      typings.expressAsyncHandler.mod(
        (param) => handleRequest(param.asInstanceOf[Req], fn))

    private def handleRequest(request: Req,
                              fn: js.Function3[Req, Res, NextFunction, Future[Unit]]): UndefOr[js.Promise[Unit]] = {
      for {res <- request.res
           next <- request.next} yield fn(request, res, next).toJSPromise
    }

  }
}
App.scala
package example

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.AskPattern.{Askable, schedulerFromActorSystem}
import akka.util.Timeout
import com.typesafe.config.ConfigFactory
import example.asyncHandler.{Req, Res}
import org.scalablytyped.runtime.StringDictionary
import typings.express.mod as express
import typings.node.bufferMod.global.Buffer
import wvlet.airframe.log

import java.net.URLDecoder
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.{DurationDouble, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future}
import scala.scalajs.js
import scala.scalajs.js.{isUndefined, undefined}
import scala.util.{Failure, Success}

object App {
  implicit val system: ActorSystem[PuppeteerBrowser.Command] = ActorSystem(PuppeteerBrowser.Actor(), "actorSystem")
  implicit val timeout: Timeout = 30.seconds
  val Handler = asyncHandler((req, res, next) => handleRequest(req, res))
  val app = express()

  def handleRequest(req: Req, res: Res): Future[Any] = (for {
    id <- req.params.get("id")
    url <- req.query.asInstanceOf[StringDictionary[String]].get("url")
  } yield system.askWithStatus[Buffer](PuppeteerBrowser.CapturePageContent(id, url, _)) map { buffer =>
    res.set("Content-Type", "text/html")
    res.send(buffer)
  }).getOrElse(Future.successful {
    res.set("Content-Type", "text/html")
    res.send(Buffer.from("requires both id and url"))
  })
  app.get("/:id", Handler)

  log.initNoColor

  def main(args: Array[String]): Unit = app.listen(3000, () => {
    println("Server started!")
  })
}

では、ゲームを始めよう。

http://localhost:3000/1234?url=http://localhost:3000/1234?url=https://www.netflix.com/jp/title/81040344
7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0