Skip to content

Commit f077afd

Browse files
committed
Bind method this to its parent object
extractMethods pushed each function into methodsById directly, so the host invoked it with `this` undefined. Methods like { n: 0, inc() { return ++this.n } } failed at runtime. Bind the function to its source container on extraction so `this` points at the original object.
1 parent 3dcf561 commit f077afd

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/host.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export async function createSandbox(opts?: CreateSandboxOptions): Promise<Sandbo
144144
switch (typeof value) {
145145
case "function":
146146
methods[key] = methodsById.length;
147-
methodsById.push(value);
147+
methodsById.push(value.bind(source));
148148
break;
149149
case "object": {
150150
if (value === null) {

tests/basics.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,53 @@ describe("globals", () => {
209209
test("rejects unserializable globals", async () => {
210210
await expect(createSandbox({ globals: { sym: Symbol("nope") } })).rejects.toThrow();
211211
});
212+
213+
test("method `this` binds to its parent object", async () => {
214+
const counter = {
215+
n: 0,
216+
inc() {
217+
return ++this.n;
218+
},
219+
};
220+
sandbox = await createSandbox({ globals: { counter } });
221+
expect(await sandbox.evaluate("counter.inc()")).toBe(1);
222+
expect(await sandbox.evaluate("counter.inc()")).toBe(2);
223+
expect(counter.n).toBe(2);
224+
});
225+
226+
test("nested method `this` binds to its immediate parent", async () => {
227+
const inner = {
228+
label: "hi",
229+
greet() {
230+
return this.label;
231+
},
232+
};
233+
sandbox = await createSandbox({ globals: { outer: { inner } } });
234+
expect(await expression(sandbox, "await outer.inner.greet()")).toBe("hi");
235+
});
236+
237+
test("sibling methods share the same `this`", async () => {
238+
const store = {
239+
value: 10,
240+
get() {
241+
return this.value;
242+
},
243+
set(v: number) {
244+
this.value = v;
245+
},
246+
};
247+
sandbox = await createSandbox({ globals: { store } });
248+
expect(await sandbox.evaluate("store.get()")).toBe(10);
249+
await sandbox.run("await store.set(99)");
250+
expect(await sandbox.evaluate("store.get()")).toBe(99);
251+
});
252+
253+
test("top-level functions still work without `this`", async () => {
254+
sandbox = await createSandbox({
255+
globals: { add: (a: number, b: number) => a + b },
256+
});
257+
expect(await expression(sandbox, "await add(2, 3)")).toBe(5);
258+
});
212259
});
213260

214261
describe("global value types", () => {

0 commit comments

Comments
 (0)