Files
waste-go/web/src/store/index.ts

263 lines
7.3 KiB
TypeScript
Raw Normal View History

import { create } from 'zustand'
import type { PeerInfo, NetworkInfo, ChatMessage, FileEntry, IpcMessage, PeerConnState, CandidateType } from '../types'
export interface PeerStatus {
connState?: PeerConnState
candidateType?: CandidateType
remoteAddress?: string
lastSeen?: number
}
import { DaemonAdapter } from '../adapter/daemon'
import { BrowserAdapter } from '../adapter/browser'
type AnyAdapter = DaemonAdapter | BrowserAdapter
interface WasteState {
// connection
adapter: AnyAdapter | null
adapterMode: 'daemon' | 'browser' | null
daemonStatus: 'disconnected' | 'connecting' | 'connected'
// identity
masterAlias: string | null
masterId: string | null
localPeer: PeerInfo | null
// networks
networks: NetworkInfo[]
activeNetworkId: string | null
// peers
connectedPeers: PeerInfo[]
// chat — keyed by room
messages: Record<string, ChatMessage[]>
activeRoom: string
// file listings — keyed by peer id
fileLists: Record<string, FileEntry[]>
// per-peer connection status (browser mode) and last-seen timestamps
peerStatus: Record<string, PeerStatus>
// identity backup export result (cleared when consumed)
exportedBackup: string | null
// file browser state
activeFilePeer: string | null
// actions
connect: (url: string) => void
connectBrowser: () => void
disconnect: () => void
send: (msg: IpcMessage) => void
setActiveNetwork: (id: string) => void
setActiveRoom: (room: string) => void
setActiveFilePeer: (peerId: string | null) => void
setSharedFiles: (files: Map<string, File>) => void
browseFiles: (peerId: string) => void
sendFileTo: (peerId: string, file: File) => void
handleEvent: (msg: IpcMessage) => void
}
export const useWaste = create<WasteState>((set, get) => ({
adapter: null,
adapterMode: null,
daemonStatus: 'disconnected',
masterAlias: null,
masterId: null,
localPeer: null,
networks: [],
activeNetworkId: null,
connectedPeers: [],
messages: {},
activeRoom: 'general',
fileLists: {},
exportedBackup: null,
peerStatus: {},
activeFilePeer: null,
connect(url: string) {
const adapter = new DaemonAdapter(url)
adapter.onStatusChange = (s) => set({ daemonStatus: s })
adapter.on((msg) => get().handleEvent(msg))
adapter.connect()
set({ adapter, adapterMode: 'daemon' })
},
connectBrowser() {
const adapter = new BrowserAdapter()
adapter.onStatusChange = (s) => set({ daemonStatus: s })
adapter.on((msg) => get().handleEvent(msg))
adapter.connect()
set({ adapter, adapterMode: 'browser' })
},
disconnect() {
const a = get().adapter
if (a instanceof DaemonAdapter) a.disconnect()
else if (a instanceof BrowserAdapter) a.disconnect()
set({ adapter: null, adapterMode: null, daemonStatus: 'disconnected' })
},
send(msg) {
get().adapter?.send(msg)
},
setActiveNetwork(id) {
set({ activeNetworkId: id })
},
setActiveRoom(room) {
set({ activeRoom: room })
},
setActiveFilePeer(peerId) {
set({ activeFilePeer: peerId })
},
setSharedFiles(files) {
const a = get().adapter
if (a instanceof BrowserAdapter) a.setSharedFiles(files)
},
sendFileTo(peerId, file) {
const a = get().adapter
if (a instanceof BrowserAdapter) a.sendFileTo(peerId, file)
},
browseFiles(peerId) {
const a = get().adapter
if (a instanceof BrowserAdapter) {
a.requestBrowse(peerId)
} else {
get().send({ type: 'get_file_list', peer_id: peerId as unknown as import('../types').PeerID })
}
set({ activeFilePeer: peerId })
},
handleEvent(msg) {
switch (msg.type) {
case 'state_snapshot': {
const networks = msg.networks ?? []
set({
masterAlias: msg.master_alias ?? null,
masterId: msg.master_id ?? null,
localPeer: msg.local_peer ?? null,
networks,
connectedPeers: msg.connected_peers ?? [],
activeNetworkId: networks[0]?.network_id ?? null,
})
break
}
case 'network_joined': {
set(s => ({
localPeer: s.localPeer ?? msg.local_peer ?? null,
networks: s.networks.some(n => n.network_id === msg.network_id)
? s.networks
: [...s.networks, {
network_id: msg.network_id!,
network_name: msg.network_name!,
local_peer: msg.local_peer,
share_dir: msg.share_dir,
}],
activeNetworkId: s.activeNetworkId ?? msg.network_id!,
}))
break
}
case 'network_left': {
set(s => ({
networks: s.networks.filter(n => n.network_id !== msg.network_id),
activeNetworkId: s.activeNetworkId === msg.network_id
? (s.networks.find(n => n.network_id !== msg.network_id)?.network_id ?? null)
: s.activeNetworkId,
}))
break
}
case 'peer_connected': {
if (msg.peer) {
set(s => ({
connectedPeers: s.connectedPeers.some(p => p.id === msg.peer!.id)
? s.connectedPeers
: [...s.connectedPeers, msg.peer!],
}))
}
break
}
case 'session_ready': {
if (msg.peer_id && msg.nick) {
set(s => ({
connectedPeers: s.connectedPeers.map(p =>
p.id === msg.peer_id ? { ...p, alias: msg.nick! } : p
),
}))
}
break
}
case 'peer_disconnected': {
set(s => ({
connectedPeers: s.connectedPeers.filter(p => p.id !== msg.peer_id),
}))
break
}
case 'message_received': {
if (msg.message) {
const m = msg.message
const room = m.room
const fromId = String(m.from)
set(s => {
const existing = s.messages[room] ?? []
if (m.mid && existing.some(e => e.mid === m.mid)) return s
const prev = s.peerStatus[fromId] ?? {}
return {
messages: { ...s.messages, [room]: [...existing, m] },
peerStatus: { ...s.peerStatus, [fromId]: { ...prev, lastSeen: m.ts } },
}
})
}
break
}
case 'peer_status': {
const pid = String(msg.peer_id)
set(s => {
const prev = s.peerStatus[pid] ?? {}
return {
peerStatus: {
...s.peerStatus,
[pid]: {
...prev,
...(msg.conn_state ? { connState: msg.conn_state } : {}),
...(msg.candidate_type ? { candidateType: msg.candidate_type } : {}),
...(msg.remote_address !== undefined ? { remoteAddress: msg.remote_address } : {}),
},
},
}
})
break
}
case 'identity_exported': {
if (msg.backup) set({ exportedBackup: msg.backup })
break
}
case 'file_list': {
if (msg.peer_id && msg.files) {
set(s => ({
fileLists: { ...s.fileLists, [msg.peer_id!]: msg.files! },
}))
}
break
}
case 'file_complete': {
// path holds the blob URL, offer.name holds the filename
if (msg.path && msg.offer?.name) {
const a = document.createElement('a')
a.href = msg.path
a.download = msg.offer.name
a.click()
}
break
}
}
},
}))