Slide 1

Slide 1 text

ReactiveX で REQ する Nostr ぽーまん (@penpenpng) Nostr 勉強会 #3

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Review: NIP-01

Slide 5

Slide 5 text

【復習】 EVENT / REQ ってなんだっけ EVENT = クライアントが生成し、リレーに送信する情報 (のうち、他のクライアントへの伝達を目的とするもの ) ● プロフィール (kind0) ● 投稿 (kind1) ● リアクション (kind7) ● Zap (kind9734, kind9735) REQ = クライアントがリレーに「EVENTが来たら教えてね」って言うやつ (EVENT の購読) クライアント リレー クライアント kind1 来たら教えてや ええで REQ kind1: ぽわ~ なんかきたで ぽわ~言うとるな

Slide 6

Slide 6 text

["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"]

Slide 7

Slide 7 text

Introduction: REQ is complicated

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

リクエストは投げっぱなしではない リクエストに対して「追加の非同期な操作」 (=CLOSE) が発生しうる クライアント リレー データちょうだい ちょい待ってな… ほいな あ、まだある これも これも これも入荷したで もうええわ (CLOSE) ● 同じ subId を異なる filter の REQ で上書きする操作も見ようによっては「追加の非同期な操作」 ● REQ とは、非同期 N パラメータと非同期 M レスポンスから成る、本質的に時系列的な操作

Slide 10

Slide 10 text

「多重化した伝送路が多重に存在する」というあまりにもややこしい構造 クライアント リレー WebSocket REQ 多重化した伝送路 クライアント リレー リレー リレー …が多重に存在する ● 定義上の REQ とはこの図中の1本の細い線であるわけだが、 アプリケーションが求める "REQ" とは多重化した3本の細い線である ● 当然ながら「追加の非同期な操作」も多重化している

Slide 11

Slide 11 text

再接続処理という鬼門 ● リレーとの WebSocket 通信は案外安定しないので、再接続が頻繁に発生する ● ところで、伝送路が失われると、それに紐づいた REQ 仮想伝送路も当然失われる クライアント リレー リレー リレー ● WebSocket 再接続時には、伝送路に紐づく REQ をすべて再構成する必要がある ● 再構成 = REQ メッセージの再送 とは限らない ○ `since: now` を再送したとして、それはアプリケーションの意図通りであろうか?

Slide 12

Slide 12 text

「リレー設定を変更する」という頭痛の種 クライアント リレー リレー リレー リレー 消します 足します ● 接続先リレーを変更したとき、REQ は「自然に」維持される必要がある ● 再接続のときと同様に、自然な再構成とは REQ メッセージの再送そのものではない

Slide 13

Slide 13 text

サブスクリプション上限という低すぎる天井 ひとつの伝送路に入れることができる REQ の上限は決まっている (大体10前後のことが多い) クライアント リレー WebSocket REQ 3つまでしか入らないよ! すなわち、REQ の待ち行列を用意しなければならない REQ 待機所 ⋮

Slide 14

Slide 14 text

なんでこんなに面倒なのか ● 「ライブラリがないから」に尽きる ○ こんなわけわからん通信方式の先例はないので、ライブラリはない ○ REST API などのありふれた方式にも一定の面倒はあるものだが、 ライブラリがそれらのほとんどを隠蔽する ○ ライブラリがないので、先述の面倒事をすべてアプリケーションプログラマが 捌かなければならない ● 本当にまったくない? ○ nostr-tools があるといえばあるが… ○ nostr-fetch はある ■ 過去のイベントを取得する限りにおいて極めて自然なインターフェースを提供する ● REQ Subscription モデルが表現するすべてのユースケースをカバーできるような 便利なインターフェースがほしい

Slide 15

Slide 15 text

では、求められるインターフェースとは? ● 少なくとも 1 リクエスト : 非同期 N レスポンス を表現できることが求められる ● ところで、こちらの表をご覧ください: 同期的と読み替えてもいい 非同期的と読み替えてもいい

Slide 16

Slide 16 text

では、求められるインターフェースとは? ref. https://rxjs.dev/guide/observable

Slide 17

Slide 17 text

ReactiveX (Rx)

Slide 18

Slide 18 text

