rx-nostr
Version:
A library based on RxJS, which allows Nostr applications to easily communicate with relays.
278 lines (258 loc) • 8.34 kB
text/typescript
import * as Nostr from "nostr-typedef";
import { Observable, of, type OperatorFunction, Subject } from "rxjs";
import { LazyFilter, ReqPacket } from "../packet.js";
/**
* The RxReq interface that is provided for RxNostr (**not for users**).
*/
export interface RxReq<S extends RxReqStrategy = RxReqStrategy> {
/** @internal User should not use this directly. The RxReq strategy. It is read-only and must not change. */
strategy: S;
/** @internal User should not use this directly. Used to construct subId. */
rxReqId: string;
/** @internal User should not use this directly. Get an Observable of ReqPacket. */
getReqPacketObservable(): Observable<ReqPacket>;
}
/**
* REQ strategy.
*
* See comments on `createRxForwardReq()`, `createRxBackwardReq()` and `createRxOneshotReq()
*/
export type RxReqStrategy = "forward" | "backward";
export interface RxReqPipeable {
/**
* Returns itself overriding only `getReqObservable()`.
* It is useful for throttling and other control purposes.
*/
pipe(): RxReq;
pipe(op1: OperatorFunction<ReqPacket, ReqPacket>): RxReq;
pipe<A>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, ReqPacket>,
): RxReq;
pipe<A, B>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, ReqPacket>,
): RxReq;
pipe<A, B, C>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, C>,
op4: OperatorFunction<C, ReqPacket>,
): RxReq;
pipe<A, B, C, D>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, C>,
op4: OperatorFunction<C, D>,
op5: OperatorFunction<D, ReqPacket>,
): RxReq;
pipe<A, B, C, D, E>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, C>,
op4: OperatorFunction<C, D>,
op5: OperatorFunction<D, E>,
op6: OperatorFunction<E, ReqPacket>,
): RxReq;
pipe<A, B, C, D, E, F>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, C>,
op4: OperatorFunction<C, D>,
op5: OperatorFunction<D, E>,
op6: OperatorFunction<E, F>,
op7: OperatorFunction<F, ReqPacket>,
): RxReq;
pipe<A, B, C, D, E, F, G>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, C>,
op4: OperatorFunction<C, D>,
op5: OperatorFunction<D, E>,
op6: OperatorFunction<E, F>,
op7: OperatorFunction<F, G>,
op8: OperatorFunction<G, ReqPacket>,
): RxReq;
pipe<A, B, C, D, E, F, G, H>(
op1: OperatorFunction<ReqPacket, A>,
op2: OperatorFunction<A, B>,
op3: OperatorFunction<B, C>,
op4: OperatorFunction<C, D>,
op5: OperatorFunction<D, E>,
op6: OperatorFunction<E, F>,
op7: OperatorFunction<F, G>,
op8: OperatorFunction<G, H>,
op9: OperatorFunction<H, ReqPacket>,
): RxReq;
}
export type RxReqEmittable<O = void> = O extends void
? {
/** Start new REQ on the RxNostr with which the RxReq is associated. */
emit(filters: LazyFilter | LazyFilter[]): void;
}
: {
/** Start new REQ on the RxNostr with which the RxReq is associated. */
emit(filters: LazyFilter | LazyFilter[], options?: O): void;
};
/**
* Notify RxNostr that it does not intend to send any more REQs.
* The Observable that returned by `use()` is complete
* when all REQs that have already been sent have been completed.
*/
export interface RxReqOverable {
over(): void;
}
const createRxReq = <S extends RxReqStrategy>(params: {
strategy: S;
rxReqId?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
operators?: OperatorFunction<any, any>[];
subject?: Subject<ReqPacket>;
}): RxReq<S> &
RxReqEmittable<{ relays: string[] }> &
RxReqOverable &
RxReqPipeable => {
const { strategy } = params;
const _operators = params.operators ?? [];
const rxReqId = params.rxReqId ?? getRandomDigitsString();
const filters$ = params.subject ?? new Subject<ReqPacket>();
return {
strategy,
rxReqId,
getReqPacketObservable(): Observable<ReqPacket> {
return filters$.pipe(...(_operators as []));
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pipe(...operators: OperatorFunction<any, any>[]): RxReq {
return createRxReq({
strategy,
rxReqId,
operators: [..._operators, ...operators],
subject: filters$,
});
},
emit(filters: LazyFilter | LazyFilter[], options?: { relays: string[] }) {
filters$.next({ filters: normalizeFilters(filters), ...(options ?? {}) });
},
over() {
filters$.complete();
},
};
};
/**
* 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/v1/req-strategy.html#backward-strategy).
*/
export function createRxBackwardReq(
rxReqId?: string,
): RxReq<"backward"> &
RxReqEmittable<{ relays: string[] }> &
RxReqOverable &
RxReqPipeable {
return createRxReq({
strategy: "backward",
rxReqId,
});
}
/**
* 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.
* - In most cases, you should not specify `limit` for filters.
*
* For more information, see [document](https://penpenpng.github.io/rx-nostr/v1/req-strategy.html#forward-strategy).
*/
export function createRxForwardReq(
rxReqId?: string,
): RxReq<"forward"> & RxReqEmittable & RxReqPipeable {
return createRxReq({
strategy: "forward",
rxReqId,
});
}
/**
* 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/v1/req-strategy.html#oneshot-strategy).
*/
export function createRxOneshotReq(params: {
filters: LazyFilter | LazyFilter[];
rxReqId?: string;
}): RxReq<"backward"> {
return {
strategy: "backward",
rxReqId: params.rxReqId ?? getRandomDigitsString(),
getReqPacketObservable: () =>
of({ filters: normalizeFilters(params.filters) }),
};
}
export interface Mixin<R, T> {
(): ThisType<R> & T;
}
function getRandomDigitsString() {
return `${Math.floor(Math.random() * 1000000)}`;
}
function normalizeFilter(filter: LazyFilter): LazyFilter | null {
const res: LazyFilter = {};
const isTagName = (s: string): s is Nostr.TagName => /^#[a-zA-Z]$/.test(s);
for (const key of Object.keys(filter)) {
if (key === "limit" && (filter[key] ?? -1) >= 0) {
res[key] = filter[key];
continue;
}
if (key === "since" || key === "until") {
const f = filter[key];
if (typeof f !== "number" || (f ?? -1) >= 0) {
res[key] = f;
continue;
}
}
if (
(isTagName(key) || key === "ids" || key === "authors") &&
filter[key] !== undefined &&
(filter[key]?.length ?? -1) > 0
) {
res[key] = filter[key];
continue;
}
if (
key === "kinds" &&
filter[key] !== undefined &&
(filter[key]?.length ?? -1) > 0
) {
res[key] = filter[key];
continue;
}
if (key === "search" && filter[key] !== undefined) {
res[key] = filter[key];
continue;
}
}
const timeRangeIsValid =
typeof res.since !== "number" ||
typeof res.until !== "number" ||
res.since <= res.until;
if (!timeRangeIsValid) {
return null;
}
return res;
}
function normalizeFilters(filters: LazyFilter | LazyFilter[]): LazyFilter[] {
return (Array.isArray(filters) ? filters : [filters])
.map((e) => normalizeFilter(e))
.filter((e): e is LazyFilter => e !== null);
}