most-box 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/README.md +3 -2
  2. package/out/404/index.html +2 -2
  3. package/out/404.html +2 -2
  4. package/out/__next.__PAGE__.txt +6 -6
  5. package/out/__next._full.txt +23 -20
  6. package/out/__next._head.txt +3 -3
  7. package/out/__next._index.txt +8 -6
  8. package/out/__next._tree.txt +6 -4
  9. package/out/_next/static/chunks/0-n3pg7th.zza.js +1 -0
  10. package/out/_next/static/chunks/0.4j.0k5a64vg.js +1 -0
  11. package/out/_next/static/chunks/0.ozi1_x2.m.~.js +1 -0
  12. package/out/_next/static/chunks/0.t5wlt51zou5.js +1 -0
  13. package/out/_next/static/chunks/0.w4hkvap~bva.js +1 -0
  14. package/out/_next/static/chunks/00-u5nq76f0.j.js +1 -0
  15. package/out/_next/static/chunks/00d9h1tddnnnd.js +1 -0
  16. package/out/_next/static/chunks/00fm8lijienf1.js +1 -0
  17. package/out/_next/static/chunks/00o9ht.f2qm00.css +4 -0
  18. package/out/_next/static/chunks/00tkdqwxch-3s.js +1 -0
  19. package/out/_next/static/chunks/00zi-erhjrny2.js +2 -0
  20. package/out/_next/static/chunks/01l3o90g~1z42.js +1 -0
  21. package/out/_next/static/chunks/01mfky9camw6i.js +1 -0
  22. package/out/_next/static/chunks/01r.v-pqs1vrm.js +1 -0
  23. package/out/_next/static/chunks/03edqrb4zdj~g.js +31 -0
  24. package/out/_next/static/chunks/03h_6oo-gqkhz.js +1 -0
  25. package/out/_next/static/chunks/04hcgsanv1hhu.js +1 -0
  26. package/out/_next/static/chunks/05g2q0w5b34.g.js +1 -0
  27. package/out/_next/static/chunks/05of77xycbt8~.js +1 -0
  28. package/out/_next/static/chunks/05zwemzfjx3sh.js +1 -0
  29. package/out/_next/static/chunks/06dpc5df94.v1.js +1 -0
  30. package/out/_next/static/chunks/06e1~1-z_ic9a.js +1 -0
  31. package/out/_next/static/chunks/075s7sn.ns~u5.js +1 -0
  32. package/out/_next/static/chunks/07dynrbvd3.f4.js +1 -0
  33. package/out/_next/static/chunks/07p~uva5pwgwe.js +1 -0
  34. package/out/_next/static/chunks/07r9nn-pzlgg1.js +1 -0
  35. package/out/_next/static/chunks/08.72abkgwy9g.js +1 -0
  36. package/out/_next/static/chunks/084xf0edl9sfo.js +1 -0
  37. package/out/_next/static/chunks/08576xhv~~jck.js +1 -0
  38. package/out/_next/static/chunks/08u211~k~qu52.js +1 -0
  39. package/out/_next/static/chunks/098.p.2-zm4p7.js +1 -0
  40. package/out/_next/static/chunks/09f1gfke9m5wg.css +1 -0
  41. package/out/_next/static/chunks/09ngvtajm7e5y.js +1 -0
  42. package/out/_next/static/chunks/09ps~-43n5qyo.js +1 -0
  43. package/out/_next/static/chunks/09v7_0gclxr46.js +1 -0
  44. package/out/_next/static/chunks/09xyi6fpro_d-.css +1 -0
  45. package/out/_next/static/chunks/09yql86dir9c4.js +1 -0
  46. package/out/_next/static/chunks/09zmlfljowj1~.js +1 -0
  47. package/out/_next/static/chunks/0_npg_pcoywti.js +5 -0
  48. package/out/_next/static/chunks/0_r_mk1~6bosc.js +1 -0
  49. package/out/_next/static/chunks/0_s~ebb-7b2hr.js +1 -0
  50. package/out/_next/static/chunks/0_w-0-2z5oqd_.js +1 -0
  51. package/out/_next/static/chunks/0ao1lbi4b.sfa.js +1 -0
  52. package/out/_next/static/chunks/0arm0a6adt7cc.css +1 -0
  53. package/out/_next/static/chunks/0bld2u_ld~va2.js +1 -0
  54. package/out/_next/static/chunks/0bliugh5lxw55.js +1 -0
  55. package/out/_next/static/chunks/{0e_h0d3ekzks8.css → 0c9j3eq_14vv2.css} +1 -1
  56. package/out/_next/static/chunks/0cn9a7aimbdzq.js +1 -0
  57. package/out/_next/static/chunks/0d3f-nk3c.2re.js +1 -0
  58. package/out/_next/static/chunks/0d4bueddmcnca.js +1 -0
  59. package/out/_next/static/chunks/0dtohpf7~3d12.js +1 -0
  60. package/out/_next/static/chunks/0e-3e8h7g99yf.js +1 -0
  61. package/out/_next/static/chunks/0e531nije_ln2.js +1 -0
  62. package/out/_next/static/chunks/0e5zvj_rh0z3m.js +1 -0
  63. package/out/_next/static/chunks/0f4y~rkk-n81e.js +1 -0
  64. package/out/_next/static/chunks/0fk~0~p7ivfn1.js +1 -0
  65. package/out/_next/static/chunks/0fw6juc~lsj3z.js +1 -0
  66. package/out/_next/static/chunks/0g0u7785a73vo.js +1 -0
  67. package/out/_next/static/chunks/0g_fpgh7drfda.js +1 -0
  68. package/out/_next/static/chunks/0gtwvy1z9ksa7.css +1 -0
  69. package/out/_next/static/chunks/0gze5uso1mbe9.js +1 -0
  70. package/out/_next/static/chunks/0h4r.qtmpa6eh.js +1 -0
  71. package/out/_next/static/chunks/0hf.aosc-7172.js +1 -0
  72. package/out/_next/static/chunks/0hgz35c1ejbs9.js +1 -0
  73. package/out/_next/static/chunks/0hrw-r.xmvmsq.js +1 -0
  74. package/out/_next/static/chunks/0hzg4al.v~8~m.js +1 -0
  75. package/out/_next/static/chunks/0ip9xrols_83o.js +1 -0
  76. package/out/_next/static/chunks/0j27tcmtt4ly7.js +1 -0
  77. package/out/_next/static/chunks/0j3v4mq67wtnh.js +1 -0
  78. package/out/_next/static/chunks/0j4-d0qf.v~kn.js +1 -0
  79. package/out/_next/static/chunks/0jhdeq.j9_02m.js +1 -0
  80. package/out/_next/static/chunks/0jy63h3i-y69i.js +1 -0
  81. package/out/_next/static/chunks/0kdnx_u-60k9s.js +1 -0
  82. package/out/_next/static/chunks/0kq~edq42o1-c.js +1 -0
  83. package/out/_next/static/chunks/0l682p362d-5w.js +1 -0
  84. package/out/_next/static/chunks/0lkmf5ry.s_7w.js +1 -0
  85. package/out/_next/static/chunks/0m68p9txef5rs.js +1 -0
  86. package/out/_next/static/chunks/0mme-fm5d2oz2.js +1 -0
  87. package/out/_next/static/chunks/0myp4sjagr~h0.js +1 -0
  88. package/out/_next/static/chunks/0n.qlfk~z7o.6.js +1 -0
  89. package/out/_next/static/chunks/0n4t80gjc3q5h.js +1 -0
  90. package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 0o6lrkxy4jwag.js} +1 -1
  91. package/out/_next/static/chunks/0o98f1yq..o.8.js +1 -0
  92. package/out/_next/static/chunks/0oz3yl6_-716p.js +1 -0
  93. package/out/_next/static/chunks/0p486m03-zfoi.js +1 -0
  94. package/out/_next/static/chunks/0qou.u2e2dy48.css +24 -0
  95. package/out/_next/static/chunks/0qqupeexg83u7.js +1 -0
  96. package/out/_next/static/chunks/0r1~k82nji8sf.js +1 -0
  97. package/out/_next/static/chunks/0rb-ri481.kc9.js +1 -0
  98. package/out/_next/static/chunks/0rsnmahfd.59p.js +1 -0
  99. package/out/_next/static/chunks/0rt6rgnvr-s_p.js +1 -0
  100. package/out/_next/static/chunks/0runh28p_gg6..js +1 -0
  101. package/out/_next/static/chunks/0shy.t1fwqcev.js +1 -0
  102. package/out/_next/static/chunks/{0d3shmwh5_nmn.js → 0t2xr05rlu96l.js} +1 -1
  103. package/out/_next/static/chunks/0t6h56rhg1y5i.js +1 -0
  104. package/out/_next/static/chunks/0tdqd1zunusgk.js +1 -0
  105. package/out/_next/static/chunks/0ujbnp38x63ek.js +1 -0
  106. package/out/_next/static/chunks/0usvo~vu7r8np.js +736 -0
  107. package/out/_next/static/chunks/0v68pdrp54lb-.js +1 -0
  108. package/out/_next/static/chunks/0v7qp4hv-_._r.js +1 -0
  109. package/out/_next/static/chunks/0vsm0m5sxrb.3.js +1 -0
  110. package/out/_next/static/chunks/0vzlz.iboqo3c.js +1 -0
  111. package/out/_next/static/chunks/0w87vbpkf-ogd.js +1 -0
  112. package/out/_next/static/chunks/0wuwlgcn6gxqt.js +1 -0
  113. package/out/_next/static/chunks/0xgg0~kmf3gd-.js +1 -0
  114. package/out/_next/static/chunks/0xj24-70ptdzp.js +1 -0
  115. package/out/_next/static/chunks/0xl5_avhu._i8.js +1 -0
  116. package/out/_next/static/chunks/0xxlx772fr3x4.js +1 -0
  117. package/out/_next/static/chunks/0y.li-~3oybew.js +1 -0
  118. package/out/_next/static/chunks/0yl2t7cs-n_ng.js +1 -0
  119. package/out/_next/static/chunks/0yq3kh.hchtm_.js +1 -0
  120. package/out/_next/static/chunks/0ys0l5au.9c2c.js +1 -0
  121. package/out/_next/static/chunks/0z48pmi6buytt.js +1 -0
  122. package/out/_next/static/chunks/0zapnvgy89mg..js +1 -0
  123. package/out/_next/static/chunks/0~.-vxi5oc.r0.js +1 -0
  124. package/out/_next/static/chunks/0~3ik-hfp9s-7.js +1 -0
  125. package/out/_next/static/chunks/0~4f5p6tvn1lq.js +1 -0
  126. package/out/_next/static/chunks/0~_0ys.2whxbw.js +1 -0
  127. package/out/_next/static/chunks/0~_ui9l7.2sxf.js +1 -0
  128. package/out/_next/static/chunks/1037jlyw5~7ht.js +1 -0
  129. package/out/_next/static/chunks/1045hfzu533z0.js +1 -0
  130. package/out/_next/static/chunks/104e5nmc.c-pl.js +1 -0
  131. package/out/_next/static/chunks/109taw1pbh-0b.js +1 -0
  132. package/out/_next/static/chunks/10kvl8vj_plm-.js +1 -0
  133. package/out/_next/static/chunks/10x7~onqwp338.js +1 -0
  134. package/out/_next/static/chunks/10ynz1dy483wf.js +1 -0
  135. package/out/_next/static/chunks/11hds.mg~4_r-.js +1 -0
  136. package/out/_next/static/chunks/11ibzaklcauw~.js +1 -0
  137. package/out/_next/static/chunks/11z.0s6.42b.p.js +1 -0
  138. package/out/_next/static/chunks/12-9n56l0y3yr.js +1 -0
  139. package/out/_next/static/chunks/126enaq~f7scl.js +1 -0
  140. package/out/_next/static/chunks/1380op_pfk.qo.js +1 -0
  141. package/out/_next/static/chunks/146oiw1bggtn4.js +1 -0
  142. package/out/_next/static/chunks/14_po2rb_arn4.js +1 -0
  143. package/out/_next/static/chunks/14a4fwbiq.l3z.js +1 -0
  144. package/out/_next/static/chunks/14cowsqn95m1k.js +1 -0
  145. package/out/_next/static/chunks/14dtd3l03v.kx.js +1 -0
  146. package/out/_next/static/chunks/14tm3qa-v9o-4.js +1 -0
  147. package/out/_next/static/chunks/15-o4kb-evqd7.js +1 -0
  148. package/out/_next/static/chunks/157z7bowux3xj.js +1 -0
  149. package/out/_next/static/chunks/15m1_677az2cm.js +1 -0
  150. package/out/_next/static/chunks/15v.~.ne6ogkk.js +1 -0
  151. package/out/_next/static/chunks/16i.qbk8t8gf_.js +1 -0
  152. package/out/_next/static/chunks/16m27azcs4k6w.js +1 -0
  153. package/out/_next/static/chunks/16u9f35gylw8l.js +1 -0
  154. package/out/_next/static/chunks/17ajyb5ogk5yj.js +1 -0
  155. package/out/_next/static/chunks/17dyfxbq8yz8n.js +1 -0
  156. package/out/_next/static/chunks/180zln9pcq9ih.js +1 -0
  157. package/out/_next/static/chunks/1814izi5gh.kp.js +1 -0
  158. package/out/_next/static/chunks/turbopack-0xs6mybc~5t_3.js +1 -0
  159. package/out/_next/static/media/KaTeX_AMS-Regular.0b~8ki5y928w2.woff +0 -0
  160. package/out/_next/static/media/KaTeX_AMS-Regular.0p1vbqd84i2~o.woff2 +0 -0
  161. package/out/_next/static/media/KaTeX_AMS-Regular.173t6ktr7uf-w.ttf +0 -0
  162. package/out/_next/static/media/KaTeX_Caligraphic-Bold.01-pzluls4zgb.woff2 +0 -0
  163. package/out/_next/static/media/KaTeX_Caligraphic-Bold.0x2v1lwn~880f.woff +0 -0
  164. package/out/_next/static/media/KaTeX_Caligraphic-Bold.16zv5fax0h0ka.ttf +0 -0
  165. package/out/_next/static/media/KaTeX_Caligraphic-Regular.02i3z7wig438t.ttf +0 -0
  166. package/out/_next/static/media/KaTeX_Caligraphic-Regular.0rysu1t-ncjq8.woff2 +0 -0
  167. package/out/_next/static/media/KaTeX_Caligraphic-Regular.10927swgekwun.woff +0 -0
  168. package/out/_next/static/media/KaTeX_Fraktur-Bold.0e-16u10iuyyf.woff +0 -0
  169. package/out/_next/static/media/KaTeX_Fraktur-Bold.0et27v~3~4uhe.ttf +0 -0
  170. package/out/_next/static/media/KaTeX_Fraktur-Bold.0w23i72~hprpq.woff2 +0 -0
  171. package/out/_next/static/media/KaTeX_Fraktur-Regular.0b.riegzdfue2.woff +0 -0
  172. package/out/_next/static/media/KaTeX_Fraktur-Regular.0rekyoa-52fj_.woff2 +0 -0
  173. package/out/_next/static/media/KaTeX_Fraktur-Regular.0vjwa15znhk~4.ttf +0 -0
  174. package/out/_next/static/media/KaTeX_Main-Bold.09i7~607shf-h.ttf +0 -0
  175. package/out/_next/static/media/KaTeX_Main-Bold.09lmynrorhcbw.woff +0 -0
  176. package/out/_next/static/media/KaTeX_Main-Bold.16pfc63_du6mx.woff2 +0 -0
  177. package/out/_next/static/media/KaTeX_Main-BoldItalic.0cp37g7x1q8h6.woff +0 -0
  178. package/out/_next/static/media/KaTeX_Main-BoldItalic.0d54rk08rx11s.woff2 +0 -0
  179. package/out/_next/static/media/KaTeX_Main-BoldItalic.15j6k~hix2t_0.ttf +0 -0
  180. package/out/_next/static/media/KaTeX_Main-Italic.0382gqciexmbu.woff +0 -0
  181. package/out/_next/static/media/KaTeX_Main-Italic.06o5nq0_91v60.woff2 +0 -0
  182. package/out/_next/static/media/KaTeX_Main-Italic.0su4i6mm18-wo.ttf +0 -0
  183. package/out/_next/static/media/KaTeX_Main-Regular.08zh8z.7shijf.ttf +0 -0
  184. package/out/_next/static/media/KaTeX_Main-Regular.0diheg01zyoph.woff +0 -0
  185. package/out/_next/static/media/KaTeX_Main-Regular.0kaf-ag2_wkm-.woff2 +0 -0
  186. package/out/_next/static/media/KaTeX_Math-BoldItalic.0ajzxypnbx1h1.ttf +0 -0
  187. package/out/_next/static/media/KaTeX_Math-BoldItalic.0ck1myuerwyqw.woff +0 -0
  188. package/out/_next/static/media/KaTeX_Math-BoldItalic.0ja97dn.cpc87.woff2 +0 -0
  189. package/out/_next/static/media/KaTeX_Math-Italic.09xkhecjcn5r9.woff +0 -0
  190. package/out/_next/static/media/KaTeX_Math-Italic.0x23a-bmp-5tg.ttf +0 -0
  191. package/out/_next/static/media/KaTeX_Math-Italic.0zrha2c4sl2je.woff2 +0 -0
  192. package/out/_next/static/media/KaTeX_SansSerif-Bold.05a9.pc1j_zx9.woff2 +0 -0
  193. package/out/_next/static/media/KaTeX_SansSerif-Bold.0jcl-ayi1uun0.woff +0 -0
  194. package/out/_next/static/media/KaTeX_SansSerif-Bold.0re8y.dm7.mt5.ttf +0 -0
  195. package/out/_next/static/media/KaTeX_SansSerif-Italic.0a0234dc3s62j.woff2 +0 -0
  196. package/out/_next/static/media/KaTeX_SansSerif-Italic.0judofdln9731.woff +0 -0
  197. package/out/_next/static/media/KaTeX_SansSerif-Italic.10z1iap9pfus8.ttf +0 -0
  198. package/out/_next/static/media/KaTeX_SansSerif-Regular.0h9yjlugq4q_e.woff +0 -0
  199. package/out/_next/static/media/KaTeX_SansSerif-Regular.0v6gcj32-czft.woff2 +0 -0
  200. package/out/_next/static/media/KaTeX_SansSerif-Regular.0zm18kga42ebc.ttf +0 -0
  201. package/out/_next/static/media/KaTeX_Script-Regular.0c4.h-mer83d_.woff2 +0 -0
  202. package/out/_next/static/media/KaTeX_Script-Regular.0q14y6zkzlpob.ttf +0 -0
  203. package/out/_next/static/media/KaTeX_Script-Regular.0ze6v4r_-99oy.woff +0 -0
  204. package/out/_next/static/media/KaTeX_Size1-Regular.013x6a4ierotp.woff2 +0 -0
  205. package/out/_next/static/media/KaTeX_Size1-Regular.0kidw0oi.m68o.woff +0 -0
  206. package/out/_next/static/media/KaTeX_Size1-Regular.0m6y-i6wfokni.ttf +0 -0
  207. package/out/_next/static/media/KaTeX_Size2-Regular.0blpmluwilgbg.woff +0 -0
  208. package/out/_next/static/media/KaTeX_Size2-Regular.0d5inmyp-tyv3.woff2 +0 -0
  209. package/out/_next/static/media/KaTeX_Size2-Regular.0wnhnvj-.k9d5.ttf +0 -0
  210. package/out/_next/static/media/KaTeX_Size3-Regular.01h0xm_sfctj3.woff +0 -0
  211. package/out/_next/static/media/KaTeX_Size3-Regular.0iukctyhw5j56.woff2 +0 -0
  212. package/out/_next/static/media/KaTeX_Size3-Regular.0jl8mqyf4gzpn.ttf +0 -0
  213. package/out/_next/static/media/KaTeX_Size4-Regular.0w3.rb_c4stzk.woff2 +0 -0
  214. package/out/_next/static/media/KaTeX_Size4-Regular.0wr_9l81-mu06.ttf +0 -0
  215. package/out/_next/static/media/KaTeX_Size4-Regular.12tvaesf3.zl3.woff +0 -0
  216. package/out/_next/static/media/KaTeX_Typewriter-Regular.0c4zdxz~8frhm.woff2 +0 -0
  217. package/out/_next/static/media/KaTeX_Typewriter-Regular.0cgrzn5l3kao5.woff +0 -0
  218. package/out/_next/static/media/KaTeX_Typewriter-Regular.128~qc3858otl.ttf +0 -0
  219. package/out/_not-found/__next._full.txt +21 -19
  220. package/out/_not-found/__next._head.txt +3 -3
  221. package/out/_not-found/__next._index.txt +8 -6
  222. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  223. package/out/_not-found/__next._not-found.txt +3 -3
  224. package/out/_not-found/__next._tree.txt +3 -2
  225. package/out/_not-found/index.html +2 -2
  226. package/out/_not-found/index.txt +21 -19
  227. package/out/admin/__next._full.txt +23 -0
  228. package/out/admin/__next._head.txt +5 -0
  229. package/out/admin/__next._index.txt +9 -0
  230. package/out/admin/__next._tree.txt +5 -0
  231. package/out/admin/__next.admin.__PAGE__.txt +9 -0
  232. package/out/admin/__next.admin.txt +5 -0
  233. package/out/admin/index.html +15 -0
  234. package/out/admin/index.txt +23 -0
  235. package/out/app/__next._full.txt +15 -13
  236. package/out/app/__next._head.txt +3 -3
  237. package/out/app/__next._index.txt +8 -6
  238. package/out/app/__next._tree.txt +4 -2
  239. package/out/app/__next.app.__PAGE__.txt +4 -4
  240. package/out/app/__next.app.txt +3 -4
  241. package/out/app/index.html +2 -2
  242. package/out/app/index.txt +15 -13
  243. package/out/changelog/__next._full.txt +24 -21
  244. package/out/changelog/__next._head.txt +3 -3
  245. package/out/changelog/__next._index.txt +8 -6
  246. package/out/changelog/__next._tree.txt +5 -3
  247. package/out/changelog/__next.changelog.__PAGE__.txt +5 -5
  248. package/out/changelog/__next.changelog.txt +3 -3
  249. package/out/changelog/index.html +2 -2
  250. package/out/changelog/index.txt +24 -21
  251. package/out/chat/__next._full.txt +16 -14
  252. package/out/chat/__next._head.txt +3 -3
  253. package/out/chat/__next._index.txt +8 -6
  254. package/out/chat/__next._tree.txt +5 -3
  255. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  256. package/out/chat/__next.chat.txt +4 -5
  257. package/out/chat/index.html +2 -2
  258. package/out/chat/index.txt +16 -14
  259. package/out/docs/__next._full.txt +24 -21
  260. package/out/docs/__next._head.txt +3 -3
  261. package/out/docs/__next._index.txt +8 -6
  262. package/out/docs/__next._tree.txt +5 -3
  263. package/out/docs/__next.docs.__PAGE__.txt +5 -5
  264. package/out/docs/__next.docs.txt +3 -3
  265. package/out/docs/getting-started/__next._full.txt +24 -21
  266. package/out/docs/getting-started/__next._head.txt +3 -3
  267. package/out/docs/getting-started/__next._index.txt +8 -6
  268. package/out/docs/getting-started/__next._tree.txt +5 -3
  269. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +5 -5
  270. package/out/docs/getting-started/__next.docs.getting-started.txt +3 -3
  271. package/out/docs/getting-started/__next.docs.txt +3 -3
  272. package/out/docs/getting-started/index.html +2 -2
  273. package/out/docs/getting-started/index.txt +24 -21
  274. package/out/docs/index.html +2 -2
  275. package/out/docs/index.txt +24 -21
  276. package/out/download/__next._full.txt +33 -32
  277. package/out/download/__next._head.txt +3 -3
  278. package/out/download/__next._index.txt +8 -6
  279. package/out/download/__next._tree.txt +5 -3
  280. package/out/download/__next.download.__PAGE__.txt +10 -11
  281. package/out/download/__next.download.txt +3 -3
  282. package/out/download/index.html +2 -2
  283. package/out/download/index.txt +33 -32
  284. package/out/index.html +2 -2
  285. package/out/index.txt +23 -20
  286. package/out/note/__next._full.txt +24 -0
  287. package/out/note/__next._head.txt +5 -0
  288. package/out/note/__next._index.txt +9 -0
  289. package/out/note/__next._tree.txt +4 -0
  290. package/out/note/__next.note.__PAGE__.txt +9 -0
  291. package/out/note/__next.note.txt +5 -0
  292. package/out/note/edit/__next._full.txt +24 -0
  293. package/out/note/edit/__next._head.txt +5 -0
  294. package/out/note/edit/__next._index.txt +9 -0
  295. package/out/note/edit/__next._tree.txt +4 -0
  296. package/out/note/edit/__next.note.edit.__PAGE__.txt +9 -0
  297. package/out/note/edit/__next.note.edit.txt +5 -0
  298. package/out/note/edit/__next.note.txt +5 -0
  299. package/out/note/edit/index.html +15 -0
  300. package/out/note/edit/index.txt +24 -0
  301. package/out/note/index.html +15 -0
  302. package/out/note/index.txt +24 -0
  303. package/out/ping/__next._full.txt +22 -19
  304. package/out/ping/__next._head.txt +3 -3
  305. package/out/ping/__next._index.txt +8 -6
  306. package/out/ping/__next._tree.txt +5 -3
  307. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  308. package/out/ping/__next.ping.txt +3 -3
  309. package/out/ping/index.html +2 -2
  310. package/out/ping/index.txt +22 -19
  311. package/out/web3/__next._full.txt +16 -14
  312. package/out/web3/__next._head.txt +3 -3
  313. package/out/web3/__next._index.txt +8 -6
  314. package/out/web3/__next._tree.txt +5 -3
  315. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  316. package/out/web3/__next.web3.txt +4 -5
  317. package/out/web3/ed25519/__next._full.txt +14 -12
  318. package/out/web3/ed25519/__next._head.txt +3 -3
  319. package/out/web3/ed25519/__next._index.txt +8 -6
  320. package/out/web3/ed25519/__next._tree.txt +5 -3
  321. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  322. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  323. package/out/web3/ed25519/__next.web3.txt +4 -5
  324. package/out/web3/ed25519/index.html +1 -1
  325. package/out/web3/ed25519/index.txt +14 -12
  326. package/out/web3/index.html +2 -2
  327. package/out/web3/index.txt +16 -14
  328. package/out/web3/tools/__next._full.txt +14 -12
  329. package/out/web3/tools/__next._head.txt +3 -3
  330. package/out/web3/tools/__next._index.txt +8 -6
  331. package/out/web3/tools/__next._tree.txt +5 -3
  332. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  333. package/out/web3/tools/__next.web3.tools.txt +3 -3
  334. package/out/web3/tools/__next.web3.txt +4 -5
  335. package/out/web3/tools/index.html +1 -1
  336. package/out/web3/tools/index.txt +14 -12
  337. package/package.json +12 -2
  338. package/server/index.js +558 -101
  339. package/server/src/config.js +4 -0
  340. package/server/src/core/cid.js +53 -32
  341. package/server/src/index.js +772 -122
  342. package/server/src/node/config.js +159 -0
  343. package/server/src/node/logs.js +94 -0
  344. package/server/src/utils/api.js +80 -2
  345. package/server/src/utils/errors.js +7 -0
  346. package/server/src/utils/mostWallet.js +34 -1
  347. package/server/src/utils/noteBackup.js +119 -0
  348. package/server/src/utils/noteUtils.js +120 -0
  349. package/server/src/utils/userIdentity.js +8 -60
  350. package/out/_next/static/chunks/003jnm.v5tzw5.js +0 -1
  351. package/out/_next/static/chunks/00re8v.gbcywn.js +0 -1
  352. package/out/_next/static/chunks/00s106sbq8t9v.js +0 -1
  353. package/out/_next/static/chunks/012hi627qrdnn.js +0 -1
  354. package/out/_next/static/chunks/0174xh3wfsjm1.js +0 -2
  355. package/out/_next/static/chunks/02~o2nmo5pmy1.js +0 -1
  356. package/out/_next/static/chunks/07t.dhhokszz5.css +0 -1
  357. package/out/_next/static/chunks/0_wia9ofmsi1c.css +0 -2
  358. package/out/_next/static/chunks/0ah8fihozo2_u.js +0 -5
  359. package/out/_next/static/chunks/0bzupvr5gt3k9.js +0 -31
  360. package/out/_next/static/chunks/0gdluj423gso1.js +0 -1
  361. package/out/_next/static/chunks/0gmoiq06srjay.css +0 -1
  362. package/out/_next/static/chunks/0imkasy7kb67u.js +0 -1
  363. package/out/_next/static/chunks/0jjc_b9q_ldi2.js +0 -1
  364. package/out/_next/static/chunks/0jl~j62iz2uvr.js +0 -1
  365. package/out/_next/static/chunks/0lqslm813wk_h.js +0 -1
  366. package/out/_next/static/chunks/0q782fxxd0lx~.js +0 -1
  367. package/out/_next/static/chunks/0slwj0c46k5cu.js +0 -1
  368. package/out/_next/static/chunks/0sorqk.oc6b7j.css +0 -1
  369. package/out/_next/static/chunks/0tapzqc6hgvx-.js +0 -1
  370. package/out/_next/static/chunks/0xsc7z5x8n7wg.js +0 -1
  371. package/out/_next/static/chunks/0zm~gys2jwl0g.js +0 -1
  372. package/out/_next/static/chunks/turbopack-0a_g3u0ud~jb8.js +0 -1
  373. /package/out/_next/static/{MKMAM_PgkfJypNTQAws_M → sIuUKxnnGU7K9Tu9UDKE8}/_buildManifest.js +0 -0
  374. /package/out/_next/static/{MKMAM_PgkfJypNTQAws_M → sIuUKxnnGU7K9Tu9UDKE8}/_clientMiddlewareManifest.js +0 -0
  375. /package/out/_next/static/{MKMAM_PgkfJypNTQAws_M → sIuUKxnnGU7K9Tu9UDKE8}/_ssgManifest.js +0 -0
