wireweave 0.1.1

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.
@@ -0,0 +1,170 @@
1
+ const DEFAULT_RELAYS = [
2
+ 'wss://relay.damus.io',
3
+ 'wss://relay.primal.net',
4
+ 'wss://nos.lol',
5
+ 'wss://relay.snort.social'
6
+ ];
7
+
8
+ const trim = (set, max, keep) => {
9
+ if (set.size <= max) return set;
10
+ const arr = Array.from(set);
11
+ return new Set(arr.slice(arr.length - keep));
12
+ };
13
+
14
+ export class RelayPool extends EventTarget {
15
+ constructor({ relays = DEFAULT_RELAYS, verifyEvent = null, WebSocketImpl = null } = {}) {
16
+ super();
17
+ this.urls = [...relays];
18
+ this.relays = new Map();
19
+ this.subs = new Map();
20
+ this.pending = [];
21
+ this.seen = new Set();
22
+ this.verifyEvent = verifyEvent;
23
+ this.WS = WebSocketImpl || (typeof WebSocket !== 'undefined' ? WebSocket : null);
24
+ if (!this.WS) throw new Error('No WebSocket implementation available');
25
+ }
26
+
27
+ connect() {
28
+ for (const url of this.urls) this._open(url);
29
+ }
30
+
31
+ disconnect() {
32
+ for (const [, r] of this.relays) {
33
+ if (r.ws) { r.ws.onclose = null; r.ws.onerror = null; try { r.ws.close(); } catch {} }
34
+ }
35
+ this.relays.clear();
36
+ }
37
+
38
+ _open(url) {
39
+ const existing = this.relays.get(url);
40
+ if (existing?.ws && (existing.ws.readyState === 0 || existing.ws.readyState === 1)) return;
41
+ const relay = existing || { ws: null, status: 'connecting', subIds: new Set(), latencyMs: null, failCount: 0, reconnectDelay: 1000, _reqSentAt: null, _openedAt: null };
42
+ relay.status = 'connecting';
43
+ relay.latencyMs = null;
44
+ this.relays.set(url, relay);
45
+ let ws;
46
+ try { ws = new this.WS(url); }
47
+ catch (e) { relay.status = 'error'; this._emit('relay-status', { url, status: 'error' }); return; }
48
+ relay.ws = ws;
49
+ ws.onopen = () => {
50
+ relay.status = 'connected';
51
+ relay._openedAt = Date.now();
52
+ this._emit('relay-status', { url, status: 'connected' });
53
+ for (const [subId, sub] of this.subs) {
54
+ ws.send(JSON.stringify(['REQ', subId, ...sub.filters]));
55
+ relay.subIds.add(subId);
56
+ if (!relay._reqSentAt) relay._reqSentAt = Date.now();
57
+ }
58
+ this._drainPending();
59
+ };
60
+ ws.onmessage = (e) => {
61
+ if (relay._reqSentAt && relay.latencyMs === null) {
62
+ relay.latencyMs = Date.now() - relay._reqSentAt;
63
+ relay._reqSentAt = null;
64
+ }
65
+ try { this._handle(url, typeof e.data === 'string' ? e.data : e.data.toString()); } catch {}
66
+ };
67
+ ws.onerror = () => { relay.status = 'error'; this._emit('relay-status', { url, status: 'error' }); };
68
+ ws.onclose = () => {
69
+ relay.status = 'closed';
70
+ this._emit('relay-status', { url, status: 'closed' });
71
+ const sustained = relay._openedAt && Date.now() - relay._openedAt > 5000;
72
+ if (sustained) { relay.failCount = 0; relay.reconnectDelay = 1000; }
73
+ else { relay.failCount++; relay.reconnectDelay = Math.min(relay.reconnectDelay * 2, 30000); }
74
+ relay._openedAt = null;
75
+ setTimeout(() => this._open(url), relay.reconnectDelay);
76
+ };
77
+ }
78
+
79
+ _handle(url, raw) {
80
+ const msg = JSON.parse(raw);
81
+ if (!Array.isArray(msg) || msg.length < 2) return;
82
+ const [type, subId] = msg;
83
+ if (type === 'EVENT') {
84
+ const event = msg[2];
85
+ if (!event?.id) return;
86
+ if (event.created_at > Math.floor(Date.now() / 1000) + 300) return;
87
+ if (this.seen.has(event.id)) return;
88
+ if (this.verifyEvent) {
89
+ try { if (!this.verifyEvent(event)) return; } catch { return; }
90
+ }
91
+ this.seen.add(event.id);
92
+ this.seen = trim(this.seen, 10000, 5000);
93
+ const sub = this.subs.get(subId);
94
+ sub?.onEvent?.(event);
95
+ this._emit('event', { subId, event });
96
+ } else if (type === 'EOSE') {
97
+ this.subs.get(subId)?.onEose?.();
98
+ this._emit('eose', { subId });
99
+ } else if (type === 'NOTICE') {
100
+ this._emit('notice', { url, message: msg[1] });
101
+ } else if (type === 'OK' && !msg[2]) {
102
+ this._emit('reject', { id: msg[1], reason: msg[3] || '' });
103
+ }
104
+ }
105
+
106
+ subscribe(subId, filters, onEvent, onEose) {
107
+ subId = subId.length > 64 ? subId.slice(0, 64) : subId;
108
+ this.subs.set(subId, { filters, onEvent, onEose });
109
+ for (const [, relay] of this.relays) {
110
+ if (relay.ws?.readyState === 1) {
111
+ relay.ws.send(JSON.stringify(['REQ', subId, ...filters]));
112
+ relay.subIds.add(subId);
113
+ if (!relay._reqSentAt) relay._reqSentAt = Date.now();
114
+ }
115
+ }
116
+ return subId;
117
+ }
118
+
119
+ unsubscribe(subId) {
120
+ subId = subId.length > 64 ? subId.slice(0, 64) : subId;
121
+ for (const [, relay] of this.relays) {
122
+ if (relay.ws?.readyState === 1 && relay.subIds.has(subId)) {
123
+ relay.ws.send(JSON.stringify(['CLOSE', subId]));
124
+ relay.subIds.delete(subId);
125
+ }
126
+ }
127
+ this.subs.delete(subId);
128
+ }
129
+
130
+ publish(event) {
131
+ let sent = false;
132
+ for (const [, relay] of this.relays) {
133
+ if (relay.ws?.readyState === 1) {
134
+ relay.ws.send(JSON.stringify(['EVENT', event]));
135
+ sent = true;
136
+ }
137
+ }
138
+ if (!sent) this.pending.push(event);
139
+ return sent;
140
+ }
141
+
142
+ _drainPending() {
143
+ const pending = this.pending.splice(0);
144
+ for (const e of pending) this.publish(e);
145
+ }
146
+
147
+ isConnected() {
148
+ for (const [, r] of this.relays) if (r.ws?.readyState === 1) return true;
149
+ return false;
150
+ }
151
+
152
+ status() {
153
+ const out = [];
154
+ for (const [url, r] of this.relays) out.push({ url, status: r.status, latencyMs: r.latencyMs });
155
+ return out;
156
+ }
157
+
158
+ heal() {
159
+ for (const [url, r] of this.relays) {
160
+ if (!r.ws || r.ws.readyState === 2 || r.ws.readyState === 3) {
161
+ r.reconnectDelay = 1000;
162
+ this._open(url);
163
+ }
164
+ }
165
+ }
166
+
167
+ _emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail })); }
168
+ }
169
+
170
+ export const createRelayPool = (opts) => new RelayPool(opts);