Files
dce-go/_examples/apis/flex-websocket.html
T

483 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Websocket test</title>
<style>
body{width: 800px; margin: 50px auto}
.chat-box {display: flex; border: 1px solid #aaa; border-radius: 6px;}
.cb-left {flex: auto; border-right: 1px solid #aaa}
.chat-board {height: 500px; overflow-y: auto; border-bottom: 1px solid #aaa;}
.msg-bubble {padding: 10px;}
.msg-bubble.self {text-align: right}
.msg-head {font-size: 14px; color: #777;}
.msg-head .time-wrap {font-size: 12px; margin-left: 10px;}
.msg-bubble.self .time-wrap {margin: 0 10px 0 0;}
.msg-bubble p {margin: 0; padding: 10px 5px;}
.editor-wrap {display: flex;}
.editor-wrap #editor {flex: auto; border: 0; border-right: 1px solid #aaa; border-bottom-left-radius: 8px;}
.editor-wrap button {width: 80px; height: 48px; border: 0;}
.group-members {width: 160px; list-style: none; margin: 0; padding: 0;}
.group-members li {margin: 10px;}
</style>
</head>
<body>
<h1>[<span class="online-state"></span>] <span class="session-user"></span>'s group chat</h1>
<div class="chat-box">
<div class="cb-left">
<div class="chat-board"></div>
<form class="editor-wrap">
<textarea id="editor"></textarea>
<button type="submit">Send</button>
</form>
</div>
<ul class="group-members"></ul>
</div>
<script>
class FlexWebsocketClient {
ws
sidHandler
requestMapping = {}
listenMapping = {}
constructor(address, sidHandler, onopen, onclose, onmessage) {
const self = this
this.sidHandler = sidHandler
this.ws = new WebSocket(address)
this.ws.binaryType = "arraybuffer"
this.ws.onopen = onopen
this.ws.onclose = onclose
this.ws.onmessage = function (ev) {
let fp
try {
fp = FlexPackage.deserialize(Array.from(new Uint8Array(ev.data)))
} catch (e) {
onmessage && onmessage(ev.data) || console.error(e)
return
}
self.sidHandler(fp)
if (fp.code || fp.message) {
console.warn(`Code: ${fp.code}, Message: ${fp.message}`)
}
if (fp.id in self.requestMapping) {
self.requestMapping[fp.id](fp)
} else if (fp.path in self.listenMapping) {
self.listenMapping[fp.path](fp)
} else if (onmessage) {
onmessage(fp, true)
}
}
}
bind(path, callback) {
this.listenMapping[path] = callback
}
sendText(content) {
this.ws.send(content)
}
send(path, content, id) {
const pkg = FlexPackage.new(path, content, this.sidHandler(), id)
this.sendText(pkg.serialize())
return pkg.id
}
async request(path, content) {
const reqId = this.send(path, content, -1)
const self = this
return new Promise(resolve => {
self.requestMapping[reqId] = function(fp) {
resolve(fp.body)
delete self.requestMapping[fp.id]
}
})
}
}
class PackageField {
constructor(field, kind) {
this.field = field
this.kind = kind
}
}
const baseFields = [
new PackageField("id", "uint"),
new PackageField("path", "string"),
new PackageField("numPath", "uint"),
new PackageField("sid", "string"),
new PackageField("code", "int"),
new PackageField("message", "string"),
new PackageField("body", "bytes"),
];
class FlexPackage {
static #reqId = 0
id
path
numPath
sid
code
message
body
static new(path, body, sid, id, numPath) {
if (id === -1) {
id = ++FlexPackage.#reqId
}
const fp = new FlexPackage
fp.id = id
fp.path = path
fp.sid = sid
fp.numPath = numPath
fp.body = body
return fp
}
serialize() {
let flag = 0;
const totalFields = baseFields.length;
const numHeadVec = [];
const textBuffer = [];
const bodyBuffer = [];
for (const [i, fc] of baseFields.entries()) {
let nh;
let textSeq;
const prop = this[fc.field];
if (prop === undefined) {
continue;
} else if (fc.kind === "int") {
nh = FlexNum.intPackHead(prop);
} else if (fc.kind === "uint") {
nh = FlexNum.uintPackHead(prop);
} else if (fc.kind === "string") {
textSeq = new TextEncoder().encode(prop);
nh = FlexNum.non0LenPackHead(textSeq.length);
} else if (fc.kind === "bytes") {
textSeq = new TextEncoder().encode(prop);
nh = FlexNum.non0LenPackHead(textSeq.length);
} else {
continue;
}
flag |= 1 << i;
numHeadVec.push(nh);
if (fc.kind === "bytes") {
textSeq && bodyBuffer.push(textSeq);
} else if (textSeq) {
textBuffer.push(textSeq);
}
}
const flagSeq = FlexNum.uintSerialize(flag);
const flagLen = flagSeq.length;
const buffer = [...flagSeq];
buffer[buffer.length + flagLen - 1] = undefined;
for (const [i, nh] of numHeadVec.entries()) {
buffer[flagLen+i] = nh[0];
buffer.push(...FlexNum.packBody(nh));
}
for (const part of textBuffer) {
buffer.push(...part);
}
for (const part of bodyBuffer) {
buffer.push(...part);
}
return Uint8Array.from(buffer);
}
static deserialize(seq) {
const fp = new FlexPackage;
const head = seq.splice(0, 1)?.[0];
if (!head) {
return fp;
}
const nh = FlexNum.parseHead(head, false);
let flag = nh[0];
if (nh[1] > 0) {
const flagBodySeq = seq.splice(0, nh[1]);
flagBodySeq.unshift(nh[3]);
flag = FlexNum.parse(flagBodySeq);
}
const onesCount = FlexNum.onesCount(flag);
const numHeadList = seq.splice(0, onesCount)
if (numHeadList.length < onesCount) {
return fp;
}
const numInfoList = new Array(onesCount).fill(0)
let nhi = 0;
let bitsLen = FlexNum.bitsLen(flag);
for (let i=0; i<bitsLen; i++) {
if (((1 << i) & flag) === 0) {
continue;
}
const nh = FlexNum.parseHead(numHeadList[nhi], true);
const numBodySeq = seq.splice(0, nh[1]);
numInfoList[nhi] = [i, nh, numBodySeq];
nhi ++;
}
if (bitsLen > baseFields.length) {
return fp;
}
for (const ni of numInfoList) {
const fc = baseFields[ni[0]];
if (fc.kind === "string" || fc.kind === "bytes") {
const len = FlexNum.non0LenParse([ni[1][3], ...ni[2]]);
const sq = seq.splice(0, len);
if (sq.length < len) {
return fp;
}
fp[fc.field] = new TextDecoder().decode(Uint8Array.from(sq));
} else if (fc.kind === "int") {
const val = FlexNum.intParse([ni[1][0], ...ni[2]], ni[1][2]);
fp[fc.field] = val;
} else if (fc.kind === "uint") {
const val = FlexNum.parse([ni[1][3], ...ni[2]]);
fp[fc.field] = val;
}
}
return fp
}
}
class FlexNum {
static uintSerialize(unsigned) {
return this.#serialize(...this.uintPackHead(unsigned));
}
static intSerialize(integer) {
return this.#serialize(...this.intPackHead(integer));
}
static non0LenPackHead(unsigned) {
return this.uintPackHead(unsigned - 1);
}
static uintPackHead(unsigned) {
const usize = Math.abs(unsigned);
const bitsLen = this.bitsLen(usize);
const [head, bytesLen] = this.#packHead(usize, bitsLen);
return [head, bytesLen, bitsLen, usize];
}
static intPackHead(integer) {
let unsigned = 0
if (integer < 0) {
unsigned = Math.abs(integer)
}
let bitsLen = FlexNum.bitsLen(unsigned)
let [head, bytesLen] = FlexNum.#packHead(unsigned, bitsLen)
if (integer < 0) {
let negative = 1
if (bytesLen < 7) {
negative = 1 << (6 - bytesLen)
}
head |= negative
}
return [head, bytesLen, bitsLen, unsigned]
}
static #packHead(unsigned, bitsLen) {
let bytesLen = Math.floor(bitsLen / 8)
let headMaskShift = 8 - bytesLen
let headBits = 0
if (bytesLen > 5) {
bytesLen = 8
headMaskShift = 2
} else if (bitsLen%8 > 7-bytesLen) {
bytesLen ++
headMaskShift --
} else {
headBits |= unsigned >> (bytesLen * 8)
}
return [255 << headMaskShift & 255 | headBits, bytesLen]
}
static #serialize(head, bytesLen, bitsLen, u64) {
let units = new Uint8Array(bytesLen + 1)
units[0] = head
units.set(this.packBody(u64, bitsLen, bytesLen), 1)
return units
}
static packBody(usize, bitsLen, bytesLen) {
let units = new Uint8Array(bytesLen)
for (let i=0; i<bytesLen && i*8 < bitsLen; i++) {
units[bytesLen-i-1] = usize >> (i * 8) & 255
}
return units
}
static uintDeserialize(seq) {
[seq[0]] = this.parseHead(seq[0], false)
return this.parse(seq)
}
static intDeserialize(seq) {
const [headBits, _, negative] = this.parseHead(seq[0], true)
seq[0] = headBits
return this.intParse(seq, negative)
}
static parseHead(head, sign) {
let unsignedBits = 0
let bytesLen = 0
let negative = false
let originalBits = 0
for (let i = 0; i<8; i ++) {
if ((128 >> i & head) === 0) {
if ((bytesLen = i) > 5) {
bytesLen = 8
originalBits = 1 & head
} else {
originalBits = 127 >> bytesLen & head
}
break
}
}
unsignedBits = originalBits
if (sign) {
if (bytesLen === 8) {
negative = (1 & head) === 1
} else {
let signShift = 0
if ((negative = (64 >> bytesLen & head) > 0)) {
signShift = 1
}
unsignedBits = 127 >> bytesLen >> signShift & head
}
}
return [unsignedBits, bytesLen, negative, originalBits]
}
static intParse(seq, negative) {
const u64 = this.parse(seq)
if (negative) {
return -u64
}
return u64
}
static non0LenParse(seq) {
return this.parse(seq) + 1
}
static parse(seq) {
let u64 = 0
for (let i = 0; i < seq.length; i++) {
if (seq[i] > 0) {
u64 |= seq[i] << ((seq.length -i - 1) * 8)
}
}
return u64
}
static bitsLen(num) {
let len = 0
do {
len++
num >>= 1
} while (num > 0)
return len
}
static onesCount(num) {
let count = 0
for (let i = this.bitsLen(num) - 1; i >=0; i --) {
if ((1 << i & num) > 0) {
count ++
}
}
return count
}
}
const sessionUser = {}
const sessionUserElem = document.querySelector(".session-user")
const chatBoard = document.querySelector(".chat-board")
const editorWrap = document.querySelector(".editor-wrap")
const editor = document.getElementById("editor")
const onlineState = document.querySelector(".online-state")
const groupMembers = document.querySelector(".group-members")
function sidHandler(fp) {
if (fp) {
if (fp?.sid && fp.sid.length > 0) {
sessionStorage.setItem("session-id", fp.sid)
}
} else {
// return location.pathname.replace(/^\/+/, "").replace(/^([^/]+).*/, "$1")
return sessionStorage.getItem("session-id")
}
}
const SWC = new FlexWebsocketClient(
"{{.ServerAddr}}" + (sidHandler() ? "/"+sidHandler() : ""),
sidHandler,
() => onlineState.textContent = "Online",
() => onlineState.textContent = "Offline",
)
SWC.bind("sync-user-list", fp => {
groupMembers.innerHTML = ""
const resp = JSON.parse(fp.body)
if (resp.sessionUser) {
Object.assign(sessionUser, resp.sessionUser)
sessionUserElem.textContent = sessionUser.nick
}
for (const user of resp.userList) {
const userElem = document.createElement("li")
userElem.textContent = user.nick
if (user.id === sessionUser.id) {
userElem.className = "self"
}
groupMembers.appendChild(userElem)
}
})
SWC.bind("sync-new-message", fp => {
const msgPkg = JSON.parse(fp.body)
showMsg(msgPkg)
})
function showMsg(msgPkg) {
const isSelf = msgPkg.uid === sessionUser.id
const msgBubble = chatBoard.appendChild(document.createElement("div"))
msgBubble.className = "msg-bubble " + (isSelf ? "self" : "")
const msgHead = msgBubble.appendChild(document.createElement("div"))
msgHead.className = "msg-head"
const userWrap = msgHead.appendChild(document.createElement("span"))
userWrap.className = "user-wrap"
userWrap.textContent = isSelf ? "ME" : msgPkg.nick
const timeWrap = document.createElement("span")
timeWrap.className = "time-wrap"
timeWrap.textContent = msgPkg.time
if (isSelf) {
msgHead.insertBefore(timeWrap, userWrap)
} else {
msgHead.appendChild(timeWrap)
}
const msgContent = msgBubble.appendChild(document.createElement("p"))
msgContent.textContent = msgPkg.msg
chatBoard.scrollTop = chatBoard.scrollHeight
}
editorWrap.onsubmit = function () {
sendMsg()
return false
}
async function sendMsg() {
if (editor.value === "") {
alert("empty message cannot send")
return
}
await SWC.request("send", editor.value)
editor.value = ""
}
</script>
</body>
</html>