2025-09 開発スクラップ 主にLivebookのコード読解

はじめに
- どこかに書いておけば何かの役に立つだろう(?)なメモや、ただのトラブルの供養です。
- 話題に上げているLivebookバージョンは
@version "0.17.0-dev"
- コード生成AI環境は Claude Opus/Sonnet 4

Livebook よりプラクティスの切り出し ↓
-
data-p-*
をHookで使う共通の属性としている- それを処理する共通関数を作っている
-
assets/js/lib/
に共通ライブラリ群 -
if Mix.env() == :test do
からのdefp setup_tests
がある atlib/livebook/application.ex
。アプリ起動前に実行したいからコード注入やむなしだったのかな。 -
Livebook.Config
がモジュールとして独立していて、設定周りはそこに関数を集約している様子 - サイドバーの実装は面白い。表示と処理をうまく分離させていて、処理をhooksとして用意しsubscribeやイベントハンドラもattach_hookなどでアサインしている。
-
{Task.Supervisor, name: Livebook.TaskSupervisor}
でTaskを切り出している。LiveViewプロセスは画面切り替えで落ちるからだろうと思われる。 -
lib/livebook/utils/unique_tasks.ex
みたいい複数defmodule
があるファイルもある。NotFoundError
を中に書いているところもある。 - 全画面共通系は、
attach_hook
を活用するなどで切り出せる。 - HTML上で操作のためにつけている属性は
data-el-*
- 画面の一部LiveComponentは
*_live/*_component.ex
、共通利用のLiveComponentは<*context_name>/*_component.ex
かな -
render
に関わる関数がファイル上部に全てあるのかな? // これは少し意外。トップのrender
だけ上で細かい(LiveComponent内のコンポーネント単位 defp相当)はファイル下部が好きなので... -
lib/livebook/session.ex
とlib/livebook_web/helpers/session.ex
があるが、一度ヘルパーを通すときはwebで扱う情報(socketとか)に情報を入れるユースということだろうか。 - ノートを表示するLiveView(SessionLive)は
.html.heex
ファイルを使わずに、render
を別モジュールにdelegate
している。確かに結局defp
が欲しくなるし、ファイル分けるにしてもテンプレートファイルじゃなくていいかもしれない。 -
defguard
やdefguardp
を使っているところがある(初めてみた)。 - 単にデータを変換するような関数は、
get_
とかconv_
とかなしでデータ名_from
が多い、かもしれない。
その他
-
Keyword.validate!
系がわりとある -
def handle_params(%{}, _url, socket) when socket.assigns.live_action == :public_new_notebook do
長いパターンマッチは使わずにwhen
で処理している様子

Livebook 前回みたときから時間が空いたので手元で試す。

elixir-1.8 で mix setup
後に phx.server

ERROR!!! [Livebook] You must set the environment variable ELIXIR_ERL_OPTIONS="-epmd_module Elixir.Livebook.EPMD" before the command (and exclusively before the command)

とりあえず指示通りに指定すれば動く。Livebook側で :erl_epmd
をカスタマイズしている理由は:
TODO

Erlang/OTP 27 [erts-15.0.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
[notice] :alarm_handler: {:set, {:system_memory_high_watermark, []}}
[info] Reading storage from <>/tmp/livebook_data/dev/livebook_config.v1.ets
[info] Running LivebookWeb.IframeEndpoint with Bandit 1.6.8 at 127.0.0.1:4001 (http)
[Livebook] Application running at http://localhost:4000/
[info] Running LivebookWeb.Endpoint with Bandit 1.6.8 at 127.0.0.1:4000 (http)
Interactive Elixir (1.18.4) - press Ctrl+C to exit (type h() ENTER for help)

livebook_data/dev/livebook_config.v1.ets
を使う様子。ETSの活躍場面は設定保存面、ETSといっても永続化している。

tidewave のパッケージを入れようとしたら最初から入っていた。
そのため設定だけを済ます。
{
"mcpServers": {
"tidewave": {
"type": "http",
"url": "http://localhost:4000/tidewave/mcp"
}
}
}
- refs: https://hexdocs.pm/tidewave/claude_code.html
-
type
はsseではなくhttpを今使っている様子

Livebook 環境変数と意味、あるいはコード読解のきっかけとして。

Livebook.config_runtime()
なる関数がある。runtime.exs
から呼び出している。
...どうやらLivebookそのものをアプリケーションにそのまま組み込んだときのために、関数でruntime.exs
には書かずに呼び出す形を取っている模様。attached
とかもせずに、PJにパッケージとして入れ込むときのもの(?)。serverless
という設定があるのでそっちも付随して設定が必要そう。

LIVEBOOK_ALLOW_URI_SCHEMES=ftp,others:
Livebookで https://github.com/rehypejs/rehype-sanitize を使っている。そのhref
属性として使用可能なURIスキーマを加えることができる。
phx-hook="MarkdownRenderer"
に渡されている。タグのdata-p-*
属性を各Hookで使うオプション的なものとしている様子。
...手厚い。でも[hoge](others://example.com)
の反映は確認できたけど、others://example.com
直書きだとリンクにしてくれない。自動でリンク化するのはremark-gfm
の仕事でそちらはカスタマイズできなさそうではある。

LIVEBOOK_APP_SERVICE_NAME=:
LIVEBOOK_APP_SERVICE_URL=:
System settingに表示+リンク、詳しい用途不明。触ることはなさそう。

LIVEBOOK_APPS_PATH=pwd
/notes/apps/:
LIVEBOOK_APPS_PATH_PASSWORD=:
指定したディレクトリの.livemd
をアプリケーションとして実行時にデプロイする。WebアプリのプラットフォームとしてのLivebook
(直接関係ないけど、起動時にライブラリ群をロードしてくれそうだからMix.install
したいものを忍ばせた.livemd
なファイルを置いておくといいかもしれない? 書いた)

LIVEBOOK_APPS_PATH_WARMUP:
例えばmanual
にしておいて、Dockerイメージ作成時Dockerfileで実行しておけば、インスタンス起動ごとの実行を省略できるので起動が早くなる。
=> Mix.install()
はパッケージによっては時間を食うので、ありがたい。

LIVEBOOK_AWS_CREDENTIALS:略

LIVEBOOK_BASE_URL_PATH:
LIVEBOOK_PUBLIC_BASE_URL_PATH:
リバースプロキシを使うなどで、URLが見かけと違うときに見かけのURLを設定する。

LIVEBOOK_CACERTFILE:
認証局証明書へのパス。あまり使わないだろうけど社内認証局を使う外部リソースとかへのアクセスに。

LIVEBOOK_CLUSTER:
LIVEBOOK_COOKIE:
dns_clusterのqueryとして扱われるものと、クッキー設定。
コードをよむと dns:~
で設定しないといけない様子。
指定するとクラスタに参加する。// 明示的に実施したことがないが、活用するプロジェクトはどこか面白そう。

LIVEBOOK_DATA_PATH:
内部的なデータの置き場所。指定なしだと、:filename.basedir/2
を使っている => :filename.basedir(:user_data, "livebook")
。
test実行時に削除するようなコードがあるから指定するときは注意したほうがよさそう。指定しないときは環境ごとのconfig
の設定を使う。

LIVEBOOK_DEFAULT_RUNTIME:
ノートをstandaloneで動かすか、他のNodeにattachさせるか、入れ子で使っているかの設定。Projectの補助用途ならattachしておく。遊ぶだけならstandalone

LIVEBOOK_FIPS:
FIPS: Federal Information Processing Standards 要件対応
要件であり、守る必要があるなら遵守した実装にする必要がある。

LIVEBOOK_FORCE_SSL_HOST:
localhost時には機能しない。 // こんなのもあるのか。

LIVEBOOK_HOME:
画面のファイルブラウザの初期ディレクトリになるので割と大事。デフォルトはユーザーホームだが、プロジェクトホームとか livemd
を置くルートディレクトリにすべき。

LIVEBOOK_IDENTITY_PROVIDER:
ベーシック認証からクラウド活用まで。

LIVEBOOK_IFRAME_PORT:
LIVEBOOK_IFRAME_URL:
Livebook上のJSは、開いているノートとぶつからないように(あるいはその他理由のため)用意されたiframe中で実行される。そのiframeに内容をJSで書き足して、その結果をノートでは見せている。
https
では特に指定がないとlivebookusercontent.com
を拝借している。// なるほど
https://livebookusercontent.com/iframe/v5.html
をベースに自分で用意すれば、共通のカスタマイズができるということになる。

LIVEBOOK_IP:
LIVEBOOK_PORT:
そこまで使うことがなさそうだが、Dockerで閉じるときに0.0.0.0 にするなど。ポートは同じマシン中で複数動かしたり、ポートを変更する際に。あわせてLIVEBOOK_IFRAME_PORT
もおそらく変更が必要。

LIVEBOOK_LOG_LEVEL:
LIVEBOOK_LOG_METADATA:
LIVEBOOK_LOG_FORMAT:
Loggerに渡される設定

LIVEBOOK_NODE:
Node名を指定。Node.start(node, :longnames)
しているのでFQDNなどを使う必要あり。

LIVEBOOK_PASSWORD:
パスワードを設定可能。
prod環境で設定していない場合が起動時にトークンが発行されるのでそれを使う。
dev環境ではauthentication
自体が:disabled
なので未設定ならば求められない。

LIVEBOOK_PROXY_HEADERS:
リバースプロキシのときのあるある設定(なるべく使わないでいたい)

LIVEBOOK_SECRET_KEY_BASE:割愛

LIVEBOOK_SHUTDOWN_ENABLED:
サイドバーから終了操作ができるかどうか。終了操作したあとにどうやって再開するんだろう(Webアプリ)

LIVEBOOK_TOKEN_ENABLED:
false
にするとprodでも無条件にアクセスできるのだろう(とても危険)

LIVEBOOK_UPDATE_INSTRUCTIONS_URL:割愛
提供サイドの設定項目

LIVEBOOK_WITHIN_IFRAME:
デフォルトはfalse
。Livebookそのものを外部サイトからiframeで入れ込めるようにするときにtrue
にする模様。
活用すると、Livebook Appsを埋め込みまくった1つのページが作れるのかもしれない。

その他
- MIX_INSTALL_DIR: 確か
Mix.install()
の保存先なので、重たいライブラリがあるなら(docker運用下)指定して、永続化させておくのが良かった記憶

Livebook 起動されるサーバと役割 Livebook.Applicationより

LivebookWeb.Telemetry:
自動生成される項目よりも少なめになっている。特に特徴はなし。
// そういえば自動作成された後に真面目に吟味したことがなかった。

{Phoenix.PubSub, name: Livebook.PubSub}:
活用しているところが多いので略。後述

{Task.Supervisor, name: Livebook.TaskSupervisor}:
Taskを扱うSupervisorを起動している。
// こういうのあったか。例えばLiveViewプロセスが親で単にTaskを起動するとlive_navigation
とかされると子もなくなる(はず)けど、任せてしまっておけば問題なくTaskは続けられる。例えば画面アクセス時起点でDBに何か記録するみたいなもの(仮)は、専用プロセスにぶん投げたほうが良い。
Livebookでは例えばファイル保存処理で使っている様子。確かにページ切り替えられても保存処理が完了したいから適例。

Livebook.Utils.UniqueTask:
Livebookが複数セッションを前提にしている中で、一度で良くて重複が困る処理を担当している。
...わりとLivebookな強みに関わる模様。というのは、リモートで実行したセルの結果を受け取るときに使っている様子で、そのときの重複取得を防いでいる大事な役割。
📝 ref = Process.monitor(pid)
して重複タスクも完了を待っている。

Livebook.Storage:
{{namespace, entity_id}, attribute, value, timestamp}
secretの保存など。ETSだがfileに保存しているためアプリを落としても保存されている。

{Livebook.Utils.SupervisionStep, {:migration, &Livebook.Migration.run/0}}:
その場で(非同期で)実行するためのLivebook.Utils.SupervisionStep
を用意して、Migrationを走らせている。
// あとでTask.Supervisor
に積むのではいけないのだろうか。
migrationはEctoのそれとは無関係で、アプリケーションのバージョンアップに伴う処理をいれる想定に見える。

Livebook.UpdateCheck:
バージョンアップのチェック。チェックした結果はstateとして保持。いまのバージョンは設定にてMix.Project.config()[:version]
している。

Livebook.SystemResources:
その通り、システムリソースを定期的に計測してetsに保存している。これは↑のStorage
とは別で単なるets活用 // state でもたない理由があるだろうか。

Livebook.NotebookManager:
ホームに表示される最近使ったノートブック、まわりの管理。
実装面では {:noreply, state, {:continue, :dump_state}}
からのhandle_continue
でStorageへの保存を行っている点が面白い。

{Livebook.Tracker, pubsub_server: Livebook.PubSub}:
Phoenix.Tracker
を使っている。diff
で検知したいのが背景。開いているセッション(=ノート)と、稼働しているアプリをTrackしている様子。
- pubsub_serverがオプション指定なのは不明
- セッションに参加しているユーザーは
Process.monitor
でpid監視なのでまた別

Livebook.EPMD.NodePool:
Nodeごとに毎回アトムを発行するとメモリや枯渇を招くため、一度作成したものを(解放済みなら)再利用するための仕組み。なるほど。
LivebookはノートごとにNodeを起動しているので、atomの大量消費を抑えているもの。

Livebook.Session.FileGuard:

{DynamicSupervisor, name: Livebook.RuntimeSupervisor, strategy: :one_for_one}:
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one}:
{DynamicSupervisor, name: Livebook.HubsSupervisor, strategy: :one_for_one}:
それぞれのプロセスの管理用途。ものが違うので別途準備している。
Trackerとはまた別で、Trackerは情報保持の観点であり、こちらはプロセス管理用途。

{Registry, keys: :unique, name: Livebook.HubsRegistry}:
HubのためのRegistry。あるHubの情報を引くためにレジストリしておくイメージ。// SessionはTrackerがあるし、Runtimeは内部で使うプロセスなのでレジストリなどはない。

{Livebook.Utils.SupervisionStep, {:boot, boot(create_teams_hub)}}:
Hubの初期化(非同期で)。Hubは簡単には1つの設定群。手元かstagingかprodかといった単位で切り替えができるので、それぞれの設定(DB向き先とか)を反映して、ノートを実行できる。

Livebook.Apps.DeploymentSupervisor:
Apps独自のsupervisorを起動している。// 深い
- Apps.ManagerWatcher: Managerの起動と落ちたときの各処理。Managerが
:global
なので、各クラスタがwatchしているということだろうか。

Livebook PubSub活用シーン
(js側にも簡易的にあるがここではサーバ側~Phoenix.PubSub)
broadcast, local_broadcast, direct_broadcast はそれぞれ、全ノード、同一ノード、特定ノードへの送信で、使い分けできる。
// web系でのsubscribeは大抵リスト更新とかなので調べない。

"apps:#{slug}":

"tracker_apps":
appsについてTrackerからの通知受け。Trackerがノード情報を持っておりdirectが適当(// localでも可?)

"apps_manager":
appsの管理サーバ(:global起動)からの通知受け。だと思うけど local_broadcast
を使っているのがよくわからない。各クラスタに通知するなら普通のbroadcast
な気がした(深くは追わないけど)。

"hubs:#{topic}":
各クラスタの情報を得るために、NotebookManagerなどが欲しいtopicで購読する。

"notebook_manager:recent_notebooks":
"notebook_manager:starred_notebooks":
ノートブック状況の通知。こちらはbroadcast
なので組んでいたら反映される(未確認)。

"sessions:#{session_id}":
あるノートブック(セッション単位)を複数のクライアント(〜LiveViewプロセス)が使うため、セッションは基本的にoperationによる更新(通知)で同期を計っている。

defp runtime_messages_topic(session_id, topic, subtopic):
runtimeとsessionプロセス間でのtopic

"tracker_sessions":
sessionsについてTrackerからの通知受け。Trackerがノード情報を持っておりdirectが適当(// localでも可?)

"settings":
設定通知用途

"system_resources":
システムのリソース状況更新次の通知。ローカルなマシンの話なのでlocal_broadcast
のみ。

"teams:#{topic}":
sessionと同様、複数クライアントを想定して基本的にはtopic経由の通知更新を行う。

"users:#{user_id}":
ユーザー情報の変更の通知更新を行う。

"manager_watcher":
テストでしか使っていない様子。

📝 異なるクラスタ(Livebook)から同じファイルを開こうとするとエラーになるらしい。あくまで同じsession(URL)に参加する形をとれば、共同編集ができる。

📝 Livebook Apps内で定義したモジュールも再利用できると嬉しい、と試したものの(最初からruntimeがあるといっても)やはりステップが多い。.livemd
からelixir
を抽出するライブラリを作ってMix.install()
する方が幸せかもしれない。

Livebook Plug/Hooks ホーム画面他共通

/sessions/:id/assets/:hash/*file_parts
のfile_parts部分は、images/icon.png
といったときに、%{"file_parts" => ["images", "icon_png"]
となる。 // 使ったことなかった。

LivebookWeb.Confirm
の作り方はプラクティスとして面白い。厄介というか手間というかな確認画面を見事(だと思うけど)に切り出している。

live_session :app, session: ~
の形でいれるsessionは、liveview_sessionになるから重たいデータも入れられる。

レイアウトデザイン

use Phoenix.LiveView, layout: {LivebookWeb.Layouts, :live}
- 全体mainはあるある
<main role="main" class="grow flex flex-col h-screen">
-
flash_group
はcore_component
のままなんだ。特にlive_componentで出しているわけではなさそう。

Homeでは実際にはLayoutComponents.layout()
を使っている。

live-region
スクリーンリーダー用の表示更新もある様子

サイドバー(on Home):
<nav
class="hidden md:flex w-[17rem] h-full py-2 md:py-5 bg-gray-900"
aria-label="sidebar"
data-el-sidebar
>
- 操作のための属性は
data-el-sidebar
-
md
以上では常に出していて隠す機能はなし(ホームでのサイドバー)。小さいときのみ表示可否変更ができる - イベントなどの処理はhooksで切り出している(調査済み省略)
カレント(今開いているか)でのデザイン分けは下記。各リンクはnavigate
だったので、おそらくpatchで移動可能な粒度のサイドメニューはない様子。
defp sidebar_link_text_color(to, current) when to == current, do: "text-white"
defp sidebar_link_text_color(_to, _current), do: "text-gray-400"

ホーム画面で使うlive_component
の切り出しの一例
<.live_component
module={LivebookWeb.HomeLive.SessionListComponent}
id="session-list"
sessions={@sessions}
starred_notebooks={@starred_notebooks}
memory={@memory}
/>
- 命名規則的には、ある画面専用なら
livebook_web/home_live/hoge_component.ex
で、共通で使うものはlivebook_web/<name>/hoge_component.ex
みたいな感じかな。画面付属なら*_live
フォルダ以下ということ。 // おそらく一般的

ホーム画面のimport
など一部使っていない(だろう)機能が残っている様子。

ノートブック画面まわり。基本のURLは /sessions/:id
-
session
情報そのものは永続化対象ではなくdefstruct
として定義されている。// ホーム画面などに表示するときに使うような情報粒度というところだろうか。 -
state
がサーバ状況としての保持情報、ほか、Session.Data(state.data
)という全クライアントで共有するデータがある。

session_id
は boot_id
, node_id
からなる
boot_id = Livebook.Config.random_boot_id()
node_part = node_hash(node())
random_part = :crypto.strong_rand_bytes(11)
binary = <<boot_id::binary, node_part::binary, random_part::binary>>
# 3B + 16B + 11B = 30B is suitable for base32 encoding without padding
Base.encode32(binary, case: :lower)
node_id
はローカルではないクライアントがノード接続(既に開かれているファイルならそのsessionをもつサーバへ接続,PubSub購読)に必要。boot_id
は提供元Livebookが再起動していると確実にsession_id
がすでに存在していない判定でチェックしている(?)

Livebook.Session.Worker ...?
これをちらっと見た感じでは、ただ中継しているだけである。sessionそのものもサーバだが、workerサーバとして置く必要性がいまひとつわからない。責務としてプロセスを分けたかったんだろうか?でもワークしていることがない...

セルの実行まわり

- エディタまわりの仕組み
- 編集中の同期の仕組み
- 保存時の同期の仕組み
- 実行時の結果の受け取りと同期の仕組み
- ...

セルを作成するメニューの出し方:
表示する処理自体は、各セクション(見出しや各セルのくくり)の下にひっついている。あとはそれらが何もないときに出している。
メニュー表示の出し入れをどうしているのかと思ったらhover:
で透過率のクラスを変えているだけだった。なるほど、この場合は表示されないときは、ノートのほど良い余白、になるからそれで十分か。
snippet_definitions
:
Block > CODE
のところにあるメニューに出すものだが、カスタマイズはできなさそう、固定。
smart_cell_definitions
:
Smart
のところにあるメニューに出すもので、自作のパッケージから追加できる。
技術的には、ライブラリから規定のregister
関数で追加するmoduleを登録すれば、それが反映されるというもの。追加スマートセルもいくつかのプロトコルに従って実装が必要。

既存のセルの表示まで:
render
側ではHook(と置き場所になるdom)だけ置いて、push_event()
によって描画(およびJSエディタの初期化)を行っている。画面が表示されているときに出ているのはLiveEditor
で描画しているsourceとなる。
LiveEditorのマウント前に、置き場所となっているdomのところにsourceを飾らずに表示している処理がある(マウント時に消している)。これはどうやらスクロール系の都合の様子。

セルの作成から表示まで:
- メニューをクリックして選択
- Sessionモジュールで、sessionプロセスに実行させるために
cast
(非同期)したり、セルの作成をつないでいる各クライアントにbroadcast
している。 -
Data.apply_operation
が特徴的で、各LiveViewプロセス側で実行しているし、sessionプロセス自身でも実行している。LiveViewプロセスが同じNode内だと重複実行なんだろうけど、違うNodeがあることを前提に、透過的に処理をしている(のだろうか)。 - ↑で重複実行と書いたが、実際に
Data.apply_operation
で駆動されるアクションは、プロセスごとに動作が違って、sessionプロセスはセルのruntime
を実行させるし、LiveViewプロセスは単に画面を更新するためのpush_event
をしている様子。 -
runtime
は実行完了後にsessionプロセスに通知、そこからbroadcastして購読しているLiveViewプロセスがそれぞれ画面更新して、結果が反映される。
// ロジカルしてて面白いが、難しいな、分散システム

スマートセル SmartCell まわりの確認

考えてみればruntimeが独立しているからそれはそうなんだけど、KinoはLivebook自体には含まれていない。Livebookの本体でしていることは、UIにメニューとして出してあげることや、メニュー押下したときにデータを渡してあげるなどのフロー。
Livebookでは、主要なKino
を使うメニューはハードコードされていて押すとKino
のインストールが求められる(standalone)。自前でMix.install
で追加したものは、動的に追加されて(ライブラリ内でKino.SmartCell.register
している必要)、メニューに反映される。

ということで、自作スマートセルを作ったらKino.SmartCell.register(MyCell)
で登録が必要。// おそらく多くのライブラリではapplication.ex
で行っていると思われる。

メニューが選択されたときには、セルが作られる。Kino.SmartCell
でもって必要なものをMyCell
から取り出す。
選択されたときにMyCell.init()
がコールされる。みたところではattrs
は空%{}
だった。実装としては、使いたい変数をctxにassign
(初期化)しておく。
@impl true
def init(attrs, ctx) do
{:ok, ctx}
end
attrs
がJSON互換のmap形式、ctx
が%Kino.JS.Live.Context
で状態を保持する自由なストアという感じか。attrs
は、ctx
から、コード実行・ノートの保存復元(init
)できる形にしたもの。

クライアント接続時にはMyCell.handle_connect()
がコールされる。この返り値には、JS側で使用するためのpayload
を返す。たぶんto_payload
関数を作ってしまった方が意味がわかりやすい。
JSで使用するもの=画面のUIとして提供するもの

to_source
はセルで実行するコードであり、画面上でソースに切り替えたときに表示されるコードになる。
つまりcx |> to_attrs() |> to_source()
の関係がある。
描画するための情報の流れ:
Kino.VegaLite.render()
=> Kino.render()
=> Kino.Render.to_livebook()
Livebookの利用者が見ているのは、to_livebook
で返された情報で実行されたiframe内のHTML
独自の描画をしたいときは、Kino.JS
に従った実装をしたモジュールを作って、それをto_source
内から呼び出す。