@steve02081504/fount-p2p 0.0.0
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 +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** @typedef {{ query?: { locales?: string | string[] } }} ExpressLikeRequest */
|
|
2
|
+
|
|
3
|
+
/** @type {((replicaUsername: string, entityHash: string, locales: string[]) => Promise<object>) | null} */
|
|
4
|
+
let infoDefaultsProvider = null
|
|
5
|
+
|
|
6
|
+
/** @type {((req: ExpressLikeRequest, replicaUsername: string) => string[]) | null} */
|
|
7
|
+
let localesFromRequestProvider = null
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {(replicaUsername: string, entityHash: string, locales: string[]) => Promise<object>} provider 资料默认值解析器
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
export function registerEntityPresentationProvider(provider) {
|
|
14
|
+
infoDefaultsProvider = provider
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {(req: ExpressLikeRequest, replicaUsername: string) => string[]} provider 请求区域设置解析器
|
|
19
|
+
* @returns {void}
|
|
20
|
+
*/
|
|
21
|
+
export function registerLocalesFromRequestProvider(provider) {
|
|
22
|
+
localesFromRequestProvider = provider
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} replicaUsername 查看者 replica
|
|
27
|
+
* @param {string} entityHash 目标实体
|
|
28
|
+
* @param {string[]} locales 区域设置优先级
|
|
29
|
+
* @returns {Promise<object | null>} 展示默认值或 null
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveInfoDefaultsForEntity(replicaUsername, entityHash, locales) {
|
|
32
|
+
if (!infoDefaultsProvider) return null
|
|
33
|
+
return infoDefaultsProvider(replicaUsername, entityHash, locales)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {ExpressLikeRequest} req HTTP 请求
|
|
38
|
+
* @param {string} replicaUsername 查看者 replica
|
|
39
|
+
* @returns {string[]} 区域设置列表
|
|
40
|
+
*/
|
|
41
|
+
export function localesFromRequest(req, replicaUsername) {
|
|
42
|
+
if (localesFromRequestProvider) return localesFromRequestProvider(req, replicaUsername)
|
|
43
|
+
return ['zh-CN', 'en-UK']
|
|
44
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
2
|
+
import { getEntityStore } from '../node/instance.mjs'
|
|
3
|
+
|
|
4
|
+
import { profileAvatarFileUrl } from './files/url.mjs'
|
|
5
|
+
import {
|
|
6
|
+
applyAvatarToAllLocales,
|
|
7
|
+
normalizeLocalizedMap,
|
|
8
|
+
resolveProfilePresentation,
|
|
9
|
+
} from './localized_core.mjs'
|
|
10
|
+
import { resolveInfoDefaultsForEntity } from './presentation_registry.mjs'
|
|
11
|
+
import { isWritableLocalEntity } from './replica.mjs'
|
|
12
|
+
|
|
13
|
+
/** 超过该毫秒未心跳则视为离线 */
|
|
14
|
+
const HEARTBEAT_STALE_MS = 120_000
|
|
15
|
+
|
|
16
|
+
const MANUAL_STATUSES = new Set(['online', 'idle', 'dnd', 'invisible', 'away', 'busy', 'offline'])
|
|
17
|
+
const PROFILE_JSON = 'profile.json'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} entityHash 128 位 entityHash
|
|
21
|
+
* @param {{ nodeHash: string, subjectHash: string }} parsed parseEntityHash 结果
|
|
22
|
+
* @returns {object} 默认资料
|
|
23
|
+
*/
|
|
24
|
+
function getDefaultProfile(entityHash, parsed) {
|
|
25
|
+
return {
|
|
26
|
+
entityHash,
|
|
27
|
+
nodeHash: parsed.nodeHash,
|
|
28
|
+
subjectHash: parsed.subjectHash,
|
|
29
|
+
localized: {},
|
|
30
|
+
status: 'online',
|
|
31
|
+
customStatus: '',
|
|
32
|
+
lastSeenAt: 0,
|
|
33
|
+
stats: {
|
|
34
|
+
joinedAt: Date.now(),
|
|
35
|
+
messageCount: 0,
|
|
36
|
+
groupCount: 0,
|
|
37
|
+
channelCount: 0,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} profileData 原始对象
|
|
44
|
+
* @returns {object} 可写入磁盘的资料
|
|
45
|
+
*/
|
|
46
|
+
function toStoredProfile(profileData) {
|
|
47
|
+
return {
|
|
48
|
+
entityHash: profileData.entityHash,
|
|
49
|
+
nodeHash: profileData.nodeHash,
|
|
50
|
+
subjectHash: profileData.subjectHash,
|
|
51
|
+
localized: normalizeLocalizedMap(profileData.localized),
|
|
52
|
+
status: profileData.status || 'online',
|
|
53
|
+
customStatus: String(profileData.customStatus || '').trim(),
|
|
54
|
+
lastSeenAt: profileData.lastSeenAt || 0,
|
|
55
|
+
stats: {
|
|
56
|
+
joinedAt: profileData.stats?.joinedAt || Date.now(),
|
|
57
|
+
messageCount: profileData.stats?.messageCount || 0,
|
|
58
|
+
groupCount: profileData.stats?.groupCount || 0,
|
|
59
|
+
channelCount: profileData.stats?.channelCount || 0,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} profile 用户资料
|
|
66
|
+
* @param {string} [viewerEntityHash] 查看者 entityHash
|
|
67
|
+
* @param {{ isSelf?: boolean }} [options] isSelf 为 true 时隐身对本人可见
|
|
68
|
+
* @returns {string} 有效状态
|
|
69
|
+
*/
|
|
70
|
+
export function computeEffectiveStatus(profile, viewerEntityHash, options = {}) {
|
|
71
|
+
const stored = String(profile?.status || 'online')
|
|
72
|
+
const isSelf = options.isSelf
|
|
73
|
+
?? (viewerEntityHash && profile?.entityHash === viewerEntityHash)
|
|
74
|
+
const lastSeen = profile?.lastSeenAt || 0
|
|
75
|
+
const recentlySeen = lastSeen > 0 && Date.now() - lastSeen < HEARTBEAT_STALE_MS
|
|
76
|
+
|
|
77
|
+
if (stored === 'invisible')
|
|
78
|
+
return isSelf ? 'invisible' : 'offline'
|
|
79
|
+
|
|
80
|
+
if (!recentlySeen)
|
|
81
|
+
return 'offline'
|
|
82
|
+
|
|
83
|
+
return stored
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} entityHash 128 位 entityHash
|
|
88
|
+
* @param {string | null} [replicaUsername] 展示默认字段用
|
|
89
|
+
* @param {{ groupId?: string, skipPresentation?: boolean, locales?: string[], infoDefaults?: object }} [options] 选项
|
|
90
|
+
* @returns {Promise<object>} 资料对象
|
|
91
|
+
*/
|
|
92
|
+
export async function getProfile(entityHash, replicaUsername = null, options = {}) {
|
|
93
|
+
const parsed = parseEntityHash(entityHash)
|
|
94
|
+
if (!parsed) throw new Error('invalid entityHash')
|
|
95
|
+
|
|
96
|
+
const store = getEntityStore()
|
|
97
|
+
const defaultProfile = getDefaultProfile(parsed.entityHash, parsed)
|
|
98
|
+
let stored = defaultProfile
|
|
99
|
+
|
|
100
|
+
const onDisk = await store.readEntityJson(parsed.entityHash, PROFILE_JSON)
|
|
101
|
+
if (onDisk)
|
|
102
|
+
stored = toStoredProfile({ ...defaultProfile, ...onDisk })
|
|
103
|
+
else if (isWritableLocalEntity(parsed.entityHash))
|
|
104
|
+
await store.writeEntityJson(parsed.entityHash, PROFILE_JSON, stored)
|
|
105
|
+
|
|
106
|
+
const locales = options.locales || ['zh-CN', 'en-UK']
|
|
107
|
+
const merged = {
|
|
108
|
+
...stored,
|
|
109
|
+
entityHash: parsed.entityHash,
|
|
110
|
+
nodeHash: parsed.nodeHash,
|
|
111
|
+
subjectHash: parsed.subjectHash,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.skipPresentation) return merged
|
|
115
|
+
|
|
116
|
+
let infoDefaults = options.infoDefaults
|
|
117
|
+
if (!infoDefaults && replicaUsername)
|
|
118
|
+
infoDefaults = await resolveInfoDefaultsForEntity(replicaUsername, parsed.entityHash, locales)
|
|
119
|
+
if (!infoDefaults)
|
|
120
|
+
infoDefaults = { name: `${parsed.subjectHash.slice(0, 8)}…${parsed.subjectHash.slice(-4)}`, avatar: '', description: '', description_markdown: '', version: '', author: '', home_page: '', issue_page: '', tags: [], links: [] }
|
|
121
|
+
|
|
122
|
+
const resolved = resolveProfilePresentation(merged, locales, infoDefaults)
|
|
123
|
+
return {
|
|
124
|
+
...merged,
|
|
125
|
+
...resolved,
|
|
126
|
+
infoDefaults,
|
|
127
|
+
localeKeys: Object.keys(merged.localized),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} replicaUsername 副本用户名 所有者
|
|
133
|
+
* @param {string} entityHash 128 位 entityHash
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
export async function recordHeartbeat(replicaUsername, entityHash) {
|
|
137
|
+
void replicaUsername
|
|
138
|
+
const profile = await getProfile(entityHash, null, { skipPresentation: true })
|
|
139
|
+
profile.lastSeenAt = Date.now()
|
|
140
|
+
await getEntityStore().writeEntityJson(entityHash, PROFILE_JSON, toStoredProfile(profile))
|
|
141
|
+
return { lastSeenAt: profile.lastSeenAt }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {string} replicaUsername 副本用户名 所有者
|
|
146
|
+
* @param {string} entityHash 128 位 entityHash
|
|
147
|
+
* @param {object} updates 更新内容
|
|
148
|
+
* @param {{ groupId?: string, skipPresentation?: boolean, locales?: string[] }} [options] 选项
|
|
149
|
+
* @returns {Promise<object>} 更新后的资料
|
|
150
|
+
*/
|
|
151
|
+
export async function updateProfile(replicaUsername, entityHash, updates, options = {}) {
|
|
152
|
+
if (!isWritableLocalEntity(entityHash))
|
|
153
|
+
throw new Error('entity not writable on this replica')
|
|
154
|
+
|
|
155
|
+
const profile = await getProfile(entityHash, replicaUsername, {
|
|
156
|
+
groupId: options.groupId,
|
|
157
|
+
skipPresentation: true,
|
|
158
|
+
})
|
|
159
|
+
const parsed = parseEntityHash(entityHash)
|
|
160
|
+
|
|
161
|
+
const localized = updates.localized != null
|
|
162
|
+
? normalizeLocalizedMap(updates.localized)
|
|
163
|
+
: profile.localized
|
|
164
|
+
|
|
165
|
+
const updatedProfile = toStoredProfile({
|
|
166
|
+
...profile,
|
|
167
|
+
entityHash: parsed.entityHash,
|
|
168
|
+
nodeHash: parsed.nodeHash,
|
|
169
|
+
subjectHash: parsed.subjectHash,
|
|
170
|
+
localized,
|
|
171
|
+
status: updates.status != null ? updates.status : profile.status,
|
|
172
|
+
customStatus: updates.customStatus != null ? updates.customStatus : profile.customStatus,
|
|
173
|
+
lastSeenAt: updates.lastSeenAt != null ? updates.lastSeenAt : profile.lastSeenAt,
|
|
174
|
+
stats: updates.stats ? { ...profile.stats, ...updates.stats } : profile.stats,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await getEntityStore().writeEntityJson(entityHash, PROFILE_JSON, updatedProfile)
|
|
178
|
+
if (options.skipPresentation) return updatedProfile
|
|
179
|
+
const locales = options.locales || ['zh-CN', 'en-UK']
|
|
180
|
+
const infoDefaults = await resolveInfoDefaultsForEntity(replicaUsername, entityHash, locales)
|
|
181
|
+
const resolved = resolveProfilePresentation(updatedProfile, locales, infoDefaults)
|
|
182
|
+
return { ...updatedProfile, ...resolved, infoDefaults, localeKeys: Object.keys(updatedProfile.localized) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} replicaUsername 副本用户名 所有者
|
|
187
|
+
* @param {string} entityHash 128 位 entityHash
|
|
188
|
+
* @param {Buffer} fileBuffer 文件缓冲区
|
|
189
|
+
* @param {string} filename 文件名
|
|
190
|
+
* @returns {Promise<string>} 头像 URL
|
|
191
|
+
*/
|
|
192
|
+
export async function uploadAvatar(replicaUsername, entityHash, fileBuffer, filename) {
|
|
193
|
+
if (!isWritableLocalEntity(entityHash))
|
|
194
|
+
throw new Error('entity not writable on this replica')
|
|
195
|
+
|
|
196
|
+
const { putFileManifest } = await import('./files/evfs.mjs')
|
|
197
|
+
await putFileManifest({
|
|
198
|
+
ownerEntityHash: entityHash,
|
|
199
|
+
logicalPath: 'profile/avatar',
|
|
200
|
+
plaintext: fileBuffer,
|
|
201
|
+
name: filename || 'avatar',
|
|
202
|
+
mimeType: 'image/png',
|
|
203
|
+
ceMode: 'convergent',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const avatarUrl = profileAvatarFileUrl(entityHash)
|
|
207
|
+
const profile = await getProfile(entityHash, replicaUsername, { skipPresentation: true })
|
|
208
|
+
await updateProfile(replicaUsername, entityHash, {
|
|
209
|
+
localized: applyAvatarToAllLocales(profile.localized, avatarUrl),
|
|
210
|
+
}, { skipPresentation: true })
|
|
211
|
+
return avatarUrl
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {string} entityHash 128 位 entityHash
|
|
216
|
+
* @returns {Promise<object>} 统计字段
|
|
217
|
+
*/
|
|
218
|
+
export async function getStats(entityHash) {
|
|
219
|
+
const profile = await getProfile(entityHash)
|
|
220
|
+
return profile.stats
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} replicaUsername 副本用户名 所有者
|
|
225
|
+
* @param {string} entityHash 128 位 entityHash
|
|
226
|
+
* @param {string} status 状态
|
|
227
|
+
* @param {string} [customStatus] 自定义状态
|
|
228
|
+
* @returns {Promise<{ status: string, customStatus: string, lastSeenAt: number }>} 更新后的状态字段
|
|
229
|
+
*/
|
|
230
|
+
export async function updateStatus(replicaUsername, entityHash, status, customStatus = '') {
|
|
231
|
+
if (!MANUAL_STATUSES.has(status))
|
|
232
|
+
throw new Error('invalid status')
|
|
233
|
+
const updated = await updateProfile(replicaUsername, entityHash, {
|
|
234
|
+
status,
|
|
235
|
+
customStatus,
|
|
236
|
+
lastSeenAt: Date.now(),
|
|
237
|
+
}, { skipPresentation: true })
|
|
238
|
+
return {
|
|
239
|
+
status: updated.status,
|
|
240
|
+
customStatus: updated.customStatus,
|
|
241
|
+
lastSeenAt: updated.lastSeenAt,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 确保本节点操作者实体目录存在。
|
|
247
|
+
* @param {string} replicaUsername 副本用户名 所有者
|
|
248
|
+
* @param {string} entityHash 128 位 entityHash
|
|
249
|
+
* @returns {Promise<object>} 本节点实体资料
|
|
250
|
+
*/
|
|
251
|
+
export async function ensureLocalEntityProfile(replicaUsername, entityHash) {
|
|
252
|
+
void replicaUsername
|
|
253
|
+
if (!isWritableLocalEntity(entityHash))
|
|
254
|
+
throw new Error('entity not on local node')
|
|
255
|
+
return getProfile(entityHash, replicaUsername, { skipPresentation: true })
|
|
256
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
2
|
+
import { operatorEntityHashFromKeys, getNodeHash } from '../node/identity.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
|
|
6
|
+
* @returns {string | null} 本节点操作者 entityHash
|
|
7
|
+
*/
|
|
8
|
+
export function resolveLocalOperatorEntityHash(recoveryPubKeyHex) {
|
|
9
|
+
return operatorEntityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} entityHash 目标 entityHash
|
|
14
|
+
* @returns {boolean} 是否为本节点可写实体
|
|
15
|
+
*/
|
|
16
|
+
export function isWritableLocalEntity(entityHash) {
|
|
17
|
+
const parsed = parseEntityHash(entityHash)
|
|
18
|
+
if (!parsed) return false
|
|
19
|
+
return parsed.nodeHash === getNodeHash()
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @type {Map<string, (replicaUsername: string, groupId: string) => Promise<object | null>>} */
|
|
2
|
+
const providersByOwner = new Map()
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} ownerId 注册方
|
|
6
|
+
* @param {(replicaUsername: string, groupId: string) => Promise<object | null>} provider 物化会话
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export function registerMaterializedSessionProvider(ownerId, provider) {
|
|
10
|
+
providersByOwner.set(String(ownerId), provider)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} ownerId 注册方
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function unregisterMaterializedSessionProvider(ownerId) {
|
|
18
|
+
providersByOwner.delete(String(ownerId))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} replicaUsername 副本用户名
|
|
23
|
+
* @param {string} groupId 群 ID
|
|
24
|
+
* @param {string} [ownerId] 注册方 id;省略时遍历所有 provider
|
|
25
|
+
* @returns {Promise<object | null>} 物化会话
|
|
26
|
+
*/
|
|
27
|
+
export async function getMaterializedSession(replicaUsername, groupId, ownerId) {
|
|
28
|
+
if (ownerId != null) {
|
|
29
|
+
const provider = providersByOwner.get(String(ownerId))
|
|
30
|
+
if (!provider) return null
|
|
31
|
+
return provider(replicaUsername, groupId)
|
|
32
|
+
}
|
|
33
|
+
for (const provider of providersByOwner.values()) {
|
|
34
|
+
const session = await provider(replicaUsername, groupId)
|
|
35
|
+
if (session) return session
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { b64ToU8 } from '../core/bytes_codec.mjs'
|
|
2
|
+
import { verifiedChunkBytes } from '../files/chunk_fetch_verify.mjs'
|
|
3
|
+
|
|
4
|
+
/** @type {Map<string, { expectedHash: string, timer: ReturnType<typeof setTimeout>, resolve: (v: Uint8Array | null) => void, reject?: (e: Error) => void }>} */
|
|
5
|
+
export const pendingChunkFetches = new Map()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export const MAX_PENDING_CHUNK_FETCHES = 2048
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 注册 chunk 拉取等待槽(requestId 或 compositeKey)。
|
|
14
|
+
* @param {string} key 唯一等待键
|
|
15
|
+
* @param {string} expectedHash 期望 64 hex 密文哈希
|
|
16
|
+
* @param {number} timeoutMs 超时毫秒
|
|
17
|
+
* @param {{ rejectOnTimeout?: boolean }} [opts] rejectOnTimeout 时 Promise 以 Error 拒绝
|
|
18
|
+
* @returns {{ done: Promise<Uint8Array | null>, cancel: () => void }} 等待 Promise 与取消函数
|
|
19
|
+
*/
|
|
20
|
+
export function registerChunkFetchWait(key, expectedHash, timeoutMs, opts = {}) {
|
|
21
|
+
if (!key || pendingChunkFetches.size >= MAX_PENDING_CHUNK_FETCHES)
|
|
22
|
+
return {
|
|
23
|
+
done: Promise.resolve(null), /**
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
cancel: () => { }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @type {(value: Uint8Array | null | Error) => void} */
|
|
30
|
+
let settle
|
|
31
|
+
const done = new Promise((resolve, reject) => {
|
|
32
|
+
/**
|
|
33
|
+
* @param {Uint8Array | null | Error} value 块数据或错误
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
function settleWait(value) {
|
|
37
|
+
if (value instanceof Error) reject(value)
|
|
38
|
+
else resolve(value)
|
|
39
|
+
}
|
|
40
|
+
settle = settleWait
|
|
41
|
+
})
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
pendingChunkFetches.delete(key)
|
|
44
|
+
if (opts.rejectOnTimeout)
|
|
45
|
+
settle(new Error('chunk fetch timeout'))
|
|
46
|
+
else
|
|
47
|
+
settle(null)
|
|
48
|
+
}, timeoutMs)
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {Uint8Array | null} data 块数据
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
function finish(data) {
|
|
55
|
+
clearTimeout(timer)
|
|
56
|
+
pendingChunkFetches.delete(key)
|
|
57
|
+
settle(data)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {Error | unknown} err 拒绝原因
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
function rejectWait(err) {
|
|
65
|
+
finish(err instanceof Error ? err : new Error(String(err)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pendingChunkFetches.set(key, {
|
|
69
|
+
expectedHash,
|
|
70
|
+
timer,
|
|
71
|
+
resolve: finish,
|
|
72
|
+
reject: rejectWait,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
done,
|
|
77
|
+
/**
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
cancel: () => {
|
|
81
|
+
clearTimeout(timer)
|
|
82
|
+
pendingChunkFetches.delete(key)
|
|
83
|
+
settle(null)
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 按等待键解析入站 chunk 响应(校验哈希后 resolve)。
|
|
90
|
+
* @param {string} key 等待键
|
|
91
|
+
* @param {string} expectedHash 期望哈希
|
|
92
|
+
* @param {Uint8Array | null} bytes 密文块
|
|
93
|
+
* @returns {boolean} 是否命中并完成等待
|
|
94
|
+
*/
|
|
95
|
+
export function resolveChunkFetchWait(key, expectedHash, bytes) {
|
|
96
|
+
const entry = pendingChunkFetches.get(key)
|
|
97
|
+
if (!entry || entry.expectedHash !== expectedHash) return false
|
|
98
|
+
const verified = bytes ? verifiedChunkBytes(expectedHash, bytes) : null
|
|
99
|
+
if (bytes && !verified) return false
|
|
100
|
+
clearTimeout(entry.timer)
|
|
101
|
+
pendingChunkFetches.delete(key)
|
|
102
|
+
entry.resolve(verified)
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 处理 fed_chunk_data / 带 requestId 的响应载荷。
|
|
108
|
+
* @param {object} payload 入站载荷
|
|
109
|
+
* @returns {boolean} 是否命中 pending
|
|
110
|
+
*/
|
|
111
|
+
export function resolvePendingChunkFetch(payload) {
|
|
112
|
+
const requestId = String(payload?.requestId || '')
|
|
113
|
+
if (!requestId) return false
|
|
114
|
+
const entry = pendingChunkFetches.get(requestId)
|
|
115
|
+
if (!entry) return false
|
|
116
|
+
if (payload?.dataB64) {
|
|
117
|
+
try {
|
|
118
|
+
const bytes = b64ToU8(String(payload.dataB64))
|
|
119
|
+
return resolveChunkFetchWait(requestId, entry.expectedHash, bytes)
|
|
120
|
+
}
|
|
121
|
+
catch { /* keep waiting */ }
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
clearTimeout(entry.timer)
|
|
125
|
+
pendingChunkFetches.delete(requestId)
|
|
126
|
+
entry.resolve(null)
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 联邦分块拉取调度:按 peer 轮转分配缺失块,支持超时重试与广播兜底。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @typedef {'pending' | 'inflight' | 'done' | 'failed'} ChunkFetchState */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string[]} chunkHashes 缺失块哈希(有序)
|
|
9
|
+
* @param {string[]} peerIds 可用 peerId 列表
|
|
10
|
+
* @returns {Map<string, string>} chunkHash → 首选 peerId
|
|
11
|
+
*/
|
|
12
|
+
export function assignChunksToPeers(chunkHashes, peerIds) {
|
|
13
|
+
/** @type {Map<string, string>} */
|
|
14
|
+
const out = new Map()
|
|
15
|
+
if (!peerIds.length) return out
|
|
16
|
+
const list = peerIds.filter(Boolean)
|
|
17
|
+
for (let i = 0; i < chunkHashes.length; i++)
|
|
18
|
+
out.set(chunkHashes[i], list[i % list.length])
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
|
|
24
|
+
* @param {string[]} chunkHashes 待拉取块
|
|
25
|
+
* @param {string[]} peerIds 可用 peer
|
|
26
|
+
* @param {{ maxAttempts?: number }} [opts] 选项
|
|
27
|
+
* @returns {{ assignments: Map<string, string>, broadcast: string[] }} 分配与需广播块
|
|
28
|
+
*/
|
|
29
|
+
export function planChunkFetches(table, chunkHashes, peerIds, opts = {}) {
|
|
30
|
+
const maxAttempts = Math.max(1, Number(opts.maxAttempts) || 3)
|
|
31
|
+
const assignments = assignChunksToPeers(chunkHashes, peerIds)
|
|
32
|
+
/** @type {string[]} */
|
|
33
|
+
const broadcast = []
|
|
34
|
+
for (const hash of chunkHashes) {
|
|
35
|
+
const row = table.get(hash) || { state: 'pending', attempts: 0 }
|
|
36
|
+
if (row.state === 'done') continue
|
|
37
|
+
if (row.state === 'inflight') continue
|
|
38
|
+
if (row.attempts >= maxAttempts) {
|
|
39
|
+
broadcast.push(hash)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
const peerId = assignments.get(hash)
|
|
43
|
+
if (!peerId) broadcast.push(hash)
|
|
44
|
+
}
|
|
45
|
+
return { assignments, broadcast }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
|
|
50
|
+
* @param {string} chunkHash 块哈希
|
|
51
|
+
* @param {string} peerId 目标 peer
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
export function markChunkInflight(table, chunkHash, peerId) {
|
|
55
|
+
const prev = table.get(chunkHash) || { state: 'pending', attempts: 0 }
|
|
56
|
+
table.set(chunkHash, {
|
|
57
|
+
state: 'inflight',
|
|
58
|
+
peerId,
|
|
59
|
+
attempts: prev.attempts + 1,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
|
|
65
|
+
* @param {string} chunkHash 块哈希
|
|
66
|
+
* @returns {void}
|
|
67
|
+
*/
|
|
68
|
+
export function markChunkDone(table, chunkHash) {
|
|
69
|
+
table.set(chunkHash, { state: 'done', attempts: 0 })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
|
|
74
|
+
* @param {string} chunkHash 块哈希
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
77
|
+
export function markChunkFailed(table, chunkHash) {
|
|
78
|
+
const prev = table.get(chunkHash) || { state: 'pending', attempts: 0 }
|
|
79
|
+
table.set(chunkHash, { state: 'failed', attempts: prev.attempts })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
|
|
84
|
+
* @returns {{ done: number, pending: number, inflight: number, failed: number, total: number }} 进度
|
|
85
|
+
*/
|
|
86
|
+
export function chunkFetchProgress(table) {
|
|
87
|
+
let done = 0
|
|
88
|
+
let pending = 0
|
|
89
|
+
let inflight = 0
|
|
90
|
+
let failed = 0
|
|
91
|
+
for (const row of table.values())
|
|
92
|
+
if (row.state === 'done') done++
|
|
93
|
+
else if (row.state === 'inflight') inflight++
|
|
94
|
+
else if (row.state === 'failed') failed++
|
|
95
|
+
else pending++
|
|
96
|
+
return { done, pending, inflight, failed, total: table.size }
|
|
97
|
+
}
|