most-box 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +26 -0
  2. package/cli.js +2 -2
  3. package/out/404/index.html +15 -0
  4. package/out/404.html +15 -0
  5. package/out/__next.__PAGE__.txt +9 -0
  6. package/out/__next._full.txt +18 -0
  7. package/out/__next._head.txt +5 -0
  8. package/out/__next._index.txt +6 -0
  9. package/out/__next._tree.txt +2 -0
  10. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_buildManifest.js +11 -0
  11. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_clientMiddlewareManifest.js +1 -0
  12. package/out/_next/static/0h4f4QFk_KC9FlSRfQACk/_ssgManifest.js +1 -0
  13. package/out/_next/static/chunks/00l-yd3t8dvwz.js +5 -0
  14. package/out/_next/static/chunks/03k8t3tgym~8~.js +1 -0
  15. package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
  16. package/out/_next/static/chunks/09vfh8lfuacc0.css +1 -0
  17. package/out/_next/static/chunks/0bogtdbh.dcu1.js +1 -0
  18. package/out/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
  19. package/out/_next/static/chunks/0f73psqhr8dre.css +1 -0
  20. package/out/_next/static/chunks/0fbi7z4_.4j1j.js +1 -0
  21. package/out/_next/static/chunks/0ht900cau6_ur.js +31 -0
  22. package/out/_next/static/chunks/0ohm.ia-4ec60.js +1 -0
  23. package/out/_next/static/chunks/0u5ydb-f0.vxl.js +1 -0
  24. package/out/_next/static/chunks/14t2m1on-s5v~.js +1 -0
  25. package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +1 -0
  26. package/out/_not-found/__next._full.txt +16 -0
  27. package/out/_not-found/__next._head.txt +5 -0
  28. package/out/_not-found/__next._index.txt +6 -0
  29. package/out/_not-found/__next._not-found/__PAGE__.txt +5 -0
  30. package/out/_not-found/__next._not-found.txt +5 -0
  31. package/out/_not-found/__next._tree.txt +2 -0
  32. package/out/_not-found/index.html +15 -0
  33. package/out/_not-found/index.txt +16 -0
  34. package/out/app.css +1535 -0
  35. package/out/bundle.js +107 -0
  36. package/out/bundle.js.map +7 -0
  37. package/out/chat/__next._full.txt +19 -0
  38. package/out/chat/__next._head.txt +5 -0
  39. package/out/chat/__next._index.txt +6 -0
  40. package/out/chat/__next._tree.txt +3 -0
  41. package/out/chat/__next.chat/__PAGE__.txt +9 -0
  42. package/out/chat/__next.chat.txt +5 -0
  43. package/out/chat/index.html +15 -0
  44. package/out/chat/index.txt +19 -0
  45. package/out/chat-page.js +112 -0
  46. package/out/chat.css +378 -0
  47. package/out/favicon.ico +0 -0
  48. package/out/index.html +15 -0
  49. package/out/index.js +148 -0
  50. package/out/index.txt +18 -0
  51. package/package.json +11 -6
  52. package/public/app.css +20 -4
  53. package/public/bundle.js +1 -1
  54. package/public/bundle.js.map +7 -0
  55. package/public/chat-page.js +112 -0
  56. package/public/chat.css +378 -0
  57. package/public/index.js +148 -0
  58. package/server.js +188 -6
  59. package/src/config.js +12 -1
  60. package/src/core/cid.js +7 -3
  61. package/src/index.js +475 -7
  62. package/src/utils/api.js +6 -0
  63. package/build.mjs +0 -40
  64. package/public/app.jsx +0 -1543
  65. package/public/bundle.css +0 -1
  66. package/public/error-boundary.jsx +0 -50
  67. package/public/index.html +0 -16
  68. package/public/index.jsx +0 -20
package/src/index.js CHANGED
@@ -12,7 +12,9 @@ import EventEmitter from 'eventemitter3'
12
12
  import Hyperswarm from 'hyperswarm'
13
13
  import Corestore from 'corestore'
14
14
  import Hyperdrive from 'hyperdrive'
