Javascript-форум (https://javascript.ru/forum/)
-   Общие вопросы Javascript (https://javascript.ru/forum/misc/)
-   -   Помогите решить задачу (https://javascript.ru/forum/misc/85773-pomogite-reshit-zadachu.html)

AlexandrDr 27.02.2024 09:08

Помогите решить задачу
 
Есть следующий код:

class DummyClient {
    async connect() {
        return new Promise(resolve=>{
            setTimeout(()=>{
                console.log('connect');
                resolve()
            }
            , 2000);
        }
        )
    }

    async doSomething() {
        console.log('done something')
    }
}

class ApiWrapper {
    #client;

    async getClient() {
        if (!this.#client) {
            const client = new DummyClient();
            await client.connect();
            this.#client = client;
        }
        return this.#client;
    }

    async doSomething() {
        const client = await this.getClient();
        return client.doSomething();
    }
}

const run = async()=>{
    const api = new ApiWrapper();

    await Promise.all([api.doSomething(), api.doSomething(), api.doSomething(), api.doSomething()]);
}

run();


сейчас он выводит в консоль
connect
done something
connect
done something
connect
done something
connect
done something

Нужно чтоб было
connect
done something
done something
done something
done something

Исправлять можно только код внутри класса ApiWrapper.
Я изменяю класс вот так:

class ApiWrapper {
    #client;

    async getClient() {
        if (!this.#client) {
            this.#client = new DummyClient();
            this.#client.connect()
        }
        return this.#client
    }

    async doSomething() {
        const client = await this.getClient();
        await client.doSomething();
    }
}

но в выводе у меня нарушенный порядок
done something
done something
done something
done something
connect

Не могу понять в чем проблема, понимаю что у меня сначала исполняется синхронный код, а потом результат промиса. как исправить не понимаю пока

Aetae 27.02.2024 12:35

Очевидно, в том что потерял await. Буквально "подождать".
Было "подождать this.#client.connect()", стало просто "this.#client.connect()".
Потому в первом случае он сначала коннектит, потом уж что-то делает, а во втором - он что-то делает, а потом уже когда-нибудь конектит.

AlexandrDr 27.02.2024 12:45

Вложений: 1
В таком случае порядок все равно неверный получается.

Выводит

done something
done something
done something
connect
done something

а нужно

connect
done something
done something
done something
done something

AlexandrDr 27.02.2024 12:47

class ApiWrapper {
    #client;

    async getClient() {
        if (!this.#client) {
            this.#client = new DummyClient();
            this.#client.connect()
        }
        return this.#client
    }

    async doSomething() {
        const client = await this.getClient();
        setTimeout(client.doSomething, 4000)
    }
}


вот так вывод получается правильный, но мне сказали это костыльное решение и надо по-другому. Не могу понять как

Aetae 27.02.2024 13:47

А, ну да, задачка забавная, на понимание. Утащу к себе, буду на собеседованиях давать. Решается просто.:⁠)

Вот тебе промежуточное решение:
async getClient() {
  if (!this.#client) {
    const client = new DummyClient();
    this.#client = client.connect().then(() => client);
  }
  return this.#client;
}

Оно будет работать и, в принципе, может считаться законченным, но семантически не красиво.:⁠)

AlexandrDr 27.02.2024 14:21

спасибо

ksa 29.02.2024 11:43

Предложу такой вариант...

class DummyClient {
    connect() {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log('connect');
                resolve()
            }
            , 2000);
        })
    }

    async doSomething() {
        console.log('done something')
    }
}

class ApiWrapper {
    #client;
	#connect = false

    async getClient() {
		this.connect = true
		const client = new DummyClient();
		await client.connect();
		this.#client = client;
    }

    async doSomething(n) {
        while (!this.#client) {
			if (!this.connect) await this.getClient();
			await this.pause(100)
		}
		const v = await this.#client.doSomething();
		return v
    }
	pause(t) {
        return new Promise(resolve => {
            setTimeout(resolve, t);
        })
	}
}

