mirror of
https://github.com/Rolands-Laucis/Socio.git
synced 2026-05-15 06:05:53 -06:00
prop proxy feat pub
This commit is contained in:
parent
17b9d45a3c
commit
964e0a7a24
6 changed files with 80 additions and 40 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
4
core/package-lock.json
generated
4
core/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
2
core/types.d.ts
vendored
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue