diff --git a/.changeset/shaggy-bees-attack.md b/.changeset/shaggy-bees-attack.md new file mode 100644 index 00000000000..6df0cc127a3 --- /dev/null +++ b/.changeset/shaggy-bees-attack.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Tolerate undefined `concast.sources` if `complete` called earlier than `concast.start` diff --git a/src/utilities/observables/Concast.ts b/src/utilities/observables/Concast.ts index b088733a0b1..deff3ce1730 100644 --- a/src/utilities/observables/Concast.ts +++ b/src/utilities/observables/Concast.ts @@ -85,9 +85,12 @@ export class Concast extends Observable { } } - // A consumable array of source observables, incrementally consumed - // each time this.handlers.complete is called. - private sources: Source[]; + // A consumable array of source observables, incrementally consumed each time + // this.handlers.complete is called. This private field is not initialized + // until the concast.start method is called, which can happen asynchronously + // if a Promise is passed to the Concast constructor, so undefined is a + // possible value for this.sources before concast.start is called. + private sources: Source[] | undefined; private start(sources: ConcastSourcesIterable) { if (this.sub !== void 0) return; @@ -185,9 +188,14 @@ export class Concast extends Observable { }, complete: () => { - const { sub } = this; + const { sub, sources = [] } = this; if (sub !== null) { - const value = this.sources.shift(); + // If complete is called before concast.start, this.sources may be + // undefined, so we use a default value of [] for sources. That works + // here because it falls into the if (!value) {...} block, which + // appropriately terminates the Concast, even if this.sources might + // eventually have been initialized to a non-empty array. + const value = sources.shift(); if (!value) { if (sub) setTimeout(() => sub.unsubscribe()); this.sub = null; diff --git a/src/utilities/observables/__tests__/Concast.ts b/src/utilities/observables/__tests__/Concast.ts index 002376c2d8f..c14746a2a3d 100644 --- a/src/utilities/observables/__tests__/Concast.ts +++ b/src/utilities/observables/__tests__/Concast.ts @@ -1,6 +1,6 @@ import { itAsync } from "../../../testing/core"; import { Observable } from "../Observable"; -import { Concast } from "../Concast"; +import { Concast, ConcastSourcesIterable } from "../Concast"; describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { itAsync("can concatenate other observables", (resolve, reject) => { @@ -30,6 +30,36 @@ describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { }); }); + itAsync("Can tolerate being completed before input Promise resolves", (resolve, reject) => { + let resolvePromise: (sources: ConcastSourcesIterable) => void; + const delayPromise = new Promise>(resolve => { + resolvePromise = resolve; + }); + + const concast = new Concast(delayPromise); + const observer = { + next() { + reject(new Error("should not have called observer.next")); + }, + error: reject, + complete() { + reject(new Error("should not have called observer.complete")); + }, + }; + + concast.addObserver(observer); + concast.removeObserver(observer); + + return concast.promise.then(finalResult => { + expect(finalResult).toBeUndefined(); + resolvePromise([]); + return delayPromise; + }).then(delayedPromiseResult => { + expect(delayedPromiseResult).toEqual([]); + resolve(); + }).catch(reject); + }); + itAsync("behaves appropriately if unsubscribed before first result", (resolve, reject) => { const concast = new Concast([ new Promise(resolve => setTimeout(resolve, 100)).then(