# Solving JavaScript Proxy Onions
Thinking hard about interesting experiments in the [Solid JS Discord](https://discord.gg/solidjs), stumble upon something I didn't think before, Proxy onions. Basically two unrelated proxies wrapping each other. What happens is that we end up with stuff like `a(b(a()))`.
**Spoiler**: In summary, for when overwrapping is undesired, my discoveries are that a WeakMap is not enough, and caching proxies inside the proxy could unwrap (or skip a layer of the onion) unexpectedly.
---
To make it easy to understand, lets gradually build a proxy that support Onions, and Scoped Onions.
## Simple Proxy
A simple proxy could look like:
```js
class Handler {
get(target, key) {
return target[key];
}
}
function createProxy(value) {
return new Proxy(value, new Handler());
}
```
The following code will create two different proxies, which _most_ of the time is something we _may_ want to avoid.
```js
const value = {};
const o1 = createProxy(value);
const o2 = createProxy(value);
console.log(o1 !== o2);
```
## Proxy Cached with WeakMap
Lets use a WeakMap to add caching, so whenever we pass an object we always get the same proxy for the same object.
```js
class Handler {
get(target, key) {
return target[key];
}
}
const wm = new WeakMap();
function createProxy(value) {
if (!wm.has(value)) {
wm.set(value, new Proxy(value, new Handler()));
}
return wm.get(value);
}
```
Now the returned proxy will be the same
```js
const value = {};
const o1 = createProxy(value);
const o2 = createProxy(value);
console.log(o1 === o2);
```
## Proxy Onions
This is where it gets interesting, because lets add a different proxy.
```js
class Handler1 {
get(target, key) {
return target[key];
}
}
class Handler2 {
get(target, key) {
return target[key];
}
}
const wm1 = new WeakMap();
function createProxy1(value) {
if (!wm1.has(value)) {
wm1.set(value, new Proxy(value, new Handler1()));
}
return wm1.get(value);
}
const wm2 = new WeakMap();
function createProxy2(value) {
if (!wm2.has(value)) {
wm2.set(value, new Proxy(value, new Handler2()));
}
return wm2.get(value);
}
```
If now we do something on the lines of the following, (which could happen when we pass objects around), we would end with two different `createProxy1` proxies for the _same_ `value`, as our WeakMap doesn't have a reference for any of the objects.
```js
const value = {};
const o = createProxy1(createProxy2(createProxy1(value)));
```
_Still, the WeakMap is welcome because any object with a references will reuse a proxy._
## What We Need
Given the following example code, when the last `createProxy1` gets evaluated
```js
const o = createProxy1(createProxy2(createProxy1(value)));
```
1. it needs to _see-through_ `createProxy2`
2. discover that's already proxied with `createProxy1`
3. return **what was passed to it**, and not the first proxy, because if not we will lose `createProxy2` wrapper
## Solving The Onion With The Symbol Trick
Learned The Symbol Trick reading [Solid JS Source code](https://github.com/solidjs/solid), basically without adding a new property to the object, we can `get` a special property (usually a `Symbol`) and do something in special if the key responds as we expect. In this case returning a simple `true`
```js
const $Handler1 = Symbol();
class Handler1 {
get(target, key) {
if (key === $Handler1) return true;
return target[key];
}
}
const $Handler2 = Symbol();
class Handler2 {
get(target, key) {
if (key === $Handler2) return true;
return target[key];
}
}
const wm1 = new WeakMap();
function createProxy1(value) {
if (value[$Handler1]) {
return value;
}
if (!wm1.has(value)) {
wm1.set(value, new Proxy(value, new Handler1()));
}
return wm1.get(value);
}
const wm2 = new WeakMap();
function createProxy2(value) {
if (value[$Handler2]) {
return value;
}
if (!wm2.has(value)) {
wm2.set(value, new Proxy(value, new Handler2()));
}
return wm2.get(value);
}
```
So now we can _see-through_ `createProxy2` and do nothing.
```js
const value = {};
const o1 = createProxy1(createProxy2(createProxy1(value)));
const o2 = createProxy2(createProxy1(value));
console.log(o1 === o2);
```
It's a bit counter intuitive, but it works because when we do `proxy2[handler1]` it cannot find it, and then does `target[key]` which is forwarded to `proxy1[handler1]` returning `true`.
## Scoping, Recursively
Now if we want for the proxy to have its own recursive cache, without being shared globally (as it happens in the previous example), for writing a `copy-on-write` or something like Solid JS `projections`, we can do the following:
[please note this example is only for demonstrating persistent scoped references (it doesnt do `copy-on-write`)]
```js
const $Handler = Symbol();
class Handler {
constructor(wm) {
this.wm = wm; // NOTICE
}
get(target, key, proxy) {
if (key === $Handler) return this.wm; // NOTICE
return createProxy(target[key], this.wm);
}
set(target, key, value, proxy) {
target[key] = createProxy(value, this.wm);
return true;
}
}
function createProxy(value, wm = new WeakMap()) {
if (typeof value !== "object") return value;
if (value[$Handler] === wm /* NOTICE**/) {
return value;
}
if (!wm.has(value)) {
wm.set(value, new Proxy(value, new Handler(wm)));
}
return wm.get(value);
}
```
When we create a proxy, we wont pass a WeakMap, but it will create one. Passing it to the proxy handler constructor as `new Handler(wm)` and sharing it recursively with `createProxy(value, this.wm)`, whenever the proxy returns an `object`.
Making all references to any sub-object stable within the same proxy.
```js
const value = { data: { a: "hola" } };
const o1 = createProxy(value);
const o2 = createProxy(value);
console.log(1, o1 !== o2);
console.log(2, o1.data !== value.data);
console.log(3, o1.data !== o2.data);
o1.data2 = o1.data;
console.log(4, o1.data === o1.data2);
o1.data2 = value.data;
console.log(5, o1.data === o1.data2);
o2.data2 = o2.data;
o2.data3 = o2.data2;
console.log(6, o2.data === o2.data2);
console.log(7, o2.data2 === o2.data3);
console.log(8, o1.data2 !== o2.data2);
console.log(9, o1.data !== o2.data);
```
But there's more, when we do the following (nesting a proxy inside the other):
```js
const value = { data: { a: "hola" } };
const o1 = createProxy(value);
const o2 = createProxy(value);
o1.something = o2;
```
It wont _see-through_ `o2` and return `o2` proxy. The `keyed` symbol returns the `WeakMap` and the condition wont match.
```js
if (value[$Handler] === wm /* NOTICE**/) {
return value;
}
```
The symbol is used for identity matching, not for querying the `WeakMap`. `o2.wm` is not the same one as `o1.wm`.
Even then, when the object is not the same, `o2` will still reflect contents as `o1.something` but in a new instance, as our point was to not share references globally, but share them internally.
## A Simple Projection
Now with this new knowledge we can create something in the lines of a Solid JS Projection, which will mirror an object and allow you to write to it, without changing the original.
https://playground.solidjs.com/anonymous/967cd91f-4087-46c9-ba4c-1c89a46835a6
## Bonus Track
- The following example shows how you can return the wrong layer of an onion (the wrong proxy) if you arent careful https://playground.solidjs.com/anonymous/fe5ccf70-efab-40ab-9323-6aabaf2cedfa
- Not all proxies should follow what has been demostrated, but avoiding overwrapping is important.
---
### Links
- Ryan Carniato [hackmd profile](https://hackmd.io/@0u1u3zEAQAO0iYWVAStEvw) - loads of intersting notes (he also likes onions, thanks for the term!)
- Solid JS [Discord](https://discord.gg/solidjs)
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy