Yew(Rust)のDocker imageサイズを0.2%まで削減してみた

Cloud RunでRustのYewのブログを配信しているのですが、Docker imageが1GBを超えていたので大幅に削減してみました

Docker imageが1.3GB

このブログをNext.jsからRustに入れ替えてからしばらくの間時間を取ることができなかったので、特別な対応を取らずにリリースしていた結果、Docker imageが1.3GBと非常に大きいサイズに膨れ上がっておりました

Rustのオフィシャルイメージは大きいことが知られていますが、さすがにこの大きさだとDocker imageを保存するコストや実行されるまでのスピードに影響があると思い、削減するためのアクションを取り始めました

Rustに限らずバイナリを生成して実行することができる場合、Dockerで複数のステージを構築して、実行時はバイナリだけを含めた軽量のイメージを作成できるみたいなので段階的に分けていきます

実行ステージをdebianへ

とりあえずdebianを実行ステージで利用して、バイナリだけを含める実装を行います

※開発時のメモ書きなので不足箇所や不要箇所が含まれているかもしれません

FROM rust:1.70.0 as builder

RUN apt-get update && apt-get install -y make g++ binaryen

RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk

RUN cargo build --release
RUN make ssr_build

# 実行 Stage
FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y libgcc1 libstdc++6 bash

EXPOSE 8080
COPY --from=builder /usr/ssr_server/dist/ /dist/
COPY --from=builder /tmp/target/release/simple_ssr_server /simple_ssr_server

ENTRYPOINT ["./simple_ssr_server"]
CMD ["--dir", "dist"]

こちらのDockerfileを実行すると46.3MBとなり、1.3GBと比較すると約3.5%まで削減できました
(46.3 / (1.3 * 1024) = 0.03478)

debianはlsコマンドや必要な機能群が含まれており、比較的大きなイメージではありますが、エラーが発生することもなく簡単に実装できるのかなり効果的でした

alpineでさらに小さく

さらにDocker imageを小さくするためにalpineに変更します

FROM rust:1.70.0-alpine as builder

RUN apk add --no-cache build-base npm binaryen

WORKDIR /usr
COPY . .

RUN rustup target add x86_64-unknown-linux-musl
RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk

RUN cargo build --release --target x86_64-unknown-linux-musl

WORKDIR /usr/crates/ssr_server

RUN trunk build --release -d ./dist
RUN cp robots.txt ./dist/robots.txt
RUN cargo build --release --target x86_64-unknown-linux-musl --features=ssr --bin simple_ssr_server --


FROM alpine:latest
RUN apk add --no-cache libgcc libstdc++ ca-certificates

EXPOSE 8080
COPY --from=builder /usr/crates/ssr_server/dist/ /dist/
COPY --from=builder /usr/target/x86_64-unknown-linux-musl/release/simple_ssr_server /simple_ssr_server

ENTRYPOINT ["./simple_ssr_server"]
CMD ["--dir", "dist"]

alpineを実行ステージのimageに設定するとのDockerfileを実行すると、6.9MBまで削減でき、1.3GBと比較すると約0.5%まで小さくできたということになります
(6.9 / (1.3 * 1024) = 0.00518)

ちなみにですが、alpineに変更するタイミングでopenssl関連の動的リンク問題が発生して2週間ほどハマるという大失態を犯してしまいました

この記事の下部に記載しましたので、興味があればご覧ください

完成版Dockerfile、scratchで最小化

こちらが完成版のDockerfileです

scratchという最小のイメージでバイナリだけを設置したDockerfileです

FROM rust:1.70.0 as builder

