@nostrify/nostrify 0.46.4
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/.turbo/turbo-build.log +5 -0
- package/BunkerURI.test.ts +28 -0
- package/BunkerURI.ts +58 -0
- package/CHANGELOG.md +562 -0
- package/LICENSE +21 -0
- package/NBrowserSigner.test.ts +170 -0
- package/NBrowserSigner.ts +100 -0
- package/NCache.bench.ts +81 -0
- package/NCache.test.ts +22 -0
- package/NCache.ts +73 -0
- package/NConnectSigner.test.ts +102 -0
- package/NConnectSigner.ts +189 -0
- package/NIP05.test.ts +67 -0
- package/NIP05.ts +52 -0
- package/NIP50.test.ts +58 -0
- package/NIP50.ts +24 -0
- package/NIP98.test.ts +181 -0
- package/NIP98.ts +97 -0
- package/NKinds.test.ts +42 -0
- package/NKinds.ts +26 -0
- package/NPool.test.ts +117 -0
- package/NPool.ts +224 -0
- package/NRelay1.test.ts +174 -0
- package/NRelay1.ts +440 -0
- package/NSchema.test.ts +94 -0
- package/NSchema.ts +255 -0
- package/NSecSigner.bench.ts +55 -0
- package/NSecSigner.test.ts +26 -0
- package/NSecSigner.ts +60 -0
- package/NSet.bench.ts +10 -0
- package/NSet.test.ts +92 -0
- package/NSet.ts +203 -0
- package/README.md +314 -0
- package/RelayError.test.ts +23 -0
- package/RelayError.ts +22 -0
- package/dist/BunkerURI.d.ts +23 -0
- package/dist/BunkerURI.d.ts.map +1 -0
- package/dist/BunkerURI.js +52 -0
- package/dist/BunkerURI.js.map +1 -0
- package/dist/NBrowserSigner.d.ts +27 -0
- package/dist/NBrowserSigner.d.ts.map +1 -0
- package/dist/NBrowserSigner.js +96 -0
- package/dist/NBrowserSigner.js.map +1 -0
- package/dist/NCache.d.ts +34 -0
- package/dist/NCache.d.ts.map +1 -0
- package/dist/NCache.js +63 -0
- package/dist/NCache.js.map +1 -0
- package/dist/NConnectSigner.d.ts +50 -0
- package/dist/NConnectSigner.d.ts.map +1 -0
- package/dist/NConnectSigner.js +130 -0
- package/dist/NConnectSigner.js.map +1 -0
- package/dist/NIP05.d.ts +15 -0
- package/dist/NIP05.d.ts.map +1 -0
- package/dist/NIP05.js +40 -0
- package/dist/NIP05.js.map +1 -0
- package/dist/NIP50.d.ts +10 -0
- package/dist/NIP50.d.ts.map +1 -0
- package/dist/NIP50.js +26 -0
- package/dist/NIP50.js.map +1 -0
- package/dist/NIP98.d.ts +15 -0
- package/dist/NIP98.d.ts.map +1 -0
- package/dist/NIP98.js +71 -0
- package/dist/NIP98.js.map +1 -0
- package/dist/NKinds.d.ts +13 -0
- package/dist/NKinds.d.ts.map +1 -0
- package/dist/NKinds.js +27 -0
- package/dist/NKinds.js.map +1 -0
- package/dist/NPool.d.ts +91 -0
- package/dist/NPool.d.ts.map +1 -0
- package/dist/NPool.js +185 -0
- package/dist/NPool.js.map +1 -0
- package/dist/NRelay1.d.ts +80 -0
- package/dist/NRelay1.d.ts.map +1 -0
- package/dist/NRelay1.js +336 -0
- package/dist/NRelay1.js.map +1 -0
- package/dist/NSchema.d.ts +73 -0
- package/dist/NSchema.d.ts.map +1 -0
- package/dist/NSchema.js +215 -0
- package/dist/NSchema.js.map +1 -0
- package/dist/NSecSigner.d.ts +29 -0
- package/dist/NSecSigner.d.ts.map +1 -0
- package/dist/NSecSigner.js +52 -0
- package/dist/NSecSigner.js.map +1 -0
- package/dist/NSet.d.ts +76 -0
- package/dist/NSet.d.ts.map +1 -0
- package/dist/NSet.js +174 -0
- package/dist/NSet.js.map +1 -0
- package/dist/RelayError.d.ts +10 -0
- package/dist/RelayError.d.ts.map +1 -0
- package/dist/RelayError.js +23 -0
- package/dist/RelayError.js.map +1 -0
- package/dist/ln/LNURL.d.ts +55 -0
- package/dist/ln/LNURL.d.ts.map +1 -0
- package/dist/ln/LNURL.js +105 -0
- package/dist/ln/LNURL.js.map +1 -0
- package/dist/ln/mod.d.ts +4 -0
- package/dist/ln/mod.d.ts.map +1 -0
- package/dist/ln/mod.js +6 -0
- package/dist/ln/mod.js.map +1 -0
- package/dist/ln/types/LNURLCallback.d.ts +8 -0
- package/dist/ln/types/LNURLCallback.d.ts.map +1 -0
- package/dist/ln/types/LNURLCallback.js +3 -0
- package/dist/ln/types/LNURLCallback.js.map +1 -0
- package/dist/ln/types/LNURLDetails.d.ts +20 -0
- package/dist/ln/types/LNURLDetails.d.ts.map +1 -0
- package/dist/ln/types/LNURLDetails.js +3 -0
- package/dist/ln/types/LNURLDetails.js.map +1 -0
- package/dist/mod.d.ts +16 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +32 -0
- package/dist/mod.js.map +1 -0
- package/dist/test/ErrorRelay.d.ts +21 -0
- package/dist/test/ErrorRelay.d.ts.map +1 -0
- package/dist/test/ErrorRelay.js +26 -0
- package/dist/test/ErrorRelay.js.map +1 -0
- package/dist/test/MockRelay.d.ts +20 -0
- package/dist/test/MockRelay.d.ts.map +1 -0
- package/dist/test/MockRelay.js +66 -0
- package/dist/test/MockRelay.js.map +1 -0
- package/dist/test/TestRelayServer.d.ts +25 -0
- package/dist/test/TestRelayServer.d.ts.map +1 -0
- package/dist/test/TestRelayServer.js +134 -0
- package/dist/test/TestRelayServer.js.map +1 -0
- package/dist/test/mod.d.ts +8 -0
- package/dist/test/mod.d.ts.map +1 -0
- package/dist/test/mod.js +28 -0
- package/dist/test/mod.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/uploaders/BlossomUploader.d.ts +26 -0
- package/dist/uploaders/BlossomUploader.d.ts.map +1 -0
- package/dist/uploaders/BlossomUploader.js +71 -0
- package/dist/uploaders/BlossomUploader.js.map +1 -0
- package/dist/uploaders/NostrBuildUploader.d.ts +24 -0
- package/dist/uploaders/NostrBuildUploader.d.ts.map +1 -0
- package/dist/uploaders/NostrBuildUploader.js +67 -0
- package/dist/uploaders/NostrBuildUploader.js.map +1 -0
- package/dist/uploaders/mod.d.ts +3 -0
- package/dist/uploaders/mod.d.ts.map +1 -0
- package/dist/uploaders/mod.js +8 -0
- package/dist/uploaders/mod.js.map +1 -0
- package/dist/utils/CircularSet.d.ts +13 -0
- package/dist/utils/CircularSet.d.ts.map +1 -0
- package/dist/utils/CircularSet.js +35 -0
- package/dist/utils/CircularSet.js.map +1 -0
- package/dist/utils/Machina.d.ts +36 -0
- package/dist/utils/Machina.d.ts.map +1 -0
- package/dist/utils/Machina.js +66 -0
- package/dist/utils/Machina.js.map +1 -0
- package/dist/utils/N64.d.ts +9 -0
- package/dist/utils/N64.d.ts.map +1 -0
- package/dist/utils/N64.js +23 -0
- package/dist/utils/N64.js.map +1 -0
- package/dist/utils/mod.d.ts +3 -0
- package/dist/utils/mod.d.ts.map +1 -0
- package/dist/utils/mod.js +8 -0
- package/dist/utils/mod.js.map +1 -0
- package/ln/LNURL.test.ts +87 -0
- package/ln/LNURL.ts +146 -0
- package/ln/mod.ts +4 -0
- package/ln/types/LNURLCallback.ts +7 -0
- package/ln/types/LNURLDetails.ts +19 -0
- package/mod.ts +16 -0
- package/package.json +23 -0
- package/test/ErrorRelay.test.ts +19 -0
- package/test/ErrorRelay.ts +40 -0
- package/test/MockRelay.test.ts +20 -0
- package/test/MockRelay.ts +92 -0
- package/test/TestRelayServer.ts +156 -0
- package/test/mod.ts +28 -0
- package/tsconfig.json +14 -0
- package/uploaders/BlossomUploader.test.ts +26 -0
- package/uploaders/BlossomUploader.ts +98 -0
- package/uploaders/NostrBuildUploader.test.ts +22 -0
- package/uploaders/NostrBuildUploader.ts +89 -0
- package/uploaders/mod.ts +2 -0
- package/utils/CircularSet.test.ts +15 -0
- package/utils/CircularSet.ts +34 -0
- package/utils/Machina.test.ts +91 -0
- package/utils/Machina.ts +66 -0
- package/utils/N64.test.ts +27 -0
- package/utils/N64.ts +23 -0
- package/utils/mod.ts +2 -0
package/NKinds.test.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
|
|
3
|
+
import { NKinds } from './NKinds.ts';
|
|
4
|
+
|
|
5
|
+
Deno.test('NKinds', () => {
|
|
6
|
+
assertEquals(NKinds.regular(1000), true);
|
|
7
|
+
assertEquals(NKinds.regular(10000), false);
|
|
8
|
+
assertEquals(NKinds.regular(0), false);
|
|
9
|
+
assertEquals(NKinds.regular(44), true);
|
|
10
|
+
assertEquals(NKinds.regular(45), false);
|
|
11
|
+
assertEquals(NKinds.regular(100000), false);
|
|
12
|
+
|
|
13
|
+
assertEquals(NKinds.replaceable(1000), false);
|
|
14
|
+
assertEquals(NKinds.replaceable(10000), true);
|
|
15
|
+
assertEquals(NKinds.replaceable(0), true);
|
|
16
|
+
assertEquals(NKinds.replaceable(3), true);
|
|
17
|
+
assertEquals(NKinds.replaceable(44), false);
|
|
18
|
+
assertEquals(NKinds.replaceable(45), false);
|
|
19
|
+
assertEquals(NKinds.replaceable(100000), false);
|
|
20
|
+
|
|
21
|
+
assertEquals(NKinds.ephemeral(1000), false);
|
|
22
|
+
assertEquals(NKinds.ephemeral(10000), false);
|
|
23
|
+
assertEquals(NKinds.ephemeral(0), false);
|
|
24
|
+
assertEquals(NKinds.ephemeral(3), false);
|
|
25
|
+
assertEquals(NKinds.ephemeral(44), false);
|
|
26
|
+
assertEquals(NKinds.ephemeral(45), false);
|
|
27
|
+
assertEquals(NKinds.ephemeral(20000), true);
|
|
28
|
+
assertEquals(NKinds.ephemeral(30000), false);
|
|
29
|
+
assertEquals(NKinds.ephemeral(40000), false);
|
|
30
|
+
assertEquals(NKinds.ephemeral(100000), false);
|
|
31
|
+
|
|
32
|
+
assertEquals(NKinds.addressable(1000), false);
|
|
33
|
+
assertEquals(NKinds.addressable(10000), false);
|
|
34
|
+
assertEquals(NKinds.addressable(0), false);
|
|
35
|
+
assertEquals(NKinds.addressable(3), false);
|
|
36
|
+
assertEquals(NKinds.addressable(44), false);
|
|
37
|
+
assertEquals(NKinds.addressable(45), false);
|
|
38
|
+
assertEquals(NKinds.addressable(20000), false);
|
|
39
|
+
assertEquals(NKinds.addressable(30000), true);
|
|
40
|
+
assertEquals(NKinds.addressable(40000), false);
|
|
41
|
+
assertEquals(NKinds.addressable(100000), false);
|
|
42
|
+
});
|
package/NKinds.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class NKinds {
|
|
2
|
+
/** Events are **regular**, which means they're all expected to be stored by relays. */
|
|
3
|
+
static regular(kind: number): boolean {
|
|
4
|
+
return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */
|
|
8
|
+
static replaceable(kind: number): boolean {
|
|
9
|
+
return (10000 <= kind && kind < 20000) || [0, 3].includes(kind);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Events are **ephemeral**, which means they are not expected to be stored by relays. */
|
|
13
|
+
static ephemeral(kind: number): boolean {
|
|
14
|
+
return 20000 <= kind && kind < 30000;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Events are **addressable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */
|
|
18
|
+
static addressable(kind: number): boolean {
|
|
19
|
+
return 30000 <= kind && kind < 40000;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @deprecated Use `NKinds.addressable()` instead. */
|
|
23
|
+
static parameterizedReplaceable(kind: number): boolean {
|
|
24
|
+
return NKinds.addressable(kind);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/NPool.test.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { NostrEvent } from '@nostrify/types';
|
|
2
|
+
import { assert, assertEquals } from '@std/assert';
|
|
3
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
4
|
+
|
|
5
|
+
import { NPool } from './NPool.ts';
|
|
6
|
+
import { NRelay1 } from './NRelay1.ts';
|
|
7
|
+
|
|
8
|
+
import events from '../../fixtures/events.json' with { type: 'json' };
|
|
9
|
+
import { TestRelayServer } from './test/TestRelayServer.ts';
|
|
10
|
+
|
|
11
|
+
const event1s = events
|
|
12
|
+
.filter((e) => e.kind === 1)
|
|
13
|
+
.toSorted((_) => 0.5 - Math.random())
|
|
14
|
+
.slice(0, 10);
|
|
15
|
+
|
|
16
|
+
Deno.test('NPool.query', async () => {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const tid = setTimeout(() => controller.abort(), 5000);
|
|
19
|
+
|
|
20
|
+
await using server1 = new TestRelayServer();
|
|
21
|
+
await using server2 = new TestRelayServer();
|
|
22
|
+
|
|
23
|
+
for (const event of event1s) {
|
|
24
|
+
await server1.event(event);
|
|
25
|
+
await server2.event(event);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await using pool = new NPool({
|
|
29
|
+
open: (url) => new NRelay1(url),
|
|
30
|
+
reqRouter: (filters) =>
|
|
31
|
+
new Map([
|
|
32
|
+
[server1.url, filters],
|
|
33
|
+
[server2.url, filters],
|
|
34
|
+
]),
|
|
35
|
+
eventRouter: () => [server1.url],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const events = await pool.query([{ kinds: [1], limit: 15 }]);
|
|
39
|
+
|
|
40
|
+
assertEquals(events.length, 10);
|
|
41
|
+
assert(events[0].created_at >= events[1].created_at);
|
|
42
|
+
|
|
43
|
+
clearTimeout(tid);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Deno.test('NPool.req', async () => {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const tid = setTimeout(() => controller.abort(), 3000);
|
|
49
|
+
|
|
50
|
+
await using server1 = new TestRelayServer();
|
|
51
|
+
await using server2 = new TestRelayServer();
|
|
52
|
+
|
|
53
|
+
for (const event of event1s) {
|
|
54
|
+
await server1.event(event);
|
|
55
|
+
await server2.event(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const events: NostrEvent[] = [];
|
|
59
|
+
|
|
60
|
+
await using pool = new NPool({
|
|
61
|
+
open: (url) => new NRelay1(url),
|
|
62
|
+
reqRouter: (filters) =>
|
|
63
|
+
new Map([
|
|
64
|
+
[server1.url, filters],
|
|
65
|
+
[server2.url, filters],
|
|
66
|
+
]),
|
|
67
|
+
eventRouter: () => [server1.url],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
for await (const msg of pool.req([{ kinds: [1], limit: 3 }], { signal: controller.signal })) {
|
|
71
|
+
if (msg[0] === 'EVENT') {
|
|
72
|
+
events.push(msg[2]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (events.length === 3) break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
assertEquals(events.length, 3);
|
|
79
|
+
|
|
80
|
+
clearTimeout(tid);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
Deno.test('NPool.event', async () => {
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const tid = setTimeout(() => controller.abort(), 5000);
|
|
86
|
+
|
|
87
|
+
await using server1 = new TestRelayServer();
|
|
88
|
+
await using server2 = new TestRelayServer();
|
|
89
|
+
|
|
90
|
+
for (const event of event1s) {
|
|
91
|
+
await server1.event(event);
|
|
92
|
+
await server2.event(event);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const event: NostrEvent = finalizeEvent({
|
|
96
|
+
kind: 1,
|
|
97
|
+
content: 'This is an automated test from Nostrify: https://gitlab.com/soapbox-pub/nostrify',
|
|
98
|
+
tags: [['unique', 'uniqueTag']],
|
|
99
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
100
|
+
}, generateSecretKey());
|
|
101
|
+
|
|
102
|
+
await using pool = new NPool({
|
|
103
|
+
open: (url) => new NRelay1(url),
|
|
104
|
+
reqRouter: (filters) =>
|
|
105
|
+
new Map([
|
|
106
|
+
[server1.url, filters],
|
|
107
|
+
[server2.url, filters],
|
|
108
|
+
]),
|
|
109
|
+
eventRouter: () => [server1.url],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await pool.event(event, { signal: controller.signal });
|
|
113
|
+
|
|
114
|
+
assertEquals((await pool.query([{ kinds: [1], '#unique': ['uniqueTag'] }], { signal: controller.signal })).length, 1);
|
|
115
|
+
|
|
116
|
+
clearTimeout(tid);
|
|
117
|
+
});
|
package/NPool.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { NostrEvent, NostrFilter, NostrRelayCLOSED, NostrRelayEOSE, NostrRelayEVENT, NRelay } from '@nostrify/types';
|
|
2
|
+
import { getFilterLimit } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { CircularSet } from './utils/CircularSet';
|
|
5
|
+
import { Machina } from './utils/Machina';
|
|
6
|
+
import { NSet } from './NSet';
|
|
7
|
+
|
|
8
|
+
export interface NPoolOpts<T extends NRelay> {
|
|
9
|
+
/** Creates an `NRelay` instance for the given URL. */
|
|
10
|
+
open(url: string): T;
|
|
11
|
+
/** Determines the relays to use for making `REQ`s to the given filters. To support the Outbox model, it should analyze the `authors` field of the filters. */
|
|
12
|
+
reqRouter(
|
|
13
|
+
filters: NostrFilter[],
|
|
14
|
+
):
|
|
15
|
+
| ReadonlyMap<string, NostrFilter[]>
|
|
16
|
+
| Promise<ReadonlyMap<string, NostrFilter[]>>;
|
|
17
|
+
/** Determines the relays to use for publishing the given event. To support the Outbox model, it should analyze the `pubkey` field of the event. */
|
|
18
|
+
eventRouter(event: NostrEvent): string[] | Promise<string[]>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `NPool` class is a `NRelay` implementation for connecting to multiple relays.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const pool = new NPool({
|
|
26
|
+
* open: (url) => new NRelay1(url),
|
|
27
|
+
* reqRouter: async (filters) => new Map([
|
|
28
|
+
* ['wss://relay1.mostr.pub', filters],
|
|
29
|
+
* ['wss://relay2.mostr.pub', filters],
|
|
30
|
+
* ]),
|
|
31
|
+
* eventRouter: async (event) => ['wss://relay1.mostr.pub', 'wss://relay2.mostr.pub'],
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Now you can use the pool like a regular relay.
|
|
35
|
+
* for await (const msg of pool.req([{ kinds: [1] }])) {
|
|
36
|
+
* if (msg[0] === 'EVENT') console.log(msg[2]);
|
|
37
|
+
* if (msg[0] === 'EOSE') break;
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* This class is designed with the Outbox model in mind.
|
|
42
|
+
* Instead of passing relay URLs into each method, you pass functions into the contructor that statically-analyze filters and events to determine which relays to use for requesting and publishing events.
|
|
43
|
+
* If a relay wasn't already connected, it will be opened automatically.
|
|
44
|
+
* Defining `open` will also let you use any relay implementation, such as `NRelay1`.
|
|
45
|
+
*
|
|
46
|
+
* Note that `pool.req` may stream duplicate events, while `pool.query` will correctly process replaceable events and deletions within the event set before returning them.
|
|
47
|
+
*
|
|
48
|
+
* `pool.req` will only emit an `EOSE` when all relays in its set have emitted an `EOSE`, and likewise for `CLOSED`.
|
|
49
|
+
*/
|
|
50
|
+
export class NPool<T extends NRelay = NRelay> implements NRelay {
|
|
51
|
+
private _relays = new Map<string, T>();
|
|
52
|
+
|
|
53
|
+
constructor(private opts: NPoolOpts<T>) {}
|
|
54
|
+
|
|
55
|
+
/** Get or create a relay instance for the given URL. */
|
|
56
|
+
public relay(url: string): T {
|
|
57
|
+
const relay = this._relays.get(url);
|
|
58
|
+
|
|
59
|
+
if (relay) {
|
|
60
|
+
return relay;
|
|
61
|
+
} else {
|
|
62
|
+
const relay = this.opts.open(url);
|
|
63
|
+
this._relays.set(url, relay);
|
|
64
|
+
return relay;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Returns a new pool instance that uses the given relays. Connections are shared with the original pool. */
|
|
69
|
+
public group(urls: string[]): NPool<T> {
|
|
70
|
+
return new NPool({
|
|
71
|
+
open: (url) => this.relay(url),
|
|
72
|
+
reqRouter: (filters) => new Map(urls.map((url) => [url, filters])),
|
|
73
|
+
eventRouter: () => urls,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public get relays(): ReadonlyMap<string, T> {
|
|
78
|
+
return this._relays;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sends a `REQ` to relays based on the configured `reqRouter`.
|
|
83
|
+
*
|
|
84
|
+
* `EVENT` messages from the selected relays are yielded.
|
|
85
|
+
* `EOSE` and `CLOSE` messages are only yielded when all relays have emitted them.
|
|
86
|
+
*
|
|
87
|
+
* Deduplication of `EVENT` messages is attempted, so that each event is only yielded once.
|
|
88
|
+
* A circular set of 1000 is used to track seen event IDs, so it's possible that very
|
|
89
|
+
* long-running subscriptions (with over 1000 results) may yield duplicate events.
|
|
90
|
+
*/
|
|
91
|
+
async *req(
|
|
92
|
+
filters: NostrFilter[],
|
|
93
|
+
opts?: { signal?: AbortSignal; relays?: string[] },
|
|
94
|
+
): AsyncIterable<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED> {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const signal = opts?.signal ? AbortSignal.any([opts.signal, controller.signal]) : controller.signal;
|
|
97
|
+
|
|
98
|
+
const routes = opts?.relays
|
|
99
|
+
? new Map(opts.relays.map((url) => [url, filters]))
|
|
100
|
+
: await this.opts.reqRouter(filters);
|
|
101
|
+
|
|
102
|
+
if (routes.size < 1) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const machina = new Machina<
|
|
107
|
+
NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED
|
|
108
|
+
>(signal);
|
|
109
|
+
|
|
110
|
+
const eoses = new Set<string>();
|
|
111
|
+
const closes = new Set<string>();
|
|
112
|
+
const events = new CircularSet<string>(1000);
|
|
113
|
+
|
|
114
|
+
for (const [url, filters] of routes.entries()) {
|
|
115
|
+
const relay = this.relay(url);
|
|
116
|
+
(async () => {
|
|
117
|
+
for await (const msg of relay.req(filters, { signal })) {
|
|
118
|
+
if (msg[0] === 'EOSE') {
|
|
119
|
+
eoses.add(url);
|
|
120
|
+
if (eoses.size === routes.size) {
|
|
121
|
+
machina.push(msg);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (msg[0] === 'CLOSED') {
|
|
125
|
+
closes.add(url);
|
|
126
|
+
if (closes.size === routes.size) {
|
|
127
|
+
machina.push(msg);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (msg[0] === 'EVENT') {
|
|
131
|
+
const [, , event] = msg;
|
|
132
|
+
if (!events.has(event.id)) {
|
|
133
|
+
events.add(event.id);
|
|
134
|
+
machina.push(msg);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})().catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
for await (const msg of machina) {
|
|
143
|
+
yield msg;
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
controller.abort();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Events are sent to relays according to the `eventRouter`.
|
|
152
|
+
* Returns a fulfilled promise if ANY relay accepted the event,
|
|
153
|
+
* or a rejected promise if ALL relays rejected or failed to publish the event.
|
|
154
|
+
*/
|
|
155
|
+
async event(
|
|
156
|
+
event: NostrEvent,
|
|
157
|
+
opts?: { signal?: AbortSignal; relays?: string[] },
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const relayUrls = opts?.relays ?? await this.opts.eventRouter(event);
|
|
160
|
+
|
|
161
|
+
if (!relayUrls.length) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// @ts-ignore Promise.any exists for sure
|
|
166
|
+
await Promise.any(
|
|
167
|
+
relayUrls.map((url) => this.relay(url).event(event, opts)),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* This method calls `.req` internally and then post-processes the results.
|
|
173
|
+
* Please read the definition of `.req`.
|
|
174
|
+
*
|
|
175
|
+
* - The strategy is to seek regular events quickly, and to wait to find the latest versions of replaceable events.
|
|
176
|
+
* - Filters for replaceable events will wait for all relays to `EOSE` (or `CLOSE`, or for the signal to be aborted) to ensure the latest event versions are retrieved.
|
|
177
|
+
* - Filters for regular events will stop as soon as the filters are fulfilled.
|
|
178
|
+
* - Events are deduplicated, sorted, and only the latest version of replaceable events is kept.
|
|
179
|
+
* - If the signal is aborted, this method will return partial results instead of throwing.
|
|
180
|
+
*
|
|
181
|
+
* To implement a custom strategy, call `.req` directly.
|
|
182
|
+
*/
|
|
183
|
+
async query(
|
|
184
|
+
filters: NostrFilter[],
|
|
185
|
+
opts?: { signal?: AbortSignal; relays?: string[] },
|
|
186
|
+
): Promise<NostrEvent[]> {
|
|
187
|
+
const map = new Map<string, NostrEvent>();
|
|
188
|
+
const events = new NSet(map);
|
|
189
|
+
|
|
190
|
+
const limit = filters.reduce(
|
|
191
|
+
(result, filter) => result + getFilterLimit(filter),
|
|
192
|
+
0,
|
|
193
|
+
);
|
|
194
|
+
if (limit === 0) return [];
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
for await (const msg of this.req(filters, opts)) {
|
|
198
|
+
if (msg[0] === 'EOSE') break;
|
|
199
|
+
if (msg[0] === 'EVENT') events.add(msg[2]);
|
|
200
|
+
if (msg[0] === 'CLOSED') break;
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Skip errors, return partial results.
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Don't sort results of search filters.
|
|
207
|
+
if (filters.some((filter) => typeof filter.search === 'string')) {
|
|
208
|
+
return [...map.values()];
|
|
209
|
+
} else {
|
|
210
|
+
return [...events];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Close all the relays in the pool. */
|
|
215
|
+
async close(): Promise<void> {
|
|
216
|
+
await Promise.all(
|
|
217
|
+
[...this._relays.values()].map((relay) => relay.close()),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
222
|
+
await this.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
package/NRelay1.test.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { NostrEvent } from '@nostrify/types';
|
|
2
|
+
import { assert, assertEquals, assertRejects } from '@std/assert';
|
|
3
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
4
|
+
import { WebsocketEvent } from 'websocket-ts';
|
|
5
|
+
|
|
6
|
+
import { genEvent } from './test/mod.ts';
|
|
7
|
+
import { TestRelayServer } from './test/TestRelayServer.ts';
|
|
8
|
+
import { NRelay1 } from './NRelay1.ts';
|
|
9
|
+
|
|
10
|
+
import events from '../../fixtures/events.json' with { type: 'json' };
|
|
11
|
+
|
|
12
|
+
const event1s = events
|
|
13
|
+
.filter((e) => e.kind === 1)
|
|
14
|
+
.toSorted((_) => 0.5 - Math.random())
|
|
15
|
+
.slice(0, 10);
|
|
16
|
+
|
|
17
|
+
Deno.test('NRelay1.query', async () => {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const tid = setTimeout(() => controller.abort(), 3000);
|
|
20
|
+
|
|
21
|
+
await using server = new TestRelayServer();
|
|
22
|
+
|
|
23
|
+
for (const event of event1s) {
|
|
24
|
+
await server.event(event);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await using relay = new NRelay1(server.url);
|
|
28
|
+
const events = await relay.query([{ kinds: [1], limit: 3 }], { signal: controller.signal });
|
|
29
|
+
|
|
30
|
+
assertEquals(events.length, 3);
|
|
31
|
+
assert(events[0].created_at >= events[1].created_at);
|
|
32
|
+
|
|
33
|
+
clearTimeout(tid);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
Deno.test('NRelay1.query mismatched filter', async () => {
|
|
37
|
+
await using server = new TestRelayServer({
|
|
38
|
+
handleMessage(socket, msg) {
|
|
39
|
+
if (msg[0] === 'REQ') {
|
|
40
|
+
const [, subId, ..._filters] = msg;
|
|
41
|
+
socket.send(JSON.stringify(['EVENT', subId, genEvent({ kind: 9001 })]));
|
|
42
|
+
socket.send(JSON.stringify(['EOSE', subId]));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await using relay = new NRelay1(server.url);
|
|
48
|
+
const events = await relay.query([{ kinds: [1] }]);
|
|
49
|
+
|
|
50
|
+
assertEquals(events, []);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
Deno.test('NRelay1.req', async () => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const tid = setTimeout(() => controller.abort(), 3000);
|
|
56
|
+
|
|
57
|
+
await using server = new TestRelayServer();
|
|
58
|
+
|
|
59
|
+
for (const event of event1s) {
|
|
60
|
+
await server.event(event);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await using relay = new NRelay1(server.url);
|
|
64
|
+
const events: NostrEvent[] = [];
|
|
65
|
+
|
|
66
|
+
for await (const msg of relay.req([{ kinds: [1], limit: 3 }], { signal: controller.signal })) {
|
|
67
|
+
if (msg[0] === 'EVENT') {
|
|
68
|
+
events.push(msg[2]);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
assertEquals(events.length, 1);
|
|
74
|
+
|
|
75
|
+
clearTimeout(tid);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
Deno.test('NRelay1.event', async () => {
|
|
79
|
+
await using server = new TestRelayServer();
|
|
80
|
+
await using relay = new NRelay1(server.url);
|
|
81
|
+
|
|
82
|
+
const event: NostrEvent = finalizeEvent({
|
|
83
|
+
kind: 1,
|
|
84
|
+
content: 'This is an automated test from Nostrify: https://gitlab.com/soapbox-pub/nostrify',
|
|
85
|
+
tags: [],
|
|
86
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
87
|
+
}, generateSecretKey());
|
|
88
|
+
|
|
89
|
+
await relay.event(event);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
Deno.test('NRelay1 backoff', async (t) => {
|
|
93
|
+
await using server = new TestRelayServer();
|
|
94
|
+
await using relay = new NRelay1(server.url);
|
|
95
|
+
|
|
96
|
+
await t.step('websocket opens', async () => {
|
|
97
|
+
await new Promise((resolve) => relay.socket.addEventListener(WebsocketEvent.open, resolve, { once: true }));
|
|
98
|
+
assertEquals(relay.socket.readyState, WebSocket.OPEN);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Start a subscription so the relay will reconnect
|
|
102
|
+
(async () => {
|
|
103
|
+
try {
|
|
104
|
+
for await (const _msg of relay.req([{ kinds: [0] }])) {
|
|
105
|
+
// Do nothing
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
//
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
|
|
112
|
+
await t.step('websocket closes when server closes', async () => {
|
|
113
|
+
const waitForClose = new Promise((resolve) =>
|
|
114
|
+
relay.socket.addEventListener(WebsocketEvent.close, resolve, { once: true })
|
|
115
|
+
);
|
|
116
|
+
await server.close();
|
|
117
|
+
await waitForClose;
|
|
118
|
+
assertEquals(relay.socket.readyState, WebSocket.CLOSED);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await t.step('websocket reopens when server reopens', async () => {
|
|
122
|
+
server.open();
|
|
123
|
+
await new Promise((resolve) => relay.socket.addEventListener(WebsocketEvent.open, resolve, { once: true }));
|
|
124
|
+
assertEquals(relay.socket.readyState, WebSocket.OPEN);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
Deno.test('NRelay1 idleTimeout', async (t) => {
|
|
129
|
+
await using server = new TestRelayServer();
|
|
130
|
+
await using relay = new NRelay1(server.url, { idleTimeout: 100 });
|
|
131
|
+
|
|
132
|
+
await t.step('websocket opens', async () => {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
134
|
+
assertEquals(relay.socket.readyState, WebSocket.OPEN);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await t.step('websocket closes after idleTimeout', async () => {
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
139
|
+
assertEquals(relay.socket.readyState, WebSocket.CLOSED);
|
|
140
|
+
assertEquals(relay.socket.closedByUser, true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await t.step('websocket wakes up during activity', async () => {
|
|
144
|
+
await relay.event(events[0]);
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
|
+
assertEquals(relay.socket.readyState, WebSocket.OPEN);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
Deno.test('NRelay1.count rejects when the server sends CLOSED', async () => {
|
|
151
|
+
await using server = new TestRelayServer({
|
|
152
|
+
handleMessage(socket, msg) {
|
|
153
|
+
if (msg[0] === 'COUNT') {
|
|
154
|
+
server.send(socket, ['CLOSED', msg[1], 'unsupported: COUNT is not supported']);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await using relay = new NRelay1(server.url);
|
|
160
|
+
|
|
161
|
+
await assertRejects(() => relay.count([{ kinds: [1] }]));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
Deno.test('NRelay1 closes when it receives a binary message', async () => {
|
|
165
|
+
await using server = new TestRelayServer({
|
|
166
|
+
handleMessage(socket) {
|
|
167
|
+
socket.send(new Uint8Array([0x00, 0x01, 0x02, 0x03]));
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await using relay = new NRelay1(server.url);
|
|
172
|
+
|
|
173
|
+
await assertRejects(() => relay.query([{ kinds: [1] }]));
|
|
174
|
+
});
|