Skip to content

Commit

Permalink
feat: add get/set prototype handlers and static method for Nexo
Browse files Browse the repository at this point in the history
  • Loading branch information
drusco committed Sep 6, 2024
1 parent c09673e commit 4c8a50c
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 16 deletions.
9 changes: 9 additions & 0 deletions src/Nexo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,13 @@ describe("Nexo", () => {

expect(keys).toStrictEqual(["foo"]);
});

it("Gets the prototype of a proyx", () => {
const nexo = new Nexo();
const proxy = nexo.create();
const proxyArray = nexo.create([]);

expect(Nexo.getPrototypeOf(proxy)).toBeNull();
expect(Nexo.getPrototypeOf(proxyArray)).toBe(Array.prototype);
});
});
8 changes: 8 additions & 0 deletions src/Nexo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class Nexo extends NexoEmitter {
return Reflect.ownKeys(proxy);
}

static getPrototypeOf(proxy: nx.Proxy): object {
const { sandbox } = Nexo.wrap(proxy);
if (sandbox) {
return Reflect.getPrototypeOf(sandbox);
}
return Reflect.getPrototypeOf(proxy);
}

use(id: string, target?: nx.traceable): nx.Proxy {
if (!target && this.entries.has(id)) {
// returns an existing proxy by its id
Expand Down
69 changes: 69 additions & 0 deletions src/handlers/getPrototypeOf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Nexo from "../Nexo.js";
import ProxyEvent from "../events/ProxyEvent.js";

describe("getPrototypeOf", () => {
it("Emits an event with custom data", () => {
const nexo = new Nexo();
const proxy = nexo.create();
const wrapper = Nexo.wrap(proxy);
const listener = jest.fn();

nexo.on("proxy.getPrototypeOf", listener);
wrapper.on("proxy.getPrototypeOf", listener);

const prototype = Reflect.getPrototypeOf(proxy);

const [event]: [ProxyEvent<{ data: object }>] = listener.mock.lastCall;

expect(listener).toHaveBeenCalledTimes(2);
expect(prototype).toBeNull();
expect(event).toBeInstanceOf(ProxyEvent);
expect(event.cancelable).toBe(false);
expect(event.target).toBe(proxy);
expect(event.data).toBe(prototype);
});

it("Returns the traceable target prototype", () => {
const nexo = new Nexo();
const target = [];
const proxy = nexo.create(target);

const prototype = Reflect.getPrototypeOf(target);

expect(Reflect.getPrototypeOf(proxy)).toBe(prototype);
expect(Object.getPrototypeOf(proxy)).toBe(prototype);
expect(Nexo.getPrototypeOf(proxy)).toBe(prototype);
});

it("Returns null for proxies without a traceable target", () => {
const nexo = new Nexo();
const proxy = nexo.create();

expect(Reflect.getPrototypeOf(proxy)).toBeNull();
expect(Object.getPrototypeOf(proxy)).toBeNull();
expect(Nexo.getPrototypeOf(proxy)).toBeNull();
});

it("Returns a custom prototype", () => {
const nexo = new Nexo();
const proxy = nexo.create();

Reflect.setPrototypeOf(proxy, Array.prototype);

expect(Reflect.getPrototypeOf(proxy)).toBeNull();
expect(Object.getPrototypeOf(proxy)).toBeNull();
expect(Nexo.getPrototypeOf(proxy)).toBe(Array.prototype);
});

it("Updates the targets current prototype", () => {
const nexo = new Nexo();
const target = new Map();
const proxy = nexo.create(target);

Reflect.setPrototypeOf(proxy, Array.prototype);

expect(Reflect.getPrototypeOf(proxy)).toBe(Array.prototype);
expect(Object.getPrototypeOf(proxy)).toBe(Array.prototype);
expect(Nexo.getPrototypeOf(proxy)).toBe(Array.prototype);
});
});
18 changes: 6 additions & 12 deletions src/handlers/getPrototypeOf.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import type nx from "../types/Nexo.js";
import isTraceable from "../utils/isTraceable.js";
import ProxyEvent from "../events/ProxyEvent.js";
import map from "../utils/maps.js";
import Nexo from "../Nexo.js";

const getPrototypeOf = (target: nx.traceable): object => {
const proxy = map.tracables.get(target);
const { sandbox } = Nexo.wrap(proxy);
const prototype = Reflect.getPrototypeOf(target);
const proto = sandbox ? Reflect.getPrototypeOf(sandbox) : prototype;

new ProxyEvent("getPrototypeOf", { target: proxy });
new ProxyEvent("getPrototypeOf", { target: proxy, data: proto });

try {
if (isTraceable(target)) {
return Object.getPrototypeOf(target);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// empty
}

return null;
return prototype;
};

export default getPrototypeOf;
118 changes: 118 additions & 0 deletions src/handlers/setPrototypeOf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Nexo from "../Nexo.js";
import ProxyError from "../errors/ProxyError.js";
import ProxyEvent from "../events/ProxyEvent.js";

describe("setPrototypeOf", () => {
it("Emits an event with custom data", () => {
const nexo = new Nexo();
const proxy = nexo.create();
const wrapper = Nexo.wrap(proxy);
const listener = jest.fn();

nexo.on("proxy.setPrototypeOf", listener);
wrapper.on("proxy.setPrototypeOf", listener);

Reflect.setPrototypeOf(proxy, Array.prototype);
const [event]: [ProxyEvent<{ prototype: object }>] = listener.mock.lastCall;

expect(listener).toHaveBeenCalledTimes(2);
expect(event).toBeInstanceOf(ProxyEvent);
expect(event.cancelable).toBe(true);
expect(event.target).toBe(proxy);
expect(event.data.prototype).toBe(Array.prototype);
});

it("Can prevent the default event behavior", () => {
const nexo = new Nexo();
const proxy = nexo.create();

nexo.on("proxy.setPrototypeOf", (event: ProxyEvent) => {
event.preventDefault();
return;
});

Reflect.setPrototypeOf(proxy, Array.prototype);

expect(Reflect.getPrototypeOf(proxy)).toBeNull();
expect(Object.getPrototypeOf(proxy)).toBeNull();
expect(Nexo.getPrototypeOf(proxy)).toBeNull();
});

it("Cannot replace the prototype with a non-object", () => {
const nexo = new Nexo();
const proxy = nexo.create();

nexo.on("proxy.setPrototypeOf", (event: ProxyEvent) => {
event.preventDefault();
return "non-object";
});

const setPrototypeOf = Reflect.setPrototypeOf.bind(
null,
proxy,
Array.prototype,
);

expect(setPrototypeOf).toThrow(ProxyError);
expect(Reflect.getPrototypeOf(proxy)).toBeNull();
expect(Object.getPrototypeOf(proxy)).toBeNull();
expect(Nexo.getPrototypeOf(proxy)).toBeNull();
});

it("Can set a new prototype on a proxy without traceable target", () => {
const nexo = new Nexo();
const proxy = nexo.create();

nexo.on("proxy.setPrototypeOf", (event: ProxyEvent) => {
event.preventDefault();
return Array.prototype;
});

Reflect.setPrototypeOf(proxy, null);

expect(Reflect.getPrototypeOf(proxy)).toBeNull();
expect(Object.getPrototypeOf(proxy)).toBeNull();
expect(Nexo.getPrototypeOf(proxy)).toBe(Array.prototype);
});

it("Can set a new prototype on a proxy with traceable target", () => {
const nexo = new Nexo();
const target = {};
const proxy = nexo.create(target);

nexo.on("proxy.setPrototypeOf", (event: ProxyEvent) => {
event.preventDefault();
return Array.prototype;
});

Reflect.setPrototypeOf(proxy, null);

expect(Reflect.getPrototypeOf(proxy)).toBe(Array.prototype);
expect(Reflect.getPrototypeOf(target)).toBe(Array.prototype);
expect(Object.getPrototypeOf(proxy)).toBe(Array.prototype);
expect(Object.getPrototypeOf(target)).toBe(Array.prototype);
expect(Nexo.getPrototypeOf(proxy)).toBe(Array.prototype);
});

it("Throws an error when the traceable target is not extensible", () => {
const nexo = new Nexo();
const proxy = nexo.create();

Reflect.preventExtensions(proxy);

const setNewPrototype = Reflect.setPrototypeOf.bind(
null,
proxy,
Array.prototype,
);

const setSamePrototype = Reflect.setPrototypeOf.bind(
null,
proxy,
Nexo.getPrototypeOf(proxy),
);

expect(setNewPrototype).toThrow(ProxyError);
expect(setSamePrototype).not.toThrow();
});
});
37 changes: 37 additions & 0 deletions src/handlers/setPrototypeOf.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
import type nx from "../types/Nexo.js";
import map from "../utils/maps.js";
import ProxyEvent from "../events/ProxyEvent.js";
import ProxyError from "../errors/ProxyError.js";

const setPrototypeOf = (target: nx.traceable, prototype: object): boolean => {
const proxy = map.tracables.get(target);
const { sandbox } = map.proxies.get(proxy);
const extensible = Reflect.isExtensible(target);
const currentPrototype = Reflect.getPrototypeOf(target);

const event = new ProxyEvent("setPrototypeOf", {
target: proxy,
cancelable: true,
data: { target, prototype },
});

if (!extensible && currentPrototype !== prototype) {
throw new ProxyError(
"Prototype cannot be changed because the target object is not extensible",
proxy,
);
}

if (event.defaultPrevented) {
// Throw error when returning a prototype that is not an object
if (
event.returnValue !== undefined &&
typeof event.returnValue !== "object"
) {
throw new ProxyError(
"Cannot set the new prototype because it is not an object or null",
proxy,
);
}
// Try applying the returned prototype to either the sandbox or the target
if (typeof event.returnValue === "object") {
if (!Reflect.setPrototypeOf(sandbox || target, event.returnValue)) {
throw new ProxyError(
`Could not set prototype of ${sandbox ? "sandbox" : "target"}`,
proxy,
);
}
return true;
}
// The 'setPrototypeOf' event got prevented
return false;
}

if (!Reflect.setPrototypeOf(sandbox || target, prototype)) {
throw new ProxyError(
`Could not set prototype of ${sandbox ? "sandbox" : "target"}`,
proxy,
);
}

return true;
};

Expand Down
9 changes: 5 additions & 4 deletions src/utils/getProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const getProxy = (nexo: Nexo, target?: nx.traceable, id?: string): nx.Proxy => {

// create proxy

const proxyTarget = target || function () {};
const fn = Object.setPrototypeOf(function () {}, null);
const proxyTarget = target || fn;
const revocable = Proxy.revocable(proxyTarget, handlers);
const traceable = isTraceable(target);
const proxy = revocable.proxy as nx.Proxy;
Expand All @@ -38,10 +39,10 @@ const getProxy = (nexo: Nexo, target?: nx.traceable, id?: string): nx.Proxy => {

if (!traceable) {
// Remove function related properties for proxies without traceable target
for (const key of Reflect.ownKeys(proxyTarget)) {
const descriptor = Object.getOwnPropertyDescriptor(proxyTarget, key);
for (const key of Reflect.ownKeys(fn)) {
const descriptor = Object.getOwnPropertyDescriptor(fn, key);
if (descriptor.configurable) {
delete proxyTarget[key];
delete fn[key];
}
}
}
Expand Down

0 comments on commit 4c8a50c

Please sign in to comment.