イ〇ゲーム「その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つの性質を持つことを前提に実装する処理を設計する必要がある。
-
アクターはメッセージを受けることができる
-
アクターは新たなアクターを生成することができる
-
アクターはアクターにメッセージを送信することができ、メッセージの送信先は自分自身でもよい。
-
アクターは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のプラグインが必要になる。
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1”)
Akka.jsをScala3でビルドするプロジェクトで使う必要があるため、.cross(CrossVersion.for3Use2_13)
を指定する。Scala3でビルドをしないと後述するexpress.js
のTypeScriptの型情報からScala.jsの型を抽出する際に失敗するため、今回はやむを得ずScala3でビルドすることにした。
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もインストールする。
addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta36”)
...
"@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のアクターで実装したものがこちらだ。
PuppeteerBrowser
とPuppeteerPage
の二つのアクターが連携し一度に複数のリクエストを並列に処理できる実装になっている。Puppeteerがどのようにリクエストを処理しているのかを具体的には説明しないが、ここで紹介する実装パターンはいずれ参考にしてもらえると思う。
アクターの実装方法は、Lightbend社のドキュメントを参照してもらうとして、この2つのアクターで、前述した4つの性質が実装されていることに気づいただろうか? 例えば、PuppeteerPage
アクターは15分間メッセージを受けることがなかった場合に停止するようになっているのだが、それは
3. アクターはアクターにメッセージを送信することができ、メッセージの送信先は自分自身でもよい。
性質が使われていると言える。 もう一箇所、この性質を使った実装があるのだが是非探してみてほしい。
どちらのアクターも常にactive
の状態に戻るように実装している。idle
からactive
に状態遷移するとき、必ずbuffer
に滞留するメッセージが次にアクターが実行されるタイミングで処理される。
どちらもactive
の状態から、コードを読み進めてもらうと全体の処理の流れをつかむことができると思うのだが、経験を積んだプログラマの読者であれば、アクターが行う処理の追加と変更は比較的容易であることが分かると思う。
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])
}
}
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
で受け取ったリクエストをメッセージに変換してアクターに送信している。
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
}
}
}
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