@@ -33,6 +33,7 @@ import {
33
33
  PeerNotFoundError,
34
34
  IntegrityError,
35
35
  PermissionError,
36
+ ConflictError,
36
37
  EngineNotInitializedError,
37
38
  } from './utils/errors.js'
38
39
  import {
@@ -51,6 +52,8 @@ import {
51
52
  DOWNLOAD_POLL_INTERVAL_MIN,
52
53
  DOWNLOAD_POLL_INTERVAL_MAX,
53
54
  DRIVE_UPDATE_INTERVAL,
55
+ HOLDING_REJOIN_BATCH_SIZE,
56
+ HOLDING_REJOIN_BATCH_DELAY,
54
57
  PROGRESS_THROTTLE,
55
58
  DEFAULT_READ_LIMIT,
56
59
  CHANNEL_NAME_MIN_LENGTH,
@@ -61,16 +64,22 @@ import {
61
64
  MAX_MESSAGE_LENGTH,
62
65
  } from './config.js'
63
66
 
67
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
68
+
64
69
  export class MostBoxEngine extends EventEmitter {
65
70
  #store = null
66
71
  #swarm = null
67
72
  #drives = new Map()
68
73
  #publishedFiles = []
74
+ #holdings = []
69
75
  #trashFiles = []
70
76
  #initialized = false
71
77
  #options = null
72
78
  #activeDownloads = new Map()
73
79
  #drivePromises = new Map()
80
+ #fileDiscoveries = new Map()
81
+ #seedStates = new Map()
82
+ #holdingResumeTask = null
74
83
 
75
84
  #channels = []
76
85
  #channelCores = new Map()
@@ -99,6 +108,7 @@ export class MostBoxEngine extends EventEmitter {
99
108
  downloadPath:
100
109
  options.downloadPath || path.join(options.dataPath, 'downloads'),
101
110
  maxFileSize: options.maxFileSize || MAX_FILE_SIZE,
111
+ downloadTimeout: options.downloadTimeout || DOWNLOAD_TIMEOUT,
102
112
  }
103
113
  }
104
114
 
@@ -230,6 +240,17 @@ export class MostBoxEngine extends EventEmitter {
230
240
  `[MostBox] Loaded ${this.#publishedFiles.length} published files`
231
241
  )
232
242
 
243
+ this.#holdings = this.#loadHoldingsMetadata()
244
+ console.log(`[MostBox] Loaded ${this.#holdings.length} node holdings`)
245
+
246
+ for (const holding of this.#holdings) {
247
+ this.#setSeedState(holding.cid, {
248
+ status: 'queued',
249
+ topic: holding.topic,
250
+ driveName: holding.driveName,
251
+ })
252
+ }
253
+
233
254
  this.#trashFiles = this.#loadTrashMetadata()
