Add standalone browser mode: no daemon required

BrowserAdapter speaks yaw/2.1 signaling + WebRTC protocol entirely in
the browser (libsodium-wrappers for Ed25519/X25519). Identity stored in
localStorage; peers connect via any waste/yaw2 anchor.

When served from a non-localhost origin the app defaults to browser
mode. On localhost the daemon adapter is tried first with a one-click
switch to browser mode. config.js (gitignored, served by anchor) lets
the host pre-inject signalURL and other defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-23 00:06:41 +02:00
parent add7c5fea8
commit 5bc16daae1
8 changed files with 820 additions and 35 deletions

View File

@@ -1,10 +1,14 @@
import { create } from 'zustand'
import type { PeerInfo, NetworkInfo, ChatMessage, FileEntry, IpcMessage } from '../types'
import { DaemonAdapter } from '../adapter/daemon'
import { BrowserAdapter } from '../adapter/browser'
type AnyAdapter = DaemonAdapter | BrowserAdapter
interface WasteState {
// connection
adapter: DaemonAdapter | null
adapter: AnyAdapter | null
adapterMode: 'daemon' | 'browser' | null
daemonStatus: 'disconnected' | 'connecting' | 'connected'
// identity
@@ -31,6 +35,7 @@ interface WasteState {
// actions
connect: (url: string) => void
connectBrowser: () => void
disconnect: () => void
send: (msg: IpcMessage) => void
setActiveNetwork: (id: string) => void
@@ -40,6 +45,7 @@ interface WasteState {
export const useWaste = create<WasteState>((set, get) => ({
adapter: null,
adapterMode: null,
daemonStatus: 'disconnected',
masterAlias: null,
masterId: null,
@@ -57,12 +63,22 @@ export const useWaste = create<WasteState>((set, get) => ({
adapter.onStatusChange = (s) => set({ daemonStatus: s })
adapter.on((msg) => get().handleEvent(msg))
adapter.connect()
set({ adapter })
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() {
get().adapter?.disconnect()
set({ adapter: null, daemonStatus: 'disconnected' })
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) {
@@ -93,11 +109,13 @@ export const useWaste = create<WasteState>((set, get) => ({
}
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!,