diff --git a/CodePush.js b/CodePush.js index 87604a46..86093d96 100644 --- a/CodePush.js +++ b/CodePush.js @@ -1,4 +1,5 @@ import { AcquisitionManager as Sdk, DownloadStatus } from "./lib/commonjs/acquisition-sdk/acquisition-sdk"; +import { onceUntilRejected } from "./lib/commonjs/once-until-rejected"; import { Alert } from "./AlertAdapter"; import requestFetchAdapter from "./request-fetch-adapter"; import { AppState, NativeEventEmitter, Platform } from "react-native"; @@ -312,18 +313,8 @@ function getPromisifiedSdk(requestFetchAdapter, config) { return sdk; } -// This ensures that notifyApplicationReadyInternal is only called once -// in the lifetime of this module instance. -const notifyApplicationReady = (() => { - let notifyApplicationReadyPromise; - return () => { - if (!notifyApplicationReadyPromise) { - notifyApplicationReadyPromise = notifyApplicationReadyInternal(); - } - - return notifyApplicationReadyPromise; - }; -})(); +// Succeeds once per module instance; a failed call is retried by the next caller (e.g. sync()). +const notifyApplicationReady = onceUntilRejected(notifyApplicationReadyInternal); async function notifyApplicationReadyInternal() { await NativeCodePush.notifyApplicationReady(); diff --git a/src/__tests__/once-until-rejected.test.ts b/src/__tests__/once-until-rejected.test.ts new file mode 100644 index 00000000..0bb3e19f --- /dev/null +++ b/src/__tests__/once-until-rejected.test.ts @@ -0,0 +1,34 @@ +import * as assert from 'assert'; + +import { onceUntilRejected } from '../once-until-rejected'; + +describe('onceUntilRejected', () => { + it('does not run again after it resolves', async () => { + let calls = 0; + const run = onceUntilRejected(async () => ++calls); + + assert.strictEqual(await run(), 1); + assert.strictEqual(await run(), 1); + assert.strictEqual(calls, 1); + }); + + it('shares a pending attempt, and runs again after it rejects', async () => { + let calls = 0; + const run = onceUntilRejected(async () => { + calls++; + if (calls === 1) { + throw new Error('first attempt failed'); + } + return calls; + }); + + const first = run(); + const second = run(); + assert.strictEqual(first, second); + await assert.rejects(first, /first attempt failed/); + + assert.strictEqual(await run(), 2); + assert.strictEqual(await run(), 2); + assert.strictEqual(calls, 2); + }); +}); diff --git a/src/once-until-rejected.ts b/src/once-until-rejected.ts new file mode 100644 index 00000000..132247a6 --- /dev/null +++ b/src/once-until-rejected.ts @@ -0,0 +1,13 @@ +// Dedupes concurrent and repeat calls, but forgets a rejected attempt so a transient failure isn't permanent. +export function onceUntilRejected(fn: () => Promise): () => Promise { + let promise: Promise | undefined; + return () => { + if (!promise) { + promise = fn().catch((error) => { + promise = undefined; + throw error; + }); + } + return promise; + }; +}