*iroi*

mechairoi の Blog

Cloudflare SQLite in Durable Objects の可能性

Cloudflareのバースデーウィークで発表されたSQLite in Durable Objectsが面白そうだったので少し調べてみました。D1の裏で使われていた機能が一般のユーザーでも使えるようになったようです。

blog.cloudflare.com

概要

Zero-latency SQLite storage in every Durable ObjectBuild a seat booking app with SQLite in Durable Objects | Cloudflare Durable Objects docs を読むとより詳細な情報が得られますが、大まかには以下のようになります

  • DurableObject クラスを継承したクラスを定義して登録すると、そのクラスの中のコードはローカルマシンにストレージを持った別のV8 isolateで動く。
  • メソッドを呼び出すと、裏でそのDurable Object(DO)にRPC*1
  • ストレージはリージョン内の複数のデータセンターで冗長化されている。
  • ストレージのインタフェースがKVストアだったのがSQLiteになりました。

良さそうなところ

SQLite と同じスレッドでユーザーコードが動く

Zero-latency SQLite storage in every Durable Objectにも書かれていますね。 N+1だけでなく、ツリーのような再帰的な構造を扱ったり、GraphQLのAPIを作るにも使えるかもしれません。 バックアップや冗長化などがマネージドなSQLiteで、ユーザーコードが同じマシンで動かせるのは大きな魅力です。

トランザクションが使える

D1ではトランザクションが使えませんでしたが、Cloudflareの開発者によるとDOsでは this.ctx.storage.transactionSyncトランザクションを使えるようです*2(BEGINは利用不可)。 試した限りではINSERT OR ROLLBACKのような文は使えなくて、 INSERT (OR ABORT) を使ってunique 制約に違反するなど、JavaScript 側の例外でロールバックできるようです。トランザクション内でawaitもできませんが、十分に実用できそうです。

以前はストアドプロシージャーを使ってトランザクションを実現するといった話もありましたが*3SQLite in DOsの方が柔軟でよさそうですね。

多数のデータベースを扱える

D1は1つのbindingが1つのデータベースで、1つのWorkersには5000個くらいしかbindingできませんでした*4DOsは1つのbindingが1つのネームスペースで、ネームスペースの中にはいくつでもDO(=データベース)を作れます。 それぞれのDOが別のV8 isolateになるので、適切にパーティショニングできれば書き込みもスケールしそうです。

活かせそうな応用例としては

  • モバイルアプリ用のユーザーデータを保存する。シングルプレイ寄りのゲームとか、ユーザーごとの閲覧履歴をサーバーと同期するとか。
  • SNSのようなタイムラインをユーザーごとにDOを用意して、GAしたWorkers Queuesでfan outするのも面白そうです。

地理的にユーザーの近くにDOが作られそうなのもいいですね。

また読み込み性能のスケールのために read replica? のような機能の開発も進行中のようです*5

1行のサイズ制限がD1の2倍

ドキュメントのミスでなければD1は1MBですがDOsは2MB *6*7

おわり

機会があれば使ってみたいですね。Drizzle ORMは割と早く対応してくれそうな気がしてます。

Material Color Utilitiesの9つのDynamic Scheme

github.com

Material Color UtilitiesはMaterial 3のカラーシステムに関するアルゴリズムやユーティリティを含むライブラリです。 HCT色空間や 画像から代表色を抽出するquantizeなど様々なコンポーネントを含んでいます。最近リリースされたv0.3.0 *1で非推奨になったSchemeの代替として用意されているDynamic Schemeが面白かったので紹介します。

Dynamic Schemeはコンテンツから抽出した色や指定した色、コントラストレベル、variantなどのいくつかのパラメータからのカラースキーマを生成する仕組みです*2。 ライブラリには9つのvariantが用意されていて、例えばAndroidのMaterial Youのデフォルトと同じにするならSchemeTonalSpotというvariantを使います。詳しくはDynamic Schemeのドキュメントを参照。

それぞれのパラメータがどのような影響を与えるのか理解しやすいように、インタラクティブにパラメータを変更してカラースキーマを確認できるようにしてみました。

https://codepen.io/mechairoi/full/mdodrJj

シードカラーやコントラストレベルなどのパラメータを固定して、variantを切り替えると下のようになります。

グレースケールに近いものもあったりして面白いですね。最後に各variantの説明をソースコードから引用します。

SchemeContent

A scheme that places the source color in Scheme.primaryContainer.

Primary Container is the source color, adjusted for color relativity. It maintains constant appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in dark mode. Tertiary Container is the complement to the source color, using TemperatureCache. It also maintains constant appearance.

SchemeExpressive

A Dynamic Color theme that is intentionally detached from the source color.

SchemeFidelity

A scheme that places the source color in Scheme.primaryContainer.

