onelobby 0.5.0 → 0.6.0
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 +2 -2
- package/blueprint.schema.json +8 -1
- package/dist/blueprint.d.ts +4 -0
- package/dist/blueprint.js +5 -0
- package/dist/blueprint.js.map +1 -1
- package/dist/codec.d.ts +23 -0
- package/dist/codec.js +28 -0
- package/dist/codec.js.map +1 -1
- package/dist/direct/browser-host.d.ts +46 -0
- package/dist/direct/browser-host.js +93 -0
- package/dist/direct/browser-host.js.map +1 -0
- package/dist/direct/hub-host.d.ts +34 -23
- package/dist/direct/hub-host.js +75 -59
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub-local.d.ts +22 -0
- package/dist/direct/hub-local.js +46 -0
- package/dist/direct/hub-local.js.map +1 -0
- package/dist/direct/hub.d.ts +37 -10
- package/dist/direct/hub.js +114 -23
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +36 -3
- package/dist/direct/index.js +146 -20
- package/dist/direct/index.js.map +1 -1
- package/dist/direct/metrics.d.ts +12 -0
- package/dist/direct/metrics.js +45 -0
- package/dist/direct/metrics.js.map +1 -0
- package/dist/direct/portmap.d.ts +9 -2
- package/dist/direct/portmap.js +7 -2
- package/dist/direct/portmap.js.map +1 -1
- package/dist/direct/sha1.d.ts +16 -0
- package/dist/direct/sha1.js +111 -0
- package/dist/direct/sha1.js.map +1 -0
- package/dist/direct-address.d.ts +2 -2
- package/dist/direct-address.js.map +1 -1
- package/dist/friendly.d.ts +23 -0
- package/dist/friendly.js +28 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/internals.d.ts +9 -0
- package/dist/internals.js +9 -0
- package/dist/internals.js.map +1 -1
- package/dist/join-code-kind.d.ts +10 -0
- package/dist/join-code-kind.js +10 -0
- package/dist/join-code-kind.js.map +1 -0
- package/dist/joincode.d.ts +10 -1
- package/dist/joincode.js +23 -3
- package/dist/joincode.js.map +1 -1
- package/dist/lobby.d.ts +65 -4
- package/dist/lobby.js +151 -12
- package/dist/lobby.js.map +1 -1
- package/dist/lockstep.d.ts +8 -0
- package/dist/lockstep.js +23 -2
- package/dist/lockstep.js.map +1 -1
- package/dist/nostr/crypto.d.ts +20 -0
- package/dist/nostr/crypto.js +78 -0
- package/dist/nostr/crypto.js.map +1 -0
- package/dist/nostr/host.d.ts +66 -0
- package/dist/nostr/host.js +196 -0
- package/dist/nostr/host.js.map +1 -0
- package/dist/nostr/limits.d.ts +31 -0
- package/dist/nostr/limits.js +36 -0
- package/dist/nostr/limits.js.map +1 -0
- package/dist/nostr/link.d.ts +70 -0
- package/dist/nostr/link.js +180 -0
- package/dist/nostr/link.js.map +1 -0
- package/dist/nostr/relays.d.ts +79 -0
- package/dist/nostr/relays.js +315 -0
- package/dist/nostr/relays.js.map +1 -0
- package/dist/nostr/room.d.ts +43 -0
- package/dist/nostr/room.js +104 -0
- package/dist/nostr/room.js.map +1 -0
- package/dist/nostr/secp256k1.d.ts +10 -0
- package/dist/nostr/secp256k1.js +171 -0
- package/dist/nostr/secp256k1.js.map +1 -0
- package/dist/nostr/socket.d.ts +48 -0
- package/dist/nostr/socket.js +141 -0
- package/dist/nostr/socket.js.map +1 -0
- package/dist/onelobby.bundle.js +146 -0
- package/dist/onelobby.bundle.js.map +7 -0
- package/dist/onelobby.bundle.mjs +11 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +31 -3
- package/dist/onelobby.global.js +2599 -46
- package/dist/onelobby.global.js.map +4 -4
- package/dist/onelobby.global.min.js +11 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +68 -14
- package/dist/onelobby.js.map +1 -1
- package/dist/pause.d.ts +85 -0
- package/dist/pause.js +160 -0
- package/dist/pause.js.map +1 -0
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js.map +1 -1
- package/dist/webrtc.d.ts +1 -0
- package/dist/webrtc.js +6 -1
- package/dist/webrtc.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every Nostr limit and timer shared with the C# client. Pinned by `spec/vectors/nostr-limits.json` (test/nostr.test.ts);
|
|
3
|
+
* C# mirror: OneLobby.Direct/Nostr/NostrLimits.cs. Change a number in all three.
|
|
4
|
+
*/
|
|
5
|
+
// Reliable link (spec s.4)
|
|
6
|
+
export const BATCH_MS = 30;
|
|
7
|
+
export const RESEND_MS = 3000;
|
|
8
|
+
export const MAX_RESENDS = 5;
|
|
9
|
+
export const BEAT_MS = 10_000;
|
|
10
|
+
export const SILENCE_MS = 30_000;
|
|
11
|
+
export const MAX_HELD = 64;
|
|
12
|
+
/** Outbound: queued texts plus unacknowledged frames, at most this many frames and bytes; excess texts are dropped. */
|
|
13
|
+
export const MAX_OUT_FRAMES = 128;
|
|
14
|
+
export const MAX_OUT_BYTES = 256 * 1024;
|
|
15
|
+
export const MAX_FRAME_BYTES = 30_000; // under the 32 KiB content limit, room for the JSON envelope
|
|
16
|
+
export const TICK_MS = 250;
|
|
17
|
+
// Host bridge: new joiner token bucket (spec s.4)
|
|
18
|
+
export const NEW_SESSION_BURST = 16;
|
|
19
|
+
export const MAX_SESSION_BURST = 64;
|
|
20
|
+
export const NEW_SESSION_REFILL_MS = 1_000;
|
|
21
|
+
export const WAITING_MAX = 64;
|
|
22
|
+
export const WAITING_STALE_MS = 5_000;
|
|
23
|
+
// Joiner socket (spec s.4)
|
|
24
|
+
export const OPEN_EVERY_MS = 2000;
|
|
25
|
+
export const GIVE_UP_MS = 15_000;
|
|
26
|
+
// Relay waits (spec s.6)
|
|
27
|
+
/** `hostDirect`: how long to wait for a first relay before hosting by IP alone (the IP code shows first, the Nostr code takes over later). */
|
|
28
|
+
export const HOST_FIRST_RELAY_MS = 3_000;
|
|
29
|
+
/** `hostNostr` (no IP fallback): how long to wait for a first relay before `direct_unreachable`. */
|
|
30
|
+
export const HOST_NOSTR_FIRST_RELAY_MS = 10_000;
|
|
31
|
+
/** A joiner: how long to wait for a first relay before `connect_failed`. */
|
|
32
|
+
export const JOINER_FIRST_RELAY_MS = 10_000;
|
|
33
|
+
/** After the first relay: at most this long for the rest of the list; ends at once when every relay has answered. */
|
|
34
|
+
export const RELAY_REST_WAIT_MS = 1500;
|
|
35
|
+
export const RELAY_REST_POLL_MS = 50;
|
|
36
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/nostr/limits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2BAA2B;AAC3B,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC;AAC3B,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC;AAC9B,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC;AAC9B,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC;AACjC,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC;AAC3B,uHAAuH;AACvH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC;AACxC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,CAAC,6DAA6D;AACpG,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC;AAE3B,kDAAkD;AAClD,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAC3C,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC9B,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC,2BAA2B;AAC3B,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;AAClC,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC;AAEjC,yBAAyB;AACzB,8IAA8I;AAC9I,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AACzC,oGAAoG;AACpG,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAChD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAC5C,qHAAqH;AACrH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC","sourcesContent":["/**\n * Every Nostr limit and timer shared with the C# client. Pinned by `spec/vectors/nostr-limits.json` (test/nostr.test.ts);\n * C# mirror: OneLobby.Direct/Nostr/NostrLimits.cs. Change a number in all three.\n */\n\n// Reliable link (spec s.4)\nexport const BATCH_MS = 30;\nexport const RESEND_MS = 3000;\nexport const MAX_RESENDS = 5;\nexport const BEAT_MS = 10_000;\nexport const SILENCE_MS = 30_000;\nexport const MAX_HELD = 64;\n/** Outbound: queued texts plus unacknowledged frames, at most this many frames and bytes; excess texts are dropped. */\nexport const MAX_OUT_FRAMES = 128;\nexport const MAX_OUT_BYTES = 256 * 1024;\nexport const MAX_FRAME_BYTES = 30_000; // under the 32 KiB content limit, room for the JSON envelope\nexport const TICK_MS = 250;\n\n// Host bridge: new joiner token bucket (spec s.4)\nexport const NEW_SESSION_BURST = 16;\nexport const MAX_SESSION_BURST = 64;\nexport const NEW_SESSION_REFILL_MS = 1_000;\nexport const WAITING_MAX = 64;\nexport const WAITING_STALE_MS = 5_000;\n\n// Joiner socket (spec s.4)\nexport const OPEN_EVERY_MS = 2000;\nexport const GIVE_UP_MS = 15_000;\n\n// Relay waits (spec s.6)\n/** `hostDirect`: how long to wait for a first relay before hosting by IP alone (the IP code shows first, the Nostr code takes over later). */\nexport const HOST_FIRST_RELAY_MS = 3_000;\n/** `hostNostr` (no IP fallback): how long to wait for a first relay before `direct_unreachable`. */\nexport const HOST_NOSTR_FIRST_RELAY_MS = 10_000;\n/** A joiner: how long to wait for a first relay before `connect_failed`. */\nexport const JOINER_FIRST_RELAY_MS = 10_000;\n/** After the first relay: at most this long for the rest of the list; ends at once when every relay has answered. */\nexport const RELAY_REST_WAIT_MS = 1500;\nexport const RELAY_REST_POLL_MS = 50;\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame layer of Nostr signaling (spec/nostr-signaling.md s.4): ordered, de-duplicated, acknowledged delivery of
|
|
3
|
+
* hub protocol texts between two pubkeys over lossy relays. One {@link ReliableLink} per peer; it knows nothing about
|
|
4
|
+
* relays, keys or encryption: the owner stamps and ships the frames it emits.
|
|
5
|
+
*/
|
|
6
|
+
import type { Clock } from "../clock.js";
|
|
7
|
+
import { BATCH_MS, RESEND_MS, MAX_RESENDS, BEAT_MS, SILENCE_MS, MAX_HELD, MAX_OUT_FRAMES, MAX_OUT_BYTES, MAX_FRAME_BYTES } from "./limits.js";
|
|
8
|
+
export type FrameType = "open" | "data" | "beat" | "close";
|
|
9
|
+
/** The wire frame (JSON in the encrypted event content). */
|
|
10
|
+
export interface Frame {
|
|
11
|
+
v: 1;
|
|
12
|
+
type: FrameType;
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
seq: number;
|
|
16
|
+
ack: number;
|
|
17
|
+
msgs?: string[];
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/** What a link emits; the owner adds v / from / to. */
|
|
21
|
+
export type OutFrame = Pick<Frame, "type" | "seq" | "ack"> & {
|
|
22
|
+
msgs?: string[];
|
|
23
|
+
reason?: string;
|
|
24
|
+
};
|
|
25
|
+
export { BATCH_MS, RESEND_MS, MAX_RESENDS, BEAT_MS, SILENCE_MS, MAX_HELD, MAX_OUT_FRAMES, MAX_OUT_BYTES, MAX_FRAME_BYTES };
|
|
26
|
+
export interface LinkOptions {
|
|
27
|
+
clock: Clock;
|
|
28
|
+
/** Ships a frame to the peer. */
|
|
29
|
+
send: (f: OutFrame) => void;
|
|
30
|
+
/** An in-order hub protocol text from the peer. */
|
|
31
|
+
deliver: (text: string) => void;
|
|
32
|
+
/** The peer sent `close`. */
|
|
33
|
+
onPeerClose: (reason: string) => void;
|
|
34
|
+
/** Gave up: five resends unanswered, or 30 s of silence (reason "timeout"). */
|
|
35
|
+
onDead: (reason: string) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare class ReliableLink {
|
|
38
|
+
private readonly o;
|
|
39
|
+
private queue;
|
|
40
|
+
private queueBytes;
|
|
41
|
+
private unackedBytes;
|
|
42
|
+
private nextSeq;
|
|
43
|
+
private readonly unacked;
|
|
44
|
+
private rcvNext;
|
|
45
|
+
private readonly held;
|
|
46
|
+
private ackOwed;
|
|
47
|
+
private lastSent;
|
|
48
|
+
private lastHeard;
|
|
49
|
+
private cancelFlush;
|
|
50
|
+
private cancelTick;
|
|
51
|
+
private done;
|
|
52
|
+
constructor(o: LinkOptions);
|
|
53
|
+
get closed(): boolean;
|
|
54
|
+
/** Queues a hub text; sent within 30 ms, packed with its neighbours. */
|
|
55
|
+
send(text: string): void;
|
|
56
|
+
/** Any frame arrived from the peer (including `open`, which only proves it is alive). */
|
|
57
|
+
heard(): void;
|
|
58
|
+
/** `data`, `beat` and `close` frames from the peer. */
|
|
59
|
+
receive(f: Frame): void;
|
|
60
|
+
/** Sends what is queued, then `close` (once), and stops. */
|
|
61
|
+
close(reason?: string): void;
|
|
62
|
+
/** Stops the timers without telling the peer. */
|
|
63
|
+
stop(): void;
|
|
64
|
+
private scheduleFlush;
|
|
65
|
+
private flush;
|
|
66
|
+
private dropUnacked;
|
|
67
|
+
private emit;
|
|
68
|
+
private tick;
|
|
69
|
+
private die;
|
|
70
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { BATCH_MS, RESEND_MS, MAX_RESENDS, BEAT_MS, SILENCE_MS, MAX_HELD, MAX_OUT_FRAMES, MAX_OUT_BYTES, MAX_FRAME_BYTES, TICK_MS } from "./limits.js";
|
|
2
|
+
export { BATCH_MS, RESEND_MS, MAX_RESENDS, BEAT_MS, SILENCE_MS, MAX_HELD, MAX_OUT_FRAMES, MAX_OUT_BYTES, MAX_FRAME_BYTES };
|
|
3
|
+
const utf8 = new TextEncoder();
|
|
4
|
+
const byteLen = (s) => utf8.encode(s).length;
|
|
5
|
+
export class ReliableLink {
|
|
6
|
+
o;
|
|
7
|
+
queue = [];
|
|
8
|
+
queueBytes = 0;
|
|
9
|
+
unackedBytes = 0;
|
|
10
|
+
nextSeq = 1;
|
|
11
|
+
unacked = new Map();
|
|
12
|
+
rcvNext = 1; // next seq to deliver
|
|
13
|
+
held = new Map();
|
|
14
|
+
ackOwed = false;
|
|
15
|
+
lastSent;
|
|
16
|
+
lastHeard;
|
|
17
|
+
cancelFlush = null;
|
|
18
|
+
cancelTick = null;
|
|
19
|
+
done = false;
|
|
20
|
+
constructor(o) {
|
|
21
|
+
this.o = o;
|
|
22
|
+
this.lastSent = this.lastHeard = o.clock.now();
|
|
23
|
+
this.cancelTick = o.clock.setInterval(() => this.tick(), TICK_MS);
|
|
24
|
+
}
|
|
25
|
+
get closed() {
|
|
26
|
+
return this.done;
|
|
27
|
+
}
|
|
28
|
+
/** Queues a hub text; sent within 30 ms, packed with its neighbours. */
|
|
29
|
+
send(text) {
|
|
30
|
+
if (this.done)
|
|
31
|
+
return;
|
|
32
|
+
// relays refuse high publish rates: past the cap the text is dropped, the connection is not
|
|
33
|
+
const n = byteLen(text);
|
|
34
|
+
if (this.queue.length + this.unacked.size >= MAX_OUT_FRAMES || this.queueBytes + this.unackedBytes + n > MAX_OUT_BYTES)
|
|
35
|
+
return;
|
|
36
|
+
this.queue.push(text);
|
|
37
|
+
this.queueBytes += n;
|
|
38
|
+
this.scheduleFlush();
|
|
39
|
+
}
|
|
40
|
+
/** Any frame arrived from the peer (including `open`, which only proves it is alive). */
|
|
41
|
+
heard() {
|
|
42
|
+
this.lastHeard = this.o.clock.now();
|
|
43
|
+
}
|
|
44
|
+
/** `data`, `beat` and `close` frames from the peer. */
|
|
45
|
+
receive(f) {
|
|
46
|
+
if (this.done)
|
|
47
|
+
return;
|
|
48
|
+
this.heard();
|
|
49
|
+
if (f.type === "close") {
|
|
50
|
+
const reason = typeof f.reason === "string" ? f.reason : "";
|
|
51
|
+
this.stop();
|
|
52
|
+
this.o.onPeerClose(reason);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (f.type !== "data" && f.type !== "beat")
|
|
56
|
+
return;
|
|
57
|
+
if (Number.isInteger(f.ack))
|
|
58
|
+
for (const s of [...this.unacked.keys()])
|
|
59
|
+
if (s <= f.ack)
|
|
60
|
+
this.dropUnacked(s);
|
|
61
|
+
if (f.type === "beat")
|
|
62
|
+
return;
|
|
63
|
+
const msgs = Array.isArray(f.msgs) ? f.msgs.filter((m) => typeof m === "string") : [];
|
|
64
|
+
if (!Number.isInteger(f.seq) || f.seq < 1)
|
|
65
|
+
return;
|
|
66
|
+
this.ackOwed = true;
|
|
67
|
+
if (f.seq === this.rcvNext) {
|
|
68
|
+
this.rcvNext++;
|
|
69
|
+
for (const m of msgs)
|
|
70
|
+
this.o.deliver(m);
|
|
71
|
+
for (let h = this.held.get(this.rcvNext); h && !this.done; h = this.held.get(this.rcvNext)) {
|
|
72
|
+
this.held.delete(this.rcvNext);
|
|
73
|
+
this.rcvNext++;
|
|
74
|
+
for (const m of h)
|
|
75
|
+
this.o.deliver(m);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (f.seq > this.rcvNext && f.seq <= this.rcvNext + MAX_HELD)
|
|
79
|
+
this.held.set(f.seq, msgs);
|
|
80
|
+
// f.seq < rcvNext: a duplicate; the ack below tells the sender to stop resending
|
|
81
|
+
this.scheduleFlush();
|
|
82
|
+
}
|
|
83
|
+
/** Sends what is queued, then `close` (once), and stops. */
|
|
84
|
+
close(reason = "") {
|
|
85
|
+
if (this.done)
|
|
86
|
+
return;
|
|
87
|
+
this.flush();
|
|
88
|
+
this.o.send({ type: "close", seq: 0, ack: this.rcvNext - 1, ...(reason ? { reason } : {}) });
|
|
89
|
+
this.stop();
|
|
90
|
+
}
|
|
91
|
+
/** Stops the timers without telling the peer. */
|
|
92
|
+
stop() {
|
|
93
|
+
this.done = true;
|
|
94
|
+
this.cancelFlush?.();
|
|
95
|
+
this.cancelTick?.();
|
|
96
|
+
this.cancelFlush = this.cancelTick = null;
|
|
97
|
+
this.unacked.clear();
|
|
98
|
+
this.unackedBytes = 0;
|
|
99
|
+
this.held.clear();
|
|
100
|
+
this.queue = [];
|
|
101
|
+
this.queueBytes = 0;
|
|
102
|
+
}
|
|
103
|
+
scheduleFlush() {
|
|
104
|
+
if (this.cancelFlush || this.done)
|
|
105
|
+
return;
|
|
106
|
+
this.cancelFlush = this.o.clock.setTimeout(() => {
|
|
107
|
+
this.cancelFlush = null;
|
|
108
|
+
this.flush();
|
|
109
|
+
}, BATCH_MS);
|
|
110
|
+
}
|
|
111
|
+
flush() {
|
|
112
|
+
if (this.done)
|
|
113
|
+
return;
|
|
114
|
+
this.cancelFlush?.();
|
|
115
|
+
this.cancelFlush = null;
|
|
116
|
+
const now = this.o.clock.now();
|
|
117
|
+
if (this.queue.length === 0) {
|
|
118
|
+
if (this.ackOwed)
|
|
119
|
+
this.emit({ type: "beat", seq: 0, ack: this.rcvNext - 1 }, now);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let batch = [];
|
|
123
|
+
let bytes = 0;
|
|
124
|
+
const ship = () => {
|
|
125
|
+
if (batch.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
const frame = { type: "data", seq: this.nextSeq++, ack: this.rcvNext - 1, msgs: batch };
|
|
128
|
+
this.unacked.set(frame.seq, { bytes, frame, lastSent: now, resends: 0 });
|
|
129
|
+
this.unackedBytes += bytes;
|
|
130
|
+
this.emit(frame, now);
|
|
131
|
+
batch = [];
|
|
132
|
+
bytes = 0;
|
|
133
|
+
};
|
|
134
|
+
this.queueBytes = 0;
|
|
135
|
+
for (const m of this.queue.splice(0)) {
|
|
136
|
+
const n = byteLen(m);
|
|
137
|
+
if (batch.length > 0 && bytes + n > MAX_FRAME_BYTES)
|
|
138
|
+
ship();
|
|
139
|
+
batch.push(m);
|
|
140
|
+
bytes += n;
|
|
141
|
+
}
|
|
142
|
+
ship();
|
|
143
|
+
}
|
|
144
|
+
dropUnacked(seq) {
|
|
145
|
+
const p = this.unacked.get(seq);
|
|
146
|
+
if (!p)
|
|
147
|
+
return;
|
|
148
|
+
this.unackedBytes -= p.bytes;
|
|
149
|
+
this.unacked.delete(seq);
|
|
150
|
+
}
|
|
151
|
+
emit(f, now) {
|
|
152
|
+
this.lastSent = now;
|
|
153
|
+
this.ackOwed = false;
|
|
154
|
+
this.o.send(f);
|
|
155
|
+
}
|
|
156
|
+
tick() {
|
|
157
|
+
if (this.done)
|
|
158
|
+
return;
|
|
159
|
+
const now = this.o.clock.now();
|
|
160
|
+
if (now - this.lastHeard >= SILENCE_MS)
|
|
161
|
+
return this.die();
|
|
162
|
+
for (const p of this.unacked.values()) {
|
|
163
|
+
if (now - p.lastSent < RESEND_MS)
|
|
164
|
+
continue;
|
|
165
|
+
if (p.resends >= MAX_RESENDS)
|
|
166
|
+
return this.die();
|
|
167
|
+
p.resends++;
|
|
168
|
+
p.lastSent = now;
|
|
169
|
+
this.o.send({ ...p.frame, ack: this.rcvNext - 1 });
|
|
170
|
+
this.lastSent = now;
|
|
171
|
+
}
|
|
172
|
+
if (now - this.lastSent >= BEAT_MS)
|
|
173
|
+
this.emit({ type: "beat", seq: 0, ack: this.rcvNext - 1 }, now);
|
|
174
|
+
}
|
|
175
|
+
die() {
|
|
176
|
+
this.stop();
|
|
177
|
+
this.o.onDead("timeout");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../../src/nostr/link.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The frame layer of Nostr signaling (spec/nostr-signaling.md s.4): ordered, de-duplicated, acknowledged delivery of\n * hub protocol texts between two pubkeys over lossy relays. One {@link ReliableLink} per peer; it knows nothing about\n * relays, keys or encryption: the owner stamps and ships the frames it emits.\n */\nimport type { Clock } from \"../clock.js\";\nimport { BATCH_MS, RESEND_MS, MAX_RESENDS, BEAT_MS, SILENCE_MS, MAX_HELD, MAX_OUT_FRAMES, MAX_OUT_BYTES, MAX_FRAME_BYTES, TICK_MS } from \"./limits.js\";\n\nexport type FrameType = \"open\" | \"data\" | \"beat\" | \"close\";\n\n/** The wire frame (JSON in the encrypted event content). */\nexport interface Frame {\n v: 1;\n type: FrameType;\n from: string;\n to: string;\n seq: number;\n ack: number;\n msgs?: string[];\n reason?: string;\n}\n\n/** What a link emits; the owner adds v / from / to. */\nexport type OutFrame = Pick<Frame, \"type\" | \"seq\" | \"ack\"> & { msgs?: string[]; reason?: string };\n\nexport { BATCH_MS, RESEND_MS, MAX_RESENDS, BEAT_MS, SILENCE_MS, MAX_HELD, MAX_OUT_FRAMES, MAX_OUT_BYTES, MAX_FRAME_BYTES };\n\nconst utf8 = new TextEncoder();\nconst byteLen = (s: string) => utf8.encode(s).length;\n\nexport interface LinkOptions {\n clock: Clock;\n /** Ships a frame to the peer. */\n send: (f: OutFrame) => void;\n /** An in-order hub protocol text from the peer. */\n deliver: (text: string) => void;\n /** The peer sent `close`. */\n onPeerClose: (reason: string) => void;\n /** Gave up: five resends unanswered, or 30 s of silence (reason \"timeout\"). */\n onDead: (reason: string) => void;\n}\n\ninterface Pending {\n bytes: number;\n frame: OutFrame;\n lastSent: number;\n resends: number;\n}\n\nexport class ReliableLink {\n private readonly o: LinkOptions;\n private queue: string[] = [];\n private queueBytes = 0;\n private unackedBytes = 0;\n private nextSeq = 1;\n private readonly unacked = new Map<number, Pending>();\n private rcvNext = 1; // next seq to deliver\n private readonly held = new Map<number, string[]>();\n private ackOwed = false;\n private lastSent: number;\n private lastHeard: number;\n private cancelFlush: (() => void) | null = null;\n private cancelTick: (() => void) | null = null;\n private done = false;\n\n constructor(o: LinkOptions) {\n this.o = o;\n this.lastSent = this.lastHeard = o.clock.now();\n this.cancelTick = o.clock.setInterval(() => this.tick(), TICK_MS);\n }\n\n get closed(): boolean {\n return this.done;\n }\n\n /** Queues a hub text; sent within 30 ms, packed with its neighbours. */\n send(text: string): void {\n if (this.done) return;\n // relays refuse high publish rates: past the cap the text is dropped, the connection is not\n const n = byteLen(text);\n if (this.queue.length + this.unacked.size >= MAX_OUT_FRAMES || this.queueBytes + this.unackedBytes + n > MAX_OUT_BYTES) return;\n this.queue.push(text);\n this.queueBytes += n;\n this.scheduleFlush();\n }\n\n /** Any frame arrived from the peer (including `open`, which only proves it is alive). */\n heard(): void {\n this.lastHeard = this.o.clock.now();\n }\n\n /** `data`, `beat` and `close` frames from the peer. */\n receive(f: Frame): void {\n if (this.done) return;\n this.heard();\n if (f.type === \"close\") {\n const reason = typeof f.reason === \"string\" ? f.reason : \"\";\n this.stop();\n this.o.onPeerClose(reason);\n return;\n }\n if (f.type !== \"data\" && f.type !== \"beat\") return;\n if (Number.isInteger(f.ack)) for (const s of [...this.unacked.keys()]) if (s <= f.ack) this.dropUnacked(s);\n if (f.type === \"beat\") return;\n const msgs = Array.isArray(f.msgs) ? f.msgs.filter((m) => typeof m === \"string\") : [];\n if (!Number.isInteger(f.seq) || f.seq < 1) return;\n this.ackOwed = true;\n if (f.seq === this.rcvNext) {\n this.rcvNext++;\n for (const m of msgs) this.o.deliver(m);\n for (let h = this.held.get(this.rcvNext); h && !this.done; h = this.held.get(this.rcvNext)) {\n this.held.delete(this.rcvNext);\n this.rcvNext++;\n for (const m of h) this.o.deliver(m);\n }\n } else if (f.seq > this.rcvNext && f.seq <= this.rcvNext + MAX_HELD) this.held.set(f.seq, msgs);\n // f.seq < rcvNext: a duplicate; the ack below tells the sender to stop resending\n this.scheduleFlush();\n }\n\n /** Sends what is queued, then `close` (once), and stops. */\n close(reason = \"\"): void {\n if (this.done) return;\n this.flush();\n this.o.send({ type: \"close\", seq: 0, ack: this.rcvNext - 1, ...(reason ? { reason } : {}) });\n this.stop();\n }\n\n /** Stops the timers without telling the peer. */\n stop(): void {\n this.done = true;\n this.cancelFlush?.();\n this.cancelTick?.();\n this.cancelFlush = this.cancelTick = null;\n this.unacked.clear();\n this.unackedBytes = 0;\n this.held.clear();\n this.queue = [];\n this.queueBytes = 0;\n }\n\n private scheduleFlush(): void {\n if (this.cancelFlush || this.done) return;\n this.cancelFlush = this.o.clock.setTimeout(() => {\n this.cancelFlush = null;\n this.flush();\n }, BATCH_MS);\n }\n\n private flush(): void {\n if (this.done) return;\n this.cancelFlush?.();\n this.cancelFlush = null;\n const now = this.o.clock.now();\n if (this.queue.length === 0) {\n if (this.ackOwed) this.emit({ type: \"beat\", seq: 0, ack: this.rcvNext - 1 }, now);\n return;\n }\n let batch: string[] = [];\n let bytes = 0;\n const ship = () => {\n if (batch.length === 0) return;\n const frame: OutFrame = { type: \"data\", seq: this.nextSeq++, ack: this.rcvNext - 1, msgs: batch };\n this.unacked.set(frame.seq, { bytes, frame, lastSent: now, resends: 0 });\n this.unackedBytes += bytes;\n this.emit(frame, now);\n batch = [];\n bytes = 0;\n };\n this.queueBytes = 0;\n for (const m of this.queue.splice(0)) {\n const n = byteLen(m);\n if (batch.length > 0 && bytes + n > MAX_FRAME_BYTES) ship();\n batch.push(m);\n bytes += n;\n }\n ship();\n }\n\n private dropUnacked(seq: number): void {\n const p = this.unacked.get(seq);\n if (!p) return;\n this.unackedBytes -= p.bytes;\n this.unacked.delete(seq);\n }\n\n private emit(f: OutFrame, now: number): void {\n this.lastSent = now;\n this.ackOwed = false;\n this.o.send(f);\n }\n\n private tick(): void {\n if (this.done) return;\n const now = this.o.clock.now();\n if (now - this.lastHeard >= SILENCE_MS) return this.die();\n for (const p of this.unacked.values()) {\n if (now - p.lastSent < RESEND_MS) continue;\n if (p.resends >= MAX_RESENDS) return this.die();\n p.resends++;\n p.lastSent = now;\n this.o.send({ ...p.frame, ack: this.rcvNext - 1 });\n this.lastSent = now;\n }\n if (now - this.lastSent >= BEAT_MS) this.emit({ type: \"beat\", seq: 0, ack: this.rcvNext - 1 }, now);\n }\n\n private die(): void {\n this.stop();\n this.o.onDead(\"timeout\");\n }\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nostr events and the relay pool (spec/nostr-signaling.md s.3 and s.6). Anonymous: every connection uses a throw-away
|
|
3
|
+
* key and carries encrypted frames only.
|
|
4
|
+
*/
|
|
5
|
+
import type { Clock } from "../clock.js";
|
|
6
|
+
import type { SocketLike } from "../transport.js";
|
|
7
|
+
export declare const NOSTR_KIND = 25050;
|
|
8
|
+
export interface NostrEvent {
|
|
9
|
+
id: string;
|
|
10
|
+
pubkey: string;
|
|
11
|
+
created_at: number;
|
|
12
|
+
kind: number;
|
|
13
|
+
tags: string[][];
|
|
14
|
+
content: string;
|
|
15
|
+
sig: string;
|
|
16
|
+
}
|
|
17
|
+
/** NIP-01 event id: SHA-256 of `[0, pubkey, created_at, kind, tags, content]` serialised without whitespace. */
|
|
18
|
+
export declare function eventId(pubkey: string, createdAt: number, kind: number, tags: string[][], content: string): Promise<string>;
|
|
19
|
+
/** A signed kind-25050 event (BIP340 Schnorr over the id). */
|
|
20
|
+
export declare function makeEvent(secret: Uint8Array, topic: string, content: string, createdAt: number): Promise<NostrEvent>;
|
|
21
|
+
/** The WebSocket constructor to a relay; the browser's, or Node 22+'s global one. Tests pass their own. */
|
|
22
|
+
export type RelayConnect = (url: string) => SocketLike;
|
|
23
|
+
export declare const defaultRelayConnect: RelayConnect;
|
|
24
|
+
export interface RelayPoolOptions {
|
|
25
|
+
urls: readonly string[];
|
|
26
|
+
topic: string;
|
|
27
|
+
connect: RelayConnect;
|
|
28
|
+
clock: Clock;
|
|
29
|
+
/** Unix ms. */
|
|
30
|
+
wall: () => number;
|
|
31
|
+
onEvent: (ev: NostrEvent) => void;
|
|
32
|
+
/** Called when the number of connected relays changes. */
|
|
33
|
+
onChange?: () => void;
|
|
34
|
+
}
|
|
35
|
+
/** Every relay at once, each reconnected with backoff (1 s doubling to 30 s); events deduped by id. */
|
|
36
|
+
export declare class RelayPool {
|
|
37
|
+
private readonly o;
|
|
38
|
+
private readonly relays;
|
|
39
|
+
private readonly seen;
|
|
40
|
+
private readonly sub;
|
|
41
|
+
private closed;
|
|
42
|
+
private waiters;
|
|
43
|
+
constructor(o: RelayPoolOptions);
|
|
44
|
+
get total(): number;
|
|
45
|
+
/** Relays connected right now (and not skipped). */
|
|
46
|
+
get connected(): number;
|
|
47
|
+
/** Every relay has either connected, failed a connect, or been skipped: waiting longer for the rest is pointless. */
|
|
48
|
+
get settled(): boolean;
|
|
49
|
+
start(): void;
|
|
50
|
+
/** Resolves true once one relay is connected, false after `timeoutMs` (or when closed). */
|
|
51
|
+
whenUp(timeoutMs: number): Promise<boolean>;
|
|
52
|
+
publish(ev: NostrEvent): void;
|
|
53
|
+
close(): void;
|
|
54
|
+
private changed;
|
|
55
|
+
private trySend;
|
|
56
|
+
private dial;
|
|
57
|
+
private drop;
|
|
58
|
+
private retryLater;
|
|
59
|
+
private skip;
|
|
60
|
+
private onMessage;
|
|
61
|
+
}
|
|
62
|
+
/** Default public relays (probed 2026-10-26, docs/design/no-server-fallback.md): all accept and echo ephemeral kind 25050. */
|
|
63
|
+
export declare const DEFAULT_NOSTR_RELAYS: readonly string[];
|
|
64
|
+
/** Public STUN servers for Nostr joiners (spec s.5). */
|
|
65
|
+
export declare const DEFAULT_NOSTR_STUN: readonly string[];
|
|
66
|
+
export interface RelayCheck {
|
|
67
|
+
url: string;
|
|
68
|
+
ok: boolean;
|
|
69
|
+
ms: number;
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Probes each relay: connect, subscribe, publish one kind-25050 event to itself under a random topic with a random
|
|
74
|
+
* key, and see it echoed. Anonymous (nothing about the player is sent). Use it to pick `hosting.nostrRelays`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function checkNostrRelays(urls: readonly string[], timeoutMs?: number, opts?: {
|
|
77
|
+
connect?: RelayConnect;
|
|
78
|
+
clock?: Clock;
|
|
79
|
+
}): Promise<RelayCheck[]>;
|