234
255
  console.log(`[MostBox] Loaded ${this.#trashFiles.length} trash files`)
235
256
 
@@ -274,6 +295,7 @@ export class MostBoxEngine extends EventEmitter {
274
295
  this.#initialized = true
275
296
  console.log(`[MostBox] Engine initialized successfully`)
276
297
  this.emit('ready')
298
+ this.#resumeHoldingsInBackground()
277
299
 
278
300
  return this
279
301
  }
@@ -295,6 +317,9 @@ export class MostBoxEngine extends EventEmitter {
295
317
 
296
318
  await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
297
319
  this.#drives.clear()
320
+ this.#fileDiscoveries.clear()
321
+ this.#seedStates.clear()
322
+ this.#holdingResumeTask = null
298
323
 
299
324
  for (const core of this.#channelCores.values()) {
300
325
  try {
@@ -359,9 +384,11 @@ export class MostBoxEngine extends EventEmitter {
359
384
  * Hyperdrive 中存储 key 为 '/' + cid,metadata 中存储 displayName(用户看到的路径)
360
385
  * @param {string|Buffer} content - 文件路径(字符串)或内容(Buffer)
361
386
  * @param {string} [fileName] - 文件名(Buffer 输入时必填)
387
+ * @param {object} [options] - 发布选项
388
+ * @param {string|null} [options.localPath] - 持有记录中的本地路径
362
389
  * @returns {Promise<{ cid: string, link: string, fileName: string }>}
363
390
  */
364
- async publishFile(content, fileName) {
391
+ async publishFile(content, fileName, options = {}) {
365
392
  this.#ensureInitialized()
366
393
 
367
394
  let cleanPath = null
@@ -409,6 +436,9 @@ export class MostBoxEngine extends EventEmitter {
409
436
 
410
437
  const { cid: rootCid } = await calculateCid(content)
411
438
  const cidString = rootCid.toString()
439
+ const { driveName: name } = this.#getCidInfo(cidString)
440
+ const holdingLocalPath =
441
+ options.localPath === undefined ? cleanPath : options.localPath
412
442
 
413
443
  // 检查相同内容是否已存在
414
444
  const existingIndex = this.#publishedFiles.findIndex(
@@ -416,17 +446,28 @@ export class MostBoxEngine extends EventEmitter {
416
446
  )
417
447
  if (existingIndex !== -1) {
418
448
  const existing = this.#publishedFiles[existingIndex]
449
+ await this.#joinCidTopicInternal(cidString, {
450
+ server: true,
451
+ client: false,
452
+ })
453
+ this.#upsertHolding({
454
+ cid: cidString,
455
+ fileName: existing.fileName,
456
+ size: fileSize,
457
+ localPath: holdingLocalPath,
458
+ driveName: name,
459
+ source: 'published',
460
+ temporary: false,
461
+ })
419
462
  return {
420
463
  cid: cidString,
421
- link: `most://${cidString}`,
464
+ link: `most://${cidString}?filename=${encodeURIComponent(existing.fileName)}`,
422
465
  fileName: existing.fileName,
423
466
  alreadyExists: true,
424
467
  }
425
468
  }
426
469
 
427
470
  // 获取或创建该 CID 对应的 drive
428
- const hashHex = b4a.toString(rootCid.multihash.digest, 'hex')
429
- const name = `drive-${hashHex}`
430
471
  let drive = this.#drives.get(name)
431
472
 
432
473
  if (!drive) {
@@ -434,12 +475,15 @@ export class MostBoxEngine extends EventEmitter {
434
475
  server: true,
435
476
  client: false,
436
477
  })
437
- const discovery = this.#swarm.join(drive.discoveryKey, {
478
+ this.#swarm.join(drive.discoveryKey, {
438
479
  server: true,
439
480
  client: false,
440
481
  })
441
- await discovery.flushed()
442
482
  }
