most-box 0.1.0 → 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.
- package/README.md +3 -2
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +6 -6
- package/out/__next._full.txt +23 -20
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +8 -6
- package/out/__next._tree.txt +6 -4
- package/out/_next/static/chunks/0-n3pg7th.zza.js +1 -0
- package/out/_next/static/chunks/0.4j.0k5a64vg.js +1 -0
- package/out/_next/static/chunks/0.ozi1_x2.m.~.js +1 -0
- package/out/_next/static/chunks/0.t5wlt51zou5.js +1 -0
- package/out/_next/static/chunks/0.w4hkvap~bva.js +1 -0
- package/out/_next/static/chunks/00-u5nq76f0.j.js +1 -0
- package/out/_next/static/chunks/00d9h1tddnnnd.js +1 -0
- package/out/_next/static/chunks/00fm8lijienf1.js +1 -0
- package/out/_next/static/chunks/00o9ht.f2qm00.css +4 -0
- package/out/_next/static/chunks/00tkdqwxch-3s.js +1 -0
- package/out/_next/static/chunks/00zi-erhjrny2.js +2 -0
- package/out/_next/static/chunks/01l3o90g~1z42.js +1 -0
- package/out/_next/static/chunks/01mfky9camw6i.js +1 -0
- package/out/_next/static/chunks/01r.v-pqs1vrm.js +1 -0
- package/out/_next/static/chunks/03edqrb4zdj~g.js +31 -0
- package/out/_next/static/chunks/03h_6oo-gqkhz.js +1 -0
- package/out/_next/static/chunks/04hcgsanv1hhu.js +1 -0
- package/out/_next/static/chunks/05g2q0w5b34.g.js +1 -0
- package/out/_next/static/chunks/05of77xycbt8~.js +1 -0
- package/out/_next/static/chunks/05zwemzfjx3sh.js +1 -0
- package/out/_next/static/chunks/06dpc5df94.v1.js +1 -0
- package/out/_next/static/chunks/06e1~1-z_ic9a.js +1 -0
- package/out/_next/static/chunks/075s7sn.ns~u5.js +1 -0
- package/out/_next/static/chunks/07dynrbvd3.f4.js +1 -0
- package/out/_next/static/chunks/07p~uva5pwgwe.js +1 -0
- package/out/_next/static/chunks/07r9nn-pzlgg1.js +1 -0
- package/out/_next/static/chunks/08.72abkgwy9g.js +1 -0
- package/out/_next/static/chunks/084xf0edl9sfo.js +1 -0
- package/out/_next/static/chunks/08576xhv~~jck.js +1 -0
- package/out/_next/static/chunks/08u211~k~qu52.js +1 -0
- package/out/_next/static/chunks/098.p.2-zm4p7.js +1 -0
- package/out/_next/static/chunks/09f1gfke9m5wg.css +1 -0
- package/out/_next/static/chunks/09ngvtajm7e5y.js +1 -0
- package/out/_next/static/chunks/09ps~-43n5qyo.js +1 -0
- package/out/_next/static/chunks/09v7_0gclxr46.js +1 -0
- package/out/_next/static/chunks/09xyi6fpro_d-.css +1 -0
- package/out/_next/static/chunks/09yql86dir9c4.js +1 -0
- package/out/_next/static/chunks/09zmlfljowj1~.js +1 -0
- package/out/_next/static/chunks/0_npg_pcoywti.js +5 -0
- package/out/_next/static/chunks/0_r_mk1~6bosc.js +1 -0
- package/out/_next/static/chunks/0_s~ebb-7b2hr.js +1 -0
- package/out/_next/static/chunks/0_w-0-2z5oqd_.js +1 -0
- package/out/_next/static/chunks/0ao1lbi4b.sfa.js +1 -0
- package/out/_next/static/chunks/0arm0a6adt7cc.css +1 -0
- package/out/_next/static/chunks/0bld2u_ld~va2.js +1 -0
- package/out/_next/static/chunks/0bliugh5lxw55.js +1 -0
- package/out/_next/static/chunks/{0e_h0d3ekzks8.css → 0c9j3eq_14vv2.css} +1 -1
- package/out/_next/static/chunks/0cn9a7aimbdzq.js +1 -0
- package/out/_next/static/chunks/0d3f-nk3c.2re.js +1 -0
- package/out/_next/static/chunks/0d4bueddmcnca.js +1 -0
- package/out/_next/static/chunks/0dtohpf7~3d12.js +1 -0
- package/out/_next/static/chunks/0e-3e8h7g99yf.js +1 -0
- package/out/_next/static/chunks/0e531nije_ln2.js +1 -0
- package/out/_next/static/chunks/0e5zvj_rh0z3m.js +1 -0
- package/out/_next/static/chunks/0f4y~rkk-n81e.js +1 -0
- package/out/_next/static/chunks/0fk~0~p7ivfn1.js +1 -0
- package/out/_next/static/chunks/0fw6juc~lsj3z.js +1 -0
- package/out/_next/static/chunks/0g0u7785a73vo.js +1 -0
- package/out/_next/static/chunks/0g_fpgh7drfda.js +1 -0
- package/out/_next/static/chunks/0gtwvy1z9ksa7.css +1 -0
- package/out/_next/static/chunks/0gze5uso1mbe9.js +1 -0
- package/out/_next/static/chunks/0h4r.qtmpa6eh.js +1 -0
- package/out/_next/static/chunks/0hf.aosc-7172.js +1 -0
- package/out/_next/static/chunks/0hgz35c1ejbs9.js +1 -0
- package/out/_next/static/chunks/0hrw-r.xmvmsq.js +1 -0
- package/out/_next/static/chunks/0hzg4al.v~8~m.js +1 -0
- package/out/_next/static/chunks/0ip9xrols_83o.js +1 -0
- package/out/_next/static/chunks/0j27tcmtt4ly7.js +1 -0
- package/out/_next/static/chunks/0j3v4mq67wtnh.js +1 -0
- package/out/_next/static/chunks/0j4-d0qf.v~kn.js +1 -0
- package/out/_next/static/chunks/0jhdeq.j9_02m.js +1 -0
- package/out/_next/static/chunks/0jy63h3i-y69i.js +1 -0
- package/out/_next/static/chunks/0kdnx_u-60k9s.js +1 -0
- package/out/_next/static/chunks/0kq~edq42o1-c.js +1 -0
- package/out/_next/static/chunks/0l682p362d-5w.js +1 -0
- package/out/_next/static/chunks/0lkmf5ry.s_7w.js +1 -0
- package/out/_next/static/chunks/0m68p9txef5rs.js +1 -0
- package/out/_next/static/chunks/0mme-fm5d2oz2.js +1 -0
- package/out/_next/static/chunks/0myp4sjagr~h0.js +1 -0
- package/out/_next/static/chunks/0n.qlfk~z7o.6.js +1 -0
- package/out/_next/static/chunks/0n4t80gjc3q5h.js +1 -0
- package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 0o6lrkxy4jwag.js} +1 -1
- package/out/_next/static/chunks/0o98f1yq..o.8.js +1 -0
- package/out/_next/static/chunks/0oz3yl6_-716p.js +1 -0
- package/out/_next/static/chunks/0p486m03-zfoi.js +1 -0
- package/out/_next/static/chunks/0qou.u2e2dy48.css +24 -0
- package/out/_next/static/chunks/0qqupeexg83u7.js +1 -0
- package/out/_next/static/chunks/0r1~k82nji8sf.js +1 -0
- package/out/_next/static/chunks/0rb-ri481.kc9.js +1 -0
- package/out/_next/static/chunks/0rsnmahfd.59p.js +1 -0
- package/out/_next/static/chunks/0rt6rgnvr-s_p.js +1 -0
- package/out/_next/static/chunks/0runh28p_gg6..js +1 -0
- package/out/_next/static/chunks/0shy.t1fwqcev.js +1 -0
- package/out/_next/static/chunks/{0d3shmwh5_nmn.js → 0t2xr05rlu96l.js} +1 -1
- package/out/_next/static/chunks/0t6h56rhg1y5i.js +1 -0
- package/out/_next/static/chunks/0tdqd1zunusgk.js +1 -0
- package/out/_next/static/chunks/0ujbnp38x63ek.js +1 -0
- package/out/_next/static/chunks/0usvo~vu7r8np.js +736 -0
- package/out/_next/static/chunks/0v68pdrp54lb-.js +1 -0
- package/out/_next/static/chunks/0v7qp4hv-_._r.js +1 -0
- package/out/_next/static/chunks/0vsm0m5sxrb.3.js +1 -0
- package/out/_next/static/chunks/0vzlz.iboqo3c.js +1 -0
- package/out/_next/static/chunks/0w87vbpkf-ogd.js +1 -0
- package/out/_next/static/chunks/0wuwlgcn6gxqt.js +1 -0
- package/out/_next/static/chunks/0xgg0~kmf3gd-.js +1 -0
- package/out/_next/static/chunks/0xj24-70ptdzp.js +1 -0
- package/out/_next/static/chunks/0xl5_avhu._i8.js +1 -0
- package/out/_next/static/chunks/0xxlx772fr3x4.js +1 -0
- package/out/_next/static/chunks/0y.li-~3oybew.js +1 -0
- package/out/_next/static/chunks/0yl2t7cs-n_ng.js +1 -0
- package/out/_next/static/chunks/0yq3kh.hchtm_.js +1 -0
- package/out/_next/static/chunks/0ys0l5au.9c2c.js +1 -0
- package/out/_next/static/chunks/0z48pmi6buytt.js +1 -0
- package/out/_next/static/chunks/0zapnvgy89mg..js +1 -0
- package/out/_next/static/chunks/0~.-vxi5oc.r0.js +1 -0
- package/out/_next/static/chunks/0~3ik-hfp9s-7.js +1 -0
- package/out/_next/static/chunks/0~4f5p6tvn1lq.js +1 -0
- package/out/_next/static/chunks/0~_0ys.2whxbw.js +1 -0
- package/out/_next/static/chunks/0~_ui9l7.2sxf.js +1 -0
- package/out/_next/static/chunks/1037jlyw5~7ht.js +1 -0
- package/out/_next/static/chunks/1045hfzu533z0.js +1 -0
- package/out/_next/static/chunks/104e5nmc.c-pl.js +1 -0
- package/out/_next/static/chunks/109taw1pbh-0b.js +1 -0
- package/out/_next/static/chunks/10kvl8vj_plm-.js +1 -0
- package/out/_next/static/chunks/10x7~onqwp338.js +1 -0
- package/out/_next/static/chunks/10ynz1dy483wf.js +1 -0
- package/out/_next/static/chunks/11hds.mg~4_r-.js +1 -0
- package/out/_next/static/chunks/11ibzaklcauw~.js +1 -0
- package/out/_next/static/chunks/11z.0s6.42b.p.js +1 -0
- package/out/_next/static/chunks/12-9n56l0y3yr.js +1 -0
- package/out/_next/static/chunks/126enaq~f7scl.js +1 -0
- package/out/_next/static/chunks/1380op_pfk.qo.js +1 -0
- package/out/_next/static/chunks/146oiw1bggtn4.js +1 -0
- package/out/_next/static/chunks/14_po2rb_arn4.js +1 -0
- package/out/_next/static/chunks/14a4fwbiq.l3z.js +1 -0
- package/out/_next/static/chunks/14cowsqn95m1k.js +1 -0
- package/out/_next/static/chunks/14dtd3l03v.kx.js +1 -0
- package/out/_next/static/chunks/14tm3qa-v9o-4.js +1 -0
- package/out/_next/static/chunks/15-o4kb-evqd7.js +1 -0
- package/out/_next/static/chunks/157z7bowux3xj.js +1 -0
- package/out/_next/static/chunks/15m1_677az2cm.js +1 -0
- package/out/_next/static/chunks/15v.~.ne6ogkk.js +1 -0
- package/out/_next/static/chunks/16i.qbk8t8gf_.js +1 -0
- package/out/_next/static/chunks/16m27azcs4k6w.js +1 -0
- package/out/_next/static/chunks/16u9f35gylw8l.js +1 -0
- package/out/_next/static/chunks/17ajyb5ogk5yj.js +1 -0
- package/out/_next/static/chunks/17dyfxbq8yz8n.js +1 -0
- package/out/_next/static/chunks/180zln9pcq9ih.js +1 -0
- package/out/_next/static/chunks/1814izi5gh.kp.js +1 -0
- package/out/_next/static/chunks/turbopack-0xs6mybc~5t_3.js +1 -0
- package/out/_next/static/media/KaTeX_AMS-Regular.0b~8ki5y928w2.woff +0 -0
- package/out/_next/static/media/KaTeX_AMS-Regular.0p1vbqd84i2~o.woff2 +0 -0
- package/out/_next/static/media/KaTeX_AMS-Regular.173t6ktr7uf-w.ttf +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Bold.01-pzluls4zgb.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Bold.0x2v1lwn~880f.woff +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Bold.16zv5fax0h0ka.ttf +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Regular.02i3z7wig438t.ttf +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Regular.0rysu1t-ncjq8.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Caligraphic-Regular.10927swgekwun.woff +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Bold.0e-16u10iuyyf.woff +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Bold.0et27v~3~4uhe.ttf +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Bold.0w23i72~hprpq.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Regular.0b.riegzdfue2.woff +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Regular.0rekyoa-52fj_.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Fraktur-Regular.0vjwa15znhk~4.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Bold.09i7~607shf-h.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Bold.09lmynrorhcbw.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-Bold.16pfc63_du6mx.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Main-BoldItalic.0cp37g7x1q8h6.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-BoldItalic.0d54rk08rx11s.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Main-BoldItalic.15j6k~hix2t_0.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Italic.0382gqciexmbu.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-Italic.06o5nq0_91v60.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Main-Italic.0su4i6mm18-wo.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Regular.08zh8z.7shijf.ttf +0 -0
- package/out/_next/static/media/KaTeX_Main-Regular.0diheg01zyoph.woff +0 -0
- package/out/_next/static/media/KaTeX_Main-Regular.0kaf-ag2_wkm-.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Math-BoldItalic.0ajzxypnbx1h1.ttf +0 -0
- package/out/_next/static/media/KaTeX_Math-BoldItalic.0ck1myuerwyqw.woff +0 -0
- package/out/_next/static/media/KaTeX_Math-BoldItalic.0ja97dn.cpc87.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Math-Italic.09xkhecjcn5r9.woff +0 -0
- package/out/_next/static/media/KaTeX_Math-Italic.0x23a-bmp-5tg.ttf +0 -0
- package/out/_next/static/media/KaTeX_Math-Italic.0zrha2c4sl2je.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Bold.05a9.pc1j_zx9.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Bold.0jcl-ayi1uun0.woff +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Bold.0re8y.dm7.mt5.ttf +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Italic.0a0234dc3s62j.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Italic.0judofdln9731.woff +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Italic.10z1iap9pfus8.ttf +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Regular.0h9yjlugq4q_e.woff +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Regular.0v6gcj32-czft.woff2 +0 -0
- package/out/_next/static/media/KaTeX_SansSerif-Regular.0zm18kga42ebc.ttf +0 -0
- package/out/_next/static/media/KaTeX_Script-Regular.0c4.h-mer83d_.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Script-Regular.0q14y6zkzlpob.ttf +0 -0
- package/out/_next/static/media/KaTeX_Script-Regular.0ze6v4r_-99oy.woff +0 -0
- package/out/_next/static/media/KaTeX_Size1-Regular.013x6a4ierotp.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size1-Regular.0kidw0oi.m68o.woff +0 -0
- package/out/_next/static/media/KaTeX_Size1-Regular.0m6y-i6wfokni.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size2-Regular.0blpmluwilgbg.woff +0 -0
- package/out/_next/static/media/KaTeX_Size2-Regular.0d5inmyp-tyv3.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size2-Regular.0wnhnvj-.k9d5.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size3-Regular.01h0xm_sfctj3.woff +0 -0
- package/out/_next/static/media/KaTeX_Size3-Regular.0iukctyhw5j56.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size3-Regular.0jl8mqyf4gzpn.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size4-Regular.0w3.rb_c4stzk.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Size4-Regular.0wr_9l81-mu06.ttf +0 -0
- package/out/_next/static/media/KaTeX_Size4-Regular.12tvaesf3.zl3.woff +0 -0
- package/out/_next/static/media/KaTeX_Typewriter-Regular.0c4zdxz~8frhm.woff2 +0 -0
- package/out/_next/static/media/KaTeX_Typewriter-Regular.0cgrzn5l3kao5.woff +0 -0
- package/out/_next/static/media/KaTeX_Typewriter-Regular.128~qc3858otl.ttf +0 -0
- package/out/_not-found/__next._full.txt +21 -19
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +8 -6
- package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +3 -2
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +21 -19
- package/out/admin/__next._full.txt +23 -0
- package/out/admin/__next._head.txt +5 -0
- package/out/admin/__next._index.txt +9 -0
- package/out/admin/__next._tree.txt +5 -0
- package/out/admin/__next.admin.__PAGE__.txt +9 -0
- package/out/admin/__next.admin.txt +5 -0
- package/out/admin/index.html +15 -0
- package/out/admin/index.txt +23 -0
- package/out/app/__next._full.txt +15 -13
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +8 -6
- package/out/app/__next._tree.txt +4 -2
- package/out/app/__next.app.__PAGE__.txt +4 -4
- package/out/app/__next.app.txt +3 -4
- package/out/app/index.html +2 -2
- package/out/app/index.txt +15 -13
- package/out/changelog/__next._full.txt +24 -21
- package/out/changelog/__next._head.txt +3 -3
- package/out/changelog/__next._index.txt +8 -6
- package/out/changelog/__next._tree.txt +5 -3
- package/out/changelog/__next.changelog.__PAGE__.txt +5 -5
- package/out/changelog/__next.changelog.txt +3 -3
- package/out/changelog/index.html +2 -2
- package/out/changelog/index.txt +24 -21
- package/out/chat/__next._full.txt +16 -14
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +8 -6
- package/out/chat/__next._tree.txt +5 -3
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +4 -5
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +16 -14
- package/out/docs/__next._full.txt +24 -21
- package/out/docs/__next._head.txt +3 -3
- package/out/docs/__next._index.txt +8 -6
- package/out/docs/__next._tree.txt +5 -3
- package/out/docs/__next.docs.__PAGE__.txt +5 -5
- package/out/docs/__next.docs.txt +3 -3
- package/out/docs/getting-started/__next._full.txt +24 -21
- package/out/docs/getting-started/__next._head.txt +3 -3
- package/out/docs/getting-started/__next._index.txt +8 -6
- package/out/docs/getting-started/__next._tree.txt +5 -3
- package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +5 -5
- package/out/docs/getting-started/__next.docs.getting-started.txt +3 -3
- package/out/docs/getting-started/__next.docs.txt +3 -3
- package/out/docs/getting-started/index.html +2 -2
- package/out/docs/getting-started/index.txt +24 -21
- package/out/docs/index.html +2 -2
- package/out/docs/index.txt +24 -21
- package/out/download/__next._full.txt +33 -32
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +8 -6
- package/out/download/__next._tree.txt +5 -3
- package/out/download/__next.download.__PAGE__.txt +10 -11
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +33 -32
- package/out/index.html +2 -2
- package/out/index.txt +23 -20
- package/out/note/__next._full.txt +24 -0
- package/out/note/__next._head.txt +5 -0
- package/out/note/__next._index.txt +9 -0
- package/out/note/__next._tree.txt +4 -0
- package/out/note/__next.note.__PAGE__.txt +9 -0
- package/out/note/__next.note.txt +5 -0
- package/out/note/edit/__next._full.txt +24 -0
- package/out/note/edit/__next._head.txt +5 -0
- package/out/note/edit/__next._index.txt +9 -0
- package/out/note/edit/__next._tree.txt +4 -0
- package/out/note/edit/__next.note.edit.__PAGE__.txt +9 -0
- package/out/note/edit/__next.note.edit.txt +5 -0
- package/out/note/edit/__next.note.txt +5 -0
- package/out/note/edit/index.html +15 -0
- package/out/note/edit/index.txt +24 -0
- package/out/note/index.html +15 -0
- package/out/note/index.txt +24 -0
- package/out/ping/__next._full.txt +22 -19
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +8 -6
- package/out/ping/__next._tree.txt +5 -3
- package/out/ping/__next.ping.__PAGE__.txt +5 -5
- package/out/ping/__next.ping.txt +3 -3
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +22 -19
- package/out/web3/__next._full.txt +16 -14
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +8 -6
- package/out/web3/__next._tree.txt +5 -3
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +4 -5
- package/out/web3/ed25519/__next._full.txt +14 -12
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +8 -6
- package/out/web3/ed25519/__next._tree.txt +5 -3
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
- package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
- package/out/web3/ed25519/__next.web3.txt +4 -5
- package/out/web3/ed25519/index.html +1 -1
- package/out/web3/ed25519/index.txt +14 -12
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +16 -14
- package/out/web3/tools/__next._full.txt +14 -12
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +8 -6
- package/out/web3/tools/__next._tree.txt +5 -3
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
- package/out/web3/tools/__next.web3.tools.txt +3 -3
- package/out/web3/tools/__next.web3.txt +4 -5
- package/out/web3/tools/index.html +1 -1
- package/out/web3/tools/index.txt +14 -12
- package/package.json +12 -5
- package/server/index.js +558 -101
- package/server/src/config.js +4 -0
- package/server/src/core/cid.js +53 -32
- package/server/src/index.js +772 -122
- package/server/src/node/config.js +159 -0
- package/server/src/node/logs.js +94 -0
- package/server/src/utils/api.js +80 -2
- package/server/src/utils/errors.js +7 -0
- package/server/src/utils/mostWallet.js +34 -1
- package/server/src/utils/noteBackup.js +119 -0
- package/server/src/utils/noteUtils.js +120 -0
- package/server/src/utils/userIdentity.js +8 -60
- package/out/_next/static/chunks/003jnm.v5tzw5.js +0 -1
- package/out/_next/static/chunks/00re8v.gbcywn.js +0 -1
- package/out/_next/static/chunks/00s106sbq8t9v.js +0 -1
- package/out/_next/static/chunks/012hi627qrdnn.js +0 -1
- package/out/_next/static/chunks/0174xh3wfsjm1.js +0 -2
- package/out/_next/static/chunks/02~o2nmo5pmy1.js +0 -1
- package/out/_next/static/chunks/07t.dhhokszz5.css +0 -1
- package/out/_next/static/chunks/0_wia9ofmsi1c.css +0 -2
- package/out/_next/static/chunks/0ah8fihozo2_u.js +0 -5
- package/out/_next/static/chunks/0bzupvr5gt3k9.js +0 -31
- package/out/_next/static/chunks/0gdluj423gso1.js +0 -1
- package/out/_next/static/chunks/0gmoiq06srjay.css +0 -1
- package/out/_next/static/chunks/0imkasy7kb67u.js +0 -1
- package/out/_next/static/chunks/0jjc_b9q_ldi2.js +0 -1
- package/out/_next/static/chunks/0jl~j62iz2uvr.js +0 -1
- package/out/_next/static/chunks/0lqslm813wk_h.js +0 -1
- package/out/_next/static/chunks/0q782fxxd0lx~.js +0 -1
- package/out/_next/static/chunks/0slwj0c46k5cu.js +0 -1
- package/out/_next/static/chunks/0sorqk.oc6b7j.css +0 -1
- package/out/_next/static/chunks/0tapzqc6hgvx-.js +0 -1
- package/out/_next/static/chunks/0xsc7z5x8n7wg.js +0 -1
- package/out/_next/static/chunks/0zm~gys2jwl0g.js +0 -1
- package/out/_next/static/chunks/turbopack-0a_g3u0ud~jb8.js +0 -1
- /package/out/_next/static/{iOB2EBwOGZ0iYW7Lbg9u_ → sIuUKxnnGU7K9Tu9UDKE8}/_buildManifest.js +0 -0
- /package/out/_next/static/{iOB2EBwOGZ0iYW7Lbg9u_ → sIuUKxnnGU7K9Tu9UDKE8}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{iOB2EBwOGZ0iYW7Lbg9u_ → sIuUKxnnGU7K9Tu9UDKE8}/_ssgManifest.js +0 -0
package/server/src/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
655
|
+
`[MostBox] Waiting for drive entry /${cidString} (timeout: ${downloadTimeout / 1000}s)...`
|
|
575
656
|
)
|
|
576
|
-
const
|
|
657
|
+
const driveKey = '/' + cidString
|
|
658
|
+
const entry = await this.#waitForDriveEntry(
|
|
577
659
|
drive,
|
|
578
|
-
|
|
660
|
+
driveKey,
|
|
661
|
+
downloadTimeout,
|
|
579
662
|
taskId,
|
|
580
663
|
taskState
|
|
581
664
|
)
|
|
582
665
|
|
|
583
|
-
if (
|
|
584
|
-
console.log(`[MostBox]
|
|
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 =
|
|
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 ${
|
|
692
|
+
`[MostBox] Found expected entry ${driveKey}, starting download...`
|
|
610
693
|
)
|
|
611
694
|
|
|
612
|
-
const targetDir = this.#options.
|
|
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
|
-
|
|
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',
|
|
682
|
-
ws.on('error',
|
|
683
|
-
rs.on('error',
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
889
|
-
await
|
|
890
|
-
|
|
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 =
|
|
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
|
-
|
|
918
|
-
await
|
|
919
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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<
|
|
2453
|
+
* @returns {Promise<object|null>} - Hyperdrive entry
|
|
1805
2454
|
*/
|
|
1806
|
-
async #
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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
|
-
|
|
1859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1918
|
-
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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
|
|
2594
|
+
return null
|
|
1945
2595
|
}
|
|
1946
2596
|
}
|
|
1947
2597
|
|