diff --git a/src/state/ObservableScope.test.ts b/src/state/ObservableScope.test.ts index 99f2b424..a0c71268 100644 --- a/src/state/ObservableScope.test.ts +++ b/src/state/ObservableScope.test.ts @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { BehaviorSubject, combineLatest, Subject } from "rxjs"; import { logger } from "matrix-js-sdk/lib/logger"; +import { sleep } from "matrix-js-sdk/lib/utils"; import { Epoch, @@ -102,3 +103,92 @@ describe("Epoch", () => { s$.complete(); }); }); + +describe("Reconcile", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should wait clean up before processing next", async () => { + vi.useFakeTimers(); + const scope = new ObservableScope(); + const behavior$ = new BehaviorSubject(0); + + const setup = vi.fn().mockImplementation(async () => await sleep(100)); + const cleanup = vi + .fn() + .mockImplementation(async (n: number) => await sleep(100)); + scope.reconcile(behavior$, async (value) => { + await setup(); + return async (): Promise => { + await cleanup(value); + }; + }); + // Let the initial setup process + await vi.advanceTimersByTimeAsync(120); + expect(setup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledTimes(0); + + // Send next value + behavior$.next(1); + await vi.advanceTimersByTimeAsync(50); + // Should not have started setup for 1 yet + expect(setup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledWith(0); + + // Let cleanup finish + await vi.advanceTimersByTimeAsync(50); + // Now setup for 1 should have started + expect(setup).toHaveBeenCalledTimes(2); + }); + + it("should skip intermediates values that are not setup", async () => { + vi.useFakeTimers(); + const scope = new ObservableScope(); + const behavior$ = new BehaviorSubject(0); + + const setup = vi + .fn() + .mockImplementation(async (n: number) => await sleep(100)); + + const cleanupLock = Promise.withResolvers(); + const cleanup = vi + .fn() + .mockImplementation(async (n: number) => await cleanupLock.promise); + + scope.reconcile(behavior$, async (value) => { + await setup(value); + return async (): Promise => { + await cleanup(value); + }; + }); + // Let the initial setup process (0) + await vi.advanceTimersByTimeAsync(120); + + // Send 4 next values quickly + behavior$.next(1); + behavior$.next(2); + behavior$.next(3); + behavior$.next(4); + + await vi.advanceTimersByTimeAsync(3000); + // should have only called cleanup for 0 + expect(cleanup).toHaveBeenCalledTimes(1); + expect(cleanup).toHaveBeenCalledWith(0); + // Let cleanup finish + cleanupLock.resolve(undefined); + await vi.advanceTimersByTimeAsync(120); + + // Now setup for 4 should have started, skipping 1,2,3 + expect(setup).toHaveBeenCalledTimes(2); + expect(setup).toHaveBeenCalledWith(4); + expect(setup).not.toHaveBeenCalledWith(1); + expect(setup).not.toHaveBeenCalledWith(2); + expect(setup).not.toHaveBeenCalledWith(3); + }); +});