483
+ await this.#joinCidTopicInternal(cidString, {
484
+ server: true,
485
+ client: false,
486
+ })
443
487
 
444
488
  this.emit('publish:progress', { stage: 'uploading', file: safeFileName })
445
489
 
@@ -490,6 +534,15 @@ export class MostBoxEngine extends EventEmitter {
490
534
  starred: false,
491
535
  })
492
536
  this.#savePublishedMetadata()
537
+ this.#upsertHolding({
538
+ cid: cidString,
539
+ fileName: safeFileName,
540
+ size: fileSize,
541
+ localPath: holdingLocalPath,
542
+ driveName: name,
543
+ source: 'published',
544
+ temporary: false,
545
+ })
493
546
 
494
547
  const result = {
495
548
  cid: cidString,
@@ -505,13 +558,18 @@ export class MostBoxEngine extends EventEmitter {
505
558
  * 从 P2P 网络下载文件
506
559
  * @param {string} link - most:// 链接
507
560
  * @param {string} [taskId] - 用于取消的任务 ID
561
+ * @param {object} [options] - 下载选项
562
+ * @param {number} [options.timeout] - 等待 P2P 内容的超时时间(毫秒)
563
+ * @param {number} [options.streamReadTimeout] - 下载流无进度超时时间(毫秒)
508
564
  * @returns {Promise<{ taskId: string, fileName: string, savedPath: string, alreadyExists?: boolean }>}
509
565
  */
510
- async downloadFile(link, taskId = null) {
566
+ async downloadFile(link, taskId = null, options = {}) {
511
567
  this.#ensureInitialized()
512
568
 
513
569
  taskId =
514
570
  taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
571
+ const downloadTimeout = options.timeout || this.#options.downloadTimeout
572
+ const streamReadTimeout = options.streamReadTimeout ?? STREAM_READ_TIMEOUT
515
573
 
516
574
  console.log(
517
575
  `[MostBox] Starting download for link: ${link} (taskId: ${taskId})`
@@ -527,10 +585,32 @@ export class MostBoxEngine extends EventEmitter {
527
585
  }
528
586
  const cidString = parsed.cid
529
587
  console.log(`[MostBox] Parsed CID: ${cidString}`)
588
+ const parsedCid = CID.parse(cidString)
589
+ const { driveName: name } = this.#getCidInfo(cidString)
530
590
 
531
591
  const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
532
592
  if (existingFile) {
533
593
  console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
594
+ const existingHolding = this.#holdings.find(
595
+ item => item.cid === cidString
596
+ )
597
+ const existingSize = Number(existingFile.size)
598
+ await this.#joinCidTopicInternal(cidString, {
599
+ server: true,
600
+ client: false,
601
+ })
602
+ this.#upsertHolding({
603
+ cid: cidString,
604
+ fileName: existingFile.fileName,
605
+ size:
606
+ existingHolding?.size ??
607
+ (Number.isFinite(existingSize) ? existingSize : 0),
608
+ localPath:
609
+ existingHolding?.localPath || existingFile.localPath || null,
610
+ driveName: existingFile.driveName || name,
611
+ source: existingHolding?.source || 'published',
612
+ temporary: existingHolding?.temporary === true,
613
+ })
534
614
  return {
535
615
  taskId,
536
616
  fileName: existingFile.fileName,
@@ -540,12 +620,8 @@ export class MostBoxEngine extends EventEmitter {
540
620
 
541
621
  const linkFileName = parsed.fileName
542
622
 
543
- const parsedCid = CID.parse(cidString)
544
- const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
545
-
546
623
  if (taskState.aborted) throw new Error('Download cancelled')
547
624
 
548
- const name = `drive-${hashHex}`
549
625
  let drive = this.#drives.get(name)
550
626
 
551
627
  if (!drive) {
@@ -558,33 +634,40 @@ export class MostBoxEngine extends EventEmitter {
558
634
  this.emit('download:status', { taskId, status: 'connecting' })
559
635
 
560
636
  console.log(`[MostBox] Joining swarm for drive discovery...`)
561
- await this.#swarm
562
- .join(drive.discoveryKey, { server: true, client: true })
563
- .flushed()
564
- console.log(`[MostBox] Swarm join flushed`)
637
+ this.#swarm.join(drive.discoveryKey, {
638
+ server: true,
639
+ client: true,
640
+ })
641
+ console.log(`[MostBox] Swarm join requested`)
565
642
  } else {
566
643
  console.log(`[MostBox] Using existing drive: ${name}`)
567
644
  }
645
+ await this.#joinCidTopicInternal(cidString, {
646
+ server: true,
647
+ client: true,
648
+ })
568
649
 
569
650
  if (taskState.aborted) throw new Error('Download cancelled')
570
651
 
571
652
  this.emit('download:status', { taskId, status: 'finding-peers' })