15
+ import Hypercore from 'hypercore'
15
16
  import b4a from 'b4a'
17
+ import crypto from 'node:crypto'
16
18
  import { CID } from 'multiformats/cid'
17
19
  import fs from 'node:fs'
18
20
  import path from 'node:path'
@@ -20,7 +22,7 @@ import path from 'node:path'
20
22
  import { calculateCid, parseMostLink } from './core/cid.js'
21
23
  import { sanitizeFilename, validateAndSanitizePath, validateFileSize, checkDirectoryWritable, formatFileSize } from './utils/security.js'
22
24
  import { ValidationError, PathSecurityError, FileSizeError, PeerNotFoundError, IntegrityError, PermissionError, EngineNotInitializedError } from './utils/errors.js'
23
- import { GLOBAL_SHARED_SEED_STRING, MAX_FILE_SIZE, CONNECTION_TIMEOUT, DOWNLOAD_TIMEOUT, SWARM_BOOTSTRAP, MAX_PEERS, SWARM_KEEP_ALIVE_INTERVAL, SWARM_RANDOM_PUNCH_INTERVAL, DRIVE_ENTRY_TIMEOUT, DRIVE_SYNC_TIMEOUT, STREAM_READ_TIMEOUT, DOWNLOAD_POLL_INTERVAL, PROGRESS_THROTTLE, DEFAULT_READ_LIMIT } from './config.js'
25
+ import { GLOBAL_SHARED_SEED_STRING, MAX_FILE_SIZE, CONNECTION_TIMEOUT, DOWNLOAD_TIMEOUT, SWARM_BOOTSTRAP, MAX_PEERS, SWARM_KEEP_ALIVE_INTERVAL, SWARM_RANDOM_PUNCH_INTERVAL, DRIVE_ENTRY_TIMEOUT, DRIVE_SYNC_TIMEOUT, STREAM_READ_TIMEOUT, DOWNLOAD_POLL_INTERVAL, PROGRESS_THROTTLE, DEFAULT_READ_LIMIT, CHANNEL_NAME_MIN_LENGTH, CHANNEL_NAME_MAX_LENGTH, CHANNEL_NAME_REGEX, CHANNEL_NAME_PREFIX, CHANNEL_TOPIC_STRING, CHANNEL_MESSAGE_LIMIT, MAX_MESSAGE_LENGTH } from './config.js'
24
26
 
