@stubber/virtual-worker 1.5.5 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ /* eslint-disable id-match */
2
+ import { create_error_conceptual } from "#app/functions/create_error_conceptual.js";
3
+ import { create_error_technical } from "#app/functions/create_error_technical.js";
4
+ import { create_success } from "#app/functions/create_success.js";
5
+ import { get_chromium_page } from "../../helpers/get_chromium_page.js";
6
+ import * as playwright from "playwright";
7
+
8
+ const DEFAULT_TIMEOUT_MS = 30000;
9
+ const MAX_SERIALIZATION_DEPTH = 6;
10
+ const MAX_SERIALIZATION_KEYS = 100;
11
+ const async_function_constructor = async function () {}.constructor;
12
+
13
+ /**
14
+ * Runs user-provided Playwright code against the current Chromium page.
15
+ * The code is executed as the body of an async function, so `await` and `return`
16
+ * can be used directly.
17
+ *
18
+ * Exposed variables inside the execution context:
19
+ * - `page`: current Playwright page
20
+ * - `context`: the page's BrowserContext
21
+ * - `browser`: the underlying browser instance, if available
22
+ * - `playwright`: Playwright module namespace
23
+ * - `args`: arbitrary user-provided arguments
24
+ * - `params`: original params object
25
+ * - `stubber_context`: task stubber context
26
+ *
27
+ * This runs in-process by design. Isolation is expected to come from the
28
+ * worker container, not from a JavaScript sandbox.
29
+ * @param {Object} params
30
+ * @param {string} params.code - The Playwright code to execute
31
+ * @param {Object} [params.args] - Optional arguments to pass to the code
32
+ * @param {number} [params.timeout_ms] - Optional timeout for code execution in milliseconds (default: 30000)
33
+ *
34
+ *
35
+ */
36
+ export const browser_execute = async (params, stubber_context) => {
37
+ if (!params || typeof params.code !== "string" || !params.code.trim()) {
38
+ return create_error_conceptual({ message: "Missing required parameter: code" });
39
+ }
40
+
41
+ const page_result = await get_chromium_page(params, stubber_context);
42
+ if (!page_result.success) {
43
+ return page_result;
44
+ }
45
+
46
+ const page = page_result.payload;
47
+ const context = page.context();
48
+ const browser = context.browser();
49
+ const timeout_ms = normalize_timeout(params.timeout_ms);
50
+
51
+ if (timeout_ms === null) {
52
+ return create_error_conceptual({
53
+ message: "timeout_ms must be a non-negative number",
54
+ details: { timeout_ms: params.timeout_ms },
55
+ });
56
+ }
57
+
58
+ try {
59
+ const logs = [];
60
+ const execution_console = create_execution_console(logs);
61
+ const execution = run_user_code(params.code, {
62
+ page,
63
+ context,
64
+ browser,
65
+ playwright,
66
+ params,
67
+ args: params.args || {},
68
+ console: execution_console,
69
+ setTimeout,
70
+ clearTimeout,
71
+ setInterval,
72
+ clearInterval,
73
+ URL,
74
+ URLSearchParams,
75
+ TextEncoder,
76
+ TextDecoder,
77
+ AbortController,
78
+ AbortSignal,
79
+ });
80
+
81
+ const raw_result = await with_timeout(Promise.resolve(execution), timeout_ms);
82
+ const result = serialize_for_payload(raw_result);
83
+
84
+ return create_success({
85
+ message: "Playwright code executed successfully",
86
+ payload: {
87
+ result,
88
+ logs,
89
+ },
90
+ });
91
+ } catch (error) {
92
+ return create_error_technical(error);
93
+ }
94
+ };
95
+
96
+ /**
97
+ * @param {string} code
98
+ * @param {object} execution_scope
99
+ */
100
+ const run_user_code = (code, execution_scope) => {
101
+ const parameter_names = Object.keys(execution_scope);
102
+ const parameter_values = Object.values(execution_scope);
103
+ const execute = new async_function_constructor(...parameter_names, code);
104
+
105
+ return execute(...parameter_values);
106
+ };
107
+
108
+ /**
109
+ * @param {unknown} timeout_ms
110
+ * @returns {number|null}
111
+ */
112
+ const normalize_timeout = (timeout_ms) => {
113
+ if (!timeout_ms) {
114
+ return DEFAULT_TIMEOUT_MS;
115
+ }
116
+
117
+ const normalized = Number(timeout_ms);
118
+
119
+ if (Number.isNaN(normalized) || normalized < 0) {
120
+ return null;
121
+ }
122
+
123
+ return normalized;
124
+ };
125
+
126
+ /**
127
+ * @param {Promise<unknown>} promise
128
+ * @param {number} timeout_ms
129
+ */
130
+ const with_timeout = async (promise, timeout_ms) => {
131
+ if (timeout_ms === 0) {
132
+ return await promise;
133
+ }
134
+
135
+ let timeout_id;
136
+
137
+ const timeout_promise = new Promise((_, reject) => {
138
+ timeout_id = setTimeout(() => {
139
+ reject(new Error(`Playwright code execution timed out after ${timeout_ms}ms`));
140
+ }, timeout_ms);
141
+ });
142
+
143
+ try {
144
+ return await Promise.race([promise, timeout_promise]);
145
+ } finally {
146
+ clearTimeout(timeout_id);
147
+ }
148
+ };
149
+
150
+ /**
151
+ * @param {Array<{ level: string, args: unknown[] }>} logs
152
+ */
153
+ const create_execution_console = (logs) => {
154
+ return {
155
+ debug: (...args) => log_execution("debug", args, logs),
156
+ info: (...args) => log_execution("info", args, logs),
157
+ log: (...args) => log_execution("log", args, logs),
158
+ warn: (...args) => log_execution("warn", args, logs),
159
+ error: (...args) => log_execution("error", args, logs),
160
+ };
161
+ };
162
+
163
+ /**
164
+ * @param {string} level
165
+ * @param {unknown[]} args
166
+ * @param {Array<{ level: string, args: unknown[] }>} logs
167
+ */
168
+ const log_execution = (level, args, logs) => {
169
+ const serialized_args = args.map((arg) => serialize_for_payload(arg));
170
+ logs.push({ level, args: serialized_args });
171
+
172
+ const logger = console[level] || console.log;
173
+ logger("[browser_execute]", ...serialized_args);
174
+ };
175
+
176
+ /**
177
+ * @param {unknown} value
178
+ * @param {WeakSet<object>} [seen]
179
+ * @param {number} [depth]
180
+ * @returns {unknown}
181
+ */
182
+ const serialize_for_payload = (value, seen = new WeakSet(), depth = 0) => {
183
+ if (value === null || value === undefined) {
184
+ return value ?? null;
185
+ }
186
+
187
+ if (depth >= MAX_SERIALIZATION_DEPTH) {
188
+ return `[MaxDepth:${get_constructor_name(value)}]`;
189
+ }
190
+
191
+ const value_type = typeof value;
192
+
193
+ if (value_type === "string" || value_type === "number" || value_type === "boolean") {
194
+ return value;
195
+ }
196
+
197
+ if (value_type === "bigint") {
198
+ return value.toString();
199
+ }
200
+
201
+ if (value_type === "function") {
202
+ return `[Function:${value.name || "anonymous"}]`;
203
+ }
204
+
205
+ if (Array.isArray(value)) {
206
+ return value.map((item) => serialize_for_payload(item, seen, depth + 1));
207
+ }
208
+
209
+ if (value instanceof Error) {
210
+ return {
211
+ name: value.name,
212
+ message: value.message,
213
+ stack: value.stack?.split("\n").slice(0, 2).join("\n"),
214
+ };
215
+ }
216
+
217
+ if (value instanceof Date) {
218
+ return value.toISOString();
219
+ }
220
+
221
+ if (value_type === "object") {
222
+ if (seen.has(value)) {
223
+ return `[Circular:${get_constructor_name(value)}]`;
224
+ }
225
+
226
+ seen.add(value);
227
+
228
+ const playwright_summary = summarize_playwright_object(value);
229
+ if (playwright_summary) {
230
+ return playwright_summary;
231
+ }
232
+
233
+ const result = {};
234
+ const entries = Object.entries(value).slice(0, MAX_SERIALIZATION_KEYS);
235
+ for (const [key, entry_value] of entries) {
236
+ result[key] = serialize_for_payload(entry_value, seen, depth + 1);
237
+ }
238
+
239
+ return result;
240
+ }
241
+
242
+ return String(value);
243
+ };
244
+
245
+ /**
246
+ * @param {unknown} value
247
+ */
248
+ const get_constructor_name = (value) => {
249
+ return value && typeof value === "object" && value.constructor?.name ? value.constructor.name : typeof value;
250
+ };
251
+
252
+ /**
253
+ * @param {unknown} value
254
+ * @returns {object|null}
255
+ */
256
+ const summarize_playwright_object = (value) => {
257
+ const constructor_name = get_constructor_name(value);
258
+
259
+ if (constructor_name === "Page") {
260
+ return {
261
+ type: "Page",
262
+ url: safely_call(value, "url"),
263
+ };
264
+ }
265
+
266
+ if (constructor_name === "BrowserContext") {
267
+ const pages = safely_call(value, "pages");
268
+
269
+ return {
270
+ type: "BrowserContext",
271
+ page_count: Array.isArray(pages) ? pages.length : null,
272
+ };
273
+ }
274
+
275
+ if (constructor_name === "Browser") {
276
+ return {
277
+ type: "Browser",
278
+ is_connected: safely_call(value, "isConnected"),
279
+ };
280
+ }
281
+
282
+ if (constructor_name === "Locator") {
283
+ return {
284
+ type: "Locator",
285
+ };
286
+ }
287
+
288
+ if (constructor_name === "ElementHandle") {
289
+ return {
290
+ type: "ElementHandle",
291
+ };
292
+ }
293
+
294
+ return null;
295
+ };
296
+
297
+ /**
298
+ * @param {unknown} value
299
+ * @param {string} method_name
300
+ */
301
+ const safely_call = (value, method_name) => {
302
+ try {
303
+ if (value && typeof value[method_name] === "function") {
304
+ return value[method_name]();
305
+ }
306
+ } catch {
307
+ return null;
308
+ }
309
+
310
+ return null;
311
+ };
@@ -17,6 +17,7 @@ import { api_proxy } from "./api_proxy/api_proxy.js";
17
17
  import { browser_extract_markdown } from "./browser/browser_extract_markdown.js";
