@steve02081504/fount-p2p 0.0.22 → 0.0.24

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 (71) hide show
  1. package/README.md +8 -6
  2. package/core/entity_id.mjs +5 -5
  3. package/core/logical_entity.mjs +1 -1
  4. package/core/object.mjs +9 -0
  5. package/core/partpath.mjs +10 -0
  6. package/core/random_id.mjs +4 -2
  7. package/crypto/crypto.mjs +26 -93
  8. package/dag/storage.mjs +0 -5
  9. package/dag/strip_extensions.mjs +1 -3
  10. package/{wire/part_query_cache.mjs → federation/part_query/cache.mjs} +7 -7
  11. package/{wire/part_query.mjs → federation/part_query/runtime.mjs} +36 -77
  12. package/files/assemble.mjs +5 -5
  13. package/files/assemble_stream.mjs +2 -2
  14. package/files/chunk/fetch.mjs +101 -0
  15. package/files/chunk/pending.mjs +59 -0
  16. package/files/{chunk_provider_registry.mjs → chunk/provider_registry.mjs} +4 -5
  17. package/files/chunk/responder.mjs +104 -0
  18. package/files/{chunk_store.mjs → chunk/store.mjs} +2 -2
  19. package/files/{chunk_fetch_verify.mjs → chunk/verify.mjs} +2 -2
  20. package/files/evfs.mjs +18 -19
  21. package/files/fed/fetch_shared.mjs +39 -0
  22. package/files/fed/responder.mjs +47 -0
  23. package/files/{acl.mjs → manifest/acl.mjs} +4 -4
  24. package/files/{manifest_acl_registry.mjs → manifest/acl_registry.mjs} +14 -17
  25. package/files/{manifest_fetch.mjs → manifest/fetch.mjs} +47 -52
  26. package/files/{manifest.mjs → manifest/normalize.mjs} +2 -2
  27. package/files/manifest/pending.mjs +53 -0
  28. package/files/{public_manifest.mjs → manifest/public.mjs} +12 -12
  29. package/files/transfer_key.mjs +2 -2
  30. package/files/transfer_key_registry.mjs +7 -7
  31. package/index.mjs +4 -4
  32. package/infra/service.mjs +1 -1
  33. package/link/handshake.mjs +0 -3
  34. package/mailbox/deliver_or_store.mjs +0 -2
  35. package/mailbox/parse.mjs +1 -1
  36. package/mailbox/store.mjs +0 -9
  37. package/mailbox/wire.mjs +24 -16
  38. package/node/reputation_sync.mjs +33 -29
  39. package/package.json +9 -8
  40. package/registries/inbound.mjs +19 -9
  41. package/registries/part_path.mjs +3 -8
  42. package/schemas/discovery.mjs +1 -1
  43. package/schemas/federation_pull.mjs +1 -1
  44. package/schemas/part_query.mjs +6 -5
  45. package/transport/group_link_set.mjs +2 -2
  46. package/transport/link_registry.mjs +2 -2
  47. package/transport/node_scope/features.mjs +166 -0
  48. package/transport/node_scope/wire.mjs +124 -0
  49. package/transport/remote_user_room.mjs +0 -1
  50. package/{rooms → transport}/scoped_link.mjs +1 -1
  51. package/transport/user_room.mjs +15 -28
  52. package/utils/fetch_wait.mjs +98 -0
  53. package/wire/adapter.mjs +13 -0
  54. package/wire/ingress.mjs +1 -8
  55. package/wire/part/fanout.mjs +99 -0
  56. package/wire/part/group.mjs +45 -0
  57. package/wire/{part_ingress.mjs → part/ingress.mjs} +58 -67
  58. package/wire/{part_invoke.mjs → part/invoke.mjs} +1 -22
  59. package/wire/part/pending.mjs +32 -0
  60. package/wire/part/query.mjs +91 -0
  61. package/wire/subscribe.mjs +13 -0
  62. package/federation/chunk_fetch_pending.mjs +0 -124
  63. package/federation/manifest_fetch_pending.mjs +0 -88
  64. package/files/chunk_fetch.mjs +0 -99
  65. package/files/chunk_responder.mjs +0 -142
  66. package/transport/advert_ingest.mjs +0 -17
  67. package/transport/node_scope.mjs +0 -289
  68. package/wire/group_part.mjs +0 -43
  69. package/wire/part_common.mjs +0 -55
  70. package/wire/part_fanout.mjs +0 -52
  71. /package/{wire → schemas}/part_query.tunables.json +0 -0