[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 と表現される概念について、ストリーム的な側面をより強調したい意図がある。

Slide 19

Slide 19 text

ものごとをストリームとみなす コードの引用: https://rxjs.dev/guide/overview ● 「なんか (=クリックイベント) あったらこれ (下線部)を 実行しておいて」という何の変哲もないコード ● ここで「なんか」を可視化する図を書いてみると … ○ 「なんか」(=丸印) がどこからともなく現れ 下線部に到達したらコールバックが実行される …と思える

Slide 20

Slide 20 text

ストリームを RxJS で表現する Observer Observable コードの引用: https://rxjs.dev/guide/overview ● RxJS を使って同等のコードを書いたものがこちら ● Rx では、この青いストリームを Observable、赤いコンシューマを Observer と呼んでいる ● 先のコードとの決定的な違いはコードの 中の `fromEvent()` がストリームそのものであるところ ○ 先のコードではストリームと 思える概念がコードの外にあっただけ

Slide 21

Slide 21 text

コード上にストリームを表現できると何が嬉しいのか ● ずばり、ストリームを演算できるようになる点がうれしい ○ f(observable) なるシグネチャを持つ関数 f を考えられるようになる (Operator) ○ 実際には f(g(h(observable))) のような演算が頻発するので、 pipe() による演算を行うことになる

Slide 22

Slide 22 text

Operator による演算の例 1 - map() map() の Marble diagram https://rxjs.dev/api/index/function/map

Slide 23

Slide 23 text

Operator による演算の例 2 - mergeMap() mergeMap() の Marble diagram https://rxjs.dev/api/operators/mergeMap

Slide 24

Slide 24 text

Operator による演算の例 3 - switchAll() switchAll() の Marble diagram https://rxjs.dev/api/operators/switchAll

Slide 25

Slide 25 text

Operator を「ゲート」とみなす ● Operator の作用は今まで見てきたように Marble diagram で表現するのが一般的 ● …なのだが、今後のスライドでは画面面積の都合で「ゲート」記法を導入することにする map(x => x*3) filter(x => x%2 === 0) delay(1000) これは左から整数を入れると、 ○ 3倍されて、 ○ 奇数は捨てて、 ○ 残った整数を 1000 ミリ秒後に排出する ような (あまり意味のない) Observable[4] になる [4] 左からものを入れられるので、正確に言えば Subject となる 整数 整数

Slide 26

Slide 26 text

Applying ReactiveX to REQ

Slide 27

Slide 27 text

REQ Subscription をモデリングする ● 素朴に考えると REQ Subscription は Subject でモデリングできる ○ Subject は手動で値を「流す」ことができる Observable リレーに投げる リレーから受け取る REQ EVENT/EOSE リレー ● pros ○ 「非同期 N パラメータ / 非同期 M レスポンス」の構造は既に表現できている ● cons ○ subId をいちいち指定するのが面倒くさい ○ 適当なタイミングで CLOSE を投げる (= unsubscribe する) のが面倒くさい ■ アプリケーションロジックは subId の具体値にも CLOSE タイミングのフルコントロールにも (大抵は) 興味はない

Slide 28

Slide 28 text

素朴 REQ モデルの問題点に関する考察 subId をいちいち指定するのが面倒くさい 意味のある subId の選び方は実のところ 2 通りしかない ● まったく新しい subId を生成する ● 過去のいずれかの REQ と同じ subId を使う 適当なタイミングで CLOSE を投げるのが面倒くさい CLOSE を投げたくなるタイミングは大抵が次の 3 通り ● EOSE を受け取った ● 新しい REQ を発行したので、古い REQ がいらなくなった ● 欲しい情報が手に入った ○ これはパターン化できないので手動でやってもらうのが現実的

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 いらない 過去のREQと同じ いらない CLOSE のタイミング subId

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT に興味がある いらない 過去のREQと同じ いらない 未来の EVENT に興味がある CLOSE のタイミング subId ● まとめると、以上の青マスに対応する 2 つの戦略を用意しておけば、 アプリケーションプログラマは subId と CLOSE の管理から解放される ● 以後、この 2 つの戦略をそれぞれ Backward Strategy / Forward Strategy と呼ぶことにする

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Forward Strategy 投げる 受け取る filters EVENT リレー REQ に変換 投げる 受け取る REQ EVENT/EOSE リレー 素朴 REQ モデル Forward Strategy 固定値の subId を付与 (CLOSE の役割も担う) 入力と出力の型が整理されてうれしいの図 その 2 CLOSE 読み捨て

Slide 40

Slide 40 text

アプリケーションから見ると filters EVENT リレー なんやかんやいい感じにする ☑ 解決: subId をいちいち指定するのが面倒くさい ☑ 解決: 適当なタイミングで CLOSE を投げる (= unsubscribe する) のが面倒くさい

Slide 41

Slide 41 text

これでオールハッピーだったんだっけ? ● サブスクリプション上限のケア ● 再接続処理 ● エラーハンドリング ● 「リレー設定を変更する」のハンドリング 🫠 …… が こちらを みている!

Slide 42

Slide 42 text

Solution: Case rx-nostr

Slide 43

Slide 43 text

全体構成 ● RxReq, RxNostr という2種類のオブジェクトを定める ● RxReq ○ Strategy を定める ○ アプリケーションから filter を受け入れる ● RxNostr ○ リレーたちとの通信をなんやかんやいい感じにする ○ アプリケーションに EVENT を返す filters EVENT リレー なんやかんやいい感じにする RxReq RxNostr Application Logic リレー リレー filters

Slide 44

Slide 44 text

全体構成 ● 内部オブジェクトとして、Connection を定める ○ ひとつのリレーに対する伝送路と、その中の REQ 仮想伝送路の管理に責任を持つ ○ 部分解決: 再接続処理 ○ 解決: サブスクリプション上限のケア EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection リレー リレー リレー filters filters

Slide 45

Slide 45 text

再接続処理についてもっと詳しく ● 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 になる

Slide 46

Slide 46 text

異常切断の検知 ● ところで、WebSocket が切断された理由がリレー起因なのかを正確に知る方法はない ○ RFC-6455 は Close frame におけるステータスコードを定めてはいる[5]が、 リレー実装がどれを返すのかはわからない (返すかすらわからない) ○ 切断の理由がリレー実装レイヤーにあるとも限らない ○ リレーが "行儀よく" コネクションを閉じたときは `websocket.onerror` は呼ばれない ■ onerror が呼ばれる厳密な条件がわからない。誰か教えてほしい [5] https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1

Slide 47

Slide 47 text

異常切断の検知 ● 4000 番台のコードはアプリケーションの都合で好き勝手使っていいことになっている ○ 実際、Nostr Protocol は "4000" を "Don't Retry" の意味で使うよう定めている ○ rx-nostr はこれを遵守するので 4000 を返されたときは静かに死ぬ ● rx-nostr が自分自身の都合で WebSocket を切断するすべてのケースにコードをあててしまえば それ以外のコードをもって閉じられた場合を異常切断とみなすことができる ○ 4537 と 4538 を rx-nostr 自身の都合のために定義して使っている ○ leet 的なアレで番号を決めた気がしたが、由来は忘れた。もう誰にもわからない

Slide 48

Slide 48 text

エラーハンドリング ● Rx の Observable にはストリーム上で「エラー」を返す仕組みがあるが、 一度エラーを返したストリームは終了してしまう ● 「多重化した伝送路が多重に存在する」ので、エラーは複数回投げられる可能性がある ● ⇒ エラー情報を別の Observable にまとめ上げて提供する

Slide 49

Slide 49 text

エラーハンドリング ● 解決: エラーハンドリング

Slide 50

Slide 50 text

「リレー設定を変更する」と闘う EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection リレー リレー リレー lazy filters lazy filters リレー設定が変更されるたびにここを筋肉で書き換える 解決💪: 「リレー設定を変更する」のハンドリング

Slide 51

Slide 51 text

Appendix: rx-nostr's code example

Slide 52

Slide 52 text

Operator is 便利 重複排除 可能な範囲でソート EVENT EVENT 署名を検証

Slide 53

Slide 53 text

送信もできるよ

Slide 54

Slide 54 text

Summary

Slide 55

Slide 55 text

おつノス! ● 真面目に REQ をハンドルするのはあまりにも面倒くさいので、 便利なライブラリが必要とされている ● ReactiveX (Rx) と REQ の相性はいい ● Rx を利用して REQ をハンドルするライブラリを作ってみたよ