A.Olokhtonov
2 years ago
commit
4466640439
25 changed files with 1691 additions and 0 deletions
@ -0,0 +1,18 @@ |
|||||||
|
desk.local { |
||||||
|
redir /ws /ws/ |
||||||
|
redir /desk /desk/ |
||||||
|
|
||||||
|
handle /ws/* { |
||||||
|
reverse_proxy 127.0.0.1:3003 |
||||||
|
} |
||||||
|
|
||||||
|
handle /api/* { |
||||||
|
reverse_proxy 127.0.0.1:3003 |
||||||
|
} |
||||||
|
|
||||||
|
handle_path /desk/* { |
||||||
|
root * /code/desk2/client |
||||||
|
try_files {path} /index.html |
||||||
|
file_server |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
function on_down(e) { |
||||||
|
if (e.button === 1) { |
||||||
|
const event = undo_event(); |
||||||
|
queue_event(event); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (e.button != 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const x = Math.round(e.clientX + window.scrollX); |
||||||
|
const y = Math.round(e.clientY + window.scrollY); |
||||||
|
|
||||||
|
storage.state.drawing = true; |
||||||
|
|
||||||
|
if (storage.ctx1.lineWidth !== storage.cursor.width) { |
||||||
|
storage.ctx1.lineWidth = storage.cursor.width; |
||||||
|
} |
||||||
|
|
||||||
|
storage.cursor.x = x; |
||||||
|
storage.cursor.y = y; |
||||||
|
|
||||||
|
const predraw = predraw_event(x, y); |
||||||
|
storage.current_stroke.push(predraw); |
||||||
|
|
||||||
|
fire_event(predraw); |
||||||
|
} |
||||||
|
|
||||||
|
function on_move(e) { |
||||||
|
const last_x = storage.cursor.x; |
||||||
|
const last_y = storage.cursor.y; |
||||||
|
|
||||||
|
const x = storage.cursor.x = Math.round(e.clientX + window.scrollX); |
||||||
|
const y = storage.cursor.y = Math.round(e.clientY + window.scrollY); |
||||||
|
const width = storage.cursor.width; |
||||||
|
|
||||||
|
elements.cursor.style.transform = `translate3D(${Math.round(x - width / 2)}px, ${Math.round(y - width / 2)}px, 0)`; |
||||||
|
|
||||||
|
if (storage.state.drawing) { |
||||||
|
storage.ctx1.beginPath(); |
||||||
|
|
||||||
|
storage.ctx1.moveTo(last_x, last_y); |
||||||
|
storage.ctx1.lineTo(x, y); |
||||||
|
|
||||||
|
storage.ctx1.stroke(); |
||||||
|
|
||||||
|
const predraw = predraw_event(x, y); |
||||||
|
storage.current_stroke.push(predraw); |
||||||
|
|
||||||
|
fire_event(predraw); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function on_up(e) { |
||||||
|
if (e.button != 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
storage.state.drawing = false; |
||||||
|
|
||||||
|
const event = stroke_event(); |
||||||
|
storage.current_stroke = []; |
||||||
|
await queue_event(event); |
||||||
|
} |
||||||
|
|
||||||
|
function on_keydown(e) { |
||||||
|
if (e.code === 'Space' && !storage.state.drawing) { |
||||||
|
elements.cursor.classList.add('dhide'); |
||||||
|
elements.canvas0.classList.add('moving'); |
||||||
|
storage.state.moving = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function on_keyup(e) { |
||||||
|
if (e.code === 'Space' && storage.state.moving) { |
||||||
|
elements.cursor.classList.remove('dhide'); |
||||||
|
elements.canvas0.classList.remove('moving'); |
||||||
|
storage.state.moving = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function on_leave(e) { |
||||||
|
if (storage.state.drawing) { |
||||||
|
on_up(e); |
||||||
|
storage.state.drawing = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (storage.state.moving) { |
||||||
|
elements.cursor.classList.remove('dhide'); |
||||||
|
elements.canvas0.classList.remove('moving'); |
||||||
|
storage.state.moving = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
html, body { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.dhide { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
.canvas { |
||||||
|
cursor: crosshair; |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.canvas.moving { |
||||||
|
cursor: move; |
||||||
|
} |
||||||
|
|
||||||
|
.cursor { |
||||||
|
display: none; |
||||||
|
position: absolute; |
||||||
|
background: white; |
||||||
|
border-radius: 50%; |
||||||
|
box-sizing: border-box; |
||||||
|
border: 1px solid black; |
||||||
|
z-index: 10; |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
|
||||||
|
#canvas0 { |
||||||
|
z-index: 0; |
||||||
|
} |
||||||
|
|
||||||
|
#canvas1 { |
||||||
|
z-index: 1; |
||||||
|
pointer-events: none; |
||||||
|
opacity: 0.3; |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
function draw_stroke(stroke) { |
||||||
|
const points = stroke.points; |
||||||
|
|
||||||
|
if (points.length === 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// console.debug(points)
|
||||||
|
|
||||||
|
storage.ctx0.beginPath(); |
||||||
|
storage.ctx0.moveTo(points[0].x, points[0].y); |
||||||
|
|
||||||
|
for (let i = 1; i < points.length; ++i) { |
||||||
|
const p = points[i]; |
||||||
|
storage.ctx0.lineTo(p.x, p.y); |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx0.stroke(); |
||||||
|
} |
||||||
|
|
||||||
|
function redraw_predraw() { |
||||||
|
storage.ctx1.clearRect(0, 0, storage.ctx1.canvas.width, storage.ctx1.canvas.height); |
||||||
|
} |
||||||
|
|
||||||
|
function predraw_user(user_id, event) { |
||||||
|
if (!(user_id in storage.predraw)) { |
||||||
|
storage.predraw[user_id] = []; |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx1.beginPath(); |
||||||
|
if (storage.predraw[user_id].length > 0) { |
||||||
|
const last = storage.predraw[user_id][storage.predraw[user_id].length - 1]; |
||||||
|
storage.ctx1.moveTo(last.x, last.y); |
||||||
|
storage.ctx1.lineTo(event.x, event.y); |
||||||
|
} else { |
||||||
|
storage.ctx1.moveTo(event.x, event.y); |
||||||
|
} |
||||||
|
storage.ctx1.stroke(); |
||||||
|
|
||||||
|
storage.predraw[user_id].push({ 'x': event.x, 'y': event.y }); |
||||||
|
} |
After Width: | Height: | Size: 957 B |
After Width: | Height: | Size: 671 B |
@ -0,0 +1,21 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<title>Desk</title> |
||||||
|
<link rel="icon" type="image/png" href="favicon.png"> |
||||||
|
<link rel="stylesheet" type="text/css" href="default.css"> |
||||||
|
<script type="text/javascript" src="index.js"></script> |
||||||
|
<script type="text/javascript" src="cursor.js"></script> |
||||||
|
<script type="text/javascript" src="websocket.js"></script> |
||||||
|
<script type="text/javascript" src="send.js"></script> |
||||||
|
<script type="text/javascript" src="recv.js"></script> |
||||||
|
<script type="text/javascript" src="math.js"></script> |
||||||
|
<script type="text/javascript" src="draw.js"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="cursor" id="cursor"></div> |
||||||
|
<canvas class="canvas" id="canvas0"></canvas> |
||||||
|
<canvas class="canvas" id="canvas1"></canvas> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,148 @@ |
|||||||
|
let ws = null; |
||||||
|
let ls = window.localStorage; |
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', main); |
||||||
|
|
||||||
|
const EVENT = Object.freeze({ |
||||||
|
PREDRAW: 10, |
||||||
|
STROKE: 20, |
||||||
|
UNDO: 30, |
||||||
|
REDO: 31, |
||||||
|
}); |
||||||
|
const MESSAGE = Object.freeze({ |
||||||
|
INIT: 100, |
||||||
|
SYN: 101, |
||||||
|
ACK: 102, |
||||||
|
FULL: 103, |
||||||
|
FIRE: 104, |
||||||
|
JOIN: 105, |
||||||
|
}); |
||||||
|
|
||||||
|
const config = { |
||||||
|
ws_url: 'wss://desk.local/ws/', |
||||||
|
sync_timeout: 1000, |
||||||
|
ws_reconnect_timeout: 2000, |
||||||
|
}; |
||||||
|
|
||||||
|
const storage = { |
||||||
|
'state': { |
||||||
|
'drawing': false, |
||||||
|
'moving': false, |
||||||
|
}, |
||||||
|
|
||||||
|
'predraw': {}, |
||||||
|
'timers': {}, |
||||||
|
'me': {}, |
||||||
|
|
||||||
|
'sn': 0, // what WE think SERVER SN is (we tell this to the server, it uses to decide how much stuff to SYN to us)
|
||||||
|
'server_lsn': 0, // what SERVER said LSN is (used to decide how much stuff to SYN)
|
||||||
|
'lsn': 0, // what actual LSN is (can't just use length of local queue because it gets cleared)
|
||||||
|
'queue': [], // to server
|
||||||
|
'events': [], // from server
|
||||||
|
'current_stroke': [], |
||||||
|
|
||||||
|
'desk_id': 123, |
||||||
|
|
||||||
|
'canvas': { |
||||||
|
'width': 2000, |
||||||
|
'height': 2000, |
||||||
|
'offset_x': 0, |
||||||
|
'offset_y': 0, |
||||||
|
}, |
||||||
|
|
||||||
|
'cursor': { |
||||||
|
'width': 8, |
||||||
|
'x': 0, |
||||||
|
'y': 0, |
||||||
|
} |
||||||
|
}; |
||||||
|
const elements = { |
||||||
|
'cursor': null, |
||||||
|
'canvas0': null, |
||||||
|
'canvas1': null, |
||||||
|
}; |
||||||
|
|
||||||
|
function event_size(event) { |
||||||
|
let size = 1 + 1; // type + padding
|
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
size += 2 * 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
size += 2 + event.points.length * 2 * 2; // u16 (count) + count * (u16, u16) points
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return size; |
||||||
|
} |
||||||
|
|
||||||
|
function predraw_event(x, y) { |
||||||
|
return { |
||||||
|
'type': EVENT.PREDRAW, |
||||||
|
'x': x, |
||||||
|
'y': y |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function stroke_event() { |
||||||
|
return { |
||||||
|
'type': EVENT.STROKE, |
||||||
|
'points': storage.current_stroke, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function undo_event() { |
||||||
|
return { 'type': EVENT.UNDO }; |
||||||
|
} |
||||||
|
|
||||||
|
function redo_event() { |
||||||
|
return { 'type': EVENT.REDO }; |
||||||
|
} |
||||||
|
|
||||||
|
function main() { |
||||||
|
const url = new URL(window.location.href); |
||||||
|
const parts = url.pathname.split('/'); |
||||||
|
|
||||||
|
storage.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0; |
||||||
|
|
||||||
|
ws_connect(); |
||||||
|
|
||||||
|
elements.canvas0 = document.getElementById('canvas0'); |
||||||
|
elements.canvas1 = document.getElementById('canvas1'); |
||||||
|
elements.cursor = document.getElementById('cursor'); |
||||||
|
elements.cursor.style.width = storage.cursor.width + 'px'; |
||||||
|
elements.cursor.style.height = storage.cursor.width + 'px'; |
||||||
|
|
||||||
|
storage.canvas.rect = elements.canvas0.getBoundingClientRect(); |
||||||
|
|
||||||
|
storage.ctx0 = elements.canvas0.getContext('2d'); |
||||||
|
storage.ctx1 = elements.canvas1.getContext('2d'); |
||||||
|
storage.ctx1.canvas.width = storage.ctx0.canvas.width = storage.canvas.width; |
||||||
|
storage.ctx1.canvas.height = storage.ctx0.canvas.height = storage.canvas.height; |
||||||
|
|
||||||
|
storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round'; |
||||||
|
storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width; |
||||||
|
|
||||||
|
window.addEventListener('pointerdown', on_down) |
||||||
|
window.addEventListener('pointermove', on_move) |
||||||
|
window.addEventListener('pointerup', on_up); |
||||||
|
window.addEventListener('pointercancel', on_up); |
||||||
|
window.addEventListener('touchstart', (e) => e.preventDefault()); |
||||||
|
window.addEventListener('keydown', on_keydown); |
||||||
|
window.addEventListener('keyup', on_keyup); |
||||||
|
// window.addEventListener('pointerleave', on_leave);
|
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
function rdp_find_max(points, start, end) { |
||||||
|
const EPS = 0.5; |
||||||
|
|
||||||
|
let result = -1; |
||||||
|
let max_dist = 0; |
||||||
|
|
||||||
|
const a = points[start]; |
||||||
|
const b = points[end]; |
||||||
|
|
||||||
|
const dx = b.x - a.x; |
||||||
|
const dy = b.y - a.y; |
||||||
|
|
||||||
|
const dist_ab = Math.sqrt(dx * dx + dy * dy); |
||||||
|
const sin_theta = dy / dist_ab; |
||||||
|
const cos_theta = dx / dist_ab; |
||||||
|
|
||||||
|
for (let i = start; i < end; ++i) { |
||||||
|
const p = points[i]; |
||||||
|
|
||||||
|
const ox = p.x - a.x; |
||||||
|
const oy = p.y - a.y; |
||||||
|
|
||||||
|
const rx = cos_theta * ox + sin_theta * oy; |
||||||
|
const ry = -sin_theta * ox + cos_theta * oy; |
||||||
|
|
||||||
|
const x = rx + a.x; |
||||||
|
const y = ry + a.y; |
||||||
|
|
||||||
|
const dist = Math.abs(y - a.y); |
||||||
|
|
||||||
|
if (dist > EPS && dist > max_dist) { |
||||||
|
result = i; |
||||||
|
max_dist = dist; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function process_rdp_r(points, start, end) { |
||||||
|
let result = []; |
||||||
|
|
||||||
|
const max = rdp_find_max(points, start, end); |
||||||
|
|
||||||
|
if (max !== -1) { |
||||||
|
const before = process_rdp_r(points, start, max); |
||||||
|
const after = process_rdp_r(points, max, end); |
||||||
|
result = [...before, points[max], ...after]; |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function process_rdp(points) { |
||||||
|
const result = process_rdp_r(points, 0, points.length - 1); |
||||||
|
result.unshift(points[0]); |
||||||
|
result.push(points[points.length - 1]); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function process_ewmv(points, round = false) { |
||||||
|
const result = []; |
||||||
|
const alpha = 0.4; |
||||||
|
|
||||||
|
result.push(points[0]); |
||||||
|
|
||||||
|
for (let i = 1; i < points.length; ++i) { |
||||||
|
const p = points[i]; |
||||||
|
const x = Math.round(alpha * p.x + (1 - alpha) * result[result.length - 1].x); |
||||||
|
const y = Math.round(alpha * p.y + (1 - alpha) * result[result.length - 1].y); |
||||||
|
result.push({'x': x, 'y': y}); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function process_stroke(points) { |
||||||
|
const result0 = process_ewmv(points); |
||||||
|
const result1 = process_rdp(result0, true); |
||||||
|
return result1; |
||||||
|
} |
||||||
|
|
||||||
|
function stroke_stats(points, width) { |
||||||
|
let length = 0; |
||||||
|
let xmin = points[0].x, ymin = points[0].y; |
||||||
|
let xmax = xmin, ymax = ymin; |
||||||
|
|
||||||
|
for (let i = 0; i < points.length; ++i) { |
||||||
|
const point = points[i]; |
||||||
|
if (point.x < xmin) xmin = point.x; |
||||||
|
if (point.y < ymin) ymin = point.y; |
||||||
|
if (point.x > xmax) xmax = point.x; |
||||||
|
if (point.y > ymax) ymax = point.y; |
||||||
|
|
||||||
|
if (i > 0) { |
||||||
|
const last = points[i - 1]; |
||||||
|
const dx = point.x - last.x; |
||||||
|
const dy = point.y - last.y; |
||||||
|
length += Math.sqrt(dx * dx + dy * dy); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
xmin -= width; |
||||||
|
ymin -= width; |
||||||
|
xmax += width * 2; |
||||||
|
ymax += width * 2; |
||||||
|
|
||||||
|
const bbox = { |
||||||
|
'xmin': Math.floor(xmin), |
||||||
|
'ymin': Math.floor(ymin), |
||||||
|
'xmax': Math.ceil(xmax), |
||||||
|
'ymax': Math.ceil(ymax) |
||||||
|
}; |
||||||
|
|
||||||
|
return { |
||||||
|
'bbox': bbox, |
||||||
|
'length': length, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function rectangles_intersect(a, b) { |
||||||
|
const result = ( |
||||||
|
a.xmin <= b.xmax |
||||||
|
&& a.xmax >= b.xmin |
||||||
|
&& a.ymin <= b.ymax |
||||||
|
&& a.ymax >= b.ymin |
||||||
|
); |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function stroke_intersects_region(points, bbox) { |
||||||
|
const stats = stroke_stats(points, storage.cursor.width); |
||||||
|
return rectangles_intersect(stats.bbox, bbox); |
||||||
|
} |
@ -0,0 +1,229 @@ |
|||||||
|
function deserializer_create(buffer, dataview) { |
||||||
|
return { |
||||||
|
'offset': 0, |
||||||
|
'size': buffer.byteLength, |
||||||
|
'buffer': buffer, |
||||||
|
'view': dataview, |
||||||
|
'strview': new Uint8Array(buffer), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u8(d) { |
||||||
|
const value = d.view.getUint8(d.offset); |
||||||
|
d.offset += 1; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u16(d) { |
||||||
|
const value = d.view.getUint16(d.offset, true); |
||||||
|
d.offset += 2; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u32(d) { |
||||||
|
const value = d.view.getUint32(d.offset, true); |
||||||
|
d.offset += 4; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u16array(d, count) { |
||||||
|
const result = []; |
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) { |
||||||
|
const item = d.view.getUint16(d.offset, true); |
||||||
|
d.offset += 2; |
||||||
|
result.push(item); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function des_event(d) { |
||||||
|
const event = {}; |
||||||
|
|
||||||
|
event.type = des_u8(d); |
||||||
|
event.user_id = des_u32(d); |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
event.x = des_u16(d); |
||||||
|
event.y = des_u16(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
const point_count = des_u16(d); |
||||||
|
const coords = des_u16array(d, point_count * 2); |
||||||
|
|
||||||
|
event.points = []; |
||||||
|
|
||||||
|
for (let i = 0; i < point_count; ++i) { |
||||||
|
const x = coords[2 * i + 0]; |
||||||
|
const y = coords[2 * i + 1]; |
||||||
|
event.points.push({'x': x, 'y': y}); |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return event; |
||||||
|
} |
||||||
|
|
||||||
|
function redraw_region(bbox) { |
||||||
|
storage.ctx0.save(); |
||||||
|
storage.ctx0.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
||||||
|
|
||||||
|
storage.ctx0.beginPath(); |
||||||
|
storage.ctx0.rect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
||||||
|
storage.ctx0.clip(); |
||||||
|
|
||||||
|
for (const event of storage.events) { |
||||||
|
if (event.type === EVENT.STROKE && !event.deleted) { |
||||||
|
if (stroke_intersects_region(event.points, bbox)) { |
||||||
|
draw_stroke(event); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx0.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
function handle_event(event) { |
||||||
|
console.debug(`event type ${event.type} from user ${event.user_id}`); |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.STROKE: { |
||||||
|
if (event.user_id in storage.predraw || event.user_id === storage.me.id) { |
||||||
|
storage.predraw[event.user_id] = []; |
||||||
|
redraw_predraw(); |
||||||
|
} |
||||||
|
|
||||||
|
draw_stroke(event); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: { |
||||||
|
for (let i = storage.events.length - 1; i >=0; --i) { |
||||||
|
const other_event = storage.events[i]; |
||||||
|
if (other_event.type === EVENT.STROKE && other_event.user_id === event.user_id && !other_event.deleted) { |
||||||
|
other_event.deleted = true; |
||||||
|
const stats = stroke_stats(other_event.points, storage.cursor.width); |
||||||
|
redraw_region(stats.bbox); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function handle_message(d) { |
||||||
|
const message_type = des_u8(d); |
||||||
|
|
||||||
|
console.debug(message_type); |
||||||
|
|
||||||
|
switch (message_type) { |
||||||
|
case MESSAGE.JOIN: |
||||||
|
case MESSAGE.INIT: { |
||||||
|
storage.me.id = des_u32(d); |
||||||
|
storage.server_lsn = des_u32(d); |
||||||
|
|
||||||
|
if (storage.server_lsn > storage.lsn) { |
||||||
|
// Server knows something that we don't
|
||||||
|
storage.lsn = storage.server_lsn; |
||||||
|
} |
||||||
|
|
||||||
|
if (message_type === MESSAGE.JOIN) { |
||||||
|
ls.setItem('sessionId', des_u32(d)); |
||||||
|
console.debug('join in'); |
||||||
|
} else { |
||||||
|
console.debug('init in'); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const event_count = des_u32(d); |
||||||
|
|
||||||
|
console.debug(`${event_count} events in init`); |
||||||
|
storage.ctx0.clearRect(0, 0, storage.ctx0.canvas.width, storage.ctx0.canvas.height); |
||||||
|
|
||||||
|
for (let i = 0; i < event_count; ++i) { |
||||||
|
const event = des_event(d); |
||||||
|
handle_event(event); |
||||||
|
storage.events.push(event); |
||||||
|
} |
||||||
|
|
||||||
|
send_ack(event_count); |
||||||
|
|
||||||
|
sync_queue(); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.FIRE: { |
||||||
|
const user_id = des_u32(d); |
||||||
|
const predraw_event = des_event(d); |
||||||
|
|
||||||
|
predraw_user(user_id, predraw_event); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.ACK: { |
||||||
|
const lsn = des_u32(d); |
||||||
|
|
||||||
|
console.debug(`ack ${lsn} in`); |
||||||
|
|
||||||
|
if (lsn > storage.server_lsn) { |
||||||
|
// ACKs may arrive out of order
|
||||||
|
storage.server_lsn = lsn; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.SYN: { |
||||||
|
const sn = des_u32(d); |
||||||
|
const count = des_u32(d); |
||||||
|
|
||||||
|
const we_expect = sn - storage.sn; |
||||||
|
const first = count - we_expect; |
||||||
|
|
||||||
|
console.debug(`syn ${sn} in`); |
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) { |
||||||
|
const event = des_event(d); |
||||||
|
if (i >= first) { |
||||||
|
handle_event(event); |
||||||
|
storage.events.push(event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
storage.sn = sn; |
||||||
|
|
||||||
|
await send_ack(sn); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
function serializer_create(size) { |
||||||
|
const buffer = new ArrayBuffer(size); |
||||||
|
return { |
||||||
|
'offset': 0, |
||||||
|
'size': size, |
||||||
|
'buffer': buffer, |
||||||
|
'view': new DataView(buffer), |
||||||
|
'strview': new Uint8Array(buffer), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_u8(s, value) { |
||||||
|
s.view.setUint8(s.offset, value); |
||||||
|
s.offset += 1; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_u16(s, value) { |
||||||
|
s.view.setUint16(s.offset, value, true); |
||||||
|
s.offset += 2; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_u32(s, value) { |
||||||
|
s.view.setUint32(s.offset, value, true); |
||||||
|
s.offset += 4; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_event(s, event) { |
||||||
|
ser_u8(s, event.type); |
||||||
|
ser_u8(s, 0); // padding for 16bit alignment
|
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
ser_u16(s, event.x); |
||||||
|
ser_u16(s, event.y); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
ser_u16(s, event.points.length); |
||||||
|
|
||||||
|
console.debug('original', event.points); |
||||||
|
|
||||||
|
for (const point of event.points) { |
||||||
|
ser_u16(s, point.x); |
||||||
|
ser_u16(s, point.y); |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function send_ack(sn) { |
||||||
|
const s = serializer_create(1 + 4); |
||||||
|
|
||||||
|
ser_u8(s, MESSAGE.ACK); |
||||||
|
ser_u32(s, sn); |
||||||
|
|
||||||
|
console.debug(`ack ${sn} out`); |
||||||
|
|
||||||
|
if (ws) await ws.send(s.buffer); |
||||||
|
} |
||||||
|
|
||||||
|
async function sync_queue() { |
||||||
|
if (ws === null) { |
||||||
|
console.debug('socket has closed, stopping SYNs'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let size = 1 + 1 + 4 + 4; // opcode + lsn + event count
|
||||||
|
let count = storage.lsn - storage.server_lsn; |
||||||
|
|
||||||
|
if (count === 0) { |
||||||
|
console.debug('server ACKed all events, clearing queue'); |
||||||
|
storage.queue.length = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = count - 1; i >= 0; --i) { |
||||||
|
const event = storage.queue[storage.queue.length - 1 - i]; |
||||||
|
size += event_size(event); |
||||||
|
} |
||||||
|
|
||||||
|
const s = serializer_create(size); |
||||||
|
|
||||||
|
ser_u8(s, MESSAGE.SYN); |
||||||
|
ser_u8(s, 0); // padding for 16bit alignment
|
||||||
|
ser_u32(s, storage.lsn); |
||||||
|
ser_u32(s, count); |
||||||
|
|
||||||
|
for (let i = count - 1; i >= 0; --i) { |
||||||
|
const event = storage.queue[storage.queue.length - 1 - i]; |
||||||
|
ser_event(s, event); |
||||||
|
} |
||||||
|
|
||||||
|
console.debug(`syn ${storage.lsn} out`); |
||||||
|
|
||||||
|
if (ws) await ws.send(s.buffer); |
||||||
|
|
||||||
|
setTimeout(sync_queue, config.sync_timeout); |
||||||
|
} |
||||||
|
|
||||||
|
function push_event(event) { |
||||||
|
storage.lsn += 1; |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.STROKE: { |
||||||
|
const points = process_stroke(event.points); |
||||||
|
storage.queue.push({ 'type': EVENT.STROKE, 'points': points }); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
storage.queue.push(event); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Queue an event and initialize repated sends until ACKed
|
||||||
|
function queue_event(event, skip = false) { |
||||||
|
push_event(event); |
||||||
|
|
||||||
|
if (skip) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (storage.timers.queue_sync) { |
||||||
|
clearTimeout(storage.timers.queue_sync); |
||||||
|
} |
||||||
|
|
||||||
|
sync_queue(); |
||||||
|
} |
||||||
|
|
||||||
|
// Fire and forget. Doesn't do anything if we are offline
|
||||||
|
async function fire_event(event) { |
||||||
|
const s = serializer_create(1 + event_size(event)); |
||||||
|
|
||||||
|
ser_u8(s, MESSAGE.FIRE); |
||||||
|
ser_event(s, event); |
||||||
|
|
||||||
|
if (ws) await ws.send(s.buffer); |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
function ws_connect() { |
||||||
|
const session_id = ls.getItem('sessionId') || '0'; |
||||||
|
const desk_id = storage.desk_id; |
||||||
|
|
||||||
|
ws = new WebSocket(`${config.ws_url}?deskId=${desk_id}&sessionId=${session_id}`); |
||||||
|
|
||||||
|
ws.addEventListener('open', on_open); |
||||||
|
ws.addEventListener('message', on_message); |
||||||
|
ws.addEventListener('error', on_error); |
||||||
|
ws.addEventListener('close', on_close); |
||||||
|
} |
||||||
|
|
||||||
|
function on_open() { |
||||||
|
clearTimeout(storage.timers.ws_reconnect); |
||||||
|
console.debug('open') |
||||||
|
} |
||||||
|
|
||||||
|
async function on_message(event) { |
||||||
|
const data = event.data; |
||||||
|
const message_data = await data.arrayBuffer(); |
||||||
|
const view = new DataView(message_data); |
||||||
|
const d = deserializer_create(message_data, view); |
||||||
|
|
||||||
|
await handle_message(d); |
||||||
|
} |
||||||
|
|
||||||
|
function on_close() { |
||||||
|
ws = null; |
||||||
|
console.debug('close'); |
||||||
|
storage.timers.ws_reconnect = setTimeout(ws_connect, config.ws_reconnect_timeout); |
||||||
|
} |
||||||
|
|
||||||
|
function on_error() { |
||||||
|
ws.close(); |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
import * as storage from './storage'; |
||||||
|
import * as server from './server'; |
||||||
|
|
||||||
|
storage.startup(); |
||||||
|
server.startup(); |
Binary file not shown.
@ -0,0 +1,4 @@ |
|||||||
|
export const HOST = '127.0.0.1'; |
||||||
|
export const PORT = 3003; |
||||||
|
export const DATADIR = 'data'; |
||||||
|
export const SYNC_TIMEOUT = 1000; |
Binary file not shown.
@ -0,0 +1,67 @@ |
|||||||
|
import { EVENT } from './enums'; |
||||||
|
|
||||||
|
export function create(dataview) { |
||||||
|
return { |
||||||
|
'offset': 0, |
||||||
|
'size': dataview.byteLength, |
||||||
|
'view': dataview, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function u8(d) { |
||||||
|
const value = d.view.getUint8(d.offset); |
||||||
|
d.offset += 1; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
export function u16(d) { |
||||||
|
const value = d.view.getUint16(d.offset, true); |
||||||
|
d.offset += 2; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
export function u32(d) { |
||||||
|
const value = d.view.getUint32(d.offset, true); |
||||||
|
d.offset += 4; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function u16array(d, count) { |
||||||
|
const array = new Uint16Array(d.view.buffer, d.offset, count); |
||||||
|
d.offset += count * 2; |
||||||
|
return array; |
||||||
|
} |
||||||
|
|
||||||
|
export function event(d) { |
||||||
|
const event = {}; |
||||||
|
|
||||||
|
event.type = u8(d); |
||||||
|
u8(d); // padding
|
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
event.x = u16(d); |
||||||
|
event.y = u16(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
const point_count = u16(d); |
||||||
|
event.points = u16array(d, point_count * 2); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
console.trace(); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return event; |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
export const SESSION = Object.freeze({ |
||||||
|
OPENED: 0, // nothing sent/recved yet
|
||||||
|
READY: 1, // init complete
|
||||||
|
CLOSED: 2, // socket closed, but we might continute this same session on another socket
|
||||||
|
}); |
||||||
|
|
||||||
|
export const EVENT = Object.freeze({ |
||||||
|
PREDRAW: 10, |
||||||
|
STROKE: 20, |
||||||
|
UNDO: 30, |
||||||
|
REDO: 31, |
||||||
|
}); |
||||||
|
|
||||||
|
export const MESSAGE = Object.freeze({ |
||||||
|
INIT: 100, |
||||||
|
SYN: 101, |
||||||
|
ACK: 102, |
||||||
|
FULL: 103, |
||||||
|
FIRE: 104, |
||||||
|
JOIN: 105, |
||||||
|
}); |
||||||
|
|
||||||
|
export const SNS = Object.freeze({ |
||||||
|
DESK: 1, |
||||||
|
SESSION: 2, |
||||||
|
}); |
@ -0,0 +1,4 @@ |
|||||||
|
export function route(req) { |
||||||
|
console.log('HTTP:', req.url); |
||||||
|
return new Response(req.url); |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
import crypto from 'crypto'; |
||||||
|
|
||||||
|
export function crypto_random32() { |
||||||
|
const arr = new Uint8Array(4); |
||||||
|
const dataview = new DataView(arr.buffer); |
||||||
|
|
||||||
|
crypto.getRandomValues(arr); |
||||||
|
|
||||||
|
return dataview.getUint32(0); |
||||||
|
} |
||||||
|
|
||||||
|
export function fast_random32() { |
||||||
|
return Math.floor(Math.random() * 4294967296); |
||||||
|
} |
@ -0,0 +1,129 @@ |
|||||||
|
import * as des from './deserializer'; |
||||||
|
import * as send from './send'; |
||||||
|
import * as math from './math'; |
||||||
|
import * as storage from './storage'; |
||||||
|
|
||||||
|
import { SESSION, MESSAGE, EVENT } from './enums'; |
||||||
|
import { sessions, desks } from './storage'; |
||||||
|
|
||||||
|
// Session ACKed events up to SN
|
||||||
|
function recv_ack(d, session) { |
||||||
|
const sn = des.u32(d); |
||||||
|
|
||||||
|
session.state = SESSION.READY; |
||||||
|
session.sn = sn; |
||||||
|
|
||||||
|
console.log(`ack ${sn} in`); |
||||||
|
} |
||||||
|
|
||||||
|
function handle_event(session, event) { |
||||||
|
switch (event.type) { |
||||||
|
case EVENT.STROKE: { |
||||||
|
event.stroke_id = math.fast_random32(); |
||||||
|
storage.put_stroke(event.stroke_id, session.desk_id, event.points); |
||||||
|
storage.put_event(event); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: { |
||||||
|
storage.put_event(event); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
console.trace(); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function recv_syn(d, session) { |
||||||
|
const padding = des.u8(d); |
||||||
|
const lsn = des.u32(d); |
||||||
|
const count = des.u32(d); |
||||||
|
|
||||||
|
console.log(`syn ${lsn} in, total size = ${d.size}`); |
||||||
|
|
||||||
|
const we_expect = lsn - session.lsn; |
||||||
|
const first = count - we_expect; |
||||||
|
const events = []; |
||||||
|
|
||||||
|
console.log(`we expect ${we_expect}, count ${count}`); |
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) { |
||||||
|
const event = des.event(d); |
||||||
|
if (i >= first) { |
||||||
|
event.desk_id = session.desk_id; |
||||||
|
event.user_id = session.user_id; |
||||||
|
event.stroke_id = null; |
||||||
|
handle_event(session, event); |
||||||
|
events.push(event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
desks[session.desk_id].sn += we_expect; |
||||||
|
desks[session.desk_id].events.push(...events); |
||||||
|
session.lsn = lsn; |
||||||
|
|
||||||
|
storage.save_desk_sn(session.desk_id, desks[session.desk_id].sn); |
||||||
|
storage.save_session_lsn(session.id, lsn); |
||||||
|
|
||||||
|
send.send_ack(session.ws, lsn); |
||||||
|
send.sync_desk(session.desk_id); |
||||||
|
} |
||||||
|
|
||||||
|
function recv_fire(d, session) { |
||||||
|
const event = des.event(d); |
||||||
|
|
||||||
|
for (const sid in sessions) { |
||||||
|
const other = sessions[sid]; |
||||||
|
|
||||||
|
if (other.id === session.id) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (other.state !== SESSION.READY) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (other.desk_id != session.desk_id) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
send.send_fire(other.ws, session.user_id, event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function handle_message(ws, d) { |
||||||
|
if (!(ws.data.session_id in sessions)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const session = sessions[ws.data.session_id]; |
||||||
|
const desk_id = session.desk_id; |
||||||
|
const message_type = des.u8(d); |
||||||
|
|
||||||
|
switch (message_type) { |
||||||
|
case MESSAGE.FIRE: { |
||||||
|
recv_fire(d, session); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.SYN: { |
||||||
|
recv_syn(d, session); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.ACK: { |
||||||
|
recv_ack(d, session); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
console.trace(); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,186 @@ |
|||||||
|
import * as math from './math'; |
||||||
|
import * as ser from './serializer'; |
||||||
|
import * as storage from './storage'; |
||||||
|
import * as config from './config'; |
||||||
|
|
||||||
|
import { MESSAGE, SESSION, EVENT } from './enums'; |
||||||
|
import { sessions, desks } from './storage'; |
||||||
|
|
||||||
|
function event_size(event) { |
||||||
|
let size = 1 + 4; // type + user_id
|
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
size += 2 * 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
size += 2; // point count
|
||||||
|
size += event.points.byteLength; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
console.trace(); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return size; |
||||||
|
} |
||||||
|
|
||||||
|
function create_session(ws, desk_id) { |
||||||
|
const user = { |
||||||
|
id: math.crypto_random32(), |
||||||
|
login: 'unnamed', |
||||||
|
}; |
||||||
|
|
||||||
|
const session = { |
||||||
|
id: math.crypto_random32(), |
||||||
|
user_id: user.id, |
||||||
|
desk_id: desk_id, |
||||||
|
state: SESSION.OPENED, |
||||||
|
sn: 0, |
||||||
|
lsn: 0, |
||||||
|
ws: ws, |
||||||
|
}; |
||||||
|
|
||||||
|
storage.create_user(user); |
||||||
|
storage.create_session(session); |
||||||
|
|
||||||
|
sessions[session.id] = session; |
||||||
|
|
||||||
|
return session; |
||||||
|
} |
||||||
|
|
||||||
|
export async function send_init(ws) { |
||||||
|
const session_id = ws.data.session_id; |
||||||
|
const desk_id = ws.data.desk_id; |
||||||
|
const desk = desks[desk_id]; |
||||||
|
|
||||||
|
let opcode = MESSAGE.INIT; |
||||||
|
let size = 1 + 4 + 4 + 4 + 4; // opcode + user_id + lsn + event count + stroke count
|
||||||
|
let session = null; |
||||||
|
|
||||||
|
if (session_id in sessions && sessions[session_id].desk_id == desk_id) { |
||||||
|
session = sessions[session_id]; |
||||||
|
} else { |
||||||
|
size += 4; // session id
|
||||||
|
opcode = MESSAGE.JOIN; |
||||||
|
session = create_session(ws, desk_id); |
||||||
|
ws.data.session_id = session.id; |
||||||
|
} |
||||||
|
|
||||||
|
session.desk_id = desk_id; |
||||||
|
session.ws = ws; |
||||||
|
session.sn = 0; // Always re-send everything on reconnect
|
||||||
|
session.state = SESSION.OPENED; |
||||||
|
|
||||||
|
console.log(`session ${session.id} opened`); |
||||||
|
|
||||||
|
for (const event of desk.events) { |
||||||
|
size += event_size(event); |
||||||
|
} |
||||||
|
|
||||||
|
const s = ser.create(size); |
||||||
|
|
||||||
|
ser.u8(s, opcode); |
||||||
|
ser.u32(s, session.user_id); |
||||||
|
ser.u32(s, session.lsn); |
||||||
|
|
||||||
|
if (opcode === MESSAGE.JOIN) { |
||||||
|
ser.u32(s, session.id); |
||||||
|
} |
||||||
|
|
||||||
|
ser.u32(s, desk.events.length); |
||||||
|
|
||||||
|
for (const event of desk.events) { |
||||||
|
ser.event(s, event); |
||||||
|
} |
||||||
|
|
||||||
|
await ws.send(s.buffer); |
||||||
|
} |
||||||
|
|
||||||
|
export function send_ack(ws, lsn) { |
||||||
|
const size = 1 + 4; // opcode + lsn
|
||||||
|
const s = ser.create(size); |
||||||
|
|
||||||
|
ser.u8(s, MESSAGE.ACK); |
||||||
|
ser.u32(s, lsn); |
||||||
|
|
||||||
|
console.log(`ack ${lsn} out`); |
||||||
|
|
||||||
|
ws.send(s.buffer); |
||||||
|
} |
||||||
|
|
||||||
|
export function send_fire(ws, user_id, event) { |
||||||
|
const s = ser.create(1 + 4 + event_size(event)); |
||||||
|
|
||||||
|
ser.u8(s, MESSAGE.FIRE); |
||||||
|
ser.u32(s, user_id); |
||||||
|
ser.event(s, event); |
||||||
|
|
||||||
|
ws.send(s.buffer); |
||||||
|
} |
||||||
|
|
||||||
|
async function sync_session(session_id) { |
||||||
|
if (!(session_id in sessions)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const session = sessions[session_id]; |
||||||
|
const desk = desks[session.desk_id]; |
||||||
|
|
||||||
|
if (session.state !== SESSION.READY) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let size = 1 + 4 + 4; // opcode + sn + event count
|
||||||
|
let count = desk.sn - session.sn; |
||||||
|
|
||||||
|
if (count === 0) { |
||||||
|
console.log('client ACKed all events'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = count - 1; i >= 0; --i) { |
||||||
|
const event = desk.events[desk.events.length - 1 - i]; |
||||||
|
size += event_size(event); |
||||||
|
} |
||||||
|
|
||||||
|
const s = ser.create(size); |
||||||
|
|
||||||
|
ser.u8(s, MESSAGE.SYN); |
||||||
|
ser.u32(s, desk.sn); |
||||||
|
ser.u32(s, count); |
||||||
|
|
||||||
|
for (let i = count - 1; i >= 0; --i) { |
||||||
|
const event = desk.events[desk.events.length - 1 - i]; |
||||||
|
ser.event(s, event); |
||||||
|
} |
||||||
|
|
||||||
|
console.debug(`syn ${desk.sn} out`); |
||||||
|
|
||||||
|
await session.ws.send(s.buffer); |
||||||
|
|
||||||
|
session.sync_timer = setTimeout(() => sync_session(session_id), config.SYNC_TIMEOUT); |
||||||
|
} |
||||||
|
|
||||||
|
export function sync_desk(desk_id) { |
||||||
|
for (const sid in sessions) { |
||||||
|
const session = sessions[sid]; |
||||||
|
if (session.state === SESSION.READY && session.desk_id == desk_id) { // NOT ===, because might be string or int IDK
|
||||||
|
if (session.sync_timer) { |
||||||
|
clearTimeout(session.sync_timer); |
||||||
|
} |
||||||
|
sync_session(sid); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
import { EVENT } from './enums'; |
||||||
|
|
||||||
|
export function create(size) { |
||||||
|
const buffer = new ArrayBuffer(size); |
||||||
|
return { |
||||||
|
'offset': 0, |
||||||
|
'size': size, |
||||||
|
'buffer': buffer, |
||||||
|
'view': new DataView(buffer), |
||||||
|
'strview': new Uint8Array(buffer), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function u8(s, value) { |
||||||
|
s.view.setUint8(s.offset, value); |
||||||
|
s.offset += 1; |
||||||
|
} |
||||||
|
|
||||||
|
export function u16(s, value) { |
||||||
|
s.view.setUint16(s.offset, value, true); |
||||||
|
s.offset += 2; |
||||||
|
} |
||||||
|
|
||||||
|
export function u32(s, value) { |
||||||
|
s.view.setUint32(s.offset, value, true); |
||||||
|
s.offset += 4; |
||||||
|
} |
||||||
|
|
||||||
|
export function bytes(s, bytes) { |
||||||
|
s.strview.set(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), s.offset); |
||||||
|
s.offset += bytes.byteLength; |
||||||
|
} |
||||||
|
|
||||||
|
export function event(s, event) { |
||||||
|
u8(s, event.type); |
||||||
|
u32(s, event.user_id); |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
u16(s, event.x); |
||||||
|
u16(s, event.y); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
const points_bytes = event.points; |
||||||
|
u16(s, points_bytes.byteLength / 2 / 2); // each point is 2 u16s
|
||||||
|
bytes(s, points_bytes); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
console.trace(); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
import * as config from './config'; |
||||||
|
import * as storage from './storage'; |
||||||
|
import * as http_server from './http'; |
||||||
|
import * as math from './math'; |
||||||
|
import * as ser from './serializer'; |
||||||
|
import * as des from './deserializer'; |
||||||
|
import * as send from './send'; |
||||||
|
import * as recv from './recv'; |
||||||
|
|
||||||
|
import { MESSAGE, EVENT, SESSION, SNS } from './enums'; |
||||||
|
import { sessions, desks } from './storage'; |
||||||
|
|
||||||
|
export function startup() { |
||||||
|
Bun.serve({ |
||||||
|
hostname: config.HOST, |
||||||
|
port: config.PORT, |
||||||
|
|
||||||
|
fetch(req, server) { |
||||||
|
const url = new URL(req.url); |
||||||
|
|
||||||
|
if (url.pathname == '/ws/') { |
||||||
|
const desk_id = url.searchParams.get('deskId') || '0'; |
||||||
|
const session_id = url.searchParams.get('sessionId') || '0'; |
||||||
|
|
||||||
|
if (!(desk_id in desks)) { |
||||||
|
const desk = { |
||||||
|
id: desk_id, |
||||||
|
sn: 0, |
||||||
|
events: [], |
||||||
|
}; |
||||||
|
|
||||||
|
storage.create_desk(desk_id); |
||||||
|
desks[desk_id] = desk; |
||||||
|
} |
||||||
|
|
||||||
|
server.upgrade(req, { |
||||||
|
data: { |
||||||
|
desk_id: desk_id, |
||||||
|
session_id: session_id, |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
return http_server.route(req); |
||||||
|
}, |
||||||
|
|
||||||
|
websocket: { |
||||||
|
open(ws) { |
||||||
|
send.send_init(ws); |
||||||
|
}, |
||||||
|
|
||||||
|
async message(ws, u8array) { |
||||||
|
const dataview = new DataView(u8array.buffer); |
||||||
|
const d = des.create(dataview); |
||||||
|
await recv.handle_message(ws, d); |
||||||
|
}, |
||||||
|
|
||||||
|
close(ws, code, message) { |
||||||
|
if (ws.data.session_id in sessions) { |
||||||
|
console.log(`session ${ws.data.session_id} closed`); |
||||||
|
sessions[ws.data.session_id].state = SESSION.CLOSED; |
||||||
|
sessions[ws.data.session_id].ws = null; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
error(ws, error) { |
||||||
|
close(ws, 1000, error); // Treat error as normal close
|
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
console.log(`Running on ${config.HOST}:${config.PORT}`) |
||||||
|
} |
@ -0,0 +1,201 @@ |
|||||||
|
import * as config from './config'; |
||||||
|
import * as sqlite from 'bun:sqlite'; |
||||||
|
|
||||||
|
import { EVENT, SESSION } from './enums'; |
||||||
|
|
||||||
|
export const sessions = {}; |
||||||
|
export const desks = {}; |
||||||
|
|
||||||
|
let db = null; |
||||||
|
const queries = {}; |
||||||
|
|
||||||
|
export function startup() { |
||||||
|
const path = `${config.DATADIR}/db.sqlite`; |
||||||
|
|
||||||
|
db = new sqlite.Database(path, { create: true }); |
||||||
|
|
||||||
|
db.query(`CREATE TABLE IF NOT EXISTS desks (
|
||||||
|
id INTEGER PRIMARY KEY, |
||||||
|
sn INTEGER, |
||||||
|
title TEXT |
||||||
|
);`).run();
|
||||||
|
|
||||||
|
db.query(`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY, |
||||||
|
login TEXT |
||||||
|
);`).run();
|
||||||
|
|
||||||
|
db.query(`CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id INTEGER PRIMARY KEY, |
||||||
|
user_id INTEGER, |
||||||
|
desk_id INTEGER, |
||||||
|
lsn INTEGER, |
||||||
|
|
||||||
|
FOREIGN KEY (user_id) |
||||||
|
REFERENCES users (id) |
||||||
|
ON DELETE CASCADE |
||||||
|
ON UPDATE NO ACTION, |
||||||
|
|
||||||
|
FOREIGN KEY (desk_id) |
||||||
|
REFERENCES desks (id) |
||||||
|
ON DELETE CASCADE |
||||||
|
ON UPDATE NO ACTION |
||||||
|
);`).run();
|
||||||
|
|
||||||
|
db.query(`CREATE TABLE IF NOT EXISTS strokes (
|
||||||
|
id INTEGER PRIMARY KEY, |
||||||
|
desk_id INTEGER, |
||||||
|
points BLOB, |
||||||
|
|
||||||
|
FOREIGN KEY (desk_id) |
||||||
|
REFERENCES desks (id) |
||||||
|
ON DELETE CASCADE |
||||||
|
ON UPDATE NO ACTION |
||||||
|
);`).run();
|
||||||
|
|
||||||
|
db.query(`CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id INTEGER PRIMARY KEY, |
||||||
|
type INTEGER, |
||||||
|
desk_id INTEGER, |
||||||
|
user_id INTEGER, |
||||||
|
stroke_id INTEGER, |
||||||
|
x INTEGER, |
||||||
|
y INTEGER, |
||||||
|
|
||||||
|
FOREIGN KEY (desk_id) |
||||||
|
REFERENCES desks (id) |
||||||
|
ON DELETE CASCADE |
||||||
|
ON UPDATE NO ACTION, |
||||||
|
|
||||||
|
FOREIGN KEY (user_id) |
||||||
|
REFERENCES users (id) |
||||||
|
ON DELETE CASCADE |
||||||
|
ON UPDATE NO ACTION |
||||||
|
|
||||||
|
FOREIGN KEY (stroke_id) |
||||||
|
REFERENCES strokes (id) |
||||||
|
ON DELETE CASCADE |
||||||
|
ON UPDATE NO ACTION |
||||||
|
);`).run();
|
||||||
|
|
||||||
|
db.query(`CREATE INDEX IF NOT EXISTS idx_events_desk_id
|
||||||
|
ON events (desk_id); |
||||||
|
`).run();
|
||||||
|
|
||||||
|
db.query(`CREATE INDEX IF NOT EXISTS idx_strokes_desk_id
|
||||||
|
ON strokes (desk_id); |
||||||
|
`).run();
|
||||||
|
|
||||||
|
const res1 = db.query('SELECT COUNT(id) as count FROM desks').get(); |
||||||
|
const res2 = db.query('SELECT COUNT(id) as count FROM events').get(); |
||||||
|
const res3 = db.query('SELECT COUNT(id) as count FROM strokes').get(); |
||||||
|
const res4 = db.query('SELECT COUNT(id) as count FROM users').get(); |
||||||
|
const res5 = db.query('SELECT COUNT(id) as count FROM sessions').get(); |
||||||
|
|
||||||
|
queries.desks = db.query('SELECT id, sn FROM desks'); |
||||||
|
queries.events = db.query('SELECT id, desk_id, user_id, stroke_id, type, x, y FROM events'); |
||||||
|
queries.sessions = db.query('SELECT id, lsn, user_id, desk_id FROM sessions'); |
||||||
|
queries.strokes = db.query('SELECT id, points FROM strokes'); |
||||||
|
queries.empty_desk = db.query('INSERT INTO desks (id, title, sn) VALUES (?1, ?2, 0)'); |
||||||
|
queries.desk_strokes = db.query('SELECT id, points FROM strokes WHERE desk_id = ?1'); |
||||||
|
queries.put_desk_stroke = db.query('INSERT INTO strokes (id, desk_id, points) VALUES (?1, ?2, ?3)'); |
||||||
|
queries.clear_desk_events = db.query('DELETE FROM events WHERE desk_id = ?1'); |
||||||
|
queries.set_desk_sn = db.query('UPDATE desks SET sn = ?1 WHERE id = ?2'); |
||||||
|
queries.save_session_lsn = db.query('UPDATE sessions SET lsn = ?1 WHERE id = ?2'); |
||||||
|
queries.create_session = db.query('INSERT INTO sessions (id, lsn, user_id, desk_id) VALUES (?1, 0, ?2, ?3)'); |
||||||
|
queries.create_user = db.query('INSERT INTO users (id, login) VALUES (?1, ?2)'); |
||||||
|
queries.put_event = db.query('INSERT INTO events (type, desk_id, user_id, stroke_id, x, y) VALUES (?1, ?2, ?3, ?4, ?5, ?6)'); |
||||||
|
|
||||||
|
console.log(`Storing data in ${path}`); |
||||||
|
console.log(`Entity count at startup:
|
||||||
|
${res1.count} desks |
||||||
|
${res2.count} events |
||||||
|
${res3.count} strokes |
||||||
|
${res4.count} users |
||||||
|
${res5.count} sessions` |
||||||
|
); |
||||||
|
|
||||||
|
const stored_desks = get_desks(); |
||||||
|
const stored_events = get_events(); |
||||||
|
const stored_strokes = get_strokes(); |
||||||
|
const stored_sessions = get_sessions(); |
||||||
|
|
||||||
|
const stroke_dict = {}; |
||||||
|
|
||||||
|
for (const stroke of stored_strokes) { |
||||||
|
stroke.points = new Uint16Array(stroke.points.buffer); |
||||||
|
stroke_dict[stroke.id] = stroke; |
||||||
|
} |
||||||
|
|
||||||
|
for (const desk of stored_desks) { |
||||||
|
desks[desk.id] = desk; |
||||||
|
desks[desk.id].events = []; |
||||||
|
} |
||||||
|
|
||||||
|
for (const event of stored_events) { |
||||||
|
if (event.type === EVENT.STROKE) { |
||||||
|
event.points = stroke_dict[event.stroke_id].points; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
desks[event.desk_id].events.push(event); |
||||||
|
} |
||||||
|
|
||||||
|
for (const session of stored_sessions) { |
||||||
|
session.state = SESSION.CLOSED; |
||||||
|
session.ws = null; |
||||||
|
sessions[session.id] = session; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function get_strokes() { |
||||||
|
return queries.strokes.all(); |
||||||
|
} |
||||||
|
|
||||||
|
export function get_sessions() { |
||||||
|
return queries.sessions.all(); |
||||||
|
} |
||||||
|
|
||||||
|
export function get_desks() { |
||||||
|
return queries.desks.all(); |
||||||
|
} |
||||||
|
|
||||||
|
export function get_events() { |
||||||
|
return queries.events.all(); |
||||||
|
} |
||||||
|
|
||||||
|
export function get_desk_strokes(desk_id) { |
||||||
|
return queries.desk_strokes.all(desk_id); |
||||||
|
} |
||||||
|
|
||||||
|
export function put_event(event) { |
||||||
|
return queries.put_event.get(event.type, event.desk_id || 0, event.user_id || 0, event.stroke_id || 0, event.x || 0, event.y || 0); |
||||||
|
} |
||||||
|
|
||||||
|
export function put_stroke(stroke_id, desk_id, points) { |
||||||
|
return queries.put_desk_stroke.get(stroke_id, desk_id, new Uint8Array(points.buffer, points.byteOffset, points.byteLength)); |
||||||
|
} |
||||||
|
|
||||||
|
export function clear_events(desk_id) { |
||||||
|
return queries.clear_desk_events.get(desk_id); |
||||||
|
} |
||||||
|
|
||||||
|
export function create_desk(desk_id, title = 'untitled') { |
||||||
|
return queries.empty_desk.get(desk_id, title); |
||||||
|
} |
||||||
|
|
||||||
|
export function save_desk_sn(desk_id, sn) { |
||||||
|
return queries.set_desk_sn.get(sn, desk_id); |
||||||
|
} |
||||||
|
|
||||||
|
export function create_session(session) { |
||||||
|
return queries.create_session.get(session.id, session.user_id, session.desk_id); |
||||||
|
} |
||||||
|
|
||||||
|
export function create_user(user) { |
||||||
|
return queries.create_user.get(user.id, user.login); |
||||||
|
} |
||||||
|
|
||||||
|
export function save_session_lsn(session_id, lsn) { |
||||||
|
return queries.save_session_lsn.get(lsn, session_id); |
||||||
|
} |
Loading…
Reference in new issue