RUN apt-get update && apt-get install -y binaryen musl-tools && rm -rf /var/lib/apt/lists/*

WORKDIR /usr
COPY . .

RUN rustup target add x86_64-unknown-linux-musl
RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk

RUN cargo build --release --target x86_64-unknown-linux-musl

WORKDIR /usr/crates/ssr_server

RUN trunk build --release -d ./dist
RUN cp robots.txt ./dist/robots.txt
RUN cargo build --release --target x86_64-unknown-linux-musl --features=ssr --bin simple_ssr_server --

# Runtime Stage
FROM scratch

EXPOSE 8080
COPY --from=builder /usr/crates/ssr_server/dist/ /dist/
COPY --from=builder /usr/target/x86_64-unknown-linux-musl/release/simple_ssr_server /simple_ssr_server

ENTRYPOINT ["/simple_ssr_server"]
CMD ["--dir", "dist"]

scratchにバイナリを含めるだけなので2.6MBまで削減でき、1.3GBと比較すると約0.2%の大きさになりました
(2.6 / (1.3 * 1024) = 0.00195)

小さくすることは正義👍

動的リンク問題

問題の確認

実行ステージのイメージをalpineに変更したタイミングでDockerのbuildはできるけど実行するとアプリが落ちるという問題が発生しました

cargo run --release --target x86_64-unknown-linux-musl --features=ssr --bin simple_ssr_server -- --dir dist

// 実行時のエラー ↓

Compiling anymap2 v0.13.0
error: failed to run custom build command for `openssl-sys v0.9.93`

調べてみると動的リンクの問題が発生しているようです

動的リンクとはプログラムを実行する際に、外部のライブラリとリンクされる方式を指す用語のことです

なので、alpineに動的リンクされているライブラリを含めるか動的リンクを行わないという選択肢があります

※確か静的リンクにしてください、的なメッセージが出て静的にした気がします。間違っていたらすみません、、、、

動的リンクされているか確認

まずは本当に動的リンクされているかを確認するためにDockerfileの中にlddコマンドを追加して、依存ライブラリがあるのかどうか?を検証します

RUN ldd /usr/src/app/target/x86_64-unknown-linux-musl/release/simple_ssr_server

実行したところopenssl関連の動的リンクが発生していることを確認できました

/lib/ld-musl-x86_64.so.1 (0x7f00695ed000)
libssl.so.3 => /lib/libssl.so.3 (0x7f0068f50000)
libcrypto.so.3 => /lib/libcrypto.so.3 (0x7f0068b97000)
libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f00695ed000)

opensslがどこで利用されているかを確認

Rustには依存関係を表示してくれるtreeというコマンドがあるので実行みます

どうやらreqwestの中にopensslが含まれるようですね

cargo tree --target all

├── infrastructure v0.0.0 
│   ├── reqwest v0.11.20
│   │   ├── hyper-tls v0.5.0
│   │   │   ├── bytes v1.5.0
│   │   │   ├── hyper v0.14.27 (*)
│   │   │   ├── native-tls v0.2.11
│   │   │   │   ├── lazy_static v1.4.0
│   │   │   │   ├── libc v0.2.148
│   │   │   │   ├── log v0.4.20
│   │   │   │   ├── openssl v0.10.57 ← ここに入ってる
│   │   │   │   │   ├── bitflags v2.4.0
│   │   │   │   │   ├── cfg-if v1.0.0
│   │   │   │   │   ├── foreign-types v0.3.2
│   │   │   │   │   │   └── foreign-types-shared v0.1.1
│   │   │   │   │   ├── libc v0.2.148
│   │   │   │   │   ├── once_cell v1.18.0
│   │   │   │   │   ├── openssl-macros v0.1.1 (proc-macro)
│   │   │   │   │   │   ├── proc-macro2 v1.0.67 (*)
│   │   │   │   │   │   ├── quote v1.0.33 (*)
│   │   │   │   │   │   └── syn v2.0.33 (*)
│   │   │   │   │   └── openssl-sys v0.9.93
│   │   │   │   │       └── libc v0.2.148
│   │   │   │   │       [build-dependencies]
│   │   │   │   │       ├── cc v1.0.83 (*)
│   │   │   │   │       ├── pkg-config v0.3.27
│   │   │   │   │       └── vcpkg v0.2.15
│   │   │   │   ├── openssl-probe v0.1.5
│   │   │   │   ├── openssl-sys v0.9.93 (*)

opensslの問題解消

調査してみると、reqwestのfeatureを変更することでopensslの問題を解決できるようなので、Cargo.tomlに記載されているreqwestを下記のコードのように修正します

reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] }

修正完了後にtreeコマンドを実行するとopensslからrusttlsに切り替わっており、buildして実行してもエラーが起きませんでしたので、問題を解消することができました

cargo tree --target all

├── infrastructure v0.0.0 
│   ├── reqwest v0.11.20
│   │   ├── hyper-rustls v0.24.1
│   │   │   ├── futures-util v0.3.28 (*)
│   │   │   ├── http v0.2.9 (*)
│   │   │   ├── hyper v0.14.27 (*)
│   │   │   ├── rustls v0.21.7
│   │   │   │   ├── log v0.4.20
│   │   │   │   ├── ring v0.16.20
│   │   │   │   │   ├── libc v0.2.148
│   │   │   │   │   ├── once_cell v1.18.0
│   │   │   │   │   ├── spin v0.5.2
│   │   │   │   │   ├── untrusted v0.7.1
│   │   │   │   │   ├── web-sys v0.3.64
│   │   │   │   │   │   ├── js-sys v0.3.64
│   │   │   │   │   │   │   └── wasm-bindgen v0.2.87
│   │   │   │   │   │   │       ├── cfg-if v1.0.0
│   │   │   │   │   │   │       └── wasm-bindgen-m

まとめ

  • マルチステージビルドで大幅なDocker imageサイズ削減が可能
  • opensslの問題は他のプロジェクトでも起こり得るので、覚えとくと良いかも
  • SSR用のaxumなので、FEではなくBE寄りの内容だった

参考記事

Rustに入門する〜2日目 reqwestでGET通信をする〜

File exists (add ! to override) がtsxファイルで発生する問題について

Neovimで開発を行っている際に、なぜかtsxファイルを保存する際にFile exsists (add ! to override)という問題が発生しました

問題点

tsxファイルを保存する際に毎回 w! を求められる問題が発生していて、開発効率があまりにも悪い状態となっておりました

また、定義元にジャンプを試みた際もエラーが発生することもあり、開発体験が悪いだけではなく開発自体に支障が出ておりました

前提条件

今回の問題を解決するにあたり、前提となるpluginとNeovimのversionです

  • Neovim: 0.9.1
  • nvim-treesitter/nvim-treesitter: 0.9.0
  • p00f/nvim-ts-rainbow: masterブランチから参照していたのでverは不明

解決方法

p00f/nvim-ts-rainbow をプラグインを削除することで解消できました

今回の問題が発生していた要因を詳細に調査することはできていないのですが、LSP系のログを漁っていたところ、下記の内容を確認できておりました

Error detected while processing BufReadPost Autocommands for "*":
Error executing lua callback: ...brew/Cellar/neovim/0.9.1/share/nvim/runtime/filetype.lua:21: Error executing lua: ...brew/Cellar/neovim/0.9
.1/share/nvim/runtime/filetype.lua:22: BufReadPost Autocommands for "*"..FileType Autocommands for "*": Vim(append):Error executing lua call
back: ...im/0.9.1/share/nvim/runtime/lua/vim/treesitter/query.lua:259: query: invalid structure at position 538 for language tsx
stack traceback:
        [C]: in function '_ts_parse_query'
        ...im/0.9.1/share/nvim/runtime/lua/vim/treesitter/query.lua:259: in function 'get_query'
        ...cker/start/nvim-treesitter/lua/nvim-treesitter/query.lua:108: in function 'get_query'
        ...m/site/pack/packer/start/nvim-ts-rainbow/lua/rainbow.lua:26: in function 'is_supported'
        ...er/start/nvim-treesitter/lua/nvim-treesitter/configs.lua:387: in function 'is_enabled'
        ...er/start/nvim-treesitter/lua/nvim-treesitter/configs.lua:507: in function 'attach_module'
        ...er/start/nvim-treesitter/lua/nvim-treesitter/configs.lua:532: in function 'reattach_module'

特定の問題がはっきりと記載されている状態ではありませんが、treesitterの記載が多いため、treesitterに依存しているプラグインを順番に削除した結果解決するに至りました

参考記事

なし、:LspLog コマンドから問題点を把握してプラグインを削除することで解決できました👍

Rust製YewブログをSSR化してみた

ここ数ヶ月でNext.jsで作成していたブログを思い切ってRust製のReact likeなフレームワークであるYewにリプレイスしてみました

ブログをNext.jsからwasm(Rust)に移行してみた

その際にSSR化や諸々の修正が残っており、その残作業について、Rustで実装する過程の情報を記事としてまとめてみましたので、同じような実装をしている・試みている方の参考となれば幸いです🙏

やっぱりSSR化大変だった

情報が少ない

「情報が少ない」状態で実装するのは結構大変でした

参考となる公式の情報↓

公式から提供されているコードはあるので簡単に実装できると考えていたのですが、Rustの所有権の問題がSSRする時点で発生したりと想定以上のエラーに悩まされかなりの時間を浪費してしまいました

当初は全部WASMでEdgeで動かすという方針だったため、CloudflareのWorkersで稼働させる予定でしたが、

  • 公式サンプルの情報不足
  • 私のRustの経験不足

等の理由でCloud Runに切り替えています

YewのSSRは実験的機能

SSRは機能としては提供されていますが、実験的な機能であり、まだまだ制約があります

特に大変だったポイントがWeb APIを利用することができない、という点です

こちらが公式のメッセージ

Web APIs such as web_sys are not available when your component is rendering on the server side. Your application will panic if you try to use them. You should isolate logics that need Web APIs in use_effect or use_effect_with as effects are not executed during server-side rendering.

https://yew.rs/ja/docs/next/advanced-topics/server-side-rendering

Web API利用できないということは、「DOMの操作ができない」ということです

SSR計画時点では下記のcrateを利用して、headのmetaタグを変更したり、bodyの中身を変更してSEO対策するつもりでいました

web_sysを利用すると簡単にDOM操作が行えるのでSSRモード以前はSPAのブログとしてuse_effectの中でDOM操作でSEO対策を行っていました

しかしSSRモードで実行するとエラーが大量に発生し、web_sysの利用箇所を削除、に至ります

※現時点2023/06/25時点では利用できないのであって、バージョンが変わるタイミングで利用可能になる可能性はあります

headのmetaタグはSSR時に注入する方針

SSR化の動機はSEO対策だったので、割と早期に問題にぶち当たりました

前セクションで記述の通り、Web APIが利用できないためバックエンドから取得したデータをHTMLに反映することができない状態で(できるけどheadやbodyにデータが反映される前にresponseが完了してしまう)、SEO的にベストとは言い難い実装です

さらにプラスで問題となったのが、Yewではindex.htmlの存在があり、こちらがエントリーポイントとなっています

NextやRailsのようにLayoutをテンプレート化してheadをある程度機動的に触ることができないという制約が課せられています

従って、bodyタグにバックエンドからのデータを反映させるだけでなく、headに関してもmetaタグを反映させる、ということも必要となります

公式のサンプルで具体的な方法を提示されていなかったため

  • SPAのままでweb_sysを利用する
  • SSRレンダリング時にheadにデータを注入する
  • headを諦める

という選択肢がありましたが、下記のリポジトリの実装を見て、2点目の「SSRレンダリング時にheadにデータを注入する」という方法で実現できそうだったのでこちらで実装しました

blog-romira-dev

※コードの公開と解説感謝です🙏

サーバー実装

※ここの部分はメモしていなかったため記憶で書いています。流し読みでお願いします🙏

基本的な実装は公式サンプルの通りなので特に問題なかったですが、Yewアプリで定義したAPI通信のコードをサーバーで利用することでハマってしまいました

下記のコードは修正後なので問題なく動きますが、Yewアプリで定義したinfrastructureのコードをimportして利用するとスレッド間共有に関する問題が発生してしまいました(確かスレッド間でしたが保証できません🙏)

pub async fn fetch_data_from_api(slug: &str) -> Result<PostFromApi, reqwest::Error> {
    let url = format!("https://api.masahiro.me/api/posts/{}", slug);
    let client = Client::new();
    let response = client.get(&url).send().await?;
    let body = response.json::<PostFromApi>().await?;
    Ok(body)
}

let meta_future = tokio::spawn(async move {
    let api_response = fetch_data_from_api(&slug).await;
    let post_from_api = match api_response {
        Ok(body) => body,
        Err(err) => panic!("error: {}", err),
    };
}

https://github.com/masahiro04/masahiro-me/blob/70459059e644adb6e85feef7b42b92b1cf8a05a9/ssr_server/src/bin/simple_ssr_server.rs#L45-L78

非同期を実行する際はRustはマルチスレッドで実行することを考慮するので、スレッド間でデータを参照する場合は冪等であることを求めます

従ってYewアプリのinfrastructureで定義したコードを利用する際も、データの冪等性を求められるのですが、外部の実行コードであるYewアプリの中のinfrastructureを実行する際は、それを特定すべき?のようなエラーが発生していました

個人レベルのブログで採算を気にしないで良い環境のため、綺麗にコードをまとめたかったのですが、なかなか面倒そうな問題だったため、reqwestを利用したメソッドを定義して対応することで問題を回避しました

CDN

WASMをEdgeで動かす、という制約を自分自身に設けていましたが、サーバーをWorkersで動かすのも大変そうだったのでCloud Runに載せてCloudflareのCDN経由で配信に変更しました

Cloud RunのカスタムドメインマッピングでCloudflareを使う

DX(Developer experience)

SSRを実装する際は

  • Yewアプリをbuild
  • cargo runで実行

という流れになるので、Yewの修正を行う際は必ずbuildしてからSSRのサーバーを起動する必要があります

なので、開発環境では dev用のMakefileを実行して、デプロイの際はSSR buildを利用することで開発速度を落とさないように工夫しました

※通常のSPAサイトであれば trunk serveでhot reloadがかかるので特に問題ありません

dev:
	cd app && \
	trunk serve
ssr_build:
	cd app && \
	cd ../ssr_server && \
	trunk build --release -d ./dist && \
	cp robots.txt ./dist/robots.txt && \
	cd ../ssr_server && \
	cargo build --release --features=ssr --bin simple_ssr_server --

ssr_run:
	cd ssr_server && \
	cargo run --release --features=ssr --bin simple_ssr_server -- --dir dist

Build時間長い

Rustのbuildが思ったよりも遅かったです

遅いこと自体は同僚に聞いていたので心構えしていたのですが、想像以上に遅く、特にDockerのimageを作成する際は長すぎて仕事で利用する際はかなり注意する必要があると感じました

個人用途で、デプロイ頻度は低いので問題にはなりませんが、いつか時間を作って改善してみたいです

所感

「TypeScriptやNext.jsは偉大である」と改めて感じました

今後はRustもフロント領域に殴り込みをかけていくことになるかと思いますが、ほとんどの箇所ではTypeScriptでとにかく速度を求める部分にのみWASMを適用がやはり吉だと感じます

とはいえ、「Rustでフロント開発しています!」というかなりパンチの効いた採用フレーズを使えるタイミングではあるとも思うので、戦略的には割とアリな気もしています

今後のチャレンジ

今後は

  • Firebase Authentication
  • FIrebase Firestore
  • Cloudflare Workersにリプレイス(やっぱりWASMとEdgeにこだわりたい)

なども利用できるようにチャレンジしてみようと思います🦀🚀

ブログをNext.jsからYew(Rust)に移行してみた

定期的にブログの構成や中身を変えているのですが、今回は

  • Next.js + WordPress + Vercel

から

  • Yew(Rust) + WordPress + Cloudflare pages / workers

に移行しました

インフラもごっそり入れ替えることは結構リスキーですが、

  • 個人レベルのプロジェクトであること
  • Rustという選択肢が取られ始めていること
  • Edge computingにチャレンジしてみたい

という理由があり、少しだけ攻めてみました

利用しているversionについて

Rust edition: 2021
Yew: 0.19.3
Worker: 0.0.15

Rustについて

一般的にRustは難しい言語と考えられていますが、個人的にはGoとそこまで変わらないのでは?と思いました🤔

ただし、所有権と非同期処理についてはそこそこハマったので、こちらの2点は注意が必要かと思います

既に詳細に記述されている記事がたくさんあるので、参考にしたURLを記載しています

所有権のムーブについて

所有権の移動(ムーブ)とは

所有権のライフタイムについて

RustのLifetimeってなんなん

ブログ移行後の構成図

ブログ移行後は下記の構成へと変更しました

元々記事はWordPressに記載しているので、WordPressの記事をBFF部分のworkerで整形したりしています

実装初期はCloudflare pagesに全てまとめる予定でしたが、pagesは表示するだけでコンテンツの整形など全てserverでまとめた方が後々楽そうだと思い分けています

そしてpagesはYewというReact likeなフレームワークを利用しています

実装で大変だったところ

この記事のメイン部分です
Rustでwasmを生成してCloudflare pagesでEdge computingを実現している内容はあまり見受けられなかったので、実装する上で辛かった点、悩んだ実装内容についてまとめました🙏

SSR

SSRは一応お試し的な状態ではあるのですが、wasm環境のみで実装するのはかなり難しそうなので、現状「諦めている」という状態です

Server-side Rendering

しかし、クローラが確認してくれることや、sitemap等の対策を行うことである程度はGoogleがよしなにやってくれている、とのことだったので、通常のSPAでチャレンジするという方針にしました

wasm製アプリケーションもGoogleボットはきちんとインデックスしてくれる

環境変数

wasm環境下でもdotenvぐらいいけるでしょ!と考えていたのですが、buildタイミング、実行タイミングを考慮したり、そもそも情報が足りていないという状態で、環境変数を扱うことはできていません、、、、

今回は個人ブログですし、github actionsで利用するCloudflareのAPIキー以外は、WordPressのリンクだけなので、特に問題視はしていません

pagesで/functionsを利用

pagesでは/functions ディレクトリでworkerを稼働させることができます

そしてwasm + 呼び出すjsファイルだけでも良いかも?と考えていたのですが、決まったフォーマットがあるようです

当初はRustでworkers向けのプロジェクトをwasm生成すればそのまま実行できる、と考えていましたがリリースなどを確認すると、jsからwasmを呼び出す、ということでしか利用できなさそうです

Use the language of your choice with Pages Functions via WebAssembly

// functions/api/distance-between.js

import wasmModule from "../../pkg/distance.wasm";

export async function onRequest({ request }) {
  const moduleInstance = await WebAssembly.instantiate(wasmModule);
  const distance = await moduleInstance.exports.distance_between();

  return new Response(distance);
}

.tomlファイルの環境変数対応

workersではデプロイや実行するタイミングでwrangler.tomlというファイルが必要です

可能であれば環境変数を使いたかったのですが、そもそもtomlファイル自体が対応していないとのことだったので、CIでシェルで生成するという方法を取りました

// wrangler_template.sh
#!/bin/bash

export $(cat .env)
envsubst '$ACCOUNT_ID $KV_BINDING $KV_ID $KV_PREVIEW_ID' < wrangler.toml.template > wrangler.toml
----

// wrangler.toml.template
name = "blog-content-worker"
workers_dev = true
compatibility_date = "2022-02-24"

main = "build/worker/shim.mjs"
account_id="$ACCOUNT_ID"

kv_namespaces = [
  { binding = "$KV_BINDING", id = "$KV_ID", preview_id = "$KV_PREVIEW_ID"}
]

[env.prod]
WORKERS_RS_VERSION = "0.0.9"

[build]
command = "cargo install -q worker-build && worker-build --release"

[[rules]]
globs = ["**/*.wasm"]
type = "CompiledWasm"

