@steve02081504/fount-p2p 0.0.23 → 0.0.25
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 +8 -6
- package/core/object.mjs +9 -0
- package/core/partpath.mjs +10 -0
- package/dag/storage.mjs +0 -5
- package/dag/strip_extensions.mjs +1 -3
- package/{wire/part_query_cache.mjs → federation/part_query/cache.mjs} +7 -7
- package/{wire/part_query.mjs → federation/part_query/runtime.mjs} +36 -77
- package/files/assemble.mjs +5 -5
- package/files/assemble_stream.mjs +2 -2
- package/files/chunk/fetch.mjs +101 -0
- package/files/chunk/pending.mjs +59 -0
- package/files/{chunk_provider_registry.mjs → chunk/provider_registry.mjs} +4 -5
- package/files/chunk/responder.mjs +104 -0
- package/files/{chunk_store.mjs → chunk/store.mjs} +2 -2
- package/files/{chunk_fetch_verify.mjs → chunk/verify.mjs} +2 -2
- package/files/evfs.mjs +18 -19
- package/files/fed/fetch_shared.mjs +39 -0
- package/files/fed/responder.mjs +47 -0
- package/files/{acl.mjs → manifest/acl.mjs} +4 -4
- package/files/{manifest_acl_registry.mjs → manifest/acl_registry.mjs} +14 -17
- package/files/{manifest_fetch.mjs → manifest/fetch.mjs} +47 -52
- package/files/{manifest.mjs → manifest/normalize.mjs} +2 -2
- package/files/manifest/pending.mjs +53 -0
- package/files/{public_manifest.mjs → manifest/public.mjs} +12 -12
- package/files/transfer_key.mjs +2 -2
- package/files/transfer_key_registry.mjs +7 -7
- package/index.mjs +4 -4
- package/infra/service.mjs +1 -1
- package/link/handshake.mjs +0 -3
- package/mailbox/deliver_or_store.mjs +0 -2
- package/mailbox/parse.mjs +1 -1
- package/mailbox/store.mjs +0 -9
- package/mailbox/wire.mjs +24 -16
- package/node/reputation_sync.mjs +33 -29
- package/package.json +10 -10
- package/registries/inbound.mjs +19 -9
- package/registries/part_path.mjs +3 -8
- package/schemas/discovery.mjs +1 -1
- package/schemas/federation_pull.mjs +1 -1
- package/schemas/part_query.mjs +6 -5
- package/transport/group_link_set.mjs +2 -2
- package/transport/link_registry.mjs +2 -2
- package/transport/node_scope/features.mjs +166 -0
- package/transport/node_scope/wire.mjs +143 -0
- package/transport/remote_user_room.mjs +0 -1
- package/{rooms → transport}/scoped_link.mjs +1 -1
- package/transport/user_room.mjs +15 -28
- package/utils/fetch_wait.mjs +98 -0
- package/wire/adapter.mjs +13 -0
- package/wire/ingress.mjs +1 -8
- package/wire/part/fanout.mjs +99 -0
- package/wire/part/group.mjs +45 -0
- package/wire/{part_ingress.mjs → part/ingress.mjs} +58 -67
- package/wire/{part_invoke.mjs → part/invoke.mjs} +1 -22
- package/wire/part/pending.mjs +32 -0
- package/wire/part/query.mjs +91 -0
- package/wire/subscribe.mjs +13 -0
- package/federation/chunk_fetch_pending.mjs +0 -124
- package/federation/manifest_fetch_pending.mjs +0 -88
- package/files/chunk_fetch.mjs +0 -99
- package/files/chunk_responder.mjs +0 -142
- package/transport/advert_ingest.mjs +0 -17
- package/transport/node_scope.mjs +0 -289
- package/wire/group_part.mjs +0 -43
- package/wire/part_common.mjs +0 -55
- package/wire/part_fanout.mjs +0 -52
- /package/{wire → schemas}/part_query.tunables.json +0 -0
|
@@ -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,143 @@
|
|
|
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 {Set<(context: NodeScopeContext, wire: NodeScopeWire) => void>} */
|
|
10
|
+
const nodeScopeWireHooks = new Set()
|
|
11
|
+
|
|
12
|
+
/** @type {NodeScopeContext} */
|
|
13
|
+
const nodeScopeContext = { replicaUsername: '' }
|
|
14
|
+
|
|
15
|
+
/** @type {NodeScopeWire | null} */
|
|
16
|
+
let nodeScopeWire = null
|
|
17
|
+
|
|
18
|
+
/** @type {(() => void) | null} */
|
|
19
|
+
let nodeScopeSubscribeCleanup = null
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @returns {NodeScopeWire} node scope 的 wire 适配器
|
|
23
|
+
*/
|
|
24
|
+
function createNodeScopeWire() {
|
|
25
|
+
return {
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} name action 名
|
|
28
|
+
* @param {(payload: unknown, peerId: string) => void} handler 回调
|
|
29
|
+
* @returns {() => void} 取消订阅
|
|
30
|
+
*/
|
|
31
|
+
on(name, handler) {
|
|
32
|
+
if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
|
|
33
|
+
nodeActionHandlers.get(name).add(handler)
|
|
34
|
+
return () => {
|
|
35
|
+
const set = nodeActionHandlers.get(name)
|
|
36
|
+
if (!set) return
|
|
37
|
+
set.delete(handler)
|
|
38
|
+
if (!set.size) nodeActionHandlers.delete(name)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} name action 名
|
|
43
|
+
* @param {unknown} payload 载荷
|
|
44
|
+
* @param {string} peerId 目标 peer
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
send(name, payload, peerId) {
|
|
48
|
+
if (!peerId) return
|
|
49
|
+
void sendToNodeLink(peerId, { scope: 'node', action: name, payload }).catch(() => { })
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} action - action 名
|
|
56
|
+
* @returns {boolean} 是否已挂载处理器
|
|
57
|
+
*/
|
|
58
|
+
export function hasNodeScopeAction(action) {
|
|
59
|
+
return (nodeActionHandlers.get(action)?.size ?? 0) > 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} action - action 名
|
|
64
|
+
* @returns {number} 已注册的处理器数量
|
|
65
|
+
*/
|
|
66
|
+
export function countNodeScopeActionHandlers(action) {
|
|
67
|
+
return nodeActionHandlers.get(action)?.size ?? 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 测试/调试:直接派发已挂载的 node action。
|
|
72
|
+
* @param {string} action - action 名
|
|
73
|
+
* @param {unknown} payload - 载荷
|
|
74
|
+
* @param {string} peerId - 发送方 nodeHash
|
|
75
|
+
* @returns {boolean} 是否有处理器被调用
|
|
76
|
+
*/
|
|
77
|
+
export function dispatchNodeScopeAction(action, payload, peerId) {
|
|
78
|
+
const handlers = nodeActionHandlers.get(action)
|
|
79
|
+
if (!handlers?.size) return false
|
|
80
|
+
for (const handler of handlers)
|
|
81
|
+
try { handler(payload, peerId) } catch { /* ignore */ }
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @returns {NodeScopeWire | null} 当前 wire,未 ensure 时为 null
|
|
87
|
+
*/
|
|
88
|
+
export function getNodeScopeWire() {
|
|
89
|
+
return nodeScopeWire
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @returns {NodeScopeContext} 可变 node scope 上下文
|
|
94
|
+
*/
|
|
95
|
+
export function getNodeScopeContext() {
|
|
96
|
+
return nodeScopeContext
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 只订阅 node scope 派发,不挂任何 feature。
|
|
101
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
102
|
+
* @returns {() => void} 取消订阅的 dispose
|
|
103
|
+
*/
|
|
104
|
+
export function ensureNodeScope(options = {}) {
|
|
105
|
+
if (options.replicaUsername != null)
|
|
106
|
+
nodeScopeContext.replicaUsername = String(options.replicaUsername)
|
|
107
|
+
if (nodeScopeSubscribeCleanup) return nodeScopeSubscribeCleanup
|
|
108
|
+
nodeScopeSubscribeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
|
|
109
|
+
const handlers = nodeActionHandlers.get(envelope.action)
|
|
110
|
+
if (!handlers?.size) return
|
|
111
|
+
for (const handler of handlers)
|
|
112
|
+
try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
|
|
113
|
+
})
|
|
114
|
+
if (!nodeScopeWire) {
|
|
115
|
+
nodeScopeWire = createNodeScopeWire()
|
|
116
|
+
for (const hook of nodeScopeWireHooks)
|
|
117
|
+
try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
|
|
118
|
+
}
|
|
119
|
+
return nodeScopeSubscribeCleanup
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 在 node scope wire 创建时(或已存在时立即)回调;shell 用此挂自定义 action。
|
|
124
|
+
* @param {(context: NodeScopeContext, wire: NodeScopeWire) => void} hook - wire 就绪回调
|
|
125
|
+
* @returns {() => void} 取消注册的 dispose
|
|
126
|
+
*/
|
|
127
|
+
export function registerNodeScopeWireHook(hook) {
|
|
128
|
+
nodeScopeWireHooks.add(hook)
|
|
129
|
+
if (nodeScopeWire)
|
|
130
|
+
try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
|
|
131
|
+
return () => nodeScopeWireHooks.delete(hook)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 卸掉 scope 订阅与 wire(feature 须先卸)。
|
|
136
|
+
* @returns {void}
|
|
137
|
+
*/
|
|
138
|
+
export function clearNodeScopeSubscribe() {
|
|
139
|
+
nodeScopeSubscribeCleanup?.()
|
|
140
|
+
nodeScopeSubscribeCleanup = null
|
|
141
|
+
nodeScopeWire = null
|
|
142
|
+
nodeActionHandlers.clear()
|
|
143
|
+
}
|
|
@@ -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
|
package/transport/user_room.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 {
|
|
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(用 `
|
|
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 =
|
|
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 =
|
|
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 {
|
|
119
|
+
* @returns {Promise<boolean>} 是否发出
|
|
129
120
|
*/
|
|
130
121
|
sendToPeer(peerId, actionName, payload) {
|
|
131
|
-
|
|
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
|
-
|
|
157
|
+
if (fanoutLimit <= 0) return 0
|
|
158
|
+
const slot = await ensureUserRoom({ replicaUsername: username })
|
|
169
159
|
let sent = 0
|
|
170
|
-
const peers = shuffleInPlace(
|
|
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
|
|
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'
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 有界 pending fetch 等待表(chunk / manifest 等入站响应槽)。
|
|
3
|
+
* @template T
|
|
4
|
+
* @param {{ maxSize: number }} options 容量
|
|
5
|
+
* @returns {{
|
|
6
|
+
* pending: Map<string, { expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } }>,
|
|
7
|
+
* register: (key: string, expectedKey: string, timeoutMs: number, options?: { rejectOnTimeout?: boolean }) => { done: Promise<T | null>, cancel: () => void },
|
|
8
|
+
* peek: (key: string) => { expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } } | undefined,
|
|
9
|
+
* settle: (key: string, value: T | null | Error) => boolean,
|
|
10
|
+
* }} 等待表 API
|
|
11
|
+
*/
|
|
12
|
+
export function createFetchWaitTable({ maxSize }) {
|
|
13
|
+
/** @type {Map<string, { expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } }>} */
|
|
14
|
+
const pending = new Map()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} key 等待键
|
|
18
|
+
* @param {T | null | Error} value 结果或错误
|
|
19
|
+
* @returns {boolean} 是否命中并完成
|
|
20
|
+
*/
|
|
21
|
+
function settle(key, value) {
|
|
22
|
+
const entry = pending.get(key)
|
|
23
|
+
if (!entry) return false
|
|
24
|
+
clearTimeout(entry.timer)
|
|
25
|
+
pending.delete(key)
|
|
26
|
+
entry.finish(value)
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
pending,
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} key 唯一等待键
|
|
35
|
+
* @param {string} expectedKey 期望匹配键
|
|
36
|
+
* @param {number} timeoutMs 超时
|
|
37
|
+
* @param {{ rejectOnTimeout?: boolean }} [options] 超时是否 reject
|
|
38
|
+
* @returns {{ done: Promise<T | null>, cancel: () => void }} 等待句柄
|
|
39
|
+
*/
|
|
40
|
+
register(key, expectedKey, timeoutMs, options = {}) {
|
|
41
|
+
if (!key)
|
|
42
|
+
return {
|
|
43
|
+
done: Promise.resolve(null),
|
|
44
|
+
/** @returns {void} */
|
|
45
|
+
cancel: () => { },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const existing = pending.get(key)
|
|
49
|
+
if (existing) return existing.handle
|
|
50
|
+
|
|
51
|
+
if (pending.size >= maxSize)
|
|
52
|
+
return {
|
|
53
|
+
done: Promise.resolve(null),
|
|
54
|
+
/** @returns {void} */
|
|
55
|
+
cancel: () => { },
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @type {(value: T | null | Error) => void} */
|
|
59
|
+
let finish
|
|
60
|
+
const done = new Promise((resolve, reject) => {
|
|
61
|
+
/**
|
|
62
|
+
* @param {T | null | Error} value 完成值或错误
|
|
63
|
+
* @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
finish = value => {
|
|
66
|
+
if (value instanceof Error) reject(value)
|
|
67
|
+
else resolve(value)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
pending.delete(key)
|
|
73
|
+
if (options.rejectOnTimeout) finish(new Error('fetch timeout'))
|
|
74
|
+
else finish(null)
|
|
75
|
+
}, timeoutMs)
|
|
76
|
+
|
|
77
|
+
/** @type {{ done: Promise<T | null>, cancel: () => void }} */
|
|
78
|
+
const handle = {
|
|
79
|
+
done,
|
|
80
|
+
/** @returns {void} 取消等待 */
|
|
81
|
+
cancel: () => settle(key, null),
|
|
82
|
+
}
|
|
83
|
+
pending.set(key, { expectedKey, timer, finish, handle })
|
|
84
|
+
return handle
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 只读查看,不移除。
|
|
89
|
+
* @param {string} key 等待键
|
|
90
|
+
* @returns {{ expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } } | undefined} 等待条目
|
|
91
|
+
*/
|
|
92
|
+
peek(key) {
|
|
93
|
+
return pending.get(key)
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
settle,
|
|
97
|
+
}
|
|
98
|
+
}
|
package/wire/adapter.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void
|
|
4
|
+
* send: (name: string, payload: unknown, peerId: string | null) => void
|
|
5
|
+
* }} WireAdapter
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ replicaUsername?: string }} WireContext
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Wire 适配器与上下文的类型导出(运行时代码见各 wire 模块)。 */
|
|
13
|
+
export {}
|
package/wire/ingress.mjs
CHANGED
|
@@ -4,14 +4,7 @@
|
|
|
4
4
|
import { Buffer } from 'node:buffer'
|
|
5
5
|
|
|
6
6
|
import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {unknown} value 待判定值
|
|
10
|
-
* @returns {value is Record<string, unknown>} 是否为非 null 的普通对象(非数组)
|
|
11
|
-
*/
|
|
12
|
-
export function isPlainObject(value) {
|
|
13
|
-
return value != null && !Array.isArray(value) && typeof value === 'object'
|
|
14
|
-
}
|
|
7
|
+
import { isPlainObject } from '../core/object.mjs'
|
|
15
8
|
|
|
16
9
|
/**
|
|
17
10
|
* 解析 WebSocket / Trystero 入站 JSON 帧。
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { getNodeHash } from '../../node/identity.mjs'
|
|
4
|
+
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../../trust_graph/registry.mjs'
|
|
5
|
+
|
|
6
|
+
import { unwrapPartInvokeResult } from './invoke.mjs'
|
|
7
|
+
import { pendingPartInvoke } from './pending.mjs'
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('./invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
10
|
+
/** @typedef {import('./invoke.mjs').PartInvoke} PartInvoke */
|
|
11
|
+
|
|
12
|
+
/** 时间线 part_timeline_put fanout 上限 */
|
|
13
|
+
export const TIMELINE_FANOUT_LIMIT = 8
|
|
14
|
+
/** part_invoke RPC collect 默认响应数 */
|
|
15
|
+
export const PART_INVOKE_FANOUT_DEFAULT = 6
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} partpath part 路径
|
|
19
|
+
* @param {PartInvoke} invoke 调用体
|
|
20
|
+
* @param {string} [nodeHash] 来源节点
|
|
21
|
+
* @param {string} [requestId] RPC 请求 id
|
|
22
|
+
* @returns {object} part_invoke 线载荷
|
|
23
|
+
*/
|
|
24
|
+
function buildPartInvokePayload(partpath, invoke, nodeHash, requestId) {
|
|
25
|
+
return {
|
|
26
|
+
partpath,
|
|
27
|
+
invoke,
|
|
28
|
+
...nodeHash ? { nodeHash } : {},
|
|
29
|
+
...requestId ? { requestId } : {},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {PartInvokeResponse[]} results collect 原始结果
|
|
35
|
+
* @returns {object[]} 仅含成功 result 的载荷
|
|
36
|
+
*/
|
|
37
|
+
export function partInvokeDataRows(results) {
|
|
38
|
+
/** @type {object[]} */
|
|
39
|
+
const rows = []
|
|
40
|
+
for (const row of results) {
|
|
41
|
+
const data = unwrapPartInvokeResult(row)
|
|
42
|
+
if (data != null) rows.push(/** @type {object} */ data)
|
|
43
|
+
}
|
|
44
|
+
return rows
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {PartInvokeResponse[]} results collect 原始结果
|
|
49
|
+
* @returns {string[]} 邻居返回的错误信息
|
|
50
|
+
*/
|
|
51
|
+
export function partInvokeErrorMessages(results) {
|
|
52
|
+
/** @type {string[]} */
|
|
53
|
+
const errors = []
|
|
54
|
+
for (const row of results) {
|
|
55
|
+
const message = row?.error?.message
|
|
56
|
+
if (message) errors.push(message)
|
|
57
|
+
}
|
|
58
|
+
return errors
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 向 trust-graph top-K 扇出 part_invoke,收集 part_invoke_response。
|
|
63
|
+
* 调用方须已挂载 `attachPartWire`(否则收不到 response)。
|
|
64
|
+
* @param {string} username trust graph 上下文
|
|
65
|
+
* @param {string} partpath part 路径
|
|
66
|
+
* @param {PartInvoke} invoke 调用体
|
|
67
|
+
* @param {number} [timeoutMs=2500] 超时
|
|
68
|
+
* @param {number} [maxResponses=6] 最多响应数
|
|
69
|
+
* @returns {Promise<PartInvokeResponse[]>} 邻居 PartInvokeResponse(含 error)
|
|
70
|
+
*/
|
|
71
|
+
export async function collectPartInvokeResponses(username, partpath, invoke, timeoutMs = 2500, maxResponses = PART_INVOKE_FANOUT_DEFAULT) {
|
|
72
|
+
const requestId = randomUUID()
|
|
73
|
+
const nodeHash = getNodeHash()
|
|
74
|
+
/** @type {PartInvokeResponse[]} */
|
|
75
|
+
const responses = []
|
|
76
|
+
|
|
77
|
+
const waitForResponses = new Promise(resolve => {
|
|
78
|
+
/** 超时或收满响应后清理 pending 并完成 Promise。 */
|
|
79
|
+
const finish = () => {
|
|
80
|
+
clearTimeout(timer)
|
|
81
|
+
pendingPartInvoke.delete(requestId)
|
|
82
|
+
resolve(responses)
|
|
83
|
+
}
|
|
84
|
+
const timer = setTimeout(finish, timeoutMs)
|
|
85
|
+
pendingPartInvoke.set(requestId, { responses, finish, maxResponses, respondedPeers: new Set() })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const sent = await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(
|
|
89
|
+
username,
|
|
90
|
+
'part_invoke',
|
|
91
|
+
buildPartInvokePayload(partpath, invoke, nodeHash, requestId),
|
|
92
|
+
maxResponses,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const pending = pendingPartInvoke.get(requestId)
|
|
96
|
+
if (pending && sent === 0) pending.finish()
|
|
97
|
+
|
|
98
|
+
return waitForResponses
|
|
99
|
+
}
|