Skip to content

Commit 6be9f0d

Browse files
authored
Refactor FunctionLoader so that it returns a cloned function each time (#564)
1 parent 7591acf commit 6be9f0d

File tree

2 files changed

+35
-16
lines changed

2 files changed

+35
-16
lines changed

src/FunctionLoader.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class FunctionLoader implements IFunctionLoader {
1818
[k: string]: {
1919
info: FunctionInfo;
2020
func: Function;
21+
thisArg: unknown;
2122
};
2223
} = {};
2324

@@ -42,15 +43,11 @@ export class FunctionLoader implements IFunctionLoader {
4243
script = require(scriptFilePath);
4344
}
4445
const entryPoint = <string>(metadata && metadata.entryPoint);
45-
const userFunction = getEntryPoint(script, entryPoint);
46-
if (typeof userFunction !== 'function') {
47-
throw new InternalException(
48-
'The resolved entry point is not a function and cannot be invoked by the functions runtime. Make sure the function has been correctly exported.'
49-
);
50-
}
46+
const [userFunction, thisArg] = getEntryPoint(script, entryPoint);
5147
this.#loadedFunctions[functionId] = {
5248
info: new FunctionInfo(metadata),
5349
func: userFunction,
50+
thisArg,
5451
};
5552
}
5653

@@ -66,7 +63,8 @@ export class FunctionLoader implements IFunctionLoader {
6663
getFunc(functionId: string): Function {
6764
const loadedFunction = this.#loadedFunctions[functionId];
6865
if (loadedFunction && loadedFunction.func) {
69-
return loadedFunction.func;
66+
// `bind` is necessary to set the `this` arg, but it's also nice because it makes a clone of the function, preventing this invocation from affecting future invocations
67+
return loadedFunction.func.bind(loadedFunction.thisArg);
7068
} else {
7169
throw new InternalException(`Function code for '${functionId}' is not loaded and cannot be invoked.`);
7270
}
@@ -83,9 +81,10 @@ export class FunctionLoader implements IFunctionLoader {
8381
}
8482
}
8583

86-
function getEntryPoint(f: any, entryPoint?: string): Function {
84+
function getEntryPoint(f: any, entryPoint?: string): [Function, unknown] {
85+
let thisArg: unknown;
8786
if (f !== null && typeof f === 'object') {
88-
const obj = f;
87+
thisArg = f;
8988
if (entryPoint) {
9089
// the module exports multiple functions
9190
// and an explicit entry point was named
@@ -99,12 +98,6 @@ function getEntryPoint(f: any, entryPoint?: string): Function {
9998
// 'run' or 'index' by convention
10099
f = f.run || f.index;
101100
}
102-
103-
if (typeof f === 'function') {
104-
return function () {
105-
return f.apply(obj, arguments);
106-
};
107-
}
108101
}
109102

110103
if (!f) {
@@ -116,7 +109,11 @@ function getEntryPoint(f: any, entryPoint?: string): Function {
116109
"you must indicate the entry point, either by naming it 'run' or 'index', or by naming it " +
117110
"explicitly via the 'entryPoint' metadata property.";
118111
throw new InternalException(msg);
112+
} else if (typeof f !== 'function') {
113+
throw new InternalException(
114+
'The resolved entry point is not a function and cannot be invoked by the functions runtime. Make sure the function has been correctly exported.'
115+
);
119116
}
120117

121-
return f;
118+
return [f, thisArg];
122119
}

test/FunctionLoader.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ describe('FunctionLoader', () => {
144144
expect(result.then).to.be.a('function');
145145
});
146146

147+
it("function returned is a clone so that it can't affect other executions", async () => {
148+
mock('test', { test: async () => {} });
149+
150+
await loader.load(
151+
'functionId',
152+
<rpc.IRpcFunctionMetadata>{
153+
scriptFile: 'test',
154+
entryPoint: 'test',
155+
},
156+
{}
157+
);
158+
159+
const userFunction = loader.getFunc('functionId');
160+
Object.assign(userFunction, { hello: 'world' });
161+
162+
const userFunction2 = loader.getFunc('functionId');
163+
164+
expect(userFunction).to.not.equal(userFunction2);
165+
expect(userFunction['hello']).to.equal('world');
166+
expect(userFunction2['hello']).to.be.undefined;
167+
});
168+
147169
it('respects .cjs extension', () => {
148170
const result = loader.isESModule('test.cjs', {
149171
type: 'module',

0 commit comments

Comments
 (0)