nothing-browser 0.0.16 → 0.0.17

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.
package/piggy.ts CHANGED
@@ -2,287 +2,26 @@
2
2
  import { detectBinary, type BinaryMode } from "./piggy/launch/detect";
3
3
  import { spawnBrowser, killBrowser, spawnBrowserOnSocket } from "./piggy/launch/spawn";
4
4
  import { PiggyClient } from "./piggy/client";
5
- import { setClient, setHumanMode, createSiteObject } from "./piggy/register";
5
+ import { setClient, setHumanMode, createSiteObject, type SiteObject } from "./piggy/register";
6
6
  import { routeRegistry, keepAliveSites, startServer, stopServer } from "./piggy/server";
7
+ import { TabPool } from "./piggy/pool";
7
8
  import logger from "./piggy/logger";
8
- import type { Logger } from "ernest-logger";
9
- import type { Elysia } from "elysia";
10
- import type { RouteHandler, BeforeMiddleware, RouteConfig } from "./piggy/server";
11
9
 
12
- // ── Core types ───────────────────────────────────────────────────────────────
13
-
14
- export type TabMode = "tab" | "process";
15
-
16
- /** Full API surface of a registered site. */
17
- export interface SiteObject {
18
- readonly _name: string;
19
- readonly _tabId: string;
20
-
21
- // ── Navigation ─────────────────────────────────────────────────────────────
22
- navigate(url?: string, opts?: { retries?: number }): Promise<void>;
23
- reload(): Promise<void>;
24
- goBack(): Promise<void>;
25
- goForward(): Promise<void>;
26
- waitForNavigation(): Promise<void>;
27
- title(): Promise<string>;
28
- /** Returns the last known URL (synchronous — does NOT hit the browser). */
29
- url(): string;
30
- content(): Promise<string>;
31
-
32
- // ── Timing ────────────────────────────────────────────────────────────────
33
- wait(ms: number): Promise<void>;
34
- waitForSelector(selector: string, timeout?: number): Promise<void>;
35
- waitForVisible(selector: string, timeout?: number): Promise<void>;
36
- waitForResponse(pattern: string, timeout?: number): Promise<void>;
37
-
38
- // ── Init script ───────────────────────────────────────────────────────────
39
- addInitScript(js: string | (() => void)): Promise<SiteObject>;
40
-
41
- // ── Event emitter ─────────────────────────────────────────────────────────
42
- on(event: "navigate", handler: (url: string) => void): () => void;
43
- on(event: string, handler: (data: unknown) => void): () => void;
44
- off(event: string, handler: (data: unknown) => void): void;
45
-
46
- // ── Interactions ──────────────────────────────────────────────────────────
47
- click(selector: string, opts?: { retries?: number; timeout?: number }): Promise<boolean>;
48
- doubleClick(selector: string): Promise<boolean>;
49
- hover(selector: string): Promise<boolean>;
50
- type(
51
- selector: string,
52
- text: string,
53
- opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }
54
- ): Promise<boolean>;
55
- select(selector: string, value: string): Promise<boolean>;
56
- evaluate<T = unknown>(js: string | ((...args: unknown[]) => T), ...args: unknown[]): Promise<T>;
57
-
58
- keyboard: {
59
- press(key: string): Promise<boolean>;
60
- combo(combo: string): Promise<boolean>;
61
- };
62
- mouse: {
63
- move(x: number, y: number): Promise<boolean>;
64
- drag(from: { x: number; y: number }, to: { x: number; y: number }): Promise<boolean>;
65
- };
66
- scroll: {
67
- to(selector: string): Promise<boolean>;
68
- by(px: number): Promise<boolean>;
69
- };
70
-
71
- // ── Fetch ─────────────────────────────────────────────────────────────────
72
- fetchText(selector: string): Promise<string | null>;
73
- fetchLinks(selector: string): Promise<string[]>;
74
- fetchImages(selector: string): Promise<string[]>;
75
- search: {
76
- css(query: string): Promise<unknown>;
77
- id(query: string): Promise<unknown>;
78
- };
79
-
80
- // ── Screenshot / PDF ──────────────────────────────────────────────────────
81
- screenshot(filePath?: string): Promise<string>;
82
- pdf(filePath?: string): Promise<string>;
83
- blockImages(): Promise<void>;
84
- unblockImages(): Promise<void>;
85
-
86
- // ── Cookies ───────────────────────────────────────────────────────────────
87
- cookies: {
88
- set(name: string, value: string, domain: string, path?: string): Promise<void>;
89
- get(name: string): Promise<unknown>;
90
- delete(name: string): Promise<void>;
91
- list(): Promise<unknown[]>;
92
- };
93
-
94
- // ── Interception ──────────────────────────────────────────────────────────
95
- intercept: {
96
- block(pattern: string): Promise<void>;
97
- redirect(pattern: string, redirectUrl: string): Promise<void>;
98
- headers(pattern: string, headers: Record<string, string>): Promise<void>;
99
- respond(
100
- pattern: string,
101
- handlerOrResponse:
102
- | { status?: number; contentType?: string; body: string }
103
- | ((req: { url: string; method: string }) => {
104
- status?: number;
105
- contentType?: string;
106
- body: string;
107
- })
108
- ): Promise<SiteObject>;
109
- modifyResponse(
110
- pattern: string,
111
- handler: (response: {
112
- body: string;
113
- status: number;
114
- headers: Record<string, string>;
115
- }) => Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
116
- ): Promise<SiteObject>;
117
- clear(): Promise<void>;
118
- };
119
-
120
- // ── Network capture ───────────────────────────────────────────────────────
121
- capture: {
122
- start(): Promise<void>;
123
- stop(): Promise<void>;
124
- requests(): Promise<unknown[]>;
125
- ws(): Promise<unknown[]>;
126
- cookies(): Promise<unknown[]>;
127
- storage(): Promise<unknown>;
128
- clear(): Promise<void>;
129
- };
130
-
131
- // ── Session ───────────────────────────────────────────────────────────────
132
- session: {
133
- export(): Promise<unknown>;
134
- import(data: unknown): Promise<void>;
135
- };
136
-
137
- // ── Expose / unexpose ─────────────────────────────────────────────────────
138
- exposeFunction(
139
- fnName: string,
140
- handler: (data: unknown) => Promise<unknown> | unknown
141
- ): Promise<SiteObject>;
142
- unexposeFunction(fnName: string): Promise<SiteObject>;
143
- clearExposedFunctions(): Promise<SiteObject>;
144
- exposeAndInject(
145
- fnName: string,
146
- handler: (data: unknown) => Promise<unknown> | unknown,
147
- injectionJs: string | ((fnName: string) => string)
148
- ): Promise<SiteObject>;
149
-
150
- // ── Elysia route registration ─────────────────────────────────────────────
151
- api(
152
- path: string,
153
- handler: RouteHandler,
154
- opts?: {
155
- ttl?: number;
156
- before?: BeforeMiddleware[];
157
- method?: "GET" | "POST" | "PUT" | "DELETE";
158
- }
159
- ): SiteObject;
160
-
161
- noclose(): SiteObject;
162
- close(): Promise<void>;
163
- }
164
-
165
- // ── Route summary ─────────────────────────────────────────────────────────────
166
-
167
- export interface RouteSummary {
168
- site: string;
169
- method: RouteConfig["method"];
170
- path: string;
171
- ttl: number;
172
- middlewareCount: number;
173
- }
174
-
175
- // ── Multi-site proxy helpers ──────────────────────────────────────────────────
176
-
177
- export type AllProxy = {
178
- [K in keyof SiteObject]: SiteObject[K] extends (...args: infer A) => Promise<infer R>
179
- ? (...args: A) => Promise<R[]>
180
- : SiteObject[K] extends (...args: infer A) => infer R
181
- ? (...args: A) => Promise<R[]>
182
- : never;
183
- };
184
-
185
- export type DiffProxy = {
186
- [K in keyof SiteObject]: SiteObject[K] extends (...args: infer A) => Promise<infer R>
187
- ? (...args: A) => Promise<Record<string, R>>
188
- : SiteObject[K] extends (...args: infer A) => infer R
189
- ? (...args: A) => Promise<Record<string, R>>
190
- : never;
191
- };
192
-
193
- // ── Option bags ───────────────────────────────────────────────────────────────
194
-
195
- export interface LaunchOptions {
196
- mode?: TabMode;
197
- binary?: BinaryMode;
198
- }
199
-
200
- export interface RegisterOptions {
201
- binary?: BinaryMode;
202
- }
203
-
204
- export interface ServeOptions {
205
- hostname?: string;
206
- }
207
-
208
- // ── PiggyBase — named methods, no index signature ─────────────────────────────
209
-
210
- export interface PiggyBase {
211
- launch(opts?: LaunchOptions): Promise<PiggyBase>;
212
- register(name: string, url: string, opts?: RegisterOptions): Promise<PiggyBase>;
213
- actHuman(enable: boolean): PiggyBase;
214
- mode(m: TabMode): PiggyBase;
215
- expose(
216
- name: string,
217
- handler: (data: unknown) => Promise<unknown> | unknown,
218
- tabId?: string
219
- ): Promise<PiggyBase>;
220
- unexpose(name: string, tabId?: string): Promise<PiggyBase>;
221
- serve(port: number, opts?: ServeOptions): Promise<Elysia>;
222
- stopServer(): void;
223
- routes(): RouteSummary[];
224
- all(sites: SiteObject[]): AllProxy;
225
- diff(sites: SiteObject[]): DiffProxy;
226
- close(opts?: { force?: boolean }): Promise<void>;
227
- detect(mode?: BinaryMode): string | null;
228
- logger: Logger;
229
- }
230
-
231
- // ── PiggyWithSites<S> — PiggyBase + typed site keys ──────────────────────────
232
-
233
- export type PiggyWithSites<S extends string> = PiggyBase & {
234
- [K in S]: SiteObject;
235
- };
236
-
237
- // ── usePiggy — takes the piggy import, returns it typed ──────────────────────
238
- //
239
- // The variable is ALWAYS called `piggy`. No rename. No new variable.
240
- // Just pass the import in, get it back fully typed.
241
- //
242
- // import piggy, { usePiggy } from 'nothing-browser';
243
- // usePiggy<"amazon" | "walmart" | "ebay">(piggy);
244
- //
245
- // await piggy.launch({ mode: "tab" });
246
- // await piggy.register("amazon", "https://www.amazon.com/");
247
- // await piggy.register("walmart", "https://www.walmart.com/");
248
- // await piggy.register("ebay", "https://www.ebay.com/");
249
- //
250
- // await piggy.amazon.navigate(); // ✅ full autocomplete
251
- // await piggy.walmart.click("..."); // ✅
252
- // await piggy.ebay.screenshot(); // ✅
253
- //
254
- // How it works: TypeScript narrows the type of `piggy` at the call site
255
- // via the returned typed reference. Assign back to `piggy` with `let`:
256
- //
257
- // import piggyRaw, { usePiggy } from 'nothing-browser';
258
- // const piggy = usePiggy<"amazon" | "walmart" | "ebay">(piggyRaw);
259
- //
260
- // OR use the reassignment pattern with `let` for maximum cleanliness:
261
- //
262
- // import { piggy as _piggy, usePiggy } from 'nothing-browser';
263
- // const piggy = usePiggy<"amazon" | "walmart" | "ebay">(_piggy);
264
- //
265
- // Either way: `piggy` is the word in your code, always.
266
-
267
- export function usePiggy<S extends string>(): PiggyWithSites<S> {
268
- return piggy as unknown as PiggyWithSites<S>;
269
- }
270
-
271
- // ── Module-private state ──────────────────────────────────────────────────────
10
+ type TabMode = "tab" | "process";
272
11
 
