From 878df5be64a82c24b76ebf0b317212049e274fee Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Wed, 15 May 2024 16:58:03 -0500 Subject: [PATCH] Add ability to specify capabilities in setup (#255) and clarify that setup options are merged instead of replaced. --- src/InvocationModel.ts | 4 ++-- src/ProgrammingModel.ts | 12 ++++++---- src/converters/toRpcHttp.ts | 4 ++-- src/setup.ts | 27 ++++++++++++++++------ test/setup.test.ts | 46 +++++++++++++++++++++++++++++++++++++ types/app.d.ts | 3 ++- types/setup.d.ts | 8 ++++++- 7 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 test/setup.test.ts diff --git a/src/InvocationModel.ts b/src/InvocationModel.ts index bfb9227..d1958ae 100644 --- a/src/InvocationModel.ts +++ b/src/InvocationModel.ts @@ -24,7 +24,7 @@ import { AzFuncSystemError } from './errors'; import { waitForProxyRequest } from './http/httpProxy'; import { createStreamRequest } from './http/HttpRequest'; import { InvocationContext } from './InvocationContext'; -import { isHttpStreamEnabled } from './setup'; +import { enableHttpStream } from './setup'; import { isHttpTrigger, isTimerTrigger, isTrigger } from './utils/isTrigger'; import { isDefined, nonNullProp, nonNullValue } from './utils/nonNull'; @@ -76,7 +76,7 @@ export class InvocationModel implements coreTypes.InvocationModel { const bindingType = rpcBinding.type; let input: unknown; - if (isHttpTrigger(bindingType) && isHttpStreamEnabled()) { + if (isHttpTrigger(bindingType) && enableHttpStream) { const proxyRequest = await waitForProxyRequest(this.#coreCtx.invocationId); input = createStreamRequest(proxyRequest, nonNullProp(req, 'triggerMetadata')); } else { diff --git a/src/ProgrammingModel.ts b/src/ProgrammingModel.ts index bf245b1..009ed92 100644 --- a/src/ProgrammingModel.ts +++ b/src/ProgrammingModel.ts @@ -6,7 +6,7 @@ import { CoreInvocationContext, WorkerCapabilities } from '@azure/functions-core import { version } from './constants'; import { setupHttpProxy } from './http/httpProxy'; import { InvocationModel } from './InvocationModel'; -import { isHttpStreamEnabled, lockSetup } from './setup'; +import { capabilities as libraryCapabilities, enableHttpStream, lockSetup } from './setup'; export class ProgrammingModel implements coreTypes.ProgrammingModel { name = '@azure/functions'; @@ -16,14 +16,16 @@ export class ProgrammingModel implements coreTypes.ProgrammingModel { return new InvocationModel(coreCtx); } - async getCapabilities(capabilities: WorkerCapabilities): Promise { + async getCapabilities(workerCapabilities: WorkerCapabilities): Promise { lockSetup(); - if (isHttpStreamEnabled()) { + if (enableHttpStream) { const httpUri = await setupHttpProxy(); - capabilities.HttpUri = httpUri; + workerCapabilities.HttpUri = httpUri; } - return capabilities; + Object.assign(workerCapabilities, libraryCapabilities); + + return workerCapabilities; } } diff --git a/src/converters/toRpcHttp.ts b/src/converters/toRpcHttp.ts index 9767237..c314f17 100644 --- a/src/converters/toRpcHttp.ts +++ b/src/converters/toRpcHttp.ts @@ -5,7 +5,7 @@ import { RpcHttpData, RpcTypedData } from '@azure/functions-core'; import { AzFuncSystemError } from '../errors'; import { sendProxyResponse } from '../http/httpProxy'; import { HttpResponse } from '../http/HttpResponse'; -import { isHttpStreamEnabled } from '../setup'; +import { enableHttpStream } from '../setup'; import { toRpcHttpCookie } from './toRpcHttpCookie'; import { toRpcTypedData } from './toRpcTypedData'; @@ -19,7 +19,7 @@ export async function toRpcHttp(invocationId: string, data: unknown): Promise = {}; + export function setup(opts: SetupOptions): void { if (setupLocked) { throw new AzFuncSystemError("Setup options can't be changed after app startup has finished."); @@ -27,10 +29,21 @@ export function setup(opts: SetupOptions): void { } } - options = opts; - workerSystemLog('information', `Setup options: ${JSON.stringify(options)}`); -} + if (isDefined(opts.enableHttpStream)) { + enableHttpStream = opts.enableHttpStream; + } -export function isHttpStreamEnabled(): boolean { - return !!options.enableHttpStream; + if (opts.capabilities) { + for (let [key, val] of Object.entries(opts.capabilities)) { + if (isDefined(val)) { + val = String(val); + workerSystemLog('debug', `Capability ${key} set to ${val}.`); + capabilities[key] = val; + } + } + } + + if (enableHttpStream) { + workerSystemLog('debug', `HTTP streaming enabled.`); + } } diff --git a/test/setup.test.ts b/test/setup.test.ts new file mode 100644 index 0000000..a650341 --- /dev/null +++ b/test/setup.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import 'mocha'; +import { expect } from 'chai'; +import { capabilities, enableHttpStream, setup } from '../src/setup'; + +describe('setup', () => { + it('enableHttpStream', () => { + // default + expect(enableHttpStream).to.equal(false); + + // set to true + setup({ enableHttpStream: true }); + expect(enableHttpStream).to.equal(true); + + // don't change if not explicitly set + setup({}); + expect(enableHttpStream).to.equal(true); + setup({ capabilities: {} }); + expect(enableHttpStream).to.equal(true); + + // set to false + setup({ enableHttpStream: false }); + expect(enableHttpStream).to.equal(false); + }); + + it('capabilities', () => { + // default + expect(capabilities).to.deep.equal({}); + + // various setting & merging without replacing + setup({ capabilities: { a: '1' } }); + expect(capabilities).to.deep.equal({ a: '1' }); + setup({}); + expect(capabilities).to.deep.equal({ a: '1' }); + setup({ capabilities: { b: '2' } }); + expect(capabilities).to.deep.equal({ a: '1', b: '2' }); + setup({ capabilities: { a: '3' } }); + expect(capabilities).to.deep.equal({ a: '3', b: '2' }); + + // boolean converted to string + setup({ capabilities: { c: true } }); + expect(capabilities).to.deep.equal({ a: '3', b: '2', c: 'true' }); + }); +}); diff --git a/types/app.d.ts b/types/app.d.ts index 43e4dea..c2f7608 100644 --- a/types/app.d.ts +++ b/types/app.d.ts @@ -15,7 +15,8 @@ import { WarmupFunctionOptions } from './warmup'; /** * Optional method to configure the behavior of your app. - * This can only be done during app startup, before invocations occur + * This can only be done during app startup, before invocations occur. + * If called multiple times, options will be merged with the previous options specified. */ export declare function setup(options: SetupOptions): void; diff --git a/types/setup.d.ts b/types/setup.d.ts index c5237fb..92af251 100644 --- a/types/setup.d.ts +++ b/types/setup.d.ts @@ -3,8 +3,14 @@ export interface SetupOptions { /** - * PREVIEW: Stream http requests and responses instead of loading entire body in memory. + * Stream http requests and responses instead of loading entire body in memory. * [Learn more here](https://aka.ms/AzFuncNodeHttpStreams) */ enableHttpStream?: boolean; + + /** + * Dictionary of Node.js worker capabilities. + * This will be merged with existing capabilities specified by the Node.js worker and library. + */ + capabilities?: Record; }