Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active April 4, 2018 19:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbuckton/cc5b18a505e779dc69f7cbde2d74a7ef to your computer and use it in GitHub Desktop.
Save rbuckton/cc5b18a505e779dc69f7cbde2d74a7ef to your computer and use it in GitHub Desktop.
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
}
}
}

By combining decorators and the ref proposal, we could conceivably end up with a workable solution for "friend"-like behavior with classes:

// a.js
import { B } from "./b.js";

@friend(ref B) // give B access to all private fields of A
class A {
    #x;
}

// b.js
import { A } from "./a.js";

let backchannel;
@friend.accept(ref A, ref backchannel)
class B {
    getX(a) {
        return backchannel.get(a, "#x");
    }
}

Using ref would allow us to mostly avoid issues with circularity and TDZ (since it creates a reference to the binding and not the value). The friend.accept decorator effectively creates a link between A and B that is exposed via backchannel. The backchannel object then lazily verifies that B was granted friend access to A.

@ljharb
Copy link

ljharb commented Apr 1, 2018

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?

@rbuckton
Copy link
Author

rbuckton commented Apr 4, 2018

It can, yes, but isn't as terse and has some footguns. For example, one value of ref is that it avoids (or rather defers) TDZ since it creates a reference to the binding (even if uninitialized) rather than the value. To do this today you would need to create a closure, e.g. () => B instead of B. However, its fairly easy for someone to inadvertently write B instead of the closure, and its impossible to correctly determine whether the thing passed in is a closure for B or just B since both have a typeof of "function" (and B could have been written as an ES5-style "class").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment