mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-26 15:16:23 +02:00
ui: npm run format for new upstream formatting rules
This commit is contained in:
@@ -1,12 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Wifi, WifiOff, Copy, Check, Users, AlertCircle, Loader2, RefreshCw } from '@lucide/svelte';
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
Copy,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Users,
|
||||
Wifi,
|
||||
WifiOff
|
||||
} from '@lucide/svelte';
|
||||
import { SettingsGroup } from '$lib/components/app/settings';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { SettingsGroup } from '$lib/components/app/settings';
|
||||
import { webrtcStore } from '$lib/stores/webrtc.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
// -- host state
|
||||
let codeCopied = $state(false);
|
||||
@@ -46,10 +55,13 @@
|
||||
async function handleJoin() {
|
||||
joinError = '';
|
||||
const code = joinInput.trim().replace(/\s/g, '');
|
||||
|
||||
if (code.length < 40) {
|
||||
joinError = 'Code must be 40 characters';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
joining = true;
|
||||
try {
|
||||
await webrtcStore.joinAsClient(code);
|
||||
@@ -185,8 +197,8 @@
|
||||
<SettingsGroup title="Join">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Connect to a remote llama.cpp instance. All requests will be routed through the
|
||||
peer-to-peer tunnel.
|
||||
Connect to a remote llama.cpp instance. All requests will be routed through the peer-to-peer
|
||||
tunnel.
|
||||
</p>
|
||||
|
||||
{#if webrtcStore.mode === 'client'}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { ClientTunnel, HostTunnel, generatePassCode, generateRoomCode } from '$lib/utils/webrtc-tunnel';
|
||||
import {
|
||||
ClientTunnel,
|
||||
generatePassCode,
|
||||
generateRoomCode,
|
||||
HostTunnel
|
||||
} from '$lib/utils/webrtc-tunnel';
|
||||
|
||||
// Stores the generated host codes; persists until explicitly regenerated.
|
||||
const HOST_CODES_KEY = 'llama_webrtc_host_codes';
|
||||
@@ -31,10 +36,12 @@ class WebRTCStore {
|
||||
if (browser) {
|
||||
// Load persisted host codes so the UI can show them before host is enabled.
|
||||
const saved = this.readHostCodes();
|
||||
|
||||
if (saved) {
|
||||
this._roomCode = saved.roomCode;
|
||||
this._passCode = saved.passCode;
|
||||
}
|
||||
|
||||
this.restoreSession();
|
||||
}
|
||||
}
|
||||
@@ -76,7 +83,7 @@ class WebRTCStore {
|
||||
passCode = generatePassCode();
|
||||
this._roomCode = roomCode;
|
||||
this._passCode = passCode;
|
||||
this.writeHostCodes({ roomCode, passCode });
|
||||
this.writeHostCodes({ passCode, roomCode });
|
||||
}
|
||||
|
||||
await this.activateHost(roomCode, passCode);
|
||||
@@ -86,9 +93,10 @@ class WebRTCStore {
|
||||
async regenerateCodes(): Promise<void> {
|
||||
const roomCode = generateRoomCode();
|
||||
const passCode = generatePassCode();
|
||||
|
||||
this._roomCode = roomCode;
|
||||
this._passCode = passCode;
|
||||
this.writeHostCodes({ roomCode, passCode });
|
||||
this.writeHostCodes({ passCode, roomCode });
|
||||
|
||||
if (this.mode === 'host') {
|
||||
this.hostTunnel?.stop();
|
||||
@@ -113,7 +121,7 @@ class WebRTCStore {
|
||||
await tunnel.start();
|
||||
this.hostTunnel = tunnel;
|
||||
this.status = 'connected';
|
||||
this.writeSession({ mode: 'host', roomCode, passCode });
|
||||
this.writeSession({ mode: 'host', passCode, roomCode });
|
||||
} catch (e) {
|
||||
this.hostTunnel = null;
|
||||
this.status = 'error';
|
||||
@@ -141,6 +149,7 @@ class WebRTCStore {
|
||||
|
||||
const roomCode = shareCode.slice(0, 8);
|
||||
const passCode = shareCode.slice(8);
|
||||
|
||||
await this.activateClient(roomCode, passCode);
|
||||
}
|
||||
|
||||
@@ -165,9 +174,10 @@ class WebRTCStore {
|
||||
try {
|
||||
await tunnel.connect();
|
||||
this.clientTunnel = tunnel;
|
||||
this.writeSession({ mode: 'client', roomCode, passCode });
|
||||
this.writeSession({ mode: 'client', passCode, roomCode });
|
||||
// Release any requests that were queued while connecting.
|
||||
const waiters = this.connectionWaiters.splice(0);
|
||||
|
||||
for (const w of waiters) w.resolve();
|
||||
} catch (e) {
|
||||
this.clientTunnel = null;
|
||||
@@ -178,7 +188,9 @@ class WebRTCStore {
|
||||
// Reject queued requests.
|
||||
const waiters = this.connectionWaiters.splice(0);
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
|
||||
for (const w of waiters) w.reject(err);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -198,28 +210,28 @@ class WebRTCStore {
|
||||
|
||||
private installInterceptor(): void {
|
||||
if (this.originalFetch) return; // already installed
|
||||
|
||||
this.originalFetch = window.fetch.bind(window);
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
try {
|
||||
const url =
|
||||
input instanceof Request
|
||||
? input.url
|
||||
: input instanceof URL
|
||||
? input.href
|
||||
: String(input);
|
||||
input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
|
||||
const parsed = new URL(url, window.location.href);
|
||||
|
||||
if (parsed.origin === window.location.origin) {
|
||||
return this.tunnelFetch(input, init);
|
||||
}
|
||||
} catch {
|
||||
// not a parseable URL — fall through
|
||||
}
|
||||
|
||||
return this.originalFetch!(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
private uninstallInterceptor(): void {
|
||||
if (!this.originalFetch) return;
|
||||
|
||||
window.fetch = this.originalFetch;
|
||||
this.originalFetch = null;
|
||||
}
|
||||
@@ -233,12 +245,14 @@ class WebRTCStore {
|
||||
if (this.clientTunnel?.isConnected) {
|
||||
return this.clientTunnel.fetch(input, init);
|
||||
}
|
||||
|
||||
// If we are still connecting, queue the request until the tunnel opens.
|
||||
if (this.mode === 'client' && this.status === 'connecting') {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.connectionWaiters.push({ resolve, reject });
|
||||
this.connectionWaiters.push({ reject, resolve });
|
||||
}).then(() => this.clientTunnel!.fetch(input, init));
|
||||
}
|
||||
|
||||
throw new Error('tunnel not connected');
|
||||
}
|
||||
|
||||
@@ -249,6 +263,7 @@ class WebRTCStore {
|
||||
private readHostCodes(): HostCodes | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(HOST_CODES_KEY);
|
||||
|
||||
return raw ? (JSON.parse(raw) as HostCodes) : null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -262,8 +277,11 @@ class WebRTCStore {
|
||||
private restoreSession(): void {
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSION_KEY);
|
||||
|
||||
if (!raw) return;
|
||||
|
||||
const session = JSON.parse(raw) as SessionData;
|
||||
|
||||
if (session.mode === 'host') {
|
||||
void this.activateHost(session.roomCode, session.passCode);
|
||||
} else if (session.mode === 'client') {
|
||||
|
||||
@@ -12,30 +12,24 @@
|
||||
*/
|
||||
|
||||
const STUN_CONFIG: RTCConfiguration = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||||
]
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]
|
||||
};
|
||||
|
||||
const TRACKER_URLS = [
|
||||
'wss://tracker.openwebtorrent.com',
|
||||
'wss://tracker.btorrent.xyz'
|
||||
];
|
||||
|
||||
const TRACKER_URLS = ['wss://tracker.openwebtorrent.com', 'wss://tracker.btorrent.xyz'];
|
||||
const ICE_GATHER_TIMEOUT_MS = 10_000;
|
||||
const ANNOUNCE_INTERVAL_MS = 30_000;
|
||||
const CONNECT_TIMEOUT_MS = 30_000;
|
||||
const TRACKER_CONNECT_TIMEOUT_MS = 10_000;
|
||||
|
||||
// Characters that are unambiguous to read aloud or type
|
||||
const CODE_CHARS = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
|
||||
|
||||
function randomStr(len: number): string {
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
crypto.getRandomValues(bytes);
|
||||
let result = '';
|
||||
|
||||
for (const b of bytes) result += CODE_CHARS[b % CODE_CHARS.length];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -56,12 +50,15 @@ function waitForIceComplete(pc: RTCPeerConnection): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pc.iceGatheringState === 'complete') {
|
||||
resolve();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error('ICE gathering timed out')),
|
||||
ICE_GATHER_TIMEOUT_MS
|
||||
);
|
||||
|
||||
pc.addEventListener('icegatheringstatechange', () => {
|
||||
if (pc.iceGatheringState === 'complete') {
|
||||
clearTimeout(timer);
|
||||
@@ -94,6 +91,7 @@ class Tracker {
|
||||
connect(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
this.ws = ws;
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error('tracker connect timeout')),
|
||||
@@ -114,6 +112,7 @@ class Tracker {
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data as string) as TrackerMsg;
|
||||
|
||||
if (msg.offer && msg.peer_id && msg.offer_id) {
|
||||
this.onOffer?.(
|
||||
msg.peer_id as string,
|
||||
@@ -136,28 +135,32 @@ class Tracker {
|
||||
}
|
||||
}
|
||||
|
||||
announce(opts: {
|
||||
numwant?: number;
|
||||
offers?: Array<{ offer_id: string; offer: RTCSessionDescriptionInit }>;
|
||||
} = {}): void {
|
||||
announce(
|
||||
opts: {
|
||||
numwant?: number;
|
||||
offers?: Array<{ offer_id: string; offer: RTCSessionDescriptionInit }>;
|
||||
} = {}
|
||||
): void {
|
||||
const msg: TrackerMsg = {
|
||||
action: 'announce',
|
||||
info_hash: this.infoHash,
|
||||
peer_id: this.peerId,
|
||||
numwant: opts.numwant ?? 0
|
||||
numwant: opts.numwant ?? 0,
|
||||
peer_id: this.peerId
|
||||
};
|
||||
|
||||
if (opts.offers) msg.offers = opts.offers;
|
||||
|
||||
this.send(msg);
|
||||
}
|
||||
|
||||
sendAnswer(toPeerId: string, offerId: string, answer: RTCSessionDescriptionInit): void {
|
||||
this.send({
|
||||
action: 'announce',
|
||||
info_hash: this.infoHash,
|
||||
peer_id: this.peerId,
|
||||
to_peer_id: toPeerId,
|
||||
answer,
|
||||
offer_id: offerId
|
||||
info_hash: this.infoHash,
|
||||
offer_id: offerId,
|
||||
peer_id: this.peerId,
|
||||
to_peer_id: toPeerId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,7 +233,9 @@ const CHUNK_BYTES = 8192;
|
||||
|
||||
function uint8ToBase64(bytes: Uint8Array): string {
|
||||
let binary = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
@@ -289,11 +294,13 @@ export class HostTunnel {
|
||||
|
||||
private async connectOneTracker(url: string): Promise<void> {
|
||||
const tracker = new Tracker(this.infoHash, this.peerId);
|
||||
|
||||
tracker.onOffer = (fromPeerId, offerId, offer) => {
|
||||
void this.handleOffer(tracker, fromPeerId, offerId, offer);
|
||||
};
|
||||
tracker.onClose = () => {
|
||||
this.trackers = this.trackers.filter((t) => t !== tracker);
|
||||
|
||||
if (!this.stopped) {
|
||||
setTimeout(() => void this.connectOneTracker(url), 5000);
|
||||
}
|
||||
@@ -311,8 +318,10 @@ export class HostTunnel {
|
||||
): Promise<void> {
|
||||
try {
|
||||
const pc = new RTCPeerConnection(STUN_CONFIG);
|
||||
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await pc.createAnswer();
|
||||
|
||||
await pc.setLocalDescription(answer);
|
||||
await waitForIceComplete(pc);
|
||||
tracker.sendAnswer(fromPeerId, offerId, pc.localDescription!);
|
||||
@@ -331,6 +340,7 @@ export class HostTunnel {
|
||||
this.peers.delete(peerId);
|
||||
this.callbacks.onPeerCountChange?.(this.peers.size);
|
||||
}
|
||||
|
||||
pc.close();
|
||||
};
|
||||
|
||||
@@ -343,13 +353,14 @@ export class HostTunnel {
|
||||
if (msg.pass === this.passCode) {
|
||||
authenticated = true;
|
||||
channel.send(JSON.stringify({ type: 'auth_ok' } satisfies AuthOkMsg));
|
||||
this.peers.set(peerId, { pc, channel });
|
||||
this.peers.set(peerId, { channel, pc });
|
||||
this.callbacks.onPeerCountChange?.(this.peers.size);
|
||||
} else {
|
||||
channel.send(JSON.stringify({ type: 'auth_fail' } satisfies AuthFailMsg));
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -365,60 +376,67 @@ export class HostTunnel {
|
||||
}
|
||||
|
||||
private async handleRequest(channel: RTCDataChannel, msg: ReqMsg): Promise<void> {
|
||||
const { id, method, path, headers, body } = msg;
|
||||
const { body, headers, id, method, path } = msg;
|
||||
const t0 = performance.now();
|
||||
const ac = new AbortController();
|
||||
|
||||
this.activeRequests.set(id, ac);
|
||||
|
||||
try {
|
||||
const init: RequestInit = { method, headers, signal: ac.signal };
|
||||
const init: RequestInit = { headers, method, signal: ac.signal };
|
||||
|
||||
if (body !== null) init.body = base64ToUint8(body).buffer as ArrayBuffer;
|
||||
|
||||
const response = await fetch(path, init);
|
||||
|
||||
const resHeaders: Record<string, string> = {};
|
||||
|
||||
response.headers.forEach((v, k) => {
|
||||
resHeaders[k] = v;
|
||||
});
|
||||
|
||||
console.log(`[rtc] ${method} ${path} -> ${response.status} (${Math.round(performance.now() - t0)}ms)`);
|
||||
console.log(
|
||||
`[rtc] ${method} ${path} -> ${response.status} (${Math.round(performance.now() - t0)}ms)`
|
||||
);
|
||||
|
||||
channel.send(
|
||||
JSON.stringify({
|
||||
type: 'res_start',
|
||||
headers: resHeaders,
|
||||
id,
|
||||
status: response.status,
|
||||
headers: resHeaders
|
||||
type: 'res_start'
|
||||
} satisfies ResStartMsg)
|
||||
);
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
for (let i = 0; i < value.length; i += CHUNK_BYTES) {
|
||||
const slice = value.subarray(i, i + CHUNK_BYTES);
|
||||
|
||||
channel.send(
|
||||
JSON.stringify({
|
||||
type: 'res_chunk',
|
||||
data: uint8ToBase64(slice),
|
||||
id,
|
||||
data: uint8ToBase64(slice)
|
||||
type: 'res_chunk'
|
||||
} satisfies ResChunkMsg)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channel.send(JSON.stringify({ type: 'res_end', id } satisfies ResEndMsg));
|
||||
channel.send(JSON.stringify({ id, type: 'res_end' } satisfies ResEndMsg));
|
||||
} catch (e) {
|
||||
// AbortError means the client cancelled — no need to send an error back.
|
||||
if (!(e instanceof DOMException && e.name === 'AbortError')) {
|
||||
const errMsg = e instanceof Error ? e.message : String(e);
|
||||
|
||||
console.error(`[rtc] ${method} ${path} -> error: ${errMsg}`);
|
||||
channel.send(
|
||||
JSON.stringify({ type: 'res_err', id, message: errMsg } satisfies ResErrMsg)
|
||||
);
|
||||
channel.send(JSON.stringify({ id, message: errMsg, type: 'res_err' } satisfies ResErrMsg));
|
||||
}
|
||||
} finally {
|
||||
this.activeRequests.delete(id);
|
||||
@@ -427,9 +445,11 @@ export class HostTunnel {
|
||||
|
||||
stop(): void {
|
||||
this.stopped = true;
|
||||
|
||||
if (this.announceTimer) clearInterval(this.announceTimer);
|
||||
|
||||
for (const t of this.trackers) t.close();
|
||||
for (const { pc, channel } of this.peers.values()) {
|
||||
for (const { channel, pc } of this.peers.values()) {
|
||||
channel.close();
|
||||
pc.close();
|
||||
}
|
||||
@@ -480,31 +500,38 @@ export class ClientTunnel {
|
||||
|
||||
async connect(): Promise<void> {
|
||||
let lastError: Error = new Error('no trackers available');
|
||||
|
||||
for (const url of TRACKER_URLS) {
|
||||
try {
|
||||
await this.connectViaTracker(url);
|
||||
|
||||
return;
|
||||
} catch (e) {
|
||||
lastError = e instanceof Error ? e : new Error(String(e));
|
||||
this.cleanupConnection();
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
private async connectViaTracker(trackerUrl: string): Promise<void> {
|
||||
const offerId = randomStr(20);
|
||||
const pc = new RTCPeerConnection(STUN_CONFIG);
|
||||
|
||||
this.pc = pc;
|
||||
|
||||
const channel = pc.createDataChannel('tunnel', { ordered: true });
|
||||
|
||||
this.channel = channel;
|
||||
|
||||
const offer = await pc.createOffer();
|
||||
|
||||
await pc.setLocalDescription(offer);
|
||||
await waitForIceComplete(pc);
|
||||
|
||||
const tracker = new Tracker(this.infoHash, this.peerId);
|
||||
|
||||
this.tracker = tracker;
|
||||
await tracker.connect(trackerUrl);
|
||||
|
||||
@@ -515,6 +542,7 @@ export class ClientTunnel {
|
||||
|
||||
tracker.onAnswer = async (_offerId, answer) => {
|
||||
if (_offerId !== offerId) return;
|
||||
|
||||
try {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
} catch (e) {
|
||||
@@ -524,12 +552,13 @@ export class ClientTunnel {
|
||||
};
|
||||
|
||||
channel.onopen = () => {
|
||||
channel.send(JSON.stringify({ type: 'auth', pass: this.passCode } satisfies AuthMsg));
|
||||
channel.send(JSON.stringify({ pass: this.passCode, type: 'auth' } satisfies AuthMsg));
|
||||
};
|
||||
|
||||
channel.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data as string) as TunnelMsg;
|
||||
|
||||
if (msg.type === 'auth_ok') {
|
||||
clearTimeout(timer);
|
||||
this.callbacks.onConnected?.();
|
||||
@@ -557,7 +586,7 @@ export class ClientTunnel {
|
||||
|
||||
tracker.announce({
|
||||
numwant: 1,
|
||||
offers: [{ offer_id: offerId, offer: pc.localDescription! }]
|
||||
offers: [{ offer: pc.localDescription!, offer_id: offerId }]
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -570,7 +599,9 @@ export class ClientTunnel {
|
||||
msg.type !== 'res_err'
|
||||
)
|
||||
return;
|
||||
|
||||
const req = this.pending.get(msg.id);
|
||||
|
||||
if (!req) return;
|
||||
|
||||
if (msg.type === 'res_start') {
|
||||
@@ -616,26 +647,28 @@ export class ClientTunnel {
|
||||
// Extract path+query so the host fetches relative to its own origin
|
||||
const reqUrl = new URL(request.url);
|
||||
const path = reqUrl.pathname + reqUrl.search;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
request.headers.forEach((v, k) => {
|
||||
headers[k] = v;
|
||||
});
|
||||
|
||||
let bodyB64: string | null = null;
|
||||
|
||||
const bodyBytes = await request.arrayBuffer();
|
||||
|
||||
if (bodyBytes.byteLength > 0) {
|
||||
bodyB64 = uint8ToBase64(new Uint8Array(bodyBytes));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let streamController!: ReadableStreamDefaultController<Uint8Array>;
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(ctrl) {
|
||||
streamController = ctrl;
|
||||
}
|
||||
});
|
||||
|
||||
const abortHandler = () => {
|
||||
this.pending.delete(id);
|
||||
try {
|
||||
@@ -644,18 +677,16 @@ export class ClientTunnel {
|
||||
// stream may already be closed
|
||||
}
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
|
||||
// Tell the host to stop the in-flight fetch
|
||||
if (this.channel?.readyState === 'open') {
|
||||
this.channel.send(JSON.stringify({ type: 'cancel', id } satisfies CancelMsg));
|
||||
this.channel.send(JSON.stringify({ id, type: 'cancel' } satisfies CancelMsg));
|
||||
}
|
||||
};
|
||||
|
||||
signal?.addEventListener('abort', abortHandler, { once: true });
|
||||
|
||||
this.pending.set(id, {
|
||||
onStart: (status, resHeaders) => {
|
||||
resolve(new Response(stream, { status, headers: resHeaders }));
|
||||
},
|
||||
onChunk: (data) => {
|
||||
streamController.enqueue(base64ToUint8(data));
|
||||
},
|
||||
@@ -672,17 +703,20 @@ export class ClientTunnel {
|
||||
}
|
||||
reject(new Error(message));
|
||||
this.pending.delete(id);
|
||||
},
|
||||
onStart: (status, resHeaders) => {
|
||||
resolve(new Response(stream, { headers: resHeaders, status }));
|
||||
}
|
||||
});
|
||||
|
||||
this.channel!.send(
|
||||
JSON.stringify({
|
||||
type: 'req',
|
||||
body: bodyB64,
|
||||
headers,
|
||||
id,
|
||||
method: request.method,
|
||||
path,
|
||||
headers,
|
||||
body: bodyB64
|
||||
type: 'req'
|
||||
} satisfies ReqMsg)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -249,7 +249,6 @@
|
||||
$effect(() => {
|
||||
checkApiKey();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
Reference in New Issue
Block a user