@steve02081504/fount-p2p 0.0.11 → 0.0.12
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/README.md +1 -1
- package/core/bytes_codec.mjs +65 -10
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/discovery/bt/index.mjs +5 -5
- package/discovery/bt/runtime.mjs +3 -3
- package/discovery/mdns.mjs +7 -1
- package/discovery/nostr.mjs +53 -99
- package/federation/chunk_fetch_pending.mjs +4 -5
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/topo_order_memo.mjs +11 -4
- package/files/chunk_fetch.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -124
- package/link/handshake.mjs +5 -5
- package/link/pipe.mjs +49 -75
- package/link/providers/ble_gatt.mjs +14 -26
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +21 -9
- package/mailbox/deliver_or_store.mjs +1 -1
- package/node/identity.mjs +4 -4
- package/node/network.mjs +9 -9
- package/overlay/index.mjs +13 -13
- package/package.json +1 -1
- package/rooms/scoped_link.mjs +19 -35
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +20 -0
- package/transport/group_link_set.mjs +22 -41
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +113 -105
- package/transport/offer_answer.mjs +6 -2
- package/transport/peer_pool.mjs +53 -64
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +8 -2
- package/transport/signal_crypto.mjs +25 -3
- package/transport/user_room.mjs +22 -15
- package/trust_graph/send.mjs +1 -1
- package/utils/emit_safe.mjs +11 -0
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +6 -8
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +2 -3
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
|
|
|
45
45
|
|
|
46
46
|
| Module | Role |
|
|
47
47
|
|---|---|
|
|
48
|
-
| `link_registry.mjs` |
|
|
48
|
+
| `link_registry.mjs` | fount-network facade: dial fallback, scope/overlay |
|
|
49
49
|
| `runtime_bootstrap.mjs` | Progressive `ensureRuntime` (register + background listen / discovery / BT) |
|
|
50
50
|
| `offer_answer.mjs` | Discovery-signal glare for offer/answer providers |
|
|
51
51
|
| `signal_crypto.mjs` | Rendezvous topics + AES-GCM signal packets |
|
package/core/bytes_codec.mjs
CHANGED
|
@@ -2,23 +2,78 @@
|
|
|
2
2
|
* Base64 / hex / bytes 互转(无 Node 依赖,浏览器与 Node 共用)。
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const HEX = '0123456789abcdef'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {number} code UTF-16 码元(期望为 0-9 / a-f / A-F)
|
|
9
|
+
* @returns {number} 0–15;非法为 -1
|
|
10
|
+
*/
|
|
11
|
+
function hexNibble(code) {
|
|
12
|
+
if (code >= 48 && code <= 57) return code - 48
|
|
13
|
+
if (code >= 97 && code <= 102) return code - 87
|
|
14
|
+
if (code >= 65 && code <= 70) return code - 55
|
|
15
|
+
return -1
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
|
-
* @param {Uint8Array}
|
|
7
|
-
* @returns {string}
|
|
19
|
+
* @param {Uint8Array} bytes 原始字节
|
|
20
|
+
* @returns {string} 小写 hex
|
|
21
|
+
*/
|
|
22
|
+
export function bytesToHex(bytes) {
|
|
23
|
+
let out = ''
|
|
24
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
25
|
+
const byte = bytes[index]
|
|
26
|
+
out += HEX[byte >> 4] + HEX[byte & 15]
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} hex hex 文本(可含空白;大小写不敏感)
|
|
33
|
+
* @returns {Uint8Array} 解码后的字节
|
|
8
34
|
*/
|
|
9
|
-
export function
|
|
35
|
+
export function hexToBytes(hex) {
|
|
36
|
+
const text = hex.trim().toLowerCase()
|
|
37
|
+
if (text.length % 2) throw new Error('p2p: hex length must be even')
|
|
38
|
+
const out = new Uint8Array(text.length / 2)
|
|
39
|
+
for (let index = 0; index < out.length; index++) {
|
|
40
|
+
const high = hexNibble(text.charCodeAt(index * 2))
|
|
41
|
+
const low = hexNibble(text.charCodeAt(index * 2 + 1))
|
|
42
|
+
if (high < 0 || low < 0) throw new Error('p2p: invalid hex')
|
|
43
|
+
out[index] = (high << 4) | low
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ArrayBuffer / TypedArray / Uint8Array → Uint8Array;`allowString` 时接受文本。
|
|
50
|
+
* @param {unknown} value 待转换值
|
|
51
|
+
* @param {{ allowString?: boolean }} [options] `allowString` 时把非字节输入当文本编码
|
|
52
|
+
* @returns {Uint8Array} 字节视图(可能与输入共享底层 buffer)
|
|
53
|
+
*/
|
|
54
|
+
export function toBytes(value, options = {}) {
|
|
55
|
+
if (value instanceof Uint8Array) return value
|
|
56
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value)
|
|
57
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
58
|
+
if (options.allowString)
|
|
59
|
+
return new TextEncoder().encode(typeof value === 'string' ? value : String(value ?? ''))
|
|
60
|
+
throw new Error('p2p: bytes must be Uint8Array-compatible')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Uint8Array} bytes 原始字节
|
|
65
|
+
* @returns {string} 标准 Base64
|
|
66
|
+
*/
|
|
67
|
+
export function bytesToBase64(bytes) {
|
|
10
68
|
let binary = ''
|
|
11
|
-
for (let index = 0; index <
|
|
69
|
+
for (let index = 0; index < bytes.length; index++) binary += String.fromCharCode(bytes[index])
|
|
12
70
|
return btoa(binary)
|
|
13
71
|
}
|
|
14
72
|
|
|
15
73
|
/**
|
|
16
|
-
* @param {string}
|
|
74
|
+
* @param {string} base64 标准 Base64
|
|
17
75
|
* @returns {Uint8Array} 解码后的字节
|
|
18
76
|
*/
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
const out = new Uint8Array(bin.length)
|
|
22
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
23
|
-
return out
|
|
77
|
+
export function base64ToBytes(base64) {
|
|
78
|
+
return Uint8Array.from(atob(base64), char => char.charCodeAt(0))
|
|
24
79
|
}
|
package/core/tcp_port.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @returns {number | null} 有效端口或 null(未提供 / 非法)
|
|
5
5
|
*/
|
|
6
6
|
export function normalizeTcpPort(port) {
|
|
7
|
-
if (port
|
|
7
|
+
if (!port) return null
|
|
8
8
|
const value = Number(port)
|
|
9
9
|
if (!Number.isInteger(value) || value < 1 || value > 65535) return null
|
|
10
10
|
return value
|
|
@@ -49,9 +49,9 @@ export function isSignedCheckpoint(checkpoint) {
|
|
|
49
49
|
* @returns {Promise<{ valid: boolean, reason?: string }>} 校验结果;`valid` 为 false 时 `reason` 说明原因
|
|
50
50
|
*/
|
|
51
51
|
export async function verifyRemoteCheckpoint(checkpoint) {
|
|
52
|
-
if (!checkpoint
|
|
52
|
+
if (!checkpoint)
|
|
53
53
|
return { valid: false, reason: 'checkpoint missing or not an object' }
|
|
54
|
-
if (!
|
|
54
|
+
if (!checkpoint.eventIdsInEpoch?.length)
|
|
55
55
|
return { valid: false, reason: 'eventIdsInEpoch missing or empty' }
|
|
56
56
|
const root = merkleRoot(checkpoint.eventIdsInEpoch)
|
|
57
57
|
if (checkpoint.epoch_root_hash !== root)
|
package/dag/canonicalize_row.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { assertHex64, HEX_ID_64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
|
8
8
|
* @param {string} key 字段名
|
|
9
9
|
*/
|
|
10
10
|
function canonicalizeHexField(obj, key) {
|
|
11
|
-
if (obj[key]
|
|
11
|
+
if (!obj[key]) return
|
|
12
12
|
const normalized = normalizeHex64(obj[key])
|
|
13
13
|
if (!HEX_ID_64.test(normalized))
|
|
14
14
|
throw new Error(`${key} must be 64 hex characters`)
|
|
@@ -27,7 +27,7 @@ export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Se
|
|
|
27
27
|
for (const key of hexKeys)
|
|
28
28
|
canonicalizeHexField(out, key)
|
|
29
29
|
for (const key of entityHashKeys) {
|
|
30
|
-
if (out[key]
|
|
30
|
+
if (!out[key]) continue
|
|
31
31
|
const entityHash = String(out[key]).toLowerCase()
|
|
32
32
|
if (!isEntityHash128(entityHash))
|
|
33
33
|
throw new Error(`${key} must be 128 hex characters`)
|
|
@@ -54,7 +54,7 @@ export function canonicalizeSignedRow(event, options = {}) {
|
|
|
54
54
|
const out = options.prepare ? options.prepare({ ...event }) : { ...event }
|
|
55
55
|
out.id = assertHex64(out.id, 'id')
|
|
56
56
|
out.sender = assertHex64(out.sender, 'sender')
|
|
57
|
-
if (
|
|
57
|
+
if (out.prev_event_ids)
|
|
58
58
|
out.prev_event_ids = out.prev_event_ids.map((id, index) =>
|
|
59
59
|
assertHex64(id, `prev_event_ids[${index}]`),
|
|
60
60
|
)
|
package/dag/index.mjs
CHANGED
|
@@ -51,8 +51,7 @@ export function computeLocalTipsHash(tipIds) {
|
|
|
51
51
|
* @returns {string[]} 去重并字典序排序后的父事件 id 数组
|
|
52
52
|
*/
|
|
53
53
|
export function sortedPrevEventIds(raw) {
|
|
54
|
-
|
|
55
|
-
return [...new Set(raw.filter(id => EVENT_ID_HEX.test(String(id))))].sort()
|
|
54
|
+
return [...new Set((raw || []).filter(id => EVENT_ID_HEX.test(id)))].sort()
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
/**
|
|
@@ -226,7 +225,7 @@ export function topologicalCanonicalOrder(metas) {
|
|
|
226
225
|
|
|
227
226
|
while (ready.size) {
|
|
228
227
|
const next = ready.pop()
|
|
229
|
-
if (next
|
|
228
|
+
if (!next) break
|
|
230
229
|
ordered.push(next)
|
|
231
230
|
for (const childId of children.get(next) || []) {
|
|
232
231
|
const remaining = (parentCount.get(childId) || 0) - 1
|
package/discovery/bt/index.mjs
CHANGED
|
@@ -46,10 +46,10 @@ function serializeAdvertBlob(adverts) {
|
|
|
46
46
|
function parseAdvertBlob(raw) {
|
|
47
47
|
try {
|
|
48
48
|
const parsed = JSON.parse(Buffer.from(raw).toString('utf8'))
|
|
49
|
-
if (!
|
|
49
|
+
if (!parsed?.entries?.length) return []
|
|
50
50
|
return parsed.entries.map(entry => ({
|
|
51
|
-
topic:
|
|
52
|
-
bytes: Uint8Array.from(Buffer.from(
|
|
51
|
+
topic: entry.topic || '',
|
|
52
|
+
bytes: Uint8Array.from(Buffer.from(entry.data || '', 'base64')),
|
|
53
53
|
})).filter(entry => entry.topic && entry.bytes.byteLength)
|
|
54
54
|
}
|
|
55
55
|
catch {
|
|
@@ -148,10 +148,10 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
148
148
|
const parsed = JSON.parse(Buffer.from(data).toString('utf8'))
|
|
149
149
|
const topic = String(parsed?.topic || '')
|
|
150
150
|
const bytes = Uint8Array.from(Buffer.from(String(parsed?.data || ''), 'base64'))
|
|
151
|
-
if (topic && bytes.byteLength)
|
|
151
|
+
if (topic && bytes.byteLength)
|
|
152
152
|
for (const listener of signalListeners.get(topic) || [])
|
|
153
153
|
listener(bytes, { provider: 'bt' })
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
callback(bleno.Characteristic.RESULT_SUCCESS)
|
|
156
156
|
}
|
|
157
157
|
catch {
|
package/discovery/bt/runtime.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
1
2
|
import { existsSync, readdirSync } from 'node:fs'
|
|
2
3
|
import { dirname, join } from 'node:path'
|
|
3
4
|
import process from 'node:process'
|
|
4
5
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { spawn } from 'node:child_process'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 解析 Bluetooth 角色(scan / dual)。
|
|
@@ -92,14 +92,14 @@ export async function canUseBluetoothRuntime(timeoutMs = 3000) {
|
|
|
92
92
|
cachedRuntimeOk = false
|
|
93
93
|
return false
|
|
94
94
|
}
|
|
95
|
-
if (!probeInflight)
|
|
95
|
+
if (!probeInflight)
|
|
96
96
|
probeInflight = probeBluetoothInSubprocess(timeoutMs)
|
|
97
97
|
.then(ok => {
|
|
98
98
|
cachedRuntimeOk = ok
|
|
99
99
|
return ok
|
|
100
100
|
})
|
|
101
101
|
.finally(() => { probeInflight = null })
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
return probeInflight
|
|
104
104
|
}
|
|
105
105
|
|
package/discovery/mdns.mjs
CHANGED
|
@@ -129,7 +129,13 @@ export function createMdnsDiscoveryProvider(options = {}) {
|
|
|
129
129
|
bucket.get(topic).add(listener)
|
|
130
130
|
const release = acquire()
|
|
131
131
|
return () => {
|
|
132
|
-
bucket.get(topic)
|
|
132
|
+
const set = bucket.get(topic)
|
|
133
|
+
if (!set) {
|
|
134
|
+
release()
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
set.delete(listener)
|
|
138
|
+
if (!set.size) bucket.delete(topic)
|
|
133
139
|
release()
|
|
134
140
|
}
|
|
135
141
|
}
|
package/discovery/nostr.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto'
|
|
|
3
3
|
import { schnorr } from '@noble/curves/secp256k1.js'
|
|
4
4
|
import WebSocket from 'ws'
|
|
5
5
|
|
|
6
|
+
import { base64ToBytes, hexToBytes, bytesToBase64, bytesToHex } from '../core/bytes_codec.mjs'
|
|
6
7
|
import { sha256Hex } from '../crypto/crypto.mjs'
|
|
7
8
|
|
|
8
9
|
/** 默认 Nostr 中继 URL 列表。 */
|
|
@@ -57,15 +58,9 @@ function dropWebSocket(ws) {
|
|
|
57
58
|
*/
|
|
58
59
|
function dedupeRelayUrls(urls) {
|
|
59
60
|
const seen = new Set()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const trimmed = String(url || '').trim()
|
|
64
|
-
if (!trimmed || seen.has(trimmed)) continue
|
|
65
|
-
seen.add(trimmed)
|
|
66
|
-
out.push(trimmed)
|
|
67
|
-
}
|
|
68
|
-
return out
|
|
61
|
+
return (urls || [])
|
|
62
|
+
.map(url => url.trim())
|
|
63
|
+
.filter(trimmed => trimmed && !seen.has(trimmed) && (seen.add(trimmed), true))
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
/**
|
|
@@ -74,50 +69,10 @@ function dedupeRelayUrls(urls) {
|
|
|
74
69
|
* @returns {string[]} 合并后的中继 URL 列表
|
|
75
70
|
*/
|
|
76
71
|
export function mergeSignalingRelayUrls(userRelayUrls) {
|
|
77
|
-
const merged = dedupeRelayUrls([...DEFAULT_RELAY_URLS, ...
|
|
72
|
+
const merged = dedupeRelayUrls([...DEFAULT_RELAY_URLS, ...userRelayUrls || []])
|
|
78
73
|
return merged.length ? merged : [...DEFAULT_RELAY_URLS]
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
/**
|
|
82
|
-
* 字节数组转十六进制字符串。
|
|
83
|
-
* @param {Uint8Array} bytes 输入字节
|
|
84
|
-
* @returns {string} 小写 hex 字符串
|
|
85
|
-
*/
|
|
86
|
-
function bytesToHex(bytes) {
|
|
87
|
-
return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 十六进制字符串转字节数组。
|
|
92
|
-
* @param {string} hex 输入 hex 字符串
|
|
93
|
-
* @returns {Uint8Array} 解码后的字节
|
|
94
|
-
*/
|
|
95
|
-
function hexToBytes(hex) {
|
|
96
|
-
const normalized = String(hex || '').trim().toLowerCase()
|
|
97
|
-
const out = new Uint8Array(Math.floor(normalized.length / 2))
|
|
98
|
-
for (let index = 0; index < out.length; index++)
|
|
99
|
-
out[index] = parseInt(normalized.slice(index * 2, index * 2 + 2), 16)
|
|
100
|
-
return out
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 字节数组转 base64 字符串。
|
|
105
|
-
* @param {Uint8Array} bytes 输入字节
|
|
106
|
-
* @returns {string} base64 编码
|
|
107
|
-
*/
|
|
108
|
-
function bytesToBase64(bytes) {
|
|
109
|
-
return btoa(String.fromCharCode(...bytes))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* base64 字符串转字节数组。
|
|
114
|
-
* @param {string} base64 输入 base64 字符串
|
|
115
|
-
* @returns {Uint8Array} 解码后的字节
|
|
116
|
-
*/
|
|
117
|
-
function base64ToBytes(base64) {
|
|
118
|
-
return Uint8Array.from(atob(base64).split('').map(ch => ch.charCodeAt(0)))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
76
|
/**
|
|
122
77
|
* 签名 Nostr 事件。
|
|
123
78
|
* @param {number} kind 事件 kind
|
|
@@ -231,6 +186,37 @@ function connectRelaysProgressive(relayUrls, onOpen, signal, sockets) {
|
|
|
231
186
|
}).catch(() => { /* 单路失败正常降级 */ })
|
|
232
187
|
}
|
|
233
188
|
|
|
189
|
+
/**
|
|
190
|
+
* 订阅指定 kind 的 Nostr 事件(渐进连中继,立即返回 cleanup)。
|
|
191
|
+
* @param {string[]} relayUrls 中继 URL 列表
|
|
192
|
+
* @param {{ kind: number, topic: string, tagX: string, onPayload: (bytes: Uint8Array, meta: { relayUrl: string, event: object }) => void }} options 订阅选项
|
|
193
|
+
* @returns {() => void} 取消订阅
|
|
194
|
+
*/
|
|
195
|
+
function subscribeNostrKind(relayUrls, options) {
|
|
196
|
+
const { kind, topic, tagX, onPayload } = options
|
|
197
|
+
const abortController = new AbortController()
|
|
198
|
+
/** @type {import('ws').WebSocket[]} */
|
|
199
|
+
const sockets = []
|
|
200
|
+
const subscriptionId = randomBytes(8).toString('hex')
|
|
201
|
+
connectRelaysProgressive(relayUrls, (ws, relayUrl) => {
|
|
202
|
+
ws.on('message', data => {
|
|
203
|
+
if (abortController.signal.aborted) return
|
|
204
|
+
let parsed
|
|
205
|
+
try { parsed = JSON.parse(String(data)) } catch { return }
|
|
206
|
+
if (parsed?.[0] !== 'EVENT') return
|
|
207
|
+
const nostrEvent = parsed[2]
|
|
208
|
+
if (nostrEvent?.kind !== kind) return
|
|
209
|
+
try { onPayload(base64ToBytes(nostrEvent.content), { relayUrl, event: nostrEvent }) }
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
})
|
|
212
|
+
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [kind], '#t': [topic], '#x': [tagX] }]))
|
|
213
|
+
}, abortController.signal, sockets)
|
|
214
|
+
return () => {
|
|
215
|
+
abortController.abort()
|
|
216
|
+
for (const ws of sockets) dropWebSocket(ws)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
234
220
|
/**
|
|
235
221
|
* 创建 Nostr discovery provider。
|
|
236
222
|
* subscribe/onSignal/advertise 首调用不阻塞等公网中继;连上后渐进生效。
|
|
@@ -240,9 +226,7 @@ function connectRelaysProgressive(relayUrls, onOpen, signal, sockets) {
|
|
|
240
226
|
* @returns {import('./index.mjs').DiscoveryProvider} Nostr 发现提供者
|
|
241
227
|
*/
|
|
242
228
|
export function createNostrDiscoveryProvider(options = {}) {
|
|
243
|
-
const relayUrls = options.relayUrls
|
|
244
|
-
? dedupeRelayUrls(options.relayUrls)
|
|
245
|
-
: [...DEFAULT_RELAY_URLS]
|
|
229
|
+
const relayUrls = dedupeRelayUrls(options.relayUrls || DEFAULT_RELAY_URLS)
|
|
246
230
|
const secretKey = randomBytes(32)
|
|
247
231
|
return {
|
|
248
232
|
id: 'nostr',
|
|
@@ -255,25 +239,25 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
255
239
|
* @returns {Promise<() => void>} 取消广播函数
|
|
256
240
|
*/
|
|
257
241
|
async advertise(topic, bytes) {
|
|
258
|
-
const
|
|
242
|
+
const abortController = new AbortController()
|
|
259
243
|
/**
|
|
260
244
|
* 向中继发布当前 advert。
|
|
261
245
|
* @returns {Promise<void>}
|
|
262
246
|
*/
|
|
263
247
|
const publish = async () => {
|
|
264
|
-
if (
|
|
248
|
+
if (abortController.signal.aborted) return
|
|
265
249
|
const event = await signNostrEvent(
|
|
266
250
|
NOSTR_ADVERT_KIND,
|
|
267
251
|
[['t', topic], ['x', 'advert']],
|
|
268
252
|
bytesToBase64(bytes),
|
|
269
253
|
secretKey,
|
|
270
254
|
)
|
|
271
|
-
await publishEvent(relayUrls, event,
|
|
255
|
+
await publishEvent(relayUrls, event, abortController.signal)
|
|
272
256
|
}
|
|
273
257
|
void publish().catch(() => { })
|
|
274
258
|
const timer = setInterval(() => { void publish().catch(() => { }) }, 5 * 60_000)
|
|
275
259
|
return () => {
|
|
276
|
-
|
|
260
|
+
abortController.abort()
|
|
277
261
|
clearInterval(timer)
|
|
278
262
|
}
|
|
279
263
|
},
|
|
@@ -284,27 +268,12 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
284
268
|
* @returns {Promise<() => void>} 取消订阅函数
|
|
285
269
|
*/
|
|
286
270
|
async subscribe(topic, onAdvert) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (ac.signal.aborted) return
|
|
294
|
-
let parsed
|
|
295
|
-
try { parsed = JSON.parse(String(data)) } catch { return }
|
|
296
|
-
if (!Array.isArray(parsed) || parsed[0] !== 'EVENT') return
|
|
297
|
-
const nostrEvent = parsed[2]
|
|
298
|
-
if (nostrEvent?.kind !== NOSTR_ADVERT_KIND) return
|
|
299
|
-
try { onAdvert(base64ToBytes(String(nostrEvent.content || '')), { relayUrl, event: nostrEvent }) }
|
|
300
|
-
catch { /* ignore */ }
|
|
301
|
-
})
|
|
302
|
-
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [NOSTR_ADVERT_KIND], '#t': [topic], '#x': ['advert'] }]))
|
|
303
|
-
}, ac.signal, sockets)
|
|
304
|
-
return () => {
|
|
305
|
-
ac.abort()
|
|
306
|
-
for (const ws of sockets) dropWebSocket(ws)
|
|
307
|
-
}
|
|
271
|
+
return subscribeNostrKind(relayUrls, {
|
|
272
|
+
kind: NOSTR_ADVERT_KIND,
|
|
273
|
+
topic,
|
|
274
|
+
tagX: 'advert',
|
|
275
|
+
onPayload: onAdvert,
|
|
276
|
+
})
|
|
308
277
|
},
|
|
309
278
|
/**
|
|
310
279
|
* 向中继发布 signal 事件(按需发送,仍等待至少一路成功)。
|
|
@@ -329,27 +298,12 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
329
298
|
* @returns {Promise<() => void>} 取消订阅函数
|
|
330
299
|
*/
|
|
331
300
|
async onSignal(topic, onSignal) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (ac.signal.aborted) return
|
|
339
|
-
let parsed
|
|
340
|
-
try { parsed = JSON.parse(String(data)) } catch { return }
|
|
341
|
-
if (!Array.isArray(parsed) || parsed[0] !== 'EVENT') return
|
|
342
|
-
const nostrEvent = parsed[2]
|
|
343
|
-
if (nostrEvent?.kind !== NOSTR_SIGNAL_KIND) return
|
|
344
|
-
try { onSignal(base64ToBytes(String(nostrEvent.content || '')), { relayUrl, event: nostrEvent }) }
|
|
345
|
-
catch { /* ignore */ }
|
|
346
|
-
})
|
|
347
|
-
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [NOSTR_SIGNAL_KIND], '#t': [topic], '#x': ['signal'] }]))
|
|
348
|
-
}, ac.signal, sockets)
|
|
349
|
-
return () => {
|
|
350
|
-
ac.abort()
|
|
351
|
-
for (const ws of sockets) dropWebSocket(ws)
|
|
352
|
-
}
|
|
301
|
+
return subscribeNostrKind(relayUrls, {
|
|
302
|
+
kind: NOSTR_SIGNAL_KIND,
|
|
303
|
+
topic,
|
|
304
|
+
tagX: 'signal',
|
|
305
|
+
onPayload: onSignal,
|
|
306
|
+
})
|
|
353
307
|
},
|
|
354
308
|
}
|
|
355
309
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { base64ToBytes } from '../core/bytes_codec.mjs'
|
|
2
2
|
import { verifiedChunkBytes } from '../files/chunk_fetch_verify.mjs'
|
|
3
3
|
|
|
4
4
|
/** @type {Map<string, { expectedHash: string, timer: ReturnType<typeof setTimeout>, resolve: (v: Uint8Array | null) => void, reject?: (e: Error) => void }>} */
|
|
@@ -109,14 +109,13 @@ export function resolveChunkFetchWait(key, expectedHash, bytes) {
|
|
|
109
109
|
* @returns {boolean} 是否命中 pending
|
|
110
110
|
*/
|
|
111
111
|
export function resolvePendingChunkFetch(payload) {
|
|
112
|
-
const requestId =
|
|
112
|
+
const requestId = typeof payload?.requestId === 'string' ? payload.requestId : ''
|
|
113
113
|
if (!requestId) return false
|
|
114
114
|
const entry = pendingChunkFetches.get(requestId)
|
|
115
115
|
if (!entry) return false
|
|
116
|
-
if (payload?.
|
|
116
|
+
if (typeof payload?.dataBase64 === 'string') {
|
|
117
117
|
try {
|
|
118
|
-
|
|
119
|
-
return resolveChunkFetchWait(requestId, entry.expectedHash, bytes)
|
|
118
|
+
return resolveChunkFetchWait(requestId, entry.expectedHash, base64ToBytes(payload.dataBase64))
|
|
120
119
|
}
|
|
121
120
|
catch { /* keep waiting */ }
|
|
122
121
|
return false
|
|
@@ -56,7 +56,7 @@ export function resolveEventTopologicalOrder(events, cache, options = {}) {
|
|
|
56
56
|
const tipsHash = computeLocalTipsHash(tips)
|
|
57
57
|
const eventCount = events.length
|
|
58
58
|
|
|
59
|
-
if (cache
|
|
59
|
+
if (cache?.order?.length) {
|
|
60
60
|
if (cache.tipsHash === tipsHash && cache.eventCount === eventCount) {
|
|
61
61
|
const ok = cache.order.length === eventCount && cache.order.every(id => byId.has(id))
|
|
62
62
|
if (ok) return cache.order
|
|
@@ -131,9 +131,8 @@ export function reduceEntityKeyRotate(state, event) {
|
|
|
131
131
|
export function reduceEntityKeyRevoke(state, event) {
|
|
132
132
|
const newGeneration = Number(event.content?.newGeneration)
|
|
133
133
|
const activePubKeyHex = normalizeHex64(event.content?.activePubKeyHex || '')
|
|
134
|
-
const revokeGenerations =
|
|
135
|
-
|
|
136
|
-
: []
|
|
134
|
+
const revokeGenerations = (event.content?.revokeGenerations || [])
|
|
135
|
+
.map(Number).filter(Number.isFinite)
|
|
137
136
|
if (!Number.isFinite(newGeneration) || newGeneration < 0 || !isHex64(activePubKeyHex))
|
|
138
137
|
return state
|
|
139
138
|
state.entityKeyHistory = state.entityKeyHistory || []
|
|
@@ -181,7 +180,7 @@ export function foldEntityKeyHistoryFromEvents(events) {
|
|
|
181
180
|
*/
|
|
182
181
|
export function entityKeyRevokeSignBytes(revokeBody) {
|
|
183
182
|
const body = {
|
|
184
|
-
revokeGenerations: (revokeBody.revokeGenerations || []).map(
|
|
183
|
+
revokeGenerations: (revokeBody.revokeGenerations || []).map(Number),
|
|
185
184
|
newGeneration: Number(revokeBody.newGeneration),
|
|
186
185
|
activePubKeyHex: normalizeHex64(revokeBody.activePubKeyHex || ''),
|
|
187
186
|
entityHash: String(revokeBody.entityHash || '').trim().toLowerCase(),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { createLruMap } from '../utils/lru.mjs'
|
|
2
|
+
|
|
3
|
+
/** 进程内拓扑序 memo 上限(每条目持有整份 order 数组) */
|
|
4
|
+
const MEMO_MAX = 64
|
|
5
|
+
|
|
6
|
+
/** @type {ReturnType<typeof createLruMap<string, { fp: string, order: string[] }>>} */
|
|
7
|
+
const memoByKey = createLruMap(MEMO_MAX)
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* 进程内拓扑序 memo;`resolveOrder` 可接入磁盘缓存等实现。
|
|
@@ -12,11 +17,13 @@ const memoByKey = new Map()
|
|
|
12
17
|
export function resolveTopologicalOrderMemoCached(memoKey, fingerprint, resolveOrder, options = {}) {
|
|
13
18
|
if (!options.force) {
|
|
14
19
|
const cached = memoByKey.get(memoKey)
|
|
15
|
-
if (cached?.fp === fingerprint && cached.order.length)
|
|
20
|
+
if (cached?.fp === fingerprint && cached.order.length) {
|
|
21
|
+
memoByKey.touch(memoKey, cached)
|
|
16
22
|
return cached.order
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
const order = resolveOrder()
|
|
19
|
-
memoByKey.
|
|
26
|
+
memoByKey.touch(memoKey, { fp: fingerprint, order })
|
|
20
27
|
return order
|
|
21
28
|
}
|
|
22
29
|
|
package/files/chunk_fetch.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { bytesToBase64 } from '../core/bytes_codec.mjs'
|
|
4
4
|
import {
|
|
5
5
|
MAX_PENDING_CHUNK_FETCHES,
|
|
6
6
|
pendingChunkFetches,
|
|
@@ -78,5 +78,5 @@ export async function handleIncomingChunkGet(username, payload, sendResponse, pe
|
|
|
78
78
|
if (!await hasChunk(hash)) return
|
|
79
79
|
const chunkBytes = await getChunk(hash)
|
|
80
80
|
if (!chunkBytes?.length) return
|
|
81
|
-
sendResponse({ requestId: payload.requestId,
|
|
81
|
+
sendResponse({ requestId: payload.requestId, dataBase64: bytesToBase64(chunkBytes) }, peerId)
|
|
82
82
|
}
|
package/files/evfs.mjs
CHANGED
|
@@ -86,7 +86,7 @@ export async function storeManifestParts(manifest, partBytes) {
|
|
|
86
86
|
*/
|
|
87
87
|
export async function readManifestPlaintext(replicaUsername, manifest, options = {}) {
|
|
88
88
|
const dagGroupId = manifest.meta?.groupId
|
|
89
|
-
if (
|
|
89
|
+
if (manifest.meta?.dagParts?.length && dagGroupId) {
|
|
90
90
|
const dagPlain = await readDagManifestPlaintext(replicaUsername, manifest)
|
|
91
91
|
if (dagPlain) return dagPlain
|
|
92
92
|
}
|
|
@@ -110,7 +110,7 @@ export async function readManifestPlaintext(replicaUsername, manifest, options =
|
|
|
110
110
|
*/
|
|
111
111
|
export async function readManifestPlaintextStream(replicaUsername, manifest, options = {}) {
|
|
112
112
|
const dagGroupId = manifest.meta?.groupId
|
|
113
|
-
if (
|
|
113
|
+
if (manifest.meta?.dagParts?.length && dagGroupId) {
|
|
114
114
|
const plain = await readManifestPlaintext(replicaUsername, manifest, options)
|
|
115
115
|
if (!plain) return null
|
|
116
116
|
return Readable.from([plain])
|
package/link/channel_mux.mjs
CHANGED
|
@@ -57,7 +57,7 @@ const DEFAULT_ACTION_PRIORITIES = Object.freeze({
|
|
|
57
57
|
* @returns {number} 优先级(未知 action 默认 5)
|
|
58
58
|
*/
|
|
59
59
|
export function resolveActionPriority(action) {
|
|
60
|
-
return DEFAULT_ACTION_PRIORITIES[
|
|
60
|
+
return DEFAULT_ACTION_PRIORITIES[action] ?? 5
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -67,12 +67,11 @@ export function resolveActionPriority(action) {
|
|
|
67
67
|
* @returns {'control' | 'bulk'} 目标通道名
|
|
68
68
|
*/
|
|
69
69
|
export function pickChannel(action, byteLength) {
|
|
70
|
-
|
|
71
|
-
if (normalized === 'ping' || normalized === 'pong' || normalized.startsWith('route_'))
|
|
70
|
+
if (action === 'ping' || action === 'pong' || action.startsWith('route_'))
|
|
72
71
|
return CHANNEL_CONTROL
|
|
73
|
-
if (
|
|
72
|
+
if (byteLength > BULK_CHANNEL_MIN_BYTES)
|
|
74
73
|
return CHANNEL_BULK
|
|
75
|
-
return resolveActionPriority(
|
|
74
|
+
return resolveActionPriority(action) <= 3 ? CHANNEL_CONTROL : CHANNEL_BULK
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|
|
@@ -162,13 +161,14 @@ export function createChannelSendQueues(options) {
|
|
|
162
161
|
const flush = channelName => {
|
|
163
162
|
scheduled[channelName] = false
|
|
164
163
|
const channel = getChannel(channelName)
|
|
165
|
-
if (
|
|
164
|
+
if (channel?.readyState !== 'open') return
|
|
166
165
|
const queue = queues[channelName]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
channel.send(
|
|
166
|
+
let i = 0
|
|
167
|
+
while (i < queue.length) {
|
|
168
|
+
if (readBufferedAmount(channel) > highWatermarkBytes) break
|
|
169
|
+
channel.send(queue[i++].bytes)
|
|
171
170
|
}
|
|
171
|
+
if (i > 0) queue.splice(0, i)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
return {
|