socio
Version:
A WebSocket Real-Time Communication (RTC) API framework.
108 lines (107 loc) • 4.49 kB
JavaScript
import { LogHandler, E } from './logging.js';
import { RateLimiter } from './ratelimit.js';
import { yaml_stringify, FastHash, ClientMessageKind } from './utils.js';
export class SocioSession extends LogHandler {
#ws;
#subs = new Map();
#authenticated = false;
#perms = new Map();
#destroyed = 0;
verbose = false;
last_seen = 0;
session_opts = { session_timeout_ttl_ms: Infinity, max_payload_size: 1024 };
name;
constructor(client_id, ws_client, client_ipAddr, { logging = { verbose: false, hard_crash: false }, default_perms, session_opts, name } = {}) {
super({ ...logging, prefix: 'SocioSession' });
this.#ws = ws_client;
this.#ws['socio_client_id'] = client_id;
this.#ws['socio_client_ipAddr'] = client_ipAddr;
if (default_perms)
this.#perms = default_perms;
this.verbose = logging.verbose || false;
this.session_opts = Object.assign(this.session_opts, session_opts);
this.name = name;
this.last_seen_now();
}
get web_socket() { return this.#ws; }
get id() { return this.#ws['socio_client_id']; }
get ipAddr() { return this.#ws['socio_client_ipAddr']; }
Send(kind, ...data) {
if (this.#destroyed)
return;
if (data.length < 1)
throw new E('Not enough arguments to send data!', { kind, data });
return new Promise((resolve) => {
setImmediate(() => {
const payload = yaml_stringify(Object.assign({}, { kind: kind, data: data[0] }, ...data.slice(1)));
if (this.session_opts?.max_payload_size && payload.length < this.session_opts.max_payload_size) {
this.HandleDebug(`blocked a send: [${ClientMessageKind[kind]}] to [${this.id}] for exceeding max payload size [${this.session_opts.max_payload_size}] with size [${payload.length}]`);
}
else {
this.#ws.send(payload);
if (this.verbose)
this.HandleInfo(`sent: [${ClientMessageKind[kind]}] to [${this.name ? this.name + ' | ' : ''}${this.id}]`, ...(kind != ClientMessageKind.RECV_FILES ? data : []));
this.last_seen_now();
}
resolve();
});
});
}
RegisterSub(tables, id, sql, params, rate_limit) {
if (!this.#subs.has(id))
this.#subs.set(id, { tables, sql, params, rate_limiter: rate_limit ? new RateLimiter(rate_limit) : undefined, cache_hash: FastHash(sql + JSON.stringify(params)) });
else
throw new E('MSG ID already registered as Sub!', tables, id, sql, params);
}
UnRegisterSub(id) {
return this.#subs.delete(id) ? 1 : 0;
}
*GetSubsForTables(tables = []) {
for (const [id, hook] of this.#subs.entries())
if (hook.tables.some(t => tables.includes(t)))
yield { ...hook, id };
}
get authenticated() { return this.#authenticated; }
async Authenticate(auth_func, params = null) {
const auth = await auth_func(this, params);
this.#authenticated = auth === true;
return auth;
}
HasPermFor(verb = '', table = '') { return this.#perms.has(verb) && this.#perms.get(verb)?.includes(table); }
AddPermFor(verb = '', table = '') {
if (this.#perms.has(verb)) {
if (!this.#perms.get(verb).includes(table))
this.#perms.get(verb).push(table);
}
else
this.#perms.set(verb, [table]);
}
last_seen_now() { this.last_seen = (new Date()).getTime(); }
CloseConnection(code) {
if (this.#ws?.close)
this.#ws.close(code);
if (this.#ws?.terminate)
this.#ws.terminate();
}
Destroy(remove_session_callback, ttl_ms, force = false) {
if (force) {
this.CloseConnection();
remove_session_callback();
}
else
this.#destroyed = setTimeout(remove_session_callback, ttl_ms);
}
Restore() {
if (this.#destroyed)
clearTimeout(this.#destroyed);
this.#destroyed = 0;
}
ClearSubs() { this.#subs.clear(); }
CopySessionFrom(old_client) {
this.#authenticated = old_client.#authenticated;
this.#perms = old_client.#perms;
this.verbose = old_client.verbose;
this.last_seen = old_client.last_seen;
this.name = old_client.name;
}
}