applesauce-relay 0.0.0-next-20250327153627 → 0.0.0-next-20250330153313
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/README.md +94 -0
- package/dist/group.d.ts +10 -0
- package/dist/group.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/operators/mark-from-relay.d.ts +1 -1
- package/dist/operators/only-events.d.ts +1 -1
- package/dist/pool.d.ts +11 -4
- package/dist/pool.js +21 -23
- package/dist/relay.d.ts +19 -13
- package/dist/relay.js +80 -28
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -1,3 +1,97 @@
|
|
|
1
1
|
# Applesauce Relay
|
|
2
2
|
|
|
3
3
|
`applesauce-relay` is a nostr relay communication framework built on top of [RxJS](https://rxjs.dev/)
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### Single Relay
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Relay } from "./relay";
|
|
11
|
+
|
|
12
|
+
// Connect to a single relay
|
|
13
|
+
const relay = new Relay("wss://relay.example.com");
|
|
14
|
+
|
|
15
|
+
// Subscribe to events
|
|
16
|
+
relay
|
|
17
|
+
.req({
|
|
18
|
+
kinds: [1],
|
|
19
|
+
limit: 10,
|
|
20
|
+
})
|
|
21
|
+
.subscribe((response) => {
|
|
22
|
+
if (response === "EOSE") {
|
|
23
|
+
console.log("End of stored events");
|
|
24
|
+
} else {
|
|
25
|
+
console.log("Received event:", response);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Publish an event
|
|
30
|
+
const event = {
|
|
31
|
+
kind: 1,
|
|
32
|
+
content: "Hello Nostr!",
|
|
33
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
34
|
+
tags: [],
|
|
35
|
+
// ... other required fields
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
relay.event(event).subscribe((response) => {
|
|
39
|
+
console.log(`Published:`, response.ok);
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Relay Pool
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { RelayPool } from "./pool";
|
|
47
|
+
|
|
48
|
+
// Create a pool and connect to multiple relays
|
|
49
|
+
const pool = new RelayPool();
|
|
50
|
+
const relays = ["wss://relay1.example.com", "wss://relay2.example.com"];
|
|
51
|
+
|
|
52
|
+
// Subscribe to events from multiple relays
|
|
53
|
+
pool
|
|
54
|
+
.req(relays, {
|
|
55
|
+
kinds: [1],
|
|
56
|
+
limit: 10,
|
|
57
|
+
})
|
|
58
|
+
.subscribe((response) => {
|
|
59
|
+
if (response === "EOSE") {
|
|
60
|
+
console.log("End of stored events");
|
|
61
|
+
} else {
|
|
62
|
+
console.log("Received event:", response);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Publish to multiple relays
|
|
67
|
+
pool.event(relays, event).subscribe((response) => {
|
|
68
|
+
console.log(`Published to ${response.from}:`, response.ok);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Relay Group
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { RelayPool } from "./pool";
|
|
76
|
+
|
|
77
|
+
const pool = new RelayPool();
|
|
78
|
+
const relays = ["wss://relay1.example.com", "wss://relay2.example.com"];
|
|
79
|
+
|
|
80
|
+
// Create a group (automatically deduplicates events)
|
|
81
|
+
const group = pool.group(relays);
|
|
82
|
+
|
|
83
|
+
// Subscribe to events
|
|
84
|
+
group
|
|
85
|
+
.req({
|
|
86
|
+
kinds: [1],
|
|
87
|
+
limit: 10,
|
|
88
|
+
})
|
|
89
|
+
.subscribe((response) => {
|
|
90
|
+
console.log("Received:", response);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Publish to all relays in group
|
|
94
|
+
group.event(event).subscribe((response) => {
|
|
95
|
+
console.log(`Published to ${response.from}:`, response.ok);
|
|
96
|
+
});
|
|
97
|
+
```
|
package/dist/group.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { Filter } from "nostr-tools";
|
|
4
|
+
import { IRelay, Nip01Actions, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
5
|
+
export declare class RelayGroup implements Nip01Actions {
|
|
6
|
+
relays: IRelay[];
|
|
7
|
+
constructor(relays: IRelay[]);
|
|
8
|
+
req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
|
|
9
|
+
event(event: NostrEvent): Observable<PublishResponse>;
|
|
10
|
+
}
|
package/dist/group.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { combineLatest, filter, map, merge } from "rxjs";
|
|
2
|
+
import { onlyEvents } from "./operators/only-events.js";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
export class RelayGroup {
|
|
5
|
+
relays;
|
|
6
|
+
constructor(relays) {
|
|
7
|
+
this.relays = relays;
|
|
8
|
+
}
|
|
9
|
+
req(filters, id = nanoid()) {
|
|
10
|
+
const requests = this.relays.reduce((acc, r) => ({ ...acc, [r.url]: r.req(filters, id) }), {});
|
|
11
|
+
// Create stream of events only
|
|
12
|
+
const events = merge(...Object.values(requests)).pipe(onlyEvents());
|
|
13
|
+
// Create stream that emits EOSE when all relays have sent EOSE
|
|
14
|
+
const eose = combineLatest(
|
|
15
|
+
// Create a new map of requests that only emits EOSE
|
|
16
|
+
Object.fromEntries(Object.entries(requests).map(([url, observable]) => [url, observable.pipe(filter((m) => m === "EOSE"))]))).pipe(map(() => "EOSE"));
|
|
17
|
+
// Merge events and the single EOSE stream
|
|
18
|
+
return merge(events, eose);
|
|
19
|
+
}
|
|
20
|
+
event(event) {
|
|
21
|
+
return merge(...this.relays.map((r) => r.event(event)));
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { MonoTypeOperatorFunction } from "rxjs";
|
|
2
|
-
import { SubscriptionResponse } from "../
|
|
2
|
+
import { SubscriptionResponse } from "../types.js";
|
|
3
3
|
/** Marks all events as from the relay */
|
|
4
4
|
export declare function markFromRelay(relay: string): MonoTypeOperatorFunction<SubscriptionResponse>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OperatorFunction } from "rxjs";
|
|
2
2
|
import { NostrEvent } from "nostr-tools";
|
|
3
|
-
import { SubscriptionResponse } from "../
|
|
3
|
+
import { SubscriptionResponse } from "../types.js";
|
|
4
4
|
/** Filter subscription responses and only return the events */
|
|
5
5
|
export declare function onlyEvents(): OperatorFunction<SubscriptionResponse, NostrEvent>;
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { NostrEvent, type Filter } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
|
-
import { Relay,
|
|
3
|
+
import { Relay, RelayOptions } from "./relay.js";
|
|
4
|
+
import { PublishResponse, SubscriptionResponse } from "./types.js";
|
|
5
|
+
import { RelayGroup } from "./group.js";
|
|
4
6
|
export declare class RelayPool {
|
|
7
|
+
options?: RelayOptions | undefined;
|
|
5
8
|
relays: Map<string, Relay>;
|
|
9
|
+
groups: Map<string, RelayGroup>;
|
|
10
|
+
constructor(options?: RelayOptions | undefined);
|
|
6
11
|
/** Get or create a new relay connection */
|
|
7
12
|
relay(url: string): Relay;
|
|
8
|
-
/**
|
|
9
|
-
|
|
13
|
+
/** Create a group of relays */
|
|
14
|
+
group(relays: string[]): RelayGroup;
|
|
15
|
+
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
16
|
+
req(relays: string[], filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
|
|
10
17
|
/** Send an EVENT message to multiple relays */
|
|
11
|
-
event(relays: string[], event: NostrEvent): Observable<PublishResponse
|
|
18
|
+
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
12
19
|
}
|
package/dist/pool.js
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
|
-
import { combineLatest, endWith, ignoreElements, merge, takeWhile } from "rxjs";
|
|
2
|
-
import { nanoid } from "nanoid";
|
|
3
1
|
import { Relay } from "./relay.js";
|
|
4
|
-
import {
|
|
2
|
+
import { RelayGroup } from "./group.js";
|
|
5
3
|
export class RelayPool {
|
|
4
|
+
options;
|
|
6
5
|
relays = new Map();
|
|
6
|
+
groups = new Map();
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
7
10
|
/** Get or create a new relay connection */
|
|
8
11
|
relay(url) {
|
|
9
12
|
let relay = this.relays.get(url);
|
|
10
13
|
if (relay)
|
|
11
14
|
return relay;
|
|
12
15
|
else {
|
|
13
|
-
relay = new Relay(url);
|
|
16
|
+
relay = new Relay(url, this.options);
|
|
14
17
|
this.relays.set(url, relay);
|
|
15
18
|
return relay;
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
endWith("EOSE"));
|
|
32
|
-
// create a stream that only emits events
|
|
33
|
-
const events = merge(...requests).pipe(onlyEvents());
|
|
34
|
-
// merge events and single EOSE streams
|
|
35
|
-
return merge(events, eose);
|
|
21
|
+
/** Create a group of relays */
|
|
22
|
+
group(relays) {
|
|
23
|
+
const key = relays.sort().join(",");
|
|
24
|
+
let group = this.groups.get(key);
|
|
25
|
+
if (group)
|
|
26
|
+
return group;
|
|
27
|
+
group = new RelayGroup(relays.map((url) => this.relay(url)));
|
|
28
|
+
this.groups.set(key, group);
|
|
29
|
+
return group;
|
|
30
|
+
}
|
|
31
|
+
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
32
|
+
req(relays, filters, id) {
|
|
33
|
+
return this.group(relays).req(filters, id);
|
|
36
34
|
}
|
|
37
35
|
/** Send an EVENT message to multiple relays */
|
|
38
36
|
event(relays, event) {
|
|
39
|
-
return
|
|
37
|
+
return this.group(relays).event(event);
|
|
40
38
|
}
|
|
41
39
|
}
|
package/dist/relay.d.ts
CHANGED
|
@@ -2,29 +2,35 @@ import { BehaviorSubject, Observable } from "rxjs";
|
|
|
2
2
|
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
3
3
|
import { type Filter, type NostrEvent } from "nostr-tools";
|
|
4
4
|
import { logger } from "applesauce-core";
|
|
5
|
-
|
|
6
|
-
export type PublishResponse = {
|
|
7
|
-
ok: boolean;
|
|
8
|
-
message?: string;
|
|
9
|
-
from: string;
|
|
10
|
-
};
|
|
5
|
+
import { IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
11
6
|
export type RelayOptions = {
|
|
12
7
|
WebSocket?: WebSocketSubjectConfig<any>["WebSocketCtor"];
|
|
13
8
|
};
|
|
14
|
-
export declare class Relay {
|
|
9
|
+
export declare class Relay implements IRelay {
|
|
15
10
|
url: string;
|
|
16
|
-
log: typeof logger;
|
|
17
|
-
socket
|
|
11
|
+
protected log: typeof logger;
|
|
12
|
+
protected socket: WebSocketSubject<any>;
|
|
18
13
|
connected$: BehaviorSubject<boolean>;
|
|
19
|
-
challenge$:
|
|
14
|
+
challenge$: BehaviorSubject<string | null>;
|
|
20
15
|
authenticated$: BehaviorSubject<boolean>;
|
|
21
|
-
notices$:
|
|
16
|
+
notices$: BehaviorSubject<string[]>;
|
|
17
|
+
/** An observable of all messages from the relay */
|
|
18
|
+
message$: Observable<any>;
|
|
19
|
+
/** An observable of NOTICE messages from the relay */
|
|
20
|
+
notice$: Observable<string>;
|
|
21
|
+
get connected(): boolean;
|
|
22
|
+
get challenge(): string | null;
|
|
23
|
+
get notices(): string[];
|
|
24
|
+
get authenticated(): boolean;
|
|
22
25
|
protected authRequiredForReq: BehaviorSubject<boolean>;
|
|
23
26
|
protected authRequiredForPublish: BehaviorSubject<boolean>;
|
|
24
|
-
protected
|
|
27
|
+
protected resetState(): void;
|
|
28
|
+
/** An internal observable that is responsible for watching all messages and updating state */
|
|
29
|
+
protected watchTower: Observable<never>;
|
|
25
30
|
constructor(url: string, opts?: RelayOptions);
|
|
26
31
|
protected waitForAuth<T extends unknown = unknown>(requireAuth: Observable<boolean>, observable: Observable<T>): Observable<T>;
|
|
27
|
-
|
|
32
|
+
multiplex<T>(open: () => any, close: () => any, filter: (message: any) => boolean): Observable<T>;
|
|
33
|
+
req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
|
|
28
34
|
/** send an Event message */
|
|
29
35
|
event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
|
|
30
36
|
/** send and AUTH message */
|
package/dist/relay.js
CHANGED
|
@@ -1,75 +1,122 @@
|
|
|
1
|
-
import { BehaviorSubject, combineLatest,
|
|
1
|
+
import { BehaviorSubject, combineLatest, filter, ignoreElements, map, merge, NEVER, of, scan, share, switchMap, take, takeWhile, tap, timeout, } from "rxjs";
|
|
2
2
|
import { webSocket } from "rxjs/webSocket";
|
|
3
3
|
import { nanoid } from "nanoid";
|
|
4
4
|
import { logger } from "applesauce-core";
|
|
5
5
|
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
6
6
|
export class Relay {
|
|
7
7
|
url;
|
|
8
|
-
log = logger.extend("
|
|
9
|
-
socket
|
|
8
|
+
log = logger.extend("Relay");
|
|
9
|
+
socket;
|
|
10
10
|
connected$ = new BehaviorSubject(false);
|
|
11
|
-
challenge
|
|
11
|
+
challenge$ = new BehaviorSubject(null);
|
|
12
12
|
authenticated$ = new BehaviorSubject(false);
|
|
13
|
-
notices
|
|
13
|
+
notices$ = new BehaviorSubject([]);
|
|
14
|
+
/** An observable of all messages from the relay */
|
|
15
|
+
message$;
|
|
16
|
+
/** An observable of NOTICE messages from the relay */
|
|
17
|
+
notice$;
|
|
18
|
+
// sync state
|
|
19
|
+
get connected() {
|
|
20
|
+
return this.connected$.value;
|
|
21
|
+
}
|
|
22
|
+
get challenge() {
|
|
23
|
+
return this.challenge$.value;
|
|
24
|
+
}
|
|
25
|
+
get notices() {
|
|
26
|
+
return this.notices$.value;
|
|
27
|
+
}
|
|
28
|
+
get authenticated() {
|
|
29
|
+
return this.authenticated$.value;
|
|
30
|
+
}
|
|
14
31
|
authRequiredForReq = new BehaviorSubject(false);
|
|
15
32
|
authRequiredForPublish = new BehaviorSubject(false);
|
|
16
|
-
|
|
17
|
-
this
|
|
18
|
-
this.
|
|
19
|
-
|
|
33
|
+
resetState() {
|
|
34
|
+
// NOTE: only update the values if they need to be changed, otherwise this will cause an infinite loop
|
|
35
|
+
if (this.challenge$.value !== null)
|
|
36
|
+
this.challenge$.next(null);
|
|
37
|
+
if (this.authenticated$.value)
|
|
38
|
+
this.authenticated$.next(false);
|
|
39
|
+
if (this.notices$.value.length > 0)
|
|
40
|
+
this.notices$.next([]);
|
|
41
|
+
if (this.authRequiredForReq.value)
|
|
42
|
+
this.authRequiredForReq.next(false);
|
|
43
|
+
if (this.authRequiredForPublish.value)
|
|
44
|
+
this.authRequiredForPublish.next(false);
|
|
20
45
|
}
|
|
46
|
+
/** An internal observable that is responsible for watching all messages and updating state */
|
|
47
|
+
watchTower;
|
|
21
48
|
constructor(url, opts) {
|
|
22
49
|
this.url = url;
|
|
23
50
|
this.log = this.log.extend(url);
|
|
24
|
-
this.socket
|
|
51
|
+
this.socket = webSocket({
|
|
25
52
|
url,
|
|
26
53
|
openObserver: {
|
|
27
54
|
next: () => {
|
|
28
55
|
this.log("Connected");
|
|
29
56
|
this.connected$.next(true);
|
|
30
|
-
this.
|
|
57
|
+
this.resetState();
|
|
31
58
|
},
|
|
32
59
|
},
|
|
33
60
|
closeObserver: {
|
|
34
61
|
next: () => {
|
|
35
62
|
this.log("Disconnected");
|
|
36
63
|
this.connected$.next(false);
|
|
37
|
-
this.
|
|
64
|
+
this.resetState();
|
|
38
65
|
},
|
|
39
66
|
},
|
|
40
67
|
WebSocketCtor: opts?.WebSocket,
|
|
41
68
|
});
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
// listen for AUTH messages
|
|
45
|
-
filter((message) => message[0] === "AUTH"),
|
|
46
|
-
// pick the challenge string out
|
|
47
|
-
map((m) => m[1]),
|
|
48
|
-
// cache and share the challenge
|
|
49
|
-
shareReplay(1));
|
|
50
|
-
this.notices$ = this.socket$.pipe(
|
|
69
|
+
this.message$ = this.socket.asObservable();
|
|
70
|
+
this.notice$ = this.message$.pipe(
|
|
51
71
|
// listen for NOTICE messages
|
|
52
72
|
filter((m) => m[0] === "NOTICE"),
|
|
53
73
|
// pick the string out of the message
|
|
54
74
|
map((m) => m[1]));
|
|
75
|
+
// Update the notices state
|
|
76
|
+
const notice = this.notice$.pipe(
|
|
77
|
+
// Track all notices
|
|
78
|
+
scan((acc, notice) => [...acc, notice], []),
|
|
79
|
+
// Update the notices state
|
|
80
|
+
tap((notices) => this.notices$.next(notices)));
|
|
81
|
+
// Update the challenge state
|
|
82
|
+
const challenge = this.message$.pipe(
|
|
83
|
+
// listen for AUTH messages
|
|
84
|
+
filter((message) => message[0] === "AUTH"),
|
|
85
|
+
// pick the challenge string out
|
|
86
|
+
map((m) => m[1]),
|
|
87
|
+
// Update the challenge state
|
|
88
|
+
tap((challenge) => {
|
|
89
|
+
this.log("Received AUTH challenge", challenge);
|
|
90
|
+
this.challenge$.next(challenge);
|
|
91
|
+
}));
|
|
92
|
+
// Merge all watchers
|
|
93
|
+
this.watchTower = merge(notice, challenge).pipe(
|
|
94
|
+
// Never emit any values
|
|
95
|
+
ignoreElements(),
|
|
96
|
+
// There should only be a single watch tower
|
|
97
|
+
share());
|
|
55
98
|
}
|
|
56
99
|
waitForAuth(requireAuth, observable) {
|
|
57
100
|
return combineLatest([requireAuth, this.authenticated$]).pipe(
|
|
58
101
|
// return EMPTY if auth is required and not authenticated
|
|
59
102
|
switchMap(([required, authenticated]) => {
|
|
60
103
|
if (required && !authenticated)
|
|
61
|
-
return
|
|
104
|
+
return NEVER;
|
|
62
105
|
else
|
|
63
106
|
return observable;
|
|
64
107
|
}));
|
|
65
108
|
}
|
|
109
|
+
multiplex(open, close, filter) {
|
|
110
|
+
return this.socket.multiplex(open, close, filter);
|
|
111
|
+
}
|
|
66
112
|
req(filters, id = nanoid()) {
|
|
67
|
-
|
|
68
|
-
.multiplex(() => ["REQ", id, ...filters], () => ["CLOSE", id], (message) => (message[0] === "EVENT" || message[0] === "CLOSE" || message[0] === "EOSE") && message[1] === id)
|
|
113
|
+
const request = this.socket
|
|
114
|
+
.multiplex(() => (Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters]), () => ["CLOSE", id], (message) => (message[0] === "EVENT" || message[0] === "CLOSE" || message[0] === "EOSE") && message[1] === id)
|
|
69
115
|
.pipe(
|
|
70
116
|
// listen for CLOSE auth-required
|
|
71
117
|
tap((m) => {
|
|
72
118
|
if (m[0] === "CLOSE" && m[1].startsWith("auth-required") && !this.authRequiredForReq.value) {
|
|
119
|
+
this.log("Auth required for REQ");
|
|
73
120
|
this.authRequiredForReq.next(true);
|
|
74
121
|
}
|
|
75
122
|
}),
|
|
@@ -85,14 +132,17 @@ export class Relay {
|
|
|
85
132
|
// mark events as from relays
|
|
86
133
|
markFromRelay(this.url),
|
|
87
134
|
// if no events are seen in 10s, emit EOSE
|
|
135
|
+
// TODO: this should emit EOSE event if events are seen, the timeout should be for only the EOSE message
|
|
88
136
|
timeout({
|
|
89
137
|
first: 10_000,
|
|
90
138
|
with: () => merge(of("EOSE"), NEVER),
|
|
91
|
-
}))
|
|
139
|
+
}));
|
|
140
|
+
// Wait for auth if required and make sure to start the watch tower
|
|
141
|
+
return this.waitForAuth(this.authRequiredForReq, merge(this.watchTower, request));
|
|
92
142
|
}
|
|
93
143
|
/** send an Event message */
|
|
94
144
|
event(event, verb = "EVENT") {
|
|
95
|
-
const observable = this.socket
|
|
145
|
+
const observable = this.socket
|
|
96
146
|
.multiplex(() => [verb, event], () => void 0, (m) => m[0] === "OK" && m[1] === event.id)
|
|
97
147
|
.pipe(
|
|
98
148
|
// format OK message
|
|
@@ -102,14 +152,16 @@ export class Relay {
|
|
|
102
152
|
// listen for OK auth-required
|
|
103
153
|
tap(({ ok, message }) => {
|
|
104
154
|
if (ok === false && message.startsWith("auth-required") && !this.authRequiredForPublish.value) {
|
|
155
|
+
this.log("Auth required for publish");
|
|
105
156
|
this.authRequiredForPublish.next(true);
|
|
106
157
|
}
|
|
107
158
|
}));
|
|
159
|
+
const withWatchTower = merge(this.watchTower, observable);
|
|
108
160
|
// skip wait for auth if verb is AUTH
|
|
109
161
|
if (verb === "AUTH")
|
|
110
|
-
return
|
|
162
|
+
return withWatchTower;
|
|
111
163
|
else
|
|
112
|
-
return this.waitForAuth(this.authRequiredForPublish,
|
|
164
|
+
return this.waitForAuth(this.authRequiredForPublish, withWatchTower);
|
|
113
165
|
}
|
|
114
166
|
/** send and AUTH message */
|
|
115
167
|
auth(event) {
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { WebSocketSubject } from "rxjs/webSocket";
|
|
4
|
+
export type SubscriptionResponse = "EOSE" | NostrEvent;
|
|
5
|
+
export type PublishResponse = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
from: string;
|
|
9
|
+
};
|
|
10
|
+
export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
|
|
11
|
+
export interface IRelayState {
|
|
12
|
+
connected$: Observable<boolean>;
|
|
13
|
+
challenge$: Observable<string | null>;
|
|
14
|
+
authenticated$: Observable<boolean>;
|
|
15
|
+
notices$: Observable<string[]>;
|
|
16
|
+
}
|
|
17
|
+
export interface Nip01Actions {
|
|
18
|
+
/** Send an EVENT message */
|
|
19
|
+
event(event: NostrEvent): Observable<PublishResponse>;
|
|
20
|
+
/** Send a REQ message */
|
|
21
|
+
req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
|
|
22
|
+
}
|
|
23
|
+
export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
|
|
24
|
+
url: string;
|
|
25
|
+
message$: Observable<any>;
|
|
26
|
+
notice$: Observable<string>;
|
|
27
|
+
readonly connected: boolean;
|
|
28
|
+
readonly authenticated: boolean;
|
|
29
|
+
readonly challenge: string | null;
|
|
30
|
+
readonly notices: string[];
|
|
31
|
+
/** Send an AUTH message */
|
|
32
|
+
auth(event: NostrEvent): Observable<{
|
|
33
|
+
ok: boolean;
|
|
34
|
+
message?: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface IPoolActions {
|
|
38
|
+
/** Send an EVENT message */
|
|
39
|
+
event(relays: string[], event: NostrEvent): Observable<PublishResponse[]>;
|
|
40
|
+
/** Send a REQ message */
|
|
41
|
+
req(relays: string[], filters: Filter | Filter[]): Observable<SubscriptionResponse[]>;
|
|
42
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
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-20250330153313",
|
|
4
4
|
"description": "A collection of observable based loaders built on rx-nostr",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,6 +21,21 @@
|
|
|
21
21
|
"require": "./dist/index.js",
|
|
22
22
|
"types": "./dist/index.d.ts"
|
|
23
23
|
},
|
|
24
|
+
"./pool": {
|
|
25
|
+
"import": "./dist/pool.js",
|
|
26
|
+
"require": "./dist/pool.js",
|
|
27
|
+
"types": "./dist/pool.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./relay": {
|
|
30
|
+
"import": "./dist/relay.js",
|
|
31
|
+
"require": "./dist/relay.js",
|
|
32
|
+
"types": "./dist/relay.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"./types": {
|
|
35
|
+
"import": "./dist/types.js",
|
|
36
|
+
"require": "./dist/types.js",
|
|
37
|
+
"types": "./dist/types.d.ts"
|
|
38
|
+
},
|
|
24
39
|
"./operators": {
|
|
25
40
|
"import": "./dist/operators/index.js",
|
|
26
41
|
"require": "./dist/operators/index.js",
|
|
@@ -33,7 +48,7 @@
|
|
|
33
48
|
}
|
|
34
49
|
},
|
|
35
50
|
"dependencies": {
|
|
36
|
-
"applesauce-core": "0.0.0-next-
|
|
51
|
+
"applesauce-core": "0.0.0-next-20250330153313",
|
|
37
52
|
"nanoid": "^5.0.9",
|
|
38
53
|
"nostr-tools": "^2.10.4",
|
|
39
54
|
"rxjs": "^7.8.1"
|