断线后自动重连并重新同步任务列表
This commit is contained in:
+19
@@ -1,4 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {onMounted, onBeforeUnmount} from "vue";
|
||||||
|
import store from "./store/index.js";
|
||||||
|
const resumeProgress = () => {
|
||||||
|
if (store.state.isAuth && document.visibilityState === 'visible') store.dispatch("initWebsocket");
|
||||||
|
};
|
||||||
|
const pauseProgress = () => store.dispatch("disconnectWebsocket");
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('online', resumeProgress);
|
||||||
|
window.addEventListener('pageshow', resumeProgress);
|
||||||
|
window.addEventListener('pagehide', pauseProgress);
|
||||||
|
document.addEventListener('visibilitychange', resumeProgress);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('online', resumeProgress);
|
||||||
|
window.removeEventListener('pageshow', resumeProgress);
|
||||||
|
window.removeEventListener('pagehide', pauseProgress);
|
||||||
|
document.removeEventListener('visibilitychange', resumeProgress);
|
||||||
|
pauseProgress();
|
||||||
|
});
|
||||||
import Side from "./components/Side.vue";
|
import Side from "./components/Side.vue";
|
||||||
import DashBoard from "./components/DashBoard.vue";
|
import DashBoard from "./components/DashBoard.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-table-wrapper">
|
<div class="side-table-wrapper">
|
||||||
<div v-show="loadComplete" class="load_complete">
|
<div v-show="loadComplete" class="load_complete">
|
||||||
|
<el-tag :type="store.state.connectionStatus === 'connected' ? 'success' : 'warning'" role="status">
|
||||||
|
{{ {connected: '进度已连接', connecting: '正在连接进度', reconnecting: '连接中断,正在重连', disconnected: '进度连接已断开'}[store.state.connectionStatus] }}
|
||||||
|
</el-tag>
|
||||||
<el-table :data="currentTasks"
|
<el-table :data="currentTasks"
|
||||||
:height="tableHeight"
|
:height="tableHeight"
|
||||||
:empty-text="emptyText"
|
:empty-text="emptyText"
|
||||||
|
|||||||
+39
-32
@@ -2,6 +2,9 @@ import vuex from "vuex"
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import {ElMessage} from "element-plus"
|
import {ElMessage} from "element-plus"
|
||||||
import qs from "qs"
|
import qs from "qs"
|
||||||
|
import {createTaskSocket} from "../utils/taskSocket.js"
|
||||||
|
let taskSocket
|
||||||
|
let taskRefreshVersion = 0
|
||||||
const BaseUrl = "https://downloader.lionwebsite.xyz/"
|
const BaseUrl = "https://downloader.lionwebsite.xyz/"
|
||||||
const GalleryManageUrl = BaseUrl + "GalleryManage"
|
const GalleryManageUrl = BaseUrl + "GalleryManage"
|
||||||
|
|
||||||
@@ -29,31 +32,33 @@ const actions = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateGalleryTasks(context){
|
updateGalleryTasks(context){
|
||||||
|
const version = ++taskRefreshVersion
|
||||||
return axios.get(GalleryManageUrl, {
|
return axios.get(GalleryManageUrl, {
|
||||||
params:{
|
params:{
|
||||||
AuthCode: context.state.AuthCode,
|
AuthCode: context.state.AuthCode,
|
||||||
type: 'all'
|
type: 'all'
|
||||||
}
|
}
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if(res.data.result === "success")
|
if(res.data.result === "success" && version === taskRefreshVersion)
|
||||||
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
|
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
postGalleryTask(context, data){
|
postGalleryTask(context, data){
|
||||||
axios.post(GalleryManageUrl + '?' + qs.stringify({
|
return axios.post(GalleryManageUrl + '?' + qs.stringify({
|
||||||
AuthCode: context.state.AuthCode,
|
AuthCode: context.state.AuthCode,
|
||||||
...data
|
...data
|
||||||
}, {indices:false})).then((res) => {
|
}, {indices:false})).then((res) => {
|
||||||
if(res.data.result === "success") {
|
if(res.data.result === "success") {
|
||||||
ElMessage("提交成功")
|
ElMessage("提交成功")
|
||||||
context.commit("_setChosenGallery", {gallery: false,
|
context.commit("_setChosenGallery", {gallery: false})
|
||||||
resolution:data.targetResolution})
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
if(res.data.data)
|
if(res.data.data)
|
||||||
ElMessage(res.data.data)
|
ElMessage(res.data.data)
|
||||||
else
|
else
|
||||||
ElMessage("提交失败")
|
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){
|
queryGalleryTask(context, link){
|
||||||
@@ -86,30 +91,40 @@ const actions = {
|
|||||||
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
|
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
|
||||||
context.dispatch("initWebsocket").then()
|
context.dispatch("initWebsocket").then()
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
|
context.dispatch("disconnectWebsocket")
|
||||||
context.commit("_unAuthed")
|
context.commit("_unAuthed")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initWebsocket(context){
|
initWebsocket(context){
|
||||||
context.state.websocket = new WebSocket("wss://downloader.lionwebsite.xyz/ws/")
|
taskSocket?.stop()
|
||||||
context.state.websocket.onopen = () => {
|
taskSocket = createTaskSocket({
|
||||||
context.state.websocket.send("DownloaderWebsocket")
|
url: "wss://downloader.lionwebsite.xyz/ws/",
|
||||||
}
|
onState: status => context.commit("_setConnectionStatus", status),
|
||||||
|
onOpen: () => {
|
||||||
context.state.websocket.onmessage = (event) => {
|
// Catch up on every connection, including events missed during an outage.
|
||||||
let message = JSON.parse(event.data)
|
Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")]).catch(() => {})
|
||||||
|
},
|
||||||
switch (message.type){
|
onMessage: event => {
|
||||||
case "updateTasks":
|
let message
|
||||||
|
try { message = JSON.parse(event.data) }
|
||||||
|
catch { return }
|
||||||
|
if(message.type === "updateTasks" && Array.isArray(message.data))
|
||||||
context.commit("_updateGalleryTaskProceeding", message.data)
|
context.commit("_updateGalleryTaskProceeding", message.data)
|
||||||
break
|
else if(message.type === "fullUpdate")
|
||||||
case "fullUpdate":
|
context.dispatch("updateGalleryTasks").catch(() => {})
|
||||||
context.dispatch("updateGalleryTasks").then()
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
taskSocket.start()
|
||||||
|
},
|
||||||
|
disconnectWebsocket(context){
|
||||||
|
taskSocket?.stop()
|
||||||
|
taskSocket = undefined
|
||||||
|
context.commit("_setConnectionStatus", "disconnected")
|
||||||
},
|
},
|
||||||
loadWeekUsedAmount(context){
|
loadWeekUsedAmount(context){
|
||||||
axios.get(GalleryManageUrl + "/weekUsedAmount", {
|
return axios.get(GalleryManageUrl + "/weekUsedAmount", {
|
||||||
params: {
|
params: {
|
||||||
AuthCode: context.state.AuthCode
|
AuthCode: context.state.AuthCode
|
||||||
}
|
}
|
||||||
@@ -197,6 +212,9 @@ const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
_setConnectionStatus(state, status){
|
||||||
|
state.connectionStatus = status
|
||||||
|
},
|
||||||
_collectGallery(state, gid){
|
_collectGallery(state, gid){
|
||||||
let tasks = state.totalGalleryTask
|
let tasks = state.totalGalleryTask
|
||||||
for(let i=0; i< tasks.length; i++){
|
for(let i=0; i< tasks.length; i++){
|
||||||
@@ -354,17 +372,6 @@ const mutations = {
|
|||||||
deleteTask(tasks, 'gid', gid)
|
deleteTask(tasks, 'gid', gid)
|
||||||
},
|
},
|
||||||
_setChosenGallery(state,data){
|
_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
|
state.chosenGallery = data.gallery
|
||||||
},
|
},
|
||||||
_setCategory(state, category){
|
_setCategory(state, category){
|
||||||
@@ -403,7 +410,7 @@ const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
websocket: null, //websocket
|
connectionStatus: "disconnected", // live task updates
|
||||||
|
|
||||||
totalGalleryTask: [], //存放数据的数组
|
totalGalleryTask: [], //存放数据的数组
|
||||||
chosenGallery: false, //准备下载
|
chosenGallery: false, //准备下载
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user