I wanted to create a protected member in a javascript (ES) class but, of course, there is no such concept. So I devised a way that would let me create a member or something that "no one could touch if it's not theirs". This code is already working and, as a matter of fact, already in production. I just need opinions/comments or suggestion on this. Here's the entire code:
define([],()=>{
class base extends EventTarget{
#loaded
#listener
#event
constructor(src,buffer,listener){
super()
worker.fetch(src).then(a=>{
delete a.id
buffer.data=a
this.#loaded=!0
this.dispatchEvent(new Event('load'))
this.#listener=listener
worker.listen(src,msg=>{
listener(msg)
this.#fire(msg)
})
})
document.addEventListener(this.#event=src+'event',this.#handler)
}
onload(cb){
if(!this.#loaded)
this.addEventListener('load',cb)
else
cb.call(this)
}
get loaded(){return this.#loaded}
onupdated(fn){this.addEventListener('updated',fn)}
offupdated(fn){this.removeEventListener('updated',fn)}
update(data,o=null){
this.#listener({data,o},this)
document.removeEventListener(this.#event,this.#handler)
worker.postevent(src,data,o)
document.addEventListener(this.#event,this.#handler)
this.#fire({data,o})
}
#handler({detail}){
this.#listener(detail)
}
#fire(dx){
this.dispatchEvent(new CustomEvent('updated',{detail:dx}))
}
}
const base_store=(src,priv)=>{
return class extends base{
constructor(){
super(src,priv,msg=>priv.listener(msg,this))
}
}
},
base_sales=(src,xpriv)=>{
const priv={
listener(msg,store){
const {o,data}=msg;
if(!o||o==='m') {
xpriv.data[data.key]=data
}
else {
xpriv.listener(msg,store)
}
},
get data(){return xpriv.data},
set data(a){xpriv.data=a}
}
return class extends base_store(src,priv){
get $(){return priv.data}
}
},
initAgents=()=>{
const priv={
listener:"agents_update",
data:null
}
return new class extends base_store('agent',priv){
get $(){return priv.data}
}
},
initBranches=()=>{
const priv={
listener:"branches_update",
data:null
}
let main,count
return new class extends base_store('branch',priv){
get $(){return priv.data}
get main(){
if(!main){
if(main===void 0) {
if(!priv.data)return null
main=Object.values(priv.data).find(a=>a.ismain)
}
}
return main
}
get count(){
if(void 0===count)
count=priv.data?Object.entries(priv.data).length:0
return count
}
}
},
initClients=()=>{
const priv={
listener:"clients_update",
data:null
}
return new class extends base_store('client',priv){
key(key,type){return ekUtils.touint(key|((type||0)<<31))}
get(key,type){return priv.data.all[this.key(key,type)]}
get all(){return priv.data.all}
get person(){return priv.data.person}
get entity(){return priv.data.entity}
}
},
initQuotes=()=>{
const priv={
listener:"quotes_update",
data:null
}
return new class extends base_sales('quote',priv){
}
},
initSales=()=>{
const priv={
listener:"sales_update",
data:null
}
return new class extends base_sales('sales',priv){
}
},
initStocks=()=>{
const priv={
listener:"stocks_update",
data:null,
serials:null
}
return new class Stock extends base_store('stock',priv){
static #aborted=Symbol('Stock.aborted')
get all(){return priv.data.all}
get branch(){return priv.data.branch}
get aborted(){return Stock.#aborted}
get serials(){return priv.serials}
get filter(){return priv.data.filter}
count=(stk,brn)=>{
return o
}
compare=(a,b)=>{
'number'===typeof a&&(a=priv.data.all[a])
'number'===typeof b&&(b=priv.data.all[b])
if(a.category===b.category)
return a.name.ekComp(b.name)
if(!a.category)
return 1
if(!b.category)
return -1
return a.category-b.category
}
find=(data,inc_sn=!0)=>{
return new Promise(async res=>{
let ok
do{
ok=!0
if(inc_sn) {
let find=await this.#find_by_sn(!0,data)
if(data.abort){
res()
return
}
if(!data.restart&&find){
find.issn=!0
res([find])
return
}
}
const what=(data.value??data).toLowerCase()
let finds=[]
for(let b in priv.data.all){
if(data.abort||data.restart)
break
let c=priv.data.all[b]
if((c.barcode&&c.barcode.includes(what))||c.name.toLowerCase().includes(what)){
finds.push(c)
}
else{
let m=c.modelStr
if(!m&&c.model&&lookup&&lookup.model){
m=lookup.model.find(a=>a.key===c.model)?.value
m&&(c.modelStr=m)
}
m&&m.toLowerCase().includes(what)&&finds.push(c)
}
}
if(data.abort){
console.log('aborted')
res()
return
}
if(data.restart){
console.log('restarted')
ok=!1
}
!data.abort&&!data.restart&&res(finds)
}while(!ok)
})
}
#find_sn(data,sns,available){
const what=(data.value??data).toLowerCase()
for(let key in sns){
if(data.abort||data.restart)
break
let sndata=sns[key]
key=parseInt(key)
if(sndata.available?.length){
let i=sndata.available.findIndex(a=>a.ekComp(what)===0)
if(i!=-1){
return {sn:sndata.available[i],stock:key,available:!0}
}
}
if(available)continue
if(sndata.outs?.length){
let i=sndata.outs.findIndex(a=>a.ekComp(what)===0)
if(i!=-1){
return {sn:sndata.outs[i],stock:key,available:!1}
}
}
}
return null
}
#find_by_sn(internal,data,branch,available){
return new Promise(res=>{
this.load_sns(sns=>{
let ok=!0
do{
ok=!0
if(branch) {
const find=this.#find_sn(data,sns[branch],{get ok(){return ok}},available)
if(data.abort){
res()
internal||(data.abort=!1)
return
}
if(find) {
find.branch=branch
res(find)
return
}
else if(data.restart){
if(internal) {
res()
return
}
ok=!1
}
}
else {
for(let brn in sns){
if(data.abort){
res()
internal||(data.abort=!1)
return
}
if(data.restart){
if(internal) {
res()
return
}
ok=!1
break
}
const find=this.#find_sn(data,sns[brn],{get ok(){return ok}},available)
if(find) {
find.branch=brn
res(find)
return
}
}
}
}while(!ok)
res(!1)
})
})
}
find_by_sn(data,branch,available){
return this.#find_by_sn(0,data,branch,available)
}
load_sns=(()=>{
const cbs=[]
return cb=>{
if(!priv.serials) {
cbs.includes(cb)||(cbs[cbs.length]=cb)
if(null===priv.serials){
priv.serials=0
worker.query('stock','serials').then(a=>{
delete a.id
priv.serials=a
while(cbs.length){
const cb=cbs.shift()
cb.call(this,priv.serials)
}
})
}
}
else
cb.call(this,priv.serials)
}
})()
}
},
initSuppliers=()=>{
const priv={
listener:"suppliers_update",
data:null
}
return new class extends base_store('supplier',priv){
get $(){return priv.data}
}
}
let stocks,sales,quotes,clients,branches,suppliers,agents
return{
get stocks(){
return stocks||=initStocks()
},
get sales(){
return sales||=initSales()
},
get quotes(){
return quotes||=initQuotes()
},
get clients(){
return clients||=initClients()
},
get branches(){
return branches||=initBranches()
},
get suppliers(){
return suppliers||=initSuppliers()
},
get agents(){
return agents||=initAgents()
}
}
})
I did not achieve that protected member thing. But I think, speaking of the goal, priv have the utmost protection it needs.
NOTES
define()here is not that typical define in requirejs although it works more or less the same way.- values of
listenerinpriv(i.e.priv.listener) are placeholders that will be replaced by the server when the script is requested because SharedWorker and
main thread uses the same logic in those parts. workeris a global constant defined elsewhere.- Other objects/methods used or called here are defined elsewhere.
- Some objects have still minimal members. Just leave it at that for now.
This is supposed to be an answer to @Berqi's question in the comments but I though I can answer it here more clearly
The worker here is a global const encapsulating SharedWorker object. Among its roles are:
- fetch data from the SharedWorker
- receive (listen to) updates from the SharedWorker and broadcast the update via dispatchEvent.
- it also encapsulates BroadcastChannel for synchronization with other tabs
- when a data is posted to the server it posts update to the SharedWorker and BroadcastChannel
The SharedWorker's tasks, among others:
- establish connection to SSE server
- broadcast push notification to main thread via ports where the worker is listening
- fetch data from the server and serve it the main thread when the worker post a fetch message
The goal of using SharedWorker is to minimize the connection to the server. The mainthread, from time to time, makes its own connection to the server where the SharedWorker is not necessary. For example fetching data, large and necessary but for very particular use.
#stuff. Here's 2c from a Python user, I typically have the meta-programming defined in a parent class (or metaclass if available). However I'm more of a fan of the_privateidiom, as meta programing can be rather 'fun'. Interesting question, hope you get a good review. \$\endgroup\$worker.fetch(…),worker.listen(…),worker.postevent(…)andworker.query(…)methods do? What do they return, when/how do they call their callbacks? \$\endgroup\$EventTarget? Do you want users of this code to fire and listen to arbitrary events on your objects? Would you accept them firingloadandupdateevents? How are theonload,get loaded,onupdated,offupdated,updatemethods meant to be used? \$\endgroup\$$sigil? Are the objects that have this property supposed to have a publicpriv.dataobject? \$\endgroup\$