feat(Airtop Node): Implement windows list API and other improvements (#16748)

Co-authored-by: Eugene <eugene@n8n.io>
Co-authored-by: Daria <daria.staferova@n8n.io>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
Co-authored-by: Charlie Kolb <charlie@n8n.io>
Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: shortstacked <declan@n8n.io>
Co-authored-by: oleg <me@olegivaniv.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: Jaakko Husso <jaakko@n8n.io>
Co-authored-by: Raúl Gómez Morales <raul00gm@gmail.com>
Co-authored-by: Suguru Inoue <suguru@n8n.io>
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: RomanDavydchuk <roman.davydchuk@n8n.io>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Ricardo Espinoza <ricardo@n8n.io>
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
Cesar Sanchez
2025-07-04 18:56:14 +01:00
committed by GitHub
parent bb9679c4fa
commit 621745e291
21 changed files with 587 additions and 163 deletions

View File

@@ -1,18 +1,30 @@
import { NodeApiError, type IExecuteFunctions, type INode, type IDataObject } from 'n8n-workflow';
import {
NodeApiError,
type IExecuteFunctions,
type INode,
type IDataObject,
jsonParse,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type Stream from 'node:stream';
import { SESSION_MODE } from './actions/common/fields';
import type { TScrollingMode } from './constants';
import { BASE_URL, type TScrollingMode } from './constants';
import {
ERROR_MESSAGES,
DEFAULT_TIMEOUT_MINUTES,
DEFAULT_DOWNLOAD_TIMEOUT_SECONDS,
MIN_TIMEOUT_MINUTES,
MAX_TIMEOUT_MINUTES,
SESSION_STATUS,
OPERATION_TIMEOUT,
} from './constants';
import { apiRequest } from './transport';
import type { IAirtopResponse, IAirtopSessionResponse } from './transport/types';
import type {
IAirtopResponse,
IAirtopServerEvent,
IAirtopSessionResponse,
} from './transport/types';
/**
* Validate a required string field
@@ -364,7 +376,7 @@ export async function createSession(
this: IExecuteFunctions,
parameters: IDataObject,
timeout = OPERATION_TIMEOUT,
): Promise<{ sessionId: string }> {
): Promise<{ sessionId: string; data: IAirtopSessionResponse }> {
// Request session creation
const response = (await apiRequest.call(
this,
@@ -401,7 +413,12 @@ export async function createSession(
sessionStatus = sessionStatusResponse.data.status;
}
return { sessionId };
return {
sessionId,
data: {
...response,
},
};
}
/**
@@ -446,3 +463,77 @@ export async function createSessionAndWindow(
this.logger.info(`[${node.name}] Window successfully created.`);
return { sessionId, windowId };
}
/**
* SSE Helpers
*/
/**
* Parses a server event from a string
* @param eventText - The string to parse
* @returns The parsed event or null if the string is not a valid event
*/
function parseEvent(eventText: string): IAirtopServerEvent | null {
const dataLine = eventText.split('\n').find((line) => line.startsWith('data:'));
if (!dataLine) {
return null;
}
const jsonStr = dataLine.replace('data: ', '').trim();
return jsonParse<IAirtopServerEvent>(jsonStr, {
errorMessage: 'Failed to parse server event',
});
}
/**
* Waits for a session event to occur
* @param this - The execution context providing access to n8n functionality
* @param sessionId - ID of the session to check for events
* @param condition - Function to check if the event meets the condition
* @param timeoutInSeconds - Maximum time in seconds to wait before failing (defaults to DEFAULT_DOWNLOAD_TIMEOUT_SECONDS)
* @returns Promise resolving to the event when the condition is met
*/
export async function waitForSessionEvent(
this: IExecuteFunctions,
sessionId: string,
condition: (event: IAirtopServerEvent) => boolean,
timeoutInSeconds = DEFAULT_DOWNLOAD_TIMEOUT_SECONDS,
): Promise<IAirtopServerEvent> {
const url = `${BASE_URL}/sessions/${sessionId}/events?all=true`;
let stream: Stream;
const eventPromise = new Promise<IAirtopServerEvent>(async (resolve) => {
stream = (await this.helpers.httpRequestWithAuthentication.call(this, 'airtopApi', {
method: 'GET',
url,
encoding: 'stream',
})) as Stream;
stream.on('data', (data: Uint8Array) => {
const event = parseEvent(data.toString());
if (!event) {
return;
}
// handle event
if (condition(event)) {
stream.removeAllListeners();
resolve(event);
return;
}
});
});
const timeoutPromise = new Promise<void>((_resolve, reject) => {
setTimeout(() => {
reject(
new NodeApiError(this.getNode(), {
message: ERROR_MESSAGES.TIMEOUT_REACHED,
code: 500,
}),
);
stream.removeAllListeners();
}, timeoutInSeconds * 1000);
});
const result = await Promise.race([eventPromise, timeoutPromise]);
return result as IAirtopServerEvent;
}