+
+ {{ {connected: '进度已连接', connecting: '正在连接进度', reconnecting: '连接中断,正在重连', disconnected: '进度连接已断开'}[store.state.connectionStatus] }}
+
{
- if(res.data.result === "success")
+ if(res.data.result === "success" && version === taskRefreshVersion)
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
})
},
postGalleryTask(context, data){
- axios.post(GalleryManageUrl + '?' + qs.stringify({
+ return axios.post(GalleryManageUrl + '?' + qs.stringify({
AuthCode: context.state.AuthCode,
...data
}, {indices:false})).then((res) => {
if(res.data.result === "success") {
ElMessage("提交成功")
- context.commit("_setChosenGallery", {gallery: false,
- resolution:data.targetResolution})
+ context.commit("_setChosenGallery", {gallery: false})
}
else
if(res.data.data)
ElMessage(res.data.data)
else
ElMessage("提交失败")
+ // A node timeout can still leave a persisted task; always reload its actual state.
+ return Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")])
})
},
queryGalleryTask(context, link){
@@ -86,30 +91,40 @@ const actions = {
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
context.dispatch("initWebsocket").then()
}
- else
+ else {
+ context.dispatch("disconnectWebsocket")
context.commit("_unAuthed")
+ }
})
},
initWebsocket(context){
- context.state.websocket = new WebSocket("wss://downloader.lionwebsite.xyz/ws/")
- context.state.websocket.onopen = () => {
- context.state.websocket.send("DownloaderWebsocket")
- }
-
- context.state.websocket.onmessage = (event) => {
- let message = JSON.parse(event.data)
-
- switch (message.type){
- case "updateTasks":
+ taskSocket?.stop()
+ taskSocket = createTaskSocket({
+ url: "wss://downloader.lionwebsite.xyz/ws/",
+ onState: status => context.commit("_setConnectionStatus", status),
+ onOpen: () => {
+ // Catch up on every connection, including events missed during an outage.
+ Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")]).catch(() => {})
+ },
+ onMessage: event => {
+ let message
+ try { message = JSON.parse(event.data) }
+ catch { return }
+ if(message.type === "updateTasks" && Array.isArray(message.data))
context.commit("_updateGalleryTaskProceeding", message.data)
- break
- case "fullUpdate":
- context.dispatch("updateGalleryTasks").then()
+ else if(message.type === "fullUpdate")
+ context.dispatch("updateGalleryTasks").catch(() => {})
}
- }
+ })
+ taskSocket.start()
+ },
+ disconnectWebsocket(context){
+ taskSocket?.stop()
+ taskSocket = undefined
+ context.commit("_setConnectionStatus", "disconnected")
},
loadWeekUsedAmount(context){
- axios.get(GalleryManageUrl + "/weekUsedAmount", {
+ return axios.get(GalleryManageUrl + "/weekUsedAmount", {
params: {
AuthCode: context.state.AuthCode
}
@@ -197,6 +212,9 @@ const actions = {
}
const mutations = {
+ _setConnectionStatus(state, status){
+ state.connectionStatus = status
+ },
_collectGallery(state, gid){
let tasks = state.totalGalleryTask
for(let i=0; i< tasks.length; i++){
@@ -354,17 +372,6 @@ const mutations = {
deleteTask(tasks, 'gid', gid)
},
_setChosenGallery(state,data){
- if(data.gallery === false) {
- state.chosenGallery.shortName = getShortname(state.chosenGallery.name)
- state.chosenGallery.resolution = data.resolution
- state.chosenGallery.fileSize = "等待下载完成后再查看"
- state.chosenGallery.createTimeDisplay = "等待下载完成后再查看"
- state.chosenGallery.progress = "已提交"
- state.chosenGallery.downloader = state.userId
- state.chosenGallery.thumb_link = GalleryManageUrl + "/ehThumbnail?path=" + state.chosenGallery.thumb_link
- state.totalGalleryTask.push(state.chosenGallery)
- state.downloadGallery.push(state.chosenGallery)
- }
state.chosenGallery = data.gallery
},
_setCategory(state, category){
@@ -403,7 +410,7 @@ const mutations = {
}
const state = {
- websocket: null, //websocket
+ connectionStatus: "disconnected", // live task updates
totalGalleryTask: [], //存放数据的数组
chosenGallery: false, //准备下载
diff --git a/src/utils/taskSocket.js b/src/utils/taskSocket.js
new file mode 100644
index 0000000..6d0893a
--- /dev/null
+++ b/src/utils/taskSocket.js
@@ -0,0 +1,76 @@
+export function createTaskSocket({url, onOpen, onMessage, onState,
+ createSocket = address => new WebSocket(address),
+ setTimer = setTimeout, clearTimer = clearTimeout}) {
+ let socket = null
+ let retryTimer
+ let connectTimer
+ let attempts = 0
+ let stopped = true
+
+ function detach() {
+ clearTimer(connectTimer)
+ connectTimer = undefined
+ if (socket) {
+ const previous = socket
+ socket = null
+ previous.onopen = previous.onclose = previous.onerror = previous.onmessage = null
+ try { previous.close() } catch { /* already closed */ }
+ }
+ }
+
+ function schedule() {
+ if (stopped) return
+ onState('reconnecting')
+ const delay = Math.min(1000 * 2 ** attempts, 30000)
+ attempts = Math.min(attempts + 1, 5)
+ clearTimer(retryTimer)
+ retryTimer = setTimer(connect, delay)
+ }
+
+ function connect() {
+ if (stopped) return
+ clearTimer(retryTimer)
+ retryTimer = undefined
+ onState(attempts ? 'reconnecting' : 'connecting')
+ let current
+ try { current = socket = createSocket(url) }
+ catch { schedule(); return }
+ const retry = () => {
+ if (stopped || socket !== current) return
+ detach()
+ schedule()
+ }
+ current.onopen = () => {
+ if (stopped || socket !== current) return
+ clearTimer(connectTimer)
+ connectTimer = undefined
+ attempts = 0
+ try { current.send('DownloaderWebsocket') }
+ catch { retry(); return }
+ onState('connected')
+ onOpen()
+ }
+ current.onmessage = event => {
+ if (!stopped && socket === current) onMessage(event)
+ }
+ current.onclose = retry
+ current.onerror = retry
+ connectTimer = setTimer(retry, 10000)
+ }
+
+ return {
+ start() {
+ if (!stopped) return
+ stopped = false
+ attempts = 0
+ connect()
+ },
+ stop() {
+ stopped = true
+ clearTimer(retryTimer)
+ retryTimer = undefined
+ detach()
+ onState('disconnected')
+ }
+ }
+}
diff --git a/tests/taskSocket.test.mjs b/tests/taskSocket.test.mjs
new file mode 100644
index 0000000..ce81b48
--- /dev/null
+++ b/tests/taskSocket.test.mjs
@@ -0,0 +1,63 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import {createTaskSocket} from '../src/utils/taskSocket.js'
+
+function fixture() {
+ const sockets = [], states = [], messages = [], timers = new Map()
+ let id = 0, opens = 0
+ const manager = createTaskSocket({url: 'wss://example.invalid',
+ onOpen: () => opens++, onState: value => states.push(value), onMessage: event => messages.push(event.data),
+ setTimer: (fn, delay) => { timers.set(++id, {fn, delay}); return id },
+ clearTimer: key => timers.delete(key),
+ createSocket: () => {
+ const socket = {sent: [], send(value) { this.sent.push(value) }, close() { this.closed = true }}
+ sockets.push(socket)
+ return socket
+ }
+ })
+ function tick(delay) {
+ const entry = [...timers].find(([, timer]) => timer.delay === delay)
+ assert.ok(entry, `expected timer ${delay}`)
+ timers.delete(entry[0]); entry[1].fn()
+ }
+ return {manager, sockets, states, messages, timers, tick, opens: () => opens}
+}
+
+test('reconnects and refreshes after each successful connection', () => {
+ const f = fixture(); f.manager.start()
+ f.sockets[0].onopen()
+ assert.deepEqual(f.sockets[0].sent, ['DownloaderWebsocket'])
+ f.sockets[0].onclose()
+ f.tick(1000)
+ f.sockets[1].onopen()
+ assert.equal(f.opens(), 2)
+ assert.equal(f.states.at(-1), 'connected')
+ assert.equal(f.timers.size, 0)
+ f.manager.stop()
+})
+
+test('backs off and times out connection attempts', () => {
+ const f = fixture(); f.manager.start()
+ f.tick(10000)
+ assert.equal(f.sockets[0].closed, true)
+ f.tick(1000); f.sockets[1].onerror(); f.tick(2000)
+ f.sockets[2].onerror(); f.tick(4000)
+ assert.equal(f.sockets.length, 4)
+ f.manager.stop()
+})
+
+test('stop cancels timers and ignores callbacks from replaced sockets', () => {
+ const f = fixture(); f.manager.start()
+ const oldOpen = f.sockets[0].onopen, oldMessage = f.sockets[0].onmessage
+ f.sockets[0].onerror()
+ f.manager.stop()
+ assert.equal(f.timers.size, 0)
+ f.manager.start()
+ oldOpen(); oldMessage({data: 'stale'})
+ assert.equal(f.opens(), 0)
+ assert.deepEqual(f.messages, [])
+ f.sockets[1].onopen()
+ f.sockets[1].onmessage({data: 'current'})
+ assert.deepEqual(f.messages, ['current'])
+ f.manager.stop()
+})