Last active
April 8, 2022 13:11
-
-
Save jaandrle/641685884ac3f9cad1e030c049451ca2 to your computer and use it in GitHub Desktop.
Just another version of PubSub
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* jshint esversion: 6,-W097, -W040, browser: true, expr: true, undef: true, devel: true */ | |
/** | |
* Event info shared across multiple `publish` calls (e. g. topic name). | |
* | |
* @typedef pubsub_TopicInfo | |
* @property {string} [name] Event name/identification. | |
* */ | |
/** | |
* Topic(s) options and topic **refence** to be used in subscribe/publish/… functions. | |
* | |
* @typedef pubsub_Topic | |
* @type {object} | |
* @property {pubsub_TopicInfo} [info] Event info shared across multiple `publish` calls (e. g. topic name). | |
* @property {boolean} [cached=false] Keep last published data and when new linstener is registered call this function with keeped data. | |
* @property {function} [mapper] Converts topic `data` from `publish` function to what listeners are expecting. (recommendation to use DATA_IN, DATA – see {@link topic}) | |
* @property {boolean} [once=false] Topic can be published only one time. | |
* @property {any} [DATA_IN] Use **only in documentation** to describe publish/subscribe data | |
* @property {any} DATA Use **only in documentation** to describe publish/subscribe data | |
* */ | |
/** | |
* @typedef pubsub_Event | |
* @type {object} | |
* @property {pubsub_TopicInfo} info Event info shared across multiple `publish` calls (e. g. topic name). | |
* @property {any} data Event data, see {@link pubsub_Topic.DATA}. | |
* */ | |
/** | |
* Function listening given topic(s). | |
* | |
* @callback pubsub_Listener | |
* @param {pubsub_Event} event | |
* */ | |
const pubsub= (function(){ | |
// #region internal vars | |
/** | |
* @type {WeakMap<pubsub_Topic,Set<pubsub_Listener>>} | |
* */ | |
const listeners= new WeakMap(); | |
/** | |
* @type {WeakMap<pubsub_Topic,Set>} | |
* */ | |
const data_cached= new WeakMap(); | |
// #endregion internal vars | |
return { topic, subscribe, unsubscribe, publish, publish_ }; | |
// #region public functions | |
/** | |
* Publishs `data` for given `topic`. | |
* | |
* @param {pubsub_Topic} topic | |
* @param {any} data It is recommended to use `topic.DATA` in Typescript/JSDoc to provide info about data type (see {@link topic}). | |
* @throws {TypeError} Unregistered `topic` | |
* */ | |
function publish(topic, data){ | |
if(testTopic(topic)) return; | |
data= toEvent(topic, data); | |
listeners.get(topic).forEach(f=> f(data)); | |
if(topic.cached) data_cached.set(topic, data); | |
if(topic.once) listeners.delete(topic); | |
} | |
/** | |
* Publishs `data` for given `topic` and waits for respones (async functions/`(…)=> Promise`). | |
* | |
* @param {pubsub_Topic} topic | |
* @param {any} data It is recommended to use `topic.DATA` in Typescript/JSDoc to provide info about data type (see {@link topic}). | |
* @returns {Promise<0|1>} OK|once | |
* */ | |
function publish_(topic, data){ | |
try{ if(testTopic(topic)) return Promise.resolve(1); } | |
catch (e){ Promise.reject(e); } | |
data= toEvent(topic, data); | |
let promises= []; | |
listeners.get(topic).forEach(function(f){ | |
const p= f(data); | |
if(p instanceof Promise) promises.push(p); | |
}); | |
return Promise.all(promises).then(function(){ | |
if(topic.cached) data_cached.set(topic, data); | |
if(topic.once) listeners.delete(topic); | |
return 0; | |
}); | |
} | |
/** | |
* Register `listener` function to be called when `topic` will be emmited. | |
* | |
* @param {pubsub_Topic} topic | |
* @param {pubsub_Listener} listener | |
* @throws {TypeError} Unregistered `topic` | |
* */ | |
function subscribe(topic, listener){ | |
if(topic.cached) listener(data_cached.get(topic)); | |
if(testTopic(topic)) return; | |
listeners.get(topic).add(listener); | |
} | |
/** | |
* Remove `listener` from `topic`. | |
* | |
* @param {pubsub_Topic} topic | |
* @param {pubsub_Listener} listener | |
* */ | |
function unsubscribe(topic, listener){ | |
return listeners.has(topic) ? listeners.get(topic).delete(listener) : undefined; | |
} | |
/** | |
* Creates topic to be used in subscribe/publish/… functions. | |
* | |
* It is recommended to add JSDoc/Typescript property **DATA** to specify | |
* expected data format/type (see {@link publish}/{@link pubsub_Listener}) | |
* – see *example*. | |
* | |
* @template {pubsub_Topic} T | |
* @param {T} options See {@link pubsub_Topic} | |
* @returns {T} | |
* @example | |
* /** | |
* * Test | |
* * @property {string} DATA | |
* *\/ | |
* const ontest= pubsub.topic({ cached: true }); | |
* */ | |
function topic(options= {}){ | |
listeners.set(options, new Set()); | |
return options; | |
} | |
// #endregion public functions | |
// #region private functions | |
/** | |
* @param {pubsub_Topic} topic | |
* @param {any} data | |
* @returns {pubsub_Event} | |
* */ | |
function toEvent({ info= {}, mapper }, data){ | |
if(mapper) data= mapper(data); | |
return { info, data }; | |
} | |
/** | |
* @param {pubsub_Topic} topic | |
* @throws {TypeError} non-topic | |
* @returns {0|1} OK|once | |
* */ | |
function testTopic(topic){ | |
if(listeners.has(topic)) return 0; | |
if(topic.once) return 1; | |
const topic_str= JSON.stringify(topic); | |
throw new TypeError(`Given topic '${topic_str}' is not supported (see pubsub_TopicOptions). Topic are created via 'topic' function.`); | |
} | |
// #endregion private functions | |
})(); | |
/** | |
* Test | |
* @property {string} DATA Test data | |
* */ | |
const ontest= pubsub.topic({ once: true }); | |
pubsub.subscribe(ontest, function({ info, data }){ | |
console.log({ info, data }); | |
}); | |
pubsub.publish(ontest, "Ahoj"); | |
// vim: set tabstop=4 shiftwidth=4 textwidth=250 expandtab : | |
// vim>60: set foldmethod=marker foldmarker=#region,#endregion : |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* jshint esversion: 6,-W097, -W040, browser: true, expr: true, undef: true */ | |
/* global console */ | |
var _class=(function registrator(_){return _()})(function moduleBody(){let api=Object.create(null);api.isClass=function(c){return!!(checkClassProto(c)&&!c._.instance_id)};api.instanceOf=function(i,c){if(!(checkProto(i)&&i._.instance_id)||!checkClassProto(c)){throw new TypeError("It schould be 'i:instance, c:class'!")}return i._.class_id===c._.class_id};api.isEqual=function(a,b){if(!checkClassProto(a)||!checkClassProto(b)){throw new TypeError("Arguments schould be instance or class!")}if((a._.instance_id&&!b._.instance_id)||(!a._.instance_id&&b._.instance_id)){throw new TypeError("Arguments schould have the same type!")}if(a._.instance_id&&b._.instance_id){return a._.instance_id===b._.instance_id}else{return a._.class_id===b._.class_id}};api.getID=function(ic){if(!checkClassProto(ic)){throw new TypeError("Argument 'ic' schould be instance or class!")}const{class_id,instance_id}=ic._;return instance_id||class_id};api.messageString=function(ic,message){if(!checkClassProto(ic)){throw new TypeError("Argument 'ic' schould be instance or class!")}const{class_id,instance_id}=ic._;return `${ message }${line()} ${instance_id_text()}class: '${ class_id }'`;function line(){return "\n"}function instance_id_text(){return typeof instance_id==="undefined"?" ":"instance: '"+instance_id+"'\n of "}};function declaration(_class_pattern){if(typeof _class_pattern!=="function"){throw new Error("Wrong _class declaration!")}const class_id=setID();const instanceProto=()=>Object.freeze(Object.assign(Object.create(null),{class_id,instance_id:setID()}));let _static=Object.create(null);_class_pattern({_constructor,_static,_depends});_static._=Object.freeze(Object.assign(Object.create(null),{class_id}));_static[Symbol.hasInstance]=t=>api.instanceOf(t,_static);return Object.freeze(_static);function _constructor(_constructor_pattern){if(Reflect.has(_static,"create")&&!Reflect.isExtensible(_static,"create")){throw new Error("Factory constructor 'create' already exists!")}_static.create=function(def={}){let _this=Object.create(null);_constructor_pattern(_this,def);_this._=instanceProto();return Object.freeze(_this)}}function _depends(_class){if(_static._||_static.create){throw new Error("Command '_depends' should be used on the top of class declaration")}return api.isClass(_class)&&_class}}Reflect.setPrototypeOf(declaration,Object.freeze(api));return declaration;function getFileAndLine(){const _err_arr=(new Error()).stack.split("\n");let ret=_err_arr.length-1;const _err=(_err_arr.filter(errorFilter).pop()||_err_arr.shift()).replace(/\\/g,"/");return _err.substr(_err.lastIndexOf("/")+1);function errorFilter(l,i){if(l.substr(0,11)==="declaration"){ret=i+1}return ret===i}}function getDateAndTime(){const[,_m,_d,_y,_t]=(new Date()).toString().split(" ");return[_d,_m,_y,_t].join("-")}function setID(){return getDateAndTime()+"/"+getFileAndLine()}function checkProto(candidate){return typeof candidate==="object"&&candidate._}function checkClassProto(candidate){return checkProto(candidate)&&candidate._.class_id}}); | |
const PubSub= _class(function({ _constructor }){ | |
_constructor(function(_this){ | |
const /* store */ | |
functions= [], | |
listeners= new Map(); | |
let /* arguments for subscribers (no deep copy and all `fun` can see all keys!) */ | |
class_data= Object.create(null); | |
_this.subscribe= function(data, fun){ | |
_this.publish(data); | |
const data_keys= Object.keys(data); | |
const id= ( i=> i!==-1 ? i : functions.push(fun)-1 )( functions.indexOf(fun) ); | |
for(let i=0, i_key; (i_key= data_keys[i]); i++){ | |
if(!listeners.has(i_key)) listeners.set(i_key, new Set([ id ])); | |
else listeners.set(i_key, listeners.get(i_key).add(id)); | |
} | |
Object.assign(class_data, data); | |
}; | |
_this.publish= function(data){ | |
const data_keys_updated= Object.keys(class_data).filter(k=> data[k]!==class_data[k]); | |
const /* to call */ | |
funs_to_call= new Set(), | |
registerCall= i=>funs_to_call.add(i); | |
for(let i=0, key; (key= data_keys_updated[i]); i++){ | |
class_data[key]= data[key]; | |
if(listeners.has(key)) listeners.get(key).forEach(registerCall); | |
} | |
const funCall= i=> functions[i](class_data); | |
funs_to_call.forEach(funCall); | |
}; | |
}); | |
}); | |
const testPubSub= PubSub.create(); | |
testPubSub.subscribe({ a: "A" }, console.log);//1: `a`->`console.log` | |
testPubSub.subscribe({ a: "A" }, console.log);//2: `a` exists, `console.log` exists => ignored | |
testPubSub.subscribe({ b: "B" }, console.log);//3: `b`->`console.log`, `console.log` already exits => links to 1: | |
testPubSub.subscribe({ b: "B" });//4: `b`===`b` => nothing | |
testPubSub.publish({ a: "B", b: "A" });//5: 1x`console.log` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TODO: https://gist.github.com/jaandrle/641685884ac3f9cad1e030c049451ca2#file-pubsub-topics-js-L143