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.
@@ -0,0 +1,297 @@
1
+ import { doc, runTransaction, serverTimestamp, collection, getDocs, updateDoc } from 'firebase/firestore';
2
+ export class RoomManager {
3
+ constructor(client, signaling) {
4
+ this.heartbeatInterval = null;
5
+ this.client = client;
6
+ this.signaling = signaling;
7
+ }
8
+ get db() {
9
+ return this.signaling.db;
10
+ }
11
+ get tag() {
12
+ // We access tag via signaling options, but we don't have it exposed directly on Signaling instance publicly
13
+ // A bit of a hack, but let's assume options are available or we pass it
14
+ // For now, let's fix Signaling to expose tag or access it differently.
15
+ // Actually, let's just re-use the tag from the signaling instance if we expose it or passed it.
16
+ // I will update Signaling.ts to expose tag as public readonly or getter.
17
+ // For now I'll cast to any as a temporary measure or fix Signaling in next step.
18
+ return this.signaling.tag;
19
+ }
20
+ /**
21
+ * Creates a new room (or joins the special 'demo' room if specified)
22
+ */
23
+ async createRoom() {
24
+ if (!this.signaling.currentUser)
25
+ throw new Error("Must be signed in");
26
+ const myTicket = await this.client.getTicket();
27
+ const userId = this.signaling.currentUser.uid;
28
+ const myNodeId = await this.client.getNodeId();
29
+ // Generate 6-char random ID
30
+ // Try up to 3 times to account for collisions
31
+ let roomId = '';
32
+ let created = false;
33
+ for (let i = 0; i < 3; i++) {
34
+ roomId = Math.random().toString(36).substring(2, 8).toUpperCase();
35
+ const roomRef = doc(this.db, `custom/${this.tag}/rooms`, roomId);
36
+ const memberRef = doc(this.db, `custom/${this.tag}/rooms`, roomId, 'members', myNodeId);
37
+ try {
38
+ await runTransaction(this.db, async (transaction) => {
39
+ const roomDoc = await transaction.get(roomRef);
40
+ if (roomDoc.exists()) {
41
+ throw "Room already exists"; // Collision, try again
42
+ }
43
+ // Create room parent
44
+ transaction.set(roomRef, {
45
+ hostId: userId,
46
+ memberCount: 1,
47
+ maxMembers: 10,
48
+ status: 'open',
49
+ createdAt: serverTimestamp()
50
+ });
51
+ // Add self as member
52
+ transaction.set(memberRef, {
53
+ userId: userId, // Store userId for auth verification
54
+ ticket: myTicket,
55
+ joinedAt: serverTimestamp()
56
+ });
57
+ });
58
+ console.log(`Room created: ${roomId}`);
59
+ created = true;
60
+ break;
61
+ }
62
+ catch (e) {
63
+ if (e === "Room already exists") {
64
+ console.log(`Collision for roomId ${roomId}, retrying...`);
65
+ continue;
66
+ }
67
+ throw e; // Real error
68
+ }
69
+ }
70
+ if (!created) {
71
+ throw new Error("Failed to create room after multiple attempts (ID collisions).");
72
+ }
73
+ return roomId;
74
+ }
75
+ /**
76
+ * Joins a room by ID.
77
+ */
78
+ async joinRoom(roomId) {
79
+ if (!this.signaling.currentUser)
80
+ throw new Error("Must be signed in");
81
+ const myTicket = await this.client.getTicket();
82
+ const userId = this.signaling.currentUser.uid;
83
+ const myNodeId = await this.client.getNodeId();
84
+ const normalizedId = roomId.toUpperCase();
85
+ const targetRoomId = roomId === 'demo' ? 'demo' : roomId;
86
+ const roomRef = doc(this.db, `custom/${this.tag}/rooms`, targetRoomId);
87
+ const memberRef = doc(this.db, `custom/${this.tag}/rooms`, targetRoomId, 'members', myNodeId);
88
+ let createdDemo = false;
89
+ try {
90
+ await runTransaction(this.db, async (transaction) => {
91
+ const roomDoc = await transaction.get(roomRef);
92
+ if (!roomDoc.exists()) {
93
+ if (targetRoomId === 'demo') {
94
+ // Create demo room on the fly
95
+ transaction.set(roomRef, {
96
+ hostId: 'system',
97
+ memberCount: 1,
98
+ maxMembers: 100, // Explicit 100 limit for demo
99
+ status: 'open',
100
+ createdAt: serverTimestamp(),
101
+ isDemo: true
102
+ });
103
+ createdDemo = true;
104
+ }
105
+ else {
106
+ throw "Room does not exist!";
107
+ }
108
+ }
109
+ else {
110
+ const data = roomDoc.data();
111
+ const currentCount = data.memberCount || 0;
112
+ const maxMembers = data.maxMembers || 10;
113
+ if (currentCount >= maxMembers) {
114
+ // Check if I am ALREADY a member
115
+ const memberDoc = await transaction.get(memberRef);
116
+ if (memberDoc.exists()) {
117
+ // Already joined, just update ticket
118
+ transaction.update(memberRef, { ticket: myTicket });
119
+ return;
120
+ }
121
+ throw "Room is full!";
122
+ }
123
+ transaction.update(roomRef, { memberCount: currentCount + 1 });
124
+ }
125
+ // Add/Update member
126
+ if (!createdDemo) {
127
+ transaction.set(memberRef, {
128
+ userId: userId,
129
+ ticket: myTicket,
130
+ joinedAt: serverTimestamp()
131
+ }, { merge: true });
132
+ }
133
+ else {
134
+ transaction.set(memberRef, {
135
+ userId: userId,
136
+ ticket: myTicket,
137
+ joinedAt: serverTimestamp()
138
+ });
139
+ }
140
+ });
141
+ console.log(`Joined room: ${targetRoomId}`);
142
+ }
143
+ catch (e) {
144
+ console.error("Join Transaction failed: ", e);
145
+ throw e;
146
+ }
147
+ // BOOTSTRAP: Connect to existing members
148
+ const membersRef = collection(this.db, `custom/${this.tag}/rooms`, targetRoomId, 'members');
149
+ const membersSnap = await getDocs(membersRef);
150
+ const connections = [];
151
+ for (const doc of membersSnap.docs) {
152
+ if (doc.id === myNodeId)
153
+ continue; // Skip self
154
+ const data = doc.data();
155
+ // Check expiration
156
+ let isExpired = false;
157
+ const now = Date.now();
158
+ if (data.expiresAt) {
159
+ const exp = data.expiresAt.seconds ? data.expiresAt.seconds * 1000 : new Date(data.expiresAt).getTime();
160
+ if (exp < now)
161
+ isExpired = true;
162
+ }
163
+ else {
164
+ const lastSeen = data.lastSeenAt?.toMillis() || data.joinedAt?.toMillis() || 0;
165
+ if (now - lastSeen > 5 * 60 * 1000)
166
+ isExpired = true;
167
+ }
168
+ if (isExpired)
169
+ continue;
170
+ const ticket = data.ticket;
171
+ if (ticket) {
172
+ try {
173
+ console.log(`Bootstrapping: Connecting to peer ${doc.id}...`);
174
+ const conn = await this.client.connect(ticket);
175
+ conn.send({ type: 'chat', text: `Use ${userId} joined the room.` });
176
+ connections.push(conn);
177
+ }
178
+ catch (e) {
179
+ console.warn(`Failed to connect to peer ${doc.id}:`, e);
180
+ }
181
+ }
182
+ }
183
+ // Start Heartbeat
184
+ this.startHeartbeat(targetRoomId, myNodeId);
185
+ // Prune Stale Members (Lazy cleanup)
186
+ this.pruneStaleMembers(targetRoomId).catch(console.error);
187
+ return connections;
188
+ }
189
+ async leaveRoom(roomId) {
190
+ if (!this.signaling.currentUser)
191
+ return;
192
+ const myNodeId = await this.client.getNodeId();
193
+ if (!myNodeId)
194
+ return;
195
+ console.log(`Leaving room ${roomId}...`);
196
+ this.stopHeartbeat();
197
+ const targetRoomId = roomId === 'demo' ? 'demo' : roomId;
198
+ const roomRef = doc(this.db, `custom/${this.tag}/rooms`, targetRoomId);
199
+ const memberRef = doc(this.db, `custom/${this.tag}/rooms`, targetRoomId, 'members', myNodeId);
200
+ try {
201
+ await runTransaction(this.db, async (transaction) => {
202
+ const roomDoc = await transaction.get(roomRef);
203
+ const memberDoc = await transaction.get(memberRef);
204
+ if (!memberDoc.exists()) {
205
+ return;
206
+ }
207
+ transaction.delete(memberRef);
208
+ if (roomDoc.exists()) {
209
+ const currentCount = roomDoc.data().memberCount || 0;
210
+ if (currentCount > 0) {
211
+ transaction.update(roomRef, { memberCount: currentCount - 1 });
212
+ }
213
+ }
214
+ });
215
+ console.log(`Left room: ${targetRoomId}`);
216
+ }
217
+ catch (e) {
218
+ console.warn("Leave room failed:", e);
219
+ throw e;
220
+ }
221
+ }
222
+ // --- Heartbeats & Cleanup ---
223
+ // --- Heartbeats & Cleanup ---
224
+ startHeartbeat(roomId, memberId) {
225
+ if (this.heartbeatInterval)
226
+ clearInterval(this.heartbeatInterval);
227
+ const memberRef = doc(this.db, `custom/${this.tag}/rooms`, roomId, 'members', memberId);
228
+ const update = () => {
229
+ // Expires in 2 minutes
230
+ const expiresAt = new Date(Date.now() + 2 * 60 * 1000);
231
+ updateDoc(memberRef, {
232
+ lastSeenAt: serverTimestamp(),
233
+ expiresAt: expiresAt
234
+ }).catch(e => {
235
+ console.warn("Heartbeat failed:", e);
236
+ if (e.message && e.message.includes('No document to update')) {
237
+ console.log("Member document not found, stopping heartbeat.");
238
+ clearInterval(this.heartbeatInterval);
239
+ this.heartbeatInterval = null;
240
+ }
241
+ });
242
+ };
243
+ update(); // Initial update
244
+ this.heartbeatInterval = setInterval(update, 60000); // 1 minute
245
+ }
246
+ stopHeartbeat() {
247
+ if (this.heartbeatInterval) {
248
+ clearInterval(this.heartbeatInterval);
249
+ this.heartbeatInterval = null;
250
+ console.log("Heartbeat stopped.");
251
+ }
252
+ }
253
+ async pruneStaleMembers(roomId) {
254
+ const membersRef = collection(this.db, `custom/${this.tag}/rooms`, roomId, 'members');
255
+ const now = Date.now();
256
+ const snapshot = await getDocs(membersRef);
257
+ const prunePromises = snapshot.docs.map(async (docSnap) => {
258
+ const data = docSnap.data();
259
+ let isExpired = false;
260
+ if (data.expiresAt) {
261
+ // Handle both Firestore Timestamp and JS Date/number
262
+ const exp = data.expiresAt.seconds ? data.expiresAt.seconds * 1000 : new Date(data.expiresAt).getTime();
263
+ if (exp < now)
264
+ isExpired = true;
265
+ }
266
+ else {
267
+ // Fallback for legacy members without expiresAt
268
+ const lastSeen = data.lastSeenAt?.toMillis() || data.joinedAt?.toMillis() || 0;
269
+ if (now - lastSeen > 5 * 60 * 1000)
270
+ isExpired = true;
271
+ }
272
+ if (isExpired) {
273
+ console.log(`Pruning stale member ${docSnap.id}`);
274
+ try {
275
+ const roomRef = doc(this.db, `custom/${this.tag}/rooms`, roomId);
276
+ await runTransaction(this.db, async (transaction) => {
277
+ const roomDoc = await transaction.get(roomRef);
278
+ if (!roomDoc.exists())
279
+ return;
280
+ const memberDoc = await transaction.get(docSnap.ref);
281
+ if (!memberDoc.exists())
282
+ return;
283
+ transaction.delete(docSnap.ref);
284
+ const currentCount = roomDoc.data().memberCount || 0;
285
+ if (currentCount > 0) {
286
+ transaction.update(roomRef, { memberCount: currentCount - 1 });
287
+ }
288
+ });
289
+ }
290
+ catch (e) {
291
+ console.warn(`Failed to prune member ${docSnap.id}:`, e);
292
+ }
293
+ }
294
+ });
295
+ await Promise.all(prunePromises);
296
+ }
297
+ }
@@ -0,0 +1,37 @@
1
+ import { FirebaseApp } from 'firebase/app';
2
+ import { Auth, User } from 'firebase/auth';
3
+ import { Firestore } from 'firebase/firestore';
4
+ export interface SignalingOptions {
5
+ tag: string;
6
+ storagePrefix?: string;
7
+ }
8
+ export declare class Signaling {
9
+ app: FirebaseApp;
10
+ auth: Auth;
11
+ db: Firestore;
12
+ tag: string;
13
+ private options;
14
+ private authListeners;
15
+ constructor(options: SignalingOptions);
16
+ get currentUser(): User | null;
17
+ onAuthChange(callback: (user: User | null) => void): void;
18
+ signInAnonymously(): Promise<void>;
19
+ signInWithPluto(): void;
20
+ signOut(): Promise<void>;
21
+ private checkForSSOToken;
22
+ updatePresence(localNodeId: string, ticketStr: string, isOnline?: boolean, ttlMs?: number): Promise<void>;
23
+ setOffline(localNodeId: string): Promise<void>;
24
+ cleanupStaleDevices(): Promise<void>;
25
+ searchDevices(excludeNodeId?: string): Promise<{
26
+ deviceId: string;
27
+ deviceName: string;
28
+ online: boolean;
29
+ ticket: string;
30
+ }[]>;
31
+ onDevicesChange(callback: (devices: {
32
+ deviceId: string;
33
+ deviceName: string;
34
+ online: boolean;
35
+ ticket: string;
36
+ }[]) => void, excludeNodeId?: string): () => void;
37
+ }
@@ -0,0 +1,199 @@
1
+ import { initializeApp } from 'firebase/app';
2
+ import { getAuth, onAuthStateChanged, signInWithCustomToken, signInAnonymously } from 'firebase/auth';
3
+ import { getFirestore, doc, setDoc, updateDoc, serverTimestamp, deleteDoc, collection, query, where, getDocs, onSnapshot, Timestamp } from 'firebase/firestore';
4
+ // Internal Firebase Config
5
+ const FIREBASE_CONFIG = {
6
+ apiKey: "AIzaSyAxyDE1xSYNAk5Ohe8VvKWi3xHOxB4cNV8",
7
+ authDomain: "plutonium.hargreaves.dev",
8
+ projectId: "files-bb895",
9
+ storageBucket: "files-bb895.firebasestorage.app",
10
+ messagingSenderId: "734446020343",
11
+ appId: "1:734446020343:web:da52ff2b8170058fb6d5f2",
12
+ measurementId: "G-FM0GQ9QF30"
13
+ };
14
+ export class Signaling {
15
+ constructor(options) {
16
+ // Auth listeners
17
+ this.authListeners = [];
18
+ if (!options.tag)
19
+ throw new Error("Signaling requires a 'tag' option.");
20
+ this.tag = options.tag;
21
+ this.options = options;
22
+ this.app = initializeApp(FIREBASE_CONFIG);
23
+ this.auth = getAuth(this.app);
24
+ // Enable persistence
25
+ import('firebase/auth').then(({ setPersistence, browserLocalPersistence }) => {
26
+ setPersistence(this.auth, browserLocalPersistence).catch(console.error);
27
+ });
28
+ this.db = getFirestore(this.app);
29
+ // Check for SSO token immediately
30
+ this.checkForSSOToken();
31
+ // Propagate auth state changes
32
+ onAuthStateChanged(this.auth, (user) => {
33
+ this.authListeners.forEach(cb => cb(user));
34
+ });
35
+ }
36
+ get currentUser() {
37
+ return this.auth.currentUser;
38
+ }
39
+ onAuthChange(callback) {
40
+ this.authListeners.push(callback);
41
+ // Fire immediately with current state if known
42
+ if (this.auth.currentUser !== undefined) {
43
+ callback(this.auth.currentUser);
44
+ }
45
+ }
46
+ async signInAnonymously() {
47
+ await signInAnonymously(this.auth);
48
+ }
49
+ signInWithPluto() {
50
+ const plutoAuthUrl = 'https://plutonium.hargreaves.dev/sso/authorize';
51
+ const redirectUri = window.location.href;
52
+ const url = new URL(plutoAuthUrl);
53
+ url.searchParams.set('redirect_uri', redirectUri);
54
+ window.location.href = url.toString();
55
+ }
56
+ async signOut() {
57
+ await this.auth.signOut();
58
+ await this.signInAnonymously();
59
+ }
60
+ async checkForSSOToken() {
61
+ if (typeof window === 'undefined')
62
+ return;
63
+ const hash = window.location.hash;
64
+ if (hash && hash.includes('token=')) {
65
+ const params = new URLSearchParams(hash.substring(1));
66
+ const token = params.get('token');
67
+ if (token) {
68
+ console.log("Found SSO token, signing in...");
69
+ try {
70
+ await signInWithCustomToken(this.auth, token);
71
+ console.log("SSO Sign in successful");
72
+ window.history.replaceState(null, '', window.location.pathname + window.location.search);
73
+ }
74
+ catch (e) {
75
+ console.error("SSO Sign in failed", e);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ // --- Presence ---
81
+ async updatePresence(localNodeId, ticketStr, isOnline = true, ttlMs = 5 * 60 * 1000) {
82
+ if (!this.auth.currentUser)
83
+ return;
84
+ let name = "Web Client";
85
+ if (typeof navigator !== 'undefined') {
86
+ if (navigator.userAgent.includes("Chrome"))
87
+ name = "Chrome";
88
+ else if (navigator.userAgent.includes("Firefox"))
89
+ name = "Firefox";
90
+ else if (navigator.userAgent.includes("Safari"))
91
+ name = "Safari";
92
+ }
93
+ if (this.options.storagePrefix) {
94
+ const prefix = this.options.storagePrefix.replace(/_$/, '');
95
+ name = `${name} (${prefix})`;
96
+ }
97
+ const deviceRef = doc(this.db, 'users', this.auth.currentUser.uid, 'devices', localNodeId);
98
+ const expiresAt = Timestamp.fromMillis(Date.now() + ttlMs);
99
+ try {
100
+ await setDoc(deviceRef, {
101
+ userId: this.auth.currentUser.uid,
102
+ nodeId: localNodeId,
103
+ ticket: ticketStr,
104
+ deviceName: name,
105
+ platformType: 'web',
106
+ tag: this.tag,
107
+ online: isOnline,
108
+ lastSeenAt: serverTimestamp(),
109
+ expiresAt: expiresAt
110
+ }, { merge: true });
111
+ }
112
+ catch (e) {
113
+ console.warn("Update presence failed", e);
114
+ }
115
+ }
116
+ async setOffline(localNodeId) {
117
+ if (this.auth.currentUser && localNodeId) {
118
+ const deviceRef = doc(this.db, 'users', this.auth.currentUser.uid, 'devices', localNodeId);
119
+ updateDoc(deviceRef, { online: false }).catch(() => { });
120
+ }
121
+ }
122
+ async cleanupStaleDevices() {
123
+ if (!this.auth.currentUser)
124
+ return;
125
+ const devicesRef = collection(this.db, 'users', this.auth.currentUser.uid, 'devices');
126
+ // Delete expired devices
127
+ const now = Timestamp.now();
128
+ const q = query(devicesRef, where('expiresAt', '<', now));
129
+ try {
130
+ const snapshot = await getDocs(q);
131
+ const deletePromises = [];
132
+ snapshot.forEach(doc => {
133
+ deletePromises.push(deleteDoc(doc.ref));
134
+ });
135
+ if (deletePromises.length > 0) {
136
+ await Promise.all(deletePromises);
137
+ }
138
+ }
139
+ catch (e) {
140
+ console.warn("Failed to cleanup stale devices:", e);
141
+ }
142
+ }
143
+ // --- Discovery ---
144
+ async searchDevices(excludeNodeId) {
145
+ if (!this.auth.currentUser)
146
+ return [];
147
+ const devicesRef = collection(this.db, 'users', this.auth.currentUser.uid, 'devices');
148
+ const snapshot = await getDocs(devicesRef);
149
+ const devices = [];
150
+ snapshot.forEach(doc => {
151
+ const data = doc.data();
152
+ if (excludeNodeId && data.nodeId === excludeNodeId)
153
+ return;
154
+ // Check expiration
155
+ if (data.expiresAt) {
156
+ const expiresCmp = data.expiresAt instanceof Timestamp ? data.expiresAt.toMillis() : (data.expiresAt.seconds * 1000);
157
+ if (expiresCmp < Date.now())
158
+ return; // Expired
159
+ }
160
+ devices.push({
161
+ deviceId: doc.id,
162
+ deviceName: data.deviceName || 'Unknown',
163
+ online: data.online,
164
+ ticket: data.ticket || data.nodeId
165
+ });
166
+ });
167
+ return devices;
168
+ }
169
+ onDevicesChange(callback, excludeNodeId) {
170
+ if (!this.auth.currentUser)
171
+ return () => { };
172
+ const devicesRef = collection(this.db, 'users', this.auth.currentUser.uid, 'devices');
173
+ // Filter by tag to only show relevant devices
174
+ const q = query(devicesRef, where('tag', '==', this.tag));
175
+ const unsub = onSnapshot(q, (snapshot) => {
176
+ const devices = [];
177
+ const now = Date.now();
178
+ snapshot.forEach(doc => {
179
+ const data = doc.data();
180
+ if (excludeNodeId && data.nodeId === excludeNodeId)
181
+ return;
182
+ // Check expiration
183
+ if (data.expiresAt) {
184
+ const expiresCmp = data.expiresAt instanceof Timestamp ? data.expiresAt.toMillis() : (data.expiresAt.seconds * 1000);
185
+ if (expiresCmp < now)
186
+ return; // Expired
187
+ }
188
+ devices.push({
189
+ deviceId: doc.id,
190
+ deviceName: data.deviceName || 'Unknown',
191
+ online: data.online,
192
+ ticket: data.ticket || data.nodeId
193
+ });
194
+ });
195
+ callback(devices);
196
+ });
197
+ return unsub;
198
+ }
199
+ }
@@ -0,0 +1,8 @@
1
+ export * from './core/Client';
2
+ export * from './core/Connection';
3
+ export * from './core/Signaling';
4
+ export * from './core/Room';
5
+ export * from './ConnectionManager';
6
+ export * from './api/PlutoWebSocket';
7
+ export * from './api/PlutoWebTransport';
8
+ export * from './api/PlutoPeerConnection';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './core/Client';
2
+ export * from './core/Connection';
3
+ export * from './core/Signaling';
4
+ export * from './core/Room';
5
+ export * from './ConnectionManager';
6
+ export * from './api/PlutoWebSocket';
7
+ export * from './api/PlutoWebTransport';
8
+ export * from './api/PlutoPeerConnection';
@@ -0,0 +1,9 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Client, ClientOptions } from '../core/Client';
3
+ export interface PlutoProviderProps {
4
+ client?: Client;
5
+ clientOptions?: ClientOptions;
6
+ children: ReactNode;
7
+ }
8
+ export declare const PlutoProvider: React.FC<PlutoProviderProps>;
9
+ export declare const usePlutoClient: () => Client;
@@ -0,0 +1,34 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+ import { Client } from '../core/Client';
3
+ const PlutoContext = createContext(null);
4
+ export const PlutoProvider = ({ client: existingClient, clientOptions, children }) => {
5
+ const [client, setClient] = useState(existingClient || null);
6
+ useEffect(() => {
7
+ if (existingClient) {
8
+ setClient(existingClient);
9
+ return;
10
+ }
11
+ if (clientOptions) {
12
+ const newClient = new Client(clientOptions);
13
+ newClient.init().then(() => {
14
+ setClient(newClient);
15
+ });
16
+ return () => {
17
+ // Should we stop listening on unmount?
18
+ // Mostly yes, to cleanup resources in that context context.
19
+ newClient.stopListening();
20
+ };
21
+ }
22
+ }, [existingClient, clientOptions]);
23
+ if (!client) {
24
+ return null; // Or some loading state? null is safer for now.
25
+ }
26
+ return (React.createElement(PlutoContext.Provider, { value: client }, children));
27
+ };
28
+ export const usePlutoClient = () => {
29
+ const context = useContext(PlutoContext);
30
+ if (!context) {
31
+ throw new Error("usePlutoClient must be used within a PlutoProvider");
32
+ }
33
+ return context;
34
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "pluto-rtc",
3
+ "version": "0.0.2",
4
+ "private": false,
5
+ "description": "A WebRTC library for Plutonium, supporting secure P2P communication.",
6
+ "keywords": [
7
+ "webrtc",
8
+ "p2p",
9
+ "networking",
10
+ "plutonium",
11
+ "iroh"
12
+ ],
13
+ "author": "Bryant Hargreaves",
14
+ "license": "MIT",
15
+ "homepage": "https://plutonium.hargreaves.dev",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/bluestarburst/Plutonium.git"
19
+ },
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "wasm"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "build:wasm": "export CC=/opt/homebrew/opt/llvm/bin/clang && export AR=/opt/homebrew/opt/llvm/bin/llvm-ar && cd ../src-tauri/crates/desktop/iroh-wasm && wasm-pack build --target web --out-dir ../../../../pluto-rtc/wasm/pkg && rm -rf ../../../../pluto-rtc/wasm/pkg/.gitignore",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "coverage": "vitest run --coverage",
32
+ "prepublishOnly": "npm run build:wasm && npm run build"
33
+ },
34
+ "dependencies": {
35
+ "firebase": "^9.0.0 || ^10.0.0 || ^11.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "firebase": "^9.0.0 || ^10.0.0 || ^11.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "@vitest/coverage-v8": "^4.0.18",
43
+ "happy-dom": "^20.5.0",
44
+ "typescript": "^5.0.0",
45
+ "vitest": "^4.0.18"
46
+ }
47
+ }