@@ -1,4 +1,18 @@
1
- /** @typedef {import('../wire/part_invoke.mjs').PartInvokeResponse} PartInvokeResponse */
1
+ /**
2
+ * @typedef {{ message: string, code: string }} PartInvokeError
3
+ */
4
+
5
+ /**
6
+ * @typedef {object} PartInvokeResultResponse
7
+ * @property {unknown} result
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} PartInvokeErrorResponse
12
+ * @property {PartInvokeError} error
13
+ */
14
+
15
+ /** @typedef {PartInvokeResultResponse | PartInvokeErrorResponse} PartInvokeResponse */
2
16
 
3
17
  /**
4
18
  * @typedef {{
@@ -29,7 +43,7 @@ const deliveryHandlers = new Map()
29
43
  * @returns {void}
30
44
  */
31
45
  export function registerRpcInboundHandler(type, handler) {
32
- rpcHandlers.set(String(type || '').trim(), handler)
46
+ rpcHandlers.set(type, handler)
33
47
  }
34
48
 
35
49
  /**
@@ -38,7 +52,7 @@ export function registerRpcInboundHandler(type, handler) {
38
52
  * @returns {void}
39
53
  */
40
54
  export function registerDeliveryInboundHandler(type, handler) {
41
- deliveryHandlers.set(String(type || '').trim(), handler)
55
+ deliveryHandlers.set(type, handler)
42
56
  }
43
57
 
44
58
  /**
@@ -47,9 +61,7 @@ export function registerDeliveryInboundHandler(type, handler) {
47
61
  * @returns {Promise<PartInvokeResponse | null>} 处理器返回值
48
62
  */
49
63
  export async function dispatchRpcInbound(inboundContext, message) {
50
- const type = String(message?.type || '').trim()
51
- if (!type) return null
52
- const handler = rpcHandlers.get(type)
64
+ const handler = rpcHandlers.get(message.type)
53
65
  if (!handler) return null
54
66
  return handler(inboundContext, message)
55
67
  }
