prop proxy feat pub

This commit is contained in:
Rolands 2024-11-06 18:49:20 +01:00
parent 17b9d45a3c
commit 964e0a7a24
6 changed files with 80 additions and 40 deletions

View file

@ -441,6 +441,32 @@ Now clients can connect to the socio server with a url as usual, but the url wou
### Server Props
A shared JSON serializable value/object/state on the server that is live synced to subscribed clients and is modifyable by clients and the server.
The simplest use (since v1.10.0):
```ts
//server code
import { SocioServer } from 'socio/dist/core';
const socserv = new SocioServer(...)
// create with the unique name "my_obj"
socserv.RegisterProp('my_obj', {num:0}, {emit_to_sender:false});
//more technical stuff below, but here "emit_to_sender" prevents infinite loops in the network.
```
Then in the browser any client can get this object, which will always be completely synced for all clients:
```ts
//browser code
const sc = new SocioClient(...);
await sc.ready();
const my_obj = await sc.Prop('my_obj'); //get it by the unique name. Under the hood, this returns a js Proxy https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
// use like a regular js obj, but its value is always synced (magic!):
if(my_obj?.num === 0) my_obj.num += 1;
my_obj.num--;
my_obj['num'] = 0;
// etc.
```
More fine-grained control version:
```ts
//server code
import { SocioServer } from 'socio/dist/core'
@ -459,7 +485,7 @@ socserv.RegisterProp('color', '#ffffff', {
//success, so assign
return socserv.SetPropVal('color', new_val); //assign the prop. Returns truthy, if was set succesfully
}, //default SocioServer.SetPropVal
}, //default is SocioServer.SetPropVal()
client_writable:true, //clients can change this value. Default true
send_as_diff:false, //send only the differences in the prop values. Overrules the diff global flag. Default false.
emit_to_sender:false, //emit an update to the original client, that set a prop val and caused the update to happen, if the client is subbed to this prop. Default false.
@ -482,7 +508,7 @@ col = await sc.GetProp('color', true); //last arg local=true
Though usable for realtime web chat applications, i advise against that. There is a socio/chat.ts file that handles such a usecase in a more generic and extendable way.
To be more network efficient, Socio can be set to use the [recursive-diff](https://www.npmjs.com/package/recursive-diff) lib for props. This is a good idea when your prop is a large or deeply nested JS object and only small parts of its structure get updated. Only differeneces in this object will be sent through the network on PROP_UPD msgs. Keep in mind, that if one of these msgs gets lost for a client, then its frontend prop will go out of sync unnoticeably and irreparably. The setup is a flag on the SocioServer constructor options:
To be more network efficient, Socio can be set to use the [recursive-diff](https://www.npmjs.com/package/recursive-diff) lib for props. This is a good idea when your prop is a large or deeply nested JS object and only small parts of its structure get updated. Only differeneces in this object will be sent through the network on PROP_UPD msgs. Keep in mind, that if one of these msgs gets lost for a client, then its frontend prop will go out of sync unnoticeably and irreparably, though i've not seen this happen with the WS protocol. The setup is a flag on the SocioServer constructor options:
```ts
//server code

View file

