@peerbit/pubsub 5.2.11 → 5.3.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.
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +3 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +90 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +712 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +1591 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +297 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +73 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +183 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +1 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts +101 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim-lib.js +540 -38
- package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
- package/dist/benchmark/fanout-tree-sim.d.ts +0 -9
- package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim.js +369 -28
- package/dist/benchmark/fanout-tree-sim.js.map +1 -1
- package/dist/benchmark/index.js +20 -0
- package/dist/benchmark/index.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts +4 -0
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.js +58 -3
- package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim.js +18 -0
- package/dist/benchmark/pubsub-topic-sim.js.map +1 -1
- package/dist/src/debounced-set.d.ts +1 -1
- package/dist/src/debounced-set.d.ts.map +1 -1
- package/dist/src/debounced-set.js +5 -1
- package/dist/src/debounced-set.js.map +1 -1
- package/dist/src/fanout-tree-codec.d.ts +330 -0
- package/dist/src/fanout-tree-codec.d.ts.map +1 -0
- package/dist/src/fanout-tree-codec.js +1209 -0
- package/dist/src/fanout-tree-codec.js.map +1 -0
- package/dist/src/fanout-tree-parent-upgrade.d.ts +117 -0
- package/dist/src/fanout-tree-parent-upgrade.d.ts.map +1 -0
- package/dist/src/fanout-tree-parent-upgrade.js +135 -0
- package/dist/src/fanout-tree-parent-upgrade.js.map +1 -0
- package/dist/src/fanout-tree.d.ts +245 -73
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +2290 -1057
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +58 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +316 -45
- package/dist/src/index.js.map +1 -1
- package/dist/src/topic-root-control-plane.d.ts +13 -0
- package/dist/src/topic-root-control-plane.d.ts.map +1 -1
- package/dist/src/topic-root-control-plane.js +45 -0
- package/dist/src/topic-root-control-plane.js.map +1 -1
- package/package.json +9 -12
- package/src/debounced-set.ts +5 -0
- package/src/fanout-tree-codec.ts +1580 -0
- package/src/fanout-tree-parent-upgrade.ts +353 -0
- package/src/fanout-tree.ts +4558 -2230
- package/src/index.ts +425 -59
- package/src/topic-root-control-plane.ts +48 -0
package/dist/src/fanout-tree.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { getPublicKeyFromPeerId, ready, sha256Sync, toBase64 } from "@peerbit/crypto";
|
|
2
1
|
import { multiaddr } from "@multiformats/multiaddr";
|
|
2
|
+
import { getPublicKeyFromPeerId, ready, sha256Sync, toBase64, } from "@peerbit/crypto";
|
|
3
3
|
import { DirectStream, dontThrowIfDeliveryError, } from "@peerbit/stream";
|
|
4
4
|
import { AnyWhere, DataMessage, } from "@peerbit/stream-interface";
|
|
5
5
|
import { AbortError, TimeoutError, delay } from "@peerbit/time";
|
|
6
6
|
import { anySignal } from "any-signal";
|
|
7
7
|
import { Uint8ArrayList } from "uint8arraylist";
|
|
8
8
|
import { normalizeUint8Array, toUint8Array } from "./bytes.js";
|
|
9
|
+
import { JOIN_REJECT_LOW_BID, JOIN_REJECT_NO_CAPACITY, JOIN_REJECT_NOT_ATTACHED, JOIN_REJECT_REDIRECT_ADDR_MAX, JOIN_REJECT_REDIRECT_MAX, MSG_DATA, MSG_END, MSG_FETCH_REQ, MSG_IHAVE, MSG_JOIN_ACCEPT, MSG_JOIN_REJECT, MSG_JOIN_REQ, MSG_KICK, MSG_LEAVE, MSG_PARENT_PROBE_REPLY, MSG_PARENT_PROBE_REQ, MSG_PROVIDER_ANNOUNCE, MSG_PROVIDER_NOTIFY, MSG_PROVIDER_QUERY, MSG_PROVIDER_REPLY, MSG_PROVIDER_SUBSCRIBE, MSG_PROVIDER_UNSUBSCRIBE, MSG_PUBLISH_PROXY, MSG_REPAIR_REQ, MSG_ROUTE_QUERY, MSG_ROUTE_REPLY, MSG_TRACKER_ANNOUNCE, MSG_TRACKER_FEEDBACK, MSG_TRACKER_QUERY, MSG_TRACKER_REPLY, MSG_UNICAST, MSG_UNICAST_ACK, PARENT_PROBE_FLAG_ACCEPTING, PARENT_PROBE_FLAG_OVERLOADED, PARENT_PROBE_FLAG_REPAIRING, PARENT_PROBE_FLAG_ROOTED, TRACKER_FEEDBACK_DIAL_FAILED, TRACKER_FEEDBACK_JOINED, TRACKER_FEEDBACK_JOIN_REJECT, TRACKER_FEEDBACK_JOIN_TIMEOUT, UNICAST_ACK_DEFAULT_TIMEOUT_MS, clampU16, readU32BE, tsFanoutWireCodec, writeU32BE, } from "./fanout-tree-codec.js";
|
|
10
|
+
import { evaluateParentUpgradeGate, normalizeParentUpgradePolicy, recordParentUpgradeSkip, } from "./fanout-tree-parent-upgrade.js";
|
|
9
11
|
import { TopicRootControlPlane } from "./topic-root-control-plane.js";
|
|
10
12
|
const CONTROL_PRIORITY = 10;
|
|
11
13
|
const DATA_PRIORITY = 1;
|
|
12
14
|
const ROUTE_PROXY_TIMEOUT_MS = 10_000;
|
|
15
|
+
// Backstop bounds for the route-proxy machinery under query storms (#983):
|
|
16
|
+
// each pending entry retains a live timer closure and a Set of child hashes.
|
|
17
|
+
const ROUTE_PROXY_MAX_PENDING = 2_048;
|
|
18
|
+
const ROUTE_PROXY_MAX_WAITERS = 512;
|
|
13
19
|
const ROUTE_CACHE_MAX_ENTRIES_HARD_CAP = 100_000;
|
|
14
20
|
const PEER_HINT_MAX_ENTRIES_HARD_CAP = 100_000;
|
|
15
21
|
// Best-effort address cache for join candidates learned via trackers/redirects.
|
|
@@ -35,8 +41,15 @@ const OVERLOAD_KICK_MAX_PER_EVENT = 4;
|
|
|
35
41
|
const DATA_WRITE_FAIL_KICK_STREAK_THRESHOLD = 3;
|
|
36
42
|
const DATA_WRITE_FAIL_KICK_COOLDOWN_MS = 2_000;
|
|
37
43
|
const DATA_WRITE_FAIL_KICK_MAX_PER_EVENT = 4;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
44
|
+
const PARENT_REPAIR_DEAD_STREAK_THRESHOLD = 16;
|
|
45
|
+
const PARENT_REPAIR_DEAD_MIN_LIVENESS_MS = 15_000;
|
|
46
|
+
const REPAIR_RETRY_MIN_MS = 1_000;
|
|
47
|
+
const REPAIR_RETRY_INTERVAL_FACTOR = 5;
|
|
48
|
+
// A stream can remain locally readable/writable while silently dropping every
|
|
49
|
+
// control frame. Repeated unanswered JOIN requests are end-to-end evidence that
|
|
50
|
+
// the peer path needs to be rebuilt.
|
|
51
|
+
const JOIN_TIMEOUT_RESET_STREAK_THRESHOLD = 3;
|
|
52
|
+
const JOIN_TIMEOUT_RESET_COOLDOWN_MS = 10_000;
|
|
40
53
|
const JOIN_REJECT_REDIRECT_QUEUE_MAX = 64;
|
|
41
54
|
// When a relay loses its parent, pause before trying to rejoin so its children can
|
|
42
55
|
// attach elsewhere (helps avoid disconnected components stabilizing).
|
|
@@ -45,622 +58,16 @@ const JOIN_REJECT_REDIRECT_QUEUE_MAX = 64;
|
|
|
45
58
|
// This gives kicked children a window to reattach elsewhere (helps avoid stable
|
|
46
59
|
// disconnected components). Leaf nodes (no children) rejoin immediately.
|
|
47
60
|
const RELAY_REJOIN_COOLDOWN_MS = 3_000;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const MSG_UNICAST = 12;
|
|
55
|
-
const MSG_ROUTE_QUERY = 13;
|
|
56
|
-
const MSG_ROUTE_REPLY = 14;
|
|
57
|
-
const MSG_PUBLISH_PROXY = 15;
|
|
58
|
-
const MSG_LEAVE = 16;
|
|
59
|
-
const MSG_UNICAST_ACK = 17;
|
|
60
|
-
const MSG_REPAIR_REQ = 20;
|
|
61
|
-
const MSG_FETCH_REQ = 21;
|
|
62
|
-
const MSG_IHAVE = 22;
|
|
63
|
-
const MSG_TRACKER_ANNOUNCE = 30;
|
|
64
|
-
const MSG_TRACKER_QUERY = 31;
|
|
65
|
-
const MSG_TRACKER_REPLY = 32;
|
|
66
|
-
const MSG_TRACKER_FEEDBACK = 33;
|
|
67
|
-
const MSG_PROVIDER_ANNOUNCE = 34;
|
|
68
|
-
const MSG_PROVIDER_QUERY = 35;
|
|
69
|
-
const MSG_PROVIDER_REPLY = 36;
|
|
70
|
-
const MSG_PROVIDER_SUBSCRIBE = 37;
|
|
71
|
-
const MSG_PROVIDER_UNSUBSCRIBE = 38;
|
|
72
|
-
const MSG_PROVIDER_NOTIFY = 39;
|
|
73
|
-
const JOIN_REJECT_NOT_ATTACHED = 1;
|
|
74
|
-
const JOIN_REJECT_NO_CAPACITY = 2;
|
|
75
|
-
const JOIN_REJECT_LOW_BID = 3;
|
|
76
|
-
const TRACKER_FEEDBACK_JOINED = 1;
|
|
77
|
-
const TRACKER_FEEDBACK_DIAL_FAILED = 2;
|
|
78
|
-
const TRACKER_FEEDBACK_JOIN_TIMEOUT = 3;
|
|
79
|
-
const TRACKER_FEEDBACK_JOIN_REJECT = 4;
|
|
80
|
-
const textEncoder = new TextEncoder();
|
|
81
|
-
const textDecoder = new TextDecoder();
|
|
82
|
-
const writeU32BE = (buf, offset, value) => {
|
|
83
|
-
buf[offset + 0] = (value >>> 24) & 0xff;
|
|
84
|
-
buf[offset + 1] = (value >>> 16) & 0xff;
|
|
85
|
-
buf[offset + 2] = (value >>> 8) & 0xff;
|
|
86
|
-
buf[offset + 3] = value & 0xff;
|
|
87
|
-
};
|
|
88
|
-
const readU32BE = (buf, offset) => ((buf[offset + 0] << 24) |
|
|
89
|
-
(buf[offset + 1] << 16) |
|
|
90
|
-
(buf[offset + 2] << 8) |
|
|
91
|
-
buf[offset + 3]) >>> 0;
|
|
92
|
-
const writeU64BE = (buf, offset, value) => {
|
|
93
|
-
let v = value & 0xffffffffffffffffn;
|
|
94
|
-
for (let i = 7; i >= 0; i--) {
|
|
95
|
-
buf[offset + i] = Number(v & 0xffn);
|
|
96
|
-
v >>= 8n;
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
const readU64BE = (buf, offset) => {
|
|
100
|
-
let v = 0n;
|
|
101
|
-
for (let i = 0; i < 8; i++) {
|
|
102
|
-
v = (v << 8n) | BigInt(buf[offset + i]);
|
|
103
|
-
}
|
|
104
|
-
return v;
|
|
105
|
-
};
|
|
106
|
-
const writeU16BE = (buf, offset, value) => {
|
|
107
|
-
buf[offset + 0] = (value >>> 8) & 0xff;
|
|
108
|
-
buf[offset + 1] = value & 0xff;
|
|
109
|
-
};
|
|
110
|
-
const readU16BE = (buf, offset) => ((buf[offset + 0] << 8) | buf[offset + 1]) >>> 0;
|
|
111
|
-
const encodeJoinReq = (channelKey, reqId, bidPerByte) => {
|
|
112
|
-
const buf = new Uint8Array(1 + 32 + 4 + 4);
|
|
113
|
-
buf[0] = MSG_JOIN_REQ;
|
|
114
|
-
buf.set(channelKey, 1);
|
|
115
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
116
|
-
writeU32BE(buf, 37, bidPerByte >>> 0);
|
|
117
|
-
return buf;
|
|
118
|
-
};
|
|
119
|
-
const MAX_ROUTE_HOPS = 32;
|
|
120
|
-
const UNICAST_FLAG_ACK = 1;
|
|
121
|
-
const UNICAST_ACK_DEFAULT_TIMEOUT_MS = 30_000;
|
|
122
|
-
const encodeJoinAccept = (channelKey, reqId, level, parentRouteFromRoot) => {
|
|
123
|
-
const routeBytes = [];
|
|
124
|
-
let bytes = 1 + 32 + 4 + 2 + 1;
|
|
125
|
-
let count = 0;
|
|
126
|
-
for (const hop of parentRouteFromRoot ?? []) {
|
|
127
|
-
if (count >= MAX_ROUTE_HOPS)
|
|
128
|
-
break;
|
|
129
|
-
if (typeof hop !== "string")
|
|
130
|
-
continue;
|
|
131
|
-
const hb = textEncoder.encode(hop);
|
|
132
|
-
if (hb.length === 0 || hb.length > 255)
|
|
133
|
-
continue;
|
|
134
|
-
routeBytes.push(hb);
|
|
135
|
-
bytes += 1 + hb.length;
|
|
136
|
-
count += 1;
|
|
137
|
-
}
|
|
138
|
-
const buf = new Uint8Array(bytes);
|
|
139
|
-
buf[0] = MSG_JOIN_ACCEPT;
|
|
140
|
-
buf.set(channelKey, 1);
|
|
141
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
142
|
-
writeU16BE(buf, 37, level & 0xffff);
|
|
143
|
-
buf[39] = count & 0xff;
|
|
144
|
-
let offset = 40;
|
|
145
|
-
for (const hb of routeBytes) {
|
|
146
|
-
buf[offset++] = hb.length & 0xff;
|
|
147
|
-
buf.set(hb, offset);
|
|
148
|
-
offset += hb.length;
|
|
149
|
-
}
|
|
150
|
-
return buf;
|
|
151
|
-
};
|
|
152
|
-
const encodeJoinReject = (channelKey, reqId, reason, redirects) => {
|
|
153
|
-
const encoded = [];
|
|
154
|
-
for (const r of redirects ?? []) {
|
|
155
|
-
if (!r?.hash)
|
|
156
|
-
continue;
|
|
157
|
-
const hashBytes = textEncoder.encode(r.hash);
|
|
158
|
-
if (hashBytes.length === 0 || hashBytes.length > 255)
|
|
159
|
-
continue;
|
|
160
|
-
const addrs = (r.addrs ?? [])
|
|
161
|
-
.filter((a) => a instanceof Uint8Array)
|
|
162
|
-
.filter((a) => a.length > 0 && a.length <= 0xffff)
|
|
163
|
-
.slice(0, JOIN_REJECT_REDIRECT_ADDR_MAX);
|
|
164
|
-
if (addrs.length === 0)
|
|
165
|
-
continue;
|
|
166
|
-
encoded.push({ hashBytes, addrs });
|
|
167
|
-
if (encoded.length >= JOIN_REJECT_REDIRECT_MAX)
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
if (encoded.length === 0) {
|
|
171
|
-
const buf = new Uint8Array(1 + 32 + 4 + 1);
|
|
172
|
-
buf[0] = MSG_JOIN_REJECT;
|
|
173
|
-
buf.set(channelKey, 1);
|
|
174
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
175
|
-
buf[37] = reason & 0xff;
|
|
176
|
-
return buf;
|
|
177
|
-
}
|
|
178
|
-
let bytes = 1 + 32 + 4 + 1 + 1;
|
|
179
|
-
for (const e of encoded) {
|
|
180
|
-
bytes += 1 + e.hashBytes.length + 1;
|
|
181
|
-
for (const a of e.addrs)
|
|
182
|
-
bytes += 2 + a.length;
|
|
183
|
-
}
|
|
184
|
-
const buf = new Uint8Array(bytes);
|
|
185
|
-
buf[0] = MSG_JOIN_REJECT;
|
|
186
|
-
buf.set(channelKey, 1);
|
|
187
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
188
|
-
buf[37] = reason & 0xff;
|
|
189
|
-
buf[38] = Math.max(0, Math.min(255, encoded.length)) & 0xff;
|
|
190
|
-
let offset = 39;
|
|
191
|
-
for (const e of encoded) {
|
|
192
|
-
buf[offset++] = e.hashBytes.length & 0xff;
|
|
193
|
-
buf.set(e.hashBytes, offset);
|
|
194
|
-
offset += e.hashBytes.length;
|
|
195
|
-
buf[offset++] = Math.max(0, Math.min(255, e.addrs.length)) & 0xff;
|
|
196
|
-
for (const a of e.addrs) {
|
|
197
|
-
writeU16BE(buf, offset, a.length);
|
|
198
|
-
offset += 2;
|
|
199
|
-
buf.set(a, offset);
|
|
200
|
-
offset += a.length;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return buf;
|
|
204
|
-
};
|
|
205
|
-
const encodeKick = (channelKey) => {
|
|
206
|
-
const buf = new Uint8Array(1 + 32);
|
|
207
|
-
buf[0] = MSG_KICK;
|
|
208
|
-
buf.set(channelKey, 1);
|
|
209
|
-
return buf;
|
|
210
|
-
};
|
|
211
|
-
const encodeEnd = (channelKey, lastSeqExclusive) => {
|
|
212
|
-
const buf = new Uint8Array(1 + 32 + 4);
|
|
213
|
-
buf[0] = MSG_END;
|
|
214
|
-
buf.set(channelKey, 1);
|
|
215
|
-
writeU32BE(buf, 33, lastSeqExclusive >>> 0);
|
|
216
|
-
return buf;
|
|
217
|
-
};
|
|
218
|
-
const encodeRepairReq = (channelKey, reqId, missingSeqs) => {
|
|
219
|
-
const count = Math.max(0, Math.min(255, missingSeqs.length));
|
|
220
|
-
const buf = new Uint8Array(1 + 32 + 4 + 1 + count * 4);
|
|
221
|
-
buf[0] = MSG_REPAIR_REQ;
|
|
222
|
-
buf.set(channelKey, 1);
|
|
223
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
224
|
-
buf[37] = count & 0xff;
|
|
225
|
-
for (let i = 0; i < count; i++) {
|
|
226
|
-
writeU32BE(buf, 38 + i * 4, missingSeqs[i] >>> 0);
|
|
227
|
-
}
|
|
228
|
-
return buf;
|
|
229
|
-
};
|
|
230
|
-
const encodeFetchReq = (channelKey, reqId, missingSeqs) => {
|
|
231
|
-
const count = Math.max(0, Math.min(255, missingSeqs.length));
|
|
232
|
-
const buf = new Uint8Array(1 + 32 + 4 + 1 + count * 4);
|
|
233
|
-
buf[0] = MSG_FETCH_REQ;
|
|
234
|
-
buf.set(channelKey, 1);
|
|
235
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
236
|
-
buf[37] = count & 0xff;
|
|
237
|
-
for (let i = 0; i < count; i++) {
|
|
238
|
-
writeU32BE(buf, 38 + i * 4, missingSeqs[i] >>> 0);
|
|
239
|
-
}
|
|
240
|
-
return buf;
|
|
241
|
-
};
|
|
242
|
-
const encodeIHave = (channelKey, haveFrom, haveToExclusive) => {
|
|
243
|
-
const buf = new Uint8Array(1 + 32 + 4 + 4);
|
|
244
|
-
buf[0] = MSG_IHAVE;
|
|
245
|
-
buf.set(channelKey, 1);
|
|
246
|
-
writeU32BE(buf, 33, haveFrom >>> 0);
|
|
247
|
-
writeU32BE(buf, 37, haveToExclusive >>> 0);
|
|
248
|
-
return buf;
|
|
249
|
-
};
|
|
250
|
-
const encodeData = (payload) => {
|
|
251
|
-
const buf = new Uint8Array(1 + payload.length);
|
|
252
|
-
buf[0] = MSG_DATA;
|
|
253
|
-
buf.set(payload, 1);
|
|
254
|
-
return buf;
|
|
255
|
-
};
|
|
256
|
-
const encodePublishProxy = (channelKey, payload) => {
|
|
257
|
-
const buf = new Uint8Array(1 + 32 + payload.length);
|
|
258
|
-
buf[0] = MSG_PUBLISH_PROXY;
|
|
259
|
-
buf.set(channelKey, 1);
|
|
260
|
-
buf.set(payload, 33);
|
|
261
|
-
return buf;
|
|
262
|
-
};
|
|
263
|
-
const encodeLeave = (channelKey) => {
|
|
264
|
-
const buf = new Uint8Array(1 + 32);
|
|
265
|
-
buf[0] = MSG_LEAVE;
|
|
266
|
-
buf.set(channelKey, 1);
|
|
267
|
-
return buf;
|
|
268
|
-
};
|
|
269
|
-
const encodeUnicast = (channelKey, route, payload, options) => {
|
|
270
|
-
const wantsAck = options?.ackToken != null;
|
|
271
|
-
const flags = wantsAck ? UNICAST_FLAG_ACK : 0;
|
|
272
|
-
const toRouteBytes = [];
|
|
273
|
-
let bytes = 1 + 32 + 1 + (wantsAck ? 8 : 0) + 1;
|
|
274
|
-
let toCount = 0;
|
|
275
|
-
for (const hop of route ?? []) {
|
|
276
|
-
if (toCount >= MAX_ROUTE_HOPS)
|
|
277
|
-
break;
|
|
278
|
-
if (typeof hop !== "string")
|
|
279
|
-
continue;
|
|
280
|
-
const hb = textEncoder.encode(hop);
|
|
281
|
-
if (hb.length === 0 || hb.length > 255)
|
|
282
|
-
continue;
|
|
283
|
-
toRouteBytes.push(hb);
|
|
284
|
-
bytes += 1 + hb.length;
|
|
285
|
-
toCount += 1;
|
|
286
|
-
}
|
|
287
|
-
const replyRouteBytes = [];
|
|
288
|
-
let replyCount = 0;
|
|
289
|
-
if (wantsAck) {
|
|
290
|
-
bytes += 1; // replyRouteCount
|
|
291
|
-
for (const hop of options?.replyRoute ?? []) {
|
|
292
|
-
if (replyCount >= MAX_ROUTE_HOPS)
|
|
293
|
-
break;
|
|
294
|
-
if (typeof hop !== "string")
|
|
295
|
-
continue;
|
|
296
|
-
const hb = textEncoder.encode(hop);
|
|
297
|
-
if (hb.length === 0 || hb.length > 255)
|
|
298
|
-
continue;
|
|
299
|
-
replyRouteBytes.push(hb);
|
|
300
|
-
bytes += 1 + hb.length;
|
|
301
|
-
replyCount += 1;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
bytes += payload.byteLength;
|
|
305
|
-
const buf = new Uint8Array(bytes);
|
|
306
|
-
buf[0] = MSG_UNICAST;
|
|
307
|
-
buf.set(channelKey, 1);
|
|
308
|
-
buf[33] = flags & 0xff;
|
|
309
|
-
let offset = 34;
|
|
310
|
-
if (wantsAck) {
|
|
311
|
-
writeU64BE(buf, offset, options.ackToken);
|
|
312
|
-
offset += 8;
|
|
313
|
-
}
|
|
314
|
-
buf[offset++] = toCount & 0xff;
|
|
315
|
-
for (const hb of toRouteBytes) {
|
|
316
|
-
buf[offset++] = hb.length & 0xff;
|
|
317
|
-
buf.set(hb, offset);
|
|
318
|
-
offset += hb.length;
|
|
319
|
-
}
|
|
320
|
-
if (wantsAck) {
|
|
321
|
-
buf[offset++] = replyCount & 0xff;
|
|
322
|
-
for (const hb of replyRouteBytes) {
|
|
323
|
-
buf[offset++] = hb.length & 0xff;
|
|
324
|
-
buf.set(hb, offset);
|
|
325
|
-
offset += hb.length;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
buf.set(payload, offset);
|
|
329
|
-
return buf;
|
|
330
|
-
};
|
|
331
|
-
const encodeUnicastAck = (channelKey, ackToken, route) => {
|
|
332
|
-
const routeBytes = [];
|
|
333
|
-
let bytes = 1 + 32 + 8 + 1;
|
|
334
|
-
let count = 0;
|
|
335
|
-
for (const hop of route ?? []) {
|
|
336
|
-
if (count >= MAX_ROUTE_HOPS)
|
|
337
|
-
break;
|
|
338
|
-
if (typeof hop !== "string")
|
|
339
|
-
continue;
|
|
340
|
-
const hb = textEncoder.encode(hop);
|
|
341
|
-
if (hb.length === 0 || hb.length > 255)
|
|
342
|
-
continue;
|
|
343
|
-
routeBytes.push(hb);
|
|
344
|
-
bytes += 1 + hb.length;
|
|
345
|
-
count += 1;
|
|
346
|
-
}
|
|
347
|
-
const buf = new Uint8Array(bytes);
|
|
348
|
-
buf[0] = MSG_UNICAST_ACK;
|
|
349
|
-
buf.set(channelKey, 1);
|
|
350
|
-
writeU64BE(buf, 33, ackToken);
|
|
351
|
-
buf[41] = count & 0xff;
|
|
352
|
-
let offset = 42;
|
|
353
|
-
for (const hb of routeBytes) {
|
|
354
|
-
buf[offset++] = hb.length & 0xff;
|
|
355
|
-
buf.set(hb, offset);
|
|
356
|
-
offset += hb.length;
|
|
357
|
-
}
|
|
358
|
-
return buf;
|
|
359
|
-
};
|
|
360
|
-
const encodeRouteQuery = (channelKey, reqId, targetHash) => {
|
|
361
|
-
const targetBytes = textEncoder.encode(targetHash);
|
|
362
|
-
const targetLen = Math.max(0, Math.min(255, targetBytes.length));
|
|
363
|
-
const buf = new Uint8Array(1 + 32 + 4 + 1 + targetLen);
|
|
364
|
-
buf[0] = MSG_ROUTE_QUERY;
|
|
365
|
-
buf.set(channelKey, 1);
|
|
366
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
367
|
-
buf[37] = targetLen & 0xff;
|
|
368
|
-
buf.set(targetBytes.subarray(0, targetLen), 38);
|
|
369
|
-
return buf;
|
|
370
|
-
};
|
|
371
|
-
const encodeRouteReply = (channelKey, reqId, route) => {
|
|
372
|
-
const routeBytes = [];
|
|
373
|
-
let bytes = 1 + 32 + 4 + 1;
|
|
374
|
-
let count = 0;
|
|
375
|
-
for (const hop of route ?? []) {
|
|
376
|
-
if (count >= MAX_ROUTE_HOPS)
|
|
377
|
-
break;
|
|
378
|
-
if (typeof hop !== "string")
|
|
379
|
-
continue;
|
|
380
|
-
const hb = textEncoder.encode(hop);
|
|
381
|
-
if (hb.length === 0 || hb.length > 255)
|
|
382
|
-
continue;
|
|
383
|
-
routeBytes.push(hb);
|
|
384
|
-
bytes += 1 + hb.length;
|
|
385
|
-
count += 1;
|
|
386
|
-
}
|
|
387
|
-
const buf = new Uint8Array(bytes);
|
|
388
|
-
buf[0] = MSG_ROUTE_REPLY;
|
|
389
|
-
buf.set(channelKey, 1);
|
|
390
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
391
|
-
buf[37] = count & 0xff;
|
|
392
|
-
let offset = 38;
|
|
393
|
-
for (const hb of routeBytes) {
|
|
394
|
-
buf[offset++] = hb.length & 0xff;
|
|
395
|
-
buf.set(hb, offset);
|
|
396
|
-
offset += hb.length;
|
|
397
|
-
}
|
|
398
|
-
return buf;
|
|
399
|
-
};
|
|
400
|
-
const decodeRoute = (data, offsetStart, routeCount) => {
|
|
401
|
-
let offset = offsetStart;
|
|
402
|
-
const route = [];
|
|
403
|
-
for (let i = 0; i < routeCount; i++) {
|
|
404
|
-
if (offset + 1 > data.length)
|
|
405
|
-
break;
|
|
406
|
-
const len = data[offset++];
|
|
407
|
-
if (len === 0)
|
|
408
|
-
break;
|
|
409
|
-
if (offset + len > data.length)
|
|
410
|
-
break;
|
|
411
|
-
if (route.length < MAX_ROUTE_HOPS) {
|
|
412
|
-
route.push(textDecoder.decode(data.subarray(offset, offset + len)));
|
|
413
|
-
}
|
|
414
|
-
offset += len;
|
|
415
|
-
}
|
|
416
|
-
return { route, offset };
|
|
417
|
-
};
|
|
418
|
-
const clampU16 = (v) => Math.max(0, Math.min(0xffff, v | 0));
|
|
419
|
-
const encodeTrackerAnnounce = (channelKey, ttlMs, level, maxChildren, freeSlots, bidPerByte, addrs) => {
|
|
420
|
-
const addrCount = Math.max(0, Math.min(255, addrs.length));
|
|
421
|
-
let bytes = 1 + 32 + 4 + 2 + 2 + 2 + 4 + 1;
|
|
422
|
-
const addrBytes = new Array(addrCount);
|
|
423
|
-
for (let i = 0; i < addrCount; i++) {
|
|
424
|
-
const b = addrs[i].bytes;
|
|
425
|
-
addrBytes[i] = b;
|
|
426
|
-
bytes += 2 + b.length;
|
|
427
|
-
}
|
|
428
|
-
const buf = new Uint8Array(bytes);
|
|
429
|
-
buf[0] = MSG_TRACKER_ANNOUNCE;
|
|
430
|
-
buf.set(channelKey, 1);
|
|
431
|
-
writeU32BE(buf, 33, Math.max(0, Math.floor(ttlMs)) >>> 0);
|
|
432
|
-
writeU16BE(buf, 37, clampU16(level));
|
|
433
|
-
writeU16BE(buf, 39, clampU16(maxChildren));
|
|
434
|
-
writeU16BE(buf, 41, clampU16(freeSlots));
|
|
435
|
-
writeU32BE(buf, 43, Math.max(0, Math.floor(bidPerByte)) >>> 0);
|
|
436
|
-
buf[47] = addrCount & 0xff;
|
|
437
|
-
let offset = 48;
|
|
438
|
-
for (let i = 0; i < addrCount; i++) {
|
|
439
|
-
const b = addrBytes[i];
|
|
440
|
-
writeU16BE(buf, offset, b.length);
|
|
441
|
-
offset += 2;
|
|
442
|
-
buf.set(b, offset);
|
|
443
|
-
offset += b.length;
|
|
444
|
-
}
|
|
445
|
-
return buf;
|
|
446
|
-
};
|
|
447
|
-
const encodeTrackerQuery = (channelKey, reqId, want) => {
|
|
448
|
-
const buf = new Uint8Array(1 + 32 + 4 + 2);
|
|
449
|
-
buf[0] = MSG_TRACKER_QUERY;
|
|
450
|
-
buf.set(channelKey, 1);
|
|
451
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
452
|
-
writeU16BE(buf, 37, clampU16(want));
|
|
453
|
-
return buf;
|
|
454
|
-
};
|
|
455
|
-
const encodeTrackerReply = (channelKey, reqId, entries) => {
|
|
456
|
-
const count = Math.max(0, Math.min(255, entries.length));
|
|
457
|
-
let bytes = 1 + 32 + 4 + 1;
|
|
458
|
-
const encoded = [];
|
|
459
|
-
for (let i = 0; i < count; i++) {
|
|
460
|
-
const e = entries[i];
|
|
461
|
-
const hashBytes = textEncoder.encode(e.hash);
|
|
462
|
-
if (hashBytes.length > 255)
|
|
463
|
-
continue;
|
|
464
|
-
const addrCount = Math.max(0, Math.min(255, e.addrs.length));
|
|
465
|
-
const addrs = e.addrs.slice(0, addrCount);
|
|
466
|
-
bytes += 1 + hashBytes.length + 2 + 2 + 4 + 1;
|
|
467
|
-
for (const a of addrs)
|
|
468
|
-
bytes += 2 + a.length;
|
|
469
|
-
encoded.push({
|
|
470
|
-
hashBytes,
|
|
471
|
-
level: e.level,
|
|
472
|
-
freeSlots: e.freeSlots,
|
|
473
|
-
bidPerByte: e.bidPerByte,
|
|
474
|
-
addrs,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
const buf = new Uint8Array(bytes);
|
|
478
|
-
buf[0] = MSG_TRACKER_REPLY;
|
|
479
|
-
buf.set(channelKey, 1);
|
|
480
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
481
|
-
buf[37] = Math.max(0, Math.min(255, encoded.length)) & 0xff;
|
|
482
|
-
let offset = 38;
|
|
483
|
-
for (const e of encoded) {
|
|
484
|
-
buf[offset++] = e.hashBytes.length & 0xff;
|
|
485
|
-
buf.set(e.hashBytes, offset);
|
|
486
|
-
offset += e.hashBytes.length;
|
|
487
|
-
writeU16BE(buf, offset, clampU16(e.level));
|
|
488
|
-
offset += 2;
|
|
489
|
-
writeU16BE(buf, offset, clampU16(e.freeSlots));
|
|
490
|
-
offset += 2;
|
|
491
|
-
writeU32BE(buf, offset, Math.max(0, Math.floor(e.bidPerByte)) >>> 0);
|
|
492
|
-
offset += 4;
|
|
493
|
-
buf[offset++] = Math.max(0, Math.min(255, e.addrs.length)) & 0xff;
|
|
494
|
-
for (const a of e.addrs) {
|
|
495
|
-
writeU16BE(buf, offset, a.length);
|
|
496
|
-
offset += 2;
|
|
497
|
-
buf.set(a, offset);
|
|
498
|
-
offset += a.length;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return buf;
|
|
502
|
-
};
|
|
503
|
-
const encodeTrackerFeedback = (channelKey, candidateHash, event, reason) => {
|
|
504
|
-
const hashBytes = textEncoder.encode(candidateHash);
|
|
505
|
-
const hashLen = Math.max(0, Math.min(255, hashBytes.length));
|
|
506
|
-
const buf = new Uint8Array(1 + 32 + 1 + hashLen + 1 + 1);
|
|
507
|
-
buf[0] = MSG_TRACKER_FEEDBACK;
|
|
508
|
-
buf.set(channelKey, 1);
|
|
509
|
-
buf[33] = hashLen & 0xff;
|
|
510
|
-
buf.set(hashBytes.subarray(0, hashLen), 34);
|
|
511
|
-
buf[34 + hashLen] = event & 0xff;
|
|
512
|
-
buf[34 + hashLen + 1] = reason & 0xff;
|
|
513
|
-
return buf;
|
|
514
|
-
};
|
|
515
|
-
const encodeProviderAnnounce = (namespaceKey, ttlMs, addrs) => {
|
|
516
|
-
const addrCount = Math.max(0, Math.min(255, addrs.length));
|
|
517
|
-
let bytes = 1 + 32 + 4 + 1;
|
|
518
|
-
const addrBytes = new Array(addrCount);
|
|
519
|
-
for (let i = 0; i < addrCount; i++) {
|
|
520
|
-
const b = addrs[i].bytes;
|
|
521
|
-
addrBytes[i] = b;
|
|
522
|
-
bytes += 2 + b.length;
|
|
523
|
-
}
|
|
524
|
-
const buf = new Uint8Array(bytes);
|
|
525
|
-
buf[0] = MSG_PROVIDER_ANNOUNCE;
|
|
526
|
-
buf.set(namespaceKey, 1);
|
|
527
|
-
writeU32BE(buf, 33, Math.max(0, Math.floor(ttlMs)) >>> 0);
|
|
528
|
-
buf[37] = addrCount & 0xff;
|
|
529
|
-
let offset = 38;
|
|
530
|
-
for (let i = 0; i < addrCount; i++) {
|
|
531
|
-
const b = addrBytes[i];
|
|
532
|
-
writeU16BE(buf, offset, b.length);
|
|
533
|
-
offset += 2;
|
|
534
|
-
buf.set(b, offset);
|
|
535
|
-
offset += b.length;
|
|
536
|
-
}
|
|
537
|
-
return buf;
|
|
538
|
-
};
|
|
539
|
-
const encodeProviderQuery = (namespaceKey, reqId, want, seed) => {
|
|
540
|
-
const buf = new Uint8Array(1 + 32 + 4 + 2 + 4);
|
|
541
|
-
buf[0] = MSG_PROVIDER_QUERY;
|
|
542
|
-
buf.set(namespaceKey, 1);
|
|
543
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
544
|
-
writeU16BE(buf, 37, clampU16(want));
|
|
545
|
-
writeU32BE(buf, 39, seed >>> 0);
|
|
546
|
-
return buf;
|
|
547
|
-
};
|
|
548
|
-
const encodeProviderEntries = (entries) => {
|
|
549
|
-
const count = Math.max(0, Math.min(255, entries.length));
|
|
550
|
-
let bytes = 1;
|
|
551
|
-
const encoded = [];
|
|
552
|
-
for (let i = 0; i < count; i++) {
|
|
553
|
-
const e = entries[i];
|
|
554
|
-
const hashBytes = textEncoder.encode(e.hash);
|
|
555
|
-
if (hashBytes.length > 255)
|
|
556
|
-
continue;
|
|
557
|
-
const addrCount = Math.max(0, Math.min(255, e.addrs.length));
|
|
558
|
-
const addrs = e.addrs.slice(0, addrCount);
|
|
559
|
-
bytes += 1 + hashBytes.length + 1;
|
|
560
|
-
for (const a of addrs)
|
|
561
|
-
bytes += 2 + a.length;
|
|
562
|
-
encoded.push({ hashBytes, addrs });
|
|
563
|
-
}
|
|
564
|
-
return { bytes, encoded };
|
|
565
|
-
};
|
|
566
|
-
const encodeProviderReply = (namespaceKey, reqId, entries) => {
|
|
567
|
-
const { bytes: entryBytes, encoded } = encodeProviderEntries(entries);
|
|
568
|
-
let bytes = 1 + 32 + 4 + entryBytes;
|
|
569
|
-
const buf = new Uint8Array(bytes);
|
|
570
|
-
buf[0] = MSG_PROVIDER_REPLY;
|
|
571
|
-
buf.set(namespaceKey, 1);
|
|
572
|
-
writeU32BE(buf, 33, reqId >>> 0);
|
|
573
|
-
buf[37] = Math.max(0, Math.min(255, encoded.length)) & 0xff;
|
|
574
|
-
let offset = 38;
|
|
575
|
-
for (const e of encoded) {
|
|
576
|
-
buf[offset++] = e.hashBytes.length & 0xff;
|
|
577
|
-
buf.set(e.hashBytes, offset);
|
|
578
|
-
offset += e.hashBytes.length;
|
|
579
|
-
buf[offset++] = Math.max(0, Math.min(255, e.addrs.length)) & 0xff;
|
|
580
|
-
for (const a of e.addrs) {
|
|
581
|
-
writeU16BE(buf, offset, a.length);
|
|
582
|
-
offset += 2;
|
|
583
|
-
buf.set(a, offset);
|
|
584
|
-
offset += a.length;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return buf;
|
|
588
|
-
};
|
|
589
|
-
const encodeProviderSubscribe = (namespaceKey, want, ttlMs) => {
|
|
590
|
-
const buf = new Uint8Array(1 + 32 + 2 + 4);
|
|
591
|
-
buf[0] = MSG_PROVIDER_SUBSCRIBE;
|
|
592
|
-
buf.set(namespaceKey, 1);
|
|
593
|
-
writeU16BE(buf, 33, clampU16(want));
|
|
594
|
-
writeU32BE(buf, 35, Math.max(0, Math.floor(ttlMs)) >>> 0);
|
|
595
|
-
return buf;
|
|
596
|
-
};
|
|
597
|
-
const encodeProviderUnsubscribe = (namespaceKey) => {
|
|
598
|
-
const buf = new Uint8Array(1 + 32);
|
|
599
|
-
buf[0] = MSG_PROVIDER_UNSUBSCRIBE;
|
|
600
|
-
buf.set(namespaceKey, 1);
|
|
601
|
-
return buf;
|
|
602
|
-
};
|
|
603
|
-
const encodeProviderNotify = (namespaceKey, entries) => {
|
|
604
|
-
const { bytes: entryBytes, encoded } = encodeProviderEntries(entries);
|
|
605
|
-
let bytes = 1 + 32 + entryBytes;
|
|
606
|
-
const buf = new Uint8Array(bytes);
|
|
607
|
-
buf[0] = MSG_PROVIDER_NOTIFY;
|
|
608
|
-
buf.set(namespaceKey, 1);
|
|
609
|
-
buf[33] = Math.max(0, Math.min(255, encoded.length)) & 0xff;
|
|
610
|
-
let offset = 34;
|
|
611
|
-
for (const e of encoded) {
|
|
612
|
-
buf[offset++] = e.hashBytes.length & 0xff;
|
|
613
|
-
buf.set(e.hashBytes, offset);
|
|
614
|
-
offset += e.hashBytes.length;
|
|
615
|
-
buf[offset++] = Math.max(0, Math.min(255, e.addrs.length)) & 0xff;
|
|
616
|
-
for (const a of e.addrs) {
|
|
617
|
-
writeU16BE(buf, offset, a.length);
|
|
618
|
-
offset += 2;
|
|
619
|
-
buf.set(a, offset);
|
|
620
|
-
offset += a.length;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
return buf;
|
|
624
|
-
};
|
|
625
|
-
const decodeProviderEntries = (data, offsetStart, maxCount) => {
|
|
626
|
-
let offset = offsetStart;
|
|
627
|
-
const providers = [];
|
|
628
|
-
const limit = Math.min(maxCount, 255);
|
|
629
|
-
for (let i = 0; i < limit; i++) {
|
|
630
|
-
if (offset + 1 > data.length)
|
|
631
|
-
break;
|
|
632
|
-
const hashLen = data[offset++];
|
|
633
|
-
if (offset + hashLen > data.length)
|
|
634
|
-
break;
|
|
635
|
-
const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
|
|
636
|
-
offset += hashLen;
|
|
637
|
-
if (offset + 1 > data.length)
|
|
638
|
-
break;
|
|
639
|
-
const addrCount = data[offset++];
|
|
640
|
-
const addrs = [];
|
|
641
|
-
const addrMax = Math.min(addrCount, 16);
|
|
642
|
-
for (let j = 0; j < addrMax; j++) {
|
|
643
|
-
if (offset + 2 > data.length)
|
|
644
|
-
break;
|
|
645
|
-
const len = readU16BE(data, offset);
|
|
646
|
-
offset += 2;
|
|
647
|
-
if (offset + len > data.length)
|
|
648
|
-
break;
|
|
649
|
-
const bytes = data.subarray(offset, offset + len);
|
|
650
|
-
offset += len;
|
|
651
|
-
try {
|
|
652
|
-
addrs.push(multiaddr(bytes));
|
|
653
|
-
}
|
|
654
|
-
catch {
|
|
655
|
-
// ignore invalid
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (!hash)
|
|
659
|
-
continue;
|
|
660
|
-
providers.push({ hash, addrs });
|
|
61
|
+
const PARENT_UPGRADE_ROOT_RESERVATION_TTL_MS = 5_000;
|
|
62
|
+
const stableUnitInterval = (input) => {
|
|
63
|
+
let hash = 0x811c9dc5;
|
|
64
|
+
for (let i = 0; i < input.length; i++) {
|
|
65
|
+
hash ^= input.charCodeAt(i);
|
|
66
|
+
hash = Math.imul(hash, 0x01000193);
|
|
661
67
|
}
|
|
662
|
-
return
|
|
68
|
+
return (hash >>> 0) / 0x100000000;
|
|
663
69
|
};
|
|
70
|
+
const textEncoder = new TextEncoder();
|
|
664
71
|
const createDeferred = () => {
|
|
665
72
|
let resolve;
|
|
666
73
|
let reject;
|
|
@@ -695,11 +102,50 @@ const createEmptyMetrics = () => ({
|
|
|
695
102
|
joinAcceptReceived: 0,
|
|
696
103
|
joinRejectSent: 0,
|
|
697
104
|
joinRejectReceived: 0,
|
|
105
|
+
joinPeerResets: 0,
|
|
698
106
|
kickSent: 0,
|
|
699
107
|
kickReceived: 0,
|
|
700
108
|
reparentDisconnect: 0,
|
|
701
109
|
reparentStale: 0,
|
|
702
110
|
reparentKicked: 0,
|
|
111
|
+
reparentUpgrade: 0,
|
|
112
|
+
reparentUpgradeSkipLeaf: 0,
|
|
113
|
+
reparentUpgradeSkipRepair: 0,
|
|
114
|
+
reparentUpgradeSkipData: 0,
|
|
115
|
+
reparentUpgradeSkipCooldown: 0,
|
|
116
|
+
reparentUpgradeSkipQuiet: 0,
|
|
117
|
+
reparentUpgradeSkipBudget: 0,
|
|
118
|
+
reparentUpgradeSkipCandidateLevel: 0,
|
|
119
|
+
reparentUpgradeSkipCandidateSlots: 0,
|
|
120
|
+
reparentUpgradeSkipCandidatePressure: 0,
|
|
121
|
+
reparentUpgradeSkipRootPressure: 0,
|
|
122
|
+
reparentUpgradeSkipProbeNoReply: 0,
|
|
123
|
+
reparentUpgradeSkipProbeNotRooted: 0,
|
|
124
|
+
reparentUpgradeSkipProbeRepair: 0,
|
|
125
|
+
reparentUpgradeSkipProbeLag: 0,
|
|
126
|
+
reparentUpgradeSkipProbeOverloaded: 0,
|
|
127
|
+
reparentUpgradeSkipProbeCooldown: 0,
|
|
128
|
+
parentProbeReqSent: 0,
|
|
129
|
+
parentProbeReqReceived: 0,
|
|
130
|
+
parentProbeReplySent: 0,
|
|
131
|
+
parentProbeReplyReceived: 0,
|
|
132
|
+
parentUpgradeRootReservationCreated: 0,
|
|
133
|
+
parentUpgradeRootReservationConsumed: 0,
|
|
134
|
+
parentUpgradeRootReservationRejected: 0,
|
|
135
|
+
parentUpgradeRootReservationMarginRejected: 0,
|
|
136
|
+
parentUpgradeRootReservationBlocked: 0,
|
|
137
|
+
parentUpgradeRootReservationExpired: 0,
|
|
138
|
+
parentShadowStart: 0,
|
|
139
|
+
parentShadowObserve: 0,
|
|
140
|
+
parentShadowPromote: 0,
|
|
141
|
+
parentShadowReset: 0,
|
|
142
|
+
parentShadowRejectNoReply: 0,
|
|
143
|
+
parentShadowRejectNotRooted: 0,
|
|
144
|
+
parentShadowRejectCapacity: 0,
|
|
145
|
+
parentShadowRejectRepair: 0,
|
|
146
|
+
parentShadowRejectLag: 0,
|
|
147
|
+
parentShadowRejectOverloaded: 0,
|
|
148
|
+
parentShadowRejectLevel: 0,
|
|
703
149
|
endSent: 0,
|
|
704
150
|
endReceived: 0,
|
|
705
151
|
repairReqSent: 0,
|
|
@@ -727,6 +173,8 @@ const createEmptyMetrics = () => ({
|
|
|
727
173
|
routeProxyQueries: 0,
|
|
728
174
|
routeProxyTimeouts: 0,
|
|
729
175
|
routeProxyFanout: 0,
|
|
176
|
+
routeProxyCoalesced: 0,
|
|
177
|
+
routeProxyRejected: 0,
|
|
730
178
|
});
|
|
731
179
|
const isDataId = (id) => id.length === 32 &&
|
|
732
180
|
id[0] === ID_PREFIX[0] &&
|
|
@@ -737,6 +185,8 @@ export class FanoutTree extends DirectStream {
|
|
|
737
185
|
channelsBySuffixKey = new Map();
|
|
738
186
|
cachedSuffixKey = new WeakMap();
|
|
739
187
|
metricsBySuffixKey = new Map();
|
|
188
|
+
joinTimeoutStreakByPeer = new Map();
|
|
189
|
+
joinResetCooldownUntilByPeer = new Map();
|
|
740
190
|
bootstraps = [];
|
|
741
191
|
trackerBySuffixKey = new Map();
|
|
742
192
|
trackerNamespaceLru = new Map();
|
|
@@ -748,20 +198,33 @@ export class FanoutTree extends DirectStream {
|
|
|
748
198
|
underlayPeerDisconnectHandler;
|
|
749
199
|
pendingProviderQueryBySuffixKey = new Map();
|
|
750
200
|
providerAnnounceBySuffixKey = new Map();
|
|
201
|
+
parentUpgradeShadowInFlightSuffixKey;
|
|
751
202
|
defaultUploadOverheadBytes = 128;
|
|
752
203
|
random;
|
|
753
204
|
unicastAckNodeTag32;
|
|
754
205
|
unicastAckSeq = 0;
|
|
755
206
|
topicRootControlPlane;
|
|
207
|
+
// Native fanout components of the rust-core DirectStream engine
|
|
208
|
+
// (`@peerbit/network-rust`). When set, every frame encode/decode of the
|
|
209
|
+
// `/peerbit/fanout-tree/0.5.0` codec plus the parent-upgrade policy/gate
|
|
210
|
+
// decisions run natively (byte- and decision-identical, covered by the
|
|
211
|
+
// golden parity suite); the channel state machine, timers and events
|
|
212
|
+
// stay host-side. Unset (the default) leaves every code path as-is.
|
|
213
|
+
nativeFanout;
|
|
214
|
+
codec;
|
|
756
215
|
constructor(components, opts) {
|
|
757
|
-
const { random, topicRootControlPlane, ...rest } = (opts ??
|
|
216
|
+
const { random, topicRootControlPlane, ...rest } = (opts ??
|
|
217
|
+
{});
|
|
758
218
|
super(components, FANOUT_PROTOCOLS, {
|
|
759
219
|
canRelayMessage: false,
|
|
760
220
|
...rest,
|
|
761
221
|
});
|
|
762
222
|
this.random = typeof random === "function" ? random : Math.random;
|
|
223
|
+
this.nativeFanout = this.rustCore?.fanout;
|
|
224
|
+
this.codec = this.nativeFanout ?? tsFanoutWireCodec;
|
|
763
225
|
this.unicastAckNodeTag32 = readU32BE(sha256Sync(textEncoder.encode(this.publicKeyHash)), 0);
|
|
764
|
-
this.topicRootControlPlane =
|
|
226
|
+
this.topicRootControlPlane =
|
|
227
|
+
topicRootControlPlane ?? new TopicRootControlPlane();
|
|
765
228
|
const onPeerDisconnect = (ev) => {
|
|
766
229
|
const peerId = ev?.detail;
|
|
767
230
|
if (!peerId)
|
|
@@ -783,6 +246,8 @@ export class FanoutTree extends DirectStream {
|
|
|
783
246
|
this.components.events.removeEventListener("peer:disconnect", this.underlayPeerDisconnectHandler);
|
|
784
247
|
this.underlayPeerDisconnectHandler = undefined;
|
|
785
248
|
}
|
|
249
|
+
this.joinTimeoutStreakByPeer.clear();
|
|
250
|
+
this.joinResetCooldownUntilByPeer.clear();
|
|
786
251
|
return super.stop();
|
|
787
252
|
}
|
|
788
253
|
setBootstraps(addrs) {
|
|
@@ -878,6 +343,24 @@ export class FanoutTree extends DirectStream {
|
|
|
878
343
|
}
|
|
879
344
|
return cached;
|
|
880
345
|
}
|
|
346
|
+
/** Decoded provider entries (raw multiaddr bytes) -> candidates. Invalid
|
|
347
|
+
* multiaddrs are dropped exactly like the historical inline parsing. */
|
|
348
|
+
toProviderCandidates(entries) {
|
|
349
|
+
const candidates = [];
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
const addrs = [];
|
|
352
|
+
for (const bytes of entry.addrs) {
|
|
353
|
+
try {
|
|
354
|
+
addrs.push(multiaddr(bytes));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// ignore invalid multiaddrs
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
candidates.push({ hash: entry.hash, addrs });
|
|
361
|
+
}
|
|
362
|
+
return candidates;
|
|
363
|
+
}
|
|
881
364
|
rememberProviderCandidates(id, candidates, expiresAt) {
|
|
882
365
|
if (candidates.length === 0)
|
|
883
366
|
return;
|
|
@@ -942,7 +425,7 @@ export class FanoutTree extends DirectStream {
|
|
|
942
425
|
addrs: candidate.addrs.map((a) => a.bytes),
|
|
943
426
|
expiresAt: now + 60_000,
|
|
944
427
|
}));
|
|
945
|
-
void this._sendControl(hash, encodeProviderNotify(id.key, entries)).catch(dontThrowIfDeliveryError);
|
|
428
|
+
void this._sendControl(hash, this.codec.encodeProviderNotify(id.key, entries)).catch(dontThrowIfDeliveryError);
|
|
946
429
|
}
|
|
947
430
|
this.pruneProviderWatchersIfEmpty(id.suffixKey);
|
|
948
431
|
}
|
|
@@ -1064,7 +547,7 @@ export class FanoutTree extends DirectStream {
|
|
|
1064
547
|
return;
|
|
1065
548
|
const ttlMs = ttlOverrideMs != null ? ttlOverrideMs : state.ttlMs;
|
|
1066
549
|
const addrs = this.getSelfAnnounceAddrs();
|
|
1067
|
-
const bytes = encodeProviderAnnounce(state.id.key, ttlMs, addrs);
|
|
550
|
+
const bytes = this.codec.encodeProviderAnnounce(state.id.key, ttlMs, addrs);
|
|
1068
551
|
await this._sendControlMany(peers, bytes);
|
|
1069
552
|
}
|
|
1070
553
|
nextProviderReqId(suffixKey) {
|
|
@@ -1094,7 +577,7 @@ export class FanoutTree extends DirectStream {
|
|
|
1094
577
|
const shuffleInPlace = (arr, seed32) => {
|
|
1095
578
|
if (arr.length <= 1)
|
|
1096
579
|
return;
|
|
1097
|
-
if (
|
|
580
|
+
if (seed32 >>> 0 === 0) {
|
|
1098
581
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
1099
582
|
const j = Math.floor(this.random() * (i + 1));
|
|
1100
583
|
const tmp = arr[i];
|
|
@@ -1143,7 +626,7 @@ export class FanoutTree extends DirectStream {
|
|
|
1143
626
|
? await this.ensureBootstrapPeers(bootstraps, dialTimeoutMs, signal, bootstrapMaxPeers)
|
|
1144
627
|
: [];
|
|
1145
628
|
const perTrackerTimeout = Math.max(0, Math.floor(options.queryTimeoutMs ?? 1_000));
|
|
1146
|
-
const overallTimeout = Math.max(0, Math.floor(options.timeoutMs ??
|
|
629
|
+
const overallTimeout = Math.max(0, Math.floor(options.timeoutMs ?? 8_000));
|
|
1147
630
|
const deadlineAt = overallTimeout > 0 ? Date.now() + overallTimeout : 0;
|
|
1148
631
|
const byReq = this.pendingProviderQueryBySuffixKey.get(id.suffixKey) ||
|
|
1149
632
|
new Map();
|
|
@@ -1153,9 +636,13 @@ export class FanoutTree extends DirectStream {
|
|
|
1153
636
|
const p = new Promise((resolve) => {
|
|
1154
637
|
byReq.set(reqId, { resolve });
|
|
1155
638
|
});
|
|
1156
|
-
void this._sendControl(trackerHash, encodeProviderQuery(id.key, reqId, want, seed));
|
|
1157
|
-
const remainingMs = deadlineAt > 0
|
|
1158
|
-
|
|
639
|
+
void this._sendControl(trackerHash, this.codec.encodeProviderQuery(id.key, reqId, want, seed));
|
|
640
|
+
const remainingMs = deadlineAt > 0
|
|
641
|
+
? Math.max(0, deadlineAt - Date.now())
|
|
642
|
+
: perTrackerTimeout;
|
|
643
|
+
const timeoutMs = deadlineAt > 0
|
|
644
|
+
? Math.min(perTrackerTimeout, remainingMs)
|
|
645
|
+
: perTrackerTimeout;
|
|
1159
646
|
const res = await Promise.race([
|
|
1160
647
|
p,
|
|
1161
648
|
delay(timeoutMs, { signal }).then(() => null),
|
|
@@ -1252,7 +739,7 @@ export class FanoutTree extends DirectStream {
|
|
|
1252
739
|
this.providerWatchesBySuffixKey.delete(id.suffixKey);
|
|
1253
740
|
}
|
|
1254
741
|
for (const trackerHash of watch.trackerPeers) {
|
|
1255
|
-
void this._sendControl(trackerHash, encodeProviderUnsubscribe(id.key)).catch(dontThrowIfDeliveryError);
|
|
742
|
+
void this._sendControl(trackerHash, this.codec.encodeProviderUnsubscribe(id.key)).catch(dontThrowIfDeliveryError);
|
|
1256
743
|
}
|
|
1257
744
|
watch.trackerPeers = [];
|
|
1258
745
|
};
|
|
@@ -1272,12 +759,15 @@ export class FanoutTree extends DirectStream {
|
|
|
1272
759
|
: [];
|
|
1273
760
|
watch.trackerPeers = trackerPeers;
|
|
1274
761
|
for (const trackerHash of trackerPeers) {
|
|
1275
|
-
void this._sendControl(trackerHash, encodeProviderSubscribe(id.key, watch.want, watch.ttlMs)).catch(dontThrowIfDeliveryError);
|
|
762
|
+
void this._sendControl(trackerHash, this.codec.encodeProviderSubscribe(id.key, watch.want, watch.ttlMs)).catch(dontThrowIfDeliveryError);
|
|
1276
763
|
}
|
|
1277
764
|
}
|
|
1278
765
|
catch (error) {
|
|
1279
766
|
if (error instanceof AbortError ||
|
|
1280
|
-
(error &&
|
|
767
|
+
(error &&
|
|
768
|
+
typeof error === "object" &&
|
|
769
|
+
"name" in error &&
|
|
770
|
+
error.name === "AbortError")) {
|
|
1281
771
|
return;
|
|
1282
772
|
}
|
|
1283
773
|
}
|
|
@@ -1337,9 +827,9 @@ export class FanoutTree extends DirectStream {
|
|
|
1337
827
|
const neighborRepair = opts.neighborRepair === true;
|
|
1338
828
|
const neighborRepairPeers = Math.max(0, Math.floor(opts.neighborRepairPeers ?? 2));
|
|
1339
829
|
const neighborMeshPeers = Math.max(0, Math.floor(opts.neighborMeshPeers ?? Math.max(0, neighborRepairPeers * 2)));
|
|
1340
|
-
const neighborAnnounceIntervalMs = Math.max(0, Math.floor(opts.neighborAnnounceIntervalMs ??
|
|
1341
|
-
const neighborMeshRefreshIntervalMs = Math.max(0, Math.floor(opts.neighborMeshRefreshIntervalMs ??
|
|
1342
|
-
const neighborHaveTtlMs = Math.max(0, Math.floor(opts.neighborHaveTtlMs ??
|
|
830
|
+
const neighborAnnounceIntervalMs = Math.max(0, Math.floor(opts.neighborAnnounceIntervalMs ?? 800));
|
|
831
|
+
const neighborMeshRefreshIntervalMs = Math.max(0, Math.floor(opts.neighborMeshRefreshIntervalMs ?? 10_000));
|
|
832
|
+
const neighborHaveTtlMs = Math.max(0, Math.floor(opts.neighborHaveTtlMs ?? 8_000));
|
|
1343
833
|
const neighborRepairBudgetBps = Math.max(0, Math.floor(opts.neighborRepairBudgetBps ?? 0));
|
|
1344
834
|
const neighborRepairBurstMs = Math.max(0, Math.floor(opts.neighborRepairBurstMs ?? 1_000));
|
|
1345
835
|
const neighborRepairTokenCapacity = neighborRepairBudgetBps > 0 && neighborRepairBurstMs > 0
|
|
@@ -1364,6 +854,14 @@ export class FanoutTree extends DirectStream {
|
|
|
1364
854
|
parent: undefined,
|
|
1365
855
|
children: new Map(),
|
|
1366
856
|
rejoinCooldownUntil: 0,
|
|
857
|
+
parentUpgradeCount: 0,
|
|
858
|
+
parentUpgradeLastAt: 0,
|
|
859
|
+
parentUpgradeBackoffMs: 0,
|
|
860
|
+
parentUpgradeBackoffUntil: 0,
|
|
861
|
+
parentUpgradeRetryAfterSeq: -1,
|
|
862
|
+
parentUpgradeStaleRootProbeRound: 0,
|
|
863
|
+
parentUpgradeTrackerNoCapacityUntil: 0,
|
|
864
|
+
parentProbeRejectBackoffMsByHash: new Map(),
|
|
1367
865
|
joinedAtLeastOnce: opts.role === "root",
|
|
1368
866
|
routeFromRoot: opts.role === "root" ? [this.publicKeyHash] : undefined,
|
|
1369
867
|
routeByPeer: new Map(),
|
|
@@ -1413,22 +911,33 @@ export class FanoutTree extends DirectStream {
|
|
|
1413
911
|
: undefined,
|
|
1414
912
|
nextExpectedSeq: 0,
|
|
1415
913
|
missingSeqs: new Set(),
|
|
914
|
+
repairRequestedAtBySeq: new Map(),
|
|
1416
915
|
endSeqExclusive: -1,
|
|
1417
916
|
lastRepairSentAt: 0,
|
|
917
|
+
parentRepairUnansweredStreak: 0,
|
|
1418
918
|
lastParentDataAt: 0,
|
|
919
|
+
lastParentUpgradeActivityAt: 0,
|
|
1419
920
|
receivedAnyParentData: false,
|
|
921
|
+
parentDataLatencySamples: 0,
|
|
922
|
+
parentDataLatencyEwmaMs: 0,
|
|
923
|
+
parentDataLatencyMaxMs: 0,
|
|
1420
924
|
channelPeers: new Map(),
|
|
1421
925
|
knownCandidateAddrs: new Map(),
|
|
1422
926
|
lazyPeers: new Set(),
|
|
1423
927
|
haveByPeer: new Map(),
|
|
1424
928
|
maxSeqSeen: -1,
|
|
1425
929
|
lastIHaveSentAt: 0,
|
|
930
|
+
cachedAnnounceTrackerPeers: undefined,
|
|
1426
931
|
lastIHaveSentMaxSeq: -1,
|
|
1427
932
|
pendingJoin: new Map(),
|
|
1428
933
|
pendingTrackerQuery: new Map(),
|
|
934
|
+
pendingParentProbe: new Map(),
|
|
935
|
+
parentUpgradeReservationsByHash: new Map(),
|
|
936
|
+
parentProbeRejectUntilByHash: new Map(),
|
|
1429
937
|
pendingRouteQuery: new Map(),
|
|
1430
938
|
pendingUnicastAck: new Map(),
|
|
1431
939
|
pendingRouteProxy: new Map(),
|
|
940
|
+
routeProxyByTarget: new Map(),
|
|
1432
941
|
bootstrapOverride: undefined,
|
|
1433
942
|
bootstrapDialTimeoutMs: 10_000,
|
|
1434
943
|
bootstrapMaxPeers: 0,
|
|
@@ -1487,24 +996,133 @@ export class FanoutTree extends DirectStream {
|
|
|
1487
996
|
}
|
|
1488
997
|
ch.pendingUnicastAck.clear();
|
|
1489
998
|
}
|
|
999
|
+
resetParentDataLatency(ch) {
|
|
1000
|
+
ch.parentDataLatencySamples = 0;
|
|
1001
|
+
ch.parentDataLatencyEwmaMs = 0;
|
|
1002
|
+
ch.parentDataLatencyMaxMs = 0;
|
|
1003
|
+
}
|
|
1004
|
+
noteParentDataLatency(ch, timestamp) {
|
|
1005
|
+
const sentAt = Number(timestamp);
|
|
1006
|
+
if (!Number.isFinite(sentAt) || sentAt <= 0)
|
|
1007
|
+
return;
|
|
1008
|
+
const latencyMs = Date.now() - sentAt;
|
|
1009
|
+
if (!Number.isFinite(latencyMs) || latencyMs < 0 || latencyMs > 60_000) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const samples = Math.min(0xffff, ch.parentDataLatencySamples + 1);
|
|
1013
|
+
ch.parentDataLatencySamples = samples;
|
|
1014
|
+
ch.parentDataLatencyEwmaMs =
|
|
1015
|
+
samples <= 1
|
|
1016
|
+
? latencyMs
|
|
1017
|
+
: ch.parentDataLatencyEwmaMs * 0.875 + latencyMs * 0.125;
|
|
1018
|
+
ch.parentDataLatencyMaxMs = Math.max(ch.parentDataLatencyMaxMs, latencyMs);
|
|
1019
|
+
}
|
|
1020
|
+
clearParentUpgradeGrace(ch, notifyPreviousParent = false, notifyCandidateParent = false) {
|
|
1021
|
+
const grace = ch.parentUpgradeGrace;
|
|
1022
|
+
if (!grace)
|
|
1023
|
+
return [];
|
|
1024
|
+
clearTimeout(grace.timer);
|
|
1025
|
+
ch.parentUpgradeGrace = undefined;
|
|
1026
|
+
const sends = [];
|
|
1027
|
+
if (notifyPreviousParent && grace.previousParent !== ch.parent) {
|
|
1028
|
+
sends.push(this._sendControl(grace.previousParent, this.codec.encodeLeave(ch.id.key)).catch(() => { }));
|
|
1029
|
+
}
|
|
1030
|
+
if (notifyCandidateParent && grace.candidateParent !== ch.parent) {
|
|
1031
|
+
sends.push(this._sendControl(grace.candidateParent, this.codec.encodeLeave(ch.id.key)).catch(() => { }));
|
|
1032
|
+
}
|
|
1033
|
+
return sends;
|
|
1034
|
+
}
|
|
1035
|
+
commitParentUpgradeGrace(ch) {
|
|
1036
|
+
const grace = ch.parentUpgradeGrace;
|
|
1037
|
+
if (!grace)
|
|
1038
|
+
return;
|
|
1039
|
+
clearTimeout(grace.timer);
|
|
1040
|
+
ch.parentUpgradeGrace = undefined;
|
|
1041
|
+
if (ch.closed || ch.parent !== grace.candidateParent)
|
|
1042
|
+
return;
|
|
1043
|
+
void this._sendControl(grace.previousParent, this.codec.encodeLeave(ch.id.key)).catch(() => { });
|
|
1044
|
+
}
|
|
1045
|
+
rollbackParentUpgradeGrace(ch) {
|
|
1046
|
+
const grace = ch.parentUpgradeGrace;
|
|
1047
|
+
if (!grace)
|
|
1048
|
+
return;
|
|
1049
|
+
clearTimeout(grace.timer);
|
|
1050
|
+
ch.parentUpgradeGrace = undefined;
|
|
1051
|
+
if (ch.closed || ch.parent !== grace.candidateParent)
|
|
1052
|
+
return;
|
|
1053
|
+
ch.parent = grace.previousParent;
|
|
1054
|
+
ch.level = grace.previousLevel;
|
|
1055
|
+
ch.routeFromRoot = grace.previousRouteFromRoot
|
|
1056
|
+
? [...grace.previousRouteFromRoot]
|
|
1057
|
+
: undefined;
|
|
1058
|
+
ch.lastParentDataAt = grace.previousLastParentDataAt;
|
|
1059
|
+
ch.lastParentUpgradeActivityAt =
|
|
1060
|
+
grace.previousLastParentUpgradeActivityAt;
|
|
1061
|
+
ch.receivedAnyParentData = grace.previousReceivedAnyParentData;
|
|
1062
|
+
this.resetParentDataLatency(ch);
|
|
1063
|
+
this.touchPeerHint(ch, grace.previousParent);
|
|
1064
|
+
void this._sendControl(grace.candidateParent, this.codec.encodeLeave(ch.id.key)).catch(() => { });
|
|
1065
|
+
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
1066
|
+
}
|
|
1067
|
+
startParentUpgradeGrace(ch, options) {
|
|
1068
|
+
if (ch.closed || options.previousParent === options.candidateParent)
|
|
1069
|
+
return;
|
|
1070
|
+
void Promise.all(this.clearParentUpgradeGrace(ch, true, true)).catch(() => { });
|
|
1071
|
+
const timeoutMs = Math.max(0, Math.floor(options.timeoutMs));
|
|
1072
|
+
if (timeoutMs <= 0) {
|
|
1073
|
+
void this._sendControl(options.previousParent, this.codec.encodeLeave(ch.id.key)).catch(() => { });
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const timer = setTimeout(() => this.rollbackParentUpgradeGrace(ch), timeoutMs);
|
|
1077
|
+
ch.parentUpgradeGrace = {
|
|
1078
|
+
previousParent: options.previousParent,
|
|
1079
|
+
candidateParent: options.candidateParent,
|
|
1080
|
+
previousLevel: options.previousLevel,
|
|
1081
|
+
previousRouteFromRoot: options.previousRouteFromRoot
|
|
1082
|
+
? [...options.previousRouteFromRoot]
|
|
1083
|
+
: undefined,
|
|
1084
|
+
previousLastParentDataAt: options.previousLastParentDataAt,
|
|
1085
|
+
previousLastParentUpgradeActivityAt: options.previousLastParentUpgradeActivityAt,
|
|
1086
|
+
previousReceivedAnyParentData: options.previousReceivedAnyParentData,
|
|
1087
|
+
timer,
|
|
1088
|
+
candidateFirstMessages: options.candidateFirstMessages,
|
|
1089
|
+
previousFirstMessages: options.previousFirstMessages,
|
|
1090
|
+
minCandidateFirstMessages: options.minCandidateFirstMessages,
|
|
1091
|
+
minCandidateAdvantageMessages: options.minCandidateAdvantageMessages,
|
|
1092
|
+
maxPreviousFirstMessages: options.maxPreviousFirstMessages,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1490
1095
|
detachFromParent(ch) {
|
|
1096
|
+
if (this.parentUpgradeShadowInFlightSuffixKey === ch.id.suffixKey) {
|
|
1097
|
+
this.parentUpgradeShadowInFlightSuffixKey = undefined;
|
|
1098
|
+
}
|
|
1099
|
+
void Promise.all(this.clearParentUpgradeGrace(ch, true, true)).catch(() => { });
|
|
1491
1100
|
ch.parent = undefined;
|
|
1492
1101
|
ch.level = Number.POSITIVE_INFINITY;
|
|
1493
1102
|
ch.routeFromRoot = undefined;
|
|
1494
1103
|
ch.routeByPeer.clear();
|
|
1495
1104
|
ch.lastParentDataAt = 0;
|
|
1105
|
+
ch.lastParentUpgradeActivityAt = 0;
|
|
1496
1106
|
ch.receivedAnyParentData = false;
|
|
1107
|
+
this.resetParentDataLatency(ch);
|
|
1497
1108
|
ch.pendingJoin.clear();
|
|
1109
|
+
ch.pendingParentProbe.clear();
|
|
1110
|
+
ch.parentShadow = undefined;
|
|
1111
|
+
ch.parentUpgradeBackoffMs = 0;
|
|
1112
|
+
ch.parentUpgradeBackoffUntil = 0;
|
|
1113
|
+
ch.parentUpgradeRetryAfterSeq = -1;
|
|
1114
|
+
ch.parentUpgradeStaleRootProbeRound = 0;
|
|
1115
|
+
ch.parentUpgradeTrackerNoCapacityUntil = 0;
|
|
1116
|
+
ch.parentProbeRejectUntilByHash.clear();
|
|
1117
|
+
ch.parentProbeRejectBackoffMsByHash.clear();
|
|
1498
1118
|
ch.pendingRouteQuery.clear();
|
|
1499
1119
|
this.abortPendingUnicastAcks(ch, new AbortError("fanout channel detached"));
|
|
1500
|
-
|
|
1501
|
-
clearTimeout(pending.timer);
|
|
1502
|
-
}
|
|
1503
|
-
ch.pendingRouteProxy.clear();
|
|
1120
|
+
this.clearRouteProxies(ch);
|
|
1504
1121
|
}
|
|
1505
1122
|
onPeerDisconnectedFromUnderlay(peerHash) {
|
|
1506
1123
|
if (!peerHash)
|
|
1507
1124
|
return;
|
|
1125
|
+
this.joinTimeoutStreakByPeer.delete(peerHash);
|
|
1508
1126
|
// Detach from a disconnected parent immediately, so children can rejoin.
|
|
1509
1127
|
// This is more reliable than polling `getConnections()` because the underlay
|
|
1510
1128
|
// can flap/reconnect faster than the join loop cadence.
|
|
@@ -1558,19 +1176,24 @@ export class FanoutTree extends DirectStream {
|
|
|
1558
1176
|
const kickChildren = options?.kickChildren !== false;
|
|
1559
1177
|
const pendingSends = [];
|
|
1560
1178
|
if (notifyParent && !ch.isRoot && ch.parent) {
|
|
1561
|
-
pendingSends.push(this._sendControl(ch.parent, encodeLeave(ch.id.key)).catch(() => { }));
|
|
1179
|
+
pendingSends.push(this._sendControl(ch.parent, this.codec.encodeLeave(ch.id.key)).catch(() => { }));
|
|
1562
1180
|
}
|
|
1181
|
+
pendingSends.push(...this.clearParentUpgradeGrace(ch, notifyParent && !ch.isRoot, false));
|
|
1563
1182
|
if (kickChildren && ch.children.size > 0) {
|
|
1564
1183
|
pendingSends.push(this.kickChildren(ch).catch(() => { }));
|
|
1565
1184
|
}
|
|
1566
1185
|
ch.pendingJoin.clear();
|
|
1567
1186
|
ch.pendingTrackerQuery.clear();
|
|
1187
|
+
ch.pendingParentProbe.clear();
|
|
1188
|
+
ch.parentProbeRejectUntilByHash.clear();
|
|
1189
|
+
ch.parentProbeRejectBackoffMsByHash.clear();
|
|
1190
|
+
ch.parentShadow = undefined;
|
|
1191
|
+
if (this.parentUpgradeShadowInFlightSuffixKey === ch.id.suffixKey) {
|
|
1192
|
+
this.parentUpgradeShadowInFlightSuffixKey = undefined;
|
|
1193
|
+
}
|
|
1568
1194
|
ch.pendingRouteQuery.clear();
|
|
1569
1195
|
this.abortPendingUnicastAcks(ch, new AbortError("fanout channel closed"));
|
|
1570
|
-
|
|
1571
|
-
clearTimeout(pending.timer);
|
|
1572
|
-
}
|
|
1573
|
-
ch.pendingRouteProxy.clear();
|
|
1196
|
+
this.clearRouteProxies(ch);
|
|
1574
1197
|
ch.parent = undefined;
|
|
1575
1198
|
ch.children.clear();
|
|
1576
1199
|
ch.lazyPeers.clear();
|
|
@@ -1603,6 +1226,11 @@ export class FanoutTree extends DirectStream {
|
|
|
1603
1226
|
routeCacheMaxEntries: ch.routeCacheMaxEntries,
|
|
1604
1227
|
};
|
|
1605
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Returns diagnostic counters for tests and simulation harnesses.
|
|
1231
|
+
*
|
|
1232
|
+
* @internal
|
|
1233
|
+
*/
|
|
1606
1234
|
getChannelMetrics(topic, root) {
|
|
1607
1235
|
const id = this.getChannelId(topic, root);
|
|
1608
1236
|
return this.getMetricsForSuffixKey(id.suffixKey);
|
|
@@ -1662,11 +1290,23 @@ export class FanoutTree extends DirectStream {
|
|
|
1662
1290
|
if (!ch.joinedOnce)
|
|
1663
1291
|
ch.joinedOnce = createDeferred();
|
|
1664
1292
|
if (!ch.joinLoop) {
|
|
1665
|
-
|
|
1293
|
+
const joinedOnce = ch.joinedOnce;
|
|
1294
|
+
const joinLoop = this._joinLoop(ch, joinOpts)
|
|
1295
|
+
.catch((err) => {
|
|
1666
1296
|
// Surface join errors to the caller without crashing the process
|
|
1667
1297
|
// via an unhandled rejection (joinLoop is not generally awaited).
|
|
1668
|
-
ch.
|
|
1298
|
+
if (ch.joinLoop === joinLoop)
|
|
1299
|
+
ch.joinLoop = undefined;
|
|
1300
|
+
if (ch.joinedOnce === joinedOnce && !ch.joinedAtLeastOnce) {
|
|
1301
|
+
ch.joinedOnce = undefined;
|
|
1302
|
+
}
|
|
1303
|
+
joinedOnce.reject(err);
|
|
1304
|
+
})
|
|
1305
|
+
.finally(() => {
|
|
1306
|
+
if (ch.joinLoop === joinLoop)
|
|
1307
|
+
ch.joinLoop = undefined;
|
|
1669
1308
|
});
|
|
1309
|
+
ch.joinLoop = joinLoop;
|
|
1670
1310
|
}
|
|
1671
1311
|
return ch.joinedOnce.promise;
|
|
1672
1312
|
}
|
|
@@ -1706,7 +1346,9 @@ export class FanoutTree extends DirectStream {
|
|
|
1706
1346
|
updatedAt: Date.now(),
|
|
1707
1347
|
};
|
|
1708
1348
|
}
|
|
1709
|
-
|
|
1349
|
+
// Speculative probe: a miss here says nothing about cache quality (the
|
|
1350
|
+
// caller falls through to resolveRouteToken, which counts its own miss).
|
|
1351
|
+
const route = this.getCachedRoute(ch, targetHash, { recordMiss: false });
|
|
1710
1352
|
if (!route)
|
|
1711
1353
|
return undefined;
|
|
1712
1354
|
const entry = ch.routeByPeer.get(targetHash);
|
|
@@ -1751,8 +1393,7 @@ export class FanoutTree extends DirectStream {
|
|
|
1751
1393
|
ch.knownCandidateAddrs.delete(hash);
|
|
1752
1394
|
ch.knownCandidateAddrs.set(hash, limited);
|
|
1753
1395
|
while (ch.knownCandidateAddrs.size > KNOWN_CANDIDATE_ADDRS_MAX_ENTRIES) {
|
|
1754
|
-
const oldest = ch.knownCandidateAddrs.keys().next()
|
|
1755
|
-
.value;
|
|
1396
|
+
const oldest = ch.knownCandidateAddrs.keys().next().value;
|
|
1756
1397
|
if (!oldest)
|
|
1757
1398
|
break;
|
|
1758
1399
|
ch.knownCandidateAddrs.delete(oldest);
|
|
@@ -1833,23 +1474,27 @@ export class FanoutTree extends DirectStream {
|
|
|
1833
1474
|
ch.metrics.routeCacheEvictions += 1;
|
|
1834
1475
|
}
|
|
1835
1476
|
}
|
|
1836
|
-
getCachedRoute(ch, targetHash) {
|
|
1477
|
+
getCachedRoute(ch, targetHash, opts) {
|
|
1478
|
+
const recordMiss = opts?.recordMiss !== false;
|
|
1837
1479
|
this.pruneRouteCache(ch);
|
|
1838
1480
|
const entry = ch.routeByPeer.get(targetHash);
|
|
1839
1481
|
if (!entry) {
|
|
1840
|
-
|
|
1482
|
+
if (recordMiss)
|
|
1483
|
+
ch.metrics.routeCacheMisses += 1;
|
|
1841
1484
|
return undefined;
|
|
1842
1485
|
}
|
|
1843
1486
|
const now = Date.now();
|
|
1844
1487
|
if (ch.routeCacheTtlMs > 0 && now - entry.updatedAt > ch.routeCacheTtlMs) {
|
|
1845
1488
|
ch.routeByPeer.delete(targetHash);
|
|
1846
|
-
|
|
1489
|
+
if (recordMiss)
|
|
1490
|
+
ch.metrics.routeCacheMisses += 1;
|
|
1847
1491
|
ch.metrics.routeCacheExpirations += 1;
|
|
1848
1492
|
return undefined;
|
|
1849
1493
|
}
|
|
1850
1494
|
if (!this.isRouteValidForChannel(ch, entry.route)) {
|
|
1851
1495
|
ch.routeByPeer.delete(targetHash);
|
|
1852
|
-
|
|
1496
|
+
if (recordMiss)
|
|
1497
|
+
ch.metrics.routeCacheMisses += 1;
|
|
1853
1498
|
return undefined;
|
|
1854
1499
|
}
|
|
1855
1500
|
// LRU touch
|
|
@@ -1883,14 +1528,92 @@ export class FanoutTree extends DirectStream {
|
|
|
1883
1528
|
if (!proxy)
|
|
1884
1529
|
return;
|
|
1885
1530
|
ch.pendingRouteProxy.delete(proxyReqId);
|
|
1531
|
+
const targetKey = `${proxy.direction}:${proxy.targetHash}`;
|
|
1532
|
+
if (ch.routeProxyByTarget.get(targetKey) === proxyReqId) {
|
|
1533
|
+
ch.routeProxyByTarget.delete(targetKey);
|
|
1534
|
+
}
|
|
1886
1535
|
clearTimeout(proxy.timer);
|
|
1536
|
+
const reply = (requester, downstreamReqId) => {
|
|
1537
|
+
void this._sendControl(requester, this.codec.encodeRouteReply(ch.id.key, downstreamReqId, route)).catch(() => { });
|
|
1538
|
+
};
|
|
1887
1539
|
if (proxy.localResolve) {
|
|
1888
1540
|
proxy.localResolve(route);
|
|
1889
|
-
return;
|
|
1890
1541
|
}
|
|
1891
|
-
|
|
1542
|
+
else {
|
|
1543
|
+
reply(proxy.requester, proxy.downstreamReqId);
|
|
1544
|
+
}
|
|
1545
|
+
if (proxy.waiters) {
|
|
1546
|
+
for (const w of proxy.waiters) {
|
|
1547
|
+
if ("localResolve" in w) {
|
|
1548
|
+
w.localResolve(route);
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
reply(w.requester, w.downstreamReqId);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Coalesce onto an in-flight same-target/same-direction proxy search, if one
|
|
1558
|
+
* exists. Returns true when the requester was attached as a waiter (or
|
|
1559
|
+
* answered empty on waiter overflow) and no new search should start.
|
|
1560
|
+
*/
|
|
1561
|
+
coalesceRouteProxy(ch, targetHash, direction, waiter) {
|
|
1562
|
+
const targetKey = `${direction}:${targetHash}`;
|
|
1563
|
+
const existingId = ch.routeProxyByTarget.get(targetKey);
|
|
1564
|
+
if (existingId == null)
|
|
1565
|
+
return false;
|
|
1566
|
+
const existing = ch.pendingRouteProxy.get(existingId);
|
|
1567
|
+
if (!existing) {
|
|
1568
|
+
ch.routeProxyByTarget.delete(targetKey);
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
if ((existing.waiters?.length ?? 0) >= ROUTE_PROXY_MAX_WAITERS) {
|
|
1572
|
+
ch.metrics.routeProxyRejected += 1;
|
|
1573
|
+
if ("localResolve" in waiter) {
|
|
1574
|
+
waiter.localResolve(undefined);
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
void this._sendControl(waiter.requester, this.codec.encodeRouteReply(ch.id.key, waiter.downstreamReqId)).catch(() => { });
|
|
1578
|
+
}
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
(existing.waiters ??= []).push(waiter);
|
|
1582
|
+
ch.metrics.routeProxyCoalesced += 1;
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Drop all in-flight route-proxy state for a channel (detach/kick/close).
|
|
1587
|
+
* Local resolvers are settled with `undefined` so root-origin
|
|
1588
|
+
* `resolveRouteToken()` callers never hang; remote requesters time out on
|
|
1589
|
+
* their own deadlines, as before.
|
|
1590
|
+
*/
|
|
1591
|
+
clearRouteProxies(ch) {
|
|
1592
|
+
for (const pending of ch.pendingRouteProxy.values()) {
|
|
1593
|
+
clearTimeout(pending.timer);
|
|
1594
|
+
pending.localResolve?.(undefined);
|
|
1595
|
+
if (pending.waiters) {
|
|
1596
|
+
for (const w of pending.waiters) {
|
|
1597
|
+
if ("localResolve" in w)
|
|
1598
|
+
w.localResolve(undefined);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
ch.pendingRouteProxy.clear();
|
|
1603
|
+
ch.routeProxyByTarget.clear();
|
|
1892
1604
|
}
|
|
1893
1605
|
proxyRouteQuery(ch, requester, downstreamReqId, targetHash, candidates, timeoutMs = ROUTE_PROXY_TIMEOUT_MS) {
|
|
1606
|
+
// Direction matters for coalescing correctness: an upstream lookup and the
|
|
1607
|
+
// root's downstream flood for the same target may legitimately transit the
|
|
1608
|
+
// same node concurrently (e.g. the target lives in the requester's own
|
|
1609
|
+
// branch), and merging them would make them wait on each other.
|
|
1610
|
+
const direction = candidates.length === 1 && candidates[0] === ch.parent ? "up" : "down";
|
|
1611
|
+
if (this.coalesceRouteProxy(ch, targetHash, direction, {
|
|
1612
|
+
requester,
|
|
1613
|
+
downstreamReqId,
|
|
1614
|
+
})) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1894
1617
|
const unique = [];
|
|
1895
1618
|
const seen = new Set();
|
|
1896
1619
|
for (const candidate of candidates) {
|
|
@@ -1903,7 +1626,12 @@ export class FanoutTree extends DirectStream {
|
|
|
1903
1626
|
unique.push(candidate);
|
|
1904
1627
|
}
|
|
1905
1628
|
if (unique.length === 0) {
|
|
1906
|
-
void this._sendControl(requester, encodeRouteReply(ch.id.key, downstreamReqId)).catch(() => { });
|
|
1629
|
+
void this._sendControl(requester, this.codec.encodeRouteReply(ch.id.key, downstreamReqId)).catch(() => { });
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (ch.pendingRouteProxy.size >= ROUTE_PROXY_MAX_PENDING) {
|
|
1633
|
+
ch.metrics.routeProxyRejected += 1;
|
|
1634
|
+
void this._sendControl(requester, this.codec.encodeRouteReply(ch.id.key, downstreamReqId)).catch(() => { });
|
|
1907
1635
|
return;
|
|
1908
1636
|
}
|
|
1909
1637
|
ch.metrics.routeProxyQueries += 1;
|
|
@@ -1918,9 +1646,11 @@ export class FanoutTree extends DirectStream {
|
|
|
1918
1646
|
downstreamReqId,
|
|
1919
1647
|
timer,
|
|
1920
1648
|
expectedReplies: new Set(unique),
|
|
1649
|
+
targetHash,
|
|
1650
|
+
direction,
|
|
1921
1651
|
});
|
|
1922
|
-
|
|
1923
|
-
|
|
1652
|
+
ch.routeProxyByTarget.set(`${direction}:${targetHash}`, proxyReqId);
|
|
1653
|
+
void this._sendControlMany(unique, this.codec.encodeRouteQuery(ch.id.key, proxyReqId, targetHash)).catch(() => {
|
|
1924
1654
|
this.completeRouteProxy(ch, proxyReqId);
|
|
1925
1655
|
});
|
|
1926
1656
|
}
|
|
@@ -1950,14 +1680,47 @@ export class FanoutTree extends DirectStream {
|
|
|
1950
1680
|
const timeoutMs = Math.max(1, Math.floor(options?.timeoutMs ?? 3_000));
|
|
1951
1681
|
return await new Promise((resolve, reject) => {
|
|
1952
1682
|
let settled = false;
|
|
1683
|
+
let proxyReqId;
|
|
1953
1684
|
const onAbort = () => {
|
|
1954
1685
|
if (settled)
|
|
1955
1686
|
return;
|
|
1956
1687
|
settled = true;
|
|
1957
|
-
|
|
1958
|
-
|
|
1688
|
+
clearTimeout(backstop);
|
|
1689
|
+
if (proxyReqId != null) {
|
|
1690
|
+
// Tear the search down only if nobody else coalesced onto it;
|
|
1691
|
+
// otherwise let it finish for the waiters (our localResolve
|
|
1692
|
+
// is a no-op once settled).
|
|
1693
|
+
const entry = ch.pendingRouteProxy.get(proxyReqId);
|
|
1694
|
+
if (entry && (!entry.waiters || entry.waiters.length === 0)) {
|
|
1695
|
+
this.completeRouteProxy(ch, proxyReqId);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1959
1698
|
reject(new AbortError());
|
|
1960
1699
|
};
|
|
1700
|
+
const localResolve = (route) => {
|
|
1701
|
+
if (settled)
|
|
1702
|
+
return;
|
|
1703
|
+
settled = true;
|
|
1704
|
+
clearTimeout(backstop);
|
|
1705
|
+
if (options?.signal) {
|
|
1706
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
1707
|
+
}
|
|
1708
|
+
if (this.isRouteValidForChannel(ch, route)) {
|
|
1709
|
+
this.cacheRoute(ch, route);
|
|
1710
|
+
resolve([...route]);
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
resolve(undefined);
|
|
1714
|
+
};
|
|
1715
|
+
// Per-caller timeout: when coalesced, the shared in-flight search
|
|
1716
|
+
// may outlive this caller's own deadline.
|
|
1717
|
+
const backstop = setTimeout(() => localResolve(undefined), timeoutMs);
|
|
1718
|
+
if (options?.signal) {
|
|
1719
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1720
|
+
}
|
|
1721
|
+
if (this.coalesceRouteProxy(ch, targetHash, "down", { localResolve })) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1961
1724
|
const unique = [];
|
|
1962
1725
|
const seen = new Set();
|
|
1963
1726
|
for (const candidate of candidates) {
|
|
@@ -1970,42 +1733,32 @@ export class FanoutTree extends DirectStream {
|
|
|
1970
1733
|
unique.push(candidate);
|
|
1971
1734
|
}
|
|
1972
1735
|
if (unique.length === 0) {
|
|
1973
|
-
|
|
1736
|
+
localResolve(undefined);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (ch.pendingRouteProxy.size >= ROUTE_PROXY_MAX_PENDING) {
|
|
1740
|
+
ch.metrics.routeProxyRejected += 1;
|
|
1741
|
+
localResolve(undefined);
|
|
1974
1742
|
return;
|
|
1975
1743
|
}
|
|
1976
1744
|
ch.metrics.routeProxyQueries += 1;
|
|
1977
1745
|
ch.metrics.routeProxyFanout += unique.length;
|
|
1978
|
-
|
|
1746
|
+
proxyReqId = this.nextReqId(ch);
|
|
1979
1747
|
const timer = setTimeout(() => {
|
|
1980
1748
|
ch.metrics.routeProxyTimeouts += 1;
|
|
1981
1749
|
this.completeRouteProxy(ch, proxyReqId);
|
|
1982
1750
|
}, timeoutMs);
|
|
1983
|
-
if (options?.signal) {
|
|
1984
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1985
|
-
}
|
|
1986
1751
|
ch.pendingRouteProxy.set(proxyReqId, {
|
|
1987
1752
|
requester: this.publicKeyHash,
|
|
1988
1753
|
downstreamReqId: 0,
|
|
1989
1754
|
timer,
|
|
1990
1755
|
expectedReplies: new Set(unique),
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
settled = true;
|
|
1995
|
-
clearTimeout(timer);
|
|
1996
|
-
if (options?.signal) {
|
|
1997
|
-
options.signal.removeEventListener("abort", onAbort);
|
|
1998
|
-
}
|
|
1999
|
-
if (this.isRouteValidForChannel(ch, route)) {
|
|
2000
|
-
this.cacheRoute(ch, route);
|
|
2001
|
-
resolve([...route]);
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
resolve(undefined);
|
|
2005
|
-
},
|
|
1756
|
+
targetHash,
|
|
1757
|
+
direction: "down",
|
|
1758
|
+
localResolve,
|
|
2006
1759
|
});
|
|
2007
|
-
|
|
2008
|
-
|
|
1760
|
+
ch.routeProxyByTarget.set(`down:${targetHash}`, proxyReqId);
|
|
1761
|
+
void this._sendControlMany(unique, this.codec.encodeRouteQuery(ch.id.key, proxyReqId, targetHash)).catch(() => {
|
|
2009
1762
|
this.completeRouteProxy(ch, proxyReqId);
|
|
2010
1763
|
});
|
|
2011
1764
|
});
|
|
@@ -2052,8 +1805,7 @@ export class FanoutTree extends DirectStream {
|
|
|
2052
1805
|
resolve(undefined);
|
|
2053
1806
|
},
|
|
2054
1807
|
});
|
|
2055
|
-
void this._sendControl(ch.parent, encodeRouteQuery(ch.id.key, reqId, targetHash))
|
|
2056
|
-
.catch((error) => {
|
|
1808
|
+
void this._sendControl(ch.parent, this.codec.encodeRouteQuery(ch.id.key, reqId, targetHash)).catch((error) => {
|
|
2057
1809
|
if (settled)
|
|
2058
1810
|
return;
|
|
2059
1811
|
settled = true;
|
|
@@ -2101,7 +1853,7 @@ export class FanoutTree extends DirectStream {
|
|
|
2101
1853
|
throw new Error("Unicast route token target mismatch");
|
|
2102
1854
|
}
|
|
2103
1855
|
if (target === this.publicKeyHash) {
|
|
2104
|
-
const wire = encodeUnicast(ch.id.key, toRoute, payload);
|
|
1856
|
+
const wire = this.codec.encodeUnicast(ch.id.key, toRoute, payload);
|
|
2105
1857
|
const message = await this.createMessage(wire, {
|
|
2106
1858
|
mode: new AnyWhere(),
|
|
2107
1859
|
priority: CONTROL_PRIORITY,
|
|
@@ -2137,7 +1889,7 @@ export class FanoutTree extends DirectStream {
|
|
|
2137
1889
|
if (replyRoute[replyRoute.length - 1] !== selfHash) {
|
|
2138
1890
|
throw new Error("Cannot unicast with ACK: self route token target mismatch");
|
|
2139
1891
|
}
|
|
2140
|
-
const data = encodeUnicast(ch.id.key, toRoute, payload, {
|
|
1892
|
+
const data = this.codec.encodeUnicast(ch.id.key, toRoute, payload, {
|
|
2141
1893
|
ackToken,
|
|
2142
1894
|
replyRoute,
|
|
2143
1895
|
});
|
|
@@ -2231,7 +1983,7 @@ export class FanoutTree extends DirectStream {
|
|
|
2231
1983
|
if (toRoute[0] !== ch.id.root) {
|
|
2232
1984
|
throw new Error("Unicast route token root mismatch");
|
|
2233
1985
|
}
|
|
2234
|
-
const data = encodeUnicast(ch.id.key, toRoute, payload);
|
|
1986
|
+
const data = this.codec.encodeUnicast(ch.id.key, toRoute, payload);
|
|
2235
1987
|
if (ch.isRoot) {
|
|
2236
1988
|
const target = toRoute[toRoute.length - 1];
|
|
2237
1989
|
if (target === this.publicKeyHash) {
|
|
@@ -2314,7 +2066,7 @@ export class FanoutTree extends DirectStream {
|
|
|
2314
2066
|
if (!ch.parent) {
|
|
2315
2067
|
throw new Error(`Cannot proxy publish while not attached to a parent (topic=${topic} root=${root} self=${this.publicKeyHash})`);
|
|
2316
2068
|
}
|
|
2317
|
-
await this._sendControl(ch.parent, encodePublishProxy(ch.id.key, payload));
|
|
2069
|
+
await this._sendControl(ch.parent, this.codec.encodePublishProxy(ch.id.key, payload));
|
|
2318
2070
|
}
|
|
2319
2071
|
async publishToChannelMaybe(topic, root, payload) {
|
|
2320
2072
|
const id = this.getChannelId(topic, root);
|
|
@@ -2412,7 +2164,8 @@ export class FanoutTree extends DirectStream {
|
|
|
2412
2164
|
return;
|
|
2413
2165
|
if (!ch.isRoot)
|
|
2414
2166
|
return;
|
|
2415
|
-
|
|
2167
|
+
ch.endSeqExclusive = Math.max(ch.endSeqExclusive, lastSeqExclusive);
|
|
2168
|
+
await this._sendControlMany([...ch.children.keys()], this.codec.encodeEnd(ch.id.key, lastSeqExclusive));
|
|
2416
2169
|
}
|
|
2417
2170
|
makeDataId(ch, seq) {
|
|
2418
2171
|
const id = new Uint8Array(32);
|
|
@@ -2497,6 +2250,14 @@ export class FanoutTree extends DirectStream {
|
|
|
2497
2250
|
m.trackerFeedbackSent += transmissions;
|
|
2498
2251
|
m.controlBytesSentTracker += sentBytes;
|
|
2499
2252
|
break;
|
|
2253
|
+
case MSG_PARENT_PROBE_REQ:
|
|
2254
|
+
m.parentProbeReqSent += transmissions;
|
|
2255
|
+
m.controlBytesSentJoin += sentBytes;
|
|
2256
|
+
break;
|
|
2257
|
+
case MSG_PARENT_PROBE_REPLY:
|
|
2258
|
+
m.parentProbeReplySent += transmissions;
|
|
2259
|
+
m.controlBytesSentJoin += sentBytes;
|
|
2260
|
+
break;
|
|
2500
2261
|
case MSG_PROVIDER_ANNOUNCE:
|
|
2501
2262
|
case MSG_PROVIDER_QUERY:
|
|
2502
2263
|
case MSG_PROVIDER_REPLY:
|
|
@@ -2562,6 +2323,14 @@ export class FanoutTree extends DirectStream {
|
|
|
2562
2323
|
m.trackerFeedbackReceived += 1;
|
|
2563
2324
|
m.controlBytesReceivedTracker += bytesReceived;
|
|
2564
2325
|
break;
|
|
2326
|
+
case MSG_PARENT_PROBE_REQ:
|
|
2327
|
+
m.parentProbeReqReceived += 1;
|
|
2328
|
+
m.controlBytesReceivedJoin += bytesReceived;
|
|
2329
|
+
break;
|
|
2330
|
+
case MSG_PARENT_PROBE_REPLY:
|
|
2331
|
+
m.parentProbeReplyReceived += 1;
|
|
2332
|
+
m.controlBytesReceivedJoin += bytesReceived;
|
|
2333
|
+
break;
|
|
2565
2334
|
case MSG_PROVIDER_ANNOUNCE:
|
|
2566
2335
|
case MSG_PROVIDER_QUERY:
|
|
2567
2336
|
case MSG_PROVIDER_REPLY:
|
|
@@ -2590,6 +2359,38 @@ export class FanoutTree extends DirectStream {
|
|
|
2590
2359
|
return;
|
|
2591
2360
|
return ch.cachePayloads[idx];
|
|
2592
2361
|
}
|
|
2362
|
+
noteJoinResponse(peerHash) {
|
|
2363
|
+
this.joinTimeoutStreakByPeer.delete(peerHash);
|
|
2364
|
+
}
|
|
2365
|
+
noteJoinTimeout(ch, peerHash) {
|
|
2366
|
+
const streak = (this.joinTimeoutStreakByPeer.get(peerHash) ?? 0) + 1;
|
|
2367
|
+
if (streak < JOIN_TIMEOUT_RESET_STREAK_THRESHOLD) {
|
|
2368
|
+
this.joinTimeoutStreakByPeer.set(peerHash, streak);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
this.joinTimeoutStreakByPeer.delete(peerHash);
|
|
2372
|
+
const now = Date.now();
|
|
2373
|
+
for (const [hash, until] of this.joinResetCooldownUntilByPeer) {
|
|
2374
|
+
if (until <= now)
|
|
2375
|
+
this.joinResetCooldownUntilByPeer.delete(hash);
|
|
2376
|
+
}
|
|
2377
|
+
const resetCooldownUntil = this.joinResetCooldownUntilByPeer.get(peerHash) ?? 0;
|
|
2378
|
+
if (resetCooldownUntil > now)
|
|
2379
|
+
return;
|
|
2380
|
+
const stream = this.peers.get(peerHash);
|
|
2381
|
+
if (!stream)
|
|
2382
|
+
return;
|
|
2383
|
+
this.joinResetCooldownUntilByPeer.set(peerHash, now + JOIN_TIMEOUT_RESET_COOLDOWN_MS);
|
|
2384
|
+
ch.metrics.joinPeerResets += 1;
|
|
2385
|
+
try {
|
|
2386
|
+
void this.components.connectionManager
|
|
2387
|
+
.closeConnections(stream.peerId)
|
|
2388
|
+
.catch(() => { });
|
|
2389
|
+
}
|
|
2390
|
+
catch {
|
|
2391
|
+
// Best-effort reset. The join loop keeps retrying other candidates.
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2593
2394
|
async _sendControl(to, bytes) {
|
|
2594
2395
|
const stream = this.peers.get(to);
|
|
2595
2396
|
if (!stream)
|
|
@@ -2603,18 +2404,18 @@ export class FanoutTree extends DirectStream {
|
|
|
2603
2404
|
}
|
|
2604
2405
|
async _sendControlMany(to, bytes) {
|
|
2605
2406
|
if (to.length === 0)
|
|
2606
|
-
return;
|
|
2407
|
+
return true;
|
|
2607
2408
|
const streams = to
|
|
2608
2409
|
.map((t) => this.peers.get(t))
|
|
2609
2410
|
.filter((s) => Boolean(s));
|
|
2610
2411
|
if (streams.length === 0)
|
|
2611
|
-
return;
|
|
2412
|
+
return false;
|
|
2612
2413
|
this.recordControlSend(bytes, streams.length);
|
|
2613
2414
|
const message = await this.createMessage(bytes, {
|
|
2614
2415
|
mode: new AnyWhere(),
|
|
2615
2416
|
priority: CONTROL_PRIORITY,
|
|
2616
2417
|
});
|
|
2617
|
-
|
|
2418
|
+
return this.publishMessageMaybe(this.publicKey, message, streams);
|
|
2618
2419
|
}
|
|
2619
2420
|
refillUploadTokens(ch, now = Date.now()) {
|
|
2620
2421
|
if (ch.uploadLimitBps <= 0)
|
|
@@ -2636,7 +2437,8 @@ export class FanoutTree extends DirectStream {
|
|
|
2636
2437
|
if (elapsedMs <= 0)
|
|
2637
2438
|
return;
|
|
2638
2439
|
ch.neighborRepairLastRefillAt = now;
|
|
2639
|
-
ch.neighborRepairTokens = Math.min(ch.neighborRepairTokenCapacity, ch.neighborRepairTokens +
|
|
2440
|
+
ch.neighborRepairTokens = Math.min(ch.neighborRepairTokenCapacity, ch.neighborRepairTokens +
|
|
2441
|
+
(elapsedMs * ch.neighborRepairBudgetBps) / 1_000);
|
|
2640
2442
|
}
|
|
2641
2443
|
takeIngressBudget(ch, kind, fromHash, costBytes, now = Date.now()) {
|
|
2642
2444
|
if (!fromHash)
|
|
@@ -2673,7 +2475,7 @@ export class FanoutTree extends DirectStream {
|
|
|
2673
2475
|
async _sendData(ch, to, seq, payload) {
|
|
2674
2476
|
this.markCached(ch, seq, payload);
|
|
2675
2477
|
ch.maxSeqSeen = Math.max(ch.maxSeqSeen, seq);
|
|
2676
|
-
const framed = encodeData(payload);
|
|
2478
|
+
const framed = this.codec.encodeData(payload);
|
|
2677
2479
|
const message = await this.createMessage(framed, {
|
|
2678
2480
|
mode: new AnyWhere(),
|
|
2679
2481
|
priority: DATA_PRIORITY,
|
|
@@ -2742,14 +2544,12 @@ export class FanoutTree extends DirectStream {
|
|
|
2742
2544
|
});
|
|
2743
2545
|
const kickCount = Math.min(OVERLOAD_KICK_MAX_PER_EVENT, droppedCandidates.length);
|
|
2744
2546
|
if (kickCount > 0) {
|
|
2745
|
-
const toKick = droppedCandidates
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
ch.dataWriteFailStreakByChild.delete(h);
|
|
2749
|
-
}
|
|
2547
|
+
const toKick = droppedCandidates
|
|
2548
|
+
.slice(0, kickCount)
|
|
2549
|
+
.map((c) => c.hash);
|
|
2750
2550
|
ch.overloadStreak = 0;
|
|
2751
2551
|
ch.lastOverloadKickAt = now;
|
|
2752
|
-
void this.
|
|
2552
|
+
void this.kickChildHashes(ch, toKick).catch(() => { });
|
|
2753
2553
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
2754
2554
|
}
|
|
2755
2555
|
}
|
|
@@ -2778,13 +2578,6 @@ export class FanoutTree extends DirectStream {
|
|
|
2778
2578
|
const stream = c.stream;
|
|
2779
2579
|
if (!stream.isWritable) {
|
|
2780
2580
|
writeDrops += 1;
|
|
2781
|
-
if (ch.children.has(c.hash)) {
|
|
2782
|
-
const streak = (ch.dataWriteFailStreakByChild.get(c.hash) ?? 0) + 1;
|
|
2783
|
-
ch.dataWriteFailStreakByChild.set(c.hash, streak);
|
|
2784
|
-
if (streak >= DATA_WRITE_FAIL_KICK_STREAK_THRESHOLD) {
|
|
2785
|
-
writeFailKickCandidates.push(c.hash);
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
2581
|
continue;
|
|
2789
2582
|
}
|
|
2790
2583
|
try {
|
|
@@ -2831,12 +2624,8 @@ export class FanoutTree extends DirectStream {
|
|
|
2831
2624
|
const kickCount = Math.min(DATA_WRITE_FAIL_KICK_MAX_PER_EVENT, unique.length);
|
|
2832
2625
|
if (kickCount > 0) {
|
|
2833
2626
|
const toKick = unique.slice(0, kickCount);
|
|
2834
|
-
for (const h of toKick) {
|
|
2835
|
-
ch.children.delete(h);
|
|
2836
|
-
ch.dataWriteFailStreakByChild.delete(h);
|
|
2837
|
-
}
|
|
2838
2627
|
ch.lastDataWriteFailKickAt = now;
|
|
2839
|
-
void this.
|
|
2628
|
+
void this.kickChildHashes(ch, toKick, { resetPeerConnections: true }).catch(() => { });
|
|
2840
2629
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
2841
2630
|
}
|
|
2842
2631
|
}
|
|
@@ -2915,14 +2704,12 @@ export class FanoutTree extends DirectStream {
|
|
|
2915
2704
|
});
|
|
2916
2705
|
const kickCount = Math.min(OVERLOAD_KICK_MAX_PER_EVENT, droppedCandidates.length);
|
|
2917
2706
|
if (kickCount > 0) {
|
|
2918
|
-
const toKick = droppedCandidates
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
ch.dataWriteFailStreakByChild.delete(h);
|
|
2922
|
-
}
|
|
2707
|
+
const toKick = droppedCandidates
|
|
2708
|
+
.slice(0, kickCount)
|
|
2709
|
+
.map((c) => c.hash);
|
|
2923
2710
|
ch.overloadStreak = 0;
|
|
2924
2711
|
ch.lastOverloadKickAt = now;
|
|
2925
|
-
void this.
|
|
2712
|
+
void this.kickChildHashes(ch, toKick).catch(() => { });
|
|
2926
2713
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
2927
2714
|
}
|
|
2928
2715
|
}
|
|
@@ -2940,11 +2727,6 @@ export class FanoutTree extends DirectStream {
|
|
|
2940
2727
|
const stream = c.stream;
|
|
2941
2728
|
if (!stream.isWritable) {
|
|
2942
2729
|
writeDrops += 1;
|
|
2943
|
-
const streak = (ch.dataWriteFailStreakByChild.get(c.hash) ?? 0) + 1;
|
|
2944
|
-
ch.dataWriteFailStreakByChild.set(c.hash, streak);
|
|
2945
|
-
if (streak >= DATA_WRITE_FAIL_KICK_STREAK_THRESHOLD) {
|
|
2946
|
-
writeFailKickCandidates.push(c.hash);
|
|
2947
|
-
}
|
|
2948
2730
|
continue;
|
|
2949
2731
|
}
|
|
2950
2732
|
try {
|
|
@@ -2989,12 +2771,8 @@ export class FanoutTree extends DirectStream {
|
|
|
2989
2771
|
const kickCount = Math.min(DATA_WRITE_FAIL_KICK_MAX_PER_EVENT, unique.length);
|
|
2990
2772
|
if (kickCount > 0) {
|
|
2991
2773
|
const toKick = unique.slice(0, kickCount);
|
|
2992
|
-
for (const h of toKick) {
|
|
2993
|
-
ch.children.delete(h);
|
|
2994
|
-
ch.dataWriteFailStreakByChild.delete(h);
|
|
2995
|
-
}
|
|
2996
2774
|
ch.lastDataWriteFailKickAt = now;
|
|
2997
|
-
void this.
|
|
2775
|
+
void this.kickChildHashes(ch, toKick, { resetPeerConnections: true }).catch(() => { });
|
|
2998
2776
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
2999
2777
|
}
|
|
3000
2778
|
}
|
|
@@ -3002,6 +2780,7 @@ export class FanoutTree extends DirectStream {
|
|
|
3002
2780
|
noteReceivedSeq(ch, fromHash, seq) {
|
|
3003
2781
|
if (!ch.repairEnabled)
|
|
3004
2782
|
return;
|
|
2783
|
+
ch.repairRequestedAtBySeq.delete(seq);
|
|
3005
2784
|
if (!ch.parent || fromHash !== ch.parent) {
|
|
3006
2785
|
// Neighbor-assisted repair may deliver payload from a peer other than the
|
|
3007
2786
|
// current parent; treat it as a "hole fill" only.
|
|
@@ -3025,7 +2804,9 @@ export class FanoutTree extends DirectStream {
|
|
|
3025
2804
|
}
|
|
3026
2805
|
if (seq >= ch.nextExpectedSeq) {
|
|
3027
2806
|
const maxBackfill = Math.max(0, ch.repairMaxBackfillMessages);
|
|
3028
|
-
const start = maxBackfill > 0
|
|
2807
|
+
const start = maxBackfill > 0
|
|
2808
|
+
? Math.max(ch.nextExpectedSeq, Math.max(0, seq - maxBackfill))
|
|
2809
|
+
: ch.nextExpectedSeq;
|
|
3029
2810
|
for (let s = start; s < seq; s++)
|
|
3030
2811
|
ch.missingSeqs.add(s);
|
|
3031
2812
|
ch.nextExpectedSeq = seq + 1;
|
|
@@ -3072,21 +2853,32 @@ export class FanoutTree extends DirectStream {
|
|
|
3072
2853
|
ch.missingSeqs.delete(s);
|
|
3073
2854
|
}
|
|
3074
2855
|
}
|
|
2856
|
+
for (const s of ch.repairRequestedAtBySeq.keys()) {
|
|
2857
|
+
if (!ch.missingSeqs.has(s))
|
|
2858
|
+
ch.repairRequestedAtBySeq.delete(s);
|
|
2859
|
+
}
|
|
3075
2860
|
if (ch.missingSeqs.size === 0)
|
|
3076
2861
|
return false;
|
|
3077
|
-
if (ch.repairIntervalMs > 0 &&
|
|
2862
|
+
if (ch.repairIntervalMs > 0 &&
|
|
2863
|
+
now - ch.lastRepairSentAt < ch.repairIntervalMs) {
|
|
3078
2864
|
return false;
|
|
3079
2865
|
}
|
|
3080
|
-
ch.
|
|
3081
|
-
const missing = [...ch.missingSeqs]
|
|
2866
|
+
const repairRetryMs = Math.max(REPAIR_RETRY_MIN_MS, ch.repairIntervalMs * REPAIR_RETRY_INTERVAL_FACTOR);
|
|
2867
|
+
const missing = [...ch.missingSeqs]
|
|
2868
|
+
.filter((s) => now - (ch.repairRequestedAtBySeq.get(s) ?? 0) >= repairRetryMs)
|
|
2869
|
+
.sort((a, b) => a - b);
|
|
3082
2870
|
const count = Math.min(ch.repairMaxPerReq, missing.length, 255);
|
|
3083
2871
|
if (count <= 0)
|
|
3084
2872
|
return false;
|
|
2873
|
+
ch.lastRepairSentAt = now;
|
|
3085
2874
|
const reqId = (this.random() * 0xffffffff) >>> 0;
|
|
3086
2875
|
const slice = missing.slice(0, count);
|
|
2876
|
+
for (const s of slice)
|
|
2877
|
+
ch.repairRequestedAtBySeq.set(s, now);
|
|
3087
2878
|
let sent = false;
|
|
3088
2879
|
if (ch.parent) {
|
|
3089
|
-
await this._sendControl(ch.parent, encodeRepairReq(ch.id.key, reqId, slice));
|
|
2880
|
+
await this._sendControl(ch.parent, this.codec.encodeRepairReq(ch.id.key, reqId, slice));
|
|
2881
|
+
ch.parentRepairUnansweredStreak += 1;
|
|
3090
2882
|
sent = true;
|
|
3091
2883
|
}
|
|
3092
2884
|
if (ch.neighborRepair && ch.neighborRepairPeers > 0) {
|
|
@@ -3136,35 +2928,25 @@ export class FanoutTree extends DirectStream {
|
|
|
3136
2928
|
updatedAt,
|
|
3137
2929
|
};
|
|
3138
2930
|
});
|
|
3139
|
-
scored.
|
|
2931
|
+
const viable = scored.filter((s) => s.coverage > 0);
|
|
2932
|
+
viable.sort((a, b) => {
|
|
3140
2933
|
if (a.coverage !== b.coverage)
|
|
3141
2934
|
return b.coverage - a.coverage;
|
|
3142
|
-
if (a.
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
return bl - al;
|
|
3151
|
-
}
|
|
3152
|
-
else {
|
|
3153
|
-
const al = ch.lazyPeers.has(a.peerHash) ? 1 : 0;
|
|
3154
|
-
const bl = ch.lazyPeers.has(b.peerHash) ? 1 : 0;
|
|
3155
|
-
if (al !== bl)
|
|
3156
|
-
return bl - al;
|
|
3157
|
-
if (a.successRate !== b.successRate)
|
|
3158
|
-
return b.successRate - a.successRate;
|
|
3159
|
-
if (a.updatedAt !== b.updatedAt)
|
|
3160
|
-
return b.updatedAt - a.updatedAt;
|
|
3161
|
-
}
|
|
2935
|
+
if (a.successRate !== b.successRate)
|
|
2936
|
+
return b.successRate - a.successRate;
|
|
2937
|
+
if (a.updatedAt !== b.updatedAt)
|
|
2938
|
+
return b.updatedAt - a.updatedAt;
|
|
2939
|
+
const al = ch.lazyPeers.has(a.peerHash) ? 1 : 0;
|
|
2940
|
+
const bl = ch.lazyPeers.has(b.peerHash) ? 1 : 0;
|
|
2941
|
+
if (al !== bl)
|
|
2942
|
+
return bl - al;
|
|
3162
2943
|
return a.peerHash < b.peerHash ? -1 : a.peerHash > b.peerHash ? 1 : 0;
|
|
3163
2944
|
});
|
|
3164
|
-
let peers =
|
|
2945
|
+
let peers = viable.slice(0, ch.neighborRepairPeers).map((s) => s.peerHash);
|
|
3165
2946
|
if (peers.length > 0) {
|
|
3166
|
-
const bytes = encodeFetchReq(ch.id.key, reqId, slice);
|
|
3167
|
-
if (ch.neighborRepairBudgetBps > 0 &&
|
|
2947
|
+
const bytes = this.codec.encodeFetchReq(ch.id.key, reqId, slice);
|
|
2948
|
+
if (ch.neighborRepairBudgetBps > 0 &&
|
|
2949
|
+
ch.neighborRepairTokenCapacity > 0) {
|
|
3168
2950
|
this.refillNeighborRepairTokens(ch, now);
|
|
3169
2951
|
const costPerPeer = Math.max(1, bytes.byteLength);
|
|
3170
2952
|
const affordable = Math.floor(ch.neighborRepairTokens / costPerPeer);
|
|
@@ -3269,13 +3051,33 @@ export class FanoutTree extends DirectStream {
|
|
|
3269
3051
|
const bootstraps = this.getBootstrapsForChannel(ch);
|
|
3270
3052
|
if (bootstraps.length === 0)
|
|
3271
3053
|
return;
|
|
3272
|
-
|
|
3054
|
+
// Announce to ALL trackers, not a random one per round: with B bootstraps
|
|
3055
|
+
// and per-round random selection a given directory only refreshes an
|
|
3056
|
+
// entry every ~announceIntervalMs * B on average, which starves
|
|
3057
|
+
// directories (and exceeds the announce TTL entirely once B grows).
|
|
3058
|
+
// Use already-connected tracker peers and only fall back to dialing when
|
|
3059
|
+
// none are connected: serial dial timeouts here would otherwise make
|
|
3060
|
+
// every announce tick glacial.
|
|
3061
|
+
let peers = ch.cachedAnnounceTrackerPeers ?? [];
|
|
3062
|
+
peers = peers.filter((h) => Boolean(this.peers.get(h)));
|
|
3063
|
+
if (peers.length === 0) {
|
|
3064
|
+
// Dial only when no tracker is connected at all: dialing every tick
|
|
3065
|
+
// makes the announce loop glacial and starves everything behind it.
|
|
3066
|
+
peers = await this.ensureBootstrapPeers(bootstraps, ch.bootstrapDialTimeoutMs, signal, 0);
|
|
3067
|
+
}
|
|
3068
|
+
ch.cachedAnnounceTrackerPeers = peers;
|
|
3273
3069
|
if (peers.length === 0)
|
|
3274
3070
|
return;
|
|
3275
3071
|
this.pruneDisconnectedChildren(ch);
|
|
3276
|
-
const
|
|
3072
|
+
const parentUpgradeNoCapacity = ch.isRoot && ch.parentUpgradeTrackerNoCapacityUntil > now;
|
|
3073
|
+
const freeSlots = parentUpgradeNoCapacity
|
|
3074
|
+
? 0
|
|
3075
|
+
: Math.max(0, ch.effectiveMaxChildren - ch.children.size);
|
|
3076
|
+
const announceTtlMs = parentUpgradeNoCapacity
|
|
3077
|
+
? Math.max(1_000, Math.min(ch.announceTtlMs, ch.parentUpgradeTrackerNoCapacityUntil - now))
|
|
3078
|
+
: ch.announceTtlMs;
|
|
3277
3079
|
const addrs = this.getSelfAnnounceAddrs();
|
|
3278
|
-
const bytes = encodeTrackerAnnounce(ch.id.key,
|
|
3080
|
+
const bytes = this.codec.encodeTrackerAnnounce(ch.id.key, announceTtlMs, Number.isFinite(ch.level) ? ch.level : 0xffff, ch.effectiveMaxChildren, freeSlots, ch.bidPerByte, addrs);
|
|
3279
3081
|
await this._sendControlMany(peers, bytes);
|
|
3280
3082
|
}
|
|
3281
3083
|
async _announceLoop(ch) {
|
|
@@ -3289,6 +3091,18 @@ export class FanoutTree extends DirectStream {
|
|
|
3289
3091
|
catch {
|
|
3290
3092
|
// ignore
|
|
3291
3093
|
}
|
|
3094
|
+
try {
|
|
3095
|
+
// Child watermark heartbeat first: it only writes to already
|
|
3096
|
+
// connected peers (children/mesh) and must not starve behind the
|
|
3097
|
+
// dial-bound tracker announce below. Parents (root included - it
|
|
3098
|
+
// runs no repair/mesh loop) advertise their have-range to children
|
|
3099
|
+
// so a child whose data writes were silently lost can detect the
|
|
3100
|
+
// gap and repair.
|
|
3101
|
+
await this.maybeSendIHave(ch, Date.now());
|
|
3102
|
+
}
|
|
3103
|
+
catch {
|
|
3104
|
+
// ignore
|
|
3105
|
+
}
|
|
3292
3106
|
try {
|
|
3293
3107
|
await this.announceToTrackers(ch, signal);
|
|
3294
3108
|
}
|
|
@@ -3408,27 +3222,62 @@ export class FanoutTree extends DirectStream {
|
|
|
3408
3222
|
return { haveFrom, haveToExclusive };
|
|
3409
3223
|
}
|
|
3410
3224
|
async maybeSendIHave(ch, now = Date.now()) {
|
|
3411
|
-
|
|
3225
|
+
const meshEnabled = ch.neighborRepair && ch.neighborMeshPeers > 0;
|
|
3226
|
+
const childWatermarkEnabled = ch.repairEnabled && ch.children.size > 0;
|
|
3227
|
+
if (!meshEnabled && !childWatermarkEnabled) {
|
|
3412
3228
|
return;
|
|
3413
|
-
|
|
3229
|
+
}
|
|
3230
|
+
if (ch.neighborAnnounceIntervalMs <= 0) {
|
|
3414
3231
|
return;
|
|
3415
|
-
|
|
3232
|
+
}
|
|
3233
|
+
if (now - ch.lastIHaveSentAt < ch.neighborAnnounceIntervalMs) {
|
|
3416
3234
|
return;
|
|
3417
|
-
|
|
3235
|
+
}
|
|
3236
|
+
const hasNewData = ch.maxSeqSeen > ch.lastIHaveSentMaxSeq;
|
|
3237
|
+
// Children also need a periodic watermark even without new data: a burst
|
|
3238
|
+
// of data writes can be lost on a not-yet-writable stream and a child
|
|
3239
|
+
// that received nothing has no gap signal to trigger repair. A periodic
|
|
3240
|
+
// IHAVE from the parent converts that silent loss into a repairable gap.
|
|
3241
|
+
const heartbeatDue = childWatermarkEnabled &&
|
|
3242
|
+
(ch.maxSeqSeen >= 0 || ch.endSeqExclusive > 0) &&
|
|
3243
|
+
now - ch.lastIHaveSentAt >=
|
|
3244
|
+
Math.max(ch.neighborAnnounceIntervalMs * 4, 2_000);
|
|
3245
|
+
if (!hasNewData && !heartbeatDue) {
|
|
3418
3246
|
return;
|
|
3419
|
-
|
|
3247
|
+
}
|
|
3248
|
+
const recipients = new Set();
|
|
3249
|
+
if (meshEnabled) {
|
|
3250
|
+
for (const h of ch.lazyPeers) {
|
|
3251
|
+
if (this.peers.get(h))
|
|
3252
|
+
recipients.add(h);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
if (childWatermarkEnabled) {
|
|
3256
|
+
for (const h of ch.children.keys()) {
|
|
3257
|
+
if (this.peers.get(h))
|
|
3258
|
+
recipients.add(h);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (recipients.size === 0) {
|
|
3420
3262
|
return;
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
if (range.haveToExclusive
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3263
|
+
}
|
|
3264
|
+
let sent = false;
|
|
3265
|
+
const range = this.getHaveRange(ch);
|
|
3266
|
+
if (range && range.haveToExclusive > range.haveFrom) {
|
|
3267
|
+
await this._sendControlMany([...recipients], this.codec.encodeIHave(ch.id.key, range.haveFrom, range.haveToExclusive));
|
|
3268
|
+
sent = true;
|
|
3269
|
+
}
|
|
3270
|
+
if (childWatermarkEnabled && ch.endSeqExclusive > 0) {
|
|
3271
|
+
const children = [...ch.children.keys()].filter((h) => this.peers.get(h));
|
|
3272
|
+
if (children.length > 0) {
|
|
3273
|
+
await this._sendControlMany(children, this.codec.encodeEnd(ch.id.key, ch.endSeqExclusive));
|
|
3274
|
+
sent = true;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
if (sent) {
|
|
3278
|
+
ch.lastIHaveSentAt = now;
|
|
3279
|
+
ch.lastIHaveSentMaxSeq = ch.maxSeqSeen;
|
|
3280
|
+
}
|
|
3432
3281
|
}
|
|
3433
3282
|
async ensureMeshPeers(ch, signal) {
|
|
3434
3283
|
if (ch.neighborMeshPeers <= 0)
|
|
@@ -3471,6 +3320,10 @@ export class FanoutTree extends DirectStream {
|
|
|
3471
3320
|
}
|
|
3472
3321
|
}
|
|
3473
3322
|
async refreshMeshCandidates(ch, signal) {
|
|
3323
|
+
if (ch.neighborMeshPeers <= 0)
|
|
3324
|
+
return;
|
|
3325
|
+
if (ch.lazyPeers.size >= ch.neighborMeshPeers)
|
|
3326
|
+
return;
|
|
3474
3327
|
const bootstraps = this.getBootstrapsForChannel(ch);
|
|
3475
3328
|
if (bootstraps.length === 0)
|
|
3476
3329
|
return;
|
|
@@ -3497,18 +3350,23 @@ export class FanoutTree extends DirectStream {
|
|
|
3497
3350
|
try {
|
|
3498
3351
|
this.pruneLazyPeers(ch);
|
|
3499
3352
|
this.pruneHaveByPeer(ch, now);
|
|
3353
|
+
await this.ensureMeshPeers(ch, signal);
|
|
3500
3354
|
const refreshMs = Math.max(0, ch.neighborMeshRefreshIntervalMs);
|
|
3501
|
-
if (
|
|
3355
|
+
if (ch.lazyPeers.size < ch.neighborMeshPeers &&
|
|
3356
|
+
(refreshMs === 0 || now - lastRefreshAt >= refreshMs)) {
|
|
3502
3357
|
lastRefreshAt = now;
|
|
3503
3358
|
await this.refreshMeshCandidates(ch, signal);
|
|
3359
|
+
await this.ensureMeshPeers(ch, signal);
|
|
3504
3360
|
}
|
|
3505
|
-
await this.ensureMeshPeers(ch, signal);
|
|
3506
3361
|
await this.maybeSendIHave(ch, now);
|
|
3507
3362
|
}
|
|
3508
3363
|
catch {
|
|
3509
3364
|
// ignore
|
|
3510
3365
|
}
|
|
3511
|
-
const intervals = [
|
|
3366
|
+
const intervals = [
|
|
3367
|
+
ch.neighborAnnounceIntervalMs,
|
|
3368
|
+
ch.neighborMeshRefreshIntervalMs,
|
|
3369
|
+
].filter((v) => v > 0);
|
|
3512
3370
|
const sleepMs = intervals.length > 0 ? Math.min(...intervals) : 500;
|
|
3513
3371
|
await delay(Math.max(50, sleepMs));
|
|
3514
3372
|
}
|
|
@@ -3522,7 +3380,7 @@ export class FanoutTree extends DirectStream {
|
|
|
3522
3380
|
const p = new Promise((resolve) => {
|
|
3523
3381
|
ch.pendingTrackerQuery.set(reqId, { resolve });
|
|
3524
3382
|
});
|
|
3525
|
-
void this._sendControl(trackerHash, encodeTrackerQuery(ch.id.key, reqId, want));
|
|
3383
|
+
void this._sendControl(trackerHash, this.codec.encodeTrackerQuery(ch.id.key, reqId, want));
|
|
3526
3384
|
const res = await Promise.race([
|
|
3527
3385
|
p,
|
|
3528
3386
|
delay(perTrackerTimeout, { signal }).then(() => null),
|
|
@@ -3552,7 +3410,11 @@ export class FanoutTree extends DirectStream {
|
|
|
3552
3410
|
return false;
|
|
3553
3411
|
try {
|
|
3554
3412
|
await this.components.connectionManager.openConnection(a);
|
|
3555
|
-
await this.waitFor(hash, {
|
|
3413
|
+
await this.waitFor(hash, {
|
|
3414
|
+
seek: "present",
|
|
3415
|
+
timeout: timeoutMs,
|
|
3416
|
+
signal,
|
|
3417
|
+
});
|
|
3556
3418
|
return true;
|
|
3557
3419
|
}
|
|
3558
3420
|
catch {
|
|
@@ -3567,7 +3429,9 @@ export class FanoutTree extends DirectStream {
|
|
|
3567
3429
|
return [];
|
|
3568
3430
|
try {
|
|
3569
3431
|
const peer = await this.components.peerStore.get(stream.peerId);
|
|
3570
|
-
const addresses = Array.isArray(peer?.addresses)
|
|
3432
|
+
const addresses = Array.isArray(peer?.addresses)
|
|
3433
|
+
? peer.addresses
|
|
3434
|
+
: [];
|
|
3571
3435
|
const out = [];
|
|
3572
3436
|
for (const a of addresses) {
|
|
3573
3437
|
const ma = a?.multiaddr ?? a;
|
|
@@ -3634,7 +3498,10 @@ export class FanoutTree extends DirectStream {
|
|
|
3634
3498
|
if (out.length >= max)
|
|
3635
3499
|
break;
|
|
3636
3500
|
seen.add(e.hash);
|
|
3637
|
-
out.push({
|
|
3501
|
+
out.push({
|
|
3502
|
+
hash: e.hash,
|
|
3503
|
+
addrs: e.addrs.slice(0, JOIN_REJECT_REDIRECT_ADDR_MAX),
|
|
3504
|
+
});
|
|
3638
3505
|
}
|
|
3639
3506
|
}
|
|
3640
3507
|
if (out.length >= max)
|
|
@@ -3665,12 +3532,166 @@ export class FanoutTree extends DirectStream {
|
|
|
3665
3532
|
}
|
|
3666
3533
|
async sendJoinReject(ch, toHash, reqId, reason) {
|
|
3667
3534
|
const redirects = await this.pickJoinRejectRedirects(ch, toHash, JOIN_REJECT_REDIRECT_MAX);
|
|
3668
|
-
await this._sendControl(toHash, encodeJoinReject(ch.id.key, reqId, reason, redirects));
|
|
3535
|
+
await this._sendControl(toHash, this.codec.encodeJoinReject(ch.id.key, reqId, reason, redirects));
|
|
3669
3536
|
}
|
|
3670
3537
|
async sendTrackerFeedback(ch, trackerPeers, candidateHash, event, reason = 0) {
|
|
3671
3538
|
if (trackerPeers.length === 0)
|
|
3672
3539
|
return;
|
|
3673
|
-
await this._sendControlMany(trackerPeers, encodeTrackerFeedback(ch.id.key, candidateHash, event, reason));
|
|
3540
|
+
await this._sendControlMany(trackerPeers, this.codec.encodeTrackerFeedback(ch.id.key, candidateHash, event, reason));
|
|
3541
|
+
}
|
|
3542
|
+
pruneParentUpgradeReservations(ch, now = Date.now()) {
|
|
3543
|
+
for (const [hash, reservation] of ch.parentUpgradeReservationsByHash) {
|
|
3544
|
+
if (reservation.expiresAt <= now) {
|
|
3545
|
+
ch.parentUpgradeReservationsByHash.delete(hash);
|
|
3546
|
+
ch.metrics.parentUpgradeRootReservationExpired += 1;
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
if (ch.children.has(hash)) {
|
|
3550
|
+
ch.parentUpgradeReservationsByHash.delete(hash);
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
parentUpgradeReservationCount(ch, excludeHash, now = Date.now()) {
|
|
3555
|
+
this.pruneParentUpgradeReservations(ch, now);
|
|
3556
|
+
let count = 0;
|
|
3557
|
+
for (const hash of ch.parentUpgradeReservationsByHash.keys()) {
|
|
3558
|
+
if (hash === excludeHash)
|
|
3559
|
+
continue;
|
|
3560
|
+
if (ch.children.has(hash))
|
|
3561
|
+
continue;
|
|
3562
|
+
count += 1;
|
|
3563
|
+
}
|
|
3564
|
+
return count;
|
|
3565
|
+
}
|
|
3566
|
+
createParentUpgradeReservation(ch, toHash, minFreeSlots = 0, now = Date.now()) {
|
|
3567
|
+
if (!ch.isRoot || ch.children.has(toHash))
|
|
3568
|
+
return 0;
|
|
3569
|
+
const maxChildren = Math.max(0, Math.floor(ch.effectiveMaxChildren));
|
|
3570
|
+
if (maxChildren <= 0)
|
|
3571
|
+
return 0;
|
|
3572
|
+
const reserved = this.parentUpgradeReservationCount(ch, toHash, now);
|
|
3573
|
+
const requestedMinFreeSlots = Math.max(0, Math.floor(minFreeSlots));
|
|
3574
|
+
const freeSlots = maxChildren - ch.children.size - reserved;
|
|
3575
|
+
if (freeSlots <= 0 ||
|
|
3576
|
+
(requestedMinFreeSlots > 0 && freeSlots < requestedMinFreeSlots)) {
|
|
3577
|
+
ch.metrics.parentUpgradeRootReservationBlocked += 1;
|
|
3578
|
+
return 0;
|
|
3579
|
+
}
|
|
3580
|
+
const token = (this.random() * 0xffffffff) >>> 0 || 1;
|
|
3581
|
+
ch.parentUpgradeReservationsByHash.set(toHash, {
|
|
3582
|
+
token,
|
|
3583
|
+
expiresAt: now + PARENT_UPGRADE_ROOT_RESERVATION_TTL_MS,
|
|
3584
|
+
minFreeSlots: requestedMinFreeSlots,
|
|
3585
|
+
});
|
|
3586
|
+
ch.metrics.parentUpgradeRootReservationCreated += 1;
|
|
3587
|
+
return token;
|
|
3588
|
+
}
|
|
3589
|
+
consumeParentUpgradeReservation(ch, fromHash, token, now = Date.now()) {
|
|
3590
|
+
if (!ch.isRoot || token <= 0)
|
|
3591
|
+
return false;
|
|
3592
|
+
this.pruneParentUpgradeReservations(ch, now);
|
|
3593
|
+
const reservation = ch.parentUpgradeReservationsByHash.get(fromHash);
|
|
3594
|
+
if (!reservation)
|
|
3595
|
+
return false;
|
|
3596
|
+
if (reservation.expiresAt <= now) {
|
|
3597
|
+
ch.parentUpgradeReservationsByHash.delete(fromHash);
|
|
3598
|
+
ch.metrics.parentUpgradeRootReservationExpired += 1;
|
|
3599
|
+
return false;
|
|
3600
|
+
}
|
|
3601
|
+
if (reservation.token !== token)
|
|
3602
|
+
return false;
|
|
3603
|
+
const requestedMinFreeSlots = Math.max(0, Math.floor(reservation.minFreeSlots ?? 0));
|
|
3604
|
+
if (requestedMinFreeSlots > 0) {
|
|
3605
|
+
const maxChildren = Math.max(0, Math.floor(ch.effectiveMaxChildren));
|
|
3606
|
+
const freeSlots = Math.max(0, maxChildren - ch.children.size);
|
|
3607
|
+
if (freeSlots < requestedMinFreeSlots) {
|
|
3608
|
+
ch.parentUpgradeReservationsByHash.delete(fromHash);
|
|
3609
|
+
ch.metrics.parentUpgradeRootReservationMarginRejected += 1;
|
|
3610
|
+
return false;
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
ch.parentUpgradeReservationsByHash.delete(fromHash);
|
|
3614
|
+
ch.metrics.parentUpgradeRootReservationConsumed += 1;
|
|
3615
|
+
return true;
|
|
3616
|
+
}
|
|
3617
|
+
isRootedForParentProbe(ch) {
|
|
3618
|
+
if (ch.isRoot)
|
|
3619
|
+
return true;
|
|
3620
|
+
const route = ch.routeFromRoot;
|
|
3621
|
+
return Boolean(ch.parent &&
|
|
3622
|
+
Array.isArray(route) &&
|
|
3623
|
+
route.length >= 2 &&
|
|
3624
|
+
route[0] === ch.id.root &&
|
|
3625
|
+
route[route.length - 1] === this.publicKeyHash);
|
|
3626
|
+
}
|
|
3627
|
+
encodeParentProbeReplyForChannel(ch, reqId, toHash, minFreeSlots = 0, reserveRootCapacity = true) {
|
|
3628
|
+
this.pruneDisconnectedChildren(ch);
|
|
3629
|
+
const rooted = this.isRootedForParentProbe(ch);
|
|
3630
|
+
const requestedMinFreeSlots = Math.max(0, Math.floor(minFreeSlots));
|
|
3631
|
+
const maxChildren = Math.max(0, Math.floor(ch.effectiveMaxChildren));
|
|
3632
|
+
const reserved = ch.isRoot
|
|
3633
|
+
? this.parentUpgradeReservationCount(ch, toHash)
|
|
3634
|
+
: 0;
|
|
3635
|
+
const freeSlots = rooted
|
|
3636
|
+
? Math.max(0, maxChildren - ch.children.size - reserved)
|
|
3637
|
+
: 0;
|
|
3638
|
+
const requesterHasEnoughSlots = freeSlots > 0 &&
|
|
3639
|
+
(requestedMinFreeSlots <= 0 || freeSlots >= requestedMinFreeSlots);
|
|
3640
|
+
if (ch.isRoot &&
|
|
3641
|
+
rooted &&
|
|
3642
|
+
maxChildren > 0 &&
|
|
3643
|
+
!ch.children.has(toHash) &&
|
|
3644
|
+
!requesterHasEnoughSlots) {
|
|
3645
|
+
ch.metrics.parentUpgradeRootReservationBlocked += 1;
|
|
3646
|
+
}
|
|
3647
|
+
const reservationToken = ch.isRoot && rooted && requesterHasEnoughSlots && reserveRootCapacity
|
|
3648
|
+
? this.createParentUpgradeReservation(ch, toHash, requestedMinFreeSlots)
|
|
3649
|
+
: 0;
|
|
3650
|
+
const repairing = ch.missingSeqs.size > 0;
|
|
3651
|
+
const overloaded = ch.overloadStreak > 0;
|
|
3652
|
+
let flags = 0;
|
|
3653
|
+
if (rooted)
|
|
3654
|
+
flags |= PARENT_PROBE_FLAG_ROOTED;
|
|
3655
|
+
if (rooted &&
|
|
3656
|
+
(reservationToken > 0 ||
|
|
3657
|
+
(!ch.isRoot && requesterHasEnoughSlots) ||
|
|
3658
|
+
(ch.isRoot && requesterHasEnoughSlots && !reserveRootCapacity))) {
|
|
3659
|
+
flags |= PARENT_PROBE_FLAG_ACCEPTING;
|
|
3660
|
+
}
|
|
3661
|
+
if (repairing)
|
|
3662
|
+
flags |= PARENT_PROBE_FLAG_REPAIRING;
|
|
3663
|
+
if (overloaded)
|
|
3664
|
+
flags |= PARENT_PROBE_FLAG_OVERLOADED;
|
|
3665
|
+
return this.codec.encodeParentProbeReply(ch.id.key, reqId, {
|
|
3666
|
+
flags,
|
|
3667
|
+
level: Number.isFinite(ch.level) ? ch.level : 0xffff,
|
|
3668
|
+
maxChildren,
|
|
3669
|
+
freeSlots,
|
|
3670
|
+
// For roots, expose reserved slots as child pressure so concurrent
|
|
3671
|
+
// upgrade probes cannot all pass the load-ratio check from stale state.
|
|
3672
|
+
children: ch.isRoot ? ch.children.size + reserved : ch.children.size,
|
|
3673
|
+
haveToExclusive: ch.maxSeqSeen >= 0 ? ch.maxSeqSeen + 1 : 0,
|
|
3674
|
+
missingSeqs: ch.missingSeqs.size,
|
|
3675
|
+
dataWriteDrops: ch.metrics.dataWriteDrops,
|
|
3676
|
+
droppedForwards: ch.droppedForwards,
|
|
3677
|
+
reservationToken,
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
async probeParentCandidate(ch, parentHash, timeoutMs, signal, minFreeSlots = 0, reserveRootCapacity = true) {
|
|
3681
|
+
if (!this.peers.get(parentHash))
|
|
3682
|
+
return;
|
|
3683
|
+
const reqId = (this.random() * 0xffffffff) >>> 0;
|
|
3684
|
+
const p = new Promise((resolve) => {
|
|
3685
|
+
ch.pendingParentProbe.set(reqId, { resolve });
|
|
3686
|
+
});
|
|
3687
|
+
await this._sendControl(parentHash, this.codec.encodeParentProbeReq(ch.id.key, reqId, minFreeSlots, reserveRootCapacity));
|
|
3688
|
+
const res = await Promise.race([
|
|
3689
|
+
p,
|
|
3690
|
+
delay(Math.max(1, timeoutMs), { signal }).then(() => undefined),
|
|
3691
|
+
]);
|
|
3692
|
+
if (!res)
|
|
3693
|
+
ch.pendingParentProbe.delete(reqId);
|
|
3694
|
+
return res;
|
|
3674
3695
|
}
|
|
3675
3696
|
async _joinLoop(ch, joinOpts) {
|
|
3676
3697
|
const retryMs = Math.max(1, Math.floor(joinOpts.retryMs ?? 200));
|
|
@@ -3686,6 +3707,9 @@ export class FanoutTree extends DirectStream {
|
|
|
3686
3707
|
const trackerQueryIntervalMs = Math.max(0, Math.floor(joinOpts.trackerQueryIntervalMs ?? ch.trackerQueryIntervalMs));
|
|
3687
3708
|
const joinAttemptsPerRound = Math.max(1, Math.floor(joinOpts.joinAttemptsPerRound ?? 8));
|
|
3688
3709
|
const candidateCooldownMs = Math.max(0, Math.floor(joinOpts.candidateCooldownMs ?? 2_000));
|
|
3710
|
+
const parentUpgrade = this.nativeFanout
|
|
3711
|
+
? this.nativeFanout.normalizeParentUpgradePolicy(joinOpts)
|
|
3712
|
+
: normalizeParentUpgradePolicy(joinOpts);
|
|
3689
3713
|
const candidateScoringModeRaw = joinOpts.candidateScoringMode ?? "ranked-shuffle";
|
|
3690
3714
|
const candidateScoringMode = candidateScoringModeRaw === "ranked-strict" ||
|
|
3691
3715
|
candidateScoringModeRaw === "weighted" ||
|
|
@@ -3705,6 +3729,56 @@ export class FanoutTree extends DirectStream {
|
|
|
3705
3729
|
? anySignal([this.closeController.signal, joinOpts.signal])
|
|
3706
3730
|
: this.closeController.signal;
|
|
3707
3731
|
const signal = combinedSignal;
|
|
3732
|
+
let nextParentUpgradeCheckAt = 0;
|
|
3733
|
+
let parentUpgradeCheckSeq = 0;
|
|
3734
|
+
let parentUpgradeActiveGuardBackoffMs = 0;
|
|
3735
|
+
const scheduleNextParentUpgradeCheck = (now, first = false, minDelayMs = parentUpgrade.intervalMs) => {
|
|
3736
|
+
if (parentUpgrade.intervalMs <= 0) {
|
|
3737
|
+
nextParentUpgradeCheckAt = 0;
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
const factor = first
|
|
3741
|
+
? stableUnitInterval(`${ch.id.suffixKey}:${this.publicKeyHash}:parent-upgrade-phase`)
|
|
3742
|
+
: 0.75 +
|
|
3743
|
+
stableUnitInterval(`${ch.id.suffixKey}:${this.publicKeyHash}:parent-upgrade-interval:${parentUpgradeCheckSeq++}`) *
|
|
3744
|
+
0.5;
|
|
3745
|
+
nextParentUpgradeCheckAt =
|
|
3746
|
+
now +
|
|
3747
|
+
Math.max(1, Math.floor(parentUpgrade.intervalMs * factor), Math.floor(minDelayMs));
|
|
3748
|
+
};
|
|
3749
|
+
const resetParentUpgradeActiveGuardBackoff = () => {
|
|
3750
|
+
parentUpgradeActiveGuardBackoffMs = 0;
|
|
3751
|
+
};
|
|
3752
|
+
const parentUpgradeGuardDelayMs = (reason, now) => {
|
|
3753
|
+
const baseDelayMs = (() => {
|
|
3754
|
+
switch (reason) {
|
|
3755
|
+
case "data":
|
|
3756
|
+
return Math.max(parentUpgrade.quietMs, parentUpgrade.repairQuietMs);
|
|
3757
|
+
case "repair":
|
|
3758
|
+
return parentUpgrade.repairQuietMs;
|
|
3759
|
+
case "quiet":
|
|
3760
|
+
return ch.lastParentUpgradeActivityAt > 0
|
|
3761
|
+
? Math.max(0, parentUpgrade.quietMs -
|
|
3762
|
+
(now - ch.lastParentUpgradeActivityAt))
|
|
3763
|
+
: parentUpgrade.quietMs;
|
|
3764
|
+
case "cooldown":
|
|
3765
|
+
return Math.max(0, ch.parentUpgradeBackoffUntil - now, ch.parentUpgradeLastAt > 0
|
|
3766
|
+
? parentUpgrade.cooldownMs - (now - ch.parentUpgradeLastAt)
|
|
3767
|
+
: 0);
|
|
3768
|
+
default:
|
|
3769
|
+
return parentUpgrade.intervalMs;
|
|
3770
|
+
}
|
|
3771
|
+
})();
|
|
3772
|
+
if (reason !== "data" && reason !== "repair") {
|
|
3773
|
+
resetParentUpgradeActiveGuardBackoff();
|
|
3774
|
+
return baseDelayMs;
|
|
3775
|
+
}
|
|
3776
|
+
parentUpgradeActiveGuardBackoffMs =
|
|
3777
|
+
parentUpgradeActiveGuardBackoffMs > 0
|
|
3778
|
+
? Math.min(Math.max(parentUpgrade.failedBackoff.maxMs, baseDelayMs), Math.max(baseDelayMs, parentUpgradeActiveGuardBackoffMs * 2))
|
|
3779
|
+
: baseDelayMs;
|
|
3780
|
+
return parentUpgradeActiveGuardBackoffMs;
|
|
3781
|
+
};
|
|
3708
3782
|
try {
|
|
3709
3783
|
for (;;) {
|
|
3710
3784
|
if (ch.closed) {
|
|
@@ -3730,6 +3804,8 @@ export class FanoutTree extends DirectStream {
|
|
|
3730
3804
|
ch.metrics.reparentDisconnect += 1;
|
|
3731
3805
|
const hadChildren = ch.children.size > 0;
|
|
3732
3806
|
this.detachFromParent(ch);
|
|
3807
|
+
nextParentUpgradeCheckAt = 0;
|
|
3808
|
+
resetParentUpgradeActiveGuardBackoff();
|
|
3733
3809
|
// If we lose our parent, we are no longer on the rooted tree; detach children so
|
|
3734
3810
|
// they can rejoin as well (prevents stable disconnected components).
|
|
3735
3811
|
void this.kickChildren(ch).catch(() => { });
|
|
@@ -3742,50 +3818,130 @@ export class FanoutTree extends DirectStream {
|
|
|
3742
3818
|
const endedAndComplete = ch.endSeqExclusive > 0 &&
|
|
3743
3819
|
ch.missingSeqs.size === 0 &&
|
|
3744
3820
|
ch.nextExpectedSeq >= ch.endSeqExclusive;
|
|
3745
|
-
const expectingData = ch.missingSeqs.size > 0 ||
|
|
3746
|
-
|
|
3747
|
-
ch.
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3821
|
+
const expectingData = ch.missingSeqs.size > 0 ||
|
|
3822
|
+
(ch.repairEnabled && ch.maxSeqSeen >= 0 && ch.endSeqExclusive < 0) ||
|
|
3823
|
+
(ch.maxDataAgeMs > 0 && !endedAndComplete);
|
|
3824
|
+
// Repair requests travel child->parent on a direction that works
|
|
3825
|
+
// even when parent->child data writes silently fail. A repair that
|
|
3826
|
+
// does not fill a gap is not sufficient evidence by itself though:
|
|
3827
|
+
// the repair payload can be dropped or the parent cache can miss.
|
|
3828
|
+
// Re-parent only after both repeated unanswered repairs and a
|
|
3829
|
+
// quiet parent liveness window.
|
|
3830
|
+
const parentRepairDeadLivenessMs = Math.max(PARENT_REPAIR_DEAD_MIN_LIVENESS_MS, ch.repairIntervalMs * PARENT_REPAIR_DEAD_STREAK_THRESHOLD, ch.neighborAnnounceIntervalMs * 8);
|
|
3831
|
+
const parentRepairNow = Date.now();
|
|
3832
|
+
const parentLivenessQuietMs = ch.lastParentDataAt > 0 ? parentRepairNow - ch.lastParentDataAt : 0;
|
|
3833
|
+
const parentRepairDead = ch.repairEnabled &&
|
|
3834
|
+
ch.parentRepairUnansweredStreak >=
|
|
3835
|
+
PARENT_REPAIR_DEAD_STREAK_THRESHOLD &&
|
|
3836
|
+
parentLivenessQuietMs >= parentRepairDeadLivenessMs;
|
|
3837
|
+
if (parentRepairDead ||
|
|
3838
|
+
(staleAfterMs > 0 &&
|
|
3839
|
+
ch.receivedAnyParentData &&
|
|
3840
|
+
ch.lastParentDataAt > 0 &&
|
|
3841
|
+
Date.now() - ch.lastParentDataAt > staleAfterMs &&
|
|
3842
|
+
expectingData)) {
|
|
3751
3843
|
// Parent is "alive" at the stream layer but we're not receiving
|
|
3752
3844
|
// data at the expected rate; detach and try to re-parent.
|
|
3753
3845
|
ch.metrics.reparentStale += 1;
|
|
3754
3846
|
ch.parent = undefined;
|
|
3847
|
+
ch.parentRepairUnansweredStreak = 0;
|
|
3755
3848
|
ch.level = Number.POSITIVE_INFINITY;
|
|
3756
3849
|
ch.routeFromRoot = undefined;
|
|
3757
3850
|
ch.routeByPeer.clear();
|
|
3758
3851
|
ch.lastParentDataAt = 0;
|
|
3852
|
+
ch.lastParentUpgradeActivityAt = 0;
|
|
3759
3853
|
ch.receivedAnyParentData = false;
|
|
3760
3854
|
ch.pendingJoin.clear();
|
|
3761
|
-
ch.
|
|
3762
|
-
|
|
3763
|
-
|
|
3855
|
+
ch.pendingParentProbe.clear();
|
|
3856
|
+
ch.parentShadow = undefined;
|
|
3857
|
+
void Promise.all(this.clearParentUpgradeGrace(ch, true, true)).catch(() => { });
|
|
3858
|
+
if (this.parentUpgradeShadowInFlightSuffixKey === ch.id.suffixKey) {
|
|
3859
|
+
this.parentUpgradeShadowInFlightSuffixKey = undefined;
|
|
3764
3860
|
}
|
|
3765
|
-
ch.
|
|
3861
|
+
ch.parentUpgradeRetryAfterSeq = -1;
|
|
3862
|
+
ch.parentProbeRejectUntilByHash.clear();
|
|
3863
|
+
ch.parentProbeRejectBackoffMsByHash.clear();
|
|
3864
|
+
nextParentUpgradeCheckAt = 0;
|
|
3865
|
+
resetParentUpgradeActiveGuardBackoff();
|
|
3866
|
+
ch.pendingRouteQuery.clear();
|
|
3867
|
+
this.clearRouteProxies(ch);
|
|
3766
3868
|
void this.kickChildren(ch).catch(() => { });
|
|
3767
|
-
await delay(retryMs);
|
|
3869
|
+
await delay(retryMs, { signal });
|
|
3768
3870
|
continue;
|
|
3769
3871
|
}
|
|
3872
|
+
if (parentUpgrade.intervalMs > 0 && ch.level > 1) {
|
|
3873
|
+
const now = Date.now();
|
|
3874
|
+
if (nextParentUpgradeCheckAt === 0) {
|
|
3875
|
+
scheduleNextParentUpgradeCheck(now, true, parentUpgrade.dataGuard || parentUpgrade.repairGuard
|
|
3876
|
+
? Math.max(parentUpgrade.quietMs, parentUpgrade.repairQuietMs)
|
|
3877
|
+
: parentUpgrade.intervalMs);
|
|
3878
|
+
}
|
|
3879
|
+
const due = now >= nextParentUpgradeCheckAt;
|
|
3880
|
+
if (due) {
|
|
3881
|
+
const gateOptions = {
|
|
3882
|
+
leafOnly: parentUpgrade.leafOnly,
|
|
3883
|
+
repairGuard: parentUpgrade.repairGuard,
|
|
3884
|
+
dataGuard: parentUpgrade.dataGuard,
|
|
3885
|
+
endedAndComplete,
|
|
3886
|
+
maxPerPeer: parentUpgrade.maxPerPeer,
|
|
3887
|
+
cooldownMs: parentUpgrade.cooldownMs,
|
|
3888
|
+
quietMs: parentUpgrade.quietMs,
|
|
3889
|
+
repairQuietMs: parentUpgrade.repairQuietMs,
|
|
3890
|
+
now,
|
|
3891
|
+
};
|
|
3892
|
+
const gate = this.nativeFanout
|
|
3893
|
+
? this.nativeFanout.evaluateParentUpgradeGate(ch, gateOptions)
|
|
3894
|
+
: evaluateParentUpgradeGate(ch, gateOptions);
|
|
3895
|
+
if ("reason" in gate) {
|
|
3896
|
+
recordParentUpgradeSkip(ch.metrics, gate.reason);
|
|
3897
|
+
scheduleNextParentUpgradeCheck(now, false, parentUpgradeGuardDelayMs(gate.reason, now));
|
|
3898
|
+
}
|
|
3899
|
+
else {
|
|
3900
|
+
resetParentUpgradeActiveGuardBackoff();
|
|
3901
|
+
const improved = await this.maybeImproveParent(ch, {
|
|
3902
|
+
signal,
|
|
3903
|
+
candidateShuffleTopK,
|
|
3904
|
+
candidateScoringMode,
|
|
3905
|
+
candidateScoringWeights,
|
|
3906
|
+
joinAttemptsPerRound,
|
|
3907
|
+
joinReqTimeoutMs,
|
|
3908
|
+
parentUpgrade,
|
|
3909
|
+
trackerPeers: ch.cachedBootstrapPeers.filter((h) => Boolean(this.peers.get(h))),
|
|
3910
|
+
});
|
|
3911
|
+
scheduleNextParentUpgradeCheck(Date.now());
|
|
3912
|
+
if (improved) {
|
|
3913
|
+
ch.metrics.reparentUpgrade += 1;
|
|
3914
|
+
ch.parentUpgradeCount += 1;
|
|
3915
|
+
ch.parentUpgradeLastAt = Date.now();
|
|
3916
|
+
ch.parentUpgradeBackoffMs = 0;
|
|
3917
|
+
ch.parentUpgradeBackoffUntil = 0;
|
|
3918
|
+
ch.parentUpgradeRetryAfterSeq = -1;
|
|
3919
|
+
ch.parentUpgradeStaleRootProbeRound = 0;
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3770
3924
|
if (!ch.joinedOnce)
|
|
3771
3925
|
ch.joinedOnce = createDeferred();
|
|
3772
3926
|
ch.joinedOnce.resolve();
|
|
3773
3927
|
ch.joinedAtLeastOnce = true;
|
|
3774
3928
|
// Once attached, we don't need a fast retry cadence; keep polling coarse to
|
|
3775
3929
|
// avoid excessive timers when simulating many nodes in one process.
|
|
3776
|
-
await delay(Math.max(retryMs, 1_000));
|
|
3930
|
+
await delay(Math.max(retryMs, 1_000), { signal });
|
|
3777
3931
|
continue;
|
|
3778
3932
|
}
|
|
3779
3933
|
// `timeoutMs` is meant to bound the initial `joinChannel()` await, not to
|
|
3780
3934
|
// stop re-parenting attempts for long-running nodes.
|
|
3781
|
-
if (!ch.joinedAtLeastOnce &&
|
|
3935
|
+
if (!ch.joinedAtLeastOnce &&
|
|
3936
|
+
timeoutMs > 0 &&
|
|
3937
|
+
Date.now() - start > timeoutMs) {
|
|
3782
3938
|
const bootstrapsCount = this.getBootstrapsForChannel(ch).length;
|
|
3783
3939
|
const rootPeer = this.peers.get(ch.id.root);
|
|
3784
3940
|
const rootNeighbor = Boolean(rootPeer && rootPeer.isReadable && rootPeer.isWritable);
|
|
3785
3941
|
const bootstrapHint = bootstrapsCount === 0 && !rootNeighbor
|
|
3786
3942
|
? " No fanout bootstraps are configured for this channel, and the root is not a direct neighbor. If this peer reached the network via a bootstrap or relay node, initialize it with Peerbit.bootstrap(...) instead of Peerbit.dial(...), or configure FanoutTree.setBootstraps(...) before joining sharded topics."
|
|
3787
3943
|
: "";
|
|
3788
|
-
throw new Error(`fanout join timed out after ${timeoutMs}ms (topic=${ch.id.topic} root=${ch.id.root} self=${this.publicKeyHash} rootNeighbor=${rootNeighbor} peers=${this.peers.size} bootstraps=${bootstrapsCount}).${bootstrapHint}`);
|
|
3944
|
+
throw new Error(`fanout join timed out after ${timeoutMs}ms (topic=${ch.id.topic} root=${ch.id.root} self=${this.publicKeyHash} rootNeighbor=${rootNeighbor} peers=${this.peers.size} bootstraps=${bootstrapsCount} joinReqSent=${ch.metrics.joinReqSent} joinAcceptReceived=${ch.metrics.joinAcceptReceived} joinRejectReceived=${ch.metrics.joinRejectReceived} peerResets=${ch.metrics.joinPeerResets}).${bootstrapHint}`);
|
|
3789
3945
|
}
|
|
3790
3946
|
const cooldownMs = ch.rejoinCooldownUntil - Date.now();
|
|
3791
3947
|
if (cooldownMs > 0) {
|
|
@@ -3881,12 +4037,19 @@ export class FanoutTree extends DirectStream {
|
|
|
3881
4037
|
source: 1,
|
|
3882
4038
|
});
|
|
3883
4039
|
}
|
|
3884
|
-
// Fallback: try a bounded set of already-connected peers.
|
|
4040
|
+
// Fallback: try a bounded set of already-connected peers. Bootstrap
|
|
4041
|
+
// peers are often just trackers, but they can also be the only
|
|
4042
|
+
// connected channel host and may not list themselves in tracker
|
|
4043
|
+
// replies. Keep them as low-priority candidates unless tracker state
|
|
4044
|
+
// already represented them above.
|
|
4045
|
+
const bootstrapPeerSet = new Set(bootstrapPeers);
|
|
3885
4046
|
let connectedFallbackAdded = 0;
|
|
3886
4047
|
const connectedFallbackMax = 64;
|
|
3887
4048
|
for (const h of this.peers.keys()) {
|
|
3888
4049
|
if (h === this.publicKeyHash)
|
|
3889
4050
|
continue;
|
|
4051
|
+
if (bootstrapPeerSet.has(h) && candidatesByHash.has(h))
|
|
4052
|
+
continue;
|
|
3890
4053
|
upsertCandidate({
|
|
3891
4054
|
hash: h,
|
|
3892
4055
|
addrs: [],
|
|
@@ -3926,12 +4089,14 @@ export class FanoutTree extends DirectStream {
|
|
|
3926
4089
|
if (until != null && until > now)
|
|
3927
4090
|
nextAt = Math.min(nextAt, until);
|
|
3928
4091
|
}
|
|
3929
|
-
const waitMs = nextAt !== Number.POSITIVE_INFINITY
|
|
4092
|
+
const waitMs = nextAt !== Number.POSITIVE_INFINITY
|
|
4093
|
+
? Math.max(1, nextAt - now)
|
|
4094
|
+
: retryMs;
|
|
3930
4095
|
const capMs = Math.max(retryMs, trackerQueryIntervalMs > 0 ? trackerQueryIntervalMs : retryMs);
|
|
3931
|
-
await delay(Math.max(1, Math.min(waitMs, capMs)));
|
|
4096
|
+
await delay(Math.max(1, Math.min(waitMs, capMs)), { signal });
|
|
3932
4097
|
continue;
|
|
3933
4098
|
}
|
|
3934
|
-
await delay(retryMs);
|
|
4099
|
+
await delay(retryMs, { signal });
|
|
3935
4100
|
continue;
|
|
3936
4101
|
}
|
|
3937
4102
|
let ordered = [...candidates];
|
|
@@ -4097,6 +4262,7 @@ export class FanoutTree extends DirectStream {
|
|
|
4097
4262
|
}
|
|
4098
4263
|
}
|
|
4099
4264
|
if (res.timedOut) {
|
|
4265
|
+
this.noteJoinTimeout(ch, c.hash);
|
|
4100
4266
|
try {
|
|
4101
4267
|
await this.sendTrackerFeedback(ch, bootstrapPeers, c.hash, TRACKER_FEEDBACK_JOIN_TIMEOUT);
|
|
4102
4268
|
}
|
|
@@ -4137,26 +4303,922 @@ export class FanoutTree extends DirectStream {
|
|
|
4137
4303
|
const stream = this.peers.get(h);
|
|
4138
4304
|
if (!stream)
|
|
4139
4305
|
continue;
|
|
4140
|
-
void this.components.connectionManager
|
|
4306
|
+
void this.components.connectionManager
|
|
4307
|
+
.closeConnections(stream.peerId)
|
|
4308
|
+
.catch(() => { });
|
|
4141
4309
|
}
|
|
4142
4310
|
continue;
|
|
4143
4311
|
}
|
|
4144
|
-
await delay(retryMs);
|
|
4312
|
+
await delay(retryMs, { signal });
|
|
4145
4313
|
}
|
|
4146
4314
|
}
|
|
4147
4315
|
finally {
|
|
4148
4316
|
signal.clear?.();
|
|
4149
4317
|
}
|
|
4150
4318
|
}
|
|
4151
|
-
async
|
|
4152
|
-
|
|
4319
|
+
async maybeImproveParent(ch, options) {
|
|
4320
|
+
const parentUpgrade = options.parentUpgrade;
|
|
4321
|
+
const currentParent = ch.parent;
|
|
4322
|
+
if (!currentParent || ch.closed || ch.isRoot || ch.level <= 1)
|
|
4323
|
+
return false;
|
|
4324
|
+
if (parentUpgrade.mode === "shadow" &&
|
|
4325
|
+
this.parentUpgradeShadowInFlightSuffixKey != null &&
|
|
4326
|
+
this.parentUpgradeShadowInFlightSuffixKey !== ch.id.suffixKey) {
|
|
4327
|
+
ch.metrics.reparentUpgradeSkipBudget += 1;
|
|
4328
|
+
return false;
|
|
4329
|
+
}
|
|
4330
|
+
const staleRootProbeRound = Math.max(0, Math.floor(ch.parentUpgradeStaleRootProbeRound || 0));
|
|
4331
|
+
ch.parentUpgradeStaleRootProbeRound = Math.min(0x7fffffff, staleRootProbeRound + 1);
|
|
4332
|
+
const upgradeRandom = (scope) => stableUnitInterval(`${ch.id.suffixKey}:${this.publicKeyHash}:${currentParent}:${scope}`);
|
|
4333
|
+
const clearFailedBackoff = () => {
|
|
4334
|
+
ch.parentUpgradeBackoffMs = 0;
|
|
4335
|
+
ch.parentUpgradeBackoffUntil = 0;
|
|
4336
|
+
};
|
|
4337
|
+
const clearShadowInFlight = () => {
|
|
4338
|
+
if (this.parentUpgradeShadowInFlightSuffixKey === ch.id.suffixKey) {
|
|
4339
|
+
this.parentUpgradeShadowInFlightSuffixKey = undefined;
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
const resetShadow = () => {
|
|
4343
|
+
if (!ch.parentShadow)
|
|
4344
|
+
return;
|
|
4345
|
+
ch.parentShadow = undefined;
|
|
4346
|
+
clearShadowInFlight();
|
|
4347
|
+
ch.metrics.parentShadowReset += 1;
|
|
4348
|
+
};
|
|
4349
|
+
const deferParentUpgradeRetryUntilFreshData = (multiplier = 1) => {
|
|
4350
|
+
const minFreshMessages = Math.max(1, parentUpgrade.shadow.dualPathMinMessages *
|
|
4351
|
+
Math.max(1, Math.floor(multiplier)));
|
|
4352
|
+
ch.parentUpgradeRetryAfterSeq = Math.max(ch.parentUpgradeRetryAfterSeq, ch.maxSeqSeen + minFreshMessages - 1);
|
|
4353
|
+
};
|
|
4354
|
+
const applyFailedBackoff = () => {
|
|
4355
|
+
const minMs = parentUpgrade.failedBackoff.minMs;
|
|
4356
|
+
const maxMs = parentUpgrade.failedBackoff.maxMs;
|
|
4357
|
+
if (minMs <= 0 || maxMs <= 0) {
|
|
4358
|
+
ch.parentUpgradeBackoffMs = 0;
|
|
4359
|
+
ch.parentUpgradeBackoffUntil = 0;
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
const nextMs = ch.parentUpgradeBackoffMs > 0
|
|
4363
|
+
? Math.min(maxMs, Math.max(minMs, ch.parentUpgradeBackoffMs * 2))
|
|
4364
|
+
: minMs;
|
|
4365
|
+
ch.parentUpgradeBackoffMs = nextMs;
|
|
4366
|
+
const jitter = 0.75 + upgradeRandom(`failed-backoff:${nextMs}`) * 0.5;
|
|
4367
|
+
ch.parentUpgradeBackoffUntil =
|
|
4368
|
+
Date.now() + Math.max(1, Math.floor(nextMs * jitter));
|
|
4369
|
+
};
|
|
4370
|
+
const isDirectNeighbor = (hash) => {
|
|
4371
|
+
const peer = this.peers.get(hash);
|
|
4372
|
+
if (!peer)
|
|
4373
|
+
return false;
|
|
4374
|
+
try {
|
|
4375
|
+
const conns = this.components.connectionManager.getConnections(peer.peerId);
|
|
4376
|
+
return (conns?.length ?? 0) > 0;
|
|
4377
|
+
}
|
|
4378
|
+
catch {
|
|
4379
|
+
return peer.isReadable || peer.isWritable;
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4382
|
+
const candidatesByHash = new Map();
|
|
4383
|
+
const upsertCandidate = (c) => {
|
|
4384
|
+
const prev = candidatesByHash.get(c.hash);
|
|
4385
|
+
if (!prev) {
|
|
4386
|
+
candidatesByHash.set(c.hash, { ...c });
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
if (prev.addrs.length === 0 && c.addrs.length > 0)
|
|
4390
|
+
prev.addrs = c.addrs;
|
|
4391
|
+
prev.level = Math.min(prev.level, c.level);
|
|
4392
|
+
prev.freeSlots = Math.max(prev.freeSlots, c.freeSlots);
|
|
4393
|
+
prev.bidPerByte = Math.max(prev.bidPerByte, c.bidPerByte);
|
|
4394
|
+
prev.source = Math.min(prev.source, c.source);
|
|
4395
|
+
};
|
|
4396
|
+
const nonRootMinLevelGain = Math.max(parentUpgrade.minLevelGain, parentUpgrade.nonRootMinLevelGain);
|
|
4397
|
+
const rootMinLevelGain = Math.max(parentUpgrade.minLevelGain, parentUpgrade.rootMinLevelGain);
|
|
4398
|
+
const rootMinSubtreeGain = Math.max(parentUpgrade.minLevelGain, parentUpgrade.rootMinSubtreeGain);
|
|
4399
|
+
const levelGainFor = (parentLevel) => ch.level - (parentLevel + 1);
|
|
4400
|
+
const impactedPeersForRoot = () => Math.max(1, 1 + ch.children.size);
|
|
4401
|
+
const rootSubtreeGainFor = (parentLevel) => levelGainFor(parentLevel) * impactedPeersForRoot();
|
|
4402
|
+
const isEnoughLevelGain = (hash, parentLevel) => {
|
|
4403
|
+
const levelGain = levelGainFor(parentLevel);
|
|
4404
|
+
if (hash !== ch.id.root)
|
|
4405
|
+
return levelGain >= nonRootMinLevelGain;
|
|
4406
|
+
if (levelGain >= rootMinLevelGain)
|
|
4407
|
+
return true;
|
|
4408
|
+
return rootSubtreeGainFor(parentLevel) >= rootMinSubtreeGain;
|
|
4409
|
+
};
|
|
4410
|
+
let localChannelCount = 1;
|
|
4411
|
+
if (this.channelsBySuffixKey instanceof Map) {
|
|
4412
|
+
localChannelCount = 0;
|
|
4413
|
+
for (const localCh of this.channelsBySuffixKey.values()) {
|
|
4414
|
+
if (localCh.closed)
|
|
4415
|
+
continue;
|
|
4416
|
+
localChannelCount += 1;
|
|
4417
|
+
if (localChannelCount > 1)
|
|
4418
|
+
break;
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
const singleChannelBranch = ch.children.size > 0 && localChannelCount <= 1;
|
|
4422
|
+
const multiChannelBranch = ch.children.size > 0 && localChannelCount > 1;
|
|
4423
|
+
const endedAndComplete = ch.endSeqExclusive > 0 &&
|
|
4424
|
+
ch.missingSeqs.size === 0 &&
|
|
4425
|
+
ch.nextExpectedSeq >= ch.endSeqExclusive;
|
|
4426
|
+
const singleChannelSettledLeaf = ch.children.size === 0 && localChannelCount <= 1 && endedAndComplete;
|
|
4427
|
+
const multiChannelSettledLeaf = ch.children.size === 0 && localChannelCount > 1 && endedAndComplete;
|
|
4428
|
+
const knownChannelPeerCount = Math.max(1, ch.channelPeers?.size ?? 1);
|
|
4429
|
+
const defaultStaleRootProbeProbability = 0.015625;
|
|
4430
|
+
const configuredStaleRootProbeProbabilityRaw = Number(parentUpgrade.staleRootProbeProbability);
|
|
4431
|
+
const staleRootProbeBaseProbability = Number.isFinite(configuredStaleRootProbeProbabilityRaw)
|
|
4432
|
+
? Math.max(0, Math.min(1, configuredStaleRootProbeProbabilityRaw))
|
|
4433
|
+
: defaultStaleRootProbeProbability;
|
|
4434
|
+
const defaultMultiChannelSettledLeafRootCandidate = parentUpgrade.leafOnly &&
|
|
4435
|
+
multiChannelSettledLeaf &&
|
|
4436
|
+
staleRootProbeBaseProbability <= defaultStaleRootProbeProbability;
|
|
4437
|
+
const hasDefaultMultiChannelLeafRootSignal = () => {
|
|
4438
|
+
if (!defaultMultiChannelSettledLeafRootCandidate)
|
|
4439
|
+
return true;
|
|
4440
|
+
if (!isEnoughLevelGain(ch.id.root, 0))
|
|
4441
|
+
return false;
|
|
4442
|
+
if ((ch.parentDataLatencySamples ?? 0) < 8)
|
|
4443
|
+
return false;
|
|
4444
|
+
if ((ch.parentDataLatencyEwmaMs ?? 0) < 64 &&
|
|
4445
|
+
(ch.parentDataLatencyMaxMs ?? 0) < 256) {
|
|
4446
|
+
return false;
|
|
4447
|
+
}
|
|
4448
|
+
if (staleRootProbeBaseProbability <= 0)
|
|
4449
|
+
return false;
|
|
4450
|
+
const sampleProbability = Math.min(1, staleRootProbeBaseProbability);
|
|
4451
|
+
return (stableUnitInterval(`${ch.id.suffixKey}:${this.publicKeyHash}:${ch.id.root}:multi-channel-leaf-root-signal:${ch.maxSeqSeen}`) < sampleProbability);
|
|
4452
|
+
};
|
|
4453
|
+
const defaultMultiChannelLeafRootSignal = hasDefaultMultiChannelLeafRootSignal();
|
|
4454
|
+
if (defaultMultiChannelSettledLeafRootCandidate &&
|
|
4455
|
+
defaultMultiChannelLeafRootSignal &&
|
|
4456
|
+
ch.id.root !== this.publicKeyHash &&
|
|
4457
|
+
isDirectNeighbor(ch.id.root) &&
|
|
4458
|
+
(options.trackerPeers?.length ?? 0) > 0) {
|
|
4459
|
+
const rootMinFreeSlots = parentUpgrade.rootMinFreeSlots;
|
|
4460
|
+
const cachedRoot = ch.cachedTrackerCandidates.find((c) => c.hash === ch.id.root);
|
|
4461
|
+
if (!cachedRoot || cachedRoot.freeSlots < rootMinFreeSlots) {
|
|
4462
|
+
const refreshed = await this.queryTrackers(ch, options.trackerPeers ?? [], Math.max(16, ch.cachedTrackerCandidates.length), parentUpgrade.probe.timeoutMs, options.signal).catch(() => []);
|
|
4463
|
+
if (refreshed.length > 0) {
|
|
4464
|
+
ch.cachedTrackerCandidates = refreshed;
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
const configuredRejectCooldownMs = Math.max(0, parentUpgrade.probe.rejectCooldownMs);
|
|
4469
|
+
const rejectCooldownMs = localChannelCount > 1
|
|
4470
|
+
? Math.max(configuredRejectCooldownMs, 20_000)
|
|
4471
|
+
: configuredRejectCooldownMs;
|
|
4472
|
+
const rejectCooldownMaxMs = Math.max(rejectCooldownMs, parentUpgrade.probe.rejectCooldownMaxMs);
|
|
4473
|
+
const clearProbeRejectBackoff = (candidateHash) => {
|
|
4474
|
+
ch.parentProbeRejectUntilByHash.delete(candidateHash);
|
|
4475
|
+
ch.parentProbeRejectBackoffMsByHash.delete(candidateHash);
|
|
4476
|
+
};
|
|
4477
|
+
const rejectProbeCandidate = (candidateHash) => {
|
|
4478
|
+
if (rejectCooldownMs <= 0)
|
|
4479
|
+
return;
|
|
4480
|
+
const nextBackoffMs = (ch.parentProbeRejectBackoffMsByHash.get(candidateHash) ?? 0) > 0
|
|
4481
|
+
? Math.min(rejectCooldownMaxMs, Math.max(rejectCooldownMs, (ch.parentProbeRejectBackoffMsByHash.get(candidateHash) ?? 0) *
|
|
4482
|
+
2))
|
|
4483
|
+
: rejectCooldownMs;
|
|
4484
|
+
ch.parentProbeRejectBackoffMsByHash.set(candidateHash, nextBackoffMs);
|
|
4485
|
+
const jitter = 0.75 +
|
|
4486
|
+
upgradeRandom(`probe-reject:${candidateHash}:${nextBackoffMs}`) * 0.5;
|
|
4487
|
+
ch.parentProbeRejectUntilByHash.set(candidateHash, Date.now() + Math.max(1, Math.floor(nextBackoffMs * jitter)));
|
|
4488
|
+
while (ch.parentProbeRejectBackoffMsByHash.size >
|
|
4489
|
+
KNOWN_CANDIDATE_ADDRS_MAX_ENTRIES) {
|
|
4490
|
+
const oldest = ch.parentProbeRejectBackoffMsByHash.keys().next()
|
|
4491
|
+
.value;
|
|
4492
|
+
if (!oldest)
|
|
4493
|
+
break;
|
|
4494
|
+
ch.parentProbeRejectBackoffMsByHash.delete(oldest);
|
|
4495
|
+
ch.parentProbeRejectUntilByHash.delete(oldest);
|
|
4496
|
+
}
|
|
4497
|
+
while (ch.parentProbeRejectUntilByHash.size > KNOWN_CANDIDATE_ADDRS_MAX_ENTRIES) {
|
|
4498
|
+
const oldest = ch.parentProbeRejectUntilByHash.keys().next().value;
|
|
4499
|
+
if (!oldest)
|
|
4500
|
+
break;
|
|
4501
|
+
ch.parentProbeRejectUntilByHash.delete(oldest);
|
|
4502
|
+
ch.parentProbeRejectBackoffMsByHash.delete(oldest);
|
|
4503
|
+
}
|
|
4504
|
+
};
|
|
4505
|
+
const minFreeSlotsFor = (hash) => hash === ch.id.root
|
|
4506
|
+
? parentUpgrade.rootMinFreeSlots
|
|
4507
|
+
: parentUpgrade.minFreeSlots;
|
|
4508
|
+
const refreshCachedTrackerCandidate = (hash, level, freeSlots) => {
|
|
4509
|
+
const cached = ch.cachedTrackerCandidates.find((c) => c.hash === hash);
|
|
4510
|
+
if (!cached)
|
|
4511
|
+
return;
|
|
4512
|
+
if (Number.isFinite(level))
|
|
4513
|
+
cached.level = Math.max(0, Math.floor(level));
|
|
4514
|
+
if (Number.isFinite(freeSlots)) {
|
|
4515
|
+
cached.freeSlots = Math.max(0, Math.floor(freeSlots));
|
|
4516
|
+
}
|
|
4517
|
+
};
|
|
4518
|
+
const sendProbeTrackerFeedback = (candidateHash, reason) => {
|
|
4519
|
+
const trackerPeers = options.trackerPeers ?? [];
|
|
4520
|
+
if (trackerPeers.length === 0)
|
|
4521
|
+
return;
|
|
4522
|
+
void this.sendTrackerFeedback(ch, trackerPeers, candidateHash, TRACKER_FEEDBACK_JOIN_REJECT, reason).catch(() => { });
|
|
4523
|
+
};
|
|
4524
|
+
for (const c of ch.cachedTrackerCandidates) {
|
|
4525
|
+
if (c.hash === this.publicKeyHash)
|
|
4526
|
+
continue;
|
|
4527
|
+
if (!isDirectNeighbor(c.hash))
|
|
4528
|
+
continue;
|
|
4529
|
+
upsertCandidate({
|
|
4530
|
+
hash: c.hash,
|
|
4531
|
+
addrs: c.addrs,
|
|
4532
|
+
level: c.level,
|
|
4533
|
+
freeSlots: Number.isFinite(c.freeSlots)
|
|
4534
|
+
? Math.max(0, Math.floor(c.freeSlots))
|
|
4535
|
+
: 0,
|
|
4536
|
+
bidPerByte: c.bidPerByte,
|
|
4537
|
+
source: 0,
|
|
4538
|
+
});
|
|
4539
|
+
}
|
|
4540
|
+
if (ch.id.root !== this.publicKeyHash &&
|
|
4541
|
+
isDirectNeighbor(ch.id.root) &&
|
|
4542
|
+
!candidatesByHash.has(ch.id.root)) {
|
|
4543
|
+
upsertCandidate({
|
|
4544
|
+
hash: ch.id.root,
|
|
4545
|
+
addrs: [],
|
|
4546
|
+
level: 0,
|
|
4547
|
+
freeSlots: 1,
|
|
4548
|
+
bidPerByte: 0,
|
|
4549
|
+
source: -1,
|
|
4550
|
+
});
|
|
4551
|
+
}
|
|
4552
|
+
let connectedFallbackAdded = 0;
|
|
4553
|
+
const connectedFallbackMax = 64;
|
|
4554
|
+
for (const h of this.peers.keys()) {
|
|
4555
|
+
if (h === this.publicKeyHash)
|
|
4556
|
+
continue;
|
|
4557
|
+
if (!isDirectNeighbor(h))
|
|
4558
|
+
continue;
|
|
4559
|
+
upsertCandidate({
|
|
4560
|
+
hash: h,
|
|
4561
|
+
addrs: [],
|
|
4562
|
+
level: 0xffff,
|
|
4563
|
+
freeSlots: 0,
|
|
4564
|
+
bidPerByte: 0,
|
|
4565
|
+
source: 2,
|
|
4566
|
+
});
|
|
4567
|
+
connectedFallbackAdded += 1;
|
|
4568
|
+
if (connectedFallbackAdded >= connectedFallbackMax)
|
|
4569
|
+
break;
|
|
4570
|
+
}
|
|
4571
|
+
const orderedCandidates = [];
|
|
4572
|
+
for (const c of candidatesByHash.values()) {
|
|
4573
|
+
if (c.hash === this.publicKeyHash || c.hash === currentParent)
|
|
4574
|
+
continue;
|
|
4575
|
+
const canVerifyRootCapacityLive = parentUpgrade.verifyStaleRootCapacity === true &&
|
|
4576
|
+
parentUpgrade.mode === "shadow" &&
|
|
4577
|
+
c.hash === ch.id.root;
|
|
4578
|
+
const requiredMinFreeSlots = minFreeSlotsFor(c.hash);
|
|
4579
|
+
if (c.hash === ch.id.root &&
|
|
4580
|
+
defaultMultiChannelSettledLeafRootCandidate &&
|
|
4581
|
+
!defaultMultiChannelLeafRootSignal) {
|
|
4582
|
+
ch.metrics.reparentUpgradeSkipCandidateSlots += 1;
|
|
4583
|
+
continue;
|
|
4584
|
+
}
|
|
4585
|
+
if (canVerifyRootCapacityLive && c.freeSlots < requiredMinFreeSlots) {
|
|
4586
|
+
const staleRootProbeProbability = (() => {
|
|
4587
|
+
if (singleChannelBranch && staleRootProbeBaseProbability > 0) {
|
|
4588
|
+
const highValueBranch = rootSubtreeGainFor(c.level) >=
|
|
4589
|
+
Math.max(rootMinSubtreeGain + 1, 4);
|
|
4590
|
+
const branchCap = highValueBranch ? 0.25 : 0.2;
|
|
4591
|
+
return Math.max(staleRootProbeBaseProbability, Math.min(branchCap, staleRootProbeBaseProbability * 8));
|
|
4592
|
+
}
|
|
4593
|
+
if (multiChannelBranch && staleRootProbeBaseProbability > 0) {
|
|
4594
|
+
return Math.max(staleRootProbeBaseProbability, Math.min(0.03125, staleRootProbeBaseProbability * 2));
|
|
4595
|
+
}
|
|
4596
|
+
if (singleChannelSettledLeaf && staleRootProbeBaseProbability > 0) {
|
|
4597
|
+
return 0.5;
|
|
4598
|
+
}
|
|
4599
|
+
if (multiChannelSettledLeaf && parentUpgrade.leafOnly) {
|
|
4600
|
+
return staleRootProbeBaseProbability >
|
|
4601
|
+
defaultStaleRootProbeProbability
|
|
4602
|
+
? Math.max(staleRootProbeBaseProbability, Math.min(0.125, 3 / knownChannelPeerCount))
|
|
4603
|
+
: 0;
|
|
4604
|
+
}
|
|
4605
|
+
return Math.min(1, staleRootProbeBaseProbability *
|
|
4606
|
+
Math.max(1, Math.min(4, staleRootProbeRound + 1)));
|
|
4607
|
+
})();
|
|
4608
|
+
const staleRootProbeScore = stableUnitInterval(`${ch.id.suffixKey}:${this.publicKeyHash}:${c.hash}:stale-root-probe:${staleRootProbeRound}`);
|
|
4609
|
+
if (staleRootProbeScore >= staleRootProbeProbability) {
|
|
4610
|
+
ch.metrics.reparentUpgradeSkipCandidateSlots += 1;
|
|
4611
|
+
continue;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
if (!canVerifyRootCapacityLive && c.freeSlots < requiredMinFreeSlots) {
|
|
4615
|
+
ch.metrics.reparentUpgradeSkipCandidateSlots += 1;
|
|
4616
|
+
continue;
|
|
4617
|
+
}
|
|
4618
|
+
if (!isEnoughLevelGain(c.hash, c.level)) {
|
|
4619
|
+
ch.metrics.reparentUpgradeSkipCandidateLevel += 1;
|
|
4620
|
+
continue;
|
|
4621
|
+
}
|
|
4622
|
+
orderedCandidates.push(c);
|
|
4623
|
+
}
|
|
4624
|
+
let ordered = orderedCandidates.sort((a, b) => {
|
|
4625
|
+
if (a.level !== b.level)
|
|
4626
|
+
return a.level - b.level;
|
|
4627
|
+
if (a.freeSlots !== b.freeSlots)
|
|
4628
|
+
return b.freeSlots - a.freeSlots;
|
|
4629
|
+
if (a.bidPerByte !== b.bidPerByte)
|
|
4630
|
+
return b.bidPerByte - a.bidPerByte;
|
|
4631
|
+
if (a.source !== b.source)
|
|
4632
|
+
return a.source - b.source;
|
|
4633
|
+
return a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0;
|
|
4634
|
+
});
|
|
4635
|
+
if (ordered.length === 0) {
|
|
4636
|
+
if (parentUpgrade.mode === "probe" || parentUpgrade.mode === "shadow") {
|
|
4637
|
+
applyFailedBackoff();
|
|
4638
|
+
}
|
|
4639
|
+
return false;
|
|
4640
|
+
}
|
|
4641
|
+
if (options.candidateScoringMode === "ranked-shuffle") {
|
|
4642
|
+
if (options.candidateShuffleTopK > 0 && ordered.length > 1) {
|
|
4643
|
+
const k = Math.min(options.candidateShuffleTopK, ordered.length);
|
|
4644
|
+
const shuffleScope = ordered.map((c) => c.hash).join(",");
|
|
4645
|
+
for (let i = k - 1; i > 0; i--) {
|
|
4646
|
+
const j = Math.floor(upgradeRandom(`shuffle:${shuffleScope}:${i}`) * (i + 1));
|
|
4647
|
+
const tmp = ordered[i];
|
|
4648
|
+
ordered[i] = ordered[j];
|
|
4649
|
+
ordered[j] = tmp;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
else if (options.candidateScoringMode === "weighted") {
|
|
4654
|
+
const wLevel = Number.isFinite(options.candidateScoringWeights.level)
|
|
4655
|
+
? Math.max(0, options.candidateScoringWeights.level)
|
|
4656
|
+
: 0;
|
|
4657
|
+
const wSlots = Number.isFinite(options.candidateScoringWeights.freeSlots)
|
|
4658
|
+
? Math.max(0, options.candidateScoringWeights.freeSlots)
|
|
4659
|
+
: 0;
|
|
4660
|
+
const wConnected = Number.isFinite(options.candidateScoringWeights.connected)
|
|
4661
|
+
? Math.max(0, options.candidateScoringWeights.connected)
|
|
4662
|
+
: 0;
|
|
4663
|
+
const wBid = Number.isFinite(options.candidateScoringWeights.bidPerByte)
|
|
4664
|
+
? Math.max(0, options.candidateScoringWeights.bidPerByte)
|
|
4665
|
+
: 0;
|
|
4666
|
+
const wSource = Number.isFinite(options.candidateScoringWeights.source)
|
|
4667
|
+
? Math.max(0, options.candidateScoringWeights.source)
|
|
4668
|
+
: 0;
|
|
4669
|
+
const k = options.candidateShuffleTopK > 0
|
|
4670
|
+
? Math.min(options.candidateShuffleTopK, ordered.length)
|
|
4671
|
+
: 0;
|
|
4672
|
+
const hasWeightedSignal = wLevel > 0 || wSlots > 0 || wConnected > 0 || wBid > 0 || wSource > 0;
|
|
4673
|
+
if (k > 1 && hasWeightedSignal) {
|
|
4674
|
+
const head = ordered.slice(0, k);
|
|
4675
|
+
const tail = ordered.slice(k);
|
|
4676
|
+
const weightOf = (c) => {
|
|
4677
|
+
const level = Math.max(0, Math.floor(c.level));
|
|
4678
|
+
const freeSlots = Math.max(0, Math.floor(c.freeSlots));
|
|
4679
|
+
const bidPerByte = Math.max(0, Math.floor(c.bidPerByte));
|
|
4680
|
+
const source = Math.max(0, Math.floor(c.source));
|
|
4681
|
+
const connected = Boolean(this.peers.get(c.hash));
|
|
4682
|
+
let weight = 1;
|
|
4683
|
+
if (wLevel > 0)
|
|
4684
|
+
weight *= 1 / (1 + wLevel * level);
|
|
4685
|
+
if (wSlots > 0)
|
|
4686
|
+
weight *= 1 + wSlots * freeSlots;
|
|
4687
|
+
if (wConnected > 0 && connected)
|
|
4688
|
+
weight *= 1 + wConnected;
|
|
4689
|
+
if (wBid > 0)
|
|
4690
|
+
weight *= 1 + wBid * bidPerByte;
|
|
4691
|
+
if (wSource > 0)
|
|
4692
|
+
weight *= 1 / (1 + wSource * source);
|
|
4693
|
+
return weight;
|
|
4694
|
+
};
|
|
4695
|
+
const out = [];
|
|
4696
|
+
const remaining = [...head];
|
|
4697
|
+
while (remaining.length > 0) {
|
|
4698
|
+
let sum = 0;
|
|
4699
|
+
const weights = new Array(remaining.length);
|
|
4700
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
4701
|
+
const w = weightOf(remaining[i]);
|
|
4702
|
+
const v = Number.isFinite(w) ? Math.max(0, w) : 0;
|
|
4703
|
+
weights[i] = v;
|
|
4704
|
+
sum += v;
|
|
4705
|
+
}
|
|
4706
|
+
if (sum <= 0) {
|
|
4707
|
+
out.push(...remaining);
|
|
4708
|
+
break;
|
|
4709
|
+
}
|
|
4710
|
+
let r = upgradeRandom(`weighted:${out.length}:${remaining
|
|
4711
|
+
.map((candidate) => candidate.hash)
|
|
4712
|
+
.join(",")}`) * sum;
|
|
4713
|
+
let pick = 0;
|
|
4714
|
+
for (; pick < weights.length; pick++) {
|
|
4715
|
+
r -= weights[pick];
|
|
4716
|
+
if (r <= 0)
|
|
4717
|
+
break;
|
|
4718
|
+
}
|
|
4719
|
+
if (pick >= remaining.length)
|
|
4720
|
+
pick = remaining.length - 1;
|
|
4721
|
+
out.push(remaining[pick]);
|
|
4722
|
+
remaining.splice(pick, 1);
|
|
4723
|
+
}
|
|
4724
|
+
ordered = out.concat(tail);
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
if (parentUpgrade.mode === "probe" || parentUpgrade.mode === "shadow") {
|
|
4728
|
+
const mode = parentUpgrade.mode;
|
|
4729
|
+
const probeLimit = parentUpgrade.mode === "shadow"
|
|
4730
|
+
? 1
|
|
4731
|
+
: parentUpgrade.probe.maxPerRound;
|
|
4732
|
+
const shadowObserveMsForGate = parentUpgrade.shadow.observeMs;
|
|
4733
|
+
const shadowMinObservationsForGate = parentUpgrade.shadow.minObservations;
|
|
4734
|
+
const probeTimeoutMs = parentUpgrade.probe.timeoutMs;
|
|
4735
|
+
const maxLag = parentUpgrade.probe.maxLagMessages;
|
|
4736
|
+
const now = Date.now();
|
|
4737
|
+
const candidatesToProbe = [];
|
|
4738
|
+
const shadowCandidate = mode === "shadow" && ch.parentShadow
|
|
4739
|
+
? ordered.find((candidate) => candidate.hash === ch.parentShadow?.hash)
|
|
4740
|
+
: undefined;
|
|
4741
|
+
if (mode === "shadow" &&
|
|
4742
|
+
shadowCandidate &&
|
|
4743
|
+
ch.parentShadow &&
|
|
4744
|
+
shadowObserveMsForGate > 0 &&
|
|
4745
|
+
now - ch.parentShadow.startedAt < shadowObserveMsForGate) {
|
|
4746
|
+
return false;
|
|
4747
|
+
}
|
|
4748
|
+
const probeOrder = shadowCandidate
|
|
4749
|
+
? [
|
|
4750
|
+
shadowCandidate,
|
|
4751
|
+
...ordered.filter((candidate) => candidate.hash !== shadowCandidate.hash),
|
|
4752
|
+
]
|
|
4753
|
+
: ordered;
|
|
4754
|
+
for (const candidate of probeOrder) {
|
|
4755
|
+
const rejectUntil = ch.parentProbeRejectUntilByHash.get(candidate.hash);
|
|
4756
|
+
if (rejectUntil != null && rejectUntil > now) {
|
|
4757
|
+
ch.metrics.reparentUpgradeSkipProbeCooldown += 1;
|
|
4758
|
+
continue;
|
|
4759
|
+
}
|
|
4760
|
+
if (rejectUntil != null)
|
|
4761
|
+
ch.parentProbeRejectUntilByHash.delete(candidate.hash);
|
|
4762
|
+
candidatesToProbe.push(candidate);
|
|
4763
|
+
if (candidatesToProbe.length >= probeLimit)
|
|
4764
|
+
break;
|
|
4765
|
+
}
|
|
4766
|
+
if (candidatesToProbe.length === 0) {
|
|
4767
|
+
applyFailedBackoff();
|
|
4768
|
+
return false;
|
|
4769
|
+
}
|
|
4770
|
+
const shouldReserveRootCapacityForProbe = (candidate) => {
|
|
4771
|
+
if (candidate.hash !== ch.id.root)
|
|
4772
|
+
return true;
|
|
4773
|
+
if (mode !== "shadow")
|
|
4774
|
+
return true;
|
|
4775
|
+
if (endedAndComplete && candidate.level === 0)
|
|
4776
|
+
return true;
|
|
4777
|
+
if (!ch.parentShadow || ch.parentShadow.hash !== candidate.hash) {
|
|
4778
|
+
return (shadowObserveMsForGate <= 0 && shadowMinObservationsForGate <= 1);
|
|
4779
|
+
}
|
|
4780
|
+
return (now - ch.parentShadow.startedAt >= shadowObserveMsForGate &&
|
|
4781
|
+
ch.parentShadow.observations + 1 >= shadowMinObservationsForGate);
|
|
4782
|
+
};
|
|
4783
|
+
const rejectShadowCandidate = (candidateHash, reason) => {
|
|
4784
|
+
if (mode !== "shadow")
|
|
4785
|
+
return;
|
|
4786
|
+
switch (reason) {
|
|
4787
|
+
case "noReply":
|
|
4788
|
+
ch.metrics.parentShadowRejectNoReply += 1;
|
|
4789
|
+
break;
|
|
4790
|
+
case "notRooted":
|
|
4791
|
+
ch.metrics.parentShadowRejectNotRooted += 1;
|
|
4792
|
+
break;
|
|
4793
|
+
case "capacity":
|
|
4794
|
+
ch.metrics.parentShadowRejectCapacity += 1;
|
|
4795
|
+
break;
|
|
4796
|
+
case "repair":
|
|
4797
|
+
ch.metrics.parentShadowRejectRepair += 1;
|
|
4798
|
+
break;
|
|
4799
|
+
case "lag":
|
|
4800
|
+
ch.metrics.parentShadowRejectLag += 1;
|
|
4801
|
+
break;
|
|
4802
|
+
case "overloaded":
|
|
4803
|
+
ch.metrics.parentShadowRejectOverloaded += 1;
|
|
4804
|
+
break;
|
|
4805
|
+
case "level":
|
|
4806
|
+
ch.metrics.parentShadowRejectLevel += 1;
|
|
4807
|
+
break;
|
|
4808
|
+
}
|
|
4809
|
+
if (ch.parentShadow?.hash === candidateHash)
|
|
4810
|
+
resetShadow();
|
|
4811
|
+
deferParentUpgradeRetryUntilFreshData();
|
|
4812
|
+
};
|
|
4813
|
+
const probed = await Promise.all(candidatesToProbe.map(async (candidate) => ({
|
|
4814
|
+
candidate,
|
|
4815
|
+
reply: await this.probeParentCandidate(ch, candidate.hash, probeTimeoutMs, options.signal, minFreeSlotsFor(candidate.hash), shouldReserveRootCapacityForProbe(candidate)),
|
|
4816
|
+
})));
|
|
4817
|
+
const liveCandidates = [];
|
|
4818
|
+
for (const { candidate, reply } of probed) {
|
|
4819
|
+
if (!reply) {
|
|
4820
|
+
ch.metrics.reparentUpgradeSkipProbeNoReply += 1;
|
|
4821
|
+
rejectShadowCandidate(candidate.hash, "noReply");
|
|
4822
|
+
rejectProbeCandidate(candidate.hash);
|
|
4823
|
+
continue;
|
|
4824
|
+
}
|
|
4825
|
+
if (!reply.rooted) {
|
|
4826
|
+
ch.metrics.reparentUpgradeSkipProbeNotRooted += 1;
|
|
4827
|
+
refreshCachedTrackerCandidate(candidate.hash, reply.level, 0);
|
|
4828
|
+
sendProbeTrackerFeedback(candidate.hash, JOIN_REJECT_NOT_ATTACHED);
|
|
4829
|
+
rejectShadowCandidate(candidate.hash, "notRooted");
|
|
4830
|
+
rejectProbeCandidate(candidate.hash);
|
|
4831
|
+
continue;
|
|
4832
|
+
}
|
|
4833
|
+
const liveLevel = Number.isFinite(reply.level)
|
|
4834
|
+
? Math.max(0, Math.floor(reply.level))
|
|
4835
|
+
: candidate.level;
|
|
4836
|
+
const liveFreeSlots = Number.isFinite(reply.freeSlots)
|
|
4837
|
+
? Math.max(0, Math.floor(reply.freeSlots))
|
|
4838
|
+
: 0;
|
|
4839
|
+
refreshCachedTrackerCandidate(candidate.hash, liveLevel, liveFreeSlots);
|
|
4840
|
+
const requiredMinFreeSlots = minFreeSlotsFor(candidate.hash);
|
|
4841
|
+
if (!reply.accepting || liveFreeSlots < requiredMinFreeSlots) {
|
|
4842
|
+
ch.metrics.reparentUpgradeSkipCandidateSlots += 1;
|
|
4843
|
+
if (!reply.accepting || liveFreeSlots <= 0) {
|
|
4844
|
+
sendProbeTrackerFeedback(candidate.hash, JOIN_REJECT_NO_CAPACITY);
|
|
4845
|
+
}
|
|
4846
|
+
rejectShadowCandidate(candidate.hash, "capacity");
|
|
4847
|
+
rejectProbeCandidate(candidate.hash);
|
|
4848
|
+
continue;
|
|
4849
|
+
}
|
|
4850
|
+
const configuredMaxChildLoadRatioRaw = candidate.hash === ch.id.root
|
|
4851
|
+
? parentUpgrade.rootMaxChildLoadRatio
|
|
4852
|
+
: parentUpgrade.maxChildLoadRatio;
|
|
4853
|
+
const configuredMaxChildLoadRatio = candidate.hash === ch.id.root &&
|
|
4854
|
+
localChannelCount > 1 &&
|
|
4855
|
+
Number(configuredMaxChildLoadRatioRaw) > 0
|
|
4856
|
+
? Math.min(0.2, Number(configuredMaxChildLoadRatioRaw))
|
|
4857
|
+
: configuredMaxChildLoadRatioRaw;
|
|
4858
|
+
const maxChildLoadRatio = Math.max(0, Number(configuredMaxChildLoadRatio));
|
|
4859
|
+
if (maxChildLoadRatio > 0 &&
|
|
4860
|
+
reply.maxChildren > 0 &&
|
|
4861
|
+
(reply.children + 1) / reply.maxChildren > maxChildLoadRatio) {
|
|
4862
|
+
ch.metrics.reparentUpgradeSkipCandidateSlots += 1;
|
|
4863
|
+
ch.metrics.reparentUpgradeSkipCandidatePressure += 1;
|
|
4864
|
+
if (candidate.hash === ch.id.root) {
|
|
4865
|
+
ch.metrics.reparentUpgradeSkipRootPressure += 1;
|
|
4866
|
+
refreshCachedTrackerCandidate(candidate.hash, liveLevel, 0);
|
|
4867
|
+
sendProbeTrackerFeedback(candidate.hash, JOIN_REJECT_NO_CAPACITY);
|
|
4868
|
+
}
|
|
4869
|
+
rejectShadowCandidate(candidate.hash, "capacity");
|
|
4870
|
+
rejectProbeCandidate(candidate.hash);
|
|
4871
|
+
continue;
|
|
4872
|
+
}
|
|
4873
|
+
if (reply.repairing || reply.missingSeqs > 0) {
|
|
4874
|
+
ch.metrics.reparentUpgradeSkipProbeRepair += 1;
|
|
4875
|
+
rejectShadowCandidate(candidate.hash, "repair");
|
|
4876
|
+
rejectProbeCandidate(candidate.hash);
|
|
4877
|
+
continue;
|
|
4878
|
+
}
|
|
4879
|
+
if (reply.overloaded) {
|
|
4880
|
+
ch.metrics.reparentUpgradeSkipProbeOverloaded += 1;
|
|
4881
|
+
rejectShadowCandidate(candidate.hash, "overloaded");
|
|
4882
|
+
rejectProbeCandidate(candidate.hash);
|
|
4883
|
+
continue;
|
|
4884
|
+
}
|
|
4885
|
+
if (ch.maxSeqSeen >= 0 &&
|
|
4886
|
+
reply.haveToExclusive + maxLag < ch.maxSeqSeen + 1) {
|
|
4887
|
+
ch.metrics.reparentUpgradeSkipProbeLag += 1;
|
|
4888
|
+
rejectShadowCandidate(candidate.hash, "lag");
|
|
4889
|
+
rejectProbeCandidate(candidate.hash);
|
|
4890
|
+
continue;
|
|
4891
|
+
}
|
|
4892
|
+
if (!isEnoughLevelGain(candidate.hash, liveLevel)) {
|
|
4893
|
+
ch.metrics.reparentUpgradeSkipCandidateLevel += 1;
|
|
4894
|
+
rejectShadowCandidate(candidate.hash, "level");
|
|
4895
|
+
rejectProbeCandidate(candidate.hash);
|
|
4896
|
+
continue;
|
|
4897
|
+
}
|
|
4898
|
+
clearProbeRejectBackoff(candidate.hash);
|
|
4899
|
+
liveCandidates.push({
|
|
4900
|
+
...candidate,
|
|
4901
|
+
level: liveLevel,
|
|
4902
|
+
freeSlots: liveFreeSlots,
|
|
4903
|
+
haveToExclusive: reply.haveToExclusive,
|
|
4904
|
+
reservationToken: reply.reservationToken,
|
|
4905
|
+
});
|
|
4906
|
+
}
|
|
4907
|
+
ordered = liveCandidates.sort((a, b) => {
|
|
4908
|
+
if (a.level !== b.level)
|
|
4909
|
+
return a.level - b.level;
|
|
4910
|
+
if (a.freeSlots !== b.freeSlots)
|
|
4911
|
+
return b.freeSlots - a.freeSlots;
|
|
4912
|
+
if (a.bidPerByte !== b.bidPerByte)
|
|
4913
|
+
return b.bidPerByte - a.bidPerByte;
|
|
4914
|
+
if (a.source !== b.source)
|
|
4915
|
+
return a.source - b.source;
|
|
4916
|
+
return a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0;
|
|
4917
|
+
});
|
|
4918
|
+
if (ordered.length === 0) {
|
|
4919
|
+
applyFailedBackoff();
|
|
4920
|
+
return false;
|
|
4921
|
+
}
|
|
4922
|
+
clearFailedBackoff();
|
|
4923
|
+
if (mode === "shadow") {
|
|
4924
|
+
const nowAfterProbe = Date.now();
|
|
4925
|
+
const observeMs = parentUpgrade.shadow.observeMs;
|
|
4926
|
+
const minObservations = shadowMinObservationsForGate;
|
|
4927
|
+
const requireDataForShadowPromotion = parentUpgrade.shadow.dualPathMs > 0;
|
|
4928
|
+
const selected = ch.parentShadow != null
|
|
4929
|
+
? (ordered.find((candidate) => candidate.hash === ch.parentShadow?.hash) ?? ordered[0])
|
|
4930
|
+
: ordered[0];
|
|
4931
|
+
if (!selected) {
|
|
4932
|
+
resetShadow();
|
|
4933
|
+
return false;
|
|
4934
|
+
}
|
|
4935
|
+
if (endedAndComplete &&
|
|
4936
|
+
selected.hash === ch.id.root &&
|
|
4937
|
+
selected.level === 0 &&
|
|
4938
|
+
!requireDataForShadowPromotion) {
|
|
4939
|
+
if (!ch.parentShadow || ch.parentShadow.hash !== selected.hash) {
|
|
4940
|
+
ch.parentShadow = {
|
|
4941
|
+
hash: selected.hash,
|
|
4942
|
+
startedAt: nowAfterProbe - observeMs,
|
|
4943
|
+
observations: minObservations,
|
|
4944
|
+
level: selected.level,
|
|
4945
|
+
freeSlots: selected.freeSlots,
|
|
4946
|
+
haveToExclusive: "haveToExclusive" in selected &&
|
|
4947
|
+
typeof selected.haveToExclusive === "number"
|
|
4948
|
+
? selected.haveToExclusive
|
|
4949
|
+
: 0,
|
|
4950
|
+
liveDataMessages: 0,
|
|
4951
|
+
liveFirstDataMessages: 0,
|
|
4952
|
+
liveParentFirstDataMessages: 0,
|
|
4953
|
+
liveCandidateLeadSamples: 0,
|
|
4954
|
+
liveCandidateLeadMsTotal: 0,
|
|
4955
|
+
liveCandidateFirstAtBySeq: new Map(),
|
|
4956
|
+
liveParentFirstAtBySeq: new Map(),
|
|
4957
|
+
liveComparedSeqs: new Set(),
|
|
4958
|
+
liveMaxSeqSeen: -1,
|
|
4959
|
+
liveLastDataAt: 0,
|
|
4960
|
+
};
|
|
4961
|
+
this.parentUpgradeShadowInFlightSuffixKey = ch.id.suffixKey;
|
|
4962
|
+
ch.metrics.parentShadowStart += 1;
|
|
4963
|
+
}
|
|
4964
|
+
else {
|
|
4965
|
+
ch.parentShadow.observations += 1;
|
|
4966
|
+
ch.parentShadow.level = selected.level;
|
|
4967
|
+
ch.parentShadow.freeSlots = selected.freeSlots;
|
|
4968
|
+
ch.parentShadow.haveToExclusive =
|
|
4969
|
+
"haveToExclusive" in selected &&
|
|
4970
|
+
typeof selected.haveToExclusive === "number"
|
|
4971
|
+
? selected.haveToExclusive
|
|
4972
|
+
: 0;
|
|
4973
|
+
ch.metrics.parentShadowObserve += 1;
|
|
4974
|
+
}
|
|
4975
|
+
ordered = [selected];
|
|
4976
|
+
}
|
|
4977
|
+
else {
|
|
4978
|
+
if (!ch.parentShadow || ch.parentShadow.hash !== selected.hash) {
|
|
4979
|
+
ch.parentShadow = {
|
|
4980
|
+
hash: selected.hash,
|
|
4981
|
+
startedAt: nowAfterProbe,
|
|
4982
|
+
observations: 1,
|
|
4983
|
+
level: selected.level,
|
|
4984
|
+
freeSlots: selected.freeSlots,
|
|
4985
|
+
haveToExclusive: "haveToExclusive" in selected &&
|
|
4986
|
+
typeof selected.haveToExclusive === "number"
|
|
4987
|
+
? selected.haveToExclusive
|
|
4988
|
+
: 0,
|
|
4989
|
+
liveDataMessages: 0,
|
|
4990
|
+
liveFirstDataMessages: 0,
|
|
4991
|
+
liveParentFirstDataMessages: 0,
|
|
4992
|
+
liveCandidateLeadSamples: 0,
|
|
4993
|
+
liveCandidateLeadMsTotal: 0,
|
|
4994
|
+
liveCandidateFirstAtBySeq: new Map(),
|
|
4995
|
+
liveParentFirstAtBySeq: new Map(),
|
|
4996
|
+
liveComparedSeqs: new Set(),
|
|
4997
|
+
liveMaxSeqSeen: -1,
|
|
4998
|
+
liveLastDataAt: 0,
|
|
4999
|
+
};
|
|
5000
|
+
this.parentUpgradeShadowInFlightSuffixKey = ch.id.suffixKey;
|
|
5001
|
+
ch.metrics.parentShadowStart += 1;
|
|
5002
|
+
return false;
|
|
5003
|
+
}
|
|
5004
|
+
ch.parentShadow.observations += 1;
|
|
5005
|
+
ch.parentShadow.level = selected.level;
|
|
5006
|
+
ch.parentShadow.freeSlots = selected.freeSlots;
|
|
5007
|
+
ch.parentShadow.haveToExclusive =
|
|
5008
|
+
"haveToExclusive" in selected &&
|
|
5009
|
+
typeof selected.haveToExclusive === "number"
|
|
5010
|
+
? selected.haveToExclusive
|
|
5011
|
+
: 0;
|
|
5012
|
+
ch.metrics.parentShadowObserve += 1;
|
|
5013
|
+
if (ch.parentShadow.observations < minObservations ||
|
|
5014
|
+
nowAfterProbe - ch.parentShadow.startedAt < observeMs) {
|
|
5015
|
+
return false;
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
ordered = [selected];
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
let attempts = 0;
|
|
5022
|
+
for (const candidate of ordered) {
|
|
5023
|
+
if (options.signal.aborted)
|
|
5024
|
+
break;
|
|
5025
|
+
if (attempts >= options.joinAttemptsPerRound)
|
|
5026
|
+
break;
|
|
5027
|
+
attempts += 1;
|
|
5028
|
+
if (!isDirectNeighbor(candidate.hash))
|
|
5029
|
+
continue;
|
|
5030
|
+
const previousParent = ch.parent;
|
|
5031
|
+
const previousLevel = ch.level;
|
|
5032
|
+
const previousRouteFromRoot = ch.routeFromRoot
|
|
5033
|
+
? [...ch.routeFromRoot]
|
|
5034
|
+
: undefined;
|
|
5035
|
+
const previousLastParentDataAt = ch.lastParentDataAt;
|
|
5036
|
+
const previousLastParentUpgradeActivityAt = ch.lastParentUpgradeActivityAt;
|
|
5037
|
+
const previousReceivedAnyParentData = ch.receivedAnyParentData;
|
|
5038
|
+
const shadowDualPathMs = parentUpgrade.shadow.dualPathMs;
|
|
5039
|
+
const shadowDualPathMinMessages = parentUpgrade.shadow.dualPathMinMessages;
|
|
5040
|
+
const shadowDualPathMinAdvantageMessages = Math.max(1, Math.min(16, Math.ceil(shadowDualPathMinMessages / 2)));
|
|
5041
|
+
const shadowDualPathMinLeadSamples = Math.max(1, Math.min(32, shadowDualPathMinMessages));
|
|
5042
|
+
const shadowDualPathMaxParentFirstMessages = Math.max(0, Math.floor(shadowDualPathMinMessages / 8));
|
|
5043
|
+
const shadowDualPathMinLeadMsAvg = shadowDualPathMinMessages <= 1 ? 0 : 128;
|
|
5044
|
+
const useShadowDualPath = parentUpgrade.mode === "shadow" &&
|
|
5045
|
+
shadowDualPathMs > 0 &&
|
|
5046
|
+
previousParent != null &&
|
|
5047
|
+
previousParent !== candidate.hash;
|
|
5048
|
+
const seqAtShadowAttach = ch.maxSeqSeen;
|
|
5049
|
+
const reqId = (this.random() * 0xffffffff) >>> 0;
|
|
5050
|
+
const res = await this.tryJoinOnce(ch, candidate.hash, reqId, options.joinReqTimeoutMs, options.signal, {
|
|
5051
|
+
allowReplace: true,
|
|
5052
|
+
parentUpgradeReservationToken: candidate.hash === ch.id.root &&
|
|
5053
|
+
"reservationToken" in candidate &&
|
|
5054
|
+
typeof candidate.reservationToken === "number"
|
|
5055
|
+
? candidate.reservationToken
|
|
5056
|
+
: 0,
|
|
5057
|
+
shadowAttach: useShadowDualPath,
|
|
5058
|
+
});
|
|
5059
|
+
if (res.ok) {
|
|
5060
|
+
if (useShadowDualPath) {
|
|
5061
|
+
const acceptedParentLevel = Number.isFinite(res.parentLevel)
|
|
5062
|
+
? Math.max(0, Math.floor(res.parentLevel))
|
|
5063
|
+
: candidate.level;
|
|
5064
|
+
if (!isEnoughLevelGain(candidate.hash, acceptedParentLevel)) {
|
|
5065
|
+
void this._sendControl(candidate.hash, this.codec.encodeLeave(ch.id.key)).catch(() => { });
|
|
5066
|
+
resetShadow();
|
|
5067
|
+
rejectProbeCandidate(candidate.hash);
|
|
5068
|
+
deferParentUpgradeRetryUntilFreshData(2);
|
|
5069
|
+
applyFailedBackoff();
|
|
5070
|
+
return false;
|
|
5071
|
+
}
|
|
5072
|
+
const shadow = ch.parentShadow;
|
|
5073
|
+
if (shadow?.hash === candidate.hash) {
|
|
5074
|
+
shadow.liveDataMessages = 0;
|
|
5075
|
+
shadow.liveFirstDataMessages = 0;
|
|
5076
|
+
shadow.liveParentFirstDataMessages = 0;
|
|
5077
|
+
shadow.liveCandidateLeadSamples = 0;
|
|
5078
|
+
shadow.liveCandidateLeadMsTotal = 0;
|
|
5079
|
+
shadow.liveCandidateFirstAtBySeq.clear();
|
|
5080
|
+
shadow.liveParentFirstAtBySeq.clear();
|
|
5081
|
+
shadow.liveComparedSeqs.clear();
|
|
5082
|
+
shadow.liveMaxSeqSeen = seqAtShadowAttach;
|
|
5083
|
+
shadow.liveLastDataAt = 0;
|
|
5084
|
+
}
|
|
5085
|
+
const deadline = Date.now() + shadowDualPathMs;
|
|
5086
|
+
for (;;) {
|
|
5087
|
+
if (options.signal.aborted)
|
|
5088
|
+
break;
|
|
5089
|
+
const currentShadow = ch.parentShadow;
|
|
5090
|
+
const observedFreshCandidateData = currentShadow?.hash === candidate.hash &&
|
|
5091
|
+
currentShadow.liveFirstDataMessages >=
|
|
5092
|
+
shadowDualPathMinMessages &&
|
|
5093
|
+
currentShadow.liveParentFirstDataMessages <=
|
|
5094
|
+
shadowDualPathMaxParentFirstMessages &&
|
|
5095
|
+
currentShadow.liveFirstDataMessages >=
|
|
5096
|
+
currentShadow.liveParentFirstDataMessages +
|
|
5097
|
+
shadowDualPathMinAdvantageMessages &&
|
|
5098
|
+
currentShadow.liveCandidateLeadSamples >=
|
|
5099
|
+
shadowDualPathMinLeadSamples &&
|
|
5100
|
+
currentShadow.liveCandidateLeadMsTotal >=
|
|
5101
|
+
currentShadow.liveCandidateLeadSamples *
|
|
5102
|
+
shadowDualPathMinLeadMsAvg &&
|
|
5103
|
+
currentShadow.liveMaxSeqSeen > seqAtShadowAttach;
|
|
5104
|
+
if (observedFreshCandidateData) {
|
|
5105
|
+
const parentRoute = res.parentRouteFromRoot;
|
|
5106
|
+
if (!parentRoute || parentRoute.length === 0)
|
|
5107
|
+
break;
|
|
5108
|
+
ch.parent = candidate.hash;
|
|
5109
|
+
ch.level = Math.max(1, Math.floor(res.parentLevel ?? candidate.level) + 1);
|
|
5110
|
+
ch.routeFromRoot = [...parentRoute, this.publicKeyHash];
|
|
5111
|
+
ch.joinedAtLeastOnce = true;
|
|
5112
|
+
ch.lastParentDataAt =
|
|
5113
|
+
currentShadow.liveLastDataAt > 0
|
|
5114
|
+
? currentShadow.liveLastDataAt
|
|
5115
|
+
: Date.now();
|
|
5116
|
+
ch.lastParentUpgradeActivityAt = ch.lastParentDataAt;
|
|
5117
|
+
ch.receivedAnyParentData = true;
|
|
5118
|
+
this.resetParentDataLatency(ch);
|
|
5119
|
+
this.touchPeerHint(ch, candidate.hash);
|
|
5120
|
+
if (previousParent) {
|
|
5121
|
+
this.startParentUpgradeGrace(ch, {
|
|
5122
|
+
previousParent,
|
|
5123
|
+
candidateParent: candidate.hash,
|
|
5124
|
+
previousLevel,
|
|
5125
|
+
previousRouteFromRoot,
|
|
5126
|
+
previousLastParentDataAt,
|
|
5127
|
+
previousLastParentUpgradeActivityAt,
|
|
5128
|
+
previousReceivedAnyParentData,
|
|
5129
|
+
candidateFirstMessages: 0,
|
|
5130
|
+
previousFirstMessages: 0,
|
|
5131
|
+
minCandidateFirstMessages: shadowDualPathMinMessages,
|
|
5132
|
+
minCandidateAdvantageMessages: shadowDualPathMinAdvantageMessages,
|
|
5133
|
+
maxPreviousFirstMessages: shadowDualPathMaxParentFirstMessages,
|
|
5134
|
+
timeoutMs: shadowDualPathMs,
|
|
5135
|
+
});
|
|
5136
|
+
}
|
|
5137
|
+
ch.metrics.parentShadowPromote += 1;
|
|
5138
|
+
ch.parentShadow = undefined;
|
|
5139
|
+
clearShadowInFlight();
|
|
5140
|
+
clearFailedBackoff();
|
|
5141
|
+
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
5142
|
+
return true;
|
|
5143
|
+
}
|
|
5144
|
+
const remainingMs = deadline - Date.now();
|
|
5145
|
+
if (remainingMs <= 0)
|
|
5146
|
+
break;
|
|
5147
|
+
await delay(Math.min(50, remainingMs), {
|
|
5148
|
+
signal: options.signal,
|
|
5149
|
+
});
|
|
5150
|
+
}
|
|
5151
|
+
if (ch.parent === previousParent) {
|
|
5152
|
+
ch.level = previousLevel;
|
|
5153
|
+
ch.routeFromRoot = previousRouteFromRoot;
|
|
5154
|
+
ch.lastParentDataAt = previousLastParentDataAt;
|
|
5155
|
+
ch.lastParentUpgradeActivityAt =
|
|
5156
|
+
previousLastParentUpgradeActivityAt;
|
|
5157
|
+
ch.receivedAnyParentData = previousReceivedAnyParentData;
|
|
5158
|
+
}
|
|
5159
|
+
void this._sendControl(candidate.hash, this.codec.encodeLeave(ch.id.key)).catch(() => { });
|
|
5160
|
+
if (ch.parentShadow) {
|
|
5161
|
+
ch.parentShadow = undefined;
|
|
5162
|
+
clearShadowInFlight();
|
|
5163
|
+
ch.metrics.parentShadowReset += 1;
|
|
5164
|
+
}
|
|
5165
|
+
// A shadow cutover can only be proven by fresh data. If no
|
|
5166
|
+
// candidate-first data arrived, wait for enough newer data before
|
|
5167
|
+
// spending more probes on the same channel. Also cool down this
|
|
5168
|
+
// candidate; accepting JOIN without proving live data is a weak
|
|
5169
|
+
// parent signal, not a reason to probe it repeatedly.
|
|
5170
|
+
rejectProbeCandidate(candidate.hash);
|
|
5171
|
+
deferParentUpgradeRetryUntilFreshData(2);
|
|
5172
|
+
applyFailedBackoff();
|
|
5173
|
+
return false;
|
|
5174
|
+
}
|
|
5175
|
+
const newParent = ch.parent;
|
|
5176
|
+
if (previousParent && newParent && newParent !== previousParent) {
|
|
5177
|
+
void this._sendControl(previousParent, this.codec.encodeLeave(ch.id.key)).catch(() => { });
|
|
5178
|
+
if (parentUpgrade.mode === "shadow") {
|
|
5179
|
+
ch.metrics.parentShadowPromote += 1;
|
|
5180
|
+
ch.parentShadow = undefined;
|
|
5181
|
+
clearShadowInFlight();
|
|
5182
|
+
clearFailedBackoff();
|
|
5183
|
+
}
|
|
5184
|
+
return true;
|
|
5185
|
+
}
|
|
5186
|
+
return false;
|
|
5187
|
+
}
|
|
5188
|
+
if (useShadowDualPath) {
|
|
5189
|
+
if (ch.parent === previousParent) {
|
|
5190
|
+
ch.level = previousLevel;
|
|
5191
|
+
ch.routeFromRoot = previousRouteFromRoot;
|
|
5192
|
+
ch.lastParentDataAt = previousLastParentDataAt;
|
|
5193
|
+
ch.lastParentUpgradeActivityAt =
|
|
5194
|
+
previousLastParentUpgradeActivityAt;
|
|
5195
|
+
ch.receivedAnyParentData = previousReceivedAnyParentData;
|
|
5196
|
+
}
|
|
5197
|
+
if (ch.parentShadow?.hash === candidate.hash) {
|
|
5198
|
+
ch.parentShadow = undefined;
|
|
5199
|
+
clearShadowInFlight();
|
|
5200
|
+
ch.metrics.parentShadowReset += 1;
|
|
5201
|
+
}
|
|
5202
|
+
rejectProbeCandidate(candidate.hash);
|
|
5203
|
+
deferParentUpgradeRetryUntilFreshData(2);
|
|
5204
|
+
applyFailedBackoff();
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
return false;
|
|
5209
|
+
}
|
|
5210
|
+
async tryJoinOnce(ch, parentHash, reqId, timeoutMs, signal, options) {
|
|
5211
|
+
if (ch.parent && options?.allowReplace !== true)
|
|
4153
5212
|
return { ok: true };
|
|
4154
5213
|
if (!this.peers.get(parentHash))
|
|
4155
5214
|
return { ok: false, timedOut: true };
|
|
4156
5215
|
const p = new Promise((resolve) => {
|
|
4157
|
-
ch.pendingJoin.set(reqId, {
|
|
5216
|
+
ch.pendingJoin.set(reqId, {
|
|
5217
|
+
resolve,
|
|
5218
|
+
shadowAttach: options?.shadowAttach === true,
|
|
5219
|
+
});
|
|
4158
5220
|
});
|
|
4159
|
-
await this._sendControl(parentHash, encodeJoinReq(ch.id.key, reqId, ch.bidPerByte));
|
|
5221
|
+
await this._sendControl(parentHash, this.codec.encodeJoinReq(ch.id.key, reqId, ch.bidPerByte, options?.parentUpgradeReservationToken));
|
|
4160
5222
|
const res = await Promise.race([
|
|
4161
5223
|
p,
|
|
4162
5224
|
delay(Math.max(1, timeoutMs), { signal }).then(() => ({
|
|
@@ -4168,13 +5230,41 @@ export class FanoutTree extends DirectStream {
|
|
|
4168
5230
|
ch.pendingJoin.delete(reqId);
|
|
4169
5231
|
return res;
|
|
4170
5232
|
}
|
|
4171
|
-
async
|
|
4172
|
-
const
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
5233
|
+
async kickChildHashes(ch, children, options) {
|
|
5234
|
+
const unique = [...new Set(children)].filter((h) => ch.children.has(h));
|
|
5235
|
+
for (const h of unique) {
|
|
5236
|
+
ch.children.delete(h);
|
|
5237
|
+
ch.dataWriteFailStreakByChild.delete(h);
|
|
5238
|
+
}
|
|
5239
|
+
if (unique.length === 0)
|
|
4176
5240
|
return;
|
|
4177
|
-
|
|
5241
|
+
const resetPeerConnections = options?.resetPeerConnections === true;
|
|
5242
|
+
let kickFailed = false;
|
|
5243
|
+
try {
|
|
5244
|
+
const kickDelivered = await this._sendControlMany(unique, this.codec.encodeKick(ch.id.key));
|
|
5245
|
+
kickFailed = !kickDelivered;
|
|
5246
|
+
}
|
|
5247
|
+
catch (error) {
|
|
5248
|
+
kickFailed = true;
|
|
5249
|
+
throw error;
|
|
5250
|
+
}
|
|
5251
|
+
finally {
|
|
5252
|
+
if (resetPeerConnections && kickFailed) {
|
|
5253
|
+
// If KICK cannot be written on the existing stream, close the peer
|
|
5254
|
+
// connection so the topology change becomes observable.
|
|
5255
|
+
for (const h of unique) {
|
|
5256
|
+
const stream = this.peers.get(h);
|
|
5257
|
+
if (!stream)
|
|
5258
|
+
continue;
|
|
5259
|
+
void this.components.connectionManager
|
|
5260
|
+
.closeConnections(stream.peerId)
|
|
5261
|
+
.catch(() => { });
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
async kickChildren(ch) {
|
|
5267
|
+
await this.kickChildHashes(ch, [...ch.children.keys()]);
|
|
4178
5268
|
}
|
|
4179
5269
|
async onDataMessage(from, peerStream, message, seenBefore) {
|
|
4180
5270
|
const ignore = this.shouldIgnore(message, seenBefore);
|
|
@@ -4203,6 +5293,8 @@ export class FanoutTree extends DirectStream {
|
|
|
4203
5293
|
kind === MSG_TRACKER_QUERY ||
|
|
4204
5294
|
kind === MSG_TRACKER_REPLY ||
|
|
4205
5295
|
kind === MSG_TRACKER_FEEDBACK ||
|
|
5296
|
+
kind === MSG_PARENT_PROBE_REQ ||
|
|
5297
|
+
kind === MSG_PARENT_PROBE_REPLY ||
|
|
4206
5298
|
kind === MSG_PROVIDER_ANNOUNCE ||
|
|
4207
5299
|
kind === MSG_PROVIDER_QUERY ||
|
|
4208
5300
|
kind === MSG_PROVIDER_REPLY ||
|
|
@@ -4224,33 +5316,26 @@ export class FanoutTree extends DirectStream {
|
|
|
4224
5316
|
fromHash === ch.parent)) {
|
|
4225
5317
|
return false;
|
|
4226
5318
|
}
|
|
5319
|
+
if (!ch && kind === MSG_JOIN_REQ && data.length >= 1 + 32 + 4) {
|
|
5320
|
+
// A joiner probed us for a channel we do not host (connected-peer
|
|
5321
|
+
// fallbacks routinely hit bootstraps and non-subscribers). Failing
|
|
5322
|
+
// fast lets the joiner spend its probe budget elsewhere instead of
|
|
5323
|
+
// burning a full join-request timeout and a long cooldown on us.
|
|
5324
|
+
const reqId = readU32BE(data, 33);
|
|
5325
|
+
void this._sendControl(fromHash, this.codec.encodeJoinReject(channelKey, reqId, JOIN_REJECT_NOT_ATTACHED)).catch(() => { });
|
|
5326
|
+
return true;
|
|
5327
|
+
}
|
|
4227
5328
|
if (kind === MSG_TRACKER_ANNOUNCE) {
|
|
4228
|
-
|
|
5329
|
+
const decoded = this.codec.decodeTrackerAnnounce(data);
|
|
5330
|
+
if (!decoded)
|
|
4229
5331
|
return false;
|
|
4230
|
-
const ttlMs
|
|
4231
|
-
const level = readU16BE(data, 37);
|
|
4232
|
-
// maxChildren is currently unused, but kept in the wire format.
|
|
4233
|
-
// const maxChildren = readU16BE(data, 39);
|
|
4234
|
-
const freeSlots = readU16BE(data, 41);
|
|
4235
|
-
const bidPerByte = readU32BE(data, 43);
|
|
4236
|
-
const addrCount = data[47];
|
|
4237
|
-
let offset = 48;
|
|
4238
|
-
const addrs = [];
|
|
4239
|
-
const max = Math.min(addrCount, 16);
|
|
4240
|
-
for (let i = 0; i < max; i++) {
|
|
4241
|
-
if (offset + 2 > data.length)
|
|
4242
|
-
break;
|
|
4243
|
-
const len = readU16BE(data, offset);
|
|
4244
|
-
offset += 2;
|
|
4245
|
-
if (offset + len > data.length)
|
|
4246
|
-
break;
|
|
4247
|
-
addrs.push(data.subarray(offset, offset + len));
|
|
4248
|
-
offset += len;
|
|
4249
|
-
}
|
|
5332
|
+
const { ttlMs, level, freeSlots, bidPerByte, addrs } = decoded;
|
|
4250
5333
|
if (addrs.length === 0) {
|
|
4251
5334
|
try {
|
|
4252
5335
|
const peer = await this.components.peerStore.get(peerStream.peerId);
|
|
4253
|
-
const addresses = Array.isArray(peer?.addresses)
|
|
5336
|
+
const addresses = Array.isArray(peer?.addresses)
|
|
5337
|
+
? peer.addresses
|
|
5338
|
+
: [];
|
|
4254
5339
|
for (const a of addresses) {
|
|
4255
5340
|
const ma = a?.multiaddr ?? a;
|
|
4256
5341
|
const bytes = normalizeUint8Array(ma?.bytes);
|
|
@@ -4296,10 +5381,10 @@ export class FanoutTree extends DirectStream {
|
|
|
4296
5381
|
return true;
|
|
4297
5382
|
}
|
|
4298
5383
|
if (kind === MSG_TRACKER_QUERY) {
|
|
4299
|
-
|
|
5384
|
+
const decoded = this.codec.decodeTrackerQuery(data);
|
|
5385
|
+
if (!decoded)
|
|
4300
5386
|
return false;
|
|
4301
|
-
const reqId =
|
|
4302
|
-
const want = readU16BE(data, 37);
|
|
5387
|
+
const { reqId, want } = decoded;
|
|
4303
5388
|
const now = Date.now();
|
|
4304
5389
|
this.touchTrackerNamespace(suffixKey, now);
|
|
4305
5390
|
const byPeer = this.trackerBySuffixKey.get(suffixKey);
|
|
@@ -4312,8 +5397,9 @@ export class FanoutTree extends DirectStream {
|
|
|
4312
5397
|
}
|
|
4313
5398
|
if (hash === fromHash)
|
|
4314
5399
|
continue;
|
|
4315
|
-
if (e.freeSlots <= 0)
|
|
5400
|
+
if (e.freeSlots <= 0) {
|
|
4316
5401
|
continue;
|
|
5402
|
+
}
|
|
4317
5403
|
entries.push(e);
|
|
4318
5404
|
}
|
|
4319
5405
|
this.pruneTrackerNamespaceIfEmpty(suffixKey);
|
|
@@ -4327,21 +5413,28 @@ export class FanoutTree extends DirectStream {
|
|
|
4327
5413
|
return b.bidPerByte - a.bidPerByte;
|
|
4328
5414
|
return a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0;
|
|
4329
5415
|
});
|
|
4330
|
-
|
|
4331
|
-
|
|
5416
|
+
// Returning the deterministic head of the directory sends every
|
|
5417
|
+
// concurrent joiner to the same few candidates while deeper capacity
|
|
5418
|
+
// goes unprobed. Sample the reply from a wider best-first pool so
|
|
5419
|
+
// load spreads across all advertised capacity.
|
|
5420
|
+
const wantCount = clampU16(want);
|
|
5421
|
+
const poolSize = Math.min(entries.length, Math.max(wantCount * 4, wantCount));
|
|
5422
|
+
const pool = entries.slice(0, poolSize);
|
|
5423
|
+
for (let i = 0; i < Math.min(wantCount, pool.length); i++) {
|
|
5424
|
+
const j = i + Math.floor(this.random() * (pool.length - i));
|
|
5425
|
+
const tmp = pool[i];
|
|
5426
|
+
pool[i] = pool[j];
|
|
5427
|
+
pool[j] = tmp;
|
|
5428
|
+
}
|
|
5429
|
+
const picked = pool.slice(0, wantCount);
|
|
5430
|
+
void this._sendControl(fromHash, this.codec.encodeTrackerReply(channelKey, reqId, picked));
|
|
4332
5431
|
return true;
|
|
4333
5432
|
}
|
|
4334
5433
|
if (kind === MSG_TRACKER_FEEDBACK) {
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
const hashLen = data[33];
|
|
4338
|
-
let offset = 34;
|
|
4339
|
-
if (offset + hashLen + 2 > data.length)
|
|
5434
|
+
const decoded = this.codec.decodeTrackerFeedback(data);
|
|
5435
|
+
if (!decoded)
|
|
4340
5436
|
return false;
|
|
4341
|
-
const candidateHash
|
|
4342
|
-
offset += hashLen;
|
|
4343
|
-
const event = data[offset++] & 0xff;
|
|
4344
|
-
const reason = data[offset++] & 0xff;
|
|
5437
|
+
const { candidateHash, event, reason } = decoded;
|
|
4345
5438
|
const byPeer = this.trackerBySuffixKey.get(suffixKey);
|
|
4346
5439
|
if (!byPeer)
|
|
4347
5440
|
return true;
|
|
@@ -4359,13 +5452,15 @@ export class FanoutTree extends DirectStream {
|
|
|
4359
5452
|
entry.freeSlots = clampU16(Math.max(0, entry.freeSlots - 1));
|
|
4360
5453
|
return true;
|
|
4361
5454
|
}
|
|
4362
|
-
if (event === TRACKER_FEEDBACK_DIAL_FAILED ||
|
|
5455
|
+
if (event === TRACKER_FEEDBACK_DIAL_FAILED ||
|
|
5456
|
+
event === TRACKER_FEEDBACK_JOIN_TIMEOUT) {
|
|
4363
5457
|
byPeer.delete(candidateHash);
|
|
4364
5458
|
this.pruneTrackerNamespaceIfEmpty(suffixKey);
|
|
4365
5459
|
return true;
|
|
4366
5460
|
}
|
|
4367
5461
|
if (event === TRACKER_FEEDBACK_JOIN_REJECT) {
|
|
4368
|
-
if (reason === JOIN_REJECT_NO_CAPACITY ||
|
|
5462
|
+
if (reason === JOIN_REJECT_NO_CAPACITY ||
|
|
5463
|
+
reason === JOIN_REJECT_NOT_ATTACHED) {
|
|
4369
5464
|
entry.freeSlots = 0;
|
|
4370
5465
|
// Expire earlier to reduce time-to-recovery if the entry is stale.
|
|
4371
5466
|
entry.expiresAt = Math.min(entry.expiresAt, now + 2_000);
|
|
@@ -4375,24 +5470,11 @@ export class FanoutTree extends DirectStream {
|
|
|
4375
5470
|
return true;
|
|
4376
5471
|
}
|
|
4377
5472
|
if (kind === MSG_PROVIDER_ANNOUNCE) {
|
|
4378
|
-
|
|
5473
|
+
const decoded = this.codec.decodeProviderAnnounce(data);
|
|
5474
|
+
if (!decoded)
|
|
4379
5475
|
return false;
|
|
4380
5476
|
const providerId = this.getProviderNamespaceIdFromKey(channelKey, suffixKey);
|
|
4381
|
-
const ttlMs =
|
|
4382
|
-
const addrCount = data[37];
|
|
4383
|
-
let offset = 38;
|
|
4384
|
-
const addrs = [];
|
|
4385
|
-
const max = Math.min(addrCount, 16);
|
|
4386
|
-
for (let i = 0; i < max; i++) {
|
|
4387
|
-
if (offset + 2 > data.length)
|
|
4388
|
-
break;
|
|
4389
|
-
const len = readU16BE(data, offset);
|
|
4390
|
-
offset += 2;
|
|
4391
|
-
if (offset + len > data.length)
|
|
4392
|
-
break;
|
|
4393
|
-
addrs.push(data.subarray(offset, offset + len));
|
|
4394
|
-
offset += len;
|
|
4395
|
-
}
|
|
5477
|
+
const { ttlMs, addrs } = decoded;
|
|
4396
5478
|
if (addrs.length === 0) {
|
|
4397
5479
|
try {
|
|
4398
5480
|
const peer = await this.components.peerStore.get(peerStream.peerId);
|
|
@@ -4446,11 +5528,10 @@ export class FanoutTree extends DirectStream {
|
|
|
4446
5528
|
return true;
|
|
4447
5529
|
}
|
|
4448
5530
|
if (kind === MSG_PROVIDER_QUERY) {
|
|
4449
|
-
|
|
5531
|
+
const decoded = this.codec.decodeProviderQuery(data);
|
|
5532
|
+
if (!decoded)
|
|
4450
5533
|
return false;
|
|
4451
|
-
const reqId =
|
|
4452
|
-
const want = readU16BE(data, 37);
|
|
4453
|
-
const seed = readU32BE(data, 39);
|
|
5534
|
+
const { reqId, want, seed } = decoded;
|
|
4454
5535
|
const now = Date.now();
|
|
4455
5536
|
this.touchProviderNamespace(suffixKey, now);
|
|
4456
5537
|
const byPeer = this.providerBySuffixKey.get(suffixKey);
|
|
@@ -4491,14 +5572,15 @@ export class FanoutTree extends DirectStream {
|
|
|
4491
5572
|
}
|
|
4492
5573
|
}
|
|
4493
5574
|
const picked = entries.slice(0, clampU16(want));
|
|
4494
|
-
void this._sendControl(fromHash, encodeProviderReply(channelKey, reqId, picked));
|
|
5575
|
+
void this._sendControl(fromHash, this.codec.encodeProviderReply(channelKey, reqId, picked));
|
|
4495
5576
|
return true;
|
|
4496
5577
|
}
|
|
4497
5578
|
if (kind === MSG_PROVIDER_SUBSCRIBE) {
|
|
4498
|
-
|
|
5579
|
+
const decoded = this.codec.decodeProviderSubscribe(data);
|
|
5580
|
+
if (!decoded)
|
|
4499
5581
|
return false;
|
|
4500
|
-
const want = Math.max(1,
|
|
4501
|
-
const ttlMs = Math.min(120_000, Math.max(1_000,
|
|
5582
|
+
const want = Math.max(1, decoded.want);
|
|
5583
|
+
const ttlMs = Math.min(120_000, Math.max(1_000, decoded.ttlMs));
|
|
4502
5584
|
const now = Date.now();
|
|
4503
5585
|
let watchers = this.providerWatchersBySuffixKey.get(suffixKey);
|
|
4504
5586
|
if (!watchers) {
|
|
@@ -4520,18 +5602,18 @@ export class FanoutTree extends DirectStream {
|
|
|
4520
5602
|
return true;
|
|
4521
5603
|
}
|
|
4522
5604
|
if (kind === MSG_PROVIDER_REPLY) {
|
|
4523
|
-
|
|
5605
|
+
const decoded = this.codec.decodeProviderReply(data);
|
|
5606
|
+
if (!decoded)
|
|
4524
5607
|
return false;
|
|
4525
|
-
const reqId =
|
|
5608
|
+
const reqId = decoded.reqId;
|
|
4526
5609
|
const pendingByReq = this.pendingProviderQueryBySuffixKey.get(suffixKey);
|
|
4527
5610
|
const pending = pendingByReq?.get(reqId);
|
|
4528
5611
|
if (!pending)
|
|
4529
5612
|
return true;
|
|
4530
5613
|
pendingByReq.delete(reqId);
|
|
4531
|
-
const count = data[37];
|
|
4532
5614
|
const now = Date.now();
|
|
4533
5615
|
this.touchProviderNamespace(suffixKey, now);
|
|
4534
|
-
const
|
|
5616
|
+
const candidates = this.toProviderCandidates(decoded.entries);
|
|
4535
5617
|
this.rememberProviderCandidates(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), candidates, now + 60_000);
|
|
4536
5618
|
this.pruneProviderNamespaceIfEmpty(suffixKey);
|
|
4537
5619
|
if (candidates.length > 0) {
|
|
@@ -4541,11 +5623,11 @@ export class FanoutTree extends DirectStream {
|
|
|
4541
5623
|
return true;
|
|
4542
5624
|
}
|
|
4543
5625
|
if (kind === MSG_PROVIDER_NOTIFY) {
|
|
4544
|
-
|
|
5626
|
+
const decoded = this.codec.decodeProviderNotify(data);
|
|
5627
|
+
if (!decoded)
|
|
4545
5628
|
return false;
|
|
4546
|
-
const count = data[33];
|
|
4547
5629
|
const now = Date.now();
|
|
4548
|
-
const
|
|
5630
|
+
const providers = this.toProviderCandidates(decoded.entries);
|
|
4549
5631
|
this.rememberProviderCandidates(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), providers, now + 60_000);
|
|
4550
5632
|
if (providers.length > 0) {
|
|
4551
5633
|
this.emitProviderUpdate(this.getProviderNamespaceIdFromKey(channelKey, suffixKey), providers);
|
|
@@ -4555,45 +5637,18 @@ export class FanoutTree extends DirectStream {
|
|
|
4555
5637
|
if (kind === MSG_TRACKER_REPLY) {
|
|
4556
5638
|
if (!ch)
|
|
4557
5639
|
return true;
|
|
4558
|
-
|
|
5640
|
+
const decoded = this.codec.decodeTrackerReply(data);
|
|
5641
|
+
if (!decoded)
|
|
4559
5642
|
return false;
|
|
4560
|
-
const reqId =
|
|
5643
|
+
const reqId = decoded.reqId;
|
|
4561
5644
|
const pending = ch.pendingTrackerQuery.get(reqId);
|
|
4562
|
-
if (
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
const count = data[37];
|
|
4566
|
-
let offset = 38;
|
|
5645
|
+
if (pending) {
|
|
5646
|
+
ch.pendingTrackerQuery.delete(reqId);
|
|
5647
|
+
}
|
|
4567
5648
|
const candidates = [];
|
|
4568
|
-
const
|
|
4569
|
-
for (let i = 0; i < max; i++) {
|
|
4570
|
-
if (offset + 1 > data.length)
|
|
4571
|
-
break;
|
|
4572
|
-
const hashLen = data[offset++];
|
|
4573
|
-
if (offset + hashLen > data.length)
|
|
4574
|
-
break;
|
|
4575
|
-
const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
|
|
4576
|
-
offset += hashLen;
|
|
4577
|
-
if (offset + 2 + 2 + 4 + 1 > data.length)
|
|
4578
|
-
break;
|
|
4579
|
-
const level = readU16BE(data, offset);
|
|
4580
|
-
offset += 2;
|
|
4581
|
-
const freeSlots = readU16BE(data, offset);
|
|
4582
|
-
offset += 2;
|
|
4583
|
-
const bidPerByte = readU32BE(data, offset);
|
|
4584
|
-
offset += 4;
|
|
4585
|
-
const addrCount = data[offset++];
|
|
5649
|
+
for (const entry of decoded.entries) {
|
|
4586
5650
|
const addrs = [];
|
|
4587
|
-
const
|
|
4588
|
-
for (let j = 0; j < addrMax; j++) {
|
|
4589
|
-
if (offset + 2 > data.length)
|
|
4590
|
-
break;
|
|
4591
|
-
const len = readU16BE(data, offset);
|
|
4592
|
-
offset += 2;
|
|
4593
|
-
if (offset + len > data.length)
|
|
4594
|
-
break;
|
|
4595
|
-
const bytes = data.subarray(offset, offset + len);
|
|
4596
|
-
offset += len;
|
|
5651
|
+
for (const bytes of entry.addrs) {
|
|
4597
5652
|
try {
|
|
4598
5653
|
addrs.push(multiaddr(bytes));
|
|
4599
5654
|
}
|
|
@@ -4601,10 +5656,56 @@ export class FanoutTree extends DirectStream {
|
|
|
4601
5656
|
// ignore invalid multiaddrs
|
|
4602
5657
|
}
|
|
4603
5658
|
}
|
|
4604
|
-
candidates.push({
|
|
4605
|
-
|
|
5659
|
+
candidates.push({
|
|
5660
|
+
hash: entry.hash,
|
|
5661
|
+
level: entry.level,
|
|
5662
|
+
freeSlots: entry.freeSlots,
|
|
5663
|
+
bidPerByte: entry.bidPerByte,
|
|
5664
|
+
addrs,
|
|
5665
|
+
});
|
|
5666
|
+
this.cacheKnownCandidateAddrs(ch, entry.hash, addrs);
|
|
4606
5667
|
}
|
|
4607
|
-
pending
|
|
5668
|
+
if (pending) {
|
|
5669
|
+
pending.resolve(candidates);
|
|
5670
|
+
}
|
|
5671
|
+
else if (candidates.length > 0) {
|
|
5672
|
+
// Late reply: the query already timed out, but the candidate
|
|
5673
|
+
// list is still fresh directory state. Under load the 1s query
|
|
5674
|
+
// timeout is routinely missed while trackers hold plenty of
|
|
5675
|
+
// capacity - dropping these replies starves joiners of
|
|
5676
|
+
// candidates they already paid a round trip for.
|
|
5677
|
+
const merged = new Map();
|
|
5678
|
+
for (const c of ch.cachedTrackerCandidates)
|
|
5679
|
+
merged.set(c.hash, c);
|
|
5680
|
+
for (const c of candidates)
|
|
5681
|
+
merged.set(c.hash, c);
|
|
5682
|
+
ch.cachedTrackerCandidates = [...merged.values()].slice(-256);
|
|
5683
|
+
}
|
|
5684
|
+
return true;
|
|
5685
|
+
}
|
|
5686
|
+
if (kind === MSG_PARENT_PROBE_REQ) {
|
|
5687
|
+
if (!ch || ch.closed)
|
|
5688
|
+
return true;
|
|
5689
|
+
const decoded = this.codec.decodeParentProbeReq(data);
|
|
5690
|
+
if (!decoded)
|
|
5691
|
+
return false;
|
|
5692
|
+
this.touchPeerHint(ch, fromHash);
|
|
5693
|
+
void this._sendControl(fromHash, this.encodeParentProbeReplyForChannel(ch, decoded.reqId, fromHash, decoded.minFreeSlots, decoded.reserveRootCapacity)).catch(() => { });
|
|
5694
|
+
return true;
|
|
5695
|
+
}
|
|
5696
|
+
if (kind === MSG_PARENT_PROBE_REPLY) {
|
|
5697
|
+
if (!ch || ch.closed)
|
|
5698
|
+
return true;
|
|
5699
|
+
const decoded = this.codec.decodeParentProbeReply(data, fromHash);
|
|
5700
|
+
if (!decoded)
|
|
5701
|
+
return false;
|
|
5702
|
+
const { reqId, ...reply } = decoded;
|
|
5703
|
+
const pending = ch.pendingParentProbe.get(reqId);
|
|
5704
|
+
if (!pending)
|
|
5705
|
+
return true;
|
|
5706
|
+
ch.pendingParentProbe.delete(reqId);
|
|
5707
|
+
pending.resolve(reply);
|
|
5708
|
+
this.touchPeerHint(ch, fromHash);
|
|
4608
5709
|
return true;
|
|
4609
5710
|
}
|
|
4610
5711
|
if (!ch)
|
|
@@ -4623,20 +5724,20 @@ export class FanoutTree extends DirectStream {
|
|
|
4623
5724
|
return true;
|
|
4624
5725
|
}
|
|
4625
5726
|
if (kind === MSG_ROUTE_QUERY) {
|
|
4626
|
-
|
|
5727
|
+
const decoded = this.codec.decodeRouteQuery(data);
|
|
5728
|
+
if (!decoded)
|
|
4627
5729
|
return false;
|
|
4628
|
-
const reqId =
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
void this._sendControl(fromHash, encodeRouteReply(ch.id.key, reqId)).catch(() => { });
|
|
5730
|
+
const reqId = decoded.reqId;
|
|
5731
|
+
if (decoded.targetHash == null) {
|
|
5732
|
+
void this._sendControl(fromHash, this.codec.encodeRouteReply(ch.id.key, reqId)).catch(() => { });
|
|
4632
5733
|
return true;
|
|
4633
5734
|
}
|
|
4634
|
-
const targetHash =
|
|
5735
|
+
const targetHash = decoded.targetHash;
|
|
4635
5736
|
const localRoute = targetHash === this.publicKeyHash && ch.routeFromRoot
|
|
4636
5737
|
? ch.routeFromRoot
|
|
4637
5738
|
: this.getCachedRoute(ch, targetHash);
|
|
4638
5739
|
if (this.isRouteValidForChannel(ch, localRoute)) {
|
|
4639
|
-
void this._sendControl(fromHash, encodeRouteReply(ch.id.key, reqId, localRoute)).catch(() => { });
|
|
5740
|
+
void this._sendControl(fromHash, this.codec.encodeRouteReply(ch.id.key, reqId, localRoute)).catch(() => { });
|
|
4640
5741
|
return true;
|
|
4641
5742
|
}
|
|
4642
5743
|
if (ch.isRoot) {
|
|
@@ -4646,7 +5747,7 @@ export class FanoutTree extends DirectStream {
|
|
|
4646
5747
|
if (rootRoute)
|
|
4647
5748
|
this.cacheRoute(ch, rootRoute);
|
|
4648
5749
|
if (rootRoute) {
|
|
4649
|
-
void this._sendControl(fromHash, encodeRouteReply(ch.id.key, reqId, rootRoute)).catch(() => { });
|
|
5750
|
+
void this._sendControl(fromHash, this.codec.encodeRouteReply(ch.id.key, reqId, rootRoute)).catch(() => { });
|
|
4650
5751
|
return true;
|
|
4651
5752
|
}
|
|
4652
5753
|
}
|
|
@@ -4659,16 +5760,19 @@ export class FanoutTree extends DirectStream {
|
|
|
4659
5760
|
}
|
|
4660
5761
|
// Cache miss on a parent->child (or root->child) query:
|
|
4661
5762
|
// recursively search subtree branches and reply with the first valid route.
|
|
4662
|
-
this.proxyRouteQuery(ch, fromHash, reqId, targetHash, [
|
|
5763
|
+
this.proxyRouteQuery(ch, fromHash, reqId, targetHash, [
|
|
5764
|
+
...ch.children.keys(),
|
|
5765
|
+
]);
|
|
4663
5766
|
return true;
|
|
4664
5767
|
}
|
|
4665
5768
|
if (kind === MSG_ROUTE_REPLY) {
|
|
4666
|
-
|
|
5769
|
+
const decoded = this.codec.decodeRouteReply(data);
|
|
5770
|
+
if (!decoded)
|
|
4667
5771
|
return false;
|
|
4668
|
-
const reqId =
|
|
4669
|
-
const
|
|
4670
|
-
|
|
4671
|
-
|
|
5772
|
+
const { reqId, route } = decoded;
|
|
5773
|
+
const parsedRoute = this.isRouteValidForChannel(ch, route)
|
|
5774
|
+
? route
|
|
5775
|
+
: undefined;
|
|
4672
5776
|
if (parsedRoute) {
|
|
4673
5777
|
this.cacheRoute(ch, parsedRoute);
|
|
4674
5778
|
}
|
|
@@ -4694,11 +5798,16 @@ export class FanoutTree extends DirectStream {
|
|
|
4694
5798
|
return true;
|
|
4695
5799
|
}
|
|
4696
5800
|
if (kind === MSG_IHAVE) {
|
|
4697
|
-
|
|
5801
|
+
const decoded = this.codec.decodeIHave(data);
|
|
5802
|
+
if (!decoded)
|
|
4698
5803
|
return false;
|
|
4699
|
-
const haveFrom =
|
|
4700
|
-
const haveToExclusive = readU32BE(data, 37);
|
|
5804
|
+
const { haveFrom, haveToExclusive } = decoded;
|
|
4701
5805
|
const now = Date.now();
|
|
5806
|
+
if (ch.parent && fromHash === ch.parent) {
|
|
5807
|
+
ch.lastParentDataAt = now;
|
|
5808
|
+
ch.receivedAnyParentData = true;
|
|
5809
|
+
ch.parentRepairUnansweredStreak = 0;
|
|
5810
|
+
}
|
|
4702
5811
|
const prev = ch.haveByPeer.get(fromHash);
|
|
4703
5812
|
if (prev) {
|
|
4704
5813
|
prev.haveFrom = haveFrom;
|
|
@@ -4715,6 +5824,16 @@ export class FanoutTree extends DirectStream {
|
|
|
4715
5824
|
});
|
|
4716
5825
|
}
|
|
4717
5826
|
this.touchPeerHint(ch, fromHash, now);
|
|
5827
|
+
if (ch.parent && fromHash === ch.parent && ch.maxDataAgeMs === 0) {
|
|
5828
|
+
// A watermark from our parent doubles as gap detection: if data
|
|
5829
|
+
// writes to us were lost (e.g. burst into a not-yet-writable
|
|
5830
|
+
// stream) we may have received nothing at all and would
|
|
5831
|
+
// otherwise never know data exists. Mark the advertised range
|
|
5832
|
+
// missing so the repair loop pulls it. Deadline-oriented live
|
|
5833
|
+
// channels (maxDataAgeMs > 0) skip this: catching up on stale
|
|
5834
|
+
// history would trade deadline latency for completeness.
|
|
5835
|
+
this.noteEnd(ch, fromHash, haveToExclusive);
|
|
5836
|
+
}
|
|
4718
5837
|
// Opportunistic reciprocity: if we still have room in our lazy mesh, add
|
|
4719
5838
|
// peers that are actively exchanging IHAVE summaries with us.
|
|
4720
5839
|
if (ch.neighborRepair &&
|
|
@@ -4728,11 +5847,21 @@ export class FanoutTree extends DirectStream {
|
|
|
4728
5847
|
return true;
|
|
4729
5848
|
}
|
|
4730
5849
|
if (kind === MSG_JOIN_REQ) {
|
|
4731
|
-
|
|
5850
|
+
const decoded = this.codec.decodeJoinReq(data);
|
|
5851
|
+
if (!decoded)
|
|
4732
5852
|
return false;
|
|
4733
|
-
const reqId =
|
|
4734
|
-
const bidPerByte = readU32BE(data, 37);
|
|
5853
|
+
const { reqId, bidPerByte, parentUpgradeReservationToken } = decoded;
|
|
4735
5854
|
this.pruneDisconnectedChildren(ch);
|
|
5855
|
+
const hasParentUpgradeReservation = ch.isRoot && parentUpgradeReservationToken > 0;
|
|
5856
|
+
const consumedParentUpgradeReservation = hasParentUpgradeReservation
|
|
5857
|
+
? this.consumeParentUpgradeReservation(ch, fromHash, parentUpgradeReservationToken)
|
|
5858
|
+
: false;
|
|
5859
|
+
if (hasParentUpgradeReservation && !consumedParentUpgradeReservation) {
|
|
5860
|
+
ch.metrics.parentUpgradeRootReservationRejected += 1;
|
|
5861
|
+
void this.sendJoinReject(ch, fromHash, reqId, JOIN_REJECT_NO_CAPACITY).catch(() => { });
|
|
5862
|
+
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
5863
|
+
return true;
|
|
5864
|
+
}
|
|
4736
5865
|
// Only accept if we're already attached.
|
|
4737
5866
|
if (!ch.isRoot && !ch.parent) {
|
|
4738
5867
|
void this.sendJoinReject(ch, fromHash, reqId, JOIN_REJECT_NOT_ATTACHED).catch(() => { });
|
|
@@ -4755,8 +5884,16 @@ export class FanoutTree extends DirectStream {
|
|
|
4755
5884
|
void this.sendJoinReject(ch, fromHash, reqId, JOIN_REJECT_NO_CAPACITY).catch(() => { });
|
|
4756
5885
|
return true;
|
|
4757
5886
|
}
|
|
4758
|
-
if (!ch.children.has(fromHash) &&
|
|
4759
|
-
|
|
5887
|
+
if (!ch.children.has(fromHash) &&
|
|
5888
|
+
(consumedParentUpgradeReservation
|
|
5889
|
+
? ch.children.size >= ch.effectiveMaxChildren
|
|
5890
|
+
: ch.children.size +
|
|
5891
|
+
this.parentUpgradeReservationCount(ch, fromHash) >=
|
|
5892
|
+
ch.effectiveMaxChildren)) {
|
|
5893
|
+
const blockedByReservedRootCapacity = ch.isRoot &&
|
|
5894
|
+
!consumedParentUpgradeReservation &&
|
|
5895
|
+
ch.children.size < ch.effectiveMaxChildren;
|
|
5896
|
+
if (!ch.allowKick || blockedByReservedRootCapacity) {
|
|
4760
5897
|
void this.sendJoinReject(ch, fromHash, reqId, JOIN_REJECT_NO_CAPACITY).catch(() => { });
|
|
4761
5898
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
4762
5899
|
return true;
|
|
@@ -4774,56 +5911,72 @@ export class FanoutTree extends DirectStream {
|
|
|
4774
5911
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
4775
5912
|
return true;
|
|
4776
5913
|
}
|
|
4777
|
-
|
|
4778
|
-
ch.dataWriteFailStreakByChild.delete(worstChild);
|
|
4779
|
-
void this._sendControl(worstChild, encodeKick(ch.id.key));
|
|
5914
|
+
void this.kickChildHashes(ch, [worstChild]).catch(() => { });
|
|
4780
5915
|
}
|
|
4781
5916
|
ch.children.set(fromHash, { bidPerByte });
|
|
5917
|
+
if (ch.isRoot && consumedParentUpgradeReservation) {
|
|
5918
|
+
ch.parentUpgradeTrackerNoCapacityUntil = Math.max(ch.parentUpgradeTrackerNoCapacityUntil, Date.now() + 2_000);
|
|
5919
|
+
}
|
|
4782
5920
|
this.touchPeerHint(ch, fromHash);
|
|
4783
|
-
|
|
5921
|
+
const joinAccept = this.codec.encodeJoinAccept(ch.id.key, reqId, ch.level, ch.routeFromRoot, this.getHaveRange(ch));
|
|
5922
|
+
void (async () => {
|
|
5923
|
+
await this._sendControl(fromHash, joinAccept);
|
|
5924
|
+
if (ch.endSeqExclusive > 0) {
|
|
5925
|
+
await this._sendControl(fromHash, this.codec.encodeEnd(ch.id.key, ch.endSeqExclusive));
|
|
5926
|
+
}
|
|
5927
|
+
})().catch(() => { });
|
|
4784
5928
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
4785
5929
|
return true;
|
|
4786
5930
|
}
|
|
4787
5931
|
if (kind === MSG_JOIN_ACCEPT || kind === MSG_JOIN_REJECT) {
|
|
4788
|
-
|
|
5932
|
+
const reqId = this.codec.decodeJoinResponseReqId(data);
|
|
5933
|
+
if (reqId == null)
|
|
4789
5934
|
return false;
|
|
4790
|
-
|
|
5935
|
+
// Any explicit join response proves this path is alive end to end.
|
|
5936
|
+
this.noteJoinResponse(fromHash);
|
|
4791
5937
|
const pending = ch.pendingJoin.get(reqId);
|
|
4792
5938
|
if (!pending)
|
|
4793
5939
|
return true;
|
|
4794
5940
|
ch.pendingJoin.delete(reqId);
|
|
4795
5941
|
if (kind === MSG_JOIN_ACCEPT) {
|
|
4796
|
-
|
|
5942
|
+
const decoded = this.codec.decodeJoinAccept(data);
|
|
5943
|
+
if (!decoded)
|
|
4797
5944
|
return false;
|
|
4798
|
-
const parentLevel =
|
|
4799
|
-
const routeCount = Math.min(255, data[39]);
|
|
4800
|
-
let offset = 40;
|
|
4801
|
-
const parentRouteFromRoot = [];
|
|
4802
|
-
const max = Math.min(routeCount, MAX_ROUTE_HOPS);
|
|
4803
|
-
for (let i = 0; i < max; i++) {
|
|
4804
|
-
if (offset + 1 > data.length)
|
|
4805
|
-
break;
|
|
4806
|
-
const len = data[offset++];
|
|
4807
|
-
if (len === 0)
|
|
4808
|
-
break;
|
|
4809
|
-
if (offset + len > data.length)
|
|
4810
|
-
break;
|
|
4811
|
-
parentRouteFromRoot.push(textDecoder.decode(data.subarray(offset, offset + len)));
|
|
4812
|
-
offset += len;
|
|
4813
|
-
}
|
|
5945
|
+
const { parentLevel, parentRouteFromRoot } = decoded;
|
|
4814
5946
|
const hasValidParentRoute = parentRouteFromRoot.length > 0 &&
|
|
4815
5947
|
parentRouteFromRoot[0] === ch.id.root &&
|
|
4816
5948
|
parentRouteFromRoot[parentRouteFromRoot.length - 1] === fromHash;
|
|
4817
5949
|
// Defensive: a JOIN_ACCEPT without a rooted route token (unless the parent is the
|
|
4818
5950
|
// actual root) can create stable disconnected components. Treat it as a reject.
|
|
4819
5951
|
if (fromHash !== ch.id.root && !hasValidParentRoute) {
|
|
4820
|
-
pending.resolve({
|
|
5952
|
+
pending.resolve({
|
|
5953
|
+
ok: false,
|
|
5954
|
+
rejectReason: JOIN_REJECT_NOT_ATTACHED,
|
|
5955
|
+
});
|
|
5956
|
+
return true;
|
|
5957
|
+
}
|
|
5958
|
+
const acceptedParentRoute = hasValidParentRoute || fromHash === ch.id.root
|
|
5959
|
+
? fromHash === ch.id.root && !hasValidParentRoute
|
|
5960
|
+
? [ch.id.root]
|
|
5961
|
+
: parentRouteFromRoot
|
|
5962
|
+
: undefined;
|
|
5963
|
+
if (pending.shadowAttach) {
|
|
5964
|
+
this.touchPeerHint(ch, fromHash);
|
|
5965
|
+
pending.resolve({
|
|
5966
|
+
ok: true,
|
|
5967
|
+
parentLevel,
|
|
5968
|
+
parentRouteFromRoot: acceptedParentRoute,
|
|
5969
|
+
});
|
|
4821
5970
|
return true;
|
|
4822
5971
|
}
|
|
5972
|
+
const attachedAt = Date.now();
|
|
4823
5973
|
ch.parent = fromHash;
|
|
4824
5974
|
ch.level = parentLevel + 1;
|
|
4825
5975
|
ch.joinedAtLeastOnce = true;
|
|
4826
|
-
ch.lastParentDataAt =
|
|
5976
|
+
ch.lastParentDataAt = attachedAt;
|
|
5977
|
+
ch.lastParentUpgradeActivityAt = attachedAt;
|
|
5978
|
+
ch.parentRepairUnansweredStreak = 0;
|
|
5979
|
+
this.resetParentDataLatency(ch);
|
|
4827
5980
|
// Treat JOIN_ACCEPT as parent liveness for stale re-parenting:
|
|
4828
5981
|
// if callers enable `staleAfterMs`, we should be able to detach even
|
|
4829
5982
|
// before the first data message arrives (for example, during churn
|
|
@@ -4837,58 +5990,56 @@ export class FanoutTree extends DirectStream {
|
|
|
4837
5990
|
// Minimal fallback: parent is the root.
|
|
4838
5991
|
ch.routeFromRoot = [ch.id.root, this.publicKeyHash];
|
|
4839
5992
|
}
|
|
5993
|
+
if (ch.repairEnabled &&
|
|
5994
|
+
ch.maxDataAgeMs === 0 &&
|
|
5995
|
+
decoded.haveRange != null) {
|
|
5996
|
+
// Optional appendix: the parent's current have-range. Lets a
|
|
5997
|
+
// freshly attached child learn immediately which sequences
|
|
5998
|
+
// already exist, so lost deliveries surface as missing seqs
|
|
5999
|
+
// (and repair) instead of silent absence.
|
|
6000
|
+
const { haveFrom, haveToExclusive } = decoded.haveRange;
|
|
6001
|
+
if (haveToExclusive > haveFrom) {
|
|
6002
|
+
// Mark only; the repair loop's own cadence picks the gaps
|
|
6003
|
+
// up, giving in-flight live deliveries time to land first
|
|
6004
|
+
// (an immediate pull here races them into duplicates).
|
|
6005
|
+
this.noteEnd(ch, fromHash, haveToExclusive);
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
4840
6008
|
this.touchPeerHint(ch, fromHash);
|
|
4841
|
-
pending.resolve({
|
|
6009
|
+
pending.resolve({
|
|
6010
|
+
ok: true,
|
|
6011
|
+
parentLevel,
|
|
6012
|
+
parentRouteFromRoot: acceptedParentRoute,
|
|
6013
|
+
});
|
|
4842
6014
|
this.dispatchEvent(new CustomEvent("fanout:joined", {
|
|
4843
|
-
detail: {
|
|
6015
|
+
detail: {
|
|
6016
|
+
topic: ch.id.topic,
|
|
6017
|
+
root: ch.id.root,
|
|
6018
|
+
parent: fromHash,
|
|
6019
|
+
},
|
|
4844
6020
|
}));
|
|
4845
6021
|
void this.announceToTrackers(ch, this.closeController.signal).catch(() => { });
|
|
4846
6022
|
}
|
|
4847
6023
|
else {
|
|
4848
|
-
|
|
6024
|
+
const decoded = this.codec.decodeJoinReject(data);
|
|
6025
|
+
if (!decoded)
|
|
4849
6026
|
return false;
|
|
4850
|
-
const reason =
|
|
6027
|
+
const reason = decoded.reason;
|
|
4851
6028
|
const redirects = [];
|
|
4852
|
-
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
if (offset + 1 > data.length)
|
|
4858
|
-
break;
|
|
4859
|
-
const hashLen = data[offset++];
|
|
4860
|
-
if (hashLen === 0)
|
|
4861
|
-
break;
|
|
4862
|
-
if (offset + hashLen > data.length)
|
|
4863
|
-
break;
|
|
4864
|
-
const hash = textDecoder.decode(data.subarray(offset, offset + hashLen));
|
|
4865
|
-
offset += hashLen;
|
|
4866
|
-
if (offset + 1 > data.length)
|
|
4867
|
-
break;
|
|
4868
|
-
const addrCount = Math.min(255, data[offset++]);
|
|
4869
|
-
const addrs = [];
|
|
4870
|
-
const addrMax = Math.min(addrCount, JOIN_REJECT_REDIRECT_ADDR_MAX);
|
|
4871
|
-
for (let j = 0; j < addrMax; j++) {
|
|
4872
|
-
if (offset + 2 > data.length)
|
|
4873
|
-
break;
|
|
4874
|
-
const len = readU16BE(data, offset);
|
|
4875
|
-
offset += 2;
|
|
4876
|
-
if (offset + len > data.length)
|
|
4877
|
-
break;
|
|
4878
|
-
const bytes = data.subarray(offset, offset + len);
|
|
4879
|
-
offset += len;
|
|
4880
|
-
try {
|
|
4881
|
-
addrs.push(multiaddr(bytes));
|
|
4882
|
-
}
|
|
4883
|
-
catch {
|
|
4884
|
-
// ignore invalid multiaddrs
|
|
4885
|
-
}
|
|
6029
|
+
for (const redirect of decoded.redirects) {
|
|
6030
|
+
const addrs = [];
|
|
6031
|
+
for (const bytes of redirect.addrs) {
|
|
6032
|
+
try {
|
|
6033
|
+
addrs.push(multiaddr(bytes));
|
|
4886
6034
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
this.cacheKnownCandidateAddrs(ch, hash, addrs);
|
|
6035
|
+
catch {
|
|
6036
|
+
// ignore invalid multiaddrs
|
|
4890
6037
|
}
|
|
4891
6038
|
}
|
|
6039
|
+
if (redirect.hash && addrs.length > 0) {
|
|
6040
|
+
redirects.push({ hash: redirect.hash, addrs });
|
|
6041
|
+
this.cacheKnownCandidateAddrs(ch, redirect.hash, addrs);
|
|
6042
|
+
}
|
|
4892
6043
|
}
|
|
4893
6044
|
pending.resolve({ ok: false, rejectReason: reason, redirects });
|
|
4894
6045
|
}
|
|
@@ -4954,11 +6105,10 @@ export class FanoutTree extends DirectStream {
|
|
|
4954
6105
|
const isFromChild = ch.children.has(fromHash);
|
|
4955
6106
|
if (!isFromParent && !isFromChild)
|
|
4956
6107
|
return true;
|
|
4957
|
-
|
|
6108
|
+
const decoded = this.codec.decodeUnicastAck(data);
|
|
6109
|
+
if (!decoded)
|
|
4958
6110
|
return false;
|
|
4959
|
-
const ackToken =
|
|
4960
|
-
const routeCount = Math.min(255, data[41]);
|
|
4961
|
-
const decoded = decodeRoute(data, 42, routeCount);
|
|
6111
|
+
const ackToken = decoded.ackToken;
|
|
4962
6112
|
const route = decoded.route;
|
|
4963
6113
|
const target = route.length > 0 ? route[route.length - 1] : "";
|
|
4964
6114
|
const origin = message.header.signatures?.publicKeys?.[0]?.hashcode?.() ?? fromHash;
|
|
@@ -5038,33 +6188,13 @@ export class FanoutTree extends DirectStream {
|
|
|
5038
6188
|
return true;
|
|
5039
6189
|
}
|
|
5040
6190
|
}
|
|
5041
|
-
|
|
6191
|
+
const decoded = this.codec.decodeUnicast(data);
|
|
6192
|
+
if (!decoded)
|
|
5042
6193
|
return false;
|
|
5043
|
-
const
|
|
5044
|
-
let offset = 34;
|
|
5045
|
-
let ackToken;
|
|
5046
|
-
if (flags & UNICAST_FLAG_ACK) {
|
|
5047
|
-
if (data.length < offset + 8 + 1)
|
|
5048
|
-
return false;
|
|
5049
|
-
ackToken = readU64BE(data, offset);
|
|
5050
|
-
offset += 8;
|
|
5051
|
-
}
|
|
5052
|
-
const routeCount = Math.min(255, data[offset]);
|
|
5053
|
-
offset += 1;
|
|
5054
|
-
const decoded = decodeRoute(data, offset, routeCount);
|
|
6194
|
+
const ackToken = decoded.ackToken;
|
|
5055
6195
|
const route = decoded.route;
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
if (ackToken != null) {
|
|
5059
|
-
if (data.length < offset + 1)
|
|
5060
|
-
return false;
|
|
5061
|
-
const replyCount = Math.min(255, data[offset]);
|
|
5062
|
-
offset += 1;
|
|
5063
|
-
const decodedReply = decodeRoute(data, offset, replyCount);
|
|
5064
|
-
replyRoute = decodedReply.route;
|
|
5065
|
-
offset = decodedReply.offset;
|
|
5066
|
-
}
|
|
5067
|
-
const payload = data.subarray(offset);
|
|
6196
|
+
const replyRoute = decoded.replyRoute;
|
|
6197
|
+
const payload = data.subarray(decoded.payloadOffset);
|
|
5068
6198
|
const target = route.length > 0 ? route[route.length - 1] : "";
|
|
5069
6199
|
const origin = message.header.signatures?.publicKeys?.[0]?.hashcode?.() ?? fromHash;
|
|
5070
6200
|
this.touchPeerHint(ch, fromHash);
|
|
@@ -5094,7 +6224,7 @@ export class FanoutTree extends DirectStream {
|
|
|
5094
6224
|
if (canAck) {
|
|
5095
6225
|
const nextHop = replyRoute[1];
|
|
5096
6226
|
if (nextHop && ch.children.has(nextHop)) {
|
|
5097
|
-
void this._sendControl(nextHop, encodeUnicastAck(ch.id.key, ackToken, replyRoute)).catch(() => { });
|
|
6227
|
+
void this._sendControl(nextHop, this.codec.encodeUnicastAck(ch.id.key, ackToken, replyRoute)).catch(() => { });
|
|
5098
6228
|
}
|
|
5099
6229
|
}
|
|
5100
6230
|
}
|
|
@@ -5106,6 +6236,11 @@ export class FanoutTree extends DirectStream {
|
|
|
5106
6236
|
const stream = this.peers.get(nextHop);
|
|
5107
6237
|
if (!stream)
|
|
5108
6238
|
return true;
|
|
6239
|
+
// A validated source route is passing through: learn it (and the
|
|
6240
|
+
// reply route) so subsequent unicasts skip route discovery.
|
|
6241
|
+
this.cacheRoute(ch, route);
|
|
6242
|
+
if (replyRoute)
|
|
6243
|
+
this.cacheRoute(ch, replyRoute);
|
|
5109
6244
|
void stream
|
|
5110
6245
|
.waitForWrite(message.bytes(), Number(message.header.priority ?? CONTROL_PRIORITY), this.closeController.signal)
|
|
5111
6246
|
.catch(() => { });
|
|
@@ -5137,7 +6272,7 @@ export class FanoutTree extends DirectStream {
|
|
|
5137
6272
|
replyRoute[0] === ch.id.root &&
|
|
5138
6273
|
replyRoute[replyRoute.length - 1] === origin;
|
|
5139
6274
|
if (canAck) {
|
|
5140
|
-
void this._sendControl(ch.parent, encodeUnicastAck(ch.id.key, ackToken, replyRoute)).catch(() => { });
|
|
6275
|
+
void this._sendControl(ch.parent, this.codec.encodeUnicastAck(ch.id.key, ackToken, replyRoute)).catch(() => { });
|
|
5141
6276
|
}
|
|
5142
6277
|
}
|
|
5143
6278
|
return true;
|
|
@@ -5148,6 +6283,9 @@ export class FanoutTree extends DirectStream {
|
|
|
5148
6283
|
const stream = this.peers.get(nextHop);
|
|
5149
6284
|
if (!stream)
|
|
5150
6285
|
return true;
|
|
6286
|
+
this.cacheRoute(ch, route);
|
|
6287
|
+
if (replyRoute)
|
|
6288
|
+
this.cacheRoute(ch, replyRoute);
|
|
5151
6289
|
void stream
|
|
5152
6290
|
.waitForWrite(message.bytes(), Number(message.header.priority ?? CONTROL_PRIORITY), this.closeController.signal)
|
|
5153
6291
|
.catch(() => { });
|
|
@@ -5171,14 +6309,18 @@ export class FanoutTree extends DirectStream {
|
|
|
5171
6309
|
ch.routeFromRoot = undefined;
|
|
5172
6310
|
ch.routeByPeer.clear();
|
|
5173
6311
|
ch.lastParentDataAt = 0;
|
|
6312
|
+
ch.lastParentUpgradeActivityAt = 0;
|
|
5174
6313
|
ch.receivedAnyParentData = false;
|
|
5175
6314
|
ch.pendingJoin.clear();
|
|
6315
|
+
ch.pendingParentProbe.clear();
|
|
6316
|
+
ch.parentShadow = undefined;
|
|
6317
|
+
void Promise.all(this.clearParentUpgradeGrace(ch, true, true)).catch(() => { });
|
|
6318
|
+
if (this.parentUpgradeShadowInFlightSuffixKey === ch.id.suffixKey) {
|
|
6319
|
+
this.parentUpgradeShadowInFlightSuffixKey = undefined;
|
|
6320
|
+
}
|
|
5176
6321
|
ch.pendingRouteQuery.clear();
|
|
5177
6322
|
this.abortPendingUnicastAcks(ch, new AbortError("fanout channel kicked"));
|
|
5178
|
-
|
|
5179
|
-
clearTimeout(pending.timer);
|
|
5180
|
-
}
|
|
5181
|
-
ch.pendingRouteProxy.clear();
|
|
6323
|
+
this.clearRouteProxies(ch);
|
|
5182
6324
|
void this.kickChildren(ch).catch(() => { });
|
|
5183
6325
|
this.dispatchEvent(new CustomEvent("fanout:kicked", {
|
|
5184
6326
|
detail: { topic: ch.id.topic, root: ch.id.root, from: fromHash },
|
|
@@ -5186,27 +6328,30 @@ export class FanoutTree extends DirectStream {
|
|
|
5186
6328
|
return true;
|
|
5187
6329
|
}
|
|
5188
6330
|
if (kind === MSG_END) {
|
|
5189
|
-
|
|
6331
|
+
const lastSeqExclusive = this.codec.decodeEnd(data);
|
|
6332
|
+
if (lastSeqExclusive == null)
|
|
5190
6333
|
return false;
|
|
5191
|
-
|
|
6334
|
+
if (ch.parent && fromHash === ch.parent) {
|
|
6335
|
+
ch.lastParentDataAt = Date.now();
|
|
6336
|
+
ch.receivedAnyParentData = true;
|
|
6337
|
+
ch.parentRepairUnansweredStreak = 0;
|
|
6338
|
+
}
|
|
5192
6339
|
ch.endSeqExclusive = Math.max(ch.endSeqExclusive, lastSeqExclusive);
|
|
5193
6340
|
this.touchPeerHint(ch, fromHash);
|
|
5194
6341
|
this.noteEnd(ch, fromHash, lastSeqExclusive);
|
|
5195
6342
|
if (ch.children.size > 0) {
|
|
5196
|
-
void this._sendControlMany([...ch.children.keys()], encodeEnd(ch.id.key, lastSeqExclusive));
|
|
6343
|
+
void this._sendControlMany([...ch.children.keys()], this.codec.encodeEnd(ch.id.key, lastSeqExclusive));
|
|
5197
6344
|
}
|
|
5198
6345
|
void this.tickRepair(ch).catch(() => { });
|
|
5199
6346
|
return true;
|
|
5200
6347
|
}
|
|
5201
6348
|
if (kind === MSG_REPAIR_REQ) {
|
|
5202
|
-
|
|
6349
|
+
const seqs = this.codec.decodeRepairSeqs(data);
|
|
6350
|
+
if (!seqs)
|
|
5203
6351
|
return false;
|
|
5204
6352
|
if (!ch.children.has(fromHash))
|
|
5205
6353
|
return true;
|
|
5206
|
-
const
|
|
5207
|
-
const max = Math.min(count, Math.floor((data.length - 38) / 4));
|
|
5208
|
-
for (let i = 0; i < max; i++) {
|
|
5209
|
-
const seq = readU32BE(data, 38 + i * 4);
|
|
6354
|
+
for (const seq of seqs) {
|
|
5210
6355
|
const cached = this.getCached(ch, seq);
|
|
5211
6356
|
if (!cached) {
|
|
5212
6357
|
ch.metrics.cacheMissesServed += 1;
|
|
@@ -5218,13 +6363,11 @@ export class FanoutTree extends DirectStream {
|
|
|
5218
6363
|
return true;
|
|
5219
6364
|
}
|
|
5220
6365
|
if (kind === MSG_FETCH_REQ) {
|
|
5221
|
-
|
|
6366
|
+
const seqs = this.codec.decodeRepairSeqs(data);
|
|
6367
|
+
if (!seqs)
|
|
5222
6368
|
return false;
|
|
5223
6369
|
this.touchPeerHint(ch, fromHash);
|
|
5224
|
-
const
|
|
5225
|
-
const max = Math.min(count, Math.floor((data.length - 38) / 4));
|
|
5226
|
-
for (let i = 0; i < max; i++) {
|
|
5227
|
-
const seq = readU32BE(data, 38 + i * 4);
|
|
6370
|
+
for (const seq of seqs) {
|
|
5228
6371
|
const cached = this.getCached(ch, seq);
|
|
5229
6372
|
if (!cached) {
|
|
5230
6373
|
ch.metrics.cacheMissesServed += 1;
|
|
@@ -5248,10 +6391,100 @@ export class FanoutTree extends DirectStream {
|
|
|
5248
6391
|
return false;
|
|
5249
6392
|
const seq = readU32BE(id, 4);
|
|
5250
6393
|
if (ch.parent && fromHash === ch.parent) {
|
|
5251
|
-
|
|
6394
|
+
const receivedAt = Date.now();
|
|
6395
|
+
ch.lastParentDataAt = receivedAt;
|
|
6396
|
+
ch.lastParentUpgradeActivityAt = receivedAt;
|
|
5252
6397
|
ch.receivedAnyParentData = true;
|
|
6398
|
+
ch.parentRepairUnansweredStreak = 0;
|
|
6399
|
+
}
|
|
6400
|
+
const hadCachedBeforeData = this.getCached(ch, seq) != null;
|
|
6401
|
+
if (ch.parent && fromHash === ch.parent && !hadCachedBeforeData) {
|
|
6402
|
+
this.noteParentDataLatency(ch, message.header.timestamp);
|
|
5253
6403
|
}
|
|
5254
6404
|
ch.maxSeqSeen = Math.max(ch.maxSeqSeen, seq);
|
|
6405
|
+
if (ch.parentUpgradeRetryAfterSeq >= 0 &&
|
|
6406
|
+
seq > ch.parentUpgradeRetryAfterSeq) {
|
|
6407
|
+
ch.parentUpgradeRetryAfterSeq = -1;
|
|
6408
|
+
}
|
|
6409
|
+
const parentShadow = ch.parentShadow;
|
|
6410
|
+
if (parentShadow != null) {
|
|
6411
|
+
const now = Date.now();
|
|
6412
|
+
const pruneShadowSeqMap = (map) => {
|
|
6413
|
+
while (map.size > 512) {
|
|
6414
|
+
const oldest = map.keys().next().value;
|
|
6415
|
+
if (oldest == null)
|
|
6416
|
+
break;
|
|
6417
|
+
map.delete(oldest);
|
|
6418
|
+
}
|
|
6419
|
+
};
|
|
6420
|
+
const pruneShadowSeqSet = (set) => {
|
|
6421
|
+
while (set.size > 512) {
|
|
6422
|
+
const oldest = set.keys().next().value;
|
|
6423
|
+
if (oldest == null)
|
|
6424
|
+
break;
|
|
6425
|
+
set.delete(oldest);
|
|
6426
|
+
}
|
|
6427
|
+
};
|
|
6428
|
+
const rememberShadowArrival = (map) => {
|
|
6429
|
+
if (map.has(seq))
|
|
6430
|
+
return map.get(seq);
|
|
6431
|
+
map.set(seq, now);
|
|
6432
|
+
pruneShadowSeqMap(map);
|
|
6433
|
+
return now;
|
|
6434
|
+
};
|
|
6435
|
+
const recordCandidateLead = (candidateAt, parentAt) => {
|
|
6436
|
+
if (candidateAt == null ||
|
|
6437
|
+
parentAt == null ||
|
|
6438
|
+
parentShadow.liveComparedSeqs.has(seq)) {
|
|
6439
|
+
return;
|
|
6440
|
+
}
|
|
6441
|
+
parentShadow.liveComparedSeqs.add(seq);
|
|
6442
|
+
pruneShadowSeqSet(parentShadow.liveComparedSeqs);
|
|
6443
|
+
if (candidateAt < parentAt) {
|
|
6444
|
+
parentShadow.liveCandidateLeadSamples += 1;
|
|
6445
|
+
parentShadow.liveCandidateLeadMsTotal += parentAt - candidateAt;
|
|
6446
|
+
}
|
|
6447
|
+
};
|
|
6448
|
+
if (parentShadow.hash === fromHash) {
|
|
6449
|
+
const candidateAt = rememberShadowArrival(parentShadow.liveCandidateFirstAtBySeq);
|
|
6450
|
+
recordCandidateLead(candidateAt, parentShadow.liveParentFirstAtBySeq.get(seq));
|
|
6451
|
+
if (seq > parentShadow.liveMaxSeqSeen) {
|
|
6452
|
+
parentShadow.liveMaxSeqSeen = seq;
|
|
6453
|
+
parentShadow.liveDataMessages += 1;
|
|
6454
|
+
}
|
|
6455
|
+
if (!hadCachedBeforeData) {
|
|
6456
|
+
parentShadow.liveFirstDataMessages += 1;
|
|
6457
|
+
}
|
|
6458
|
+
parentShadow.liveLastDataAt = now;
|
|
6459
|
+
}
|
|
6460
|
+
else if (ch.parent != null && fromHash === ch.parent) {
|
|
6461
|
+
const parentAt = rememberShadowArrival(parentShadow.liveParentFirstAtBySeq);
|
|
6462
|
+
recordCandidateLead(parentShadow.liveCandidateFirstAtBySeq.get(seq), parentAt);
|
|
6463
|
+
if (!hadCachedBeforeData) {
|
|
6464
|
+
parentShadow.liveParentFirstDataMessages += 1;
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
const parentUpgradeGrace = ch.parentUpgradeGrace;
|
|
6469
|
+
if (parentUpgradeGrace != null && !hadCachedBeforeData) {
|
|
6470
|
+
if (fromHash === parentUpgradeGrace.candidateParent) {
|
|
6471
|
+
parentUpgradeGrace.candidateFirstMessages += 1;
|
|
6472
|
+
}
|
|
6473
|
+
else if (fromHash === parentUpgradeGrace.previousParent) {
|
|
6474
|
+
parentUpgradeGrace.previousFirstMessages += 1;
|
|
6475
|
+
}
|
|
6476
|
+
if (parentUpgradeGrace.previousFirstMessages >
|
|
6477
|
+
parentUpgradeGrace.maxPreviousFirstMessages) {
|
|
6478
|
+
this.rollbackParentUpgradeGrace(ch);
|
|
6479
|
+
}
|
|
6480
|
+
else if (parentUpgradeGrace.candidateFirstMessages >=
|
|
6481
|
+
parentUpgradeGrace.minCandidateFirstMessages &&
|
|
6482
|
+
parentUpgradeGrace.candidateFirstMessages >=
|
|
6483
|
+
parentUpgradeGrace.previousFirstMessages +
|
|
6484
|
+
parentUpgradeGrace.minCandidateAdvantageMessages) {
|
|
6485
|
+
this.commitParentUpgradeGrace(ch);
|
|
6486
|
+
}
|
|
6487
|
+
}
|
|
5255
6488
|
this.noteReceivedSeq(ch, fromHash, seq);
|
|
5256
6489
|
const payload = data.subarray(1);
|
|
5257
6490
|
ch.metrics.dataReceives += 1;
|