applesauce-relay 0.0.0-next-20250414124006 → 0.0.0-next-20250423145737

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.
@@ -3,15 +3,17 @@ import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
3
  import { getSeenRelays } from "applesauce-core/helpers";
4
4
  import { WS } from "vitest-websocket-mock";
5
5
  import { Relay } from "../relay.js";
6
+ import { filter } from "rxjs/operators";
7
+ import { firstValueFrom } from "rxjs";
6
8
  let mockRelay;
7
9
  let relay;
8
10
  beforeEach(async () => {
9
- mockRelay = new WS("wss://test");
11
+ mockRelay = new WS("wss://test", { jsonProtocol: true });
10
12
  relay = new Relay("wss://test");
13
+ relay.keepAlive = 0;
11
14
  });
15
+ // Wait for server to close to prevent memory leaks
12
16
  afterEach(async () => {
13
- mockRelay.close();
14
- // Wait for server to close to prevent memory leaks
15
17
  await WS.clean();
16
18
  });
17
19
  const mockEvent = {
@@ -24,44 +26,48 @@ const mockEvent = {
24
26
  sig: "5a57b5a12bba4b7cf0121077b1421cf4df402c5c221376c076204fc4f7519e28ce6508f26ddc132c406ccfe6e62cc6db857b96c788565cdca9674fe9a0710ac2",
25
27
  };
26
28
  describe("req", () => {
29
+ it("should trigger connection to relay", async () => {
30
+ subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
31
+ // Wait for connection
32
+ await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
33
+ expect(relay.connected).toBe(true);
34
+ });
27
35
  it("should send REQ and CLOSE messages", async () => {
28
36
  // Create subscription that completes after first EOSE
29
37
  const sub = relay.req([{ kinds: [1] }], "sub1").subscribe();
30
38
  // Verify REQ was sent
31
- const reqMessage = await mockRelay.nextMessage;
32
- expect(JSON.parse(reqMessage)).toEqual(["REQ", "sub1", { kinds: [1] }]);
39
+ expect(await mockRelay.nextMessage).toEqual(["REQ", "sub1", { kinds: [1] }]);
33
40
  // Send EOSE to complete subscription
34
- mockRelay.send(JSON.stringify(["EOSE", "sub1"]));
41
+ mockRelay.send(["EOSE", "sub1"]);
35
42
  // Complete the subscription
36
43
  sub.unsubscribe();
37
44
  // Verify CLOSE was sent
38
- const closeMessage = await mockRelay.nextMessage;
39
- expect(JSON.parse(closeMessage)).toEqual(["CLOSE", "sub1"]);
45
+ expect(await mockRelay.nextMessage).toEqual(["CLOSE", "sub1"]);
40
46
  });
41
47
  it("should emit nostr event and EOSE", async () => {
42
48
  const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
43
49
  // Send EVENT message
44
- mockRelay.send(JSON.stringify(["EVENT", "sub1", mockEvent]));
50
+ mockRelay.send(["EVENT", "sub1", mockEvent]);
45
51
  // Send EOSE message
46
- mockRelay.send(JSON.stringify(["EOSE", "sub1"]));
52
+ mockRelay.send(["EOSE", "sub1"]);
47
53
  expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
48
54
  });
49
55
  it("should ignore EVENT and EOSE messages that do not match subscription id", async () => {
50
56
  const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
51
57
  // Send EVENT message with wrong subscription id
52
- mockRelay.send(JSON.stringify(["EVENT", "wrong_sub", mockEvent]));
58
+ mockRelay.send(["EVENT", "wrong_sub", mockEvent]);
53
59
  // Send EOSE message with wrong subscription id
54
- mockRelay.send(JSON.stringify(["EOSE", "wrong_sub"]));
60
+ mockRelay.send(["EOSE", "wrong_sub"]);
55
61
  // Send EVENT message with correct subscription id
56
- mockRelay.send(JSON.stringify(["EVENT", "sub1", mockEvent]));
62
+ mockRelay.send(["EVENT", "sub1", mockEvent]);
57
63
  // Send EOSE message with correct subscription id
58
- mockRelay.send(JSON.stringify(["EOSE", "sub1"]));
64
+ mockRelay.send(["EOSE", "sub1"]);
59
65
  expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
60
66
  });
61
67
  it("should mark events with their source relay", async () => {
62
68
  const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
63
69
  // Send EVENT message
64
- mockRelay.send(JSON.stringify(["EVENT", "sub1", mockEvent]));
70
+ mockRelay.send(["EVENT", "sub1", mockEvent]);
65
71
  // Get the received event
66
72
  const receivedEvent = spy.getValues()[0];
67
73
  // Verify the event was marked as seen from this relay
@@ -70,35 +76,39 @@ describe("req", () => {
70
76
  it("should complete subscription when CLOSED message is received", async () => {
71
77
  const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
72
78
  // Send CLOSED message for the subscription
73
- mockRelay.send(JSON.stringify(["CLOSED", "sub1", "reason"]));
79
+ mockRelay.send(["CLOSED", "sub1", "reason"]);
74
80
  // Verify the subscription completed
75
81
  expect(spy.receivedComplete()).toBe(true);
76
82
  });
77
83
  });
78
84
  describe("event", () => {
85
+ it("should trigger connection to relay", async () => {
86
+ subscribeSpyTo(relay.event(mockEvent));
87
+ // Wait for connection
88
+ await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
89
+ expect(relay.connected).toBe(true);
90
+ });
79
91
  it("observable should complete when matching OK response received", async () => {
80
92
  const spy = subscribeSpyTo(relay.event(mockEvent));
81
93
  // Verify EVENT message was sent
82
- const eventMessage = await mockRelay.nextMessage;
83
- expect(JSON.parse(eventMessage)).toEqual(["EVENT", mockEvent]);
94
+ expect(await mockRelay.nextMessage).toEqual(["EVENT", mockEvent]);
84
95
  // Send matching OK response
85
- mockRelay.send(JSON.stringify(["OK", mockEvent.id, true, ""]));
96
+ mockRelay.send(["OK", mockEvent.id, true, ""]);
86
97
  await spy.onComplete();
87
98
  expect(spy.receivedComplete()).toBe(true);
88
99
  });
89
100
  it("should ignore OK responses for different events", async () => {
90
101
  const spy = subscribeSpyTo(relay.event(mockEvent));
91
102
  // Send non-matching OK response
92
- mockRelay.send(JSON.stringify(["OK", "different_id", true, ""]));
103
+ mockRelay.send(["OK", "different_id", true, ""]);
93
104
  expect(spy.receivedComplete()).toBe(false);
94
105
  // Send matching OK response
95
- mockRelay.send(JSON.stringify(["OK", mockEvent.id, true, ""]));
106
+ mockRelay.send(["OK", mockEvent.id, true, ""]);
96
107
  expect(spy.receivedComplete()).toBe(true);
97
108
  });
98
109
  it("should send EVENT message to relay", async () => {
99
110
  relay.event(mockEvent).subscribe();
100
- const eventMessage = await mockRelay.nextMessage;
101
- expect(JSON.parse(eventMessage)).toEqual(["EVENT", mockEvent]);
111
+ expect(await mockRelay.nextMessage).toEqual(["EVENT", mockEvent]);
102
112
  });
103
113
  it("should complete with error if no OK received within 10s", async () => {
104
114
  vi.useFakeTimers();
@@ -109,37 +119,65 @@ describe("event", () => {
109
119
  expect(spy.getLastValue()).toEqual({ ok: false, from: "wss://test", message: "Timeout" });
110
120
  });
111
121
  });
112
- describe("notice$", () => {
122
+ describe("notices$", () => {
123
+ it("should not trigger connection to relay", async () => {
124
+ subscribeSpyTo(relay.notices$);
125
+ expect(relay.connected).toBe(false);
126
+ });
113
127
  it("should accumulate notices in notices$ state", async () => {
114
128
  subscribeSpyTo(relay.req({ kinds: [1] }));
115
129
  // Send multiple NOTICE messages
116
- mockRelay.send(JSON.stringify(["NOTICE", "Notice 1"]));
117
- mockRelay.send(JSON.stringify(["NOTICE", "Notice 2"]));
118
- mockRelay.send(JSON.stringify(["NOTICE", "Notice 3"]));
130
+ mockRelay.send(["NOTICE", "Notice 1"]);
131
+ mockRelay.send(["NOTICE", "Notice 2"]);
132
+ mockRelay.send(["NOTICE", "Notice 3"]);
119
133
  // Verify the notices state contains all messages
120
134
  expect(relay.notices$.value).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
121
135
  });
122
136
  it("should ignore non-NOTICE messages", async () => {
123
137
  subscribeSpyTo(relay.req({ kinds: [1] }));
124
- mockRelay.send(JSON.stringify(["NOTICE", "Important notice"]));
125
- mockRelay.send(JSON.stringify(["OTHER", "other message"]));
138
+ mockRelay.send(["NOTICE", "Important notice"]);
139
+ mockRelay.send(["OTHER", "other message"]);
126
140
  // Verify only NOTICE messages are in the state
127
141
  expect(relay.notices$.value).toEqual(["Important notice"]);
128
142
  });
129
143
  });
130
144
  describe("challenge$", () => {
145
+ it("should not trigger connection to relay", async () => {
146
+ subscribeSpyTo(relay.challenge$);
147
+ expect(relay.connected).toBe(false);
148
+ });
131
149
  it("should set challenge$ when AUTH message received", async () => {
132
150
  subscribeSpyTo(relay.req({ kinds: [1] }));
133
151
  // Send AUTH message with challenge string
134
- mockRelay.send(JSON.stringify(["AUTH", "challenge-string-123"]));
152
+ mockRelay.send(["AUTH", "challenge-string-123"]);
135
153
  // Verify challenge$ was set
136
154
  expect(relay.challenge$.value).toBe("challenge-string-123");
137
155
  });
138
156
  it("should ignore non-AUTH messages", async () => {
139
157
  subscribeSpyTo(relay.req({ kinds: [1] }));
140
- mockRelay.send(JSON.stringify(["NOTICE", "Not a challenge"]));
141
- mockRelay.send(JSON.stringify(["OTHER", "other message"]));
158
+ mockRelay.send(["NOTICE", "Not a challenge"]);
159
+ mockRelay.send(["OTHER", "other message"]);
142
160
  // Verify challenge$ remains null
143
161
  expect(relay.challenge$.value).toBe(null);
144
162
  });
145
163
  });
164
+ // describe("keepAlive", () => {
165
+ // it("should close the socket connection after keepAlive timeout", async () => {
166
+ // vi.useFakeTimers();
167
+ // // Set a short keepAlive timeout for testing
168
+ // relay.keepAlive = 100; // 100ms for quick testing
169
+ // // Subscribe to the relay to ensure it is active
170
+ // const sub = subscribeSpyTo(relay.req([{ kinds: [1] }]));
171
+ // // Wait for connection
172
+ // await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
173
+ // // Close the subscription
174
+ // sub.unsubscribe();
175
+ // // Fast-forward time by 10ms
176
+ // await vi.advanceTimersByTimeAsync(10);
177
+ // // should still be connected
178
+ // expect(relay.connected).toBe(true);
179
+ // // Wait for the keepAlive timeout to elapse
180
+ // await vi.advanceTimersByTimeAsync(150);
181
+ // expect(relay.connected).toBe(false);
182
+ // });
183
+ // });
package/dist/relay.d.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { logger } from "applesauce-core";
2
+ import { type Filter, type NostrEvent } from "nostr-tools";
1
3
  import { BehaviorSubject, Observable } from "rxjs";
2
4
  import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
3
- import { type Filter, type NostrEvent } from "nostr-tools";
4
- import { logger } from "applesauce-core";
5
5
  import { IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
6
6
  export type RelayOptions = {
7
7
  WebSocket?: WebSocketSubjectConfig<any>["WebSocketCtor"];
@@ -26,6 +26,8 @@ export declare class Relay implements IRelay {
26
26
  eoseTimeout: number;
27
27
  /** How long to wait for an OK message from the relay */
28
28
  eventTimeout: number;
29
+ /** How long to keep the connection alive after nothing is subscribed */
30
+ keepAlive: number;
29
31
  protected authRequiredForReq: BehaviorSubject<boolean>;
30
32
  protected authRequiredForPublish: BehaviorSubject<boolean>;
31
33
  protected resetState(): void;
package/dist/relay.js CHANGED
@@ -1,7 +1,7 @@
1
- import { BehaviorSubject, combineLatest, defer, filter, ignoreElements, map, merge, NEVER, of, scan, share, switchMap, take, takeWhile, tap, timeout, } from "rxjs";
2
- import { webSocket } from "rxjs/webSocket";
3
- import { nanoid } from "nanoid";
4
1
  import { logger } from "applesauce-core";
2
+ import { nanoid } from "nanoid";
3
+ import { BehaviorSubject, combineLatest, defer, filter, ignoreElements, map, merge, NEVER, of, scan, share, switchMap, take, takeWhile, tap, timeout, timer } from "rxjs";
4
+ import { webSocket } from "rxjs/webSocket";
5
5
  import { markFromRelay } from "./operators/mark-from-relay.js";
6
6
  export class Relay {
7
7
  url;
@@ -32,6 +32,8 @@ export class Relay {
32
32
  eoseTimeout = 10_000;
33
33
  /** How long to wait for an OK message from the relay */
34
34
  eventTimeout = 10_000;
35
+ /** How long to keep the connection alive after nothing is subscribed */
36
+ keepAlive = 30_000;
35
37
  authRequiredForReq = new BehaviorSubject(false);
36
38
  authRequiredForPublish = new BehaviorSubject(false);
37
39
  resetState() {
@@ -98,7 +100,7 @@ export class Relay {
98
100
  // Never emit any values
99
101
  ignoreElements(),
100
102
  // There should only be a single watch tower
101
- share());
103
+ share({ resetOnRefCountZero: () => timer(this.keepAlive) }));
102
104
  }
103
105
  waitForAuth(
104
106
  // NOTE: require BehaviorSubject so it always has a value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20250414124006",
3
+ "version": "0.0.0-next-20250423145737",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@noble/hashes": "^1.7.1",
57
- "applesauce-core": "0.0.0-next-20250414124006",
57
+ "applesauce-core": "0.0.0-next-20250423145737",
58
58
  "nanoid": "^5.0.9",
59
59
  "nostr-tools": "^2.10.4",
60
60
  "rxjs": "^7.8.1"
@@ -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
- };
@@ -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,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 {};
@@ -1,177 +0,0 @@
1
- import { expect } from "vitest";
2
- import { deriveToReceiveMessage } from "vitest-websocket-mock";
3
- import { TimeoutError, withTimeout } from "./utils.js";
4
- const beToRelayAUTH = function (actual, event) {
5
- const pass = isNostrMessage("AUTH", actual) && (event === undefined || matchEvent(actual[1], event, this.equals.bind(this)));
6
- return {
7
- message: message("a to-relay-AUTH message", ["AUTH", event ?? "*"], this, actual, pass),
8
- pass,
9
- };
10
- };
11
- const beToRelayCLOSE = function (actual, subId) {
12
- const pass = isNostrMessage("CLOSE", actual) && (subId === undefined || actual[1] === subId);
13
- return {
14
- message: message("a to-relay-CLOSE message", ["CLOSE", subId ?? "*"], this, actual, pass),
15
- pass,
16
- };
17
- };
18
- const beToRelayCOUNT = function (actual, pred) {
19
- const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
20
- const filters = typeof pred === "object" && pred !== undefined ? pred.slice(1) : null;
21
- const pass = isNostrMessage("COUNT", actual) &&
22
- (subId === null || actual[1] === subId) &&
23
- (filters === null || this.equals(actual.slice(2), filters));
24
- return {
25
- message: message("a to-relay-COUNT message", ["COUNT", subId ?? "*", ...(filters === null ? ["*"] : filters)], this, actual, pass),
26
- pass,
27
- };
28
- };
29
- const beToRelayEVENT = function (actual, event) {
30
- const pass = isNostrMessage("EVENT", actual) && (event === undefined || matchEvent(actual[1], event, this.equals.bind(this)));
31
- return {
32
- message: message("a to-relay-EVENT message", ["EVENT", event ?? "*"], this, actual, pass),
33
- pass,
34
- };
35
- };
36
- const beToRelayREQ = function (actual, pred) {
37
- const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
38
- const filters = typeof pred === "object" && pred !== undefined ? pred.slice(1) : null;
39
- const pass = isNostrMessage("REQ", actual) &&
40
- (subId === null || actual[1] === subId) &&
41
- (filters === null || this.equals(actual.slice(2), filters));
42
- return {
43
- message: message("a to-relay-REQ message", ["REQ", subId ?? "*", ...(filters === null ? ["*"] : filters)], this, actual, pass),
44
- pass,
45
- };
46
- };
47
- const beToClientAUTH = function (actual, challengeMessage) {
48
- const pass = isNostrMessage("AUTH", actual) && (challengeMessage === undefined || actual[1] === challengeMessage);
49
- return {
50
- message: message("a to-client-AUTH message", ["AUTH", challengeMessage ?? "*"], this, actual, pass),
51
- pass,
52
- };
53
- };
54
- const beToClientCOUNT = function (actual, pred) {
55
- const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
56
- const count = typeof pred === "object" && pred !== undefined ? pred[1] : null;
57
- const pass = isNostrMessage("COUNT", actual) &&
58
- (subId === null || actual[1] === subId) &&
59
- (count === null || this.equals(actual[2], { count }));
60
- return {
61
- message: message("a to-client-COUNT message", ["COUNT", subId ?? "*", count === null ? "*" : { count }], this, actual, pass),
62
- pass,
63
- };
64
- };
65
- const beToClientEOSE = function (actual, subId) {
66
- const pass = isNostrMessage("EOSE", actual) && (subId === undefined || actual[1] === subId);
67
- return {
68
- message: message("a to-client-EOSE message", ["COUNT", subId ?? "*"], this, actual, pass),
69
- pass,
70
- };
71
- };
72
- const beToClientEVENT = function (actual, pred) {
73
- const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
74
- const event = typeof pred === "object" && pred !== undefined ? pred[1] : null;
75
- const pass = isNostrMessage("EVENT", actual) &&
76
- (subId === null || actual[1] === subId) &&
77
- (event === null || matchEvent(actual[2], event, this.equals.bind(this)));
78
- return {
79
- message: message("a to-client-EVENT message", ["EVENT", subId ?? "*", event ?? "*"], this, actual, pass),
80
- pass,
81
- };
82
- };
83
- const beToClientNOTICE = function (actual, noticeMessage) {
84
- const pass = isNostrMessage("NOTICE", actual) && (noticeMessage === undefined || actual[1] === noticeMessage);
85
- return {
86
- message: message("a to-client-NOTICE message", ["EVENT", noticeMessage ?? "*"], this, actual, pass),
87
- pass,
88
- };
89
- };
90
- const beToClientOK = function (actual, expected) {
91
- const pass = isNostrMessage("OK", actual) &&
92
- (expected === undefined ||
93
- (actual[1] === expected[0] &&
94
- actual[2] === expected[1] &&
95
- (expected[2] === undefined || actual[3] === expected[2])));
96
- return {
97
- message: message("a to-client-OK message", ["OK", ...(expected ?? ["*", "*", "*"])], this, actual, pass),
98
- pass,
99
- };
100
- };
101
- const toReceiveAUTH = deriveToReceiveMessage("toReceiveAUTH", beToRelayAUTH);
102
- const toReceiveCLOSE = deriveToReceiveMessage("toReceiveCLOSE", beToRelayCLOSE);
103
- const toReceiveCOUNT = deriveToReceiveMessage("toReceiveCOUNT", beToRelayCOUNT);
104
- const toReceiveEVENT = deriveToReceiveMessage("toReceiveEVENT", beToRelayEVENT);
105
- const toReceiveREQ = deriveToReceiveMessage("toReceiveREQ", beToRelayREQ);
106
- const toSeeAUTH = clientMathcer("toSeeAUTH", beToClientAUTH);
107
- const toSeeCOUNT = clientMathcer("toSeeCOUNT", beToClientCOUNT);
108
- const toSeeEOSE = clientMathcer("toSeeEOSE", beToClientEOSE);
109
- const toSeeEVENT = clientMathcer("toSeeEVENT", beToClientEVENT);
110
- const toSeeNOTICE = clientMathcer("toSeeNOTICE", beToClientNOTICE);
111
- const toSeeOK = clientMathcer("toSeeOK", beToClientOK);
112
- expect.extend({
113
- beToRelayAUTH,
114
- beToRelayCLOSE,
115
- beToRelayCOUNT,
116
- beToRelayEVENT,
117
- beToRelayREQ,
118
- beToClientAUTH,
119
- beToClientCOUNT,
120
- beToClientEOSE,
121
- beToClientEVENT,
122
- beToClientNOTICE,
123
- beToClientOK,
124
- toReceiveAUTH,
125
- toReceiveCLOSE,
126
- toReceiveCOUNT,
127
- toReceiveEVENT,
128
- toReceiveREQ,
129
- toSeeAUTH,
130
- toSeeCOUNT,
131
- toSeeEOSE,
132
- toSeeEVENT,
133
- toSeeNOTICE,
134
- toSeeOK,
135
- });
136
- function clientMathcer(name, fn) {
137
- return async function (spy, pred, options) {
138
- if (spy.__createClientSpy) {
139
- try {
140
- return await withTimeout(async () => fn.call(this, await spy.next(), pred, options));
141
- }
142
- catch (err) {
143
- if (err instanceof TimeoutError) {
144
- return {
145
- pass: this.isNot,
146
- message: () => this.utils.matcherHint(`${this.isNot ? ".not" : ""}.${name}`, "ClientSpy", "expected") +
147
- "\n\n" +
148
- `Client spy was waiting for the next message from the relay, but timed out.`,
149
- };
150
- }
151
- else {
152
- throw err;
153
- }
154
- }
155
- }
156
- else {
157
- return {
158
- pass: this.isNot,
159
- message: () => this.utils.matcherHint(`${this.isNot ? ".not" : ""}.${name}`, "ClientSpy", "expected") +
160
- "\n\n" +
161
- `Mathcer \`${name}\` must be used for ClientSpy, but now being used for:\n\n` +
162
- `${this.utils.printReceived(spy)}\n`,
163
- };
164
- }
165
- };
166
- }
167
- function isNostrMessage(type, message) {
168
- return Array.isArray(message) && message[0] === type;
169
- }
170
- function matchEvent(received, expected, equal) {
171
- const eventKeys = ["id", "sig", "kind", "tags", "pubkey", "content", "created_at"];
172
- const isEvent = (x) => typeof x === "object" && x !== null && eventKeys.every((key) => key in x);
173
- return (isEvent(received) && eventKeys.every((key) => expected[key] === undefined || equal(received[key], expected[key])));
174
- }
175
- function message(entityName, expected, state, actual, pass) {
176
- return () => `It was expected ${pass ? "not " : ""}to be ${entityName}, like this:\n\n${state.utils.printExpected(expected)}\n\nbut got:\n\n${state.utils.printReceived(actual)}\n`;
177
- }
@@ -1,84 +0,0 @@
1
- import { CloseOptions } from "mock-socket";
2
- import Nostr from "nostr-typedef";
3
- import { WS } from "vitest-websocket-mock";
4
- export interface MockServerSocket {
5
- id: number;
6
- send(message: unknown): void;
7
- close(options?: CloseOptions): void;
8
- }
9
- export interface MockServerBehavior {
10
- onOpen?(socket: MockServerSocket): void;
11
- onMessage?(socket: MockServerSocket, message: string): void;
12
- onClose?(socket: MockServerSocket): void;
13
- onError?(socket: MockServerSocket, error: Error): void;
14
- }
15
- interface MockServer extends WS {
16
- getSockets: (count: number, options?: {
17
- timeout?: number;
18
- }) => Promise<MockServerSocket[]>;
19
- getSocket: (index: number) => Promise<MockServerSocket>;
20
- getSocketsSync: () => MockServerSocket[];
21
- }
22
- export declare function createMockServer(url: string, behavior: MockServerBehavior): MockServer;
23
- export interface MockRelay extends MockServer {
24
- /**
25
- * Pop the latest message that has not yet been consumed.
26
- * If such a message does not yet exist, it waits for the message
27
- * until it times out.
28
- *
29
- * The default timeout is 1000 miliseconds.
30
- */
31
- next: (timeout?: number) => Promise<Nostr.ToRelayMessage.Any>;
32
- /**
33
- * Pop the latest `count` messages that have not yet been consumed.
34
- * If such a message does not yet exist, it waits for the message
35
- * until it times out.
36
- *
37
- * The default timeout is 1000 miliseconds.
38
- */
39
- nexts: (count: number, timeout?: number) => Promise<Nostr.ToRelayMessage.Any[]>;
40
- /**
41
- * Send messages of any format from the given socket mock.
42
- * If `socket` is omitted, it will be sent from all active socket mocks.
43
- *
44
- * If the message is a string, as-is string will be sent,
45
- * otherwise JSON strigify will be attempted.
46
- *
47
- * Note that the messages sent by this method are outside
48
- * the management of the mock relay.
49
- * This means that if, for example, you use this method to send an OK message,
50
- * the mock relay will not know about it.
51
- * Therefore, subsequent calls to emitOK() may send a duplicate OK message.
52
- * */
53
- emit(message: unknown, socket?: MockServerSocket): string;
54
- /**
55
- * Send COUNT messages from all socket mocks
56
- * holding active COUNT subscriptions (in other words,
57
- * subscriptions that have not yet responded with COUNT)
58
- * with the given subId.
59
- */
60
- emitCOUNT(subId: string, count?: number): Nostr.ToClientMessage.COUNT;
61
- /**
62
- * Send EOSE messages from all socket mocks
63
- * holding active REQ subscriptions (in other words,
64
- * subscriptions that have not yet been CLOSE'd)
65
- * with the given subId.
66
- */
67
- emitEOSE(subId: string): Nostr.ToClientMessage.EOSE;
68
- /**
69
- * Send EVENT messages from all socket mocks
70
- * holding active REQ subscriptions (in other words,
71
- * subscriptions that have not yet been CLOSE'd)
72
- * with the given subId.
73
- */
74
- emitEVENT(subId: string, event?: Partial<Nostr.Event>): Nostr.ToClientMessage.EVENT;
75
- /**
76
- * Send OK messages from all socket mocks
77
- * holding active EVENT (in other words, an EVENT
78
- * that has not yet returned an OK message)
79
- * with the given eventId.
80
- */
81
- emitOK(eventId: string, succeeded: boolean, message?: string): Nostr.ToClientMessage.OK;
82
- }
83
- export declare function createMockRelay(url: string): MockRelay;
84
- export {};
@@ -1,181 +0,0 @@
1
- import { WS } from "vitest-websocket-mock";
2
- import { faker } from "./faker.js";
3
- import { withTimeout } from "./utils.js";
4
- export function createMockServer(url, behavior) {
5
- const server = new WS(url, { jsonProtocol: true });
6
- const sockets = [];
7
- let resolvers = [];
8
- let nextId = 1;
9
- server.on("connection", (_socket) => {
10
- const socket = {
11
- id: nextId++,
12
- send(message) {
13
- if (_socket.readyState !== WebSocket.OPEN) {
14
- return;
15
- }
16
- _socket.send(typeof message === "string" ? message : `${JSON.stringify(message)}`);
17
- },
18
- close(options) {
19
- _socket.close(options);
20
- },
21
- };
22
- sockets.push(socket);
23
- resolvers.filter(([count]) => count <= sockets.length).forEach(([, resolve]) => resolve([...sockets]));
24
- resolvers = resolvers.filter(([count]) => count > sockets.length);
25
- behavior.onOpen?.(socket);
26
- _socket.on("message", (message) => {
27
- if (typeof message !== "string") {
28
- throw new Error("Unexpected type message");
29
- }
30
- behavior.onMessage?.(socket, message);
31
- });
32
- _socket.on("close", () => {
33
- behavior.onClose?.(socket);
34
- });
35
- _socket.on("error", (error) => {
36
- behavior.onError?.(socket, error);
37
- });
38
- });
39
- const getSockets = async (count, options) => {
40
- const promise = new Promise((resolve) => {
41
- if (sockets.length >= count) {
42
- resolve([...sockets]);
43
- }
44
- else {
45
- resolvers.push([count, resolve]);
46
- }
47
- });
48
- return withTimeout(promise, `Mock relay was waiting for ${count} connections to be established, but timed out.`, options?.timeout);
49
- };
50
- const getSocket = async (index) => {
51
- const sockets = await getSockets(index + 1);
52
- return sockets[index];
53
- };
54
- return Object.assign(server, {
55
- getSockets,
56
- getSocket,
57
- getSocketsSync: () => [...sockets],
58
- });
59
- }
60
- function relayBehavior() {
61
- const reqs = new Map();
62
- const counts = new Map();
63
- const events = new Map();
64
- const sockets = new Set();
65
- return {
66
- onOpen(socket) {
67
- sockets.add(socket);
68
- },
69
- onMessage(socket, rawMessage) {
70
- const message = JSON.parse(rawMessage);
71
- switch (message[0]) {
72
- case "CLOSE": {
73
- const subId = message[1];
74
- reqs.get(socket)?.delete(subId);
75
- break;
76
- }
77
- case "COUNT": {
78
- if (!counts.has(socket)) {
79
- counts.set(socket, new Set());
80
- }
81
- const subId = message[1];
82
- counts.get(socket)?.add(subId);
83
- break;
84
- }
85
- case "EVENT": {
86
- if (!events.has(socket)) {
87
- events.set(socket, new Set());
88
- }
89
- const eventId = message[1].id;
90
- events.get(socket)?.add(eventId);
91
- break;
92
- }
93
- case "REQ": {
94
- if (!reqs.has(socket)) {
95
- reqs.set(socket, new Set());
96
- }
97
- const subId = message[1];
98
- reqs.get(socket)?.add(subId);
99
- break;
100
- }
101
- }
102
- },
103
- onClose(socket) {
104
- reqs.delete(socket);
105
- counts.delete(socket);
106
- events.delete(socket);
107
- sockets.delete(socket);
108
- },
109
- emit(message, socket) {
110
- const toBeSent = typeof message === "string" ? message : `${JSON.stringify(message)}`;
111
- if (socket) {
112
- socket.send(toBeSent);
113
- }
114
- else {
115
- for (const socket of sockets) {
116
- socket.send(toBeSent);
117
- }
118
- }
119
- return toBeSent;
120
- },
121
- emitCOUNT(subId, count) {
122
- const toBeSent = faker.toClientMessage.COUNT(subId, count);
123
- for (const [socket, subIds] of counts.entries()) {
124
- if (subIds.has(subId)) {
125
- socket.send(toBeSent);
126
- }
127
- subIds.delete(subId);
128
- }
129
- return toBeSent;
130
- },
131
- emitEOSE(subId) {
132
- const toBeSent = faker.toClientMessage.EOSE(subId);
133
- for (const [socket, subIds] of reqs.entries()) {
134
- if (subIds.has(subId)) {
135
- socket.send(toBeSent);
136
- }
137
- }
138
- return toBeSent;
139
- },
140
- emitEVENT(subId, event) {
141
- const toBeSent = faker.toClientMessage.EVENT(subId, event);
142
- for (const [socket, subIds] of reqs.entries()) {
143
- if (subIds.has(subId)) {
144
- socket.send(toBeSent);
145
- }
146
- }
147
- return toBeSent;
148
- },
149
- emitOK(eventId, succeeded, message) {
150
- const toBeSent = faker.toClientMessage.OK(eventId, succeeded, message);
151
- for (const [socket, eventIds] of events.entries()) {
152
- if (eventIds.has(eventId)) {
153
- socket.send(toBeSent);
154
- }
155
- eventIds.delete(eventId);
156
- }
157
- return toBeSent;
158
- },
159
- };
160
- }
161
- export function createMockRelay(url) {
162
- const behavior = relayBehavior();
163
- const server = createMockServer(url, behavior);
164
- const { emit, emitCOUNT, emitEOSE, emitEVENT, emitOK } = behavior;
165
- const timeoutMessage = "Mock relay was waiting for the next message from the client, but timed out.";
166
- return Object.assign(server, {
167
- next: (timeout) => withTimeout(server.nextMessage, timeoutMessage, timeout),
168
- nexts: (count, timeout) => withTimeout(async () => {
169
- const messages = [];
170
- for (let i = 0; i < count; i++) {
171
- messages.push((await server.nextMessage));
172
- }
173
- return messages;
174
- }, timeoutMessage, timeout),
175
- emit,
176
- emitCOUNT,
177
- emitEOSE,
178
- emitEVENT,
179
- emitOK,
180
- });
181
- }
@@ -1,7 +0,0 @@
1
- import Nostr from "nostr-typedef";
2
- export interface ClientSpy {
3
- __createClientSpy: true;
4
- next: () => Promise<Nostr.ToClientMessage.Any>;
5
- dispose: () => void;
6
- }
7
- export declare function createClientSpy(addOnMessageListener: (listener: (message: Nostr.ToClientMessage.Any) => void) => void): ClientSpy;
@@ -1,65 +0,0 @@
1
- export function createClientSpy(addOnMessageListener) {
2
- let queue = { type: "empty" };
3
- addOnMessageListener((message) => {
4
- switch (queue.type) {
5
- case "empty": {
6
- queue = {
7
- type: "message",
8
- items: [message],
9
- };
10
- break;
11
- }
12
- case "message": {
13
- queue.items.push(message);
14
- break;
15
- }
16
- case "resolver": {
17
- const item = queue.items.shift();
18
- if (item) {
19
- const resolve = item[0];
20
- resolve(message);
21
- }
22
- if (queue.items.length <= 0) {
23
- queue = { type: "empty" };
24
- }
25
- }
26
- }
27
- });
28
- return {
29
- __createClientSpy: true,
30
- next: () => new Promise((resolve, reject) => {
31
- switch (queue.type) {
32
- case "empty": {
33
- queue = {
34
- type: "resolver",
35
- items: [[resolve, reject]],
36
- };
37
- break;
38
- }
39
- case "message": {
40
- const msg = queue.items.shift();
41
- if (msg) {
42
- resolve(msg);
43
- }
44
- if (queue.items.length <= 0) {
45
- queue = { type: "empty" };
46
- }
47
- break;
48
- }
49
- case "resolver": {
50
- queue.items.push([resolve, reject]);
51
- break;
52
- }
53
- }
54
- }),
55
- dispose: () => {
56
- if (queue.type === "resolver") {
57
- for (const item of queue.items) {
58
- const reject = item[1];
59
- reject(new Error());
60
- }
61
- }
62
- queue = { type: "empty" };
63
- },
64
- };
65
- }
@@ -1,3 +0,0 @@
1
- export declare class TimeoutError extends Error {
2
- }
3
- export declare function withTimeout<T>(task: Promise<T> | (() => Promise<T>), message?: string, timeout?: number): Promise<T>;
@@ -1,8 +0,0 @@
1
- export class TimeoutError extends Error {
2
- }
3
- export function withTimeout(task, message, timeout = 1000) {
4
- return Promise.race([
5
- typeof task === "function" ? task() : task,
6
- new Promise((_, reject) => setTimeout(() => reject(new TimeoutError(message)), timeout)),
7
- ]);
8
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,9 +0,0 @@
1
- import { z } from "zod";
2
- const IncomingREQ = z.tuple([z.literal("REQ"), z.string(), z.array(z.any())]);
3
- const IncomingEVENT = z.tuple([z.literal("EVENT"), z.any()]);
4
- const IncomingCLOSE = z.tuple([z.literal("CLOSE"), z.string()]);
5
- const IncomingAUTH = z.tuple([z.literal("AUTH"), z.any()]);
6
- const OutgoingEVENT = z.tuple([z.literal("EVENT"), z.string(), z.any()]);
7
- const OutgoingNOTICE = z.tuple([z.literal("NOTICE"), z.string()]);
8
- const OutgoingCLOSE = z.tuple([z.literal("CLOSE"), z.string()]);
9
- const OutgoingAUTH = z.tuple([z.literal("AUTH"), z.string()]);