From ab3b4488671295ca73a8e415424c33328c55a976 Mon Sep 17 00:00:00 2001 From: Petra Jaros Date: Fri, 31 Mar 2023 13:42:12 -0400 Subject: [PATCH] Defer importing ky-universal until actually needed This accomplishes two things: * It prevents a dangling promise with an import in it. If the module is loaded (queuing up the import promise) and then no clients are ever used, and thus never awaited, the import can happen too late. In particular, Jest will break if the import happens after the test is complete. * It avoids a dynamic import altogether when the client isn't needed. In particular, Jest uses Node's VM API which only experimentally supports dynamic imports (and ESM modules altogether). This change means that merely loading the http-client module doesn't require enabling that experimental support. --- lib/deferred.js | 16 ++++++++++++++ lib/httpClient.js | 9 ++++---- tests/deferred.spec.js | 50 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 lib/deferred.js create mode 100644 tests/deferred.spec.js diff --git a/lib/deferred.js b/lib/deferred.js new file mode 100644 index 0000000..e97c30c --- /dev/null +++ b/lib/deferred.js @@ -0,0 +1,16 @@ +export function deferred(f) { + let promise; + + return { + then( + onfulfilled, + onrejected + ) { + promise ||= new Promise(resolve => resolve(f())); + return promise.then( + onfulfilled, + onrejected + ); + }, + }; +} diff --git a/lib/httpClient.js b/lib/httpClient.js index ae24afb..a8cbbaa 100644 --- a/lib/httpClient.js +++ b/lib/httpClient.js @@ -2,9 +2,10 @@ * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. */ import {convertAgent} from './agentCompatibility.js'; +import {deferred} from './deferred.js'; -export const kyOriginalPromise = import('ky-universal') - .then(({default: ky}) => ky); +export const kyOriginalPromise = deferred(() => import('ky-universal') + .then(({default: ky}) => ky)); export const DEFAULT_HEADERS = { Accept: 'application/ld+json, application/json' @@ -33,7 +34,7 @@ export function createInstance({ params = convertAgent(params); // create new ky instance that will asynchronously resolve - const kyPromise = parent.then(kyBase => { + const kyPromise = deferred(() => parent.then(kyBase => { let ky; if(parent === kyOriginalPromise) { // ensure default headers, allow overrides @@ -46,7 +47,7 @@ export function createInstance({ ky = kyBase.extend({headers, ...params}); } return ky; - }); + })); return _createHttpClient(kyPromise); } diff --git a/tests/deferred.spec.js b/tests/deferred.spec.js new file mode 100644 index 0000000..04e6adc --- /dev/null +++ b/tests/deferred.spec.js @@ -0,0 +1,50 @@ +import {deferred} from '../lib/deferred.js'; + +describe('deferred()', () => { + it('resolves to the return value of its function', async () => { + const d = deferred(() => { + return 'return value'; + }); + + const ret = await d; + ret.should.equal('return value'); + }); + + it('defers execution until awaited', async () => { + let executionCount = 0; + executionCount.should.equal(0); + + const d = deferred(() => { + executionCount++; + return 'return value'; + }); + + executionCount.should.equal(0); + await d; + executionCount.should.equal(1); + }); + + it('only executes once', async () => { + let executionCount = 0; + executionCount.should.equal(0); + + const d = deferred(() => { + executionCount++; + return 'return value'; + }); + + await d; + await d; + executionCount.should.equal(1); + }); + + it('unwraps returned promises', async () => { + const d = deferred(() => { + return Promise.resolve('return value'); + }); + + const ret = await d; + ret.should.equal('return value'); + }); +}); +