applesauce-relay 0.0.0-next-20250916134023 → 0.0.0-next-20250919114711
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/group.d.ts +2 -2
- package/dist/pool.d.ts +5 -1
- package/dist/pool.js +20 -1
- package/dist/relay.d.ts +6 -0
- package/dist/relay.js +18 -0
- package/dist/types.d.ts +9 -1
- package/package.json +5 -3
- 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__/fake-user.d.ts +0 -10
- package/dist/__tests__/fake-user.js +0 -31
- package/dist/__tests__/faker.d.ts +0 -19
- package/dist/__tests__/faker.js +0 -56
- package/dist/__tests__/group.test.d.ts +0 -1
- package/dist/__tests__/group.test.js +0 -106
- package/dist/__tests__/matchers.d.ts +0 -60
- package/dist/__tests__/matchers.js +0 -177
- package/dist/__tests__/mock.d.ts +0 -84
- package/dist/__tests__/mock.js +0 -181
- 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 -654
- package/dist/__tests__/spy.d.ts +0 -7
- package/dist/__tests__/spy.js +0 -65
- package/dist/__tests__/utils.d.ts +0 -3
- package/dist/__tests__/utils.js +0 -8
- package/dist/operators/__tests__/exports.test.d.ts +0 -1
- package/dist/operators/__tests__/exports.test.js +0 -15
- package/dist/operators/__tests__/to-event-store.test.d.ts +0 -1
- package/dist/operators/__tests__/to-event-store.test.js +0 -18
- package/dist/server/connection.d.ts +0 -1
- package/dist/server/connection.js +0 -9
package/dist/group.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
|
-
import { IEventStoreActions } from "applesauce-core";
|
|
3
|
+
import { IAsyncEventStoreActions, IEventStoreActions } from "applesauce-core";
|
|
4
4
|
import { FilterInput, IGroup, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
5
5
|
export declare class RelayGroup implements IGroup {
|
|
6
6
|
relays: IRelay[];
|
|
7
7
|
constructor(relays: IRelay[]);
|
|
8
8
|
/** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
|
|
9
|
-
protected mergeEOSE(requests: Observable<SubscriptionResponse>[], eventStore?: IEventStoreActions): Observable<import("nostr-tools").Event | "EOSE">;
|
|
9
|
+
protected mergeEOSE(requests: Observable<SubscriptionResponse>[], eventStore?: IEventStoreActions | IAsyncEventStoreActions): Observable<import("nostr-tools").Event | "EOSE">;
|
|
10
10
|
/**
|
|
11
11
|
* Make a request to all relays
|
|
12
12
|
* @note This does not deduplicate events
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type NostrEvent } from "nostr-tools";
|
|
2
|
-
import { BehaviorSubject, Observable } from "rxjs";
|
|
2
|
+
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
3
3
|
import { RelayGroup } from "./group.js";
|
|
4
4
|
import { Relay, RelayOptions } from "./relay.js";
|
|
5
5
|
import { FilterInput, IPool, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
@@ -9,6 +9,10 @@ export declare class RelayPool implements IPool {
|
|
|
9
9
|
get groups(): Map<string, RelayGroup>;
|
|
10
10
|
relays$: BehaviorSubject<Map<string, Relay>>;
|
|
11
11
|
get relays(): Map<string, Relay>;
|
|
12
|
+
/** A signal when a relay is added */
|
|
13
|
+
add$: Subject<IRelay>;
|
|
14
|
+
/** A signal when a relay is removed */
|
|
15
|
+
remove$: Subject<IRelay>;
|
|
12
16
|
/** An array of relays to never connect to */
|
|
13
17
|
blacklist: Set<string>;
|
|
14
18
|
constructor(options?: RelayOptions | undefined);
|
package/dist/pool.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BehaviorSubject } from "rxjs";
|
|
1
|
+
import { BehaviorSubject, Subject } from "rxjs";
|
|
2
2
|
import { normalizeURL } from "applesauce-core/helpers";
|
|
3
3
|
import { RelayGroup } from "./group.js";
|
|
4
4
|
import { Relay } from "./relay.js";
|
|
@@ -12,10 +12,27 @@ export class RelayPool {
|
|
|
12
12
|
get relays() {
|
|
13
13
|
return this.relays$.value;
|
|
14
14
|
}
|
|
15
|
+
/** A signal when a relay is added */
|
|
16
|
+
add$ = new Subject();
|
|
17
|
+
/** A signal when a relay is removed */
|
|
18
|
+
remove$ = new Subject();
|
|
15
19
|
/** An array of relays to never connect to */
|
|
16
20
|
blacklist = new Set();
|
|
17
21
|
constructor(options) {
|
|
18
22
|
this.options = options;
|
|
23
|
+
// Listen for relays being added and removed to emit connect / disconnect signals
|
|
24
|
+
// const listeners = new Map<IRelay, Subscription>();
|
|
25
|
+
// this.add$.subscribe((relay) =>
|
|
26
|
+
// listeners.set(
|
|
27
|
+
// relay,
|
|
28
|
+
// relay.connected$.subscribe((conn) => (conn ? this.connect$.next(relay) : this.disconnect$.next(relay))),
|
|
29
|
+
// ),
|
|
30
|
+
// );
|
|
31
|
+
// this.remove$.subscribe((relay) => {
|
|
32
|
+
// const listener = listeners.get(relay);
|
|
33
|
+
// if (listener) listener.unsubscribe();
|
|
34
|
+
// listeners.delete(relay);
|
|
35
|
+
// });
|
|
19
36
|
}
|
|
20
37
|
filterBlacklist(urls) {
|
|
21
38
|
return urls.filter((url) => !this.blacklist.has(url));
|
|
@@ -35,6 +52,7 @@ export class RelayPool {
|
|
|
35
52
|
relay = new Relay(url, this.options);
|
|
36
53
|
this.relays.set(url, relay);
|
|
37
54
|
this.relays$.next(this.relays);
|
|
55
|
+
this.add$.next(relay);
|
|
38
56
|
return relay;
|
|
39
57
|
}
|
|
40
58
|
/** Create a group of relays */
|
|
@@ -68,6 +86,7 @@ export class RelayPool {
|
|
|
68
86
|
instance?.close();
|
|
69
87
|
this.relays.delete(instance.url);
|
|
70
88
|
this.relays$.next(this.relays);
|
|
89
|
+
this.remove$.next(instance);
|
|
71
90
|
}
|
|
72
91
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
73
92
|
req(relays, filters, id) {
|
package/dist/relay.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type RelayOptions = {
|
|
|
14
14
|
eoseTimeout?: number;
|
|
15
15
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
16
16
|
eventTimeout?: number;
|
|
17
|
+
/** How long to wait for a publish to complete (default 30s) */
|
|
18
|
+
publishTimeout?: number;
|
|
17
19
|
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
18
20
|
keepAlive?: number;
|
|
19
21
|
};
|
|
@@ -70,6 +72,8 @@ export declare class Relay implements IRelay {
|
|
|
70
72
|
eoseTimeout: number;
|
|
71
73
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
72
74
|
eventTimeout: number;
|
|
75
|
+
/** How long to wait for a publish to complete (default 30s) */
|
|
76
|
+
publishTimeout: number;
|
|
73
77
|
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
74
78
|
keepAlive: number;
|
|
75
79
|
protected receivedAuthRequiredForReq: BehaviorSubject<boolean>;
|
|
@@ -101,6 +105,8 @@ export declare class Relay implements IRelay {
|
|
|
101
105
|
protected customRetryOperator<T extends unknown = unknown>(times: undefined | boolean | number | RetryConfig, base?: RetryConfig): MonoTypeOperatorFunction<T>;
|
|
102
106
|
/** Internal operator for creating the repeat() operator */
|
|
103
107
|
protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
|
|
108
|
+
/** Internal operator for creating the timeout() operator */
|
|
109
|
+
protected customTimeoutOperator<T extends unknown = unknown>(timeout: undefined | boolean | number, defaultTimeout: number): MonoTypeOperatorFunction<T>;
|
|
104
110
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
105
111
|
subscription(filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
106
112
|
/** Makes a single request that retires on errors and completes on EOSE */
|
package/dist/relay.js
CHANGED
|
@@ -77,6 +77,8 @@ export class Relay {
|
|
|
77
77
|
eoseTimeout = 10_000;
|
|
78
78
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
79
79
|
eventTimeout = 10_000;
|
|
80
|
+
/** How long to wait for a publish to complete (default 30s) */
|
|
81
|
+
publishTimeout = 30_000;
|
|
80
82
|
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
81
83
|
keepAlive = 30_000;
|
|
82
84
|
// Subjects that track if an "auth-required" message has been received for REQ or EVENT
|
|
@@ -108,6 +110,8 @@ export class Relay {
|
|
|
108
110
|
this.eoseTimeout = opts.eoseTimeout;
|
|
109
111
|
if (opts?.eventTimeout !== undefined)
|
|
110
112
|
this.eventTimeout = opts.eventTimeout;
|
|
113
|
+
if (opts?.publishTimeout !== undefined)
|
|
114
|
+
this.publishTimeout = opts.publishTimeout;
|
|
111
115
|
if (opts?.keepAlive !== undefined)
|
|
112
116
|
this.keepAlive = opts.keepAlive;
|
|
113
117
|
// Create an observable that tracks boolean authentication state
|
|
@@ -378,6 +382,18 @@ export class Relay {
|
|
|
378
382
|
else
|
|
379
383
|
return repeat(times);
|
|
380
384
|
}
|
|
385
|
+
/** Internal operator for creating the timeout() operator */
|
|
386
|
+
customTimeoutOperator(timeout, defaultTimeout) {
|
|
387
|
+
// Do nothing if disabled
|
|
388
|
+
if (timeout === false)
|
|
389
|
+
return identity;
|
|
390
|
+
// If true default to 30 seconds
|
|
391
|
+
else if (timeout === true)
|
|
392
|
+
return simpleTimeout(defaultTimeout);
|
|
393
|
+
// Otherwise use the timeout value or default to 30 seconds
|
|
394
|
+
else
|
|
395
|
+
return simpleTimeout(timeout ?? defaultTimeout);
|
|
396
|
+
}
|
|
381
397
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
382
398
|
subscription(filters, opts) {
|
|
383
399
|
return this.req(filters, opts?.id).pipe(
|
|
@@ -410,6 +426,8 @@ export class Relay {
|
|
|
410
426
|
}),
|
|
411
427
|
// Retry the publish until it succeeds or the number of retries is reached
|
|
412
428
|
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
429
|
+
// Add timeout for publishing
|
|
430
|
+
this.customTimeoutOperator(opts?.timeout, this.publishTimeout),
|
|
413
431
|
// Single subscription
|
|
414
432
|
share()));
|
|
415
433
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type PublishOptions = {
|
|
|
21
21
|
* @see https://rxjs.dev/api/index/function/retry
|
|
22
22
|
*/
|
|
23
23
|
reconnect?: boolean | number | Parameters<typeof retry>[0];
|
|
24
|
+
/** Timeout for publish in milliseconds (default 30 seconds) */
|
|
25
|
+
timeout?: number | boolean;
|
|
24
26
|
};
|
|
25
27
|
/** Options for the request method on the pool and relay */
|
|
26
28
|
export type RequestOptions = SubscriptionOptions;
|
|
@@ -58,6 +60,7 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
58
60
|
challenge$: Observable<string | null>;
|
|
59
61
|
authenticated$: Observable<boolean>;
|
|
60
62
|
notices$: Observable<string[]>;
|
|
63
|
+
error$: Observable<Error | null>;
|
|
61
64
|
readonly connected: boolean;
|
|
62
65
|
readonly authenticated: boolean;
|
|
63
66
|
readonly challenge: string | null;
|
|
@@ -91,7 +94,12 @@ export interface IGroup {
|
|
|
91
94
|
/** Open a subscription with retries */
|
|
92
95
|
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
93
96
|
}
|
|
94
|
-
|
|
97
|
+
/** Signals emitted by the pool */
|
|
98
|
+
export interface IPoolSignals {
|
|
99
|
+
add$: Observable<IRelay>;
|
|
100
|
+
remove$: Observable<IRelay>;
|
|
101
|
+
}
|
|
102
|
+
export interface IPool extends IPoolSignals {
|
|
95
103
|
/** Get or create a relay */
|
|
96
104
|
relay(url: string): IRelay;
|
|
97
105
|
/** Create a relay group */
|
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-20250919114711",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,14 +52,15 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@noble/hashes": "^1.7.1",
|
|
55
|
-
"applesauce-core": "0.0.0-next-
|
|
55
|
+
"applesauce-core": "0.0.0-next-20250919114711",
|
|
56
56
|
"nanoid": "^5.0.9",
|
|
57
57
|
"nostr-tools": "~2.15",
|
|
58
58
|
"rxjs": "^7.8.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
62
|
-
"applesauce-signers": "0.0.0-next-
|
|
62
|
+
"applesauce-signers": "0.0.0-next-20250919114711",
|
|
63
|
+
"rimraf": "^6.0.1",
|
|
63
64
|
"typescript": "^5.7.3",
|
|
64
65
|
"vitest": "^3.2.4",
|
|
65
66
|
"vitest-websocket-mock": "^0.5.0"
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"url": "lightning:nostrudel@geyser.fund"
|
|
70
71
|
},
|
|
71
72
|
"scripts": {
|
|
73
|
+
"prebuild": "rimraf dist",
|
|
72
74
|
"build": "tsc",
|
|
73
75
|
"watch:build": "tsc --watch > /dev/null",
|
|
74
76
|
"test": "vitest run --passWithNoTests",
|
|
@@ -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,10 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as Nostr from "nostr-typedef";
|
|
2
|
-
export declare const faker: {
|
|
3
|
-
filter(filter?: Nostr.Filter): Nostr.Filter;
|
|
4
|
-
filters(): Nostr.Filter[];
|
|
5
|
-
event<const K extends number>(event?: Partial<Nostr.Event<K>>): Nostr.Event<K>;
|
|
6
|
-
AUTH(event?: Partial<Nostr.Event<Nostr.Kind.ClientAuthentication>>): Nostr.ToRelayMessage.AUTH;
|
|
7
|
-
CLOSE(subId: string): Nostr.ToRelayMessage.CLOSE;
|
|
8
|
-
COUNT(subId: string, filters?: Nostr.Filter[]): Nostr.ToRelayMessage.COUNT;
|
|
9
|
-
EVENT(event?: Partial<Nostr.Event>): Nostr.ToRelayMessage.EVENT;
|
|
10
|
-
REQ(subId: string, filters?: Nostr.Filter[]): Nostr.ToRelayMessage.REQ;
|
|
11
|
-
toClientMessage: {
|
|
12
|
-
AUTH(message?: string): Nostr.ToClientMessage.AUTH;
|
|
13
|
-
COUNT(subId: string, count?: number): Nostr.ToClientMessage.COUNT;
|
|
14
|
-
EOSE(subId: string): Nostr.ToClientMessage.EOSE;
|
|
15
|
-
EVENT(subId: string, event?: Partial<Nostr.Event>): Nostr.ToClientMessage.EVENT;
|
|
16
|
-
NOTICE(message?: string): Nostr.ToClientMessage.NOTICE;
|
|
17
|
-
OK(eventId: string, succeeded: boolean, message?: string): Nostr.ToClientMessage.OK;
|
|
18
|
-
};
|
|
19
|
-
};
|
package/dist/__tests__/faker.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { kinds } from "nostr-tools";
|
|
2
|
-
export const faker = {
|
|
3
|
-
filter(filter) {
|
|
4
|
-
return filter ?? { kinds: [0] };
|
|
5
|
-
},
|
|
6
|
-
filters() {
|
|
7
|
-
return [faker.filter()];
|
|
8
|
-
},
|
|
9
|
-
event(event) {
|
|
10
|
-
return {
|
|
11
|
-
id: "*",
|
|
12
|
-
content: "*",
|
|
13
|
-
created_at: 0,
|
|
14
|
-
kind: 0,
|
|
15
|
-
pubkey: "*",
|
|
16
|
-
sig: "*",
|
|
17
|
-
tags: [],
|
|
18
|
-
...event,
|
|
19
|
-
};
|
|
20
|
-
},
|
|
21
|
-
AUTH(event) {
|
|
22
|
-
return ["AUTH", { ...faker.event(event), kind: kinds.ClientAuth }];
|
|
23
|
-
},
|
|
24
|
-
CLOSE(subId) {
|
|
25
|
-
return ["CLOSE", subId];
|
|
26
|
-
},
|
|
27
|
-
COUNT(subId, filters) {
|
|
28
|
-
return ["COUNT", subId, ...(filters ?? faker.filters())];
|
|
29
|
-
},
|
|
30
|
-
EVENT(event) {
|
|
31
|
-
return ["EVENT", faker.event(event)];
|
|
32
|
-
},
|
|
33
|
-
REQ(subId, filters) {
|
|
34
|
-
return ["REQ", subId, ...(filters ?? faker.filters())];
|
|
35
|
-
},
|
|
36
|
-
toClientMessage: {
|
|
37
|
-
AUTH(message) {
|
|
38
|
-
return ["AUTH", message ?? "*"];
|
|
39
|
-
},
|
|
40
|
-
COUNT(subId, count) {
|
|
41
|
-
return ["COUNT", subId, { count: count ?? 0 }];
|
|
42
|
-
},
|
|
43
|
-
EOSE(subId) {
|
|
44
|
-
return ["EOSE", subId];
|
|
45
|
-
},
|
|
46
|
-
EVENT(subId, event) {
|
|
47
|
-
return ["EVENT", subId, faker.event(event)];
|
|
48
|
-
},
|
|
49
|
-
NOTICE(message) {
|
|
50
|
-
return ["NOTICE", message ?? "*"];
|
|
51
|
-
},
|
|
52
|
-
OK(eventId, succeeded, message) {
|
|
53
|
-
return ["OK", eventId, succeeded, message ?? "*"];
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
};
|
|
@@ -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,60 +0,0 @@
|
|
|
1
|
-
import Nostr from "nostr-typedef";
|
|
2
|
-
declare module "vitest" {
|
|
3
|
-
interface Assertion<T = any> extends CustomMatchers {
|
|
4
|
-
}
|
|
5
|
-
interface AsymmetricMatchersContaining extends CustomMatchers {
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
interface CustomMatchers {
|
|
9
|
-
beToRelayAUTH(): void;
|
|
10
|
-
beToRelayAUTH(event: Partial<Nostr.Event<Nostr.Kind.ClientAuthentication>>): void;
|
|
11
|
-
beToRelayCLOSE(): void;
|
|
12
|
-
beToRelayCLOSE(subId: string): void;
|
|
13
|
-
beToRelayCOUNT(): void;
|
|
14
|
-
beToRelayCOUNT(subId: string): void;
|
|
15
|
-
beToRelayCOUNT(expected: [subId: string, ...filters: Nostr.Filter[]]): void;
|
|
16
|
-
beToRelayEVENT(): void;
|
|
17
|
-
beToRelayEVENT(event: Partial<Nostr.Event>): void;
|
|
18
|
-
beToRelayREQ(): void;
|
|
19
|
-
beToRelayREQ(subId: string): void;
|
|
20
|
-
beToRelayREQ(expected: [subId: string, ...filters: Nostr.Filter[]]): void;
|
|
21
|
-
beToClientAUTH(): void;
|
|
22
|
-
beToClientAUTH(challengeMessage: string): void;
|
|
23
|
-
beToClientCOUNT(): void;
|
|
24
|
-
beToClientCOUNT(subId: string): void;
|
|
25
|
-
beToClientCOUNT(expected: [subId: string, count: number]): void;
|
|
26
|
-
beToClientEOSE(): void;
|
|
27
|
-
beToClientEOSE(subId: string): void;
|
|
28
|
-
beToClientEVENT(): void;
|
|
29
|
-
beToClientEVENT(subId: string): void;
|
|
30
|
-
beToClientEVENT(expected: [subId: string, event: Partial<Nostr.Event>]): void;
|
|
31
|
-
beToClientNOTICE(): void;
|
|
32
|
-
beToClientNOTICE(message: string): void;
|
|
33
|
-
beToClientOK(expected: [eventId: string, succeeded: boolean, message?: string]): void;
|
|
34
|
-
toReceiveAUTH(): Promise<void>;
|
|
35
|
-
toReceiveAUTH(event: Partial<Nostr.Event<Nostr.Kind.ClientAuthentication>>): Promise<void>;
|
|
36
|
-
toReceiveCLOSE(): Promise<void>;
|
|
37
|
-
toReceiveCLOSE(subId: string): Promise<void>;
|
|
38
|
-
toReceiveCOUNT(): Promise<void>;
|
|
39
|
-
toReceiveCOUNT(subId: string): Promise<void>;
|
|
40
|
-
toReceiveCOUNT(expected: [subId: string, ...filters: Nostr.Filter[]]): Promise<void>;
|
|
41
|
-
toReceiveEVENT(): Promise<void>;
|
|
42
|
-
toReceiveEVENT(event: Partial<Nostr.Event>): Promise<void>;
|
|
43
|
-
toReceiveREQ(): Promise<void>;
|
|
44
|
-
toReceiveREQ(subId: string): Promise<void>;
|
|
45
|
-
toReceiveREQ(expected: [subId: string, ...filters: Nostr.Filter[]]): Promise<void>;
|
|
46
|
-
toSeeAUTH(): Promise<void>;
|
|
47
|
-
toSeeAUTH(challengeMessage: string): Promise<void>;
|
|
48
|
-
toSeeCOUNT(): Promise<void>;
|
|
49
|
-
toSeeCOUNT(subId: string): Promise<void>;
|
|
50
|
-
toSeeCOUNT(expected: [subId: string, count: number]): Promise<void>;
|
|
51
|
-
toSeeEOSE(): Promise<void>;
|
|
52
|
-
toSeeEOSE(subId: string): Promise<void>;
|
|
53
|
-
toSeeEVENT(): Promise<void>;
|
|
54
|
-
toSeeEVENT(subId: string): Promise<void>;
|
|
55
|
-
toSeeEVENT(expected: [subId: string, event: Partial<Nostr.Event>]): Promise<void>;
|
|
56
|
-
toSeeNOTICE(): Promise<void>;
|
|
57
|
-
toSeeNOTICE(message: string): Promise<void>;
|
|
58
|
-
toSeeOK(expected: [eventId: string, succeeded: boolean, message?: string]): Promise<void>;
|
|
59
|
-
}
|
|
60
|
-
export {};
|