Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Nostr REQ with Rx / Rx で REQ する Nostr

penpenpng
August 04, 2023

Nostr REQ with Rx / Rx で REQ する Nostr

penpenpng

August 04, 2023
Tweet

More Decks by penpenpng

Other Decks in Programming

Transcript

  1. Abstract • 真面目に REQ をハンドルするのはあまりにも面倒くさいので、 便利なライブラリが必要とされている ◦ そもそも REQ ってなんだっけ

    ◦ 何がそんなに面倒なんだっけ • ReactiveX と REQ の相性はいい ◦ なんで相性がいいの ◦ 面倒事をどう解消できるの • ReactiveX を利用して REQ をハンドルするライブラリを作ってみたよ ◦ 設計どうなってるの ◦ どんないいことがあるの
  2. Abstract • 真面目に REQ をハンドルするのはあまりにも面倒くさいので、 便利なライブラリが必要とされている ◦ そもそも REQ ってなんだっけ?

    ◦ 何がそんなに面倒なんだっけ? • ReactiveX と REQ の相性はいい ◦ なんで相性がいいの? ◦ 面倒事をどう解消できるの? • ReactiveX を利用して REQ をハンドルするライブラリを作ってみたよ ◦ 設計どうなってるの? ◦ どんないいことがあるの?
  3. 【復習】 EVENT / REQ ってなんだっけ EVENT = クライアントが生成し、リレーに送信する情報 (のうち、他のクライアントへの伝達を目的とするもの )

    • プロフィール (kind0) • 投稿 (kind1) • リアクション (kind7) • Zap (kind9734, kind9735) REQ = クライアントがリレーに「EVENTが来たら教えてね」って言うやつ (EVENT の購読) クライアント リレー クライアント kind1 来たら教えてや ええで REQ kind1: ぽわ~ なんかきたで ぽわ~言うとるな
  4. ["REQ", "powa", { kinds: [1] }] 【復習】 Nostr の REQ

    の仕組み クライアント リレー WebSocket ["REQ", "wayo", { kinds: [3] }] ["REQ", "moge", { kinds: [2] }] REQ 他所のクライアント 他所のクライアント 他所のクライアント ["EVENT", "powa", { ... }] ["EVENT", "powa", { ... }] ["EOSE", "powa"] ["EVENT", "powa", { ... }] ... ["EVENT", "moge", { ... }] ["EVENT", "moge", { ... }] ["EOSE", "moge"] ["EVENT", "wayo", { ... }] ["EVENT", "wayo", { ... }] ["EOSE", "wayo"]
  5. 1 リクエスト : 非同期 N レスポンス 一般的な Web API コール

    = 1 リクエスト : 非同期 1 レスポンス クライアント HTTPサーバ データちょうだい ちょい待ってな… ほいな Nostr の REQ = 1 リクエスト : 非同期 N レスポンス[1] クライアント リレー データちょうだい ちょい待ってな… ほいな あ、まだある これも これも これも入荷したで async function request(params) {/* ... */} /* は? */ [1]「それは async iterable を返せばいいのでは?」と思ったあなたは鋭いですが、次のスライドで述べる事情等によりここではすっとぼけています。
  6. リクエストは投げっぱなしではない リクエストに対して「追加の非同期な操作」 (=CLOSE) が発生しうる クライアント リレー データちょうだい ちょい待ってな… ほいな あ、まだある

    これも これも これも入荷したで もうええわ (CLOSE) • 同じ subId を異なる filter の REQ で上書きする操作も見ようによっては「追加の非同期な操作」 • REQ とは、非同期 N パラメータと非同期 M レスポンスから成る、本質的に時系列的な操作
  7. 「多重化した伝送路が多重に存在する」というあまりにもややこしい構造 クライアント リレー WebSocket REQ 多重化した伝送路 クライアント リレー リレー リレー

    …が多重に存在する • 定義上の REQ とはこの図中の1本の細い線であるわけだが、 アプリケーションが求める "REQ" とは多重化した3本の細い線である • 当然ながら「追加の非同期な操作」も多重化している
  8. 再接続処理という鬼門 • リレーとの WebSocket 通信は案外安定しないので、再接続が頻繁に発生する • ところで、伝送路が失われると、それに紐づいた REQ 仮想伝送路も当然失われる クライアント

    リレー リレー リレー • WebSocket 再接続時には、伝送路に紐づく REQ をすべて再構成する必要がある • 再構成 = REQ メッセージの再送 とは限らない ◦ `since: now` を再送したとして、それはアプリケーションの意図通りであろうか?
  9. 「リレー設定を変更する」という頭痛の種 クライアント リレー リレー リレー リレー 消します 足します • 接続先リレーを変更したとき、REQ

    は「自然に」維持される必要がある • 再接続のときと同様に、自然な再構成とは REQ メッセージの再送そのものではない
  10. なんでこんなに面倒なのか • 「ライブラリがないから」に尽きる ◦ こんなわけわからん通信方式の先例はないので、ライブラリはない ◦ REST API などのありふれた方式にも一定の面倒はあるものだが、 ライブラリがそれらのほとんどを隠蔽する

    ◦ ライブラリがないので、先述の面倒事をすべてアプリケーションプログラマが 捌かなければならない • 本当にまったくない? ◦ nostr-tools があるといえばあるが… ◦ nostr-fetch はある ▪ 過去のイベントを取得する限りにおいて極めて自然なインターフェースを提供する • REQ Subscription モデルが表現するすべてのユースケースをカバーできるような 便利なインターフェースがほしい
  11. では、求められるインターフェースとは? • 少なくとも 1 リクエスト : 非同期 N レスポンス を表現できることが求められる

    • ところで、こちらの表をご覧ください: 同期的と読み替えてもいい 非同期的と読み替えてもいい
  12. [2] https://reactivex.io/languages.html ReactiveX (Rx) ってなんだ • ReactiveX (Rx) は Observer

    Pattern を生かした API の定義の集合 ◦ 具体的な実装は言語ごとにライブラリとして存在する ▪ JavaScript, Java, C#, Scala, Clojure, C++, Lua, Ruby, Python, Go, Groovy, JRuby, Kotlin, Swift, PHP, Elixir, Dart[2] ◦ ある程度は移植性があると言ってもいい • RxJS は JavaScript における Rx の実装 • 特徴的な点は: ◦ Observable (Subject を示す Rx に特有の言い回し) をストリーム[3]とみなす点 ◦ ストリームを演算するための Operator という概念 ▪ 高次 Observable すら簡単に取り回すことができる • と、言葉で言われてもピンと来ないと思うので次のスライドで例を見ていきます [3] 公式の用語ではない。公式ドキュメントでは observable sequences と表現される概念について、ストリーム的な側面をより強調したい意図がある。
  13. ものごとをストリームとみなす コードの引用: https://rxjs.dev/guide/overview • 「なんか (=クリックイベント) あったらこれ (下線部)を 実行しておいて」という何の変哲もないコード •

    ここで「なんか」を可視化する図を書いてみると … ◦ 「なんか」(=丸印) がどこからともなく現れ 下線部に到達したらコールバックが実行される …と思える
  14. ストリームを RxJS で表現する Observer Observable コードの引用: https://rxjs.dev/guide/overview • RxJS を使って同等のコードを書いたものがこちら

    • Rx では、この青いストリームを Observable、赤いコンシューマを Observer と呼んでいる • 先のコードとの決定的な違いはコードの 中の `fromEvent()` がストリームそのものであるところ ◦ 先のコードではストリームと 思える概念がコードの外にあっただけ
  15. Operator を「ゲート」とみなす • Operator の作用は今まで見てきたように Marble diagram で表現するのが一般的 • …なのだが、今後のスライドでは画面面積の都合で「ゲート」記法を導入することにする

    map(x => x*3) filter(x => x%2 === 0) delay(1000) これは左から整数を入れると、 ◦ 3倍されて、 ◦ 奇数は捨てて、 ◦ 残った整数を 1000 ミリ秒後に排出する ような (あまり意味のない) Observable[4] になる [4] 左からものを入れられるので、正確に言えば Subject となる 整数 整数
  16. REQ Subscription をモデリングする • 素朴に考えると REQ Subscription は Subject でモデリングできる

    ◦ Subject は手動で値を「流す」ことができる Observable リレーに投げる リレーから受け取る REQ EVENT/EOSE リレー • pros ◦ 「非同期 N パラメータ / 非同期 M レスポンス」の構造は既に表現できている • cons ◦ subId をいちいち指定するのが面倒くさい ◦ 適当なタイミングで CLOSE を投げる (= unsubscribe する) のが面倒くさい ▪ アプリケーションロジックは subId の具体値にも CLOSE タイミングのフルコントロールにも (大抵は) 興味はない
  17. 素朴 REQ モデルの問題点に関する考察 subId をいちいち指定するのが面倒くさい 意味のある subId の選び方は実のところ 2 通りしかない

    • まったく新しい subId を生成する • 過去のいずれかの REQ と同じ subId を使う 適当なタイミングで CLOSE を投げるのが面倒くさい CLOSE を投げたくなるタイミングは大抵が次の 3 通り • EOSE を受け取った • 新しい REQ を発行したので、古い REQ がいらなくなった • 欲しい情報が手に入った ◦ これはパターン化できないので手動でやってもらうのが現実的
  18. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去のREQと同じ CLOSE

    のタイミング subId • 表を使って整理していく
  19. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去のREQと同じ CLOSE

    のタイミング subId • 赤マスに注目 ◦ 「過去の REQ と同じ subId を持ち、EOSE が来たときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ をキャンセルしながら、過去の EVENT を収集する REQ」 ◦ 既にある REQ = 同種の REQ = 同じマスに属する REQ という仮定をおく
  20. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 ◯ 過去のREQと同じ

    ↑ CLOSE のタイミング subId • 赤マスに注目 ◦ 「過去の REQ と同じ subId を持ち、EOSE が来たときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ をキャンセルしながら、過去の EVENT を収集する REQ」 ◦ 既にある REQ = 同種の REQ = 同じマスに属する REQ という仮定をおく • 「既にある REQ」もどうせ EOSE が来たら止まる。わざわざキャンセルする必要性は薄い ◦ ⇒ 青マスで代替可能
  21. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 ◯ 過去のREQと同じ

    ↑ CLOSE のタイミング subId • 赤マスに注目 ◦ 「ユニークな subId を持ち、新しい REQ が発行されたときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ を邪魔せずに、新しい REQ が発行されたときに CLOSE する REQ」 ◦ 例によって「既にある REQ は同種の REQ」という仮定をおく
  22. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 ◯ 過去のREQと同じ

    ↑ CLOSE のタイミング subId • 赤マスに注目 ◦ 「ユニークな subId を持ち、新しい REQ が発行されたときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ を邪魔せずに、新しい REQ が発行されたときに CLOSE する REQ」 ◦ 例によって「既にある REQ は同種の REQ」という仮定をおく • 「既にある REQ を邪魔しない」と「新しい REQ が発行されたときに CLOSE する」は矛盾 ◦ 意味のない組み合わせ
  23. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT

    に興味がある いらない 過去のREQと同じ いらない CLOSE のタイミング subId • EOSE のタイミングで CLOSE する ◦ ⇒ 過去の EVENT にしか興味がない
  24. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT

    に興味がある いらない 過去のREQと同じ いらない 未来の EVENT に興味がある CLOSE のタイミング subId • EOSE のタイミングで CLOSE する ◦ ⇒ 過去の EVENT にしか興味がない • CLOSE のタイミングが EOSE に依らない ◦ ⇒ 特定の条件に合致する EVENT にのみ興味があるか、未来の EVENT に興味がある ◦ ⇒ 未来の EVENT にしか興味がない ▪ ……と乱暴にまとめてしまってもいい。なぜなら「特定の条件に合致する EVENT」 が過去にあるか未来にあるかに応じて戦略を選んでもらえばいいから
  25. subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT

    に興味がある いらない 過去のREQと同じ いらない 未来の EVENT に興味がある CLOSE のタイミング subId • まとめると、以上の青マスに対応する 2 つの戦略を用意しておけば、 アプリケーションプログラマは subId と CLOSE の管理から解放される • 以後、この 2 つの戦略をそれぞれ Backward Strategy / Forward Strategy と呼ぶことにする
  26. Backward Strategy 投げる 受け取る filters EVENT リレー REQ に変換 投げる

    受け取る REQ EVENT/EOSE リレー 素朴 REQ モデル Backward Strategy ユニークな subId を付与 入力と出力の型が整理されてうれしいの図 その 1 CLOSE
  27. Forward Strategy 投げる 受け取る filters EVENT リレー REQ に変換 投げる

    受け取る REQ EVENT/EOSE リレー 素朴 REQ モデル Forward Strategy 固定値の subId を付与 (CLOSE の役割も担う) 入力と出力の型が整理されてうれしいの図 その 2 CLOSE 読み捨て
  28. 全体構成 • RxReq, RxNostr という2種類のオブジェクトを定める • RxReq ◦ Strategy を定める

    ◦ アプリケーションから filter を受け入れる • RxNostr ◦ リレーたちとの通信をなんやかんやいい感じにする ◦ アプリケーションに EVENT を返す filters EVENT リレー なんやかんやいい感じにする RxReq RxNostr Application Logic リレー リレー filters
  29. 全体構成 • 内部オブジェクトとして、Connection を定める ◦ ひとつのリレーに対する伝送路と、その中の REQ 仮想伝送路の管理に責任を持つ ◦ 部分解決:

    再接続処理 ◦ 解決: サブスクリプション上限のケア EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection リレー リレー リレー filters filters
  30. 再接続処理についてもっと詳しく • filter の `since` と `until` に関数を許す lazy filter

    を定める ◦ 他のフィルター項目に関しては未対応 ◦ `since: () => Math.floor(new Date().getTime() / 1000)` とすればいつでも新鮮な タイムラインが手に入る ◦ 解決: 再接続処理 EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection リレー リレー リレー lazy filters lazy filters lazy filter は ここで評価されて filter になる
  31. 異常切断の検知 • ところで、WebSocket が切断された理由がリレー起因なのかを正確に知る方法はない ◦ RFC-6455 は Close frame におけるステータスコードを定めてはいる[5]が、

    リレー実装がどれを返すのかはわからない (返すかすらわからない) ◦ 切断の理由がリレー実装レイヤーにあるとも限らない ◦ リレーが "行儀よく" コネクションを閉じたときは `websocket.onerror` は呼ばれない ▪ onerror が呼ばれる厳密な条件がわからない。誰か教えてほしい [5] https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
  32. 異常切断の検知 • 4000 番台のコードはアプリケーションの都合で好き勝手使っていいことになっている ◦ 実際、Nostr Protocol は "4000" を

    "Don't Retry" の意味で使うよう定めている ◦ rx-nostr はこれを遵守するので 4000 を返されたときは静かに死ぬ • rx-nostr が自分自身の都合で WebSocket を切断するすべてのケースにコードをあててしまえば それ以外のコードをもって閉じられた場合を異常切断とみなすことができる ◦ 4537 と 4538 を rx-nostr 自身の都合のために定義して使っている ◦ leet 的なアレで番号を決めた気がしたが、由来は忘れた。もう誰にもわからない
  33. 「リレー設定を変更する」と闘う EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection

    リレー リレー リレー lazy filters lazy filters リレー設定が変更されるたびにここを筋肉で書き換える 解決💪: 「リレー設定を変更する」のハンドリング