diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts index c37b640f3f..2630d9f7bb 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts @@ -32,6 +32,7 @@ describe("session syncer", () => { stateService = mock(); stateService.hasInSessionMemory.mockResolvedValue(false); sut = new SessionSyncer(behaviorSubject, stateService, metaData); + jest.spyOn(sut as any, "debounceMs", "get").mockReturnValue(0); }); afterEach(() => { @@ -88,7 +89,7 @@ describe("session syncer", () => { sut.init(); - expect(sut["ignoreNUpdates"]).toBe(3); + expect(sut["ignoreNUpdates"]).toBe(1); }); it("should ignore BehaviorSubject's initial value", () => { @@ -128,28 +129,41 @@ describe("session syncer", () => { describe("a value is emitted on the observable", () => { let sendMessageSpy: jest.SpyInstance; - beforeEach(() => { + beforeEach(async () => { sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); sut.init(); + // allow initial value to be set + await awaitAsync(); behaviorSubject.next("test"); }); it("should update the session memory", async () => { // await finishing of fire-and-forget operation - await new Promise((resolve) => setTimeout(resolve, 100)); + await awaitAsync(); expect(stateService.setInSessionMemory).toHaveBeenCalledTimes(1); expect(stateService.setInSessionMemory).toHaveBeenCalledWith(sessionKey, "test"); }); it("should update sessionSyncers in other contexts", async () => { // await finishing of fire-and-forget operation - await new Promise((resolve) => setTimeout(resolve, 100)); + await awaitAsync(); expect(sendMessageSpy).toHaveBeenCalledTimes(1); expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, { id: sut.id }); }); + + it("should debounce subject updates", async () => { + behaviorSubject.next("test2"); + behaviorSubject.next("test3"); + + // await finishing of fire-and-forget operation + await awaitAsync(); + + expect(stateService.setInSessionMemory).toHaveBeenCalledTimes(1); + expect(stateService.setInSessionMemory).toHaveBeenCalledWith(sessionKey, "test3"); + }); }); describe("A message is received", () => { diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts index 91b371ef81..74f1857706 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts @@ -1,4 +1,11 @@ -import { BehaviorSubject, concatMap, ReplaySubject, Subject, Subscription } from "rxjs"; +import { + BehaviorSubject, + concatMap, + ReplaySubject, + Subject, + Subscription, + debounceTime, +} from "rxjs"; import { Utils } from "@bitwarden/common/misc/utils"; @@ -13,6 +20,9 @@ export class SessionSyncer { // ignore initial values private ignoreNUpdates = 0; + private get debounceMs() { + return 500; + } constructor( private subject: Subject, @@ -30,10 +40,8 @@ export class SessionSyncer { init() { switch (this.subject.constructor) { - case ReplaySubject: - // ignore all updates currently in the buffer - this.ignoreNUpdates = (this.subject as any)._buffer.length; - break; + // ignore all updates currently in the buffer + case ReplaySubject: // N = 1 due to debounce case BehaviorSubject: this.ignoreNUpdates = 1; break; @@ -58,6 +66,7 @@ export class SessionSyncer { // contexts. If so, this is handled by destruction of the context. this.subscription = this.subject .pipe( + debounceTime(this.debounceMs), concatMap(async (next) => { if (this.ignoreNUpdates > 0) { this.ignoreNUpdates -= 1; @@ -92,8 +101,15 @@ export class SessionSyncer { } private async updateSession(value: any) { - await this.stateService.setInSessionMemory(this.metaData.sessionKey, value); - await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id }); + try { + await this.stateService.setInSessionMemory(this.metaData.sessionKey, value); + await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id }); + } catch (e) { + if (e.message === "Could not establish connection. Receiving end does not exist.") { + return; + } + throw e; + } } private get updateMessageCommand() {