Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Nostr REQ with Rx / Rx で REQ する Nostr
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
penpenpng
August 04, 2023
Programming
15k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Nostr REQ with Rx / Rx で REQ する Nostr
penpenpng
August 04, 2023
More Decks by penpenpng
See All by penpenpng
How Skylight was created / Techniques for constructing URL Entity.
penpenpng
0
370
Nostr Web Client のための (Service) Worker 活用法
penpenpng
0
460
Other Decks in Programming
See All in Programming
代数的データ型って何が嬉しいの? #frontend_phpcon_do
kajitack
8
3.3k
JavaDoc 再入門
nagise
0
320
AIで効率化できた業務・日常
ochtum
0
120
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
540
Go1.27で導入されるジェネリクスメソッドでできること
mackee
0
110
「エンジニアインターン、どうやって取った?」準備のリアルを語るLT会 Progate BAR
akiomatic
0
130
並列実装の現場、2ヶ月間実務でAIを使い倒したAIもPCも私も限界が近い
ming_ayami
0
120
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.2k
メソッドのジェネリクスでGoの夢は広がるか? / Kyoto.go #65
utgwkk
3
690
AI時代の仕事技芸論 — ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ
kuranuki
2
660
Claspは野良GASの夢をみるか
takter00
0
180
These Five Tricks Can Make Your Apps Greener, Cheaper, & Nicer
hollycummins
0
280
Featured
See All Featured
Building the Perfect Custom Keyboard
takai
2
790
Leading Effective Engineering Teams in the AI Era
addyosmani
9
2k
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
1
3.6k
The Illustrated Children's Guide to Kubernetes
chrisshort
51
52k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.5k
Between Models and Reality
mayunak
4
330
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.2k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
141
35k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
1
200
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
231
23k
How GitHub (no longer) Works
holman
316
150k
The World Runs on Bad Software
bkeepers
PRO
72
12k
Transcript
ReactiveX で REQ する Nostr ぽーまん (@penpenpng) Nostr 勉強会 #3
Abstract • 真面目に REQ をハンドルするのはあまりにも面倒くさいので、 便利なライブラリが必要とされている ◦ そもそも REQ ってなんだっけ
◦ 何がそんなに面倒なんだっけ • ReactiveX と REQ の相性はいい ◦ なんで相性がいいの ◦ 面倒事をどう解消できるの • ReactiveX を利用して REQ をハンドルするライブラリを作ってみたよ ◦ 設計どうなってるの ◦ どんないいことがあるの
Abstract • 真面目に REQ をハンドルするのはあまりにも面倒くさいので、 便利なライブラリが必要とされている ◦ そもそも REQ ってなんだっけ?
◦ 何がそんなに面倒なんだっけ? • ReactiveX と REQ の相性はいい ◦ なんで相性がいいの? ◦ 面倒事をどう解消できるの? • ReactiveX を利用して REQ をハンドルするライブラリを作ってみたよ ◦ 設計どうなってるの? ◦ どんないいことがあるの?
Review: NIP-01
【復習】 EVENT / REQ ってなんだっけ EVENT = クライアントが生成し、リレーに送信する情報 (のうち、他のクライアントへの伝達を目的とするもの )
• プロフィール (kind0) • 投稿 (kind1) • リアクション (kind7) • Zap (kind9734, kind9735) REQ = クライアントがリレーに「EVENTが来たら教えてね」って言うやつ (EVENT の購読) クライアント リレー クライアント kind1 来たら教えてや ええで REQ kind1: ぽわ~ なんかきたで ぽわ~言うとるな
["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"]
Introduction: REQ is complicated
1 リクエスト : 非同期 N レスポンス 一般的な Web API コール
= 1 リクエスト : 非同期 1 レスポンス クライアント HTTPサーバ データちょうだい ちょい待ってな… ほいな Nostr の REQ = 1 リクエスト : 非同期 N レスポンス[1] クライアント リレー データちょうだい ちょい待ってな… ほいな あ、まだある これも これも これも入荷したで async function request(params) {/* ... */} /* は? */ [1]「それは async iterable を返せばいいのでは?」と思ったあなたは鋭いですが、次のスライドで述べる事情等によりここではすっとぼけています。
リクエストは投げっぱなしではない リクエストに対して「追加の非同期な操作」 (=CLOSE) が発生しうる クライアント リレー データちょうだい ちょい待ってな… ほいな あ、まだある
これも これも これも入荷したで もうええわ (CLOSE) • 同じ subId を異なる filter の REQ で上書きする操作も見ようによっては「追加の非同期な操作」 • REQ とは、非同期 N パラメータと非同期 M レスポンスから成る、本質的に時系列的な操作
「多重化した伝送路が多重に存在する」というあまりにもややこしい構造 クライアント リレー WebSocket REQ 多重化した伝送路 クライアント リレー リレー リレー
…が多重に存在する • 定義上の REQ とはこの図中の1本の細い線であるわけだが、 アプリケーションが求める "REQ" とは多重化した3本の細い線である • 当然ながら「追加の非同期な操作」も多重化している
再接続処理という鬼門 • リレーとの WebSocket 通信は案外安定しないので、再接続が頻繁に発生する • ところで、伝送路が失われると、それに紐づいた REQ 仮想伝送路も当然失われる クライアント
リレー リレー リレー • WebSocket 再接続時には、伝送路に紐づく REQ をすべて再構成する必要がある • 再構成 = REQ メッセージの再送 とは限らない ◦ `since: now` を再送したとして、それはアプリケーションの意図通りであろうか?
「リレー設定を変更する」という頭痛の種 クライアント リレー リレー リレー リレー 消します 足します • 接続先リレーを変更したとき、REQ
は「自然に」維持される必要がある • 再接続のときと同様に、自然な再構成とは REQ メッセージの再送そのものではない
サブスクリプション上限という低すぎる天井 ひとつの伝送路に入れることができる REQ の上限は決まっている (大体10前後のことが多い) クライアント リレー WebSocket REQ 3つまでしか入らないよ!
すなわち、REQ の待ち行列を用意しなければならない REQ 待機所 ⋮
なんでこんなに面倒なのか • 「ライブラリがないから」に尽きる ◦ こんなわけわからん通信方式の先例はないので、ライブラリはない ◦ REST API などのありふれた方式にも一定の面倒はあるものだが、 ライブラリがそれらのほとんどを隠蔽する
◦ ライブラリがないので、先述の面倒事をすべてアプリケーションプログラマが 捌かなければならない • 本当にまったくない? ◦ nostr-tools があるといえばあるが… ◦ nostr-fetch はある ▪ 過去のイベントを取得する限りにおいて極めて自然なインターフェースを提供する • REQ Subscription モデルが表現するすべてのユースケースをカバーできるような 便利なインターフェースがほしい
では、求められるインターフェースとは? • 少なくとも 1 リクエスト : 非同期 N レスポンス を表現できることが求められる
• ところで、こちらの表をご覧ください: 同期的と読み替えてもいい 非同期的と読み替えてもいい
では、求められるインターフェースとは? ref. https://rxjs.dev/guide/observable
ReactiveX (Rx)
[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 と表現される概念について、ストリーム的な側面をより強調したい意図がある。
ものごとをストリームとみなす コードの引用: https://rxjs.dev/guide/overview • 「なんか (=クリックイベント) あったらこれ (下線部)を 実行しておいて」という何の変哲もないコード •
ここで「なんか」を可視化する図を書いてみると … ◦ 「なんか」(=丸印) がどこからともなく現れ 下線部に到達したらコールバックが実行される …と思える
ストリームを RxJS で表現する Observer Observable コードの引用: https://rxjs.dev/guide/overview • RxJS を使って同等のコードを書いたものがこちら
• Rx では、この青いストリームを Observable、赤いコンシューマを Observer と呼んでいる • 先のコードとの決定的な違いはコードの 中の `fromEvent()` がストリームそのものであるところ ◦ 先のコードではストリームと 思える概念がコードの外にあっただけ
コード上にストリームを表現できると何が嬉しいのか • ずばり、ストリームを演算できるようになる点がうれしい ◦ f(observable) なるシグネチャを持つ関数 f を考えられるようになる (Operator) ◦
実際には f(g(h(observable))) のような演算が頻発するので、 pipe() による演算を行うことになる
Operator による演算の例 1 - map() map() の Marble diagram https://rxjs.dev/api/index/function/map
Operator による演算の例 2 - mergeMap() mergeMap() の Marble diagram https://rxjs.dev/api/operators/mergeMap
Operator による演算の例 3 - switchAll() switchAll() の Marble diagram https://rxjs.dev/api/operators/switchAll
Operator を「ゲート」とみなす • Operator の作用は今まで見てきたように Marble diagram で表現するのが一般的 • …なのだが、今後のスライドでは画面面積の都合で「ゲート」記法を導入することにする
map(x => x*3) filter(x => x%2 === 0) delay(1000) これは左から整数を入れると、 ◦ 3倍されて、 ◦ 奇数は捨てて、 ◦ 残った整数を 1000 ミリ秒後に排出する ような (あまり意味のない) Observable[4] になる [4] 左からものを入れられるので、正確に言えば Subject となる 整数 整数
Applying ReactiveX to REQ
REQ Subscription をモデリングする • 素朴に考えると REQ Subscription は Subject でモデリングできる
◦ Subject は手動で値を「流す」ことができる Observable リレーに投げる リレーから受け取る REQ EVENT/EOSE リレー • pros ◦ 「非同期 N パラメータ / 非同期 M レスポンス」の構造は既に表現できている • cons ◦ subId をいちいち指定するのが面倒くさい ◦ 適当なタイミングで CLOSE を投げる (= unsubscribe する) のが面倒くさい ▪ アプリケーションロジックは subId の具体値にも CLOSE タイミングのフルコントロールにも (大抵は) 興味はない
素朴 REQ モデルの問題点に関する考察 subId をいちいち指定するのが面倒くさい 意味のある subId の選び方は実のところ 2 通りしかない
• まったく新しい subId を生成する • 過去のいずれかの REQ と同じ subId を使う 適当なタイミングで CLOSE を投げるのが面倒くさい CLOSE を投げたくなるタイミングは大抵が次の 3 通り • EOSE を受け取った • 新しい REQ を発行したので、古い REQ がいらなくなった • 欲しい情報が手に入った ◦ これはパターン化できないので手動でやってもらうのが現実的
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去のREQと同じ CLOSE
のタイミング subId • 表を使って整理していく
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去のREQと同じ CLOSE
のタイミング subId • 赤マスに注目 ◦ 「過去の REQ と同じ subId を持ち、EOSE が来たときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ をキャンセルしながら、過去の EVENT を収集する REQ」 ◦ 既にある REQ = 同種の REQ = 同じマスに属する REQ という仮定をおく
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 ◯ 過去のREQと同じ
↑ CLOSE のタイミング subId • 赤マスに注目 ◦ 「過去の REQ と同じ subId を持ち、EOSE が来たときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ をキャンセルしながら、過去の EVENT を収集する REQ」 ◦ 既にある REQ = 同種の REQ = 同じマスに属する REQ という仮定をおく • 「既にある REQ」もどうせ EOSE が来たら止まる。わざわざキャンセルする必要性は薄い ◦ ⇒ 青マスで代替可能
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 ◯ 過去のREQと同じ
↑ CLOSE のタイミング subId • 赤マスに注目 ◦ 「ユニークな subId を持ち、新しい REQ が発行されたときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ を邪魔せずに、新しい REQ が発行されたときに CLOSE する REQ」 ◦ 例によって「既にある REQ は同種の REQ」という仮定をおく
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 ◯ 過去のREQと同じ
↑ CLOSE のタイミング subId • 赤マスに注目 ◦ 「ユニークな subId を持ち、新しい REQ が発行されたときに CLOSE する REQ 」 ◦ ⇔ 「既にある REQ を邪魔せずに、新しい REQ が発行されたときに CLOSE する REQ」 ◦ 例によって「既にある REQ は同種の REQ」という仮定をおく • 「既にある REQ を邪魔しない」と「新しい REQ が発行されたときに CLOSE する」は矛盾 ◦ 意味のない組み合わせ
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 いらない 過去のREQと同じ
いらない CLOSE のタイミング subId
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT
に興味がある いらない 過去のREQと同じ いらない CLOSE のタイミング subId • EOSE のタイミングで CLOSE する ◦ ⇒ 過去の EVENT にしか興味がない
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT
に興味がある いらない 過去のREQと同じ いらない 未来の EVENT に興味がある CLOSE のタイミング subId • EOSE のタイミングで CLOSE する ◦ ⇒ 過去の EVENT にしか興味がない • CLOSE のタイミングが EOSE に依らない ◦ ⇒ 特定の条件に合致する EVENT にのみ興味があるか、未来の EVENT に興味がある ◦ ⇒ 未来の EVENT にしか興味がない ▪ ……と乱暴にまとめてしまってもいい。なぜなら「特定の条件に合致する EVENT」 が過去にあるか未来にあるかに応じて戦略を選んでもらえばいいから
subId と CLOSE に関する考察 EOSE 新しい REQ ユニーク値 過去の EVENT
に興味がある いらない 過去のREQと同じ いらない 未来の EVENT に興味がある CLOSE のタイミング subId • まとめると、以上の青マスに対応する 2 つの戦略を用意しておけば、 アプリケーションプログラマは subId と CLOSE の管理から解放される • 以後、この 2 つの戦略をそれぞれ Backward Strategy / Forward Strategy と呼ぶことにする
Backward Strategy 投げる 受け取る filters EVENT リレー REQ に変換 投げる
受け取る REQ EVENT/EOSE リレー 素朴 REQ モデル Backward Strategy ユニークな subId を付与 入力と出力の型が整理されてうれしいの図 その 1 CLOSE
Forward Strategy 投げる 受け取る filters EVENT リレー REQ に変換 投げる
受け取る REQ EVENT/EOSE リレー 素朴 REQ モデル Forward Strategy 固定値の subId を付与 (CLOSE の役割も担う) 入力と出力の型が整理されてうれしいの図 その 2 CLOSE 読み捨て
アプリケーションから見ると filters EVENT リレー なんやかんやいい感じにする ☑ 解決: subId をいちいち指定するのが面倒くさい ☑
解決: 適当なタイミングで CLOSE を投げる (= unsubscribe する) のが面倒くさい
これでオールハッピーだったんだっけ? • サブスクリプション上限のケア • 再接続処理 • エラーハンドリング • 「リレー設定を変更する」のハンドリング 🫠
…… が こちらを みている!
Solution: Case rx-nostr
全体構成 • RxReq, RxNostr という2種類のオブジェクトを定める • RxReq ◦ Strategy を定める
◦ アプリケーションから filter を受け入れる • RxNostr ◦ リレーたちとの通信をなんやかんやいい感じにする ◦ アプリケーションに EVENT を返す filters EVENT リレー なんやかんやいい感じにする RxReq RxNostr Application Logic リレー リレー filters
全体構成 • 内部オブジェクトとして、Connection を定める ◦ ひとつのリレーに対する伝送路と、その中の REQ 仮想伝送路の管理に責任を持つ ◦ 部分解決:
再接続処理 ◦ 解決: サブスクリプション上限のケア EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection リレー リレー リレー filters filters
再接続処理についてもっと詳しく • 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 になる
異常切断の検知 • ところで、WebSocket が切断された理由がリレー起因なのかを正確に知る方法はない ◦ RFC-6455 は Close frame におけるステータスコードを定めてはいる[5]が、
リレー実装がどれを返すのかはわからない (返すかすらわからない) ◦ 切断の理由がリレー実装レイヤーにあるとも限らない ◦ リレーが "行儀よく" コネクションを閉じたときは `websocket.onerror` は呼ばれない ▪ onerror が呼ばれる厳密な条件がわからない。誰か教えてほしい [5] https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
異常切断の検知 • 4000 番台のコードはアプリケーションの都合で好き勝手使っていいことになっている ◦ 実際、Nostr Protocol は "4000" を
"Don't Retry" の意味で使うよう定めている ◦ rx-nostr はこれを遵守するので 4000 を返されたときは静かに死ぬ • rx-nostr が自分自身の都合で WebSocket を切断するすべてのケースにコードをあててしまえば それ以外のコードをもって閉じられた場合を異常切断とみなすことができる ◦ 4537 と 4538 を rx-nostr 自身の都合のために定義して使っている ◦ leet 的なアレで番号を決めた気がしたが、由来は忘れた。もう誰にもわからない
エラーハンドリング • Rx の Observable にはストリーム上で「エラー」を返す仕組みがあるが、 一度エラーを返したストリームは終了してしまう • 「多重化した伝送路が多重に存在する」ので、エラーは複数回投げられる可能性がある •
⇒ エラー情報を別の Observable にまとめ上げて提供する
エラーハンドリング • 解決: エラーハンドリング
「リレー設定を変更する」と闘う EVENT Connection なんやかんやいい感じにする RxReq RxNostr Application Logic Connection Connection
リレー リレー リレー lazy filters lazy filters リレー設定が変更されるたびにここを筋肉で書き換える 解決💪: 「リレー設定を変更する」のハンドリング
Appendix: rx-nostr's code example
Operator is 便利 重複排除 可能な範囲でソート EVENT EVENT 署名を検証
送信もできるよ
Summary
おつノス! • 真面目に REQ をハンドルするのはあまりにも面倒くさいので、 便利なライブラリが必要とされている • ReactiveX (Rx) と
REQ の相性はいい • Rx を利用して REQ をハンドルするライブラリを作ってみたよ