外部のSitemapにリダイレクトさせる方法

pagesには _redirectsというファイルをrootに配置して下記のような内容を記載することでリダイレクトを実現できる仕組みがあります

下記の実装だと、 / にアクセスすると /pages/1 にリダイレクトする、という記述です

/ /pages/1 301

この実装で、workersに作成した別のdomainにリダイレクトさせて、結果的にsitemapの内容を表示できると考えていたのですが、設定したドメイン(pagesプロジェクト内部)レベルのリダイレクトらしく、機動的に外部にアクセスを転送することはできない模様

なのでfunctionsでこちらの一般的なリバースプロキシを追加して対応しました

const handleReverseProxy: PagesFunction = async (context) => {
  const originalUrl = context.request.url;
  const url = new URL(originalUrl);
  if (url.pathname.indexOf("/sitemap") !== 0) {
    return await context.next();
  }
  const newUrl = new URL("https://api.masahiro.me/sitemap");
  const response = await fetch(new Request(newUrl));
  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: new Headers(response.headers),
  });
};

Twitter cardに値を表示する方法

今回はSPAで生成しているので、metatagに関してはページを表示した後に動的に変更しています

なのでURLをtwitterやslack等のプラットフォームで掲載しても、初期値が表示されるだけで、クリック率や流入にも影響を与えてしまいそうです

  • SSR
  • SSG
  • workersで生HTMLを生成する古典的なSSR