@ -257,7 +257,7 @@ export class SocioClient extends LogHandler {
// notify dev and crash with 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, 'files received: ' + file_count);
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 });
}
@ -447,7 +447,7 @@ export class SocioClient extends LogHandler {
const { id, prom } = this.CreateQueryPromise();
this.Send(CoreMessageKind.PROP_GET, { id, prop: prop_name } as S_PROP_GET_data);
this.#UpdateQueryPromisePayloadSize(id);
return prom as Promise<data_base & data_result_block>;
return prom as Promise<any>;
}
}
SubscribeProp(prop_name: PropKey, onUpdate: PropUpdateCallback, { rate_limit = null, receive_initial_update = true }: { rate_limit?: RateLimit | null, receive_initial_update?: boolean } = {}): Promise<{ id: id, result: { success: Bit } } | any> {
@ -496,32 +496,43 @@ export class SocioClient extends LogHandler {
return (prom as unknown) as Promise<{ prop: PropKey }>;
} catch (e: err) { this.HandleError(e); return null; }
}
// async Prop(prop_name: PropKey){
// const client_this = this; //use for inside the Proxy scope
// const prop_proxy = new Proxy(await this.GetProp(prop_name), {
// get(p: PropValue, property) {
// return p[property];
// // const res = await client_this.GetProp.bind(client_this)(prop_name);
// // return res?.result?.success === 1 ? res?.result?.res[property] : p[property];
// },
// //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) {
// //the sub will run this too, which means the value would've already been set
// if (p[property] !== new_val){
// p[property] = new_val;
// client_this.SetProp.bind(client_this)(prop_name, p);
// }
// 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){
const client_this = this; //use for inside the Proxy scope
const prop = await this.GetProp(prop_name, false);
if(prop === undefined){
this.HandleError(new E(`Couldnt retrieve server prop [${prop_name}]`, { prop_name, prop }));
return undefined;
}
if (typeof prop !== 'object') {
this.HandleError(new E(`Can only proxy js objects, but [${prop_name}] is not an object.`, { prop_name, prop }));
return undefined;
}
const prop_proxy = new Proxy(prop, {
get(p: PropValue, property) {
// log(`${prop_name} GET`, {p, property});
return p[property];
},
//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 });
//the sub will run this too, which means the value would've already been set
if (p[property] !== new_val){
p[property] = new_val;
client_this.SetProp.bind(client_this)(prop_name, p);
}
// return true;
// // return (await client_this.SetProp.bind(client_this)(prop_name, p))?.result?.success === 1;
// }
// });
return true;
// return (await client_this.SetProp.bind(client_this)(prop_name, p))?.result?.success === 1;
}
});
// this.SubscribeProp(prop_name, (new_val) => {
// for(const [key, val] of Object.entries(new_val)) prop_proxy[key] = val; //set each key, bcs cant just assign the new obj, bcs then it wouldnt be a proxy anymore
// });
// return prop_proxy;
// }
await client_this.SubscribeProp(prop_name, (new_val) => {
// log(`${prop_name} SUB UPD`, { new_val });
for (const [key, val] of Object.entries(new_val)) prop_proxy[key] = val; //set each key, bcs cant just assign the new obj, bcs then it wouldnt be a proxy anymore
});
return prop_proxy;
}
// socio query marker related --auth and --perm:
@ -551,9 +562,11 @@ export class SocioClient extends LogHandler {
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 (data.result.success === 1){
(q as QueryPromise).res(data?.result?.res as any);
else this.#HandleServerError(data.result.error);
// 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){
@ -565,7 +578,7 @@ export class SocioClient extends LogHandler {
if ((q as QueryObject).onUpdate?.error)
//@ts-expect-error
(q as QueryObject).onUpdate.error(data.result.error);
else this.#HandleServerError(``, data.result.error);
else this.#HandleServerError(``, data.result?.error as string);
}
this.#queries.delete(data.id); //clear memory
@ -623,7 +636,7 @@ export class SocioClient extends LogHandler {
else{
const error = 'Failed to reconnect'
this.HandleError(new E(error, data));
this.#HandleServerError(error, data.result.error);
this.#HandleServerError(error, data.result?.error as string);
}
}

View file

@ -24,7 +24,7 @@ import type { data_base, S_SUB_data, ServerMessageDataObj, S_UNSUB_data, S_SQL_d
// client data msg
import type { C_RES_data, C_CON_data, C_UPD_data, C_AUTH_data, C_GET_PERM_data, C_PROP_UPD_data, C_RECON_Data, C_RECV_FILES_Data } from './types.d.ts'; //types over network for the data object
import type { id, PropKey, PropValue, PropAssigner, PropOpts, ClientID, FS_Util_Response, ServerLifecycleHooks, LoggingOpts, Bit, SessionOpts } from './types.d.ts';
import type { id, PropKey, PropValue, PropAssigner, PropOpts, ClientID, FS_Util_Response, ServerLifecycleHooks, LoggingOpts, Bit, SessionOpts, data_result_block } from './types.d.ts';
import { ClientMessageKind } from './core-client.js';
import type { RateLimit } from './ratelimit.js';
import type { SocioStringObj } from './sql-parsing.js';
@ -384,10 +384,11 @@ export class SocioServer extends LogHandler {
}
case CoreMessageKind.PROP_GET: {
this.#CheckPropExists((data as S_PROP_GET_data)?.prop, client, data.id as id, `Prop key [${(data as S_PROP_GET_data)?.prop}] does not exist on the backend! [#prop-reg-not-found-get]`);
const prop_val = this.GetPropVal((data as S_PROP_GET_data)?.prop);
client.Send(ClientMessageKind.RES, {
id: data.id,
result: this.GetPropVal((data as S_PROP_GET_data).prop)
});
result: { success: prop_val !== undefined ? 1 : 0, res: prop_val, error: prop_val === undefined ? 'Server couldnt find prop' : ''}
} as data_result_block);
break;
}
case CoreMessageKind.PROP_SET: {
@ -395,7 +396,7 @@ export class SocioServer extends LogHandler {
if (this.#props.get((data as S_PROP_SET_data).prop as string)?.client_writable) {
//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 } }); //resolve this request to true, so the client knows everything went fine.
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;

View file

@ -1,12 +1,12 @@
{
"name": "socio",
"version": "1.9.0",
"version": "1.10.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "socio",
"version": "1.9.0",
"version": "1.10.0",
"license": "MIT",
"dependencies": {
"js-yaml": "^4.1.0",

View file

@ -1,6 +1,6 @@
{
"name": "socio",
"version": "1.9.0",
"version": "1.10.0",
"description": "A WebSocket Real-Time Communication (RTC) API framework.",
"main": "./dist/core.js",
"type": "module",

2
core/types.d.ts vendored
View file

@ -64,7 +64,7 @@ export type Server_Error_ClientHook = (client:SocioClient, error_msgs:string[])
// over network data types
export type data_base = { id: id };
export type data_result_block = { result: { success: BIT, res?: any, error: string } };
export type data_result_block = { result: { success: BIT, res?: any, error?: string } };
// server receive data in Message from client
export type S_SUB_data = data_base & ClientSubscribeOpts & { rate_limit: RateLimit | null };