const run = async()=>{
    const api = new ApiWrapper();

    await Promise.all([
		api.doSomething(1), 
		api.doSomething(2), 
		api.doSomething(3), 
		api.doSomething(4)
	]);
}

run();

Aetae 01.03.2024 04:48

ksa, лучше решения автора только тем, что гарантировано работает. На моём собесе ты бы получил маленький минус(не окончательный) за такое решение.:⁠) Никакой элегантности, левые не нужные таймеры...

ksa 01.03.2024 09:20

Цитата:

Сообщение от Aetae
Никакой элегантности, левые не нужные таймеры...

Я не особо спец в написании классов... :cray:
Использую "функциональное" программирование. Стараюсь тренироваться по мере возможностей.

Такой вот еще вариант сделал, но там опять есть pause.

class DummyClient {
    async connect() {
        return new Promise(resolve=>{
            setTimeout(()=>{
                console.log('connect');
                resolve()
            }
            , 2000);
        }
        )
    }

    async doSomething() {
        console.log('done something')
    }
}

class ApiWrapper extends DummyClient {
	#connect = null
    async doSomething() {
		while (!this.#connect) {
			if (this.#connect == null) {
				this.#connect = false
				await this.connect()
				this.#connect = true
			}
			await this.pause(100)
		}
        await super.doSomething()
    }
	pause(t) {
		return new Promise(res => setTimeout(res, t))
	}
}

const run = async () => {
    const api = new ApiWrapper();

    await Promise.all([
		api.doSomething(), 
		api.doSomething(), 
		api.doSomething(), 
		api.doSomething()
	]);
}

run();

ksa 01.03.2024 09:21

Aetae, покажи свой вариант полностью. Как ты сделал тот класс ApiWrapper?

roland 01.03.2024 10:20

class ApiWrapper {
  #client
  #connect

  constructor() {
    this.#client = new DummyClient()
    this.#connect = this.#client.connect()
  }

  async doSomething() {
    await this.#connect
    this.#client.doSomething()
  }
}


Цитата:

Сообщение от Aetae (Сообщение 554839)
ksa, На моём собесе ты бы получил маленький минус(не окончательный) за такое решение.:⁠) Никакой элегантности, левые не нужные таймеры...

У ksa решение простое и читаемое, хоть далеко и не оптимальное.

У Вас же, Aetae, решение проигрывает в простоте и читаемости. Конечно, отчасти это вина первоначального кода, но здесь же не соревнование в скорости написания решений. Для оценки "элегантности" в контексте собеседования решение от самого интервьюера должно быть как минимум простым, читаемым и сопровождаемым.

ksa 01.03.2024 10:32

roland, во. Я теперь понял чего в супе не хватало... :yes:
Я как раз и не понимал как соединение можно сделать через конструктор.

Nexus 01.03.2024 14:11

Я бы сделал как-то так, но метод connect обертки у меня явно костыльный, т.к., имхо, хранить состояние соединения должен сам клиент, а не пытаться открыть соединение на каждый вызов метода `connect`:
interface IClient {
    connect(): Promise<void>;

    doSomething(): Promise<void>;
}

declare const DummyClient: new() => IClient;

class ApiWrapper {
    #client!: IClient;

    #connectionPromise: Promise<void> | null = null;
    #connectionState: 'connected' | 'connecting' | 'disconnected' = 'disconnected';

    constructor(client?: IClient) {
        this.#client = client ?? new DummyClient();
    }

    async connect() {
        if (this.#connectionState !== 'disconnected') {
            return this.#connectionPromise;
        }

        this.#connectionState = 'connecting';

        return this.#connectionPromise = this.#client.connect().then(v => {
            this.#connectionState = 'connected';

            return v;
        }).catch(e => {
            this.#connectionState = 'disconnected';

            throw e;
        });
    }

    async doSomething() {
        await this.connect();

        return this.#client.doSomething();
    }
}


ts playground

Aetae 02.03.2024 13:02

Nexus, +.

