Skip to content

Instantly share code, notes, and snippets.

@jaandrle
Last active April 8, 2022 13:11
Show Gist options
  • Save jaandrle/641685884ac3f9cad1e030c049451ca2 to your computer and use it in GitHub Desktop.
Save jaandrle/641685884ac3f9cad1e030c049451ca2 to your computer and use it in GitHub Desktop.
Just another version of PubSub
/* 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 :
/* 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