18
18
  import { browser_wait } from "./browser/browser_wait.js";
19
19
  import { browser_find } from "./browser/browser_find.js";
20
+ import { browser_execute } from "./browser/browser_execute.js";
20
21
 
21
22
  const all_commands = {
22
23
  browser_click,
@@ -32,6 +33,7 @@ const all_commands = {
32
33
  browser_manage_sessions,
33
34
  browser_wait,
34
35
  browser_find,
36
+ browser_execute,
35
37
 
36
38
  cli_run,
37
39
 
@@ -0,0 +1,70 @@
1
+ @baseUrl = http://localhost:3000
2
+ @apiKey = 123-456-789
3
+ @orguuid = c8cfd7f1-8015-43ff-8878-d22c136a2325
4
+ @stubref = my-stub-ref
5
+
6
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
7
+ Content-Type: application/json
8
+ stubber-virtual-worker-apikey: {{apiKey}}
9
+
10
+ {
11
+ "task": {
12
+ "tasktype": "virtual_worker_send_commands",
13
+ "task_name": "virtual_worker_send_commands",
14
+ "params": {
15
+ "commands": {
16
+ "command_1": {
17
+ "commandtype": "browser_navigate",
18
+ "params": {
19
+ "url": "https://en.wikipedia.org/wiki/Main_Page"
20
+ }
21
+ },
22
+ "command_2": {
23
+ "commandtype": "browser_execute",
24
+ "params": {
25
+ "args": {
26
+ "selector": "h1"
27
+ },
28
+ "code": "const title = await page.title();\nconst headings = await page.locator(args.selector).allInnerTexts();\nconsole.log({ title, headings });\nreturn { title, headings, url: page.url() };"
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "_stubber": {
34
+ "orguuid": "{{orguuid}}",
35
+ "stubref": "{{stubref}}"
36
+ }
37
+ }
38
+ }
39
+
40
+ ###
41
+
42
+ # Run eval on the current page context.
43
+ # Assumes the virtual worker already has an active browser page for this stub.
44
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
45
+ Content-Type: application/json
46
+ stubber-virtual-worker-apikey: {{apiKey}}
47
+
48
+ {
49
+ "task": {
50
+ "tasktype": "virtual_worker_send_commands",
51
+ "task_name": "virtual_worker_send_commands",
52
+ "params": {
53
+ "commands": {
54
+ "command_1": {
55
+ "commandtype": "browser_execute",
56
+ "params": {
57
+ "args": {
58
+ "expression": "document.title"
59
+ },
60
+ "code": "const result = await page.evaluate((expression) => eval(expression), args.expression);\nreturn { expression: args.expression, result, url: page.url() };"
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "_stubber": {
66
+ "orguuid": "{{orguuid}}",
67
+ "stubref": "{{stubref}}"
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,39 @@
1
+ @baseUrl = http://localhost:3000
2
+ @apiKey = 123-456-789
3
+ @orguuid = c8cfd7f1-8015-43ff-8878-d22c136a2325
4
+ @stubref = my-stub-ref
5
+
6
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
7
+ Content-Type: application/json
8
+ stubber-virtual-worker-apikey: {{apiKey}}
9
+
10
+ {
11
+ "task": {
12
+ "tasktype": "virtual_worker_send_commands",
13
+ "task_name": "virtual_worker_send_commands",
14
+ "params": {
15
+ "commands": {
16
+ "command_1": {
17
+ "commandtype": "browser_navigate",
18
+ "params": {
19
+ "url": "https://fast.com"
20
+ }
21
+ },
22
+ "command_2": {
23
+ "commandtype": "browser_wait",
24
+ "params": {
25
+ "ms": 2000
26
+ }
27
+ },
28
+ "command_3": {
29
+ "commandtype": "browser_extract_markdown",
30
+ "params": {}
31
+ }
32
+ }
33
+ },
34
+ "_stubber": {
35
+ "orguuid": "{{orguuid}}",
36
+ "stubref": "{{stubref}}"
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,39 @@
1
+ @baseUrl = http://localhost:3000
2
+ @apiKey = 123-456-789
3
+ @orguuid = c8cfd7f1-8015-43ff-8878-d22c136a2325
4
+ @stubref = my-stub-ref
5
+
6
+ POST {{baseUrl}}/api/v1/task-gateway/virtual_worker_send_commands
7
+ Content-Type: application/json
8
+ stubber-virtual-worker-apikey: {{apiKey}}
9
+
10
+ {
11
+ "task": {
12
+ "tasktype": "virtual_worker_send_commands",
13
+ "task_name": "virtual_worker_send_commands",
14
+ "params": {
15
+ "commands": {
16
+ "command_1": {
17
+ "commandtype": "browser_navigate",
18
+ "params": {
19
+ "url": "https://berkshirehathaway.com"
20
+ }
21
+ },
22
+ "command_2": {
23
+ "commandtype": "browser_find",
24
+ "params": {
25
+ "text": {
26
+ "pattern": "Warren.*Buffett",
27
+ "flags": "i"
28
+ },
29
+ "parent_levels": 1
30
+ }
31
+ }
32
+ }
33
+ },
34
+ "_stubber": {
35
+ "orguuid": "{{orguuid}}",
36
+ "stubref": "{{stubref}}"
37
+ }
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubber/virtual-worker",
3
- "version": "1.5.5",
3
+ "version": "1.6.0",
4
4
  "description": "Template to easily create a node app and keep development standards",
5
5
  "main": "app.js",
6
6
  "directories": {
package/devel/tests.sh DELETED
@@ -1,68 +0,0 @@
1
- curl -X POST "http://localhost:3000/api/v1/task-gateway/virtual_worker_send_commands" \
2
- -H "Content-Type: application/json" \
3
- -H "stubber-virtual-worker-apikey: 123-456-789" \
4
- -d '{
5
- "task": {
6
- "tasktype": "virtual_worker_send_commands",
7
- "task_name": "virtual_worker_send_commands",
8
- "params": {
9
- "commands": {
10
- "command_1": {
11
- "commandtype": "browser_navigate",
12
- "params": {
13
- "url": "https://fast.com"
14
- }
15
- },
16
- "command_2": {
17
- "commandtype": "browser_wait",
18
- "params": {
19
- "ms": 2000
20
- }
21
- },
22
- "command_3": {
23
- "commandtype": "browser_extract_markdown",
24
- "params": {
25
- }
26
- }
27
- }
28
- },
29
- "_stubber": {
30
- "orguuid": "c8cfd7f1-8015-43ff-8878-d22c136a2325",
31
- "stubref": "my-stub-ref"
32
- }
33
- }
34
- }'
35
-
36
- curl -X POST "http://localhost:3000/api/v1/task-gateway/virtual_worker_send_commands" \
37
- -H "Content-Type: application/json" \
38
- -H "stubber-virtual-worker-apikey: 123-456-789" \
39
- -d '{
40
- "task": {
41
- "tasktype": "virtual_worker_send_commands",
42
- "task_name": "virtual_worker_send_commands",
43
- "params": {
44
- "commands": {
45
- "command_1": {
46
- "commandtype": "browser_navigate",
47
- "params": {
48
- "url": "https://berkshirehathaway.com"
49
- }
50
- },
51
- "command_2": {
52
- "commandtype": "browser_find",
53
- "params": {
54
- "text": {
55
- "pattern": "Warren.*Buffett",
56
- "flags": "i"
57
- },
58
- "parent_levels": 1
59
- }
60
- }
61
- }
62
- },
63
- "_stubber": {
64
- "orguuid": "c8cfd7f1-8015-43ff-8878-d22c136a2325",
65
- "stubref": "my-stub-ref"
66
- }
67
- }
68
- }'