572
653
 
573
654
  console.log(
574
- `[MostBox] Waiting for drive content (timeout: ${DOWNLOAD_TIMEOUT / 1000}s)...`
655
+ `[MostBox] Waiting for drive entry /${cidString} (timeout: ${downloadTimeout / 1000}s)...`
575
656
  )
576
- const entries = await this.#waitForDriveContent(
657
+ const driveKey = '/' + cidString
658
+ const entry = await this.#waitForDriveEntry(
577
659
  drive,
578
- DOWNLOAD_TIMEOUT,
660
+ driveKey,
661
+ downloadTimeout,
579
662
  taskId,
580
663
  taskState
581
664
  )
582
665
 
583
- if (entries.length === 0) {
584
- console.log(`[MostBox] No entries found after timeout`)
666
+ if (!entry) {
667
+ console.log(`[MostBox] Expected drive entry ${driveKey} not found`)
585
668
 
586
669
  const peerCount = this.#swarm.connections.size
587
- let errorMessage = 'No files found in drive. '
670
+ let errorMessage = `Expected file ${driveKey} was not found. `
588
671
 
589
672
  if (peerCount === 0) {
590
673
  errorMessage +=
@@ -606,10 +689,10 @@ export class MostBoxEngine extends EventEmitter {
606
689
  if (taskState.aborted) throw new Error('Download cancelled')
607
690
 
608
691
  console.log(
609
- `[MostBox] Found ${entries.length} entries, starting download...`
692
+ `[MostBox] Found expected entry ${driveKey}, starting download...`
610
693
  )
611
694
 
612
- const targetDir = this.#options.dataPath
695
+ const targetDir = this.#options.downloadPath
613
696
 
614
697
  const writableCheck = await checkDirectoryWritable(targetDir)
615
698
  if (!writableCheck.writable) {
@@ -617,6 +700,7 @@ export class MostBoxEngine extends EventEmitter {
617
700
  }
618
701
 
619
702
  // 下载文件
703
+ const entries = [entry]
620
704
  for (const entry of entries) {
621
705
  const cleanKey = entry.key.replace(/^[\/\\]/, '')
622
706
  const sanitizedFileName = linkFileName
@@ -634,6 +718,10 @@ export class MostBoxEngine extends EventEmitter {
634
718
  }
635
719
 
636
720
  const savePath = path.join(targetDir, sanitizedFileName)
721
+ fs.mkdirSync(path.dirname(savePath), { recursive: true })
722
+ if (fs.existsSync(savePath)) {
723
+ throw new ConflictError(`已有同名文件: ${sanitizedFileName}`)
724
+ }
637
725
 
638
726
  this.emit('download:status', {
639
727
  taskId,
@@ -652,14 +740,54 @@ export class MostBoxEngine extends EventEmitter {
652
740
  let lastProgressUpdate = 0
653
741
 
654
742
  await new Promise((resolve, reject) => {
743
+ let settled = false
744
+ let readTimer = null
745
+
746
+ const clearReadTimer = () => {
747
+ if (readTimer) {
748
+ clearTimeout(readTimer)
749
+ readTimer = null
750
+ }
751
+ }
752
+
753
+ const fail = err => {
754
+ if (settled) return
755
+ settled = true
756
+ clearReadTimer()
757
+ rs.destroy(err)
758
+ ws.destroy()
759
+ fs.unlink(savePath, () => {})
760
+ reject(err)
761
+ }
762
+
763
+ const complete = () => {
764
+ if (settled) return
765
+ settled = true
766
+ clearReadTimer()
767
+ resolve()
768
+ }
769
+
770
+ const resetReadTimer = () => {
771
+ clearReadTimer()
772
+ if (streamReadTimeout > 0) {
773
+ readTimer = setTimeout(() => {
774
+ fail(
775
+ new Error(
776
+ `Download stalled: no data received for ${streamReadTimeout / 1000}s`
777
+ )
778
+ )
779
+ }, streamReadTimeout)
780
+ }
781
+ }
782
+
783
+ resetReadTimer()
784
+
655
785
  rs.on('data', chunk => {
656
786
  if (taskState.aborted) {
657
- rs.destroy()
658
- ws.destroy()
659
- fs.unlink(savePath, () => {})
660
- reject(new Error('Download cancelled'))
787
+ fail(new Error('Download cancelled'))
661
788
  return
662
789
  }
790
+ resetReadTimer()
663
791
  loadedBytes += chunk.length
664
792
  const now = Date.now()
665
793
  if (
@@ -678,9 +806,19 @@ export class MostBoxEngine extends EventEmitter {
678
806
  })
679
807
 
680
808
  rs.pipe(ws)
681
- ws.on('finish', resolve)
682
- ws.on('error', reject)
683
- rs.on('error', reject)
809
+ ws.on('finish', complete)
810
+ ws.on('error', fail)
811
+ rs.on('error', fail)
812
+ rs.on('close', () => {
813
+ if (taskState.aborted) {
814
+ fail(new Error('Download cancelled'))
815
+ }
816
+ })
817
+ ws.on('close', () => {
818
+ if (taskState.aborted) {
819
+ fail(new Error('Download cancelled'))
820
+ }
821
+ })
684
822
  })
685
823
 
686
824
  if (taskState.aborted) throw new Error('Download cancelled')
@@ -738,6 +876,16 @@ export class MostBoxEngine extends EventEmitter {
738
876
  })
739
877
  }
740
878
  this.#savePublishedMetadata()
879
+ const savedSize = totalBytes || fs.statSync(savePath).size
880
+ this.#upsertHolding({
881
+ cid: cidString,
882
+ fileName: sanitizedFileName,
883
+ size: savedSize,
884
+ localPath: savePath,
885
+ driveName: name,
886
+ source: 'downloaded',
887
+ temporary: true,
888
+ })
741
889
 
742
890
  this.emit('download:success', result)
743
891
  return result
@@ -747,6 +895,86 @@ export class MostBoxEngine extends EventEmitter {
747
895
  }
748
896
  }
749
897
 
898
+ /**
899
+ * 检测 most:// 链接当前是否能找到可下载内容,但不读取文件内容。
900
+ * @param {string} link - most:// 链接
901
+ * @param {object} [options] - 检测选项
902
+ * @param {number} [options.timeout] - 等待 P2P 内容的超时时间(毫秒)
903
+ * @returns {Promise<{ available: boolean, cid: string, fileName: string, size: number|null, alreadyExists?: boolean }>}
904
+ */
905
+ async checkDownloadAvailability(link, options = {}) {
906
+ this.#ensureInitialized()
907
+
908
+ const timeout = options.timeout || DRIVE_ENTRY_TIMEOUT
909
+ const parsed = parseMostLink(link)
910
+ if (parsed.error) {
911
+ throw new ValidationError(parsed.error)
912
+ }
913
+
914
+ const cidString = parsed.cid
915
+ const { driveName: name } = this.#getCidInfo(cidString)
916
+ const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
917
+ if (existingFile) {
918
+ return {
919
+ available: true,
920
+ cid: cidString,
921
+ fileName: existingFile.fileName,
922
+ size: Number(existingFile.size) || null,
923
+ alreadyExists: true,
924
+ }
925
+ }
926
+
927
+ const writableCheck = await checkDirectoryWritable(
928
+ this.#options.downloadPath
929
+ )
930
+ if (!writableCheck.writable) {
931
+ throw new PermissionError(writableCheck.error)
932
+ }
933
+
934
+ let drive = this.#drives.get(name)
935
+
936
+ if (!drive) {
937
+ drive = await this.#getOrCreateDrive(name, {
938
+ server: true,
939
+ client: true,
940
+ })
941
+
942
+ this.#swarm.join(drive.discoveryKey, {
943
+ server: true,
944
+ client: true,
945
+ })
946
+ }
947
+
948
+ await this.#joinCidTopicInternal(cidString, {
949
+ server: true,
950
+ client: true,
951
+ })
952
+
953
+ const driveKey = '/' + cidString
954
+ const entry = await this.#waitForDriveEntry(drive, driveKey, timeout)
955
+
956
+ if (!entry) {
957
+ throw new PeerNotFoundError(
958
+ '当前没有发现可下载的在线种子,请稍后重试或确认发布者在线'
959
+ )
960
+ }
961
+
962
+ let size = null
963
+ try {
964
+ const stat = await drive.entry(entry.key)
965
+ if (stat?.value?.blob) {
966
+ size = stat.value.blob.byteLength || 0
967
+ }
968
+ } catch {}
969
+
970
+ return {
971
+ available: true,
972
+ cid: cidString,
973
+ fileName: parsed.fileName,
974
+ size,
975
+ }
976
+ }
977
+
750
978
  /**
751
979
  * 列出所有已发布文件
752
980
  * @param {object} [options] - 筛选选项
@@ -764,7 +992,7 @@ export class MostBoxEngine extends EventEmitter {
764
992
  return files.map(f => ({
765
993
  fileName: f.fileName,
766
994
  cid: f.cid,
767
- link: `most://${f.cid}`,
995
+ link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
768
996
  publishedAt: f.publishedAt,
769
997
  starred: f.starred || false,
770
998
  }))
@@ -799,17 +1027,28 @@ export class MostBoxEngine extends EventEmitter {
799
1027
  const index = this.#publishedFiles.findIndex(f => f.cid === cid)
800
1028
  if (index !== -1) {
801
1029
  const fileRecord = this.#publishedFiles[index]
1030
+ const holding = this.#holdings.find(item => item.cid === fileRecord.cid)
802
1031
 
803
1032
  this.#trashFiles.push({
804
1033
  fileName: fileRecord.fileName,
805
1034
  cid: fileRecord.cid,
806
- driveName: fileRecord.driveName,
1035
+ driveName:
1036
+ fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName,
1037
+ size: holding?.size ?? fileRecord.size ?? 0,
1038
+ localPath: holding?.localPath || fileRecord.localPath || null,
1039
+ source: holding?.source || 'published',
807
1040
  publishedAt: fileRecord.publishedAt,
808
1041
  starred: fileRecord.starred || false,
809
1042
  deletedAt: new Date().toISOString(),
810
1043
  })
811
1044
  this.#saveTrashMetadata()
812
1045
 
1046
+ await this.#leaveCidTopic(fileRecord.cid)
1047
+ await this.#closeDriveForSeed(
1048
+ fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1049
+ )
1050
+ this.#removeHolding(fileRecord.cid)
1051
+
813
1052
  this.#publishedFiles.splice(index, 1)
814
1053
  this.#savePublishedMetadata()
815
1054
  }
@@ -825,7 +1064,7 @@ export class MostBoxEngine extends EventEmitter {
825
1064
  return this.#trashFiles.map(f => ({
826
1065
  fileName: f.fileName,
827
1066
  cid: f.cid,
828
- link: `most://${f.cid}`,
1067
+ link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
829
1068
  publishedAt: f.publishedAt,
830
1069
  starred: f.starred || false,
831
1070
  deletedAt: f.deletedAt,
@@ -835,9 +1074,9 @@ export class MostBoxEngine extends EventEmitter {
835
1074
  /**
836
1075
  * 从回收站恢复文件
837
1076
  * @param {string} cid - 要恢复文件的 CID
838
- * @returns {Array} 更新后的已发布文件列表
1077
+ * @returns {Promise<Array>} 更新后的已发布文件列表
839
1078
  */
