3
\$\begingroup\$

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 listener in priv (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.
  • worker is 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.

New contributor
Abet Giron is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
7
  • \$\begingroup\$ Been a little while since I've done JS and am unfamiliar with the # 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 _private idiom, as meta programing can be rather 'fun'. Interesting question, hope you get a good review. \$\endgroup\$ Commented yesterday
  • \$\begingroup\$ What do the worker.fetch(…), worker.listen(…), worker.postevent(…) and worker.query(…) methods do? What do they return, when/how do they call their callbacks? \$\endgroup\$ Commented 7 hours ago
  • \$\begingroup\$ Is there a specific reason you are subclassing EventTarget? Do you want users of this code to fire and listen to arbitrary events on your objects? Would you accept them firing load and update events? How are the onload, get loaded, onupdated, offupdated, update methods meant to be used? \$\endgroup\$ Commented 6 hours ago
  • \$\begingroup\$ Is there a significance to the $ sigil? Are the objects that have this property supposed to have a public priv.data object? \$\endgroup\$ Commented 6 hours ago
  • \$\begingroup\$ Is this your source code or the formatted version of the minified production code? \$\endgroup\$ Commented 6 hours ago

1 Answer 1

0
\$\begingroup\$

I've gone down this, for lack of a better description, rabbit hole - for example using WeakMap to encapsulate the class definition hierarchy itself, utilizing accessors which verify membership, and thereby protected status. The design can get quite involved and the benefits are limited. However, using what's already provided in the way ES private members are handled, there is a trick to doing this in a much simpler way, but it's also somewhat limited in what you can do with it.

A minimal example looks like this:

class Parent {
  #protectedParent = 42
  static Child = class Child extends Parent {
    static whatsNotMine ( instance ) {
      // ha! - also works from instance methods, but NOT
      // by using `this.#..` syntax.. because it isn't on `this`
      console.log ( instance.#protectedParent )
      // NOPE!
      // console.log ( this.#protectedParent )
    }
    #privateLocal = 'no peeking'
    get whatsMine () { return this.#privateLocal }
  }
  get whatsOurs () { return this.#protectedParent }
}

Parent.Child.whatsNotMine ( new Parent.Child () ) // works
Parent.Child.whatsNotMine ( new Parent () ) // works

Caveats

  • does not go in the other direction, i.e. the parent cannot access the child's private properties
  • requires the subclass to be an inner class, i.e. defined within the parent class scope
  • the protected member is not a this property, but a "that" property, however reference.#protected syntax is allowed because it is invoked from a method within the outer class scope
  • deep hierarchies are achievable, but probably would benefit from using a build tool, i.e. no separate code files otherwise and so on

Benefits

  • much cleaner code, easier to understand
  • actually makes intuitive sense and follows the expected behavior of protected members with little additional effort

Edit

See also "private stamping".

\$\endgroup\$
1
  • \$\begingroup\$ My ultimate goal is to have a protected data which, in OOP's sense, is a protected member. But I think what I've achieved here goes beyond that concept because the data shared between the base_store class (or whatever experts call it) and its child class is something only them two knows and understands. Even objects created based on the child class will never know about it. And that is (beyond) my goal. \$\endgroup\$ Commented 6 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.