などの可能性を考えたのですが、そもそもuse agentで判別できるのでは?と考えこちらを実装しました

まずはfunctionsのコード

const handleBot: PagesFunction = async (context) => {
  const originalUrl = context.request.url;
  if (!originalUrl.includes("/posts/")) {
    return await context.next();
  }
  const isSocialMediaBot = (userAgent: string): boolean => {
    return ["Twitterbot", "Slackbot"].some((botUserAgent) =>
      userAgent.includes(botUserAgent)
    );
  };

  const userAgent = context.request.headers.get("user-agent") ?? "";
  if (!isSocialMediaBot(userAgent)) {
    return await context.next();
  }
  const url = new URL(context.request.url);
  const splitedUrl = url.pathname.split("/");
  const slug = splitedUrl[splitedUrl.length - 1];
  const newUrl = new URL(`https://api.masahiro.me/meta?slug=${slug}`);
  const resp = await fetch(new Request(newUrl));
  return new Response(resp.body, {
    headers: { "Content-Type": "text/html" },
  });
};

そしてこちらがHTMLを生成するworkersのコード
該当のURLにslugを投げるとheadに適切なmetatagをセットしてHTMLを生成してpagesにResponseを返す -> pagesのfunctionsレベルでresponseを生成するのでSPAにアクセスすることなく、use agentで判断ができています