25
27
  export class MostBoxEngine extends EventEmitter {
26
28
  #store = null
@@ -33,6 +35,11 @@ export class MostBoxEngine extends EventEmitter {
33
35
  #activeDownloads = new Map()
34
36
  #drivePromises = new Map()
35
37
 
38
+ #channels = []
39
+ #channelCores = new Map()
40
+ #channelDiscoveries = new Map()
41
+ #channelPeers = new Map()
42
+
36
43
  /**
37
44
  * 创建新的 MostBoxEngine 实例
38
45
  * @param {object} options - 配置选项
@@ -114,16 +121,14 @@ export class MostBoxEngine extends EventEmitter {
114
121
  })
115
122
 
116
123
  this.#swarm.on('connection', (conn, info) => {
117
- console.log(`[MostBox] New peer connection established`)
118
124
  conn.on('error', (err) => {
119
125
  if (err.code === 'SSL_ERROR' || err.message?.includes('handshake')) {
120
- console.warn('[MostBox] Connection warning:', err.message)
121
126
  return
122
127
  }
123
- console.error('[MostBox] Connection error:', err.message)
124
128
  })
125
129
 
126
130
  this.#store.replicate(conn)
131
+ this.#handleChannelConnection(conn).catch(() => {})
127
132
  this.emit('connection', conn)
128
133
  })
129
134
 
@@ -133,6 +138,41 @@ export class MostBoxEngine extends EventEmitter {
133
138
  this.#trashFiles = this.#loadTrashMetadata()
134
139
  console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
135
140
 
141
+ this.#channels = this.#loadChannelsMetadata()
142
+ console.log(`[MostBox] Loaded ${this.#channels.length} channels`)
143
+
144
+ for (const channel of this.#channels) {
145
+ try {
146
+ const ns = this.#store.namespace(`channel-${channel.name}`)
147
+ const core = ns.get({ key: b4a.from(channel.coreKey, 'hex'), valueEncoding: 'json' })
148
+ await core.ready()
149
+ this.#channelCores.set(channel.name, core)
150
+ this.#channelPeers.set(channel.name, new Map())
151
+
152
+ let lastCoreLength = core.length
153
+ core.on('append', async () => {
154
+ if (core.length > lastCoreLength) {
155
+ for (let i = lastCoreLength; i < core.length; i++) {
156
+ try {
157
+ const entry = await core.get(i)
158
+ if (entry && entry.type === 'message') {
159
+ this.emit('channel:message', { channel: channel.name, message: entry })
160
+ }
161
+ } catch {}
162
+ }
163
+ lastCoreLength = core.length
164
+ }
165
+ })
166
+
167
+ const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
168
+ const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
169
+ this.#channelDiscoveries.set(channel.name, discovery)
170
+ console.log(`[MostBox] Rejoined channel: ${channel.name}`)
171
+ } catch (err) {
172
+ console.warn(`[MostBox] Failed to rejoin channel ${channel.name}:`, err.message)
173
+ }
174
+ }
175
+
136
176
  this.#initialized = true
137
177
  console.log(`[MostBox] Engine initialized successfully`)
138
178
  this.emit('ready')
@@ -158,6 +198,14 @@ export class MostBoxEngine extends EventEmitter {
158
198
  await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
159
199
  this.#drives.clear()
160
200
 
201
+ for (const core of this.#channelCores.values()) {
202
+ try { await core.close() } catch {}
203
+ }
204
+ this.#channelCores.clear()
205
+ this.#channelDiscoveries.clear()
206
+ this.#channelPeers.clear()
207
+ this.#channels = []
208
+
161
209
  if (this.#swarm) {
162
210
  await this.#swarm.destroy()
163
211
  this.#swarm = null
@@ -316,7 +364,7 @@ export class MostBoxEngine extends EventEmitter {
316
364
 
317
365
  const result = {
318
366
  cid: cidString,
319
- link: `most://${cidString}`,
367
+ link: `most://${cidString}?filename=${encodeURIComponent(safeFileName)}`,
320
368
  fileName: safeFileName
321
369
  }
322
370
 
@@ -358,6 +406,8 @@ export class MostBoxEngine extends EventEmitter {
358
406
  }
359
407
  }
360
408
 
409
+ const linkFileName = parsed.fileName
410
+
361
411
  const parsedCid = CID.parse(cidString)
362
412
  const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
363
413
 
@@ -422,8 +472,9 @@ export class MostBoxEngine extends EventEmitter {
422
472
  // 下载文件
423
473
  for (const entry of entries) {
424
474
  const cleanKey = entry.key.replace(/^[\/\\]/, '')
425
- // 用原始文件名作为 displayName
426
- const sanitizedFileName = sanitizeFilename(cleanKey)
475
+ const sanitizedFileName = linkFileName
476
+ ? sanitizeFilename(linkFileName)
477
+ : sanitizeFilename(cleanKey)
427
478
 
428
479
  let totalBytes = 0
429
480
  try {
@@ -971,6 +1022,328 @@ export class MostBoxEngine extends EventEmitter {
971
1022
  return drive
972
1023
  }
973
1024
 
1025
+ // --- 频道管理 ---
1026
+
1027
+ /**
1028
+ * 创建或加入频道
1029
+ * @param {string} name - 频道名
1030
+ * @param {string} [type='personal'] - 频道类型
1031
+ * @returns {Promise<{ name: string, key: string }>}
1032
+ */
1033
+ async createChannel(name, type = 'personal') {
1034
+ this.#ensureInitialized()
1035
+
1036
+ if (!CHANNEL_NAME_REGEX.test(name)) {
1037
+ throw new Error('频道名只能包含字母、数字、下划线和连字符')
1038
+ }
1039
+ if (name.length < CHANNEL_NAME_MIN_LENGTH) {
1040
+ throw new Error(`频道名至少 ${CHANNEL_NAME_MIN_LENGTH} 个字符`)
1041
+ }
1042
+ if (name.length > CHANNEL_NAME_MAX_LENGTH) {
1043
+ throw new Error(`频道名最多 ${CHANNEL_NAME_MAX_LENGTH} 个字符`)
1044
+ }
1045
+
1046
+ const existing = this.#channels.find(c => c.name === name)
1047
+ if (existing) {
1048
+ return { name: existing.name, key: existing.coreKey }
1049
+ }
1050
+
1051
+ const ns = this.#store.namespace(`channel-${name}`)
1052
+ const core = ns.get({ name: 'messages', valueEncoding: 'json' })
1053
+ await core.ready()
1054
+
1055
+ const discoveryKey = this.#generateChannelDiscoveryKey(name)
1056
+ const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
1057
+ await discovery.flushed()
1058
+
1059
+ let lastCoreLength = core.length
1060
+ core.on('append', async () => {
1061
+ if (core.length > lastCoreLength) {
1062
+
1063
+ for (let i = lastCoreLength; i < core.length; i++) {
1064
+ try {
1065
+ const entry = await core.get(i)
1066
+ if (entry && entry.type === 'message') {
1067
+
1068
+ this.emit('channel:message', { channel: name, message: entry })
1069
+ }
1070
+ } catch (err) {
1071
+
1072
+ }
1073
+ }
1074
+ lastCoreLength = core.length
1075
+ }
1076
+ })
1077
+
1078
+ const channelInfo = {
1079
+ name,
1080
+ discoveryKey: b4a.toString(discoveryKey, 'hex'),
1081
+ coreKey: b4a.toString(core.key, 'hex'),
1082
+ createdAt: new Date().toISOString(),
1083
+ type
1084
+ }
1085
+
1086
+ this.#channels.push(channelInfo)
1087
+ this.#channelCores.set(name, core)
1088
+ this.#channelPeers.set(name, new Map())
1089
+ this.#saveChannelsMetadata()
1090
+
1091
+ console.log(`[MostBox] Channel created: ${name}`)
1092
+ this.emit('channel:joined', { name, key: channelInfo.coreKey })
1093
+
1094
+ return { name, key: channelInfo.coreKey }
1095
+ }
1096
+
1097
+ /**
1098
+ * 加入已有频道(通过频道名和 coreKey)
1099
+ * @param {string} name - 频道名
1100
+ * @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
1101
+ * @returns {Promise<{ name: string, key: string }>}
1102
+ */
1103
+ async joinChannel(name, coreKey = null) {
1104
+ this.#ensureInitialized()
1105
+
1106
+ const existing = this.#channels.find(c => c.name === name)
1107
+ if (existing) {
1108
+ return { name: existing.name, key: existing.coreKey }
1109
+ }
1110
+
1111
+ if (!coreKey) {
1112
+ throw new Error('加入已有频道需要提供 coreKey')
1113
+ }
1114
+
1115
+ const ns = this.#store.namespace(`channel-${name}`)
1116
+ const core = ns.get({ key: b4a.from(coreKey, 'hex'), valueEncoding: 'json' })
1117
+ await core.ready()
1118
+
1119
+ const discoveryKey = this.#generateChannelDiscoveryKey(name)
1120
+ const discovery = this.#swarm.join(discoveryKey, { server: true, client: true })
1121
+ await discovery.flushed()
1122
+
1123
+ let lastCoreLength = core.length
1124
+ core.on('append', async () => {
1125
+ if (core.length > lastCoreLength) {
1126
+
1127
+ for (let i = lastCoreLength; i < core.length; i++) {
1128
+ try {
1129
+ const entry = await core.get(i)
1130
+ if (entry && entry.type === 'message') {
1131
+
1132
+ this.emit('channel:message', { channel: name, message: entry })
1133
+ }
1134
+ } catch (err) {
1135
+
1136
+ }
1137
+ }
1138
+ lastCoreLength = core.length
1139
+ }
1140
+ })
1141
+
1142
+ const channelInfo = {
1143
+ name,
1144
+ discoveryKey: b4a.toString(discoveryKey, 'hex'),
1145
+ coreKey,
1146
+ createdAt: new Date().toISOString(),
1147
+ type: 'group'
1148
+ }
1149
+
1150
+ this.#channels.push(channelInfo)
1151
+ this.#channelCores.set(name, core)
1152
+ this.#channelPeers.set(name, new Map())
1153
+ this.#saveChannelsMetadata()
1154
+
1155
+ console.log(`[MostBox] Joined channel: ${name}`)
1156
+ this.emit('channel:joined', { name, key: coreKey })
1157
+
1158
+ return { name, key: coreKey }
1159
+ }
1160
+
1161
+ /**
1162
+ * 离开频道
1163
+ * @param {string} name - 频道名
1164
+ * @returns {Promise<string[]>} 剩余频道列表
1165
+ */
1166
+ async leaveChannel(name) {
1167
+ this.#ensureInitialized()
1168
+
1169
+ const index = this.#channels.findIndex(c => c.name === name)
1170
+ if (index === -1) {
1171
+ throw new Error('频道不存在')
1172
+ }
1173
+
1174
+ const channel = this.#channels[index]
1175
+
1176
+ const discovery = this.#channelDiscoveries.get(name)
1177
+ if (discovery) {
1178
+ try {
1179
+ await this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex'))
1180
+ } catch {}
1181
+ this.#channelDiscoveries.delete(name)
1182
+ }
1183
+
1184
+ const core = this.#channelCores.get(name)
1185
+ if (core) {
1186
+ try {
1187
+ await core.close()
1188
+ } catch {}
1189
+ this.#channelCores.delete(name)
1190
+ }
1191
+
1192
+ this.#channelPeers.delete(name)
1193
+ this.#channels.splice(index, 1)
1194
+ this.#saveChannelsMetadata()
1195
+
1196
+ console.log(`[MostBox] Left channel: ${name}`)
1197
+ this.emit('channel:left', { name })
1198
+
1199
+ return this.listChannels()
1200
+ }
1201
+
1202
+ /**
1203
+ * 列出所有频道
1204
+ * @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
1205
+ */
1206
+ listChannels() {
1207
+ this.#ensureInitialized()
1208
+
1209
+ return this.#channels.map(c => ({
1210
+ name: c.name,
1211
+ coreKey: c.coreKey,
1212
+ createdAt: c.createdAt,
1213
+ type: c.type,
1214
+ peerCount: (this.#channelPeers.get(c.name) || new Map()).size
1215
+ }))
1216
+ }
1217
+
1218
+ /**
1219
+ * 获取频道消息
1220
+ * @param {string} name - 频道名
1221
+ * @param {object} [options] - 选项
1222
+ * @param {number} [options.limit=100] - 消息数量
1223
+ * @param {number} [options.offset=0] - 偏移量
1224
+ * @returns {Promise<Array>}
1225
+ */
1226
+ async getChannelMessages(name, options = {}) {
1227
+ this.#ensureInitialized()
1228
+
1229
+ const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
1230
+
1231
+ const core = this.#channelCores.get(name)
1232
+ if (!core) {
1233
+ throw new Error('频道未初始化')
1234
+ }
1235
+
1236
+ const messages = []
1237
+ const total = core.length
1238
+ const start = Math.max(0, total - offset - limit)
1239
+ const end = total - offset
1240
+
1241
+ for (let i = start; i < end; i++) {
1242
+ try {
1243
+ const entry = await core.get(i)
1244
+ messages.push(entry)
1245
+ } catch {
1246
+ break
1247
+ }
1248
+ }
1249
+
1250
+ return messages
1251
+ }
1252
+
1253
+ /**
1254
+ * 发送消息到频道
1255
+ * @param {string} name - 频道名
1256
+ * @param {string} content - 消息内容
1257
+ * @param {string} [authorName] - 作者名(可选,默认使用 displayName)
1258
+ * @returns {Promise<object>}
1259
+ */
1260
+ async sendMessage(name, content, authorName = null) {
1261
+ this.#ensureInitialized()
1262
+
1263
+ const core = this.#channelCores.get(name)
1264
+ if (!core) {
1265
+ throw new Error('频道未初始化')
1266
+ }
1267
+
1268
+ if (!content || !content.trim()) {
1269
+ throw new Error('消息内容不能为空')
1270
+ }
1271
+
1272
+ const trimmed = content.trim()
1273
+ if (trimmed.length > MAX_MESSAGE_LENGTH) {
1274
+ throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
1275
+ }
1276
+
1277
+ const displayName = this.getNodeId().slice(0, 4)
1278
+
1279
+ const message = {
1280
+ type: 'message',
1281
+ author: this.getNodeId(),
1282
+ authorName: displayName,
1283
+ content: trimmed,
1284
+ timestamp: Date.now()
1285
+ }
1286
+
1287
+ await core.append(message)
1288
+
1289
+ this.emit('channel:message', { channel: name, message })
1290
+
1291
+ return message
1292
+ }
1293
+
1294
+ /**
1295
+ * 获取频道内在线用户
1296
+ * @param {string} name - 频道名
1297
+ * @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
1298
+ */
1299
+ getChannelPeers(name) {
1300
+ this.#ensureInitialized()
1301
+
1302
+ const peers = this.#channelPeers.get(name)
1303
+ if (!peers) {
1304
+ throw new Error('频道未初始化')
1305
+ }
1306
+
1307
+ return [...peers.values()].map(p => ({
1308
+ peerId: p.peerId,
1309
+ authorName: p.authorName,
1310
+ lastSeen: p.lastSeen
1311
+ }))
1312
+ }
1313
+
1314
+ /**
1315
+ * 获取显示名
1316
+ * @returns {string|null}
1317
+ */
1318
+ getDisplayName() {
1319
+ try {
1320
+ const configPath = this.#getConfigPath()
1321
+ if (fs.existsSync(configPath)) {
1322
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
1323
+ return config.displayName || null
1324
+ }
1325
+ } catch {}
1326
+ return null
1327
+ }
1328
+
1329
+ /**
1330
+ * 设置显示名
1331
+ * @param {string} name - 显示名
1332
+ */
1333
+ setDisplayName(name) {
1334
+ try {
1335
+ const configPath = this.#getConfigPath()
1336
+ const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {}
1337
+ config.displayName = name.trim()
1338
+ const tmpPath = configPath + '.tmp'
1339
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
1340
+ fs.renameSync(tmpPath, configPath)
1341
+ return true
1342
+ } catch {
1343
+ return false
1344
+ }
1345
+ }
1346
+
974
1347
  // --- 私有方法 ---
