---
# System prepended metadata

title: Solving JavaScript Proxy Onions
tags: [proxies]

---

# 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