pub async fn run(req: Request, env: Env) -> Result<Response> {
    let router = Router::new();
    router
        .get_async("/meta", handle_get_meta_request)
        .run(req, env)
        .await
}

pub async fn handle_get_meta_request(req: Request, ctx: RouteContext<()>) -> Result<Response> {
    // 省略
    let post = match result {
        Some(post) => post,
        None => return Response::error("Not found", 404),
    };
    let meta = render_meta(&post);
    let mut resp = Response::ok(meta).unwrap().with_cors(&cors).unwrap();
    resp.headers_mut().set("content-type", "text/html").unwrap();
    Ok(resp)
}

Twitterで表示すると意図したUIが表示されています

Sitemapを作成する方法

事前にファイルを作成するか都度生成するかで悩んだのですが、workersでKVを利用してキャッシュを生成していたので、そのデータを利用してXML形式のレスポンスを返す、という方針で実装しました

そこそこ悩んだのですが、これが一番楽な実装かと思います

pub async fn handle_get_sitemap_request(req: Request, ctx: RouteContext<()>) -> Result<Response> {
    log_request(&req);
    let slugs = fetch_slugs_usecase(&ctx.env).await.unwrap();
    let mut sitemap = String::new();
    sitemap.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
    sitemap.push_str("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");
    sitemap.push_str(
        "<url>
            <loc>https://masahiro.me/pages/1</loc>
            <priority>1.0</priority>
        </url>",
    );
    sitemap.push_str(
        "<url>
            <loc>https://masahiro.me/projects</loc>
            <priority>0.8</priority>
        </url>",
    );
    sitemap.push_str(
        "<url>
            <loc>https://masahiro.me/about</loc>
            <priority>0.8</priority>
        </url>",
    );
    for slug in slugs {
        sitemap.push_str(&format!(
            "<url>
                <loc>https://masahiro.me/posts/{}</loc>
                <priority>0.8</priority>
            </url>",
            slug
        ));
    }
    sitemap.push_str("</urlset>");
    let cors = Cors::new()
        .with_origins(vec!["*".to_string()].iter())
        .with_max_age(86400)
        .with_allowed_headers(vec!["*".to_string()])
        .with_methods(vec![Method::Get, Method::Options, Method::Head]);
    let mut res = Response::ok(sitemap).unwrap().with_cors(&cors).unwrap();
    res.headers_mut()
        .set("content-type", "application/xml")
        .unwrap();
    Ok(res)
}

wasmサイズの最適化

適切な設定を加えないとbuildサイズはそこそこ大きいです

Yew側のサイズは何もしていない状態で1.5MGで、個人的にはかなり大きい印象

次はLTO(Link Time Optimization)の設定を追加

# Cargo.toml

[profile.release]
lto = true

サイズは0.2MB小さくなりました

最後はコードサイズ最適化の設定を追加

# Cargo.toml

[profile.release]
opt-level = z

最終的には949KBまで削減することができました

ちなみにですが、ここからさらに圧縮するためにwasmの中を確認してみたのですが、50%ほどはもしかしたら削減できそうな気も?という内容でした

cargo install twiggy
twiggy top -n 10 dist/masahiro-89414b795cb07fda_bg.wasm
 Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────
        256994 ┊    26.14% ┊ data[0]
        238288 ┊    24.24% ┊ "function names" subsection
         15614 ┊     1.59% ┊ http::header::name::parse_hdr::hc3914b1af2df09b6
         12390 ┊     1.26% ┊ <yew::html::component::lifecycle::RenderRunner<COMP> as yew::scheduler::Runnable>::run::h0f98a0286f25da9c
         11863 ┊     1.21% ┊ url::host::Host::parse::hd93938c25f0b0e37
          9828 ┊     1.00% ┊ reqwest::wasm::request::RequestBuilder::send::{{closure}}::hf898947247698921
          8493 ┊     0.86% ┊ core::fmt::float::float_to_decimal_common_shortest::h104ccad32a6d10e2
          8139 ┊     0.83% ┊ <yew::html::component::lifecycle::RenderRunner<COMP> as yew::scheduler::Runnable>::run::h84df7697082f83b4
          6979 ┊     0.71% ┊ core::fmt::float::float_to_decimal_common_exact::h37c3cd7e1a60c62a
          6976 ┊     0.71% ┊ <yew::virtual_dom::vnode::VNode as yew::virtual_dom::VDiff>::apply::he2bab8f4b493960d
        407650 ┊    41.46% ┊ ... and 2259 more.
        983214 ┊   100.00% ┊ Σ [2269 Total Rows]

