Organize uploader storage and restore transfer readiness
This commit is contained in:
@@ -450,6 +450,7 @@ async function logout() {
|
||||
await api('/auth/logout', 'POST', {});
|
||||
clearPrivate();
|
||||
await refreshAccount();
|
||||
emit('devices');
|
||||
reconnect();
|
||||
await refresh();
|
||||
});
|
||||
|
||||
@@ -66,6 +66,9 @@ import {
|
||||
spaces,
|
||||
error,
|
||||
transferCooldown,
|
||||
transferConnecting,
|
||||
waitForConnection,
|
||||
requestTransfer,
|
||||
bootstrap,
|
||||
api,
|
||||
refresh,
|
||||
@@ -168,7 +171,7 @@ const receiving = computed(() =>
|
||||
r.transferStatus !== 'failed',
|
||||
),
|
||||
);
|
||||
createNearbySearch({
|
||||
const nearbySearch = createNearbySearch({
|
||||
connected,
|
||||
device,
|
||||
spaces,
|
||||
@@ -176,6 +179,24 @@ createNearbySearch({
|
||||
publish: publishLocation,
|
||||
clear: clearLocation,
|
||||
});
|
||||
const preparingTransfer = ref(false);
|
||||
async function prepareTransfer(target) {
|
||||
preparingTransfer.value = true;
|
||||
try {
|
||||
await waitForConnection();
|
||||
if (
|
||||
target.sources?.includes('nearby') &&
|
||||
!target.sources.some((source) => ['network', 'space'].includes(source))
|
||||
) {
|
||||
if (!(await nearbySearch.ensureReady()))
|
||||
throw Error(
|
||||
'기기 연결을 복구하지 못했어요. 다시 시도하거나 다른 기기 연결을 이용해 주세요.',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
preparingTransfer.value = false;
|
||||
}
|
||||
}
|
||||
const receiver = createDeliveryReceiver({ download, markRead, onError: showError });
|
||||
const blockedLinks = receiver.blockedLinks;
|
||||
const receivedTexts = receiver.receivedTexts;
|
||||
@@ -509,12 +530,9 @@ async function forwardVault(item) {
|
||||
}
|
||||
const owner = account.value?.id;
|
||||
await act(async () => {
|
||||
await api(
|
||||
`/vault/${item.accessId}/send`,
|
||||
'POST',
|
||||
{ targetDeviceId: selected.value.id },
|
||||
crypto.randomUUID(),
|
||||
);
|
||||
const target = selected.value;
|
||||
await prepareTransfer(target);
|
||||
await requestTransfer(`/vault/${item.accessId}/send`, { targetDeviceId: target.id });
|
||||
if (account.value?.id !== owner) return;
|
||||
forwardedItem.value = item;
|
||||
sendOpen.value = true;
|
||||
@@ -629,10 +647,12 @@ function selectPeer(peer) {
|
||||
async function submitClipboard(content) {
|
||||
await act(async () => {
|
||||
if (!selected.value) throw Error('받을 기기를 선택해 주세요.');
|
||||
const target = selected.value;
|
||||
await prepareTransfer(target);
|
||||
forwardedItem.value = null;
|
||||
itemToSend.value = null;
|
||||
await send(
|
||||
selected.value.id,
|
||||
target.id,
|
||||
content.kind,
|
||||
content.kind === 'text' ? { text: content.text } : { urls: content.urls },
|
||||
);
|
||||
@@ -657,11 +677,15 @@ async function closeReceivedText(item) {
|
||||
async function submitFiles(files) {
|
||||
await act(async () => {
|
||||
if (!selected.value) throw Error('받을 기기를 선택해 주세요.');
|
||||
const target = selected.value;
|
||||
forwardedItem.value = null;
|
||||
itemToSend.value = null;
|
||||
if (currentTransfer.value?.kind === 'file' && currentTransfer.value.status === 'accepted')
|
||||
await retryFiles(currentTransfer.value.id, files);
|
||||
else await sendFiles(selected.value.id, files);
|
||||
else {
|
||||
await prepareTransfer(target);
|
||||
await sendFiles(target.id, files);
|
||||
}
|
||||
});
|
||||
}
|
||||
async function createSpace() {
|
||||
@@ -1195,6 +1219,7 @@ async function spaceAction(space, action, memberId) {
|
||||
:clipboard-key="selectionEpoch"
|
||||
:read-clipboard-on-selection="true"
|
||||
:busy="busy"
|
||||
:preparing="preparingTransfer || transferConnecting"
|
||||
:retry-seconds="retrySeconds"
|
||||
:transfer="currentTransfer"
|
||||
:progress="
|
||||
|
||||
@@ -19,6 +19,7 @@ const props = defineProps({
|
||||
targetKey: String,
|
||||
clipboardKey: Number,
|
||||
busy: Boolean,
|
||||
preparing: Boolean,
|
||||
retrySeconds: { type: Number, default: 0 },
|
||||
disabled: Boolean,
|
||||
transfer: Object,
|
||||
@@ -521,15 +522,17 @@ function size(value) {
|
||||
@click="submit"
|
||||
>
|
||||
{{
|
||||
!transfer && retrySeconds > 0
|
||||
? `${retrySeconds}초 후 전송 가능`
|
||||
: retryable
|
||||
? '다시 전송'
|
||||
: transfer
|
||||
? '전송 취소'
|
||||
: saveMode
|
||||
? '저장하기'
|
||||
: '전송하기'
|
||||
preparing
|
||||
? '연결 확인 중'
|
||||
: !transfer && retrySeconds > 0
|
||||
? `${retrySeconds}초 후 전송 가능`
|
||||
: retryable
|
||||
? '다시 전송'
|
||||
: transfer
|
||||
? '전송 취소'
|
||||
: saveMode
|
||||
? '저장하기'
|
||||
: '전송하기'
|
||||
}}<X v-if="transfer && !retryable" :size="16" /><ArrowUpRight v-else :size="16" />
|
||||
</button>
|
||||
<div v-if="retryable" class="content-switches">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readDeviceInfo } from './deviceInfo';
|
||||
import { PASSWORD_MIN_LENGTH } from '@hop/shared';
|
||||
import { createRefreshQueue } from './refreshQueue';
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { io } from 'socket.io-client';
|
||||
export const account = ref(null),
|
||||
registrationOpen = ref(false),
|
||||
@@ -27,6 +27,7 @@ export const device = ref(null),
|
||||
error = ref('');
|
||||
export const pendingPeers = ref([]);
|
||||
export const transferCooldown = ref({ until: 0, targetDeviceId: null });
|
||||
export const transferConnecting = ref(false);
|
||||
let pendingTimer;
|
||||
let deviceInfo = {};
|
||||
export const nextCursor = ref(null);
|
||||
@@ -46,6 +47,46 @@ export function reserveLocationAcceptance(requestId) {
|
||||
}
|
||||
let socket;
|
||||
let reauthorizing;
|
||||
export async function waitForConnection() {
|
||||
if (socket?.connected) return;
|
||||
if (!socket) throw Error('서버 연결을 확인해 주세요.');
|
||||
await new Promise((resolve, reject) => {
|
||||
const stop = watch(connected, (ready) => {
|
||||
if (!ready) return;
|
||||
clearTimeout(timer);
|
||||
stop();
|
||||
resolve();
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
stop();
|
||||
reject(Error('서버에 다시 연결하지 못했어요. 잠시 후 다시 시도해 주세요.'));
|
||||
}, 5000);
|
||||
socket.connect();
|
||||
});
|
||||
}
|
||||
export async function requestTransfer(path, input) {
|
||||
const key = crypto.randomUUID(),
|
||||
owner = device.value?.id;
|
||||
try {
|
||||
transferConnecting.value = !socket?.connected;
|
||||
await waitForConnection();
|
||||
const deadline = Date.now() + 5000;
|
||||
while (true) {
|
||||
if (device.value?.id !== owner)
|
||||
throw Error('기기 세션이 변경되었습니다. 다시 선택해 주세요.');
|
||||
try {
|
||||
return await api(path, 'POST', input, key);
|
||||
} catch (error) {
|
||||
// Only a known, still-authorized peer in its reconnect grace period is retried.
|
||||
if (error.code !== 'TARGET_RECONNECTING' || Date.now() >= deadline) throw error;
|
||||
transferConnecting.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
transferConnecting.value = false;
|
||||
}
|
||||
}
|
||||
const drafts = new Map();
|
||||
const completing = new Set();
|
||||
export async function api(path, method = 'GET', body, key, signal) {
|
||||
@@ -222,17 +263,12 @@ export const refresh = createRefreshQueue(async () => {
|
||||
});
|
||||
export async function send(targetDeviceId, kind, values) {
|
||||
const content = kind === 'text' ? values.text : values.urls.join('\n');
|
||||
const { requestId } = await api(
|
||||
'/transfer-requests',
|
||||
'POST',
|
||||
{
|
||||
targetDeviceId,
|
||||
kind,
|
||||
byteSize: new TextEncoder().encode(content).length,
|
||||
count: kind === 'text' ? 1 : values.urls.length,
|
||||
},
|
||||
crypto.randomUUID(),
|
||||
);
|
||||
const { requestId } = await requestTransfer('/transfer-requests', {
|
||||
targetDeviceId,
|
||||
kind,
|
||||
byteSize: new TextEncoder().encode(content).length,
|
||||
count: kind === 'text' ? 1 : values.urls.length,
|
||||
});
|
||||
drafts.set(requestId, { ...values, key: crypto.randomUUID() });
|
||||
await refresh();
|
||||
return requestId;
|
||||
@@ -317,7 +353,7 @@ export async function sendFiles(targetDeviceId, files) {
|
||||
type: f.type || 'application/octet-stream',
|
||||
})),
|
||||
};
|
||||
const { requestId } = await api('/transfer-requests', 'POST', input, crypto.randomUUID());
|
||||
const { requestId } = await requestTransfer('/transfer-requests', input);
|
||||
fileDrafts.set(requestId, files);
|
||||
await refresh();
|
||||
return requestId;
|
||||
|
||||
@@ -9,6 +9,7 @@ export function createNearbySearch({ connected, device, spaces, settings, publis
|
||||
timer,
|
||||
permission,
|
||||
permissionQuery,
|
||||
locationRequest,
|
||||
disposed = false,
|
||||
blocked = false;
|
||||
const ready = () =>
|
||||
@@ -47,8 +48,13 @@ export function createNearbySearch({ connected, device, spaces, settings, publis
|
||||
});
|
||||
return permissionQuery;
|
||||
}
|
||||
async function locate() {
|
||||
if (!ready() || blocked || locating.value || disposed) return;
|
||||
function locate() {
|
||||
if (!ready() || blocked || disposed) return;
|
||||
if (locating.value) return locationRequest;
|
||||
locationRequest = readLocation();
|
||||
return locationRequest;
|
||||
}
|
||||
async function readLocation() {
|
||||
const current = ++revision;
|
||||
locating.value = true;
|
||||
clearTimeout(timer);
|
||||
@@ -84,7 +90,8 @@ export function createNearbySearch({ connected, device, spaces, settings, publis
|
||||
}
|
||||
}
|
||||
async function resume() {
|
||||
if (!ready() || blocked || active.value || locating.value || disposed) return;
|
||||
if (!ready() || blocked || active.value || disposed) return;
|
||||
if (locating.value) return locationRequest;
|
||||
const current = revision;
|
||||
const state = await readPermission();
|
||||
if (disposed || current !== revision || !ready()) return;
|
||||
@@ -92,7 +99,7 @@ export function createNearbySearch({ connected, device, spaces, settings, publis
|
||||
blocked = true;
|
||||
return;
|
||||
}
|
||||
void locate();
|
||||
return locate();
|
||||
}
|
||||
function visibility() {
|
||||
visible = document.visibilityState === 'visible';
|
||||
@@ -119,5 +126,9 @@ export function createNearbySearch({ connected, device, spaces, settings, publis
|
||||
document.removeEventListener('visibilitychange', visibility);
|
||||
if (permission) permission.onchange = null;
|
||||
});
|
||||
return { active, locating };
|
||||
async function ensureReady() {
|
||||
await resume();
|
||||
return ready() && active.value && !blocked && !disposed;
|
||||
}
|
||||
return { active, locating, ensureReady };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user