Primary Container is the source color, adjusted for color relativity. It maintains constant appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in dark mode. Tertiary Container is the complement to the source color, using TemperatureCache. It also maintains constant appearance.

SchemeFruitSalad

A playful theme - the source color's hue does not appear in the theme.

SchemeMonochrome

A Dynamic Color theme that is grayscale.

SchemeNeutral

A Dynamic Color theme that is near grayscale.

SchemeRainbow

A playful theme - the source color's hue does not appear in the theme.

SchemeTonalSpot

A Dynamic Color theme with low to medium colorfulness and a Tertiary TonalPalette with a hue related to the source color.

The default Material You theme on Android 12 and 13.

SchemeVibrant

A Dynamic Color theme that maxes out colorfulness at each position in the Primary Tonal Palette.

*1:TypeScript 版のバージョン

*2: variant を使わず複数の色を指定する方法もあります

go-readabilityをwasmにしてCloudflare Workersで本文抽出する

先日Cloudflare Workersでウェブページの本文を抽出したくなったことがありました。本文抽出といえば、@mozilla/readabilityが使えそうです。しかし依存に含まれるnwsapiにはFunctionコンストラクタが多用されており*1、Cloudflare Workersでは動作しません。これを修正するのも大変そうです。

そこで、TinyGoを使用してgo-readabilityをwasmにコンパイルしてみることにしました。

TinyGo のドキュメントUsing WASM | TinyGo のままではCloudflare Workersでは動かなかったので、TinyGoに含まれるwasm_exec.jsを少し修正したり*2、 import側もwasmファイルをfetchしているのを変更したり*3すると無事動きました。

動作するコードは https://github.com/mechairoi/cloudflare-go-readability-demo

Cloudflareにデプロイするか、ローカルでnpm run devを実行してから、/?url=https://example.com/ のようにアクセスすると、本文がテキストで返ってくるはずです。ビルドサイズは圧縮後約850kBで、Cloudflare Workersのフリープランは1MBなのでセーフですね。golangのwasmはRustと比べるとJavaScriptからの利用に少し癖があるように感じましたが、ちゃんと動いてくれて便利ですね。

おまけ

masukomi/arc90-readability: a copy of the original arc90 repo with links to many of the current ports.によるとReadabilityライブラリには多くの実装があるようです。実は最初はRustで書かれたkumabook/readabilityをwasmにコンパイルして使っていました。 しかし、試しているといくつかのページうまく抽出できないケースがあったり、またmozilla/readabilityをrustで書き直すにkumabook/readabilityの実装はMozilla版と比べると古いという記述もあったので、今回はgo-readabilityを使用することにしました。

PlaywrightのレポートをCloudflare PagesにデプロイしてGitHub IdPでアクセス制限する

導入

PlaywrightやReg SuitなどのVRT(Visual Regression Testing)の結果は、HTMLで出力されます。Playwright のヘルプ1 でもレポートをダウンロードして確認する方法が記載されていますが毎回行うのは面倒です。また、レポートをGitHub Pagesにデプロイする方法もありますが、アクセス制限にはEnterpriseプランが必要だったり、Pull Requestごとの結果を保存するのに手間がかかるなどの問題があります 2 3Amazon S3にレポートを置く方法もアクセス制限が面倒なことが知られています。

そこでこのエントリでは、レポートをCloudflare Pagesにデプロイする方法を紹介します。Cloudflare PagesにはPreview deployments4があるため、Pull Requestごとのレポートをデプロイできます。また、Cloudflare Zero Trustの機能を使えば、アクセス制限も容易です。リポジトリの閲覧と同様のアクセス制限をかけるのが多くの場合で合理的と思われるので、GitHubのOrganizationやTeamでアクセス制限するのがおすすめです。

設定方法

まず、Zero TrustのIdentity ProviderとしてGitHubを追加します。手順はGitHub - IdP Integration · Cloudflare Zero Trust docsに従ってください。

次に、Cloudflare Pagesを作成します。「Upload Assets」を選び、名前を入力して「Create Project」をクリックします。Preview deploymentsしか使用しないので、アップロードは不要です。

Access Policyの設定

Pagesの「Manage」>「Access policy」から「Enable access policy」ボタンを押すと、Cloudflare AccessのApplicationが作成されます。「Manage Policies」を押して Cloudflare Access の Application 一覧へ

alt

Applicationを選び、「Edit」から「Authentication」タブでIdPをGitHubに設定し、保存します。

「Policies」タブでPolicyの「Edit」から、「Configure Rules」で「GitHub Organization」を選びOrganization(とTeam)を指定します。

GitHub Actionsでデプロイ

