@steve02081504/fount-p2p 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/{entity → core}/logical_entity.mjs +2 -1
- package/federation/manifest_fetch_pending.mjs +90 -0
- package/{entity/files → files}/acl.mjs +3 -3
- package/files/chunk_fetch.mjs +2 -33
- package/files/chunk_responder.mjs +60 -2
- package/{entity/files → files}/evfs.mjs +39 -21
- package/{entity/files → files}/evfs_ref.mjs +3 -5
- package/files/fetch_fanout.mjs +38 -0
- package/{entity/files → files}/manifest_acl_registry.mjs +4 -4
- package/files/manifest_fetch.mjs +97 -0
- package/files/public_manifest.mjs +153 -0
- package/link/channel_mux.mjs +2 -0
- package/node/identity.mjs +33 -2
- package/node/personal_block.mjs +1 -1
- package/package.json +1 -3
- package/transport/rtc_connection_budget.mjs +2 -0
- package/entity/files/replica_host_cache.mjs +0 -46
- package/entity/files/url.mjs +0 -19
- package/entity/hosting_registry.mjs +0 -47
- package/entity/localized_core.mjs +0 -141
- package/entity/logical_entity_id_registry.mjs +0 -40
- package/entity/node_hash.mjs +0 -15
- package/entity/presentation_registry.mjs +0 -44
- package/entity/profile.mjs +0 -256
- package/entity/replica.mjs +0 -20
- package/entity/session_snapshot_registry.mjs +0 -38
- package/registries/p2p_viewer.mjs +0 -35
package/entity/profile.mjs
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
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
|
|
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
|
-
}
|
package/entity/replica.mjs
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
2
|
-
import { entityHashFromKeys, getNodeHash } from '../node/identity.mjs'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
|
|
6
|
-
* @returns {string | null} 本节点 entityHash
|
|
7
|
-
*/
|
|
8
|
-
export function resolveLocalEntityHashFromRecoveryPubKeyHex(recoveryPubKeyHex) {
|
|
9
|
-
return entityHashFromKeys(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
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/** @type {Map<string, (replicaUsername: string, groupId: string) => Promise<string>>} */
|
|
2
|
-
const groupMemberEntityByOwner = new Map()
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {string} ownerId 注册方
|
|
6
|
-
* @param {(replicaUsername: string, groupId: string) => Promise<string>} resolver 群成员 entityHash
|
|
7
|
-
* @returns {void}
|
|
8
|
-
*/
|
|
9
|
-
export function registerGroupMemberEntityResolver(ownerId, resolver) {
|
|
10
|
-
groupMemberEntityByOwner.set(String(ownerId), resolver)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {string} ownerId 注册方
|
|
15
|
-
* @returns {void}
|
|
16
|
-
*/
|
|
17
|
-
export function unregisterGroupMemberEntityResolver(ownerId) {
|
|
18
|
-
groupMemberEntityByOwner.delete(String(ownerId))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} replicaUsername 副本用户名
|
|
23
|
-
* @param {string} groupId 群 ID
|
|
24
|
-
* @returns {Promise<string | null>} entityHash;无注册方时 null
|
|
25
|
-
*/
|
|
26
|
-
export async function resolveGroupMemberEntityHash(replicaUsername, groupId) {
|
|
27
|
-
for (const resolver of groupMemberEntityByOwner.values())
|
|
28
|
-
try {
|
|
29
|
-
const eh = await resolver(replicaUsername, groupId)
|
|
30
|
-
if (eh) return eh
|
|
31
|
-
}
|
|
32
|
-
catch { /* next */ }
|
|
33
|
-
|
|
34
|
-
return null
|
|
35
|
-
}
|