@soapbox.pub/nostr-lora 0.1.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/assembler.d.ts +74 -0
- package/dist/connection-manager.d.ts +30 -0
- package/dist/constants.d.ts +31 -0
- package/dist/event-codec.d.ts +15 -0
- package/dist/gm-protocol.d.ts +30 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/lora-transport.d.ts +109 -0
- package/dist/protocol.d.ts +78 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +57 -0
- package/dist/react.js.map +1 -0
- package/dist/send-queue.d.ts +40 -0
- package/dist/serial-connection.d.ts +41 -0
- package/dist/serial-connection.js +955 -0
- package/dist/serial-connection.js.map +1 -0
- package/dist/use-lora-transport.d.ts +32 -0
- package/dist/utils.d.ts +24 -0
- package/package.json +48 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
var D = Object.defineProperty;
|
|
2
|
+
var U = (o, s, t) => s in o ? D(o, s, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[s] = t;
|
|
3
|
+
var a = (o, s, t) => U(o, typeof s != "symbol" ? s + "" : s, t);
|
|
4
|
+
import { verifyEvent as K } from "nostr-tools";
|
|
5
|
+
import { WebSerialConnection as L, Constants as P } from "@liamcottle/meshcore.js";
|
|
6
|
+
const g = {
|
|
7
|
+
DATA: 1,
|
|
8
|
+
REQUEST: 2,
|
|
9
|
+
GM: 3
|
|
10
|
+
}, E = {
|
|
11
|
+
FIRST_CHUNK_PAYLOAD: 124,
|
|
12
|
+
SUBSEQUENT_CHUNK_PAYLOAD: 151,
|
|
13
|
+
// 168 - 17
|
|
14
|
+
EVENT_ID_PREFIX_LEN: 14,
|
|
15
|
+
// first 14 bytes of event_id (2^112 collision resistance)
|
|
16
|
+
MAX_CHUNKS: 8,
|
|
17
|
+
MAX_COMPRESSED_SIZE: 1152
|
|
18
|
+
// 1024 content + ~128 for 64-byte sig (hex)
|
|
19
|
+
};
|
|
20
|
+
class y {
|
|
21
|
+
static makeTable() {
|
|
22
|
+
if (!this.table) {
|
|
23
|
+
this.table = new Uint32Array(256);
|
|
24
|
+
for (let s = 0; s < 256; s++) {
|
|
25
|
+
let t = s;
|
|
26
|
+
for (let e = 0; e < 8; e++)
|
|
27
|
+
t = t & 1 ? 3988292384 ^ t >>> 1 : t >>> 1;
|
|
28
|
+
this.table[s] = t;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
static calculate(s) {
|
|
33
|
+
this.makeTable();
|
|
34
|
+
let t = 4294967295;
|
|
35
|
+
for (let e = 0; e < s.length; e++)
|
|
36
|
+
t = t >>> 8 ^ this.table[(t ^ s[e]) & 255];
|
|
37
|
+
return (t ^ 4294967295) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
a(y, "table", null);
|
|
41
|
+
class q {
|
|
42
|
+
static async compress(s) {
|
|
43
|
+
const e = new Blob([s]).stream().pipeThrough(
|
|
44
|
+
new CompressionStream("deflate-raw")
|
|
45
|
+
), n = await new Response(e).blob();
|
|
46
|
+
return new Uint8Array(await n.arrayBuffer());
|
|
47
|
+
}
|
|
48
|
+
static async decompress(s) {
|
|
49
|
+
const e = new Blob([s]).stream().pipeThrough(
|
|
50
|
+
new DecompressionStream("deflate-raw")
|
|
51
|
+
), n = await new Response(e).blob();
|
|
52
|
+
return new Uint8Array(await n.arrayBuffer());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class b {
|
|
56
|
+
static createFromCompressed(s, t, e, n) {
|
|
57
|
+
const i = y.calculate(e);
|
|
58
|
+
let r = e.length - E.FIRST_CHUNK_PAYLOAD, u = 1;
|
|
59
|
+
for (; r > 0; )
|
|
60
|
+
u++, r -= E.SUBSEQUENT_CHUNK_PAYLOAD;
|
|
61
|
+
if (u > E.MAX_CHUNKS)
|
|
62
|
+
throw new Error(`Event requires ${u} chunks, max is ${E.MAX_CHUNKS}`);
|
|
63
|
+
const c = [];
|
|
64
|
+
let h = 0;
|
|
65
|
+
for (let l = 0; l < u; l++) {
|
|
66
|
+
const T = l === u - 1, _ = l === 0 ? E.FIRST_CHUNK_PAYLOAD : E.SUBSEQUENT_CHUNK_PAYLOAD, I = Math.min(_, e.length - h), w = e.slice(h, h + I);
|
|
67
|
+
l === 0 ? c.push(this.encodeFirst(
|
|
68
|
+
s,
|
|
69
|
+
t,
|
|
70
|
+
u,
|
|
71
|
+
i,
|
|
72
|
+
n,
|
|
73
|
+
w,
|
|
74
|
+
T
|
|
75
|
+
)) : c.push(this.encodeSubsequent(
|
|
76
|
+
s.slice(0, E.EVENT_ID_PREFIX_LEN),
|
|
77
|
+
l,
|
|
78
|
+
w,
|
|
79
|
+
T
|
|
80
|
+
)), h += I;
|
|
81
|
+
}
|
|
82
|
+
return c;
|
|
83
|
+
}
|
|
84
|
+
static encodeFirst(s, t, e, n, i, r, u) {
|
|
85
|
+
const c = new Uint8Array(44 + r.length);
|
|
86
|
+
let h = 0;
|
|
87
|
+
return c[h++] = g.DATA << 4 | (u ? 1 : 0), c[h++] = 0, c[h++] = 0, c.set(s.slice(0, 32), h), h += 32, c[h++] = e >> 8 & 255, c[h++] = e & 255, c[h++] = t >> 8 & 255, c[h++] = t & 255, c[h++] = n >> 24 & 255, c[h++] = n >> 16 & 255, c[h++] = n >> 8 & 255, c[h++] = n & 255, c[h++] = i, c.set(r, h), c;
|
|
88
|
+
}
|
|
89
|
+
static encodeSubsequent(s, t, e, n) {
|
|
90
|
+
const i = new Uint8Array(17 + e.length);
|
|
91
|
+
let r = 0;
|
|
92
|
+
return i[r++] = g.DATA << 4 | (n ? 1 : 0), i.set(s.slice(0, 14), r), r += 14, i[r++] = t >> 8 & 255, i[r++] = t & 255, i.set(e, r), i;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Decode DATA packet.
|
|
96
|
+
* Distinguishes first vs subsequent by peeking at bytes 1-2:
|
|
97
|
+
* - First chunk: bytes 1-2 are chunk_index == 0x0000, event_id at offset 3
|
|
98
|
+
* - Subsequent chunk: bytes 1-2 are part of event_id_prefix, chunk_index at offset 15
|
|
99
|
+
*
|
|
100
|
+
* For subsequent chunks, the first 2 bytes of event_id_prefix are statistically
|
|
101
|
+
* never 0x0000 for real event IDs, so 0x0000 reliably identifies first chunks.
|
|
102
|
+
*/
|
|
103
|
+
static decode(s) {
|
|
104
|
+
const e = (s[0] & 1) !== 0;
|
|
105
|
+
if ((s[1] << 8 | s[2]) === 0) {
|
|
106
|
+
const i = s.slice(3, 35), r = s[35] << 8 | s[36], u = s[37] << 8 | s[38], c = (s[39] << 24 | s[40] << 16 | s[41] << 8 | s[42]) >>> 0, h = s[43], l = s.slice(44);
|
|
107
|
+
return { type: "first", eventId: i, eventKind: u, totalChunks: r, checksum: c, ttl: h, payload: l, isLast: e };
|
|
108
|
+
} else {
|
|
109
|
+
const i = s.slice(1, 15), r = s[15] << 8 | s[16], u = s.slice(17);
|
|
110
|
+
return { type: "subsequent", eventIdPrefix: i, chunkIndex: r, payload: u, isLast: e };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
class R {
|
|
115
|
+
static create(s, t) {
|
|
116
|
+
const e = new Uint8Array(16);
|
|
117
|
+
let n = 0;
|
|
118
|
+
e[n++] = g.REQUEST << 4, e.set(s.slice(0, E.EVENT_ID_PREFIX_LEN), n), n += E.EVENT_ID_PREFIX_LEN;
|
|
119
|
+
let i = 0;
|
|
120
|
+
for (const r of t)
|
|
121
|
+
r < 8 && (i |= 1 << r);
|
|
122
|
+
return e[n++] = i & 255, e;
|
|
123
|
+
}
|
|
124
|
+
static decode(s) {
|
|
125
|
+
const t = s.slice(1, 1 + E.EVENT_ID_PREFIX_LEN), e = s[1 + E.EVENT_ID_PREFIX_LEN], n = [];
|
|
126
|
+
for (let i = 0; i < 8; i++)
|
|
127
|
+
e & 1 << i && n.push(i);
|
|
128
|
+
return { eventIdPrefix: t, missingChunks: n };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
class v {
|
|
132
|
+
static create(s, t, e, n = Math.floor(Date.now() / 1e3)) {
|
|
133
|
+
const i = new Uint8Array(21);
|
|
134
|
+
let r = 0;
|
|
135
|
+
return i[r++] = g.GM << 4, i.set(s.slice(0, 8), r), r += 8, i[r++] = n >> 24 & 255, i[r++] = n >> 16 & 255, i[r++] = n >> 8 & 255, i[r++] = n & 255, i[r++] = t >> 24 & 255, i[r++] = t >> 16 & 255, i[r++] = t >> 8 & 255, i[r++] = t & 255, i[r++] = e >> 24 & 255, i[r++] = e >> 16 & 255, i[r++] = e >> 8 & 255, i[r++] = e & 255, i;
|
|
136
|
+
}
|
|
137
|
+
static decode(s) {
|
|
138
|
+
const t = s.slice(1, 9), e = (s[9] << 24 | s[10] << 16 | s[11] << 8 | s[12]) >>> 0, n = (s[13] << 24 | s[14] << 16 | s[15] << 8 | s[16]) >>> 0, i = (s[17] << 24 | s[18] << 16 | s[19] << 8 | s[20]) >>> 0;
|
|
139
|
+
return { nodeId: t, timestamp: e, eventCount: n, recentSince: i };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function F(o) {
|
|
143
|
+
return !o || o.length === 0 ? -1 : o[0] >> 4 & 15;
|
|
144
|
+
}
|
|
145
|
+
const G = new TextEncoder(), X = new TextDecoder();
|
|
146
|
+
async function J(o) {
|
|
147
|
+
if (!o.id || typeof o.id != "string")
|
|
148
|
+
throw new Error("Event must have string id");
|
|
149
|
+
if (!o.pubkey || typeof o.pubkey != "string")
|
|
150
|
+
throw new Error("Event must have string pubkey");
|
|
151
|
+
if (!o.created_at || typeof o.created_at != "number")
|
|
152
|
+
throw new Error("Event must have numeric created_at");
|
|
153
|
+
if (o.kind === void 0 || typeof o.kind != "number")
|
|
154
|
+
throw new Error("Event must have numeric kind");
|
|
155
|
+
if (!Array.isArray(o.tags))
|
|
156
|
+
throw new Error("Event tags must be array");
|
|
157
|
+
if (typeof o.content != "string")
|
|
158
|
+
throw new Error("Event content must be string");
|
|
159
|
+
if (!o.sig || typeof o.sig != "string")
|
|
160
|
+
throw new Error("Event must have string sig");
|
|
161
|
+
const s = JSON.stringify([
|
|
162
|
+
0,
|
|
163
|
+
o.pubkey,
|
|
164
|
+
o.created_at,
|
|
165
|
+
o.kind,
|
|
166
|
+
o.tags,
|
|
167
|
+
o.content,
|
|
168
|
+
o.sig
|
|
169
|
+
]), t = await q.compress(G.encode(s));
|
|
170
|
+
if (t.length > E.MAX_COMPRESSED_SIZE)
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Compressed event too large: ${t.length} bytes (max ${E.MAX_COMPRESSED_SIZE})`
|
|
173
|
+
);
|
|
174
|
+
const e = y.calculate(t);
|
|
175
|
+
return { compressed: t, checksum: e };
|
|
176
|
+
}
|
|
177
|
+
async function $(o, s, t) {
|
|
178
|
+
const e = o.reduce((l, T) => l + T.length, 0), n = new Uint8Array(e);
|
|
179
|
+
let i = 0;
|
|
180
|
+
for (const l of o)
|
|
181
|
+
n.set(l, i), i += l.length;
|
|
182
|
+
const r = y.calculate(n);
|
|
183
|
+
if (r !== s)
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Checksum mismatch: expected 0x${s.toString(16)}, got 0x${r.toString(16)}`
|
|
186
|
+
);
|
|
187
|
+
const u = await q.decompress(n), c = JSON.parse(X.decode(u)), h = {
|
|
188
|
+
id: t,
|
|
189
|
+
pubkey: c[1],
|
|
190
|
+
created_at: c[2],
|
|
191
|
+
kind: c[3],
|
|
192
|
+
tags: c[4],
|
|
193
|
+
content: c[5],
|
|
194
|
+
sig: c[6]
|
|
195
|
+
};
|
|
196
|
+
if (!K(h))
|
|
197
|
+
throw new Error(`Invalid signature for event ${t.substring(0, 16)}…`);
|
|
198
|
+
return h;
|
|
199
|
+
}
|
|
200
|
+
function p(o) {
|
|
201
|
+
const s = new Uint8Array(o.length / 2);
|
|
202
|
+
for (let t = 0; t < s.length; t++)
|
|
203
|
+
s[t] = parseInt(o.substr(t * 2, 2), 16);
|
|
204
|
+
return s;
|
|
205
|
+
}
|
|
206
|
+
function m(o) {
|
|
207
|
+
return Array.from(o).map((s) => s.toString(16).padStart(2, "0")).join("");
|
|
208
|
+
}
|
|
209
|
+
function N(o, s) {
|
|
210
|
+
for (let t = 0; t < s.length; t++)
|
|
211
|
+
if (o[t] !== s[t]) return !1;
|
|
212
|
+
return !0;
|
|
213
|
+
}
|
|
214
|
+
function S(o) {
|
|
215
|
+
return new Promise((s) => setTimeout(s, o));
|
|
216
|
+
}
|
|
217
|
+
class A {
|
|
218
|
+
constructor() {
|
|
219
|
+
a(this, "listeners", /* @__PURE__ */ new Map());
|
|
220
|
+
}
|
|
221
|
+
on(s, t) {
|
|
222
|
+
return this.listeners.has(s) || this.listeners.set(s, /* @__PURE__ */ new Set()), this.listeners.get(s).add(t), () => {
|
|
223
|
+
var e;
|
|
224
|
+
return (e = this.listeners.get(s)) == null ? void 0 : e.delete(t);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
emit(s, ...t) {
|
|
228
|
+
for (const e of this.listeners.get(s) ?? [])
|
|
229
|
+
try {
|
|
230
|
+
e(...t);
|
|
231
|
+
} catch (n) {
|
|
232
|
+
console.error(`[EventEmitter] '${s}' listener threw:`, n);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
class B {
|
|
237
|
+
constructor(s) {
|
|
238
|
+
a(this, "eventId");
|
|
239
|
+
a(this, "eventKind");
|
|
240
|
+
a(this, "totalChunks");
|
|
241
|
+
a(this, "checksum");
|
|
242
|
+
a(this, "chunks");
|
|
243
|
+
a(this, "receivedChunks");
|
|
244
|
+
this.eventId = s.eventId, this.eventKind = s.eventKind, this.totalChunks = s.totalChunks, this.checksum = s.checksum, this.chunks = new Array(this.totalChunks), this.chunks[0] = s.payload, this.receivedChunks = 1;
|
|
245
|
+
}
|
|
246
|
+
get isComplete() {
|
|
247
|
+
return this.receivedChunks === this.totalChunks;
|
|
248
|
+
}
|
|
249
|
+
/** Returns true if this was a new chunk, false if duplicate. */
|
|
250
|
+
addChunk(s, t) {
|
|
251
|
+
return this.chunks[s] !== void 0 ? !1 : (this.chunks[s] = t, this.receivedChunks++, !0);
|
|
252
|
+
}
|
|
253
|
+
missingIndices() {
|
|
254
|
+
const s = [];
|
|
255
|
+
for (let t = 0; t < this.totalChunks; t++)
|
|
256
|
+
this.chunks[t] === void 0 && s.push(t);
|
|
257
|
+
return s;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
class V {
|
|
261
|
+
constructor() {
|
|
262
|
+
/** In-progress sessions keyed by the full event ID as a hex string. */
|
|
263
|
+
a(this, "sessions", /* @__PURE__ */ new Map());
|
|
264
|
+
/** Subsequent chunks that arrived before their first chunk. */
|
|
265
|
+
a(this, "pendingSubsequent", []);
|
|
266
|
+
}
|
|
267
|
+
// ─── First-chunk path ───────────────────────────────────────────────────
|
|
268
|
+
handleFirst(s, t) {
|
|
269
|
+
const e = m(s.eventId);
|
|
270
|
+
if (s.ttl === 0) return { action: "none" };
|
|
271
|
+
if (s.totalChunks > E.MAX_CHUNKS) return { action: "none" };
|
|
272
|
+
if (t.has(e)) return { action: "none" };
|
|
273
|
+
if (this.sessions.has(e)) return { action: "none" };
|
|
274
|
+
const n = new B(s);
|
|
275
|
+
return this.sessions.set(e, n), this._replayPending(e, n), n.isComplete ? (this.sessions.delete(e), { action: "complete", session: n }) : { action: "started", session: n };
|
|
276
|
+
}
|
|
277
|
+
// ─── Subsequent-chunk path ──────────────────────────────────────────────
|
|
278
|
+
handleSubsequent(s, t) {
|
|
279
|
+
const e = this._findSessionForPrefix(s.eventIdPrefix);
|
|
280
|
+
if (!e)
|
|
281
|
+
return this.pendingSubsequent.push({
|
|
282
|
+
eventIdPrefix: s.eventIdPrefix,
|
|
283
|
+
chunkIndex: s.chunkIndex,
|
|
284
|
+
payload: s.payload,
|
|
285
|
+
isLast: s.isLast,
|
|
286
|
+
arrivedAt: Date.now()
|
|
287
|
+
}), this._prunePending(t), { action: "pending", prefix: s.eventIdPrefix };
|
|
288
|
+
const n = this.sessions.get(e);
|
|
289
|
+
return n.addChunk(s.chunkIndex, s.payload) ? n.isComplete ? (this.sessions.delete(e), { action: "complete", session: n }) : { action: "progress", session: n, eventIdHex: e } : { action: "none" };
|
|
290
|
+
}
|
|
291
|
+
// ─── Timer-driven queries ────────────────────────────────────────────────
|
|
292
|
+
/** Returns missing chunk indices for a session, or null if it no longer exists. */
|
|
293
|
+
missingChunks(s) {
|
|
294
|
+
const t = this.sessions.get(s);
|
|
295
|
+
return t ? t.missingIndices() : null;
|
|
296
|
+
}
|
|
297
|
+
/** Abandon an in-progress session after max retries exceeded. */
|
|
298
|
+
abandonSession(s) {
|
|
299
|
+
this.sessions.delete(s);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Returns missing chunk indices for a pending-subsequent group
|
|
303
|
+
* (chunk 0 still missing), or null if no buffered chunks exist for this prefix.
|
|
304
|
+
*/
|
|
305
|
+
missingChunksForPending(s) {
|
|
306
|
+
const t = this.pendingSubsequent.filter(
|
|
307
|
+
(c) => m(c.eventIdPrefix) === s
|
|
308
|
+
);
|
|
309
|
+
if (t.length === 0) return null;
|
|
310
|
+
const e = new Set(t.map((c) => c.chunkIndex)), n = t.find((c) => c.isLast), i = Math.max(...e), r = n ? n.chunkIndex + 1 : i + 1, u = [];
|
|
311
|
+
for (let c = 0; c < r; c++)
|
|
312
|
+
e.has(c) || u.push(c);
|
|
313
|
+
return u.length > 0 ? u : null;
|
|
314
|
+
}
|
|
315
|
+
/** Abandon all pending entries for a given prefix after max retries exceeded. */
|
|
316
|
+
abandonPending(s) {
|
|
317
|
+
this.pendingSubsequent = this.pendingSubsequent.filter(
|
|
318
|
+
(t) => m(t.eventIdPrefix) !== s
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
/** True if a session is currently in progress for this event ID. */
|
|
322
|
+
hasSession(s) {
|
|
323
|
+
return this.sessions.has(s);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Returns the raw eventId bytes for an in-progress session, or null.
|
|
327
|
+
* Used by the transport to build REQUEST prefix bytes.
|
|
328
|
+
*/
|
|
329
|
+
getSessionEventId(s) {
|
|
330
|
+
var t;
|
|
331
|
+
return ((t = this.sessions.get(s)) == null ? void 0 : t.eventId) ?? null;
|
|
332
|
+
}
|
|
333
|
+
// ─── Internals ───────────────────────────────────────────────────────────
|
|
334
|
+
_findSessionForPrefix(s) {
|
|
335
|
+
for (const [t] of this.sessions.entries())
|
|
336
|
+
if (N(p(t), s)) return t;
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
_replayPending(s, t) {
|
|
340
|
+
const e = [];
|
|
341
|
+
for (const n of this.pendingSubsequent)
|
|
342
|
+
N(p(s), n.eventIdPrefix) ? t.addChunk(n.chunkIndex, n.payload) : e.push(n);
|
|
343
|
+
this.pendingSubsequent = e;
|
|
344
|
+
}
|
|
345
|
+
_prunePending(s) {
|
|
346
|
+
const t = Date.now() - s;
|
|
347
|
+
this.pendingSubsequent = this.pendingSubsequent.filter(
|
|
348
|
+
(e) => e.arrivedAt > t
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const d = {
|
|
353
|
+
// ── SendQueue ─────────────────────────────────────────────────────────────
|
|
354
|
+
/** Base delay between packets within a single queue item (ms). */
|
|
355
|
+
INTER_PACKET_DELAY: 2e3,
|
|
356
|
+
/** Maximum additional random jitter added on top of INTER_PACKET_DELAY (ms). */
|
|
357
|
+
INTER_PACKET_JITTER: 500,
|
|
358
|
+
// ── GmProtocol ────────────────────────────────────────────────────────────
|
|
359
|
+
/** Per-peer minimum response interval before the first backoff multiplier kicks in (ms). */
|
|
360
|
+
GM_BACKOFF_BASE: 300 * 1e3,
|
|
361
|
+
/** Maximum number of our own events to re-share in response to one GM. */
|
|
362
|
+
GM_MAX_SHARE_EVENTS: 3,
|
|
363
|
+
/** Lower bound of the random pre-share / pre-reply delay (ms). */
|
|
364
|
+
GM_JITTER_MIN: 500,
|
|
365
|
+
/** Upper bound of the random pre-share / pre-reply delay (ms). */
|
|
366
|
+
GM_JITTER_MAX: 1500,
|
|
367
|
+
// ── LoRaTransport ─────────────────────────────────────────────────────────
|
|
368
|
+
/** How long to wait for the next chunk before declaring a gap and sending REQUEST (ms). */
|
|
369
|
+
REQUEST_INACTIVITY: 3e4,
|
|
370
|
+
/** Number of REQUEST retries before abandoning a partial event. */
|
|
371
|
+
REQUEST_MAX_RETRIES: 3,
|
|
372
|
+
/** How long to keep sent chunk packets around for potential retransmission (ms). */
|
|
373
|
+
SENT_CHUNKS_TTL: 300 * 1e3,
|
|
374
|
+
/** How long before we stop deduplicating a previously-seen event ID (ms). */
|
|
375
|
+
DEDUP_TIMEOUT: 900 * 1e3,
|
|
376
|
+
/** How long to wait for subsequent chunks of a partial event (ms). */
|
|
377
|
+
EVENT_TIMEOUT: 3e4,
|
|
378
|
+
/** TTL assigned to locally-created events (decremented on each re-broadcast hop). */
|
|
379
|
+
INITIAL_TTL: 6
|
|
380
|
+
};
|
|
381
|
+
class O extends A {
|
|
382
|
+
constructor(t, e = {}, n) {
|
|
383
|
+
super();
|
|
384
|
+
a(this, "sendFn");
|
|
385
|
+
a(this, "items", []);
|
|
386
|
+
a(this, "running", !1);
|
|
387
|
+
a(this, "seq", 0);
|
|
388
|
+
a(this, "log");
|
|
389
|
+
a(this, "INTER_PACKET_DELAY");
|
|
390
|
+
a(this, "INTER_PACKET_JITTER");
|
|
391
|
+
if (typeof t != "function")
|
|
392
|
+
throw new Error("SendQueue requires a sendFn(packet) argument");
|
|
393
|
+
this.sendFn = t, this.log = n ?? null, this.INTER_PACKET_DELAY = e.INTER_PACKET_DELAY ?? d.INTER_PACKET_DELAY, this.INTER_PACKET_JITTER = e.INTER_PACKET_JITTER ?? d.INTER_PACKET_JITTER;
|
|
394
|
+
}
|
|
395
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
396
|
+
enqueue(t, e, n, i = {}) {
|
|
397
|
+
const r = ++this.seq;
|
|
398
|
+
return this.items.push({ id: r, label: e, type: n, packets: t, meta: i, status: "pending" }), this.notifyUpdate(), this.drain(), r;
|
|
399
|
+
}
|
|
400
|
+
get snapshot() {
|
|
401
|
+
return this.items.map((t) => ({
|
|
402
|
+
id: t.id,
|
|
403
|
+
label: t.label,
|
|
404
|
+
type: t.type,
|
|
405
|
+
packetCount: t.packets.length,
|
|
406
|
+
status: t.status
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
// ─── Internal ────────────────────────────────────────────────────────────
|
|
410
|
+
notifyUpdate() {
|
|
411
|
+
this.emit("queue:update", this.snapshot);
|
|
412
|
+
}
|
|
413
|
+
drain() {
|
|
414
|
+
this.running || (this.running = !0, this.run().catch((t) => {
|
|
415
|
+
var e;
|
|
416
|
+
(e = this.log) == null || e.error("[nostr-lora] send queue drain error:", t), this.running = !1;
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
async run() {
|
|
420
|
+
var t;
|
|
421
|
+
for (; this.items.length > 0; ) {
|
|
422
|
+
const e = this.items[0];
|
|
423
|
+
e.status = "sending", this.notifyUpdate();
|
|
424
|
+
try {
|
|
425
|
+
for (let n = 0; n < e.packets.length; n++) {
|
|
426
|
+
const i = e.packets[n];
|
|
427
|
+
await this.sendFn(i), this.emit("packet:send", i, e), n < e.packets.length - 1 && await S(
|
|
428
|
+
this.INTER_PACKET_DELAY + Math.random() * this.INTER_PACKET_JITTER
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
e.meta._onSent && await e.meta._onSent();
|
|
432
|
+
} catch (n) {
|
|
433
|
+
(t = this.log) == null || t.error("[nostr-lora] send queue item error:", n), this.emit(
|
|
434
|
+
"error",
|
|
435
|
+
n instanceof Error ? n : new Error(String(n))
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
this.items.shift(), this.notifyUpdate(), this.items.length > 0 && await S(
|
|
439
|
+
this.INTER_PACKET_DELAY + Math.random() * this.INTER_PACKET_JITTER
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
this.running = !1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
class x extends A {
|
|
446
|
+
constructor(t, e = {}, n) {
|
|
447
|
+
super();
|
|
448
|
+
a(this, "getNodeId");
|
|
449
|
+
a(this, "getSentEvents");
|
|
450
|
+
a(this, "sendEvent");
|
|
451
|
+
a(this, "sendGm");
|
|
452
|
+
a(this, "backoff", /* @__PURE__ */ new Map());
|
|
453
|
+
a(this, "lastResponse", /* @__PURE__ */ new Map());
|
|
454
|
+
a(this, "log");
|
|
455
|
+
a(this, "BACKOFF_BASE");
|
|
456
|
+
a(this, "MAX_SHARE_EVENTS");
|
|
457
|
+
a(this, "JITTER_MIN");
|
|
458
|
+
a(this, "JITTER_MAX");
|
|
459
|
+
this.getNodeId = t.getNodeId, this.getSentEvents = t.getSentEvents, this.sendEvent = t.sendEvent, this.sendGm = t.sendGm, this.log = n ?? null, this.BACKOFF_BASE = e.GM_BACKOFF_BASE ?? d.GM_BACKOFF_BASE, this.MAX_SHARE_EVENTS = e.GM_MAX_SHARE_EVENTS ?? d.GM_MAX_SHARE_EVENTS, this.JITTER_MIN = e.GM_JITTER_MIN ?? d.GM_JITTER_MIN, this.JITTER_MAX = e.GM_JITTER_MAX ?? d.GM_JITTER_MAX;
|
|
460
|
+
}
|
|
461
|
+
async handlePacket(t) {
|
|
462
|
+
var I, w, k, M;
|
|
463
|
+
const e = v.decode(t);
|
|
464
|
+
this.emit("gm:receive", e, t.length);
|
|
465
|
+
const n = this.getNodeId();
|
|
466
|
+
if (!n) return;
|
|
467
|
+
const i = m(e.nodeId), r = m(n);
|
|
468
|
+
if (i === r) return;
|
|
469
|
+
const u = this.backoff.get(i) ?? 1, c = u * this.BACKOFF_BASE, h = this.lastResponse.get(i) ?? 0, l = Date.now();
|
|
470
|
+
if (l - h < c) {
|
|
471
|
+
this.backoff.set(i, Math.min(u * 2, 16)), (I = this.log) == null || I.debug(
|
|
472
|
+
`[nostr-lora] GM from ${i.substring(0, 16)} backed off (${u}x)`
|
|
473
|
+
);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.backoff.set(i, 1), this.lastResponse.set(i, l);
|
|
477
|
+
const T = this.getSentEvents(), _ = [];
|
|
478
|
+
for (const f of T.values())
|
|
479
|
+
if (f.created_at > e.recentSince && f.kind === 1 && (_.push(f), _.length >= this.MAX_SHARE_EVENTS))
|
|
480
|
+
break;
|
|
481
|
+
for (const f of _) {
|
|
482
|
+
await S(this.jitter());
|
|
483
|
+
try {
|
|
484
|
+
await this.sendEvent(f), (w = this.log) == null || w.debug(
|
|
485
|
+
`[nostr-lora] GM auto-share: sent event ${f.id.substring(0, 16)}`
|
|
486
|
+
);
|
|
487
|
+
} catch (C) {
|
|
488
|
+
(k = this.log) == null || k.warn("[nostr-lora] GM auto-share failed:", C);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (Math.random() < 0.5) {
|
|
492
|
+
await S(this.jitter());
|
|
493
|
+
const f = T.size, C = this.oldestTimestamp(T);
|
|
494
|
+
await this.sendGm(n, f, C), (M = this.log) == null || M.debug(
|
|
495
|
+
`[nostr-lora] GM auto-reply sent to ${i.substring(0, 16)}`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
jitter() {
|
|
500
|
+
return this.JITTER_MIN + Math.random() * (this.JITTER_MAX - this.JITTER_MIN);
|
|
501
|
+
}
|
|
502
|
+
oldestTimestamp(t) {
|
|
503
|
+
let e = Math.floor(Date.now() / 1e3);
|
|
504
|
+
for (const n of t.values())
|
|
505
|
+
n.created_at < e && (e = n.created_at);
|
|
506
|
+
return e;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
class j extends A {
|
|
510
|
+
constructor(t, e = {}) {
|
|
511
|
+
super();
|
|
512
|
+
a(this, "connection");
|
|
513
|
+
a(this, "nodeId");
|
|
514
|
+
a(this, "log");
|
|
515
|
+
a(this, "eventTimeout");
|
|
516
|
+
a(this, "dedupTimeout");
|
|
517
|
+
a(this, "initialTtl");
|
|
518
|
+
a(this, "requestInactivity");
|
|
519
|
+
a(this, "requestMaxRetries");
|
|
520
|
+
a(this, "sentChunksTtl");
|
|
521
|
+
a(this, "assembler");
|
|
522
|
+
a(this, "queue");
|
|
523
|
+
a(this, "gm");
|
|
524
|
+
a(this, "recentEventIds", /* @__PURE__ */ new Map());
|
|
525
|
+
a(this, "inactivityTimers", /* @__PURE__ */ new Map());
|
|
526
|
+
a(this, "pendingTimers", /* @__PURE__ */ new Map());
|
|
527
|
+
a(this, "sentEvents", /* @__PURE__ */ new Map());
|
|
528
|
+
a(this, "sentChunks", /* @__PURE__ */ new Map());
|
|
529
|
+
this.connection = t, this.nodeId = e.nodeId ?? null, this.log = e.logger ?? null, this.eventTimeout = e.eventTimeout ?? d.EVENT_TIMEOUT, this.dedupTimeout = e.dedupTimeout ?? d.DEDUP_TIMEOUT, this.initialTtl = e.initialTtl ?? d.INITIAL_TTL, this.requestInactivity = e.requestInactivity ?? d.REQUEST_INACTIVITY, this.requestMaxRetries = e.requestMaxRetries ?? d.REQUEST_MAX_RETRIES, this.sentChunksTtl = e.sentChunksTtl ?? d.SENT_CHUNKS_TTL, this.assembler = new V(), this.queue = new O(
|
|
530
|
+
async (n) => {
|
|
531
|
+
await this.connection.sendRawData(n);
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
INTER_PACKET_DELAY: e.interPacketDelay ?? d.INTER_PACKET_DELAY,
|
|
535
|
+
INTER_PACKET_JITTER: e.interPacketJitter ?? d.INTER_PACKET_JITTER
|
|
536
|
+
},
|
|
537
|
+
this.log
|
|
538
|
+
), this.queue.on("packet:send", (n, i) => {
|
|
539
|
+
i.type === "data" && this.emit("packet:send", b.decode(n), n.length);
|
|
540
|
+
}), this.queue.on(
|
|
541
|
+
"queue:update",
|
|
542
|
+
(n) => this.emit("queue:update", n)
|
|
543
|
+
), this.queue.on("error", (n) => this.emit("error", n)), this.gm = new x(
|
|
544
|
+
{
|
|
545
|
+
getNodeId: () => this.nodeId,
|
|
546
|
+
getSentEvents: () => this.sentEvents,
|
|
547
|
+
sendEvent: (n) => this.sendEvent(n),
|
|
548
|
+
sendGm: (n, i, r) => this.sendGm(n, i, r)
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
GM_BACKOFF_BASE: e.gmBackoffBase ?? d.GM_BACKOFF_BASE,
|
|
552
|
+
GM_MAX_SHARE_EVENTS: e.gmMaxShareEvents ?? d.GM_MAX_SHARE_EVENTS,
|
|
553
|
+
GM_JITTER_MIN: e.gmJitterMin ?? d.GM_JITTER_MIN,
|
|
554
|
+
GM_JITTER_MAX: e.gmJitterMax ?? d.GM_JITTER_MAX
|
|
555
|
+
},
|
|
556
|
+
this.log
|
|
557
|
+
), this.gm.on(
|
|
558
|
+
"gm:receive",
|
|
559
|
+
(n, i) => this.emit("gm:receive", n, i)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
563
|
+
/**
|
|
564
|
+
* Open the connection and wire up data/lifecycle events.
|
|
565
|
+
* This triggers the browser port picker for `SerialConnectionManager`.
|
|
566
|
+
*/
|
|
567
|
+
async begin() {
|
|
568
|
+
this.connection.on("data", (t) => {
|
|
569
|
+
this.handleRawDataPacket(t);
|
|
570
|
+
}), this.connection.on("connect", (t) => {
|
|
571
|
+
this.emit("connect", t);
|
|
572
|
+
}), this.connection.on("disconnect", () => {
|
|
573
|
+
this.emit("disconnect");
|
|
574
|
+
}), this.connection.on("error", (t) => {
|
|
575
|
+
this.emit("error", t);
|
|
576
|
+
}), await this.connection.open();
|
|
577
|
+
}
|
|
578
|
+
/** Close the connection. */
|
|
579
|
+
async end() {
|
|
580
|
+
await this.connection.close();
|
|
581
|
+
}
|
|
582
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
583
|
+
async sendEvent(t) {
|
|
584
|
+
try {
|
|
585
|
+
const { compressed: e } = await J(t), n = p(t.id), i = b.createFromCompressed(
|
|
586
|
+
n,
|
|
587
|
+
t.kind,
|
|
588
|
+
e,
|
|
589
|
+
this.initialTtl
|
|
590
|
+
), r = `EVENT kind:${t.kind} id:${t.id.substring(0, 8)}… (${i.length} pkt)`;
|
|
591
|
+
this.sentChunks.set(t.id, {
|
|
592
|
+
packets: i.slice(),
|
|
593
|
+
sentAt: Date.now()
|
|
594
|
+
}), this.pruneSentChunks(), this.queue.enqueue(i, r, "data", {
|
|
595
|
+
_onSent: async () => {
|
|
596
|
+
this.sentEvents.set(t.id, t), this.emit("event:send", t);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
} catch (e) {
|
|
600
|
+
throw this.emit(
|
|
601
|
+
"error",
|
|
602
|
+
e instanceof Error ? e : new Error(String(e))
|
|
603
|
+
), e;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async sendGm(t, e = 0, n = 0) {
|
|
607
|
+
const i = Math.floor(Date.now() / 1e3), r = v.create(
|
|
608
|
+
t,
|
|
609
|
+
e,
|
|
610
|
+
n,
|
|
611
|
+
i
|
|
612
|
+
), u = `GM node:${m(t).substring(0, 16)}… events:${e}`;
|
|
613
|
+
this.queue.enqueue([r], u, "gm", {
|
|
614
|
+
_onSent: async () => {
|
|
615
|
+
this.emit(
|
|
616
|
+
"packet:send",
|
|
617
|
+
v.decode(r),
|
|
618
|
+
r.length
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Feed a raw radio packet into the transport.
|
|
625
|
+
* Only needed when implementing a custom `ConnectionManager` that doesn't
|
|
626
|
+
* use the event interface — `begin()` wires this automatically otherwise.
|
|
627
|
+
*/
|
|
628
|
+
async handleRawDataPacket(t) {
|
|
629
|
+
var e;
|
|
630
|
+
try {
|
|
631
|
+
const n = new Uint8Array(
|
|
632
|
+
(t.data instanceof ArrayBuffer, t.data)
|
|
633
|
+
);
|
|
634
|
+
switch (this.emit("packet:receive", {
|
|
635
|
+
snr: t.snr || 0,
|
|
636
|
+
rssi: t.rssi || 0,
|
|
637
|
+
size: n.length
|
|
638
|
+
}), F(n)) {
|
|
639
|
+
case g.DATA:
|
|
640
|
+
await this.handleDataPacket(n);
|
|
641
|
+
break;
|
|
642
|
+
case g.REQUEST:
|
|
643
|
+
await this.handleRequestPacket(n);
|
|
644
|
+
break;
|
|
645
|
+
case g.GM:
|
|
646
|
+
await this.handleGmPacket(n);
|
|
647
|
+
break;
|
|
648
|
+
default:
|
|
649
|
+
(e = this.log) == null || e.warn(
|
|
650
|
+
"[nostr-lora] unknown packet type, flags=0x" + n[0].toString(16)
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
} catch (n) {
|
|
654
|
+
this.emit(
|
|
655
|
+
"error",
|
|
656
|
+
n instanceof Error ? n : new Error(String(n))
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/** Update the inter-packet timing on the underlying send queue. */
|
|
661
|
+
setTimingOptions({ delay: t, jitter: e } = {}) {
|
|
662
|
+
t !== void 0 && (this.queue.INTER_PACKET_DELAY = t), e !== void 0 && (this.queue.INTER_PACKET_JITTER = e);
|
|
663
|
+
}
|
|
664
|
+
/** Number of events sent by this node (available for GM announcements). */
|
|
665
|
+
get sentEventCount() {
|
|
666
|
+
return this.sentEvents.size;
|
|
667
|
+
}
|
|
668
|
+
// ── Receive: DATA ─────────────────────────────────────────────────────────
|
|
669
|
+
async handleDataPacket(t) {
|
|
670
|
+
const e = b.decode(t);
|
|
671
|
+
if (this.emit("chunk:receive", e, t.length), e.type === "first") {
|
|
672
|
+
const n = m(e.eventId), i = this.assembler.handleFirst(
|
|
673
|
+
e,
|
|
674
|
+
this.recentEventIds
|
|
675
|
+
);
|
|
676
|
+
i.action === "complete" ? (this.clearInactivityTimer(n), this.clearPendingTimer(
|
|
677
|
+
m(
|
|
678
|
+
e.eventId.slice(0, E.EVENT_ID_PREFIX_LEN)
|
|
679
|
+
)
|
|
680
|
+
), await this.completeSession(i.session)) : i.action === "started" && (this.resetInactivityTimer(n), this.clearPendingTimer(
|
|
681
|
+
m(
|
|
682
|
+
e.eventId.slice(0, E.EVENT_ID_PREFIX_LEN)
|
|
683
|
+
)
|
|
684
|
+
));
|
|
685
|
+
} else {
|
|
686
|
+
const n = this.assembler.handleSubsequent(
|
|
687
|
+
e,
|
|
688
|
+
this.eventTimeout
|
|
689
|
+
);
|
|
690
|
+
n.action === "complete" ? (this.clearInactivityTimer(m(n.session.eventId)), await this.completeSession(n.session)) : n.action === "progress" ? this.resetInactivityTimer(n.eventIdHex) : n.action === "pending" && this.resetPendingTimer(n.prefix);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async completeSession(t) {
|
|
694
|
+
const e = m(t.eventId);
|
|
695
|
+
try {
|
|
696
|
+
const n = await $(
|
|
697
|
+
t.chunks.filter((i) => i !== void 0),
|
|
698
|
+
t.checksum,
|
|
699
|
+
e
|
|
700
|
+
);
|
|
701
|
+
this.recentEventIds.set(e, Date.now()), this.cleanupDeduplication(), this.emit("event:receive", n);
|
|
702
|
+
} catch (n) {
|
|
703
|
+
this.emit(
|
|
704
|
+
"error",
|
|
705
|
+
n instanceof Error ? n : new Error(String(n))
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// ── Receive: REQUEST ──────────────────────────────────────────────────────
|
|
710
|
+
async handleRequestPacket(t) {
|
|
711
|
+
const e = R.decode(t), n = m(e.eventIdPrefix);
|
|
712
|
+
this.emit("request:receive", n, e.missingChunks);
|
|
713
|
+
let i = null, r = null;
|
|
714
|
+
for (const [h, l] of this.sentChunks.entries())
|
|
715
|
+
if (h.startsWith(n)) {
|
|
716
|
+
i = h, r = l;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
if (!r || !i) return;
|
|
720
|
+
const u = e.missingChunks.filter((h) => h < r.packets.length).map((h) => r.packets[h]);
|
|
721
|
+
if (u.length === 0) return;
|
|
722
|
+
const c = `RETX id:${i.substring(0, 8)}… chunks:[${e.missingChunks.join(",")}]`;
|
|
723
|
+
this.queue.enqueue(u, c, "data");
|
|
724
|
+
}
|
|
725
|
+
// ── Receive: GM ───────────────────────────────────────────────────────────
|
|
726
|
+
async handleGmPacket(t) {
|
|
727
|
+
await this.gm.handlePacket(t);
|
|
728
|
+
}
|
|
729
|
+
// ── Inactivity timers: session has chunk 0 ────────────────────────────────
|
|
730
|
+
resetInactivityTimer(t) {
|
|
731
|
+
const e = this.inactivityTimers.get(t), n = (e == null ? void 0 : e.requestCount) ?? 0;
|
|
732
|
+
e != null && e.timer && clearTimeout(e.timer);
|
|
733
|
+
const i = setTimeout(
|
|
734
|
+
() => this.onChunkInactivity(t),
|
|
735
|
+
this.requestInactivity
|
|
736
|
+
);
|
|
737
|
+
this.inactivityTimers.set(t, { timer: i, requestCount: n });
|
|
738
|
+
}
|
|
739
|
+
clearInactivityTimer(t) {
|
|
740
|
+
const e = this.inactivityTimers.get(t);
|
|
741
|
+
e != null && e.timer && clearTimeout(e.timer), this.inactivityTimers.delete(t);
|
|
742
|
+
}
|
|
743
|
+
async onChunkInactivity(t) {
|
|
744
|
+
var c;
|
|
745
|
+
const e = this.inactivityTimers.get(t);
|
|
746
|
+
if (!e) return;
|
|
747
|
+
const n = this.assembler.missingChunks(t);
|
|
748
|
+
if (!(n != null && n.length)) {
|
|
749
|
+
this.inactivityTimers.delete(t);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (e.requestCount++, e.requestCount > this.requestMaxRetries) {
|
|
753
|
+
(c = this.log) == null || c.warn(
|
|
754
|
+
`[nostr-lora] giving up on ${t.substring(0, 16)} after ${this.requestMaxRetries} REQUESTs`
|
|
755
|
+
), this.clearInactivityTimer(t), this.assembler.abandonSession(t);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const r = (this.assembler.getSessionEventId(t) ?? p(t)).slice(0, E.EVENT_ID_PREFIX_LEN), u = `REQ id:${t.substring(0, 8)}… missing:[${n.join(",")}] (#${e.requestCount})`;
|
|
759
|
+
this.queue.enqueue(
|
|
760
|
+
[R.create(r, n)],
|
|
761
|
+
u,
|
|
762
|
+
"request"
|
|
763
|
+
), this.emit(
|
|
764
|
+
"request:send",
|
|
765
|
+
t,
|
|
766
|
+
n,
|
|
767
|
+
e.requestCount
|
|
768
|
+
), e.timer = setTimeout(
|
|
769
|
+
() => this.onChunkInactivity(t),
|
|
770
|
+
this.requestInactivity
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
// ── Inactivity timers: chunk 0 still missing ──────────────────────────────
|
|
774
|
+
resetPendingTimer(t) {
|
|
775
|
+
const e = m(t), n = this.pendingTimers.get(e), i = (n == null ? void 0 : n.requestCount) ?? 0;
|
|
776
|
+
n != null && n.timer && clearTimeout(n.timer);
|
|
777
|
+
const r = setTimeout(
|
|
778
|
+
() => this.onPendingInactivity(e, t),
|
|
779
|
+
this.requestInactivity
|
|
780
|
+
);
|
|
781
|
+
this.pendingTimers.set(e, { timer: r, requestCount: i });
|
|
782
|
+
}
|
|
783
|
+
clearPendingTimer(t) {
|
|
784
|
+
const e = this.pendingTimers.get(t);
|
|
785
|
+
e != null && e.timer && clearTimeout(e.timer), this.pendingTimers.delete(t);
|
|
786
|
+
}
|
|
787
|
+
async onPendingInactivity(t, e) {
|
|
788
|
+
var u;
|
|
789
|
+
const n = this.pendingTimers.get(t);
|
|
790
|
+
if (!n) return;
|
|
791
|
+
const i = this.assembler.missingChunksForPending(t);
|
|
792
|
+
if (!i) {
|
|
793
|
+
this.pendingTimers.delete(t);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (n.requestCount++, n.requestCount > this.requestMaxRetries) {
|
|
797
|
+
(u = this.log) == null || u.warn(
|
|
798
|
+
`[nostr-lora] giving up on pending prefix ${t} after ${this.requestMaxRetries} REQUESTs`
|
|
799
|
+
), this.assembler.abandonPending(t), this.pendingTimers.delete(t);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const r = `REQ prefix:${t.substring(0, 8)}… missing:[${i.join(",")}] (#${n.requestCount})`;
|
|
803
|
+
this.queue.enqueue(
|
|
804
|
+
[R.create(e, i)],
|
|
805
|
+
r,
|
|
806
|
+
"request"
|
|
807
|
+
), this.emit(
|
|
808
|
+
"request:send",
|
|
809
|
+
t,
|
|
810
|
+
i,
|
|
811
|
+
n.requestCount
|
|
812
|
+
), n.timer = setTimeout(
|
|
813
|
+
() => this.onPendingInactivity(t, e),
|
|
814
|
+
this.requestInactivity
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
// ── Maintenance ───────────────────────────────────────────────────────────
|
|
818
|
+
pruneSentChunks() {
|
|
819
|
+
const t = Date.now() - this.sentChunksTtl;
|
|
820
|
+
for (const [e, n] of this.sentChunks.entries())
|
|
821
|
+
n.sentAt < t && this.sentChunks.delete(e);
|
|
822
|
+
}
|
|
823
|
+
cleanupDeduplication() {
|
|
824
|
+
const t = Date.now();
|
|
825
|
+
for (const [e, n] of this.recentEventIds.entries())
|
|
826
|
+
t - n > this.dedupTimeout && this.recentEventIds.delete(e);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
class z extends A {
|
|
830
|
+
constructor(t) {
|
|
831
|
+
super();
|
|
832
|
+
a(this, "connection", null);
|
|
833
|
+
a(this, "label", null);
|
|
834
|
+
a(this, "log");
|
|
835
|
+
this.log = t ?? null;
|
|
836
|
+
}
|
|
837
|
+
// ── ConnectionManager interface ───────────────────────────────────────────
|
|
838
|
+
async open() {
|
|
839
|
+
if (!this.isWebSerialSupported())
|
|
840
|
+
throw new Error(
|
|
841
|
+
"Web Serial is not supported in this browser. Use Chrome or Edge."
|
|
842
|
+
);
|
|
843
|
+
try {
|
|
844
|
+
if (this.connection = await L.open(), !this.connection)
|
|
845
|
+
throw new Error("No serial port selected");
|
|
846
|
+
this.label = this.buildPortLabel(this.connection.serialPort), this.setupHandlers();
|
|
847
|
+
} catch (t) {
|
|
848
|
+
this.connection = null;
|
|
849
|
+
const e = t instanceof Error ? t : new Error(String(t));
|
|
850
|
+
throw this.emit("error", e), e;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async close() {
|
|
854
|
+
var t;
|
|
855
|
+
if (this.connection) {
|
|
856
|
+
try {
|
|
857
|
+
await this.connection.close();
|
|
858
|
+
} catch (e) {
|
|
859
|
+
(t = this.log) == null || t.warn("[nostr-lora] Error closing serial port:", e);
|
|
860
|
+
}
|
|
861
|
+
this.connection = null, this.label = null, this.emit("disconnect");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async sendRawData(t) {
|
|
865
|
+
if (!this.connection)
|
|
866
|
+
throw new Error("Not connected to device");
|
|
867
|
+
try {
|
|
868
|
+
const e = new Uint8Array(0);
|
|
869
|
+
await this.connection.sendCommandSendRawData(e, t);
|
|
870
|
+
} catch (e) {
|
|
871
|
+
const n = e instanceof Error ? e : new Error(String(e));
|
|
872
|
+
throw this.emit("error", n), n;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// ── Extra capabilities ────────────────────────────────────────────────────
|
|
876
|
+
/** Configure the LoRa radio parameters. Must be called after `open()`. */
|
|
877
|
+
async setRadioConfig(t) {
|
|
878
|
+
if (!this.connection) throw new Error("Not connected to device");
|
|
879
|
+
await this.connection.setRadioParams(
|
|
880
|
+
t.frequency,
|
|
881
|
+
t.bandwidth,
|
|
882
|
+
t.spreadingFactor,
|
|
883
|
+
t.codingRate
|
|
884
|
+
), t.power !== void 0 && await this.connection.setTxPower(t.power);
|
|
885
|
+
}
|
|
886
|
+
isConnected() {
|
|
887
|
+
return this.connection !== null;
|
|
888
|
+
}
|
|
889
|
+
isWebSerialSupported() {
|
|
890
|
+
return typeof navigator < "u" && typeof navigator.serial < "u";
|
|
891
|
+
}
|
|
892
|
+
/** The USB VID:PID or "Serial device" label for the connected port. */
|
|
893
|
+
get portLabel() {
|
|
894
|
+
return this.label;
|
|
895
|
+
}
|
|
896
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
897
|
+
buildPortLabel(t) {
|
|
898
|
+
try {
|
|
899
|
+
const e = t.getInfo();
|
|
900
|
+
if (e.usbVendorId !== void 0 && e.usbProductId !== void 0) {
|
|
901
|
+
const n = e.usbVendorId.toString(16).padStart(4, "0").toUpperCase(), i = e.usbProductId.toString(16).padStart(4, "0").toUpperCase();
|
|
902
|
+
return `USB ${n}:${i}`;
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
return "Serial device";
|
|
907
|
+
}
|
|
908
|
+
setupHandlers() {
|
|
909
|
+
this.connection && (this.connection.once(
|
|
910
|
+
P.ResponseCodes.DeviceInfo,
|
|
911
|
+
(t) => {
|
|
912
|
+
const e = {
|
|
913
|
+
name: t.manufacturerModel ?? "Unknown",
|
|
914
|
+
firmwareVersion: t.firmwareVer != null ? String(t.firmwareVer) : "Unknown"
|
|
915
|
+
};
|
|
916
|
+
this.emit("deviceInfo", e);
|
|
917
|
+
}
|
|
918
|
+
), this.connection.on("connected", async () => {
|
|
919
|
+
var t, e;
|
|
920
|
+
try {
|
|
921
|
+
await this.connection.sendCommandAppStart();
|
|
922
|
+
} catch (n) {
|
|
923
|
+
(t = this.log) == null || t.warn("[nostr-lora] AppStart failed:", n);
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
const n = Math.floor(Date.now() / 1e3);
|
|
927
|
+
await this.connection.sendCommandSetDeviceTime(n);
|
|
928
|
+
} catch (n) {
|
|
929
|
+
(e = this.log) == null || e.warn("[nostr-lora] Time sync failed:", n);
|
|
930
|
+
}
|
|
931
|
+
this.emit("connect", this.label);
|
|
932
|
+
}), this.connection.on("disconnected", () => {
|
|
933
|
+
var t;
|
|
934
|
+
(t = this.log) == null || t.log("[nostr-lora] Serial device disconnected"), this.connection = null, this.label = null, this.emit("disconnect");
|
|
935
|
+
}), this.connection.on(
|
|
936
|
+
P.PushCodes.RawData,
|
|
937
|
+
(t) => {
|
|
938
|
+
this.emit("data", {
|
|
939
|
+
data: t.payload,
|
|
940
|
+
snr: t.lastSnr,
|
|
941
|
+
rssi: t.lastRssi
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
export {
|
|
948
|
+
V as A,
|
|
949
|
+
d as D,
|
|
950
|
+
x as G,
|
|
951
|
+
j as L,
|
|
952
|
+
O as S,
|
|
953
|
+
z as a
|
|
954
|
+
};
|
|
955
|
+
//# sourceMappingURL=serial-connection.js.map
|