GitHub Actionsでcloudflare/pages-actionアクションを使ってPagesにデプロイします。APIトークンの作り方などは cloudflare/pages-action を参考にしてください。

    steps:
      ...
      - name: Publish test report to Cloudflare Pages
        id: publish-report
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: YOUR_ACCOUNT_ID
          projectName: YOUR_PROJECT_NAME
          directory: ./playwright-report/
          workingDirectory: .

レポートのURLを commit status に入れておくと便利です。

    permissions:
      statuses: "write"

    steps:
      ...
      - name: Create commit status
        uses: actions/github-script@v7
        env:
          TEST_STATE: ${{ steps.run-test.outcome }} # error, failure, pending, success のいずれか
          TARGET_URL: ${{ steps.publish-report.outputs.url }}
          SHA: ${{ github.event.pull_request.head.sha || github.event.after }}
        with:
          script: |
            github.rest.repos.createCommitStatus({
              state: process.env.TEST_STATE,
              context: 'playwright',
              description: 'playwright test results',
              owner: context.repo.owner,
              repo: context.repo.repo,
              sha: process.env.SHA,
              target_url: process.env.TARGET_URL,
            });

さらに Cloudflare Pagesの{name}.pages.devドメインに自分だけアクセスできるようにアクセス制限をつける手順 | Web Scratch のようにPreview用でないドメインもアクセス制限しておくと安心です。

まとめ

閲覧する時の認証が少し遅い気がしますがダウンロードしなくてよいのは便利です。GitHub Actions 以外のコストはほとんど無料である点も嬉しいですね。

iOS Safari でキーボード表示時にフッターを固定するもう1つの方法

zenn.dev

エディタを作っていると iOS Safari でキーボードを開いたときの挙動は本当に大変ですよね。 この記事では、キーボードを開いたままスクロールしてもフッターを固定できるようなワークアラウンドを最近見つけたので紹介します。 Safari 16 以降が必要。キーボード開閉時に追従が遅れる挙動は改善しません。

デモは https://mechairoi.github.io/ios-safari-virtual-keyboard/iPhoneiOS Simulator でお試しください。

GitHub - mechairoi/ios-safari-virtual-keyboard

Virtual Keyboard の挙動

Virtual Keyboard を開いたときには以下のようなことが起こっていると推測しています。

  • Visual_Viewport_API で取得できる Visual Viewport のサイズがキーボードを除いた領域のサイズまで小さくなります。
  • レイアウトの計算をするときにはキーボード表示前の Visual Viewport が使われます。vh 単位や fixed, sticky 要素の位置などは表示前と変わりません。
  • スクロールしたときに、キーボード表示前の Visual Viewport のうち画面に表示されている位置が変わることがあり、fixed 要素が動いて見えます。
    • Layout Viewport の上に大小二つの Visual Viewport が重なっていてそこから見ている感じです、ややこしいですね。
  • Layout Viewport の下?に謎の余白が追加されます。下にスクロールし続けると余白が表示されます。
  • キーボードと重なる位置にある入力要素をタップしてキーボードを開いたとき、キーボードに隠れないようにするためかスクロールが発生します。
    • また、キーボードを開いている状態で他の入力要素等にフォーカスが移動したときにもスクロールが発生します。

ワークアラウンド

まず画面全体にスクロール可能な要素を表示します。さらにSafari 16 から使えるようになった overscroll-behavior: contain をすることでスワイプやフリック操作による window のスクロールを防ぎます。window がスクロールしなければ固定した要素が動いて見えることはありません。

#root {
  overflow-y: scroll;
  overscroll-behavior: contain;
  width: 100svw;
  height: 100svh;
}

キーボード開閉時などどうしてもスクロールが発生してしまうタイミングでは JavaScript で計算して要素を移動したりするのはこれまでの方法と変わらないです。

ダイアログなど他の要素を画面に固定したいときは position:sticky を使うか、position:fixedposition:absolute で固定した要素の内側もスクロール可能にしてoverscroll-behavior: contain します。overscroll-behavior: contain しないと固定した要素上でスワイプしたときにwindow がスクロールしてしまいます。また選択範囲のすぐ隣にツールバー出すときはコンテンツに対して固定できれば良いので position:relativeposition:absolute が使えます。

他にも以下のような工夫をしてます。

  • コンテンツが少ないときも overscroll-behavior を効かせるために min-height を指定して微妙にスクロールできるようにします。
  • overscroll-behavior-y:contain があると Safari では pull to refresh できなくなってしまいます。キーボードを閉じているときは pull to refresh できるように overscroll-behavior-y:contain はキーボードが開いている間だけ有効にします。
  • maximum-scale=1ツールバーを非表示機能を使ったタイミングで微妙に拡大されてしまうのを防ぎます。指定していても最近のブラウザではピンチアウトで拡大できると思います…
  • キーボードを開いた時にカーソルがヘッダやフッタの下に隠れてしまうことがあるので調整が必要ですが、少し頑張ればなんとかなるでしょう。

