applesauce-relay 1.2.0 → 2.1.1
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/negentropy.d.ts +2 -2
- package/dist/negentropy.js +1 -1
- package/dist/operators/complete-on-eose.js +2 -5
- 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/relay.d.ts +2 -2
- package/dist/relay.js +9 -8
- package/package.json +5 -4
- package/dist/__tests__/auth.test.d.ts +0 -1
- package/dist/__tests__/auth.test.js +0 -111
- package/dist/__tests__/exports.test.d.ts +0 -1
- package/dist/__tests__/exports.test.js +0 -19
- package/dist/__tests__/group.test.d.ts +0 -1
- package/dist/__tests__/group.test.js +0 -106
- package/dist/__tests__/pool.test.d.ts +0 -1
- package/dist/__tests__/pool.test.js +0 -96
- package/dist/__tests__/relay.test.d.ts +0 -1
- package/dist/__tests__/relay.test.js +0 -644
- package/dist/operators/__tests__/exports.test.d.ts +0 -1
- package/dist/operators/__tests__/exports.test.js +0 -15
package/dist/negentropy.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IEventStoreRead } from "applesauce-core";
|
|
2
2
|
import { Filter } from "nostr-tools";
|
|
3
3
|
import { MultiplexWebSocket } from "./types.js";
|
|
4
4
|
import { NegentropyStorageVector } from "./lib/negentropy.js";
|
|
5
|
-
export declare function buildStorageFromFilter(store:
|
|
5
|
+
export declare function buildStorageFromFilter(store: IEventStoreRead, filter: Filter): NegentropyStorageVector;
|
|
6
6
|
export declare function buildStorageVector(items: {
|
|
7
7
|
id: string;
|
|
8
8
|
created_at: number;
|
package/dist/negentropy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Negentropy, NegentropyStorageVector } from "./lib/negentropy.js";
|
|
|
5
5
|
const log = logger.extend("negentropy");
|
|
6
6
|
export function buildStorageFromFilter(store, filter) {
|
|
7
7
|
const storage = new NegentropyStorageVector();
|
|
8
|
-
for (const event of store.
|
|
8
|
+
for (const event of store.getByFilters(filter))
|
|
9
9
|
storage.insert(event.created_at, event.id);
|
|
10
10
|
storage.seal();
|
|
11
11
|
return storage;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { takeWhile } from "rxjs";
|
|
2
|
-
export function completeOnEose(
|
|
3
|
-
|
|
4
|
-
return takeWhile((m) => m !== "EOSE", true);
|
|
5
|
-
else
|
|
6
|
-
return takeWhile((m) => m !== "EOSE", false);
|
|
2
|
+
export function completeOnEose(inclusive) {
|
|
3
|
+
return takeWhile((m) => m !== "EOSE", inclusive);
|
|
7
4
|
}
|
|
@@ -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/relay.d.ts
CHANGED
|
@@ -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>;
|
package/dist/relay.js
CHANGED
|
@@ -2,8 +2,9 @@ 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, endWith, filter, finalize, from, ignoreElements, isObservable, map, merge, mergeMap, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, 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
|
+
import { ensureHttpURL } from "applesauce-core/helpers";
|
|
7
8
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
8
9
|
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
9
10
|
/** An error that is thrown when a REQ is closed from the relay side */
|
|
@@ -85,7 +86,7 @@ export class Relay {
|
|
|
85
86
|
if (this.receivedAuthRequiredForEvent.value)
|
|
86
87
|
this.receivedAuthRequiredForEvent.next(false);
|
|
87
88
|
}
|
|
88
|
-
/** An internal observable that is responsible for watching all messages and updating state */
|
|
89
|
+
/** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
|
|
89
90
|
watchTower;
|
|
90
91
|
constructor(url, opts) {
|
|
91
92
|
this.url = url;
|
|
@@ -190,11 +191,13 @@ export class Relay {
|
|
|
190
191
|
.pipe(take(1))
|
|
191
192
|
.subscribe(() => this.ready$.next(true));
|
|
192
193
|
}
|
|
193
|
-
/** Wait for
|
|
194
|
+
/** Wait for authentication state, make connection and then wait for authentication if required */
|
|
194
195
|
waitForAuth(
|
|
195
196
|
// NOTE: require BehaviorSubject so it always has a value
|
|
196
197
|
requireAuth, observable) {
|
|
197
198
|
return combineLatest([requireAuth, this.authenticated$]).pipe(
|
|
199
|
+
// Once the auth state is known, make a connection and watch for auth challenges
|
|
200
|
+
mergeWith(this.watchTower),
|
|
198
201
|
// wait for auth not required or authenticated
|
|
199
202
|
filter(([required, authenticated]) => !required || authenticated),
|
|
200
203
|
// complete after the first value so this does not repeat
|
|
@@ -284,10 +287,8 @@ export class Relay {
|
|
|
284
287
|
// format OK message
|
|
285
288
|
map((m) => ({ ok: m[2], message: m[3], from: this.url })));
|
|
286
289
|
});
|
|
287
|
-
// Start the watch tower
|
|
288
|
-
const
|
|
289
|
-
// Add complete operators
|
|
290
|
-
const observable = withWatchTower.pipe(
|
|
290
|
+
// Start the watch tower and add complete operators
|
|
291
|
+
const observable = merge(this.watchTower, base).pipe(
|
|
291
292
|
// complete on first value
|
|
292
293
|
take(1),
|
|
293
294
|
// listen for OK auth-required
|
|
@@ -351,7 +352,7 @@ export class Relay {
|
|
|
351
352
|
}
|
|
352
353
|
/** Static method to fetch the NIP-11 information document for a relay */
|
|
353
354
|
static fetchInformationDocument(url) {
|
|
354
|
-
return from(fetch(url, { headers: { Accept: "application/nostr+json" } }).then((res) => res.json())).pipe(
|
|
355
|
+
return from(fetch(ensureHttpURL(url), { headers: { Accept: "application/nostr+json" } }).then((res) => res.json())).pipe(
|
|
355
356
|
// if the fetch fails, return null
|
|
356
357
|
catchError(() => of(null)),
|
|
357
358
|
// timeout after 10s
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,16 +54,17 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@noble/hashes": "^1.7.1",
|
|
57
|
-
"applesauce-core": "^
|
|
57
|
+
"applesauce-core": "^2.0.0",
|
|
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": "^2.0.0",
|
|
64
65
|
"@vitest/expect": "^3.1.1",
|
|
65
66
|
"typescript": "^5.7.3",
|
|
66
|
-
"vitest": "^3.
|
|
67
|
+
"vitest": "^3.2.3",
|
|
67
68
|
"vitest-websocket-mock": "^0.5.0"
|
|
68
69
|
},
|
|
69
70
|
"funding": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
3
|
-
import { WS } from "vitest-websocket-mock";
|
|
4
|
-
import { Relay } from "../relay.js";
|
|
5
|
-
let server;
|
|
6
|
-
let relay;
|
|
7
|
-
beforeEach(async () => {
|
|
8
|
-
server = new WS("wss://test", { jsonProtocol: true });
|
|
9
|
-
relay = new Relay("wss://test");
|
|
10
|
-
// Create a persistent subscription to keep the connection open
|
|
11
|
-
// @ts-expect-error
|
|
12
|
-
subscribeSpyTo(relay.socket);
|
|
13
|
-
});
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
server.close();
|
|
16
|
-
// Wait for server to close to prevent memory leaks
|
|
17
|
-
await WS.clean();
|
|
18
|
-
});
|
|
19
|
-
const mockEvent = {
|
|
20
|
-
kind: 1,
|
|
21
|
-
id: "00007641c9c3e65a71843933a44a18060c7c267a4f9169efa3735ece45c8f621",
|
|
22
|
-
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
|
23
|
-
created_at: 1743712795,
|
|
24
|
-
tags: [["nonce", "13835058055282167643", "16"]],
|
|
25
|
-
content: "This is just stupid: https://codestr.fiatjaf.com/",
|
|
26
|
-
sig: "5a57b5a12bba4b7cf0121077b1421cf4df402c5c221376c076204fc4f7519e28ce6508f26ddc132c406ccfe6e62cc6db857b96c788565cdca9674fe9a0710ac2",
|
|
27
|
-
};
|
|
28
|
-
describe("event", () => {
|
|
29
|
-
it("should wait for auth before sending EVENT if auth-required received", async () => {
|
|
30
|
-
// Create first event subscription
|
|
31
|
-
const spy1 = subscribeSpyTo(relay.event(mockEvent));
|
|
32
|
-
// Verify EVENT was sent
|
|
33
|
-
const firstEventMessage = await server.nextMessage;
|
|
34
|
-
expect(firstEventMessage).toEqual(["EVENT", mockEvent]);
|
|
35
|
-
// Send auth-required response
|
|
36
|
-
server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
|
|
37
|
-
// Create second event subscription - this should not send EVENT yet
|
|
38
|
-
const spy2 = subscribeSpyTo(relay.event(mockEvent));
|
|
39
|
-
// Should not have received any messages
|
|
40
|
-
expect(server.messages.length).toBe(1);
|
|
41
|
-
// Send AUTH challenge
|
|
42
|
-
server.send(["AUTH", "challenge-string"]);
|
|
43
|
-
// Send auth event
|
|
44
|
-
const authEvent = { ...mockEvent, id: "auth-id" };
|
|
45
|
-
subscribeSpyTo(relay.auth(authEvent));
|
|
46
|
-
// Verify AUTH was sent
|
|
47
|
-
const authMessage = await server.nextMessage;
|
|
48
|
-
expect(authMessage).toEqual(["AUTH", authEvent]);
|
|
49
|
-
// Send successful auth response
|
|
50
|
-
server.send(["OK", authEvent.id, true, ""]);
|
|
51
|
-
// Now the second EVENT should be sent
|
|
52
|
-
const secondEventMessage = await server.nextMessage;
|
|
53
|
-
expect(secondEventMessage).toEqual(["EVENT", mockEvent]);
|
|
54
|
-
// Send OK response for second event
|
|
55
|
-
server.send(["OK", mockEvent.id, true, ""]);
|
|
56
|
-
expect(spy1.getLastValue()).toEqual({
|
|
57
|
-
ok: false,
|
|
58
|
-
message: "auth-required: need to authenticate",
|
|
59
|
-
from: "wss://test",
|
|
60
|
-
});
|
|
61
|
-
expect(spy2.getLastValue()).toEqual({ ok: true, message: "", from: "wss://test" });
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
describe("req", () => {
|
|
65
|
-
it("should wait for auth before sending REQ if auth-required received", async () => {
|
|
66
|
-
// Create first REQ subscription
|
|
67
|
-
const filters = [{ kinds: [1], limit: 10 }];
|
|
68
|
-
subscribeSpyTo(relay.req(filters, "sub1"), { expectErrors: true });
|
|
69
|
-
// Verify REQ was sent
|
|
70
|
-
const firstReqMessage = await server.nextMessage;
|
|
71
|
-
expect(firstReqMessage).toEqual(["REQ", "sub1", ...filters]);
|
|
72
|
-
// Send auth-required response
|
|
73
|
-
server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
|
|
74
|
-
// Consume the client CLOSE message for sub1
|
|
75
|
-
await server.nextMessage;
|
|
76
|
-
// Create second REQ subscription - this should not send REQ yet
|
|
77
|
-
subscribeSpyTo(relay.req(filters, "sub2"), { expectErrors: true });
|
|
78
|
-
// Should not have received any messages
|
|
79
|
-
expect(server.messages).not.toContain(["REQ", "sub2", ...filters]);
|
|
80
|
-
// Send AUTH challenge
|
|
81
|
-
server.send(["AUTH", "challenge-string"]);
|
|
82
|
-
// Send auth event
|
|
83
|
-
const authEvent = { ...mockEvent, id: "auth-id" };
|
|
84
|
-
subscribeSpyTo(relay.auth(authEvent));
|
|
85
|
-
// Verify AUTH was sent
|
|
86
|
-
const authMessage = await server.nextMessage;
|
|
87
|
-
expect(authMessage).toEqual(["AUTH", authEvent]);
|
|
88
|
-
// Send successful auth response
|
|
89
|
-
server.send(["OK", authEvent.id, true, ""]);
|
|
90
|
-
// Now the second REQ should be sent
|
|
91
|
-
const secondReqMessage = await server.nextMessage;
|
|
92
|
-
expect(secondReqMessage).toEqual(["REQ", "sub2", ...filters]);
|
|
93
|
-
// Send some events for the second subscription
|
|
94
|
-
server.send(["EVENT", "sub2", mockEvent]);
|
|
95
|
-
server.send(["EOSE", "sub2"]);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
describe("auth", () => {
|
|
99
|
-
it("should set authenticated state after successful AUTH challenge response", async () => {
|
|
100
|
-
// Send AUTH challenge
|
|
101
|
-
server.send(["AUTH", "challenge-string"]);
|
|
102
|
-
// Send auth event response
|
|
103
|
-
const authEvent = { ...mockEvent, id: "auth-id" };
|
|
104
|
-
subscribeSpyTo(relay.auth(authEvent));
|
|
105
|
-
// Verify AUTH was sent
|
|
106
|
-
await expect(server).toReceiveMessage(["AUTH", authEvent]);
|
|
107
|
-
// Send successful auth response
|
|
108
|
-
server.send(["OK", authEvent.id, true, ""]);
|
|
109
|
-
expect(relay.authenticated).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
3
|
-
import { WS } from "vitest-websocket-mock";
|
|
4
|
-
import { Relay } from "../relay.js";
|
|
5
|
-
import { RelayGroup } from "../group.js";
|
|
6
|
-
import { of } from "rxjs";
|
|
7
|
-
let mockRelay1;
|
|
8
|
-
let mockRelay2;
|
|
9
|
-
let relay1;
|
|
10
|
-
let relay2;
|
|
11
|
-
let group;
|
|
12
|
-
let mockEvent;
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
// Create mock relays
|
|
15
|
-
mockRelay1 = new WS("wss://relay1.test", { jsonProtocol: true });
|
|
16
|
-
mockRelay2 = new WS("wss://relay2.test", { jsonProtocol: true });
|
|
17
|
-
// Mock empty information document
|
|
18
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of(null));
|
|
19
|
-
// Create relays
|
|
20
|
-
relay1 = new Relay("wss://relay1.test");
|
|
21
|
-
relay2 = new Relay("wss://relay2.test");
|
|
22
|
-
// Create group
|
|
23
|
-
group = new RelayGroup([relay1, relay2]);
|
|
24
|
-
mockEvent = {
|
|
25
|
-
kind: 1,
|
|
26
|
-
id: "test-id",
|
|
27
|
-
pubkey: "test-pubkey",
|
|
28
|
-
created_at: 1234567890,
|
|
29
|
-
tags: [],
|
|
30
|
-
content: "test content",
|
|
31
|
-
sig: "test-sig",
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
afterEach(async () => {
|
|
35
|
-
mockRelay1.close();
|
|
36
|
-
mockRelay2.close();
|
|
37
|
-
await WS.clean();
|
|
38
|
-
});
|
|
39
|
-
describe("req", () => {
|
|
40
|
-
it("should make requests to multiple relays", async () => {
|
|
41
|
-
group.req([{ kinds: [1] }], "test-sub").subscribe();
|
|
42
|
-
await expect(mockRelay1).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
|
|
43
|
-
await expect(mockRelay2).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
|
|
44
|
-
});
|
|
45
|
-
it("should emit events from all relays", async () => {
|
|
46
|
-
const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
|
|
47
|
-
await expect(mockRelay1).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
|
|
48
|
-
await expect(mockRelay2).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
|
|
49
|
-
mockRelay1.send(["EVENT", "test-sub", { ...mockEvent, id: "1" }]);
|
|
50
|
-
mockRelay2.send(["EVENT", "test-sub", { ...mockEvent, id: "2" }]);
|
|
51
|
-
expect(spy.getValues()).toEqual([expect.objectContaining({ id: "1" }), expect.objectContaining({ id: "2" })]);
|
|
52
|
-
});
|
|
53
|
-
it("should only emit EOSE once all relays have emitted EOSE", async () => {
|
|
54
|
-
const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
|
|
55
|
-
mockRelay1.send(["EOSE", "test-sub"]);
|
|
56
|
-
expect(spy.getValues()).not.toContain("EOSE");
|
|
57
|
-
mockRelay2.send(["EOSE", "test-sub"]);
|
|
58
|
-
expect(spy.getValues()).toContain("EOSE");
|
|
59
|
-
});
|
|
60
|
-
it("should ignore relays that have an error", async () => {
|
|
61
|
-
const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
|
|
62
|
-
mockRelay1.error();
|
|
63
|
-
mockRelay2.send(["EVENT", "test-sub", mockEvent]);
|
|
64
|
-
mockRelay2.send(["EOSE", "test-sub"]);
|
|
65
|
-
expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
66
|
-
});
|
|
67
|
-
it("should emit EOSE if all relays error", async () => {
|
|
68
|
-
const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
|
|
69
|
-
mockRelay1.error();
|
|
70
|
-
mockRelay2.error();
|
|
71
|
-
expect(spy.getValues()).toEqual(["EOSE"]);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe("event", () => {
|
|
75
|
-
it("should send EVENT to all relays in the group", async () => {
|
|
76
|
-
group.event(mockEvent).subscribe();
|
|
77
|
-
await expect(mockRelay1).toReceiveMessage(["EVENT", mockEvent]);
|
|
78
|
-
await expect(mockRelay2).toReceiveMessage(["EVENT", mockEvent]);
|
|
79
|
-
});
|
|
80
|
-
it("should emit OK messages from all relays", async () => {
|
|
81
|
-
const spy = subscribeSpyTo(group.event(mockEvent));
|
|
82
|
-
mockRelay1.send(["OK", mockEvent.id, true, ""]);
|
|
83
|
-
mockRelay2.send(["OK", mockEvent.id, true, ""]);
|
|
84
|
-
expect(spy.getValues()).toEqual([
|
|
85
|
-
expect.objectContaining({ ok: true, from: "wss://relay1.test", message: "" }),
|
|
86
|
-
expect.objectContaining({ ok: true, from: "wss://relay2.test", message: "" }),
|
|
87
|
-
]);
|
|
88
|
-
});
|
|
89
|
-
it("should complete when all relays have sent OK messages", async () => {
|
|
90
|
-
const spy = subscribeSpyTo(group.event(mockEvent));
|
|
91
|
-
mockRelay1.send(["OK", mockEvent.id, true, ""]);
|
|
92
|
-
expect(spy.receivedComplete()).toBe(false);
|
|
93
|
-
mockRelay2.send(["OK", mockEvent.id, true, ""]);
|
|
94
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
it("should handle relay errors and still complete", async () => {
|
|
97
|
-
const spy = subscribeSpyTo(group.event(mockEvent));
|
|
98
|
-
mockRelay1.error();
|
|
99
|
-
mockRelay2.send(["OK", mockEvent.id, true, ""]);
|
|
100
|
-
expect(spy.getValues()).toEqual([
|
|
101
|
-
expect.objectContaining({ ok: false, from: "wss://relay1.test", message: "Unknown error" }),
|
|
102
|
-
expect.objectContaining({ ok: true, from: "wss://relay2.test", message: "" }),
|
|
103
|
-
]);
|
|
104
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { expect, beforeEach, afterEach, describe, it } from "vitest";
|
|
2
|
-
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
3
|
-
import { WS } from "vitest-websocket-mock";
|
|
4
|
-
import { RelayPool } from "../pool.js";
|
|
5
|
-
let pool;
|
|
6
|
-
let mockServer1;
|
|
7
|
-
let mockServer2;
|
|
8
|
-
let mockEvent;
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
// Create mock WebSocket servers
|
|
11
|
-
mockServer1 = new WS("wss://relay1.example.com");
|
|
12
|
-
mockServer2 = new WS("wss://relay2.example.com");
|
|
13
|
-
pool = new RelayPool();
|
|
14
|
-
mockEvent = {
|
|
15
|
-
kind: 1,
|
|
16
|
-
id: "test-id",
|
|
17
|
-
pubkey: "test-pubkey",
|
|
18
|
-
created_at: 1743712795,
|
|
19
|
-
tags: [],
|
|
20
|
-
content: "test content",
|
|
21
|
-
sig: "test-sig",
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
mockServer1.close();
|
|
26
|
-
mockServer2.close();
|
|
27
|
-
// Clean up WebSocket mocks
|
|
28
|
-
await WS.clean();
|
|
29
|
-
});
|
|
30
|
-
describe("relay", () => {
|
|
31
|
-
it("should create a new relay", () => {
|
|
32
|
-
const url = "wss://relay1.example.com/";
|
|
33
|
-
const relay = pool.relay(url);
|
|
34
|
-
expect(relay).toBeDefined();
|
|
35
|
-
expect(pool.relays.get(url)).toBe(relay);
|
|
36
|
-
});
|
|
37
|
-
it("should return existing relay connection if already exists", () => {
|
|
38
|
-
const url = "wss://relay1.example.com";
|
|
39
|
-
const relay1 = pool.relay(url);
|
|
40
|
-
const relay2 = pool.relay(url);
|
|
41
|
-
expect(relay1).toBe(relay2);
|
|
42
|
-
expect(pool.relays.size).toBe(1);
|
|
43
|
-
});
|
|
44
|
-
it("should normalize relay urls", () => {
|
|
45
|
-
expect(pool.relay("wss://relay.example.com")).toBe(pool.relay("wss://relay.example.com/"));
|
|
46
|
-
expect(pool.relay("wss://relay.example.com:443")).toBe(pool.relay("wss://relay.example.com/"));
|
|
47
|
-
expect(pool.relay("ws://relay.example.com:80")).toBe(pool.relay("ws://relay.example.com/"));
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe("group", () => {
|
|
51
|
-
it("should create a relay group", () => {
|
|
52
|
-
const urls = ["wss://relay1.example.com/", "wss://relay2.example.com/"];
|
|
53
|
-
const group = pool.group(urls);
|
|
54
|
-
expect(group).toBeDefined();
|
|
55
|
-
expect(pool.groups.get(urls.sort().join(","))).toBe(group);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe("req", () => {
|
|
59
|
-
it("should send subscription to multiple relays", async () => {
|
|
60
|
-
const urls = ["wss://relay1.example.com", "wss://relay2.example.com"];
|
|
61
|
-
const filters = { kinds: [1] };
|
|
62
|
-
const spy = subscribeSpyTo(pool.req(urls, filters));
|
|
63
|
-
// Verify REQ was sent to both relays
|
|
64
|
-
const req1 = await mockServer1.nextMessage;
|
|
65
|
-
const req2 = await mockServer2.nextMessage;
|
|
66
|
-
// Both messages should be REQ messages with the same filter
|
|
67
|
-
expect(JSON.parse(req1)[0]).toBe("REQ");
|
|
68
|
-
expect(JSON.parse(req2)[0]).toBe("REQ");
|
|
69
|
-
expect(JSON.parse(req1)[2]).toEqual(filters);
|
|
70
|
-
expect(JSON.parse(req2)[2]).toEqual(filters);
|
|
71
|
-
// Send EVENT from first relay
|
|
72
|
-
mockServer1.send(JSON.stringify(["EVENT", JSON.parse(req1)[1], mockEvent]));
|
|
73
|
-
// Send EOSE from both relays
|
|
74
|
-
mockServer1.send(JSON.stringify(["EOSE", JSON.parse(req1)[1]]));
|
|
75
|
-
mockServer2.send(JSON.stringify(["EOSE", JSON.parse(req2)[1]]));
|
|
76
|
-
expect(spy.getValues()).toContainEqual(expect.objectContaining(mockEvent));
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe("event", () => {
|
|
80
|
-
it("should publish to multiple relays", async () => {
|
|
81
|
-
const urls = ["wss://relay1.example.com/", "wss://relay2.example.com/"];
|
|
82
|
-
const spy = subscribeSpyTo(pool.event(urls, mockEvent));
|
|
83
|
-
// Verify EVENT was sent to both relays
|
|
84
|
-
const event1 = await mockServer1.nextMessage;
|
|
85
|
-
const event2 = await mockServer2.nextMessage;
|
|
86
|
-
expect(JSON.parse(event1)).toEqual(["EVENT", mockEvent]);
|
|
87
|
-
expect(JSON.parse(event2)).toEqual(["EVENT", mockEvent]);
|
|
88
|
-
// Send OK responses from both relays
|
|
89
|
-
mockServer1.send(JSON.stringify(["OK", mockEvent.id, true, ""]));
|
|
90
|
-
mockServer2.send(JSON.stringify(["OK", mockEvent.id, true, ""]));
|
|
91
|
-
expect(spy.getValues()).toEqual([
|
|
92
|
-
{ ok: true, from: "wss://relay1.example.com/", message: "" },
|
|
93
|
-
{ ok: true, from: "wss://relay2.example.com/", message: "" },
|
|
94
|
-
]);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
3
|
-
import { getSeenRelays } from "applesauce-core/helpers";
|
|
4
|
-
import { WS } from "vitest-websocket-mock";
|
|
5
|
-
import { Relay } from "../relay.js";
|
|
6
|
-
import { filter } from "rxjs/operators";
|
|
7
|
-
import { firstValueFrom, of, Subject, throwError, timer } from "rxjs";
|
|
8
|
-
const defaultMockInfo = {
|
|
9
|
-
name: "Test Relay",
|
|
10
|
-
description: "Test Relay Description",
|
|
11
|
-
pubkey: "testpubkey",
|
|
12
|
-
contact: "test@example.com",
|
|
13
|
-
supported_nips: [1, 2, 3],
|
|
14
|
-
software: "test-software",
|
|
15
|
-
version: "1.0.0",
|
|
16
|
-
};
|
|
17
|
-
let server;
|
|
18
|
-
let relay;
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
// Mock empty information document
|
|
21
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of(null));
|
|
22
|
-
// Create mock relay
|
|
23
|
-
server = new WS("wss://test", { jsonProtocol: true });
|
|
24
|
-
// Create relay
|
|
25
|
-
relay = new Relay("wss://test");
|
|
26
|
-
relay.keepAlive = 0;
|
|
27
|
-
});
|
|
28
|
-
// Wait for server to close to prevent memory leaks
|
|
29
|
-
afterEach(async () => {
|
|
30
|
-
await WS.clean();
|
|
31
|
-
vi.clearAllTimers();
|
|
32
|
-
vi.useRealTimers();
|
|
33
|
-
});
|
|
34
|
-
const mockEvent = {
|
|
35
|
-
kind: 1,
|
|
36
|
-
id: "00007641c9c3e65a71843933a44a18060c7c267a4f9169efa3735ece45c8f621",
|
|
37
|
-
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
|
38
|
-
created_at: 1743712795,
|
|
39
|
-
tags: [["nonce", "13835058055282167643", "16"]],
|
|
40
|
-
content: "This is just stupid: https://codestr.fiatjaf.com/",
|
|
41
|
-
sig: "5a57b5a12bba4b7cf0121077b1421cf4df402c5c221376c076204fc4f7519e28ce6508f26ddc132c406ccfe6e62cc6db857b96c788565cdca9674fe9a0710ac2",
|
|
42
|
-
};
|
|
43
|
-
describe("req", () => {
|
|
44
|
-
it("should trigger connection to relay", async () => {
|
|
45
|
-
subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
46
|
-
// Wait for connection
|
|
47
|
-
await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
|
|
48
|
-
expect(relay.connected).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
it("should send expected messages to relay", async () => {
|
|
51
|
-
subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
52
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
53
|
-
});
|
|
54
|
-
it("should not close the REQ when EOSE is received", async () => {
|
|
55
|
-
// Create subscription that completes after first EOSE
|
|
56
|
-
const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
57
|
-
// Verify REQ was sent
|
|
58
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
59
|
-
// Send EOSE to complete subscription
|
|
60
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
61
|
-
server.send(["EOSE", "sub1"]);
|
|
62
|
-
// Verify the subscription did not complete
|
|
63
|
-
expect(sub.receivedComplete()).toBe(false);
|
|
64
|
-
expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
65
|
-
});
|
|
66
|
-
it("should send CLOSE when unsubscribed", async () => {
|
|
67
|
-
// Create subscription that completes after first EOSE
|
|
68
|
-
const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
69
|
-
// Verify REQ was sent
|
|
70
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
71
|
-
// Complete the subscription
|
|
72
|
-
sub.unsubscribe();
|
|
73
|
-
// Verify CLOSE was sent
|
|
74
|
-
await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
|
|
75
|
-
});
|
|
76
|
-
it("should emit nostr event and EOSE", async () => {
|
|
77
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
78
|
-
await server.connected;
|
|
79
|
-
// Send EVENT message
|
|
80
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
81
|
-
// Send EOSE message
|
|
82
|
-
server.send(["EOSE", "sub1"]);
|
|
83
|
-
expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
84
|
-
});
|
|
85
|
-
it("should ignore EVENT and EOSE messages that do not match subscription id", async () => {
|
|
86
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
87
|
-
await server.connected;
|
|
88
|
-
// Send EVENT message with wrong subscription id
|
|
89
|
-
server.send(["EVENT", "wrong_sub", mockEvent]);
|
|
90
|
-
// Send EOSE message with wrong subscription id
|
|
91
|
-
server.send(["EOSE", "wrong_sub"]);
|
|
92
|
-
// Send EVENT message with correct subscription id
|
|
93
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
94
|
-
// Send EOSE message with correct subscription id
|
|
95
|
-
server.send(["EOSE", "sub1"]);
|
|
96
|
-
expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
97
|
-
});
|
|
98
|
-
it("should mark events with their source relay", async () => {
|
|
99
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
100
|
-
await server.connected;
|
|
101
|
-
// Send EVENT message
|
|
102
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
103
|
-
// Get the received event
|
|
104
|
-
const receivedEvent = spy.getValues()[0];
|
|
105
|
-
// Verify the event was marked as seen from this relay
|
|
106
|
-
expect(getSeenRelays(receivedEvent)).toContain("wss://test");
|
|
107
|
-
});
|
|
108
|
-
it("should error subscription when CLOSED message is received", async () => {
|
|
109
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
|
|
110
|
-
await server.connected;
|
|
111
|
-
// Send CLOSED message for the subscription
|
|
112
|
-
server.send(["CLOSED", "sub1", "reason"]);
|
|
113
|
-
// Verify the subscription completed
|
|
114
|
-
expect(spy.receivedError()).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
it("should not send multiple REQ messages for multiple subscriptions", async () => {
|
|
117
|
-
const sub = relay.req([{ kinds: [1] }], "sub1");
|
|
118
|
-
sub.subscribe();
|
|
119
|
-
sub.subscribe();
|
|
120
|
-
// Wait for all messages to be sent
|
|
121
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
122
|
-
expect(server.messages).toEqual([["REQ", "sub1", { kinds: [1] }]]);
|
|
123
|
-
});
|
|
124
|
-
it("should wait for authentication if relay responds with auth-required", async () => {
|
|
125
|
-
// First subscription to trigger auth-required
|
|
126
|
-
const firstSub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
|
|
127
|
-
await server.nextMessage;
|
|
128
|
-
// Send CLOSED message with auth-required reason
|
|
129
|
-
server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
|
|
130
|
-
// wait for complete
|
|
131
|
-
await firstSub.onError();
|
|
132
|
-
await server.nextMessage;
|
|
133
|
-
// Create a second subscription that should wait for auth
|
|
134
|
-
const secondSub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub2"), { expectErrors: true });
|
|
135
|
-
// Verify no REQ message was sent yet (waiting for auth)
|
|
136
|
-
expect(server).not.toHaveReceivedMessages(["REQ", "sub2", { kinds: [1] }]);
|
|
137
|
-
// Simulate successful authentication
|
|
138
|
-
relay.authenticated$.next(true);
|
|
139
|
-
// Now the REQ should be sent
|
|
140
|
-
await expect(server).toReceiveMessage(["REQ", "sub2", { kinds: [1] }]);
|
|
141
|
-
// Send EVENT and EOSE to complete the subscription
|
|
142
|
-
server.send(["EVENT", "sub2", mockEvent]);
|
|
143
|
-
server.send(["EOSE", "sub2"]);
|
|
144
|
-
// Verify the second subscription received the event and EOSE
|
|
145
|
-
expect(secondSub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
146
|
-
});
|
|
147
|
-
it("should wait for authentication if relay info document has limitations.auth_required = true", async () => {
|
|
148
|
-
// Mock the fetchInformationDocument method to return a document with auth_required = true
|
|
149
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
|
|
150
|
-
name: "Auth Required Relay",
|
|
151
|
-
description: "A relay that requires authentication",
|
|
152
|
-
pubkey: "",
|
|
153
|
-
contact: "",
|
|
154
|
-
supported_nips: [1, 2, 4],
|
|
155
|
-
software: "",
|
|
156
|
-
version: "",
|
|
157
|
-
limitation: {
|
|
158
|
-
auth_required: true,
|
|
159
|
-
},
|
|
160
|
-
}));
|
|
161
|
-
// Create a subscription that should wait for auth
|
|
162
|
-
const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
163
|
-
// Wait 10ms to ensure the information document is fetched
|
|
164
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
165
|
-
// Verify no REQ message was sent yet (waiting for auth)
|
|
166
|
-
expect(server).not.toHaveReceivedMessages(["REQ", "sub1", { kinds: [1] }]);
|
|
167
|
-
// Simulate successful authentication
|
|
168
|
-
relay.authenticated$.next(true);
|
|
169
|
-
// Now the REQ should be sent
|
|
170
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
171
|
-
// Send EVENT and EOSE to complete the subscription
|
|
172
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
173
|
-
server.send(["EOSE", "sub1"]);
|
|
174
|
-
// Verify the subscription received the event and EOSE
|
|
175
|
-
expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
176
|
-
});
|
|
177
|
-
it("should throw error if relay closes connection with error", async () => {
|
|
178
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
|
|
179
|
-
await server.connected;
|
|
180
|
-
// Send CLOSE message with error
|
|
181
|
-
server.error({
|
|
182
|
-
reason: "error message",
|
|
183
|
-
code: 1000,
|
|
184
|
-
wasClean: false,
|
|
185
|
-
});
|
|
186
|
-
// Verify the subscription completed with an error
|
|
187
|
-
expect(spy.receivedError()).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
it("should not return EOSE while waiting for the relay to be ready", async () => {
|
|
190
|
-
vi.useFakeTimers();
|
|
191
|
-
// @ts-expect-error
|
|
192
|
-
relay.ready$.next(false);
|
|
193
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
|
|
194
|
-
// Fast-forward time by 20 seconds
|
|
195
|
-
await vi.advanceTimersByTimeAsync(20000);
|
|
196
|
-
expect(spy.receivedComplete()).toBe(false);
|
|
197
|
-
expect(spy.receivedError()).toBe(false);
|
|
198
|
-
expect(spy.receivedNext()).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
it("should wait when relay isn't ready", async () => {
|
|
201
|
-
// @ts-expect-error
|
|
202
|
-
relay.ready$.next(false);
|
|
203
|
-
subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
204
|
-
// Wait 10ms to ensure the relay didn't receive anything
|
|
205
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
206
|
-
expect(server.messages.length).toBe(0);
|
|
207
|
-
// @ts-expect-error
|
|
208
|
-
relay.ready$.next(true);
|
|
209
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
210
|
-
});
|
|
211
|
-
it("should wait for filters if filters are provided as an observable", async () => {
|
|
212
|
-
const filters = new Subject();
|
|
213
|
-
subscribeSpyTo(relay.req(filters, "sub1"));
|
|
214
|
-
// Wait 10sm and ensure no messages were sent yet
|
|
215
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
216
|
-
expect(server.messagesToConsume.pendingItems.length).toBe(0);
|
|
217
|
-
// Send REQ message with filters
|
|
218
|
-
filters.next([{ kinds: [1] }]);
|
|
219
|
-
// Wait for the REQ message to be sent
|
|
220
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
221
|
-
});
|
|
222
|
-
it("should update filters if filters are provided as an observable", async () => {
|
|
223
|
-
const filters = new Subject();
|
|
224
|
-
subscribeSpyTo(relay.req(filters, "sub1"));
|
|
225
|
-
// Send REQ message with filters
|
|
226
|
-
filters.next([{ kinds: [1] }]);
|
|
227
|
-
// Should send REQ message with new filters
|
|
228
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
229
|
-
// Send REQ message with filters
|
|
230
|
-
filters.next([{ kinds: [2] }]);
|
|
231
|
-
// Should send new REQ message with new filters
|
|
232
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [2] }]);
|
|
233
|
-
// It should not send CLOSE message
|
|
234
|
-
await expect(server.messages).not.toContain(["CLOSE", "sub1"]);
|
|
235
|
-
});
|
|
236
|
-
it("should complete if filters are provided as an observable that completes", async () => {
|
|
237
|
-
const filters = new Subject();
|
|
238
|
-
const sub = subscribeSpyTo(relay.req(filters, "sub1"));
|
|
239
|
-
// Send REQ message with filters
|
|
240
|
-
filters.next([{ kinds: [1] }]);
|
|
241
|
-
// Complete the observable
|
|
242
|
-
filters.complete();
|
|
243
|
-
await sub.onComplete();
|
|
244
|
-
expect(sub.receivedComplete()).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
describe("event", () => {
|
|
248
|
-
it("should wait for authentication if relay responds with auth-required", async () => {
|
|
249
|
-
// First event to trigger auth-required
|
|
250
|
-
const firstSpy = subscribeSpyTo(relay.event(mockEvent));
|
|
251
|
-
await expect(server).toReceiveMessage(["EVENT", mockEvent]);
|
|
252
|
-
// Send OK with auth-required message
|
|
253
|
-
server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
|
|
254
|
-
await firstSpy.onComplete();
|
|
255
|
-
// Create a second event that should wait for auth
|
|
256
|
-
const secondSpy = subscribeSpyTo(relay.event(mockEvent));
|
|
257
|
-
// Verify no EVENT message was sent yet (waiting for auth)
|
|
258
|
-
expect(server).not.toHaveReceivedMessages(["EVENT", mockEvent]);
|
|
259
|
-
// Simulate successful authentication
|
|
260
|
-
relay.authenticated$.next(true);
|
|
261
|
-
// Now the EVENT should be sent
|
|
262
|
-
await expect(server).toReceiveMessage(["EVENT", mockEvent]);
|
|
263
|
-
// Send OK response to complete the event
|
|
264
|
-
server.send(["OK", mockEvent.id, true, ""]);
|
|
265
|
-
// Verify the second event completed successfully
|
|
266
|
-
await secondSpy.onComplete();
|
|
267
|
-
expect(secondSpy.receivedComplete()).toBe(true);
|
|
268
|
-
});
|
|
269
|
-
it("should wait for authentication if relay info document has limitations.auth_required = true", async () => {
|
|
270
|
-
// Mock the fetchInformationDocument method to return a document with auth_required = true
|
|
271
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
|
|
272
|
-
name: "Auth Required Relay",
|
|
273
|
-
description: "A relay that requires authentication",
|
|
274
|
-
pubkey: "",
|
|
275
|
-
contact: "",
|
|
276
|
-
supported_nips: [1, 2, 4],
|
|
277
|
-
software: "",
|
|
278
|
-
version: "",
|
|
279
|
-
limitation: {
|
|
280
|
-
auth_required: true,
|
|
281
|
-
},
|
|
282
|
-
}));
|
|
283
|
-
// Create a subscription that should wait for auth
|
|
284
|
-
const sub = subscribeSpyTo(relay.event(mockEvent));
|
|
285
|
-
// Wait 10ms to ensure the information document is fetched
|
|
286
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
287
|
-
// Verify no REQ message was sent yet (waiting for auth)
|
|
288
|
-
expect(server).not.toHaveReceivedMessages(["EVENT", mockEvent]);
|
|
289
|
-
// Simulate successful authentication
|
|
290
|
-
relay.authenticated$.next(true);
|
|
291
|
-
// Now the REQ should be sent
|
|
292
|
-
await expect(server).toReceiveMessage(["EVENT", mockEvent]);
|
|
293
|
-
// Send EVENT and EOSE to complete the subscription
|
|
294
|
-
server.send(["OK", mockEvent.id, true, ""]);
|
|
295
|
-
// Verify the subscription completed
|
|
296
|
-
await sub.onComplete();
|
|
297
|
-
expect(sub.receivedComplete()).toBe(true);
|
|
298
|
-
});
|
|
299
|
-
it("should trigger connection to relay", async () => {
|
|
300
|
-
subscribeSpyTo(relay.event(mockEvent));
|
|
301
|
-
// Wait for connection
|
|
302
|
-
await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
|
|
303
|
-
expect(relay.connected).toBe(true);
|
|
304
|
-
});
|
|
305
|
-
it("observable should complete when matching OK response received", async () => {
|
|
306
|
-
const spy = subscribeSpyTo(relay.event(mockEvent));
|
|
307
|
-
// Verify EVENT message was sent
|
|
308
|
-
expect(await server.nextMessage).toEqual(["EVENT", mockEvent]);
|
|
309
|
-
// Send matching OK response
|
|
310
|
-
server.send(["OK", mockEvent.id, true, ""]);
|
|
311
|
-
await spy.onComplete();
|
|
312
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
313
|
-
});
|
|
314
|
-
it("should ignore OK responses for different events", async () => {
|
|
315
|
-
const spy = subscribeSpyTo(relay.event(mockEvent));
|
|
316
|
-
await server.connected;
|
|
317
|
-
// Send non-matching OK response
|
|
318
|
-
server.send(["OK", "different_id", true, ""]);
|
|
319
|
-
expect(spy.receivedComplete()).toBe(false);
|
|
320
|
-
// Send matching OK response
|
|
321
|
-
server.send(["OK", mockEvent.id, true, ""]);
|
|
322
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
323
|
-
});
|
|
324
|
-
it("should send EVENT message to relay", async () => {
|
|
325
|
-
relay.event(mockEvent).subscribe();
|
|
326
|
-
expect(await server.nextMessage).toEqual(["EVENT", mockEvent]);
|
|
327
|
-
});
|
|
328
|
-
it("should complete with error if no OK received within 10s", async () => {
|
|
329
|
-
vi.useFakeTimers();
|
|
330
|
-
const spy = subscribeSpyTo(relay.event(mockEvent));
|
|
331
|
-
// Fast-forward time by 10 seconds
|
|
332
|
-
await vi.advanceTimersByTimeAsync(10000);
|
|
333
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
334
|
-
expect(spy.getLastValue()).toEqual({ ok: false, from: "wss://test", message: "Timeout" });
|
|
335
|
-
});
|
|
336
|
-
it("should throw error if relay closes connection with error", async () => {
|
|
337
|
-
const spy = subscribeSpyTo(relay.event(mockEvent), { expectErrors: true });
|
|
338
|
-
await server.connected;
|
|
339
|
-
// Send CLOSE message with error
|
|
340
|
-
server.error({
|
|
341
|
-
reason: "error message",
|
|
342
|
-
code: 1000,
|
|
343
|
-
wasClean: false,
|
|
344
|
-
});
|
|
345
|
-
// Verify the subscription completed with an error
|
|
346
|
-
expect(spy.receivedError()).toBe(true);
|
|
347
|
-
});
|
|
348
|
-
it("should not throw a timeout error while waiting for the relay to be ready", async () => {
|
|
349
|
-
vi.useFakeTimers();
|
|
350
|
-
// @ts-expect-error
|
|
351
|
-
relay.ready$.next(false);
|
|
352
|
-
const spy = subscribeSpyTo(relay.event(mockEvent), { expectErrors: true });
|
|
353
|
-
// Fast-forward time by 20 seconds
|
|
354
|
-
await vi.advanceTimersByTimeAsync(20000);
|
|
355
|
-
expect(spy.receivedComplete()).toBe(false);
|
|
356
|
-
expect(spy.receivedError()).toBe(false);
|
|
357
|
-
});
|
|
358
|
-
it("should wait when relay isn't ready", async () => {
|
|
359
|
-
// @ts-expect-error
|
|
360
|
-
relay.ready$.next(false);
|
|
361
|
-
subscribeSpyTo(relay.event(mockEvent));
|
|
362
|
-
// Wait 10ms to ensure the relay didn't receive anything
|
|
363
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
364
|
-
expect(server.messages.length).toBe(0);
|
|
365
|
-
// @ts-expect-error
|
|
366
|
-
relay.ready$.next(true);
|
|
367
|
-
await expect(server).toReceiveMessage(["EVENT", mockEvent]);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
describe("notices$", () => {
|
|
371
|
-
it("should not trigger connection to relay", async () => {
|
|
372
|
-
subscribeSpyTo(relay.notices$);
|
|
373
|
-
expect(relay.connected).toBe(false);
|
|
374
|
-
});
|
|
375
|
-
it("should accumulate notices in notices$ state", async () => {
|
|
376
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
377
|
-
// Send multiple NOTICE messages
|
|
378
|
-
server.send(["NOTICE", "Notice 1"]);
|
|
379
|
-
server.send(["NOTICE", "Notice 2"]);
|
|
380
|
-
server.send(["NOTICE", "Notice 3"]);
|
|
381
|
-
// Verify the notices state contains all messages
|
|
382
|
-
expect(relay.notices$.value).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
|
|
383
|
-
});
|
|
384
|
-
it("should ignore non-NOTICE messages", async () => {
|
|
385
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
386
|
-
server.send(["NOTICE", "Important notice"]);
|
|
387
|
-
server.send(["OTHER", "other message"]);
|
|
388
|
-
// Verify only NOTICE messages are in the state
|
|
389
|
-
expect(relay.notices$.value).toEqual(["Important notice"]);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
describe("notice$", () => {
|
|
393
|
-
it("should not trigger connection to relay", async () => {
|
|
394
|
-
subscribeSpyTo(relay.notice$);
|
|
395
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
396
|
-
expect(relay.connected).toBe(false);
|
|
397
|
-
});
|
|
398
|
-
it("should emit NOTICE messages when they are received", async () => {
|
|
399
|
-
const spy = subscribeSpyTo(relay.notice$);
|
|
400
|
-
// Start connection
|
|
401
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
402
|
-
// Send multiple NOTICE messages
|
|
403
|
-
server.send(["NOTICE", "Notice 1"]);
|
|
404
|
-
server.send(["NOTICE", "Notice 2"]);
|
|
405
|
-
server.send(["NOTICE", "Notice 3"]);
|
|
406
|
-
// Verify the notices state contains all messages
|
|
407
|
-
expect(spy.getValues()).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
|
|
408
|
-
});
|
|
409
|
-
it("should ignore non-NOTICE messages", async () => {
|
|
410
|
-
const spy = subscribeSpyTo(relay.notice$);
|
|
411
|
-
// Start connection
|
|
412
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
413
|
-
server.send(["NOTICE", "Important notice"]);
|
|
414
|
-
server.send(["OTHER", "other message"]);
|
|
415
|
-
// Verify only NOTICE messages are in the state
|
|
416
|
-
expect(spy.getValues()).toEqual(["Important notice"]);
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
describe("message$", () => {
|
|
420
|
-
it("should not trigger connection to relay", async () => {
|
|
421
|
-
subscribeSpyTo(relay.message$);
|
|
422
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
423
|
-
expect(relay.connected).toBe(false);
|
|
424
|
-
});
|
|
425
|
-
it("should emit all messages when they are received", async () => {
|
|
426
|
-
const spy = subscribeSpyTo(relay.message$);
|
|
427
|
-
// Start connection
|
|
428
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
429
|
-
// Send multiple NOTICE messages
|
|
430
|
-
server.send(["NOTICE", "Notice 1"]);
|
|
431
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
432
|
-
server.send(["EOSE", "sub1"]);
|
|
433
|
-
// Verify the notices state contains all messages
|
|
434
|
-
expect(spy.getValues()).toEqual([
|
|
435
|
-
["NOTICE", "Notice 1"],
|
|
436
|
-
["EVENT", "sub1", mockEvent],
|
|
437
|
-
["EOSE", "sub1"],
|
|
438
|
-
]);
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
describe("challenge$", () => {
|
|
442
|
-
it("should not trigger connection to relay", async () => {
|
|
443
|
-
subscribeSpyTo(relay.challenge$);
|
|
444
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
445
|
-
expect(relay.connected).toBe(false);
|
|
446
|
-
});
|
|
447
|
-
it("should set challenge$ when AUTH message received", async () => {
|
|
448
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
449
|
-
// Send AUTH message with challenge string
|
|
450
|
-
server.send(["AUTH", "challenge-string-123"]);
|
|
451
|
-
// Verify challenge$ was set
|
|
452
|
-
expect(relay.challenge$.value).toBe("challenge-string-123");
|
|
453
|
-
});
|
|
454
|
-
it("should ignore non-AUTH messages", async () => {
|
|
455
|
-
subscribeSpyTo(relay.req({ kinds: [1] }));
|
|
456
|
-
server.send(["NOTICE", "Not a challenge"]);
|
|
457
|
-
server.send(["OTHER", "other message"]);
|
|
458
|
-
// Verify challenge$ remains null
|
|
459
|
-
expect(relay.challenge$.value).toBe(null);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
describe("information$", () => {
|
|
463
|
-
it("should fetch information document when information$ is subscribed to", async () => {
|
|
464
|
-
// Mock the fetchInformationDocument method
|
|
465
|
-
const mockInfo = { ...defaultMockInfo, limitation: { auth_required: false } };
|
|
466
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(of(mockInfo));
|
|
467
|
-
// Subscribe to information$
|
|
468
|
-
const sub = subscribeSpyTo(relay.information$);
|
|
469
|
-
// Verify fetchInformationDocument was called with the relay URL
|
|
470
|
-
expect(Relay.fetchInformationDocument).toHaveBeenCalledWith(relay.url);
|
|
471
|
-
// Verify the information was emitted
|
|
472
|
-
expect(sub.getLastValue()).toEqual(mockInfo);
|
|
473
|
-
});
|
|
474
|
-
it("should return null when fetchInformationDocument fails", async () => {
|
|
475
|
-
// Mock the fetchInformationDocument method to throw an error
|
|
476
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(throwError(() => new Error("Failed to fetch")));
|
|
477
|
-
// Subscribe to information$
|
|
478
|
-
const sub = subscribeSpyTo(relay.information$);
|
|
479
|
-
// Verify fetchInformationDocument was called
|
|
480
|
-
expect(Relay.fetchInformationDocument).toHaveBeenCalled();
|
|
481
|
-
// Verify null was emitted
|
|
482
|
-
expect(sub.getLastValue()).toBeNull();
|
|
483
|
-
});
|
|
484
|
-
it("should cache the information document", async () => {
|
|
485
|
-
// Mock the fetchInformationDocument method
|
|
486
|
-
const mockInfo = { ...defaultMockInfo, limitation: { auth_required: true } };
|
|
487
|
-
vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(of(mockInfo));
|
|
488
|
-
// Subscribe to information$ multiple times
|
|
489
|
-
const sub1 = subscribeSpyTo(relay.information$);
|
|
490
|
-
const sub2 = subscribeSpyTo(relay.information$);
|
|
491
|
-
// Verify fetchInformationDocument was called only once
|
|
492
|
-
expect(Relay.fetchInformationDocument).toHaveBeenCalledTimes(1);
|
|
493
|
-
// Verify both subscriptions received the same information
|
|
494
|
-
expect(sub1.getLastValue()).toEqual(mockInfo);
|
|
495
|
-
expect(sub2.getLastValue()).toEqual(mockInfo);
|
|
496
|
-
// Verify the internal state was updated
|
|
497
|
-
expect(relay.information).toEqual(mockInfo);
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
describe("createReconnectTimer", () => {
|
|
501
|
-
it("should create a reconnect timer when relay closes with error", async () => {
|
|
502
|
-
const reconnectTimer = vi.fn().mockReturnValue(timer(1000));
|
|
503
|
-
vi.spyOn(Relay, "createReconnectTimer").mockReturnValue(reconnectTimer);
|
|
504
|
-
relay = new Relay("wss://test");
|
|
505
|
-
const spy = subscribeSpyTo(relay.req([{ kinds: [1] }]), { expectErrors: true });
|
|
506
|
-
// Send CLOSE message with error
|
|
507
|
-
server.error({
|
|
508
|
-
reason: "error message",
|
|
509
|
-
code: 1000,
|
|
510
|
-
wasClean: false,
|
|
511
|
-
});
|
|
512
|
-
// Verify the subscription errored
|
|
513
|
-
expect(spy.receivedError()).toBe(true);
|
|
514
|
-
expect(reconnectTimer).toHaveBeenCalledWith(expect.any(Error), 0);
|
|
515
|
-
});
|
|
516
|
-
it("should set ready$ to false until the reconnect timer completes", async () => {
|
|
517
|
-
vi.useFakeTimers();
|
|
518
|
-
const reconnectTimer = vi.fn().mockReturnValue(timer(1000));
|
|
519
|
-
vi.spyOn(Relay, "createReconnectTimer").mockReturnValue(reconnectTimer);
|
|
520
|
-
relay = new Relay("wss://test");
|
|
521
|
-
subscribeSpyTo(relay.req([{ kinds: [1] }]), { expectErrors: true });
|
|
522
|
-
// Send CLOSE message with error
|
|
523
|
-
server.error({
|
|
524
|
-
reason: "error message",
|
|
525
|
-
code: 1000,
|
|
526
|
-
wasClean: false,
|
|
527
|
-
});
|
|
528
|
-
// @ts-expect-error
|
|
529
|
-
expect(relay.ready$.value).toBe(false);
|
|
530
|
-
// Fast-forward time by 10ms
|
|
531
|
-
await vi.advanceTimersByTimeAsync(5000);
|
|
532
|
-
// @ts-expect-error
|
|
533
|
-
expect(relay.ready$.value).toBe(true);
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
describe("publish", () => {
|
|
537
|
-
it("should retry when auth-required is received and authentication is completed", async () => {
|
|
538
|
-
// First attempt to publish
|
|
539
|
-
const spy = subscribeSpyTo(relay.publish(mockEvent));
|
|
540
|
-
// Verify EVENT was sent
|
|
541
|
-
await expect(server).toReceiveMessage(["EVENT", mockEvent]);
|
|
542
|
-
// Send auth-required response
|
|
543
|
-
server.send(["AUTH", "challenge-string"]);
|
|
544
|
-
server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
|
|
545
|
-
// Send auth event
|
|
546
|
-
const authEvent = { ...mockEvent, id: "auth-id" };
|
|
547
|
-
subscribeSpyTo(relay.auth(authEvent));
|
|
548
|
-
// Verify AUTH was sent
|
|
549
|
-
await expect(server).toReceiveMessage(["AUTH", authEvent]);
|
|
550
|
-
// Send successful auth response
|
|
551
|
-
server.send(["OK", authEvent.id, true, ""]);
|
|
552
|
-
// Wait for the event to be sent again
|
|
553
|
-
await expect(server).toReceiveMessage(["EVENT", mockEvent]);
|
|
554
|
-
// Send successful response for the retried event
|
|
555
|
-
server.send(["OK", mockEvent.id, true, ""]);
|
|
556
|
-
// Verify the final result is successful
|
|
557
|
-
expect(spy.getLastValue()).toEqual({ ok: true, message: "", from: "wss://test" });
|
|
558
|
-
});
|
|
559
|
-
it("should error after max retries", async () => {
|
|
560
|
-
const spy = subscribeSpyTo(relay.publish(mockEvent, { retries: 0 }), { expectErrors: true });
|
|
561
|
-
// Close with error
|
|
562
|
-
server.error({ reason: "error message", code: 1000, wasClean: false });
|
|
563
|
-
// Verify the subscription errored
|
|
564
|
-
expect(spy.receivedError()).toBe(true);
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
describe("request", () => {
|
|
568
|
-
it("should retry when auth-required is received and authentication is completed", async () => {
|
|
569
|
-
// First attempt to request
|
|
570
|
-
const spy = subscribeSpyTo(relay.request({ kinds: [1] }, { id: "sub1" }));
|
|
571
|
-
// Verify REQ was sent
|
|
572
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
573
|
-
// Send auth-required response
|
|
574
|
-
server.send(["AUTH", "challenge-string"]);
|
|
575
|
-
server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
|
|
576
|
-
// Wait for subscription to close
|
|
577
|
-
await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
|
|
578
|
-
// Send auth event
|
|
579
|
-
const authEvent = { ...mockEvent, id: "auth-id" };
|
|
580
|
-
const authSpy = subscribeSpyTo(relay.auth(authEvent));
|
|
581
|
-
// Verify AUTH was sent
|
|
582
|
-
await expect(server).toReceiveMessage(["AUTH", authEvent]);
|
|
583
|
-
server.send(["OK", authEvent.id, true, ""]);
|
|
584
|
-
// Wait for auth to complete
|
|
585
|
-
await authSpy.onComplete();
|
|
586
|
-
// Wait for retry
|
|
587
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
588
|
-
// Send response
|
|
589
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
590
|
-
server.send(["EOSE", "sub1"]);
|
|
591
|
-
// Verify the final result is successful
|
|
592
|
-
expect(spy.getLastValue()).toEqual(expect.objectContaining(mockEvent));
|
|
593
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
describe("subscription", () => {
|
|
597
|
-
it("should retry when auth-required is received and authentication is completed", async () => {
|
|
598
|
-
// First attempt to request
|
|
599
|
-
const spy = subscribeSpyTo(relay.subscription({ kinds: [1] }, { id: "sub1" }));
|
|
600
|
-
// Verify REQ was sent
|
|
601
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
602
|
-
// Send auth-required response
|
|
603
|
-
server.send(["AUTH", "challenge-string"]);
|
|
604
|
-
server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
|
|
605
|
-
// Wait for subscription to close
|
|
606
|
-
await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
|
|
607
|
-
// Send auth event
|
|
608
|
-
const authEvent = { ...mockEvent, id: "auth-id" };
|
|
609
|
-
const authSpy = subscribeSpyTo(relay.auth(authEvent));
|
|
610
|
-
// Verify AUTH was sent
|
|
611
|
-
await expect(server).toReceiveMessage(["AUTH", authEvent]);
|
|
612
|
-
server.send(["OK", authEvent.id, true, ""]);
|
|
613
|
-
// Wait for auth to complete
|
|
614
|
-
await authSpy.onComplete();
|
|
615
|
-
// Wait for retry
|
|
616
|
-
await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
|
|
617
|
-
// Send response
|
|
618
|
-
server.send(["EVENT", "sub1", mockEvent]);
|
|
619
|
-
server.send(["EOSE", "sub1"]);
|
|
620
|
-
// Verify the final result is successful
|
|
621
|
-
expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
|
|
622
|
-
expect(spy.receivedComplete()).toBe(false);
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
// describe("keepAlive", () => {
|
|
626
|
-
// it("should close the socket connection after keepAlive timeout", async () => {
|
|
627
|
-
// vi.useFakeTimers();
|
|
628
|
-
// // Set a short keepAlive timeout for testing
|
|
629
|
-
// relay.keepAlive = 100; // 100ms for quick testing
|
|
630
|
-
// // Subscribe to the relay to ensure it is active
|
|
631
|
-
// const sub = subscribeSpyTo(relay.req([{ kinds: [1] }]));
|
|
632
|
-
// // Wait for connection
|
|
633
|
-
// await server.connected;
|
|
634
|
-
// // Close the subscription
|
|
635
|
-
// sub.unsubscribe();
|
|
636
|
-
// // Fast-forward time by 10ms
|
|
637
|
-
// await vi.advanceTimersByTimeAsync(10);
|
|
638
|
-
// // should still be connected
|
|
639
|
-
// expect(relay.connected).toBe(true);
|
|
640
|
-
// // Wait for the keepAlive timeout to elapse
|
|
641
|
-
// await vi.advanceTimersByTimeAsync(150);
|
|
642
|
-
// expect(relay.connected).toBe(false);
|
|
643
|
-
// });
|
|
644
|
-
// });
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
});
|