most-box 0.2.3 → 0.2.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 +3 -26
- package/electron/main.js +366 -30
- package/electron/preload.js +16 -1
- package/electron/updateChecker.js +18 -11
- package/electron/updateChecker.test.js +23 -7
- package/out/admin/index.html +0 -0
- package/out/app/index.html +0 -0
- package/out/assets/AppShell-gILTcRs-.js +1 -0
- package/out/assets/FilePreviewOverlay-CM-fcIaD.js +1 -0
- package/out/assets/LanguageToggle-B99eQcTE.js +1 -0
- package/out/assets/MarketingHeader-Q2cpkqAM.js +1 -0
- package/out/assets/MarketingLayout-X3Nj2JvA.js +1 -0
- package/out/assets/{MarketingThemeToggle-DHVvLtms.js → MarketingThemeToggle-z4jr4-vt.js} +1 -1
- package/out/assets/{MilkdownEditor-WdGJyEa8.js → MilkdownEditor-Bv2VA3bu.js} +1 -1
- package/out/assets/{OpenSidebarButton-CtsDYSq0.js → OpenSidebarButton-CaQJk5LD.js} +1 -1
- package/out/assets/{SidebarAccount-4NFTcAF7.js → SidebarAccount-CpNVqNva.js} +1 -1
- package/out/assets/arrow-right-mNO53HQi.js +1 -0
- package/out/assets/{circle-alert-BQoSDxUe.js → circle-alert-CofRTRHr.js} +1 -1
- package/out/assets/cloud-DAGFCFIp.js +1 -0
- package/out/assets/{copy-BtJHJnqH.js → copy-5aEv78Eq.js} +1 -1
- package/out/assets/{download-Bg-OdoxM.js → download-Bhqn6hL1.js} +1 -1
- package/out/assets/{downloadValidation-DS9_BV-n.js → downloadValidation-xzpa5rap.js} +1 -1
- package/out/assets/{external-link-m8ZIQe4p.js → external-link-CNR21yFA.js} +1 -1
- package/out/assets/{file-text-DG1orIkZ.js → file-text-CFfvvCnM.js} +1 -1
- package/out/assets/folder-BGJCjyba.js +1 -0
- package/out/assets/{hard-drive-BB-sllXA.js → hard-drive-4vHpA5kz.js} +1 -1
- package/out/assets/index-CgErXULe.css +1 -0
- package/out/assets/index-DN0kD5zr.js +31 -0
- package/out/assets/{index.lazy-C3z-lGsL.js → index.lazy-0DsJzEoI.js} +1 -1
- package/out/assets/{index.lazy-DwKnzBXo.js → index.lazy-CJBRtbWA.js} +1 -1
- package/out/assets/{index.lazy-DSjaivix.js → index.lazy-CPV-dZxo.js} +1 -1
- package/out/assets/{index.lazy-Dvo4Ncqe.js → index.lazy-CQavt0-9.js} +2 -2
- package/out/assets/index.lazy-CUq9lbK_.js +1 -0
- package/out/assets/index.lazy-CxpNXlwa.js +1 -0
- package/out/assets/index.lazy-DOgKxDuF.js +1 -0
- package/out/assets/index.lazy-Dzf-q-Vf.js +1 -0
- package/out/assets/index.lazy-GtegOPkJ.js +2 -0
- package/out/assets/index.lazy-H7NATHUs.js +1 -0
- package/out/assets/index.lazy-b_N8nrYw.js +1 -0
- package/out/assets/index.lazy-erexqght.js +3 -0
- package/out/assets/index.lazy-s3nUpCQ1.js +1 -0
- package/out/assets/{key-round-DIQ3Xt5F.js → key-round-BpSdpOHi.js} +1 -1
- package/out/assets/loader-ByMmV2_4.js +1 -0
- package/out/assets/{lock-tf1t2yuy.js → lock-C0pdaery.js} +1 -1
- package/out/assets/{message-square-DaQH7q-P.js → message-square-Eu86zZup.js} +1 -1
- package/out/assets/{mp-DYPYks13.js → mp-DjXDNjls.js} +1 -1
- package/out/assets/{music-DaFvU2DL.js → music-Btrhub6z.js} +1 -1
- package/out/assets/{notebook-pen-CqEFOHKx.js → notebook-pen-CfdFgaDE.js} +1 -1
- package/out/assets/play-D5r9Yvbq.js +1 -0
- package/out/assets/plus-Mlh7r_bm.js +1 -0
- package/out/assets/{refresh-cw-roxAhABl.js → refresh-cw--JB8k6mH.js} +1 -1
- package/out/assets/{save-DR0O9ReR.js → save-DN4eprSt.js} +1 -1
- package/out/assets/search-CbzMyENy.js +1 -0
- package/out/assets/{send-m1XCcuPn.js → send-D8p6f8OD.js} +1 -1
- package/out/assets/{shield-check-BwcvTU4U.js → shield-check-c0labdRH.js} +1 -1
- package/out/assets/{trash-2-CNpsqYc1.js → trash-2-zdhWec_U.js} +1 -1
- package/out/assets/{triangle-alert-DP9EP7IM.js → triangle-alert-D-qigSSn.js} +1 -1
- package/out/assets/upload-BD23c5Pz.js +1 -0
- package/out/assets/{useChannelMessages-qJTlBnzX.js → useChannelMessages-DS1ntAKN.js} +2 -2
- package/out/assets/useGameRoom-BSPkCY3n.js +1 -0
- package/out/assets/userProfile-BmzBA_Bk.js +1 -0
- package/out/assets/{userStore-C4vdYsQp.js → userStore-q0BBZsIV.js} +1 -1
- package/out/assets/{wallet-CozFU6yK.js → wallet-DmZMdK9A.js} +1 -1
- package/out/assets/{wifi-DIR3g_8A.js → wifi-CVqaPDcv.js} +1 -1
- 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 +8 -1
- package/server/index.js +2 -2
- package/server/src/core/syncTimestamp.js +10 -0
- package/server/src/http/nodeStatus.js +16 -16
- package/server/src/http/routePolicy.js +2 -2
- package/server/src/http/routes/nodeRoutes.js +21 -19
- package/server/src/index.js +578 -960
- package/server/src/utils/accountBackup.js +134 -0
- package/out/assets/AppShell-B1U4lDqV.js +0 -1
- package/out/assets/FilePreviewOverlay-Cme4vUhr.js +0 -1
- package/out/assets/LanguageToggle-CBpXWdO9.js +0 -1
- package/out/assets/MarketingHeader-CkcBATx2.js +0 -1
- package/out/assets/MarketingLayout-mQV6ciYc.js +0 -1
- package/out/assets/arrow-right-RldN7Rwi.js +0 -1
- package/out/assets/cloud-DHoTMeZY.js +0 -1
- package/out/assets/index-BJ7E8LJN.js +0 -31
- package/out/assets/index-C0xqKeu-.css +0 -1
- package/out/assets/index.lazy-B5hOQuoO.js +0 -2
- package/out/assets/index.lazy-B9ligvAg.js +0 -1
- package/out/assets/index.lazy-BZ7nMBcY.js +0 -3
- package/out/assets/index.lazy-C-2BI6Ew.js +0 -1
- package/out/assets/index.lazy-Cda9sU_h.js +0 -1
- package/out/assets/index.lazy-D5ybxeOu.js +0 -1
- package/out/assets/index.lazy-DMZi3gid.js +0 -1
- package/out/assets/index.lazy-Dvgt53xN.js +0 -1
- package/out/assets/index.lazy-GQILyi6o.js +0 -1
- package/out/assets/play-B7q6F75-.js +0 -1
- package/out/assets/plus-H2i2mspM.js +0 -1
- package/out/assets/search-ncblG-zw.js +0 -1
- package/out/assets/upload-V--8p13l.js +0 -1
- package/out/assets/useGameRoom-C9hu7Cez.js +0 -1
- package/server/src/core/userSyncKeys.js +0 -36
- package/server/src/utils/noteBackup.js +0 -116
package/server/src/index.js
CHANGED
|
@@ -43,14 +43,9 @@ import {
|
|
|
43
43
|
cloneMetadataRecord,
|
|
44
44
|
} from './core/ownerMetadata.js'
|
|
45
45
|
import {
|
|
46
|
-
USER_SYNC_SCHEMA_VERSION,
|
|
47
|
-
USER_SYNC_NAMESPACE_PREFIX,
|
|
48
|
-
normalizeUserSyncKey,
|
|
49
|
-
deriveUserSyncId,
|
|
50
|
-
getUserSyncName,
|
|
51
46
|
getSyncTimestamp,
|
|
52
47
|
getNextSyncTimestamp,
|
|
53
|
-
} from './core/
|
|
48
|
+
} from './core/syncTimestamp.js'
|
|
54
49
|
import { createOfflineSwarm } from './node/offlineSwarm.js'
|
|
55
50
|
import {
|
|
56
51
|
sanitizeFilename,
|
|
@@ -163,11 +158,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
163
158
|
#channelCandidateCache = new Map()
|
|
164
159
|
#channelStreams = new Set()
|
|
165
160
|
|
|
166
|
-
#
|
|
167
|
-
#userSyncCores = new Map()
|
|
168
|
-
#userSyncCoreOffsets = new Map()
|
|
169
|
-
#userSyncDiscoveries = new Map()
|
|
170
|
-
#userSyncMetadata = { sessions: {}, clocks: {}, profiles: {} }
|
|
161
|
+
#accountMetadata = { profiles: {} }
|
|
171
162
|
|
|
172
163
|
#chatSwarm = null
|
|
173
164
|
|
|
@@ -365,7 +356,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
365
356
|
}
|
|
366
357
|
}
|
|
367
358
|
|
|
368
|
-
this.#
|
|
359
|
+
this.#accountMetadata = this.#loadAccountMetadata()
|
|
369
360
|
|
|
370
361
|
this.#initialized = true
|
|
371
362
|
console.log(`[MostBox] Engine initialized successfully`)
|
|
@@ -429,20 +420,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
429
420
|
this.#channelStreams.clear()
|
|
430
421
|
this.#channels = []
|
|
431
422
|
|
|
432
|
-
for (const [, coresMap] of this.#userSyncCores) {
|
|
433
|
-
for (const [, core] of coresMap) {
|
|
434
|
-
try {
|
|
435
|
-
await core.close()
|
|
436
|
-
} catch (err) {
|
|
437
|
-
console.warn('[MostBox] Failed to close user sync core:', err.message)
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
this.#userSyncSessions.clear()
|
|
442
|
-
this.#userSyncCores.clear()
|
|
443
|
-
this.#userSyncCoreOffsets.clear()
|
|
444
|
-
this.#userSyncDiscoveries.clear()
|
|
445
|
-
|
|
446
423
|
if (this.#store) {
|
|
447
424
|
await this.#store.close()
|
|
448
425
|
this.#store = null
|
|
@@ -649,9 +626,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
649
626
|
}
|
|
650
627
|
|
|
651
628
|
this.emit('publish:success', result)
|
|
652
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
653
|
-
file: this.#formatFileForSync(fileRecord, 'active'),
|
|
654
|
-
})
|
|
655
629
|
return result
|
|
656
630
|
}
|
|
657
631
|
|
|
@@ -1018,10 +992,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1018
992
|
})
|
|
1019
993
|
|
|
1020
994
|
this.emit('download:success', result)
|
|
1021
|
-
const syncedFile = publishedBucket.find(file => file.cid === cidString)
|
|
1022
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
1023
|
-
file: this.#formatFileForSync(syncedFile, 'active'),
|
|
1024
|
-
})
|
|
1025
995
|
return result
|
|
1026
996
|
}
|
|
1027
997
|
} finally {
|
|
@@ -1193,9 +1163,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1193
1163
|
files[index].starred = !files[index].starred
|
|
1194
1164
|
files[index].syncUpdatedAt = getNextSyncTimestamp(files[index].syncUpdatedAt)
|
|
1195
1165
|
this.#savePublishedMetadata()
|
|
1196
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
1197
|
-
file: this.#formatFileForSync(files[index], 'active'),
|
|
1198
|
-
})
|
|
1199
1166
|
return {
|
|
1200
1167
|
cid,
|
|
1201
1168
|
starred: files[index].starred,
|
|
@@ -1236,9 +1203,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1236
1203
|
files.splice(index, 1)
|
|
1237
1204
|
this.#setPublishedBucket(ownerAddress, files)
|
|
1238
1205
|
this.#savePublishedMetadata()
|
|
1239
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:trash', {
|
|
1240
|
-
file: this.#formatFileForSync(trashRecord, 'trash'),
|
|
1241
|
-
})
|
|
1242
1206
|
|
|
1243
1207
|
if (!this.#hasPublishedReference(fileRecord.cid)) {
|
|
1244
1208
|
await this.#leaveCidTopic(fileRecord.cid)
|
|
@@ -1302,9 +1266,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1302
1266
|
trashFiles.splice(index, 1)
|
|
1303
1267
|
this.#setTrashBucket(ownerAddress, trashFiles)
|
|
1304
1268
|
this.#saveTrashMetadata()
|
|
1305
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
1306
|
-
file: this.#formatFileForSync(publishedFiles[existingIndex], 'active'),
|
|
1307
|
-
})
|
|
1308
1269
|
return this.listPublishedFiles({ ownerAddress })
|
|
1309
1270
|
}
|
|
1310
1271
|
|
|
@@ -1348,9 +1309,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1348
1309
|
source: fileRecord.source || 'published',
|
|
1349
1310
|
})
|
|
1350
1311
|
}
|
|
1351
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
1352
|
-
file: this.#formatFileForSync(publishedRecord, 'active'),
|
|
1353
|
-
})
|
|
1354
1312
|
|
|
1355
1313
|
return this.listPublishedFiles({ ownerAddress })
|
|
1356
1314
|
}
|
|
@@ -1373,10 +1331,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1373
1331
|
trashFiles.splice(index, 1)
|
|
1374
1332
|
this.#setTrashBucket(ownerAddress, trashFiles)
|
|
1375
1333
|
this.#saveTrashMetadata()
|
|
1376
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:delete', {
|
|
1377
|
-
cid: fileRecord.cid,
|
|
1378
|
-
syncUpdatedAt: getNextSyncTimestamp(fileRecord.syncUpdatedAt),
|
|
1379
|
-
})
|
|
1380
1334
|
|
|
1381
1335
|
if (!this.#hasAnyUserReference(fileRecord.cid)) {
|
|
1382
1336
|
try {
|
|
@@ -1404,10 +1358,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1404
1358
|
this.#setTrashBucket(ownerAddress, [])
|
|
1405
1359
|
this.#saveTrashMetadata()
|
|
1406
1360
|
for (const fileRecord of removedTrash) {
|
|
1407
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:delete', {
|
|
1408
|
-
cid: fileRecord.cid,
|
|
1409
|
-
syncUpdatedAt: getNextSyncTimestamp(fileRecord.syncUpdatedAt),
|
|
1410
|
-
})
|
|
1411
1361
|
if (this.#hasAnyUserReference(fileRecord.cid)) continue
|
|
1412
1362
|
const driveName =
|
|
1413
1363
|
fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
|
|
@@ -1515,9 +1465,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1515
1465
|
files[index].syncUpdatedAt = getNextSyncTimestamp(files[index].syncUpdatedAt)
|
|
1516
1466
|
files[index].publishedAt = new Date(files[index].syncUpdatedAt).toISOString()
|
|
1517
1467
|
this.#savePublishedMetadata()
|
|
1518
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
1519
|
-
file: this.#formatFileForSync(files[index], 'active'),
|
|
1520
|
-
})
|
|
1521
1468
|
return {
|
|
1522
1469
|
cid,
|
|
1523
1470
|
fileName: safeFileName,
|
|
@@ -1569,11 +1516,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1569
1516
|
|
|
1570
1517
|
if (updatedFiles.length > 0) {
|
|
1571
1518
|
this.#savePublishedMetadata()
|
|
1572
|
-
for (const { file } of updates) {
|
|
1573
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'file:upsert', {
|
|
1574
|
-
file: this.#formatFileForSync(file, 'active'),
|
|
1575
|
-
})
|
|
1576
|
-
}
|
|
1577
1519
|
}
|
|
1578
1520
|
|
|
1579
1521
|
return { files: updatedFiles }
|
|
@@ -1788,8 +1730,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1788
1730
|
left.pipe(right).pipe(left)
|
|
1789
1731
|
this.#handleChannelConnection(leftChat).catch(() => {})
|
|
1790
1732
|
peerEngine.#handleChannelConnection(rightChat).catch(() => {})
|
|
1791
|
-
this.#exchangeUserSyncSessions(peerEngine).catch(() => {})
|
|
1792
|
-
peerEngine.#exchangeUserSyncSessions(this).catch(() => {})
|
|
1793
1733
|
|
|
1794
1734
|
return {
|
|
1795
1735
|
close: () => {
|
|
@@ -1801,96 +1741,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1801
1741
|
}
|
|
1802
1742
|
}
|
|
1803
1743
|
|
|
1804
|
-
/**
|
|
1805
|
-
* 启动当前账号的隐藏 user.sync 元数据同步。
|
|
1806
|
-
*/
|
|
1807
|
-
async startUserSync(ownerAddressInput, input = {}) {
|
|
1808
|
-
this.#ensureInitialized()
|
|
1809
|
-
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1810
|
-
if (!ownerAddress) {
|
|
1811
|
-
throw new ValidationError('valid owner address is required')
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
const syncTopicKey = normalizeUserSyncKey(input.syncTopicKey)
|
|
1815
|
-
const syncCipherKey = normalizeUserSyncKey(input.syncCipherKey)
|
|
1816
|
-
const syncMacKey = normalizeUserSyncKey(input.syncMacKey)
|
|
1817
|
-
if (!syncTopicKey || !syncCipherKey || !syncMacKey) {
|
|
1818
|
-
throw new ValidationError('valid user sync keys are required')
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
const syncId = deriveUserSyncId(syncTopicKey)
|
|
1822
|
-
const syncName = getUserSyncName(syncId)
|
|
1823
|
-
const persisted = this.#userSyncMetadata.sessions?.[ownerAddress]
|
|
1824
|
-
const session = {
|
|
1825
|
-
ownerAddress,
|
|
1826
|
-
syncId,
|
|
1827
|
-
syncName,
|
|
1828
|
-
syncTopicKey,
|
|
1829
|
-
syncCipherKey,
|
|
1830
|
-
syncMacKey,
|
|
1831
|
-
writerId:
|
|
1832
|
-
persisted?.syncId === syncId && persisted?.writerId
|
|
1833
|
-
? persisted.writerId
|
|
1834
|
-
: createChannelWriterId(),
|
|
1835
|
-
localWriterCoreKey:
|
|
1836
|
-
persisted?.syncId === syncId ? persisted.localWriterCoreKey || '' : '',
|
|
1837
|
-
writerCoreKeys:
|
|
1838
|
-
persisted?.syncId === syncId
|
|
1839
|
-
? uniqueStrings(persisted.writerCoreKeys)
|
|
1840
|
-
: [],
|
|
1841
|
-
startedAt: new Date().toISOString(),
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
this.#userSyncSessions.set(ownerAddress, session)
|
|
1845
|
-
await this.#openUserSyncRuntime(session)
|
|
1846
|
-
await this.#joinUserSyncDiscovery(session)
|
|
1847
|
-
this.#persistUserSyncSession(session)
|
|
1848
|
-
await this.#appendUserSyncSnapshot(ownerAddress, 'start')
|
|
1849
|
-
|
|
1850
|
-
return this.getUserSyncStatus(ownerAddress)
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
getUserSyncStatus(ownerAddressInput) {
|
|
1854
|
-
this.#ensureInitialized()
|
|
1855
|
-
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1856
|
-
if (!ownerAddress) {
|
|
1857
|
-
throw new ValidationError('valid owner address is required')
|
|
1858
|
-
}
|
|
1859
|
-
const session = this.#userSyncSessions.get(ownerAddress)
|
|
1860
|
-
if (!session) {
|
|
1861
|
-
const persisted = this.#userSyncMetadata.sessions?.[ownerAddress]
|
|
1862
|
-
return {
|
|
1863
|
-
enabled: false,
|
|
1864
|
-
ownerAddress,
|
|
1865
|
-
syncName: persisted?.syncName || '',
|
|
1866
|
-
syncId: persisted?.syncId || '',
|
|
1867
|
-
peerCount: 0,
|
|
1868
|
-
writerCoreKeys: uniqueStrings(persisted?.writerCoreKeys),
|
|
1869
|
-
localWriterCoreKey: persisted?.localWriterCoreKey || '',
|
|
1870
|
-
lastSyncedAt: persisted?.lastSyncedAt || '',
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
return {
|
|
1875
|
-
enabled: true,
|
|
1876
|
-
ownerAddress,
|
|
1877
|
-
syncName: session.syncName,
|
|
1878
|
-
syncId: session.syncId,
|
|
1879
|
-
peerCount: this.#chatSwarm?.connections?.size || 0,
|
|
1880
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
1881
|
-
localWriterCoreKey: session.localWriterCoreKey,
|
|
1882
|
-
lastSyncedAt:
|
|
1883
|
-
this.#userSyncMetadata.sessions?.[ownerAddress]?.lastSyncedAt || '',
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
1744
|
getUserProfile(ownerAddressInput) {
|
|
1888
1745
|
this.#ensureInitialized()
|
|
1889
1746
|
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1890
1747
|
if (!ownerAddress) {
|
|
1891
1748
|
throw new ValidationError('valid owner address is required')
|
|
1892
1749
|
}
|
|
1893
|
-
const profile = this.#
|
|
1750
|
+
const profile = this.#accountMetadata.profiles?.[ownerAddress]
|
|
1894
1751
|
return profile ? { ...profile } : null
|
|
1895
1752
|
}
|
|
1896
1753
|
|
|
@@ -1901,22 +1758,21 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1901
1758
|
throw new ValidationError('valid owner address is required')
|
|
1902
1759
|
}
|
|
1903
1760
|
const existing = this.getUserProfile(ownerAddress)
|
|
1904
|
-
const profile = this.#
|
|
1761
|
+
const profile = this.#normalizeAccountProfileRecord(
|
|
1905
1762
|
ownerAddress,
|
|
1906
1763
|
profileInput,
|
|
1907
|
-
getNextSyncTimestamp(existing?.
|
|
1764
|
+
getNextSyncTimestamp(existing?.updatedAt)
|
|
1908
1765
|
)
|
|
1909
1766
|
if (!profile) {
|
|
1910
1767
|
throw new ValidationError('valid profile is required')
|
|
1911
1768
|
}
|
|
1912
|
-
if (existing && profile.
|
|
1769
|
+
if (existing && profile.updatedAt <= existing.updatedAt) {
|
|
1913
1770
|
return { ...existing }
|
|
1914
1771
|
}
|
|
1915
1772
|
|
|
1916
|
-
this.#
|
|
1917
|
-
this.#
|
|
1918
|
-
this.#
|
|
1919
|
-
this.#appendUserSyncOpSoon(ownerAddress, 'profile:upsert', { profile })
|
|
1773
|
+
this.#accountMetadata.profiles = this.#accountMetadata.profiles || {}
|
|
1774
|
+
this.#accountMetadata.profiles[ownerAddress] = profile
|
|
1775
|
+
this.#saveAccountMetadata()
|
|
1920
1776
|
|
|
1921
1777
|
const changedChannels = this.#applyUserProfileToJoinedChannels(
|
|
1922
1778
|
ownerAddress,
|
|
@@ -1936,6 +1792,134 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1936
1792
|
return { ...profile }
|
|
1937
1793
|
}
|
|
1938
1794
|
|
|
1795
|
+
exportUserData(ownerAddressInput) {
|
|
1796
|
+
this.#ensureInitialized()
|
|
1797
|
+
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1798
|
+
if (!ownerAddress) {
|
|
1799
|
+
throw new ValidationError('valid owner address is required')
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const profile = this.getUserProfile(ownerAddress)
|
|
1803
|
+
const files = this.#getPublishedBucket(ownerAddress)
|
|
1804
|
+
.map(file => this.#formatAccountFileForBackup(file, 'active'))
|
|
1805
|
+
.filter(Boolean)
|
|
1806
|
+
const trashFiles = this.#getTrashBucket(ownerAddress)
|
|
1807
|
+
.map(file => this.#formatAccountFileForBackup(file, 'trash'))
|
|
1808
|
+
.filter(Boolean)
|
|
1809
|
+
const channels = this.#channels
|
|
1810
|
+
.filter(channel => this.#channelHasMember(channel, ownerAddress))
|
|
1811
|
+
.map(channel => this.#formatAccountChannelForBackup(channel, ownerAddress))
|
|
1812
|
+
.filter(Boolean)
|
|
1813
|
+
|
|
1814
|
+
return {
|
|
1815
|
+
type: 'mostbox.account-backup',
|
|
1816
|
+
schemaVersion: 1,
|
|
1817
|
+
ownerAddress,
|
|
1818
|
+
exportedAt: new Date().toISOString(),
|
|
1819
|
+
profile,
|
|
1820
|
+
files,
|
|
1821
|
+
trashFiles,
|
|
1822
|
+
channels,
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
async importUserData(ownerAddressInput, backupInput = {}, options = {}) {
|
|
1827
|
+
this.#ensureInitialized()
|
|
1828
|
+
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1829
|
+
if (!ownerAddress) {
|
|
1830
|
+
throw new ValidationError('valid owner address is required')
|
|
1831
|
+
}
|
|
1832
|
+
if (
|
|
1833
|
+
!backupInput ||
|
|
1834
|
+
typeof backupInput !== 'object' ||
|
|
1835
|
+
backupInput.type !== 'mostbox.account-backup' ||
|
|
1836
|
+
Number(backupInput.schemaVersion) !== 1
|
|
1837
|
+
) {
|
|
1838
|
+
throw new ValidationError('invalid account backup data')
|
|
1839
|
+
}
|
|
1840
|
+
if (normalizeOwnerAddress(backupInput.ownerAddress) !== ownerAddress) {
|
|
1841
|
+
throw new PermissionError('backup owner does not match current user')
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const result = {
|
|
1845
|
+
profileUpdated: false,
|
|
1846
|
+
filesAdded: 0,
|
|
1847
|
+
filesUpdated: 0,
|
|
1848
|
+
trashFilesAdded: 0,
|
|
1849
|
+
trashFilesUpdated: 0,
|
|
1850
|
+
channelsAdded: 0,
|
|
1851
|
+
channelsUpdated: 0,
|
|
1852
|
+
skipped: 0,
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const profileResult = this.#mergeAccountProfileRecord(
|
|
1856
|
+
ownerAddress,
|
|
1857
|
+
backupInput.profile,
|
|
1858
|
+
{
|
|
1859
|
+
overwrite: options.overwriteProfile === true,
|
|
1860
|
+
}
|
|
1861
|
+
)
|
|
1862
|
+
result.profileUpdated = profileResult.changed
|
|
1863
|
+
if (profileResult.skipped) result.skipped += 1
|
|
1864
|
+
|
|
1865
|
+
let filesChanged = false
|
|
1866
|
+
for (const file of Array.isArray(backupInput.files) ? backupInput.files : []) {
|
|
1867
|
+
const mergeResult = this.#mergeAccountFileRecord(ownerAddress, file, 'active')
|
|
1868
|
+
if (mergeResult.added) result.filesAdded += 1
|
|
1869
|
+
else if (mergeResult.updated) result.filesUpdated += 1
|
|
1870
|
+
else result.skipped += 1
|
|
1871
|
+
filesChanged = mergeResult.changed || filesChanged
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
for (const file of Array.isArray(backupInput.trashFiles)
|
|
1875
|
+
? backupInput.trashFiles
|
|
1876
|
+
: []) {
|
|
1877
|
+
const mergeResult = this.#mergeAccountFileRecord(ownerAddress, file, 'trash')
|
|
1878
|
+
if (mergeResult.added) result.trashFilesAdded += 1
|
|
1879
|
+
else if (mergeResult.updated) result.trashFilesUpdated += 1
|
|
1880
|
+
else result.skipped += 1
|
|
1881
|
+
filesChanged = mergeResult.changed || filesChanged
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
let channelsChanged = false
|
|
1885
|
+
for (const channel of Array.isArray(backupInput.channels)
|
|
1886
|
+
? backupInput.channels
|
|
1887
|
+
: []) {
|
|
1888
|
+
const mergeResult = await this.#mergeAccountChannelRecord(
|
|
1889
|
+
ownerAddress,
|
|
1890
|
+
channel
|
|
1891
|
+
)
|
|
1892
|
+
if (mergeResult.added) result.channelsAdded += 1
|
|
1893
|
+
else if (mergeResult.updated) result.channelsUpdated += 1
|
|
1894
|
+
else result.skipped += 1
|
|
1895
|
+
channelsChanged = mergeResult.changed || channelsChanged
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (filesChanged) {
|
|
1899
|
+
this.#savePublishedMetadata()
|
|
1900
|
+
this.#saveTrashMetadata()
|
|
1901
|
+
this.emit('user:metadata:updated', {
|
|
1902
|
+
ownerAddress,
|
|
1903
|
+
scope: 'files',
|
|
1904
|
+
})
|
|
1905
|
+
}
|
|
1906
|
+
if (channelsChanged) {
|
|
1907
|
+
this.#saveChannelsMetadata()
|
|
1908
|
+
this.emit('user:metadata:updated', {
|
|
1909
|
+
ownerAddress,
|
|
1910
|
+
scope: 'channels',
|
|
1911
|
+
})
|
|
1912
|
+
}
|
|
1913
|
+
if (result.profileUpdated) {
|
|
1914
|
+
this.emit('user:metadata:updated', {
|
|
1915
|
+
ownerAddress,
|
|
1916
|
+
scope: 'profile',
|
|
1917
|
+
})
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
return result
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1939
1923
|
async cacheFile(cid, options = {}) {
|
|
1940
1924
|
this.#ensureInitialized()
|
|
1941
1925
|
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
@@ -1955,6 +1939,290 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1955
1939
|
return result
|
|
1956
1940
|
}
|
|
1957
1941
|
|
|
1942
|
+
/**
|
|
1943
|
+
* 按 CID 将内容下载到指定路径,不写入用户文件列表。
|
|
1944
|
+
* 用于桌面客户端更新等内部缓存场景。
|
|
1945
|
+
* @param {object} input - 下载参数
|
|
1946
|
+
* @param {string} input.cid - 文件 CID
|
|
1947
|
+
* @param {string} input.targetPath - 目标文件路径
|
|
1948
|
+
* @param {string} [input.fileName] - 持有记录展示名
|
|
1949
|
+
* @param {string} [input.taskId] - 下载任务 ID
|
|
1950
|
+
* @param {number} [input.timeout] - 等待 P2P 内容的超时时间
|
|
1951
|
+
* @param {number} [input.streamReadTimeout] - 下载流无进度超时时间
|
|
1952
|
+
* @param {boolean} [input.overwrite=true] - 是否覆盖已存在目标文件
|
|
1953
|
+
* @param {string} [input.source='update'] - holding 来源
|
|
1954
|
+
*/
|
|
1955
|
+
async downloadCidToPath(input = {}) {
|
|
1956
|
+
this.#ensureInitialized()
|
|
1957
|
+
|
|
1958
|
+
const cid = input.cid
|
|
1959
|
+
const targetPath = input.targetPath
|
|
1960
|
+
if (!cid) throw new ValidationError('cid is required')
|
|
1961
|
+
if (!targetPath) throw new ValidationError('targetPath is required')
|
|
1962
|
+
|
|
1963
|
+
this.#getCidInfo(cid)
|
|
1964
|
+
const taskId =
|
|
1965
|
+
input.taskId || `cid_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
1966
|
+
const taskState = { aborted: false, readStream: null, writeStream: null }
|
|
1967
|
+
const timeout = input.timeout || this.#options.downloadTimeout
|
|
1968
|
+
const streamReadTimeout =
|
|
1969
|
+
input.streamReadTimeout ?? STREAM_READ_TIMEOUT
|
|
1970
|
+
const fileName = sanitizeFilename(input.fileName || path.basename(targetPath))
|
|
1971
|
+
const overwrite = input.overwrite !== false
|
|
1972
|
+
const resolvedTargetPath = path.resolve(targetPath)
|
|
1973
|
+
const targetDir = path.dirname(resolvedTargetPath)
|
|
1974
|
+
const tempPath = `${resolvedTargetPath}.part`
|
|
1975
|
+
|
|
1976
|
+
this.#activeDownloads.set(taskId, taskState)
|
|
1977
|
+
|
|
1978
|
+
try {
|
|
1979
|
+
const writableCheck = await checkDirectoryWritable(targetDir)
|
|
1980
|
+
if (!writableCheck.writable) {
|
|
1981
|
+
throw new PermissionError(writableCheck.error)
|
|
1982
|
+
}
|
|
1983
|
+
if (!overwrite && fs.existsSync(resolvedTargetPath)) {
|
|
1984
|
+
throw new ConflictError(`File already exists: ${resolvedTargetPath}`)
|
|
1985
|
+
}
|
|
1986
|
+
fs.rmSync(tempPath, { force: true })
|
|
1987
|
+
|
|
1988
|
+
const { driveName } = this.#getCidInfo(cid)
|
|
1989
|
+
let drive = this.#drives.get(driveName)
|
|
1990
|
+
if (!drive) {
|
|
1991
|
+
drive = await this.#getOrCreateDrive(driveName, {
|
|
1992
|
+
server: true,
|
|
1993
|
+
client: true,
|
|
1994
|
+
})
|
|
1995
|
+
this.emit('download:status', { taskId, status: 'connecting' })
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
await this.#joinCidTopicInternal(cid, {
|
|
1999
|
+
server: false,
|
|
2000
|
+
client: true,
|
|
2001
|
+
})
|
|
2002
|
+
|
|
2003
|
+
if (taskState.aborted) throw new Error('Download cancelled')
|
|
2004
|
+
this.emit('download:status', { taskId, status: 'finding-peers' })
|
|
2005
|
+
|
|
2006
|
+
const driveKey = '/' + cid
|
|
2007
|
+
const entry = await this.#waitForDriveEntry(
|
|
2008
|
+
drive,
|
|
2009
|
+
driveKey,
|
|
2010
|
+
timeout,
|
|
2011
|
+
taskId,
|
|
2012
|
+
taskState
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
if (!entry) {
|
|
2016
|
+
throw new PeerNotFoundError(
|
|
2017
|
+
'当前没有发现可下载的在线种子,请稍后重试或确认发布者在线'
|
|
2018
|
+
)
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
let totalBytes = 0
|
|
2022
|
+
try {
|
|
2023
|
+
const stat = await drive.entry(entry.key)
|
|
2024
|
+
totalBytes = Number(stat?.value?.blob?.byteLength) || 0
|
|
2025
|
+
} catch {}
|
|
2026
|
+
|
|
2027
|
+
this.emit('download:status', {
|
|
2028
|
+
taskId,
|
|
2029
|
+
status: 'downloading',
|
|
2030
|
+
file: fileName,
|
|
2031
|
+
size: totalBytes ? formatFileSize(totalBytes) : null,
|
|
2032
|
+
})
|
|
2033
|
+
|
|
2034
|
+
const rs = drive.createReadStream(entry.key)
|
|
2035
|
+
const ws = fs.createWriteStream(tempPath)
|
|
2036
|
+
taskState.readStream = rs
|
|
2037
|
+
taskState.writeStream = ws
|
|
2038
|
+
|
|
2039
|
+
let loadedBytes = 0
|
|
2040
|
+
let lastProgressUpdate = 0
|
|
2041
|
+
|
|
2042
|
+
await new Promise((resolve, reject) => {
|
|
2043
|
+
let settled = false
|
|
2044
|
+
let readTimer = null
|
|
2045
|
+
|
|
2046
|
+
const clearReadTimer = () => {
|
|
2047
|
+
if (readTimer) {
|
|
2048
|
+
clearTimeout(readTimer)
|
|
2049
|
+
readTimer = null
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const fail = err => {
|
|
2054
|
+
if (settled) return
|
|
2055
|
+
settled = true
|
|
2056
|
+
clearReadTimer()
|
|
2057
|
+
rs.destroy(err)
|
|
2058
|
+
ws.destroy()
|
|
2059
|
+
fs.unlink(tempPath, () => {})
|
|
2060
|
+
reject(err)
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const complete = () => {
|
|
2064
|
+
if (settled) return
|
|
2065
|
+
settled = true
|
|
2066
|
+
clearReadTimer()
|
|
2067
|
+
resolve()
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
const resetReadTimer = () => {
|
|
2071
|
+
clearReadTimer()
|
|
2072
|
+
if (streamReadTimeout > 0) {
|
|
2073
|
+
readTimer = setTimeout(() => {
|
|
2074
|
+
fail(
|
|
2075
|
+
new Error(
|
|
2076
|
+
`Download stalled: no data received for ${streamReadTimeout / 1000}s`
|
|
2077
|
+
)
|
|
2078
|
+
)
|
|
2079
|
+
}, streamReadTimeout)
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
resetReadTimer()
|
|
2084
|
+
|
|
2085
|
+
rs.on('data', chunk => {
|
|
2086
|
+
if (taskState.aborted) {
|
|
2087
|
+
fail(new Error('Download cancelled'))
|
|
2088
|
+
return
|
|
2089
|
+
}
|
|
2090
|
+
resetReadTimer()
|
|
2091
|
+
loadedBytes += chunk.length
|
|
2092
|
+
const now = Date.now()
|
|
2093
|
+
if (
|
|
2094
|
+
totalBytes > 0 &&
|
|
2095
|
+
now - lastProgressUpdate > PROGRESS_THROTTLE
|
|
2096
|
+
) {
|
|
2097
|
+
lastProgressUpdate = now
|
|
2098
|
+
this.emit('download:progress', {
|
|
2099
|
+
taskId,
|
|
2100
|
+
loaded: loadedBytes,
|
|
2101
|
+
total: totalBytes,
|
|
2102
|
+
percent: Math.round((loadedBytes / totalBytes) * 100),
|
|
2103
|
+
})
|
|
2104
|
+
}
|
|
2105
|
+
})
|
|
2106
|
+
|
|
2107
|
+
rs.pipe(ws)
|
|
2108
|
+
ws.on('finish', complete)
|
|
2109
|
+
ws.on('error', fail)
|
|
2110
|
+
rs.on('error', fail)
|
|
2111
|
+
rs.on('close', () => {
|
|
2112
|
+
if (taskState.aborted) fail(new Error('Download cancelled'))
|
|
2113
|
+
})
|
|
2114
|
+
ws.on('close', () => {
|
|
2115
|
+
if (taskState.aborted) fail(new Error('Download cancelled'))
|
|
2116
|
+
})
|
|
2117
|
+
})
|
|
2118
|
+
|
|
2119
|
+
if (taskState.aborted) throw new Error('Download cancelled')
|
|
2120
|
+
|
|
2121
|
+
this.emit('download:status', { taskId, status: 'verifying' })
|
|
2122
|
+
const { cid: downloadedCid, size } = await calculateCid(tempPath)
|
|
2123
|
+
if (downloadedCid.toString() !== cid) {
|
|
2124
|
+
fs.unlinkSync(tempPath)
|
|
2125
|
+
throw new IntegrityError(
|
|
2126
|
+
`File content CID mismatch. Expected ${cid}, got ${downloadedCid.toString()}.`
|
|
2127
|
+
)
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (fs.existsSync(resolvedTargetPath)) {
|
|
2131
|
+
fs.rmSync(resolvedTargetPath, { force: true })
|
|
2132
|
+
}
|
|
2133
|
+
fs.renameSync(tempPath, resolvedTargetPath)
|
|
2134
|
+
|
|
2135
|
+
await this.seedCidFileFromPath(cid, resolvedTargetPath, {
|
|
2136
|
+
fileName,
|
|
2137
|
+
source: input.source || 'update',
|
|
2138
|
+
})
|
|
2139
|
+
|
|
2140
|
+
const result = {
|
|
2141
|
+
taskId,
|
|
2142
|
+
cid,
|
|
2143
|
+
fileName,
|
|
2144
|
+
savedPath: resolvedTargetPath,
|
|
2145
|
+
size,
|
|
2146
|
+
}
|
|
2147
|
+
this.emit('download:success', result)
|
|
2148
|
+
return result
|
|
2149
|
+
} finally {
|
|
2150
|
+
this.#activeDownloads.delete(taskId)
|
|
2151
|
+
fs.rmSync(tempPath, { force: true })
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* 将本地文件按既有 CID 写入 Hyperdrive 并记录 holding,不写入用户文件列表。
|
|
2157
|
+
* @param {string} cid - 文件 CID
|
|
2158
|
+
* @param {string} filePath - 本地文件路径
|
|
2159
|
+
* @param {object} [options] - 选项
|
|
2160
|
+
* @param {string} [options.fileName] - 持有记录展示名
|
|
2161
|
+
* @param {string} [options.source='update'] - holding 来源
|
|
2162
|
+
*/
|
|
2163
|
+
async seedCidFileFromPath(cid, filePath, options = {}) {
|
|
2164
|
+
this.#ensureInitialized()
|
|
2165
|
+
if (!cid) throw new ValidationError('cid is required')
|
|
2166
|
+
if (!filePath) throw new ValidationError('filePath is required')
|
|
2167
|
+
|
|
2168
|
+
this.#getCidInfo(cid)
|
|
2169
|
+
const sizeValidation = await validateFileSize(
|
|
2170
|
+
filePath,
|
|
2171
|
+
this.#options.maxFileSize
|
|
2172
|
+
)
|
|
2173
|
+
if (!sizeValidation.valid) {
|
|
2174
|
+
throw new FileSizeError(sizeValidation.error, sizeValidation.size)
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
const { cid: actualCid } = await calculateCid(filePath)
|
|
2178
|
+
const actualCidString = actualCid.toString()
|
|
2179
|
+
if (actualCidString !== cid) {
|
|
2180
|
+
throw new IntegrityError(
|
|
2181
|
+
`File content CID mismatch. Expected ${cid}, got ${actualCidString}.`
|
|
2182
|
+
)
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
const existingHolding = this.#holdings.find(holding => holding.cid === cid)
|
|
2186
|
+
if (!existingHolding) {
|
|
2187
|
+
this.#checkCapacity(sizeValidation.size || 0)
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
const { driveName } = this.#getCidInfo(cid)
|
|
2191
|
+
const drive = await this.#getOrCreateDrive(driveName, {
|
|
2192
|
+
server: true,
|
|
2193
|
+
client: false,
|
|
2194
|
+
})
|
|
2195
|
+
const driveKey = '/' + cid
|
|
2196
|
+
const localContent = await this.#getLocalCidContent(cid, {
|
|
2197
|
+
public: true,
|
|
2198
|
+
allowHoldingFallback: true,
|
|
2199
|
+
})
|
|
2200
|
+
|
|
2201
|
+
if (!localContent) {
|
|
2202
|
+
const readStream = fs.createReadStream(filePath)
|
|
2203
|
+
const writeStream = drive.createWriteStream(driveKey)
|
|
2204
|
+
await new Promise((resolve, reject) => {
|
|
2205
|
+
readStream.pipe(writeStream)
|
|
2206
|
+
writeStream.on('finish', resolve)
|
|
2207
|
+
writeStream.on('error', reject)
|
|
2208
|
+
readStream.on('error', reject)
|
|
2209
|
+
})
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
await this.#joinCidTopicInternal(cid, {
|
|
2213
|
+
server: true,
|
|
2214
|
+
client: false,
|
|
2215
|
+
})
|
|
2216
|
+
|
|
2217
|
+
return this.#upsertHolding({
|
|
2218
|
+
cid,
|
|
2219
|
+
fileName: sanitizeFilename(options.fileName || path.basename(filePath)),
|
|
2220
|
+
size: sizeValidation.size || 0,
|
|
2221
|
+
driveName,
|
|
2222
|
+
source: options.source || 'update',
|
|
2223
|
+
})
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1958
2226
|
/**
|
|
1959
2227
|
* 读取已发布文件的内容(用于预览)
|
|
1960
2228
|
* Hyperdrive 中用 CID 作为 key 存储
|
|
@@ -2202,7 +2470,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2202
2470
|
if (writerKeysChanged || memberChanged) {
|
|
2203
2471
|
existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
|
|
2204
2472
|
this.#saveChannelsMetadata()
|
|
2205
|
-
this.#appendUserSyncChannelUpsertSoon(existing, ownerAddress)
|
|
2206
2473
|
this.#broadcastChannelHello()
|
|
2207
2474
|
}
|
|
2208
2475
|
return this.#formatChannelForResponse(existing, ownerAddress)
|
|
@@ -2212,8 +2479,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2212
2479
|
channelType,
|
|
2213
2480
|
options
|
|
2214
2481
|
)
|
|
2215
|
-
const joinedChannel = this.#resolveChannel(joined.channelKey, ownerAddress)
|
|
2216
|
-
this.#appendUserSyncChannelUpsertSoon(joinedChannel, ownerAddress)
|
|
2217
2482
|
return joined
|
|
2218
2483
|
}
|
|
2219
2484
|
const joined = await this.#joinChannelFromCandidate(
|
|
@@ -2221,8 +2486,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2221
2486
|
channelType,
|
|
2222
2487
|
options
|
|
2223
2488
|
)
|
|
2224
|
-
const joinedChannel = this.#resolveChannel(joined.channelKey, ownerAddress)
|
|
2225
|
-
this.#appendUserSyncChannelUpsertSoon(joinedChannel, ownerAddress)
|
|
2226
2489
|
return joined
|
|
2227
2490
|
}
|
|
2228
2491
|
|
|
@@ -2238,7 +2501,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2238
2501
|
channelId: channelInfo.channelId,
|
|
2239
2502
|
key: channelInfo.channelKey,
|
|
2240
2503
|
})
|
|
2241
|
-
this.#appendUserSyncChannelUpsertSoon(channelInfo, ownerAddress)
|
|
2242
2504
|
|
|
2243
2505
|
return this.#formatChannelForResponse(channelInfo, ownerAddress)
|
|
2244
2506
|
}
|
|
@@ -2274,7 +2536,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2274
2536
|
if (writerKeysChanged || memberChanged) {
|
|
2275
2537
|
existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
|
|
2276
2538
|
this.#saveChannelsMetadata()
|
|
2277
|
-
this.#appendUserSyncChannelUpsertSoon(existing, options.ownerAddress)
|
|
2278
2539
|
this.#broadcastChannelHello()
|
|
2279
2540
|
}
|
|
2280
2541
|
return this.#formatChannelForResponse(existing, options.ownerAddress)
|
|
@@ -2289,11 +2550,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2289
2550
|
...options,
|
|
2290
2551
|
channelKey,
|
|
2291
2552
|
})
|
|
2292
|
-
const joinedChannel = this.#resolveChannel(
|
|
2293
|
-
joined.channelKey,
|
|
2294
|
-
options.ownerAddress
|
|
2295
|
-
)
|
|
2296
|
-
this.#appendUserSyncChannelUpsertSoon(joinedChannel, options.ownerAddress)
|
|
2297
2553
|
return joined
|
|
2298
2554
|
}
|
|
2299
2555
|
|
|
@@ -2318,7 +2574,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2318
2574
|
)
|
|
2319
2575
|
const syncUpdatedAt = getNextSyncTimestamp(channel.syncUpdatedAt)
|
|
2320
2576
|
channel.syncUpdatedAt = syncUpdatedAt
|
|
2321
|
-
this.#appendUserSyncChannelLeaveSoon(channel, ownerAddress, syncUpdatedAt)
|
|
2322
2577
|
if (channel.members.length > 0) {
|
|
2323
2578
|
this.#saveChannelsMetadata()
|
|
2324
2579
|
return this.listChannels({ ownerAddress })
|
|
@@ -2426,7 +2681,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2426
2681
|
|
|
2427
2682
|
channel.syncUpdatedAt = getNextSyncTimestamp(channel.syncUpdatedAt)
|
|
2428
2683
|
this.#saveChannelsMetadata()
|
|
2429
|
-
this.#appendUserSyncChannelUpsertSoon(channel, ownerAddress)
|
|
2430
2684
|
return trimmed
|
|
2431
2685
|
}
|
|
2432
2686
|
|
|
@@ -2452,7 +2706,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2452
2706
|
|
|
2453
2707
|
channel.syncUpdatedAt = getNextSyncTimestamp(channel.syncUpdatedAt)
|
|
2454
2708
|
this.#saveChannelsMetadata()
|
|
2455
|
-
this.#appendUserSyncChannelUpsertSoon(channel, ownerAddress)
|
|
2456
2709
|
return Boolean(channel.pinnedBy[ownerAddress])
|
|
2457
2710
|
}
|
|
2458
2711
|
|
|
@@ -3210,403 +3463,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3210
3463
|
}
|
|
3211
3464
|
}
|
|
3212
3465
|
|
|
3213
|
-
#
|
|
3214
|
-
return this.#store.namespace(session.syncName)
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
async #openUserSyncRuntime(session) {
|
|
3218
|
-
const ns = this.#getUserSyncNamespace(session)
|
|
3219
|
-
const localCore = session.localWriterCoreKey
|
|
3220
|
-
? ns.get({
|
|
3221
|
-
key: b4a.from(session.localWriterCoreKey, 'hex'),
|
|
3222
|
-
valueEncoding: 'json',
|
|
3223
|
-
})
|
|
3224
|
-
: ns.get({
|
|
3225
|
-
name: `writer-${session.writerId}`,
|
|
3226
|
-
valueEncoding: 'json',
|
|
3227
|
-
})
|
|
3228
|
-
await localCore.ready()
|
|
3229
|
-
session.localWriterCoreKey = b4a.toString(localCore.key, 'hex')
|
|
3230
|
-
session.writerCoreKeys = uniqueStrings([
|
|
3231
|
-
...session.writerCoreKeys,
|
|
3232
|
-
session.localWriterCoreKey,
|
|
3233
|
-
])
|
|
3234
|
-
|
|
3235
|
-
if (!this.#userSyncCores.has(session.ownerAddress)) {
|
|
3236
|
-
this.#userSyncCores.set(session.ownerAddress, new Map())
|
|
3237
|
-
}
|
|
3238
|
-
this.#userSyncCores
|
|
3239
|
-
.get(session.ownerAddress)
|
|
3240
|
-
.set(session.localWriterCoreKey, localCore)
|
|
3241
|
-
this.#setupUserSyncAppendListener(
|
|
3242
|
-
localCore,
|
|
3243
|
-
session,
|
|
3244
|
-
session.localWriterCoreKey
|
|
3245
|
-
)
|
|
3246
|
-
|
|
3247
|
-
for (const writerCoreKey of session.writerCoreKeys) {
|
|
3248
|
-
if (writerCoreKey && writerCoreKey !== session.localWriterCoreKey) {
|
|
3249
|
-
await this.#openRemoteUserSyncCore(session, writerCoreKey)
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
async #joinUserSyncDiscovery(session) {
|
|
3255
|
-
if (this.#userSyncDiscoveries.has(session.ownerAddress)) return
|
|
3256
|
-
const discoveryKey = this.#generateUserSyncDiscoveryKey(session.syncId)
|
|
3257
|
-
const appDiscovery = this.#swarm.join(discoveryKey, {
|
|
3258
|
-
server: true,
|
|
3259
|
-
client: true,
|
|
3260
|
-
})
|
|
3261
|
-
const chatDiscovery = this.#chatSwarm.join(
|
|
3262
|
-
discoveryKey,
|
|
3263
|
-
{ server: true, client: true }
|
|
3264
|
-
)
|
|
3265
|
-
this.#userSyncDiscoveries.set(session.ownerAddress, {
|
|
3266
|
-
appDiscovery,
|
|
3267
|
-
chatDiscovery,
|
|
3268
|
-
})
|
|
3269
|
-
}
|
|
3270
|
-
|
|
3271
|
-
async #openRemoteUserSyncCore(session, writerCoreKey) {
|
|
3272
|
-
const normalizedCoreKey = String(writerCoreKey || '').trim()
|
|
3273
|
-
if (
|
|
3274
|
-
!normalizedCoreKey ||
|
|
3275
|
-
normalizedCoreKey === session.localWriterCoreKey
|
|
3276
|
-
) {
|
|
3277
|
-
return null
|
|
3278
|
-
}
|
|
3279
|
-
if (!this.#userSyncCores.has(session.ownerAddress)) {
|
|
3280
|
-
this.#userSyncCores.set(session.ownerAddress, new Map())
|
|
3281
|
-
}
|
|
3282
|
-
const coresMap = this.#userSyncCores.get(session.ownerAddress)
|
|
3283
|
-
if (coresMap.has(normalizedCoreKey)) return coresMap.get(normalizedCoreKey)
|
|
3284
|
-
|
|
3285
|
-
const ns = this.#getUserSyncNamespace(session)
|
|
3286
|
-
const core = ns.get({
|
|
3287
|
-
key: b4a.from(normalizedCoreKey, 'hex'),
|
|
3288
|
-
valueEncoding: 'json',
|
|
3289
|
-
})
|
|
3290
|
-
await core.ready()
|
|
3291
|
-
coresMap.set(normalizedCoreKey, core)
|
|
3292
|
-
session.writerCoreKeys = uniqueStrings([
|
|
3293
|
-
...session.writerCoreKeys,
|
|
3294
|
-
normalizedCoreKey,
|
|
3295
|
-
])
|
|
3296
|
-
this.#persistUserSyncSession(session)
|
|
3297
|
-
this.#setupUserSyncAppendListener(core, session, normalizedCoreKey)
|
|
3298
|
-
return core
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
#setupUserSyncAppendListener(core, session, coreKey) {
|
|
3302
|
-
const offsetKey = `${session.ownerAddress}:${coreKey}`
|
|
3303
|
-
let processing = false
|
|
3304
|
-
const processEntries = async () => {
|
|
3305
|
-
if (processing) return
|
|
3306
|
-
processing = true
|
|
3307
|
-
try {
|
|
3308
|
-
let index = this.#userSyncCoreOffsets.get(offsetKey) || 0
|
|
3309
|
-
while (index < core.length) {
|
|
3310
|
-
const entry = await core.get(index)
|
|
3311
|
-
await this.#applyUserSyncEntry(session, entry)
|
|
3312
|
-
index += 1
|
|
3313
|
-
this.#userSyncCoreOffsets.set(offsetKey, index)
|
|
3314
|
-
}
|
|
3315
|
-
} finally {
|
|
3316
|
-
processing = false
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
core.on('append', () => {
|
|
3321
|
-
processEntries().catch(err => {
|
|
3322
|
-
console.warn('[MostBox] Failed to process user sync entry:', err.message)
|
|
3323
|
-
})
|
|
3324
|
-
})
|
|
3325
|
-
processEntries().catch(err => {
|
|
3326
|
-
console.warn('[MostBox] Failed to process user sync entries:', err.message)
|
|
3327
|
-
})
|
|
3328
|
-
}
|
|
3329
|
-
|
|
3330
|
-
#encodeUserSyncEntry(session, op) {
|
|
3331
|
-
const nonce = crypto.randomBytes(12)
|
|
3332
|
-
const cipher = crypto.createCipheriv(
|
|
3333
|
-
'aes-256-gcm',
|
|
3334
|
-
Buffer.from(session.syncCipherKey, 'hex'),
|
|
3335
|
-
nonce
|
|
3336
|
-
)
|
|
3337
|
-
const encrypted = Buffer.concat([
|
|
3338
|
-
cipher.update(JSON.stringify(op), 'utf8'),
|
|
3339
|
-
cipher.final(),
|
|
3340
|
-
])
|
|
3341
|
-
const tag = cipher.getAuthTag()
|
|
3342
|
-
const body = `${nonce.toString('hex')}.${encrypted.toString('hex')}.${tag.toString('hex')}`
|
|
3343
|
-
const mac = crypto
|
|
3344
|
-
.createHmac('sha256', Buffer.from(session.syncMacKey, 'hex'))
|
|
3345
|
-
.update(body)
|
|
3346
|
-
.digest('hex')
|
|
3347
|
-
|
|
3348
|
-
return {
|
|
3349
|
-
type: 'user-sync-op',
|
|
3350
|
-
schemaVersion: USER_SYNC_SCHEMA_VERSION,
|
|
3351
|
-
ownerAddress: session.ownerAddress,
|
|
3352
|
-
syncId: session.syncId,
|
|
3353
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
3354
|
-
body,
|
|
3355
|
-
mac,
|
|
3356
|
-
createdAt: new Date(op.timestamp).toISOString(),
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
#decodeUserSyncEntry(session, entry) {
|
|
3361
|
-
if (
|
|
3362
|
-
!entry ||
|
|
3363
|
-
entry.type !== 'user-sync-op' ||
|
|
3364
|
-
entry.syncId !== session.syncId ||
|
|
3365
|
-
entry.ownerAddress !== session.ownerAddress ||
|
|
3366
|
-
Number(entry.schemaVersion) !== USER_SYNC_SCHEMA_VERSION
|
|
3367
|
-
) {
|
|
3368
|
-
return null
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
const expectedMac = crypto
|
|
3372
|
-
.createHmac('sha256', Buffer.from(session.syncMacKey, 'hex'))
|
|
3373
|
-
.update(String(entry.body || ''))
|
|
3374
|
-
.digest('hex')
|
|
3375
|
-
if (expectedMac !== entry.mac) return null
|
|
3376
|
-
|
|
3377
|
-
const [nonceHex, encryptedHex, tagHex] = String(entry.body || '').split('.')
|
|
3378
|
-
if (!nonceHex || !encryptedHex || !tagHex) return null
|
|
3379
|
-
|
|
3380
|
-
try {
|
|
3381
|
-
const decipher = crypto.createDecipheriv(
|
|
3382
|
-
'aes-256-gcm',
|
|
3383
|
-
Buffer.from(session.syncCipherKey, 'hex'),
|
|
3384
|
-
Buffer.from(nonceHex, 'hex')
|
|
3385
|
-
)
|
|
3386
|
-
decipher.setAuthTag(Buffer.from(tagHex, 'hex'))
|
|
3387
|
-
const decrypted = Buffer.concat([
|
|
3388
|
-
decipher.update(Buffer.from(encryptedHex, 'hex')),
|
|
3389
|
-
decipher.final(),
|
|
3390
|
-
])
|
|
3391
|
-
return JSON.parse(decrypted.toString('utf8'))
|
|
3392
|
-
} catch {
|
|
3393
|
-
return null
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
async #appendUserSyncOp(ownerAddressInput, kind, payload = {}) {
|
|
3398
|
-
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
3399
|
-
const session = this.#userSyncSessions.get(ownerAddress)
|
|
3400
|
-
if (!session?.localWriterCoreKey) return null
|
|
3401
|
-
const coresMap = this.#userSyncCores.get(ownerAddress)
|
|
3402
|
-
const core = coresMap?.get(session.localWriterCoreKey)
|
|
3403
|
-
if (!core) return null
|
|
3404
|
-
|
|
3405
|
-
const op = {
|
|
3406
|
-
opId: `${Date.now()}-${crypto.randomBytes(6).toString('hex')}`,
|
|
3407
|
-
schemaVersion: USER_SYNC_SCHEMA_VERSION,
|
|
3408
|
-
kind,
|
|
3409
|
-
ownerAddress,
|
|
3410
|
-
timestamp: Date.now(),
|
|
3411
|
-
payload,
|
|
3412
|
-
}
|
|
3413
|
-
this.#markUserSyncClockForOp(op)
|
|
3414
|
-
await core.append(this.#encodeUserSyncEntry(session, op))
|
|
3415
|
-
this.#touchUserSyncSession(session)
|
|
3416
|
-
return op
|
|
3417
|
-
}
|
|
3418
|
-
|
|
3419
|
-
#appendUserSyncOpSoon(ownerAddress, kind, payload = {}) {
|
|
3420
|
-
this.#appendUserSyncOp(ownerAddress, kind, payload).catch(err => {
|
|
3421
|
-
console.warn('[MostBox] Failed to append user sync op:', err.message)
|
|
3422
|
-
})
|
|
3423
|
-
}
|
|
3424
|
-
|
|
3425
|
-
async #appendUserSyncSnapshot(ownerAddressInput, reason = 'snapshot') {
|
|
3426
|
-
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
3427
|
-
if (!this.#userSyncSessions.has(ownerAddress)) return null
|
|
3428
|
-
const files = this.#getPublishedBucket(ownerAddress).map(file =>
|
|
3429
|
-
this.#formatFileForSync(file, 'active')
|
|
3430
|
-
)
|
|
3431
|
-
const trashFiles = this.#getTrashBucket(ownerAddress).map(file =>
|
|
3432
|
-
this.#formatFileForSync(file, 'trash')
|
|
3433
|
-
)
|
|
3434
|
-
const channels = this.#channels
|
|
3435
|
-
.filter(channel => this.#channelHasMember(channel, ownerAddress))
|
|
3436
|
-
.map(channel => this.#formatChannelForSync(channel, ownerAddress))
|
|
3437
|
-
.filter(Boolean)
|
|
3438
|
-
const profile = this.#formatUserProfileForSync(ownerAddress)
|
|
3439
|
-
return this.#appendUserSyncOp(ownerAddress, 'snapshot', {
|
|
3440
|
-
reason,
|
|
3441
|
-
profile,
|
|
3442
|
-
files,
|
|
3443
|
-
trashFiles,
|
|
3444
|
-
channels,
|
|
3445
|
-
})
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
async #applyUserSyncEntry(session, entry) {
|
|
3449
|
-
const op = this.#decodeUserSyncEntry(session, entry)
|
|
3450
|
-
if (!op || op.ownerAddress !== session.ownerAddress) return false
|
|
3451
|
-
if (Array.isArray(entry.writerCoreKeys)) {
|
|
3452
|
-
await this.#mergeUserSyncWriterCoreKeys(session, entry.writerCoreKeys)
|
|
3453
|
-
}
|
|
3454
|
-
return this.#applyUserSyncOp(session, op)
|
|
3455
|
-
}
|
|
3456
|
-
|
|
3457
|
-
async #applyUserSyncOp(session, op) {
|
|
3458
|
-
let changedFiles = false
|
|
3459
|
-
let changedChannels = false
|
|
3460
|
-
let changedProfile = false
|
|
3461
|
-
if (op.kind === 'snapshot') {
|
|
3462
|
-
const payload = op.payload || {}
|
|
3463
|
-
const profileResult = this.#applyUserSyncProfileRecord(
|
|
3464
|
-
session.ownerAddress,
|
|
3465
|
-
payload.profile,
|
|
3466
|
-
getSyncTimestamp(payload.profile?.syncUpdatedAt, op.timestamp)
|
|
3467
|
-
)
|
|
3468
|
-
changedProfile = profileResult.changedProfile || changedProfile
|
|
3469
|
-
changedChannels = profileResult.changedChannels || changedChannels
|
|
3470
|
-
for (const file of Array.isArray(payload.files) ? payload.files : []) {
|
|
3471
|
-
changedFiles =
|
|
3472
|
-
this.#applyUserSyncFileRecord(
|
|
3473
|
-
session.ownerAddress,
|
|
3474
|
-
file,
|
|
3475
|
-
'active',
|
|
3476
|
-
getSyncTimestamp(file.syncUpdatedAt, op.timestamp)
|
|
3477
|
-
) || changedFiles
|
|
3478
|
-
}
|
|
3479
|
-
for (const file of Array.isArray(payload.trashFiles) ? payload.trashFiles : []) {
|
|
3480
|
-
changedFiles =
|
|
3481
|
-
this.#applyUserSyncFileRecord(
|
|
3482
|
-
session.ownerAddress,
|
|
3483
|
-
file,
|
|
3484
|
-
'trash',
|
|
3485
|
-
getSyncTimestamp(file.syncUpdatedAt, op.timestamp)
|
|
3486
|
-
) || changedFiles
|
|
3487
|
-
}
|
|
3488
|
-
for (const channel of Array.isArray(payload.channels) ? payload.channels : []) {
|
|
3489
|
-
changedChannels =
|
|
3490
|
-
(await this.#applyUserSyncChannelRecord(
|
|
3491
|
-
session.ownerAddress,
|
|
3492
|
-
channel,
|
|
3493
|
-
getSyncTimestamp(channel.syncUpdatedAt, op.timestamp)
|
|
3494
|
-
)) || changedChannels
|
|
3495
|
-
}
|
|
3496
|
-
} else if (op.kind === 'file:upsert') {
|
|
3497
|
-
changedFiles = this.#applyUserSyncFileRecord(
|
|
3498
|
-
session.ownerAddress,
|
|
3499
|
-
op.payload?.file,
|
|
3500
|
-
'active',
|
|
3501
|
-
getSyncTimestamp(op.payload?.file?.syncUpdatedAt, op.timestamp)
|
|
3502
|
-
)
|
|
3503
|
-
} else if (op.kind === 'file:trash') {
|
|
3504
|
-
changedFiles = this.#applyUserSyncFileRecord(
|
|
3505
|
-
session.ownerAddress,
|
|
3506
|
-
op.payload?.file,
|
|
3507
|
-
'trash',
|
|
3508
|
-
getSyncTimestamp(op.payload?.file?.syncUpdatedAt, op.timestamp)
|
|
3509
|
-
)
|
|
3510
|
-
} else if (op.kind === 'file:delete') {
|
|
3511
|
-
changedFiles = await this.#applyUserSyncFileDelete(
|
|
3512
|
-
session.ownerAddress,
|
|
3513
|
-
op.payload?.cid,
|
|
3514
|
-
getSyncTimestamp(op.payload?.syncUpdatedAt, op.timestamp)
|
|
3515
|
-
)
|
|
3516
|
-
} else if (op.kind === 'channel:upsert') {
|
|
3517
|
-
changedChannels = await this.#applyUserSyncChannelRecord(
|
|
3518
|
-
session.ownerAddress,
|
|
3519
|
-
op.payload?.channel,
|
|
3520
|
-
getSyncTimestamp(op.payload?.channel?.syncUpdatedAt, op.timestamp)
|
|
3521
|
-
)
|
|
3522
|
-
} else if (op.kind === 'channel:leave') {
|
|
3523
|
-
changedChannels = this.#applyUserSyncChannelLeave(
|
|
3524
|
-
session.ownerAddress,
|
|
3525
|
-
op.payload?.channelKey,
|
|
3526
|
-
getSyncTimestamp(op.payload?.syncUpdatedAt, op.timestamp)
|
|
3527
|
-
)
|
|
3528
|
-
} else if (op.kind === 'profile:upsert') {
|
|
3529
|
-
const profileResult = this.#applyUserSyncProfileRecord(
|
|
3530
|
-
session.ownerAddress,
|
|
3531
|
-
op.payload?.profile,
|
|
3532
|
-
getSyncTimestamp(op.payload?.profile?.syncUpdatedAt, op.timestamp)
|
|
3533
|
-
)
|
|
3534
|
-
changedProfile = profileResult.changedProfile
|
|
3535
|
-
changedChannels = changedChannels || profileResult.changedChannels
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
|
-
if (changedProfile) {
|
|
3539
|
-
this.emit('user:metadata:updated', {
|
|
3540
|
-
ownerAddress: session.ownerAddress,
|
|
3541
|
-
scope: 'profile',
|
|
3542
|
-
})
|
|
3543
|
-
}
|
|
3544
|
-
if (changedFiles) {
|
|
3545
|
-
this.#savePublishedMetadata()
|
|
3546
|
-
this.#saveTrashMetadata()
|
|
3547
|
-
this.emit('user:metadata:updated', {
|
|
3548
|
-
ownerAddress: session.ownerAddress,
|
|
3549
|
-
scope: 'files',
|
|
3550
|
-
})
|
|
3551
|
-
}
|
|
3552
|
-
if (changedChannels) {
|
|
3553
|
-
this.#saveChannelsMetadata()
|
|
3554
|
-
this.emit('user:metadata:updated', {
|
|
3555
|
-
ownerAddress: session.ownerAddress,
|
|
3556
|
-
scope: 'channels',
|
|
3557
|
-
})
|
|
3558
|
-
}
|
|
3559
|
-
if (changedFiles || changedChannels) {
|
|
3560
|
-
this.#touchUserSyncSession(session)
|
|
3561
|
-
}
|
|
3562
|
-
if (changedProfile) {
|
|
3563
|
-
this.#touchUserSyncSession(session)
|
|
3564
|
-
}
|
|
3565
|
-
return changedFiles || changedChannels || changedProfile
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
async #mergeUserSyncWriterCoreKeys(session, writerCoreKeys = []) {
|
|
3569
|
-
const nextKeys = uniqueStrings(writerCoreKeys)
|
|
3570
|
-
if (nextKeys.length === 0) return false
|
|
3571
|
-
const previous = new Set(session.writerCoreKeys || [])
|
|
3572
|
-
let changed = false
|
|
3573
|
-
for (const writerCoreKey of nextKeys) {
|
|
3574
|
-
if (!previous.has(writerCoreKey)) {
|
|
3575
|
-
previous.add(writerCoreKey)
|
|
3576
|
-
changed = true
|
|
3577
|
-
}
|
|
3578
|
-
if (writerCoreKey !== session.localWriterCoreKey) {
|
|
3579
|
-
await this.#openRemoteUserSyncCore(session, writerCoreKey)
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
if (changed) {
|
|
3583
|
-
session.writerCoreKeys = [...previous]
|
|
3584
|
-
this.#persistUserSyncSession(session)
|
|
3585
|
-
}
|
|
3586
|
-
return changed
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
async #exchangeUserSyncSessions(peerEngine) {
|
|
3590
|
-
for (const session of this.#userSyncSessions.values()) {
|
|
3591
|
-
const peerSession = peerEngine.#userSyncSessions.get(session.ownerAddress)
|
|
3592
|
-
if (!peerSession || peerSession.syncId !== session.syncId) continue
|
|
3593
|
-
await this.#mergeUserSyncWriterCoreKeys(
|
|
3594
|
-
session,
|
|
3595
|
-
peerSession.writerCoreKeys
|
|
3596
|
-
)
|
|
3597
|
-
await peerEngine.#mergeUserSyncWriterCoreKeys(
|
|
3598
|
-
peerSession,
|
|
3599
|
-
session.writerCoreKeys
|
|
3600
|
-
)
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
#formatFileForSync(file, state = 'active') {
|
|
3466
|
+
#formatAccountFileForBackup(file, state = 'active') {
|
|
3605
3467
|
const cid = String(file?.cid || '').trim()
|
|
3606
3468
|
if (!cid) return null
|
|
3607
3469
|
const { driveName } = this.#getCidInfo(cid)
|
|
3608
|
-
const
|
|
3609
|
-
file.syncUpdatedAt || file.deletedAt || file.publishedAt
|
|
3470
|
+
const updatedAt = getSyncTimestamp(
|
|
3471
|
+
file.updatedAt || file.syncUpdatedAt || file.deletedAt || file.publishedAt
|
|
3610
3472
|
)
|
|
3611
3473
|
return {
|
|
3612
3474
|
cid,
|
|
@@ -3617,19 +3479,21 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3617
3479
|
publishedAt:
|
|
3618
3480
|
typeof file.publishedAt === 'string'
|
|
3619
3481
|
? file.publishedAt
|
|
3620
|
-
: new Date(
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3482
|
+
: new Date(updatedAt).toISOString(),
|
|
3483
|
+
...(state === 'trash'
|
|
3484
|
+
? {
|
|
3485
|
+
deletedAt:
|
|
3486
|
+
typeof file.deletedAt === 'string'
|
|
3487
|
+
? file.deletedAt
|
|
3488
|
+
: new Date(updatedAt).toISOString(),
|
|
3489
|
+
}
|
|
3490
|
+
: {}),
|
|
3627
3491
|
starred: Boolean(file.starred),
|
|
3628
|
-
|
|
3492
|
+
updatedAt,
|
|
3629
3493
|
}
|
|
3630
3494
|
}
|
|
3631
3495
|
|
|
3632
|
-
#
|
|
3496
|
+
#formatAccountChannelForBackup(channel, ownerAddress) {
|
|
3633
3497
|
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3634
3498
|
if (
|
|
3635
3499
|
!channel ||
|
|
@@ -3639,6 +3503,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3639
3503
|
) {
|
|
3640
3504
|
return null
|
|
3641
3505
|
}
|
|
3506
|
+
const updatedAt = getSyncTimestamp(
|
|
3507
|
+
channel.updatedAt || channel.syncUpdatedAt || channel.lastMessageAt || channel.createdAt
|
|
3508
|
+
)
|
|
3642
3509
|
return {
|
|
3643
3510
|
channelId: channel.channelId,
|
|
3644
3511
|
channelKey: channel.channelKey,
|
|
@@ -3651,93 +3518,54 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3651
3518
|
),
|
|
3652
3519
|
remark: channel.remarks?.[owner] || '',
|
|
3653
3520
|
pinned: Boolean(channel.pinnedBy?.[owner]),
|
|
3654
|
-
|
|
3655
|
-
channel.syncUpdatedAt || channel.lastMessageAt || channel.createdAt
|
|
3656
|
-
),
|
|
3521
|
+
updatedAt,
|
|
3657
3522
|
}
|
|
3658
3523
|
}
|
|
3659
3524
|
|
|
3660
|
-
#
|
|
3661
|
-
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3662
|
-
if (!owner) return null
|
|
3663
|
-
const profile = this.#userSyncMetadata.profiles?.[owner]
|
|
3664
|
-
return profile ? { ...profile } : null
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
#normalizeUserProfileRecord(ownerAddress, record, timestamp = Date.now()) {
|
|
3525
|
+
#normalizeAccountProfileRecord(ownerAddress, record, timestamp = Date.now()) {
|
|
3668
3526
|
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3669
3527
|
if (!owner || !record || typeof record !== 'object') return null
|
|
3670
|
-
const
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
return {
|
|
3674
|
-
displayName,
|
|
3675
|
-
avatar,
|
|
3676
|
-
syncUpdatedAt,
|
|
3677
|
-
}
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
#applyUserSyncProfileRecord(ownerAddress, record, timestamp) {
|
|
3681
|
-
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3682
|
-
const profile = this.#normalizeUserProfileRecord(owner, record, timestamp)
|
|
3683
|
-
if (!profile) {
|
|
3684
|
-
return { changedProfile: false, changedChannels: false }
|
|
3685
|
-
}
|
|
3686
|
-
if (!this.#shouldApplyUserSyncEntity(owner, 'profile', profile.syncUpdatedAt)) {
|
|
3687
|
-
return { changedProfile: false, changedChannels: false }
|
|
3688
|
-
}
|
|
3689
|
-
|
|
3690
|
-
const existing = this.#userSyncMetadata.profiles?.[owner]
|
|
3691
|
-
this.#userSyncMetadata.profiles = this.#userSyncMetadata.profiles || {}
|
|
3692
|
-
this.#userSyncMetadata.profiles[owner] = profile
|
|
3693
|
-
this.#setUserSyncClock(owner, 'profile', profile.syncUpdatedAt)
|
|
3694
|
-
const changedChannels = this.#applyUserProfileToJoinedChannels(
|
|
3695
|
-
owner,
|
|
3696
|
-
profile
|
|
3528
|
+
const updatedAt = getSyncTimestamp(
|
|
3529
|
+
record.updatedAt || record.syncUpdatedAt,
|
|
3530
|
+
timestamp
|
|
3697
3531
|
)
|
|
3698
3532
|
return {
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
existing.avatar !== profile.avatar ||
|
|
3703
|
-
Number(existing.syncUpdatedAt) !== profile.syncUpdatedAt,
|
|
3704
|
-
changedChannels,
|
|
3533
|
+
displayName: normalizeChannelDisplayName(record.displayName, owner),
|
|
3534
|
+
avatar: normalizeChannelAvatar(record.avatar),
|
|
3535
|
+
updatedAt,
|
|
3705
3536
|
}
|
|
3706
3537
|
}
|
|
3707
3538
|
|
|
3708
|
-
#
|
|
3539
|
+
#mergeAccountProfileRecord(ownerAddress, record, options = {}) {
|
|
3709
3540
|
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3541
|
+
const profile = this.#normalizeAccountProfileRecord(owner, record)
|
|
3542
|
+
if (!profile) return { changed: false, skipped: true }
|
|
3543
|
+
const existing = this.#accountMetadata.profiles?.[owner]
|
|
3544
|
+
if (
|
|
3545
|
+
existing &&
|
|
3546
|
+
!options.overwrite &&
|
|
3547
|
+
profile.updatedAt <= getSyncTimestamp(existing.updatedAt, 0)
|
|
3548
|
+
) {
|
|
3549
|
+
return { changed: false, skipped: true }
|
|
3550
|
+
}
|
|
3551
|
+
if (
|
|
3552
|
+
existing &&
|
|
3553
|
+
existing.displayName === profile.displayName &&
|
|
3554
|
+
existing.avatar === profile.avatar &&
|
|
3555
|
+
getSyncTimestamp(existing.updatedAt, 0) === profile.updatedAt
|
|
3556
|
+
) {
|
|
3557
|
+
return { changed: false, skipped: true }
|
|
3720
3558
|
}
|
|
3721
|
-
return changed
|
|
3722
|
-
}
|
|
3723
|
-
|
|
3724
|
-
#appendUserSyncChannelUpsertSoon(channel, ownerAddress) {
|
|
3725
|
-
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3726
|
-
const record = this.#formatChannelForSync(channel, owner)
|
|
3727
|
-
if (!record) return
|
|
3728
|
-
this.#appendUserSyncOpSoon(owner, 'channel:upsert', { channel: record })
|
|
3729
|
-
}
|
|
3730
3559
|
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
this.#
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
})
|
|
3560
|
+
this.#accountMetadata.profiles = this.#accountMetadata.profiles || {}
|
|
3561
|
+
this.#accountMetadata.profiles[owner] = profile
|
|
3562
|
+
this.#saveAccountMetadata()
|
|
3563
|
+
const changedChannels = this.#applyUserProfileToJoinedChannels(owner, profile)
|
|
3564
|
+
if (changedChannels) this.#saveChannelsMetadata()
|
|
3565
|
+
return { changed: true, skipped: false }
|
|
3738
3566
|
}
|
|
3739
3567
|
|
|
3740
|
-
#
|
|
3568
|
+
#normalizeAccountFileRecord(record, state) {
|
|
3741
3569
|
if (!record || typeof record !== 'object') return null
|
|
3742
3570
|
const cid = String(record.cid || '').trim()
|
|
3743
3571
|
if (!cid) return null
|
|
@@ -3749,7 +3577,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3749
3577
|
}
|
|
3750
3578
|
const fileName = sanitizeFilename(record.fileName || cid)
|
|
3751
3579
|
if (!fileName || fileName === 'unnamed') return null
|
|
3752
|
-
const
|
|
3580
|
+
const updatedAt = getSyncTimestamp(
|
|
3581
|
+
record.updatedAt || record.syncUpdatedAt || record.deletedAt || record.publishedAt
|
|
3582
|
+
)
|
|
3753
3583
|
return {
|
|
3754
3584
|
cid,
|
|
3755
3585
|
fileName,
|
|
@@ -3759,38 +3589,53 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3759
3589
|
publishedAt:
|
|
3760
3590
|
typeof record.publishedAt === 'string'
|
|
3761
3591
|
? record.publishedAt
|
|
3762
|
-
: new Date(
|
|
3592
|
+
: new Date(updatedAt).toISOString(),
|
|
3763
3593
|
deletedAt:
|
|
3764
3594
|
typeof record.deletedAt === 'string'
|
|
3765
3595
|
? record.deletedAt
|
|
3766
3596
|
: state === 'trash'
|
|
3767
|
-
? new Date(
|
|
3597
|
+
? new Date(updatedAt).toISOString()
|
|
3768
3598
|
: '',
|
|
3769
3599
|
starred: Boolean(record.starred),
|
|
3770
|
-
|
|
3600
|
+
updatedAt,
|
|
3771
3601
|
}
|
|
3772
3602
|
}
|
|
3773
3603
|
|
|
3774
|
-
#
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
}
|
|
3604
|
+
#getRecordUpdatedAt(record) {
|
|
3605
|
+
return getSyncTimestamp(
|
|
3606
|
+
record?.updatedAt || record?.syncUpdatedAt || record?.deletedAt || record?.publishedAt,
|
|
3607
|
+
0
|
|
3608
|
+
)
|
|
3609
|
+
}
|
|
3781
3610
|
|
|
3782
|
-
|
|
3783
|
-
const
|
|
3784
|
-
|
|
3611
|
+
#mergeAccountFileRecord(ownerAddress, record, state) {
|
|
3612
|
+
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3613
|
+
const normalized = this.#normalizeAccountFileRecord(record, state)
|
|
3614
|
+
if (!owner || !normalized) {
|
|
3615
|
+
return { changed: false, added: false, updated: false }
|
|
3616
|
+
}
|
|
3785
3617
|
|
|
3618
|
+
const publishedFiles = [...this.#getPublishedBucket(owner)]
|
|
3619
|
+
const trashFiles = [...this.#getTrashBucket(owner)]
|
|
3786
3620
|
const publishedIndex = publishedFiles.findIndex(
|
|
3787
3621
|
file => file.cid === normalized.cid
|
|
3788
3622
|
)
|
|
3789
3623
|
const trashIndex = trashFiles.findIndex(file => file.cid === normalized.cid)
|
|
3624
|
+
const existingActive = publishedIndex === -1 ? null : publishedFiles[publishedIndex]
|
|
3625
|
+
const existingTrash = trashIndex === -1 ? null : trashFiles[trashIndex]
|
|
3626
|
+
const existingUpdatedAt = Math.max(
|
|
3627
|
+
this.#getRecordUpdatedAt(existingActive),
|
|
3628
|
+
this.#getRecordUpdatedAt(existingTrash)
|
|
3629
|
+
)
|
|
3630
|
+
if (normalized.updatedAt <= existingUpdatedAt) {
|
|
3631
|
+
return { changed: false, added: false, updated: false }
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3790
3634
|
const localHolding = this.#holdings.find(
|
|
3791
3635
|
holding => holding.cid === normalized.cid
|
|
3792
3636
|
)
|
|
3793
|
-
const localSource = localHolding?.source ||
|
|
3637
|
+
const localSource = localHolding?.source || normalized.source
|
|
3638
|
+
const wasKnown = Boolean(existingActive || existingTrash)
|
|
3794
3639
|
|
|
3795
3640
|
if (state === 'active') {
|
|
3796
3641
|
const nextRecord = {
|
|
@@ -3801,22 +3646,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3801
3646
|
source: localSource,
|
|
3802
3647
|
publishedAt: normalized.publishedAt,
|
|
3803
3648
|
starred: normalized.starred,
|
|
3804
|
-
syncUpdatedAt: normalized.
|
|
3805
|
-
}
|
|
3806
|
-
if (publishedIndex === -1) {
|
|
3807
|
-
publishedFiles.push(nextRecord)
|
|
3808
|
-
changed = true
|
|
3809
|
-
} else if (
|
|
3810
|
-
JSON.stringify(publishedFiles[publishedIndex]) !==
|
|
3811
|
-
JSON.stringify(nextRecord)
|
|
3812
|
-
) {
|
|
3813
|
-
publishedFiles[publishedIndex] = nextRecord
|
|
3814
|
-
changed = true
|
|
3815
|
-
}
|
|
3816
|
-
if (trashIndex !== -1) {
|
|
3817
|
-
trashFiles.splice(trashIndex, 1)
|
|
3818
|
-
changed = true
|
|
3649
|
+
syncUpdatedAt: normalized.updatedAt,
|
|
3819
3650
|
}
|
|
3651
|
+
if (publishedIndex === -1) publishedFiles.push(nextRecord)
|
|
3652
|
+
else publishedFiles[publishedIndex] = nextRecord
|
|
3653
|
+
if (trashIndex !== -1) trashFiles.splice(trashIndex, 1)
|
|
3820
3654
|
} else {
|
|
3821
3655
|
const nextRecord = {
|
|
3822
3656
|
fileName: normalized.fileName,
|
|
@@ -3826,72 +3660,46 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3826
3660
|
source: localSource,
|
|
3827
3661
|
publishedAt: normalized.publishedAt,
|
|
3828
3662
|
starred: normalized.starred,
|
|
3829
|
-
deletedAt: normalized.deletedAt || new Date(normalized.
|
|
3830
|
-
syncUpdatedAt: normalized.
|
|
3831
|
-
}
|
|
3832
|
-
if (trashIndex === -1) {
|
|
3833
|
-
trashFiles.push(nextRecord)
|
|
3834
|
-
changed = true
|
|
3835
|
-
} else if (
|
|
3836
|
-
JSON.stringify(trashFiles[trashIndex]) !== JSON.stringify(nextRecord)
|
|
3837
|
-
) {
|
|
3838
|
-
trashFiles[trashIndex] = nextRecord
|
|
3839
|
-
changed = true
|
|
3840
|
-
}
|
|
3841
|
-
if (publishedIndex !== -1) {
|
|
3842
|
-
publishedFiles.splice(publishedIndex, 1)
|
|
3843
|
-
changed = true
|
|
3663
|
+
deletedAt: normalized.deletedAt || new Date(normalized.updatedAt).toISOString(),
|
|
3664
|
+
syncUpdatedAt: normalized.updatedAt,
|
|
3844
3665
|
}
|
|
3666
|
+
if (trashIndex === -1) trashFiles.push(nextRecord)
|
|
3667
|
+
else trashFiles[trashIndex] = nextRecord
|
|
3668
|
+
if (publishedIndex !== -1) publishedFiles.splice(publishedIndex, 1)
|
|
3845
3669
|
}
|
|
3846
3670
|
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3671
|
+
this.#setPublishedBucket(owner, publishedFiles)
|
|
3672
|
+
this.#setTrashBucket(owner, trashFiles)
|
|
3673
|
+
return {
|
|
3674
|
+
changed: true,
|
|
3675
|
+
added: !wasKnown,
|
|
3676
|
+
updated: wasKnown,
|
|
3851
3677
|
}
|
|
3852
|
-
return changed
|
|
3853
3678
|
}
|
|
3854
3679
|
|
|
3855
|
-
async #
|
|
3856
|
-
const
|
|
3857
|
-
if (!
|
|
3858
|
-
|
|
3859
|
-
const entityKey = `file:${cid}`
|
|
3860
|
-
if (!this.#shouldApplyUserSyncEntity(ownerAddress, entityKey, syncUpdatedAt)) {
|
|
3861
|
-
return false
|
|
3862
|
-
}
|
|
3863
|
-
|
|
3864
|
-
const publishedFiles = this.#getPublishedBucket(ownerAddress).filter(
|
|
3865
|
-
file => file.cid !== cid
|
|
3866
|
-
)
|
|
3867
|
-
const trashFiles = this.#getTrashBucket(ownerAddress).filter(
|
|
3868
|
-
file => file.cid !== cid
|
|
3869
|
-
)
|
|
3870
|
-
const changed =
|
|
3871
|
-
publishedFiles.length !== this.#getPublishedBucket(ownerAddress).length ||
|
|
3872
|
-
trashFiles.length !== this.#getTrashBucket(ownerAddress).length
|
|
3873
|
-
this.#setPublishedBucket(ownerAddress, publishedFiles)
|
|
3874
|
-
this.#setTrashBucket(ownerAddress, trashFiles)
|
|
3875
|
-
this.#setUserSyncClock(ownerAddress, entityKey, syncUpdatedAt)
|
|
3876
|
-
if (changed && !this.#hasAnyUserReference(cid)) {
|
|
3877
|
-
await this.#cleanupUnreferencedCids([cid])
|
|
3680
|
+
async #mergeAccountChannelRecord(ownerAddress, record) {
|
|
3681
|
+
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3682
|
+
if (!owner || !record || typeof record !== 'object') {
|
|
3683
|
+
return { changed: false, added: false, updated: false }
|
|
3878
3684
|
}
|
|
3879
|
-
return changed
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
async #applyUserSyncChannelRecord(ownerAddress, record, timestamp) {
|
|
3883
|
-
if (!record || typeof record !== 'object') return false
|
|
3884
3685
|
const channelId = normalizeChannelId(record.channelId)
|
|
3885
|
-
const
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
return false
|
|
3686
|
+
const channelKey = buildChannelKey(channelId)
|
|
3687
|
+
if (
|
|
3688
|
+
!channelId ||
|
|
3689
|
+
!channelKey ||
|
|
3690
|
+
TRANSIENT_CHANNEL_TYPES.has(String(record.type || ''))
|
|
3691
|
+
) {
|
|
3692
|
+
return { changed: false, added: false, updated: false }
|
|
3892
3693
|
}
|
|
3893
3694
|
|
|
3695
|
+
const updatedAt = getSyncTimestamp(record.updatedAt || record.syncUpdatedAt)
|
|
3894
3696
|
let channel = this.#channels.find(item => item.channelKey === channelKey)
|
|
3697
|
+
const existingUpdatedAt = this.#getRecordUpdatedAt(channel)
|
|
3698
|
+
if (channel && updatedAt <= existingUpdatedAt) {
|
|
3699
|
+
return { changed: false, added: false, updated: false }
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
const added = !channel
|
|
3895
3703
|
let changed = false
|
|
3896
3704
|
if (!channel) {
|
|
3897
3705
|
channel = {
|
|
@@ -3901,7 +3709,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3901
3709
|
createdAt:
|
|
3902
3710
|
typeof record.createdAt === 'string'
|
|
3903
3711
|
? record.createdAt
|
|
3904
|
-
: new Date(
|
|
3712
|
+
: new Date(updatedAt).toISOString(),
|
|
3905
3713
|
lastMessageAt:
|
|
3906
3714
|
typeof record.lastMessageAt === 'string' ? record.lastMessageAt : '',
|
|
3907
3715
|
type: String(record.type || 'personal').trim() || 'personal',
|
|
@@ -3909,7 +3717,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3909
3717
|
localWriterCoreKey: '',
|
|
3910
3718
|
writerCoreKeys: uniqueStrings(record.writerCoreKeys),
|
|
3911
3719
|
members: [],
|
|
3912
|
-
syncUpdatedAt,
|
|
3720
|
+
syncUpdatedAt: updatedAt,
|
|
3913
3721
|
}
|
|
3914
3722
|
this.#channels.push(channel)
|
|
3915
3723
|
changed = true
|
|
@@ -3926,174 +3734,58 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3926
3734
|
channel.lastMessageAt = record.lastMessageAt
|
|
3927
3735
|
changed = true
|
|
3928
3736
|
}
|
|
3929
|
-
channel.
|
|
3737
|
+
channel.type = String(record.type || channel.type || 'personal').trim()
|
|
3738
|
+
channel.syncUpdatedAt = updatedAt
|
|
3930
3739
|
}
|
|
3931
3740
|
|
|
3932
3741
|
if (
|
|
3933
3742
|
this.#upsertChannelMember(channel, {
|
|
3934
|
-
ownerAddress,
|
|
3935
|
-
displayName:
|
|
3936
|
-
record.member?.displayName || record.remark || '',
|
|
3743
|
+
ownerAddress: owner,
|
|
3744
|
+
displayName: record.member?.displayName || record.remark || '',
|
|
3937
3745
|
avatar: record.member?.avatar || '',
|
|
3938
3746
|
})
|
|
3939
3747
|
) {
|
|
3940
3748
|
changed = true
|
|
3941
3749
|
}
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
}
|
|
3750
|
+
|
|
3751
|
+
channel.remarks = channel.remarks || {}
|
|
3752
|
+
const remark = String(record.remark || '').slice(0, 50)
|
|
3753
|
+
if (remark) channel.remarks[owner] = remark
|
|
3754
|
+
else delete channel.remarks[owner]
|
|
3755
|
+
|
|
3949
3756
|
channel.pinnedBy = channel.pinnedBy || {}
|
|
3950
|
-
if (record.pinned)
|
|
3951
|
-
|
|
3952
|
-
} else {
|
|
3953
|
-
delete channel.pinnedBy[ownerAddress]
|
|
3954
|
-
}
|
|
3955
|
-
this.#setUserSyncClock(ownerAddress, entityKey, syncUpdatedAt)
|
|
3757
|
+
if (record.pinned) channel.pinnedBy[owner] = true
|
|
3758
|
+
else delete channel.pinnedBy[owner]
|
|
3956
3759
|
|
|
3957
3760
|
if (!this.#channelLocalCoreKey.get(channel.channelKey)) {
|
|
3958
3761
|
await this.#openChannelRuntime(channel)
|
|
3959
3762
|
await this.#joinChannelDiscoveryTopics(channel)
|
|
3960
|
-
changed = true
|
|
3961
3763
|
this.emit('channel:joined', {
|
|
3962
3764
|
channel: channel.channelKey,
|
|
3963
3765
|
channelKey: channel.channelKey,
|
|
3964
3766
|
channelId: channel.channelId,
|
|
3965
3767
|
key: channel.channelKey,
|
|
3966
3768
|
})
|
|
3769
|
+
changed = true
|
|
3967
3770
|
}
|
|
3968
|
-
return changed
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
|
-
#applyUserSyncChannelLeave(ownerAddress, channelKeyInput, timestamp) {
|
|
3972
|
-
const channelKey = normalizeChannelKey(channelKeyInput)
|
|
3973
|
-
if (!channelKey) return false
|
|
3974
|
-
const syncUpdatedAt = getSyncTimestamp(timestamp)
|
|
3975
|
-
const entityKey = `channel:${channelKey}`
|
|
3976
|
-
if (!this.#shouldApplyUserSyncEntity(ownerAddress, entityKey, syncUpdatedAt)) {
|
|
3977
|
-
return false
|
|
3978
|
-
}
|
|
3979
|
-
const channel = this.#channels.find(item => item.channelKey === channelKey)
|
|
3980
|
-
if (!channel) {
|
|
3981
|
-
this.#setUserSyncClock(ownerAddress, entityKey, syncUpdatedAt)
|
|
3982
|
-
return false
|
|
3983
|
-
}
|
|
3984
|
-
const before = channel.members?.length || 0
|
|
3985
|
-
channel.members = (channel.members || []).filter(
|
|
3986
|
-
member => normalizeOwnerAddress(member?.address) !== ownerAddress
|
|
3987
|
-
)
|
|
3988
|
-
if (channel.remarks) delete channel.remarks[ownerAddress]
|
|
3989
|
-
if (channel.pinnedBy) delete channel.pinnedBy[ownerAddress]
|
|
3990
|
-
this.#setUserSyncClock(ownerAddress, entityKey, syncUpdatedAt)
|
|
3991
|
-
const changed = before !== channel.members.length
|
|
3992
|
-
if (channel.members.length === 0) {
|
|
3993
|
-
this.#channels = this.#channels.filter(item => item.channelKey !== channelKey)
|
|
3994
|
-
}
|
|
3995
|
-
if (changed) {
|
|
3996
|
-
this.emit('channel:left', {
|
|
3997
|
-
channel: channelKey,
|
|
3998
|
-
channelKey,
|
|
3999
|
-
channelId: channel.channelId,
|
|
4000
|
-
name: channel.channelId,
|
|
4001
|
-
})
|
|
4002
|
-
}
|
|
4003
|
-
return changed
|
|
4004
|
-
}
|
|
4005
3771
|
|
|
4006
|
-
|
|
4007
|
-
const owner = normalizeOwnerAddress(ownerAddress)
|
|
4008
|
-
return Number(this.#userSyncMetadata.clocks?.[owner]?.[entityKey]) || 0
|
|
3772
|
+
return { changed: true, added, updated: !added || changed }
|
|
4009
3773
|
}
|
|
4010
3774
|
|
|
4011
|
-
#
|
|
3775
|
+
#applyUserProfileToJoinedChannels(ownerAddress, profile) {
|
|
4012
3776
|
const owner = normalizeOwnerAddress(ownerAddress)
|
|
4013
|
-
if (!owner || !
|
|
4014
|
-
|
|
4015
|
-
this.#
|
|
4016
|
-
this.#
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
#shouldApplyUserSyncEntity(ownerAddress, entityKey, timestamp) {
|
|
4025
|
-
return getSyncTimestamp(timestamp) > this.#getUserSyncClock(ownerAddress, entityKey)
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4028
|
-
#markUserSyncClockForOp(op) {
|
|
4029
|
-
const ownerAddress = normalizeOwnerAddress(op.ownerAddress)
|
|
4030
|
-
if (!ownerAddress) return
|
|
4031
|
-
if (op.kind === 'file:upsert' || op.kind === 'file:trash') {
|
|
4032
|
-
const cid = op.payload?.file?.cid
|
|
4033
|
-
const timestamp = getSyncTimestamp(
|
|
4034
|
-
op.payload?.file?.syncUpdatedAt,
|
|
4035
|
-
op.timestamp
|
|
4036
|
-
)
|
|
4037
|
-
if (cid) this.#setUserSyncClock(ownerAddress, `file:${cid}`, timestamp)
|
|
4038
|
-
} else if (op.kind === 'file:delete') {
|
|
4039
|
-
const cid = op.payload?.cid
|
|
4040
|
-
const timestamp = getSyncTimestamp(
|
|
4041
|
-
op.payload?.syncUpdatedAt,
|
|
4042
|
-
op.timestamp
|
|
4043
|
-
)
|
|
4044
|
-
if (cid) this.#setUserSyncClock(ownerAddress, `file:${cid}`, timestamp)
|
|
4045
|
-
} else if (op.kind === 'channel:upsert') {
|
|
4046
|
-
const channelKey = op.payload?.channel?.channelKey
|
|
4047
|
-
const timestamp = getSyncTimestamp(
|
|
4048
|
-
op.payload?.channel?.syncUpdatedAt,
|
|
4049
|
-
op.timestamp
|
|
4050
|
-
)
|
|
4051
|
-
if (channelKey) {
|
|
4052
|
-
this.#setUserSyncClock(ownerAddress, `channel:${channelKey}`, timestamp)
|
|
4053
|
-
}
|
|
4054
|
-
} else if (op.kind === 'channel:leave') {
|
|
4055
|
-
const channelKey = op.payload?.channelKey
|
|
4056
|
-
const timestamp = getSyncTimestamp(
|
|
4057
|
-
op.payload?.syncUpdatedAt,
|
|
4058
|
-
op.timestamp
|
|
4059
|
-
)
|
|
4060
|
-
if (channelKey) {
|
|
4061
|
-
this.#setUserSyncClock(ownerAddress, `channel:${channelKey}`, timestamp)
|
|
4062
|
-
}
|
|
4063
|
-
} else if (op.kind === 'profile:upsert') {
|
|
4064
|
-
const timestamp = getSyncTimestamp(
|
|
4065
|
-
op.payload?.profile?.syncUpdatedAt,
|
|
4066
|
-
op.timestamp
|
|
4067
|
-
)
|
|
4068
|
-
this.#setUserSyncClock(ownerAddress, 'profile', timestamp)
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4071
|
-
|
|
4072
|
-
#persistUserSyncSession(session) {
|
|
4073
|
-
this.#userSyncMetadata.sessions = this.#userSyncMetadata.sessions || {}
|
|
4074
|
-
this.#userSyncMetadata.sessions[session.ownerAddress] = {
|
|
4075
|
-
ownerAddress: session.ownerAddress,
|
|
4076
|
-
syncId: session.syncId,
|
|
4077
|
-
syncName: session.syncName,
|
|
4078
|
-
writerId: session.writerId,
|
|
4079
|
-
localWriterCoreKey: session.localWriterCoreKey,
|
|
4080
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
4081
|
-
startedAt: session.startedAt,
|
|
4082
|
-
lastSyncedAt:
|
|
4083
|
-
this.#userSyncMetadata.sessions?.[session.ownerAddress]?.lastSyncedAt ||
|
|
4084
|
-
'',
|
|
4085
|
-
updatedAt: new Date().toISOString(),
|
|
4086
|
-
}
|
|
4087
|
-
this.#saveUserSyncMetadata()
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
#touchUserSyncSession(session) {
|
|
4091
|
-
this.#persistUserSyncSession(session)
|
|
4092
|
-
const persisted = this.#userSyncMetadata.sessions?.[session.ownerAddress]
|
|
4093
|
-
if (persisted) {
|
|
4094
|
-
persisted.lastSyncedAt = new Date().toISOString()
|
|
4095
|
-
this.#saveUserSyncMetadata()
|
|
3777
|
+
if (!owner || !profile) return false
|
|
3778
|
+
let changed = false
|
|
3779
|
+
for (const channel of this.#channels) {
|
|
3780
|
+
if (!this.#channelHasMember(channel, owner)) continue
|
|
3781
|
+
changed =
|
|
3782
|
+
this.#upsertChannelMember(channel, {
|
|
3783
|
+
ownerAddress: owner,
|
|
3784
|
+
displayName: profile.displayName,
|
|
3785
|
+
avatar: profile.avatar,
|
|
3786
|
+
}) || changed
|
|
4096
3787
|
}
|
|
3788
|
+
return changed
|
|
4097
3789
|
}
|
|
4098
3790
|
|
|
4099
3791
|
#getFileRuntimeStats(cid) {
|
|
@@ -4674,8 +4366,8 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4674
4366
|
return path.join(this.#options.dataPath, 'trash-files.json')
|
|
4675
4367
|
}
|
|
4676
4368
|
|
|
4677
|
-
#
|
|
4678
|
-
return path.join(this.#options.dataPath, '
|
|
4369
|
+
#getAccountMetadataPath() {
|
|
4370
|
+
return path.join(this.#options.dataPath, 'account-metadata.json')
|
|
4679
4371
|
}
|
|
4680
4372
|
|
|
4681
4373
|
#atomicWrite(filePath, data) {
|
|
@@ -4770,73 +4462,41 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4770
4462
|
}
|
|
4771
4463
|
}
|
|
4772
4464
|
|
|
4773
|
-
#
|
|
4465
|
+
#loadAccountMetadata() {
|
|
4774
4466
|
try {
|
|
4775
|
-
const metadataPath = this.#
|
|
4467
|
+
const metadataPath = this.#getAccountMetadataPath()
|
|
4776
4468
|
if (fs.existsSync(metadataPath)) {
|
|
4777
4469
|
const parsed = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
|
|
4778
|
-
const sessions = {}
|
|
4779
|
-
for (const [owner, session] of Object.entries(parsed.sessions || {})) {
|
|
4780
|
-
const ownerAddress = normalizeOwnerAddress(owner)
|
|
4781
|
-
const syncId = String(session?.syncId || '').trim()
|
|
4782
|
-
if (!ownerAddress || !syncId) continue
|
|
4783
|
-
sessions[ownerAddress] = {
|
|
4784
|
-
ownerAddress,
|
|
4785
|
-
syncId,
|
|
4786
|
-
syncName: String(session.syncName || getUserSyncName(syncId)),
|
|
4787
|
-
writerId: String(session.writerId || ''),
|
|
4788
|
-
localWriterCoreKey: String(session.localWriterCoreKey || ''),
|
|
4789
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
4790
|
-
startedAt: String(session.startedAt || ''),
|
|
4791
|
-
lastSyncedAt: String(session.lastSyncedAt || ''),
|
|
4792
|
-
updatedAt: String(session.updatedAt || ''),
|
|
4793
|
-
}
|
|
4794
|
-
}
|
|
4795
|
-
|
|
4796
|
-
const clocks = {}
|
|
4797
|
-
for (const [owner, records] of Object.entries(parsed.clocks || {})) {
|
|
4798
|
-
const ownerAddress = normalizeOwnerAddress(owner)
|
|
4799
|
-
if (!ownerAddress || !records || typeof records !== 'object') continue
|
|
4800
|
-
clocks[ownerAddress] = {}
|
|
4801
|
-
for (const [entityKey, timestamp] of Object.entries(records)) {
|
|
4802
|
-
const value = Number(timestamp)
|
|
4803
|
-
if (entityKey && Number.isFinite(value) && value > 0) {
|
|
4804
|
-
clocks[ownerAddress][entityKey] = value
|
|
4805
|
-
}
|
|
4806
|
-
}
|
|
4807
|
-
}
|
|
4808
|
-
|
|
4809
4470
|
const profiles = {}
|
|
4810
4471
|
for (const [owner, profile] of Object.entries(parsed.profiles || {})) {
|
|
4811
4472
|
const ownerAddress = normalizeOwnerAddress(owner)
|
|
4812
|
-
const normalized = this.#
|
|
4473
|
+
const normalized = this.#normalizeAccountProfileRecord(
|
|
4813
4474
|
ownerAddress,
|
|
4814
4475
|
profile,
|
|
4815
|
-
profile?.syncUpdatedAt
|
|
4476
|
+
profile?.updatedAt || profile?.syncUpdatedAt
|
|
4816
4477
|
)
|
|
4817
4478
|
if (normalized) profiles[ownerAddress] = normalized
|
|
4818
4479
|
}
|
|
4819
|
-
|
|
4820
|
-
return { sessions, clocks, profiles }
|
|
4480
|
+
return { profiles }
|
|
4821
4481
|
}
|
|
4822
4482
|
} catch (err) {
|
|
4823
4483
|
console.warn(
|
|
4824
|
-
'Failed to load
|
|
4484
|
+
'Failed to load account metadata, using empty state:',
|
|
4825
4485
|
err.message
|
|
4826
4486
|
)
|
|
4827
4487
|
}
|
|
4828
|
-
return {
|
|
4488
|
+
return { profiles: {} }
|
|
4829
4489
|
}
|
|
4830
4490
|
|
|
4831
|
-
#
|
|
4491
|
+
#saveAccountMetadata() {
|
|
4832
4492
|
try {
|
|
4833
|
-
const metadataPath = this.#
|
|
4493
|
+
const metadataPath = this.#getAccountMetadataPath()
|
|
4834
4494
|
this.#atomicWrite(
|
|
4835
4495
|
metadataPath,
|
|
4836
|
-
JSON.stringify(this.#
|
|
4496
|
+
JSON.stringify(this.#accountMetadata, null, 2)
|
|
4837
4497
|
)
|
|
4838
4498
|
} catch (err) {
|
|
4839
|
-
console.error('Failed to save
|
|
4499
|
+
console.error('Failed to save account metadata:', err.message)
|
|
4840
4500
|
}
|
|
4841
4501
|
}
|
|
4842
4502
|
|
|
@@ -4940,14 +4600,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4940
4600
|
return hash
|
|
4941
4601
|
}
|
|
4942
4602
|
|
|
4943
|
-
#generateUserSyncDiscoveryKey(syncId) {
|
|
4944
|
-
const hash = crypto
|
|
4945
|
-
.createHash('sha256')
|
|
4946
|
-
.update(`${CHANNEL_NAME_PREFIX}${USER_SYNC_NAMESPACE_PREFIX}${syncId}`)
|
|
4947
|
-
.digest()
|
|
4948
|
-
return hash
|
|
4949
|
-
}
|
|
4950
|
-
|
|
4951
4603
|
#setupChannelAppendListener(core, channelKey) {
|
|
4952
4604
|
let lastCoreLength = core.length
|
|
4953
4605
|
core.on('append', async () => {
|
|
@@ -5036,21 +4688,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
5036
4688
|
this.#channelLocalCoreKey.get(channel.channelKey),
|
|
5037
4689
|
]),
|
|
5038
4690
|
}))
|
|
5039
|
-
const userSyncSessions = [...this.#userSyncSessions.values()].map(
|
|
5040
|
-
session => ({
|
|
5041
|
-
ownerAddress: session.ownerAddress,
|
|
5042
|
-
syncId: session.syncId,
|
|
5043
|
-
syncName: session.syncName,
|
|
5044
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
5045
|
-
})
|
|
5046
|
-
)
|
|
5047
|
-
|
|
5048
4691
|
return {
|
|
5049
4692
|
type: 'channel-hello',
|
|
5050
4693
|
peerId: this.getNodeId(),
|
|
5051
4694
|
authorName: this.getNodeId().slice(0, 4),
|
|
5052
4695
|
channels,
|
|
5053
|
-
userSyncSessions,
|
|
5054
4696
|
}
|
|
5055
4697
|
}
|
|
5056
4698
|
|
|
@@ -5132,30 +4774,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
5132
4774
|
}
|
|
5133
4775
|
}
|
|
5134
4776
|
|
|
5135
|
-
const remoteUserSyncSessions = Array.isArray(msg.userSyncSessions)
|
|
5136
|
-
? msg.userSyncSessions
|
|
5137
|
-
.filter(session => session && typeof session === 'object')
|
|
5138
|
-
.map(session => ({
|
|
5139
|
-
ownerAddress: normalizeOwnerAddress(session.ownerAddress),
|
|
5140
|
-
syncId: String(session.syncId || '').trim(),
|
|
5141
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
5142
|
-
}))
|
|
5143
|
-
.filter(session => session.ownerAddress && session.syncId)
|
|
5144
|
-
: []
|
|
5145
|
-
|
|
5146
|
-
for (const remoteSession of remoteUserSyncSessions) {
|
|
5147
|
-
const localSession = this.#userSyncSessions.get(
|
|
5148
|
-
remoteSession.ownerAddress
|
|
5149
|
-
)
|
|
5150
|
-
if (!localSession || localSession.syncId !== remoteSession.syncId) {
|
|
5151
|
-
continue
|
|
5152
|
-
}
|
|
5153
|
-
await this.#mergeUserSyncWriterCoreKeys(
|
|
5154
|
-
localSession,
|
|
5155
|
-
remoteSession.writerCoreKeys
|
|
5156
|
-
)
|
|
5157
|
-
}
|
|
5158
|
-
|
|
5159
4777
|
this.emit('channel:peer:online', {
|
|
5160
4778
|
peerId: msg.peerId,
|
|
5161
4779
|
authorName: msg.authorName,
|