この方法でも実現できるUIに制限があり現実的にはある程度妥協することになりそうです。Safariinteractive-widget=resizes-content に対応してくれるとかなり解決する気がするので、なんとかお願いします。

参考文献

SQLiteでLinderaを使った日本語全文検索

これは はてなエンジニアアドベントカレンダー2023 3日目の記事です。

昨日は id:pokutuna さんの

blog.pokutuna.com

でした。私も若い頃に同僚とGitHub上で白熱してしまい観光名所になってしまっていたような気がします。気を付けていきましょう。


さて、この記事では SQLiteでLinderaを使った日本語全文検索をする話を紹介します。

モチベーション

laiso.hatenablog.com

上の記事でも話題になっているように個人開発ではDBのコストは問題です。同様に全文検索したいときにもコストに頭を悩ませているのではないでしょうか?

たとえば Amazon OpenSearch Service は t2.micro でも東京リージョンでは $20.44 /月/台 から 、冗長化には最低で3台必要です。他にもマネージドなDBで全文検索するにもプラグインや拡張が自由に入れられず日本語の検索に向いたトークナイザが使えなかったりと、安価で楽に運用できる方法はあまりありません。

そこで fly.io の LiteFS Cloud なら10GBまでは $5/月/組織です。fly.io の請求は月 $5未満? *1*2 は $0 になるようなので実質無料(?)*3で冗長構成です 。あとはいい感じに全文検索ができるようになれば解決です。Litestream も良さそうですね。

SQLite のカスタムトークナイザ

SQLite には SQLite FTS5 Extension という全文検索用の Extension があり、これを使うとSQL全文検索できます。

CREATE VIRTUAL TABLE email USING fts5(sender, title, body);
SELECT * FROM email WHERE email MATCH ? ORDER BY bm25(fts);

デフォルトではあまり日本語の検索に向いたトークナイザはありませんがカスタムトークナイザを load して使うことができます。

既存の日本語向けのカスタムトークナイザは手元でうまく動かせなかったりしたので、signalapp/Signal-FTS5-Extension を fork してみました。オリジナルでは Unicode Text Segmentation を使ってますが、日本語を検索したいので lindera-morphology/lindera を使うように書き換えていきます。Lindera は Meilisearch などでも使われている Rust で書かれた形態素解析ライブラリです。動詞の活用や漢数字の正規化もやってくれます。

できたら .load してテーブル作成時にtokenizerを指定して使います

.load "./lib/libsignal_tokenizer" "signal_fts5_tokenizer_init"
CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize="signal_tokenizer");

signalapp/Signal-FTS5-Extension のライセンスは AGPL 3.0 なので忘れず公開しましょう。

https://github.com/mechairoi/Signal-FTS5-Extension

デモ

試しに簡単なWebアプリケーションを書いて、fly.ioの一番小さいマシン(shared 1 cpu, 256MB)にデプロイしてみます。

https://wispy-wildflower-1272.fly.dev/

アクセスが無いときは0台にスケールする設定にしているのでコールドスタートは遅いです。 辞書は lindera-ipadic、データセットは日本語 Wikipedia のサブセット(134万件、335MB) を使っていて、DBのファイルサイズは1021 MBになりました。 アプリケーションのソースコードはこちら

https://github.com/mechairoi/litefs-fts-demo

マッチする記事数が多くなるクエリでは少し遅くなりますが、インクリメンタルサーチでも違和感なく使える速度が出ていると思います。


この記事は はてなエンジニア Advent Calendar 2023 3日目の記事でした。 次は id:mizdra さんです。楽しみですね。

*1:https://community.fly.io/t/questions-on-100-billing-discount/11133

*2:先月はダッシュボードでは $5.06 でしたが 100% Discount でした

*3:無料で使うにはインスタンスのストレージがトータル3GBまで、詳しくは https://fly.io/docs/about/pricing/

ghq/fzf で選んだリポジトリに対応する tmux の session を作ったり探したりするスクリプト

gist41201c4579d17253b7bd26e699c6dccb

  1. リポジトリghq | fzf で選ぶ
  2. リポジトリに対応する session が tmux になければ作る
  3. 対応する session が存在する場合はそれを前面に

といったことを行うスクリプトです。

f:id:mechairoi:20171226225115g:plain

session の current directory がリポジトリのルートに設定されるので、別のシェルを使いたいときは tmux new-window などで新しい端末を開けば cd する必要もありません。 作業リポジトリを切り替えたいときも、このスクリプトを起動して選ぶだけで、以前の session があれば記憶とともに蘇ることでしょう。

gist41201c4579d17253b7bd26e699c6dccb