Постепенное возвращение результатов поиска из API-эндпоинта NextJS

Вопрос или проблема

У меня есть конечная точка /api/search в моем приложении NextJS, которая запускает 5 различных поисков для заданной строки запроса. Некоторые результаты возвращаются гораздо быстрее других, поэтому я хочу возвращать результаты по мере их готовности. Судя по всему, правильным решением будет вернуть ReadableStream из конечной точки.

Я внес это изменение, и оно работает с одним внутренним поиском. Однако, когда я пытаюсь вернуть несколько JSON-частей, они все соединяются вместе и возвращаются сразу, что, конечно, генерирует недействительный JSON.

  • Я нашел комментарий на GitHub, который предлагает использовать TextEncoder.encode(), чтобы части правильно стримились, так что теперь я это делаю. К сожалению, ничего не изменилось.
  • Исходный пример использует pull() вместо того, чтобы помещать все внутрь start(). Я попробовал это, но все результаты ПО-прежнему возвращаются вместе.
  • Похоже, что результаты всегда возвращаются в одном и том же порядке, предположительно из-за цикла for. Я на самом деле хочу, чтобы они возвращались по мере завершения. Многие примеры используют асинхронный итератор, но я не думаю, что это подойдет мне, потому что я не знаю, в каком порядке помещать элементы на этапе создания итерации.

Как я могу вернуть действительный JSON по частям по мере готовности результатов?

/app/api/search/route.js

export async function GET(request) {
    const searchParams = request.nextUrl.searchParams
    const query = searchParams.get('q')
    const year = searchParams.get('year')

    // Определите задачи для параллельного выполнения
    const tasks = [
        async () => ({ type: 'coordinates', result: await parseCoordinates(query) }),
        async () => ({ type: 'address', result: await runAddressSearch(query) }),
        async () => ({ type: 'metro', result: await runMetroSearch(year, query) }),
        async () => ({ type: 'road', result: await runRoadSearch(year, query) }),
        async () => ({ type: 'district', result: await runDistrictSearch(year, query) }),
    ];

    // Настройте ReadableStream для инкрементального ответа
    const stream = new ReadableStream({
        async start(controller) {
            const taskPromises = tasks.map((task, index) =>
                task().then(
                    (result) => ({ status: 'fulfilled', index, ...result }),
                    (error) => ({ status: 'rejected', index, error })
                )
            );

            const encoder = new TextEncoder()

            // Обработайте каждую задачу по мере ее завершения
            for (const taskPromise of taskPromises) {
                const result = await taskPromise;
                console.log("promise result:", result)

                if (result.status === 'fulfilled') {
                    if (result.result.length > 0) {
                        controller.enqueue(encoder.encode(
                            JSON.stringify({ type: result.type, result: result.result }) + '\n'
                        ));
                    }
                } else {
                    controller.enqueue(encoder.encode(
                        JSON.stringify({ type: result.type, error: result.error.toString() }) + '\n'));
                }
            }

            controller.close();
        },
    },
    // Не уверен, необходим ли этот блок кэша или корректен ли?
    {
        highWaterMark: 1,
        size(chunk) {
            return 1;
        },
    });

    return new Response(stream)
}

Ответ или решение

Возвращение результатов поиска с использованием потоков в Next.js API

В вашем запросе рассматривается создание API-метода в приложении на Next.js, который возвращает результаты поиска по запросу (query string) по мере их доступности. Это особенно актуально, когда выполнение различных поисковых операций занимает разное время. Ниже приведены основные рекомендации по реализации данной функциональности с использованием ReadableStream.

Проблема

Ваш текущий подход, при котором результаты собираются в массив и возвращаются сразу после завершения всех поисков, приводит к некорректному формату JSON. Это связано с тем, что вы не обрабатываете результаты по мере их завершения, и, следовательно, все искомые данные возвращаются как единый объект, а не последовательно.

Решение

Для эффективного потокового возврата результатов необходимо использовать ReadableStream и управлять потоком данных с помощью метода enqueue(). Ниже приведены шаги для реализации данного подхода:

  1. Асинхронные задачи: Определите асинхронные функции для запуска поисковых запросов, которые будут возвращать Promise.

  2. Использование TextEncoder: Он полезен для кодирования строк в формат, совместимый с потоками. Каждый кусочек данных, возвращаемый в стриме, должен быть корректно закодирован.

  3. Параллельная обработка: Вы можете использовать Promise.all() для запуска задач параллельно и обработать их результаты в порядке завершения. Это позволит вам выводить данные по мере их готовности.

Реализация

Ниже приведен пример кода, который демонстрирует, как организовать потоковый возврат результатов в API Next.js.

export async function GET(request) {
    const searchParams = request.nextUrl.searchParams;
    const query = searchParams.get('q');
    const year = searchParams.get('year');

    const tasks = [
        async () => ({ type: 'coordinates', result: await parseCoordinates(query) }),
        async () => ({ type: 'address', result: await runAddressSearch(query) }),
        async () => ({ type: 'metro', result: await runMetroSearch(year, query) }),
        async () => ({ type: 'road', result: await runRoadSearch(year, query) }),
        async () => ({ type: 'district', result: await runDistrictSearch(year, query) }),
    ];

    const stream = new ReadableStream({
        async start(controller) {
            const encoder = new TextEncoder();

            // Обработка задач параллельно, но возврат результатов по мере готовности
            for (const task of tasks) {
                task().then(result => {
                    if (result.result.length > 0) {
                        controller.enqueue(encoder.encode(
                            JSON.stringify({ type: result.type, result: result.result }) + '\n'
                        ));
                    } else {
                        controller.enqueue(encoder.encode(
                            JSON.stringify({ type: result.type, message: 'Нет результатов' }) + '\n'
                        ));
                    }
                }).catch(error => {
                    controller.enqueue(encoder.encode(
                        JSON.stringify({ type: 'error', error: error.toString() }) + '\n'
                    ));
                });
            }
        }
    });

    return new Response(stream, { headers: { 'Content-Type': 'application/json' } });
}

Заключение

С помощью выделенного потока ReadableStream вы сможете обеспечить возвращение результатов поиска по мере их готовности, исключив проблемы с некорректным форматом данных. Данный подход не только улучшит пользовательский опыт, но и повысит эффективность работы вашего API.

Обратите внимание на то, что использование controller.close() здесь не требуется, так как потоки должны оставаться открытыми для возврата данных по мере их генерации. Убедитесь, что вы обрабатываете возможные ошибки при выполнении поисковых запросов, чтобы поток оставался стабильным и предсказуемым.

Оцените материал
Добавить комментарий

Капча загружается...