@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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/tcp_port.mjs +1 -1
  4. package/crypto/checkpoint_sign.mjs +2 -2
  5. package/dag/canonicalize_row.mjs +3 -3
  6. package/dag/index.mjs +2 -3
  7. package/discovery/bt/index.mjs +5 -5
  8. package/discovery/bt/runtime.mjs +3 -3
  9. package/discovery/mdns.mjs +7 -1
  10. package/discovery/nostr.mjs +53 -99
  11. package/federation/chunk_fetch_pending.mjs +4 -5
  12. package/federation/dag_order_cache.mjs +1 -1
  13. package/federation/entity_key_chain.mjs +3 -4
  14. package/federation/topo_order_memo.mjs +11 -4
  15. package/files/chunk_fetch.mjs +2 -2
  16. package/files/evfs.mjs +2 -2
  17. package/link/channel_mux.mjs +10 -10
  18. package/link/frame.mjs +76 -124
  19. package/link/handshake.mjs +5 -5
  20. package/link/pipe.mjs +49 -75
  21. package/link/providers/ble_gatt.mjs +14 -26
  22. package/link/providers/lan_tcp.mjs +17 -32
  23. package/link/providers/link_id_pipe.mjs +46 -0
  24. package/link/providers/webrtc.mjs +42 -51
  25. package/link/rtc.mjs +21 -9
  26. package/mailbox/deliver_or_store.mjs +1 -1
  27. package/node/identity.mjs +4 -4
  28. package/node/network.mjs +9 -9
  29. package/overlay/index.mjs +13 -13
  30. package/package.json +1 -1
  31. package/rooms/scoped_link.mjs +19 -35
  32. package/schemas/discovery.mjs +1 -2
  33. package/schemas/federation_pull.mjs +2 -2
  34. package/schemas/part_query.mjs +1 -1
  35. package/schemas/remote_event.mjs +1 -1
  36. package/transport/advert_ingest.mjs +20 -0
  37. package/transport/group_link_set.mjs +22 -41
  38. package/transport/ice_servers.mjs +2 -2
  39. package/transport/link_registry.mjs +113 -105
  40. package/transport/offer_answer.mjs +6 -2
  41. package/transport/peer_pool.mjs +53 -64
  42. package/transport/remote_user_room.mjs +13 -15
  43. package/transport/rtc_connection_budget.mjs +18 -4
  44. package/transport/runtime_bootstrap.mjs +8 -2
  45. package/transport/signal_crypto.mjs +25 -3
  46. package/transport/user_room.mjs +22 -15
  47. package/trust_graph/send.mjs +1 -1
  48. package/utils/emit_safe.mjs +11 -0
  49. package/utils/shuffle.mjs +13 -0
  50. package/utils/ttl_map.mjs +29 -4
  51. package/wire/ingress.mjs +1 -1
  52. package/wire/part_ingress.mjs +6 -8
  53. package/wire/part_invoke.mjs +4 -4
  54. package/wire/part_query.mjs +2 -3
  55. package/wire/part_query.tunables.json +6 -1
  56. package/wire/part_query_cache.mjs +1 -1
  57. 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` | Fount-network facade: dial fallback, scope/overlay |
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 |
@@ -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} u8 原始字节
7
- * @returns {string} 标准 Base64 文本
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 u8ToB64(u8) {
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 < u8.length; index++) binary += String.fromCharCode(u8[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} b64 标准 Base64 文本
74
+ * @param {string} base64 标准 Base64
17
75
  * @returns {Uint8Array} 解码后的字节
18
76
  */
19
- export function b64ToU8(b64) {
20
- const bin = atob(b64)
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 == null || port === '') return null
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 || typeof checkpoint !== 'object')
52
+ if (!checkpoint)
53
53
  return { valid: false, reason: 'checkpoint missing or not an object' }
54
- if (!Array.isArray(checkpoint.eventIdsInEpoch) || checkpoint.eventIdsInEpoch.length === 0)
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)
@@ -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] == null || obj[key] === '') return
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] == null || out[key] === '') continue
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 (Array.isArray(out.prev_event_ids))
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
- if (!Array.isArray(raw)) return []
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 == null) break
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
@@ -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 (!Array.isArray(parsed?.entries)) return []
49
+ if (!parsed?.entries?.length) return []
50
50
  return parsed.entries.map(entry => ({
51
- topic: String(entry?.topic || ''),
52
- bytes: Uint8Array.from(Buffer.from(String(entry?.data || ''), 'base64')),
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 {
@@ -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
 
@@ -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)?.delete(listener)
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
  }
@@ -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
- /** @type {string[]} */
61
- const out = []
62
- for (const url of Array.isArray(urls) ? urls : []) {
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, ...Array.isArray(userRelayUrls) ? userRelayUrls : []])
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 != null
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 ac = new AbortController()
242
+ const abortController = new AbortController()
259
243
  /**
260
244
  * 向中继发布当前 advert。
261
245
  * @returns {Promise<void>}
262
246
  */
263
247
  const publish = async () => {
264
- if (ac.signal.aborted) return
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, ac.signal)
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
- ac.abort()
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
- const ac = new AbortController()
288
- /** @type {import('ws').WebSocket[]} */
289
- const sockets = []
290
- const subscriptionId = randomBytes(8).toString('hex')
291
- connectRelaysProgressive(relayUrls, (ws, relayUrl) => {
292
- ws.on('message', data => {
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
- const ac = new AbortController()
333
- /** @type {import('ws').WebSocket[]} */
334
- const sockets = []
335
- const subscriptionId = randomBytes(8).toString('hex')
336
- connectRelaysProgressive(relayUrls, (ws, relayUrl) => {
337
- ws.on('message', data => {
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 { b64ToU8 } from '../core/bytes_codec.mjs'
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 = String(payload?.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?.dataB64) {
116
+ if (typeof payload?.dataBase64 === 'string') {
117
117
  try {
118
- const bytes = b64ToU8(String(payload.dataB64))
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 && Array.isArray(cache.order) && cache.order.length) {
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 = Array.isArray(event.content?.revokeGenerations)
135
- ? event.content.revokeGenerations.map(g => Number(g)).filter(Number.isFinite)
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(g => Number(g)),
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
- /** @type {Map<string, { fp: string, order: string[] }>} */
2
- const memoByKey = new Map()
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.set(memoKey, { fp: fingerprint, order })
26
+ memoByKey.touch(memoKey, { fp: fingerprint, order })
20
27
  return order
21
28
  }
22
29
 
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
 
3
- import { u8ToB64 } from '../core/bytes_codec.mjs'
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, dataB64: u8ToB64(chunkBytes) }, peerId)
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 (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
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 (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
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])
@@ -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[String(action || '').trim()] ?? 5
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
- const normalized = String(action || '').trim()
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 (Number(byteLength) > BULK_CHANNEL_MIN_BYTES)
72
+ if (byteLength > BULK_CHANNEL_MIN_BYTES)
74
73
  return CHANNEL_BULK
75
- return resolveActionPriority(normalized) <= 3 ? CHANNEL_CONTROL : CHANNEL_BULK
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 (!channel || channel.readyState !== 'open') return
164
+ if (channel?.readyState !== 'open') return
166
165
  const queue = queues[channelName]
167
- while (queue.length) {
168
- if (readBufferedAmount(channel) > highWatermarkBytes) return
169
- const item = queue.shift()
170
- channel.send(item.bytes)
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 {