@jackwener/opencli 0.3.0 → 0.4.1

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.
@@ -5,6 +5,23 @@
5
5
  import type { IPage } from '../../types.js';
6
6
  import { render } from '../template.js';
7
7
 
8
+ /** Simple async concurrency limiter */
9
+ async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
10
+ const results: R[] = new Array(items.length);
11
+ let index = 0;
12
+
13
+ async function worker() {
14
+ while (index < items.length) {
15
+ const i = index++;
16
+ results[i] = await fn(items[i], i);
17
+ }
18
+ }
19
+
20
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
21
+ await Promise.all(workers);
22
+ return results;
23
+ }
24
+
8
25
  /** Single URL fetch helper */
9
26
  async function fetchSingle(
10
27
  page: IPage | null, url: string, method: string,
@@ -39,6 +56,46 @@ async function fetchSingle(
39
56
  `);
40
57
  }
41
58
 
59
+ /**
60
+ * Batch fetch: send all URLs into the browser as a single evaluate() call.
61
+ * This eliminates N-1 cross-process IPC round trips, performing all fetches
62
+ * inside the V8 engine and returning results as one JSON array.
63
+ */
64
+ async function fetchBatchInBrowser(
65
+ page: IPage, urls: string[], method: string,
66
+ headers: Record<string, string>, concurrency: number,
67
+ ): Promise<any[]> {
68
+ const headersJs = JSON.stringify(headers);
69
+ const urlsJs = JSON.stringify(urls);
70
+ return page.evaluate(`
71
+ async () => {
72
+ const urls = ${urlsJs};
73
+ const method = "${method}";
74
+ const headers = ${headersJs};
75
+ const concurrency = ${concurrency};
76
+
77
+ const results = new Array(urls.length);
78
+ let idx = 0;
79
+
80
+ async function worker() {
81
+ while (idx < urls.length) {
82
+ const i = idx++;
83
+ try {
84
+ const resp = await fetch(urls[i], { method, headers, credentials: "include" });
85
+ results[i] = await resp.json();
86
+ } catch (e) {
87
+ results[i] = { error: e.message };
88
+ }
89
+ }
90
+ }
91
+
92
+ const workers = Array.from({ length: Math.min(concurrency, urls.length) }, () => worker());
93
+ await Promise.all(workers);
94
+ return results;
95
+ }
96
+ `);
97
+ }
98
+
42
99
  export async function stepFetch(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
43
100
  const urlOrObj = typeof params === 'string' ? params : (params?.url ?? '');
44
101
  const method = params?.method ?? 'GET';
@@ -48,12 +105,33 @@ export async function stepFetch(page: IPage | null, params: any, data: any, args
48
105
 
49
106
  // Per-item fetch when data is array and URL references item
50
107
  if (Array.isArray(data) && urlTemplate.includes('item')) {
51
- const results: any[] = [];
52
- for (let i = 0; i < data.length; i++) {
53
- const itemUrl = String(render(urlTemplate, { args, data, item: data[i], index: i }));
54
- results.push(await fetchSingle(page, itemUrl, method, queryParams, headers, args, data));
108
+ const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 5;
109
+
110
+ // Render all URLs upfront
111
+ const renderedHeaders: Record<string, string> = {};
112
+ for (const [k, v] of Object.entries(headers)) renderedHeaders[k] = String(render(v, { args, data }));
113
+ const renderedParams: Record<string, string> = {};
114
+ for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));
115
+
116
+ const urls = data.map((item: any, index: number) => {
117
+ let url = String(render(urlTemplate, { args, data, item, index }));
118
+ if (Object.keys(renderedParams).length > 0) {
119
+ const qs = new URLSearchParams(renderedParams).toString();
120
+ url = `${url}${url.includes('?') ? '&' : '?'}${qs}`;
121
+ }
122
+ return url;
123
+ });
124
+
125
+ // BATCH IPC: if browser is available, batch all fetches into a single evaluate() call
126
+ if (page !== null) {
127
+ return fetchBatchInBrowser(page, urls, method.toUpperCase(), renderedHeaders, concurrency);
55
128
  }
56
- return results;
129
+
130
+ // Non-browser: use concurrent pool (already optimized)
131
+ return mapConcurrent(data, concurrency, async (item, index) => {
132
+ const itemUrl = String(render(urlTemplate, { args, data, item, index }));
133
+ return fetchSingle(null, itemUrl, method, queryParams, headers, args, data);
134
+ });
57
135
  }
58
136
  const url = render(urlOrObj, { args, data });
59
137
  return fetchSingle(page, String(url), method, queryParams, headers, args, data);
@@ -42,6 +42,8 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
42
42
  async () => {
43
43
  // ── 1. Setup capture proxy (fetch + XHR dual interception) ──
44
44
  let captured = null;
45
+ let captureResolve;
46
+ const capturePromise = new Promise(r => { captureResolve = r; });
45
47
  const capturePattern = ${JSON.stringify(capturePattern)};
46
48
 
47
49
  // Intercept fetch API
@@ -52,7 +54,7 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
52
54
  const url = typeof fetchArgs[0] === 'string' ? fetchArgs[0]
53
55
  : fetchArgs[0] instanceof Request ? fetchArgs[0].url : String(fetchArgs[0]);
54
56
  if (capturePattern && url.includes(capturePattern) && !captured) {
55
- try { captured = await resp.clone().json(); } catch {}
57
+ try { captured = await resp.clone().json(); captureResolve(); } catch {}
56
58
  }
57
59
  } catch {}
58
60
  return resp;
@@ -71,13 +73,13 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
71
73
  const origHandler = xhr.onreadystatechange;
72
74
  xhr.onreadystatechange = function() {
73
75
  if (xhr.readyState === 4 && !captured) {
74
- try { captured = JSON.parse(xhr.responseText); } catch {}
76
+ try { captured = JSON.parse(xhr.responseText); captureResolve(); } catch {}
75
77
  }
76
78
  if (origHandler) origHandler.apply(this, arguments);
77
79
  };
78
80
  const origOnload = xhr.onload;
79
81
  xhr.onload = function() {
80
- if (!captured) { try { captured = JSON.parse(xhr.responseText); } catch {} }
82
+ if (!captured) { try { captured = JSON.parse(xhr.responseText); captureResolve(); } catch {} }
81
83
  if (origOnload) origOnload.apply(this, arguments);
82
84
  };
83
85
  }
@@ -117,9 +119,9 @@ export async function stepTap(page: IPage, params: any, data: any, args: Record<
117
119
  await ${actionCall};
118
120
 
119
121
  // ── 4. Wait for network response ──
120
- const deadline = Date.now() + ${timeout} * 1000;
121
- while (!captured && Date.now() < deadline) {
122
- await new Promise(r => setTimeout(r, 200));
122
+ if (!captured) {
123
+ const timeoutPromise = new Promise(r => setTimeout(r, ${timeout} * 1000));
124
+ await Promise.race([capturePromise, timeoutPromise]);
123
125
  }
124
126
  } finally {
125
127
  // ── 5. Always restore originals ──
package/src/registry.ts CHANGED
@@ -34,6 +34,9 @@ export interface CliCommand {
34
34
  pipeline?: any[];
35
35
  timeoutSeconds?: number;
36
36
  source?: string;
37
+ /** Internal: lazy-loaded TS module support */
38
+ _lazy?: boolean;
39
+ _modulePath?: string;
37
40
  }
38
41
 
39
42
  export interface CliOptions {
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export interface IPage {
12
12
  click(ref: string): Promise<void>;
13
13
  typeText(ref: string, text: string): Promise<void>;
14
14
  pressKey(key: string): Promise<void>;
15
- wait(seconds: number): Promise<void>;
15
+ wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void>;
16
16
  tabs(): Promise<any>;
17
17
  closeTab(index?: number): Promise<void>;
18
18
  newTab(): Promise<void>;