diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index 9f5475df9d..ee78a5c048 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -373,7 +373,11 @@ describe("UserStateSubject", () => { singleUserId$.next(SomeUser); await awaitAsync(); - expect(state.nextMock).toHaveBeenCalledWith({ foo: "next" }); + expect(state.nextMock).toHaveBeenCalledWith({ + foo: "next", + // FIXME: don't leak this detail into the test + "$^$ALWAYS_UPDATE_KLUDGE_PROPERTY$^$": 0, + }); }); it("waits to evaluate `UserState.update` until singleUserEncryptor$ emits", async () => { @@ -394,7 +398,13 @@ describe("UserStateSubject", () => { await awaitAsync(); const encrypted = { foo: "encrypt(next)" }; - expect(state.nextMock).toHaveBeenCalledWith({ id: null, secret: encrypted, disclosed: null }); + expect(state.nextMock).toHaveBeenCalledWith({ + id: null, + secret: encrypted, + disclosed: null, + // FIXME: don't leak this detail into the test + "$^$ALWAYS_UPDATE_KLUDGE_PROPERTY$^$": 0, + }); }); it("applies dynamic constraints", async () => { diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index 845ab25c80..0b562cc7a1 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -43,6 +43,23 @@ import { UserStateSubjectDependencies } from "./user-state-subject-dependencies" type Constrained = { constraints: Readonly>; state: State }; +// FIXME: The subject should always repeat the value when it's own `next` method is called. +// +// Chrome StateService only calls `next` when the underlying values changes. When enforcing, +// say, a minimum constraint, any value beneath the minimum becomes the minimum. This prevents +// invalid data received in sequence from calling `next` because the state provider doesn't +// emit. +// +// The hack is pretty simple. Insert arbitrary data into the saved data to ensure +// that it *always* changes. +// +// Any real fix will be fairly complex because it needs to recognize *fast* when it +// is waiting. Alternatively, the kludge could become a format properly fed by random noise. +// +// NOTE: this only matters for plaintext objects; encrypted fields change with every +// update b/c their IVs change. +const ALWAYS_UPDATE_KLUDGE = "$^$ALWAYS_UPDATE_KLUDGE_PROPERTY$^$"; + /** * Adapt a state provider to an rxjs subject. * @@ -420,8 +437,25 @@ export class UserStateSubject< private inputSubscription: Unsubscribable; private outputSubscription: Unsubscribable; + private counter = 0; + private onNext(value: unknown) { - this.state.update(() => value).catch((e: any) => this.onError(e)); + this.state + .update(() => { + if (typeof value === "object") { + // related: ALWAYS_UPDATE_KLUDGE FIXME + const counter = this.counter++; + if (counter > Number.MAX_SAFE_INTEGER) { + this.counter = 0; + } + + const kludge = value as any; + kludge[ALWAYS_UPDATE_KLUDGE] = counter; + } + + return value; + }) + .catch((e: any) => this.onError(e)); } private onError(value: any) { diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts index bd26642157..b6b4307343 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts @@ -1163,7 +1163,11 @@ describe("CredentialGeneratorService", () => { await awaitAsync(); const result = await firstValueFrom(stateProvider.getUserState$(SettingsKey, SomeUser)); - expect(result).toEqual({ foo: "next value" }); + expect(result).toEqual({ + foo: "next value", + // FIXME: don't leak this detail into the test + "$^$ALWAYS_UPDATE_KLUDGE_PROPERTY$^$": 0, + }); }); it("waits for the user to become available", async () => {