import { promises as fsPromises } from 'fs' import { encryptMsg, decryptMsg, getSnapshotFilePath } from './utils' import { throttle } from '@common/utils' import { Server } from 'socket.io' import { sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' import { LIST_IDS } from '@common/constants' // type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull let io: Server | null let syncingId: string | null = null const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time)) const patchListData = (listData: Partial): LX.Sync.ListData => { return Object.assign({ defaultList: [], loveList: [], userList: [], }, listData) } const getRemoteListData = async(socket: LX.Sync.Socket): Promise => await new Promise((resolve, reject) => { console.log('getRemoteListData') const handleError = (reason: string) => { socket.removeListener('list:sync', handleSuccess) socket.removeListener('disconnect', handleError) reject(new Error(reason)) } const handleSuccess = (enData: string) => { socket.removeListener('disconnect', handleError) socket.removeListener('list:sync', handleSuccess) console.log('getRemoteListData', 'handleSuccess') const data: LX.Sync.Data | null = JSON.parse(decryptMsg(socket.data.keyInfo, enData)) if (!data) { reject(new Error('Get remote list data failed')) return } if (data.action != 'getData') return resolve(patchListData(data.data)) } socket.on('disconnect', handleError) socket.on('list:sync', handleSuccess) socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' }))) }) // const getAllLists = async() => { // const lists = [] // lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics).map(m => toOldMusicInfo(m)) }))) // lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics).map(m => toOldMusicInfo(m)) }))) // for await (const list of userLists) { // lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics).map(m => toOldMusicInfo(m)) }))) // } // return lists // } const getLocalListData = async(): Promise => { const lists: LX.Sync.ListData = { defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT), loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE), userList: [], } const userListInfos = await global.lx.worker.dbService.getAllUserList() for await (const list of userListInfos) { lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id) .then(musics => ({ ...list, list: musics }))) } return lists } const getSyncMode = async(keyInfo: LX.Sync.KeyInfo): Promise => await new Promise((resolve, reject) => { let removeListener = sendSelectMode(keyInfo, (mode) => { removeListener() resolve(mode) }) }) const finishedSync = (socket: LX.Sync.Socket) => { return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'finished', }))) } const setLocalList = (listData: LX.Sync.ListData) => { void global.lx.event_list.list_data_overwrite(listData, true) } const setRemotelList = async(socket: LX.Sync.Socket, listData: LX.Sync.ListData) => { if (!io) return const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets() for (const socket of sockets) { // if (excludeIds.includes(socket.data.keyInfo.clientId)) continue socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData }))) } } const writeFilePromises: Map> = new Map() const updateSnapshot = async(path: string, data: string) => { console.log('updateSnapshot', path) let writeFilePromise = writeFilePromises.get(path) ?? Promise.resolve() writeFilePromise = writeFilePromise.then(async() => { if (writeFilePromise !== writeFilePromises.get(path)) return await fsPromises.writeFile(path, data) }) writeFilePromises.set(path, writeFilePromise) await writeFilePromise.finally(() => { if (writeFilePromise !== writeFilePromises.get(path)) return writeFilePromises.delete(path) }) } type UserDataObj = Record const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => { const userListDataObj: UserDataObj = {} for (const list of listData.userList) userListDataObj[list.id] = list return userListDataObj } const handleMergeList = ( sourceList: LX.Music.MusicInfo[], targetList: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, ): LX.Music.MusicInfo[] => { let newList switch (addMusicLocationType) { case 'top': newList = [...targetList, ...sourceList] break case 'bottom': default: newList = [...sourceList, ...targetList] break } const map: Map = new Map() const ids: Array = [] switch (addMusicLocationType) { case 'top': newList = [...targetList, ...sourceList] for (let i = newList.length - 1; i > -1; i--) { const item = newList[i] if (map.has(item.id)) continue ids.unshift(item.id) map.set(item.id, item) } break case 'bottom': default: newList = [...sourceList, ...targetList] for (const item of newList) { if (map.has(item.id)) continue ids.push(item.id) map.set(item.id, item) } break } return ids.map(id => map.get(id)) as LX.Music.MusicInfo[] } const mergeList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync.ListData): LX.Sync.ListData => { const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType'] const newListData: LX.Sync.ListData = { defaultList: [], loveList: [], userList: [], } newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType) newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType) const userListDataObj = createUserListDataObj(sourceListData) newListData.userList = [...sourceListData.userList] targetListData.userList.forEach((list, index) => { const targetUpdateTime = list?.locationUpdateTime ?? 0 const sourceList = userListDataObj[list.id] if (sourceList) { sourceList.list = handleMergeList(sourceList.list, list.list, addMusicLocationType) const sourceUpdateTime = sourceList?.locationUpdateTime ?? 0 if (targetUpdateTime >= sourceUpdateTime) return // 调整位置 const [newList] = newListData.userList.splice(newListData.userList.findIndex(l => l.id == list.id), 1) newList.locationUpdateTime = targetUpdateTime newListData.userList.splice(index, 0, newList) } else { if (targetUpdateTime) { newListData.userList.splice(index, 0, list) } else { newListData.userList.push(list) } } }) return newListData } const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync.ListData): LX.Sync.ListData => { const newListData: LX.Sync.ListData = { defaultList: [], loveList: [], userList: [], } newListData.defaultList = sourceListData.defaultList newListData.loveList = sourceListData.loveList const userListDataObj = createUserListDataObj(sourceListData) newListData.userList = [...sourceListData.userList] targetListData.userList.forEach((list, index) => { if (userListDataObj[list.id]) return if (list?.locationUpdateTime) { newListData.userList.splice(index, 0, list) } else { newListData.userList.push(list) } }) return newListData } const handleMergeListData = async(socket: LX.Sync.Socket): Promise => { let isSelectingMode = false const handleDisconnect = () => { if (!isSelectingMode) return sendCloseSelectMode() } socket.on('disconnect', handleDisconnect) isSelectingMode = true const mode: LX.Sync.Mode = await getSyncMode(socket.data.keyInfo) isSelectingMode = false const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) console.log('handleMergeListData', 'remoteListData, localListData') let listData: LX.Sync.ListData switch (mode) { case 'merge_local_remote': listData = mergeList(localListData, remoteListData) break case 'merge_remote_local': listData = mergeList(remoteListData, localListData) break case 'overwrite_local_remote': listData = overwriteList(localListData, remoteListData) break case 'overwrite_remote_local': listData = overwriteList(remoteListData, localListData) break case 'overwrite_local_remote_full': listData = localListData break case 'overwrite_remote_local_full': listData = remoteListData break case 'none': return null case 'cancel': socket.disconnect(true) throw new Error('cancel') } return listData } const handleSyncList = async(socket: LX.Sync.Socket): Promise => { const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) console.log('handleSyncList', 'remoteListData, localListData') const listData: LX.Sync.ListData = { defaultList: [], loveList: [], userList: [], } if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) { if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { const mergedList = await handleMergeListData(socket) console.log('handleMergeListData', 'mergedList') // console.log(mergedList) if (!mergedList) return null listData.defaultList = mergedList.defaultList listData.loveList = mergedList.loveList listData.userList = mergedList.userList setLocalList(mergedList) void setRemotelList(socket, mergedList) } else { void setRemotelList(socket, localListData) listData.defaultList = localListData.defaultList listData.loveList = localListData.loveList listData.userList = localListData.userList } } else { if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { setLocalList(remoteListData) listData.defaultList = remoteListData.defaultList listData.loveList = remoteListData.loveList listData.userList = remoteListData.userList } else { listData.defaultList = localListData.defaultList listData.loveList = localListData.loveList listData.userList = localListData.userList } } return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({ defaultList: listData.defaultList, loveList: listData.loveList, userList: listData.userList, })).then(() => { socket.data.isCreatedSnapshot = true return listData }) } const mergeListDataFromSnapshot = ( sourceList: LX.Music.MusicInfo[], targetList: LX.Music.MusicInfo[], snapshotList: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, ): LX.Music.MusicInfo[] => { const removedListIds: Set = new Set() const sourceListItemIds: Set = new Set() const targetListItemIds: Set = new Set() for (const m of sourceList) sourceListItemIds.add(m.id) for (const m of targetList) targetListItemIds.add(m.id) if (snapshotList) { for (const m of snapshotList) { if (!sourceListItemIds.has(m.id) || !targetListItemIds.has(m.id)) removedListIds.add(m.id) } } let newList const map: Map = new Map() const ids = [] switch (addMusicLocationType) { case 'top': newList = [...targetList, ...sourceList] for (let i = newList.length - 1; i > -1; i--) { const item = newList[i] if (map.has(item.id) || removedListIds.has(item.id)) continue ids.unshift(item.id) map.set(item.id, item) } break case 'bottom': default: newList = [...sourceList, ...targetList] for (const item of newList) { if (map.has(item.id) || removedListIds.has(item.id)) continue ids.push(item.id) map.set(item.id, item) } break } return ids.map(id => map.get(id)) as LX.Music.MusicInfo[] } const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot: LX.Sync.ListData): Promise => { const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType'] const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) const newListData: LX.Sync.ListData = { defaultList: [], loveList: [], userList: [], } newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType) newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType) const localUserListData = createUserListDataObj(localListData) const remoteUserListData = createUserListDataObj(remoteListData) const snapshotUserListData = createUserListDataObj(snapshot) const removedListIds: Set = new Set() const localUserListIds: Set = new Set() const remoteUserListIds: Set = new Set() for (const l of localListData.userList) localUserListIds.add(l.id) for (const l of remoteListData.userList) remoteUserListIds.add(l.id) for (const l of snapshot.userList) { if (!localUserListIds.has(l.id) || !remoteUserListIds.has(l.id)) removedListIds.add(l.id) } let newUserList: LX.List.UserListInfoFull[] = [] for (const list of localListData.userList) { if (removedListIds.has(list.id)) continue const remoteList = remoteUserListData[list.id] let newList: LX.List.UserListInfoFull if (remoteList) { newList = { ...list, list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotUserListData[list.id].list, addMusicLocationType) } } else { newList = { ...list } } newUserList.push(newList) } remoteListData.userList.forEach((list, index) => { if (removedListIds.has(list.id)) return const remoteUpdateTime = list?.locationUpdateTime ?? 0 if (localUserListData[list.id]) { const localUpdateTime = localUserListData[list.id]?.locationUpdateTime ?? 0 if (localUpdateTime >= remoteUpdateTime) return // 调整位置 const [newList] = newUserList.splice(newUserList.findIndex(l => l.id == list.id), 1) newList.locationUpdateTime = localUpdateTime newUserList.splice(index, 0, newList) } else { if (remoteUpdateTime) { newUserList.splice(index, 0, { ...list }) } else { newUserList.push({ ...list }) } } }) newListData.userList = newUserList setLocalList(newListData) void setRemotelList(socket, newListData) return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({ defaultList: newListData.defaultList, loveList: newListData.loveList, userList: newListData.userList, })).then(() => { socket.data.isCreatedSnapshot = true return newListData }) } const registerUpdateSnapshotTask = (socket: LX.Sync.Socket, snapshot: LX.Sync.ListData) => { if (!socket.data.isCreatedSnapshot) return const handleUpdateSnapshot = throttle(() => { // TODO: 同步成功后再保存快照 void getLocalListData().then(({ defaultList, loveList, userList }) => { if (defaultList != null) snapshot.defaultList = defaultList if (loveList != null) snapshot.loveList = loveList if (userList != null) snapshot.userList = userList void updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot)) }) }, 2000) global.lx.event_list.on('list_changed', handleUpdateSnapshot) socket.on('disconnect', () => { global.lx.event_list.off('list_changed', handleUpdateSnapshot) }) } const syncList = async(socket: LX.Sync.Socket): Promise => { socket.data.snapshotFilePath = getSnapshotFilePath(socket.data.keyInfo) let fileData: any let isSyncRequired = false try { fileData = (await fsPromises.readFile(socket.data.snapshotFilePath)).toString() fileData = JSON.parse(fileData) } catch (error) { const err = error as NodeJS.ErrnoException if (err.code != 'ENOENT') throw err isSyncRequired = true } console.log('isSyncRequired', isSyncRequired) if (isSyncRequired) return await handleSyncList(socket) return await handleMergeListDataFromSnapshot(socket, patchListData(fileData)) } const checkSyncQueue = async(): Promise => { if (!syncingId) return console.log('sync queue...') await wait() await checkSyncQueue() } // export { // syncList = async(_io: Server, socket: LX.Sync.Socket) => { // io = _io // await checkSyncQueue() // syncingId = socket.data.keyInfo.clientId // return await syncList(socket).then(newListData => { // registerUpdateSnapshotTask(socket, { ...newListData }) // return finishedSync(socket) // }).finally(() => { // syncingId = null // }) // } // } const _syncList = async(_io: Server, socket: LX.Sync.Socket) => { io = _io await checkSyncQueue() syncingId = socket.data.keyInfo.clientId return await syncList(socket).then(newListData => { if (newListData) registerUpdateSnapshotTask(socket, { ...newListData }) return finishedSync(socket) }).finally(() => { syncingId = null }) } const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => { const filePath = getSnapshotFilePath(keyInfo) await fsPromises.unlink(filePath) } export { _syncList as syncList, removeSnapshot, }