From 07ff2a9d3c6eb385ad36180bc78163c71077c67f Mon Sep 17 00:00:00 2001 From: Vlad <11474041+vlad-timofeev@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:40:05 -0400 Subject: [PATCH] Fix login form issue when using password auto-fill (#5047) Signed-off-by: Vlad Timofeev <11474041+vlad-timofeev@users.noreply.github.com> --- plugins/login-resources/package.json | 2 + .../src/__tests__/mutex.test.ts | 28 +++++++ .../src/components/Form.svelte | 5 +- plugins/login-resources/src/mutex.ts | 84 +++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 plugins/login-resources/src/__tests__/mutex.test.ts create mode 100644 plugins/login-resources/src/mutex.ts diff --git a/plugins/login-resources/package.json b/plugins/login-resources/package.json index 67af413c25..873418b304 100644 --- a/plugins/login-resources/package.json +++ b/plugins/login-resources/package.json @@ -6,10 +6,12 @@ "license": "EPL-2.0", "scripts": { "build": "compile ui", + "test": "jest --passWithNoTests --silent", "build:docs": "api-extractor run --local", "format": "format src", "build:watch": "compile ui", "_phase:build": "compile ui", + "_phase:test": "jest --passWithNoTests --silent", "_phase:format": "format src", "_phase:validate": "compile validate" }, diff --git a/plugins/login-resources/src/__tests__/mutex.test.ts b/plugins/login-resources/src/__tests__/mutex.test.ts new file mode 100644 index 0000000000..58afbd34a6 --- /dev/null +++ b/plugins/login-resources/src/__tests__/mutex.test.ts @@ -0,0 +1,28 @@ +import { makeSequential } from '../mutex' + +describe('mutex', () => { + let results: number[] + + beforeEach(() => { + results = [] + }) + + async function waitAndPushResult (millis: number): Promise { + await new Promise((resolve) => { + setTimeout(resolve, millis) + }) + results.push(millis) + } + + it('expect sequential execution', async () => { + const myUpdate = makeSequential(waitAndPushResult) + await Promise.all([myUpdate(50), myUpdate(30), myUpdate(40), myUpdate(10), myUpdate(20)]) + expect(results).toEqual([50, 30, 40, 10, 20]) + }) + + it('expect parallel execution', async () => { + const myUpdate = waitAndPushResult + await Promise.all([myUpdate(50), myUpdate(30), myUpdate(40), myUpdate(10), myUpdate(20)]) + expect(results).toEqual([10, 20, 30, 40, 50]) + }) +}) diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte index 7f86b7a642..8c1f053036 100644 --- a/plugins/login-resources/src/components/Form.svelte +++ b/plugins/login-resources/src/components/Form.svelte @@ -31,6 +31,7 @@ import { onMount } from 'svelte' import { BottomAction, getHref } from '..' import login from '../plugin' + import { makeSequential } from '../mutex' import Providers from './Providers.svelte' interface Field { @@ -66,7 +67,7 @@ $: $themeStore.language && validate($themeStore.language) - async function validate (language: string): Promise { + const validate = makeSequential(async function validateAsync (language: string): Promise { if (ignoreInitialValidation) return true for (const field of fields) { const v = object[field.name] @@ -101,7 +102,7 @@ } status = OK return true - } + }) validate($themeStore.language) let inAction = false diff --git a/plugins/login-resources/src/mutex.ts b/plugins/login-resources/src/mutex.ts new file mode 100644 index 0000000000..b45530db9a --- /dev/null +++ b/plugins/login-resources/src/mutex.ts @@ -0,0 +1,84 @@ +// +// Copyright © 2022 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + Class that allows synchronization of async functions to prevent race conditions. + Inspired by https://stackoverflow.com/a/51086893 +**/ +class Mutex { + currentLock: Promise + + constructor () { + // initially the lock is not held + this.currentLock = Promise.resolve() + } + + /** + Acquires the lock. Usage example: + + ``` + let mutex = new Mutex() + + async function synchronizedFunction() { + const unlockMutex = await mutex.lock() + try { + // critical section + } finally { + unlockMutex() + } + } + ``` + + @return {Promise} promise that must be awaited in order to acquire the lock. + When the promise is fulfulled it returns a function that must be invoked to release the lock. + **/ + async lock (): Promise { + // function which invocation releases the lock + let releaseLock: VoidFunction + // this Promise is fulfilled as soon as the function above is invoked + const afterReleasePromise = new Promise((resolve) => { + releaseLock = () => { + resolve() + } + }) + // Caller gets a promise that resolves + // only after the current outstanding lock resolves + const blockingPromise = this.currentLock.then(() => releaseLock) + // Don't allow the next request until the new promise is done + this.currentLock = afterReleasePromise + // Return the new promise + return await blockingPromise + } +} + +type AnyFunction = (...args: any[]) => Promise +/** + This function wraps around the Mutex implementation above and provides a simple interface. + + @return {Promise} a sequential version of the passed function. This version guarantees + that its invocations are executed one by one. +**/ +export function makeSequential> (fn: T): (...args: Parameters) => Promise { + const mutex = new Mutex() + + return async function (...args: Parameters): Promise { + const unlockMutex = await mutex.lock() + try { + return await fn(...args) + } finally { + unlockMutex() + } + } +}