@decentnetwork/lan 0.1.297 → 0.1.298

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.
@@ -10,6 +10,10 @@ export interface PacedForwarderOptions {
10
10
  maxFlowBytes?: number;
11
11
  /** Max queued bytes across a destination before shed (backstop). */
12
12
  maxDestBytes?: number;
13
+ /** Longest a packet may sit queued before it is dropped instead of sent.
14
+ * Bounds LATENCY, which the byte caps cannot: a low-rate flow never fills
15
+ * 256 KB and so was never shed, however long the path had been stalled. */
16
+ maxSojournMs?: number;
13
17
  /** Token-bucket burst ceiling as seconds-worth of rate. */
14
18
  burstSeconds?: number;
15
19
  now?: () => number;
@@ -29,6 +29,10 @@
29
29
  const DEFAULT_RATE_BPS = 12_500_000; // 100 Mbps — effectively uncapped until measured
30
30
  const DEFAULT_MAX_FLOW_BYTES = 256 * 1024;
31
31
  const DEFAULT_MAX_DEST_BYTES = 4 * 1024 * 1024;
32
+ // Generous next to any real mesh RTT (relay paths run in the hundreds of ms),
33
+ // decisive against a stall measured in minutes. A packet older than this has
34
+ // outlived whatever asked for it.
35
+ const DEFAULT_MAX_SOJOURN_MS = 3_000;
32
36
  const DEFAULT_BURST_SECONDS = 0.05; // 50ms of rate as burst — smooths without adding latency
33
37
  const MIN_RATE_BPS = 32 * 1024; // never pace below 256 kbps
34
38
  // LOSS-DRIVEN pacing. The old rule paced to `delivered × headroom`, but the
@@ -98,6 +102,7 @@ export class PacedForwarder {
98
102
  send: opts.send,
99
103
  defaultRateBps: opts.defaultRateBps ?? DEFAULT_RATE_BPS,
100
104
  maxFlowBytes: opts.maxFlowBytes ?? DEFAULT_MAX_FLOW_BYTES,
105
+ maxSojournMs: opts.maxSojournMs ?? DEFAULT_MAX_SOJOURN_MS,
101
106
  maxDestBytes: opts.maxDestBytes ?? DEFAULT_MAX_DEST_BYTES,
102
107
  burstSeconds: opts.burstSeconds ?? DEFAULT_BURST_SECONDS,
103
108
  now: opts.now ?? Date.now,
@@ -162,7 +167,7 @@ export class PacedForwarder {
162
167
  }
163
168
  let f = d.flows.get(flowKey);
164
169
  if (!f) {
165
- f = { key: flowKey, packets: [], bytes: 0 };
170
+ f = { key: flowKey, packets: [], enqueuedAt: [], bytes: 0 };
166
171
  d.flows.set(flowKey, f);
167
172
  d.order.push(flowKey);
168
173
  }
@@ -170,6 +175,7 @@ export class PacedForwarder {
170
175
  // congestion to just that flow's TCP without starving the others.
171
176
  if (f.bytes + packet.length > this.#opts.maxFlowBytes) {
172
177
  const oldest = f.packets.shift();
178
+ f.enqueuedAt.shift();
173
179
  if (oldest) {
174
180
  f.bytes -= oldest.length;
175
181
  d.totalBytes -= oldest.length;
@@ -177,6 +183,7 @@ export class PacedForwarder {
177
183
  }
178
184
  }
179
185
  f.packets.push(packet);
186
+ f.enqueuedAt.push(this.#opts.now());
180
187
  f.bytes += packet.length;
181
188
  d.totalBytes += packet.length;
182
189
  this.#drain(dstIp);
@@ -205,6 +212,19 @@ export class PacedForwarder {
205
212
  d.draining = true;
206
213
  this.#pump(dstIp, d);
207
214
  }
215
+ /** Discard packets that have waited past maxSojournMs. Returns true when the
216
+ * flow emptied, so the caller can skip it this round. */
217
+ #dropStale(d, f) {
218
+ const cutoff = this.#opts.now() - this.#opts.maxSojournMs;
219
+ while (f.packets.length > 0 && f.enqueuedAt[0] < cutoff) {
220
+ const stale = f.packets.shift();
221
+ f.enqueuedAt.shift();
222
+ f.bytes -= stale.length;
223
+ d.totalBytes -= stale.length;
224
+ this.#dropped++;
225
+ }
226
+ return f.packets.length === 0;
227
+ }
208
228
  #pump(dstIp, d) {
209
229
  d.draining = false;
210
230
  const now = this.#opts.now();
@@ -237,8 +257,29 @@ export class PacedForwarder {
237
257
  this.#opts.schedule(() => this.#pump(dstIp, d), ms);
238
258
  return;
239
259
  }
260
+ // Drop what has waited too long, BEFORE spending a token on it.
261
+ //
262
+ // The queue was bounded by bytes only — 256 KB a flow — which a
263
+ // high-rate flow trips in a moment and a low-rate one never trips at
264
+ // all. Measured 2026-08-31 against a peer on a Thai mobile link: ICMP
265
+ // echo replies arriving with RTTs of 180 to 250 SECONDS, each one
266
+ // exactly 1000ms lower than the last, which is the shape of a backlog
267
+ // flushing after the path came back. At 84 bytes a ping and one a
268
+ // second, filling 256 KB would take fifty minutes; the packets simply
269
+ // sat there. Delivering a four-minute-old packet is worse than dropping
270
+ // it — the sender gave up long ago, and TCP above reads it as a wildly
271
+ // inflated RTT.
272
+ //
273
+ // Bound the SOJOURN TIME, which is what CoDel is about and what a byte
274
+ // cap cannot express.
275
+ if (this.#dropStale(d, f)) {
276
+ d.order.splice(d.cursor % d.order.length, 1);
277
+ d.flows.delete(f.key);
278
+ continue;
279
+ }
240
280
  // Emit one packet from this flow, then advance the cursor (fairness).
241
281
  const pkt = f.packets.shift();
282
+ f.enqueuedAt.shift();
242
283
  f.bytes -= size;
243
284
  d.totalBytes -= size;
244
285
  d.tokens -= size;
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.297";
1
+ window.__DK_UI_VERSION="0.1.298";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.297",
3
+ "version": "0.1.298",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",