nothing-browser 0.0.16 → 0.0.18

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,25 @@
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 ───────────────────────────────────────────────────────
279
-
280
- const piggy: PiggyBase = {
281
- launch: async (opts?: LaunchOptions): Promise<PiggyBase> => {
17
+ const piggy: any = {
18
+ // ── Local launch (socket) ─────────────────────────────────────────────────
19
+ launch: async (opts?: { mode?: TabMode; binary?: BinaryMode }) => {
282
20
  _tabMode = opts?.mode ?? "tab";
283
21
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
284
22
  await spawnBrowser(binaryMode);
285
- await new Promise<void>((r) => setTimeout(r, 500));
23
+ await new Promise(r => setTimeout(r, 500));
286
24
  _client = new PiggyClient();
287
25
  await _client.connect();
288
26
  setClient(_client);
@@ -290,72 +28,126 @@ const piggy: PiggyBase = {
290
28
  return piggy;
291
29
  },
292
30
 
293
- register: async (name: string, url: string, opts?: RegisterOptions): Promise<PiggyBase> => {
31
+ // ── Remote connect (HTTP) ─────────────────────────────────────────────────
32
+ connect: async (opts: { host: string; key: string }) => {
33
+ _tabMode = "tab";
34
+ _client = new PiggyClient({ host: opts.host, key: opts.key });
35
+ await _client.connect();
36
+ setClient(_client);
37
+ logger.info(`[piggy] connected (HTTP) → ${opts.host}`);
38
+ return piggy;
39
+ },
40
+
41
+ // ── Register ──────────────────────────────────────────────────────────────
42
+ register: async (
43
+ name: string,
44
+ url: string,
45
+ opts?: {
46
+ binary?: BinaryMode;
47
+ pool?: number;
48
+ }
49
+ ) => {
294
50
  if (!url?.trim()) throw new Error(`No URL for site "${name}"`);
295
51
  const binaryMode: BinaryMode = opts?.binary ?? "headless";
296
-
297
- let siteObj: SiteObject;
52
+ const poolSize = opts?.pool ?? 0;
298
53
 
299
54
  if (_tabMode === "tab") {
300
- 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}`);
55
+ if (!_client) throw new Error("No client. Call piggy.launch() or piggy.connect() first.");
56
+
57
+ if (poolSize > 1) {
58
+ const pool = new TabPool(_client, poolSize, url, name);
59
+ await pool.init();
60
+ const siteObj = createSiteObject(name, url, _client, "default", pool);
61
+ _sites[name] = siteObj;
62
+ piggy[name] = siteObj;
63
+ logger.success(`[${name}] registered with pool of ${poolSize} tabs`);
64
+ } else {
65
+ const tabId = await _client.newTab();
66
+ const siteObj = createSiteObject(name, url, _client, tabId);
67
+ _sites[name] = siteObj;
68
+ piggy[name] = siteObj;
69
+ logger.success(`[${name}] registered as tab ${tabId}`);
70
+ }
304
71
  } else {
305
72
  const socketName = `piggy_${name}`;
306
73
  await spawnBrowserOnSocket(socketName, binaryMode);
307
- await new Promise<void>((r) => setTimeout(r, 500));
74
+ await new Promise(r => setTimeout(r, 500));
308
75
  const c = new PiggyClient(socketName);
309
76
  await c.connect();
310
77
  _extraProcs.push({ socket: socketName, client: c });
311
- siteObj = createSiteObject(name, url, c, "default") as SiteObject;
78
+ const siteObj = createSiteObject(name, url, c, "default");
79
+ _sites[name] = siteObj;
80
+ piggy[name] = siteObj;
312
81
  logger.success(`[${name}] registered as process on "${socketName}"`);
313
82
  }
314
83
 
315
- _sites[name] = siteObj;
316
- (piggy as unknown as Record<string, unknown>)[name] = siteObj;
317
84
  return piggy;
318
85
  },
319
86
 
320
- actHuman: (enable: boolean): PiggyBase => {
87
+ // ── Global controls ───────────────────────────────────────────────────────
88
+ actHuman: (enable: boolean) => {
321
89
  setHumanMode(enable);
322
90
  logger.info(`[piggy] actHuman: ${enable}`);
323
91
  return piggy;
324
92
  },
325
93
 
326
- mode: (m: TabMode): PiggyBase => {
327
- _tabMode = m;
328
- return piggy;
329
- },
94
+ mode: (m: TabMode) => { _tabMode = m; return piggy; },
330
95
 
331
- expose: async (
332
- name: string,
333
- handler: (data: unknown) => Promise<unknown> | unknown,
334
- tabId = "default"
335
- ): Promise<PiggyBase> => {
336
- if (!_client) throw new Error("No client. Call piggy.launch() first.");
96
+ // ── Expose Function ───────────────────────────────────────────────────────
97
+ expose: async (name: string, handler: (data: any) => Promise<any> | any, tabId = "default") => {
98
+ if (!_client) throw new Error("No client. Call piggy.launch() or piggy.connect() first.");
337
99
  await _client.exposeFunction(name, handler, tabId);
338
100
  logger.success(`[piggy] exposed global function: ${name}`);
339
101
  return piggy;
340
102
  },
341
103
 
342
- unexpose: async (name: string, tabId = "default"): Promise<PiggyBase> => {
343
- if (!_client) throw new Error("No client. Call piggy.launch() first.");
104
+ unexpose: async (name: string, tabId = "default") => {
105
+ if (!_client) throw new Error("No client. Call piggy.launch() or piggy.connect() first.");
344
106
  await _client.unexposeFunction(name, tabId);
345
107
  logger.info(`[piggy] unexposed function: ${name}`);
346
108
  return piggy;
347
109
  },
348
110
 
349
- serve: (port: number, opts?: ServeOptions): Promise<Elysia> =>
350
- startServer(port, opts?.hostname),
111
+ // ── Proxy ─────────────────────────────────────────────────────────────────
112
+ proxy: {
113
+ load: (path: string) => { if (!_client) throw new Error("No client"); return _client.proxyLoad(path); },
114
+ fetch: (url: string) => { if (!_client) throw new Error("No client"); return _client.proxyFetch(url); },
115
+ ovpn: (path: string) => { if (!_client) throw new Error("No client"); return _client.proxyOvpn(path); },
116
+ set: (opts: Parameters<PiggyClient["proxySet"]>[0]) => { if (!_client) throw new Error("No client"); return _client.proxySet(opts); },
117
+ test: () => { if (!_client) throw new Error("No client"); return _client.proxyTest(); },
118
+ testStop: () => { if (!_client) throw new Error("No client"); return _client.proxyTestStop(); },
119
+ next: () => { if (!_client) throw new Error("No client"); return _client.proxyNext(); },
120
+ disable: () => { if (!_client) throw new Error("No client"); return _client.proxyDisable(); },
121
+ enable: () => { if (!_client) throw new Error("No client"); return _client.proxyEnable(); },
122
+ current: () => { if (!_client) throw new Error("No client"); return _client.proxyCurrent(); },
123
+ stats: () => { if (!_client) throw new Error("No client"); return _client.proxyStats(); },
124
+ list: (limit?: number) => { if (!_client) throw new Error("No client"); return _client.proxyList(limit); },
125
+ rotation: (mode: "none" | "timed" | "perrequest", interval?: number) => { if (!_client) throw new Error("No client"); return _client.proxyRotation(mode, interval); },
126
+ config: (opts: { skipDead?: boolean; autoCheck?: boolean }) => { if (!_client) throw new Error("No client"); return _client.proxyConfig(opts); },
127
+ save: (path: string, filter?: "alive" | "dead" | "all") => { if (!_client) throw new Error("No client"); return _client.proxySave(path, filter); },
128
+ on: (event: string, handler: (data: any) => void) => { if (!_client) throw new Error("No client"); return _client.onProxyEvent(event, handler); },
129
+ },
130
+
131
+ // ── Elysia server ─────────────────────────────────────────────────────────
132
+ serve: (
133
+ port: number,
134
+ opts?: {
135
+ hostname?: string;
136
+ title?: string;
137
+ version?: string;
138
+ description?: string;
139
+ path?: string;
140
+ }
141
+ ) => startServer(port, opts?.hostname, opts),
351
142
 
352
143
  stopServer,
353
144
 
354
- routes: (): RouteSummary[] =>
355
- Array.from(routeRegistry.entries()).map(([key, cfg]): RouteSummary => {
145
+ // ── Route listing ─────────────────────────────────────────────────────────
146
+ routes: () =>
147
+ Array.from(routeRegistry.entries()).map(([key, cfg]) => {
356
148
  const [site] = key.split(":");
357
149
  return {
358
- site: site ?? key,
150
+ site,
359
151
  method: cfg.method,
360
152
  path: `/${site}${cfg.path}`,
361
153
  ttl: cfg.ttl,
@@ -363,31 +155,24 @@ const piggy: PiggyBase = {
363
155
  };
364
156
  }),
365
157
 
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
- ),
158
+ // ── Multi-site ────────────────────────────────────────────────────────────
159
+ all: (sites: SiteObject[]) =>
160
+ new Proxy({} as any, {
161
+ get: (_, method: string) =>
162
+ (...args: any[]) => Promise.all(sites.map((s: any) => s[method]?.(...args))),
375
163
  }),
376
164
 
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]]));
165
+ diff: (sites: SiteObject[]) =>
166
+ new Proxy({} as any, {
167
+ get: (_, method: string) =>
168
+ async (...args: any[]) => {
169
+ const results = await Promise.all(sites.map((s: any) => s[method]?.(...args)));
170
+ return Object.fromEntries(sites.map((s: any, i) => [s._name ?? i, results[i]]));
387
171
  },
388
172
  }),
389
173
 
390
- close: async (opts?: { force?: boolean }): Promise<void> => {
174
+ // ── Shutdown ──────────────────────────────────────────────────────────────
175
+ close: async (opts?: { force?: boolean }) => {
391
176
  stopServer();
392
177
  if (opts?.force) {
393
178
  for (const { client: c } of _extraProcs) c.disconnect();
@@ -397,7 +182,7 @@ const piggy: PiggyBase = {
397
182
  keepAliveSites.clear();
398
183
  } else {
399
184
  for (const [name, site] of Object.entries(_sites)) {
400
- if (!keepAliveSites.has(name)) await site.close?.();
185
+ if (!keepAliveSites.has(name)) await (site as any).close?.();
401
186
  }
402
187
  if (keepAliveSites.size === 0) {
403
188
  for (const { client: c } of _extraProcs) c.disconnect();
@@ -415,5 +200,18 @@ const piggy: PiggyBase = {
415
200
  logger,
416
201
  };
417
202
 
203
+ // ── usePiggy ──────────────────────────────────────────────────────────────────
204
+ // Typed accessor — call AFTER register() so sites exist on piggy.
205
+ // const { amazon, ebay } = usePiggy<"amazon" | "ebay">()
206
+
207
+ type TypedPiggy<Sites extends string> = typeof piggy & {
208
+ [K in Sites]: SiteObject;
209
+ };
210
+
211
+ export function usePiggy<Sites extends string>(): TypedPiggy<Sites> {
212
+ return piggy as TypedPiggy<Sites>;
213
+ }
214
+
215
+ export type { SiteObject };
418
216
  export default piggy;
419
217
  export { piggy };