Add automatic nearby discovery and simplify device presence UI

This commit is contained in:
2026-10-12 17:53:10 +09:00
parent 7e9bb1ec36
commit 5f431ee153
23 changed files with 1148 additions and 106 deletions

View File

@@ -1,3 +1,4 @@
import { readDeviceInfo } from './deviceInfo';
import { createRefreshQueue } from './refreshQueue';
import { ref } from 'vue';
import { io } from 'socket.io-client';
@@ -23,7 +24,22 @@ export const device = ref(null),
spaces = ref([]),
discoveryEnabled = ref(false),
error = ref('');
export const pendingPeers = ref([]);
let pendingTimer;
let deviceInfo = {};
export const nextCursor = ref(null);
export const locationSettings = ref({ enabled: false, radiusMeters: 100, refreshMs: 120000 });
export async function publishLocation(position) {
if (!socket?.connected) throw Error('서버 연결을 확인해 주세요.');
const result = await socket.timeout(5000).volatile.emitWithAck('location:update', position);
if (!result?.ok) throw Error(result?.message ?? '위치를 확인하지 못했습니다.');
}
export function clearLocation() {
if (socket?.connected) socket.emit('location:clear');
}
export function reserveLocationAcceptance(requestId) {
if (socket?.connected) socket.emit('location:accept', { requestId });
}
let socket;
let reauthorizing;
const drafts = new Map();
@@ -58,13 +74,14 @@ export async function bootstrap() {
if (navigator.locks) await navigator.locks.request('hop-session-bootstrap', init);
else await init();
await refreshAccount();
deviceInfo = await readDeviceInfo();
connect();
await refresh();
startReconciliation();
}
function connect() {
socket?.disconnect();
socket = io({ auth: { csrfToken: csrfToken.value }, reconnection: true });
socket = io({ auth: { csrfToken: csrfToken.value, deviceInfo }, reconnection: true });
socket.on('connect', () => {
connected.value = true;
void refresh().catch(showError);
@@ -91,7 +108,7 @@ function connect() {
csrfToken.value = session.csrfToken;
sessionExpiresAt.value = session.expiresAt;
if (socket) {
socket.auth = { csrfToken: csrfToken.value };
socket.auth = { csrfToken: csrfToken.value, deviceInfo };
socket.connect();
}
})()
@@ -100,10 +117,20 @@ function connect() {
reauthorizing = undefined;
});
});
socket.on('device:updated', (event) => {
const known = new Set(peers.value.map((peer) => peer.id));
pendingPeers.value = (event?.devices ?? [])
.filter((peer) => !known.has(peer.id))
.map((peer) => peer.id);
clearTimeout(pendingTimer);
pendingTimer = setTimeout(() => {
pendingPeers.value = [];
}, 5000);
void refresh().catch(showError);
});
for (const event of [
'device:online',
'device:offline',
'device:updated',
'space:updated',
'request:created',
'request:updated',
@@ -119,7 +146,15 @@ export const refresh = createRefreshQueue(async () => {
const snapshot = await api('/device/snapshot');
device.value = snapshot.device;
peers.value = snapshot.devices;
pendingPeers.value = pendingPeers.value.filter(
(id) => !snapshot.devices.some((peer) => peer.id === id),
);
discoveryEnabled.value = snapshot.sameNetworkDiscoveryEnabled;
locationSettings.value = snapshot.locationDiscovery ?? {
enabled: false,
radiusMeters: 100,
refreshMs: 120000,
};
requests.value = snapshot.requests;
items.value = snapshot.items;
nextCursor.value = snapshot.nextCursor;