975
1348
 
976
1349
  #ensureInitialized() {
@@ -1073,6 +1446,101 @@ export class MostBoxEngine extends EventEmitter {
1073
1446
  }
1074
1447
  }
1075
1448
 
1449
+ #getChannelsMetadataPath() {
1450
+ return path.join(this.#options.dataPath, 'channels.json')
1451
+ }
1452
+
1453
+ #getConfigPath() {
1454
+ return path.join(this.#options.dataPath, 'channel-config.json')
1455
+ }
1456
+
1457
+ #loadChannelsMetadata() {
1458
+ try {
1459
+ const metadataPath = this.#getChannelsMetadataPath()
1460
+ if (fs.existsSync(metadataPath)) {
1461
+ const data = fs.readFileSync(metadataPath, 'utf-8')
1462
+ return JSON.parse(data)
1463
+ }
1464
+ } catch (err) {
1465
+ console.warn('Failed to load channels metadata, using empty list:', err.message)
1466
+ }
1467
+ return []
1468
+ }
1469
+
1470
+ #saveChannelsMetadata() {
1471
+ try {
1472
+ const metadataPath = this.#getChannelsMetadataPath()
1473
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#channels, null, 2))
1474
+ } catch (err) {
1475
+ console.error('Failed to save channels metadata:', err.message)
1476
+ }
1477
+ }
1478
+
1479
+ #generateChannelDiscoveryKey(name) {
1480
+ const hash = crypto.createHash('sha256')
1481
+ .update(`${CHANNEL_NAME_PREFIX}${name}`)
1482
+ .digest()
1483
+ return hash
1484
+ }
1485
+
1486
+ async #handleChannelConnection(conn) {
1487
+ const stream = conn
1488
+ let connectedPeerId = null
1489
+ let connectedAuthorName = null
1490
+
1491
+ const helloMessage = JSON.stringify({
1492
+ type: 'channel-hello',
1493
+ peerId: this.getNodeId(),
1494
+ authorName: this.getNodeId().slice(0, 4),
1495
+ channels: this.#channels.map(c => c.name)
1496
+ })
1497
+
1498
+ try {
1499
+ stream.write(helloMessage)
1500
+ } catch {
1501
+ return
1502
+ }
1503
+
1504
+ stream.on('data', async (data) => {
1505
+ try {
1506
+ const msg = JSON.parse(data.toString())
1507
+ if (msg.type === 'channel-hello') {
1508
+ connectedPeerId = msg.peerId
1509
+ connectedAuthorName = msg.authorName
1510
+
1511
+ const theirChannels = new Set(msg.channels || [])
1512
+ for (const [name, peers] of this.#channelPeers) {
1513
+ if (theirChannels.has(name)) {
1514
+ peers.set(msg.peerId, {
1515
+ peerId: msg.peerId,
1516
+ authorName: msg.authorName,
1517
+ lastSeen: Date.now()
1518
+ })
1519
+ const core = this.#channelCores.get(name)
1520
+ if (core) {
1521
+ const ns = this.#store.namespace(`channel-${name}`)
1522
+ ns.replicate(conn).catch(() => {})
1523
+ }
1524
+ }
1525
+ }
1526
+ this.emit('channel:peer:online', { peerId: msg.peerId, authorName: msg.authorName })
1527
+ }
1528
+ } catch {}
1529
+ })
1530
+
1531
+ stream.on('close', () => {
1532
+ if (connectedPeerId) {
1533
+ for (const [name, peers] of this.#channelPeers) {
1534
+ if (peers.has(connectedPeerId)) {
1535
+ const peer = peers.get(connectedPeerId)
1536
+ peers.delete(connectedPeerId)
1537
+ this.emit('channel:peer:offline', { peerId: connectedPeerId, authorName: peer?.authorName })
1538
+ }
1539
+ }
1540
+ }
1541
+ })
1542
+ }
1543
+
1076
1544
  /**
1077
1545
  * 等待驱动器内容从对等节点或本地可用
1078
1546
  * @param {Hyperdrive} drive - 要检查的驱动器
@@ -0,0 +1,6 @@
1
+ import ky from 'ky'
2
+
3
+ export const api = ky.create({
4
+ prefix: '',
5
+ throwHttpErrors: false,
6
+ })
package/build.mjs DELETED
@@ -1,40 +0,0 @@
1
- import * as esbuild from 'esbuild'
2
- import fs from 'fs'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
- const publicDir = path.join(__dirname, 'public')
8
- const outFile = path.join(publicDir, 'bundle.js')
9
-
10
- const isWatch = process.argv.includes('--watch')
11
- const isDev = process.argv.includes('--dev')
12
-
13
- const buildOptions = {
14
- entryPoints: [path.join(publicDir, 'index.jsx')],
15
- bundle: true,
16
- outfile: outFile,
17
- format: 'esm',
18
- jsx: 'automatic',
19
- jsxImportSource: 'react',
20
- loader: {
21
- '.js': 'jsx',
22
- '.jsx': 'jsx',
23
- },
24
- define: {
25
- 'process.env.NODE_ENV': isDev ? '"development"' : '"production"'
26
- },
27
- minify: !isDev,
28
- sourcemap: isDev,
29
- target: ['es2020'],
30
- logLevel: 'info',
31
- }
32
-
33
- if (isWatch) {
34
- const ctx = await esbuild.context(buildOptions)
35
- await ctx.watch()
36
- console.log('[Build] Watching for changes...')
37
- } else {
38
- await esbuild.build(buildOptions)
39
- console.log('[Build] Done.')
40
- }