From e967a4dec07f84f8996529ba6a39b0e09ef9da5c Mon Sep 17 00:00:00 2001 From: Robat Williams Date: Thu, 21 Dec 2023 11:31:22 +0000 Subject: [PATCH] Extract ConcurrencyLimitedFetch into own file --- README.md | 2 +- manifest-localhost.xml | 2 +- manifest.xml | 2 +- shared.html | 2 +- src/functions/ConcurrencyLimitedFetch.mjs | 59 +++++++++++++++ src/functions/{functions.js => functions.mjs} | 72 +------------------ src/functions/functions.test.mjs | 2 +- 7 files changed, 67 insertions(+), 74 deletions(-) create mode 100644 src/functions/ConcurrencyLimitedFetch.mjs rename src/functions/{functions.js => functions.mjs} (72%) diff --git a/README.md b/README.md index 4873f60..b6f3708 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,8 @@ npm run sideload:desktop | Path | Description | | --- | --- | -| src/functions/functions.js | The JavaScript functions which implement the functions. | | src/functions/functions.json | The metadata which details each function and references its implementation. | +| src/functions/functions.mjs | The JavaScript functions which implement the functions. | | shared.html | Root page loaded in the background during add-in startup. | | manifest-local.xml | A version of manifest.xml which references https://localhost:3000/ for add-in development use. | | manifest.xml | Configures where the add-in should be loaded from and what features it will make use of. | diff --git a/manifest-localhost.xml b/manifest-localhost.xml index aabfb79..e019d63 100644 --- a/manifest-localhost.xml +++ b/manifest-localhost.xml @@ -60,7 +60,7 @@ - + diff --git a/manifest.xml b/manifest.xml index 549991b..503797a 100644 --- a/manifest.xml +++ b/manifest.xml @@ -60,7 +60,7 @@ - + diff --git a/shared.html b/shared.html index d153491..0e40fa3 100644 --- a/shared.html +++ b/shared.html @@ -5,7 +5,7 @@ type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" > - + diff --git a/src/functions/ConcurrencyLimitedFetch.mjs b/src/functions/ConcurrencyLimitedFetch.mjs new file mode 100644 index 0000000..28a23bb --- /dev/null +++ b/src/functions/ConcurrencyLimitedFetch.mjs @@ -0,0 +1,59 @@ +/** + * Limits the number of concurrent requests to the API. + * + * 1. Prevents flooding the API when full recalculation occurs in a sheet with + * many completion cells (e.g. when opening the workbook). + * 2. Gives the user a chance to cancel mass recalculation (e.g. by undo or + * delete) before all the requests are dispatched. + */ +export default class ConcurrencyLimitedFetch { + /** + * High enough to be unnoticeable for small scenarios, and to complete large + * scenarios in reasonable time. + * Low enough to avoid incurring excessive costs before the user has a chance + * to cancel, even for large model input/output sizes. + */ + static _PENDING_LIMIT = 10; + + _queue = []; + _pendingCount = 0; + + fetch(resource, options) { + const promise = new Promise((resolve, reject) => { + const task = { + args: { resource, options }, + resolve, + reject, + }; + this._queue.push(task); + }); + + this._process(); + + return promise; + } + + _process() { + if ( + this._queue.length === 0 || + this._pendingCount > ConcurrencyLimitedFetch._PENDING_LIMIT + ) { + return; + } + + const task = this._queue.shift(); + + if (task.args.options.signal.aborted) { + task.reject(task.args.options.signal.reason); + } + + this._pendingCount++; + const promise = fetch(task.args.resource, task.args.options); + + promise.then(task.resolve, task.reject); + promise.finally(() => { + this._pendingCount--; + this._process(); + }); + } +} diff --git a/src/functions/functions.js b/src/functions/functions.mjs similarity index 72% rename from src/functions/functions.js rename to src/functions/functions.mjs index 6fc9bb5..4766a38 100644 --- a/src/functions/functions.js +++ b/src/functions/functions.mjs @@ -1,62 +1,4 @@ -/** - * Limits the number of concurrent requests to the API. - * - * 1. Prevents flooding the API when full recalculation occurs in a sheet with - * many completion cells (e.g. when opening the workbook). - * 2. Gives the user a chance to cancel mass recalculation (e.g. by undo or - * delete) before all the requests are dispatched. - */ -class ConcurrencyLimitedFetch { - /** - * High enough to be unnoticeable for small scenarios, and to complete large - * scenarios in reasonable time. - * Low enough to avoid incurring excessive costs before the user has a chance - * to cancel, even for large model input/output sizes. - */ - static _PENDING_LIMIT = 10; - - _queue = []; - _pendingCount = 0; - - fetch(resource, options) { - const promise = new Promise((resolve, reject) => { - const task = { - args: { resource, options }, - resolve, - reject, - }; - this._queue.push(task); - }); - - this._process(); - - return promise; - } - - _process() { - if ( - this._queue.length === 0 || - this._pendingCount > ConcurrencyLimitedFetch._PENDING_LIMIT - ) { - return; - } - - const task = this._queue.shift(); - - if (task.args.options.signal.aborted) { - task.reject(task.args.options.signal.reason); - } - - this._pendingCount++; - const promise = fetch(task.args.resource, task.args.options); - - promise.then(task.resolve, task.reject); - promise.finally(() => { - this._pendingCount--; - this._process(); - }); - } -} +import ConcurrencyLimitedFetch from './ConcurrencyLimitedFetch.mjs'; const COMPLETION_ENTITY_KIND = 'openai-excel-functions:chat-completion'; const EMPTY_OR_ZERO = 0; @@ -64,7 +6,7 @@ const EMPTY_OR_ZERO = 0; const fetcher = new ConcurrencyLimitedFetch(); CustomFunctions.associate('CHAT_COMPLETE', chatComplete); -async function chatComplete(messages, params, invocation) { +export async function chatComplete(messages, params, invocation) { const { API_KEY: apiKey, API_BASE: apiBase = 'https://api.openai.com/', @@ -152,7 +94,7 @@ async function chatComplete(messages, params, invocation) { // Terminology note: our _cost_ is driven by usage and OpenAI's _prices_. CustomFunctions.associate('COST', cost); -function cost(completionsMatrix, pricesMatrix) { +export function cost(completionsMatrix, pricesMatrix) { const allPrices = Object.fromEntries( pricesMatrix.map((row) => [row[0], { input: row[1], output: row[2] }]), ); @@ -225,11 +167,3 @@ function mapObject(object, callback) { Object.entries(object).map(([key, value]) => [key, callback(value)]), ); } - -// For unit testing. -if (typeof module === 'object') { - module.exports = { - chatComplete, - cost, - }; -} diff --git a/src/functions/functions.test.mjs b/src/functions/functions.test.mjs index cfe21e4..6ef4d14 100644 --- a/src/functions/functions.test.mjs +++ b/src/functions/functions.test.mjs @@ -1,8 +1,8 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { makeCompletionEntity } from '../../testFramework/completionEntityStub.mjs'; -import { chatComplete, cost } from './functions.js'; import { makeCompletionResponse } from '../../testFramework/completionResponseStub.mjs'; +import { chatComplete, cost } from './functions.mjs'; describe('CHAT_COMPLETE', () => { it('makes a completion for given messages', async (t) => {