From 6c8fb5c0e04390c4aef99f90c2a8bd1b3d432239 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Fri, 25 Feb 2022 15:47:12 -0800 Subject: [PATCH 01/10] Refactor source code to use async/await instead of done --- src/Context.ts | 92 ++------ src/eventHandlers/invocationRequest.ts | 213 +++++++++++-------- src/setupEventStream.ts | 2 +- test/ContextTests.ts | 12 +- test/eventHandlers/invocationRequest.test.ts | 16 +- 5 files changed, 155 insertions(+), 180 deletions(-) diff --git a/src/Context.ts b/src/Context.ts index 586fc3d5..94aef60a 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -22,16 +22,12 @@ import { import { FunctionInfo } from './FunctionInfo'; import { Request } from './http/Request'; import { Response } from './http/Response'; +import EventEmitter = require('events'); import LogLevel = rpc.RpcLog.Level; -import LogCategory = rpc.RpcLog.RpcLogCategory; -export function CreateContextAndInputs( - info: FunctionInfo, - request: rpc.IInvocationRequest, - logCallback: LogCallback, - callback: ResultCallback -) { - const context = new InvocationContext(info, request, logCallback, callback); +export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback) { + const doneEmitter = new EventEmitter(); + const context = new InvocationContext(info, request, logCallback, doneEmitter); const bindings: ContextBindings = {}; const inputs: any[] = []; @@ -76,6 +72,7 @@ export function CreateContextAndInputs( return { context: context, inputs: inputs, + doneEmitter, }; } @@ -95,7 +92,7 @@ class InvocationContext implements Context { info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, - callback: ResultCallback + doneEmitter: EventEmitter ) { this.invocationId = request.invocationId; this.traceContext = fromRpcTraceContext(request.traceContext); @@ -107,89 +104,32 @@ class InvocationContext implements Context { }; this.executionContext = executionContext; this.bindings = {}; - let _done = false; - let _promise = false; // Log message that is tied to function invocation - this.log = Object.assign( - (...args: any[]) => logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args), - { - error: (...args: any[]) => - logWithAsyncCheck(_done, logCallback, LogLevel.Error, executionContext, ...args), - warn: (...args: any[]) => - logWithAsyncCheck(_done, logCallback, LogLevel.Warning, executionContext, ...args), - info: (...args: any[]) => - logWithAsyncCheck(_done, logCallback, LogLevel.Information, executionContext, ...args), - verbose: (...args: any[]) => - logWithAsyncCheck(_done, logCallback, LogLevel.Trace, executionContext, ...args), - } - ); + this.log = Object.assign((...args: any[]) => logCallback(LogLevel.Information, ...args), { + error: (...args: any[]) => logCallback(LogLevel.Error, ...args), + warn: (...args: any[]) => logCallback(LogLevel.Warning, ...args), + info: (...args: any[]) => logCallback(LogLevel.Information, ...args), + verbose: (...args: any[]) => logCallback(LogLevel.Trace, ...args), + }); this.bindingData = getNormalizedBindingData(request); this.bindingDefinitions = getBindingDefinitions(info); - // isPromise is a hidden parameter that we set to true in the event of a returned promise - this.done = (err?: any, result?: any, isPromise?: boolean) => { - _promise = isPromise === true; - if (_done) { - if (_promise) { - logCallback( - LogLevel.Error, - LogCategory.User, - "Error: Choose either to return a promise or call 'done'. Do not use both in your script." - ); - } else { - logCallback( - LogLevel.Error, - LogCategory.User, - "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'." - ); - } - return; - } - _done = true; - - // Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object - if (info.httpOutputName && this.res && this.bindings[info.httpOutputName] === undefined) { - this.bindings[info.httpOutputName] = this.res; - } - - callback(err, { - return: result, - bindings: this.bindings, - }); + this.done = (err?: unknown, result?: any) => { + doneEmitter.emit('done', err, result); }; } } -// Emit warning if trying to log after function execution is done. -function logWithAsyncCheck( - done: boolean, - log: LogCallback, - level: LogLevel, - executionContext: ExecutionContext, - ...args: any[] -) { - if (done) { - let badAsyncMsg = - "Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited or calls to 'done' made before function execution completes. "; - badAsyncMsg += `Function name: ${executionContext.functionName}. Invocation Id: ${executionContext.invocationId}. `; - badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 `; - log(LogLevel.Warning, LogCategory.System, badAsyncMsg); - } - return log(level, LogCategory.User, ...args); -} - export interface InvocationResult { return: any; bindings: ContextBindings; } -export type DoneCallback = (err?: Error | string, result?: any) => void; - -export type LogCallback = (level: LogLevel, category: rpc.RpcLog.RpcLogCategory, ...args: any[]) => void; +export type DoneCallback = (err?: unknown, result?: any) => void; -export type ResultCallback = (err?: any, result?: InvocationResult) => void; +export type LogCallback = (level: LogLevel, ...args: any[]) => void; export interface Dict { [key: string]: T; diff --git a/src/eventHandlers/invocationRequest.ts b/src/eventHandlers/invocationRequest.ts index 97e26706..aa817516 100644 --- a/src/eventHandlers/invocationRequest.ts +++ b/src/eventHandlers/invocationRequest.ts @@ -3,8 +3,9 @@ import { format } from 'util'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; -import { CreateContextAndInputs, LogCallback, ResultCallback } from '../Context'; +import { CreateContextAndInputs } from '../Context'; import { toTypedData } from '../converters'; +import { isError } from '../utils/ensureErrorType'; import { nonNullProp } from '../utils/nonNull'; import { toRpcStatus } from '../utils/toRpcStatus'; import { WorkerChannel } from '../WorkerChannel'; @@ -16,9 +17,20 @@ import LogLevel = rpc.RpcLog.Level; * @param requestId gRPC message request id * @param msg gRPC message content */ -export function invocationRequest(channel: WorkerChannel, requestId: string, msg: rpc.IInvocationRequest) { +export async function invocationRequest(channel: WorkerChannel, requestId: string, msg: rpc.IInvocationRequest) { + const response: rpc.IInvocationResponse = { + invocationId: msg.invocationId, + result: toRpcStatus(), + }; + // explicitly set outputData to empty array to concat later + response.outputData = []; + + let isDone = false; + let resultIsPromise = false; + const info = channel.functionLoader.getInfo(nonNullProp(msg, 'functionId')); - const logCallback: LogCallback = (level, category, ...args) => { + + function log(level: LogLevel, category: LogCategory, ...args: any[]) { channel.log({ invocationId: msg.invocationId, category: `${info.name}.Invocation`, @@ -26,18 +38,68 @@ export function invocationRequest(channel: WorkerChannel, requestId: string, msg level: level, logCategory: category, }); - }; + } + function systemLog(level: LogLevel, ...args: any[]) { + log(level, LogCategory.System, ...args); + } + function userLog(level: LogLevel, ...args: any[]) { + if (isDone) { + let badAsyncMsg = + "Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited or calls to 'done' made before function execution completes. "; + badAsyncMsg += `Function name: ${info.name}. Invocation Id: ${msg.invocationId}. `; + badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 `; + systemLog(LogLevel.Warning, badAsyncMsg); + } + log(level, LogCategory.User, ...args); + } // Log invocation details to ensure the invocation received by node worker - logCallback(LogLevel.Debug, LogCategory.System, 'Received FunctionInvocationRequest'); + systemLog(LogLevel.Debug, 'Received FunctionInvocationRequest'); - const resultCallback: ResultCallback = (err: unknown, result) => { - const response: rpc.IInvocationResponse = { - invocationId: msg.invocationId, - result: toRpcStatus(err), - }; - // explicitly set outputData to empty array to concat later - response.outputData = []; + function onDone(): void { + if (isDone) { + const message = resultIsPromise + ? "Error: Choose either to return a promise or call 'done'. Do not use both in your script." + : "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'."; + systemLog(LogLevel.Error, message); + } + isDone = true; + } + + const { context, inputs, doneEmitter } = CreateContextAndInputs(info, msg, userLog); + try { + const legacyDoneTask = new Promise((resolve, reject) => { + doneEmitter.on('done', (err?: unknown, result?: any) => { + onDone(); + if (isError(err)) { + reject(err); + } else { + resolve(result); + } + }); + }); + + let userFunction = channel.functionLoader.getFunc(nonNullProp(msg, 'functionId')); + userFunction = channel.runInvocationRequestBefore(context, userFunction); + let rawResult = userFunction(context, ...inputs); + resultIsPromise = rawResult && typeof rawResult.then === 'function'; + let resultTask: Promise; + if (resultIsPromise) { + rawResult = Promise.resolve(rawResult).then((r) => { + onDone(); + return r; + }); + resultTask = Promise.race([rawResult, legacyDoneTask]); + } else { + resultTask = legacyDoneTask; + } + + const result = await resultTask; + + // Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object + if (info.httpOutputName && context.res && context.bindings[info.httpOutputName] === undefined) { + context.bindings[info.httpOutputName] = context.res; + } // As legacy behavior, falsy values get serialized to `null` in AzFunctions. // This breaks Durable Functions expectations, where customers expect any @@ -46,86 +108,61 @@ export function invocationRequest(channel: WorkerChannel, requestId: string, msg // values get serialized. const isDurableBinding = info?.bindings?.name?.type == 'activityTrigger'; - try { - if (result || (isDurableBinding && result != null)) { - const returnBinding = info.getReturnBinding(); - // Set results from return / context.done - if (result.return || (isDurableBinding && result.return != null)) { - // $return binding is found: return result data to $return binding - if (returnBinding) { - response.returnValue = returnBinding.converter(result.return); - // $return binding is not found: read result as object of outputs - } else { - response.outputData = Object.keys(info.outputBindings) - .filter((key) => result.return[key] !== undefined) - .map( - (key) => - { - name: key, - data: info.outputBindings[key].converter(result.return[key]), - } - ); - } - // returned value does not match any output bindings (named or $return) - // if not http, pass along value - if (!response.returnValue && response.outputData.length == 0 && !info.hasHttpTrigger) { - response.returnValue = toTypedData(result.return); - } - } - // Set results from context.bindings - if (result.bindings) { - response.outputData = response.outputData.concat( - Object.keys(info.outputBindings) - // Data from return prioritized over data from context.bindings - .filter((key) => { - const definedInBindings: boolean = result.bindings[key] !== undefined; - const hasReturnValue = !!result.return; - const hasReturnBinding = !!returnBinding; - const definedInReturn: boolean = - hasReturnValue && !hasReturnBinding && result.return[key] !== undefined; - return definedInBindings && !definedInReturn; - }) - .map( - (key) => - { - name: key, - data: info.outputBindings[key].converter(result.bindings[key]), - } - ) + const returnBinding = info.getReturnBinding(); + // Set results from return / context.done + if (result || (isDurableBinding && result != null)) { + // $return binding is found: return result data to $return binding + if (returnBinding) { + response.returnValue = returnBinding.converter(result); + // $return binding is not found: read result as object of outputs + } else { + response.outputData = Object.keys(info.outputBindings) + .filter((key) => result[key] !== undefined) + .map( + (key) => + { + name: key, + data: info.outputBindings[key].converter(result[key]), + } ); - } } - } catch (err) { - response.result = toRpcStatus(err); + // returned value does not match any output bindings (named or $return) + // if not http, pass along value + if (!response.returnValue && response.outputData.length == 0 && !info.hasHttpTrigger) { + response.returnValue = toTypedData(result); + } } - channel.eventStream.write({ - requestId: requestId, - invocationResponse: response, - }); - - channel.runInvocationRequestAfter(context); - }; - - const { context, inputs } = CreateContextAndInputs(info, msg, logCallback, resultCallback); - let userFunction = channel.functionLoader.getFunc(nonNullProp(msg, 'functionId')); - - userFunction = channel.runInvocationRequestBefore(context, userFunction); - - // catch user errors from the same async context in the event loop and correlate with invocation - // throws from asynchronous work (setTimeout, etc) are caught by 'unhandledException' and cannot be correlated with invocation - try { - const result = userFunction(context, ...inputs); - - if (result && typeof result.then === 'function') { - result - .then((result) => { - (context.done)(null, result, true); - }) - .catch((err) => { - (context.done)(err, null, true); - }); + // Set results from context.bindings + if (context.bindings) { + response.outputData = response.outputData.concat( + Object.keys(info.outputBindings) + // Data from return prioritized over data from context.bindings + .filter((key) => { + const definedInBindings: boolean = context.bindings[key] !== undefined; + const hasReturnValue = !!result; + const hasReturnBinding = !!returnBinding; + const definedInReturn: boolean = + hasReturnValue && !hasReturnBinding && result[key] !== undefined; + return definedInBindings && !definedInReturn; + }) + .map( + (key) => + { + name: key, + data: info.outputBindings[key].converter(context.bindings[key]), + } + ) + ); } } catch (err) { - resultCallback(err); + response.result = toRpcStatus(err); + isDone = true; } + + channel.eventStream.write({ + requestId: requestId, + invocationResponse: response, + }); + + channel.runInvocationRequestAfter(context); } diff --git a/src/setupEventStream.ts b/src/setupEventStream.ts index 8382072c..52c9e4ff 100644 --- a/src/setupEventStream.ts +++ b/src/setupEventStream.ts @@ -31,7 +31,7 @@ export function setupEventStream(workerId: string, channel: WorkerChannel): void void functionLoadRequest(channel, msg.requestId, nonNullProp(msg, eventName)); break; case 'invocationRequest': - invocationRequest(channel, msg.requestId, nonNullProp(msg, eventName)); + void invocationRequest(channel, msg.requestId, nonNullProp(msg, eventName)); break; case 'workerInitRequest': workerInitRequest(channel, msg.requestId, nonNullProp(msg, eventName)); diff --git a/test/ContextTests.ts b/test/ContextTests.ts index b6adea7f..67392f30 100644 --- a/test/ContextTests.ts +++ b/test/ContextTests.ts @@ -25,11 +25,9 @@ const timerTriggerInput: rpc.IParameterBinding = { describe('Context', () => { let _logger: any; - let _resultCallback: any; beforeEach(() => { _logger = sinon.spy(); - _resultCallback = sinon.spy(); }); it('camelCases timer trigger input when appropriate', async () => { @@ -49,7 +47,7 @@ describe('Context', () => { }, }, }); - const workerOutputs = CreateContextAndInputs(info, msg, _logger, _resultCallback); + const workerOutputs = CreateContextAndInputs(info, msg, _logger); const myTimerWorker = workerOutputs.inputs[0]; expect(myTimerWorker.schedule).to.be.empty; expect(myTimerWorker.scheduleStatus.last).to.equal('2016-10-04T10:15:00+00:00'); @@ -76,7 +74,7 @@ describe('Context', () => { }, }); - const { context } = CreateContextAndInputs(info, msg, _logger, _resultCallback); + const { context } = CreateContextAndInputs(info, msg, _logger); expect(context.bindingData.sys).to.be.undefined; expect(context.bindingData.invocationId).to.equal('1'); expect(context.invocationId).to.equal('1'); @@ -110,7 +108,7 @@ describe('Context', () => { }, }); - const { context } = CreateContextAndInputs(info, msg, _logger, _resultCallback); + const { context } = CreateContextAndInputs(info, msg, _logger); const { bindingData } = context; expect(bindingData.sys.methodName).to.equal('test'); expect(bindingData.sys.randGuid).to.not.be.undefined; @@ -163,7 +161,7 @@ describe('Context', () => { }, }); - const { context } = CreateContextAndInputs(info, msg, _logger, _resultCallback); + const { context } = CreateContextAndInputs(info, msg, _logger); const { bindingData } = context; expect(bindingData.invocationId).to.equal('1'); expect(bindingData.headers.header1).to.equal('value1'); @@ -207,7 +205,7 @@ describe('Context', () => { }, }); - const { context } = CreateContextAndInputs(info, msg, _logger, _resultCallback); + const { context } = CreateContextAndInputs(info, msg, _logger); const { bindingData } = context; expect(bindingData.invocationId).to.equal('1'); expect(bindingData.headers.header1).to.equal('value1'); diff --git a/test/eventHandlers/invocationRequest.test.ts b/test/eventHandlers/invocationRequest.test.ts index 71036e10..6afe8f86 100644 --- a/test/eventHandlers/invocationRequest.test.ts +++ b/test/eventHandlers/invocationRequest.test.ts @@ -169,7 +169,7 @@ namespace Msg { invocationId: '1', message: "Error: Choose either to return a promise or call 'done'. Do not use both in your script.", level: LogLevel.Error, - logCategory: LogCategory.User, + logCategory: LogCategory.System, }, }; export const duplicateDoneLog: rpc.IStreamingMessage = { @@ -178,7 +178,7 @@ namespace Msg { invocationId: '1', message: "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'.", level: LogLevel.Error, - logCategory: LogCategory.User, + logCategory: LogCategory.System, }, }; export const unexpectedLogAfterDoneLog: rpc.IStreamingMessage = { @@ -438,8 +438,8 @@ describe('invocationRequest', () => { sendInvokeMessage([httpInputData]); await stream.assertCalledWith( Msg.receivedInvocLog(), - Msg.invocResponse([getHttpResponse()]), - Msg.asyncAndDoneLog + Msg.asyncAndDoneLog, + Msg.invocResponse([getHttpResponse()]) ); }); @@ -452,8 +452,8 @@ describe('invocationRequest', () => { sendInvokeMessage([httpInputData]); await stream.assertCalledWith( Msg.receivedInvocLog(), - Msg.invocResponse([getHttpResponse()]), - Msg.duplicateDoneLog + Msg.duplicateDoneLog, + Msg.invocResponse([getHttpResponse()]) ); }); @@ -466,9 +466,9 @@ describe('invocationRequest', () => { sendInvokeMessage([httpInputData]); await stream.assertCalledWith( Msg.receivedInvocLog(), - Msg.invocResponse([getHttpResponse()]), Msg.unexpectedLogAfterDoneLog, - Msg.userTestLog + Msg.userTestLog, + Msg.invocResponse([getHttpResponse()]) ); }); From 81f01543534a58e810b1771ba82cd8bd53dc44e7 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Mon, 28 Feb 2022 16:55:46 -0800 Subject: [PATCH 02/10] Add pre and post invocation hooks --- .eslintrc.json | 1 + src/Disposable.ts | 35 ++ src/WorkerChannel.ts | 71 ++-- src/eventHandlers/invocationRequest.ts | 26 +- test/eventHandlers/invocationRequest.test.ts | 361 +++++++++++++------ tsconfig.json | 3 + workerTypes/index.d.ts | 83 +++++ 7 files changed, 429 insertions(+), 151 deletions(-) create mode 100644 src/Disposable.ts create mode 100644 workerTypes/index.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index edd0417a..2682af37 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,6 +43,7 @@ "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/unbound-method": "off", "no-empty": "off", + "prefer-const": ["error", { "destructuring": "all" }], "prefer-rest-params": "off", "prefer-spread": "off" }, diff --git a/src/Disposable.ts b/src/Disposable.ts new file mode 100644 index 00000000..c40aee79 --- /dev/null +++ b/src/Disposable.ts @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +/** + * Based off of VS Code + * https://github.com/microsoft/vscode/blob/a64e8e5673a44e5b9c2d493666bde684bd5a135c/src/vs/workbench/api/common/extHostTypes.ts#L32 + */ +export class Disposable { + static from(...inDisposables: { dispose(): any }[]): Disposable { + let disposables: ReadonlyArray<{ dispose(): any }> | undefined = inDisposables; + return new Disposable(function () { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + disposables = undefined; + } + }); + } + + #callOnDispose?: () => any; + + constructor(callOnDispose: () => any) { + this.#callOnDispose = callOnDispose; + } + + dispose(): any { + if (this.#callOnDispose instanceof Function) { + this.#callOnDispose(); + this.#callOnDispose = undefined; + } + } +} diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index be64af26..0ea61115 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -1,25 +1,23 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { Context } from '@azure/functions'; +import { HookCallback } from '@azure/functions-worker'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; +import { Disposable } from './Disposable'; import { IFunctionLoader } from './FunctionLoader'; import { IEventStream } from './GrpcClient'; - -type InvocationRequestBefore = (context: Context, userFn: Function) => Function; -type InvocationRequestAfter = (context: Context) => void; +import Module = require('module'); export class WorkerChannel { public eventStream: IEventStream; public functionLoader: IFunctionLoader; - private _invocationRequestBefore: InvocationRequestBefore[]; - private _invocationRequestAfter: InvocationRequestAfter[]; + private _preInvocationHooks: HookCallback[] = []; + private _postInvocationHooks: HookCallback[] = []; constructor(eventStream: IEventStream, functionLoader: IFunctionLoader) { this.eventStream = eventStream; this.functionLoader = functionLoader; - this._invocationRequestBefore = []; - this._invocationRequestAfter = []; + this.initWorkerModule(this); } /** @@ -33,32 +31,49 @@ export class WorkerChannel { }); } - /** - * Register a patching function to be run before User Function is executed. - * Hook should return a patched version of User Function. - */ - public registerBeforeInvocationRequest(beforeCb: InvocationRequestBefore): void { - this._invocationRequestBefore.push(beforeCb); + public registerHook(hookName: string, callback: HookCallback): Disposable { + const hooks = this.getHooks(hookName); + hooks.push(callback); + return new Disposable(() => { + const index = hooks.indexOf(callback); + if (index > -1) { + hooks.splice(index, 1); + } + }); } - /** - * Register a function to be run after User Function resolves. - */ - public registerAfterInvocationRequest(afterCb: InvocationRequestAfter): void { - this._invocationRequestAfter.push(afterCb); + public async executeHooks(hookName: string, context: {}): Promise { + const callbacks = this.getHooks(hookName); + for (const callback of callbacks) { + await callback(context); + } } - public runInvocationRequestBefore(context: Context, userFunction: Function): Function { - let wrappedFunction = userFunction; - for (const before of this._invocationRequestBefore) { - wrappedFunction = before(context, wrappedFunction); + private getHooks(hookName: string): HookCallback[] { + switch (hookName) { + case 'preInvocation': + return this._preInvocationHooks; + case 'postInvocation': + return this._postInvocationHooks; + default: + throw new RangeError(`Unrecognized hook "${hookName}"`); } - return wrappedFunction; } - public runInvocationRequestAfter(context: Context) { - for (const after of this._invocationRequestAfter) { - after(context); - } + private initWorkerModule(channel: WorkerChannel) { + const workerApi = { + registerHook: (hookName: string, callback: HookCallback) => channel.registerHook(hookName, callback), + Disposable, + }; + + Module.prototype.require = new Proxy(Module.prototype.require, { + apply(target, thisArg, argArray) { + if (argArray[0] === '@azure/functions-worker') { + return workerApi; + } else { + return Reflect.apply(target, thisArg, argArray); + } + }, + }); } } diff --git a/src/eventHandlers/invocationRequest.ts b/src/eventHandlers/invocationRequest.ts index aa817516..89557d7d 100644 --- a/src/eventHandlers/invocationRequest.ts +++ b/src/eventHandlers/invocationRequest.ts @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. +import { PostInvocationContext, PreInvocationContext } from '@azure/functions-worker'; import { format } from 'util'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; import { CreateContextAndInputs } from '../Context'; @@ -66,7 +67,7 @@ export async function invocationRequest(channel: WorkerChannel, requestId: strin isDone = true; } - const { context, inputs, doneEmitter } = CreateContextAndInputs(info, msg, userLog); + let { context, inputs, doneEmitter } = CreateContextAndInputs(info, msg, userLog); try { const legacyDoneTask = new Promise((resolve, reject) => { doneEmitter.on('done', (err?: unknown, result?: any) => { @@ -79,8 +80,12 @@ export async function invocationRequest(channel: WorkerChannel, requestId: strin }); }); - let userFunction = channel.functionLoader.getFunc(nonNullProp(msg, 'functionId')); - userFunction = channel.runInvocationRequestBefore(context, userFunction); + const userFunction = channel.functionLoader.getFunc(nonNullProp(msg, 'functionId')); + const preInvocContext: PreInvocationContext = { invocationContext: context, inputs }; + + await channel.executeHooks('preInvocation', preInvocContext); + inputs = preInvocContext.inputs; + let rawResult = userFunction(context, ...inputs); resultIsPromise = rawResult && typeof rawResult.then === 'function'; let resultTask: Promise; @@ -94,7 +99,18 @@ export async function invocationRequest(channel: WorkerChannel, requestId: strin resultTask = legacyDoneTask; } - const result = await resultTask; + const postInvocContext: PostInvocationContext = Object.assign(preInvocContext, { result: null, error: null }); + try { + postInvocContext.result = await resultTask; + } catch (err) { + postInvocContext.error = err; + } + await channel.executeHooks('postInvocation', postInvocContext); + + if (isError(postInvocContext.error)) { + throw postInvocContext.error; + } + const result = postInvocContext.result; // Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object if (info.httpOutputName && context.res && context.bindings[info.httpOutputName] === undefined) { @@ -163,6 +179,4 @@ export async function invocationRequest(channel: WorkerChannel, requestId: strin requestId: requestId, invocationResponse: response, }); - - channel.runInvocationRequestAfter(context); } diff --git a/test/eventHandlers/invocationRequest.test.ts b/test/eventHandlers/invocationRequest.test.ts index 6afe8f86..9e43a500 100644 --- a/test/eventHandlers/invocationRequest.test.ts +++ b/test/eventHandlers/invocationRequest.test.ts @@ -2,13 +2,13 @@ // Licensed under the MIT License. import { AzureFunction, Context } from '@azure/functions'; +import * as workerTypes from '@azure/functions-worker'; import { expect } from 'chai'; import 'mocha'; import * as sinon from 'sinon'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; import { FunctionInfo } from '../../src/FunctionInfo'; import { FunctionLoader } from '../../src/FunctionLoader'; -import { WorkerChannel } from '../../src/WorkerChannel'; import { beforeEventHandlerSuite } from './beforeEventHandlerSuite'; import { TestEventStream } from './TestEventStream'; import LogCategory = rpc.RpcLog.RpcLogCategory; @@ -57,7 +57,7 @@ namespace Binding { }; export const queue = { bindings: { - test: { + testOutput: { type: 'queue', direction: 1, dataType: 1, @@ -77,6 +77,8 @@ function addSuffix(asyncFunc: AzureFunction, callbackFunc: AzureFunction): [Azur ]; } +let hookData: string; + namespace TestFunc { const basicAsync = async (context: Context) => { context.log('testUserLog'); @@ -111,6 +113,27 @@ namespace TestFunc { }; export const resHttp = addSuffix(resHttpAsync, resHttpCallback); + const logHookDataAsync = async (context: Context) => { + hookData += 'invoc'; + context.log(hookData); + return 'hello'; + }; + const logHookDataCallback = (context: Context) => { + hookData += 'invoc'; + context.log(hookData); + context.done(null, 'hello'); + }; + export const logHookData = addSuffix(logHookDataAsync, logHookDataCallback); + + const logInputAsync = async (context: Context, input: any) => { + context.log(input); + }; + const logInputCallback = (context: Context, input: any) => { + context.log(input); + context.done(); + }; + export const logInput = addSuffix(logInputAsync, logInputCallback); + const multipleBindingsAsync = async (context: Context) => { context.bindings.queueOutput = 'queue message'; context.bindings.overriddenQueueOutput = 'start message'; @@ -191,15 +214,17 @@ namespace Msg { logCategory: LogCategory.System, }, }; - export const userTestLog: rpc.IStreamingMessage = { - rpcLog: { - category: 'testFuncName.Invocation', - invocationId: '1', - message: 'testUserLog', - level: LogLevel.Information, - logCategory: LogCategory.User, - }, - }; + export function userTestLog(data = 'testUserLog'): rpc.IStreamingMessage { + return { + rpcLog: { + category: 'testFuncName.Invocation', + invocationId: '1', + message: data, + level: LogLevel.Information, + logCategory: LogCategory.User, + }, + }; + } export const invocResFailed: rpc.IStreamingMessage = { requestId: 'testReqId', invocationResponse: { @@ -245,17 +270,50 @@ namespace Msg { } } +namespace InputData { + export const http = { + name: 'req', + data: { + data: 'http', + http: { + body: { + string: 'blahh', + }, + rawBody: { + string: 'blahh', + }, + }, + }, + }; + + export const string = { + name: 'testInput', + data: { + data: 'string', + string: 'testStringData', + }, + }; +} + describe('invocationRequest', () => { - let channel: WorkerChannel; let stream: TestEventStream; let loader: sinon.SinonStubbedInstance; + let workerApi: typeof workerTypes; + let testDisposables: workerTypes.Disposable[] = []; + + before(async () => { + ({ stream, loader } = beforeEventHandlerSuite()); + workerApi = await import('@azure/functions-worker'); + }); - before(() => { - ({ stream, loader, channel } = beforeEventHandlerSuite()); + beforeEach(async () => { + hookData = ''; }); afterEach(async () => { await stream.afterEachEventHandlerTest(); + workerApi.Disposable.from(...testDisposables).dispose(); + testDisposables = []; }); function sendInvokeMessage(inputData?: rpc.IParameterBinding[] | null): void { @@ -269,21 +327,6 @@ describe('invocationRequest', () => { }); } - const httpInputData = { - name: 'req', - data: { - data: 'http', - http: { - body: { - string: 'blahh', - }, - rawBody: { - string: 'blahh', - }, - }, - }, - }; - function getHttpResponse(rawBody?: string | {} | undefined, name = 'res'): rpc.IParameterBinding { let body: rpc.ITypedData; if (typeof rawBody === 'string') { @@ -311,10 +354,10 @@ describe('invocationRequest', () => { it('invokes function' + suffix, async () => { loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), - Msg.userTestLog, + Msg.userTestLog(), Msg.invocResponse([getHttpResponse()]) ); }); @@ -324,7 +367,7 @@ describe('invocationRequest', () => { it('returns correct data with $return binding' + suffix, async () => { loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.httpReturn)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); const expectedOutput = getHttpResponse(undefined, '$return'); const expectedReturnValue = { http: { @@ -366,7 +409,7 @@ describe('invocationRequest', () => { it('serializes output binding data through context.done' + suffix, async () => { loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); const expectedOutput = [getHttpResponse({ hello: 'world' })]; await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse(expectedOutput)); }); @@ -386,7 +429,7 @@ describe('invocationRequest', () => { name: 'testFuncName', }) ); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); const expectedOutput = [ getHttpResponse({ hello: 'world' }), { @@ -410,7 +453,7 @@ describe('invocationRequest', () => { it('returns failed status for user error' + suffix, async () => { loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.queue)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResFailed); }); } @@ -426,7 +469,7 @@ describe('invocationRequest', () => { it('empty function does not return invocation response', async () => { loader.getFunc.returns(() => {}); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); await stream.assertCalledWith(Msg.receivedInvocLog()); }); @@ -435,7 +478,7 @@ describe('invocationRequest', () => { context.done(); }); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), Msg.asyncAndDoneLog, @@ -449,7 +492,7 @@ describe('invocationRequest', () => { context.done(); }); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), Msg.duplicateDoneLog, @@ -463,11 +506,11 @@ describe('invocationRequest', () => { context.log('testUserLog'); }); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), Msg.unexpectedLogAfterDoneLog, - Msg.userTestLog, + Msg.userTestLog(), Msg.invocResponse([getHttpResponse()]) ); }); @@ -479,109 +522,193 @@ describe('invocationRequest', () => { return 'hello'; }); loader.getInfo.returns(new FunctionInfo(Binding.httpRes)); - sendInvokeMessage([httpInputData]); + sendInvokeMessage([InputData.http]); // wait for first two messages to ensure invocation happens await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([getHttpResponse()])); // then add extra context.log _context!.log('testUserLog'); - await stream.assertCalledWith(Msg.unexpectedLogAfterDoneLog, Msg.userTestLog); + await stream.assertCalledWith(Msg.unexpectedLogAfterDoneLog, Msg.userTestLog()); }); - describe('#invocationRequestBefore, #invocationRequestAfter', () => { - afterEach(() => { - channel['_invocationRequestAfter'] = []; - channel['_invocationRequestBefore'] = []; - }); + for (const [func, suffix] of TestFunc.logHookData) { + it('preInvocationHook' + suffix, async () => { + loader.getFunc.returns(func); + loader.getInfo.returns(new FunctionInfo(Binding.queue)); - it('should apply hook before user function is executed', async () => { - channel.registerBeforeInvocationRequest((context, userFunction) => { - context['magic_flag'] = 'magic value'; - return userFunction.bind({ __wrapped: true }); - }); + testDisposables.push( + workerApi.registerHook('preInvocation', () => { + hookData += 'pre'; + }) + ); - channel.registerBeforeInvocationRequest((context, userFunction) => { - context['secondary_flag'] = 'magic value'; - return userFunction; - }); + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith( + Msg.receivedInvocLog(), + Msg.userTestLog('preinvoc'), + Msg.invocResponse([], { string: 'hello' }) + ); + expect(hookData).to.equal('preinvoc'); + }); + } - loader.getFunc.returns(function (this: any, context) { - expect(context['magic_flag']).to.equal('magic value'); - expect(context['secondary_flag']).to.equal('magic value'); - expect(this.__wrapped).to.equal(true); - expect(channel['_invocationRequestBefore'].length).to.equal(2); - expect(channel['_invocationRequestAfter'].length).to.equal(0); - context.done(); - }); + for (const [func, suffix] of TestFunc.logInput) { + it('preInvocationHook respects change to inputs' + suffix, async () => { + loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.queue)); - sendInvokeMessage([httpInputData]); - await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); + testDisposables.push( + workerApi.registerHook('preInvocation', (context: workerTypes.PreInvocationContext) => { + expect(context.inputs.length).to.equal(1); + expect(context.inputs[0]).to.equal('testStringData'); + context.inputs = ['changedStringData']; + }) + ); + + sendInvokeMessage([InputData.string]); + await stream.assertCalledWith( + Msg.receivedInvocLog(), + Msg.userTestLog('changedStringData'), + Msg.invocResponse([]) + ); }); + } - it('should apply hook after user function is executed (callback)', async () => { - let finished = false; - let count = 0; - channel.registerAfterInvocationRequest((_context) => { - expect(finished).to.equal(true); - count += 1; - }); + for (const [func, suffix] of TestFunc.logHookData) { + it('postInvocationHook' + suffix, async () => { + loader.getFunc.returns(func); + loader.getInfo.returns(new FunctionInfo(Binding.queue)); - loader.getFunc.returns((context: Context) => { - finished = true; - expect(channel['_invocationRequestBefore'].length).to.equal(0); - expect(channel['_invocationRequestAfter'].length).to.equal(1); - expect(count).to.equal(0); - context.done(); - }); + testDisposables.push( + workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + hookData += 'post'; + expect(context.result).to.equal('hello'); + expect(context.error).to.be.null; + }) + ); + + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith( + Msg.receivedInvocLog(), + Msg.userTestLog('invoc'), + Msg.invocResponse([], { string: 'hello' }) + ); + expect(hookData).to.equal('invocpost'); + }); + } + + for (const [func, suffix] of TestFunc.logHookData) { + it('postInvocationHook respects change to context.result' + suffix, async () => { + loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.queue)); - sendInvokeMessage([httpInputData]); - await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); - expect(count).to.equal(1); + testDisposables.push( + workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + hookData += 'post'; + expect(context.result).to.equal('hello'); + expect(context.error).to.be.null; + context.result = 'world'; + }) + ); + + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith( + Msg.receivedInvocLog(), + Msg.userTestLog('invoc'), + Msg.invocResponse([], { string: 'world' }) + ); + expect(hookData).to.equal('invocpost'); }); + } - it('should apply hook after user function resolves (promise)', async () => { - let finished = false; - let count = 0; - channel.registerAfterInvocationRequest((_context) => { - expect(finished).to.equal(true); - count += 1; - }); + for (const [func, suffix] of TestFunc.error) { + it('postInvocationHook executes if function throws error' + suffix, async () => { + loader.getFunc.returns(func); + loader.getInfo.returns(new FunctionInfo(Binding.queue)); - loader.getFunc.returns(async () => { - finished = true; - expect(channel['_invocationRequestBefore'].length).to.equal(0); - expect(channel['_invocationRequestAfter'].length).to.equal(1); - expect(count).to.equal(0); - }); + testDisposables.push( + workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + hookData += 'post'; + expect(context.result).to.be.null; + expect(context.error).to.equal(testError); + }) + ); + + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResFailed); + expect(hookData).to.equal('post'); + }); + } + + for (const [func, suffix] of TestFunc.error) { + it('postInvocationHook respects change to context.error' + suffix, async () => { + loader.getFunc.returns(func); loader.getInfo.returns(new FunctionInfo(Binding.queue)); - sendInvokeMessage([httpInputData]); - await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); - expect(count).to.equal(1); + testDisposables.push( + workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + hookData += 'post'; + expect(context.result).to.be.null; + expect(context.error).to.equal(testError); + context.error = null; + context.result = 'hello'; + }) + ); + + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([], { string: 'hello' })); + expect(hookData).to.equal('post'); }); + } - it('should apply hook after user function rejects (promise)', async () => { - let finished = false; - let count = 0; - channel.registerAfterInvocationRequest((_context) => { - expect(finished).to.equal(true); - count += 1; - }); + it('pre and post invocation hooks share data', async () => { + loader.getFunc.returns(async () => {}); + loader.getInfo.returns(new FunctionInfo(Binding.queue)); - loader.getFunc.returns(async () => { - finished = true; - expect(channel['_invocationRequestBefore'].length).to.equal(0); - expect(channel['_invocationRequestAfter'].length).to.equal(1); - expect(count).to.equal(0); - throw testError; - }); - loader.getInfo.returns(new FunctionInfo(Binding.queue)); + testDisposables.push( + workerApi.registerHook('preInvocation', (context: workerTypes.PreInvocationContext) => { + context['hello'] = 'world'; + hookData += 'pre'; + }) + ); - sendInvokeMessage([httpInputData]); - await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResFailed); - expect(count).to.equal(1); + testDisposables.push( + workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + expect(context['hello']).to.equal('world'); + hookData += 'post'; + }) + ); + + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); + expect(hookData).to.equal('prepost'); + }); + + it('dispose hooks', async () => { + loader.getFunc.returns(async () => {}); + loader.getInfo.returns(new FunctionInfo(Binding.queue)); + + const disposableA: workerTypes.Disposable = workerApi.registerHook('preInvocation', () => { + hookData += 'a'; }); + testDisposables.push(disposableA); + const disposableB: workerTypes.Disposable = workerApi.registerHook('preInvocation', () => { + hookData += 'b'; + }); + testDisposables.push(disposableB); + + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); + expect(hookData).to.equal('ab'); + + disposableA.dispose(); + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); + expect(hookData).to.equal('abb'); + + disposableB.dispose(); + sendInvokeMessage([InputData.http]); + await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([])); + expect(hookData).to.equal('abb'); }); for (const [func, suffix] of TestFunc.returnEmptyString) { diff --git a/tsconfig.json b/tsconfig.json index 10fb7691..4d724619 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,9 @@ "paths": { "@azure/functions": [ "types" + ], + "@azure/functions-worker": [ + "workerTypes" ] } } diff --git a/workerTypes/index.d.ts b/workerTypes/index.d.ts new file mode 100644 index 00000000..c20ef6d5 --- /dev/null +++ b/workerTypes/index.d.ts @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { Context } from '@azure/functions'; + +/** + * This module is shipped as a built-in part of the Azure Functions Node.js worker and is available at runtime + */ +declare module '@azure/functions-worker' { + /** + * Register a hook to interact with the lifecycle of Azure Functions. + * Hooks are executed in the order they were registered and will block execution if they throw an error + */ + export function registerHook( + hookName: 'preInvocation', + callback: (context: PreInvocationContext) => void | Promise + ): Disposable; + export function registerHook( + hookName: 'postInvocation', + callback: (context: PostInvocationContext) => void | Promise + ): Disposable; + export function registerHook(hookName: string, callback: HookCallback): Disposable; + + export type HookCallback = (context: {}) => void | Promise; + + /** + * Context on a function that is about to be executed + * This object will be passed to all pre and post invocation hooks and can be used to share information between hooks + */ + export interface PreInvocationContext { + /** + * The context object passed to the function + */ + invocationContext: Context; + + /** + * The input values for this specific invocation. Changes to this array _will_ affect the inputs passed to your function + */ + inputs: any[]; + } + + /** + * Context on a function that has just executed + * This object will be passed to all pre and post invocation hooks and can be used to share information between hooks + */ + export interface PostInvocationContext extends PreInvocationContext { + /** + * The result of the function, or null if there is no result. Changes to this value _will_ affect the overall result of the function + */ + result: any; + + /** + * The error for the function, or null if there is no error. Changes to this value _will_ affect the overall result of the function + */ + error: any; + } + + /** + * Represents a type which can release resources, such as event listening or a timer. + */ + export class Disposable { + /** + * Combine many disposable-likes into one. You can use this method when having objects with a dispose function which aren't instances of `Disposable`. + * + * @param disposableLikes Objects that have at least a `dispose`-function member. Note that asynchronous dispose-functions aren't awaited. + * @return Returns a new disposable which, upon dispose, will dispose all provided disposables. + */ + static from(...disposableLikes: { dispose: () => any }[]): Disposable; + + /** + * Creates a new disposable that calls the provided function on dispose. + * *Note* that an asynchronous function is not awaited. + * + * @param callOnDispose Function that disposes something. + */ + constructor(callOnDispose: () => any); + + /** + * Dispose this object. + */ + dispose(): any; + } +} From 8827568947188eebe5963115ec69edbe105f81b0 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 2 Mar 2022 14:26:19 -0800 Subject: [PATCH 03/10] Move initWorkerModule to its own file like setupEventStream --- src/Worker.ts | 2 ++ src/WorkerChannel.ts | 19 --------------- src/setupWorkerModule.ts | 24 +++++++++++++++++++ test/eventHandlers/beforeEventHandlerSuite.ts | 2 ++ 4 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 src/setupWorkerModule.ts diff --git a/src/Worker.ts b/src/Worker.ts index ebf77868..c7a56d9e 100644 --- a/src/Worker.ts +++ b/src/Worker.ts @@ -5,6 +5,7 @@ import * as parseArgs from 'minimist'; import { FunctionLoader } from './FunctionLoader'; import { CreateGrpcEventStream } from './GrpcClient'; import { setupEventStream } from './setupEventStream'; +import { setupWorkerModule } from './setupWorkerModule'; import { ensureErrorType } from './utils/ensureErrorType'; import { InternalException } from './utils/InternalException'; import { systemError, systemLog } from './utils/Logger'; @@ -42,6 +43,7 @@ export function startNodeWorker(args) { const channel = new WorkerChannel(eventStream, new FunctionLoader()); setupEventStream(workerId, channel); + setupWorkerModule(channel); eventStream.write({ requestId: requestId, diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index 0ea61115..fd721220 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -6,7 +6,6 @@ import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-wo import { Disposable } from './Disposable'; import { IFunctionLoader } from './FunctionLoader'; import { IEventStream } from './GrpcClient'; -import Module = require('module'); export class WorkerChannel { public eventStream: IEventStream; @@ -17,7 +16,6 @@ export class WorkerChannel { constructor(eventStream: IEventStream, functionLoader: IFunctionLoader) { this.eventStream = eventStream; this.functionLoader = functionLoader; - this.initWorkerModule(this); } /** @@ -59,21 +57,4 @@ export class WorkerChannel { throw new RangeError(`Unrecognized hook "${hookName}"`); } } - - private initWorkerModule(channel: WorkerChannel) { - const workerApi = { - registerHook: (hookName: string, callback: HookCallback) => channel.registerHook(hookName, callback), - Disposable, - }; - - Module.prototype.require = new Proxy(Module.prototype.require, { - apply(target, thisArg, argArray) { - if (argArray[0] === '@azure/functions-worker') { - return workerApi; - } else { - return Reflect.apply(target, thisArg, argArray); - } - }, - }); - } } diff --git a/src/setupWorkerModule.ts b/src/setupWorkerModule.ts new file mode 100644 index 00000000..70e4dc8c --- /dev/null +++ b/src/setupWorkerModule.ts @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { HookCallback } from '@azure/functions-worker'; +import { Disposable } from './Disposable'; +import { WorkerChannel } from './WorkerChannel'; +import Module = require('module'); + +export function setupWorkerModule(channel: WorkerChannel): void { + const workerApi = { + registerHook: (hookName: string, callback: HookCallback) => channel.registerHook(hookName, callback), + Disposable, + }; + + Module.prototype.require = new Proxy(Module.prototype.require, { + apply(target, thisArg, argArray) { + if (argArray[0] === '@azure/functions-worker') { + return workerApi; + } else { + return Reflect.apply(target, thisArg, argArray); + } + }, + }); +} diff --git a/test/eventHandlers/beforeEventHandlerSuite.ts b/test/eventHandlers/beforeEventHandlerSuite.ts index 78b19fa2..254c2023 100644 --- a/test/eventHandlers/beforeEventHandlerSuite.ts +++ b/test/eventHandlers/beforeEventHandlerSuite.ts @@ -4,6 +4,7 @@ import * as sinon from 'sinon'; import { FunctionLoader } from '../../src/FunctionLoader'; import { setupEventStream } from '../../src/setupEventStream'; +import { setupWorkerModule } from '../../src/setupWorkerModule'; import { WorkerChannel } from '../../src/WorkerChannel'; import { TestEventStream } from './TestEventStream'; @@ -12,5 +13,6 @@ export function beforeEventHandlerSuite() { const loader = sinon.createStubInstance(FunctionLoader); const channel = new WorkerChannel(stream, loader); setupEventStream('workerId', channel); + setupWorkerModule(channel); return { stream, loader, channel }; } From f9d2afe696b60ffd0018e57b44a21f7c2738df55 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 9 Mar 2022 11:33:23 -0800 Subject: [PATCH 04/10] Refactor hook contexts to be more intuitive --- src/eventHandlers/invocationRequest.ts | 13 +++++++-- test/eventHandlers/invocationRequest.test.ts | 4 +-- workerTypes/index.d.ts | 30 +++++++++++++++++--- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/eventHandlers/invocationRequest.ts b/src/eventHandlers/invocationRequest.ts index 89557d7d..2451e62d 100644 --- a/src/eventHandlers/invocationRequest.ts +++ b/src/eventHandlers/invocationRequest.ts @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { PostInvocationContext, PreInvocationContext } from '@azure/functions-worker'; +import { HookData, PostInvocationContext, PreInvocationContext } from '@azure/functions-worker'; import { format } from 'util'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; import { CreateContextAndInputs } from '../Context'; @@ -80,8 +80,9 @@ export async function invocationRequest(channel: WorkerChannel, requestId: strin }); }); + const hookData: HookData = {}; const userFunction = channel.functionLoader.getFunc(nonNullProp(msg, 'functionId')); - const preInvocContext: PreInvocationContext = { invocationContext: context, inputs }; + const preInvocContext: PreInvocationContext = { hookData, invocationContext: context, inputs }; await channel.executeHooks('preInvocation', preInvocContext); inputs = preInvocContext.inputs; @@ -99,7 +100,13 @@ export async function invocationRequest(channel: WorkerChannel, requestId: strin resultTask = legacyDoneTask; } - const postInvocContext: PostInvocationContext = Object.assign(preInvocContext, { result: null, error: null }); + const postInvocContext: PostInvocationContext = { + hookData, + invocationContext: context, + inputs, + result: null, + error: null, + }; try { postInvocContext.result = await resultTask; } catch (err) { diff --git a/test/eventHandlers/invocationRequest.test.ts b/test/eventHandlers/invocationRequest.test.ts index 9e43a500..d9ca9f26 100644 --- a/test/eventHandlers/invocationRequest.test.ts +++ b/test/eventHandlers/invocationRequest.test.ts @@ -666,14 +666,14 @@ describe('invocationRequest', () => { testDisposables.push( workerApi.registerHook('preInvocation', (context: workerTypes.PreInvocationContext) => { - context['hello'] = 'world'; + context.hookData['hello'] = 'world'; hookData += 'pre'; }) ); testDisposables.push( workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { - expect(context['hello']).to.equal('world'); + expect(context.hookData['hello']).to.equal('world'); hookData += 'post'; }) ); diff --git a/workerTypes/index.d.ts b/workerTypes/index.d.ts index c20ef6d5..066b77d0 100644 --- a/workerTypes/index.d.ts +++ b/workerTypes/index.d.ts @@ -23,11 +23,23 @@ declare module '@azure/functions-worker' { export type HookCallback = (context: {}) => void | Promise; + export type HookData = { [key: string]: any }; + + /** + * Base interface for all hook context objects + */ + export interface HookContext { + /** + * The recommended place to share data between hooks + */ + hookData: HookData; + } + /** * Context on a function that is about to be executed - * This object will be passed to all pre and post invocation hooks and can be used to share information between hooks + * This object will be passed to all pre invocation hooks */ - export interface PreInvocationContext { + export interface PreInvocationContext extends HookContext { /** * The context object passed to the function */ @@ -41,9 +53,19 @@ declare module '@azure/functions-worker' { /** * Context on a function that has just executed - * This object will be passed to all pre and post invocation hooks and can be used to share information between hooks + * This object will be passed to all post invocation hooks */ - export interface PostInvocationContext extends PreInvocationContext { + export interface PostInvocationContext extends HookContext { + /** + * The context object passed to the function + */ + invocationContext: Context; + + /** + * The input values for this specific invocation + */ + inputs: any[]; + /** * The result of the function, or null if there is no result. Changes to this value _will_ affect the overall result of the function */ From 1962249456345dc804158a784bc2a7a3fcd39f08 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 9 Mar 2022 12:01:09 -0800 Subject: [PATCH 05/10] fixes to worker types --- src/WorkerChannel.ts | 4 ++-- workerTypes/index.d.ts | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index fd721220..a32fd29a 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { HookCallback } from '@azure/functions-worker'; +import { HookCallback, HookContext } from '@azure/functions-worker'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import { Disposable } from './Disposable'; import { IFunctionLoader } from './FunctionLoader'; @@ -40,7 +40,7 @@ export class WorkerChannel { }); } - public async executeHooks(hookName: string, context: {}): Promise { + public async executeHooks(hookName: string, context: HookContext): Promise { const callbacks = this.getHooks(hookName); for (const callback of callbacks) { await callback(context); diff --git a/workerTypes/index.d.ts b/workerTypes/index.d.ts index 066b77d0..b00ea41c 100644 --- a/workerTypes/index.d.ts +++ b/workerTypes/index.d.ts @@ -11,17 +11,13 @@ declare module '@azure/functions-worker' { * Register a hook to interact with the lifecycle of Azure Functions. * Hooks are executed in the order they were registered and will block execution if they throw an error */ - export function registerHook( - hookName: 'preInvocation', - callback: (context: PreInvocationContext) => void | Promise - ): Disposable; - export function registerHook( - hookName: 'postInvocation', - callback: (context: PostInvocationContext) => void | Promise - ): Disposable; + export function registerHook(hookName: 'preInvocation', callback: PreInvocationCallback): Disposable; + export function registerHook(hookName: 'postInvocation', callback: PostInvocationCallback): Disposable; export function registerHook(hookName: string, callback: HookCallback): Disposable; - export type HookCallback = (context: {}) => void | Promise; + export type HookCallback = (context: HookContext) => void | Promise; + export type PreInvocationCallback = (context: PreInvocationContext) => void | Promise; + export type PostInvocationCallback = (context: PostInvocationContext) => void | Promise; export type HookData = { [key: string]: any }; From 59e72427c115a7cd745f2730feaec53babd0deaa Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Mon, 14 Mar 2022 15:44:43 -0700 Subject: [PATCH 06/10] Add comment to `setupWorkerModule` --- src/setupWorkerModule.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/setupWorkerModule.ts b/src/setupWorkerModule.ts index 70e4dc8c..417033bf 100644 --- a/src/setupWorkerModule.ts +++ b/src/setupWorkerModule.ts @@ -6,6 +6,11 @@ import { Disposable } from './Disposable'; import { WorkerChannel } from './WorkerChannel'; import Module = require('module'); +/** + * Intercepts the default "require" method so that we can provide our own "built-in" module + * This module is essentially the publicly accessible API for our worker + * This module is available to users only at runtime, not as an installable npm package + */ export function setupWorkerModule(channel: WorkerChannel): void { const workerApi = { registerHook: (hookName: string, callback: HookCallback) => channel.registerHook(hookName, callback), From e22daf138c462014ff5a06c452f9862674acc19d Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 23 Mar 2022 11:09:58 -0700 Subject: [PATCH 07/10] Rename to "core" based on offline poll --- src/WorkerChannel.ts | 2 +- src/eventHandlers/invocationRequest.ts | 2 +- src/setupWorkerModule.ts | 4 ++-- test/eventHandlers/invocationRequest.test.ts | 4 ++-- tsconfig.json | 4 ++-- {workerTypes => types-core}/index.d.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename {workerTypes => types-core}/index.d.ts (98%) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index 05799b17..b2504330 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { HookCallback, HookContext } from '@azure/functions-worker'; +import { HookCallback, HookContext } from '@azure/functions-core'; import { readJson } from 'fs-extra'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import { Disposable } from './Disposable'; diff --git a/src/eventHandlers/invocationRequest.ts b/src/eventHandlers/invocationRequest.ts index 1794dfde..ffed3165 100644 --- a/src/eventHandlers/invocationRequest.ts +++ b/src/eventHandlers/invocationRequest.ts @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { HookData, PostInvocationContext, PreInvocationContext } from '@azure/functions-worker'; +import { HookData, PostInvocationContext, PreInvocationContext } from '@azure/functions-core'; import { format } from 'util'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; import { CreateContextAndInputs } from '../Context'; diff --git a/src/setupWorkerModule.ts b/src/setupWorkerModule.ts index 417033bf..74ada3d5 100644 --- a/src/setupWorkerModule.ts +++ b/src/setupWorkerModule.ts @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { HookCallback } from '@azure/functions-worker'; +import { HookCallback } from '@azure/functions-core'; import { Disposable } from './Disposable'; import { WorkerChannel } from './WorkerChannel'; import Module = require('module'); @@ -19,7 +19,7 @@ export function setupWorkerModule(channel: WorkerChannel): void { Module.prototype.require = new Proxy(Module.prototype.require, { apply(target, thisArg, argArray) { - if (argArray[0] === '@azure/functions-worker') { + if (argArray[0] === '@azure/functions-core') { return workerApi; } else { return Reflect.apply(target, thisArg, argArray); diff --git a/test/eventHandlers/invocationRequest.test.ts b/test/eventHandlers/invocationRequest.test.ts index c351eee9..50233dcf 100644 --- a/test/eventHandlers/invocationRequest.test.ts +++ b/test/eventHandlers/invocationRequest.test.ts @@ -4,7 +4,7 @@ /* eslint-disable deprecation/deprecation */ import { AzureFunction, Context } from '@azure/functions'; -import * as workerTypes from '@azure/functions-worker'; +import * as workerTypes from '@azure/functions-core'; import { expect } from 'chai'; import 'mocha'; import * as sinon from 'sinon'; @@ -305,7 +305,7 @@ describe('invocationRequest', () => { before(async () => { ({ stream, loader } = beforeEventHandlerSuite()); - workerApi = await import('@azure/functions-worker'); + workerApi = await import('@azure/functions-core'); }); beforeEach(async () => { diff --git a/tsconfig.json b/tsconfig.json index 4d724619..44222174 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,8 @@ "@azure/functions": [ "types" ], - "@azure/functions-worker": [ - "workerTypes" + "@azure/functions-core": [ + "types-core" ] } } diff --git a/workerTypes/index.d.ts b/types-core/index.d.ts similarity index 98% rename from workerTypes/index.d.ts rename to types-core/index.d.ts index b00ea41c..05be85ac 100644 --- a/workerTypes/index.d.ts +++ b/types-core/index.d.ts @@ -6,7 +6,7 @@ import { Context } from '@azure/functions'; /** * This module is shipped as a built-in part of the Azure Functions Node.js worker and is available at runtime */ -declare module '@azure/functions-worker' { +declare module '@azure/functions-core' { /** * Register a hook to interact with the lifecycle of Azure Functions. * Hooks are executed in the order they were registered and will block execution if they throw an error From f8d9cd9c11c8bd109b65516f6d63b045bd0b9c77 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 23 Mar 2022 11:21:47 -0700 Subject: [PATCH 08/10] One more "worker" -> "core" --- src/Worker.ts | 4 ++-- src/{setupWorkerModule.ts => setupCoreModule.ts} | 2 +- test/eventHandlers/beforeEventHandlerSuite.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/{setupWorkerModule.ts => setupCoreModule.ts} (93%) diff --git a/src/Worker.ts b/src/Worker.ts index c7a56d9e..a9d0d030 100644 --- a/src/Worker.ts +++ b/src/Worker.ts @@ -4,8 +4,8 @@ import * as parseArgs from 'minimist'; import { FunctionLoader } from './FunctionLoader'; import { CreateGrpcEventStream } from './GrpcClient'; +import { setupCoreModule } from './setupCoreModule'; import { setupEventStream } from './setupEventStream'; -import { setupWorkerModule } from './setupWorkerModule'; import { ensureErrorType } from './utils/ensureErrorType'; import { InternalException } from './utils/InternalException'; import { systemError, systemLog } from './utils/Logger'; @@ -43,7 +43,7 @@ export function startNodeWorker(args) { const channel = new WorkerChannel(eventStream, new FunctionLoader()); setupEventStream(workerId, channel); - setupWorkerModule(channel); + setupCoreModule(channel); eventStream.write({ requestId: requestId, diff --git a/src/setupWorkerModule.ts b/src/setupCoreModule.ts similarity index 93% rename from src/setupWorkerModule.ts rename to src/setupCoreModule.ts index 74ada3d5..47187b85 100644 --- a/src/setupWorkerModule.ts +++ b/src/setupCoreModule.ts @@ -11,7 +11,7 @@ import Module = require('module'); * This module is essentially the publicly accessible API for our worker * This module is available to users only at runtime, not as an installable npm package */ -export function setupWorkerModule(channel: WorkerChannel): void { +export function setupCoreModule(channel: WorkerChannel): void { const workerApi = { registerHook: (hookName: string, callback: HookCallback) => channel.registerHook(hookName, callback), Disposable, diff --git a/test/eventHandlers/beforeEventHandlerSuite.ts b/test/eventHandlers/beforeEventHandlerSuite.ts index 254c2023..c543c4ec 100644 --- a/test/eventHandlers/beforeEventHandlerSuite.ts +++ b/test/eventHandlers/beforeEventHandlerSuite.ts @@ -3,8 +3,8 @@ import * as sinon from 'sinon'; import { FunctionLoader } from '../../src/FunctionLoader'; +import { setupCoreModule } from '../../src/setupCoreModule'; import { setupEventStream } from '../../src/setupEventStream'; -import { setupWorkerModule } from '../../src/setupWorkerModule'; import { WorkerChannel } from '../../src/WorkerChannel'; import { TestEventStream } from './TestEventStream'; @@ -13,6 +13,6 @@ export function beforeEventHandlerSuite() { const loader = sinon.createStubInstance(FunctionLoader); const channel = new WorkerChannel(stream, loader); setupEventStream('workerId', channel); - setupWorkerModule(channel); + setupCoreModule(channel); return { stream, loader, channel }; } From deb43db5dc8619e2fcb1c5b5c6907579377da829 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 23 Mar 2022 12:17:35 -0700 Subject: [PATCH 09/10] more "worker" -> "core" --- src/setupCoreModule.ts | 4 +-- test/eventHandlers/invocationRequest.test.ts | 30 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/setupCoreModule.ts b/src/setupCoreModule.ts index 47187b85..34bf511e 100644 --- a/src/setupCoreModule.ts +++ b/src/setupCoreModule.ts @@ -12,7 +12,7 @@ import Module = require('module'); * This module is available to users only at runtime, not as an installable npm package */ export function setupCoreModule(channel: WorkerChannel): void { - const workerApi = { + const coreApi = { registerHook: (hookName: string, callback: HookCallback) => channel.registerHook(hookName, callback), Disposable, }; @@ -20,7 +20,7 @@ export function setupCoreModule(channel: WorkerChannel): void { Module.prototype.require = new Proxy(Module.prototype.require, { apply(target, thisArg, argArray) { if (argArray[0] === '@azure/functions-core') { - return workerApi; + return coreApi; } else { return Reflect.apply(target, thisArg, argArray); } diff --git a/test/eventHandlers/invocationRequest.test.ts b/test/eventHandlers/invocationRequest.test.ts index 50233dcf..e0e22183 100644 --- a/test/eventHandlers/invocationRequest.test.ts +++ b/test/eventHandlers/invocationRequest.test.ts @@ -4,7 +4,7 @@ /* eslint-disable deprecation/deprecation */ import { AzureFunction, Context } from '@azure/functions'; -import * as workerTypes from '@azure/functions-core'; +import * as coreTypes from '@azure/functions-core'; import { expect } from 'chai'; import 'mocha'; import * as sinon from 'sinon'; @@ -300,12 +300,12 @@ namespace InputData { describe('invocationRequest', () => { let stream: TestEventStream; let loader: sinon.SinonStubbedInstance; - let workerApi: typeof workerTypes; - let testDisposables: workerTypes.Disposable[] = []; + let coreApi: typeof coreTypes; + let testDisposables: coreTypes.Disposable[] = []; before(async () => { ({ stream, loader } = beforeEventHandlerSuite()); - workerApi = await import('@azure/functions-core'); + coreApi = await import('@azure/functions-core'); }); beforeEach(async () => { @@ -314,7 +314,7 @@ describe('invocationRequest', () => { afterEach(async () => { await stream.afterEachEventHandlerTest(); - workerApi.Disposable.from(...testDisposables).dispose(); + coreApi.Disposable.from(...testDisposables).dispose(); testDisposables = []; }); @@ -538,7 +538,7 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('preInvocation', () => { + coreApi.registerHook('preInvocation', () => { hookData += 'pre'; }) ); @@ -559,7 +559,7 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('preInvocation', (context: workerTypes.PreInvocationContext) => { + coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { expect(context.inputs.length).to.equal(1); expect(context.inputs[0]).to.equal('testStringData'); context.inputs = ['changedStringData']; @@ -581,7 +581,7 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { hookData += 'post'; expect(context.result).to.equal('hello'); expect(context.error).to.be.null; @@ -604,7 +604,7 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { hookData += 'post'; expect(context.result).to.equal('hello'); expect(context.error).to.be.null; @@ -628,7 +628,7 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { hookData += 'post'; expect(context.result).to.be.null; expect(context.error).to.equal(testError); @@ -647,7 +647,7 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { hookData += 'post'; expect(context.result).to.be.null; expect(context.error).to.equal(testError); @@ -667,14 +667,14 @@ describe('invocationRequest', () => { loader.getInfo.returns(new FunctionInfo(Binding.queue)); testDisposables.push( - workerApi.registerHook('preInvocation', (context: workerTypes.PreInvocationContext) => { + coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { context.hookData['hello'] = 'world'; hookData += 'pre'; }) ); testDisposables.push( - workerApi.registerHook('postInvocation', (context: workerTypes.PostInvocationContext) => { + coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { expect(context.hookData['hello']).to.equal('world'); hookData += 'post'; }) @@ -689,11 +689,11 @@ describe('invocationRequest', () => { loader.getFunc.returns(async () => {}); loader.getInfo.returns(new FunctionInfo(Binding.queue)); - const disposableA: workerTypes.Disposable = workerApi.registerHook('preInvocation', () => { + const disposableA: coreTypes.Disposable = coreApi.registerHook('preInvocation', () => { hookData += 'a'; }); testDisposables.push(disposableA); - const disposableB: workerTypes.Disposable = workerApi.registerHook('preInvocation', () => { + const disposableB: coreTypes.Disposable = coreApi.registerHook('preInvocation', () => { hookData += 'b'; }); testDisposables.push(disposableB); From a6ae5552d67529d9dda993662c8faaa06ead325a Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 23 Mar 2022 14:20:19 -0700 Subject: [PATCH 10/10] use # for private --- src/WorkerChannel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index b2504330..5e0630f1 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -20,8 +20,8 @@ export class WorkerChannel { public eventStream: IEventStream; public functionLoader: IFunctionLoader; public packageJson: PackageJson; - private _preInvocationHooks: HookCallback[] = []; - private _postInvocationHooks: HookCallback[] = []; + #preInvocationHooks: HookCallback[] = []; + #postInvocationHooks: HookCallback[] = []; constructor(eventStream: IEventStream, functionLoader: IFunctionLoader) { this.eventStream = eventStream; @@ -41,7 +41,7 @@ export class WorkerChannel { } public registerHook(hookName: string, callback: HookCallback): Disposable { - const hooks = this.getHooks(hookName); + const hooks = this.#getHooks(hookName); hooks.push(callback); return new Disposable(() => { const index = hooks.indexOf(callback); @@ -52,18 +52,18 @@ export class WorkerChannel { } public async executeHooks(hookName: string, context: HookContext): Promise { - const callbacks = this.getHooks(hookName); + const callbacks = this.#getHooks(hookName); for (const callback of callbacks) { await callback(context); } } - private getHooks(hookName: string): HookCallback[] { + #getHooks(hookName: string): HookCallback[] { switch (hookName) { case 'preInvocation': - return this._preInvocationHooks; + return this.#preInvocationHooks; case 'postInvocation': - return this._postInvocationHooks; + return this.#postInvocationHooks; default: throw new RangeError(`Unrecognized hook "${hookName}"`); }