pluto-rtc 0.0.2
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/README.md +84 -0
- package/dist/Connection.d.ts +35 -0
- package/dist/Connection.js +146 -0
- package/dist/ConnectionManager.d.ts +38 -0
- package/dist/ConnectionManager.js +78 -0
- package/dist/api/MediaTransport.d.ts +7 -0
- package/dist/api/MediaTransport.js +262 -0
- package/dist/api/PlutoPeerConnection.d.ts +38 -0
- package/dist/api/PlutoPeerConnection.js +242 -0
- package/dist/api/PlutoWebSocket.d.ts +24 -0
- package/dist/api/PlutoWebSocket.js +112 -0
- package/dist/api/PlutoWebTransport.d.ts +28 -0
- package/dist/api/PlutoWebTransport.js +88 -0
- package/dist/core/Client.d.ts +70 -0
- package/dist/core/Client.js +326 -0
- package/dist/core/Connection.d.ts +66 -0
- package/dist/core/Connection.js +392 -0
- package/dist/core/Room.d.ts +29 -0
- package/dist/core/Room.js +297 -0
- package/dist/core/Signaling.d.ts +37 -0
- package/dist/core/Signaling.js +199 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.js +34 -0
- package/package.json +47 -0
- package/wasm/pkg/README.md +218 -0
- package/wasm/pkg/iroh_wasm.d.ts +148 -0
- package/wasm/pkg/iroh_wasm.js +1382 -0
- package/wasm/pkg/iroh_wasm_bg.wasm +0 -0
- package/wasm/pkg/iroh_wasm_bg.wasm.d.ts +58 -0
- package/wasm/pkg/package.json +15 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { MediaTransport } from './MediaTransport';
|
|
2
|
+
// Tiny SDP helpers to make the ticket look like an SDP so generic parsers don't choke immediately
|
|
3
|
+
// although we usually just pass the string. The demo code uses desc.sdp.
|
|
4
|
+
function toSDP(ticket) {
|
|
5
|
+
return `v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=pluto-ticket:${ticket}\r\n`;
|
|
6
|
+
}
|
|
7
|
+
function fromSDP(sdp) {
|
|
8
|
+
const match = sdp.match(/a=pluto-ticket:(.+)(\r\n|$)/);
|
|
9
|
+
return match ? match[1] : null;
|
|
10
|
+
}
|
|
11
|
+
export class PlutoPeerConnection extends EventTarget {
|
|
12
|
+
static setDefaultClient(client) {
|
|
13
|
+
this.defaultClient = client;
|
|
14
|
+
}
|
|
15
|
+
constructor(configuration, clientInstance) {
|
|
16
|
+
super();
|
|
17
|
+
this.onicecandidate = null;
|
|
18
|
+
this.oniceconnectionstatechange = null;
|
|
19
|
+
this.ontrack = null;
|
|
20
|
+
this.onsignalingstatechange = null;
|
|
21
|
+
this.connectionState = 'new';
|
|
22
|
+
this.iceConnectionState = 'new';
|
|
23
|
+
this.signalingState = 'stable';
|
|
24
|
+
this.localDescription = null;
|
|
25
|
+
this.remoteDescription = null;
|
|
26
|
+
this.connection = null;
|
|
27
|
+
/**
|
|
28
|
+
* Manually provide an existing connection (e.g. from an incoming connection listener)
|
|
29
|
+
*/
|
|
30
|
+
// Simplified PlutoPeerConnection
|
|
31
|
+
// Implementation: Delegates to Core Connection
|
|
32
|
+
this.bufferTracks = [];
|
|
33
|
+
this.receivers = {};
|
|
34
|
+
this.mediaSenders = {};
|
|
35
|
+
// Try to resolve client: constructor arg -> config arg (custom) -> static default
|
|
36
|
+
const client = clientInstance || configuration?.client || PlutoPeerConnection.defaultClient;
|
|
37
|
+
if (!client) {
|
|
38
|
+
throw new Error("PlutoPeerConnection requires a 'Client' instance. Pass it in constructor or use PlutoPeerConnection.setDefaultClient().");
|
|
39
|
+
}
|
|
40
|
+
this.client = client;
|
|
41
|
+
}
|
|
42
|
+
async createOffer(options) {
|
|
43
|
+
// Get our ticket
|
|
44
|
+
let ticket;
|
|
45
|
+
try {
|
|
46
|
+
ticket = await this.client.getTicket();
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error("Failed to get ticket for offer:", e);
|
|
50
|
+
ticket = "error-no-ticket";
|
|
51
|
+
}
|
|
52
|
+
const sdp = toSDP(ticket);
|
|
53
|
+
return { type: 'offer', sdp };
|
|
54
|
+
}
|
|
55
|
+
async createAnswer(options) {
|
|
56
|
+
// Return dummy answer.
|
|
57
|
+
// In "half-signaling", the connection is arguably already established by the offerer (if they connected)
|
|
58
|
+
// OR the answerer (if they connected upon receiving offer).
|
|
59
|
+
// Strategy: The SIDE THAT RECEIVES THE TICKET connects.
|
|
60
|
+
// Usually, 'offer' contains ticket. 'Remote' receives offer -> Remote connects to Local.
|
|
61
|
+
return { type: 'answer', sdp: toSDP('answer-dummy') };
|
|
62
|
+
}
|
|
63
|
+
async setLocalDescription(desc) {
|
|
64
|
+
this.localDescription = desc;
|
|
65
|
+
this.signalingState = (desc.type === 'offer') ? 'have-local-offer' : 'stable';
|
|
66
|
+
// If we just set a local offer, we should simulate gathering candidates (immediate finish)
|
|
67
|
+
if (desc.type === 'offer') {
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
// Emit a null candidate to signal end of candidates
|
|
70
|
+
const event = new Event('icecandidate');
|
|
71
|
+
event.candidate = null;
|
|
72
|
+
if (this.onicecandidate)
|
|
73
|
+
this.onicecandidate(event);
|
|
74
|
+
this.dispatchEvent(event);
|
|
75
|
+
}, 10);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async setRemoteDescription(desc) {
|
|
79
|
+
this.remoteDescription = desc;
|
|
80
|
+
this.signalingState = (desc.type === 'offer') ? 'have-remote-offer' : 'stable';
|
|
81
|
+
// Check if description has a ticket
|
|
82
|
+
const ticket = fromSDP(desc.sdp || '');
|
|
83
|
+
if (ticket && ticket !== 'answer-dummy' && ticket !== 'error-no-ticket') {
|
|
84
|
+
// We received a ticket! Connect to it.
|
|
85
|
+
this.connect(ticket);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Maybe it's an answer? If so, we assume the OTHER side connected to US.
|
|
89
|
+
// We just wait for the connection to come in via client.onConnection?
|
|
90
|
+
// Actually, if we are the Offerer, we are passive.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async addIceCandidate(candidate) {
|
|
94
|
+
// No-op for Iroh/Pluto
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
async handleIncomingConnection(connection) {
|
|
98
|
+
this.connection = connection;
|
|
99
|
+
this.setupHooks();
|
|
100
|
+
// Flush buffer
|
|
101
|
+
this.bufferTracks.forEach(t => this.addTrack(t.track));
|
|
102
|
+
this.bufferTracks = [];
|
|
103
|
+
}
|
|
104
|
+
setupHooks() {
|
|
105
|
+
if (!this.connection)
|
|
106
|
+
return;
|
|
107
|
+
this.connection.onDisconnect(() => this.close());
|
|
108
|
+
this.connection.onTrack((ev) => {
|
|
109
|
+
if (this.ontrack)
|
|
110
|
+
this.ontrack(ev);
|
|
111
|
+
// Clone event to avoid "event is already being dispatched" error
|
|
112
|
+
const newEvent = new RTCTrackEvent('track', {
|
|
113
|
+
track: ev.track,
|
|
114
|
+
streams: [...ev.streams],
|
|
115
|
+
receiver: ev.receiver,
|
|
116
|
+
transceiver: ev.transceiver
|
|
117
|
+
});
|
|
118
|
+
this.dispatchEvent(newEvent);
|
|
119
|
+
});
|
|
120
|
+
// Listen for Iroh media (Fallback/Legacy)
|
|
121
|
+
this.connection.onMedia((type, data) => {
|
|
122
|
+
let track = this.receivers[type];
|
|
123
|
+
if (!track) {
|
|
124
|
+
// New track received!
|
|
125
|
+
track = MediaTransport.receiveTrack(type, () => {
|
|
126
|
+
this.connection?.send({ type: 'PLI', kind: type }).catch(console.error);
|
|
127
|
+
});
|
|
128
|
+
if (track) {
|
|
129
|
+
this.receivers[type] = track;
|
|
130
|
+
// Helper to fire Event
|
|
131
|
+
const event = new Event('track');
|
|
132
|
+
event.track = track;
|
|
133
|
+
event.streams = [new MediaStream([track])];
|
|
134
|
+
event.receiver = { track };
|
|
135
|
+
event.transceiver = { receiver: event.receiver, sender: {}, direction: 'receiveonly' };
|
|
136
|
+
if (this.ontrack)
|
|
137
|
+
this.ontrack(event);
|
|
138
|
+
this.dispatchEvent(event);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (track)
|
|
142
|
+
(track.feed(data));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
addTrack(track, ...streams) {
|
|
146
|
+
const sender = {
|
|
147
|
+
track,
|
|
148
|
+
replaceTrack: async () => { },
|
|
149
|
+
getStats: async () => new Map()
|
|
150
|
+
};
|
|
151
|
+
if (this.connection) {
|
|
152
|
+
// Use Native if available, fallback to Iroh is handled inside Connection or we do dual?
|
|
153
|
+
// Connection now has addTransceiver/addTrack which ADDS to the native PC.
|
|
154
|
+
// But we ALSO need to start the Iroh stream for fallback (Graceful).
|
|
155
|
+
// 1. Add to Native
|
|
156
|
+
this.connection.addTransceiver(track, { direction: 'sendrecv' });
|
|
157
|
+
// 2. Start Iroh Stream (Legacy)
|
|
158
|
+
this.startIrohStream(track);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.bufferTracks.push({ track });
|
|
162
|
+
}
|
|
163
|
+
return sender;
|
|
164
|
+
}
|
|
165
|
+
async startIrohStream(track) {
|
|
166
|
+
if (!this.connection)
|
|
167
|
+
return;
|
|
168
|
+
const conn = this.connection;
|
|
169
|
+
try {
|
|
170
|
+
const sender = await MediaTransport.sendTrack(track, async (data) => {
|
|
171
|
+
// Stop if connection closed
|
|
172
|
+
if (!this.connection || this.connection.isClosed) {
|
|
173
|
+
// We can throw here to stop MediaTransport
|
|
174
|
+
throw new Error("Connection closed");
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await conn.sendMedia(track.kind, data);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
if (e.message && e.message.includes('Connection is closed')) {
|
|
181
|
+
// Expected race condition on close
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.error("Iroh send failed", e);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (sender) {
|
|
188
|
+
this.mediaSenders[track.kind] = sender;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
console.error("Failed to start Iroh stream", e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// ... createDataChannel delegates ...
|
|
196
|
+
createDataChannel(label) {
|
|
197
|
+
// We return a proxy that calls connection.send
|
|
198
|
+
return new PlutoDataChannel(label, this);
|
|
199
|
+
}
|
|
200
|
+
sendData(data) {
|
|
201
|
+
this.connection?.send(data);
|
|
202
|
+
}
|
|
203
|
+
// ... connect ...
|
|
204
|
+
async connect(ticket) {
|
|
205
|
+
this.connection = await this.client.connect(ticket);
|
|
206
|
+
this.setupHooks();
|
|
207
|
+
}
|
|
208
|
+
// ... close ...
|
|
209
|
+
close() {
|
|
210
|
+
this.connection?.disconnect();
|
|
211
|
+
// Stop all legacy senders
|
|
212
|
+
Object.values(this.mediaSenders).forEach(sender => {
|
|
213
|
+
try {
|
|
214
|
+
sender.stop();
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
console.warn("Error stopping sender", e);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
this.mediaSenders = {};
|
|
221
|
+
this.updateState('closed');
|
|
222
|
+
}
|
|
223
|
+
updateState(state) {
|
|
224
|
+
this.connectionState = state;
|
|
225
|
+
this.iceConnectionState = (state === 'closed') ? 'closed' : 'connected'; // aprox
|
|
226
|
+
this.dispatchEvent(new Event('connectionstatechange'));
|
|
227
|
+
this.dispatchEvent(new Event('iceconnectionstatechange'));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
PlutoPeerConnection.defaultClient = null;
|
|
231
|
+
// ... Simple Data Channel Proxy ...
|
|
232
|
+
class PlutoDataChannel extends EventTarget {
|
|
233
|
+
constructor(label, pc) {
|
|
234
|
+
super();
|
|
235
|
+
this.label = label;
|
|
236
|
+
this.pc = pc;
|
|
237
|
+
setTimeout(() => this.emitOpen(), 0);
|
|
238
|
+
}
|
|
239
|
+
send(data) { this.pc.sendData(data); }
|
|
240
|
+
emitOpen() { this.dispatchEvent(new Event('open')); if (this.onopen)
|
|
241
|
+
this.onopen(new Event('open')); }
|
|
242
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Client } from '../core/Client';
|
|
2
|
+
export declare class PlutoWebSocket extends EventTarget {
|
|
3
|
+
static readonly CONNECTING = 0;
|
|
4
|
+
static readonly OPEN = 1;
|
|
5
|
+
static readonly CLOSING = 2;
|
|
6
|
+
static readonly CLOSED = 3;
|
|
7
|
+
readonly url: string;
|
|
8
|
+
readyState: number;
|
|
9
|
+
bufferedAmount: number;
|
|
10
|
+
extensions: string;
|
|
11
|
+
protocol: string;
|
|
12
|
+
binaryType: "blob" | "arraybuffer";
|
|
13
|
+
onopen: ((this: PlutoWebSocket, ev: Event) => any) | null;
|
|
14
|
+
onmessage: ((this: PlutoWebSocket, ev: MessageEvent) => any) | null;
|
|
15
|
+
onclose: ((this: PlutoWebSocket, ev: CloseEvent) => any) | null;
|
|
16
|
+
onerror: ((this: PlutoWebSocket, ev: Event) => any) | null;
|
|
17
|
+
private client;
|
|
18
|
+
private connection;
|
|
19
|
+
constructor(url: string, protocols?: string | string[], clientInstance?: Client);
|
|
20
|
+
private connect;
|
|
21
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
22
|
+
close(code?: number, reason?: string): void;
|
|
23
|
+
private handleClose;
|
|
24
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export class PlutoWebSocket extends EventTarget {
|
|
2
|
+
constructor(url, protocols, clientInstance) {
|
|
3
|
+
super();
|
|
4
|
+
this.readyState = PlutoWebSocket.CONNECTING;
|
|
5
|
+
this.bufferedAmount = 0;
|
|
6
|
+
this.extensions = "";
|
|
7
|
+
this.protocol = "";
|
|
8
|
+
this.binaryType = "blob";
|
|
9
|
+
this.onopen = null;
|
|
10
|
+
this.onmessage = null;
|
|
11
|
+
this.onclose = null;
|
|
12
|
+
this.onerror = null;
|
|
13
|
+
this.connection = null;
|
|
14
|
+
this.url = url;
|
|
15
|
+
// We need a Client instance. If not provided, we have to throw or use a singleton?
|
|
16
|
+
// Since Pluto requires setup (tag, auth), it's hard to make a 100% drop-in without configuration.
|
|
17
|
+
// We will assume the user has a Client initialized or we provide one via a static method,
|
|
18
|
+
// BUT for a true new WebSocket replacement, we might rely on a global or strictly require a passed client if not configuring here.
|
|
19
|
+
// However, the standard WebSocket(url) doesn't take a client.
|
|
20
|
+
// To make it easy, we will support passing the client in the 2nd arg if protocols is ignored/abused,
|
|
21
|
+
// or just expect the developer to instantiate this class with the client.
|
|
22
|
+
if (clientInstance) {
|
|
23
|
+
this.client = clientInstance;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Fallback: This is tricky. We'll throw for now.
|
|
27
|
+
throw new Error("PlutoWebSocket requires a 'Client' instance to be passed in constructor currently.");
|
|
28
|
+
}
|
|
29
|
+
this.connect();
|
|
30
|
+
}
|
|
31
|
+
async connect() {
|
|
32
|
+
try {
|
|
33
|
+
// URL format: pluto://<ticket>
|
|
34
|
+
let ticket = this.url;
|
|
35
|
+
if (ticket.startsWith('pluto://')) {
|
|
36
|
+
ticket = ticket.replace('pluto://', '');
|
|
37
|
+
}
|
|
38
|
+
else if (ticket.startsWith('wss://') || ticket.startsWith('ws://')) {
|
|
39
|
+
// Ignore standard WS urls or treat as ticket?
|
|
40
|
+
// Assume ticket only for now.
|
|
41
|
+
}
|
|
42
|
+
this.connection = await this.client.connect(ticket);
|
|
43
|
+
this.readyState = PlutoWebSocket.OPEN;
|
|
44
|
+
const event = new Event('open');
|
|
45
|
+
if (this.onopen)
|
|
46
|
+
this.onopen(event);
|
|
47
|
+
this.dispatchEvent(event);
|
|
48
|
+
this.connection.onMessage((msg) => {
|
|
49
|
+
let data = msg;
|
|
50
|
+
// If msg is buffer/uint8array, respect binaryType
|
|
51
|
+
if (this.binaryType === 'arraybuffer' && (msg instanceof Uint8Array)) {
|
|
52
|
+
data = msg.buffer;
|
|
53
|
+
}
|
|
54
|
+
const msgEvent = new MessageEvent('message', {
|
|
55
|
+
data,
|
|
56
|
+
origin: this.url,
|
|
57
|
+
});
|
|
58
|
+
if (this.onmessage)
|
|
59
|
+
this.onmessage(msgEvent);
|
|
60
|
+
this.dispatchEvent(msgEvent);
|
|
61
|
+
});
|
|
62
|
+
this.connection.onDisconnect(() => {
|
|
63
|
+
this.handleClose();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.error("WebSocket connection failure:", e);
|
|
68
|
+
const event = new Event('error');
|
|
69
|
+
if (this.onerror)
|
|
70
|
+
this.onerror(event);
|
|
71
|
+
this.dispatchEvent(event);
|
|
72
|
+
this.handleClose(1006, "Connection failed");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
send(data) {
|
|
76
|
+
if (this.readyState !== PlutoWebSocket.OPEN || !this.connection) {
|
|
77
|
+
throw new Error("WebSocket is not open");
|
|
78
|
+
}
|
|
79
|
+
if (data instanceof Blob) {
|
|
80
|
+
data.arrayBuffer().then(buf => {
|
|
81
|
+
this.connection?.send(buf);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// @ts-ignore - Connection.send handles these
|
|
86
|
+
this.connection.send(data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
close(code, reason) {
|
|
90
|
+
if (this.readyState === PlutoWebSocket.CLOSED)
|
|
91
|
+
return;
|
|
92
|
+
this.readyState = PlutoWebSocket.CLOSING;
|
|
93
|
+
if (this.connection) {
|
|
94
|
+
this.connection.disconnect();
|
|
95
|
+
}
|
|
96
|
+
// Force close if it takes too long?
|
|
97
|
+
this.handleClose(code, reason);
|
|
98
|
+
}
|
|
99
|
+
handleClose(code = 1000, reason = "") {
|
|
100
|
+
if (this.readyState === PlutoWebSocket.CLOSED)
|
|
101
|
+
return;
|
|
102
|
+
this.readyState = PlutoWebSocket.CLOSED;
|
|
103
|
+
const event = new CloseEvent('close', { code, reason, wasClean: true });
|
|
104
|
+
if (this.onclose)
|
|
105
|
+
this.onclose(event);
|
|
106
|
+
this.dispatchEvent(event);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
PlutoWebSocket.CONNECTING = 0;
|
|
110
|
+
PlutoWebSocket.OPEN = 1;
|
|
111
|
+
PlutoWebSocket.CLOSING = 2;
|
|
112
|
+
PlutoWebSocket.CLOSED = 3;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Client } from '../core/Client';
|
|
2
|
+
export declare class PlutoWebTransport {
|
|
3
|
+
readonly ready: Promise<void>;
|
|
4
|
+
closed: Promise<void>;
|
|
5
|
+
datagrams: any;
|
|
6
|
+
incomingBidirectionalStreams: ReadableStream<WebTransportBidirectionalStream>;
|
|
7
|
+
incomingUnidirectionalStreams: ReadableStream<ReadableStream<Uint8Array>>;
|
|
8
|
+
private client;
|
|
9
|
+
private connection;
|
|
10
|
+
private url;
|
|
11
|
+
private _readyResolve;
|
|
12
|
+
private _readyReject;
|
|
13
|
+
private _closedResolve;
|
|
14
|
+
private _closedReject;
|
|
15
|
+
private incomingBiController;
|
|
16
|
+
private incomingUniController;
|
|
17
|
+
private remoteNodeId;
|
|
18
|
+
constructor(url: string, options?: any, clientInstance?: Client);
|
|
19
|
+
private setupListeners;
|
|
20
|
+
private connect;
|
|
21
|
+
createBidirectionalStream(): Promise<WebTransportBidirectionalStream>;
|
|
22
|
+
createUnidirectionalStream(): Promise<WritableStream<Uint8Array>>;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
export interface WebTransportBidirectionalStream {
|
|
26
|
+
readable: ReadableStream<Uint8Array>;
|
|
27
|
+
writable: WritableStream<Uint8Array>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export class PlutoWebTransport {
|
|
2
|
+
constructor(url, options, clientInstance) {
|
|
3
|
+
this.connection = null;
|
|
4
|
+
this.remoteNodeId = null;
|
|
5
|
+
this.url = url;
|
|
6
|
+
if (clientInstance) {
|
|
7
|
+
this.client = clientInstance;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
throw new Error("PlutoWebTransport requires a 'Client' instance to be passed in constructor currently.");
|
|
11
|
+
}
|
|
12
|
+
this.ready = new Promise((resolve, reject) => {
|
|
13
|
+
this._readyResolve = resolve;
|
|
14
|
+
this._readyReject = reject;
|
|
15
|
+
});
|
|
16
|
+
this.closed = new Promise((resolve, reject) => {
|
|
17
|
+
this._closedResolve = resolve;
|
|
18
|
+
this._closedReject = reject;
|
|
19
|
+
});
|
|
20
|
+
this.datagrams = {
|
|
21
|
+
readable: new ReadableStream(),
|
|
22
|
+
writable: new WritableStream()
|
|
23
|
+
};
|
|
24
|
+
this.incomingBidirectionalStreams = new ReadableStream({
|
|
25
|
+
start: (controller) => { this.incomingBiController = controller; }
|
|
26
|
+
});
|
|
27
|
+
this.incomingUnidirectionalStreams = new ReadableStream({
|
|
28
|
+
start: (controller) => { this.incomingUniController = controller; }
|
|
29
|
+
});
|
|
30
|
+
this.setupListeners();
|
|
31
|
+
this.connect();
|
|
32
|
+
}
|
|
33
|
+
setupListeners() {
|
|
34
|
+
this.client.onIncomingStream((incoming) => {
|
|
35
|
+
// Filter by remote Node ID if we are connected
|
|
36
|
+
if (!this.remoteNodeId || incoming.remoteNodeId !== this.remoteNodeId) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (incoming.type === 'bi') {
|
|
40
|
+
const stream = incoming.stream; // BiStream
|
|
41
|
+
this.incomingBiController.enqueue({
|
|
42
|
+
readable: stream.recv,
|
|
43
|
+
writable: stream.send
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (incoming.type === 'uni') {
|
|
47
|
+
const stream = incoming.stream; // ReadableStream
|
|
48
|
+
this.incomingUniController.enqueue(stream);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async connect() {
|
|
53
|
+
try {
|
|
54
|
+
let ticket = this.url;
|
|
55
|
+
if (ticket.startsWith('pluto://')) {
|
|
56
|
+
ticket = ticket.replace('pluto://', '');
|
|
57
|
+
}
|
|
58
|
+
else if (ticket.startsWith('https://')) {
|
|
59
|
+
// WebTransport uses https, but we treat it as ticket for now or need a way to resolve
|
|
60
|
+
}
|
|
61
|
+
this.connection = await this.client.connect(ticket);
|
|
62
|
+
this.remoteNodeId = this.connection.deviceId; // Get peer ID from connection
|
|
63
|
+
this._readyResolve();
|
|
64
|
+
this.connection.onDisconnect(() => {
|
|
65
|
+
this._closedResolve();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
this._readyReject(e);
|
|
70
|
+
this._closedReject(e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async createBidirectionalStream() {
|
|
74
|
+
await this.ready;
|
|
75
|
+
if (!this.connection || !this.remoteNodeId)
|
|
76
|
+
throw new Error("Connection not established");
|
|
77
|
+
return await this.client.openBi(this.remoteNodeId);
|
|
78
|
+
}
|
|
79
|
+
async createUnidirectionalStream() {
|
|
80
|
+
await this.ready;
|
|
81
|
+
if (!this.connection || !this.remoteNodeId)
|
|
82
|
+
throw new Error("Connection not established");
|
|
83
|
+
return await this.client.openUni(this.remoteNodeId);
|
|
84
|
+
}
|
|
85
|
+
close() {
|
|
86
|
+
this.connection?.disconnect();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Connection } from './Connection';
|
|
2
|
+
import { Signaling } from './Signaling';
|
|
3
|
+
import { RoomManager, JoinRequest } from './Room';
|
|
4
|
+
export interface ClientOptions {
|
|
5
|
+
tag: string;
|
|
6
|
+
storagePrefix?: string;
|
|
7
|
+
deviceTTL?: number;
|
|
8
|
+
nodeIdPersistence?: 'persistent' | 'ephemeral';
|
|
9
|
+
secretKey?: string;
|
|
10
|
+
experimental?: {
|
|
11
|
+
nativeWebRTC?: RTCConfiguration;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare class Client {
|
|
15
|
+
signaling: Signaling;
|
|
16
|
+
rooms: RoomManager;
|
|
17
|
+
private wasmLoaded;
|
|
18
|
+
private wasmPkg;
|
|
19
|
+
private node;
|
|
20
|
+
private connections;
|
|
21
|
+
private connectionListeners;
|
|
22
|
+
private disconnectionListeners;
|
|
23
|
+
private messageListeners;
|
|
24
|
+
private roomRequestListeners;
|
|
25
|
+
private streamListeners;
|
|
26
|
+
private localNodeId;
|
|
27
|
+
private listening;
|
|
28
|
+
private options;
|
|
29
|
+
private pendingConnection;
|
|
30
|
+
private presenceInterval;
|
|
31
|
+
constructor(options: ClientOptions);
|
|
32
|
+
get currentUser(): import("@firebase/auth").User | null;
|
|
33
|
+
onAuthChange(callback: (user: any) => void): void;
|
|
34
|
+
signInAnonymously(): Promise<void>;
|
|
35
|
+
signInWithPluto(): void;
|
|
36
|
+
signOut(): Promise<void>;
|
|
37
|
+
searchDevices(): Promise<{
|
|
38
|
+
deviceId: string;
|
|
39
|
+
deviceName: string;
|
|
40
|
+
online: boolean;
|
|
41
|
+
ticket: string;
|
|
42
|
+
}[]>;
|
|
43
|
+
onDevicesChange(callback: any): () => void;
|
|
44
|
+
init(): Promise<void>;
|
|
45
|
+
connect(ticket: string): Promise<Connection>;
|
|
46
|
+
openBi(nodeId: string): Promise<{
|
|
47
|
+
readable: ReadableStream<Uint8Array>;
|
|
48
|
+
writable: WritableStream<Uint8Array>;
|
|
49
|
+
}>;
|
|
50
|
+
openUni(nodeId: string): Promise<WritableStream<Uint8Array>>;
|
|
51
|
+
getConnections(): Connection[];
|
|
52
|
+
getTicket(): Promise<string>;
|
|
53
|
+
getNodeId(): Promise<string>;
|
|
54
|
+
startListening(): Promise<void>;
|
|
55
|
+
stopListening(): void;
|
|
56
|
+
private updatePresence;
|
|
57
|
+
private listenLoop;
|
|
58
|
+
private handleIncomingStream;
|
|
59
|
+
private registerConnection;
|
|
60
|
+
onConnection(callback: (conn: Connection) => void): void;
|
|
61
|
+
onDisconnection(callback: (conn: Connection) => void): void;
|
|
62
|
+
onMessage(callback: (conn: Connection, msg: any) => void): void;
|
|
63
|
+
onIncomingStream(callback: (stream: {
|
|
64
|
+
type: 'bi' | 'uni';
|
|
65
|
+
stream: any;
|
|
66
|
+
remoteNodeId: string;
|
|
67
|
+
}) => void): void;
|
|
68
|
+
onRoomJoinRequest(callback: (request: JoinRequest) => void): void;
|
|
69
|
+
private handleMessage;
|
|
70
|
+
}
|