273
12
  let _client: PiggyClient | null = null;
274
13
  let _tabMode: TabMode = "tab";
275
14
  const _extraProcs: { socket: string; client: PiggyClient }[] = [];
276
- const _sites: Record<string, SiteObject> = {};
15
+ const _sites: Record<string, SiteObject> = [];
277
16
 
278
- // ── The piggy singleton ───────────────────────────────────────────────────────
17
+ const piggy: any = {
18
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
279
19
 
280
- const piggy: PiggyBase = {
281
- launch: async (opts?: LaunchOptions): Promise<PiggyBase> => {
20
+ launch: async (opts?: { mode?: TabMode; binary?: BinaryMode }) => {
282
21
  _tabMode = opts?.mode ?? "tab";
283
22
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
284
23
  await spawnBrowser(binaryMode);
285
- await new Promise<void>((r) => setTimeout(r, 500));
24
+ await new Promise(r => setTimeout(r, 500));
286
25
  _client = new PiggyClient();
287
26
  await _client.connect();
288
27
  setClient(_client);
@@ -290,72 +29,104 @@ const piggy: PiggyBase = {
290
29
  return piggy;
291
30
  },
292
31
 
293
- register: async (name: string, url: string, opts?: RegisterOptions): Promise<PiggyBase> => {
32
+ register: async (
33
+ name: string,
34
+ url: string,
35
+ opts?: {
36
+ binary?: BinaryMode;
37
+ pool?: number; // number of tabs to pool — enables concurrent requests
38
+ }
39
+ ) => {
294
40
  if (!url?.trim()) throw new Error(`No URL for site "${name}"`);
295
41
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
296
-
297
- let siteObj: SiteObject;
42
+ const poolSize = opts?.pool ?? 0;
298
43
 
299
44
  if (_tabMode === "tab") {
300
45
  if (!_client) throw new Error("No client. Call piggy.launch() first.");
301
- const tabId = await _client.newTab();
302
- siteObj = createSiteObject(name, url, _client, tabId) as SiteObject;
303
- logger.success(`[${name}] registered as tab ${tabId}`);
46
+
47
+ if (poolSize > 1) {
48
+ // Pool mode create N tabs, wrap in TabPool
49
+ const pool = new TabPool(_client, poolSize, url, name);
50
+ await pool.init();
51
+
52
+ // Use first tab as the "default" tabId for event subscriptions
53
+ const firstTabId = "default";
54
+ const siteObj = createSiteObject(name, url, _client, firstTabId, pool);
55
+ _sites[name] = siteObj;
56
+ piggy[name] = siteObj;
57
+ logger.success(`[${name}] registered with pool of ${poolSize} tabs`);
58
+ } else {
59
+ // Single tab mode
60
+ const tabId = await _client.newTab();
61
+ const siteObj = createSiteObject(name, url, _client, tabId);
62
+ _sites[name] = siteObj;
63
+ piggy[name] = siteObj;
64
+ logger.success(`[${name}] registered as tab ${tabId}`);
65
+ }
304
66
  } else {
305
67
  const socketName = `piggy_${name}`;
306
68
  await spawnBrowserOnSocket(socketName, binaryMode);
307
- await new Promise<void>((r) => setTimeout(r, 500));
69
+ await new Promise(r => setTimeout(r, 500));
308
70
  const c = new PiggyClient(socketName);
309
71
  await c.connect();
310
72
  _extraProcs.push({ socket: socketName, client: c });
311
- siteObj = createSiteObject(name, url, c, "default") as SiteObject;
73
+ const siteObj = createSiteObject(name, url, c, "default");
74
+ _sites[name] = siteObj;
75
+ piggy[name] = siteObj;
312
76
  logger.success(`[${name}] registered as process on "${socketName}"`);
313
77
  }
314
78
 
315
- _sites[name] = siteObj;
316
- (piggy as unknown as Record<string, unknown>)[name] = siteObj;
317
79
  return piggy;
318
80
  },
319
81
 
320
- actHuman: (enable: boolean): PiggyBase => {
82
+ // ── Global controls ───────────────────────────────────────────────────────
83
+
84
+ actHuman: (enable: boolean) => {
321
85
  setHumanMode(enable);
322
86
  logger.info(`[piggy] actHuman: ${enable}`);
323
87
  return piggy;
324
88
  },
325
89
 
326
- mode: (m: TabMode): PiggyBase => {
327
- _tabMode = m;
328
- return piggy;
329
- },
90
+ mode: (m: TabMode) => { _tabMode = m; return piggy; },
330
91
 
331
- expose: async (
332
- name: string,
333
- handler: (data: unknown) => Promise<unknown> | unknown,
334
- tabId = "default"
335
- ): Promise<PiggyBase> => {
92
+ // ── Expose Function ───────────────────────────────────────────────────────
93
+
94
+ expose: async (name: string, handler: (data: any) => Promise<any> | any, tabId = "default") => {
336
95
  if (!_client) throw new Error("No client. Call piggy.launch() first.");
337
96
  await _client.exposeFunction(name, handler, tabId);
338
97
  logger.success(`[piggy] exposed global function: ${name}`);
339
98
  return piggy;
340
99
  },
341
100
 
342
- unexpose: async (name: string, tabId = "default"): Promise<PiggyBase> => {
101
+ unexpose: async (name: string, tabId = "default") => {
343
102
  if (!_client) throw new Error("No client. Call piggy.launch() first.");
344
103
  await _client.unexposeFunction(name, tabId);
345
104
  logger.info(`[piggy] unexposed function: ${name}`);
346
105
  return piggy;
347
106
  },
348
107
 
349
- serve: (port: number, opts?: ServeOptions): Promise<Elysia> =>
350
- startServer(port, opts?.hostname),
108
+ // ── Elysia server ─────────────────────────────────────────────────────────
109
+
110
+ serve: (
111
+ port: number,
112
+ opts?: {
113
+ hostname?: string;
114
+ title?: string;
115
+ version?: string;
116
+ description?: string;
117
+ path?: string;
118
+ }
119
+ ) => startServer(port, opts?.hostname, opts),
351
120
 
352
121
  stopServer,
353
122
 
354
- routes: (): RouteSummary[] =>
355
- Array.from(routeRegistry.entries()).map(([key, cfg]): RouteSummary => {
123
+ // ── Route listing ─────────────────────────────────────────────────────────
124
+
125
+ routes: () =>
126
+ Array.from(routeRegistry.entries()).map(([key, cfg]) => {
356
127
  const [site] = key.split(":");
357
128
  return {
358
- site: site ?? key,
129
+ site,
359
130
  method: cfg.method,
360
131
  path: `/${site}${cfg.path}`,
361
132
  ttl: cfg.ttl,
@@ -363,31 +134,26 @@ const piggy: PiggyBase = {
363
134
  };
364
135
  }),
365
136
 
366
- all: (sites: SiteObject[]): AllProxy =>
367
- new Proxy({} as AllProxy, {
368
- get: (_target, method: string) =>
369
- (...args: unknown[]) =>
370
- Promise.all(
371
- sites.map((s) =>
372
- (s as unknown as Record<string, (...a: unknown[]) => unknown>)[method]?.(...args)
373
- )
374
- ),
137
+ // ── Multi-site ────────────────────────────────────────────────────────────
138
+
139
+ all: (sites: SiteObject[]) =>
140
+ new Proxy({} as any, {
141
+ get: (_, method: string) =>
142
+ (...args: any[]) => Promise.all(sites.map((s: any) => s[method]?.(...args))),
375
143
  }),
376
144
 
377
- diff: (sites: SiteObject[]): DiffProxy =>
378
- new Proxy({} as DiffProxy, {
379
- get: (_target, method: string) =>
380
- async (...args: unknown[]) => {
381
- const results = await Promise.all(
382
- sites.map((s) =>
383
- (s as unknown as Record<string, (...a: unknown[]) => unknown>)[method]?.(...args)
384
- )
385
- );
386
- return Object.fromEntries(sites.map((s, i) => [s._name ?? i, results[i]]));
145
+ diff: (sites: SiteObject[]) =>
146
+ new Proxy({} as any, {
147
+ get: (_, method: string) =>
148
+ async (...args: any[]) => {
149
+ const results = await Promise.all(sites.map((s: any) => s[method]?.(...args)));
150
+ return Object.fromEntries(sites.map((s: any, i) => [s._name ?? i, results[i]]));
387
151
  },
388
152
  }),
389
153
 
390
- close: async (opts?: { force?: boolean }): Promise<void> => {
154
+ // ── Shutdown ──────────────────────────────────────────────────────────────
155
+
156
+ close: async (opts?: { force?: boolean }) => {
391
157
  stopServer();
392
158
  if (opts?.force) {
393
159
  for (const { client: c } of _extraProcs) c.disconnect();
@@ -397,7 +163,7 @@ const piggy: PiggyBase = {
397
163
  keepAliveSites.clear();
398
164
  } else {
399
165
  for (const [name, site] of Object.entries(_sites)) {
400
- if (!keepAliveSites.has(name)) await site.close?.();
166
+ if (!keepAliveSites.has(name)) await (site as any).close?.();
401
167
  }
402
168
  if (keepAliveSites.size === 0) {
403
169
  for (const { client: c } of _extraProcs) c.disconnect();
@@ -415,5 +181,18 @@ const piggy: PiggyBase = {
415
181
  logger,
416
182
  };
417
183
 
184
+ // ── usePiggy ──────────────────────────────────────────────────────────────────
185
+ // Typed accessor — call AFTER register() so sites exist on piggy.
186
+ // const { amazon, ebay } = usePiggy<"amazon" | "ebay">()
187
+
188
+ type TypedPiggy<Sites extends string> = typeof piggy & {
189
+ [K in Sites]: SiteObject;
190
+ };
191
+
192
+ export function usePiggy<Sites extends string>(): TypedPiggy<Sites> {
193
+ return piggy as TypedPiggy<Sites>;
194
+ }
195
+
196
+ export type { SiteObject };
418
197
  export default piggy;
419
198
  export { piggy };