#

section

(1 articles)

Introduction to rx-nostr (without RxJS) / RxJS を学ばずに使う rx-nostr

日本語の記事は後ろにあります。 The article in Japanese follows after in English. # Introduction to rx-nostr (without RxJS) This article is for Day 11 of Nostr Advent Calendar 2023 ([Venue 1](https://adventar.org/calendars/8794), [Venue 2](https://adventar.org/calendars/8880)). Yesterday's articles were "[Math for Schnorr signature - elliptic curve](https://fivebythree.net/project/secp256k1nostr/ellipticcurve/)" by Jun-san and "[Review of Gijutsu Shoten 15 from the perspective of a sales](https://habla.news/ja/u/rira224@rira224.github.io/1701671746970)" by Rira-san. --- Hello, Nostr. I'm poman ([ぽーまん](nostr:npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza)). Recently, new major version of my library "[rx-nostr](https://github.com/penpenpng/rx-nostr)" was released. Since this is a good opportunity, I'd like to talk about what rx-nostr is or is not. I hope that this entry let you use my library. rx-nostr is a library for Nostr client to communicate with relays. In other words, what rx-nostr essentially does is just following: - It takes [filters](https://github.com/nostr-protocol/nips/blob/master/01.md#from-client-to-relay-sending-events-and-creating-subscriptions), then send them to relays, and return EVENT message subscription. - It takes [events](https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures), then send them to relays, and return OK message subscrition. rx-nostr doesn't care about kind of events. It doesn't wrap the content of events, just provide messages as-is to users. It doesn't provide [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)-like Promise-based handy API. rx-nostr handles communication between client and relays as [Observable](https://rxjs.dev/guide/observable), not Promise. (If you are familiar with [RxJS](https://rxjs.dev), you can handle Observable at will. This is a part of the value of the library, but this article talks about the other part.) Would a library that could do only this be considered useless? The value of rx-nostr lies in the "carefulness" with which these communications are conducted. ## "Carefully" communications with relays The most part of specification about communication on Nostr is in [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md). The specification is very simple as summarized into the two sentences: - Clients packs what it wants to send into an `EVENT`, and then sends it. - Clients packs what it wants to receive into an `REQ`, and then sends it. It is very easy to **naively** implement the process based on the few rules. This is a short example of `REQ`: ```js const ws = new WebSocket("wss://example.com"); ws.onopen = () => { ws.send(JSON.stringify(["REQ", { kinds: [1] }])); }; ws.onmessage = (ev) => { console.log(JSON.parse(ev.data)); }; ``` So, is it easy to implement this **carefully**? **No**. There are many trivial (but taking time and effort) points in real applications. Does your application do the followings? - Does it verify the signatures? - Does it ensure that `REQ`'d events really match the filters' condition? - Does it ignore expired events based on [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md)? - Assume there is a `REQ` that is listening a timeline. When an end-user adds/removes relay config, does your application reflect it to the timeline immediately? - Relays have [`max_subscriptions`](https://github.com/nostr-protocol/nips/blob/master/11.md#server-limitations). When it tries to make `REQ` subscription more than this, does it queue them? - Even if each relay has different `max_subscriptions`? - In addition to relays configured by end-user, your application may access temporary relays, for example to fetch `nevent1`. In that case... - Does it keep only one WebSocket connection even if the temporary relays overlap with the default relays? (NIP-01 says that clients should do so.) - When communication on the temporary relays is finished, does your application closes the WebSocket connection? - Does it try to reconnect? - [As RFC says](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.2.3), does it use some form of backoff when trying to reconnect? - As NIP-01 says, does it stop reconnection if the close code is 4000? - After reconnection, does it send `EVENT` that was about to be issued during the reconnection attempt? - After reconnection, does it (re)construct `REQ` that was ongoing before reconnection or was about to be issued during the reconnection attempt? - If does so, is `since`/`until` of reconstructed `REQ` based on relative time of origin at reconnection? - Does it monitor the status of connections? rx-nostr does all of those automatically! ## "Carefully" communications by rx-nostr All you need to know to use rx-nostr is about **Backward** and **Forward**. (Being familiar with RxJS helps you use rx-nostr more useful, but it is not required.) These are rx-nostr's unique but not hard concept. Backward is to `CLOSE` on receiving `EOSE`, and Forward is not to `CLOSE` and to keep `REQ` permanently. That is all. These distinctions allow rx-nostr to optimize communication. The example of naive `REQ` implementation described above is Forward. Let's implement the same with rx-nostr. First of all, we create a `RxNostr` instance, which manages connections and communications with relays: ```js const rxNostr = createRxNostr(); rxNostr.setDefaultRelays(["wss://example.com"]); ``` Next, we create a Forward `RxReq`, connect it to `RxNostr`, and define a listener: ```js const rxReq = createRxForwardReq(); rxNostr .use(rxReq) .subscribe((packet) => { console.log(packet.message); }); ``` Finally, we issue `REQ` through `RxReq`: ```js rxReq.emit({ kinds: [1] }); ``` `emit()` can be called any number of times. Forward `RxReq` overwrites previous `REQ`, and Backward `RxReq` keeps all old `REQ`s and add new one. The complete code is now as follows: ```js import { createRxForwardReq, createRxNostr } from "rx-nostr"; const rxNostr = createRxNostr(); rxNostr.setDefaultRelays(["wss://example.com"]); const rxReq = createRxForwardReq(); rxNostr .use(rxReq) .subscribe((packet) => { console.log(packet.message); }); rxReq.emit({ kinds: [1] }); ``` Now we resolve all trivial points described above. For instance, when end-user changes default relay configuration, we do: ```js rxNostr.setDefaultRelays(["wss://example.com", "wss://second.example.com"]); ``` It will update the ongoing `{ kinds: [1] }` subscription, and we will get events on `wss://second.example.com`. We can use `use()`'s option for temporary relays. Use Backward `RxReq` to get some events from temporary relay as following: ```js const rxReq = createRxBackwardReq(); rxNostr .use(rxReq, { relays: "wss://temp.example.com" }) .subscribe((packet) => { console.log(packet.message); }); rxReq.emit({ ids: [EVENT_ID] }); ``` It will get `EVENT_ID` event from `wss://temp.example.com` and close the WebSocket connection automatically. (I know that `relays` option should be available in `emit()`. Please wait for future updates.) In truth, you can do much more. For more detailed usage, plese see the official documentation... ah sorry, documentation for v2.x is not yet. ## Summary I explained what rx-nostr can do without assuming knowledge of RxJS. If I have a chance, I would like to write about what can be achieved by combining it with RxJS. Tomorrow's article is by クリプト彼氏@仮想通貨擬人化-san and Lokuyow-san! Foo! (The following is a Japanese article of the same.) # RxJS を学ばずに使う rx-nostr この記事は Nostr Advent Calendar 2023 ([第一会場](https://adventar.org/calendars/8794), [第二会場](https://adventar.org/calendars/8880)) の第一会場 11 日目の記事です。昨日 10 日目の記事は Jun さんの『[Schnorr 署名に使われる数学 - 楕円曲線](https://fivebythree.net/project/secp256k1nostr/ellipticcurve/)』と りらさんの『[営業目線で振り返る技術書典15](https://habla.news/ja/u/rira224@rira224.github.io/1701671746970)』でした。 --- はろー Nostr、[ぽーまん](nostr:npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza)です。 先日、拙作ライブラリ [rx-nostr](https://github.com/penpenpng/rx-nostr) の 2.0.0 がリリースされました。いい機会なので、rx-nostr とは何であるのか、あるいは何ではないのかという話をしようと思います。あわよくばこれを読んだ皆様に使って頂こうという魂胆でございます。 rx-nostr はクライアントがリレー群と通信するためのライブラリです。言い換えると、rx-nostr が本質的に行うことはたったこれだけです: - [フィルター](https://github.com/nostr-protocol/nips/blob/master/01.md#from-client-to-relay-sending-events-and-creating-subscriptions)を受け取ると、それをリレーに送信し、EVENT の購読を返します。 - [イベント](https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures)を受け取ると、それをリレーに送信し、OK の購読を返します。 実際に送受信されている情報の種類 (kind) には頓着しません。つまり rx-nostr は受信したコンテンツを何かでラップすることはなく、送られてきたものをただただそのままユーザに提示するだけです。 [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) のような Promise ベースの手軽な API の提供もしません。rx-nostr は通信をあるがままに扱うので、返されるものは扱いやすい Promise ではなく [Observable](https://rxjs.dev/guide/observable) です。([RxJS](https://rxjs.dev) と連携すれば Observable も自由自在に取り回せるようになります。これが rx-nostr が提供する価値のうちの半分ですが、この記事で主張したいのはもう半分の方です。) たったこれだけのライブラリは無用なものだと思われるでしょうか? rx-nostr の価値はこれらの通信が「丁寧に」行われることにこそあります。 ## リレーと「丁寧に」通信する リレーとの通信にかかる仕様はそのほとんどすべてが [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) に記述されています。その仕様はいたってシンプルで、要旨は次の 2 点に集約されると言ってもいいでしょう: - クライアントは情報を送信したければ、送信したいものに `EVENT` と書いてリレーに送信せよ。 - クライアントは情報を受信したければ、受信したいものに `REQ` と書いてリレーに送信せよ。 このわずかな規約に基づく送受信処理を**素朴に**実装するのは実に簡単です。受信処理に限って例を挙げると、次のようにたった数行で書くことができます。 ```js const ws = new WebSocket("wss://example.com"); ws.onopen = () => { ws.send(JSON.stringify(["REQ", { kinds: [1] }])); }; ws.onmessage = (ev) => { console.log(JSON.parse(ev.data)); }; ``` ではこの送受信処理を**丁寧に**実装するのは簡単でしょうか?答えは **No** です。現実のアプリケーションには考慮しなければならない些事 (ただし大変に面倒くさい些事) が実に多くつきまといます。例えばあなたのアプリケーションは…… - 署名の検証はしていますか? - `REQ` の結果リレーから返されたイベントが本当にフィルターの条件に合致するか確認していますか? - [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) に基づいて期限切れのイベントは無視していますか? - タイムラインを取得している `REQ` があったとして、エンドユーザが途中でリレーを追加・削除したとき、タイムラインはそれを即座に反映できますか? - リレーには [`max_subscriptions`](https://github.com/nostr-protocol/nips/blob/master/11.md#server-limitations) が定められています。これを超える量の `REQ` を発行しようとするとき、キューイングはされますか? - リレーごとに `max_subscriptions` の値が異なるとしても、それは動作しますか? - エンドユーザがクライアントに設定しているデフォルトのリレーに加えて、一時的なリレーにアクセスする必要に迫られることがあります (`nevent1` を取得するときなど)。 - 一時的なリレーがデフォルトのリレーと重複するとしても WebSocket 接続は 1 つに抑えられていますか? (NIP-01 はすべての通信を単一の WebSocket 接続の上で行うよう定めています。) - 一時的なリレーとの通信が終了したとき、WebSocket を切断していますか? - WebSocket が瞬断したとき、再接続していますか? - 再接続は [RFC が定めるように](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.2.3) バックオフ戦略を採用していますか? - NIP-01 が定めるように、コード 4000 で終了した場合には再接続を行わないよう実装していますか? - 再接続試行中に送信されようとしていた `EVENT` を再接続成功時に送信していますか? - 再接続前に発行されていた、あるいは再接続試行中に発行しようとしていた `REQ` を再接続成功時に復元していますか? - 復元していたとして、復元後の `REQ` の `since`/`until` は再接続時起点の相対時刻に基づくことができますか? - 各リレーとの WebSocket の接続状況をモニタリングしていますか? rx-nostr はこれらのすべてを肩代わりします! ## rx-nostr で「丁寧に」通信する rx-nostr を使う際に覚えておかなければならないのは **Backward** と **Forward** の概念だけです (RxJS についても詳しい方が便利ではありますが、必須ではないです)。これは rx-nostr 独自の概念ですがさほど難しくはありません。`EOSE` を受信したときに自動で `CLOSE` するべき `REQ` のことを Backward、`EOSE` を無視して永続する `REQ` のことを Forward と呼んでいるだけです。これらの使い分けは rx-nostr が通信を最適化するために利用されます。 先程の素朴な `REQ` の実装は Forward です。これと同じコードを rx-nostr で書いてみましょう。まずは `RxNostr` インスタンスを用意します。これはリレーとの通信を管理してくれるオブジェクトです。 ```js const rxNostr = createRxNostr(); rxNostr.setDefaultRelays(["wss://example.com"]); ``` 次に Forward な `RxReq` を用意して `RxNostr` につなげると同時に、メッセージがやってきたときに何をするのかを定義します。 ```js const rxReq = createRxForwardReq(); rxNostr .use(rxReq) .subscribe((packet) => { console.log(packet.message); }); ``` 最後に `RxReq` を通じて `REQ` を発行します。 ```js rxReq.emit({ kinds: [1] }); ``` `emit()` は何回でも呼び出すことができます。Forward な `RxReq` では以前に `emit()` した `REQ` を上書きしますが、Backward な `RxReq` では前回までの `REQ` を保ったまま新しい `REQ` を加えます。 完全なコードは次の通りになりました: ```js import { createRxForwardReq, createRxNostr } from "rx-nostr"; const rxNostr = createRxNostr(); rxNostr.setDefaultRelays(["wss://example.com"]); const rxReq = createRxForwardReq(); rxNostr .use(rxReq) .subscribe((packet) => { console.log(packet.message); }); rxReq.emit({ kinds: [1] }); ``` これだけで前節であげた「些事」はすべて解決されています。例えば、エンドユーザがデフォルトのリレーを追加した場合には次のようにします: ```js rxNostr.setDefaultRelays(["wss://example.com", "wss://second.example.com"]); ``` すると既に発行されて継続中の `{ kinds: [1] }` は `wss://second.example.com` の上でも購読されるようになります。 一時的なリレーを使いたい場合は `use()` のオプションが利用できます。例えば、ある event を特定のリレーから取得したくなった場合、Backward な `RxReq` とともに次のように書きます: ```js const rxReq = createRxBackwardReq(); rxNostr .use(rxReq, { relays: "wss://temp.example.com" }) .subscribe((packet) => { console.log(packet.message); }); rxReq.emit({ ids: [EVENT_ID] }); ``` すると `{ ids: [EVENT_ID] }` は `wss://temp.example.com` から取得され、取得され次第 WebSocket 接続は自動で閉じられます。 (本当は `relays` オプションが `emit()` の中で利用できた方がいいと思っています。今後のアップデートをお待ち下さい。) 実際にはもっと色々なことができます。より詳しい使い方は公式ドキュメントをご覧ください……と言いたいところなのですが、v2.x に対応するドキュメントはまだありません。書きます……。 ## おわりに RxJS の知識を前提としない範囲で rx-nostr に何ができるのかを解説しました。今度機会があれば RxJS と組み合わせることで何が実現できるかも書けたらいいと思っています。 明日 12 日のアドベントカレンダーは クリプト彼氏@仮想通貨擬人化 さんと Lokuyow さんの記事です!わいわい!