840
- restoreTrashFile(cid) {
1079
+ async restoreTrashFile(cid) {
841
1080
  this.#ensureInitialized()
842
1081
  const index = this.#trashFiles.findIndex(f => f.cid === cid)
843
1082
  if (index === -1) {
@@ -862,6 +1101,20 @@ export class MostBoxEngine extends EventEmitter {
862
1101
  this.#trashFiles.splice(index, 1)
863
1102
  this.#saveTrashMetadata()
864
1103
 
1104
+ await this.#joinCidTopicInternal(fileRecord.cid, {
1105
+ server: true,
1106
+ client: false,
1107
+ })
1108
+ this.#upsertHolding({
1109
+ cid: fileRecord.cid,
1110
+ fileName: fileRecord.fileName,
1111
+ size: Number(fileRecord.size) || 0,
1112
+ localPath: fileRecord.localPath || null,
1113
+ driveName,
1114
+ source: fileRecord.source || 'published',
1115
+ temporary: fileRecord.source === 'downloaded',
1116
+ })
1117
+
865
1118
  return this.listPublishedFiles()
866
1119
  }
867
1120
 
@@ -875,21 +1128,19 @@ export class MostBoxEngine extends EventEmitter {
875
1128
  const index = this.#trashFiles.findIndex(f => f.cid === cid)
876
1129
  if (index !== -1) {
877
1130
  const fileRecord = this.#trashFiles[index]
878
- const driveName = fileRecord.driveName
879
-
880
- const drive = this.#drives.get(driveName)
881
- if (drive) {
882
- try {
883
- await drive.del('/' + fileRecord.cid)
884
- } catch {
885
- // 文件可能不存在于驱动器中
886
- }
1131
+ const driveName =
1132
+ fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
887
1133
 
888
- await this.#swarm.leave(drive.discoveryKey)
889
- await drive.close()
890
- this.#drives.delete(driveName)
1134
+ try {
1135
+ const drive = await this.#getOrCreateDrive(driveName)
1136
+ await drive.del('/' + fileRecord.cid)
1137
+ } catch {
1138
+ // 文件可能不存在于驱动器中
891
1139
  }
1140
+ await this.#closeDriveForSeed(driveName)
892
1141
 
1142
+ await this.#leaveCidTopic(fileRecord.cid)
1143
+ this.#removeHolding(fileRecord.cid)
893
1144
  this.#trashFiles.splice(index, 1)
894
1145
  this.#saveTrashMetadata()
895
1146
  }
@@ -904,20 +1155,18 @@ export class MostBoxEngine extends EventEmitter {
904
1155
  this.#ensureInitialized()
905
1156
 
906
1157
  for (const fileRecord of this.#trashFiles) {
907
- const driveName = fileRecord.driveName
908
-
909
- const drive = this.#drives.get(driveName)
910
- if (drive) {
911
- try {
912
- await drive.del('/' + fileRecord.cid)
913
- } catch {
914
- // 文件可能不存在
915
- }
1158
+ const driveName =
1159
+ fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
916
1160
 
917
- await this.#swarm.leave(drive.discoveryKey)
918
- await drive.close()
919
- this.#drives.delete(driveName)
1161
+ try {
1162
+ const drive = await this.#getOrCreateDrive(driveName)
1163
+ await drive.del('/' + fileRecord.cid)
1164
+ } catch {
1165
+ // 文件可能不存在
920
1166
  }
1167
+ await this.#closeDriveForSeed(driveName)
1168
+ await this.#leaveCidTopic(fileRecord.cid)
1169
+ this.#removeHolding(fileRecord.cid)
921
1170
  }
922
1171
 
923
1172
  this.#trashFiles = []
@@ -1007,7 +1256,7 @@ export class MostBoxEngine extends EventEmitter {
1007
1256
  return {
1008
1257
  cid,
1009
1258
  fileName: safeFileName,
1010
- link: `most://${cid}`,
1259
+ link: `most://${cid}?filename=${encodeURIComponent(safeFileName)}`,
1011
1260
  }
1012
1261
  }
1013
1262
 
@@ -1034,7 +1283,7 @@ export class MostBoxEngine extends EventEmitter {
1034
1283
  updatedFiles.push({
1035
1284
  cid: file.cid,
1036
1285
  fileName: file.fileName,
1037
- link: `most://${file.cid}`,
1286
+ link: `most://${file.cid}?filename=${encodeURIComponent(file.fileName)}`,
1038
1287
  })
1039
1288
  }
1040
1289
  }
@@ -1054,15 +1303,143 @@ export class MostBoxEngine extends EventEmitter {
1054
1303
  const task = this.#activeDownloads.get(taskId)
1055
1304
  if (task) {
1056
1305
  task.aborted = true
1057
- if (task.readStream) task.readStream.destroy()
1306
+ const err = new Error('Download cancelled')
1307
+ if (task.readStream) task.readStream.destroy(err)
1058
1308
  if (task.writeStream) task.writeStream.destroy()
1059
1309
  }
1060
1310
  }
1061
1311
 
1312
+ hasDownloadNameConflict(fileName) {
1313
+ this.#ensureInitialized()
1314
+ const sanitizedFileName = sanitizeFilename(fileName)
1315
+ const savePath = path.join(this.#options.downloadPath, sanitizedFileName)
1316
+ return fs.existsSync(savePath)
1317
+ }
1318
+
1319
+ setMaxFileSize(maxFileSize) {
1320
+ const parsed = Number(maxFileSize)
1321
+ if (!Number.isFinite(parsed) || parsed < 0) {
1322
+ throw new ValidationError('maxFileSize must be a non-negative number')
1323
+ }
1324
+ this.#options.maxFileSize = Math.floor(parsed)
1325
+ }
1326
+
1062
1327
  getPublishedFiles() {
1063
1328
  return this.#publishedFiles
1064
1329
  }
1065
1330
 
1331
+ /**
1332
+ * 列出当前节点持有的可做种文件副本
1333
+ * @returns {Array}
1334
+ */
1335
+ listHoldings() {
1336
+ this.#ensureInitialized()
1337
+ return this.#holdings.map(holding => {
1338
+ const seedState = this.#seedStates.get(holding.cid)
1339
+ const status =
1340
+ seedState?.status ||
1341
+ (this.#fileDiscoveries.has(holding.cid) ? 'active' : 'queued')
1342
+ return {
1343
+ ...holding,
1344
+ joined: status === 'active' && this.#fileDiscoveries.has(holding.cid),
1345
+ seedStatus: status,
1346
+ seedError: seedState?.error,
1347
+ seedStatusUpdatedAt: seedState?.updatedAt,
1348
+ link: `most://${holding.cid}?filename=${encodeURIComponent(holding.fileName || holding.cid)}`,
1349
+ }
1350
+ })
1351
+ }
1352
+
1353
+ /**
1354
+ * 手动记录节点已持有的文件副本
1355
+ * @param {object} record - 持有记录
1356
+ */
1357
+ async addHolding(record) {
1358
+ this.#ensureInitialized()
1359
+ const holding = this.#normalizeHolding(record)
1360
+ await this.#joinCidTopicInternal(holding.cid, {
1361
+ server: true,
1362
+ client: false,
1363
+ })
1364
+ return this.#upsertHolding(holding)
1365
+ }
1366
+
1367
+ /**
1368
+ * 按 CID digest topic 拉取完整文件副本
1369
+ * @param {object} input - 拉取参数
1370
+ * @param {string} [input.link] - most:// 链接
1371
+ * @param {string} [input.cid] - 文件 CID
1372
+ * @param {string} [input.fileName] - 保存文件名
1373
+ * @param {string} [input.taskId] - 下载任务 ID
1374
+ * @param {number} [input.timeout] - 等待 P2P 内容的超时时间
1375
+ */
1376
+ async pullByCid(input = {}) {
1377
+ this.#ensureInitialized()
1378
+
1379
+ if (input.link) {
1380
+ const parsed = parseMostLink(input.link)
1381
+ if (parsed.error) {
1382
+ throw new ValidationError(parsed.error)
1383
+ }
1384
+ const result = await this.downloadFile(input.link, input.taskId || null, {
1385
+ timeout: input.timeout,
1386
+ })
1387
+ return {
1388
+ ...result,
1389
+ cid: parsed.cid,
1390
+ }
1391
+ }
1392
+
1393
+ const cid = input.cid
1394
+ if (!cid) {
1395
+ throw new ValidationError('cid is required')
1396
+ }
1397
+
1398
+ this.#getCidInfo(cid)
1399
+ const fileName = sanitizeFilename(input.fileName || `${cid}.bin`)
1400
+ const link = `most://${cid}?filename=${encodeURIComponent(fileName)}`
1401
+ const result = await this.downloadFile(link, input.taskId || null, {
1402
+ timeout: input.timeout,
1403
+ })
1404
+
1405
+ return {
1406
+ ...result,
1407
+ cid,
1408
+ }
1409
+ }
1410
+
1411
+ /**
1412
+ * 按 CID digest 加入文件 topic
1413
+ * @param {string} cid - 文件 CID
1414
+ * @param {object} [options] - Hyperswarm join 选项
1415
+ */
1416
+ async joinCidTopic(cid, options = {}) {
1417
+ this.#ensureInitialized()
1418
+ return this.#joinCidTopicInternal(cid, options)
1419
+ }
1420
+
1421
+ /**
1422
+ * 用内存复制流连接两个本地引擎,供本地集成测试和诊断使用。
1423
+ */
1424
+ replicateWith(peerEngine) {
1425
+ this.#ensureInitialized()
1426
+ peerEngine.#ensureInitialized()
1427
+
1428
+ const left = this.#store.replicate(true, { live: true })
1429
+ const right = peerEngine.#store.replicate(false, { live: true })
1430
+
1431
+ left.on('error', () => {})
1432
+ right.on('error', () => {})
1433
+ left.pipe(right).pipe(left)
1434
+
1435
+ return {
1436
+ close: () => {
1437
+ left.destroy()
1438
+ right.destroy()
1439
+ },
1440
+ }
1441
+ }
1442
+
1066
1443
  /**
1067
1444
  * 读取已发布文件的内容(用于预览)
1068
1445
  * Hyperdrive 中用 CID 作为 key 存储
@@ -1243,12 +1620,10 @@ export class MostBoxEngine extends EventEmitter {
1243
1620
  server: true,
1244
1621
  client: true,
1245
1622
  })
1246
- await appDiscovery.flushed()
1247
1623
  const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
1248
1624
  server: true,
1249
1625
  client: true,
1250
1626
  })
1251
- await chatDiscovery.flushed()
1252
1627
 
1253
1628
  this.#setupChannelAppendListener(core, name)
1254
1629
 
@@ -1304,12 +1679,10 @@ export class MostBoxEngine extends EventEmitter {
1304
1679
  server: true,
1305
1680
  client: true,
1306
1681
  })
1307
- await appDiscovery.flushed()
1308
1682
  const chatDiscovery = this.#chatSwarm.join(chatDiscoveryKey, {
1309
1683
  server: true,
1310
1684
  client: true,
1311
1685
  })
1312
- await chatDiscovery.flushed()
1313
1686
 
1314
1687
  this.#setupChannelAppendListener(core, name)
1315
1688
 
@@ -1351,29 +1724,25 @@ export class MostBoxEngine extends EventEmitter {
1351
1724
 
1352
1725
  const appDiscovery = this.#channelDiscoveries.get(name)
1353
1726
  if (appDiscovery && this.#swarm) {
1354
- try {
1355
- await this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex'))
1356
- } catch (err) {
1727
+ this.#channelDiscoveries.delete(name)
1728
+ this.#swarm.leave(b4a.from(channel.discoveryKey, 'hex')).catch(err => {
1357
1729
  console.warn(
1358
1730
  `[MostBox] Failed to leave app swarm for ${name}:`,
1359
1731
  err.message
1360
1732
  )
1361
- }
1362
- this.#channelDiscoveries.delete(name)
1733
+ })
1363
1734
  }
1364
1735
 
1365
1736
  const chatDiscovery = this.#channelChatDiscoveries.get(name)
1366
1737
  if (chatDiscovery && this.#chatSwarm) {
1367
- try {
1368
- const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
1369
- await this.#chatSwarm.leave(chatDiscoveryKey)
1370
- } catch (err) {
1738
+ this.#channelChatDiscoveries.delete(name)
1739
+ const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
1740
+ this.#chatSwarm.leave(chatDiscoveryKey).catch(err => {
1371
1741
  console.warn(
1372
1742
  `[MostBox] Failed to leave chat swarm for ${name}:`,
1373
1743
  err.message
1374
1744
  )
1375
- }
1376
- this.#channelChatDiscoveries.delete(name)
1745
+ })
1377
1746
  }
1378
1747
 
1379
1748
  const core = this.#channelCores.get(name)
@@ -1553,6 +1922,255 @@ export class MostBoxEngine extends EventEmitter {
1553
1922
  }
1554
1923
  }
1555
1924
 
1925
+ #getCidInfo(cid) {
1926
+ try {
1927
+ const parsedCid = CID.parse(cid)
1928
+ const topic = b4a.from(parsedCid.multihash.digest)
1929
+ if (topic.length !== 32) {
1930
+ throw new ValidationError('CID digest must be 32 bytes')
1931
+ }
1932
+ const topicHex = b4a.toString(topic, 'hex')
1933
+ return {
1934
+ topic,
1935
+ topicHex,
1936
+ driveName: `drive-${topicHex}`,
1937
+ }
1938
+ } catch (err) {
1939
+ if (err instanceof ValidationError) {
1940
+ throw err
1941
+ }
1942
+ throw new ValidationError('Invalid CID format')
1943
+ }
1944
+ }
1945
+
1946
+ #setSeedState(cid, patch = {}) {
1947
+ const previous = this.#seedStates.get(cid) || {}
1948
+ const next = {
1949
+ ...previous,
1950
+ cid,
1951
+ ...patch,
1952
+ updatedAt: new Date().toISOString(),
1953
+ }
1954
+ this.#seedStates.set(cid, next)
1955
+ this.emit('seed:state', next)
1956
+ return next
1957
+ }
1958
+
1959
+ #clearSeedState(cid) {
1960
+ if (this.#seedStates.delete(cid)) {
1961
+ this.emit('seed:state:removed', { cid })
1962
+ }
1963
+ }
1964
+
1965
+ #resumeHoldingsInBackground() {
1966
+ if (this.#holdingResumeTask || this.#holdings.length === 0) {
1967
+ return
1968
+ }
1969
+
1970
+ const holdings = [...this.#holdings]
1971
+ this.#holdingResumeTask = (async () => {
1972
+ for (
1973
+ let index = 0;
1974
+ index < holdings.length && this.#initialized;
1975
+ index += HOLDING_REJOIN_BATCH_SIZE
1976
+ ) {
1977
+ const batch = holdings.slice(index, index + HOLDING_REJOIN_BATCH_SIZE)
1978
+ await Promise.allSettled(
1979
+ batch.map(async holding => {
1980
+ if (!this.#holdings.some(current => current.cid === holding.cid)) {
1981
+ return
1982
+ }
1983
+ await this.#joinCidTopicInternal(holding.cid, {
1984
+ server: true,
1985
+ client: false,
1986
+ })
1987
+ console.log(`[MostBox] Rejoined CID topic: ${holding.cid}`)
1988
+ })
1989
+ )
1990
+
1991
+ if (
1992
+ index + HOLDING_REJOIN_BATCH_SIZE < holdings.length &&
1993
+ this.#initialized
1994
+ ) {
1995
+ await sleep(HOLDING_REJOIN_BATCH_DELAY)
1996
+ }
1997
+ }
1998
+ })()
1999
+ .catch(err => {
2000
+ console.warn('[MostBox] Failed to resume holdings:', err.message)
2001
+ })
2002
+ .finally(() => {
2003
+ this.#holdingResumeTask = null
2004
+ })
2005
+ }
2006
+
2007
+ #normalizeHolding(record = {}) {
2008
+ const cid = record.cid
2009
+ if (!cid) {
2010
+ throw new ValidationError('cid is required')
2011
+ }
2012
+
2013
+ const { topicHex, driveName } = this.#getCidInfo(cid)
2014
+ if (record.topic && record.topic !== topicHex) {
2015
+ throw new ValidationError('topic must match CID digest')
2016
+ }
2017
+
2018
+ const size = Number(record.size)
2019
+ if (!Number.isFinite(size) || size < 0) {
2020
+ throw new ValidationError('size must be a non-negative number')
2021
+ }
2022
+
2023
+ return {
2024
+ cid,
2025
+ fileName: record.fileName || cid,
2026
+ size,
2027
+ localPath: record.localPath || null,
2028
+ topic: topicHex,
2029
+ driveName: record.driveName || driveName,
2030
+ source: record.source || 'manual',
2031
+ temporary: record.temporary === true,
2032
+ }
2033
+ }
2034
+
2035
+ #upsertHolding(record) {
2036
+ const holding = this.#normalizeHolding(record)
2037
+ const now = new Date().toISOString()
2038
+ const index = this.#holdings.findIndex(f => f.cid === holding.cid)
2039
+ const next =
2040
+ index === -1
2041
+ ? { ...holding, createdAt: now, updatedAt: now }
2042
+ : { ...this.#holdings[index], ...holding, updatedAt: now }
2043
+
2044
+ if (index === -1) {
2045
+ this.#holdings.push(next)
2046
+ } else {
2047
+ this.#holdings[index] = next
2048
+ }
2049
+
2050
+ this.#saveHoldingsMetadata()
2051
+ this.emit('holding:updated', next)
2052
+ const seedState = this.#seedStates.get(next.cid)
2053
+ return {
2054
+ ...next,
2055
+ joined: this.#fileDiscoveries.has(next.cid),
2056
+ seedStatus:
2057
+ seedState?.status ||
2058
+ (this.#fileDiscoveries.has(next.cid) ? 'active' : 'queued'),
2059
+ seedError: seedState?.error,
2060
+ seedStatusUpdatedAt: seedState?.updatedAt,
2061
+ }
2062
+ }
2063
+
2064
+ #removeHolding(cid) {
2065
+ const before = this.#holdings.length
2066
+ this.#holdings = this.#holdings.filter(holding => holding.cid !== cid)
2067
+ if (this.#holdings.length !== before) {
2068
+ this.#saveHoldingsMetadata()
2069
+ this.emit('holding:removed', { cid })
2070
+ }
2071
+ this.#clearSeedState(cid)
2072
+ }
2073
+
2074
+ async #joinCidTopicInternal(cid, options = {}) {
2075
+ const { topic, topicHex, driveName } = this.#getCidInfo(cid)
2076
+ this.#setSeedState(cid, {
2077
+ status: 'joining',
2078
+ topic: topicHex,
2079
+ driveName,
2080
+ error: undefined,
2081
+ })
2082
+
2083
+ try {
2084
+ await this.#getOrCreateDrive(driveName)
2085
+
2086
+ const existing = this.#fileDiscoveries.get(cid)
2087
+ if (existing) {
2088
+ this.#setSeedState(cid, {
2089
+ status: 'active',
2090
+ topic: topicHex,
2091
+ driveName,
2092
+ error: undefined,
2093
+ })
2094
+ return {
2095
+ cid,
2096
+ topic: topicHex,
2097
+ driveName,
2098
+ joined: true,
2099
+ }
2100
+ }
2101
+
2102
+ const discovery = this.#swarm.join(topic, {
2103
+ server: options.server !== false,
2104
+ client: options.client === true,
2105
+ })
2106
+
2107
+ this.#fileDiscoveries.set(cid, {
2108
+ discovery,
2109
+ topic: topicHex,
2110
+ driveName,
2111
+ })
2112
+ this.#setSeedState(cid, {
2113
+ status: 'active',
2114
+ topic: topicHex,
2115
+ driveName,
2116
+ error: undefined,
2117
+ })
2118
+ this.emit('file:topic:joined', { cid, topic: topicHex, driveName })
2119
+
2120
+ return {
2121
+ cid,
2122
+ topic: topicHex,
2123
+ driveName,
2124
+ joined: true,
2125
+ }
2126
+ } catch (err) {
2127
+ this.#setSeedState(cid, {
2128
+ status: 'error',
2129
+ topic: topicHex,
2130
+ driveName,
2131
+ error: err.message,
2132
+ })
2133
+ throw err
2134
+ }
2135
+ }
2136
+
2137
+ async #leaveCidTopic(cid) {
2138
+ const existing = this.#fileDiscoveries.get(cid)
2139
+ if (!existing || !this.#swarm) {
2140
+ this.#setSeedState(cid, { status: 'paused' })
2141
+ return
2142
+ }
2143
+
2144
+ this.#fileDiscoveries.delete(cid)
2145
+ this.#swarm.leave(b4a.from(existing.topic, 'hex')).catch(err => {
2146
+ console.warn(`[MostBox] Failed to leave CID topic ${cid}:`, err.message)
2147
+ })
2148
+ this.#setSeedState(cid, {
2149
+ status: 'paused',
2150
+ topic: existing.topic,
2151
+ driveName: existing.driveName,
2152
+ })
2153
+ }
2154
+
2155
+ async #closeDriveForSeed(driveName) {
2156
+ const drive = this.#drives.get(driveName)
2157
+ if (!drive) {
2158
+ return null
2159
+ }
2160
+
2161
+ if (this.#swarm) {
2162
+ this.#swarm.leave(drive.discoveryKey).catch(err => {
2163
+ console.warn(
2164
+ `[MostBox] Failed to leave drive discovery ${driveName}:`,
2165
+ err.message
2166
+ )
2167
+ })
2168
+ }
2169
+ await drive.close()
2170
+ this.#drives.delete(driveName)
2171
+ return drive
2172
+ }
2173
+
1556
2174
  async #getOrCreateDrive(name, _options = { server: true, client: false }) {
1557
2175
  if (this.#drives.has(name)) return this.#drives.get(name)
1558
2176
  if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
@@ -1597,6 +2215,10 @@ export class MostBoxEngine extends EventEmitter {
1597
2215
  return path.join(this.#options.dataPath, 'published-files.json')
1598
2216
  }
1599
2217
 
2218
+ #getHoldingsMetadataPath() {
2219
+ return path.join(this.#options.dataPath, 'node-holdings.json')
2220
+ }
2221
+
1600
2222
  #getTrashMetadataPath() {
1601
2223
  return path.join(this.#options.dataPath, 'trash-files.json')
1602
2224
  }
@@ -1636,6 +2258,32 @@ export class MostBoxEngine extends EventEmitter {
1636
2258
  }
1637
2259
  }
1638
2260
 
2261
+ #loadHoldingsMetadata() {
2262
+ try {
2263
+ const metadataPath = this.#getHoldingsMetadataPath()
2264
+ if (fs.existsSync(metadataPath)) {
2265
+ const data = fs.readFileSync(metadataPath, 'utf-8')
2266
+ const parsed = JSON.parse(data)
2267
+ return parsed.map(record => this.#normalizeHolding(record))
2268
+ }
2269
+ } catch (err) {
2270
+ console.warn(
2271
+ 'Failed to load node holdings metadata, using empty list:',
2272
+ err.message
2273
+ )
2274
+ }
2275
+ return []
2276
+ }
2277
+
2278
+ #saveHoldingsMetadata() {
2279
+ try {
2280
+ const metadataPath = this.#getHoldingsMetadataPath()
2281
+ this.#atomicWrite(metadataPath, JSON.stringify(this.#holdings, null, 2))
2282
+ } catch (err) {
2283
+ console.error('Failed to save node holdings metadata:', err.message)
2284
+ }
2285
+ }
2286
+
1639
2287
  #loadTrashMetadata() {
1640
2288
  try {
1641
2289
  const metadataPath = this.#getTrashMetadataPath()
@@ -1796,14 +2444,21 @@ export class MostBoxEngine extends EventEmitter {
1796
2444
  }
1797
2445
 
1798
2446
  /**
1799
- * 等待驱动器内容从对等节点或本地可用
2447
+ * 等待指定 Hyperdrive key 从对等节点或本地可用。
1800
2448
  * @param {Hyperdrive} drive - 要检查的驱动器
2449
+ * @param {string} key - 期望的 Hyperdrive key,固定为 /<cid>
1801
2450
  * @param {number} timeout - 最大等待时间(毫秒)
1802
2451
  * @param {string} [taskId] - 用于取消的任务 ID
1803
2452
  * @param {object} [taskState] - 任务状态对象
1804
- * @returns {Promise<Array>} - 条目列表
2453
+ * @returns {Promise<object|null>} - Hyperdrive entry
1805
2454
  */
1806
- async #waitForDriveContent(drive, timeout, taskId = null, taskState = null) {
2455
+ async #waitForDriveEntry(
2456
+ drive,
2457
+ key,
2458
+ timeout,
2459
+ taskId = null,
2460
+ taskState = null
2461
+ ) {
1807
2462
  const startTime = Date.now()
1808
2463
  let pollInterval = DOWNLOAD_POLL_INTERVAL_MIN
1809
2464
  let lastPeerCount = 0
@@ -1811,15 +2466,12 @@ export class MostBoxEngine extends EventEmitter {
1811
2466
  let bootstrapNodesChecked = false
1812
2467
  let lastUpdateTime = 0
1813
2468
 
1814
- const localEntries = []
1815
2469
  try {
1816
- for await (const entry of drive.list()) {
1817
- localEntries.push(entry)
1818
- }
1819
- if (localEntries.length > 0) {
1820
- console.log(`[MostBox] Found ${localEntries.length} entries locally`)
1821
- this.emit('download:status', { taskId, status: 'syncing' })
1822
- return localEntries
2470
+ const localEntry = await drive.entry(key)
2471
+ if (localEntry) {
2472
+ console.log(`[MostBox] Found expected entry ${key} locally`)
2473
+ if (taskId) this.emit('download:status', { taskId, status: 'syncing' })
2474
+ return localEntry
1823
2475
  }
1824
2476
  } catch {}
1825
2477
 
@@ -1853,32 +2505,32 @@ export class MostBoxEngine extends EventEmitter {
1853
2505
 
1854
2506
  await tryUpdateDrive()
1855
2507
 
1856
- const entries = []
1857
2508
  try {
1858
- for await (const entry of drive.list()) {
1859
- entries.push(entry)
2509
+ const entry = await drive.entry(key)
2510
+ if (entry) {
2511
+ console.log(`[MostBox] Found ${key} after ${elapsed}s`)
2512
+ if (taskId) {
2513
+ this.emit('download:status', { taskId, status: 'syncing' })
2514
+ }
2515
+ return entry
1860
2516
  }
1861
2517
  } catch {}
1862
2518
 
1863
- if (entries.length > 0) {
1864
- console.log(
1865
- `[MostBox] Found ${entries.length} entries after ${elapsed}s`
1866
- )
1867
- this.emit('download:status', { taskId, status: 'syncing' })
1868
- return entries
1869
- }
1870
-
1871
2519
  if (hasPeers) {
1872
2520
  const newStatus = 'syncing'
1873
2521
  if (lastStatus !== newStatus) {
1874
- this.emit('download:status', { taskId, status: newStatus })
2522
+ if (taskId) {
2523
+ this.emit('download:status', { taskId, status: newStatus })
2524
+ }
1875
2525
  lastStatus = newStatus
1876
2526
  }
1877
2527
  pollInterval = Math.min(pollInterval + 200, DOWNLOAD_POLL_INTERVAL_MAX)
1878
2528
  } else {
1879
2529
  const newStatus = 'finding-peers'
1880
2530
  if (lastStatus !== newStatus) {
1881
- this.emit('download:status', { taskId, status: newStatus })
2531
+ if (taskId) {
2532
+ this.emit('download:status', { taskId, status: newStatus })
2533
+ }
1882
2534
  lastStatus = newStatus
1883
2535
  }
1884
2536
  pollInterval = DOWNLOAD_POLL_INTERVAL_MIN
@@ -1912,36 +2564,34 @@ export class MostBoxEngine extends EventEmitter {
1912
2564
 
1913
2565
  await tryUpdateDrive()
1914
2566
 
1915
- const entries = []
1916
2567
  try {
1917
- for await (const entry of drive.list()) {
1918
- entries.push(entry)
2568
+ const entry = await drive.entry(key)
2569
+ if (entry) {
2570
+ console.log(`[MostBox] Found ${key} on final attempt`)
2571
+ return entry
1919
2572
  }
1920
2573
  } catch (err) {
1921
2574
  console.log(`[MostBox] Final attempt failed: ${err.message}`)
1922
2575
  }
1923
2576
 
1924
- console.log(`[MostBox] Final entry count: ${entries.length}`)
1925
-
1926
- if (entries.length === 0) {
1927
- const peerCount = this.#swarm.connections.size
1928
- console.log(`[MostBox] Diagnostic information:`)
1929
- console.log(`[MostBox] - Peer count: ${peerCount}`)
1930
- console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
1931
- console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
2577
+ const peerCount = this.#swarm.connections.size
2578
+ console.log(`[MostBox] Diagnostic information:`)
2579
+ console.log(`[MostBox] - Expected key: ${key}`)
2580
+ console.log(`[MostBox] - Peer count: ${peerCount}`)
2581
+ console.log(`[MostBox] - Bootstrap nodes: ${SWARM_BOOTSTRAP.length}`)
2582
+ console.log(`[MostBox] - Timeout: ${timeout / 1000}s`)
1932
2583
 
1933
- if (peerCount === 0) {
1934
- console.log(
1935
- `[MostBox] Suggestion: Check network connectivity and firewall settings`
1936
- )
1937
- } else {
1938
- console.log(
1939
- `[MostBox] Suggestion: Publisher may be offline or file may have been removed`
1940
- )
1941
- }
2584
+ if (peerCount === 0) {
2585
+ console.log(
2586
+ `[MostBox] Suggestion: Check network connectivity and firewall settings`
2587
+ )
2588
+ } else {
2589
+ console.log(
2590
+ `[MostBox] Suggestion: Publisher may be offline or file may have been removed`
2591
+ )
1942
2592
  }
1943
2593
 
1944
- return entries
2594
+ return null
1945
2595
  }
1946
2596
  }
1947
2597