most-box 0.2.9 → 0.3.1
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 +41 -21
- package/electron/main.js +18 -1
- package/electron/preload.cjs +8 -0
- package/out/admin/index.html +0 -0
- package/out/app/index.html +0 -0
- package/out/assets/AccountMenu-NjOgag0d.js +1 -0
- package/out/assets/FilePreviewOverlay-CZp6F1s_.js +1 -0
- package/out/assets/MarketingHeader-7B-OT2XX.js +1 -0
- package/out/assets/MarketingLayout-CyiSCygk.js +1 -0
- package/out/assets/{MilkdownEditor-CT6SHYKG.js → MilkdownEditor-WedhCoFH.js} +1 -1
- package/out/assets/OpenSidebarButton-BoSmO8CY.js +1 -0
- package/out/assets/SidebarHomeLink-CkhGDvZ8.js +1 -0
- package/out/assets/arrow-right-DUxAoZqb.js +1 -0
- package/out/assets/{ui-BpDGD3IR.js → avatar-BZwHW4ll.js} +2 -2
- package/out/assets/circle-alert-DSZ0bG_3.js +1 -0
- package/out/assets/copy-CK7HMulu.js +1 -0
- package/out/assets/download-Q1uCiwif.js +1 -0
- package/out/assets/downloadValidation-Ch966YTl.js +1 -0
- package/out/assets/external-link-DGzNJOqQ.js +1 -0
- package/out/assets/file-text-C3v0L_lQ.js +1 -0
- package/out/assets/folder-CwtLO09O.js +1 -0
- package/out/assets/hard-drive-B2V5USzR.js +1 -0
- package/out/assets/i18n-DZaOYCSO.js +1 -0
- package/out/assets/index-BRhsWNj1.css +1 -0
- package/out/assets/index-CBkHUp3F.css +1 -0
- package/out/assets/index-CYVRmppz.css +1 -0
- package/out/assets/index-DluhgSHX.js +31 -0
- package/out/assets/index.lazy-B5TYRvFJ.js +1 -0
- package/out/assets/index.lazy-BeDctVci.js +1 -0
- package/out/assets/index.lazy-BjmAkVtV.js +1 -0
- package/out/assets/index.lazy-CHJbzlK6.js +1 -0
- package/out/assets/index.lazy-CITrkWMQ.js +1 -0
- package/out/assets/index.lazy-CTGE5fUt.js +2 -0
- package/out/assets/index.lazy-CUxcXUMy.js +2 -0
- package/out/assets/index.lazy-D84xQtPb.js +3 -0
- package/out/assets/index.lazy-DRqAgqpV.js +1 -0
- package/out/assets/index.lazy-DSAmY1q_.js +1 -0
- package/out/assets/index.lazy-DvbVQRSL.js +1 -0
- package/out/assets/index.lazy-gAJi-rJV.js +1 -0
- package/out/assets/index.lazy-rzyMd-Gz.js +1 -0
- package/out/assets/key-round-Dn5tud-4.js +1 -0
- package/out/assets/loader-CLoDCtNy.js +1 -0
- package/out/assets/lock-BEWKuIqK.js +1 -0
- package/out/assets/message-square-C8ZiYztG.js +1 -0
- package/out/assets/mp-CGBtY36n.js +1 -0
- package/out/assets/music-BsTA8b_M.js +1 -0
- package/out/assets/notebook-pen-DgWzxf2y.js +1 -0
- package/out/assets/play-CVkbAJJU.js +1 -0
- package/out/assets/plus-D1kCYb8G.js +1 -0
- package/out/assets/save-X5c7jlCg.js +1 -0
- package/out/assets/search-DAV9vDOq.js +1 -0
- package/out/assets/send-C5JjYo-A.js +1 -0
- package/out/assets/shield-check-DuD6OLde.js +1 -0
- package/out/assets/smartphone-gVFruyjO.js +1 -0
- package/out/assets/trash-2-CE-3Dc63.js +1 -0
- package/out/assets/triangle-alert-DnOxsFpB.js +1 -0
- package/out/assets/upload-CsnBv3pd.js +1 -0
- package/out/assets/useChannelMessages-Bs_RWgO4.js +4 -0
- package/out/assets/useGameRoom-BxDxm3rp.js +1 -0
- package/out/assets/userProfile-UphvP_bG.js +1 -0
- package/out/assets/wallet-VDaoXwjI.js +1 -0
- package/out/assets/wifi-DfWbmlGE.js +1 -0
- package/out/chat/index.html +0 -0
- package/out/chat/join/index.html +0 -0
- package/out/download/index.html +2 -2
- package/out/game/gandengyan/index.html +0 -0
- package/out/game/index.html +0 -0
- package/out/game/zhajinhua/index.html +0 -0
- package/out/index.html +2 -2
- package/out/note/index.html +0 -0
- package/out/ping/index.html +2 -2
- package/out/profile/index.html +0 -0
- package/out/web3/index.html +0 -0
- package/package.json +2 -5
- package/server/index.js +59 -2
- package/server/src/http/app.js +9 -1
- package/server/src/http/nodeStatus.js +6 -0
- package/server/src/http/routePolicy.js +1 -0
- package/server/src/http/routes/channelRoutes.js +12 -0
- package/server/src/http/routes/noteVaultRoutes.js +85 -0
- package/server/src/index.js +476 -3
- package/server/src/utils/noteVault.js +327 -0
- package/electron/preload.js +0 -6
- package/out/assets/FilePreviewOverlay-BYZIAMJ-.js +0 -1
- package/out/assets/LanguageToggle-CSQtRZ3A.js +0 -1
- package/out/assets/MarketingHeader-ceWdRCpe.js +0 -1
- package/out/assets/MarketingLayout-Bun1d2wL.js +0 -1
- package/out/assets/MarketingThemeToggle-CIKP8x9W.js +0 -1
- package/out/assets/OpenSidebarButton-CLSCB7CG.js +0 -1
- package/out/assets/SidebarAccount-CNh0kvtB.js +0 -1
- package/out/assets/SidebarHomeLink-DXv0c9Ob.js +0 -1
- package/out/assets/arrow-right-BU9c_SL4.js +0 -1
- package/out/assets/circle-alert-iuHzciph.js +0 -1
- package/out/assets/copy-DEzFUzeX.js +0 -1
- package/out/assets/download-jEPn2njL.js +0 -1
- package/out/assets/downloadValidation-Bzl7cci9.js +0 -1
- package/out/assets/external-link-BFaoZeK0.js +0 -1
- package/out/assets/file-text-DPA1riJ_.js +0 -1
- package/out/assets/folder-Cfgc70_F.js +0 -1
- package/out/assets/hard-drive-Dar_AZEq.js +0 -1
- package/out/assets/i18n-NjvOSuV6.js +0 -1
- package/out/assets/index-3OD3Chi9.css +0 -1
- package/out/assets/index-BHZhApbo.css +0 -1
- package/out/assets/index-BI8oNSVC.js +0 -31
- package/out/assets/index-BSOvFG3o.css +0 -1
- package/out/assets/index.lazy-7omTRykl.js +0 -2
- package/out/assets/index.lazy-BOMHingE.js +0 -3
- package/out/assets/index.lazy-BSY5_gby.js +0 -1
- package/out/assets/index.lazy-C4ipXKZ7.js +0 -1
- package/out/assets/index.lazy-CPZbEnJZ.js +0 -1
- package/out/assets/index.lazy-Ch4Y17pJ.js +0 -1
- package/out/assets/index.lazy-CvBIIYbm.js +0 -1
- package/out/assets/index.lazy-Cz-OZwYL.js +0 -1
- package/out/assets/index.lazy-D4zNn3D6.js +0 -2
- package/out/assets/index.lazy-DRY3xoQp.js +0 -1
- package/out/assets/index.lazy-_mEd0Q_u.js +0 -1
- package/out/assets/index.lazy-bAp_Hgck.js +0 -1
- package/out/assets/index.lazy-ur0joIFl.js +0 -1
- package/out/assets/key-round-DNcpSuMw.js +0 -1
- package/out/assets/loader-BXQtdens.js +0 -1
- package/out/assets/lock-CLIE8cct.js +0 -1
- package/out/assets/message-square-Dp09WY1I.js +0 -1
- package/out/assets/mp-B6kdA94c.js +0 -1
- package/out/assets/music-B19Tg557.js +0 -1
- package/out/assets/notebook-pen-C8TlK5sB.js +0 -1
- package/out/assets/play-B-BjsTZz.js +0 -1
- package/out/assets/plus-4hygj-1Z.js +0 -1
- package/out/assets/save-BjrbPwhJ.js +0 -1
- package/out/assets/search-DSkiWdvX.js +0 -1
- package/out/assets/send-BL4d5iOt.js +0 -1
- package/out/assets/shield-check-DzDuWO8r.js +0 -1
- package/out/assets/smartphone-tRRFFO0a.js +0 -1
- package/out/assets/trash-2-V12tJ8lI.js +0 -1
- package/out/assets/triangle-alert-DGWc8ZHu.js +0 -1
- package/out/assets/upload-CoCptKya.js +0 -1
- package/out/assets/useChannelMessages-BeKF0gwo.js +0 -3
- package/out/assets/useGameRoom-cs9YpYt7.js +0 -1
- package/out/assets/userProfile-qcc60tHK.js +0 -1
- package/out/assets/wallet-T7ONZ3Kg.js +0 -1
- package/out/assets/wifi-Bgo8eHY0.js +0 -1
package/server/src/index.js
CHANGED
|
@@ -93,6 +93,8 @@ import {
|
|
|
93
93
|
} from './config.js'
|
|
94
94
|
|
|
95
95
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
96
|
+
const CHANNEL_PRESENCE_HEARTBEAT_MS = 15 * 1000
|
|
97
|
+
const CHANNEL_PRESENCE_TIMEOUT_MS = 45 * 1000
|
|
96
98
|
const CHANNEL_WELCOME_MESSAGE = '我加入了群聊'
|
|
97
99
|
|
|
98
100
|
function createMemoryDuplexPair() {
|
|
@@ -158,6 +160,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
158
160
|
#channelPeers = new Map()
|
|
159
161
|
#channelCandidateCache = new Map()
|
|
160
162
|
#channelStreams = new Set()
|
|
163
|
+
#channelPresenceSessions = new Map()
|
|
164
|
+
#channelPresenceProfiles = new Map()
|
|
165
|
+
#channelPresenceSweepTimer = null
|
|
161
166
|
|
|
162
167
|
#accountMetadata = { profiles: {} }
|
|
163
168
|
|
|
@@ -190,6 +195,90 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
getChannelPresence(channelKeyInput, options = {}) {
|
|
199
|
+
this.#ensureInitialized()
|
|
200
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
201
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
202
|
+
this.#pruneStaleChannelPresence()
|
|
203
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
joinChannelPresence(channelKeyInput, options = {}) {
|
|
207
|
+
this.#ensureInitialized()
|
|
208
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
209
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
210
|
+
const event = this.#upsertChannelPresenceSession(channel, {
|
|
211
|
+
...options,
|
|
212
|
+
address: options.ownerAddress,
|
|
213
|
+
local: true,
|
|
214
|
+
})
|
|
215
|
+
if (event) {
|
|
216
|
+
this.#broadcastChannelPresence(event)
|
|
217
|
+
}
|
|
218
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
heartbeatChannelPresence(channelKeyInput, options = {}) {
|
|
222
|
+
this.#ensureInitialized()
|
|
223
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
224
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
225
|
+
const event = this.#touchChannelPresenceSession(channel, {
|
|
226
|
+
...options,
|
|
227
|
+
address: options.ownerAddress,
|
|
228
|
+
local: true,
|
|
229
|
+
})
|
|
230
|
+
this.#broadcastChannelPresence(
|
|
231
|
+
event ||
|
|
232
|
+
this.#formatChannelPresence(
|
|
233
|
+
channel.channelKey,
|
|
234
|
+
options.ownerAddress,
|
|
235
|
+
'heartbeat'
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
updateChannelPresenceProfile(channelKeyInput, options = {}) {
|
|
242
|
+
this.#ensureInitialized()
|
|
243
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
244
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
245
|
+
const event = this.#updateChannelPresenceProfile(channel, {
|
|
246
|
+
...options,
|
|
247
|
+
address: options.ownerAddress,
|
|
248
|
+
local: true,
|
|
249
|
+
})
|
|
250
|
+
if (event) {
|
|
251
|
+
this.#broadcastChannelPresence(event)
|
|
252
|
+
}
|
|
253
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
leaveChannelPresence(channelKeyInput, options = {}) {
|
|
257
|
+
this.#ensureInitialized()
|
|
258
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
259
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
260
|
+
const events = this.#removeChannelPresenceSessions(channel.channelKey, {
|
|
261
|
+
...options,
|
|
262
|
+
address: options.ownerAddress,
|
|
263
|
+
})
|
|
264
|
+
events.forEach(event => this.#broadcastChannelPresence(event))
|
|
265
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
clearChannelPresenceSource(sourceId, options = {}) {
|
|
269
|
+
this.#ensureInitialized()
|
|
270
|
+
const events = this.#removeChannelPresenceSessionsBySource(sourceId)
|
|
271
|
+
if (options.broadcast) {
|
|
272
|
+
events.forEach(event => this.#broadcastChannelPresence(event))
|
|
273
|
+
}
|
|
274
|
+
return events
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
pruneChannelPresence() {
|
|
278
|
+
this.#ensureInitialized()
|
|
279
|
+
return this.#pruneStaleChannelPresence()
|
|
280
|
+
}
|
|
281
|
+
|
|
193
282
|
/**
|
|
194
283
|
* 初始化引擎 — 必须在调用其他方法之前调用
|
|
195
284
|
*/
|
|
@@ -363,6 +452,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
363
452
|
console.log(`[MostBox] Engine initialized successfully`)
|
|
364
453
|
this.emit('ready')
|
|
365
454
|
this.#resumeHoldingsInBackground()
|
|
455
|
+
this.#startChannelPresenceSweeper()
|
|
366
456
|
|
|
367
457
|
return this
|
|
368
458
|
}
|
|
@@ -419,6 +509,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
419
509
|
this.#channelPeers.clear()
|
|
420
510
|
this.#channelCandidateCache.clear()
|
|
421
511
|
this.#channelStreams.clear()
|
|
512
|
+
this.#clearChannelPresenceRuntime()
|
|
422
513
|
this.#channels = []
|
|
423
514
|
|
|
424
515
|
if (this.#store) {
|
|
@@ -3111,6 +3202,312 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3111
3202
|
)
|
|
3112
3203
|
}
|
|
3113
3204
|
|
|
3205
|
+
#normalizePresenceSessionId(sessionId) {
|
|
3206
|
+
return String(sessionId || 'default').trim().slice(0, 120) || 'default'
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
#normalizePresenceSourceId(options = {}) {
|
|
3210
|
+
const sourceId = String(options.sourceId || '').trim()
|
|
3211
|
+
if (sourceId) return sourceId.slice(0, 160)
|
|
3212
|
+
const sourcePeerId = String(options.sourcePeerId || '').trim()
|
|
3213
|
+
if (sourcePeerId) return `peer:${sourcePeerId}`.slice(0, 180)
|
|
3214
|
+
return 'local'
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
#getPresenceSessionKey(options = {}) {
|
|
3218
|
+
return [
|
|
3219
|
+
this.#normalizePresenceSourceId(options),
|
|
3220
|
+
normalizeOwnerAddress(options.address),
|
|
3221
|
+
this.#normalizePresenceSessionId(options.sessionId),
|
|
3222
|
+
].join(':')
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
#getChannelPresenceSessionMap(channelKey) {
|
|
3226
|
+
if (!this.#channelPresenceSessions.has(channelKey)) {
|
|
3227
|
+
this.#channelPresenceSessions.set(channelKey, new Map())
|
|
3228
|
+
}
|
|
3229
|
+
return this.#channelPresenceSessions.get(channelKey)
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
#getChannelPresenceProfileMap(channelKey) {
|
|
3233
|
+
if (!this.#channelPresenceProfiles.has(channelKey)) {
|
|
3234
|
+
this.#channelPresenceProfiles.set(channelKey, new Map())
|
|
3235
|
+
}
|
|
3236
|
+
return this.#channelPresenceProfiles.get(channelKey)
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
#isChannelPresenceAddressOnline(channelKey, address) {
|
|
3240
|
+
const normalizedAddress = normalizeOwnerAddress(address)
|
|
3241
|
+
if (!normalizedAddress) return false
|
|
3242
|
+
const sessions = this.#channelPresenceSessions.get(channelKey)
|
|
3243
|
+
if (!sessions) return false
|
|
3244
|
+
return [...sessions.values()].some(
|
|
3245
|
+
session => session.address === normalizedAddress
|
|
3246
|
+
)
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
#getChannelPresenceList(channelKey) {
|
|
3250
|
+
const sessions = this.#channelPresenceSessions.get(channelKey)
|
|
3251
|
+
if (!sessions) return []
|
|
3252
|
+
const addresses = uniqueStrings(
|
|
3253
|
+
[...sessions.values()].map(session => session.address)
|
|
3254
|
+
)
|
|
3255
|
+
return addresses
|
|
3256
|
+
.map(address => this.#formatChannelPresence(channelKey, address))
|
|
3257
|
+
.filter(Boolean)
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
#formatChannelPresence(channelKey, address, status = 'online') {
|
|
3261
|
+
const normalizedAddress = normalizeOwnerAddress(address)
|
|
3262
|
+
if (!normalizedAddress) return null
|
|
3263
|
+
const sessions = [
|
|
3264
|
+
...(this.#channelPresenceSessions.get(channelKey)?.values() || []),
|
|
3265
|
+
].filter(session => session.address === normalizedAddress)
|
|
3266
|
+
const profile = this.#channelPresenceProfiles
|
|
3267
|
+
.get(channelKey)
|
|
3268
|
+
?.get(normalizedAddress)
|
|
3269
|
+
const lastSeen = Math.max(
|
|
3270
|
+
0,
|
|
3271
|
+
Number(profile?.lastSeen) || 0,
|
|
3272
|
+
...sessions.map(session => Number(session.lastSeen) || 0)
|
|
3273
|
+
)
|
|
3274
|
+
return {
|
|
3275
|
+
channelKey,
|
|
3276
|
+
channelId:
|
|
3277
|
+
this.#channels.find(channel => channel.channelKey === channelKey)
|
|
3278
|
+
?.channelId || channelKey,
|
|
3279
|
+
address: normalizedAddress,
|
|
3280
|
+
displayName: profile?.displayName || undefined,
|
|
3281
|
+
avatar: profile?.avatar || undefined,
|
|
3282
|
+
profileUpdatedAt: profile?.profileUpdatedAt || undefined,
|
|
3283
|
+
lastSeen,
|
|
3284
|
+
online: sessions.length > 0,
|
|
3285
|
+
local: sessions.some(session => session.local),
|
|
3286
|
+
status,
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
#upsertChannelPresenceProfile(channelKey, address, options = {}, now = Date.now()) {
|
|
3291
|
+
const normalizedAddress = normalizeOwnerAddress(address)
|
|
3292
|
+
if (!normalizedAddress) return false
|
|
3293
|
+
const hasDisplayName = Object.prototype.hasOwnProperty.call(
|
|
3294
|
+
options,
|
|
3295
|
+
'displayName'
|
|
3296
|
+
)
|
|
3297
|
+
const hasAvatar = Object.prototype.hasOwnProperty.call(options, 'avatar')
|
|
3298
|
+
const profileUpdatedAt = Number(options.profileUpdatedAt)
|
|
3299
|
+
const hasProfileUpdatedAt =
|
|
3300
|
+
Number.isFinite(profileUpdatedAt) && profileUpdatedAt > 0
|
|
3301
|
+
if (!hasDisplayName && !hasAvatar && !hasProfileUpdatedAt) return false
|
|
3302
|
+
|
|
3303
|
+
const profiles = this.#getChannelPresenceProfileMap(channelKey)
|
|
3304
|
+
const previous = profiles.get(normalizedAddress)
|
|
3305
|
+
const nextUpdatedAt = hasProfileUpdatedAt
|
|
3306
|
+
? Math.floor(profileUpdatedAt)
|
|
3307
|
+
: now
|
|
3308
|
+
if (
|
|
3309
|
+
previous?.profileUpdatedAt &&
|
|
3310
|
+
hasProfileUpdatedAt &&
|
|
3311
|
+
nextUpdatedAt < previous.profileUpdatedAt
|
|
3312
|
+
) {
|
|
3313
|
+
return false
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
const next = {
|
|
3317
|
+
address: normalizedAddress,
|
|
3318
|
+
displayName: previous?.displayName || '',
|
|
3319
|
+
avatar: previous?.avatar || '',
|
|
3320
|
+
profileUpdatedAt: nextUpdatedAt,
|
|
3321
|
+
lastSeen: now,
|
|
3322
|
+
}
|
|
3323
|
+
if (hasDisplayName) {
|
|
3324
|
+
next.displayName = normalizeChannelDisplayName(
|
|
3325
|
+
options.displayName,
|
|
3326
|
+
normalizedAddress
|
|
3327
|
+
)
|
|
3328
|
+
}
|
|
3329
|
+
if (hasAvatar) {
|
|
3330
|
+
next.avatar = normalizeChannelAvatar(options.avatar)
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
const changed =
|
|
3334
|
+
!previous ||
|
|
3335
|
+
previous.displayName !== next.displayName ||
|
|
3336
|
+
previous.avatar !== next.avatar ||
|
|
3337
|
+
previous.profileUpdatedAt !== next.profileUpdatedAt
|
|
3338
|
+
profiles.set(normalizedAddress, next)
|
|
3339
|
+
return changed
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
#emitChannelPresence(channelKey, address, status) {
|
|
3343
|
+
const event = this.#formatChannelPresence(channelKey, address, status)
|
|
3344
|
+
if (event) {
|
|
3345
|
+
this.emit('channel:presence', event)
|
|
3346
|
+
}
|
|
3347
|
+
return event
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
#upsertChannelPresenceSession(channel, options = {}) {
|
|
3351
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3352
|
+
if (!address) return null
|
|
3353
|
+
const now = Number(options.lastSeen) || Date.now()
|
|
3354
|
+
const channelKey = channel.channelKey
|
|
3355
|
+
const wasOnline = this.#isChannelPresenceAddressOnline(channelKey, address)
|
|
3356
|
+
const session = {
|
|
3357
|
+
sessionId: this.#normalizePresenceSessionId(options.sessionId),
|
|
3358
|
+
sourceId: this.#normalizePresenceSourceId(options),
|
|
3359
|
+
address,
|
|
3360
|
+
channelKey,
|
|
3361
|
+
lastSeen: now,
|
|
3362
|
+
local: options.local === true,
|
|
3363
|
+
sourcePeerId: String(options.sourcePeerId || '').trim(),
|
|
3364
|
+
}
|
|
3365
|
+
this.#getChannelPresenceSessionMap(channelKey).set(
|
|
3366
|
+
this.#getPresenceSessionKey(session),
|
|
3367
|
+
session
|
|
3368
|
+
)
|
|
3369
|
+
const profileChanged = this.#upsertChannelPresenceProfile(
|
|
3370
|
+
channelKey,
|
|
3371
|
+
address,
|
|
3372
|
+
options,
|
|
3373
|
+
now
|
|
3374
|
+
)
|
|
3375
|
+
if (!wasOnline) {
|
|
3376
|
+
return this.#emitChannelPresence(channelKey, address, 'online')
|
|
3377
|
+
}
|
|
3378
|
+
if (profileChanged) {
|
|
3379
|
+
return this.#emitChannelPresence(channelKey, address, 'profile')
|
|
3380
|
+
}
|
|
3381
|
+
return null
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
#touchChannelPresenceSession(channel, options = {}) {
|
|
3385
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3386
|
+
if (!address) return null
|
|
3387
|
+
const channelKey = channel.channelKey
|
|
3388
|
+
const sessionKey = this.#getPresenceSessionKey({
|
|
3389
|
+
...options,
|
|
3390
|
+
address,
|
|
3391
|
+
})
|
|
3392
|
+
const sessions = this.#getChannelPresenceSessionMap(channelKey)
|
|
3393
|
+
const existing = sessions.get(sessionKey)
|
|
3394
|
+
if (!existing) {
|
|
3395
|
+
return this.#upsertChannelPresenceSession(channel, {
|
|
3396
|
+
...options,
|
|
3397
|
+
address,
|
|
3398
|
+
})
|
|
3399
|
+
}
|
|
3400
|
+
existing.lastSeen = Number(options.lastSeen) || Date.now()
|
|
3401
|
+
sessions.set(sessionKey, existing)
|
|
3402
|
+
return null
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
#updateChannelPresenceProfile(channel, options = {}) {
|
|
3406
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3407
|
+
if (!address) return null
|
|
3408
|
+
const now = Number(options.lastSeen) || Date.now()
|
|
3409
|
+
const changed = this.#upsertChannelPresenceProfile(
|
|
3410
|
+
channel.channelKey,
|
|
3411
|
+
address,
|
|
3412
|
+
options,
|
|
3413
|
+
now
|
|
3414
|
+
)
|
|
3415
|
+
if (changed && this.#isChannelPresenceAddressOnline(channel.channelKey, address)) {
|
|
3416
|
+
return this.#emitChannelPresence(channel.channelKey, address, 'profile')
|
|
3417
|
+
}
|
|
3418
|
+
return null
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
#removeChannelPresenceSessions(channelKey, options = {}) {
|
|
3422
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3423
|
+
const sourceId = this.#normalizePresenceSourceId(options)
|
|
3424
|
+
const sessionId = options.sessionId
|
|
3425
|
+
? this.#normalizePresenceSessionId(options.sessionId)
|
|
3426
|
+
: ''
|
|
3427
|
+
const sessions = this.#channelPresenceSessions.get(channelKey)
|
|
3428
|
+
if (!sessions || (!address && !sourceId)) return []
|
|
3429
|
+
|
|
3430
|
+
const touchedAddresses = new Set()
|
|
3431
|
+
for (const [key, session] of [...sessions]) {
|
|
3432
|
+
if (address && session.address !== address) continue
|
|
3433
|
+
if (sourceId && session.sourceId !== sourceId) continue
|
|
3434
|
+
if (sessionId && session.sessionId !== sessionId) continue
|
|
3435
|
+
touchedAddresses.add(session.address)
|
|
3436
|
+
sessions.delete(key)
|
|
3437
|
+
}
|
|
3438
|
+
if (sessions.size === 0) {
|
|
3439
|
+
this.#channelPresenceSessions.delete(channelKey)
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
return [...touchedAddresses]
|
|
3443
|
+
.filter(item => !this.#isChannelPresenceAddressOnline(channelKey, item))
|
|
3444
|
+
.map(item => this.#emitChannelPresence(channelKey, item, 'offline'))
|
|
3445
|
+
.filter(Boolean)
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
#removeChannelPresenceSessionsBySource(sourceId) {
|
|
3449
|
+
const normalizedSourceId = String(sourceId || '').trim()
|
|
3450
|
+
if (!normalizedSourceId) return []
|
|
3451
|
+
const events = []
|
|
3452
|
+
for (const [channelKey, sessions] of [...this.#channelPresenceSessions]) {
|
|
3453
|
+
const touchedAddresses = new Set()
|
|
3454
|
+
for (const [key, session] of [...sessions]) {
|
|
3455
|
+
if (session.sourceId !== normalizedSourceId) continue
|
|
3456
|
+
touchedAddresses.add(session.address)
|
|
3457
|
+
sessions.delete(key)
|
|
3458
|
+
}
|
|
3459
|
+
if (sessions.size === 0) {
|
|
3460
|
+
this.#channelPresenceSessions.delete(channelKey)
|
|
3461
|
+
}
|
|
3462
|
+
for (const address of touchedAddresses) {
|
|
3463
|
+
if (!this.#isChannelPresenceAddressOnline(channelKey, address)) {
|
|
3464
|
+
const event = this.#emitChannelPresence(channelKey, address, 'offline')
|
|
3465
|
+
if (event) events.push(event)
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return events
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
#pruneStaleChannelPresence(now = Date.now()) {
|
|
3473
|
+
const events = []
|
|
3474
|
+
for (const [channelKey, sessions] of [...this.#channelPresenceSessions]) {
|
|
3475
|
+
const touchedAddresses = new Set()
|
|
3476
|
+
for (const [key, session] of [...sessions]) {
|
|
3477
|
+
if (now - session.lastSeen <= CHANNEL_PRESENCE_TIMEOUT_MS) continue
|
|
3478
|
+
touchedAddresses.add(session.address)
|
|
3479
|
+
sessions.delete(key)
|
|
3480
|
+
}
|
|
3481
|
+
if (sessions.size === 0) {
|
|
3482
|
+
this.#channelPresenceSessions.delete(channelKey)
|
|
3483
|
+
}
|
|
3484
|
+
for (const address of touchedAddresses) {
|
|
3485
|
+
if (!this.#isChannelPresenceAddressOnline(channelKey, address)) {
|
|
3486
|
+
const event = this.#emitChannelPresence(channelKey, address, 'offline')
|
|
3487
|
+
if (event) events.push(event)
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
return events
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
#startChannelPresenceSweeper() {
|
|
3495
|
+
if (this.#channelPresenceSweepTimer) return
|
|
3496
|
+
this.#channelPresenceSweepTimer = setInterval(() => {
|
|
3497
|
+
this.#pruneStaleChannelPresence()
|
|
3498
|
+
}, CHANNEL_PRESENCE_HEARTBEAT_MS)
|
|
3499
|
+
this.#channelPresenceSweepTimer.unref?.()
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
#clearChannelPresenceRuntime() {
|
|
3503
|
+
if (this.#channelPresenceSweepTimer) {
|
|
3504
|
+
clearInterval(this.#channelPresenceSweepTimer)
|
|
3505
|
+
this.#channelPresenceSweepTimer = null
|
|
3506
|
+
}
|
|
3507
|
+
this.#channelPresenceSessions.clear()
|
|
3508
|
+
this.#channelPresenceProfiles.clear()
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3114
3511
|
async #getNextChannelMessageTimestamp(channelKey) {
|
|
3115
3512
|
const coresMap = this.#channelCores.get(channelKey)
|
|
3116
3513
|
let maxTimestamp = 0
|
|
@@ -4473,6 +4870,40 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4473
4870
|
}
|
|
4474
4871
|
}
|
|
4475
4872
|
|
|
4873
|
+
#sendChannelPresence(stream, event) {
|
|
4874
|
+
if (!stream || stream.destroyed || stream.writableEnded || !event) {
|
|
4875
|
+
this.#channelStreams.delete(stream)
|
|
4876
|
+
return false
|
|
4877
|
+
}
|
|
4878
|
+
try {
|
|
4879
|
+
stream.write(
|
|
4880
|
+
`${JSON.stringify({
|
|
4881
|
+
type: 'channel-presence',
|
|
4882
|
+
peerId: this.getNodeId(),
|
|
4883
|
+
channelId: event.channelId,
|
|
4884
|
+
channelKey: event.channelKey,
|
|
4885
|
+
address: event.address,
|
|
4886
|
+
status: event.status,
|
|
4887
|
+
displayName: event.displayName,
|
|
4888
|
+
avatar: event.avatar,
|
|
4889
|
+
profileUpdatedAt: event.profileUpdatedAt,
|
|
4890
|
+
lastSeen: event.lastSeen || Date.now(),
|
|
4891
|
+
sessionId: event.sessionId || 'default',
|
|
4892
|
+
})}\n`
|
|
4893
|
+
)
|
|
4894
|
+
return true
|
|
4895
|
+
} catch {
|
|
4896
|
+
this.#channelStreams.delete(stream)
|
|
4897
|
+
return false
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
#broadcastChannelPresence(event) {
|
|
4902
|
+
for (const stream of [...this.#channelStreams]) {
|
|
4903
|
+
this.#sendChannelPresence(stream, event)
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4476
4907
|
async #processChannelHelloMessage(msg) {
|
|
4477
4908
|
if (msg.type !== 'channel-hello') return null
|
|
4478
4909
|
|
|
@@ -4554,6 +4985,45 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4554
4985
|
return msg.peerId
|
|
4555
4986
|
}
|
|
4556
4987
|
|
|
4988
|
+
#processChannelPresenceMessage(msg) {
|
|
4989
|
+
if (msg.type !== 'channel-presence') return null
|
|
4990
|
+
const peerId = String(msg.peerId || '').trim()
|
|
4991
|
+
if (!peerId || peerId === this.getNodeId()) return null
|
|
4992
|
+
const channelId = normalizeChannelId(msg.channelId || msg.channelKey)
|
|
4993
|
+
const channelKey = buildChannelKey(channelId)
|
|
4994
|
+
const localChannel = this.#channels.find(
|
|
4995
|
+
channel => channel.channelKey === channelKey
|
|
4996
|
+
)
|
|
4997
|
+
if (!localChannel) return peerId
|
|
4998
|
+
|
|
4999
|
+
const address = normalizeOwnerAddress(msg.address)
|
|
5000
|
+
if (!address) return peerId
|
|
5001
|
+
|
|
5002
|
+
const status = String(msg.status || '').trim()
|
|
5003
|
+
const options = {
|
|
5004
|
+
address,
|
|
5005
|
+
sessionId: msg.sessionId,
|
|
5006
|
+
sourcePeerId: peerId,
|
|
5007
|
+
local: false,
|
|
5008
|
+
displayName: msg.displayName,
|
|
5009
|
+
avatar: msg.avatar,
|
|
5010
|
+
profileUpdatedAt: msg.profileUpdatedAt,
|
|
5011
|
+
lastSeen: Number(msg.lastSeen) || Date.now(),
|
|
5012
|
+
}
|
|
5013
|
+
|
|
5014
|
+
if (status === 'online') {
|
|
5015
|
+
this.#upsertChannelPresenceSession(localChannel, options)
|
|
5016
|
+
} else if (status === 'heartbeat') {
|
|
5017
|
+
this.#touchChannelPresenceSession(localChannel, options)
|
|
5018
|
+
} else if (status === 'profile') {
|
|
5019
|
+
this.#updateChannelPresenceProfile(localChannel, options)
|
|
5020
|
+
} else if (status === 'offline') {
|
|
5021
|
+
this.#removeChannelPresenceSessions(localChannel.channelKey, options)
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
return peerId
|
|
5025
|
+
}
|
|
5026
|
+
|
|
4557
5027
|
async #handleChannelConnection(conn) {
|
|
4558
5028
|
const stream = conn
|
|
4559
5029
|
let connectedPeerId = null
|
|
@@ -4572,9 +5042,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4572
5042
|
newlineIndex = readBuffer.indexOf('\n')
|
|
4573
5043
|
if (!line) continue
|
|
4574
5044
|
try {
|
|
4575
|
-
const
|
|
4576
|
-
|
|
4577
|
-
|
|
5045
|
+
const message = JSON.parse(line)
|
|
5046
|
+
const peerId =
|
|
5047
|
+
message.type === 'channel-presence'
|
|
5048
|
+
? this.#processChannelPresenceMessage(message)
|
|
5049
|
+
: await this.#processChannelHelloMessage(message)
|
|
4578
5050
|
if (peerId) connectedPeerId = peerId
|
|
4579
5051
|
} catch (err) {
|
|
4580
5052
|
console.warn(`[MostBox] Failed to process channel data:`, err.message)
|
|
@@ -4603,6 +5075,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4603
5075
|
})
|
|
4604
5076
|
}
|
|
4605
5077
|
}
|
|
5078
|
+
this.#removeChannelPresenceSessionsBySource(`peer:${connectedPeerId}`)
|
|
4606
5079
|
}
|
|
4607
5080
|
}
|
|
4608
5081
|
|