Skip to content

Commit

Permalink
docs: code comments
Browse files Browse the repository at this point in the history
  • Loading branch information
penpenpng committed Jul 8, 2023
1 parent be0093b commit 797094f
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 57 deletions.
4 changes: 4 additions & 0 deletions docs/docs/relay-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ rxNostr.switchRelays(await window.nostr.getRelays());
## Reactivity

リレー構成の変更は、現在確立している REQ サブスクリプションに直ちに反映されます。すなわち、新しい構成のもとでもはや読み取りが許可されなくなったリレーにおける REQ は即座に CLOSE され、逆に新しく読み取りが可能になったリレーに対しては同等の REQ が自動的に送信されます。

## Auto Reconnection

WebSocket が予期しない理由で切断されたとき、rx-nostr は [exponential backoff and jitter](https://aws.amazon.com/jp/blogs/architecture/exponential-backoff-and-jitter/) 戦略に従って自動で再接続を試みます。この挙動は `createRxNostr()` のオプションで変更できます。
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { toHex } from "./nostr/bech32";
export {
compareEvents,
earlierEvent,
Expand All @@ -9,6 +10,7 @@ export {
now,
} from "./nostr/event";
export { isFiltered } from "./nostr/filter";
export { fetchRelayInfo } from "./nostr/nip11";
export * from "./operator";
export * from "./packet";
export * from "./req";
Expand Down
1 change: 1 addition & 0 deletions src/nostr/bech32.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { bytesToHex } from "@noble/hashes/utils";
import { bech32 } from "@scure/base";

/** Convert bech32 format string to HEX format string. */
export function toHex(str: string): string {
const { words } = bech32.decode(str);
const data = new Uint8Array(bech32.fromWords(words));
Expand Down
12 changes: 12 additions & 0 deletions src/nostr/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import { toHex } from "./bech32";

const utf8Encoder = new TextEncoder();

/**
* Return a signed event that is ready for sending.
*/
export async function getSignedEvent(
params: Nostr.EventParameters,
/** Private key in bech32 format of HEX format. If omitted, attempt to use NIP-07 interface. */
seckey?: string
): Promise<Nostr.Event> {
const event = {
Expand Down Expand Up @@ -71,10 +75,12 @@ export async function getSignedEvent(
}
}

/** Calculate and return public key in HEX format. */
export function getPublicKey(seckey: string): string {
return bytesToHex(schnorr.getPublicKey(seckey));
}

/** Calculate and return event's hash (ID). */
export function getEventHash(event: Nostr.UnsignedEvent): string {
const serialized = JSON.stringify([
0,
Expand All @@ -87,10 +93,12 @@ export function getEventHash(event: Nostr.UnsignedEvent): string {
return bytesToHex(sha256(utf8Encoder.encode(serialized)));
}

/** Calculate and return schnorr signature. */
export function getSignature(eventHash: string, seckey: string): string {
return bytesToHex(schnorr.sign(eventHash, seckey));
}

/** Verify the given event and return true if it is valid. */
export function verify(event: Nostr.Event): boolean {
try {
return schnorr.verify(event.sig, getEventHash(event), event.pubkey);
Expand Down Expand Up @@ -119,18 +127,22 @@ export function ensureRequiredFields(
return true;
}

/** Return current time that can be used for `created_at`. */
export function now() {
return Math.floor(new Date().getTime() / 1000);
}

/** Return an event that has earlier `created_at`. */
export function earlierEvent(a: Nostr.Event, b: Nostr.Event): Nostr.Event {
return compareEvents(a, b) < 0 ? a : b;
}

/** Return an event that has later `created_at`. */
export function laterEvent(a: Nostr.Event, b: Nostr.Event): Nostr.Event {
return compareEvents(a, b) < 0 ? b : a;
}

/** Sort key function to sort events based on `created_at`. */
export function compareEvents(a: Nostr.Event, b: Nostr.Event): number {
if (a.id === b.id) {
return 0;
Expand Down
3 changes: 3 additions & 0 deletions src/nostr/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const makeMatchFilterOptions = defineDefaultOptions<MatchFilterOptions>({
untilInclusive: false,
});

/**
* Return true if the given filter matches the given filters.
*/
export function isFiltered(
event: Nostr.Event,
filters: Nostr.Filter | Nostr.Filter[],
Expand Down
16 changes: 16 additions & 0 deletions src/nostr/nip11.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Nostr from "nostr-typedef";

/**
* Fetch relay's information based on [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md).
*/
export async function fetchRelayInfo(
url: string
): Promise<Nostr.Nip11.RelayInfo> {
const u = new URL(url);
u.protocol = u.protocol.replace(/^ws(s?):/, "http$1:");

const res = await fetch(u.toString(), {
headers: { Accept: "application/nostr+json" },
});
return res.json();
}
20 changes: 20 additions & 0 deletions src/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,19 @@ export function filterKind<K extends Nostr.Kind>(
return filter<EventPacket>(({ event }) => event.kind === kind);
}

/**
* Filter events based on a REQ filter object.
*/
export function filterBy(
filters: Nostr.Filter | Nostr.Filter[],
options?: MatchFilterOptions
): MonoTypeOperatorFunction<EventPacket> {
return filter(({ event }) => isFiltered(event, filters, options));
}

/**
* Accumulate latest events in order of new arrival (based on `created_at`).
*/
export function timeline(
limit?: number
): OperatorFunction<EventPacket, EventPacket[]> {
Expand All @@ -106,7 +112,13 @@ function defaultMergeFilter(
return [...a, ...b];
}

/**
* Map REQ packets into a single REQ packet.
*
* It is useful to reduce REQ requests in a time interval.
*/
export function batch(
/** Function used for merge REQ filters. Default behavior is simple concatenation. */
mergeFilter?: MergeFilter
): OperatorFunction<ReqPacket[], ReqPacket> {
return map((f) =>
Expand All @@ -122,6 +134,11 @@ export function batch(
);
}

/**
* Chunk a REQ packet into multiple REQ packets.
*
* It is useful to avoid to send large REQ filter.
*/
export function chunk(
predicate: (f: Nostr.Filter[]) => boolean,
toChunk: (f: Nostr.Filter[]) => Nostr.Filter[][]
Expand All @@ -131,6 +148,9 @@ export function chunk(
);
}

/**
* Almost RxJS's `timeout`, but won't throw.
*/
export function completeOnTimeout<T>(
time: number
): MonoTypeOperatorFunction<T> {
Expand Down
31 changes: 24 additions & 7 deletions src/packet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,37 @@ export interface MessagePacket {
message: Nostr.ToClientMessage.Any;
}

/**
* Packets emitted when WebSocket connection state is changed.
*/
export interface ConnectionStatePacket {
from: string;
state: ConnectionState;
}

/**
* WebSocket connection state.
*
* - `not-started`: Not started yet, or closed by expected ways.
* - `starting`: Attempting to connect (for reasons other than error recovery).
* - `ongoing`: Active, but may be temporarily closed as idling.
* - `reconnecting`: Trying to reconnect for error recovery.
* - `error`: Inactive because of an unexpected error. You can try to recover by reconnect()
* - `rejected`: Inactive because of closing code 4000. You can try to reconnect, but should not do.
* - `terminated`: No longer available because of dispose()
*/
export type ConnectionState =
| "not-started" // Not started yet, or closed by expected ways.
| "starting" // Attempting to connect (for reasons other than error recovery).
| "ongoing" // Active, but may be temporarily closed as idling.
| "reconnecting" // Trying to reconnect for error recovery.
| "error" // Inactive because of an unexpected error. You can try to recover by reconnect()
| "rejected" // Inactive because of closing code 4000. You can try to reconnect, but should not do.
| "terminated"; // No longer available because of dispose()
| "not-started"
| "starting"
| "ongoing"
| "reconnecting"
| "error"
| "rejected"
| "terminated";

/**
* Packets represents OK messages associated with an EVENT submission.
*/
export interface OkPacket {
from: string;
id: string;
Expand Down
84 changes: 49 additions & 35 deletions src/req.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@ import { ReqPacket } from "./packet";
import type { Override } from "./util";

/**
* The RxReq interface that is provided for RxNostr (not for users).
* The RxReq interface that is provided for RxNostr (**not for users**).
*/
export interface RxReq<S extends RxReqStrategy = RxReqStrategy> {
/** The RxReq strategy. It is read-only and must not change. */
/** @internal User should not use this directly.The RxReq strategy. It is read-only and must not change. */
get strategy(): S;
/** Used to construct subId. */
/** @internal User should not use this directly. Used to construct subId. */
get rxReqId(): string;
/** Get an Observable of ReqPacket. */
/** @internal User should not use this directly. Get an Observable of ReqPacket. */
getReqObservable(): Observable<ReqPacket>;
}

/**
* REQ strategy.
*
* See comments on `createRxForwardReq()`, `createRxBackwardReq()` and `createRxOneshotReq()
*/
export type RxReqStrategy = "forward" | "backward" | "oneshot";

/**
* The RxReq interface that is provided for users (not for RxNostr).
*/
export interface RxReqController {
/** Start new REQ or stop REQ on the RxNostr with witch the RxReq is associated. */
emit(filters: Nostr.Filter[] | null): void;

/**
* Returns itself overriding only `getReqObservable()`.
Expand Down Expand Up @@ -45,16 +60,6 @@ export interface RxReq<S extends RxReqStrategy = RxReqStrategy> {
): RxReq;
}

export type RxReqStrategy = "forward" | "backward" | "oneshot";

/**
* The RxReq interface that is provided for users (not for RxNostr).
*/
export interface RxReqController {
/** Start new REQ or stop REQ on the RxNostr with witch the RxReq is associated. */
emit(filters: Nostr.Filter[] | null): void;
}

abstract class RxReqBase implements RxReq {
protected filters$ = new BehaviorSubject<ReqPacket>(null);
private _rxReqId: string;
Expand Down Expand Up @@ -124,23 +129,23 @@ abstract class RxReqBase implements RxReq {
}
}

/**
* Create a RxReq instance based on the backward strategy.
* It is useful if you want to retrieve past events that have already been published.
*
* In backward strategy:
* - All REQs have different subIds.
* - All REQ-subscriptions keep alive until timeout or getting EOSE.
* - In most cases, you should specify `until` or `limit` for filters.
*
* For more information, see [document](https://penpenpng.github.io/rx-nostr/docs/req-strategy.html#backward-strategy).
*/
export function createRxBackwardReq(
subIdBase?: string
): RxReq<"backward"> & RxReqController {
return new RxBackwardReq(subIdBase);
}

/**
* Base class for RxReq based on the backward strategy.
* This is useful if you want to retrieve past events that have already been published.
*
* In backward strategy:
* - All REQs have different subIds.
* - All subscriptions keep alive until timeout or getting EOSE.
* - Observable corresponding to each time REQ is flattened by `mergeAll()`.
* - https://rxjs.dev/api/operators/mergeAll
* - In most cases, you should specify `limit` for filters.
*/
class RxBackwardReq extends RxReqBase implements RxReqController {
constructor(rxReqId?: string) {
super(rxReqId);
Expand All @@ -151,24 +156,24 @@ class RxBackwardReq extends RxReqBase implements RxReqController {
}
}

export function createRxForwardReq(
subId?: string
): RxReq<"forward"> & RxReqController {
return new RxForwardReq(subId);
}

/**
* Base class for RxReq based on the forward strategy.
* This is useful if you want to listen future events.
* Create a RxReq instance based on the forward strategy.
* It is useful if you want to listen future events.
*
* In forward strategy:
* - All REQs have the same subId.
* - When a new REQ is issued, the old REQ is overwritten and terminated immediately.
* The latest REQ keeps alive until it is overwritten or explicitly terminated.
* - Observable corresponding to each time REQ is flattened by `switchAll()`.
* - https://rxjs.dev/api/operators/switchAll
* - In most cases, you should not specify `limit` for filters.
*
* For more information, see [document](https://penpenpng.github.io/rx-nostr/docs/req-strategy.html#forward-strategy).
*/
export function createRxForwardReq(
subId?: string
): RxReq<"forward"> & RxReqController {
return new RxForwardReq(subId);
}

class RxForwardReq extends RxReqBase implements RxReqController {
constructor(rxReqId?: string) {
super(rxReqId);
Expand All @@ -179,6 +184,13 @@ class RxForwardReq extends RxReqBase implements RxReqController {
}
}

/**
* Create a RxReq instance based on the oneshot strategy.
* It is almost the same as backward strategy, however can publish only one REQ
* and the Observable completes on EOSE.
*
* For more information, see [document](https://penpenpng.github.io/rx-nostr/docs/req-strategy.html#oneshot-strategy).
*/
export function createRxOneshotReq(req: {
filters: Nostr.Filter[];
subId?: string;
Expand All @@ -201,12 +213,14 @@ export interface Mixin<R, T> {
(): ThisType<R> & T;
}

/** NOTE: unstable feature */
export function mixin<R extends object, T extends object>(
def: () => ThisType<R> & T
): Mixin<R, T> {
return def;
}

/** NOTE: unstable feature */
export function extend<B extends R, R extends object, T extends object>(
base: B,
mixin: Mixin<R, T>
Expand Down
Loading

0 comments on commit 797094f

Please sign in to comment.