mirror of
https://github.com/Rolands-Laucis/Socio.git
synced 2026-05-15 06:05:53 -06:00
file upload not working
This commit is contained in:
parent
71710b7ca9
commit
dcb6c36f42
9 changed files with 354 additions and 362 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { LogHandler, E } from "./logging.js";
|
||||
import { WebSocket as nodeWebSocket } from "ws";
|
||||
import { yaml_parse, yaml_stringify, ClientMessageKind } from './utils.js';
|
||||
import { socio_decode, socio_encode, ClientMessageKind } from './utils.js';
|
||||
|
||||
//types
|
||||
import type { id, PropValue, LoggingOpts } from './types.d.ts';
|
||||
|
|
@ -31,8 +31,8 @@ export class AdminClient extends LogHandler{
|
|||
this.#ws.on('message', this.#Message.bind(this));
|
||||
}
|
||||
|
||||
#Message(d:string, isBinary:boolean){
|
||||
const { kind, data }: { kind: ClientMessageKind; data: MessageDataObj } = yaml_parse(d)
|
||||
#Message(d:Uint8Array, isBinary:boolean){
|
||||
const { kind, data }: { kind: ClientMessageKind; data: MessageDataObj } = socio_decode(d)
|
||||
|
||||
switch(kind){
|
||||
case ClientMessageKind.CON:{
|
||||
|
|
@ -74,7 +74,7 @@ export class AdminClient extends LogHandler{
|
|||
});
|
||||
|
||||
//send out the request
|
||||
this.#ws.send(yaml_stringify({ kind: 'ADMIN', data: { id: id, client_secret:this.#client_secret, function: function_name, args: args } }));
|
||||
this.#ws.send(socio_encode({ kind: 'ADMIN', data: { id: id, client_secret:this.#client_secret, function: function_name, args: args } }));
|
||||
|
||||
//let the caller await the promise resolve
|
||||
return prom;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import pako from 'pako'; //https://github.com/nodeca/pako
|
|||
import * as diff_lib from 'recursive-diff'; //https://www.npmjs.com/package/recursive-diff
|
||||
|
||||
import { LogHandler, E, err, log, info, done } from './logging.js';
|
||||
import { yaml_parse, yaml_stringify, clamp, ServerMessageKind, ClientMessageKind, initLifecycleHooks } from './utils.js';
|
||||
import { socio_decode, socio_encode, clamp, ServerMessageKind, ClientMessageKind, initLifecycleHooks } from './utils.js';
|
||||
|
||||
import { ErrorOrigin } from './logging.js'; //its an enum, not a type, so this import
|
||||
|
||||
|
|
@ -20,21 +20,21 @@ import type { ClientMessageDataObj } from './types.d.ts';
|
|||
|
||||
import type { RateLimit } from './ratelimit.js';
|
||||
type SubscribeCallbackObjectSuccess = ((res: object | object[]) => void) | null;
|
||||
type SubscribeCallbackObject = { success: SubscribeCallbackObjectSuccess, error?: Function};
|
||||
type SubscribeCallbackObject = { success: SubscribeCallbackObjectSuccess, error?: Function };
|
||||
type QueryObject = ClientSubscribeOpts & { onUpdate: SubscribeCallbackObject };
|
||||
type QueryPromise = { res: Function, prom:Promise<any> | null, start_buff: number, payload_size?:number };
|
||||
type QueryPromise = { res: Function, prom: Promise<any> | null, start_buff: number, payload_size?: number };
|
||||
export type ProgressOnUpdate = (percentage: number) => void;
|
||||
|
||||
type PropUpdateCallback = ((new_val: PropValue, diff?: diff_lib.rdiffResult[]) => void) | null;
|
||||
export type ClientProp = { val: PropValue | undefined, subs: { [id: id]: PropUpdateCallback } };
|
||||
export type SocioClientOptions = {
|
||||
url?: string,
|
||||
name?: string,
|
||||
keep_alive?: boolean,
|
||||
reconnect_tries?: number,
|
||||
name?: string,
|
||||
keep_alive?: boolean,
|
||||
reconnect_tries?: number,
|
||||
persistent?: boolean,
|
||||
hooks?: Partial<ClientLifecycleHooks>,
|
||||
allow_rpc?:boolean,
|
||||
allow_rpc?: boolean,
|
||||
} & LoggingOpts;
|
||||
type ConnectOptions = { url?: string, keep_alive?: boolean, reconnect_tries?: number };
|
||||
|
||||
|
|
@ -50,52 +50,52 @@ type PropSubOpts = { rate_limit?: RateLimit | null, receive_initial_update?: boo
|
|||
export class SocioClient extends LogHandler {
|
||||
// private:
|
||||
#ws: WebSocket | null = null;
|
||||
#client_id:ClientID = '';
|
||||
#latency:number = 0;
|
||||
#client_id: ClientID = '';
|
||||
#latency: number = 0;
|
||||
#is_ready: Function | boolean = false;
|
||||
#authenticated=false;
|
||||
#authenticated = false;
|
||||
|
||||
#queries: Map<id, QueryObject | QueryPromise> = new Map(); //keeps a dict of all subscribed queries
|
||||
#props: Map<PropKey, ClientProp> = new Map();
|
||||
|
||||
static #key:id = 1; //all instances will share this number, such that they are always kept unique. Tho each of these clients would make a different session on the backend, but still
|
||||
static #key: id = 1; //all instances will share this number, such that they are always kept unique. Tho each of these clients would make a different session on the backend, but still
|
||||
|
||||
//public:
|
||||
config: SocioClientOptions;
|
||||
key_generator: (() => number | string) | undefined;
|
||||
lifecycle_hooks!: ClientLifecycleHooks; //assign your function to hook on these. They will be called if they exist
|
||||
rpc_dict: {[f_name:string]: Function} = {};
|
||||
rpc_dict: { [f_name: string]: Function } = {};
|
||||
//If the hook returns a truthy value, then it is assumed, that the hook handled the msg and the lib will not. Otherwise, by default, the lib handles the msg.
|
||||
//discon has to be an async function, such that you may await the new ready(), but socio wont wait for it to finish.
|
||||
// progs: Map<Promise<any>, number> = new Map(); //the promise is that of a socio generic data going out from client async. Number is WS send buffer payload size at the time of query
|
||||
|
||||
constructor({
|
||||
url,
|
||||
name,
|
||||
logging = { verbose: false, hard_crash: false },
|
||||
keep_alive = true,
|
||||
reconnect_tries = 1,
|
||||
persistent = false,
|
||||
hooks = {},
|
||||
allow_rpc = false,
|
||||
}: SocioClientOptions = {}) {
|
||||
url,
|
||||
name,
|
||||
logging = { verbose: false, hard_crash: false },
|
||||
keep_alive = true,
|
||||
reconnect_tries = 1,
|
||||
persistent = false,
|
||||
hooks = {},
|
||||
allow_rpc = false,
|
||||
}: SocioClientOptions = {}) {
|
||||
super({ ...logging, prefix: name ? `SocioClient:${name}` : 'SocioClient' });
|
||||
|
||||
// public:
|
||||
this.config = { url, name, logging, keep_alive, reconnect_tries, persistent, allow_rpc };
|
||||
this.lifecycle_hooks = { ...initLifecycleHooks<ClientLifecycleHooks>(), ...hooks };
|
||||
|
||||
|
||||
// connect right away if url provided, or the user can call Connect later manually. This is useful for creating the class variable when the page is not loaded yet
|
||||
if(url){
|
||||
if (url) {
|
||||
this.Connect();
|
||||
}
|
||||
}
|
||||
|
||||
Connect({ url = this.config?.url, keep_alive = this.config?.keep_alive || false, reconnect_tries = this.config?.reconnect_tries || 0 }: ConnectOptions = {}) {
|
||||
// checks
|
||||
if(!url) throw new E('Must provide a WebSocket URL to connect to! [#no-url]');
|
||||
if(this.#ws && this.#ws.readyState === WebSocket.OPEN) throw new E('Socio WebSocket is already connected! Please disconnect first before connecting again or create a new instance. [#already-connected]');
|
||||
|
||||
if (!url) throw new E('Must provide a WebSocket URL to connect to! [#no-url]');
|
||||
if (this.#ws && this.#ws.readyState === WebSocket.OPEN) throw new E('Socio WebSocket is already connected! Please disconnect first before connecting again or create a new instance. [#already-connected]');
|
||||
|
||||
this.#latency = (new Date()).getTime();
|
||||
this.#connect(url, keep_alive, this.verbose || false, reconnect_tries);
|
||||
|
||||
|
|
@ -107,17 +107,18 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
async #connect(url: string, keep_alive: boolean, verbose: boolean, reconnect_tries:number){
|
||||
|
||||
async #connect(url: string, keep_alive: boolean, verbose: boolean, reconnect_tries: number) {
|
||||
this.#ws = new WebSocket(url);
|
||||
this.#ws.binaryType = 'arraybuffer';
|
||||
this.#ws.addEventListener('message', this.#message.bind(this));
|
||||
// on socket error events, keep retrying the connection until retries runs out
|
||||
if (keep_alive && reconnect_tries){
|
||||
if (keep_alive && reconnect_tries) {
|
||||
this.#ws.addEventListener("close", (event: CloseEvent) => { this.#RetryConn(url, keep_alive, verbose, reconnect_tries, event) });
|
||||
this.#ws.addEventListener("error", (event: Event) => { this.#RetryConn(url, keep_alive, verbose, reconnect_tries, event) });
|
||||
}
|
||||
// or notify everything that the connection has failed
|
||||
else{
|
||||
else {
|
||||
const notify = (event: Event | CloseEvent) => {
|
||||
// log to console, as thats the first sign for the dev that smth is wrong
|
||||
if (event instanceof CloseEvent) this.HandleInfo('Connection closed.');
|
||||
|
|
@ -131,7 +132,7 @@ export class SocioClient extends LogHandler {
|
|||
this.#ws.addEventListener("error", notify);
|
||||
}
|
||||
}
|
||||
#RetryConn(url: string, keep_alive: boolean, verbose: boolean, reconnect_tries: number, event:any) {
|
||||
#RetryConn(url: string, keep_alive: boolean, verbose: boolean, reconnect_tries: number, event: any) {
|
||||
this.#HandleClientError(new E(`"${this.config.name || ''}" WebSocket closed. Retrying... Event details:`, event));
|
||||
this.#resetConn(); //invalidate any state this session had
|
||||
this.#connect(url, keep_alive, verbose, reconnect_tries - 1); //reconnect
|
||||
|
|
@ -153,8 +154,8 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
|
||||
async #message(event: MessageEvent) {
|
||||
try{
|
||||
const { kind, data }: { kind: ClientMessageKind; data: ClientMessageDataObj } = yaml_parse(event.data);
|
||||
try {
|
||||
const { kind, data }: { kind: ClientMessageKind; data: ClientMessageDataObj } = socio_decode(new Uint8Array(event.data));
|
||||
this.HandleInfo('recv:', ClientMessageKind[kind], data);
|
||||
if (typeof data.id === 'number' && typeof SocioClient.#key === 'number' && data.id > SocioClient.#key)
|
||||
SocioClient.#key = data.id; //if reveive a larger ID from server than current, then use that to avoid conflicts between client ID counters
|
||||
|
|
@ -165,7 +166,7 @@ export class SocioClient extends LogHandler {
|
|||
return;
|
||||
|
||||
switch (kind) {
|
||||
case ClientMessageKind.CON:{
|
||||
case ClientMessageKind.CON: {
|
||||
this.#client_id = data as C_CON_data;//should just be a string
|
||||
this.#latency = (new Date()).getTime() - this.#latency;
|
||||
|
||||
|
|
@ -175,13 +176,11 @@ export class SocioClient extends LogHandler {
|
|||
await this.#GetReconToken(); //get new recon token and push to local storage
|
||||
}
|
||||
|
||||
if (this.#is_ready !== false && typeof this.#is_ready === "function")
|
||||
//set ready state to true and resolve ready() promise, if exists
|
||||
if (this.#is_ready !== true && typeof this.#is_ready === "function")
|
||||
this.#is_ready(true); //resolve promise to true
|
||||
else
|
||||
this.#is_ready = true;
|
||||
if (this.verbose) this.done(`Socio WebSocket [${this.config.name}] connected.`);
|
||||
|
||||
this.#is_ready = true;
|
||||
if (this.verbose) this.done(`Socio WebSocket [${this.config?.name || this.#client_id || 'NAME'}] connected.`);
|
||||
|
||||
// once ready, attempt to use the given name as a global identifier. No problem, if this fails.
|
||||
//persistance would restore the old name, so no need to announce again.
|
||||
|
|
@ -192,26 +191,26 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.UPD:{
|
||||
case ClientMessageKind.UPD: {
|
||||
this.#FindID(kind, (data as C_UPD_data).id);
|
||||
const q = this.#queries.get((data as C_UPD_data).id) as QueryObject;
|
||||
if ((data as C_UPD_data).result.success === 1 && q.onUpdate.success){
|
||||
if ((data as C_UPD_data).result.success === 1 && q.onUpdate.success) {
|
||||
q.onUpdate.success(((data as C_UPD_data).result.res));
|
||||
}else{
|
||||
if (q.onUpdate?.error){
|
||||
} else {
|
||||
if (q.onUpdate?.error) {
|
||||
q.onUpdate.error((data as C_UPD_data).result.error);
|
||||
}else{
|
||||
} else {
|
||||
throw new E('Subscription query doesnt handle incoming server error:', (data as C_UPD_data).result.error)
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.PONG:{
|
||||
case ClientMessageKind.PONG: {
|
||||
this.#FindID(kind, (data as data_base)?.id)
|
||||
this.HandleInfo('pong', (data as data_base)?.id);
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.AUTH:{
|
||||
case ClientMessageKind.AUTH: {
|
||||
this.#FindID(kind, (data as C_AUTH_data)?.id)
|
||||
if ((data as C_AUTH_data)?.result?.success !== 1)
|
||||
this.HandleInfo(`AUTH returned FALSE, which means websocket has not authenticated.`);
|
||||
|
|
@ -221,11 +220,11 @@ export class SocioClient extends LogHandler {
|
|||
this.#queries.delete((data as C_AUTH_data).id); //clear memory
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.GET_PERM:{
|
||||
case ClientMessageKind.GET_PERM: {
|
||||
this.#FindID(kind, (data as C_GET_PERM_data)?.id)
|
||||
if ((data as C_GET_PERM_data)?.result?.success as Bit !== 1)
|
||||
this.HandleInfo(`Server rejected grant perm for ${(data as C_GET_PERM_data)?.verb} on ${(data as C_GET_PERM_data)?.table}.`);
|
||||
else{
|
||||
else {
|
||||
const q = this.#queries.get((data as C_GET_PERM_data).id) as QueryPromise
|
||||
q.res((data as C_GET_PERM_data)?.result.success === 1 ? true : false); //promise expects resolve with boolean
|
||||
}
|
||||
|
|
@ -233,24 +232,24 @@ export class SocioClient extends LogHandler {
|
|||
this.#queries.delete((data as C_GET_PERM_data).id) //clear memory
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.RES:{
|
||||
case ClientMessageKind.RES: {
|
||||
this.#HandleBasicPromiseMessage(kind, data as C_RES_data);
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.PROP_UPD:{
|
||||
case ClientMessageKind.PROP_UPD: {
|
||||
if (data.hasOwnProperty('prop') && data.hasOwnProperty('id') && (data.hasOwnProperty('prop_val') || data.hasOwnProperty('prop_val_diff'))) {
|
||||
const prop = this.#props.get((data as C_PROP_UPD_data).prop);
|
||||
let prop_val: PropValue | diff_lib.rdiffResult[] | undefined;
|
||||
if (prop){
|
||||
if (typeof prop.subs[(data as C_PROP_UPD_data).id] === 'function'){
|
||||
if (prop) {
|
||||
if (typeof prop.subs[(data as C_PROP_UPD_data).id] === 'function') {
|
||||
// get the new factual value value of the prop either from the server or from applying the diff from server
|
||||
if ((data as C_PROP_UPD_data).hasOwnProperty('prop_val')){ //take factual
|
||||
if ((data as C_PROP_UPD_data).hasOwnProperty('prop_val')) { //take factual
|
||||
prop_val = (data as C_PROP_UPD_data).prop_val;
|
||||
}
|
||||
}
|
||||
else if ((data as C_PROP_UPD_data).hasOwnProperty('prop_val_diff')) { //apply diff
|
||||
prop_val = diff_lib.applyDiff(prop.val, (data as C_PROP_UPD_data).prop_val_diff!);
|
||||
}
|
||||
else throw new E('Prop upd data didnt have either factual val or diff val to use. [#prop-upd-no-val]', {kind, data});
|
||||
else throw new E('Prop upd data didnt have either factual val or diff val to use. [#prop-upd-no-val]', { kind, data });
|
||||
prop.val = prop_val; //set the new val
|
||||
|
||||
// afterwards call the client subscription with the new val and the received diff
|
||||
|
|
@ -259,10 +258,10 @@ export class SocioClient extends LogHandler {
|
|||
} else {
|
||||
throw new E('Prop UPD called, but subscribed prop is missing a registered callback for this data ID. [#prop-subs-id-not-found]', { data, callback: prop.subs[(data as C_PROP_UPD_data).id] });
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
throw new E('Prop not found by name. [#prop-name-not-found]', { data, prop_name: (data as C_PROP_UPD_data).prop });
|
||||
}
|
||||
|
||||
|
||||
// if this is the initial update from the SUB_PROP call, resolve the promise
|
||||
// if (this.#queries.has((data as C_PROP_UPD_data).id))
|
||||
// (this.#queries.get((data as C_PROP_UPD_data).id) as QueryPromise).res(prop_val); //resolve the promise
|
||||
|
|
@ -270,7 +269,7 @@ export class SocioClient extends LogHandler {
|
|||
} else throw new E('Not enough prop info sent from server to perform prop update.', { data: data as C_PROP_UPD_data })
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.PROP_DROP:{
|
||||
case ClientMessageKind.PROP_DROP: {
|
||||
if ((data as C_PROP_UPD_data)?.prop && data.hasOwnProperty('id')) {
|
||||
if (this.#props.has((data as C_PROP_UPD_data).prop)) {
|
||||
delete this.#props.get((data as C_PROP_UPD_data).prop)?.subs[(data as C_PROP_UPD_data).id];
|
||||
|
|
@ -283,11 +282,11 @@ export class SocioClient extends LogHandler {
|
|||
} else throw new E('Not enough prop info sent from server to perform prop drop.', data);
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.CMD: {if(this.lifecycle_hooks.cmd) this.lifecycle_hooks.cmd(data); break;} //the server pushed some data to this client, let the dev handle it
|
||||
case ClientMessageKind.RECON:{
|
||||
if ((data as C_RECON_Data)?.id){
|
||||
case ClientMessageKind.CMD: { if (this.lifecycle_hooks.cmd) this.lifecycle_hooks.cmd(data); break; } //the server pushed some data to this client, let the dev handle it
|
||||
case ClientMessageKind.RECON: {
|
||||
if ((data as C_RECON_Data)?.id) {
|
||||
this.#FindID(kind, (data as data_base).id);
|
||||
|
||||
|
||||
(this.#queries.get((data as data_base).id) as QueryPromise).res(data as C_RECON_Data);
|
||||
this.#queries.delete((data as data_base).id); //clear memory
|
||||
}
|
||||
|
|
@ -296,16 +295,16 @@ export class SocioClient extends LogHandler {
|
|||
this.#Reconnect(data as C_RECON_Data);
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.RECV_FILES:{
|
||||
case ClientMessageKind.RECV_FILES: {
|
||||
this.#FindID(kind, (data as C_RECV_FILES_Data)?.id);
|
||||
|
||||
// cant exit early, bcs the query needs to be resolved to smth and deleted, so use these vars here instead
|
||||
let resolve_with: File[] | null = null;
|
||||
let error: string | null = null;
|
||||
if ((data as C_RECV_FILES_Data).result.success === 1) {
|
||||
if ((data as C_RECV_FILES_Data)?.files){
|
||||
if ((data as C_RECV_FILES_Data)?.files) {
|
||||
resolve_with = ParseSocioFiles((data as C_RECV_FILES_Data).files as SocioFiles);
|
||||
}else{
|
||||
} else {
|
||||
resolve_with = null;
|
||||
error = 'Received 0 files. Something must\'ve gone wrong, bcs success was true. [#recv-0-files]';
|
||||
}
|
||||
|
|
@ -317,31 +316,31 @@ export class SocioClient extends LogHandler {
|
|||
//@ts-expect-error
|
||||
this.#queries.get((data as C_RECV_FILES_Data).id)?.res(resolve_with); //resolve the promise with files or null bcs of error
|
||||
this.#queries.delete((data as C_RECV_FILES_Data).id); //clear memory
|
||||
|
||||
|
||||
// notify dev and crash with error
|
||||
if(error){
|
||||
if (error) {
|
||||
const file_count = Object.keys((data as C_RECV_FILES_Data)?.files || {}).length;
|
||||
this.#HandleServerError(error, (data as C_RECV_FILES_Data)?.result?.error as string, 'files received: ' + file_count);
|
||||
throw new E(error, { err_msg:(data as C_RECV_FILES_Data).result.error, file_count });
|
||||
throw new E(error, { err_msg: (data as C_RECV_FILES_Data).result.error, file_count });
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.TIMEOUT:{
|
||||
case ClientMessageKind.TIMEOUT: {
|
||||
if (this.lifecycle_hooks.timeout)
|
||||
this.lifecycle_hooks.timeout(this);
|
||||
break;
|
||||
}
|
||||
case ClientMessageKind.RPC: {
|
||||
if(this.config.allow_rpc !== true){
|
||||
if (this.config.allow_rpc !== true) {
|
||||
this.HandleDebug('Received RPC, but the client hasnt enabled it. [#rpc-client-not-enabled]', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// let the hook handle it
|
||||
if(this.lifecycle_hooks.rpc){
|
||||
if (this.lifecycle_hooks.rpc) {
|
||||
const res = await this.lifecycle_hooks.rpc(this, (data as S_RPC_data).origin_client, (data as S_RPC_data).f_name, (data as S_RPC_data).args)
|
||||
if (res !== undefined){
|
||||
if (res !== undefined) {
|
||||
this.Send(ServerMessageKind.OK, { id: data.id, return: res });
|
||||
return;
|
||||
}
|
||||
|
|
@ -353,7 +352,7 @@ export class SocioClient extends LogHandler {
|
|||
result = await this.rpc_dict[(data as S_RPC_data).f_name]((data as S_RPC_data).origin_client, ...(data as S_RPC_data).args);
|
||||
else if ((data as S_RPC_data).target_client === null && (data as S_RPC_data).f_name in this) //secondly on the client class functions if target is null, bcs thats from the server
|
||||
result = await this[(data as S_RPC_data).f_name](...(data as S_RPC_data).args);
|
||||
else
|
||||
else
|
||||
this.HandleDebug('Received RPC, but the function name doesnt exist on this client. [#rpc-client-no-function]', data);
|
||||
|
||||
// return the result back to the server, so it can return it back to the origin client
|
||||
|
|
@ -361,23 +360,23 @@ export class SocioClient extends LogHandler {
|
|||
break;
|
||||
}
|
||||
// case ClientMessageKind.: { break; }
|
||||
default:{
|
||||
default: {
|
||||
const exhaustiveCheck: never = kind; // This ensures that if a new enum value is added and not handled, it will result in a compile-time error
|
||||
throw new E(`Unrecognized message kind!`, { kind, data });
|
||||
}
|
||||
}
|
||||
} catch (e:err) { this.#HandleClientError(e) }
|
||||
} catch (e: err) { this.#HandleClientError(e) }
|
||||
}
|
||||
|
||||
//accepts infinite arguments of data to send and will append these params as new key:val pairs to the parent object
|
||||
Send(kind: ServerMessageKind, ...data){ //data is an array of parameters to this func, where every element (after first) is an object. First param can also not be an object in some cases
|
||||
try{
|
||||
Send(kind: ServerMessageKind, ...data) { //data is an array of parameters to this func, where every element (after first) is an object. First param can also not be an object in some cases
|
||||
try {
|
||||
if (data.length < 1) throw new E('Not enough arguments to send data! kind;data:', kind, ...data); //the first argument must always be the data to send. Other params may be objects with aditional keys to be added in the future
|
||||
this.#ws?.send(yaml_stringify(Object.assign({}, { kind, data: data[0] }, ...data.slice(1))));
|
||||
this.#ws?.send(socio_encode(Object.assign({}, { kind, data: data[0] }, ...data.slice(1))));
|
||||
this.HandleInfo('sent:', ServerMessageKind[kind], ...data);
|
||||
} catch (e: err) { this.#HandleClientError(e); }
|
||||
}
|
||||
SendFiles(files:File[], other_data:object|undefined=undefined){
|
||||
SendFiles(files: File[], other_data: object | undefined = undefined) {
|
||||
const { id, prom } = this.CreateQueryPromise(); //this up here, bcs we await in the lower lines, so that a prog tracker async can find this query as soon as it is available.
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/IIFE pattern bcs we need to use await there, but marking this function as async will actually return a new promise instead of the one returned here.
|
||||
|
|
@ -396,11 +395,11 @@ export class SocioClient extends LogHandler {
|
|||
type: file.type
|
||||
};
|
||||
const file_bytes_buffer = await file.arrayBuffer();
|
||||
proc_files.set(file.name, { meta, bin: Uint8ArrayToSocioFileBase64(file_bytes_buffer)}); //this is the best way that i could find. JS is really unhappy about binary data
|
||||
proc_files.set(file.name, { meta, bin: pako.deflate(file_bytes_buffer) }); // MessagePack supports binary natively
|
||||
}
|
||||
|
||||
//create the server request as usual
|
||||
const socio_form_data = { id, files: proc_files };
|
||||
const socio_form_data = { id, files: Object.fromEntries(proc_files) }; // Convert Map to object for MessagePack
|
||||
if (other_data)
|
||||
socio_form_data['data'] = other_data; //add the other data if exists
|
||||
this.Send(ServerMessageKind.UP_FILES, socio_form_data as S_UP_FILES_data);
|
||||
|
|
@ -422,20 +421,20 @@ export class SocioClient extends LogHandler {
|
|||
});
|
||||
return prom;
|
||||
}
|
||||
CreateQueryPromise(){
|
||||
CreateQueryPromise() {
|
||||
// creates a basic promise that is trackable. It is resolved either by Message receive switch block by msg ID in queries, or by whatever has a reference to this returned prom
|
||||
|
||||
//https://advancedweb.hu/how-to-add-timeout-to-a-promise-in-javascript/ should implement promise timeouts
|
||||
const id = this.GenKey;
|
||||
const prom = new Promise((res) => {
|
||||
this.#queries.set(id, { res, prom:null, start_buff: this.#ws?.bufferedAmount || 0 });
|
||||
this.#queries.set(id, { res, prom: null, start_buff: this.#ws?.bufferedAmount || 0 });
|
||||
}) as Promise<unknown>;
|
||||
(this.#queries.get(id) as QueryPromise).prom = prom;
|
||||
|
||||
|
||||
// this.progs.set(prom, this.#ws?.bufferedAmount || 0); //add this to progress tracking
|
||||
return {id, prom};
|
||||
return { id, prom };
|
||||
}
|
||||
#UpdateQueryPromisePayloadSize(query_id:id){
|
||||
#UpdateQueryPromisePayloadSize(query_id: id) {
|
||||
if (!this.#queries.has(query_id)) return;
|
||||
(this.#queries.get(query_id) as QueryPromise).payload_size = (this.#ws?.bufferedAmount || 0) - (this.#queries.get(query_id) as QueryPromise)?.start_buff || 0;
|
||||
}
|
||||
|
|
@ -457,7 +456,7 @@ export class SocioClient extends LogHandler {
|
|||
Ping(id_num = undefined) {
|
||||
this.Send(ServerMessageKind.PING, { id: typeof id_num === 'number' ? id_num : this.GenKey });
|
||||
}
|
||||
|
||||
|
||||
async DiscoverSessions<K extends DiscoveryBy>(by: K = 'ID' as K): Promise<DiscoveryReturn[K]> {
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.DISCOVERY, { id });
|
||||
|
|
@ -485,17 +484,17 @@ export class SocioClient extends LogHandler {
|
|||
for (const q of [...this.#queries.keys()])
|
||||
this.Unsubscribe(q, force);
|
||||
}
|
||||
IdentifySelf(name:string){
|
||||
if(!name) throw new E('Must provide a unique string name to indetify this session globally.');
|
||||
IdentifySelf(name: string) {
|
||||
if (!name) throw new E('Must provide a unique string name to indetify this session globally.');
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.IDENTIFY, {id, name});
|
||||
this.Send(ServerMessageKind.IDENTIFY, { id, name });
|
||||
return prom as Promise<data_base & data_result_block>;
|
||||
}
|
||||
|
||||
|
||||
//subscribe to an sql query. Can add multiple callbacks where ever in your code, if their sql queries are identical
|
||||
//returns the created ID for that query, to use to unsubscribe all callbacks to the query
|
||||
Subscribe({ sql = undefined, endpoint = undefined, params = null }: ClientSubscribeOpts = {}, onUpdate: SubscribeCallbackObjectSuccess = null, status_callbacks: { error?: (e: string) => void } = {}, rate_limit: RateLimit | null = null): id | null{
|
||||
Subscribe({ sql = undefined, endpoint = undefined, params = null }: ClientSubscribeOpts = {}, onUpdate: SubscribeCallbackObjectSuccess = null, status_callbacks: { error?: (e: string) => void } = {}, rate_limit: RateLimit | null = null): id | null {
|
||||
//params for sql is the object that will be passed as params to your query func
|
||||
//optionally can also supply an endpoint name instead of an sql string. Cannot do both. The endpoint is your own keyname for a sql query defined on the backend in a special file.
|
||||
|
||||
|
|
@ -514,26 +513,26 @@ export class SocioClient extends LogHandler {
|
|||
return id; //the ID of the query
|
||||
} catch (e: err) { this.#HandleClientError(e); return null; }
|
||||
}
|
||||
async Unsubscribe(sub_id: id, force=false) {
|
||||
async Unsubscribe(sub_id: id, force = false) {
|
||||
try {
|
||||
if (this.#queries.has(sub_id)){
|
||||
if(force)//will first delete from here, to not wait for server response
|
||||
if (this.#queries.has(sub_id)) {
|
||||
if (force)//will first delete from here, to not wait for server response
|
||||
this.#queries.delete(sub_id);
|
||||
|
||||
|
||||
//set up new msg to the backend informing a wish to unregister query.
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.UNSUB, { id, unreg_id: sub_id } as S_UNSUB_data)
|
||||
|
||||
const res = await (prom as unknown) as Bit; //await the response from backend
|
||||
if(res === 1)//if successful, then remove the subscribe from the client
|
||||
if (res === 1)//if successful, then remove the subscribe from the client
|
||||
this.#queries.delete(sub_id);
|
||||
return res;//forward the success status to the developer
|
||||
}
|
||||
else
|
||||
throw new E('Cannot unsubscribe query, because provided ID is not currently tracked.', sub_id);
|
||||
} catch (e:err) { this.#HandleClientError(e); return false; }
|
||||
} catch (e: err) { this.#HandleClientError(e); return false; }
|
||||
}
|
||||
Query(sql: string, params: object | null | Array<any> = null, { sql_is_endpoint = undefined, onUpdate, freq_ms = undefined }: { sql_is_endpoint?:boolean, onUpdate?: ProgressOnUpdate, freq_ms?:number } = {}){
|
||||
Query(sql: string, params: object | null | Array<any> = null, { sql_is_endpoint = undefined, onUpdate, freq_ms = undefined }: { sql_is_endpoint?: boolean, onUpdate?: ProgressOnUpdate, freq_ms?: number } = {}) {
|
||||
//set up a promise which resolve function is in the queries data structure, such that in the message handler it can be called, therefor the promise resolved, therefor awaited and return from this function
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
|
||||
|
|
@ -549,7 +548,7 @@ export class SocioClient extends LogHandler {
|
|||
}
|
||||
|
||||
|
||||
SetProp(prop_name: PropKey, new_val: PropValue, prop_upd_as_diff?:boolean){
|
||||
SetProp(prop_name: PropKey, new_val: PropValue, prop_upd_as_diff?: boolean) {
|
||||
try {
|
||||
//set up a promise which resolve function is in the queries data structure, such that in the message handler it can be called, therefor the promise resolved, therefor awaited and return from this function
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
|
|
@ -559,9 +558,9 @@ export class SocioClient extends LogHandler {
|
|||
return prom as Promise<data_base & data_result_block>;
|
||||
} catch (e: err) { this.#HandleClientError(e); return null; }
|
||||
}
|
||||
GetProp(prop_name: PropKey, local: boolean = false){
|
||||
if (local) return { result: { success: 1, res: this.#props.get(prop_name)?.val as PropValue }};
|
||||
else{
|
||||
GetProp(prop_name: PropKey, local: boolean = false) {
|
||||
if (local) return { result: { success: 1, res: this.#props.get(prop_name)?.val as PropValue } };
|
||||
else {
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.PROP_GET, { id, prop: prop_name } as S_PROP_GET_data);
|
||||
this.#UpdateQueryPromisePayloadSize(id);
|
||||
|
|
@ -608,16 +607,16 @@ export class SocioClient extends LogHandler {
|
|||
RegisterProp(prop_name: PropKey | undefined | null, initial_value: any = null, prop_reg_opts: Omit<PropOpts, "observationaly_temporary"> = {}) { //"client_writable" &
|
||||
try {
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.PROP_REG, { id, prop:prop_name, initial_value, opts: prop_reg_opts } as S_PROP_REG_data);
|
||||
this.Send(ServerMessageKind.PROP_REG, { id, prop: prop_name, initial_value, opts: prop_reg_opts } as S_PROP_REG_data);
|
||||
this.#UpdateQueryPromisePayloadSize(id);
|
||||
|
||||
|
||||
return (prom as unknown) as Promise<{ prop: PropKey }>;
|
||||
} catch (e: err) { this.#HandleClientError(e); return null; }
|
||||
}
|
||||
// get a PropProxy object. The prop has to be a js object datatype on the server side. This automagically handles the PropGet, PropSet and SubscribeProp base functions for you.
|
||||
async Prop(prop_name: PropKey, { prop_sub_opts = {}, prop_upd_as_diff = false}:{ prop_sub_opts?: PropSubOpts, prop_upd_as_diff?: boolean} = {}){
|
||||
async Prop(prop_name: PropKey, { prop_sub_opts = {}, prop_upd_as_diff = false }: { prop_sub_opts?: PropSubOpts, prop_upd_as_diff?: boolean } = {}) {
|
||||
const prop = await this.GetProp(prop_name, false);
|
||||
if(prop === undefined){
|
||||
if (prop === undefined) {
|
||||
this.#HandleClientError(new E(`Couldnt retrieve server prop [${prop_name}]`, { prop_name, prop }));
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -636,7 +635,7 @@ export class SocioClient extends LogHandler {
|
|||
//ive run tests in other projects and the async set does work fine. TS doesnt want to allow it for some reason
|
||||
set(p: PropValue, property, new_val) {
|
||||
// log(`${prop_name} SET`, { p, property, new_val });
|
||||
|
||||
|
||||
// always set the val
|
||||
p[property] = new_val;
|
||||
|
||||
|
|
@ -662,7 +661,7 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
|
||||
// socio query marker related --auth and --perm:
|
||||
Authenticate(params:object={}){ //params here can be anything, like username and password stuff etc. The backend server auth function callback will receive this entire object
|
||||
Authenticate(params: object = {}) { //params here can be anything, like username and password stuff etc. The backend server auth function callback will receive this entire object
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.AUTH, { id, params } as S_AUTH_data);
|
||||
this.#UpdateQueryPromisePayloadSize(id);
|
||||
|
|
@ -677,45 +676,45 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
return (prom as unknown) as Promise<boolean>;
|
||||
}
|
||||
|
||||
|
||||
// use null to call a function on the server, handled by the server hook
|
||||
async RPC(target_client: ClientID | string | null, f_name:string, ...args:any[]){
|
||||
const {id, prom} = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.RPC, { ...{ target_client, origin_client:this.client_id, f_name, args } as S_RPC_data, id }); //id last, bcs ts complains it would be over written by the spread
|
||||
async RPC(target_client: ClientID | string | null, f_name: string, ...args: any[]) {
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
this.Send(ServerMessageKind.RPC, { ...{ target_client, origin_client: this.client_id, f_name, args } as S_RPC_data, id }); //id last, bcs ts complains it would be over written by the spread
|
||||
return await (prom as Promise<any>);
|
||||
}
|
||||
|
||||
|
||||
//checks if the ID of a query exists, otherwise rejects/throws and logs. This is used in a bunch of message receive cases at the start.
|
||||
#FindID(kind: ClientMessageKind, id: id) {
|
||||
if (!this.#queries.has(id))
|
||||
throw new E(`A received socio message [querry_id ${id}, ${ClientMessageKind[kind]}] is not currently in tracked queries!`);
|
||||
}
|
||||
#HandleBasicPromiseMessage(kind: ClientMessageKind, data: C_RES_data){
|
||||
#HandleBasicPromiseMessage(kind: ClientMessageKind, data: C_RES_data) {
|
||||
this.#FindID(kind, data?.id);
|
||||
const q = this.#queries.get(data.id) as QueryObject | QueryPromise;
|
||||
if (q.hasOwnProperty('res')){
|
||||
if (data.result.success === 1){
|
||||
if (q.hasOwnProperty('res')) {
|
||||
if (data.result.success === 1) {
|
||||
(q as QueryPromise).res(data?.result?.res as any);
|
||||
// log('query prom res called', {kind, q, data});
|
||||
}
|
||||
else this.#HandleServerError(data.result?.error as string);
|
||||
}
|
||||
else if (q.hasOwnProperty('onUpdate'))
|
||||
if (data.result.success === 1){
|
||||
if (data.result.success === 1) {
|
||||
if ((q as QueryObject).onUpdate?.success)
|
||||
//@ts-expect-error
|
||||
(q as QueryObject).onUpdate.success(data.result.res);
|
||||
}
|
||||
else{
|
||||
else {
|
||||
if ((q as QueryObject).onUpdate?.error)
|
||||
//@ts-expect-error
|
||||
(q as QueryObject).onUpdate.error(data.result.error);
|
||||
else this.#HandleServerError(``, data.result?.error as string);
|
||||
}
|
||||
|
||||
|
||||
this.#queries.delete(data.id); //clear memory
|
||||
}
|
||||
#HandleServerError(...error_msgs:string[]){
|
||||
#HandleServerError(...error_msgs: string[]) {
|
||||
if (this.lifecycle_hooks.server_error)
|
||||
this.lifecycle_hooks.server_error(this, error_msgs);
|
||||
else
|
||||
|
|
@ -727,15 +726,15 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
|
||||
//generates a unique key either via static counter or user provided key gen func
|
||||
get GenKey(): id {return this?.key_generator ? this.key_generator() : ++(SocioClient.#key as number);} //can cast to number, bcs by default the lib always uses a number
|
||||
get client_id(){return this.#client_id;}
|
||||
get GenKey(): id { return this?.key_generator ? this.key_generator() : ++(SocioClient.#key as number); } //can cast to number, bcs by default the lib always uses a number
|
||||
get client_id() { return this.#client_id; }
|
||||
get web_socket() { return this.#ws; } //the WebSocket instance has some useful properties https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#instance_properties
|
||||
get client_address_info() { return { url: this.#ws?.url, protocol: this.#ws?.protocol, extensions: this.#ws?.extensions }; } //for convenience
|
||||
get latency() { return this.#latency; } //shows the latency in ms of the initial connection handshake to determine network speed for this session. Might be useful to inform the user, if its slow.
|
||||
ready(): Promise<boolean> { return this.#is_ready === true ? (new Promise(res => res(true))) : (new Promise(res => this.#is_ready = res)) }
|
||||
Close() { this.#ws?.close(); }
|
||||
|
||||
async #GetReconToken(name: string = this.config.name as string){
|
||||
async #GetReconToken(name: string = this.config.name as string) {
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
|
||||
//ask the server for a one-time auth token
|
||||
|
|
@ -745,32 +744,32 @@ export class SocioClient extends LogHandler {
|
|||
//save down the token. Name is used to map new instance to old instance by same name.
|
||||
localStorage.setItem(`Socio_recon_token_${name}`, token); //https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage localstorage is origin locked, so should be safe to store this here
|
||||
}
|
||||
RefreshReconToken(name: string = this.config.name as string){return this.#GetReconToken(name);}
|
||||
RefreshReconToken(name: string = this.config.name as string) { return this.#GetReconToken(name); }
|
||||
|
||||
async #TryReconnect(name: string = this.config.name as string){
|
||||
async #TryReconnect(name: string = this.config.name as string) {
|
||||
const key = `Socio_recon_token_${name}`
|
||||
const token = localStorage.getItem(key);
|
||||
|
||||
if (token){
|
||||
if (token) {
|
||||
localStorage.removeItem(key); //one-time use
|
||||
|
||||
const { id, prom } = this.CreateQueryPromise();
|
||||
|
||||
//ask the server for a reconnection to an old session via our one-time token
|
||||
this.Send(ServerMessageKind.RECON, { id, type: 'USE', token } as S_RECON_USE_data);
|
||||
this.Send(ServerMessageKind.RECON, { id, type: 'USE', token } as S_RECON_USE_data);
|
||||
const res = await (prom as unknown as Promise<C_RECON_Data>);
|
||||
this.#Reconnect(res); //sets the trusted values from the server, like auth bool
|
||||
return res.result.success === 1;
|
||||
}else return false;
|
||||
} else return false;
|
||||
}
|
||||
//sets the trusted values from the server, like auth bool
|
||||
#Reconnect(data:C_RECON_Data){
|
||||
#Reconnect(data: C_RECON_Data) {
|
||||
if (data.result.success === 1) {
|
||||
this.#authenticated = data.auth;
|
||||
this.config.name = data.name;
|
||||
this.done(`${this.config.name} reconnected successfully. ${data.old_client_id} -> ${this.#client_id} (old client ID -> new/current client ID)`, data);
|
||||
}
|
||||
else{
|
||||
else {
|
||||
const error = 'Failed to reconnect'
|
||||
this.#HandleClientError(new E(error, data));
|
||||
this.#HandleServerError(error, data.result?.error as string);
|
||||
|
|
@ -778,7 +777,7 @@ export class SocioClient extends LogHandler {
|
|||
}
|
||||
|
||||
// for dev debug, if u want
|
||||
LogMaps(){
|
||||
LogMaps() {
|
||||
this.debug('queries', [...this.#queries.entries()]);
|
||||
this.debug('props', [...this.#props.entries()]);
|
||||
}
|
||||
|
|
@ -786,9 +785,9 @@ export class SocioClient extends LogHandler {
|
|||
// finds a query by its promise and registers a % update callback on its sent progress. NOTE this is not at all accurate.
|
||||
// The calculations are very time sensitive, since network speeds are super fast these days. You must set up this timer as soon as possible.
|
||||
// returns the timer ID, if it was created, so that the dev can terminate the timer manually.
|
||||
TrackProgressOfQueryPromise(prom: Promise<any>, onUpdate: ProgressOnUpdate, freq_ms = 33.34){
|
||||
for (const [id, q] of this.#queries as Map<id, QueryPromise>){
|
||||
if (q?.prom == prom){
|
||||
TrackProgressOfQueryPromise(prom: Promise<any>, onUpdate: ProgressOnUpdate, freq_ms = 33.34) {
|
||||
for (const [id, q] of this.#queries as Map<id, QueryPromise>) {
|
||||
if (q?.prom == prom) {
|
||||
return this.#CreateProgTrackingTimer(id, q.start_buff, q.payload_size || 0, onUpdate, freq_ms);
|
||||
}
|
||||
}
|
||||
|
|
@ -797,7 +796,7 @@ export class SocioClient extends LogHandler {
|
|||
|
||||
TrackProgressOfQueryID(query_id: id, onUpdate: ProgressOnUpdate, freq_ms = 33.34) {
|
||||
const q: QueryPromise = (this.#queries.get(query_id) as QueryPromise);
|
||||
if(q) return this.#CreateProgTrackingTimer(query_id, q.start_buff, q.payload_size || 0, onUpdate, freq_ms);
|
||||
if (q) return this.#CreateProgTrackingTimer(query_id, q.start_buff, q.payload_size || 0, onUpdate, freq_ms);
|
||||
else return null;
|
||||
}
|
||||
|
||||
|
|
@ -805,12 +804,12 @@ export class SocioClient extends LogHandler {
|
|||
// Returns the timer ID, in case the dev wants to stop it manually.
|
||||
// This might call onUpdate multiple times with 0 before the % starts going up.
|
||||
// NOTE, use request anim frame instead? Canceling it is a bit of a hastle and it would run faster than needed sometimes. But no slower than the framerate.
|
||||
#CreateProgTrackingTimer(query_id: id, start_buff: number, payload_size: number, onUpdate: ProgressOnUpdate, freq_ms = 33.34){
|
||||
#CreateProgTrackingTimer(query_id: id, start_buff: number, payload_size: number, onUpdate: ProgressOnUpdate, freq_ms = 33.34) {
|
||||
let last_buff_size = this.#ws?.bufferedAmount || 0;
|
||||
const intervalID = setInterval(() => {
|
||||
if (!payload_size){
|
||||
if (!payload_size) {
|
||||
payload_size = (this.#queries.get(query_id) as QueryPromise)?.payload_size || 0; //check if it exists now
|
||||
if(!payload_size) return; //skip if still not ready
|
||||
if (!payload_size) return; //skip if still not ready
|
||||
last_buff_size = this.#ws?.bufferedAmount || 0; //reset this as well, bcs it should be 0, if payload was 0. Since the payload hasnt yet been added to the buffer, but will be now.
|
||||
}
|
||||
const later_payload_ids = Array.from((this.#queries as Map<id, QueryPromise>).keys()).filter(id => id > query_id);
|
||||
|
|
@ -820,7 +819,7 @@ export class SocioClient extends LogHandler {
|
|||
last_buff_size = now_buff_size;
|
||||
|
||||
//as the now needle moves closer to 0, move the start need by the same amount. When it crosses over 0, then we've started to send out this query payload
|
||||
start_buff -= delta_buff;
|
||||
start_buff -= delta_buff;
|
||||
const p = (start_buff * -100) / (payload_size as number); //start buff below 0 is the amount of sent out so far. Invert and divide by total payload size * 100 for %.
|
||||
|
||||
onUpdate(clamp(p, 0, 100)); //while start needle is > 0, this will have negative %. When -(start needle) > payload, will be over 100%
|
||||
|
|
@ -834,16 +833,18 @@ export class SocioClient extends LogHandler {
|
|||
}
|
||||
}
|
||||
|
||||
export function ParseSocioFiles(files:SocioFiles){
|
||||
if(!files) return [];
|
||||
export function ParseSocioFiles(files: SocioFiles) {
|
||||
if (!files) return [];
|
||||
const files_array: File[] = [];
|
||||
for (const [filename, file_data] of files.entries())
|
||||
files_array.push(new File([SocioFileBase64ToUint8Array(file_data.bin)], filename, { type: file_data.meta.type, lastModified: file_data.meta.lastModified }));
|
||||
// Handle both Map (original) and plain object (MessagePack converts Maps to objects)
|
||||
const entries = files instanceof Map ? files.entries() : Object.entries(files);
|
||||
for (const [filename, file_data] of entries)
|
||||
files_array.push(new File([(typeof file_data.bin === 'string' ? SocioFileBase64ToUint8Array(file_data.bin) : pako.inflate(file_data.bin)) as BlobPart], filename, { type: file_data.meta.type, lastModified: file_data.meta.lastModified }));
|
||||
return files_array;
|
||||
}
|
||||
|
||||
// Helper function to decompress and encode data from Base64
|
||||
export function SocioFileBase64ToUint8Array(base64:string='') {
|
||||
export function SocioFileBase64ToUint8Array(base64: string = '') {
|
||||
return pako.inflate(Uint8Array.from(window.atob(base64), (v) => v.charCodeAt(0)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import * as diff_lib from 'recursive-diff'; //https://www.npmjs.com/package/recu
|
|||
|
||||
//mine
|
||||
import { QueryIsSelect, ParseQueryTables, ParseQueryVerb } from './sql-parsing.js';
|
||||
import { SocioStringParse, GetAllMethodNamesOf, yaml_parse, initLifecycleHooks } from './utils.js';
|
||||
import { SocioStringParse, GetAllMethodNamesOf, socio_decode, initLifecycleHooks } from './utils.js';
|
||||
import { E, LogHandler, err, log, info, done, ErrorOrigin } from './logging.js';
|
||||
import { UUID, type SocioSecurity } from './secure.js';
|
||||
import { SocioSession, type SubObj } from './core-session.js';
|
||||
|
|
@ -30,33 +30,33 @@ import type { SocioStringObj } from './sql-parsing.js';
|
|||
export type QueryFuncParams = { id?: id, sql: string, params?: any };
|
||||
export type QueryFunction = (client: SocioSession, id: id, sql: string, params?: any) => Promise<object>;
|
||||
|
||||
type SessionsDefaults = {
|
||||
timeouts: boolean,
|
||||
timeouts_check_interval_ms?: number,
|
||||
session_delete_delay_ms?: number,
|
||||
recon_ttl_ms?: number
|
||||
type SessionsDefaults = {
|
||||
timeouts: boolean,
|
||||
timeouts_check_interval_ms?: number,
|
||||
session_delete_delay_ms?: number,
|
||||
recon_ttl_ms?: number
|
||||
} & SessionOpts;
|
||||
type DecryptOptions = { decrypt_sql: boolean, decrypt_prop: boolean, decrypt_endpoint: boolean };
|
||||
type DBOpts = {
|
||||
Query?: QueryFunction,
|
||||
Arbiter?: (initiator: { client: SocioSession, sql: string, params: any }, current: { client: SocioSession, hook: SubObj }) => boolean | Promise<boolean>,
|
||||
allowed_SQL_verbs?: string[]
|
||||
type DBOpts = {
|
||||
Query?: QueryFunction,
|
||||
Arbiter?: (initiator: { client: SocioSession, sql: string, params: any }, current: { client: SocioSession, hook: SubObj }) => boolean | Promise<boolean>,
|
||||
allowed_SQL_verbs?: string[]
|
||||
};
|
||||
type SocioServerOptions = {
|
||||
db: DBOpts,
|
||||
socio_security?: SocioSecurity | null,
|
||||
type SocioServerOptions = {
|
||||
db: DBOpts,
|
||||
socio_security?: SocioSecurity | null,
|
||||
decrypt_opts?: DecryptOptions,
|
||||
allow_discovery?: boolean,
|
||||
allow_rpc?:boolean,
|
||||
hard_crash?: boolean,
|
||||
session_defaults?: SessionsDefaults,
|
||||
prop_upd_diff?: boolean,
|
||||
auto_recon_by_ip?: boolean,
|
||||
send_sensitive_error_msgs_to_client?:boolean,
|
||||
hooks?: Partial<ServerLifecycleHooks>
|
||||
[key:string]:any
|
||||
allow_rpc?: boolean,
|
||||
hard_crash?: boolean,
|
||||
session_defaults?: SessionsDefaults,
|
||||
prop_upd_diff?: boolean,
|
||||
auto_recon_by_ip?: boolean,
|
||||
send_sensitive_error_msgs_to_client?: boolean,
|
||||
hooks?: Partial<ServerLifecycleHooks>
|
||||
[key: string]: any
|
||||
} & LoggingOpts;
|
||||
type AdminServerMessageDataObj = {function:string, args?:any[], secure_key:string};
|
||||
type AdminServerMessageDataObj = { function: string, args?: any[], secure_key: string };
|
||||
|
||||
|
||||
//NB! some fields in these variables are private for safety reasons, but also bcs u shouldnt be altering them, only if through my defined ways. They are mostly expected to be constants.
|
||||
|
|
@ -67,14 +67,14 @@ export class SocioServer extends LogHandler {
|
|||
#sessions: Map<ClientID, SocioSession> = new Map(); //Maps are quite more performant than objects. And their keys dont overlap with Object prototype.
|
||||
|
||||
//if constructor is given a SocioSecure object, then that will be used to decrypt all incomming messages, if the msg flag is set
|
||||
#secure: { socio_security: SocioSecurity | null, allow_discovery: boolean, allow_rpc:boolean } & DecryptOptions;
|
||||
#secure: { socio_security: SocioSecurity | null, allow_discovery: boolean, allow_rpc: boolean } & DecryptOptions;
|
||||
#cypther_text_cache: Map<string, SocioStringObj> = new Map(); //decyphering at runtime is costly, so cache validated, secure results.
|
||||
|
||||
//backend props, e.g. strings for colors, that clients can subscribe to and alter
|
||||
#props: Map<PropKey, { val: PropValue, assigner: PropAssigner, updates: Map<ClientID, { id: id, rate_limiter?: RateLimiter }> } & PropOpts> = new Map();
|
||||
|
||||
//rate limits server functions globally
|
||||
#ratelimits: { [key: string]: RateLimiter | null } = { con: null, upd:null};
|
||||
#ratelimits: { [key: string]: RateLimiter | null } = { con: null, upd: null };
|
||||
|
||||
//If the hook returns a truthy value, then it is assumed, that the hook handled the msg and the lib will not. Otherwise, by default, the lib handles the msg.
|
||||
//msg hook receives all incomming msgs to the server.
|
||||
|
|
@ -97,29 +97,29 @@ export class SocioServer extends LogHandler {
|
|||
session_defaults: SessionsDefaults = { timeouts: false, timeouts_check_interval_ms: 1000 * 60, session_timeout_ttl_ms: Infinity, session_delete_delay_ms: 1000 * 5, recon_ttl_ms: 1000 * 60 * 60 };
|
||||
lifecycle_hooks!: ServerLifecycleHooks; //Add your callback to a valid hook key here. They will be called if they exist
|
||||
prop_reg_timeout_ms!: number;
|
||||
auto_recon_by_ip:boolean = false;
|
||||
send_sensitive_error_msgs_to_client!:boolean;
|
||||
allow_rpc!:boolean;
|
||||
auto_recon_by_ip: boolean = false;
|
||||
send_sensitive_error_msgs_to_client!: boolean;
|
||||
allow_rpc!: boolean;
|
||||
|
||||
constructor(opts: ServerOptions | undefined = {}, {
|
||||
db,
|
||||
socio_security = null,
|
||||
allow_discovery = false,
|
||||
allow_rpc = false,
|
||||
logging = { verbose: false, hard_crash: false },
|
||||
decrypt_opts = { decrypt_sql: true, decrypt_prop: false, decrypt_endpoint: false },
|
||||
session_defaults = undefined,
|
||||
prop_upd_diff = false,
|
||||
prop_reg_timeout_ms = 1000 * 10,
|
||||
auto_recon_by_ip = false,
|
||||
send_sensitive_error_msgs_to_client = true,
|
||||
hooks = {},
|
||||
}: SocioServerOptions){
|
||||
super({ ...logging, prefix:'SocioServer'});
|
||||
constructor(opts: ServerOptions | undefined = {}, {
|
||||
db,
|
||||
socio_security = null,
|
||||
allow_discovery = false,
|
||||
allow_rpc = false,
|
||||
logging = { verbose: false, hard_crash: false },
|
||||
decrypt_opts = { decrypt_sql: true, decrypt_prop: false, decrypt_endpoint: false },
|
||||
session_defaults = undefined,
|
||||
prop_upd_diff = false,
|
||||
prop_reg_timeout_ms = 1000 * 10,
|
||||
auto_recon_by_ip = false,
|
||||
send_sensitive_error_msgs_to_client = true,
|
||||
hooks = {},
|
||||
}: SocioServerOptions) {
|
||||
super({ ...logging, prefix: 'SocioServer' });
|
||||
//verbose - print stuff to the console using my lib. Doesnt affect the log handlers
|
||||
//hard_crash will just crash the class instance and propogate (throw) the error encountered without logging it anywhere - up to you to handle.
|
||||
//both are public and settable at runtime
|
||||
|
||||
|
||||
//private:
|
||||
this.#wss = new WebSocketServer({ ...opts, clientTracking: true }); //take a look at the WebSocketServer docs - the opts can have a server param, that can be your http server
|
||||
this.#secure = { socio_security, ...decrypt_opts, allow_discovery, allow_rpc };
|
||||
|
|
@ -136,27 +136,27 @@ export class SocioServer extends LogHandler {
|
|||
|
||||
this.#wss.on('connection', this.#Connect.bind(this)); //https://thenewstack.io/mastering-javascript-callbacks-bind-apply-call/ have to bind 'this' to the function, otherwise it will use the .on()'s 'this', so that this.[prop] are not undefined
|
||||
this.#wss.on('close', (...stuff) => { this.HandleInfo('WebSocketServer close event', ...stuff) });
|
||||
this.#wss.on('error', (...stuff) => { this.HandleError(new E('WebSocketServer error event', ...stuff))});
|
||||
this.#wss.on('error', (...stuff) => { this.HandleError(new E('WebSocketServer error event', ...stuff)) });
|
||||
|
||||
//set up interval timer to check if sessions are timed out.
|
||||
if (this.session_defaults.timeouts)
|
||||
setInterval(this.#CheckSessionsTimeouts.bind(this), this.session_defaults.timeouts_check_interval_ms);
|
||||
|
||||
// log info for the dev
|
||||
if (this.verbose){
|
||||
if (this.verbose) {
|
||||
const addr: AddressInfo = this.#wss.address() as AddressInfo;
|
||||
this.done(`Created SocioServer on`, addr);
|
||||
if (addr.family == 'ws')
|
||||
this.HandleInfo('WARNING! Your server is using an unsecure WebSocket protocol, setup wss:// instead, when you can!');
|
||||
if (!socio_security)
|
||||
this.HandleInfo('WARNING! Please use the SocioSecurity class in production to securely de/encrypt Socio strings from clients!');
|
||||
if (this.send_sensitive_error_msgs_to_client)
|
||||
if (this.send_sensitive_error_msgs_to_client)
|
||||
this.HandleInfo('WARNING! send_sensitive_error_msgs_to_client field IS TRUE, which means server error messages are sent to the client as is. They might include sesitive info. If false, the server will only send a generic error message.')
|
||||
}
|
||||
}
|
||||
|
||||
async #Connect(conn: WebSocket, request: IncomingMessage){
|
||||
try{
|
||||
async #Connect(conn: WebSocket, request: IncomingMessage) {
|
||||
try {
|
||||
//construct the new session with a unique client ID
|
||||
let client_id: ClientID = (this.lifecycle_hooks.gen_client_id ? await this.lifecycle_hooks.gen_client_id() : UUID())?.toString();
|
||||
while (this.#sessions.has(client_id)) //avoid id collisions
|
||||
|
|
@ -167,7 +167,7 @@ export class SocioServer extends LogHandler {
|
|||
const client_ip = 'x-forwarded-for' in request?.headers ? request.headers['x-forwarded-for'].split(',')[0].trim() : request.socket.remoteAddress;
|
||||
|
||||
//create the socio session class and save down the client id ref for convenience later
|
||||
const client = new SocioSession(client_id, conn, client_ip, { logging: { verbose: this.verbose }, session_opts: { session_timeout_ttl_ms: this.session_defaults.session_timeout_ttl_ms, max_payload_size: this.session_defaults.max_payload_size} });
|
||||
const client = new SocioSession(client_id, conn, client_ip, { logging: { verbose: this.verbose }, session_opts: { session_timeout_ttl_ms: this.session_defaults.session_timeout_ttl_ms, max_payload_size: this.session_defaults.max_payload_size } });
|
||||
this.#sessions.set(client_id, client);
|
||||
|
||||
//pass the object to the connection hook, if it exists. It cant take over
|
||||
|
|
@ -181,14 +181,14 @@ export class SocioServer extends LogHandler {
|
|||
this.#Message.bind(this)(this.#sessions.get(client_id), req, isBinary);
|
||||
else conn?.close();
|
||||
});
|
||||
conn.on('close', (code:number, reason:Buffer) => { this.#SocketClosed.bind(this)(client, {code, reason:reason.toString('utf8')}) });
|
||||
conn.on('close', (code: number, reason: Buffer) => { this.#SocketClosed.bind(this)(client, { code, reason: reason.toString('utf8') }) });
|
||||
conn.on('error', (error: Error) => { this.#SocketClosed.bind(this)(client, error) }); //https://github.com/websockets/ws/blob/master/doc/ws.md#event-error-1
|
||||
|
||||
|
||||
// socio can recognize that the IP matches an existing session, so it can reconnect to it, keeping the old sessions data
|
||||
if(this.auto_recon_by_ip){
|
||||
if (this.auto_recon_by_ip) {
|
||||
// find an IP matching session
|
||||
for (const [id, ses] of this.#sessions.entries()){
|
||||
if(id !== client_id && ses.ipAddr === client_ip){
|
||||
for (const [id, ses] of this.#sessions.entries()) {
|
||||
if (id !== client_id && ses.ipAddr === client_ip) {
|
||||
//recon procedure
|
||||
const old_client = this.#sessions.get(id) as SocioSession;
|
||||
this.ReconnectClientSession(client, old_client);
|
||||
|
|
@ -204,7 +204,7 @@ export class SocioServer extends LogHandler {
|
|||
} catch (e: err) { this.HandleError(e); }
|
||||
}
|
||||
|
||||
async #SocketClosed(client:SocioSession, event_args:any){
|
||||
async #SocketClosed(client: SocioSession, event_args: any) {
|
||||
//trigger hook
|
||||
if (this.lifecycle_hooks.discon)
|
||||
await this.lifecycle_hooks.discon(client);
|
||||
|
|
@ -222,14 +222,22 @@ export class SocioServer extends LogHandler {
|
|||
}, this.session_defaults.session_delete_delay_ms as number);
|
||||
}
|
||||
|
||||
get new_global_id(){return ++this.#global_largest_id}
|
||||
async #Message(client:SocioSession, req: Buffer | ArrayBuffer | Buffer[], isBinary: Boolean){
|
||||
get new_global_id() { return ++this.#global_largest_id }
|
||||
async #Message(client: SocioSession, req: Buffer | ArrayBuffer | Buffer[], isBinary: Boolean) {
|
||||
// general try for crashes.
|
||||
// The catch just notifies the server of the error,
|
||||
// but the client cannot be notified, since at that time the message ID might not be known
|
||||
try{
|
||||
//handle binary data and return
|
||||
if(isBinary){
|
||||
try {
|
||||
let kind: ServerMessageKind;
|
||||
let data: ServerMessageDataObj;
|
||||
try {
|
||||
const decoded = socio_decode(req as unknown as Uint8Array);
|
||||
kind = decoded.kind;
|
||||
data = decoded.data;
|
||||
|
||||
if (kind === undefined || data === undefined) throw new Error('Not a socio message');
|
||||
} catch (e) {
|
||||
// if decoding fails, assume it is a BLOB
|
||||
this.HandleInfo(`recv: BLOB from ${client.id}`)
|
||||
if (this.lifecycle_hooks.blob) {
|
||||
if (await this.lifecycle_hooks.blob(client, req))
|
||||
|
|
@ -239,8 +247,6 @@ export class SocioServer extends LogHandler {
|
|||
else client.Send(ClientMessageKind.RES, { id: 'BLOB', result: { success: 0, error: 'Server does not handle the BLOB hook.' } } as C_RES_data);
|
||||
return;
|
||||
}
|
||||
|
||||
const { kind, data }: { kind: ServerMessageKind; data: ServerMessageDataObj } = yaml_parse(req.toString());
|
||||
const client_id = client.id; //cache the ID, since its used so much here
|
||||
// save the biggest ID found to avoid ID collisions when sending msgs between clients, since they all have their own ID counter
|
||||
if (typeof data.id === 'number' && data.id > this.#global_largest_id) this.#global_largest_id = data.id;
|
||||
|
|
@ -248,7 +254,7 @@ export class SocioServer extends LogHandler {
|
|||
// this try catch allows the body to freely throw E or strings or crash in any other way,
|
||||
// and the client will still receive a RES with success:0, since now it has the message ID from data
|
||||
// it will then throw again to the outter try
|
||||
try{
|
||||
try {
|
||||
//if the socio security instance exists and some specific string fields was recieved and they are to be decrypted, then do so here
|
||||
if (this.#secure.socio_security) {
|
||||
for (const field of ['sql', 'prop', 'endpoint'])
|
||||
|
|
@ -257,7 +263,7 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
|
||||
if (kind !== ServerMessageKind.OK) //this
|
||||
this.HandleInfo(`recv: [${ServerMessageKind[kind]}] from [${client.name ? client.name + ' | ' : ''}${client_id}]`, kind != ServerMessageKind.UP_FILES ? data : `File count: ${(data as S_UP_FILES_data).files?.size}`);
|
||||
this.HandleInfo(`recv: [${ServerMessageKind[kind]}] from [${client.name ? client.name + ' | ' : ''}${client_id}]`, kind != ServerMessageKind.UP_FILES ? data : `File count: ${(data as S_UP_FILES_data).files?.size}`);
|
||||
|
||||
//let the developer handle the msg
|
||||
if (this.lifecycle_hooks.msg)
|
||||
|
|
@ -278,7 +284,7 @@ export class SocioServer extends LogHandler {
|
|||
if (this.lifecycle_hooks.endpoint)
|
||||
//@ts-expect-error
|
||||
(data as S_SUB_data).sql = await this.lifecycle_hooks.endpoint(client, (data as S_SUB_data).endpoint);
|
||||
else throw new E('Client sent endpoint instead of SQL, but its hook is missing, so cant resolve it. [#no-endpoint-hook-SUB]', {kind, data});
|
||||
else throw new E('Client sent endpoint instead of SQL, but its hook is missing, so cant resolve it. [#no-endpoint-hook-SUB]', { kind, data });
|
||||
}
|
||||
|
||||
// check that there is sql to work with; the verb can be parsed; verb is allowed
|
||||
|
|
@ -333,17 +339,17 @@ export class SocioServer extends LogHandler {
|
|||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 1, res: await res } } as C_RES_data); //wait for result and send it back
|
||||
|
||||
//if the sql wasnt a SELECT, but altered some resource, then need to propogate that to other connection hooks
|
||||
if (query_verb !== 'SELECT'){
|
||||
if(query_verb === 'DROP'){
|
||||
if (query_verb !== 'SELECT') {
|
||||
if (query_verb === 'DROP') {
|
||||
//to avoid problems for the dev, Socio will auto unsub all subs to the dropped table. The clients needn't be notified, since they just wont ever receive and UPD for it anymore. Which isnt an issue.
|
||||
const dropped_table = ParseQueryTables((data as S_SQL_data).sql);
|
||||
if(dropped_table){
|
||||
for(const session of this.#sessions.values())
|
||||
if (dropped_table) {
|
||||
for (const session of this.#sessions.values())
|
||||
for (const sub of session.GetSubsForTables(dropped_table))
|
||||
session.UnRegisterSub(sub.id);
|
||||
}
|
||||
else throw new E('Failed to parse table of a client DROP query', {kind, data});
|
||||
}else
|
||||
else throw new E('Failed to parse table of a client DROP query', { kind, data });
|
||||
} else
|
||||
this.Update(client, (data as S_SQL_data).sql || '', (data as S_SQL_data)?.params);
|
||||
}
|
||||
break;
|
||||
|
|
@ -431,7 +437,7 @@ export class SocioServer extends LogHandler {
|
|||
const prop_val = this.GetPropVal((data as S_PROP_GET_data)?.prop);
|
||||
client.Send(ClientMessageKind.RES, {
|
||||
id: data.id,
|
||||
result: { success: prop_val !== undefined ? 1 : 0, res: prop_val, error: prop_val === undefined ? 'Server couldnt find prop' : ''}
|
||||
result: { success: prop_val !== undefined ? 1 : 0, res: prop_val, error: prop_val === undefined ? 'Server couldnt find prop' : '' }
|
||||
} as data_result_block);
|
||||
break;
|
||||
}
|
||||
|
|
@ -441,7 +447,7 @@ export class SocioServer extends LogHandler {
|
|||
//UpdatePropVal does not set the new val, rather it calls the assigner, which is responsible for setting the new value.
|
||||
const result = this.UpdatePropVal((data as S_PROP_SET_data).prop as string, (data as S_PROP_SET_data)?.prop_val, client.id, data.hasOwnProperty('prop_upd_as_diff') ? (data as S_PROP_SET_data).prop_upd_as_diff : this.#prop_upd_diff); //the assigner inside Update dictates, if this was a successful set.
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: result } } as data_result_block); //resolve this request to true, so the client knows everything went fine.
|
||||
}
|
||||
}
|
||||
else throw new E('Prop is not client_writable.', data);
|
||||
break;
|
||||
}
|
||||
|
|
@ -583,10 +589,10 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
case ServerMessageKind.IDENTIFY: { //use for session to identify itself with a unique human-readable string
|
||||
const name = (data as { id: id, name: string }).name;
|
||||
|
||||
if (Object.values(this.GetSessionsInfo()).some(s => s.name === name)){
|
||||
|
||||
if (Object.values(this.GetSessionsInfo()).some(s => s.name === name)) {
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 0, error: 'A session already has this name!' } });
|
||||
}else{
|
||||
} else {
|
||||
client.name = name;
|
||||
|
||||
if (this.lifecycle_hooks?.identify)
|
||||
|
|
@ -598,30 +604,30 @@ export class SocioServer extends LogHandler {
|
|||
break;
|
||||
}
|
||||
case ServerMessageKind.DISCOVERY: {
|
||||
if(this.#secure.allow_discovery === true){
|
||||
if (this.#secure.allow_discovery === true) {
|
||||
// let the dev hook handle the discovery logic of what info to get and send from sessions to client
|
||||
if (this.lifecycle_hooks?.discovery)
|
||||
if (await this.lifecycle_hooks.discovery(client, data))
|
||||
if (await this.lifecycle_hooks.discovery(client, data))
|
||||
return;
|
||||
|
||||
// or use my provided basic info response
|
||||
client.Send(ClientMessageKind.RES, {id: data.id, result: {success:1, res: this.GetSessionsInfo()}})
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 1, res: this.GetSessionsInfo() } })
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ServerMessageKind.RPC: {
|
||||
// rpc must be enabled
|
||||
if(this.#secure.allow_rpc !== true){
|
||||
if (this.#secure.allow_rpc !== true) {
|
||||
const error = 'Client tried RPC, but the server hasnt enabled it. [#rpc-not-enabled]';
|
||||
this.HandleDebug(error, client, data);
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 0, error }});
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 0, error } });
|
||||
return;
|
||||
}
|
||||
|
||||
// let the RPC hook handle it. If it returns anything other than undefined, that will be sent back as the result early
|
||||
if (this.lifecycle_hooks.rpc) {
|
||||
const res = await this.lifecycle_hooks.rpc((data as S_RPC_data).target_client, (data as S_RPC_data).f_name, (data as S_RPC_data).args);
|
||||
if(res !== undefined){
|
||||
if (res !== undefined) {
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 1, res } });
|
||||
return;
|
||||
}
|
||||
|
|
@ -629,14 +635,14 @@ export class SocioServer extends LogHandler {
|
|||
|
||||
// hook didnt handle it, so do some magic
|
||||
// if its null, then assume its meant for the server functions
|
||||
if ((data as S_RPC_data).target_client === null){
|
||||
if ((data as S_RPC_data).f_name in this){
|
||||
if ((data as S_RPC_data).target_client === null) {
|
||||
if ((data as S_RPC_data).f_name in this) {
|
||||
const res = this[(data as S_RPC_data).f_name](...(data as S_RPC_data).args);
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 1, res } });
|
||||
return;
|
||||
}
|
||||
else throw new E(`Client RPC to server, but there is no [${(data as S_RPC_data).f_name}] function on the SocioServer class instance! [#unknown-server-func-rpc]`, { client_id: client.id, data });
|
||||
}else{
|
||||
} else {
|
||||
const target_c = this.#sessions.get((data as S_RPC_data).target_client!);
|
||||
if (!target_c) {
|
||||
const error = 'Client tried RPC, but the target client doesnt exist. [#rpc-no-target]';
|
||||
|
|
@ -655,7 +661,7 @@ export class SocioServer extends LogHandler {
|
|||
this.#CreateClientQueryPromise(new_id, ServerMessageKind.RPC)
|
||||
.then(res => {
|
||||
//respond with the original ID of the 1st client
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 1, res }});
|
||||
client.Send(ClientMessageKind.RES, { id: data.id, result: { success: 1, res } });
|
||||
})
|
||||
// ive set up a timeout for this promise, but it might fail for other reasons too
|
||||
.catch(reason => {
|
||||
|
|
@ -667,12 +673,12 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
case ServerMessageKind.OK: {
|
||||
const q = this.#client_queries.get(data.id);
|
||||
if(q){
|
||||
if (q) {
|
||||
this.HandleInfo(`recv: [OK ${ServerMessageKind[q.for_msg_kind]}] from [${client.name ? client.name + ' | ' : ''}${client_id}]`, data);
|
||||
q.resolve((data as data_base & {return:any}).return); //resolve the promise thats being awaited in some other kind case
|
||||
q.resolve((data as data_base & { return: any }).return); //resolve the promise thats being awaited in some other kind case
|
||||
this.#client_queries.delete(data.id); //remove it
|
||||
}
|
||||
else throw new E(`Received OK from client for an unknown client query. [#client-query-not-found]`, {sender:client.id, data});
|
||||
}
|
||||
else throw new E(`Received OK from client for an unknown client query. [#client-query-not-found]`, { sender: client.id, data });
|
||||
break;
|
||||
}
|
||||
// case ServerMessageKind: { break;}
|
||||
|
|
@ -681,7 +687,7 @@ export class SocioServer extends LogHandler {
|
|||
throw new E(`Unrecognized message kind! [#unknown-msg-kind]`, { kind, data });
|
||||
}
|
||||
}
|
||||
}catch(e:err){
|
||||
} catch (e: err) {
|
||||
client.Send(ClientMessageKind.RES, {
|
||||
id: data.id,
|
||||
result: { success: 0, error: this.send_sensitive_error_msgs_to_client ? String(e) : 'Server had an error with this request.' }
|
||||
|
|
@ -692,14 +698,14 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
|
||||
//this assumes that this.#secure.socio_security is properly assigned
|
||||
#Decrypt(client:SocioSession, str:string, is_sql:boolean):string{
|
||||
#Decrypt(client: SocioSession, str: string, is_sql: boolean): string {
|
||||
let socio_string_obj: SocioStringObj;
|
||||
|
||||
// first check the cache, if this cyphertext has already been verified as valid and secure
|
||||
if (this.#cypther_text_cache.has(str))
|
||||
socio_string_obj = this.#cypther_text_cache.get(str) as SocioStringObj;
|
||||
// otherwise decrypt
|
||||
else{
|
||||
else {
|
||||
//check crypt format "[iv_base64] [encrypted_text_base64] [auth_tag_base64]" where each part is base64 encoded
|
||||
const parts = str.includes(' ') ? str.split(' ') : [];
|
||||
if (parts.length != 3)
|
||||
|
|
@ -737,10 +743,10 @@ export class SocioServer extends LogHandler {
|
|||
return socio_string_obj.str;
|
||||
}
|
||||
|
||||
async Update(initiator:SocioSession, sql:string, params:object | null){
|
||||
async Update(initiator: SocioSession, sql: string, params: object | null) {
|
||||
//rate limit check
|
||||
if(this.#ratelimits.upd)
|
||||
if(this.#ratelimits.upd.CheckLimit())
|
||||
if (this.#ratelimits.upd)
|
||||
if (this.#ratelimits.upd.CheckLimit())
|
||||
return;
|
||||
|
||||
//hand off to hook
|
||||
|
|
@ -752,14 +758,14 @@ export class SocioServer extends LogHandler {
|
|||
throw 'SocioServer.Update requires a Database Query function on SocioServer! [#no-db-query-UPDATE]';
|
||||
|
||||
//or go through each session's every hook and query the DB for its result, then send it to the client
|
||||
try{
|
||||
try {
|
||||
const tables = ParseQueryTables(sql);
|
||||
if (tables.length == 0) throw new E('Update ParseQueryTables didnt find any table names in the SQL. Something must be wrong.', { initiator, sql, params})
|
||||
|
||||
if (tables.length == 0) throw new E('Update ParseQueryTables didnt find any table names in the SQL. Something must be wrong.', { initiator, sql, params })
|
||||
|
||||
const cache: Map<number, object> = new Map(); //cache the queries to not spam the DB in this loop
|
||||
|
||||
for (const client of this.#sessions.values()){
|
||||
for (const hook of client.GetSubsForTables(tables)){ //GetSubsForTables always returns array. If empty, then the foreach wont run, so each sql guaranteed to have hooks array
|
||||
for (const client of this.#sessions.values()) {
|
||||
for (const hook of client.GetSubsForTables(tables)) { //GetSubsForTables always returns array. If empty, then the foreach wont run, so each sql guaranteed to have hooks array
|
||||
//rate limit check
|
||||
if (hook.rate_limiter && hook.rate_limiter.CheckLimit()) return;
|
||||
|
||||
|
|
@ -778,24 +784,24 @@ export class SocioServer extends LogHandler {
|
|||
.then(res => {
|
||||
client.Send(ClientMessageKind.UPD, {
|
||||
id: hook.id,
|
||||
result: { success: 1, res}
|
||||
result: { success: 1, res }
|
||||
} as C_UPD_data);
|
||||
cache.set(hook.cache_hash, res);
|
||||
})
|
||||
.catch(error => client.Send(ClientMessageKind.UPD, {
|
||||
id: hook.id,
|
||||
result: {success: 0, error},
|
||||
result: { success: 0, error },
|
||||
} as C_UPD_data));
|
||||
};
|
||||
}
|
||||
} catch (e:err) { this.HandleError(e) }
|
||||
} catch (e: err) { this.HandleError(e) }
|
||||
}
|
||||
|
||||
#CheckPropExists(prop: PropKey | undefined, client: SocioSession, msg_id:id, error_msg: string){
|
||||
if (!prop || !(this.#props.has(prop))){
|
||||
#CheckPropExists(prop: PropKey | undefined, client: SocioSession, msg_id: id, error_msg: string) {
|
||||
if (!prop || !(this.#props.has(prop))) {
|
||||
client.Send(ClientMessageKind.RES, {
|
||||
id: msg_id,
|
||||
result: {success:0, error:error_msg}
|
||||
result: { success: 0, error: error_msg }
|
||||
} as C_RES_data);
|
||||
throw new E(error_msg, prop, client.id);
|
||||
}
|
||||
|
|
@ -805,9 +811,9 @@ export class SocioServer extends LogHandler {
|
|||
// this.lifecycle_hooks[f_name] = handler;
|
||||
// }
|
||||
|
||||
RegisterRateLimit(f_name: string, ratelimit: RateLimit | null = null){
|
||||
RegisterRateLimit(f_name: string, ratelimit: RateLimit | null = null) {
|
||||
try {
|
||||
if (f_name in this.#ratelimits){
|
||||
if (f_name in this.#ratelimits) {
|
||||
if (ratelimit) {
|
||||
this.#ratelimits[f_name] = new RateLimiter(ratelimit);
|
||||
}
|
||||
|
|
@ -824,27 +830,27 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
get RateLimitNames() { return Object.keys(this.#ratelimits) }
|
||||
|
||||
GetClientSession(client_id=''){
|
||||
GetClientSession(client_id = '') {
|
||||
return this.#sessions.get(client_id);
|
||||
}
|
||||
|
||||
//assigner defaults to basic setter
|
||||
RegisterProp(key: PropKey, val: PropValue, { assigner = this.SetPropVal.bind(this), client_writable = true, send_as_diff = undefined, emit_to_sender = false, observationaly_temporary=false }: { assigner?: PropAssigner } & PropOpts = {}){
|
||||
try{
|
||||
RegisterProp(key: PropKey, val: PropValue, { assigner = this.SetPropVal.bind(this), client_writable = true, send_as_diff = undefined, emit_to_sender = false, observationaly_temporary = false }: { assigner?: PropAssigner } & PropOpts = {}) {
|
||||
try {
|
||||
if (this.#props.has(key))
|
||||
throw new E(`Prop key [${key}] has already been registered and for client continuity is forbiden to over-write at runtime. [#prop-key-exists]`);
|
||||
else
|
||||
this.#props.set(key, { val, assigner, updates: new Map(), client_writable, send_as_diff, emit_to_sender, observationaly_temporary });
|
||||
if(observationaly_temporary)
|
||||
if (observationaly_temporary)
|
||||
this.HandleDebug('Temporary Prop registered!', key);
|
||||
|
||||
} catch (e: err) { this.HandleError(e) }
|
||||
}
|
||||
UnRegisterProp(key: PropKey){
|
||||
UnRegisterProp(key: PropKey) {
|
||||
try {
|
||||
const prop = this.#props.get(key);
|
||||
if (!prop) throw new E(`Prop key [${key}] not registered! [#UnRegisterProp-prop-not-found]`);
|
||||
|
||||
|
||||
//drop the prop first, so that it cant be subbed to while informing clients - a rare but potential issue
|
||||
if (!this.#props.delete(key))
|
||||
throw new E(`Error deleting prop key [${key}]. [#prop-key-del-error]`);
|
||||
|
|
@ -857,14 +863,14 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
} catch (e: err) { this.HandleError(e) }
|
||||
}
|
||||
GetPropVal(key: PropKey): PropValue | undefined{
|
||||
GetPropVal(key: PropKey): PropValue | undefined {
|
||||
return this.#props.get(key)?.val;
|
||||
}
|
||||
//UpdatePropVal does not set the new val, rather it calls the assigner, which is responsible for setting the new value.
|
||||
UpdatePropVal(key: PropKey, new_val: PropValue, sender_client_id: ClientID | null, send_as_diff = this.#prop_upd_diff):Bit{//this will propogate the change, if it is assigned, to all subscriptions
|
||||
UpdatePropVal(key: PropKey, new_val: PropValue, sender_client_id: ClientID | null, send_as_diff = this.#prop_upd_diff): Bit {//this will propogate the change, if it is assigned, to all subscriptions
|
||||
const prop = this.#props.get(key);
|
||||
if (!prop) throw new E(`Prop key [${key}] not registered! [#prop-update-not-found]`);
|
||||
|
||||
|
||||
const old_prop_val = prop.val; //bcs the assigner somehow changes this property. Weird.
|
||||
//Dont think JS allows such ref pointers to work. But this then keeps the correct val.
|
||||
//This idea works bcs the mutator of the data should be the first to run this and all other session will get informed here with that sessions diff.
|
||||
|
|
@ -879,9 +885,9 @@ export class SocioServer extends LogHandler {
|
|||
if (sender_client_id === client_id && prop.emit_to_sender === false) continue; //prop can be set to not emit an update back to the initiator of this prop set.
|
||||
|
||||
//do the thing
|
||||
if (this.#sessions.has(client_id)){
|
||||
if (this.#sessions.has(client_id)) {
|
||||
//prepare object of both cases
|
||||
const upd_data = { id: args.id, prop:key };
|
||||
const upd_data = { id: args.id, prop: key };
|
||||
|
||||
//overload the global Socio Server flag with a per prop flag
|
||||
if (prop?.send_as_diff && typeof prop?.send_as_diff == 'boolean') send_as_diff = prop.send_as_diff;
|
||||
|
|
@ -906,7 +912,7 @@ export class SocioServer extends LogHandler {
|
|||
return 0;
|
||||
}
|
||||
SetPropVal(key: PropKey, new_val: PropValue): boolean { //this hard sets the value without checks or updating clients
|
||||
try{
|
||||
try {
|
||||
const prop = this.#props.get(key);
|
||||
if (prop === undefined)
|
||||
throw new E(`Prop key [${key}] not registered! [#prop-set-not-found]`);
|
||||
|
|
@ -917,9 +923,9 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
|
||||
//send some data to all clients by their ID or unique name, if they have one. By default emits to all connected clients
|
||||
async SendToClients(clients: (ClientID | string)[] = [], data: object = {}, kind: ClientMessageKind = ClientMessageKind.CMD){
|
||||
async SendToClients(clients: (ClientID | string)[] = [], data: object = {}, kind: ClientMessageKind = ClientMessageKind.CMD) {
|
||||
let sessions = this.#sessions.values(); //all clients by default
|
||||
if(clients.length) //filter specified ones
|
||||
if (clients.length) //filter specified ones
|
||||
sessions = sessions.filter(c => clients.includes(c.id) || (c?.name && clients.includes(c.name)));
|
||||
|
||||
// queue up all the sends at once and let the async event loop figure out the optimal paralel stuff
|
||||
|
|
@ -929,7 +935,7 @@ export class SocioServer extends LogHandler {
|
|||
|
||||
return Promise.all(proms); //return a promise of when all the sends have been awaited
|
||||
}
|
||||
#CreateClientQueryPromise(id: id, for_msg_kind:ServerMessageKind){
|
||||
#CreateClientQueryPromise(id: id, for_msg_kind: ServerMessageKind) {
|
||||
return new Promise((res, rej) => {
|
||||
// add timeout, so the server doesnt fill memory for unresponsive clients
|
||||
const timer = setTimeout(() => {
|
||||
|
|
@ -946,25 +952,25 @@ export class SocioServer extends LogHandler {
|
|||
}
|
||||
|
||||
//https://stackoverflow.com/a/54875979/8422448
|
||||
async #Admin(function_name:string = '', args:any[] = []){
|
||||
try{
|
||||
async #Admin(function_name: string = '', args: any[] = []) {
|
||||
try {
|
||||
if (GetAllMethodNamesOf(this).includes(function_name))
|
||||
return this[function_name].call(this, ...args); //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
|
||||
else
|
||||
return `[${function_name}] is not a name of a function on the SocioServer instance`;
|
||||
}catch(e){return e;}
|
||||
} catch (e) { return e; }
|
||||
}
|
||||
get methods() { return GetAllMethodNamesOf(this) }
|
||||
|
||||
#ClearClientSessionSubs(client_id:string){
|
||||
#ClearClientSessionSubs(client_id: string) {
|
||||
this.#sessions.get(client_id)?.ClearSubs(); //clear query subs
|
||||
for (const prop of this.#props.values()) { prop.updates.delete(client_id); }; //clear prop subs
|
||||
}
|
||||
|
||||
async #CheckSessionsTimeouts(){
|
||||
async #CheckSessionsTimeouts() {
|
||||
const now = (new Date()).getTime();
|
||||
for (const client of this.#sessions.values()){
|
||||
if (now >= client.last_seen + client.session_opts.session_timeout_ttl_ms){
|
||||
for (const client of this.#sessions.values()) {
|
||||
if (now >= client.last_seen + client.session_opts.session_timeout_ttl_ms) {
|
||||
await client.Send(ClientMessageKind.TIMEOUT, {});
|
||||
client.CloseConnection();
|
||||
this.HandleInfo('Session timed out.', client.id);
|
||||
|
|
@ -975,7 +981,7 @@ export class SocioServer extends LogHandler {
|
|||
// stop deletion of old session for a moment
|
||||
// copy old sesh info to new sesh, cuz thats the new TCP connection
|
||||
// destroy old sesh for good
|
||||
ReconnectClientSession(new_session: SocioSession, old_session: SocioSession, client_notify_msg_id?:id){
|
||||
ReconnectClientSession(new_session: SocioSession, old_session: SocioSession, client_notify_msg_id?: id) {
|
||||
const new_id = new_session.id, old_id = old_session.id;
|
||||
old_session.Restore();//stop the old session deletion, since a reconnect was actually attempted
|
||||
new_session.CopySessionFrom(old_session);
|
||||
|
|
@ -991,17 +997,17 @@ export class SocioServer extends LogHandler {
|
|||
}, this.session_defaults.session_delete_delay_ms as number);
|
||||
|
||||
//notify the client
|
||||
const data = { result: { success: 1 }, old_client_id: old_id, auth: new_session.authenticated, name:new_session.name };
|
||||
const data = { result: { success: 1 }, old_client_id: old_id, auth: new_session.authenticated, name: new_session.name };
|
||||
if (client_notify_msg_id) data['id'] = client_notify_msg_id;
|
||||
new_session.Send(ClientMessageKind.RECON, data as C_RECON_Data);
|
||||
}
|
||||
|
||||
GetSessionsInfo(){
|
||||
return Object.fromEntries([...this.#sessions.values()].map(s => [s.id, { name: s.name, ip: s.ipAddr}]));
|
||||
GetSessionsInfo() {
|
||||
return Object.fromEntries([...this.#sessions.values()].map(s => [s.id, { name: s.name, ip: s.ipAddr }]));
|
||||
}
|
||||
|
||||
get prop_ids(){return this.#props.keys();}
|
||||
get session_ids(){return this.#sessions.keys();}
|
||||
get prop_ids() { return this.#props.keys(); }
|
||||
get session_ids() { return this.#sessions.keys(); }
|
||||
get server_info() { return this.#wss.address(); }
|
||||
get raw_websocket_server() { return this.#wss; }
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { LogHandler, E, log, info, done } from './logging.js';
|
||||
import { RateLimiter } from './ratelimit.js';
|
||||
import { yaml_stringify, FastHash, ClientMessageKind } from './utils.js';
|
||||
import { socio_encode, FastHash, ClientMessageKind } from './utils.js';
|
||||
|
||||
//types
|
||||
import type { WebSocket } from 'ws'; //https://github.com/websockets/ws https://github.com/websockets/ws/blob/master/doc/ws.md
|
||||
|
|
@ -61,7 +61,7 @@ export class SocioSession extends LogHandler {
|
|||
// the setImmediate trick to turn a sync task into an async task, since ws.send() is sync for some reason. If you dont await Send(), it is actually a bit faster this way
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
const payload = yaml_stringify(Object.assign({}, { kind: kind, data: data[0] }, ...data.slice(1)));
|
||||
const payload = socio_encode(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 {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,14 @@ export function SaveFilesToDiskPath(string_array_path: string[], files: SocioFil
|
|||
return new Promise((res, rej) => {
|
||||
try {
|
||||
if (!string_array_path || !files) return rej({ result: 0, error: 'SaveFilesToDiskPath: Function arguments are falsy. [#SaveFilesToDiskPath-falsy-args]' });
|
||||
for (const [filename, file_data] of files.entries()) {
|
||||
// Handle both Map (original) and plain object (MessagePack converts Maps to objects)
|
||||
const entries = files instanceof Map ? files.entries() : Object.entries(files);
|
||||
for (const [filename, file_data] of entries) {
|
||||
const file_path = os_path.join(...string_array_path, filename);
|
||||
const bin = pako.inflate(Buffer.from(file_data.bin, 'base64').buffer as ArrayBuffer); //file_data.bin should be a base64 encoded string, so make a buffer from it and decompress with pako
|
||||
console.log('DEBUG file_data.bin:', typeof file_data.bin, file_data.bin instanceof Uint8Array, file_data.bin instanceof Buffer, Array.isArray(file_data.bin));
|
||||
const bin = pako.inflate(file_data.bin); // Decompress binary data
|
||||
?pako.inflate(Buffer.from(file_data.bin, 'base64').buffer as ArrayBuffer) // Legacy Base64 format
|
||||
: pako.inflate(file_data.bin); // MessagePack sends raw compressed binary (Uint8Array)
|
||||
fs.writeFileSync(file_path, bin, { flag: 'w' });
|
||||
}
|
||||
res({ result: 1 });
|
||||
|
|
@ -29,7 +34,7 @@ export function ReadFilesFromDisk(file_paths: string[]): Promise<FS_Util_Respons
|
|||
for (const path of file_paths) {
|
||||
const filename = os_path.basename(path);
|
||||
const file = fs.readFileSync(path);
|
||||
files.set(filename, { meta: { size: file.byteLength }, bin: Buffer.from(pako.deflate(file.buffer as ArrayBuffer)).toString('base64')}); //compress the file binary and conver to base64 string
|
||||
files.set(filename, { meta: { size: file.byteLength }, bin: pako.deflate(file.buffer as ArrayBuffer) }); // MessagePack handles binary natively
|
||||
}
|
||||
res({ result: 1, files });
|
||||
} catch (e: any) { rej({ result: 0, error: e?.message || String(e) }); }
|
||||
|
|
|
|||
28
core/package-lock.json
generated
28
core/package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
|||
"version": "1.14.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"magic-string": "^0.30.5",
|
||||
"pako": "^2.1.0",
|
||||
|
|
@ -30,6 +31,15 @@
|
|||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz",
|
||||
"integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
|
|
@ -79,9 +89,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
|
@ -154,6 +165,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"@msgpack/msgpack": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz",
|
||||
"integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="
|
||||
},
|
||||
"@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
|
|
@ -199,9 +215,9 @@
|
|||
}
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/Rolands-Laucis/Socio#readme",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"magic-string": "^0.30.5",
|
||||
"pako": "^2.1.0",
|
||||
|
|
|
|||
12
core/types.d.ts
vendored
12
core/types.d.ts
vendored
|
|
@ -14,11 +14,11 @@ type Base64String = string;
|
|||
//props
|
||||
type PropKey = string;
|
||||
type PropValue = any;
|
||||
type PropAssigner = (key: PropKey, new_val:PropValue, sender_client?:SocioSession) => boolean;
|
||||
type PropOpts = { client_writable?: boolean, send_as_diff?: boolean, emit_to_sender?: boolean, observationaly_temporary?:boolean };
|
||||
type PropAssigner = (key: PropKey, new_val: PropValue, sender_client?: SocioSession) => boolean;
|
||||
type PropOpts = { client_writable?: boolean, send_as_diff?: boolean, emit_to_sender?: boolean, observationaly_temporary?: boolean };
|
||||
|
||||
//misc
|
||||
type SocioFiles = Map<string, { meta: { size: number, lastModified?: number, type?: string }, bin: Base64String }>; //bin is a base64 string of the bytes of the raw file
|
||||
type SocioFiles = Map<string, { meta: { size: number, lastModified?: number, type?: string }, bin: Uint8Array | Base64String }> | { [filename: string]: { meta: { size: number, lastModified?: number, type?: string }, bin: Uint8Array | Base64String } }; //bin is either raw compressed binary (MessagePack) or base64 string (legacy), files can be Map or object
|
||||
type QueryMarker = 'socio' | 'auth' | 'perm';
|
||||
type FS_Util_Response = { result: Bit, error?: string | Error | E | object | any, files?: SocioFiles }
|
||||
type LoggingOpts = { logging?: LoggerOptions };
|
||||
|
|
@ -91,7 +91,7 @@ type S_RECON_USE_data = data_base & { type: 'USE', token: string };
|
|||
type S_UP_FILES_data = data_base & { files: SocioFiles, data?: object };
|
||||
type S_GET_FILES_data = data_base & { data: any };
|
||||
type S_SERV_data = data_base & { data?: any };
|
||||
type S_RPC_data = data_base & { target_client: ClientID | string | null, origin_client: ClientID | string, f_name:string, args: any[] };
|
||||
type S_RPC_data = data_base & { target_client: ClientID | string | null, origin_client: ClientID | string, f_name: string, args: any[] };
|
||||
type ServerMessageDataObj = data_base | S_SERV_data | S_GET_FILES_data | S_UP_FILES_data | S_RECON_USE_data | S_RECON_GET_data | S_PROP_REG_data | S_PROP_SET_data | S_PROP_GET_data | S_PROP_UNSUB_data | S_GET_PERM_data | S_PROP_SUB_data | S_SUB_data | S_UNSUB_data | S_SQL_data | S_AUTH_data;
|
||||
|
||||
// client receive data in Message from server
|
||||
|
|
@ -100,8 +100,8 @@ type C_RES_data = data_base & data_result_block;
|
|||
type C_UPD_data = data_base & data_result_block;
|
||||
type C_AUTH_data = data_base & data_result_block;
|
||||
type C_GET_PERM_data = data_base & data_result_block & { verb: string, table: string };
|
||||
type C_PROP_UPD_data = data_base & { prop: string } & ({ prop_val?: PropValue , prop_val_diff?: diff_lib.rdiffResult[] });
|
||||
type C_RECON_Data = data_base & data_result_block & { old_client_id: ClientID, auth: boolean, name?:string };
|
||||
type C_PROP_UPD_data = data_base & { prop: string } & ({ prop_val?: PropValue, prop_val_diff?: diff_lib.rdiffResult[] });
|
||||
type C_RECON_Data = data_base & data_result_block & { old_client_id: ClientID, auth: boolean, name?: string };
|
||||
type C_RECV_FILES_Data = data_base & data_result_block & { files: SocioFiles };
|
||||
// type C_PROP_REG_data = data_base & data_result_block & { prop?: string, initial_value: any, opts: Omit<PropOpts, "observationaly_temporary"> };
|
||||
type ClientMessageDataObj = data_base | CON_data | RES_data | AUTH_data | PROP_UPD_data | RECON_Data | RECV_FILES_Data;
|
||||
|
|
@ -5,23 +5,23 @@ import type { SocioStringObj } from "./sql-parsing.js";
|
|||
|
||||
// these are not types, but compile to js dicts, so cannot be imported as types, but also cannot be declared in types.d.ts, bcs that doesnt produce a js file
|
||||
export enum ServerMessageKind {
|
||||
SUB,
|
||||
UNSUB,
|
||||
SQL,
|
||||
PING,
|
||||
AUTH,
|
||||
GET_PERM,
|
||||
PROP_SUB,
|
||||
PROP_UNSUB,
|
||||
PROP_GET,
|
||||
PROP_SET,
|
||||
PROP_REG,
|
||||
SERV,
|
||||
ADMIN,
|
||||
RECON,
|
||||
UP_FILES,
|
||||
GET_FILES,
|
||||
IDENTIFY,
|
||||
SUB,
|
||||
UNSUB,
|
||||
SQL,
|
||||
PING,
|
||||
AUTH,
|
||||
GET_PERM,
|
||||
PROP_SUB,
|
||||
PROP_UNSUB,
|
||||
PROP_GET,
|
||||
PROP_SET,
|
||||
PROP_REG,
|
||||
SERV,
|
||||
ADMIN,
|
||||
RECON,
|
||||
UP_FILES,
|
||||
GET_FILES,
|
||||
IDENTIFY,
|
||||
DISCOVERY,
|
||||
RPC,
|
||||
OK,
|
||||
|
|
@ -62,7 +62,7 @@ export function initLifecycleHooks<T extends Record<string, unknown>>(): T {
|
|||
export function sleep(seconds: number = 2) {
|
||||
return new Promise(res => setTimeout(res, seconds * 1000))
|
||||
}
|
||||
export function clamp(x:number, min:number, max:number){
|
||||
export function clamp(x: number, min: number, max: number) {
|
||||
return Math.min(Math.max(x, min), max);
|
||||
}
|
||||
|
||||
|
|
@ -101,50 +101,13 @@ export const perMessageDeflate = {
|
|||
// should not be compressed if context takeover is disabled.
|
||||
}
|
||||
|
||||
//JSON utils for Maps ------------- Credit: STEVE SEWELL https://www.builder.io/blog/maps
|
||||
// export function MapReplacer(key: string, value: any) {
|
||||
// if (value instanceof Map) {
|
||||
// return { __type: 'Map', value: Object.fromEntries(value) }
|
||||
// }
|
||||
// if (value instanceof Set) {
|
||||
// return { __type: 'Set', value: Array.from(value) }
|
||||
// }
|
||||
// return value
|
||||
// }
|
||||
// export function MapReviver(key: string, value: any) {
|
||||
// if (value?.__type === 'Set') {
|
||||
// return new Set(value.value)
|
||||
// }
|
||||
// if (value?.__type === 'Map') {
|
||||
// return new Map(Object.entries(value.value))
|
||||
// }
|
||||
// return value
|
||||
// }
|
||||
|
||||
// YAML utils for Maps and Sets ------------- https://www.npmjs.com/package/js-yaml
|
||||
import yaml from 'js-yaml';
|
||||
const mapType = new yaml.Type('!map', {
|
||||
kind: 'mapping',
|
||||
construct: (data) => new Map(Object.entries(data)),
|
||||
instanceOf: Map,
|
||||
// @ts-expect-error
|
||||
represent: (map:Map<string, any>) => Object.fromEntries(map.entries())
|
||||
});
|
||||
const setType = new yaml.Type('!set', {
|
||||
kind: 'sequence',
|
||||
construct: data => new Set(data),
|
||||
instanceOf: Set,
|
||||
// @ts-expect-error
|
||||
represent: (set:Set<any>) => Array.from(set)
|
||||
});
|
||||
export const schema = yaml.DEFAULT_SCHEMA.extend([mapType, setType]);
|
||||
// export const yaml_dump_opts = { schema, indent: 1, noArrayIndent: true };
|
||||
export function yaml_stringify(o:any){return yaml.dump(o, { schema, indent: 1, noArrayIndent: true });}
|
||||
export function yaml_parse(str: string) {return yaml.load(str, { schema }) as any;}
|
||||
import { encode, decode } from "@msgpack/msgpack";
|
||||
export function socio_encode(o: any) { return encode(o); }
|
||||
export function socio_decode(buffer: Uint8Array) { return decode(buffer) as any; }
|
||||
|
||||
// Credit: https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781 (modified)
|
||||
// super simple, naive, yet fast way to generate a hash for a subscription query. Used to keep a cache while in the core Update function.
|
||||
export function FastHash(str:string) {
|
||||
export function FastHash(str: string) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue