Files
parcer/mx-interpreter/interpret.ts

140 lines
4.1 KiB
TypeScript

/* eslint-disable no-await-in-loop, no-restricted-syntax */
import { Page, PageScreenshotOptions } from 'playwright';
import path from 'path';
import { EventEmitter } from 'events';
import {
Where, What, PageState, Workflow, WorkflowFile,
ParamType, SelectorArray, CustomFunctions,
} from './types/workflow';
import { operators, meta } from './types/logic';
import { arrayToObject } from './utils/utils';
import Concurrency from './utils/concurrency';
import Preprocessor from './preprocessor';
import log, { Level } from './utils/logger';
/**
* Defines optional intepreter options (passed in constructor)
*/
interface InterpreterOptions {
maxRepeats: number;
maxConcurrency: number;
serializableCallback: (output: any) => (void | Promise<void>);
binaryCallback: (output: any, mimeType: string) => (void | Promise<void>);
debug: boolean;
debugChannel: Partial<{
activeId: Function,
debugMessage: Function,
}>
}
/**
* Class for running the Smart Workflows.
*/
export default class Interpreter extends EventEmitter {
private workflow: Workflow;
private initializedWorkflow: Workflow | null;
private options: InterpreterOptions;
private concurrency : Concurrency;
private stopper : Function | null = null;
private log : typeof log;
constructor(workflow: WorkflowFile, options?: Partial<InterpreterOptions>) {
super();
this.workflow = workflow.workflow;
this.initializedWorkflow = null;
this.options = {
maxRepeats: 5,
maxConcurrency: 5,
serializableCallback: (data) => { log(JSON.stringify(data), Level.WARN); },
binaryCallback: () => { log('Received binary data, thrashing them.', Level.WARN); },
debug: false,
debugChannel: {},
...options,
};
this.concurrency = new Concurrency(this.options.maxConcurrency);
this.log = (...args) => log(...args);
const error = Preprocessor.validateWorkflow(workflow);
if (error) {
throw (error);
}
if (this.options.debugChannel?.debugMessage) {
const oldLog = this.log;
// @ts-ignore
this.log = (...args: Parameters<typeof oldLog>) => {
if (args[1] !== Level.LOG) {
this.options.debugChannel.debugMessage!(typeof args[0] === 'string' ? args[0] : args[0].message);
}
oldLog(...args);
};
}
}
/**
* Returns the context object from given Page and the current workflow.\
* \
* `workflow` is used for selector extraction - function searches for used selectors to
* look for later in the page's context.
* @param page Playwright Page object
* @param workflow Current **initialized** workflow (array of where-what pairs).
* @returns {PageState} State of the current page.
*/
private async getState(page: Page, workflow: Workflow) : Promise<PageState> {
/**
* All the selectors present in the current Workflow
*/
const selectors = Preprocessor.extractSelectors(workflow);
/**
* Determines whether the element targetted by the selector is [actionable](https://playwright.dev/docs/actionability).
* @param selector Selector to be queried
* @returns True if the targetted element is actionable, false otherwise.
*/
const actionable = async (selector: string) : Promise<boolean> => {
try {
const proms = [
page.isEnabled(selector, { timeout: 500 }),
page.isVisible(selector, { timeout: 500 }),
];
return await Promise.all(proms).then((bools) => bools.every((x) => x));
} catch (e) {
// log(<Error>e, Level.ERROR);
return false;
}
};
/**
* Object of selectors present in the current page.
*/
const presentSelectors : SelectorArray = await Promise.all(
selectors.map(async (selector) => {
if (await actionable(selector)) {
return [selector];
}
return [];
}),
).then((x) => x.flat());
return {
url: page.url(),
cookies: (await page.context().cookies([page.url()]))
.reduce((p, cookie) => (
{
...p,
[cookie.name]: cookie.value,
}), {}),
selectors: presentSelectors,
};
}
}