uncloud-p2p 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/example/index.html +0 -59
- package/example/main.ts +0 -64
- package/src/files.ts +0 -44
- package/src/index.ts +0 -351
- package/src/uncloudproto.ts +0 -69
- package/tsconfig.json +0 -16
package/package.json
CHANGED
package/example/index.html
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Uncloud P2P Demo</title>
|
|
7
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
-
</head>
|
|
9
|
-
<body class="bg-zinc-950 text-zinc-100 font-sans min-h-screen p-12">
|
|
10
|
-
<div
|
|
11
|
-
class="max-w-2xl mx-auto border border-zinc-800 bg-zinc-900 p-8 rounded-xl"
|
|
12
|
-
>
|
|
13
|
-
<h1 class="text-2xl font-black uppercase tracking-widest mb-6">
|
|
14
|
-
P2P Node Connection
|
|
15
|
-
</h1>
|
|
16
|
-
|
|
17
|
-
<div class="flex gap-2 mb-8">
|
|
18
|
-
<input
|
|
19
|
-
id="nodeId"
|
|
20
|
-
type="text"
|
|
21
|
-
placeholder="Enter Node ID"
|
|
22
|
-
class="bg-black border border-zinc-800 px-4 py-2 rounded flex-1 font-mono text-sm focus:border-yellow-400 outline-none"
|
|
23
|
-
/>
|
|
24
|
-
<button
|
|
25
|
-
id="connectBtn"
|
|
26
|
-
class="bg-zinc-100 text-black px-6 py-2 rounded font-bold uppercase text-xs hover:bg-yellow-400 transition-colors"
|
|
27
|
-
>
|
|
28
|
-
Connect
|
|
29
|
-
</button>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<div class="space-y-4">
|
|
33
|
-
<div
|
|
34
|
-
class="flex justify-between items-center border-b border-zinc-800 pb-2"
|
|
35
|
-
>
|
|
36
|
-
<span class="text-[10px] font-mono text-zinc-500 uppercase"
|
|
37
|
-
>Status</span
|
|
38
|
-
>
|
|
39
|
-
<span
|
|
40
|
-
id="status"
|
|
41
|
-
class="text-xs font-black uppercase tracking-tighter text-zinc-600"
|
|
42
|
-
>Idle</span
|
|
43
|
-
>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div
|
|
47
|
-
id="logs"
|
|
48
|
-
class="bg-black p-4 h-48 overflow-y-auto font-mono text-[11px] text-zinc-400 space-y-1 rounded border border-zinc-800"
|
|
49
|
-
>
|
|
50
|
-
<div class="text-zinc-600 italic">
|
|
51
|
-
// Connection logs will appear here...
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<script type="module" src="./main.ts"></script>
|
|
58
|
-
</body>
|
|
59
|
-
</html>
|
package/example/main.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { UncloudP2P, ConnectionState } from "../src/index";
|
|
2
|
-
import { Actions } from "../src/uncloudproto";
|
|
3
|
-
import { ListFiles, LoadFileText } from "../src/files";
|
|
4
|
-
|
|
5
|
-
const statusEl = document.getElementById("status")!;
|
|
6
|
-
const logsEl = document.getElementById("logs")!;
|
|
7
|
-
const connectBtn = document.getElementById("connectBtn") as HTMLButtonElement;
|
|
8
|
-
const nodeIdInput = document.getElementById("nodeId") as HTMLInputElement;
|
|
9
|
-
|
|
10
|
-
function log(msg: string, type: string = "info") {
|
|
11
|
-
const div = document.createElement("div");
|
|
12
|
-
const colors = {
|
|
13
|
-
info: "text-zinc-400",
|
|
14
|
-
success: "text-emerald-400",
|
|
15
|
-
error: "text-red-500",
|
|
16
|
-
warn: "text-yellow-400",
|
|
17
|
-
};
|
|
18
|
-
div.className = colors[type as keyof typeof colors];
|
|
19
|
-
div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
|
20
|
-
logsEl.appendChild(div);
|
|
21
|
-
logsEl.scrollTop = logsEl.scrollHeight;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const updateStatusUI = (state: ConnectionState) => {
|
|
25
|
-
statusEl.textContent = state;
|
|
26
|
-
const colors: Record<string, string> = {
|
|
27
|
-
connected: "text-emerald-500",
|
|
28
|
-
error: "text-red-500",
|
|
29
|
-
connecting: "text-yellow-400",
|
|
30
|
-
idle: "text-zinc-600",
|
|
31
|
-
};
|
|
32
|
-
statusEl.className = `text-xs font-black uppercase tracking-tighter ${colors[state] || "text-zinc-400"}`;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
connectBtn.onclick = async () => {
|
|
36
|
-
const nodeId = nodeIdInput.value.trim();
|
|
37
|
-
if (!nodeId) return alert("Please enter a Node ID");
|
|
38
|
-
|
|
39
|
-
const p2p = new UncloudP2P({
|
|
40
|
-
baseUrl: "http://localhost:3001",
|
|
41
|
-
nodeId: nodeId,
|
|
42
|
-
onStateChange: updateStatusUI,
|
|
43
|
-
onLog: (msg, type) => log(msg, type),
|
|
44
|
-
onMessage: (data) => log(`Message: ${data}`, "success"),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
log(`Initializing connection to ${nodeId}...`);
|
|
48
|
-
await p2p.connect();
|
|
49
|
-
await p2p.waitForOpen();
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const files = await ListFiles(p2p);
|
|
53
|
-
|
|
54
|
-
files.forEach((file) => {
|
|
55
|
-
log(`${file.Name} (${(file.SizeBytes / 1024).toFixed(2)} KB)`, "info");
|
|
56
|
-
});
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.error("Command failed: ", err);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const text = await LoadFileText(p2p, "test/dst/file.txt");
|
|
62
|
-
|
|
63
|
-
log(text, "info");
|
|
64
|
-
};
|
package/src/files.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// util functions that wrap the core of uncloudp2p
|
|
2
|
-
|
|
3
|
-
import { Actions, FileMetadata } from "./uncloudproto";
|
|
4
|
-
import { UncloudP2P } from "./index";
|
|
5
|
-
|
|
6
|
-
export interface NodeFile {
|
|
7
|
-
MD5: string;
|
|
8
|
-
MimeType: string;
|
|
9
|
-
Name: string;
|
|
10
|
-
SizeBytes: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function ListFiles(p2p: UncloudP2P): Promise<NodeFile[]> {
|
|
14
|
-
try {
|
|
15
|
-
const resp = await p2p.sendCommand<NodeFile[]>(
|
|
16
|
-
Actions.CLIENT_TO_NODE_LIST_FILES,
|
|
17
|
-
null,
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
return resp;
|
|
21
|
-
} catch (err) {
|
|
22
|
-
throw new Error(`failed to retreive file list from node: ${err}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function LoadFileText(
|
|
27
|
-
p2p: UncloudP2P,
|
|
28
|
-
file: string,
|
|
29
|
-
): Promise<string> {
|
|
30
|
-
const blob = await p2p.downloadToMemory(file);
|
|
31
|
-
return await blob.text();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function LoadFileBytes(
|
|
35
|
-
p2p: UncloudP2P,
|
|
36
|
-
file: string,
|
|
37
|
-
): Promise<Uint8Array<ArrayBuffer>> {
|
|
38
|
-
const blob = await p2p.downloadToMemory(file);
|
|
39
|
-
return await blob.bytes();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function LoadFile(p2p: UncloudP2P, file: string): Promise<Blob> {
|
|
43
|
-
return await p2p.downloadToMemory(file);
|
|
44
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
StreamNames,
|
|
3
|
-
newMessageJSON,
|
|
4
|
-
Actions,
|
|
5
|
-
PendingRequest,
|
|
6
|
-
Action,
|
|
7
|
-
newMessage,
|
|
8
|
-
} from "./uncloudproto";
|
|
9
|
-
|
|
10
|
-
export interface IceCandidate {
|
|
11
|
-
candidate: string;
|
|
12
|
-
sdpMid: string | null;
|
|
13
|
-
sdpMLineIndex: number | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface IceExchangePayload {
|
|
17
|
-
sdp: string;
|
|
18
|
-
candidates: IceCandidate[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type ConnectionState =
|
|
22
|
-
| "idle"
|
|
23
|
-
| "gathering"
|
|
24
|
-
| "signaling"
|
|
25
|
-
| "connecting"
|
|
26
|
-
| "connected"
|
|
27
|
-
| "disconnected"
|
|
28
|
-
| "error";
|
|
29
|
-
|
|
30
|
-
interface P2POptions {
|
|
31
|
-
nodeId: string;
|
|
32
|
-
baseUrl?: string; // Allow setting the API base URL
|
|
33
|
-
iceServers?: RTCIceServer[];
|
|
34
|
-
onStateChange?: (state: ConnectionState) => void;
|
|
35
|
-
onMessage?: (data: string) => void;
|
|
36
|
-
onLog?: (
|
|
37
|
-
message: string,
|
|
38
|
-
type?: "info" | "error" | "success" | "warn",
|
|
39
|
-
) => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class UncloudP2P {
|
|
43
|
-
private pc: RTCPeerConnection | null = null;
|
|
44
|
-
private dc: RTCDataChannel | null = null;
|
|
45
|
-
private localCandidates: IceCandidate[] = [];
|
|
46
|
-
private options: P2POptions;
|
|
47
|
-
private pendingRequests: Map<string, PendingRequest> = new Map();
|
|
48
|
-
private pendingDownloads: Map<
|
|
49
|
-
string,
|
|
50
|
-
{ resolve: (blob: Blob) => void; reject: (err: any) => void }
|
|
51
|
-
> = new Map();
|
|
52
|
-
|
|
53
|
-
constructor(options: P2POptions) {
|
|
54
|
-
this.options = {
|
|
55
|
-
baseUrl: "https://uncloud.mpsoftware.io",
|
|
56
|
-
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
|
57
|
-
...options,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private log(
|
|
62
|
-
msg: string,
|
|
63
|
-
type: "info" | "error" | "success" | "warn" = "info",
|
|
64
|
-
) {
|
|
65
|
-
this.options.onLog?.(msg, type);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Initializes the PeerConnection and starts the ICE gathering process.
|
|
70
|
-
*/
|
|
71
|
-
public async connect(): Promise<void> {
|
|
72
|
-
try {
|
|
73
|
-
this.updateState("gathering");
|
|
74
|
-
|
|
75
|
-
this.pc = new RTCPeerConnection({
|
|
76
|
-
iceServers: this.options.iceServers,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Create the persistent CONTROL_STREAM immediately (Offer side)
|
|
80
|
-
this.dc = this.pc.createDataChannel(StreamNames.CONTROL_STREAM, {
|
|
81
|
-
ordered: true,
|
|
82
|
-
});
|
|
83
|
-
this.setupControlChannelHooks(this.dc);
|
|
84
|
-
|
|
85
|
-
// ICE Gathering Logic
|
|
86
|
-
this.pc.onicecandidate = async (e) => {
|
|
87
|
-
if (!e.candidate) {
|
|
88
|
-
this.log("ICE gathering complete, performing exchange...", "info");
|
|
89
|
-
await this.performExchange();
|
|
90
|
-
} else {
|
|
91
|
-
this.localCandidates.push({
|
|
92
|
-
candidate: e.candidate.candidate,
|
|
93
|
-
sdpMid: e.candidate.sdpMid,
|
|
94
|
-
sdpMLineIndex: e.candidate.sdpMLineIndex,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// Incoming data handler
|
|
100
|
-
this.pc.ondatachannel = (event) => {
|
|
101
|
-
const channel = event.channel;
|
|
102
|
-
if (channel.label === StreamNames.NODE_TO_CLIENT_FILE_TRANSFER) {
|
|
103
|
-
this.log("Inbound file stream detected", "info");
|
|
104
|
-
this.setupDownloadChannelHooks(channel);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const offer = await this.pc.createOffer();
|
|
109
|
-
await this.pc.setLocalDescription(offer);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
this.handleError(err);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Hits the exchange endpoint to swap SDP and ICE candidates.
|
|
117
|
-
*/
|
|
118
|
-
private async performExchange() {
|
|
119
|
-
if (!this.pc || !this.pc.localDescription) return;
|
|
120
|
-
|
|
121
|
-
this.updateState("signaling");
|
|
122
|
-
|
|
123
|
-
const payload: IceExchangePayload = {
|
|
124
|
-
sdp: this.pc.localDescription.sdp,
|
|
125
|
-
candidates: this.localCandidates,
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const endpoint = `${this.options.baseUrl}/api/public/node/${this.options.nodeId}/exchangeCandidates`;
|
|
130
|
-
|
|
131
|
-
const response = await fetch(endpoint, {
|
|
132
|
-
method: "POST",
|
|
133
|
-
headers: { "Content-Type": "application/json" },
|
|
134
|
-
body: JSON.stringify(payload),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
if (!response.ok)
|
|
138
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
139
|
-
|
|
140
|
-
const nodeResponse: IceExchangePayload = await response.json();
|
|
141
|
-
|
|
142
|
-
await this.pc.setRemoteDescription(
|
|
143
|
-
new RTCSessionDescription({ type: "answer", sdp: nodeResponse.sdp }),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
for (const cand of nodeResponse.candidates) {
|
|
147
|
-
await this.pc.addIceCandidate(cand);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.updateState("connecting");
|
|
151
|
-
} catch (err) {
|
|
152
|
-
this.handleError(err);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
public async sendCommand<T = any>(
|
|
157
|
-
action: Action,
|
|
158
|
-
body: any,
|
|
159
|
-
timeoutMs: number = 5000,
|
|
160
|
-
): Promise<T> {
|
|
161
|
-
if (!this.dc || this.dc.readyState !== "open") {
|
|
162
|
-
throw new Error("P2P Control channel is not open");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const message = newMessage(action, body);
|
|
166
|
-
const correlationId = message.correlationId;
|
|
167
|
-
|
|
168
|
-
return new Promise((resolve, reject) => {
|
|
169
|
-
// 1. Set up the timeout to prevent hanging promises
|
|
170
|
-
const timeout = window.setTimeout(() => {
|
|
171
|
-
if (this.pendingRequests.has(correlationId)) {
|
|
172
|
-
this.pendingRequests.delete(correlationId);
|
|
173
|
-
reject(new Error(`Command ${action} timed out after ${timeoutMs}ms`));
|
|
174
|
-
}
|
|
175
|
-
}, timeoutMs);
|
|
176
|
-
|
|
177
|
-
// 2. Store the callbacks
|
|
178
|
-
this.pendingRequests.set(correlationId, { resolve, reject, timeout });
|
|
179
|
-
|
|
180
|
-
this.dc!.send(message.toJSON());
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private setupControlChannelHooks(channel: RTCDataChannel) {
|
|
185
|
-
channel.onopen = () => this.updateState("connected");
|
|
186
|
-
channel.onclose = () => this.updateState("disconnected");
|
|
187
|
-
|
|
188
|
-
channel.onmessage = (e) => {
|
|
189
|
-
try {
|
|
190
|
-
const response = JSON.parse(e.data);
|
|
191
|
-
|
|
192
|
-
// Check if this is a response to a pending command
|
|
193
|
-
if (
|
|
194
|
-
response.correlationId &&
|
|
195
|
-
this.pendingRequests.has(response.correlationId)
|
|
196
|
-
) {
|
|
197
|
-
const pending = this.pendingRequests.get(response.correlationId)!;
|
|
198
|
-
|
|
199
|
-
clearTimeout(pending.timeout);
|
|
200
|
-
this.pendingRequests.delete(response.correlationId);
|
|
201
|
-
|
|
202
|
-
if (response.error) {
|
|
203
|
-
pending.reject(response.error);
|
|
204
|
-
} else {
|
|
205
|
-
pending.resolve(response.body);
|
|
206
|
-
}
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Otherwise, treat as a standard broadcast message
|
|
211
|
-
this.options.onMessage?.(e.data);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
this.log("Failed to parse incoming P2P message", "error");
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private setupDownloadChannelHooks(channel: RTCDataChannel) {
|
|
219
|
-
let metadata: { name: string; size: number } | null = null;
|
|
220
|
-
let receivedSize = 0;
|
|
221
|
-
const chunks: Uint8Array[] = [];
|
|
222
|
-
|
|
223
|
-
channel.binaryType = "arraybuffer";
|
|
224
|
-
|
|
225
|
-
channel.onmessage = (event) => {
|
|
226
|
-
if (typeof event.data === "string") {
|
|
227
|
-
metadata = JSON.parse(event.data).body;
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (event.data instanceof ArrayBuffer) {
|
|
232
|
-
chunks.push(new Uint8Array(event.data));
|
|
233
|
-
receivedSize += event.data.byteLength;
|
|
234
|
-
|
|
235
|
-
if (metadata && receivedSize >= metadata.size) {
|
|
236
|
-
const finalBlob = new Blob(chunks as any);
|
|
237
|
-
|
|
238
|
-
// 3. Check if this was a "To Memory" request
|
|
239
|
-
const pending = this.pendingDownloads.get(metadata.name);
|
|
240
|
-
if (pending) {
|
|
241
|
-
pending.resolve(finalBlob);
|
|
242
|
-
this.pendingDownloads.delete(metadata.name);
|
|
243
|
-
} else {
|
|
244
|
-
// Default behavior: Trigger browser save
|
|
245
|
-
this.saveBlob(chunks, metadata.name);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
channel.close();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
channel.onerror = () => {
|
|
254
|
-
if (metadata)
|
|
255
|
-
this.pendingDownloads
|
|
256
|
-
.get(metadata.name)
|
|
257
|
-
?.reject(new Error("Channel error"));
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private saveBlob(chunks: Uint8Array[], fileName: string) {
|
|
262
|
-
const blob = new Blob(chunks as any);
|
|
263
|
-
const url = URL.createObjectURL(blob);
|
|
264
|
-
const a = document.createElement("a");
|
|
265
|
-
a.href = url;
|
|
266
|
-
a.download = fileName;
|
|
267
|
-
a.click();
|
|
268
|
-
URL.revokeObjectURL(url);
|
|
269
|
-
this.log(`Download complete: ${fileName}`, "success");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
public async downloadFile(fileName: string): Promise<void> {
|
|
273
|
-
this.log(`Requesting download: ${fileName}...`);
|
|
274
|
-
|
|
275
|
-
// Request the file via the established Control Channel
|
|
276
|
-
return this.sendCommand(Actions.NODE_TO_CLIENT_FILE_TRANSFER, {
|
|
277
|
-
name: fileName,
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
public async downloadToMemory(fileName: string): Promise<Blob> {
|
|
282
|
-
this.log(`Downloading ${fileName} to memory...`);
|
|
283
|
-
|
|
284
|
-
return new Promise(async (resolve, reject) => {
|
|
285
|
-
// 1. Store the promise callbacks indexed by filename
|
|
286
|
-
// Note: If you expect multiple concurrent downloads of the same name,
|
|
287
|
-
// you'd need a more unique ID here.
|
|
288
|
-
this.pendingDownloads.set(fileName, { resolve, reject });
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
// 2. Trigger the request
|
|
292
|
-
await this.sendCommand(Actions.NODE_TO_CLIENT_FILE_TRANSFER, {
|
|
293
|
-
name: fileName,
|
|
294
|
-
});
|
|
295
|
-
} catch (err) {
|
|
296
|
-
this.pendingDownloads.delete(fileName);
|
|
297
|
-
reject(err);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Returns a promise that resolves when the control channel is open.
|
|
304
|
-
*/
|
|
305
|
-
public async waitForOpen(timeoutMs: number = 10000): Promise<void> {
|
|
306
|
-
if (this.dc && this.dc.readyState === "open") return;
|
|
307
|
-
|
|
308
|
-
return new Promise((resolve, reject) => {
|
|
309
|
-
const timer = setTimeout(() => {
|
|
310
|
-
reject(new Error("Timeout waiting for P2P channel to open"));
|
|
311
|
-
}, timeoutMs);
|
|
312
|
-
|
|
313
|
-
// Check every 100ms or use the existing onopen hook
|
|
314
|
-
const checkInterval = setInterval(() => {
|
|
315
|
-
if (this.dc && this.dc.readyState === "open") {
|
|
316
|
-
clearInterval(checkInterval);
|
|
317
|
-
clearTimeout(timer);
|
|
318
|
-
resolve();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// If we hit an error state, stop checking
|
|
322
|
-
if (
|
|
323
|
-
this.pc?.iceConnectionState === "failed" ||
|
|
324
|
-
this.pc?.iceConnectionState === "closed"
|
|
325
|
-
) {
|
|
326
|
-
clearInterval(checkInterval);
|
|
327
|
-
clearTimeout(timer);
|
|
328
|
-
reject(new Error("P2P Connection failed before channel opened"));
|
|
329
|
-
}
|
|
330
|
-
}, 100);
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private updateState(state: ConnectionState) {
|
|
335
|
-
this.options.onStateChange?.(state);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private handleError(err: any) {
|
|
339
|
-
this.log(`P2P Error: ${err.message || err}`, "error");
|
|
340
|
-
this.updateState("error");
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
public disconnect() {
|
|
344
|
-
this.dc?.close();
|
|
345
|
-
this.pc?.close();
|
|
346
|
-
this.pc = null;
|
|
347
|
-
this.dc = null;
|
|
348
|
-
this.localCandidates = [];
|
|
349
|
-
this.updateState("idle");
|
|
350
|
-
}
|
|
351
|
-
}
|
package/src/uncloudproto.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
type Version = number;
|
|
2
|
-
|
|
3
|
-
export const WEBRTC_CONTROL_PROTO_VERSION: Version = 1;
|
|
4
|
-
|
|
5
|
-
export class ControlMessage<T = unknown> {
|
|
6
|
-
constructor(
|
|
7
|
-
public version: Version,
|
|
8
|
-
public action: Action,
|
|
9
|
-
public correlationId: string,
|
|
10
|
-
public body?: T,
|
|
11
|
-
) {}
|
|
12
|
-
|
|
13
|
-
toJSON(): string {
|
|
14
|
-
return JSON.stringify({
|
|
15
|
-
version: this.version,
|
|
16
|
-
action: this.action,
|
|
17
|
-
correlationId: this.correlationId,
|
|
18
|
-
body: this.body,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FileMetadata {
|
|
24
|
-
name: string;
|
|
25
|
-
size: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function newMessage<T>(action: Action, body?: T): ControlMessage<T> {
|
|
29
|
-
const correlationId = crypto.randomUUID();
|
|
30
|
-
|
|
31
|
-
return new ControlMessage(
|
|
32
|
-
WEBRTC_CONTROL_PROTO_VERSION,
|
|
33
|
-
action,
|
|
34
|
-
correlationId,
|
|
35
|
-
body,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function newMessageJSON<T>(action: Action, body?: T): string {
|
|
40
|
-
return JSON.stringify(newMessage<T>(action, body));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// COPY ME FROM SERVER SIDE
|
|
44
|
-
// THEY MUST MATCH
|
|
45
|
-
|
|
46
|
-
export const StreamNames = {
|
|
47
|
-
CONTROL_STREAM: "control_stream",
|
|
48
|
-
CLIENT_TO_NODE_FILE_TRANSFER: "client_to_node_file_transfer",
|
|
49
|
-
NODE_TO_CLIENT_FILE_TRANSFER: "node_to_client_file_transfer",
|
|
50
|
-
} as const;
|
|
51
|
-
|
|
52
|
-
export type StreamName = (typeof StreamNames)[keyof typeof StreamNames];
|
|
53
|
-
|
|
54
|
-
// COPY ME FROM SERVER SIDE
|
|
55
|
-
// THEY MUST MATCH
|
|
56
|
-
|
|
57
|
-
export const Actions = {
|
|
58
|
-
CLIENT_TO_NODE_FILE_TRANSFER: "client_to_node_file_transfer",
|
|
59
|
-
NODE_TO_CLIENT_FILE_TRANSFER: "node_to_client_file_transfer",
|
|
60
|
-
CLIENT_TO_NODE_LIST_FILES: "client_to_node_list_files",
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export type Action = (typeof Actions)[keyof typeof Actions];
|
|
64
|
-
|
|
65
|
-
export type PendingRequest = {
|
|
66
|
-
resolve: (value: any) => void;
|
|
67
|
-
reject: (reason: any) => void;
|
|
68
|
-
timeout: number;
|
|
69
|
-
};
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"lib": ["DOM", "ES2020"],
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"skipLibCheck": true,
|
|
13
|
-
"forceConsistentCasingInFileNames": true,
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*"],
|
|
16
|
-
}
|