@steve02081504/fount-p2p 0.0.36 → 0.0.37
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/AGENTS.md +1 -1
- package/discovery/adverts.mjs +16 -8
- package/discovery/nostr/census.mjs +281 -0
- package/discovery/nostr/census_math.mjs +54 -0
- package/discovery/nostr/constants.mjs +43 -0
- package/discovery/nostr/index.mjs +612 -0
- package/discovery/nostr/relays.mjs +1071 -0
- package/discovery/nostr/selection.mjs +224 -0
- package/discovery/nostr/session.mjs +584 -0
- package/docs/nostr_relay_discovery.md +92 -0
- package/files/manifest/fetch.mjs +4 -6
- package/governance/branch.mjs +1 -1
- package/governance/join_pow.mjs +2 -2
- package/index.mjs +11 -4
- package/link/handshake.mjs +135 -23
- package/link/providers/link_id_pipe.mjs +1 -1
- package/link/providers/{nostr.mjs → nostr/index.mjs} +19 -14
- package/mailbox/deliver_or_store.mjs +3 -4
- package/mailbox/store.mjs +1 -1
- package/node/feature_config.mjs +30 -0
- package/node/instance.mjs +21 -0
- package/node/reputation_store.mjs +1 -2
- package/node/storage.mjs +23 -0
- package/package.json +10 -4
- package/pages/app.mjs +172 -0
- package/pages/index.html +36 -0
- package/reputation/engine.mjs +1 -2
- package/transport/runtime_bootstrap.mjs +39 -3
- package/discovery/nostr.mjs +0 -1073
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { nodeDebug, shortHash } from '../../node/log.mjs'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BACKOFF_BASE_MS,
|
|
5
|
+
BACKOFF_CAP_MS,
|
|
6
|
+
DEFAULT_RTT_MS,
|
|
7
|
+
LAST_GOOD_RELAYS_MAX,
|
|
8
|
+
MAX_ROUTING_ATTEMPTS,
|
|
9
|
+
MAX_ROUTING_FANOUT,
|
|
10
|
+
ROUND0_TARGET_COUNT,
|
|
11
|
+
} from './constants.mjs'
|
|
12
|
+
import {
|
|
13
|
+
computeRelayHealth,
|
|
14
|
+
getPeerRoute,
|
|
15
|
+
getPinnedRelays,
|
|
16
|
+
getPoolByUrl,
|
|
17
|
+
getWorkingRelays,
|
|
18
|
+
recordPublishResult,
|
|
19
|
+
resolveRelayConnectTarget,
|
|
20
|
+
setPeerRoute,
|
|
21
|
+
} from './relays.mjs'
|
|
22
|
+
import { publishViaSharedRelay } from './session.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 路由退避延迟(attempt 从 0 开始)。
|
|
26
|
+
* @param {number} attempt 尝试轮次
|
|
27
|
+
* @returns {number} 退避毫秒
|
|
28
|
+
*/
|
|
29
|
+
export function backoffDelay(attempt) {
|
|
30
|
+
if (attempt <= 0) return 0
|
|
31
|
+
return Math.min(BACKOFF_BASE_MS * (2 ** (attempt - 1)), BACKOFF_CAP_MS)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 计算对端声称的中继的「综合分」:本机健康分 + 对端上报 RTT。
|
|
36
|
+
* @param {string} url 对端 listen relay
|
|
37
|
+
* @param {import('./relays.mjs').PeerRoute | null} route 对端路由
|
|
38
|
+
* @returns {number} 综合分(越低越优)
|
|
39
|
+
*/
|
|
40
|
+
function compositeScore(url, route) {
|
|
41
|
+
const ownPool = getPoolByUrl().get(url)
|
|
42
|
+
return (ownPool ? computeRelayHealth(ownPool) : DEFAULT_RTT_MS) + (route?.peerPool?.find(item => item.url === url)?.rttMs ?? DEFAULT_RTT_MS)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 对端声称的监听 relay 集,按综合分升序。
|
|
47
|
+
* @param {string} nodeHash 目标节点
|
|
48
|
+
* @returns {Array<{ url: string, score: number }>} 排序后的对端中继
|
|
49
|
+
*/
|
|
50
|
+
export function getReachPeerRelays(nodeHash) {
|
|
51
|
+
const route = getPeerRoute(nodeHash)
|
|
52
|
+
if (!route?.listenRelays?.length) return []
|
|
53
|
+
return route.listenRelays
|
|
54
|
+
.map(url => ({ url, score: compositeScore(url, route) }))
|
|
55
|
+
.sort((a, b) => a.score - b.score)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 按健康分加权随机采样(权重 = 1/score,score 越低越可能被选)。
|
|
60
|
+
* 与 getWorkingRelays 共用 computeRelayHealth 评分语义。
|
|
61
|
+
* @param {import('./relays.mjs').RelayPoolEntry[]} entries 候选
|
|
62
|
+
* @param {number} k 采样数量
|
|
63
|
+
* @returns {string[]} 采样 URL
|
|
64
|
+
*/
|
|
65
|
+
export function weightedRandomSample(entries, k) {
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('./relays.mjs').RelayPoolEntry} entry 候选条目
|
|
68
|
+
* @returns {number} 权重
|
|
69
|
+
*/
|
|
70
|
+
const weightOf = entry => Math.max(Number.EPSILON, 1 / computeRelayHealth(entry))
|
|
71
|
+
let total = entries.map(weightOf).reduce((a, b) => a + b, 0)
|
|
72
|
+
const picked = []
|
|
73
|
+
const remaining = [...entries]
|
|
74
|
+
while (picked.length < k && remaining.length) {
|
|
75
|
+
let remainingWeight = Math.random() * total
|
|
76
|
+
let position = 0
|
|
77
|
+
for (let index = 0; index < remaining.length; index++) {
|
|
78
|
+
const weight = weightOf(remaining[index])
|
|
79
|
+
if (remainingWeight < weight) { position = index; break }
|
|
80
|
+
remainingWeight -= weight
|
|
81
|
+
}
|
|
82
|
+
const chosen = remaining.splice(position, 1)[0]
|
|
83
|
+
picked.push(chosen.url)
|
|
84
|
+
total -= weightOf(chosen)
|
|
85
|
+
}
|
|
86
|
+
return picked
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 从历史成功集 + 对端综合分补足,扩展至不超过 limit。
|
|
91
|
+
* @param {string} nodeHash 目标节点
|
|
92
|
+
* @param {string[]} base 基础 URL
|
|
93
|
+
* @param {number} limit 上限
|
|
94
|
+
* @returns {string[]} 去重扩展结果
|
|
95
|
+
*/
|
|
96
|
+
export function expandFromHistory(nodeHash, base, limit) {
|
|
97
|
+
const seen = new Set()
|
|
98
|
+
const out = []
|
|
99
|
+
for (const url of base) {
|
|
100
|
+
if (seen.has(url)) continue
|
|
101
|
+
seen.add(url); out.push(url)
|
|
102
|
+
if (out.length >= limit) break
|
|
103
|
+
}
|
|
104
|
+
for (const { url } of getReachPeerRelays(nodeHash)) {
|
|
105
|
+
if (seen.has(url)) continue
|
|
106
|
+
seen.add(url); out.push(url)
|
|
107
|
+
if (out.length >= limit) break
|
|
108
|
+
}
|
|
109
|
+
for (const entry of getWorkingRelays()) {
|
|
110
|
+
if (seen.has(entry.url)) continue
|
|
111
|
+
seen.add(entry.url); out.push(entry.url)
|
|
112
|
+
if (out.length >= limit) break
|
|
113
|
+
}
|
|
114
|
+
return out.slice(0, limit)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 计算到指定节点的当前轮路由目标集。
|
|
119
|
+
* Round 0:对端声称前4(无则本机工作集前4,再空则 pinned)。
|
|
120
|
+
* Round ≥1:历史成功 + 对端综合分补足 + 加权采样,附退避;总扇出封顶。
|
|
121
|
+
* @param {string} nodeHash 目标节点
|
|
122
|
+
* @param {number} attempt 当前尝试轮次(0 起)
|
|
123
|
+
* @returns {{ urls: string[], backoffDelay: number }} 目标集与退避
|
|
124
|
+
*/
|
|
125
|
+
export function handshakeTargets(nodeHash, attempt) {
|
|
126
|
+
const seen = new Set()
|
|
127
|
+
const targets = []
|
|
128
|
+
/** @param {string} url relay URL @returns {void} */
|
|
129
|
+
const push = url => { if (!seen.has(url)) { seen.add(url); targets.push(url) } }
|
|
130
|
+
/** @returns {void} Round 0 目标 */
|
|
131
|
+
const round0 = () => {
|
|
132
|
+
const reach = getReachPeerRelays(nodeHash)
|
|
133
|
+
if (reach.length) {
|
|
134
|
+
for (const { url } of reach.slice(0, ROUND0_TARGET_COUNT)) push(url)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const working = getWorkingRelays()
|
|
138
|
+
if (working.length) {
|
|
139
|
+
for (const entry of working.slice(0, ROUND0_TARGET_COUNT)) push(entry.url)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
for (const url of getPinnedRelays().slice(0, ROUND0_TARGET_COUNT)) push(url)
|
|
143
|
+
}
|
|
144
|
+
if (attempt <= 0) {
|
|
145
|
+
round0()
|
|
146
|
+
return { urls: targets.slice(0, MAX_ROUTING_FANOUT), backoffDelay: 0 }
|
|
147
|
+
}
|
|
148
|
+
const route = getPeerRoute(nodeHash)
|
|
149
|
+
if (route?.lastGoodNostrRelays?.length)
|
|
150
|
+
for (const url of expandFromHistory(nodeHash, route.lastGoodNostrRelays, LAST_GOOD_RELAYS_MAX)) push(url)
|
|
151
|
+
else
|
|
152
|
+
for (const url of weightedRandomSample(getWorkingRelays(), LAST_GOOD_RELAYS_MAX)) push(url)
|
|
153
|
+
|
|
154
|
+
const reach = getReachPeerRelays(nodeHash)
|
|
155
|
+
if (reach.length) for (const { url } of reach.slice(0, ROUND0_TARGET_COUNT)) push(url)
|
|
156
|
+
else for (const entry of getWorkingRelays().slice(0, ROUND0_TARGET_COUNT)) push(entry.url)
|
|
157
|
+
|
|
158
|
+
return { urls: targets.slice(0, MAX_ROUTING_FANOUT), backoffDelay: backoffDelay(attempt) }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 记录成功 relay 到 peerRoutes[hash].lastGoodNostrRelays(去重,保留最近 16)。
|
|
163
|
+
* @param {string} nodeHash 目标节点
|
|
164
|
+
* @param {string[]} okRelays 成功 relay
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
function recordPeerRouteLastGood(nodeHash, okRelays) {
|
|
168
|
+
setPeerRoute(nodeHash, { lastGoodNostrRelays: [...new Set([...getPeerRoute(nodeHash)?.lastGoodNostrRelays || [], ...okRelays])].slice(0, LAST_GOOD_RELAYS_MAX) })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 路由发布事件到目标节点:多 relay 并行,任一 OK 即记录 lastGood 并返回;全败则记录并退避重试。
|
|
173
|
+
* @param {string} toNodeHash 目标节点
|
|
174
|
+
* @param {object} event Nostr 事件
|
|
175
|
+
* @param {AbortSignal} [signal] 取消信号
|
|
176
|
+
* @returns {Promise<boolean>} 是否成功发布
|
|
177
|
+
*/
|
|
178
|
+
export async function routePublishEvent(toNodeHash, event, signal) {
|
|
179
|
+
for (let attempt = 0; attempt < MAX_ROUTING_ATTEMPTS; attempt++) {
|
|
180
|
+
if (signal?.aborted) return false
|
|
181
|
+
const { urls, backoffDelay: delayMs } = handshakeTargets(toNodeHash, attempt)
|
|
182
|
+
if (!urls.length) return false
|
|
183
|
+
const valid = (await Promise.all(urls.map(async url => ({ url, target: await resolveRelayConnectTarget(url) }))))
|
|
184
|
+
.filter(entry => entry.target)
|
|
185
|
+
if (!valid.length) return false
|
|
186
|
+
const targets = valid.map(entry => entry.url)
|
|
187
|
+
const results = await Promise.allSettled(valid.map(({ url, target }) => publishViaSharedRelay(url, event, signal, target)))
|
|
188
|
+
const okRelays = targets.filter((_, index) => {
|
|
189
|
+
const result = results[index]
|
|
190
|
+
return result.status === 'fulfilled' && result.value === true
|
|
191
|
+
})
|
|
192
|
+
if (okRelays.length) {
|
|
193
|
+
recordPeerRouteLastGood(toNodeHash, okRelays)
|
|
194
|
+
for (const url of okRelays) recordPublishResult(url, true)
|
|
195
|
+
nodeDebug('p2p:nostr route ok', {
|
|
196
|
+
peer: shortHash(toNodeHash),
|
|
197
|
+
attempt,
|
|
198
|
+
relays: okRelays,
|
|
199
|
+
})
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
for (const url of targets) recordPublishResult(url, false)
|
|
203
|
+
if (attempt >= MAX_ROUTING_ATTEMPTS - 1) return false
|
|
204
|
+
if (signal?.aborted) return false
|
|
205
|
+
await new Promise(resolve => {
|
|
206
|
+
/**
|
|
207
|
+
* 处理 abort 事件:清理定时器与监听器后立即结算。
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
const onAbort = () => {
|
|
211
|
+
clearTimeout(timer)
|
|
212
|
+
signal?.removeEventListener('abort', onAbort)
|
|
213
|
+
resolve()
|
|
214
|
+
}
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
signal?.removeEventListener('abort', onAbort)
|
|
217
|
+
resolve()
|
|
218
|
+
}, delayMs)
|
|
219
|
+
timer.unref?.()
|
|
220
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
return false
|
|
224
|
+
}
|