Skip to content

Commit

Permalink
Refactor FunctionLoader so that it returns a cloned function each time (
Browse files Browse the repository at this point in the history
  • Loading branch information
ejizba committed Apr 8, 2022
1 parent 7591acf commit 6be9f0d
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 16 deletions.
29 changes: 13 additions & 16 deletions src/FunctionLoader.ts
Expand Up @@ -18,6 +18,7 @@ export class FunctionLoader implements IFunctionLoader {
[k: string]: {
info: FunctionInfo;
func: Function;
thisArg: unknown;
};
} = {};

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

Expand All @@ -66,7 +63,8 @@ export class FunctionLoader implements IFunctionLoader {
getFunc(functionId: string): Function {
const loadedFunction = this.#loadedFunctions[functionId];
if (loadedFunction && loadedFunction.func) {
return loadedFunction.func;
// `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
return loadedFunction.func.bind(loadedFunction.thisArg);
} else {
throw new InternalException(`Function code for '${functionId}' is not loaded and cannot be invoked.`);
}
Expand All @@ -83,9 +81,10 @@ export class FunctionLoader implements IFunctionLoader {
}
}

function getEntryPoint(f: any, entryPoint?: string): Function {
function getEntryPoint(f: any, entryPoint?: string): [Function, unknown] {
let thisArg: unknown;
if (f !== null && typeof f === 'object') {
const obj = f;
thisArg = f;
if (entryPoint) {
// the module exports multiple functions
// and an explicit entry point was named
Expand All @@ -99,12 +98,6 @@ function getEntryPoint(f: any, entryPoint?: string): Function {
// 'run' or 'index' by convention
f = f.run || f.index;
}

if (typeof f === 'function') {
return function () {
return f.apply(obj, arguments);
};
}
}

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

return f;
return [f, thisArg];
}
22 changes: 22 additions & 0 deletions test/FunctionLoader.test.ts
Expand Up @@ -144,6 +144,28 @@ describe('FunctionLoader', () => {
expect(result.then).to.be.a('function');
});

it("function returned is a clone so that it can't affect other executions", async () => {
mock('test', { test: async () => {} });

await loader.load(
'functionId',
<rpc.IRpcFunctionMetadata>{
scriptFile: 'test',
entryPoint: 'test',
},
{}
);

const userFunction = loader.getFunc('functionId');
Object.assign(userFunction, { hello: 'world' });

const userFunction2 = loader.getFunc('functionId');

expect(userFunction).to.not.equal(userFunction2);
expect(userFunction['hello']).to.equal('world');
expect(userFunction2['hello']).to.be.undefined;
});

it('respects .cjs extension', () => {
const result = loader.isESModule('test.cjs', {
type: 'module',
Expand Down

0 comments on commit 6be9f0d

Please sign in to comment.