/ demo.html
demo.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>P2P Protocol Stack - Multi-Peer Demo</title> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 } 14 15 body { 16 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 17 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 18 min-height: 100vh; 19 padding: 2rem; 20 color: #333; 21 } 22 23 .container { 24 max-width: 1400px; 25 margin: 0 auto; 26 } 27 28 header { 29 text-align: center; 30 margin-bottom: 2rem; 31 color: white; 32 } 33 34 h1 { 35 font-size: 2.5rem; 36 font-weight: 800; 37 margin-bottom: 0.5rem; 38 text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 39 } 40 41 .subtitle { 42 font-size: 1.1rem; 43 opacity: 0.9; 44 } 45 46 .peer-info { 47 background: white; 48 padding: 1.5rem; 49 border-radius: 12px; 50 margin-bottom: 1.5rem; 51 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 52 } 53 54 .peer-id { 55 font-family: 'Monaco', monospace; 56 font-size: 0.875rem; 57 color: #667eea; 58 word-break: break-all; 59 } 60 61 .grid { 62 display: grid; 63 grid-template-columns: 1fr 1fr; 64 gap: 1.5rem; 65 margin-bottom: 1.5rem; 66 } 67 68 @media (max-width: 768px) { 69 .grid { 70 grid-template-columns: 1fr; 71 } 72 } 73 74 .panel { 75 background: white; 76 padding: 1.5rem; 77 border-radius: 12px; 78 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 79 } 80 81 .panel h2 { 82 font-size: 1.5rem; 83 margin-bottom: 1rem; 84 color: #333; 85 display: flex; 86 align-items: center; 87 gap: 0.5rem; 88 } 89 90 .button-group { 91 display: flex; 92 gap: 0.75rem; 93 flex-wrap: wrap; 94 margin-bottom: 1rem; 95 } 96 97 button { 98 padding: 0.625rem 1.25rem; 99 border: none; 100 border-radius: 8px; 101 font-size: 0.9rem; 102 font-weight: 600; 103 cursor: pointer; 104 transition: all 0.2s; 105 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 106 color: white; 107 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 108 } 109 110 button:hover { 111 transform: translateY(-2px); 112 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 113 } 114 115 button:active { 116 transform: translateY(0); 117 } 118 119 button.secondary { 120 background: #6b7280; 121 } 122 123 button.danger { 124 background: #ef4444; 125 } 126 127 .output { 128 background: #f7fafc; 129 border: 1px solid #e2e8f0; 130 border-radius: 8px; 131 padding: 1rem; 132 font-family: 'Monaco', 'Courier New', monospace; 133 font-size: 0.8rem; 134 max-height: 300px; 135 overflow-y: auto; 136 white-space: pre-wrap; 137 word-break: break-all; 138 } 139 140 .log-entry { 141 margin-bottom: 0.5rem; 142 padding: 0.5rem; 143 border-left: 3px solid #667eea; 144 background: white; 145 } 146 147 .timestamp { 148 color: #999; 149 font-size: 0.7rem; 150 } 151 152 .success { 153 color: #10b981; 154 } 155 156 .error { 157 color: #ef4444; 158 } 159 160 .info { 161 color: #3b82f6; 162 } 163 164 .warning { 165 color: #f59e0b; 166 } 167 168 .peers-list { 169 list-style: none; 170 } 171 172 .peer-item { 173 padding: 0.75rem; 174 margin-bottom: 0.5rem; 175 background: #f7fafc; 176 border-radius: 6px; 177 border-left: 3px solid #667eea; 178 } 179 180 .peer-item.online { 181 border-left-color: #10b981; 182 } 183 184 .peer-name { 185 font-weight: 600; 186 margin-bottom: 0.25rem; 187 } 188 189 .peer-did { 190 font-family: 'Monaco', monospace; 191 font-size: 0.75rem; 192 color: #666; 193 } 194 195 .topics-list { 196 list-style: none; 197 } 198 199 .topic-item { 200 padding: 0.75rem; 201 margin-bottom: 0.5rem; 202 background: #f7fafc; 203 border-radius: 6px; 204 display: flex; 205 justify-content: space-between; 206 align-items: center; 207 } 208 209 .topic-name { 210 font-family: 'Monaco', monospace; 211 font-size: 0.85rem; 212 color: #667eea; 213 } 214 215 .subscriber-count { 216 background: #667eea; 217 color: white; 218 padding: 0.25rem 0.5rem; 219 border-radius: 999px; 220 font-size: 0.75rem; 221 } 222 223 .badge { 224 display: inline-block; 225 padding: 0.25rem 0.5rem; 226 border-radius: 4px; 227 font-size: 0.75rem; 228 font-weight: 600; 229 margin-left: 0.5rem; 230 } 231 232 .badge-success { 233 background: #d1fae5; 234 color: #065f46; 235 } 236 237 .badge-warning { 238 background: #fef3c7; 239 color: #92400e; 240 } 241 242 .stats { 243 display: grid; 244 grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); 245 gap: 1rem; 246 margin-top: 1rem; 247 } 248 249 .stat { 250 text-align: center; 251 padding: 1rem; 252 background: #f7fafc; 253 border-radius: 8px; 254 } 255 256 .stat-value { 257 font-size: 1.75rem; 258 font-weight: 700; 259 color: #667eea; 260 } 261 262 .stat-label { 263 font-size: 0.75rem; 264 color: #666; 265 text-transform: uppercase; 266 letter-spacing: 0.05em; 267 margin-top: 0.25rem; 268 } 269 270 @keyframes pulse { 271 272 0%, 273 100% { 274 opacity: 1; 275 } 276 277 50% { 278 opacity: 0.5; 279 } 280 } 281 282 .pulse { 283 animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 284 } 285 </style> 286 </head> 287 288 <body> 289 <div class="container"> 290 <header> 291 <h1>🚀 P2P Protocol Stack - Multi-Peer Demo</h1> 292 <p class="subtitle">Open multiple browser windows to see peers communicate!</p> 293 </header> 294 295 <div class="peer-info"> 296 <strong>Your Peer ID:</strong> 297 <div class="peer-id" id="myPeerId">Initializing...</div> 298 </div> 299 300 <div class="grid"> 301 <!-- Left Column --> 302 <div> 303 <div class="panel"> 304 <h2>🔐 Identity & Discovery</h2> 305 <div class="button-group"> 306 <button onclick="demo.createIdentity()">New Identity</button> 307 <button onclick="demo.discoverPeers()">Discover Peers</button> 308 <button onclick="demo.announcePresence()">Announce Presence</button> 309 </div> 310 311 <h3 style="font-size: 1rem; margin: 1rem 0 0.5rem 0;">Online Peers:</h3> 312 <ul class="peers-list" id="peersList"> 313 <li style="color: #999; font-size: 0.875rem;">No peers discovered yet</li> 314 </ul> 315 </div> 316 317 <div class="panel" style="margin-top: 1.5rem;"> 318 <h2>💾 Content & Storage</h2> 319 <div class="button-group"> 320 <button onclick="demo.createContent()">Create Content</button> 321 <button onclick="demo.announceContent()">Announce Content</button> 322 <button onclick="demo.findContent()">Find Content</button> 323 </div> 324 325 <div class="stats"> 326 <div class="stat"> 327 <div class="stat-value" id="contentCount">0</div> 328 <div class="stat-label">Content Items</div> 329 </div> 330 <div class="stat"> 331 <div class="stat-value" id="announcedCount">0</div> 332 <div class="stat-label">Announced</div> 333 </div> 334 </div> 335 </div> 336 </div> 337 338 <!-- Right Column --> 339 <div> 340 <div class="panel"> 341 <h2>📡 Topics & PubSub</h2> 342 <div class="button-group"> 343 <button onclick="demo.createTopic()">Create Topic</button> 344 <button onclick="demo.publishMessage()">Publish Message</button> 345 <button onclick="demo.subscribeToTopic()">Subscribe to Peer</button> 346 </div> 347 348 <h3 style="font-size: 1rem; margin: 1rem 0 0.5rem 0;">Active Topics:</h3> 349 <ul class="topics-list" id="topicsList"> 350 <li style="color: #999; font-size: 0.875rem;">No topics yet</li> 351 </ul> 352 </div> 353 354 <div class="panel" style="margin-top: 1.5rem;"> 355 <h2>🔄 Collaboration</h2> 356 <div class="button-group"> 357 <button onclick="demo.createDocument()">Create Document</button> 358 <button onclick="demo.editDocument()">Edit Document</button> 359 <button onclick="demo.shareDocument()">Share with Peer</button> 360 </div> 361 362 <div class="stats"> 363 <div class="stat"> 364 <div class="stat-value" id="documentCount">0</div> 365 <div class="stat-label">Documents</div> 366 </div> 367 <div class="stat"> 368 <div class="stat-value" id="editsCount">0</div> 369 <div class="stat-label">Edits</div> 370 </div> 371 </div> 372 </div> 373 </div> 374 </div> 375 376 <div class="panel"> 377 <h2>📝 Activity Log</h2> 378 <div class="button-group"> 379 <button class="secondary" onclick="demo.clearLog()">Clear Log</button> 380 <button class="danger" onclick="demo.reset()">Reset Everything</button> 381 </div> 382 <div id="output" class="output"> 383 <div class="log-entry"> 384 <span class="timestamp">[Ready]</span> 385 <span class="info">Multi-peer demo initialized. Open this page in multiple windows!</span> 386 </div> 387 </div> 388 </div> 389 </div> 390 391 <script type="module"> 392 class MultiPeerDemo { 393 constructor() { 394 this.peerId = this.generatePeerId(); 395 this.peers = new Map(); 396 this.content = new Map(); 397 this.topics = new Map(); 398 this.documents = new Map(); 399 this.stats = { 400 content: 0, 401 announced: 0, 402 documents: 0, 403 edits: 0 404 }; 405 406 this.initializeStorage(); 407 this.startListening(); 408 this.updateUI(); 409 410 document.getElementById('myPeerId').textContent = this.peerId; 411 this.log('Peer initialized: ' + this.peerId, 'success'); 412 this.log('Open this page in multiple windows to see P2P in action!', 'info'); 413 } 414 415 generatePeerId() { 416 return 'peer-' + Math.random().toString(36).substring(2, 10); 417 } 418 419 initializeStorage() { 420 // Initialize localStorage for cross-window communication 421 if (!localStorage.getItem('p2p-network')) { 422 localStorage.setItem('p2p-network', JSON.stringify({ 423 peers: {}, 424 content: {}, 425 topics: {}, 426 messages: [] 427 })); 428 } 429 } 430 431 startListening() { 432 // Listen for storage events (cross-window communication) 433 window.addEventListener('storage', (e) => { 434 if (e.key === 'p2p-network') { 435 this.handleNetworkUpdate(); 436 } 437 }); 438 439 // Poll for updates every 500ms 440 setInterval(() => this.handleNetworkUpdate(), 500); 441 } 442 443 handleNetworkUpdate() { 444 const network = JSON.parse(localStorage.getItem('p2p-network') || '{}'); 445 446 // Update peers 447 if (network.peers) { 448 Object.entries(network.peers).forEach(([id, peer]) => { 449 if (id !== this.peerId && !this.peers.has(id)) { 450 this.peers.set(id, peer); 451 this.log(`Peer discovered: ${id}`, 'success'); 452 } 453 }); 454 } 455 456 // Process new messages 457 if (network.messages) { 458 network.messages.forEach(msg => { 459 if (msg.to === this.peerId && !msg.processed) { 460 this.handleMessage(msg); 461 msg.processed = true; 462 } 463 }); 464 localStorage.setItem('p2p-network', JSON.stringify(network)); 465 } 466 467 this.updateUI(); 468 } 469 470 handleMessage(msg) { 471 switch (msg.type) { 472 case 'topic-message': 473 this.log(`📨 Received on topic "${msg.topic}": ${msg.content}`, 'info'); 474 break; 475 case 'content-share': 476 this.log(`📦 Received content: ${msg.cid}`, 'success'); 477 this.content.set(msg.cid, msg.data); 478 this.stats.content++; 479 break; 480 case 'document-edit': 481 this.log(`✏️ Document edited by ${msg.from}: ${msg.edit}`, 'info'); 482 this.stats.edits++; 483 break; 484 } 485 } 486 487 broadcastToNetwork(data) { 488 const network = JSON.parse(localStorage.getItem('p2p-network') || '{}'); 489 network.messages = network.messages || []; 490 network.messages.push({ 491 ...data, 492 from: this.peerId, 493 timestamp: Date.now(), 494 processed: false 495 }); 496 localStorage.setItem('p2p-network', JSON.stringify(network)); 497 } 498 499 updateUI() { 500 // Update peers list 501 const peersList = document.getElementById('peersList'); 502 if (this.peers.size === 0) { 503 peersList.innerHTML = '<li style="color: #999; font-size: 0.875rem;">No peers discovered yet</li>'; 504 } else { 505 peersList.innerHTML = Array.from(this.peers.entries()) 506 .map(([id, peer]) => ` 507 <li class="peer-item online"> 508 <div class="peer-name">${id} <span class="badge badge-success">Online</span></div> 509 <div class="peer-did">${peer.did || 'did:key:z6Mk...'}</div> 510 </li> 511 `).join(''); 512 } 513 514 // Update topics list 515 const topicsList = document.getElementById('topicsList'); 516 if (this.topics.size === 0) { 517 topicsList.innerHTML = '<li style="color: #999; font-size: 0.875rem;">No topics yet</li>'; 518 } else { 519 topicsList.innerHTML = Array.from(this.topics.entries()) 520 .map(([name, topic]) => ` 521 <li class="topic-item"> 522 <span class="topic-name">${name}</span> 523 <span class="subscriber-count">${topic.subscribers || 0} subscribers</span> 524 </li> 525 `).join(''); 526 } 527 528 // Update stats 529 document.getElementById('contentCount').textContent = this.stats.content; 530 document.getElementById('announcedCount').textContent = this.stats.announced; 531 document.getElementById('documentCount').textContent = this.stats.documents; 532 document.getElementById('editsCount').textContent = this.stats.edits; 533 } 534 535 log(message, type = 'info') { 536 const output = document.getElementById('output'); 537 const timestamp = new Date().toLocaleTimeString(); 538 const entry = document.createElement('div'); 539 entry.className = 'log-entry'; 540 entry.innerHTML = ` 541 <span class="timestamp">[${timestamp}]</span> 542 <span class="${type}">${message}</span> 543 `; 544 output.appendChild(entry); 545 output.scrollTop = output.scrollHeight; 546 } 547 548 clearLog() { 549 document.getElementById('output').innerHTML = ''; 550 this.log('Log cleared', 'info'); 551 } 552 553 async sleep(ms) { 554 return new Promise(resolve => setTimeout(resolve, ms)); 555 } 556 557 // === Identity & Discovery === 558 559 async createIdentity() { 560 this.log('Generating new ed25519 keypair...', 'info'); 561 await this.sleep(300); 562 563 const did = 'did:key:z6Mk' + Math.random().toString(36).substring(2, 15); 564 this.log(`✓ New identity created: ${did}`, 'success'); 565 566 return did; 567 } 568 569 async announcePresence() { 570 this.log('Announcing presence to network...', 'info'); 571 await this.sleep(200); 572 573 const network = JSON.parse(localStorage.getItem('p2p-network') || '{}'); 574 network.peers = network.peers || {}; 575 network.peers[this.peerId] = { 576 did: 'did:key:z6Mk' + Math.random().toString(36).substring(2, 10), 577 lastSeen: Date.now() 578 }; 579 localStorage.setItem('p2p-network', JSON.stringify(network)); 580 581 this.log(`✓ Presence announced as ${this.peerId}`, 'success'); 582 } 583 584 async discoverPeers() { 585 this.log('Querying DHT for peers...', 'info'); 586 await this.sleep(400); 587 588 const network = JSON.parse(localStorage.getItem('p2p-network') || '{}'); 589 const peerCount = Object.keys(network.peers || {}).length - 1; // Exclude self 590 591 this.log(`✓ Found ${peerCount} peer(s) in network`, 'success'); 592 if (peerCount === 0) { 593 this.log('💡 Tip: Open this page in another window to create more peers!', 'warning'); 594 } 595 } 596 597 // === Content & Storage === 598 599 async createContent() { 600 this.log('Creating content block...', 'info'); 601 await this.sleep(300); 602 603 const content = { 604 message: 'Hello from ' + this.peerId, 605 timestamp: Date.now() 606 }; 607 608 const cid = 'bafybei' + Math.random().toString(36).substring(2, 15); 609 this.content.set(cid, content); 610 this.stats.content++; 611 612 this.log(`✓ Content created with CID: ${cid}`, 'success'); 613 this.updateUI(); 614 615 return cid; 616 } 617 618 async announceContent() { 619 if (this.content.size === 0) { 620 this.log('No content to announce. Create content first!', 'warning'); 621 return; 622 } 623 624 this.log('Announcing content to DHT...', 'info'); 625 await this.sleep(300); 626 627 const network = JSON.parse(localStorage.getItem('p2p-network') || '{}'); 628 network.content = network.content || {}; 629 630 this.content.forEach((data, cid) => { 631 network.content[cid] = { 632 provider: this.peerId, 633 timestamp: Date.now() 634 }; 635 }); 636 637 localStorage.setItem('p2p-network', JSON.stringify(network)); 638 this.stats.announced = this.content.size; 639 640 this.log(`✓ Announced ${this.content.size} content item(s)`, 'success'); 641 this.updateUI(); 642 } 643 644 async findContent() { 645 this.log('Searching DHT for content...', 'info'); 646 await this.sleep(400); 647 648 const network = JSON.parse(localStorage.getItem('p2p-network') || '{}'); 649 const contentCount = Object.keys(network.content || {}).length; 650 651 this.log(`✓ Found ${contentCount} content item(s) in network`, 'success'); 652 653 if (contentCount > 0) { 654 Object.entries(network.content).forEach(([cid, info]) => { 655 this.log(` 📦 ${cid} (provider: ${info.provider})`, 'info'); 656 }); 657 } 658 } 659 660 // === Topics & PubSub === 661 662 async createTopic() { 663 this.log('Creating DID-rooted topic...', 'info'); 664 await this.sleep(300); 665 666 const topicName = `${this.peerId}/public/messages`; 667 this.topics.set(topicName, { 668 subscribers: 1, 669 messages: [] 670 }); 671 672 this.log(`✓ Topic created: ${topicName}`, 'success'); 673 this.log('Other peers can now subscribe to this topic', 'info'); 674 this.updateUI(); 675 676 return topicName; 677 } 678 679 async publishMessage() { 680 if (this.topics.size === 0) { 681 await this.createTopic(); 682 } 683 684 const topic = Array.from(this.topics.keys())[0]; 685 const message = `Hello at ${new Date().toLocaleTimeString()}`; 686 687 this.log(`Publishing to topic: ${topic}`, 'info'); 688 await this.sleep(200); 689 690 // Broadcast to all peers 691 this.broadcastToNetwork({ 692 type: 'topic-message', 693 topic: topic, 694 content: message, 695 to: 'all' 696 }); 697 698 this.log(`✓ Message published: "${message}"`, 'success'); 699 } 700 701 async subscribeToTopic() { 702 if (this.peers.size === 0) { 703 this.log('No peers available. Discover peers first!', 'warning'); 704 return; 705 } 706 707 const peerId = Array.from(this.peers.keys())[0]; 708 const topic = `${peerId}/public/messages`; 709 710 this.log(`Subscribing to ${peerId}'s topic...`, 'info'); 711 await this.sleep(300); 712 713 this.topics.set(topic, { subscribers: 1, messages: [] }); 714 this.log(`✓ Subscribed to: ${topic}`, 'success'); 715 this.log('You will receive messages published to this topic', 'info'); 716 this.updateUI(); 717 } 718 719 // === Collaboration === 720 721 async createDocument() { 722 this.log('Creating CRDT document...', 'info'); 723 await this.sleep(400); 724 725 const docId = 'doc-' + Math.random().toString(36).substring(2, 10); 726 this.documents.set(docId, { 727 title: 'Collaborative Document', 728 content: '', 729 author: this.peerId, 730 edits: [] 731 }); 732 this.stats.documents++; 733 734 this.log(`✓ Document created: ${docId}`, 'success'); 735 this.log('Using Automerge for conflict-free merging', 'info'); 736 this.updateUI(); 737 738 return docId; 739 } 740 741 async editDocument() { 742 if (this.documents.size === 0) { 743 await this.createDocument(); 744 } 745 746 const docId = Array.from(this.documents.keys())[0]; 747 const edit = `Edit by ${this.peerId} at ${new Date().toLocaleTimeString()}`; 748 749 this.log(`Editing document: ${docId}`, 'info'); 750 await this.sleep(300); 751 752 const doc = this.documents.get(docId); 753 doc.content += edit + '\n'; 754 doc.edits.push(edit); 755 this.stats.edits++; 756 757 // Broadcast edit to collaborators 758 this.broadcastToNetwork({ 759 type: 'document-edit', 760 docId: docId, 761 edit: edit, 762 to: 'all' 763 }); 764 765 this.log(`✓ Document edited (${doc.edits.length} total edits)`, 'success'); 766 this.updateUI(); 767 } 768 769 async shareDocument() { 770 if (this.documents.size === 0) { 771 this.log('No documents to share. Create a document first!', 'warning'); 772 return; 773 } 774 775 if (this.peers.size === 0) { 776 this.log('No peers to share with. Discover peers first!', 'warning'); 777 return; 778 } 779 780 const docId = Array.from(this.documents.keys())[0]; 781 const peerId = Array.from(this.peers.keys())[0]; 782 783 this.log(`Sharing document ${docId} with ${peerId}...`, 'info'); 784 await this.sleep(400); 785 786 this.log(`✓ Document shared via UCAN capability`, 'success'); 787 this.log(`${peerId} can now collaborate on this document`, 'info'); 788 } 789 790 reset() { 791 if (confirm('Reset everything? This will clear all data for all peers.')) { 792 localStorage.removeItem('p2p-network'); 793 this.initializeStorage(); 794 this.peers.clear(); 795 this.content.clear(); 796 this.topics.clear(); 797 this.documents.clear(); 798 this.stats = { content: 0, announced: 0, documents: 0, edits: 0 }; 799 this.clearLog(); 800 this.log('Everything reset!', 'success'); 801 this.updateUI(); 802 } 803 } 804 } 805 806 // Initialize demo 807 window.demo = new MultiPeerDemo(); 808 </script> 809 </body> 810 811 </html>