A.Olokhtonov
2 years ago
commit
4466640439
25 changed files with 1691 additions and 0 deletions
@ -0,0 +1,18 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
export function route(req) { |
||||
console.log('HTTP:', req.url); |
||||
return new Response(req.url); |
||||
} |
@ -0,0 +1,14 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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