Rustのみで実装することの難しさ

当初はマークアップ以外はRustだけで実装する予定でした

しかし/functionsディレクトリを利用しようとすると、どうしてもTypeScriptを記述する必要があり、一部TypeScriptを利用してしまっています。。。

作業を終えて考えること

Rustを開発で使うべき?

正直使っても良いと考えてます
コンパイラが非常に強力なので物凄く安全に開発することができるので、フロントでもバックエンドでも問題ないかと思います

ただし、認証系のcrateは希薄なので自前で実装する必要がほとんどかと
その場合認証系のリスクを背負うことになるので、そこだけが心配です

ランタイムエラーは起こせる

Rustのメリットとしてランタイムエラー排除、があるかと思います
正直どの言語でも同じことだと思いますが、エンジニアが横着すると普通にエラーを起こせてしまうので丁寧な実装が必要です

// 場合によってはエラー起きる
let kv_data = kv_client.get(&cache_key).await.unwrap();

// エラーの場合もハンドリングしてる
let kv_data = kv_client.get(&cache_key).await;
let data = match kv_data {
  Ok(data) => data,
  Err(e) => { // do something }
};

NimやGoと比べると

所有権があるおかげで非常に使いやすく、個人的には3つのうちでダントツでRustが使いやすいと思います

Nimを時々触っている + 流行ってほしいという気持ちがあるのでNimをおすすめしたいところではありますが、エコシステムや言語のコンパイラの強力さを考えると、ビジネス価値的にRustはやはり良い選択肢だと考えています

TODO(残り作業)

  • wasmで環境変数を参照
  • pagesの/functions ディレクトリでwasmを直接実行(継続的に方法を模索したい)
  • DDDのコードとして完成させたい
  • テスト
  • SSR or SSGの可能性の模索
  • 必要なcrateを自作 or OSSにコミット
  • wasmサイズの圧縮

Rustブログのリポジトリ

https://github.com/masahiro04/masahiro-me

2022の振り返りと2023の抱負

みなさま新年明けましておめでとうございます🎍

昨年は短いような長いような1年でしたが、進みたい方向へ順調に歩みを進めることができ、非常に実りある時間を過ごせました

お客様、参加しているチームの皆様、大変お世話になりました🙇‍♀️

昨年の振り返り

シードからシリーズBへ

長らくシード期のプロダクト開発を行なっていましたが、やはり専門性を身につけたいと考え、シリーズB以降の企業とお付き合いをさせていただく、という決断をしました

開発スタイルの大幅な変更

今までは

  • 最速の開発
  • リリース
  • 仮説検証

ということを行なっていたので、早く作れること、が自分の一番の武器となっておりました

しかしシステムが成長する過程で早く開発して効果検証するよりも、

  • 技術負債を減らす
  • 継続的な開発ができること
  • 個人ではなくチーム戦
  • 命名やアークテクチャの重要性

などが頻繁に議論され、自分が意識していた早く作れることは既に価値がかなり薄れていること、という実感がありました

なので、今まで慣れ親しんだ開発スタイルの一切を捨てて、大規模プロダクト開発を行うためのスタイルへと大きく変更しました

※以前のようなスピードがなく驚いている方もいらっしゃるかもしれませんが、明確な方針があってのことです🙏

本年の抱負

今年は下記4点を重点的に業務でも個人でも積極的に学び、来年以降に活かしていきたいと考えています

  • Edge computing
  • Infrastructure
  • Firebase( NoSQL )
  • 高稼動性担保

現時点では全て中途半端なので、「フロント×インフラ」と行ったら大久保、と名前を呼んでいただけるようになりたいです

経験も能力も足りていない要素が多いですが、昨年に続き3年間は勉強期間と捉えておりますので、温かい目で見守っていただけますと幸いです

本年はお客様、チームの皆様と大きく成長することができる1年を実現できるように精一杯努力しますので、本年もどうぞよろしくお願い申し上げます🎍🎍🎍🎍🎍

Vimプラグイン自作の方法

基本的にはGithubを漁って自分に合ったプラグインを探して利用する、という方針だったのですが、どうしても実現できない機能がいくつかあり、新しいチャレンジを行うという試みでプラグインの自作に踏み出してみました

難しいことはやらずに簡易的な機能を提供するだけのプラグインをとりあえず作ってみます🦀

事前準備

現在NeoVimとそれなりに多いプラグインで構成されているので、最小構成のVim環境を新しいブランチを作成しましょう

特にやる必要はないかもですが、今後作成していく自作プラグインは一応Githubで公開予定なので可能な限り依存を少なくする、という意味合いで準備します

※deinやmotion移動等だけ残してそれ以外は消しました

超最小構成*.vimの実行

最小構成のvimに変更したら、任意のdirに sample.vim というファイルを作成します

[masahirookubo@MacBook-Pro-2 (arm64):~/dotfiles][feature/plugins]
% touch sample.vim

そしてprint文を記載して実行

echo "hogehgoe!!!"

:source sample.vim

動きました!!

最小構成ではこちらでいけますので、一旦これで完了とします

超最小構成のvimプラグインの実装

最小構成のプログラムが稼働することを検証できたので、次はもう少しプログラムを追加してそれをremoteで公開してみます

まずはpluginを作っていくためのdirなどの準備コマンドの実行

# 任意のdirへ移動してから実行
mkdir sample_plugin
mkdir plugin
touch plugin/sample_plugin.vim
vi plugin/sample_plugin.vim

作成したら、下記のプログラムを追加

function! sample_plugin#SamplePlugin()
  echo 'It works!'
endfunction

command! SamplePlugin call sample_plugin#SamplePlugin()

プログラムの追加が完了したら、ファイルを読み込ませて実行してみます

:source sample_plugin.vim
:SamplePlugin

問題なく読み込めています👍

GIthubから利用してみる

localでの稼働も検証できたので、Githubに上げてdeinで追加して検証を行います

※パッケージマネージャはお使いのものを利用してください

[[plugins]]
repo = 'masahiro04/sample_plugin'

追加した後はvimを起動して下記を実行・検証してみます

「sam」と打ち込むと候補が出力され

エンターを押すと、実装したプラグインが表示されました!

もうちょっと難しいと思ってたんですけど、思いのほか簡単に実装できたので、自分が作りたいプラグインをこれから作ってみようと思います!!!

参考記事

はじめてのvimscriptとvimプラグインの作成

簡単なVimプラグインを作って基礎を学ぶ

[dein] Vim(lua):E5107: Error loading lua [string “:lua”]:3: ‘=’ expected near ‘nnoremap’

NeoVimでLuaを導入したところタイトルのエラーが発生しました

どうやらこのエラーは、luaの前方にスペースやタブが入っていてもエラーが出るようなので、

[[plugins]]
repo = 'nvim-telescope/telescope.nvim'
depends = ['plenary.nvim']
hook_add = '''
  lua << EOF
    require('telescope').setup()
  EOF
  nnoremap <leader>ff <cmd>lua require('telescope.builtin').find_files()<cr>
'''

こちらに修正したところ解決できました!

[[plugins]]
repo = 'nvim-telescope/telescope.nvim'
depends = ['plenary.nvim']
hook_add = '''
lua << EOF
  require('telescope').setup()
EOF
  nnoremap <leader>ff <cmd>lua require('telescope.builtin').find_files()<cr>
'''

参考記事

Unable to use lua inside a `.vim` file

Flutterにcloud_firestore導入したらAMSupportURLConnectionDelegate is implemented in bothが出た

FlutterにFirebase追加したら下記のエラーが発生しました

※一部だけ掲載予定でしたが、思いのほかハマったので後続の方のために全て記載しています

バージョン情報

flutter: 3.0.5
Dart: 2.18 ?ぐらい、ここあんま関係ない
Mac: M1 MAX
Changing current working directory to: /Users/masahirookubo/me/flutter/firestore_test
Launching lib/main.dart on iPad Pro (12.9-inch) (5th generation) in debug mode...
Running pod install...                                             16.4s
Running Xcode build...
Xcode build done.                                           54.7s
Failed to build iOS app
Error output from Xcode build:
↳
    objc[54809]: Class AMSupportURLConnectionDelegate is implemented in both /usr/lib/libauthinstall.dylib (0x1ddd5eb90) and /Library/Apple/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/MobileDevice (0x103b382c8). One of the two will be used. Which one is undefined.
    objc[54809]: Class AMSupportURLSession is implemented in both /usr/lib/libauthinstall.dylib (0x1ddd5ebe0) and /Library/Apple/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/MobileDevice (0x103b38318). One of the two will be used. Which one is undefined.
    ** BUILD FAILED **


Xcode's output:
↳
    Writing result bundle at path:
        /var/folders/bb/n3dq0s2963v_4c8dw8ygjlgm0000gn/T/flutter_tools.IPaDUI/flutter_ios_build_temp_dirGu70oL/temporary_xcresult_bundle

    While building module 'absl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:
    In file included from <module-includes>:1:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/abseil-umbrella.h:13:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/algorithm/algorithm.h:29:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:52:10: error: include of non-modular header inside framework module 'absl.base.config': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h' [-Werror,-Wnon-modular-include-in-framework-module]
    #include <limits.h>
             ^
    While building module 'absl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:
    In file included from <module-includes>:1:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/abseil-umbrella.h:13:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/algorithm/algorithm.h:29:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:67:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/policy_checks.h:28:10: error: include of non-modular header inside framework module 'absl.base.policy_checks': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h' [-Werror,-Wnon-modular-include-in-framework-module]
    #include <limits.h>
             ^
    While building module 'absl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:
    In file included from <module-includes>:1:
    In file included from /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/abseil-umbrella.h:53:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/internal/spinlock_linux.inc:17:10: fatal error: 'linux/futex.h' file not found
    #include <linux/futex.h>
             ^~~~~~~~~~~~~~~
    3 errors generated.
    /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:21:10: fatal error: could not build module 'absl'
    #include "absl/strings/str_join.h"
     ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
    While building module 'openssl' imported from /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/tsi/ssl_transport_security.h:25:
    In file included from <module-includes>:1:
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'siphash.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'x509_vfy.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'hpke.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'e_os2.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'blake2.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'hrss.h' [-Wincomplete-umbrella]
    /Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework/Headers/umbrella.h:39:1: warning: umbrella header for module 'openssl' does not include header 'trust_token.h' [-Wincomplete-umbrella]
    7 warnings generated.
    7 warnings and 4 errors generated.
    note: Using new build system
    note: Planning
    note: Build preparation complete
    note: Building targets in dependency order
    /Users/masahirookubo/me/flutter/firestore_test/ios/Pods/Pods.xcodeproj: warning: The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 15.2.99. (in target 'leveldb-library' from project 'Pods')

    Result bundle written to path:
        /var/folders/bb/n3dq0s2963v_4c8dw8ygjlgm0000gn/T/flutter_tools.IPaDUI/flutter_ios_build_temp_dirGu70oL/temporary_xcresult_bundle


Lexical or Preprocessor Issue (Xcode): Include of non-modular header inside framework module 'absl.base.config': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h'
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:51:9

Lexical or Preprocessor Issue (Xcode): Include of non-modular header inside framework module 'absl.base.policy_checks': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h'
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/policy_checks.h:27:9

Lexical or Preprocessor Issue (Xcode): 'linux/futex.h' file not found
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/internal/spinlock_linux.inc:16:9

Parse Issue (Xcode): Could not build module 'absl'
/Users/masahirookubo/me/flutter/firestore_test/ios/Pods/gRPC-Core/src/core/ext/xds/xds_server_config_fetcher.cc:20:9

Could not build the application for the simulator.
Error launching application on iPad Pro (12.9-inch) (5th generation).

[masahirookubo@MacBook-Pro-2 (arm64):~/me/flutter/firestore_test/ios]!+[master]
%

結論

XCodeを再インストールしました

下記のようなコマンドも実行しましたが特に変わらず

cd ios
rm -rf ~/Library/Caches/CocoaPods
rm -rf Podfile.lock
rm -rf Pods
rm -rf ~/Library/Developer/Xcode/DerivedData/*

下記の手順でXCodeを削除、そしてinstallで解決できました

sudo rm -rf /Applications/Xcode.app
sudo rm -rf /Library/Preferences/com.apple.dt.Xcode.plist
sudo rm -rf ~/Library/Preferences/com.apple.dt.Xcode.plist
sudo rm -rf ~/Library/Caches/com.apple.dt.Xcode
sudo rm -rf ~/Library/Application Support/Xcode
sudo rm -rf ~/Library/Developer/Xcode
sudo rm -rf ~/Library/Developer/CoreSimulator

// こちらは念の為実行、理由は後述
sudo rm -rf /Library/Developer/CommandLineTools

問題の原因

再インストール直前で、2点が目につきました

 One of the two will be used. Which one is undefined

Lexical or Preprocessor Issue (Xcode): Include of non-modular header inside framework module 'absl.base.config': '/Library/Developer/Toolchains/swift-5.6.2-RELEASE.xctoolchain/usr/bin/../include/c++/v1/limits.h'
/Users/masahirookubo/me/flutter/firestore_test/build/ios/Debug-iphonesimulator/abseil/absl.framework/Headers/base/config.h:51:9

1行目のエラー情報は割とあるあるだと思うのですが、下の方の情報にはswift-5.6.2がToolchainsから参照されています

ここは思いっきり心当たりがあって、1ヶ月ほど前にvimでswift開発をするためにToolchainsやswiftバージョンをを変えるために、必要なコードをダウンロードしてきたものです

途中までは流石に問題を引き起こすことはないだろう、ぐらいに考えていたのですが、

  • Flutterアプリをcreate
  • flirebaseを導入
  • cloudstore導入で急にエラー

はあまりにもおかし過ぎるし、もしこれが普遍的な問題であれば社会的な問題にもつながると思います

※大きなインフラなので冗談抜きで本当に問題として取り上げられ、twitterなどですぐに情報が回るはず

なので一度削除する、という選択に踏み切りました

注意点

再インストール自体は簡単な選択ですが、

  • 時間かかる(2, 3時間ぐらいかかった。通信状況などにも大きく依存するかと)
  • アカウント登録情報も削除される(Apple IDでログインすれば良いだけだけど、手元に情報を用意しておく必要あり)
  • もしかしたら、重要なファイルや情報なども削除されるかも
  • Toolchainsを削除するとgitなど関連するものを全て削除されるので、削除前によく検討すること

が必要です

自分の場合は基本的にDockerで環境を整えている、かつSwift、Dartでの開発しか行っていないのですぐに踏み切れましたが、複数の案件に関わっている場合や、時間がない場合は特に注意が必要です

まとめ

おそらく上記の順番で実行すれば、cloudfireだけでなくほとんどの問題解決の最終手段になり得ると思います

調べてもダメな場合は、寝る前に再インストールして朝実行ということも可能かと思うので、困っている方は最後の手段としておすすめです🙏

参考記事

Xcodeの再インストール

Supabase dbでカスタムなqueryを発行する方法

SQL Editorでカスタムなqueryの登録試みていたのですが、エラーが発生し結構詰まってしまったので記事にしてみました

結論

create or replaceで登録ではなく、posgreのviewを利用することで対応することで解決しました

こちらがそれぞれのコード


-- 動かない
create or replace function get_answers() 
returns table (
  id int,
  body text,
  topic_id int,
  reply_to int,
  created_at timestamp,
  count int
)
language sql
as $$
  select *, (select count(*) from answers a2 where a1.id = a2.reply_to) from answers a1;
$$;

-- 動く!
CREATE VIEW public.hogehuga AS
    select *, (select count(*) from answers a2 where a1.id = a2.reply_to) from answers a1;

試したこと

まずは動かないコードですが、公式docや技術ブログ系では機能するといった情報がありましたが、私が実行したところ、id単体(別カラムでもOK)では問題なく機能するのですが、

※動くコード


create or replace function get_answers() 
returns table (
  id int
)
language sql
as $$
  select id from answers;
$$;

2つ以上のカラムをreturnしようとしたら下記のエラーが発生しました
※最初に提示したコードを実行

Failed to run sql query: cannot change return type of existing function

エラーを見る限り、既存のfunctionのreturn値を変更できない、とのことですが、そもそもidやtopic_idなど単体では実行が可能だったので、複数選択することで問題が発生するものと思われます

※データ型が正確ではない、という可能性もあります→つまり上記の私のコードが間違い

PostgreSQLのView機能について

数時間粘っても問題解決できなかったので、下記の参考記事で紹介されている、posgreのviewという機能を利用することにしました

view is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user.

https://supabase.com/blog/2020/11/18/postgresql-views

簡単に説明すると、特定のqueryを作成して、それに命名した上で実行できるショートカットを作成する、ということです

なので、上記の実行したいコマンドを下記のように設定して実行することで、求める結果を獲得することが可能となります

CREATE VIEW public.hogehuga AS
    select *, (select count(*) from answers a2 where a1.id = a2.reply_to) from answers a1;

こちらが呼び出し元のコード

final answersRequest = client.from('hogehuga').select('*').execute();

取得できました!

参考記事

Supabaseのデータベースを使うときに役立つ情報

Postgres Views

NeoVim + COCのhover UIが崩れる問題の解消

iOSのビルドやUI以外は原則vimで開発を行なっているのですが、COCのホバーで情報を確認する際にUIが崩れていて長い間じわじわと精神的苦痛が伴っていたのですが解決できたので情報を共有します

結論下記をコメントアウトしました

" NG
set ambiwidth=double

" OK
" set ambiwidth=double

before

after, 綺麗!!!感激です😭😭

今までは定義元へジャンプしたりして対処していたのですが、本来は上記画像のように丁寧な見出し分的な情報があるみたいです😳

そもそも何故ambiwidthをセットしていたかというと、「vimで全角文字を表示するため」という理由でした

正直利用ケースは皆無なのですが、何故か入っていた、、、、その結果非常に大きな苦痛を強いられていたので、これからは定期的にvimrcを育てていこう、と思える痛い経験でした🙇‍♂️

参考記事

[Vim] 文字崩れ, 文字が残ってしまうのを防ぐ