|
const friendship = new WeakMap(); |
|
|
|
function friend(ref granteeClass) { |
|
return function (descriptor) { |
|
descriptor.finisher = (granterClass) => { |
|
let info = friendship.get(granterClass); |
|
if (!info) { |
|
const names = descriptor.elements |
|
.filter(element => element.name instanceof PrivateName) |
|
.reduce((map, element) => map.set(element.name.description, element.name), new Map()); |
|
info = { names, grants: new Set() }; |
|
friendship.set(granterClass, info); |
|
} |
|
info.grants.add({ granted: false, granteeClassRef: ref granteeClass }); |
|
}; |
|
return descriptor; |
|
}; |
|
} |
|
|
|
friend.accept = accept; |
|
function accept(ref granterClass, ref backchannel) { |
|
const source = new BackchannelSource(ref granterClass); |
|
backchannel = source.backchannel; |
|
return function (descriptor) { |
|
descriptor.finisher = (granteeClass) => { |
|
source.setGranteeClass(granteeClass); |
|
}; |
|
}; |
|
} |
|
|
|
class BackchannelSource { |
|
state = "uninitialized"; |
|
|
|
constructor(ref granterClass) { |
|
this.granterClassRef = ref granterClass; |
|
this.backchannel = { |
|
get: (instance, key) => this.get(instance, key), |
|
set: (instance, key, value) => this.set(instance, key, value) |
|
}; |
|
} |
|
|
|
setGranteeClass(granteeClass) { |
|
this.granteeClass = granteeClass; |
|
} |
|
|
|
get(instance, key) { |
|
return this.getPrivateName(key).get(instance); |
|
} |
|
|
|
set(instance, key, value) { |
|
this.getPrivateName(key).set(instance, value); |
|
} |
|
|
|
getPrivateName(key) { |
|
this.checkGrant(); |
|
const name = this.grantedNames.get(key); |
|
if (!name) throw new TypeError(); |
|
return name; |
|
} |
|
|
|
checkGrant() { |
|
check: if (this.state === "uninitialized") { |
|
if (this.granteeClass === undefined) { |
|
this.state = "invalid"; |
|
break check; |
|
} |
|
|
|
let granterClass; |
|
try { |
|
granterClass = this.granterClassRef.value; |
|
} |
|
catch { |
|
this.state = "invalid"; |
|
break check; |
|
} |
|
|
|
const grantInfo = friendship.get(granterClass); |
|
if (!grantInfo) { |
|
this.state = "invalid"; |
|
break check; |
|
} |
|
|
|
let hasGrant = false; |
|
for (const grant of grantInfo.grants) { |
|
try { |
|
if (grant.granteeClassRef.value === this.granteeClass) { |
|
if (grant.granted) { |
|
this.state = "invalid"; |
|
break check; |
|
} |
|
hasGrant = true; |
|
grant.granted = true; |
|
break; |
|
} |
|
} |
|
catch { |
|
// do nothing |
|
} |
|
} |
|
|
|
if (!hasGrant) { |
|
this.state = "invalid"; |
|
break check; |
|
} |
|
|
|
this.grantedNames = grantInfo.names; |
|
this.state = "valid"; |
|
} |
|
|
|
switch (this.state) { |
|
case "invalid": throw new TypeError(); |
|
// NOTE: more specific error cases to follow |
|
} |
|
} |
|
} |
Using a ref here is certainly a more ergonomic and robust approach than what's already possible, which is passing an object or array, and using mutation to communicate - or, by passing a callback that's invoked with
backchannel(which is probably simpler).To clarify; this is a cleaner way for A and B to share privileged access, provided that both A and B already know about each other, but it can already be achieved without decorators or refs?