Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor network module #22

Merged
merged 2 commits into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 57 additions & 22 deletions web/src/network/peerUtils.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,62 @@
import Peer from "peerjs";

export type Conn = Peer["connections"][0];
export const isValidPeerJsId = (
roomId: string,
peerJsId: unknown
): peerJsId is string =>
typeof peerJsId === "string" && peerJsId.startsWith(`${roomId}_`);

export const isConnectedConn = (conn: Conn, ignoreConnecting = false) => {
if (!conn) return false;
const peerConn = conn.peerConnection;
if (!peerConn) return false;
const connState = peerConn.connectionState;
if (connState === "connected") return true;
if (!ignoreConnecting) {
if (connState === "connecting" || connState === "new") {
return true;
}
}
// for safari
if (!connState && peerConn.signalingState === "stable") return true;
return false;
};
export const generatePeerJsId = (roomId: string, peerId: number) =>
`${roomId}_${peerId}`;

export const getPeerIdFromPeerJsId = (peerJsId: string) =>
Number(peerJsId.split("_")[1]);

export const getPeerIdFromConn = (conn: Peer.DataConnection) =>
getPeerIdFromPeerJsId(conn.peer);

export const getLivePeers = (myPeer: Peer) => {
const peers = Object.keys(myPeer.connections);
const livePeers = peers.filter((peer) =>
myPeer.connections[peer].some((conn: Conn) => isConnectedConn(conn, true))
);
return livePeers;
export const createConnectionMap = () => {
type Value = {
conn: Peer.DataConnection;
live: boolean;
};
const map = new Map<string, Value>();
const addConn = (conn: Peer.DataConnection) => {
map.set(conn.peer, { conn, live: false });
};
const markLive = (conn: Peer.DataConnection) => {
const value = map.get(conn.peer);
if (value) {
value.live = true;
}
};
const isLive = (peerJsId: string) => {
const value = map.get(peerJsId);
return value ? value.live : false;
};
const hasConn = (peerJsId: string) => map.has(peerJsId);
const delConn = (conn: Peer.DataConnection) => {
const value = map.get(conn.peer);
if (value && value.conn === conn) {
map.delete(conn.peer);
}
};
const getLivePeerJsIds = () =>
Array.from(map.keys()).filter((k) => map.get(k)?.live);
const forEachLiveConns = (callback: (conn: Peer.DataConnection) => void) => {
for (const value of map.values()) {
if (value.live) {
callback(value.conn);
}
}
};
return {
addConn,
markLive,
isLive,
hasConn,
delConn,
getLivePeerJsIds,
forEachLiveConns,
};
};
115 changes: 52 additions & 63 deletions web/src/network/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import Peer from "peerjs";

import { rand4 } from "../utils/crypto";
import { sleep } from "../utils/sleep";
import { Conn, isConnectedConn, getLivePeers } from "./peerUtils";
import {
isValidPeerJsId,
generatePeerJsId,
getPeerIdFromPeerJsId,
getPeerIdFromConn,
createConnectionMap,
} from "./peerUtils";

const SEED_PEERS = 5; // config
const guessSeed = (id: string) => Number(id.split("_")[1]) < SEED_PEERS;
const guessSeed = (id: string) => getPeerIdFromPeerJsId(id) < SEED_PEERS;

export type NetworkStatus =
| { type: "CONNECTING_SEED_PEERS" }
Expand All @@ -27,7 +33,7 @@ const createMyPeer = (
updateNetworkStatus({ type: "INITIALIZING_PEER", index });
const isSeed = index < SEED_PEERS;
const peerId = isSeed ? index : rand4();
const id = `${roomId}_${peerId}`;
const id = generatePeerJsId(roomId, peerId);
console.log("createMyPeer", index, id);
const peer = new Peer(id);
return new Promise((resolve) => {
Expand Down Expand Up @@ -58,60 +64,45 @@ export const createRoom = (
) => {
let myPeer: Peer | null = null;
let lastBroadcastData: unknown | null = null;
const connMap = createConnectionMap();

const showConnectedStatus = (closedPeerId?: number) => {
const showConnectedStatus = () => {
if (!myPeer) return;
const peerIds = getLivePeers(myPeer)
.map((id) => Number(id.split("_")[1]))
.filter((peerId) => peerId !== closedPeerId);
const peerIds = connMap.getLivePeerJsIds().map(getPeerIdFromPeerJsId);
updateNetworkStatus({ type: "CONNECTED_PEERS", peerIds });
};

const connectPeer = (id: string) => {
if (!myPeer) return;
if (myPeer.id === id) return;
const conns = myPeer.connections[id];
const hasEffectiveConn = conns && conns.some(isConnectedConn);
if (hasEffectiveConn) return;
console.log(
"connectPeer",
id,
conns &&
conns.map(
(c: Conn) => c.peerConnection && c.peerConnection.connectionState
)
);
if (connMap.hasConn(id)) return;
console.log("connectPeer", id);
const conn = myPeer.connect(id, { serialization: "json" });
connMap.addConn(conn);
initConnection(conn);
};

const broadcastData = (data: unknown) => {
lastBroadcastData = data;
if (myPeer) {
Object.keys(myPeer.connections).forEach((key) => {
if (!myPeer) return;
const peers = getLivePeers(myPeer);
myPeer.connections[key].forEach((conn: Conn) => {
if (conn.open) {
try {
conn.send({ data, peers });
} catch (e) {
console.error("broadcastData", e);
}
}
});
});
}
if (!myPeer) return;
const peers = connMap.getLivePeerJsIds();
connMap.forEachLiveConns((conn) => {
try {
conn.send({ data, peers });
} catch (e) {
console.error("broadcastData", e);
}
});
};

const handlePayload = (conn: Conn, payload: unknown) => {
const handlePayload = (conn: Peer.DataConnection, payload: unknown) => {
try {
const peerId = Number(conn.peer.split("_")[1]);
const peerId = getPeerIdFromConn(conn);
if (payload && typeof payload === "object") {
receiveData(peerId, (payload as { data: unknown }).data);
if (Array.isArray((payload as { peers: unknown }).peers)) {
(payload as { peers: unknown[] }).peers.forEach((peer) => {
if (typeof peer === "string" && peer.startsWith(`${roomId}_`)) {
if (isValidPeerJsId(roomId, peer)) {
connectPeer(peer);
}
});
Expand All @@ -122,41 +113,43 @@ export const createRoom = (
}
};

const initConnection = (conn: Conn) => {
const initConnection = (conn: Peer.DataConnection) => {
conn.on("open", () => {
connMap.markLive(conn);
showConnectedStatus();
if (myPeer && lastBroadcastData) {
conn.send({
data: lastBroadcastData,
peers: getLivePeers(myPeer),
peers: connMap.getLivePeerJsIds(),
});
}
});
conn.on("data", (payload: unknown) => handlePayload(conn, payload));
conn.on("close", async () => {
connMap.delConn(conn);
console.log("dataConnection closed", conn);
showConnectedStatus(Number(conn.peer.split("_")[1]));
if (guessSeed(conn.peer)) reInitMyPeer(conn.peer);
});
conn.on("error", async (err: Error) => {
console.error("dataConnection error", conn, err);
showConnectedStatus(Number(conn.peer.split("_")[1]));
showConnectedStatus();
if (guessSeed(conn.peer)) reInitMyPeer(conn.peer);
});
};

const initMyPeer = async () => {
if (myPeer) return;
myPeer = await createMyPeer(0, roomId, updateNetworkStatus);
if (process.env.NODE_ENV !== "production") {
(window as any).myPeer = myPeer;
}
myPeer.on("connection", (conn) => {
console.log("new connection received", conn);
const peerId = Number(conn.peer.split("_")[1]);
const peerId = getPeerIdFromConn(conn);
updateNetworkStatus({ type: "NEW_CONNECTION", peerId });
connMap.addConn(conn);
initConnection(conn);
connMap.markLive(conn);
});
updateNetworkStatus({ type: "CONNECTING_SEED_PEERS" });
for (let i = 0; i < SEED_PEERS; i += 1) {
const id = `${roomId}_${i}`;
const id = generatePeerJsId(roomId, i);
connectPeer(id);
}
};
Expand All @@ -167,36 +160,32 @@ export const createRoom = (
if (guessSeed(myPeer.id)) return;
const waitSec = 30 + Math.floor(Math.random() * 60);
console.log(
"Disconnected seed peer: " +
disconnectedId.split("_")[1] +
", reinit in " +
waitSec +
"sec..."
`Disconnected seed peer: ${getPeerIdFromPeerJsId(
disconnectedId
)}, reinit in ${waitSec}sec...`
);
await sleep(waitSec * 1000);
if (!myPeer) return;
if (guessSeed(myPeer.id)) return;
let checkSeeds = true;
for (let i = 0; i < SEED_PEERS; i += 1) {
const id = `${roomId}_${i}`;
const conns = myPeer.connections[id] || [];
if (!conns.some(isConnectedConn)) {
checkSeeds = false;
}
}
if (checkSeeds) {
const existsAllSeeds = Array.from(Array(SEED_PEERS).keys()).every((i) => {
const id = generatePeerJsId(roomId, i);
return connMap.isLive(id);
});
if (existsAllSeeds) {
showConnectedStatus();
return;
}
myPeer.destroy();
const oldPeer = myPeer;
myPeer = null;
oldPeer.destroy();
initMyPeer();
};

const dispose = () => {
if (myPeer) {
myPeer.destroy();
// do not set null
const oldPeer = myPeer;
myPeer = null;
oldPeer.destroy();
}
};

Expand Down
1 change: 1 addition & 0 deletions web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dom.iterable",
"esnext"
],
"downlevelIteration": true,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
Expand Down