ksa, ну вот примерно как Nexus бы сделал, только выкинул бы возврат оригинального connect (и занулял бы connectionPromise по завершению, чтоб не висело в памяти: что бы там оно не возвращало - оно нам не нужно с гарантией 99%). Ну и connectionPromise назвал бы pendingConnection или просто pending. Но это всё фигня. :⁠)

Кстати нашёл пока лучшее применение chat gpt - придумывать названия переменным и функциям. Решил самую сложную задачу в программировании можно сказать.:⁠)

P.S. В тему о промисах - знает кто-нить готовую либу с нормальной поддержкой, которая бы умела бы делать debounce асинхронной функции, но не простой, а чтоб если таймер вышел - он всё равно не позволял повторного запуска пока предыдущий асинхронный вызов не завершится. Т.е. допустим у нас
const debounced = debounce(async (arg) => {
  console.log(arg);
  await delay(1000);
}, 100);
debounced(1);
await delay(300);
debounced(2);
await delay(300);
debounced(3);

// большинство либ - 1, 2, 3
// иногда - 1, но это debounce, а throttle
// надо - 1, 3

Второй вариант который тоже нужен - более стандрантый, но споддрежкой AbortControler'a, чтоб если таймер вышел и снова вызвали когда предыдущий промис ещё идёт - тригеррил AbortControler.abort();:
// надо - 1/aborted, 2/aborted, 3


Сейчас пишу свою версию, но там не всё так просто..:⁠)

ksa 03.03.2024 10:50

Пока мне больше всего нравится вариант от камрада roland. :)

Alexandroppolus 03.03.2024 21:01

Цитата:

Сообщение от Nexus
this.#client = client ?? new DummyClient();

имхо, плохая идея. Наметившееся соблюдение DIP сразу идет лесом.

а так, согласен. Хотя, возможно, connectionPromise лучше хранить вообще в самом клиенте. Типа, нужно новое подключение - создали новый клиент.

Alexandroppolus 03.03.2024 21:06

Цитата:

Сообщение от Aetae
// большинство либ - 1, 2, 3

// надо - 1, 3

т.е. в этом примере те либы рисуют 1, 2, 3 с интервалами 300 мс, а тебе нужно, чтобы сначала 2 встала на ожидание, потом её по-тихому сменила 3, и в итоге запустилась 3?

Nexus 04.03.2024 16:46

Цитата:

Сообщение от Alexandroppolus
имхо, плохая идея. Наметившееся соблюдение DIP сразу идет лесом.

Вот тут, откровенно, не понял. Как зависимость класса нарушает принцип инверсии зависимостей?
Или вы про дефолтное значение?

Если второе, то это из-за условий автора:
Цитата:

Сообщение от AlexandrDr
Исправлять можно только код внутри класса ApiWrapper.


Alexandroppolus 04.03.2024 17:29

Nexus,
а, точно. Не заметил этого странного условия про ApiWrapper.

да, я говорил про дефолтное значение.

Aetae 04.03.2024 19:27

Цитата:

Сообщение от Alexandroppolus (Сообщение 554877)
т.е. в этом примере те либы рисуют 1, 2, 3 с интервалами 300 мс, а тебе нужно, чтобы сначала 2 встала на ожидание, потом её по-тихому сменила 3, и в итоге запустилась 3?

Вроде так.
Я таки нашёл похожее на то что надо:
1й вариант с 1, 3: perfect-debounce.
Из минусов - промисы завершаются не по порядку: при завершении 1 и начале 3: "одновременно" отстреливаются все предыдущее debunced в порядке 2, 3, 1 с возвратом 1. (возврат 3 ждёт следующих обращений)
Поведение своеобразное, на на мои хотелки ложилось хорошо.

2й вариант с 1/aborted, 2/aborted, 3: awesome-debounce-promise
Только он не использует signal, а свой cancel который оставляет первые два в вечном pending. Довольно интересное, хотя и нишевое решение.:)

Вообще интересное исследование получилось, о том что разные люди хотят от казалось-бы простой штуки debounce.
В итоге всё равно самописку на signal использовал.:)


Часовой пояс GMT +3, время: 17:33.