peers syncing for a single user
This commit is contained in:
45
db.js
45
db.js
@@ -3,6 +3,7 @@
|
|||||||
// name: string;
|
// name: string;
|
||||||
// email: string;
|
// email: string;
|
||||||
// }
|
// }
|
||||||
|
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
|
||||||
const postStoreName = "posts";
|
const postStoreName = "posts";
|
||||||
let keyBase = "dandelion_posts_v1_";
|
let keyBase = "dandelion_posts_v1_";
|
||||||
let key = "";
|
let key = "";
|
||||||
@@ -117,6 +118,50 @@ export async function addDataArray(userID, array) {
|
|||||||
console.error('Error in opening database:', error);
|
console.error('Error in opening database:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function mergeDataArray(userID, array) {
|
||||||
|
try {
|
||||||
|
const db = await openDatabase(userID);
|
||||||
|
const transaction = db.transaction(postStoreName, "readwrite");
|
||||||
|
const store = transaction.objectStore(postStoreName);
|
||||||
|
const index = store.index("postIDIndex");
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
console.log("Transaction completed successfully");
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
transaction.onerror = (event) => {
|
||||||
|
console.error("Transaction error:", event.target.error);
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
let postsToWrite = [];
|
||||||
|
for (let post of array) {
|
||||||
|
try {
|
||||||
|
let havePost = await new Promise((resolve, reject) => {
|
||||||
|
const getRequest = index.getKey(post.post_id);
|
||||||
|
getRequest.onerror = (e) => {
|
||||||
|
console.log(e.target.error);
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
getRequest.onsuccess = async (e) => {
|
||||||
|
const key = e.target.result;
|
||||||
|
resolve(key !== undefined);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// console.log(post.post_id, havePost);
|
||||||
|
if (!havePost) {
|
||||||
|
postsToWrite.push(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error processing post:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Writing ${postsToWrite.length} posts`);
|
||||||
|
await addDataArray(userID, postsToWrite);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error in opening database:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function getData(userID, lowerID, upperID) {
|
export async function getData(userID, lowerID, upperID) {
|
||||||
const db = await openDatabase(userID);
|
const db = await openDatabase(userID);
|
||||||
const transaction = db.transaction(postStoreName, "readonly");
|
const transaction = db.transaction(postStoreName, "readonly");
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIENTCCAx2gAwIBAgISA0tgMbNwsOJA3iNAkTnMQwBRMA0GCSqGSIb3DQEBCwUA
|
|
||||||
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
|
|
||||||
EwJSMzAeFw0yNDAzMTIwMDAyMDJaFw0yNDA2MTAwMDAyMDFaMCQxIjAgBgNVBAMT
|
|
||||||
GWRhbmRlbGlvbi5hbmRlcmJlcmcuY28udWswWTATBgcqhkjOPQIBBggqhkjOPQMB
|
|
||||||
BwNCAASEd7+0ppvG0iyaAjv8urB8LVYKSIYO8GLp6H9P2fwZjzWXfC8iQMhP9jaW
|
|
||||||
aQaAl73gS4iXpODDzYJ/Hwi6siPto4ICHDCCAhgwDgYDVR0PAQH/BAQDAgeAMB0G
|
|
||||||
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1Ud
|
|
||||||
DgQWBBTSo2KL0pYAUZ1gdOwVAQk8GmoCITAfBgNVHSMEGDAWgBQULrMXt1hWy65Q
|
|
||||||
CUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9y
|
|
||||||
My5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iub3Jn
|
|
||||||
LzAkBgNVHREEHTAbghlkYW5kZWxpb24uYW5kZXJiZXJnLmNvLnVrMBMGA1UdIAQM
|
|
||||||
MAowCAYGZ4EMAQIBMIIBBQYKKwYBBAHWeQIEAgSB9gSB8wDxAHYASLDja9qmRzQP
|
|
||||||
5WoC+p0w6xxSActW3SyB2bu/qznYhHMAAAGOMC5ElAAABAMARzBFAiBpEIL68r0M
|
|
||||||
XSzwppDSd+KLGdfyKzFjYmgdBw29Ol+IywIhANMk6NUGYXEeqhMjLV5ZDj5OG6AF
|
|
||||||
orRzRgxsWtZYteM5AHcAO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcA
|
|
||||||
AAGOMC5EmgAABAMASDBGAiEAl6ot3y1mAFXw3ZNVeJOwAscHc4/d6bCEgaqFlcCy
|
|
||||||
iS8CIQDuYEcvEJV2TyXGFyrqAKlmw7E1WO63OWhc6uE+EwsGAjANBgkqhkiG9w0B
|
|
||||||
AQsFAAOCAQEAVZCdcCiSvDhRkLTPtZUK74RDkH3DJtTM6bVHxdZXHEINlR62LyRo
|
|
||||||
TI76XZXL4iLQWzu3XzPw0X0DyNnhyR4EGsy+yRZEPbBWtCIyz0Mulw+V5vxlLHEe
|
|
||||||
+7z9qus0mVU8Yt0fRHDc7oQEY+hqPIuFl/sekCkqL16Oy+kSuzNiB71f50fiYsp0
|
|
||||||
jb50fGb1ykcimCwehUQPeKcC9O88eE7Hxk7lD86VV6s/IyrfKze+AfvRcYGpGYWg
|
|
||||||
PB/v71LjtKrLNcPggQ0DQD4kRhR3/NqU+Tp6h17sb7WYLH0IgTc0VoSyNQg/rWet
|
|
||||||
g9zvvsrT6zrzSxcbocs4SVNFGF/mfv3WKw==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
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
|
|
||||||
nLRbwHOoq7hHwg==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
163
index.html
163
index.html
@@ -14,154 +14,7 @@
|
|||||||
|
|
||||||
<!-- <script src="/lib/webtorrent/webtorrent_1_8_0.min.js"></script> -->
|
<!-- <script src="/lib/webtorrent/webtorrent_1_8_0.min.js"></script> -->
|
||||||
<link rel="manifest" href="/app.webmanifest">
|
<link rel="manifest" href="/app.webmanifest">
|
||||||
<style>
|
<link rel="stylesheet" href="/main.css">
|
||||||
@font-face {
|
|
||||||
font-family: 'Virgil';
|
|
||||||
/* Define the name you want to reference your font with */
|
|
||||||
src: url('./virgil.woff2') format('woff2');
|
|
||||||
/* URL to the font file and format */
|
|
||||||
font-weight: normal;
|
|
||||||
/* Define the weight of the font */
|
|
||||||
font-style: normal;
|
|
||||||
/* Define the style of the font */
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Bookerly';
|
|
||||||
/* Define the name you want to reference your font with */
|
|
||||||
src: url('./bookerly.woff2') format('woff2');
|
|
||||||
/* URL to the font file and format */
|
|
||||||
font-weight: normal;
|
|
||||||
/* Define the weight of the font */
|
|
||||||
font-style: normal;
|
|
||||||
/* Define the style of the font */
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Bookerly, sans-serif;
|
|
||||||
color: rgb(202, 208, 211);
|
|
||||||
background-color: black;
|
|
||||||
/* Use the font with a fallback */
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border-color: rgb(60, 60, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
#textarea_post {
|
|
||||||
font-size: medium;
|
|
||||||
font-family: Bookerly;
|
|
||||||
background-color: rgb(0, 0, 0);
|
|
||||||
color: rgb(202, 208, 211);
|
|
||||||
width: 100%;
|
|
||||||
/* Make the textarea take up 100% of the body's width */
|
|
||||||
box-sizing: border-box;
|
|
||||||
/* Include padding and border in the element's total width and height */
|
|
||||||
padding-left: 30px;
|
|
||||||
padding-right: 30px;
|
|
||||||
border: 1px solid rgb(132, 136, 138);
|
|
||||||
resize: vertical;
|
|
||||||
/* Allows the textarea to be resized vertically only */
|
|
||||||
border-radius: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 0px;
|
|
||||||
/* Add some padding around the flex container */
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
max-width: 600px;
|
|
||||||
/* Your preferred max width for the content */
|
|
||||||
flex: 1;
|
|
||||||
/* Shorthand for flex-grow, flex-shrink and flex-basis */
|
|
||||||
min-width: 300px;
|
|
||||||
/* Minimum width the content can shrink to */
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 0 5px rgb(60, 60, 60);
|
|
||||||
text-align: left;
|
|
||||||
overflow-x: hidden;
|
|
||||||
/* Hide horizontal overflow inside the flex container */
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed {
|
|
||||||
max-width: 800px;
|
|
||||||
border-color: red;
|
|
||||||
border: 1px, solid;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.postImage {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#log {
|
|
||||||
font-family: monospace;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
font-size: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
height: 150px;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buttons {
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#button_post {
|
|
||||||
margin-right:40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgb(29, 155, 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|
||||||
#torrent-content {
|
|
||||||
border:solid 1px;
|
|
||||||
width:800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: rgb(0, 0, 0);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding-left:10px;
|
|
||||||
padding-right:10px;
|
|
||||||
padding-top:5px;
|
|
||||||
padding-bottom:5px;
|
|
||||||
margin-left:5px;
|
|
||||||
color:rgb(255,255,255);
|
|
||||||
/* border:solid 1px white; */
|
|
||||||
border: 1px solid rgb(132, 136, 138);
|
|
||||||
color: rgb(202, 208, 211);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -174,15 +27,23 @@
|
|||||||
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
|
|
||||||
<div id="info" style="display:none">
|
<div id="info" style="display:block">
|
||||||
|
<div id="profile">
|
||||||
|
<span class="form_label">username:</span><span class="form_field" id="username" contenteditable="true">unnamed</span>
|
||||||
|
</div>
|
||||||
|
<div id="following">
|
||||||
|
<div>fiona</div>
|
||||||
|
<div>fiona</div>
|
||||||
|
</div>
|
||||||
<div id="log" ></div>
|
<div id="log" ></div>
|
||||||
|
<div id="connectURL"></div>
|
||||||
<div id="qrcode"></div>
|
<div id="qrcode"></div>
|
||||||
<!-- <dotlottie-player src="https://lottie.host/272b60dd-462d-42a3-8ed6-fec4143633d6/X4FxBascRI.json" background="transparent" speed="1" style="width: 300px; height: 300px" direction="1" playMode="normal" loop controls autoplay></dotlottie-player> -->
|
<!-- <dotlottie-player src="https://lottie.host/272b60dd-462d-42a3-8ed6-fec4143633d6/X4FxBascRI.json" background="transparent" speed="1" style="width: 300px; height: 300px" direction="1" playMode="normal" loop controls autoplay></dotlottie-player> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- <div id="peer_display"><canvas></canvas></div> -->
|
<!-- <div id="peer_display"><canvas></canvas></div> -->
|
||||||
<div id="buttons">
|
<div id="buttons">
|
||||||
<button id="button_font1" >font1</button>
|
<!-- <button id="button_font1" >font1</button>
|
||||||
<button id="button_font2" >font2 </button>
|
<button id="button_font2" >font2 </button> -->
|
||||||
<button id="import_tweets" >import</button>
|
<button id="import_tweets" >import</button>
|
||||||
<button id="clear_posts" >clear </button>
|
<button id="clear_posts" >clear </button>
|
||||||
<button id="update_app" >check for updates</button>
|
<button id="update_app" >check for updates</button>
|
||||||
|
|||||||
322
main.go
322
main.go
@@ -1,58 +1,20 @@
|
|||||||
// TODO
|
|
||||||
// 1. Convery network messages to flatbuffers so we don't need to parse JSON in Go.
|
|
||||||
// Use binary WS messages. Generate for Go and JS and use that everywhere. We can
|
|
||||||
// also use this for WebRTC messages.
|
|
||||||
// 2. Keep a list of all nodes that bootstrapped in the last N minutes.
|
|
||||||
// 3. When a node bootstraps, send it a random list of the nodes we know about.
|
|
||||||
// 4. Do the signalling to connect those nodes.
|
|
||||||
// 5. Each node will know about N other nodes. To find get the data for a feed,
|
|
||||||
// a node will need to find another node that has that data. To do that we'll need to
|
|
||||||
// implement a search message that is sent to all currently connected nodes, and they
|
|
||||||
// forward to all their nodes, passing back the address of the node that has the data.
|
|
||||||
// Once we find it, we'll do the signalling to connect to it via Web RTC via our existing connected nodes.
|
|
||||||
// -----
|
|
||||||
// Feeds. People can curate feeds which can be any combination of hashtags, serch terms and users.
|
|
||||||
// Invite-only communities. Just block everyone else even if they post to it.
|
|
||||||
// Limit to friends and friends of friends
|
|
||||||
|
|
||||||
// MVP
|
|
||||||
// You connect to the person you want to get the post from to get the post
|
|
||||||
// they give you the post
|
|
||||||
// If they're offline, you can't get their updates.
|
|
||||||
// This is very stupid, but it's simplest thing.
|
|
||||||
// The bootstrap server connects you to them directly via WebRTC
|
|
||||||
// This will make the thing actually function as a little toy for people to play with.
|
|
||||||
// This will let us test whether background tabs respond to webrtc requests.
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
// Need to have identity sorted out
|
|
||||||
// When you read someone's posts, you also cache them locally
|
|
||||||
// cache priority goes mutuals->people you follow->people who you folllow, follow, so you're always
|
|
||||||
// caching your mutual's posts
|
|
||||||
// Posts are samll, so caching per-post will work fine.
|
|
||||||
// Then the process is for the bootstrap server to remember all nodes and what they're caching
|
|
||||||
// This will allow distributed content delivery but put a memory and bendwidth strain on the
|
|
||||||
// bootstrap sever. Look into Web Transport for the raspberry pi overhead. Could buy a few more RPIs
|
|
||||||
// and make a little cluster
|
|
||||||
|
|
||||||
// ✅ Domain name so we can get a certificate and serve HTTPS / HTTP3
|
|
||||||
|
|
||||||
// Think about compiling Typescript on initial access and caching the JS in a service worker
|
|
||||||
// so you don't need a build system to change things.
|
|
||||||
// Think about self-hosting the client so the system can be completely self-hosted
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// "strings"
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
"github.com/andybalholm/brotli"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -61,7 +23,7 @@ import (
|
|||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
return origin == "https://ddlion.net"
|
return origin == "https://ddlion.net" || origin == "https://ddln.app"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +36,7 @@ type Message struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageHandler func([]byte, *websocket.Conn) error
|
type MessageHandler func([]byte, *Peer) ([]byte, error)
|
||||||
|
|
||||||
var messageHandlers = make(map[string]MessageHandler)
|
var messageHandlers = make(map[string]MessageHandler)
|
||||||
|
|
||||||
@@ -82,19 +44,45 @@ func registerHandler(messageType string, handler MessageHandler) {
|
|||||||
messageHandlers[messageType] = handler
|
messageHandlers[messageType] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func dispatchMessage(message []byte, conn *websocket.Conn) error {
|
func dispatchMessage(message []byte, peer *Peer) ([]byte, error) {
|
||||||
var msg Message
|
var msg Message
|
||||||
if err := json.Unmarshal(message, &msg); err != nil {
|
if err := json.Unmarshal(message, &msg); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, ok := messageHandlers[msg.Type]
|
handler, ok := messageHandlers[msg.Type]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("No handler registered for message type: %s", msg.Type)
|
err := fmt.Errorf("no handler registered for message type: %s", msg.Type)
|
||||||
return nil
|
return []byte(fmt.Sprintf(`{"type":"error", "message": "%s"}`, err.Error())), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler(message, conn)
|
return handler(message, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
writeWait = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
send chan []byte
|
||||||
|
lastActive time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePeer(peerID string, peer *Peer) {
|
||||||
|
delete(peerConnections, peerID)
|
||||||
|
|
||||||
|
for userID, peers := range userPeers {
|
||||||
|
delete(peers, peerID)
|
||||||
|
if len(peers) == 0 {
|
||||||
|
delete(userPeers, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(connectionPeers, peer.conn)
|
||||||
|
|
||||||
|
// Close the peer's send channel
|
||||||
|
close(peer.send)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -109,39 +97,93 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
conn.SetCloseHandler(websocketCloseHandler)
|
conn.SetCloseHandler(websocketCloseHandler)
|
||||||
|
|
||||||
|
// Create a Peer object with a buffered channel for sending messages
|
||||||
|
peer := &Peer{
|
||||||
|
conn: conn,
|
||||||
|
send: make(chan []byte, 256),
|
||||||
|
lastActive: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the write loop in a separate goroutine
|
||||||
|
go writePump(peer)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, message, err := conn.ReadMessage()
|
_, message, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("ReadMessage error:", err)
|
log.Println("ReadMessage error:", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
log.Printf("recv: %s", message)
|
|
||||||
|
|
||||||
if err := dispatchMessage(message, conn); err != nil {
|
peer.lastActive = time.Now()
|
||||||
|
|
||||||
|
fmt.Println("ws<-", connectionPeers[conn], ":", string(message[:min(80, len(message))]))
|
||||||
|
|
||||||
|
response, err := dispatchMessage(message, peer)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
log.Printf("Error dispatching message: %v", err)
|
log.Printf("Error dispatching message: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if response != nil {
|
||||||
|
// Send the response to the write loop
|
||||||
|
peer.send <- response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example handlers
|
// Clean up when the connection is closed
|
||||||
func handlePing(message []byte, conn *websocket.Conn) error {
|
close(peer.send)
|
||||||
|
peerID := connectionPeers[peer.conn]
|
||||||
|
if peerID != "" {
|
||||||
|
delete(peerConnections, peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePump(peer *Peer) {
|
||||||
|
defer func() {
|
||||||
|
peer.conn.Close()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message, ok := <-peer.send:
|
||||||
|
if !ok {
|
||||||
|
// Channel closed, close the connection
|
||||||
|
peer.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
fmt.Println("ws->", connectionPeers[peer.conn], ":", string(message[:min(80, len(message))]))
|
||||||
|
|
||||||
|
err := peer.conn.WriteMessage(websocket.TextMessage, message)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WriteMessage error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePing(message []byte, peer *Peer) ([]byte, error) {
|
||||||
var pingMsg struct {
|
var pingMsg struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
PeerID string `json:"peer_id"`
|
PeerID string `json:"peer_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(message, &pingMsg); err != nil {
|
if err := json.Unmarshal(message, &pingMsg); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Printf("Received ping from peer: %s", pingMsg.PeerID)
|
|
||||||
return nil
|
// log.Printf("Received ping from peer: %s", pingMsg.PeerID)
|
||||||
|
|
||||||
|
return []byte(`{"type":"pong"}`), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerSet map[string]struct{}
|
type PeerSet map[string]struct{}
|
||||||
|
|
||||||
var userPeers = make(map[string]PeerSet)
|
var userPeers = make(map[string]PeerSet)
|
||||||
var peerConnections = make(map[string]*websocket.Conn)
|
var peerConnections = make(map[string]*Peer)
|
||||||
|
var connectionPeers = make(map[*websocket.Conn]string)
|
||||||
|
|
||||||
func handleHello(message []byte, conn *websocket.Conn) error {
|
func handleHello(message []byte, peer *Peer) ([]byte, error) {
|
||||||
|
|
||||||
var m struct {
|
var m struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -150,32 +192,65 @@ func handleHello(message []byte, conn *websocket.Conn) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(message, &m); err != nil {
|
if err := json.Unmarshal(message, &m); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// log.Printf("Received hello from peer: %s, user:%s", m.PeerID, m.UserID)
|
||||||
if userPeers[m.UserID] == nil {
|
if userPeers[m.UserID] == nil {
|
||||||
userPeers[m.UserID] = make(PeerSet)
|
userPeers[m.UserID] = make(PeerSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
userPeers[m.UserID][m.PeerID] = struct{}{}
|
userPeers[m.UserID][m.PeerID] = struct{}{}
|
||||||
peerConnections[m.PeerID] = conn
|
peerConnections[m.PeerID] = peer
|
||||||
|
connectionPeers[peer.conn] = m.PeerID
|
||||||
|
|
||||||
jsonData, _ := json.MarshalIndent(userPeers, "", " ")
|
jsonData, _ := json.MarshalIndent(userPeers, "", " ")
|
||||||
fmt.Println(string(jsonData), peerConnections)
|
fmt.Println(string(jsonData), peerConnections)
|
||||||
|
|
||||||
log.Printf("Received connect from peer: %s, user:%s", m.PeerID, m.UserID)
|
// return all the peers we know about, with their user_id and peer_id
|
||||||
return nil
|
|
||||||
|
return []byte(fmt.Sprintf(`{"type":"hello", "userPeers": %s}`, string(jsonData))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoggingHandler logs requests and delegates them to the underlying handler.
|
func handlePeerMessage(message []byte, peer *Peer) ([]byte, error) {
|
||||||
// type LoggingHandler struct {
|
|
||||||
// handler http.Handler
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (lh *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
type InnerMessage struct {
|
||||||
// log.Printf("Serving file: %s", r.URL.Path)
|
Type string `json:"type"`
|
||||||
// lh.handler.ServeHTTP(w, r)
|
UserID string `json:"user_id"`
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
type PeerMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
From string `json:"from"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Message InnerMessage `json:"message"`
|
||||||
|
}
|
||||||
|
var m PeerMessage
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("peer message type %s from %s to %s with message length %d\n", m.Message.Type, m.From, m.To, len(message))
|
||||||
|
|
||||||
|
toPeer := peerConnections[m.To]
|
||||||
|
|
||||||
|
if toPeer == nil {
|
||||||
|
fmt.Printf("Couldn't find peer %s\n", m.To)
|
||||||
|
fmt.Println(peerConnections)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message to the recipient's send channel
|
||||||
|
select {
|
||||||
|
case toPeer.send <- message:
|
||||||
|
default:
|
||||||
|
fmt.Println("Could not send message to peer; channel full or closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// No response for this type of message
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BrotliResponseWriter wraps http.ResponseWriter to support Brotli compression
|
// BrotliResponseWriter wraps http.ResponseWriter to support Brotli compression
|
||||||
type brotliResponseWriter struct {
|
type brotliResponseWriter struct {
|
||||||
@@ -207,8 +282,11 @@ func noDirListing(h http.Handler, root string) http.HandlerFunc {
|
|||||||
|
|
||||||
log.Printf("Serving: %s to ip %s, useragent %s", r.URL.Path, r.RemoteAddr, r.UserAgent())
|
log.Printf("Serving: %s to ip %s, useragent %s", r.URL.Path, r.RemoteAddr, r.UserAgent())
|
||||||
|
|
||||||
|
// w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
|
||||||
// Check if client supports Brotli encoding
|
// Check if client supports Brotli encoding
|
||||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
|
// if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
|
||||||
|
if false {
|
||||||
w.Header().Set("Content-Encoding", "br")
|
w.Header().Set("Content-Encoding", "br")
|
||||||
w.Header().Del("Content-Length") // Cannot know content length with compressed data
|
w.Header().Del("Content-Length") // Cannot know content length with compressed data
|
||||||
|
|
||||||
@@ -231,31 +309,115 @@ func noDirListing(h http.Handler, root string) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Create a channel to receive OS signals for graceful shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Create a channel to signal when the program should shut down
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
// Define the directory to serve and the port to listen on
|
||||||
dir := "./"
|
dir := "./"
|
||||||
port := 6789
|
port := 6789
|
||||||
|
|
||||||
addr := ":" + strconv.Itoa(port)
|
addr := ":" + strconv.Itoa(port)
|
||||||
log.Printf("Starting server on %s", addr)
|
log.Printf("Starting server on %s", addr)
|
||||||
|
|
||||||
// Register handlers
|
// Register message handlers
|
||||||
registerHandler("hello", handleHello)
|
registerHandler("hello", handleHello)
|
||||||
registerHandler("ping", handlePing)
|
registerHandler("ping", handlePing)
|
||||||
|
registerHandler("peer_message", handlePeerMessage)
|
||||||
|
|
||||||
// Set up file server and WebSocket endpoint
|
// Set up file server and WebSocket endpoint
|
||||||
fs := http.FileServer(http.Dir(dir))
|
fs := http.FileServer(http.Dir(dir))
|
||||||
// loggingHandler := &LoggingHandler{handler: fs}
|
|
||||||
// http.Handle("/", loggingHandler)
|
|
||||||
http.Handle("/", noDirListing(fs, dir))
|
http.Handle("/", noDirListing(fs, dir))
|
||||||
|
|
||||||
http.HandleFunc("/ws", handleWebSocket)
|
http.HandleFunc("/ws", handleWebSocket)
|
||||||
|
|
||||||
// Configure and start the HTTP server
|
// Configure the HTTP server
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: nil, // nil uses the default ServeMux, which we configured above
|
Handler: nil, // Use the default ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Server is configured and serving on port %d...", port)
|
// Start the inactivity monitor goroutine
|
||||||
log.Fatal(server.ListenAndServeTLS("/etc/letsencrypt/live/ddlion.net/fullchain.pem", "/etc/letsencrypt/live/ddlion.net/privkey.pem"))
|
go func() {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Collect inactive peers
|
||||||
|
var inactivePeers []string
|
||||||
|
for peerID, peer := range peerConnections {
|
||||||
|
if now.Sub(peer.lastActive) > 60*time.Second {
|
||||||
|
inactivePeers = append(inactivePeers, peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove inactive peers
|
||||||
|
for _, peerID := range inactivePeers {
|
||||||
|
peer := peerConnections[peerID]
|
||||||
|
|
||||||
|
if peer != nil {
|
||||||
|
log.Printf("Peer %s inactive for more than 60 seconds. Closing connection.", peerID)
|
||||||
|
peer.conn.Close()
|
||||||
|
removePeer(peerID, peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Run a goroutine to handle graceful shutdown
|
||||||
|
go func() {
|
||||||
|
sig := <-sigChan
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Received signal:", sig)
|
||||||
|
|
||||||
|
// Perform cleanup here
|
||||||
|
fmt.Println("Shutting down gracefully...")
|
||||||
|
|
||||||
|
// Create a context with timeout for the shutdown
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Attempt to gracefully shut down the server
|
||||||
|
if err := server.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatalf("Server Shutdown Failed:%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal that shutdown is complete
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start the HTTP server in a separate goroutine
|
||||||
|
go func() {
|
||||||
|
log.Printf("Server is configured and serving on port %d...", port)
|
||||||
|
if err := server.ListenAndServeTLS(
|
||||||
|
"/etc/letsencrypt/live/ddlion.net/fullchain.pem",
|
||||||
|
"/etc/letsencrypt/live/ddlion.net/privkey.pem",
|
||||||
|
); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("Could not listen on %s: %v\n", addr, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Program is running. Press Ctrl+C to exit.")
|
||||||
|
|
||||||
|
// Wait for the shutdown signal
|
||||||
|
<-done
|
||||||
|
fmt.Println("Program has exited.")
|
||||||
}
|
}
|
||||||
|
|||||||
209
main.js
209
main.js
@@ -1,4 +1,5 @@
|
|||||||
import { getData, addData, addDataArray, clearData, deleteData } from "./db.js";
|
// TODO: virtual list, only rerender what's needed so things can keep playing.
|
||||||
|
import { getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData } from "./db.js";
|
||||||
// let posts:any;
|
// let posts:any;
|
||||||
// let keyBase = "dandelion_posts_v1_"
|
// let keyBase = "dandelion_posts_v1_"
|
||||||
// let key:string = "";
|
// let key:string = "";
|
||||||
@@ -19,6 +20,7 @@ function uuidv4() {
|
|||||||
let logLines = [];
|
let logLines = [];
|
||||||
let logLength = 10;
|
let logLength = 10;
|
||||||
function log(message) {
|
function log(message) {
|
||||||
|
console.log(message);
|
||||||
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
|
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
|
||||||
if (logLines.length > 10) {
|
if (logLines.length > 10) {
|
||||||
logLines = logLines.slice(logLines.length - logLength);
|
logLines = logLines.slice(logLines.length - logLength);
|
||||||
@@ -59,8 +61,9 @@ window.addEventListener('scroll', () => {
|
|||||||
// You can perform your action here
|
// You can perform your action here
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// let peer = await new PeerConnection(peer_id);
|
||||||
// let connectionReply = await wsConnection.send('hello');
|
// let connectionReply = await wsConnection.send('hello');
|
||||||
// for (let peeer of connectionReply) {
|
// for (let peer of connectionReply) {
|
||||||
// let peerConnection = await wsConnection.send('connect', peer.id);
|
// let peerConnection = await wsConnection.send('connect', peer.id);
|
||||||
// if (peerConnection) {
|
// if (peerConnection) {
|
||||||
// this.peers.push(peerConnection);
|
// this.peers.push(peerConnection);
|
||||||
@@ -71,6 +74,67 @@ window.addEventListener('scroll', () => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
class wsConnection {
|
class wsConnection {
|
||||||
|
send(message) {
|
||||||
|
let json = "";
|
||||||
|
try {
|
||||||
|
json = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e, "wsConnection send: Couldn't serialize message", message);
|
||||||
|
}
|
||||||
|
log(`ws->${json.slice(0, 80)}`);
|
||||||
|
this.websocket.send(json);
|
||||||
|
}
|
||||||
|
helloResponseHandler(data) {
|
||||||
|
for (let [userID, peerIDs] of Object.entries(data.userPeers)) {
|
||||||
|
this.peers.set(userID, [...Object.keys(peerIDs)]);
|
||||||
|
for (let peerID of [...Object.keys(peerIDs)]) {
|
||||||
|
if (peerID === this.peerID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.send({
|
||||||
|
type: "peer_message",
|
||||||
|
from: this.peerID,
|
||||||
|
to: peerID,
|
||||||
|
message: { type: "get_posts_for_user", user_id: userID }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pongHandler(data) {
|
||||||
|
}
|
||||||
|
async getPostsForUserResponseHandler(data) {
|
||||||
|
// log(`getPostsForUserResponse: ${data}`)
|
||||||
|
let message = data.message;
|
||||||
|
console.log(`getPostsForUserResponseHandler Got ${message.posts.length} from peer ${data.from}`);
|
||||||
|
for (let post of message.posts) {
|
||||||
|
post.post_timestamp = new Date(post.post_timestamp);
|
||||||
|
}
|
||||||
|
console.log(`Merging same user peer posts...`);
|
||||||
|
await mergeDataArray(message.user_id, data.message.posts);
|
||||||
|
if (message.user_id === this.userID) {
|
||||||
|
app.posts = await app.loadPosts(this.userID) ?? [];
|
||||||
|
app.render(app.posts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getPostsForUserHandler(data) {
|
||||||
|
let message = data.message;
|
||||||
|
let posts = await getAllData(message.user_id) ?? []; // this doesn't get all posts!
|
||||||
|
posts = posts.map((post) => post.data);
|
||||||
|
// let posts = await getAllData(message.user_id) ?? [];
|
||||||
|
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, message: { type: "get_posts_for_user_response", posts: posts, user_id: message.user_id } };
|
||||||
|
this.send(responseMessage);
|
||||||
|
}
|
||||||
|
async peerMessageHandler(data) {
|
||||||
|
log(`peerMessageHandler ${data}`);
|
||||||
|
let peerMessageType = data.message.type;
|
||||||
|
let handler = this.peerMessageHandlers.get(peerMessageType);
|
||||||
|
if (!handler) {
|
||||||
|
console.error(`got peer message type we don't have a handler for: ${peerMessageType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler(data);
|
||||||
|
}
|
||||||
connect() {
|
connect() {
|
||||||
if (this.websocket?.readyState === WebSocket.OPEN) {
|
if (this.websocket?.readyState === WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
@@ -87,24 +151,32 @@ class wsConnection {
|
|||||||
console.log(error.message);
|
console.log(error.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.websocket.onopen = (evt) => {
|
this.websocket.onopen = (event) => {
|
||||||
log("ws:connected");
|
log("ws:connected");
|
||||||
this.websocket.send(`{"type":"hello", "user_id": "${this.userID}", "peer_id":"${this.peerID}"}`);
|
this.send({ type: "hello", user_id: this.userID, peer_id: this.peerID });
|
||||||
this.websocketPingInterval = window.setInterval(() => {
|
this.websocketPingInterval = window.setInterval(() => {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.websocket.send(`{"type":"ping", "peer_id": "${this.peerID}"}`);
|
this.send({ type: "ping", peer_id: this.peerID });
|
||||||
}, 10000);
|
}, 10000);
|
||||||
};
|
};
|
||||||
this.websocket.onclose = (evt) => {
|
this.websocket.onclose = (event) => {
|
||||||
log("ws:disconnected");
|
log("ws:disconnected");
|
||||||
// this.retry *= 2;
|
// this.retry *= 2;
|
||||||
log(`Retrying in ${this.retry} seconds`);
|
log(`Retrying in ${this.retry} seconds`);
|
||||||
window.setTimeout(() => { this.connect(); }, this.retry * 1000);
|
window.setTimeout(() => { this.connect(); }, this.retry * 1000);
|
||||||
};
|
};
|
||||||
this.websocket.onmessage = (event) => {
|
this.websocket.onmessage = (event) => {
|
||||||
log('ws:response: ' + event.data);
|
log('ws:<-' + event.data.slice(0, 80));
|
||||||
|
let data = JSON.parse(event.data);
|
||||||
|
let { type } = data;
|
||||||
|
let handler = this.messageHandlers.get(type);
|
||||||
|
if (!handler) {
|
||||||
|
console.warn(`Got a message we can't handle:`, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler(data);
|
||||||
};
|
};
|
||||||
this.websocket.onerror = (event) => {
|
this.websocket.onerror = (event) => {
|
||||||
log('ws:error: ' + event);
|
log('ws:error: ' + event);
|
||||||
@@ -120,36 +192,28 @@ class wsConnection {
|
|||||||
this.websocketPingInterval = 0;
|
this.websocketPingInterval = 0;
|
||||||
this.retry = 10;
|
this.retry = 10;
|
||||||
this.state = 'disconnected';
|
this.state = 'disconnected';
|
||||||
|
this.peers = new Map();
|
||||||
|
this.messageHandlers = new Map();
|
||||||
|
this.peerMessageHandlers = new Map();
|
||||||
this.userID = userID;
|
this.userID = userID;
|
||||||
this.peerID = peerID;
|
this.peerID = peerID;
|
||||||
|
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
|
||||||
|
this.messageHandlers.set('pong', this.pongHandler);
|
||||||
|
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
|
||||||
|
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
|
||||||
|
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserResponseHandler.bind(this));
|
||||||
this.connect();
|
this.connect();
|
||||||
if (!this.websocket) {
|
if (!this.websocket) {
|
||||||
// set a timer and retry?
|
// set a timer and retry?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// function connectWebsocket(userID: string) {
|
|
||||||
// let websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
|
|
||||||
// websocket.onopen = function (evt) {
|
|
||||||
// log("Websocket: CONNECTED");
|
|
||||||
// websocket.send(`{"messageType":"connect", "id": "${userID}"}`);
|
|
||||||
// let websocketPingInterval = window.setInterval(() => { websocket.send(`{"messageType":"ping", "id": "${userID}"}`); }, 5000)
|
|
||||||
// };
|
|
||||||
// websocket.onclose = function (evt) {
|
|
||||||
// log("Websocket: DISCONNECTED");
|
|
||||||
// };
|
|
||||||
// websocket.onmessage = function (evt) {
|
|
||||||
// log('Websocket: RESPONSE: ' + evt.data);
|
|
||||||
// };
|
|
||||||
// websocket.onerror = function (evt) {
|
|
||||||
// log('Websocket: ERROR: ' + evt);
|
|
||||||
// };
|
|
||||||
// return websocket;
|
|
||||||
// }
|
|
||||||
class App {
|
class App {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.username = '';
|
||||||
this.userID = '';
|
this.userID = '';
|
||||||
this.peerID = '';
|
this.peerID = '';
|
||||||
|
this.posts = [];
|
||||||
this.time = 0;
|
this.time = 0;
|
||||||
}
|
}
|
||||||
initMarkdown() {
|
initMarkdown() {
|
||||||
@@ -207,9 +271,9 @@ class App {
|
|||||||
// let tweets = JSON.parse(tweetJSON);
|
// let tweets = JSON.parse(tweetJSON);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (let entry of tweetArchive) {
|
for (let entry of tweetArchive) {
|
||||||
if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
|
// if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
let mediaURL = entry.tweet?.entities?.media?.[0]?.media_url_https;
|
let mediaURL = entry.tweet?.entities?.media?.[0]?.media_url_https;
|
||||||
let isImage = false;
|
let isImage = false;
|
||||||
if (mediaURL) {
|
if (mediaURL) {
|
||||||
@@ -266,16 +330,16 @@ class App {
|
|||||||
console.error("Service Worker registration failed:", error);
|
console.error("Service Worker registration failed:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addPost(userID, posts, postText) {
|
addPost(userID, postText) {
|
||||||
if ((typeof postText !== "string") || postText.length === 0) {
|
if ((typeof postText !== "string") || postText.length === 0) {
|
||||||
log("Not posting an empty string...");
|
log("Not posting an empty string...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let post = new Post(`bobbydigitales`, userID, postText, new Date());
|
let post = new Post(this.username, userID, postText, new Date());
|
||||||
posts.push(post);
|
this.posts.push(post);
|
||||||
// localStorage.setItem(key, JSON.stringify(posts));
|
// localStorage.setItem(key, JSON.stringify(posts));
|
||||||
addData(userID, post);
|
addData(userID, post);
|
||||||
this.render(posts);
|
this.render(this.posts);
|
||||||
}
|
}
|
||||||
getPeerID() {
|
getPeerID() {
|
||||||
let id = localStorage.getItem("peer_id");
|
let id = localStorage.getItem("peer_id");
|
||||||
@@ -293,6 +357,14 @@ class App {
|
|||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
getUsername() {
|
||||||
|
let username = localStorage.getItem("dandelion_username");
|
||||||
|
if (!username) {
|
||||||
|
username = "not_set";
|
||||||
|
localStorage.setItem("dandelion_username", username);
|
||||||
|
}
|
||||||
|
return username;
|
||||||
|
}
|
||||||
setFont(fontName, fontSize) {
|
setFont(fontName, fontSize) {
|
||||||
let content = document.getElementById('content');
|
let content = document.getElementById('content');
|
||||||
if (!content) {
|
if (!content) {
|
||||||
@@ -313,9 +385,11 @@ class App {
|
|||||||
log("offline");
|
log("offline");
|
||||||
});
|
});
|
||||||
// Event listener for going online
|
// Event listener for going online
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', async () => {
|
||||||
log("online");
|
log("online");
|
||||||
connection.connect();
|
connection.connect();
|
||||||
|
this.posts = await this.loadPosts(this.userID) ?? [];
|
||||||
|
this.render(this.posts);
|
||||||
});
|
});
|
||||||
log(`Online status: ${navigator.onLine ? "online" : "offline"}`);
|
log(`Online status: ${navigator.onLine ? "online" : "offline"}`);
|
||||||
}
|
}
|
||||||
@@ -357,14 +431,17 @@ class App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
initButtons(userID, posts, registration) {
|
initButtons(userID, posts, registration) {
|
||||||
let font1Button = document.getElementById("button_font1");
|
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
|
||||||
let font2Button = document.getElementById("button_font2");
|
// let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
|
||||||
let importTweetsButton = document.getElementById("import_tweets");
|
let importTweetsButton = document.getElementById("import_tweets");
|
||||||
let clearPostsButton = document.getElementById("clear_posts");
|
let clearPostsButton = document.getElementById("clear_posts");
|
||||||
let updateApp = document.getElementById("update_app");
|
let updateApp = document.getElementById("update_app");
|
||||||
let ddlnLogoButton = document.getElementById('ddln-logo-button');
|
let ddlnLogoButton = document.getElementById('ddln-logo-button');
|
||||||
font1Button.addEventListener('click', () => { this.setFont('Bookerly', '16px'); });
|
let usernameField = document.getElementById('username');
|
||||||
font2Button.addEventListener('click', () => { this.setFont('Virgil', '16px'); });
|
usernameField?.addEventListener('input', (event) => {
|
||||||
|
this.username = event.target.innerText;
|
||||||
|
localStorage.setItem("dandelion_username", this.username);
|
||||||
|
});
|
||||||
importTweetsButton.addEventListener('click', async () => {
|
importTweetsButton.addEventListener('click', async () => {
|
||||||
let file = await this.selectFile('text/*');
|
let file = await this.selectFile('text/*');
|
||||||
console.log(file);
|
console.log(file);
|
||||||
@@ -388,7 +465,7 @@ class App {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
postButton.addEventListener("click", () => {
|
postButton.addEventListener("click", () => {
|
||||||
this.addPost(userID, posts, postText.value);
|
this.addPost(userID, postText.value);
|
||||||
postText.value = "";
|
postText.value = "";
|
||||||
});
|
});
|
||||||
updateApp.addEventListener("click", () => {
|
updateApp.addEventListener("click", () => {
|
||||||
@@ -416,40 +493,54 @@ class App {
|
|||||||
let urlParams = (new URL(window.location.href)).searchParams;
|
let urlParams = (new URL(window.location.href)).searchParams;
|
||||||
let connection_userID = urlParams.get('connect');
|
let connection_userID = urlParams.get('connect');
|
||||||
let registration = undefined;
|
let registration = undefined;
|
||||||
if (urlParams.get("sw") === "true") {
|
// if (urlParams.get("sw") === "true") {
|
||||||
registration = await this.registerServiceWorker();
|
registration = await this.registerServiceWorker();
|
||||||
}
|
// }
|
||||||
if (connection_userID) {
|
if (connection_userID) {
|
||||||
console.log('connect', connection_userID);
|
console.log('connect', connection_userID);
|
||||||
localStorage.setItem("dandelion_id", connection_userID);
|
localStorage.setItem("dandelion_id", connection_userID);
|
||||||
}
|
}
|
||||||
let posts = [];
|
this.username = this.getUsername();
|
||||||
|
document.getElementById('username').innerText = this.username;
|
||||||
let userID = this.getUserID();
|
let userID = this.getUserID();
|
||||||
let peerID = this.getPeerID();
|
let peerID = this.getPeerID();
|
||||||
this.userID = userID;
|
this.userID = userID;
|
||||||
this.peerID = peerID;
|
this.peerID = peerID;
|
||||||
|
let connectURL = `https://${document.location.hostname}?connect=${this.userID}`;
|
||||||
|
document.getElementById('connectURL').innerHTML = `<a href="${connectURL}">connect</a>`;
|
||||||
|
let qrcode = await new QRCode(document.getElementById('qrcode'), {
|
||||||
|
text: connectURL,
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
colorDark: "#000000",
|
||||||
|
colorLight: "#ffffff",
|
||||||
|
correctLevel: QRCode.CorrectLevel.H
|
||||||
|
});
|
||||||
|
let qrcodeImage = document.querySelector('#qrcode > img');
|
||||||
|
qrcodeImage.classList.add('qrcode_image');
|
||||||
log(`user:${userID} peer:${peerID}`);
|
log(`user:${userID} peer:${peerID}`);
|
||||||
let websocket = new wsConnection(userID, peerID);
|
let websocket = new wsConnection(userID, peerID);
|
||||||
window.addEventListener('beforeunload', () => { websocket.disconnect(); });
|
window.addEventListener('beforeunload', () => { websocket.disconnect(); });
|
||||||
this.initOffline(websocket);
|
this.initOffline(websocket);
|
||||||
this.initButtons(userID, posts, registration);
|
this.initButtons(userID, this.posts, registration);
|
||||||
let time = 0;
|
let time = 0;
|
||||||
let delta = 0;
|
let delta = 0;
|
||||||
if (navigator.storage && navigator.storage.persist && !navigator.storage.persisted) {
|
// let isPersisted = await navigator?.storage?.persisted();
|
||||||
debugger;
|
// if (!isPersisted) {
|
||||||
const isPersisted = await navigator.storage.persist();
|
// debugger;
|
||||||
log(`Persisted storage granted: ${isPersisted}`);
|
// const isPersisted = await navigator.storage.persist();
|
||||||
}
|
// log(`Persisted storage granted: ${isPersisted}`);
|
||||||
log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
|
// }
|
||||||
|
// log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
|
||||||
this.initMarkdown();
|
this.initMarkdown();
|
||||||
// let main = await fetch("/main.js");
|
// let main = await fetch("/main.js");
|
||||||
// let code = await main.text();
|
// let code = await main.text();
|
||||||
// console.log(code);
|
// console.log(code);
|
||||||
// registration.active.postMessage({type:"updateMain", code:code});
|
// registration.active.postMessage({type:"updateMain", code:code});
|
||||||
posts = await this.loadPosts(userID) ?? [];
|
this.posts = await this.loadPosts(userID) ?? [];
|
||||||
// debugger;
|
// debugger;
|
||||||
this.timerStart();
|
this.timerStart();
|
||||||
this.render(posts); // , (postID:string)=>{this.deletePost(userID, postID)}
|
this.render(this.posts); // , (postID:string)=>{this.deletePost(userID, postID)}
|
||||||
let renderTime = this.timerDelta();
|
let renderTime = this.timerDelta();
|
||||||
log(`render took: ${renderTime.toFixed(2)}ms`);
|
log(`render took: ${renderTime.toFixed(2)}ms`);
|
||||||
if (performance?.memory) {
|
if (performance?.memory) {
|
||||||
@@ -476,7 +567,6 @@ class App {
|
|||||||
}
|
}
|
||||||
contentDiv.innerHTML = "";
|
contentDiv.innerHTML = "";
|
||||||
let count = 0;
|
let count = 0;
|
||||||
new QRCode(document.getElementById('qrcode'), `https://ddlion.net/?connect=${this.userID}`);
|
|
||||||
for (let i = posts.length - 1; i >= 0; i--) {
|
for (let i = posts.length - 1; i >= 0; i--) {
|
||||||
let postData = posts[i];
|
let postData = posts[i];
|
||||||
let post = this.renderPost(postData, posts);
|
let post = this.renderPost(postData, posts);
|
||||||
@@ -493,8 +583,10 @@ class App {
|
|||||||
}
|
}
|
||||||
contentDiv.appendChild(fragment);
|
contentDiv.appendChild(fragment);
|
||||||
}
|
}
|
||||||
deletePost(userID, postID) {
|
async deletePost(userID, postID) {
|
||||||
deleteData(userID, postID);
|
deleteData(userID, postID);
|
||||||
|
this.posts = await this.loadPosts(userID) ?? [];
|
||||||
|
this.render(this.posts);
|
||||||
}
|
}
|
||||||
renderPost(post, posts) {
|
renderPost(post, posts) {
|
||||||
if (!(post.hasOwnProperty("text"))) {
|
if (!(post.hasOwnProperty("text"))) {
|
||||||
@@ -504,13 +596,20 @@ class App {
|
|||||||
let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
|
let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
|
||||||
let deleteButton = document.createElement('button');
|
let deleteButton = document.createElement('button');
|
||||||
deleteButton.innerText = 'delete';
|
deleteButton.innerText = 'delete';
|
||||||
// deleteButton.onclick = ()=>{deletefunc(post.post_id)};
|
let editButton = document.createElement('button');
|
||||||
|
editButton.innerText = 'edit';
|
||||||
|
deleteButton.onclick = () => { this.deletePost(this.userID, post.post_id); };
|
||||||
let postTemplate = `<div><hr>
|
let postTemplate = `<div><hr>
|
||||||
<div><span class='header' title='${timestamp}'>@${post.author} - ${post.post_timestamp.toLocaleDateString()}</span></div>
|
<div>
|
||||||
|
<span class='header' title='${timestamp}'>@${post.author} -
|
||||||
|
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
|
||||||
|
</span>
|
||||||
|
<span id="deleteButton"></span><span id="editButton"></span></div>
|
||||||
<div>${marked.parse(post.text)}</div>
|
<div>${marked.parse(post.text)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
containerDiv.innerHTML = postTemplate;
|
containerDiv.innerHTML = postTemplate;
|
||||||
containerDiv.appendChild(deleteButton);
|
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
||||||
|
containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||||
// if (!("image_data" in post && post.image_data)) {
|
// if (!("image_data" in post && post.image_data)) {
|
||||||
// containerDiv.appendChild(timestampDiv);
|
// containerDiv.appendChild(timestampDiv);
|
||||||
// return containerDiv;
|
// return containerDiv;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgh092YwNP2/D6bshI
|
|
||||||
vzZNcEn/bpxtz7h3Otown6UXP6GhRANCAASEd7+0ppvG0iyaAjv8urB8LVYKSIYO
|
|
||||||
8GLp6H9P2fwZjzWXfC8iQMhP9jaWaQaAl73gS4iXpODDzYJ/Hwi6siPt
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
58
src/db.ts
58
src/db.ts
@@ -4,6 +4,9 @@
|
|||||||
// email: string;
|
// email: string;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// Efficiently storing data in indexdb: https://stackoverflow.com/a/62975917
|
||||||
|
|
||||||
const postStoreName: string = "posts";
|
const postStoreName: string = "posts";
|
||||||
let keyBase = "dandelion_posts_v1_"
|
let keyBase = "dandelion_posts_v1_"
|
||||||
let key = "";
|
let key = "";
|
||||||
@@ -156,6 +159,60 @@ export async function addDataArray(userID: string, array: any[]): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function mergeDataArray(userID: string, array:any[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const db = await openDatabase(userID);
|
||||||
|
const transaction = db.transaction(postStoreName, "readwrite");
|
||||||
|
const store = transaction.objectStore(postStoreName);
|
||||||
|
const index = store.index("postIDIndex");
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
console.log("Transaction completed successfully");
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.onerror = (event) => {
|
||||||
|
console.error("Transaction error:", (event.target as any).error);
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
let postsToWrite:any = [];
|
||||||
|
|
||||||
|
for (let post of array) {
|
||||||
|
try {
|
||||||
|
let havePost = await new Promise<boolean>((resolve, reject) => {
|
||||||
|
const getRequest = index.getKey(post.post_id);
|
||||||
|
|
||||||
|
getRequest.onerror = (e) => {
|
||||||
|
console.log((e.target as IDBRequest).error);
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
getRequest.onsuccess = async (e) => {
|
||||||
|
const key = (e.target as IDBRequest).result;
|
||||||
|
resolve(key !== undefined)
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(post.post_id, havePost);
|
||||||
|
if (!havePost ) {
|
||||||
|
postsToWrite.push(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing post:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Writing ${postsToWrite.length} posts`);
|
||||||
|
|
||||||
|
await addDataArray(userID, postsToWrite);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in opening database:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getData(userID: string, lowerID: Date, upperID: Date): Promise<any | undefined> {
|
export async function getData(userID: string, lowerID: Date, upperID: Date): Promise<any | undefined> {
|
||||||
const db = await openDatabase(userID);
|
const db = await openDatabase(userID);
|
||||||
const transaction = db.transaction(postStoreName, "readonly");
|
const transaction = db.transaction(postStoreName, "readonly");
|
||||||
@@ -191,6 +248,7 @@ export async function getData(userID: string, lowerID: Date, upperID: Date): Pro
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getAllData(userID: string): Promise<any | undefined> {
|
export async function getAllData(userID: string): Promise<any | undefined> {
|
||||||
const db = await openDatabase(userID);
|
const db = await openDatabase(userID);
|
||||||
const transaction = db.transaction(postStoreName, "readonly");
|
const transaction = db.transaction(postStoreName, "readonly");
|
||||||
|
|||||||
258
src/main.ts
258
src/main.ts
@@ -1,4 +1,8 @@
|
|||||||
import { openDatabase, getData, addData, addDataArray, clearData, deleteData} from "./db.js"
|
// TODO: virtual list, only rerender what's needed so things can keep playing.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData } from "./db.js"
|
||||||
|
|
||||||
declare let WebTorrent: any;
|
declare let WebTorrent: any;
|
||||||
|
|
||||||
@@ -31,6 +35,7 @@ function uuidv4() {
|
|||||||
let logLines: string[] = [];
|
let logLines: string[] = [];
|
||||||
let logLength = 10;
|
let logLength = 10;
|
||||||
function log(message: string) {
|
function log(message: string) {
|
||||||
|
console.log(message);
|
||||||
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
|
logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
|
||||||
if (logLines.length > 10) {
|
if (logLines.length > 10) {
|
||||||
logLines = logLines.slice(logLines.length - logLength);
|
logLines = logLines.slice(logLines.length - logLength);
|
||||||
@@ -93,8 +98,10 @@ window.addEventListener('scroll', () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// let peer = await new PeerConnection(peer_id);
|
||||||
|
|
||||||
// let connectionReply = await wsConnection.send('hello');
|
// let connectionReply = await wsConnection.send('hello');
|
||||||
// for (let peeer of connectionReply) {
|
// for (let peer of connectionReply) {
|
||||||
// let peerConnection = await wsConnection.send('connect', peer.id);
|
// let peerConnection = await wsConnection.send('connect', peer.id);
|
||||||
// if (peerConnection) {
|
// if (peerConnection) {
|
||||||
// this.peers.push(peerConnection);
|
// this.peers.push(peerConnection);
|
||||||
@@ -108,7 +115,6 @@ window.addEventListener('scroll', () => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
class wsConnection {
|
class wsConnection {
|
||||||
websocket: WebSocket | null = null;
|
websocket: WebSocket | null = null;
|
||||||
userID = "";
|
userID = "";
|
||||||
@@ -116,7 +122,88 @@ class wsConnection {
|
|||||||
websocketPingInterval: number = 0;
|
websocketPingInterval: number = 0;
|
||||||
retry = 10;
|
retry = 10;
|
||||||
state = 'disconnected';
|
state = 'disconnected';
|
||||||
|
peers: Map<string, string[]> = new Map();
|
||||||
|
|
||||||
|
messageHandlers: Map<string, (event: any) => void> = new Map();
|
||||||
|
peerMessageHandlers: Map<string, (data: any) => void> = new Map();
|
||||||
|
|
||||||
|
|
||||||
|
send(message: any) {
|
||||||
|
let json = ""
|
||||||
|
try {
|
||||||
|
json = JSON.stringify(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e, "wsConnection send: Couldn't serialize message", message);
|
||||||
|
}
|
||||||
|
log(`ws->${json.slice(0,80)}`)
|
||||||
|
this.websocket!.send(json);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
helloResponseHandler(data: any) {
|
||||||
|
for (let [userID, peerIDs] of Object.entries(data.userPeers)) {
|
||||||
|
this.peers.set(userID, [...Object.keys(peerIDs as any)]);
|
||||||
|
|
||||||
|
for (let peerID of [...Object.keys(peerIDs as any)]) {
|
||||||
|
if (peerID === this.peerID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.send({
|
||||||
|
type:"peer_message",
|
||||||
|
from:this.peerID,
|
||||||
|
to:peerID,
|
||||||
|
message:{type:"get_posts_for_user", user_id:userID}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pongHandler(data: any) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPostsForUserResponseHandler(data: any) {
|
||||||
|
// log(`getPostsForUserResponse: ${data}`)
|
||||||
|
|
||||||
|
let message = data.message;
|
||||||
|
console.log(`getPostsForUserResponseHandler Got ${message.posts.length} from peer ${data.from}`);
|
||||||
|
for (let post of message.posts) {
|
||||||
|
post.post_timestamp = new Date(post.post_timestamp);
|
||||||
|
}
|
||||||
|
console.log(`Merging same user peer posts...`)
|
||||||
|
await mergeDataArray(message.user_id, data.message.posts)
|
||||||
|
|
||||||
|
|
||||||
|
if (message.user_id === this.userID) {
|
||||||
|
app.posts = await app.loadPosts(this.userID) ?? [];
|
||||||
|
app.render(app.posts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPostsForUserHandler(data: any) {
|
||||||
|
let message = data.message;
|
||||||
|
let posts = await getAllData(message.user_id) ?? []; // this doesn't get all posts!
|
||||||
|
posts = posts.map((post:any)=>post.data)
|
||||||
|
// let posts = await getAllData(message.user_id) ?? [];
|
||||||
|
|
||||||
|
let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, message: { type: "get_posts_for_user_response", posts: posts, user_id: message.user_id } }
|
||||||
|
this.send(responseMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
async peerMessageHandler(data: any) {
|
||||||
|
log(`peerMessageHandler ${data}`)
|
||||||
|
|
||||||
|
let peerMessageType = data.message.type;
|
||||||
|
|
||||||
|
let handler = this.peerMessageHandlers.get(peerMessageType);
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
console.error(`got peer message type we don't have a handler for: ${peerMessageType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(data);
|
||||||
|
}
|
||||||
|
|
||||||
connect(): void {
|
connect(): void {
|
||||||
if (this.websocket?.readyState === WebSocket.OPEN) {
|
if (this.websocket?.readyState === WebSocket.OPEN) {
|
||||||
@@ -133,18 +220,18 @@ class wsConnection {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.websocket.onopen = (evt) => {
|
this.websocket.onopen = (event) => {
|
||||||
log("ws:connected");
|
log("ws:connected");
|
||||||
this.websocket!.send(`{"type":"hello", "user_id": "${this.userID}", "peer_id":"${this.peerID}"}`);
|
this.send({type:"hello", user_id: this.userID, peer_id:this.peerID});
|
||||||
this.websocketPingInterval = window.setInterval(() => {
|
this.websocketPingInterval = window.setInterval(() => {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.websocket!.send(`{"type":"ping", "peer_id": "${this.peerID}"}`);
|
this.send({type:"ping", peer_id: this.peerID});
|
||||||
}, 10_000)
|
}, 10_000)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.websocket.onclose = (evt) => {
|
this.websocket.onclose = (event) => {
|
||||||
log("ws:disconnected");
|
log("ws:disconnected");
|
||||||
// this.retry *= 2;
|
// this.retry *= 2;
|
||||||
log(`Retrying in ${this.retry} seconds`);
|
log(`Retrying in ${this.retry} seconds`);
|
||||||
@@ -152,7 +239,20 @@ class wsConnection {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.websocket.onmessage = (event) => {
|
this.websocket.onmessage = (event) => {
|
||||||
log('ws:response: ' + event.data);
|
log('ws:<-' + event.data.slice(0,80));
|
||||||
|
|
||||||
|
let data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
let { type } = data;
|
||||||
|
|
||||||
|
let handler = this.messageHandlers.get(type);
|
||||||
|
if (!handler) {
|
||||||
|
console.warn(`Got a message we can't handle:`, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(data);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.websocket.onerror = (event) => {
|
this.websocket.onerror = (event) => {
|
||||||
@@ -168,6 +268,13 @@ class wsConnection {
|
|||||||
constructor(userID: string, peerID: string) {
|
constructor(userID: string, peerID: string) {
|
||||||
this.userID = userID;
|
this.userID = userID;
|
||||||
this.peerID = peerID;
|
this.peerID = peerID;
|
||||||
|
|
||||||
|
this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
|
||||||
|
this.messageHandlers.set('pong', this.pongHandler);
|
||||||
|
this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
|
||||||
|
|
||||||
|
this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
|
||||||
|
this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserResponseHandler.bind(this));
|
||||||
this.connect();
|
this.connect();
|
||||||
|
|
||||||
if (!this.websocket) {
|
if (!this.websocket) {
|
||||||
@@ -177,34 +284,11 @@ class wsConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function connectWebsocket(userID: string) {
|
|
||||||
// let websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
|
|
||||||
|
|
||||||
// websocket.onopen = function (evt) {
|
|
||||||
// log("Websocket: CONNECTED");
|
|
||||||
// websocket.send(`{"messageType":"connect", "id": "${userID}"}`);
|
|
||||||
|
|
||||||
// let websocketPingInterval = window.setInterval(() => { websocket.send(`{"messageType":"ping", "id": "${userID}"}`); }, 5000)
|
|
||||||
// };
|
|
||||||
|
|
||||||
// websocket.onclose = function (evt) {
|
|
||||||
// log("Websocket: DISCONNECTED");
|
|
||||||
// };
|
|
||||||
|
|
||||||
// websocket.onmessage = function (evt) {
|
|
||||||
// log('Websocket: RESPONSE: ' + evt.data);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// websocket.onerror = function (evt) {
|
|
||||||
// log('Websocket: ERROR: ' + evt);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return websocket;
|
|
||||||
// }
|
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
|
username: string = '';
|
||||||
userID: string = '';
|
userID: string = '';
|
||||||
peerID: string = '';
|
peerID: string = '';
|
||||||
|
posts: Post[] = [];
|
||||||
|
|
||||||
initMarkdown() {
|
initMarkdown() {
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
@@ -282,9 +366,9 @@ class App {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (let entry of tweetArchive) {
|
for (let entry of tweetArchive) {
|
||||||
if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
|
// if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
let mediaURL: string = entry.tweet?.entities?.media?.[0]?.media_url_https;
|
let mediaURL: string = entry.tweet?.entities?.media?.[0]?.media_url_https;
|
||||||
let isImage = false;
|
let isImage = false;
|
||||||
@@ -355,19 +439,19 @@ class App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addPost(userID: string, posts: Post[], postText: string) {
|
addPost(userID: string, postText: string) {
|
||||||
if ((typeof postText !== "string") || postText.length === 0) {
|
if ((typeof postText !== "string") || postText.length === 0) {
|
||||||
log("Not posting an empty string...")
|
log("Not posting an empty string...")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let post = new Post(`bobbydigitales`, userID, postText, new Date());
|
let post = new Post(this.username, userID, postText, new Date());
|
||||||
|
|
||||||
posts.push(post);
|
this.posts.push(post);
|
||||||
// localStorage.setItem(key, JSON.stringify(posts));
|
// localStorage.setItem(key, JSON.stringify(posts));
|
||||||
addData(userID, post)
|
addData(userID, post)
|
||||||
|
|
||||||
this.render(posts);
|
this.render(this.posts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -393,6 +477,17 @@ class App {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUsername() {
|
||||||
|
let username = localStorage.getItem("dandelion_username");
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
username = "not_set"
|
||||||
|
localStorage.setItem("dandelion_username", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
setFont(fontName: string, fontSize: string) {
|
setFont(fontName: string, fontSize: string) {
|
||||||
|
|
||||||
let content = document.getElementById('content');
|
let content = document.getElementById('content');
|
||||||
@@ -420,10 +515,11 @@ class App {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Event listener for going online
|
// Event listener for going online
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', async () => {
|
||||||
log("online")
|
log("online")
|
||||||
connection.connect();
|
connection.connect();
|
||||||
|
this.posts = await this.loadPosts(this.userID) ?? [];
|
||||||
|
this.render(this.posts);
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Online status: ${navigator.onLine ? "online" : "offline"}`)
|
log(`Online status: ${navigator.onLine ? "online" : "offline"}`)
|
||||||
@@ -474,15 +570,18 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initButtons(userID: string, posts: Post[], registration: ServiceWorkerRegistration | undefined) {
|
initButtons(userID: string, posts: Post[], registration: ServiceWorkerRegistration | undefined) {
|
||||||
let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
|
// let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
|
||||||
let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
|
// let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
|
||||||
let importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
|
let importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
|
||||||
let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
|
let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
|
||||||
let updateApp = document.getElementById("update_app") as HTMLButtonElement;
|
let updateApp = document.getElementById("update_app") as HTMLButtonElement;
|
||||||
let ddlnLogoButton = document.getElementById('ddln-logo-button') as HTMLDivElement;
|
let ddlnLogoButton = document.getElementById('ddln-logo-button') as HTMLDivElement;
|
||||||
|
|
||||||
font1Button.addEventListener('click', () => { this.setFont('Bookerly', '16px') });
|
let usernameField = document.getElementById('username');
|
||||||
font2Button.addEventListener('click', () => { this.setFont('Virgil', '16px') });
|
usernameField?.addEventListener('input', (event:any)=>{
|
||||||
|
this.username = event.target.innerText;
|
||||||
|
localStorage.setItem("dandelion_username", this.username);
|
||||||
|
})
|
||||||
|
|
||||||
importTweetsButton.addEventListener('click', async () => {
|
importTweetsButton.addEventListener('click', async () => {
|
||||||
let file = await this.selectFile('text/*');
|
let file = await this.selectFile('text/*');
|
||||||
@@ -516,7 +615,7 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
postButton.addEventListener("click", () => {
|
postButton.addEventListener("click", () => {
|
||||||
this.addPost(userID, posts, postText.value);
|
this.addPost(userID, postText.value);
|
||||||
postText.value = "";
|
postText.value = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -555,37 +654,54 @@ class App {
|
|||||||
let connection_userID = urlParams.get('connect');
|
let connection_userID = urlParams.get('connect');
|
||||||
|
|
||||||
let registration = undefined;
|
let registration = undefined;
|
||||||
if (urlParams.get("sw") === "true") {
|
// if (urlParams.get("sw") === "true") {
|
||||||
registration = await this.registerServiceWorker();
|
registration = await this.registerServiceWorker();
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (connection_userID) {
|
if (connection_userID) {
|
||||||
console.log('connect', connection_userID);
|
console.log('connect', connection_userID);
|
||||||
localStorage.setItem("dandelion_id", connection_userID);
|
localStorage.setItem("dandelion_id", connection_userID);
|
||||||
}
|
}
|
||||||
|
|
||||||
let posts: Post[] = [];
|
this.username = this.getUsername();
|
||||||
|
document.getElementById('username')!.innerText = this.username;
|
||||||
let userID = this.getUserID();
|
let userID = this.getUserID();
|
||||||
let peerID = this.getPeerID();
|
let peerID = this.getPeerID();
|
||||||
this.userID = userID;
|
this.userID = userID;
|
||||||
this.peerID = peerID;
|
this.peerID = peerID;
|
||||||
|
|
||||||
|
let connectURL = `https://${document.location.hostname}?connect=${this.userID}`;
|
||||||
|
document.getElementById('connectURL')!.innerHTML = `<a href="${connectURL}">connect</a>`;
|
||||||
|
|
||||||
|
let qrcode = await new QRCode(document.getElementById('qrcode'), {
|
||||||
|
text: connectURL,
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
colorDark: "#000000",
|
||||||
|
colorLight: "#ffffff",
|
||||||
|
correctLevel: QRCode.CorrectLevel.H
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let qrcodeImage:HTMLImageElement = document.querySelector('#qrcode > img') as HTMLImageElement;
|
||||||
|
qrcodeImage.classList.add('qrcode_image');
|
||||||
|
|
||||||
log(`user:${userID} peer:${peerID}`);
|
log(`user:${userID} peer:${peerID}`);
|
||||||
let websocket = new wsConnection(userID, peerID);
|
let websocket = new wsConnection(userID, peerID);
|
||||||
window.addEventListener('beforeunload', () => { websocket.disconnect() })
|
window.addEventListener('beforeunload', () => { websocket.disconnect() })
|
||||||
this.initOffline(websocket);
|
this.initOffline(websocket);
|
||||||
this.initButtons(userID, posts, registration);
|
this.initButtons(userID, this.posts, registration);
|
||||||
|
|
||||||
let time = 0;
|
let time = 0;
|
||||||
let delta = 0;
|
let delta = 0;
|
||||||
if (navigator.storage && navigator.storage.persist && !navigator.storage.persisted) {
|
// let isPersisted = await navigator?.storage?.persisted();
|
||||||
debugger;
|
// if (!isPersisted) {
|
||||||
const isPersisted = await navigator.storage.persist();
|
// debugger;
|
||||||
log(`Persisted storage granted: ${isPersisted}`);
|
// const isPersisted = await navigator.storage.persist();
|
||||||
}
|
// log(`Persisted storage granted: ${isPersisted}`);
|
||||||
|
// }
|
||||||
|
|
||||||
log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
|
// log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
|
||||||
|
|
||||||
this.initMarkdown();
|
this.initMarkdown();
|
||||||
|
|
||||||
@@ -594,12 +710,12 @@ class App {
|
|||||||
// console.log(code);
|
// console.log(code);
|
||||||
// registration.active.postMessage({type:"updateMain", code:code});
|
// registration.active.postMessage({type:"updateMain", code:code});
|
||||||
|
|
||||||
posts = await this.loadPosts(userID) ?? [];
|
this.posts = await this.loadPosts(userID) ?? [];
|
||||||
|
|
||||||
// debugger;
|
// debugger;
|
||||||
|
|
||||||
this.timerStart();
|
this.timerStart();
|
||||||
this.render(posts); // , (postID:string)=>{this.deletePost(userID, postID)}
|
this.render(this.posts); // , (postID:string)=>{this.deletePost(userID, postID)}
|
||||||
let renderTime = this.timerDelta();
|
let renderTime = this.timerDelta();
|
||||||
|
|
||||||
log(`render took: ${renderTime.toFixed(2)}ms`);
|
log(`render took: ${renderTime.toFixed(2)}ms`);
|
||||||
@@ -635,9 +751,6 @@ class App {
|
|||||||
contentDiv.innerHTML = "";
|
contentDiv.innerHTML = "";
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|
||||||
new QRCode(document.getElementById('qrcode'), `https://ddlion.net/?connect=${this.userID}`);
|
|
||||||
|
|
||||||
for (let i = posts.length - 1; i >= 0; i--) {
|
for (let i = posts.length - 1; i >= 0; i--) {
|
||||||
let postData = posts[i];
|
let postData = posts[i];
|
||||||
|
|
||||||
@@ -661,8 +774,10 @@ class App {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePost(userID:string, postID:string) {
|
async deletePost(userID: string, postID: string) {
|
||||||
deleteData(userID, postID)
|
deleteData(userID, postID)
|
||||||
|
this.posts = await this.loadPosts(userID) ?? [];
|
||||||
|
this.render(this.posts);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPost(post: Post, posts: Post[]) {
|
renderPost(post: Post, posts: Post[]) {
|
||||||
@@ -673,19 +788,24 @@ class App {
|
|||||||
|
|
||||||
let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
|
let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
|
||||||
|
|
||||||
let deleteButton = document.createElement('button');
|
let deleteButton = document.createElement('button'); deleteButton.innerText = 'delete';
|
||||||
deleteButton.innerText = 'delete';
|
let editButton = document.createElement('button'); editButton.innerText = 'edit';
|
||||||
// deleteButton.onclick = ()=>{deletefunc(post.post_id)};
|
deleteButton.onclick = () => { this.deletePost(this.userID, post.post_id) };
|
||||||
|
|
||||||
let postTemplate =
|
let postTemplate =
|
||||||
`<div><hr>
|
`<div><hr>
|
||||||
<div><span class='header' title='${timestamp}'>@${post.author} - ${post.post_timestamp.toLocaleDateString()}</span></div>
|
<div>
|
||||||
|
<span class='header' title='${timestamp}'>@${post.author} -
|
||||||
|
<span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
|
||||||
|
</span>
|
||||||
|
<span id="deleteButton"></span><span id="editButton"></span></div>
|
||||||
<div>${marked.parse(post.text)}</div>
|
<div>${marked.parse(post.text)}</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
containerDiv.innerHTML = postTemplate;
|
containerDiv.innerHTML = postTemplate;
|
||||||
|
|
||||||
containerDiv.appendChild(deleteButton);
|
containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
|
||||||
|
containerDiv.querySelector('#editButton')?.appendChild(editButton);
|
||||||
|
|
||||||
// if (!("image_data" in post && post.image_data)) {
|
// if (!("image_data" in post && post.image_data)) {
|
||||||
// containerDiv.appendChild(timestampDiv);
|
// containerDiv.appendChild(timestampDiv);
|
||||||
|
|||||||
82
src/sw.ts
82
src/sw.ts
@@ -2,13 +2,13 @@
|
|||||||
const cacheName = "dandelion_cache_v1";
|
const cacheName = "dandelion_cache_v1";
|
||||||
|
|
||||||
const contentToCache = [
|
const contentToCache = [
|
||||||
"/index.html",
|
'/index.html',
|
||||||
"/main.js",
|
'/main.css',
|
||||||
"/marked.min.js",
|
'/main.js',
|
||||||
"/db.js",
|
'lib//marked.min.js',
|
||||||
"/bookerly.woff2",
|
'lib/qrcode.min.js',
|
||||||
"/virgil.woff2",
|
'/db.js',
|
||||||
"/favicon.ico"
|
'/favicon.ico'
|
||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener("install", (e: any) => {
|
self.addEventListener("install", (e: any) => {
|
||||||
@@ -24,49 +24,53 @@ self.addEventListener("install", (e:any) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (e:any) => {
|
async function responder(event: any) {
|
||||||
e.respondWith(
|
console.log('Fetching', event.request.url);
|
||||||
(async () => {
|
|
||||||
const r = await caches.match(e.request);
|
|
||||||
if (r) {
|
|
||||||
console.log(
|
|
||||||
`[Service Worker] Cache hit for resource: ${e.request.url}`
|
|
||||||
);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
let response = await fetch(event.request);
|
||||||
try {
|
|
||||||
console.log(
|
|
||||||
`[Service Worker] Cache miss, attempting to fetch resource: ${e.request.url}`
|
|
||||||
);
|
|
||||||
|
|
||||||
response = await fetch(e.request);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
console.log(
|
|
||||||
`[Service Worker] Adding resource to cache: ${e.request.url}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error(`Failed to fetch resource: ${e.request.url}`)
|
console.log('Fetch failed, falling back to cache', event.request.url);
|
||||||
|
let cacheMatch = await caches.match(event.request);
|
||||||
|
if (!cacheMatch) {
|
||||||
|
// DUnno what to return here!
|
||||||
}
|
}
|
||||||
cache.put(e.request, response.clone());
|
return cacheMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 206) {
|
||||||
|
console.log('Not caching partial content');
|
||||||
return response;
|
return response;
|
||||||
})()
|
}
|
||||||
);
|
|
||||||
|
console.log('Fetch successful, updating cache');
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
try {
|
||||||
|
cache.put(event.request, response.clone()).catch((error)=>console.log('failed to cache', event.request, error));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to cache', event.request)
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('fetch', function (event: any) {
|
||||||
|
event.respondWith(responder(event));
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("message", async (e) => {
|
addEventListener("message", async (e) => {
|
||||||
console.log(`Message received: ${e.data}`);
|
console.log(`Message received:`, e.data);
|
||||||
|
|
||||||
switch (e.data.type) {
|
switch (e.data.type) {
|
||||||
case "updateMain":
|
case "update_app":
|
||||||
const cache = await caches.open(cacheName);
|
const cache = await caches.open(cacheName);
|
||||||
console.log(`[Service Worker] Caching new resource: main.js`);
|
console.log(`[Service Worker] Caching resources`);
|
||||||
cache.put("/main.js", new Response());
|
// cache.put("/main.js", new Response());
|
||||||
|
|
||||||
|
for (let item of contentToCache) {
|
||||||
|
cache.delete(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.addAll(contentToCache);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
171
src/webRTC.ts
171
src/webRTC.ts
@@ -1,83 +1,112 @@
|
|||||||
|
class PeerManager {
|
||||||
|
connect(peerID:string) {
|
||||||
|
// Connect to the peer that has the peer id peerID
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(peerID:string) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PeerConnection {
|
||||||
|
static config = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: "stun:stun.l.google.com" },
|
||||||
|
{ urls: "stun:stun1.l.google.com" },
|
||||||
|
{ urls: "stun:stun2.l.google.com" },
|
||||||
|
{ urls: "stun:stun3.l.google.com" },
|
||||||
|
{ urls: "stun:stun4.l.google.com" },
|
||||||
|
],};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
iceServers: [{ urls: "stun: stun.l.google.com" }],
|
iceServers: [{ urls: "stun:stun.mystunserver.tld" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
let localConnection = new RTCPeerConnection();
|
let polite = true;
|
||||||
|
|
||||||
function handleSendChannelStatusChange() {
|
// const signaler = new SignalingChannel();
|
||||||
console.log(handleSendChannelStatusChange);
|
const signaler:any = {}
|
||||||
|
const pc = new RTCPeerConnection(config);
|
||||||
|
|
||||||
|
|
||||||
|
const constraints = { audio: true, video: true };
|
||||||
|
const selfVideo = document.querySelector("video.selfview");
|
||||||
|
const remoteVideo = document.querySelector("video.remoteview");
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
for (const track of stream.getTracks()) {
|
||||||
|
pc.addTrack(track, stream);
|
||||||
}
|
}
|
||||||
|
// selfVideo.srcObject = stream;
|
||||||
let sendChannel = localConnection.createDataChannel("sendChannel");
|
} catch (err) {
|
||||||
sendChannel.onopen = handleSendChannelStatusChange;
|
console.error(err);
|
||||||
sendChannel.onclose = handleSendChannelStatusChange;
|
|
||||||
|
|
||||||
|
|
||||||
let remoteConnection = new RTCPeerConnection();
|
|
||||||
remoteConnection.ondatachannel = receiveChannelCallback;
|
|
||||||
|
|
||||||
|
|
||||||
localConnection.onicecandidate = (e:any) =>
|
|
||||||
!e.candidate ||
|
|
||||||
remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
|
|
||||||
|
|
||||||
remoteConnection.onicecandidate = (e) =>
|
|
||||||
!e.candidate ||
|
|
||||||
localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleCreateDescriptionError(error:any) {
|
|
||||||
console.log(`Unable to create an offer: ${error.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLocalAddCandidateSuccess() {
|
|
||||||
console.log('handleLocalAddCandidateSuccess');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRemoteAddCandidateSuccess() {
|
|
||||||
console.log('handleRemoteAddCandidateSuccess');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddCandidateError() {
|
|
||||||
console.log("Oh noes! addICECandidate failed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
localConnection
|
|
||||||
.createOffer()
|
|
||||||
.then((offer) => localConnection.setLocalDescription(offer))
|
|
||||||
.then(() =>
|
|
||||||
remoteConnection.setRemoteDescription(localConnection.localDescription as RTCSessionDescriptionInit),
|
|
||||||
)
|
|
||||||
.then(() => remoteConnection.createAnswer())
|
|
||||||
.then((answer) => remoteConnection.setLocalDescription(answer))
|
|
||||||
.then(() =>
|
|
||||||
localConnection.setRemoteDescription(remoteConnection.localDescription as RTCSessionDescriptionInit),
|
|
||||||
)
|
|
||||||
.catch(handleCreateDescriptionError);
|
|
||||||
|
|
||||||
function handleReceiveChannelStatusChange(event:any) {
|
|
||||||
let receiveChannel = event.channel;
|
|
||||||
|
|
||||||
if (receiveChannel) {
|
|
||||||
console.log(
|
|
||||||
`Receive channel's status has changed to ${receiveChannel.readyState}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function receiveChannelCallback(event:any) {
|
|
||||||
let receiveChannel = event.channel;
|
pc.ontrack = ({ track, streams }) => {
|
||||||
receiveChannel.onmessage = handleReceiveMessage;
|
track.onunmute = () => {
|
||||||
receiveChannel.onopen = handleReceiveChannelStatusChange;
|
// if (remoteVideo.srcObject) {
|
||||||
receiveChannel.onclose = handleReceiveChannelStatusChange;
|
// return;
|
||||||
|
// }
|
||||||
|
// remoteVideo.srcObject = streams[0];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let makingOffer = false;
|
||||||
|
|
||||||
|
pc.onnegotiationneeded = async () => {
|
||||||
|
try {
|
||||||
|
makingOffer = true;
|
||||||
|
await pc.setLocalDescription();
|
||||||
|
signaler.send({ description: pc.localDescription });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
makingOffer = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pc.onicecandidate = ({ candidate }) => signaler.send({ candidate });
|
||||||
|
|
||||||
|
let ignoreOffer = false;
|
||||||
|
|
||||||
|
signaler.onmessage = async ({ data: { description, candidate } }: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
if (description) {
|
||||||
|
const offerCollision =
|
||||||
|
description.type === "offer" &&
|
||||||
|
(makingOffer || pc.signalingState !== "stable");
|
||||||
|
|
||||||
|
ignoreOffer = !polite && offerCollision;
|
||||||
|
if (ignoreOffer) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMessage(message:string) {
|
await pc.setRemoteDescription(description);
|
||||||
sendChannel.send(message);
|
if (description.type === "offer") {
|
||||||
|
await pc.setLocalDescription();
|
||||||
|
signaler.send({ description: pc.localDescription });
|
||||||
}
|
}
|
||||||
|
} else if (candidate) {
|
||||||
function handleReceiveMessage(event:any) {
|
try {
|
||||||
console.log(event.data);
|
await pc.addIceCandidate(candidate);
|
||||||
|
} catch (err) {
|
||||||
|
if (!ignoreOffer) {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
64
sw.js
64
sw.js
@@ -2,13 +2,13 @@
|
|||||||
// Establish a cache name
|
// Establish a cache name
|
||||||
const cacheName = "dandelion_cache_v1";
|
const cacheName = "dandelion_cache_v1";
|
||||||
const contentToCache = [
|
const contentToCache = [
|
||||||
"/index.html",
|
'/index.html',
|
||||||
"/main.js",
|
'/main.css',
|
||||||
"/marked.min.js",
|
'/main.js',
|
||||||
"/db.js",
|
'lib//marked.min.js',
|
||||||
"/bookerly.woff2",
|
'lib/qrcode.min.js',
|
||||||
"/virgil.woff2",
|
'/db.js',
|
||||||
"/favicon.ico"
|
'/favicon.ico'
|
||||||
];
|
];
|
||||||
self.addEventListener("install", (e) => {
|
self.addEventListener("install", (e) => {
|
||||||
e.waitUntil((async () => {
|
e.waitUntil((async () => {
|
||||||
@@ -17,37 +17,45 @@ self.addEventListener("install", (e) => {
|
|||||||
await cache.addAll(contentToCache);
|
await cache.addAll(contentToCache);
|
||||||
})());
|
})());
|
||||||
});
|
});
|
||||||
self.addEventListener("fetch", (e) => {
|
async function responder(event) {
|
||||||
e.respondWith((async () => {
|
console.log('Fetching', event.request.url);
|
||||||
const r = await caches.match(e.request);
|
let response = await fetch(event.request);
|
||||||
if (r) {
|
if (!response) {
|
||||||
console.log(`[Service Worker] Cache hit for resource: ${e.request.url}`);
|
console.log('Fetch failed, falling back to cache', event.request.url);
|
||||||
return r;
|
let cacheMatch = await caches.match(event.request);
|
||||||
|
if (!cacheMatch) {
|
||||||
|
// DUnno what to return here!
|
||||||
}
|
}
|
||||||
let response;
|
return cacheMatch;
|
||||||
|
}
|
||||||
|
if (response.status === 206) {
|
||||||
|
console.log('Not caching partial content');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
console.log('Fetch successful, updating cache');
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
try {
|
try {
|
||||||
console.log(`[Service Worker] Cache miss, attempting to fetch resource: ${e.request.url}`);
|
cache.put(event.request, response.clone()).catch((error) => console.log('failed to cache', event.request, error));
|
||||||
response = await fetch(e.request);
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.warn(e);
|
console.log('failed to cache', event.request);
|
||||||
}
|
}
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
console.log(`[Service Worker] Adding resource to cache: ${e.request.url}`);
|
|
||||||
if (!response) {
|
|
||||||
throw new Error(`Failed to fetch resource: ${e.request.url}`);
|
|
||||||
}
|
|
||||||
cache.put(e.request, response.clone());
|
|
||||||
return response;
|
return response;
|
||||||
})());
|
}
|
||||||
|
self.addEventListener('fetch', function (event) {
|
||||||
|
event.respondWith(responder(event));
|
||||||
});
|
});
|
||||||
addEventListener("message", async (e) => {
|
addEventListener("message", async (e) => {
|
||||||
console.log(`Message received: ${e.data}`);
|
console.log(`Message received:`, e.data);
|
||||||
switch (e.data.type) {
|
switch (e.data.type) {
|
||||||
case "updateMain":
|
case "update_app":
|
||||||
const cache = await caches.open(cacheName);
|
const cache = await caches.open(cacheName);
|
||||||
console.log(`[Service Worker] Caching new resource: main.js`);
|
console.log(`[Service Worker] Caching resources`);
|
||||||
cache.put("/main.js", new Response());
|
// cache.put("/main.js", new Response());
|
||||||
|
for (let item of contentToCache) {
|
||||||
|
cache.delete(item);
|
||||||
|
}
|
||||||
|
await cache.addAll(contentToCache);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"sw.js","sourceRoot":"","sources":["src/sw.ts"],"names":[],"mappings":";AAAA,yBAAyB;AACzB,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,MAAM,cAAc,GAAG;IACvB,aAAa;IACb,UAAU;IACV,gBAAgB;IAChB,QAAQ;IACR,iBAAiB;IACjB,eAAe;IACf,cAAc;CACb,CAAC;AAEF,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAK,EAAE,EAAE;IACzC,CAAC,CAAC,SAAS,CACT,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CACT,qDAAqD,EACrD,cAAc,CACf,CAAC;QACF,MAAM,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACrC,CAAC,CAAC,EAAE,CACL,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAK,EAAE,EAAE;IACvC,CAAC,CAAC,WAAW,CACX,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,EAAE,CAAC;YACN,OAAO,CAAC,GAAG,CACT,4CAA4C,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAC5D,CAAC;YACF,OAAO,CAAC,CAAC;QACX,CAAC;QAED,IAAI,QAAQ,CAAC;QACb,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CACT,8DAA8D,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAC9E,CAAC;YAEF,QAAQ,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CACT,8CAA8C,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAC9D,CAAC;QAEF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QAC/D,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;QACvC,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC,EAAE,CACL,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,gBAAgB,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IACtC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAE3C,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,YAAY;YACf,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;YAC9D,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,QAAQ,EAAE,CAAC,CAAC;YACtC,MAAM;IACV,CAAC;AACH,CAAC,CAAC,CAAC"}
|
{"version":3,"file":"sw.js","sourceRoot":"","sources":["src/sw.ts"],"names":[],"mappings":"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"}
|
||||||
132
webRTC.js
132
webRTC.js
@@ -1,56 +1,96 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const config = {
|
class PeerManager {
|
||||||
iceServers: [{ urls: "stun: stun.l.google.com" }],
|
connect(peerID) {
|
||||||
|
// Connect to the peer that has the peer id peerID
|
||||||
|
}
|
||||||
|
disconnect(peerID) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PeerConnection {
|
||||||
|
}
|
||||||
|
PeerConnection.config = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: "stun:stun.l.google.com" },
|
||||||
|
{ urls: "stun:stun1.l.google.com" },
|
||||||
|
{ urls: "stun:stun2.l.google.com" },
|
||||||
|
{ urls: "stun:stun3.l.google.com" },
|
||||||
|
{ urls: "stun:stun4.l.google.com" },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
let localConnection = new RTCPeerConnection();
|
const config = {
|
||||||
function handleSendChannelStatusChange() {
|
iceServers: [{ urls: "stun:stun.mystunserver.tld" }],
|
||||||
console.log(handleSendChannelStatusChange);
|
};
|
||||||
|
let polite = true;
|
||||||
|
// const signaler = new SignalingChannel();
|
||||||
|
const signaler = {};
|
||||||
|
const pc = new RTCPeerConnection(config);
|
||||||
|
const constraints = { audio: true, video: true };
|
||||||
|
const selfVideo = document.querySelector("video.selfview");
|
||||||
|
const remoteVideo = document.querySelector("video.remoteview");
|
||||||
|
async function start() {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
for (const track of stream.getTracks()) {
|
||||||
|
pc.addTrack(track, stream);
|
||||||
}
|
}
|
||||||
let sendChannel = localConnection.createDataChannel("sendChannel");
|
// selfVideo.srcObject = stream;
|
||||||
sendChannel.onopen = handleSendChannelStatusChange;
|
|
||||||
sendChannel.onclose = handleSendChannelStatusChange;
|
|
||||||
let remoteConnection = new RTCPeerConnection();
|
|
||||||
remoteConnection.ondatachannel = receiveChannelCallback;
|
|
||||||
localConnection.onicecandidate = (e) => !e.candidate ||
|
|
||||||
remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
|
|
||||||
remoteConnection.onicecandidate = (e) => !e.candidate ||
|
|
||||||
localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
|
|
||||||
function handleCreateDescriptionError(error) {
|
|
||||||
console.log(`Unable to create an offer: ${error.toString()}`);
|
|
||||||
}
|
}
|
||||||
function handleLocalAddCandidateSuccess() {
|
catch (err) {
|
||||||
console.log('handleLocalAddCandidateSuccess');
|
console.error(err);
|
||||||
}
|
|
||||||
function handleRemoteAddCandidateSuccess() {
|
|
||||||
console.log('handleRemoteAddCandidateSuccess');
|
|
||||||
}
|
|
||||||
function handleAddCandidateError() {
|
|
||||||
console.log("Oh noes! addICECandidate failed!");
|
|
||||||
}
|
|
||||||
localConnection
|
|
||||||
.createOffer()
|
|
||||||
.then((offer) => localConnection.setLocalDescription(offer))
|
|
||||||
.then(() => remoteConnection.setRemoteDescription(localConnection.localDescription))
|
|
||||||
.then(() => remoteConnection.createAnswer())
|
|
||||||
.then((answer) => remoteConnection.setLocalDescription(answer))
|
|
||||||
.then(() => localConnection.setRemoteDescription(remoteConnection.localDescription))
|
|
||||||
.catch(handleCreateDescriptionError);
|
|
||||||
function handleReceiveChannelStatusChange(event) {
|
|
||||||
let receiveChannel = event.channel;
|
|
||||||
if (receiveChannel) {
|
|
||||||
console.log(`Receive channel's status has changed to ${receiveChannel.readyState}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function receiveChannelCallback(event) {
|
pc.ontrack = ({ track, streams }) => {
|
||||||
let receiveChannel = event.channel;
|
track.onunmute = () => {
|
||||||
receiveChannel.onmessage = handleReceiveMessage;
|
// if (remoteVideo.srcObject) {
|
||||||
receiveChannel.onopen = handleReceiveChannelStatusChange;
|
// return;
|
||||||
receiveChannel.onclose = handleReceiveChannelStatusChange;
|
// }
|
||||||
|
// remoteVideo.srcObject = streams[0];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let makingOffer = false;
|
||||||
|
pc.onnegotiationneeded = async () => {
|
||||||
|
try {
|
||||||
|
makingOffer = true;
|
||||||
|
await pc.setLocalDescription();
|
||||||
|
signaler.send({ description: pc.localDescription });
|
||||||
}
|
}
|
||||||
function sendMessage(message) {
|
catch (err) {
|
||||||
sendChannel.send(message);
|
console.error(err);
|
||||||
}
|
}
|
||||||
function handleReceiveMessage(event) {
|
finally {
|
||||||
console.log(event.data);
|
makingOffer = false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
pc.onicecandidate = ({ candidate }) => signaler.send({ candidate });
|
||||||
|
let ignoreOffer = false;
|
||||||
|
signaler.onmessage = async ({ data: { description, candidate } }) => {
|
||||||
|
try {
|
||||||
|
if (description) {
|
||||||
|
const offerCollision = description.type === "offer" &&
|
||||||
|
(makingOffer || pc.signalingState !== "stable");
|
||||||
|
ignoreOffer = !polite && offerCollision;
|
||||||
|
if (ignoreOffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await pc.setRemoteDescription(description);
|
||||||
|
if (description.type === "offer") {
|
||||||
|
await pc.setLocalDescription();
|
||||||
|
signaler.send({ description: pc.localDescription });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (candidate) {
|
||||||
|
try {
|
||||||
|
await pc.addIceCandidate(candidate);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (!ignoreOffer) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
//# sourceMappingURL=webRTC.js.map
|
//# sourceMappingURL=webRTC.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"webRTC.js","sourceRoot":"","sources":["src/webRTC.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"webRTC.js","sourceRoot":"","sources":["src/webRTC.ts"],"names":[],"mappings":"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"}
|
||||||
Reference in New Issue
Block a user