@@ -60,9 +72,7 @@ export async function dispatchRpcInbound(inboundContext, message) {
60
72
  * @returns {Promise<void>}
61
73
  */
62
74
  export async function dispatchDeliveryInbound(inboundContext, message) {
63
- const type = String(message?.type || '').trim()
64
- if (!type) return
65
- const handler = deliveryHandlers.get(type)
75
+ const handler = deliveryHandlers.get(message.type)
66
76
  if (!handler) return
67
77
  await handler(inboundContext, message)
68
78
  }
@@ -1,5 +1,3 @@
1
- import { normalizePartpath } from '../wire/part_invoke.mjs'
2
-
3
1
  /** @type {Map<string, string>} shell 逻辑名 → partpath */
4
2
  const shellPartpaths = new Map()
5
3
 
@@ -10,10 +8,7 @@ const shellPartpaths = new Map()
10
8
  * @returns {void}
11
9
  */
12
10
  export function registerShellPartpath(shellKey, partpath) {
13
- const key = String(shellKey || '').trim()
14
- const path = normalizePartpath(partpath)
15
- if (!key || !path) throw new Error('invalid_shell_partpath_registration')
16
- shellPartpaths.set(key, path)
11
+ shellPartpaths.set(shellKey, partpath)
17
12
  }
18
13
 
19
14
  /**
@@ -21,7 +16,7 @@ export function registerShellPartpath(shellKey, partpath) {
21
16
  * @returns {void}
22
17
  */
23
18
  export function unregisterShellPartpath(shellKey) {
24
- shellPartpaths.delete(String(shellKey || '').trim())
19
+ shellPartpaths.delete(shellKey)
25
20
  }
26
21
 
27
22
  /**
@@ -29,7 +24,7 @@ export function unregisterShellPartpath(shellKey) {
29
24
  * @returns {string} 已注册的 partpath
30
25
  */
31
26
  export function getShellPartpath(shellKey) {
32
- const path = shellPartpaths.get(String(shellKey || '').trim())
27
+ const path = shellPartpaths.get(shellKey)
33
28
  if (!path) throw new Error(`shell partpath not registered: ${shellKey}`)
34
29
  return path
35
30
  }
@@ -1,5 +1,5 @@
1
1
  import { assertHex64, isHex64, isSignatureHex128, normalizeHex64 } from '../core/hexIds.mjs'
2
- import { isPlainObject } from '../wire/ingress.mjs'
2
+ import { isPlainObject } from '../core/object.mjs'
3
3
 
4
4
  const MAX_DISCOVERY_ADS = 64
5
5
 
@@ -2,8 +2,8 @@
2
2
  * 联邦补拉 attestation / HPKE 响应 wire 解析(无 attestation/envelope 即丢弃)。
3
3
  */
4
4
  import { isHex64, isSignatureHex128, normalizeHex64 } from '../core/hexIds.mjs'
5
+ import { isPlainObject } from '../core/object.mjs'
5
6
  import { EVENT_ID_HEX } from '../dag/index.mjs'
6
- import { isPlainObject } from '../wire/ingress.mjs'
7
7
 
8
8
  /** @typedef {{ requesterPubKeyHash: string, groupId: string, requestId: string, timestamp: number, wantIds?: string[], signature: string }} PullAttestation */
9
9
 
@@ -2,9 +2,10 @@ import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { canonicalStringify } from '../core/canonical_json.mjs'
4
4
  import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
5
- import { isPlainObject } from '../wire/ingress.mjs'
6
- import { normalizePartpath } from '../wire/part_invoke.mjs'
7
- import partQueryTunables from '../wire/part_query.tunables.json' with { type: 'json' }
5
+ import { isPlainObject } from '../core/object.mjs'
6
+ import { parsePartpath } from '../core/partpath.mjs'
7
+
8
+ import partQueryTunables from './part_query.tunables.json' with { type: 'json' }
8
9
 
9
10
  /**
10
11
  * @typedef {{
@@ -100,7 +101,7 @@ export function parsePartQueryReq(value, tunables = partQueryTunables) {
100
101
  if (!requestId) return null
101
102
  const originNodeHash = normalizeHex64(value.originNodeHash)
102
103
  if (!isHex64(originNodeHash)) return null
103
- const partpath = normalizePartpath(value.partpath)
104
+ const partpath = parsePartpath(value.partpath)
104
105
  if (!partpath) return null
105
106
  const kind = String(value.kind || '').trim()
106
107
  if (!kind) return null
@@ -143,7 +144,7 @@ export function parsePartQueryRes(value, tunables = partQueryTunables) {
143
144
  * @returns {string | null} 规范化三元组的缓存材料;非法 null
144
145
  */
145
146
  export function normalizePartQueryCacheMaterial(partpath, kind, query) {
146
- const path = normalizePartpath(partpath)
147
+ const path = parsePartpath(partpath)
147
148
  const k = String(kind || '').trim()
148
149
  if (!path || !k) return null
149
150
  if (measureJsonBytes(query) > partQueryTunables.maxQueryBytes) return null
@@ -1,9 +1,9 @@
1
+ import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
1
2
  import { listVisibleNodeHashes, startGroupPresence, watchVerifiedGroupAdverts } from '../discovery/index.mjs'
2
3
  import { loadPeerPoolView } from '../node/network.mjs'
3
4
  import { loadReputation } from '../node/reputation_store.mjs'
4
5
  import { emitSafe } from '../utils/emit_safe.mjs'
5
6
 
6
- import { applyAdvertPeerHints } from './advert_ingest.mjs'
7
7
  import { getLinkRegistry } from './link_registry.mjs'
8
8
  import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './peer_pool.mjs'
9
9
  import { loadTransportTunables } from './tunables.mjs'
@@ -208,7 +208,7 @@ export function createGroupLinkSet(options) {
208
208
  registerCleanup(await watchVerifiedGroupAdverts(roomSecret, async (verifiedNodeHash, body, meta) => {
209
209
  if (verifiedNodeHash === selfNodeHash) return
210
210
  if (!allowNode(verifiedNodeHash)) return
211
- applyAdvertPeerHints(verifiedNodeHash, body, meta)
211
+ noteAdvertPeerHints(verifiedNodeHash, body, meta)
212
212
  notePeerCandidate(verifiedNodeHash)
213
213
  }))
214
214
 
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
4
4
  import { keyPairFromSeed } from '../crypto/crypto.mjs'
5
+ import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
5
6
  import { watchVerifiedNodeAdvert, setDiscoveryLinkDialer, prepareConnectToNode } from '../discovery/index.mjs'
6
7
  import { setDiscoveryPeerClueListener } from '../discovery/peer_clue.mjs'
7
8
  import { listLinkProviders } from '../link/providers/index.mjs'
@@ -13,7 +14,6 @@ import { ms } from '../utils/duration.mjs'
13
14
  import { emitSafe } from '../utils/emit_safe.mjs'
14
15
  import { createLruMap } from '../utils/lru.mjs'
15
16
 
16
- import { applyAdvertPeerHints } from './advert_ingest.mjs'
17
17
  import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
18
18
  import { createMeshKeepalive } from './mesh_keepalive.mjs'
19
19
  import { createOfferAnswerDial } from './offer_answer.mjs'
@@ -650,7 +650,7 @@ export function createLinkRegistry(options = {}) {
650
650
  async watchNodeAdvert(nodeHash, onAdvert) {
651
651
  const hash = normalizeHex64(nodeHash)
652
652
  return await watchVerifiedNodeAdvert(hash, async (verifiedNodeHash, body, meta) => {
653
- applyAdvertPeerHints(verifiedNodeHash, body, meta)
653
+ noteAdvertPeerHints(verifiedNodeHash, body, meta)
654
654
  recentAdverts.touch(verifiedNodeHash, Date.now())
655
655
  dialCooldown.delete(verifiedNodeHash)
656
656
  await Promise.resolve(onAdvert(verifiedNodeHash, body))
@@ -0,0 +1,166 @@
1
+ import { attachNodeScopeFedResponder } from '../../files/fed/responder.mjs'
2
+ import { attachMailboxWire } from '../../mailbox/wire.mjs'
3
+ import { attachPartWire } from '../../wire/part/ingress.mjs'
4
+ import { attachPartQueryWire } from '../../wire/part/query.mjs'
5
+
6
+ import {
7
+ clearNodeScopeSubscribe,
8
+ ensureNodeScope,
9
+ getNodeScopeContext,
10
+ getNodeScopeWire,
11
+ } from './wire.mjs'
12
+
13
+ /** @typedef {import('./wire.mjs').NodeScopeWire} NodeScopeWire */
14
+ /** @typedef {import('./wire.mjs').NodeScopeContext} NodeScopeContext */
15
+
16
+ /** @type {Set<() => void>} */
17
+ const nodeScopeFeatureDisposers = new Set()
18
+
19
+ /** @type {Map<string, { count: number, disposeCore: () => void }>} */
20
+ const featureAttachRefs = new Map()
21
+
22
+ /**
23
+ * @param {() => void} dispose - feature 卸载函数
24
+ * @returns {() => void} 包装后的 dispose(同时从跟踪集移除)
25
+ */
26
+ function trackFeatureDisposer(dispose) {
27
+ nodeScopeFeatureDisposers.add(dispose)
28
+ return () => {
29
+ dispose()
30
+ nodeScopeFeatureDisposers.delete(dispose)
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 同一 feature 多次 attach 共享一份 wire;dispose 引用计数归零才卸。
36
+ * @param {string} key - feature 去重键
37
+ * @param {() => () => void} attachCore - 首次 attach 时执行,返回核心 dispose
38
+ * @returns {() => void} 引用计数包装的 dispose
39
+ */
40
+ function attachFeatureRefCounted(key, attachCore) {
41
+ let entry = featureAttachRefs.get(key)
42
+ if (!entry) {
43
+ entry = { count: 0, disposeCore: attachCore() }
44
+ featureAttachRefs.set(key, entry)
45
+ }
46
+ entry.count++
47
+ return trackFeatureDisposer(() => {
48
+ const current = featureAttachRefs.get(key)
49
+ if (!current) return
50
+ current.count--
51
+ if (current.count > 0) return
52
+ current.disposeCore()
53
+ featureAttachRefs.delete(key)
54
+ })
55
+ }
56
+
57
+ /**
58
+ * @param {string} key - feature 去重键
59
+ * @param {(wire: NodeScopeWire, context: NodeScopeContext) => () => void} attachCore - 首次 attach
60
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
61
+ * @returns {() => void} 引用计数包装的 dispose
62
+ */
63
+ export function attachNodeScopeFeature(key, attachCore, options = {}) {
64
+ ensureNodeScope(options)
65
+ return attachFeatureRefCounted(key, () => attachCore(getNodeScopeWire(), getNodeScopeContext()))
66
+ }
67
+
68
+ /** @type {Record<string, (wire: NodeScopeWire, context: NodeScopeContext) => () => void>} */
69
+ const builtinFeatures = {
70
+ /**
71
+ * @param {NodeScopeWire} wire node scope wire
72
+ * @param {NodeScopeContext} context 入站上下文
73
+ * @returns {() => void} 取消挂载的 dispose
74
+ */
75
+ mailbox: (wire, context) => attachMailboxWire(context, wire),
76
+ /**
77
+ * @param {NodeScopeWire} wire node scope wire
78
+ * @param {NodeScopeContext} context 入站上下文
79
+ * @returns {() => void} 取消挂载的 dispose
80
+ */
81
+ part: (wire, context) => attachPartWire(context, wire),
82
+ /**
83
+ * @param {NodeScopeWire} wire node scope wire
84
+ * @param {NodeScopeContext} context 入站上下文
85
+ * @returns {() => void} 取消挂载的 dispose
86
+ */
87
+ partQuery: (wire, context) => attachPartQueryWire(context, wire),
88
+ /**
89
+ * @param {NodeScopeWire} wire node scope wire
90
+ * @returns {() => void} 取消挂载的 dispose
91
+ */
92
+ chunks: wire => attachNodeScopeFedResponder(wire),
93
+ }
94
+
95
+ /**
96
+ * @param {keyof typeof builtinFeatures} key - 内置 feature 名
97
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
98
+ * @returns {() => void} 引用计数包装的 dispose
99
+ */
100
+ function attachBuiltin(key, options = {}) {
101
+ return attachNodeScopeFeature(key, builtinFeatures[key], options)
102
+ }
103
+
104
+ /**
105
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
106
+ * @returns {() => void} 取消挂载的 dispose
107
+ */
108
+ export function attachNodeScopeMailbox(options = {}) {
109
+ return attachBuiltin('mailbox', options)
110
+ }
111
+
112
+ /**
113
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
114
+ * @returns {() => void} 取消挂载的 dispose
115
+ */
116
+ export function attachNodeScopePart(options = {}) {
117
+ return attachBuiltin('part', options)
118
+ }
119
+
120
+ /**
121
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
122
+ * @returns {() => void} 取消挂载的 dispose
123
+ */
124
+ export function attachNodeScopePartQuery(options = {}) {
125
+ return attachBuiltin('partQuery', options)
126
+ }
127
+
128
+ /**
129
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
130
+ * @returns {() => void} 取消挂载的 dispose
131
+ */
132
+ export function attachNodeScopeChunks(options = {}) {
133
+ return attachBuiltin('chunks', options)
134
+ }
135
+
136
+ /**
137
+ * 全业务 preset(part / partQuery / mailbox / chunks)。
138
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
139
+ * @returns {() => void} 取消全部 preset 挂载的 dispose
140
+ */
141
+ export function attachNodeScopeDefaultFeatures(options = {}) {
142
+ const disposers = [
143
+ attachNodeScopePart(options),
144
+ attachNodeScopePartQuery(options),
145
+ attachNodeScopeMailbox(options),
146
+ attachNodeScopeChunks(options),
147
+ ]
148
+ return () => {
149
+ for (const dispose of disposers) dispose()
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 卸掉 feature 挂载;可选保留 node scope 订阅。
155
+ * @param {{ keepSubscribe?: boolean }} [options] - true 时保留 node scope 订阅
156
+ * @returns {void}
157
+ */
158
+ export function stopNodeScopeRuntime(options = {}) {
159
+ for (const dispose of [...nodeScopeFeatureDisposers]) dispose()
160
+ nodeScopeFeatureDisposers.clear()
161
+ for (const entry of featureAttachRefs.values())
162
+ try { entry.disposeCore() } catch { /* ignore */ }
163
+ featureAttachRefs.clear()
164
+ if (options.keepSubscribe) return
165
+ clearNodeScopeSubscribe()
166
+ }
@@ -0,0 +1,124 @@
1
+ import { subscribeScope, sendToNodeLink } from '../link_registry.mjs'
2
+
3
+ /** @typedef {import('../../wire/adapter.mjs').WireAdapter} NodeScopeWire */
4
+ /** @typedef {{ replicaUsername: string }} NodeScopeContext */
5
+
6
+ /** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
7
+ const nodeActionHandlers = new Map()
8
+
9
+ /** @type {NodeScopeContext} */
10
+ const nodeScopeContext = { replicaUsername: '' }
11
+
12
+ /** @type {NodeScopeWire | null} */
13
+ let nodeScopeWire = null
14
+
15
+ /** @type {(() => void) | null} */
16
+ let nodeScopeSubscribeCleanup = null
17
+
18
+ /**
19
+ * @returns {NodeScopeWire} node scope 的 wire 适配器
20
+ */
21
+ function createNodeScopeWire() {
22
+ return {
23
+ /**
24
+ * @param {string} name action 名
25
+ * @param {(payload: unknown, peerId: string) => void} handler 回调
26
+ * @returns {() => void} 取消订阅
27
+ */
28
+ on(name, handler) {
29
+ if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
30
+ nodeActionHandlers.get(name).add(handler)
31
+ return () => {
32
+ const set = nodeActionHandlers.get(name)
33
+ if (!set) return
34
+ set.delete(handler)
35
+ if (!set.size) nodeActionHandlers.delete(name)
36
+ }
37
+ },
38
+ /**
39
+ * @param {string} name action 名
40
+ * @param {unknown} payload 载荷
41
+ * @param {string} peerId 目标 peer
42
+ * @returns {void}
43
+ */
44
+ send(name, payload, peerId) {
45
+ if (!peerId) return
46
+ void sendToNodeLink(peerId, { scope: 'node', action: name, payload }).catch(() => { })
47
+ },
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @param {string} action - action 名
53
+ * @returns {boolean} 是否已挂载处理器
54
+ */
55
+ export function hasNodeScopeAction(action) {
56
+ return (nodeActionHandlers.get(action)?.size ?? 0) > 0
57
+ }
58
+
59
+ /**
60
+ * @param {string} action - action 名
61
+ * @returns {number} 已注册的处理器数量
62
+ */
63
+ export function countNodeScopeActionHandlers(action) {
64
+ return nodeActionHandlers.get(action)?.size ?? 0
65
+ }
66
+
67
+ /**
68
+ * 测试/调试:直接派发已挂载的 node action。
69
+ * @param {string} action - action 名
70
+ * @param {unknown} payload - 载荷
71
+ * @param {string} peerId - 发送方 nodeHash
72
+ * @returns {boolean} 是否有处理器被调用
73
+ */
74
+ export function dispatchNodeScopeAction(action, payload, peerId) {
75
+ const handlers = nodeActionHandlers.get(action)
76
+ if (!handlers?.size) return false
77
+ for (const handler of handlers)
78
+ try { handler(payload, peerId) } catch { /* ignore */ }
79
+ return true
80
+ }
81
+
82
+ /**
83
+ * @returns {NodeScopeWire | null} 当前 wire,未 ensure 时为 null
84
+ */
85
+ export function getNodeScopeWire() {
86
+ return nodeScopeWire
87
+ }
88
+
89
+ /**
90
+ * @returns {NodeScopeContext} 可变 node scope 上下文
91
+ */
92
+ export function getNodeScopeContext() {
93
+ return nodeScopeContext
94
+ }
95
+
96
+ /**
97
+ * 只订阅 node scope 派发,不挂任何 feature。
98
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
99
+ * @returns {() => void} 取消订阅的 dispose
100
+ */
101
+ export function ensureNodeScope(options = {}) {
102
+ if (options.replicaUsername != null)
103
+ nodeScopeContext.replicaUsername = String(options.replicaUsername)
104
+ if (nodeScopeSubscribeCleanup) return nodeScopeSubscribeCleanup
105
+ nodeScopeSubscribeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
106
+ const handlers = nodeActionHandlers.get(envelope.action)
107
+ if (!handlers?.size) return
108
+ for (const handler of handlers)
109
+ try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
110
+ })
111
+ nodeScopeWire ||= createNodeScopeWire()
112
+ return nodeScopeSubscribeCleanup
113
+ }
114
+
115
+ /**
116
+ * 卸掉 scope 订阅与 wire(feature 须先卸)。
117
+ * @returns {void}
118
+ */
119
+ export function clearNodeScopeSubscribe() {
120
+ nodeScopeSubscribeCleanup?.()
121
+ nodeScopeSubscribeCleanup = null
122
+ nodeScopeWire = null
123
+ nodeActionHandlers.clear()
124
+ }
@@ -28,7 +28,6 @@ registerFederationRoomProvider('remote-user-room', () => {
28
28
  * @returns {Promise<RemoteUserRoomSlot>} 房间槽
29
29
  */
30
30
  export async function ensureRemoteUserRoom(username, targetNodeHash) {
31
- void username
32
31
  const key = targetNodeHash.toLowerCase()
33
32
  const existing = slots.get(key)
34
33
  if (existing) return existing
@@ -1,4 +1,4 @@
1
- import { createGroupLinkSet } from '../transport/group_link_set.mjs'
1
+ import { createGroupLinkSet } from './group_link_set.mjs'
2
2
 
3
3
  /**
4
4
  * scoped 房间:group_link_set 的薄预设(任意 scope + allowNode + 发现即拨)。
@@ -5,21 +5,12 @@ import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
5
5
  import { shuffleInPlace } from '../utils/shuffle.mjs'
6
6
 
7
7
  import { getLinkRegistry, listLinks, sendToNodeLink } from './link_registry.mjs'
8
- import {
9
- attachUserRoomDefaultWires,
10
- ensureNodeScope,
11
- } from './node_scope.mjs'
8
+ import { attachNodeScopeDefaultFeatures } from './node_scope/features.mjs'
9
+ import { ensureNodeScope } from './node_scope/wire.mjs'
12
10
  import { USER_ROOM_SCOPE } from './room_scopes.mjs'
13
11
 
14
- /**
15
- * 经 node scope 向对端发 action。
16
- * @param {string} peerId 对端 nodeHash
17
- * @param {string} action 动作名
18
- * @param {unknown} payload 载荷
19
- * @returns {Promise<boolean>} 是否发出
20
- */
21
- const sendNodeAction = (peerId, action, payload) =>
22
- sendToNodeLink(peerId, { scope: 'node', action, payload })
12
+ /** User Room 随机 peer 转发默认上限 */
13
+ const USER_ROOM_PEER_FANOUT_DEFAULT = 6
23
14
 
24
15
  /** @type {Promise<UserRoomSlot> | null} */
25
16
  let userRoomInflight = null
@@ -50,7 +41,7 @@ export function activeLinkRoster() {
50
41
  * roomId: string
51
42
  * roomSecret: string
52
43
  * room: object | null
53
- * sendToPeer: (peerId: string, actionName: string, payload: unknown) => void
44
+ * sendToPeer: (peerId: string, actionName: string, payload: unknown) => Promise<boolean>
54
45
  * getRoster: () => Array<{ peerId: string, remoteNodeHash: string | undefined }>
55
46
  * getPeerIdByNodeHash: (nodeHash: string) => string | null
56
47
  * }} UserRoomSlot
@@ -74,7 +65,7 @@ registerFederationRoomProvider('user-room', () => {
74
65
  * @param {string} peerId - 目标 peer
75
66
  * @param {string} actionName - node scope action 名
76
67
  * @param {unknown} payload - 载荷
77
- * @returns {void}
68
+ * @returns {Promise<boolean>} 是否发出
78
69
  */
79
70
  sendToPeer: (peerId, actionName, payload) => slot.sendToPeer(peerId, actionName, payload),
80
71
  }]
@@ -95,7 +86,7 @@ export function resolveUserRoomCredentials() {
95
86
  }
96
87
 
97
88
  /**
98
- * 用户房间槽 + runtime;默认不挂业务 wire(用 `attachUserRoomDefaultWires` / `attachDefaultWires: true`)。
89
+ * 用户房间槽 + runtime;默认不挂业务 wire(用 `attachNodeScopeDefaultFeatures` / `attachDefaultWires: true`)。
99
90
  * @param {{ replicaUsername?: string, attachDefaultWires?: boolean }} [options] - 副本用户名与是否挂载默认 wire
100
91
  * @returns {Promise<UserRoomSlot>} 用户房间槽
101
92
  */
@@ -105,7 +96,7 @@ export async function ensureUserRoom(options = {}) {
105
96
  ensureNodeScope({ replicaUsername: options.replicaUsername })
106
97
  if (userRoomSlot) {
107
98
  if (attachDefaultWires && !userRoomDefaultWiresDispose)
108
- userRoomDefaultWiresDispose = attachUserRoomDefaultWires({ replicaUsername: options.replicaUsername })
99
+ userRoomDefaultWiresDispose = attachNodeScopeDefaultFeatures({ replicaUsername: options.replicaUsername })
109
100
  return userRoomSlot
110
101
  }
111
102
  if (userRoomInflight) return await userRoomInflight
@@ -115,7 +106,7 @@ export async function ensureUserRoom(options = {}) {
115
106
  await getLinkRegistry().ensureRuntime()
116
107
  ensureNodeScope({ replicaUsername: options.replicaUsername })
117
108
  if (attachDefaultWires && !userRoomDefaultWiresDispose)
118
- userRoomDefaultWiresDispose = attachUserRoomDefaultWires({ replicaUsername: options.replicaUsername })
109
+ userRoomDefaultWiresDispose = attachNodeScopeDefaultFeatures({ replicaUsername: options.replicaUsername })
119
110
  const creds = resolveUserRoomCredentials()
120
111
  userRoomSlot = {
121
112
  roomId: creds.roomId,
@@ -125,10 +116,10 @@ export async function ensureUserRoom(options = {}) {
125
116
  * @param {string} peerId - 目标 peer
126
117
  * @param {string} actionName - node scope action 名
127
118
  * @param {unknown} payload - 载荷
128
- * @returns {void}
119
+ * @returns {Promise<boolean>} 是否发出
129
120
  */
130
121
  sendToPeer(peerId, actionName, payload) {
131
- void sendNodeAction(peerId, actionName, payload).catch(() => { })
122
+ return sendToNodeLink(peerId, { scope: 'node', action: actionName, payload }).catch(() => false)
132
123
  },
133
124
  /**
134
125
  * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 当前活跃链路 roster
@@ -162,16 +153,15 @@ export async function ensureUserRoom(options = {}) {
162
153
  * @returns {Promise<number>} 成功转发的 peer 数
163
154
  */
164
155
  export async function deliverToUserRoomPeers(username, actionName, payload, exceptPeerId = null, limit) {
165
- void username
166
- const { USER_ROOM_PEER_FANOUT_DEFAULT } = await import('../wire/part_common.mjs')
167
156
  const fanoutLimit = limit ?? USER_ROOM_PEER_FANOUT_DEFAULT
168
- const body = { ...payload, nodeHash: getNodeHash() }
157
+ if (fanoutLimit <= 0) return 0
158
+ const slot = await ensureUserRoom({ replicaUsername: username })
169
159
  let sent = 0
170
- const peers = shuffleInPlace(activeLinkRoster()
160
+ const peers = shuffleInPlace(slot.getRoster()
171
161
  .filter(({ peerId }) => peerId && peerId !== exceptPeerId))
172
162
  for (const { peerId } of peers)
173
163
  try {
174
- if (await sendNodeAction(peerId, actionName, body))
164
+ if (await slot.sendToPeer(peerId, actionName, { ...payload, nodeHash: getNodeHash() }))
175
165
  sent++
176
166
  if (sent >= fanoutLimit) break
177
167
  }
@@ -179,6 +169,3 @@ export async function deliverToUserRoomPeers(username, actionName, payload, exce
179
169
 
180
170
  return sent
181
171
  }
182
-
183
- /** 再导出:node-scope 订阅与默认 wires(见 `node_scope.mjs`)。 */
184
- export { attachUserRoomDefaultWires, ensureNodeScope } from './node_scope.mjs'