applesauce-relay 0.0.0-next-20250505205231 → 0.0.0-next-20250526151506
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/exports.test.d.ts +1 -0
- package/dist/__tests__/exports.test.js +19 -0
- package/dist/__tests__/fake-user.d.ts +10 -0
- package/dist/__tests__/fake-user.js +31 -0
- package/dist/__tests__/relay.test.js +48 -3
- package/dist/group.d.ts +5 -5
- package/dist/operators/__tests__/exports.test.d.ts +1 -0
- package/dist/operators/__tests__/exports.test.js +15 -0
- package/dist/operators/__tests__/to-event-store.test.d.ts +1 -0
- package/dist/operators/__tests__/to-event-store.test.js +18 -0
- package/dist/operators/store-events.d.ts +1 -1
- package/dist/operators/store-events.js +1 -1
- package/dist/operators/to-event-store.d.ts +4 -1
- package/dist/operators/to-event-store.js +8 -12
- package/dist/pool.d.ts +5 -5
- package/dist/relay.d.ts +4 -4
- package/dist/relay.js +25 -11
- package/dist/types.d.ts +11 -9
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as exports from "../index.js";
|
|
3
|
+
describe("exports", () => {
|
|
4
|
+
it("should export the expected functions", () => {
|
|
5
|
+
expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
|
|
6
|
+
[
|
|
7
|
+
"Relay",
|
|
8
|
+
"RelayGroup",
|
|
9
|
+
"RelayPool",
|
|
10
|
+
"ReqCloseError",
|
|
11
|
+
"completeOnEose",
|
|
12
|
+
"markFromRelay",
|
|
13
|
+
"onlyEvents",
|
|
14
|
+
"storeEvents",
|
|
15
|
+
"toEventStore",
|
|
16
|
+
]
|
|
17
|
+
`);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
|
2
|
+
import type { NostrEvent } from "nostr-tools";
|
|
3
|
+
export declare class FakeUser extends SimpleSigner {
|
|
4
|
+
pubkey: string;
|
|
5
|
+
event(data?: Partial<NostrEvent>): NostrEvent;
|
|
6
|
+
note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
7
|
+
profile(profile?: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
8
|
+
contacts(pubkeys?: string[]): import("nostr-tools").Event;
|
|
9
|
+
list(tags?: string[][], extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { unixNow } from "applesauce-core/helpers";
|
|
2
|
+
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
|
3
|
+
import { finalizeEvent, getPublicKey, kinds } from "nostr-tools";
|
|
4
|
+
export class FakeUser extends SimpleSigner {
|
|
5
|
+
pubkey = getPublicKey(this.key);
|
|
6
|
+
event(data) {
|
|
7
|
+
return finalizeEvent({
|
|
8
|
+
kind: data?.kind ?? kinds.ShortTextNote,
|
|
9
|
+
content: data?.content || "",
|
|
10
|
+
created_at: data?.created_at ?? unixNow(),
|
|
11
|
+
tags: data?.tags || [],
|
|
12
|
+
}, this.key);
|
|
13
|
+
}
|
|
14
|
+
note(content = "Hello World", extra) {
|
|
15
|
+
return this.event({ kind: kinds.ShortTextNote, content, ...extra });
|
|
16
|
+
}
|
|
17
|
+
profile(profile = {}, extra) {
|
|
18
|
+
return this.event({ kind: kinds.Metadata, content: JSON.stringify({ ...profile }), ...extra });
|
|
19
|
+
}
|
|
20
|
+
contacts(pubkeys = []) {
|
|
21
|
+
return this.event({ kind: kinds.Contacts, tags: pubkeys.map((p) => ["p", p]) });
|
|
22
|
+
}
|
|
23
|
+
list(tags = [], extra) {
|
|
24
|
+
return this.event({
|
|
25
|
+
kind: kinds.Bookmarksets,
|
|
26
|
+
content: "",
|
|
27
|
+
tags: [["d", String(Math.round(Math.random() * 10000))], ...tags],
|
|
28
|
+
...extra,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -4,7 +4,7 @@ import { getSeenRelays } from "applesauce-core/helpers";
|
|
|
4
4
|
import { WS } from "vitest-websocket-mock";
|
|
5
5
|
import { Relay } from "../relay.js";
|
|
6
6
|
import { filter } from "rxjs/operators";
|
|
7
|
-
import { firstValueFrom, of, throwError, timer } from "rxjs";
|
|
7
|
+
import { firstValueFrom, of, Subject, throwError, timer } from "rxjs";
|
|
8
8
|
const defaultMockInfo = {
|
|
9
9
|
name: "Test Relay",
|
|
10
10
|
description: "Test Relay Description",
|
|
@@ -144,7 +144,7 @@ describe("req", () => {
|
|
|
144
144
|
// Verify the second subscription received the event and EOSE
|
|
145
145
|
expect(secondSub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
146
146
|
});
|
|
147
|
-
it("should wait for authentication if relay info document has limitations.auth_required = true", async () => {
|
|
147
|
+
it("should open connection and wait for authentication if relay info document has limitations.auth_required = true", async () => {
|
|
148
148
|
// Mock the fetchInformationDocument method to return a document with auth_required = true
|
|
149
149
|
vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
|
|
150
150
|
name: "Auth Required Relay",
|
|
@@ -162,10 +162,20 @@ describe("req", () => {
|
|
|
162
162
|
const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
163
163
|
// Wait 10ms to ensure the information document is fetched
|
|
164
164
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
165
|
+
// Wait for connection
|
|
166
|
+
await server.connected;
|
|
165
167
|
// Verify no REQ message was sent yet (waiting for auth)
|
|
166
168
|
expect(server).not.toHaveReceivedMessages(["REQ", "sub1", { kinds: [1] }]);
|
|
169
|
+
// Send AUTH challenge
|
|
170
|
+
server.send(["AUTH", "challenge"]);
|
|
171
|
+
// Send auth response
|
|
172
|
+
subscribeSpyTo(relay.auth(mockEvent));
|
|
173
|
+
// Verify the auth event was sent
|
|
174
|
+
await expect(server.nextMessage).resolves.toEqual(["AUTH", mockEvent]);
|
|
175
|
+
// Accept auth
|
|
176
|
+
server.send(["OK", mockEvent.id, true, ""]);
|
|
167
177
|
// Simulate successful authentication
|
|
168
|
-
relay.authenticated
|
|
178
|
+
expect(relay.authenticated).toBe(true);
|
|
169
179
|
// Now the REQ should be sent
|
|
170
180
|
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
171
181
|
// Send EVENT and EOSE to complete the subscription
|
|
@@ -208,6 +218,41 @@ describe("req", () => {
|
|
|
208
218
|
relay.ready$.next(true);
|
|
209
219
|
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
210
220
|
});
|
|
221
|
+
it("should wait for filters if filters are provided as an observable", async () => {
|
|
222
|
+
const filters = new Subject();
|
|
223
|
+
subscribeSpyTo(relay.req(filters, "sub1"));
|
|
224
|
+
// Wait 10sm and ensure no messages were sent yet
|
|
225
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
226
|
+
expect(server.messagesToConsume.pendingItems.length).toBe(0);
|
|
227
|
+
// Send REQ message with filters
|
|
228
|
+
filters.next([{ kinds: [1] }]);
|
|
229
|
+
// Wait for the REQ message to be sent
|
|
230
|
+
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
231
|
+
});
|
|
232
|
+
it("should update filters if filters are provided as an observable", async () => {
|
|
233
|
+
const filters = new Subject();
|
|
234
|
+
subscribeSpyTo(relay.req(filters, "sub1"));
|
|
235
|
+
// Send REQ message with filters
|
|
236
|
+
filters.next([{ kinds: [1] }]);
|
|
237
|
+
// Should send REQ message with new filters
|
|
238
|
+
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
239
|
+
// Send REQ message with filters
|
|
240
|
+
filters.next([{ kinds: [2] }]);
|
|
241
|
+
// Should send new REQ message with new filters
|
|
242
|
+
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [2] }]);
|
|
243
|
+
// It should not send CLOSE message
|
|
244
|
+
await expect(server.messages).not.toContain(["CLOSE", "sub1"]);
|
|
245
|
+
});
|
|
246
|
+
it("should complete if filters are provided as an observable that completes", async () => {
|
|
247
|
+
const filters = new Subject();
|
|
248
|
+
const sub = subscribeSpyTo(relay.req(filters, "sub1"));
|
|
249
|
+
// Send REQ message with filters
|
|
250
|
+
filters.next([{ kinds: [1] }]);
|
|
251
|
+
// Complete the observable
|
|
252
|
+
filters.complete();
|
|
253
|
+
await sub.onComplete();
|
|
254
|
+
expect(sub.receivedComplete()).toBe(true);
|
|
255
|
+
});
|
|
211
256
|
});
|
|
212
257
|
describe("event", () => {
|
|
213
258
|
it("should wait for authentication if relay responds with auth-required", async () => {
|
package/dist/group.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
|
-
import { IGroup, IRelay, PublishResponse, SubscriptionResponse, PublishOptions, RequestOptions, SubscriptionOptions } from "./types.js";
|
|
3
|
+
import { IGroup, IRelay, PublishResponse, SubscriptionResponse, PublishOptions, RequestOptions, SubscriptionOptions, FilterInput } from "./types.js";
|
|
4
4
|
export declare class RelayGroup implements IGroup {
|
|
5
5
|
relays: IRelay[];
|
|
6
6
|
constructor(relays: IRelay[]);
|
|
7
7
|
/** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
|
|
8
8
|
protected mergeEOSE(...requests: Observable<SubscriptionResponse>[]): Observable<import("nostr-tools").Event | "EOSE">;
|
|
9
9
|
/** Make a request to all relays */
|
|
10
|
-
req(filters:
|
|
10
|
+
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
11
11
|
/** Send an event to all relays */
|
|
12
12
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
13
13
|
/** Publish an event to all relays with retries ( default 3 retries ) */
|
|
14
14
|
publish(event: NostrEvent, opts?: PublishOptions): Observable<PublishResponse>;
|
|
15
15
|
/** Request events from all relays with retries ( default 3 retries ) */
|
|
16
|
-
request(filters:
|
|
16
|
+
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
17
17
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
18
|
-
subscription(filters:
|
|
18
|
+
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
19
19
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as exports from "../index.js";
|
|
3
|
+
describe("exports", () => {
|
|
4
|
+
it("should export the expected functions", () => {
|
|
5
|
+
expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
|
|
6
|
+
[
|
|
7
|
+
"completeOnEose",
|
|
8
|
+
"markFromRelay",
|
|
9
|
+
"onlyEvents",
|
|
10
|
+
"storeEvents",
|
|
11
|
+
"toEventStore",
|
|
12
|
+
]
|
|
13
|
+
`);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EventStore } from "applesauce-core";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { FakeUser } from "../../__tests__/fake-user.js";
|
|
4
|
+
import { of } from "rxjs";
|
|
5
|
+
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
6
|
+
import { toEventStore } from "../to-event-store.js";
|
|
7
|
+
const user = new FakeUser();
|
|
8
|
+
describe("toEventStore", () => {
|
|
9
|
+
it("should remove duplicate events", () => {
|
|
10
|
+
const eventStore = new EventStore();
|
|
11
|
+
const event = user.note("original content");
|
|
12
|
+
const source = of(event, { ...event }, { ...event }, { ...event });
|
|
13
|
+
const spy = subscribeSpyTo(source.pipe(toEventStore(eventStore)));
|
|
14
|
+
expect(spy.getValuesLength()).toBe(1);
|
|
15
|
+
expect(spy.getValueAt(0).length).toBe(1);
|
|
16
|
+
expect(spy.getValueAt(0)[0]).toBe(event);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IEventStore } from "applesauce-core";
|
|
2
2
|
import { MonoTypeOperatorFunction } from "rxjs";
|
|
3
3
|
import { SubscriptionResponse } from "../types.js";
|
|
4
|
-
/** Sends all events to the event store */
|
|
4
|
+
/** Sends all events to the event store but does not remove duplicates */
|
|
5
5
|
export declare function storeEvents(eventStore: IEventStore): MonoTypeOperatorFunction<SubscriptionResponse>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tap } from "rxjs";
|
|
2
|
-
/** Sends all events to the event store */
|
|
2
|
+
/** Sends all events to the event store but does not remove duplicates */
|
|
3
3
|
export function storeEvents(eventStore) {
|
|
4
4
|
return (source) => {
|
|
5
5
|
return source.pipe(tap((event) => typeof event !== "string" && eventStore.add(event)));
|
|
@@ -2,5 +2,8 @@ import { OperatorFunction } from "rxjs";
|
|
|
2
2
|
import { IEventStore } from "applesauce-core";
|
|
3
3
|
import { NostrEvent } from "nostr-tools";
|
|
4
4
|
import { SubscriptionResponse } from "../types.js";
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Adds all events to event store and returns a deduplicated timeline when EOSE is received
|
|
7
|
+
* @deprecated use `mapEventsToStore` and `mapEventsToTimeline` instead
|
|
8
|
+
*/
|
|
6
9
|
export declare function toEventStore(eventStore: IEventStore): OperatorFunction<SubscriptionResponse, NostrEvent[]>;
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
1
|
+
import { mapEventsToStore, mapEventsToTimeline } from "applesauce-core/observable";
|
|
3
2
|
import { completeOnEose } from "./complete-on-eose.js";
|
|
4
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Adds all events to event store and returns a deduplicated timeline when EOSE is received
|
|
5
|
+
* @deprecated use `mapEventsToStore` and `mapEventsToTimeline` instead
|
|
6
|
+
*/
|
|
5
7
|
export function toEventStore(eventStore) {
|
|
6
8
|
return (source) => source.pipe(
|
|
7
9
|
// Complete when there are not events
|
|
8
10
|
completeOnEose(),
|
|
11
|
+
// Save events to store and remove duplicates
|
|
12
|
+
mapEventsToStore(eventStore, true),
|
|
9
13
|
// Add the events to an array
|
|
10
|
-
|
|
11
|
-
// Get the current instance of this event
|
|
12
|
-
let e = eventStore.add(event);
|
|
13
|
-
// If its not in the timeline, add it
|
|
14
|
-
if (events.includes(e))
|
|
15
|
-
return events;
|
|
16
|
-
else
|
|
17
|
-
return insertEventIntoDescendingList(events, e);
|
|
18
|
-
}, []));
|
|
14
|
+
mapEventsToTimeline());
|
|
19
15
|
}
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type NostrEvent } from "nostr-tools";
|
|
2
2
|
import { BehaviorSubject, Observable } from "rxjs";
|
|
3
3
|
import { RelayGroup } from "./group.js";
|
|
4
4
|
import { Relay, RelayOptions } from "./relay.js";
|
|
5
|
-
import { IPool, PublishResponse, PublishOptions, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
5
|
+
import { IPool, PublishResponse, PublishOptions, RequestOptions, SubscriptionOptions, SubscriptionResponse, FilterInput } from "./types.js";
|
|
6
6
|
export declare class RelayPool implements IPool {
|
|
7
7
|
options?: RelayOptions | undefined;
|
|
8
8
|
groups$: BehaviorSubject<Map<string, RelayGroup>>;
|
|
@@ -18,13 +18,13 @@ export declare class RelayPool implements IPool {
|
|
|
18
18
|
/** Create a group of relays */
|
|
19
19
|
group(relays: string[]): RelayGroup;
|
|
20
20
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
21
|
-
req(relays: string[], filters:
|
|
21
|
+
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
22
22
|
/** Send an EVENT message to multiple relays */
|
|
23
23
|
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
24
24
|
/** Publish an event to multiple relays */
|
|
25
25
|
publish(relays: string[], event: NostrEvent, opts?: PublishOptions): Observable<PublishResponse>;
|
|
26
26
|
/** Request events from multiple relays */
|
|
27
|
-
request(relays: string[], filters:
|
|
27
|
+
request(relays: string[], filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
28
28
|
/** Open a subscription to multiple relays */
|
|
29
|
-
subscription(relays: string[], filters:
|
|
29
|
+
subscription(relays: string[], filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
30
30
|
}
|
package/dist/relay.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type Filter, type NostrEvent } from "nostr-tools";
|
|
|
3
3
|
import { BehaviorSubject, Observable } from "rxjs";
|
|
4
4
|
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
5
5
|
import { RelayInformation } from "nostr-tools/nip11";
|
|
6
|
-
import { AuthSigner, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
6
|
+
import { AuthSigner, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
7
7
|
/** An error that is thrown when a REQ is closed from the relay side */
|
|
8
8
|
export declare class ReqCloseError extends Error {
|
|
9
9
|
}
|
|
@@ -61,12 +61,12 @@ export declare class Relay implements IRelay {
|
|
|
61
61
|
protected authRequiredForReq: Observable<boolean>;
|
|
62
62
|
protected authRequiredForEvent: Observable<boolean>;
|
|
63
63
|
protected resetState(): void;
|
|
64
|
-
/** An internal observable that is responsible for watching all messages and updating state */
|
|
64
|
+
/** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
|
|
65
65
|
protected watchTower: Observable<never>;
|
|
66
66
|
constructor(url: string, opts?: RelayOptions);
|
|
67
67
|
/** Set ready = false and start the reconnect timer */
|
|
68
68
|
protected startReconnectTimer(error: Error | CloseEvent): void;
|
|
69
|
-
/** Wait for
|
|
69
|
+
/** Wait for authentication state, make connection and then wait for authentication if required */
|
|
70
70
|
protected waitForAuth<T extends unknown = unknown>(requireAuth: Observable<boolean>, observable: Observable<T>): Observable<T>;
|
|
71
71
|
/** Wait for the relay to be ready to accept connections */
|
|
72
72
|
protected waitForReady<T extends unknown = unknown>(observable: Observable<T>): Observable<T>;
|
|
@@ -74,7 +74,7 @@ export declare class Relay implements IRelay {
|
|
|
74
74
|
/** Send a message to the relay */
|
|
75
75
|
next(message: any): void;
|
|
76
76
|
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
77
|
-
req(filters:
|
|
77
|
+
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
78
78
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
79
79
|
event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
|
|
80
80
|
/** send and AUTH message */
|
package/dist/relay.js
CHANGED
|
@@ -2,7 +2,7 @@ import { logger } from "applesauce-core";
|
|
|
2
2
|
import { simpleTimeout } from "applesauce-core/observable";
|
|
3
3
|
import { nanoid } from "nanoid";
|
|
4
4
|
import { nip42 } from "nostr-tools";
|
|
5
|
-
import { BehaviorSubject, catchError, combineLatest, defer, filter, from, ignoreElements, map, merge, mergeMap, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, tap, throwError, timeout, timer, } from "rxjs";
|
|
5
|
+
import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, ignoreElements, isObservable, map, merge, mergeMap, mergeWith, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
|
|
6
6
|
import { webSocket } from "rxjs/webSocket";
|
|
7
7
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
8
8
|
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
@@ -85,7 +85,7 @@ export class Relay {
|
|
|
85
85
|
if (this.receivedAuthRequiredForEvent.value)
|
|
86
86
|
this.receivedAuthRequiredForEvent.next(false);
|
|
87
87
|
}
|
|
88
|
-
/** An internal observable that is responsible for watching all messages and updating state */
|
|
88
|
+
/** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
|
|
89
89
|
watchTower;
|
|
90
90
|
constructor(url, opts) {
|
|
91
91
|
this.url = url;
|
|
@@ -190,11 +190,13 @@ export class Relay {
|
|
|
190
190
|
.pipe(take(1))
|
|
191
191
|
.subscribe(() => this.ready$.next(true));
|
|
192
192
|
}
|
|
193
|
-
/** Wait for
|
|
193
|
+
/** Wait for authentication state, make connection and then wait for authentication if required */
|
|
194
194
|
waitForAuth(
|
|
195
195
|
// NOTE: require BehaviorSubject so it always has a value
|
|
196
196
|
requireAuth, observable) {
|
|
197
197
|
return combineLatest([requireAuth, this.authenticated$]).pipe(
|
|
198
|
+
// Once the auth state is known, make a connection and watch for auth challenges
|
|
199
|
+
mergeWith(this.watchTower),
|
|
198
200
|
// wait for auth not required or authenticated
|
|
199
201
|
filter(([required, authenticated]) => !required || authenticated),
|
|
200
202
|
// complete after the first value so this does not repeat
|
|
@@ -225,10 +227,24 @@ export class Relay {
|
|
|
225
227
|
}
|
|
226
228
|
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
227
229
|
req(filters, id = nanoid()) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
230
|
+
// Convert filters input into an observable, if its a normal value merge it with NEVER so it never completes
|
|
231
|
+
const input = isObservable(filters) ? filters : merge(of(filters), NEVER);
|
|
232
|
+
// Create an observable that completes when the upstream observable completes
|
|
233
|
+
const complete = input.pipe(ignoreElements(), endWith(null));
|
|
234
|
+
// Create an observable that filters responses from the relay to just the ones for this REQ
|
|
235
|
+
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id));
|
|
236
|
+
// Create an observable that controls sending the filters and closing the REQ
|
|
237
|
+
const control = input.pipe(
|
|
238
|
+
// Send the filters when they change
|
|
239
|
+
tap((filters) => this.socket.next(Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters])),
|
|
240
|
+
// Close the req when unsubscribed
|
|
241
|
+
finalize(() => this.socket.next(["CLOSE", id])),
|
|
242
|
+
// Once filters have been sent, switch to listening for messages
|
|
243
|
+
switchMap(() => messages));
|
|
244
|
+
// Start the watch tower with the observables
|
|
245
|
+
const observable = merge(this.watchTower, control).pipe(
|
|
246
|
+
// Complete the subscription when the input is completed
|
|
247
|
+
takeUntil(complete),
|
|
232
248
|
// Map the messages to events, EOSE, or throw an error
|
|
233
249
|
map((message) => {
|
|
234
250
|
if (message[0] === "EOSE")
|
|
@@ -270,10 +286,8 @@ export class Relay {
|
|
|
270
286
|
// format OK message
|
|
271
287
|
map((m) => ({ ok: m[2], message: m[3], from: this.url })));
|
|
272
288
|
});
|
|
273
|
-
// Start the watch tower
|
|
274
|
-
const
|
|
275
|
-
// Add complete operators
|
|
276
|
-
const observable = withWatchTower.pipe(
|
|
289
|
+
// Start the watch tower and add complete operators
|
|
290
|
+
const observable = merge(this.watchTower, base).pipe(
|
|
277
291
|
// complete on first value
|
|
278
292
|
take(1),
|
|
279
293
|
// listen for OK auth-required
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventTemplate, Filter, NostrEvent } from "nostr-tools";
|
|
1
|
+
import { type EventTemplate, type Filter, type NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
import { WebSocketSubject } from "rxjs/webSocket";
|
|
4
4
|
export type SubscriptionResponse = NostrEvent | "EOSE";
|
|
@@ -28,11 +28,13 @@ export type SubscriptionOptions = {
|
|
|
28
28
|
export type AuthSigner = {
|
|
29
29
|
signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
|
|
30
30
|
};
|
|
31
|
+
/** The type of input the REQ method accepts */
|
|
32
|
+
export type FilterInput = Filter | Filter[] | Observable<Filter | Filter[]>;
|
|
31
33
|
export interface Nip01Actions {
|
|
32
34
|
/** Send an EVENT message */
|
|
33
35
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
34
36
|
/** Send a REQ message */
|
|
35
|
-
req(filters:
|
|
37
|
+
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
36
38
|
}
|
|
37
39
|
export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
|
|
38
40
|
url: string;
|
|
@@ -52,12 +54,12 @@ export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
|
|
|
52
54
|
retries?: number;
|
|
53
55
|
}): Observable<PublishResponse>;
|
|
54
56
|
/** Send a REQ message with retries */
|
|
55
|
-
request(filters:
|
|
57
|
+
request(filters: FilterInput, opts?: {
|
|
56
58
|
id?: string;
|
|
57
59
|
retries?: number;
|
|
58
60
|
}): Observable<NostrEvent>;
|
|
59
61
|
/** Open a subscription with retries */
|
|
60
|
-
subscription(filters:
|
|
62
|
+
subscription(filters: FilterInput, opts?: {
|
|
61
63
|
id?: string;
|
|
62
64
|
retries?: number;
|
|
63
65
|
}): Observable<SubscriptionResponse>;
|
|
@@ -68,12 +70,12 @@ export interface IGroup extends Nip01Actions {
|
|
|
68
70
|
retries?: number;
|
|
69
71
|
}): Observable<PublishResponse>;
|
|
70
72
|
/** Send a REQ message with retries */
|
|
71
|
-
request(filters:
|
|
73
|
+
request(filters: FilterInput, opts?: {
|
|
72
74
|
id?: string;
|
|
73
75
|
retries?: number;
|
|
74
76
|
}): Observable<NostrEvent>;
|
|
75
77
|
/** Open a subscription with retries */
|
|
76
|
-
subscription(filters:
|
|
78
|
+
subscription(filters: FilterInput, opts?: {
|
|
77
79
|
id?: string;
|
|
78
80
|
retries?: number;
|
|
79
81
|
}): Observable<SubscriptionResponse>;
|
|
@@ -82,7 +84,7 @@ export interface IPool {
|
|
|
82
84
|
/** Send an EVENT message */
|
|
83
85
|
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
84
86
|
/** Send a REQ message */
|
|
85
|
-
req(relays: string[], filters:
|
|
87
|
+
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
86
88
|
/** Get or create a relay */
|
|
87
89
|
relay(url: string): IRelay;
|
|
88
90
|
/** Create a relay group */
|
|
@@ -92,12 +94,12 @@ export interface IPool {
|
|
|
92
94
|
retries?: number;
|
|
93
95
|
}): Observable<PublishResponse>;
|
|
94
96
|
/** Send a REQ message to relays with retries */
|
|
95
|
-
request(relays: string[], filters:
|
|
97
|
+
request(relays: string[], filters: FilterInput, opts?: {
|
|
96
98
|
id?: string;
|
|
97
99
|
retries?: number;
|
|
98
100
|
}): Observable<NostrEvent>;
|
|
99
101
|
/** Open a subscription to relays with retries */
|
|
100
|
-
subscription(relays: string[], filters:
|
|
102
|
+
subscription(relays: string[], filters: FilterInput, opts?: {
|
|
101
103
|
id?: string;
|
|
102
104
|
retries?: number;
|
|
103
105
|
}): Observable<SubscriptionResponse>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20250526151506",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,13 +54,14 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@noble/hashes": "^1.7.1",
|
|
57
|
-
"applesauce-core": "
|
|
57
|
+
"applesauce-core": "0.0.0-next-20250526151506",
|
|
58
58
|
"nanoid": "^5.0.9",
|
|
59
|
-
"nostr-tools": "^2.
|
|
59
|
+
"nostr-tools": "^2.13",
|
|
60
60
|
"rxjs": "^7.8.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
64
|
+
"applesauce-signers": "0.0.0-next-20250526151506",
|
|
64
65
|
"@vitest/expect": "^3.1.1",
|
|
65
66
|
"typescript": "^5.7.3",
|
|
66
67
|
"vitest": "^3.1.1",
|